GitHub
10/10/2026, 11:43 AMvuln_finding, with a vuln_min_severity threshold:
| Threshold | Matches |
| --------- | ------------------------------------------- |
| kev | Known-exploited findings only |
| critical | Critical findings, plus all known-exploited |
| high | High and above, plus all known-exploited |
| medium | Medium and above, plus all known-exploited |
| low | Low and above, plus all known-exploited |
| any | Everything, including unknown severity |
Thresholds are case- and space-insensitive and are stored normalized. Environment and node scoping, cooldown and channels work as for every other source.
• Watcher in osctrl-tls (pkg/alerts/vuln_watcher.go). It runs every minute and needs both --alerts-enabled and --vuln-enabled on TLS.
• Cursor: a Redis key, osctrl:alert:vuln:cursor, holds the last finding ID processed. Restarts don't re-alert. A reopened finding keeps its ID, so it doesn't alert again.
• Missing cursor (first enable, Redis flushed): the watcher starts from the newest finding and never replays history.
• Redis error: the watcher skips the sweep instead of replaying findings.
• Possible findings never alert.
• Grouping. Findings are grouped per rule, environment and advisory, so one advisory affecting many nodes is one notification. Each node is counted once.
• A single-node hit keeps the entity node_uuid:advisory_id and the node attribution.
• A deleted node is named by its UUID.
• Storm cap. Each rule sends at most 10 advisories per environment per hour, worst first (KEV, then severity), plus one summary per hour for the rest.
• The budget is stored in Redis (osctrl:alert:vuln:budget:*), so it holds across sweeps and across osctrl-tls replicas.
• If Redis fails, the watcher falls back to a cap of 10 per sweep.
• API. vuln_min_severity is added to the rule create and update request and to the rule response. Creating or updating a vuln_finding rule returns 400 when vulnerability monitoring is off. OpenAPI is regenerated.
• UI. The rule form has a "New vulnerability findings" source and a "Minimum vulnerability severity" select, and the rules table shows the threshold. The source is offered only when vulnerability monitoring is enabled.
Posture score
• Extension point in `pkg/posture`:
• ScoringRule.Grade lets the evidence set a control's severity, and with it its weight;
• posture.EvidenceSource lets another subsystem add per-node evidence;
• PostureManager.ScoreNode scores one node from its posture records plus that evidence.
The node score handler and the fleet risk summaries (GetSummaryByNode(s)) use them. pkg/posture still doesn't import pkg/vulns.
• New control: "Known vulnerabilities" (ISO 27001 A.8.8). Rule key: known_vulnerabilities. It counts open confirmed findings:
| Open confirmed findings | Result |
| ------------------------------------- | ----------------------- |
| Any KEV or critical | Fail at critical weight |
| Otherwise any high | Fail at high weight |
| Otherwise medium, low or unknown | Warn |
| None, with some packages not assessed | Warn |
| None, everything assessed | Pass |
• Unevaluated, never a pass:
• before advisory data has loaded;
• for a node that has no findings and was never matched;
• for a node that has no findings and no assessed packages, for example Amazon Linux, an OS-only Windows node, or an ecosystem whose feed hasn't synced.
• Wiring. The control is added in osctrl-api only when both --posture-enabled and --vuln-enabled are on.
Matching (change to the backend from #1076)
• Packages in an ecosystem without a successful osv:<dir> sync now count as not assessed, as the spec requires. Before, they were matched against an empty advisory table and looked clean.
• "Not assessed" counts go up for such nodes until their feed syncs.
Health
• The health page gets an "Advisory feeds" item:
• degraded when a feed is failing;
• unknown before the first sync;
• stale after three missed sync intervals.
• It reads under the health request's timeout, so a stalled database can't hang the page.
• The item's name is translated in all 20 locales.
Docs
ARCHITECTURE.md, pkg/vulns/README.md (new Alerts, Posture score and Health sections) and the spec's implementation notes.
Security and operational impact
• Off by default. With --vuln-enabled off, alerting and posture behave as before. Posture scores are identical when no evidence source is wired, and a test covers this.
• Schema: one additive, nullable column, alert_rules.vuln_min_severity, added through AutoMigrate. No other model changes.
• Redis: two new key families, the cursor and the hourly budget, both under osctrl:alert:vuln:.
• Authorization: unchanged. The alert-rule endpoints keep their existing checks, and the health item sits behind the existing admin-only health status endpoint.
• Untrusted content: alert details include hostnames and package names from node inventory. This is the same trust model as existing log-based alerts.
• Notification volume is bounded per rule and environment per hour, including during an inventory profile rollout.
• Known limitations:
• A finding that later becomes known-exploited (added to CISA KEV) or rises in severity does not alert again. A kev rule fires only for findings that are already KEV when recorded. This is documented; an escalation cursor is a planned follow-up.
• A node's first inventory counts as new findings. The hourly cap bounds it.
• osctrl-tls replicas sweep without coordination; the shared budget bounds the total.
• Rollout:
1. Enable --vuln-enabled on the API and wait for the first sync.
2. Enable --vuln-enabled and --alerts-enabled on TLS.
3. Apply the inventory profile.
4. Create vuln_finding rules.
Validation
• gofmt -l pkg cmd (empty), go vet ./..., go build ./...
• `go test ./... -count=1`: all 55 packages pass
• go test ./pkg/alerts -race
• `golangci-lint run ./pkg/alerts/... ./pkg/posture/... ./pkg/vulns/... ./cmd/...`: 0 issues
• make openapi and `make openapi-check`: the only diff is vuln_min_severity on the rule request and response schemas
• `cd frontend && npm run check && npm test && npm run build`: 489 tests pass
• Mutation checks:
• removing the per-sweep cap fails the tests;
• bypassing the Redis budget fails the tests;
• dropping the "advisory data loaded" guard on the posture control fails the tests;
• dropping the "assessed package" guard fails the tests;
• ignoring the health request's timeout fails the tests.
• A fresh-context review of the whole branch was done. The issues it found are fixed with tests that failed first:
• the posture control passed unassessed nodes;
• the notification cap applied per sweep instead of per hour;
• the health item ignored the request timeout;
• alerts double-counted nodes.
• Not run:
• TestRedisVulnBudgetLive needs REDIS_URL, so it was skipped. Run `REDIS_URL=redis://… go test ./pk…
jmpsec/osctrlGitHub
10/10/2026, 12:09 PM