Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Security advisory watch

Template: security-advisory-watch ยท category: security ยท risk: monitor_only

Poll an RSS/Atom security feed on an interval and page a channel on every new advisory whose title matches a keyword. Firing is deduped on the advisory link, and the first poll only records a baseline: booting on an established feed does not page its history. No networks, no signer: neither embedded engine boots.

This is the reference use of the trigger.web HTTP polling trigger.

Honest scope

Web signal, not browser automation. rflow fetches and parses the feed (with an rflow/<version> User-Agent, a 30s timeout and a 2 MiB body cap). It never logs in, clicks, or drives a browser. Respect the feed operator's robots.txt and rate limits: poll no faster than the feed actually updates.

When to use it

  • page the on-call when a dependency posts a critical security advisory
  • watch a protocol's announcements feed for pause/upgrade/incident notices
  • turn any RSS/Atom feed into deduped, journaled, replayable rflow runs

How it works

# recipe: partial
rflow_version: 1
name: security-advisory-watch
 
config:
  port: 3940
  db_connection: ${DATABASE_URL}
 
# pure off-chain: no networks and no signer - neither embedded engine boots
 
notifications:
  channels:
    pager:
      pagerduty:
        routing_key: ${PAGERDUTY_ROUTING_KEY}
 
workflows:
  security-advisory-watch:
    trigger: 
      web: 
        url: https://example.com/security/feed.xml
        every: 5m
        mode: rss
        where: "${{ 'critical' in (item.title | lower) }}"
        idempotency_key: "${{ item.link }}"
    steps:
      - id: page
        notify:
          channel: pager
          message: "Security advisory: ${{ item.title }} ${{ item.link }}"
    on_failure: drop
  • where is evaluated per feed entry over the item root ({ id, title, link, published, summary }). Only matching entries fire.
  • idempotency_key makes the advisory's link the claim identity: an edited advisory (same link, touched title) creates no second run. Without it the dedupe key falls back to the item's content fingerprint, so an edit counts as a new item.

Generate it

rflow new --template security-advisory-watch
cd security-advisory-watch
cp .env.example .env    # DATABASE_URL + pager credentials
docker compose up -d
rflow validate && rflow start

Non-interactive:

rflow new --template security-advisory-watch --yes \
  --answer feed_url=https://example.com/security/feed.xml \
  --answer match_keyword=critical \
  --answer pager=pagerduty

Inputs

keytypedefault
project_namestringsecurity-advisory-watch
feed_urlstringhttps://example.com/security/feed.xml
feed_modechoice (rss | atom)rss
poll_everyduration5m (floor 30s)
match_keywordstringcritical
channelstringpager
pagerchoice (pagerduty | telegram)pagerduty

Required env vars

DATABASE_URL, plus PAGERDUTY_ROUTING_KEY or TG_BOT_TOKEN + TG_CHAT_ID depending on the pager choice.

Rehearse offline

rflow test security-advisory-watch --fixture fixtures/advisory-item.json

The fixture is one parsed feed entry ({ "item": {...} }), so the page text renders without touching the live feed.

Common modifications

  • watch a JSON API instead: mode: json with path: "$.advisories"
  • watch a status page: mode: html_text with selector: ".incident" and changed: true (fire on any change rather than per new item)
  • authenticated feeds: add headers: { Authorization: "Bearer ${{ secrets.feed_token }}" } (header values are never journaled or logged)