exposuregate 0.1.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.
- exposuregate-0.1.0/LICENSE +21 -0
- exposuregate-0.1.0/PKG-INFO +594 -0
- exposuregate-0.1.0/README.md +557 -0
- exposuregate-0.1.0/pyproject.toml +80 -0
- exposuregate-0.1.0/setup.cfg +4 -0
- exposuregate-0.1.0/src/exposuregate/__init__.py +1 -0
- exposuregate-0.1.0/src/exposuregate/__main__.py +200 -0
- exposuregate-0.1.0/src/exposuregate/archive.py +145 -0
- exposuregate-0.1.0/src/exposuregate/config.py +242 -0
- exposuregate-0.1.0/src/exposuregate/diffing.py +40 -0
- exposuregate-0.1.0/src/exposuregate/freshness.py +58 -0
- exposuregate-0.1.0/src/exposuregate/gitio.py +131 -0
- exposuregate-0.1.0/src/exposuregate/grading.py +211 -0
- exposuregate-0.1.0/src/exposuregate/lockfiles/__init__.py +50 -0
- exposuregate-0.1.0/src/exposuregate/lockfiles/npm.py +109 -0
- exposuregate-0.1.0/src/exposuregate/lockfiles/poetry.py +87 -0
- exposuregate-0.1.0/src/exposuregate/lockfiles/requirements.py +121 -0
- exposuregate-0.1.0/src/exposuregate/lockfiles/uv.py +76 -0
- exposuregate-0.1.0/src/exposuregate/models.py +99 -0
- exposuregate-0.1.0/src/exposuregate/osv.py +152 -0
- exposuregate-0.1.0/src/exposuregate/rebuild.py +165 -0
- exposuregate-0.1.0/src/exposuregate/report.py +177 -0
- exposuregate-0.1.0/src/exposuregate/scan.py +398 -0
- exposuregate-0.1.0/src/exposuregate/store.py +332 -0
- exposuregate-0.1.0/src/exposuregate/update.py +90 -0
- exposuregate-0.1.0/src/exposuregate/versions.py +72 -0
- exposuregate-0.1.0/src/exposuregate.egg-info/PKG-INFO +594 -0
- exposuregate-0.1.0/src/exposuregate.egg-info/SOURCES.txt +47 -0
- exposuregate-0.1.0/src/exposuregate.egg-info/dependency_links.txt +1 -0
- exposuregate-0.1.0/src/exposuregate.egg-info/entry_points.txt +2 -0
- exposuregate-0.1.0/src/exposuregate.egg-info/requires.txt +15 -0
- exposuregate-0.1.0/src/exposuregate.egg-info/top_level.txt +1 -0
- exposuregate-0.1.0/tests/test_archive.py +229 -0
- exposuregate-0.1.0/tests/test_classify.py +69 -0
- exposuregate-0.1.0/tests/test_cli_e2e.py +580 -0
- exposuregate-0.1.0/tests/test_config.py +185 -0
- exposuregate-0.1.0/tests/test_diffing.py +98 -0
- exposuregate-0.1.0/tests/test_freshness.py +132 -0
- exposuregate-0.1.0/tests/test_gitio.py +130 -0
- exposuregate-0.1.0/tests/test_grading.py +263 -0
- exposuregate-0.1.0/tests/test_models.py +56 -0
- exposuregate-0.1.0/tests/test_osv.py +238 -0
- exposuregate-0.1.0/tests/test_planted.py +96 -0
- exposuregate-0.1.0/tests/test_readme.py +25 -0
- exposuregate-0.1.0/tests/test_rebuild.py +142 -0
- exposuregate-0.1.0/tests/test_report.py +146 -0
- exposuregate-0.1.0/tests/test_store.py +156 -0
- exposuregate-0.1.0/tests/test_update.py +112 -0
- exposuregate-0.1.0/tests/test_versions.py +45 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Karthikeyan Sadayamuthu
|
|
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,594 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: exposuregate
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Gate that grades dependency versions added in lockfiles using OSV and OEBC data.
|
|
5
|
+
Author: Karthikeyan Sadayamuthu
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sadayamuthu/exposuregate
|
|
8
|
+
Project-URL: Repository, https://github.com/sadayamuthu/exposuregate
|
|
9
|
+
Project-URL: Issues, https://github.com/sadayamuthu/exposuregate/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/sadayamuthu/exposuregate#readme
|
|
11
|
+
Project-URL: Changelog, https://github.com/sadayamuthu/exposuregate/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: security,vulnerability,supply-chain,kev,epss,osv,lockfile,pre-commit,dependencies
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: pyyaml
|
|
25
|
+
Requires-Dist: packaging
|
|
26
|
+
Requires-Dist: tomli; python_version < "3.11"
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff; extra == "dev"
|
|
31
|
+
Requires-Dist: mypy; extra == "dev"
|
|
32
|
+
Requires-Dist: types-PyYAML; extra == "dev"
|
|
33
|
+
Requires-Dist: jsonschema; extra == "dev"
|
|
34
|
+
Requires-Dist: build; extra == "dev"
|
|
35
|
+
Requires-Dist: twine; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
<div align="center">
|
|
39
|
+
|
|
40
|
+
# ExposureGate
|
|
41
|
+
|
|
42
|
+
<h3>Exploited-Vulnerability Gate for Lockfiles</h3>
|
|
43
|
+
|
|
44
|
+
<p>Pre-Commit & Pull-Request Dependency Exposure Gate</p>
|
|
45
|
+
|
|
46
|
+
[](https://github.com/sadayamuthu/exposuregate/actions/workflows/ci.yml)
|
|
47
|
+
[](https://www.python.org/downloads/)
|
|
48
|
+
[](https://github.com/sadayamuthu/exposuregate/blob/main/LICENSE)
|
|
49
|
+
[](https://pypi.org/project/exposuregate/)
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
[Quick Start](#quick-start) · [How It Works](#how-it-works) · [What It Grades](#what-it-grades) · [Installation](#installation) · [Configuration](#configuration) · [CLI Usage](#cli-usage)
|
|
54
|
+
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## What is ExposureGate?
|
|
60
|
+
|
|
61
|
+
ExposureGate is a pre-commit hook and pull-request check. It stops a change from adding a
|
|
62
|
+
dependency version that has a known, exploited vulnerability. It is the counterpart of
|
|
63
|
+
[ControlGate](https://github.com/sadayamuthu/controlgate): ControlGate grades control findings in
|
|
64
|
+
a diff against OCBC, and ExposureGate grades the dependency versions a diff adds against OEBC.
|
|
65
|
+
|
|
66
|
+
OEBC (the Open Exposure Baseline Catalog) says which CVEs are being exploited: KEV-listed CVEs
|
|
67
|
+
are CRITICAL, and EPSS and CVSS set the lower tiers. OEBC has no package names or version ranges,
|
|
68
|
+
so ExposureGate joins it to a local export of the OSV database (see [Data Sources](#data-sources)).
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Quick Start
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Install
|
|
76
|
+
pip install exposuregate
|
|
77
|
+
|
|
78
|
+
# Download the OEBC catalog and the OSV PyPI and npm exports (the only network step)
|
|
79
|
+
exposuregate update-data
|
|
80
|
+
exposuregate data-info
|
|
81
|
+
|
|
82
|
+
# Pre-commit (the default): grade the staged lockfiles against HEAD
|
|
83
|
+
git add uv.lock
|
|
84
|
+
exposuregate scan
|
|
85
|
+
|
|
86
|
+
# Pull request: grade HEAD against its merge-base with main
|
|
87
|
+
exposuregate scan --mode pr --target-branch main
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Exit code 0 is PASS or WARN, 1 is BLOCK, 2 is DENY or a usage error. See
|
|
91
|
+
[Exit codes](#exit-codes).
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## How It Works
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
git commit / pull request
|
|
99
|
+
↓
|
|
100
|
+
lockfile diff (requirements*.txt, poetry.lock, uv.lock, package-lock.json)
|
|
101
|
+
↓
|
|
102
|
+
added or changed (package, version) pins, direct and transitive
|
|
103
|
+
↓
|
|
104
|
+
OSV advisories that affect each version → their CVE aliases
|
|
105
|
+
↓
|
|
106
|
+
OEBC exposure_tier for each CVE (CRITICAL = KEV-listed, then EPSS and CVSS)
|
|
107
|
+
↓
|
|
108
|
+
policy: block / warn / pass per finding, then transitive cap and suppressions
|
|
109
|
+
↓
|
|
110
|
+
Verdict: DENY / BLOCK / WARN / PASS
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`scan` never uses the network. It reads the local data snapshot written by `update-data`, and
|
|
114
|
+
fails closed when that data is missing, stale, from the future or invalid.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## What It Grades
|
|
119
|
+
|
|
120
|
+
| Type | Files (by basename, anywhere in the repo) | Ecosystem |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `requirements` | `requirements*.txt` (pip-compile style, `==` pins) | PyPI |
|
|
123
|
+
| `poetry` | `poetry.lock` (dependency kind from the `pyproject.toml` next to it) | PyPI |
|
|
124
|
+
| `uv` | `uv.lock` | PyPI |
|
|
125
|
+
| `npm` | `package-lock.json` (lockfileVersion 2 or 3) | npm |
|
|
126
|
+
|
|
127
|
+
It grades every locked version that a change adds or changes, direct and transitive. Each
|
|
128
|
+
finding is tagged `direct`, `transitive` or `unknown`. Packages the change leaves alone are not
|
|
129
|
+
graded, except with `--mode full`. Git, path, URL, editable and workspace sources are listed under
|
|
130
|
+
`skipped` and are not graded.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Installation
|
|
135
|
+
|
|
136
|
+
Python 3.10 or later. The only runtime dependencies are `pyyaml`, `packaging`, and `tomli` on
|
|
137
|
+
Python 3.10.
|
|
138
|
+
|
|
139
|
+
### From PyPI
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
pip install exposuregate
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
[View ExposureGate on PyPI](https://pypi.org/project/exposuregate/)
|
|
146
|
+
|
|
147
|
+
### From source
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
pip install git+https://github.com/sadayamuthu/exposuregate
|
|
151
|
+
# or, from a checkout
|
|
152
|
+
pip install .
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### As a Pre-Commit Hook
|
|
156
|
+
|
|
157
|
+
```yaml
|
|
158
|
+
# .pre-commit-config.yaml
|
|
159
|
+
repos:
|
|
160
|
+
- repo: https://github.com/sadayamuthu/exposuregate
|
|
161
|
+
rev: v0.1.0
|
|
162
|
+
hooks:
|
|
163
|
+
- id: exposuregate
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The hook runs `exposuregate scan --mode pre-commit` with `always_run: true` and `verbose: true`.
|
|
167
|
+
A `files` regex would bypass your configured globs (custom `lockfiles.*.globs`,
|
|
168
|
+
`unsupported_globs`) and pre-commit hides the output of passing hooks, which would hide WARN
|
|
169
|
+
reports. The scan itself decides what matters: a commit that changes no graded lockfile needs no
|
|
170
|
+
data and exits quickly with PASS.
|
|
171
|
+
|
|
172
|
+
Run `exposuregate update-data` regularly (daily) so the hook is not denied for stale data. A
|
|
173
|
+
commit that changes no graded lockfile never checks data freshness, so it is never blocked by
|
|
174
|
+
stale data.
|
|
175
|
+
|
|
176
|
+
### As a GitHub Action
|
|
177
|
+
|
|
178
|
+
There are two ways to run ExposureGate on pull requests. Both check out with `fetch-depth: 0`,
|
|
179
|
+
cache the data directory keyed on the date, run `update-data` and then
|
|
180
|
+
`scan --mode pr --target-branch <base branch>`. The base branch usually exists only as
|
|
181
|
+
`origin/<branch>` in the checkout, and `--target-branch` falls back to that.
|
|
182
|
+
|
|
183
|
+
**Option 1: the composite action.** [`action.yml`](https://github.com/sadayamuthu/exposuregate/blob/main/action.yml) at the root of this repository
|
|
184
|
+
installs ExposureGate with pip, from the action's own checkout by default, so the tool always
|
|
185
|
+
matches the action ref (`@v0.1.0` runs ExposureGate 0.1.0). It then runs the scan:
|
|
186
|
+
|
|
187
|
+
```yaml
|
|
188
|
+
# .github/workflows/exposuregate.yml
|
|
189
|
+
name: exposuregate
|
|
190
|
+
on:
|
|
191
|
+
pull_request:
|
|
192
|
+
|
|
193
|
+
permissions:
|
|
194
|
+
contents: read
|
|
195
|
+
pull-requests: write # only for the PR comment step
|
|
196
|
+
|
|
197
|
+
jobs:
|
|
198
|
+
exposuregate:
|
|
199
|
+
runs-on: ubuntu-latest
|
|
200
|
+
steps:
|
|
201
|
+
- uses: actions/checkout@v4
|
|
202
|
+
with:
|
|
203
|
+
fetch-depth: 0
|
|
204
|
+
|
|
205
|
+
- id: exposuregate
|
|
206
|
+
uses: sadayamuthu/exposuregate@v0.1.0
|
|
207
|
+
|
|
208
|
+
- name: Upload report.json
|
|
209
|
+
if: always()
|
|
210
|
+
uses: actions/upload-artifact@v4
|
|
211
|
+
with:
|
|
212
|
+
name: exposuregate-report
|
|
213
|
+
path: exposuregate-report/report.json
|
|
214
|
+
|
|
215
|
+
- name: Post report.md as a PR comment
|
|
216
|
+
if: always() && hashFiles('exposuregate-report/report.md') != ''
|
|
217
|
+
continue-on-error: true # fork PRs get a read-only token and cannot comment
|
|
218
|
+
env:
|
|
219
|
+
GH_TOKEN: ${{ github.token }}
|
|
220
|
+
run: gh pr comment ${{ github.event.pull_request.number }} --body-file exposuregate-report/report.md
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
| Input | Default | Meaning |
|
|
224
|
+
|---|---|---|
|
|
225
|
+
| `target-branch` | the PR's base branch, else the repository's default branch, else `main` | Passed to `--target-branch` |
|
|
226
|
+
| `data-dir` | `~/.cache/exposuregate` | Data directory (`--data-dir`), cached per day; `~` is the home directory and a relative path is relative to the workspace |
|
|
227
|
+
| `update-data` | `true` | `false` skips the cache restore, `update-data` and the cache save, for example when `args` passes `--oebc`/`--osv-dir` files |
|
|
228
|
+
| `ecosystems` | `PyPI npm` | OSV exports for `update-data --ecosystems` |
|
|
229
|
+
| `config` | none | Config file (`--config`); otherwise `.exposuregate.yml` in `working-directory` |
|
|
230
|
+
| `output-dir` | `exposuregate-report` | Where `report.json` and `report.md` are written (`--output-dir`) |
|
|
231
|
+
| `args` | none | Extra `scan` arguments, such as `--fail-open` or `--max-age-days 200`. Split on whitespace with no quoting, so no argument may contain a space. `--mode`, `--target-branch`, `--path`, `--data-dir`, `--output-dir` and `--format` are set by the action and rejected here (use the inputs) |
|
|
232
|
+
| `version` | the action's own checkout | Set an exact version such as `0.1.0`, or `latest`, to install that release from PyPI instead |
|
|
233
|
+
| `python-version` | `3.12` | Python for `actions/setup-python` |
|
|
234
|
+
| `working-directory` | `.` | The repository to scan (`--path`) |
|
|
235
|
+
|
|
236
|
+
| Output | Meaning |
|
|
237
|
+
|---|---|
|
|
238
|
+
| `verdict` | `PASS`, `WARN`, `BLOCK` or `DENY`; empty when the scan failed before writing a report |
|
|
239
|
+
| `json` | Absolute path of `report.json` |
|
|
240
|
+
| `markdown` | Absolute path of `report.md` |
|
|
241
|
+
| `exit-code` | The scan's exit code |
|
|
242
|
+
|
|
243
|
+
The action fails the step with the scan's own exit code: 1 for BLOCK, 2 for DENY or a usage
|
|
244
|
+
error, so a WARN does not fail the job. It also writes the Markdown report to the job summary.
|
|
245
|
+
The day's data is saved to the cache right after `update-data`, before the scan, so a BLOCK or
|
|
246
|
+
DENY still caches it. If `update-data` fails, the step only warns, and the scan uses the cached
|
|
247
|
+
data or is denied for missing or stale data.
|
|
248
|
+
|
|
249
|
+
On pull requests from forks, `GITHUB_TOKEN` is read-only, so the PR comment step cannot post;
|
|
250
|
+
it is marked `continue-on-error` and the report is still in the artifact and the job summary.
|
|
251
|
+
|
|
252
|
+
**Option 2: raw steps.** [`docs/github-action.yml`](https://github.com/sadayamuthu/exposuregate/blob/main/docs/github-action.yml) is a ready-to-copy
|
|
253
|
+
workflow that does the same with plain `run:` steps, if you would rather not depend on the
|
|
254
|
+
action. It installs ExposureGate from PyPI, uploads `report.json` and posts `report.md` as a PR
|
|
255
|
+
comment (skipped when there is no report, and allowed to fail on fork PRs).
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Data: `update-data`
|
|
260
|
+
|
|
261
|
+
`scan` never uses the network. Download the data first:
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
exposuregate update-data # OEBC catalog + OSV PyPI and npm exports
|
|
265
|
+
exposuregate update-data --ecosystems PyPI
|
|
266
|
+
exposuregate data-info # what is in the local snapshot, and how old it is
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
The data goes into `~/.cache/exposuregate`. You can change this with `data.dir` in the config,
|
|
270
|
+
`EXPOSUREGATE_DATA_DIR` or `--data-dir`. The download is validated and indexed into
|
|
271
|
+
`index.sqlite`. The new index is written to a temporary file and renamed into place, so a failed
|
|
272
|
+
update leaves the old data as it was.
|
|
273
|
+
|
|
274
|
+
Freshness: data older than `data.max_age_days` (7 by default) is **stale**, and a scan that needs
|
|
275
|
+
it is denied (exit 2). This is checked per source (OEBC, and OSV for each ecosystem) and only for
|
|
276
|
+
ecosystems the change touches.
|
|
277
|
+
|
|
278
|
+
> **Operational note.** OEBC's newest published catalog is from 2026-06-14. Until OEBC's daily
|
|
279
|
+
> build is fixed, every scan that needs OEBC is denied as `data_stale` under the defaults. This is
|
|
280
|
+
> the intended fail-closed behaviour. The explicit escape hatches are `--max-age-days N` (or
|
|
281
|
+
> `data.oebc_max_age_days`) and `--fail-open`.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Configuration
|
|
286
|
+
|
|
287
|
+
ExposureGate reads `.exposuregate.yml` from the directory given by `--path` (default: the
|
|
288
|
+
current directory), or the file given by `--config`. The file is deep-merged over the defaults
|
|
289
|
+
below, and lists replace the default list. An unknown key, a wrong type or an invalid value is a
|
|
290
|
+
usage error (exit 2). Every key changes behaviour. CLI flags override the config.
|
|
291
|
+
|
|
292
|
+
These are the defaults. `tests/test_readme.py` checks this block against
|
|
293
|
+
`exposuregate.config.DEFAULTS`, so the two cannot drift apart.
|
|
294
|
+
|
|
295
|
+
<!-- config-defaults:start -->
|
|
296
|
+
```yaml
|
|
297
|
+
fail_closed: true # false: report denials with enforced: false, verdict floor WARN
|
|
298
|
+
data:
|
|
299
|
+
dir: ~/.cache/exposuregate # overridden by EXPOSUREGATE_DATA_DIR, then --data-dir
|
|
300
|
+
max_age_days: 7 # limit for both sources; --max-age-days sets this one
|
|
301
|
+
oebc_max_age_days: null # per-source override
|
|
302
|
+
osv_max_age_days: null # per-source override
|
|
303
|
+
policy: # actions: block | warn | pass
|
|
304
|
+
CRITICAL: block
|
|
305
|
+
HIGH: warn
|
|
306
|
+
MEDIUM: pass
|
|
307
|
+
LOW: pass
|
|
308
|
+
not_in_oebc: warn
|
|
309
|
+
no_cve: warn
|
|
310
|
+
unparseable_version: warn
|
|
311
|
+
unpinned: warn
|
|
312
|
+
unsupported_lockfile: warn
|
|
313
|
+
transitive_max_action: block # cap for transitive findings (warn = transitive is advisory)
|
|
314
|
+
lockfiles: # globs match the file's basename
|
|
315
|
+
requirements: { enabled: true, globs: ["requirements*.txt"] }
|
|
316
|
+
poetry: { enabled: true, globs: ["poetry.lock"] }
|
|
317
|
+
uv: { enabled: true, globs: ["uv.lock"] }
|
|
318
|
+
npm: { enabled: true, globs: ["package-lock.json"] }
|
|
319
|
+
unsupported_globs: [yarn.lock, pnpm-lock.yaml, Pipfile.lock, bun.lock, bun.lockb,
|
|
320
|
+
Cargo.lock, go.sum, Gemfile.lock, composer.lock, gradle.lockfile]
|
|
321
|
+
exclusions:
|
|
322
|
+
paths: [] # fnmatch globs on the full repo-relative path
|
|
323
|
+
suppressions: []
|
|
324
|
+
reporting:
|
|
325
|
+
format: [json, markdown]
|
|
326
|
+
output_dir: .exposuregate/reports # relative to --path; null to disable
|
|
327
|
+
```
|
|
328
|
+
<!-- config-defaults:end -->
|
|
329
|
+
|
|
330
|
+
`exclusions.paths` is matched against the **full repo-relative path**, not the basename. To
|
|
331
|
+
exclude `examples/demo/uv.lock`, write `examples/*/uv.lock` or `examples/*` (`*` also matches
|
|
332
|
+
`/`). A bare `uv.lock` matches only the root lockfile. Matching is case-sensitive. Excluded files
|
|
333
|
+
are listed in the report with status `excluded`.
|
|
334
|
+
|
|
335
|
+
### Suppressions
|
|
336
|
+
|
|
337
|
+
```yaml
|
|
338
|
+
suppressions:
|
|
339
|
+
- id: CVE-2024-12345 # CVE or advisory ID (required)
|
|
340
|
+
package: urllib3 # optional, normalized name
|
|
341
|
+
ecosystem: PyPI # optional
|
|
342
|
+
reason: "not reachable: we never call X" # required, non-empty
|
|
343
|
+
expires: 2027-01-31 # required
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
An expired suppression does not apply. It is listed under `expired_suppressions` and produces a
|
|
347
|
+
`suppression_expired` WARN finding.
|
|
348
|
+
|
|
349
|
+
### Default policy
|
|
350
|
+
|
|
351
|
+
| Reason | Condition | Action |
|
|
352
|
+
|---|---|---|
|
|
353
|
+
| `graded` | CVE in OEBC with tier CRITICAL (KEV-listed) | block |
|
|
354
|
+
| `graded` | tier HIGH | warn |
|
|
355
|
+
| `graded` | tier MEDIUM or LOW | pass |
|
|
356
|
+
| `not_in_oebc` | the advisory's CVE is not in the OEBC snapshot | warn |
|
|
357
|
+
| `no_cve` | the advisory has no CVE alias | warn |
|
|
358
|
+
| `unparseable_version` | the version does not parse in its ecosystem's scheme | warn |
|
|
359
|
+
| `unpinned` | a requirements line that is not `==`/`===` | warn |
|
|
360
|
+
| `unsupported_lockfile` | `yarn.lock`, `pnpm-lock.yaml`, `Cargo.lock`, ... changed | warn |
|
|
361
|
+
|
|
362
|
+
After that, `policy.transitive_max_action` caps the action of transitive findings. Then a
|
|
363
|
+
matching, unexpired suppression sets the action to `pass` and records `suppressed_by`. Every
|
|
364
|
+
advisory match appears in the report, including passed and suppressed ones.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## CLI Usage
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
# pre-commit (the default): staged lockfiles vs HEAD
|
|
372
|
+
exposuregate scan
|
|
373
|
+
|
|
374
|
+
# pull request: HEAD vs merge-base with the target branch
|
|
375
|
+
exposuregate scan --mode pr --target-branch main
|
|
376
|
+
|
|
377
|
+
# explicit refs, for replaying a historical PR (no merge-base is taken)
|
|
378
|
+
exposuregate scan --base <sha> --head <sha>
|
|
379
|
+
|
|
380
|
+
# everything currently pinned
|
|
381
|
+
exposuregate scan --mode full
|
|
382
|
+
|
|
383
|
+
# two files directly (reproducible evaluation)
|
|
384
|
+
exposuregate scan --old old/uv.lock --new new/uv.lock --lockfile-type uv \
|
|
385
|
+
--oebc oebc.json.gz --osv-dir osv/ --as-of 2026-11-13
|
|
386
|
+
|
|
387
|
+
# data management
|
|
388
|
+
exposuregate update-data
|
|
389
|
+
exposuregate data-info
|
|
390
|
+
|
|
391
|
+
# rebuild OEBC as of a past date (see "Replaying a past date")
|
|
392
|
+
exposuregate rebuild-oebc --as-of 2026-03-01 --kev known_exploited_vulnerabilities.json \
|
|
393
|
+
--epss-dir epss/ --oebc latest.json.gz --out oebc-2026-03-01.json.gz
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
In `--mode pr`, `--target-branch` (default `main`) falls back to `origin/<branch>` when there is
|
|
397
|
+
no local branch of that name, as in a CI checkout. `--manifest FILE` supplies the
|
|
398
|
+
`pyproject.toml` for poetry direct/transitive tagging in `--old/--new` mode.
|
|
399
|
+
|
|
400
|
+
### Exit codes
|
|
401
|
+
|
|
402
|
+
| Verdict | Meaning | Exit |
|
|
403
|
+
|---|---|---|
|
|
404
|
+
| `PASS` | no finding with action `warn` or `block` | 0 |
|
|
405
|
+
| `WARN` | the worst finding action is `warn` | 0 |
|
|
406
|
+
| `BLOCK` | at least one finding with action `block` | 1 |
|
|
407
|
+
| `DENY` | a fail-closed condition (data missing, stale, from the future or invalid; a lockfile that will not parse; a git error) with `fail_closed: true` | 2 |
|
|
408
|
+
| (none) | usage or config error: bad flags, unknown config key, not a git repo when a git mode is asked for | 2 |
|
|
409
|
+
|
|
410
|
+
Precedence is DENY > BLOCK > WARN > PASS. When a scan is denied, ExposureGate still grades
|
|
411
|
+
everything it can, so the report shows the findings next to the denial reasons. With
|
|
412
|
+
`fail_closed: false` or `--fail-open`, denials are reported with `enforced: false` and the
|
|
413
|
+
verdict comes from the findings, but a scan with any denial is never better than WARN.
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## Output Formats
|
|
418
|
+
|
|
419
|
+
| Format | Use Case |
|
|
420
|
+
|--------|----------|
|
|
421
|
+
| `json` | Programmatic consumption, agents, dashboards; conforms to [`schemas/report.schema.json`](https://github.com/sadayamuthu/exposuregate/blob/main/schemas/report.schema.json) |
|
|
422
|
+
| `markdown` | PR comments, job summaries, terminal output |
|
|
423
|
+
|
|
424
|
+
Reports go to stdout, JSON first and then Markdown, or in the order given by
|
|
425
|
+
`--format json markdown`. If `reporting.output_dir` or `--output-dir` is set, they are also
|
|
426
|
+
written there as `report.json` and `report.md`. Progress lines go to stderr.
|
|
427
|
+
|
|
428
|
+
The default `output_dir` writes reports inside the repository, so add `.exposuregate/` to your
|
|
429
|
+
`.gitignore`.
|
|
430
|
+
|
|
431
|
+
Each finding records the lockfile, package, version and previous version, whether the dependency
|
|
432
|
+
is `direct` or `transitive`, the reason, the CVE and advisory IDs, the OEBC tier with its KEV,
|
|
433
|
+
EPSS and CVSS data, the `fixed_in` versions from OSV, the action before and after the transitive
|
|
434
|
+
cap and suppressions, and `suppressed_by`.
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## Replaying a past date
|
|
439
|
+
|
|
440
|
+
For evaluation, a PR can be graded as it would have been graded on date D. OEBC serves only
|
|
441
|
+
`latest` and OSV's export is current-only, so there are two ways to do this:
|
|
442
|
+
|
|
443
|
+
**Option A: rebuild OEBC as of D.**
|
|
444
|
+
|
|
445
|
+
```bash
|
|
446
|
+
exposuregate rebuild-oebc --as-of 2026-03-01 --kev known_exploited_vulnerabilities.json \
|
|
447
|
+
--epss-dir epss/ --oebc latest.json.gz --out oebc-2026-03-01.json.gz
|
|
448
|
+
exposuregate scan --base <sha> --head <sha> --as-of 2026-03-01 --replay \
|
|
449
|
+
--oebc oebc-2026-03-01.json.gz --osv-dir osv/
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
`rebuild-oebc` applies OEBC's tier rule to the KEV entries with `dateAdded <= D`. Every one of
|
|
453
|
+
them is CRITICAL, even when OEBC and EPSS do not list it. It uses the EPSS daily file
|
|
454
|
+
`epss_scores-D.csv.gz`, or the nearest earlier one, and CVSS from the current catalog. The output
|
|
455
|
+
records each input's path, date and sha256 under `rebuilt_from`. `--replay` drops OSV advisories
|
|
456
|
+
published after D, and the report records `osv_mode: current_filtered`. Advisory ranges and
|
|
457
|
+
aliases are still today's, which is a known leak. OSV staleness and `data_from_future` checks are
|
|
458
|
+
not enforced in replay.
|
|
459
|
+
|
|
460
|
+
**Option B: the snapshot archive.**
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
exposuregate scan --base <sha> --head <sha> --as-of 2026-11-13 \
|
|
464
|
+
--archive ~/data/exposuregate-archive --max-age-days 200
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
`--archive DIR` reads the daily snapshot archive written by the paper's daily archive script,
|
|
468
|
+
which lives in the papers repository, not in this one. It
|
|
469
|
+
uses the OEBC catalog and the OSV state as archived on the latest day on or before `--as-of`
|
|
470
|
+
(`osv_mode: archived`), so nothing from later leaks in. `--archive` needs `--as-of` and cannot be
|
|
471
|
+
combined with `--oebc`/`--osv-dir`. The archived OEBC is still checked for freshness against
|
|
472
|
+
`--as-of`. Until OEBC's daily build is fixed, every archived catalog is the 2026-06-14 build, so an
|
|
473
|
+
archive replay is denied as stale unless you pass `--max-age-days`, as in the example.
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## Planted-Exposure Set
|
|
478
|
+
|
|
479
|
+
`tests/test_planted.py` runs 24 planted cases against data frozen from the 2026-11-13 archive
|
|
480
|
+
day (OEBC built 2026-06-14). The cases cover every lockfile type: a direct KEV pin (BLOCK), a
|
|
481
|
+
transitive KEV pin (BLOCK, tagged `transitive`), a HIGH pin (WARN), a LOW pin (PASS) and a benign
|
|
482
|
+
bump (PASS). Further cases document OEBC staleness: CVEs that were KEV-listed after the OEBC build
|
|
483
|
+
grade as `not_in_oebc` or LOW. The cases, the chosen versions and the reason for each choice are in
|
|
484
|
+
`tests/fixtures/planted/cases.json`. `scripts/freeze_fixtures.py` regenerates the frozen data
|
|
485
|
+
from the archive and checks it.
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
489
|
+
## Limitations
|
|
490
|
+
|
|
491
|
+
- **No reachability.** A vulnerable version is flagged whether or not the vulnerable code is
|
|
492
|
+
called.
|
|
493
|
+
- **Lockfiles only.** Manifests are not resolved. A `pyproject.toml` or `package.json` change
|
|
494
|
+
without its lockfile is listed as `manifest_only` and not graded. `yarn.lock`,
|
|
495
|
+
`pnpm-lock.yaml`, `Pipfile.lock`, `package-lock.json` v1, Go, Maven and Cargo are not graded.
|
|
496
|
+
A changed unsupported lockfile gives a WARN, and a v1 `package-lock.json` is denied.
|
|
497
|
+
- **Requirements markers are ignored.** A pin is graded even when its environment marker means
|
|
498
|
+
it would never be installed. The marker is recorded on the finding, but this is a known source of
|
|
499
|
+
false positives. `-r`/`-c` includes are not followed.
|
|
500
|
+
- **OEBC tiers are used as published.** A CVE KEV-listed after the OEBC snapshot was built is
|
|
501
|
+
`not_in_oebc` (WARN) or carries its older tier until OEBC is rebuilt. With OEBC's build stalled
|
|
502
|
+
at 2026-06-14, this already matters: see the `stale_oebc` planted cases.
|
|
503
|
+
- **Replay leakage (option A).** OSV ranges and aliases are as of today. CVSS comes from the
|
|
504
|
+
current OEBC catalog.
|
|
505
|
+
- **Matching** uses OSV `versions` lists and `ECOSYSTEM`/`SEMVER` ranges. `GIT` ranges are
|
|
506
|
+
ignored. A range with an unparseable event version is skipped.
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
## Development
|
|
511
|
+
|
|
512
|
+
```bash
|
|
513
|
+
make install-dev # Install with dev dependencies
|
|
514
|
+
make test # Run tests
|
|
515
|
+
make test-cov # Run tests with coverage (fails under 90%)
|
|
516
|
+
make lint # Lint with ruff
|
|
517
|
+
make format # Apply ruff's automatic lint fixes
|
|
518
|
+
make typecheck # Type check with mypy (strict)
|
|
519
|
+
make check # Run all checks (lint + typecheck + test)
|
|
520
|
+
make build # Build distribution packages
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
Or without `make`:
|
|
524
|
+
|
|
525
|
+
```bash
|
|
526
|
+
python -m venv .venv && .venv/bin/pip install -e '.[dev]'
|
|
527
|
+
.venv/bin/pytest -q --cov=exposuregate --cov-report=term-missing
|
|
528
|
+
.venv/bin/ruff check . && .venv/bin/mypy src
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## AI Agent Skills
|
|
534
|
+
|
|
535
|
+
ExposureGate provides skills for popular AI coding assistants. They teach the agent to run
|
|
536
|
+
`exposuregate scan` before committing a lockfile change, read the JSON report, and fix a finding
|
|
537
|
+
by moving the package to a version in its `fixed_in` list, never by adding a suppression without a
|
|
538
|
+
reason and an expiry date.
|
|
539
|
+
|
|
540
|
+
The agent prompts and workflows are located in the [`skills/`](https://github.com/sadayamuthu/exposuregate/tree/main/skills) directory:
|
|
541
|
+
|
|
542
|
+
- **Antigravity**: Full workflow definitions available in `skills/antigravity/exposuregate/`
|
|
543
|
+
- **Cursor**: Repository rules available in `skills/cursor/.cursorrules`
|
|
544
|
+
- **Claude Code**: System prompt instructions in `skills/claude_code/.clauderules`
|
|
545
|
+
- **Codex**: Integration prompts in `skills/codex/instructions.md`
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
## Data Sources
|
|
550
|
+
|
|
551
|
+
ExposureGate joins two public data sets, both downloaded by `exposuregate update-data`:
|
|
552
|
+
|
|
553
|
+
- **[OEBC](https://github.com/sadayamuthu/oebc)**, the Open Exposure Baseline Catalog, published
|
|
554
|
+
by [OpenAstra](https://openastra.org): one `exposure_tier` per CVE, from CISA KEV (CRITICAL),
|
|
555
|
+
FIRST EPSS and CVSS. Downloaded from `https://openastra.org/oebc/catalog/v0.1/latest.json.gz`.
|
|
556
|
+
- **[OSV](https://osv.dev)**: the per-ecosystem exports of the OSV database
|
|
557
|
+
(`https://osv-vulnerabilities.storage.googleapis.com/<ecosystem>/all.zip`, for `PyPI` and
|
|
558
|
+
`npm`), which map package versions to advisories and advisories to CVE aliases.
|
|
559
|
+
|
|
560
|
+
`exposuregate data-info` shows the OEBC build date and the OSV export dates in the local
|
|
561
|
+
snapshot, and every report records them under `data`.
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
## Contributing
|
|
566
|
+
|
|
567
|
+
Bug reports, wrong or missed findings and new lockfile parsers are welcome. See
|
|
568
|
+
[CONTRIBUTING.md](https://github.com/sadayamuthu/exposuregate/blob/main/CONTRIBUTING.md) for the development setup, the tests and how to add a lockfile
|
|
569
|
+
parser.
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
## Security
|
|
574
|
+
|
|
575
|
+
Please report security problems privately, not in a public issue. See
|
|
576
|
+
[SECURITY.md](https://github.com/sadayamuthu/exposuregate/blob/main/SECURITY.md).
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
## License
|
|
581
|
+
|
|
582
|
+
[MIT License](https://github.com/sadayamuthu/exposuregate/blob/main/LICENSE). See [CHANGELOG.md](https://github.com/sadayamuthu/exposuregate/blob/main/CHANGELOG.md) for release notes.
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
<div align="center">
|
|
587
|
+
|
|
588
|
+
**ExposureGate is an open-source exploited-vulnerability gate for dependency lockfiles**
|
|
589
|
+
|
|
590
|
+
ExposureGate is managed by [OpenAstra](https://openastra.org).
|
|
591
|
+
|
|
592
|
+
[PyPI](https://pypi.org/project/exposuregate/) · [GitHub](https://github.com/sadayamuthu/exposuregate) · [OEBC](https://github.com/sadayamuthu/oebc) · [ControlGate](https://github.com/sadayamuthu/controlgate)
|
|
593
|
+
|
|
594
|
+
</div>
|