Skip to content

Finding

sereto.finding

FindingGroup dataclass

Represents a finding group.

Attributes:

Name Type Description
name str

The name of the finding group.

explicit_risk Risk | None

Risk to be used for the group. Overrides the calculated risks from sub-findings.

sub_findings list[SubFinding]

A list of sub-findings in the group.

_target_locators list[LocatorModel]

A list of locators used to find the target.

_finding_group_locators list[LocatorModel]

A list of locators defined on the finding group.

Source code in sereto/finding.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
@dataclass
class FindingGroup:
    """Represents a finding group.

    Attributes:
        name: The name of the finding group.
        explicit_risk: Risk to be used for the group. Overrides the calculated risks from sub-findings.
        sub_findings: A list of sub-findings in the group.
        _target_locators: A list of locators used to find the target.
        _finding_group_locators: A list of locators defined on the finding group.
    """

    name: str
    explicit_risk: Risk | None
    sub_findings: list[SubFinding]
    _target_locators: list[LocatorModel]
    _finding_group_locators: list[LocatorModel]

    @classmethod
    @validate_call
    def load(
        cls,
        name: str,
        group_desc: FindingGroupModel,
        findings_dir: DirectoryPath,
        target_locators: list[LocatorModel],
        templates: DirectoryPath,
    ) -> Self:
        """Load a finding group.

        Args:
            name: The name of the finding group.
            group_desc: The description of the finding group.
            findings_dir: The path to the findings directory.
            target_locators: The locators used to find the target.
            templates: The path to the templates directory.

        Returns:
            The loaded finding group object.
        """
        sub_findings = [
            SubFinding.load_from(path=findings_dir / f"{name}.md.j2", templates=templates)
            for name in group_desc.findings
        ]

        return cls(
            name=name,
            explicit_risk=group_desc.risk,
            sub_findings=sub_findings,
            _target_locators=target_locators,
            _finding_group_locators=group_desc.locators,
        )

    def dumps_toml(self) -> str:
        """Dump the finding group to a TOML string."""
        lines = [f'["{self.name}"]']
        if self.explicit_risk is not None:
            lines.append(f'risk = "{self.explicit_risk.value}"')
        if len(self.sub_findings) == 1:
            lines.append(f'findings = ["{self.sub_findings[0].uname}"]')
        else:
            lines.append("findings = [")
            for sf in self.sub_findings:
                lines.append(f'    "{sf.uname}",')
            lines.append("]")
        return "\n".join(lines)

    @property
    def risk(self) -> Risk:
        """Get the finding group risk.

        Returns:
            The explicit risk if set, otherwise the highest risk from the sub-findings.
        """
        if self.explicit_risk is not None:
            return self.explicit_risk
        return max([sf.risk for sf in self.sub_findings], key=lambda r: r.to_int())

    @property
    def locators(self) -> list[LocatorModel]:
        """Return a de-duplicated list of locators for the finding group.

        Precedence (first non-empty wins):
        1. Explicit locators defined on the finding group
        2. All locators gathered from sub-findings
        3. Locators inherited from the target
        """

        def _unique(seq: list[LocatorModel]) -> list[LocatorModel]:
            """Preserve order while removing duplicates, ignoring 'description'."""
            seen: set[tuple[Any, Any]] = set()
            result: list[LocatorModel] = []
            for loc in seq:
                key = (loc.type, loc.value)
                if key not in seen:
                    seen.add(key)
                    result.append(loc)
            return result

        # 1. Explicit locators on the group
        if self._finding_group_locators:
            return _unique(self._finding_group_locators)

        # 2. Locators from sub-findings
        sub_locators = _unique([loc for sf in self.sub_findings for loc in sf.locators])
        if sub_locators:
            return sub_locators

        # 3. Fallback to target locators
        return _unique(self._target_locators)

    @validate_call
    def filter_locators(self, type: str | Iterable[str]) -> list[LocatorModel]:
        """Filter locators by type.

        Args:
            type: The type of locators to filter by. Can be a single type or an iterable of types.

        Returns:
            A list of locators of the specified type.
        """
        type = [type] if isinstance(type, str) else list(type)
        return [loc for loc in self.locators if loc.type in type]

    @property
    @validate_call
    def uname(self) -> str:
        """Unique name of the finding group."""
        return lower_alphanum(f"finding_group_{self.name}")

