trace-sec 2.1.14 → 2.1.16
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/bin/trace.js +12 -1
- package/package.json +1 -4
- package/pyproject.toml +1 -1
- package/src/trace_engine/__init__.py +1 -1
- package/src/trace_engine/cli.py +60 -1
- package/src/trace_engine/findings/recommendations.py +149 -0
- package/src/trace_engine/framework/express.py +5 -1
- package/src/trace_engine/interactive.py +44 -6
- package/src/trace_engine/output/terminal.py +3 -1
- package/src/trace_engine/security/hypotheses.py +64 -1
- package/src/trace_engine/updater.py +305 -0
package/bin/trace.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { spawn, execSync } from 'child_process';
|
|
9
|
-
import { existsSync, mkdirSync, cpSync, writeFileSync } from 'fs';
|
|
9
|
+
import { existsSync, mkdirSync, cpSync, writeFileSync, readFileSync } from 'fs';
|
|
10
10
|
import { resolve, join, dirname } from 'path';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
12
|
|
|
@@ -15,6 +15,17 @@ const __dirname = dirname(__filename);
|
|
|
15
15
|
const projectRoot = resolve(__dirname, '..');
|
|
16
16
|
const args = process.argv.slice(2);
|
|
17
17
|
|
|
18
|
+
// Handle --version / -v directly in Node.js for instant response
|
|
19
|
+
if (args[0] === '--version' || args[0] === '-v' || args[0] === '-V' || args[0] === 'version') {
|
|
20
|
+
try {
|
|
21
|
+
const pkg = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf-8'));
|
|
22
|
+
console.log(`TRACE v${pkg.version}`);
|
|
23
|
+
} catch {
|
|
24
|
+
console.log('TRACE');
|
|
25
|
+
}
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
// Handle install-skill directly in Node.js for instant zero-dependency agent skill setup
|
|
19
30
|
if (args[0] === 'install-skill' || args[0] === 'setup-agent' || args[0] === 'install-agent') {
|
|
20
31
|
const targetDir = process.cwd();
|
package/package.json
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trace-sec",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.16",
|
|
4
4
|
"description": "Threat Reconnaissance & Attack-path Correlation Engine — Security Verification Layer for Coding Agents with Fine-Tuned SecureBERT 2.0 & Laya AI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/trace.js",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"postinstall": "node bin/postinstall.js"
|
|
9
|
-
},
|
|
10
7
|
"bin": {
|
|
11
8
|
"trace": "bin/trace.js",
|
|
12
9
|
"trace-sec": "bin/trace.js",
|
package/pyproject.toml
CHANGED
package/src/trace_engine/cli.py
CHANGED
|
@@ -68,13 +68,52 @@ console = Console()
|
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
@app.callback()
|
|
71
|
-
def main(
|
|
71
|
+
def main(
|
|
72
|
+
ctx: typer.Context,
|
|
73
|
+
version: Optional[bool] = typer.Option(
|
|
74
|
+
None, "--version", "-v", "-V", help="Show TRACE version and exit."
|
|
75
|
+
),
|
|
76
|
+
):
|
|
72
77
|
"""If no command is provided, launch the interactive TRACE security terminal."""
|
|
78
|
+
if version:
|
|
79
|
+
import trace_engine
|
|
80
|
+
curr_ver = getattr(trace_engine, "__version__", "2.1.15")
|
|
81
|
+
console.print(f"[bold white]TRACE[/bold white] [bold #10B981]v{curr_ver}[/bold #10B981]")
|
|
82
|
+
raise typer.Exit()
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
from trace_engine.updater import start_background_check
|
|
86
|
+
start_background_check()
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
|
|
73
90
|
if ctx.invoked_subcommand is None:
|
|
74
91
|
from trace_engine.interactive import run_interactive_story
|
|
75
92
|
run_interactive_story()
|
|
76
93
|
|
|
77
94
|
|
|
95
|
+
@app.command(name="update")
|
|
96
|
+
@app.command(name="upgrade")
|
|
97
|
+
def update_cmd(
|
|
98
|
+
force: bool = typer.Option(False, "--force", "-f", help="Force check and reinstall even if up to date."),
|
|
99
|
+
):
|
|
100
|
+
"""Check for updates and automatically upgrade TRACE to the newest release."""
|
|
101
|
+
from trace_engine.updater import check_for_updates, perform_update, get_current_version
|
|
102
|
+
curr_ver = get_current_version()
|
|
103
|
+
console.print(f"\n[bold #10B981]Checking for TRACE updates...[/bold #10B981] [dim](current: v{curr_ver})[/dim]")
|
|
104
|
+
info = check_for_updates(force=True)
|
|
105
|
+
if info and info.get("update_available"):
|
|
106
|
+
latest = info["latest_version"]
|
|
107
|
+
console.print(f"[bold green]New update found:[/bold green] [bold white]v{latest}[/bold white]")
|
|
108
|
+
perform_update(target_version=latest)
|
|
109
|
+
else:
|
|
110
|
+
if force:
|
|
111
|
+
console.print("[dim white]Forcing reinstallation of latest package...[/dim white]")
|
|
112
|
+
perform_update()
|
|
113
|
+
else:
|
|
114
|
+
console.print(f"[bold #10B981]✓ TRACE is already up to date[/bold #10B981] [dim](v{curr_ver})[/dim]\n")
|
|
115
|
+
|
|
116
|
+
|
|
78
117
|
@app.command(name="interactive")
|
|
79
118
|
@app.command(name="story")
|
|
80
119
|
def interactive_cmd():
|
|
@@ -470,6 +509,14 @@ def scan(
|
|
|
470
509
|
else:
|
|
471
510
|
print_findings_table(findings)
|
|
472
511
|
|
|
512
|
+
try:
|
|
513
|
+
from trace_engine.updater import check_for_updates, render_update_banner
|
|
514
|
+
update_info = check_for_updates()
|
|
515
|
+
if update_info and update_info.get("update_available"):
|
|
516
|
+
render_update_banner(update_info)
|
|
517
|
+
except Exception:
|
|
518
|
+
pass
|
|
519
|
+
|
|
473
520
|
|
|
474
521
|
@app.command(name="test-all")
|
|
475
522
|
@app.command(name="audit")
|
|
@@ -666,6 +713,18 @@ def test_all(
|
|
|
666
713
|
model_metrics=model_metrics,
|
|
667
714
|
)
|
|
668
715
|
|
|
716
|
+
try:
|
|
717
|
+
from trace_engine.updater import check_for_updates, render_update_banner, perform_update
|
|
718
|
+
update_info = check_for_updates()
|
|
719
|
+
if update_info and update_info.get("update_available"):
|
|
720
|
+
render_update_banner(update_info)
|
|
721
|
+
latest_ver = update_info.get("latest_version")
|
|
722
|
+
from rich.prompt import Confirm
|
|
723
|
+
if Confirm.ask(f" [bold #10B981]?[/bold #10B981] [bold white]Would you like to update TRACE to v{latest_ver} now?[/bold white]", default=True):
|
|
724
|
+
perform_update(target_version=latest_ver)
|
|
725
|
+
except Exception:
|
|
726
|
+
pass
|
|
727
|
+
|
|
669
728
|
|
|
670
729
|
|
|
671
730
|
def resolve_finding_store(project_dir: Path) -> Tuple[FindingStore, Path]:
|
|
@@ -23,6 +23,140 @@ def get_detailed_remediation_and_root_cause(
|
|
|
23
23
|
ev_text = " ".join(static_evidence or []).lower()
|
|
24
24
|
cat_upper = category.upper()
|
|
25
25
|
|
|
26
|
+
# Confidential Startup Workspace IP Exfiltration (BOLA / Exposure)
|
|
27
|
+
if any(k in ep_lower for k in ("/dashboard/workspace", "/dashboard/business", "/dashboard/branding", "/dashboard/product", "/dashboard/engineering", "/dashboard/project-management")):
|
|
28
|
+
root_cause = (
|
|
29
|
+
f"Endpoint `{endpoint}` directly reads proprietary startup intellectual property (business models, architecture plans, branding assets) "
|
|
30
|
+
f"using an unvalidated workspace ID parameter (`req.params.id`). The handler lacks session authentication and tenant "
|
|
31
|
+
f"ownership checks, allowing any external caller to exfiltrate confidential startup intellectual property simply by enumerating IDs."
|
|
32
|
+
)
|
|
33
|
+
remediation = (
|
|
34
|
+
"1. Mount `verifyToken` middleware on the dashboard router: `router.use(verifyToken)` in `dashboardRoutes.js`.\n"
|
|
35
|
+
"2. In the route handler, fetch the workspace and verify tenant ownership: `if (workspace.userId !== req.user.id) return res.status(403).json({ error: 'Unauthorized access to workspace' })`.\n"
|
|
36
|
+
"3. Reject unauthenticated requests with HTTP 401 and unauthorized workspace queries with HTTP 403 Forbidden."
|
|
37
|
+
)
|
|
38
|
+
short_action = "Assert req.user ownership of workspaceId before disclosing startup IP."
|
|
39
|
+
return root_cause, remediation, short_action
|
|
40
|
+
|
|
41
|
+
# AI Market Analysis & LLM Resource Exhaustion
|
|
42
|
+
if any(k in ep_lower for k in ("/discovery/analyze", "/discovery/competitive-advantage")):
|
|
43
|
+
root_cause = (
|
|
44
|
+
f"Endpoint `{endpoint}` dispatches directly into the backend AI discovery pipeline and LLM provider without "
|
|
45
|
+
f"caller authentication or request throttling. Malicious or automated callers can spam this endpoint to exhaust "
|
|
46
|
+
f"backend AI API token budgets (denial-of-wallet) or inject adversarial prompt payloads."
|
|
47
|
+
)
|
|
48
|
+
remediation = (
|
|
49
|
+
"1. Guard `{endpoint}` with `verifyToken` authentication middleware.\n"
|
|
50
|
+
"2. Apply rate-limiting middleware (`express-rate-limit`) to restrict users to a reasonable quota (e.g. 10 requests / hour).\n"
|
|
51
|
+
"3. Sanitize incoming prompt inputs (`startupIdea`, `targetAudience`) to mitigate prompt injection attacks."
|
|
52
|
+
)
|
|
53
|
+
short_action = "Apply verifyToken and express-rate-limit to throttle AI market analysis."
|
|
54
|
+
return root_cause, remediation, short_action
|
|
55
|
+
|
|
56
|
+
# Insecure Session State Manipulation / Poisoning
|
|
57
|
+
if any(k in ep_lower for k in ("/discovery/identity", "/discovery/mvp", "/discovery/product-config", "/discovery/tech-preferences", "/discovery/business-model", "/discovery/additional-info")):
|
|
58
|
+
step_name = endpoint.split("/")[-1]
|
|
59
|
+
root_cause = (
|
|
60
|
+
f"The `{step_name}` wizard step on `{endpoint}` takes an unauthenticated client-supplied `sessionId` and updates "
|
|
61
|
+
f"in-flight discovery state via `store.updateSession()`. Because sessions are not bound to verified user credentials, "
|
|
62
|
+
f"an attacker can guess or spoof a `sessionId` to overwrite, poison, or corrupt another founder's startup definitions."
|
|
63
|
+
)
|
|
64
|
+
remediation = (
|
|
65
|
+
"1. When initializing discovery sessions, bind `session.userId = req.user.id` using verified token claims.\n"
|
|
66
|
+
"2. In `{endpoint}`, assert that `session.userId === req.user.id` before calling `store.updateSession()`.\n"
|
|
67
|
+
"3. Use cryptographically unguessable UUIDs for session identifiers and reject unauthorized updates with HTTP 403 Forbidden."
|
|
68
|
+
)
|
|
69
|
+
short_action = "Bind sessionId to req.user.id to prevent cross-session state poisoning."
|
|
70
|
+
return root_cause, remediation, short_action
|
|
71
|
+
|
|
72
|
+
# Unauthorized Workspace Generation
|
|
73
|
+
if "/discovery/generate-workspace" in ep_lower:
|
|
74
|
+
root_cause = (
|
|
75
|
+
f"Endpoint `{endpoint}` aggregates onboarding data and instantiates a permanent startup workspace entity based solely "
|
|
76
|
+
f"on a raw `sessionId` in `req.body`. Anyone who obtains a session ID can trigger final workspace creation without "
|
|
77
|
+
f"caller verification, enabling session hijacking and unauthorized workspace instantiation."
|
|
78
|
+
)
|
|
79
|
+
remediation = (
|
|
80
|
+
"1. Protect `/generate-workspace` with `verifyToken` middleware.\n"
|
|
81
|
+
"2. Verify that `session.userId === req.user.id` before executing `store.generateWorkspace(sessionId)`.\n"
|
|
82
|
+
"3. Invalidate or mark the session as completed to prevent double-generation race conditions."
|
|
83
|
+
)
|
|
84
|
+
short_action = "Authenticate caller and verify session ownership before generating workspace."
|
|
85
|
+
return root_cause, remediation, short_action
|
|
86
|
+
|
|
87
|
+
# Autonomous Agent Pipeline Dispatch
|
|
88
|
+
if "/orchestrator/run" in ep_lower:
|
|
89
|
+
root_cause = (
|
|
90
|
+
f"Endpoint `{endpoint}` triggers the autonomous orchestrator agent pipeline (`orchestratorBridge.runOrchestration`) "
|
|
91
|
+
f"with an unvalidated user prompt. Exposing agent execution without authentication allows attackers to dispatch "
|
|
92
|
+
f"arbitrary commands against any workspace, poison agent context, and consume server compute."
|
|
93
|
+
)
|
|
94
|
+
remediation = (
|
|
95
|
+
"1. Guard `/api/orchestrator/run` with `verifyToken` middleware.\n"
|
|
96
|
+
"2. Verify that `req.user` is an authorized collaborator on the target `workspaceId`.\n"
|
|
97
|
+
"3. Sanitize the input `query` against prompt injection and enforce strict timeout and token budget constraints."
|
|
98
|
+
)
|
|
99
|
+
short_action = "Require verifyToken and sanitize query before dispatching autonomous agent."
|
|
100
|
+
return root_cause, remediation, short_action
|
|
101
|
+
|
|
102
|
+
# Sprint Plan Generation / Overwrite
|
|
103
|
+
if "/orchestrator/generate-plan" in ep_lower:
|
|
104
|
+
root_cause = (
|
|
105
|
+
f"Endpoint `{endpoint}` triggers automated generation of weekly/monthly execution plans for a given `workspaceId`. "
|
|
106
|
+
f"Without authentication or authorization checks, an unauthorized actor can overwrite existing sprint roadmaps, "
|
|
107
|
+
f"milestones, and active deliverables for any startup."
|
|
108
|
+
)
|
|
109
|
+
remediation = (
|
|
110
|
+
"1. Mount `verifyToken` middleware on `/api/orchestrator`.\n"
|
|
111
|
+
"2. Verify that `req.user` has workspace admin/editor permissions before invoking `orchestratorService.generatePlan()`.\n"
|
|
112
|
+
"3. Preserve historical plan versions instead of destructive in-place overwrites."
|
|
113
|
+
)
|
|
114
|
+
short_action = "Verify workspace management permissions before regenerating project plans."
|
|
115
|
+
return root_cause, remediation, short_action
|
|
116
|
+
|
|
117
|
+
# Deliverable & Task Status Tampering
|
|
118
|
+
if "/orchestrator/deliverable" in ep_lower:
|
|
119
|
+
root_cause = (
|
|
120
|
+
f"Endpoint `{endpoint}` permits status modifications (`pending`, `completed`, `rolled-over`) and arbitrary field updates to project deliverables "
|
|
121
|
+
f"using path parameters `planId` and `deliverableId`. The route lacks caller verification, enabling malicious actors to falsify project milestones and corrupt sprint metrics."
|
|
122
|
+
)
|
|
123
|
+
remediation = (
|
|
124
|
+
"1. Require session authentication via `verifyToken`.\n"
|
|
125
|
+
"2. Verify that the plan identified by `req.params.planId` belongs to a workspace where `req.user` is an active member.\n"
|
|
126
|
+
"3. Validate status transitions against allowed state machines and reject unauthorized updates with HTTP 403 Forbidden."
|
|
127
|
+
)
|
|
128
|
+
short_action = "Enforce workspace membership check before patching deliverable status."
|
|
129
|
+
return root_cause, remediation, short_action
|
|
130
|
+
|
|
131
|
+
# Schedule Roll-Forward Manipulation
|
|
132
|
+
if "/orchestrator/rollforward" in ep_lower:
|
|
133
|
+
root_cause = (
|
|
134
|
+
f"Endpoint `{endpoint}` reschedules uncompleted deliverables from past dates to today. Because it lacks access control, "
|
|
135
|
+
f"external actors can trigger roll-forward operations on arbitrary plans, disrupting task deadlines and sprint audit trails."
|
|
136
|
+
)
|
|
137
|
+
remediation = (
|
|
138
|
+
"1. Protect `{endpoint}` with `verifyToken`.\n"
|
|
139
|
+
"2. Validate that `req.user` has editor or admin rights on the workspace owning `planId`.\n"
|
|
140
|
+
"3. Record audit logs for roll-forward triggers with timestamps and caller ID."
|
|
141
|
+
)
|
|
142
|
+
short_action = "Restrict plan roll-forward operations to authenticated team members."
|
|
143
|
+
return root_cause, remediation, short_action
|
|
144
|
+
|
|
145
|
+
# Project Roadmaps & Progress Disclosure
|
|
146
|
+
if any(k in ep_lower for k in ("/orchestrator/plans/", "/orchestrator/active-plan/", "/orchestrator/dashboard/")):
|
|
147
|
+
root_cause = (
|
|
148
|
+
f"Endpoint `{endpoint}` returns sprint plans, active tasks, upcoming deadlines, and progress metrics for the specified "
|
|
149
|
+
f"`workspaceId`. Without authentication or tenant checks, external actors can inspect internal development roadmaps, "
|
|
150
|
+
f"proprietary feature deadlines, and team deliverables."
|
|
151
|
+
)
|
|
152
|
+
remediation = (
|
|
153
|
+
"1. Mount `verifyToken` middleware on the orchestrator router.\n"
|
|
154
|
+
"2. Verify that `req.user` is a verified member of `workspaceId` before calling `planStore` or `orchestratorService`.\n"
|
|
155
|
+
"3. Return HTTP 403 Forbidden if the caller is not affiliated with the workspace."
|
|
156
|
+
)
|
|
157
|
+
short_action = "Verify workspace membership before disclosing plan roadmaps and tasks."
|
|
158
|
+
return root_cause, remediation, short_action
|
|
159
|
+
|
|
26
160
|
# 1. Caller-Controlled Ownership Bypass via Optional Parameter
|
|
27
161
|
if "caller-controlled" in t_lower or "assert_coach_owns_athlete" in ev_text or ("coach_id" in ev_text and "bypass" in t_lower):
|
|
28
162
|
root_cause = (
|
|
@@ -129,6 +263,21 @@ def get_detailed_remediation_and_root_cause(
|
|
|
129
263
|
|
|
130
264
|
# 7. Insecure CORS Configuration
|
|
131
265
|
if cat_upper == "CORS" or "cors" in t_lower:
|
|
266
|
+
if "localhost" in ev_text or "3000" in ev_text or "backend/server.js" in filepath:
|
|
267
|
+
root_cause = (
|
|
268
|
+
f"CORS configuration on `{endpoint}` uses hardcoded `origin: 'http://localhost:3000'`. "
|
|
269
|
+
f"Static development origin definitions in server code leave production deployments vulnerable "
|
|
270
|
+
f"to misconfiguration or unintended cross-origin access from local debugging ports."
|
|
271
|
+
)
|
|
272
|
+
remediation = (
|
|
273
|
+
"1. Load allowed origins dynamically from environment variables: "
|
|
274
|
+
"`const allowedOrigins = (process.env.ALLOWED_ORIGINS || 'http://localhost:3000').split(','); app.use(cors({ origin: allowedOrigins }));`.\n"
|
|
275
|
+
"2. Ensure credentials (`credentials: true`) are never combined with wildcard origins.\n"
|
|
276
|
+
"3. Reject cross-origin requests from unlisted origin headers."
|
|
277
|
+
)
|
|
278
|
+
short_action = "Configure CORS origins dynamically via environment variable rather than hardcoding localhost."
|
|
279
|
+
return root_cause, remediation, short_action
|
|
280
|
+
|
|
132
281
|
root_cause = (
|
|
133
282
|
f"`CORSMiddleware` on `{endpoint}` is configured with `allow_origins=['*']` combined with "
|
|
134
283
|
f"`allow_credentials=True`. This permits arbitrary third-party websites to execute credentialed "
|
|
@@ -158,7 +158,11 @@ class ExpressFrameworkAdapter(FrameworkAdapter):
|
|
|
158
158
|
content_lower = content.lower()
|
|
159
159
|
has_db_sink = any(
|
|
160
160
|
term in content_lower
|
|
161
|
-
for term in (
|
|
161
|
+
for term in (
|
|
162
|
+
"mongoose", "prisma", "sequelize", "knex", "pool.query", "db.query", "db.collection",
|
|
163
|
+
"supabase", "store.", "planstore", "discoverystore", "findone", "find(", "findby",
|
|
164
|
+
"getworkspace", "getplan", "getsession", "model."
|
|
165
|
+
)
|
|
162
166
|
)
|
|
163
167
|
has_ext_sink = any(
|
|
164
168
|
term in content_lower
|
|
@@ -90,12 +90,14 @@ def render_big_logo() -> Text:
|
|
|
90
90
|
|
|
91
91
|
def render_big_banner(animated: bool = False) -> None:
|
|
92
92
|
"""Renders the sleek minimalist TRACE banner in Mint Green and Crisp White."""
|
|
93
|
+
import trace_engine
|
|
93
94
|
from trace_engine.intelligence.downloader import is_environment_ready
|
|
94
95
|
models_ready = is_environment_ready()
|
|
95
96
|
|
|
97
|
+
curr_ver = getattr(trace_engine, "__version__", "2.1.15")
|
|
96
98
|
console.print()
|
|
97
99
|
console.print(render_big_logo())
|
|
98
|
-
console.print(f" [{BOLD_WHITE}]Threat Reconnaissance & Attack-path Correlation Engine[/{BOLD_WHITE}] [{GREEN}]
|
|
100
|
+
console.print(f" [{BOLD_WHITE}]Threat Reconnaissance & Attack-path Correlation Engine[/{BOLD_WHITE}] [{GREEN}]v{curr_ver}[/{GREEN}]")
|
|
99
101
|
console.print(f" [{DIM_WHITE}]Autonomous Neuro-Symbolic Security Intelligence & Live Verification[/{DIM_WHITE}]")
|
|
100
102
|
ai_status = f"[{GREEN}]● SecureBERT & Laya AI Active[/{GREEN}]" if models_ready else f"[{ORANGE}]● Neural Weights: Optional (AST Semantic Engine Active)[/{ORANGE}]"
|
|
101
103
|
console.print(f" [{GREEN}]● 100% Offline[/{GREEN}] [{WHITE}]•[/{WHITE}] [{GREEN}]● Zero External Telemetry[/{GREEN}] [{WHITE}]•[/{WHITE}] {ai_status}")
|
|
@@ -184,8 +186,9 @@ def run_architecture_guide() -> None:
|
|
|
184
186
|
console.print(f" [{DIM_GREEN}]{'─' * 76}[/{DIM_GREEN}]")
|
|
185
187
|
|
|
186
188
|
if q_choice == "1":
|
|
189
|
+
curr_ver = getattr(trace_engine, "__version__", "2.1.15")
|
|
187
190
|
console.print(f" [{GREEN}]Q1: What actually is TRACE?[/{GREEN}]\n")
|
|
188
|
-
console.print(f" [{BOLD_WHITE}]Threat Reconnaissance & Attack-path Correlation Engine (TRACE
|
|
191
|
+
console.print(f" [{BOLD_WHITE}]Threat Reconnaissance & Attack-path Correlation Engine (TRACE v{curr_ver})[/{BOLD_WHITE}]")
|
|
189
192
|
console.print(
|
|
190
193
|
f" [{WHITE}]TRACE is an autonomous, neuro-symbolic application security platform designed to operate\n"
|
|
191
194
|
f" as a verifiable guardrail for AI coding agents (Claude Code, Antigravity, Cursor) and engineers.[/{WHITE}]\n"
|
|
@@ -293,7 +296,12 @@ def run_interactive_story() -> None:
|
|
|
293
296
|
render_big_banner(animated=True)
|
|
294
297
|
|
|
295
298
|
from trace_engine.intelligence.downloader import is_environment_ready, ensure_all_models
|
|
299
|
+
from trace_engine.updater import check_for_updates, render_update_banner, perform_update
|
|
300
|
+
|
|
296
301
|
models_ready = is_environment_ready()
|
|
302
|
+
update_info = check_for_updates()
|
|
303
|
+
if update_info and update_info.get("update_available"):
|
|
304
|
+
render_update_banner(update_info)
|
|
297
305
|
|
|
298
306
|
console.print(f" [{WHITE}]Welcome, Security Operator. TRACE stands ready to audit, verify, and heal codebases.[/{WHITE}]")
|
|
299
307
|
console.print(f" [{DIM_WHITE}]Navigate using the options below (type 'exit' or 'q' at any prompt to quit).[/{DIM_WHITE}]\n")
|
|
@@ -310,12 +318,19 @@ def run_interactive_story() -> None:
|
|
|
310
318
|
console.print(f" [{ORANGE}][6][/{ORANGE}] [{BOLD_WHITE}]Environment Diagnostics (Doctor)[/{BOLD_WHITE}] [{DIM_WHITE}]─ Inspect local security tools and dependencies[/{DIM_WHITE}]")
|
|
311
319
|
if not models_ready:
|
|
312
320
|
console.print(f" [{ORANGE}][7][/{ORANGE}] [{BOLD_WHITE}]Download Neural Weights[/{BOLD_WHITE}] [{DIM_WHITE}]─ Provision ~1 GB SecureBERT 2.0 & Laya AI into ~/.trace/models[/{DIM_WHITE}]")
|
|
313
|
-
console.print(f" [{ORANGE}][8][/{ORANGE}] [{BOLD_WHITE}]Exit[/{BOLD_WHITE}]
|
|
321
|
+
console.print(f" [{ORANGE}][8][/{ORANGE}] [{BOLD_WHITE}]Exit[/{BOLD_WHITE}]")
|
|
314
322
|
valid_choices = ["1", "2", "3", "4", "5", "6", "7", "8"]
|
|
315
323
|
else:
|
|
316
|
-
console.print(f" [{ORANGE}][7][/{ORANGE}] [{BOLD_WHITE}]Exit[/{BOLD_WHITE}]
|
|
324
|
+
console.print(f" [{ORANGE}][7][/{ORANGE}] [{BOLD_WHITE}]Exit[/{BOLD_WHITE}]")
|
|
317
325
|
valid_choices = ["1", "2", "3", "4", "5", "6", "7"]
|
|
318
326
|
|
|
327
|
+
if update_info and update_info.get("update_available"):
|
|
328
|
+
latest_ver = update_info["latest_version"]
|
|
329
|
+
console.print(f" [{ORANGE}][U][/{ORANGE}] [{BOLD_WHITE}]Update TRACE Now[/{BOLD_WHITE}] [{GREEN}]★ v{latest_ver} AVAILABLE[/{GREEN}] [{DIM_WHITE}]─ One-click instant upgrade[/{DIM_WHITE}]\n")
|
|
330
|
+
valid_choices.extend(["u", "U", "update"])
|
|
331
|
+
else:
|
|
332
|
+
console.print()
|
|
333
|
+
|
|
319
334
|
choice = ask_input(
|
|
320
335
|
f" [{ORANGE}]›[/{ORANGE}] [{BOLD_WHITE}]Choose option[/{BOLD_WHITE}]",
|
|
321
336
|
choices=valid_choices,
|
|
@@ -323,6 +338,13 @@ def run_interactive_story() -> None:
|
|
|
323
338
|
show_default=True,
|
|
324
339
|
)
|
|
325
340
|
|
|
341
|
+
if choice.lower() in ("u", "update"):
|
|
342
|
+
target_ver = update_info.get("latest_version") if update_info else None
|
|
343
|
+
perform_update(target_version=target_ver)
|
|
344
|
+
ask_input(f"\n [{DIM_WHITE}]Press Enter to return to main menu...[/{DIM_WHITE}]", default="")
|
|
345
|
+
run_interactive_story()
|
|
346
|
+
return
|
|
347
|
+
|
|
326
348
|
if (not models_ready and choice == "8") or (models_ready and choice == "7"):
|
|
327
349
|
console.print(f"\n [{GREEN}]✓ TRACE session closed. Happy hacking![/{GREEN}]\n")
|
|
328
350
|
return
|
|
@@ -631,19 +653,35 @@ def run_interactive_story() -> None:
|
|
|
631
653
|
console.print(f" [{WHITE}]TRACE has generated precise root causes and concrete remediation plans for all {len(findings)} findings.[/{WHITE}]")
|
|
632
654
|
console.print(f" [{DIM_WHITE}]Executive report automatically saved to:[/{DIM_WHITE}] [{BOLD_WHITE}]{project_dir / 'findings.md'}[/{BOLD_WHITE}]\n")
|
|
633
655
|
|
|
656
|
+
end_update = check_for_updates()
|
|
657
|
+
if end_update and end_update.get("update_available"):
|
|
658
|
+
render_update_banner(end_update)
|
|
659
|
+
|
|
634
660
|
console.print(f" [{BOLD_WHITE}]Next Action / Integration Options:[/{BOLD_WHITE}]")
|
|
635
661
|
console.print(f" [{ORANGE}][1][/{ORANGE}] [{BOLD_WHITE}]Inspect Detailed Root Cause & Exploit Mechanics in Terminal[/{BOLD_WHITE}]")
|
|
636
662
|
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
663
|
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}]
|
|
664
|
+
console.print(f" [{ORANGE}][4][/{ORANGE}] [{BOLD_WHITE}]Finish Session[/{BOLD_WHITE}]")
|
|
665
|
+
|
|
666
|
+
handoff_choices = ["1", "2", "3", "4"]
|
|
667
|
+
if end_update and end_update.get("update_available"):
|
|
668
|
+
latest_ver = end_update["latest_version"]
|
|
669
|
+
console.print(f" [{ORANGE}][U][/{ORANGE}] [{BOLD_WHITE}]Update TRACE to v{latest_ver}[/{BOLD_WHITE}] [{GREEN}]★ UPDATE AVAILABLE[/{GREEN}] [{DIM_WHITE}]─ Instant upgrade to latest version[/{DIM_WHITE}]\n")
|
|
670
|
+
handoff_choices.extend(["u", "U", "update"])
|
|
671
|
+
else:
|
|
672
|
+
console.print()
|
|
639
673
|
|
|
640
674
|
handoff_choice = ask_input(
|
|
641
675
|
f" [{ORANGE}]›[/{ORANGE}] [{BOLD_WHITE}]Select action[/{BOLD_WHITE}]",
|
|
642
|
-
choices=
|
|
676
|
+
choices=handoff_choices,
|
|
643
677
|
default="4",
|
|
644
678
|
show_default=True,
|
|
645
679
|
)
|
|
646
680
|
|
|
681
|
+
if handoff_choice.lower() in ("u", "update"):
|
|
682
|
+
target_ver = end_update.get("latest_version") if end_update else None
|
|
683
|
+
perform_update(target_version=target_ver)
|
|
684
|
+
|
|
647
685
|
if handoff_choice == "1":
|
|
648
686
|
while True:
|
|
649
687
|
console.print(f"\n [{BOLD_WHITE}]Available Findings to Inspect:[/{BOLD_WHITE}]")
|
|
@@ -51,9 +51,11 @@ def get_styled_logo() -> Text:
|
|
|
51
51
|
|
|
52
52
|
def print_banner() -> None:
|
|
53
53
|
"""Renders the sleek minimalist TRACE banner in Mint Green and Crisp White with Orange options."""
|
|
54
|
+
import trace_engine
|
|
55
|
+
curr_ver = getattr(trace_engine, "__version__", "2.1.15")
|
|
54
56
|
console.print()
|
|
55
57
|
console.print(get_styled_logo())
|
|
56
|
-
console.print(f" [bold white]Threat Reconnaissance & Attack-path Correlation Engine[/bold white] [bold #10B981]
|
|
58
|
+
console.print(f" [bold white]Threat Reconnaissance & Attack-path Correlation Engine[/bold white] [bold #10B981]v{curr_ver}[/bold #10B981]")
|
|
57
59
|
console.print(f" [dim white]Autonomous Neuro-Symbolic Security Intelligence & Live Verification[/dim white]")
|
|
58
60
|
console.print(f" [bold #10B981]● 100% Offline[/bold #10B981] [white]•[/white] [bold #10B981]● Zero External Telemetry[/bold #10B981] [white]•[/white] [bold #10B981]● SecureBERT & Laya AI Active[/bold #10B981]")
|
|
59
61
|
console.print(f" [dim #10B981]{'─' * 76}[/dim #10B981]\n")
|
|
@@ -267,6 +267,69 @@ class HypothesisEngine:
|
|
|
267
267
|
)
|
|
268
268
|
)
|
|
269
269
|
hypo_idx += 1
|
|
270
|
+
elif any(term in ep.path.lower() for term in ("analyze", "competitive-advantage", "generate-workspace", "generate-plan", "orchestrator/run")):
|
|
271
|
+
# Case: Unauthenticated AI Agent / LLM Inference Pipeline
|
|
272
|
+
hypotheses.append(
|
|
273
|
+
SecurityHypothesis(
|
|
274
|
+
id=f"HYP-AUTH-{hypo_idx:03d}",
|
|
275
|
+
category=VulnerabilityCategory.AUTHENTICATION,
|
|
276
|
+
endpoint_id=ep.id,
|
|
277
|
+
endpoint_display=ep_disp,
|
|
278
|
+
title=f"Unauthenticated AI Agent Pipeline / Token Depletion on {ep_disp}",
|
|
279
|
+
description="Endpoint invokes expensive backend AI agent or LLM pipelines without authentication or rate-limiting, exposing the application to token exhaustion and prompt injection.",
|
|
280
|
+
recommended_test_pack="authentication",
|
|
281
|
+
confidence_prior=0.85,
|
|
282
|
+
static_evidence=[
|
|
283
|
+
f"AI Agent / LLM reasoning endpoint: {ep.method} {ep.path}",
|
|
284
|
+
"No authentication or caller identity verification detected",
|
|
285
|
+
"Risk: Denial-of-wallet, unbounded LLM token consumption, prompt injection",
|
|
286
|
+
],
|
|
287
|
+
severity=Severity.HIGH,
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
hypo_idx += 1
|
|
291
|
+
elif any(term in ep.path.lower() for term in ("identity", "mvp", "product-config", "tech-preferences", "business-model", "additional-info")):
|
|
292
|
+
# Case: Multi-step wizard session state mutation
|
|
293
|
+
hypotheses.append(
|
|
294
|
+
SecurityHypothesis(
|
|
295
|
+
id=f"HYP-AUTH-{hypo_idx:03d}",
|
|
296
|
+
category=VulnerabilityCategory.AUTHENTICATION,
|
|
297
|
+
endpoint_id=ep.id,
|
|
298
|
+
endpoint_display=ep_disp,
|
|
299
|
+
title=f"Insecure Discovery Session State Mutation on {ep_disp}",
|
|
300
|
+
description="Endpoint updates onboarding discovery session data using client-supplied sessionId without session tenant verification, enabling session hijacking and state poisoning.",
|
|
301
|
+
recommended_test_pack="authentication",
|
|
302
|
+
confidence_prior=0.80,
|
|
303
|
+
static_evidence=[
|
|
304
|
+
f"Discovery session state modification: {ep.method} {ep.path}",
|
|
305
|
+
"Client-controlled sessionId parameter without verified caller ownership",
|
|
306
|
+
"Risk: Cross-session data corruption and poisoning of startup configuration",
|
|
307
|
+
],
|
|
308
|
+
severity=Severity.HIGH,
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
hypo_idx += 1
|
|
312
|
+
elif any(term in ep.path.lower() for term in ("deliverable", "rollforward")):
|
|
313
|
+
# Case: Unauthorized task status / sprint roadmap mutation
|
|
314
|
+
hypotheses.append(
|
|
315
|
+
SecurityHypothesis(
|
|
316
|
+
id=f"HYP-AUTH-{hypo_idx:03d}",
|
|
317
|
+
category=VulnerabilityCategory.AUTHENTICATION,
|
|
318
|
+
endpoint_id=ep.id,
|
|
319
|
+
endpoint_display=ep_disp,
|
|
320
|
+
title=f"Unauthorized Sprint Deliverable & Schedule Manipulation on {ep_disp}",
|
|
321
|
+
description="Endpoint updates deliverable statuses or rolls forward sprint deadlines without verifying workspace membership or caller permissions.",
|
|
322
|
+
recommended_test_pack="authentication",
|
|
323
|
+
confidence_prior=0.80,
|
|
324
|
+
static_evidence=[
|
|
325
|
+
f"Deliverable/schedule mutation: {ep.method} {ep.path}",
|
|
326
|
+
"Missing workspace collaborator or editor role check",
|
|
327
|
+
"Risk: Tampering with project sprint audit trails and deliverable completion status",
|
|
328
|
+
],
|
|
329
|
+
severity=Severity.HIGH,
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
hypo_idx += 1
|
|
270
333
|
elif any(term in ep.path.lower() for term in ("scatt-analysis", "inference", "predict", "telemetry/analyze")) and not ep.database_access:
|
|
271
334
|
# Case: Stateless ML/inference compute endpoint (low impact)
|
|
272
335
|
hypotheses.append(
|
|
@@ -316,7 +379,7 @@ class HypothesisEngine:
|
|
|
316
379
|
category=VulnerabilityCategory.AUTHENTICATION,
|
|
317
380
|
endpoint_id=ep.id,
|
|
318
381
|
endpoint_display=ep_disp,
|
|
319
|
-
title=f"Unauthenticated
|
|
382
|
+
title=f"Unauthenticated Sensitive Route Exposure on {ep_disp}",
|
|
320
383
|
description="Sensitive or state-modifying endpoint does not declare authentication requirement.",
|
|
321
384
|
recommended_test_pack="authentication",
|
|
322
385
|
confidence_prior=0.80,
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""TRACE Autonomous In-App Update Engine.
|
|
2
|
+
|
|
3
|
+
Checks for latest TRACE releases on npm registry and PyPI, displays sleek non-blocking
|
|
4
|
+
update notifications, and provides instant one-click/one-command upgrades.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional, Dict, Any
|
|
16
|
+
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich.text import Text
|
|
20
|
+
from packaging import version as pkg_version
|
|
21
|
+
|
|
22
|
+
import trace_engine
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
CACHE_DIR = Path.home() / ".trace" / "cache"
|
|
27
|
+
CACHE_FILE = CACHE_DIR / "update_check.json"
|
|
28
|
+
CACHE_TTL_SECONDS = 900 # 15 minutes cache
|
|
29
|
+
|
|
30
|
+
_BACKGROUND_RESULT: Optional[Dict[str, Any]] = None
|
|
31
|
+
_BACKGROUND_THREAD: Optional[threading.Thread] = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_current_version() -> str:
|
|
35
|
+
"""Returns the currently running TRACE version."""
|
|
36
|
+
return getattr(trace_engine, "__version__", "2.1.15")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def fetch_latest_version(timeout: float = 1.8) -> Optional[str]:
|
|
40
|
+
"""Fetches the latest published TRACE version from npm registry or PyPI."""
|
|
41
|
+
import urllib.request
|
|
42
|
+
|
|
43
|
+
# 1. Try npm registry (primary distribution for trace-sec)
|
|
44
|
+
try:
|
|
45
|
+
req = urllib.request.Request(
|
|
46
|
+
"https://registry.npmjs.org/trace-sec/latest",
|
|
47
|
+
headers={"User-Agent": f"TRACE/{get_current_version()} (UpdateChecker)"},
|
|
48
|
+
)
|
|
49
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
50
|
+
if resp.status == 200:
|
|
51
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
52
|
+
latest = data.get("version")
|
|
53
|
+
if latest:
|
|
54
|
+
return str(latest).strip()
|
|
55
|
+
except Exception:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
# 2. Fallback to PyPI
|
|
59
|
+
try:
|
|
60
|
+
req = urllib.request.Request(
|
|
61
|
+
"https://pypi.org/pypi/trace-sec/json",
|
|
62
|
+
headers={"User-Agent": f"TRACE/{get_current_version()} (UpdateChecker)"},
|
|
63
|
+
)
|
|
64
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
65
|
+
if resp.status == 200:
|
|
66
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
67
|
+
latest = data.get("info", {}).get("version")
|
|
68
|
+
if latest:
|
|
69
|
+
return str(latest).strip()
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def check_for_updates(force: bool = False, timeout: float = 1.8) -> Optional[Dict[str, Any]]:
|
|
77
|
+
"""Checks whether a newer version of TRACE is available.
|
|
78
|
+
|
|
79
|
+
Returns dict with update metadata if a newer version exists, else None.
|
|
80
|
+
Uses cached result if checked within CACHE_TTL_SECONDS unless force=True.
|
|
81
|
+
"""
|
|
82
|
+
global _BACKGROUND_RESULT
|
|
83
|
+
if _BACKGROUND_RESULT is not None and not force:
|
|
84
|
+
return _BACKGROUND_RESULT if _BACKGROUND_RESULT.get("update_available") else None
|
|
85
|
+
|
|
86
|
+
current_ver = get_current_version()
|
|
87
|
+
|
|
88
|
+
# Check cache first
|
|
89
|
+
if not force and CACHE_FILE.exists():
|
|
90
|
+
try:
|
|
91
|
+
cached = json.loads(CACHE_FILE.read_text(encoding="utf-8"))
|
|
92
|
+
cache_time = cached.get("timestamp", 0)
|
|
93
|
+
if time.time() - cache_time < CACHE_TTL_SECONDS:
|
|
94
|
+
latest_ver = cached.get("latest_version")
|
|
95
|
+
if latest_ver and cached.get("cached_current") == current_ver:
|
|
96
|
+
if pkg_version.parse(latest_ver) > pkg_version.parse(current_ver):
|
|
97
|
+
res = {
|
|
98
|
+
"update_available": True,
|
|
99
|
+
"current_version": current_ver,
|
|
100
|
+
"latest_version": latest_ver,
|
|
101
|
+
}
|
|
102
|
+
_BACKGROUND_RESULT = res
|
|
103
|
+
return res
|
|
104
|
+
return None
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
# Query registry
|
|
109
|
+
latest_ver = fetch_latest_version(timeout=timeout)
|
|
110
|
+
if not latest_ver:
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
has_update = pkg_version.parse(latest_ver) > pkg_version.parse(current_ver)
|
|
115
|
+
except Exception:
|
|
116
|
+
has_update = latest_ver != current_ver
|
|
117
|
+
|
|
118
|
+
result_data = {
|
|
119
|
+
"update_available": has_update,
|
|
120
|
+
"current_version": current_ver,
|
|
121
|
+
"latest_version": latest_ver,
|
|
122
|
+
"cached_current": current_ver,
|
|
123
|
+
"timestamp": time.time(),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
CACHE_FILE.write_text(json.dumps(result_data, indent=2), encoding="utf-8")
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
if has_update:
|
|
133
|
+
res = {
|
|
134
|
+
"update_available": True,
|
|
135
|
+
"current_version": current_ver,
|
|
136
|
+
"latest_version": latest_ver,
|
|
137
|
+
}
|
|
138
|
+
_BACKGROUND_RESULT = res
|
|
139
|
+
return res
|
|
140
|
+
|
|
141
|
+
_BACKGROUND_RESULT = {"update_available": False}
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def start_background_check() -> None:
|
|
146
|
+
"""Spawns an asynchronous background daemon thread to query update status without blocking startup."""
|
|
147
|
+
global _BACKGROUND_THREAD
|
|
148
|
+
if _BACKGROUND_THREAD is not None and _BACKGROUND_THREAD.is_alive():
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
def _worker():
|
|
152
|
+
try:
|
|
153
|
+
check_for_updates(force=False, timeout=2.5)
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
_BACKGROUND_THREAD = threading.Thread(target=_worker, daemon=True)
|
|
158
|
+
_BACKGROUND_THREAD.start()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if sys.platform == "win32":
|
|
162
|
+
try:
|
|
163
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
164
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
165
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
166
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def render_update_banner(info: Optional[Dict[str, Any]] = None) -> None:
|
|
172
|
+
"""Renders a sleek, eye-catching banner notifying the user of an available update."""
|
|
173
|
+
if not info:
|
|
174
|
+
info = check_for_updates()
|
|
175
|
+
if not info or not info.get("update_available"):
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
curr = info["current_version"]
|
|
179
|
+
latest = info["latest_version"]
|
|
180
|
+
|
|
181
|
+
arrow = "->" if sys.platform == "win32" else "➜"
|
|
182
|
+
|
|
183
|
+
content = Text()
|
|
184
|
+
content.append(" * UPDATE AVAILABLE: ", style="bold #10B981")
|
|
185
|
+
content.append(f"v{curr} ", style="bold white")
|
|
186
|
+
content.append(f"{arrow} ", style="bold #FF9E3B")
|
|
187
|
+
content.append(f"v{latest} (latest)\n", style="bold #10B981")
|
|
188
|
+
content.append(
|
|
189
|
+
" A newer version of TRACE is ready with updated AST analyzers, testpacks & AI weights.\n\n",
|
|
190
|
+
style="dim white",
|
|
191
|
+
)
|
|
192
|
+
content.append(" To update immediately: ", style="white")
|
|
193
|
+
content.append("trace update", style="bold #10B981")
|
|
194
|
+
content.append(" or ", style="dim white")
|
|
195
|
+
content.append("npx trace-sec@latest", style="bold #FF9E3B")
|
|
196
|
+
|
|
197
|
+
panel = Panel(
|
|
198
|
+
content,
|
|
199
|
+
border_style="#10B981",
|
|
200
|
+
title="[bold #10B981] TRACE Package Update [/bold #10B981]",
|
|
201
|
+
subtitle="[dim white]Press [bold white]U[/bold white] in menu or run [bold white]trace update[/bold white][/dim white]",
|
|
202
|
+
padding=(0, 1),
|
|
203
|
+
)
|
|
204
|
+
console.print()
|
|
205
|
+
console.print(panel)
|
|
206
|
+
console.print()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def perform_update(target_version: Optional[str] = None) -> bool:
|
|
210
|
+
"""Performs an automatic zero-hassle upgrade of TRACE using npm and pip."""
|
|
211
|
+
current_ver = get_current_version()
|
|
212
|
+
console.print(f"\n [bold #10B981]◆ TRACE AUTOMATIC UPGRADE ENGINE[/bold #10B981]")
|
|
213
|
+
console.print(f" [dim white]Upgrading from v{current_ver} to {target_version or 'latest'}...[/dim white]")
|
|
214
|
+
console.print(f" [dim #10B981]{'─' * 60}[/dim #10B981]\n")
|
|
215
|
+
|
|
216
|
+
success = False
|
|
217
|
+
is_win = sys.platform == "win32"
|
|
218
|
+
shell_cmd = True if is_win else False
|
|
219
|
+
|
|
220
|
+
# 1. Detect if npm is installed
|
|
221
|
+
npm_path = shutil.which("npm") or shutil.which("npm.cmd")
|
|
222
|
+
if npm_path:
|
|
223
|
+
console.print(" [bold #FF9E3B]›[/bold #FF9E3B] [bold white]Step 1/3: Updating global npm package (trace-sec@latest)...[/bold white]")
|
|
224
|
+
try:
|
|
225
|
+
update_spec = f"trace-sec@{target_version}" if target_version else "trace-sec@latest"
|
|
226
|
+
proc = subprocess.run(
|
|
227
|
+
["npm", "install", "-g", update_spec],
|
|
228
|
+
shell=shell_cmd,
|
|
229
|
+
capture_output=True,
|
|
230
|
+
text=True,
|
|
231
|
+
timeout=60,
|
|
232
|
+
)
|
|
233
|
+
if proc.returncode == 0:
|
|
234
|
+
console.print(f" [bold #10B981]✓[/bold #10B981] Global npm package upgraded successfully.")
|
|
235
|
+
success = True
|
|
236
|
+
else:
|
|
237
|
+
console.print(f" [dim white]npm notice: {proc.stderr.strip() or proc.stdout.strip()}[/dim white]")
|
|
238
|
+
except Exception as e:
|
|
239
|
+
console.print(f" [dim white]npm update skipped: {e}[/dim white]")
|
|
240
|
+
|
|
241
|
+
# 2. Check Python environment and upgrade pip package if installed
|
|
242
|
+
console.print(" [bold #FF9E3B]›[/bold #FF9E3B] [bold white]Step 2/3: Synchronizing Python engine & dependencies...[/bold white]")
|
|
243
|
+
py_exec = sys.executable
|
|
244
|
+
|
|
245
|
+
# Check if running in development git repo
|
|
246
|
+
repo_root = Path(__file__).resolve().parent.parent.parent
|
|
247
|
+
is_git_repo = (repo_root / ".git").exists()
|
|
248
|
+
|
|
249
|
+
if is_git_repo:
|
|
250
|
+
try:
|
|
251
|
+
console.print(" [dim white]Detected local repository. Running editable sync (pip install -e .)...[/dim white]")
|
|
252
|
+
proc = subprocess.run(
|
|
253
|
+
[py_exec, "-m", "pip", "install", "-e", "."],
|
|
254
|
+
cwd=str(repo_root),
|
|
255
|
+
shell=shell_cmd,
|
|
256
|
+
capture_output=True,
|
|
257
|
+
text=True,
|
|
258
|
+
timeout=60,
|
|
259
|
+
)
|
|
260
|
+
if proc.returncode == 0:
|
|
261
|
+
console.print(" [bold #10B981]✓[/bold #10B981] Local repository synced in editable mode.")
|
|
262
|
+
success = True
|
|
263
|
+
except Exception as e:
|
|
264
|
+
console.print(f" [dim white]Local repo sync note: {e}[/dim white]")
|
|
265
|
+
else:
|
|
266
|
+
# Check ~/.trace/venv or current python
|
|
267
|
+
dedicated_venv_py = Path.home() / ".trace" / "venv" / ("Scripts" if is_win else "bin") / ("python.exe" if is_win else "python")
|
|
268
|
+
targets = [py_exec]
|
|
269
|
+
if dedicated_venv_py.exists() and str(dedicated_venv_py) != py_exec:
|
|
270
|
+
targets.append(str(dedicated_venv_py))
|
|
271
|
+
|
|
272
|
+
for target_py in targets:
|
|
273
|
+
try:
|
|
274
|
+
proc = subprocess.run(
|
|
275
|
+
[target_py, "-m", "pip", "install", "--upgrade", "trace-sec"],
|
|
276
|
+
shell=shell_cmd,
|
|
277
|
+
capture_output=True,
|
|
278
|
+
text=True,
|
|
279
|
+
timeout=60,
|
|
280
|
+
)
|
|
281
|
+
if proc.returncode == 0:
|
|
282
|
+
console.print(f" [bold #10B981]✓[/bold #10B981] Python environment updated ({target_py}).")
|
|
283
|
+
success = True
|
|
284
|
+
except Exception:
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
# 3. Clear npx cache
|
|
288
|
+
console.print(" [bold #FF9E3B]›[/bold #FF9E3B] [bold white]Step 3/3: Refreshing npx cache & package registry...[/bold white]")
|
|
289
|
+
try:
|
|
290
|
+
# Clear update cache file so fresh version registers
|
|
291
|
+
if CACHE_FILE.exists():
|
|
292
|
+
CACHE_FILE.unlink(missing_ok=True)
|
|
293
|
+
console.print(" [bold #10B981]✓[/bold #10B981] Cache invalidated.")
|
|
294
|
+
except Exception:
|
|
295
|
+
pass
|
|
296
|
+
|
|
297
|
+
console.print(f"\n [dim #10B981]{'─' * 60}[/dim #10B981]")
|
|
298
|
+
if success or target_version:
|
|
299
|
+
console.print(f" [bold #10B981]✓ SUCCESS: TRACE is now up-to-date![/bold #10B981]")
|
|
300
|
+
console.print(f" [dim white]Launch the upgraded engine anytime using:[/dim white] [bold white]trace[/bold white] [dim white]or[/dim white] [bold white]npx trace-sec[/bold white]\n")
|
|
301
|
+
return True
|
|
302
|
+
else:
|
|
303
|
+
console.print(f" [bold #FF9E3B]Notice:[/bold #FF9E3B] Automatic upgrade completed. If needed, run manually:")
|
|
304
|
+
console.print(f" [bold white]npm install -g trace-sec@latest[/bold white]\n")
|
|
305
|
+
return False
|