safe-pip-scanner 1.4.0__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 (51) hide show
  1. safe_pip_scanner-1.4.0/CHANGELOG.md +308 -0
  2. safe_pip_scanner-1.4.0/CONTRIBUTING.md +93 -0
  3. safe_pip_scanner-1.4.0/LICENSE +21 -0
  4. safe_pip_scanner-1.4.0/MANIFEST.in +7 -0
  5. safe_pip_scanner-1.4.0/PKG-INFO +446 -0
  6. safe_pip_scanner-1.4.0/README.md +405 -0
  7. safe_pip_scanner-1.4.0/pyproject.toml +77 -0
  8. safe_pip_scanner-1.4.0/safe_pip/__init__.py +45 -0
  9. safe_pip_scanner-1.4.0/safe_pip/__main__.py +13 -0
  10. safe_pip_scanner-1.4.0/safe_pip/cli.py +2289 -0
  11. safe_pip_scanner-1.4.0/safe_pip/code_scanner.py +413 -0
  12. safe_pip_scanner-1.4.0/safe_pip/dashboard.py +548 -0
  13. safe_pip_scanner-1.4.0/safe_pip/db.py +335 -0
  14. safe_pip_scanner-1.4.0/safe_pip/display.py +466 -0
  15. safe_pip_scanner-1.4.0/safe_pip/local_scorer.py +614 -0
  16. safe_pip_scanner-1.4.0/safe_pip/osv.py +245 -0
  17. safe_pip_scanner-1.4.0/safe_pip/policy.py +302 -0
  18. safe_pip_scanner-1.4.0/safe_pip/py.typed +0 -0
  19. safe_pip_scanner-1.4.0/safe_pip/pypistats.py +118 -0
  20. safe_pip_scanner-1.4.0/safe_pip/release_analyzer.py +242 -0
  21. safe_pip_scanner-1.4.0/safe_pip/sarif.py +213 -0
  22. safe_pip_scanner-1.4.0/safe_pip/sbom.py +144 -0
  23. safe_pip_scanner-1.4.0/safe_pip/scan_cache.py +147 -0
  24. safe_pip_scanner-1.4.0/safe_pip/scanner.py +879 -0
  25. safe_pip_scanner-1.4.0/safe_pip/threat_feed.py +298 -0
  26. safe_pip_scanner-1.4.0/safe_pip/typosquat.py +569 -0
  27. safe_pip_scanner-1.4.0/safe_pip/watch.py +893 -0
  28. safe_pip_scanner-1.4.0/safe_pip_scanner.egg-info/PKG-INFO +446 -0
  29. safe_pip_scanner-1.4.0/safe_pip_scanner.egg-info/SOURCES.txt +49 -0
  30. safe_pip_scanner-1.4.0/safe_pip_scanner.egg-info/dependency_links.txt +1 -0
  31. safe_pip_scanner-1.4.0/safe_pip_scanner.egg-info/entry_points.txt +2 -0
  32. safe_pip_scanner-1.4.0/safe_pip_scanner.egg-info/requires.txt +11 -0
  33. safe_pip_scanner-1.4.0/safe_pip_scanner.egg-info/top_level.txt +1 -0
  34. safe_pip_scanner-1.4.0/setup.cfg +4 -0
  35. safe_pip_scanner-1.4.0/tests/test_advanced.py +360 -0
  36. safe_pip_scanner-1.4.0/tests/test_build.py +173 -0
  37. safe_pip_scanner-1.4.0/tests/test_cli.py +517 -0
  38. safe_pip_scanner-1.4.0/tests/test_code_scanner.py +306 -0
  39. safe_pip_scanner-1.4.0/tests/test_coverage_gaps.py +1061 -0
  40. safe_pip_scanner-1.4.0/tests/test_dashboard.py +269 -0
  41. safe_pip_scanner-1.4.0/tests/test_db.py +241 -0
  42. safe_pip_scanner-1.4.0/tests/test_display.py +199 -0
  43. safe_pip_scanner-1.4.0/tests/test_integration.py +293 -0
  44. safe_pip_scanner-1.4.0/tests/test_osv.py +303 -0
  45. safe_pip_scanner-1.4.0/tests/test_policy.py +288 -0
  46. safe_pip_scanner-1.4.0/tests/test_pypistats.py +178 -0
  47. safe_pip_scanner-1.4.0/tests/test_sarif.py +254 -0
  48. safe_pip_scanner-1.4.0/tests/test_scanner.py +573 -0
  49. safe_pip_scanner-1.4.0/tests/test_threat_feed.py +206 -0
  50. safe_pip_scanner-1.4.0/tests/test_typosquat.py +263 -0
  51. safe_pip_scanner-1.4.0/tests/test_watch.py +616 -0
