Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/advisory-exceptions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"version": 1,
"exceptions": []
}
16 changes: 15 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
# Dependabot configuration: three ecosystems, grouped except for uv-build.
# Dependabot configuration: four ecosystems, grouped except for uv-build.
# Auto-merge (dependabot-auto-merge.yml) supplies the ruleset's guarded bot approval
# and squash-merges after Validate is green. Human-authored PRs still require human
# approval. auto-fix.yml mechanically repairs lint-only failures.
Expand Down Expand Up @@ -73,3 +73,17 @@ updates:
- "dependencies"
- "github-actions"
open-pull-requests-limit: 10

# The native OSV CLI container owns one immutable scanner pin. Keeping it here
# lets the same review, cooldown, and required-check policy maintain the scanner.
- package-ecosystem: "docker"
directory: "/.github/security-scanner"
schedule:
interval: "daily"
cooldown:
default-days: 7
commit-message:
prefix: "ci"
labels:
- "dependencies"
open-pull-requests-limit: 10
3 changes: 3 additions & 0 deletions .github/osv-scanner-empty.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Raw scans never apply advisory-wide or package-wide suppression.
# Exact temporary risk acceptances live in advisory-exceptions.json and are
# adjudicated against complete raw results by scripts/advisory_policy.py.
2 changes: 2 additions & 0 deletions .github/security-scanner/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Single scanner version owner; Dependabot's docker ecosystem maintains this pin.
FROM ghcr.io/google/osv-scanner:v2.6.0@sha256:afd838850ac1a0fcc15ff4a041dc9ba11123c3f0d2666217a5f0fcf9222b55fa
68 changes: 66 additions & 2 deletions .github/workflows/auto-fix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,74 @@ jobs:
github-token: ${{ github.token }}
path: fixes

- name: Read the trusted PR file tree
if: steps.guard.outputs.skip == 'false' && steps.download.outcome == 'success'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
REPO: ${{ github.repository }}
run: gh api "repos/$REPO/git/trees/$HEAD_SHA?recursive=1" > trusted-tree.json

- name: Validate mechanical fix scope
id: scope
if: steps.guard.outputs.skip == 'false' && steps.download.outcome == 'success'
# This code comes from the trusted base workflow. Never load a checker
# from the PR artifact or checkout before minting a write credential.
shell: python
run: |
# BEGIN AUTOFIX SCOPE GUARD
import json
import os
from pathlib import Path

fixes = json.loads(Path("fixes/fixes.json").read_text(encoding="utf-8"))
tree = json.loads(Path("trusted-tree.json").read_text(encoding="utf-8"))
if tree.get("truncated") is not False:
raise ValueError("Cannot verify a partial PR file tree")
tracked = {
item["path"]: item["mode"]
for item in tree["tree"]
if item["type"] == "blob"
}
protected_files = {
".github/advisory-exceptions.json",
".github/osv-scanner-empty.toml",
"pyproject.toml",
}
protected_prefixes = (
".github/workflows/",
".github/security-scanner/",
"scripts/",
)
if not isinstance(fixes, list):
raise ValueError("Mechanical fixes must be a JSON array")
for entry in fixes:
if not isinstance(entry, dict):
raise ValueError("Malformed mechanical fix entry")
path = entry.get("path")
if (
not isinstance(path, str)
or "\\" in path
or any(part in {"", ".", ".."} for part in path.split("/"))
or path not in tracked
or entry.get("mode") != tracked[path]
or entry.get("status") != "M"
or entry.get("mode") not in {"100644", "100755"}
or path in protected_files
or path.startswith(protected_prefixes)
):
raise ValueError(
f"Refusing auto-fix of protected, new, or noncanonical path: {path!r}. "
"A maintainer must review policy and automation changes."
)
with Path(os.environ["GITHUB_OUTPUT"]).open("a", encoding="utf-8") as output:
output.write("safe=true\n")
# END AUTOFIX SCOPE GUARD

- name: Mint a GitHub App token
id: app-token
# Only mint a write-capable token once we actually have fixes to push.
if: steps.guard.outputs.skip == 'false' && steps.download.outcome == 'success'
if: steps.scope.outputs.safe == 'true'
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.APP_CLIENT_ID }}
Expand All @@ -85,7 +149,7 @@ jobs:
permission-contents: write

- name: Rebuild the fix commit via the Git Data API and update the branch
if: steps.guard.outputs.skip == 'false' && steps.download.outcome == 'success'
if: steps.scope.outputs.safe == 'true'
env:
# gh api authenticates as the App installation (Contents: write).
GH_TOKEN: ${{ steps.app-token.outputs.token }}
Expand Down
46 changes: 39 additions & 7 deletions .github/workflows/security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,44 @@ jobs:
- name: Run the full prek gate
run: uv run prek run --all-files --show-diff-on-failure --color always

- name: uv audit (advisory scan of uv.lock)
run: uv audit --preview-features audit
# Run advisory scans in a fresh job: test/pre-commit code in full-gate must not
# be able to mutate the policy, adapters, or source tree used by the scanners.
deps-security:
name: deps-security
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Harden the runner
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit

- name: Check out the repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "0.11.29"
# gate owns the shared cache write; parallel saves race on the same key.
save-cache: false

- name: Sync the environment
env:
UV_MALWARE_CHECK: "1"
run: uv sync --locked

- name: Audit dependencies with the bounded no-fix policy
run: bash scripts/audit-dependencies.sh

- name: OSV-Scanner (repo-wide)
uses: google/osv-scanner-action/osv-scanner-action@a345acffa64b0eaede81a3d9aae6141214d9c8fc # v2.6.0
- name: Preserve raw dependency audit evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
scan-args: |-
--recursive
./
name: dependency-audit-${{ github.job }}
path: ${{ runner.temp }}/optimizer-dependency-audit/
retention-days: 14
if-no-files-found: warn
16 changes: 9 additions & 7 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,17 @@ jobs:
UV_MALWARE_CHECK: "1"
run: uv sync --locked

- name: uv audit (advisory scan of uv.lock)
run: uv audit --preview-features audit
- name: Audit dependencies with the bounded no-fix policy
run: bash scripts/audit-dependencies.sh

- name: OSV-Scanner (repo-wide)
uses: google/osv-scanner-action/osv-scanner-action@a345acffa64b0eaede81a3d9aae6141214d9c8fc # v2.6.0
- name: Preserve raw dependency audit evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
scan-args: |-
--recursive
./
name: dependency-audit-${{ github.job }}
path: ${{ runner.temp }}/optimizer-dependency-audit/
retention-days: 14
if-no-files-found: warn

# Conventional-Commit PR-title lint. The squash merge uses the PR title as the
# release-relevant commit message, so the title must be a valid Conventional
Expand Down
10 changes: 10 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,16 @@ repos:
pass_filenames: false
always_run: true

# Validate the exception contract offline as part of the same local/CI gate.
- repo: local
hooks:
- id: advisory-policy
name: bounded advisory exception policy
language: system
entry: uv run python -m scripts.advisory_policy
pass_filenames: false
always_run: true

# -- Security related hooks --------------------------------------------
- repo: https://github.com/PyCQA/bandit
rev: 92ae8b82fb422a639f0ed8d99e96cea769594e08 # frozen: 1.9.4
Expand Down
57 changes: 56 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ on four automation behaviors:
[`CHANGELOG.md`](CHANGELOG.md), tag, and cut a GitHub Release. Publishing to PyPI (OIDC
trusted publishing with PEP 740 attestations) is gated behind the `PUBLISH_TO_PYPI` repo
variable; while it is off, the release path still builds and validates the artifact.
- **Dependabot** (`.github/dependabot.yml`) opens cooldown-gated PRs daily for three
- **Dependabot** (`.github/dependabot.yml`) opens cooldown-gated PRs daily for four
ecosystems: grouped `uv` dev tools, grouped `pre-commit` hook revisions, and grouped
`github-actions` pins. `uv-build` stays in its own PR because Dependabot cannot refresh
its build-system range reliably inside a group.
Expand All @@ -68,3 +68,58 @@ on four automation behaviors:

