Skip to content
This repository was archived by the owner on May 12, 2026. It is now read-only.
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
13 changes: 7 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,6 @@ jobs:
with:
fetch-depth: 0

- name: Verify action SHA pins are commits
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: bash scripts/verify-action-pins.sh

- name: Resolve tag
id: tag
shell: bash
Expand All @@ -43,6 +37,13 @@ jobs:
fi
echo "tag=${REF}" >> "$GITHUB_OUTPUT"

- name: Verify action SHA pins and scan.yml lockstep
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EXPECTED_RELEASE_TAG: ${{ steps.tag.outputs.tag }}
shell: bash
run: bash scripts/verify-action-pins.sh

- name: Self-scan against the tagged ref
id: scan
uses: ./
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- id: scan
uses: ob-aion/pruner@0.2.14
uses: ob-aion/pruner@0.2.15
with:
fail-on: ${{ inputs.fail-on }}
skill-pattern: ${{ inputs.skill-pattern }}
Expand Down
21 changes: 21 additions & 0 deletions .github/workflows/self-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,24 @@ jobs:
name: pruner-self-scan-report
path: .pruner/
retention-days: 14

# Fixture job. Trips sub-critical findings; green outcome asserts
# `pruner scan` exit 1 threads through `coroboros-pack-run` and the gate
# step without being masked as a workflow failure (0.2.8 bug class).
# The `pruner-self-scan` job above scans `.` with `.pruner-ignore.yml`
# applied — every Pruner-side fixture finding sits in the allowlist, so
# `pruner scan` exits 0 and the propagation contract goes untested there.
pruner-finding-positive:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- id: scan
uses: ./
with:
fail-on: critical
target-path: examples/finding-positive-skill
skill-pattern: SKILL.md
report-output: ./.pruner-finding-positive
22 changes: 22 additions & 0 deletions .pruner-ignore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,25 @@ ignores:
- rule: PI-PERM-001
path: examples/vulnerable-skill/SKILL.md
justification: "Intentional vulnerable-skill fixture — allowed-tools mismatch with sibling scripts demonstrates PI-PERM-001."

# finding-positive-skill — intentional fixture. The path-rooted entries below
# suppress the findings in `pruner-self-scan` (target-path: `.`, paths land as
# `examples/finding-positive-skill/SKILL.md`). The `pruner-finding-positive`
# job scans `examples/finding-positive-skill` directly, so finding paths are
# bare `SKILL.md` and the entries do not match — findings fire there, exit-code
# propagation gets exercised.
- rule: FC001
path: examples/finding-positive-skill/SKILL.md
justification: "Intentional finding-positive-skill fixture — non-kebab-case name trips FC001 to keep `pruner scan` exit 1 wired through `coroboros-pack-run`."

- rule: FC003
path: examples/finding-positive-skill/SKILL.md
justification: "Intentional finding-positive-skill fixture — top-level `custom_field` trips FC003 for the same exit-code-propagation purpose."

- rule: FC004
path: examples/finding-positive-skill/SKILL.md
justification: "Intentional finding-positive-skill fixture — `metadata.version` trips FC004 for the same exit-code-propagation purpose."

- rule: FC005
path: examples/finding-positive-skill/SKILL.md
justification: "Intentional finding-positive-skill fixture — non-SPDX license string trips FC005 for the same exit-code-propagation purpose."
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## v0.2.15 - 12/05/2026

Two CI additions close bug classes that the 0.2.7 and 0.2.8 releases exposed. Both stayed open through the 0.2.13 → 0.2.14 revert cycle. Neither validates cross-repo reusable-workflow behaviour from inside Pruner's own CI — a real consumer PR remains the only honest surface for that. Both target a specific bug class rather than a generic CI gap.

- **`scripts/verify-action-pins.sh` — pre-tag lockstep check.** Opt-in extension activated when `EXPECTED_RELEASE_TAG` is set in the script's environment. Reads `.github/workflows/scan.yml`, extracts the `uses: ob-aion/pruner@<X.Y.Z>` literal, and exits 1 if the literal does not match the tag being cut. Script header documents the env-var contract. Closes the 0.2.7 bug class at the release-pipeline gate, before any release asset is built. Four consecutive releases (0.2.3-0.2.6) had shipped `scan.yml` pinned to the previous composite.
- **`.github/workflows/release.yml` — tag resolution moved ahead of pin verification.** The `Resolve tag` step now precedes the renamed `Verify action SHA pins and scan.yml lockstep` step, which receives the tag via `EXPECTED_RELEASE_TAG`. No behavioural change to the SHA-pin loop or any downstream step.
- **`examples/finding-positive-skill/` — new fixture.** Minimal skill trips four sub-critical Coroboros pack rules. FC001 high: `name: "Bad Name"` fails kebab-case. FC003 medium: top-level `custom_field` (must live under `metadata:`). FC004 low: `metadata.version` present (Coroboros house rule forbids it). FC005 low: `license: "Apache 2"` is not a valid SPDX identifier. Zero critical by design. Companion `EXPECTATIONS.md` documents the cross-walk. Distinct from `examples/vulnerable-skill/`: 21 findings, 7 critical, every finding allowlisted in `.pruner-ignore.yml`, validating the full Coroboros + Cisco detection surface.
- **`.github/workflows/self-scan.yml` — `pruner-finding-positive` job added.** Invokes the composite via `uses: ./` against `examples/finding-positive-skill` at `fail-on: critical`. Green outcome asserts that `pruner scan` exit 1 threads through `coroboros-pack-run` and the gate step. Workflow stays green even though findings exist (all below threshold). The pre-existing `pruner-self-scan` job scans `.` from repo root with `.pruner-ignore.yml` applied. Every Pruner-side fixture finding sat in the allowlist, so `pruner scan` exited 0. The 0.2.8 bug never surfaced on the maintainer side until the first consumer integration on `coroboros/agent-skills`. Branch protection on `main` requires `pruner-self-scan` already. Promoting `pruner-finding-positive` to a required status check is a one-line `gh api` follow-up after the first green run on `main`.
- **`.pruner-ignore.yml` — four entries added for the new fixture.** Path-rooted entries (`examples/finding-positive-skill/SKILL.md`) suppress the findings inside `pruner-self-scan` so the Security tab and PR review surface stay clean. The new `pruner-finding-positive` job scans the fixture directly, so finding paths are bare `SKILL.md` and the entries do not match there — findings fire, exit-code propagation gets exercised. Same pattern as the 14 vulnerable-skill entries already in the file.
- **`.github/workflows/scan.yml` — synced to `ob-aion/pruner@0.2.15`** per the lockstep contract, now verified pre-tag by `scripts/verify-action-pins.sh`.

## v0.2.14 - 12/05/2026

Hotfix that reverts the 0.2.13 `scan.yml` structural fix. The new pattern parsed `github.workflow_ref` and `github.workflow_sha` to derive the Pruner repo + SHA, then `actions/checkout`'d that into `.pruner-action` and invoked the composite via `uses: ./.pruner-action`. The pattern passed Pruner's own PR-only `test-scan-yml.yml` validator because both context variables resolve to the workflow's hosting repo when the reusable workflow is called locally (`uses: ./.github/workflows/scan.yml`). It failed on the first real cross-repo consumer call (`coroboros/agent-skills@scan.yml@0.2.13`) because under remote invocation, **the entire `github.*` context refers to the caller's workflow, not the called reusable workflow** — `WORKFLOW_REF=coroboros/agent-skills/.github/workflows/ci.yml@refs/pull/29/merge`, `FALLBACK_REPOSITORY=coroboros/agent-skills`. The second checkout therefore cloned `agent-skills` (no `action.yml`) and `uses: ./.pruner-action` errored. GHA docs are explicit on this: *"The `github` context, with the exception of `github.token`, references the calling workflow."* No documented context exposes the called reusable workflow's own repo/ref/sha; expressions in `uses:` action refs are not supported (`actions/runner#1493`). The pattern is structurally unfixable. Scorecard documents the same chicken-and-egg in its `RELEASE.md` and lives with the manual two-step. Pruner does the same starting here.
Expand Down
30 changes: 30 additions & 0 deletions docs/sha-pinning.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,33 @@ bash scripts/verify-action-pins.sh
```

Dependabot itself resolves to commit SHAs correctly when it opens a bump PR, so the bug class is mostly introduced by manual pins. The pre-flight catches both paths.

## Pre-tag lockstep check

The same script gates one more invariant when `EXPECTED_RELEASE_TAG` is set in its environment. `.github/workflows/scan.yml` self-references the composite via `uses: ob-aion/pruner@<X.Y.Z>`. That literal must match the tag being cut, or consumers pinning `scan.yml@<X.Y.Z>` end up running an earlier composite internally (the 0.2.7 bug class — four consecutive releases shipped out of lockstep).

`release.yml` resolves the tag first and exports it to the verifier:

```yaml
- name: Resolve tag
id: tag
shell: bash
run: |
REF="${GITHUB_REF#refs/tags/}"
echo "tag=${REF}" >> "$GITHUB_OUTPUT"

