veyra-proof 0.2.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.
- veyra_proof/__init__.py +4 -0
- veyra_proof/__main__.py +6 -0
- veyra_proof/cli.py +530 -0
- veyra_proof/command_runner.py +214 -0
- veyra_proof/config.py +329 -0
- veyra_proof/discovery.py +112 -0
- veyra_proof/hashing.py +79 -0
- veyra_proof/installer.py +150 -0
- veyra_proof/models.py +186 -0
- veyra_proof/platform_info.py +111 -0
- veyra_proof/reporting.py +311 -0
- veyra_proof/templates/report.md.j2 +50 -0
- veyra_proof/tool_registry.py +242 -0
- veyra_proof/tools/__init__.py +52 -0
- veyra_proof/tools/anti_theater.py +465 -0
- veyra_proof/tools/audit.py +94 -0
- veyra_proof/tools/base.py +342 -0
- veyra_proof/tools/cargo_check.py +59 -0
- veyra_proof/tools/cargo_test.py +68 -0
- veyra_proof/tools/check_external_types.py +71 -0
- veyra_proof/tools/clippy.py +59 -0
- veyra_proof/tools/deny.py +116 -0
- veyra_proof/tools/dylint.py +75 -0
- veyra_proof/tools/e2e.py +667 -0
- veyra_proof/tools/fuzz.py +110 -0
- veyra_proof/tools/geiger.py +136 -0
- veyra_proof/tools/kani.py +94 -0
- veyra_proof/tools/llvm_cov.py +146 -0
- veyra_proof/tools/machete.py +71 -0
- veyra_proof/tools/miri.py +86 -0
- veyra_proof/tools/mutants.py +82 -0
- veyra_proof/tools/nextest.py +68 -0
- veyra_proof/tools/reproducible_build.py +185 -0
- veyra_proof/tools/rustfmt.py +77 -0
- veyra_proof/tools/sast_optional.py +167 -0
- veyra_proof/tools/semver_checks.py +90 -0
- veyra_proof/tools/vet.py +71 -0
- veyra_proof/workspace.py +136 -0
- veyra_proof-0.2.0.dist-info/METADATA +339 -0
- veyra_proof-0.2.0.dist-info/RECORD +44 -0
- veyra_proof-0.2.0.dist-info/WHEEL +5 -0
- veyra_proof-0.2.0.dist-info/entry_points.txt +2 -0
- veyra_proof-0.2.0.dist-info/licenses/LICENSE +22 -0
- veyra_proof-0.2.0.dist-info/top_level.txt +1 -0
veyra_proof/__init__.py
ADDED
veyra_proof/__main__.py
ADDED
veyra_proof/cli.py
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
"""CLI for Veyra Proof."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
import traceback
|
|
9
|
+
from dataclasses import asdict
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Sequence
|
|
12
|
+
|
|
13
|
+
from veyra_proof import __version__
|
|
14
|
+
from veyra_proof.command_runner import CommandRunner
|
|
15
|
+
from veyra_proof.config import load_veyra_config
|
|
16
|
+
from veyra_proof.discovery import run_doctor
|
|
17
|
+
from veyra_proof.hashing import compare_snapshots, snapshot_project_files
|
|
18
|
+
from veyra_proof.installer import confirm_installs, execute_install_plan
|
|
19
|
+
from veyra_proof.models import DetectionResult, InstallPlan, ToolResult
|
|
20
|
+
from veyra_proof.platform_info import detect_platform
|
|
21
|
+
from veyra_proof.reporting import (
|
|
22
|
+
classify_project_changes,
|
|
23
|
+
decide,
|
|
24
|
+
default_evidence_dir,
|
|
25
|
+
render_markdown,
|
|
26
|
+
utc_timestamp_dir,
|
|
27
|
+
write_evidence,
|
|
28
|
+
)
|
|
29
|
+
from veyra_proof.tool_registry import (
|
|
30
|
+
required_tool_ids,
|
|
31
|
+
registry_rows,
|
|
32
|
+
select_tools,
|
|
33
|
+
unknown_include_ids,
|
|
34
|
+
)
|
|
35
|
+
from veyra_proof.tools.base import AuditContext, utc_now_iso
|
|
36
|
+
from veyra_proof.workspace import WorkspaceError, resolve_workspace
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _parse_csv(value: str | None) -> list[str]:
|
|
40
|
+
if not value:
|
|
41
|
+
return []
|
|
42
|
+
return [part.strip() for part in value.split(",") if part.strip()]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
46
|
+
parser = argparse.ArgumentParser(
|
|
47
|
+
prog="veyra",
|
|
48
|
+
description="Audit a local Rust workspace against defective, insecure, or theatrical/fake code.",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument("--version", action="version", version=f"veyra {__version__}")
|
|
51
|
+
|
|
52
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
53
|
+
|
|
54
|
+
sub.add_parser("doctor", help="Diagnose Python/OS/Cargo/rustup without installing anything")
|
|
55
|
+
sub.add_parser("list-tools", help="List tool registry metadata")
|
|
56
|
+
|
|
57
|
+
def add_common(p: argparse.ArgumentParser) -> None:
|
|
58
|
+
p.add_argument("path", type=str, help="Path to Cargo package or workspace")
|
|
59
|
+
p.add_argument("--profile", choices=["quick", "standard", "hardened"], default="standard")
|
|
60
|
+
p.add_argument("--yes", action="store_true", help="Non-interactive authorization for installs")
|
|
61
|
+
p.add_argument("--install-missing", action="store_true", help="Allow installing compatible missing tools")
|
|
62
|
+
p.add_argument("--update-lockfile", action="store_true", help="Authorize cargo update")
|
|
63
|
+
p.add_argument(
|
|
64
|
+
"--allow-manifest-changes",
|
|
65
|
+
action="store_true",
|
|
66
|
+
help="Authorize future Cargo.toml mutations (none implemented in V1)",
|
|
67
|
+
)
|
|
68
|
+
p.add_argument("--include", type=str, default="", help="Comma-separated tool ids to run only")
|
|
69
|
+
p.add_argument("--exclude", type=str, default="", help="Comma-separated tool ids to exclude")
|
|
70
|
+
p.add_argument("--evidence-dir", type=str, default=None)
|
|
71
|
+
p.add_argument("--format", choices=["terminal", "markdown", "json", "both"], default="both")
|
|
72
|
+
p.add_argument("--fail-on", choices=["none", "findings", "error", "inconclusive"], default="findings")
|
|
73
|
+
p.add_argument("--dry-run", action="store_true")
|
|
74
|
+
p.add_argument("--timeout", type=float, default=900.0)
|
|
75
|
+
p.add_argument("--verbose", action="store_true")
|
|
76
|
+
|
|
77
|
+
plan = sub.add_parser("plan", help="Show execution plan without running checks")
|
|
78
|
+
add_common(plan)
|
|
79
|
+
|
|
80
|
+
audit = sub.add_parser("audit", help="Run the audit")
|
|
81
|
+
add_common(audit)
|
|
82
|
+
return parser
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def cmd_doctor() -> int:
|
|
86
|
+
report = run_doctor(CommandRunner())
|
|
87
|
+
print(json.dumps(report.to_dict(), indent=2))
|
|
88
|
+
if not report.cargo_ok:
|
|
89
|
+
print("\nCargo is required and was not detected.", file=sys.stderr)
|
|
90
|
+
return 2
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def cmd_list_tools() -> int:
|
|
95
|
+
print(json.dumps(registry_rows(), indent=2))
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _build_plan_dict(
|
|
100
|
+
*,
|
|
101
|
+
ctx: AuditContext,
|
|
102
|
+
tools: list,
|
|
103
|
+
detections: dict[str, DetectionResult],
|
|
104
|
+
install_plans: list[InstallPlan],
|
|
105
|
+
profile: str,
|
|
106
|
+
update_lockfile: bool,
|
|
107
|
+
allow_manifest_changes: bool,
|
|
108
|
+
yes: bool,
|
|
109
|
+
unknown_includes: list[str],
|
|
110
|
+
) -> dict:
|
|
111
|
+
commands_preview = []
|
|
112
|
+
for tool in tools:
|
|
113
|
+
det = detections.get(tool.id)
|
|
114
|
+
cmds = tool.planned_commands(ctx) if det and det.available else []
|
|
115
|
+
if det and not det.available:
|
|
116
|
+
note = det.message or "missing"
|
|
117
|
+
elif not cmds:
|
|
118
|
+
note = "no commands (likely config-gated skip at run time)"
|
|
119
|
+
else:
|
|
120
|
+
note = "ready"
|
|
121
|
+
commands_preview.append(
|
|
122
|
+
{
|
|
123
|
+
"tool": tool.id,
|
|
124
|
+
"available": bool(det and det.available),
|
|
125
|
+
"commands": cmds,
|
|
126
|
+
"note": note,
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
return {
|
|
130
|
+
"profile": profile,
|
|
131
|
+
"selected_tools": [t.id for t in tools],
|
|
132
|
+
"unknown_includes": unknown_includes,
|
|
133
|
+
"detections": {k: v.to_dict() for k, v in detections.items()},
|
|
134
|
+
"pending_installs": [p.to_dict() for p in install_plans],
|
|
135
|
+
"commands_preview": commands_preview,
|
|
136
|
+
"cargo_lock_policy": {
|
|
137
|
+
"update_lockfile_authorized": update_lockfile,
|
|
138
|
+
"will_run_cargo_update": update_lockfile,
|
|
139
|
+
},
|
|
140
|
+
"cargo_toml_policy": {
|
|
141
|
+
"allow_manifest_changes": allow_manifest_changes,
|
|
142
|
+
"automatic_mutations_implemented": False,
|
|
143
|
+
"message": (
|
|
144
|
+
"Manifest changes are authorized but no automatic manifest mutation is implemented."
|
|
145
|
+
if allow_manifest_changes
|
|
146
|
+
else "Cargo.toml modifications are prohibited by default."
|
|
147
|
+
),
|
|
148
|
+
},
|
|
149
|
+
"authorization_mode": "non-interactive (--yes)" if yes else "interactive",
|
|
150
|
+
"timestamp_utc": ctx.extras.get("timestamp"),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _prepare_context(
|
|
155
|
+
args: argparse.Namespace,
|
|
156
|
+
*,
|
|
157
|
+
create_evidence_dir: bool,
|
|
158
|
+
) -> tuple[AuditContext | None, dict, list, dict[str, DetectionResult], list[InstallPlan], int | None]:
|
|
159
|
+
platform = detect_platform(CommandRunner(verbose=bool(args.verbose)))
|
|
160
|
+
if not platform.cargo_path:
|
|
161
|
+
print("ERROR: Cargo not found on PATH. Install Rust/Cargo before auditing.", file=sys.stderr)
|
|
162
|
+
return None, {}, [], {}, [], 2
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
workspace = resolve_workspace(args.path, CommandRunner(verbose=bool(args.verbose)))
|
|
166
|
+
except WorkspaceError as exc:
|
|
167
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
168
|
+
return None, {}, [], {}, [], 2
|
|
169
|
+
|
|
170
|
+
config = load_veyra_config(Path(workspace.workspace_root))
|
|
171
|
+
include = _parse_csv(args.include)
|
|
172
|
+
exclude = _parse_csv(args.exclude)
|
|
173
|
+
unknown = unknown_include_ids(include)
|
|
174
|
+
if unknown:
|
|
175
|
+
print(f"WARNING: unknown --include tool id(s): {', '.join(unknown)}", file=sys.stderr)
|
|
176
|
+
|
|
177
|
+
tools = select_tools(
|
|
178
|
+
args.profile,
|
|
179
|
+
include=include,
|
|
180
|
+
exclude=exclude,
|
|
181
|
+
config_enable=config.tools.enable,
|
|
182
|
+
config_disable=config.tools.disable,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
timestamp = utc_timestamp_dir()
|
|
186
|
+
if args.evidence_dir:
|
|
187
|
+
evidence = Path(args.evidence_dir)
|
|
188
|
+
# If user path has no timestamp leaf, still record our stamp for reports
|
|
189
|
+
else:
|
|
190
|
+
evidence = default_evidence_dir(Path(workspace.workspace_root), timestamp)
|
|
191
|
+
|
|
192
|
+
if create_evidence_dir and not args.dry_run:
|
|
193
|
+
evidence.mkdir(parents=True, exist_ok=True)
|
|
194
|
+
(evidence / "tools").mkdir(exist_ok=True)
|
|
195
|
+
|
|
196
|
+
runner = CommandRunner(dry_run=bool(args.dry_run), verbose=bool(args.verbose))
|
|
197
|
+
detect_runner = CommandRunner(verbose=bool(args.verbose), dry_run=False)
|
|
198
|
+
|
|
199
|
+
common = dict(
|
|
200
|
+
workspace=workspace,
|
|
201
|
+
platform=platform,
|
|
202
|
+
config=config,
|
|
203
|
+
evidence_dir=evidence,
|
|
204
|
+
profile=args.profile,
|
|
205
|
+
timeout_seconds=float(args.timeout),
|
|
206
|
+
verbose=bool(args.verbose),
|
|
207
|
+
yes=bool(args.yes),
|
|
208
|
+
install_missing=bool(args.install_missing),
|
|
209
|
+
update_lockfile=bool(args.update_lockfile),
|
|
210
|
+
allow_manifest_changes=bool(args.allow_manifest_changes),
|
|
211
|
+
include=include,
|
|
212
|
+
exclude=exclude,
|
|
213
|
+
extras={"timestamp": timestamp},
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
ctx = AuditContext(runner=runner, dry_run=bool(args.dry_run), **common)
|
|
217
|
+
detect_ctx = AuditContext(runner=detect_runner, dry_run=False, **common)
|
|
218
|
+
|
|
219
|
+
detections: dict[str, DetectionResult] = {}
|
|
220
|
+
install_plans: list[InstallPlan] = []
|
|
221
|
+
for tool in tools:
|
|
222
|
+
if not tool.platform_supported(platform):
|
|
223
|
+
detections[tool.id] = DetectionResult(
|
|
224
|
+
tool.id,
|
|
225
|
+
"SKIPPED",
|
|
226
|
+
False,
|
|
227
|
+
message=f"Incompatible with {platform.os.value}/{platform.architecture}",
|
|
228
|
+
)
|
|
229
|
+
continue
|
|
230
|
+
det = tool.detect(detect_ctx)
|
|
231
|
+
detections[tool.id] = det
|
|
232
|
+
if not det.available and args.install_missing:
|
|
233
|
+
plan = tool.install_plan(detect_ctx)
|
|
234
|
+
if plan is not None:
|
|
235
|
+
install_plans.append(plan)
|
|
236
|
+
|
|
237
|
+
plan = _build_plan_dict(
|
|
238
|
+
ctx=detect_ctx,
|
|
239
|
+
tools=tools,
|
|
240
|
+
detections=detections,
|
|
241
|
+
install_plans=install_plans,
|
|
242
|
+
profile=args.profile,
|
|
243
|
+
update_lockfile=bool(args.update_lockfile),
|
|
244
|
+
allow_manifest_changes=bool(args.allow_manifest_changes),
|
|
245
|
+
yes=bool(args.yes),
|
|
246
|
+
unknown_includes=unknown,
|
|
247
|
+
)
|
|
248
|
+
return ctx, plan, tools, detections, install_plans, None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def cmd_plan(args: argparse.Namespace) -> int:
|
|
252
|
+
ctx, plan, tools, detections, install_plans, err = _prepare_context(args, create_evidence_dir=False)
|
|
253
|
+
if ctx is None:
|
|
254
|
+
return err or 2
|
|
255
|
+
for tool in tools:
|
|
256
|
+
det = detections.get(tool.id)
|
|
257
|
+
if det and not det.available and tool.id not in {p.tool_id for p in install_plans}:
|
|
258
|
+
plan.setdefault("auto_skip", []).append({"tool": tool.id, "reason": det.message or "missing"})
|
|
259
|
+
print(json.dumps(plan, indent=2))
|
|
260
|
+
return 0
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def cmd_audit(args: argparse.Namespace) -> int:
|
|
264
|
+
ctx, plan, tools, detections, install_plans, err = _prepare_context(args, create_evidence_dir=True)
|
|
265
|
+
if ctx is None:
|
|
266
|
+
return err or 2
|
|
267
|
+
|
|
268
|
+
timestamp = ctx.extras["timestamp"]
|
|
269
|
+
|
|
270
|
+
if args.allow_manifest_changes:
|
|
271
|
+
print(
|
|
272
|
+
"Manifest changes are authorized but no automatic manifest mutation is implemented.",
|
|
273
|
+
file=sys.stderr,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
if args.dry_run:
|
|
277
|
+
print(json.dumps({"dry_run": True, "plan": plan}, indent=2))
|
|
278
|
+
return 0
|
|
279
|
+
|
|
280
|
+
lockfile_existed = ctx.workspace.has_lockfile
|
|
281
|
+
before = snapshot_project_files(ctx.root)
|
|
282
|
+
hashes_before = {k: v.to_dict() for k, v in before.items()}
|
|
283
|
+
|
|
284
|
+
live = CommandRunner(verbose=bool(args.verbose), dry_run=False)
|
|
285
|
+
|
|
286
|
+
if install_plans:
|
|
287
|
+
authorized = confirm_installs(install_plans, yes=bool(args.yes))
|
|
288
|
+
if not authorized:
|
|
289
|
+
print("Installation not authorized. Aborting before mutating the system.", file=sys.stderr)
|
|
290
|
+
return 2
|
|
291
|
+
plan["install_authorization"] = "non-interactive (--yes)" if args.yes else "interactive confirmation"
|
|
292
|
+
for ip in install_plans:
|
|
293
|
+
outcome = execute_install_plan(ip, live, timeout=float(args.timeout))
|
|
294
|
+
plan.setdefault("install_outcomes", []).append(asdict(outcome))
|
|
295
|
+
tool = next(t for t in tools if t.id == ip.tool_id)
|
|
296
|
+
if not outcome.success:
|
|
297
|
+
# Persist install failure as ERROR result
|
|
298
|
+
out, errp = tool.write_logs(
|
|
299
|
+
ctx,
|
|
300
|
+
json.dumps(outcome.steps_run, indent=2),
|
|
301
|
+
outcome.error or "install failed",
|
|
302
|
+
)
|
|
303
|
+
fail = tool.make_result(
|
|
304
|
+
status="ERROR",
|
|
305
|
+
severity="error",
|
|
306
|
+
summary=outcome.error or "installation failed",
|
|
307
|
+
started_at=utc_now_iso(),
|
|
308
|
+
command=ip.steps[-1].command if ip.steps else None,
|
|
309
|
+
exit_code=1,
|
|
310
|
+
stdout_path=out,
|
|
311
|
+
stderr_path=errp,
|
|
312
|
+
environment={"phase": "install"},
|
|
313
|
+
)
|
|
314
|
+
tool.save_result_json(ctx, fail)
|
|
315
|
+
detections[tool.id] = DetectionResult(
|
|
316
|
+
tool.id, "ERROR", False, message=outcome.error or "install failed"
|
|
317
|
+
)
|
|
318
|
+
# Keep going with other tools; this tool will be ERROR/skipped below
|
|
319
|
+
else:
|
|
320
|
+
detections[tool.id] = tool.detect(AuditContext(
|
|
321
|
+
workspace=ctx.workspace,
|
|
322
|
+
platform=ctx.platform,
|
|
323
|
+
config=ctx.config,
|
|
324
|
+
runner=live,
|
|
325
|
+
evidence_dir=ctx.evidence_dir,
|
|
326
|
+
profile=ctx.profile,
|
|
327
|
+
timeout_seconds=ctx.timeout_seconds,
|
|
328
|
+
verbose=ctx.verbose,
|
|
329
|
+
include=ctx.include,
|
|
330
|
+
exclude=ctx.exclude,
|
|
331
|
+
extras=ctx.extras,
|
|
332
|
+
))
|
|
333
|
+
|
|
334
|
+
if args.update_lockfile:
|
|
335
|
+
if not args.yes:
|
|
336
|
+
try:
|
|
337
|
+
answer = input("Authorize `cargo update`? [y/N]: ")
|
|
338
|
+
except EOFError:
|
|
339
|
+
answer = "n"
|
|
340
|
+
if answer.strip().lower() not in {"y", "yes"}:
|
|
341
|
+
print("cargo update not authorized.", file=sys.stderr)
|
|
342
|
+
return 2
|
|
343
|
+
upd = live.run(["cargo", "update"], cwd=ctx.root, timeout=args.timeout)
|
|
344
|
+
plan["cargo_update"] = upd.to_dict()
|
|
345
|
+
if upd.timed_out or upd.exit_code != 0:
|
|
346
|
+
print("cargo update failed; continuing with existing lockfile state.", file=sys.stderr)
|
|
347
|
+
else:
|
|
348
|
+
try:
|
|
349
|
+
ctx.workspace = resolve_workspace(ctx.workspace.workspace_root, live)
|
|
350
|
+
except WorkspaceError as exc:
|
|
351
|
+
print(f"ERROR after cargo update: {exc}", file=sys.stderr)
|
|
352
|
+
return 2
|
|
353
|
+
|
|
354
|
+
results: list[ToolResult] = []
|
|
355
|
+
selected_ids = [t.id for t in tools]
|
|
356
|
+
req_ids = required_tool_ids(
|
|
357
|
+
args.profile,
|
|
358
|
+
workspace=ctx.workspace,
|
|
359
|
+
selected_ids=selected_ids,
|
|
360
|
+
config=ctx.config,
|
|
361
|
+
)
|
|
362
|
+
missing_required: list[str] = []
|
|
363
|
+
|
|
364
|
+
# Ensure evidence runner is live
|
|
365
|
+
ctx.runner = live
|
|
366
|
+
|
|
367
|
+
for tool in tools:
|
|
368
|
+
det = detections.get(tool.id)
|
|
369
|
+
if det and det.status == "ERROR" and not det.available:
|
|
370
|
+
# install failed already recorded
|
|
371
|
+
existing = ctx.evidence_dir / "tools" / tool.id / "result.json"
|
|
372
|
+
if existing.is_file():
|
|
373
|
+
data = json.loads(existing.read_text(encoding="utf-8"))
|
|
374
|
+
results.append(
|
|
375
|
+
ToolResult(
|
|
376
|
+
tool_id=data["tool_id"],
|
|
377
|
+
display_name=data["display_name"],
|
|
378
|
+
status=data["status"],
|
|
379
|
+
severity=data["severity"],
|
|
380
|
+
command=data.get("command"),
|
|
381
|
+
exit_code=data.get("exit_code"),
|
|
382
|
+
started_at=data["started_at"],
|
|
383
|
+
finished_at=data["finished_at"],
|
|
384
|
+
duration_seconds=data["duration_seconds"],
|
|
385
|
+
stdout_path=data.get("stdout_path"),
|
|
386
|
+
stderr_path=data.get("stderr_path"),
|
|
387
|
+
findings=[],
|
|
388
|
+
summary=data["summary"],
|
|
389
|
+
environment=data.get("environment") or {},
|
|
390
|
+
artifact_hashes=data.get("artifact_hashes") or {},
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
continue
|
|
394
|
+
if det and det.status == "SKIPPED":
|
|
395
|
+
results.append(
|
|
396
|
+
tool.skipped(ctx, det.message or "skipped", skip_kind="incompatible")
|
|
397
|
+
)
|
|
398
|
+
continue
|
|
399
|
+
if det and not det.available:
|
|
400
|
+
kind = "missing"
|
|
401
|
+
results.append(tool.skipped(ctx, det.message or "tool missing and not installed", skip_kind=kind))
|
|
402
|
+
if tool.id in req_ids:
|
|
403
|
+
missing_required.append(tool.id)
|
|
404
|
+
continue
|
|
405
|
+
try:
|
|
406
|
+
result = tool.run(ctx)
|
|
407
|
+
if not (ctx.evidence_dir / "tools" / tool.id / "result.json").is_file():
|
|
408
|
+
tool.save_result_json(ctx, result)
|
|
409
|
+
except Exception as exc: # noqa: BLE001
|
|
410
|
+
out, errp = tool.write_logs(ctx, "", traceback.format_exc())
|
|
411
|
+
result = tool.make_result(
|
|
412
|
+
status="ERROR",
|
|
413
|
+
severity="error",
|
|
414
|
+
summary=f"Internal tool error: {exc}",
|
|
415
|
+
started_at=utc_now_iso(),
|
|
416
|
+
stdout_path=out,
|
|
417
|
+
stderr_path=errp,
|
|
418
|
+
)
|
|
419
|
+
tool.save_result_json(ctx, result)
|
|
420
|
+
results.append(result)
|
|
421
|
+
|
|
422
|
+
after = snapshot_project_files(ctx.root)
|
|
423
|
+
raw_changes = compare_snapshots(before, after)
|
|
424
|
+
changes, unauthorized = classify_project_changes(
|
|
425
|
+
raw_changes,
|
|
426
|
+
update_lockfile=bool(args.update_lockfile),
|
|
427
|
+
allow_manifest_changes=bool(args.allow_manifest_changes),
|
|
428
|
+
lockfile_existed_before=lockfile_existed,
|
|
429
|
+
)
|
|
430
|
+
hashes_after = {k: v.to_dict() for k, v in after.items()}
|
|
431
|
+
|
|
432
|
+
decision = decide(
|
|
433
|
+
results,
|
|
434
|
+
fail_on=args.fail_on,
|
|
435
|
+
required_tool_ids=req_ids,
|
|
436
|
+
hash_incongruence=bool(unauthorized),
|
|
437
|
+
missing_required=missing_required,
|
|
438
|
+
)
|
|
439
|
+
if unauthorized:
|
|
440
|
+
decision.reason = (
|
|
441
|
+
f"Unauthorized project file changes detected: {', '.join(unauthorized)}. " + decision.reason
|
|
442
|
+
)
|
|
443
|
+
decision.decision = "REJECTED"
|
|
444
|
+
decision.exit_code = 1
|
|
445
|
+
|
|
446
|
+
os_arch = f"{ctx.platform.os.value} / {ctx.platform.architecture}"
|
|
447
|
+
toolchain = ctx.platform.active_toolchain or ctx.platform.rustc_version or "unknown"
|
|
448
|
+
md = render_markdown(
|
|
449
|
+
workspace=ctx.workspace.workspace_root,
|
|
450
|
+
timestamp=timestamp,
|
|
451
|
+
os_arch=os_arch,
|
|
452
|
+
toolchain=toolchain,
|
|
453
|
+
profile=args.profile,
|
|
454
|
+
decision=decision,
|
|
455
|
+
results=results,
|
|
456
|
+
changes=changes,
|
|
457
|
+
)
|
|
458
|
+
report_json = {
|
|
459
|
+
"workspace": ctx.workspace.to_dict(),
|
|
460
|
+
"decision": decision.to_dict(),
|
|
461
|
+
"results": [r.to_dict() for r in results],
|
|
462
|
+
"changes": changes,
|
|
463
|
+
"required_tool_ids": req_ids,
|
|
464
|
+
"config_defaults_used": ctx.config.defaults_used,
|
|
465
|
+
"install_authorization": plan.get("install_authorization"),
|
|
466
|
+
"timestamp_utc": timestamp,
|
|
467
|
+
}
|
|
468
|
+
manifest = {
|
|
469
|
+
"version": __version__,
|
|
470
|
+
"timestamp_utc": timestamp,
|
|
471
|
+
"profile": args.profile,
|
|
472
|
+
"fail_on": args.fail_on,
|
|
473
|
+
"decision": decision.to_dict(),
|
|
474
|
+
"platform": ctx.platform.to_dict(),
|
|
475
|
+
"commands": [r.command for r in results],
|
|
476
|
+
"results_status": {r.tool_id: r.status for r in results},
|
|
477
|
+
"hashes_before": hashes_before,
|
|
478
|
+
"hashes_after": hashes_after,
|
|
479
|
+
"required_tool_ids": req_ids,
|
|
480
|
+
}
|
|
481
|
+
write_evidence(
|
|
482
|
+
ctx.evidence_dir,
|
|
483
|
+
report_md=md,
|
|
484
|
+
report_json=report_json,
|
|
485
|
+
manifest=manifest,
|
|
486
|
+
environment=ctx.platform.to_dict(),
|
|
487
|
+
workspace=ctx.workspace.to_dict(),
|
|
488
|
+
changes=changes,
|
|
489
|
+
plan=plan,
|
|
490
|
+
hashes={"before": hashes_before, "after": hashes_after},
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
fmt = args.format
|
|
494
|
+
if fmt in {"terminal", "both", "markdown"}:
|
|
495
|
+
print(md)
|
|
496
|
+
print(f"\nEvidence written to: {ctx.evidence_dir}")
|
|
497
|
+
if fmt in {"json"}:
|
|
498
|
+
print(json.dumps(report_json, indent=2))
|
|
499
|
+
if fmt == "both":
|
|
500
|
+
print(f"JSON report: {ctx.evidence_dir / 'report.json'}")
|
|
501
|
+
|
|
502
|
+
return decision.exit_code
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
506
|
+
parser = build_parser()
|
|
507
|
+
try:
|
|
508
|
+
args = parser.parse_args(list(argv) if argv is not None else None)
|
|
509
|
+
if args.command == "doctor":
|
|
510
|
+
return cmd_doctor()
|
|
511
|
+
if args.command == "list-tools":
|
|
512
|
+
return cmd_list_tools()
|
|
513
|
+
if args.command == "plan":
|
|
514
|
+
return cmd_plan(args)
|
|
515
|
+
if args.command == "audit":
|
|
516
|
+
return cmd_audit(args)
|
|
517
|
+
parser.error(f"Unknown command: {args.command}")
|
|
518
|
+
return 3
|
|
519
|
+
except SystemExit as exc:
|
|
520
|
+
code = exc.code
|
|
521
|
+
return int(code) if isinstance(code, int) else (0 if code is None else 3)
|
|
522
|
+
except Exception as exc: # noqa: BLE001
|
|
523
|
+
print(f"INTERNAL ERROR: {exc}", file=sys.stderr)
|
|
524
|
+
if "--verbose" in (argv or sys.argv):
|
|
525
|
+
traceback.print_exc()
|
|
526
|
+
return 3
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
if __name__ == "__main__":
|
|
530
|
+
raise SystemExit(main())
|