fix(reporting): require advisory_cvss for dependency findings + add SCA TUI renderer (#753)

Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
This commit is contained in:
devin-ai-integration[bot]
2026-07-12 17:31:49 -07:00
committed by GitHub
co-authored by Ahmed Allam
parent 24279e3279
commit a87bfb4881
9 changed files with 468 additions and 9 deletions
+58
View File
@@ -371,6 +371,19 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
text.append("Target: ", style=self.FIELD_STYLE) text.append("Target: ", style=self.FIELD_STYLE)
text.append(target) text.append(target)
dep_meta = vuln.get("dependency_metadata") or {}
for label, key in (
("Package", "package_name"),
("Ecosystem", "package_ecosystem"),
("Installed Version", "installed_version"),
("Fixed Version", "fixed_version"),
):
value = dep_meta.get(key)
if value:
text.append("\n\n")
text.append(f"{label}: ", style=self.FIELD_STYLE)
text.append(str(value))
endpoint = vuln.get("endpoint", "") endpoint = vuln.get("endpoint", "")
if endpoint: if endpoint:
text.append("\n\n") text.append("\n\n")
@@ -389,6 +402,18 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
text.append("CVE: ", style=self.FIELD_STYLE) text.append("CVE: ", style=self.FIELD_STYLE)
text.append(cve) text.append(cve)
cwe = vuln.get("cwe", "")
if cwe:
text.append("\n\n")
text.append("CWE: ", style=self.FIELD_STYLE)
text.append(cwe)
fix_effort = vuln.get("fix_effort", "")
if fix_effort:
text.append("\n\n")
text.append("Fix Effort: ", style=self.FIELD_STYLE)
text.append(str(fix_effort).title())
cvss_breakdown = vuln.get("cvss_breakdown", {}) cvss_breakdown = vuln.get("cvss_breakdown", {})
if cvss_breakdown: if cvss_breakdown:
cvss_parts = [] cvss_parts = []
@@ -434,6 +459,13 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
text.append("\n") text.append("\n")
text.append(technical_analysis) text.append(technical_analysis)
evidence = vuln.get("evidence", "")
if evidence:
text.append("\n\n")
text.append("Evidence", style=self.FIELD_STYLE)
text.append("\n")
text.append(evidence)
poc_description = vuln.get("poc_description", "") poc_description = vuln.get("poc_description", "")
if poc_description: if poc_description:
text.append("\n\n") text.append("\n\n")
@@ -455,6 +487,13 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
text.append("\n") text.append("\n")
text.append(remediation_steps) text.append(remediation_steps)
assumptions = vuln.get("assumptions", "")
if assumptions:
text.append("\n\n")
text.append("Assumptions", style=self.FIELD_STYLE)
text.append("\n")
text.append(assumptions)
return text return text
def _get_markdown_report(self) -> str: def _get_markdown_report(self) -> str:
@@ -476,14 +515,27 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
lines.append(f"**Agent:** {vuln['agent_name']}") lines.append(f"**Agent:** {vuln['agent_name']}")
if vuln.get("target"): if vuln.get("target"):
lines.append(f"**Target:** {vuln['target']}") lines.append(f"**Target:** {vuln['target']}")
dep_meta = vuln.get("dependency_metadata") or {}
if dep_meta.get("package_name"):
lines.append(f"**Package:** {dep_meta['package_name']}")
if dep_meta.get("package_ecosystem"):
lines.append(f"**Ecosystem:** {dep_meta['package_ecosystem']}")
if dep_meta.get("installed_version"):
lines.append(f"**Installed Version:** {dep_meta['installed_version']}")
if dep_meta.get("fixed_version"):
lines.append(f"**Fixed Version:** {dep_meta['fixed_version']}")
if vuln.get("endpoint"): if vuln.get("endpoint"):
lines.append(f"**Endpoint:** {vuln['endpoint']}") lines.append(f"**Endpoint:** {vuln['endpoint']}")
if vuln.get("method"): if vuln.get("method"):
lines.append(f"**Method:** {vuln['method']}") lines.append(f"**Method:** {vuln['method']}")
if vuln.get("cve"): if vuln.get("cve"):
lines.append(f"**CVE:** {vuln['cve']}") lines.append(f"**CVE:** {vuln['cve']}")
if vuln.get("cwe"):
lines.append(f"**CWE:** {vuln['cwe']}")
if vuln.get("cvss") is not None: if vuln.get("cvss") is not None:
lines.append(f"**CVSS:** {vuln['cvss']}") lines.append(f"**CVSS:** {vuln['cvss']}")
if vuln.get("fix_effort"):
lines.append(f"**Fix Effort:** {str(vuln['fix_effort']).title()}")
cvss_breakdown = vuln.get("cvss_breakdown", {}) cvss_breakdown = vuln.get("cvss_breakdown", {})
if cvss_breakdown: if cvss_breakdown:
@@ -514,6 +566,9 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
if vuln.get("technical_analysis"): if vuln.get("technical_analysis"):
lines.extend(["", "## Technical Analysis", "", vuln["technical_analysis"]]) lines.extend(["", "## Technical Analysis", "", vuln["technical_analysis"]])
if vuln.get("evidence"):
lines.extend(["", "## Evidence", "", vuln["evidence"]])
if vuln.get("poc_description") or vuln.get("poc_script_code"): if vuln.get("poc_description") or vuln.get("poc_script_code"):
lines.extend(["", "## Proof of Concept", ""]) lines.extend(["", "## Proof of Concept", ""])
if vuln.get("poc_description"): if vuln.get("poc_description"):
@@ -552,6 +607,9 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
if vuln.get("remediation_steps"): if vuln.get("remediation_steps"):
lines.extend(["", "## Remediation", "", vuln["remediation_steps"]]) lines.extend(["", "## Remediation", "", vuln["remediation_steps"]])
if vuln.get("assumptions"):
lines.extend(["", "## Assumptions", "", vuln["assumptions"]])
lines.append("") lines.append("")
return "\n".join(lines) return "\n".join(lines)
@@ -256,3 +256,176 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
css_classes = cls.get_css_classes("completed") css_classes = cls.get_css_classes("completed")
return Static(padded, classes=css_classes) return Static(padded, classes=css_classes)
@register_tool_renderer
class CreateDependencyReportRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "create_dependency_report"
css_classes: ClassVar[list[str]] = ["tool-call", "reporting-tool"]
SEVERITY_COLORS: ClassVar[dict[str, str]] = {
"critical": "#dc2626",
"high": "#ea580c",
"medium": "#d97706",
"low": "#65a30d",
"info": "#0284c7",
}
@classmethod
def _get_cvss_color(cls, cvss_score: float) -> str:
if cvss_score >= 9.0:
return "#dc2626"
if cvss_score >= 7.0:
return "#ea580c"
if cvss_score >= 4.0:
return "#d97706"
if cvss_score >= 0.1:
return "#65a30d"
return "#6b7280"
@classmethod
def _render_unsuccessful(cls, args: dict[str, Any], result: dict[str, Any]) -> Static:
text = Text()
text.append("📦 ")
text.append("Dependency (SCA) Report", style="bold #ea580c")
title = args.get("title", "")
if title:
text.append("\n\n")
text.append("Title: ", style=FIELD_STYLE)
text.append(title)
warning = result.get("warning")
if result.get("success") is False:
errors = result.get("errors")
detail = (
"; ".join(errors) if isinstance(errors, list) and errors else result.get("error")
)
label, style = "✗ Not created: ", "bold #dc2626"
fallback = "Report was not created."
else:
detail = warning
label, style = "⚠ Not persisted: ", "bold #d97706"
fallback = "Report could not be persisted."
text.append("\n\n")
text.append(label, style=style)
text.append(str(detail or fallback))
padded = Text()
padded.append("\n\n")
padded.append_text(text)
padded.append("\n\n")
return Static(padded, classes=cls.get_css_classes("failed"))
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915
args = tool_data.get("args", {})
result = tool_data.get("result", {})
if isinstance(result, dict) and (result.get("success") is False or result.get("warning")):
return cls._render_unsuccessful(args, result)
title = args.get("title", "")
description = args.get("description", "")
impact = args.get("impact", "")
target = args.get("target", "")
technical_analysis = args.get("technical_analysis", "")
remediation_steps = args.get("remediation_steps", "")
assumptions = args.get("assumptions", "")
package_name = args.get("package_name", "")
package_ecosystem = args.get("package_ecosystem", "")
installed_version = args.get("installed_version", "")
fixed_version = args.get("fixed_version", "")
cve = args.get("cve", "")
cwe = args.get("cwe", "")
advisory_cvss = args.get("advisory_cvss")
fix_effort = args.get("fix_effort", "")
severity = ""
if isinstance(result, dict):
severity = result.get("severity", "")
text = Text()
text.append("📦 ")
text.append("Dependency (SCA) Report", style="bold #ea580c")
if title:
text.append("\n\n")
text.append("Title: ", style=FIELD_STYLE)
text.append(title)
if severity:
text.append("\n\n")
text.append("Severity: ", style=FIELD_STYLE)
severity_color = cls.SEVERITY_COLORS.get(severity.lower(), "#6b7280")
text.append(severity.upper(), style=f"bold {severity_color}")
if advisory_cvss is not None:
text.append("\n\n")
text.append("Advisory CVSS: ", style=FIELD_STYLE)
try:
score = float(advisory_cvss)
text.append(str(score), style=f"bold {cls._get_cvss_color(score)}")
except (TypeError, ValueError):
text.append(str(advisory_cvss), style=DIM_STYLE)
if cve:
text.append("\n\n")
text.append("CVE: ", style=FIELD_STYLE)
text.append(cve)
if cwe:
text.append("\n\n")
text.append("CWE: ", style=FIELD_STYLE)
text.append(cwe)
if package_name:
text.append("\n\n")
text.append("Package: ", style=FIELD_STYLE)
text.append(package_name, style=FILE_STYLE)
if package_ecosystem:
text.append(f" ({package_ecosystem})", style=DIM_STYLE)
if installed_version:
text.append("\n\n")
text.append("Installed: ", style=FIELD_STYLE)
text.append(installed_version, style=BEFORE_STYLE)
if fixed_version:
text.append("", style=DIM_STYLE)
text.append("Fixed: ", style=FIELD_STYLE)
text.append(fixed_version, style=AFTER_STYLE)
if fix_effort:
text.append("\n\n")
text.append("Fix Effort: ", style=FIELD_STYLE)
text.append(fix_effort)
if target:
text.append("\n\n")
text.append("Target: ", style=FIELD_STYLE)
text.append(target)
for label, value in [
("Description", description),
("Impact", impact),
("Technical Analysis", technical_analysis),
("Assumptions", assumptions),
("Remediation", remediation_steps),
]:
if value:
text.append("\n\n")
text.append(label, style=FIELD_STYLE)
text.append("\n")
text.append(value)
if not title:
text.append("\n ")
text.append("Creating dependency report...", style="dim")
padded = Text()
padded.append("\n\n")
padded.append_text(text)
padded.append("\n\n")
css_classes = cls.get_css_classes("completed")
return Static(padded, classes=css_classes)
+17
View File
@@ -124,8 +124,13 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
f"**Found:** {report.get('timestamp', 'unknown')}", f"**Found:** {report.get('timestamp', 'unknown')}",
] ]
dep_meta = report.get("dependency_metadata") or {}
metadata: list[tuple[str, Any]] = [ metadata: list[tuple[str, Any]] = [
("Target", report.get("target")), ("Target", report.get("target")),
("Package", dep_meta.get("package_name")),
("Ecosystem", dep_meta.get("package_ecosystem")),
("Installed Version", dep_meta.get("installed_version")),
("Fixed Version", dep_meta.get("fixed_version")),
("Endpoint", report.get("endpoint")), ("Endpoint", report.get("endpoint")),
("Method", report.get("method")), ("Method", report.get("method")),
("CVE", report.get("cve")), ("CVE", report.get("cve")),
@@ -134,6 +139,8 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
cvss = report.get("cvss") cvss = report.get("cvss")
if cvss is not None: if cvss is not None:
metadata.append(("CVSS", cvss)) metadata.append(("CVSS", cvss))
if report.get("fix_effort"):
metadata.append(("Fix Effort", str(report["fix_effort"]).title()))
for label, value in metadata: for label, value in metadata:
if value: if value:
lines.append(f"**{label}:** {value}") lines.append(f"**{label}:** {value}")
@@ -143,6 +150,11 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
lines.append(report.get("description") or "No description provided.") lines.append(report.get("description") or "No description provided.")
lines.append("") lines.append("")
if report.get("evidence"):
lines.append("## Evidence\n")
lines.append(str(report["evidence"]))
lines.append("")
if report.get("impact"): if report.get("impact"):
lines.append("## Impact\n") lines.append("## Impact\n")
lines.append(str(report["impact"])) lines.append(str(report["impact"]))
@@ -194,4 +206,9 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
lines.append(str(report["remediation_steps"])) lines.append(str(report["remediation_steps"]))
lines.append("") lines.append("")
if report.get("assumptions"):
lines.append("## Assumptions\n")
lines.append(str(report["assumptions"]))
lines.append("")
return "\n".join(lines) return "\n".join(lines)
+1
View File
@@ -41,6 +41,7 @@ The skills are dynamically injected into the agent's system prompt, allowing it
Notable source-aware skills: Notable source-aware skills:
- `source_aware_whitebox` (coordination): white-box orchestration playbook - `source_aware_whitebox` (coordination): white-box orchestration playbook
- `source_aware_sast` (custom): semgrep/AST/secrets/supply-chain static triage workflow - `source_aware_sast` (custom): semgrep/AST/secrets/supply-chain static triage workflow
- `dependency_cve_scanning` (custom): trivy-based SCA workflow for reporting known dependency CVEs via `create_dependency_report`
--- ---
@@ -0,0 +1,138 @@
---
name: dependency-cve-scanning
description: Supply-chain / SCA playbook — scan repository lockfiles for known dependency CVEs and report them with create_dependency_report (no dynamic PoC required)
---
# Dependency / Supply-Chain CVE Scanning (SCA)
Use this skill on white-box / repository scans to make sure a repository pinning a
**known-vulnerable dependency** is actually reported as a finding, instead of being
discovered and then silently dropped because it cannot be dynamically exploited.
Known-CVE dependency findings are a first-class deliverable. Report each one with
the dedicated `create_dependency_report` tool.
## Why this skill exists
A vulnerable dependency pinned in a lockfile (e.g. `lodash@4.17.4` with a known
prototype-pollution CVE) usually cannot be dynamically PoC'd from the outside —
the vulnerable code path may not even be reachable from a running endpoint. The
normal "no report without a dynamic PoC" rule would suppress it. For these
findings the proof is the **lockfile entry + scanner output + published
advisory**, not an exploit script. This is the one explicit exception to the
dynamic-validation rule, and it exists only for `create_dependency_report`.
## Scan procedure
Run from the repo root and store output in the shared artifact directory used by
the source-aware pass:
```bash
ART=/workspace/.strix-source-aware
mkdir -p "$ART"
# Record the vuln DB age so a stale DB is a visible signal, not a silent clean scan.
trivy version --format json 2>/dev/null | tee "$ART/trivy-version.json"
# inspect .VulnerabilityDB.UpdatedAt / NextUpdate
# Lockfile/manifest -> known-CVE matching. Try a best-effort DB refresh first so a
# sandbox with egress gets the freshest CVEs; if the update fails, fall back to the
# cached DB instead of failing the scan. --offline-scan keeps per-package advisory
# lookups offline.
trivy fs --scanners vuln --timeout 30m --offline-scan \
--format json --output "$ART/trivy-sca.json" . \
|| trivy fs --scanners vuln --timeout 30m --offline-scan --skip-db-update \
--format json --output "$ART/trivy-sca.json" . \
|| true
```
If `.VulnerabilityDB.UpdatedAt` is more than a few weeks old (the sandbox had no
egress to refresh it), treat it as a scan limitation and note it in the
`assumptions` of dependency findings — a stale DB that still returns *some* results
will not trip the "zero results is suspicious" heuristic, so its age is the only
staleness signal.
Trivy reads the lockfiles/manifests it finds, including:
`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `poetry.lock`,
`requirements.txt`, `Pipfile.lock`, `go.mod`/`go.sum`, `Gemfile.lock`,
`pom.xml`/`gradle.lockfile`, `Cargo.lock`, `composer.lock`, etc.
If trivy returns zero vulnerabilities on a repo with dependencies, treat it as
suspicious: confirm the vuln DB is present (`trivy-version.json`) and that
lockfiles exist.
## Interpreting results
For each entry under `.Results[].Vulnerabilities[]` in `trivy-sca.json`, collect:
- `VulnerabilityID` — the CVE (or GHSA; prefer the CVE if both are present)
- `PkgName` and `InstalledVersion` — the affected package + pinned version
- `FixedVersion` — the version that resolves it
- `Target` — the lockfile path it came from
- `.Results[].Type` (e.g. `npm`, `pip`, `gomod`, `pom`, `gemspec`, `cargo`) — the
package ecosystem; normalize to the registry name lowercased (`npm`, `pypi`,
`go`, `maven`, `rubygems`, `cargo`, `composer`, `nuget`, ...)
- `CVSS` — the published advisory base score
- `PrimaryURL` / references — to verify the advisory
Deduplicate by `(CVE, PkgName, InstalledVersion)`. File one
`create_dependency_report` per CVE — do not batch multiple CVEs into one report.
### Reachability is a confidence modifier, not a gate
Do NOT suppress or downgrade a known CVE just because you could not prove the
vulnerable code path is reachable. Report it, set `advisory_cvss` from the
advisory, and use `assumptions` to note reachability (e.g. "the vulnerable
`template()` API does not appear to be imported in application code, so practical
exploitability is uncertain"). If you *can* show reachability or chain it into a
dynamic exploit, do that and report it as a normal dynamic finding with
`create_vulnerability_report` instead.
## Reporting
Report each confirmed known CVE with the dedicated `create_dependency_report`
tool (NOT `create_vulnerability_report` — that tool is for dynamically validated
findings and rejects empty PoC fields):
- Set `cve` to the verified `CVE-YYYY-NNNNN` id (required). If you only have a
GHSA, look up the mapped CVE; if there is genuinely no CVE, do not report it
with this tool.
- There are no PoC fields — `create_dependency_report` does not take
`poc_description` / `poc_script_code` / `code_locations`. The proof lives in
`description` and `technical_analysis` (scanner output + advisory).
- **Always fill the structured dependency fields** (they power the dedicated
dependency-report card; do not leave them only in free-text):
- `package_name``PkgName` (required).
- `installed_version``InstalledVersion` (required).
- `package_ecosystem` — normalized ecosystem from `.Results[].Type` (lowercased,
e.g. `npm`, `pypi`, `go`, `maven`, `rubygems`, `cargo`) (required).
- `fixed_version``FixedVersion` (leave empty only if no fix is published).
- Reference the repo-relative `Target` lockfile path in `description` /
`technical_analysis` (no leading slash) so the finding is traceable.
- Put the concrete proof in `description` / `technical_analysis`: package name,
installed/affected version, fixed version, lockfile path, and the relevant
trivy output excerpt.
- **Always set `advisory_cvss` to the published advisory base score (0.010.0).**
Severity is derived *solely* from this number: read it off the advisory (`CVSS`
in trivy output, or the NVD/GHSA page) and pass the real value. The tool rejects
a call that omits it, because guessing a score both inflates low CVEs and
deflates critical ones.
- Set `cwe` to the most specific `CWE-NNN` when the advisory names one.
- Do NOT cap severity at LOW just because there is no dynamic reproduction — use
the advisory score.
- Use `assumptions` for reachability/exploitability caveats.
Verify the CVE with `web_search` when available before reporting. Never guess or
hallucinate a CVE id.
## Anti-patterns
- Do not report a dependency CVE with `create_vulnerability_report`; use
`create_dependency_report`.
- Do not report a finding without a verified CVE id.
- Do not batch multiple CVEs into one report.
- Do not omit `advisory_cvss` — the tool rejects it, and it is the single input
that determines dependency severity.
- Do not silently drop a known CVE because it lacks a dynamic PoC — that is the
exact failure this skill prevents.
- Do not downgrade advisory severity for lack of dynamic reproduction.
+5
View File
@@ -121,6 +121,11 @@ trivy fs --scanners vuln,misconfig --timeout 30m --offline-scan \
--format json --output /workspace/.strix-source-aware/trivy-fs.json . || true --format json --output /workspace/.strix-source-aware/trivy-fs.json . || true
``` ```
Known-CVE dependency findings are the one exception to the "report only after
dynamic validation" rule below: report each one with `create_dependency_report`
(not `create_vulnerability_report`), setting `advisory_cvss` from the published
advisory. `load_skill(["dependency_cve_scanning"])` for the full SCA workflow.
## JavaScript-Side Coverage ## JavaScript-Side Coverage
For frontends and Node services, layer these on top of the language-agnostic For frontends and Node services, layer these on top of the language-agnostic
+15 -8
View File
@@ -198,8 +198,7 @@ async def _do_create( # noqa: PLR0912
fix_effort = (fix_effort or "").strip().lower() fix_effort = (fix_effort or "").strip().lower()
if fix_effort not in _VALID_FIX_EFFORT: if fix_effort not in _VALID_FIX_EFFORT:
errors.append( errors.append(
f"Invalid fix_effort: {fix_effort!r}. " f"Invalid fix_effort: {fix_effort!r}. Must be one of: {sorted(_VALID_FIX_EFFORT)}"
f"Must be one of: {sorted(_VALID_FIX_EFFORT)}"
) )
if not isinstance(cvss_breakdown, dict) or not cvss_breakdown: if not isinstance(cvss_breakdown, dict) or not cvss_breakdown:
@@ -610,7 +609,7 @@ _DEP_SEVERITY_FROM_CVSS = {
def _dependency_severity(advisory_cvss: float | None) -> tuple[float, str]: def _dependency_severity(advisory_cvss: float | None) -> tuple[float, str]:
if advisory_cvss is None: if advisory_cvss is None:
return 0.0, "medium" return 0.0, "info"
score = max(0.0, min(10.0, advisory_cvss)) score = max(0.0, min(10.0, advisory_cvss))
for (lo, hi), label in _DEP_SEVERITY_FROM_CVSS.items(): for (lo, hi), label in _DEP_SEVERITY_FROM_CVSS.items():
if lo <= score < hi or (hi == 10.0 and score == 10.0): if lo <= score < hi or (hi == 10.0 and score == 10.0):
@@ -652,7 +651,7 @@ def _build_dependency_evidence(
return evidence return evidence
async def _do_create_dependency( async def _do_create_dependency( # noqa: PLR0912
*, *,
title: str, title: str,
description: str, description: str,
@@ -705,7 +704,13 @@ async def _do_create_dependency(
f"Invalid fix_effort: {fix_effort!r}. Must be one of: {sorted(_VALID_FIX_EFFORT)}" f"Invalid fix_effort: {fix_effort!r}. Must be one of: {sorted(_VALID_FIX_EFFORT)}"
) )
if advisory_cvss is not None and not 0.0 <= advisory_cvss <= 10.0: if advisory_cvss is None:
errors.append(
"advisory_cvss is required: read the published advisory base score "
"(0.0-10.0) off the advisory (trivy CVSS / NVD / GHSA). Severity is "
"derived solely from it — do not omit it or the finding cannot be rated."
)
elif not 0.0 <= advisory_cvss <= 10.0:
errors.append(f"advisory_cvss must be between 0.0 and 10.0, got {advisory_cvss}") errors.append(f"advisory_cvss must be between 0.0 and 10.0, got {advisory_cvss}")
if errors: if errors:
@@ -810,13 +815,13 @@ async def create_dependency_report(
cve: str, cve: str,
package_name: str, package_name: str,
installed_version: str, installed_version: str,
advisory_cvss: float,
impact: str, impact: str,
remediation_steps: str, remediation_steps: str,
assumptions: str, assumptions: str,
package_ecosystem: str, package_ecosystem: str,
fixed_version: str | None = None, fixed_version: str | None = None,
cwe: str | None = None, cwe: str | None = None,
advisory_cvss: float | None = None,
technical_analysis: str | None = None, technical_analysis: str | None = None,
fix_effort: str = "low", fix_effort: str = "low",
) -> str: ) -> str:
@@ -864,8 +869,10 @@ async def create_dependency_report(
package_ecosystem: e.g. ``npm`` / ``pypi`` / ``maven`` / ``go``. package_ecosystem: e.g. ``npm`` / ``pypi`` / ``maven`` / ``go``.
fixed_version: First non-vulnerable version, if known. fixed_version: First non-vulnerable version, if known.
cwe: ``CWE-NNN`` (most specific) if certain, else omit. cwe: ``CWE-NNN`` (most specific) if certain, else omit.
advisory_cvss: Published advisory base score (0.0-10.0) if known; advisory_cvss: **Required.** Published advisory base score
drives severity. Omit if unknown (defaults to medium). (0.0-10.0) — read it off the advisory (trivy CVSS / NVD / GHSA).
Severity is derived solely from this score, so it must be the
real published value; do not guess or omit it.
technical_analysis: Optional deeper mechanism/root-cause detail. technical_analysis: Optional deeper mechanism/root-cause detail.
fix_effort: One of ``trivial`` / ``low`` / ``medium`` / ``high`` fix_effort: One of ``trivial`` / ``low`` / ``medium`` / ``high``
(dependency upgrades are usually ``trivial``/``low``). (dependency upgrades are usually ``trivial``/``low``).
+34
View File
@@ -79,6 +79,40 @@ def test_render_vulnerability_md_includes_core_sections() -> None:
assert "**Endpoint:** /api/login" in md assert "**Endpoint:** /api/login" in md
def test_render_vulnerability_md_includes_dependency_fields() -> None:
md = render_vulnerability_md(
_sample_report(
title="CVE-2021-23337 in lodash 4.17.20",
severity="high",
target="repo/package.json",
endpoint=None,
method=None,
cve="CVE-2021-23337",
cwe="CWE-94",
cvss=7.2,
fix_effort="trivial",
finding_class="dependency_cve",
evidence="**Advisory evidence:** `CVE-2021-23337` applies to `lodash`.",
assumptions="Assumes lodash ships in deployed builds.",
dependency_metadata={
"package_name": "lodash",
"package_ecosystem": "npm",
"installed_version": "4.17.20",
"fixed_version": "4.17.21",
},
remediation_steps="Upgrade to 4.17.21.",
),
)
assert "**Package:** lodash" in md
assert "**Ecosystem:** npm" in md
assert "**Installed Version:** 4.17.20" in md
assert "**Fixed Version:** 4.17.21" in md
assert "**CWE:** CWE-94" in md
assert "**Fix Effort:** Trivial" in md
assert "## Evidence" in md
assert "## Assumptions" in md
def test_write_vulnerabilities_creates_markdown_csv_and_json(tmp_path: Path) -> None: def test_write_vulnerabilities_creates_markdown_csv_and_json(tmp_path: Path) -> None:
reports = [ reports = [
_sample_report(id="vuln-0001", severity="medium", timestamp="2026-07-02 11:00:00 UTC"), _sample_report(id="vuln-0001", severity="medium", timestamp="2026-07-02 11:00:00 UTC"),
+27 -1
View File
@@ -192,6 +192,30 @@ async def test_dependency_report_with_zero_cvss_remains_low_severity(
assert report["cvss"] == 0.0 assert report["cvss"] == 0.0
async def test_dependency_report_requires_advisory_cvss(report_state: ReportState) -> None:
result = await _do_create_dependency(
title="CVE-2024-0001 in sample 1.0.0",
description="Published advisory affects the pinned version.",
target="repo/package.json",
cve="CVE-2024-0001",
package_name="sample",
installed_version="1.0.0",
impact="Some impact.",
remediation_steps="Upgrade to 1.0.1.",
assumptions="Assumes the package ships in deployed builds.",
package_ecosystem="npm",
fixed_version="1.0.1",
cwe=None,
advisory_cvss=None,
technical_analysis=None,
fix_effort="low",
)
assert result["success"] is False
assert any("advisory_cvss is required" in e for e in result["errors"])
assert not report_state.vulnerability_reports
async def test_dependency_report_dedupe_candidate_includes_dependency_metadata( async def test_dependency_report_dedupe_candidate_includes_dependency_metadata(
report_state: ReportState, report_state: ReportState,
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
@@ -535,4 +559,6 @@ def test_vuln_tool_exposes_new_params() -> None:
dep_props = create_dependency_report.params_json_schema["properties"] dep_props = create_dependency_report.params_json_schema["properties"]
for field in ("package_name", "installed_version", "cve", "advisory_cvss"): for field in ("package_name", "installed_version", "cve", "advisory_cvss"):
assert field in dep_props assert field in dep_props
assert "package_ecosystem" in create_dependency_report.params_json_schema["required"] dep_required = create_dependency_report.params_json_schema["required"]
assert "package_ecosystem" in dep_required
assert "advisory_cvss" in dep_required