Skip to content

Report

sereto.report

copy_skel(templates, dst, overwrite=False)

Copy the content of a templates skel directory to a destination directory.

A skel directory is a directory that contains a set of files and directories that can be used as a template for creating new projects. This function copies the contents of the skel directory located at the path specified by templates to the destination directory specified by dst.

Parameters:

Name Type Description Default
templates Path

The path to the directory containing the skel directory.

required
dst Path

The destination directory to copy the skel directory contents to.

required
overwrite bool

Whether to allow overwriting of existing files in the destination directory. If True, existing files will be overwritten. If False (default), a SeretoPathError will be raised if the destination directory already exists.

False

Raises:

Type Description
SeretoPathError

If the destination directory already exists and overwrite is False.

Source code in sereto/report.py
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
@validate_call
def copy_skel(templates: Path, dst: Path, overwrite: bool = False) -> None:
    """Copy the content of a templates `skel` directory to a destination directory.

    A `skel` directory is a directory that contains a set of files and directories that can be used as a template
    for creating new projects. This function copies the contents of the `skel` directory located at
    the path specified by `templates` to the destination directory specified by `dst`.

    Args:
        templates: The path to the directory containing the `skel` directory.
        dst: The destination directory to copy the `skel` directory contents to.
        overwrite: Whether to allow overwriting of existing files in the destination directory.
            If `True`, existing files will be overwritten. If `False` (default), a `SeretoPathError` will be raised
            if the destination directory already exists.

    Raises:
        SeretoPathError: If the destination directory already exists and `overwrite` is `False`.
    """
    skel_path: Path = templates / "skel"
    Console().log(f"Copying 'skel' directory: '{skel_path}' -> '{dst}'")

    for item in skel_path.iterdir():
        dst_item: Path = dst / (item.relative_to(skel_path))
        if not overwrite and dst_item.exists():
            raise SeretoPathError("Destination already exists")
        if item.is_file():
            Console().log(f" - copy file: '{item.relative_to(skel_path)}'")
            copy2(item, dst_item, follow_symlinks=False)
        if item.is_dir():
            Console().log(f" - copy dir: '{item.relative_to(skel_path)}'")
            copytree(item, dst_item, dirs_exist_ok=overwrite)

create_source_archive(settings)

Create a source archive for the report.

This function creates a source archive for the report by copying all the files not matching any ignore pattern in the report directory to a compressed archive file.

Parameters:

Name Type Description Default
settings Settings

Global settings.

required
Source code in sereto/report.py
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
145
146
147
148
149
150
151
152
153
154
def create_source_archive(settings: Settings) -> None:
    """Create a source archive for the report.

    This function creates a source archive for the report by copying all the files not matching any
    ignore pattern in the report directory to a compressed archive file.

    Args:
        settings: Global settings.
    """
    report_path = Report.get_path(dir_subtree=settings.reports_path)
    archive_path = report_path / "source.tgz"

    if not (seretoignore_path := report_path / ".seretoignore").is_file():
        Console().log(f"no '.seretoignore' file found: '{seretoignore_path}'")
        ignore_lines = []
    elif not evaluate_size_threshold(file=seretoignore_path, max_bytes=10_485_760, interactive=True):
        raise SeretoValueError("File '.seretoignore' size not within thresholds")
    else:
        with seretoignore_path.open("r") as seretoignore:
            ignore_lines = seretoignore.readlines()

    Console().log(f"creating source archive: '{archive_path}'")

    with tarfile.open(archive_path, "w:gz") as tar:
        for item in report_path.rglob("*"):
            relative_path = item.relative_to(report_path)

            if not item.is_file() or item.is_symlink():
                Console().log(f"- skipping directory or symlink: '{relative_path}'")
                continue

            if _is_ignored(str(relative_path), ignore_lines):
                Console().log(f"- skipping item: '{relative_path}'")
                continue

            Console().log(f"+ adding item: '{relative_path}'")
            tar.add(item, arcname=str(item.relative_to(report_path.parent)))

    encrypt_file(archive_path)

delete_source_archive(settings)

Delete the source archive.

Parameters:

Name Type Description Default
report

Report's representation.

required
settings Settings

Global settings.

required
Source code in sereto/report.py
157
158
159
160
161
162
163
164
165
166
167
168
169
def delete_source_archive(settings: Settings) -> None:
    """Delete the source archive.

    Args:
        report: Report's representation.
        settings: Global settings.
    """
    report_path = Report.get_path(dir_subtree=settings.reports_path)

    for archive_path in [report_path / "source.tgz", report_path / "source.sereto"]:
        if archive_path.is_file():
            archive_path.unlink()
            Console().log(f"deleted source archive: '{archive_path}'")

