Skip to content
Guides & References

Nightly security advisory watch list with GitHub Actions, Claude Code and rConfig MCP

Keep open vendor advisories in a Git watch list. Each night an AI agent re-checks rConfig's configs and reports newly exposed and remediated devices.

Iris: AI CenterReference designUpdated
On this page

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.

watchlist.yaml
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

.github/workflows/advisory-watch.yml
name: Advisory watch list
on:
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.sh

The 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.

scripts/advisory-watch.sh
#!/usr/bin/env bash
set -euo pipefail
PROMPT=$(cat <<EOF
Using rConfig's tools only, classify devices against each advisory below.
For each advisory: list devices in scope with get_devices, then use
search_configs on their latest running configs for the exposed_if_any and
mitigated_if lines. Classify each device in scope as exposed, mitigated,
not_enabled or unknown, following the advisory's notes. Quote the matching
line 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.json
jq -e 'has("error") | not' tonight.json > /dev/null || { echo "rConfig check failed"; exit 1; }
mkdir -p state && touch state/latest.json
for 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.json
git add state/latest.json
git -c user.name=advisory-watch -c user.email=advisory-watch@example.net \
commit -m "Advisory watch $(date -u +%F)" && git push

An illustrative issue comment:

2026-11-12: 3 exposed tonight
Newly exposed:
lon-acc-22
No longer exposed:
dub-acc-07
dub-acc-09
Unknown: ams-acc-03

lon-acc-22 is the line to act on: a device that wasn’t exposed yesterday is.


Limits to know

  • Search limits apply. search_configs scans 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 as incomplete.
  • The config is the evidence, not the version. Many advisories also depend on the software version. If a device has no show version capture in rConfig, the agent can’t confirm it, so it lands in unknown. Treat unknown as work to do, not as safe.
  • GitHub schedules drift. Scheduled workflows can start late under load, and queued runs can be dropped. Use workflow_dispatch to 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.

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.

Type to search.