mcpField
S
security-runner

security-runner

github.com/fellowship-devセキュリティ3170 回インストール2 週間前に更新
リポジトリ

security-runner について

対象リポジトリの未解決Dependabotアラートを分類し、対応と結果報告を行うセキュリティトリアージ用スキルです。共通の分類基準に沿って、修正PR、Issue、週次ダイジェストへの記録、または却下に振り分けます。

主な機能

  1. 01未解決のDependabotアラートを取得
  2. 02security-checkに基づくアラート分類
  3. 03修正PRの作成またはIssue登録
  4. 04バックログ項目の週次ダイジェスト追加
  5. 05誤検知や対象外アラートの却下
  6. 06レポート生成と任意の投稿

活用シーン

  1. 01リポジトリの週次セキュリティ確認
  2. 02重大なCVE公開後の対象リポジトリのトリアージ
  3. 03スプリント開始前のアラート backlog 解消
  4. 04Snykアラート急増への対応

インストール

コマンドをコピーしてターミナルで実行します。

$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:各アラートを分類

各アラートにセキュリティチェックの判断マトリクスを適用します。

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/バックログ — 週次ダイジェストに記載
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:マージ戦略を遵守

マージ戦略の適用は、ステップ 3 の process_p0_p1_alert 内で行われます。ここでは $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 に廃止されました

コメント