- name: Verify action SHA pins and scan.yml lockstep
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EXPECTED_RELEASE_TAG: ${{ steps.tag.outputs.tag }}
shell: bash
run: bash scripts/verify-action-pins.sh
```

Local invocation before tagging:

```bash
EXPECTED_RELEASE_TAG=0.2.15 bash scripts/verify-action-pins.sh
```

Exit codes: `0` OK, `1` bad SHA pin or lockstep mismatch, `2` missing `gh` CLI or missing `scan.yml`. Running the script without the env var keeps the SHA-only behaviour — useful for routine Dependabot review.
4 changes: 2 additions & 2 deletions docs/threat-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,5 @@ Pruner's own attack surface is small but real:
- **Wrapper package (`pruner-wrapper`).** Runtime deps are `pyyaml` and `cisco-ai-skill-scanner` (the pinned engine). Stdlib otherwise. The dependency surface is reviewable in `wrapper/pyproject.toml`. Dependabot tracks bumps; CODEOWNERS-required review.
- **Cisco engine.** Apache-2.0, multi-thousand-LOC scanner. Pinned at `2.0.9`. License-drift check runs at install ([`scripts/setup-cisco.sh`](../scripts/setup-cisco.sh)) and halts the action if the upstream license marker changes. Monthly cron probe at [`.github/workflows/cisco-upstream-check.yml`](../.github/workflows/cisco-upstream-check.yml) opens an `upstream-drift` issue on archival, license change, or 90-day staleness.
- **Composite action.** Every `uses:` line is SHA-pinned with a `# v1.2.3` comment for human-readable diff review. No JS / no compiled `dist/`.
- **Self-scan.** Pruner scans Pruner on every push ([`.github/workflows/self-scan.yml`](../.github/workflows/self-scan.yml)). A failing self-scan blocks merge.
- **Release integrity.** [`release.yml`](../.github/workflows/release.yml) re-runs the full scan against the tagged ref. Drift between `main` and the tag fails the release.
- **Self-scan.** Pruner scans Pruner on every push ([`.github/workflows/self-scan.yml`](../.github/workflows/self-scan.yml)). Two jobs: `pruner-self-scan` against the repo root (broad coverage), and `pruner-finding-positive` against [`examples/finding-positive-skill/`](../examples/finding-positive-skill/) (exit-code propagation under `fail-on: critical`). A failing job blocks merge.
- **Release integrity.** [`release.yml`](../.github/workflows/release.yml) re-runs the full scan against the tagged ref. Drift between `main` and the tag fails the release. [`scripts/verify-action-pins.sh`](../scripts/verify-action-pins.sh) gates the release on two invariants: every SHA-pinned `uses:` line resolves to a real commit (not a tag-object SHA), and `scan.yml`'s `uses: ob-aion/pruner@<X.Y.Z>` matches the tag being cut.
29 changes: 29 additions & 0 deletions examples/finding-positive-skill/EXPECTATIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Finding-positive skill — expected findings

When `pruner scan examples/finding-positive-skill --without-cisco` runs, the
Coroboros pack should report exactly the following findings. Zero critical
by design.

| Rule | File | Severity | Why |
|---|---|---|---|
| FC001 | `SKILL.md` | high | `name` is `"Bad Name"` — contains a space, fails kebab-case. |
| FC003 | `SKILL.md` | medium | `custom_field` at top level (must live under `metadata:`). |
| FC004 | `SKILL.md` | low | `metadata.version` present (Coroboros house rule forbids it). |
| FC005 | `SKILL.md` | low | `license: "Apache 2"` is not a valid SPDX identifier (correct: `Apache-2.0`). |

The fixture keeps `pruner scan` exit 1 threaded through the composite action under `fail-on: critical`. Workflow exit 0 is the expected outcome.

## Dual-path allowlist behaviour