locators property

Return a de-duplicated list of locators for the finding group.

Precedence (first non-empty wins): 1. Explicit locators defined on the finding group 2. All locators gathered from sub-findings 3. Locators inherited from the target

risk property

Get the finding group risk.

Returns:

Type Description
Risk

The explicit risk if set, otherwise the highest risk from the sub-findings.

uname property

Unique name of the finding group.

dumps_toml()

Dump the finding group to a TOML string.

Source code in sereto/finding.py
185
186
187
188
189
190
191
192
193
194
195
196
197
def dumps_toml(self) -> str:
    """Dump the finding group to a TOML string."""
    lines = [f'["{self.name}"]']
    if self.explicit_risk is not None:
        lines.append(f'risk = "{self.explicit_risk.value}"')
    if len(self.sub_findings) == 1:
        lines.append(f'findings = ["{self.sub_findings[0].uname}"]')
    else:
        lines.append("findings = [")
        for sf in self.sub_findings:
            lines.append(f'    "{sf.uname}",')
        lines.append("]")
    return "\n".join(lines)

filter_locators(type)

Filter locators by type.

Parameters:

Name Type Description Default
type str | Iterable[str]

The type of locators to filter by. Can be a single type or an iterable of types.

required

Returns:

Type Description
list[LocatorModel]

A list of locators of the specified type.

Source code in sereto/finding.py
243
244
245
246
247
248
249
250
251
252
253
254
@validate_call
def filter_locators(self, type: str | Iterable[str]) -> list[LocatorModel]:
    """Filter locators by type.

    Args:
        type: The type of locators to filter by. Can be a single type or an iterable of types.

    Returns:
        A list of locators of the specified type.
    """
    type = [type] if isinstance(type, str) else list(type)
    return [loc for loc in self.locators if loc.type in type]

load(name, group_desc, findings_dir, target_locators, templates) classmethod

Load a finding group.

Parameters:

Name Type Description Default
name str

The name of the finding group.

required
group_desc FindingGroupModel

The description of the finding group.

required
findings_dir DirectoryPath

The path to the findings directory.

required
target_locators list[LocatorModel]

The locators used to find the target.

required
templates DirectoryPath

The path to the templates directory.

required

Returns:

Type Description
Self

The loaded finding group object.

Source code in sereto/finding.py
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
181
182
183
@classmethod
@validate_call
def load(
    cls,
    name: str,
    group_desc: FindingGroupModel,
    findings_dir: DirectoryPath,
    target_locators: list[LocatorModel],
    templates: DirectoryPath,
) -> Self:
    """Load a finding group.

    Args:
        name: The name of the finding group.
        group_desc: The description of the finding group.
        findings_dir: The path to the findings directory.
        target_locators: The locators used to find the target.
        templates: The path to the templates directory.

    Returns:
        The loaded finding group object.
    """
    sub_findings = [
        SubFinding.load_from(path=findings_dir / f"{name}.md.j2", templates=templates)
        for name in group_desc.findings
    ]

    return cls(
        name=name,
        explicit_risk=group_desc.risk,
        sub_findings=sub_findings,
        _target_locators=target_locators,
        _finding_group_locators=group_desc.locators,
    )

Findings dataclass

Represents a collection of all finding groups inside a target.

Attributes:

Name Type Description
groups list[FindingGroup]

A list of finding groups.

target_dir FilePath

The path to the target directory containing the findings.

target_locators list[LocatorModel]

A list of locators used to find the target.

