Skip to content

feat(security): recognize Elastic Defend as an antivirus provider (#2018) - #2068

Merged
ToddHebebrand merged 2 commits into
mainfrom
fix/2018-elastic-defend-av
Jun 30, 2026
Merged

feat(security): recognize Elastic Defend as an antivirus provider (#2018)#2068
ToddHebebrand merged 2 commits into
mainfrom
fix/2018-elastic-defend-av

Conversation

@ToddHebebrand

Copy link
Copy Markdown
Collaborator

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) excludes other — so an Elastic-protected device showed up with no recognized AV.

This adds elastic_defend as a first-class antivirus provider, mirroring exactly how the existing products (SentinelOne, CrowdStrike, ESET, …) are wired through the pipeline.

Changes

  • agent agent/internal/security/status.goproviderFromName maps any elastic-containing WSC display name to elastic_defend, placed before the broad defender case so an "Elastic Defender"-style name isn't misclassified as Microsoft Defender.
  • apisecurity_provider pg enum gains elastic_defend (new idempotent migration 2026-07-05-security-provider-elastic-defend.sql, ADD VALUE IF NOT EXISTS … BEFORE 'other'), the Drizzle enum, the securityProviderValues zod set, normalizeProvider cases (elastic_defend/elastic_endpoint/elastic_agent/elastic), the providerCatalog entry (Elastic Defend / vendor Elastic), and the compliance-report label map.
  • tests — Go table test for providerFromName; API test for normalizeProvider plus a sync guard asserting providerCatalog + securityProviderValues stay in step (the dashboard does providerCatalog[normalizeProvider(x)], which throws if they drift).

Verification

  • go test -race ./internal/security/... — pass
  • vitest run for helpers.provider.test.ts, securityComplianceReport.test.ts, agents/schemas.test.ts, security.test.ts, autoMigrate.test.ts — pass
  • tsc --noEmit (apps/api) — clean

Refs #2018

🤖 Generated with Claude Code

)

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>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 30, 2026

Copy link
Copy Markdown

Deploying breeze with  Cloudflare Pages  Cloudflare Pages

Latest commit: 94f75cf
Status:🚫  Build failed.

View logs

…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>
@ToddHebebrand

Copy link
Copy Markdown
Collaborator Author

Review run: /pr-review-toolkit (code-reviewer + pr-test-analyzer) over origin/main...HEAD.

Findings: Code review clean — confirmed all provider sources stay in sync (Go providerFromName, pg enum migration, Drizzle enum, securityProviderValues, normalizeProvider, providerCatalog, compliance label) and that the migration is idempotent and ordered ADD VALUE … BEFORE 'other' to match the Drizzle enum order (no drift). The elastic-before-defender ordering is intentional and tested. No missed provider enumeration on the web side (consumes provider names data-driven from the API). The pre-existing bitdefender/defender substring ordering quirk is out of scope (filed separately).

Test analyzer: two suggestions folded in (commit 94f75cfbe): (a) a securityComplianceReport test asserting an elastic_defend + RTP-on device is counted as AV-covered (anyAv 67%, not unprotected) and labeled "Elastic Defend"; (b) a Go "Elastic Defender" case locking the elastic-before-defender ordering.

Tests:

  • go test -race ./internal/security/... — pass (incl. new TestProviderFromName)
  • vitest runhelpers.provider.test.ts, securityComplianceReport.test.ts, agents/schemas.test.ts, security.test.ts, autoMigrate.test.ts — all pass
  • tsc --noEmit (apps/api) — clean

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 Contains(lower, "elastic") so it catches any Elastic-branded WSC display name regardless of the exact string.

Status: review-clean, awaiting maintainer merge. Community issue — PR uses Refs #2018 (reporter verifies before close).

@ToddHebebrand
ToddHebebrand merged commit f9202a2 into main Jun 30, 2026
40 of 42 checks passed
@ToddHebebrand
ToddHebebrand deleted the fix/2018-elastic-defend-av branch June 30, 2026 14:43
ToddHebebrand added a commit that referenced this pull request Jun 30, 2026
) (#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>
ToddHebebrand added a commit that referenced this pull request Jul 1, 2026
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>
ToddHebebrand added a commit that referenced this pull request Jul 10, 2026
## 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant