feat(security): recognize Elastic Defend as an antivirus provider (#2018) - #2068
Conversation
) Elastic Defend (Elastic Agent / Elastic Endpoint Security) registers with Windows Security Center, but the agent's provider normalizer mapped its display name to `other`. AV-coverage detection excludes `other`, so devices protected by Elastic Defend were reported as unprotected. Add `elastic_defend` as a first-class provider across the detection pipeline, mirroring the existing AV products: - agent: providerFromName maps any "elastic" WSC display name to elastic_defend (ordered before the broad "defender" case so it isn't misread as Defender) - api: security_provider pg enum (+ migration), securityProviderValues zod set, normalizeProvider cases, providerCatalog entry, compliance-report label - tests: Go table test for providerFromName; API test for normalizeProvider plus a catalog/zod sync guard (dashboard indexes providerCatalog[normalize(x)]) Refs #2018 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…k provider ordering - securityComplianceReport: an elastic_defend + RTP-on device is counted as AV-covered (anyAv 67%, not unprotected) and labeled "Elastic Defend". - providerFromName: add an "Elastic Defender" case locking the elastic-before-defender ordering. Refs #2018 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Review run: /pr-review-toolkit (code-reviewer + pr-test-analyzer) over Findings: Code review clean — confirmed all provider sources stay in sync (Go Test analyzer: two suggestions folded in (commit Tests:
Note: WSC only surfaces Elastic Defend when its Windows malware-protection registers with Security Center; pure-EDR deployments may not appear. The agent matcher uses a substring Status: review-clean, awaiting maintainer merge. Community issue — PR uses |
) (#2085) ## Summary `providerFromName` in `agent/internal/security/status.go` is a first-match substring `switch`. The `defender` case preceded the `bitdefender` case, and `"bitdefender"` contains the substring `"defender"`, so any Windows Security Center product whose display name contains "Bitdefender" normalized to `windows_defender` instead of `bitdefender` — the `bitdefender` arm was effectively dead code for real Bitdefender names. This reorders the switch so the more specific `bitdefender` match wins, mirroring the elastic-before-defender approach. AV *coverage* counting was unaffected (both are non-`other` with RTP); this fixes the provider/vendor mislabeling on the security dashboard. ## Changes - Reorder `bitdefender` case before `defender` in `providerFromName`. - Add `agent/internal/security/status_provider_test.go` with a table-driven `TestProviderFromName` that locks in the ordering. The two bitdefender cases fail against the old ordering (verified: `providerFromName("Bitdefender") = "windows_defender"`) and pass with the fix. ## Testing - `go test -race ./internal/security/...` — green. - Confirmed the new test is non-vacuous by reverting the ordering: the bitdefender subtests fail with the exact misclassification described in the issue. ## Note PR #2068 (Elastic Defend, `fix/2018-elastic-defend-av`) also edits this switch and introduces a `status_provider_test.go`. #2068 is **not yet merged to main**, so this branched off fresh `origin/main` (neither the `elastic` case nor the test file present). The two inserts are independent; whichever merges second will need a trivial rebase to combine both cases and the test tables. Closes #2075 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Todd Hebebrand <todd@lanternops.io> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Documentation sweep for the **v0.88.0** release (range `v0.87.0..v0.88.0`). Updates 15 existing docs pages to reflect shipped features and fixes — all `Update`s, no new pages. ## Pages updated - **Networking** — `features/integrations.mdx` (self-hosted UniFi controller: agent-mediated, no cloud key, one controller → many orgs; + cloud Site Manager accuracy, #2097/#2103), `features/discovery.mdx` (native network-device detail page + `GET /discovery/assets/:id`, #1998), `features/dns-security.mdx` (Pi-hole v6: `piholeVersion`, app password, 429 `no_seats`, #2069) - **Security & incidents** — `features/incident-response.mdx` + `reference/api.mdx` (EDR-aware Incidents feed, `GET /incidents/feed`, #2095), `features/pam.mdx` + `security/pam.mdx` + `reference/api.mdx` (SHA-256 signer **thumbprint pin** — API-only, fail-closed, XOR signer-group, #2080), `features/security.mdx` (Elastic Defend provider row, #2068) - **Patching** — `features/configuration-policies.mdx` + `features/patch-management.mdx` ("Manage Windows Update exclusively through Breeze" toggle, default OFF, `NoAutoUpdate=1`, #2079; `releaseDate` + firmware/drivers-only reject, #2116; partner-wide owner accuracy + priority-direction fix, #2064), `features/deployments.mdx` (`.exe`/`.msi` detection rules + `forceReinstall`, #2088) - **Ops workflows** — `features/maintenance-windows.mdx` (reboot-if-pending sweep, #2096), `features/ticketing.mdx` (`unknownSenderMode` + `dropUnverifiedSenders`, #2105), `features/alerts.mdx` (suppress: no-suppress-when-resolved, "Forever", expiry reaper, bulk suppress, #2110), `features/reports.mdx` (posture report now UI-selectable, #2087) ## Also - `scripts/docs-review/mapping.json` — +4 code→doc mappings (incidents→api, softwareDeployment→deployments, new `maintenanceRebootWorker.ts`→maintenance-windows, `devices/network` page→discovery) - `scripts/docs-review/last-reviewed.json` — tracker bumped to `v0.88.0` - **`AssignmentsTab.tsx`** — one-line fix: the policy **Priority** tooltip said "higher values override lower ones," but the resolver sorts `priority ASC` with first-wins (`featureConfigResolver.ts:207`, `configurationPolicy.ts:1328`) — i.e. **lower-number-wins**. Tooltip corrected to match. (Small code fix bundled here because it's the same priority-direction accuracy issue the docs sweep surfaced.) `pnpm --filter docs build` passes — **130 pages**, no MDX/frontmatter errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Todd Hebebrand <todd@lanternops.io> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary PR #2068 added Elastic Defend to the AV detection set, but only where an AV provider gets enumerated in the first place — Windows Security Center products and the macOS mdatp path. On Linux, `CollectStatus` never populated any AV info at all: `Provider` stayed `"other"` and `AVProducts` stayed empty, so Linux hosts protected by Elastic Defend showed as unprotected in AV coverage. The reporter on #2018 confirmed exactly this: "Not functional yet since I am running on Linux." ## What this does Linux has no security-center registry, so the agent now probes the Elastic Defend sensor directly (`agent/internal/security/status_elastic.go`), mirroring the darwin third-party pattern (`getMacDefenderStatus`): - **Install signal:** `/opt/Elastic/Endpoint/elastic-endpoint` — the Endpoint Security sensor Elastic Defend installs. The Elastic *Agent* binary alone (`/opt/Elastic/Agent`) is just a log/metrics shipper and deliberately does NOT count as AV. - **Running signal:** `elastic-endpoint` process via a `/proc` cmdline scan (`/proc/<pid>/comm` truncates at 15 chars and "elastic-endpoint" is 16 — cmdline avoids that), with `systemctl is-active ElasticEndpoint.service` as a fallback. A running sensor is also accepted without the default binary path (non-default install prefixes). - **Reported as:** `Provider=elastic_defend`, an `AVProduct` entry, and `RealTimeProtection=true` when the sensor is running — the coverage gate (`securityComplianceReport.ts`) is `provider !== 'other' && rtp === true`, so both are set. Best-effort version via `elastic-endpoint version`. No API-side changes needed: `normalizeProvider` and the `security_provider` enum already accept `elastic_defend` since #2068. ## Tests - Table-driven detection tests with injected probes (installed/running matrix, agent-only exclusion, non-default prefix), `/proc` scan tests against a fake proc root (16-char name, substring non-match, kernel-thread empty cmdline), and version-parse tests. - Files avoid the implicit `_linux.go` build-constraint suffix so the logic compiles and tests on every GOOS; the entry point is runtime-guarded like the darwin path. - `go test -race ./internal/security/... ./internal/heartbeat/...` green; `go build` for GOOS=linux and GOOS=windows green; `go vet` clean. Closes #2018 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Todd Hebebrand <todd@lanternops.io> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Antivirus Coverage did not recognize Elastic Defend agents, so devices protected by Elastic Defend were reported as unprotected (issue #2018).
Elastic Defend (Elastic Agent / Elastic Endpoint Security) registers itself with Windows Security Center, and the agent reads WSC AV products. But the provider normalizer mapped Elastic's display name to
other, and AV-coverage detection (compliance report / provider labeling) excludesother— so an Elastic-protected device showed up with no recognized AV.This adds
elastic_defendas a first-class antivirus provider, mirroring exactly how the existing products (SentinelOne, CrowdStrike, ESET, …) are wired through the pipeline.Changes
agent/internal/security/status.go—providerFromNamemaps anyelastic-containing WSC display name toelastic_defend, placed before the broaddefendercase so an "Elastic Defender"-style name isn't misclassified as Microsoft Defender.security_providerpg enum gainselastic_defend(new idempotent migration2026-07-05-security-provider-elastic-defend.sql,ADD VALUE IF NOT EXISTS … BEFORE 'other'), the Drizzle enum, thesecurityProviderValueszod set,normalizeProvidercases (elastic_defend/elastic_endpoint/elastic_agent/elastic), theproviderCatalogentry (Elastic Defend/ vendorElastic), and the compliance-report label map.providerFromName; API test fornormalizeProviderplus a sync guard assertingproviderCatalog+securityProviderValuesstay in step (the dashboard doesproviderCatalog[normalizeProvider(x)], which throws if they drift).Verification
go test -race ./internal/security/...— passvitest runforhelpers.provider.test.ts,securityComplianceReport.test.ts,agents/schemas.test.ts,security.test.ts,autoMigrate.test.ts— passtsc --noEmit(apps/api) — cleanRefs #2018
🤖 Generated with Claude Code