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 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 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 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 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 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 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') 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
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 '')
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),
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 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 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())
import os from sarna.core.config import config from sarna.model.base import Base, db __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, db.ForeignKey('user.id', onupdate="CASCADE", ondelete="CASCADE"), primary_key=True
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)