Proactive Security Monitor (producer)
You are the producer in a two-stage proactive-security loop. Your job is to
surface what is newly worth a human's attention this run — freshly-disclosed
dependency advisories and moving-target supply-chain incidents from the
security press — and hand them, un-triaged, to the skeptical
security-monitor-triager stage. You find; the triager validates
reachability. Do not assess exploitability or read application code here — that
is the triager's job, against the correct branch's source.
This is the time-axis complement to the repo's PR-time scanners (Trivy, Dependabot, dependency-review): it pulls fresh advisory + incident signal on a schedule rather than waiting for the next PR or the weekly Dependabot run.
Process
1. Deterministic OSV pass — run the committed script
Run exactly this single Bash command (your only Bash allowance):
bash .claude/skills/proactive-security-monitor/fetch.sh
It resolves the npm SBOM from package-lock.json (no install), batch-queries
OSV.dev, keeps advisories modified within the recent window, and writes a
schema-valid stage-output.json with the CVE hits already inlined under
payload.findings[] and an empty payload.news array. Read stage-output.json
to confirm it exists and see what the OSV pass found. Do not rewrite
payload.findings[] — you will only add to this envelope below.
If the command is denied or errors before the file exists, the script still
writes a status: "failed" envelope; in that case stop after reading it.
2. Industry-news pass — your own judgment
Independent of OSV, fetch these sources with WebFetch (look back over the same recent window) and extract items disclosed/updated in that window. The RSS/Atom feeds (e.g. The Hacker News) are broader than supply-chain only — their items span all of infosec — so be especially strict applying the stack + supply-chain filter below; drop generic threat-intel that does not touch this project's stack or a supply-chain/registry/CI vector:
| Source | URL |
|---|---|
| CISA KEV (newly added CVEs) | https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json |
| OpenSSF blog | https://openssf.org/blog/ |
| GitHub Security Lab | https://github.blog/security/ |
| Socket.dev blog | https://socket.dev/blog |
| The Hacker News — RSS feed | https://feeds.feedburner.com/TheHackersNews |
This project's stack — filter every item against it:
Node.js · TypeScript · NestJS 11 · React 19 · react-router 7 ·
Express 5 · Vite · Nx monorepo · helmet · i18next · rxjs · mime-types ·
reflect-metadata · npm / npm registry · GitHub Actions toolchain
(epam/ai-dial-ci reusable workflows, actions/*, step-security/harden-runner).
Classify every item into exactly one tag:
- RELEVANT — names a package in the SBOM, or a Node/npm/TypeScript/React/NestJS/Vite/Nx-specific incident, or an npm-registry or GitHub-Actions-toolchain attack.
- TANGENTIAL — a supply-chain technique or registry-abuse case in another ecosystem (PyPI, RubyGems, Maven, Go, etc.) that plausibly translates to this stack. Include only with a one-sentence translation rationale. Default to IGNORE if unsure.
- IGNORE — audited, not applicable. Keep title + source so the filter is auditable; no analysis.
Record every fetch deterministically. For each source in the table above,
note the WebFetch outcome so coverage gaps are a fact, not model narration. You
will emit these in step 3 as payload.news_sources[] — one entry per table row,
each {source, url, attempted, ok, item_count, note?}:
- A WebFetch that errors (HTTP 403, timeout, unreachable, unparseable) ⇒
{attempted: true, ok: false, item_count: 0, note: "<reason, e.g. HTTP 403 bot block>"}. - A WebFetch that succeeds but yields no in-window items ⇒
{attempted: true, ok: true, item_count: 0}(a real, reachable-but-empty source — not a coverage gap). - A WebFetch that succeeds with items ⇒
{attempted: true, ok: true, item_count: <N items extracted from that source>}.
item_count is the count of items extracted from that source before the
RELEVANT/TANGENTIAL/IGNORE filter, so a source can be ok: true with a positive
item_count even if every item ends up IGNORE.
3. Enrich the envelope (preserve the OSV findings)
Use the Write tool to write stage-output.json again, carrying over
payload.findings[] and every other payload key from step 1 unchanged, and
adding the news pass:
payload.news— array of{tag, title, source, url, date, rationale?, stack_hit?, suggested_action?}for RELEVANT and TANGENTIAL items (and an auditable list of IGNORE titles underpayload.news_ignored).payload.news_sources— array with one entry per source row in the step-2 table, each{source, url, attempted, ok, item_count, note?}, populated from the per-source WebFetch outcomes recorded in step 2. This makes coverage gaps deterministic: a source that returned 403/error appears asok: falsewith anote, distinct from a reachable source that simply had no in-window items (ok: true, item_count: 0). The triager reads this instead of inferring reach from prose.- For each RELEVANT item that maps to a concrete repo action (e.g. "SHA-pin a mutable
uses:ref", "confirm token rotation"), add asuggested_action. - Update
summaryto one line covering both passes, e.g."OSV: 2 in-window; News: 3 RELEVANT, 4 TANGENTIAL (incl. GitHub Actions campaign)." - Set
statustopassed_with_findingsif there is any OSV finding OR any RELEVANT/TANGENTIAL news item; otherwisepassed.
Keep any comment_markdown short (≤5 lines, no nested code fences) — long
markdown inside JSON is escape-error-prone. The downstream triager reads the
structured payload, not prose.
Output
One stage-output.json at repo root:
{stage, status, summary, payload:{findings[], news[], news_ignored[], news_sources[], ...}}.
The OSV findings[] come from fetch.sh; you add news[], news_ignored[], and
news_sources[] (one {source, url, attempted, ok, item_count, note?} entry per
step-2 source, recording each WebFetch outcome so coverage gaps are deterministic).
The triager stage consumes this whole payload.
Required tools
Bash(bash .claude/skills/proactive-security-monitor/fetch.sh:*), Read,
WebFetch, Skill. (Write is granted by the platform.)