Files
evandrodevbr c17192c9fe feat: vendorizar as 10 skills do pilar de seguranca em skills/
Copia local (nao fork, nao submodule) de safedeps, sca-audit,
secrets-scan, code-review-security, api-security-review,
web-security-review, cve-triage, patch-prioritization,
dependency-scanning e hermaguard -- commit exato de origem e licenca
de cada uma em skills/NOTICE.md. Elimina o passo de clone externo
pro pilar de seguranca do wf-security-review: git clone deste repo
ja traz tudo funcional.

Corrige tambem o comando de instalacao pro Claude Code nos dois
READMEs e no docs/SKILLS.md: Claude Code exige um diretorio por
skill (skills/<nome>/SKILL.md), nao aceita os .md soltos que o
comando antigo copiava -- confirmado testando a instalacao real.
2026-08-24 00:32:20 -03:00

389 lines
17 KiB
Python

"""
Hermaguard Coverage — Test Suite
Tests import and call the real tool. No placebo assertions.
Covers: registry integrity, ledger build, report validation (pass/fail),
language rules, CLI round-trip.
"""
import importlib.util
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parent
def _load_module(name, relpath):
spec = importlib.util.spec_from_file_location(name, ROOT / relpath)
if spec is None or spec.loader is None:
raise ImportError(f"Could not load {relpath}")
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod # required for dataclasses with from __future__ annotations
spec.loader.exec_module(mod)
return mod
COV = _load_module("hermaguard_coverage", "tools/hermaguard-coverage/hermaguard-coverage.py")
def _full_coverage(ledger, finding_class=None):
"""Build a complete coverage array for the given ledger."""
cov = []
for c in ledger["classes"]:
if not c["applicable"]:
cov.append({"class_id": c["id"], "status": "not_applicable", "reason": c.get("reason", "")})
elif c["id"] == finding_class:
cov.append({"class_id": c["id"], "status": "finding"})
else:
cov.append({"class_id": c["id"], "status": "clean"})
return cov
class TestRegistryIntegrity(unittest.TestCase):
def test_self_check_passes(self):
self.assertEqual(COV.self_check(), [])
def test_registry_size(self):
# 25 CWE Top-25 + 3 CWE extras + 4 Hermaguard-native
self.assertEqual(len(COV.CLASSES), 32)
def test_top25_ids_exact(self):
ranked = [c for c in COV.CLASSES if c.get("rank") and 1 <= c["rank"] <= 25]
self.assertEqual(len(ranked), 25)
expected = {"CWE-79","CWE-787","CWE-89","CWE-352","CWE-22","CWE-125","CWE-78",
"CWE-416","CWE-862","CWE-434","CWE-94","CWE-20","CWE-77","CWE-287",
"CWE-269","CWE-502","CWE-200","CWE-863","CWE-918","CWE-119","CWE-476",
"CWE-798","CWE-190","CWE-400","CWE-306"}
self.assertEqual({c["id"] for c in ranked}, expected)
def test_top25_ranks_unique(self):
ranks = [c["rank"] for c in COV.CLASSES if c.get("rank")]
self.assertEqual(sorted(ranks), list(range(1, 26)))
def test_registry_mutation_detected(self):
# Tamper detection: adding a duplicate id must fail self-check
COV.CLASSES.append({"id": "CWE-89", "name": "dup", "status": "always"})
try:
errs = COV.self_check()
self.assertTrue(any("duplicate" in e for e in errs))
finally:
COV.CLASSES.pop()
class TestLedgerBuild(unittest.TestCase):
def test_python_only(self):
led = COV.build_ledger(["src/app.py"])
na = {c["id"] for c in led["classes"] if not c["applicable"]}
# memory-unsafe classes excluded for Python
self.assertIn("CWE-787", na)
self.assertIn("CWE-416", na)
applicable = {c["id"] for c in led["classes"] if c["applicable"]}
self.assertIn("CWE-89", applicable)
self.assertIn("HG-ASYNC", applicable)
def test_c_files_get_memory_classes(self):
led = COV.build_ledger(["driver.c"])
applicable = {c["id"] for c in led["classes"] if c["applicable"]}
self.assertIn("CWE-787", applicable)
self.assertIn("CWE-125", applicable)
self.assertIn("CWE-416", applicable)
def test_rust_gets_memory_classes_as_hint(self):
led = COV.build_ledger(["main.rs"])
by_id = {c["id"]: c for c in led["classes"]}
self.assertTrue(by_id["CWE-787"]["applicable"])
self.assertEqual(by_id["CWE-787"].get("hint"), "Rust: applies via unsafe blocks")
def test_mixed_languages_union(self):
led = COV.build_ledger(["a.py", "b.ts", "c.go"])
applicable = {c["id"] for c in led["classes"] if c["applicable"]}
# union: TS brings nothing memory-unsafe; all still N/A
self.assertNotIn("CWE-787", applicable)
def test_files_sorted_in_ledger(self):
led = COV.build_ledger(["b.py", "a.py"])
self.assertEqual(led["files"], ["a.py", "b.py"])
class TestReportValidation(unittest.TestCase):
def setUp(self):
self.ledger = COV.build_ledger(["src/app.py", "src/handler.ts"])
def test_full_coverage_passes(self):
report = {
"coverage": _full_coverage(self.ledger, finding_class="CWE-89"),
"findings": [{"id": "HG-1", "classes": ["CWE-89"], "file": "app.py",
"severity": "CRITICAL", "trigger_condition": "f-string SQL",
"consequence": "injection"}],
}
result = COV.check_report(report, self.ledger)
self.assertTrue(result["ok"], result["errors"])
self.assertEqual(result["accounted"], result["applicable"])
def test_missing_coverage_entry_fails(self):
cov = [e for e in _full_coverage(self.ledger) if e["class_id"] != "CWE-400"]
result = COV.check_report({"coverage": cov, "findings": []}, self.ledger)
self.assertFalse(result["ok"])
self.assertIn("CWE-400", result["gaps"])
def test_no_coverage_array_fails(self):
result = COV.check_report({"findings": []}, self.ledger)
self.assertFalse(result["ok"])
self.assertIn("no `coverage` array", result["errors"][0])
def test_finding_claim_without_backing_finding_fails(self):
cov = _full_coverage(self.ledger, finding_class="CWE-22")
result = COV.check_report({"coverage": cov, "findings": []}, self.ledger)
self.assertFalse(result["ok"])
self.assertTrue(any("no finding cites this class" in e for e in result["errors"]))
def test_duplicate_class_entry_fails(self):
cov = _full_coverage(self.ledger)
cov.append({"class_id": "CWE-89", "status": "clean"})
result = COV.check_report({"coverage": cov, "findings": []}, self.ledger)
self.assertFalse(result["ok"])
self.assertTrue(any("appears twice" in e for e in result["errors"]))
def test_invalid_status_fails(self):
cov = _full_coverage(self.ledger)
for e in cov:
if e["class_id"] == "HG-STATE":
e["status"] = "maybe"
result = COV.check_report({"coverage": cov, "findings": []}, self.ledger)
self.assertFalse(result["ok"])
self.assertTrue(any("invalid status" in e for e in result["errors"]))
def test_na_without_reason_fails(self):
cov = _full_coverage(self.ledger)
for e in cov:
if e["class_id"] == "CWE-89":
e["status"] = "not_applicable" # demote without reason
e.pop("reason", None)
result = COV.check_report({"coverage": cov, "findings": []}, self.ledger)
self.assertFalse(result["ok"])
self.assertTrue(any("without a reason" in e for e in result["errors"]))
def test_unknown_class_cited_fails(self):
cov = _full_coverage(self.ledger)
cov.append({"class_id": "CWE-9999", "status": "clean"})
result = COV.check_report({"coverage": cov, "findings": []}, self.ledger)
self.assertFalse(result["ok"])
self.assertTrue(any("unknown class" in e for e in result["errors"]))
class TestCLI(unittest.TestCase):
def test_cli_self_check(self):
r = subprocess.run(
[sys.executable, str(ROOT / "tools/hermaguard-coverage/hermaguard-coverage.py"), "--self-check"],
capture_output=True, text=True)
self.assertEqual(r.returncode, 0, r.stderr)
self.assertIn("self-check OK", r.stdout)
def test_cli_round_trip(self):
with tempfile.TemporaryDirectory() as d:
led_path = str(Path(d) / "ledger.json")
rep_ok = str(Path(d) / "ok.json")
rep_bad = str(Path(d) / "bad.json")
r = subprocess.run(
[sys.executable, str(ROOT / "tools/hermaguard-coverage/hermaguard-coverage.py"),
"--files", "a.py,b.ts", "--ledger", led_path],
capture_output=True, text=True)
self.assertEqual(r.returncode, 0, r.stderr)
ledger = json.loads(Path(led_path).read_text())
ok_report = {"coverage": _full_coverage(ledger, "CWE-89"),
"findings": [{"id": "HG-1", "classes": ["CWE-89"]}]}
Path(rep_ok).write_text(json.dumps(ok_report))
bad_cov = [e for e in ok_report["coverage"] if e["class_id"] != "HG-PARTIAL-WRITE"]
Path(rep_bad).write_text(json.dumps({"coverage": bad_cov, "findings": []}))
tool = str(ROOT / "tools/hermaguard-coverage/hermaguard-coverage.py")
r_ok = subprocess.run([sys.executable, tool, "--check-report", rep_ok, "--ledger", led_path, "--quiet"],
capture_output=True, text=True)
self.assertEqual(r_ok.returncode, 0, r_ok.stdout + r_ok.stderr)
r_bad = subprocess.run([sys.executable, tool, "--check-report", rep_bad, "--ledger", led_path, "--quiet"],
capture_output=True, text=True)
self.assertEqual(r_bad.returncode, 1)
self.assertIn("HG-PARTIAL-WRITE", r_bad.stdout)
if __name__ == "__main__":
unittest.main()
def _load_grader():
return _load_module("hermaguard_grader", "tools/hermaguard-grader/hermaguard_grader.py")
class TestGrader(unittest.TestCase):
"""L0-L6 grader — real oracle behaviour, not just self-test passthrough."""
@classmethod
def setUpClass(cls):
cls.g = _load_grader()
def test_self_test_corpus_passes(self):
rc = self.g.self_test()
self.assertEqual(rc, 0)
def test_privilege_axis_gives_l5(self):
f = self.g.grade_finding({"id": "X", "severity": "HIGH",
"harm_axes": {"completed": True, "reversible": False,
"cross_scope": False, "privilege": True}})
self.assertEqual(f["L_level"], "L5")
def test_severity_fallback_never_undergrades(self):
for tier, floor in self.g.SEVERITY_FALLBACK_FLOOR.items():
f = self.g.grade_finding({"id": "X", "severity": tier})
self.assertGreaterEqual(self.g._LEVEL_RANK[f["L_level"]],
self.g._LEVEL_RANK[floor], msg=tier)
self.assertEqual(f["grading_method"], "severity-fallback")
def test_grading_is_additive(self):
original = {"id": "X", "severity": "LOW", "keep": "me"}
f = self.g.grade_finding(dict(original))
self.assertEqual(f["keep"], "me")
for k in original:
self.assertIn(k, f)
def test_escalation_chain_raises_peak_to_l6(self):
report = {"findings": [
{"id": "A", "severity": "HIGH", "harm_axes": {"completed": True, "reversible": True,
"cross_scope": True, "privilege": False}}, # L4
{"id": "B", "severity": "CRITICAL", "harm_axes": {"completed": True, "reversible": False,
"cross_scope": False, "privilege": True}}, # L5
]}
out = self.g.grade_report(report)
by_id = {f["id"]: f for f in out["findings"]}
self.assertEqual(by_id["B"]["L_level"], "L6")
self.assertTrue(out["severity_grading"]["escalation_chain_detected"])
def test_local_chain_not_raised(self):
report = {"findings": [
{"id": "A", "severity": "MEDIUM", "harm_axes": {"completed": True, "reversible": True,
"cross_scope": False, "privilege": False}}, # L2
{"id": "B", "severity": "HIGH", "harm_axes": {"completed": True, "reversible": False,
"cross_scope": False, "privilege": False}}, # L3
]}
out = self.g.grade_report(report)
self.assertFalse(out["severity_grading"]["escalation_chain_detected"])
by_id = {f["id"]: f for f in out["findings"]}
self.assertEqual(by_id["B"]["L_level"], "L3")
def _load_benchmark():
return _load_module("hermaguard_benchmark", "tools/hermaguard-benchmark/benchmark.py")
class TestBenchmarkDeterministicScoring(unittest.TestCase):
"""Class-based matching replaces fuzzy keyword overlap for new ground truth."""
@classmethod
def setUpClass(cls):
cls.bm = _load_benchmark()
cls.bug = {
"id": "999-test", "title": "t", "severity": "HIGH", "category": "security",
"expected_findings": [
{"file": "app.py", "trigger": "x", "description": "x", "severity": "HIGH", "classes": ["CWE-89"]},
],
}
def test_class_match_exact(self):
run = {"run_ok": True, "findings": [
{"id": "HG-1", "file": "app.py", "severity": "HIGH", "classes": ["CWE-89", "CWE-20"]},
]}
s = self.bm.score_run(self.bug, run)
self.assertEqual(s["true_positives"], 1)
self.assertEqual(s["false_positives"], 0)
self.assertEqual(s["match_modes"]["class"], 1)
def test_class_match_requires_file_match(self):
run = {"run_ok": True, "findings": [
{"id": "HG-1", "file": "other.py", "severity": "HIGH", "classes": ["CWE-89"]},
]}
s = self.bm.score_run(self.bug, run)
self.assertEqual(s["true_positives"], 0)
self.assertEqual(s["false_negatives"], 1)
def test_no_class_intersection_no_match(self):
run = {"run_ok": True, "findings": [
{"id": "HG-1", "file": "app.py", "severity": "HIGH", "classes": ["CWE-22"]},
]}
s = self.bm.score_run(self.bug, run)
self.assertEqual(s["true_positives"], 0)
def test_deterministic_identical_inputs(self):
run = {"run_ok": True, "findings": [
{"id": "HG-1", "file": "app.py", "severity": "HIGH", "classes": ["CWE-89"]},
{"id": "HG-2", "file": "app.py", "severity": "MEDIUM", "classes": ["CWE-22"]},
]}
r1 = self.bm.score_run(self.bug, run)
r2 = self.bm.score_run(self.bug, run)
self.assertEqual(r1["matches"], r2["matches"])
self.assertEqual((r1["precision_pct"], r1["recall_pct"]), (r2["precision_pct"], r2["recall_pct"]))
def test_legacy_keyword_fallback_still_works(self):
bug_legacy = {
"id": "998-legacy", "title": "t", "severity": "HIGH", "category": "security",
"expected_findings": [
{"file": "app.py", "trigger": "sql injection unparameterised query", "description": "sqli", "severity": "CRITICAL"},
],
}
run = {"run_ok": True, "findings": [
{"id": "HG-1", "file": "app.py", "severity": "CRITICAL",
"trigger_condition": "sql injection unparameterised query", "consequence": "data breach"},
]}
s = self.bm.score_run(bug_legacy, run)
self.assertEqual(s["true_positives"], 1)
self.assertEqual(s["match_modes"]["keyword"], 1)
def test_crashed_run_never_scored_ok(self):
# Regression: a result file with an error key must not score as OK
run = {"error": "agent crashed", "findings": []}
s = self.bm.score_run(self.bug, run)
self.assertFalse(s["run_ok"])
def test_missing_run_ok_flag_does_not_crash(self):
# Regression: result files predating the run_ok flag previously
# raised KeyError inside score_run, killing the whole benchmark.
run = {"findings": [
{"id": "HG-1", "file": "app.py", "severity": "HIGH", "classes": ["CWE-89"]},
]}
s = self.bm.score_run(self.bug, run)
self.assertTrue(s["run_ok"]) # defaults to completed
self.assertEqual(s["true_positives"], 1)
def test_real_corpus_scoring_perfect_and_zero(self):
import pathlib
bugs_dir = pathlib.Path("tools/hermaguard-benchmark/bugs")
# Perfect run against real ground truth -> 100/100/100
gt = json.loads((bugs_dir / "001-sql-injection/ground-truth.json").read_text())
bug = {"id": "001", "title": gt["title"], "severity": gt["severity"],
"category": gt["category"], "expected_findings": gt["findings"]}
exp = gt["findings"][0]
run = {"findings": [{"id": "HG-1", "file": exp["file"],
"severity": exp["severity"], "classes": exp["classes"]}]}
s = self.bm.score_run(bug, run)
self.assertEqual((s["precision_pct"], s["recall_pct"], s["f1_score"]), (100.0, 100.0, 100.0))
# Wrong-class run -> 0
run2 = {"findings": [{"id": "HG-1", "file": exp["file"],
"severity": exp["severity"], "classes": ["CWE-79"]}]}
s2 = self.bm.score_run(bug, run2)
self.assertEqual((s2["precision_pct"], s2["recall_pct"], s2["f1_score"]), (0.0, 0.0, 0.0))
def test_corpus_integrity(self):
import pathlib
bugs_dir = pathlib.Path("tools/hermaguard-benchmark/bugs")
dirs = sorted([p for p in bugs_dir.iterdir() if p.is_dir()])
self.assertEqual(len(dirs), 25, f"expected 25 bugs, found {len(dirs)}")
for d in dirs:
gt = json.loads((d / "ground-truth.json").read_text())
self.assertTrue(gt["findings"], f"{d.name}: no findings in ground truth")
for f in gt["findings"]:
self.assertIn("classes", f, f"{d.name}: ground truth missing classes")