Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions evidence/packager.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,11 +261,17 @@ def create_bundle(inputs: BundleInputs) -> dict[str, Any]:
"evaluations": evaluations,
}

manifest_path = resolve_within_root(manifest_dir, f"{tag}.yaml")
tag_path = Path(tag)
Copy link
Copy Markdown
Contributor Author

@cubic-dev-ai cubic-dev-ai Bot Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic for creating nested directories from namespaced tags is flawed. The implementation in evidence/packager.py incorrectly parses the tag string, and as a result, the assertions in the corresponding test in tests/test_evidence_bundle.py will fail. The code does not produce the intended directory structure.

Prompt for AI agents
Address the following comment on evidence/packager.py at line 264:

<comment>The logic for creating nested directories from namespaced tags is flawed. The implementation in `evidence/packager.py` incorrectly parses the tag string, and as a result, the assertions in the corresponding test in `tests/test_evidence_bundle.py` will fail. The code does not produce the intended directory structure.</comment>

<file context>
@@ -261,11 +261,17 @@ def create_bundle(inputs: BundleInputs) -&gt; dict[str, Any]:
     }
 
-    manifest_path = resolve_within_root(manifest_dir, f&quot;{tag}.yaml&quot;)
+    tag_path = Path(tag)
+    tag_manifest_dir = resolve_within_root(manifest_dir, str(tag_path.parent)) if tag_path.parent != Path(&quot;.&quot;) else manifest_dir
+    tag_bundle_dir = resolve_within_root(bundle_dir, str(tag_path.parent)) if tag_path.parent != Path(&quot;.&quot;) else bundle_dir
</file context>
Fix with Cubic

tag_manifest_dir = resolve_within_root(manifest_dir, str(tag_path.parent)) if tag_path.parent != Path(".") else manifest_dir
tag_bundle_dir = resolve_within_root(bundle_dir, str(tag_path.parent)) if tag_path.parent != Path(".") else bundle_dir
tag_manifest_dir.mkdir(parents=True, exist_ok=True)
tag_bundle_dir.mkdir(parents=True, exist_ok=True)

manifest_path = tag_manifest_dir / f"{tag_path.name}.yaml"
with manifest_path.open("w", encoding="utf-8") as handle:
yaml.safe_dump(manifest, handle, sort_keys=False)

bundle_path = resolve_within_root(bundle_dir, f"{tag}.zip")
bundle_path = tag_bundle_dir / f"{tag_path.name}.zip"
Comment on lines +264 to +274
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve namespace in evidence API

Creating bundles now writes manifests and archives into nested directories based on the tag (e.g., namespace/repo:v1.0.0 ends up under manifests/namespace/repo.yaml and bundles/namespace/repo.zip). The FastAPI evidence routes still glob only manifest_dir/*.yaml and build bundle paths as bundle_dir / f"{tag}.zip", and the /{release} route cannot even accept slashes in the tag. As a result, namespaced bundles generated by this change will never be listed or fetched through the API despite being written to disk. Consider teaching the API to recurse through namespace directories (and using a path parameter for release) so these bundles remain accessible.

Useful? React with 👍 / 👎.

with ZipFile(bundle_path, "w") as archive:
for source, arcname in bundle_files:
archive.write(source, arcname)
Expand Down
59 changes: 58 additions & 1 deletion tests/test_evidence_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
load_policy,
)
from services.evidence.store import EvidenceStore
from evidence.packager import _collect_files
from evidence.packager import _collect_files, create_bundle


def _write_json(path: Path, payload: dict) -> Path:
Expand Down Expand Up @@ -190,6 +190,63 @@ def test_evaluate_policy_warn_and_fail() -> None:
assert evaluations["checks"]["provenance_attestations"]["status"] == "fail"


def test_create_bundle_with_namespaced_tag(tmp_path: Path) -> None:
tag = "namespace/repo:v1.0.0"
normalized = _write_json(
tmp_path / "artifacts/sbom/normalized.json", {"components": []}
)
quality_json = _write_json(
tmp_path / "analysis/sbom_quality_report.json",
{"metrics": {"coverage_percent": 95.0, "license_coverage_percent": 90.0}},
)
quality_html = tmp_path / "reports/sbom_quality_report.html"
quality_html.parent.mkdir(parents=True, exist_ok=True)
quality_html.write_text("<html>quality</html>", encoding="utf-8")
risk_report = _write_json(
tmp_path / "artifacts/risk.json",
{"summary": {"component_count": 2, "cve_count": 1, "max_risk_score": 60.0}},
)
provenance_dir = tmp_path / "artifacts/attestations"
provenance_dir.mkdir(parents=True, exist_ok=True)
(provenance_dir / "build.json").write_text("{}", encoding="utf-8")
repro_attestation = _write_json(
tmp_path / "artifacts/repro/attestations" / f"{tag.replace('/', '_').replace(':', '_')}.json",
{"match": True},
)

inputs = BundleInputs(
tag=tag,
normalized_sbom=normalized,
sbom_quality_json=quality_json,
sbom_quality_html=quality_html,
risk_report=risk_report,
provenance_dir=provenance_dir,
repro_attestation=repro_attestation,
output_dir=tmp_path / "evidence",
)
manifest = create_bundle(inputs)

# Verify the bundle and manifest are created in the namespace directories
bundle_path = Path(manifest["bundle_path"])
manifest_path = Path(manifest["manifest_path"])

# Check that the paths preserve the namespace structure
assert bundle_path.parent.name == "repo"
assert bundle_path.parent.parent.name == "namespace"
assert manifest_path.parent.name == "repo"
assert manifest_path.parent.parent.name == "namespace"

# Check that the files exist
assert bundle_path.is_file()
assert manifest_path.is_file()

# Verify manifest contains the full tag
import yaml
with manifest_path.open("r", encoding="utf-8") as f:
manifest_data = yaml.safe_load(f)
assert manifest_data["tag"] == tag


def test_collect_files_handles_nested_directories(tmp_path: Path) -> None:
extras = tmp_path / "extras"
extras.mkdir()
Expand Down
Loading