@vk.amogh/trace 2.1.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/README.md +208 -0
- package/bin/trace.js +112 -0
- package/package.json +46 -0
- package/pyproject.toml +39 -0
- package/src/trace_engine/__init__.py +8 -0
- package/src/trace_engine/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/__pycache__/cli.cpython-311.pyc +0 -0
- package/src/trace_engine/__pycache__/doctor.cpython-311.pyc +0 -0
- package/src/trace_engine/__pycache__/interactive.cpython-311.pyc +0 -0
- package/src/trace_engine/__pycache__/verify.cpython-311.pyc +0 -0
- package/src/trace_engine/ai/__init__.py +7 -0
- package/src/trace_engine/ai/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/ai/__pycache__/base.cpython-311.pyc +0 -0
- package/src/trace_engine/ai/__pycache__/ollama.cpython-311.pyc +0 -0
- package/src/trace_engine/ai/__pycache__/planner.cpython-311.pyc +0 -0
- package/src/trace_engine/ai/base.py +23 -0
- package/src/trace_engine/ai/ollama.py +50 -0
- package/src/trace_engine/ai/planner.py +40 -0
- package/src/trace_engine/apm/__init__.py +18 -0
- package/src/trace_engine/apm/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/apm/__pycache__/builder.cpython-311.pyc +0 -0
- package/src/trace_engine/apm/__pycache__/edges.cpython-311.pyc +0 -0
- package/src/trace_engine/apm/__pycache__/model.cpython-311.pyc +0 -0
- package/src/trace_engine/apm/__pycache__/nodes.cpython-311.pyc +0 -0
- package/src/trace_engine/apm/__pycache__/serialization.cpython-311.pyc +0 -0
- package/src/trace_engine/apm/builder.py +208 -0
- package/src/trace_engine/apm/edges.py +25 -0
- package/src/trace_engine/apm/model.py +107 -0
- package/src/trace_engine/apm/nodes.py +27 -0
- package/src/trace_engine/apm/serialization.py +105 -0
- package/src/trace_engine/benchmark/__init__.py +5 -0
- package/src/trace_engine/benchmark/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/benchmark/__pycache__/owasp.cpython-311.pyc +0 -0
- package/src/trace_engine/benchmark/owasp.py +183 -0
- package/src/trace_engine/cli.py +1184 -0
- package/src/trace_engine/config/__init__.py +35 -0
- package/src/trace_engine/config/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/config/__pycache__/defaults.cpython-311.pyc +0 -0
- package/src/trace_engine/config/__pycache__/loader.cpython-311.pyc +0 -0
- package/src/trace_engine/config/__pycache__/settings.cpython-311.pyc +0 -0
- package/src/trace_engine/config/defaults.py +48 -0
- package/src/trace_engine/config/loader.py +64 -0
- package/src/trace_engine/config/settings.py +72 -0
- package/src/trace_engine/doctor.py +250 -0
- package/src/trace_engine/findings/__init__.py +15 -0
- package/src/trace_engine/findings/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/findings/__pycache__/correlate.cpython-311.pyc +0 -0
- package/src/trace_engine/findings/__pycache__/model.cpython-311.pyc +0 -0
- package/src/trace_engine/findings/__pycache__/recommendations.cpython-311.pyc +0 -0
- package/src/trace_engine/findings/__pycache__/store.cpython-311.pyc +0 -0
- package/src/trace_engine/findings/correlate.py +103 -0
- package/src/trace_engine/findings/model.py +40 -0
- package/src/trace_engine/findings/recommendations.py +35 -0
- package/src/trace_engine/findings/store.py +39 -0
- package/src/trace_engine/framework/__init__.py +58 -0
- package/src/trace_engine/framework/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/base.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/csharp.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/dart.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/django.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/express.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/fastapi.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/flask.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/go.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/nextjs.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/php.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/react_router.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/ruby.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/rust.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/__pycache__/springboot.cpython-311.pyc +0 -0
- package/src/trace_engine/framework/base.py +49 -0
- package/src/trace_engine/framework/csharp.py +111 -0
- package/src/trace_engine/framework/dart.py +152 -0
- package/src/trace_engine/framework/django.py +188 -0
- package/src/trace_engine/framework/express.py +82 -0
- package/src/trace_engine/framework/fastapi.py +135 -0
- package/src/trace_engine/framework/flask.py +108 -0
- package/src/trace_engine/framework/go.py +94 -0
- package/src/trace_engine/framework/nextjs.py +200 -0
- package/src/trace_engine/framework/php.py +98 -0
- package/src/trace_engine/framework/react_router.py +331 -0
- package/src/trace_engine/framework/ruby.py +69 -0
- package/src/trace_engine/framework/rust.py +101 -0
- package/src/trace_engine/framework/springboot.py +145 -0
- package/src/trace_engine/harness/__init__.py +19 -0
- package/src/trace_engine/harness/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/harness/__pycache__/benchmark.cpython-311.pyc +0 -0
- package/src/trace_engine/harness/__pycache__/context.cpython-311.pyc +0 -0
- package/src/trace_engine/harness/__pycache__/engine.cpython-311.pyc +0 -0
- package/src/trace_engine/harness/__pycache__/patcher.cpython-311.pyc +0 -0
- package/src/trace_engine/harness/__pycache__/remediators.cpython-311.pyc +0 -0
- package/src/trace_engine/harness/benchmark.py +55 -0
- package/src/trace_engine/harness/context.py +49 -0
- package/src/trace_engine/harness/engine.py +187 -0
- package/src/trace_engine/harness/patcher.py +143 -0
- package/src/trace_engine/harness/remediators.py +307 -0
- package/src/trace_engine/ingest/__init__.py +15 -0
- package/src/trace_engine/ingest/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/ingest/__pycache__/files.cpython-311.pyc +0 -0
- package/src/trace_engine/ingest/__pycache__/hashing.cpython-311.pyc +0 -0
- package/src/trace_engine/ingest/__pycache__/ignore.cpython-311.pyc +0 -0
- package/src/trace_engine/ingest/__pycache__/repository.cpython-311.pyc +0 -0
- package/src/trace_engine/ingest/files.py +60 -0
- package/src/trace_engine/ingest/hashing.py +21 -0
- package/src/trace_engine/ingest/ignore.py +108 -0
- package/src/trace_engine/ingest/repository.py +50 -0
- package/src/trace_engine/intelligence/__init__.py +15 -0
- package/src/trace_engine/intelligence/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/__pycache__/evaluation.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/__pycache__/orchestrator.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/__pycache__/tracebench.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/evaluation.py +828 -0
- package/src/trace_engine/intelligence/laya/__init__.py +19 -0
- package/src/trace_engine/intelligence/laya/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/laya/__pycache__/prompts.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/laya/__pycache__/router.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/laya/__pycache__/schemas.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/laya/__pycache__/telemetry.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/laya/__pycache__/thresholds.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/laya/prompts.py +67 -0
- package/src/trace_engine/intelligence/laya/router.py +352 -0
- package/src/trace_engine/intelligence/laya/schemas.py +56 -0
- package/src/trace_engine/intelligence/laya/telemetry.py +48 -0
- package/src/trace_engine/intelligence/laya/thresholds.py +12 -0
- package/src/trace_engine/intelligence/orchestrator.py +130 -0
- package/src/trace_engine/intelligence/securebert/__init__.py +6 -0
- package/src/trace_engine/intelligence/securebert/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/securebert/__pycache__/cache.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/securebert/__pycache__/classifier.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/securebert/cache.py +37 -0
- package/src/trace_engine/intelligence/securebert/classifier.py +240 -0
- package/src/trace_engine/intelligence/tracebench.py +61 -0
- package/src/trace_engine/intelligence/training/__init__.py +21 -0
- package/src/trace_engine/intelligence/training/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/training/__pycache__/dataset.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/training/__pycache__/dataset_importers.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/training/__pycache__/laya_trainer.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/training/__pycache__/lora_system2.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/training/__pycache__/morefixes_pipeline.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/training/__pycache__/slicer.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/training/__pycache__/train_all.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/training/__pycache__/trainer.cpython-311.pyc +0 -0
- package/src/trace_engine/intelligence/training/dataset.py +320 -0
- package/src/trace_engine/intelligence/training/dataset_importers.py +349 -0
- package/src/trace_engine/intelligence/training/laya_trainer.py +678 -0
- package/src/trace_engine/intelligence/training/lora_system2.py +162 -0
- package/src/trace_engine/intelligence/training/morefixes_pipeline.py +463 -0
- package/src/trace_engine/intelligence/training/slicer.py +129 -0
- package/src/trace_engine/intelligence/training/train_all.py +1009 -0
- package/src/trace_engine/intelligence/training/trainer.py +321 -0
- package/src/trace_engine/interactive.py +623 -0
- package/src/trace_engine/mcp/__init__.py +6 -0
- package/src/trace_engine/mcp/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/mcp/__pycache__/config.cpython-311.pyc +0 -0
- package/src/trace_engine/mcp/__pycache__/server.cpython-311.pyc +0 -0
- package/src/trace_engine/mcp/config.py +42 -0
- package/src/trace_engine/mcp/server.py +398 -0
- package/src/trace_engine/output/__init__.py +28 -0
- package/src/trace_engine/output/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/output/__pycache__/html.cpython-311.pyc +0 -0
- package/src/trace_engine/output/__pycache__/markdown.cpython-311.pyc +0 -0
- package/src/trace_engine/output/__pycache__/sarif.cpython-311.pyc +0 -0
- package/src/trace_engine/output/__pycache__/terminal.cpython-311.pyc +0 -0
- package/src/trace_engine/output/html.py +142 -0
- package/src/trace_engine/output/markdown.py +65 -0
- package/src/trace_engine/output/sarif.py +180 -0
- package/src/trace_engine/output/terminal.py +537 -0
- package/src/trace_engine/parsing/__init__.py +26 -0
- package/src/trace_engine/parsing/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/parsing/__pycache__/calls.cpython-311.pyc +0 -0
- package/src/trace_engine/parsing/__pycache__/language.cpython-311.pyc +0 -0
- package/src/trace_engine/parsing/__pycache__/locations.cpython-311.pyc +0 -0
- package/src/trace_engine/parsing/__pycache__/parser.cpython-311.pyc +0 -0
- package/src/trace_engine/parsing/__pycache__/symbols.cpython-311.pyc +0 -0
- package/src/trace_engine/parsing/calls.py +14 -0
- package/src/trace_engine/parsing/language.py +24 -0
- package/src/trace_engine/parsing/locations.py +17 -0
- package/src/trace_engine/parsing/parser.py +465 -0
- package/src/trace_engine/parsing/symbols.py +45 -0
- package/src/trace_engine/plugin/__init__.py +157 -0
- package/src/trace_engine/plugin/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/plugin/__pycache__/evaluator.cpython-311.pyc +0 -0
- package/src/trace_engine/plugin/__pycache__/swebench_adapter.cpython-311.pyc +0 -0
- package/src/trace_engine/plugin/__pycache__/task.cpython-311.pyc +0 -0
- package/src/trace_engine/plugin/bundle/hooks.json +24 -0
- package/src/trace_engine/plugin/bundle/mcp_config.json +11 -0
- package/src/trace_engine/plugin/bundle/plugin.json +20 -0
- package/src/trace_engine/plugin/bundle/rules/security_remediation.md +40 -0
- package/src/trace_engine/plugin/bundle/skills/trace-security-harness/SKILL.md +118 -0
- package/src/trace_engine/plugin/evaluator.py +105 -0
- package/src/trace_engine/plugin/swebench_adapter.py +96 -0
- package/src/trace_engine/plugin/task.py +48 -0
- package/src/trace_engine/policy/__init__.py +5 -0
- package/src/trace_engine/policy/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/policy/__pycache__/scope.cpython-311.pyc +0 -0
- package/src/trace_engine/policy/scope.py +80 -0
- package/src/trace_engine/runtime/__init__.py +7 -0
- package/src/trace_engine/runtime/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/runtime/__pycache__/client.cpython-311.pyc +0 -0
- package/src/trace_engine/runtime/__pycache__/observations.cpython-311.pyc +0 -0
- package/src/trace_engine/runtime/__pycache__/target.cpython-311.pyc +0 -0
- package/src/trace_engine/runtime/client.py +81 -0
- package/src/trace_engine/runtime/observations.py +18 -0
- package/src/trace_engine/runtime/target.py +23 -0
- package/src/trace_engine/security/__init__.py +16 -0
- package/src/trace_engine/security/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/security/__pycache__/fusion.cpython-311.pyc +0 -0
- package/src/trace_engine/security/__pycache__/hypotheses.cpython-311.pyc +0 -0
- package/src/trace_engine/security/__pycache__/signals.cpython-311.pyc +0 -0
- package/src/trace_engine/security/__pycache__/timing.cpython-311.pyc +0 -0
- package/src/trace_engine/security/fusion.py +126 -0
- package/src/trace_engine/security/hypotheses.py +251 -0
- package/src/trace_engine/security/signals.py +28 -0
- package/src/trace_engine/security/timing.py +132 -0
- package/src/trace_engine/testpacks/__init__.py +18 -0
- package/src/trace_engine/testpacks/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/authentication.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/base.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/bfla.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/bola.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/cors.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/deserialization.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/injection.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/mass_assignment.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/path_traversal.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/registry.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/ssrf.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/__pycache__/ssti.cpython-311.pyc +0 -0
- package/src/trace_engine/testpacks/authentication.py +60 -0
- package/src/trace_engine/testpacks/base.py +63 -0
- package/src/trace_engine/testpacks/bfla.py +70 -0
- package/src/trace_engine/testpacks/bola.py +94 -0
- package/src/trace_engine/testpacks/cors.py +85 -0
- package/src/trace_engine/testpacks/deserialization.py +86 -0
- package/src/trace_engine/testpacks/injection.py +179 -0
- package/src/trace_engine/testpacks/mass_assignment.py +70 -0
- package/src/trace_engine/testpacks/path_traversal.py +117 -0
- package/src/trace_engine/testpacks/registry.py +44 -0
- package/src/trace_engine/testpacks/ssrf.py +85 -0
- package/src/trace_engine/testpacks/ssti.py +96 -0
- package/src/trace_engine/tools/__init__.py +6 -0
- package/src/trace_engine/tools/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/trace_engine/tools/__pycache__/adapters.cpython-311.pyc +0 -0
- package/src/trace_engine/tools/__pycache__/base.cpython-311.pyc +0 -0
- package/src/trace_engine/tools/__pycache__/registry.cpython-311.pyc +0 -0
- package/src/trace_engine/tools/adapters.py +111 -0
- package/src/trace_engine/tools/base.py +68 -0
- package/src/trace_engine/tools/registry.py +36 -0
- package/src/trace_engine/verify.py +156 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Finding store for saving and loading correlated security results."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
from trace_engine.findings.model import Finding
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FindingStore:
|
|
10
|
+
"""Manages persistence of correlated findings in the .trace directory."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, trace_dir: Path):
|
|
13
|
+
self.trace_dir = trace_dir
|
|
14
|
+
self.store_file = trace_dir / "findings.json"
|
|
15
|
+
|
|
16
|
+
def save_findings(self, findings: List[Finding]) -> None:
|
|
17
|
+
"""Save a list of findings to JSON."""
|
|
18
|
+
self.trace_dir.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
data = [f.model_dump() for f in findings]
|
|
20
|
+
with open(self.store_file, "w", encoding="utf-8") as f:
|
|
21
|
+
json.dump(data, f, indent=2)
|
|
22
|
+
|
|
23
|
+
def load_findings(self) -> List[Finding]:
|
|
24
|
+
"""Load findings from JSON file."""
|
|
25
|
+
if not self.store_file.exists():
|
|
26
|
+
return []
|
|
27
|
+
try:
|
|
28
|
+
with open(self.store_file, "r", encoding="utf-8") as f:
|
|
29
|
+
data = json.load(f)
|
|
30
|
+
return [Finding.model_validate(item) for item in data]
|
|
31
|
+
except Exception:
|
|
32
|
+
return []
|
|
33
|
+
|
|
34
|
+
def get_finding_by_id(self, finding_id: str) -> Optional[Finding]:
|
|
35
|
+
"""Retrieve finding by ID."""
|
|
36
|
+
for f in self.load_findings():
|
|
37
|
+
if f.id.lower() == finding_id.lower():
|
|
38
|
+
return f
|
|
39
|
+
return None
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Framework adapters for web frameworks and API route extraction."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
from trace_engine.framework.base import FrameworkAdapter, Endpoint, EndpointParameter
|
|
5
|
+
from trace_engine.framework.fastapi import FastAPIFrameworkAdapter
|
|
6
|
+
from trace_engine.framework.flask import FlaskFrameworkAdapter
|
|
7
|
+
from trace_engine.framework.django import DjangoFrameworkAdapter
|
|
8
|
+
from trace_engine.framework.express import ExpressFrameworkAdapter
|
|
9
|
+
from trace_engine.framework.react_router import ReactRouterFrameworkAdapter
|
|
10
|
+
from trace_engine.framework.nextjs import NextJSFrameworkAdapter
|
|
11
|
+
from trace_engine.framework.dart import DartFrameworkAdapter
|
|
12
|
+
from trace_engine.framework.springboot import SpringBootFrameworkAdapter
|
|
13
|
+
from trace_engine.framework.go import GoFrameworkAdapter
|
|
14
|
+
from trace_engine.framework.php import PHPFrameworkAdapter
|
|
15
|
+
from trace_engine.framework.ruby import RubyFrameworkAdapter
|
|
16
|
+
from trace_engine.framework.rust import RustFrameworkAdapter
|
|
17
|
+
from trace_engine.framework.csharp import CSharpFrameworkAdapter
|
|
18
|
+
|
|
19
|
+
_ADAPTERS: List[FrameworkAdapter] = [
|
|
20
|
+
FastAPIFrameworkAdapter(),
|
|
21
|
+
FlaskFrameworkAdapter(),
|
|
22
|
+
DjangoFrameworkAdapter(),
|
|
23
|
+
ExpressFrameworkAdapter(),
|
|
24
|
+
ReactRouterFrameworkAdapter(),
|
|
25
|
+
NextJSFrameworkAdapter(),
|
|
26
|
+
DartFrameworkAdapter(),
|
|
27
|
+
SpringBootFrameworkAdapter(),
|
|
28
|
+
GoFrameworkAdapter(),
|
|
29
|
+
PHPFrameworkAdapter(),
|
|
30
|
+
RubyFrameworkAdapter(),
|
|
31
|
+
RustFrameworkAdapter(),
|
|
32
|
+
CSharpFrameworkAdapter(),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_adapters() -> List[FrameworkAdapter]:
|
|
37
|
+
return _ADAPTERS
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"FrameworkAdapter",
|
|
42
|
+
"Endpoint",
|
|
43
|
+
"EndpointParameter",
|
|
44
|
+
"FastAPIFrameworkAdapter",
|
|
45
|
+
"FlaskFrameworkAdapter",
|
|
46
|
+
"DjangoFrameworkAdapter",
|
|
47
|
+
"ExpressFrameworkAdapter",
|
|
48
|
+
"ReactRouterFrameworkAdapter",
|
|
49
|
+
"NextJSFrameworkAdapter",
|
|
50
|
+
"DartFrameworkAdapter",
|
|
51
|
+
"SpringBootFrameworkAdapter",
|
|
52
|
+
"GoFrameworkAdapter",
|
|
53
|
+
"PHPFrameworkAdapter",
|
|
54
|
+
"RubyFrameworkAdapter",
|
|
55
|
+
"RustFrameworkAdapter",
|
|
56
|
+
"CSharpFrameworkAdapter",
|
|
57
|
+
"get_adapters",
|
|
58
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Normalized API endpoint models and framework adapter base class."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import List, Optional, Dict, Any
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
from trace_engine.parsing.locations import SourceLocation
|
|
7
|
+
from trace_engine.parsing.parser import ParsedFile
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EndpointParameter(BaseModel):
|
|
11
|
+
name: str
|
|
12
|
+
location: str = "query" # "path", "query", "body", "header"
|
|
13
|
+
param_type: Optional[str] = "string"
|
|
14
|
+
required: bool = True
|
|
15
|
+
user_controlled: bool = True
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Endpoint(BaseModel):
|
|
19
|
+
"""Normalized API endpoint representation according to TRACE specification."""
|
|
20
|
+
id: str
|
|
21
|
+
method: str
|
|
22
|
+
path: str
|
|
23
|
+
handler_name: str
|
|
24
|
+
auth_required: bool = False
|
|
25
|
+
roles: List[str] = Field(default_factory=list)
|
|
26
|
+
parameters: List[EndpointParameter] = Field(default_factory=list)
|
|
27
|
+
database_access: bool = False
|
|
28
|
+
object_identifier: bool = False
|
|
29
|
+
state_changing: bool = False
|
|
30
|
+
external_network: bool = False
|
|
31
|
+
sensitive_data: bool = False
|
|
32
|
+
source: SourceLocation
|
|
33
|
+
|
|
34
|
+
def display_name(self) -> str:
|
|
35
|
+
return f"{self.method.upper()} {self.path}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FrameworkAdapter(ABC):
|
|
39
|
+
"""Abstract adapter for extracting endpoints from parsed ASTs."""
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def can_handle(self, parsed_file: ParsedFile) -> bool:
|
|
43
|
+
"""Determines if this adapter applies to the given file."""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def extract_endpoints(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
|
|
48
|
+
"""Extracts normalized endpoints from the file."""
|
|
49
|
+
pass
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""C# and ASP.NET Core framework adapter for controller and minimal API route extraction."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
from trace_engine.parsing.parser import ParsedFile
|
|
6
|
+
from trace_engine.parsing.locations import SourceLocation
|
|
7
|
+
from trace_engine.framework.base import FrameworkAdapter, Endpoint, EndpointParameter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CSharpFrameworkAdapter(FrameworkAdapter):
|
|
11
|
+
"""Detects and extracts C# ASP.NET Core endpoints, controller actions, and minimal APIs."""
|
|
12
|
+
|
|
13
|
+
def can_handle(self, parsed_file: ParsedFile) -> bool:
|
|
14
|
+
if parsed_file.language != "csharp":
|
|
15
|
+
norm = parsed_file.file_path.replace("\\", "/").lower()
|
|
16
|
+
return norm.endswith(".cs")
|
|
17
|
+
return True
|
|
18
|
+
|
|
19
|
+
def extract_endpoints(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
|
|
20
|
+
endpoints: List[Endpoint] = []
|
|
21
|
+
norm_file = parsed_file.file_path.replace("\\", "/")
|
|
22
|
+
content_lower = content.lower()
|
|
23
|
+
|
|
24
|
+
has_db_sink = any(
|
|
25
|
+
term in content_lower
|
|
26
|
+
for term in ("fromsqlraw", "executesqlraw", "dbcontext", "_context.", "sqlconnection", "dapper", "queryasync")
|
|
27
|
+
)
|
|
28
|
+
has_ext_sink = any(
|
|
29
|
+
term in content_lower
|
|
30
|
+
for term in ("httpclient", "getasync", "postasync", "restsharp")
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# 1. Controller HTTP action attributes: [HttpGet("api/v1/users/{id}")] or [HttpPost("orders")]
|
|
34
|
+
attr_pattern = re.compile(
|
|
35
|
+
r"""\[Http(Get|Post|Put|Delete|Patch)\s*(?:\(\s*["']([^"']*)["']\s*\))?\]\s*(?:\[[^\]]+\]\s*)*public\s+(?:async\s+)?(?:Task<[a-zA-Z0-9_<>]+>|[a-zA-Z0-9_<>]+)\s+([a-zA-Z0-9_]+)""",
|
|
36
|
+
re.MULTILINE
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Extract base route from [Route("api/[controller]")] if present
|
|
40
|
+
base_route_match = re.search(r"""\[Route\s*\(\s*["']([^"']+)["']\s*\)\]""", content)
|
|
41
|
+
base_route = base_route_match.group(1).rstrip("/") if base_route_match else ""
|
|
42
|
+
|
|
43
|
+
for match in attr_pattern.finditer(content):
|
|
44
|
+
method = match.group(1).upper()
|
|
45
|
+
action_route = match.group(2) or ""
|
|
46
|
+
handler = match.group(3)
|
|
47
|
+
line_no = content[:match.start()].count("\n") + 1
|
|
48
|
+
|
|
49
|
+
if action_route.startswith("/"):
|
|
50
|
+
raw_path = action_route
|
|
51
|
+
elif base_route:
|
|
52
|
+
raw_path = f"/{base_route}/{action_route}".rstrip("/")
|
|
53
|
+
else:
|
|
54
|
+
raw_path = f"/{action_route}" if action_route else f"/{handler}"
|
|
55
|
+
|
|
56
|
+
raw_path = "/" + raw_path.lstrip("/")
|
|
57
|
+
path_params = re.findall(r'\{([a-zA-Z0-9_]+)\}', raw_path)
|
|
58
|
+
|
|
59
|
+
ep_id = f"ep_cs_{norm_file}_{line_no}_{method}".replace("/", "_").replace(".", "_")
|
|
60
|
+
endpoints.append(
|
|
61
|
+
Endpoint(
|
|
62
|
+
id=ep_id,
|
|
63
|
+
method=method,
|
|
64
|
+
path=raw_path,
|
|
65
|
+
handler_name=handler,
|
|
66
|
+
auth_required="[authorize]" in content_lower or "authorize" in content_lower,
|
|
67
|
+
roles=["admin"] if "admin" in raw_path.lower() else [],
|
|
68
|
+
parameters=[EndpointParameter(name=p, location="path", required=True) for p in path_params],
|
|
69
|
+
database_access=has_db_sink,
|
|
70
|
+
object_identifier=bool(path_params),
|
|
71
|
+
state_changing=method in ("POST", "PUT", "DELETE", "PATCH"),
|
|
72
|
+
external_network=has_ext_sink,
|
|
73
|
+
sensitive_data="admin" in raw_path.lower() or "user" in raw_path.lower(),
|
|
74
|
+
source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# 2. Minimal APIs: app.MapGet("/api/v1/users/{id}", ...)
|
|
79
|
+
map_pattern = re.compile(
|
|
80
|
+
r"""app\.Map(Get|Post|Put|Delete)\s*\(\s*["']([^"']+)["']\s*,\s*([a-zA-Z0-9_\.]+|async|\()""",
|
|
81
|
+
re.IGNORECASE,
|
|
82
|
+
)
|
|
83
|
+
for match in map_pattern.finditer(content):
|
|
84
|
+
method = match.group(1).upper()
|
|
85
|
+
raw_path = match.group(2).strip()
|
|
86
|
+
handler = "minimal_api_handler"
|
|
87
|
+
line_no = content[:match.start()].count("\n") + 1
|
|
88
|
+
|
|
89
|
+
norm_path = raw_path if raw_path.startswith("/") else f"/{raw_path}"
|
|
90
|
+
path_params = re.findall(r'\{([a-zA-Z0-9_]+)\}', norm_path)
|
|
91
|
+
|
|
92
|
+
ep_id = f"ep_cs_map_{norm_file}_{line_no}_{method}".replace("/", "_").replace(".", "_")
|
|
93
|
+
endpoints.append(
|
|
94
|
+
Endpoint(
|
|
95
|
+
id=ep_id,
|
|
96
|
+
method=method,
|
|
97
|
+
path=norm_path,
|
|
98
|
+
handler_name=handler,
|
|
99
|
+
auth_required="requireauthorization" in content_lower,
|
|
100
|
+
roles=["admin"] if "admin" in norm_path.lower() else [],
|
|
101
|
+
parameters=[EndpointParameter(name=p, location="path", required=True) for p in path_params],
|
|
102
|
+
database_access=has_db_sink,
|
|
103
|
+
object_identifier=bool(path_params),
|
|
104
|
+
state_changing=method in ("POST", "PUT", "DELETE", "PATCH"),
|
|
105
|
+
external_network=has_ext_sink,
|
|
106
|
+
sensitive_data="admin" in norm_path.lower() or "user" in norm_path.lower(),
|
|
107
|
+
source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return endpoints
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Dart framework adapter for Shelf, Dart Frog, Angel, and Flutter API routes."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List, Dict, Optional
|
|
5
|
+
from trace_engine.parsing.parser import ParsedFile
|
|
6
|
+
from trace_engine.framework.base import FrameworkAdapter, Endpoint, EndpointParameter
|
|
7
|
+
from trace_engine.parsing.locations import SourceLocation
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DartFrameworkAdapter(FrameworkAdapter):
|
|
11
|
+
"""Detects and extracts Dart API endpoints, Shelf routes, and Dart Frog handlers."""
|
|
12
|
+
|
|
13
|
+
def can_handle(self, parsed_file: ParsedFile) -> bool:
|
|
14
|
+
if parsed_file.language != "dart":
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
norm_path = parsed_file.file_path.replace("\\", "/").lower()
|
|
18
|
+
|
|
19
|
+
# If in routes, controllers, or api directory
|
|
20
|
+
if any(f"/{d}/" in norm_path or norm_path.startswith(f"{d}/") for d in ("routes", "controllers", "api", "endpoints")):
|
|
21
|
+
return True
|
|
22
|
+
|
|
23
|
+
# Check imports
|
|
24
|
+
for imp in parsed_file.imports:
|
|
25
|
+
mod_low = imp.module.lower()
|
|
26
|
+
if any(term in mod_low for term in ("shelf", "dart_frog", "angel", "conduit")):
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
def extract_endpoints(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
|
|
32
|
+
endpoints: List[Endpoint] = []
|
|
33
|
+
norm_file_path = parsed_file.file_path.replace("\\", "/")
|
|
34
|
+
content_lower = content.lower()
|
|
35
|
+
|
|
36
|
+
# Database sinks
|
|
37
|
+
has_db_sink = any(
|
|
38
|
+
term in content_lower
|
|
39
|
+
for term in (
|
|
40
|
+
"postgres", "mysql", "sqlite", "drift", "sembast", "mongo",
|
|
41
|
+
"supabase", "db.query", "db.execute", "findbyid", "findone",
|
|
42
|
+
"repository", "table("
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# External network sinks
|
|
47
|
+
has_ext_sink = any(
|
|
48
|
+
term in content_lower
|
|
49
|
+
for term in (
|
|
50
|
+
"http.get", "http.post", "http.put", "http.delete",
|
|
51
|
+
"client.get", "client.post", "dio.get", "dio.post"
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Authentication detection
|
|
56
|
+
auth_signals = (
|
|
57
|
+
"requireauth", "verifytoken", "jwt", "bearer", "authmiddleware",
|
|
58
|
+
"isauthenticated", "currentuser", "admin", "hasrole"
|
|
59
|
+
)
|
|
60
|
+
auth_required = any(sig in content_lower for sig in auth_signals) or "admin" in norm_file_path.lower()
|
|
61
|
+
roles = ["admin"] if "admin" in content_lower or "admin" in norm_file_path.lower() else []
|
|
62
|
+
|
|
63
|
+
# 1. Shelf Router: router.get('/path', _handler) or router.get('/path', (req) => ...)
|
|
64
|
+
shelf_pattern = re.compile(
|
|
65
|
+
r"""(?:router|app)\.(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]\s*,\s*([a-zA-Z0-9_$]+|\([^\)]*\))""",
|
|
66
|
+
re.IGNORECASE
|
|
67
|
+
)
|
|
68
|
+
for match in shelf_pattern.finditer(content):
|
|
69
|
+
method = match.group(1).upper()
|
|
70
|
+
raw_path = match.group(2)
|
|
71
|
+
raw_handler = match.group(3)
|
|
72
|
+
handler_name = raw_handler if not raw_handler.startswith("(") else "closure_handler"
|
|
73
|
+
line_no = content[:match.start()].count("\n") + 1
|
|
74
|
+
|
|
75
|
+
# Convert Shelf <param> and :param to {param}
|
|
76
|
+
norm_path = re.sub(r'<([a-zA-Z0-9_]+)>', r'{\1}', raw_path)
|
|
77
|
+
norm_path = re.sub(r':([a-zA-Z0-9_]+)', r'{\1}', norm_path)
|
|
78
|
+
norm_path = "/" + norm_path.lstrip("/")
|
|
79
|
+
|
|
80
|
+
path_params = re.findall(r'\{([a-zA-Z0-9_]+)\}', norm_path)
|
|
81
|
+
ep_id = f"ep_dart_{norm_file_path}_{line_no}_{method}".replace("/", "_").replace(".", "_")
|
|
82
|
+
|
|
83
|
+
sensitive = any(
|
|
84
|
+
term in norm_path.lower() or term in content_lower
|
|
85
|
+
for term in ("admin", "user", "auth", "billing", "payment", "order", "token", "secret", "webhook")
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
endpoints.append(
|
|
89
|
+
Endpoint(
|
|
90
|
+
id=ep_id,
|
|
91
|
+
method=method,
|
|
92
|
+
path=norm_path,
|
|
93
|
+
handler_name=handler_name,
|
|
94
|
+
auth_required=auth_required or ("admin" in norm_path.lower()),
|
|
95
|
+
roles=roles if "admin" not in norm_path.lower() else ["admin"],
|
|
96
|
+
parameters=[EndpointParameter(name=p, location="path", required=True) for p in path_params],
|
|
97
|
+
database_access=has_db_sink,
|
|
98
|
+
object_identifier=bool(path_params),
|
|
99
|
+
state_changing=method in ("POST", "PUT", "DELETE", "PATCH"),
|
|
100
|
+
external_network=has_ext_sink,
|
|
101
|
+
sensitive_data=sensitive,
|
|
102
|
+
source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# 2. Dart Frog: routes/api/v1/projects/[id].dart
|
|
107
|
+
if not endpoints and "routes/" in norm_file_path:
|
|
108
|
+
clean = norm_file_path
|
|
109
|
+
idx = clean.find("routes/")
|
|
110
|
+
clean = clean[idx + 7:]
|
|
111
|
+
clean = re.sub(r'\.dart$', '', clean)
|
|
112
|
+
clean = re.sub(r'\[([a-zA-Z0-9_]+)\]', r'{\1}', clean)
|
|
113
|
+
clean = re.sub(r'(?:^|/)index$', '', clean)
|
|
114
|
+
norm_path = "/" + clean.lstrip("/")
|
|
115
|
+
if not norm_path:
|
|
116
|
+
norm_path = "/"
|
|
117
|
+
|
|
118
|
+
path_params = re.findall(r'\{([a-zA-Z0-9_]+)\}', norm_path)
|
|
119
|
+
sensitive = any(
|
|
120
|
+
term in norm_path.lower() or term in content_lower
|
|
121
|
+
for term in ("admin", "user", "auth", "billing", "payment", "token", "webhook")
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Look for onRequest or method checks
|
|
125
|
+
methods = []
|
|
126
|
+
for m in re.finditer(r'''HttpMethod\.(get|post|put|delete)''', content, re.IGNORECASE):
|
|
127
|
+
methods.append(m.group(1).upper())
|
|
128
|
+
|
|
129
|
+
if not methods:
|
|
130
|
+
methods = ["GET", "POST"]
|
|
131
|
+
|
|
132
|
+
for meth in set(methods):
|
|
133
|
+
ep_id = f"ep_dart_frog_{norm_file_path}_{meth}".replace("/", "_").replace(".", "_")
|
|
134
|
+
endpoints.append(
|
|
135
|
+
Endpoint(
|
|
136
|
+
id=ep_id,
|
|
137
|
+
method=meth,
|
|
138
|
+
path=norm_path,
|
|
139
|
+
handler_name="onRequest",
|
|
140
|
+
auth_required=auth_required or ("admin" in norm_path.lower()),
|
|
141
|
+
roles=roles if "admin" not in norm_path.lower() else ["admin"],
|
|
142
|
+
parameters=[EndpointParameter(name=p, location="path", required=True) for p in path_params],
|
|
143
|
+
database_access=has_db_sink,
|
|
144
|
+
object_identifier=bool(path_params),
|
|
145
|
+
state_changing=meth in ("POST", "PUT", "DELETE", "PATCH"),
|
|
146
|
+
external_network=has_ext_sink,
|
|
147
|
+
sensitive_data=sensitive,
|
|
148
|
+
source=SourceLocation(file=parsed_file.file_path, line_start=1, line_end=1),
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return endpoints
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Django and Django REST Framework adapter for endpoint and route discovery."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List, Set, Optional
|
|
5
|
+
from trace_engine.parsing.parser import ParsedFile
|
|
6
|
+
from trace_engine.parsing.locations import SourceLocation
|
|
7
|
+
from trace_engine.framework.base import FrameworkAdapter, Endpoint, EndpointParameter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DjangoFrameworkAdapter(FrameworkAdapter):
|
|
11
|
+
"""Detects and extracts Django and Django REST Framework (DRF) endpoints, URL routing, and security decorators."""
|
|
12
|
+
|
|
13
|
+
def can_handle(self, parsed_file: ParsedFile) -> bool:
|
|
14
|
+
if parsed_file.language != "python":
|
|
15
|
+
return False
|
|
16
|
+
# Check imports for Django or DRF
|
|
17
|
+
for imp in parsed_file.imports:
|
|
18
|
+
mod = imp.module.lower()
|
|
19
|
+
if "django" in mod or "rest_framework" in mod:
|
|
20
|
+
return True
|
|
21
|
+
# Check file naming conventions or patterns
|
|
22
|
+
if "urls.py" in parsed_file.file_path.lower() or "views.py" in parsed_file.file_path.lower():
|
|
23
|
+
return True
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
def extract_endpoints(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
|
|
27
|
+
endpoints: List[Endpoint] = []
|
|
28
|
+
|
|
29
|
+
# 1. Parse urls.py patterns: path('api/v1/orders/<int:order_id>/', views.get_order)
|
|
30
|
+
endpoints.extend(self._extract_from_urlpatterns(parsed_file, content))
|
|
31
|
+
|
|
32
|
+
# 2. Parse views.py: Class-Based Views (APIView, View, ViewSet) and function views (@api_view)
|
|
33
|
+
endpoints.extend(self._extract_from_views(parsed_file, content))
|
|
34
|
+
|
|
35
|
+
return endpoints
|
|
36
|
+
|
|
37
|
+
def _extract_from_urlpatterns(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
|
|
38
|
+
endpoints: List[Endpoint] = []
|
|
39
|
+
if "urlpatterns" not in content and "path(" not in content and "re_path(" not in content:
|
|
40
|
+
return endpoints
|
|
41
|
+
|
|
42
|
+
# Pattern: path('orders/<int:order_id>/', views.order_detail, name='order-detail')
|
|
43
|
+
# Pattern: re_path(r'^api/v1/export/?$', ExportView.as_view())
|
|
44
|
+
path_pattern = re.compile(
|
|
45
|
+
r"""(?:path|re_path)\s*\(\s*r?["']([^"']*)["']\s*,\s*([a-zA-Z0-9_\.]+)(?:\.as_view\s*\(\s*\))?""",
|
|
46
|
+
re.MULTILINE,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
for match in path_pattern.finditer(content):
|
|
50
|
+
raw_path = match.group(1).strip()
|
|
51
|
+
handler_name = match.group(2).strip()
|
|
52
|
+
|
|
53
|
+
# Normalize Django path converters: <int:id>, <str:slug>, <uuid:pk> -> {id}, {slug}, {pk}
|
|
54
|
+
path_params = re.findall(r"<(?:[a-zA-Z0-9_]+:)?([a-zA-Z0-9_]+)>", raw_path)
|
|
55
|
+
clean_path = re.sub(r"<(?:[a-zA-Z0-9_]+:)?([a-zA-Z0-9_]+)>", r"{\1}", raw_path)
|
|
56
|
+
# Remove regex anchor symbols
|
|
57
|
+
clean_path = re.sub(r"[\^\$\?]+", "", clean_path).rstrip("/")
|
|
58
|
+
norm_path = clean_path if clean_path.startswith("/") else f"/{clean_path}"
|
|
59
|
+
|
|
60
|
+
# Infer methods: standard Django views typically handle GET or POST
|
|
61
|
+
methods = ["GET"]
|
|
62
|
+
if any(term in handler_name.lower() for term in ("create", "delete", "update", "post", "mutation", "reset", "patch")):
|
|
63
|
+
methods = ["POST"]
|
|
64
|
+
|
|
65
|
+
auth_required = any(
|
|
66
|
+
term in content.lower()
|
|
67
|
+
for term in ("login_required", "isauthenticated", "permission_classes", "tokenauthentication")
|
|
68
|
+
)
|
|
69
|
+
roles = ["admin"] if "admin" in handler_name.lower() or "admin" in norm_path.lower() else []
|
|
70
|
+
|
|
71
|
+
line_no = content[: match.start()].count("\n") + 1
|
|
72
|
+
src_loc = SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no, column_start=0, column_end=0)
|
|
73
|
+
|
|
74
|
+
for m in methods:
|
|
75
|
+
ep_id = f"ep_django_{parsed_file.file_path}_{handler_name}_{m}".replace("/", "_").replace(".", "_")
|
|
76
|
+
endpoints.append(
|
|
77
|
+
Endpoint(
|
|
78
|
+
id=ep_id,
|
|
79
|
+
method=m,
|
|
80
|
+
path=norm_path,
|
|
81
|
+
handler_name=handler_name,
|
|
82
|
+
auth_required=auth_required,
|
|
83
|
+
roles=roles,
|
|
84
|
+
parameters=[
|
|
85
|
+
EndpointParameter(name=p, location="path", required=True)
|
|
86
|
+
for p in path_params
|
|
87
|
+
],
|
|
88
|
+
database_access=True, # Django views almost universally access ORM models
|
|
89
|
+
object_identifier=bool(path_params),
|
|
90
|
+
state_changing=m in ("POST", "PUT", "DELETE", "PATCH"),
|
|
91
|
+
external_network=False,
|
|
92
|
+
sensitive_data="admin" in norm_path.lower() or "auth" in norm_path.lower() or "user" in norm_path.lower(),
|
|
93
|
+
source=src_loc,
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return endpoints
|
|
98
|
+
|
|
99
|
+
def _extract_from_views(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
|
|
100
|
+
endpoints: List[Endpoint] = []
|
|
101
|
+
content_lines = content.splitlines()
|
|
102
|
+
|
|
103
|
+
# 1. DRF @api_view(['GET', 'POST'])
|
|
104
|
+
for fn in parsed_file.functions:
|
|
105
|
+
fn_methods = []
|
|
106
|
+
for dec in fn.decorators:
|
|
107
|
+
dec_text = dec.raw_text.strip()
|
|
108
|
+
api_match = re.search(r"""@?api_view\s*\(\s*\[([^\]]+)\]""", dec_text, re.IGNORECASE)
|
|
109
|
+
if api_match:
|
|
110
|
+
fn_methods = [
|
|
111
|
+
m.strip().strip("'\"").upper()
|
|
112
|
+
for m in api_match.group(1).split(",")
|
|
113
|
+
if m.strip()
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
if fn_methods:
|
|
117
|
+
fn_body = fn.body_text or ""
|
|
118
|
+
auth_required = any(
|
|
119
|
+
term in fn_body.lower() or term in "".join(d.raw_text.lower() for d in fn.decorators)
|
|
120
|
+
for term in ("isauthenticated", "isadminuser", "login_required", "permission_classes")
|
|
121
|
+
)
|
|
122
|
+
roles = ["admin"] if "admin" in fn.name.lower() else []
|
|
123
|
+
for m in fn_methods:
|
|
124
|
+
ep_id = f"ep_drf_{parsed_file.file_path}_{fn.name}_{m}".replace("/", "_").replace(".", "_")
|
|
125
|
+
endpoints.append(
|
|
126
|
+
Endpoint(
|
|
127
|
+
id=ep_id,
|
|
128
|
+
method=m,
|
|
129
|
+
path=f"/api/{fn.name.replace('_', '-')}",
|
|
130
|
+
handler_name=fn.qualified_name,
|
|
131
|
+
auth_required=auth_required,
|
|
132
|
+
roles=roles,
|
|
133
|
+
parameters=[],
|
|
134
|
+
database_access="objects." in fn_body or "filter(" in fn_body or ".raw(" in fn_body,
|
|
135
|
+
object_identifier=False,
|
|
136
|
+
state_changing=m in ("POST", "PUT", "DELETE", "PATCH"),
|
|
137
|
+
external_network="requests." in fn_body or "httpx." in fn_body or "urllib" in fn_body,
|
|
138
|
+
sensitive_data="admin" in fn.name.lower() or "secret" in fn.name.lower(),
|
|
139
|
+
source=fn.location,
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# 2. DRF Class-Based Views (APIView, ModelViewSet, GenericAPIView)
|
|
144
|
+
for cls in parsed_file.classes:
|
|
145
|
+
is_view_class = any(
|
|
146
|
+
base in cls.base_classes
|
|
147
|
+
for base in ("APIView", "View", "GenericAPIView", "ModelViewSet", "ViewSet", "ReadOnlyModelViewSet")
|
|
148
|
+
) or cls.name.endswith("View") or cls.name.endswith("ViewSet")
|
|
149
|
+
|
|
150
|
+
if is_view_class:
|
|
151
|
+
start_l = max(0, cls.location.line_start - 1)
|
|
152
|
+
end_l = min(len(content_lines), cls.location.line_end)
|
|
153
|
+
class_body = "\n".join(content_lines[start_l:end_l])
|
|
154
|
+
|
|
155
|
+
auth_required = any(
|
|
156
|
+
term in class_body.lower()
|
|
157
|
+
for term in ("isauthenticated", "isadminuser", "permission_classes", "isauthenticatedorreadonly")
|
|
158
|
+
)
|
|
159
|
+
roles = ["admin"] if "admin" in cls.name.lower() or "isadminuser" in class_body.lower() else []
|
|
160
|
+
|
|
161
|
+
# Look for handler methods: get, post, put, delete, patch
|
|
162
|
+
for fn in cls.methods:
|
|
163
|
+
method_name = fn.name.lower()
|
|
164
|
+
if method_name in ("get", "post", "put", "delete", "patch"):
|
|
165
|
+
http_method = method_name.upper()
|
|
166
|
+
ep_id = f"ep_cbv_{parsed_file.file_path}_{cls.name}_{http_method}".replace("/", "_").replace(".", "_")
|
|
167
|
+
base_path = f"/api/{cls.name.replace('View', '').replace('ViewSet', '').lower()}"
|
|
168
|
+
fn_body = fn.body_text or ""
|
|
169
|
+
|
|
170
|
+
endpoints.append(
|
|
171
|
+
Endpoint(
|
|
172
|
+
id=ep_id,
|
|
173
|
+
method=http_method,
|
|
174
|
+
path=base_path,
|
|
175
|
+
handler_name=f"{cls.name}.{fn.name}",
|
|
176
|
+
auth_required=auth_required,
|
|
177
|
+
roles=roles,
|
|
178
|
+
parameters=[],
|
|
179
|
+
database_access="objects." in fn_body or "filter(" in fn_body or ".raw(" in fn_body,
|
|
180
|
+
object_identifier=http_method in ("PUT", "DELETE", "PATCH"),
|
|
181
|
+
state_changing=http_method in ("POST", "PUT", "DELETE", "PATCH"),
|
|
182
|
+
external_network="requests." in fn_body or "httpx." in fn_body,
|
|
183
|
+
sensitive_data="admin" in cls.name.lower() or "user" in cls.name.lower(),
|
|
184
|
+
source=fn.location,
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return endpoints
|