torusguard 1.3.5 → 1.3.6
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 +35 -35
- package/.torusguard/runs/report-latest.html +2685 -303
- package/.torusguard/runs/results-latest.sarif +20 -0
- package/.torusguard/runs/run-20260918-104221-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260918-104221-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260918-104221-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260918-104221-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260918-104236-audit/findings.json +2314 -0
- package/.torusguard/runs/run-20260918-104236-audit/findings.md +960 -0
- package/.torusguard/runs/run-20260918-104236-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260918-104236-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260918-105723-audit/findings.json +2382 -0
- package/.torusguard/runs/run-20260918-105723-audit/findings.md +960 -0
- package/.torusguard/runs/run-20260918-105723-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260918-105723-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260918-105731-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260918-105731-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260918-105731-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260918-105731-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260918-105940-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260918-105940-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260918-105940-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260918-105940-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__/diff_guard.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/finding_scorer.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__/memory_engine.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/monorepo_detector.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/recheck_runner.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/report_sync.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/run_manager.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/sarif_exporter.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/stack_detect.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/term_ui.cpython-314.pyc +0 -0
- package/.torusguard/scripts/apply_runner.py +11 -5
- package/.torusguard/scripts/audit_runner.py +222 -15
- package/.torusguard/scripts/harden_runner.py +3 -3
- package/.torusguard/scripts/html_reporter.py +2668 -648
- package/.torusguard/scripts/recheck_runner.py +3 -3
- package/.torusguard/scripts/recipes_runner.py +3 -3
- package/.torusguard/scripts/report_sync.py +370 -74
- package/.torusguard/scripts/sarif_exporter.py +48 -8
- package/.torusguard/scripts/stack_detect.py +44 -2
- package/.torusguard/scripts/term_ui.py +1 -1
- package/.torusguard/skills/torusguard/SKILL.md +75 -72
- package/.torusguard/skills/torusguard-apply/SKILL.md +84 -80
- package/.torusguard/skills/torusguard-audit/SKILL.md +87 -83
- package/.torusguard/skills/torusguard-authorize/SKILL.md +7 -2
- package/.torusguard/skills/torusguard-exploit-check/SKILL.md +7 -2
- package/.torusguard/skills/torusguard-full/SKILL.md +7 -2
- package/.torusguard/skills/torusguard-harden/SKILL.md +98 -94
- package/.torusguard/skills/torusguard-init/SKILL.md +68 -64
- package/.torusguard/skills/torusguard-recheck/SKILL.md +81 -77
- package/.torusguard/skills/torusguard-report/SKILL.md +93 -72
- package/.torusguard/skills/torusguard-status/SKILL.md +62 -58
- package/.torusguard/skills/torusguard-verify/SKILL.md +7 -2
- package/.torusguard/skills/torusguard-web-validate/SKILL.md +7 -2
- package/.torusguard/workflows/apply.md +1 -1
- package/.torusguard/workflows/audit.md +1 -1
- package/.torusguard/workflows/authorize.md +1 -1
- package/.torusguard/workflows/exploit-check.md +1 -1
- package/.torusguard/workflows/harden.md +1 -1
- package/.torusguard/workflows/init.md +1 -1
- package/.torusguard/workflows/memory.md +1 -1
- package/.torusguard/workflows/recheck.md +1 -1
- package/.torusguard/workflows/report.md +1 -1
- package/.torusguard/workflows/status.md +1 -1
- package/.torusguard/workflows/verify.md +1 -1
- package/.torusguard/workflows/web-validate.md +1 -1
- package/README.md +14 -8
- package/bin/torusguard.js +56 -20
- package/package.json +1 -1
- package/skills/torusguard/SKILL.md +1 -1
- package/skills/torusguard/bootstrap.py +7 -2
- package/skills/torusguard/payload/.manifest.json +35 -35
- package/skills/torusguard/payload/scripts/__pycache__/apply_runner.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/audit_runner.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/diff_guard.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/finding_scorer.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/finding_scorer.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/harden_runner.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/html_reporter.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/html_reporter.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/manifest_builder.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/memory_engine.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/memory_engine.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/monorepo_detector.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/monorepo_detector.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/recheck_runner.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/report_sync.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/report_sync.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/rules_sync.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/run_manager.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/run_manager.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/sarif_exporter.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/stack_detect.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/stack_detect.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/term_ui.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/term_ui.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/apply_runner.py +11 -5
- package/skills/torusguard/payload/scripts/audit_runner.py +222 -15
- package/skills/torusguard/payload/scripts/harden_runner.py +3 -3
- package/skills/torusguard/payload/scripts/html_reporter.py +2668 -648
- package/skills/torusguard/payload/scripts/recheck_runner.py +3 -3
- package/skills/torusguard/payload/scripts/recipes_runner.py +3 -3
- package/skills/torusguard/payload/scripts/report_sync.py +370 -74
- package/skills/torusguard/payload/scripts/sarif_exporter.py +48 -8
- package/skills/torusguard/payload/scripts/stack_detect.py +44 -2
- package/skills/torusguard/payload/scripts/term_ui.py +1 -1
- package/skills/torusguard/payload/skills/torusguard/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-apply/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-authorize/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-exploit-check/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-full/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-harden/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-init/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-recheck/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-report/SKILL.md +40 -23
- package/skills/torusguard/payload/skills/torusguard-status/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-verify/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-web-validate/SKILL.md +1 -1
- package/skills/torusguard/payload/workflows/apply.md +1 -1
- package/skills/torusguard/payload/workflows/audit.md +1 -1
- package/skills/torusguard/payload/workflows/authorize.md +1 -1
- package/skills/torusguard/payload/workflows/exploit-check.md +1 -1
- package/skills/torusguard/payload/workflows/harden.md +1 -1
- package/skills/torusguard/payload/workflows/init.md +1 -1
- package/skills/torusguard/payload/workflows/memory.md +1 -1
- package/skills/torusguard/payload/workflows/recheck.md +1 -1
- package/skills/torusguard/payload/workflows/report.md +1 -1
- package/skills/torusguard/payload/workflows/status.md +1 -1
- package/skills/torusguard/payload/workflows/verify.md +1 -1
- package/skills/torusguard/payload/workflows/web-validate.md +1 -1
- package/skills/torusguard-apply/SKILL.md +1 -1
- package/skills/torusguard-audit/SKILL.md +1 -1
- package/skills/torusguard-authorize/SKILL.md +1 -1
- package/skills/torusguard-exploit-check/SKILL.md +1 -1
- package/skills/torusguard-full/SKILL.md +1 -1
- package/skills/torusguard-harden/SKILL.md +1 -1
- package/skills/torusguard-init/SKILL.md +1 -1
- package/skills/torusguard-recheck/SKILL.md +1 -1
- package/skills/torusguard-report/SKILL.md +40 -23
- package/skills/torusguard-status/SKILL.md +1 -1
- package/skills/torusguard-verify/SKILL.md +1 -1
- package/skills/torusguard-web-validate/SKILL.md +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
TorusGuard Targeted Differential Recheck Engine (v1.3.
|
|
3
|
+
TorusGuard Targeted Differential Recheck Engine (v1.3.6)
|
|
4
4
|
Executes scoped differential re-audits verifying only impacted files and adjacent boundaries.
|
|
5
5
|
Evaluates formal status transitions (Confirmed Fixed vs Regressed vs Unresolved), updates run manifest,
|
|
6
6
|
and records verification telemetry in the persistent security memory subsystem.
|
|
@@ -57,7 +57,7 @@ def box_line(content: str, width: int = 67, border: str = "│", border_color: s
|
|
|
57
57
|
return term_ui.format_box_line(content, width=width, border=border, border_color=border_color)
|
|
58
58
|
return f" {border_color}{border}{RESET} {content}"
|
|
59
59
|
|
|
60
|
-
def box_header(title: str, subtitle: str = "", version: str = "v1.3.
|
|
60
|
+
def box_header(title: str, subtitle: str = "", version: str = "v1.3.6", border_color: str = CYAN) -> str:
|
|
61
61
|
if term_ui:
|
|
62
62
|
return term_ui.card_header(title, subtitle, version, border_color)
|
|
63
63
|
return f"=== {title} ({version}) ==="
|
|
@@ -106,7 +106,7 @@ def execute_recheck(target_root: Path, run_id_arg: Optional[str] = None) -> Dict
|
|
|
106
106
|
|
|
107
107
|
# Header Card
|
|
108
108
|
print()
|
|
109
|
-
print(box_header("🛡️ TORUSGUARD DIFFERENTIAL RECHECK ENGINE", "Targeted Differential AST Scan & Regression Verification", "v1.3.
|
|
109
|
+
print(box_header("🛡️ TORUSGUARD DIFFERENTIAL RECHECK ENGINE", "Targeted Differential AST Scan & Regression Verification", "v1.3.6"))
|
|
110
110
|
print()
|
|
111
111
|
|
|
112
112
|
print(border_top("Recheck Verification Scope"))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
TorusGuard Golden Fix Recipe Explorer (v1.3.
|
|
3
|
+
TorusGuard Golden Fix Recipe Explorer (v1.3.6)
|
|
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.3.
|
|
50
|
+
def box_header(title: str, subtitle: str = "", version: str = "v1.3.6", 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}) ==="
|
|
@@ -80,7 +80,7 @@ def list_recipes(target_root: Path, detail_id: Optional[str] = None, json_output
|
|
|
80
80
|
|
|
81
81
|
# Header Card
|
|
82
82
|
print()
|
|
83
|
-
print(box_header("🛡️ TORUSGUARD GOLDEN FIX RECIPES", "Distilled Ponytail Fixes (<= 35 add, <= 25 del) in Persistent Memory", "v1.3.
|
|
83
|
+
print(box_header("🛡️ TORUSGUARD GOLDEN FIX RECIPES", "Distilled Ponytail Fixes (<= 35 add, <= 25 del) in Persistent Memory", "v1.3.6"))
|
|
84
84
|
print()
|
|
85
85
|
|
|
86
86
|
if not recipes:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
TorusGuard Living Security Report Sync Engine (v1.3.
|
|
3
|
+
TorusGuard Living Security Report Sync Engine (v1.3.6)
|
|
4
4
|
Maintains a living, ground-truth `security_report.md` ledger at the root of the workspace.
|
|
5
5
|
Tracks finding lifecycles from Discovery -> Candidate -> Applied -> Resolved -> Regressed.
|
|
6
6
|
Pure Python 3.10+ standard library (zero external dependencies).
|
|
@@ -10,6 +10,7 @@ import sys
|
|
|
10
10
|
import os
|
|
11
11
|
import re
|
|
12
12
|
import json
|
|
13
|
+
import time
|
|
13
14
|
import datetime
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from typing import Dict, List, Any, Optional, Tuple
|
|
@@ -42,9 +43,106 @@ def get_report_path(target_root: Path) -> Path:
|
|
|
42
43
|
"""Return absolute path to security_report.md at the workspace root."""
|
|
43
44
|
return target_root / "security_report.md"
|
|
44
45
|
|
|
46
|
+
|
|
47
|
+
def atomic_write_text(file_path: Path, content: str, max_retries: int = 3, backoff_base: float = 0.05) -> bool:
|
|
48
|
+
"""
|
|
49
|
+
Atomically write text to disk using a temporary file and atomic replace.
|
|
50
|
+
Handles Windows file-locking (e.g. browser read locks on open HTML reports)
|
|
51
|
+
via exponential backoff retries.
|
|
52
|
+
"""
|
|
53
|
+
file_path = Path(file_path).resolve()
|
|
54
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
|
|
56
|
+
pid = os.getpid()
|
|
57
|
+
ts = int(time.time() * 1000)
|
|
58
|
+
tmp_path = file_path.with_name(f".tmp.{pid}.{ts}.{file_path.name}")
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
with open(tmp_path, "w", encoding="utf-8") as f:
|
|
62
|
+
f.write(content)
|
|
63
|
+
f.flush()
|
|
64
|
+
os.fsync(f.fileno())
|
|
65
|
+
except Exception as e:
|
|
66
|
+
if tmp_path.exists():
|
|
67
|
+
try:
|
|
68
|
+
tmp_path.unlink()
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
raise e
|
|
72
|
+
|
|
73
|
+
for attempt in range(max_retries):
|
|
74
|
+
try:
|
|
75
|
+
os.replace(tmp_path, file_path)
|
|
76
|
+
return True
|
|
77
|
+
except (PermissionError, OSError):
|
|
78
|
+
if attempt == max_retries - 1:
|
|
79
|
+
try:
|
|
80
|
+
file_path.write_text(content, encoding="utf-8")
|
|
81
|
+
if tmp_path.exists():
|
|
82
|
+
tmp_path.unlink()
|
|
83
|
+
return True
|
|
84
|
+
except Exception:
|
|
85
|
+
if tmp_path.exists():
|
|
86
|
+
try:
|
|
87
|
+
tmp_path.unlink()
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
raise
|
|
91
|
+
time.sleep(backoff_base * (2 ** attempt))
|
|
92
|
+
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def reconcile_from_run_artifacts(target_root: Path) -> List[Dict[str, Any]]:
|
|
97
|
+
"""
|
|
98
|
+
Authoritative fallback: if security_report.md is missing or damaged,
|
|
99
|
+
heal state from the latest run's findings.json in .torusguard/runs/.
|
|
100
|
+
"""
|
|
101
|
+
runs_dir = target_root / ".torusguard" / "runs"
|
|
102
|
+
if not runs_dir.is_dir():
|
|
103
|
+
return []
|
|
104
|
+
|
|
105
|
+
run_folders = sorted(
|
|
106
|
+
[d for d in runs_dir.iterdir() if d.is_dir() and (d / "findings.json").is_file()],
|
|
107
|
+
key=lambda p: p.stat().st_mtime,
|
|
108
|
+
reverse=True
|
|
109
|
+
)
|
|
110
|
+
if not run_folders:
|
|
111
|
+
return []
|
|
112
|
+
|
|
113
|
+
latest_file = run_folders[0] / "findings.json"
|
|
114
|
+
try:
|
|
115
|
+
data = json.loads(latest_file.read_text(encoding="utf-8"))
|
|
116
|
+
if isinstance(data, list):
|
|
117
|
+
return data
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
return []
|
|
121
|
+
|
|
122
|
+
|
|
45
123
|
def parse_existing_report(report_path: Path) -> Dict[str, Any]:
|
|
46
|
-
"""Parse existing security_report.md into structured data if it exists.
|
|
124
|
+
"""Parse existing security_report.md into structured data if it exists.
|
|
125
|
+
Supports both the legacy format and the new v2 summary-first format.
|
|
126
|
+
Heals from latest run findings if report is absent or damaged.
|
|
127
|
+
"""
|
|
47
128
|
if not report_path.is_file():
|
|
129
|
+
recovered = reconcile_from_run_artifacts(report_path.parent)
|
|
130
|
+
if recovered:
|
|
131
|
+
f_map = {}
|
|
132
|
+
for r in recovered:
|
|
133
|
+
fid = r.get("finding_id", "")
|
|
134
|
+
if fid:
|
|
135
|
+
f_map[fid] = {
|
|
136
|
+
"finding_id": fid,
|
|
137
|
+
"rule_id": r.get("rule_id", "TG-GEN"),
|
|
138
|
+
"title": r.get("title", "Security Finding"),
|
|
139
|
+
"status": "🔴 OPEN",
|
|
140
|
+
"severity": r.get("severity", "Medium"),
|
|
141
|
+
"confidence": f"{r.get('confidence_score', 70)}% ({r.get('confidence_band', 'High Confidence')})",
|
|
142
|
+
"target": f"{r.get('file_path', '')}:{r.get('line_number', 1)}",
|
|
143
|
+
"history": []
|
|
144
|
+
}
|
|
145
|
+
return {"findings": f_map, "metadata": {}}
|
|
48
146
|
return {"findings": {}, "metadata": {}}
|
|
49
147
|
|
|
50
148
|
try:
|
|
@@ -53,8 +151,9 @@ def parse_existing_report(report_path: Path) -> Dict[str, Any]:
|
|
|
53
151
|
return {"findings": {}, "metadata": {}}
|
|
54
152
|
|
|
55
153
|
findings = {}
|
|
56
|
-
|
|
57
|
-
|
|
154
|
+
|
|
155
|
+
# ── Format A (legacy): ### [RULE-ID] Title ──
|
|
156
|
+
card_pattern_legacy = re.compile(
|
|
58
157
|
r'###\s+\[(TG-[A-Z]+-\d+)\]\s+(.*?)\n'
|
|
59
158
|
r'- \*\*Finding ID:\*\*\s+`([^`]+)`\n'
|
|
60
159
|
r'- \*\*Status:\*\*\s+([^\n]+)\n'
|
|
@@ -63,7 +162,7 @@ def parse_existing_report(report_path: Path) -> Dict[str, Any]:
|
|
|
63
162
|
re.MULTILINE
|
|
64
163
|
)
|
|
65
164
|
|
|
66
|
-
for m in
|
|
165
|
+
for m in card_pattern_legacy.finditer(content):
|
|
67
166
|
rule_id = m.group(1).strip()
|
|
68
167
|
title = m.group(2).strip()
|
|
69
168
|
finding_id = m.group(3).strip()
|
|
@@ -83,7 +182,49 @@ def parse_existing_report(report_path: Path) -> Dict[str, Any]:
|
|
|
83
182
|
"history": []
|
|
84
183
|
}
|
|
85
184
|
|
|
86
|
-
#
|
|
185
|
+
# ── Format B (new v2): ### TG-RULE-001 · Title `status` `confidence` ──
|
|
186
|
+
card_pattern_v2 = re.compile(
|
|
187
|
+
r'###\s+(TG-[A-Z]+-\d+)\s+·\s+(.*?)\s+`([^`]+)`\s+`([^`]+)`\n'
|
|
188
|
+
r'- \*\*Finding ID:\*\*\s+`([^`]+)`(?:[^\n]*\*\*Severity:\*\*\s+`([^`]+)`)?\n'
|
|
189
|
+
r'(?:📁|- \*\*Target:\*\*)\s*`([^`\n]+)`',
|
|
190
|
+
re.MULTILINE
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
for m in card_pattern_v2.finditer(content):
|
|
194
|
+
rule_id = m.group(1).strip()
|
|
195
|
+
title = m.group(2).strip()
|
|
196
|
+
status = m.group(3).strip()
|
|
197
|
+
confidence = m.group(4).strip()
|
|
198
|
+
finding_id = m.group(5).strip()
|
|
199
|
+
sev_explicit = m.group(6).strip() if m.group(6) else None
|
|
200
|
+
target = m.group(7).strip()
|
|
201
|
+
|
|
202
|
+
if finding_id not in findings: # Don't overwrite legacy matches
|
|
203
|
+
if sev_explicit:
|
|
204
|
+
severity = sev_explicit
|
|
205
|
+
else:
|
|
206
|
+
# Infer severity from section header or status
|
|
207
|
+
severity = "High" # Default
|
|
208
|
+
match_pos = m.start()
|
|
209
|
+
preceding = content[:match_pos]
|
|
210
|
+
if "Critical" in preceding.split("##")[-1]:
|
|
211
|
+
severity = "Critical"
|
|
212
|
+
elif "Medium" in preceding.split("##")[-1]:
|
|
213
|
+
severity = "Medium"
|
|
214
|
+
|
|
215
|
+
findings[finding_id] = {
|
|
216
|
+
"finding_id": finding_id,
|
|
217
|
+
"rule_id": rule_id,
|
|
218
|
+
"title": title,
|
|
219
|
+
"status": status,
|
|
220
|
+
"severity": severity,
|
|
221
|
+
"confidence": confidence,
|
|
222
|
+
"target": target,
|
|
223
|
+
"history": []
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# Extract history from inline blockquote format (new) or table format (legacy)
|
|
227
|
+
# Legacy: #### Lifecycle History for `finding_id`
|
|
87
228
|
history_pattern = re.compile(r'#### Lifecycle History for `([^`]+)`\n([\s\S]*?)(?=\n---|\n###|\Z)')
|
|
88
229
|
for hm in history_pattern.finditer(content):
|
|
89
230
|
fid = hm.group(1).strip()
|
|
@@ -100,8 +241,40 @@ def parse_existing_report(report_path: Path) -> Dict[str, Any]:
|
|
|
100
241
|
"details": cols[3]
|
|
101
242
|
})
|
|
102
243
|
|
|
244
|
+
# New format: > Last: Action — timestamp · `run_id`
|
|
245
|
+
inline_history = re.compile(
|
|
246
|
+
r'###\s+TG-[A-Z]+-\d+.*?\n.*?Finding ID:\*\*\s+`([^`]+)`.*?'
|
|
247
|
+
r'>\s+Last:\s+(.*?)\s+—\s+(.*?)\s+·\s+`([^`]+)`',
|
|
248
|
+
re.DOTALL
|
|
249
|
+
)
|
|
250
|
+
for hm in inline_history.finditer(content):
|
|
251
|
+
fid = hm.group(1).strip()
|
|
252
|
+
if fid in findings and not findings[fid]["history"]:
|
|
253
|
+
findings[fid]["history"].append({
|
|
254
|
+
"timestamp": hm.group(3).strip(),
|
|
255
|
+
"action": hm.group(2).strip(),
|
|
256
|
+
"run_id": hm.group(4).strip(),
|
|
257
|
+
"details": "Restored from inline history"
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
|
|
103
261
|
return {"findings": findings, "raw_content": content}
|
|
104
262
|
|
|
263
|
+
|
|
264
|
+
def parse_metadata_block(content: str) -> Dict[str, Any]:
|
|
265
|
+
"""Parse the YAML metadata block between TORUSGUARD_METADATA_START/END markers."""
|
|
266
|
+
metadata = {}
|
|
267
|
+
meta_match = re.search(
|
|
268
|
+
r'<!-- TORUSGUARD_METADATA_START\s*\n(.*?)\nTORUSGUARD_METADATA_END -->',
|
|
269
|
+
content, re.DOTALL
|
|
270
|
+
)
|
|
271
|
+
if meta_match:
|
|
272
|
+
for line in meta_match.group(1).strip().splitlines():
|
|
273
|
+
if ':' in line:
|
|
274
|
+
key, _, val = line.partition(':')
|
|
275
|
+
metadata[key.strip()] = val.strip()
|
|
276
|
+
return metadata
|
|
277
|
+
|
|
105
278
|
def compute_health_score(findings: List[Dict[str, Any]]) -> Tuple[int, str]:
|
|
106
279
|
"""
|
|
107
280
|
Compute security health score (0-100) based on remaining open/regressed findings.
|
|
@@ -127,7 +300,8 @@ def compute_health_score(findings: List[Dict[str, Any]]) -> Tuple[int, str]:
|
|
|
127
300
|
return score, status
|
|
128
301
|
|
|
129
302
|
def render_report(findings: List[Dict[str, Any]], target_root: Path) -> str:
|
|
130
|
-
"""Render the
|
|
303
|
+
"""Render the living security_report.md with summary-first layout,
|
|
304
|
+
AI-parseable metadata, severity grouping, and concise finding cards."""
|
|
131
305
|
score, status_text = compute_health_score(findings)
|
|
132
306
|
now_str = format_timestamp()
|
|
133
307
|
|
|
@@ -146,36 +320,11 @@ def render_report(findings: List[Dict[str, Any]], target_root: Path) -> str:
|
|
|
146
320
|
med_count = sum(1 for f in findings if f["severity"].lower() == "medium")
|
|
147
321
|
low_count = sum(1 for f in findings if f["severity"].lower() == "low")
|
|
148
322
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
"> **Notice:** This document is the living, single-source-of-truth ledger for security findings,",
|
|
153
|
-
"> candidate patches, and verified closures across this repository. Updated automatically by",
|
|
154
|
-
"> TorusGuard CLI commands (`audit`, `harden`, `apply`, `recheck`) and AI chat workflows.",
|
|
155
|
-
"",
|
|
156
|
-
"---",
|
|
157
|
-
"",
|
|
158
|
-
"## 📊 Security Posture Overview",
|
|
159
|
-
"",
|
|
160
|
-
f"- **Health Score:** `{score}/100` — **{status_text}**",
|
|
161
|
-
f"- **Last Updated:** `{now_str}`",
|
|
162
|
-
f"- **Total Tracked Findings:** `{total}`",
|
|
163
|
-
f"- **Status Breakdown:** `🔴 {open_count} Open` · `🟠 {verified_count} Verified` · `🟡 {candidate_count} Candidate` · `🔵 {applied_count} Applied` · `🟢 {resolved_count} Resolved` · `❌ {regressed_count} Regressed` · `⚪ {fp_count} Suppressed`",
|
|
164
|
-
"",
|
|
165
|
-
"| Severity | Total | Open / Regressed | Resolved | Candidate / Applied |",
|
|
166
|
-
"| :--- | :---: | :---: | :---: | :---: |",
|
|
167
|
-
f"| **Critical** | {crit_count} | {sum(1 for f in findings if f['severity'].lower() == 'critical' and ('OPEN' in f['status'] or 'REGRESSED' in f['status']))} | {sum(1 for f in findings if f['severity'].lower() == 'critical' and 'RESOLVED' in f['status'])} | {sum(1 for f in findings if f['severity'].lower() == 'critical' and ('CANDIDATE' in f['status'] or 'APPLIED' in f['status']))} |",
|
|
168
|
-
f"| **High** | {high_count} | {sum(1 for f in findings if f['severity'].lower() == 'high' and ('OPEN' in f['status'] or 'REGRESSED' in f['status']))} | {sum(1 for f in findings if f['severity'].lower() == 'high' and 'RESOLVED' in f['status'])} | {sum(1 for f in findings if f['severity'].lower() == 'high' and ('CANDIDATE' in f['status'] or 'APPLIED' in f['status']))} |",
|
|
169
|
-
f"| **Medium** | {med_count} | {sum(1 for f in findings if f['severity'].lower() == 'medium' and ('OPEN' in f['status'] or 'REGRESSED' in f['status']))} | {sum(1 for f in findings if f['severity'].lower() == 'medium' and 'RESOLVED' in f['status'])} | {sum(1 for f in findings if f['severity'].lower() == 'medium' and ('CANDIDATE' in f['status'] or 'APPLIED' in f['status']))} |",
|
|
170
|
-
f"| **Low** | {low_count} | {sum(1 for f in findings if f['severity'].lower() == 'low' and ('OPEN' in f['status'] or 'REGRESSED' in f['status']))} | {sum(1 for f in findings if f['severity'].lower() == 'low' and 'RESOLVED' in f['status'])} | {sum(1 for f in findings if f['severity'].lower() == 'low' and ('CANDIDATE' in f['status'] or 'APPLIED' in f['status']))} |",
|
|
171
|
-
"",
|
|
172
|
-
"---",
|
|
173
|
-
"",
|
|
174
|
-
"## 🔍 Tracked Findings Detail",
|
|
175
|
-
""
|
|
176
|
-
]
|
|
323
|
+
crit_open = sum(1 for f in findings if f["severity"].lower() == "critical" and ("OPEN" in f["status"] or "REGRESSED" in f["status"]))
|
|
324
|
+
high_open = sum(1 for f in findings if f["severity"].lower() == "high" and ("OPEN" in f["status"] or "REGRESSED" in f["status"]))
|
|
325
|
+
med_open = sum(1 for f in findings if f["severity"].lower() == "medium" and ("OPEN" in f["status"] or "REGRESSED" in f["status"]))
|
|
177
326
|
|
|
178
|
-
# Sort findings
|
|
327
|
+
# Sort findings by status priority and severity
|
|
179
328
|
sev_rank = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
|
180
329
|
def sort_key(f):
|
|
181
330
|
is_active = 0 if ("OPEN" in f["status"] or "REGRESSED" in f["status"]) else 1
|
|
@@ -184,50 +333,143 @@ def render_report(findings: List[Dict[str, Any]], target_root: Path) -> str:
|
|
|
184
333
|
|
|
185
334
|
sorted_findings = sorted(findings, key=sort_key)
|
|
186
335
|
|
|
187
|
-
for
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
336
|
+
# Group by severity for active findings
|
|
337
|
+
active = [f for f in sorted_findings if "OPEN" in f["status"] or "REGRESSED" in f["status"]]
|
|
338
|
+
resolved = [f for f in sorted_findings if "RESOLVED" in f["status"]]
|
|
339
|
+
in_progress = [f for f in sorted_findings if any(s in f["status"] for s in ("CANDIDATE", "APPLIED", "VERIFIED"))]
|
|
340
|
+
suppressed = [f for f in sorted_findings if "FALSE POSITIVE" in f["status"]]
|
|
341
|
+
|
|
342
|
+
critical_findings = [f for f in active if f["severity"].lower() == "critical"]
|
|
343
|
+
high_findings = [f for f in active if f["severity"].lower() == "high"]
|
|
344
|
+
medium_findings = [f for f in active if f["severity"].lower() in ("medium", "low")]
|
|
345
|
+
|
|
346
|
+
# ── Build the report ──
|
|
347
|
+
lines = [
|
|
348
|
+
"# 🛡️ TorusGuard Security Report",
|
|
349
|
+
"",
|
|
350
|
+
"<!-- TORUSGUARD_METADATA_START",
|
|
351
|
+
f"health_score: {score}",
|
|
352
|
+
f"status: {status_text}",
|
|
353
|
+
f"total_findings: {total}",
|
|
354
|
+
f"critical: {crit_count}",
|
|
355
|
+
f"critical_open: {crit_open}",
|
|
356
|
+
f"high: {high_count}",
|
|
357
|
+
f"high_open: {high_open}",
|
|
358
|
+
f"medium: {med_count}",
|
|
359
|
+
f"medium_open: {med_open}",
|
|
360
|
+
f"low: {low_count}",
|
|
361
|
+
f"open: {open_count}",
|
|
362
|
+
f"resolved: {resolved_count}",
|
|
363
|
+
f"regressed: {regressed_count}",
|
|
364
|
+
f"suppressed: {fp_count}",
|
|
365
|
+
f"last_scan: {now_str}",
|
|
366
|
+
"TORUSGUARD_METADATA_END -->",
|
|
367
|
+
"",
|
|
368
|
+
"## Executive Summary",
|
|
369
|
+
"",
|
|
370
|
+
"| Metric | Value |",
|
|
371
|
+
"| :--- | :--- |",
|
|
372
|
+
f"| **Health Score** | `{score}/100` — {status_text} |",
|
|
373
|
+
f"| **Critical (Open)** | {crit_open} |",
|
|
374
|
+
f"| **High (Open)** | {high_open} |",
|
|
375
|
+
f"| **Medium (Open)** | {med_open} |",
|
|
376
|
+
f"| **Resolved** | {resolved_count} |",
|
|
377
|
+
f"| **In Progress** | {candidate_count + applied_count} |",
|
|
378
|
+
f"| **Last Scan** | {now_str} |",
|
|
379
|
+
"",
|
|
380
|
+
]
|
|
381
|
+
|
|
382
|
+
# Action required callout
|
|
383
|
+
if crit_open > 0:
|
|
384
|
+
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
|
+
lines.append("")
|
|
386
|
+
|
|
387
|
+
lines.append("---")
|
|
388
|
+
lines.append("")
|
|
389
|
+
|
|
390
|
+
# ── Critical Findings Section ──
|
|
391
|
+
if critical_findings:
|
|
392
|
+
lines.append(f"## 🔴 Critical Findings ({len(critical_findings)})")
|
|
195
393
|
lines.append("")
|
|
196
|
-
|
|
394
|
+
for f in critical_findings:
|
|
395
|
+
lines.extend(_render_finding_card(f))
|
|
396
|
+
|
|
397
|
+
# ── High Findings Section ──
|
|
398
|
+
if high_findings:
|
|
399
|
+
lines.append(f"## 🟠 High Findings ({len(high_findings)})")
|
|
400
|
+
lines.append("")
|
|
401
|
+
for f in high_findings:
|
|
402
|
+
lines.extend(_render_finding_card(f))
|
|
403
|
+
|
|
404
|
+
# ── Medium/Low Findings Section ──
|
|
405
|
+
if medium_findings:
|
|
406
|
+
lines.append(f"## 🟡 Medium / Low Findings ({len(medium_findings)})")
|
|
197
407
|
lines.append("")
|
|
198
|
-
|
|
408
|
+
for f in medium_findings:
|
|
409
|
+
lines.extend(_render_finding_card(f))
|
|
410
|
+
|
|
411
|
+
# ── In Progress Section ──
|
|
412
|
+
if in_progress:
|
|
413
|
+
lines.append(f"## 🔵 In Progress ({len(in_progress)})")
|
|
414
|
+
lines.append("")
|
|
415
|
+
for f in in_progress:
|
|
416
|
+
lines.extend(_render_finding_card(f))
|
|
417
|
+
|
|
418
|
+
# ── Resolved Section ──
|
|
419
|
+
if resolved:
|
|
420
|
+
lines.append(f"## ✅ Resolved ({len(resolved)})")
|
|
199
421
|
lines.append("")
|
|
200
|
-
|
|
201
|
-
lines.
|
|
202
|
-
lines.append("")
|
|
203
|
-
if f.get("snippet"):
|
|
204
|
-
lines.append("```")
|
|
205
|
-
lines.append(f["snippet"].strip())
|
|
206
|
-
lines.append("```")
|
|
207
|
-
lines.append("")
|
|
208
|
-
if f.get("proposed_diff"):
|
|
209
|
-
lines.append("**Proposed Minimal Patch Preview (Ponytail Bounded):**")
|
|
210
|
-
lines.append("```diff")
|
|
211
|
-
lines.append(f["proposed_diff"].strip())
|
|
212
|
-
lines.append("```")
|
|
213
|
-
lines.append("")
|
|
214
|
-
|
|
215
|
-
# History table
|
|
216
|
-
history = f.get("history", [])
|
|
217
|
-
if history:
|
|
218
|
-
lines.append(f"#### Lifecycle History for `{f['finding_id']}`")
|
|
219
|
-
lines.append("| Timestamp | Action | Run ID | Details |")
|
|
220
|
-
lines.append("| :--- | :--- | :--- | :--- |")
|
|
221
|
-
for h in history:
|
|
222
|
-
lines.append(f"| {h['timestamp']} | {h['action']} | `{h['run_id']}` | {h['details']} |")
|
|
223
|
-
lines.append("")
|
|
422
|
+
for f in resolved:
|
|
423
|
+
lines.extend(_render_finding_card(f))
|
|
224
424
|
|
|
425
|
+
# ── Suppressed Section ──
|
|
426
|
+
if suppressed:
|
|
427
|
+
lines.append(f"## ⚪ Suppressed ({len(suppressed)})")
|
|
428
|
+
lines.append("")
|
|
429
|
+
for f in suppressed:
|
|
430
|
+
lines.append(f"- **{f['rule_id']}** · {f['title']} — `{f['target']}` · False Positive")
|
|
431
|
+
lines.append("")
|
|
225
432
|
lines.append("---")
|
|
226
433
|
lines.append("")
|
|
227
434
|
|
|
435
|
+
# Footer
|
|
436
|
+
lines.append("---")
|
|
437
|
+
lines.append(f"*Generated by TorusGuard v1.3.6 · {now_str} · 100% Local-First*")
|
|
438
|
+
lines.append("")
|
|
439
|
+
|
|
228
440
|
return "\n".join(lines)
|
|
229
441
|
|
|
230
442
|
|
|
443
|
+
def _render_finding_card(f: Dict[str, Any]) -> List[str]:
|
|
444
|
+
"""Render a concise finding card (4-6 lines max)."""
|
|
445
|
+
status_full = f["status"]
|
|
446
|
+
conf = f.get('confidence', '70% (High Confidence)')
|
|
447
|
+
sev = f.get('severity', 'High')
|
|
448
|
+
card = [
|
|
449
|
+
f"### {f['rule_id']} · {f['title']} `{status_full}` `{conf}`",
|
|
450
|
+
f"- **Finding ID:** `{f['finding_id']}` · **Severity:** `{sev}`",
|
|
451
|
+
f"📁 `{f['target']}`",
|
|
452
|
+
f"**Problem:** {f.get('what_is_wrong', f.get('description', 'Security violation detected.'))}",
|
|
453
|
+
]
|
|
454
|
+
if f.get("what_should_change"):
|
|
455
|
+
card.append(f"**Fix:** {f['what_should_change']}")
|
|
456
|
+
if f.get("proposed_diff"):
|
|
457
|
+
card.append("")
|
|
458
|
+
card.append("```diff")
|
|
459
|
+
card.append(f["proposed_diff"].strip())
|
|
460
|
+
card.append("```")
|
|
461
|
+
card.append("")
|
|
462
|
+
# Include lifecycle history inline (compact)
|
|
463
|
+
history = f.get("history", [])
|
|
464
|
+
if history and len(history) > 0:
|
|
465
|
+
last_event = history[-1]
|
|
466
|
+
card.append(f"> Last: {last_event.get('action', '?')} — {last_event.get('timestamp', '?')} · `{last_event.get('run_id', '?')}`")
|
|
467
|
+
card.append("")
|
|
468
|
+
card.append("---")
|
|
469
|
+
card.append("")
|
|
470
|
+
return card
|
|
471
|
+
|
|
472
|
+
|
|
231
473
|
def record_audit_findings(target_root: Path, findings: List[Dict[str, Any]], run_id: str):
|
|
232
474
|
"""
|
|
233
475
|
Called at the conclusion of `npx torusguard audit`.
|
|
@@ -298,7 +540,8 @@ def record_audit_findings(target_root: Path, findings: List[Dict[str, Any]], run
|
|
|
298
540
|
|
|
299
541
|
# Render and save
|
|
300
542
|
rendered = render_report(list(existing_map.values()), target_root)
|
|
301
|
-
report_file
|
|
543
|
+
atomic_write_text(report_file, rendered)
|
|
544
|
+
sync_html_if_exists(target_root)
|
|
302
545
|
return report_file
|
|
303
546
|
|
|
304
547
|
|
|
@@ -340,7 +583,8 @@ def record_harden_bundles(target_root: Path, bundles: List[Dict[str, Any]], run_
|
|
|
340
583
|
})
|
|
341
584
|
|
|
342
585
|
rendered = render_report(list(existing_map.values()), target_root)
|
|
343
|
-
report_file
|
|
586
|
+
atomic_write_text(report_file, rendered)
|
|
587
|
+
sync_html_if_exists(target_root)
|
|
344
588
|
return report_file
|
|
345
589
|
|
|
346
590
|
|
|
@@ -378,7 +622,8 @@ def record_applied_patches(target_root: Path, applied_bundles: List[Dict[str, An
|
|
|
378
622
|
})
|
|
379
623
|
|
|
380
624
|
rendered = render_report(list(existing_map.values()), target_root)
|
|
381
|
-
report_file
|
|
625
|
+
atomic_write_text(report_file, rendered)
|
|
626
|
+
sync_html_if_exists(target_root)
|
|
382
627
|
return report_file
|
|
383
628
|
|
|
384
629
|
|
|
@@ -419,5 +664,56 @@ def record_recheck_results(target_root: Path, recheck_results: Dict[str, Any], r
|
|
|
419
664
|
})
|
|
420
665
|
|
|
421
666
|
rendered = render_report(list(existing_map.values()), target_root)
|
|
422
|
-
report_file
|
|
667
|
+
atomic_write_text(report_file, rendered)
|
|
668
|
+
sync_html_if_exists(target_root)
|
|
669
|
+
return report_file
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def record_rollback_results(target_root: Path, restored_files: List[str], run_id: str):
|
|
673
|
+
"""
|
|
674
|
+
Called at the conclusion of `npx torusguard rollback`.
|
|
675
|
+
Reverts findings for restored files back to OPEN 🔴.
|
|
676
|
+
"""
|
|
677
|
+
report_file = get_report_path(target_root)
|
|
678
|
+
existing = parse_existing_report(report_file)
|
|
679
|
+
existing_map = existing["findings"]
|
|
680
|
+
now_str = format_timestamp()
|
|
681
|
+
|
|
682
|
+
norm_restored = {rf.replace("\\", "/") for rf in restored_files}
|
|
683
|
+
|
|
684
|
+
for item in existing_map.values():
|
|
685
|
+
target_path = item.get("target", "").split(":")[0].replace("\\", "/")
|
|
686
|
+
if any(rf in target_path or target_path in rf for rf in norm_restored):
|
|
687
|
+
item["status"] = "🔴 OPEN"
|
|
688
|
+
item["history"].append({
|
|
689
|
+
"timestamp": now_str,
|
|
690
|
+
"action": "Rollback Restored",
|
|
691
|
+
"run_id": run_id,
|
|
692
|
+
"details": f"File {target_path} reverted from pre-apply snapshot. Status restored to OPEN."
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
rendered = render_report(list(existing_map.values()), target_root)
|
|
696
|
+
atomic_write_text(report_file, rendered)
|
|
697
|
+
sync_html_if_exists(target_root)
|
|
423
698
|
return report_file
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def sync_html_if_exists(target_root: Path):
|
|
702
|
+
"""
|
|
703
|
+
If the user has already generated report.html (at root or in .torusguard/runs),
|
|
704
|
+
refresh it so the HTML dashboard stays in lockstep with security_report.md.
|
|
705
|
+
"""
|
|
706
|
+
root_html = target_root / "report.html"
|
|
707
|
+
runs_html = target_root / ".torusguard" / "runs" / "report-latest.html"
|
|
708
|
+
if root_html.is_file() or runs_html.is_file():
|
|
709
|
+
try:
|
|
710
|
+
s_dir = Path(__file__).resolve().parent
|
|
711
|
+
if str(s_dir) not in sys.path:
|
|
712
|
+
sys.path.insert(0, str(s_dir))
|
|
713
|
+
import html_reporter
|
|
714
|
+
if runs_html.is_file():
|
|
715
|
+
html_reporter.emit_html_report(target_path=runs_html, root_dir=target_root)
|
|
716
|
+
if root_html.is_file():
|
|
717
|
+
html_reporter.emit_html_report(target_path=root_html, root_dir=target_root)
|
|
718
|
+
except Exception:
|
|
719
|
+
pass
|