"""Tests for scanners/cve_data_schema.py — CVEEntry dataclass + load/save I/O.""" import json from pathlib import Path import pytest from scanners.cve_data_schema import CVEEntry, load_cve_data, save_cve_data # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- def _sample_entry(**overrides) -> CVEEntry: base = dict( cve_id="TEST-2026-001", package="torch", title="Test RCE via torch.load", triggers=["torch.load("], safe_variants=["torch.load(f, weights_only=True)"], severity="ERROR", confidence="confirmed", owasp="A06:2021-Vulnerable_and_Outdated_Components", category="ml-security", remediation="Upgrade torch >= 2.6", affected_versions="<2.6.0", fix_version="2.6.0", references=["https://osv.dev/TEST-2026-001"], ) base.update(overrides) return CVEEntry(**base) # --------------------------------------------------------------------------- # CVEEntry dataclass # --------------------------------------------------------------------------- class TestCVEEntryDataclass: def test_all_fields_accessible(self): e = _sample_entry() assert e.cve_id == "TEST-2026-001" assert e.package == "torch" assert e.triggers == ["torch.load("] assert e.severity == "ERROR" assert e.confidence == "confirmed" def test_references_defaults_to_empty_list(self): e = CVEEntry( cve_id="X", package="p", title="t", triggers=[], safe_variants=[], severity="ERROR", confidence="confirmed", owasp="A06", category="security", remediation="fix", affected_versions="*", fix_version="N/A", ) assert e.references == [] def test_triggers_is_mutable_list(self): e = _sample_entry() e.triggers.append("torch.jit.load(") assert len(e.triggers) == 2 # --------------------------------------------------------------------------- # load_cve_data # --------------------------------------------------------------------------- class TestLoadCVEData: def test_loads_real_data_file(self): entries = load_cve_data() assert len(entries) >= 22 for e in entries: assert isinstance(e, CVEEntry) assert e.cve_id assert e.package assert e.severity in ("ERROR", "WARNING", "INFO") assert e.confidence in ("confirmed", "likely", "possible") def test_returns_empty_for_missing_file(self, tmp_path): result = load_cve_data(tmp_path / "nonexistent.json") assert result == [] def test_loads_from_custom_path(self, tmp_path): p = tmp_path / "cve.json" data = { "version": "1.0", "updated": "2026-01-01T00:00:00Z", "entries": [ { "cve_id": "CUSTOM-001", "package": "numpy", "title": "Custom test", "triggers": ["np.load("], "safe_variants": [], "severity": "WARNING", "confidence": "likely", "owasp": "A06", "category": "security", "remediation": "fix", "affected_versions": "<2.0", "fix_version": "2.0", "references": [], } ], } p.write_text(json.dumps(data), encoding="utf-8") entries = load_cve_data(p) assert len(entries) == 1 assert entries[0].cve_id == "CUSTOM-001" assert entries[0].package == "numpy" def test_ignores_empty_entries_list(self, tmp_path): p = tmp_path / "empty.json" p.write_text(json.dumps({"version": "1.0", "updated": "2026-01-01T00:00:00Z", "entries": []})) assert load_cve_data(p) == [] def test_all_real_entries_have_triggers_list(self): for e in load_cve_data(): assert isinstance(e.triggers, list) def test_all_real_entries_have_references_list(self): for e in load_cve_data(): assert isinstance(e.references, list) # --------------------------------------------------------------------------- # save_cve_data # --------------------------------------------------------------------------- class TestSaveCVEData: def test_roundtrip(self, tmp_path): p = tmp_path / "out.json" entries = [_sample_entry()] save_cve_data(entries, path=p) loaded = load_cve_data(p) assert len(loaded) == 1 assert loaded[0].cve_id == "TEST-2026-001" assert loaded[0].triggers == ["torch.load("] def test_saves_multiple_entries(self, tmp_path): p = tmp_path / "multi.json" entries = [_sample_entry(cve_id=f"TEST-{i:03d}") for i in range(5)] save_cve_data(entries, path=p) loaded = load_cve_data(p) assert len(loaded) == 5 def test_output_is_valid_json(self, tmp_path): p = tmp_path / "valid.json" save_cve_data([_sample_entry()], path=p) raw = json.loads(p.read_text(encoding="utf-8")) assert raw["version"] == "1.0" assert "updated" in raw assert isinstance(raw["entries"], list) def test_custom_updated_timestamp(self, tmp_path): p = tmp_path / "ts.json" save_cve_data([_sample_entry()], path=p, updated="2026-05-21T00:00:00Z") raw = json.loads(p.read_text(encoding="utf-8")) assert raw["updated"] == "2026-05-21T00:00:00Z" def test_auto_updated_timestamp_format(self, tmp_path): p = tmp_path / "auto_ts.json" save_cve_data([_sample_entry()], path=p) raw = json.loads(p.read_text(encoding="utf-8")) # Should be ISO 8601 UTC string ending in Z assert raw["updated"].endswith("Z") def test_overwrites_existing_file(self, tmp_path): p = tmp_path / "overwrite.json" save_cve_data([_sample_entry(cve_id="FIRST")], path=p) save_cve_data([_sample_entry(cve_id="SECOND"), _sample_entry(cve_id="THIRD")], path=p) loaded = load_cve_data(p) assert len(loaded) == 2 assert loaded[0].cve_id == "SECOND" def test_empty_entries_saves_valid_file(self, tmp_path): p = tmp_path / "empty.json" save_cve_data([], path=p) loaded = load_cve_data(p) assert loaded == []