torusguard 1.3.6 → 1.4.0

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.
Files changed (81) hide show
  1. package/.torusguard/.manifest.json +9 -8
  2. package/.torusguard/runs/report-latest.html +3529 -364
  3. package/.torusguard/runs/run-20260918-105940-audit/manifest.json +2 -1
  4. package/.torusguard/runs/run-20260918-105940-audit/remediation.md +7 -0
  5. package/.torusguard/runs/run-20260919-145701-audit/findings.json +1 -0
  6. package/.torusguard/runs/run-20260919-145701-audit/findings.md +8 -0
  7. package/.torusguard/runs/run-20260919-145701-audit/manifest.json +12 -0
  8. package/.torusguard/runs/run-20260919-145701-audit/summary.md +5 -0
  9. package/.torusguard/runs/run-20260919-151729-audit/findings.json +1 -0
  10. package/.torusguard/runs/run-20260919-151729-audit/findings.md +8 -0
  11. package/.torusguard/runs/run-20260919-151729-audit/manifest.json +13 -0
  12. package/.torusguard/runs/run-20260919-151729-audit/remediation.md +7 -0
  13. package/.torusguard/runs/run-20260919-151729-audit/summary.md +5 -0
  14. package/.torusguard/runs/run-20260919-152340-audit/findings.json +1 -0
  15. package/.torusguard/runs/run-20260919-152340-audit/findings.md +8 -0
  16. package/.torusguard/runs/run-20260919-152340-audit/manifest.json +12 -0
  17. package/.torusguard/runs/run-20260919-152340-audit/summary.md +5 -0
  18. package/.torusguard/runs/run-20260919-152345-audit/findings.json +1 -0
  19. package/.torusguard/runs/run-20260919-152345-audit/findings.md +8 -0
  20. package/.torusguard/runs/run-20260919-152345-audit/manifest.json +12 -0
  21. package/.torusguard/runs/run-20260919-152345-audit/summary.md +5 -0
  22. package/.torusguard/runs/run-20260919-153500-audit/findings.json +1 -0
  23. package/.torusguard/runs/run-20260919-153500-audit/findings.md +8 -0
  24. package/.torusguard/runs/run-20260919-153500-audit/manifest.json +12 -0
  25. package/.torusguard/runs/run-20260919-153500-audit/summary.md +5 -0
  26. package/.torusguard/scripts/__pycache__/apply_runner.cpython-314.pyc +0 -0
  27. package/.torusguard/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
  28. package/.torusguard/scripts/__pycache__/harden_runner.cpython-314.pyc +0 -0
  29. package/.torusguard/scripts/__pycache__/html_reporter.cpython-314.pyc +0 -0
  30. package/.torusguard/scripts/__pycache__/manifest_builder.cpython-314.pyc +0 -0
  31. package/.torusguard/scripts/__pycache__/recipes_runner.cpython-314.pyc +0 -0
  32. package/.torusguard/scripts/__pycache__/report_sync.cpython-314.pyc +0 -0
  33. package/.torusguard/scripts/apply_runner.py +101 -8
  34. package/.torusguard/scripts/audit_runner.py +116 -11
  35. package/.torusguard/scripts/harden_runner.py +122 -68
  36. package/.torusguard/scripts/html_reporter.py +1170 -253
  37. package/.torusguard/scripts/manifest_builder.py +2 -0
  38. package/.torusguard/scripts/recheck_runner.py +2 -1
  39. package/.torusguard/scripts/recipes_runner.py +91 -8
  40. package/.torusguard/scripts/report_sync.py +134 -7
  41. package/.torusguard/scripts/status_runner.py +267 -0
  42. package/README.md +108 -80
  43. package/bin/torusguard.js +190 -20
  44. package/package.json +6 -2
  45. package/skills/torusguard/__pycache__/bootstrap.cpython-314.pyc +0 -0
  46. package/skills/torusguard/bootstrap.py +45 -6
  47. package/skills/torusguard/payload/.manifest.json +29 -14
  48. package/skills/torusguard/payload/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
  49. package/skills/torusguard/payload/scripts/__pycache__/html_reporter.cpython-314.pyc +0 -0
  50. package/skills/torusguard/payload/scripts/__pycache__/report_sync.cpython-314.pyc +0 -0
  51. package/skills/torusguard/payload/scripts/__pycache__/sarif_exporter.cpython-314.pyc +0 -0
  52. package/skills/torusguard/payload/scripts/__pycache__/term_ui.cpython-314.pyc +0 -0
  53. package/skills/torusguard/payload/scripts/apply_runner.py +101 -8
  54. package/skills/torusguard/payload/scripts/audit_runner.py +116 -11
  55. package/skills/torusguard/payload/scripts/harden_runner.py +122 -68
  56. package/skills/torusguard/payload/scripts/html_reporter.py +1170 -253
  57. package/skills/torusguard/payload/scripts/manifest_builder.py +2 -0
  58. package/skills/torusguard/payload/scripts/recheck_runner.py +2 -1
  59. package/skills/torusguard/payload/scripts/recipes_runner.py +91 -8
  60. package/skills/torusguard/payload/scripts/report_sync.py +134 -7
  61. package/skills/torusguard/payload/scripts/status_runner.py +267 -0
  62. package/skills/torusguard/payload/workflows/apply.md +9 -1
  63. package/skills/torusguard/payload/workflows/audit.md +8 -2
  64. package/skills/torusguard/payload/workflows/harden.md +6 -1
  65. package/skills/torusguard/payload/workflows/init.md +7 -1
  66. package/skills/torusguard/payload/workflows/recipes.md +55 -0
  67. package/skills/torusguard/payload/workflows/report.md +59 -55
  68. package/skills/torusguard/payload/workflows/status.md +4 -1
  69. package/skills/torusguard/payload/workflows/torusguard-apply.md +110 -0
  70. package/skills/torusguard/payload/workflows/torusguard-audit.md +105 -0
  71. package/skills/torusguard/payload/workflows/torusguard-authorize.md +96 -0
  72. package/skills/torusguard/payload/workflows/torusguard-exploit-check.md +98 -0
  73. package/skills/torusguard/payload/workflows/torusguard-harden.md +104 -0
  74. package/skills/torusguard/payload/workflows/torusguard-init.md +106 -0
  75. package/skills/torusguard/payload/workflows/torusguard-recheck.md +101 -0
  76. package/skills/torusguard/payload/workflows/torusguard-recipes.md +55 -0
  77. package/skills/torusguard/payload/workflows/torusguard-report.md +105 -0
  78. package/skills/torusguard/payload/workflows/torusguard-status.md +103 -0
  79. package/skills/torusguard/payload/workflows/torusguard-verify.md +100 -0
  80. package/skills/torusguard/payload/workflows/torusguard-web-validate.md +96 -0
  81. package/skills/torusguard/payload/workflows/torusguard.md +19 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "torusguard",
