关于 security-runner
这项安全分流技能处理目标仓库中的未解决 Dependabot 警报,进行分类、执行相应操作并汇报结果。它依据共享的分类框架,将警报转为修复 PR、问题、周报条目或忽略处理。
主要功能
- 01获取未解决的 Dependabot 警报
- 02使用 security-check 分类警报
- 03提交修复 PR 或创建问题
- 04将积压项加入每周摘要
- 05忽略误报或无关警报
- 06生成报告并可选择发布
适用场景
- 01每周扫描多个仓库的安全问题
- 02关键 CVE 披露后分流受影响仓库
- 03在迭代开始前清理警报积压
- 04处理 Snyk 警报激增
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ fellowship-dev/ dogfooded-skills --skill security-runner安装后在 Claude Code 中输入/security-runner确认已加载。
详细介绍
security-runner
为目标仓库执行安全分流流水线。对未关闭的 Dependabot 警报进行分类, 逐一采取行动,并报告结果。
请先运行 /security-check,了解此技能应用的分类框架。
通过 npx 安装:
npx skills add fellowship-dev/dogfooded-skills/skills/ops/security-runner
适用场景
- 每周 cron(周一 05:00)——自动扫描所有活跃仓库
- 关键 CVE 披露后——对受影响的仓库进行定向分流
- Snyk PR/警报激增后——在冲刺开始前清理积压
前置条件
# Verify gh auth and repo access
gh repo view "$REPO" --json name -q .name || { echo "ERROR: cannot access $REPO"; exit 1; }
# Dependabot alerts require repo admin scope
gh api repos/"$REPO"/dependabot/alerts --paginate --jq '.[0].number' 2>&1 | head -1
如果警报端点返回 403,说明令牌缺少 security_events 作用域,或未启用 Dependabot。
步骤 0:设置
REPO="${1:-$PYLOT_REPO}"
TODAY=$(date +%Y-%m-%d)
REPORT_PATH="/tmp/security-runner-${REPO//\//-}-${TODAY}.md"
# Every issue goes through the shared filing helper (pylot-cli): dedupe → comment,
# non-P0/P1 → weekly digest, max 3 new issues per run (P0 exempt).
FF="${PYLOT_WORKSPACE:-$HOME/.claude}/skills/pylot-cli/scripts/file-finding.sh"
# Check merge strategy — read from Pylot control plane, not target repo
# (crew.yml lives in $PYLOT_DIR, not in the repos being scanned)
MERGE_STRATEGY=$(grep -A5 "$(echo "$REPO" | cut -d/ -f2)" "$PYLOT_DIR/crew.yml" 2>/dev/null | \
grep merge_strategy | head -1 | awk '{print $2}' || echo "restricted")
echo "Repo: $REPO"
echo "Merge strategy: $MERGE_STRATEGY"
echo "Report: $REPORT_PATH"
步骤 1:获取未关闭的警报
# Fetch all open Dependabot alerts
ALERTS=$(gh api repos/"$REPO"/dependabot/alerts \
--paginate \
--jq '.[] | select(.state=="open") | {
number: .number,
package: .dependency.package.name,
ecosystem: .dependency.package.ecosystem,
manifest: .dependency.manifest_path,
scope: .dependency.scope,
severity: .security_advisory.severity,
cvss_score: .security_advisory.cvss.score,
cve_id: .security_advisory.cve_id,
summary: .security_advisory.summary,
patched_versions: (.security_vulnerability.patched_versions // "none"),
vulnerable_range: .security_vulnerability.vulnerable_version_range,
current_version: .security_vulnerability.package.ecosystem,
auto_dismissed: .auto_dismissed_at,
html_url: .html_url
}' 2>&1)
ALERT_COUNT=$(echo "$ALERTS" | python3 -c "import sys,json; data=[json.loads(l) for l in sys.stdin if l.strip()]; print(len(data))" 2>/dev/null || echo "0")
echo "Open alerts: $ALERT_COUNT"
if [ "$ALERT_COUNT" = "0" ]; then
echo "No open alerts — nothing to do."
echo "# Security Runner: $REPO — $TODAY" > "$REPORT_PATH"
echo "**No open Dependabot alerts.**" >> "$REPORT_PATH"
exit 0
fi
# Initialize counters used in Step 5 report
COUNT_P0=0; COUNT_P1=0; COUNT_P2=0; COUNT_BACKLOG=0; COUNT_DISMISS=0
DETAIL_LOG=""
步骤 2:对每条警报进行分类
对每条警报应用 security-check 决策矩阵:
classify_alert() {
local severity="$1" # critical|high|medium|low
local scope="$2" # runtime|development (from Dependabot)
local manifest="$3" # path to manifest file
# Map Dependabot scope to exploitability
local exploitability="dev-only"
if [ "$scope" = "runtime" ]; then
exploitability="network-reachable"
fi
# test-only: infer from manifest path — only when not explicitly runtime-scoped
# (guards against false downgrades on monorepos where test/ dirs contain runtime deps)
if [ "$scope" != "runtime" ] && echo "$manifest" | grep -qiE 'test|spec|__tests__|cypress'; then
exploitability="test-only"
fi
# Decision matrix
case "${severity}__${exploitability}" in
"critical__network-reachable") echo "P0" ;;
"critical__dev-only"|"high__network-reachable") echo "P1" ;;
"critical__test-only"|"high__dev-only"|"high__test-only"|"medium__network-reachable") echo "P2" ;;
"medium__dev-only"|"medium__test-only"|"low__network-reachable") echo "backlog" ;;
*) echo "dismiss" ;;
esac
}
步骤 3:逐一处理警报
根据警报的分类采取相应行动:
P0 / P1 — 提交修复 PR(安全补丁)或创建 issue(破坏性变更)
process_p0_p1_alert() {
local pkg="$1"
local patched="$2"
local alert_url="$3"
local priority="$4"
# Check if a Dependabot PR already exists for this package
EXISTING_PR=$(gh pr list --repo "$REPO" --state open --json number,title \
--jq ".[] | select(.title | test(\"$pkg\"; \"i\")) | .number" 2>/dev/null | head -1)
if [ -n "$EXISTING_PR" ]; then
echo " → Existing PR #$EXISTING_PR for $pkg — labeling $priority"
gh pr edit "$EXISTING_PR" --repo "$REPO" --add-label "security,$priority" 2>/dev/null || true
# Apply merge strategy here where $EXISTING_PR is in scope
if [ "$MERGE_STRATEGY" = "auto-merge" ]; then
gh pr merge "$EXISTING_PR" --repo "$REPO" --auto --squash 2>/dev/null && \
echo " → Auto-merge enabled on PR #$EXISTING_PR"
else
gh pr edit "$EXISTING_PR" --repo "$REPO" --add-label "ready-to-merge" 2>/dev/null && \
echo " → Labeled PR #$EXISTING_PR as ready-to-merge (restricted repo — human must merge)"
fi
return
fi
if [ "$patched" = "none" ]; then
# No patch available — create issue with upgrade path
bash "$FF" --repo "$REPO" --severity "$priority" --search "\"$pkg\" label:security" \
--title "security: no patch for $pkg ($priority)" \
--label security --label "$priority" \
--body "## Vulnerability\n\nPackage: \`$pkg\`\nPriority: $priority\nDependabot alert: $alert_url\n\nNo patched version available. Options:\n- [ ] Pin to last non-vulnerable version\n- [ ] Find alternative package\n- [ ] Remove dependency if unused\n\ncc: @maxfindel" 2>/dev/null
else
# No public GitHub API endpoint exists to trigger Dependabot PR creation directly.
# Create a tracking issue and direct the team to bump manually or await Dependabot's schedule.
echo " → Patch available ($patched) — creating tracking issue for $pkg"
bash "$FF" --repo "$REPO" --severity "$priority" --search "\"$pkg\" label:security" \
--title "security: bump $pkg to $patched ($priority)" \
--label security --label "$priority" \
--body "## Action Required\n\nPackage: \`$pkg\`\nFixed in: \`$patched\`\nPriority: $priority\nDependabot alert: $alert_url\n\nDependabot has not auto-created a PR. Options:\n- [ ] Wait for Dependabot's next scheduled run (Mon 05:00)\n- [ ] Manually bump \`$pkg\` to \`$patched\` and open a PR\n\nMonitor: https://github.com/$REPO/security/dependabot" 2>/dev/null && \
echo " → Tracking finding recorded for $pkg → $patched"
fi
}
P2 / Backlog — 添加到每周摘要
process_p2_backlog_alert() {
local pkg="$1"
local severity="$2"
local summary="$3"
local alert_url="$4"
local priority="$5"
# Non-blocking: the helper comments on an open issue for $pkg or appends to the weekly digest.
bash "$FF" --repo "$REPO" --search "\"$pkg\" label:security" \
--title "security: upgrade $pkg ($severity — $priority)" \
--body "## Vulnerability\n\nPackage: \`$pkg\`\nSeverity: $severity\nSummary: $summary\nDependabot alert: $alert_url\n\nBatch in next monthly dependency cycle. Verify no breaking changes before upgrading." 2>/dev/null
}
忽略 — 误报或无关项
dismiss_alert() {
local alert_number="$1"
local reason="$2" # tolerated_risk | inaccurate | not_used | no_bandwidth
gh api repos/"$REPO"/dependabot/alerts/"$alert_number" \
--method PATCH \
--field state=dismissed \
--field dismissed_reason="$reason" \
--field dismissed_comment="Dismissed by security-runner: $reason. Review quarterly." 2>/dev/null
echo " → Dismissed alert #$alert_number (reason: $reason)"
}
步骤 3b:编排 — 遍历所有警报
定义上述函数后,遍历 $ALERTS 并路由每条警报:
# Process substitution keeps the loop in the current shell so counter variables
# (COUNT_P0, DETAIL_LOG, etc.) survive to Step 5. A pipe would run the body in a
# subshell and silently discard every assignment.
while IFS= read -r alert_json; do
[ -z "$alert_json" ] && continue
pkg=$(echo "$alert_json" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['package'])")
severity=$(echo "$alert_json" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['severity'])")
scope=$(echo "$alert_json" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('scope','development'))")
manifest=$(echo "$alert_json" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['manifest'])")
patched=$(echo "$alert_json" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['patched_versions'])")
summary=$(echo "$alert_json" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['summary'])")
alert_url=$(echo "$alert_json" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['html_url'])")
alert_num=$(echo "$alert_json" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['number'])")
priority=$(classify_alert "$severity" "$scope" "$manifest")
echo "[$priority] $pkg ($severity, scope=$scope)"
DETAIL_LOG="${DETAIL_LOG}\n- [$priority] \`$pkg\` — $severity — $summary"
case "$priority" in
P0|P1)
process_p0_p1_alert "$pkg" "$patched" "$alert_url" "$priority"
[ "$priority" = "P0" ] && COUNT_P0=$((COUNT_P0 + 1)) || COUNT_P1=$((COUNT_P1 + 1))
;;
P2)
process_p2_backlog_alert "$pkg" "$severity" "$summary" "$alert_url" "$priority"
COUNT_P2=$((COUNT_P2 + 1))
;;
backlog)
process_p2_backlog_alert "$pkg" "$severity" "$summary" "$alert_url" "$priority"
COUNT_BACKLOG=$((COUNT_BACKLOG + 1))
;;
dismiss)
dismiss_alert "$alert_num" "tolerated_risk"
COUNT_DISMISS=$((COUNT_DISMISS + 1))
;;
esac
done < <(echo "$ALERTS")
步骤 4:遵循合并策略
合并策略的执行在 process_p0_p1_alert(上文步骤 3)内完成,
此时可通过 $EXISTING_PR 获取 PR 编号。该策略在步骤 0 中读取一次
($MERGE_STRATEGY),并作为全局变量供函数使用。
步骤 5:生成摘要报告
cat > "$REPORT_PATH" << REPORT
# Security Runner: $REPO
**Date:** $TODAY
**Alerts scanned:** $ALERT_COUNT
**Merge strategy:** $MERGE_STRATEGY
## Summary
| Priority | Count | Action |
|---|---|---|
| P0 | $COUNT_P0 | PRs opened / existing PRs labeled |
| P1 | $COUNT_P1 | PRs opened / existing PRs labeled |
| P2 | $COUNT_P2 | Issues created for batch cycle |
| Backlog | $COUNT_BACKLOG | Issues created (no urgency) |
| Dismissed | $COUNT_DISMISS | Dismissed via API |
## Detail
$DETAIL_LOG
## Next Steps
- P0/P1 PRs: review and merge within priority window
- P2 issues: include in next monthly dependency batch (/deps-runner)
- Dismissed alerts: review quarterly for changed risk posture
- Re-run: \`/security-runner $REPO\` after merges to confirm 0 open alerts
REPORT
echo "Report saved: $REPORT_PATH"
cat "$REPORT_PATH"
步骤 6:发布报告(可选)
如果在 Pylot 环境中运行:
# Post to Quest
if [ -n "$PYLOT_API" ] && [ -n "$GH_TOKEN" ]; then
curl -s -X POST "${PYLOT_API}/admin/events" \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Content-Type: application/json" \
-d "$(python3 -c "
import json, sys
content = open('$REPORT_PATH').read()
print(json.dumps({
'source': 'security-runner',
'type': 'security.triage',
'title': 'Security Triage: $REPO',
'meta': {'content': content, 'repo': '$REPO', 'alert_count': $ALERT_COUNT}
}))
")" 2>/dev/null || true
fi
完整流水线(编排示例)
#!/bin/bash
# Run security-runner against all active fellowship-dev repos
REPOS="fellowship-dev/booster-pack fellowship-dev/inbox-angel fellowship-dev/pylot fellowship-dev/quest fellowship-dev/spec-kit fellowship-dev/v0-operator fellowship-dev/dogfooded-skills fellowship-dev/flowchad"
for REPO in $REPOS; do
echo "=== Triaging $REPO ==="
/security-runner "$REPO" || echo "WARN: runner failed for $REPO"
done
Cron 集成
# In crew.yml, per team:
cron:
- schedule: "0 5 * * 1" # Every Monday at 05:00
task: "Weekly security triage: process open Dependabot/Snyk alerts, open fix PRs for safe patches"
输出:Pylot 结果标记
完成时输出:
[pylot:$PYLOT_OUTCOME_NONCE] outcome="N alerts triaged: X PRs, Y issues, Z dismissed" status=success
[pylot:$PYLOT_OUTCOME_NONCE] outcome="blocked: Dependabot API returned 403 — token missing security_events scope" status=blocked
相关技能
/security-check— 此技能执行的分类框架/deps-runner— 非安全类依赖更新;采用相同的 PR 模式/entropy-check— 可将安全评分纳入域等级- 直接检查 Dependabot 是否已配置;
maintenance技能已于 2026-09-14 退役
评论
支持 Markdown