| """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
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
| 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"))
|
|
|
| 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 == []
|
|
|