trace-sec 0.0.0-stage → 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 +210 -2
- package/bin/trace.js +112 -0
- package/package.json +44 -4
- 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,98 @@
|
|
|
1
|
+
"""PHP and Laravel framework adapter for endpoint and route discovery."""
|
|
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 PHPFrameworkAdapter(FrameworkAdapter):
|
|
11
|
+
"""Detects and extracts PHP endpoints from Laravel routes and native PHP API scripts."""
|
|
12
|
+
|
|
13
|
+
def can_handle(self, parsed_file: ParsedFile) -> bool:
|
|
14
|
+
if parsed_file.language != "php":
|
|
15
|
+
norm = parsed_file.file_path.replace("\\", "/").lower()
|
|
16
|
+
return norm.endswith(".php")
|
|
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
|
+
# Database and system sinks
|
|
25
|
+
has_db_sink = any(
|
|
26
|
+
term in content_lower
|
|
27
|
+
for term in (
|
|
28
|
+
"mysqli_query", "pdo::query", "db::raw", "db::select", "db::table",
|
|
29
|
+
"eloquent", "->get()", "->find(", "->first()"
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
has_ext_sink = any(
|
|
33
|
+
term in content_lower
|
|
34
|
+
for term in ("curl_exec", "file_get_contents", "http::get", "guzzle")
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# 1. Laravel Route definitions: Route::get('/users/{id}', [UserController::class, 'show'])
|
|
38
|
+
laravel_pattern = re.compile(
|
|
39
|
+
r"""Route::(get|post|put|delete|patch|options)\s*\(\s*['"]([^'"]+)['"]\s*,\s*([^)]+)\)""",
|
|
40
|
+
re.IGNORECASE,
|
|
41
|
+
)
|
|
42
|
+
for match in laravel_pattern.finditer(content):
|
|
43
|
+
method = match.group(1).upper()
|
|
44
|
+
raw_path = match.group(2).strip()
|
|
45
|
+
handler = match.group(3).strip()
|
|
46
|
+
line_no = content[:match.start()].count("\n") + 1
|
|
47
|
+
|
|
48
|
+
norm_path = raw_path if raw_path.startswith("/") else f"/{raw_path}"
|
|
49
|
+
path_params = re.findall(r"\{([a-zA-Z0-9_]+)\}", norm_path)
|
|
50
|
+
|
|
51
|
+
ep_id = f"ep_php_{norm_file}_{line_no}_{method}".replace("/", "_").replace(".", "_")
|
|
52
|
+
endpoints.append(
|
|
53
|
+
Endpoint(
|
|
54
|
+
id=ep_id,
|
|
55
|
+
method=method,
|
|
56
|
+
path=norm_path,
|
|
57
|
+
handler_name=handler,
|
|
58
|
+
auth_required="auth" in content_lower or "middleware('auth" in content_lower,
|
|
59
|
+
roles=["admin"] if "admin" in norm_path.lower() or "admin" in content_lower else [],
|
|
60
|
+
parameters=[EndpointParameter(name=p, location="path", required=True) for p in path_params],
|
|
61
|
+
database_access=has_db_sink,
|
|
62
|
+
object_identifier=bool(path_params),
|
|
63
|
+
state_changing=method in ("POST", "PUT", "DELETE", "PATCH"),
|
|
64
|
+
external_network=has_ext_sink,
|
|
65
|
+
sensitive_data="admin" in norm_path.lower() or "user" in norm_path.lower(),
|
|
66
|
+
source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# 2. Native PHP file scripts: e.g. api/users.php or get_order.php
|
|
71
|
+
if not endpoints and any(seg in norm_file.lower() for seg in ("/api/", "endpoints/", "public/")):
|
|
72
|
+
script_path = "/" + norm_file.lstrip("/").replace(".php", "")
|
|
73
|
+
params = []
|
|
74
|
+
for get_match in re.findall(r"""\$_GET\s*\[\s*['"]([a-zA-Z0-9_]+)['"]\s*\]""", content):
|
|
75
|
+
params.append(EndpointParameter(name=get_match, location="query", required=False))
|
|
76
|
+
for post_match in re.findall(r"""\$_POST\s*\[\s*['"]([a-zA-Z0-9_]+)['"]\s*\]""", content):
|
|
77
|
+
params.append(EndpointParameter(name=post_match, location="body", required=False))
|
|
78
|
+
|
|
79
|
+
method = "POST" if "$_POST" in content else "GET"
|
|
80
|
+
endpoints.append(
|
|
81
|
+
Endpoint(
|
|
82
|
+
id=f"ep_php_script_{norm_file}".replace("/", "_").replace(".", "_"),
|
|
83
|
+
method=method,
|
|
84
|
+
path=script_path,
|
|
85
|
+
handler_name=parsed_file.file_path,
|
|
86
|
+
auth_required="session_start" in content_lower or "auth" in content_lower,
|
|
87
|
+
roles=["admin"] if "admin" in script_path.lower() else [],
|
|
88
|
+
parameters=params,
|
|
89
|
+
database_access=has_db_sink,
|
|
90
|
+
object_identifier=any(p.name in ("id", "user_id", "order_id") for p in params),
|
|
91
|
+
state_changing=method == "POST",
|
|
92
|
+
external_network=has_ext_sink,
|
|
93
|
+
sensitive_data="admin" in script_path.lower() or "user" in script_path.lower(),
|
|
94
|
+
source=SourceLocation(file=parsed_file.file_path, line_start=1, line_end=max(1, content.count("\n"))),
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return endpoints
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""React Router v7 and Remix framework adapter for endpoint discovery."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Dict, Optional, Tuple
|
|
6
|
+
from trace_engine.parsing.parser import ParsedFile
|
|
7
|
+
from trace_engine.framework.base import FrameworkAdapter, Endpoint, EndpointParameter
|
|
8
|
+
from trace_engine.parsing.locations import SourceLocation
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ReactRouterFrameworkAdapter(FrameworkAdapter):
|
|
12
|
+
"""Detects and extracts React Router v7 and Remix endpoints, loaders, and actions."""
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
# Maps (normalized_file_key) -> url_path
|
|
16
|
+
self._route_cache: Dict[str, str] = {}
|
|
17
|
+
self._parsed_route_files: set = set()
|
|
18
|
+
|
|
19
|
+
def _normalize_key(self, path_str: str) -> str:
|
|
20
|
+
clean = path_str.replace("\\", "/").strip("/")
|
|
21
|
+
if clean.startswith("app/"):
|
|
22
|
+
clean = clean[4:]
|
|
23
|
+
return clean
|
|
24
|
+
|
|
25
|
+
def _discover_and_parse_routes_file(self, parsed_file: ParsedFile) -> None:
|
|
26
|
+
"""Looks up routes.ts / routes.js up the directory tree and caches mappings."""
|
|
27
|
+
file_p = Path(parsed_file.absolute_path) if parsed_file.absolute_path else Path(parsed_file.file_path)
|
|
28
|
+
candidates = []
|
|
29
|
+
|
|
30
|
+
if file_p.is_absolute():
|
|
31
|
+
search_dirs = [file_p.parent] + list(file_p.parents)
|
|
32
|
+
else:
|
|
33
|
+
search_dirs = [Path(".").resolve()]
|
|
34
|
+
|
|
35
|
+
for d in search_dirs:
|
|
36
|
+
for cand_name in ("app/routes.ts", "routes.ts", "app/routes.js", "routes.js", "app/routes.tsx", "routes.tsx"):
|
|
37
|
+
cand = d / cand_name
|
|
38
|
+
if cand.exists() and cand.is_file():
|
|
39
|
+
candidates.append(cand)
|
|
40
|
+
break
|
|
41
|
+
if candidates:
|
|
42
|
+
break
|
|
43
|
+
|
|
44
|
+
for cand in candidates:
|
|
45
|
+
abs_cand_str = str(cand.resolve())
|
|
46
|
+
if abs_cand_str in self._parsed_route_files:
|
|
47
|
+
continue
|
|
48
|
+
self._parsed_route_files.add(abs_cand_str)
|
|
49
|
+
try:
|
|
50
|
+
content = cand.read_text(encoding="utf-8", errors="replace")
|
|
51
|
+
self._parse_routes_config(content)
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
def _parse_routes_config(self, content: str) -> None:
|
|
56
|
+
"""Parses route definitions from React Router v7 routes.ts."""
|
|
57
|
+
# 1. index("routes/home.tsx")
|
|
58
|
+
for m in re.finditer(r'''index\s*\(\s*["']([^"']+)["']''', content):
|
|
59
|
+
file_target = self._normalize_key(m.group(1))
|
|
60
|
+
self._route_cache[file_target] = "/"
|
|
61
|
+
|
|
62
|
+
# 2. route("path", "file")
|
|
63
|
+
for m in re.finditer(r'''route\s*\(\s*["']([^"']+)["']\s*,\s*["']([^"']+)["']''', content):
|
|
64
|
+
path_part = m.group(1)
|
|
65
|
+
file_target = self._normalize_key(m.group(2))
|
|
66
|
+
norm_url = "/" + path_part.lstrip("/")
|
|
67
|
+
self._route_cache[file_target] = norm_url
|
|
68
|
+
|
|
69
|
+
# 3. layout("layout_file", [ ... ])
|
|
70
|
+
# Also map layout file if needed
|
|
71
|
+
for m in re.finditer(r'''layout\s*\(\s*["']([^"']+)["']''', content):
|
|
72
|
+
file_target = self._normalize_key(m.group(1))
|
|
73
|
+
if file_target not in self._route_cache:
|
|
74
|
+
self._route_cache[file_target] = "/admin" if "admin" in file_target else "/"
|
|
75
|
+
|
|
76
|
+
def _infer_path_from_filename(self, file_path: str) -> str:
|
|
77
|
+
"""Derives route path from Remix / React Router convention-based file names."""
|
|
78
|
+
clean = self._normalize_key(file_path)
|
|
79
|
+
# Strip routes/ prefix
|
|
80
|
+
if clean.startswith("routes/"):
|
|
81
|
+
clean = clean[7:]
|
|
82
|
+
|
|
83
|
+
# Strip extension
|
|
84
|
+
clean = re.sub(r'\.(tsx|ts|jsx|js)$', '', clean)
|
|
85
|
+
|
|
86
|
+
# Handle escaped characters like sitemap[.xml] -> sitemap.xml
|
|
87
|
+
clean = clean.replace("[.", ".").replace(".]", ".")
|
|
88
|
+
clean = clean.replace("[", "").replace("]", "")
|
|
89
|
+
|
|
90
|
+
# Dots in flat routes represent slashes: api.check-username -> api/check-username
|
|
91
|
+
clean = clean.replace(".", "/")
|
|
92
|
+
|
|
93
|
+
# Remix param conventions: $id -> {id}, :id -> {id}
|
|
94
|
+
clean = re.sub(r'\$([a-zA-Z0-9_]+)', r'{\1}', clean)
|
|
95
|
+
clean = re.sub(r':([a-zA-Z0-9_]+)', r'{\1}', clean)
|
|
96
|
+
|
|
97
|
+
# Index routes
|
|
98
|
+
clean = re.sub(r'(?:^|/)_?index$', '', clean)
|
|
99
|
+
|
|
100
|
+
# Remove pathless layout segments (e.g. _auth, _public)
|
|
101
|
+
parts = [p for p in clean.split("/") if not (p.startswith("_") and not p == "")]
|
|
102
|
+
clean = "/".join(parts)
|
|
103
|
+
|
|
104
|
+
norm_path = "/" + clean.lstrip("/")
|
|
105
|
+
return norm_path or "/"
|
|
106
|
+
|
|
107
|
+
def can_handle(self, parsed_file: ParsedFile) -> bool:
|
|
108
|
+
if parsed_file.language not in ("javascript", "typescript"):
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
norm_path = parsed_file.file_path.replace("\\", "/")
|
|
112
|
+
basename = norm_path.split("/")[-1]
|
|
113
|
+
|
|
114
|
+
# Ignore configuration files and hidden/dot files
|
|
115
|
+
if "config" in basename.lower() or basename.startswith("."):
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
# If it's the routes configuration file itself
|
|
119
|
+
if re.search(r'(?:^|/)(?:app/)?routes\.(ts|js|tsx|jsx)$', norm_path):
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
# Lazy check and load routes.ts if present
|
|
123
|
+
self._discover_and_parse_routes_file(parsed_file)
|
|
124
|
+
|
|
125
|
+
# Check if file is in cached route table
|
|
126
|
+
key = self._normalize_key(norm_path)
|
|
127
|
+
if key in self._route_cache:
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
# Check if file is within a routes directory (Remix / React Router convention)
|
|
131
|
+
if "/routes/" in norm_path or norm_path.startswith("routes/"):
|
|
132
|
+
# Exclude non-route subdirectories like styles, assets, or types
|
|
133
|
+
if any(f"/{d}/" in norm_path for d in ("styles", "assets", "types", "+types")):
|
|
134
|
+
return False
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
def extract_endpoints(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
|
|
140
|
+
endpoints: List[Endpoint] = []
|
|
141
|
+
norm_file_path = parsed_file.file_path.replace("\\", "/")
|
|
142
|
+
|
|
143
|
+
# Ensure routes.ts is parsed
|
|
144
|
+
self._discover_and_parse_routes_file(parsed_file)
|
|
145
|
+
|
|
146
|
+
# If this file is routes.ts itself, the routes are mapped to individual handler files
|
|
147
|
+
if re.search(r'(?:^|/)(?:app/)?routes\.(ts|js|tsx|jsx)$', norm_file_path):
|
|
148
|
+
# Parse route config to make sure route cache is up to date
|
|
149
|
+
self._parse_routes_config(content)
|
|
150
|
+
return []
|
|
151
|
+
|
|
152
|
+
# Determine route URL path
|
|
153
|
+
norm_key = self._normalize_key(norm_file_path)
|
|
154
|
+
raw_path = self._route_cache.get(norm_key)
|
|
155
|
+
if not raw_path:
|
|
156
|
+
# Try alternate key without leading dir
|
|
157
|
+
basename = norm_file_path.split("/")[-1]
|
|
158
|
+
raw_path = self._route_cache.get(basename)
|
|
159
|
+
|
|
160
|
+
if not raw_path:
|
|
161
|
+
raw_path = self._infer_path_from_filename(norm_file_path)
|
|
162
|
+
|
|
163
|
+
# Normalize dynamic path segments :id -> {id}
|
|
164
|
+
url_path = re.sub(r':([a-zA-Z0-9_]+)', r'{\1}', raw_path)
|
|
165
|
+
url_path = re.sub(r'\$([a-zA-Z0-9_]+)', r'{\1}', url_path)
|
|
166
|
+
|
|
167
|
+
path_params = re.findall(r'\{([a-zA-Z0-9_]+)\}', url_path)
|
|
168
|
+
|
|
169
|
+
# Analyze handler bodies and symbols
|
|
170
|
+
content_lower = content.lower()
|
|
171
|
+
|
|
172
|
+
# Database sinks
|
|
173
|
+
has_db_sink = any(
|
|
174
|
+
term in content_lower
|
|
175
|
+
for term in (
|
|
176
|
+
"supabase", "prisma", "sequelize", "mongoose", "knex",
|
|
177
|
+
"pool.query", "client.query", "db.query", "db.select",
|
|
178
|
+
".from(", ".rpc(", ".server"
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# External network sinks
|
|
183
|
+
has_ext_sink = any(
|
|
184
|
+
term in content_lower
|
|
185
|
+
for term in (
|
|
186
|
+
"axios", "fetch(", "nodemailer", "got(", "needle",
|
|
187
|
+
"http.request", "https.request", "transporter.sendmail",
|
|
188
|
+
"octokit", "stripe"
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Authentication & Role detection
|
|
193
|
+
auth_signals = (
|
|
194
|
+
"requireauth", "requireadmin", "requireadmingate", "getsessionuser",
|
|
195
|
+
"requiregithub", "supabase.auth", "verifytoken", "jwt.verify",
|
|
196
|
+
"isauthenticated", "authsession", "getserversession", "getauth"
|
|
197
|
+
)
|
|
198
|
+
auth_required = any(sig in content_lower for sig in auth_signals) or "admin" in url_path.lower()
|
|
199
|
+
roles = ["admin"] if "admin" in content_lower or "admin" in url_path.lower() else []
|
|
200
|
+
|
|
201
|
+
sensitive_data = any(
|
|
202
|
+
term in url_path.lower() or term in content_lower
|
|
203
|
+
for term in (
|
|
204
|
+
"admin", "user", "auth", "login", "register", "profile",
|
|
205
|
+
"password", "token", "secret", "webhook", "session", "payment"
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Parameters
|
|
210
|
+
parameters: List[EndpointParameter] = [
|
|
211
|
+
EndpointParameter(name=p, location="path", required=True, user_controlled=True)
|
|
212
|
+
for p in path_params
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
# Extract Query Parameters: searchParams.get("...")
|
|
216
|
+
for m in re.finditer(r'''(?:searchParams|nextUrl\.searchParams)\.get\s*\(\s*["']([^"']+)["']''', content):
|
|
217
|
+
p_name = m.group(1)
|
|
218
|
+
if not any(p.name == p_name for p in parameters):
|
|
219
|
+
parameters.append(
|
|
220
|
+
EndpointParameter(name=p_name, location="query", required=False, user_controlled=True)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Extract Body Parameters: formData.get("...")
|
|
224
|
+
for m in re.finditer(r'''formData\.get\s*\(\s*["']([^"']+)["']''', content):
|
|
225
|
+
p_name = m.group(1)
|
|
226
|
+
if not any(p.name == p_name for p in parameters):
|
|
227
|
+
parameters.append(
|
|
228
|
+
EndpointParameter(name=p_name, location="body", required=True, user_controlled=True)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Check for loader (GET)
|
|
232
|
+
loader_match = re.search(
|
|
233
|
+
r'''(?:export\s+)?(?:async\s+)?function\s+loader\b|export\s+(?:const|let|var)\s+loader\b''',
|
|
234
|
+
content
|
|
235
|
+
)
|
|
236
|
+
if loader_match:
|
|
237
|
+
line_no = content[:loader_match.start()].count("\n") + 1
|
|
238
|
+
ep_id = f"ep_rr_get_{norm_file_path}_{line_no}".replace("/", "_").replace(".", "_")
|
|
239
|
+
endpoints.append(
|
|
240
|
+
Endpoint(
|
|
241
|
+
id=ep_id,
|
|
242
|
+
method="GET",
|
|
243
|
+
path=url_path,
|
|
244
|
+
handler_name="loader",
|
|
245
|
+
auth_required=auth_required,
|
|
246
|
+
roles=roles,
|
|
247
|
+
parameters=parameters,
|
|
248
|
+
database_access=has_db_sink,
|
|
249
|
+
object_identifier=bool(path_params),
|
|
250
|
+
state_changing=False,
|
|
251
|
+
external_network=has_ext_sink,
|
|
252
|
+
sensitive_data=sensitive_data,
|
|
253
|
+
source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Check for action (POST / mutation)
|
|
258
|
+
action_match = re.search(
|
|
259
|
+
r'''(?:export\s+)?(?:async\s+)?function\s+action\b|export\s+(?:const|let|var)\s+action\b''',
|
|
260
|
+
content
|
|
261
|
+
)
|
|
262
|
+
if action_match:
|
|
263
|
+
line_no = content[:action_match.start()].count("\n") + 1
|
|
264
|
+
ep_id = f"ep_rr_post_{norm_file_path}_{line_no}".replace("/", "_").replace(".", "_")
|
|
265
|
+
endpoints.append(
|
|
266
|
+
Endpoint(
|
|
267
|
+
id=ep_id,
|
|
268
|
+
method="POST",
|
|
269
|
+
path=url_path,
|
|
270
|
+
handler_name="action",
|
|
271
|
+
auth_required=auth_required,
|
|
272
|
+
roles=roles,
|
|
273
|
+
parameters=parameters,
|
|
274
|
+
database_access=has_db_sink,
|
|
275
|
+
object_identifier=bool(path_params),
|
|
276
|
+
state_changing=True,
|
|
277
|
+
external_network=has_ext_sink,
|
|
278
|
+
sensitive_data=sensitive_data,
|
|
279
|
+
source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Check for explicit HTTP verbs: GET, POST, PUT, DELETE, PATCH
|
|
284
|
+
for v_match in re.finditer(r'''export\s+(?:async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b''', content):
|
|
285
|
+
verb = v_match.group(1).upper()
|
|
286
|
+
line_no = content[:v_match.start()].count("\n") + 1
|
|
287
|
+
ep_id = f"ep_rr_{verb.lower()}_{norm_file_path}_{line_no}".replace("/", "_").replace(".", "_")
|
|
288
|
+
endpoints.append(
|
|
289
|
+
Endpoint(
|
|
290
|
+
id=ep_id,
|
|
291
|
+
method=verb,
|
|
292
|
+
path=url_path,
|
|
293
|
+
handler_name=verb,
|
|
294
|
+
auth_required=auth_required,
|
|
295
|
+
roles=roles,
|
|
296
|
+
parameters=parameters,
|
|
297
|
+
database_access=has_db_sink,
|
|
298
|
+
object_identifier=bool(path_params),
|
|
299
|
+
state_changing=verb in ("POST", "PUT", "DELETE", "PATCH"),
|
|
300
|
+
external_network=has_ext_sink,
|
|
301
|
+
sensitive_data=sensitive_data,
|
|
302
|
+
source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# If file is a route file (e.g. in routes/ or mapped) but defines neither loader nor action nor verbs,
|
|
307
|
+
# it is a page component serving GET
|
|
308
|
+
if not endpoints:
|
|
309
|
+
default_match = re.search(r'''export\s+default\s+(?:function|const|class)?\s*([a-zA-Z0-9_$]+)?''', content)
|
|
310
|
+
handler_name = default_match.group(1) if (default_match and default_match.group(1)) else "default"
|
|
311
|
+
line_no = content[:default_match.start()].count("\n") + 1 if default_match else 1
|
|
312
|
+
ep_id = f"ep_rr_page_{norm_file_path}_{line_no}".replace("/", "_").replace(".", "_")
|
|
313
|
+
endpoints.append(
|
|
314
|
+
Endpoint(
|
|
315
|
+
id=ep_id,
|
|
316
|
+
method="GET",
|
|
317
|
+
path=url_path,
|
|
318
|
+
handler_name=handler_name,
|
|
319
|
+
auth_required=auth_required,
|
|
320
|
+
roles=roles,
|
|
321
|
+
parameters=parameters,
|
|
322
|
+
database_access=has_db_sink,
|
|
323
|
+
object_identifier=bool(path_params),
|
|
324
|
+
state_changing=False,
|
|
325
|
+
external_network=has_ext_sink,
|
|
326
|
+
sensitive_data=sensitive_data,
|
|
327
|
+
source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
return endpoints
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Ruby on Rails framework adapter for endpoint and route discovery."""
|
|
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 RubyFrameworkAdapter(FrameworkAdapter):
|
|
11
|
+
"""Detects and extracts Ruby on Rails routes, controllers, and sinks."""
|
|
12
|
+
|
|
13
|
+
def can_handle(self, parsed_file: ParsedFile) -> bool:
|
|
14
|
+
if parsed_file.language != "ruby":
|
|
15
|
+
norm = parsed_file.file_path.replace("\\", "/").lower()
|
|
16
|
+
return norm.endswith(".rb")
|
|
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 ("active_record", "connection.execute", "find_by_sql", "where(", "find(")
|
|
27
|
+
)
|
|
28
|
+
has_ext_sink = any(
|
|
29
|
+
term in content_lower
|
|
30
|
+
for term in ("net::http", "httparty", "faraday", "restclient")
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Rails routes: get '/users/:id', to: 'users#show'
|
|
34
|
+
# post '/orders', to: 'orders#create'
|
|
35
|
+
route_pattern = re.compile(
|
|
36
|
+
r"""(?:get|post|put|delete|patch)\s+['"]([^'"]+)['"](?:\s*,\s*to:\s*['"]([^'"]+)['"])?""",
|
|
37
|
+
re.IGNORECASE,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
for match in route_pattern.finditer(content):
|
|
41
|
+
raw_path = match.group(1).strip()
|
|
42
|
+
handler = match.group(2) or "rails_controller_action"
|
|
43
|
+
line_no = content[:match.start()].count("\n") + 1
|
|
44
|
+
|
|
45
|
+
method = match.group(0).split()[0].upper()
|
|
46
|
+
norm_path = re.sub(r':([a-zA-Z0-9_]+)', r'{\1}', raw_path)
|
|
47
|
+
norm_path = "/" + norm_path.lstrip("/")
|
|
48
|
+
path_params = re.findall(r'\{([a-zA-Z0-9_]+)\}', norm_path)
|
|
49
|
+
|
|
50
|
+
ep_id = f"ep_ruby_{norm_file}_{line_no}_{method}".replace("/", "_").replace(".", "_")
|
|
51
|
+
endpoints.append(
|
|
52
|
+
Endpoint(
|
|
53
|
+
id=ep_id,
|
|
54
|
+
method=method,
|
|
55
|
+
path=norm_path,
|
|
56
|
+
handler_name=handler,
|
|
57
|
+
auth_required="authenticate" in content_lower or "before_action :authenticate" in content_lower,
|
|
58
|
+
roles=["admin"] if "admin" in norm_path.lower() else [],
|
|
59
|
+
parameters=[EndpointParameter(name=p, location="path", required=True) for p in path_params],
|
|
60
|
+
database_access=has_db_sink,
|
|
61
|
+
object_identifier=bool(path_params),
|
|
62
|
+
state_changing=method in ("POST", "PUT", "DELETE", "PATCH"),
|
|
63
|
+
external_network=has_ext_sink,
|
|
64
|
+
sensitive_data="admin" in norm_path.lower() or "user" in norm_path.lower(),
|
|
65
|
+
source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return endpoints
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Rust framework adapter for Actix-web, Axum, and Rocket 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 RustFrameworkAdapter(FrameworkAdapter):
|
|
11
|
+
"""Detects and extracts Rust routes from Actix-web, Axum, and Rocket."""
|
|
12
|
+
|
|
13
|
+
def can_handle(self, parsed_file: ParsedFile) -> bool:
|
|
14
|
+
if parsed_file.language != "rust":
|
|
15
|
+
norm = parsed_file.file_path.replace("\\", "/").lower()
|
|
16
|
+
return norm.endswith(".rs")
|
|
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 ("sqlx::", "diesel::", "sea_orm", "query_as!", "query!", "execute(")
|
|
27
|
+
)
|
|
28
|
+
has_ext_sink = any(
|
|
29
|
+
term in content_lower
|
|
30
|
+
for term in ("reqwest::", "hyper::", "surf::")
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# 1. Actix-web / Rocket macros: #[get("/path/{id}")] or #[post("/path")]
|
|
34
|
+
macro_pattern = re.compile(
|
|
35
|
+
r"""#\[(get|post|put|delete|patch)\s*\(\s*["']([^"']+)["']\s*\)\]\s*(?:pub\s+)?(?:async\s+)?fn\s+([a-zA-Z0-9_]+)""",
|
|
36
|
+
re.IGNORECASE,
|
|
37
|
+
)
|
|
38
|
+
for match in macro_pattern.finditer(content):
|
|
39
|
+
method = match.group(1).upper()
|
|
40
|
+
raw_path = match.group(2).strip()
|
|
41
|
+
handler = match.group(3).strip()
|
|
42
|
+
line_no = content[:match.start()].count("\n") + 1
|
|
43
|
+
|
|
44
|
+
norm_path = raw_path if raw_path.startswith("/") else f"/{raw_path}"
|
|
45
|
+
norm_path = re.sub(r'<([a-zA-Z0-9_]+)>', r'{\1}', norm_path)
|
|
46
|
+
path_params = re.findall(r'\{([a-zA-Z0-9_]+)\}', norm_path)
|
|
47
|
+
|
|
48
|
+
ep_id = f"ep_rust_{norm_file}_{line_no}_{method}".replace("/", "_").replace(".", "_")
|
|
49
|
+
endpoints.append(
|
|
50
|
+
Endpoint(
|
|
51
|
+
id=ep_id,
|
|
52
|
+
method=method,
|
|
53
|
+
path=norm_path,
|
|
54
|
+
handler_name=handler,
|
|
55
|
+
auth_required="auth" in content_lower or "claims" in content_lower,
|
|
56
|
+
roles=["admin"] if "admin" in norm_path.lower() else [],
|
|
57
|
+
parameters=[EndpointParameter(name=p, location="path", required=True) for p in path_params],
|
|
58
|
+
database_access=has_db_sink,
|
|
59
|
+
object_identifier=bool(path_params),
|
|
60
|
+
state_changing=method in ("POST", "PUT", "DELETE", "PATCH"),
|
|
61
|
+
external_network=has_ext_sink,
|
|
62
|
+
sensitive_data="admin" in norm_path.lower() or "user" in norm_path.lower(),
|
|
63
|
+
source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# 2. Axum routes: .route("/path", get(handler)) or post(handler)
|
|
68
|
+
axum_pattern = re.compile(
|
|
69
|
+
r"""\.route\s*\(\s*["']([^"']+)["']\s*,\s*(get|post|put|delete|patch)\s*\(\s*([a-zA-Z0-9_:]+)\s*\)\s*\)""",
|
|
70
|
+
re.IGNORECASE,
|
|
71
|
+
)
|
|
72
|
+
for match in axum_pattern.finditer(content):
|
|
73
|
+
raw_path = match.group(1).strip()
|
|
74
|
+
method = match.group(2).upper()
|
|
75
|
+
handler = match.group(3).strip()
|
|
76
|
+
line_no = content[:match.start()].count("\n") + 1
|
|
77
|
+
|
|
78
|
+
norm_path = re.sub(r':([a-zA-Z0-9_]+)', r'{\1}', raw_path)
|
|
79
|
+
norm_path = "/" + norm_path.lstrip("/")
|
|
80
|
+
path_params = re.findall(r'\{([a-zA-Z0-9_]+)\}', norm_path)
|
|
81
|
+
|
|
82
|
+
ep_id = f"ep_rust_axum_{norm_file}_{line_no}_{method}".replace("/", "_").replace(".", "_")
|
|
83
|
+
endpoints.append(
|
|
84
|
+
Endpoint(
|
|
85
|
+
id=ep_id,
|
|
86
|
+
method=method,
|
|
87
|
+
path=norm_path,
|
|
88
|
+
handler_name=handler,
|
|
89
|
+
auth_required="auth" in content_lower or "claims" in content_lower,
|
|
90
|
+
roles=["admin"] if "admin" in norm_path.lower() else [],
|
|
91
|
+
parameters=[EndpointParameter(name=p, location="path", required=True) for p in path_params],
|
|
92
|
+
database_access=has_db_sink,
|
|
93
|
+
object_identifier=bool(path_params),
|
|
94
|
+
state_changing=method in ("POST", "PUT", "DELETE", "PATCH"),
|
|
95
|
+
external_network=has_ext_sink,
|
|
96
|
+
sensitive_data="admin" in norm_path.lower() or "user" in norm_path.lower(),
|
|
97
|
+
source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return endpoints
|