embed_source_archive(settings, version)

Embed the source archive in the report PDF.

Parameters:

Name Type Description Default
report

Report's representation.

required
settings Settings

Global settings.

required
Source code in sereto/report.py
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
def embed_source_archive(settings: Settings, version: ReportVersion) -> None:
    """Embed the source archive in the report PDF.

    Args:
        report: Report's representation.
        settings: Global settings.
    """
    report_path = Report.get_path(dir_subtree=settings.reports_path)
    encrypted_archive_path = report_path / "source.sereto"
    archive_path = encrypted_archive_path if encrypted_archive_path.is_file() else report_path / "source.tgz"
    report_pdf_path = report_path / f"report{version.path_suffix}.pdf"

    reader = PdfReader(report_pdf_path, strict=True)
    writer = PdfWriter()

    # Copy all pages from the reader to the writer
    for page in reader.pages:
        writer.add_page(page)

    # Embed the source archive
    with archive_path.open("rb") as f:
        writer.add_attachment(filename=archive_path.name, data=f.read())

    # Write the output PDF
    with report_pdf_path.open("wb") as output_pdf:
        writer.write(output_pdf)

extract_attachment_from(pdf, name)

Extracts an attachment from a given PDF file and writes it to a temporary file.

Parameters:

Name Type Description Default
pdf Path

The path to the PDF file from which to extract the attachment.

required
name str

The name of the attachment to extract.

required

Returns:

Type Description
Path

The path to the temporary file containing the extracted attachment.

Raises:

Type Description
SeretoValueError

If no or multiple attachments with the same name are found in the PDF.

Source code in sereto/report.py
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
@validate_call
def extract_attachment_from(pdf: Path, name: str) -> Path:
    """Extracts an attachment from a given PDF file and writes it to a temporary file.

    Args:
        pdf: The path to the PDF file from which to extract the attachment.
        name: The name of the attachment to extract.

    Returns:
        The path to the temporary file containing the extracted attachment.

    Raises:
        SeretoValueError: If no or multiple attachments with the same name are found in the PDF.
    """
    if not pdf.is_file():
        raise SeretoPathError(f"file not found: '{pdf}'")

    # Read the PDF file
    reader = PdfReader(pdf, strict=True)

    # Check if the attachment is present
    if name not in reader.attachments:
        Console().log(f"no '{name}' attachment found in '{pdf}'")
        Console().log(f"[blue]Manually inspect the file to make sure the attachment '{name}' is present")
        raise SeretoValueError(f"no '{name}' attachment found in '{pdf}'")

    # PDF attachment names are not unique; check if there is only one attachment with the expected name
    if len(reader.attachments[name]) != 1:
        Console().log(f"[yellow]only single '{name}' attachment should be present")
        Console().log("[blue]Manually extract the correct file and use `sereto decrypt` command instead")
        raise SeretoValueError(f"multiple '{name}' attachments found")

    # Extract the attachment's content
    content: bytes = reader.attachments[name][0]

    # Write the content to a temporary file
    output_file = Path(gettempdir()) / name
    output_file.write_bytes(content)

    return output_file

load_report(f)

Decorator which calls load_report_function and provides Report as the first argument

Source code in sereto/report.py
37
38
39
40
41
42
43
44
45
46
def load_report(f: Callable[..., R]) -> Callable[..., R]:
    """Decorator which calls `load_report_function` and provides Report as the first argument"""

    @wraps(f)
    def wrapper(settings: Settings, *args: P.args, **kwargs: P.kwargs) -> R:
        report = load_report_function(settings=settings)
        report.load_runtime_vars(settings=settings)
        return get_current_context().invoke(f, report, settings, *args, **kwargs)

    return wrapper

new_report(settings, report_id)

Generates a new report with the specified ID.

Parameters:

Name Type Description Default
settings Settings

Global settings.

required
report_id TypeReportId

The ID of the new report. This should be a string that uniquely identifies the report.

required

Raises:

Type Description
SeretoValueError

If a report with the specified ID already exists in the reports directory.

