Exemplo n.º 1
0
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}
Exemplo n.º 2
0
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()
Exemplo n.º 3
0
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
Exemplo n.º 4
0
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)
Exemplo n.º 5
0
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())
Exemplo n.º 6
0
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()
Exemplo n.º 7
0
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))
Exemplo n.º 8
0
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())