slopsentry 0.1.0__py3-none-any.whl
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.
- slopsentry/__init__.py +3 -0
- slopsentry/checks/__init__.py +3 -0
- slopsentry/checks/age.py +74 -0
- slopsentry/checks/exists.py +17 -0
- slopsentry/checks/lookalike.py +126 -0
- slopsentry/cli.py +123 -0
- slopsentry/data/__init__.py +1 -0
- slopsentry/data/top_pypi.json +5010 -0
- slopsentry/hook.py +310 -0
- slopsentry/registry.py +62 -0
- slopsentry/scoring.py +81 -0
- slopsentry/spec.py +18 -0
- slopsentry-0.1.0.dist-info/METADATA +235 -0
- slopsentry-0.1.0.dist-info/RECORD +17 -0
- slopsentry-0.1.0.dist-info/WHEEL +4 -0
- slopsentry-0.1.0.dist-info/entry_points.txt +2 -0
- slopsentry-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: slopsentry
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Check packages for slopsquatting before they are installed.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Rajeshr-developer/slopsentry
|
|
6
|
+
Project-URL: Repository, https://github.com/Rajeshr-developer/slopsentry
|
|
7
|
+
Project-URL: Issues, https://github.com/Rajeshr-developer/slopsentry/issues
|
|
8
|
+
Author-email: Rajeshr-developer <rajeshr.developer@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: claude-code,pypi,security,slopsquatting,supply-chain,typosquatting
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Requires-Dist: packaging>=24
|
|
27
|
+
Requires-Dist: rich>=13
|
|
28
|
+
Requires-Dist: typer>=0.12
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# SlopSentry
|
|
35
|
+
|
|
36
|
+
SlopSentry checks packages for **slopsquatting** before you install them: AI-hallucinated
|
|
37
|
+
package names and malicious lookalikes that attackers register to catch them.
|
|
38
|
+
|
|
39
|
+
<!-- TODO: demo GIF of the Claude Code hook blocking a hallucinated package -->
|
|
40
|
+
<!--  -->
|
|
41
|
+
|
|
42
|
+
> **Alpha (0.1.0):** only Python package installs from PyPI are checked. npm and other
|
|
43
|
+
> ecosystems aren't supported yet.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
SlopSentry needs Python 3.10 or newer. Install it as a standalone tool with
|
|
48
|
+
[pipx](https://pipx.pypa.io/):
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
pipx install slopsentry
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
or run it without installing, using [uv](https://docs.astral.sh/uv/):
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
uvx slopsentry check requests
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`pip install slopsentry` works too.
|
|
61
|
+
|
|
62
|
+
For development:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
python3 -m venv .venv
|
|
66
|
+
.venv/bin/pip install -e ".[dev]"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
slopsentry check requests
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`check` runs every check, adds up the risk points, and gives a verdict:
|
|
76
|
+
|
|
77
|
+
| Check | Flags | Points |
|
|
78
|
+
| --------- | ----------------------------------------------------- | ------ |
|
|
79
|
+
| exists | package not found on PyPI | 100 |
|
|
80
|
+
| age | first release under 90 days ago, or no released files | 40 |
|
|
81
|
+
| age | two or fewer releases | 20 |
|
|
82
|
+
| age | no homepage or repository link | 20 |
|
|
83
|
+
| lookalike | resembles a popular package (see below) | 50 |
|
|
84
|
+
| lookalike | resembles one, but the age check passes cleanly | 15 |
|
|
85
|
+
|
|
86
|
+
| Verdict | Total points | Exit code |
|
|
87
|
+
| ------- | ------------ | --------- |
|
|
88
|
+
| PASS | under 30 | 0 |
|
|
89
|
+
| WARN | 30 to 69 | 0 |
|
|
90
|
+
| BLOCK | 70 or more | 1 |
|
|
91
|
+
|
|
92
|
+
If PyPI can't be reached, the package can't be verified: the verdict is at least WARN and
|
|
93
|
+
the exit code 0, or ERROR with exit code 2 under `--strict`. Empty or invalid input is an
|
|
94
|
+
ERROR with exit code 2. Thresholds live in `slopsentry/scoring.py`.
|
|
95
|
+
|
|
96
|
+
Without `--explain`, `check` prints one summary line plus the reasons for a WARN or
|
|
97
|
+
BLOCK. With `--explain` it prints a table of every check's status, detail and points:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
slopsentry check --explain requests
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Lookalike check
|
|
104
|
+
|
|
105
|
+
The lookalike check compares the name with the 5,000 most-downloaded PyPI packages,
|
|
106
|
+
bundled in `slopsentry/data/top_pypi.json`. Popular packages are never flagged, so
|
|
107
|
+
`requests` passes even though it's close to other names. A name is flagged if:
|
|
108
|
+
|
|
109
|
+
- it differs from a popular name only by separators (`django-rest-framework` vs
|
|
110
|
+
`djangorestframework`),
|
|
111
|
+
- it adds or removes `py-`, `python-`, `-py`, `-python` or `-lib` (`python-requests`,
|
|
112
|
+
`pandas-py`), or
|
|
113
|
+
- it's within Damerau-Levenshtein distance 1 of a popular name, or 2 for names longer
|
|
114
|
+
than 10 characters (`reqeusts`, `numpyy`).
|
|
115
|
+
|
|
116
|
+
Two guards cut false positives on legitimate packages:
|
|
117
|
+
|
|
118
|
+
1. **Short names:** names under 5 characters get no edit-distance matching, since almost
|
|
119
|
+
every short name is one edit from another (`sax`/`six`, `od`/`id`).
|
|
120
|
+
2. **Package families:** a distance-2 match is ignored when the names differ in exactly
|
|
121
|
+
one whole `-` segment, such as `mypy-boto3-emr` and `mypy-boto3-ec2`.
|
|
122
|
+
|
|
123
|
+
The reason names the package it resembles, e.g. "looks like requests". If the package is
|
|
124
|
+
established (the age check passes cleanly), the match adds only 15 points and reads
|
|
125
|
+
"looks like requests (established package)": shown in `--explain`, but not enough to warn
|
|
126
|
+
by itself. A new or thin lookalike adds 50, so one that is also brand new blocks.
|
|
127
|
+
|
|
128
|
+
To refresh the bundled list (it needs the network, or a downloaded copy of the dataset):
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
.venv/bin/python scripts/update_top_packages.py
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`PACKAGE` can be a full requirement: `requests==2.31.0` and `requests[socks]` are
|
|
135
|
+
checked as `requests`. Invalid input is rejected before any request is made.
|
|
136
|
+
|
|
137
|
+
Names are normalised per PEP 503, so `Requests` and `typing_extensions` match
|
|
138
|
+
`requests` and `typing-extensions`.
|
|
139
|
+
|
|
140
|
+
Use `--strict` to treat an unreachable registry as a failure:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
slopsentry check --strict requests
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Claude Code hook
|
|
147
|
+
|
|
148
|
+
`slopsentry hook` is a Claude Code `PreToolUse` hook. It reads the hook event on stdin,
|
|
149
|
+
finds install commands in Bash tool calls, scores each package, and answers on stdout:
|
|
150
|
+
|
|
151
|
+
| Worst verdict | Decision |
|
|
152
|
+
| ------------- | ------------------------------------------------ |
|
|
153
|
+
| BLOCK | `deny`, with the package, score and reasons |
|
|
154
|
+
| WARN | `ask`, so you confirm before it runs |
|
|
155
|
+
| PASS | allowed silently |
|
|
156
|
+
|
|
157
|
+
It recognises `pip install` (also `pip3`, `python -m pip`, `python3 -m pip`), `uv add`,
|
|
158
|
+
`uv pip install` and `poetry add`, including in chained commands (`&&`, `||`, `;`, `|`)
|
|
159
|
+
and after `sudo`, `env` or `VAR=value` prefixes. Flags, local paths, URLs, `-e` and `-r`
|
|
160
|
+
arguments are skipped. Commands that use a custom index (`--index-url`, `-i`,
|
|
161
|
+
`--extra-index-url`, `-f`/`--find-links`, uv's `--index`/`--default-index`, poetry's
|
|
162
|
+
`--source`) aren't checked, since their packages may not be on PyPI. In 0.1.0 only
|
|
163
|
+
Python installs are checked; `npm install`, `yarn add` and other ecosystems pass through
|
|
164
|
+
unchecked.
|
|
165
|
+
|
|
166
|
+
The hook fails open: it always exits 0. Commands it can't parse, registry errors (5 second
|
|
167
|
+
timeout) and its own crashes all allow the command. Crashes are appended with a timestamp
|
|
168
|
+
to `~/.slopsentry/hook.log`; add `--debug` to also print them to stderr.
|
|
169
|
+
|
|
170
|
+
### Setting up the hook
|
|
171
|
+
|
|
172
|
+
1. Install SlopSentry so `slopsentry` is on your `PATH`:
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
pipx install slopsentry
|
|
176
|
+
slopsentry --help
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
2. Add the hook to `.claude/settings.json` in a project, or to `~/.claude/settings.json`
|
|
180
|
+
to cover every project:
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"hooks": {
|
|
185
|
+
"PreToolUse": [
|
|
186
|
+
{
|
|
187
|
+
"matcher": "Bash",
|
|
188
|
+
"hooks": [
|
|
189
|
+
{
|
|
190
|
+
"type": "command",
|
|
191
|
+
"command": "slopsentry hook || true",
|
|
192
|
+
"timeout": 30
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`|| true` keeps Claude Code working if `slopsentry` isn't installed on a machine
|
|
202
|
+
where the settings file is shared.
|
|
203
|
+
|
|
204
|
+
3. Restart Claude Code, then check it in `/hooks`. Asking Claude to run
|
|
205
|
+
`pip install reqeusts` should be blocked.
|
|
206
|
+
|
|
207
|
+
If you use uv and don't want to install anything, use `"command": "uvx slopsentry hook || true"`
|
|
208
|
+
instead. The first run downloads SlopSentry, so it's slower.
|
|
209
|
+
|
|
210
|
+
When working on SlopSentry itself, point the hook at your checkout instead:
|
|
211
|
+
`"\"$CLAUDE_PROJECT_DIR\"/.venv/bin/slopsentry hook || true"`.
|
|
212
|
+
|
|
213
|
+
### Known gaps
|
|
214
|
+
|
|
215
|
+
The hook reads the command text; it doesn't run a shell. These installs get past it:
|
|
216
|
+
|
|
217
|
+
- **`bash -c "pip install x"`** (and `sh -c`, `eval`): the install is inside a string
|
|
218
|
+
argument that isn't parsed.
|
|
219
|
+
- **`$(...)` and backtick subshells**, e.g. `pip install $(cat pkgs.txt)`: the package
|
|
220
|
+
names only exist once the shell runs.
|
|
221
|
+
- **`xargs`**, e.g. `cat pkgs.txt | xargs pip install`: the names arrive on stdin.
|
|
222
|
+
- **`-r requirements.txt`**: requirement files aren't read, so their packages aren't
|
|
223
|
+
checked.
|
|
224
|
+
- **`--extra-index-url` dependency confusion**: commands with an extra index are skipped
|
|
225
|
+
entirely. pip picks the highest version across all indexes, so an attacker can publish
|
|
226
|
+
your private package's name on PyPI with a higher version, and it wins.
|
|
227
|
+
|
|
228
|
+
## Data attribution
|
|
229
|
+
|
|
230
|
+
`slopsentry/data/top_pypi.json` is derived from
|
|
231
|
+
[Top PyPI Packages](https://hugovk.dev/top-pypi-packages/) by Hugo van Kemenade and
|
|
232
|
+
contributors ([DOI 10.5281/zenodo.23082845](https://doi.org/10.5281/zenodo.23082845)),
|
|
233
|
+
release 2026.10.1, licensed under
|
|
234
|
+
[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). Changes: kept the top 5,000
|
|
235
|
+
names in download order, normalised them per PEP 503, and removed the download counts.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
slopsentry/__init__.py,sha256=CtawsIAF3GUfWb3VoF9zo_f_XWbX8zhI_EIfptuNBak,98
|
|
2
|
+
slopsentry/cli.py,sha256=0WKdSxu5Ke1oWy1uvtkkniFOGvFRf7ZqCBhRFQnaafE,4145
|
|
3
|
+
slopsentry/hook.py,sha256=mts4T3wSz0UZRT0OHNhOzpP0Sjhn4Du3S3eu5_PvaIU,11671
|
|
4
|
+
slopsentry/registry.py,sha256=WDfqu62KV9TgD15N1tzoOC0cAteF4V8hHcPRhhHc4Kg,2196
|
|
5
|
+
slopsentry/scoring.py,sha256=SYaSzo3jr_eCkAaiKK483eX8s-wnUGQr0YAoYr4OJSY,2220
|
|
6
|
+
slopsentry/spec.py,sha256=4fmdv-yYZb30XVaYQXOYLvweFy-Z-tQx0QOGrCRDMEA,703
|
|
7
|
+
slopsentry/checks/__init__.py,sha256=_0RZrNZFECsKNO6JwP-rVnn8igGtFfJV62ONzDBbX-g,89
|
|
8
|
+
slopsentry/checks/age.py,sha256=s-onWUNtT9C6XBx9PtU7pjKdz486-GCHXesgjBBJXZI,2454
|
|
9
|
+
slopsentry/checks/exists.py,sha256=gIDZB-76Weq_bKwHdjHhDzchR4_KJ5Tn7GXG0INvWtk,493
|
|
10
|
+
slopsentry/checks/lookalike.py,sha256=P9BcLQAdwrMOtOYoiKJMohw64vCHlYclOQrtloZJ7wM,4922
|
|
11
|
+
slopsentry/data/__init__.py,sha256=1ngRWhLp3cYxZ3_p2EYHHimayoCtAoE9sdfywZLBE6o,26
|
|
12
|
+
slopsentry/data/top_pypi.json,sha256=RPEyyXenUSbe3dgS80rnvxwqiufuSKyBqQpxiA2830s,94708
|
|
13
|
+
slopsentry-0.1.0.dist-info/METADATA,sha256=8gFYzFlHLp3x4Z37JuMbFQzY1hhFJrIO35uvI5qDr2I,9269
|
|
14
|
+
slopsentry-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
15
|
+
slopsentry-0.1.0.dist-info/entry_points.txt,sha256=-KcJn4Zi-Ygc9VUhF4-oCL2VUsAhpKIymCz28RvTqNw,50
|
|
16
|
+
slopsentry-0.1.0.dist-info/licenses/LICENSE,sha256=bz1w0UQuFAWPHdtciZCmm1lKvuvXk3O_rCEGqPlWlXs,1074
|
|
17
|
+
slopsentry-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rajeshr-developer
|
|
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.
|