Source code in sereto/report.py
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
@validate_call
def new_report(
    settings: Settings,
    report_id: TypeReportId,
) -> None:
    """Generates a new report with the specified ID.

    Args:
        settings: Global settings.
        report_id: The ID of the new report. This should be a string that uniquely identifies the report.

    Raises:
        SeretoValueError: If a report with the specified ID already exists in the `reports` directory.
    """
    Console().log(f"Generating a new report with ID {report_id!r}")

    if (new_path := (settings.reports_path / report_id)).exists():
        raise SeretoValueError("report with specified ID already exists")
    else:
        new_path.mkdir()

    Console().print("[cyan]We will ask you a few questions to set up the new report.\n")

    report_name: str = Prompt.ask("Name of the report", console=Console())
    sereto_ver = importlib.metadata.version("sereto")

    cfg = Config(
        sereto_version=SeretoVersion.from_str(sereto_ver),
        id=report_id,
        name=report_name,
        report_version=ReportVersion.from_str("v1.0"),
    )

    Console().log("Copy report skeleton")
    copy_skel(templates=settings.templates_path, dst=new_path)

    config_path: Path = new_path / "config.json"
    Console().log(f"Writing the config '{config_path}'")
    with config_path.open("w", encoding="utf-8") as f:
        f.write(cfg.model_dump_json(indent=2))

render_report_j2(report, settings, version, convert_recipe=None)

Renders Jinja templates into TeX files.

This function processes Jinja templates for report, approach and scope in each target, and all relevant findings.

Parameters:

Name Type Description Default
report Report

Report's representation.

required
settings Settings

Global settings.

required
version ReportVersion

The version of the report which should be rendered.

required
convert_recipe str | None

Name which will be used to pick a recipe from Render configuration. If none is provided, the first recipe with a matching format is used.

None
Source code in sereto/report.py
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
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
@validate_call
def render_report_j2(
    report: Report,
    settings: Settings,
    version: ReportVersion,
    convert_recipe: str | None = None,
) -> None:
    """Renders Jinja templates into TeX files.

    This function processes Jinja templates for report, approach and scope in each target, and all relevant findings.

    Args:
        report: Report's representation.
        settings: Global settings.
        version: The version of the report which should be rendered.
        convert_recipe: Name which will be used to pick a recipe from Render configuration. If none is provided, the
            first recipe with a matching format is used.
    """
    cfg = report.config.at_version(version=version)
    report_path = Report.get_path(dir_subtree=settings.reports_path)

    for target in cfg.targets:
        # render_target_findings_j2(target=target, settings=settings, version=version, convert_recipe=convert_recipe)
        render_target_j2(
            target=target, report=report, settings=settings, version=version, convert_recipe=convert_recipe
        )

        for finding_group in target.findings_config.finding_groups:
            render_finding_group_j2(
                finding_group=finding_group, target=target, report=report, settings=settings, version=version
            )

    report_j2_path = report_path / f"report{version.path_suffix}.tex.j2"
    if not report_j2_path.is_file():
        raise SeretoPathError(f"template not found: '{report_j2_path}'")

    # make shallow dict - values remain objects on which we can call their methods in Jinja
    cfg_dict = {key: getattr(cfg, key) for key in cfg.model_dump()}
    report_generator = render_j2(
        templates=report_path,
        file=report_j2_path,
        vars={"version": version, "report_path": report_path, **cfg_dict},
    )

    with report_j2_path.with_suffix("").open("w", encoding="utf-8") as f:
        for chunk in report_generator:
            f.write(chunk)
        Console().log(f"rendered Jinja template: {report_j2_path.with_suffix('').relative_to(report_path)}")

report_create_missing(report, settings, version)

Creates missing target directories from config.

This function creates any missing target directories and populates them with content of the "skel" directory from templates.

Parameters:

Name Type Description Default
report Report

Report's representation.

required
settings Settings

Global settings.

required
version ReportVersion

The version of the report.

required
Source code in sereto/report.py
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
@validate_call
def report_create_missing(report: Report, settings: Settings, version: ReportVersion) -> None:
    """Creates missing target directories from config.

    This function creates any missing target directories and populates them with content of the "skel" directory from
    templates.

    Args:
        report: Report's representation.
        settings: Global settings.
        version: The version of the report.
    """
    cfg = report.config.at_version(version=version)

    for target in cfg.targets:
        assert target.path is not None
        category_templates = settings.templates_path / "categories" / target.category

        if not target.path.is_dir():
            Console().log(f"target directory not found, creating: '{target.path}'")
            target.path.mkdir()
            if (category_templates / "skel").is_dir():
                Console().log(f"""populating new target directory from: '{category_templates / "skel"}'""")
                copy_skel(templates=category_templates, dst=target.path)
            else:
                Console().log(f"no 'skel' directory found: {category_templates}'")

            create_findings_config(target=target, report=report, templates=category_templates / "findings")

        risks = get_risks(target=target, version=version)
        risks_plot(risks=risks, path=target.path / "risks.png")

        for finding_group in target.findings_config.finding_groups:
            finding_group_j2_path = target.path / "findings" / f"{finding_group.uname}.tex.j2"
            if not finding_group_j2_path.is_file():
                copy2(category_templates / "finding_group.tex.j2", finding_group_j2_path, follow_symlinks=False)