torusguard 1.3.6 → 2.0.0-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.torusguard/.manifest.json +9 -8
- package/.torusguard/auth.json +5 -0
- package/.torusguard/rules_catalog.json +242 -518
- package/.torusguard/runs/report-latest.html +3529 -364
- package/.torusguard/runs/run-20260918-105940-audit/manifest.json +2 -1
- package/.torusguard/runs/run-20260918-105940-audit/remediation.md +7 -0
- package/.torusguard/runs/run-20260919-145701-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260919-145701-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260919-145701-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260919-145701-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260919-151729-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260919-151729-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260919-151729-audit/manifest.json +13 -0
- package/.torusguard/runs/run-20260919-151729-audit/remediation.md +7 -0
- package/.torusguard/runs/run-20260919-151729-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260919-152340-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260919-152340-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260919-152340-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260919-152340-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260919-152345-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260919-152345-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260919-152345-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260919-152345-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260919-153500-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260919-153500-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260919-153500-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260919-153500-audit/summary.md +5 -0
- package/.torusguard/scripts/__pycache__/apply_runner.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/harden_runner.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/html_reporter.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/manifest_builder.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/recipes_runner.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/report_sync.cpython-314.pyc +0 -0
- package/.torusguard/scripts/apply_runner.py +101 -8
- package/.torusguard/scripts/audit_runner.py +116 -11
- package/.torusguard/scripts/harden_runner.py +122 -68
- package/.torusguard/scripts/html_reporter.py +1170 -253
- package/.torusguard/scripts/manifest_builder.py +2 -0
- package/.torusguard/scripts/recheck_runner.py +2 -1
- package/.torusguard/scripts/recipes_runner.py +91 -8
- package/.torusguard/scripts/report_sync.py +134 -7
- package/.torusguard/scripts/status_runner.py +267 -0
- package/.torusguard/snapshots/20260921-182638/go.mod.bak +11 -0
- package/.torusguard/snapshots/20260921-185213/file.go.bak +5 -0
- package/README.md +314 -364
- package/bin/torusguard.js +190 -20
- package/package.json +6 -2
- package/skills/torusguard/__pycache__/bootstrap.cpython-314.pyc +0 -0
- package/skills/torusguard/bootstrap.py +45 -6
- package/skills/torusguard/payload/.manifest.json +29 -14
- package/skills/torusguard/payload/TORUSGUARD.md +145 -145
- package/skills/torusguard/payload/agents/auditor.md +41 -41
- package/skills/torusguard/payload/agents/profiler.md +49 -49
- package/skills/torusguard/payload/agents/remediator.md +41 -41
- package/skills/torusguard/payload/agents/reviewer.md +39 -39
- package/skills/torusguard/payload/agents/validator.md +46 -46
- package/skills/torusguard/payload/config/scope.json +24 -24
- package/skills/torusguard/payload/references/csharp-security.md +41 -41
- package/skills/torusguard/payload/references/go-security.md +41 -41
- package/skills/torusguard/payload/references/java-security.md +40 -40
- package/skills/torusguard/payload/references/polyglot-security-matrix.md +25 -25
- package/skills/torusguard/payload/references/rust-security.md +40 -40
- package/skills/torusguard/payload/rules/custom/.gitkeep +1 -1
- package/skills/torusguard/payload/rules/custom/README.md +30 -30
- package/skills/torusguard/payload/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/html_reporter.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/report_sync.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/sarif_exporter.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/term_ui.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/apply_runner.py +101 -8
- package/skills/torusguard/payload/scripts/audit_runner.py +116 -11
- package/skills/torusguard/payload/scripts/harden_runner.py +122 -68
- package/skills/torusguard/payload/scripts/html_reporter.py +1170 -253
- package/skills/torusguard/payload/scripts/manifest_builder.py +2 -0
- package/skills/torusguard/payload/scripts/recheck_runner.py +2 -1
- package/skills/torusguard/payload/scripts/recipes_runner.py +91 -8
- package/skills/torusguard/payload/scripts/report_sync.py +134 -7
- package/skills/torusguard/payload/scripts/rules_sync.py +321 -321
- package/skills/torusguard/payload/scripts/safety_gate.py +64 -64
- package/skills/torusguard/payload/scripts/status_runner.py +267 -0
- package/skills/torusguard/payload/skills/torusguard/references/csharp-security.md +41 -41
- package/skills/torusguard/payload/skills/torusguard/references/go-security.md +41 -41
- package/skills/torusguard/payload/skills/torusguard/references/java-security.md +40 -40
- package/skills/torusguard/payload/skills/torusguard/references/polyglot-security-matrix.md +25 -25
- package/skills/torusguard/payload/skills/torusguard/references/rust-security.md +40 -40
- package/skills/torusguard/payload/templates/audit-report.template.md +54 -54
- package/skills/torusguard/payload/templates/authorization.template.md +34 -34
- package/skills/torusguard/payload/templates/finding-card.template.md +32 -32
- package/skills/torusguard/payload/templates/remediation-bundle.template.md +35 -35
- package/skills/torusguard/payload/workflows/apply.md +9 -1
- package/skills/torusguard/payload/workflows/audit.md +8 -2
- package/skills/torusguard/payload/workflows/harden.md +6 -1
- package/skills/torusguard/payload/workflows/init.md +7 -1
- package/skills/torusguard/payload/workflows/recipes.md +55 -0
- package/skills/torusguard/payload/workflows/report.md +59 -55
- package/skills/torusguard/payload/workflows/status.md +4 -1
- package/skills/torusguard/payload/workflows/torusguard-apply.md +110 -0
- package/skills/torusguard/payload/workflows/torusguard-audit.md +105 -0
- package/skills/torusguard/payload/workflows/torusguard-authorize.md +96 -0
- package/skills/torusguard/payload/workflows/torusguard-exploit-check.md +98 -0
- package/skills/torusguard/payload/workflows/torusguard-harden.md +104 -0
- package/skills/torusguard/payload/workflows/torusguard-init.md +106 -0
- package/skills/torusguard/payload/workflows/torusguard-recheck.md +101 -0
- package/skills/torusguard/payload/workflows/torusguard-recipes.md +55 -0
- package/skills/torusguard/payload/workflows/torusguard-report.md +105 -0
- package/skills/torusguard/payload/workflows/torusguard-status.md +103 -0
- package/skills/torusguard/payload/workflows/torusguard-verify.md +100 -0
- package/skills/torusguard/payload/workflows/torusguard-web-validate.md +96 -0
- package/skills/torusguard/payload/workflows/torusguard.md +19 -0
|
@@ -233,11 +233,12 @@ def execute_recheck(target_root: Path, run_id_arg: Optional[str] = None) -> Dict
|
|
|
233
233
|
def main():
|
|
234
234
|
parser = argparse.ArgumentParser(description="TorusGuard Differential Recheck Engine")
|
|
235
235
|
parser.add_argument("path", nargs="?", default=".", help="Target project root directory")
|
|
236
|
+
parser.add_argument("--target", "-t", help="Target project root directory (alias for path)")
|
|
236
237
|
parser.add_argument("--run", "-r", help="Explicit run ID to recheck")
|
|
237
238
|
parser.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
238
239
|
args = parser.parse_args()
|
|
239
240
|
|
|
240
|
-
target = Path(args.path).resolve()
|
|
241
|
+
target = Path(args.target or args.path).resolve()
|
|
241
242
|
result = execute_recheck(target, run_id_arg=args.run)
|
|
242
243
|
|
|
243
244
|
if args.json:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
TorusGuard Golden Fix Recipe Explorer (v1.
|
|
3
|
+
TorusGuard Golden Fix Recipe Explorer (v1.4.0)
|
|
4
4
|
Inspect, list, and export reusable, verified AST remediation code snippets
|
|
5
5
|
distilled from passing security patches and persistent memory patterns.
|
|
6
6
|
Standardized 75-column terminal UI formatting.
|
|
@@ -47,7 +47,7 @@ def box_line(content: str, width: int = 67, border: str = "│", border_color: s
|
|
|
47
47
|
return term_ui.format_box_line(content, width=width, border=border, border_color=border_color)
|
|
48
48
|
return f" {border_color}{border}{RESET} {content}"
|
|
49
49
|
|
|
50
|
-
def box_header(title: str, subtitle: str = "", version: str = "v1.
|
|
50
|
+
def box_header(title: str, subtitle: str = "", version: str = "v1.4.0", border_color: str = CYAN) -> str:
|
|
51
51
|
if term_ui:
|
|
52
52
|
return term_ui.card_header(title, subtitle, version, border_color)
|
|
53
53
|
return f"=== {title} ({version}) ==="
|
|
@@ -63,28 +63,100 @@ def border_bottom(border_color: str = CYAN, double: bool = False) -> str:
|
|
|
63
63
|
return "└" + "─" * 71 + "┘"
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
def list_recipes(target_root: Path, detail_id: Optional[str] = None, json_output: bool = False) -> None:
|
|
66
|
+
def list_recipes(target_root: Path, detail_id: Optional[str] = None, search_query: Optional[str] = None, export_path: Optional[str] = None, apply_id: Optional[str] = None, json_output: bool = False) -> None:
|
|
67
67
|
patterns_file = target_root / ".torusguard" / "memory" / "patterns.json"
|
|
68
68
|
recipes: List[Dict[str, Any]] = []
|
|
69
69
|
|
|
70
70
|
if patterns_file.is_file():
|
|
71
71
|
try:
|
|
72
72
|
patterns = json.loads(patterns_file.read_text(encoding="utf-8"))
|
|
73
|
-
|
|
73
|
+
if isinstance(patterns, list):
|
|
74
|
+
recipes = [p for p in patterns if p.get("pattern_type") == "golden_fix_recipe"]
|
|
75
|
+
elif isinstance(patterns, dict) and "patterns" in patterns:
|
|
76
|
+
recipes = [p for p in patterns["patterns"] if p.get("pattern_type") == "golden_fix_recipe"]
|
|
74
77
|
except Exception:
|
|
75
78
|
recipes = []
|
|
76
79
|
|
|
80
|
+
# 1. Search filtering
|
|
81
|
+
if search_query:
|
|
82
|
+
sq = search_query.lower()
|
|
83
|
+
recipes = [
|
|
84
|
+
r for r in recipes
|
|
85
|
+
if sq in r.get("rule_id", "").lower()
|
|
86
|
+
or sq in r.get("recipe_id", "").lower()
|
|
87
|
+
or sq in r.get("description", "").lower()
|
|
88
|
+
or sq in r.get("recipe_data", {}).get("diff_snippet", "").lower()
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
# 2. Export requested
|
|
92
|
+
if export_path:
|
|
93
|
+
out_p = Path(export_path)
|
|
94
|
+
if not out_p.is_absolute():
|
|
95
|
+
out_p = (target_root / out_p).resolve()
|
|
96
|
+
out_p.parent.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
if out_p.suffix.lower() == ".json":
|
|
98
|
+
out_p.write_text(json.dumps(recipes, indent=2), encoding="utf-8")
|
|
99
|
+
else:
|
|
100
|
+
md_lines = [
|
|
101
|
+
"# TorusGuard Golden Fix Recipes Catalog",
|
|
102
|
+
f"- Total Recipes: {len(recipes)}",
|
|
103
|
+
f"- Exported From: {target_root}",
|
|
104
|
+
""
|
|
105
|
+
]
|
|
106
|
+
for r in recipes:
|
|
107
|
+
md_lines.append(f"## [{r.get('rule_id')}] {r.get('recipe_id')}")
|
|
108
|
+
md_lines.append(f"- **Description:** {r.get('description')}")
|
|
109
|
+
rdata = r.get("recipe_data", {})
|
|
110
|
+
md_lines.append(f"- **Verified Count:** {rdata.get('verified_count', 1)}x")
|
|
111
|
+
md_lines.append("```diff")
|
|
112
|
+
md_lines.append(rdata.get("diff_snippet", ""))
|
|
113
|
+
md_lines.append("```\n")
|
|
114
|
+
out_p.write_text("\n".join(md_lines), encoding="utf-8")
|
|
115
|
+
print(f"\n {GREEN}✔ Exported {len(recipes)} golden recipe(s) to:{RESET} {CYAN}{out_p}{RESET}\n")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
# 3. Apply inspection requested
|
|
119
|
+
if apply_id:
|
|
120
|
+
target_rec = next((r for r in recipes if r.get("recipe_id") == apply_id or apply_id in r.get("recipe_id", "")), None)
|
|
121
|
+
if not target_rec:
|
|
122
|
+
print(f"\n {RED}✖ Recipe '{apply_id}' not found in golden recipe memory.{RESET}\n")
|
|
123
|
+
return
|
|
124
|
+
rdata = target_rec.get("recipe_data", {})
|
|
125
|
+
print()
|
|
126
|
+
print(box_header("🛡️ GOLDEN RECIPE APPLICATION PREVIEW", f"Recipe ID: {target_rec.get('recipe_id')}", "v1.4.0"))
|
|
127
|
+
print()
|
|
128
|
+
print(border_top("Recipe Metadata"))
|
|
129
|
+
print(box_line(f"Rule ID: {YELLOW}{target_rec.get('rule_id')}{RESET}"))
|
|
130
|
+
print(box_line(f"Description: {WHITE}{target_rec.get('description')}{RESET}"))
|
|
131
|
+
metrics = rdata.get("ponytail_metrics", {})
|
|
132
|
+
print(box_line(f"Ponytail: {GREEN}+{metrics.get('additions', 0)} / -{metrics.get('deletions', 0)} lines{RESET}"))
|
|
133
|
+
print(border_bottom())
|
|
134
|
+
print()
|
|
135
|
+
print(f" {BOLD}Diff Projection:{RESET}")
|
|
136
|
+
print(" ```diff")
|
|
137
|
+
for line in rdata.get("diff_snippet", "").splitlines():
|
|
138
|
+
if line.startswith("+"):
|
|
139
|
+
print(f" {GREEN}{line}{RESET}")
|
|
140
|
+
elif line.startswith("-"):
|
|
141
|
+
print(f" {RED}{line}{RESET}")
|
|
142
|
+
else:
|
|
143
|
+
print(f" {DIM}{line}{RESET}")
|
|
144
|
+
print(" ```")
|
|
145
|
+
print(f"\n {DIM}To apply automatically across all matching findings, run: {CYAN}npx torusguard harden && npx torusguard apply{RESET}\n")
|
|
146
|
+
return
|
|
147
|
+
|
|
77
148
|
if json_output:
|
|
78
149
|
print(json.dumps(recipes, indent=2))
|
|
79
150
|
return
|
|
80
151
|
|
|
81
152
|
# Header Card
|
|
82
153
|
print()
|
|
83
|
-
|
|
154
|
+
sub_title = f"Filtered by '{search_query}'" if search_query else "Distilled Ponytail Fixes (<= 35 add, <= 25 del) in Persistent Memory"
|
|
155
|
+
print(box_header("🛡️ TORUSGUARD GOLDEN FIX RECIPES", sub_title, "v1.4.0"))
|
|
84
156
|
print()
|
|
85
157
|
|
|
86
158
|
if not recipes:
|
|
87
|
-
print(f" {YELLOW}ℹ No golden fix recipes
|
|
159
|
+
print(f" {YELLOW}ℹ No golden fix recipes found matching query.{RESET}")
|
|
88
160
|
print(f" Run {CYAN}npx torusguard harden{RESET} followed by {CYAN}npx torusguard apply{RESET} to distill verified recipes.\n")
|
|
89
161
|
return
|
|
90
162
|
|
|
@@ -125,12 +197,23 @@ def list_recipes(target_root: Path, detail_id: Optional[str] = None, json_output
|
|
|
125
197
|
def main():
|
|
126
198
|
parser = argparse.ArgumentParser(description="TorusGuard Golden Fix Recipe Explorer")
|
|
127
199
|
parser.add_argument("path", nargs="?", default=".", help="Target project root directory")
|
|
200
|
+
parser.add_argument("--target", "-t", help="Target project root directory (alias for path)")
|
|
128
201
|
parser.add_argument("--detail", "-d", help="Inspect specific recipe ID")
|
|
202
|
+
parser.add_argument("--search", "-s", help="Filter recipes by rule, keyword, or language")
|
|
203
|
+
parser.add_argument("--export", "-e", help="Export recipes to JSON or Markdown file path")
|
|
204
|
+
parser.add_argument("--apply", "-a", help="Project and display application diff for specific recipe ID")
|
|
129
205
|
parser.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
130
206
|
args = parser.parse_args()
|
|
131
207
|
|
|
132
|
-
target = Path(args.path).resolve()
|
|
133
|
-
list_recipes(
|
|
208
|
+
target = Path(args.target or args.path).resolve()
|
|
209
|
+
list_recipes(
|
|
210
|
+
target,
|
|
211
|
+
detail_id=args.detail,
|
|
212
|
+
search_query=args.search,
|
|
213
|
+
export_path=args.export,
|
|
214
|
+
apply_id=args.apply,
|
|
215
|
+
json_output=args.json
|
|
216
|
+
)
|
|
134
217
|
|
|
135
218
|
|
|
136
219
|
if __name__ == "__main__":
|
|
@@ -184,9 +184,9 @@ def parse_existing_report(report_path: Path) -> Dict[str, Any]:
|
|
|
184
184
|
|
|
185
185
|
# ── Format B (new v2): ### TG-RULE-001 · Title `status` `confidence` ──
|
|
186
186
|
card_pattern_v2 = re.compile(
|
|
187
|
-
r'###\s+(TG-[A-Z]+-\d+)\s+·\s+(.*?)\s+`([^`]+)`\s+`([^`]+)
|
|
188
|
-
r'
|
|
189
|
-
r'(?:📁|- \*\*Target:\*\*)\s
|
|
187
|
+
r'###\s+(TG-[A-Z]+-\d+)\s+·\s+(.*?)\s+`([^`]+)`\s+`([^`]+)`[\r\n]+'
|
|
188
|
+
r'-\s+\*\*Finding ID:\*\*\s+`([^`]+)`(?:[^\r\n]*\*\*Severity:\*\*\s+`([^`]+)`)?(?:[^\r\n]*\*\*Cluster:\*\*\s+`([^`]+)`)?[^\r\n]*[\r\n]+'
|
|
189
|
+
r'(?:📁|- \*\*Target:\*\*)\s*`?([^\r\n`]+)`?(?:[\r\n]+\*\*Problem:\*\*\s+([^\r\n]+))?',
|
|
190
190
|
re.MULTILINE
|
|
191
191
|
)
|
|
192
192
|
|
|
@@ -197,7 +197,9 @@ def parse_existing_report(report_path: Path) -> Dict[str, Any]:
|
|
|
197
197
|
confidence = m.group(4).strip()
|
|
198
198
|
finding_id = m.group(5).strip()
|
|
199
199
|
sev_explicit = m.group(6).strip() if m.group(6) else None
|
|
200
|
-
|
|
200
|
+
cluster_explicit = m.group(7).strip() if m.group(7) else None
|
|
201
|
+
target = m.group(8).strip()
|
|
202
|
+
problem = m.group(9).strip() if m.group(9) else None
|
|
201
203
|
|
|
202
204
|
if finding_id not in findings: # Don't overwrite legacy matches
|
|
203
205
|
if sev_explicit:
|
|
@@ -220,6 +222,8 @@ def parse_existing_report(report_path: Path) -> Dict[str, Any]:
|
|
|
220
222
|
"severity": severity,
|
|
221
223
|
"confidence": confidence,
|
|
222
224
|
"target": target,
|
|
225
|
+
"cluster": cluster_explicit or "cluster-general",
|
|
226
|
+
"description": problem or "Security violation detected.",
|
|
223
227
|
"history": []
|
|
224
228
|
}
|
|
225
229
|
|
|
@@ -257,6 +261,37 @@ def parse_existing_report(report_path: Path) -> Dict[str, Any]:
|
|
|
257
261
|
"details": "Restored from inline history"
|
|
258
262
|
})
|
|
259
263
|
|
|
264
|
+
# Enrich findings from run history if available
|
|
265
|
+
runs_dir = report_path.parent / ".torusguard" / "runs"
|
|
266
|
+
run_findings_map: Dict[str, Any] = {}
|
|
267
|
+
if runs_dir.is_dir():
|
|
268
|
+
for d in sorted(runs_dir.iterdir()):
|
|
269
|
+
if d.is_dir() and (d / "findings.json").is_file():
|
|
270
|
+
try:
|
|
271
|
+
f_items = json.loads((d / "findings.json").read_text(encoding="utf-8"))
|
|
272
|
+
for it in f_items:
|
|
273
|
+
if isinstance(it, dict) and it.get("finding_id"):
|
|
274
|
+
run_findings_map[it["finding_id"]] = it
|
|
275
|
+
except Exception:
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
for fid, f in findings.items():
|
|
279
|
+
if fid in run_findings_map:
|
|
280
|
+
rf = run_findings_map[fid]
|
|
281
|
+
if not f.get("cluster") or f.get("cluster") == "cluster-general":
|
|
282
|
+
f["cluster"] = rf.get("cluster") or "cluster-general"
|
|
283
|
+
if not f.get("category"):
|
|
284
|
+
f["category"] = rf.get("category") or "general"
|
|
285
|
+
if not f.get("target") or "\n" in f.get("target", "") or "**Problem:**" in f.get("target", "") or f.get("target") == "unknown":
|
|
286
|
+
f["target"] = f"{rf.get('file_path', 'unknown')}:{rf.get('line_number', 1)}"
|
|
287
|
+
if not f.get("description") or f.get("description") == "Security violation detected.":
|
|
288
|
+
f["description"] = rf.get("description") or "Security invariant check failed."
|
|
289
|
+
if not f.get("snippet"):
|
|
290
|
+
f["snippet"] = rf.get("snippet") or rf.get("matched_text", "")
|
|
291
|
+
if not f.get("file_path"):
|
|
292
|
+
f["file_path"] = rf.get("file_path", "")
|
|
293
|
+
if not f.get("line_number"):
|
|
294
|
+
f["line_number"] = rf.get("line_number", 1)
|
|
260
295
|
|
|
261
296
|
return {"findings": findings, "raw_content": content}
|
|
262
297
|
|
|
@@ -384,6 +419,83 @@ def render_report(findings: List[Dict[str, Any]], target_root: Path) -> str:
|
|
|
384
419
|
lines.append(f"> ⚠️ **Action Required:** {crit_open} critical finding{'s' if crit_open != 1 else ''} require immediate attention. Run `npx torusguard harden` to generate patches.")
|
|
385
420
|
lines.append("")
|
|
386
421
|
|
|
422
|
+
# ── Governed Remediation & Problem-Solved Velocity Table ──
|
|
423
|
+
crit_resolved = max(0, crit_count - crit_open)
|
|
424
|
+
high_resolved = max(0, high_count - high_open)
|
|
425
|
+
med_resolved = max(0, med_count - med_open)
|
|
426
|
+
low_resolved = max(0, low_count)
|
|
427
|
+
|
|
428
|
+
crit_pct = int((crit_resolved / crit_count) * 100) if crit_count > 0 else 100
|
|
429
|
+
high_pct = int((high_resolved / high_count) * 100) if high_count > 0 else 100
|
|
430
|
+
med_pct = int((med_resolved / med_count) * 100) if med_count > 0 else 100
|
|
431
|
+
overall_pct = int((resolved_count / total) * 100) if total > 0 else 100
|
|
432
|
+
|
|
433
|
+
lines.extend([
|
|
434
|
+
"### ⚡ Governed Remediation & Problem-Solved Velocity",
|
|
435
|
+
"",
|
|
436
|
+
"| Severity Layer | Total Discovered | Remediated & Hardened | Active Exposure | Closure Rate | Status |",
|
|
437
|
+
"| :--- | :---: | :---: | :---: | :---: | :---: |",
|
|
438
|
+
f"| **Critical** | {crit_count} | {crit_resolved} | {crit_open} | {crit_pct}% | {'🟢 100% DEFENDED' if crit_open == 0 else '🔴 ACTION REQUIRED'} |",
|
|
439
|
+
f"| **High** | {high_count} | {high_resolved} | {high_open} | {high_pct}% | {'🟢 100% DEFENDED' if high_open == 0 else '🟠 NEEDS ATTENTION'} |",
|
|
440
|
+
f"| **Medium / Low** | {med_count + low_count} | {med_resolved + low_resolved} | {med_open} | {med_pct}% | {'🟢 100% DEFENDED' if med_open == 0 else '🟡 PENDING REVIEW'} |",
|
|
441
|
+
f"| **Total Flaws** | **{total}** | **{resolved_count}** | **{open_count}** | **{overall_pct}%** | **{'🟢 100% HARDENED' if open_count == 0 else '🟡 REMEDIATION IN PROGRESS'}** |",
|
|
442
|
+
"",
|
|
443
|
+
])
|
|
444
|
+
|
|
445
|
+
# ── Directory Attack Surface Overview Table ──
|
|
446
|
+
dir_stats: Dict[str, Dict[str, int]] = {}
|
|
447
|
+
for f in findings:
|
|
448
|
+
t = f.get('target', '')
|
|
449
|
+
fp = t.split(':')[0] if ':' in t else t
|
|
450
|
+
fp = fp.replace('\\', '/')
|
|
451
|
+
d_name = f"{fp.split('/')[0]}/" if '/' in fp else "root"
|
|
452
|
+
if d_name not in dir_stats:
|
|
453
|
+
dir_stats[d_name] = {"total": 0, "resolved": 0, "open": 0}
|
|
454
|
+
dir_stats[d_name]["total"] += 1
|
|
455
|
+
if "RESOLVED" in f.get("status", ""):
|
|
456
|
+
dir_stats[d_name]["resolved"] += 1
|
|
457
|
+
else:
|
|
458
|
+
dir_stats[d_name]["open"] += 1
|
|
459
|
+
|
|
460
|
+
if dir_stats:
|
|
461
|
+
lines.extend([
|
|
462
|
+
"### 🗺️ Directory Attack Surface Defended",
|
|
463
|
+
"",
|
|
464
|
+
"| Directory / Module | Total Invariants Checked | Vulnerabilities Neutralized | Active Risk | Defense Posture |",
|
|
465
|
+
"| :--- | :---: | :---: | :---: | :--- |",
|
|
466
|
+
])
|
|
467
|
+
for d_name, d_data in sorted(dir_stats.items(), key=lambda x: x[1]["total"], reverse=True):
|
|
468
|
+
posture_str = "🟢 Hardened Zone (100% Invariants Enforced)" if d_data["open"] == 0 else f"🔴 Active Exposure ({d_data['open']} Open Flaws)"
|
|
469
|
+
lines.append(f"| `{d_name}` | {d_data['total']} | {d_data['resolved']} | {d_data['open']} | {posture_str} |")
|
|
470
|
+
lines.append("")
|
|
471
|
+
|
|
472
|
+
# ── Root-Cause Architectural Clusters Overview Table ──
|
|
473
|
+
cluster_stats: Dict[str, Dict[str, Any]] = {}
|
|
474
|
+
for f in findings:
|
|
475
|
+
c_id = f.get('cluster') or f.get('category') or 'cluster-general'
|
|
476
|
+
if c_id not in cluster_stats:
|
|
477
|
+
cluster_stats[c_id] = {"total": 0, "resolved": 0, "open": 0, "rules": set()}
|
|
478
|
+
cluster_stats[c_id]["total"] += 1
|
|
479
|
+
cluster_stats[c_id]["rules"].add(f.get("rule_id", "TG-GEN"))
|
|
480
|
+
if "RESOLVED" in f.get("status", ""):
|
|
481
|
+
cluster_stats[c_id]["resolved"] += 1
|
|
482
|
+
else:
|
|
483
|
+
cluster_stats[c_id]["open"] += 1
|
|
484
|
+
|
|
485
|
+
if cluster_stats:
|
|
486
|
+
lines.extend([
|
|
487
|
+
"### 🏛️ Root-Cause Architectural Clusters",
|
|
488
|
+
"",
|
|
489
|
+
"| Cluster ID | Threat Architectural Domain | Neutralized Issues | Enforced Invariants | Posture State |",
|
|
490
|
+
"| :--- | :--- | :---: | :--- | :--- |",
|
|
491
|
+
])
|
|
492
|
+
for c_id, c_data in sorted(cluster_stats.items(), key=lambda x: x[1]["total"], reverse=True):
|
|
493
|
+
c_domain = c_id.replace("cluster-", "").replace("-", " ").title()
|
|
494
|
+
rules_str = ", ".join(f"`{r}`" for r in sorted(c_data["rules"])[:3])
|
|
495
|
+
c_state = "🟢 Dismantled & Defended" if c_data["open"] == 0 else f"🔴 {c_data['open']} Active Flaws"
|
|
496
|
+
lines.append(f"| `{c_id}` | {c_domain} | {c_data['resolved']} | {rules_str} | {c_state} |")
|
|
497
|
+
lines.append("")
|
|
498
|
+
|
|
387
499
|
lines.append("---")
|
|
388
500
|
lines.append("")
|
|
389
501
|
|
|
@@ -445,11 +557,26 @@ def _render_finding_card(f: Dict[str, Any]) -> List[str]:
|
|
|
445
557
|
status_full = f["status"]
|
|
446
558
|
conf = f.get('confidence', '70% (High Confidence)')
|
|
447
559
|
sev = f.get('severity', 'High')
|
|
560
|
+
target = f.get('target', 'unknown')
|
|
561
|
+
if "\n" in target:
|
|
562
|
+
target = target.splitlines()[0]
|
|
563
|
+
target = target.strip().strip("`")
|
|
564
|
+
if target.startswith("📁"):
|
|
565
|
+
target = target.replace("📁", "").strip()
|
|
566
|
+
|
|
567
|
+
desc = f.get('what_is_wrong') or f.get('description') or 'Security violation detected.'
|
|
568
|
+
if "\n" in desc:
|
|
569
|
+
desc = desc.splitlines()[0]
|
|
570
|
+
desc = desc.strip().strip("`")
|
|
571
|
+
if desc.startswith("**Problem:**"):
|
|
572
|
+
desc = desc.replace("**Problem:**", "").strip()
|
|
573
|
+
|
|
574
|
+
cluster_badge = f" · **Cluster:** `{f['cluster']}`" if f.get("cluster") else ""
|
|
448
575
|
card = [
|
|
449
576
|
f"### {f['rule_id']} · {f['title']} `{status_full}` `{conf}`",
|
|
450
|
-
f"- **Finding ID:** `{f['finding_id']}` · **Severity:** `{sev}`",
|
|
451
|
-
f"📁 `{
|
|
452
|
-
f"**Problem:** {
|
|
577
|
+
f"- **Finding ID:** `{f['finding_id']}` · **Severity:** `{sev}`{cluster_badge}",
|
|
578
|
+
f"📁 `{target}`",
|
|
579
|
+
f"**Problem:** {desc}",
|
|
453
580
|
]
|
|
454
581
|
if f.get("what_should_change"):
|
|
455
582
|
card.append(f"**Fix:** {f['what_should_change']}")
|