trace-sec 2.1.12 → 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 +15 -5
- 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/framework/fastapi.py +216 -135
- 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 +419 -64
|
@@ -1,135 +1,216 @@
|
|
|
1
|
-
"""FastAPI framework adapter for endpoint and attack-surface discovery."""
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
from typing import List, Optional
|
|
5
|
-
from trace_engine.parsing.parser import ParsedFile
|
|
6
|
-
from trace_engine.framework.base import FrameworkAdapter, Endpoint, EndpointParameter
|
|
7
|
-
from trace_engine.parsing.locations import SourceLocation
|
|
8
|
-
|
|
9
|
-
HTTP_METHODS = {"get", "post", "put", "delete", "patch", "options", "head"}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class FastAPIFrameworkAdapter(FrameworkAdapter):
|
|
13
|
-
"""Detects and extracts FastAPI/Starlette routes and auth gates."""
|
|
14
|
-
|
|
15
|
-
def can_handle(self, parsed_file: ParsedFile) -> bool:
|
|
16
|
-
if parsed_file.language != "python":
|
|
17
|
-
return False
|
|
18
|
-
for imp in parsed_file.imports:
|
|
19
|
-
if "fastapi" in imp.module.lower() or "starlette" in imp.module.lower():
|
|
20
|
-
return True
|
|
21
|
-
return False
|
|
22
|
-
|
|
23
|
-
def extract_endpoints(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
|
|
24
|
-
endpoints: List[Endpoint] = []
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
#
|
|
28
|
-
for fn in parsed_file.functions:
|
|
29
|
-
for dec in fn.decorators:
|
|
30
|
-
dec_text = dec.raw_text.strip()
|
|
31
|
-
# Pattern: @app.get('/path' ...)
|
|
32
|
-
match = re.search(
|
|
33
|
-
r"""@?(?:[a-zA-Z0-9_]+)\.(get|post|put|delete|patch)\s*\(\s*["']([^"']+)["']""",
|
|
34
|
-
dec_text,
|
|
35
|
-
re.IGNORECASE,
|
|
36
|
-
)
|
|
37
|
-
if match:
|
|
38
|
-
|
|
39
|
-
method =
|
|
40
|
-
path = match.group(2)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
1
|
+
"""FastAPI framework adapter for endpoint and attack-surface discovery."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
from trace_engine.parsing.parser import ParsedFile
|
|
6
|
+
from trace_engine.framework.base import FrameworkAdapter, Endpoint, EndpointParameter
|
|
7
|
+
from trace_engine.parsing.locations import SourceLocation
|
|
8
|
+
|
|
9
|
+
HTTP_METHODS = {"get", "post", "put", "delete", "patch", "options", "head", "websocket"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FastAPIFrameworkAdapter(FrameworkAdapter):
|
|
13
|
+
"""Detects and extracts FastAPI/Starlette routes, WebSockets, static mounts, and auth gates."""
|
|
14
|
+
|
|
15
|
+
def can_handle(self, parsed_file: ParsedFile) -> bool:
|
|
16
|
+
if parsed_file.language != "python":
|
|
17
|
+
return False
|
|
18
|
+
for imp in parsed_file.imports:
|
|
19
|
+
if "fastapi" in imp.module.lower() or "starlette" in imp.module.lower():
|
|
20
|
+
return True
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
def extract_endpoints(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
|
|
24
|
+
endpoints: List[Endpoint] = []
|
|
25
|
+
existing_keys = set()
|
|
26
|
+
|
|
27
|
+
# 1. Search decorators on parsed AST functions
|
|
28
|
+
for fn in parsed_file.functions:
|
|
29
|
+
for dec in fn.decorators:
|
|
30
|
+
dec_text = dec.raw_text.strip()
|
|
31
|
+
# Pattern: @app.get('/path' ...), @router.post('/path' ...), @app.websocket('/ws/...')
|
|
32
|
+
match = re.search(
|
|
33
|
+
r"""@?(?:[a-zA-Z0-9_]+)\.(get|post|put|delete|patch|websocket|api_route)\s*\(\s*["']([^"']+)["']""",
|
|
34
|
+
dec_text,
|
|
35
|
+
re.IGNORECASE,
|
|
36
|
+
)
|
|
37
|
+
if match:
|
|
38
|
+
raw_verb = match.group(1).upper()
|
|
39
|
+
method = "WEBSOCKET" if raw_verb == "WEBSOCKET" else ("GET" if raw_verb == "API_ROUTE" else raw_verb)
|
|
40
|
+
path = match.group(2)
|
|
41
|
+
key = (method, path)
|
|
42
|
+
if key in existing_keys:
|
|
43
|
+
continue
|
|
44
|
+
existing_keys.add(key)
|
|
45
|
+
|
|
46
|
+
ep_id = f"ep_{parsed_file.file_path.replace('/', '_').replace('.', '_')}_{fn.name}_{method}"
|
|
47
|
+
|
|
48
|
+
# Detect path parameters: {param_name}
|
|
49
|
+
path_params = re.findall(r"\{([a-zA-Z0-9_]+)\}", path)
|
|
50
|
+
params: List[EndpointParameter] = [
|
|
51
|
+
EndpointParameter(name=p, location="path", required=True)
|
|
52
|
+
for p in path_params
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
# Detect function params from AST
|
|
56
|
+
for p in fn.parameters:
|
|
57
|
+
if p.name not in ("self", "cls") and p.name not in path_params:
|
|
58
|
+
params.append(
|
|
59
|
+
EndpointParameter(
|
|
60
|
+
name=p.name,
|
|
61
|
+
location="query" if method in ("GET", "WEBSOCKET") else "body",
|
|
62
|
+
param_type=p.type_annotation or "string",
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Analyze auth dependencies in decorator or args
|
|
67
|
+
auth_required = False
|
|
68
|
+
roles = []
|
|
69
|
+
fn_body = fn.body_text or ""
|
|
70
|
+
|
|
71
|
+
auth_indicators = [
|
|
72
|
+
"current_user",
|
|
73
|
+
"auth",
|
|
74
|
+
"token",
|
|
75
|
+
"security",
|
|
76
|
+
"jwt",
|
|
77
|
+
"oauth",
|
|
78
|
+
"credentials",
|
|
79
|
+
"verify_token",
|
|
80
|
+
"api_key",
|
|
81
|
+
"require_admin",
|
|
82
|
+
"get_current_user",
|
|
83
|
+
]
|
|
84
|
+
for p in fn.parameters:
|
|
85
|
+
p_ann = (p.type_annotation or "").lower()
|
|
86
|
+
p_def = (p.default_value or "").lower()
|
|
87
|
+
if any(ind in p.name.lower() or ind in p_ann or ind in p_def for ind in auth_indicators):
|
|
88
|
+
auth_required = True
|
|
89
|
+
if "admin" in p.name.lower() or "admin" in p_ann or "admin" in p_def:
|
|
90
|
+
roles.append("admin")
|
|
91
|
+
|
|
92
|
+
if "require_admin" in dec_text.lower() or "admin" in path.lower():
|
|
93
|
+
roles.append("admin")
|
|
94
|
+
if "current_user" in dec_text.lower() or "depends(" in dec_text.lower():
|
|
95
|
+
auth_required = True
|
|
96
|
+
|
|
97
|
+
# Object identifier flag: e.g. /items/{id}, /athlete/{athlete_id}, /coach/{coach_id}
|
|
98
|
+
has_obj_id = any(
|
|
99
|
+
p.lower() == "id" or p.lower().endswith("_id") or p.lower().endswith("id")
|
|
100
|
+
for p in path_params
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# State changing
|
|
104
|
+
state_changing = method in ("POST", "PUT", "DELETE", "PATCH")
|
|
105
|
+
|
|
106
|
+
# Database access check
|
|
107
|
+
db_access = any(
|
|
108
|
+
term in fn_body.lower()
|
|
109
|
+
for term in ("db.", "session.", "cursor.", "repository", "query(", ".filter(", "database", "models.")
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Outbound network (SSRF indicator)
|
|
113
|
+
ext_network = any(
|
|
114
|
+
term in fn_body.lower()
|
|
115
|
+
for term in ("requests.", "httpx.", "urllib.", "aiohttp.", "fetch(")
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Sensitive data
|
|
119
|
+
sensitive = any(
|
|
120
|
+
term in path.lower() or term in fn_body.lower()
|
|
121
|
+
for term in ("password", "secret", "token", "credit", "card", "user", "order", "refund", "admin", "coach", "athlete", "story", "session")
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
endpoints.append(
|
|
125
|
+
Endpoint(
|
|
126
|
+
id=ep_id,
|
|
127
|
+
method=method,
|
|
128
|
+
path=path,
|
|
129
|
+
handler_name=fn.qualified_name,
|
|
130
|
+
auth_required=auth_required,
|
|
131
|
+
roles=list(set(roles)),
|
|
132
|
+
parameters=params,
|
|
133
|
+
database_access=db_access,
|
|
134
|
+
object_identifier=has_obj_id,
|
|
135
|
+
state_changing=state_changing,
|
|
136
|
+
external_network=ext_network,
|
|
137
|
+
sensitive_data=sensitive,
|
|
138
|
+
source=fn.location,
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# 2. Detect StaticFiles mounts e.g. app.mount("/session_videos", StaticFiles(directory="session_videos"), name="session_videos")
|
|
143
|
+
mount_pattern = re.compile(
|
|
144
|
+
r"""(?:app|router)\.mount\s*\(\s*["']([^"']+)["']\s*,\s*StaticFiles\s*\(\s*(?:directory\s*=\s*)?["']([^"']+)["']""",
|
|
145
|
+
re.IGNORECASE,
|
|
146
|
+
)
|
|
147
|
+
for match in mount_pattern.finditer(content):
|
|
148
|
+
mount_path = match.group(1).rstrip("/")
|
|
149
|
+
directory = match.group(2)
|
|
150
|
+
line_no = content[:match.start()].count("\n") + 1
|
|
151
|
+
ep_id = f"ep_{parsed_file.file_path.replace('/', '_').replace('.', '_')}_mount_{directory}_GET"
|
|
152
|
+
key = ("GET", f"{mount_path}/{{filepath}}")
|
|
153
|
+
if key not in existing_keys:
|
|
154
|
+
existing_keys.add(key)
|
|
155
|
+
endpoints.append(
|
|
156
|
+
Endpoint(
|
|
157
|
+
id=ep_id,
|
|
158
|
+
method="GET",
|
|
159
|
+
path=f"{mount_path}/{{filepath}}",
|
|
160
|
+
handler_name=f"StaticFiles({directory})",
|
|
161
|
+
auth_required=False,
|
|
162
|
+
roles=[],
|
|
163
|
+
parameters=[
|
|
164
|
+
EndpointParameter(name="filepath", location="path", required=True)
|
|
165
|
+
],
|
|
166
|
+
database_access=False,
|
|
167
|
+
object_identifier=True,
|
|
168
|
+
state_changing=False,
|
|
169
|
+
external_network=False,
|
|
170
|
+
sensitive_data=True,
|
|
171
|
+
source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# 3. Direct regex fallback across raw content to catch routes missed by AST function traversal
|
|
176
|
+
raw_route_pat = re.compile(
|
|
177
|
+
r"""@(?:app|router|api_router)\.(get|post|put|delete|patch|websocket)\s*\(\s*["']([^"']+)["']""",
|
|
178
|
+
re.IGNORECASE,
|
|
179
|
+
)
|
|
180
|
+
for match in raw_route_pat.finditer(content):
|
|
181
|
+
raw_verb = match.group(1).upper()
|
|
182
|
+
method = "WEBSOCKET" if raw_verb == "WEBSOCKET" else raw_verb
|
|
183
|
+
path = match.group(2)
|
|
184
|
+
key = (method, path)
|
|
185
|
+
if key in existing_keys:
|
|
186
|
+
continue
|
|
187
|
+
existing_keys.add(key)
|
|
188
|
+
|
|
189
|
+
line_no = content[:match.start()].count("\n") + 1
|
|
190
|
+
path_params = re.findall(r"\{([a-zA-Z0-9_]+)\}", path)
|
|
191
|
+
has_obj_id = any(
|
|
192
|
+
p.lower() == "id" or p.lower().endswith("_id") or p.lower().endswith("id")
|
|
193
|
+
for p in path_params
|
|
194
|
+
)
|
|
195
|
+
endpoints.append(
|
|
196
|
+
Endpoint(
|
|
197
|
+
id=f"ep_{parsed_file.file_path.replace('/', '_').replace('.', '_')}_L{line_no}_{method}",
|
|
198
|
+
method=method,
|
|
199
|
+
path=path,
|
|
200
|
+
handler_name=f"route_L{line_no}",
|
|
201
|
+
auth_required=False,
|
|
202
|
+
roles=["admin"] if "admin" in path.lower() else [],
|
|
203
|
+
parameters=[
|
|
204
|
+
EndpointParameter(name=p, location="path", required=True)
|
|
205
|
+
for p in path_params
|
|
206
|
+
],
|
|
207
|
+
database_access=True,
|
|
208
|
+
object_identifier=has_obj_id,
|
|
209
|
+
state_changing=method in ("POST", "PUT", "DELETE", "PATCH"),
|
|
210
|
+
external_network=False,
|
|
211
|
+
sensitive_data="admin" in path.lower() or "user" in path.lower() or "session" in path.lower() or "story" in path.lower(),
|
|
212
|
+
source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return endpoints
|
|
@@ -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}",
|