3
- "version": "1.3.6",
3
+ "version": "1.4.0",
4
4
  "description": "Security guardrails, governed remediation, and authorized runtime validation for AI-built web applications.",
5
5
  "main": "bin/torusguard.js",
6
6
  "bin": {
@@ -17,8 +17,12 @@
17
17
  "scripts": {
18
18
  "test": "python harness/runner.py",
19
19
  "init": "node bin/torusguard.js init",
20
+ "status": "node bin/torusguard.js status",
20
21
  "audit": "node bin/torusguard.js audit",
21
- "status": "node bin/torusguard.js status"
22
+ "harden": "node bin/torusguard.js harden",
23
+ "apply": "node bin/torusguard.js apply",
24
+ "recipes": "node bin/torusguard.js recipes",
25
+ "update": "node bin/torusguard.js update"
22
26
  },
23
27
  "keywords": [
24
28
  "security",
@@ -135,7 +135,7 @@ def print_header():
135
135
  print(f"""
136
136
  {CYAN}╭─────────────────────────────────────────────────────────────────────────╮{RESET}
137
137
  {CYAN}│{RESET} {CYAN}│{RESET}
138
- {CYAN}│{RESET} {BOLD}{WHITE}🛡️ T O R U S G U A R D{RESET} {GRAY}v1.3.0{RESET} {CYAN}│{RESET}
138
+ {CYAN}│{RESET} {BOLD}{WHITE}🛡️ T O R U S G U A R D{RESET} {GRAY}v1.4.0{RESET} {CYAN}│{RESET}
139
139
  {CYAN}│{RESET} {DIM}Autonomous Security Engine for AI-Built Applications{RESET} {CYAN}│{RESET}
140
140
  {CYAN}│{RESET} {CYAN}│{RESET}
141
141
  {CYAN}╰─────────────────────────────────────────────────────────────────────────╯{RESET}
@@ -220,7 +220,7 @@ def print_already_initialized(target_root, cfg):
220
220
 
221
221
  {BOLD}▸ Project Root:{RESET} {GREEN}{target_root}{RESET}
222
222
  {BOLD}▸ Workspace:{RESET} {GREEN}.torusguard/{RESET} {DIM}(Already Initialized){RESET}
223
- {BOLD}▸ Version:{RESET} {CYAN}{cfg.get('version', '1.3.0')}{RESET}
223
+ {BOLD}▸ Version:{RESET} {CYAN}{cfg.get('version', '1.4.0')}{RESET}
224
224
  {BOLD}▸ Severity Floor:{RESET} {YELLOW}{cfg.get('severity_threshold', 'medium')}{RESET}
225
225
 
226
226
  {DIM}To refresh templates or re-scaffold, run:{RESET}
@@ -228,7 +228,7 @@ def print_already_initialized(target_root, cfg):
228
228
  """)
229
229
 
230
230
 
231
- def scaffold_workspace(target_root=None, force=False, full_commands=False):
231
+ def scaffold_workspace(target_root=None, force=False, full_commands=False, template=None, run_audit=False):
232
232
  """Scaffold the .torusguard workspace into the target project root."""
233
233
  target_root = Path(target_root or find_project_root()).resolve()
234
234
  torusguard_target = target_root / ".torusguard"
@@ -379,12 +379,26 @@ def scaffold_workspace(target_root=None, force=False, full_commands=False):
379
379
  except Exception:
380
380
  detected_stack = None
381
381
 
382
+ TEMPLATE_MAP = {
383
+ "golang": {"language": "Go", "framework": "Gin/Fiber", "data_layer": "database/sql"},
384
+ "go": {"language": "Go", "framework": "Gin/Fiber", "data_layer": "database/sql"},
385
+ "nextjs": {"language": "TypeScript", "framework": "Next.js", "data_layer": "Prisma ORM"},
386
+ "fastapi": {"language": "Python", "framework": "FastAPI", "data_layer": "SQLAlchemy"},
387
+ "express": {"language": "JavaScript", "framework": "Express", "data_layer": "Mongoose"},
388
+ "django": {"language": "Python", "framework": "Django", "data_layer": "Django ORM"},
389
+ "react": {"language": "JavaScript/TypeScript", "framework": "React / Vite", "data_layer": "Supabase"}
390
+ }
391
+ if template and template.lower() in TEMPLATE_MAP:
392
+ detected_stack = TEMPLATE_MAP[template.lower()]
393
+
382
394
  config_file = torusguard_target / "config" / "torusguard.json"
383
395
  if config_file.exists():
384
396
  try:
385
397
  with open(config_file, "r", encoding="utf-8") as f:
386
398
  cfg = json.load(f)
387
- if detected_stack and detected_stack.get("framework") != "None":
399
+ if template and template.lower() in TEMPLATE_MAP:
400
+ cfg["detected_stack"] = TEMPLATE_MAP[template.lower()]
401
+ elif detected_stack and detected_stack.get("framework") != "None":
388
402
  cfg["detected_stack"] = {
389
403
  "language": detected_stack.get("language"),
390
404
  "framework": detected_stack.get("framework"),
@@ -516,15 +530,40 @@ Parse the requested action from `$ARGUMENTS` (e.g. `audit`, `verify`, `web-valid
516
530
 
517
531
  # ── Success Card ────────────────────────────────────────────────────────
518
532
  print_success_card()
533
+
534
+ # ── Optional Immediate Audit ────────────────────────────────────────────
535
+ if run_audit:
536
+ audit_script = torusguard_target / "scripts" / "audit_runner.py"
537
+ if audit_script.is_file():
538
+ print(f"\n {CYAN}▸ Running immediate baseline security audit (--audit)...{RESET}\n")
539
+ try:
540
+ import importlib.util
541
+ spec = importlib.util.spec_from_file_location("audit_runner", str(audit_script))
542
+ if spec and spec.loader:
543
+ ar_mod = importlib.util.module_from_spec(spec)
544
+ spec.loader.exec_module(ar_mod)
545
+ if hasattr(ar_mod, "execute_audit"):
546
+ ar_mod.execute_audit(target_root)
547
+ except Exception as e:
548
+ print(f" {YELLOW}⚠ Immediate audit notice: {e}{RESET}")
549
+
519
550
  return True
520
551
 
521
552
 
522
553
  if __name__ == "__main__":
523
554
  parser = argparse.ArgumentParser(description="TorusGuard Autonomous Workspace Bootstrapper")
524
- parser.add_argument("--target", type=str, help="Target project root directory")
555
+ parser.add_argument("--target", "-t", type=str, help="Target project root directory")
525
556
  parser.add_argument("--force", action="store_true", help="Force overwrite existing workspace")
526
557
  parser.add_argument("--full-commands", action="store_true", help="Unlock and register all 11 individual slash commands")
558
+ parser.add_argument("--template", choices=["golang", "go", "nextjs", "fastapi", "express", "django", "react"], help="Pre-configure target framework stack template")
559
+ parser.add_argument("--audit", action="store_true", help="Execute immediate baseline audit post-initialization")
527
560
  args = parser.parse_args()
528
561
 
529
- success = scaffold_workspace(target_root=args.target, force=args.force, full_commands=args.full_commands)
562
+ success = scaffold_workspace(
563
+ target_root=args.target,
564
+ force=args.force,
565
+ full_commands=args.full_commands,
566
+ template=args.template,
567
+ run_audit=args.audit
568
+ )
530
569
  sys.exit(0 if success else 1)
@@ -142,23 +142,24 @@
142
142
  "schemas/remediation.schema.json": "544904239f871dec9552973e01e09dc1f40dbb8330f20d98c319f3b24031d630",
143
143
  "schemas/retest.schema.json": "97c0407ca4365a524020c430912f686f698cc0de90043194621071e9fc681cc7",
144
144
  "schemas/runtime-evidence.schema.json": "929d2b094c1cd1f7f9db07be1eba5994e8002477eab3fd24140e1e973fd18cc1",
145
- "scripts/apply_runner.py": "f9c5374ea213b77069293e45ed3d878d7f3f0e31497c55bc97885efc14bfa57c",
146
- "scripts/audit_runner.py": "98a3975ff79f8ffb0343d5606dd84a603fd35d741e2def6d70f83bc1ee8f01ff",
145
+ "scripts/apply_runner.py": "b145a68a4bc10fcacb853f972aee81fa3607e32d282b2b850c5a63d8e72da890",
146
+ "scripts/audit_runner.py": "bc1e06ea5153ae326c4ad1de5d0cbb9503e618186623bea7cb8bb64d7b7b05ec",
147
147
  "scripts/diff_guard.py": "0dc178cdde1ac0ae1a1ea4b2eb2ce1756b2126f6156909cf55d85249f115fcbd",
148
148
  "scripts/finding_scorer.py": "a5315be6ee256c01c4539da3d1905ebc8d0840a356b83c55f9f6f2654a9f0c60",
149
- "scripts/harden_runner.py": "f6f6922f2ade98b40f3de3e211459c62483e8aa274cbe4515dbd0d308956e6b4",
150
- "scripts/html_reporter.py": "e9901a1dbeddd7b1dccd04afc9005d64954ccc1f14ba6ec4bdb4aa0ea8c2c66c",
151
- "scripts/manifest_builder.py": "c0137927789a9e40a08243e630ac3c6990beb1b87807934e98906c8ad0b99a97",
149
+ "scripts/harden_runner.py": "5d5a40029c088ccb66790a9b0128bdefcd35883ef1dac78088ceb9531392a8f1",
150
+ "scripts/html_reporter.py": "655c57be39e13c177db8842ef471664e5853f3e43b9f2a63e590df40ff9c16cb",
151
+ "scripts/manifest_builder.py": "18db6979b341a87e9bf38fe06525365d12b07d532c0dbfd2ac3ef0f539dc1f6f",
152
152
  "scripts/memory_engine.py": "b715d0ca030e95eb06ad6eda99027535df8a7e37d64897c023be6a291cf13dc6",
153
153
  "scripts/monorepo_detector.py": "5603674136a5294ebfff89efeca0a546dd4a7deb1d184a2592ff21aff2131f73",
154
- "scripts/recheck_runner.py": "dd6f35c98f6fd3d41d641bca419bb90739551c4900724a65447882b6d7dd4f54",
155
- "scripts/recipes_runner.py": "6e057860e7d0e3b612a5b444b577255c9b7009b5dce0352098ab7d6c8c6659e7",
156
- "scripts/report_sync.py": "c5f37c69e2e074c8ffd5953811907cf7c95e4c56f406ea16f3b5b9d878cfb774",
154
+ "scripts/recheck_runner.py": "89e93fcc6843493d7e5f0bf97065dbdb40106273a00e07f903250665ca25f6fb",
155
+ "scripts/recipes_runner.py": "090c06ef0de3c640a111c5646837233ceeaca494b161bd5edc630cc1371dca0e",
156
+ "scripts/report_sync.py": "5ebef684690200694a53af30db850d9808d03ec710ed21ef75c4de52b5e5217c",
157
157
  "scripts/rules_sync.py": "4101b2ca8036ff4f468800b32a8d08898e8640a74d14124af53d454706a62d3a",
158
158
  "scripts/run_manager.py": "b738ff70879e46547d32fac6befc61de9599f676411384280fb9819ea3ad7035",
159
159
  "scripts/safety_gate.py": "3379b7534b2590cf563717a448983ad8ed7e8f174f0ad65683d33e8a10f51558",
160
160
  "scripts/sarif_exporter.py": "86700765212adb3ab590ca72614bf3ad4ce7ce60e3193f7f3bd1a04d34d10f6e",
161
161
  "scripts/stack_detect.py": "c05a1ade4c8692025476bf7ea080a8f07c92e5d8e9a04f94c1654386352ffbdc",
162
+ "scripts/status_runner.py": "4c358c91950c7874bcc6a5f23f2d3fda62b0752efcf6c6b658adf02ed77c7752",
162
163
  "scripts/term_ui.py": "ca69398f85c86fe89526d6334b3ee866c890273997351113c3cc1765b7bd20c3",
163
164
  "skills/torusguard/SKILL.md": "4d5b8e43fa3c09e5e1079cbc3e2184654219a2c47b2272db394da749969318f8",
164
165
  "skills/torusguard/bootstrap.py": "3d946f388cb4befa739c9d3b1404255d9a8ea5770ebbb79e5a1b9839589e9bc7",
@@ -206,16 +207,30 @@
206
207
  "templates/authorization.template.md": "81ef843d42a19412c369f383b787465ac24173c0b14c1f5f211786eab3d10e8b",
207
208
  "templates/finding-card.template.md": "8f756edbd3e4139d3da2fb96f35bd2d2b267b33b754f187b590a89584265c1e6",
208
209
  "templates/remediation-bundle.template.md": "4d85a540e3a2e74f9b21fc8a2a1072e27ca03e97cd67ca482f529cfe722acc4d",
209
- "workflows/apply.md": "8dd37fecc44e2820e82c29e2c6b2970936734ed5102d36950889a9eb8b19dbab",
210
- "workflows/audit.md": "7e78ebb885176d65e86ed45a90a4326b942d484c2b5c8bff1d7bda141790de2f",
210
+ "workflows/apply.md": "67e1002fc6e9d68e0c7d871c40b6f3c3b9c3919c8e02106a579882729c0f6b9f",
211
+ "workflows/audit.md": "eac9102ca88d53f71da474cd55717d431fd9c3b08c66d0760da9b53a659cf89a",
211
212
  "workflows/authorize.md": "28ca58cfc656e956f7cec8160f2aadc152abcde3eb5ad5588df149007afb0df8",
212
213
  "workflows/exploit-check.md": "41e77afe912be9266c5bc1e00fd796baff22a67973eb6e86d196d34f4d7bdfa4",
213
- "workflows/harden.md": "24c62ea0233bf5cb21308497869790fa6f24b6cbb563fd2ebf858ff02b09bba9",
214
- "workflows/init.md": "9a9aeaff6bd2217323ad72fb4043f46057deaa45eb3380f679d9aef3d36bfc8b",
214
+ "workflows/harden.md": "7d92132973ff867b99e0425e3aee3c4e91868cb5193ec601069af2e857b6697f",
215
+ "workflows/init.md": "611418577ff9d5719df459f56b3fa53b91eb3717b02c4b4e5e86ed483bed3bfa",
215
216
  "workflows/memory.md": "8ad2ec154176e2540c2859a28eff9f7c2dbca0217aa29d0f4aa88b87c43f8bef",
216
217
  "workflows/recheck.md": "42eecc3ba194564d48a6a88d8003e86cafe003503e887dc565314d4aa558e8fd",
217
- "workflows/report.md": "b98652ee0552b7bb73eeb78dc80d2b0a108cc505523cbd1b3ada6064409e4629",
218
- "workflows/status.md": "dfbc2b75ba45ff70b60522addca4032a667241364b0907cd77a43bf9271dfa56",
218
+ "workflows/recipes.md": "300c1fed0167e1a299fcd33c07db05630116b18a71ec2513c385862bba0f179b",
219
+ "workflows/report.md": "ecbd67bfa95bde3c0f51e06aac6fad70d5989b0be82245116cd7ba367f378a4c",
220
+ "workflows/status.md": "7f07e726bbb0488e48b718c2f36971634b1422bc2ddfbb381eb3e1ea67c1cfbf",
221
+ "workflows/torusguard-apply.md": "67e1002fc6e9d68e0c7d871c40b6f3c3b9c3919c8e02106a579882729c0f6b9f",
222
+ "workflows/torusguard-audit.md": "eac9102ca88d53f71da474cd55717d431fd9c3b08c66d0760da9b53a659cf89a",
223
+ "workflows/torusguard-authorize.md": "28ca58cfc656e956f7cec8160f2aadc152abcde3eb5ad5588df149007afb0df8",
224
+ "workflows/torusguard-exploit-check.md": "41e77afe912be9266c5bc1e00fd796baff22a67973eb6e86d196d34f4d7bdfa4",
225
+ "workflows/torusguard-harden.md": "7d92132973ff867b99e0425e3aee3c4e91868cb5193ec601069af2e857b6697f",
226
+ "workflows/torusguard-init.md": "611418577ff9d5719df459f56b3fa53b91eb3717b02c4b4e5e86ed483bed3bfa",
227
+ "workflows/torusguard-recheck.md": "42eecc3ba194564d48a6a88d8003e86cafe003503e887dc565314d4aa558e8fd",
228
+ "workflows/torusguard-recipes.md": "300c1fed0167e1a299fcd33c07db05630116b18a71ec2513c385862bba0f179b",
229
+ "workflows/torusguard-report.md": "ecbd67bfa95bde3c0f51e06aac6fad70d5989b0be82245116cd7ba367f378a4c",
230
+ "workflows/torusguard-status.md": "7f07e726bbb0488e48b718c2f36971634b1422bc2ddfbb381eb3e1ea67c1cfbf",
231
+ "workflows/torusguard-verify.md": "6412e123a9346240e5ba898a0da73d3409d9a215d6e8f202ab2eb5719b663731",
232
+ "workflows/torusguard-web-validate.md": "3b2a3d02b69469539b4a48ed3bd355362927cc21470dbcdeccba564a9e68a371",
233
+ "workflows/torusguard.md": "4b5da6d38dae6be277bd52782121cdb181a75f1400068041310ca69deae89589",
219
234
  "workflows/verify.md": "6412e123a9346240e5ba898a0da73d3409d9a215d6e8f202ab2eb5719b663731",
220
235
  "workflows/web-validate.md": "3b2a3d02b69469539b4a48ed3bd355362927cc21470dbcdeccba564a9e68a371"
221
236
  }
@@ -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.3.6", border_color: str = CYAN) -> str:
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.3.6"))
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.3.6"))
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.3.6"))
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(target, run_id_arg=args.run, auto_approve=args.yes)
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.6)
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.3.6"))
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