codedd-cli 0.1.4__tar.gz → 0.1.5__tar.gz
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-0.1.4 → codedd_cli-0.1.5}/PKG-INFO +1 -1
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/__init__.py +1 -1
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/api/client.py +2 -2
- codedd_cli-0.1.5/codedd_cli/auditor/audit_checkpoint.py +179 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/file_auditor.py +29 -5
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/sub_audit_runner.py +267 -122
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/vulnerability_validator.py +40 -6
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/commands/audit_cmd.py +687 -7
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/commands/audits_cmd.py +12 -1
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/commands/auth_cmd.py +9 -12
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/commands/config_cmd.py +21 -20
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/commands/fix_cmd.py +466 -9
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/commands/scope_cmd.py +18 -4
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/config/constants.py +11 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/config/settings.py +23 -2
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/llm/key_manager.py +4 -1
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/pyproject.toml +1 -1
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/LICENSE +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/README.md +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/__main__.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/api/__init__.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/api/endpoints.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/api/exceptions.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/__init__.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/archetype_classifier.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/architecture_analyzer.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/architecture_prompts.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/commit_classifier.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/complexity_analyzer.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/dependency_scanner.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/enhanced_architecture_detectors.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/git_stats_collector.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/response_parser.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auditor/tier_definitions.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auth/__init__.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auth/session.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/auth/token_manager.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/cli.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/commands/__init__.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/commands/ai_docs_cmd.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/config/__init__.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/config/fix_session.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/llm/__init__.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/models/__init__.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/models/account.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/models/audit.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/models/local_directory.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/scanner/__init__.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/scanner/file_classifier.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/scanner/file_walker.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/scanner/line_counter.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/utils/__init__.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/utils/directory_validator.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/utils/display.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/utils/payload_inspector.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/utils/security.py +0 -0
- {codedd_cli-0.1.4 → codedd_cli-0.1.5}/codedd_cli/utils/validators.py +0 -0
|
@@ -37,10 +37,10 @@ class CodeDDClient:
|
|
|
37
37
|
data = resp.json()
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
|
-
def __init__(self, config: ConfigManager | None = None) -> None:
|
|
40
|
+
def __init__(self, config: ConfigManager | None = None, token: str | None = None) -> None:
|
|
41
41
|
self._config = config or ConfigManager()
|
|
42
42
|
self._base_url = self._config.api_url.rstrip("/")
|
|
43
|
-
self._token = TokenManager.retrieve()
|
|
43
|
+
self._token = token if token is not None else TokenManager.retrieve()
|
|
44
44
|
|
|
45
45
|
# For localhost HTTP, disable TLS verification (not applicable anyway)
|
|
46
46
|
# For HTTPS, always verify certificates
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Persistent checkpoint for resumable local audit sessions.
|
|
3
|
+
|
|
4
|
+
State is written to ``~/.codedd/audit_progress_{audit_uuid}.json`` after
|
|
5
|
+
every batch submission so an interrupted audit can resume where it left off.
|
|
6
|
+
Writes are atomic (write-then-rename) so a crash mid-save never corrupts the
|
|
7
|
+
file.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
_FORMAT_VERSION = 1
|
|
21
|
+
_FILENAME_PREFIX = "audit_progress_"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AuditCheckpoint:
|
|
25
|
+
"""Tracks per-sub-audit submission progress across CLI sessions."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, audit_uuid: str, config_dir: Path) -> None:
|
|
28
|
+
self._audit_uuid = audit_uuid
|
|
29
|
+
self._path = config_dir / f"{_FILENAME_PREFIX}{audit_uuid}.json"
|
|
30
|
+
self._data: dict = self._load()
|
|
31
|
+
|
|
32
|
+
# ---- Internal persistence -----------------------------------------------
|
|
33
|
+
|
|
34
|
+
def _empty(self) -> dict:
|
|
35
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
36
|
+
return {
|
|
37
|
+
"version": _FORMAT_VERSION,
|
|
38
|
+
"audit_uuid": self._audit_uuid,
|
|
39
|
+
"created_at": now,
|
|
40
|
+
"updated_at": now,
|
|
41
|
+
"sub_audits": {},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def _load(self) -> dict:
|
|
45
|
+
if not self._path.exists():
|
|
46
|
+
return self._empty()
|
|
47
|
+
try:
|
|
48
|
+
with open(self._path, encoding="utf-8") as fh:
|
|
49
|
+
data = json.load(fh)
|
|
50
|
+
if not isinstance(data, dict) or data.get("audit_uuid") != self._audit_uuid:
|
|
51
|
+
logger.warning("Checkpoint mismatch or corrupt — starting fresh.")
|
|
52
|
+
return self._empty()
|
|
53
|
+
return data
|
|
54
|
+
except Exception as exc:
|
|
55
|
+
logger.warning("Cannot read checkpoint (%s) — starting fresh.", exc)
|
|
56
|
+
return self._empty()
|
|
57
|
+
|
|
58
|
+
def _save(self) -> None:
|
|
59
|
+
self._data["updated_at"] = datetime.now(timezone.utc).isoformat()
|
|
60
|
+
tmp = self._path.with_suffix(".json.tmp")
|
|
61
|
+
try:
|
|
62
|
+
with open(tmp, "w", encoding="utf-8") as fh:
|
|
63
|
+
json.dump(self._data, fh, indent=2)
|
|
64
|
+
os.replace(tmp, self._path)
|
|
65
|
+
except Exception as exc:
|
|
66
|
+
logger.warning("Checkpoint save failed: %s", exc)
|
|
67
|
+
try:
|
|
68
|
+
tmp.unlink(missing_ok=True)
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
def _sub(self, sub_uuid: str) -> dict:
|
|
73
|
+
subs = self._data.setdefault("sub_audits", {})
|
|
74
|
+
if sub_uuid not in subs:
|
|
75
|
+
subs[sub_uuid] = {"submitted_file_paths": [], "vuln_submitted_count": 0}
|
|
76
|
+
return subs[sub_uuid]
|
|
77
|
+
|
|
78
|
+
# ---- File audit phase ---------------------------------------------------
|
|
79
|
+
|
|
80
|
+
def get_submitted_files(self, sub_uuid: str) -> set[str]:
|
|
81
|
+
"""Return the set of file_path strings already submitted for this sub-audit."""
|
|
82
|
+
return set(self._sub(sub_uuid).get("submitted_file_paths", []))
|
|
83
|
+
|
|
84
|
+
def mark_files_submitted(self, sub_uuid: str, file_paths: list[str]) -> None:
|
|
85
|
+
"""Append newly submitted file paths and persist immediately."""
|
|
86
|
+
sub = self._sub(sub_uuid)
|
|
87
|
+
existing: set[str] = set(sub.get("submitted_file_paths", []))
|
|
88
|
+
added = False
|
|
89
|
+
for fp in file_paths:
|
|
90
|
+
if fp not in existing:
|
|
91
|
+
sub.setdefault("submitted_file_paths", []).append(fp)
|
|
92
|
+
existing.add(fp)
|
|
93
|
+
added = True
|
|
94
|
+
if added:
|
|
95
|
+
self._save()
|
|
96
|
+
|
|
97
|
+
# ---- Vulnerability validation phase ------------------------------------
|
|
98
|
+
|
|
99
|
+
def get_vuln_submitted_count(self, sub_uuid: str) -> int:
|
|
100
|
+
"""Number of vuln-validation outcomes already submitted for this sub-audit."""
|
|
101
|
+
# Path-based tracking is preferred; fall back to legacy count field.
|
|
102
|
+
paths = self._sub(sub_uuid).get("vuln_validated_paths")
|
|
103
|
+
if paths is not None:
|
|
104
|
+
return len(paths)
|
|
105
|
+
return int(self._sub(sub_uuid).get("vuln_submitted_count", 0))
|
|
106
|
+
|
|
107
|
+
def mark_vulns_submitted(self, sub_uuid: str, total_count: int) -> None:
|
|
108
|
+
"""Update the total count of submitted vuln validations and persist."""
|
|
109
|
+
self._sub(sub_uuid)["vuln_submitted_count"] = total_count
|
|
110
|
+
self._save()
|
|
111
|
+
|
|
112
|
+
def get_vuln_validated_paths(self, sub_uuid: str) -> set[str]:
|
|
113
|
+
"""Return file_paths whose validation outcomes were already submitted."""
|
|
114
|
+
return set(self._sub(sub_uuid).get("vuln_validated_paths", []))
|
|
115
|
+
|
|
116
|
+
def mark_vuln_paths_submitted(self, sub_uuid: str, file_paths: list[str]) -> None:
|
|
117
|
+
"""Append newly submitted vuln file_paths and persist immediately."""
|
|
118
|
+
sub = self._sub(sub_uuid)
|
|
119
|
+
existing: list[str] = sub.setdefault("vuln_validated_paths", [])
|
|
120
|
+
existing_set = set(existing)
|
|
121
|
+
added = False
|
|
122
|
+
for fp in file_paths:
|
|
123
|
+
if fp not in existing_set:
|
|
124
|
+
existing.append(fp)
|
|
125
|
+
existing_set.add(fp)
|
|
126
|
+
added = True
|
|
127
|
+
if added:
|
|
128
|
+
self._save()
|
|
129
|
+
|
|
130
|
+
def set_vuln_candidates(self, sub_uuid: str, candidates: list[dict]) -> None:
|
|
131
|
+
"""Persist serialised validation candidates for recovery on resume."""
|
|
132
|
+
self._sub(sub_uuid)["vuln_candidates"] = candidates
|
|
133
|
+
self._save()
|
|
134
|
+
|
|
135
|
+
def get_vuln_candidates(self, sub_uuid: str) -> list[dict]:
|
|
136
|
+
"""Return previously persisted validation candidates (empty if none)."""
|
|
137
|
+
return list(self._sub(sub_uuid).get("vuln_candidates", []))
|
|
138
|
+
|
|
139
|
+
# ---- Audit plan metadata ------------------------------------------------
|
|
140
|
+
|
|
141
|
+
def set_plan_file_count(self, count: int) -> None:
|
|
142
|
+
"""Persist the total number of files in the audit plan."""
|
|
143
|
+
if count > 0:
|
|
144
|
+
self._data["plan_file_count"] = count
|
|
145
|
+
self._save()
|
|
146
|
+
|
|
147
|
+
def get_plan_file_count(self) -> int:
|
|
148
|
+
"""Return the stored audit plan file count (0 if not yet set)."""
|
|
149
|
+
return int(self._data.get("plan_file_count", 0))
|
|
150
|
+
|
|
151
|
+
# ---- Introspection ------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
def has_progress(self) -> bool:
|
|
154
|
+
"""True if the checkpoint file exists and has at least one submitted file."""
|
|
155
|
+
if not self._path.exists():
|
|
156
|
+
return False
|
|
157
|
+
return self.total_submitted_files() > 0
|
|
158
|
+
|
|
159
|
+
def total_submitted_files(self) -> int:
|
|
160
|
+
return sum(
|
|
161
|
+
len(sub.get("submitted_file_paths", []))
|
|
162
|
+
for sub in self._data.get("sub_audits", {}).values()
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def created_at(self) -> str | None:
|
|
166
|
+
return self._data.get("created_at")
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def path(self) -> Path:
|
|
170
|
+
return self._path
|
|
171
|
+
|
|
172
|
+
# ---- Lifecycle ----------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
def clear(self) -> None:
|
|
175
|
+
"""Delete checkpoint file after a successful, complete audit."""
|
|
176
|
+
try:
|
|
177
|
+
self._path.unlink(missing_ok=True)
|
|
178
|
+
except Exception as exc:
|
|
179
|
+
logger.warning("Could not delete checkpoint file: %s", exc)
|
|
@@ -21,6 +21,7 @@ from __future__ import annotations
|
|
|
21
21
|
|
|
22
22
|
import logging
|
|
23
23
|
import random
|
|
24
|
+
import threading
|
|
24
25
|
import time
|
|
25
26
|
from collections.abc import Callable
|
|
26
27
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
@@ -34,7 +35,7 @@ from codedd_cli.auditor.response_parser import (
|
|
|
34
35
|
is_response_complete,
|
|
35
36
|
parse_audit_response,
|
|
36
37
|
)
|
|
37
|
-
from codedd_cli.llm.key_manager import PROVIDER_MODELS
|
|
38
|
+
from codedd_cli.llm.key_manager import KEY_PROVIDERS, PROVIDER_MODELS
|
|
38
39
|
|
|
39
40
|
# Callback for debug dump: (full_prompt, response_text, audit_data, none_count)
|
|
40
41
|
OnDumpLLMCallback = Callable[[str, str, dict | None, int], None]
|
|
@@ -230,13 +231,11 @@ class LocalFileAuditor:
|
|
|
230
231
|
if not available:
|
|
231
232
|
return None, None
|
|
232
233
|
|
|
233
|
-
if self._preference in
|
|
234
|
+
if self._preference in KEY_PROVIDERS:
|
|
234
235
|
ordered = [self._preference] + [p for p in available if p != self._preference]
|
|
235
236
|
ordered = [p for p in ordered if p in available]
|
|
236
237
|
else:
|
|
237
|
-
|
|
238
|
-
default_order = ("anthropic", "openai", "gemini", "grok")
|
|
239
|
-
ordered = [p for p in default_order if p in available]
|
|
238
|
+
ordered = [p for p in KEY_PROVIDERS if p in available]
|
|
240
239
|
|
|
241
240
|
primary = ordered[0] if ordered else None
|
|
242
241
|
fallback = ordered[1] if len(ordered) > 1 else None
|
|
@@ -453,6 +452,8 @@ class LocalFileAuditor:
|
|
|
453
452
|
files: list[dict],
|
|
454
453
|
scope_dirs: dict[str, str],
|
|
455
454
|
on_progress: Callable[[AuditFileResult], None] | None = None,
|
|
455
|
+
on_batch: Callable[[list[AuditFileResult]], None] | None = None,
|
|
456
|
+
batch_size: int = 25,
|
|
456
457
|
) -> list[AuditFileResult]:
|
|
457
458
|
"""
|
|
458
459
|
Audit a list of files concurrently using a thread pool.
|
|
@@ -463,11 +464,17 @@ class LocalFileAuditor:
|
|
|
463
464
|
sub-audit's ``root_path``.
|
|
464
465
|
scope_dirs: Mapping of ``repo_name`` → local directory path.
|
|
465
466
|
on_progress: Optional callback invoked after each file completes.
|
|
467
|
+
on_batch: Optional callback invoked with a list of results every
|
|
468
|
+
``batch_size`` completions (and once for the final partial
|
|
469
|
+
batch). Called outside the internal lock so I/O is safe.
|
|
470
|
+
batch_size: How many results to accumulate before firing ``on_batch``.
|
|
466
471
|
|
|
467
472
|
Returns:
|
|
468
473
|
List of ``AuditFileResult`` objects (one per file).
|
|
469
474
|
"""
|
|
470
475
|
results: list[AuditFileResult] = []
|
|
476
|
+
_pending: list[AuditFileResult] = []
|
|
477
|
+
_lock = threading.Lock()
|
|
471
478
|
|
|
472
479
|
with ThreadPoolExecutor(max_workers=self._max_concurrent) as pool:
|
|
473
480
|
future_map = {}
|
|
@@ -498,10 +505,27 @@ class LocalFileAuditor:
|
|
|
498
505
|
relative_path=info["relative_path"],
|
|
499
506
|
error=str(exc),
|
|
500
507
|
)
|
|
508
|
+
|
|
501
509
|
results.append(result)
|
|
510
|
+
|
|
511
|
+
batch_to_emit: list[AuditFileResult] | None = None
|
|
512
|
+
if on_batch:
|
|
513
|
+
with _lock:
|
|
514
|
+
_pending.append(result)
|
|
515
|
+
if len(_pending) >= batch_size:
|
|
516
|
+
batch_to_emit = _pending[:batch_size]
|
|
517
|
+
del _pending[:batch_size]
|
|
518
|
+
|
|
519
|
+
if batch_to_emit is not None:
|
|
520
|
+
on_batch(batch_to_emit)
|
|
521
|
+
|
|
502
522
|
if on_progress:
|
|
503
523
|
on_progress(result)
|
|
504
524
|
|
|
525
|
+
# Emit final partial batch
|
|
526
|
+
if on_batch and _pending:
|
|
527
|
+
on_batch(_pending)
|
|
528
|
+
|
|
505
529
|
return results
|
|
506
530
|
|
|
507
531
|
# ------------------------------------------------------------------
|