aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
267 lines (187 loc) • 9.7 kB
Markdown
---
namespace: aiwg
name: ci-workflow-audit
platforms: [all]
description: Audit CI workflow files for supply-chain risk — unpinned actions, unpinned container images, pull_request-triggered jobs with secret access, curl-pipe-shell installers, and bare :latest tags. Produces a structured markdown report with file:line refs.
---
# ci-workflow-audit
Use this skill when a user wants a one-shot scan of their CI workflows
for the supply-chain attack surface that the [ci-action-pinning rule](../../rules/ci-action-pinning.md)
defines as forbidden. The rule is the gate (blocks new violations
post-deployment); this skill is the discovery tool (surfaces existing
violations in a user repo).
Pairs with `npm-supply-chain-audit` (npm-ecosystem audit) — together
they cover most of the user-side risk surface that AIWG's own
supply-chain hardening (`v2026.5.3`) addresses on the AIWG side.
## Triggers
- "audit workflow pinning"
- "ci workflow audit"
- "are my actions pinned"
- "check container image pins"
- "scan workflows for secrets"
- "supply chain risk in CI"
## Scope
Audits the following workflow file locations (all supported in parallel):
| Platform | Path |
|---|---|
| GitHub Actions | `.github/workflows/*.yml`, `.github/workflows/*.yaml` |
| Gitea Actions | `.gitea/workflows/*.yml`, `.gitea/workflows/*.yaml` |
| GitLab CI | `.gitlab-ci.yml`, `.gitlab/*.yml` (where applicable) |
| Reusable workflows | `uses: ./.github/workflows/*` references (transitive) |
Read-only. No mutations.
## Audit sequence
### 1. Inventory workflow files
```bash
find .github/workflows .gitea/workflows -type f \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null
test -f .gitlab-ci.yml && echo ".gitlab-ci.yml"
```
Record the count. Each subsequent check iterates over this set.
### 2. Action-pin audit — fail on tag-pinned `uses:`
```bash
# Detect floating tags (semver tags, branches, latest)
grep -rnHE '^\s*-\s*uses:\s*[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+@(v?[0-9]+(\.[0-9]+)*|main|master|latest)\s*(#.*)?$' \
.github/workflows/ .gitea/workflows/ 2>/dev/null
```
Each match is a finding. Severity: **HIGH** (workflow runs arbitrary
third-party code with workflow's secret access).
Acceptable exception: `uses: ./` references (local reusable workflows
in the same repo) — these pin to the calling commit. Flag separately
as INFO and recurse into them to check their `uses:` references too.
### 3. Container-image-pin audit — fail on tag-pinned `container:`/`image:`
```bash
grep -rnHE '^\s*(container|image):\s*[a-zA-Z0-9._/-]+:[a-zA-Z0-9._-]+\s*(#.*)?$' \
.github/workflows/ .gitea/workflows/ 2>/dev/null \
| grep -v 'sha256:'
```
Each match without `@sha256:` is a finding. Severity: **HIGH** (same
attack surface as actions; container provides the execution
environment for every step).
### 4. Bare `:latest` audit
```bash
grep -rnHE ':latest\b' .github/workflows/ .gitea/workflows/ 2>/dev/null
```
Each match is a finding. Severity: **CRITICAL** (`:latest` is the
canonical anti-pattern — captures both digest absence AND the most
volatile possible tag).
### 5. PR-secret-exposure audit
For each workflow file, check whether any job triggered by
`pull_request` references `secrets.*`:
```bash
# Identify pull_request-triggered jobs
for f in $(find .github/workflows .gitea/workflows -name "*.yml" -o -name "*.yaml" 2>/dev/null); do
# Extract jobs that run on pull_request and check for secret references
awk '/^on:/,/^[a-z]/ { print }' "$f" | grep -q 'pull_request' || continue
if grep -nE '\${{\s*secrets\.' "$f" >/dev/null 2>&1; then
echo "POTENTIAL SECRET EXPOSURE in $f"
grep -nE '\${{\s*secrets\.' "$f"
fi
done
```
Each match is a finding. Severity: **CRITICAL** (a contributor PR
from a fork can read repo secrets via the workflow's environment;
classic supply-chain attack vector documented in the Shai-Hulud
campaign).
Acceptable exception: jobs gated with `if: github.event.pull_request.head.repo.full_name == github.repository`
(only trusted maintainer PRs). Flag as INFO and verify the guard.
### 6. Curl-pipe-shell installer audit
```bash
grep -rnHE 'curl[^|]+\|\s*(bash|sh)' .github/workflows/ .gitea/workflows/ 2>/dev/null
```
Each `curl | sh` pattern is a finding. Severity: **MEDIUM** unless
the surrounding context includes a content-hash check (look for
`sha256sum`, `OBSERVED_SHA`, `EXPECTED_INSTALL_SHA`) — if no hash
check is present, severity is **HIGH**.
The recommended fix pattern (strict-mode opt-in) is documented in the
[`ci-action-pinning` rule](../../rules/ci-action-pinning.md) section
"Standalone tools pinned by version + checksum".
### 7. Pin-manifest presence audit
```bash
test -f ci/digests.txt || test -f .ci/digests.txt || test -f docs/ci/digests.txt
```
If no pin manifest exists, finding: **MEDIUM** (without a manifest,
diffs that change pinned references are not auditable).
## Output format
Produce a markdown report at `.aiwg/security/working/ci-workflow-audit.md`
with the structure below. If the working directory doesn't exist,
create it.
```markdown
# CI Workflow Audit
**Generated**: <ISO timestamp>
**Repo**: <repo path or URL>
**Workflow files scanned**: <count>
## Findings
### CRITICAL — Bare :latest tags
- `<file>:<line>` — `<matching line>`
### CRITICAL — PR-triggered jobs reference secrets
- `<file>:<line>` — `<matching line>`
Mitigation: Gate the job with `if: github.event.pull_request.head.repo.full_name == github.repository`
OR move the secret-using step to a separate `workflow_run`-triggered workflow.
### HIGH — Unpinned actions (tag-pinned uses:)
- `<file>:<line>` — `<matching line>`
Resolve the pin: `git ls-remote https://github.com/<owner>/<repo> refs/tags/<tag>`
Replace `@<tag>` with `@<40-char-sha>` and add a trailing `# <version>` comment.
### HIGH — Unpinned container images
- `<file>:<line>` — `<matching line>`
Resolve the digest: `docker pull <image>:<tag>` then `docker inspect <image>:<tag> --format='{{.Id}}'`
Replace with `<image>:<tag>@sha256:<digest>`.
### HIGH — curl|sh without hash check
- `<file>:<line>` — `<matching line>`
Add observed-SHA logging and strict-mode opt-in per ci-action-pinning rule §
"Standalone tools pinned by version + checksum".
### MEDIUM — curl|sh with hash check (acceptable, audit annually)
- `<file>:<line>` — `<matching line>`
### MEDIUM — No pin manifest
- No `ci/digests.txt` (or equivalent) found. Create one before applying any pinning fixes
— the manifest is the source of truth for diff review of future pin bumps.
### INFO — Local reusable workflows (transitive check)
- `<file>:<line>` — `<matching line>`
Recursive check result: <clean | findings nested below>
### INFO — PR jobs guarded against fork access
- `<file>:<line>` — guard present: `<the if: expression>`
## Clean Checks
- All workflow files have at least one signed-tag verification step (or none required).
- No unpinned references found in `<file>` family.
- ...
## Remediation Plan
Suggested order:
1. Resolve and apply digest pins for CRITICAL findings first (largest blast radius).
2. Establish or update `ci/digests.txt` to reflect the resolved pins.
3. Move PR-secret-exposure findings out of `pull_request`-triggered workflows.
4. Add observed-SHA logging to all `curl|sh` installers (HIGH → MEDIUM).
5. Re-run this audit. Iterate until clean.
## Follow-up Issues
If findings exceed a one-PR fix scope, file follow-ups for each finding category:
- `ci-pin-actions` — bulk-pin all `uses:` references
- `ci-pin-containers` — bulk-pin all container images
- `ci-pr-secret-isolation` — restructure PR-triggered workflows that reference secrets
- `ci-installer-hardening` — add content-hash checks to all `curl|sh` installers
## References
- [`ci-action-pinning` rule](../../rules/ci-action-pinning.md) — the enforcement gate
- AIWG's own [`ci/digests.txt`](https://git.integrolabs.net/roctinam/aiwg/src/branch/main/ci/digests.txt) — pin-manifest reference
- [`npm-supply-chain-audit` skill](../npm-supply-chain-audit/SKILL.md) — npm-ecosystem complement
```
## Incident-response trigger
If the audit surfaces a `pull_request`-triggered job with `secrets.*`
references that has been merged to the default branch within the past
90 days, treat it as an incident-response candidate:
1. Check the workflow run history for that file via `gh run list --workflow=<file>` (GitHub) or `mcp__git-gitea__actions_run_read` (Gitea)
2. Inventory any external contributor PRs that triggered the workflow during the exposure window
3. Rotate any secrets the workflow could have read
The other audit findings are configuration-hardening, not active
incidents — fix and move on.
## Completion criteria
The skill is "done" when:
- All seven audit checks have produced findings (or a `Clean Check` note)
- A markdown report is written to `.aiwg/security/working/ci-workflow-audit.md`
- The report's Remediation Plan section lists every finding in suggested fix order
- If any CRITICAL finding is present, the user has been explicitly notified before the skill exits
## See Also
- [`npm-supply-chain-audit` skill](../npm-supply-chain-audit/SKILL.md)
- [`supply-chain-hardening-quickstart` skill](../supply-chain-hardening-quickstart/SKILL.md)
- [`ci-action-pinning` rule](../../rules/ci-action-pinning.md)
- [`supply-chain-trust` skill](../supply-chain-trust/SKILL.md) — broader trust-chain design
## References
- AIWG supply-chain hardening post-mortem 2026-05 (Shai-Hulud campaign response)
- GitHub Actions documentation: pinning third-party actions to commit SHAs
- npm trusted publishers documentation
- Sigstore / cosign documentation