deployproof 0.1.7__tar.gz → 0.1.8__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.7/src/deployproof.egg-info → deployproof-0.1.8}/PKG-INFO +106 -9
- {deployproof-0.1.7 → deployproof-0.1.8}/README.md +105 -8
- {deployproof-0.1.7 → deployproof-0.1.8}/pyproject.toml +1 -1
- {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/__init__.py +1 -1
- {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/cli.py +4 -1
- {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/dependencies.py +128 -31
- {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/diff.py +5 -1
- {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/mutator.py +239 -44
- {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/reporter.py +22 -9
- {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/secrets.py +55 -15
- {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/symlinks.py +156 -156
- {deployproof-0.1.7 → deployproof-0.1.8/src/deployproof.egg-info}/PKG-INFO +106 -9
- {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof.egg-info/SOURCES.txt +1 -0
- deployproof-0.1.8/tests/test_baseline_collection_error.py +120 -0
- {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_cli.py +3 -3
- {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_control_flow.py +4 -3
- {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_dependencies_extractor.py +69 -0
- {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_dependencies_pypi.py +8 -0
- {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_mutator.py +17 -0
- {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_secrets.py +26 -0
- {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_stress_regressions.py +10 -3
- {deployproof-0.1.7 → deployproof-0.1.8}/LICENSE +0 -0
- {deployproof-0.1.7 → deployproof-0.1.8}/setup.cfg +0 -0
- {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/control_flow.py +0 -0
- {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/mocks.py +0 -0
- {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/wsl.py +0 -0
- {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof.egg-info/dependency_links.txt +0 -0
- {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof.egg-info/entry_points.txt +0 -0
- {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof.egg-info/requires.txt +0 -0
- {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof.egg-info/top_level.txt +0 -0
- {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_cli_dependency_scan.py +0 -0
- {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_diff.py +0 -0
- {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_edge_cases_validation.py +0 -0
- {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_mocks.py +0 -0
- {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_mutation_suites.py +0 -0
- {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_symlinks.py +0 -0
- {deployproof-0.1.7 → deployproof-0.1.8}/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.1.8
|
|
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,9 +32,11 @@ 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
|
+
[](https://pypi.org/project/deployproof/)
|
|
37
|
+
[](https://github.com/SVSPraveen/DeployProof/actions/workflows/ci.yml)
|
|
38
|
+
[](https://github.com/SVSPraveen/DeployProof)
|
|
39
|
+
[](stress_fixtures/)
|
|
38
40
|
[](LICENSE)
|
|
39
41
|
|
|
40
42
|
---
|
|
@@ -65,7 +67,100 @@ Run all verification checks against changes in the current session (git diff):
|
|
|
65
67
|
deployproof check
|
|
66
68
|
```
|
|
67
69
|
|
|
68
|
-
Output includes a section for each check — symlink scan, secrets scan, dependency scan, mock detection, control flow analysis, and mutation score — with a pass/fail line at the bottom.
|
|
70
|
+
Output includes a section for each check — symlink scan, secrets scan, dependency scan, mock detection, control flow analysis, and mutation score — with a pass/fail line at the bottom.
|
|
71
|
+
|
|
72
|
+
### Exit Codes:
|
|
73
|
+
* `0` — **PASSED**: All verification checks passed and mutation score meets threshold.
|
|
74
|
+
* `1` — **FAILED**: Code quality or security gate triggered (mutation score below threshold, untested files, hardcoded secrets, sandbox-escape symlinks, hallucinated packages, or strict flags).
|
|
75
|
+
* `2` — **ERROR**: Test environment failure (test suite failed to collect or execute before mutation testing began due to missing dependencies or broken imports).
|
|
76
|
+
|
|
77
|
+
### Example Walkthrough
|
|
78
|
+
|
|
79
|
+
Given a newly written function `calculator.py`:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
def calculate_discount(price: float, rate: float) -> float:
|
|
83
|
+
if rate > 0.5:
|
|
84
|
+
return price * 0.5
|
|
85
|
+
return price * (1.0 - rate)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
With an AI-generated test that achieves 100% line coverage by only asserting standard discounts (`rate = 0.2`):
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
def test_calculate_discount_basic():
|
|
92
|
+
assert calculate_discount(100.0, 0.2) == 80.0
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Running `deployproof check` mutates AST operators and detects that boundary conditions and threshold caps are untested:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
$ deployproof check
|
|
99
|
+
|
|
100
|
+
DeployProof - LOCAL PRE-CHECK (approximate) - not the verified score
|
|
101
|
+
====================================================================
|
|
102
|
+
|
|
103
|
+
Target Scope (1 file evaluated):
|
|
104
|
+
* calculator.py
|
|
105
|
+
|
|
106
|
+
Symlink & Sandbox Escape Scan (CWE-61/CWE-451):
|
|
107
|
+
Clean: No symlinks or sandbox-escape traversal links detected across 1 session file.
|
|
108
|
+
|
|
109
|
+
Secrets & Credentials Pre-Push Scan:
|
|
110
|
+
Clean: No hardcoded secrets or tracked .env files detected across 1 session file.
|
|
111
|
+
|
|
112
|
+
Dependency & Slopsquatting Scan (PyPI Registry & Age Analysis):
|
|
113
|
+
Clean: No new external packages introduced across 1 session file.
|
|
114
|
+
|
|
115
|
+
Mock Usage Introduced (flagged for review):
|
|
116
|
+
Clean: No modified test files in scope.
|
|
117
|
+
|
|
118
|
+
Control Flow & Error Handling (flagged for review):
|
|
119
|
+
Clean: No bare excepts, swallowed exceptions, or unreachable code detected across 1 session file.
|
|
120
|
+
|
|
121
|
+
Local Pre-Check Mutation Verification:
|
|
122
|
+
Score: 57.1% (4/7 mutants killed)
|
|
123
|
+
Status: FAILED (score 57.1% below 80.0%) (threshold: 80.0%)
|
|
124
|
+
Time: 2.27s
|
|
125
|
+
|
|
126
|
+
Skipped Constructs: None (No known unsupported constructs detected)
|
|
127
|
+
|
|
128
|
+
Surviving Mutants (3 unverified changes):
|
|
129
|
+
|
|
130
|
+
[1] calculator.py:2
|
|
131
|
+
Mutation: Replace numeric constant '0.5' with '1.5'
|
|
132
|
+
Original: if rate > 0.5:
|
|
133
|
+
Mutated: if rate > 1.5:
|
|
134
|
+
|
|
135
|
+
[2] calculator.py:3
|
|
136
|
+
Mutation: Replace numeric constant '0.5' with '1.5'
|
|
137
|
+
Original: return price * 0.5
|
|
138
|
+
Mutated: return price * 1.5
|
|
139
|
+
|
|
140
|
+
[3] calculator.py:3
|
|
141
|
+
Mutation: Replace binary operator '*' with '/'
|
|
142
|
+
Original: return price * 0.5
|
|
143
|
+
Mutated: return price / 0.5
|
|
144
|
+
|
|
145
|
+
====================================================================
|
|
146
|
+
Notice: Local pre-check only. Full verified score runs in CI on push (via mutmut).
|
|
147
|
+
Pre-check FAILED: Score 57.1% is below threshold 80.0% (3 surviving mutants).
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Adding tests for threshold cap (`rate = 0.8`) and exact boundary (`rate = 0.5`) kills all mutants:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
Local Pre-Check Mutation Verification:
|
|
154
|
+
Score: 100.0% (7/7 mutants killed)
|
|
155
|
+
Status: PASSED (threshold: 80.0%)
|
|
156
|
+
Time: 2.31s
|
|
157
|
+
|
|
158
|
+
Surviving Mutants: None (All generated mutants caught by test suite)
|
|
159
|
+
|
|
160
|
+
====================================================================
|
|
161
|
+
Notice: Local pre-check only. Full verified score runs in CI on push (via mutmut).
|
|
162
|
+
Pre-check clean: 100% of tested basic mutations caught.
|
|
163
|
+
```
|
|
69
164
|
|
|
70
165
|
### CLI Options & Flags
|
|
71
166
|
|
|
@@ -80,6 +175,8 @@ Output includes a section for each check — symlink scan, secrets scan, depende
|
|
|
80
175
|
| `deployproof check --base <ref>` | Base git ref (branch/commit/tag) to diff against. |
|
|
81
176
|
| `deployproof check --wsl` | Delegate mutation testing to `mutmut` inside WSL (Windows only). |
|
|
82
177
|
|
|
178
|
+
> **Note:** `deployproof check --wsl` (Windows only) is newer and less battle-tested than the core checks — [file an issue](https://github.com/SVSPraveen/DeployProof/issues) if you hit something.
|
|
179
|
+
|
|
83
180
|
### Machine-Readable Output (`--json`)
|
|
84
181
|
|
|
85
182
|
DeployProof provides a stable structured JSON schema for CI/CD pipelines, IDEs, and automation:
|
|
@@ -92,7 +189,7 @@ deployproof check --json
|
|
|
92
189
|
|
|
93
190
|
```json
|
|
94
191
|
{
|
|
95
|
-
"version": "0.1.
|
|
192
|
+
"version": "0.1.8",
|
|
96
193
|
"status": "passed",
|
|
97
194
|
"summary": {
|
|
98
195
|
"target_files_count": 1,
|
|
@@ -181,17 +278,17 @@ deployproof check --json
|
|
|
181
278
|
|
|
182
279
|
## See It Catch Real Bugs
|
|
183
280
|
|
|
184
|
-
Clone this repository and run the standalone stress-test suite to see DeployProof evaluate
|
|
281
|
+
Clone this repository and run the standalone stress-test suite to see DeployProof evaluate 11 planted edge cases:
|
|
185
282
|
|
|
186
283
|
```bash
|
|
187
284
|
python stress_fixtures/run_stress_tests.py
|
|
188
285
|
```
|
|
189
286
|
|
|
190
|
-
Fixtures cover weak test suites, zero-test orphan modules, planted OpenAI/AWS credentials,
|
|
287
|
+
Fixtures cover weak test suites, zero-test orphan modules, planted OpenAI/AWS credentials, GhostApproval sandbox-escape traps, swallowed exceptions/dead code, and mock-masked broken implementations.
|
|
191
288
|
|
|
192
289
|
## Status & Roadmap
|
|
193
290
|
|
|
194
|
-
- **Current (v0.1.
|
|
291
|
+
- **Current (v0.1.8):** Diff-scoped AST mutation testing (with recursive `test/`/`tests/` discovery and precise column-offset snippet reconstruction), 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` and deduplicated decorator detection), control-flow / swallowed-exception scanner (`--strict-error-handling`), 11/11 launch-day stress test suite, 88 unit tests, machine-readable `--json` output, and Tier 2 CI verification via GitHub Actions (mutmut).
|
|
195
292
|
- **Next:** Multi-language mutation support and expanded ecosystem rule packs.
|
|
196
293
|
|
|
197
294
|
## Contributing
|
|
@@ -2,9 +2,11 @@
|
|
|
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
|
+
[](https://pypi.org/project/deployproof/)
|
|
7
|
+
[](https://github.com/SVSPraveen/DeployProof/actions/workflows/ci.yml)
|
|
8
|
+
[](https://github.com/SVSPraveen/DeployProof)
|
|
9
|
+
[](stress_fixtures/)
|
|
8
10
|
[](LICENSE)
|
|
9
11
|
|
|
10
12
|
---
|
|
@@ -35,7 +37,100 @@ Run all verification checks against changes in the current session (git diff):
|
|
|
35
37
|
deployproof check
|
|
36
38
|
```
|
|
37
39
|
|
|
38
|
-
Output includes a section for each check — symlink scan, secrets scan, dependency scan, mock detection, control flow analysis, and mutation score — with a pass/fail line at the bottom.
|
|
40
|
+
Output includes a section for each check — symlink scan, secrets scan, dependency scan, mock detection, control flow analysis, and mutation score — with a pass/fail line at the bottom.
|
|
41
|
+
|
|
42
|
+
### Exit Codes:
|
|
43
|
+
* `0` — **PASSED**: All verification checks passed and mutation score meets threshold.
|
|
44
|
+
* `1` — **FAILED**: Code quality or security gate triggered (mutation score below threshold, untested files, hardcoded secrets, sandbox-escape symlinks, hallucinated packages, or strict flags).
|
|
45
|
+
* `2` — **ERROR**: Test environment failure (test suite failed to collect or execute before mutation testing began due to missing dependencies or broken imports).
|
|
46
|
+
|
|
47
|
+
### Example Walkthrough
|
|
48
|
+
|
|
49
|
+
Given a newly written function `calculator.py`:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
def calculate_discount(price: float, rate: float) -> float:
|
|
53
|
+
if rate > 0.5:
|
|
54
|
+
return price * 0.5
|
|
55
|
+
return price * (1.0 - rate)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
With an AI-generated test that achieves 100% line coverage by only asserting standard discounts (`rate = 0.2`):
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
def test_calculate_discount_basic():
|
|
62
|
+
assert calculate_discount(100.0, 0.2) == 80.0
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Running `deployproof check` mutates AST operators and detects that boundary conditions and threshold caps are untested:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
$ deployproof check
|
|
69
|
+
|
|
70
|
+
DeployProof - LOCAL PRE-CHECK (approximate) - not the verified score
|
|
71
|
+
====================================================================
|
|
72
|
+
|
|
73
|
+
Target Scope (1 file evaluated):
|
|
74
|
+
* calculator.py
|
|
75
|
+
|
|
76
|
+
Symlink & Sandbox Escape Scan (CWE-61/CWE-451):
|
|
77
|
+
Clean: No symlinks or sandbox-escape traversal links detected across 1 session file.
|
|
78
|
+
|
|
79
|
+
Secrets & Credentials Pre-Push Scan:
|
|
80
|
+
Clean: No hardcoded secrets or tracked .env files detected across 1 session file.
|
|
81
|
+
|
|
82
|
+
Dependency & Slopsquatting Scan (PyPI Registry & Age Analysis):
|
|
83
|
+
Clean: No new external packages introduced across 1 session file.
|
|
84
|
+
|
|
85
|
+
Mock Usage Introduced (flagged for review):
|
|
86
|
+
Clean: No modified test files in scope.
|
|
87
|
+
|
|
88
|
+
Control Flow & Error Handling (flagged for review):
|
|
89
|
+
Clean: No bare excepts, swallowed exceptions, or unreachable code detected across 1 session file.
|
|
90
|
+
|
|
91
|
+
Local Pre-Check Mutation Verification:
|
|
92
|
+
Score: 57.1% (4/7 mutants killed)
|
|
93
|
+
Status: FAILED (score 57.1% below 80.0%) (threshold: 80.0%)
|
|
94
|
+
Time: 2.27s
|
|
95
|
+
|
|
96
|
+
Skipped Constructs: None (No known unsupported constructs detected)
|
|
97
|
+
|
|
98
|
+
Surviving Mutants (3 unverified changes):
|
|
99
|
+
|
|
100
|
+
[1] calculator.py:2
|
|
101
|
+
Mutation: Replace numeric constant '0.5' with '1.5'
|
|
102
|
+
Original: if rate > 0.5:
|
|
103
|
+
Mutated: if rate > 1.5:
|
|
104
|
+
|
|
105
|
+
[2] calculator.py:3
|
|
106
|
+
Mutation: Replace numeric constant '0.5' with '1.5'
|
|
107
|
+
Original: return price * 0.5
|
|
108
|
+
Mutated: return price * 1.5
|
|
109
|
+
|
|
110
|
+
[3] calculator.py:3
|
|
111
|
+
Mutation: Replace binary operator '*' with '/'
|
|
112
|
+
Original: return price * 0.5
|
|
113
|
+
Mutated: return price / 0.5
|
|
114
|
+
|
|
115
|
+
====================================================================
|
|
116
|
+
Notice: Local pre-check only. Full verified score runs in CI on push (via mutmut).
|
|
117
|
+
Pre-check FAILED: Score 57.1% is below threshold 80.0% (3 surviving mutants).
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Adding tests for threshold cap (`rate = 0.8`) and exact boundary (`rate = 0.5`) kills all mutants:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
Local Pre-Check Mutation Verification:
|
|
124
|
+
Score: 100.0% (7/7 mutants killed)
|
|
125
|
+
Status: PASSED (threshold: 80.0%)
|
|
126
|
+
Time: 2.31s
|
|
127
|
+
|
|
128
|
+
Surviving Mutants: None (All generated mutants caught by test suite)
|
|
129
|
+
|
|
130
|
+
====================================================================
|
|
131
|
+
Notice: Local pre-check only. Full verified score runs in CI on push (via mutmut).
|
|
132
|
+
Pre-check clean: 100% of tested basic mutations caught.
|
|
133
|
+
```
|
|
39
134
|
|
|
40
135
|
### CLI Options & Flags
|
|
41
136
|
|
|
@@ -50,6 +145,8 @@ Output includes a section for each check — symlink scan, secrets scan, depende
|
|
|
50
145
|
| `deployproof check --base <ref>` | Base git ref (branch/commit/tag) to diff against. |
|
|
51
146
|
| `deployproof check --wsl` | Delegate mutation testing to `mutmut` inside WSL (Windows only). |
|
|
52
147
|
|
|
148
|
+
> **Note:** `deployproof check --wsl` (Windows only) is newer and less battle-tested than the core checks — [file an issue](https://github.com/SVSPraveen/DeployProof/issues) if you hit something.
|
|
149
|
+
|
|
53
150
|
### Machine-Readable Output (`--json`)
|
|
54
151
|
|
|
55
152
|
DeployProof provides a stable structured JSON schema for CI/CD pipelines, IDEs, and automation:
|
|
@@ -62,7 +159,7 @@ deployproof check --json
|
|
|
62
159
|
|
|
63
160
|
```json
|
|
64
161
|
{
|
|
65
|
-
"version": "0.1.
|
|
162
|
+
"version": "0.1.8",
|
|
66
163
|
"status": "passed",
|
|
67
164
|
"summary": {
|
|
68
165
|
"target_files_count": 1,
|
|
@@ -151,17 +248,17 @@ deployproof check --json
|
|
|
151
248
|
|
|
152
249
|
## See It Catch Real Bugs
|
|
153
250
|
|
|
154
|
-
Clone this repository and run the standalone stress-test suite to see DeployProof evaluate
|
|
251
|
+
Clone this repository and run the standalone stress-test suite to see DeployProof evaluate 11 planted edge cases:
|
|
155
252
|
|
|
156
253
|
```bash
|
|
157
254
|
python stress_fixtures/run_stress_tests.py
|
|
158
255
|
```
|
|
159
256
|
|
|
160
|
-
Fixtures cover weak test suites, zero-test orphan modules, planted OpenAI/AWS credentials,
|
|
257
|
+
Fixtures cover weak test suites, zero-test orphan modules, planted OpenAI/AWS credentials, GhostApproval sandbox-escape traps, swallowed exceptions/dead code, and mock-masked broken implementations.
|
|
161
258
|
|
|
162
259
|
## Status & Roadmap
|
|
163
260
|
|
|
164
|
-
- **Current (v0.1.
|
|
261
|
+
- **Current (v0.1.8):** Diff-scoped AST mutation testing (with recursive `test/`/`tests/` discovery and precise column-offset snippet reconstruction), 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` and deduplicated decorator detection), control-flow / swallowed-exception scanner (`--strict-error-handling`), 11/11 launch-day stress test suite, 88 unit tests, machine-readable `--json` output, and Tier 2 CI verification via GitHub Actions (mutmut).
|
|
165
262
|
- **Next:** Multi-language mutation support and expanded ecosystem rule packs.
|
|
166
263
|
|
|
167
264
|
## 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.1.8"
|
|
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"
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""DeployProof: Deterministic deployability verification for AI-assisted code."""
|
|
2
|
-
__version__ = "0.1.
|
|
2
|
+
__version__ = "0.1.8"
|
|
@@ -301,13 +301,16 @@ def handle_check(args: argparse.Namespace) -> int:
|
|
|
301
301
|
)
|
|
302
302
|
print(report_text)
|
|
303
303
|
|
|
304
|
+
if result.collection_error:
|
|
305
|
+
return 2
|
|
306
|
+
|
|
304
307
|
# Fail if mutation score threshold not met, secrets detected, symlink sandbox escapes found, hallucinated packages detected, or strict flags triggered
|
|
305
308
|
strict_mocks_triggered = bool(getattr(args, "strict_mocks", False) and mock_result.total_findings > 0)
|
|
306
309
|
strict_error_triggered = bool(
|
|
307
310
|
getattr(args, "strict_error_handling", False) and control_flow_result.total_findings > 0
|
|
308
311
|
)
|
|
309
312
|
if (
|
|
310
|
-
result.mutation_score < args.threshold
|
|
313
|
+
(result.mutation_score is not None and result.mutation_score < args.threshold)
|
|
311
314
|
or len(result.untested_files) > 0
|
|
312
315
|
or len(secrets_result.findings) > 0
|
|
313
316
|
or len(symlink_result.escape_findings) > 0
|
|
@@ -9,12 +9,14 @@ from dataclasses import dataclass
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Dict, List, Optional, Set
|
|
11
11
|
|
|
12
|
-
# Standard library module names (Python 3.10+)
|
|
13
|
-
STDLIB_MODULES: Set[str] = getattr(sys, "stdlib_module_names", set()) | {
|
|
12
|
+
# Standard library module names (Python 3.10 through 3.14+)
|
|
13
|
+
STDLIB_MODULES: Set[str] = set(getattr(sys, "stdlib_module_names", set())) | {
|
|
14
14
|
"__future__",
|
|
15
15
|
"_thread",
|
|
16
|
+
"_typeshed",
|
|
16
17
|
"abc",
|
|
17
18
|
"aifc",
|
|
19
|
+
"annotationlib",
|
|
18
20
|
"argparse",
|
|
19
21
|
"array",
|
|
20
22
|
"ast",
|
|
@@ -74,10 +76,12 @@ STDLIB_MODULES: Set[str] = getattr(sys, "stdlib_module_names", set()) | {
|
|
|
74
76
|
"ftplib",
|
|
75
77
|
"functools",
|
|
76
78
|
"gc",
|
|
79
|
+
"genericpath",
|
|
77
80
|
"getopt",
|
|
78
81
|
"getpass",
|
|
79
82
|
"gettext",
|
|
80
83
|
"glob",
|
|
84
|
+
"graphlib",
|
|
81
85
|
"grp",
|
|
82
86
|
"gzip",
|
|
83
87
|
"hashlib",
|
|
@@ -113,6 +117,7 @@ STDLIB_MODULES: Set[str] = getattr(sys, "stdlib_module_names", set()) | {
|
|
|
113
117
|
"multiprocessing",
|
|
114
118
|
"netrc",
|
|
115
119
|
"nntplib",
|
|
120
|
+
"ntpath",
|
|
116
121
|
"numbers",
|
|
117
122
|
"operator",
|
|
118
123
|
"optparse",
|
|
@@ -163,6 +168,9 @@ STDLIB_MODULES: Set[str] = getattr(sys, "stdlib_module_names", set()) | {
|
|
|
163
168
|
"socketserver",
|
|
164
169
|
"spwd",
|
|
165
170
|
"sqlite3",
|
|
171
|
+
"sre_compile",
|
|
172
|
+
"sre_constants",
|
|
173
|
+
"sre_parse",
|
|
166
174
|
"ssl",
|
|
167
175
|
"stat",
|
|
168
176
|
"statistics",
|
|
@@ -246,6 +254,35 @@ IMPORT_TO_PYPI_MAP: Dict[str, str] = {
|
|
|
246
254
|
"Bio": "biopython",
|
|
247
255
|
"psycopg2": "psycopg2",
|
|
248
256
|
"google": "protobuf",
|
|
257
|
+
"OpenSSL": "pyOpenSSL",
|
|
258
|
+
"openssl": "pyOpenSSL",
|
|
259
|
+
"OpenGL": "PyOpenGL",
|
|
260
|
+
"wx": "wxPython",
|
|
261
|
+
"gi": "PyGObject",
|
|
262
|
+
"googleapiclient": "google-api-python-client",
|
|
263
|
+
"kafka": "kafka-python",
|
|
264
|
+
"pydantic_core": "pydantic-core",
|
|
265
|
+
"pkg_resources": "setuptools",
|
|
266
|
+
"setuptools": "setuptools",
|
|
267
|
+
"brotli": "Brotli",
|
|
268
|
+
"zstandard": "zstandard",
|
|
269
|
+
"markupsafe": "MarkupSafe",
|
|
270
|
+
"sqlalchemy": "SQLAlchemy",
|
|
271
|
+
"playwright": "playwright",
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
# Known namespace umbrella roots (subpackages published separately on PyPI)
|
|
275
|
+
KNOWN_NAMESPACE_ROOTS: Set[str] = {
|
|
276
|
+
"backports",
|
|
277
|
+
"google",
|
|
278
|
+
"azure",
|
|
279
|
+
"oslo",
|
|
280
|
+
"zope",
|
|
281
|
+
"jaraco",
|
|
282
|
+
"sphinxcontrib",
|
|
283
|
+
"ruamel",
|
|
284
|
+
"zc",
|
|
285
|
+
"plone",
|
|
249
286
|
}
|
|
250
287
|
|
|
251
288
|
MANIFEST_FILENAMES = {
|
|
@@ -256,6 +293,55 @@ MANIFEST_FILENAMES = {
|
|
|
256
293
|
}
|
|
257
294
|
|
|
258
295
|
|
|
296
|
+
def is_stdlib_or_internal(module_name: str) -> bool:
|
|
297
|
+
"""Check if a module name is standard library, typeshed, or internal."""
|
|
298
|
+
cleaned = module_name.strip()
|
|
299
|
+
if not cleaned:
|
|
300
|
+
return True
|
|
301
|
+
if cleaned.startswith("_"):
|
|
302
|
+
return True
|
|
303
|
+
if cleaned in STDLIB_MODULES:
|
|
304
|
+
return True
|
|
305
|
+
if hasattr(sys, "stdlib_module_names") and cleaned in sys.stdlib_module_names:
|
|
306
|
+
return True
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def is_requirements_manifest_file(file_path: Path) -> bool:
|
|
311
|
+
"""
|
|
312
|
+
Check if a file is a requirements manifest file based on specific naming patterns.
|
|
313
|
+
|
|
314
|
+
Accepts: requirements*.txt, *-requirements.txt, requirements/*.txt, constraints.txt, *.in, etc.
|
|
315
|
+
Rejects: arbitrary .txt files (LICENSE.txt, README.txt, NOTICE.txt, hello.txt, etc.).
|
|
316
|
+
"""
|
|
317
|
+
name = file_path.name.lower()
|
|
318
|
+
parent_names = [p.name.lower() for p in file_path.parents]
|
|
319
|
+
in_req_dir = any(p in ("requirements", "reqs", "constraints") for p in parent_names)
|
|
320
|
+
|
|
321
|
+
if in_req_dir and file_path.suffix in (".txt", ".in"):
|
|
322
|
+
return True
|
|
323
|
+
|
|
324
|
+
if name in ("requirements.txt", "constraints.txt", "requirements.in", "constraints.in"):
|
|
325
|
+
return True
|
|
326
|
+
|
|
327
|
+
if name.startswith("requirements") and file_path.suffix in (".txt", ".in"):
|
|
328
|
+
return True
|
|
329
|
+
|
|
330
|
+
if name.startswith("constraints") and file_path.suffix in (".txt", ".in"):
|
|
331
|
+
return True
|
|
332
|
+
|
|
333
|
+
if name.endswith(("-requirements.txt", "_requirements.txt", ".requirements.txt", "-constraints.txt", "_constraints.txt", ".constraints.txt")):
|
|
334
|
+
return True
|
|
335
|
+
|
|
336
|
+
if name.endswith(("-requirements.in", "_requirements.in", ".requirements.in", "-constraints.in", "_constraints.in", ".constraints.in")):
|
|
337
|
+
return True
|
|
338
|
+
|
|
339
|
+
if name.endswith(".in") and any(k in name for k in ("req", "dev", "test", "base", "prod", "doc")):
|
|
340
|
+
return True
|
|
341
|
+
|
|
342
|
+
return False
|
|
343
|
+
|
|
344
|
+
|
|
259
345
|
@dataclass(frozen=True)
|
|
260
346
|
class ExtractedDependency:
|
|
261
347
|
"""Represents a newly introduced external dependency."""
|
|
@@ -277,7 +363,7 @@ def get_local_module_names(root: Path) -> Set[str]:
|
|
|
277
363
|
"""
|
|
278
364
|
local_names: Set[str] = set()
|
|
279
365
|
|
|
280
|
-
search_dirs = [root, root / "src", root / "lib", root / "app"]
|
|
366
|
+
search_dirs = [root, root / "src", root / "lib", root / "app", root / "tests", root / "test", root / "examples"]
|
|
281
367
|
for s_dir in search_dirs:
|
|
282
368
|
if not s_dir.is_dir():
|
|
283
369
|
continue
|
|
@@ -295,10 +381,11 @@ def get_local_module_names(root: Path) -> Set[str]:
|
|
|
295
381
|
except (PermissionError, OSError):
|
|
296
382
|
continue
|
|
297
383
|
|
|
298
|
-
# Also scan for
|
|
384
|
+
# Also scan recursively for package directories
|
|
299
385
|
try:
|
|
300
|
-
for py_file in root.
|
|
301
|
-
if not any(part.startswith(".") or part in ("venv", ".venv", "build", "dist", "node_modules") for part in py_file.parts):
|
|
386
|
+
for py_file in root.rglob("*.py"):
|
|
387
|
+
if not any(part.startswith(".") or part in ("venv", ".venv", "build", "dist", "node_modules", "__pycache__") for part in py_file.parts):
|
|
388
|
+
local_names.add(py_file.stem)
|
|
302
389
|
local_names.add(py_file.parent.name)
|
|
303
390
|
except (PermissionError, OSError):
|
|
304
391
|
pass
|
|
@@ -402,6 +489,9 @@ def normalize_package_name(raw_name: str) -> str:
|
|
|
402
489
|
cleaned = raw_name.strip()
|
|
403
490
|
if cleaned in IMPORT_TO_PYPI_MAP:
|
|
404
491
|
return IMPORT_TO_PYPI_MAP[cleaned]
|
|
492
|
+
for imp_k, pypi_v in IMPORT_TO_PYPI_MAP.items():
|
|
493
|
+
if imp_k.lower() == cleaned.lower():
|
|
494
|
+
return pypi_v
|
|
405
495
|
return cleaned
|
|
406
496
|
|
|
407
497
|
|
|
@@ -437,7 +527,7 @@ def extract_new_imports_from_py_file(
|
|
|
437
527
|
|
|
438
528
|
for alias in node.names:
|
|
439
529
|
top_name = alias.name.split(".")[0]
|
|
440
|
-
if top_name
|
|
530
|
+
if is_stdlib_or_internal(top_name) or top_name in local_modules:
|
|
441
531
|
continue
|
|
442
532
|
pypi_name = normalize_package_name(top_name)
|
|
443
533
|
key = f"{pypi_name}:{node.lineno}"
|
|
@@ -462,7 +552,7 @@ def extract_new_imports_from_py_file(
|
|
|
462
552
|
continue
|
|
463
553
|
|
|
464
554
|
top_name = node.module.split(".")[0]
|
|
465
|
-
if top_name
|
|
555
|
+
if is_stdlib_or_internal(top_name) or top_name in local_modules:
|
|
466
556
|
continue
|
|
467
557
|
|
|
468
558
|
pypi_name = normalize_package_name(top_name)
|
|
@@ -507,7 +597,7 @@ def extract_new_imports_from_py_file(
|
|
|
507
597
|
if isinstance(first_arg, ast.Constant) and isinstance(first_arg.value, str):
|
|
508
598
|
raw_mod_name = first_arg.value.strip()
|
|
509
599
|
top_name = raw_mod_name.split(".")[0]
|
|
510
|
-
if not top_name or top_name
|
|
600
|
+
if not top_name or is_stdlib_or_internal(top_name) or top_name in local_modules:
|
|
511
601
|
continue
|
|
512
602
|
|
|
513
603
|
pypi_name = normalize_package_name(top_name)
|
|
@@ -665,7 +755,7 @@ def _parse_requirements_file(
|
|
|
665
755
|
m = req_name_re.match(raw_line)
|
|
666
756
|
if m:
|
|
667
757
|
pkg_name = m.group(1).strip()
|
|
668
|
-
if
|
|
758
|
+
if not is_stdlib_or_internal(pkg_name) and pkg_name not in local_modules:
|
|
669
759
|
try:
|
|
670
760
|
rel_source_type = file_path.relative_to(root).as_posix()
|
|
671
761
|
except ValueError:
|
|
@@ -696,14 +786,7 @@ def extract_new_manifest_dependencies(
|
|
|
696
786
|
local_modules = get_local_module_names(root)
|
|
697
787
|
|
|
698
788
|
name = file_path.name.lower()
|
|
699
|
-
is_requirements = (
|
|
700
|
-
name == "requirements.txt"
|
|
701
|
-
or name.endswith(".requirements.txt")
|
|
702
|
-
or name.startswith("requirements-")
|
|
703
|
-
or name.endswith("-requirements.txt")
|
|
704
|
-
or name.endswith(".in")
|
|
705
|
-
or file_path.suffix == ".txt"
|
|
706
|
-
)
|
|
789
|
+
is_requirements = is_requirements_manifest_file(file_path)
|
|
707
790
|
is_pyproject = name == "pyproject.toml"
|
|
708
791
|
is_setup_py = name == "setup.py"
|
|
709
792
|
is_setup_cfg = name == "setup.cfg"
|
|
@@ -741,7 +824,7 @@ def extract_new_manifest_dependencies(
|
|
|
741
824
|
# Check section headers
|
|
742
825
|
if raw_line.startswith("[") and raw_line.endswith("]"):
|
|
743
826
|
header = raw_line.strip("[]").strip().lower()
|
|
744
|
-
if "dependencies" in header or header.endswith(".dependencies"):
|
|
827
|
+
if "dependencies" in header or header.endswith(".dependencies") or "optional-dependencies" in header:
|
|
745
828
|
in_dependency_section = True
|
|
746
829
|
else:
|
|
747
830
|
in_dependency_section = False
|
|
@@ -765,7 +848,7 @@ def extract_new_manifest_dependencies(
|
|
|
765
848
|
if m:
|
|
766
849
|
pkg_name = m.group(1).strip()
|
|
767
850
|
if pkg_name.lower() not in ("dependencies", "install_requires", "requires", "version", "name"):
|
|
768
|
-
if
|
|
851
|
+
if not is_stdlib_or_internal(pkg_name) and pkg_name not in local_modules:
|
|
769
852
|
extracted.append(
|
|
770
853
|
ExtractedDependency(
|
|
771
854
|
name=normalize_package_name(pkg_name),
|
|
@@ -791,7 +874,7 @@ def extract_new_manifest_dependencies(
|
|
|
791
874
|
if m:
|
|
792
875
|
pkg_name = m.group(1).strip()
|
|
793
876
|
if pkg_name.lower() not in ("install_requires", "extras_require", "setup", "name", "version"):
|
|
794
|
-
if
|
|
877
|
+
if not is_stdlib_or_internal(pkg_name) and pkg_name not in local_modules:
|
|
795
878
|
extracted.append(
|
|
796
879
|
ExtractedDependency(
|
|
797
880
|
name=normalize_package_name(pkg_name),
|
|
@@ -885,13 +968,22 @@ def query_pypi_registry(
|
|
|
885
968
|
import urllib.request
|
|
886
969
|
|
|
887
970
|
norm_name = package_name.strip()
|
|
971
|
+
if is_stdlib_or_internal(norm_name):
|
|
972
|
+
return ("OK", None, None, f"Standard library / internal module ({norm_name})")
|
|
973
|
+
|
|
974
|
+
if norm_name.lower() in KNOWN_NAMESPACE_ROOTS:
|
|
975
|
+
return ("OK", None, None, f"Namespace package umbrella root ('{norm_name}') - subpackages published separately on PyPI")
|
|
976
|
+
|
|
888
977
|
if cache is not None and norm_name in cache:
|
|
889
978
|
return cache[norm_name]
|
|
890
979
|
|
|
891
|
-
|
|
980
|
+
# Map import name to PyPI distribution name if known
|
|
981
|
+
canonical_pkg = normalize_package_name(norm_name)
|
|
982
|
+
|
|
983
|
+
url = f"https://pypi.org/pypi/{canonical_pkg}/json"
|
|
892
984
|
req = urllib.request.Request(
|
|
893
985
|
url,
|
|
894
|
-
headers={"User-Agent": "DeployProof/0.1.
|
|
986
|
+
headers={"User-Agent": "DeployProof/0.1.8 (https://github.com/SVSPraveen/DeployProof)"},
|
|
895
987
|
)
|
|
896
988
|
|
|
897
989
|
try:
|
|
@@ -913,8 +1005,10 @@ def query_pypi_registry(
|
|
|
913
1005
|
pass
|
|
914
1006
|
|
|
915
1007
|
if not timestamps:
|
|
916
|
-
|
|
917
|
-
|
|
1008
|
+
if canonical_pkg.lower() in KNOWN_NAMESPACE_ROOTS or norm_name.lower() in KNOWN_NAMESPACE_ROOTS:
|
|
1009
|
+
result = ("OK", None, None, f"Namespace package umbrella root ('{canonical_pkg}') - subpackages published separately on PyPI")
|
|
1010
|
+
else:
|
|
1011
|
+
result = ("MEDIUM_RISK", 0, None, "Package exists on PyPI but has 0 uploaded releases")
|
|
918
1012
|
else:
|
|
919
1013
|
earliest_dt = min(timestamps)
|
|
920
1014
|
now = datetime.datetime.now(datetime.timezone.utc)
|
|
@@ -941,12 +1035,15 @@ def query_pypi_registry(
|
|
|
941
1035
|
|
|
942
1036
|
except urllib.error.HTTPError as e:
|
|
943
1037
|
if e.code == 404:
|
|
944
|
-
|
|
945
|
-
"
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1038
|
+
if canonical_pkg.lower() in KNOWN_NAMESPACE_ROOTS or norm_name.lower() in KNOWN_NAMESPACE_ROOTS:
|
|
1039
|
+
result = ("OK", None, None, f"Namespace package umbrella root ('{norm_name}') - subpackages published separately on PyPI")
|
|
1040
|
+
else:
|
|
1041
|
+
result = (
|
|
1042
|
+
"HIGH_RISK",
|
|
1043
|
+
None,
|
|
1044
|
+
None,
|
|
1045
|
+
"Package does NOT exist on PyPI (HTTP 404) - hallucinated package name / slopsquatting vulnerability",
|
|
1046
|
+
)
|
|
950
1047
|
else:
|
|
951
1048
|
result = ("UNKNOWN", None, None, f"PyPI HTTP Error {e.code}: {e.reason}")
|
|
952
1049
|
except (urllib.error.URLError, TimeoutError, OSError) as e:
|