reporting: drop per-metric contextual CVSS reasoning, keep the summary

This commit is contained in:
Ahmed Allam 2026-08-17 09:08:07 +00:00 committed by Ahmed Allam
parent 310f310e28
commit 99e2d5d826
3 changed files with 1 additions and 38 deletions

View file

@ -288,10 +288,6 @@ only caller of `yaml.load` is `parse_manifest` in `scripts/import.py:88`, which
reaches that function, so an attacker must already hold shell access on the job reaches that function, so an attacker must already hold shell access on the job
host, and the parsed data is build metadata rather than customer records." host, and the parsed data is build metadata rather than customer records."
`contextual_cvss_metric_reasoning` is optional and takes one detailed sentence
per metric you adjusted, keyed by the metric name. Use it for the per-metric
detail that does not fit the summary.
Omit all the contextual fields when the published rating already fits, and when Omit all the contextual fields when the published rating already fits, and when
the evidence is thin. A contextual score is a claim you must be able to defend, the evidence is thin. A contextual score is a claim you must be able to defend,
and this adjustment never replaces `advisory_cvss`. and this adjustment never replaces `advisory_cvss`.

View file

@ -782,21 +782,6 @@ def _clean_contextual_cvss_metrics(raw: dict[str, str] | None) -> dict[str, str]
return metrics return metrics
def _clean_contextual_cvss_metric_reasoning(
raw: dict[str, str] | None, metrics: dict[str, str]
) -> dict[str, str]:
"""Keep per-metric justifications that belong to an adjusted metric."""
if not isinstance(raw, dict):
return {}
detail: dict[str, str] = {}
for key, value in raw.items():
metric = str(key or "").strip().upper()
text = str(value or "").strip()
if metric in metrics and text:
detail[metric] = text[:_MAX_CONTEXTUAL_REASONING_CHARS]
return detail
def _build_dependency_metadata( def _build_dependency_metadata(
*, *,
package_name: str, package_name: str,
@ -810,7 +795,6 @@ def _build_dependency_metadata(
reachability_evidence: str | None = None, reachability_evidence: str | None = None,
contextual_cvss_metrics: dict[str, str] | None = None, contextual_cvss_metrics: dict[str, str] | None = None,
contextual_cvss_reasoning: str | None = None, contextual_cvss_reasoning: str | None = None,
contextual_cvss_metric_reasoning: dict[str, str] | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
metadata: dict[str, Any] = { metadata: dict[str, Any] = {
"package_name": package_name.strip(), "package_name": package_name.strip(),
@ -839,11 +823,6 @@ def _build_dependency_metadata(
if cleaned_metrics and reasoning: if cleaned_metrics and reasoning:
metadata["contextual_cvss_metrics"] = cleaned_metrics metadata["contextual_cvss_metrics"] = cleaned_metrics
metadata["contextual_cvss_reasoning"] = reasoning[:_MAX_CONTEXTUAL_REASONING_CHARS] metadata["contextual_cvss_reasoning"] = reasoning[:_MAX_CONTEXTUAL_REASONING_CHARS]
metric_reasoning = _clean_contextual_cvss_metric_reasoning(
contextual_cvss_metric_reasoning, cleaned_metrics
)
if metric_reasoning:
metadata["contextual_cvss_metric_reasoning"] = metric_reasoning
return metadata return metadata
@ -917,7 +896,6 @@ async def _do_create_dependency( # noqa: PLR0912
reachability_evidence: str | None = None, reachability_evidence: str | None = None,
contextual_cvss_metrics: dict[str, str] | None = None, contextual_cvss_metrics: dict[str, str] | None = None,
contextual_cvss_reasoning: str | None = None, contextual_cvss_reasoning: str | None = None,
contextual_cvss_metric_reasoning: dict[str, str] | None = None,
agent_id: str | None = None, agent_id: str | None = None,
agent_name: str | None = None, agent_name: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
@ -1002,7 +980,6 @@ async def _do_create_dependency( # noqa: PLR0912
reachability_evidence=reachability_evidence, reachability_evidence=reachability_evidence,
contextual_cvss_metrics=contextual_cvss_metrics, contextual_cvss_metrics=contextual_cvss_metrics,
contextual_cvss_reasoning=contextual_cvss_reasoning, contextual_cvss_reasoning=contextual_cvss_reasoning,
contextual_cvss_metric_reasoning=contextual_cvss_metric_reasoning,
) )
evidence = _build_dependency_evidence( evidence = _build_dependency_evidence(
cve=parsed_cve, cve=parsed_cve,
@ -1116,7 +1093,6 @@ async def create_dependency_report(
reachability_evidence: str | None = None, reachability_evidence: str | None = None,
contextual_cvss_metrics: dict[str, str] | None = None, contextual_cvss_metrics: dict[str, str] | None = None,
contextual_cvss_reasoning: str | None = None, contextual_cvss_reasoning: str | None = None,
contextual_cvss_metric_reasoning: dict[str, str] | None = None,
) -> str: ) -> str:
"""File a known-CVE dependency (SCA) finding — one report per CVE x package. """File a known-CVE dependency (SCA) finding — one report per CVE x package.
@ -1244,12 +1220,6 @@ async def create_dependency_report(
never a generic statement such as "low risk". The user reads never a generic statement such as "low risk". The user reads
this text next to the adjusted score, so an adjustment this text next to the adjusted score, so an adjustment
without it is discarded. without it is discarded.
contextual_cvss_metric_reasoning: Optional per-metric detail, as a
mapping of the SAME metric names you set in
``contextual_cvss_metrics`` to one detailed sentence each
(for example ``{"MAC": "Reaching the parser needs the
--unsafe flag, which deploy/prod.yaml never sets."}``).
Entries for metrics you did not adjust are dropped.
""" """
agent_id, agent_name = _caller_identity(ctx) agent_id, agent_name = _caller_identity(ctx)
@ -1276,7 +1246,6 @@ async def create_dependency_report(
reachability_evidence=reachability_evidence, reachability_evidence=reachability_evidence,
contextual_cvss_metrics=contextual_cvss_metrics, contextual_cvss_metrics=contextual_cvss_metrics,
contextual_cvss_reasoning=contextual_cvss_reasoning, contextual_cvss_reasoning=contextual_cvss_reasoning,
contextual_cvss_metric_reasoning=contextual_cvss_metric_reasoning,
agent_id=agent_id, agent_id=agent_id,
agent_name=agent_name, agent_name=agent_name,
) )

View file

@ -884,7 +884,6 @@ def test_dep_tool_exposes_contextual_cvss_params() -> None:
for field in ( for field in (
"contextual_cvss_metrics", "contextual_cvss_metrics",
"contextual_cvss_reasoning", "contextual_cvss_reasoning",
"contextual_cvss_metric_reasoning",
): ):
assert field in dep_props assert field in dep_props
assert "source-to-sink" in dep_props["contextual_cvss_metrics"]["description"].lower() assert "source-to-sink" in dep_props["contextual_cvss_metrics"]["description"].lower()
@ -914,13 +913,12 @@ async def test_dependency_report_keeps_only_valid_contextual_metrics(
manifest_path="package-lock.json", manifest_path="package-lock.json",
contextual_cvss_metrics={"MAC": "H", "MC": "L", "AV": "N", "MPR": "Z"}, contextual_cvss_metrics={"MAC": "H", "MC": "L", "AV": "N", "MPR": "Z"},
contextual_cvss_reasoning="Only scripts/import.py reaches the sink.", contextual_cvss_reasoning="Only scripts/import.py reaches the sink.",
contextual_cvss_metric_reasoning={"MAC": "Needs a build flag.", "MPR": "dropped"},
) )
assert result["success"] is True, result assert result["success"] is True, result
metadata = report_state.vulnerability_reports[0]["dependency_metadata"] metadata = report_state.vulnerability_reports[0]["dependency_metadata"]
assert metadata["contextual_cvss_metrics"] == {"MAC": "H", "MC": "L"} assert metadata["contextual_cvss_metrics"] == {"MAC": "H", "MC": "L"}
assert metadata["contextual_cvss_reasoning"] == "Only scripts/import.py reaches the sink." assert metadata["contextual_cvss_reasoning"] == "Only scripts/import.py reaches the sink."
assert metadata["contextual_cvss_metric_reasoning"] == {"MAC": "Needs a build flag."} assert "contextual_cvss_metric_reasoning" not in metadata
@pytest.mark.asyncio @pytest.mark.asyncio