Files
dev-workflows/skills/hermaguard/tools/hermaguard-compile-rules/hermaguard-compile-rules.py
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

126 lines
4.4 KiB
Python

#!/usr/bin/env python3
"""
Hermaguard Rule Compiler — Compiles dismissed findings into suppression rules.
Reads the feedback database, identifies patterns that have been repeatedly
dismissed as false positives, and writes rules to hermaguard-rules.yaml.
Usage:
hermaguard-compile-rules --output hermaguard-rules.yaml
hermaguard-compile-rules --output .kensei/hermaguard-rules.yaml --min-dismissals 3
Based on TRACE paper: compiling user corrections into deterministic rules
reduces violation rates from 100% → 2%.
"""
import argparse
import json
import os
import sqlite3
import sys
VERSION = "1.0.0"
DB_PATH = os.environ.get("HERMAGUARD_FEEDBACK_DB", os.path.expanduser("~/.hermes/data/hermaguard-feedback.db"))
def compile_rules(min_dismissals: int = 3) -> list:
"""Query the feedback database for dismissed patterns, return rules list."""
if not os.path.isfile(DB_PATH):
return []
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
# Find patterns: (source_agent, trigger_condition) pairs dismissed 3+ times
rows = conn.execute("""
SELECT
source_agent,
trigger_condition,
file_path,
COUNT(*) as dismissals,
GROUP_CONCAT(DISTINCT finding_id) as finding_ids
FROM findings
WHERE status = 'dismissed-false-positive'
GROUP BY source_agent, trigger_condition
HAVING COUNT(*) >= ?
ORDER BY dismissals DESC
""", (min_dismissals,)).fetchall()
conn.close()
rules = []
for i, r in enumerate(rows):
agent_short = {
"edge-case-hunter": "edge_case",
"adversarial-reviewer": "adversarial",
"blast-radius-integration": "blast_radius",
}.get(r["source_agent"], r["source_agent"].replace("-", "_"))
rules.append({
"rule_id": f"HG-SUPPRESS-{i + 1:03d}",
"pattern": r["trigger_condition"],
"agent": agent_short,
"file_scope": r["file_path"],
"dismissals": r["dismissals"],
"reason": f"Dismissed {r['dismissals']}x as false positive",
"source_ids": r["finding_ids"].split(",") if r["finding_ids"] else [],
})
return rules
def main():
parser = argparse.ArgumentParser(
description="Hermaguard Rule Compiler — generate suppression rules from dismissed findings"
)
parser.add_argument("--output", required=True, help="Output YAML file path")
parser.add_argument("--min-dismissals", type=int, default=3, help="Min dismissals for a pattern to become a rule (default: 3)")
args = parser.parse_args()
rules = compile_rules(args.min_dismissals)
output = {
"version": VERSION,
"generated_at": __import__("datetime").datetime.now().isoformat(),
"rules": rules,
"stats": {
"total_rules": len(rules),
"by_agent": {},
},
}
for r in rules:
agent = r["agent"]
output["stats"]["by_agent"][agent] = output["stats"]["by_agent"].get(agent, 0) + 1
os.makedirs(os.path.dirname(args.output) or ".", exist_ok=True)
# Write YAML manually to avoid requiring PyYAML
with open(args.output, "w") as f:
f.write(f"# Hermaguard Suppression Rules\n")
f.write(f"# Auto-generated by hermaguard-compile-rules v{VERSION}\n")
f.write(f"# Generated: {output['generated_at']}\n")
f.write(f"# Total rules: {len(rules)}\n")
f.write(f"# Source: {DB_PATH}\n\n")
f.write(f"# These patterns have been repeatedly dismissed as false positives.\n")
f.write(f"# Agents skip findings matching these patterns.\n")
f.write(f"# Delete rules to re-enable checking for a pattern.\n\n")
for r in rules:
reason_line = str(r['reason']).replace("\n", " ")
f.write(f"# {reason_line} | files: {r['file_scope']}\n")
f.write(f"- rule_id: {r['rule_id']}\n")
# json.dumps yields a valid double-quoted scalar (YAML is a JSON
# superset here), so quotes/backslashes in the pattern can't break the file.
f.write(f" pattern: {json.dumps(r['pattern'])}\n")
f.write(f" agent: {r['agent']}\n")
f.write(f" dismissals: {r['dismissals']}\n")
f.write(f"\n")
print(f"Compiled {len(rules)} suppression rules from {DB_PATH}")
print(f"Agents: {json.dumps(output['stats']['by_agent'])}")
print(f"Output: {args.output}")
if __name__ == "__main__":
main()