Agent Skillsaaronjmars/aeon › Workflow Audit

Workflow Audit

GitHub

自动化审计GitHub工作流与复合操作,利用zizmor和actionlint扫描漏洞。对比历史结果分类变更,自动修复高危问题,仅在存在差异时提交PR,保持静默以优化通知效率。

skills/workflow-audit/SKILL.md aaronjmars/aeon

Trigger Scenarios

需要审计.github/workflows目录下的工作流文件 发现新的安全漏洞或回归旧有修复的问题 执行定期的CI/CD流水线安全检查

Install

npx skills add aaronjmars/aeon --skill Workflow Audit -g -y
More Options

Use without installing

npx skills use aaronjmars/aeon@Workflow Audit

指定 Agent (Claude Code)

npx skills add aaronjmars/aeon --skill Workflow Audit -a claude-code -g -y

安装 repo 全部 skill

npx skills add aaronjmars/aeon --all -g -y

预览 repo 内 skill

npx skills add aaronjmars/aeon --list

SKILL.md

Frontmatter
{
    "name": "Workflow Audit",
    "tags": [
        "dev"
    ],
    "type": "Skill",
    "category": "dev",
    "requires": [
        "GH_GLOBAL?"
    ],
    "description": "Audit .github\/workflows and composite actions with zizmor + actionlint, classify findings against the prior audit, auto-fix Critical\/High regressions, and open a PR only when something actually changed."
}

Today is ${today}. Audit every workflow file and composite action in .github/, classify findings against the most recent prior audit, auto-apply fixes for NEW Critical/High items, and open a PR only if the delta is non-empty.

Core principle: the goal is not "run monthly and paste findings" — it's to surface changes (new vulns, regressions of fixed ones) with an attacker's-eye-view per finding, and stay silent on clean runs so the notify isn't trained-to-ignore.

Preflight

0a. Bootstrap variables

today=$(date -u +%F)
REPO_NAME=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || echo "unknown/unknown")
REPO_URL=$(gh repo view --json url -q .url 2>/dev/null || echo "")

0b. Install scanners

Try in order; if both fail, exit with WORKFLOW_AUDIT_TOOL_FAIL.

# zizmor (Trail of Bits, SARIF-capable GH Actions auditor)
# Pin to a specific version for reproducibility — bump this when upgrading.
ZIZMOR_VERSION="1.24.1"
if ! command -v zizmor >/dev/null 2>&1; then
  pipx install "zizmor==${ZIZMOR_VERSION}" 2>/dev/null \
    || python3 -m pip install --user "zizmor==${ZIZMOR_VERSION}" 2>/dev/null \
    || true
  export PATH="$HOME/.local/bin:$PATH"
fi
# TODO: bump ZIZMOR_VERSION to the latest stable on the next audit of this skill.
# actionlint (Rhymond's syntax-level workflow linter)
if ! command -v actionlint >/dev/null 2>&1; then
  bash <(curl -sL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) 2>/dev/null || true
  export PATH="$PWD:$PATH"
fi

If the sandbox blocks the download, use WebFetch to pull the install script, save it locally, and bash it. If both tools still fail to install, continue with the hand-rolled pattern checks in step 2 but mark the run as WORKFLOW_AUDIT_TOOL_DEGRADED in the footer.

Steps

1. Enumerate audit targets

TARGETS=$(find .github/workflows -maxdepth 2 -type f \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null; \
          find .github/actions -type f \( -name "action.yml" -o -name "action.yaml" \) 2>/dev/null)

If $TARGETS is empty, exit with WORKFLOW_AUDIT_NO_WORKFLOWS — notify *Workflow audit* — no workflow files found under .github/ and stop.

2. Run scanners

Primary — zizmor:

mkdir -p .audit
zizmor --format sarif --persona auditor .github/workflows .github/actions \
  > .audit/zizmor.sarif 2> .audit/zizmor.err || true

Parse SARIF. Each runs[0].results[] entry yields: ruleId, level (note/warning/error), message.text, locations[0].physicalLocation.artifactLocation.uri, locations[0].physicalLocation.region.startLine, and properties["zizmor/severity"] + properties["zizmor/confidence"].

Map zizmor severity → our severity:

  • error + confidence ≥ highCritical
  • error (other confidence) or warning + confidence = highHigh
  • warningMedium
  • noteLow

