torusguard 1.0.0 → 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.
Files changed (55) hide show
  1. package/.torusguard/.manifest.json +89 -75
  2. package/.torusguard/references/csharp-security.md +41 -0
  3. package/.torusguard/references/go-security.md +41 -0
  4. package/.torusguard/references/java-security.md +40 -0
  5. package/.torusguard/references/polyglot-security-matrix.md +25 -0
  6. package/.torusguard/references/rust-security.md +40 -0
  7. package/.torusguard/rules/custom/.gitkeep +1 -0
  8. package/.torusguard/rules/custom/README.md +30 -0
  9. package/.torusguard/runs/report-latest.html +328 -0
  10. package/.torusguard/schemas/golden-recipe.schema.json +75 -0
  11. package/.torusguard/scripts/diff_guard.py +131 -4
  12. package/.torusguard/scripts/finding_scorer.py +33 -5
  13. package/.torusguard/scripts/html_reporter.py +628 -0
  14. package/.torusguard/scripts/manifest_builder.py +5 -5
  15. package/.torusguard/scripts/memory_engine.py +521 -99
  16. package/.torusguard/scripts/monorepo_detector.py +123 -10
  17. package/.torusguard/scripts/rules_sync.py +321 -0
  18. package/.torusguard/scripts/stack_detect.py +458 -26
  19. package/.torusguard/skills/torusguard/references/csharp-security.md +41 -0
  20. package/.torusguard/skills/torusguard/references/go-security.md +41 -0
  21. package/.torusguard/skills/torusguard/references/java-security.md +40 -0
  22. package/.torusguard/skills/torusguard/references/polyglot-security-matrix.md +25 -0
  23. package/.torusguard/skills/torusguard/references/rust-security.md +40 -0
  24. package/README.md +307 -410
  25. package/bin/torusguard.js +108 -4
  26. package/package.json +1 -1
  27. package/skills/torusguard/bootstrap.py +2 -2
  28. package/skills/torusguard/payload/.manifest.json +89 -75
  29. package/skills/torusguard/payload/references/csharp-security.md +41 -0
  30. package/skills/torusguard/payload/references/go-security.md +41 -0
  31. package/skills/torusguard/payload/references/java-security.md +40 -0
  32. package/skills/torusguard/payload/references/polyglot-security-matrix.md +25 -0
  33. package/skills/torusguard/payload/references/rust-security.md +40 -0
  34. package/skills/torusguard/payload/rules/custom/.gitkeep +1 -0
  35. package/skills/torusguard/payload/rules/custom/README.md +30 -0
  36. package/skills/torusguard/payload/schemas/golden-recipe.schema.json +75 -0
  37. package/skills/torusguard/payload/scripts/diff_guard.py +131 -4
  38. package/skills/torusguard/payload/scripts/finding_scorer.py +33 -5
  39. package/skills/torusguard/payload/scripts/html_reporter.py +628 -0
  40. package/skills/torusguard/payload/scripts/manifest_builder.py +5 -5
  41. package/skills/torusguard/payload/scripts/memory_engine.py +521 -99
  42. package/skills/torusguard/payload/scripts/monorepo_detector.py +123 -10
  43. package/skills/torusguard/payload/scripts/rules_sync.py +321 -0
  44. package/skills/torusguard/payload/scripts/stack_detect.py +458 -26
  45. package/skills/torusguard/payload/skills/torusguard/bootstrap.py +2 -2
  46. package/skills/torusguard/payload/skills/torusguard/references/csharp-security.md +41 -0
  47. package/skills/torusguard/payload/skills/torusguard/references/go-security.md +41 -0
  48. package/skills/torusguard/payload/skills/torusguard/references/java-security.md +40 -0
  49. package/skills/torusguard/payload/skills/torusguard/references/polyglot-security-matrix.md +25 -0
  50. package/skills/torusguard/payload/skills/torusguard/references/rust-security.md +40 -0
  51. package/.torusguard/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
  52. package/.torusguard/scripts/__pycache__/finding_scorer.cpython-311.pyc +0 -0
  53. package/.torusguard/scripts/__pycache__/memory_engine.cpython-311.pyc +0 -0
  54. package/.torusguard/scripts/__pycache__/monorepo_detector.cpython-311.pyc +0 -0
  55. package/skills/torusguard/__pycache__/bootstrap.cpython-311.pyc +0 -0
