Know which of your projects and environments are still exposed to which known vulnerability, who owns the fix, and what has happened so far.
Security feeds tell you that something is vulnerable. Scanners tell you where a version is bad at one moment. Neither answers the question a lead actually asks on Monday morning: "Is it still open in prod, and who is on it?" cvewatch watches the feeds that matter to your stack, matches them to what every environment really runs, and keeps the open, fixed and ignored status per project and environment, with the evidence, on your own machine.
$ cvewatch update -env prod -min high
Open findings: 3
CRITICAL (2)
NEW CVE-2026-109618 shop: prod [affected] (+1 ids)
PHP Group PHP: PHP Memory Corruption Vulnerability
published 6d 9h ago · 2 articles · KEV, CISA due 2026-10-29 · EPSS 100% · official fix: action
NEW CVE-2026-103515 shop: prod [affected]
GitLab has remediated an issue in GitLab EE affecting all versions from 19.2…
published 6d 22h ago · 1 article · official fix: advisory
HIGH (1)
CVE-2026-108082 middleware: prod [unchecked]
Symfony: ReDoS in HeaderUtils::split() reachable through the Cache-Control request header
published 3h ago · 0 articles · official fix: 6.4.46
Illustrative output built from real feed data. Tracking is per environment, so the same CVE can be open in prod and fixed in staging.
What's new in 2.0
- Each project has its own update schedule, and the daemon refreshes only the projects that are due.
- Changes to a project, your technologies or the alert horizon are judged against the stored advisories at once, and older history is fetched as needed.
- Projects can belong to a vendor and are never mixed in lists, searches, exports or access rules.
- CVSS is read from every version (2.0 to 4.0), and the monitor reports its own health as GREEN, YELLOW or RED.
- Environments are free names, you can add technologies the catalog lacks, and the TUI screens are switched by letters.
Upgrading from 1.x: see CHANGELOG.md, which also lists what integrations will see.
| Watches | NVD, the CVE Program's own records (vendors such as Adobe, Cisco, Microsoft), GitHub advisories, CISA KEV, Hacker News, vendor CSAF feeds and any RSS or Atom feed |
| Tech-agnostic | a built-in catalog of ~145 open and closed source technologies, and any other in a few lines of YAML |
| Knows what you run | declared techs and versions, lockfiles and SBOMs, per project and environment; setup finds them in your repository, with evidence |
| Decides | what matters to you, whether your version is affected, and what to do first |
| Tracks | open, fixed, ignored or not-affected per environment, with owner, deadline, ticket and evidence |
| Tells | Slack, signed webhook, email, a daily digest, JSON/CSV/Markdown/SARIF exports, a status page, an MCP server |
| Shows | a full-screen TUI, a static dashboard and cvewatch status: is the monitor itself working? GREEN, YELLOW or RED, as for an Elasticsearch cluster, with the problems listed until they are fixed |
| Proves | a hash-chained audit log and an evidence pack per issue |
| Runs as | one binary, on your machine, no telemetry |
flowchart LR
subgraph feeds["Feeds, built in"]
A["NVD"]
B["GitHub advisories"]
C["CISA KEV"]
D["News, Hacker News, RSS"]
end
subgraph inv["What runs, per environment"]
I1["Declared techs and versions"]
I2["Lockfiles"]
I3["SBOMs and image refs"]
end
feeds --> M{{"Match and<br/>version check"}}
inv --> M
M --> L[("Ledger<br/>one record per issue,<br/>status per project and environment")]
L --> O1["Ranked list"]
L --> O2["Slack, webhook,<br/>email, daily digest"]
L --> O3["Export<br/>JSON, CSV, Markdown"]
L --> O4["MCP for agents"]
L --> O5["Evidence pack<br/>and audit log"]
The picture people care about is the exposure matrix: every project against every environment, so "fixed in staging, still open in prod" is visible at a glance.
CVE-2026-109618 (critical · KEV · EPSS 100%)
dev int staging prod
shop fixed fixed fixed OPEN owner alice · due Oct 31 · SEC-12
middleware - - - OPEN no owner yet
admin ignored - - not affected "feature disabled"
fixed = deployed OPEN = still exposed - = project has no such environment
cvewatch is tech-agnostic: nothing in it is specific to PHP, Node or any one vendor. What it covers is data you control, in three layers:
| Layer | What you give it | What you get |
|---|---|---|
| Technologies: runtimes, frameworks, databases, servers, platforms, appliances | a name and a version in the project file; the name must be in the catalog or be one of your own (see below) | advisories about that product are matched from NVD, KEV and news, and the version is checked against affected ranges where the feed has them |
| Packages | a lockfile or SBOM: composer, npm, Go, pip, poetry, Pipfile, Gradle, Maven, CycloneDX, SPDX | exact per-package matching, with each ecosystem's own version rules |
| Anything else | just the technology name | it is matched by name and the finding stays unchecked until a person confirms the version |
About 145 technologies ship in the box, open and closed source: runtimes, frameworks, databases, web and app servers, CMS and commerce platforms, CI/CD, container and cloud tooling, network and security appliances, operating systems. cvewatch catalog fortinet searches them. Only technologies a project declares are judged, so the size of the list adds no noise, and some carry their vendor's advisory feed, which switches on with them. Something missing? A few lines in config/techs.yaml add it (or replace a shipped entry; disabled: true removes one), for example Grafana:
# config/techs.yaml (holds only your additions; the shipped catalog is always there)
techs:
- name: grafana
aliases: [grafana]
cpe: [grafana:grafana] # the product name in the NVD CPE dictionary; optional, but it gives version checkscvewatch tech add grafana -cpe grafana:grafana -yes writes the same entry without editing YAML. A term the catalog does not know gets the closest names as suggestions, or can be kept as your own keyword with +term in setup; see technologies not in the catalog.
cvewatch doctor -online confirms the CPE name exists. A project in a language without a supported lockfile (Rust, Ruby, .NET, Swift...) is still covered at the technology level, and its packages are covered as soon as you export a CycloneDX or SPDX SBOM that uses a supported ecosystem. Package-level matching for more ecosystems is added the same way the existing ones were: one adapter.
| You are... | You want... | Start here |
|---|---|---|
| A standalone user running a few systems | an honest list of what to patch first, delivered where you already look | Standalone |
| A developer or integrator | to wire it into CI, tickets, SBOMs and your own tools | Developer / integrator |
| A software factory with several products and AI agents | one source of truth for exposure that people and agents share safely | MCP and software factories |
flowchart TB
core(["cvewatch core<br/>matching · ledger · audit"])
s["Standalone<br/>CLI · Slack · digest"] --- core
d["Developer<br/>SBOMs · export · webhooks · policy"] --- core
f["Software factory<br/>MCP · access rules · evidence"] --- core
"I run a webshop, a middleware and an admin tool. I need to know what to patch this week without reading ten feeds."
- Release archive. Download the archive for your system from the releases page: Linux and macOS (
.tar.gz) and Windows (.zip), each for amd64 and arm64, withchecksums.txt. It holdscvewatch,cvewatch-tui(the full-screen interface, started bycvewatch tui), the docs and the agent skills. Put both binaries in one folder on yourPATH. - Container image.
ghcr.io/b070nd/cvewatch:v2.0.0runscvewatch daemonas an unprivileged user. Mount your project files,/dataand/alerts; compose.yaml is an example. - From source.
go install github.com/b070nd/cvewatch/app/src/cmd/cvewatch@latest(and.../cvewatch-tui@latest) builds the default branch, not a release, andcvewatch versionthen saysdev.
mkdir cvewatch-home && cd cvewatch-home
cvewatch init # writes a starter config
cvewatch setup ~/code/shop # guided: finds technologies, versions and lockfiles, shows the evidence, asks you to confirm
cvewatch doctor # checks everything and tells you what to fix
cvewatch update # fetch the feeds now, then show what is open
cvewatch tui # or browse and triage in a full-screen interfaceTokens such as GITHUB_TOKEN (a higher GitHub advisory limit) or SLACK_WEBHOOK_URL can go in a .env or .env.local file next to the config; both programs read them at start, or name files with -env-file (details).
setup looks at lockfiles, .tool-versions, go.mod, package.json, Dockerfiles, compose files, Kubernetes manifests and Terraform, also in monorepos, and says why it believes each thing ("nginx 1.25.3: image in docker-compose.yml"). Guesses it is unsure about are asked, not declared. Paths that .gitignore ignores are skipped, so build output and vendored code do not turn into projects; -all reads them too. cvewatch init -repo ~/code/shop does the same without questions.
The technology catalog and the feeds are built in, so one binary is enough. setup writes a project file per repository. cvewatch setup FILE does the same for one Dockerfile, compose file or Kubernetes manifest, and lists its base images and services as infrastructure. Check the versions it could not guess: a repository says what is declared, not what runs in prod.
- Cuts the noise. An advisory only matches when it is about a product you run, so "a Moodle plugin written in PHP" does not raise your PHP alarm. Versions are checked per environment: affected, not affected or unchecked, using each ecosystem's own rules (PEP 440, Maven, semver).
- Puts the urgent things first. Order: actively exploited (CISA KEV), then severity, open in prod, exploit probability (EPSS, optional), age. Every issue says why it ranks where it does. CVSS is read from every version the sources carry (2.0 to 4.0), with its vector and who scored it.
- Merges the duplicates. The NVD entry, the GitHub advisory, the CISA listing and two news stories become one record with all links, the articles in order, and the known fix.
- Tells you where you already look. Slack, a signed webhook or email, plus a daily "still open in prod" digest.
- Refreshes each project on its own schedule. A project file can hold
update: {every: 1h}to be refreshed more or less often than theinterval, orauto: falseto be left out of scheduled runs. The daemon wakes when the first project is due and judges and alerts only the projects that are due.cvewatch updateanduin the TUI always cover every project. - Starts each project when you say. A
sincedate skips advisories published before it, for that project only, and closes the open findings from before it as not affected. Moving the date earlier reopens them. Other projects are not affected. - Keeps projects apart. A project can name its vendor, and the full-screen interface always works in one context, such as
acme ▸ shop: its lists, counts and actions hold that project only and never mix rows of two projects.Cchooses the context, and the screens are switched by the highlighted letters in the tab bar (OFPHS). When the monitor is YELLOW or RED, its problems are listed first on the Overview, until they are fixed. - Searches what you track.
cvewatch searchfinds issues by ID, part of an ID, words or fields such asseverity:high+andproject:shop, in every status. - Looks back only as far as you say.
horizoninconfig.yamllimits what is judged, alerted and digested to the past hour, day, week, month or year, or to a custom range. It is unlimited by default, and findings that already exist stay. - Names its environments.
local,dev,prodor any name you choose. A new project starts withlocal, and what differs in one environment is written once, in awhen@prod:block.
What comes first
┌──────────────┐ ┌──────────┐ ┌──────────────┐ ┌────────┐ ┌─────┐
│ KEV: already │ → │ severity │ → │ open in prod │ → │ EPSS │ → │ age │
│ exploited │ │ │ │ │ │ (opt.) │ │ │
└──────────────┘ └──────────┘ └──────────────┘ └────────┘ └─────┘
Monday. The digest lands in Slack: "8 open · 0 overdue · critical 2". You pick the first one:
cvewatch show CVE-2026-109618 # description, fix, articles, plan per environment
cvewatch why CVE-2026-109618 # "php@8.2.10: named in the advisory's product data, in the affected range"
cvewatch assign CVE-2026-109618 -project shop -env prod -owner alice -due 2026-10-31 -ticket https://tickets.example/SEC-12Wednesday. The fix goes to staging, then prod:
cvewatch evidence add CVE-2026-109618 -project shop -env prod -note "php 8.2.30 image deployed" -url https://ci.example/run/77
cvewatch fix CVE-2026-109618 -project shop -env staging -note "8.2.30"
cvewatch fix CVE-2026-109618 -project shop -env prod -note "8.2.30"A dev-only finding. You decide it does not apply and say why, on the record:
cvewatch not-affected CVE-2026-109619 -project shop -env dev -note "dev has no network exposure"Friday. A teammate asks what was done about it. One command answers with a timeline of detection, alerts, status changes, owners and evidence:
cvewatch evidence CVE-2026-109618Always on. Run cvewatch daemon (or the Docker image) and it refreshes every few hours. The full-screen interface re-reads the state every tui.refresh (30 seconds by default) and fetches the feeds when you press u. Without a running daemon it also updates the projects that are due by itself (tui.auto, on by default), so cvewatch status does not stay at "never succeeded"; with a daemon it leaves the fetching to it. tui.fetch in config.yaml sets a fixed period instead. If you upgrade a lockfile, the open findings are re-checked on their own and each finding's history says what changed. Editing a project, techs.yaml or the alert horizon works the same way: the daemon notices within about 15 seconds, judges the stored advisories again and fetches older ones when the change needs them.
flowchart LR
D(["detected<br/>per project and environment"]) --> O["open"]
O -->|"fix deployed"| F["fixed"]
O -->|"accepted risk"| I["ignored"]
O -->|"reason given"| N["not affected"]
Any closed finding can be reopened, and when your inventory changes the open ones are re-checked against the new versions. Every change is recorded: who, when and why, in the finding's history and in the audit log.
"I want vulnerability exposure in our pipelines, our ticket system and our dashboards, without a platform to host."
Inventory is declared per project and environment, and read fresh every cycle:
# config/projects/shop.yaml
name: shop
envs: [dev, prod] # any names; left out, the old default dev, int, staging, prod applies
techs: # generic: every environment runs these
- {name: php, version: "8.3"}
lockfiles:
- {type: composer, path: /repos/shop/composer.lock}
- {type: cyclonedx, path: /releases/shop-prod.cdx.json, env: prod} # the SBOM of what is deployed in prod
when@prod: # only what differs in prod
techs: {php: "8.2.30"}
image: "registry.example/shop@sha256:ab12..." # recorded, not scannedSupported inputs: composer, npm, Go (go.mod), pip, poetry, Pipfile.lock, Gradle, Maven and CycloneDX / SPDX SBOMs. cvewatch inventory shows what it believes runs where and what it could not read.
Example: releasing. Your pipeline writes shop-prod.cdx.json (from Syft, cdxgen or your build tool) when release 2.4 goes live. On the next cycle cvewatch notices the inventory changed, re-checks the open prod findings against the new versions, flips the ones that are now outside the affected range to not affected, and records php@8.2.10 → php@8.2.30; now not_affected in each finding's history. Nobody edits a status by hand.
cvewatch export -format json -env prod > open-prod.json # versioned, see docs/schema/export-v1.schema.json
cvewatch export -format csv -min high > open-high.csv # spreadsheet-safe
cvewatch export -format md > STATUS.md # for a wiki or a PR commentJSON from export, list -json and MCP is one document: schema_version, generated_at, count, issues. Feed text sits under untrusted. Pin your parser to schema_version.
Channels switch on when their secret is in the environment, so one config file is safe to share:
# config/config.yaml
notify:
webhook: {url_env: CVEWATCH_WEBHOOK_URL, secret_env: CVEWATCH_WEBHOOK_SECRET, min_severity: high}
email: {host: smtp.example.com, from: sec@example.com, to: [leads@example.com], username_env: SMTP_USER, password_env: SMTP_PASS}
digest: {daily_at: "08:00", env: prod}Webhook payloads are JSON, signed with HMAC-SHA256 in X-Cvewatch-Signature. Verify before you trust them:
import hmac, hashlib
expected = "sha256=" + hmac.new(secret, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers["X-Cvewatch-Signature"]):
abort(401)Point the webhook at a small bridge that opens Jira or GitLab issues, or at a Teams connector. Delivery is tracked per channel, so a failing mailbox never causes duplicate Slack alerts.
policy:
rules:
- {name: kev-prod, env: prod, kev: true, owner_within: 24h, fix_within: 168h}cvewatch policy || notify-oncall "exposure deadlines missed" # exits non-zero when a rule or a due date is broken- Add a feed type: one parser file in
app/src/feeds, registered by name. - Add a channel: implement
notify.Channel(name, send alerts, send digest). - Choose storage: a JSON file by default;
storage: {backend: sqlite}when several processes share it. - Test with confidence:
src/testkitruns real cycles over a temporary config, andsrc/goldenreplays synthetic NVD, GitHub, KEV and news items through the shipped configuration. A wrong CPE name intechs.yamlfails CI.
Architecture and seams: docs/architecture.md. Contributing: CONTRIBUTING.md.
"We build many products. People and AI agents both touch dependencies. We need one source of truth for exposure, and a clear line between what agents may read and what only humans decide."
claude mcp add cvewatch -- /path/to/cvewatch -root /path/to/cvewatch-home mcpRead-only by default. The agent can call:
| Tool | Answers |
|---|---|
list_findings, search_findings |
what is open, most urgent first, filtered by severity, vendor, project, environment; or issues matching a search query |
get_vulnerability, explain |
details with articles in order and fix evidence; why it matched |
get_evidence |
the full timeline and per-environment matrix for one issue |
policy_violations |
which deadlines are broken right now |
get_calendar |
what was published this month |
get_status |
whether the monitor itself is healthy: GREEN, YELLOW or RED, the problems behind it, daemon, feeds, channels |
search_catalog, suggest_tech, preview_tech_change, discover_stack, list_projects, check_projects, preview_project_change, get_settings, preview_settings_change |
set a project and the settings up: find technologies (and the closest names for a typo), read a repository, find problems and hints in project files, preview a change to a project, to your own technologies or to the settings |
Opt in to writing with mcp -allow-write, and the agent can also set_status, assign, attach_evidence, refresh, edit_project, edit_settings, edit_tech and manage_project. The skills in skills/ walk an agent through setup (cvewatch-setup) and triage (cvewatch-triage), asking the human at every step that matters.
Example: a dependency upgrade. A developer asks the agent to bump the packages of shop. Instead of guessing, the agent calls list_findings(project="shop", env="prod", min_severity="high"), sees that symfony/http-kernel 6.4.1 is affected with an official fix in 6.4.2, upgrades exactly that, opens the merge request, and calls attach_evidence(note="MR !412 upgrades http-kernel to 6.4.2", url=...). The status stays open. After the deploy a human runs cvewatch fix ... -env prod, because "fixed" is a statement about production, not about a branch.
sequenceDiagram
actor Dev as Developer
participant Agent
participant MCP as cvewatch MCP
participant Lead as Human with CLI
Dev->>Agent: Upgrade the shop dependencies
Agent->>MCP: list_findings(project shop, env prod, min high)
MCP-->>Agent: http-kernel 6.4.1 is affected, fix in 6.4.2
Agent->>Agent: Upgrade, open merge request
Agent->>MCP: attach_evidence(note, merge request link)
Note over MCP: status stays open
Lead->>MCP: fix, after the deploy to prod
- Feed text is untrusted data. Titles and descriptions come from public websites. They are cleaned on the way in, escaped on the way out and delivered under
untrusted; the server instructions tell the agent never to follow instructions found there. - Writes are opt-in and recorded. Every change is an audit-log entry with the actor (
mcp:<user>), and the log is hash-chained, so editing history is detectable. - Per-actor access.
access:inconfig.yamllimits which projects an actor sees and whether it may write;vendor:acmegrants every project of a vendor. An agent never sees projects it has no access to. - Nothing leaves the machine except the feeds you configure, your channels and NVD lookups of the CVSS score and (only if you enable it) EPSS lookups for tracked CVE ids.
access:
alice: {projects: [shop, admin], write: true}
ci-bot: {projects: ["*"], write: false}
acme-team: {projects: ["vendor:acme"], write: false}- One install, many products. Each repository gets a project file (
cvewatch setup DIR), each environment its own versions, SBOM and image reference. The ledger is shared, so a CVE that touches three products appears once, with three rows. Projects can belong to a vendor (a customer or team,vendor: acmein the project file); rows of different projects or vendors are never merged, and every list, search and export can be narrowed to one vendor or project. - A ritual that fits your process. A daily digest of what is still open in prod;
policyas the escalation trigger;assignfor owner, deadline and ticket. - Evidence on demand.
cvewatch evidence CVE-...is the pack you hand to an auditor or a customer: when it was published, when you detected it, who was told, who owned it, what was attached, when each environment was fixed, and whether the audit chain verified. docs/compliance.md explains how this supports NIS2 vulnerability-handling duties and the GDPR (it supports them; it does not make you compliant). - Plays with your scanners. Run Trivy, Grype or OSV-Scanner for detection and SBOM generation; let cvewatch own the multi-environment open/fixed truth and the deadlines.
Agent snippet for your CLAUDE.md and details: docs/agents.md.
| cvewatch | Scanners (Trivy, Grype, OSV-Scanner) | SBOM platforms (Dependency-Track) | Feed digests | |
|---|---|---|---|---|
| Finds vulnerable components in an artifact | no, you provide the inventory | yes | uses SBOMs | no |
| Status per project and environment | yes | no | per project | no |
| Merges the same issue across sources | yes | partly | partly | no |
| Owners, deadlines, evidence, audit trail | yes | no | some | no |
| One binary, data stays local | yes | yes | a hosted service | n/a |
| Gate a CI build on severity | list -fail-on high, SARIF export |
yes | via API | no |
- Versions are checked only where feeds give ranges, and only for what you say runs. NVD product data and GitHub advisories do; news and Hacker News do not, so those findings stay
uncheckedand a person confirms. - First runs can be noisy.
cvewatch doctorand the shipped CPE names remove most of it;excludes,match_structured,match_title_onlyand-mintune the rest. Config edits are judged against the stored advisories at once;cvewatch revalidateshows what they would change before-apply. - No distro or OS-package advisories (Debian, Alpine, Red Hat...) and no offline mode: use a scanner for what is inside an image, and cvewatch for who is exposed and what happened next.
- Package-level matching covers ten input formats today (composer, npm, Go, pip, poetry, Pipfile, Gradle, Maven, CycloneDX, SPDX). Other stacks are covered by technology name, not by package.
- Fixes are what feeds say. No package or container registry is queried and nothing is scanned. Image references are recorded, not verified.
- Authorization relies on the local identity. It is for people you can identify otherwise, not a public service.
- Windows: owner-only file modes are not enforced by the OS; keep
data/in the user profile. Linux, macOS and Windows are tested in CI.
Outbound requests go only to the feeds and channels you configure, plus NVD's CVE API for the CVSS score of tracked issues no feed rated (enrich.cvss, on by default) and FIRST's EPSS service if you turn it on. No telemetry. State and reports are created owner-only, secrets live in the environment and never in logs, and all feed text is treated as untrusted. Details and a threat model: docs/security.md. Report vulnerabilities privately as described in SECURITY.md.
- docs/overview.md: how it works, the terminal UI and its keys, projects and environments, inventory, matching, tracking, policy, channels, storage
- docs/search.md: look up one issue, search syntax and fields
- docs/architecture.md: modules and seams
- docs/agents.md: MCP tools, JSON contract, a
CLAUDE.mdsnippet - docs/compliance.md: NIS2 and GDPR support, evidence an auditor can ask for
- docs/schema/export-v1.schema.json: the data contract
- CONTRIBUTING.md, CHANGELOG.md
MIT © 2026 Botond Biro