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 +1 -1
- 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 +298 -143
- package/src/trace_engine/findings/store.py +13 -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trace-sec",
|
|
3
|
-
"version": "2.1.
|
|
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
|
@@ -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,307 @@
|
|
|
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
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
"
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"
|
|
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
|
-
|
|
143
|
-
return
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
#
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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:
|
|
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}",
|