|
25 | 25 | SHEET = os.path.join(HERE, "expected-findings.yaml") |
26 | 26 | RULES_YAML = os.path.join(SCANNER, "rules", "dsgai-rules.yaml") |
27 | 27 | RULES_JSON = os.path.join(SCANNER, "rules", "dsgai-rules.json") |
| 28 | +RULES_SCHEMA = os.path.join(SCANNER, "rules", "rules.schema.json") |
| 29 | +SCAN_SCHEMA = os.path.join(SCANNER, "schemas", "dsgai-scan.schema.json") |
28 | 30 | BANNED_FIELDS = {"match_text", "content", "value", "raw_grep_output"} |
29 | 31 |
|
30 | 32 |
|
@@ -67,11 +69,24 @@ def test_all_pcres_compile(): |
67 | 69 | for r in _rules(): |
68 | 70 | p = subprocess.run([rg, "--pcre2", "-q", "-e", r["pcre"]], |
69 | 71 | input="x\n", capture_output=True, text=True) |
70 | | - if p.returncode >= 2 and "regex parse error" in p.stderr: |
71 | | - bad.append(r["id"]) |
| 72 | + # rg exit codes: 0 match, 1 no match, >=2 error (incl. any regex/PCRE2 |
| 73 | + # compile failure — the message differs between engines, so key on the |
| 74 | + # exit code, not a substring). |
| 75 | + if p.returncode >= 2: |
| 76 | + bad.append((r["id"], p.stderr.strip()[:120])) |
72 | 77 | assert not bad, f"PCREs failed to compile: {bad}" |
73 | 78 |
|
74 | 79 |
|
| 80 | +@requires_rg |
| 81 | +def test_compile_check_catches_a_broken_pattern(): |
| 82 | + """Guard the guard: a deliberately invalid PCRE must be detected, so a |
| 83 | + corrupted rule really does fail CI.""" |
| 84 | + rg = _rg() |
| 85 | + p = subprocess.run([rg, "--pcre2", "-q", "-e", "(unterminated[class"], |
| 86 | + input="x\n", capture_output=True, text=True) |
| 87 | + assert p.returncode >= 2 |
| 88 | + |
| 89 | + |
75 | 90 | @requires_rg |
76 | 91 | def test_scan_matches_sheet_exactly(scan, sheet): |
77 | 92 | def key(f): |
@@ -144,6 +159,53 @@ def test_value_bearing_execution_always_replaces(): |
144 | 159 | assert "--replace" in window, "value-bearing branch does not pass --replace" |
145 | 160 |
|
146 | 161 |
|
| 162 | +@requires_rg |
| 163 | +def test_checkpoint_validates_against_schema(scan): |
| 164 | + jsonschema = pytest.importorskip("jsonschema") |
| 165 | + schema = json.loads(open(SCAN_SCHEMA, encoding="utf-8").read()) |
| 166 | + jsonschema.validate(scan["json"], schema) |
| 167 | + |
| 168 | + |
| 169 | +def test_scan_schema_rejects_match_text(): |
| 170 | + """The redaction guarantee is machine-checkable: a finding carrying match |
| 171 | + content must be rejected by the checkpoint schema.""" |
| 172 | + jsonschema = pytest.importorskip("jsonschema") |
| 173 | + schema = json.loads(open(SCAN_SCHEMA, encoding="utf-8").read()) |
| 174 | + bad = { |
| 175 | + "schema_version": "1.0", "ruleset_version": "0.3.0", "skill_version": "0.3.0", |
| 176 | + "framework": "dsgai-2026-v1.0", "engine": "deterministic-cli", |
| 177 | + "git_commit": None, "scanned_at": "2023-11-14T22:13:20+00:00", |
| 178 | + "scan_scope": ".", "obfuscation": "strict", "controls": {}, |
| 179 | + "findings": [{ |
| 180 | + "control": "DSGAI02", "rule_id": "P02.1", "path": "config.py", |
| 181 | + "line": 7, "status": "fail", "classification": "value_bearing", |
| 182 | + "match_text": "sk-proj-LEAKED", |
| 183 | + }], |
| 184 | + "cves": [], "file_map_ref": None, |
| 185 | + } |
| 186 | + with pytest.raises(jsonschema.ValidationError): |
| 187 | + jsonschema.validate(bad, schema) |
| 188 | + |
| 189 | + |
| 190 | +def test_rules_validate_against_schema(): |
| 191 | + jsonschema = pytest.importorskip("jsonschema") |
| 192 | + rules = yaml.safe_load(open(RULES_YAML, encoding="utf-8")) |
| 193 | + schema = json.loads(open(RULES_SCHEMA, encoding="utf-8").read()) |
| 194 | + jsonschema.validate(rules, schema) |
| 195 | + |
| 196 | + |
| 197 | +def test_cli_self_guard_rejects_leaked_finding(): |
| 198 | + """The CLI's runtime (stdlib) self-check must refuse to write a checkpoint |
| 199 | + whose finding carries match content — independent of jsonschema.""" |
| 200 | + sys.path.insert(0, os.path.join(SCANNER, "cli")) |
| 201 | + import dsgai_scan |
| 202 | + cp = {k: v for k, v in zip(dsgai_scan.CHECKPOINT_REQUIRED, |
| 203 | + [None] * len(dsgai_scan.CHECKPOINT_REQUIRED))} |
| 204 | + cp["findings"] = [{"rule_id": "P02.1", "path": "x", "match_text": "sk-LEAK"}] |
| 205 | + with pytest.raises(ValueError): |
| 206 | + dsgai_scan.self_validate_checkpoint(cp) |
| 207 | + |
| 208 | + |
147 | 209 | def test_rules_json_in_sync(): |
148 | 210 | from_yaml = yaml.safe_load(open(RULES_YAML, encoding="utf-8")) |
149 | 211 | rebuilt = json.dumps(from_yaml, indent=2, sort_keys=True, ensure_ascii=False) + "\n" |
|
0 commit comments