torusguard 0.9.5 → 1.3.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.
- package/.torusguard/.manifest.json +98 -79
- package/.torusguard/config/torusguard.json +1 -1
- package/.torusguard/references/csharp-security.md +41 -0
- package/.torusguard/references/go-security.md +41 -0
- package/.torusguard/references/java-security.md +40 -0
- package/.torusguard/references/polyglot-security-matrix.md +25 -0
- package/.torusguard/references/rust-security.md +40 -0
- package/.torusguard/rules/custom/.gitkeep +1 -0
- package/.torusguard/rules/custom/README.md +30 -0
- package/.torusguard/runs/report-latest.html +328 -0
- package/.torusguard/schemas/golden-recipe.schema.json +75 -0
- package/.torusguard/schemas/memory-context.schema.json +95 -0
- package/.torusguard/schemas/memory-event.schema.json +70 -0
- package/.torusguard/schemas/memory-pattern.schema.json +65 -0
- package/.torusguard/scripts/diff_guard.py +202 -15
- package/.torusguard/scripts/finding_scorer.py +199 -86
- package/.torusguard/scripts/html_reporter.py +628 -0
- package/.torusguard/scripts/manifest_builder.py +7 -5
- package/.torusguard/scripts/memory_engine.py +1334 -0
- package/.torusguard/scripts/monorepo_detector.py +123 -10
- package/.torusguard/scripts/rules_sync.py +321 -0
- package/.torusguard/scripts/run_manager.py +171 -89
- package/.torusguard/scripts/stack_detect.py +458 -26
- package/.torusguard/skills/torusguard/SKILL.md +4 -3
- package/.torusguard/skills/torusguard/bootstrap.py +344 -56
- package/.torusguard/skills/torusguard/references/csharp-security.md +41 -0
- package/.torusguard/skills/torusguard/references/go-security.md +41 -0
- package/.torusguard/skills/torusguard/references/java-security.md +40 -0
- package/.torusguard/skills/torusguard/references/polyglot-security-matrix.md +25 -0
- package/.torusguard/skills/torusguard/references/rust-security.md +40 -0
- package/.torusguard/skills/torusguard-audit/SKILL.md +25 -25
- package/.torusguard/skills/torusguard-harden/SKILL.md +3 -3
- package/.torusguard/skills/torusguard-recheck/SKILL.md +3 -3
- package/.torusguard/workflows/memory.md +53 -0
- package/README.md +307 -401
- package/bin/torusguard.js +204 -6
- package/package.json +1 -1
- package/skills/torusguard/SKILL.md +4 -3
- package/skills/torusguard/bootstrap.py +68 -4
- package/skills/torusguard/payload/.manifest.json +98 -79
- package/skills/torusguard/payload/config/torusguard.json +1 -1
- package/skills/torusguard/payload/references/csharp-security.md +41 -0
- package/skills/torusguard/payload/references/go-security.md +41 -0
- package/skills/torusguard/payload/references/java-security.md +40 -0
- package/skills/torusguard/payload/references/polyglot-security-matrix.md +25 -0
- package/skills/torusguard/payload/references/rust-security.md +40 -0
- package/skills/torusguard/payload/rules/custom/.gitkeep +1 -0
- package/skills/torusguard/payload/rules/custom/README.md +30 -0
- package/skills/torusguard/payload/schemas/golden-recipe.schema.json +75 -0
- package/skills/torusguard/payload/schemas/memory-context.schema.json +95 -0
- package/skills/torusguard/payload/schemas/memory-event.schema.json +70 -0
- package/skills/torusguard/payload/schemas/memory-pattern.schema.json +65 -0
- package/skills/torusguard/payload/scripts/diff_guard.py +202 -15
- package/skills/torusguard/payload/scripts/finding_scorer.py +199 -86
- package/skills/torusguard/payload/scripts/html_reporter.py +628 -0
- package/skills/torusguard/payload/scripts/manifest_builder.py +7 -5
- package/skills/torusguard/payload/scripts/memory_engine.py +1334 -0
- package/skills/torusguard/payload/scripts/monorepo_detector.py +123 -10
- package/skills/torusguard/payload/scripts/rules_sync.py +321 -0
- package/skills/torusguard/payload/scripts/run_manager.py +171 -89
- package/skills/torusguard/payload/scripts/stack_detect.py +458 -26
- package/skills/torusguard/payload/skills/torusguard/SKILL.md +4 -3
- package/skills/torusguard/payload/skills/torusguard/bootstrap.py +344 -56
- package/skills/torusguard/payload/skills/torusguard/references/csharp-security.md +41 -0
- package/skills/torusguard/payload/skills/torusguard/references/go-security.md +41 -0
- package/skills/torusguard/payload/skills/torusguard/references/java-security.md +40 -0
- package/skills/torusguard/payload/skills/torusguard/references/polyglot-security-matrix.md +25 -0
- package/skills/torusguard/payload/skills/torusguard/references/rust-security.md +40 -0
- package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +25 -25
- package/skills/torusguard/payload/skills/torusguard-harden/SKILL.md +3 -3
- package/skills/torusguard/payload/skills/torusguard-recheck/SKILL.md +3 -3
- package/skills/torusguard/payload/workflows/memory.md +53 -0
- package/skills/torusguard-audit/SKILL.md +25 -25
- package/skills/torusguard-harden/SKILL.md +3 -3
- package/skills/torusguard-recheck/SKILL.md +3 -3
- package/.torusguard/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
- package/.torusguard/scripts/__pycache__/monorepo_detector.cpython-311.pyc +0 -0
- package/skills/torusguard/__pycache__/bootstrap.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/monorepo_detector.cpython-311.pyc +0 -0
|
@@ -26,7 +26,7 @@ def detect_package_info(abs_pkg_dir: Path, root: Path) -> Dict[str, Any]:
|
|
|
26
26
|
# Check typescript / javascript
|
|
27
27
|
package_json = pkg_dir / "package.json"
|
|
28
28
|
if package_json.is_file() or list(pkg_dir.glob("*.ts")) or list(pkg_dir.glob("*.js")):
|
|
29
|
-
language = "
|
|
29
|
+
language = "TypeScript" if (list(pkg_dir.glob("*.ts")) or list(pkg_dir.glob("**/*.ts"))) else "JavaScript"
|
|
30
30
|
if package_json.is_file():
|
|
31
31
|
try:
|
|
32
32
|
with open(package_json, "r", encoding="utf-8") as f:
|
|
@@ -35,6 +35,8 @@ def detect_package_info(abs_pkg_dir: Path, root: Path) -> Dict[str, Any]:
|
|
|
35
35
|
deps = {**pj.get("dependencies", {}), **pj.get("devDependencies", {})}
|
|
36
36
|
if "next" in deps:
|
|
37
37
|
framework = "nextjs"
|
|
38
|
+
elif "@nestjs/core" in deps:
|
|
39
|
+
framework = "nestjs"
|
|
38
40
|
elif "express" in deps:
|
|
39
41
|
framework = "express"
|
|
40
42
|
elif "react" in deps:
|
|
@@ -51,7 +53,7 @@ def detect_package_info(abs_pkg_dir: Path, root: Path) -> Dict[str, Any]:
|
|
|
51
53
|
reqs = pkg_dir / "requirements.txt"
|
|
52
54
|
if pyproject.is_file() or reqs.is_file() or list(pkg_dir.glob("*.py")):
|
|
53
55
|
if language == "unknown":
|
|
54
|
-
language = "
|
|
56
|
+
language = "Python"
|
|
55
57
|
content = ""
|
|
56
58
|
if pyproject.is_file():
|
|
57
59
|
content += pyproject.read_text(encoding="utf-8", errors="replace").lower()
|
|
@@ -61,17 +63,116 @@ def detect_package_info(abs_pkg_dir: Path, root: Path) -> Dict[str, Any]:
|
|
|
61
63
|
content += pyf.read_text(encoding="utf-8", errors="replace").lower()
|
|
62
64
|
|
|
63
65
|
if "fastapi" in content or "fastapi" in pkg_dir.name:
|
|
64
|
-
framework = "
|
|
66
|
+
framework = "FastAPI"
|
|
65
67
|
elif "django" in content or "django" in pkg_dir.name:
|
|
66
|
-
framework = "
|
|
68
|
+
framework = "Django"
|
|
67
69
|
elif "flask" in content or "flask" in pkg_dir.name:
|
|
68
|
-
framework = "
|
|
70
|
+
framework = "Flask"
|
|
71
|
+
|
|
72
|
+
# Check Go
|
|
73
|
+
go_mod = pkg_dir / "go.mod"
|
|
74
|
+
if go_mod.is_file() or list(pkg_dir.glob("*.go")):
|
|
75
|
+
if language == "unknown":
|
|
76
|
+
language = "Go"
|
|
77
|
+
if go_mod.is_file():
|
|
78
|
+
try:
|
|
79
|
+
gc = go_mod.read_text(encoding="utf-8", errors="replace")
|
|
80
|
+
if "gin-gonic/gin" in gc:
|
|
81
|
+
framework = "Gin"
|
|
82
|
+
elif "gofiber/fiber" in gc:
|
|
83
|
+
framework = "Fiber"
|
|
84
|
+
elif "go-chi/chi" in gc:
|
|
85
|
+
framework = "Chi"
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
# Check Rust
|
|
90
|
+
cargo_toml = pkg_dir / "Cargo.toml"
|
|
91
|
+
if cargo_toml.is_file() or list(pkg_dir.glob("*.rs")):
|
|
92
|
+
if language == "unknown":
|
|
93
|
+
language = "Rust"
|
|
94
|
+
if cargo_toml.is_file():
|
|
95
|
+
try:
|
|
96
|
+
cc = cargo_toml.read_text(encoding="utf-8", errors="replace")
|
|
97
|
+
if "actix-web" in cc:
|
|
98
|
+
framework = "Actix-web"
|
|
99
|
+
elif "axum" in cc:
|
|
100
|
+
framework = "Axum"
|
|
101
|
+
elif "rocket" in cc:
|
|
102
|
+
framework = "Rocket"
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
# Check Java / Kotlin
|
|
107
|
+
pom_xml = pkg_dir / "pom.xml"
|
|
108
|
+
b_gradle = pkg_dir / "build.gradle"
|
|
109
|
+
b_gradle_kts = pkg_dir / "build.gradle.kts"
|
|
110
|
+
if pom_xml.is_file() or b_gradle.is_file() or b_gradle_kts.is_file() or list(pkg_dir.glob("**/*.java")) or list(pkg_dir.glob("**/*.kt")):
|
|
111
|
+
if language == "unknown":
|
|
112
|
+
language = "Kotlin" if (b_gradle_kts.is_file() or list(pkg_dir.glob("**/*.kt"))) else "Java"
|
|
113
|
+
jc = ""
|
|
114
|
+
for jf in [pom_xml, b_gradle, b_gradle_kts]:
|
|
115
|
+
if jf.is_file():
|
|
116
|
+
try:
|
|
117
|
+
jc += jf.read_text(encoding="utf-8", errors="replace") + "\n"
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
if "spring-boot" in jc or "springframework" in jc:
|
|
121
|
+
framework = "Spring Boot"
|
|
122
|
+
elif "quarkus" in jc:
|
|
123
|
+
framework = "Quarkus"
|
|
124
|
+
elif "ktor" in jc:
|
|
125
|
+
framework = "Ktor"
|
|
126
|
+
|
|
127
|
+
# Check C#
|
|
128
|
+
csprojs = list(pkg_dir.glob("*.csproj")) + list(pkg_dir.glob("**/*.csproj"))
|
|
129
|
+
if csprojs or list(pkg_dir.glob("*.sln")) or list(pkg_dir.glob("*.cs")):
|
|
130
|
+
if language == "unknown":
|
|
131
|
+
language = "C#"
|
|
132
|
+
csc = ""
|
|
133
|
+
for cf in csprojs[:5]:
|
|
134
|
+
try:
|
|
135
|
+
csc += cf.read_text(encoding="utf-8", errors="replace") + "\n"
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
if "Microsoft.NET.Sdk.Web" in csc or "Microsoft.AspNetCore" in csc or "Swashbuckle" in csc:
|
|
139
|
+
framework = "ASP.NET Core"
|
|
140
|
+
|
|
141
|
+
# Check PHP
|
|
142
|
+
composer_json = pkg_dir / "composer.json"
|
|
143
|
+
if composer_json.is_file() or list(pkg_dir.glob("*.php")):
|
|
144
|
+
if language == "unknown":
|
|
145
|
+
language = "PHP"
|
|
146
|
+
if composer_json.is_file():
|
|
147
|
+
try:
|
|
148
|
+
with open(composer_json, "r", encoding="utf-8") as f:
|
|
149
|
+
c_data = json.load(f)
|
|
150
|
+
reqs = {**c_data.get("require", {}), **c_data.get("require-dev", {})}
|
|
151
|
+
if "laravel/framework" in reqs:
|
|
152
|
+
framework = "Laravel"
|
|
153
|
+
elif any("symfony" in k for k in reqs):
|
|
154
|
+
framework = "Symfony"
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
# Check Ruby
|
|
159
|
+
gemfile = pkg_dir / "Gemfile"
|
|
160
|
+
if gemfile.is_file() or list(pkg_dir.glob("*.rb")):
|
|
161
|
+
if language == "unknown":
|
|
162
|
+
language = "Ruby"
|
|
163
|
+
if gemfile.is_file():
|
|
164
|
+
try:
|
|
165
|
+
gc = gemfile.read_text(encoding="utf-8", errors="replace")
|
|
166
|
+
if "rails" in gc:
|
|
167
|
+
framework = "Ruby on Rails"
|
|
168
|
+
except Exception:
|
|
169
|
+
pass
|
|
69
170
|
|
|
70
171
|
return {
|
|
71
172
|
"name": name,
|
|
72
173
|
"path": rel_path,
|
|
73
|
-
"language": language,
|
|
74
|
-
"framework": framework
|
|
174
|
+
"language": language.lower(),
|
|
175
|
+
"framework": framework.lower()
|
|
75
176
|
}
|
|
76
177
|
|
|
77
178
|
|
|
@@ -102,21 +203,33 @@ def scan_workspace(root_dir: str = ".") -> Dict[str, Any]:
|
|
|
102
203
|
pass
|
|
103
204
|
|
|
104
205
|
# Scan common monorepo container directories and direct project subfolders
|
|
105
|
-
candidate_folders = ["apps", "packages", "services", "libs", "modules"]
|
|
206
|
+
candidate_folders = ["apps", "packages", "services", "src", "libs", "modules"]
|
|
207
|
+
manifest_names = [
|
|
208
|
+
"package.json", "pyproject.toml", "requirements.txt", "setup.py", "Cargo.toml",
|
|
209
|
+
"go.mod", "pom.xml", "build.gradle", "build.gradle.kts", "composer.json",
|
|
210
|
+
"Gemfile", "mix.exs", "pubspec.yaml", "CMakeLists.txt", "main.py", "main.go", "actions.ts"
|
|
211
|
+
]
|
|
106
212
|
found_dirs = []
|
|
107
213
|
|
|
214
|
+
def has_manifest(d: Path) -> bool:
|
|
215
|
+
if any((d / m).exists() for m in manifest_names):
|
|
216
|
+
return True
|
|
217
|
+
if list(d.glob("*.csproj")) or list(d.glob("*/*.csproj")):
|
|
218
|
+
return True
|
|
219
|
+
return False
|
|
220
|
+
|
|
108
221
|
for cf in candidate_folders:
|
|
109
222
|
container = root / cf
|
|
110
223
|
if container.is_dir():
|
|
111
224
|
for child in container.iterdir():
|
|
112
225
|
if child.is_dir() and not child.name.startswith(('.', '_')):
|
|
113
|
-
if
|
|
226
|
+
if has_manifest(child):
|
|
114
227
|
found_dirs.append(child)
|
|
115
228
|
|
|
116
229
|
# Also scan direct subdirectories if not already captured
|
|
117
230
|
for child in root.iterdir():
|
|
118
231
|
if child.is_dir() and not child.name.startswith(('.', '_')) and child.name not in candidate_folders:
|
|
119
|
-
if
|
|
232
|
+
if has_manifest(child):
|
|
120
233
|
if child not in found_dirs:
|
|
121
234
|
found_dirs.append(child)
|
|
122
235
|
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
TorusGuard AI IDE Rules Auto-Sync Engine (v1.2.0)
|
|
4
|
+
Compiles project security memory, golden fix recipes, and active guardrails
|
|
5
|
+
into prompt-optimized rule files for Cursor, Claude Code, Antigravity, and Windsurf.
|
|
6
|
+
Non-destructive: preserves existing user rules via demarcated comment fences.
|
|
7
|
+
Zero external dependencies (Python 3.10+ stdlib).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import argparse
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
# Windows console UTF-8 support
|
|
18
|
+
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
|
19
|
+
try:
|
|
20
|
+
getattr(sys.stdout, "reconfigure")(encoding="utf-8", errors="replace")
|
|
21
|
+
except (AttributeError, OSError):
|
|
22
|
+
pass
|
|
23
|
+
if sys.stderr and hasattr(sys.stderr, "reconfigure"):
|
|
24
|
+
try:
|
|
25
|
+
getattr(sys.stderr, "reconfigure")(encoding="utf-8", errors="replace")
|
|
26
|
+
except (AttributeError, OSError):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
START_MARKER = "<!-- TORUSGUARD-SECURITY-GUARDRAILS:START -->"
|
|
30
|
+
END_MARKER = "<!-- TORUSGUARD-SECURITY-GUARDRAILS:END -->"
|
|
31
|
+
MAX_PROMPT_TOKENS = 400
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def find_project_root(start_dir: str | Path | None = None) -> Path:
|
|
35
|
+
"""Detect project root directory."""
|
|
36
|
+
current = Path(start_dir or Path.cwd()).resolve()
|
|
37
|
+
markers = [".git", "package.json", "pyproject.toml", ".torusguard"]
|
|
38
|
+
for m in markers:
|
|
39
|
+
if (current / m).exists():
|
|
40
|
+
return current
|
|
41
|
+
for parent in current.parents:
|
|
42
|
+
for m in markers:
|
|
43
|
+
if (parent / m).exists():
|
|
44
|
+
return parent
|
|
45
|
+
return current
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_security_context(root_dir: Path) -> dict[str, Any]:
|
|
49
|
+
"""Load security profile and patterns from memory subsystem."""
|
|
50
|
+
tg_dir = root_dir / ".torusguard"
|
|
51
|
+
memory_dir = tg_dir / "memory"
|
|
52
|
+
|
|
53
|
+
context_file = memory_dir / "context.json"
|
|
54
|
+
patterns_file = memory_dir / "patterns.json"
|
|
55
|
+
profile_file = memory_dir / "profile.json"
|
|
56
|
+
|
|
57
|
+
context_data: dict[str, Any] = {}
|
|
58
|
+
patterns: list[dict[str, Any]] = []
|
|
59
|
+
profile: dict[str, Any] = {}
|
|
60
|
+
|
|
61
|
+
if context_file.exists():
|
|
62
|
+
try:
|
|
63
|
+
context_data = json.loads(context_file.read_text(encoding="utf-8"))
|
|
64
|
+
except (json.JSONDecodeError, OSError):
|
|
65
|
+
context_data = {}
|
|
66
|
+
|
|
67
|
+
if patterns_file.exists():
|
|
68
|
+
try:
|
|
69
|
+
patterns = json.loads(patterns_file.read_text(encoding="utf-8"))
|
|
70
|
+
except (json.JSONDecodeError, OSError):
|
|
71
|
+
patterns = []
|
|
72
|
+
|
|
73
|
+
if profile_file.exists():
|
|
74
|
+
try:
|
|
75
|
+
profile = json.loads(profile_file.read_text(encoding="utf-8"))
|
|
76
|
+
except (json.JSONDecodeError, OSError):
|
|
77
|
+
profile = {}
|
|
78
|
+
|
|
79
|
+
cfg_file = tg_dir / "config" / "torusguard.json"
|
|
80
|
+
if cfg_file.exists():
|
|
81
|
+
try:
|
|
82
|
+
cfg = json.loads(cfg_file.read_text(encoding="utf-8"))
|
|
83
|
+
det = cfg.get("detected_stack", {})
|
|
84
|
+
if not profile.get("stack"):
|
|
85
|
+
lang = det.get("language")
|
|
86
|
+
fw = det.get("framework")
|
|
87
|
+
if lang and lang != "Unknown":
|
|
88
|
+
stk = [lang]
|
|
89
|
+
if fw and fw != "None":
|
|
90
|
+
stk.append(fw)
|
|
91
|
+
profile["stack"] = stk
|
|
92
|
+
except (json.JSONDecodeError, OSError):
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
# Dynamic fallback to stack_detect if stack is still unknown
|
|
96
|
+
if not profile.get("stack"):
|
|
97
|
+
try:
|
|
98
|
+
import stack_detect # type: ignore
|
|
99
|
+
det_stk = stack_detect.detect_stack(root_dir)
|
|
100
|
+
lang = det_stk.get("language")
|
|
101
|
+
fw = det_stk.get("framework")
|
|
102
|
+
if lang and lang != "Unknown":
|
|
103
|
+
stk = [lang]
|
|
104
|
+
if fw and fw != "None":
|
|
105
|
+
stk.append(fw)
|
|
106
|
+
profile["stack"] = stk
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
"context": context_data,
|
|
112
|
+
"patterns": patterns,
|
|
113
|
+
"profile": profile
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def format_dense_security_rules(sec_data: dict[str, Any]) -> str:
|
|
118
|
+
"""
|
|
119
|
+
Format active security memory into a dense, token-budgeted markdown block (<=400 tokens),
|
|
120
|
+
dynamically tailored to the detected language and framework family.
|
|
121
|
+
"""
|
|
122
|
+
patterns = sec_data.get("patterns", [])
|
|
123
|
+
profile = sec_data.get("profile", {})
|
|
124
|
+
stack = profile.get("stack", [])
|
|
125
|
+
|
|
126
|
+
golden_recipes: list[dict[str, Any]] = []
|
|
127
|
+
regressions: list[dict[str, Any]] = []
|
|
128
|
+
common_vulns: list[dict[str, Any]] = []
|
|
129
|
+
|
|
130
|
+
for p in patterns:
|
|
131
|
+
ptype = p.get("pattern_type")
|
|
132
|
+
if ptype == "golden_fix_recipe":
|
|
133
|
+
golden_recipes.append(p)
|
|
134
|
+
elif ptype == "regression_watch":
|
|
135
|
+
regressions.append(p)
|
|
136
|
+
elif ptype in ("common_vulnerability", "recurring_fix"):
|
|
137
|
+
common_vulns.append(p)
|
|
138
|
+
|
|
139
|
+
lines: list[str] = [
|
|
140
|
+
START_MARKER,
|
|
141
|
+
"## 🛡️ TorusGuard Active Project Security Invariants",
|
|
142
|
+
"> Auto-synced from `.torusguard/memory/`. Adhere to these guardrails in all generated code.",
|
|
143
|
+
""
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
if stack:
|
|
147
|
+
lines.append(f"**Detected Stack:** {', '.join(stack)}")
|
|
148
|
+
lines.append("")
|
|
149
|
+
|
|
150
|
+
# Core Behavioral Invariants (Stack-Adaptive & Token-Optimized)
|
|
151
|
+
stack_lower = " ".join(stack).lower()
|
|
152
|
+
sql_guards = []
|
|
153
|
+
bypass_guards = []
|
|
154
|
+
|
|
155
|
+
if "go" in stack_lower:
|
|
156
|
+
sql_guards.append("`db.Query(..., args)` / GORM")
|
|
157
|
+
bypass_guards.append("`InsecureSkipVerify: true`")
|
|
158
|
+
if "rust" in stack_lower:
|
|
159
|
+
sql_guards.append("`sqlx::query!` / Diesel")
|
|
160
|
+
bypass_guards.append("`unsafe { ... }`")
|
|
161
|
+
if "java" in stack_lower or "kotlin" in stack_lower:
|
|
162
|
+
sql_guards.append("JPA / PreparedStatement")
|
|
163
|
+
bypass_guards.append("`csrf().disable()`")
|
|
164
|
+
if "c#" in stack_lower or "dotnet" in stack_lower:
|
|
165
|
+
sql_guards.append("EF Core LINQ / `FromSqlInterpolated`")
|
|
166
|
+
bypass_guards.append("`[AllowAnonymous]`")
|
|
167
|
+
if "php" in stack_lower:
|
|
168
|
+
sql_guards.append("PDO prepared statements / Eloquent")
|
|
169
|
+
bypass_guards.append("`CURLOPT_SSL_VERIFYPEER => false`")
|
|
170
|
+
if "typescript" in stack_lower or "javascript" in stack_lower or "next" in stack_lower or "express" in stack_lower:
|
|
171
|
+
sql_guards.append("Prisma / Drizzle binders")
|
|
172
|
+
bypass_guards.append("`rejectUnauthorized: false`")
|
|
173
|
+
|
|
174
|
+
if not sql_guards:
|
|
175
|
+
sql_rule = "1. **Zero Raw SQL / String Formats:** Always use parameterized queries (e.g. `%s`, `?`, or ORM bound params)."
|
|
176
|
+
else:
|
|
177
|
+
sql_rule = f"1. **Zero Raw SQL:** Always use parameterized queries ({' / '.join(sql_guards[:2])})."
|
|
178
|
+
|
|
179
|
+
if not bypass_guards:
|
|
180
|
+
bypass_rule = "4. **No Security Bypasses:** Never insert `# nosec`, `verify=False`, `@csrf_exempt`, or `bypass_auth=True`."
|
|
181
|
+
else:
|
|
182
|
+
bypass_rule = f"4. **No Security Bypasses:** Never insert `# nosec`, `verify=False`, or {', '.join(bypass_guards[:2])}."
|
|
183
|
+
|
|
184
|
+
lines.extend([
|
|
185
|
+
"### Universal Guardrails",
|
|
186
|
+
sql_rule,
|
|
187
|
+
"2. **Tenant Isolation:** Enforce tenant/owner filtering on every database lookup (never query without `tenant_id` or owner scope).",
|
|
188
|
+
"3. **Ponytail Bound Rule:** Remediation patches must be minimal (<= 35 additions, <= 25 deletions).",
|
|
189
|
+
bypass_rule,
|
|
190
|
+
""
|
|
191
|
+
])
|
|
192
|
+
|
|
193
|
+
# Golden Fix Recipes (Top 2 to preserve tokens)
|
|
194
|
+
if golden_recipes:
|
|
195
|
+
lines.append("### Verified Golden Fix Patterns")
|
|
196
|
+
for rec in golden_recipes[:2]:
|
|
197
|
+
rule = rec.get("rule_id", "TG-SEC")
|
|
198
|
+
desc = rec.get("description", "Verified AST fix")
|
|
199
|
+
r_data = rec.get("recipe_data", {})
|
|
200
|
+
before = r_data.get("before_snippet", "")
|
|
201
|
+
after = r_data.get("after_snippet", "")
|
|
202
|
+
lines.append(f"- **{rule}** ({desc}):")
|
|
203
|
+
if before and after:
|
|
204
|
+
lines.append(f" - ❌ Avoid: `{before[:70]}`")
|
|
205
|
+
lines.append(f" - ✅ Prefer: `{after[:70]}`")
|
|
206
|
+
lines.append("")
|
|
207
|
+
|
|
208
|
+
# High-Risk Regression Watches
|
|
209
|
+
if regressions:
|
|
210
|
+
lines.append("### Active Regression Watches (Do Not Re-Introduce)")
|
|
211
|
+
for reg in regressions[:3]:
|
|
212
|
+
rule = reg.get("rule_id", "TG-REG")
|
|
213
|
+
files = ", ".join(reg.get("affected_files", [])[:2]) or "tracked files"
|
|
214
|
+
lines.append(f"- **{rule}**: Recurrence risk in `{files}`.")
|
|
215
|
+
lines.append("")
|
|
216
|
+
|
|
217
|
+
lines.append(END_MARKER)
|
|
218
|
+
return "\n".join(lines)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def inject_rules_into_file(file_path: Path, rules_block: str) -> bool:
|
|
222
|
+
"""
|
|
223
|
+
Non-destructively inject or replace the TorusGuard block in a target rules file.
|
|
224
|
+
Creates parent directories if necessary.
|
|
225
|
+
"""
|
|
226
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
227
|
+
existing_content = ""
|
|
228
|
+
|
|
229
|
+
if file_path.exists():
|
|
230
|
+
try:
|
|
231
|
+
existing_content = file_path.read_text(encoding="utf-8")
|
|
232
|
+
except OSError:
|
|
233
|
+
existing_content = ""
|
|
234
|
+
|
|
235
|
+
pattern = re.compile(
|
|
236
|
+
rf"{re.escape(START_MARKER)}.*?{re.escape(END_MARKER)}",
|
|
237
|
+
re.DOTALL
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if pattern.search(existing_content):
|
|
241
|
+
updated_content = pattern.sub(rules_block, existing_content)
|
|
242
|
+
else:
|
|
243
|
+
if existing_content.strip():
|
|
244
|
+
updated_content = existing_content.rstrip() + "\n\n" + rules_block + "\n"
|
|
245
|
+
else:
|
|
246
|
+
updated_content = rules_block + "\n"
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
file_path.write_text(updated_content, encoding="utf-8")
|
|
250
|
+
return True
|
|
251
|
+
except OSError as err:
|
|
252
|
+
print(f"Error writing rules to {file_path}: {err}", file=sys.stderr)
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def sync_rules(
|
|
257
|
+
target_format: str = "all",
|
|
258
|
+
root_dir: str | Path | None = None
|
|
259
|
+
) -> dict[str, Any]:
|
|
260
|
+
"""
|
|
261
|
+
Synchronize project security invariants into target IDE rules files.
|
|
262
|
+
Supported targets: 'cursor', 'claude', 'agent', 'windsurf', 'all'.
|
|
263
|
+
"""
|
|
264
|
+
root = Path(root_dir or find_project_root()).resolve()
|
|
265
|
+
sec_data = load_security_context(root)
|
|
266
|
+
rules_block = format_dense_security_rules(sec_data)
|
|
267
|
+
|
|
268
|
+
target_files: dict[str, Path] = {
|
|
269
|
+
"cursor": root / ".cursorrules",
|
|
270
|
+
"claude": root / "CLAUDE.md",
|
|
271
|
+
"agent": root / ".agent" / "rules" / "torusguard.md",
|
|
272
|
+
"windsurf": root / ".windsurfrules"
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
synced_targets: list[str] = []
|
|
276
|
+
formats_to_sync = list(target_files.keys()) if target_format == "all" else [target_format]
|
|
277
|
+
|
|
278
|
+
for fmt in formats_to_sync:
|
|
279
|
+
if fmt in target_files:
|
|
280
|
+
target_path = target_files[fmt]
|
|
281
|
+
success = inject_rules_into_file(target_path, rules_block)
|
|
282
|
+
if success:
|
|
283
|
+
synced_targets.append(str(target_path.relative_to(root)))
|
|
284
|
+
|
|
285
|
+
# Estimate token overhead (approx 4 chars/token)
|
|
286
|
+
token_est = max(1, len(rules_block) // 4)
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
"status": "success" if synced_targets else "skipped",
|
|
290
|
+
"synced_files": synced_targets,
|
|
291
|
+
"token_estimate": token_est,
|
|
292
|
+
"within_budget": token_est <= MAX_PROMPT_TOKENS
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def main():
|
|
297
|
+
parser = argparse.ArgumentParser(description="TorusGuard AI IDE Rules Auto-Sync Engine")
|
|
298
|
+
parser.add_argument(
|
|
299
|
+
"--format",
|
|
300
|
+
choices=["all", "cursor", "claude", "agent", "windsurf"],
|
|
301
|
+
default="all",
|
|
302
|
+
help="Target AI IDE rules format to sync"
|
|
303
|
+
)
|
|
304
|
+
parser.add_argument("--root", help="Project root override")
|
|
305
|
+
parser.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
306
|
+
|
|
307
|
+
args = parser.parse_args()
|
|
308
|
+
root = Path(args.root).resolve() if args.root else None
|
|
309
|
+
|
|
310
|
+
result = sync_rules(target_format=args.format, root_dir=root)
|
|
311
|
+
if args.json:
|
|
312
|
+
print(json.dumps(result, indent=2))
|
|
313
|
+
else:
|
|
314
|
+
print(f"[SUCCESS] Synced TorusGuard security invariants to {len(result['synced_files'])} file(s):")
|
|
315
|
+
for f in result["synced_files"]:
|
|
316
|
+
print(f" - {f}")
|
|
317
|
+
print(f"Token estimate: ~{result['token_estimate']} tokens (budget <= {MAX_PROMPT_TOKENS})")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
if __name__ == "__main__":
|
|
321
|
+
main()
|