@@ -1,21 +1,132 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- TorusGuard Stack Detector Utility
4
- Inspects project directory to detect programming language, frameworks, ORM/data layer,
5
- and dependency manifests. Outputs canonical stack block and structured JSON.
3
+ TorusGuard Universal Polyglot Stack Detector (v1.3.0)
4
+ Inspects project directory to detect programming language, framework, ORM/data layer,
5
+ and dependency manifests across 16+ languages, 30+ frameworks, and 20+ data layers.
6
+ Zero external dependencies (100% Python 3.10+ standard library).
6
7
  """
7
8
 
8
9
  import sys
9
10
  import json
10
11
  import argparse
11
12
  from pathlib import Path
12
- from typing import Dict, Any, List
13
+ from typing import Any
13
14
 
14
- def detect_stack(project_root: Path) -> Dict[str, Any]:
15
- profile = {
15
+ # Windows console UTF-8 support
16
+ if sys.stdout and hasattr(sys.stdout, "reconfigure"):
17
+ try:
18
+ getattr(sys.stdout, "reconfigure")(encoding="utf-8", errors="replace")
19
+ except (AttributeError, OSError):
20
+ pass
21
+ if sys.stderr and hasattr(sys.stderr, "reconfigure"):
22
+ try:
23
+ getattr(sys.stderr, "reconfigure")(encoding="utf-8", errors="replace")
24
+ except (AttributeError, OSError):
25
+ pass
26
+
27
+ # Comprehensive Manifest Catalog across 16+ Language Ecosystems
28
+ MANIFEST_CATALOG: dict[str, str] = {
29
+ # Python
30
+ "pyproject.toml": "Python",
31
+ "requirements.txt": "Python",
32
+ "Pipfile": "Python",
33
+ "poetry.lock": "Python",
34
+ "manage.py": "Python",
35
+ "setup.py": "Python",
36
+ # JavaScript / TypeScript
37
+ "package.json": "JavaScript / TypeScript",
38
+ "pnpm-lock.yaml": "JavaScript / TypeScript",
39
+ "yarn.lock": "JavaScript / TypeScript",
40
+ "bun.lockb": "JavaScript / TypeScript",
41
+ "deno.json": "JavaScript / TypeScript",
42
+ # Go
43
+ "go.mod": "Go",
44
+ "go.sum": "Go",
45
+ # Rust
46
+ "Cargo.toml": "Rust",
47
+ "Cargo.lock": "Rust",
48
+ # Java / Kotlin
49
+ "pom.xml": "Java",
50
+ "build.gradle": "Java / Kotlin",
51
+ "build.gradle.kts": "Kotlin",
52
+ "gradlew": "Java / Kotlin",
53
+ # C# / .NET
54
+ "global.json": "C# / .NET",
55
+ # PHP
56
+ "composer.json": "PHP",
57
+ "composer.lock": "PHP",
58
+ # Ruby
59
+ "Gemfile": "Ruby",
60
+ "Gemfile.lock": "Ruby",
61
+ # Elixir
62
+ "mix.exs": "Elixir",
63
+ "mix.lock": "Elixir",
64
+ # Dart / Flutter
65
+ "pubspec.yaml": "Dart / Flutter",
66
+ "pubspec.lock": "Dart / Flutter",
67
+ # C / C++
68
+ "CMakeLists.txt": "C / C++",
69
+ "Makefile": "C / C++",
70
+ "meson.build": "C / C++",
71
+ # Scala
72
+ "build.sbt": "Scala",
73
+ # Swift
74
+ "Package.swift": "Swift"
75
+ }
76
+
77
+ # Extension mappings for fallback file census
78
+ EXTENSION_MAP: dict[str, str] = {
79
+ ".py": "Python",
80
+ ".ts": "TypeScript",
81
+ ".tsx": "TypeScript",
82
+ ".js": "JavaScript",
83
+ ".jsx": "JavaScript",
84
+ ".go": "Go",
85
+ ".rs": "Rust",
86
+ ".java": "Java",
87
+ ".cs": "C#",
88
+ ".php": "PHP",
89
+ ".rb": "Ruby",
90
+ ".kt": "Kotlin",
91
+ ".kts": "Kotlin",
92
+ ".swift": "Swift",
93
+ ".dart": "Dart",
94
+ ".ex": "Elixir",
95
+ ".exs": "Elixir",
96
+ ".scala": "Scala",
97
+ ".c": "C",
98
+ ".cpp": "C++",
99
+ ".cc": "C++",
100
+ ".h": "C / C++",
101
+ ".hpp": "C++",
102
+ ".sh": "Shell / Bash",
103
+ ".sql": "SQL"
104
+ }
105
+
106
+
107
+ def census_file_extensions(project_root: Path) -> dict[str, int]:
108
+ """Tally source file extensions to infer primary languages when no manifest is present."""
109
+ census: dict[str, int] = {}
110
+ ignored_dirs = {".git", "node_modules", "vendor", ".venv", "venv", "dist", "build", ".torusguard", "target"}
111
+
112
+ for p in project_root.rglob("*"):
113
+ if p.is_file() and not any(part in ignored_dirs for part in p.parts):
114
+ ext = p.suffix.lower()
115
+ if ext in EXTENSION_MAP:
116
+ lang = EXTENSION_MAP[ext]
117
+ census[lang] = census.get(lang, 0) + 1
118
+ return census
119
+
120
+
121
+ def detect_stack(project_root: Path) -> dict[str, Any]:
122
+ """
123
+ Universally detect programming language, framework, ORM, and dependency manifests.
124
+ """
125
+ profile: dict[str, Any] = {
16
126
  "language": "Unknown",
17
127
  "framework": "None",
18
128
  "data_layer": "None",
129
+ "ecosystem_family": "universal",
19
130
  "dependency_files": [],
20
131
  "detection_evidence": [],
21
132
  "confidence": "Needs Review",
@@ -24,31 +135,47 @@ def detect_stack(project_root: Path) -> Dict[str, Any]:
24
135
  "TG-INPUT-*",
25
136
  "TG-AUTH-*"
26
137
  ],
27
- "recommended_references": []
138
+ "recommended_references": [
139
+ "polyglot-security-matrix.md"
140
+ ]
28
141
  }
29
142
 
30
- # 1. Inspect dependency manifests
31
- dep_candidates = ["pyproject.toml", "requirements.txt", "Pipfile", "package.json", "package-lock.json", "pnpm-lock.yaml"]
32
- for dc in dep_candidates:
33
- if (project_root / dc).is_file():
34
- profile["dependency_files"].append(dc)
143
+ # 1. Discover all dependency manifest files present
144
+ for manifest_name, lang in MANIFEST_CATALOG.items():
145
+ if (project_root / manifest_name).is_file():
146
+ profile["dependency_files"].append(manifest_name)
147
+
148
+ # Check for C# csproj/sln files (recursive)
149
+ csproj_files = list(project_root.glob("**/*.csproj"))
150
+ sln_files = list(project_root.glob("**/*.sln"))
151
+ for f in (csproj_files + sln_files)[:10]:
152
+ rel = str(f.relative_to(project_root))
153
+ if rel not in profile["dependency_files"]:
154
+ profile["dependency_files"].append(rel)
155
+
156
+ # 2. Inspect Ecosystems in order of specificity
35
157
 
36
- # 2. Check Python ecosystem
158
+ # -------------------------------------------------------------------------
159
+ # A. Python Ecosystem
160
+ # -------------------------------------------------------------------------
37
161
  manage_py = project_root / "manage.py"
38
162
  settings_py = list(project_root.glob("**/settings.py"))
39
-
163
+ has_py_manifest = any(f in profile["dependency_files"] for f in ["pyproject.toml", "requirements.txt", "Pipfile", "poetry.lock"])
164
+
40
165
  if manage_py.is_file() or settings_py:
41
166
  profile["language"] = "Python"
167
+ profile["ecosystem_family"] = "python"
42
168
  profile["framework"] = "Django"
43
169
  profile["data_layer"] = "Django ORM"
44
170
  profile["confidence"] = "Confirmed"
171
+ evidence_file = "manage.py" if manage_py.is_file() else str(settings_py[0].relative_to(project_root))
45
172
  profile["detection_evidence"].append({
46
- "file": "manage.py" if manage_py.is_file() else str(settings_py[0].relative_to(project_root)),
173
+ "file": evidence_file,
47
174
  "indicator": "Django management or settings configuration"
48
175
  })
49
176
  profile["recommended_rules"].extend(["TG-DB-004", "TG-RATE-001", "TG-PLATFORM-*"])
50
177
  profile["recommended_references"].extend(["django-security.md", "python-security-overview.md"])
51
-
178
+
52
179
  # Check for DRF
53
180
  for py_file in project_root.glob("**/*.py"):
54
181
  try:
@@ -60,13 +187,15 @@ def detect_stack(project_root: Path) -> Dict[str, Any]:
60
187
  except Exception:
61
188
  continue
62
189
 
63
- # Check for FastAPI / Flask if not Django
64
- if profile["framework"] == "None":
65
- for py_file in list(project_root.glob("*.py")) + list(project_root.glob("app/**/*.py")) + list(project_root.glob("src/**/*.py")):
190
+ elif has_py_manifest or list(project_root.glob("*.py")):
191
+ # Check FastAPI / Flask / SQLAlchemy
192
+ py_search_files = list(project_root.glob("*.py")) + list(project_root.glob("app/**/*.py")) + list(project_root.glob("src/**/*.py"))
193
+ for py_file in py_search_files[:25]:
66
194
  try:
67
195
  content = py_file.read_text(encoding="utf-8", errors="ignore")
68
196
  if "FastAPI(" in content or "from fastapi" in content:
69
197
  profile["language"] = "Python"
198
+ profile["ecosystem_family"] = "python"
70
199
  profile["framework"] = "FastAPI"
71
200
  profile["confidence"] = "Confirmed"
72
201
  profile["detection_evidence"].append({
@@ -76,6 +205,7 @@ def detect_stack(project_root: Path) -> Dict[str, Any]:
76
205
  profile["recommended_references"].append("fastapi-security.md")
77
206
  elif "Flask(__name__)" in content or "from flask" in content:
78
207
  profile["language"] = "Python"
208
+ profile["ecosystem_family"] = "python"
79
209
  profile["framework"] = "Flask"
80
210
  profile["confidence"] = "Confirmed"
81
211
  profile["detection_evidence"].append({
@@ -87,24 +217,62 @@ def detect_stack(project_root: Path) -> Dict[str, Any]:
87
217
  if "sqlalchemy" in content:
88
218
  profile["data_layer"] = "SQLAlchemy"
89
219
  profile["recommended_references"].append("sqlalchemy-security.md")
220
+ elif "tortoise" in content:
221
+ profile["data_layer"] = "Tortoise ORM"
90
222
  except Exception:
91
223
  continue
92
224
 
93
- # 3. Check JavaScript / TypeScript ecosystem
225
+ if profile["language"] == "Unknown" and has_py_manifest:
226
+ profile["language"] = "Python"
227
+ profile["ecosystem_family"] = "python"
228
+ profile["confidence"] = "High Confidence"
229
+ profile["detection_evidence"].append({
230
+ "file": profile["dependency_files"][0],
231
+ "indicator": "Python dependency manifest"
232
+ })
233
+ profile["recommended_references"].append("python-security-overview.md")
234
+
235
+ # -------------------------------------------------------------------------
236
+ # B. JavaScript / TypeScript Ecosystem
237
+ # -------------------------------------------------------------------------
94
238
  pkg_json_path = project_root / "package.json"
95
- if pkg_json_path.is_file():
96
- profile["language"] = "TypeScript" if list(project_root.glob("**/*.ts*")) else "JavaScript"
239
+ has_backend_manifest = any((project_root / m).is_file() for m in [
240
+ "composer.json", "mix.exs", "Gemfile", "Cargo.toml", "go.mod", "pom.xml", "build.gradle", "build.gradle.kts"
241
+ ]) or bool(csproj_files) or bool(sln_files)
242
+
243
+ if pkg_json_path.is_file() and profile["language"] == "Unknown" and not has_backend_manifest:
244
+ is_ts = bool(list(project_root.glob("**/*.ts*")))
245
+ profile["language"] = "TypeScript" if is_ts else "JavaScript"
246
+ profile["ecosystem_family"] = "javascript"
97
247
  try:
98
248
  with open(pkg_json_path, "r", encoding="utf-8") as f:
99
249
  pkg = json.load(f)
100
250
  deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
101
-
251
+
252
+ # Framework detection
102
253
  if "next" in deps:
103
254
  profile["framework"] = "Next.js"
104
255
  profile["confidence"] = "Confirmed"
105
256
  profile["detection_evidence"].append({"file": "package.json", "indicator": "next dependency"})
106
257
  profile["recommended_references"].append("nextjs-security.md")
107
258
  profile["recommended_rules"].extend(["TG-CLIENT-*", "TG-PLATFORM-001"])
259
+ elif "@nestjs/core" in deps:
260
+ profile["framework"] = "NestJS"
261
+ profile["confidence"] = "Confirmed"
262
+ profile["detection_evidence"].append({"file": "package.json", "indicator": "NestJS dependency"})
263
+ profile["recommended_references"].append("nestjs-security.md")
264
+ elif "@sveltejs/kit" in deps:
265
+ profile["framework"] = "SvelteKit"
266
+ profile["confidence"] = "Confirmed"
267
+ profile["detection_evidence"].append({"file": "package.json", "indicator": "SvelteKit dependency"})
268
+ elif "nuxt" in deps:
269
+ profile["framework"] = "Nuxt"
270
+ profile["confidence"] = "Confirmed"
271
+ profile["detection_evidence"].append({"file": "package.json", "indicator": "Nuxt dependency"})
272
+ elif "astro" in deps:
273
+ profile["framework"] = "Astro"
274
+ profile["confidence"] = "Confirmed"
275
+ profile["detection_evidence"].append({"file": "package.json", "indicator": "Astro dependency"})
108
276
  elif "express" in deps:
109
277
  profile["framework"] = "Express"
110
278
  profile["confidence"] = "Confirmed"
@@ -116,7 +284,16 @@ def detect_stack(project_root: Path) -> Dict[str, Any]:
116
284
  profile["detection_evidence"].append({"file": "package.json", "indicator": "react dependency"})
117
285
  profile["recommended_references"].append("react-vite-security.md")
118
286
 
119
- if "@supabase/supabase-js" in deps:
287
+ # ORM / Data layer detection
288
+ if "@prisma/client" in deps or "prisma" in deps:
289
+ profile["data_layer"] = "Prisma ORM"
290
+ elif "drizzle-orm" in deps:
291
+ profile["data_layer"] = "Drizzle ORM"
292
+ elif "typeorm" in deps:
293
+ profile["data_layer"] = "TypeORM"
294
+ elif "mongoose" in deps:
295
+ profile["data_layer"] = "Mongoose"
296
+ elif "@supabase/supabase-js" in deps:
120
297
  profile["data_layer"] = "Supabase"
121
298
  profile["recommended_references"].append("supabase-security.md")
122
299
  elif "firebase" in deps or "firebase-admin" in deps:
@@ -125,11 +302,264 @@ def detect_stack(project_root: Path) -> Dict[str, Any]:
125
302
  except Exception:
126
303
  pass
127
304
 
305
+ # -------------------------------------------------------------------------
306
+ # C. Go Ecosystem
307
+ # -------------------------------------------------------------------------
308
+ go_mod = project_root / "go.mod"
309
+ if go_mod.is_file():
310
+ profile["language"] = "Go"
311
+ profile["ecosystem_family"] = "go"
312
+ profile["confidence"] = "Confirmed"
313
+ profile["detection_evidence"].append({"file": "go.mod", "indicator": "Go module manifest"})
314
+ profile["recommended_rules"].extend(["TG-SEC-*", "TG-INPUT-001", "TG-INPUT-002", "TG-DB-004", "TG-SSRF-*"])
315
+ profile["recommended_references"].append("go-security.md")
316
+
317
+ try:
318
+ go_content = go_mod.read_text(encoding="utf-8", errors="ignore")
319
+ if "github.com/gin-gonic/gin" in go_content:
320
+ profile["framework"] = "Gin"
321
+ elif "github.com/gofiber/fiber" in go_content:
322
+ profile["framework"] = "Fiber"
323
+ elif "github.com/labstack/echo" in go_content:
324
+ profile["framework"] = "Echo"
325
+ elif "github.com/go-chi/chi" in go_content:
326
+ profile["framework"] = "Chi"
327
+
328
+ if "gorm.io/gorm" in go_content:
329
+ profile["data_layer"] = "GORM"
330
+ elif "github.com/jmoiron/sqlx" in go_content:
331
+ profile["data_layer"] = "SQLx"
332
+ elif "database/sql" in go_content or "github.com/lib/pq" in go_content or "github.com/go-sql-driver/mysql" in go_content:
333
+ profile["data_layer"] = "database/sql"
334
+ except Exception:
335
+ pass
336
+
337
+ # -------------------------------------------------------------------------
338
+ # D. Rust Ecosystem
339
+ # -------------------------------------------------------------------------
340
+ cargo_toml = project_root / "Cargo.toml"
341
+ if cargo_toml.is_file() and profile["language"] == "Unknown":
342
+ profile["language"] = "Rust"
343
+ profile["ecosystem_family"] = "rust"
344
+ profile["confidence"] = "Confirmed"
345
+ profile["detection_evidence"].append({"file": "Cargo.toml", "indicator": "Cargo package manifest"})
346
+ profile["recommended_rules"].extend(["TG-SEC-*", "TG-INPUT-001", "TG-DB-004", "TG-BIZ-001"])
347
+ profile["recommended_references"].append("rust-security.md")
348
+
349
+ try:
350
+ cargo_content = cargo_toml.read_text(encoding="utf-8", errors="ignore")
351
+ if "actix-web" in cargo_content:
352
+ profile["framework"] = "Actix-web"
353
+ elif "axum" in cargo_content:
354
+ profile["framework"] = "Axum"
355
+ elif "rocket" in cargo_content:
356
+ profile["framework"] = "Rocket"
357
+
358
+ if "diesel" in cargo_content:
359
+ profile["data_layer"] = "Diesel ORM"
360
+ elif "sqlx" in cargo_content:
361
+ profile["data_layer"] = "SQLx"
362
+ elif "sea-orm" in cargo_content:
363
+ profile["data_layer"] = "SeaORM"
364
+ except Exception:
365
+ pass
366
+
367
+ # -------------------------------------------------------------------------
368
+ # E. Java / Kotlin Ecosystem
369
+ # -------------------------------------------------------------------------
370
+ pom_files = [project_root / "pom.xml"] + list(project_root.glob("*/pom.xml"))[:10]
371
+ gradle_files = [project_root / "build.gradle", project_root / "build.gradle.kts"] + list(project_root.glob("*/build.gradle*"))[:10]
372
+ has_jvm_manifest = any(f.is_file() for f in pom_files + gradle_files)
373
+ if has_jvm_manifest and profile["language"] == "Unknown":
374
+ java_files = list(project_root.glob("**/*.java"))
375
+ kt_files = list(project_root.glob("**/*.kt"))
376
+ is_kotlin = (project_root / "build.gradle.kts").is_file() or (len(kt_files) > len(java_files) and len(kt_files) > 0)
377
+ profile["language"] = "Kotlin" if is_kotlin else "Java"
378
+ profile["ecosystem_family"] = "jvm"
379
+ profile["confidence"] = "Confirmed"
380
+ ev_file = "build.gradle.kts" if (project_root / "build.gradle.kts").is_file() else ("build.gradle" if (project_root / "build.gradle").is_file() else "pom.xml")
381
+ profile["detection_evidence"].append({"file": ev_file, "indicator": f"{profile['language']} build configuration"})
382
+ profile["recommended_rules"].extend(["TG-SEC-*", "TG-AUTH-*", "TG-INPUT-001", "TG-INPUT-002", "TG-DB-004"])
383
+ profile["recommended_references"].append("java-security.md")
384
+
385
+ check_content = ""
386
+ for jf in (pom_files + gradle_files)[:15]:
387
+ if jf.is_file():
388
+ try:
389
+ check_content += jf.read_text(encoding="utf-8", errors="ignore") + "\n"
390
+ except Exception:
391
+ pass
392
+
393
+ if "quarkus" in check_content:
394
+ profile["framework"] = "Quarkus"
395
+ elif "spring-boot" in check_content or "springframework" in check_content:
396
+ profile["framework"] = "Spring Boot"
397
+ elif "ktor" in check_content:
398
+ profile["framework"] = "Ktor"
399
+ elif "micronaut" in check_content:
400
+ profile["framework"] = "Micronaut"
401
+
402
+ if "hibernate" in check_content or "spring-boot-starter-data-jpa" in check_content or "quarkus-hibernate" in check_content:
403
+ profile["data_layer"] = "Hibernate / JPA"
404
+ elif "jooq" in check_content:
405
+ profile["data_layer"] = "jOOQ"
406
+
407
+ # -------------------------------------------------------------------------
408
+ # F. C# / .NET Ecosystem
409
+ # -------------------------------------------------------------------------
410
+ if (csproj_files or sln_files) and profile["language"] == "Unknown":
411
+ profile["language"] = "C#"
412
+ profile["ecosystem_family"] = "dotnet"
413
+ profile["confidence"] = "Confirmed"
414
+ ev_file = str(csproj_files[0].relative_to(project_root)) if csproj_files else str(sln_files[0].relative_to(project_root))
415
+ profile["detection_evidence"].append({"file": ev_file, "indicator": ".NET project or solution manifest"})
416
+ profile["recommended_rules"].extend(["TG-SEC-*", "TG-AUTH-*", "TG-INPUT-001", "TG-DB-004"])
417
+ profile["recommended_references"].append("csharp-security.md")
418
+
419
+ csproj_content = ""
420
+ for cf in csproj_files[:10]:
421
+ try:
422
+ csproj_content += cf.read_text(encoding="utf-8", errors="ignore") + "\n"
423
+ except Exception:
424
+ pass
425
+
426
+ if "Microsoft.NET.Sdk.Web" in csproj_content or "Microsoft.AspNetCore" in csproj_content or "Swashbuckle" in csproj_content:
427
+ profile["framework"] = "ASP.NET Core"
428
+ if "Microsoft.EntityFrameworkCore" in csproj_content:
429
+ profile["data_layer"] = "Entity Framework Core"
430
+ elif "Dapper" in csproj_content:
431
+ profile["data_layer"] = "Dapper"
432
+
433
+ # -------------------------------------------------------------------------
434
+ # G. PHP Ecosystem
435
+ # -------------------------------------------------------------------------
436
+ composer_json = project_root / "composer.json"
437
+ if composer_json.is_file() and profile["language"] == "Unknown":
438
+ profile["language"] = "PHP"
439
+ profile["ecosystem_family"] = "php"
440
+ profile["confidence"] = "Confirmed"
441
+ profile["detection_evidence"].append({"file": "composer.json", "indicator": "PHP Composer manifest"})
442
+ profile["recommended_rules"].extend(["TG-SEC-*", "TG-INPUT-001", "TG-INPUT-002", "TG-PLATFORM-*"])
443
+ profile["recommended_references"].append("polyglot-security-matrix.md")
444
+
445
+ try:
446
+ with open(composer_json, "r", encoding="utf-8") as f:
447
+ c_data = json.load(f)
448
+ reqs = {**c_data.get("require", {}), **c_data.get("require-dev", {})}
449
+ if "laravel/framework" in reqs:
450
+ profile["framework"] = "Laravel"
451
+ profile["data_layer"] = "Eloquent ORM"
452
+ elif "symfony/framework-bundle" in reqs or any("symfony/" in k for k in reqs):
453
+ profile["framework"] = "Symfony"
454
+ profile["data_layer"] = "Doctrine ORM"
455
+ except Exception:
456
+ pass
457
+
458
+ # -------------------------------------------------------------------------
459
+ # H. Ruby Ecosystem
460
+ # -------------------------------------------------------------------------
461
+ gemfile = project_root / "Gemfile"
462
+ if gemfile.is_file() and profile["language"] == "Unknown":
463
+ profile["language"] = "Ruby"
464
+ profile["ecosystem_family"] = "ruby"
465
+ profile["confidence"] = "Confirmed"
466
+ profile["detection_evidence"].append({"file": "Gemfile", "indicator": "Ruby Bundler manifest"})
467
+ profile["recommended_rules"].extend(["TG-SEC-*", "TG-AUTH-*", "TG-INPUT-001", "TG-DB-004"])
468
+ profile["recommended_references"].append("polyglot-security-matrix.md")
469
+
470
+ try:
471
+ g_content = gemfile.read_text(encoding="utf-8", errors="ignore")
472
+ if "rails" in g_content:
473
+ profile["framework"] = "Ruby on Rails"
474
+ profile["data_layer"] = "ActiveRecord"
475
+ elif "sinatra" in g_content:
476
+ profile["framework"] = "Sinatra"
477
+ except Exception:
478
+ pass
479
+
480
+ # -------------------------------------------------------------------------
481
+ # I. Elixir Ecosystem
482
+ # -------------------------------------------------------------------------
483
+ mix_exs = project_root / "mix.exs"
484
+ if mix_exs.is_file() and profile["language"] == "Unknown":
485
+ profile["language"] = "Elixir"
486
+ profile["ecosystem_family"] = "elixir"
487
+ profile["confidence"] = "Confirmed"
488
+ profile["detection_evidence"].append({"file": "mix.exs", "indicator": "Elixir Mix manifest"})
489
+ profile["recommended_references"].append("polyglot-security-matrix.md")
490
+ try:
491
+ m_content = mix_exs.read_text(encoding="utf-8", errors="ignore")
492
+ if "phoenix" in m_content:
493
+ profile["framework"] = "Phoenix"
494
+ profile["data_layer"] = "Ecto"
495
+ except Exception:
496
+ pass
497
+
498
+ # -------------------------------------------------------------------------
499
+ # J. Dart / Flutter Ecosystem
500
+ # -------------------------------------------------------------------------
501
+ pubspec = project_root / "pubspec.yaml"
502
+ if pubspec.is_file() and profile["language"] == "Unknown":
503
+ profile["language"] = "Dart"
504
+ profile["ecosystem_family"] = "dart"
505
+ profile["confidence"] = "Confirmed"
506
+ profile["detection_evidence"].append({"file": "pubspec.yaml", "indicator": "Dart pubspec manifest"})
507
+ try:
508
+ p_content = pubspec.read_text(encoding="utf-8", errors="ignore")
509
+ if "flutter:" in p_content:
510
+ profile["framework"] = "Flutter"
511
+ except Exception:
512
+ pass
513
+
514
+ # -------------------------------------------------------------------------
515
+ # K. Fallback: Extension Census (When No Manifest or Custom Structure)
516
+ # -------------------------------------------------------------------------
517
+ census = census_file_extensions(project_root)
518
+ profile["extension_census"] = census
519
+
520
+ if profile["language"] == "Unknown" and census:
521
+ top_lang = max(census.items(), key=lambda x: x[1])[0]
522
+ profile["language"] = top_lang
523
+ profile["confidence"] = "Inferred from file census"
524
+ profile["detection_evidence"].append({
525
+ "file": "source files",
526
+ "indicator": f"{census[top_lang]} {top_lang} source file(s) found"
527
+ })
528
+ family_map = {
529
+ "Go": "go", "Rust": "rust", "Java": "jvm", "Kotlin": "jvm",
530
+ "C#": "dotnet", "PHP": "php", "Ruby": "ruby", "Python": "python",
531
+ "TypeScript": "javascript", "JavaScript": "javascript",
532
+ "C": "native", "C++": "native", "Swift": "apple", "Dart": "dart"
533
+ }
534
+ profile["ecosystem_family"] = family_map.get(top_lang, "universal")
535
+
536
+ # -------------------------------------------------------------------------
537
+ # L. Monorepo & Multi-Package Workspace Integration
538
+ # -------------------------------------------------------------------------
539
+ try:
540
+ from monorepo_detector import scan_workspace
541
+ ws_res = scan_workspace(str(project_root))
542
+ if ws_res.get("is_monorepo"):
543
+ profile["is_monorepo"] = True
544
+ profile["monorepo_type"] = ws_res.get("monorepo_type", "monorepo")
545
+ profile["sub_stacks"] = ws_res.get("packages", [])
546
+ languages_found = list(set(
547
+ p["language"] for p in ws_res.get("packages", [])
548
+ if p.get("language") and p["language"] != "unknown"
549
+ ))
550
+ profile["sub_languages"] = languages_found
551
+ if len(languages_found) > 1:
552
+ profile["ecosystem_family"] = "polyglot_monorepo"
553
+ profile["recommended_rules"].extend(["TG-SEC-*", "TG-AUTH-*", "TG-PLATFORM-*"])
554
+ except Exception:
555
+ profile["is_monorepo"] = False
556
+ profile["sub_stacks"] = []
557
+
128
558
  return profile
129
559
 
130
560
 
131
561
  def main():
132
- parser = argparse.ArgumentParser(description="TorusGuard Stack Detector")
562
+ parser = argparse.ArgumentParser(description="TorusGuard Universal Polyglot Stack Detector")
133
563
  parser.add_argument("path", nargs="?", default=".", help="Target project root directory")
134
564
  parser.add_argument("--json", action="store_true", help="Output raw JSON")
135
565
  args = parser.parse_args()
@@ -141,12 +571,14 @@ def main():
141
571
  print(json.dumps(profile, indent=2))
142
572
  else:
143
573
  evidence = profile["detection_evidence"][0]["file"] if profile["detection_evidence"] else "heuristic"
144
- print(f"## Detected Stack")
574
+ print("## Detected Stack (Universal Polyglot)")
145
575
  print(f"- Language: {profile['language']}")
576
+ print(f"- Ecosystem Family: {profile.get('ecosystem_family', 'universal')}")
146
577
  print(f"- Framework: {profile['framework']}")
147
578
  print(f"- Data layer: {profile['data_layer']}")
148
579
  print(f"- Dependency files: {', '.join(profile['dependency_files']) if profile['dependency_files'] else 'None'}")
149
580
  print(f"- Detection confidence: {profile['confidence']} ({evidence})")
150
581
 
582
+
151
583
  if __name__ == "__main__":
152
584
  main()
@@ -0,0 +1,41 @@
1
+ # TorusGuard Skill Reference: C# & ASP.NET Core Security
2
+
3
+ > **Loaded When:** A project is identified as a C# / .NET application (`*.csproj`, `*.sln`, ASP.NET Core detected).
4
+
5
+ ---
6
+
7
+ ## 🛡️ Key Inspection Areas & Rules
8
+
9
+ ### 1. Authentication & Authorization
10
+ * `TG-AUTH-001`: Ensure controllers and endpoints have `[Authorize]` attributes.
11
+ * Prohibit unvetted `[AllowAnonymous]` on sensitive business logic or user data endpoints.
12
+
13
+ ### 2. Entity Framework Core & SQL Injection
14
+ * `TG-INPUT-002`: Use LINQ expressions or `FromSqlInterpolated` with interpolated string syntax (EF Core converts parameters automatically).
15
+ * Prohibit raw `FromSqlRaw` with un-parameterized string concatenations.
16
+ * `TG-DB-004`: Configure Global Query Filters (`HasQueryFilter(e => e.TenantId == _currentTenant)`) for multi-tenant safety.
17
+
18
+ ### 3. Anti-Forgery & TLS
19
+ * `TG-CSRF-001`: Ensure `[ValidateAntiForgeryToken]` or `[AutoValidateAntiforgeryToken]` is applied to POST/PUT/DELETE actions.
20
+ * Prohibit `DangerousAcceptAnyServerCertificateValidator`.
21
+
22
+ ---
23
+
24
+ ## 🛠️ Safe Patterns Summary
25
+
26
+ ```csharp
27
+ // Safe Entity Framework Parameterized Query
28
+ var user = await context.Users
29
+ .Where(u => u.Id == userId && u.TenantId == currentTenantId)
30
+ .FirstOrDefaultAsync();
31
+
32
+ // Safe Global Query Filter in DbContext
33
+ modelBuilder.Entity<Invoice>()
34
+ .HasQueryFilter(i => i.TenantId == _tenantProvider.GetTenantId());
35
+
36
+ // Safe Controller Security
37
+ [Authorize]
38
+ [ApiController]
39
+ [Route("api/[controller]")]
40
+ public class InvoicesController : ControllerBase { ... }
41
+ ```