codedd-cli 0.1.0__py3-none-any.whl
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.
- codedd_cli/__init__.py +3 -0
- codedd_cli/__main__.py +19 -0
- codedd_cli/api/__init__.py +4 -0
- codedd_cli/api/client.py +118 -0
- codedd_cli/api/endpoints.py +44 -0
- codedd_cli/api/exceptions.py +25 -0
- codedd_cli/auditor/__init__.py +6 -0
- codedd_cli/auditor/architecture_analyzer.py +1241 -0
- codedd_cli/auditor/architecture_prompts.py +171 -0
- codedd_cli/auditor/complexity_analyzer.py +942 -0
- codedd_cli/auditor/dependency_scanner.py +2478 -0
- codedd_cli/auditor/file_auditor.py +572 -0
- codedd_cli/auditor/git_stats_collector.py +332 -0
- codedd_cli/auditor/response_parser.py +487 -0
- codedd_cli/auditor/vulnerability_validator.py +324 -0
- codedd_cli/auth/__init__.py +4 -0
- codedd_cli/auth/session.py +41 -0
- codedd_cli/auth/token_manager.py +87 -0
- codedd_cli/cli.py +69 -0
- codedd_cli/commands/__init__.py +1 -0
- codedd_cli/commands/audit_cmd.py +1877 -0
- codedd_cli/commands/audits_cmd.py +273 -0
- codedd_cli/commands/auth_cmd.py +230 -0
- codedd_cli/commands/config_cmd.py +454 -0
- codedd_cli/commands/scope_cmd.py +967 -0
- codedd_cli/config/__init__.py +4 -0
- codedd_cli/config/constants.py +22 -0
- codedd_cli/config/settings.py +369 -0
- codedd_cli/llm/__init__.py +1 -0
- codedd_cli/llm/key_manager.py +271 -0
- codedd_cli/models/__init__.py +5 -0
- codedd_cli/models/account.py +13 -0
- codedd_cli/models/audit.py +32 -0
- codedd_cli/models/local_directory.py +26 -0
- codedd_cli/scanner/__init__.py +18 -0
- codedd_cli/scanner/file_classifier.py +247 -0
- codedd_cli/scanner/file_walker.py +207 -0
- codedd_cli/scanner/line_counter.py +75 -0
- codedd_cli/utils/__init__.py +1 -0
- codedd_cli/utils/directory_validator.py +179 -0
- codedd_cli/utils/display.py +493 -0
- codedd_cli/utils/payload_inspector.py +180 -0
- codedd_cli/utils/security.py +14 -0
- codedd_cli/utils/validators.py +37 -0
- codedd_cli-0.1.0.dist-info/METADATA +276 -0
- codedd_cli-0.1.0.dist-info/RECORD +49 -0
- codedd_cli-0.1.0.dist-info/WHEEL +4 -0
- codedd_cli-0.1.0.dist-info/entry_points.txt +3 -0
- codedd_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1877 @@
|
|
|
1
|
+
"""
|
|
2
|
+
``codedd audit`` sub-commands: start.
|
|
3
|
+
|
|
4
|
+
Manages the audit lifecycle — pre-flight checks, payment, local file
|
|
5
|
+
auditing via LLM, and result submission to CodeDD.
|
|
6
|
+
|
|
7
|
+
Workflow:
|
|
8
|
+
1. ``codedd scope confirm`` – register scope with CodeDD
|
|
9
|
+
2. ``codedd audit start`` – auto-sync, pre-flight, pay/budget,
|
|
10
|
+
fetch plan, audit locally, submit results
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import time
|
|
16
|
+
import webbrowser
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import typer
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
from rich.progress import (
|
|
22
|
+
BarColumn,
|
|
23
|
+
MofNCompleteColumn,
|
|
24
|
+
Progress,
|
|
25
|
+
SpinnerColumn,
|
|
26
|
+
TextColumn,
|
|
27
|
+
)
|
|
28
|
+
from rich.prompt import Confirm
|
|
29
|
+
from rich.table import Table
|
|
30
|
+
|
|
31
|
+
from codedd_cli.api.client import CodeDDClient
|
|
32
|
+
from codedd_cli.api.endpoints import Endpoints
|
|
33
|
+
from codedd_cli.auditor.complexity_analyzer import (
|
|
34
|
+
FileComplexityResult,
|
|
35
|
+
LocalComplexityAnalyzer,
|
|
36
|
+
aggregate_complexity_results,
|
|
37
|
+
)
|
|
38
|
+
from codedd_cli.auditor.dependency_scanner import (
|
|
39
|
+
LocalDependencyScanner,
|
|
40
|
+
ManifestResult,
|
|
41
|
+
ImportResult,
|
|
42
|
+
)
|
|
43
|
+
from codedd_cli.auditor.file_auditor import AuditFileResult, LocalFileAuditor
|
|
44
|
+
from codedd_cli.auditor.vulnerability_validator import (
|
|
45
|
+
LocalVulnerabilityValidator,
|
|
46
|
+
ValidationCandidate,
|
|
47
|
+
)
|
|
48
|
+
from codedd_cli.auth.session import require_auth
|
|
49
|
+
from codedd_cli.config.settings import ConfigManager
|
|
50
|
+
from codedd_cli.llm.key_manager import PROVIDER_MODELS, LLMKeyManager
|
|
51
|
+
from codedd_cli.utils.display import (
|
|
52
|
+
STYLE_DEBUG_LOG,
|
|
53
|
+
SYMBOL_FAIL,
|
|
54
|
+
SYMBOL_INFO,
|
|
55
|
+
SYMBOL_OK,
|
|
56
|
+
SYMBOL_WARN,
|
|
57
|
+
print_error,
|
|
58
|
+
print_info,
|
|
59
|
+
print_success,
|
|
60
|
+
print_warning,
|
|
61
|
+
)
|
|
62
|
+
from codedd_cli.utils.payload_inspector import review_payload, review_request
|
|
63
|
+
|
|
64
|
+
console = Console()
|
|
65
|
+
audit_app = typer.Typer(no_args_is_help=True)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _require_active_audit(cfg: ConfigManager) -> None:
|
|
69
|
+
"""Abort if no audit has been selected."""
|
|
70
|
+
if not cfg.active_audit_uuid:
|
|
71
|
+
print_error(
|
|
72
|
+
"No active audit. Run [bold cyan]codedd audits select[/bold cyan] first."
|
|
73
|
+
)
|
|
74
|
+
raise typer.Exit(code=1)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# codedd audit start
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
@audit_app.command("start")
|
|
82
|
+
@require_auth
|
|
83
|
+
def start_audit(
|
|
84
|
+
skip_sync: bool = typer.Option(
|
|
85
|
+
False,
|
|
86
|
+
"--skip-sync",
|
|
87
|
+
help="Skip the automatic scope sync before starting.",
|
|
88
|
+
),
|
|
89
|
+
yes: bool = typer.Option(
|
|
90
|
+
False,
|
|
91
|
+
"--yes",
|
|
92
|
+
"-y",
|
|
93
|
+
help="Auto-confirm prompts (non-interactive mode).",
|
|
94
|
+
),
|
|
95
|
+
show: bool = typer.Option(
|
|
96
|
+
False,
|
|
97
|
+
"--show",
|
|
98
|
+
"-s",
|
|
99
|
+
help="Write each API request/payload to a file and open it for review before sending.",
|
|
100
|
+
),
|
|
101
|
+
debug_llm: bool = typer.Option(
|
|
102
|
+
False,
|
|
103
|
+
"--debug-llm",
|
|
104
|
+
help="Print full LLM prompt, raw response, and parsed result to the CLI (for debugging).",
|
|
105
|
+
),
|
|
106
|
+
debug_llm_full_prompt: bool = typer.Option(
|
|
107
|
+
False,
|
|
108
|
+
"--debug-llm-full-prompt",
|
|
109
|
+
help="Include full proprietary system prompt in --debug-llm output (sensitive).",
|
|
110
|
+
),
|
|
111
|
+
) -> None:
|
|
112
|
+
"""
|
|
113
|
+
Start the active audit on the CodeDD platform.
|
|
114
|
+
|
|
115
|
+
This command:
|
|
116
|
+
1. Auto-syncs local scope with CodeDD (detects file changes).
|
|
117
|
+
2. Runs pre-flight checks (status, payment, LoC budget).
|
|
118
|
+
3. Handles payment scenarios (budget deduction or Stripe checkout).
|
|
119
|
+
4. Enqueues the audit for processing.
|
|
120
|
+
"""
|
|
121
|
+
cfg = ConfigManager()
|
|
122
|
+
_require_active_audit(cfg)
|
|
123
|
+
|
|
124
|
+
audit_uuid = cfg.active_audit_uuid
|
|
125
|
+
audit_name = cfg.active_audit_name
|
|
126
|
+
audit_type = cfg.active_audit_type
|
|
127
|
+
|
|
128
|
+
console.print(
|
|
129
|
+
f"\n[bold]Starting audit:[/bold] {audit_name} [dim]({audit_type})[/dim]\n"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# -----------------------------------------------------------------------
|
|
133
|
+
# Phase 1 — Auto-sync (unless skipped)
|
|
134
|
+
# -----------------------------------------------------------------------
|
|
135
|
+
if not skip_sync:
|
|
136
|
+
sync_ok = _auto_sync(cfg, auto_confirm=yes, show=show)
|
|
137
|
+
if not sync_ok:
|
|
138
|
+
print_error("Scope sync failed or was cancelled. Audit not started.")
|
|
139
|
+
raise typer.Exit(code=1)
|
|
140
|
+
console.print()
|
|
141
|
+
|
|
142
|
+
# -----------------------------------------------------------------------
|
|
143
|
+
# Phase 1b — Verify LLM API key availability
|
|
144
|
+
# -----------------------------------------------------------------------
|
|
145
|
+
llm_ok = _check_llm_keys(cfg)
|
|
146
|
+
if not llm_ok:
|
|
147
|
+
raise typer.Exit(code=1)
|
|
148
|
+
console.print()
|
|
149
|
+
|
|
150
|
+
# -----------------------------------------------------------------------
|
|
151
|
+
# Phase 2 — Pre-flight check
|
|
152
|
+
# -----------------------------------------------------------------------
|
|
153
|
+
console.print("[dim]Pre-flight checks…[/dim]\n")
|
|
154
|
+
|
|
155
|
+
if show:
|
|
156
|
+
confirmed = review_request(
|
|
157
|
+
"GET",
|
|
158
|
+
Endpoints.AUDIT_CAN_START,
|
|
159
|
+
params={"audit_uuid": audit_uuid},
|
|
160
|
+
command_label="Audit Start — Pre-flight",
|
|
161
|
+
context_note="This request checks whether the audit can be started (payment, scope, etc.). Only the audit UUID is sent.",
|
|
162
|
+
confirm_prompt="Proceed with this request to CodeDD?",
|
|
163
|
+
)
|
|
164
|
+
if not confirmed:
|
|
165
|
+
print_info("Cancelled.")
|
|
166
|
+
raise typer.Exit(code=0)
|
|
167
|
+
|
|
168
|
+
with CodeDDClient(config=cfg) as client:
|
|
169
|
+
preflight = _fetch_preflight(client, audit_uuid)
|
|
170
|
+
|
|
171
|
+
if preflight is None:
|
|
172
|
+
raise typer.Exit(code=1)
|
|
173
|
+
|
|
174
|
+
# Server returned an error (e.g. 500 with status/message)
|
|
175
|
+
if preflight.get("status") == "error":
|
|
176
|
+
print_error(preflight.get("message", "Pre-flight check failed."))
|
|
177
|
+
raise typer.Exit(code=1)
|
|
178
|
+
|
|
179
|
+
# Legacy or alternate error shape
|
|
180
|
+
if "error" in preflight:
|
|
181
|
+
print_error(preflight["error"])
|
|
182
|
+
raise typer.Exit(code=1)
|
|
183
|
+
|
|
184
|
+
# Malformed or unexpected response
|
|
185
|
+
if "can_start" not in preflight:
|
|
186
|
+
print_error(
|
|
187
|
+
preflight.get("message", "Invalid pre-flight response from server.")
|
|
188
|
+
)
|
|
189
|
+
raise typer.Exit(code=1)
|
|
190
|
+
|
|
191
|
+
_render_preflight(preflight)
|
|
192
|
+
console.print()
|
|
193
|
+
|
|
194
|
+
# -----------------------------------------------------------------------
|
|
195
|
+
# Phase 3 — Decision tree
|
|
196
|
+
# -----------------------------------------------------------------------
|
|
197
|
+
# Use .get() so we don't KeyError when backend returns a minimal response
|
|
198
|
+
# (e.g. can_start=False, reason="Not all repositories have been scoped").
|
|
199
|
+
can_start = preflight.get("can_start", False)
|
|
200
|
+
is_paid = preflight.get("is_paid", False)
|
|
201
|
+
payment_required = preflight.get("payment_required", False)
|
|
202
|
+
loc_delta = preflight.get("loc_delta", 0)
|
|
203
|
+
loc_budget = preflight.get("loc_budget", 0)
|
|
204
|
+
total_loc = preflight.get("total_lines_of_code", 0)
|
|
205
|
+
|
|
206
|
+
use_budget = False
|
|
207
|
+
|
|
208
|
+
if can_start and is_paid and loc_delta == 0:
|
|
209
|
+
# Fully paid, no changes — good to go
|
|
210
|
+
print_success("Audit is fully paid. Ready to start.")
|
|
211
|
+
if not yes and not Confirm.ask("Start audit now?", default=True):
|
|
212
|
+
print_info("Audit start cancelled.")
|
|
213
|
+
raise typer.Exit()
|
|
214
|
+
|
|
215
|
+
elif can_start and is_paid and loc_delta > 0:
|
|
216
|
+
# Paid but scope grew, budget covers the delta
|
|
217
|
+
print_warning(
|
|
218
|
+
f"Scope increased by [bold]+{loc_delta:,}[/bold] LoC since payment.\n"
|
|
219
|
+
f" Your budget ([bold]{loc_budget:,}[/bold] LoC) can cover the difference."
|
|
220
|
+
)
|
|
221
|
+
if not yes and not Confirm.ask("Deduct from budget and start?", default=True):
|
|
222
|
+
print_info("Audit start cancelled.")
|
|
223
|
+
raise typer.Exit()
|
|
224
|
+
use_budget = True
|
|
225
|
+
|
|
226
|
+
elif can_start and not is_paid:
|
|
227
|
+
# Not paid, budget covers the full audit
|
|
228
|
+
print_info(
|
|
229
|
+
f"Using LoC budget: [bold]{total_loc:,}[/bold] LoC "
|
|
230
|
+
f"(budget: {loc_budget:,} LoC)."
|
|
231
|
+
)
|
|
232
|
+
if not yes and not Confirm.ask("Deduct from budget and start?", default=True):
|
|
233
|
+
print_info("Audit start cancelled.")
|
|
234
|
+
raise typer.Exit()
|
|
235
|
+
use_budget = True
|
|
236
|
+
|
|
237
|
+
elif payment_required and is_paid and loc_delta > 0:
|
|
238
|
+
# Paid but scope grew beyond budget — need additional payment
|
|
239
|
+
shortfall = loc_delta - loc_budget
|
|
240
|
+
print_warning(
|
|
241
|
+
f"Scope increased by [bold]+{loc_delta:,}[/bold] LoC since payment.\n"
|
|
242
|
+
f" Budget: {loc_budget:,} LoC | Shortfall: [bold red]{shortfall:,}[/bold red] LoC\n"
|
|
243
|
+
f" Additional payment is needed for the LoC difference."
|
|
244
|
+
)
|
|
245
|
+
if not Confirm.ask("Open payment page in your browser?", default=True):
|
|
246
|
+
print_info("Audit start cancelled. You can pay at [bold cyan]codedd.ai[/bold cyan].")
|
|
247
|
+
raise typer.Exit()
|
|
248
|
+
|
|
249
|
+
paid_ok = _handle_checkout(cfg, audit_uuid, loc_delta, show=show)
|
|
250
|
+
if not paid_ok:
|
|
251
|
+
raise typer.Exit(code=1)
|
|
252
|
+
|
|
253
|
+
elif payment_required and not is_paid:
|
|
254
|
+
# Not paid, budget insufficient — full payment needed
|
|
255
|
+
print_warning(
|
|
256
|
+
f"LoC budget insufficient: [bold]{loc_budget:,}[/bold] available, "
|
|
257
|
+
f"[bold]{total_loc:,}[/bold] needed.\n"
|
|
258
|
+
f" Payment required for this audit."
|
|
259
|
+
)
|
|
260
|
+
if not Confirm.ask("Open payment page in your browser?", default=True):
|
|
261
|
+
print_info("Audit start cancelled. You can pay at [bold cyan]codedd.ai[/bold cyan].")
|
|
262
|
+
raise typer.Exit()
|
|
263
|
+
|
|
264
|
+
paid_ok = _handle_checkout(cfg, audit_uuid, total_loc, show=show)
|
|
265
|
+
if not paid_ok:
|
|
266
|
+
raise typer.Exit(code=1)
|
|
267
|
+
|
|
268
|
+
else:
|
|
269
|
+
# Fallback — preflight said cannot start
|
|
270
|
+
print_error(preflight.get("reason", "Audit cannot be started."))
|
|
271
|
+
raise typer.Exit(code=1)
|
|
272
|
+
|
|
273
|
+
# -----------------------------------------------------------------------
|
|
274
|
+
# Phase 4 — Budget deduction (if applicable)
|
|
275
|
+
# -----------------------------------------------------------------------
|
|
276
|
+
if use_budget:
|
|
277
|
+
start_payload = {
|
|
278
|
+
"audit_uuid": audit_uuid,
|
|
279
|
+
"use_budget": True,
|
|
280
|
+
"local_execution": True, # budget only — CLI drives the audit locally
|
|
281
|
+
}
|
|
282
|
+
if show:
|
|
283
|
+
confirmed = review_payload(
|
|
284
|
+
start_payload,
|
|
285
|
+
command_label="Audit Start — Budget Deduction",
|
|
286
|
+
context_note="This payload tells CodeDD to deduct LoC from your budget. Only audit UUID and budget flag are sent. No server-side audit is triggered.",
|
|
287
|
+
confirm_prompt="Proceed with budget deduction on CodeDD?",
|
|
288
|
+
)
|
|
289
|
+
if not confirmed:
|
|
290
|
+
print_info("Cancelled.")
|
|
291
|
+
raise typer.Exit(code=0)
|
|
292
|
+
|
|
293
|
+
with Progress(
|
|
294
|
+
SpinnerColumn(),
|
|
295
|
+
TextColumn("[progress.description]{task.description}"),
|
|
296
|
+
console=console,
|
|
297
|
+
transient=True,
|
|
298
|
+
) as progress:
|
|
299
|
+
progress.add_task("Deducting budget…", total=None)
|
|
300
|
+
with CodeDDClient(config=cfg) as client:
|
|
301
|
+
resp = client.post(Endpoints.AUDIT_START, json=start_payload)
|
|
302
|
+
|
|
303
|
+
if resp.status_code != 200:
|
|
304
|
+
msg = "Failed to start audit (budget deduction)"
|
|
305
|
+
try:
|
|
306
|
+
msg = resp.json().get("message", msg)
|
|
307
|
+
except Exception:
|
|
308
|
+
pass
|
|
309
|
+
print_error(msg)
|
|
310
|
+
raise typer.Exit(code=1)
|
|
311
|
+
|
|
312
|
+
body = resp.json()
|
|
313
|
+
if body.get("status") != "success":
|
|
314
|
+
print_error(body.get("message", "Unknown error during budget deduction"))
|
|
315
|
+
raise typer.Exit(code=1)
|
|
316
|
+
|
|
317
|
+
loc_deducted = body.get("loc_deducted", 0)
|
|
318
|
+
if loc_deducted:
|
|
319
|
+
print_info(f" {loc_deducted:,} LoC deducted from budget")
|
|
320
|
+
|
|
321
|
+
# -----------------------------------------------------------------------
|
|
322
|
+
# Phase 5 — Local file audit
|
|
323
|
+
# -----------------------------------------------------------------------
|
|
324
|
+
_run_local_audit(
|
|
325
|
+
cfg,
|
|
326
|
+
audit_uuid,
|
|
327
|
+
show=show,
|
|
328
|
+
debug_llm=debug_llm,
|
|
329
|
+
debug_llm_full_prompt=debug_llm_full_prompt,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# ---------------------------------------------------------------------------
|
|
334
|
+
# Helpers
|
|
335
|
+
# ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
def _check_llm_keys(cfg: ConfigManager) -> bool:
|
|
338
|
+
"""
|
|
339
|
+
Verify that at least one LLM API key is configured and matches the
|
|
340
|
+
provider preference.
|
|
341
|
+
|
|
342
|
+
Displays which provider(s) will be used. Returns ``False`` if no
|
|
343
|
+
usable key is found.
|
|
344
|
+
"""
|
|
345
|
+
preference = cfg.llm_provider
|
|
346
|
+
configured = LLMKeyManager.get_configured_providers()
|
|
347
|
+
|
|
348
|
+
if not configured:
|
|
349
|
+
print_error(
|
|
350
|
+
"No LLM API keys configured.\n"
|
|
351
|
+
" CodeDD uses LLMs (Anthropic or OpenAI) to run the source code audit.\n"
|
|
352
|
+
" Add an API key with [bold cyan]codedd config set-key[/bold cyan]."
|
|
353
|
+
)
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
# Determine which providers will actually be used
|
|
357
|
+
if preference == "both":
|
|
358
|
+
active = configured # use whatever is available
|
|
359
|
+
elif preference in configured:
|
|
360
|
+
active = [preference]
|
|
361
|
+
else:
|
|
362
|
+
# Preference set to a provider without a key — fall back to what's available
|
|
363
|
+
active = configured
|
|
364
|
+
console.print(
|
|
365
|
+
f" [yellow]{SYMBOL_WARN}[/yellow] Preferred provider "
|
|
366
|
+
f"[bold]{preference}[/bold] has no key stored. "
|
|
367
|
+
f"Falling back to: {', '.join(active)}"
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Display summary
|
|
371
|
+
primary = active[0]
|
|
372
|
+
fallback = active[1] if len(active) > 1 else None
|
|
373
|
+
model_primary = PROVIDER_MODELS.get(primary, "")
|
|
374
|
+
info_parts = [f"[bold]{primary}[/bold] [dim]({model_primary})[/dim]"]
|
|
375
|
+
if fallback:
|
|
376
|
+
model_fallback = PROVIDER_MODELS.get(fallback, "")
|
|
377
|
+
info_parts.append(f"fallback: [bold]{fallback}[/bold] [dim]({model_fallback})[/dim]")
|
|
378
|
+
|
|
379
|
+
console.print(
|
|
380
|
+
f" [green]{SYMBOL_OK}[/green] LLM provider(s): {' | '.join(info_parts)}"
|
|
381
|
+
)
|
|
382
|
+
return True
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _auto_sync(cfg: ConfigManager, auto_confirm: bool = False, show: bool = False) -> bool:
|
|
386
|
+
"""
|
|
387
|
+
Run an automatic scope sync. If changes are detected, prompt to
|
|
388
|
+
re-confirm.
|
|
389
|
+
|
|
390
|
+
Returns True if sync is OK (no changes or changes were re-confirmed).
|
|
391
|
+
"""
|
|
392
|
+
# Import sync internals from scope_cmd
|
|
393
|
+
from codedd_cli.commands.scope_cmd import (
|
|
394
|
+
_compute_diff,
|
|
395
|
+
_run_reconfirm,
|
|
396
|
+
)
|
|
397
|
+
from codedd_cli.scanner.file_walker import scan_repository
|
|
398
|
+
|
|
399
|
+
dirs = cfg.scope_directories
|
|
400
|
+
if not dirs:
|
|
401
|
+
print_warning("No directories in scope. Run [bold cyan]codedd scope add[/bold cyan] first.")
|
|
402
|
+
return False
|
|
403
|
+
|
|
404
|
+
audit_uuid = cfg.active_audit_uuid
|
|
405
|
+
|
|
406
|
+
console.print("[dim]Phase 1 — Syncing scope with CodeDD…[/dim]\n")
|
|
407
|
+
|
|
408
|
+
if show:
|
|
409
|
+
confirmed = review_request(
|
|
410
|
+
"GET",
|
|
411
|
+
Endpoints.SCOPE_FILES,
|
|
412
|
+
params={"audit_uuid": audit_uuid},
|
|
413
|
+
command_label="Audit Start — Sync Scope",
|
|
414
|
+
context_note="This request fetches the registered scope from CodeDD to detect local changes. Only the audit UUID is sent.",
|
|
415
|
+
confirm_prompt="Proceed with this request to CodeDD?",
|
|
416
|
+
)
|
|
417
|
+
if not confirmed:
|
|
418
|
+
print_info("Cancelled.")
|
|
419
|
+
return False
|
|
420
|
+
|
|
421
|
+
# Fetch remote state
|
|
422
|
+
with Progress(
|
|
423
|
+
SpinnerColumn(),
|
|
424
|
+
TextColumn("[progress.description]{task.description}"),
|
|
425
|
+
console=console,
|
|
426
|
+
transient=True,
|
|
427
|
+
) as progress:
|
|
428
|
+
progress.add_task("Fetching remote scope…", total=None)
|
|
429
|
+
with CodeDDClient(config=cfg) as client:
|
|
430
|
+
resp = client.get(Endpoints.SCOPE_FILES, params={"audit_uuid": audit_uuid})
|
|
431
|
+
|
|
432
|
+
if resp.status_code != 200:
|
|
433
|
+
print_warning("Could not fetch remote scope. Skipping sync.")
|
|
434
|
+
return True # Non-fatal — let pre-flight catch issues
|
|
435
|
+
|
|
436
|
+
body = resp.json()
|
|
437
|
+
if body.get("status") != "success":
|
|
438
|
+
print_warning("Remote scope fetch returned non-success. Skipping sync.")
|
|
439
|
+
return True
|
|
440
|
+
|
|
441
|
+
remote_sub_audits = body.get("sub_audits", [])
|
|
442
|
+
remote_by_name: dict[str, list[dict]] = {}
|
|
443
|
+
for sa in remote_sub_audits:
|
|
444
|
+
remote_by_name[sa.get("repo_name", "")] = sa.get("files", [])
|
|
445
|
+
|
|
446
|
+
# Scan local and compute diffs
|
|
447
|
+
has_changes = False
|
|
448
|
+
for entry in dirs:
|
|
449
|
+
repo_path = entry["path"]
|
|
450
|
+
repo_name = entry.get("repo_name", Path(repo_path).name)
|
|
451
|
+
|
|
452
|
+
result = scan_repository(repo_path)
|
|
453
|
+
|
|
454
|
+
local_files = {
|
|
455
|
+
f.relative_path: {
|
|
456
|
+
"lines_of_code": f.lines_of_code,
|
|
457
|
+
"lines_of_doc": f.lines_of_doc,
|
|
458
|
+
}
|
|
459
|
+
for f in result.files
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
remote_files = {}
|
|
463
|
+
for rf in remote_by_name.get(repo_name, []):
|
|
464
|
+
remote_files[rf["relative_path"]] = {
|
|
465
|
+
"lines_of_code": rf.get("lines_of_code", 0),
|
|
466
|
+
"lines_of_doc": rf.get("lines_of_doc", 0),
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
diff = _compute_diff(local_files, remote_files)
|
|
470
|
+
if diff["added"] or diff["removed"] or diff["changed"]:
|
|
471
|
+
has_changes = True
|
|
472
|
+
total_changes = len(diff["added"]) + len(diff["removed"]) + len(diff["changed"])
|
|
473
|
+
console.print(
|
|
474
|
+
f" [bold yellow]{SYMBOL_WARN}[/bold yellow] [bold]{repo_name}[/bold] "
|
|
475
|
+
f"{total_changes} change(s) detected"
|
|
476
|
+
)
|
|
477
|
+
else:
|
|
478
|
+
console.print(
|
|
479
|
+
f" [bold green]{SYMBOL_OK}[/bold green] [bold]{repo_name}[/bold] in sync"
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
if has_changes:
|
|
483
|
+
console.print()
|
|
484
|
+
print_warning("Local files have changed since last registration.")
|
|
485
|
+
if auto_confirm or Confirm.ask("Re-confirm scope now?", default=True):
|
|
486
|
+
reconfirm_ok = _run_reconfirm(cfg, show=show)
|
|
487
|
+
if not reconfirm_ok:
|
|
488
|
+
print_error("Scope re-registration failed. Cannot start audit.")
|
|
489
|
+
return False
|
|
490
|
+
return True
|
|
491
|
+
else:
|
|
492
|
+
return False
|
|
493
|
+
|
|
494
|
+
return True
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _fetch_preflight(client: CodeDDClient, audit_uuid: str) -> dict | None:
|
|
498
|
+
"""
|
|
499
|
+
Call the pre-flight endpoint and return the parsed response dict.
|
|
500
|
+
Returns None on network/auth failure (error is already printed).
|
|
501
|
+
"""
|
|
502
|
+
with Progress(
|
|
503
|
+
SpinnerColumn(),
|
|
504
|
+
TextColumn("[progress.description]{task.description}"),
|
|
505
|
+
console=console,
|
|
506
|
+
transient=True,
|
|
507
|
+
) as progress:
|
|
508
|
+
progress.add_task("Running pre-flight checks…", total=None)
|
|
509
|
+
resp = client.get(
|
|
510
|
+
Endpoints.AUDIT_CAN_START,
|
|
511
|
+
params={"audit_uuid": audit_uuid},
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
if resp.status_code == 401:
|
|
515
|
+
print_error("Authentication failed. Run [bold cyan]codedd auth login[/bold cyan].")
|
|
516
|
+
return None
|
|
517
|
+
|
|
518
|
+
try:
|
|
519
|
+
return resp.json()
|
|
520
|
+
except Exception:
|
|
521
|
+
print_error("Invalid response from server.")
|
|
522
|
+
return None
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _render_preflight(preflight: dict) -> None:
|
|
526
|
+
"""Display a summary table from the pre-flight check.
|
|
527
|
+
|
|
528
|
+
Handles partial responses when the backend blocks early (e.g. not all
|
|
529
|
+
repos scoped) and omits total_lines_of_code, is_paid, loc_budget, etc.
|
|
530
|
+
"""
|
|
531
|
+
table = Table(title="Pre-flight Summary", show_header=True, padding=(0, 1))
|
|
532
|
+
table.add_column("Item", style="bold")
|
|
533
|
+
table.add_column("Value", justify="right")
|
|
534
|
+
|
|
535
|
+
total_loc = preflight.get("total_lines_of_code")
|
|
536
|
+
if total_loc is not None:
|
|
537
|
+
table.add_row("Total LoC", f"{total_loc:,}")
|
|
538
|
+
table.add_row("Repositories", str(len(preflight.get("sub_audits", []))))
|
|
539
|
+
|
|
540
|
+
is_paid = preflight.get("is_paid")
|
|
541
|
+
if is_paid is not None:
|
|
542
|
+
table.add_row(
|
|
543
|
+
"Payment",
|
|
544
|
+
"[green]Paid[/green]" if is_paid else "[yellow]Unpaid[/yellow]",
|
|
545
|
+
)
|
|
546
|
+
if is_paid and preflight.get("lines_purchased", 0) > 0:
|
|
547
|
+
table.add_row("Lines purchased", f"{preflight['lines_purchased']:,}")
|
|
548
|
+
|
|
549
|
+
loc_delta = preflight.get("loc_delta", 0)
|
|
550
|
+
if loc_delta is not None and loc_delta > 0:
|
|
551
|
+
table.add_row("Scope delta", f"[yellow]+{loc_delta:,}[/yellow]")
|
|
552
|
+
|
|
553
|
+
loc_budget = preflight.get("loc_budget")
|
|
554
|
+
if loc_budget is not None:
|
|
555
|
+
table.add_row("LoC budget", f"{loc_budget:,}")
|
|
556
|
+
|
|
557
|
+
table.add_row(
|
|
558
|
+
"Status",
|
|
559
|
+
"[green]Ready[/green]" if preflight.get("can_start") else "[red]Blocked[/red]",
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
console.print(table)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _handle_checkout(
|
|
566
|
+
cfg: ConfigManager,
|
|
567
|
+
audit_uuid: str,
|
|
568
|
+
lines_of_code: int,
|
|
569
|
+
show: bool = False,
|
|
570
|
+
) -> bool:
|
|
571
|
+
"""
|
|
572
|
+
Create a Stripe checkout session, open in the browser, and poll
|
|
573
|
+
until payment is confirmed.
|
|
574
|
+
|
|
575
|
+
Returns True if payment was confirmed, False otherwise.
|
|
576
|
+
"""
|
|
577
|
+
console.print()
|
|
578
|
+
|
|
579
|
+
checkout_payload = {"audit_uuid": audit_uuid, "lines_of_code": lines_of_code}
|
|
580
|
+
if show:
|
|
581
|
+
confirmed = review_payload(
|
|
582
|
+
checkout_payload,
|
|
583
|
+
command_label="Audit Checkout",
|
|
584
|
+
context_note="This payload requests a payment checkout session for the given audit and line count.",
|
|
585
|
+
confirm_prompt="Proceed with creating the payment session on CodeDD?",
|
|
586
|
+
)
|
|
587
|
+
if not confirmed:
|
|
588
|
+
print_info("Cancelled.")
|
|
589
|
+
return False
|
|
590
|
+
|
|
591
|
+
# Create checkout session
|
|
592
|
+
with Progress(
|
|
593
|
+
SpinnerColumn(),
|
|
594
|
+
TextColumn("[progress.description]{task.description}"),
|
|
595
|
+
console=console,
|
|
596
|
+
transient=True,
|
|
597
|
+
) as progress:
|
|
598
|
+
progress.add_task("Creating payment session…", total=None)
|
|
599
|
+
with CodeDDClient(config=cfg) as client:
|
|
600
|
+
resp = client.post(
|
|
601
|
+
Endpoints.AUDIT_CHECKOUT,
|
|
602
|
+
json=checkout_payload,
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
if resp.status_code != 200:
|
|
606
|
+
msg = "Failed to create checkout session"
|
|
607
|
+
try:
|
|
608
|
+
msg = resp.json().get("message", msg)
|
|
609
|
+
except Exception:
|
|
610
|
+
pass
|
|
611
|
+
print_error(msg)
|
|
612
|
+
return False
|
|
613
|
+
|
|
614
|
+
body = resp.json()
|
|
615
|
+
checkout_url = body.get("checkout_url", "")
|
|
616
|
+
if not checkout_url:
|
|
617
|
+
print_error("No checkout URL received from server.")
|
|
618
|
+
return False
|
|
619
|
+
|
|
620
|
+
# Open browser
|
|
621
|
+
print_info(f"Opening payment page…\n [link={checkout_url}]{checkout_url}[/link]\n")
|
|
622
|
+
try:
|
|
623
|
+
webbrowser.open(checkout_url)
|
|
624
|
+
except Exception:
|
|
625
|
+
print_warning("Could not open browser automatically. Please visit the URL above.")
|
|
626
|
+
|
|
627
|
+
# Poll for payment confirmation
|
|
628
|
+
console.print("[dim]Waiting for payment confirmation… (Ctrl+C to cancel)[/dim]\n")
|
|
629
|
+
|
|
630
|
+
poll_interval = 5 # seconds
|
|
631
|
+
max_wait = 600 # 10 minutes
|
|
632
|
+
elapsed = 0
|
|
633
|
+
|
|
634
|
+
try:
|
|
635
|
+
with Progress(
|
|
636
|
+
SpinnerColumn(),
|
|
637
|
+
TextColumn("[progress.description]{task.description}"),
|
|
638
|
+
console=console,
|
|
639
|
+
transient=True,
|
|
640
|
+
) as progress:
|
|
641
|
+
task = progress.add_task("Waiting for payment…", total=None)
|
|
642
|
+
|
|
643
|
+
while elapsed < max_wait:
|
|
644
|
+
time.sleep(poll_interval)
|
|
645
|
+
elapsed += poll_interval
|
|
646
|
+
|
|
647
|
+
with CodeDDClient(config=cfg) as client:
|
|
648
|
+
poll_resp = client.get(
|
|
649
|
+
Endpoints.AUDIT_PAYMENT_STATUS,
|
|
650
|
+
params={"audit_uuid": audit_uuid},
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
if poll_resp.status_code == 200:
|
|
654
|
+
poll_body = poll_resp.json()
|
|
655
|
+
if poll_body.get("is_paid"):
|
|
656
|
+
print_success("Payment confirmed!")
|
|
657
|
+
return True
|
|
658
|
+
|
|
659
|
+
progress.update(
|
|
660
|
+
task, description=f"Waiting for payment… ({elapsed}s)"
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
except KeyboardInterrupt:
|
|
664
|
+
console.print()
|
|
665
|
+
print_warning("Payment polling cancelled.")
|
|
666
|
+
print_info("You can complete payment at [bold cyan]codedd.ai[/bold cyan] and retry.")
|
|
667
|
+
return False
|
|
668
|
+
|
|
669
|
+
print_warning(
|
|
670
|
+
"Payment was not confirmed within the timeout.\n"
|
|
671
|
+
" Complete payment at [bold cyan]codedd.ai[/bold cyan] and run this command again."
|
|
672
|
+
)
|
|
673
|
+
return False
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
# ---------------------------------------------------------------------------
|
|
677
|
+
# Local audit execution
|
|
678
|
+
# ---------------------------------------------------------------------------
|
|
679
|
+
|
|
680
|
+
def _run_local_audit(
|
|
681
|
+
cfg: ConfigManager,
|
|
682
|
+
audit_uuid: str,
|
|
683
|
+
show: bool = False,
|
|
684
|
+
debug_llm: bool = False,
|
|
685
|
+
debug_llm_full_prompt: bool = False,
|
|
686
|
+
) -> None:
|
|
687
|
+
"""
|
|
688
|
+
Execute the full local audit flow:
|
|
689
|
+
1. Fetch the audit plan from CodeDD (file list + system prompt).
|
|
690
|
+
2. Audit each file locally via LLM (with concurrency + progress bar).
|
|
691
|
+
3. Submit results in batches to CodeDD.
|
|
692
|
+
4. Signal completion (triggers post-processing Steps 7-9).
|
|
693
|
+
"""
|
|
694
|
+
console.print()
|
|
695
|
+
|
|
696
|
+
# ---- Step 1: Fetch audit plan ----------------------------------------
|
|
697
|
+
console.print("[bold]Fetching audit plan from CodeDD…[/bold]\n")
|
|
698
|
+
|
|
699
|
+
if show:
|
|
700
|
+
confirmed = review_request(
|
|
701
|
+
"GET",
|
|
702
|
+
Endpoints.AUDIT_PLAN,
|
|
703
|
+
params={"audit_uuid": audit_uuid},
|
|
704
|
+
command_label="Audit Plan",
|
|
705
|
+
context_note=(
|
|
706
|
+
"This request fetches the audit execution plan (file list, "
|
|
707
|
+
"system prompt, config) from CodeDD. Only the audit UUID is sent."
|
|
708
|
+
),
|
|
709
|
+
confirm_prompt="Proceed with fetching the audit plan?",
|
|
710
|
+
)
|
|
711
|
+
if not confirmed:
|
|
712
|
+
print_info("Cancelled.")
|
|
713
|
+
raise typer.Exit(code=0)
|
|
714
|
+
|
|
715
|
+
with Progress(
|
|
716
|
+
SpinnerColumn(),
|
|
717
|
+
TextColumn("[progress.description]{task.description}"),
|
|
718
|
+
console=console,
|
|
719
|
+
transient=True,
|
|
720
|
+
) as progress:
|
|
721
|
+
progress.add_task("Downloading plan…", total=None)
|
|
722
|
+
with CodeDDClient(config=cfg) as client:
|
|
723
|
+
resp = client.get(
|
|
724
|
+
Endpoints.AUDIT_PLAN,
|
|
725
|
+
params={"audit_uuid": audit_uuid},
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
if resp.status_code != 200:
|
|
729
|
+
msg = "Failed to fetch audit plan"
|
|
730
|
+
try:
|
|
731
|
+
msg = resp.json().get("message", msg)
|
|
732
|
+
except Exception:
|
|
733
|
+
pass
|
|
734
|
+
print_error(msg)
|
|
735
|
+
raise typer.Exit(code=1)
|
|
736
|
+
|
|
737
|
+
plan = resp.json()
|
|
738
|
+
if plan.get("status") != "success":
|
|
739
|
+
print_error(plan.get("message", "Unexpected response from server"))
|
|
740
|
+
raise typer.Exit(code=1)
|
|
741
|
+
|
|
742
|
+
system_prompt = plan.get("prompts", {}).get("file_audit", "")
|
|
743
|
+
vulnerability_validation_prompt = plan.get("prompts", {}).get("vulnerability_validation", "")
|
|
744
|
+
if not system_prompt:
|
|
745
|
+
print_error("Server returned an empty system prompt. Cannot audit.")
|
|
746
|
+
raise typer.Exit(code=1)
|
|
747
|
+
|
|
748
|
+
sub_audits = plan.get("sub_audits", [])
|
|
749
|
+
plan_config = plan.get("config", {})
|
|
750
|
+
batch_size = plan_config.get("batch_size", 10)
|
|
751
|
+
server_max = plan_config.get("max_concurrent", 8)
|
|
752
|
+
# Use the local config value, capped by the server-side maximum
|
|
753
|
+
max_concurrent = min(cfg.llm_concurrency, server_max)
|
|
754
|
+
|
|
755
|
+
# Flatten file list and build scope_dirs mapping
|
|
756
|
+
all_files: list[dict] = []
|
|
757
|
+
scope_dirs: dict[str, str] = {}
|
|
758
|
+
total_loc = 0
|
|
759
|
+
|
|
760
|
+
local_dirs = cfg.scope_directories
|
|
761
|
+
dir_by_name = {e.get("repo_name", ""): e.get("path", "") for e in local_dirs}
|
|
762
|
+
|
|
763
|
+
for sa in sub_audits:
|
|
764
|
+
repo_name = sa.get("repo_name", "")
|
|
765
|
+
local_path = dir_by_name.get(repo_name, "")
|
|
766
|
+
scope_dirs[repo_name] = local_path
|
|
767
|
+
|
|
768
|
+
for f in sa.get("files", []):
|
|
769
|
+
f["repo_name"] = repo_name
|
|
770
|
+
f["sub_audit_uuid"] = sa["audit_uuid"]
|
|
771
|
+
all_files.append(f)
|
|
772
|
+
total_loc += f.get("lines_of_code", 0)
|
|
773
|
+
|
|
774
|
+
if not all_files:
|
|
775
|
+
print_warning("No files to audit. Check your scope selection.")
|
|
776
|
+
raise typer.Exit(code=0)
|
|
777
|
+
|
|
778
|
+
# Retrieve LLM API keys now (keyring can be slow on Windows; do it before "Auditing files…")
|
|
779
|
+
with Progress(
|
|
780
|
+
SpinnerColumn(),
|
|
781
|
+
TextColumn("[progress.description]{task.description}"),
|
|
782
|
+
console=console,
|
|
783
|
+
transient=True,
|
|
784
|
+
) as progress:
|
|
785
|
+
progress.add_task("Retrieving API keys…", total=None)
|
|
786
|
+
anthropic_key = LLMKeyManager.retrieve_key("anthropic")
|
|
787
|
+
openai_key = LLMKeyManager.retrieve_key("openai")
|
|
788
|
+
|
|
789
|
+
# Display summary
|
|
790
|
+
repo_names = [sa.get("repo_name", "?") for sa in sub_audits]
|
|
791
|
+
console.print(
|
|
792
|
+
f" Repositories: [bold]{len(sub_audits)}[/bold] ({', '.join(repo_names)})"
|
|
793
|
+
)
|
|
794
|
+
console.print(f" Files to audit: [bold]{len(all_files)}[/bold]")
|
|
795
|
+
console.print(f" Total LoC: [bold]{total_loc:,}[/bold]")
|
|
796
|
+
console.print(f" Concurrency: [bold]{max_concurrent}[/bold] parallel LLM calls\n")
|
|
797
|
+
|
|
798
|
+
# ---- Step 2: Audit files locally with progress -----------------------
|
|
799
|
+
console.print("[bold]Auditing files…[/bold]\n")
|
|
800
|
+
|
|
801
|
+
preference = cfg.llm_provider
|
|
802
|
+
# anthropic_key, openai_key already retrieved above (avoids slow keyring delay here)
|
|
803
|
+
provider_stats: dict[str, int] = {}
|
|
804
|
+
failed_files: list[AuditFileResult] = []
|
|
805
|
+
successful_results: list[AuditFileResult] = []
|
|
806
|
+
completed_count = 0
|
|
807
|
+
debug_lines: list[str] = []
|
|
808
|
+
start_time = time.monotonic()
|
|
809
|
+
|
|
810
|
+
# Build a Rich Live display with a progress bar + debug log
|
|
811
|
+
from rich.live import Live
|
|
812
|
+
|
|
813
|
+
def _build_display() -> Table:
|
|
814
|
+
"""Build the live display table (progress + last debug lines)."""
|
|
815
|
+
grid = Table.grid(padding=(0, 0))
|
|
816
|
+
grid.add_column()
|
|
817
|
+
|
|
818
|
+
# Progress bar row
|
|
819
|
+
pct = int(completed_count / len(all_files) * 100) if all_files else 0
|
|
820
|
+
elapsed_s = time.monotonic() - start_time
|
|
821
|
+
elapsed_m, elapsed_sec = divmod(int(elapsed_s), 60)
|
|
822
|
+
|
|
823
|
+
bar_width = 30
|
|
824
|
+
filled = int(bar_width * completed_count / len(all_files)) if all_files else 0
|
|
825
|
+
bar = "[green]" + "━" * filled + "[/green]" + "[dim]━[/dim]" * (bar_width - filled)
|
|
826
|
+
|
|
827
|
+
ok_count = len(successful_results)
|
|
828
|
+
fail_count = len(failed_files)
|
|
829
|
+
status_str = f"[green]{ok_count} ok[/green]"
|
|
830
|
+
if fail_count:
|
|
831
|
+
status_str += f" [red]{fail_count} failed[/red]"
|
|
832
|
+
|
|
833
|
+
grid.add_row(
|
|
834
|
+
f" {bar} [bold]{completed_count}[/bold]/{len(all_files)} "
|
|
835
|
+
f"({pct}%) {status_str} "
|
|
836
|
+
f"[dim]{elapsed_m}m {elapsed_sec:02d}s[/dim]"
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
# Last completed file
|
|
840
|
+
if successful_results or failed_files:
|
|
841
|
+
last = (successful_results + failed_files)[-1]
|
|
842
|
+
short = last.relative_path
|
|
843
|
+
if len(short) > 60:
|
|
844
|
+
short = "..." + short[-57:]
|
|
845
|
+
if last.success:
|
|
846
|
+
prov = last.provider_used or "?"
|
|
847
|
+
grid.add_row(
|
|
848
|
+
f" [green]{SYMBOL_OK}[/green] {short} "
|
|
849
|
+
f"[dim]({prov})[/dim]"
|
|
850
|
+
)
|
|
851
|
+
else:
|
|
852
|
+
grid.add_row(
|
|
853
|
+
f" [red]{SYMBOL_FAIL}[/red] {short} "
|
|
854
|
+
f"[dim]{last.error or ''}[/dim]"
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
# Debug log (last 6 lines) — light grey so they don't compete with progress
|
|
858
|
+
if debug_lines:
|
|
859
|
+
grid.add_row("")
|
|
860
|
+
for line in debug_lines[-6:]:
|
|
861
|
+
grid.add_row(f" [{STYLE_DEBUG_LOG}]{line}[/{STYLE_DEBUG_LOG}]")
|
|
862
|
+
|
|
863
|
+
return grid
|
|
864
|
+
|
|
865
|
+
def _on_debug(msg: str) -> None:
|
|
866
|
+
"""Receive debug messages from the auditor."""
|
|
867
|
+
debug_lines.append(msg)
|
|
868
|
+
# Keep buffer bounded
|
|
869
|
+
if len(debug_lines) > 50:
|
|
870
|
+
debug_lines[:] = debug_lines[-30:]
|
|
871
|
+
|
|
872
|
+
def _on_progress(result: AuditFileResult) -> None:
|
|
873
|
+
"""Callback invoked after each file completes."""
|
|
874
|
+
nonlocal completed_count
|
|
875
|
+
completed_count += 1
|
|
876
|
+
if result.success:
|
|
877
|
+
provider = result.provider_used or "unknown"
|
|
878
|
+
provider_stats[provider] = provider_stats.get(provider, 0) + 1
|
|
879
|
+
successful_results.append(result)
|
|
880
|
+
else:
|
|
881
|
+
failed_files.append(result)
|
|
882
|
+
|
|
883
|
+
def _on_dump_llm(
|
|
884
|
+
full_prompt: str,
|
|
885
|
+
response_text: str,
|
|
886
|
+
audit_data: dict | None,
|
|
887
|
+
none_count: int,
|
|
888
|
+
) -> None:
|
|
889
|
+
"""Print full prompt, raw response, and parse result to the CLI (debug)."""
|
|
890
|
+
sep = "=" * 60
|
|
891
|
+
console.print(f"\n[bold cyan]{sep}[/bold cyan]")
|
|
892
|
+
console.print("[bold cyan] LLM DEBUG DUMP[/bold cyan]")
|
|
893
|
+
console.print(f"[bold cyan]{sep}[/bold cyan]\n")
|
|
894
|
+
console.print("[bold]--- PROMPT sent to LLM ---[/bold]")
|
|
895
|
+
if debug_llm_full_prompt:
|
|
896
|
+
console.print(f"[dim]{full_prompt}[/dim]\n")
|
|
897
|
+
else:
|
|
898
|
+
console.print(
|
|
899
|
+
"[dim][system prompt redacted] Use --debug-llm-full-prompt to include it.[/dim]\n"
|
|
900
|
+
)
|
|
901
|
+
console.print("[bold]--- RAW RESPONSE from LLM ---[/bold]")
|
|
902
|
+
console.print(f"[dim]{response_text}[/dim]\n")
|
|
903
|
+
console.print("[bold]--- PARSED RESULT (response_parser) ---[/bold]")
|
|
904
|
+
console.print(f" none_response_count: [bold]{none_count}[/bold]")
|
|
905
|
+
if audit_data is not None:
|
|
906
|
+
try:
|
|
907
|
+
console.print("[dim]" + json.dumps(audit_data, indent=2) + "[/dim]")
|
|
908
|
+
except (TypeError, ValueError):
|
|
909
|
+
console.print(f"[dim]{audit_data}[/dim]")
|
|
910
|
+
else:
|
|
911
|
+
console.print(" [red]None[/red] (parse failed or incomplete)")
|
|
912
|
+
console.print()
|
|
913
|
+
|
|
914
|
+
with LocalFileAuditor(
|
|
915
|
+
anthropic_key=anthropic_key,
|
|
916
|
+
openai_key=openai_key,
|
|
917
|
+
system_prompt=system_prompt,
|
|
918
|
+
provider_preference=preference,
|
|
919
|
+
max_concurrent=max_concurrent,
|
|
920
|
+
on_debug=_on_debug,
|
|
921
|
+
on_dump_llm=_on_dump_llm if debug_llm else None,
|
|
922
|
+
) as auditor:
|
|
923
|
+
with Live(
|
|
924
|
+
_build_display(),
|
|
925
|
+
console=console,
|
|
926
|
+
refresh_per_second=4,
|
|
927
|
+
transient=False,
|
|
928
|
+
) as live:
|
|
929
|
+
# Run a background refresh so the display updates during LLM calls
|
|
930
|
+
import threading
|
|
931
|
+
|
|
932
|
+
_stop_refresh = threading.Event()
|
|
933
|
+
|
|
934
|
+
def _refresh_loop() -> None:
|
|
935
|
+
while not _stop_refresh.is_set():
|
|
936
|
+
live.update(_build_display())
|
|
937
|
+
_stop_refresh.wait(0.3)
|
|
938
|
+
|
|
939
|
+
refresh_thread = threading.Thread(target=_refresh_loop, daemon=True)
|
|
940
|
+
refresh_thread.start()
|
|
941
|
+
|
|
942
|
+
try:
|
|
943
|
+
results = auditor.audit_batch(
|
|
944
|
+
files=all_files,
|
|
945
|
+
scope_dirs=scope_dirs,
|
|
946
|
+
on_progress=_on_progress,
|
|
947
|
+
)
|
|
948
|
+
finally:
|
|
949
|
+
_stop_refresh.set()
|
|
950
|
+
refresh_thread.join(timeout=2)
|
|
951
|
+
live.update(_build_display())
|
|
952
|
+
|
|
953
|
+
elapsed = time.monotonic() - start_time
|
|
954
|
+
console.print()
|
|
955
|
+
|
|
956
|
+
if failed_files:
|
|
957
|
+
print_warning(f"{len(failed_files)} file(s) could not be audited:")
|
|
958
|
+
for f in failed_files[:10]:
|
|
959
|
+
console.print(f" [dim]{f.relative_path}[/dim] — {f.error}")
|
|
960
|
+
if len(failed_files) > 10:
|
|
961
|
+
console.print(f" … and {len(failed_files) - 10} more")
|
|
962
|
+
console.print()
|
|
963
|
+
|
|
964
|
+
if not successful_results:
|
|
965
|
+
print_error("No files were successfully audited. Nothing to submit.")
|
|
966
|
+
raise typer.Exit(code=1)
|
|
967
|
+
|
|
968
|
+
# ---- Step 2b: Local complexity analysis (cyclomatic + Halstead) ------
|
|
969
|
+
console.print("[bold]Analysing code complexity…[/bold]\n")
|
|
970
|
+
|
|
971
|
+
cx_debug_lines: list[str] = []
|
|
972
|
+
cx_completed = 0
|
|
973
|
+
cx_ok_count = 0
|
|
974
|
+
cx_fail_count = 0
|
|
975
|
+
cx_start = time.monotonic()
|
|
976
|
+
|
|
977
|
+
def _cx_on_debug(msg: str) -> None:
|
|
978
|
+
cx_debug_lines.append(msg)
|
|
979
|
+
if len(cx_debug_lines) > 30:
|
|
980
|
+
cx_debug_lines[:] = cx_debug_lines[-20:]
|
|
981
|
+
|
|
982
|
+
def _cx_build_display() -> Table:
|
|
983
|
+
grid = Table.grid(padding=(0, 0))
|
|
984
|
+
grid.add_column()
|
|
985
|
+
pct = int(cx_completed / len(all_files) * 100) if all_files else 0
|
|
986
|
+
elapsed_s = time.monotonic() - cx_start
|
|
987
|
+
elapsed_m, elapsed_sec = divmod(int(elapsed_s), 60)
|
|
988
|
+
bar_w = 30
|
|
989
|
+
filled = int(bar_w * cx_completed / len(all_files)) if all_files else 0
|
|
990
|
+
bar = "[green]" + "━" * filled + "[/green]" + "[dim]━[/dim]" * (bar_w - filled)
|
|
991
|
+
status_str = f"[green]{cx_ok_count} ok[/green]"
|
|
992
|
+
if cx_fail_count:
|
|
993
|
+
status_str += f" [red]{cx_fail_count} skipped[/red]"
|
|
994
|
+
grid.add_row(
|
|
995
|
+
f" {bar} [bold]{cx_completed}[/bold]/{len(all_files)} "
|
|
996
|
+
f"({pct}%) {status_str} "
|
|
997
|
+
f"[dim]{elapsed_m}m {elapsed_sec:02d}s[/dim]"
|
|
998
|
+
)
|
|
999
|
+
if cx_debug_lines:
|
|
1000
|
+
for line in cx_debug_lines[-3:]:
|
|
1001
|
+
grid.add_row(f" [{STYLE_DEBUG_LOG}]{line}[/{STYLE_DEBUG_LOG}]")
|
|
1002
|
+
return grid
|
|
1003
|
+
|
|
1004
|
+
cx_results: list[FileComplexityResult] = []
|
|
1005
|
+
|
|
1006
|
+
def _cx_on_progress(result: FileComplexityResult) -> None:
|
|
1007
|
+
nonlocal cx_completed, cx_ok_count, cx_fail_count
|
|
1008
|
+
cx_completed += 1
|
|
1009
|
+
if result.success:
|
|
1010
|
+
cx_ok_count += 1
|
|
1011
|
+
else:
|
|
1012
|
+
cx_fail_count += 1
|
|
1013
|
+
cx_results.append(result)
|
|
1014
|
+
|
|
1015
|
+
cx_analyzer = LocalComplexityAnalyzer(
|
|
1016
|
+
max_workers=min(max_concurrent, os.cpu_count() or 4, 8),
|
|
1017
|
+
on_debug=_cx_on_debug,
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
with Live(
|
|
1021
|
+
_cx_build_display(),
|
|
1022
|
+
console=console,
|
|
1023
|
+
refresh_per_second=4,
|
|
1024
|
+
transient=False,
|
|
1025
|
+
) as cx_live:
|
|
1026
|
+
import threading as _cx_threading
|
|
1027
|
+
|
|
1028
|
+
_cx_stop = _cx_threading.Event()
|
|
1029
|
+
|
|
1030
|
+
def _cx_refresh() -> None:
|
|
1031
|
+
while not _cx_stop.is_set():
|
|
1032
|
+
cx_live.update(_cx_build_display())
|
|
1033
|
+
_cx_stop.wait(0.3)
|
|
1034
|
+
|
|
1035
|
+
cx_refresh_t = _cx_threading.Thread(target=_cx_refresh, daemon=True)
|
|
1036
|
+
cx_refresh_t.start()
|
|
1037
|
+
try:
|
|
1038
|
+
cx_results = cx_analyzer.analyze_batch(
|
|
1039
|
+
files=all_files,
|
|
1040
|
+
scope_dirs=scope_dirs,
|
|
1041
|
+
on_progress=_cx_on_progress,
|
|
1042
|
+
)
|
|
1043
|
+
finally:
|
|
1044
|
+
_cx_stop.set()
|
|
1045
|
+
cx_refresh_t.join(timeout=2)
|
|
1046
|
+
cx_live.update(_cx_build_display())
|
|
1047
|
+
|
|
1048
|
+
cx_elapsed = time.monotonic() - cx_start
|
|
1049
|
+
cx_ok = [r for r in cx_results if r.success]
|
|
1050
|
+
console.print()
|
|
1051
|
+
if cx_ok:
|
|
1052
|
+
console.print(
|
|
1053
|
+
f" [green]{SYMBOL_OK}[/green] Complexity analysed: "
|
|
1054
|
+
f"[bold]{len(cx_ok)}[/bold] files "
|
|
1055
|
+
f"[dim]({int(cx_elapsed)}s)[/dim]"
|
|
1056
|
+
)
|
|
1057
|
+
if cx_fail_count:
|
|
1058
|
+
console.print(
|
|
1059
|
+
f" [yellow]{SYMBOL_WARN}[/yellow] {cx_fail_count} file(s) skipped "
|
|
1060
|
+
f"(non-source or unreadable)"
|
|
1061
|
+
)
|
|
1062
|
+
console.print()
|
|
1063
|
+
|
|
1064
|
+
# ---- Step 2c: Dependency scanning (manifests + imports + OSV) ---------
|
|
1065
|
+
console.print("[bold]Scanning dependencies…[/bold]\n")
|
|
1066
|
+
|
|
1067
|
+
dep_debug_lines: list[str] = []
|
|
1068
|
+
dep_start = time.monotonic()
|
|
1069
|
+
|
|
1070
|
+
def _dep_on_debug(msg: str) -> None:
|
|
1071
|
+
dep_debug_lines.append(msg)
|
|
1072
|
+
if len(dep_debug_lines) > 30:
|
|
1073
|
+
dep_debug_lines[:] = dep_debug_lines[-20:]
|
|
1074
|
+
|
|
1075
|
+
def _dep_build_display() -> Table:
|
|
1076
|
+
grid = Table.grid(padding=(0, 0))
|
|
1077
|
+
grid.add_column()
|
|
1078
|
+
elapsed_s = time.monotonic() - dep_start
|
|
1079
|
+
elapsed_m, elapsed_sec = divmod(int(elapsed_s), 60)
|
|
1080
|
+
if dep_debug_lines:
|
|
1081
|
+
for line in dep_debug_lines[-4:]:
|
|
1082
|
+
grid.add_row(f" [{STYLE_DEBUG_LOG}]{line}[/{STYLE_DEBUG_LOG}]")
|
|
1083
|
+
grid.add_row(f" [dim]{elapsed_m}m {elapsed_sec:02d}s[/dim]")
|
|
1084
|
+
return grid
|
|
1085
|
+
|
|
1086
|
+
# Fetch dependency config from an authenticated internal endpoint.
|
|
1087
|
+
# 404 means backend doesn't support it yet; defaults remain safe.
|
|
1088
|
+
dep_config: dict = {}
|
|
1089
|
+
try:
|
|
1090
|
+
with CodeDDClient(config=cfg) as client:
|
|
1091
|
+
dep_cfg_resp = client.get(Endpoints.AUDIT_DEPENDENCY_CONFIG)
|
|
1092
|
+
if dep_cfg_resp.status_code == 200:
|
|
1093
|
+
dep_cfg_body = dep_cfg_resp.json()
|
|
1094
|
+
if dep_cfg_body.get("status") == "success":
|
|
1095
|
+
dep_config = dep_cfg_body.get("config", {})
|
|
1096
|
+
_dep_on_debug(f"Dependency config loaded (version {dep_cfg_body.get('version', '?')})")
|
|
1097
|
+
elif dep_cfg_resp.status_code == 404:
|
|
1098
|
+
_dep_on_debug("Server does not support dependency config (404) — using defaults")
|
|
1099
|
+
if not dep_config and dep_cfg_resp.status_code != 404:
|
|
1100
|
+
_dep_on_debug("Warning: could not fetch dependency config — using defaults")
|
|
1101
|
+
except Exception as dep_cfg_exc:
|
|
1102
|
+
_dep_on_debug(f"Warning: dependency config fetch failed: {dep_cfg_exc}")
|
|
1103
|
+
|
|
1104
|
+
dep_scanner = LocalDependencyScanner(config=dep_config, on_debug=_dep_on_debug)
|
|
1105
|
+
|
|
1106
|
+
manifest_results: list[ManifestResult] = []
|
|
1107
|
+
import_results: list[ImportResult] = []
|
|
1108
|
+
vuln_results: dict[str, dict] = {}
|
|
1109
|
+
|
|
1110
|
+
with Live(
|
|
1111
|
+
_dep_build_display(),
|
|
1112
|
+
console=console,
|
|
1113
|
+
refresh_per_second=4,
|
|
1114
|
+
transient=False,
|
|
1115
|
+
) as dep_live:
|
|
1116
|
+
import threading as _dep_threading
|
|
1117
|
+
|
|
1118
|
+
_dep_stop = _dep_threading.Event()
|
|
1119
|
+
|
|
1120
|
+
def _dep_refresh() -> None:
|
|
1121
|
+
while not _dep_stop.is_set():
|
|
1122
|
+
dep_live.update(_dep_build_display())
|
|
1123
|
+
_dep_stop.wait(0.3)
|
|
1124
|
+
|
|
1125
|
+
dep_refresh_t = _dep_threading.Thread(target=_dep_refresh, daemon=True)
|
|
1126
|
+
dep_refresh_t.start()
|
|
1127
|
+
|
|
1128
|
+
try:
|
|
1129
|
+
# 1. Scan manifest files
|
|
1130
|
+
_dep_on_debug("Scanning manifest files…")
|
|
1131
|
+
manifest_results = dep_scanner.scan_manifests(scope_dirs)
|
|
1132
|
+
|
|
1133
|
+
total_manifest_pkgs = sum(len(m.packages) for m in manifest_results if not m.error)
|
|
1134
|
+
_dep_on_debug(
|
|
1135
|
+
f"Found {len(manifest_results)} manifest(s), "
|
|
1136
|
+
f"{total_manifest_pkgs} packages"
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
# 2. Extract imports from source files
|
|
1140
|
+
_dep_on_debug("Extracting imports from source files…")
|
|
1141
|
+
import_results = dep_scanner.scan_source_imports(all_files, scope_dirs)
|
|
1142
|
+
|
|
1143
|
+
# 3. Scan vulnerabilities via OSV
|
|
1144
|
+
if manifest_results or import_results:
|
|
1145
|
+
_dep_on_debug("Querying OSV for vulnerabilities…")
|
|
1146
|
+
vuln_results = dep_scanner.scan_vulnerabilities(manifest_results, import_results)
|
|
1147
|
+
vuln_with_issues = sum(
|
|
1148
|
+
1 for v in vuln_results.values() if v.get("vulnerability_count", 0) > 0
|
|
1149
|
+
)
|
|
1150
|
+
_dep_on_debug(
|
|
1151
|
+
f"Vulnerability scan complete: {len(vuln_results)} packages checked, "
|
|
1152
|
+
f"{vuln_with_issues} with known vulnerabilities"
|
|
1153
|
+
)
|
|
1154
|
+
finally:
|
|
1155
|
+
_dep_stop.set()
|
|
1156
|
+
dep_refresh_t.join(timeout=2)
|
|
1157
|
+
dep_live.update(_dep_build_display())
|
|
1158
|
+
|
|
1159
|
+
dep_elapsed = time.monotonic() - dep_start
|
|
1160
|
+
console.print()
|
|
1161
|
+
|
|
1162
|
+
manifest_ok = [m for m in manifest_results if not m.error]
|
|
1163
|
+
total_dep_pkgs = sum(len(m.packages) for m in manifest_ok)
|
|
1164
|
+
total_import_pkgs = sum(len(r.packages) for r in import_results)
|
|
1165
|
+
vuln_found = sum(1 for v in vuln_results.values() if v.get("vulnerability_count", 0) > 0)
|
|
1166
|
+
|
|
1167
|
+
if manifest_ok or import_results:
|
|
1168
|
+
console.print(
|
|
1169
|
+
f" [green]{SYMBOL_OK}[/green] Dependencies scanned: "
|
|
1170
|
+
f"[bold]{total_dep_pkgs}[/bold] from manifests, "
|
|
1171
|
+
f"[bold]{total_import_pkgs}[/bold] from imports "
|
|
1172
|
+
f"[dim]({int(dep_elapsed)}s)[/dim]"
|
|
1173
|
+
)
|
|
1174
|
+
if vuln_found:
|
|
1175
|
+
console.print(
|
|
1176
|
+
f" [yellow]{SYMBOL_WARN}[/yellow] {vuln_found} package(s) with known vulnerabilities"
|
|
1177
|
+
)
|
|
1178
|
+
else:
|
|
1179
|
+
console.print(
|
|
1180
|
+
f" [dim]{SYMBOL_INFO}[/dim] No dependency manifests found "
|
|
1181
|
+
f"[dim]({int(dep_elapsed)}s)[/dim]"
|
|
1182
|
+
)
|
|
1183
|
+
console.print()
|
|
1184
|
+
|
|
1185
|
+
# ---- Step 2d: Collect and submit git statistics (local repos) -------
|
|
1186
|
+
from codedd_cli.auditor.git_stats_collector import collect_git_statistics
|
|
1187
|
+
|
|
1188
|
+
console.print("[bold]Collecting git statistics…[/bold]\n")
|
|
1189
|
+
git_submitted = 0
|
|
1190
|
+
git_skipped = 0
|
|
1191
|
+
for sa in sub_audits:
|
|
1192
|
+
sub_uuid = sa.get("audit_uuid", "")
|
|
1193
|
+
repo_name = sa.get("repo_name", "")
|
|
1194
|
+
path = scope_dirs.get(repo_name, "")
|
|
1195
|
+
if not path or not os.path.isdir(path):
|
|
1196
|
+
git_skipped += 1
|
|
1197
|
+
continue
|
|
1198
|
+
stats = collect_git_statistics(
|
|
1199
|
+
path,
|
|
1200
|
+
repository_name=repo_name or None,
|
|
1201
|
+
repository_url=None,
|
|
1202
|
+
on_debug=None,
|
|
1203
|
+
)
|
|
1204
|
+
if not stats:
|
|
1205
|
+
git_skipped += 1
|
|
1206
|
+
continue
|
|
1207
|
+
payload = {
|
|
1208
|
+
"audit_uuid": sub_uuid,
|
|
1209
|
+
"repository_name": stats.get("repository_name"),
|
|
1210
|
+
"repository_url": stats.get("repository_url"),
|
|
1211
|
+
"commit_history": stats.get("commit_history", {}),
|
|
1212
|
+
"author_stats": stats.get("author_stats", {}),
|
|
1213
|
+
"merge_stats": stats.get("merge_stats", {}),
|
|
1214
|
+
"branch_stats": stats.get("branch_stats", {}),
|
|
1215
|
+
"meta_info": stats.get("meta_info", {}),
|
|
1216
|
+
"time_based_stats": stats.get("time_based_stats", {}),
|
|
1217
|
+
"release_stats": stats.get("release_stats", {}),
|
|
1218
|
+
"code_churn_stats": stats.get("code_churn_stats", {}),
|
|
1219
|
+
"collaboration_stats": stats.get("collaboration_stats", {}),
|
|
1220
|
+
}
|
|
1221
|
+
try:
|
|
1222
|
+
with CodeDDClient(config=cfg) as client:
|
|
1223
|
+
resp = client.post(Endpoints.AUDIT_GIT_STATISTICS, json=payload)
|
|
1224
|
+
if resp.status_code == 200:
|
|
1225
|
+
git_submitted += 1
|
|
1226
|
+
else:
|
|
1227
|
+
try:
|
|
1228
|
+
msg = resp.json().get("message", "git statistics submission failed")
|
|
1229
|
+
except Exception:
|
|
1230
|
+
msg = "git statistics submission failed"
|
|
1231
|
+
print_warning(f"Git stats for {repo_name}: {msg}")
|
|
1232
|
+
except Exception as e:
|
|
1233
|
+
print_warning(f"Git stats for {repo_name}: {e}")
|
|
1234
|
+
if git_submitted:
|
|
1235
|
+
console.print(
|
|
1236
|
+
f" [green]{SYMBOL_OK}[/green] Git statistics submitted: "
|
|
1237
|
+
f"[bold]{git_submitted}[/bold] repo(s)"
|
|
1238
|
+
)
|
|
1239
|
+
if git_skipped and not git_submitted:
|
|
1240
|
+
console.print(
|
|
1241
|
+
f" [dim]{SYMBOL_INFO}[/dim] No git repositories in scope or collection skipped"
|
|
1242
|
+
)
|
|
1243
|
+
console.print()
|
|
1244
|
+
|
|
1245
|
+
# ---- Step 2e: Architecture analysis (Phase 1+2+synthesis local; server does Phase 3 storage) --
|
|
1246
|
+
from codedd_cli.auditor.architecture_analyzer import run_architecture_analysis
|
|
1247
|
+
|
|
1248
|
+
console.print("[bold]Analysing architecture…[/bold]\n")
|
|
1249
|
+
|
|
1250
|
+
arch_debug_lines: list[str] = []
|
|
1251
|
+
arch_start = time.monotonic()
|
|
1252
|
+
|
|
1253
|
+
def _arch_on_debug(msg: str) -> None:
|
|
1254
|
+
arch_debug_lines.append(msg)
|
|
1255
|
+
if len(arch_debug_lines) > 30:
|
|
1256
|
+
arch_debug_lines[:] = arch_debug_lines[-20:]
|
|
1257
|
+
|
|
1258
|
+
def _arch_on_progress(msg: str) -> None:
|
|
1259
|
+
_arch_on_debug(msg)
|
|
1260
|
+
|
|
1261
|
+
def _arch_build_display() -> Table:
|
|
1262
|
+
grid = Table.grid(padding=(0, 0))
|
|
1263
|
+
grid.add_column()
|
|
1264
|
+
elapsed_s = time.monotonic() - arch_start
|
|
1265
|
+
elapsed_m, elapsed_sec = divmod(int(elapsed_s), 60)
|
|
1266
|
+
if arch_debug_lines:
|
|
1267
|
+
for line in arch_debug_lines[-5:]:
|
|
1268
|
+
grid.add_row(f" [{STYLE_DEBUG_LOG}]{line}[/{STYLE_DEBUG_LOG}]")
|
|
1269
|
+
grid.add_row(f" [dim]{elapsed_m}m {elapsed_sec:02d}s[/dim]")
|
|
1270
|
+
return grid
|
|
1271
|
+
|
|
1272
|
+
arch_submitted = 0
|
|
1273
|
+
arch_components_total = 0
|
|
1274
|
+
arch_relationships_total = 0
|
|
1275
|
+
|
|
1276
|
+
from rich.live import Live as ArchLive
|
|
1277
|
+
import threading as _arch_threading
|
|
1278
|
+
|
|
1279
|
+
with ArchLive(
|
|
1280
|
+
_arch_build_display(),
|
|
1281
|
+
console=console,
|
|
1282
|
+
refresh_per_second=4,
|
|
1283
|
+
transient=False,
|
|
1284
|
+
) as arch_live:
|
|
1285
|
+
_arch_stop = _arch_threading.Event()
|
|
1286
|
+
|
|
1287
|
+
def _arch_refresh() -> None:
|
|
1288
|
+
while not _arch_stop.is_set():
|
|
1289
|
+
arch_live.update(_arch_build_display())
|
|
1290
|
+
_arch_stop.wait(0.3)
|
|
1291
|
+
|
|
1292
|
+
arch_refresh_t = _arch_threading.Thread(target=_arch_refresh, daemon=True)
|
|
1293
|
+
arch_refresh_t.start()
|
|
1294
|
+
|
|
1295
|
+
try:
|
|
1296
|
+
for sa in sub_audits:
|
|
1297
|
+
sub_uuid = sa.get("audit_uuid", "")
|
|
1298
|
+
repo_name = sa.get("repo_name", "")
|
|
1299
|
+
file_paths = [
|
|
1300
|
+
f["file_path"] for f in all_files if f.get("sub_audit_uuid") == sub_uuid
|
|
1301
|
+
]
|
|
1302
|
+
if not file_paths:
|
|
1303
|
+
continue
|
|
1304
|
+
try:
|
|
1305
|
+
_arch_on_progress(f"Analysing {repo_name} ({len(file_paths)} files)…")
|
|
1306
|
+
phase1, phase2 = run_architecture_analysis(
|
|
1307
|
+
sub_uuid,
|
|
1308
|
+
repo_name or "repo",
|
|
1309
|
+
file_paths,
|
|
1310
|
+
scope_dirs=scope_dirs,
|
|
1311
|
+
on_progress=_arch_on_progress,
|
|
1312
|
+
on_debug=_arch_on_debug,
|
|
1313
|
+
)
|
|
1314
|
+
|
|
1315
|
+
n_comp = len(phase2.get("architectural_components") or {})
|
|
1316
|
+
n_rel = len(phase2.get("component_relationships") or [])
|
|
1317
|
+
_arch_on_debug(
|
|
1318
|
+
f"Submitting to CodeDD ({n_comp} components, {n_rel} relationships)…"
|
|
1319
|
+
)
|
|
1320
|
+
|
|
1321
|
+
payload = {
|
|
1322
|
+
"audit_uuid": sub_uuid,
|
|
1323
|
+
"phase1_results": phase1,
|
|
1324
|
+
"phase2_results": phase2,
|
|
1325
|
+
}
|
|
1326
|
+
# Architecture storage is fast now (no server-side LLM calls),
|
|
1327
|
+
# but allow extra time for TypeDB writes on large repos.
|
|
1328
|
+
with CodeDDClient(config=cfg) as client:
|
|
1329
|
+
resp = client.post(
|
|
1330
|
+
Endpoints.AUDIT_ARCHITECTURE, json=payload, timeout=120
|
|
1331
|
+
)
|
|
1332
|
+
if resp.status_code == 200:
|
|
1333
|
+
arch_submitted += 1
|
|
1334
|
+
arch_components_total += n_comp
|
|
1335
|
+
arch_relationships_total += n_rel
|
|
1336
|
+
else:
|
|
1337
|
+
try:
|
|
1338
|
+
msg = resp.json().get("message", "architecture submission failed")
|
|
1339
|
+
except Exception:
|
|
1340
|
+
msg = "architecture submission failed"
|
|
1341
|
+
print_warning(f"Architecture for {repo_name}: {msg}")
|
|
1342
|
+
except Exception as e:
|
|
1343
|
+
print_warning(f"Architecture for {repo_name}: {e}")
|
|
1344
|
+
finally:
|
|
1345
|
+
_arch_stop.set()
|
|
1346
|
+
arch_refresh_t.join(timeout=2)
|
|
1347
|
+
arch_live.update(_arch_build_display())
|
|
1348
|
+
|
|
1349
|
+
arch_elapsed = time.monotonic() - arch_start
|
|
1350
|
+
console.print()
|
|
1351
|
+
if arch_submitted:
|
|
1352
|
+
console.print(
|
|
1353
|
+
f" [green]{SYMBOL_OK}[/green] Architecture analysed: "
|
|
1354
|
+
f"[bold]{arch_components_total}[/bold] components, "
|
|
1355
|
+
f"[bold]{arch_relationships_total}[/bold] relationships "
|
|
1356
|
+
f"([bold]{arch_submitted}[/bold] repo(s)) "
|
|
1357
|
+
f"[dim]({int(arch_elapsed)}s)[/dim]"
|
|
1358
|
+
)
|
|
1359
|
+
console.print()
|
|
1360
|
+
|
|
1361
|
+
# ---- Step 3: Submit results in batches -------------------------------
|
|
1362
|
+
console.print("[bold]Submitting results to CodeDD…[/bold]\n")
|
|
1363
|
+
|
|
1364
|
+
# Group results by sub_audit_uuid
|
|
1365
|
+
results_by_sub: dict[str, list[dict]] = {}
|
|
1366
|
+
for r in successful_results:
|
|
1367
|
+
# Find the sub_audit_uuid from all_files
|
|
1368
|
+
sub_uuid = ""
|
|
1369
|
+
for f in all_files:
|
|
1370
|
+
if f["file_path"] == r.file_path:
|
|
1371
|
+
sub_uuid = f.get("sub_audit_uuid", "")
|
|
1372
|
+
break
|
|
1373
|
+
results_by_sub.setdefault(sub_uuid, []).append({
|
|
1374
|
+
"file_path": r.file_path,
|
|
1375
|
+
"audit_data": r.audit_data,
|
|
1376
|
+
})
|
|
1377
|
+
|
|
1378
|
+
total_ingested = 0
|
|
1379
|
+
total_errors = 0
|
|
1380
|
+
|
|
1381
|
+
with Progress(
|
|
1382
|
+
SpinnerColumn(),
|
|
1383
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1384
|
+
BarColumn(bar_width=30),
|
|
1385
|
+
MofNCompleteColumn(),
|
|
1386
|
+
console=console,
|
|
1387
|
+
) as progress:
|
|
1388
|
+
total_batches = sum(
|
|
1389
|
+
(len(items) + batch_size - 1) // batch_size
|
|
1390
|
+
for items in results_by_sub.values()
|
|
1391
|
+
)
|
|
1392
|
+
task = progress.add_task("Submitting…", total=total_batches)
|
|
1393
|
+
|
|
1394
|
+
with CodeDDClient(config=cfg) as client:
|
|
1395
|
+
for sub_uuid, items in results_by_sub.items():
|
|
1396
|
+
for i in range(0, len(items), batch_size):
|
|
1397
|
+
batch = items[i : i + batch_size]
|
|
1398
|
+
payload = {
|
|
1399
|
+
"audit_uuid": sub_uuid,
|
|
1400
|
+
"results": batch,
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
if show:
|
|
1404
|
+
confirmed = review_payload(
|
|
1405
|
+
{"audit_uuid": sub_uuid, "results_count": len(batch)},
|
|
1406
|
+
command_label=f"Submit Batch ({i // batch_size + 1})",
|
|
1407
|
+
context_note=(
|
|
1408
|
+
"This payload submits structured audit results "
|
|
1409
|
+
"(field names + values, NO source code) to CodeDD."
|
|
1410
|
+
),
|
|
1411
|
+
confirm_prompt="Submit this batch?",
|
|
1412
|
+
)
|
|
1413
|
+
if not confirmed:
|
|
1414
|
+
print_info("Batch skipped.")
|
|
1415
|
+
progress.advance(task)
|
|
1416
|
+
continue
|
|
1417
|
+
|
|
1418
|
+
try:
|
|
1419
|
+
resp = client.post(
|
|
1420
|
+
Endpoints.AUDIT_RESULTS, json=payload, timeout=90
|
|
1421
|
+
)
|
|
1422
|
+
|
|
1423
|
+
if resp.status_code == 200:
|
|
1424
|
+
body = resp.json()
|
|
1425
|
+
total_ingested += body.get("ingested", 0)
|
|
1426
|
+
total_errors += len(body.get("errors", []))
|
|
1427
|
+
else:
|
|
1428
|
+
msg = "batch submission failed"
|
|
1429
|
+
try:
|
|
1430
|
+
msg = resp.json().get("message", msg)
|
|
1431
|
+
except Exception:
|
|
1432
|
+
pass
|
|
1433
|
+
print_warning(f"Batch error: {msg}")
|
|
1434
|
+
total_errors += len(batch)
|
|
1435
|
+
except Exception as result_exc:
|
|
1436
|
+
print_warning(f"Result batch submission failed: {result_exc}")
|
|
1437
|
+
total_errors += len(batch)
|
|
1438
|
+
|
|
1439
|
+
progress.advance(task)
|
|
1440
|
+
|
|
1441
|
+
console.print()
|
|
1442
|
+
console.print(
|
|
1443
|
+
f" [green]{SYMBOL_OK}[/green] Results submitted: "
|
|
1444
|
+
f"[bold]{total_ingested}[/bold] ingested"
|
|
1445
|
+
+ (f", [yellow]{total_errors} error(s)[/yellow]" if total_errors else "")
|
|
1446
|
+
)
|
|
1447
|
+
console.print()
|
|
1448
|
+
|
|
1449
|
+
# ---- Step 3a.1: Run local source-based vulnerability validation --------
|
|
1450
|
+
validation_by_sub: dict[str, list[dict]] = {}
|
|
1451
|
+
file_to_sub_uuid = {f["file_path"]: f.get("sub_audit_uuid", "") for f in all_files}
|
|
1452
|
+
file_to_local_path: dict[str, str] = {}
|
|
1453
|
+
for f in all_files:
|
|
1454
|
+
repo_name = f.get("repo_name", "")
|
|
1455
|
+
rel_path = f.get("relative_path", "")
|
|
1456
|
+
local_root = scope_dirs.get(repo_name, "")
|
|
1457
|
+
if local_root and rel_path:
|
|
1458
|
+
file_to_local_path[f["file_path"]] = os.path.join(
|
|
1459
|
+
local_root, rel_path.replace("/", os.sep).replace("\\", os.sep)
|
|
1460
|
+
)
|
|
1461
|
+
|
|
1462
|
+
validation_candidates: list[ValidationCandidate] = []
|
|
1463
|
+
for r in successful_results:
|
|
1464
|
+
audit_data = r.audit_data or {}
|
|
1465
|
+
flag_color = str(audit_data.get("flag_color", "")).strip().lower()
|
|
1466
|
+
reasons = str(audit_data.get("reasons_of_flag", "")).strip()
|
|
1467
|
+
if flag_color not in {"red", "orange"}:
|
|
1468
|
+
continue
|
|
1469
|
+
if not reasons or reasons.upper() in {"N/A", "NONE"}:
|
|
1470
|
+
continue
|
|
1471
|
+
|
|
1472
|
+
local_path = file_to_local_path.get(r.file_path, "")
|
|
1473
|
+
sub_uuid = file_to_sub_uuid.get(r.file_path, "")
|
|
1474
|
+
if not local_path or not sub_uuid:
|
|
1475
|
+
continue
|
|
1476
|
+
|
|
1477
|
+
revised_time = audit_data.get("time_to_fix_flag")
|
|
1478
|
+
try:
|
|
1479
|
+
revised_time = float(revised_time) if revised_time is not None else None
|
|
1480
|
+
except (TypeError, ValueError):
|
|
1481
|
+
revised_time = None
|
|
1482
|
+
|
|
1483
|
+
validation_candidates.append(
|
|
1484
|
+
ValidationCandidate(
|
|
1485
|
+
file_path=r.file_path,
|
|
1486
|
+
local_path=local_path,
|
|
1487
|
+
hypothesis=reasons,
|
|
1488
|
+
flag_color=flag_color,
|
|
1489
|
+
stored_time_to_fix_hours=revised_time,
|
|
1490
|
+
)
|
|
1491
|
+
)
|
|
1492
|
+
|
|
1493
|
+
if validation_candidates:
|
|
1494
|
+
console.print("[bold]Validating flagged vulnerabilities locally…[/bold]\n")
|
|
1495
|
+
with LocalVulnerabilityValidator(
|
|
1496
|
+
anthropic_key=anthropic_key,
|
|
1497
|
+
openai_key=openai_key,
|
|
1498
|
+
provider_preference=preference,
|
|
1499
|
+
system_prompt=vulnerability_validation_prompt or None,
|
|
1500
|
+
on_debug=None,
|
|
1501
|
+
) as validator:
|
|
1502
|
+
validation_outcomes = validator.validate(validation_candidates)
|
|
1503
|
+
|
|
1504
|
+
for out in validation_outcomes:
|
|
1505
|
+
sub_uuid = file_to_sub_uuid.get(out.file_path, "")
|
|
1506
|
+
if not sub_uuid:
|
|
1507
|
+
continue
|
|
1508
|
+
validation_by_sub.setdefault(sub_uuid, []).append({
|
|
1509
|
+
"file_path": out.file_path,
|
|
1510
|
+
"conclusion": out.conclusion,
|
|
1511
|
+
"impact": out.impact,
|
|
1512
|
+
"confidence": out.confidence,
|
|
1513
|
+
"recommended_action": out.recommended_action,
|
|
1514
|
+
"revised_time_to_fix_hours": out.revised_time_to_fix_hours,
|
|
1515
|
+
})
|
|
1516
|
+
|
|
1517
|
+
if validation_by_sub:
|
|
1518
|
+
console.print("[bold]Submitting vulnerability validation outcomes…[/bold]\n")
|
|
1519
|
+
validation_applied = 0
|
|
1520
|
+
validation_failed = 0
|
|
1521
|
+
with CodeDDClient(config=cfg) as client:
|
|
1522
|
+
for sub_uuid, validations in validation_by_sub.items():
|
|
1523
|
+
payload = {
|
|
1524
|
+
"audit_uuid": sub_uuid,
|
|
1525
|
+
"validations": validations,
|
|
1526
|
+
}
|
|
1527
|
+
try:
|
|
1528
|
+
resp = client.post(
|
|
1529
|
+
Endpoints.AUDIT_VULNERABILITY_VALIDATION,
|
|
1530
|
+
json=payload,
|
|
1531
|
+
timeout=90,
|
|
1532
|
+
)
|
|
1533
|
+
if resp.status_code == 200:
|
|
1534
|
+
body = resp.json()
|
|
1535
|
+
validation_applied += int(body.get("applied", 0))
|
|
1536
|
+
validation_failed += len(body.get("errors", []))
|
|
1537
|
+
else:
|
|
1538
|
+
validation_failed += len(validations)
|
|
1539
|
+
except Exception:
|
|
1540
|
+
validation_failed += len(validations)
|
|
1541
|
+
|
|
1542
|
+
console.print(
|
|
1543
|
+
f" [green]{SYMBOL_OK}[/green] Validation outcomes submitted: "
|
|
1544
|
+
f"[bold]{validation_applied}[/bold] applied"
|
|
1545
|
+
+ (f", [yellow]{validation_failed} failed[/yellow]" if validation_failed else "")
|
|
1546
|
+
)
|
|
1547
|
+
console.print()
|
|
1548
|
+
|
|
1549
|
+
# ---- Step 3b: Submit complexity results --------------------------------
|
|
1550
|
+
if cx_ok:
|
|
1551
|
+
console.print("[bold]Submitting complexity metrics to CodeDD…[/bold]\n")
|
|
1552
|
+
|
|
1553
|
+
# Group by sub_audit_uuid
|
|
1554
|
+
cx_by_sub: dict[str, list[dict]] = {}
|
|
1555
|
+
for r in cx_ok:
|
|
1556
|
+
sub_uuid = ""
|
|
1557
|
+
for f in all_files:
|
|
1558
|
+
if f["file_path"] == r.file_path:
|
|
1559
|
+
sub_uuid = f.get("sub_audit_uuid", "")
|
|
1560
|
+
break
|
|
1561
|
+
cx_by_sub.setdefault(sub_uuid, []).append({
|
|
1562
|
+
"file_path": r.file_path,
|
|
1563
|
+
"metrics": r.metrics,
|
|
1564
|
+
})
|
|
1565
|
+
|
|
1566
|
+
# Compute per-sub-audit aggregated summary
|
|
1567
|
+
cx_ingested = 0
|
|
1568
|
+
cx_errors = 0
|
|
1569
|
+
|
|
1570
|
+
with Progress(
|
|
1571
|
+
SpinnerColumn(),
|
|
1572
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1573
|
+
BarColumn(bar_width=30),
|
|
1574
|
+
MofNCompleteColumn(),
|
|
1575
|
+
console=console,
|
|
1576
|
+
) as cx_prog:
|
|
1577
|
+
total_cx_batches = sum(
|
|
1578
|
+
(len(items) + batch_size - 1) // batch_size
|
|
1579
|
+
for items in cx_by_sub.values()
|
|
1580
|
+
)
|
|
1581
|
+
cx_task = cx_prog.add_task("Submitting…", total=total_cx_batches)
|
|
1582
|
+
|
|
1583
|
+
with CodeDDClient(config=cfg) as client:
|
|
1584
|
+
for sub_uuid, items in cx_by_sub.items():
|
|
1585
|
+
# Build aggregated summary for this sub-audit
|
|
1586
|
+
agg_input = {}
|
|
1587
|
+
for item in items:
|
|
1588
|
+
fp = item["file_path"]
|
|
1589
|
+
agg_input[fp] = item["metrics"]
|
|
1590
|
+
summary = aggregate_complexity_results(agg_input)
|
|
1591
|
+
|
|
1592
|
+
for i in range(0, len(items), batch_size):
|
|
1593
|
+
batch = items[i : i + batch_size]
|
|
1594
|
+
payload: dict = {
|
|
1595
|
+
"audit_uuid": sub_uuid,
|
|
1596
|
+
"results": batch,
|
|
1597
|
+
}
|
|
1598
|
+
# Attach summary only on the last batch
|
|
1599
|
+
if i + batch_size >= len(items):
|
|
1600
|
+
payload["summary"] = summary
|
|
1601
|
+
|
|
1602
|
+
if show:
|
|
1603
|
+
confirmed = review_payload(
|
|
1604
|
+
{"audit_uuid": sub_uuid, "results_count": len(batch)},
|
|
1605
|
+
command_label=f"Complexity Batch ({i // batch_size + 1})",
|
|
1606
|
+
context_note=(
|
|
1607
|
+
"This payload submits structured complexity metrics "
|
|
1608
|
+
"(cyclomatic + Halstead, NO source code) to CodeDD."
|
|
1609
|
+
),
|
|
1610
|
+
confirm_prompt="Submit this batch?",
|
|
1611
|
+
)
|
|
1612
|
+
if not confirmed:
|
|
1613
|
+
print_info("Batch skipped.")
|
|
1614
|
+
cx_prog.advance(cx_task)
|
|
1615
|
+
continue
|
|
1616
|
+
|
|
1617
|
+
try:
|
|
1618
|
+
resp = client.post(
|
|
1619
|
+
Endpoints.AUDIT_COMPLEXITY, json=payload, timeout=90
|
|
1620
|
+
)
|
|
1621
|
+
if resp.status_code == 200:
|
|
1622
|
+
body = resp.json()
|
|
1623
|
+
cx_ingested += body.get("ingested", 0)
|
|
1624
|
+
cx_errors += len(body.get("errors", []))
|
|
1625
|
+
else:
|
|
1626
|
+
msg = "complexity batch submission failed"
|
|
1627
|
+
try:
|
|
1628
|
+
msg = resp.json().get("message", msg)
|
|
1629
|
+
except Exception:
|
|
1630
|
+
pass
|
|
1631
|
+
print_warning(f"Complexity batch error: {msg}")
|
|
1632
|
+
cx_errors += len(batch)
|
|
1633
|
+
except Exception as cx_exc:
|
|
1634
|
+
print_warning(f"Complexity batch submission failed: {cx_exc}")
|
|
1635
|
+
cx_errors += len(batch)
|
|
1636
|
+
|
|
1637
|
+
cx_prog.advance(cx_task)
|
|
1638
|
+
|
|
1639
|
+
console.print()
|
|
1640
|
+
console.print(
|
|
1641
|
+
f" [green]{SYMBOL_OK}[/green] Complexity metrics submitted: "
|
|
1642
|
+
f"[bold]{cx_ingested}[/bold] ingested"
|
|
1643
|
+
+ (f", [yellow]{cx_errors} error(s)[/yellow]" if cx_errors else "")
|
|
1644
|
+
)
|
|
1645
|
+
console.print()
|
|
1646
|
+
|
|
1647
|
+
# ---- Step 3c: Submit dependency results --------------------------------
|
|
1648
|
+
if manifest_ok or import_results:
|
|
1649
|
+
console.print("[bold]Submitting dependency data to CodeDD…[/bold]\n")
|
|
1650
|
+
|
|
1651
|
+
dep_ingested_pkgs = 0
|
|
1652
|
+
dep_ingested_imports = 0
|
|
1653
|
+
dep_vulns_stored = 0
|
|
1654
|
+
dep_errors = 0
|
|
1655
|
+
|
|
1656
|
+
# Group manifest results by sub_audit_uuid (via repo_name → sub_audit)
|
|
1657
|
+
sub_audit_by_repo: dict[str, str] = {}
|
|
1658
|
+
for sa in sub_audits:
|
|
1659
|
+
sub_audit_by_repo[sa.get("repo_name", "")] = sa["audit_uuid"]
|
|
1660
|
+
|
|
1661
|
+
# Build per-sub-audit payloads
|
|
1662
|
+
dep_by_sub: dict[str, dict] = {}
|
|
1663
|
+
for mr in manifest_ok:
|
|
1664
|
+
sub_uuid = sub_audit_by_repo.get(mr.repo_name, "")
|
|
1665
|
+
if not sub_uuid:
|
|
1666
|
+
continue
|
|
1667
|
+
entry = dep_by_sub.setdefault(sub_uuid, {
|
|
1668
|
+
"manifest_packages": [],
|
|
1669
|
+
"import_packages": [],
|
|
1670
|
+
"vulnerabilities": {},
|
|
1671
|
+
})
|
|
1672
|
+
# Build manifest_path as cli:// path
|
|
1673
|
+
manifest_cli_path = f"cli://{sub_uuid}/{mr.repo_name}/{mr.manifest_path}"
|
|
1674
|
+
entry["manifest_packages"].append({
|
|
1675
|
+
"manifest_path": manifest_cli_path,
|
|
1676
|
+
"registry": mr.registry,
|
|
1677
|
+
"packages": mr.packages,
|
|
1678
|
+
})
|
|
1679
|
+
|
|
1680
|
+
for ir in import_results:
|
|
1681
|
+
# Determine sub_audit_uuid from file_path
|
|
1682
|
+
sub_uuid = ""
|
|
1683
|
+
for f in all_files:
|
|
1684
|
+
if f["file_path"] == ir.file_path:
|
|
1685
|
+
sub_uuid = f.get("sub_audit_uuid", "")
|
|
1686
|
+
break
|
|
1687
|
+
if not sub_uuid:
|
|
1688
|
+
continue
|
|
1689
|
+
entry = dep_by_sub.setdefault(sub_uuid, {
|
|
1690
|
+
"manifest_packages": [],
|
|
1691
|
+
"import_packages": [],
|
|
1692
|
+
"vulnerabilities": {},
|
|
1693
|
+
})
|
|
1694
|
+
entry["import_packages"].append({
|
|
1695
|
+
"file_path": ir.file_path,
|
|
1696
|
+
"registry_prefix": ir.registry_prefix,
|
|
1697
|
+
"packages": ir.packages,
|
|
1698
|
+
})
|
|
1699
|
+
|
|
1700
|
+
# Attach vulnerability data to each sub-audit payload
|
|
1701
|
+
for sub_uuid, entry in dep_by_sub.items():
|
|
1702
|
+
entry["vulnerabilities"] = vuln_results
|
|
1703
|
+
|
|
1704
|
+
dep_endpoint_404_shown = False # Only show once when server doesn't support the endpoint
|
|
1705
|
+
|
|
1706
|
+
with Progress(
|
|
1707
|
+
SpinnerColumn(),
|
|
1708
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1709
|
+
BarColumn(bar_width=30),
|
|
1710
|
+
MofNCompleteColumn(),
|
|
1711
|
+
console=console,
|
|
1712
|
+
) as dep_prog:
|
|
1713
|
+
dep_task = dep_prog.add_task("Submitting…", total=len(dep_by_sub))
|
|
1714
|
+
|
|
1715
|
+
with CodeDDClient(config=cfg) as client:
|
|
1716
|
+
for sub_uuid, dep_payload in dep_by_sub.items():
|
|
1717
|
+
payload = {
|
|
1718
|
+
"audit_uuid": sub_uuid,
|
|
1719
|
+
**dep_payload,
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
if show:
|
|
1723
|
+
confirmed = review_payload(
|
|
1724
|
+
{
|
|
1725
|
+
"audit_uuid": sub_uuid,
|
|
1726
|
+
"manifest_count": len(dep_payload["manifest_packages"]),
|
|
1727
|
+
"import_count": len(dep_payload["import_packages"]),
|
|
1728
|
+
"vuln_count": len(dep_payload["vulnerabilities"]),
|
|
1729
|
+
},
|
|
1730
|
+
command_label="Dependency Submission",
|
|
1731
|
+
context_note=(
|
|
1732
|
+
"This payload submits structured dependency data "
|
|
1733
|
+
"(package names + versions, NO source code) to CodeDD."
|
|
1734
|
+
),
|
|
1735
|
+
confirm_prompt="Submit dependency data?",
|
|
1736
|
+
)
|
|
1737
|
+
if not confirmed:
|
|
1738
|
+
print_info("Dependency submission skipped.")
|
|
1739
|
+
dep_prog.advance(dep_task)
|
|
1740
|
+
continue
|
|
1741
|
+
|
|
1742
|
+
try:
|
|
1743
|
+
# Dependency submission involves TypeDB writes on the
|
|
1744
|
+
# server; allow extra time for large manifests.
|
|
1745
|
+
resp = client.post(
|
|
1746
|
+
Endpoints.AUDIT_DEPENDENCIES, json=payload, timeout=120
|
|
1747
|
+
)
|
|
1748
|
+
if resp.status_code == 200:
|
|
1749
|
+
body = resp.json()
|
|
1750
|
+
dep_ingested_pkgs += body.get("ingested_packages", 0)
|
|
1751
|
+
dep_ingested_imports += body.get("ingested_imports", 0)
|
|
1752
|
+
dep_vulns_stored += body.get("vulnerabilities_stored", 0)
|
|
1753
|
+
dep_errors += len(body.get("errors", []))
|
|
1754
|
+
# Metadata enrichment (scorecards, vuln scanning)
|
|
1755
|
+
# runs as part of the Celery chain during post-processing
|
|
1756
|
+
elif resp.status_code == 404:
|
|
1757
|
+
if not dep_endpoint_404_shown:
|
|
1758
|
+
print_warning(
|
|
1759
|
+
"Server does not support dependency submission (404). "
|
|
1760
|
+
"Upgrade the CodeDD backend to enable dependency tracking."
|
|
1761
|
+
)
|
|
1762
|
+
dep_endpoint_404_shown = True
|
|
1763
|
+
else:
|
|
1764
|
+
msg = "dependency submission failed"
|
|
1765
|
+
try:
|
|
1766
|
+
msg = resp.json().get("message", msg)
|
|
1767
|
+
except Exception:
|
|
1768
|
+
pass
|
|
1769
|
+
print_warning(f"Dependency error: {msg}")
|
|
1770
|
+
dep_errors += 1
|
|
1771
|
+
except Exception as dep_submit_exc:
|
|
1772
|
+
print_warning(
|
|
1773
|
+
f"Dependency submission failed for sub-audit "
|
|
1774
|
+
f"{sub_uuid[:8]}…: {dep_submit_exc}"
|
|
1775
|
+
)
|
|
1776
|
+
dep_errors += 1
|
|
1777
|
+
|
|
1778
|
+
dep_prog.advance(dep_task)
|
|
1779
|
+
|
|
1780
|
+
console.print()
|
|
1781
|
+
console.print(
|
|
1782
|
+
f" [green]{SYMBOL_OK}[/green] Dependencies submitted: "
|
|
1783
|
+
f"[bold]{dep_ingested_pkgs}[/bold] packages, "
|
|
1784
|
+
f"[bold]{dep_ingested_imports}[/bold] imports, "
|
|
1785
|
+
f"[bold]{dep_vulns_stored}[/bold] vulns stored"
|
|
1786
|
+
+ (f", [yellow]{dep_errors} error(s)[/yellow]" if dep_errors else "")
|
|
1787
|
+
)
|
|
1788
|
+
if dep_ingested_pkgs > 0:
|
|
1789
|
+
console.print(
|
|
1790
|
+
f" [dim]{SYMBOL_INFO}[/dim] "
|
|
1791
|
+
"Package metadata enrichment (registry info, scorecards) "
|
|
1792
|
+
"will run during post-processing on CodeDD."
|
|
1793
|
+
)
|
|
1794
|
+
console.print()
|
|
1795
|
+
|
|
1796
|
+
# ---- Step 4: Signal completion ---------------------------------------
|
|
1797
|
+
console.print("[bold]Triggering post-processing on CodeDD…[/bold]\n")
|
|
1798
|
+
|
|
1799
|
+
complete_payload = {"audit_uuid": audit_uuid, "trigger_next_steps": True}
|
|
1800
|
+
if show:
|
|
1801
|
+
confirmed = review_payload(
|
|
1802
|
+
complete_payload,
|
|
1803
|
+
command_label="Audit Complete",
|
|
1804
|
+
context_note=(
|
|
1805
|
+
"This tells CodeDD that file-level auditing is done and to "
|
|
1806
|
+
"trigger consolidation / recommendations (Steps 7-9)."
|
|
1807
|
+
),
|
|
1808
|
+
confirm_prompt="Proceed with audit completion?",
|
|
1809
|
+
)
|
|
1810
|
+
if not confirmed:
|
|
1811
|
+
print_info("Cancelled.")
|
|
1812
|
+
raise typer.Exit(code=0)
|
|
1813
|
+
|
|
1814
|
+
with Progress(
|
|
1815
|
+
SpinnerColumn(),
|
|
1816
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1817
|
+
console=console,
|
|
1818
|
+
transient=True,
|
|
1819
|
+
) as prog:
|
|
1820
|
+
prog.add_task("Finalising…", total=None)
|
|
1821
|
+
with CodeDDClient(config=cfg) as client:
|
|
1822
|
+
resp = client.post(Endpoints.AUDIT_COMPLETE, json=complete_payload)
|
|
1823
|
+
|
|
1824
|
+
if resp.status_code != 200:
|
|
1825
|
+
msg = "Failed to signal audit completion"
|
|
1826
|
+
try:
|
|
1827
|
+
msg = resp.json().get("message", msg)
|
|
1828
|
+
except Exception:
|
|
1829
|
+
pass
|
|
1830
|
+
print_error(msg)
|
|
1831
|
+
raise typer.Exit(code=1)
|
|
1832
|
+
|
|
1833
|
+
complete_body = resp.json()
|
|
1834
|
+
if complete_body.get("status") != "success":
|
|
1835
|
+
print_error(complete_body.get("message", "Unknown error during completion"))
|
|
1836
|
+
raise typer.Exit(code=1)
|
|
1837
|
+
|
|
1838
|
+
# ---- Final summary ---------------------------------------------------
|
|
1839
|
+
console.print()
|
|
1840
|
+
minutes, seconds = divmod(int(elapsed), 60)
|
|
1841
|
+
time_str = f"{minutes}m {seconds}s" if minutes else f"{seconds}s"
|
|
1842
|
+
|
|
1843
|
+
print_success("[bold]Audit complete![/bold]")
|
|
1844
|
+
console.print(f" Files audited: [bold]{len(successful_results)}[/bold]")
|
|
1845
|
+
if cx_ok:
|
|
1846
|
+
console.print(f" Complexity analysed: [bold]{len(cx_ok)}[/bold] files")
|
|
1847
|
+
if arch_submitted:
|
|
1848
|
+
console.print(
|
|
1849
|
+
f" Architecture: [bold]{arch_components_total}[/bold] components, "
|
|
1850
|
+
f"[bold]{arch_relationships_total}[/bold] relationships"
|
|
1851
|
+
)
|
|
1852
|
+
if manifest_ok or import_results:
|
|
1853
|
+
console.print(
|
|
1854
|
+
f" Dependencies: [bold]{total_dep_pkgs}[/bold] packages, "
|
|
1855
|
+
f"[bold]{total_import_pkgs}[/bold] imports"
|
|
1856
|
+
)
|
|
1857
|
+
if vuln_found:
|
|
1858
|
+
console.print(f" Vulnerabilities: [bold yellow]{vuln_found}[/bold yellow] packages affected")
|
|
1859
|
+
console.print(f" Time: [bold]{time_str}[/bold]")
|
|
1860
|
+
|
|
1861
|
+
if provider_stats:
|
|
1862
|
+
provider_parts = []
|
|
1863
|
+
for p, count in sorted(provider_stats.items()):
|
|
1864
|
+
provider_parts.append(f"{p.capitalize()} ({count} files)")
|
|
1865
|
+
console.print(f" Provider: {' | '.join(provider_parts)}")
|
|
1866
|
+
|
|
1867
|
+
if complete_body.get("post_processing_triggered"):
|
|
1868
|
+
console.print(
|
|
1869
|
+
f" [dim]Post-processing (dependency enrichment, consolidation "
|
|
1870
|
+
f"& recommendations) is running on CodeDD.[/dim]"
|
|
1871
|
+
)
|
|
1872
|
+
|
|
1873
|
+
console.print()
|
|
1874
|
+
print_info(
|
|
1875
|
+
"Track results at [bold cyan]codedd.ai[/bold cyan] "
|
|
1876
|
+
"or run [bold cyan]codedd audits list[/bold cyan]."
|
|
1877
|
+
)
|