Source code in sereto/finding.py
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
@dataclass
class Findings:
    """Represents a collection of all finding groups inside a target.

    Attributes:
        groups: A list of finding groups.
        target_dir: The path to the target directory containing the findings.
        target_locators: A list of locators used to find the target.
    """

    groups: list[FindingGroup]
    target_dir: FilePath
    target_locators: list[LocatorModel]

    @classmethod
    @validate_call
    def load_from(
        cls, target_dir: DirectoryPath, target_locators: list[LocatorModel], templates: DirectoryPath
    ) -> Self:
        """Load findings belonging to the same target.

        Args:
            target_dir: The path to the target directory.
            target_locators: The locators used to find the target.
            templates: The path to the templates directory.

        Returns:
            The loaded findings object.
        """
        config = FindingsConfigModel.load_from(target_dir / "findings.toml")

        groups = [
            FindingGroup.load(
                name=name,
                group_desc=group,
                findings_dir=target_dir / "findings",
                target_locators=target_locators,
                templates=templates,
            )
            for name, group in config.items()
        ]

        # ensure group names are unique
        unique_names = [g.uname for g in groups]
        if len(unique_names) != len(set(unique_names)):
            raise SeretoValueError("finding group unique names must be unique")

        return cls(groups=groups, target_dir=target_dir, target_locators=target_locators)

    def get_path(self, category: str, name: str) -> FilePath:
        """Get the path to a sub-finding by category and name.

        Args:
            category: The category of the sub-finding.
            name: The name of the sub-finding.

        Returns:
            The path to the sub-finding file.
        """
        return self.findings_dir / f"{category.lower()}_{name}.md.j2"

    @validate_call
    def add_from_template(
        self,
        templates: DirectoryPath,
        template_path: FilePath,
        category: str,
        name: str | None = None,
        risk: Risk | None = None,
        variables: dict[str, Any] | None = None,
        overwrite: bool = False,
    ) -> None:
        """Add a sub-finding from a template, creating a new finding group.

        Args:
            templates: Path to the templates directory.
            template_path: Path to the sub-finding template.
            category: Category of the sub-finding.
            name: Name of the sub-finding group. Defaults to template name.
            risk: Risk of the sub-finding. Defaults to template risk.
            variables: Variables for the sub-finding template.
            overwrite: If True, overwrite existing sub-finding; otherwise, create with random suffix.
        """
        variables = variables or {}

        # Load template metadata and content
        template_metadata = FindingTemplateFrontmatterModel.load_from(template_path)
        template_name = template_path.name.removesuffix(".md.j2")
        _, content = frontmatter.parse(template_path.read_text(encoding="utf-8"))

        # Determine sub-finding path
        sub_finding_path = self.get_path(category=category, name=template_name)
        suffix = None

        if sub_finding_path.is_file():
            if overwrite:
                sub_finding_path.unlink()
            else:
                # Try to generate a unique filename with random suffix
                for _ in range(5):
                    suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=5))
                    sub_finding_path = self.get_path(category=category, name=f"{template_name}_{suffix}")
                    if not sub_finding_path.is_file():
                        break
                else:
                    raise SeretoPathError(
                        f"sub-finding already exists and could not generate a unique filename: {sub_finding_path}"
                    )

        # Prepare sub-finding frontmatter
        sub_finding_metadata = SubFindingFrontmatterModel(
            name=template_metadata.name,
            risk=template_metadata.risk,
            category=category,
            variables=variables,
            template_path=str(template_path.relative_to(templates)),
        )

        # Write sub-finding file
        sub_finding_path.write_text(f"+++\n{sub_finding_metadata.dumps_toml()}+++\n\n{content}", encoding="utf-8")

        # If overwriting, nothing else to do
        if overwrite:
            return

        # Load the created sub-finding
        sub_finding = SubFinding.load_from(path=sub_finding_path, templates=templates)

        # Determine group name
        group_name = name or sub_finding.name
        if suffix:
            group_name = f"{group_name} {suffix}"

        # Create finding group
        group = FindingGroup(
            name=group_name,
            explicit_risk=risk,
            sub_findings=[sub_finding],
            _target_locators=self.target_locators,
            _finding_group_locators=[],
        )

        # Append to findings.toml
        with self.config_file.open("a", encoding="utf-8") as f:
            f.write(f"\n{group.dumps_toml()}\n")

        # Add to loaded groups
        self.groups.append(group)

    @validate_call
    def select_group(self, selector: int | str | None = None) -> FindingGroup:
        """Select a finding group by index or name.

        Args:
            selector: The index or name of the finding group to select.

        Returns:
            The selected finding group.
        """
        # only single finding group present
        if selector is None:
            if len(self.groups) != 1:
                raise SeretoValueError(
                    f"cannot select finding group; no selector provided and there are {len(self.groups)} finding "
                    "groups present"
                )
            return self.groups[0]

        # by index
        if isinstance(selector, int) or selector.isnumeric():
            ix = selector - 1 if isinstance(selector, int) else int(selector) - 1
            if not (0 <= ix <= len(self.groups) - 1):
                raise SeretoValueError("finding group index out of range")
            return self.groups[ix]

        # by unique name
        matching_groups = [g for g in self.groups if g.uname == selector]
        if len(matching_groups) != 1:
            raise SeretoValueError(f"finding group with uname {selector!r} not found")
        return matching_groups[0]

    @property
    def config_file(self) -> Path:
        """Get the path to the findings.toml configuration file"""
        return self.target_dir / "findings.toml"

    @property
    def findings_dir(self) -> Path:
        """Get the path to the directory containing the findings"""
        return self.target_dir / "findings"

    @property
    def risks(self) -> Risks:
        """Get the summary of risks for the specified version."""
        return Risks(
            critical=len([g for g in self.groups if g.risk == Risk.critical]),
            high=len([g for g in self.groups if g.risk == Risk.high]),
            medium=len([g for g in self.groups if g.risk == Risk.medium]),
            low=len([g for g in self.groups if g.risk == Risk.low]),
            info=len([g for g in self.groups if g.risk == Risk.info]),
            closed=len([g for g in self.groups if g.risk == Risk.closed]),
        )