Secondary — actionlint:

actionlint -format '{{json .}}' > .audit/actionlint.json 2> .audit/actionlint.err || true

Raise actionlint errors to Medium unless they touch a security-relevant rule (expression, shellcheck with SC2086/SC2046 over a ${{ github.* }} interpolation), in which case High.

Supplemental hand-rolled checks (always run, even when zizmor succeeds): These backstop tool gaps specific to this repo:

  • toJson-into-shell injection: grep for echo '\${{ toJson\(github\.event or echo "\${{ toJson\( piped to jq or assigned via command substitution. This is the messages.yml:577 pattern (prior April 11 audit missed it). Severity: Critical.
  • persist-credentials: true on actions/checkout followed by ref: ${{ github.event.pull_request.head.sha }} (or head.ref): classic poisoned-pipeline pattern. Severity: Critical on pull_request_target triggers, High on workflow_run.
  • GITHUB_ENV / GITHUB_OUTPUT writes with user-controlled data: echo "X=${{ github.event.* }}" >> "$GITHUB_ENV" — newline-injection bypasses env masking. Severity: High.
  • Fleet-specific: spawn-instance / fleet-control / chain-runner jobs that pass ${{ inputs.* }} directly into gh workflow run, gh api repos/.../dispatches, or a run: shell without env intermediary. Severity: High.
  • Mutable ref on third-party action: uses: owner/action@branch or uses: owner/action@vN where owner is not actions, github, docker, or aws-actions. Severity: Medium (supply chain).

3. Build the current-run findings set

For each finding, emit a canonical record:

{
  "fingerprint": sha256("${rule_id}|${file}|${step_name_or_line_context}"),
  "severity": "Critical|High|Medium|Low",
  "rule_id": "template-injection|excessive-permissions|unpinned-uses|...",
  "file": ".github/workflows/messages.yml",
  "line": 577,
  "step": "Extract message",
  "pattern": "<verbatim vulnerable snippet, ≤120 chars>",
  "source": "zizmor|actionlint|hand-rolled"
}

The fingerprint is the key for delta classification — keep it stable across runs by anchoring to step name when available rather than line number (lines drift on unrelated edits).

4. Classify against prior audit (delta)

Find the most recent prior report:

PRIOR=$(ls -1 output/articles/workflow-audit-*.md 2>/dev/null | sort | tail -1)

If $PRIOR exists, extract its fingerprints from a machine-readable trailer (see step 6 format). Then label each current finding:

  • NEW — fingerprint absent from prior report
  • REINTRODUCED — fingerprint was marked Auto-fixed or Resolved in prior report, now present again
  • UNCHANGED — fingerprint present in prior report, still present
  • RESOLVED — fingerprint was present in prior report, now absent from current scan (emit as a separate section, no fix needed)

If $PRIOR does not exist, every finding is NEW.

5. Determine verdict and exit mode

Compute a one-line verdict from the delta:

Condition Verdict Exit mode
No findings at all WORKFLOW_AUDIT_CLEAN — no findings across N files CLEAN
Only UNCHANGED findings, no NEW/REINTRODUCED WORKFLOW_AUDIT_UNCHANGED — N carried over from ${PRIOR_DATE} UNCHANGED
≥1 REINTRODUCED WORKFLOW_AUDIT_REGRESSION — N previously-fixed finding(s) reintroduced REGRESSION
≥1 NEW Critical WORKFLOW_AUDIT_NEW_CRITICAL — N new critical finding(s) NEW_CRITICAL
≥1 NEW High (no critical) WORKFLOW_AUDIT_NEW_HIGH — N new high-severity finding(s) NEW_HIGH
NEW Medium/Low only WORKFLOW_AUDIT_NEW_INFO — N new lower-severity finding(s) NEW_INFO
All scanners failed WORKFLOW_AUDIT_TOOL_FAIL — zizmor and actionlint both unavailable TOOL_FAIL

Gating rule: in CLEAN and UNCHANGED modes, do not create a PR, do not send a notify, and write a log-only entry. Silence is correct on no-delta runs.

6. Write the audit report

Path: output/articles/workflow-audit-${today}.md (if the file already exists from an earlier run today, overwrite it — the latest audit of the day is authoritative).

Format:

# Workflow Security Audit — ${today}

**Verdict:** ${VERDICT_LINE}
**Repo:** [${REPO_NAME}](${REPO_URL})
**Files audited:** ${count} (${workflow_count} workflows, ${action_count} composite actions)
**Findings this run:** ${total} (${crit} critical, ${high} high, ${med} medium, ${low} low)
**Delta vs ${PRIOR_DATE or "(no prior audit)"}:** ${new_count} new, ${reintroduced_count} reintroduced, ${unchanged_count} unchanged, ${resolved_count} resolved
**Auto-fixed:** ${fixed_count}

## Regressions (previously-fixed findings now present again)

[One subsection per REINTRODUCED finding, same format as Findings.]

## New findings

[One subsection per NEW finding.]

### [CRITICAL|HIGH] ${rule_id} — ${short title}
**File:** `.github/workflows/file.yml` · **Step:** `Step Name` · **Line:** ${line}
**Pattern:**
```yaml
${verbatim snippet}

Attack chain:

  1. Entry: ${trigger} — reachable by ${who (external user / repo collaborator / scheduled)}
  2. Vector: ${what field is attacker-controlled}
  3. Sink: ${where it gets evaluated — shell / with: / github-script / GITHUB_ENV write}
  4. Reachable secrets: ${secrets in job env}
  5. Blast radius: ${what the reachable token can do — push? dispatch? comment? cross-repo?}

Fix:

# BEFORE
...
# AFTER
...

Status: Auto-fixed in this PR / Manual review required


[Medium and Low findings get a compact one-line-per-finding table, no attack chain.]

Carried over (unchanged)

Severity Rule File First seen
...

Resolved since ${PRIOR_DATE}

  • ${finding title} in ${file} — no longer present

Source status

  • zizmor: ${ok|fail|degraded}
  • actionlint: ${ok|fail|degraded}
  • hand-rolled: ${ok|fail}

The HTML-comment trailer at the bottom is the machine-readable fingerprint set the *next* run reads in step 4. Don't omit it.

### 7. Auto-fix NEW Critical/High findings (idempotent)

For each NEW Critical and NEW High finding (**not** for UNCHANGED — those failed a prior fix or are known-manual; don't thrash them):

**Idempotency check before fixing a script-injection finding:**
1. Read the step's `env:` block.
2. If a key starting with `_` already maps to the same `${{ ... }}` expression as the vulnerable interpolation, skip — the fix is already present; this is a stale finding and should be flagged in the report.
3. Otherwise insert a new `env:` key (prefix `_`, uppercase, derived from the expression: `${{ inputs.message }}` → `_INPUT_MESSAGE`; `${{ steps.msg.outputs.source }}` → `_MSG_SOURCE`; `${{ github.event.client_payload.message }}` → `_CLIENT_PAYLOAD_MESSAGE`).
4. Replace the in-`run:` interpolation with `"$_VARNAME"`.
5. Validate the edited YAML loads: `python3 -c "import yaml,sys; yaml.safe_load(open(sys.argv[1]))" "$FILE"`. If it fails, revert and mark the finding as `Manual required — auto-fix produced invalid YAML`.

Use the Edit tool for inline modifications. Do not rewrite whole files.

**Script-injection fix template:**
```yaml
# BEFORE:
- name: Step
  run: |
    VAR="${{ inputs.user_input }}"

# AFTER:
- name: Step
  env:
    _USER_INPUT: ${{ inputs.user_input }}
  run: |
    VAR="$_USER_INPUT"

toJson-into-shell fix template (the pattern April 11 missed):

# BEFORE:
MESSAGE=$(echo '${{ toJson(github.event.client_payload.message) }}' | jq -r '.')

# AFTER:
env:
  _PAYLOAD: ${{ toJson(github.event.client_payload.message) }}
...
MESSAGE=$(printf '%s' "$_PAYLOAD" | jq -r '.')

For permissions, pinning, and persist-credentials findings, do not auto-fix — always flag as Manual required (these need operator judgment about which jobs actually need the write scope, and SHA pinning requires verifying the intended commit).

8. Commit, branch, and PR (gated)

Exit modes CLEAN, UNCHANGED, TOOL_FAIL: skip this step entirely.

Otherwise:

# Reuse an existing open PR if one exists (don't spawn duplicates)
EXISTING=$(gh pr list --head fix/workflow-audit --state open --json number,url -q '.[0].url' 2>/dev/null)
BRANCH="fix/workflow-audit"
if [ -n "$EXISTING" ]; then
  git fetch origin "$BRANCH" 2>/dev/null && git checkout "$BRANCH" || git checkout -b "$BRANCH"
else
  # Version-suffix if the branch exists but no open PR (e.g. closed/merged)
  if git show-ref --quiet "refs/remotes/origin/$BRANCH"; then
    BRANCH="fix/workflow-audit-${today}"
  fi
  git checkout -b "$BRANCH"
fi

git add .github/workflows/ .github/actions/ output/articles/workflow-audit-${today}.md
git commit -m "fix(security): workflow audit ${today} — ${exit_mode}

Auto-fixed: ${fixed_count} finding(s)
Manual review: ${manual_count} finding(s)
Regressions: ${reintroduced_count}
Report: output/articles/workflow-audit-${today}.md"

git push -u origin "$BRANCH"

if [ -z "$EXISTING" ]; then
  gh pr create --title "fix: workflow security audit ${today} — ${VERDICT_LINE}" \
    --body-file <(cat <<'EOF'
## Verdict
${VERDICT_LINE}

## Summary
- **NEW:** ${new_count} (${new_crit} crit / ${new_high} high / ${new_med} med / ${new_low} low)
- **REINTRODUCED:** ${reintroduced_count}
- **UNCHANGED:** ${unchanged_count}
- **RESOLVED:** ${resolved_count}
- **Auto-fixed:** ${fixed_count}
- **Manual review:** ${manual_count}

## Attack chains worth reading first
${top_3_chain_titles}

## Full report
output/articles/workflow-audit-${today}.md

## Source status
zizmor: ${ok|fail} · actionlint: ${ok|fail} · hand-rolled: ${ok|fail}
EOF
)
else
  gh pr comment "$EXISTING" --body "Re-ran ${today}: ${VERDICT_LINE}. Auto-fixed ${fixed_count} new finding(s). See output/articles/workflow-audit-${today}.md."
fi

9. Notify (gated on exit mode)

Only in exit modes NEW_CRITICAL, NEW_HIGH, REGRESSION:

./notify "*Workflow audit — ${today}*
${VERDICT_LINE}
Auto-fixed ${fixed_count} · Manual ${manual_count}
Top chain: ${top_attack_chain_one_liner}
PR: ${pr_url}"

Exit mode NEW_INFO (medium/low only): write a log entry but do not notify (prior audits showed medium pinning/permission reminders become ignorable wallpaper).

Exit mode TOOL_FAIL: notify once with *Workflow audit — ${today}* WORKFLOW_AUDIT_TOOL_FAIL — zizmor and actionlint both unavailable, no scan completed.

Keep notify under one paragraph. No banned phrases: consider, might want to, potentially, exciting, robust, leveraging, unlocks, in this fast-moving space.

10. Log

Append to memory/logs/${today}.md:

## Workflow Security Audit
- Exit: ${EXIT_MODE}
- Verdict: ${VERDICT_LINE}
- Files audited: ${count} (${workflow_count} workflows, ${action_count} actions)
- Findings: ${total} total (${crit}C / ${high}H / ${med}M / ${low}L)
- Delta: ${new_count} new, ${reintroduced_count} reintroduced, ${unchanged_count} unchanged, ${resolved_count} resolved
- Auto-fixed: ${fixed_count}
- PR: ${pr_url or "(none — no delta)"}
- Report: output/articles/workflow-audit-${today}.md
- Source status: zizmor=${ok|fail} actionlint=${ok|fail} hand-rolled=${ok|fail}

Sandbox note

  • pipx install zizmor and pip install --user zizmor both hit PyPI — expected to work from GitHub-hosted runners (outbound to PyPI is allowed), but if the sandbox blocks them use WebFetch to retrieve the zizmor install script from https://docs.zizmor.sh/install.sh (or the release tarball from the zizmorcore/zizmor releases page) and run it locally.
  • gh CLI uses existing GITHUB_TOKEN / GH_GLOBAL. The audit + report path works with the built-in GITHUB_TOKEN, but the auto-fix path commits changes to .github/workflows/ — and the built-in token is forbidden from pushing workflow-file edits (GitHub rejects them without the workflow scope). So GH_GLOBAL (a PAT carrying that scope) is needed to actually land auto-fixes; without it, findings are still reported and flagged Manual required. That's why requires: lists it as optional — it degrades to report-only, it isn't a cross-repo concern.
  • No new secrets required beyond that. zizmor and actionlint are offline-only static analyzers.

