garnet-guard 0.1.0a1__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.
- garnet_guard-0.1.0a1/.github/ISSUE_TEMPLATE/false-positive.md +13 -0
- garnet_guard-0.1.0a1/.github/ISSUE_TEMPLATE/missed-detection.md +15 -0
- garnet_guard-0.1.0a1/.github/dependabot.yml +25 -0
- garnet_guard-0.1.0a1/.github/pull_request_template.md +25 -0
- garnet_guard-0.1.0a1/.github/workflows/ci.yml +67 -0
- garnet_guard-0.1.0a1/.github/workflows/garnet-guard.yml +39 -0
- garnet_guard-0.1.0a1/.github/workflows/release.yml +116 -0
- garnet_guard-0.1.0a1/.github/workflows/scorecard.yml +38 -0
- garnet_guard-0.1.0a1/.gitignore +37 -0
- garnet_guard-0.1.0a1/.python-version +1 -0
- garnet_guard-0.1.0a1/AGENTS.md +66 -0
- garnet_guard-0.1.0a1/ARCHITECTURE.md +158 -0
- garnet_guard-0.1.0a1/CHANGELOG.md +49 -0
- garnet_guard-0.1.0a1/CLAUDE.md +66 -0
- garnet_guard-0.1.0a1/CODE_OF_CONDUCT.md +11 -0
- garnet_guard-0.1.0a1/CONTRIBUTING.md +59 -0
- garnet_guard-0.1.0a1/GOVERNANCE.md +23 -0
- garnet_guard-0.1.0a1/LICENSE +202 -0
- garnet_guard-0.1.0a1/NOTICE +8 -0
- garnet_guard-0.1.0a1/OPEN_CORE.md +67 -0
- garnet_guard-0.1.0a1/PKG-INFO +227 -0
- garnet_guard-0.1.0a1/PRINCIPLES.md +34 -0
- garnet_guard-0.1.0a1/PROJECT_BRIEF.md +27 -0
- garnet_guard-0.1.0a1/README.md +194 -0
- garnet_guard-0.1.0a1/ROADMAP.md +116 -0
- garnet_guard-0.1.0a1/SECURITY.md +40 -0
- garnet_guard-0.1.0a1/THREAT_MODEL.md +63 -0
- garnet_guard-0.1.0a1/VISION.md +43 -0
- garnet_guard-0.1.0a1/WORKING_RECORD.md +155 -0
- garnet_guard-0.1.0a1/action/comment/action.yml +71 -0
- garnet_guard-0.1.0a1/action/comment/find-pr.sh +32 -0
- garnet_guard-0.1.0a1/action/comment/render.sh +58 -0
- garnet_guard-0.1.0a1/action/examples/garnet-guard-comment.yml +34 -0
- garnet_guard-0.1.0a1/action/examples/garnet-guard-fork.yml +48 -0
- garnet_guard-0.1.0a1/action/examples/garnet-guard.yml +43 -0
- garnet_guard-0.1.0a1/action/post-comment.sh +49 -0
- garnet_guard-0.1.0a1/action/run.sh +220 -0
- garnet_guard-0.1.0a1/action.yml +113 -0
- garnet_guard-0.1.0a1/backtest/__init__.py +0 -0
- garnet_guard-0.1.0a1/backtest/__main__.py +213 -0
- garnet_guard-0.1.0a1/backtest/clean.toml +36 -0
- garnet_guard-0.1.0a1/backtest/corpus-npm-1000.json +14747 -0
- garnet_guard-0.1.0a1/backtest/corpus-npm.json +3020 -0
- garnet_guard-0.1.0a1/backtest/corpus-pypi-1000.json +15404 -0
- garnet_guard-0.1.0a1/backtest/corpus-results/REPORT.md +116 -0
- garnet_guard-0.1.0a1/backtest/corpus-results/after-pr8/summary.json +39 -0
- garnet_guard-0.1.0a1/backtest/corpus-results/after-pr8/updates.json +10672 -0
- garnet_guard-0.1.0a1/backtest/corpus-results/summary.json +39 -0
- garnet_guard-0.1.0a1/backtest/corpus-results/triage.csv +19 -0
- garnet_guard-0.1.0a1/backtest/corpus-results/triage.json +182 -0
- garnet_guard-0.1.0a1/backtest/corpus-results/updates.json +10671 -0
- garnet_guard-0.1.0a1/backtest/corpus-results-npm/REPORT.md +79 -0
- garnet_guard-0.1.0a1/backtest/corpus-results-npm/summary.json +40 -0
- garnet_guard-0.1.0a1/backtest/corpus-results-npm/updates.json +18789 -0
- garnet_guard-0.1.0a1/backtest/corpus-results-npm-1000/REPORT.md +133 -0
- garnet_guard-0.1.0a1/backtest/corpus-results-npm-1000/summary.json +48 -0
- garnet_guard-0.1.0a1/backtest/corpus-results-npm-1000/triage.json +1608 -0
- garnet_guard-0.1.0a1/backtest/corpus-results-npm-1000/updates.json +89870 -0
- garnet_guard-0.1.0a1/backtest/corpus-results-pypi-1000/REPORT.md +189 -0
- garnet_guard-0.1.0a1/backtest/corpus-results-pypi-1000/summary.json +53 -0
- garnet_guard-0.1.0a1/backtest/corpus-results-pypi-1000/triage.json +2444 -0
- garnet_guard-0.1.0a1/backtest/corpus-results-pypi-1000/updates.json +104884 -0
- garnet_guard-0.1.0a1/backtest/corpus.json +1584 -0
- garnet_guard-0.1.0a1/backtest/corpus.py +396 -0
- garnet_guard-0.1.0a1/backtest/incidents.toml +852 -0
- garnet_guard-0.1.0a1/backtest/npm-incidents.toml +88 -0
- garnet_guard-0.1.0a1/backtest/top-npm-1000.json +1008 -0
- garnet_guard-0.1.0a1/backtest/top-pypi-1000.json +1007 -0
- garnet_guard-0.1.0a1/docs/API.md +333 -0
- garnet_guard-0.1.0a1/docs/CODE_TOUR.md +209 -0
- garnet_guard-0.1.0a1/docs/DEVOPS.md +17 -0
- garnet_guard-0.1.0a1/docs/README.md +16 -0
- garnet_guard-0.1.0a1/docs/RELEASING.md +59 -0
- garnet_guard-0.1.0a1/docs/SPEC-v1.md +302 -0
- garnet_guard-0.1.0a1/docs/action.md +120 -0
- garnet_guard-0.1.0a1/docs/brand/README.md +13 -0
- garnet_guard-0.1.0a1/docs/brand/badge/checked-by-garnet-guard.svg +1 -0
- garnet_guard-0.1.0a1/docs/brand/brand-kit.md +144 -0
- garnet_guard-0.1.0a1/docs/brand/favicon/favicon-16.svg +1 -0
- garnet_guard-0.1.0a1/docs/brand/lockup/lockup-horizontal-reverse.svg +1 -0
- garnet_guard-0.1.0a1/docs/brand/lockup/lockup-horizontal.svg +1 -0
- garnet_guard-0.1.0a1/docs/brand/pr-comment/pr-comment-dark.svg +1 -0
- garnet_guard-0.1.0a1/docs/brand/pr-comment/pr-comment-light.svg +1 -0
- garnet_guard-0.1.0a1/docs/decisions/0001-release-diff-guard-python-first.md +16 -0
- garnet_guard-0.1.0a1/docs/decisions/0002-implementation-in-python.md +16 -0
- garnet_guard-0.1.0a1/docs/decisions/0003-apache-2-and-dco.md +16 -0
- garnet_guard-0.1.0a1/docs/decisions/0004-github-action-first-surface.md +16 -0
- garnet_guard-0.1.0a1/docs/decisions/0005-never-execute-package-code.md +16 -0
- garnet_guard-0.1.0a1/docs/decisions/0006-open-core-boundary.md +16 -0
- garnet_guard-0.1.0a1/docs/decisions/0007-distribution-name.md +18 -0
- garnet_guard-0.1.0a1/docs/decisions/0008-no-telemetry.md +16 -0
- garnet_guard-0.1.0a1/docs/decisions/0009-trust-history-boundary.md +17 -0
- garnet_guard-0.1.0a1/docs/decisions/0010-funding-gates.md +18 -0
- garnet_guard-0.1.0a1/docs/decisions/0011-public-from-day-one.md +16 -0
- garnet_guard-0.1.0a1/docs/decisions/0012-pricing-shape.md +26 -0
- garnet_guard-0.1.0a1/docs/decisions/0013-brand-identity.md +22 -0
- garnet_guard-0.1.0a1/docs/decisions/0014-runtime-dependencies-0-1.md +30 -0
- garnet_guard-0.1.0a1/docs/decisions/0015-core-and-premium-pricing.md +35 -0
- garnet_guard-0.1.0a1/docs/decisions/README.md +23 -0
- garnet_guard-0.1.0a1/docs/decisions/template.md +10 -0
- garnet_guard-0.1.0a1/docs/expert-brief.md +31 -0
- garnet_guard-0.1.0a1/docs/handoffs/.gitkeep +0 -0
- garnet_guard-0.1.0a1/docs/marketplace.md +38 -0
- garnet_guard-0.1.0a1/docs/policy.md +101 -0
- garnet_guard-0.1.0a1/docs/rule-coverage.md +126 -0
- garnet_guard-0.1.0a1/docs/rules/GRN-C001.md +96 -0
- garnet_guard-0.1.0a1/docs/rules/GRN-C002.md +134 -0
- garnet_guard-0.1.0a1/docs/rules/GRN-C003.md +156 -0
- garnet_guard-0.1.0a1/docs/rules/GRN-C004.md +75 -0
- garnet_guard-0.1.0a1/docs/rules/GRN-C005.md +76 -0
- garnet_guard-0.1.0a1/docs/rules/GRN-C006.md +75 -0
- garnet_guard-0.1.0a1/docs/rules/GRN-C007.md +119 -0
- garnet_guard-0.1.0a1/docs/rules/GRN-E001.md +69 -0
- garnet_guard-0.1.0a1/docs/rules/GRN-E002.md +74 -0
- garnet_guard-0.1.0a1/docs/rules/GRN-E003.md +88 -0
- garnet_guard-0.1.0a1/docs/rules/GRN-E004.md +65 -0
- garnet_guard-0.1.0a1/docs/rules/GRN-E005.md +65 -0
- garnet_guard-0.1.0a1/docs/rules/GRN-K001.md +83 -0
- garnet_guard-0.1.0a1/docs/rules/GRN-T001.md +118 -0
- garnet_guard-0.1.0a1/docs/rules/GRN-T002.md +78 -0
- garnet_guard-0.1.0a1/docs/rules/GRN-T003.md +75 -0
- garnet_guard-0.1.0a1/docs/rules/GRN-T004.md +142 -0
- garnet_guard-0.1.0a1/docs/rules/GRN-T005.md +77 -0
- garnet_guard-0.1.0a1/docs/rules/GRN-T006.md +74 -0
- garnet_guard-0.1.0a1/docs/rules/GRN-T007.md +77 -0
- garnet_guard-0.1.0a1/docs/rules/GRN-T008.md +76 -0
- garnet_guard-0.1.0a1/docs/rules/README.md +83 -0
- garnet_guard-0.1.0a1/pyproject.toml +149 -0
- garnet_guard-0.1.0a1/scripts/commit-msg-hook.sh +10 -0
- garnet_guard-0.1.0a1/src/garnet_guard/__init__.py +45 -0
- garnet_guard-0.1.0a1/src/garnet_guard/checks/__init__.py +52 -0
- garnet_guard-0.1.0a1/src/garnet_guard/checks/content.py +605 -0
- garnet_guard-0.1.0a1/src/garnet_guard/checks/docs.py +220 -0
- garnet_guard-0.1.0a1/src/garnet_guard/checks/trust.py +280 -0
- garnet_guard-0.1.0a1/src/garnet_guard/cli.py +332 -0
- garnet_guard-0.1.0a1/src/garnet_guard/ecosystems/__init__.py +4 -0
- garnet_guard-0.1.0a1/src/garnet_guard/ecosystems/github.py +198 -0
- garnet_guard-0.1.0a1/src/garnet_guard/ecosystems/gittree.py +386 -0
- garnet_guard-0.1.0a1/src/garnet_guard/ecosystems/npm.py +369 -0
- garnet_guard-0.1.0a1/src/garnet_guard/ecosystems/npm_provenance.py +380 -0
- garnet_guard-0.1.0a1/src/garnet_guard/ecosystems/osv.py +19 -0
- garnet_guard-0.1.0a1/src/garnet_guard/ecosystems/provenance.py +156 -0
- garnet_guard-0.1.0a1/src/garnet_guard/ecosystems/pypi.py +355 -0
- garnet_guard-0.1.0a1/src/garnet_guard/ecosystems/source.py +131 -0
- garnet_guard-0.1.0a1/src/garnet_guard/engine.py +365 -0
- garnet_guard-0.1.0a1/src/garnet_guard/facts/__init__.py +4 -0
- garnet_guard-0.1.0a1/src/garnet_guard/facts/inventory.py +294 -0
- garnet_guard-0.1.0a1/src/garnet_guard/facts/npmmeta.py +75 -0
- garnet_guard-0.1.0a1/src/garnet_guard/facts/pyindex.py +589 -0
- garnet_guard-0.1.0a1/src/garnet_guard/lockfiles/__init__.py +12 -0
- garnet_guard-0.1.0a1/src/garnet_guard/lockfiles/base.py +102 -0
- garnet_guard-0.1.0a1/src/garnet_guard/lockfiles/diff.py +66 -0
- garnet_guard-0.1.0a1/src/garnet_guard/lockfiles/pylock.py +63 -0
- garnet_guard-0.1.0a1/src/garnet_guard/lockfiles/requirements.py +73 -0
- garnet_guard-0.1.0a1/src/garnet_guard/lockfiles/toml_locks.py +93 -0
- garnet_guard-0.1.0a1/src/garnet_guard/markdown.py +314 -0
- garnet_guard-0.1.0a1/src/garnet_guard/models.py +346 -0
- garnet_guard-0.1.0a1/src/garnet_guard/net.py +187 -0
- garnet_guard-0.1.0a1/src/garnet_guard/policy.py +347 -0
- garnet_guard-0.1.0a1/src/garnet_guard/py.typed +0 -0
- garnet_guard-0.1.0a1/src/garnet_guard/report.py +128 -0
- garnet_guard-0.1.0a1/src/garnet_guard/safe_archive.py +366 -0
- garnet_guard-0.1.0a1/src/garnet_guard/sarif.py +275 -0
- garnet_guard-0.1.0a1/src/garnet_guard/workspace.py +140 -0
- garnet_guard-0.1.0a1/tests/__init__.py +0 -0
- garnet_guard-0.1.0a1/tests/builders.py +153 -0
- garnet_guard-0.1.0a1/tests/conftest.py +18 -0
- garnet_guard-0.1.0a1/tests/fixtures/npm/nx-21.4.1-attestations.json +146 -0
- garnet_guard-0.1.0a1/tests/fixtures/npm/nx-21.4.1-version.json +39 -0
- garnet_guard-0.1.0a1/tests/fixtures/npm/tinycolor-4.2.2-attestations.json +142 -0
- garnet_guard-0.1.0a1/tests/fixtures/npm/tinycolor-4.2.2-version.json +57 -0
- garnet_guard-0.1.0a1/tests/fixtures/npm/which-3.0.1-attestations.json +96 -0
- garnet_guard-0.1.0a1/tests/fixtures/npm/which-3.0.1-version.json +46 -0
- garnet_guard-0.1.0a1/tests/fixtures/sarif-schema-2.1.0.json +3389 -0
- garnet_guard-0.1.0a1/tests/test_action.py +488 -0
- garnet_guard-0.1.0a1/tests/test_backtest_corpus.py +113 -0
- garnet_guard-0.1.0a1/tests/test_checks.py +714 -0
- garnet_guard-0.1.0a1/tests/test_cli_and_report.py +92 -0
- garnet_guard-0.1.0a1/tests/test_github.py +118 -0
- garnet_guard-0.1.0a1/tests/test_gittree.py +257 -0
- garnet_guard-0.1.0a1/tests/test_lockfiles.py +201 -0
- garnet_guard-0.1.0a1/tests/test_markdown.py +210 -0
- garnet_guard-0.1.0a1/tests/test_net.py +63 -0
- garnet_guard-0.1.0a1/tests/test_npm.py +341 -0
- garnet_guard-0.1.0a1/tests/test_policy.py +371 -0
- garnet_guard-0.1.0a1/tests/test_pyindex.py +297 -0
- garnet_guard-0.1.0a1/tests/test_real_releases.py +92 -0
- garnet_guard-0.1.0a1/tests/test_replay.py +59 -0
- garnet_guard-0.1.0a1/tests/test_rule_matrix.py +574 -0
- garnet_guard-0.1.0a1/tests/test_safe_archive.py +144 -0
- garnet_guard-0.1.0a1/tests/test_sarif.py +229 -0
- garnet_guard-0.1.0a1/tests/test_source.py +154 -0
- garnet_guard-0.1.0a1/tests/test_version.py +17 -0
- garnet_guard-0.1.0a1/uv.lock +1539 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: False positive
|
|
3
|
+
about: Garnet warned or blocked on an update that was fine
|
|
4
|
+
labels: false-positive
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
**Package and versions:** <!-- e.g. numpy 2.1.0 → 2.1.1 (PyPI) -->
|
|
8
|
+
|
|
9
|
+
**Finding(s):** <!-- rule IDs and the text Garnet printed -->
|
|
10
|
+
|
|
11
|
+
**Why it's fine:** <!-- e.g. the project moved its release workflow; link to the announcement -->
|
|
12
|
+
|
|
13
|
+
**Garnet version and how you ran it:** <!-- CLI / Action, version -->
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Missed detection
|
|
3
|
+
about: A compromised or suspicious release Garnet didn't flag
|
|
4
|
+
labels: missed-detection
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- If this is a novel bypass attackers could use today, report it privately instead (see SECURITY.md). -->
|
|
8
|
+
|
|
9
|
+
**Package and versions:**
|
|
10
|
+
|
|
11
|
+
**What was wrong with the release:** <!-- links to write-ups -->
|
|
12
|
+
|
|
13
|
+
**What Garnet reported:**
|
|
14
|
+
|
|
15
|
+
**Signal you think should have caught it:**
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: github-actions
|
|
4
|
+
directory: /
|
|
5
|
+
schedule:
|
|
6
|
+
interval: weekly
|
|
7
|
+
cooldown:
|
|
8
|
+
default-days: 7
|
|
9
|
+
groups:
|
|
10
|
+
actions:
|
|
11
|
+
patterns: ["*"]
|
|
12
|
+
commit-message:
|
|
13
|
+
prefix: ci
|
|
14
|
+
|
|
15
|
+
- package-ecosystem: uv
|
|
16
|
+
directory: /
|
|
17
|
+
schedule:
|
|
18
|
+
interval: weekly
|
|
19
|
+
cooldown:
|
|
20
|
+
default-days: 7
|
|
21
|
+
groups:
|
|
22
|
+
dev:
|
|
23
|
+
dependency-type: development
|
|
24
|
+
commit-message:
|
|
25
|
+
prefix: deps
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## What changes
|
|
2
|
+
|
|
3
|
+
<!-- What a user or reviewer would notice. -->
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
<!-- Link the issue. -->
|
|
8
|
+
|
|
9
|
+
## Sensitive paths touched
|
|
10
|
+
|
|
11
|
+
<!-- safe_archive.py, action/, workflows, release config, default policy. Write "none" if none. -->
|
|
12
|
+
|
|
13
|
+
## Rule changes
|
|
14
|
+
|
|
15
|
+
<!-- For rule changes: IDs affected, fixtures added, backtest warn-rate before → after. Otherwise "n/a". -->
|
|
16
|
+
|
|
17
|
+
## AI assistance
|
|
18
|
+
|
|
19
|
+
<!-- Which parts were generated by an agent, if any. -->
|
|
20
|
+
|
|
21
|
+
## Checklist
|
|
22
|
+
|
|
23
|
+
- [ ] Tests, ruff and mypy pass locally
|
|
24
|
+
- [ ] No hard rule in AGENTS.md is broken
|
|
25
|
+
- [ ] Commits are signed off (DCO)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
workflow_call:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
lint:
|
|
18
|
+
name: Lint and typecheck
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
timeout-minutes: 10
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
23
|
+
with:
|
|
24
|
+
persist-credentials: false
|
|
25
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
26
|
+
with:
|
|
27
|
+
python-version: "3.11"
|
|
28
|
+
enable-cache: false
|
|
29
|
+
- run: uv sync --locked
|
|
30
|
+
- run: uv run ruff check .
|
|
31
|
+
- run: uv run ruff format --check .
|
|
32
|
+
- run: uv run mypy src
|
|
33
|
+
- run: uv run lint-imports
|
|
34
|
+
|
|
35
|
+
test:
|
|
36
|
+
name: Tests (Python ${{ matrix.python }})
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
timeout-minutes: 10
|
|
39
|
+
strategy:
|
|
40
|
+
fail-fast: false
|
|
41
|
+
matrix:
|
|
42
|
+
python: ["3.11", "3.12", "3.13", "3.14"]
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
45
|
+
with:
|
|
46
|
+
persist-credentials: false
|
|
47
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
48
|
+
with:
|
|
49
|
+
python-version: ${{ matrix.python }}
|
|
50
|
+
enable-cache: false
|
|
51
|
+
- run: uv sync --locked
|
|
52
|
+
- run: uv run pytest
|
|
53
|
+
|
|
54
|
+
build:
|
|
55
|
+
name: Build distributions
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
timeout-minutes: 10
|
|
58
|
+
steps:
|
|
59
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
60
|
+
with:
|
|
61
|
+
persist-credentials: false
|
|
62
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
63
|
+
with:
|
|
64
|
+
enable-cache: false
|
|
65
|
+
- run: uv build --no-sources
|
|
66
|
+
- name: Smoke-test the built wheel
|
|
67
|
+
run: uvx --isolated --from dist/*.whl garnet --help
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Garnet Guard checks this repository's own dependency pull requests, the same
|
|
2
|
+
# way a user's repository would (action/examples/garnet-guard.yml). Dependabot
|
|
3
|
+
# opens a pull request for uv.lock each week (.github/dependabot.yml).
|
|
4
|
+
#
|
|
5
|
+
# The Action is pinned to a commit on main, not run from the pull request
|
|
6
|
+
# (uses: ./). From the pull request it would install Garnet Guard from the
|
|
7
|
+
# pull request's own uv.lock, so the updated packages would run before they
|
|
8
|
+
# were checked. Move the pin forward by hand after the Action changes, and to
|
|
9
|
+
# a release commit once one is tagged.
|
|
10
|
+
name: Garnet Guard
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
pull_request:
|
|
14
|
+
paths:
|
|
15
|
+
- "uv.lock"
|
|
16
|
+
|
|
17
|
+
permissions: {}
|
|
18
|
+
|
|
19
|
+
concurrency:
|
|
20
|
+
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
|
21
|
+
cancel-in-progress: true
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
garnet-guard:
|
|
25
|
+
name: Check dependency changes
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
timeout-minutes: 10
|
|
28
|
+
permissions:
|
|
29
|
+
contents: read # read the lockfiles and the base commit
|
|
30
|
+
pull-requests: write # post or update the comment
|
|
31
|
+
security-events: write # upload the SARIF report to code scanning
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
34
|
+
with:
|
|
35
|
+
persist-credentials: false
|
|
36
|
+
- uses: orca-solutions/garnet-guard@69c8e01d6ad51d7716fb9f9729e87bc2d21eda2b # main, 2026-10-30
|
|
37
|
+
with:
|
|
38
|
+
fail-on: block
|
|
39
|
+
sarif: true
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishes garnet-guard to PyPI when the maintainer pushes a version tag.
|
|
4
|
+
# Trusted Publishing (OIDC) replaces any stored token, and
|
|
5
|
+
# pypa/gh-action-pypi-publish signs PEP 740 attestations for every file.
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
tags: ["v*"]
|
|
10
|
+
|
|
11
|
+
permissions: {}
|
|
12
|
+
|
|
13
|
+
concurrency:
|
|
14
|
+
group: release
|
|
15
|
+
cancel-in-progress: false
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
checks:
|
|
19
|
+
name: CI
|
|
20
|
+
uses: ./.github/workflows/ci.yml
|
|
21
|
+
permissions:
|
|
22
|
+
contents: read
|
|
23
|
+
|
|
24
|
+
build:
|
|
25
|
+
name: Build
|
|
26
|
+
needs: checks
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
timeout-minutes: 10
|
|
29
|
+
permissions:
|
|
30
|
+
contents: read
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
33
|
+
with:
|
|
34
|
+
persist-credentials: false
|
|
35
|
+
fetch-depth: 0
|
|
36
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
37
|
+
with:
|
|
38
|
+
enable-cache: false
|
|
39
|
+
- name: Check the tag is on main
|
|
40
|
+
run: |
|
|
41
|
+
git fetch --no-tags origin main
|
|
42
|
+
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
|
|
43
|
+
echo "::error::Tag $GITHUB_REF_NAME points at $GITHUB_SHA, which is not on main. Release tags must be cut from main."
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
- name: Check the tag matches the project version
|
|
47
|
+
run: |
|
|
48
|
+
version="$(uv version --short)"
|
|
49
|
+
if [ "$GITHUB_REF_NAME" != "v$version" ]; then
|
|
50
|
+
echo "::error::Tag $GITHUB_REF_NAME does not match the version in pyproject.toml ($version). Bump the version in a PR, merge it, then tag v$version."
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
- run: uv build --no-sources
|
|
54
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
55
|
+
with:
|
|
56
|
+
name: dist
|
|
57
|
+
path: dist/
|
|
58
|
+
if-no-files-found: error
|
|
59
|
+
|
|
60
|
+
publish:
|
|
61
|
+
name: Publish to PyPI
|
|
62
|
+
needs: build
|
|
63
|
+
runs-on: ubuntu-latest
|
|
64
|
+
timeout-minutes: 10
|
|
65
|
+
environment:
|
|
66
|
+
name: pypi
|
|
67
|
+
url: https://pypi.org/project/garnet-guard/
|
|
68
|
+
permissions:
|
|
69
|
+
id-token: write # Trusted Publishing and attestation signing; nothing else
|
|
70
|
+
steps:
|
|
71
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
72
|
+
with:
|
|
73
|
+
name: dist
|
|
74
|
+
path: dist/
|
|
75
|
+
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
76
|
+
with:
|
|
77
|
+
attestations: true
|
|
78
|
+
|
|
79
|
+
verify:
|
|
80
|
+
name: Verify attestations on PyPI
|
|
81
|
+
needs: publish
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
timeout-minutes: 15
|
|
84
|
+
permissions:
|
|
85
|
+
contents: read
|
|
86
|
+
steps:
|
|
87
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
88
|
+
with:
|
|
89
|
+
persist-credentials: false
|
|
90
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
91
|
+
with:
|
|
92
|
+
enable-cache: false
|
|
93
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
94
|
+
with:
|
|
95
|
+
name: dist
|
|
96
|
+
path: dist/
|
|
97
|
+
- run: uv sync --locked
|
|
98
|
+
- name: Verify each published file against this repository
|
|
99
|
+
run: |
|
|
100
|
+
for path in dist/*; do
|
|
101
|
+
file="$(basename "$path")"
|
|
102
|
+
ok=""
|
|
103
|
+
for attempt in 1 2 3 4 5 6; do
|
|
104
|
+
if uv run python -m pypi_attestations verify pypi \
|
|
105
|
+
--repository "https://github.com/$GITHUB_REPOSITORY" "pypi:$file"; then
|
|
106
|
+
ok=1
|
|
107
|
+
break
|
|
108
|
+
fi
|
|
109
|
+
echo "Attestation for $file not verifiable yet (attempt $attempt); retrying in 30s."
|
|
110
|
+
sleep 30
|
|
111
|
+
done
|
|
112
|
+
if [ -z "$ok" ]; then
|
|
113
|
+
echo "::error::Could not verify the PyPI attestation for $file."
|
|
114
|
+
exit 1
|
|
115
|
+
fi
|
|
116
|
+
done
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: OpenSSF Scorecard
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
branch_protection_rule:
|
|
5
|
+
schedule:
|
|
6
|
+
- cron: "23 6 * * 1"
|
|
7
|
+
push:
|
|
8
|
+
branches: [main]
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
permissions: {}
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
analysis:
|
|
15
|
+
name: Scorecard analysis
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
permissions:
|
|
18
|
+
contents: read
|
|
19
|
+
actions: read
|
|
20
|
+
security-events: write # upload results to code scanning
|
|
21
|
+
id-token: write # publish results to the Scorecard API for the badge
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
24
|
+
with:
|
|
25
|
+
persist-credentials: false
|
|
26
|
+
- uses: ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e # v2.5.0
|
|
27
|
+
with:
|
|
28
|
+
results_file: results.sarif
|
|
29
|
+
results_format: sarif
|
|
30
|
+
publish_results: true
|
|
31
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
32
|
+
with:
|
|
33
|
+
name: scorecard-results
|
|
34
|
+
path: results.sarif
|
|
35
|
+
retention-days: 5
|
|
36
|
+
- uses: github/codeql-action/upload-sarif@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
|
37
|
+
with:
|
|
38
|
+
sarif_file: results.sarif
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Maintainer's private operating method; supplied through project instructions, never committed.
|
|
2
|
+
# AGENTS.md and CLAUDE.md are public (Garnet Guard's contributor and agent rules).
|
|
3
|
+
/.claude/
|
|
4
|
+
PR_WORKFLOW.md
|
|
5
|
+
docs/AGENT_ROLES.md
|
|
6
|
+
HANDOFF_TEMPLATES.md
|
|
7
|
+
SOFTWARE_OPERATING_GUIDE.md
|
|
8
|
+
|
|
9
|
+
# Internal, dated infrastructure inventory; never publish with app source.
|
|
10
|
+
/docs/PLATFORM_SNAPSHOT.md
|
|
11
|
+
|
|
12
|
+
# Common
|
|
13
|
+
.DS_Store
|
|
14
|
+
node_modules/
|
|
15
|
+
.env
|
|
16
|
+
.env.*
|
|
17
|
+
!.env.example
|
|
18
|
+
|
|
19
|
+
# Python
|
|
20
|
+
__pycache__/
|
|
21
|
+
*.py[cod]
|
|
22
|
+
.venv/
|
|
23
|
+
dist/
|
|
24
|
+
build/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.pytest_cache/
|
|
27
|
+
.mypy_cache/
|
|
28
|
+
.ruff_cache/
|
|
29
|
+
.coverage
|
|
30
|
+
htmlcov/
|
|
31
|
+
.hypothesis/
|
|
32
|
+
|
|
33
|
+
# Generated by python -m backtest
|
|
34
|
+
backtest/results.json
|
|
35
|
+
|
|
36
|
+
# Personal agent overrides stay local; shared entry points remain tracked.
|
|
37
|
+
CLAUDE.local.md
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Garnet Guard: instructions for coding agents
|
|
2
|
+
|
|
3
|
+
`AGENTS.md` and `CLAUDE.md` are identical copies and must stay identical. Codex reads `AGENTS.md`; Claude Code reads `CLAUDE.md`. If the maintainer's private operating guide (`SOFTWARE_OPERATING_GUIDE.md`, `PR_WORKFLOW.md`, `docs/AGENT_ROLES.md`) is present in your workspace, follow it as well; it is supplied through project instructions, not the repository. Report unavailable guidance instead of claiming it was checked.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
- Install: `uv sync`
|
|
8
|
+
- Dev server: none (CLI and GitHub Action)
|
|
9
|
+
- Focused tests: `uv run pytest tests/<path> -k <name>`; all unit tests: `uv run pytest` (no network); network tests only when asked: `uv run pytest -m network`
|
|
10
|
+
- Lint / typecheck: `uv run ruff check . && uv run ruff format --check . && uv run mypy src && uv run lint-imports`
|
|
11
|
+
- Build: `uv build`
|
|
12
|
+
- Backtest (required for rule changes): `uv run python -m backtest`
|
|
13
|
+
|
|
14
|
+
Run tests, ruff and mypy before saying a change is done.
|
|
15
|
+
|
|
16
|
+
## Start here
|
|
17
|
+
|
|
18
|
+
Read [PROJECT_BRIEF.md](PROJECT_BRIEF.md) and `WORKING_RECORD.md`, then [ARCHITECTURE.md](ARCHITECTURE.md), [docs/SPEC-v1.md](docs/SPEC-v1.md) and [docs/API.md](docs/API.md) before changing code, and [PRINCIPLES.md](PRINCIPLES.md) when a choice is unclear. Read [THREAT_MODEL.md](THREAT_MODEL.md) before touching anything that reads package contents or runs in CI.
|
|
19
|
+
|
|
20
|
+
## Project-specific
|
|
21
|
+
|
|
22
|
+
### What Garnet Guard is
|
|
23
|
+
|
|
24
|
+
A release-diff guard for dependency updates. It compares a new version of a package against the version a project already trusts and reports trust downgrades, risky content changes and known-bad matches.
|
|
25
|
+
|
|
26
|
+
### Hard rules
|
|
27
|
+
|
|
28
|
+
These are not style preferences. A change that breaks one is wrong even if tests pass.
|
|
29
|
+
|
|
30
|
+
1. **Never execute inspected package code.** No `import`, `exec`, `subprocess`, build backend or install of anything Garnet downloads. Parse with `ast` and read files as bytes.
|
|
31
|
+
2. **All archive access goes through `safe_archive.py`.** Never call `zipfile`/`tarfile` extraction elsewhere.
|
|
32
|
+
3. **Checks are pure.** No I/O in `checks/`. Fetching belongs in `ecosystems/`. Respect the import rules in docs/API.md §2.
|
|
33
|
+
4. **"Couldn't check" is never "pass".** Errors surface as findings or a non-zero exit.
|
|
34
|
+
5. **Escape all package-supplied strings** before they reach markdown, SARIF or a terminal.
|
|
35
|
+
6. **Rule IDs are permanent.** Don't renumber or reuse them.
|
|
36
|
+
7. **No new runtime dependency** without an ADR in `docs/decisions/`. Dev dependencies are fine.
|
|
37
|
+
8. **Unit tests use fixtures, not the network.**
|
|
38
|
+
9. **Never weaken a test, skip a test or lower a backtest threshold to get green.**
|
|
39
|
+
|
|
40
|
+
### High-risk paths
|
|
41
|
+
|
|
42
|
+
These are Garnet Guard's high-risk class: changes to them get focused independent review and the maintainer's deliberate review. Call them out at the top of the PR description.
|
|
43
|
+
|
|
44
|
+
- `src/garnet_guard/safe_archive.py` and `src/garnet_guard/net.py`
|
|
45
|
+
- `action/` and `.github/workflows/`
|
|
46
|
+
- release and publishing configuration
|
|
47
|
+
- default policy and rule severities
|
|
48
|
+
|
|
49
|
+
Other routine changes are verified by their author.
|
|
50
|
+
|
|
51
|
+
### How work flows
|
|
52
|
+
|
|
53
|
+
- Work happens on a branch, one issue per branch, and lands through a pull request. Agents open PRs as drafts; only the maintainer merges.
|
|
54
|
+
- Keep PRs small. If a task grows, stop and propose a split in the PR description.
|
|
55
|
+
- A PR that adds or changes a rule includes positive and negative fixtures, `garnet explain` text and the backtest delta.
|
|
56
|
+
- If a requirement is unclear, leave a question in the PR rather than guessing at behavior users will see.
|
|
57
|
+
- Commits are signed (configured signing) and signed off (`git commit -s`, DCO).
|
|
58
|
+
- **No co-author trailers or "generated by" lines in commits.** The commit-msg hook strips them. Disclose agent use in the PR description's "AI assistance" section instead.
|
|
59
|
+
|
|
60
|
+
### Style
|
|
61
|
+
|
|
62
|
+
- **User-facing text** (findings, `explain` text, PR comments, CLI messages) follows the voice rules in docs/brand/brand-kit.md: calm, exact, never "safe", evidence on every line.
|
|
63
|
+
- Python 3.11+, full type hints, `mypy --strict` on `src/`.
|
|
64
|
+
- Dataclasses (frozen) for models. Keep the public API in `garnet_guard/__init__.py` small.
|
|
65
|
+
- Findings must be deterministic: sort output, avoid timestamps in report bodies.
|
|
66
|
+
- Error messages and `explain` text are written for a developer reviewing a dependency PR: say what changed, why it matters, and what to do.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Garnet Guard architecture
|
|
2
|
+
|
|
3
|
+
This describes the engine Garnet is built around and the surfaces that use it. It is the reference for anyone, human or agent, adding code. Decisions behind it are recorded in [docs/decisions/](docs/decisions/). The exact behavior is specified in [docs/SPEC-v1.md](docs/SPEC-v1.md) and the code design in [docs/API.md](docs/API.md); where they differ from this overview, they win.
|
|
4
|
+
|
|
5
|
+
## Shape in one picture
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
lockfile (base) ─┐
|
|
9
|
+
├─► LockfileDiff ─► [ReleasePair, ...]
|
|
10
|
+
lockfile (head) ─┘ │
|
|
11
|
+
▼
|
|
12
|
+
Ecosystem adapter (pypi, npm)
|
|
13
|
+
metadata · artifacts · provenance · tags
|
|
14
|
+
(content-addressed cache)
|
|
15
|
+
│
|
|
16
|
+
▼
|
|
17
|
+
ReleaseFacts(old) ReleaseFacts(new)
|
|
18
|
+
│
|
|
19
|
+
▼
|
|
20
|
+
Checks: trust · content · known-bad ─► [Finding, ...]
|
|
21
|
+
│
|
|
22
|
+
▼
|
|
23
|
+
Policy (garnet.toml) ─► Verdict
|
|
24
|
+
│
|
|
25
|
+
▼
|
|
26
|
+
Reporters: text · JSON · SARIF · PR comment markdown
|
|
27
|
+
│
|
|
28
|
+
┌───────────────────────┬───────┴──────────────┐
|
|
29
|
+
▼ ▼ ▼
|
|
30
|
+
GitHub Action CLI (garnet check) Agent / install hooks
|
|
31
|
+
(month 2) (month 1) (month 3)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Every surface is a thin shell over one library call: `garnet_guard.evaluate(diff, policy) -> Report`.
|
|
35
|
+
|
|
36
|
+
## Core concepts
|
|
37
|
+
|
|
38
|
+
**ReleasePair.** One dependency changing: ecosystem, package name, `old` version (the baseline you trusted) and `new` version. A newly added dependency has no `old`; it gets only known-bad and absolute checks, not diff checks.
|
|
39
|
+
|
|
40
|
+
**Baseline.** In the open-source tool the baseline is simply the version in the base lockfile, i.e. the version this project already runs. If an update skips versions (2.6.1 → 2.6.4), Garnet diffs against 2.6.1 and also reports trust signals for each skipped release, since a compromise can hide in the middle. In v2 the engine also computes **per-package history from the registry** (how the last N releases were published), so the baseline isn't limited to one release; that stays in the free engine (ADR-0009). The hosted app adds what only a service can see: history observed across many organizations.
|
|
41
|
+
|
|
42
|
+
**ReleaseFacts.** Everything Garnet knows about one release, extracted once and cached:
|
|
43
|
+
|
|
44
|
+
- registry metadata (upload time, uploader, yanked status, files and hashes)
|
|
45
|
+
- provenance per file (attestation present, Sigstore identity, source repo, workflow, ref, Trusted Publishing yes/no)
|
|
46
|
+
- source link (repository URL, whether a matching git tag exists and which commit it points to)
|
|
47
|
+
- content inventory per artifact (file list, sizes, hashes, file types, `.pth` files, entry points, build backend, install scripts for npm)
|
|
48
|
+
- a static index of Python modules: top-level statements per module, imports, and calls to sensitive APIs
|
|
49
|
+
|
|
50
|
+
**Check.** A pure function over two `ReleaseFacts` that returns findings. Checks do no I/O; adapters fetch, checks judge. That makes every check unit-testable from fixtures.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
class Check(Protocol):
|
|
54
|
+
id: str # stable rule ID, e.g. "GRN-T001"
|
|
55
|
+
ecosystems: frozenset[str] # {"pypi"}, {"npm"} or both
|
|
56
|
+
def run(self, pair: ReleasePair, old: ReleaseFacts | None,
|
|
57
|
+
new: ReleaseFacts) -> list[Finding]: ...
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Finding.** `rule_id`, `severity` (info / warn / block), `package`, `old`, `new`, `title`, `evidence` (list of file path + line, URL, or attestation field), `explanation` and `remediation`. Findings must be deterministic for the same inputs so PR comments don't churn.
|
|
61
|
+
|
|
62
|
+
**Policy.** `garnet.toml` (or `[tool.garnet]` in `pyproject.toml`) maps rules to severities and holds allow-lists:
|
|
63
|
+
|
|
64
|
+
```toml
|
|
65
|
+
[garnet]
|
|
66
|
+
default = "warn" # warn | block
|
|
67
|
+
|
|
68
|
+
[garnet.rules]
|
|
69
|
+
GRN-T001 = "block" # lost provenance
|
|
70
|
+
GRN-C004 = "off" # new binaries are normal for us
|
|
71
|
+
|
|
72
|
+
[[garnet.ignore]]
|
|
73
|
+
package = "numpy"
|
|
74
|
+
rules = ["GRN-C004"]
|
|
75
|
+
reason = "ships compiled wheels"
|
|
76
|
+
until = "2027-06-30" # ignores expire
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Verdict.** The worst severity across findings after policy: `pass`, `warn` or `block`. Exit codes: 0 pass, 1 warn (only when `--fail-on warn`), 2 block, 3 Garnet error.
|
|
80
|
+
|
|
81
|
+
## Ecosystem adapters
|
|
82
|
+
|
|
83
|
+
Each ecosystem implements one interface, so checks and reporters never touch registry specifics:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
class Ecosystem(Protocol):
|
|
87
|
+
name: str
|
|
88
|
+
def parse_lockfile(self, path: Path) -> dict[str, str]: ... # name -> version
|
|
89
|
+
def release_facts(self, name: str, version: str) -> ReleaseFacts: ...
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**PyPI (month 1).** JSON API for metadata and files, the Integrity API (`/integrity/<project>/<version>/<filename>/provenance`) for PEP 740 attestations, verified with the `pypi-attestations` library. Wheels and the sdist are both inspected, because sdists carry build code. Lockfile parsers: uv.lock, poetry.lock, pylock.toml, pinned requirements.txt with hashes.
|
|
93
|
+
|
|
94
|
+
**npm (month 2).** Registry packument and tarball, npm provenance / Sigstore attestations, `scripts` and `bin` from `package.json`. Lockfile parsers: package-lock.json, pnpm-lock.yaml, yarn.lock.
|
|
95
|
+
|
|
96
|
+
**Source host.** Tag lookups go to the repository named by provenance first, falling back to project URLs in metadata. GitHub only at first, through the public API with an optional token for rate limits.
|
|
97
|
+
|
|
98
|
+
## Initial rule catalog
|
|
99
|
+
|
|
100
|
+
IDs are stable forever. A retired rule keeps its ID reserved. Exact firing conditions and default severities are in SPEC §6.
|
|
101
|
+
|
|
102
|
+
| ID | Rule | Signal |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| GRN-T001 | lost-provenance | Old release had an attestation; new one has none. |
|
|
105
|
+
| GRN-T002 | publisher-changed | Attested source repository changed. |
|
|
106
|
+
| GRN-T003 | workflow-changed | Same repo, different publishing workflow file or environment. |
|
|
107
|
+
| GRN-T004 | no-matching-tag | No git tag for the new version in the attested or declared repo. |
|
|
108
|
+
| GRN-T005 | outside-trusted-publishing | Old files came through Trusted Publishing; new ones were uploaded with a token. |
|
|
109
|
+
| GRN-T006 | files-uploaded-late | A file was added to a release long after the release was first published. |
|
|
110
|
+
| GRN-C001 | new-pth-file | A `.pth` file appears that the old release didn't have. |
|
|
111
|
+
| GRN-C002 | new-build-hook | New `setup.py` logic, custom build backend, or new npm install-time script. |
|
|
112
|
+
| GRN-C003 | new-import-time-exec | New module-level calls to `exec`, `eval`, `compile`, `subprocess`, `os.system`, `ctypes`, sockets or HTTP clients. |
|
|
113
|
+
| GRN-C004 | new-binary | New native binaries or executables not explained by the build (e.g. outside the platform wheel's normal extension modules). |
|
|
114
|
+
| GRN-C005 | obfuscated-blob | New long base64/hex/compressed literals, especially passed to exec or decode. |
|
|
115
|
+
| GRN-C006 | sdist-wheel-mismatch | Wheel contains files with no counterpart in the sdist. |
|
|
116
|
+
| GRN-C007 | code-not-in-source | A Python file in the release isn't the one at its git tag and has sensitive calls the source lacks (code injected by the build, as in ultralytics 8.3.42). |
|
|
117
|
+
| GRN-T007 | invalid-attestation | An attestation exists but fails verification (blocks by default). |
|
|
118
|
+
| GRN-T008 | hash-mismatch | A download doesn't match the registry's published hash (blocks by default). |
|
|
119
|
+
| GRN-K001 | known-malicious | OSV `MAL-` advisory for this package version. |
|
|
120
|
+
| GRN-E001–E005 | couldn't-check | Fetch, parse or budget failures, unpinned or unsupported sources. Reported, never silently passed. |
|
|
121
|
+
|
|
122
|
+
Heuristic content rules run **only on files that changed** between the two releases. That is the main lever on false positives. GuardDog's Semgrep/YARA rules are a useful reference for patterns, with their licenses respected.
|
|
123
|
+
|
|
124
|
+
## Safety of the engine itself
|
|
125
|
+
|
|
126
|
+
Garnet handles hostile input by design. These are invariants, enforced in code review and tests:
|
|
127
|
+
|
|
128
|
+
- **No execution.** Nothing from an inspected package is ever imported, run, built or installed. Python source is parsed with `ast` only.
|
|
129
|
+
- **Safe extraction.** Archives are read in memory or into a private temp dir with limits on total size, file count, compression ratio and path depth. Absolute paths, `..` segments, symlinks, hard links and device files are rejected.
|
|
130
|
+
- **Bounded work.** Timeouts on every network call, and a per-package budget; on a budget overrun Garnet reports an error finding rather than silently passing.
|
|
131
|
+
- **Fail visible.** If Garnet can't fetch or verify something, it says so in the report. It never turns "couldn't check" into "pass".
|
|
132
|
+
|
|
133
|
+
See [THREAT_MODEL.md](THREAT_MODEL.md) for the full analysis.
|
|
134
|
+
|
|
135
|
+
## Caching and determinism
|
|
136
|
+
|
|
137
|
+
Artifacts and facts are cached by content hash under the user cache dir (and `actions/cache` in CI). Facts for a given `(ecosystem, name, version)` are immutable once a release is published, except for yanks and late file uploads, which are re-checked. Outputs sort findings deterministically.
|
|
138
|
+
|
|
139
|
+
## Surfaces
|
|
140
|
+
|
|
141
|
+
**CLI.** `garnet check --base <lockfile> --head <lockfile>`, `garnet explain <rule>`, `garnet diff <pkg> <old> <new>`. Run with `uvx garnet-guard` or install with `uv tool install garnet-guard`.
|
|
142
|
+
|
|
143
|
+
**GitHub Action (month 2).** Runs on `pull_request` when a lockfile changes, compares the base and head lockfiles, and posts or updates one sticky comment plus an optional SARIF upload. Design constraints:
|
|
144
|
+
|
|
145
|
+
- Never use `pull_request_target` with a checkout of PR code.
|
|
146
|
+
- Least privilege: `contents: read`, and `pull-requests: write` only in the step that comments.
|
|
147
|
+
- Dependabot-triggered runs get a restricted token. If commenting directly isn't possible, use the two-workflow pattern: the analysis job uploads the report as an artifact, and a `workflow_run` job with write permission posts it without checking out PR code. *Verify current GitHub behavior before building.*
|
|
148
|
+
- The Action is pinned by commit SHA in all docs, and its own dependencies are hash-locked.
|
|
149
|
+
|
|
150
|
+
**Install wrapper and agent hooks (month 3).** `garnet pip install …` / `garnet uv sync` resolve first, run `evaluate` on the resulting change, then hand off to the real tool. Claude Code and Codex hooks call the same entry point before a dependency is added.
|
|
151
|
+
|
|
152
|
+
## Package layout
|
|
153
|
+
|
|
154
|
+
See [docs/API.md](docs/API.md) §9 for the full layout and §2 for the import rules between layers. The distribution is `garnet-guard` and the import package is `garnet_guard` (ADR-0007). The CLI command is `garnet`.
|
|
155
|
+
|
|
156
|
+
## Backtesting
|
|
157
|
+
|
|
158
|
+
`backtest/` replays known incidents (47 malicious releases of 36 compromised PyPI packages, listed in `backtest/incidents.toml`, and four npm account takeovers in `backtest/npm-incidents.toml`) and a clean corpus of recent releases of the top ~500 PyPI packages. Every rule change runs against it. Two numbers gate releases: detection on the incident set and warn rate on the clean corpus. Where an incident's malicious artifacts were removed from the registry, the replay uses recorded `ReleaseFacts` from public write-ups rather than the files.
|