deployproof 0.1.8__tar.gz → 0.2.1__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.
- {deployproof-0.1.8/src/deployproof.egg-info → deployproof-0.2.1}/PKG-INFO +7 -6
- {deployproof-0.1.8 → deployproof-0.2.1}/README.md +6 -5
- {deployproof-0.1.8 → deployproof-0.2.1}/pyproject.toml +1 -1
- deployproof-0.2.1/src/deployproof/__init__.py +2 -0
- deployproof-0.2.1/src/deployproof/cli.py +179 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/src/deployproof/dependencies.py +10 -5
- {deployproof-0.1.8 → deployproof-0.2.1}/src/deployproof/mutator.py +208 -201
- deployproof-0.2.1/src/deployproof/reporter.py +333 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/src/deployproof/symlinks.py +22 -5
- {deployproof-0.1.8 → deployproof-0.2.1/src/deployproof.egg-info}/PKG-INFO +7 -6
- {deployproof-0.1.8 → deployproof-0.2.1}/tests/test_cli.py +3 -3
- deployproof-0.2.1/tests/test_mutator.py +412 -0
- deployproof-0.1.8/src/deployproof/__init__.py +0 -2
- deployproof-0.1.8/src/deployproof/cli.py +0 -400
- deployproof-0.1.8/src/deployproof/reporter.py +0 -636
- deployproof-0.1.8/tests/test_mutator.py +0 -154
- {deployproof-0.1.8 → deployproof-0.2.1}/LICENSE +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/setup.cfg +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/src/deployproof/control_flow.py +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/src/deployproof/diff.py +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/src/deployproof/mocks.py +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/src/deployproof/secrets.py +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/src/deployproof/wsl.py +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/src/deployproof.egg-info/SOURCES.txt +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/src/deployproof.egg-info/dependency_links.txt +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/src/deployproof.egg-info/entry_points.txt +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/src/deployproof.egg-info/requires.txt +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/src/deployproof.egg-info/top_level.txt +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/tests/test_baseline_collection_error.py +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/tests/test_cli_dependency_scan.py +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/tests/test_control_flow.py +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/tests/test_dependencies_extractor.py +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/tests/test_dependencies_pypi.py +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/tests/test_diff.py +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/tests/test_edge_cases_validation.py +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/tests/test_mocks.py +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/tests/test_mutation_suites.py +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/tests/test_secrets.py +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/tests/test_stress_regressions.py +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/tests/test_symlinks.py +0 -0
- {deployproof-0.1.8 → deployproof-0.2.1}/tests/test_wsl.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deployproof
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: A deterministic AI-code deployability checker that catches what coverage misses.
|
|
5
5
|
Author-email: SVS Praveen <svspraveens@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -32,10 +32,10 @@ Dynamic: license-file
|
|
|
32
32
|
|
|
33
33
|
> Deterministic pre-push verification for AI-assisted codebases: AST mutation testing, credential scanning, sandbox-escape detection, mock-usage alerts, swallowed-exception checks, and dependency hallucination defense.
|
|
34
34
|
|
|
35
|
-
[](https://pypi.org/project/deployproof/)
|
|
36
36
|
[](https://pypi.org/project/deployproof/)
|
|
37
37
|
[](https://github.com/SVSPraveen/DeployProof/actions/workflows/ci.yml)
|
|
38
|
-
[](https://github.com/SVSPraveen/DeployProof)
|
|
39
39
|
[](stress_fixtures/)
|
|
40
40
|
[](LICENSE)
|
|
41
41
|
|
|
@@ -189,7 +189,7 @@ deployproof check --json
|
|
|
189
189
|
|
|
190
190
|
```json
|
|
191
191
|
{
|
|
192
|
-
"version": "0.1
|
|
192
|
+
"version": "0.2.1",
|
|
193
193
|
"status": "passed",
|
|
194
194
|
"summary": {
|
|
195
195
|
"target_files_count": 1,
|
|
@@ -262,7 +262,7 @@ deployproof check --json
|
|
|
262
262
|
|
|
263
263
|
## What It Checks
|
|
264
264
|
|
|
265
|
-
- **Mutation Score** — Mutates AST operators (`>=`, `==`, `and`, `or`, `*`, numeric constants, comparisons) in modified files and runs your test suite against each mutant. Reports surviving mutants and a percentage score. Does not use line coverage.
|
|
265
|
+
- **Mutation Score** — Mutates AST operators (`>=`, `==`, `and`, `or`, `*`, numeric constants, comparisons) in modified files and runs your test suite against each mutant. Reports surviving mutants and a percentage score. Does not use line coverage. Features atomic file restoration protected by `SIGINT`/`SIGTERM`/`SIGBREAK` signal handlers to ensure interrupted runs never leave mutated code on disk.
|
|
266
266
|
- **Secrets and Credentials** — Scans modified files for hardcoded API keys (OpenAI, Anthropic, AWS, GitHub, Stripe, private keys) and tracked `.env` files using pattern matching and entropy analysis.
|
|
267
267
|
- **Symlink and Sandbox Escape** — Resolves symbolic links and flags any whose target escapes the repository root (CWE-61 / CWE-451). Catches the class of path-traversal trick used in the GhostApproval disclosure (Wiz Research, July 2026).
|
|
268
268
|
- **Dependency and Slopsquatting** — For each new import, dynamic import (`importlib.import_module`, `__import__`), or manifest entry (including recursive `-r` includes) introduced in the diff, queries the PyPI JSON API and checks registration age. Packages that don't exist (HTTP 404) are flagged HIGH RISK; packages registered within the last 30 days are flagged MEDIUM RISK.
|
|
@@ -271,6 +271,7 @@ deployproof check --json
|
|
|
271
271
|
|
|
272
272
|
## What This Doesn't Do
|
|
273
273
|
|
|
274
|
+
- **Test-only diffs are not yet caught.** If a diff modifies or weakens assertions in a test file without changing the corresponding source file, DeployProof currently sees zero modified source lines and passes with 0 mutants evaluated. This is a known gap — see [INVESTIGATION_blastradius.md](INVESTIGATION_blastradius.md) for the reverse-mapping approach being evaluated to close it. Until this lands, DeployProof does not protect against test suites being weakened directly.
|
|
274
275
|
- **Not a full-repo audit.** Checks are scoped to files changed in the current session (git diff). Files you haven't touched are not re-evaluated.
|
|
275
276
|
- **Python only.** Mutation testing and import extraction currently support Python files only. Other languages are not scanned.
|
|
276
277
|
- **No auto-fix.** DeployProof reports findings; it does not modify your code, rewrite imports, or suggest patches.
|
|
@@ -288,7 +289,7 @@ Fixtures cover weak test suites, zero-test orphan modules, planted OpenAI/AWS cr
|
|
|
288
289
|
|
|
289
290
|
## Status & Roadmap
|
|
290
291
|
|
|
291
|
-
- **Current (v0.1
|
|
292
|
+
- **Current (v0.2.1):** Diff-scoped AST mutation testing (with recursive `test/`/`tests/` discovery, AST column-offset snippet reconstruction, and `SIGINT`/`SIGTERM`/`SIGBREAK` signal-safe disk restoration), baseline test-collection failure isolation with distinct exit code `2`, entropy-driven value-based secrets scanner (including unquoted .env values and prefix validation), GhostApproval symlink sandbox-escape detector, PyPI dependency hallucination / slopsquatting scanner (with import-to-distribution translation, recursive `-r` requirements scanning, and dynamic import detection via `importlib` / `__import__`), mock-introduction detector (`--strict-mocks`), control-flow / swallowed-exception scanner (`--strict-error-handling`), 11/11 launch-day stress test suite, 94 unit tests, machine-readable `--json` output, and Tier 2 CI verification via GitHub Actions (mutmut).
|
|
292
293
|
- **Next:** Multi-language mutation support and expanded ecosystem rule packs.
|
|
293
294
|
|
|
294
295
|
## Contributing
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
> Deterministic pre-push verification for AI-assisted codebases: AST mutation testing, credential scanning, sandbox-escape detection, mock-usage alerts, swallowed-exception checks, and dependency hallucination defense.
|
|
4
4
|
|
|
5
|
-
[](https://pypi.org/project/deployproof/)
|
|
6
6
|
[](https://pypi.org/project/deployproof/)
|
|
7
7
|
[](https://github.com/SVSPraveen/DeployProof/actions/workflows/ci.yml)
|
|
8
|
-
[](https://github.com/SVSPraveen/DeployProof)
|
|
9
9
|
[](stress_fixtures/)
|
|
10
10
|
[](LICENSE)
|
|
11
11
|
|
|
@@ -159,7 +159,7 @@ deployproof check --json
|
|
|
159
159
|
|
|
160
160
|
```json
|
|
161
161
|
{
|
|
162
|
-
"version": "0.1
|
|
162
|
+
"version": "0.2.1",
|
|
163
163
|
"status": "passed",
|
|
164
164
|
"summary": {
|
|
165
165
|
"target_files_count": 1,
|
|
@@ -232,7 +232,7 @@ deployproof check --json
|
|
|
232
232
|
|
|
233
233
|
## What It Checks
|
|
234
234
|
|
|
235
|
-
- **Mutation Score** — Mutates AST operators (`>=`, `==`, `and`, `or`, `*`, numeric constants, comparisons) in modified files and runs your test suite against each mutant. Reports surviving mutants and a percentage score. Does not use line coverage.
|
|
235
|
+
- **Mutation Score** — Mutates AST operators (`>=`, `==`, `and`, `or`, `*`, numeric constants, comparisons) in modified files and runs your test suite against each mutant. Reports surviving mutants and a percentage score. Does not use line coverage. Features atomic file restoration protected by `SIGINT`/`SIGTERM`/`SIGBREAK` signal handlers to ensure interrupted runs never leave mutated code on disk.
|
|
236
236
|
- **Secrets and Credentials** — Scans modified files for hardcoded API keys (OpenAI, Anthropic, AWS, GitHub, Stripe, private keys) and tracked `.env` files using pattern matching and entropy analysis.
|
|
237
237
|
- **Symlink and Sandbox Escape** — Resolves symbolic links and flags any whose target escapes the repository root (CWE-61 / CWE-451). Catches the class of path-traversal trick used in the GhostApproval disclosure (Wiz Research, July 2026).
|
|
238
238
|
- **Dependency and Slopsquatting** — For each new import, dynamic import (`importlib.import_module`, `__import__`), or manifest entry (including recursive `-r` includes) introduced in the diff, queries the PyPI JSON API and checks registration age. Packages that don't exist (HTTP 404) are flagged HIGH RISK; packages registered within the last 30 days are flagged MEDIUM RISK.
|
|
@@ -241,6 +241,7 @@ deployproof check --json
|
|
|
241
241
|
|
|
242
242
|
## What This Doesn't Do
|
|
243
243
|
|
|
244
|
+
- **Test-only diffs are not yet caught.** If a diff modifies or weakens assertions in a test file without changing the corresponding source file, DeployProof currently sees zero modified source lines and passes with 0 mutants evaluated. This is a known gap — see [INVESTIGATION_blastradius.md](INVESTIGATION_blastradius.md) for the reverse-mapping approach being evaluated to close it. Until this lands, DeployProof does not protect against test suites being weakened directly.
|
|
244
245
|
- **Not a full-repo audit.** Checks are scoped to files changed in the current session (git diff). Files you haven't touched are not re-evaluated.
|
|
245
246
|
- **Python only.** Mutation testing and import extraction currently support Python files only. Other languages are not scanned.
|
|
246
247
|
- **No auto-fix.** DeployProof reports findings; it does not modify your code, rewrite imports, or suggest patches.
|
|
@@ -258,7 +259,7 @@ Fixtures cover weak test suites, zero-test orphan modules, planted OpenAI/AWS cr
|
|
|
258
259
|
|
|
259
260
|
## Status & Roadmap
|
|
260
261
|
|
|
261
|
-
- **Current (v0.1
|
|
262
|
+
- **Current (v0.2.1):** Diff-scoped AST mutation testing (with recursive `test/`/`tests/` discovery, AST column-offset snippet reconstruction, and `SIGINT`/`SIGTERM`/`SIGBREAK` signal-safe disk restoration), baseline test-collection failure isolation with distinct exit code `2`, entropy-driven value-based secrets scanner (including unquoted .env values and prefix validation), GhostApproval symlink sandbox-escape detector, PyPI dependency hallucination / slopsquatting scanner (with import-to-distribution translation, recursive `-r` requirements scanning, and dynamic import detection via `importlib` / `__import__`), mock-introduction detector (`--strict-mocks`), control-flow / swallowed-exception scanner (`--strict-error-handling`), 11/11 launch-day stress test suite, 94 unit tests, machine-readable `--json` output, and Tier 2 CI verification via GitHub Actions (mutmut).
|
|
262
263
|
- **Next:** Multi-language mutation support and expanded ecosystem rule packs.
|
|
263
264
|
|
|
264
265
|
## Contributing
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "deployproof"
|
|
7
|
-
version = "0.1
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "A deterministic AI-code deployability checker that catches what coverage misses."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Command-line interface for DeployProof."""
|
|
2
|
+
import argparse
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
from deployproof import __version__
|
|
7
|
+
from deployproof.dependencies import extract_all_new_dependencies, scan_dependencies
|
|
8
|
+
from deployproof.control_flow import scan_session_files_for_control_flow
|
|
9
|
+
from deployproof.diff import DiffScopeError, InvalidBaseRefError, NotAGitRepositoryError, get_git_root, is_test_file, resolve_changed_python_files, resolve_changed_session_files
|
|
10
|
+
from deployproof.mocks import scan_session_files_for_mocks
|
|
11
|
+
from deployproof.mutator import run_mutation_tests
|
|
12
|
+
from deployproof.reporter import LARGE_FILE_LOC_THRESHOLD, format_json_report, format_report
|
|
13
|
+
from deployproof.secrets import scan_session_files_for_secrets
|
|
14
|
+
from deployproof.symlinks import scan_session_files_for_symlinks
|
|
15
|
+
from deployproof.wsl import check_wsl_readiness, run_wsl_mutmut
|
|
16
|
+
|
|
17
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
18
|
+
"""Create and configure the command-line argument parser."""
|
|
19
|
+
parser = argparse.ArgumentParser(prog='deployproof', description='DeployProof: A deterministic AI-code deployability checker.')
|
|
20
|
+
parser.add_argument('-v', '--version', action='version', version=f'%(prog)s {__version__}', help="Show program's version number and exit.")
|
|
21
|
+
subparsers = parser.add_subparsers(dest='command', help='Available commands')
|
|
22
|
+
check_parser = subparsers.add_parser('check', help='Run deployability verification checks against session changes.')
|
|
23
|
+
check_parser.add_argument('--base', type=str, default=None, help='Base git ref (branch/commit/tag) to diff against.')
|
|
24
|
+
check_parser.add_argument('--files', nargs='+', type=str, default=None, help='Explicit files to evaluate (bypasses git diff).')
|
|
25
|
+
check_parser.add_argument('--tests', nargs='+', type=str, default=None, help='Specific test file(s) or directories to execute.')
|
|
26
|
+
check_parser.add_argument('--threshold', type=float, default=80.0, help='Minimum mutation score percentage to pass (default: 80.0).')
|
|
27
|
+
check_parser.add_argument('--timeout', type=float, default=10.0, help='Test runner timeout in seconds per mutant (default: 10.0).')
|
|
28
|
+
check_parser.add_argument('--wsl', action='store_true', default=False, help='Delegate mutation testing to mutmut inside WSL (Windows only).')
|
|
29
|
+
check_parser.add_argument('--strict-mocks', action='store_true', default=False, help='Fail the verification gate if new mock or monkeypatch usage is introduced in test files.')
|
|
30
|
+
check_parser.add_argument('--strict-error-handling', action='store_true', default=False, help='Fail the verification gate if bare excepts, swallowed exceptions, or unreachable code are detected.')
|
|
31
|
+
check_parser.add_argument('--json', action='store_true', default=False, help='Output check results as a structured JSON object.')
|
|
32
|
+
subparsers.add_parser('init', help='Initialize DeployProof configuration in the current repository.')
|
|
33
|
+
return parser
|
|
34
|
+
|
|
35
|
+
def handle_check(args: argparse.Namespace) -> int:
|
|
36
|
+
"""Handle the 'check' subcommand."""
|
|
37
|
+
cwd = Path.cwd().resolve()
|
|
38
|
+
repo_root: Optional[Path] = None
|
|
39
|
+
try:
|
|
40
|
+
if args.files:
|
|
41
|
+
session_files = [Path(f).resolve() for f in args.files]
|
|
42
|
+
if session_files:
|
|
43
|
+
try:
|
|
44
|
+
repo_root = get_git_root(session_files[0].parent)
|
|
45
|
+
except DiffScopeError:
|
|
46
|
+
repo_root = session_files[0].parent
|
|
47
|
+
else:
|
|
48
|
+
repo_root = cwd
|
|
49
|
+
else:
|
|
50
|
+
repo_root = get_git_root(cwd)
|
|
51
|
+
session_files = resolve_changed_session_files(cwd=cwd, base=args.base)
|
|
52
|
+
except NotAGitRepositoryError:
|
|
53
|
+
print('Error: Not a git repository. Initialize git or specify files with --files.', file=sys.stderr)
|
|
54
|
+
return 1
|
|
55
|
+
except InvalidBaseRefError as e:
|
|
56
|
+
print(f'Error: {e}', file=sys.stderr)
|
|
57
|
+
return 1
|
|
58
|
+
except DiffScopeError as e:
|
|
59
|
+
print(f'Error: {e}', file=sys.stderr)
|
|
60
|
+
return 1
|
|
61
|
+
if not session_files:
|
|
62
|
+
if getattr(args, 'json', False):
|
|
63
|
+
import json
|
|
64
|
+
print(json.dumps({'version': __version__, 'status': 'passed', 'message': 'No modified files detected in current session.', 'scope': {'target_files': []}, 'summary': {'target_files_count': 0, 'mutation_score': 100.0, 'threshold': args.threshold, 'secrets_found': 0, 'symlink_escapes_found': 0, 'dependency_findings': {'high_risk': 0, 'medium_risk': 0, 'ok': 0, 'unknown': 0, 'unscanned': 0}, 'mock_usages_found': 0, 'control_flow_findings': 0, 'strict_mocks_active': getattr(args, 'strict_mocks', False), 'strict_mocks_triggered': False, 'strict_error_handling_active': getattr(args, 'strict_error_handling', False), 'strict_error_handling_triggered': False}, 'control_flow': {'clean': True, 'strict_gate_triggered': False, 'findings': []}}, indent=2))
|
|
65
|
+
return 0
|
|
66
|
+
else:
|
|
67
|
+
print('DeployProof: No modified files detected in current session.')
|
|
68
|
+
print('Working tree is clean. Use --base <ref> or --files <path...> to evaluate specific files.')
|
|
69
|
+
return 0
|
|
70
|
+
symlink_result = scan_session_files_for_symlinks(session_files, repo_root=repo_root or cwd)
|
|
71
|
+
secrets_result = scan_session_files_for_secrets(session_files)
|
|
72
|
+
extracted_deps = extract_all_new_dependencies(session_files, root=repo_root or cwd, base=args.base)
|
|
73
|
+
dependency_result = scan_dependencies(extracted_deps)
|
|
74
|
+
mock_result = scan_session_files_for_mocks(session_files=session_files, root=repo_root or cwd, base=args.base)
|
|
75
|
+
control_flow_result = scan_session_files_for_control_flow(session_files=session_files, root=repo_root or cwd, base=args.base)
|
|
76
|
+
if args.files:
|
|
77
|
+
non_test_files = [f for f in session_files if f.is_file() and f.suffix == '.py' and (not is_test_file(f))]
|
|
78
|
+
target_files = non_test_files if non_test_files else [f for f in session_files if f.is_file() and f.suffix == '.py']
|
|
79
|
+
else:
|
|
80
|
+
target_files = [f for f in session_files if f.is_file() and f.suffix == '.py' and (not is_test_file(f))]
|
|
81
|
+
if not getattr(args, 'json', False):
|
|
82
|
+
for f in target_files:
|
|
83
|
+
try:
|
|
84
|
+
loc = len(f.read_text(encoding='utf-8', errors='replace').splitlines())
|
|
85
|
+
if loc < LARGE_FILE_LOC_THRESHOLD:
|
|
86
|
+
try:
|
|
87
|
+
rel = f.relative_to(repo_root or cwd)
|
|
88
|
+
except ValueError:
|
|
89
|
+
rel = f
|
|
90
|
+
print(f"Notice: Large file '{rel}' ({loc} LOC) detected - mutation testing may take several minutes.")
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
if getattr(args, 'wsl', False):
|
|
94
|
+
wsl_ready, wsl_msg = check_wsl_readiness()
|
|
95
|
+
if wsl_ready:
|
|
96
|
+
if not getattr(args, 'json', False):
|
|
97
|
+
print('DeployProof - Delegating to mutmut in WSL...')
|
|
98
|
+
wsl_res = run_wsl_mutmut(repo_root or cwd, target_files)
|
|
99
|
+
if wsl_res.get('success'):
|
|
100
|
+
if not getattr(args, 'json', False):
|
|
101
|
+
print(wsl_res.get('stdout', ''))
|
|
102
|
+
has_security_issue = bool(secrets_result.findings or symlink_result.escape_findings or dependency_result.high_risk_count > 0 or (getattr(args, 'strict_mocks', False) and mock_result.total_findings > 0) or (getattr(args, 'strict_error_handling', False) and control_flow_result.total_findings > 0))
|
|
103
|
+
return 1 if has_security_issue else 0
|
|
104
|
+
elif not getattr(args, 'json', False):
|
|
105
|
+
print(f"WSL execution error: {wsl_res.get('error') or wsl_res.get('stderr')}", file=sys.stderr)
|
|
106
|
+
print('Falling back to Tier 1 local pre-check...')
|
|
107
|
+
elif not getattr(args, 'json', False):
|
|
108
|
+
print(wsl_msg)
|
|
109
|
+
print('-' * 68)
|
|
110
|
+
result = run_mutation_tests(target_files=target_files, repo_root=repo_root or cwd, test_runner_timeout=args.timeout, extra_pytest_args=args.tests)
|
|
111
|
+
if getattr(args, 'json', False):
|
|
112
|
+
report_text = format_json_report(result=result, target_files=target_files, secrets_result=secrets_result, symlink_result=symlink_result, dependency_result=dependency_result, mock_result=mock_result, control_flow_result=control_flow_result, strict_mocks=getattr(args, 'strict_mocks', False), strict_error_handling=getattr(args, 'strict_error_handling', False), repo_root=repo_root or cwd, threshold=args.threshold, version=__version__)
|
|
113
|
+
else:
|
|
114
|
+
report_text = format_report(result=result, target_files=target_files, secrets_result=secrets_result, symlink_result=symlink_result, dependency_result=dependency_result, mock_result=mock_result, control_flow_result=control_flow_result, strict_mocks=getattr(args, 'strict_mocks', False), strict_error_handling=getattr(args, 'strict_error_handling', False), repo_root=repo_root or cwd, threshold=args.threshold)
|
|
115
|
+
print(report_text)
|
|
116
|
+
if result.collection_error:
|
|
117
|
+
return 2
|
|
118
|
+
strict_mocks_triggered = bool(getattr(args, 'strict_mocks', False) and mock_result.total_findings > 0)
|
|
119
|
+
strict_error_triggered = bool(getattr(args, 'strict_error_handling', True) and control_flow_result.total_findings > 0)
|
|
120
|
+
if result.mutation_score is not None and result.mutation_score < args.threshold or len(result.untested_files) > 0 or len(secrets_result.findings) > 0 or (len(symlink_result.escape_findings) > 0) or (dependency_result.high_risk_count > 0) or strict_mocks_triggered or strict_error_triggered:
|
|
121
|
+
return 1
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
def handle_init(args: argparse.Namespace) -> int:
|
|
125
|
+
"""Handle the 'init' subcommand to initialize configuration and git hooks."""
|
|
126
|
+
cwd = Path.cwd().resolve()
|
|
127
|
+
try:
|
|
128
|
+
repo_root = get_git_root(cwd)
|
|
129
|
+
except DiffScopeError:
|
|
130
|
+
repo_root = cwd
|
|
131
|
+
print(f'DeployProof: Initializing in {repo_root}...')
|
|
132
|
+
config_path = repo_root / '.deployproof.json'
|
|
133
|
+
if not config_path.exists():
|
|
134
|
+
import json
|
|
135
|
+
default_config = {'version': __version__, 'threshold': 80.0, 'timeout': 10.0, 'secrets_scanning': True, 'symlink_scanning': True, 'dependency_scanning': True}
|
|
136
|
+
config_path.write_text(json.dumps(default_config, indent=2) + '\n', encoding='utf-8')
|
|
137
|
+
print(f' [+] Created configuration file: {config_path.name}')
|
|
138
|
+
else:
|
|
139
|
+
print(f' [.] Configuration file already exists: {config_path.name}')
|
|
140
|
+
hooks_dir = repo_root / '.git' / 'hooks'
|
|
141
|
+
if hooks_dir.is_dir():
|
|
142
|
+
pre_push_hook = hooks_dir / 'pre-push'
|
|
143
|
+
hook_script = '#!/usr/bin/env sh\n# DeployProof deterministic pre-push verification gate\ndeployproof check\n'
|
|
144
|
+
pre_push_hook.write_text(hook_script, encoding='utf-8')
|
|
145
|
+
try:
|
|
146
|
+
import stat
|
|
147
|
+
pre_push_hook.chmod(pre_push_hook.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
print(' [+] Installed git pre-push hook: .git/hooks/pre-push')
|
|
151
|
+
else:
|
|
152
|
+
print(' [i] Note: .git/hooks directory not found. Initialize git to enable automatic pre-push gating.')
|
|
153
|
+
print("\nDeployProof initialization complete. Run 'deployproof check' to verify your session changes.")
|
|
154
|
+
return 0
|
|
155
|
+
|
|
156
|
+
def main(argv: Optional[List[str]]=None) -> int:
|
|
157
|
+
"""Entry point for the DeployProof CLI."""
|
|
158
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
159
|
+
try:
|
|
160
|
+
sys.stdout.reconfigure(errors='replace')
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
if hasattr(sys.stderr, 'reconfigure'):
|
|
164
|
+
try:
|
|
165
|
+
sys.stderr.reconfigure(errors='replace')
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
parser = create_parser()
|
|
169
|
+
args = parser.parse_args(argv)
|
|
170
|
+
if not args.command:
|
|
171
|
+
parser.print_help()
|
|
172
|
+
return 0
|
|
173
|
+
if args.command == 'check':
|
|
174
|
+
return handle_check(args)
|
|
175
|
+
elif args.command == 'init':
|
|
176
|
+
return handle_init(args)
|
|
177
|
+
return 0
|
|
178
|
+
if __name__ == '__main__':
|
|
179
|
+
sys.exit(main())
|
|
@@ -983,7 +983,7 @@ def query_pypi_registry(
|
|
|
983
983
|
url = f"https://pypi.org/pypi/{canonical_pkg}/json"
|
|
984
984
|
req = urllib.request.Request(
|
|
985
985
|
url,
|
|
986
|
-
headers={"User-Agent": "DeployProof/0.1
|
|
986
|
+
headers={"User-Agent": "DeployProof/0.2.1 (https://github.com/SVSPraveen/DeployProof)"},
|
|
987
987
|
)
|
|
988
988
|
|
|
989
989
|
try:
|
|
@@ -993,6 +993,7 @@ def query_pypi_registry(
|
|
|
993
993
|
data = json.loads(raw_bytes.decode("utf-8"))
|
|
994
994
|
releases = data.get("releases", {})
|
|
995
995
|
timestamps: List[datetime.datetime] = []
|
|
996
|
+
parse_error: Optional[str] = None
|
|
996
997
|
|
|
997
998
|
for ver, files in releases.items():
|
|
998
999
|
for f in files:
|
|
@@ -1001,10 +1002,14 @@ def query_pypi_registry(
|
|
|
1001
1002
|
try:
|
|
1002
1003
|
dt = datetime.datetime.fromisoformat(t_str.replace("Z", "+00:00"))
|
|
1003
1004
|
timestamps.append(dt)
|
|
1004
|
-
except
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1005
|
+
except (ValueError, TypeError) as e:
|
|
1006
|
+
parse_error = f"Failed to parse release timestamp '{t_str}': {e}"
|
|
1007
|
+
except Exception as e:
|
|
1008
|
+
parse_error = f"Unexpected error parsing release timestamp '{t_str}': {e}"
|
|
1009
|
+
|
|
1010
|
+
if parse_error and not timestamps:
|
|
1011
|
+
result = ("UNKNOWN", None, None, f"PyPI release date parse error for package '{canonical_pkg}': {parse_error}")
|
|
1012
|
+
elif not timestamps:
|
|
1008
1013
|
if canonical_pkg.lower() in KNOWN_NAMESPACE_ROOTS or norm_name.lower() in KNOWN_NAMESPACE_ROOTS:
|
|
1009
1014
|
result = ("OK", None, None, f"Namespace package umbrella root ('{canonical_pkg}') - subpackages published separately on PyPI")
|
|
1010
1015
|
else:
|