ghostpkg 0.19.2__tar.gz → 0.21.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.
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/.gitignore +3 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/CHANGELOG.md +120 -1
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/PKG-INFO +25 -12
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/README.en.md +24 -11
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/ghostpkg/__init__.py +1 -1
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/ghostpkg/cli.py +28 -2
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/ghostpkg/jslocks.py +187 -156
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/ghostpkg/manifests.py +793 -684
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/ghostpkg/prose.py +210 -204
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/ghostpkg/registries.py +63 -3
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/ghostpkg/scanner.py +20 -2
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/pyproject.toml +1 -1
- ghostpkg-0.21.0/scripts/fieldtest.py +151 -0
- ghostpkg-0.21.0/tests/test_field_findings.py +264 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/tests/test_policy.py +57 -0
- ghostpkg-0.21.0/tests/test_review_findings.py +163 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/.github/ISSUE_TEMPLATE/false-positive.yml +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/.github/ISSUE_TEMPLATE/missed-package.yml +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/.github/ghostpkg-ignore.json +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/.github/workflows/ci.yml +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/.github/workflows/publish.yml +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/.pre-commit-hooks.yaml +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/2.0 +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/CONTRIBUTING.md +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/LICENSE +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/README.md +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/SECURITY.md +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/action.yml +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/assets/banner.html +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/assets/banner.png +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/assets/demo.gif +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/assets/make_demo.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/ghostpkg/__main__.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/ghostpkg/assess.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/ghostpkg/cache.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/ghostpkg/data.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/ghostpkg/discover.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/ghostpkg/inspection.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/ghostpkg/policy.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/ghostpkg/report.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/ghostpkg/rules.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/tests/test_action.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/tests/test_assess.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/tests/test_cache.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/tests/test_discover.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/tests/test_hostile_input.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/tests/test_inspection.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/tests/test_jslocks.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/tests/test_lockfiles.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/tests/test_manifests.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/tests/test_prose.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/tests/test_registries.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/tests/test_regressions.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/tests/test_report.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/tests/test_stated_sources.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/tests/test_versions.py +0 -0
- {ghostpkg-0.19.2 → ghostpkg-0.21.0}/tests/test_withdrawn.py +0 -0
|
@@ -6,6 +6,122 @@ this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.21.0] - 2026-09-04
|
|
10
|
+
|
|
11
|
+
Three more false blocks, all found by a new release gate that scans large real
|
|
12
|
+
repositories instead of synthetic manifests. On `home-assistant/core` alone the
|
|
13
|
+
count went **39 -> 0**.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- **`scripts/fieldtest.py`, a release gate.** It clones five repositories chosen
|
|
17
|
+
for the dependency shapes they contain, scans each, and fails on any block
|
|
18
|
+
not listed with a reason. Every name in those projects is something thousands
|
|
19
|
+
of people install daily, so **every block is false until shown otherwise**.
|
|
20
|
+
Current state: **4,461 packages checked, zero blocks.**
|
|
21
|
+
|
|
22
|
+
This exists because 605 unit tests and a 35-check acceptance pass had missed
|
|
23
|
+
eleven defects that one pass over real repositories found immediately.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- **A constraints file forbids; it does not install.** `pip` never installs
|
|
27
|
+
from one -- it only bounds a version if the package arrives some other way --
|
|
28
|
+
so the standard way to forbid a package outright is to pin it to a version
|
|
29
|
+
that cannot exist. Home Assistant does this for eight of them
|
|
30
|
+
(`pycrypto==1000000000.0.0`), and checking those pins turned deliberate
|
|
31
|
+
exclusions into **35 reported blocks**. The flag now survives nested `-c` and
|
|
32
|
+
`-r` includes. The name in a constraints file is still checked.
|
|
33
|
+
- **PEP 440 pads the release segment with zeros.** `0.8` and `0.8.0` are one
|
|
34
|
+
version, as are `1.6.6` and `1.6.6.0`. Both spellings sit in Home Assistant
|
|
35
|
+
requirements against packages that store the other form, and comparing the
|
|
36
|
+
raw text blocked `libsoundtouch` and `baidu-aip`. Worse, the test suite had
|
|
37
|
+
asserted the *opposite* -- that `1.0` and `1.0.0` differ -- so the wrong
|
|
38
|
+
behaviour was locked in by a test. Corrected, and the padding is now
|
|
39
|
+
asserted.
|
|
40
|
+
- **A block no longer rests on a cached version list.** An established package
|
|
41
|
+
is cached for a day, so a release published inside that window is invisible;
|
|
42
|
+
`opower==0.21.0` exists and was blocked from a stale list. The version list
|
|
43
|
+
is now re-fetched before any version block, which is the same rule that
|
|
44
|
+
already governs negative answers: the answer that blocks has to be fresh.
|
|
45
|
+
|
|
46
|
+
614 tests.
|
|
47
|
+
|
|
48
|
+
## [0.20.0] - 2026-09-03
|
|
49
|
+
|
|
50
|
+
Four defects found by scanning **10,109 packages** across `vercel/next.js`,
|
|
51
|
+
`home-assistant/core` and `getsentry/sentry`. Those three repositories produced
|
|
52
|
+
53 blocks and **every one of them was false**. None of these shapes occur in a
|
|
53
|
+
small synthetic manifest, which is why 571 tests and a 35-check acceptance pass
|
|
54
|
+
had missed all four.
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
- **`ghostpkg check -e npm "@scope/name"` reported success having checked
|
|
58
|
+
nothing.** The names went through the PEP 508 requirements parser, whose
|
|
59
|
+
pattern demands an alphanumeric first character, so every scoped npm name was
|
|
60
|
+
dropped in silence -- the run printed "all 0 packages look fine" and exited
|
|
61
|
+
0. On the command this tool is named for, and for a quarter of the npm
|
|
62
|
+
namespace. A false all-clear is worse than any false positive, and this is
|
|
63
|
+
the most serious defect the project has shipped.
|
|
64
|
+
- **A pinned version was compared as text rather than as a version.**
|
|
65
|
+
`aiopurpleair==2025.08.1` was blocked as non-existent while `pip download`
|
|
66
|
+
installed it happily: PyPI stores the canonical `2025.8.1`. Comparison now
|
|
67
|
+
normalises both sides -- leading zeros, a leading `v`, `-`/`_` separators and
|
|
68
|
+
the pre-release spellings. Three such pins sit in unmodified Home Assistant
|
|
69
|
+
requirements, and a false block breaks a build.
|
|
70
|
+
- **A package the checkout provides itself is no longer looked up.** All three
|
|
71
|
+
names blocked in a 6,335-package scan of `vercel/next.js` were the
|
|
72
|
+
repository's own packages, `@next/font` among them. A monorepo depends on
|
|
73
|
+
itself, and not always through `workspace:*` -- an exact pin is just as
|
|
74
|
+
common. Names declared by any `package.json` or `pyproject.toml` in the scan
|
|
75
|
+
are now excluded, which is the same rule as `workspace:` stated differently.
|
|
76
|
+
- **`MANIFEST.in` is no longer read as a requirements file.** `.in` is the
|
|
77
|
+
pip-tools convention and also the extension of a packaging directives file.
|
|
78
|
+
Read as requirements it reported `graft` as a package that exists -- there is
|
|
79
|
+
a real project of that name -- and blocked `recursive-exclude`.
|
|
80
|
+
|
|
81
|
+
### Documentation
|
|
82
|
+
- The `--json` example showed the bare array from before 0.14.0. It is a
|
|
83
|
+
`{schema, tool, summary, findings}` envelope, and the documented example now
|
|
84
|
+
matches, including `source` and `line`.
|
|
85
|
+
|
|
86
|
+
605 tests.
|
|
87
|
+
|
|
88
|
+
## [0.19.3] - 2026-09-03
|
|
89
|
+
|
|
90
|
+
Found by reviewing the published 0.19.2 rather than the working tree. Two of
|
|
91
|
+
the four are false blocks, which this project treats as its worst failure, and
|
|
92
|
+
both have the same cause: `jslocks.py` was written after the test file that
|
|
93
|
+
exists to defend the rule "a dependency naming its own source is not the
|
|
94
|
+
registry's business", so `yarn.lock` and `pnpm-lock.yaml` were never covered by
|
|
95
|
+
it.
|
|
96
|
+
|
|
97
|
+
### Fixed
|
|
98
|
+
- **A yarn `owner/repo#ref` dependency was blocked.** GitHub shorthand was
|
|
99
|
+
handled for `package.json` and missed in `yarn.lock`, so
|
|
100
|
+
`internal-lib@acme/internal-lib#v1.2.3` was looked up on npmjs, found absent,
|
|
101
|
+
and reported as a package that does not exist.
|
|
102
|
+
- **A pnpm git or URL dependency produced a nonsense package name.** The
|
|
103
|
+
protocol is only a prefix in lockfile v5; from v6 it follows `name@`, so the
|
|
104
|
+
guard never fired. `github.com/acme/forked/abc123` was read as a package
|
|
105
|
+
called `github.com/acme/forked`, and
|
|
106
|
+
`foo@https://codeload.github.com/...` as one called
|
|
107
|
+
`foo@https://codeload.github.com/acme/foo`. Both were then blocked.
|
|
108
|
+
- **`--timeout` did nothing.** `def _get_json(url, timeout=TIMEOUT)` bound the
|
|
109
|
+
module global once, at import; the CLI assigned `registries.TIMEOUT` and
|
|
110
|
+
nothing ever re-read it, so every request used 15 seconds regardless.
|
|
111
|
+
- **`.windsurfrules` was found and never scanned.** The directory search
|
|
112
|
+
offered it up, the parser refused it, and the CLI ignores an unreadable
|
|
113
|
+
*discovered* file on purpose -- so nothing was printed. There is now a test
|
|
114
|
+
asserting that every file the search returns can actually be parsed.
|
|
115
|
+
|
|
116
|
+
### Added
|
|
117
|
+
- End-to-end tests for suppression. The ignore file was loaded and applied in
|
|
118
|
+
`main()` only, and nothing asserted its effect on a verdict or an exit code:
|
|
119
|
+
the entire policy call could be replaced with `used = []` and all 539 tests
|
|
120
|
+
still passed. Three tests now cover the real path, and they fail against that
|
|
121
|
+
stub.
|
|
122
|
+
|
|
123
|
+
571 tests.
|
|
124
|
+
|
|
9
125
|
## [0.19.2] - 2026-09-02
|
|
10
126
|
|
|
11
127
|
Found by throwing hostile input at the parsers rather than by reading them.
|
|
@@ -733,7 +849,10 @@ First release.
|
|
|
733
849
|
- No corpus of hallucinated package names is shipped, following the decision of
|
|
734
850
|
the USENIX'25 authors not to publish theirs.
|
|
735
851
|
|
|
736
|
-
[Unreleased]: https://github.com/M1rwana12/ghostpkg/compare/v0.
|
|
852
|
+
[Unreleased]: https://github.com/M1rwana12/ghostpkg/compare/v0.21.0...HEAD
|
|
853
|
+
[0.21.0]: https://github.com/M1rwana12/ghostpkg/releases/tag/v0.21.0
|
|
854
|
+
[0.20.0]: https://github.com/M1rwana12/ghostpkg/releases/tag/v0.20.0
|
|
855
|
+
[0.19.3]: https://github.com/M1rwana12/ghostpkg/releases/tag/v0.19.3
|
|
737
856
|
[0.19.2]: https://github.com/M1rwana12/ghostpkg/releases/tag/v0.19.2
|
|
738
857
|
[0.19.1]: https://github.com/M1rwana12/ghostpkg/releases/tag/v0.19.1
|
|
739
858
|
[0.19.0]: https://github.com/M1rwana12/ghostpkg/releases/tag/v0.19.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ghostpkg
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.21.0
|
|
4
4
|
Summary: Catch package names that do not exist before you install them
|
|
5
5
|
Project-URL: Homepage, https://github.com/m1rwana12/ghostpkg
|
|
6
6
|
Project-URL: Issues, https://github.com/m1rwana12/ghostpkg/issues
|
|
@@ -340,19 +340,32 @@ malformed file **stops the run** rather than quietly leaving you unprotected.
|
|
|
340
340
|
### JSON output
|
|
341
341
|
|
|
342
342
|
```console
|
|
343
|
-
$ ghostpkg
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
"
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
343
|
+
$ ghostpkg scan requirements.txt --json
|
|
344
|
+
{
|
|
345
|
+
"schema": 1,
|
|
346
|
+
"tool": { "name": "ghostpkg", "version": "0.20.0" },
|
|
347
|
+
"summary": { "checked": 2, "blocked": 1, "warned": 0, "errored": 0 },
|
|
348
|
+
"findings": [
|
|
349
|
+
{
|
|
350
|
+
"name": "somepkgthatisnotreal9911",
|
|
351
|
+
"ecosystem": "pypi",
|
|
352
|
+
"verdict": "BLOCK",
|
|
353
|
+
"source": "requirements.txt",
|
|
354
|
+
"line": 2,
|
|
355
|
+
"exists": false,
|
|
356
|
+
"latest_version": null,
|
|
357
|
+
"reasons": [
|
|
358
|
+
{ "rule": "GP001", "text": "does not exist on pypi" }
|
|
359
|
+
]
|
|
360
|
+
}
|
|
361
|
+
]
|
|
362
|
+
}
|
|
354
363
|
```
|
|
355
364
|
|
|
365
|
+
`schema` is there so a consumer can tell what it is reading; `source` and
|
|
366
|
+
`line` say where the name was written. `--format github` emits workflow
|
|
367
|
+
commands instead, and `--format text` is the default.
|
|
368
|
+
|
|
356
369
|
---
|
|
357
370
|
|
|
358
371
|
## Every signal it checks
|
|
@@ -292,19 +292,32 @@ malformed file **stops the run** rather than quietly leaving you unprotected.
|
|
|
292
292
|
### JSON output
|
|
293
293
|
|
|
294
294
|
```console
|
|
295
|
-
$ ghostpkg
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
"
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
295
|
+
$ ghostpkg scan requirements.txt --json
|
|
296
|
+
{
|
|
297
|
+
"schema": 1,
|
|
298
|
+
"tool": { "name": "ghostpkg", "version": "0.20.0" },
|
|
299
|
+
"summary": { "checked": 2, "blocked": 1, "warned": 0, "errored": 0 },
|
|
300
|
+
"findings": [
|
|
301
|
+
{
|
|
302
|
+
"name": "somepkgthatisnotreal9911",
|
|
303
|
+
"ecosystem": "pypi",
|
|
304
|
+
"verdict": "BLOCK",
|
|
305
|
+
"source": "requirements.txt",
|
|
306
|
+
"line": 2,
|
|
307
|
+
"exists": false,
|
|
308
|
+
"latest_version": null,
|
|
309
|
+
"reasons": [
|
|
310
|
+
{ "rule": "GP001", "text": "does not exist on pypi" }
|
|
311
|
+
]
|
|
312
|
+
}
|
|
313
|
+
]
|
|
314
|
+
}
|
|
306
315
|
```
|
|
307
316
|
|
|
317
|
+
`schema` is there so a consumer can tell what it is reading; `source` and
|
|
318
|
+
`line` say where the name was written. `--format github` emits workflow
|
|
319
|
+
commands instead, and `--format text` is the default.
|
|
320
|
+
|
|
308
321
|
---
|
|
309
322
|
|
|
310
323
|
## Every signal it checks
|
|
@@ -15,7 +15,13 @@ from . import __version__, registries
|
|
|
15
15
|
from .assess import Finding
|
|
16
16
|
from .cache import Cache
|
|
17
17
|
from .discover import discover
|
|
18
|
-
from .manifests import
|
|
18
|
+
from .manifests import (
|
|
19
|
+
UnsupportedManifest,
|
|
20
|
+
declared_name,
|
|
21
|
+
load_manifest,
|
|
22
|
+
parse_npm_names,
|
|
23
|
+
parse_requirements,
|
|
24
|
+
)
|
|
19
25
|
from .policy import PolicyError, apply as apply_policy, load as load_policy
|
|
20
26
|
from .report import Palette, as_github, as_json, render, summarise, use_colour
|
|
21
27
|
from .scanner import DEFAULT_WORKERS, evaluate
|
|
@@ -134,6 +140,16 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
134
140
|
print(f"ghostpkg: no manifests found in {where}", file=sys.stderr)
|
|
135
141
|
return EXIT_NOTHING_SCANNED
|
|
136
142
|
|
|
143
|
+
# Names the checkout provides itself. A monorepo depends on its own
|
|
144
|
+
# packages, and not always through `workspace:*` -- an exact pin is
|
|
145
|
+
# just as common, and looking those up on the public registry
|
|
146
|
+
# blocked every one of them.
|
|
147
|
+
local: set[str] = set()
|
|
148
|
+
for path in paths:
|
|
149
|
+
own = declared_name(path)
|
|
150
|
+
if own:
|
|
151
|
+
local.add(own.lower())
|
|
152
|
+
|
|
137
153
|
for path in paths:
|
|
138
154
|
try:
|
|
139
155
|
found, ecosystem = load_manifest(path)
|
|
@@ -151,6 +167,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
151
167
|
return EXIT_ERROR
|
|
152
168
|
continue
|
|
153
169
|
for requirement in found:
|
|
170
|
+
if requirement.name.lower() in local:
|
|
171
|
+
continue # provided by this checkout
|
|
154
172
|
by_ecosystem.setdefault(
|
|
155
173
|
requirement.ecosystem or ecosystem, []
|
|
156
174
|
).append(requirement)
|
|
@@ -162,7 +180,15 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
162
180
|
return EXIT_NOTHING_SCANNED
|
|
163
181
|
else:
|
|
164
182
|
# Accept a pin on the command line too: `ghostpkg check requests==2.31.0`.
|
|
165
|
-
|
|
183
|
+
# npm names go through their own reader, because a scoped name is not
|
|
184
|
+
# expressible in PEP 508 and the requirements parser dropped every one.
|
|
185
|
+
if args.ecosystem == "npm":
|
|
186
|
+
by_ecosystem[args.ecosystem] = parse_npm_names(args.names)
|
|
187
|
+
else:
|
|
188
|
+
by_ecosystem[args.ecosystem] = parse_requirements("\n".join(args.names))
|
|
189
|
+
if not by_ecosystem[args.ecosystem]:
|
|
190
|
+
print(f"ghostpkg: no package names in {' '.join(args.names)}", file=sys.stderr)
|
|
191
|
+
return EXIT_NOTHING_SCANNED
|
|
166
192
|
|
|
167
193
|
try:
|
|
168
194
|
policy, policy_path = load_policy(args.config)
|