config_file property

Get the path to the findings.toml configuration file

findings_dir property

Get the path to the directory containing the findings

risks property

Get the summary of risks for the specified version.

add_from_template(templates, template_path, category, name=None, risk=None, variables=None, overwrite=False)

Add a sub-finding from a template, creating a new finding group.

Parameters:

Name Type Description Default
templates DirectoryPath

Path to the templates directory.

required
template_path FilePath

Path to the sub-finding template.

required
category str

Category of the sub-finding.

required
name str | None

Name of the sub-finding group. Defaults to template name.

None
risk Risk | None

Risk of the sub-finding. Defaults to template risk.

None
variables dict[str, Any] | None

Variables for the sub-finding template.

None
overwrite bool

If True, overwrite existing sub-finding; otherwise, create with random suffix.

False
Source code in sereto/finding.py
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
@validate_call
def add_from_template(
    self,
    templates: DirectoryPath,
    template_path: FilePath,
    category: str,
    name: str | None = None,
    risk: Risk | None = None,
    variables: dict[str, Any] | None = None,
    overwrite: bool = False,
) -> None:
    """Add a sub-finding from a template, creating a new finding group.

    Args:
        templates: Path to the templates directory.
        template_path: Path to the sub-finding template.
        category: Category of the sub-finding.
        name: Name of the sub-finding group. Defaults to template name.
        risk: Risk of the sub-finding. Defaults to template risk.
        variables: Variables for the sub-finding template.
        overwrite: If True, overwrite existing sub-finding; otherwise, create with random suffix.
    """
    variables = variables or {}

    # Load template metadata and content
    template_metadata = FindingTemplateFrontmatterModel.load_from(template_path)
    template_name = template_path.name.removesuffix(".md.j2")
    _, content = frontmatter.parse(template_path.read_text(encoding="utf-8"))

    # Determine sub-finding path
    sub_finding_path = self.get_path(category=category, name=template_name)
    suffix = None

    if sub_finding_path.is_file():
        if overwrite:
            sub_finding_path.unlink()
        else:
            # Try to generate a unique filename with random suffix
            for _ in range(5):
                suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=5))
                sub_finding_path = self.get_path(category=category, name=f"{template_name}_{suffix}")
                if not sub_finding_path.is_file():
                    break
            else:
                raise SeretoPathError(
                    f"sub-finding already exists and could not generate a unique filename: {sub_finding_path}"
                )

    # Prepare sub-finding frontmatter
    sub_finding_metadata = SubFindingFrontmatterModel(
        name=template_metadata.name,
        risk=template_metadata.risk,
        category=category,
        variables=variables,
        template_path=str(template_path.relative_to(templates)),
    )

    # Write sub-finding file
    sub_finding_path.write_text(f"+++\n{sub_finding_metadata.dumps_toml()}+++\n\n{content}", encoding="utf-8")

    # If overwriting, nothing else to do
    if overwrite:
        return

    # Load the created sub-finding
    sub_finding = SubFinding.load_from(path=sub_finding_path, templates=templates)

    # Determine group name
    group_name = name or sub_finding.name
    if suffix:
        group_name = f"{group_name} {suffix}"

    # Create finding group
    group = FindingGroup(
        name=group_name,
        explicit_risk=risk,
        sub_findings=[sub_finding],
        _target_locators=self.target_locators,
        _finding_group_locators=[],
    )

    # Append to findings.toml
    with self.config_file.open("a", encoding="utf-8") as f:
        f.write(f"\n{group.dumps_toml()}\n")

    # Add to loaded groups
    self.groups.append(group)

