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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"run_id": "run-20260919-145701-audit",
|
|
3
|
+
"command": "audit",
|
|
4
|
+
"target_name": "TorusGuard",
|
|
5
|
+
"created_at": "2026-09-19 14:57:01 IST",
|
|
6
|
+
"created_at_iso": "2026-09-19T14:57:01.568307+05:30",
|
|
7
|
+
"status": "completed",
|
|
8
|
+
"torusguard_version": "1.3.0",
|
|
9
|
+
"findings_count": 0,
|
|
10
|
+
"confirmed_fixed_count": 0,
|
|
11
|
+
"regressed_count": 0
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"run_id": "run-20260919-151729-audit",
|
|
3
|
+
"command": "audit",
|
|
4
|
+
"target_name": "TorusGuard",
|
|
5
|
+
"created_at": "2026-09-19 15:17:29 IST",
|
|
6
|
+
"created_at_iso": "2026-09-19T15:17:29.783790+05:30",
|
|
7
|
+
"status": "completed",
|
|
8
|
+
"torusguard_version": "1.3.0",
|
|
9
|
+
"findings_count": 0,
|
|
10
|
+
"confirmed_fixed_count": 0,
|
|
11
|
+
"regressed_count": 0,
|
|
12
|
+
"remediation_bundles_count": 0
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"run_id": "run-20260919-152340-audit",
|
|
3
|
+
"command": "audit",
|
|
4
|
+
"target_name": "TorusGuard",
|
|
5
|
+
"created_at": "2026-09-19 15:23:40 IST",
|
|
6
|
+
"created_at_iso": "2026-09-19T15:23:40.802283+05:30",
|
|
7
|
+
"status": "completed",
|
|
8
|
+
"torusguard_version": "1.3.0",
|
|
9
|
+
"findings_count": 0,
|
|
10
|
+
"confirmed_fixed_count": 0,
|
|
11
|
+
"regressed_count": 0
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"run_id": "run-20260919-152345-audit",
|
|
3
|
+
"command": "audit",
|
|
4
|
+
"target_name": "TorusGuard",
|
|
5
|
+
"created_at": "2026-09-19 15:23:45 IST",
|
|
6
|
+
"created_at_iso": "2026-09-19T15:23:45.599143+05:30",
|
|
7
|
+
"status": "completed",
|
|
8
|
+
"torusguard_version": "1.3.0",
|
|
9
|
+
"findings_count": 0,
|
|
10
|
+
"confirmed_fixed_count": 0,
|
|
11
|
+
"regressed_count": 0
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"run_id": "run-20260919-153500-audit",
|
|
3
|
+
"command": "audit",
|
|
4
|
+
"target_name": "TorusGuard",
|
|
5
|
+
"created_at": "2026-09-19 15:35:00 IST",
|
|
6
|
+
"created_at_iso": "2026-09-19T15:35:00.129289+05:30",
|
|
7
|
+
"status": "completed",
|
|
8
|
+
"torusguard_version": "1.3.0",
|
|
9
|
+
"findings_count": 0,
|
|
10
|
+
"confirmed_fixed_count": 0,
|
|
11
|
+
"regressed_count": 0
|
|
12
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -58,7 +58,7 @@ def box_line(content: str, width: int = 67, border: str = "│", border_color: s
|
|
|
58
58
|
return term_ui.format_box_line(content, width=width, border=border, border_color=border_color)
|
|
59
59
|
return f" {border_color}{border}{RESET} {content}"
|
|
60
60
|
|
|
61
|
-
def box_header(title: str, subtitle: str = "", version: str = "v1.
|
|
61
|
+
def box_header(title: str, subtitle: str = "", version: str = "v1.4.0", border_color: str = CYAN) -> str:
|
|
62
62
|
if term_ui:
|
|
63
63
|
return term_ui.card_header(title, subtitle, version, border_color)
|
|
64
64
|
return f"=== {title} ({version}) ==="
|
|
@@ -81,6 +81,35 @@ def find_latest_audit_run(runs_dir: Path) -> Optional[Path]:
|
|
|
81
81
|
return runs[0] if runs else None
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
def list_snapshots(target_root: Path) -> None:
|
|
85
|
+
"""Display historical pre-apply backup snapshots."""
|
|
86
|
+
snapshots_base = target_root / ".torusguard" / "snapshots"
|
|
87
|
+
print()
|
|
88
|
+
print(box_header("🛡️ TORUSGUARD SNAPSHOT LEDGER", "Pre-Apply Safety Snapshots (.bak)", "v1.4.0"))
|
|
89
|
+
print()
|
|
90
|
+
if not snapshots_base.is_dir():
|
|
91
|
+
print(f" {YELLOW}ℹ No snapshots directory found at:{RESET} {snapshots_base}\n")
|
|
92
|
+
return
|
|
93
|
+
snapshots = sorted([d for d in snapshots_base.iterdir() if d.is_dir()], key=lambda p: p.stat().st_mtime, reverse=True)
|
|
94
|
+
if not snapshots:
|
|
95
|
+
print(f" {YELLOW}ℹ No rollback snapshots captured yet.{RESET}\n")
|
|
96
|
+
return
|
|
97
|
+
print(border_top(f"Available Snapshots ({len(snapshots)})"))
|
|
98
|
+
for s in snapshots:
|
|
99
|
+
bak_files = list(s.rglob("*.bak"))
|
|
100
|
+
total_bytes = sum(f.stat().st_size for f in bak_files)
|
|
101
|
+
kb_str = f"{total_bytes / 1024:.1f} KB"
|
|
102
|
+
try:
|
|
103
|
+
mtime_dt = datetime.datetime.fromtimestamp(s.stat().st_mtime, tz=IST)
|
|
104
|
+
time_str = mtime_dt.strftime("%Y-%m-%d %H:%M:%S IST")
|
|
105
|
+
except Exception:
|
|
106
|
+
time_str = "Captured"
|
|
107
|
+
print(box_line(f"{GREEN}{s.name}{RESET} ({len(bak_files)} files · {kb_str})"))
|
|
108
|
+
print(box_line(f" └─ {DIM}Timestamp: {time_str}{RESET}"))
|
|
109
|
+
print(border_bottom())
|
|
110
|
+
print(f"\n {DIM}To restore any snapshot, run: {CYAN}npx torusguard rollback --run <snapshot-name>{RESET}\n")
|
|
111
|
+
|
|
112
|
+
|
|
84
113
|
def execute_rollback(target_root: Path, run_id_arg: Optional[str] = None) -> None:
|
|
85
114
|
"""Restore all target files from the pre-apply backup snapshots."""
|
|
86
115
|
snapshots_base = target_root / ".torusguard" / "snapshots"
|
|
@@ -115,7 +144,7 @@ def execute_rollback(target_root: Path, run_id_arg: Optional[str] = None) -> Non
|
|
|
115
144
|
pass
|
|
116
145
|
|
|
117
146
|
print()
|
|
118
|
-
print(box_header("🛡️ TORUSGUARD ROLLBACK RESTORATION", "Restored original files from pre-apply snapshot", "v1.
|
|
147
|
+
print(box_header("🛡️ TORUSGUARD ROLLBACK RESTORATION", "Restored original files from pre-apply snapshot", "v1.4.0"))
|
|
119
148
|
print()
|
|
120
149
|
|
|
121
150
|
print(border_top("Rollback Snapshot Restored"))
|
|
@@ -128,7 +157,7 @@ def execute_rollback(target_root: Path, run_id_arg: Optional[str] = None) -> Non
|
|
|
128
157
|
sys.exit(0)
|
|
129
158
|
|
|
130
159
|
|
|
131
|
-
def execute_apply(target_root: Path, run_id_arg: Optional[str] = None, auto_approve: bool = False) -> Dict[str, Any]:
|
|
160
|
+
def execute_apply(target_root: Path, run_id_arg: Optional[str] = None, auto_approve: bool = False, diff_only: bool = False, selective: bool = False, verify_post_apply: bool = False) -> Dict[str, Any]:
|
|
132
161
|
runs_dir = target_root / ".torusguard" / "runs"
|
|
133
162
|
if run_id_arg:
|
|
134
163
|
run_folder = runs_dir / run_id_arg
|
|
@@ -144,7 +173,7 @@ def execute_apply(target_root: Path, run_id_arg: Optional[str] = None, auto_appr
|
|
|
144
173
|
bundle_meta_files = list(bundles_dir.glob("*/metadata.json")) if bundles_dir.is_dir() else []
|
|
145
174
|
if not bundle_meta_files:
|
|
146
175
|
print()
|
|
147
|
-
print(box_header("🛡️ TORUSGUARD GOVERNED PATCH APPLIER", "Human-Gate Authorization, Snapshots & Golden Recipe Distillation", "v1.
|
|
176
|
+
print(box_header("🛡️ TORUSGUARD GOVERNED PATCH APPLIER", "Human-Gate Authorization, Snapshots & Golden Recipe Distillation", "v1.4.0"))
|
|
148
177
|
print()
|
|
149
178
|
print(f" {YELLOW}ℹ Zero candidate remediation bundles found in run: {run_folder.name}{RESET}")
|
|
150
179
|
print(f" {GRAY}The findings in this run require manual review or AI-assisted remediation.{RESET}\n")
|
|
@@ -163,13 +192,46 @@ def execute_apply(target_root: Path, run_id_arg: Optional[str] = None, auto_appr
|
|
|
163
192
|
except Exception:
|
|
164
193
|
pass
|
|
165
194
|
|
|
195
|
+
# Diff Preview Mode
|
|
196
|
+
if diff_only:
|
|
197
|
+
print()
|
|
198
|
+
print(box_header("🛡️ TORUSGUARD GOVERNED PATCH DIFF PREVIEW", "Dry-Run Unified Diff Inspection (No Files Modified)", "v1.4.0"))
|
|
199
|
+
print()
|
|
200
|
+
print(border_top(f"Candidate Patches ({len(bundles)})"))
|
|
201
|
+
for idx, b in enumerate(bundles, 1):
|
|
202
|
+
rule_id = b.get("rule_id", "TG-SEC")
|
|
203
|
+
rel_path = b.get("target_file", "")
|
|
204
|
+
line_no = b.get("line_number", 1)
|
|
205
|
+
diff = b.get("proposed_diff", "")
|
|
206
|
+
desc = b.get("what_should_change", "")
|
|
207
|
+
print(f" {YELLOW}─── [{idx}/{len(bundles)}] Candidate Patch: [{rule_id}] on {rel_path}:{line_no} ───{RESET}")
|
|
208
|
+
print(f" Strategy: {WHITE}{desc}{RESET}")
|
|
209
|
+
print(f" Ponytail: {GREEN}+{b.get('additions', 0)} / -{b.get('deletions', 0)}{RESET}\n")
|
|
210
|
+
for dline in diff.splitlines()[:16]:
|
|
211
|
+
if dline.startswith("+") and not dline.startswith("+++"):
|
|
212
|
+
print(f" {GREEN}{dline}{RESET}")
|
|
213
|
+
elif dline.startswith("-") and not dline.startswith("---"):
|
|
214
|
+
print(f" {RED}{dline}{RESET}")
|
|
215
|
+
elif dline.startswith("@"):
|
|
216
|
+
print(f" {CYAN}{dline}{RESET}")
|
|
217
|
+
else:
|
|
218
|
+
print(f" {DIM}{dline}{RESET}")
|
|
219
|
+
print()
|
|
220
|
+
print(border_bottom())
|
|
221
|
+
print(f"\n {DIM}To apply these patches to disk, run: {CYAN}npx torusguard apply{RESET} or {CYAN}npx torusguard apply --yes{RESET}\n")
|
|
222
|
+
return {
|
|
223
|
+
"status": "diff_preview",
|
|
224
|
+
"bundles_count": len(bundles),
|
|
225
|
+
"bundles": bundles
|
|
226
|
+
}
|
|
227
|
+
|
|
166
228
|
# Snapshot directory
|
|
167
229
|
snapshot_dir = target_root / ".torusguard" / "snapshots" / run_folder.name
|
|
168
230
|
snapshot_dir.mkdir(parents=True, exist_ok=True)
|
|
169
231
|
|
|
170
232
|
# Header Card
|
|
171
233
|
print()
|
|
172
|
-
print(box_header("🛡️ TORUSGUARD GOVERNED PATCH APPLIER", "Human-Gate Authorization, Snapshots & Golden Recipe Distillation", "v1.
|
|
234
|
+
print(box_header("🛡️ TORUSGUARD GOVERNED PATCH APPLIER", "Human-Gate Authorization, Snapshots & Golden Recipe Distillation", "v1.4.0"))
|
|
173
235
|
print()
|
|
174
236
|
|
|
175
237
|
print(border_top("Candidate Bundles Ready for Application"))
|
|
@@ -181,7 +243,7 @@ def execute_apply(target_root: Path, run_id_arg: Optional[str] = None, auto_appr
|
|
|
181
243
|
|
|
182
244
|
applied_count = 0
|
|
183
245
|
skipped_count = 0
|
|
184
|
-
all_approved = auto_approve
|
|
246
|
+
all_approved = False if selective else auto_approve
|
|
185
247
|
diff_summary_lines = [f"# TorusGuard Applied Patch Summary: {run_folder.name}\n"]
|
|
186
248
|
applied_bundles = []
|
|
187
249
|
|
|
@@ -357,6 +419,21 @@ def execute_apply(target_root: Path, run_id_arg: Optional[str] = None, auto_appr
|
|
|
357
419
|
print(border_bottom(border_color=GREEN, double=True))
|
|
358
420
|
print()
|
|
359
421
|
|
|
422
|
+
if verify_post_apply and applied_count > 0:
|
|
423
|
+
recheck_script = scripts_dir / "recheck_runner.py"
|
|
424
|
+
if recheck_script.is_file():
|
|
425
|
+
print(f"\n {CYAN}▸ Triggering differential recheck post-apply (--verify)...{RESET}\n")
|
|
426
|
+
try:
|
|
427
|
+
import importlib.util
|
|
428
|
+
spec = importlib.util.spec_from_file_location("recheck_runner", str(recheck_script))
|
|
429
|
+
if spec and spec.loader:
|
|
430
|
+
rc_mod = importlib.util.module_from_spec(spec)
|
|
431
|
+
spec.loader.exec_module(rc_mod)
|
|
432
|
+
if hasattr(rc_mod, "execute_recheck"):
|
|
433
|
+
rc_mod.execute_recheck(target_root, run_folder.name)
|
|
434
|
+
except Exception as e:
|
|
435
|
+
print(f" {YELLOW}⚠ Post-apply recheck notice: {e}{RESET}")
|
|
436
|
+
|
|
360
437
|
return {
|
|
361
438
|
"applied_count": applied_count,
|
|
362
439
|
"skipped_count": skipped_count,
|
|
@@ -368,19 +445,35 @@ def execute_apply(target_root: Path, run_id_arg: Optional[str] = None, auto_appr
|
|
|
368
445
|
def main():
|
|
369
446
|
parser = argparse.ArgumentParser(description="TorusGuard Governed Patch Application Engine")
|
|
370
447
|
parser.add_argument("path", nargs="?", default=".", help="Target project root directory")
|
|
448
|
+
parser.add_argument("--target", "-t", help="Target project root directory (alias for path)")
|
|
371
449
|
parser.add_argument("--run", "-r", help="Explicit run ID to apply")
|
|
372
450
|
parser.add_argument("--yes", "-y", action="store_true", help="Auto-approve all patches (non-interactive)")
|
|
451
|
+
parser.add_argument("--diff", action="store_true", help="Preview unified diffs without modifying files on disk")
|
|
452
|
+
parser.add_argument("--selective", action="store_true", help="Interactively review and select patches one by one")
|
|
453
|
+
parser.add_argument("--verify", action="store_true", help="Trigger differential AST recheck immediately post-apply")
|
|
454
|
+
parser.add_argument("--list-snapshots", action="store_true", help="List historical pre-apply backup snapshots")
|
|
373
455
|
parser.add_argument("--rollback", action="store_true", help="Rollback all applied patches using snapshots")
|
|
374
456
|
parser.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
375
457
|
args = parser.parse_args()
|
|
376
458
|
|
|
377
|
-
target = Path(args.path).resolve()
|
|
459
|
+
target = Path(args.target or args.path).resolve()
|
|
460
|
+
|
|
461
|
+
if args.list_snapshots:
|
|
462
|
+
list_snapshots(target)
|
|
463
|
+
return
|
|
378
464
|
|
|
379
465
|
if args.rollback:
|
|
380
466
|
execute_rollback(target, run_id_arg=args.run)
|
|
381
467
|
return
|
|
382
468
|
|
|
383
|
-
result = execute_apply(
|
|
469
|
+
result = execute_apply(
|
|
470
|
+
target,
|
|
471
|
+
run_id_arg=args.run,
|
|
472
|
+
auto_approve=args.yes,
|
|
473
|
+
diff_only=args.diff,
|
|
474
|
+
selective=args.selective,
|
|
475
|
+
verify_post_apply=args.verify
|
|
476
|
+
)
|
|
384
477
|
if args.json:
|
|
385
478
|
print(json.dumps(result, indent=2))
|
|
386
479
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
TorusGuard Autonomous Static Security Audit Engine (v1.
|
|
3
|
+
TorusGuard Autonomous Static Security Audit Engine (v1.4.0)
|
|
4
4
|
Multi-language static pattern & AST security scanner.
|
|
5
5
|
Evaluates source trees against canonical TorusGuard rule families,
|
|
6
6
|
augments confidence via the persistent security memory subsystem,
|
|
@@ -12,6 +12,7 @@ Pure Python 3.10+ standard library (zero external dependencies).
|
|
|
12
12
|
import sys
|
|
13
13
|
import os
|
|
14
14
|
import re
|
|
15
|
+
import time
|
|
15
16
|
import json
|
|
16
17
|
import hashlib
|
|
17
18
|
import argparse
|
|
@@ -369,7 +370,9 @@ RULE_PATTERNS: List[RulePattern] = [
|
|
|
369
370
|
"patterns": [
|
|
370
371
|
(re.compile(r'(?:query|execute|raw)\s*\(\s*f["\'].*SELECT.*\{', re.IGNORECASE), "Python f-string interpolation in raw SQL query"),
|
|
371
372
|
(re.compile(r'(?:query|execute|raw)\s*\(\s*[`"\'].*SELECT.*\$\{', re.IGNORECASE), "JavaScript template literal interpolation in SQL query"),
|
|
372
|
-
(re.compile(r'(?:query|execute|raw)\s*\(\s*["\'].*SELECT.*\+\s*(?:req|input|params)', re.IGNORECASE), "String concatenation in raw SQL query")
|
|
373
|
+
(re.compile(r'(?:query|execute|raw)\s*\(\s*["\'].*SELECT.*\+\s*(?:req|input|params)', re.IGNORECASE), "String concatenation in raw SQL query"),
|
|
374
|
+
(re.compile(r'(?:db|tx)\.(?:Query|Exec|QueryRow)\s*\(\s*fmt\.Sprintf\s*\(\s*["\'].*(?:SELECT|INSERT|UPDATE|DELETE).*%s', re.IGNORECASE), "Go database/sql query using fmt.Sprintf string interpolation instead of parameterized placeholders"),
|
|
375
|
+
(re.compile(r'(?:db|tx)\.(?:Query|Exec|QueryRow)\s*\(\s*["\'].*(?:SELECT|INSERT|UPDATE|DELETE).*\+\s*(?:r\.URL|c\.Query|input|param)', re.IGNORECASE), "Go database/sql query with string concatenation")
|
|
373
376
|
]
|
|
374
377
|
},
|
|
375
378
|
{
|
|
@@ -414,7 +417,8 @@ RULE_PATTERNS: List[RulePattern] = [
|
|
|
414
417
|
"cluster": "cluster-injection",
|
|
415
418
|
"patterns": [
|
|
416
419
|
(re.compile(r'(?:open|readFile|readFileSync)\s*\([^)]*\+\s*(?:req|params|query)'), "File read operation using unsanitized user request path"),
|
|
417
|
-
(re.compile(r'path\.join\s*\([^)]*(?:req\.params|req\.query)\.[a-zA-Z0-9_]+\)'), "Path traversal sink in path.join without basename sanitization")
|
|
420
|
+
(re.compile(r'path\.join\s*\([^)]*(?:req\.params|req\.query)\.[a-zA-Z0-9_]+\)'), "Path traversal sink in path.join without basename sanitization"),
|
|
421
|
+
(re.compile(r'(?:os\.Open|os\.ReadFile|ioutil\.ReadFile)\s*\([^)]*(?:c\.Query|c\.Param|r\.URL\.Query|filepath\.Join\([^)]*(?:c\.Query|c\.Param|r\.URL\.Query))'), "Go file read operation using unsanitized user request parameter")
|
|
418
422
|
]
|
|
419
423
|
},
|
|
420
424
|
|
|
@@ -502,7 +506,8 @@ RULE_PATTERNS: List[RulePattern] = [
|
|
|
502
506
|
"category": "ssrf",
|
|
503
507
|
"cluster": "cluster-ssrf",
|
|
504
508
|
"patterns": [
|
|
505
|
-
(re.compile(r'(?:fetch|axios\.get|requests\.get|urllib\.request\.urlopen)\s*\(\s*(?:req\.query|req\.body|request\.GET)\.[a-zA-Z0-9_]+'), "Outbound HTTP request directly invoking user-supplied URL parameter")
|
|
509
|
+
(re.compile(r'(?:fetch|axios\.get|requests\.get|urllib\.request\.urlopen)\s*\(\s*(?:req\.query|req\.body|request\.GET)\.[a-zA-Z0-9_]+'), "Outbound HTTP request directly invoking user-supplied URL parameter"),
|
|
510
|
+
(re.compile(r'http\.(?:Get|Post|Head)\s*\(\s*(?:r\.URL\.Query|c\.Query|c\.Param|req\.)'), "Go http.Get/Post directly invoking user-supplied URL parameter")
|
|
506
511
|
]
|
|
507
512
|
},
|
|
508
513
|
{
|
|
@@ -533,7 +538,9 @@ RULE_PATTERNS: List[RulePattern] = [
|
|
|
533
538
|
"category": "ssrf",
|
|
534
539
|
"cluster": "cluster-ssrf",
|
|
535
540
|
"patterns": [
|
|
536
|
-
(re.compile(r'requests\.(?:get|post|put)\s*\((?![^)]*\btimeout\s*=)[^)]*\)'), "Python requests call without explicit timeout parameter")
|
|
541
|
+
(re.compile(r'requests\.(?:get|post|put)\s*\((?![^)]*\btimeout\s*=)[^)]*\)'), "Python requests call without explicit timeout parameter"),
|
|
542
|
+
(re.compile(r'&http\.Client\s*\{\s*\}'), "Go http.Client initialized without explicit Timeout boundary (defaults to unbounded zero)"),
|
|
543
|
+
(re.compile(r'http\.DefaultClient\.(?:Get|Post|Do)\s*\('), "Go http.DefaultClient call with zero/unbounded request timeout")
|
|
537
544
|
]
|
|
538
545
|
},
|
|
539
546
|
|
|
@@ -693,7 +700,8 @@ RULE_PATTERNS: List[RulePattern] = [
|
|
|
693
700
|
"category": "supply",
|
|
694
701
|
"cluster": "cluster-supply-chain",
|
|
695
702
|
"patterns": [
|
|
696
|
-
(re.compile(r'^\s*package-lock\.json\s*$', re.MULTILINE), "Lockfile package-lock.json explicitly ignored in .gitignore")
|
|
703
|
+
(re.compile(r'^\s*package-lock\.json\s*$', re.MULTILINE), "Lockfile package-lock.json explicitly ignored in .gitignore"),
|
|
704
|
+
(re.compile(r'^\s*go\.sum\s*$', re.MULTILINE), "Go dependency checksum lockfile go.sum explicitly ignored in .gitignore")
|
|
697
705
|
]
|
|
698
706
|
},
|
|
699
707
|
{
|
|
@@ -1362,7 +1370,7 @@ def emit_run_artifacts(run_folder: Path, scored_findings: List[Dict[str, Any]],
|
|
|
1362
1370
|
pass
|
|
1363
1371
|
|
|
1364
1372
|
|
|
1365
|
-
def print_audit_dashboard(target_root: Path, file_count: int, scored_findings: List[Dict[str, Any]], clusters: Dict[str, List[Dict[str, Any]]], run_folder: Path, detected_stack: Optional[Dict[str, Any]] = None) -> None:
|
|
1373
|
+
def print_audit_dashboard(target_root: Path, file_count: int, scored_findings: List[Dict[str, Any]], clusters: Dict[str, List[Dict[str, Any]]], run_folder: Path, detected_stack: Optional[Dict[str, Any]] = None, perf_metrics: Optional[Dict[str, Any]] = None) -> None:
|
|
1366
1374
|
"""Print the unified 75-column TorusGuard Static Audit results card."""
|
|
1367
1375
|
crit_count = sum(1 for f in scored_findings if f["severity"] == "Critical")
|
|
1368
1376
|
high_count = sum(1 for f in scored_findings if f["severity"] == "High")
|
|
@@ -1373,13 +1381,17 @@ def print_audit_dashboard(target_root: Path, file_count: int, scored_findings: L
|
|
|
1373
1381
|
stack_str = f"{detected_stack.get('framework')} ({detected_stack.get('language')})"
|
|
1374
1382
|
|
|
1375
1383
|
print()
|
|
1376
|
-
print(card_header("🛡️ TORUSGUARD STATIC SECURITY AUDIT", "Autonomous AST & Invariant Security Scanner", "v1.
|
|
1384
|
+
print(card_header("🛡️ TORUSGUARD STATIC SECURITY AUDIT", "Autonomous AST & Invariant Security Scanner", "v1.4.0"))
|
|
1377
1385
|
print()
|
|
1378
1386
|
|
|
1379
1387
|
print(card_border_top("Audit Execution Scope"))
|
|
1380
1388
|
print(format_box_line(f"{BOLD}Target:{RESET} {WHITE}{str(target_root)}{RESET}"))
|
|
1381
1389
|
print(format_box_line(f"{BOLD}Stack:{RESET} {GREEN}{stack_str}{RESET}"))
|
|
1382
1390
|
print(format_box_line(f"{BOLD}Files:{RESET} {WHITE}{file_count} files evaluated across 18 rule families (74 rules){RESET}"))
|
|
1391
|
+
if perf_metrics:
|
|
1392
|
+
ms = perf_metrics.get("elapsed_ms", 0)
|
|
1393
|
+
fps = perf_metrics.get("throughput_fps", 0)
|
|
1394
|
+
print(format_box_line(f"{BOLD}Metrics:{RESET} {CYAN}{ms}ms{RESET} elapsed · {GREEN}{fps} files/sec{RESET} throughput"))
|
|
1383
1395
|
print(card_border_bottom())
|
|
1384
1396
|
print()
|
|
1385
1397
|
|
|
@@ -1389,6 +1401,17 @@ def print_audit_dashboard(target_root: Path, file_count: int, scored_findings: L
|
|
|
1389
1401
|
print(format_box_line(f"Status: {status_icon}"))
|
|
1390
1402
|
print(format_box_line(f"Findings: {RED}{crit_count} Critical{RESET} {YELLOW}{high_count} High{RESET} {CYAN}{med_count} Medium/Low{RESET} ({len(scored_findings)} total)"))
|
|
1391
1403
|
print(format_box_line(f"Clusters: {WHITE}{len(clusters)} architectural root causes identified{RESET}"))
|
|
1404
|
+
|
|
1405
|
+
# Rule Family Breakdown
|
|
1406
|
+
if scored_findings:
|
|
1407
|
+
fam_counts: Dict[str, int] = {}
|
|
1408
|
+
for f in scored_findings:
|
|
1409
|
+
rid = f.get("rule_id", "")
|
|
1410
|
+
parts = rid.split("-")
|
|
1411
|
+
fam = f"{parts[0]}-{parts[1]}" if len(parts) >= 2 else "OTHER"
|
|
1412
|
+
fam_counts[fam] = fam_counts.get(fam, 0) + 1
|
|
1413
|
+
fam_items = [f"{YELLOW}{k}{RESET}: {v}" for k, v in sorted(fam_counts.items(), key=lambda x: -x[1])[:4]]
|
|
1414
|
+
print(format_box_line(f"Families: {' '.join(fam_items)}"))
|
|
1392
1415
|
print(card_divider())
|
|
1393
1416
|
|
|
1394
1417
|
if not scored_findings:
|
|
@@ -1418,8 +1441,69 @@ def print_audit_dashboard(target_root: Path, file_count: int, scored_findings: L
|
|
|
1418
1441
|
print()
|
|
1419
1442
|
|
|
1420
1443
|
|
|
1444
|
+
def export_sarif(target_root: Path, run_folder_str: Optional[str], sarif_out: Optional[str] = None) -> None:
|
|
1445
|
+
"""Export audit run findings to OASIS SARIF v2.1.0 format."""
|
|
1446
|
+
s_dir = Path(__file__).resolve().parent
|
|
1447
|
+
sarif_script = s_dir / "sarif_exporter.py"
|
|
1448
|
+
if sarif_script.is_file():
|
|
1449
|
+
try:
|
|
1450
|
+
import importlib.util
|
|
1451
|
+
spec = importlib.util.spec_from_file_location("sarif_exporter", str(sarif_script))
|
|
1452
|
+
if spec and spec.loader:
|
|
1453
|
+
mod = importlib.util.module_from_spec(spec)
|
|
1454
|
+
spec.loader.exec_module(mod)
|
|
1455
|
+
run_id = Path(run_folder_str).name if run_folder_str else "audit-run"
|
|
1456
|
+
findings = []
|
|
1457
|
+
if run_folder_str and (Path(run_folder_str) / "findings.json").is_file():
|
|
1458
|
+
with open(Path(run_folder_str) / "findings.json", "r", encoding="utf-8") as f:
|
|
1459
|
+
findings = json.load(f)
|
|
1460
|
+
sarif_data = mod.generate_sarif(findings, run_id=run_id)
|
|
1461
|
+
out_path = Path(sarif_out) if sarif_out else (target_root / ".torusguard" / "reports" / "torusguard-audit.sarif")
|
|
1462
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1463
|
+
with open(out_path, "w", encoding="utf-8") as f:
|
|
1464
|
+
json.dump(sarif_data, f, indent=2)
|
|
1465
|
+
print(f" {GREEN}✔ OASIS SARIF v2.1.0 exported to:{RESET} {CYAN}{out_path}{RESET}\n")
|
|
1466
|
+
except Exception as e:
|
|
1467
|
+
print(f" {YELLOW}⚠ SARIF export notice: {e}{RESET}")
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
def run_watch_mode(target_root: Path, severity_floor: str = "medium", json_output: bool = False, include_tests: bool = False, sarif: bool = False, sarif_out: Optional[str] = None) -> None:
|
|
1471
|
+
"""Run audit continuously, re-scanning when files are modified."""
|
|
1472
|
+
print(f"\n {CYAN}👀 TorusGuard Watch Mode active.{RESET} Watching {WHITE}{target_root}{RESET} (Ctrl+C to stop)...\n")
|
|
1473
|
+
|
|
1474
|
+
def get_file_mtimes() -> Dict[str, float]:
|
|
1475
|
+
mtimes = {}
|
|
1476
|
+
for f in find_files_to_scan(target_root, include_tests=include_tests):
|
|
1477
|
+
try:
|
|
1478
|
+
mtimes[str(f)] = f.stat().st_mtime
|
|
1479
|
+
except OSError:
|
|
1480
|
+
pass
|
|
1481
|
+
return mtimes
|
|
1482
|
+
|
|
1483
|
+
last_mtimes = get_file_mtimes()
|
|
1484
|
+
res = execute_audit(target_root, severity_floor=severity_floor, json_output=json_output, include_tests=include_tests)
|
|
1485
|
+
if sarif:
|
|
1486
|
+
export_sarif(target_root, res.get("run_folder"), sarif_out)
|
|
1487
|
+
|
|
1488
|
+
try:
|
|
1489
|
+
while True:
|
|
1490
|
+
time.sleep(1.0)
|
|
1491
|
+
curr_mtimes = get_file_mtimes()
|
|
1492
|
+
if curr_mtimes != last_mtimes:
|
|
1493
|
+
changed = [f for f in curr_mtimes if f not in last_mtimes or curr_mtimes[f] != last_mtimes.get(f)]
|
|
1494
|
+
last_mtimes = curr_mtimes
|
|
1495
|
+
now_str = get_ist_now().strftime("%H:%M:%S")
|
|
1496
|
+
print(f"\n {CYAN}↻ File changes detected ({len(changed)} file(s)) at {now_str}. Re-scanning...{RESET}\n")
|
|
1497
|
+
res = execute_audit(target_root, severity_floor=severity_floor, json_output=json_output, include_tests=include_tests)
|
|
1498
|
+
if sarif:
|
|
1499
|
+
export_sarif(target_root, res.get("run_folder"), sarif_out)
|
|
1500
|
+
except KeyboardInterrupt:
|
|
1501
|
+
print(f"\n {YELLOW}🛑 Watch mode stopped.{RESET}\n")
|
|
1502
|
+
|
|
1503
|
+
|
|
1421
1504
|
def execute_audit(target_root: Path, severity_floor: str = "medium", json_output: bool = False, include_tests: bool = False) -> Dict[str, Any]:
|
|
1422
1505
|
"""Execute the full TorusGuard static security audit."""
|
|
1506
|
+
start_time = time.perf_counter()
|
|
1423
1507
|
target_root = target_root.resolve()
|
|
1424
1508
|
s_dir = Path(__file__).resolve().parent
|
|
1425
1509
|
|
|
@@ -1474,6 +1558,13 @@ def execute_audit(target_root: Path, severity_floor: str = "medium", json_output
|
|
|
1474
1558
|
except Exception:
|
|
1475
1559
|
pass
|
|
1476
1560
|
|
|
1561
|
+
elapsed_ms = int((time.perf_counter() - start_time) * 1000)
|
|
1562
|
+
throughput = round(len(files) / max(0.001, (elapsed_ms / 1000.0)), 1)
|
|
1563
|
+
perf_metrics = {
|
|
1564
|
+
"elapsed_ms": elapsed_ms,
|
|
1565
|
+
"throughput_fps": throughput
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1477
1568
|
result = {
|
|
1478
1569
|
"status": "completed",
|
|
1479
1570
|
"target_root": str(target_root),
|
|
@@ -1484,13 +1575,15 @@ def execute_audit(target_root: Path, severity_floor: str = "medium", json_output
|
|
|
1484
1575
|
"medium_count": sum(1 for f in scored_findings if f["severity"] in ("Medium", "Low")),
|
|
1485
1576
|
"clusters_count": len(clusters),
|
|
1486
1577
|
"run_folder": str(run_folder),
|
|
1487
|
-
"findings": scored_findings
|
|
1578
|
+
"findings": scored_findings,
|
|
1579
|
+
"elapsed_ms": elapsed_ms,
|
|
1580
|
+
"throughput_fps": throughput
|
|
1488
1581
|
}
|
|
1489
1582
|
|
|
1490
1583
|
if json_output:
|
|
1491
1584
|
print(json.dumps(result, indent=2))
|
|
1492
1585
|
else:
|
|
1493
|
-
print_audit_dashboard(target_root, len(files), scored_findings, clusters, run_folder, detected_stack)
|
|
1586
|
+
print_audit_dashboard(target_root, len(files), scored_findings, clusters, run_folder, detected_stack, perf_metrics=perf_metrics)
|
|
1494
1587
|
|
|
1495
1588
|
return result
|
|
1496
1589
|
|
|
@@ -1498,14 +1591,26 @@ def execute_audit(target_root: Path, severity_floor: str = "medium", json_output
|
|
|
1498
1591
|
def main():
|
|
1499
1592
|
parser = argparse.ArgumentParser(description="TorusGuard Static Security Audit Engine")
|
|
1500
1593
|
parser.add_argument("path", nargs="?", default=".", help="Target project root directory to audit")
|
|
1594
|
+
parser.add_argument("--target", "-t", help="Target project root directory (alias for path)")
|
|
1501
1595
|
parser.add_argument("--scope", "-s", help="Alternative path to target project")
|
|
1502
1596
|
parser.add_argument("--severity", choices=["critical", "high", "medium", "low"], default="medium", help="Severity floor")
|
|
1597
|
+
parser.add_argument("--watch", "-w", action="store_true", help="Continuous watch mode: re-scan on file save")
|
|
1598
|
+
parser.add_argument("--sarif", action="store_true", help="Automatically export findings to OASIS SARIF v2.1.0")
|
|
1599
|
+
parser.add_argument("--sarif-out", help="Output file path for SARIF export")
|
|
1503
1600
|
parser.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
1504
1601
|
parser.add_argument("--include-tests", action="store_true", help="Include test files and harness fixtures in scan")
|
|
1505
1602
|
args = parser.parse_args()
|
|
1506
1603
|
|
|
1507
|
-
target = Path(args.scope or args.path).resolve()
|
|
1604
|
+
target = Path(args.target or args.scope or args.path).resolve()
|
|
1605
|
+
|
|
1606
|
+
if args.watch:
|
|
1607
|
+
run_watch_mode(target, severity_floor=args.severity, json_output=args.json, include_tests=args.include_tests, sarif=args.sarif, sarif_out=args.sarif_out)
|
|
1608
|
+
sys.exit(0)
|
|
1609
|
+
|
|
1508
1610
|
res = execute_audit(target, severity_floor=args.severity, json_output=args.json, include_tests=args.include_tests)
|
|
1611
|
+
if args.sarif:
|
|
1612
|
+
export_sarif(target, res.get("run_folder"), args.sarif_out)
|
|
1613
|
+
|
|
1509
1614
|
sys.exit(0 if res["critical_count"] == 0 else 1)
|
|
1510
1615
|
|
|
1511
1616
|
|