Nightly security advisory watch list with GitHub Actions, Claude Code and rConfig MCP
The first answer to “how many of ours are affected?” is a snapshot. Over the following weeks devices get patched, mitigations get applied, new devices arrive with the vulnerable feature on, and someone re-enables a service while troubleshooting. This playbook keeps each open advisory in a watch list in Git. Every night a GitHub Actions job asks an agent to re-classify the estate against each one, and updates one issue per advisory with what moved since yesterday.
Best for: security and network teams tracking several open advisories, remediation progress reporting, catching re-exposure.
Tools: rConfig, Claude Code (claude -p), GitHub Actions with a self-hosted runner inside your network, GitHub Issues.
rConfig tools used: get_devices, search_configs.
Uplink guideFind devices exposed to a security advisory with an AI agent and rConfig MCPTurn a vendor security advisory into a list of affected devices: an AI agent searches stored configs in rConfig over MCP. Worked example: Cisco CVE-2023-20198.Read the guide
How it flows
flowchart LR
S["schedule 03:30 UTC"] --> J["Job on<br/>self-hosted runner"]
W[("watchlist.yaml")] --> J
P[("state/latest.json<br/>last night")] --> J
J --> A["claude -p"]
A -- "get_devices, search_configs" --> R["rConfig MCP"]
A --> N["tonight.json"]
N --> D["Compare with last night"]
D --> I["Update one issue<br/>per advisory"]
N --> C["Commit state/latest.json"]
The watch list
Each entry holds the conditions from the vendor’s advisory, written as config lines the agent can search for. The example is the one worked through in Security advisory exposure. Take the conditions for any advisory from the vendor’s current text, not from this page.
advisories: - id: cisco-sa-iosxe-webui-privesc-j22SaA4z cve: CVE-2023-20198 issue: 412 # the GitHub issue this advisory reports to scope: { vendor: Cisco, groups: [Core, Distribution, Access, WAN] } exposed_if_any: - "ip http server" - "ip http secure-server" mitigated_if: - "ip http active-session-modules none" # covers ip http server - "ip http secure-active-session-modules none" # covers ip http secure-server notes: > Exposed only on IOS XE. Use the show version capture to confirm the OS; classify as unknown if there is none.The workflow
name: Advisory watch liston: schedule: - cron: "30 3 * * *" # UTC, after rConfig's nightly backups workflow_dispatch: {}
permissions: contents: write # commit state/latest.json issues: write # comment on the advisory issues
jobs: check: runs-on: [self-hosted, netops] # inside the network, can reach rConfig timeout-minutes: 30 env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} RCONFIG_MCP_TOKEN: ${{ secrets.RCONFIG_MCP_TOKEN }} GH_TOKEN: ${{ github.token }} steps: - uses: actions/checkout@v4 - run: npm install -g @anthropic-ai/claude-code - run: ./scripts/advisory-watch.shThe repository’s .mcp.json is the one from the Claude Code guide: it reads the token from RCONFIG_MCP_TOKEN, so the secret never appears in a file.
#!/usr/bin/env bashset -euo pipefail
PROMPT=$(cat <<EOFUsing rConfig's tools only, classify devices against each advisory below.For each advisory: list devices in scope with get_devices, then usesearch_configs on their latest running configs for the exposed_if_any andmitigated_if lines. Classify each device in scope as exposed, mitigated,not_enabled or unknown, following the advisory's notes. Quote the matchingline for every exposed or mitigated device.
$(cat watchlist.yaml)
Output only JSON: {"<advisory id>": {"exposed": [{"device", "line"}],"mitigated": [...], "not_enabled": <count>, "unknown": [<device>]}}.If a search was cut short, add "incomplete": true for that advisory.If rConfig's tools fail, output {"error": "<detail>"}.EOF)
claude -p "$PROMPT" --allowedTools "mcp__rconfig__*" --output-format json \ | jq -r '.result' > tonight.jsonjq -e 'has("error") | not' tonight.json > /dev/null || { echo "rConfig check failed"; exit 1; }
mkdir -p state && touch state/latest.jsonfor id in $(yq -r '.advisories[].id' watchlist.yaml); do issue=$(yq -r ".advisories[] | select(.id == \"$id\") | .issue" watchlist.yaml) now=$(jq -r --arg id "$id" '[.[$id].exposed[].device] | sort | .[]' tonight.json) was=$(jq -r --arg id "$id" '[.[$id].exposed[]?.device] | sort | .[]' state/latest.json 2>/dev/null || true) newly=$(comm -13 <(echo "$was") <(echo "$now") | sed '/^$/d') cleared=$(comm -23 <(echo "$was") <(echo "$now") | sed '/^$/d') count=$(echo "$now" | sed '/^$/d' | wc -l) gh issue comment "$issue" --body "$(printf '**%s**: %s exposed tonight\n\nNewly exposed:\n%s\n\nNo longer exposed:\n%s\n\nUnknown: %s\n' \ "$(date -u +%F)" "$count" "${newly:-none}" "${cleared:-none}" \ "$(jq -r --arg id "$id" '.[$id].unknown | join(", ")' tonight.json)")"done
mv tonight.json state/latest.jsongit add state/latest.jsongit -c user.name=advisory-watch -c user.email=advisory-watch@example.net \ commit -m "Advisory watch $(date -u +%F)" && git pushAn illustrative issue comment:
2026-11-12: 3 exposed tonight
Newly exposed:lon-acc-22
No longer exposed:dub-acc-07dub-acc-09
Unknown: ams-acc-03lon-acc-22 is the line to act on: a device that wasn’t exposed yesterday is.
Limits to know
- Search limits apply.
search_configsscans up to 500 configs and stops after about 5 seconds. On a large estate, scope each advisory by vendor and group so the agent searches in slices. The prompt asks it to flag a cut-short search asincomplete. - The config is the evidence, not the version. Many advisories also depend on the software version. If a device has no
show versioncapture in rConfig, the agent can’t confirm it, so it lands inunknown. Treatunknownas work to do, not as safe. - GitHub schedules drift. Scheduled workflows can start late under load, and queued runs can be dropped. Use
workflow_dispatchto re-run by hand, and don’t depend on the exact minute. - Remediation shows the night after the backup. A device fixed at 10:00 shows as remediated after rConfig’s next backup and the next run.
Guardrails
- The agent has rConfig’s tools only. Issue comments and the state commit are made by the script, with the workflow’s own token.
- The rConfig token belongs to a service user scoped to the devices in the watch list’s groups.
- Retire an advisory by removing it from the watch list in a pull request and closing its issue, so the history explains why tracking stopped.
Variations
- Escalate re-exposure. When a device appears in “newly exposed” after having been remediated before, add a label to the issue and mention the security on-call team.
- Different runner. The same script runs from cron on any server inside your network. GitHub Actions adds history, review of watch-list changes and the issue thread.
- Weekly management view. A second scheduled job reads every advisory issue’s latest comment and writes one table: advisory, exposed count, trend over four weeks.
Related reference
- Config Search
- Connect an AI agent over MCP
- Policy Definitions: turn a permanent mitigation into a compliance rule
- Claude Code
Frequently asked questions
Why re-check an advisory every night?
Exposure changes after the first answer: devices get patched or mitigated, new devices are added with the vulnerable feature enabled, and someone re-enables a service during troubleshooting. A nightly re-check catches all three.
Can GitHub's hosted runners reach rConfig?
Not if rConfig is on a private network, which it usually is. Run the job on a self-hosted runner inside your network that can reach /mcp.
Where do the exposure conditions come from?
From the vendor's advisory, copied into the watch list by an engineer. The agent applies them; it doesn't decide what makes a device vulnerable.