get_path(category, name)

Get the path to a sub-finding by category and name.

Parameters:

Name Type Description Default
category str

The category of the sub-finding.

required
name str

The name of the sub-finding.

required

Returns:

Type Description
FilePath

The path to the sub-finding file.

Source code in sereto/finding.py
312
313
314
315
316
317
318
319
320
321
322
def get_path(self, category: str, name: str) -> FilePath:
    """Get the path to a sub-finding by category and name.

    Args:
        category: The category of the sub-finding.
        name: The name of the sub-finding.

    Returns:
        The path to the sub-finding file.
    """
    return self.findings_dir / f"{category.lower()}_{name}.md.j2"

load_from(target_dir, target_locators, templates) classmethod

Load findings belonging to the same target.

Parameters:

Name Type Description Default
target_dir DirectoryPath

The path to the target directory.

required
target_locators list[LocatorModel]

The locators used to find the target.

required
templates DirectoryPath

The path to the templates directory.

required

Returns:

Type Description
Self

The loaded findings object.

Source code in sereto/finding.py
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
@classmethod
@validate_call
def load_from(
    cls, target_dir: DirectoryPath, target_locators: list[LocatorModel], templates: DirectoryPath
) -> Self:
    """Load findings belonging to the same target.

    Args:
        target_dir: The path to the target directory.
        target_locators: The locators used to find the target.
        templates: The path to the templates directory.

    Returns:
        The loaded findings object.
    """
    config = FindingsConfigModel.load_from(target_dir / "findings.toml")

    groups = [
        FindingGroup.load(
            name=name,
            group_desc=group,
            findings_dir=target_dir / "findings",
            target_locators=target_locators,
            templates=templates,
        )
        for name, group in config.items()
    ]

    # ensure group names are unique
    unique_names = [g.uname for g in groups]
    if len(unique_names) != len(set(unique_names)):
        raise SeretoValueError("finding group unique names must be unique")

    return cls(groups=groups, target_dir=target_dir, target_locators=target_locators)

select_group(selector=None)

Select a finding group by index or name.

Parameters:

Name Type Description Default
selector int | str | None

The index or name of the finding group to select.

None

Returns:

Type Description
FindingGroup

The selected finding group.

