torusguard 1.3.2 → 1.3.3

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.
@@ -1,584 +1,608 @@
1
- #!/usr/bin/env python3
2
- """
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).
7
- """
8
-
9
- import sys
10
- import json
11
- import argparse
12
- from pathlib import Path
13
- from typing import Any
14
-
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] = {
126
- "language": "Unknown",
127
- "framework": "None",
128
- "data_layer": "None",
129
- "ecosystem_family": "universal",
130
- "dependency_files": [],
131
- "detection_evidence": [],
132
- "confidence": "Needs Review",
133
- "recommended_rules": [
134
- "TG-SEC-*",
135
- "TG-INPUT-*",
136
- "TG-AUTH-*"
137
- ],
138
- "recommended_references": [
139
- "polyglot-security-matrix.md"
140
- ]
141
- }
142
-
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
157
-
158
- # -------------------------------------------------------------------------
159
- # A. Python Ecosystem
160
- # -------------------------------------------------------------------------
161
- manage_py = project_root / "manage.py"
162
- settings_py = list(project_root.glob("**/settings.py"))
163
- has_py_manifest = any(f in profile["dependency_files"] for f in ["pyproject.toml", "requirements.txt", "Pipfile", "poetry.lock"])
164
-
165
- if manage_py.is_file() or settings_py:
166
- profile["language"] = "Python"
167
- profile["ecosystem_family"] = "python"
168
- profile["framework"] = "Django"
169
- profile["data_layer"] = "Django ORM"
170
- profile["confidence"] = "Confirmed"
171
- evidence_file = "manage.py" if manage_py.is_file() else str(settings_py[0].relative_to(project_root))
172
- profile["detection_evidence"].append({
173
- "file": evidence_file,
174
- "indicator": "Django management or settings configuration"
175
- })
176
- profile["recommended_rules"].extend(["TG-DB-004", "TG-RATE-001", "TG-PLATFORM-*"])
177
- profile["recommended_references"].extend(["django-security.md", "python-security-overview.md"])
178
-
179
- # Check for DRF
180
- for py_file in project_root.glob("**/*.py"):
181
- try:
182
- content = py_file.read_text(encoding="utf-8", errors="ignore")
183
- if "rest_framework" in content:
184
- profile["framework"] = "Django / DRF"
185
- profile["recommended_references"].append("drf-security.md")
186
- break
187
- except Exception:
188
- continue
189
-
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]:
194
- try:
195
- content = py_file.read_text(encoding="utf-8", errors="ignore")
196
- if "FastAPI(" in content or "from fastapi" in content:
197
- profile["language"] = "Python"
198
- profile["ecosystem_family"] = "python"
199
- profile["framework"] = "FastAPI"
200
- profile["confidence"] = "Confirmed"
201
- profile["detection_evidence"].append({
202
- "file": str(py_file.relative_to(project_root)),
203
- "indicator": "FastAPI application instantiation"
204
- })
205
- profile["recommended_references"].append("fastapi-security.md")
206
- elif "Flask(__name__)" in content or "from flask" in content:
207
- profile["language"] = "Python"
208
- profile["ecosystem_family"] = "python"
209
- profile["framework"] = "Flask"
210
- profile["confidence"] = "Confirmed"
211
- profile["detection_evidence"].append({
212
- "file": str(py_file.relative_to(project_root)),
213
- "indicator": "Flask application instantiation"
214
- })
215
- profile["recommended_references"].append("flask-security.md")
216
-
217
- if "sqlalchemy" in content:
218
- profile["data_layer"] = "SQLAlchemy"
219
- profile["recommended_references"].append("sqlalchemy-security.md")
220
- elif "tortoise" in content:
221
- profile["data_layer"] = "Tortoise ORM"
222
- except Exception:
223
- continue
224
-
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
- # -------------------------------------------------------------------------
238
- pkg_json_path = project_root / "package.json"
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"
247
- try:
248
- with open(pkg_json_path, "r", encoding="utf-8") as f:
249
- pkg = json.load(f)
250
- deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
251
-
252
- # Framework detection
253
- if "next" in deps:
254
- profile["framework"] = "Next.js"
255
- profile["confidence"] = "Confirmed"
256
- profile["detection_evidence"].append({"file": "package.json", "indicator": "next dependency"})
257
- profile["recommended_references"].append("nextjs-security.md")
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"})
276
- elif "express" in deps:
277
- profile["framework"] = "Express"
278
- profile["confidence"] = "Confirmed"
279
- profile["detection_evidence"].append({"file": "package.json", "indicator": "express dependency"})
280
- profile["recommended_references"].append("express-security.md")
281
- elif "react" in deps:
282
- profile["framework"] = "React / Vite"
283
- profile["confidence"] = "Confirmed"
284
- profile["detection_evidence"].append({"file": "package.json", "indicator": "react dependency"})
285
- profile["recommended_references"].append("react-vite-security.md")
286
-
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:
297
- profile["data_layer"] = "Supabase"
298
- profile["recommended_references"].append("supabase-security.md")
299
- elif "firebase" in deps or "firebase-admin" in deps:
300
- profile["data_layer"] = "Firebase"
301
- profile["recommended_references"].append("firebase-security.md")
302
- except Exception:
303
- pass
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
-
558
- return profile
559
-
560
-
561
- def main():
562
- parser = argparse.ArgumentParser(description="TorusGuard Universal Polyglot Stack Detector")
563
- parser.add_argument("path", nargs="?", default=".", help="Target project root directory")
564
- parser.add_argument("--json", action="store_true", help="Output raw JSON")
565
- args = parser.parse_args()
566
-
567
- project_root = Path(args.path).resolve()
568
- profile = detect_stack(project_root)
569
-
570
- if args.json:
571
- print(json.dumps(profile, indent=2))
572
- else:
573
- evidence = profile["detection_evidence"][0]["file"] if profile["detection_evidence"] else "heuristic"
574
- print("## Detected Stack (Universal Polyglot)")
575
- print(f"- Language: {profile['language']}")
576
- print(f"- Ecosystem Family: {profile.get('ecosystem_family', 'universal')}")
577
- print(f"- Framework: {profile['framework']}")
578
- print(f"- Data layer: {profile['data_layer']}")
579
- print(f"- Dependency files: {', '.join(profile['dependency_files']) if profile['dependency_files'] else 'None'}")
580
- print(f"- Detection confidence: {profile['confidence']} ({evidence})")
581
-
582
-
583
- if __name__ == "__main__":
584
- main()
1
+ #!/usr/bin/env python3
2
+ """
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).
7
+ """
8
+
9
+ import sys
10
+ import json
11
+ import argparse
12
+ from pathlib import Path
13
+ from typing import Any
14
+
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
+
122
+ def add_to_list(profile, key, value):
123
+ if value and value not in profile[key] and value != "Unknown" and value != "None":
124
+ profile[key].append(value)
125
+
126
+ def detect_stack(project_root: Path) -> dict[str, Any]:
127
+ """
128
+ Universally detect programming language, framework, ORM, and dependency manifests.
129
+ """
130
+ profile: dict[str, Any] = {
131
+ "languages_found": [],
132
+ "frameworks_found": [],
133
+ "data_layers_found": [],
134
+ "language": "Unknown",
135
+ "framework": "None",
136
+ "data_layer": "None",
137
+ "ecosystem_family": "universal",
138
+ "dependency_files": [],
139
+ "detection_evidence": [],
140
+ "confidence": "Needs Review",
141
+ "recommended_rules": [
142
+ "TG-SEC-*",
143
+ "TG-INPUT-*",
144
+ "TG-AUTH-*"
145
+ ],
146
+ "recommended_references": [
147
+ "polyglot-security-matrix.md"
148
+ ]
149
+ }
150
+
151
+ # 1. Discover all dependency manifest files present
152
+ for manifest_name, lang in MANIFEST_CATALOG.items():
153
+ if (project_root / manifest_name).is_file():
154
+ profile["dependency_files"].append(manifest_name)
155
+
156
+ # Check for C# csproj/sln files (recursive)
157
+ csproj_files = list(project_root.glob("**/*.csproj"))
158
+ sln_files = list(project_root.glob("**/*.sln"))
159
+ for f in (csproj_files + sln_files)[:10]:
160
+ rel = str(f.relative_to(project_root))
161
+ if rel not in profile["dependency_files"]:
162
+ profile["dependency_files"].append(rel)
163
+
164
+ # 2. Inspect Ecosystems in order of specificity
165
+
166
+ # -------------------------------------------------------------------------
167
+ # A. Python Ecosystem
168
+ # -------------------------------------------------------------------------
169
+ manage_py = project_root / "manage.py"
170
+ settings_py = list(project_root.glob("**/settings.py"))
171
+ has_py_manifest = any(f in profile["dependency_files"] for f in ["pyproject.toml", "requirements.txt", "Pipfile", "poetry.lock"])
172
+
173
+ if manage_py.is_file() or settings_py:
174
+ add_to_list(profile, "languages_found", "Python")
175
+ profile["ecosystem_family"] = "python"
176
+ add_to_list(profile, "frameworks_found", "Django")
177
+ add_to_list(profile, "data_layers_found", "Django ORM")
178
+ profile["confidence"] = "Confirmed"
179
+ evidence_file = "manage.py" if manage_py.is_file() else str(settings_py[0].relative_to(project_root))
180
+ profile["detection_evidence"].append({
181
+ "file": evidence_file,
182
+ "indicator": "Django management or settings configuration"
183
+ })
184
+ profile["recommended_rules"].extend(["TG-DB-004", "TG-RATE-001", "TG-PLATFORM-*"])
185
+ profile["recommended_references"].extend(["django-security.md", "python-security-overview.md"])
186
+
187
+ # Check for DRF
188
+ for py_file in project_root.glob("**/*.py"):
189
+ try:
190
+ content = py_file.read_text(encoding="utf-8", errors="ignore")
191
+ if "rest_framework" in content:
192
+ add_to_list(profile, "frameworks_found", "Django / DRF")
193
+ profile["recommended_references"].append("drf-security.md")
194
+ break
195
+ except Exception:
196
+ continue
197
+
198
+ elif has_py_manifest or list(project_root.glob("*.py")):
199
+ # Check FastAPI / Flask / SQLAlchemy
200
+ py_search_files = list(project_root.glob("*.py")) + list(project_root.glob("app/**/*.py")) + list(project_root.glob("src/**/*.py"))
201
+ for py_file in py_search_files[:25]:
202
+ try:
203
+ content = py_file.read_text(encoding="utf-8", errors="ignore")
204
+ if "FastAPI(" in content or "from fastapi" in content:
205
+ add_to_list(profile, "languages_found", "Python")
206
+ profile["ecosystem_family"] = "python"
207
+ add_to_list(profile, "frameworks_found", "FastAPI")
208
+ profile["confidence"] = "Confirmed"
209
+ profile["detection_evidence"].append({
210
+ "file": str(py_file.relative_to(project_root)),
211
+ "indicator": "FastAPI application instantiation"
212
+ })
213
+ profile["recommended_references"].append("fastapi-security.md")
214
+ elif "Flask(__name__)" in content or "from flask" in content:
215
+ add_to_list(profile, "languages_found", "Python")
216
+ profile["ecosystem_family"] = "python"
217
+ add_to_list(profile, "frameworks_found", "Flask")
218
+ profile["confidence"] = "Confirmed"
219
+ profile["detection_evidence"].append({
220
+ "file": str(py_file.relative_to(project_root)),
221
+ "indicator": "Flask application instantiation"
222
+ })
223
+ profile["recommended_references"].append("flask-security.md")
224
+
225
+ if "sqlalchemy" in content:
226
+ add_to_list(profile, "data_layers_found", "SQLAlchemy")
227
+ profile["recommended_references"].append("sqlalchemy-security.md")
228
+ elif "tortoise" in content:
229
+ add_to_list(profile, "data_layers_found", "Tortoise ORM")
230
+ except Exception:
231
+ continue
232
+
233
+ if has_py_manifest:
234
+ add_to_list(profile, "languages_found", "Python")
235
+ profile["ecosystem_family"] = "python"
236
+ profile["confidence"] = "High Confidence"
237
+ profile["detection_evidence"].append({
238
+ "file": profile["dependency_files"][0],
239
+ "indicator": "Python dependency manifest"
240
+ })
241
+ profile["recommended_references"].append("python-security-overview.md")
242
+
243
+ # -------------------------------------------------------------------------
244
+ # B. JavaScript / TypeScript Ecosystem
245
+ # -------------------------------------------------------------------------
246
+ pkg_json_path = project_root / "package.json"
247
+ has_backend_manifest = any((project_root / m).is_file() for m in [
248
+ "composer.json", "mix.exs", "Gemfile", "Cargo.toml", "go.mod", "pom.xml", "build.gradle", "build.gradle.kts"
249
+ ]) or bool(csproj_files) or bool(sln_files)
250
+
251
+ if pkg_json_path.is_file():
252
+ is_ts = bool(list(project_root.glob("**/*.ts*")))
253
+ add_to_list(profile, "languages_found", "TypeScript" if is_ts else "JavaScript")
254
+ profile["ecosystem_family"] = "javascript"
255
+ try:
256
+ with open(pkg_json_path, "r", encoding="utf-8") as f:
257
+ pkg = json.load(f)
258
+ deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
259
+
260
+ # Framework detection
261
+ if "next" in deps:
262
+ add_to_list(profile, "frameworks_found", "Next.js")
263
+ profile["confidence"] = "Confirmed"
264
+ profile["detection_evidence"].append({"file": "package.json", "indicator": "next dependency"})
265
+ profile["recommended_references"].append("nextjs-security.md")
266
+ profile["recommended_rules"].extend(["TG-CLIENT-*", "TG-PLATFORM-001"])
267
+ elif "@nestjs/core" in deps:
268
+ add_to_list(profile, "frameworks_found", "NestJS")
269
+ profile["confidence"] = "Confirmed"
270
+ profile["detection_evidence"].append({"file": "package.json", "indicator": "NestJS dependency"})
271
+ profile["recommended_references"].append("nestjs-security.md")
272
+ elif "@sveltejs/kit" in deps:
273
+ add_to_list(profile, "frameworks_found", "SvelteKit")
274
+ profile["confidence"] = "Confirmed"
275
+ profile["detection_evidence"].append({"file": "package.json", "indicator": "SvelteKit dependency"})
276
+ elif "nuxt" in deps:
277
+ add_to_list(profile, "frameworks_found", "Nuxt")
278
+ profile["confidence"] = "Confirmed"
279
+ profile["detection_evidence"].append({"file": "package.json", "indicator": "Nuxt dependency"})
280
+ elif "astro" in deps:
281
+ add_to_list(profile, "frameworks_found", "Astro")
282
+ profile["confidence"] = "Confirmed"
283
+ profile["detection_evidence"].append({"file": "package.json", "indicator": "Astro dependency"})
284
+ elif "express" in deps:
285
+ add_to_list(profile, "frameworks_found", "Express")
286
+ profile["confidence"] = "Confirmed"
287
+ profile["detection_evidence"].append({"file": "package.json", "indicator": "express dependency"})
288
+ profile["recommended_references"].append("express-security.md")
289
+ elif "react" in deps:
290
+ add_to_list(profile, "frameworks_found", "React / Vite")
291
+ profile["confidence"] = "Confirmed"
292
+ profile["detection_evidence"].append({"file": "package.json", "indicator": "react dependency"})
293
+ profile["recommended_references"].append("react-vite-security.md")
294
+
295
+ # ORM / Data layer detection
296
+ if "@prisma/client" in deps or "prisma" in deps:
297
+ add_to_list(profile, "data_layers_found", "Prisma ORM")
298
+ elif "drizzle-orm" in deps:
299
+ add_to_list(profile, "data_layers_found", "Drizzle ORM")
300
+ elif "typeorm" in deps:
301
+ add_to_list(profile, "data_layers_found", "TypeORM")
302
+ elif "mongoose" in deps:
303
+ add_to_list(profile, "data_layers_found", "Mongoose")
304
+ elif "@supabase/supabase-js" in deps:
305
+ add_to_list(profile, "data_layers_found", "Supabase")
306
+ profile["recommended_references"].append("supabase-security.md")
307
+ elif "firebase" in deps or "firebase-admin" in deps:
308
+ add_to_list(profile, "data_layers_found", "Firebase")
309
+ profile["recommended_references"].append("firebase-security.md")
310
+ except Exception:
311
+ pass
312
+
313
+ # -------------------------------------------------------------------------
314
+ # C. Go Ecosystem
315
+ # -------------------------------------------------------------------------
316
+ go_mod = project_root / "go.mod"
317
+ if go_mod.is_file():
318
+ add_to_list(profile, "languages_found", "Go")
319
+ profile["ecosystem_family"] = "go"
320
+ profile["confidence"] = "Confirmed"
321
+ profile["detection_evidence"].append({"file": "go.mod", "indicator": "Go module manifest"})
322
+ profile["recommended_rules"].extend(["TG-SEC-*", "TG-INPUT-001", "TG-INPUT-002", "TG-DB-004", "TG-SSRF-*"])
323
+ profile["recommended_references"].append("go-security.md")
324
+
325
+ try:
326
+ go_content = go_mod.read_text(encoding="utf-8", errors="ignore")
327
+ if "github.com/gin-gonic/gin" in go_content:
328
+ add_to_list(profile, "frameworks_found", "Gin")
329
+ elif "github.com/gofiber/fiber" in go_content:
330
+ add_to_list(profile, "frameworks_found", "Fiber")
331
+ elif "github.com/labstack/echo" in go_content:
332
+ add_to_list(profile, "frameworks_found", "Echo")
333
+ elif "github.com/go-chi/chi" in go_content:
334
+ add_to_list(profile, "frameworks_found", "Chi")
335
+
336
+ if "gorm.io/gorm" in go_content:
337
+ add_to_list(profile, "data_layers_found", "GORM")
338
+ elif "github.com/jmoiron/sqlx" in go_content:
339
+ add_to_list(profile, "data_layers_found", "SQLx")
340
+ elif "database/sql" in go_content or "github.com/lib/pq" in go_content or "github.com/go-sql-driver/mysql" in go_content:
341
+ add_to_list(profile, "data_layers_found", "database/sql")
342
+ except Exception:
343
+ pass
344
+
345
+ # -------------------------------------------------------------------------
346
+ # D. Rust Ecosystem
347
+ # -------------------------------------------------------------------------
348
+ cargo_toml = project_root / "Cargo.toml"
349
+ if cargo_toml.is_file():
350
+ add_to_list(profile, "languages_found", "Rust")
351
+ profile["ecosystem_family"] = "rust"
352
+ profile["confidence"] = "Confirmed"
353
+ profile["detection_evidence"].append({"file": "Cargo.toml", "indicator": "Cargo package manifest"})
354
+ profile["recommended_rules"].extend(["TG-SEC-*", "TG-INPUT-001", "TG-DB-004", "TG-BIZ-001"])
355
+ profile["recommended_references"].append("rust-security.md")
356
+
357
+ try:
358
+ cargo_content = cargo_toml.read_text(encoding="utf-8", errors="ignore")
359
+ if "actix-web" in cargo_content:
360
+ add_to_list(profile, "frameworks_found", "Actix-web")
361
+ elif "axum" in cargo_content:
362
+ add_to_list(profile, "frameworks_found", "Axum")
363
+ elif "rocket" in cargo_content:
364
+ add_to_list(profile, "frameworks_found", "Rocket")
365
+
366
+ if "diesel" in cargo_content:
367
+ add_to_list(profile, "data_layers_found", "Diesel ORM")
368
+ elif "sqlx" in cargo_content:
369
+ add_to_list(profile, "data_layers_found", "SQLx")
370
+ elif "sea-orm" in cargo_content:
371
+ add_to_list(profile, "data_layers_found", "SeaORM")
372
+ except Exception:
373
+ pass
374
+
375
+ # -------------------------------------------------------------------------
376
+ # E. Java / Kotlin Ecosystem
377
+ # -------------------------------------------------------------------------
378
+ pom_files = [project_root / "pom.xml"] + list(project_root.glob("*/pom.xml"))[:10]
379
+ gradle_files = [project_root / "build.gradle", project_root / "build.gradle.kts"] + list(project_root.glob("*/build.gradle*"))[:10]
380
+ has_jvm_manifest = any(f.is_file() for f in pom_files + gradle_files)
381
+ if has_jvm_manifest:
382
+ java_files = list(project_root.glob("**/*.java"))
383
+ kt_files = list(project_root.glob("**/*.kt"))
384
+ is_kotlin = (project_root / "build.gradle.kts").is_file() or (len(kt_files) > len(java_files) and len(kt_files) > 0)
385
+ add_to_list(profile, "languages_found", "Kotlin" if is_kotlin else "Java")
386
+ profile["ecosystem_family"] = "jvm"
387
+ profile["confidence"] = "Confirmed"
388
+ 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")
389
+ profile["detection_evidence"].append({"file": ev_file, "indicator": f"{profile['language']} build configuration"})
390
+ profile["recommended_rules"].extend(["TG-SEC-*", "TG-AUTH-*", "TG-INPUT-001", "TG-INPUT-002", "TG-DB-004"])
391
+ profile["recommended_references"].append("java-security.md")
392
+
393
+ check_content = ""
394
+ for jf in (pom_files + gradle_files)[:15]:
395
+ if jf.is_file():
396
+ try:
397
+ check_content += jf.read_text(encoding="utf-8", errors="ignore") + "\n"
398
+ except Exception:
399
+ pass
400
+
401
+ if "quarkus" in check_content:
402
+ add_to_list(profile, "frameworks_found", "Quarkus")
403
+ elif "spring-boot" in check_content or "springframework" in check_content:
404
+ add_to_list(profile, "frameworks_found", "Spring Boot")
405
+ elif "ktor" in check_content:
406
+ add_to_list(profile, "frameworks_found", "Ktor")
407
+ elif "micronaut" in check_content:
408
+ add_to_list(profile, "frameworks_found", "Micronaut")
409
+
410
+ if "hibernate" in check_content or "spring-boot-starter-data-jpa" in check_content or "quarkus-hibernate" in check_content:
411
+ add_to_list(profile, "data_layers_found", "Hibernate / JPA")
412
+ elif "jooq" in check_content:
413
+ add_to_list(profile, "data_layers_found", "jOOQ")
414
+
415
+ # -------------------------------------------------------------------------
416
+ # F. C# / .NET Ecosystem
417
+ # -------------------------------------------------------------------------
418
+ if (csproj_files or sln_files):
419
+ add_to_list(profile, "languages_found", "C#")
420
+ profile["ecosystem_family"] = "dotnet"
421
+ profile["confidence"] = "Confirmed"
422
+ ev_file = str(csproj_files[0].relative_to(project_root)) if csproj_files else str(sln_files[0].relative_to(project_root))
423
+ profile["detection_evidence"].append({"file": ev_file, "indicator": ".NET project or solution manifest"})
424
+ profile["recommended_rules"].extend(["TG-SEC-*", "TG-AUTH-*", "TG-INPUT-001", "TG-DB-004"])
425
+ profile["recommended_references"].append("csharp-security.md")
426
+
427
+ csproj_content = ""
428
+ for cf in csproj_files[:10]:
429
+ try:
430
+ csproj_content += cf.read_text(encoding="utf-8", errors="ignore") + "\n"
431
+ except Exception:
432
+ pass
433
+
434
+ if "Microsoft.NET.Sdk.Web" in csproj_content or "Microsoft.AspNetCore" in csproj_content or "Swashbuckle" in csproj_content:
435
+ add_to_list(profile, "frameworks_found", "ASP.NET Core")
436
+ if "Microsoft.EntityFrameworkCore" in csproj_content:
437
+ add_to_list(profile, "data_layers_found", "Entity Framework Core")
438
+ elif "Dapper" in csproj_content:
439
+ add_to_list(profile, "data_layers_found", "Dapper")
440
+
441
+ # -------------------------------------------------------------------------
442
+ # G. PHP Ecosystem
443
+ # -------------------------------------------------------------------------
444
+ composer_json = project_root / "composer.json"
445
+ if composer_json.is_file():
446
+ add_to_list(profile, "languages_found", "PHP")
447
+ profile["ecosystem_family"] = "php"
448
+ profile["confidence"] = "Confirmed"
449
+ profile["detection_evidence"].append({"file": "composer.json", "indicator": "PHP Composer manifest"})
450
+ profile["recommended_rules"].extend(["TG-SEC-*", "TG-INPUT-001", "TG-INPUT-002", "TG-PLATFORM-*"])
451
+ profile["recommended_references"].append("polyglot-security-matrix.md")
452
+
453
+ try:
454
+ with open(composer_json, "r", encoding="utf-8") as f:
455
+ c_data = json.load(f)
456
+ reqs = {**c_data.get("require", {}), **c_data.get("require-dev", {})}
457
+ if "laravel/framework" in reqs:
458
+ add_to_list(profile, "frameworks_found", "Laravel")
459
+ add_to_list(profile, "data_layers_found", "Eloquent ORM")
460
+ elif "symfony/framework-bundle" in reqs or any("symfony/" in k for k in reqs):
461
+ add_to_list(profile, "frameworks_found", "Symfony")
462
+ add_to_list(profile, "data_layers_found", "Doctrine ORM")
463
+ except Exception:
464
+ pass
465
+
466
+ # -------------------------------------------------------------------------
467
+ # H. Ruby Ecosystem
468
+ # -------------------------------------------------------------------------
469
+ gemfile = project_root / "Gemfile"
470
+ if gemfile.is_file():
471
+ add_to_list(profile, "languages_found", "Ruby")
472
+ profile["ecosystem_family"] = "ruby"
473
+ profile["confidence"] = "Confirmed"
474
+ profile["detection_evidence"].append({"file": "Gemfile", "indicator": "Ruby Bundler manifest"})
475
+ profile["recommended_rules"].extend(["TG-SEC-*", "TG-AUTH-*", "TG-INPUT-001", "TG-DB-004"])
476
+ profile["recommended_references"].append("polyglot-security-matrix.md")
477
+
478
+ try:
479
+ g_content = gemfile.read_text(encoding="utf-8", errors="ignore")
480
+ if "rails" in g_content:
481
+ add_to_list(profile, "frameworks_found", "Ruby on Rails")
482
+ add_to_list(profile, "data_layers_found", "ActiveRecord")
483
+ elif "sinatra" in g_content:
484
+ add_to_list(profile, "frameworks_found", "Sinatra")
485
+ except Exception:
486
+ pass
487
+
488
+ # -------------------------------------------------------------------------
489
+ # I. Elixir Ecosystem
490
+ # -------------------------------------------------------------------------
491
+ mix_exs = project_root / "mix.exs"
492
+ if mix_exs.is_file():
493
+ add_to_list(profile, "languages_found", "Elixir")
494
+ profile["ecosystem_family"] = "elixir"
495
+ profile["confidence"] = "Confirmed"
496
+ profile["detection_evidence"].append({"file": "mix.exs", "indicator": "Elixir Mix manifest"})
497
+ profile["recommended_references"].append("polyglot-security-matrix.md")
498
+ try:
499
+ m_content = mix_exs.read_text(encoding="utf-8", errors="ignore")
500
+ if "phoenix" in m_content:
501
+ add_to_list(profile, "frameworks_found", "Phoenix")
502
+ add_to_list(profile, "data_layers_found", "Ecto")
503
+ except Exception:
504
+ pass
505
+
506
+ # -------------------------------------------------------------------------
507
+ # J. Dart / Flutter Ecosystem
508
+ # -------------------------------------------------------------------------
509
+ pubspec = project_root / "pubspec.yaml"
510
+ if pubspec.is_file():
511
+ add_to_list(profile, "languages_found", "Dart")
512
+ profile["ecosystem_family"] = "dart"
513
+ profile["confidence"] = "Confirmed"
514
+ profile["detection_evidence"].append({"file": "pubspec.yaml", "indicator": "Dart pubspec manifest"})
515
+ try:
516
+ p_content = pubspec.read_text(encoding="utf-8", errors="ignore")
517
+ if "flutter:" in p_content:
518
+ add_to_list(profile, "frameworks_found", "Flutter")
519
+ except Exception:
520
+ pass
521
+
522
+ # -------------------------------------------------------------------------
523
+ # K. Fallback: Extension Census (When No Manifest or Custom Structure)
524
+ # -------------------------------------------------------------------------
525
+ census = census_file_extensions(project_root)
526
+ profile["extension_census"] = census
527
+
528
+ if not profile.get("languages_found") and census:
529
+ top_lang = max(census.items(), key=lambda x: x[1])[0]
530
+ add_to_list(profile, "languages_found", top_lang)
531
+ profile["confidence"] = "Inferred from file census"
532
+ profile["detection_evidence"].append({
533
+ "file": "source files",
534
+ "indicator": f"{census[top_lang]} {top_lang} source file(s) found"
535
+ })
536
+ family_map = {
537
+ "Go": "go", "Rust": "rust", "Java": "jvm", "Kotlin": "jvm",
538
+ "C#": "dotnet", "PHP": "php", "Ruby": "ruby", "Python": "python",
539
+ "TypeScript": "javascript", "JavaScript": "javascript",
540
+ "C": "native", "C++": "native", "Swift": "apple", "Dart": "dart"
541
+ }
542
+ profile["ecosystem_family"] = family_map.get(top_lang, "universal")
543
+
544
+ # -------------------------------------------------------------------------
545
+ # L. Monorepo & Multi-Package Workspace Integration
546
+ # -------------------------------------------------------------------------
547
+ try:
548
+ from monorepo_detector import scan_workspace
549
+ ws_res = scan_workspace(str(project_root))
550
+ if ws_res.get("is_monorepo"):
551
+ profile["is_monorepo"] = True
552
+ profile["monorepo_type"] = ws_res.get("monorepo_type", "monorepo")
553
+ profile["sub_stacks"] = ws_res.get("packages", [])
554
+ languages_found = list(set(
555
+ p["language"] for p in ws_res.get("packages", [])
556
+ if p.get("language") and p["language"] != "unknown"
557
+ ))
558
+ profile["sub_languages"] = languages_found
559
+ if len(languages_found) > 1:
560
+ profile["ecosystem_family"] = "polyglot_monorepo"
561
+ profile["recommended_rules"].extend(["TG-SEC-*", "TG-AUTH-*", "TG-PLATFORM-*"])
562
+ except Exception:
563
+ profile["is_monorepo"] = False
564
+ profile["sub_stacks"] = []
565
+
566
+ # Compile final strings
567
+ if profile.get("languages_found"):
568
+ profile["language"] = " / ".join(profile["languages_found"])
569
+ else:
570
+ profile["language"] = "Unknown"
571
+
572
+ if profile.get("frameworks_found"):
573
+ profile["framework"] = " / ".join(profile["frameworks_found"])
574
+ else:
575
+ profile["framework"] = "None"
576
+
577
+ if profile.get("data_layers_found"):
578
+ profile["data_layer"] = " / ".join(profile["data_layers_found"])
579
+ else:
580
+ profile["data_layer"] = "None"
581
+
582
+ return profile
583
+
584
+
585
+ def main():
586
+ parser = argparse.ArgumentParser(description="TorusGuard Universal Polyglot Stack Detector")
587
+ parser.add_argument("path", nargs="?", default=".", help="Target project root directory")
588
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
589
+ args = parser.parse_args()
590
+
591
+ project_root = Path(args.path).resolve()
592
+ profile = detect_stack(project_root)
593
+
594
+ if args.json:
595
+ print(json.dumps(profile, indent=2))
596
+ else:
597
+ evidence = profile["detection_evidence"][0]["file"] if profile["detection_evidence"] else "heuristic"
598
+ print("## Detected Stack (Universal Polyglot)")
599
+ print(f"- Language: {profile['language']}")
600
+ print(f"- Ecosystem Family: {profile.get('ecosystem_family', 'universal')}")
601
+ print(f"- Framework: {profile['framework']}")
602
+ print(f"- Data layer: {profile['data_layer']}")
603
+ print(f"- Dependency files: {', '.join(profile['dependency_files']) if profile['dependency_files'] else 'None'}")
604
+ print(f"- Detection confidence: {profile['confidence']} ({evidence})")
605
+
606
+
607
+ if __name__ == "__main__":
608
+ main()