Skip to content

Finding

sereto.models.finding

FindingFrontmatterModel

Bases: SeretoBaseModel

Representation of the frontmatter of a sub-finding included in project.

Attributes:

Name Type Description
name str

The name of the sub-finding.

risk Risk

The risk level of the sub-finding.

category TypeCategoryName

From which category the sub-finding originates.

variables dict[str, Any]

A dictionary of variables used in the sub-finding.

Source code in sereto/models/finding.py
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
class FindingFrontmatterModel(SeretoBaseModel):
    """Representation of the frontmatter of a sub-finding included in project.

    Attributes:
        name: The name of the sub-finding.
        risk: The risk level of the sub-finding.
        category: From which category the sub-finding originates.
        variables: A dictionary of variables used in the sub-finding.
    """

    name: str
    risk: Risk
    category: TypeCategoryName
    variables: dict[str, Any] = {}

    @field_validator("risk", mode="before")
    @classmethod
    def convert_risk_type(cls, risk: Any) -> Risk:
        """Convert risk to Risk enum."""
        match risk:
            case Risk():
                return risk
            case str():
                return Risk(risk)
            case _:
                raise ValueError("unsupported type for Risk")

    def dumps_toml(self) -> str:
        """Dump the model to a TOML-formatted string."""
        output = dedent(f"""\
            name = "{self.name}"
            risk = "{self.risk.value}"
            category = "{self.category.lower()}"

            [variables]
        """)
        output += "\n".join(f'{k} = "{v}"' for k, v in self.variables.items())
        return output + "\n"

    @classmethod
    @validate_call
    def load_from(cls, path: Path) -> Self:
        """Load FindingFrontmatterModel from a file."""
        try:
            metadata, _ = frontmatter.parse(path.read_text(), encoding="utf-8")
            return cls.model_validate(metadata)
        except ValidationError as ex:
            raise SeretoValueError(f"invalid finding frontmatter in '{path}'") from ex

convert_risk_type(risk) classmethod

Convert risk to Risk enum.

Source code in sereto/models/finding.py
80
81
82
83
84
85
86
87
88
89
90
@field_validator("risk", mode="before")
@classmethod
def convert_risk_type(cls, risk: Any) -> Risk:
    """Convert risk to Risk enum."""
    match risk:
        case Risk():
            return risk
        case str():
            return Risk(risk)
        case _:
            raise ValueError("unsupported type for Risk")

dumps_toml()

Dump the model to a TOML-formatted string.

Source code in sereto/models/finding.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def dumps_toml(self) -> str:
    """Dump the model to a TOML-formatted string."""
    output = dedent(f"""\
        name = "{self.name}"
        risk = "{self.risk.value}"
        category = "{self.category.lower()}"

        [variables]
    """)
    output += "\n".join(f'{k} = "{v}"' for k, v in self.variables.items())
    return output + "\n"

load_from(path) classmethod

Load FindingFrontmatterModel from a file.

Source code in sereto/models/finding.py
104
105
106
107
108
109
110
111
112
@classmethod
@validate_call
def load_from(cls, path: Path) -> Self:
    """Load FindingFrontmatterModel from a file."""
    try:
        metadata, _ = frontmatter.parse(path.read_text(), encoding="utf-8")
        return cls.model_validate(metadata)
    except ValidationError as ex:
        raise SeretoValueError(f"invalid finding frontmatter in '{path}'") from ex

FindingGroupModel

Bases: SeretoBaseModel

Representation of a single finding group from findings.toml.

Attributes:

Name Type Description
risks

Explicit risks associated with the finding group for specific versions.

findings list[str]

The list of sub-findings in the format of their unique name to include in the report.

Source code in sereto/models/finding.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
class FindingGroupModel(SeretoBaseModel):
    """Representation of a single finding group from `findings.toml`.

    Attributes:
        risks: Explicit risks associated with the finding group for specific versions.
        findings: The list of sub-findings in the format of their unique name to include in the report.
    """

    risk: Risk | None = None
    findings: list[str] = Field(min_length=1)

    @field_validator("risk", mode="before")
    @classmethod
    def load_risk(cls, risk: Any) -> Risk | None:
        """Convert risk to correct type."""
        match risk:
            case Risk() | None:
                return risk
            case str():
                return Risk(risk)
            case _:
                raise ValueError("invalid risk type")

    @field_validator("findings", mode="after")
    @classmethod
    def unique_finding_names(cls, findings: list[str]) -> list[str]:
        """Ensure that all finding names are unique."""
        if len(findings) != len(set(findings)):
            raise ValueError("finding names must be unique")
        return findings