Source code in sereto/finding.py
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
@validate_call
def select_group(self, selector: int | str | None = None) -> FindingGroup:
    """Select a finding group by index or name.

    Args:
        selector: The index or name of the finding group to select.

    Returns:
        The selected finding group.
    """
    # only single finding group present
    if selector is None:
        if len(self.groups) != 1:
            raise SeretoValueError(
                f"cannot select finding group; no selector provided and there are {len(self.groups)} finding "
                "groups present"
            )
        return self.groups[0]

    # by index
    if isinstance(selector, int) or selector.isnumeric():
        ix = selector - 1 if isinstance(selector, int) else int(selector) - 1
        if not (0 <= ix <= len(self.groups) - 1):
            raise SeretoValueError("finding group index out of range")
        return self.groups[ix]

    # by unique name
    matching_groups = [g for g in self.groups if g.uname == selector]
    if len(matching_groups) != 1:
        raise SeretoValueError(f"finding group with uname {selector!r} not found")
    return matching_groups[0]

SubFinding dataclass

Source code in sereto/finding.py
 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
 63
 64
 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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
@dataclass
class SubFinding:
    name: str
    risk: Risk
    vars: dict[str, Any]
    path: FilePath
    template: FilePath | None = None
    locators: list[LocatorModel] = field(default_factory=list)

    @classmethod
    @validate_call
    def load_from(cls, path: FilePath, templates: DirectoryPath) -> Self:
        """Load a sub-finding from a file.

        Args:
            path: The path to the sub-finding file.
            templates: The path to the templates directory.

        Returns:
            The loaded sub-finding object.
        """
        frontmatter = SubFindingFrontmatterModel.load_from(path)

        return cls(
            name=frontmatter.name,
            risk=frontmatter.risk,
            vars=frontmatter.variables,
            path=path,
            template=(templates / frontmatter.template_path) if frontmatter.template_path else None,
            locators=frontmatter.locators,
        )

    @property
    def uname(self) -> str:
        """Unique name of the finding."""
        return self.path.name.removesuffix(".md.j2")

    @validate_call
    def filter_locators(self, type: str | Iterable[str]) -> list[LocatorModel]:
        """Filter locators by type.

        Args:
            type: The type of locators to filter by. Can be a single type or an iterable of types.

        Returns:
            A list of locators of the specified type.
        """
        type = [type] if isinstance(type, str) else list(type)
        return [loc for loc in self.locators if loc.type in type]

    @validate_call
    def validate_vars(self) -> None:
        """Validate the variables of the sub-finding against definition in the template.

        Works only if there is a template path defined, otherwise no validation is done.

        Raises:
            SeretoValueError: If the variables are not valid.
        """
        if self.template is None:
            # no template path, no validation
            return

        # read template frontmatter
        template_frontmatter = FindingTemplateFrontmatterModel.load_from(self.template)

        # report all errors at once
        error = ""

        for var in template_frontmatter.variables:
            # check if variable is defined
            if var.name not in self.vars:
                if var.required:
                    error += f"{var.name}: {var.type_annotation} = {var.description}\n"
                    error += f"  - missing required variable in finding '{self.name}'\n"
                else:
                    # TODO: logger
                    print(
                        f"{var.name}: {var.type_annotation} = {var.description}\n"
                        f"  - optional variable is not defined in finding '{self.name}'\n"
                    )
                continue

            # variable should be a list and is not
            if var.is_list and not isinstance(self.vars[var.name], list):
                error += f"{var.name}: {var.type_annotation} = {var.description}\n"
                error += f"  - variable must be a list in finding '{self.name}'\n"
                continue

            # variable should not be a list and is
            if not var.is_list and isinstance(self.vars[var.name], list):
                error += f"{var.name}: {var.type_annotation} = {var.description}\n"
                error += f"  - variable must not be a list in finding '{self.name}'\n"
                continue

        # report all errors at once
        if len(error) > 0:
            raise SeretoValueError(f"invalid variables in finding '{self.name}'\n{error}")

uname property

Unique name of the finding.

filter_locators(type)

Filter locators by type.

Parameters:

Name Type Description Default
type str | Iterable[str]

The type of locators to filter by. Can be a single type or an iterable of types.

required

Returns:

Type Description
list[LocatorModel]

A list of locators of the specified type.

Source code in sereto/finding.py
69
70
71
72
73
74
75
76
77
78
79
80
@validate_call
def filter_locators(self, type: str | Iterable[str]) -> list[LocatorModel]:
    """Filter locators by type.

    Args:
        type: The type of locators to filter by. Can be a single type or an iterable of types.

    Returns:
        A list of locators of the specified type.
    """
    type = [type] if isinstance(type, str) else list(type)
    return [loc for loc in self.locators if loc.type in type]

