Let the caller state the project's version
--version replaces the commit hash on the cover and the project page, for generate and from-folder alike. Without it the report still falls back to repository_version from the metadata, else the scanned commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ffcd68f9f8
commit
0ace2e1803
4 files changed
+31
-10
No files matched your search
@@ -23,7 +23,7 @@ Two ways to say where the artifacts are.
|
|||||||
|
|
||||||
```
|
```
|
||||||
report_generator generate --scan_id <uuid> [--path <file-or-directory>] --config config.json
|
report_generator generate --scan_id <uuid> [--path <file-or-directory>] --config config.json
|
||||||
[--project <name>] [--manufacturer <name>]
|
[--project <name>] [--manufacturer <name>] [--version <version>]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Out of a folder** — neither Postgres nor the bucket, just the artifacts as the pipeline exported
|
**Out of a folder** — neither Postgres nor the bucket, just the artifacts as the pipeline exported
|
||||||
@@ -31,9 +31,12 @@ them:
|
|||||||
|
|
||||||
```
|
```
|
||||||
report_generator from-folder --artifacts ./sample [--path <file-or-directory>]
|
report_generator from-folder --artifacts ./sample [--path <file-or-directory>]
|
||||||
[--project <name>] [--manufacturer <name>]
|
[--project <name>] [--manufacturer <name>] [--version <version>]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`--version` sets the version shown on the cover and the project page. Without it the report uses
|
||||||
|
`repository_version` from `metadata.json`, else the commit the scan was run on.
|
||||||
|
|
||||||
`--scan_id` is optional there: the pipeline writes its own id into the paths it records in
|
`--scan_id` is optional there: the pipeline writes its own id into the paths it records in
|
||||||
`metadata.json`, so an export names its own run. Pass it for a folder assembled by hand, or one
|
`metadata.json`, so an export names its own run. Pass it for a folder assembled by hand, or one
|
||||||
whose metadata is gone.
|
whose metadata is gone.
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ class GenerateProperties(BaseModel):
|
|||||||
the renderer chose; leaving it out writes into ``reports/``.
|
the renderer chose; leaving it out writes into ``reports/``.
|
||||||
project: The name of the scanned project, as the report states it.
|
project: The name of the scanned project, as the report states it.
|
||||||
manufacturer: Who makes the scanned product, as the report states it.
|
manufacturer: Who makes the scanned product, as the report states it.
|
||||||
|
version: The version of the scanned project, as the report states it.
|
||||||
|
Replaces the commit hash the metadata names when given.
|
||||||
storage_config: The store the scan's artifacts are read from.
|
storage_config: The store the scan's artifacts are read from.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -41,6 +43,9 @@ class GenerateProperties(BaseModel):
|
|||||||
path: Path | None = Field(default=None, description="Where to write the PDF report.")
|
path: Path | None = Field(default=None, description="Where to write the PDF report.")
|
||||||
project: str | None = Field(default=None, description="The name of the scanned project.")
|
project: str | None = Field(default=None, description="The name of the scanned project.")
|
||||||
manufacturer: str | None = Field(default=None, description="The manufacturer of the scanned product.")
|
manufacturer: str | None = Field(default=None, description="The manufacturer of the scanned product.")
|
||||||
|
version: str | None = Field(
|
||||||
|
default=None, description="The version of the scanned project. Replaces the commit hash when given."
|
||||||
|
)
|
||||||
storage_config: StorageConfiguration
|
storage_config: StorageConfiguration
|
||||||
|
|
||||||
|
|
||||||
@@ -54,6 +59,7 @@ class FolderProperties(BaseModel):
|
|||||||
path: Where to write the PDF, as above.
|
path: Where to write the PDF, as above.
|
||||||
project: The name of the scanned project, as above.
|
project: The name of the scanned project, as above.
|
||||||
manufacturer: Who makes the scanned product, as above.
|
manufacturer: Who makes the scanned product, as above.
|
||||||
|
version: The version of the scanned project, as above.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
artifacts: Path = Field(description="Folder of exported artifacts to build the report from.")
|
artifacts: Path = Field(description="Folder of exported artifacts to build the report from.")
|
||||||
@@ -63,6 +69,9 @@ class FolderProperties(BaseModel):
|
|||||||
path: Path | None = Field(default=None, description="Where to write the PDF report.")
|
path: Path | None = Field(default=None, description="Where to write the PDF report.")
|
||||||
project: str | None = Field(default=None, description="The name of the scanned project.")
|
project: str | None = Field(default=None, description="The name of the scanned project.")
|
||||||
manufacturer: str | None = Field(default=None, description="The manufacturer of the scanned product.")
|
manufacturer: str | None = Field(default=None, description="The manufacturer of the scanned product.")
|
||||||
|
version: str | None = Field(
|
||||||
|
default=None, description="The version of the scanned project. Replaces the commit hash when given."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GeneratedReport(BaseModel):
|
class GeneratedReport(BaseModel):
|
||||||
@@ -102,6 +111,7 @@ def generate(properties: GenerateProperties) -> GeneratedReport:
|
|||||||
properties.path,
|
properties.path,
|
||||||
project=properties.project,
|
project=properties.project,
|
||||||
manufacturer=properties.manufacturer,
|
manufacturer=properties.manufacturer,
|
||||||
|
version=properties.version,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -125,6 +135,7 @@ def from_folder(properties: FolderProperties) -> GeneratedReport:
|
|||||||
properties.path,
|
properties.path,
|
||||||
project=properties.project,
|
project=properties.project,
|
||||||
manufacturer=properties.manufacturer,
|
manufacturer=properties.manufacturer,
|
||||||
|
version=properties.version,
|
||||||
write_metrics=True,
|
write_metrics=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -135,6 +146,7 @@ def _report(
|
|||||||
*,
|
*,
|
||||||
project: str | None = None,
|
project: str | None = None,
|
||||||
manufacturer: str | None = None,
|
manufacturer: str | None = None,
|
||||||
|
version: str | None = None,
|
||||||
write_metrics: bool = False,
|
write_metrics: bool = False,
|
||||||
) -> GeneratedReport:
|
) -> GeneratedReport:
|
||||||
"""Render the collected artifacts and write the result.
|
"""Render the collected artifacts and write the result.
|
||||||
@@ -147,6 +159,7 @@ def _report(
|
|||||||
path: Where the caller asked for the PDF, if anywhere in particular.
|
path: Where the caller asked for the PDF, if anywhere in particular.
|
||||||
project: The project name the caller supplied, if any.
|
project: The project name the caller supplied, if any.
|
||||||
manufacturer: The manufacturer the caller supplied, if any.
|
manufacturer: The manufacturer the caller supplied, if any.
|
||||||
|
version: The version the caller supplied, if any.
|
||||||
write_metrics: Whether to write ``metrics.json`` beside the PDF.
|
write_metrics: Whether to write ``metrics.json`` beside the PDF.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -156,7 +169,7 @@ def _report(
|
|||||||
PipelineError: The report cannot be built or the destination not written.
|
PipelineError: The report cannot be built or the destination not written.
|
||||||
"""
|
"""
|
||||||
metrics = build_metrics(sources)
|
metrics = build_metrics(sources)
|
||||||
name, content = render(sources, metrics, project=project, manufacturer=manufacturer)
|
name, content = render(sources, metrics, project=project, manufacturer=manufacturer, version=version)
|
||||||
destination = _destination(path, name)
|
destination = _destination(path, name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ def build_context(
|
|||||||
*,
|
*,
|
||||||
project: str | None = None,
|
project: str | None = None,
|
||||||
manufacturer: str | None = None,
|
manufacturer: str | None = None,
|
||||||
|
version: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Build the report context from a scan's parsed artifacts.
|
"""Build the report context from a scan's parsed artifacts.
|
||||||
|
|
||||||
@@ -70,6 +71,7 @@ def build_context(
|
|||||||
``vulnerabilities`` -- each absent where the scan stored none.
|
``vulnerabilities`` -- each absent where the scan stored none.
|
||||||
project: The project name the caller supplied. Wins over the one in the metadata.
|
project: The project name the caller supplied. Wins over the one in the metadata.
|
||||||
manufacturer: The manufacturer the caller supplied. The artifacts never name one.
|
manufacturer: The manufacturer the caller supplied. The artifacts never name one.
|
||||||
|
version: The version the caller supplied. Wins over the one in the metadata.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The context :func:`layout.render_pdf`
|
The context :func:`layout.render_pdf`
|
||||||
@@ -78,11 +80,12 @@ def build_context(
|
|||||||
"""
|
"""
|
||||||
metadata = documents.get("metadata") or {}
|
metadata = documents.get("metadata") or {}
|
||||||
project = project or (metadata.get("configuration") or {}).get("repository_name")
|
project = project or (metadata.get("configuration") or {}).get("repository_name")
|
||||||
|
version = version or _version(metadata)
|
||||||
tools = _tools(documents.get("findings"))
|
tools = _tools(documents.get("findings"))
|
||||||
sca = summarize_vdr(documents["vulnerabilities"]) if documents.get("vulnerabilities") else None
|
sca = summarize_vdr(documents["vulnerabilities"]) if documents.get("vulnerabilities") else None
|
||||||
|
|
||||||
context: dict[str, Any] = {
|
context: dict[str, Any] = {
|
||||||
"cover": _cover(sources, metadata, tools, sca, project=project, manufacturer=manufacturer),
|
"cover": _cover(sources, tools, sca, project=project, manufacturer=manufacturer, version=version),
|
||||||
"summary": _summary(tools, sca, documents.get("metrics")),
|
"summary": _summary(tools, sca, documents.get("metrics")),
|
||||||
"sections": ([_sca_section(sca)] if sca is not None else [])
|
"sections": ([_sca_section(sca)] if sca is not None else [])
|
||||||
+ [_tool_section(name, report) for name, report in tools],
|
+ [_tool_section(name, report) for name, report in tools],
|
||||||
@@ -90,7 +93,7 @@ def build_context(
|
|||||||
|
|
||||||
# Without the pipeline's metadata or cloc's count this page would be nothing but gaps.
|
# Without the pipeline's metadata or cloc's count this page would be nothing but gaps.
|
||||||
if metadata or documents.get("cloc"):
|
if metadata or documents.get("cloc"):
|
||||||
context["project"] = _project(metadata, project, documents.get("cloc"))
|
context["project"] = _project(project, version, documents.get("cloc"))
|
||||||
|
|
||||||
critical = _critical(tools, sca)
|
critical = _critical(tools, sca)
|
||||||
|
|
||||||
@@ -181,12 +184,12 @@ def _stated(value: Any) -> str: # noqa: ANN401
|
|||||||
|
|
||||||
def _cover(
|
def _cover(
|
||||||
sources: ReportSources,
|
sources: ReportSources,
|
||||||
metadata: dict[str, Any],
|
|
||||||
tools: list[tuple[str, SarifReport]],
|
tools: list[tuple[str, SarifReport]],
|
||||||
sca: VdrReport | None,
|
sca: VdrReport | None,
|
||||||
*,
|
*,
|
||||||
project: str | None,
|
project: str | None,
|
||||||
manufacturer: str | None,
|
manufacturer: str | None,
|
||||||
|
version: str | None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""The first page."""
|
"""The first page."""
|
||||||
technologies = ([SCA] if sca is not None else []) + [name for name, _ in tools]
|
technologies = ([SCA] if sca is not None else []) + [name for name, _ in tools]
|
||||||
@@ -200,7 +203,7 @@ def _cover(
|
|||||||
"identity": [
|
"identity": [
|
||||||
_fact("Manufacturer", manufacturer),
|
_fact("Manufacturer", manufacturer),
|
||||||
_fact("Project", project),
|
_fact("Project", project),
|
||||||
_fact("Version", _version(metadata), kind="mono"),
|
_fact("Version", version, kind="mono"),
|
||||||
_fact("Technology", ", ".join(technologies) or None, gap=True),
|
_fact("Technology", ", ".join(technologies) or None, gap=True),
|
||||||
_fact("Report Created", datetime.now(UTC).strftime("%A, %B %d, %Y")),
|
_fact("Report Created", datetime.now(UTC).strftime("%A, %B %d, %Y")),
|
||||||
_fact("Scan Identifier", sources.scan_id, kind="mono"),
|
_fact("Scan Identifier", sources.scan_id, kind="mono"),
|
||||||
@@ -248,14 +251,14 @@ def _version(metadata: dict[str, Any]) -> str | None:
|
|||||||
return commit if isinstance(commit, str) and _COMMIT.fullmatch(commit) else None
|
return commit if isinstance(commit, str) and _COMMIT.fullmatch(commit) else None
|
||||||
|
|
||||||
|
|
||||||
def _project(metadata: dict[str, Any], project: str | None, cloc: dict[str, Any] | None) -> dict[str, Any]:
|
def _project(project: str | None, version: str | None, cloc: dict[str, Any] | None) -> dict[str, Any]:
|
||||||
"""What was scanned, and how much of it there is, as cloc counted it."""
|
"""What was scanned, and how much of it there is, as cloc counted it."""
|
||||||
return {
|
return {
|
||||||
"id": "project",
|
"id": "project",
|
||||||
"title": "Project Information",
|
"title": "Project Information",
|
||||||
"overview": [
|
"overview": [
|
||||||
_fact("Project", project),
|
_fact("Project", project),
|
||||||
_fact("Version", _version(metadata), kind="mono"),
|
_fact("Version", version, kind="mono"),
|
||||||
_fact("Analysed lines of code", f"{cloc['code']:,} in {cloc['files']:,} files" if cloc else None),
|
_fact("Analysed lines of code", f"{cloc['code']:,} in {cloc['files']:,} files" if cloc else None),
|
||||||
],
|
],
|
||||||
"languages": {"title": "Main languages", "rows": _languages(cloc)},
|
"languages": {"title": "Main languages", "rows": _languages(cloc)},
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ def render(
|
|||||||
*,
|
*,
|
||||||
project: str | None = None,
|
project: str | None = None,
|
||||||
manufacturer: str | None = None,
|
manufacturer: str | None = None,
|
||||||
|
version: str | None = None,
|
||||||
) -> tuple[str, bytes]:
|
) -> tuple[str, bytes]:
|
||||||
"""Render the report of one scan.
|
"""Render the report of one scan.
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ def render(
|
|||||||
metrics: The metrics document, if one was built.
|
metrics: The metrics document, if one was built.
|
||||||
project: The project name the caller supplied; overrides the metadata's.
|
project: The project name the caller supplied; overrides the metadata's.
|
||||||
manufacturer: The manufacturer the caller supplied.
|
manufacturer: The manufacturer the caller supplied.
|
||||||
|
version: The version the caller supplied; overrides the commit the metadata names.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The file name the report should be saved under, and its bytes.
|
The file name the report should be saved under, and its bytes.
|
||||||
@@ -83,7 +85,7 @@ def render(
|
|||||||
|
|
||||||
logging_context.logger.info(f"Rendering the report from {', '.join(found)}...")
|
logging_context.logger.info(f"Rendering the report from {', '.join(found)}...")
|
||||||
|
|
||||||
context = build_context(sources, documents, project=project, manufacturer=manufacturer)
|
context = build_context(sources, documents, project=project, manufacturer=manufacturer, version=version)
|
||||||
|
|
||||||
return NAME.format(scan=sources.scan_id), render_pdf(context)
|
return NAME.format(scan=sources.scan_id), render_pdf(context)
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user