The repo-root `.pruner-ignore.yml` lists each of the four findings keyed by the path `examples/finding-positive-skill/SKILL.md`. Two scan contexts read that file:

| Job | `target-path` | Finding path | Allowlist match | Findings |
|---|---|---|---|---|
| `pruner-self-scan` | `.` | `examples/finding-positive-skill/SKILL.md` | yes | suppressed (Security tab + PR review stay clean) |
| `pruner-finding-positive` | `examples/finding-positive-skill` | `SKILL.md` | no | fire (exit-code propagation gets exercised) |

The path-based asymmetry is intentional and mirrors the existing 14 `examples/vulnerable-skill/` entries.

## Distinct from vulnerable-skill

`examples/vulnerable-skill/` trips 7 critical findings (PI-UNI-001 / PI-UNI-003 weight-locked at 1.00; PI-IDFILE-001 and PI-EXFIL-002 on `scripts/`) and validates the full Coroboros + Cisco detection surface. This fixture trips four sub-critical findings and validates a single composite-action contract.
16 changes: 16 additions & 0 deletions examples/finding-positive-skill/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
name: "Bad Name"
description: "Finding-positive fixture used by self-scan.yml to catch the 0.2.8 exit-code bug class. Trips four sub-critical Coroboros pack rules."
metadata:
version: "1.0.0"
license: "Apache 2"
custom_field: "bar"
---

# Finding-positive

Trips Coroboros pack findings strictly below the `critical` threshold.

The `pruner-finding-positive` job in `.github/workflows/self-scan.yml` invokes the composite via `uses: ./` against this fixture at `fail-on: critical`. Green outcome asserts the workflow stays green. `pruner scan` exit 1 (findings present, below threshold) must thread through `coroboros-pack-run` and the gate step. The 0.2.8 bug class.

See `EXPECTATIONS.md` for the rule cross-walk.
31 changes: 29 additions & 2 deletions scripts/verify-action-pins.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#!/usr/bin/env bash
# Verify every SHA-pinned `uses:` line resolves to a real commit, not a
# tag-object SHA — Scorecard rejects the latter as "imposter commit".
# See docs/sha-pinning.md. Exit: 0 = OK, 1 = bad pin, 2 = missing gh.
# tag-object SHA — Scorecard rejects the latter as "imposter commit". When
# EXPECTED_RELEASE_TAG is set, also verify scan.yml's `uses: ob-aion/pruner@<X>`
# matches the tag being cut (closes the 0.2.7 lockstep-skip bug class).
# See docs/sha-pinning.md. Exit: 0 = OK, 1 = bad pin, 2 = missing gh or scan.yml.

set -euo pipefail

Expand Down Expand Up @@ -44,4 +46,29 @@ while IFS= read -r line; do
done < <(grep -nHE 'uses:[[:space:]]+[^/]+/[^@]+@[a-f0-9]{40}' "${FILES[@]}" 2>/dev/null || true)

printf '\n%d pin(s) verified, %d failure(s).\n' "$PASS" "$FAIL"

# Lockstep check (fires when EXPECTED_RELEASE_TAG is set): scan.yml's
# `uses: ob-aion/pruner@<X>` must match the tag being cut.
if [ -n "${EXPECTED_RELEASE_TAG:-}" ]; then
SCAN_YML=".github/workflows/scan.yml"
if [ ! -f "$SCAN_YML" ]; then
echo "FATAL: ${SCAN_YML} not found" >&2
exit 2
fi
PIN=$(grep -nE 'uses:[[:space:]]+ob-aion/pruner@[0-9]+\.[0-9]+\.[0-9]+' "$SCAN_YML" \
| head -1 \
| sed -E 's/.*uses:[[:space:]]+ob-aion\/pruner@([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
if [ -z "$PIN" ]; then
echo "FATAL: no ob-aion/pruner@<X.Y.Z> pin found in ${SCAN_YML}" >&2
exit 2
fi
if [ "$PIN" = "$EXPECTED_RELEASE_TAG" ]; then
printf 'OK lockstep: scan.yml pins ob-aion/pruner@%s = %s\n' "$PIN" "$EXPECTED_RELEASE_TAG"
else
printf 'BAD lockstep: scan.yml pins ob-aion/pruner@%s but releasing %s\n' \
"$PIN" "$EXPECTED_RELEASE_TAG" >&2
RC=1
fi
fi

exit "${RC}"
Loading