load_from(path, templates) classmethod

Load a sub-finding from a file.

Parameters:

Name Type Description Default
path FilePath

The path to the sub-finding file.

required
templates DirectoryPath

The path to the templates directory.

required

Returns:

Type Description
Self

The loaded sub-finding object.

Source code in sereto/finding.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@classmethod
@validate_call
def load_from(cls, path: FilePath, templates: DirectoryPath) -> Self:
    """Load a sub-finding from a file.

    Args:
        path: The path to the sub-finding file.
        templates: The path to the templates directory.

    Returns:
        The loaded sub-finding object.
    """
    frontmatter = SubFindingFrontmatterModel.load_from(path)

    return cls(
        name=frontmatter.name,
        risk=frontmatter.risk,
        vars=frontmatter.variables,
        path=path,
        template=(templates / frontmatter.template_path) if frontmatter.template_path else None,
        locators=frontmatter.locators,
    )

validate_vars()

Validate the variables of the sub-finding against definition in the template.

Works only if there is a template path defined, otherwise no validation is done.

Raises:

Type Description
SeretoValueError

If the variables are not valid.

Source code in sereto/finding.py
 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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
@validate_call
def validate_vars(self) -> None:
    """Validate the variables of the sub-finding against definition in the template.

    Works only if there is a template path defined, otherwise no validation is done.

    Raises:
        SeretoValueError: If the variables are not valid.
    """
    if self.template is None:
        # no template path, no validation
        return

    # read template frontmatter
    template_frontmatter = FindingTemplateFrontmatterModel.load_from(self.template)

    # report all errors at once
    error = ""

    for var in template_frontmatter.variables:
        # check if variable is defined
        if var.name not in self.vars:
            if var.required:
                error += f"{var.name}: {var.type_annotation} = {var.description}\n"
                error += f"  - missing required variable in finding '{self.name}'\n"
            else:
                # TODO: logger
                print(
                    f"{var.name}: {var.type_annotation} = {var.description}\n"
                    f"  - optional variable is not defined in finding '{self.name}'\n"
                )
            continue

        # variable should be a list and is not
        if var.is_list and not isinstance(self.vars[var.name], list):
            error += f"{var.name}: {var.type_annotation} = {var.description}\n"
            error += f"  - variable must be a list in finding '{self.name}'\n"
            continue

        # variable should not be a list and is
        if not var.is_list and isinstance(self.vars[var.name], list):
            error += f"{var.name}: {var.type_annotation} = {var.description}\n"
            error += f"  - variable must not be a list in finding '{self.name}'\n"
            continue

    # report all errors at once
    if len(error) > 0:
        raise SeretoValueError(f"invalid variables in finding '{self.name}'\n{error}")

render_finding_group_to_tex(config, project_path, target, target_ix, finding_group, finding_group_ix, version)

Render selected finding group (top-level document) to TeX format.

Source code in sereto/finding.py
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
def render_finding_group_to_tex(
    config: "Config",
    project_path: DirectoryPath,
    target: "Target",
    target_ix: int,
    finding_group: FindingGroup,
    finding_group_ix: int,
    version: ProjectVersion,
) -> str:
    """Render selected finding group (top-level document) to TeX format."""
    version_config = config.at_version(version=version)

    # Construct path to finding group template
    template = project_path / "layouts/finding_group.tex.j2"
    if not template.is_file():
        raise SeretoPathError(f"template not found: '{template}'")

    # Render Jinja2 template
    return render_jinja2(
        templates=[
            project_path / "layouts/generated",
            project_path / "layouts",
            project_path / "includes",
            project_path,
        ],
        file=template,
        vars={
            "finding_group": finding_group,
            "finding_group_index": finding_group_ix,
            "target": target,
            "target_index": target_ix,
            "c": version_config,
            "config": config,
            "version": version,
            "project_path": project_path,
        },
    )