diff --git a/docs/10-release-notes/index.md b/docs/10-release-notes/index.md index 7c6bfbb53..bb61bc66f 100644 --- a/docs/10-release-notes/index.md +++ b/docs/10-release-notes/index.md @@ -4,6 +4,18 @@ Release notes for LimaCharlie platform components, organized by date. --- +## 2026-05-09 + +### Vulnerability Management Uplift + +Major uplift to the Vulnerability Reporting extension and its surfaces. + +- **Canonical asset-tag namespace**: introduces the `lc:asset:*` tag convention (criticality, exposure, environment, owner, compliance) for cross-cutting asset metadata. The Vulnerability Reporting extension is the first consumer; the namespace is intended to be reused across LimaCharlie surfaces. See [Asset Tag Namespace](../2-sensors-deployment/asset-tags.md). +- **Vulnerability Reporting extension**: new public-facing documentation covering setup, scan modes (`scheduled` / `manual` / `all`), per-criticality SLA windows, criticality-tag overrides, KEV + EPSS enrichment, and the full action surface. See [Vulnerability Reporting](../5-integrations/extensions/limacharlie/vulnerability-reporting.md). +- **Lifecycle workflow (Phase 2)**: documented finding states (`open`, `in_progress`, `mitigated`, `accepted`, `false_positive`), accepted-exception expiries with auto-revert, and the new `vuln_finding.*` events that customers can route via Outputs to Jira, Slack, Cases, etc. The lifecycle ships in Phase 2 of the uplift; the documentation lands ahead of GA. + +--- + ## 2026-02-08 ### Endpoint Agent 4.33.26 diff --git a/docs/2-sensors-deployment/asset-tags.md b/docs/2-sensors-deployment/asset-tags.md new file mode 100644 index 000000000..91aadc806 --- /dev/null +++ b/docs/2-sensors-deployment/asset-tags.md @@ -0,0 +1,147 @@ +# Asset Tag Namespace (`lc:asset:*`) + +`lc:asset:*` is a reserved sensor-tag namespace for marking endpoints with structured asset metadata — criticality, network exposure, environment, owner, and compliance regimes. It is a convention layered on top of [Sensor Tags](sensor-tags.md): there is no separate field on the sensor model, no migration to run, and no per-surface schema to extend. Any LimaCharlie surface that needs asset context (Vulnerabilities, D&R, Cases, Search, Query Console, Outputs, etc.) reads the same tags and gets a consistent view. + +The first consumer of the namespace is the [Vulnerability Reporting extension](../5-integrations/extensions/limacharlie/vulnerability-reporting.md), which uses `lc:asset:criticality:*` to drive risk scoring and SLA windows. Other surfaces will adopt the same parser as they need asset context. + +## Why tags + +LimaCharlie tags are already the cross-cutting metadata mechanism for sensors: + +- They are visible on every event under `routing.tags`. +- They are queryable in D&R rules, sensor selectors, LCQL, and the API. +- They can be applied at enrollment time, by mass-tagging selectors, by D&R response actions, or manually through the web app and CLI. + +Adding a new sensor-model field for asset metadata would require schema changes, per-surface adoption, and a separate write path. A tag convention sidesteps all of that: every surface that already understands tags inherits the new metadata for free. + +## Schema + +The namespace defines five tag prefixes. The value follows the prefix as a third colon-separated segment. + +| Tag | Values | Cardinality | Purpose | +|---|---|---|---| +| `lc:asset:criticality:` | `critical`, `high`, `medium`, `low` | Singleton | Asset importance. Used as a risk-score multiplier and to drive priority sort and SLA windows. | +| `lc:asset:exposure:` | `internet-facing`, `dmz`, `internal` | Singleton | Network reachability. Feeds risk scoring and filter chips. | +| `lc:asset:env:` | `prod`, `staging`, `dev`, `test` | Singleton | Environment for filtering and suppression scoping. | +| `lc:asset:owner:` | Free text | Singleton | Routing target for assignment / paging (e.g. a team name, an email, a Slack handle). | +| `lc:asset:compliance:` | Free text (e.g. `pci`, `hipaa`, `sox`, `gdpr`) | Multi-value | Compliance regimes the asset is subject to. A sensor can carry several at once. | + +### Validation rules + +The closed-set fields (`criticality`, `exposure`, `env`) only accept the values listed above. Tags with malformed or unknown values for those fields are dropped by the parser — this prevents typos like `lc:asset:criticality:hi` from creating a phantom bucket in dashboards or SLAs. + +`owner` and `compliance` accept any non-empty value after the prefix. + +If a sensor carries multiple tags for the same singleton field (for example, both `lc:asset:env:prod` and `lc:asset:env:staging`), the parser picks the first match in lexical order. This is deterministic but should be avoided — fix the tags rather than rely on the resolution order. + +`compliance` values are deduplicated and sorted alphabetically when emitted as JSON, so `lc:asset:compliance:pci` plus `lc:asset:compliance:hipaa` always renders as `["hipaa","pci"]` regardless of tag order. + +## Applying tags + +Use the [`limacharlie` CLI](../6-developer-guide/cli.md) (or the API equivalents documented in [Sensor Tags](sensor-tags.md)). + +### Tag a single sensor + +```bash +limacharlie tag add --sid SENSOR_ID --tag lc:asset:criticality:critical +limacharlie tag add --sid SENSOR_ID --tag lc:asset:exposure:internet-facing +limacharlie tag add --sid SENSOR_ID --tag lc:asset:env:prod +limacharlie tag add --sid SENSOR_ID --tag lc:asset:owner:platform-team +limacharlie tag add --sid SENSOR_ID --tag lc:asset:compliance:pci +``` + +### Tag a fleet by selector + +Mass-tagging is the practical path for any non-trivial environment. The selector uses [sensor selector expressions](../8-reference/sensor-selector-expressions.md). + +```bash +# All Linux production hosts: env=prod +limacharlie tag mass-add \ + --selector 'plat == "linux" and "prod" in tags' \ + --tag lc:asset:env:prod + +# Engineering bench (already tagged 'bender') becomes dev +limacharlie tag mass-add \ + --selector '"bender" in tags' \ + --tag lc:asset:env:dev + +# Internet-facing tier picked up via existing 'edge' tag +limacharlie tag mass-add \ + --selector '"edge" in tags' \ + --tag lc:asset:exposure:internet-facing + +# All assets in the cardholder-data scope +limacharlie tag mass-add \ + --selector '"cde" in tags' \ + --tag lc:asset:compliance:pci +``` + +Tags applied via mass-add are persistent (no TTL) unless `--ttl` is passed. Re-running mass-add is idempotent. + +### Apply at enrollment time + +Installation keys can carry a fixed list of tags applied to every sensor that enrols against them. Bake the asset metadata into separate keys per asset class — for example, one key per environment + criticality combination — so the metadata lands the moment the sensor connects. + +### Apply via D&R rules + +D&R rules can add or remove tags as a response action. This is useful when asset state can be inferred from telemetry — for example, tagging a host as `lc:asset:exposure:internet-facing` when it starts answering on a public IP, or as `lc:asset:env:prod` based on a hostname pattern. + +```yaml +respond: + - action: add tag + tag: lc:asset:env:prod +``` + +## How surfaces consume the tags + +Each consumer surface uses a canonical parser that turns a sensor's tag set into a structured `AssetMetadata` object: + +- **Go:** `ParseAssetMetadata(tags)` returns an `AssetMetadata` struct with `Criticality`, `Exposure`, `Env`, `Owner`, and `Compliance` fields. Used by extensions and backend services. +- **TypeScript:** `parseAssetMetadataFromTags(tags)` mirrors the Go shape for use in the LimaCharlie web app and any TypeScript SDK consumer. + +Both implementations share the same prefix list, the same closed-set validation, and the same tie-breaking rules so a tag set is interpreted identically across the platform. + +The Vulnerability Reporting extension exposes the parsed metadata under an `asset_metadata` field on every endpoint and finding when `include_tags=true` is requested. See the [extension page](../5-integrations/extensions/limacharlie/vulnerability-reporting.md) for the response shape. + +## Override hatches + +Organizations that already run an asset taxonomy (for example, a long-standing `crown-jewel` / `tier-1` / `tier-3` scheme) can map their existing tags into the canonical buckets without re-tagging the fleet. + +Today the override is exposed by the Vulnerability Reporting extension as a `criticality_tag_overrides` configuration field: + +```json +{ + "criticality_tag_overrides": { + "crown-jewel": "critical", + "tier-1": "high", + "tier-3": "low" + } +} +``` + +The mapping is consulted only when no canonical `lc:asset:criticality:*` tag is present on the sensor. Explicit canonical tags always win, so an org can migrate gradually: leave the override map in place, start applying canonical tags to the most important assets, and remove the override entries as coverage grows. + +Override values must be one of the four canonical buckets; any other value is silently ignored at read time and rejected when the configuration is written. + +Other surfaces will adopt the same override pattern (or its equivalent) as they consume the namespace. + +## Sample real-world tagging + +A hypothetical SaaS company runs four classes of assets. The tag plan: + +| Asset class | Tags | +|---|---| +| Production app servers (public-facing, in PCI scope) | `lc:asset:criticality:critical`, `lc:asset:exposure:internet-facing`, `lc:asset:env:prod`, `lc:asset:compliance:pci`, `lc:asset:owner:platform-team` | +| Production database tier (internal, in PCI scope) | `lc:asset:criticality:critical`, `lc:asset:exposure:internal`, `lc:asset:env:prod`, `lc:asset:compliance:pci`, `lc:asset:owner:platform-team` | +| Staging cluster (DMZ, no compliance scope) | `lc:asset:criticality:medium`, `lc:asset:exposure:dmz`, `lc:asset:env:staging`, `lc:asset:owner:platform-team` | +| Engineering laptops (internal, dev work) | `lc:asset:criticality:low`, `lc:asset:exposure:internal`, `lc:asset:env:dev`, `lc:asset:owner:it-help` | +| HR file share (internal, in HIPAA + SOX scope) | `lc:asset:criticality:high`, `lc:asset:exposure:internal`, `lc:asset:env:prod`, `lc:asset:compliance:hipaa`, `lc:asset:compliance:sox`, `lc:asset:owner:hr-ops` | + +Driven by `limacharlie tag mass-add` calls keyed off existing infrastructure tags (e.g. an installation key per asset class, a hostname prefix, a cloud-provider label propagated via the cloud adapters), the entire fleet can be classified in a single pass and kept current as new sensors enrol. + +## See Also + +- [Sensor Tags](sensor-tags.md) — General tagging mechanism and API surface +- [Sensor Selector Expressions](../8-reference/sensor-selector-expressions.md) — Selector syntax used by mass-add +- [Vulnerability Reporting Extension](../5-integrations/extensions/limacharlie/vulnerability-reporting.md) — First consumer of the namespace +- [`limacharlie` CLI](../6-developer-guide/cli.md) — `tag add` / `tag mass-add` reference diff --git a/docs/5-integrations/extensions/limacharlie/index.md b/docs/5-integrations/extensions/limacharlie/index.md index 13c46a767..e680fef37 100644 --- a/docs/5-integrations/extensions/limacharlie/index.md +++ b/docs/5-integrations/extensions/limacharlie/index.md @@ -19,6 +19,7 @@ Extensions built and maintained by LimaCharlie that extend the platform with add - [Reliable Tasking](reliable-tasking.md) - Guaranteed task delivery to sensors - [Sensor Cull](sensor-cull.md) - Automatic cleanup of inactive sensors - [Usage Alerts](usage-alerts.md) - Usage threshold notifications +- [Vulnerability Reporting](vulnerability-reporting.md) - Per-endpoint software inventory, CVE resolution, KEV/EPSS enrichment - [YARA Manager](yara-manager.md) - YARA rule management ## See Also diff --git a/docs/5-integrations/extensions/limacharlie/vulnerability-reporting.md b/docs/5-integrations/extensions/limacharlie/vulnerability-reporting.md new file mode 100644 index 000000000..cc9756980 --- /dev/null +++ b/docs/5-integrations/extensions/limacharlie/vulnerability-reporting.md @@ -0,0 +1,841 @@ +# Vulnerability Reporting + +The Vulnerability Reporting extension (`ext-vulnerability-reporting`) collects per-endpoint software inventories, resolves them against the LimaCharlie CVE database, enriches each finding with CISA KEV and FIRST EPSS data, scores them with environment-aware risk, tracks per-finding resolutions across rescans, and surfaces the results in the LimaCharlie web app and via the extension API. + +It is the first consumer of the canonical [`lc:asset:*` tag namespace](../../../2-sensors-deployment/asset-tags.md): asset criticality, exposure, environment, owner, and compliance tags are read directly off the sensors and used to prioritize findings and scope filters. + +## What it does + +1. **Inventory collection.** A scheduled per-sensor `os_packages` task runs once a day and reports the installed software set. The default `scheduled` mode installs both the schedule and an ingest D&R rule that forwards only the tracked responses for analysis. +2. **CVE resolution.** Inventories are sent to `cve.limacharlie.io`, which maps each `(package_name, package_version)` pair to the set of CVEs that affect it. +3. **Enrichment.** Each CVE is joined against CISA KEV and FIRST EPSS via `cve.limacharlie.io/enrich`. KEV / EPSS / criticality multiplier are folded into a 0-100 [LC Risk](#lc-risk) score that is persisted on every finding row. +4. **Resolutions.** Every finding is implicitly **open** unless an operator records a resolution: `mitigated`, `accepted`, or `false_positive`. Resolutions are keyed by a deterministic [fingerprint](#finding-fingerprint) so they survive rescans. +5. **Daily scans.** A per-org daily tick runs three jobs: KEV-match emission, open-finding snapshot for the burndown tile, and EPSS-percentile snapshot for the per-CVE history sparkline. +6. **Surfacing.** Findings are exposed via the LimaCharlie web app's Vulnerabilities page (KPI strip, trend tiles, filter chip-bar, KEV/EPSS columns, LC Risk score, lifecycle chips, CVE / asset detail pages, exec / compliance / remediation reports) and via the extension API ([API Actions](#api-actions)). + +The extension is stateless aside from the per-org Spanner-backed tables (`vuln_reports`, `vuln_finding_state`, `vuln_daily_snapshots`, `vuln_epss_history`, plus rollup tables) and a small `org_value` keyed at `ext_vuln_kev_known_set`. + +## Setup + +Navigate to the [Vulnerability Reporting extension page](https://app.limacharlie.io/add-ons/extension-detail/ext-vulnerability-reporting) in the marketplace. Select the organization and click **Subscribe**. + +On subscription, the extension reconciles the D&R rules it owns to match the configured `scan_mode`: + +- **`scheduled` (default):** installs the daily per-sensor `os_packages` schedule (`ext-vuln-mgmt-schedule`) and the ingest rule that forwards tracked responses to `process_packages` (`ext-vuln-mgmt-os-packages`). Right default for almost every customer. +- **`manual`:** installs no D&R rules. The operator drives tasking and forwarding themselves. Useful when scan cadence needs to be coordinated with another scheduler. +- **`all`:** installs only the ingest rule, but it forwards every `os_packages_rep` event regardless of whether the response carries the extension's tracking marker. Useful when `os_packages` is already being collected on a separate cadence. + +Reconciliation is idempotent: changing modes on an existing subscription removes any rule the new mode does not own and upserts the rules it does. Unsubscribing removes all extension-owned rules and drops every `vuln_reports` row for the org. + +A dedicated webhook adapter is provisioned automatically so the [emitted events](#events-emitted) have a destination on the event stream. + +!!! info "Permissions" + The extension uses LimaCharlie's existing RBAC. Reading findings requires `extension.use` on `ext-vulnerability-reporting`. Subscribing and editing the configuration requires the standard extension management permissions. + +## Configuration + +The configuration is edited on the extension page in the LimaCharlie web app. All fields are optional; defaults match the table below. + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `scan_mode` | enum | `scheduled` | One of `scheduled`, `manual`, `all`. See [Setup](#setup). | +| `criticality_tag_overrides` | object | `{}` | Map of `{your-tag → canonical-bucket}` for organizations that already run their own asset-tag taxonomy. See [Asset Metadata](#asset-metadata). | + +### Example + +```json +{ + "scan_mode": "scheduled", + "criticality_tag_overrides": { + "crown-jewel": "critical", + "tier-1": "high", + "tier-3": "low" + } +} +``` + +`criticality_tag_overrides` is consulted only when a sensor carries no canonical `lc:asset:criticality:*` tag. Explicit canonical tags always win, so an organization can migrate gradually. Override values must be canonical buckets; any other value is rejected at write time. + +## Asset metadata + +The extension reads sensor tags in the [`lc:asset:*` namespace](../../../2-sensors-deployment/asset-tags.md) and uses them to: + +- **Prioritize findings.** `lc:asset:criticality:*` is the multiplier in the LC Risk score. +- **Scope filters.** `lc:asset:env:*` and `lc:asset:exposure:*` populate filter chips on the Vulnerabilities page. +- **Surface compliance views.** `lc:asset:compliance:*` is multi-value; an asset can carry several regimes. +- **Route assignments.** `lc:asset:owner:*` is exposed on the asset detail page so downstream workflows (Cases, Outputs to Slack/Jira/etc.) have the routing target available. + +When `include_tags=true` is passed to `query_endpoints` or `query_cve_vuln_hosts`, each row carries an `asset_metadata` projection of the parsed tags: + +```json +{ + "sid": "550e8400-e29b-41d4-a716-446655440000", + "hostname": "web-edge-001", + "asset_metadata": { + "criticality": "critical", + "exposure": "internet-facing", + "env": "prod", + "owner": "platform-team", + "compliance": ["pci"] + } +} +``` + +Tags with malformed values for the closed-set fields (`criticality`, `exposure`, `env`) are dropped by the parser. See [Asset Tag Namespace](../../../2-sensors-deployment/asset-tags.md) for the full schema. + +## Concepts + +### Lifecycle states + +A finding has exactly one of two postures: **open** (the default — there is no resolution row) or **resolved** (a row exists in `vuln_finding_state` carrying one of three resolutions). Resolutions are keyed by a [fingerprint](#finding-fingerprint), so they survive rescans. + +| Posture | `resolution` | Description | +|---------|--------------|-------------| +| open | — (no row) | New finding. Implicit; nothing is persisted. | +| resolved | `mitigated` | Compensating control in place; finding is no longer counted as exploitable. Sets `resolved_at` (used by MTTR). | +| resolved | `accepted` | Risk has been formally accepted as an exception, optionally with an `expires_at`. Lapses back into the open count when `expires_at` is in the past. | +| resolved | `false_positive` | Confirmed not applicable (resolver mis-mapped the package, etc.). | + +The resolution row carries six columns: `resolution`, `expires_at`, `case_number`, `resolved_at`, `resolved_by`, `updated_at`. There is no per-finding audit log — re-running `set_finding_resolution` overwrites in place. To **reopen** a finding, call `set_finding_resolution` with `resolution: null`; this deletes the row. + +`case_number` is reserved for an upcoming ext-cases linkage. It is plumbed end-to-end but not surfaced in the UI today; an operator opening a case from a finding will eventually have the case number persisted here so the resolution row points at the case carrying the richer metadata. + +#### Lapsed acceptance + +`accepted` is the only resolution that supports an `expires_at`. When `expires_at` is in the past at read time the UI derives a **lapsed acceptance** signal — the row is rendered with the same urgency as an open finding and surfaces in the lapsed-exception views. The Spanner row itself is **not** mutated; the lapsed signal is purely a `resolution === 'accepted' && expires_at < now` check at read time, so auditors can still see when and why the exception was originally granted. + +#### Scope (org vs host) + +Resolution rows are written at one of two scopes: + +- **`org`** — applies to every host carrying the same `(cve, normalized_package_name)`. Use when the suppression is package-wide ("we accept this CVE for `openssl` everywhere until the next quarterly upgrade"). +- **`host`** — applies to one specific `(cve, normalized_package_name, sid)`. Use when the rationale is host-specific ("this dev workstation is allowed to keep the older `curl` until reimage"). + +Read-overlay precedence: a host-scope row beats an org-scope one for the matching row. Both use the same fingerprint algorithm; the host fingerprint mixes in the `sid`. + +### Finding fingerprint + +A fingerprint is a SHA-256 hex digest that gives a finding a stable identity across rescans, reboots, and partial reinstalls. The inputs are NUL-separated: + +- **Org scope:** `SHA-256(cve + "\x00" + normalized_package_name)` +- **Host scope:** `SHA-256(cve + "\x00" + sid + "\x00" + normalized_package_name)` + +`normalized_package_name` is the resolver's canonical product name — e.g. both "Google - Chrome" and "Google Chrome" normalize to `chrome` — so a per-org "accept this risk for chrome" mark applies to both. Clients never compute fingerprints; the backend derives them from the canonical inputs and echoes them on the response. Frontends that already have a fingerprint can pass it back in directly. + +### LC Risk + +LC Risk is a 0-100 composite score the extension uses as the canonical prioritization key across the Vulnerabilities surface. It is computed at scan time during `ApplySensorReports` (when a sensor's resolved CVE set is being written to `vuln_reports`) and persisted on the row, so list-view sorts and filters do not have to recompute it on every read. + +#### Inputs + +- The row's CVSS severity (`critical` / `high` / `medium` / `low`). +- The CVE's EPSS percentile, fetched from `/enrich`. +- KEV membership for the CVE, also from `/enrich`. +- The host's `lc:asset:criticality:*` multiplier, parsed from the sensor's tags via the canonical [`lc:asset:*` namespace](../../../2-sensors-deployment/asset-tags.md). + +#### Formula + +```text +base = SEVERITY_RANK[severity] / 4 * 60 // 0..60 +base += epss_percentile * 25 // 0..25 (epss in [0,1]) +base += 15 if in_kev else 0 // flat bump +lc_risk = clamp(round(base * criticality_mult), 0, 100) + +SEVERITY_RANK: critical=4, high=3, medium=2, low=1, unknown=0 +CRITICALITY_MULT (backend): critical=1.6, high=1.3, medium=1.0, low=0.6, unknown=1.0 +``` + +A row whose host has no canonical `lc:asset:criticality:*` tag (and whose org has no matching override) uses multiplier `1.0`. + +!!! warning "Frontend / backend `low` multiplier mismatch" + The backend's `low` multiplier is `0.6`; the frontend's hardcoded preview formula in `src/utils/lcRisk.ts` uses `0.8`. The persisted `lc_risk` and `max_lc_risk` columns (what API responses and sorted UI columns read) are authoritative. The frontend constant only matters for client-side previews when the persisted value is missing. + +#### Persistence + +- **Per-host:** `vuln_reports.lc_risk` (one int per `(sensor, package, CVE)` row). +- **Org rollup:** `vuln_cve_counts.max_lc_risk` (max across all hosts for one CVE in the org). Returned as `max_lc_risk` on `query_cves` rows. + +#### Buckets + +The web app colour-buckets the score for badges: + +| Bucket | LC Risk | +|--------|---------| +| critical | ≥ 80 | +| high | 60-79 | +| medium | 40-59 | +| low | < 40 | + +### KEV (CISA Known Exploited Vulnerabilities) + +The extension surfaces the CISA KEV catalogue per CVE. Fields exposed (from `cve.limacharlie.io/enrich`): + +| Field | Type | Description | +|-------|------|-------------| +| `in_kev` | bool | True when the CVE is in the catalogue. | +| `added` | string (YYYY-MM-DD) | Date CISA added the CVE. | +| `due` | string (YYYY-MM-DD) | CISA's mandated remediation deadline (federal civilian agencies). | +| `vendor` | string | Vendor as listed by CISA. | +| `product` | string | Product as listed by CISA. | +| `name` | string | Vulnerability name as listed by CISA. | +| `ransomware` | bool | `true` iff CISA flagged "Known" ransomware-campaign use. | + +The KEV dataset is refreshed daily by the `vulnerability-db` ingest job. The data lives in Redis under the `kev:*` namespace and a refresh does not bust the resolver result cache — KEV/EPSS are fetched on the side via `/enrich`, not folded into `/cves`. + +### EPSS (FIRST Exploit Prediction Scoring System) + +Every CVE is also scored by FIRST.org's EPSS model. Fields exposed: + +| Field | Type | Description | +|-------|------|-------------| +| `score` | float [0,1] | Probability of in-the-wild exploitation in the next 30 days. | +| `percentile` | float [0,1] | Rank among all CVEs scored on this date. | + +EPSS is also refreshed daily. + +### EPSS history (90-day series) + +EPSS percentile drifts as new exploit telemetry lands. The extension captures a daily snapshot for each CVE the org has at least one finding on, persisted to `vuln_epss_history` keyed by `(oid, cve, snapshot_date)`. The capture happens during the daily Update tick (see [Daily Update tick](#daily-update-tick)). + +The frontend's CVE detail page renders a 90-day sparkline next to the live percentile, backed by `query_epss_history`. The default 90-day window matches the sparkline; pass a larger `days` (up to 365) to backfill a longer view for reports. + +CVEs that the org has never had a finding on are not retained — there is no historical series to query for them. CVEs whose findings have all closed retain the snapshots already captured during their open period. + +### Daily snapshots + +`vuln_daily_snapshots` is the per-day open-finding count by severity. The daily Update tick streams every `vuln_reports` row for the org, applies the resolution overlay (suppressing any row that has a resolution row that is **not** lapsed-accepted), and writes one row per `(snapshot_date, severity)` carrying: + +- `open_count` — total open findings in the bucket. +- `kev_count` — subset of `open_count` whose CVE is in KEV at the time of capture. + +A bucket with zero findings is still written on a quiet day so the burndown sparkline has continuity. Resolved findings are excluded by design — the snapshot represents "what the operator still owes," not "every CVE the resolver ever returned." Lapsed acceptances (where `expires_at` is in the past) flip back into the open count. + +### Daily Update tick + +The platform scheduler (`legion_extension_manager` / `legion_scheduler`'s `ext-update-event` cron) fires `EventTypes.Update` once per subscribed org per day, spread across 24h via `MultiplexOID`. The handler runs three scans sequentially with an independent 10-minute timeout per scan; one scan's failure does not suppress the others. + +| Order | Scan | Output | +|-------|------|--------| +| 1 | `kev_match` | Emits `vuln_finding.kev_match` for CVEs that just entered KEV AND for which the org still has open findings. Diffed against an `org_value` "previously-known KEV set". | +| 2 | `daily_snapshot` | Writes the per-severity open / KEV counts for today (see [Daily snapshots](#daily-snapshots)). | +| 3 | `epss_history` | Writes one EPSS row per distinct org CVE for today (see [EPSS history](#epss-history-90-day-series)). | + +The handler also re-reconciles D&R rules (idempotent) so a config change picks up on the next tick without requiring a manual re-subscribe. + +The 24h spread means events and snapshots are **not** real-time. A KEV addition published by CISA at 09:00 UTC will surface to org A around 14:00 UTC and org B around 03:00 UTC the next day; this is by design — orgs are sharded across the day to keep the cron's load steady. Customers needing sub-day KEV matching should subscribe to the upstream CISA RSS feed directly. + +### KEV / EPSS enrichment at read time + +The extension augments every CVE with KEV and EPSS data at read time via `cve.limacharlie.io/enrich` (POST `{"cves": [...]}`, returns `{"results": {"": {kev?, epss?, exploit_refs?}, ...}}`, capped at 200 CVEs per call). + +Enrichment is opt-in per request via `include_enrichment` (defaults to `true` for user-facing actions). Set it to `false` for cheap admin queries that don't need the merged view. + +When enrichment is included: + +- **KEV match:** each affected CVE in the response carries a `kev` block. +- **EPSS score:** each CVE carries an `epss` block. +- **Exploit references:** `query_cve` (single-CVE detail) returns an `exploit_refs` array (`source` ∈ `exploit-db` / `metasploit` / `packetstorm` / `github-poc` / `vendor-or-other`; `tier` ∈ `weaponized` / `poc`). The list-view actions deliberately omit exploit refs to keep page payloads small. + +## API actions + +All actions are invoked via the standard extension request endpoint: + +```bash +curl -s -X POST \ + "https://api.limacharlie.io/v1/extension/request/ext-vulnerability-reporting" \ + -H "Authorization: Bearer $LC_JWT" \ + -d oid="YOUR_OID" \ + -d action="" \ + -d data='' +``` + +The full request and response schemas live in the extension's `requestSchema()` declaration in [`refractionPOINT/ext-vulnerability-reporting`](https://github.com/refractionPOINT/ext-vulnerability-reporting). The web app's Vulnerabilities page is a reference consumer for every action listed below. + +### Read actions + +| Action | Purpose | +|--------|---------| +| `query_cves` | Paginated CVE rollup across the org. Sort by `cve` / `count` / `severity` / `lc_risk`. Returns `max_lc_risk` per row plus optional KEV/EPSS. | +| `query_endpoints` | Paginated endpoint rollup with vulnerability counts. Returns `asset_metadata` when `include_tags=true`. | +| `query_dashboard` | Index-based counts (severity, platform_string) powering the donut + bar charts. | +| `query_host_vuln_packages` | All vulnerable packages and their CVEs for one sensor. Sort by `cve` / `score` / `severity` / `lc_risk` / `package_name` / `package_name_package_version_cve`. Returns `lc_risk` and `fix_version` per row. | +| `query_cve_vuln_hosts` | All endpoints affected by one CVE. | +| `query_cve_vuln_packages` | All `(package_name, package_version)` pairs in the org affected by one CVE, with the count of distinct sensors per pair. | +| `query_cve` | Single-CVE detail blob. With `include_enrichment=true` returns the merged KEV / EPSS / exploit-refs view. | +| `query_epss_history` | EPSS percentile + score time series for one CVE. | +| `query_daily_snapshots` | Per-day open-finding counts (and KEV subset) for the burndown tile. | +| `list_finding_resolutions` | Page through `vuln_finding_state` for the org with optional scope / resolution filters. | + +### Write actions + +| Action | Purpose | +|--------|---------| +| `scan_packages` | Trigger an out-of-band `os_packages` scan against a specific sensor. | +| `set_finding_resolution` | Set or clear a finding's resolution. Pass `resolution: null` to reopen (delete the row). | +| `bulk_set_finding_resolution` | Apply a resolution change across up to 100 findings in one call. | +| `reset_asset_findings` | Wipe every stored finding for one sensor (for reformat / reimage / decommission). Org-scope fingerprints that were only on this sensor fire `vuln_finding.closed`. | + +### Internal action + +| Action | Purpose | +|--------|---------| +| `process_packages` | Internal callback fired by the ingest D&R rule. Not user-facing. | + +### Action reference + +#### `query_cves` + +Request: + +```json +{ + "cursor": "", + "limit": 100, + "sort_by": "lc_risk", + "sort_asc": false, + "filters": { "severity": ["critical", "high"] }, + "search": { "field": "cve", "op": "contains", "value": "2024" }, + "include_tags": false, + "include_enrichment": true, + "filter_via_state": true +} +``` + +Response: + +```json +{ + "results": [ + { + "cve": "CVE-2024-12345", + "count": 42, + "severity": "critical", + "max_lc_risk": 92, + "kev": { "in_kev": true, "added": "2024-08-12", "due": "2024-09-02", "vendor": "Acme", "product": "Foo", "name": "...", "ransomware": false }, + "epss": { "score": 0.92, "percentile": 0.99 }, + "resolution": null + } + ], + "next_cursor": "100", + "total_return_count": 1 +} +``` + +Filters supported: `severity`, `kev_only`, `epss_min`, `resolution`, `criticality`, `env`, `exposure`. `kev_only` and `epss_min` force-enable `include_enrichment`. `filter_via_state` (default `true`) suppresses CVEs whose org-scope rollup is fully resolved. + +#### `query_endpoints` + +Request: + +```json +{ + "limit": 50, + "sort_by": "hostname", + "sort_asc": true, + "filters": { "platform_string": ["linux"], "criticality": ["critical"] }, + "include_tags": true +} +``` + +Response: + +```json +{ + "endpoints": [ + { + "sid": "550e8400-...", + "hostname": "web-edge-001", + "platform_string": "linux", + "platform": 1, + "count": 17, + "severity": "critical", + "asset_metadata": { "criticality": "critical", "exposure": "internet-facing", "env": "prod", "owner": "platform-team", "compliance": ["pci"] } + } + ], + "cursor": "", + "total_return_count": 1 +} +``` + +#### `query_host_vuln_packages` + +Request: + +```json +{ + "sid": "550e8400-...", + "sort_by": "lc_risk", + "sort_asc": false, + "limit": 100, + "filters": { "severity": ["critical", "high"] } +} +``` + +Response (one row per `(package_name, package_version, cve)`): + +```json +{ + "packages": [ + { + "cve": "CVE-2024-12345", + "package_name": "openssl", + "package_name_package_version_cve": "openssl 3.0.2 CVE-2024-12345", + "normalized_package_name": "openssl", + "score": 9.8, + "severity": "critical", + "fix_version": "3.0.13", + "lc_risk": 87, + "kev": { "in_kev": true, "...": "..." }, + "epss": { "score": 0.81, "percentile": 0.98 }, + "resolution": null + } + ], + "cursor": "", + "total": 1 +} +``` + +#### `query_cve_vuln_hosts` + +Request: + +```json +{ + "cve": "CVE-2024-12345", + "normalized_package_name": "openssl", + "include_tags": true, + "limit": 100 +} +``` + +Pass `normalized_package_name` so the resolution overlay can compute host-scope fingerprints; without it, only org-scope resolution hits land. Filters supported: `platform`, `platform_string`, `criticality`, `env`, `exposure`, `resolution`. + +#### `query_cve` + +Request: + +```json +{ "cve_id": "CVE-2024-12345", "include_enrichment": true } +``` + +Response: + +```json +{ + "cve": { "id": "...", "descriptions": [...], "metrics": {...}, "references": [...], "configurations": [...] }, + "kev": { "in_kev": true, "..." : "..." }, + "epss": { "score": 0.92, "percentile": 0.99 }, + "exploit_refs": [ + { "source": "exploit-db", "url": "https://...", "tier": "weaponized" } + ] +} +``` + +`exploit_refs` is the only place exploit references are returned. List-view actions deliberately omit them. + +#### `query_epss_history` + +Request: + +```json +{ "cve": "CVE-2024-12345", "days": 90 } +``` + +`days` defaults to `90`, capped at `365`. CVE must start with `CVE-`. Response is ordered `snapshot_date ASC` so it can be plotted directly: + +```json +{ + "history": [ + { "snapshot_date": "2026-02-08", "score": 0.0123, "percentile": 0.42 }, + { "snapshot_date": "2026-02-09", "score": 0.0145, "percentile": 0.45 } + ] +} +``` + +CVEs with no historical coverage in the requested window return `{ "history": [] }`. + +#### `query_daily_snapshots` + +Request: + +```json +{ "days": 30, "severities": ["critical"] } +``` + +`days` defaults to `30`, capped at `365`. `severities` defaults to all four canonical buckets. Response is ordered `(snapshot_date ASC, severity ASC)`: + +```json +{ + "snapshots": [ + { "snapshot_date": "2026-04-30", "severity": "critical", "open_count": 42, "kev_count": 5 }, + { "snapshot_date": "2026-05-01", "severity": "critical", "open_count": 39, "kev_count": 5 } + ] +} +``` + +#### `set_finding_resolution` + +Request: + +```json +{ + "scope": "host", + "cve": "CVE-2024-12345", + "normalized_package_name": "openssl", + "sid": "550e8400-...", + "resolution": "accepted", + "expires_at": "2026-06-15T00:00:00Z", + "case_number": 12345 +} +``` + +Field rules: + +- `scope` (`org` | `host`) is required. +- Either `fingerprint` OR `(cve, normalized_package_name)` must be supplied. `sid` is required when `scope=host`. +- `resolution` is one of `mitigated` / `accepted` / `false_positive`, or `null` to **reopen** the finding (deletes the row). +- `expires_at` (RFC3339) is only valid when `resolution=accepted`. It is optional; an accepted resolution without an `expires_at` never lapses. +- `case_number` is optional. It is reserved for upcoming ext-cases linkage; the field is plumbed through end-to-end but unused by the UI today. + +Response: + +```json +{ + "data": { + "fingerprint": "a3f2…", + "scope": "host", + "resolution": "accepted", + "expires_at": "2026-06-15T00:00:00Z", + "case_number": 12345, + "resolved_at": "2026-05-10T18:30:00Z", + "resolved_by": "alice@example.com", + "updated_at": "2026-05-10T18:30:00Z" + } +} +``` + +When `resolution=null` is passed the row is deleted and the response carries `resolution: null` with the now-cleared metadata fields. `resolved_at` is set to wall-clock time when the resolution becomes `mitigated`, so MTTR is derived from `(resolved_at - first_seen_at)` where `first_seen_at` comes from the matching `vuln_reports` row. + +#### `bulk_set_finding_resolution` + +Apply up to 100 resolution changes in one call. Each item carries the same fields as `set_finding_resolution`. Items are committed independently; partial successes are reported per-item. + +```json +{ + "targets": [ + { "scope": "org", "cve": "CVE-2024-12345", "normalized_package_name": "openssl", "resolution": "mitigated" }, + { "scope": "host", "cve": "CVE-2024-22222", "normalized_package_name": "openssl", "sid": "550e8400-...", "resolution": "false_positive" } + ] +} +``` + +Response: + +```json +{ + "data": { + "applied": 2, + "results": [ + { "index": 0, "fingerprint": "a3f2…", "resolution": "mitigated", "...": "..." }, + { "index": 1, "fingerprint": "b771…", "resolution": "false_positive", "...": "..." } + ], + "errors": [] + } +} +``` + +Items over 100 reject the entire call with HTTP 400. + +#### `list_finding_resolutions` + +Page through resolution rows in the org. The list returns **only resolved rows** — open findings have no row to enumerate. All filters are optional and stack as AND. + +```json +{ + "scope": "host", + "resolutions": ["accepted", "mitigated"], + "limit": 100, + "cursor": "" +} +``` + +Response: + +```json +{ + "data": { + "resolutions": [ + { + "fingerprint": "a3f2…", + "scope": "host", + "resolution": "accepted", + "expires_at": "2026-06-15T00:00:00Z", + "case_number": null, + "resolved_at": "2026-05-10T18:30:00Z", + "resolved_by": "alice@example.com", + "updated_at": "2026-05-10T18:30:00Z" + } + ], + "next_cursor": "..." + } +} +``` + +`limit` defaults to 100; the response is ordered `(scope ASC, fingerprint ASC)`. Lapsed acceptances are returned with `resolution=accepted` and an `expires_at` in the past — the lapsed signal is a derived UI check (`resolution === 'accepted' && expires_at < now`), not a separate enum value. + +#### `scan_packages` + +Trigger an out-of-band scan for one sensor: + +```json +{ "sid": "550e8400-..." } +``` + +Returns immediately; the scan completes asynchronously when the sensor reports back via the ingest D&R rule. + +#### `reset_asset_findings` + +Wipe every stored finding for one sensor. Use when the host has been reformatted, reimaged, or decommissioned and the existing findings no longer reflect reality. The next legitimate package scan repopulates findings from scratch. + +```json +{ "sid": "550e8400-..." } +``` + +Response carries the number of org-scope fingerprints that the reset cleared from the org entirely (one `vuln_finding.closed` event fires per cleared fingerprint): + +```json +{ "data": { "sid": "550e8400-...", "closed": 17 } } +``` + +Side effect: `vuln_endpoint_scans.last_scan_at` is stamped to the reset time, matching the semantic "operator declared this asset clean at this time". The next real package scan overwrites it. + +## Events emitted + +The extension emits the following events through LimaCharlie's standard webhook adapter. Customers route them via Outputs to Jira, Slack, Cases, PagerDuty, etc. + +| Event | When fired | Notable fields | +|-------|-----------|----------------| +| `vuln_finding.created` | A new finding lands for an asset (rescan write path detected a new `(oid, fingerprint)` tuple). | `cve`, `severity`, `score`, `sid`, `hostname`, `kev`, `epss`, `first_seen` | +| `vuln_finding.closed` | The last sensor holding `(cve, normalized_package_name)` cleared it on a rescan, so the org-scope fingerprint is gone. Also fires per cleared fingerprint when `reset_asset_findings` wipes a host. | `cve`, `severity`, `score`, `sid`, `hostname`, `fingerprint` | +| `vuln_finding.kev_match` | A CVE just entered CISA KEV AND the org still has at least one open finding for it. | `cve`, `kev`, `epss` | +| `vuln_finding.state_changed` | `set_finding_resolution` / `bulk_set_finding_resolution` succeeded (including reopens, where the embedded resolution row carries `scope` + `fingerprint` + `updated_at` and the resolution-related fields are nil). | `fingerprint`, embedded `resolution` row (`scope`, `resolution`, `expires_at`, `case_number`, `resolved_at`, `resolved_by`, `updated_at`) | + +Every event carries `event_type`, `oid`, and an optional `fingerprint`. Event delivery is best-effort: a failed webhook is logged at warn level and does not roll back the underlying state mutation. + +### Example output configuration + +The same Outputs surface used for detections can route vulnerability events. To send `vuln_finding.kev_match` to Slack: + +```yaml +name: vuln-kev-to-slack +module: slack +type: event +filter_event_type: vuln_finding.kev_match +slack_api_token: hive://secret/slack-webhook +slack_channel: "#vuln-priority" +``` + +## Web UI + +The web app's Vulnerabilities section is the primary surface. It mirrors the API: + +- **KPI strip** — Total findings, KEV in environment, Open critical, Critical assets. +- **Trend tiles** — MTTR by severity (driven by `resolved_at`), KEV coverage, Exposure score, and a 30-day open-critical burndown sparkline backed by `query_daily_snapshots`. +- **Filter chip-bar** — multi-select chips for severity, criticality, exposure, environment, KEV-only, EPSS bucket (`top-1` / `top-5` / `top-10` / `any`), and resolution (`open` / `mitigated` / `accepted` / `false_positive` / lapsed-accepted). +- **Charts** — donut by severity + bar by platform; both reflect the active filter when one is set. +- **Endpoints / CVEs tabs** — sortable columns including `lc_risk`, KEV badge, EPSS badge, resolution chip, asset-metadata cell. Bulk-select on the CVEs tab opens the resolution modal for batch resolution changes. +- **CVE detail page** — affected hosts, affected packages, full CVE description, references, the merged KEV/EPSS view, exploit references, a 90-day EPSS sparkline backed by `query_epss_history`, the current resolution row (if any), and a one-click "Run a hunt" that opens an LCQL hunt deeplinked to the CVE. +- **Asset detail page** — per-sensor view of vulnerable packages, asset metadata projection, top fix-version recommendation with hunt deeplink, and the resolutions currently in effect for the host. +- **Header exports** — PDF (current view), CSV (current view), full-bundle ZIP, Executive PDF, Compliance pack PDF, Remediation plan CSV. + +## Workflows + +Concrete operator playbooks. Each workflow is a numbered sequence; substitute ``, ``, etc. as appropriate. Examples assume the `limacharlie` CLI or a `curl` against the extension request endpoint. + +### 1. First-time setup + +1. Subscribe to the extension on the [marketplace page](https://app.limacharlie.io/add-ons/extension-detail/ext-vulnerability-reporting) (default `scan_mode: scheduled`). +2. Tag your sensors. At minimum `lc:asset:criticality:*`; ideally also `exposure`, `env`, `owner`, and `compliance`. Mass-tag by selector: + ```bash + limacharlie tag mass-add \ + --selector 'plat == "linux" and "edge" in tags' \ + --tag lc:asset:criticality:critical + limacharlie tag mass-add \ + --selector 'plat == "linux" and "edge" in tags' \ + --tag lc:asset:exposure:internet-facing + ``` +3. Wait for the first scheduled scan (within 24h) or trigger an out-of-band one for a representative host: + ```bash + curl -s -X POST "$LC_API/v1/extension/request/ext-vulnerability-reporting" \ + -H "Authorization: Bearer $LC_JWT" \ + -d oid="$OID" -d action="scan_packages" \ + -d data='{"sid":""}' + ``` +4. Open the Vulnerabilities page. The KPI strip and donut populate first; the trend tiles and burndown sparkline populate after the first daily Update tick (within 24h of subscribe). + +### 2. Triage a critical finding + +1. Vulnerabilities page → CVEs tab → sort by **LC Risk DESC** (default). +2. Click the row to open the side drawer. Review: + - KEV block — is exploitation observed? + - EPSS percentile — and the 90-day trend on the CVE detail page (`query_epss_history`) — is it climbing? + - Affected hosts (`query_cve_vuln_hosts`) — what asset criticality / exposure mix? +3. From the CVE detail page click **Run a hunt** — the deeplink seeds an LCQL hunt with the CVE context for live investigation. +4. Decide: + - If a compensating control is in place → **Set resolution → mitigated**. `resolved_at` is stamped and the finding drops out of the open count. + - If the operator is going to actively patch → leave the finding as `open` (no resolution row); the burndown sparkline tracks remediation by attrition (the rescan removes the row when the patch lands). + - If business has formally accepted the risk → **Set resolution → accepted** with an optional `expires_at`. + +### 3. Track remediation progress + +1. The dashboard's burndown sparkline is the daily total of open critical findings (`query_daily_snapshots`, `severities=["critical"]`, `days=30`). Slope is the operational signal. +2. The MTTR tile reads `resolved_at - first_seen_at` per severity. It populates only after the first finding is set to `mitigated`. +3. Rolling exports for steering committees: header → **Executive PDF** for an exec audience or **Remediation plan (CSV)** for ticket-import-ready output. + +### 4. Risk acceptance + +When a finding cannot be patched in time and the business formally accepts the risk: + +1. CVE row → **Set resolution → accepted**. +2. Optionally set `expires_at` (RFC3339, in the future). An accepted resolution without an `expires_at` never lapses. +3. The finding drops out of the open count until `expires_at`. When `expires_at` is in the past the UI derives a **lapsed acceptance** signal at read time — the row renders with the same urgency as an open finding so the operator knows to revisit, and the daily snapshot counts it back as open. +4. To extend, call `set_finding_resolution` again with a new `expires_at` (the row is upserted in place; `resolved_at` and `resolved_by` are refreshed). +5. To formally close, transition to `mitigated` once the patch lands. To reopen, pass `resolution: null` (deletes the row). + +For richer per-finding context (rationale, evidence, multi-party sign-off, comments), open a case from the finding — the upcoming ext-cases linkage will record the case number on the resolution row. + +```bash +curl -s -X POST "$LC_API/v1/extension/request/ext-vulnerability-reporting" \ + -H "Authorization: Bearer $LC_JWT" \ + -d oid="$OID" -d action="set_finding_resolution" \ + -d data='{ + "scope": "org", + "cve": "CVE-2024-12345", + "normalized_package_name": "openssl", + "resolution": "accepted", + "expires_at": "2026-09-01T00:00:00Z" + }' +``` + +### 5. Compliance evidence export + +1. From the page header → **Export ▾** → **Compliance pack**. +2. The frontend pre-fetches per-host packages for every host with severity ≥ High (bounded 4-way concurrency), then renders a multi-page evidence pack mapped to SOC 2 / ISO 27001 control vocabulary. +3. Resolution evidence is included per finding: `accepted` rows surface as "Risk accepted" (with `expires_at` if set); `mitigated` rows surface with `resolved_at` + `resolved_by`. +4. The `lc:asset:compliance:*` tag drives the per-framework breakdown — a host tagged `lc:asset:compliance:pci` shows up under PCI; multi-tagged hosts appear in each. + +### 6. Host-centric review + +1. Vulnerabilities page → Endpoints tab. Filter by `criticality:critical` or `exposure:internet-facing` to focus. +2. Click a row → side drawer shows asset metadata + per-host findings + top fix-version recommendation with a hunt deeplink. +3. From the asset detail page, bulk-select rows on the host vulnerabilities table → set resolution → `mitigated` (host scope) for findings the host has individually patched. + +### 7. EPSS trend monitoring + +1. CVE detail page → 90-day EPSS sparkline (top right, next to the live percentile badge). +2. A climbing slope is the early-warning signal that a CVE is becoming dangerous before it lands in KEV. Promote any CVE whose EPSS percentile is rising fast and that has no resolution row yet. +3. For longer reports, raise `days` (capped at 365): + ```json + { "cve": "CVE-2024-12345", "days": 365 } + ``` + +### 8. Bulk operations + +1. CVEs tab → multi-select rows. +2. Bulk action → **Set resolution** opens the resolution modal with the selected fingerprints as targets. Up to 100 per call. +3. Each item is committed independently; the response carries per-item `applied` count and per-item errors so a typo on row 7 doesn't roll back rows 1-6. +4. The frontend uses the response's `results[]` directly — no follow-up `list_finding_resolutions` round trip is needed. + +## Best practices + +### Choosing a resolution + +| Situation | Resolution | +|-----------|------------| +| Detected, no work yet started | (leave implicit `open` — no row) | +| Patching is in flight | (leave as `open`; the rescan removes the row when the patch lands) | +| Compensating control blocks exploitation; finding no longer counts as exploitable | `mitigated` | +| Patch lands, rescan confirms gone | (no action — the row drops out of `vuln_reports` on the next scan) | +| Cannot patch in the desired window, business-accepted exception | `accepted` (optional `expires_at`) | +| Resolver false positive (wrong product / wrong version) | `false_positive` | + +Do not use `mitigated` for "patch in progress" — `resolved_at` is stamped on entry, which would skew MTTR. Leave the finding open until the patch lands or a compensating control is documented. + +### Per-finding audit and collaboration + +Per-finding audit detail is intentionally minimal — `resolved_at`, `resolved_by`, and `updated_at` are the only timestamps recorded, and updates overwrite in place. For richer collaboration (assignee, comments, evidence, classification, multi-party sign-off), use the upcoming **ext-cases** linkage: the `case_number` field on a finding's resolution row is reserved for this. When that integration ships, opening a case from a finding will populate `case_number` and the case will carry the audit history that the resolution row deliberately does not. + +### Tagging sensors meaningfully + +Suggested tag combos for common environments: + +- **DMZ web server:** `lc:asset:criticality:critical`, `lc:asset:exposure:internet-facing`, `lc:asset:env:prod`, `lc:asset:owner:platform-team`, `lc:asset:compliance:pci`. +- **Internal worker:** `lc:asset:criticality:high`, `lc:asset:exposure:internal`, `lc:asset:env:prod`, `lc:asset:owner:platform-team`. +- **Dev workstation:** `lc:asset:criticality:low`, `lc:asset:exposure:internal`, `lc:asset:env:dev`, `lc:asset:owner:it-help`. +- **Build runner / CI:** `lc:asset:criticality:medium`, `lc:asset:exposure:internal`, `lc:asset:env:dev`, `lc:asset:owner:platform-team`. + +Drive these via `limacharlie tag mass-add` keyed off existing infrastructure tags or installation keys, so newly-enrolled sensors land already classified. See [Asset Tag Namespace](../../../2-sensors-deployment/asset-tags.md#sample-real-world-tagging) for a fuller pattern. + +### LC Risk vs raw CVSS + +CVSS severity is environment-blind: a CVSS 9.8 critical scores the same on a dev laptop and on the customer-facing API gateway. LC Risk corrects for that by multiplying in the asset-criticality bucket — a `low` host caps roughly half the score, and a `critical` host inflates it by 60%. Always sort by LC Risk first; fall back to CVSS only when explaining the score externally. + +### Tracking remediation deadlines + +The extension does not enforce a built-in remediation SLA — there is no SLA-window configuration, no per-criticality deadline persisted on findings, and no SLA-breach event. The signals that **are** available for cadence-tracking are: + +- `vuln_finding.created` — for stamping a target deadline at ingest in your ticketing system. +- `first_seen_at` (returned on every finding row) — the basis any external SLA calculation should key off. +- `vuln_finding.closed` and `mitigated`-resolution `vuln_finding.state_changed` events — for closing the loop and computing MTTR externally. + +If you need deadline alerts, wire the per-criticality clock in your downstream system (Jira / Linear / PagerDuty) using `first_seen_at` + `criticality` from the event payload. + +### "Near-real-time" expectations + +The daily Update tick is per-org, spread across 24h. KEV-match alerts and snapshot writes can lag by up to a full day for any one org. This is deliberate — the cron's load is steady rather than spiking. If you need sub-hour KEV detection, subscribe to the upstream CISA RSS feed in addition to this extension. + +### Integrating with downstream Outputs + +Route the `vuln_finding.*` events to your existing alerting pipeline. A typical wiring: + +- `vuln_finding.kev_match` → Slack `#vuln-priority` + page on-call. +- `vuln_finding.created` → ticketing system (Jira, Linear) so each new finding gets a tracked owner. High-volume on first scan; often filtered to `severity=critical` only. +- `vuln_finding.state_changed` → SIEM. Each event carries the full resolution row, so a downstream consumer can rebuild the change feed without a follow-up read. +- `vuln_finding.closed` → ticketing system (auto-resolve the matching ticket) and SIEM. + +## Glossary + +| Term | Meaning | +|------|---------| +| **CVE** | Common Vulnerabilities and Exposures — public catalogue of disclosed vulnerabilities (`CVE-YYYY-NNNN`). Maintained by MITRE. | +| **NVD** | National Vulnerability Database — NIST's CVE feed with CVSS scores, CPEs, references. The resolver's primary data source. | +| **CVSS** | Common Vulnerability Scoring System — 0-10 numeric severity score plus `low`/`medium`/`high`/`critical` bucket. | +| **CPE** | Common Platform Enumeration — structured product identifier used by NVD configurations to express which software a CVE applies to. | +| **KEV** | CISA's Known Exploited Vulnerabilities catalogue. ~1500 entries; refreshed daily. | +| **EPSS** | FIRST.org's Exploit Prediction Scoring System. Per-CVE probability + percentile of in-the-wild exploitation in the next 30 days. | +| **LC Risk** | LimaCharlie's 0-100 environment-aware risk score. Persisted per-finding; see [LC Risk](#lc-risk). | +| **MTTR** | Mean Time To Remediation. Computed from `(resolved_at - first_seen_at)` per severity bucket. | +| **Criticality** | Asset importance bucket (`critical`/`high`/`medium`/`low`). Source: `lc:asset:criticality:*` or the configured override map. | +| **Exposure** | Network reachability bucket (`internet-facing`/`dmz`/`internal`). Source: `lc:asset:exposure:*`. | +| **Env** | Environment bucket (`prod`/`staging`/`dev`/`test`). Source: `lc:asset:env:*`. | +| **Fingerprint** | SHA-256 hex of the canonical inputs that identify a finding across rescans. See [Finding fingerprint](#finding-fingerprint). | +| **Scope** | `org` (per-package, applies to every host) or `host` (per-package-per-sensor). Resolution precedence: host beats org. | +| **Resolution** | Replaces the older `state` term. A finding is either implicitly **open** (no row) or **resolved** with `resolution ∈ { mitigated, accepted, false_positive }`. See [Lifecycle states](#lifecycle-states). | +| **Lapsed acceptance** | An `accepted` resolution whose `expires_at` is in the past. Derived in the UI as `resolution === 'accepted' && expires_at < now`; the row is **not** mutated. | +| **Daily Update tick** | Per-org per-day cron firing the three daily scans. Spread across 24h. See [Daily Update tick](#daily-update-tick). | +| **KEV match** | The event fired when a CVE just entered KEV AND the org still has an open finding for it. | +| **Case number** | Optional integer on a resolution row reserved for upcoming ext-cases linkage. Plumbed through the API today; not surfaced in the UI yet. | + +## Reachability (deferred) + +"Reachability" — in the sense Wiz, CrowdStrike, and similar tools use it: determining whether the vulnerable code path in a flagged package is actually loaded into a running process — is **deferred**. It requires sensor-side telemetry the EDR does not expose today (live module-load tracking, symbol-level call-graph coverage, etc.). + +Until reachability is available, LC Risk's `lc:asset:criticality:*` multiplier is the closest in-product proxy for triage prioritization: it lets the score reflect "this CVE on a crown-jewel host" versus "this CVE on a development box" without requiring the underlying loaded-code-path signal. + +## See Also + +- [`lc:asset:*` Tag Namespace](../../../2-sensors-deployment/asset-tags.md) — Asset metadata convention consumed by this extension +- [Sensor Tags](../../../2-sensors-deployment/sensor-tags.md) — General tagging mechanism, API, and CLI +- [Outputs](../../outputs/index.md) — Routing the `vuln_finding.*` events to external systems +- [Cases](cases.md) — Optional consumer of `vuln_finding.kev_match` for triage +- [Using Extensions](../using-extensions.md) — General extension subscription and management diff --git a/mkdocs.yml b/mkdocs.yml index 8ba880e2d..dd3951a68 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -216,6 +216,7 @@ nav: - Installation Keys: 2-sensors-deployment/installation-keys.md - Sensor Connectivity: 2-sensors-deployment/connectivity.md - Sensor Tags: 2-sensors-deployment/sensor-tags.md + - Asset Tags (lc:asset:*): 2-sensors-deployment/asset-tags.md - Log Collection Guide: 2-sensors-deployment/log-collection-guide.md - Telemetry Index: 2-sensors-deployment/telemetry-index.md - Endpoint Agents: @@ -409,6 +410,7 @@ nav: - Reliable Tasking: 5-integrations/extensions/limacharlie/reliable-tasking.md - Sensor Cull: 5-integrations/extensions/limacharlie/sensor-cull.md - Usage Alerts: 5-integrations/extensions/limacharlie/usage-alerts.md + - Vulnerability Reporting: 5-integrations/extensions/limacharlie/vulnerability-reporting.md - YARA Manager: 5-integrations/extensions/limacharlie/yara-manager.md - Third Party: - Overview: 5-integrations/extensions/third-party/index.md