Skip to main content

Microsoft Defender

The Three-Pillar Model

The Defender integration is built from three pillars. The first two are read-only and active as soon as credentials are saved; the third is an opt-in write pillar documented separately.

PillarDirectionGraph permissionDefault
1. Analytics ingest — Secure Score, alerts, control profilesGraph → ESSecurityAlert.Read.AllOn once credentials are set
2. Evidence correlation — test ↔ alert matchingES ↔ ESNone extraAutomatic
3. Auto-resolve — resolve correlated alerts in DefenderES → GraphSecurityAlert.ReadWrite.AllOpt-in, disabled by default

This page covers pillars 1 and 2. Pillar 3 has its own setup walkthrough — see Auto-Resolve.

Prerequisites

  • Microsoft 365 with Defender enabled
  • Azure AD App Registration with SecurityAlert.Read.All (Application type, admin consent)

Azure AD Setup

  1. Go to Azure Portal → App Registrations → New Registration
  2. Name: "ProjectAchilles Defender Integration"
  3. Under API Permissions, add:
    • SecurityAlert.Read.All (Application type) — covers Secure Score, alerts, and control profiles
    • Click Grant admin consent
  4. Under Certificates & Secrets, create a client secret
  5. Note the Application (client) ID, Directory (tenant) ID, and Client Secret
Enabling auto-resolve later

If you plan to enable auto-resolve, it requires the additional SecurityAlert.ReadWrite.All scope. You can grant it now or add it later — the read-only integration runs fine without it.

Configuration

  1. Navigate to SettingsIntegrationsMicrosoft Defender
  2. Enter:
    • Tenant ID — Azure AD Directory (tenant) ID
    • Client ID — Application (client) ID
    • Client Secret — The secret you created
  3. Click Save and then Test Connection

Credentials are encrypted at rest with AES-256-GCM.

Sync Behavior

DataSync Interval
Secure Score + Control ProfilesEvery 6 hours
AlertsEvery 5 minutes

In Docker deployments, sync runs via setInterval. On Vercel, sync runs via Cron at /api/cron/defender-sync.

Architecture

The Defender integration is composed of four backend services that work together to pull data from Microsoft Graph, store it in Elasticsearch, and expose analytics to the frontend.

Graph Client

The Graph client (graph-client.ts) is a lightweight, SDK-free HTTP client for Microsoft Graph API. It handles the full OAuth2 lifecycle internally.

OAuth2 Client Credentials Flow:

Key behaviors:

  • Token caching with a 5-minute refresh margin before actual expiry
  • Automatic OData pagination — follows @odata.nextLink until all pages are retrieved
  • 429 retry with exponential backoff using Retry-After header
  • 401 recovery — invalidates the cached token and re-authenticates on the next call

Sync Service

The sync service (sync.service.ts) orchestrates data flow from Graph API to Elasticsearch using type-specific strategies:

Data TypeStrategyFrequencyDetails
Secure ScoresUpsert by dateEvery 6 hoursOne document per day, keyed by date
Control ProfilesFull replacementEvery 6 hoursRelatively static; entire set is re-indexed
AlertsIncrementalEvery 5 minutesUses lastUpdateDateTime filter to fetch only new/updated alerts

Each Graph API response is normalized into a consistent document structure with a doc_type discriminator field before indexing.

Initial sync behavior: The first alert sync uses a 90-day lookback (createdDateTime filter) to fetch all relevant alerts without pulling the entire tenant history. Subsequent syncs resume incrementally from the last successful sync timestamp.

Persisted sync timestamps: lastAlertSync and lastScoreSync are persisted to integrations.json (Docker) or Vercel Blob (serverless). On server restart, the sync service loads persisted timestamps and resumes incremental syncs instead of re-fetching from scratch. Timestamps are only set on successful syncs — failed fetches do not advance the watermark.

Evidence extraction: During alert sync, the service extracts evidence_hostnames and evidence_filenames from the Graph API alert's evidence metadata (deviceEvidence, processEvidence, fileEvidence). These fields enable precise cross-correlation with test executions.

Elasticsearch Storage Model

All Defender data is stored in a single index (achilles-defender) using a sparse document pattern with a doc_type discriminator:

FieldTypeUsed ByDescription
doc_typekeywordAllsecure_score, control_profile, or alert
timestampdateAllIngestion or event timestamp
score_currentfloatsecure_scoreCurrent score value
score_maxfloatsecure_scoreMaximum possible score
score_percentagefloatsecure_scorecurrent / max * 100
control_namekeywordcontrol_profileControl display name
control_categorykeywordcontrol_profileCategory grouping
implementation_costkeywordcontrol_profilelow, moderate, high
alert_idkeywordalertMicrosoft alert identifier
severitykeywordalertlow, medium, high, critical
statuskeywordalertnew, inProgress, resolved
mitre_techniqueskeyword[]alertMITRE ATT&CK technique IDs (e.g., T1566.001)
evidence_hostnameskeyword[]alertHostnames extracted from alert evidence metadata
evidence_filenameskeyword[]alertFilenames extracted from alert evidence metadata
Index Design Rationale

A single sparse index is used instead of three separate indices because the total document volume is low (typically hundreds, not millions) and it simplifies cross-document queries and index lifecycle management.

Cross-Correlation Logic

The analytics service provides four types of cross-correlation between Achilles test results and Defender data:

1. Detection Rate (per-execution)

The headline metric on the Defender tab. It is the share of attack-simulation executions — counted per MITRE technique — that have a temporally-correlated Defender alert:

detectionRate = correlatedExecutions / totalExecutions × 100

Correlation is roll-up aware (a T1574.002 test is satisfied by a parent T1574 alert, one-directionally) and excludes cyber-hygiene controls and skipped bundle stages. The full definition, exclusions, and known approximations are documented in Analytics → Microsoft Defender.

2. Per-test evidence correlation (alert drawer)

For a single test execution, the alert drawer correlates against Defender alerts using a three-tier matching strategy within a -5 min to +30 min window relative to test completion:

  • Tier 1 — Evidence-based (most precise): matches evidence_filenames containing the test binary's UUID filename AND evidence_hostnames containing the test endpoint's hostname. This proves the specific execution triggered the alert.
  • Tier 2 — Technique + hostname: falls back to MITRE technique IDs scoped by evidence_hostnames. Used when alerts lack file-level evidence.
  • Tier 3 — Technique-only: falls back to MITRE technique ID alone. Used for alerts with no evidence metadata.

The -5 min to +30 min window accounts for Defender's real-time telemetry generating alerts during test execution while excluding unrelated pre-test alerts. This per-test correlation also feeds the f0rtika.achilles_correlated flag that the auto-resolve pillar reads.

3. Technique Coverage Overlap

Maps MITRE ATT&CK techniques present in both datasets to identify:

  • Techniques tested by Achilles and detected by Defender (validated coverage)
  • Techniques tested by Achilles but not detected (detection gaps)
  • Techniques detected by Defender but not tested (untested detections)

4. Defense Score vs. Secure Score Trending

Compares the internal Defense Score (from test results) with Microsoft Secure Score over time using aligned date histograms, enabling teams to see whether improving their Secure Score configuration also improves real detection effectiveness.

Conditional Frontend Display

All Defender dashboard elements are conditionally rendered based on the useDefenderConfig hook:

useDefenderConfig() → { configured: boolean, loading: boolean }

When configured is false, Defender panels, tabs, and cross-correlation widgets are hidden entirely — the dashboard shows only Achilles-native analytics. This prevents empty states and confusion for users who have not set up the integration.

Deployment Variants

AspectDocker / Fly.io / RailwayVercel (Serverless)
Backend pathbackend/src/services/defender/backend-serverless/src/services/defender/
Settings accessSynchronous file readAsync Vercel Blob read
Sync triggersetInterval at server startupVercel Cron (/api/cron/defender-sync)
CredentialsEncrypted file or env varsEnv vars or encrypted blob

Both variants expose identical API endpoints and analytics capabilities.

Troubleshooting

Common Issues
  • "Test Connection" fails with 401: Verify that admin consent has been granted for the SecurityAlert.Read.All permission in Azure AD. Application permissions (not delegated) are required.
  • No data after saving credentials: The first sync runs on the next interval (up to 6 hours for scores, 5 minutes for alerts). Click Sync Now in the integration settings to trigger an immediate sync.
  • Missing alerts, scores, or controls: All three document types — alerts, Secure Score, and control profiles — are read with SecurityAlert.Read.All. A 403 on any of them means the permission is missing or admin consent was not granted.
  • 403 when enabling auto-resolve: Auto-resolve needs the separate SecurityAlert.ReadWrite.All scope. See Auto-Resolve → Troubleshooting.