Fix accidentially pushed folder structure
This commit is contained in:
1 parent
06593ac488
commit
a154dc927e
7 files changed
+284
-2
No files matched your search
+28
@@ -0,0 +1,28 @@
|
|||||||
|
# Bytecode and caches.
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Virtual environments.
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# Build and editable-install artefacts. `pyproject.toml` is tracked; what it produces is not.
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
|
log.txt
|
||||||
|
|
||||||
|
# Rendered output. The report is built from a scan, never committed.
|
||||||
|
*.pdf
|
||||||
|
!sample_report_template/*.pdf
|
||||||
|
|
||||||
|
# Local configuration and credentials the CLI reads.
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# macOS.
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# report_generator
|
||||||
|
|
||||||
|
Builds the PDF report of one stored scan, from the artifacts that scan put in the bucket.
|
||||||
|
|
||||||
|
## Installing
|
||||||
|
|
||||||
|
The package lives in `report_generator/`, beside the `pyproject.toml` that builds it. Install it
|
||||||
|
from the repository root — the directory holding this file, not the package directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install -e . # add [dev] for the test and lint tooling
|
||||||
|
```
|
||||||
|
|
||||||
|
Installing from inside `report_generator/` does not work: `packages.find` looks for the package as a
|
||||||
|
subdirectory, so it finds nothing, and the `report_generator` console script is then installed
|
||||||
|
pointing at a module that was never put on the path.
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
```
|
||||||
|
report_generator --scan_id <uuid> [--path <file-or-directory>] --config config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Without `--path` the report is written into `reports/`. The S3 secret comes from the environment:
|
||||||
|
`PERSISTENT_STORAGE_STORAGE_CONFIG_S3_SECRET_ACCESS_KEY` (and the database password from
|
||||||
|
`PERSISTENT_STORAGE_STORAGE_CONFIG_DATABASE_PASSWORD`). The name is not this tool's to choose: it is
|
||||||
|
derived from the config path `inject_secrets` fills in under — `persistent_storage.storage_config` —
|
||||||
|
by the one naming convention in `cc_common.config.env_var_name`, so it stays whatever the monorepo
|
||||||
|
calls it. A `.env` beside the `pyproject.toml` is read too. `--config` is resolved relative to the
|
||||||
|
working directory.
|
||||||
|
|
||||||
|
## How it is put together
|
||||||
|
|
||||||
|
Four steps, each its own module, so that the one that will change is the one that is separate:
|
||||||
|
|
||||||
|
| Module | Does |
|
||||||
|
| --- | --- |
|
||||||
|
| `report_generator/sources.py` | Reads the scan and its files out of the store. **The only module that knows which store.** |
|
||||||
|
| `report_generator/rendering.py` | Decides which stored file feeds which part of the report, and parses it. |
|
||||||
|
| `report_generator/context.py` | Maps the parsed artifacts onto the page, and writes `<xxx>` where they say nothing. |
|
||||||
|
| `report_generator/layout.py` | Draws `template/report.html.j2` through WeasyPrint. |
|
||||||
|
|
||||||
|
No parsing of its own. SARIF and the vulnerability report are read by the summarisers in
|
||||||
|
`cipherverifier.core.services`, which are written to be free of Django and of any store; the CVSS
|
||||||
|
vectors by `persistent_storage.utils.cvss_utils`.
|
||||||
|
|
||||||
|
### What it cannot fill in
|
||||||
|
|
||||||
|
A field the artifacts do not answer is written as `<xxx>`, never guessed or quietly dropped. Today
|
||||||
|
that is the report and artifact ids, the manufacturer, the analysed lines of code, the language
|
||||||
|
breakdown, and the tools and profile of each pipeline stage — the metadata records that a stage ran
|
||||||
|
and how long it took, not what it ran.
|
||||||
|
|
||||||
|
A measured zero survives: `0 filtered` is a reading, and a blank is a gap.
|
||||||
|
|
||||||
|
## Moving it to archon
|
||||||
|
|
||||||
|
`sources.collect()` is the whole of it. It returns files by name and the rows a report lists;
|
||||||
|
everything after that point works on bytes and cannot tell which store handed them over. Pointing it
|
||||||
|
at archon's tables is a change to that function and to nothing else.
|
||||||
|
|
||||||
|
## Its relationship to the monorepo
|
||||||
|
|
||||||
|
This repository is the split of `src/persistent_storage/report_generator/` out of the monorepo. The
|
||||||
|
package names nothing about itself — every import inside it is relative — so the split was a move
|
||||||
|
and not a rewrite: the sources sit unchanged under `report_generator/`, with `pyproject.toml` beside
|
||||||
|
them rather than inside them.
|
||||||
|
|
||||||
|
`report_generator/vendor/` supplies everything the package borrows from the monorepo — `cc_common`,
|
||||||
|
`persistent_storage.shared`, and the two summarisers. `vendor/__init__.py` puts the copies on the
|
||||||
|
path when the real packages are not installed, and stays out of the way when they are, so the same
|
||||||
|
sources run in both places. See `report_generator/vendor/README.md`.
|
||||||
|
|
||||||
|
Still to come across: `tests/persistent_storage/test_report_*.py` from the monorepo, with
|
||||||
|
`persistent_storage.report_generator` changed to `report_generator` in their imports. Until they
|
||||||
|
land, `testpaths = ["tests"]` in `pyproject.toml` points at a directory that does not exist yet.
|
||||||
|
|
||||||
|
On the monorepo side, `tests/persistent_storage/test_report_vendor.py` performs this same split into
|
||||||
|
a temporary directory on every run and renders a report from it, so the copies cannot quietly stop
|
||||||
|
being sufficient.
|
||||||
|
|
||||||
|
### Native dependencies
|
||||||
|
|
||||||
|
WeasyPrint needs Pango and cairo, which pip does not install:
|
||||||
|
|
||||||
|
```
|
||||||
|
brew install pango # macOS
|
||||||
|
apt-get install libpango-1.0-0 libpangoft2-1.0-0 # Debian/Ubuntu
|
||||||
|
```
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
# Packaging for report_generator as a repository of its own.
|
||||||
|
#
|
||||||
|
# The split out of the monorepo has happened: the package sits in `report_generator/` beside this
|
||||||
|
# file, which is what `packages.find` below looks for. In the monorepo the same sources live at
|
||||||
|
# `src/persistent_storage/report_generator/` and are built by the monorepo's own pyproject; this
|
||||||
|
# one is not read there.
|
||||||
|
#
|
||||||
|
# The dependency pins mirror the monorepo's, so that a tool split out of it does not silently start
|
||||||
|
# resolving different versions of the libraries it was tested against.
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=77.0"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "report-generator"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Builds the PDF report of a stored scan from the artifacts it produced."
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.14"
|
||||||
|
# PEP 639: an SPDX expression. "Proprietary" is not one; a private licence is spelled LicenseRef-*.
|
||||||
|
license = "LicenseRef-Proprietary"
|
||||||
|
authors = [{ name = "CipherCell" }]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
# The CLI, its configuration files and the properties models every command is described by.
|
||||||
|
"typer>=0.24.1",
|
||||||
|
"typer-config>=1.5.1",
|
||||||
|
"pydantic>=2.13.4",
|
||||||
|
"python-dotenv>=1.2.2",
|
||||||
|
"rich>=15.0.0",
|
||||||
|
"pyyaml>=6.0.3",
|
||||||
|
|
||||||
|
# The store: scan and artifact rows out of Postgres, artifact files out of the bucket.
|
||||||
|
"SQLAlchemy>=2.0.49",
|
||||||
|
"psycopg2-binary>=2.9.9",
|
||||||
|
"boto3>=1.43.0",
|
||||||
|
|
||||||
|
# The report: the template, and the engine that turns it into a PDF. WeasyPrint additionally
|
||||||
|
# needs the native Pango and cairo libraries, which pip does not install -- see the README.
|
||||||
|
"jinja2>=3.1.6",
|
||||||
|
"weasyprint>=69.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=9.0.0",
|
||||||
|
"mypy>=1.19.0",
|
||||||
|
"ruff>=0.15.16",
|
||||||
|
"mypy-boto3-s3>=1.43.31",
|
||||||
|
# Reads the rendered PDF back, so the tests can check what actually came out.
|
||||||
|
"pymupdf>=1.24.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
report_generator = "report_generator.__main__:run"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
# `vendor` is deliberately included: outside the monorepo it is what supplies cc_common,
|
||||||
|
# persistent_storage and the two summarisers, and `vendor/__init__.py` puts it on the path.
|
||||||
|
include = ["report_generator*"]
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
# Loaded at runtime by __file__, so it has to be installed beside the module: the Jinja template,
|
||||||
|
# the print stylesheet, the mark, and the font files the stylesheet references by relative URL.
|
||||||
|
"report_generator" = ["template/**/*"]
|
||||||
|
# The copies are data as far as packaging is concerned -- they are put on the path, not imported
|
||||||
|
# as submodules of this package.
|
||||||
|
"report_generator.vendor" = ["**/*.py", "**/py.typed"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 120
|
||||||
|
target-version = "py314"
|
||||||
|
# The copies are checked where they are written, not where they are copied to. Left in, every class
|
||||||
|
# they define is read twice under two import paths.
|
||||||
|
exclude = ["report_generator/vendor"]
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "W", "F", "I", "N", "D", "ANN", "B", "C4", "SIM", "RUF"]
|
||||||
|
# RUF100: the sources carry `# noqa` for rules the monorepo's own ruff config does not select
|
||||||
|
# either -- they are stale there too, just invisible, because the monorepo does not select RUF.
|
||||||
|
# Flagging them here would mean editing the copies, and the copies have to stay copies.
|
||||||
|
ignore = ["D203", "D213", "RUF100"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.pydocstyle]
|
||||||
|
convention = "google"
|
||||||
|
|
||||||
|
[tool.ruff.lint.isort]
|
||||||
|
# What the monorepo sorts these as: it lints from a root where all three live under `src/`, which
|
||||||
|
# ruff's default `src = [".", "src"]` makes first-party. Here they arrive through `vendor/` and
|
||||||
|
# would be sorted as third-party instead. Declared so that this repository reports the same import
|
||||||
|
# findings the monorepo does -- no more and no fewer -- and the copies can stay copies.
|
||||||
|
known-first-party = ["cc_common", "persistent_storage", "cipherverifier"]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
strict = true
|
||||||
|
python_version = "3.14"
|
||||||
|
# The copies are on the path at runtime only because `vendor/__init__.py` puts them there, which
|
||||||
|
# mypy does not execute. Named here so the borrowed modules resolve while still being excluded
|
||||||
|
# from being checked themselves.
|
||||||
|
mypy_path = ["report_generator/vendor"]
|
||||||
|
exclude = ["(^|/)report_generator/vendor/"]
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = ["weasyprint.*", "pymupdf.*"]
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
# `exclude` keeps mypy from crawling into the copies; it does not stop it following an import into
|
||||||
|
# them. They are checked where they are written, so their own errors are not this repository's to
|
||||||
|
# report -- their types still are, which is why they are followed at all rather than ignored.
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = ["cc_common.*", "persistent_storage.*", "cipherverifier.*"]
|
||||||
|
follow_imports = "silent"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"""Builds the PDF report of one stored scan, from the artifacts that scan put in the bucket.
|
||||||
|
|
||||||
|
A standalone tool rather than another persistent_storage command: it reads the store and writes a
|
||||||
|
file, and nothing else in the pipeline depends on it. The seams are drawn so that the whole package
|
||||||
|
moves to archon by changing what :mod:`sources` reads from --
|
||||||
|
everything after that point works on bytes and rows, not on a particular store.
|
||||||
|
|
||||||
|
The parts it does not own are reused rather than reimplemented, and copied into ``vendor/`` so the
|
||||||
|
package can also be lifted into a repository of its own. See ``vendor/README.md``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .vendor import activate
|
||||||
|
|
||||||
|
# Before anything else imports what this borrows. In the monorepo it finds the real packages and
|
||||||
|
# does nothing; in a repository of its own it puts the copies on the path.
|
||||||
|
activate()
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
"""Entrypoint for the report generator CLI."""
|
||||||
|
|
||||||
|
from cc_common.api.cli import CLIManager
|
||||||
|
from cc_common.config import load_environment
|
||||||
|
from persistent_storage.shared import inject_secrets
|
||||||
|
|
||||||
|
from .commands.generate import GenerateProperties, generate
|
||||||
|
|
||||||
|
|
||||||
|
def run() -> None:
|
||||||
|
"""Entrypoint for CLI."""
|
||||||
|
load_environment()
|
||||||
|
|
||||||
|
cli = CLIManager("report_generator", "Generates the PDF report of a stored scan.")
|
||||||
|
|
||||||
|
cli.register_command(
|
||||||
|
"generate",
|
||||||
|
_with_storage_secrets,
|
||||||
|
GenerateProperties,
|
||||||
|
help="Builds the PDF report of one scan from the artifacts it stored.",
|
||||||
|
)
|
||||||
|
cli.run()
|
||||||
|
|
||||||
|
|
||||||
|
def _with_storage_secrets(properties: GenerateProperties) -> object:
|
||||||
|
"""Fill the storage configuration's secrets from the environment before the command runs."""
|
||||||
|
inject_secrets(properties.storage_config)
|
||||||
|
|
||||||
|
return generate(properties)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
@@ -3,10 +3,10 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from cc_common.api import PipelineError, context
|
|
||||||
from cc_common.utils import print
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from cc_common.api import PipelineError, context
|
||||||
|
from cc_common.utils import print
|
||||||
from persistent_storage.shared import StorageConfiguration
|
from persistent_storage.shared import StorageConfiguration
|
||||||
|
|
||||||
from ..rendering import render
|
from ..rendering import render
|
||||||
|
|||||||
Binary file not shown.
Reference in new issue
Block a user