trace-sec 2.1.13 → 2.1.14

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trace-sec",
3
- "version": "2.1.13",
3
+ "version": "2.1.14",
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",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "trace-sec"
7
- version = "2.1.13"
7
+ version = "2.1.14"
8
8
  description = "Threat Reconnaissance & Attack-path Correlation Engine"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -3,6 +3,6 @@
3
3
  Local-first application security analysis and runtime validation engine.
4
4
  """
5
5
 
6
- __version__ = "2.1.13"
6
+ __version__ = "2.1.14"
7
7
  __app_name__ = "TRACE"
8
8
  __description__ = "Threat Reconnaissance & Attack-path Correlation Engine"
@@ -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=get_remediation_for_category(category_str, filepath=file_path),
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
  )
@@ -38,3 +38,5 @@ class Finding(BaseModel):
38
38
  remediation: str
39
39
  reproduction_steps: List[str] = Field(default_factory=list)
40
40
  observations: List[RuntimeObservation] = Field(default_factory=list)
41
+ root_cause: Optional[str] = None
42
+ short_action: Optional[str] = None
@@ -1,152 +1,307 @@
1
- """Remediation recommendations for security findings."""
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
- def get_remediation_for_category(category: str, filepath: str = "", framework: str = "") -> str:
127
- """Returns framework- and language-tailored remediation recommendations."""
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
- # Detect language / framework
132
- if any(file_lower.endswith(ext) for ext in (".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs")) or framework.lower() in ("express", "nextjs", "react_router", "node"):
133
- return REMEDIATION_MAP_NODE.get(
134
- cat_upper,
135
- "Review access control, authentication middleware, and input validation on this attack path.",
26
+ # 1. Caller-Controlled Ownership Bypass via Optional Parameter
27
+ 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):
28
+ root_cause = (
29
+ f"The multi-tenant authorization assertion on `{endpoint}` is conditionally guarded by a client-supplied "
30
+ f"query parameter (e.g. `coach_id`). Because the parameter is optional (`Optional[int] = Query(None)`), "
31
+ f"an attacker can simply omit `?coach_id=` from the request URL to completely skip the ownership assertion "
32
+ f"and access or modify private domain records belonging to any athlete."
136
33
  )
137
- elif file_lower.endswith(".go") or framework.lower() == "go":
138
- return REMEDIATION_MAP_GO.get(
139
- cat_upper,
140
- "Review access control and input validation on this attack path.",
34
+ remediation = (
35
+ "1. Remove `coach_id` from client-supplied query parameters.\n"
36
+ "2. Extract the authenticated caller's identity directly from verified session state or JWT claims "
37
+ "(e.g., `current_user = Depends(get_current_user)`).\n"
38
+ "3. Enforce the tenant/coach ownership check unconditionally using the session ID: "
39
+ "`assert_coach_owns_athlete(current_user.id, user_id)` before querying or mutating the database.\n"
40
+ "4. Return HTTP 403 Forbidden or 404 Not Found if the relationship check fails."
141
41
  )
142
- elif file_lower.endswith(".py") or framework.lower() in ("fastapi", "flask", "django"):
143
- return REMEDIATION_MAP_PYTHON.get(
144
- cat_upper,
145
- "Review access control and input validation on this attack path.",
42
+ short_action = "Enforce caller tenant validation from session token; do not accept optional coach_id."
43
+ return root_cause, remediation, short_action
44
+
45
+ # 2. Public StaticFiles Directory Mount
46
+ if "staticfiles" in t_lower or "staticfiles" in ev_text or "/session_videos" in ep_lower:
47
+ root_cause = (
48
+ f"The directory containing stored files is mounted publicly via FastAPI's `StaticFiles` at `{endpoint}` "
49
+ f"without authentication or tenant authorization middleware. This completely circumvents API-level access "
50
+ f"controls, allowing unauthenticated callers to enumerate, stream, and download private user recordings and "
51
+ f"uploaded files directly via static HTTP GET requests."
52
+ )
53
+ remediation = (
54
+ f"1. Remove the public directory mount `app.mount('{endpoint}', StaticFiles(...))`.\n"
55
+ "2. Implement a protected API endpoint (e.g. `GET /api/media/{file_id}`) that validates caller authentication "
56
+ "and verifies that the requested media belongs to the requesting user before streaming.\n"
57
+ "3. Stream files securely using `FileResponse` or `StreamingResponse` after authorization.\n"
58
+ "4. Alternatively, store sensitive files in an S3/GCS bucket and generate short-lived, pre-signed URLs."
146
59
  )
60
+ short_action = "Remove public StaticFiles mount; serve files via authenticated FileResponse."
61
+ return root_cause, remediation, short_action
147
62
 
148
- # Fallback to general remediation
149
- return REMEDIATION_MAP_NODE.get(cat_upper) or REMEDIATION_MAP_PYTHON.get(
150
- cat_upper,
151
- "Review access control and input validation on this attack path.",
63
+ # 3. Broken Password Hashing & Plaintext Fallback
64
+ if "password hashing" in t_lower or "plaintext" in ev_text or "verify_password" in t_lower:
65
+ root_cause = (
66
+ "Password verification logic in the authentication module contains a critical plaintext equality fallback "
67
+ "(`if hashed_password == plain_password:`) and/or relies on a single hardcoded global salt with fast, "
68
+ "un-keyed SHA-256 instead of an adaptive work-factor algorithm. This permits unhashed credential logins "
69
+ "and leaves stored hashes susceptible to commodity GPU rainbow table recovery."
70
+ )
71
+ remediation = (
72
+ "1. Immediately delete the plaintext equality comparison `if hashed_password == plain_password:`.\n"
73
+ "2. Migrate password hashing to an adaptive, slow work-factor algorithm such as `bcrypt` or `Argon2id` "
74
+ "(e.g. using `passlib.context.CryptContext(schemes=['bcrypt'])`).\n"
75
+ "3. Ensure the hashing function automatically generates and binds a unique, cryptographically random salt per user record.\n"
76
+ "4. Re-hash legacy passwords transparently upon user login."
77
+ )
78
+ short_action = "Eliminate plaintext check; migrate password hashing to bcrypt/Argon2 with per-user salt."
79
+ return root_cause, remediation, short_action
80
+
81
+ # 4. Unauthenticated Global Session / Data Wipe with Blast Radius
82
+ if "blast radius" in t_lower or ("reset" in ep_lower and ("wipe" in t_lower or "global" in t_lower)):
83
+ root_cause = (
84
+ f"Calling `{endpoint}` without request parameters defaults to iterating across all connected users "
85
+ f"and executes bulk deletion (`shutil.rmtree` and database row deletion) across all user accounts "
86
+ f"without requiring authentication, session scoping, or confirmation. A single unauthenticated HTTP call "
87
+ f"destroys the entire system's session and telemetry data."
88
+ )
89
+ remediation = (
90
+ f"1. Enforce authentication dependency `Depends(get_current_user)` on `{endpoint}`.\n"
91
+ "2. Scope the deletion logic strictly to `user_id == current_user.id` so a caller can only reset their own session.\n"
92
+ "3. If a global multi-tenant reset is required for operations, isolate it to a privileged route protected by "
93
+ "`Depends(require_admin)` and require multi-factor or secondary token confirmation."
94
+ )
95
+ short_action = "Scope deletion strictly to current_user.id; restrict global wipes to require_admin."
96
+ return root_cause, remediation, short_action
97
+
98
+ # 5. Privilege Escalation via Unvalidated Role Assignment
99
+ if "role assignment" in t_lower or ("role" in ev_text and "privilege" in t_lower):
100
+ root_cause = (
101
+ f"The user registration/creation handler on `{endpoint}` binds a client-supplied `role` string directly "
102
+ f"into the new user record without server-side validation or administrative authorization checks. "
103
+ f"An unauthenticated attacker can submit `role='ADMIN'` to instantly create a persistent superuser account."
104
+ )
105
+ remediation = (
106
+ "1. Remove `role` and administrative privilege attributes from public user creation schemas.\n"
107
+ "2. Restrict administrative user creation (`/api/admin/users/create`) with `Depends(require_admin)`.\n"
108
+ "3. Enforce a strict server-side Enum allowlist (e.g. `Role.USER`, `Role.ATHLETE`) and default all new accounts "
109
+ "to unprivileged roles regardless of client payload."
110
+ )
111
+ short_action = "Protect with require_admin; enforce server-side role allowlist on user creation."
112
+ return root_cause, remediation, short_action
113
+
114
+ # 6. Account Takeover via Unauthenticated Password Reset
115
+ if "reset" in ep_lower and "password" in ep_lower:
116
+ root_cause = (
117
+ f"The password reset endpoint `{endpoint}` modifies user credentials without requiring current password "
118
+ f"validation, an authenticated session, or an out-of-band cryptographically signed reset token. An attacker "
119
+ f"can overwrite the password of any user account by targeting their sequential ID or username."
120
+ )
121
+ remediation = (
122
+ "1. Implement an out-of-band reset flow using time-limited, single-use cryptographically random tokens "
123
+ "delivered to the verified user email address.\n"
124
+ "2. For self-service password changes by logged-in users, require current password verification.\n"
125
+ "3. For administrative password resets, guard the route with `Depends(require_admin)`."
126
+ )
127
+ short_action = "Require verified single-use reset token or require_admin check for password resets."
128
+ return root_cause, remediation, short_action
129
+
130
+ # 7. Insecure CORS Configuration
131
+ if cat_upper == "CORS" or "cors" in t_lower:
132
+ root_cause = (
133
+ f"`CORSMiddleware` on `{endpoint}` is configured with `allow_origins=['*']` combined with "
134
+ f"`allow_credentials=True`. This permits arbitrary third-party websites to execute credentialed "
135
+ f"cross-origin requests against the backend, enabling CSRF-style data exfiltration and session riding."
136
+ )
137
+ remediation = (
138
+ "1. Remove wildcard `allow_origins=['*']`.\n"
139
+ "2. Configure an explicit array of authorized frontend domains (e.g. `allow_origins=['https://app.example.com']`).\n"
140
+ "3. Never dynamically reflect incoming `Origin` headers into `Access-Control-Allow-Origin` when credentials are enabled."
141
+ )
142
+ short_action = "Remove wildcard allow_origins; define explicit list of authorized frontend domains."
143
+ return root_cause, remediation, short_action
144
+
145
+ # 8. Unauthenticated WebSockets
146
+ if "websocket" in t_lower or "websocket" in ev_text:
147
+ root_cause = (
148
+ f"The WebSocket route `{endpoint}` accepts incoming streaming connections and dispatches real-time telemetry "
149
+ f"without authenticating the client during the handshake, allowing unauthorized eavesdropping or malicious event injection."
150
+ )
151
+ remediation = (
152
+ "1. Extract and validate authentication credentials (JWT ticket or session cookie) during the WebSocket connection handshake.\n"
153
+ "2. Reject unauthenticated handshakes before calling `await websocket.accept()` using WebSocket close code 1008 (Policy Violation).\n"
154
+ "3. Associate the open WebSocket connection strictly with the validated user ID."
155
+ )
156
+ short_action = "Validate authentication token during handshake before calling websocket.accept()."
157
+ return root_cause, remediation, short_action
158
+
159
+ # 9. Stateless ML / Inference Resource Exhaustion
160
+ if "scatt-analysis" in ep_lower or "inference" in t_lower or "stateless" in t_lower:
161
+ root_cause = (
162
+ f"The compute-intensive inference endpoint `{endpoint}` is exposed to the public internet without authentication "
163
+ f"or request rate-limiting, allowing unauthorized callers to trigger CPU/GPU exhaustion and denial-of-wallet."
164
+ )
165
+ remediation = (
166
+ "1. Add API key or session token verification to authenticate callers.\n"
167
+ "2. Implement rate-limiting middleware (e.g. `slowapi` or Redis token bucket) to throttle request frequency per client IP.\n"
168
+ "3. Enforce maximum payload size limits on input telemetry arrays."
169
+ )
170
+ short_action = "Add API key authentication and rate-limiting middleware (slowapi) to throttle inference."
171
+ return root_cause, remediation, short_action
172
+
173
+ # 10. Unauthenticated User Information Disclosure
174
+ if "user information disclosure" in t_lower or ("user" in ep_lower and cat_upper == "AUTHENTICATION" and "get" in ep_lower):
175
+ root_cause = (
176
+ f"Read-only endpoint `{endpoint}` discloses user profile records (ID, username, role) to unauthenticated callers, "
177
+ f"permitting automated enumeration of all registered users in the application."
178
+ )
179
+ remediation = (
180
+ f"1. Protect `{endpoint}` with `Depends(get_current_user)` or require valid session authentication.\n"
181
+ "2. If public user profiles are intended by design, redact internal database IDs, role assignments, and private metadata."
182
+ )
183
+ short_action = "Protect user lookup with Depends(get_current_user) to prevent user enumeration."
184
+ return root_cause, remediation, short_action
185
+
186
+ fp_lower = filepath.lower()
187
+ is_node = fp_lower.endswith((".js", ".ts", ".jsx", ".tsx", ".mjs"))
188
+
189
+ # 11. General BOLA / IDOR
190
+ if cat_upper == "BOLA" or "bola" in t_lower:
191
+ root_cause = (
192
+ f"The handler for `{endpoint}` looks up database records using a client-supplied object ID from the URL or query "
193
+ f"without verifying that the requested record belongs to the authenticated user's tenant or organization. "
194
+ f"An attacker can increment or replace the ID to view or modify records belonging to other tenants."
195
+ )
196
+ if is_node:
197
+ remediation = (
198
+ "1. Extract authenticated caller ID from session state (`req.user.id` or `req.user.tenantId`).\n"
199
+ "2. Scope database queries to verify `req.user.tenantId == record.tenantId` before returning data.\n"
200
+ "3. Return HTTP 403 Forbidden or 404 Not Found if the resource is not owned by the caller."
201
+ )
202
+ short_action = "Scope database query to req.user.tenantId to enforce object-level isolation."
203
+ else:
204
+ remediation = (
205
+ "1. Extract authenticated caller ID from the verified session context (e.g. `current_user = Depends(get_current_user)`).\n"
206
+ "2. Scope database queries to both the object ID and the user's tenant: "
207
+ "`db.query(Model).filter(Model.id == obj_id, Model.tenant_id == current_user.tenant_id).first()`.\n"
208
+ "3. Return HTTP 403 Forbidden or 404 Not Found if the resource is not owned by the caller."
209
+ )
210
+ short_action = "Scope database query to authenticated current_user.tenant_id to enforce isolation."
211
+ return root_cause, remediation, short_action
212
+
213
+ # 12. General BFLA / Admin RBAC Missing
214
+ if cat_upper == "BFLA" or "admin" in ep_lower or "bfla" in t_lower:
215
+ root_cause = (
216
+ f"Privileged administrative route `{endpoint}` is accessible without verifying that the caller possesses "
217
+ f"administrative role permissions. Any standard user or unauthenticated client can trigger administrative actions."
218
+ )
219
+ if is_node:
220
+ remediation = (
221
+ "1. Mount role-verification middleware (`requireRole('admin')`) on the route.\n"
222
+ "2. Verify `req.user && req.user.role === 'admin'` before executing privileged operations."
223
+ )
224
+ short_action = "Mount requireRole('admin') middleware on privileged administrative route."
225
+ else:
226
+ remediation = (
227
+ "1. Implement declarative role-based access control (RBAC) dependencies (e.g. `Security(require_role('admin'))`).\n"
228
+ "2. Verify that `current_user.role == 'ADMIN'` on the backend before executing privileged state changes."
229
+ )
230
+ short_action = "Implement declarative RBAC dependency (require_admin) on privileged administrative route."
231
+ return root_cause, remediation, short_action
232
+
233
+ # 13. General Authentication Missing on Sensitive Route
234
+ if cat_upper == "AUTHENTICATION":
235
+ root_cause = (
236
+ f"Sensitive or state-modifying endpoint `{endpoint}` does not enforce authentication. Unauthorized callers "
237
+ f"can trigger operations or retrieve private data without providing credentials or session tokens."
238
+ )
239
+ if is_node:
240
+ remediation = (
241
+ f"1. Mount authentication middleware `verifyToken` or `router.use(authMiddleware)` on `{endpoint}`.\n"
242
+ "2. Reject unauthenticated requests with HTTP 401 Unauthorized."
243
+ )
244
+ short_action = "Mount verifyToken middleware on Express route."
245
+ else:
246
+ remediation = (
247
+ f"1. Mount authentication dependency `Depends(get_current_user)` or security middleware on `{endpoint}`.\n"
248
+ "2. Reject unauthenticated requests with HTTP 401 Unauthorized."
249
+ )
250
+ short_action = "Protect route with Depends(get_current_user) to block unauthenticated callers."
251
+ return root_cause, remediation, short_action
252
+
253
+ # 14. Path Traversal
254
+ if cat_upper == "PATH_TRAVERSAL":
255
+ root_cause = (
256
+ f"Endpoint `{endpoint}` accepts file or path parameters without verifying that the resolved path resides "
257
+ f"strictly within the intended base directory. Attackers can use path traversal sequences (`..`) to access arbitrary system files."
258
+ )
259
+ remediation = (
260
+ "1. Sanitize file path inputs using `os.path.basename()` or `pathlib.Path.resolve()`.\n"
261
+ "2. Verify that `resolved_path.is_relative_to(base_dir)` before opening or reading files."
262
+ )
263
+ short_action = "Sanitize file input with basename() and verify resolved directory containment."
264
+ return root_cause, remediation, short_action
265
+
266
+ # 15. SSRF
267
+ if cat_upper == "SSRF":
268
+ root_cause = (
269
+ f"Endpoint `{endpoint}` accepts a user-controlled destination URL and dispatches outbound HTTP requests without "
270
+ f"validating the host against an allowlist, allowing attackers to probe internal cloud metadata and RFC-1918 networks."
271
+ )
272
+ remediation = (
273
+ "1. Validate target URLs against a strict allowlist of authorized hostnames.\n"
274
+ "2. Resolve DNS names and reject loopback (`127.0.0.1`, `localhost`) and private RFC-1918 IP addresses before dispatching HTTP calls."
275
+ )
276
+ short_action = "Enforce domain allowlist and block loopback/private RFC-1918 IPs on outbound HTTP."
277
+ return root_cause, remediation, short_action
278
+
279
+ # 16. SQL / NoSQL Injection
280
+ if cat_upper == "INJECTION":
281
+ root_cause = (
282
+ f"Endpoint `{endpoint}` routes user-supplied query parameters to the database layer without verified parameterization, "
283
+ f"permitting SQL syntax injection and unauthorized database manipulation."
284
+ )
285
+ remediation = (
286
+ "1. Use parameterized queries, prepared statements, or ORM abstraction methods for all database operations.\n"
287
+ "2. Never concatenate or format raw client inputs into SQL query strings."
288
+ )
289
+ short_action = "Use parameterized SQL queries or ORM models; never concatenate raw user input."
290
+ return root_cause, remediation, short_action
291
+
292
+ # Default fallback
293
+ root_cause = f"Security vulnerability of category `{category}` identified in `{endpoint}`."
294
+ remediation = "Apply strict authentication, tenant authorization boundaries, and input validation to this attack path."
295
+ short_action = f"Apply strict access control and input validation on {category} attack path."
296
+ return root_cause, remediation, short_action
297
+
298
+
299
+ def get_remediation_for_category(category: str, filepath: str = "", framework: str = "") -> str:
300
+ """Backwards-compatible remediation accessor."""
301
+ _, rem, _ = get_detailed_remediation_and_root_cause(
302
+ title=category,
303
+ category=category,
304
+ endpoint="endpoint",
305
+ filepath=filepath,
152
306
  )
307
+ 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():
@@ -624,53 +624,57 @@ def run_interactive_story() -> None:
624
624
  )
625
625
 
626
626
  # =========================================================================
627
- # STEP 7: Autonomous AST Self-Healing Opportunity
627
+ # STEP 7: AI Coding Agent Handoff & Export Options
628
628
  # =========================================================================
629
- if findings:
630
- console.print(f"\n [{DIM_GREEN}]{'─' * 76}[/{DIM_GREEN}]")
631
- console.print(f" [{GREEN}]◆ AUTONOMOUS AST SELF-HEALING OPPORTUNITY:[/{GREEN}]")
632
- console.print(f" [{WHITE}]TRACE can synthesize surgical AST patches for detected flaws in {project_dir.name}.[/{WHITE}]")
633
- console.print(f" [{DIM_WHITE}]Includes transactional rollback protection and verification oracle testing.[/{DIM_WHITE}]\n")
634
-
635
- heal_ans = ask_input(
636
- f" [{ORANGE}]›[/{ORANGE}] [{BOLD_WHITE}]Autonomously remediate verified vulnerabilities now? (y/n)[/{BOLD_WHITE}]",
637
- choices=["y", "n", "yes", "no"],
638
- default="n",
639
- )
640
- should_heal = heal_ans.lower() in ("y", "yes")
641
-
642
- if should_heal:
643
- from trace_engine.harness.engine import AgentHarness
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 post_choice == "1":
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"- Total Correlated Findings: **{len(findings)}**",
13
+ "## Executive Summary",
14
+ f"- **Target Application:** `{project_name}`",
15
+ f"- **Total Correlated Findings:** **{len(findings)}**",
15
16
  "",
16
- "| ID | Severity | Confidence | Category | Endpoint | Title |",
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}` | {f.title} |"
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(f"#### Remediation Guidance\n{f.remediation}\n")
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("\nRemediation Guidance:\n", style="bold green")
239
- content.append(f" {f.remediation}\n", style="dim white")
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}",