From 0e28fbc1dc12f42d8122e37fbe2ecbedb875c8fe Mon Sep 17 00:00:00 2001 From: ogt Date: Tue, 14 Jul 2026 20:13:12 +0800 Subject: [PATCH] fix(security): resolve TLS inventory in runtime image --- apps/api/src/jobs/asset_scanner_job.py | 22 +++++++++++++++++++--- apps/api/tests/test_asset_scanner_job.py | 12 ++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/apps/api/src/jobs/asset_scanner_job.py b/apps/api/src/jobs/asset_scanner_job.py index 939ebc83e..cef3d872f 100644 --- a/apps/api/src/jobs/asset_scanner_job.py +++ b/apps/api/src/jobs/asset_scanner_job.py @@ -1506,11 +1506,27 @@ def _build_domain_tls_inventory_assets( return [*domains.values(), *certificates.values()], relationships +def _domain_tls_inventory_candidates( + *, + cwd: Path | None = None, + source_path: Path | None = None, +) -> tuple[Path, ...]: + relative = Path("docs/security/domain-tls-certbot-inventory.snapshot.json") + source = (source_path or Path(__file__)).resolve() + roots = (cwd or Path.cwd(), *source.parents) + return tuple(dict.fromkeys(root / relative for root in roots)) + + def _load_domain_tls_inventory_payload(path: Path | None = None) -> dict[str, Any]: if path is None: - relative = Path("docs/security/domain-tls-certbot-inventory.snapshot.json") - candidates = (Path.cwd() / relative, Path(__file__).resolve().parents[4] / relative) - path = next((candidate for candidate in candidates if candidate.is_file()), None) + path = next( + ( + candidate + for candidate in _domain_tls_inventory_candidates() + if candidate.is_file() + ), + None, + ) if path is None or not path.is_file(): raise RuntimeError("domain_tls_inventory_snapshot_missing") if path.stat().st_size > 5_000_000: diff --git a/apps/api/tests/test_asset_scanner_job.py b/apps/api/tests/test_asset_scanner_job.py index ab5c03554..8ccb262d9 100644 --- a/apps/api/tests/test_asset_scanner_job.py +++ b/apps/api/tests/test_asset_scanner_job.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio import json +from pathlib import Path from src.jobs import asset_scanner_job @@ -609,6 +610,17 @@ def test_domain_tls_inventory_projects_fingerprints_without_raw_paths() -> None: assert "privkey.pem" not in public_text +def test_domain_tls_inventory_candidates_support_shallow_runtime_paths() -> None: + candidates = asset_scanner_job._domain_tls_inventory_candidates( + cwd=Path("/runtime"), + source_path=Path("/app/src/jobs/asset_scanner_job.py"), + ) + + assert Path( + "/app/docs/security/domain-tls-certbot-inventory.snapshot.json" + ) in candidates + + def test_public_tls_expiry_classification_is_conservative() -> None: now_epoch = 1_700_000_000.0