@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,82 @@
|
|
|
1
|
+
"""Express.js framework adapter for endpoint discovery."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List
|
|
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 ExpressFrameworkAdapter(FrameworkAdapter):
|
|
11
|
+
"""Detects and extracts Express.js routes and middleware."""
|
|
12
|
+
|
|
13
|
+
def can_handle(self, parsed_file: ParsedFile) -> bool:
|
|
14
|
+
if parsed_file.language not in ("javascript", "typescript"):
|
|
15
|
+
return False
|
|
16
|
+
return any("express" in imp.module.lower() for imp in parsed_file.imports)
|
|
17
|
+
|
|
18
|
+
def extract_endpoints(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
|
|
19
|
+
endpoints: List[Endpoint] = []
|
|
20
|
+
|
|
21
|
+
# Multi-line regex supporting app, router, server, api
|
|
22
|
+
pattern = re.compile(
|
|
23
|
+
r"""(?:app|router|server|api)\.(get|post|put|delete|patch)\s*\(\s*["']([^"']+)["']\s*(?:,\s*([^)]+))?""",
|
|
24
|
+
re.IGNORECASE | re.DOTALL,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
content_lower = content.lower()
|
|
28
|
+
has_db_sink = any(
|
|
29
|
+
term in content_lower
|
|
30
|
+
for term in ("mongoose", "prisma", "sequelize", "knex", "pool.query", "db.query", "db.collection")
|
|
31
|
+
)
|
|
32
|
+
has_ext_sink = any(
|
|
33
|
+
term in content_lower
|
|
34
|
+
for term in ("axios", "node-fetch", "fetch(", "needle", "got(", "http.request", "https.request")
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
for match in pattern.finditer(content):
|
|
38
|
+
method = match.group(1).upper()
|
|
39
|
+
raw_path = match.group(2)
|
|
40
|
+
handler_and_middleware = match.group(3) or ""
|
|
41
|
+
|
|
42
|
+
# Calculate accurate line number
|
|
43
|
+
line_no = content[:match.start()].count("\n") + 1
|
|
44
|
+
|
|
45
|
+
# Convert Express :id into {id}
|
|
46
|
+
norm_path = re.sub(r":([a-zA-Z0-9_]+)", r"{\1}", raw_path)
|
|
47
|
+
path_params = re.findall(r":([a-zA-Z0-9_]+)", raw_path)
|
|
48
|
+
|
|
49
|
+
auth_required = any(
|
|
50
|
+
term in handler_and_middleware.lower()
|
|
51
|
+
for term in ("auth", "jwt", "passport", "verifytoken", "isauthenticated", "requireauth")
|
|
52
|
+
)
|
|
53
|
+
roles = ["admin"] if "admin" in handler_and_middleware.lower() or "admin" in norm_path.lower() else []
|
|
54
|
+
|
|
55
|
+
ep_id = f"ep_express_{parsed_file.file_path}_{line_no}_{method}".replace("/", "_").replace(".", "_")
|
|
56
|
+
|
|
57
|
+
endpoints.append(
|
|
58
|
+
Endpoint(
|
|
59
|
+
id=ep_id,
|
|
60
|
+
method=method,
|
|
61
|
+
path=norm_path,
|
|
62
|
+
handler_name=f"route_handler_L{line_no}",
|
|
63
|
+
auth_required=auth_required,
|
|
64
|
+
roles=roles,
|
|
65
|
+
parameters=[
|
|
66
|
+
EndpointParameter(name=p, location="path", required=True)
|
|
67
|
+
for p in path_params
|
|
68
|
+
],
|
|
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 norm_path.lower() or "user" in norm_path.lower() or "order" in norm_path.lower(),
|
|
74
|
+
source=SourceLocation(
|
|
75
|
+
file=parsed_file.file_path,
|
|
76
|
+
line_start=line_no,
|
|
77
|
+
line_end=line_no,
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return endpoints
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""FastAPI framework adapter for endpoint and attack-surface discovery."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List, 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
|
+
HTTP_METHODS = {"get", "post", "put", "delete", "patch", "options", "head"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FastAPIFrameworkAdapter(FrameworkAdapter):
|
|
13
|
+
"""Detects and extracts FastAPI/Starlette routes and auth gates."""
|
|
14
|
+
|
|
15
|
+
def can_handle(self, parsed_file: ParsedFile) -> bool:
|
|
16
|
+
if parsed_file.language != "python":
|
|
17
|
+
return False
|
|
18
|
+
for imp in parsed_file.imports:
|
|
19
|
+
if "fastapi" in imp.module.lower() or "starlette" in imp.module.lower():
|
|
20
|
+
return True
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
def extract_endpoints(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
|
|
24
|
+
endpoints: List[Endpoint] = []
|
|
25
|
+
ep_count = 0
|
|
26
|
+
|
|
27
|
+
# Also search decorators in parsed functions
|
|
28
|
+
for fn in parsed_file.functions:
|
|
29
|
+
for dec in fn.decorators:
|
|
30
|
+
dec_text = dec.raw_text.strip()
|
|
31
|
+
# Pattern: @app.get('/path' ...) or @router.post('/path' ...)
|
|
32
|
+
match = re.search(
|
|
33
|
+
r"""@?(?:[a-zA-Z0-9_]+)\.(get|post|put|delete|patch)\s*\(\s*["']([^"']+)["']""",
|
|
34
|
+
dec_text,
|
|
35
|
+
re.IGNORECASE,
|
|
36
|
+
)
|
|
37
|
+
if match:
|
|
38
|
+
ep_count += 1
|
|
39
|
+
method = match.group(1).upper()
|
|
40
|
+
path = match.group(2)
|
|
41
|
+
ep_id = f"ep_{parsed_file.file_path.replace('/', '_').replace('.', '_')}_{fn.name}_{method}"
|
|
42
|
+
|
|
43
|
+
# Detect path parameters: {param_name}
|
|
44
|
+
path_params = re.findall(r"\{([a-zA-Z0-9_]+)\}", path)
|
|
45
|
+
params: List[EndpointParameter] = [
|
|
46
|
+
EndpointParameter(name=p, location="path", required=True)
|
|
47
|
+
for p in path_params
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# Detect function params from AST
|
|
51
|
+
for p in fn.parameters:
|
|
52
|
+
if p.name not in ("self", "cls") and p.name not in path_params:
|
|
53
|
+
params.append(
|
|
54
|
+
EndpointParameter(
|
|
55
|
+
name=p.name,
|
|
56
|
+
location="query" if method == "GET" else "body",
|
|
57
|
+
param_type=p.type_annotation or "string",
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Analyze auth dependencies in decorator or args
|
|
62
|
+
auth_required = False
|
|
63
|
+
roles = []
|
|
64
|
+
fn_body = fn.body_text or ""
|
|
65
|
+
|
|
66
|
+
auth_indicators = [
|
|
67
|
+
"current_user",
|
|
68
|
+
"auth",
|
|
69
|
+
"token",
|
|
70
|
+
"security",
|
|
71
|
+
"jwt",
|
|
72
|
+
"oauth",
|
|
73
|
+
"credentials",
|
|
74
|
+
"verify_token",
|
|
75
|
+
"api_key",
|
|
76
|
+
]
|
|
77
|
+
for p in fn.parameters:
|
|
78
|
+
p_ann = (p.type_annotation or "").lower()
|
|
79
|
+
p_def = (p.default_value or "").lower()
|
|
80
|
+
if any(ind in p.name.lower() or ind in p_ann or ind in p_def for ind in auth_indicators):
|
|
81
|
+
auth_required = True
|
|
82
|
+
if "admin" in p.name.lower() or "admin" in p_ann or "admin" in p_def:
|
|
83
|
+
roles.append("admin")
|
|
84
|
+
|
|
85
|
+
if "require_admin" in dec_text.lower() or "admin" in path.lower():
|
|
86
|
+
roles.append("admin")
|
|
87
|
+
if "current_user" in dec_text.lower() or "depends(get_current_user" in dec_text.lower():
|
|
88
|
+
auth_required = True
|
|
89
|
+
|
|
90
|
+
# Object identifier flag (e.g. /items/{id})
|
|
91
|
+
has_obj_id = any(
|
|
92
|
+
p in ("id", "user_id", "order_id", "product_id", "item_id")
|
|
93
|
+
for p in path_params
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# State changing
|
|
97
|
+
state_changing = method in ("POST", "PUT", "DELETE", "PATCH")
|
|
98
|
+
|
|
99
|
+
# Database access check
|
|
100
|
+
db_access = any(
|
|
101
|
+
term in fn_body.lower()
|
|
102
|
+
for term in ("db.", "session.", "cursor.", "repository", "query(", ".filter(", "orders", "database", "find")
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Outbound network (SSRF indicator)
|
|
106
|
+
ext_network = any(
|
|
107
|
+
term in fn_body.lower()
|
|
108
|
+
for term in ("requests.", "httpx.", "urllib.", "aiohttp.", "fetch(")
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Sensitive data
|
|
112
|
+
sensitive = any(
|
|
113
|
+
term in path.lower() or term in fn_body.lower()
|
|
114
|
+
for term in ("password", "secret", "token", "credit", "card", "user", "order", "refund", "admin")
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
endpoints.append(
|
|
118
|
+
Endpoint(
|
|
119
|
+
id=ep_id,
|
|
120
|
+
method=method,
|
|
121
|
+
path=path,
|
|
122
|
+
handler_name=fn.qualified_name,
|
|
123
|
+
auth_required=auth_required,
|
|
124
|
+
roles=list(set(roles)),
|
|
125
|
+
parameters=params,
|
|
126
|
+
database_access=db_access,
|
|
127
|
+
object_identifier=has_obj_id,
|
|
128
|
+
state_changing=state_changing,
|
|
129
|
+
external_network=ext_network,
|
|
130
|
+
sensitive_data=sensitive,
|
|
131
|
+
source=fn.location,
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return endpoints
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Flask framework adapter for endpoint, Blueprint, and parameter 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 FlaskFrameworkAdapter(FrameworkAdapter):
|
|
11
|
+
"""Detects and extracts Flask routes, Blueprints, parameters, and database/network sinks."""
|
|
12
|
+
|
|
13
|
+
def can_handle(self, parsed_file: ParsedFile) -> bool:
|
|
14
|
+
if parsed_file.language != "python":
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
for imp in parsed_file.imports:
|
|
18
|
+
mod = imp.module.lower()
|
|
19
|
+
if "flask" in mod:
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
# Check for typical Flask route decorators in content
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
def extract_endpoints(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
|
|
26
|
+
endpoints: List[Endpoint] = []
|
|
27
|
+
content_lower = content.lower()
|
|
28
|
+
|
|
29
|
+
# Database and sink detection
|
|
30
|
+
has_db_sink = any(
|
|
31
|
+
term in content_lower
|
|
32
|
+
for term in (
|
|
33
|
+
"db.session", ".query.", "session.execute", "cursor.execute",
|
|
34
|
+
"sqlite3.connect", "psycopg2.connect", "find_one", "find(", "collection."
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
has_ext_sink = any(
|
|
38
|
+
term in content_lower
|
|
39
|
+
for term in ("requests.get", "requests.post", "httpx.get", "httpx.post", "urllib.request")
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
for fn in parsed_file.functions:
|
|
43
|
+
fn_body = fn.body_text or ""
|
|
44
|
+
fn_body_lower = fn_body.lower()
|
|
45
|
+
|
|
46
|
+
for dec in fn.decorators:
|
|
47
|
+
dec_text = dec.raw_text.strip()
|
|
48
|
+
# Matches @app.route('/path', methods=['GET', 'POST']) or @bp.route('/path') or @blueprint.route
|
|
49
|
+
route_match = re.search(
|
|
50
|
+
r"""@?(?:[a-zA-Z0-9_]+)\.route\s*\(\s*["']([^"']+)["'](?:\s*,\s*methods\s*=\s*\[([^\]]+)\])?""",
|
|
51
|
+
dec_text,
|
|
52
|
+
re.IGNORECASE,
|
|
53
|
+
)
|
|
54
|
+
if not route_match:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
raw_path = route_match.group(1).strip()
|
|
58
|
+
methods_raw = route_match.group(2)
|
|
59
|
+
|
|
60
|
+
if methods_raw:
|
|
61
|
+
methods = [
|
|
62
|
+
m.strip().strip("'\"").upper()
|
|
63
|
+
for m in methods_raw.split(",")
|
|
64
|
+
if m.strip()
|
|
65
|
+
]
|
|
66
|
+
else:
|
|
67
|
+
methods = ["GET"]
|
|
68
|
+
|
|
69
|
+
# Flask path parameters: <int:id>, <string:slug>, <path:filename>, <uuid:pk> -> {id}, {slug}, {filename}, {pk}
|
|
70
|
+
clean_path = re.sub(r"<(?:[a-zA-Z0-9_]+:)?([a-zA-Z0-9_]+)>", r"{\1}", raw_path)
|
|
71
|
+
norm_path = clean_path if clean_path.startswith("/") else f"/{clean_path}"
|
|
72
|
+
path_params = re.findall(r"\{([a-zA-Z0-9_]+)\}", norm_path)
|
|
73
|
+
|
|
74
|
+
# Extract request.args or request.form parameters from function body
|
|
75
|
+
query_params = re.findall(r"""request\.args\.get\s*\(\s*["']([a-zA-Z0-9_]+)["']""", fn_body)
|
|
76
|
+
body_params = re.findall(r"""request\.(?:form|json)\.get\s*\(\s*["']([a-zA-Z0-9_]+)["']""", fn_body)
|
|
77
|
+
|
|
78
|
+
params = [EndpointParameter(name=p, location="path", required=True) for p in path_params]
|
|
79
|
+
params.extend([EndpointParameter(name=p, location="query", required=False) for p in query_params])
|
|
80
|
+
params.extend([EndpointParameter(name=p, location="body", required=False) for p in body_params])
|
|
81
|
+
|
|
82
|
+
auth_required = any(
|
|
83
|
+
sig in fn_body_lower or sig in dec_text.lower()
|
|
84
|
+
for sig in ("@login_required", "@jwt_required", "current_user", "get_jwt_identity", "auth")
|
|
85
|
+
)
|
|
86
|
+
roles = ["admin"] if "admin" in fn.name.lower() or "admin" in norm_path.lower() else []
|
|
87
|
+
|
|
88
|
+
for m in methods:
|
|
89
|
+
ep_id = f"ep_flask_{parsed_file.file_path}_{fn.name}_{m}".replace("/", "_").replace(".", "_")
|
|
90
|
+
endpoints.append(
|
|
91
|
+
Endpoint(
|
|
92
|
+
id=ep_id,
|
|
93
|
+
method=m,
|
|
94
|
+
path=norm_path,
|
|
95
|
+
handler_name=fn.qualified_name,
|
|
96
|
+
auth_required=auth_required,
|
|
97
|
+
roles=roles,
|
|
98
|
+
parameters=params,
|
|
99
|
+
database_access=has_db_sink or "execute(" in fn_body_lower or "query" in fn_body_lower,
|
|
100
|
+
object_identifier=bool(path_params),
|
|
101
|
+
state_changing=m in ("POST", "PUT", "DELETE", "PATCH"),
|
|
102
|
+
external_network=has_ext_sink or "requests." in fn_body_lower or "urllib" in fn_body_lower,
|
|
103
|
+
sensitive_data="admin" in norm_path.lower() or "user" in norm_path.lower() or "secret" in norm_path.lower(),
|
|
104
|
+
source=fn.location,
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return endpoints
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Go framework adapter for Gin, Fiber, Chi, Echo, and standard net/http 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 GoFrameworkAdapter(FrameworkAdapter):
|
|
11
|
+
"""Detects and extracts Go web routes, closures, parameters, and middleware across Gin, Echo, Fiber, Chi, and net/http."""
|
|
12
|
+
|
|
13
|
+
def can_handle(self, parsed_file: ParsedFile) -> bool:
|
|
14
|
+
if parsed_file.language != "go":
|
|
15
|
+
norm = parsed_file.file_path.replace("\\", "/").lower()
|
|
16
|
+
return norm.endswith(".go")
|
|
17
|
+
return True
|
|
18
|
+
|
|
19
|
+
def extract_endpoints(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
|
|
20
|
+
endpoints: List[Endpoint] = []
|
|
21
|
+
norm_file_path = 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 ("db.query", "db.exec", "db.raw", "gorm", "sqlx", "mongo.", "redis.", "findone", "sql.open")
|
|
27
|
+
)
|
|
28
|
+
has_ext_sink = any(
|
|
29
|
+
term in content_lower
|
|
30
|
+
for term in ("http.get", "http.post", "http.client", "fasthttp", "resty", "http.newrequest", "client.do")
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
auth_signals = ("auth", "jwt", "bearer", "token", "requireauth", "admin", "middleware.auth", "authrequired")
|
|
34
|
+
auth_required = any(sig in content_lower for sig in auth_signals) or "admin" in norm_file_path.lower()
|
|
35
|
+
roles = ["admin"] if "admin" in content_lower or "admin" in norm_file_path.lower() else []
|
|
36
|
+
|
|
37
|
+
# Matches:
|
|
38
|
+
# 1. Gin/Echo: r.GET("/path", handler) or r.GET("/path", func(c *gin.Context) {
|
|
39
|
+
# 2. Chi/Fiber: r.Get("/path", handler) or app.Post("/path", func(...) {
|
|
40
|
+
# 3. net/http: http.HandleFunc("/path", handler) or mux.Handle("/path", ...)
|
|
41
|
+
pattern = re.compile(
|
|
42
|
+
r"""(?:[a-zA-Z0-9_]+)\.(GET|POST|PUT|DELETE|PATCH|Get|Post|Put|Delete|Patch|Handle|HandleFunc)\s*\(\s*["']([^"']+)["']\s*,\s*([a-zA-Z0-9_\.]+|func\s*\([^)]*\))""",
|
|
43
|
+
re.MULTILINE
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
for match in pattern.finditer(content):
|
|
47
|
+
raw_method = match.group(1).upper()
|
|
48
|
+
method = "GET" if raw_method in ("HANDLE", "HANDLEFUNC") else raw_method
|
|
49
|
+
raw_path = match.group(2)
|
|
50
|
+
raw_handler = match.group(3).strip()
|
|
51
|
+
handler_name = "closure_handler" if raw_handler.startswith("func") else raw_handler
|
|
52
|
+
line_no = content[:match.start()].count("\n") + 1
|
|
53
|
+
|
|
54
|
+
# Convert Go :id or {id} or *wildcard to standard {id}
|
|
55
|
+
norm_path = re.sub(r':([a-zA-Z0-9_]+)', r'{\1}', raw_path)
|
|
56
|
+
norm_path = re.sub(r'\*([a-zA-Z0-9_]+)', r'{\1}', norm_path)
|
|
57
|
+
norm_path = "/" + norm_path.lstrip("/")
|
|
58
|
+
|
|
59
|
+
path_params = re.findall(r'\{([a-zA-Z0-9_]+)\}', norm_path)
|
|
60
|
+
|
|
61
|
+
# Query and body parameter extraction from Go conventions
|
|
62
|
+
query_params = re.findall(r"""(?:c\.Query|r\.URL\.Query\(\)\.Get)\s*\(\s*["']([a-zA-Z0-9_]+)["']""", content)
|
|
63
|
+
body_params = re.findall(r"""(?:c\.PostForm|c\.BindJSON|c\.BodyParser)\s*\(\s*&?([a-zA-Z0-9_]+)""", content)
|
|
64
|
+
|
|
65
|
+
params = [EndpointParameter(name=p, location="path", required=True) for p in path_params]
|
|
66
|
+
params.extend([EndpointParameter(name=p, location="query", required=False) for p in query_params])
|
|
67
|
+
params.extend([EndpointParameter(name=p, location="body", required=False) for p in body_params])
|
|
68
|
+
|
|
69
|
+
ep_id = f"ep_go_{norm_file_path}_{line_no}_{method}".replace("/", "_").replace(".", "_")
|
|
70
|
+
|
|
71
|
+
sensitive = any(
|
|
72
|
+
term in norm_path.lower() or term in content_lower
|
|
73
|
+
for term in ("admin", "user", "auth", "billing", "payment", "token", "secret", "order")
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
endpoints.append(
|
|
77
|
+
Endpoint(
|
|
78
|
+
id=ep_id,
|
|
79
|
+
method=method,
|
|
80
|
+
path=norm_path,
|
|
81
|
+
handler_name=handler_name,
|
|
82
|
+
auth_required=auth_required or ("admin" in norm_path.lower()),
|
|
83
|
+
roles=roles if "admin" not in norm_path.lower() else ["admin"],
|
|
84
|
+
parameters=params,
|
|
85
|
+
database_access=has_db_sink,
|
|
86
|
+
object_identifier=bool(path_params),
|
|
87
|
+
state_changing=method in ("POST", "PUT", "DELETE", "PATCH"),
|
|
88
|
+
external_network=has_ext_sink,
|
|
89
|
+
sensitive_data=sensitive,
|
|
90
|
+
source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return endpoints
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Next.js framework adapter for App Router and Pages Router endpoint discovery."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Dict, Optional
|
|
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 NextJSFrameworkAdapter(FrameworkAdapter):
|
|
12
|
+
"""Detects and extracts Next.js API routes, route handlers, and page endpoints."""
|
|
13
|
+
|
|
14
|
+
def _infer_app_router_path(self, norm_path: str) -> str:
|
|
15
|
+
# Strip leading app/ or src/app/
|
|
16
|
+
clean = norm_path
|
|
17
|
+
if clean.startswith("src/app/"):
|
|
18
|
+
clean = clean[8:]
|
|
19
|
+
elif clean.startswith("app/"):
|
|
20
|
+
clean = clean[4:]
|
|
21
|
+
|
|
22
|
+
# Strip trailing /route.ts, /route.js, /page.tsx, etc.
|
|
23
|
+
clean = re.sub(r'/(?:route|page)\.[a-zA-Z0-9]+$', '', clean)
|
|
24
|
+
|
|
25
|
+
# Remove route groups like (auth), (dashboard)
|
|
26
|
+
parts = [p for p in clean.split("/") if not (p.startswith("(") and p.endswith(")"))]
|
|
27
|
+
clean = "/".join(parts)
|
|
28
|
+
|
|
29
|
+
# Normalize Next.js params: [[...slug]] -> {slug}, [...slug] -> {slug}, [id] -> {id}
|
|
30
|
+
clean = re.sub(r'\[\[?\.\.\.([a-zA-Z0-9_]+)\]?\]', r'{\1}', clean)
|
|
31
|
+
clean = re.sub(r'\[([a-zA-Z0-9_]+)\]', r'{\1}', clean)
|
|
32
|
+
|
|
33
|
+
norm_url = "/" + clean.lstrip("/")
|
|
34
|
+
return norm_url or "/"
|
|
35
|
+
|
|
36
|
+
def _infer_pages_router_path(self, norm_path: str) -> str:
|
|
37
|
+
# Strip leading pages/ or src/pages/
|
|
38
|
+
clean = norm_path
|
|
39
|
+
if clean.startswith("src/pages/"):
|
|
40
|
+
clean = clean[10:]
|
|
41
|
+
elif clean.startswith("pages/"):
|
|
42
|
+
clean = clean[6:]
|
|
43
|
+
|
|
44
|
+
# Strip file extension
|
|
45
|
+
clean = re.sub(r'\.[a-zA-Z0-9]+$', '', clean)
|
|
46
|
+
|
|
47
|
+
# Normalize params: [id] -> {id}
|
|
48
|
+
clean = re.sub(r'\[\[?\.\.\.([a-zA-Z0-9_]+)\]?\]', r'{\1}', clean)
|
|
49
|
+
clean = re.sub(r'\[([a-zA-Z0-9_]+)\]', r'{\1}', clean)
|
|
50
|
+
|
|
51
|
+
# Strip index
|
|
52
|
+
clean = re.sub(r'(?:^|/)index$', '', clean)
|
|
53
|
+
|
|
54
|
+
norm_url = "/" + clean.lstrip("/")
|
|
55
|
+
return norm_url or "/"
|
|
56
|
+
|
|
57
|
+
def can_handle(self, parsed_file: ParsedFile) -> bool:
|
|
58
|
+
if parsed_file.language not in ("javascript", "typescript"):
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
norm_path = parsed_file.file_path.replace("\\", "/")
|
|
62
|
+
|
|
63
|
+
# App Router route/page
|
|
64
|
+
if re.search(r'(?:^|/)(?:src/)?app/.+/(?:route|page)\.[a-zA-Z0-9]+$', norm_path):
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
# Pages Router api or page
|
|
68
|
+
if re.search(r'(?:^|/)(?:src/)?pages/(?:api/)?.+\.[a-zA-Z0-9]+$', norm_path):
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
# Imports from next
|
|
72
|
+
for imp in parsed_file.imports:
|
|
73
|
+
mod_low = imp.module.lower()
|
|
74
|
+
if mod_low == "next" or mod_low.startswith("next/"):
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
def extract_endpoints(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
|
|
80
|
+
endpoints: List[Endpoint] = []
|
|
81
|
+
norm_file_path = parsed_file.file_path.replace("\\", "/")
|
|
82
|
+
|
|
83
|
+
is_app_router = bool(re.search(r'(?:^|/)(?:src/)?app/', norm_file_path))
|
|
84
|
+
is_pages_router = bool(re.search(r'(?:^|/)(?:src/)?pages/', norm_file_path))
|
|
85
|
+
|
|
86
|
+
if is_app_router:
|
|
87
|
+
url_path = self._infer_app_router_path(norm_file_path)
|
|
88
|
+
elif is_pages_router:
|
|
89
|
+
url_path = self._infer_pages_router_path(norm_file_path)
|
|
90
|
+
else:
|
|
91
|
+
url_path = "/" + norm_file_path.split("/")[-1].split(".")[0]
|
|
92
|
+
|
|
93
|
+
path_params = re.findall(r'\{([a-zA-Z0-9_]+)\}', url_path)
|
|
94
|
+
|
|
95
|
+
content_lower = content.lower()
|
|
96
|
+
has_db_sink = any(
|
|
97
|
+
term in content_lower
|
|
98
|
+
for term in (
|
|
99
|
+
"prisma", "mongoose", "sequelize", "supabase", "knex",
|
|
100
|
+
"pool.query", "client.query", "db.query", "db.select", ".from("
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
has_ext_sink = any(
|
|
104
|
+
term in content_lower
|
|
105
|
+
for term in (
|
|
106
|
+
"axios", "fetch(", "nodemailer", "got(", "needle",
|
|
107
|
+
"http.request", "https.request", "transporter.sendmail", "octokit", "stripe"
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
auth_signals = (
|
|
112
|
+
"getserversession", "getauth", "auth()", "clerk", "next-auth",
|
|
113
|
+
"verifytoken", "requireauth", "requireadmin", "jwt.verify", "supabase.auth"
|
|
114
|
+
)
|
|
115
|
+
auth_required = any(sig in content_lower for sig in auth_signals) or "admin" in url_path.lower()
|
|
116
|
+
roles = ["admin"] if "admin" in content_lower or "admin" in url_path.lower() else []
|
|
117
|
+
|
|
118
|
+
sensitive_data = any(
|
|
119
|
+
term in url_path.lower() or term in content_lower
|
|
120
|
+
for term in (
|
|
121
|
+
"admin", "user", "auth", "login", "register", "profile",
|
|
122
|
+
"password", "token", "secret", "webhook", "session", "payment"
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
parameters: List[EndpointParameter] = [
|
|
127
|
+
EndpointParameter(name=p, location="path", required=True, user_controlled=True)
|
|
128
|
+
for p in path_params
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
# Extract searchParams
|
|
132
|
+
for m in re.finditer(r'''(?:searchParams|nextUrl\.searchParams)\.get\s*\(\s*["']([^"']+)["']''', content):
|
|
133
|
+
p_name = m.group(1)
|
|
134
|
+
if not any(p.name == p_name for p in parameters):
|
|
135
|
+
parameters.append(
|
|
136
|
+
EndpointParameter(name=p_name, location="query", required=False, user_controlled=True)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# App Router named exports: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
|
|
140
|
+
app_verbs = list(re.finditer(
|
|
141
|
+
r'''export\s+(?:async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b''',
|
|
142
|
+
content
|
|
143
|
+
))
|
|
144
|
+
|
|
145
|
+
for v_match in app_verbs:
|
|
146
|
+
verb = v_match.group(1).upper()
|
|
147
|
+
line_no = content[:v_match.start()].count("\n") + 1
|
|
148
|
+
ep_id = f"ep_next_{verb.lower()}_{norm_file_path}_{line_no}".replace("/", "_").replace(".", "_")
|
|
149
|
+
endpoints.append(
|
|
150
|
+
Endpoint(
|
|
151
|
+
id=ep_id,
|
|
152
|
+
method=verb,
|
|
153
|
+
path=url_path,
|
|
154
|
+
handler_name=verb,
|
|
155
|
+
auth_required=auth_required,
|
|
156
|
+
roles=roles,
|
|
157
|
+
parameters=parameters,
|
|
158
|
+
database_access=has_db_sink,
|
|
159
|
+
object_identifier=bool(path_params),
|
|
160
|
+
state_changing=verb in ("POST", "PUT", "DELETE", "PATCH"),
|
|
161
|
+
external_network=has_ext_sink,
|
|
162
|
+
sensitive_data=sensitive_data,
|
|
163
|
+
source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Pages Router or App Router default export
|
|
168
|
+
if not endpoints:
|
|
169
|
+
# Check for checked HTTP methods in handler body
|
|
170
|
+
checked_methods = []
|
|
171
|
+
for m in re.finditer(r'''req\.method\s*===?\s*["'](GET|POST|PUT|DELETE|PATCH)["']''', content):
|
|
172
|
+
checked_methods.append(m.group(1).upper())
|
|
173
|
+
|
|
174
|
+
methods_to_add = set(checked_methods) if checked_methods else (["GET"] if "page." in norm_file_path else ["GET", "POST"])
|
|
175
|
+
|
|
176
|
+
default_match = re.search(r'''export\s+default\s+(?:async\s+)?(?:function|const|class)?\s*([a-zA-Z0-9_$]+)?''', content)
|
|
177
|
+
handler_name = default_match.group(1) if (default_match and default_match.group(1)) else "handler"
|
|
178
|
+
line_no = content[:default_match.start()].count("\n") + 1 if default_match else 1
|
|
179
|
+
|
|
180
|
+
for meth in sorted(methods_to_add):
|
|
181
|
+
ep_id = f"ep_next_{meth.lower()}_{norm_file_path}_{line_no}".replace("/", "_").replace(".", "_")
|
|
182
|
+
endpoints.append(
|
|
183
|
+
Endpoint(
|
|
184
|
+
id=ep_id,
|
|
185
|
+
method=meth,
|
|
186
|
+
path=url_path,
|
|
187
|
+
handler_name=handler_name,
|
|
188
|
+
auth_required=auth_required,
|
|
189
|
+
roles=roles,
|
|
190
|
+
parameters=parameters,
|
|
191
|
+
database_access=has_db_sink,
|
|
192
|
+
object_identifier=bool(path_params),
|
|
193
|
+
state_changing=meth in ("POST", "PUT", "DELETE", "PATCH"),
|
|
194
|
+
external_network=has_ext_sink,
|
|
195
|
+
sensitive_data=sensitive_data,
|
|
196
|
+
source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return endpoints
|