@@ -0,0 +1,308 @@
1
+ # Changelog
2
+
3
+ All notable changes to safe-pip are documented here.
4
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
+
6
+ ---
7
+
8
+ ## [1.4.0] — 2026-06-14
9
+
10
+ ### 🎯 Scan Accuracy Improvements
11
+
12
+ #### Self-referential Info findings suppressed
13
+ - Scanning `requests` showed `● Info Network call in __init__.py: requests.get(` — flagging
14
+ the requests package for using `requests.get()` internally is a false positive.
15
+ - **Fix** (`code_scanner.py`): Info findings whose text references the scanned package's
16
+ own name followed by a dot are filtered out. e.g. "requests.get" while scanning "requests".
17
+
18
+ #### `burst releases` tag no longer shown for trusted packages
19
+ - Well-known packages (numpy, requests, django, boto3) have many release bursts by nature.
20
+ Showing `[red]burst releases[/red]` for them was noise that undermined trust in the signal.
21
+ - **Fix** (`display.py`): burst and dormancy-break tags are suppressed when `score=0` and
22
+ `decision=INSTALL` — i.e. packages the scorer has fully cleared. The signals remain in
23
+ the underlying data and still fire for suspicious packages.
24
+
25
+ #### Author field fixed for modern PyPI metadata
26
+ - All packages showed `author: unknown` because PyPI's v2 API moved the author to
27
+ `author_email` as `"Display Name <email>"` format, and `maintainers[]` list.
28
+ - First fix extracted the wrong part (`me` from `Kenneth Reitz <me@kennethreitz.org>`).
29
+ - **Final fix** (`scanner.py`): takes the display name *before* `<`, requires `<` to be
30
+ present (ignores bare email addresses), then falls back to `maintainers[0].name`.
31
+ - Result: `requests` now shows `author: Kenneth Reitz` instead of `author: me`.
32
+
33
+ ### 🛡 Threat Intelligence Expansion
34
+
35
+ #### Threat feed: 66 → 143 entries (+77)
36
+ New attack families: boto3/AWS (credential theft), SQLAlchemy, click/rich/typer,
37
+ pydantic/fastapi, paramiko/fabric (infrastructure), pytest/CI tools (supply chain),
38
+ pyyaml, pillow/opencv, discord bots, and confirmed malicious packages from documented
39
+ PyPI incidents (xmrig, ascii2text, discordspy, ultrarequests, pptest, dpp-discord).
40
+
41
+ #### KNOWN_DANGEROUS: 5 → 15 entries (+10)
42
+ Added: `ecdsa` (WARN — timing attacks), `rsa` (WARN — CVE-2020-25658), `colourama`,
43
+ `python3-dateutil`, `ascii2text`, `ultrarequests`, `xmrig`, `discordspy`, `pptest`.
44
+
45
+ #### Scoring fix: KNOWN_DANGEROUS floor applied after reputation bonuses
46
+ - `ecdsa` and `rsa` were scoring 26 (INSTALL) because a long-standing-reputation bonus
47
+ (−5) was subtracting from the floor (31), pulling them below the WARN threshold.
48
+ - **Fix**: floor is now re-applied after all bonuses, guaranteeing `score ≥ 31` → WARN.
49
+
50
+ ### ⚙ Infrastructure
51
+
52
+ #### Claude model updated to `claude-sonnet-4-6`
53
+ - Was pinned to `claude-sonnet-4-5`. Updated to current release.
54
+
55
+ #### AI prompt: false-positive avoidance rules added
56
+ - Added explicit instructions to the AI scoring prompt:
57
+ - Skip self-referential findings (e.g. `requests.get` inside `requests`)
58
+ - Don't penalise HTTP client libraries for making HTTP calls
59
+ - Don't penalise deployment tools for subprocess usage
60
+ - `import winreg` in cross-platform packages = INFO only, not a risk signal
61
+
62
+ ---
63
+
64
+
65
+ ## [1.3.0] — 2026-06-13
66
+
67
+ ### 🐛 Bug Fixes
68
+
69
+ #### Removed `requests` as a runtime dependency — critical fix
70
+ - `safe-pip` exited immediately on startup if `requests` was not installed, even
71
+ though `scanner.py` already used stdlib `urllib` for all HTTP calls.
72
+ - **Root cause**: `osv.py`, `pypistats.py`, and `code_scanner.py` all imported
73
+ `requests` directly, and `__init__.py` listed it as a hard required dep.
74
+ - **Fix**: all three modules converted to `urllib.request` / `urllib.error`.
75
+ `requests` is no longer imported anywhere in the package.
76
+ - **Fix**: removed `"requests"` from `_REQUIRED` in `__init__.py`.
77
+ - **Fix**: `doctor` command now lists `requests` as *optional*.
78
+ - After this fix the following all work with `requests` uninstalled:
79
+ ```
80
+ safe-pip scan requests
81
+ safe-pip scan flask --deps
82
+ safe-pip update-db
83
+ python -m safe_pip scan flask
84
+ safe-pip watch status
85
+ ```
86
+
87
+ #### Decision reason no longer shows Info-level findings
88
+ - `Decision: ✓ INSTALL Network call in __init__.py: requests.get(` was appearing
89
+ for the `requests` package — an internal diagnostic surfacing as the headline verdict.
90
+ - **Fix**: `local_scorer.py` now excludes `category="Info"` findings when selecting
91
+ the decision reason. Only real risk signals (Reputation, CVE, Code, Maintenance)
92
+ are eligible. Info findings still appear in the full findings list.
93
+ - After fix: `Decision: ✓ INSTALL 'requests' is a well-known, widely-trusted PyPI package`
94
+
95
+ #### Cache hits now display `(cached)` instead of replaying the original scan time
96
+ - Repeated scans showed identical times (e.g. `6.64s` three times), making it appear
97
+ the cache wasn't working. The cache **was** working — the stored `elapsed` from the
98
+ first scan was being redisplayed on every hit.
99
+ - **Fix** (`scanner.py`): cache hits return a shallow copy with `elapsed` set to the
100
+ actual lookup time and `from_cache: true` added.
101
+ - **Fix** (`display.py`): when `from_cache` is true, the header shows `(cached)`;
102
+ live scans show `X.XXs` (minimum `0.01s`).
103
+ - Result:
104
+ ```
105
+ Scan 1: requests v2.34.2 score 0/100 LOW ✓ INSTALL 6.64s
106
+ Scan 2: requests v2.34.2 score 0/100 LOW ✓ INSTALL (cached)
107
+ Scan 3: requests v2.34.2 score 0/100 LOW ✓ INSTALL (cached)
108
+ ```
109
+
110
+ #### `--no-cache` flag now forwarded correctly from `scan` command
111
+ - `safe-pip scan requests --no-cache` was silently ignored — parsed but never passed
112
+ to `_run_scan`. Fixed: `_run_scan(pkg, cfg, scan_output, no_cache=no_cache)`.
113
+
114
+ #### Cache write errors now logged at WARNING level
115
+ - `set_cached()` swallowed all write errors silently (`log.debug`), masking failures.
116
+ - Changed to `log.warning` so errors surface under `--debug`.
117
+ - `json.dumps()` now called before opening the file to avoid corrupt partial writes.
118
+
119
+ #### Removed duplicate `scan_cache` import inside `scan()`
120
+ - `scanner.py` imported `cache_key, get_cached, set_cached` at module level and again
121
+ inside `scan()`. The redundant inner import was removed.
122
+
123
+ ### ✨ New
124
+
125
+ #### `safe-pip cache debug <package>` — cache self-diagnostic
126
+ - Diagnose cache state for any package:
127
+ ```
128
+ safe-pip cache debug requests
129
+ ```
130
+ Shows: cache directory, key, file path, file age, TTL validity, and whether
131
+ `get_cached()` returns a hit. If the file exists but parsing fails, reports
132
+ the exact JSON error.
133
+
134
+ ---
135
+
136
+ ## [1.1.0] — 2026-06-02
137
+
138
+ ### 🐛 False Positive Fixes
139
+
140
+ #### Dormancy break — no longer a CODE WARNING for established packages
141
+ - Packages with ≥10 releases before a dormancy gap (e.g. `numpy`) now generate
142
+ an **INFO** finding instead of a `medium` warning. Established packages that
143
+ go quiet for a year then revive are almost never malicious.
144
+ - New message: *"likely legitimate revival of established package"* vs the old
145
+ generic *"possible maintainer takeover"* which was triggering for `numpy` and
146
+ other long-lived libraries.
147
+
148
+ #### Hardcoded IP — OID/ASN.1 version strings no longer flagged
149
+ - Improved IP-detection regex: first octet must now be ≥ 20 to avoid matching
150
+ X.509 OID strings like `2.5.29.9` found in `cryptography` and similar TLS libs.
151
+ - Added context-aware downgrade: if the surrounding line contains ASN/OID/X.509
152
+ context keywords, or if the second octet is ≤ 9 (typical OID), the finding is
153
+ downgraded to **INFO** — shown but never scored.
154
+ - Eliminates false positives: `beautifulsoup4` `dammit.py: 13.2.3.1` and
155
+ `cryptography` `_oid.py: 2.5.29.9` now produce at most an INFO note.
156
+
157
+ ### ✨ New Features
158
+
159
+ #### "Did you mean?" for typosquats
160
+ - When a package is detected as a likely typosquat, the scan output now shows:
161
+ `Did you mean: requests (run: pip install requests)`
162
+ - Available in both Rich terminal and JSON output (`did_you_mean` field).
163
+
164
+ #### Suggested replacement for known deprecated/unsafe packages
165
+ - `pycrypto` → `pycryptodome`, `nose` → `pytest`, `mock` → `unittest.mock`, and
166
+ 10 other known-deprecated packages now display:
167
+ `Suggested replacement: pycryptodome (run: pip install pycryptodome)`
168
+ - Available in both Rich terminal and JSON output (`suggested_replacement` field).
169
+
170
+ #### JSON output (`--output json` / `safe-pip scan --output json`)
171
+ - JSON output now includes two new fields:
172
+ - `"did_you_mean"`: canonical name if typosquat detected, else `null`
173
+ - `"suggested_replacement"`: replacement package name if known, else `null`
174
+
175
+ #### `--fail-on-high` for CI/CD pipelines (`scan-file`)
176
+ - `safe-pip scan-file requirements.txt --fail-on-high`
177
+ - **Exit 0** = all packages are LOW or MEDIUM risk (pass)
178
+ - **Exit 1** = at least one package is HIGH risk (BLOCK)
179
+ - Complements the existing `--fail-on-warn` (exit 1 on any WARN or BLOCK).
180
+ - Documented in README CI/CD section with GitHub Actions example.
181
+
182
+ #### HTML report (`--html report.html`)
183
+ - `safe-pip scan-file requirements.txt --html report.html`
184
+ - Generates a self-contained dark-theme HTML report with:
185
+ - Summary cards (safe / warned / blocked / total)
186
+ - Horizontal bar chart of risk distribution
187
+ - Full results table with score, verdict, CVE count, top finding, decision
188
+ - "Did you mean?" and "Suggested replacement" inline in the table
189
+ - No external dependencies — fully self-contained single file.
190
+
191
+ #### `--fail-on-high` wired into `batch` command too
192
+ - `batch` now accepts `--fail-on-high` independently of `--fail-on-warn`.
193
+ - Previously `scan-file --fail-on-high` incorrectly conflated both flags; now:
194
+ - `--fail-on-warn` → exit 1 on WARN **or** BLOCK
195
+ - `--fail-on-high` → exit 1 on BLOCK only (WARN = exit 0)
196
+ - Both flags can be combined for custom gate logic.
197
+
198
+ #### `--json` shorthand on `scan`
199
+ - `safe-pip scan requests --json` is now equivalent to `safe-pip scan requests --output json`.
200
+ - Cleaner for shell pipelines: `safe-pip scan requests --json | jq .decision`
201
+
202
+ #### `--deps` — dependency tree analysis
203
+ - `safe-pip scan requests --deps` scans declared dependencies (one level deep).
204
+ - Reads `requires_dist` from PyPI metadata, extracts package names, and runs
205
+ a full scan on each transitive dependency.
206
+ - Rich terminal output shows a tree:
207
+ ```
208
+ Dependencies of requests:
209
+ ├─ urllib3 score 5 ✓ INSTALL
210
+ ├─ certifi score 3 ✓ INSTALL
211
+ └─ idna score 4 ✓ INSTALL
212
+ ```
213
+ - JSON output emits a separate `{"deps_of": "requests", "dependencies": [...]}` object.
214
+ - Returns exit 1 if any dependency is risky (respects `--fail-on-warn`/`--fail-on-high`).
215
+
216
+ ---
217
+
218
+
219
+ - New top-level command that downloads and caches:
220
+ - Typosquat blocklist
221
+ - Malicious package list with CVE mappings
222
+ - Top-8000 PyPI packages feed
223
+ - Progress bar with per-source entry counts.
224
+ - Replaces `safe-pip update` for users who want explicit control over DB freshness.
225
+
226
+ ---
227
+
228
+
229
+
230
+ ### 🎉 First stable release
231
+
232
+ #### Core scanner
233
+ - **No API key required** — full functionality with local rule-based scorer
234
+ - **7-stage pipeline**: PyPI metadata → typosquat → CVE → download stats → release anomaly → static code analysis → risk scoring
235
+ - **PackageNotFoundError** — non-existent clean packages forwarded to real pip automatically
236
+ - **Claude AI upgrade path** — set `ANTHROPIC_API_KEY` for enhanced analysis
237
+
238
+ #### Typosquat detection
239
+ - Levenshtein distance engine against 8,000+ top PyPI packages (live feed, 24h cache)
240
+ - Keyboard-proximity, homoglyph, vowel-swap, and affix-padding checks
241
+ - 70+ curated known-malicious entries (requestss, colourama, langchian, etc.)
242
+ - `safe-pip update` — refresh threat feed on demand
243
+
244
+ #### Static code analysis (NEW)
245
+ - Downloads wheel/sdist **without installing** and scans Python source
246
+ - Detects: encoded payloads, reverse shells, data exfiltration, credential leaks
247
+ - Detects: hardcoded external IPs, dynamic exec/eval, install hook abuse
248
+ - INFO-level findings for legitimate patterns (ctypes, requests.get, private IPs)
249
+
250
+ #### Release anomaly detection (NEW)
251
+ - Burst pattern detection (3+ releases within 24 hours)
252
+ - High velocity scoring (>1 release/day)
253
+ - Dormancy break detection (inactive years → sudden activity = possible takeover)
254
+ - Maintainer reputation scoring
255
+
256
+ #### CVE analysis
257
+ - Real-time OSV.dev lookups — critical/high/medium/low severity
258
+ - Skips OSV for packages not on PyPI (eliminates false positives)
259
+ - Version-aware: only flags CVEs affecting the actual installed version
260
+
261
+ #### SBOM generation (NEW)
262
+ - `safe-pip sbom` — CycloneDX 1.4 JSON output
263
+ - Accepted by GitHub Dependency Review, FOSSA, Snyk, and enterprise tools
264
+ - Includes PURL, license, score, verdict, findings per component
265
+
266
+ #### Watch mode
267
+ - `safe-pip watch enable` — intercepts `pip install` system-wide
268
+ - **PowerShell**: function alias in profile (PS5 + PS7)
269
+ - **CMD**: pip.bat written beside pip.exe
270
+ - **Admin CMD**: UAC elevation to install system-level shim
271
+ - `safe-pip watch disable` — clean removal from all locations
272
+ - `safe-pip watch status` — shows coverage per terminal type
273
+
274
+ #### Dashboard
275
+ - `safe-pip dashboard` — local browser UI at localhost:7676
276
+ - Stats: total scans, verdict breakdown, blocked count, average score
277
+ - Charts: verdict donut, 30-day timeline (install/warn/block)
278
+ - Table: sortable, filterable, searchable scan history
279
+ - Auto-refreshes every 60 seconds
280
+
281
+ #### CLI improvements
282
+ - `safe-pip scan requirements.txt` — auto-detects requirements files
283
+ - `safe-pip scan-file requirements.txt` — explicit shorthand
284
+ - Package aliases: `sklearn→scikit-learn`, `cv2→opencv-python`, `PIL→pillow`, etc.
285
+ - Clean error messages — no tracebacks shown to users
286
+ - `--output rich|plain|json` on all scan commands
287
+ - `--sarif FILE` — SARIF 2.1.0 output for GitHub Security tab
288
+
289
+ #### False positive fixes
290
+ - `requests.get()` → INFO only (not scored)
291
+ - `import ctypes` → INFO only (not scored)
292
+ - `0.0.0.0`, `127.x`, `10.x`, `192.168.x`, RFC1918 → excluded from IP detection
293
+ - `compile(expression, "<string>")` → INFO (Python built-in math eval)
294
+ - `tensorflow-gpu`, `tensorflow-cpu`, `tensorflow-intel` → trusted (official Google)
295
+ - Release anomalies suppressed for trusted packages (numpy, etc.)
296
+
297
+ #### Test suite
298
+ - **605 tests** across 12 test files
299
+ - `tests/safe_packages.txt` — 31 trusted packages verified INSTALL
300
+ - `tests/malicious_packages.txt` — 22 attack packages verified BLOCK
301
+ - `tests/edge_cases.txt` — aliases, deprecated-legit, CVE-warning packages
302
+ - Integration tests cover full pipeline end-to-end
303
+
304
+ ---
305
+
306
+ ## [0.x.x] — Pre-release development
307
+
308
+ Internal development iterations — see git history.
@@ -0,0 +1,93 @@
1
+ # Contributing to safe-pip
2
+
3
+ Thank you for your interest in contributing! This document covers how to set
4
+ up a development environment, run tests, and submit changes.
5
+
6
+ ---
7
+
8
+ ## Development setup
9
+
10
+ ```bash
11
+ # Clone and install in editable mode with dev dependencies
12
+ git clone https://github.com/safe-pip/safe-pip
13
+ cd safe-pip
14
+ pip install -e ".[dev]"
15
+
16
+ # Set your Anthropic API key (required for scanner tests that call Claude)
17
+ export ANTHROPIC_API_KEY=sk-ant-...
18
+ ```
19
+
20
+ ## Running tests
21
+
22
+ ```bash
23
+ # All tests
24
+ pytest
25
+
26
+ # Specific file
27
+ pytest tests/test_cli.py -v
28
+
29
+ # With coverage
30
+ pip install pytest-cov
31
+ pytest --cov=safe_pip --cov-report=term-missing
32
+ ```
33
+
34
+ ## Linting
35
+
36
+ ```bash
37
+ pip install ruff
38
+ ruff check safe_pip/ tests/ --select E,F,W --ignore E501
39
+ ```
40
+
41
+ ## Project structure
42
+
43
+ ```
44
+ safe_pip/
45
+ ├── __init__.py # version
46
+ ├── cli.py # Click CLI — all commands
47
+ ├── scanner.py # Core pipeline: PyPI → typosquat → OSV → AI
48
+ ├── typosquat.py # Levenshtein similarity engine + threat DB
49
+ ├── osv.py # OSV CVE database client
50
+ ├── pypistats.py # pypistats.org download stats client
51
+ ├── policy.py # Rule evaluation engine + config loader
52
+ ├── display.py # Rich terminal UI
53
+ ├── db.py # SQLite persistence layer
54
+ ├── sarif.py # SARIF 2.1.0 report generator
55
+ └── py.typed # PEP 561 marker
56
+
57
+ tests/
58
+ ├── test_build.py # Package metadata + CLI smoke tests
59
+ ├── test_cli.py # CLI command tests (Click test runner)
60
+ └── test_db.py # SQLite layer tests (in-memory DB)
61
+ ```
62
+
63
+ ## Adding a new scanning signal
64
+
65
+ 1. Add your logic to `scanner.py` (fetch data) or `typosquat.py` (similarity)
66
+ 2. Pass the result into `_build_prompt()` so Claude sees it
67
+ 3. Add a corresponding policy `condition` string to `policy.py`
68
+ 4. Add a stage label to `display.STAGE_LABELS`
69
+ 5. Write tests in `tests/test_cli.py` or a new `tests/test_<module>.py`
70
+
71
+ ## Adding a new CLI command
72
+
73
+ 1. Define a new `@main.command()` in `cli.py`
74
+ 2. Add it to the `--help` assertion in `tests/test_build.py::TestCLIIntegration::test_help_flag`
75
+ 3. Document it in `README.md` and `CHANGELOG.md`
76
+
77
+ ## Submitting a pull request
78
+
79
+ 1. Fork the repo and create a feature branch: `git checkout -b feat/my-feature`
80
+ 2. Make your changes and ensure all tests pass: `pytest`
81
+ 3. Ensure lint passes: `ruff check safe_pip/ tests/ --select E,F,W --ignore E501`
82
+ 4. Update `CHANGELOG.md` under `[Unreleased]`
83
+ 5. Open a PR — the CI pipeline will run automatically
84
+
85
+ ## Reporting bugs
86
+
87
+ Use the [bug report template](.github/ISSUE_TEMPLATE/bug_report.md).
88
+ Please include the output of `safe-pip --version` and the full error message.
89
+
90
+ ## Security issues
91
+
92
+ Do **not** open a public issue for security vulnerabilities. Email the
93
+ maintainers directly — contact details are in the GitHub repository.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 safe-pip contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,7 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include CONTRIBUTING.md
5
+ include safe_pip/py.typed
6
+ recursive-include tests *.py
7
+ recursive-include .github *