class Active(Base, db.Model): __table_args__ = (db.UniqueConstraint('assessment_id', 'name'), ) __serialization__ = [ AttributeConfiguration(name='name', csv_sequence=1, **supported_serialization), AttributeConfiguration(name='uris', **supported_serialization), ] id = db.Column(db.Integer, primary_key=True) assessment_id = db.Column(db.Integer, db.ForeignKey('assessment.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) assessment = db.relationship(Assessment, uselist=False) name = db.Column(db.String(128)) active_resources = db.relationship('AffectedResource', back_populates='active') @property def uris(self): for resource in self.active_resources: yield resource.uri
class AffectedResource(Base, db.Model): __table_args__ = (db.UniqueConstraint('active_id', 'route'), ) __serialization__ = [ AttributeConfiguration(name='uri', csv_sequence=1, **supported_serialization), ] id = db.Column(db.Integer, primary_key=True) active_id = db.Column(db.Integer, db.ForeignKey('active.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) active = db.relationship(Active, uselist=False, back_populates='active_resources') route = db.Column(db.String(256)) findings = db.relationship('Finding', secondary=finding_affected_resource) @property def uri(self): return "{}{}".format(self.active.name, self.route or '') def delete_last_reference(self): if len(self.findings) == 1: if len(self.active.active_resources ) == 1 and self.active.active_resources[0] is self: self.active.delete() else: self.delete()
class Client(Base, db.Model): __serialization__ = [ AttributeConfiguration(name='id', csv_sequence=1, **supported_serialization), AttributeConfiguration(name='short_name', **supported_serialization), AttributeConfiguration(name='long_name', **supported_serialization), ] id = db.Column(db.Integer, primary_key=True) short_name = db.Column(db.String(64), nullable=False) long_name = db.Column(db.String(128), nullable=False) assessments = db.relationship('Assessment', back_populates='client') templates = db.relationship('Template', secondary=client_template, back_populates='clients') creator_id = db.Column(db.Integer, db.ForeignKey('user.id', onupdate="CASCADE", ondelete="CASCADE"), nullable=False) creator = db.relationship("User", back_populates="created_clients", uselist=False) managers = db.relationship('User', secondary=client_management, back_populates='managed_clients') auditors = db.relationship('User', secondary=client_audit, back_populates='audited_clients')
class Image(Base, db.Model): name = db.Column(db.String(128), primary_key=True) assessment_id = db.Column( db.Integer, db.ForeignKey('assessment.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True ) assessment = db.relationship(Assessment, back_populates='images', uselist=False) label = db.Column(db.String())
class Solution(Base, db.Model): name = db.Column(db.String(32), primary_key=True) finding_template_id = db.Column( db.Integer, db.ForeignKey('finding_template.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True ) finding_template = db.relationship(FindingTemplate, back_populates='solutions', uselist=False) lang = db.Column(Enum(Language), nullable=False) text = db.Column(db.String(), nullable=False)
class Client(Base, db.Model): __serialization__ = [ AttributeConfiguration(name='id', csv_sequence=1, **supported_serialization), AttributeConfiguration(name='short_name', **supported_serialization), AttributeConfiguration(name='long_name', **supported_serialization), ] id = db.Column(db.Integer, primary_key=True) short_name = db.Column(db.String(64), nullable=False) long_name = db.Column(db.String(128), nullable=False) assessments = db.relationship('Assessment', back_populates='client') templates = db.relationship('Template', secondary=client_template, back_populates='clients') creator_id = db.Column(db.Integer, db.ForeignKey('user.id', onupdate="CASCADE", ondelete="CASCADE"), nullable=False) creator = db.relationship("User", back_populates="created_clients", uselist=False) managers = db.relationship('User', secondary=client_management, back_populates='managed_clients') auditors = db.relationship('User', secondary=client_audit, back_populates='audited_clients') finding_counter = db.Column(db.Integer, default=0, nullable=False) def generate_finding_counter(self) -> int: tx_commit = False while not tx_commit: self.finding_counter = Client.finding_counter + 1 db.session.add(self) try: db.session.commit() tx_commit = True except Exception as ex: pass return self.finding_counter def format_finding_code(self, finding) -> str: client_name_prefix = unidecode(self.short_name).replace(" ", "_").upper() return f"{client_name_prefix}_{finding.assessment.creation_date:%Y%m%d}_{finding.client_finding_id:06d}"
class Client(Base, db.Model): id = db.Column(db.Integer, primary_key=True) short_name = db.Column(db.String(64), nullable=False) long_name = db.Column(db.String(128), nullable=False) assessments = db.relationship('Assessment', back_populates='client') templates = db.relationship('Template', backref='client') creator_id = db.Column(db.Integer, db.ForeignKey('user.id', onupdate="CASCADE", ondelete="CASCADE"), nullable=False) creator = db.relationship("User", back_populates="created_clients", uselist=False) managers = db.relationship('User', secondary=client_management, back_populates='managed_clients') auditors = db.relationship('User', secondary=client_audit, back_populates='audited_clients') def template_path(self): return os.path.join(config.TEMPLATES_PATH, str(self.id))
class AffectedResource(Base, db.Model): __table_args__ = (db.UniqueConstraint('active_id', 'route'), ) id = db.Column(db.Integer, primary_key=True) active_id = db.Column(db.Integer, db.ForeignKey('active.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) active = db.relationship(Active, uselist=False, back_populates='active_resources') route = db.Column(db.String(256)) findings = db.relationship('Finding', secondary=finding_affected_resource) @property def uri(self): return "{}{}".format(self.active.name, self.route or '')
class FindingTemplateTranslation(Base, db.Model): lang = db.Column(Enum(Language), primary_key=True) finding_template_id = db.Column( db.Integer, db.ForeignKey('finding_template.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True ) finding_template = db.relationship(FindingTemplate, back_populates='translations', uselist=False) title = db.Column(db.String(128), nullable=False) definition = db.Column(db.String(), nullable=False) references = db.Column(db.String(), nullable=False) description = db.Column(db.String()) def check_references_urls(self): url_regex = r"\[.+\]\((.+)\)" refs_lines = self.references.splitlines() for i, ref_line in enumerate(refs_lines): match_url = re.search(url_regex, ref_line) if match_url: ref = match_url.group(0) url = match_url.group(1) try: req = requests.head(url, allow_redirects=True, timeout=config.BROKEN_REFS_REQ_TIMEOUT) req.raise_for_status() refs_lines[i] = ref_line.replace(config.BROKEN_REFS_TOKEN, "", 1) except: if config.BROKEN_REFS_TOKEN not in ref_line: refs_lines[i] = ref_line.replace(ref, f"{ref}{config.BROKEN_REFS_TOKEN}", 1) self.references = "\r\n".join(refs_lines) db.session.commit()
class Template(Base, db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(32), unique=True, nullable=False) description = db.Column(db.String(128), nullable=False) last_modified = db.Column(db.DateTime, default=lambda: datetime.now(), nullable=False) file = db.Column(db.String(128), nullable=False) clients = db.relationship('Client', secondary=client_template, back_populates='templates') @staticmethod def template_path(): return config.TEMPLATES_PATH """ Multi-Select Field helper methods """ @classmethod def get_choices(cls, *args): return list( (u, u.name) for u in Template.query.filter(*args).order_by(Template.name)) @classmethod def coerce(cls, item): if isinstance(item, Template): return item return cls.query.filter_by(name=item).first() def __str__(self): return self.name
class FindingTemplateTranslation(Base, db.Model): lang = db.Column(Enum(Language), primary_key=True) finding_template_id = db.Column( db.Integer, db.ForeignKey('finding_template.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True ) finding_template = db.relationship(FindingTemplate, back_populates='translations', uselist=False) title = db.Column(db.String(128), nullable=False) definition = db.Column(db.String(), nullable=False) references = db.Column(db.String(), nullable=False) description = db.Column(db.String())
class FindingTemplate(Base, db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), nullable=False) type = db.Column(Enum(FindingType), nullable=False) owasp_category = db.Column(Enum(OWASPCategory)) owasp_mobile_category = db.Column(Enum(OWASPMobileTop10Category)) owisam_category = db.Column(Enum(OWISAMCategory)) tech_risk = db.Column(Enum(Score), nullable=False) business_risk = db.Column(Enum(Score), nullable=False) exploitability = db.Column(Enum(Score), nullable=False) dissemination = db.Column(Enum(Score), nullable=False) solution_complexity = db.Column(Enum(Score), nullable=False) creator_id = db.Column(db.Integer, db.ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) creator = db.relationship('User', back_populates='created_findings', uselist=False) solutions = db.relationship('Solution', back_populates='finding_template') translations = db.relationship('FindingTemplateTranslation', back_populates='finding_template') @property def langs(self): return {t.lang for t in self.translations}
class User(Base, db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(128), unique=True) user_type = db.Column(Enum(UserType), default=UserType.auditor, nullable=False) source = db.Column(Enum(AuthSource), default=AuthSource.database, nullable=False) passwd = db.Column(db.String(128)) creation_date = db.Column(db.DateTime, default=lambda: datetime.now(), nullable=False) last_access = db.Column(db.DateTime) login_try = db.Column(db.SmallInteger, default=0, nullable=False) is_locked = db.Column(db.Boolean(), default=False, nullable=False) otp_enabled = db.Column(db.Boolean(), default=False, nullable=False) otp_seed = db.Column(db.String(16)) created_clients = db.relationship(Client, back_populates="creator") created_assessments = db.relationship(Assessment, back_populates="creator") managed_clients = db.relationship(Client, secondary=client_management, back_populates='managers') audited_clients = db.relationship(Client, secondary=client_audit, back_populates='auditors') audited_assessments = db.relationship(Assessment, secondary=assessment_audit, back_populates='auditors') approvals = db.relationship(Assessment, secondary=auditor_approval, back_populates='approvals') created_findings = db.relationship(FindingTemplate, back_populates='creator') def __str__(self): return self.username """ Properties """ @property def is_admin(self): return self.user_type in valid_admins @property def is_manager(self): return self.user_type in valid_managers @property def is_auditor(self): return self.user_type in valid_auditors @property def name(self): return self.username """ Assessment access methods """ def get_user_assessments(self): return Assessment.query.filter( (Assessment.creator == self) | (Assessment.client_id.in_( map(lambda x: x.id, self.managed_clients))) | (Assessment.client_id.in_( map(lambda x: x.id, self.audited_clients))) | (Assessment.auditors.any(User.id == self.id))).all() """ Check permissions methods """ def owns(self, obj): if isinstance(obj, Client): return obj in self.created_clients if isinstance(obj, Assessment): return obj in self.created_assessments elif isinstance(obj, FindingTemplate): return obj in self.created_findings return False def manages(self, obj): if self.owns(obj): return True if isinstance(obj, Client): return obj in self.managed_clients elif isinstance(obj, Assessment): return self.manages(obj.client) return False def audits(self, obj): if self.owns(obj) or self.manages(obj): return True if isinstance(obj, Client): return obj in self.audited_clients elif isinstance(obj, Assessment): return self.audits(obj.client) or obj in self.audited_assessments return False """ Authentication """ def login(self): self.last_access = datetime.now() self.login_try = 0 db.session.commit() login_user(self) def get_id(self): return self.username @property def is_authenticated(self): return True @property def is_anonymous(self): return False @property def is_active(self): return not self.is_locked def set_database_passwd(self, passwd): self.passwd = generate_password_hash(passwd) db.session.commit() def change_password(self, password, new_password, otp=None): try: self.source.engine.change_password(self, password, new_password, otp) except AuthException: return False return True def check_password(self, password): try: self.source.engine.verify_passwd(self, password) except AuthException: return False return True def generate_otp(self): if self.otp_enabled: raise ValueError('otp already set') self.otp_seed = pyotp.random_base32() db.session.commit() return pyotp.totp.TOTP(self.otp_seed).provisioning_uri( self.username, issuer_name="SARNA") def enable_otp(self, otp, password): if self.otp_enabled: raise ValueError('otp already set') otp_ok = self.check_otp(otp) if otp_ok and self.check_password(password): self.otp_enabled = True db.session.commit() return self.otp_enabled def disable_otp(self, otp, password): if not self.otp_enabled: raise ValueError('otp already disabled') otp_ok = self.check_otp(otp) if otp_ok and self.check_password(password): self.otp_enabled = False db.session.commit() return not self.otp_enabled def confirm_otp(self, otp): if not self.otp_enabled: raise ValueError('otp not set') return self.check_otp(otp) def check_otp(self, otp): totp = pyotp.TOTP(self.otp_seed) return totp.verify(otp) """ Multi-Select Field helper methods """ @classmethod def get_choices(cls, *args): return list((u, u.name) for u in User.query.filter(*args).order_by(User.username)) @classmethod def coerce(cls, item): if isinstance(item, User): return item return cls.query.filter_by(username=item).first()
class Assessment(Base, db.Model): id = db.Column(db.Integer, primary_key=True) uuid = db.Column(GUID, default=uuid4, unique=True, nullable=False) name = db.Column(db.String(64), nullable=False) platform = db.Column(db.String(64), nullable=False) lang = db.Column(Enum(Language), nullable=False) type = db.Column(Enum(AssessmentType), nullable=False) status = db.Column(Enum(AssessmentStatus), nullable=False) client_id = db.Column( db.Integer, db.ForeignKey('client.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False ) client = db.relationship(Client, back_populates="assessments", uselist=False) findings = db.relationship('Finding', back_populates='assessment') actives = db.relationship('Active', back_populates='assessment') images = db.relationship('Image', back_populates='assessment') creation_date = db.Column(db.DateTime, default=lambda: datetime.now(), nullable=False) start_date = db.Column(db.Date) end_date = db.Column(db.Date) estimated_hours = db.Column(db.Integer) effective_hours = db.Column(db.Integer) approvals = db.relationship('User', secondary=auditor_approval, back_populates='approvals') creator_id = db.Column(db.Integer, db.ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) creator = db.relationship("User", back_populates="created_assessments", uselist=False) auditors = db.relationship('User', secondary=assessment_audit, back_populates='audited_assessments') def _aggregate_score(self, field): counter = Counter( map( lambda x: getattr(x, field), self.findings ) ) return [ counter[Score.Info], counter[Score.Low], counter[Score.Medium], counter[Score.High], counter[Score.Critical] ] def aggregate_finding_status(self): counter = Counter( map( lambda x: x.status, self.findings ) ) return [ counter[FindingStatus.Pending], counter[FindingStatus.Reviewed], counter[FindingStatus.Confirmed], counter[FindingStatus.False_Positive], counter[FindingStatus.Other] ] def aggregate_technical_risk(self): return self._aggregate_score('tech_risk') def aggregate_business_risk(self): return self._aggregate_score('business_risk') def evidence_path(self): return os.path.join(config.EVIDENCES_PATH, str(self.uuid))
from datetime import datetime from uuid import uuid4 from sarna.core.config import config from sarna.model.base import Base, db from sarna.model.client import Client from sarna.model.enums import Language, AssessmentType, AssessmentStatus, Score, FindingStatus from sarna.model.sql_types import Enum, GUID __all__ = ['Assessment', 'Image', 'auditor_approval', 'assessment_audit'] auditor_approval = db.Table( 'auditor_approval', db.Column( 'approving_user_id', db.Integer, db.ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True ), db.Column( 'approved_assessment_id', db.Integer, db.ForeignKey('assessment.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True ), db.Column( 'approved_at', db.DateTime, default=lambda: datetime.now(), nullable=False ) )
class FindingTemplate(Base, db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), nullable=False) type = db.Column(Enum(FindingType), nullable=False) owasp_category = db.Column(Enum(OWASPCategory)) owasp_mobile_category = db.Column(Enum(OWASPMobileTop10Category)) owisam_category = db.Column(Enum(OWISAMCategory)) tech_risk = db.Column(Enum(Score), nullable=False) business_risk = db.Column(Enum(Score), nullable=False) exploitability = db.Column(Enum(Score), nullable=False) dissemination = db.Column(Enum(Score), nullable=False) solution_complexity = db.Column(Enum(Score), nullable=False) creator_id = db.Column(db.Integer, db.ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) creator = db.relationship('User', back_populates='created_findings', uselist=False) solutions = db.relationship('Solution', back_populates='finding_template') translations = db.relationship('FindingTemplateTranslation', back_populates='finding_template') cvss_v3_vector = db.Column(db.String(128)) cvss_v3_score = db.Column(db.Float, default=0.0, nullable=False) @property def langs(self): return {t.lang for t in self.translations} @property def cvss_v3_severity(self): score = self.cvss_v3_score if score == 0: return Score.Info elif 0 < score < 4: return Score.Low elif 4 <= score < 7: return Score.Medium elif 7 <= score < 9: return Score.High else: return Score.Critical
from sarna.model.assessment import Assessment from sarna.model.base import Base, db, supported_serialization from sarna.model.enums import Score, OWASPCategory, OWISAMCategory, FindingType, FindingStatus from sarna.model.enums.category import OWASPMobileTop10Category from sarna.model.finding_template import FindingTemplate, FindingTemplateTranslation from sarna.model.sql_types import Enum __all__ = [ 'Finding', 'Active', 'AffectedResource', 'finding_affected_resource' ] finding_affected_resource = db.Table( 'finding_affected_resource', db.Column('affected_resource_id', db.Integer, db.ForeignKey('affected_resource.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True), db.Column('finding_id', db.Integer, db.ForeignKey('finding.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True)) class Finding(Base, db.Model): __serialization__ = [ AttributeConfiguration(name='id', csv_sequence=1, **supported_serialization),
from datetime import datetime from sqlathanor import AttributeConfiguration from sarna.core.config import config from sarna.model.base import Base, db, supported_serialization __all__ = ['Client', 'Template', 'client_management', 'client_audit'] client_management = db.Table( 'client_management', db.Column('managed_client_id', db.Integer, db.ForeignKey('client.id', onupdate="CASCADE", ondelete="CASCADE"), primary_key=True), db.Column('manager_id', db.Integer, db.ForeignKey('user.id', onupdate="CASCADE", ondelete="CASCADE"), primary_key=True)) client_audit = db.Table( 'client_audit', db.Column('audited_client_id', db.Integer, db.ForeignKey('client.id', onupdate="CASCADE", ondelete="CASCADE"), primary_key=True), db.Column('auditor_id', db.Integer,
class Template(Base, db.Model): name = db.Column(db.String(32), primary_key=True) client_id = db.Column(db.Integer, db.ForeignKey('client.id', onupdate="CASCADE", ondelete="CASCADE"), primary_key=True) description = db.Column(db.String(128)) file = db.Column(db.String(128), nullable=False)
class Finding(Base, db.Model): __serialization__ = [ AttributeConfiguration(name='id', csv_sequence=1, **supported_serialization), AttributeConfiguration(name='name', **supported_serialization), AttributeConfiguration(name='title', **supported_serialization), AttributeConfiguration(name='type', **supported_serialization), AttributeConfiguration(name='status', **supported_serialization), AttributeConfiguration(name='owasp_category', **supported_serialization), AttributeConfiguration(name='owasp_mobile_category', **supported_serialization), AttributeConfiguration(name='owisam_category', **supported_serialization), AttributeConfiguration(name='description', **supported_serialization), AttributeConfiguration(name='solution', **supported_serialization), AttributeConfiguration(name='tech_risk', **supported_serialization), AttributeConfiguration(name='business_risk', **supported_serialization), AttributeConfiguration(name='exploitability', **supported_serialization), AttributeConfiguration(name='dissemination', **supported_serialization), AttributeConfiguration(name='solution_complexity', **supported_serialization), AttributeConfiguration(name='definition', **supported_serialization), AttributeConfiguration(name='references', **supported_serialization), AttributeConfiguration(name='affected_resources', **supported_serialization), AttributeConfiguration(name='cvss_v3_vector', **supported_serialization), AttributeConfiguration(name='cvss_v3_score', **supported_serialization), AttributeConfiguration(name='cvss_v3_severity', **supported_serialization), AttributeConfiguration(name='client_finding_id', **supported_serialization), AttributeConfiguration(name='client_finding_code', **supported_serialization), AttributeConfiguration(name='notes', **supported_serialization) ] id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), nullable=False) type = db.Column(Enum(FindingType), nullable=False) assessment_id = db.Column( db.Integer, db.ForeignKey('assessment.id', onupdate='CASCADE', ondelete='CASCADE')) assessment = db.relationship(Assessment, back_populates='findings', uselist=False) template_id = db.Column( db.Integer, db.ForeignKey('finding_template.id', onupdate='CASCADE', ondelete='SET NULL')) template = db.relationship(FindingTemplate, uselist=False) title = db.Column(db.String(128), nullable=False) status = db.Column(Enum(FindingStatus), nullable=False, default=FindingStatus.Pending) owasp_category = db.Column(Enum(OWASPCategory)) owasp_mobile_category = db.Column(Enum(OWASPMobileTop10Category)) owisam_category = db.Column(Enum(OWISAMCategory)) description = db.Column(db.String()) solution = db.Column(db.String()) tech_risk = db.Column(Enum(Score), nullable=False) business_risk = db.Column(Enum(Score), nullable=False) exploitability = db.Column(Enum(Score), nullable=False) dissemination = db.Column(Enum(Score), nullable=False) solution_complexity = db.Column(Enum(Score), nullable=False) definition = db.Column(db.String(), nullable=False) references = db.Column(db.String(), nullable=False) affected_resources = db.relationship('AffectedResource', secondary=finding_affected_resource) cvss_v3_vector = db.Column(db.String(128)) cvss_v3_score = db.Column(db.Float, default=0.0, nullable=False) client_finding_id = db.Column(db.Integer(), nullable=False) notes = db.Column(db.String()) def update_affected_resources(self, resources: Collection[AnyStr]): resource_uris = [] for resource in resources: resource = resource.strip() if not resource: continue # Skip empty lines resource_uri = URIReference.from_string(resource) if resource_uri.is_valid(require_scheme=True): _resource_ok = resource_uri.scheme.lower() in { 'http', 'https' } and resource_uri.authority is not None _resource_ok = _resource_ok or (resource_uri.scheme == 'urn' and resource_uri.path is not None) if _resource_ok: resource_uris.append(resource_uri) continue raise ValueError('Invalid formatted URI: "{}"'.format( resource.strip())) affected_resources_to_add = set() for resource in resource_uris: if resource.authority is not None: # URL active_name = "{}://{}".format(resource.scheme, resource.authority) resource_route = resource.path if not resource_route: resource_route = "/" if resource.query: resource_route += "?" + resource.query if resource.fragment: resource_route += "#" + resource.fragment elif resource.scheme == 'urn': # URN resource_name, *path = resource.path.split('/', 1) active_name = "{}:{}".format(resource.scheme, resource_name) resource_route = "/{}".format(path[0]) if path else None else: # TODO: this should never happen. Make some warning. continue active = Active.query.filter_by(assessment=self.assessment, name=active_name).first() if not active: active = Active(assessment=self.assessment, name=active_name) affected_resource = AffectedResource(active=active, route=resource_route) active.active_resources.append(affected_resource) db.session.add(active) db.session.add(affected_resource) else: affected_resource = AffectedResource.query.filter_by( active=active, route=resource_route).first() if not affected_resource: affected_resource = AffectedResource(active=active, route=resource_route) active.active_resources.append(affected_resource) db.session.add(affected_resource) affected_resources_to_add.add(affected_resource) db.session.commit() for affected_resource in self.affected_resources: if affected_resource not in affected_resources_to_add: affected_resource.delete_last_reference() for affected_resource in affected_resources_to_add: self.affected_resources.append(affected_resource) db.session.commit() @property def cvss_v3_severity(self): score = self.cvss_v3_score if score == 0: return Score.Info elif 0 < score < 4: return Score.Low elif 4 <= score < 7: return Score.Medium elif 7 <= score < 9: return Score.High else: return Score.Critical @property def client_finding_code(self): return self.assessment.client.format_finding_code(self) @classmethod def build_from_template(cls, template: FindingTemplate, assessment: Assessment): lang = assessment.lang client = assessment.client translation: FindingTemplateTranslation = None for t in template.translations: translation = t if t.lang == lang: break return Finding(name=template.name, type=template.type, tech_risk=template.tech_risk, business_risk=template.business_risk, exploitability=template.exploitability, dissemination=template.dissemination, solution_complexity=template.solution_complexity, owasp_category=template.owasp_category, owasp_mobile_category=template.owasp_mobile_category, owisam_category=template.owisam_category, template=template, title=translation.title, definition=translation.definition, references=translation.references, description=translation.description, assessment=assessment, cvss_v3_vector=template.cvss_v3_vector, cvss_v3_score=template.cvss_v3_score, client_finding_id=client.generate_finding_counter())