Constraints

  • Never auto-fix UNCHANGED findings (if they didn't get fixed the first time there's a reason — manual-only scopes, permission decisions, or a prior auto-fix that broke YAML). Auto-fix is for NEW and REINTRODUCED Critical/High only.
  • Never auto-fix permissions, unpinned-uses, or persist-credentials findings — always flag as Manual. These need operator judgment.
  • Never run destructive git operations on main. All changes go through fix/workflow-audit or a version-suffixed branch.
  • Preserve the existing PR lifecycle — if a fix PR is open, add a comment rather than spawning a duplicate.
  • No new env vars beyond GITHUB_TOKEN / GH_GLOBAL (already in aeon.yml).
  • If exit mode is CLEAN or UNCHANGED, skip PR and notify — log only.

Version History

  • fb16753 Current 2026-07-05 12:08

Same Skill Collection

skills/action-converter/SKILL.md
skills/article/SKILL.md
skills/auto-merge/SKILL.md
skills/auto-workflow/SKILL.md
skills/autoresearch/SKILL.md
skills/bd-radar/SKILL.md
skills/changelog/SKILL.md
skills/code-health/SKILL.md
skills/cost-report/SKILL.md
skills/create-skill/SKILL.md
skills/ctrl/SKILL.md
skills/defi-overview/SKILL.md
skills/deploy-prototype/SKILL.md
skills/digest/SKILL.md
skills/distribute-tokens/SKILL.md
skills/ecosystem-pulse/SKILL.md
skills/fear-divergence/SKILL.md
skills/feature/SKILL.md
skills/fetch-tweets/SKILL.md
skills/fleet-control/SKILL.md
skills/fork-fleet/SKILL.md
skills/github-monitor/SKILL.md
skills/github-trending/SKILL.md
skills/heartbeat/SKILL.md
skills/idea-forge/SKILL.md
skills/idea-pipeline/SKILL.md
skills/inbox-triage/SKILL.md
skills/install-skill/SKILL.md
skills/investigation-report/SKILL.md
skills/issue-triage/SKILL.md
skills/last30/SKILL.md
skills/memory-flush/SKILL.md
skills/mention-radar/SKILL.md
skills/monitor-polymarket/SKILL.md
skills/narrative-convergence/SKILL.md
skills/narrative-tracker/SKILL.md
skills/okf-export/SKILL.md
skills/okf-ingest/SKILL.md
skills/onchain-monitor/SKILL.md
skills/operator-scorecard/SKILL.md
skills/picks-tracker/SKILL.md
skills/pm-manipulation/SKILL.md
skills/pm-pulse/SKILL.md
skills/pr-review/SKILL.md
skills/pr-triage/SKILL.md
skills/price-alert/SKILL.md
skills/reply-maker/SKILL.md
skills/repo-scanner/SKILL.md
skills/schedule-ads/SKILL.md
skills/search-skill/SKILL.md
skills/self-improve/SKILL.md
skills/send-email/SKILL.md
skills/skill-health/SKILL.md
skills/skill-repair/SKILL.md
skills/soul-builder/SKILL.md
skills/spawn-instance/SKILL.md
skills/strategy-builder/SKILL.md
skills/token-movers/SKILL.md
skills/token-pick/SKILL.md
skills/treasury-info/SKILL.md
skills/tx-explain/SKILL.md
skills/unlock-monitor/SKILL.md
skills/verdikta-hunter/SKILL.md
skills/vuln-scanner/SKILL.md
skills/vuln-tracker/SKILL.md
skills/write-tweet/SKILL.md
skills/x402-monitor/SKILL.md
skills/base-mcp/SKILL.md
skills/star-milestone/SKILL.md

Metadata

Files
0
Version
fb16753
Hash
5b47131a
Indexed
2026-07-05 12:08

Accueil - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-07-08 20:55
浙ICP备14020137号-1 $Carte des visiteurs$