Exemple #1
0
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
Exemple #2
0
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()
Exemple #3
0
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')
Exemple #4
0
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())
Exemple #5
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)
Exemple #6
0
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}"
Exemple #7
0
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))
Exemple #8
0
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 '')
Exemple #9
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()
Exemple #10
0
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
Exemple #11
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())
Exemple #12
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}
Exemple #13
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()
Exemple #14
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))
Exemple #15
0
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
    )
)
Exemple #16
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
Exemple #17
0
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),
Exemple #18
0
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,
Exemple #19
0
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)
Exemple #20
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())