trace-sec 2.1.13 → 2.1.15
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/package.json +1 -4
- package/pyproject.toml +1 -1
- package/src/trace_engine/__init__.py +1 -1
- package/src/trace_engine/findings/__init__.py +2 -1
- package/src/trace_engine/findings/correlate.py +12 -2
- package/src/trace_engine/findings/model.py +2 -0
- package/src/trace_engine/findings/recommendations.py +447 -143
- package/src/trace_engine/findings/store.py +13 -1
- package/src/trace_engine/framework/express.py +5 -1
- package/src/trace_engine/interactive.py +43 -39
- package/src/trace_engine/mcp/server.py +4 -0
- package/src/trace_engine/output/markdown.py +14 -7
- package/src/trace_engine/output/terminal.py +9 -3
- package/src/trace_engine/security/hypotheses.py +64 -1
package/package.json
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trace-sec",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.15",
|
|
4
4
|
"description": "Threat Reconnaissance & Attack-path Correlation Engine — Security Verification Layer for Coding Agents with Fine-Tuned SecureBERT 2.0 & Laya AI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/trace.js",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"postinstall": "node bin/postinstall.js"
|
|
9
|
-
},
|
|
10
7
|
"bin": {
|
|
11
8
|
"trace": "bin/trace.js",
|
|
12
9
|
"trace-sec": "bin/trace.js",
|
package/pyproject.toml
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from trace_engine.findings.model import Finding, Severity, FindingConfidence
|
|
4
4
|
from trace_engine.findings.correlate import EvidenceCorrelator
|
|
5
5
|
from trace_engine.findings.store import FindingStore
|
|
6
|
-
from trace_engine.findings.recommendations import get_remediation_for_category
|
|
6
|
+
from trace_engine.findings.recommendations import get_remediation_for_category, get_detailed_remediation_and_root_cause
|
|
7
7
|
|
|
8
8
|
__all__ = [
|
|
9
9
|
"Finding",
|
|
@@ -12,4 +12,5 @@ __all__ = [
|
|
|
12
12
|
"EvidenceCorrelator",
|
|
13
13
|
"FindingStore",
|
|
14
14
|
"get_remediation_for_category",
|
|
15
|
+
"get_detailed_remediation_and_root_cause",
|
|
15
16
|
]
|
|
@@ -4,7 +4,7 @@ from typing import List, Optional
|
|
|
4
4
|
from trace_engine.security.hypotheses import SecurityHypothesis
|
|
5
5
|
from trace_engine.testpacks.base import TestExecutionResult
|
|
6
6
|
from trace_engine.findings.model import Finding, Severity, FindingConfidence
|
|
7
|
-
from trace_engine.findings.recommendations import get_remediation_for_category
|
|
7
|
+
from trace_engine.findings.recommendations import get_detailed_remediation_and_root_cause, get_remediation_for_category
|
|
8
8
|
from trace_engine.apm.model import AttackPathModel
|
|
9
9
|
|
|
10
10
|
|
|
@@ -91,6 +91,14 @@ class EvidenceCorrelator:
|
|
|
91
91
|
|
|
92
92
|
file_path = source_loc.file if source_loc else ""
|
|
93
93
|
|
|
94
|
+
root_cause, remediation, short_action = get_detailed_remediation_and_root_cause(
|
|
95
|
+
title=hypothesis.title,
|
|
96
|
+
category=category_str,
|
|
97
|
+
endpoint=hypothesis.endpoint_display,
|
|
98
|
+
static_evidence=hypothesis.static_evidence,
|
|
99
|
+
filepath=file_path,
|
|
100
|
+
)
|
|
101
|
+
|
|
94
102
|
return Finding(
|
|
95
103
|
id=finding_id,
|
|
96
104
|
title=hypothesis.title,
|
|
@@ -103,7 +111,9 @@ class EvidenceCorrelator:
|
|
|
103
111
|
static_evidence=hypothesis.static_evidence,
|
|
104
112
|
runtime_evidence=runtime_evidence,
|
|
105
113
|
correlation_notes=correlation_notes,
|
|
106
|
-
remediation=
|
|
114
|
+
remediation=remediation,
|
|
115
|
+
root_cause=root_cause,
|
|
116
|
+
short_action=short_action,
|
|
107
117
|
reproduction_steps=test_result.reproduction_steps if test_result else [],
|
|
108
118
|
observations=test_result.observations if test_result else [],
|
|
109
119
|
)
|
|
@@ -1,152 +1,456 @@
|
|
|
1
|
-
"""Remediation
|
|
2
|
-
|
|
3
|
-
from typing import Optional
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
REMEDIATION_MAP_PYTHON = {
|
|
7
|
-
"BOLA": (
|
|
8
|
-
"Enforce strict object ownership validation before returning records. "
|
|
9
|
-
"Query using both the object ID and the authenticated user/tenant ID (e.g. `WHERE id = :id AND user_id = :current_user.id`)."
|
|
10
|
-
),
|
|
11
|
-
"BFLA": (
|
|
12
|
-
"Implement declarative role-based access control (RBAC) dependencies (e.g. `Security(require_role('admin'))`). "
|
|
13
|
-
"Validate user privileges on the backend for every privileged/administrative route."
|
|
14
|
-
),
|
|
15
|
-
"AUTHENTICATION": (
|
|
16
|
-
"Protect this route with authentication dependencies (e.g. `Depends(get_current_user)` or security middleware) "
|
|
17
|
-
"to ensure unauthorized users cannot trigger state-changing or sensitive operations."
|
|
18
|
-
),
|
|
19
|
-
"SSRF": (
|
|
20
|
-
"Avoid making arbitrary outbound HTTP requests with user-controlled URLs. "
|
|
21
|
-
"Implement a strict destination domain whitelist and block loopback/internal IP addresses (RFC 1918)."
|
|
22
|
-
),
|
|
23
|
-
"INJECTION": (
|
|
24
|
-
"Use parameterized queries, prepared statements, or ORM abstraction for all database operations. "
|
|
25
|
-
"Never format or concatenate raw client inputs into SQL/command strings."
|
|
26
|
-
),
|
|
27
|
-
"MASS_ASSIGNMENT": (
|
|
28
|
-
"Define explicit request DTO schemas (e.g. Pydantic models with only allowed mutable fields). "
|
|
29
|
-
"Do not allow raw dictionary binding directly into database models."
|
|
30
|
-
),
|
|
31
|
-
"PATH_TRAVERSAL": (
|
|
32
|
-
"Sanitize file path inputs using `os.path.basename()` or `pathlib.Path.resolve()`, "
|
|
33
|
-
"and verify that the resolved path is strictly within the allowed directory. Disallow path traversal sequences (`..`)."
|
|
34
|
-
),
|
|
35
|
-
"SSTI": (
|
|
36
|
-
"Avoid rendering untrusted user inputs directly inside template engines. "
|
|
37
|
-
"Use contextual output encoding or sandbox template evaluation environments."
|
|
38
|
-
),
|
|
39
|
-
"CORS": (
|
|
40
|
-
"Do not reflect arbitrary Origin headers or use wildcard `*` with credentials. "
|
|
41
|
-
"Configure an explicit allowlist of trusted origins."
|
|
42
|
-
),
|
|
43
|
-
"DESERIALIZATION": (
|
|
44
|
-
"Avoid deserializing untrusted payloads (e.g. `pickle.loads`). "
|
|
45
|
-
"Use safe serialization formats like standard JSON with strict schema validation."
|
|
46
|
-
),
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
REMEDIATION_MAP_NODE = {
|
|
50
|
-
"BOLA": (
|
|
51
|
-
"Enforce strict object ownership validation before returning or modifying records. "
|
|
52
|
-
"Scope queries using both the object ID and the authenticated user/tenant identity (e.g. `findByIdAndUserId(req.params.id, req.user.id)` or `WHERE id = ? AND tenant_id = ?`)."
|
|
53
|
-
),
|
|
54
|
-
"BFLA": (
|
|
55
|
-
"Implement role-based access control (RBAC) middleware (e.g. `requireRole('admin')` or `checkPermission(...)`). "
|
|
56
|
-
"Validate `req.user.role` on the backend before executing privileged operations."
|
|
57
|
-
),
|
|
58
|
-
"AUTHENTICATION": (
|
|
59
|
-
"Mount authentication middleware (e.g. `router.use(verifyToken)` or `app.use('/api/...', verifyToken, router)`) "
|
|
60
|
-
"to ensure unauthorized users cannot trigger state-changing or sensitive operations."
|
|
61
|
-
),
|
|
62
|
-
"SSRF": (
|
|
63
|
-
"Avoid making outbound HTTP requests (e.g. with `axios` or `fetch`) using user-controlled URLs. "
|
|
64
|
-
"Implement a strict destination domain allowlist and block loopback (`127.0.0.1`, `localhost`) and private RFC-1918 IP addresses."
|
|
65
|
-
),
|
|
66
|
-
"INJECTION": (
|
|
67
|
-
"Use parameterized queries, prepared statements, or ORM abstractions (e.g. Prisma, Mongoose, TypeORM, Knex). "
|
|
68
|
-
"Never concatenate or interpolate raw `req.body` or `req.query` into database queries or shell commands."
|
|
69
|
-
),
|
|
70
|
-
"MASS_ASSIGNMENT": (
|
|
71
|
-
"Define explicit request DTO schemas (e.g. using Zod, Joi, or TypeScript DTOs) and pick allowlisted fields from `req.body`. "
|
|
72
|
-
"Never pass raw `req.body` directly into database creation or update functions."
|
|
73
|
-
),
|
|
74
|
-
"PATH_TRAVERSAL": (
|
|
75
|
-
"Sanitize file path inputs using `path.basename()` or `path.resolve()`, "
|
|
76
|
-
"and verify that the target path strictly resides within the intended root directory (e.g. `resolvedPath.startsWith(BASE_DIR)`). Disallow directory traversal sequences (`..`)."
|
|
77
|
-
),
|
|
78
|
-
"SSTI": (
|
|
79
|
-
"Avoid rendering untrusted user inputs directly inside template engines. "
|
|
80
|
-
"Use contextual output encoding or sandbox template evaluation environments."
|
|
81
|
-
),
|
|
82
|
-
"CORS": (
|
|
83
|
-
"Do not reflect the `Origin` request header or allow `Access-Control-Allow-Origin: *` with `Access-Control-Allow-Credentials: true`. "
|
|
84
|
-
"Configure an explicit allowlist of trusted origins."
|
|
85
|
-
),
|
|
86
|
-
"DESERIALIZATION": (
|
|
87
|
-
"Avoid deserializing untrusted payloads (e.g. `node-serialize`). "
|
|
88
|
-
"Use safe data serialization formats like standard `JSON.parse` with strict schema validation."
|
|
89
|
-
),
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
REMEDIATION_MAP_GO = {
|
|
93
|
-
"BOLA": (
|
|
94
|
-
"Enforce strict tenant and user ownership validation. "
|
|
95
|
-
"Scope database queries to the authenticated user ID from context (e.g. `db.Where(\"id = ? AND user_id = ?\", id, currentUserID)`)."
|
|
96
|
-
),
|
|
97
|
-
"BFLA": (
|
|
98
|
-
"Implement role-based authorization checks in Gin/Echo middleware. "
|
|
99
|
-
"Verify user claims from context before proceeding with administrative actions."
|
|
100
|
-
),
|
|
101
|
-
"AUTHENTICATION": (
|
|
102
|
-
"Protect this route with authentication middleware (e.g. Gin/Echo middleware validating JWT/session token into context) "
|
|
103
|
-
"to ensure unauthorized users cannot trigger state-changing operations."
|
|
104
|
-
),
|
|
105
|
-
"SSRF": (
|
|
106
|
-
"Validate target URLs against an allowlist and block private IP ranges before executing `http.Get` or `http.Post`."
|
|
107
|
-
),
|
|
108
|
-
"INJECTION": (
|
|
109
|
-
"Use parameterized SQL placeholders (`?` or `$1`) with `database/sql` or GORM. Never concatenate input into raw SQL queries."
|
|
110
|
-
),
|
|
111
|
-
"MASS_ASSIGNMENT": (
|
|
112
|
-
"Bind incoming requests to dedicated request structs with strictly permitted fields instead of raw map or model structs."
|
|
113
|
-
),
|
|
114
|
-
"PATH_TRAVERSAL": (
|
|
115
|
-
"Use `filepath.Clean` and `filepath.Rel` to verify file paths stay within the target base directory."
|
|
116
|
-
),
|
|
117
|
-
"SSTI": "Ensure HTML templates use context-aware escaping (`html/template`).",
|
|
118
|
-
"CORS": "Configure explicit origins in CORS middleware. Do not allow wildcard origins with credentials.",
|
|
119
|
-
"DESERIALIZATION": "Use standard `encoding/json` with typed Go structs for decoding untrusted client data.",
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
# Default backwards-compatible map
|
|
123
|
-
REMEDIATION_MAP = REMEDIATION_MAP_PYTHON
|
|
1
|
+
"""Remediation and Root Cause Explanation Engine for TRACE security findings.
|
|
124
2
|
|
|
3
|
+
Generates precise, non-vague, technical root cause analyses, step-by-step remediation actions,
|
|
4
|
+
and concise terminal action summaries tailored to the specific vulnerability pattern.
|
|
5
|
+
"""
|
|
125
6
|
|
|
126
|
-
|
|
127
|
-
|
|
7
|
+
from typing import Optional, Dict, Any, List, Tuple
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_detailed_remediation_and_root_cause(
|
|
11
|
+
title: str,
|
|
12
|
+
category: str,
|
|
13
|
+
endpoint: str,
|
|
14
|
+
static_evidence: Optional[List[str]] = None,
|
|
15
|
+
filepath: str = "",
|
|
16
|
+
) -> Tuple[str, str, str]:
|
|
17
|
+
"""Generates: (root_cause_explanation, detailed_remediation_steps, short_action_summary).
|
|
18
|
+
|
|
19
|
+
All three outputs are tailored specifically to the exact finding rather than generic category templates.
|
|
20
|
+
"""
|
|
21
|
+
t_lower = title.lower()
|
|
22
|
+
ep_lower = endpoint.lower()
|
|
23
|
+
ev_text = " ".join(static_evidence or []).lower()
|
|
128
24
|
cat_upper = category.upper()
|
|
129
|
-
file_lower = filepath.lower()
|
|
130
25
|
|
|
131
|
-
#
|
|
132
|
-
if any(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
"
|
|
26
|
+
# Confidential Startup Workspace IP Exfiltration (BOLA / Exposure)
|
|
27
|
+
if any(k in ep_lower for k in ("/dashboard/workspace", "/dashboard/business", "/dashboard/branding", "/dashboard/product", "/dashboard/engineering", "/dashboard/project-management")):
|
|
28
|
+
root_cause = (
|
|
29
|
+
f"Endpoint `{endpoint}` directly reads proprietary startup intellectual property (business models, architecture plans, branding assets) "
|
|
30
|
+
f"using an unvalidated workspace ID parameter (`req.params.id`). The handler lacks session authentication and tenant "
|
|
31
|
+
f"ownership checks, allowing any external caller to exfiltrate confidential startup intellectual property simply by enumerating IDs."
|
|
32
|
+
)
|
|
33
|
+
remediation = (
|
|
34
|
+
"1. Mount `verifyToken` middleware on the dashboard router: `router.use(verifyToken)` in `dashboardRoutes.js`.\n"
|
|
35
|
+
"2. In the route handler, fetch the workspace and verify tenant ownership: `if (workspace.userId !== req.user.id) return res.status(403).json({ error: 'Unauthorized access to workspace' })`.\n"
|
|
36
|
+
"3. Reject unauthenticated requests with HTTP 401 and unauthorized workspace queries with HTTP 403 Forbidden."
|
|
37
|
+
)
|
|
38
|
+
short_action = "Assert req.user ownership of workspaceId before disclosing startup IP."
|
|
39
|
+
return root_cause, remediation, short_action
|
|
40
|
+
|
|
41
|
+
# AI Market Analysis & LLM Resource Exhaustion
|
|
42
|
+
if any(k in ep_lower for k in ("/discovery/analyze", "/discovery/competitive-advantage")):
|
|
43
|
+
root_cause = (
|
|
44
|
+
f"Endpoint `{endpoint}` dispatches directly into the backend AI discovery pipeline and LLM provider without "
|
|
45
|
+
f"caller authentication or request throttling. Malicious or automated callers can spam this endpoint to exhaust "
|
|
46
|
+
f"backend AI API token budgets (denial-of-wallet) or inject adversarial prompt payloads."
|
|
47
|
+
)
|
|
48
|
+
remediation = (
|
|
49
|
+
"1. Guard `{endpoint}` with `verifyToken` authentication middleware.\n"
|
|
50
|
+
"2. Apply rate-limiting middleware (`express-rate-limit`) to restrict users to a reasonable quota (e.g. 10 requests / hour).\n"
|
|
51
|
+
"3. Sanitize incoming prompt inputs (`startupIdea`, `targetAudience`) to mitigate prompt injection attacks."
|
|
52
|
+
)
|
|
53
|
+
short_action = "Apply verifyToken and express-rate-limit to throttle AI market analysis."
|
|
54
|
+
return root_cause, remediation, short_action
|
|
55
|
+
|
|
56
|
+
# Insecure Session State Manipulation / Poisoning
|
|
57
|
+
if any(k in ep_lower for k in ("/discovery/identity", "/discovery/mvp", "/discovery/product-config", "/discovery/tech-preferences", "/discovery/business-model", "/discovery/additional-info")):
|
|
58
|
+
step_name = endpoint.split("/")[-1]
|
|
59
|
+
root_cause = (
|
|
60
|
+
f"The `{step_name}` wizard step on `{endpoint}` takes an unauthenticated client-supplied `sessionId` and updates "
|
|
61
|
+
f"in-flight discovery state via `store.updateSession()`. Because sessions are not bound to verified user credentials, "
|
|
62
|
+
f"an attacker can guess or spoof a `sessionId` to overwrite, poison, or corrupt another founder's startup definitions."
|
|
63
|
+
)
|
|
64
|
+
remediation = (
|
|
65
|
+
"1. When initializing discovery sessions, bind `session.userId = req.user.id` using verified token claims.\n"
|
|
66
|
+
"2. In `{endpoint}`, assert that `session.userId === req.user.id` before calling `store.updateSession()`.\n"
|
|
67
|
+
"3. Use cryptographically unguessable UUIDs for session identifiers and reject unauthorized updates with HTTP 403 Forbidden."
|
|
68
|
+
)
|
|
69
|
+
short_action = "Bind sessionId to req.user.id to prevent cross-session state poisoning."
|
|
70
|
+
return root_cause, remediation, short_action
|
|
71
|
+
|
|
72
|
+
# Unauthorized Workspace Generation
|
|
73
|
+
if "/discovery/generate-workspace" in ep_lower:
|
|
74
|
+
root_cause = (
|
|
75
|
+
f"Endpoint `{endpoint}` aggregates onboarding data and instantiates a permanent startup workspace entity based solely "
|
|
76
|
+
f"on a raw `sessionId` in `req.body`. Anyone who obtains a session ID can trigger final workspace creation without "
|
|
77
|
+
f"caller verification, enabling session hijacking and unauthorized workspace instantiation."
|
|
78
|
+
)
|
|
79
|
+
remediation = (
|
|
80
|
+
"1. Protect `/generate-workspace` with `verifyToken` middleware.\n"
|
|
81
|
+
"2. Verify that `session.userId === req.user.id` before executing `store.generateWorkspace(sessionId)`.\n"
|
|
82
|
+
"3. Invalidate or mark the session as completed to prevent double-generation race conditions."
|
|
83
|
+
)
|
|
84
|
+
short_action = "Authenticate caller and verify session ownership before generating workspace."
|
|
85
|
+
return root_cause, remediation, short_action
|
|
86
|
+
|
|
87
|
+
# Autonomous Agent Pipeline Dispatch
|
|
88
|
+
if "/orchestrator/run" in ep_lower:
|
|
89
|
+
root_cause = (
|
|
90
|
+
f"Endpoint `{endpoint}` triggers the autonomous orchestrator agent pipeline (`orchestratorBridge.runOrchestration`) "
|
|
91
|
+
f"with an unvalidated user prompt. Exposing agent execution without authentication allows attackers to dispatch "
|
|
92
|
+
f"arbitrary commands against any workspace, poison agent context, and consume server compute."
|
|
93
|
+
)
|
|
94
|
+
remediation = (
|
|
95
|
+
"1. Guard `/api/orchestrator/run` with `verifyToken` middleware.\n"
|
|
96
|
+
"2. Verify that `req.user` is an authorized collaborator on the target `workspaceId`.\n"
|
|
97
|
+
"3. Sanitize the input `query` against prompt injection and enforce strict timeout and token budget constraints."
|
|
98
|
+
)
|
|
99
|
+
short_action = "Require verifyToken and sanitize query before dispatching autonomous agent."
|
|
100
|
+
return root_cause, remediation, short_action
|
|
101
|
+
|
|
102
|
+
# Sprint Plan Generation / Overwrite
|
|
103
|
+
if "/orchestrator/generate-plan" in ep_lower:
|
|
104
|
+
root_cause = (
|
|
105
|
+
f"Endpoint `{endpoint}` triggers automated generation of weekly/monthly execution plans for a given `workspaceId`. "
|
|
106
|
+
f"Without authentication or authorization checks, an unauthorized actor can overwrite existing sprint roadmaps, "
|
|
107
|
+
f"milestones, and active deliverables for any startup."
|
|
108
|
+
)
|
|
109
|
+
remediation = (
|
|
110
|
+
"1. Mount `verifyToken` middleware on `/api/orchestrator`.\n"
|
|
111
|
+
"2. Verify that `req.user` has workspace admin/editor permissions before invoking `orchestratorService.generatePlan()`.\n"
|
|
112
|
+
"3. Preserve historical plan versions instead of destructive in-place overwrites."
|
|
136
113
|
)
|
|
137
|
-
|
|
138
|
-
return
|
|
139
|
-
|
|
140
|
-
|
|
114
|
+
short_action = "Verify workspace management permissions before regenerating project plans."
|
|
115
|
+
return root_cause, remediation, short_action
|
|
116
|
+
|
|
117
|
+
# Deliverable & Task Status Tampering
|
|
118
|
+
if "/orchestrator/deliverable" in ep_lower:
|
|
119
|
+
root_cause = (
|
|
120
|
+
f"Endpoint `{endpoint}` permits status modifications (`pending`, `completed`, `rolled-over`) and arbitrary field updates to project deliverables "
|
|
121
|
+
f"using path parameters `planId` and `deliverableId`. The route lacks caller verification, enabling malicious actors to falsify project milestones and corrupt sprint metrics."
|
|
122
|
+
)
|
|
123
|
+
remediation = (
|
|
124
|
+
"1. Require session authentication via `verifyToken`.\n"
|
|
125
|
+
"2. Verify that the plan identified by `req.params.planId` belongs to a workspace where `req.user` is an active member.\n"
|
|
126
|
+
"3. Validate status transitions against allowed state machines and reject unauthorized updates with HTTP 403 Forbidden."
|
|
127
|
+
)
|
|
128
|
+
short_action = "Enforce workspace membership check before patching deliverable status."
|
|
129
|
+
return root_cause, remediation, short_action
|
|
130
|
+
|
|
131
|
+
# Schedule Roll-Forward Manipulation
|
|
132
|
+
if "/orchestrator/rollforward" in ep_lower:
|
|
133
|
+
root_cause = (
|
|
134
|
+
f"Endpoint `{endpoint}` reschedules uncompleted deliverables from past dates to today. Because it lacks access control, "
|
|
135
|
+
f"external actors can trigger roll-forward operations on arbitrary plans, disrupting task deadlines and sprint audit trails."
|
|
136
|
+
)
|
|
137
|
+
remediation = (
|
|
138
|
+
"1. Protect `{endpoint}` with `verifyToken`.\n"
|
|
139
|
+
"2. Validate that `req.user` has editor or admin rights on the workspace owning `planId`.\n"
|
|
140
|
+
"3. Record audit logs for roll-forward triggers with timestamps and caller ID."
|
|
141
|
+
)
|
|
142
|
+
short_action = "Restrict plan roll-forward operations to authenticated team members."
|
|
143
|
+
return root_cause, remediation, short_action
|
|
144
|
+
|
|
145
|
+
# Project Roadmaps & Progress Disclosure
|
|
146
|
+
if any(k in ep_lower for k in ("/orchestrator/plans/", "/orchestrator/active-plan/", "/orchestrator/dashboard/")):
|
|
147
|
+
root_cause = (
|
|
148
|
+
f"Endpoint `{endpoint}` returns sprint plans, active tasks, upcoming deadlines, and progress metrics for the specified "
|
|
149
|
+
f"`workspaceId`. Without authentication or tenant checks, external actors can inspect internal development roadmaps, "
|
|
150
|
+
f"proprietary feature deadlines, and team deliverables."
|
|
151
|
+
)
|
|
152
|
+
remediation = (
|
|
153
|
+
"1. Mount `verifyToken` middleware on the orchestrator router.\n"
|
|
154
|
+
"2. Verify that `req.user` is a verified member of `workspaceId` before calling `planStore` or `orchestratorService`.\n"
|
|
155
|
+
"3. Return HTTP 403 Forbidden if the caller is not affiliated with the workspace."
|
|
156
|
+
)
|
|
157
|
+
short_action = "Verify workspace membership before disclosing plan roadmaps and tasks."
|
|
158
|
+
return root_cause, remediation, short_action
|
|
159
|
+
|
|
160
|
+
# 1. Caller-Controlled Ownership Bypass via Optional Parameter
|
|
161
|
+
if "caller-controlled" in t_lower or "assert_coach_owns_athlete" in ev_text or ("coach_id" in ev_text and "bypass" in t_lower):
|
|
162
|
+
root_cause = (
|
|
163
|
+
f"The multi-tenant authorization assertion on `{endpoint}` is conditionally guarded by a client-supplied "
|
|
164
|
+
f"query parameter (e.g. `coach_id`). Because the parameter is optional (`Optional[int] = Query(None)`), "
|
|
165
|
+
f"an attacker can simply omit `?coach_id=` from the request URL to completely skip the ownership assertion "
|
|
166
|
+
f"and access or modify private domain records belonging to any athlete."
|
|
167
|
+
)
|
|
168
|
+
remediation = (
|
|
169
|
+
"1. Remove `coach_id` from client-supplied query parameters.\n"
|
|
170
|
+
"2. Extract the authenticated caller's identity directly from verified session state or JWT claims "
|
|
171
|
+
"(e.g., `current_user = Depends(get_current_user)`).\n"
|
|
172
|
+
"3. Enforce the tenant/coach ownership check unconditionally using the session ID: "
|
|
173
|
+
"`assert_coach_owns_athlete(current_user.id, user_id)` before querying or mutating the database.\n"
|
|
174
|
+
"4. Return HTTP 403 Forbidden or 404 Not Found if the relationship check fails."
|
|
175
|
+
)
|
|
176
|
+
short_action = "Enforce caller tenant validation from session token; do not accept optional coach_id."
|
|
177
|
+
return root_cause, remediation, short_action
|
|
178
|
+
|
|
179
|
+
# 2. Public StaticFiles Directory Mount
|
|
180
|
+
if "staticfiles" in t_lower or "staticfiles" in ev_text or "/session_videos" in ep_lower:
|
|
181
|
+
root_cause = (
|
|
182
|
+
f"The directory containing stored files is mounted publicly via FastAPI's `StaticFiles` at `{endpoint}` "
|
|
183
|
+
f"without authentication or tenant authorization middleware. This completely circumvents API-level access "
|
|
184
|
+
f"controls, allowing unauthenticated callers to enumerate, stream, and download private user recordings and "
|
|
185
|
+
f"uploaded files directly via static HTTP GET requests."
|
|
186
|
+
)
|
|
187
|
+
remediation = (
|
|
188
|
+
f"1. Remove the public directory mount `app.mount('{endpoint}', StaticFiles(...))`.\n"
|
|
189
|
+
"2. Implement a protected API endpoint (e.g. `GET /api/media/{file_id}`) that validates caller authentication "
|
|
190
|
+
"and verifies that the requested media belongs to the requesting user before streaming.\n"
|
|
191
|
+
"3. Stream files securely using `FileResponse` or `StreamingResponse` after authorization.\n"
|
|
192
|
+
"4. Alternatively, store sensitive files in an S3/GCS bucket and generate short-lived, pre-signed URLs."
|
|
193
|
+
)
|
|
194
|
+
short_action = "Remove public StaticFiles mount; serve files via authenticated FileResponse."
|
|
195
|
+
return root_cause, remediation, short_action
|
|
196
|
+
|
|
197
|
+
# 3. Broken Password Hashing & Plaintext Fallback
|
|
198
|
+
if "password hashing" in t_lower or "plaintext" in ev_text or "verify_password" in t_lower:
|
|
199
|
+
root_cause = (
|
|
200
|
+
"Password verification logic in the authentication module contains a critical plaintext equality fallback "
|
|
201
|
+
"(`if hashed_password == plain_password:`) and/or relies on a single hardcoded global salt with fast, "
|
|
202
|
+
"un-keyed SHA-256 instead of an adaptive work-factor algorithm. This permits unhashed credential logins "
|
|
203
|
+
"and leaves stored hashes susceptible to commodity GPU rainbow table recovery."
|
|
204
|
+
)
|
|
205
|
+
remediation = (
|
|
206
|
+
"1. Immediately delete the plaintext equality comparison `if hashed_password == plain_password:`.\n"
|
|
207
|
+
"2. Migrate password hashing to an adaptive, slow work-factor algorithm such as `bcrypt` or `Argon2id` "
|
|
208
|
+
"(e.g. using `passlib.context.CryptContext(schemes=['bcrypt'])`).\n"
|
|
209
|
+
"3. Ensure the hashing function automatically generates and binds a unique, cryptographically random salt per user record.\n"
|
|
210
|
+
"4. Re-hash legacy passwords transparently upon user login."
|
|
211
|
+
)
|
|
212
|
+
short_action = "Eliminate plaintext check; migrate password hashing to bcrypt/Argon2 with per-user salt."
|
|
213
|
+
return root_cause, remediation, short_action
|
|
214
|
+
|
|
215
|
+
# 4. Unauthenticated Global Session / Data Wipe with Blast Radius
|
|
216
|
+
if "blast radius" in t_lower or ("reset" in ep_lower and ("wipe" in t_lower or "global" in t_lower)):
|
|
217
|
+
root_cause = (
|
|
218
|
+
f"Calling `{endpoint}` without request parameters defaults to iterating across all connected users "
|
|
219
|
+
f"and executes bulk deletion (`shutil.rmtree` and database row deletion) across all user accounts "
|
|
220
|
+
f"without requiring authentication, session scoping, or confirmation. A single unauthenticated HTTP call "
|
|
221
|
+
f"destroys the entire system's session and telemetry data."
|
|
141
222
|
)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
"
|
|
223
|
+
remediation = (
|
|
224
|
+
f"1. Enforce authentication dependency `Depends(get_current_user)` on `{endpoint}`.\n"
|
|
225
|
+
"2. Scope the deletion logic strictly to `user_id == current_user.id` so a caller can only reset their own session.\n"
|
|
226
|
+
"3. If a global multi-tenant reset is required for operations, isolate it to a privileged route protected by "
|
|
227
|
+
"`Depends(require_admin)` and require multi-factor or secondary token confirmation."
|
|
146
228
|
)
|
|
229
|
+
short_action = "Scope deletion strictly to current_user.id; restrict global wipes to require_admin."
|
|
230
|
+
return root_cause, remediation, short_action
|
|
147
231
|
|
|
148
|
-
#
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
232
|
+
# 5. Privilege Escalation via Unvalidated Role Assignment
|
|
233
|
+
if "role assignment" in t_lower or ("role" in ev_text and "privilege" in t_lower):
|
|
234
|
+
root_cause = (
|
|
235
|
+
f"The user registration/creation handler on `{endpoint}` binds a client-supplied `role` string directly "
|
|
236
|
+
f"into the new user record without server-side validation or administrative authorization checks. "
|
|
237
|
+
f"An unauthenticated attacker can submit `role='ADMIN'` to instantly create a persistent superuser account."
|
|
238
|
+
)
|
|
239
|
+
remediation = (
|
|
240
|
+
"1. Remove `role` and administrative privilege attributes from public user creation schemas.\n"
|
|
241
|
+
"2. Restrict administrative user creation (`/api/admin/users/create`) with `Depends(require_admin)`.\n"
|
|
242
|
+
"3. Enforce a strict server-side Enum allowlist (e.g. `Role.USER`, `Role.ATHLETE`) and default all new accounts "
|
|
243
|
+
"to unprivileged roles regardless of client payload."
|
|
244
|
+
)
|
|
245
|
+
short_action = "Protect with require_admin; enforce server-side role allowlist on user creation."
|
|
246
|
+
return root_cause, remediation, short_action
|
|
247
|
+
|
|
248
|
+
# 6. Account Takeover via Unauthenticated Password Reset
|
|
249
|
+
if "reset" in ep_lower and "password" in ep_lower:
|
|
250
|
+
root_cause = (
|
|
251
|
+
f"The password reset endpoint `{endpoint}` modifies user credentials without requiring current password "
|
|
252
|
+
f"validation, an authenticated session, or an out-of-band cryptographically signed reset token. An attacker "
|
|
253
|
+
f"can overwrite the password of any user account by targeting their sequential ID or username."
|
|
254
|
+
)
|
|
255
|
+
remediation = (
|
|
256
|
+
"1. Implement an out-of-band reset flow using time-limited, single-use cryptographically random tokens "
|
|
257
|
+
"delivered to the verified user email address.\n"
|
|
258
|
+
"2. For self-service password changes by logged-in users, require current password verification.\n"
|
|
259
|
+
"3. For administrative password resets, guard the route with `Depends(require_admin)`."
|
|
260
|
+
)
|
|
261
|
+
short_action = "Require verified single-use reset token or require_admin check for password resets."
|
|
262
|
+
return root_cause, remediation, short_action
|
|
263
|
+
|
|
264
|
+
# 7. Insecure CORS Configuration
|
|
265
|
+
if cat_upper == "CORS" or "cors" in t_lower:
|
|
266
|
+
if "localhost" in ev_text or "3000" in ev_text or "backend/server.js" in filepath:
|
|
267
|
+
root_cause = (
|
|
268
|
+
f"CORS configuration on `{endpoint}` uses hardcoded `origin: 'http://localhost:3000'`. "
|
|
269
|
+
f"Static development origin definitions in server code leave production deployments vulnerable "
|
|
270
|
+
f"to misconfiguration or unintended cross-origin access from local debugging ports."
|
|
271
|
+
)
|
|
272
|
+
remediation = (
|
|
273
|
+
"1. Load allowed origins dynamically from environment variables: "
|
|
274
|
+
"`const allowedOrigins = (process.env.ALLOWED_ORIGINS || 'http://localhost:3000').split(','); app.use(cors({ origin: allowedOrigins }));`.\n"
|
|
275
|
+
"2. Ensure credentials (`credentials: true`) are never combined with wildcard origins.\n"
|
|
276
|
+
"3. Reject cross-origin requests from unlisted origin headers."
|
|
277
|
+
)
|
|
278
|
+
short_action = "Configure CORS origins dynamically via environment variable rather than hardcoding localhost."
|
|
279
|
+
return root_cause, remediation, short_action
|
|
280
|
+
|
|
281
|
+
root_cause = (
|
|
282
|
+
f"`CORSMiddleware` on `{endpoint}` is configured with `allow_origins=['*']` combined with "
|
|
283
|
+
f"`allow_credentials=True`. This permits arbitrary third-party websites to execute credentialed "
|
|
284
|
+
f"cross-origin requests against the backend, enabling CSRF-style data exfiltration and session riding."
|
|
285
|
+
)
|
|
286
|
+
remediation = (
|
|
287
|
+
"1. Remove wildcard `allow_origins=['*']`.\n"
|
|
288
|
+
"2. Configure an explicit array of authorized frontend domains (e.g. `allow_origins=['https://app.example.com']`).\n"
|
|
289
|
+
"3. Never dynamically reflect incoming `Origin` headers into `Access-Control-Allow-Origin` when credentials are enabled."
|
|
290
|
+
)
|
|
291
|
+
short_action = "Remove wildcard allow_origins; define explicit list of authorized frontend domains."
|
|
292
|
+
return root_cause, remediation, short_action
|
|
293
|
+
|
|
294
|
+
# 8. Unauthenticated WebSockets
|
|
295
|
+
if "websocket" in t_lower or "websocket" in ev_text:
|
|
296
|
+
root_cause = (
|
|
297
|
+
f"The WebSocket route `{endpoint}` accepts incoming streaming connections and dispatches real-time telemetry "
|
|
298
|
+
f"without authenticating the client during the handshake, allowing unauthorized eavesdropping or malicious event injection."
|
|
299
|
+
)
|
|
300
|
+
remediation = (
|
|
301
|
+
"1. Extract and validate authentication credentials (JWT ticket or session cookie) during the WebSocket connection handshake.\n"
|
|
302
|
+
"2. Reject unauthenticated handshakes before calling `await websocket.accept()` using WebSocket close code 1008 (Policy Violation).\n"
|
|
303
|
+
"3. Associate the open WebSocket connection strictly with the validated user ID."
|
|
304
|
+
)
|
|
305
|
+
short_action = "Validate authentication token during handshake before calling websocket.accept()."
|
|
306
|
+
return root_cause, remediation, short_action
|
|
307
|
+
|
|
308
|
+
# 9. Stateless ML / Inference Resource Exhaustion
|
|
309
|
+
if "scatt-analysis" in ep_lower or "inference" in t_lower or "stateless" in t_lower:
|
|
310
|
+
root_cause = (
|
|
311
|
+
f"The compute-intensive inference endpoint `{endpoint}` is exposed to the public internet without authentication "
|
|
312
|
+
f"or request rate-limiting, allowing unauthorized callers to trigger CPU/GPU exhaustion and denial-of-wallet."
|
|
313
|
+
)
|
|
314
|
+
remediation = (
|
|
315
|
+
"1. Add API key or session token verification to authenticate callers.\n"
|
|
316
|
+
"2. Implement rate-limiting middleware (e.g. `slowapi` or Redis token bucket) to throttle request frequency per client IP.\n"
|
|
317
|
+
"3. Enforce maximum payload size limits on input telemetry arrays."
|
|
318
|
+
)
|
|
319
|
+
short_action = "Add API key authentication and rate-limiting middleware (slowapi) to throttle inference."
|
|
320
|
+
return root_cause, remediation, short_action
|
|
321
|
+
|
|
322
|
+
# 10. Unauthenticated User Information Disclosure
|
|
323
|
+
if "user information disclosure" in t_lower or ("user" in ep_lower and cat_upper == "AUTHENTICATION" and "get" in ep_lower):
|
|
324
|
+
root_cause = (
|
|
325
|
+
f"Read-only endpoint `{endpoint}` discloses user profile records (ID, username, role) to unauthenticated callers, "
|
|
326
|
+
f"permitting automated enumeration of all registered users in the application."
|
|
327
|
+
)
|
|
328
|
+
remediation = (
|
|
329
|
+
f"1. Protect `{endpoint}` with `Depends(get_current_user)` or require valid session authentication.\n"
|
|
330
|
+
"2. If public user profiles are intended by design, redact internal database IDs, role assignments, and private metadata."
|
|
331
|
+
)
|
|
332
|
+
short_action = "Protect user lookup with Depends(get_current_user) to prevent user enumeration."
|
|
333
|
+
return root_cause, remediation, short_action
|
|
334
|
+
|
|
335
|
+
fp_lower = filepath.lower()
|
|
336
|
+
is_node = fp_lower.endswith((".js", ".ts", ".jsx", ".tsx", ".mjs"))
|
|
337
|
+
|
|
338
|
+
# 11. General BOLA / IDOR
|
|
339
|
+
if cat_upper == "BOLA" or "bola" in t_lower:
|
|
340
|
+
root_cause = (
|
|
341
|
+
f"The handler for `{endpoint}` looks up database records using a client-supplied object ID from the URL or query "
|
|
342
|
+
f"without verifying that the requested record belongs to the authenticated user's tenant or organization. "
|
|
343
|
+
f"An attacker can increment or replace the ID to view or modify records belonging to other tenants."
|
|
344
|
+
)
|
|
345
|
+
if is_node:
|
|
346
|
+
remediation = (
|
|
347
|
+
"1. Extract authenticated caller ID from session state (`req.user.id` or `req.user.tenantId`).\n"
|
|
348
|
+
"2. Scope database queries to verify `req.user.tenantId == record.tenantId` before returning data.\n"
|
|
349
|
+
"3. Return HTTP 403 Forbidden or 404 Not Found if the resource is not owned by the caller."
|
|
350
|
+
)
|
|
351
|
+
short_action = "Scope database query to req.user.tenantId to enforce object-level isolation."
|
|
352
|
+
else:
|
|
353
|
+
remediation = (
|
|
354
|
+
"1. Extract authenticated caller ID from the verified session context (e.g. `current_user = Depends(get_current_user)`).\n"
|
|
355
|
+
"2. Scope database queries to both the object ID and the user's tenant: "
|
|
356
|
+
"`db.query(Model).filter(Model.id == obj_id, Model.tenant_id == current_user.tenant_id).first()`.\n"
|
|
357
|
+
"3. Return HTTP 403 Forbidden or 404 Not Found if the resource is not owned by the caller."
|
|
358
|
+
)
|
|
359
|
+
short_action = "Scope database query to authenticated current_user.tenant_id to enforce isolation."
|
|
360
|
+
return root_cause, remediation, short_action
|
|
361
|
+
|
|
362
|
+
# 12. General BFLA / Admin RBAC Missing
|
|
363
|
+
if cat_upper == "BFLA" or "admin" in ep_lower or "bfla" in t_lower:
|
|
364
|
+
root_cause = (
|
|
365
|
+
f"Privileged administrative route `{endpoint}` is accessible without verifying that the caller possesses "
|
|
366
|
+
f"administrative role permissions. Any standard user or unauthenticated client can trigger administrative actions."
|
|
367
|
+
)
|
|
368
|
+
if is_node:
|
|
369
|
+
remediation = (
|
|
370
|
+
"1. Mount role-verification middleware (`requireRole('admin')`) on the route.\n"
|
|
371
|
+
"2. Verify `req.user && req.user.role === 'admin'` before executing privileged operations."
|
|
372
|
+
)
|
|
373
|
+
short_action = "Mount requireRole('admin') middleware on privileged administrative route."
|
|
374
|
+
else:
|
|
375
|
+
remediation = (
|
|
376
|
+
"1. Implement declarative role-based access control (RBAC) dependencies (e.g. `Security(require_role('admin'))`).\n"
|
|
377
|
+
"2. Verify that `current_user.role == 'ADMIN'` on the backend before executing privileged state changes."
|
|
378
|
+
)
|
|
379
|
+
short_action = "Implement declarative RBAC dependency (require_admin) on privileged administrative route."
|
|
380
|
+
return root_cause, remediation, short_action
|
|
381
|
+
|
|
382
|
+
# 13. General Authentication Missing on Sensitive Route
|
|
383
|
+
if cat_upper == "AUTHENTICATION":
|
|
384
|
+
root_cause = (
|
|
385
|
+
f"Sensitive or state-modifying endpoint `{endpoint}` does not enforce authentication. Unauthorized callers "
|
|
386
|
+
f"can trigger operations or retrieve private data without providing credentials or session tokens."
|
|
387
|
+
)
|
|
388
|
+
if is_node:
|
|
389
|
+
remediation = (
|
|
390
|
+
f"1. Mount authentication middleware `verifyToken` or `router.use(authMiddleware)` on `{endpoint}`.\n"
|
|
391
|
+
"2. Reject unauthenticated requests with HTTP 401 Unauthorized."
|
|
392
|
+
)
|
|
393
|
+
short_action = "Mount verifyToken middleware on Express route."
|
|
394
|
+
else:
|
|
395
|
+
remediation = (
|
|
396
|
+
f"1. Mount authentication dependency `Depends(get_current_user)` or security middleware on `{endpoint}`.\n"
|
|
397
|
+
"2. Reject unauthenticated requests with HTTP 401 Unauthorized."
|
|
398
|
+
)
|
|
399
|
+
short_action = "Protect route with Depends(get_current_user) to block unauthenticated callers."
|
|
400
|
+
return root_cause, remediation, short_action
|
|
401
|
+
|
|
402
|
+
# 14. Path Traversal
|
|
403
|
+
if cat_upper == "PATH_TRAVERSAL":
|
|
404
|
+
root_cause = (
|
|
405
|
+
f"Endpoint `{endpoint}` accepts file or path parameters without verifying that the resolved path resides "
|
|
406
|
+
f"strictly within the intended base directory. Attackers can use path traversal sequences (`..`) to access arbitrary system files."
|
|
407
|
+
)
|
|
408
|
+
remediation = (
|
|
409
|
+
"1. Sanitize file path inputs using `os.path.basename()` or `pathlib.Path.resolve()`.\n"
|
|
410
|
+
"2. Verify that `resolved_path.is_relative_to(base_dir)` before opening or reading files."
|
|
411
|
+
)
|
|
412
|
+
short_action = "Sanitize file input with basename() and verify resolved directory containment."
|
|
413
|
+
return root_cause, remediation, short_action
|
|
414
|
+
|
|
415
|
+
# 15. SSRF
|
|
416
|
+
if cat_upper == "SSRF":
|
|
417
|
+
root_cause = (
|
|
418
|
+
f"Endpoint `{endpoint}` accepts a user-controlled destination URL and dispatches outbound HTTP requests without "
|
|
419
|
+
f"validating the host against an allowlist, allowing attackers to probe internal cloud metadata and RFC-1918 networks."
|
|
420
|
+
)
|
|
421
|
+
remediation = (
|
|
422
|
+
"1. Validate target URLs against a strict allowlist of authorized hostnames.\n"
|
|
423
|
+
"2. Resolve DNS names and reject loopback (`127.0.0.1`, `localhost`) and private RFC-1918 IP addresses before dispatching HTTP calls."
|
|
424
|
+
)
|
|
425
|
+
short_action = "Enforce domain allowlist and block loopback/private RFC-1918 IPs on outbound HTTP."
|
|
426
|
+
return root_cause, remediation, short_action
|
|
427
|
+
|
|
428
|
+
# 16. SQL / NoSQL Injection
|
|
429
|
+
if cat_upper == "INJECTION":
|
|
430
|
+
root_cause = (
|
|
431
|
+
f"Endpoint `{endpoint}` routes user-supplied query parameters to the database layer without verified parameterization, "
|
|
432
|
+
f"permitting SQL syntax injection and unauthorized database manipulation."
|
|
433
|
+
)
|
|
434
|
+
remediation = (
|
|
435
|
+
"1. Use parameterized queries, prepared statements, or ORM abstraction methods for all database operations.\n"
|
|
436
|
+
"2. Never concatenate or format raw client inputs into SQL query strings."
|
|
437
|
+
)
|
|
438
|
+
short_action = "Use parameterized SQL queries or ORM models; never concatenate raw user input."
|
|
439
|
+
return root_cause, remediation, short_action
|
|
440
|
+
|
|
441
|
+
# Default fallback
|
|
442
|
+
root_cause = f"Security vulnerability of category `{category}` identified in `{endpoint}`."
|
|
443
|
+
remediation = "Apply strict authentication, tenant authorization boundaries, and input validation to this attack path."
|
|
444
|
+
short_action = f"Apply strict access control and input validation on {category} attack path."
|
|
445
|
+
return root_cause, remediation, short_action
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def get_remediation_for_category(category: str, filepath: str = "", framework: str = "") -> str:
|
|
449
|
+
"""Backwards-compatible remediation accessor."""
|
|
450
|
+
_, rem, _ = get_detailed_remediation_and_root_cause(
|
|
451
|
+
title=category,
|
|
452
|
+
category=category,
|
|
453
|
+
endpoint="endpoint",
|
|
454
|
+
filepath=filepath,
|
|
152
455
|
)
|
|
456
|
+
return rem
|
|
@@ -14,12 +14,24 @@ class FindingStore:
|
|
|
14
14
|
self.store_file = trace_dir / "findings.json"
|
|
15
15
|
|
|
16
16
|
def save_findings(self, findings: List[Finding]) -> None:
|
|
17
|
-
"""Save a list of findings to JSON."""
|
|
17
|
+
"""Save a list of findings to JSON and generate markdown reports."""
|
|
18
18
|
self.trace_dir.mkdir(parents=True, exist_ok=True)
|
|
19
19
|
data = [f.model_dump() for f in findings]
|
|
20
20
|
with open(self.store_file, "w", encoding="utf-8") as f:
|
|
21
21
|
json.dump(data, f, indent=2)
|
|
22
22
|
|
|
23
|
+
try:
|
|
24
|
+
from trace_engine.output.markdown import generate_markdown_report
|
|
25
|
+
project_name = self.trace_dir.parent.name if self.trace_dir.parent else "Application"
|
|
26
|
+
md_content = generate_markdown_report(findings, project_name=project_name)
|
|
27
|
+
(self.trace_dir / "findings.md").write_text(md_content, encoding="utf-8")
|
|
28
|
+
|
|
29
|
+
workspace_root = self.trace_dir.parent
|
|
30
|
+
if workspace_root and workspace_root.exists() and workspace_root != self.trace_dir:
|
|
31
|
+
(workspace_root / "findings.md").write_text(md_content, encoding="utf-8")
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
|
|
23
35
|
def load_findings(self) -> List[Finding]:
|
|
24
36
|
"""Load findings from JSON file."""
|
|
25
37
|
if not self.store_file.exists():
|
|
@@ -158,7 +158,11 @@ class ExpressFrameworkAdapter(FrameworkAdapter):
|
|
|
158
158
|
content_lower = content.lower()
|
|
159
159
|
has_db_sink = any(
|
|
160
160
|
term in content_lower
|
|
161
|
-
for term in (
|
|
161
|
+
for term in (
|
|
162
|
+
"mongoose", "prisma", "sequelize", "knex", "pool.query", "db.query", "db.collection",
|
|
163
|
+
"supabase", "store.", "planstore", "discoverystore", "findone", "find(", "findby",
|
|
164
|
+
"getworkspace", "getplan", "getsession", "model."
|
|
165
|
+
)
|
|
162
166
|
)
|
|
163
167
|
has_ext_sink = any(
|
|
164
168
|
term in content_lower
|
|
@@ -624,53 +624,57 @@ def run_interactive_story() -> None:
|
|
|
624
624
|
)
|
|
625
625
|
|
|
626
626
|
# =========================================================================
|
|
627
|
-
# STEP 7:
|
|
627
|
+
# STEP 7: AI Coding Agent Handoff & Export Options
|
|
628
628
|
# =========================================================================
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
with console.status(f" [{GREEN}]Autonomous Harness:[/{GREEN}] [{WHITE}]Refactoring AST nodes & running verification...[/{WHITE}]", spinner="dots"):
|
|
645
|
-
harness = AgentHarness(project_dir, target_url=target_url)
|
|
646
|
-
report = harness.run_self_healing_loop()
|
|
647
|
-
print_harness_report(report)
|
|
648
|
-
|
|
649
|
-
# =========================================================================
|
|
650
|
-
# STEP 8: Post-Audit Export
|
|
651
|
-
# =========================================================================
|
|
652
|
-
console.print(f"\n [{BOLD_WHITE}]Export Options:[/{BOLD_WHITE}]")
|
|
653
|
-
console.print(f" [{ORANGE}][1][/{ORANGE}] [{BOLD_WHITE}]Export OASIS SARIF v2.1.0[/{BOLD_WHITE}] [{DIM_WHITE}](GitHub Code Scanning / CI Integration)[/{DIM_WHITE}]")
|
|
654
|
-
console.print(f" [{ORANGE}][2][/{ORANGE}] [{BOLD_WHITE}]Export Markdown Report[/{BOLD_WHITE}] [{DIM_WHITE}](Executive summary document)[/{DIM_WHITE}]")
|
|
655
|
-
console.print(f" [{ORANGE}][3][/{ORANGE}] [{BOLD_WHITE}]Finish Session[/{BOLD_WHITE}]\n")
|
|
656
|
-
|
|
657
|
-
post_choice = ask_input(
|
|
658
|
-
f" [{ORANGE}]›[/{ORANGE}] [{BOLD_WHITE}]Select export option[/{BOLD_WHITE}]",
|
|
659
|
-
choices=["1", "2", "3"],
|
|
660
|
-
default="1",
|
|
629
|
+
console.print(f"\n [{DIM_GREEN}]{'─' * 76}[/{DIM_GREEN}]")
|
|
630
|
+
console.print(f" [{GREEN}]◆ TRACE AUDIT COMPLETE — AI AGENT & REMEDIATION HANDOFF:[/{GREEN}]")
|
|
631
|
+
console.print(f" [{WHITE}]TRACE has generated precise root causes and concrete remediation plans for all {len(findings)} findings.[/{WHITE}]")
|
|
632
|
+
console.print(f" [{DIM_WHITE}]Executive report automatically saved to:[/{DIM_WHITE}] [{BOLD_WHITE}]{project_dir / 'findings.md'}[/{BOLD_WHITE}]\n")
|
|
633
|
+
|
|
634
|
+
console.print(f" [{BOLD_WHITE}]Next Action / Integration Options:[/{BOLD_WHITE}]")
|
|
635
|
+
console.print(f" [{ORANGE}][1][/{ORANGE}] [{BOLD_WHITE}]Inspect Detailed Root Cause & Exploit Mechanics in Terminal[/{BOLD_WHITE}]")
|
|
636
|
+
console.print(f" [{ORANGE}][2][/{ORANGE}] [{BOLD_WHITE}]Launch TRACE MCP Server[/{BOLD_WHITE}] [{DIM_WHITE}](Connect Claude Code / Cursor as skill)[/{DIM_WHITE}]")
|
|
637
|
+
console.print(f" [{ORANGE}][3][/{ORANGE}] [{BOLD_WHITE}]Export OASIS SARIF v2.1.0[/{BOLD_WHITE}] [{DIM_WHITE}](GitHub Code Scanning / CI)[/{DIM_WHITE}]")
|
|
638
|
+
console.print(f" [{ORANGE}][4][/{ORANGE}] [{BOLD_WHITE}]Finish Session[/{BOLD_WHITE}]\n")
|
|
639
|
+
|
|
640
|
+
handoff_choice = ask_input(
|
|
641
|
+
f" [{ORANGE}]›[/{ORANGE}] [{BOLD_WHITE}]Select action[/{BOLD_WHITE}]",
|
|
642
|
+
choices=["1", "2", "3", "4"],
|
|
643
|
+
default="4",
|
|
661
644
|
show_default=True,
|
|
662
645
|
)
|
|
663
646
|
|
|
664
|
-
if
|
|
647
|
+
if handoff_choice == "1":
|
|
648
|
+
while True:
|
|
649
|
+
console.print(f"\n [{BOLD_WHITE}]Available Findings to Inspect:[/{BOLD_WHITE}]")
|
|
650
|
+
for f in findings:
|
|
651
|
+
action_text = f.short_action or f.title
|
|
652
|
+
console.print(f" [{GREEN}]• {f.id:<14}[/{GREEN}] [{WHITE}]{f.endpoint:<28}[/{WHITE}] [{DIM_WHITE}]{action_text}[/{DIM_WHITE}]")
|
|
653
|
+
f_id = ask_input(f"\n [{ORANGE}]›[/{ORANGE}] [{BOLD_WHITE}]Enter Finding ID to inspect (or 'back')[/{BOLD_WHITE}]", default="back")
|
|
654
|
+
if f_id.lower() in ("back", "exit", "q"):
|
|
655
|
+
break
|
|
656
|
+
matched = next((f for f in findings if f.id.lower() == f_id.lower()), None)
|
|
657
|
+
if matched:
|
|
658
|
+
console.print()
|
|
659
|
+
print_finding_detail(matched)
|
|
660
|
+
else:
|
|
661
|
+
console.print(f" [{ORANGE}]Finding '{f_id}' not found.[/{ORANGE}]")
|
|
662
|
+
|
|
663
|
+
elif handoff_choice == "2":
|
|
664
|
+
console.print(f"\n [{GREEN}]Starting TRACE MCP Server on port 8765...[/{GREEN}]")
|
|
665
|
+
console.print(f" [{DIM_WHITE}]Connect Claude Code or Cursor to: [bold white]http://127.0.0.1:8765/sse[/bold white][/{DIM_WHITE}]")
|
|
666
|
+
console.print(f" [{DIM_WHITE}]Press Ctrl+C to stop the MCP server when finished.[/{DIM_WHITE}]\n")
|
|
667
|
+
from trace_engine.mcp.server import run_server
|
|
668
|
+
try:
|
|
669
|
+
run_server(host="127.0.0.1", port=8765)
|
|
670
|
+
except KeyboardInterrupt:
|
|
671
|
+
console.print(f"\n [{GREEN}]✓ TRACE MCP Server stopped.[/{GREEN}]")
|
|
672
|
+
|
|
673
|
+
elif handoff_choice == "3":
|
|
665
674
|
sarif_file = project_dir / "trace-results.sarif"
|
|
666
675
|
sarif_data = generate_sarif_report(findings, project_name=project_dir.name, workspace_root=str(project_dir))
|
|
667
676
|
import json
|
|
668
677
|
sarif_file.write_text(json.dumps(sarif_data, indent=2), encoding="utf-8")
|
|
669
678
|
console.print(f" [{GREEN}]✓ Exported SARIF v2.1.0:[/{GREEN}] [{WHITE}]{sarif_file}[/{WHITE}]\n")
|
|
670
|
-
elif post_choice == "2":
|
|
671
|
-
md_file = project_dir / "TRACE_SECURITY_REPORT.md"
|
|
672
|
-
md_content = generate_markdown_report(findings, project_name=project_dir.name)
|
|
673
|
-
md_file.write_text(md_content, encoding="utf-8")
|
|
674
|
-
console.print(f" [{GREEN}]✓ Exported Markdown Report:[/{GREEN}] [{WHITE}]{md_file}[/{WHITE}]\n")
|
|
675
679
|
|
|
676
680
|
console.print(f" [{GREEN}]✓ TRACE Audit Session Complete. System Verified.[/{GREEN}]\n")
|
|
@@ -218,6 +218,8 @@ class TraceMCPServer:
|
|
|
218
218
|
"severity": f.severity.value,
|
|
219
219
|
"confidence": f.confidence.value,
|
|
220
220
|
"endpoint": f.endpoint,
|
|
221
|
+
"root_cause": f.root_cause,
|
|
222
|
+
"short_action": f.short_action,
|
|
221
223
|
"remediation": f.remediation,
|
|
222
224
|
"source": str(f.source_location) if f.source_location else None,
|
|
223
225
|
}
|
|
@@ -239,6 +241,8 @@ class TraceMCPServer:
|
|
|
239
241
|
"endpoint": finding.endpoint,
|
|
240
242
|
"source": str(finding.source_location),
|
|
241
243
|
"attack_path": finding.attack_path,
|
|
244
|
+
"root_cause": finding.root_cause,
|
|
245
|
+
"short_action": finding.short_action,
|
|
242
246
|
"static_evidence": finding.static_evidence,
|
|
243
247
|
"runtime_evidence": finding.runtime_evidence,
|
|
244
248
|
"correlation_notes": finding.correlation_notes,
|
|
@@ -10,16 +10,18 @@ def generate_markdown_report(findings: List[Finding], project_name: str = "Appli
|
|
|
10
10
|
f"# TRACE Security Assessment Report — {project_name}",
|
|
11
11
|
"Threat Reconnaissance & Attack-path Correlation Engine",
|
|
12
12
|
"",
|
|
13
|
-
"## Summary",
|
|
14
|
-
f"-
|
|
13
|
+
"## Executive Summary",
|
|
14
|
+
f"- **Target Application:** `{project_name}`",
|
|
15
|
+
f"- **Total Correlated Findings:** **{len(findings)}**",
|
|
15
16
|
"",
|
|
16
|
-
"| ID | Severity | Confidence | Category | Endpoint |
|
|
17
|
+
"| ID | Severity | Confidence | Category | Endpoint | Remediation Action |",
|
|
17
18
|
"|---|---|---|---|---|---|",
|
|
18
19
|
]
|
|
19
20
|
|
|
20
21
|
for f in findings:
|
|
22
|
+
action_summary = f.short_action or f.title
|
|
21
23
|
lines.append(
|
|
22
|
-
f"| {f.id} | {f.severity.value} | {f.confidence.value} | {f.category} | `{f.endpoint}` | {
|
|
24
|
+
f"| `{f.id}` | **{f.severity.value}** | {f.confidence.value} | {f.category} | `{f.endpoint}` | {action_summary} |"
|
|
23
25
|
)
|
|
24
26
|
|
|
25
27
|
lines.extend(["", "---", "", "## Finding Details", ""])
|
|
@@ -33,9 +35,13 @@ def generate_markdown_report(findings: List[Finding], project_name: str = "Appli
|
|
|
33
35
|
if f.source_location:
|
|
34
36
|
lines.append(f"- **Source:** `{f.source_location}`")
|
|
35
37
|
lines.append("")
|
|
36
|
-
|
|
38
|
+
|
|
39
|
+
if f.root_cause:
|
|
40
|
+
lines.append("#### Root Cause & Exploit Mechanics")
|
|
41
|
+
lines.append(f"{f.root_cause}\n")
|
|
42
|
+
|
|
37
43
|
if f.attack_path:
|
|
38
|
-
lines.append("#### Attack Path Hops")
|
|
44
|
+
lines.append("#### Attack Path Hops (APM Traversal)")
|
|
39
45
|
for hop in f.attack_path:
|
|
40
46
|
lines.append(f"1. `{hop}`")
|
|
41
47
|
lines.append("")
|
|
@@ -58,7 +64,8 @@ def generate_markdown_report(findings: List[Finding], project_name: str = "Appli
|
|
|
58
64
|
lines.append(f"{idx}. {step}")
|
|
59
65
|
lines.append("")
|
|
60
66
|
|
|
61
|
-
lines.append(
|
|
67
|
+
lines.append("#### Concrete Remediation Plan")
|
|
68
|
+
lines.append(f"{f.remediation}\n")
|
|
62
69
|
lines.append("---")
|
|
63
70
|
lines.append("")
|
|
64
71
|
|
|
@@ -220,6 +220,10 @@ def print_finding_detail(f: Finding) -> None:
|
|
|
220
220
|
prefix = " +- "
|
|
221
221
|
content.append(f"{prefix}{hop}\n", style="dim white")
|
|
222
222
|
|
|
223
|
+
if f.root_cause:
|
|
224
|
+
content.append("\nRoot Cause & Exploit Mechanics:\n", style="bold green")
|
|
225
|
+
content.append(f" {f.root_cause}\n", style="bright_white")
|
|
226
|
+
|
|
223
227
|
if f.static_evidence:
|
|
224
228
|
content.append("\nStatic Code Evidence:\n", style="bold green")
|
|
225
229
|
for ev in f.static_evidence:
|
|
@@ -235,8 +239,10 @@ def print_finding_detail(f: Finding) -> None:
|
|
|
235
239
|
for idx, step in enumerate(f.reproduction_steps, 1):
|
|
236
240
|
content.append(f" {idx}. {step}\n", style="white")
|
|
237
241
|
|
|
238
|
-
content.append("\
|
|
239
|
-
|
|
242
|
+
content.append("\nConcrete Remediation Action Plan:\n", style="bold green")
|
|
243
|
+
for line in f.remediation.split("\n"):
|
|
244
|
+
if line.strip():
|
|
245
|
+
content.append(f" {line}\n", style="white")
|
|
240
246
|
|
|
241
247
|
console.print(
|
|
242
248
|
Panel(
|
|
@@ -351,7 +357,7 @@ def print_test_all_report(
|
|
|
351
357
|
for idx, f in enumerate(sorted_findings, 1):
|
|
352
358
|
prio = priority_labels.get(f.severity, str(f.severity.value))
|
|
353
359
|
loc_str = str(f.source_location) if f.source_location else "-"
|
|
354
|
-
remediation_snippet = f.remediation.strip().split(".")[0] + "." if f.remediation else "Apply strict authorization check."
|
|
360
|
+
remediation_snippet = f.short_action or (f.remediation.strip().split(".")[0] + "." if f.remediation else "Apply strict authorization check.")
|
|
355
361
|
|
|
356
362
|
table.add_row(
|
|
357
363
|
f"#{idx}",
|
|
@@ -267,6 +267,69 @@ class HypothesisEngine:
|
|
|
267
267
|
)
|
|
268
268
|
)
|
|
269
269
|
hypo_idx += 1
|
|
270
|
+
elif any(term in ep.path.lower() for term in ("analyze", "competitive-advantage", "generate-workspace", "generate-plan", "orchestrator/run")):
|
|
271
|
+
# Case: Unauthenticated AI Agent / LLM Inference Pipeline
|
|
272
|
+
hypotheses.append(
|
|
273
|
+
SecurityHypothesis(
|
|
274
|
+
id=f"HYP-AUTH-{hypo_idx:03d}",
|
|
275
|
+
category=VulnerabilityCategory.AUTHENTICATION,
|
|
276
|
+
endpoint_id=ep.id,
|
|
277
|
+
endpoint_display=ep_disp,
|
|
278
|
+
title=f"Unauthenticated AI Agent Pipeline / Token Depletion on {ep_disp}",
|
|
279
|
+
description="Endpoint invokes expensive backend AI agent or LLM pipelines without authentication or rate-limiting, exposing the application to token exhaustion and prompt injection.",
|
|
280
|
+
recommended_test_pack="authentication",
|
|
281
|
+
confidence_prior=0.85,
|
|
282
|
+
static_evidence=[
|
|
283
|
+
f"AI Agent / LLM reasoning endpoint: {ep.method} {ep.path}",
|
|
284
|
+
"No authentication or caller identity verification detected",
|
|
285
|
+
"Risk: Denial-of-wallet, unbounded LLM token consumption, prompt injection",
|
|
286
|
+
],
|
|
287
|
+
severity=Severity.HIGH,
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
hypo_idx += 1
|
|
291
|
+
elif any(term in ep.path.lower() for term in ("identity", "mvp", "product-config", "tech-preferences", "business-model", "additional-info")):
|
|
292
|
+
# Case: Multi-step wizard session state mutation
|
|
293
|
+
hypotheses.append(
|
|
294
|
+
SecurityHypothesis(
|
|
295
|
+
id=f"HYP-AUTH-{hypo_idx:03d}",
|
|
296
|
+
category=VulnerabilityCategory.AUTHENTICATION,
|
|
297
|
+
endpoint_id=ep.id,
|
|
298
|
+
endpoint_display=ep_disp,
|
|
299
|
+
title=f"Insecure Discovery Session State Mutation on {ep_disp}",
|
|
300
|
+
description="Endpoint updates onboarding discovery session data using client-supplied sessionId without session tenant verification, enabling session hijacking and state poisoning.",
|
|
301
|
+
recommended_test_pack="authentication",
|
|
302
|
+
confidence_prior=0.80,
|
|
303
|
+
static_evidence=[
|
|
304
|
+
f"Discovery session state modification: {ep.method} {ep.path}",
|
|
305
|
+
"Client-controlled sessionId parameter without verified caller ownership",
|
|
306
|
+
"Risk: Cross-session data corruption and poisoning of startup configuration",
|
|
307
|
+
],
|
|
308
|
+
severity=Severity.HIGH,
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
hypo_idx += 1
|
|
312
|
+
elif any(term in ep.path.lower() for term in ("deliverable", "rollforward")):
|
|
313
|
+
# Case: Unauthorized task status / sprint roadmap mutation
|
|
314
|
+
hypotheses.append(
|
|
315
|
+
SecurityHypothesis(
|
|
316
|
+
id=f"HYP-AUTH-{hypo_idx:03d}",
|
|
317
|
+
category=VulnerabilityCategory.AUTHENTICATION,
|
|
318
|
+
endpoint_id=ep.id,
|
|
319
|
+
endpoint_display=ep_disp,
|
|
320
|
+
title=f"Unauthorized Sprint Deliverable & Schedule Manipulation on {ep_disp}",
|
|
321
|
+
description="Endpoint updates deliverable statuses or rolls forward sprint deadlines without verifying workspace membership or caller permissions.",
|
|
322
|
+
recommended_test_pack="authentication",
|
|
323
|
+
confidence_prior=0.80,
|
|
324
|
+
static_evidence=[
|
|
325
|
+
f"Deliverable/schedule mutation: {ep.method} {ep.path}",
|
|
326
|
+
"Missing workspace collaborator or editor role check",
|
|
327
|
+
"Risk: Tampering with project sprint audit trails and deliverable completion status",
|
|
328
|
+
],
|
|
329
|
+
severity=Severity.HIGH,
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
hypo_idx += 1
|
|
270
333
|
elif any(term in ep.path.lower() for term in ("scatt-analysis", "inference", "predict", "telemetry/analyze")) and not ep.database_access:
|
|
271
334
|
# Case: Stateless ML/inference compute endpoint (low impact)
|
|
272
335
|
hypotheses.append(
|
|
@@ -316,7 +379,7 @@ class HypothesisEngine:
|
|
|
316
379
|
category=VulnerabilityCategory.AUTHENTICATION,
|
|
317
380
|
endpoint_id=ep.id,
|
|
318
381
|
endpoint_display=ep_disp,
|
|
319
|
-
title=f"Unauthenticated
|
|
382
|
+
title=f"Unauthenticated Sensitive Route Exposure on {ep_disp}",
|
|
320
383
|
description="Sensitive or state-modifying endpoint does not declare authentication requirement.",
|
|
321
384
|
recommended_test_pack="authentication",
|
|
322
385
|
confidence_prior=0.80,
|