11import hashlib
22import json
3+ import sys
34from pathlib import Path
5+ from unittest .mock import patch
46
57import pytest
68
7- from cas_reference_product .evidence import DEFAULT_BUNDLE , EvidenceVerificationError , verify_bundle
9+ from cas_reference_product .evidence import (
10+ DEFAULT_BUNDLE ,
11+ EvidenceVerificationError ,
12+ _load_json ,
13+ main ,
14+ verify_bundle ,
15+ )
816
917
1018def copy_bundle (tmp_path : Path ) -> Path :
@@ -22,6 +30,26 @@ def write_json(path: Path, payload: dict[str, object]) -> None:
2230 path .write_text (json .dumps (payload , indent = 2 ) + "\n " , encoding = "utf-8" )
2331
2432
33+ def rebuild_evidence_digests (bundle : Path ) -> None :
34+ descriptor_path = bundle / "bundle.json"
35+ descriptor = json .loads (descriptor_path .read_text (encoding = "utf-8" ))
36+
37+ for section_name in ("sourceProvenance" , "contractRegistry" , "evaluation" , "platformWhatIf" ):
38+ section = descriptor [section_name ]
39+ section ["sha256" ] = hashlib .sha256 ((bundle / section ["path" ]).read_bytes ()).hexdigest ()
40+
41+ artifact_manifest_path = bundle / "artifact-manifest.json"
42+ artifact_manifest = json .loads (artifact_manifest_path .read_text (encoding = "utf-8" ))
43+ manifest_entries = {item ["uri" ]: item for item in artifact_manifest ["artifacts" ]}
44+ for artifact in descriptor ["artifacts" ]:
45+ digest = hashlib .sha256 ((bundle / artifact ["path" ]).read_bytes ()).hexdigest ()
46+ artifact ["sha256" ] = digest
47+ manifest_entries [artifact ["uri" ]]["sha256" ] = digest
48+
49+ write_json (artifact_manifest_path , artifact_manifest )
50+ write_json (descriptor_path , descriptor )
51+
52+
2553def test_committed_immutable_evidence_bundle_verifies () -> None :
2654 verify_bundle ()
2755
@@ -124,3 +152,161 @@ def test_evaluation_response_digest_is_mandatory(tmp_path: Path) -> None:
124152
125153 with pytest .raises (EvidenceVerificationError , match = "evaluation fixture digest mismatch" ):
126154 verify_bundle (bundle )
155+
156+
157+ def test_load_json_rejects_non_object_json (tmp_path : Path ) -> None :
158+ path = tmp_path / "array.json"
159+ path .write_text ("[1, 2, 3]" , encoding = "utf-8" )
160+
161+ with pytest .raises (EvidenceVerificationError , match = "must contain a JSON object" ):
162+ _load_json (path )
163+
164+
165+ def test_bundle_artifacts_must_be_a_non_empty_list (tmp_path : Path ) -> None :
166+ bundle = copy_bundle (tmp_path )
167+ descriptor_path = bundle / "bundle.json"
168+ descriptor = json .loads (descriptor_path .read_text (encoding = "utf-8" ))
169+ descriptor ["artifacts" ] = []
170+ write_json (descriptor_path , descriptor )
171+
172+ with pytest .raises (
173+ EvidenceVerificationError ,
174+ match = "bundle artifacts must be a non-empty list" ,
175+ ):
176+ verify_bundle (bundle )
177+
178+
179+ def test_bundle_artifact_entry_must_be_an_object (tmp_path : Path ) -> None :
180+ bundle = copy_bundle (tmp_path )
181+ descriptor_path = bundle / "bundle.json"
182+ descriptor = json .loads (descriptor_path .read_text (encoding = "utf-8" ))
183+ descriptor ["artifacts" ] = ["not-an-object" ]
184+ write_json (descriptor_path , descriptor )
185+
186+ with pytest .raises (EvidenceVerificationError , match = "bundle artifact entries must be objects" ):
187+ verify_bundle (bundle )
188+
189+
190+ def test_artifact_missing_path_or_sha256_raises (tmp_path : Path ) -> None :
191+ bundle = copy_bundle (tmp_path )
192+ descriptor_path = bundle / "bundle.json"
193+ descriptor = json .loads (descriptor_path .read_text (encoding = "utf-8" ))
194+ descriptor ["artifacts" ] = [{"uri" : "urn:test" , "path" : None , "sha256" : None }]
195+ write_json (descriptor_path , descriptor )
196+
197+ with pytest .raises (EvidenceVerificationError , match = "artifact path and sha256 must be strings" ):
198+ verify_bundle (bundle )
199+
200+
201+ def test_artifact_sha256_with_invalid_pattern_raises (tmp_path : Path ) -> None :
202+ bundle = copy_bundle (tmp_path )
203+ descriptor_path = bundle / "bundle.json"
204+ descriptor = json .loads (descriptor_path .read_text (encoding = "utf-8" ))
205+ descriptor ["artifacts" ] = [
206+ {"uri" : "urn:test" , "path" : "artifact-manifest.json" , "sha256" : "invalid" }
207+ ]
208+ write_json (descriptor_path , descriptor )
209+
210+ with pytest .raises (EvidenceVerificationError , match = "has an invalid SHA-256 digest" ):
211+ verify_bundle (bundle )
212+
213+
214+ def test_artifact_path_traversal_outside_bundle_root_raises (tmp_path : Path ) -> None :
215+ bundle = copy_bundle (tmp_path )
216+ descriptor_path = bundle / "bundle.json"
217+ descriptor = json .loads (descriptor_path .read_text (encoding = "utf-8" ))
218+ descriptor ["artifacts" ] = [
219+ {"uri" : "urn:test" , "path" : "../../outside.json" , "sha256" : "a" * 64 }
220+ ]
221+ write_json (descriptor_path , descriptor )
222+
223+ with pytest .raises (EvidenceVerificationError , match = "escapes the bundle root" ):
224+ verify_bundle (bundle )
225+
226+
227+ def test_invalid_git_sha_in_source_provenance_raises (tmp_path : Path ) -> None :
228+ bundle = copy_bundle (tmp_path )
229+ provenance_path = bundle / "artifacts" / "source-provenance.json"
230+ provenance = json .loads (provenance_path .read_text (encoding = "utf-8" ))
231+ provenance ["repositories" ][0 ]["sha" ] = "not-a-git-sha"
232+ write_json (provenance_path , provenance )
233+ rebuild_evidence_digests (bundle )
234+
235+ with pytest .raises (EvidenceVerificationError , match = "invalid immutable source reference" ):
236+ verify_bundle (bundle )
237+
238+
239+ @pytest .mark .parametrize (
240+ ("field" , "value" , "message" ),
241+ [
242+ ("summary" , {"failed" : 1 , "passed" : 0 , "total" : 1 }, "did not pass exactly one case" ),
243+ ("suiteId" , "wrong-suite" , "unexpected golden path evaluation suite" ),
244+ ],
245+ )
246+ def test_evaluation_metadata_mismatch_raises (
247+ tmp_path : Path , field : str , value : object , message : str
248+ ) -> None :
249+ bundle = copy_bundle (tmp_path )
250+ evaluation_path = bundle / "artifacts" / "eval-evidence.json"
251+ evaluation = json .loads (evaluation_path .read_text (encoding = "utf-8" ))
252+ evaluation [field ] = value
253+ write_json (evaluation_path , evaluation )
254+ rebuild_evidence_digests (bundle )
255+
256+ with pytest .raises (EvidenceVerificationError , match = message ):
257+ verify_bundle (bundle )
258+
259+
260+ def test_evaluation_response_digest_mismatch_raises (tmp_path : Path ) -> None :
261+ bundle = copy_bundle (tmp_path )
262+ evaluation_path = bundle / "artifacts" / "eval-evidence.json"
263+ evaluation = json .loads (evaluation_path .read_text (encoding = "utf-8" ))
264+ evaluation ["evidence" ][0 ]["execution" ]["responseDigest" ] = f"sha256:{ '0' * 64 } "
265+ write_json (evaluation_path , evaluation )
266+ rebuild_evidence_digests (bundle )
267+
268+ with pytest .raises (EvidenceVerificationError , match = "evaluation response digest mismatch" ):
269+ verify_bundle (bundle )
270+
271+
272+ def test_available_container_with_invalid_digest_raises (tmp_path : Path ) -> None :
273+ bundle = copy_bundle (tmp_path )
274+ descriptor_path = bundle / "bundle.json"
275+ descriptor = json .loads (descriptor_path .read_text (encoding = "utf-8" ))
276+ descriptor ["containerImage" ] = {"status" : "available" , "digest" : "invalid" }
277+ write_json (descriptor_path , descriptor )
278+
279+ with pytest .raises (EvidenceVerificationError , match = "requires a valid digest" ):
280+ verify_bundle (bundle )
281+
282+
283+ def test_verification_result_outcome_not_passed_raises (tmp_path : Path ) -> None :
284+ bundle = copy_bundle (tmp_path )
285+ result_path = bundle / "verification-result.json"
286+ result = json .loads (result_path .read_text (encoding = "utf-8" ))
287+ result ["outcome" ] = "failed"
288+ write_json (result_path , result )
289+
290+ with pytest .raises (EvidenceVerificationError , match = "canonical VerificationResult must pass" ):
291+ verify_bundle (bundle )
292+
293+
294+ def test_main_returns_zero_on_valid_bundle () -> None :
295+ with patch .object (sys , "argv" , ["evidence" ]):
296+ assert main () == 0
297+
298+
299+ def test_main_returns_one_on_invalid_bundle_path () -> None :
300+ with patch .object (sys , "argv" , ["evidence" , "/nonexistent/path/bundle" ]):
301+ assert main () == 1
302+
303+
304+ def test_main_returns_one_on_verification_failure (tmp_path : Path ) -> None :
305+ bundle = copy_bundle (tmp_path )
306+ descriptor_path = bundle / "bundle.json"
307+ descriptor = json .loads (descriptor_path .read_text (encoding = "utf-8" ))
308+ descriptor ["platformWhatIf" ]["deploymentClaim" ] = "deployed"
309+ write_json (descriptor_path , descriptor )
310+
311+ with patch .object (sys , "argv" , ["evidence" , str (bundle )]):
312+ assert main () == 1
0 commit comments