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.
Files changed (37) hide show
  1. {deployproof-0.1.7/src/deployproof.egg-info → deployproof-0.1.8}/PKG-INFO +106 -9
  2. {deployproof-0.1.7 → deployproof-0.1.8}/README.md +105 -8
  3. {deployproof-0.1.7 → deployproof-0.1.8}/pyproject.toml +1 -1
  4. {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/__init__.py +1 -1
  5. {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/cli.py +4 -1
  6. {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/dependencies.py +128 -31
  7. {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/diff.py +5 -1
  8. {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/mutator.py +239 -44
  9. {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/reporter.py +22 -9
  10. {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/secrets.py +55 -15
  11. {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/symlinks.py +156 -156
  12. {deployproof-0.1.7 → deployproof-0.1.8/src/deployproof.egg-info}/PKG-INFO +106 -9
  13. {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof.egg-info/SOURCES.txt +1 -0
  14. deployproof-0.1.8/tests/test_baseline_collection_error.py +120 -0
  15. {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_cli.py +3 -3
  16. {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_control_flow.py +4 -3
  17. {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_dependencies_extractor.py +69 -0
  18. {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_dependencies_pypi.py +8 -0
  19. {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_mutator.py +17 -0
  20. {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_secrets.py +26 -0
  21. {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_stress_regressions.py +10 -3
  22. {deployproof-0.1.7 → deployproof-0.1.8}/LICENSE +0 -0
  23. {deployproof-0.1.7 → deployproof-0.1.8}/setup.cfg +0 -0
  24. {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/control_flow.py +0 -0
  25. {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/mocks.py +0 -0
  26. {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof/wsl.py +0 -0
  27. {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof.egg-info/dependency_links.txt +0 -0
  28. {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof.egg-info/entry_points.txt +0 -0
  29. {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof.egg-info/requires.txt +0 -0
  30. {deployproof-0.1.7 → deployproof-0.1.8}/src/deployproof.egg-info/top_level.txt +0 -0
  31. {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_cli_dependency_scan.py +0 -0
  32. {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_diff.py +0 -0
  33. {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_edge_cases_validation.py +0 -0
  34. {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_mocks.py +0 -0
  35. {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_mutation_suites.py +0 -0
  36. {deployproof-0.1.7 → deployproof-0.1.8}/tests/test_symlinks.py +0 -0
  37. {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.7
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
- [![PyPI version](https://img.shields.io/pypi/v/deployproof.svg?color=007ec6)](https://pypi.org/project/deployproof/)
36
- [![Python versions](https://img.shields.io/pypi/pyversions/deployproof.svg?color=3776ab)](https://pypi.org/project/deployproof/)
37
- [![Tests](https://img.shields.io/badge/tests-79%20passed-2ea44f.svg)](https://github.com/SVSPraveen/DeployProof)
35
+ [![PyPI version](https://img.shields.io/badge/pypi-v0.1.8-007ec6.svg)](https://pypi.org/project/deployproof/)
36
+ [![Python versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-3776ab.svg)](https://pypi.org/project/deployproof/)
37
+ [![CI](https://github.com/SVSPraveen/DeployProof/actions/workflows/ci.yml/badge.svg)](https://github.com/SVSPraveen/DeployProof/actions/workflows/ci.yml)
38
+ [![Tests](https://img.shields.io/badge/tests-88%20passed-2ea44f.svg)](https://github.com/SVSPraveen/DeployProof)
39
+ [![Stress Tests](https://img.shields.io/badge/stress%20tests-11%2F11%20passed-2ea44f.svg)](stress_fixtures/)
38
40
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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. Exit code is non-zero on any finding that should block a push.
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.7",
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 7 planted edge cases:
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, and GhostApproval sandbox-escape traps.
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.7):** Diff-scoped AST mutation testing, secrets scanner (including unquoted .env values), GhostApproval symlink sandbox-escape detector, PyPI dependency hallucination / slopsquatting scanner (with 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, machine-readable `--json` output, and Tier 2 CI verification via GitHub Actions (mutmut).
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
- [![PyPI version](https://img.shields.io/pypi/v/deployproof.svg?color=007ec6)](https://pypi.org/project/deployproof/)
6
- [![Python versions](https://img.shields.io/pypi/pyversions/deployproof.svg?color=3776ab)](https://pypi.org/project/deployproof/)
7
- [![Tests](https://img.shields.io/badge/tests-79%20passed-2ea44f.svg)](https://github.com/SVSPraveen/DeployProof)
5
+ [![PyPI version](https://img.shields.io/badge/pypi-v0.1.8-007ec6.svg)](https://pypi.org/project/deployproof/)
6
+ [![Python versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-3776ab.svg)](https://pypi.org/project/deployproof/)
7
+ [![CI](https://github.com/SVSPraveen/DeployProof/actions/workflows/ci.yml/badge.svg)](https://github.com/SVSPraveen/DeployProof/actions/workflows/ci.yml)
8
+ [![Tests](https://img.shields.io/badge/tests-88%20passed-2ea44f.svg)](https://github.com/SVSPraveen/DeployProof)
9
+ [![Stress Tests](https://img.shields.io/badge/stress%20tests-11%2F11%20passed-2ea44f.svg)](stress_fixtures/)
8
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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. Exit code is non-zero on any finding that should block a push.
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.7",
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 7 planted edge cases:
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, and GhostApproval sandbox-escape traps.
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.7):** Diff-scoped AST mutation testing, secrets scanner (including unquoted .env values), GhostApproval symlink sandbox-escape detector, PyPI dependency hallucination / slopsquatting scanner (with 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, machine-readable `--json` output, and Tier 2 CI verification via GitHub Actions (mutmut).
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"
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.7"
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 all subfolders in the root tree that have __init__.py or py files (up to depth 3)
384
+ # Also scan recursively for package directories
299
385
  try:
300
- for py_file in root.glob("*/*.py"):
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 in STDLIB_MODULES or top_name in local_modules:
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 in STDLIB_MODULES or top_name in local_modules:
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 in STDLIB_MODULES or top_name in local_modules:
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 pkg_name not in STDLIB_MODULES and pkg_name not in local_modules:
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 pkg_name not in STDLIB_MODULES and pkg_name not in local_modules:
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 pkg_name not in STDLIB_MODULES and pkg_name not in local_modules:
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
- url = f"https://pypi.org/pypi/{norm_name}/json"
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.7 (https://github.com/SVSPraveen/DeployProof)"},
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
- # Package exists on PyPI but has no release files
917
- result = ("MEDIUM_RISK", 0, None, "Package exists on PyPI but has 0 uploaded releases")
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
- result = (
945
- "HIGH_RISK",
946
- None,
947
- None,
948
- "Package does NOT exist on PyPI (HTTP 404) - hallucinated package name / slopsquatting vulnerability",
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: