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.
126 lines
4.4 KiB
Python
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()
|