load_risk(risk) classmethod

Convert risk to correct type.

Source code in sereto/models/finding.py
126
127
128
129
130
131
132
133
134
135
136
@field_validator("risk", mode="before")
@classmethod
def load_risk(cls, risk: Any) -> Risk | None:
    """Convert risk to correct type."""
    match risk:
        case Risk() | None:
            return risk
        case str():
            return Risk(risk)
        case _:
            raise ValueError("invalid risk type")

unique_finding_names(findings) classmethod

Ensure that all finding names are unique.

Source code in sereto/models/finding.py
138
139
140
141
142
143
144
@field_validator("findings", mode="after")
@classmethod
def unique_finding_names(cls, findings: list[str]) -> list[str]:
    """Ensure that all finding names are unique."""
    if len(findings) != len(set(findings)):
        raise ValueError("finding names must be unique")
    return findings

FindingTemplateFrontmatterModel

Bases: SeretoBaseModel

Representation of the frontmatter of a finding template.

Attributes:

Name Type Description
name str

The name of the sub-finding.

risk Risk

The risk level of the sub-finding.

keywords list[str]

A list of keywords used to search for the sub-finding.

variables list[VarsMetadataModel]

A list of variables used in the sub-finding.

Source code in sereto/models/finding.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class FindingTemplateFrontmatterModel(SeretoBaseModel):
    """Representation of the frontmatter of a finding template.

    Attributes:
        name: The name of the sub-finding.
        risk: The risk level of the sub-finding.
        keywords: A list of keywords used to search for the sub-finding.
        variables: A list of variables used in the sub-finding.
    """

    name: str
    risk: Risk
    keywords: list[str] = []
    variables: list[VarsMetadataModel] = []

    @field_validator("risk", mode="before")
    @classmethod
    def convert_risk_type(cls, risk: Any) -> Risk:
        match risk:
            case Risk():
                return risk
            case str():
                return Risk(risk)
            case _:
                raise ValueError("unsupported type for Risk")

    @classmethod
    @validate_call
    def load_from(cls, path: Path) -> Self:
        """Load FindingTemplateFrontmatterModel from a file."""
        try:
            metadata, _ = frontmatter.parse(path.read_text(), encoding="utf-8")
            return cls.model_validate(metadata)
        except ValidationError as ex:
            raise SeretoValueError(f"invalid template frontmatter in '{path}'") from ex

load_from(path) classmethod

Load FindingTemplateFrontmatterModel from a file.

Source code in sereto/models/finding.py
54
55
56
57
58
59
60
61
62
@classmethod
@validate_call
def load_from(cls, path: Path) -> Self:
    """Load FindingTemplateFrontmatterModel from a file."""
    try:
        metadata, _ = frontmatter.parse(path.read_text(), encoding="utf-8")
        return cls.model_validate(metadata)
    except ValidationError as ex:
        raise SeretoValueError(f"invalid template frontmatter in '{path}'") from ex

FindingsConfigModel

Bases: RootModel[dict[str, FindingGroupModel]]

Model representing the included findings configuration.

The data itself is expected to be a dict where each key is the name of a finding group and the value is a FindingGroupModel.

Source code in sereto/models/finding.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
class FindingsConfigModel(RootModel[dict[str, FindingGroupModel]]):
    """Model representing the included findings configuration.

    The data itself is expected to be a dict where each key is
    the name of a finding group and the value is a FindingGroupModel.
    """

    root: dict[str, FindingGroupModel]

    @field_validator("root", mode="after")
    @classmethod
    def unique_findings(cls, findings: dict[str, FindingGroupModel]) -> dict[str, FindingGroupModel]:
        all_findings: list[str] = []
        for _, group in findings.items():
            all_findings.extend(group.findings)
        if len(all_findings) != len(set(all_findings)):
            raise ValueError("each sub-finding must be included only once")
        return findings

    @classmethod
    @validate_call
    def load_from(cls, file: FilePath) -> Self:
        try:
            with file.open(mode="rb") as f:
                return cls.model_validate(tomllib.load(f))
        except FileNotFoundError:
            raise SeretoPathError(f"file not found at '{file}'") from None
        except PermissionError:
            raise SeretoPathError(f"permission denied for '{file}'") from None
        except ValueError as e:
            raise SeretoValueError("invalid findings.toml") from e

    def items(self) -> ItemsView[str, FindingGroupModel]:
        return self.root.items()