|
| 1 | +--- |
| 2 | +name: Dependabot remediation agent |
| 3 | +description: Fetches open Dependabot alerts for the agent to process this run; the agent opens a fix PR or a triage issue linking to those alerts, then assigns svc-devtoolsbot on each alert it processed. |
| 4 | +engine: copilot |
| 5 | +on: |
| 6 | + schedule: daily |
| 7 | + workflow_dispatch: {} |
| 8 | +timeout-minutes: 45 |
| 9 | +permissions: |
| 10 | + contents: read |
| 11 | + issues: read |
| 12 | + pull-requests: read |
| 13 | +network: |
| 14 | + allowed: |
| 15 | + - threat-detection |
| 16 | + - github |
| 17 | +tools: |
| 18 | + github: |
| 19 | + allowed-repos: |
| 20 | + - mongodb-js/devtools-shared |
| 21 | + min-integrity: unapproved |
| 22 | +mcp-scripts: |
| 23 | + get-dependabot-alerts: |
| 24 | + description: >- |
| 25 | + Returns open Dependabot alerts for this repository for the current run as a JSON object |
| 26 | + with a `dependabot_alerts` array (may be empty). |
| 27 | + timeout: 120 |
| 28 | + run: | |
| 29 | + set -euo pipefail |
| 30 | + # Avoid --argjson for huge API payloads (ARG_MAX / "argument list too long"). |
| 31 | + dep_file="${RUNNER_TEMP}/get-dependabot-alerts-dep.json" |
| 32 | + gh api -H "X-GitHub-Api-Version: 2026-03-10" "repos/${GITHUB_REPOSITORY}/dependabot/alerts?state=open" --paginate >"$dep_file" |
| 33 | + jq -n \ |
| 34 | + --slurpfile dep "$dep_file" \ |
| 35 | + -f /dev/stdin <<'JQ' |
| 36 | + def sev_rank($s): |
| 37 | + ($s // "" | ascii_downcase) |
| 38 | + | if . == "critical" then 4 |
| 39 | + elif . == "high" then 3 |
| 40 | + elif . == "medium" then 2 |
| 41 | + elif . == "low" then 1 |
| 42 | + else 0 end; |
| 43 | +
|
| 44 | + def dependabot_score: |
| 45 | + sev_rank(.security_vulnerability.severity // .security_advisory.severity // ""); |
| 46 | +
|
| 47 | + def unassigned: |
| 48 | + ((.assignees // []) | length) == 0; |
| 49 | +
|
| 50 | + ( $dep[0] | map(select(unassigned)) ) as $dun | |
| 51 | + ( $dun |
| 52 | + | sort_by((.dependency.package.name // "unknown")) |
| 53 | + | group_by((.dependency.package.name // "unknown")) |
| 54 | + | map({ |
| 55 | + pkg: (.[0].dependency.package.name // "unknown"), |
| 56 | + alerts: ., |
| 57 | + maxSev: (map(dependabot_score) | max), |
| 58 | + maxNum: (map(.number) | max) |
| 59 | + }) |
| 60 | + | sort_by([- .maxSev, - .maxNum]) |
| 61 | + | if length > 0 then .[0].alerts else [] end |
| 62 | + ) as $dep_sel | |
| 63 | +
|
| 64 | + { dependabot_alerts: $dep_sel } |
| 65 | + JQ |
| 66 | + rm -f "$dep_file" |
| 67 | + env: |
| 68 | + GH_TOKEN: "${{ secrets.GH_AW_SECURITY_ADVISORY_PAT }}" |
| 69 | +safe-outputs: |
| 70 | + create-pull-request: |
| 71 | + title-prefix: "[agent] " |
| 72 | + labels: |
| 73 | + - security |
| 74 | + draft: true |
| 75 | + max: 1 |
| 76 | + auto-close-issue: false |
| 77 | + # Dependabot and supply-chain fixes routinely touch manifests; default protected-files would block those patches. |
| 78 | + protected-files: allowed |
| 79 | + create-issue: |
| 80 | + title-prefix: "[agent-triage] " |
| 81 | + labels: |
| 82 | + - security |
| 83 | + max: 1 |
| 84 | + expires: false |
| 85 | + jobs: |
| 86 | + assign-bot-to-security-alert: |
| 87 | + description: >- |
| 88 | + Set assignees to svc-devtoolsbot on one or more Dependabot alerts (claims them for this automation). |
| 89 | + Pass every alert number in a single call — the workflow allows only one assign output item per run. |
| 90 | + runs-on: ubuntu-latest |
| 91 | + output: Assignee update applied on Dependabot alert(s). |
| 92 | + permissions: |
| 93 | + contents: read |
| 94 | + inputs: |
| 95 | + alert_numbers: |
| 96 | + description: >- |
| 97 | + JSON array of Dependabot alert numbers to assign, e.g. [12,34,56] (from get-dependabot-alerts). |
| 98 | + Include all alerts you are dequeuing in this one array. |
| 99 | + required: true |
| 100 | + type: string |
| 101 | + steps: |
| 102 | + - name: Assign svc-devtoolsbot on Dependabot alert |
| 103 | + env: |
| 104 | + ALERT_QUEUE_BOT: svc-devtoolsbot |
| 105 | + GH_TOKEN: "${{ secrets.GH_AW_SECURITY_ADVISORY_PAT }}" |
| 106 | + run: | |
| 107 | + set -euo pipefail |
| 108 | + out="${GH_AW_AGENT_OUTPUT:-}" |
| 109 | + if [[ -z "${out}" || ! -f "${out}" ]]; then |
| 110 | + echo "No GH_AW_AGENT_OUTPUT or file missing" |
| 111 | + exit 0 |
| 112 | + fi |
| 113 | + failed=0 |
| 114 | + body="$(jq -nc --arg u "${ALERT_QUEUE_BOT}" '{assignees: [$u]}')" |
| 115 | + while IFS= read -r item; do |
| 116 | + while IFS= read -r num; do |
| 117 | + [[ -z "${num}" || "${num}" == "null" ]] && continue |
| 118 | + echo "Assigning ${ALERT_QUEUE_BOT} on dependabot alert ${num}" |
| 119 | + if ! echo "${body}" | gh api --method PATCH \ |
| 120 | + -H "X-GitHub-Api-Version: 2026-03-10" \ |
| 121 | + "repos/${GITHUB_REPOSITORY}/dependabot/alerts/${num}" --input -; then |
| 122 | + echo "::error::PATCH failed for dependabot alert ${num}" >&2 |
| 123 | + failed=1 |
| 124 | + fi |
| 125 | + done < <(jq -r '. as $i | |
| 126 | + ( if ($i.alert_numbers != null) then |
| 127 | + ($i.alert_numbers | if type == "array" then . elif type == "string" then (try fromjson catch []) else [] end) |
| 128 | + elif ($i.alert_number != null) then [$i.alert_number] |
| 129 | + else [] end ) | .[] | tostring' <<<"${item}") |
| 130 | + done < <(jq -c '.items[]? | select(.type == "assign_bot_to_security_alert")' "${out}") |
| 131 | + if [[ "${failed}" -ne 0 ]]; then |
| 132 | + exit 1 |
| 133 | + fi |
| 134 | +strict: true |
| 135 | +--- |
| 136 | + |
| 137 | +# Security remediation agent |
| 138 | + |
| 139 | +You work on **mongodb-js/devtools-shared**. You **do** analyze the security findings returned by **`get-dependabot-alerts`** for this run, then either **implement** changes and **`create_pull_request`**, or—when you cannot determine a safe in-repo fix—emit **`create_issue`** documenting your findings and **linking to every alert** you were asked to process (use each alert’s **`html_url`** from the payload). |
| 140 | + |
| 141 | +You **do not** dismiss alerts. |
| 142 | + |
| 143 | +## Your task |
| 144 | + |
| 145 | +1. **Fetch alerts** — Call `get-dependabot-alerts`. You receive a JSON object with a **`dependabot_alerts`** array (may be empty). Treat this as the **work queue for this run** and base your analysis only on that array. |
| 146 | + |
| 147 | +2. **If `dependabot_alerts` is empty** — Emit **`noop`** with a brief message and stop. |
| 148 | + |
| 149 | +3. **Scope** — The workflow allows **`max: 1`** pull request per run. Prefer **one coherent change** that addresses everything you can from the returned alerts when a single remediation applies; when it does not, use **`create_issue`** to cover the rest and link every alert you triaged. |
| 150 | + |
| 151 | +4. **Implement the fix** — Edit the repo as needed and follow existing project conventions. For **npm-style supply-chain fixes** (this monorepo uses `package.json` / lockfiles under the repo root and in `packages/*`): |
| 152 | + - **Prefer manifest-first remediation:** bump or change the **direct** dependency in the **`package.json` that actually declares it** (repository root or the relevant workspace package). Let the lockfile (`package-lock.json`, etc.) update as a consequence of that manifest change. **Do not** treat “only editing the lockfile to pin a transitive” as the default approach when a `package.json` range can be updated instead. |
| 153 | + - If a major version bump of the direct dependency is required, evaluate the impact of the upgrade. If the upgrade is simple and does not require a lot of changes, do it, otherwise emit a `create_issue` and link to the alert. |
| 154 | + - **Use `overrides` / resolutions only as a fallback:** add an **`overrides`** (npm) or equivalent **only when** the maintainer of the **direct** dependency has **not** shipped a release that resolves the vulnerable **transitive** on a supported range, so there is no reasonable manifest bump that clears the advisory. When you use overrides, say so in the PR body (which direct dependency is stuck, which advisory it blocks, and what you verified). |
| 155 | + |
| 156 | +5. **Outcome (choose one when the batch is non-empty)** |
| 157 | + - **Fix:** If you implemented a change, emit **`create_pull_request`** with title that follows conventional commit format (e.g. `chore(deps): bump xyz to v1.2.3`) and body that summarize changes and cite alert numbers, CVE/GHSA or rule ids from the payload. |
| 158 | + - **No fix:** If you cannot determine an appropriate in-repo remediation, emit **`create_issue`** (one issue for this run) with a clear title and body that: summarizes your analysis, states what is blocked or uncertain, and includes a **markdown list of links** to each alert’s **`html_url`** (and key ids). Do **not** use **`noop`** for “could not fix” cases. |
| 159 | + |
| 160 | +6. **Dequeue alerts (always when the batch is non-empty)** — After **`create_pull_request`** or **`create_issue`**, emit **`assign_bot_to_security_alert` exactly once** with **`alert_numbers`** set to a **JSON array** of every Dependabot **`number`** you processed from `dependabot_alerts` (e.g. `[12,34,56]`). The workflow only accepts **one** assign output per run; a single array is how you assign the bot on multiple alerts. |
0 commit comments