The configuration mirrors the local gate exactly, so "green locally" and "green in CI" mean
the same thing. For the agent-facing execution contract, see `AGENTS.md`.

## Temporary dependency risk acceptance

Both required dependency scanners use the same empty-by-default policy in
[`.github/advisory-exceptions.json`](.github/advisory-exceptions.json). This is an
escape hatch for an explicitly reviewed, short-lived **no-fix** case. Prefer a
canonical dependency or lockfile update whenever a fixed release is available.
Neither Dependabot nor the mechanical auto-fixer may create or extend an acceptance.
The privileged auto-fix consumer prevents direct protected-file writes and verifies paths against the actual PR file tree
before obtaining its write token, and refuses policy, scanner, workflow, packaging,
new-file, symlink, or mode changes. Ordinary existing-file formatting and lockfile
repairs remain eligible. This guard does not prove that arbitrary proposed source
changes are mechanically generated. Both PR and weekly dependency audits run in
a separate fresh job from tests and pre-commit hooks.

Each entry must identify one exact GHSA advisory, `PyPI` package, and version
already in `uv.lock`, plus an exclusive UTC `expiresAt`, `reason`, `noFixReason`,
and a credential-free HTTPS `upstream` reference. Accepting a vulnerability is a
maintainer risk decision; a green accepted-risk result does not mean it is fixed.
The current policy contains no accepted risks.

The PR and weekly checks run `scripts/audit-dependencies.sh`:

1. Run the pinned `uv audit` once without local ignore configuration and with
`--locked`; preserve its JSON, diagnostic output, and numeric exit status
2. Run the official, digest-pinned OSV CLI once against the repository recursively,
with a read-only source mount, an explicitly empty config, and all-package JSON
3. Validate each original report independently against the same exact-scope policy
4. Fail on operational errors, missing/incomplete/malformed results, unknown
findings, unrelated packages or versions, available fixes, stale or expired
entries
5. Report every accepted risk prominently, including its expiry and upstream
reference, and retain raw reports as the `dependency-audit-*` CI artifact

uv's adverse package statuses retain their native warning behavior and remain
visible outside the vulnerability-exception mechanism. The existing
`UV_MALWARE_CHECK=1` enforcement during environment sync remains unchanged.

There is no advisory-wide ignore flag, severity floor, second filtered scan, or
blanket `continue-on-error` gate. Unknown scanner-format changes fail closed and
require a tested adapter update. The official OSV CLI digest has a single owner in
[`.github/security-scanner/Dockerfile`](.github/security-scanner/Dockerfile), which
Dependabot maintains under the existing review and required-check policy.

The offline policy contract runs in the normal `prek` gate. To reproduce both
online scanners locally, install Docker and run:

```bash
uv sync --locked
bash scripts/audit-dependencies.sh
```

Reports are written under `${RUNNER_TEMP:-${TMPDIR:-/tmp}}/optimizer-dependency-audit`.
Use the raw reports to investigate a failure; never change a scanner exit code or
weaken a required check to make an update merge.
9 changes: 9 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,12 @@ the automation itself (workflow token scope, SHA-pinning, OIDC publishing) are i

Only the latest released version is supported. Fixes ship as new releases rather than as
patches to older versions.

## Dependency advisory policy

Required PR and weekly scans use an empty-by-default, exact-scope no-fix policy.
An exception requires explicit maintainer risk acceptance for a named advisory,
locked package/version, rationale, upstream evidence, and a UTC expiry. New,
malformed, unrelated, fixable, or expired findings remain blocking, and raw scanner
results stay visible. See [the contributor guide](CONTRIBUTING.md#temporary-dependency-risk-acceptance)
for the contract and reproduction commands.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ addopts = "-q --cov --cov-report=term-missing"

[tool.coverage.run]
branch = true
source = ["skill_optimizer", "optimize_description_v2"]
source = ["skill_optimizer", "optimize_description_v2", "scripts"]

[tool.coverage.report]
show_missing = true
Expand Down
1 change: 1 addition & 0 deletions scripts/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Repository maintenance policy checks."""
Loading
Loading