Usage guide

Start here for first scans, triage, CI gates, stateful checks, fleet scans, exporter mode, and safe report sharing. For credentials, run deadair setup <backend> or open the guide for Elastic, OpenSearch, or Microsoft Sentinel.

First scan

Start with a manual scan. Do not wire alerts before a detection engineer has read the first report.

Choose one setup command for your SIEM:

deadair setup elastic
deadair setup opensearch
deadair setup sentinel

Then verify the credential and scan:

deadair check
deadair scan

setup prints the documented read-only role and credential commands for the selected backend. check verifies the connection, required privileges, and optional capabilities such as schema visibility. scan prints the terminal report.

Freshness checks allow up to five minutes of clock skew. Later timestamps are excluded so they cannot hide older events. If no usable timestamp remains, freshness is unknown unless a bounded query proves there were no recent events.

deadair check reporting READY against a disposable Elastic lab

The screenshot above is captured from the same disposable Elastic lab as the README scan with make record-scan-lab.

Common output formats:

deadair scan --json
deadair scan --json-out report.json
deadair scan --json-out report.json --html-out report.html

Exit codes:

Code Meaning
0 scan completed with no findings that affect the configured gate
1 scan completed with one or more findings selected by the configured gate
2 scan failed or the fleet scan was incomplete

Useful connection flags:

Flag Use
--ca-cert ca.pem trust a private CA
--insecure-skip-verify lab use only; skip TLS verification
--kibana-space soc read Elastic rules from a non-default Kibana space
--timeout 90s raise the per-scan timeout

Connections honor HTTP_PROXY, HTTPS_PROXY, and NO_PROXY, including their lowercase forms. Use --ca-cert if the connection requires a private CA.

Microsoft Sentinel

deadair authenticates through DefaultAzureCredential. Identify the Log Analytics workspace by its ARM coordinates:

az login --tenant <tenant-id>
az account set --subscription <subscription-id>

export DEADAIR_BACKEND=sentinel
export DEADAIR_AZURE_SUBSCRIPTION_ID=<subscription-id>
export DEADAIR_AZURE_RESOURCE_GROUP=<resource-group>
export DEADAIR_SENTINEL_WORKSPACE=<workspace-resource-name>

deadair check
deadair scan --json-out sentinel-report.json

DEADAIR_SENTINEL_WORKSPACE_ID can override the discovered Log Analytics customer ID, but deadair still verifies it against the workspace returned by ARM. Workload identity, managed identity, and service-principal authentication use the same scan variables. See the Sentinel credential guide for role options, the custom reader used in the live lab, and the API calls.

What a Sentinel scan assesses

Sentinel scans cover enabled Scheduled and NRT rules. deadair parses KQL dependencies, then asks Sentinel and Log Analytics for evidence instead of executing the rule’s full query.

Dependency or check Evidence boundary
local tables, joins, unions, and let aliases ARM table catalog plus bounded Logs queries
saved workspace functions expanded when arguments and defaults are closed scalar values
workspace-defined ASIM functions expanded like other saved functions
recognized native ASIM parsers missing from workspace metadata assessed only when a bounded literal call returns concrete local tables with complete permission and data-source evidence
literal _GetWatchlist('alias') watchlist inventory plus a bounded zero-row query; retained as non-monitorable dependency_evidence
literal workspace() table references assessed only for explicitly mapped workspaces, as described below
table freshness separate TimeGenerated and ingestion_time() observations; each rule uses its own clock
filtered source activity advisory check only when deadair proves one direct local Analytics table followed by a closed literal filter
expected producers configured vendor, product, or device identities, measured independently of table-wide freshness
ingest lag paired event-time and ingestion-time evidence for eligible Scheduled rules
summary pipeline runtime latest completed run in a bounded seven-day LASummaryLogs query for relevant active summary rules, capped at 50 rules per scan; failures and overdue successes have stable findings

An HTTP 200 with PartialError is not a successful probe. Dynamic watchlist aliases, tabular function parameters, row-derived or dynamic arguments, parameter-driven table(), dynamic table selection, external data, and app(), resource(), ADX/cluster, and ARG references remain unassessed.

Every join leg and ordinary union leg is required. A missing leg inside an explicit union isfuzzy=true produces partial input coverage instead of making the whole rule unusable. Native ASIM calls create source edges only for concrete local tables named by the Logs response and present in the ARM catalog.

For tables shared by Scheduled and NRT rules, both clocks remain visible in the source view. A recent ingestion timestamp doesn’t clear an event-time failure. Basic and Auxiliary tables are incompatible with this analytics-rule evidence path. Sentinel does not expose authoritative rule-required fields or a bounded, authoritative per-table event and storage inventory, so required-field and unused-telemetry findings are unavailable. The CLI currently targets Azure public cloud endpoints.

Filtered source activity catches a gap that table-wide freshness cannot: one vendor, device, or operation can disappear while the shared table still looks healthy. The disposable lab exercised this path with current FortiGate data beside a stale Palo Alto Networks/PAN-OS slice. deadair runs the advisory check only when a query starts at one direct local table and immediately applies a closed, parser-supported literal filter. Remote, joined, unioned, dynamic, escaped-literal, and function-backed sources do not qualify. The result appears in rule_source_freshness; it does not replace table health or create findings. Each rule-filter collection runs at most 20 distinct queries. Rules with the same table, filter and freshness clock share one observation in that collection; their evidence and policy thresholds remain separate. Later collections query again.

For alerting on a feed, define an explicit producer expectation. A rule’s filter may select a rare security event, so deadair doesn’t assume it should produce regular traffic. The investigation guide shows the policy, source view, and maintenance setup.

Watchlists do not receive freshness, lag, schema, storage, or source-health verdicts. Their resolution is still part of the rule assessment: a missing required watchlist can make a rule disconnected, while an unavailable check remains unassessed. dependency_evidence records why.

Cross-workspace rules

To assess direct literal workspace() references, create a restricted JSON allowlist:

[
  {
    "alias": "soc",
    "azure_subscription_id": "<subscription-id>",
    "azure_resource_group": "<resource-group>",
    "sentinel_workspace": "<workspace-resource-name>",
    "sentinel_workspace_id": "<optional-customer-guid>"
  }
]

alias, subscription, resource group, and workspace name are required. The customer ID is optional and is still verified through ARM. Pass the file with --sentinel-remotes FILE or DEADAIR_SENTINEL_REMOTES. A target matches only an explicitly configured alias, a verified customer GUID, or the canonical ARM workspace ID. In a fleet file, put these objects in sentinel_remote_workspaces on the Sentinel instance instead of using --sentinel-remotes.

Textual aliases need a remote table-catalog read and a bounded zero-row query using the rule’s original literal. The response must tie permission and data-source evidence to the configured workspace. GUID and canonical ARM-ID mappings do not need this alias-identity proof. The home and remote workspaces use the same DefaultAzureCredential identity; there are no per-remote credentials.

Microsoft requires Sentinel to be deployed on every workspace referenced by a cross-workspace analytics rule. Before reading a configured remote table catalog or running a Logs probe, deadair requires the remote workspace to return the exact onboardingStates/default resource. A missing, denied, malformed, or mismatched onboarding response makes the input unavailable; it is not an absent-table result, and no table or Logs probe follows.

Once a remote is verified, its source identity uses the canonical ARM workspace ID. Local and remote tables with the same name therefore remain distinct. An unmapped target stays remote; an unreadable or not-provably-onboarded target is unavailable; only a table missing from a verified, onboarded remote catalog is empty.

deadair check tests onboarding, metadata, and query access for configured remotes referenced by enabled rules. A required remote that cannot prove onboarding or read access blocks readiness. An unused mapping is not evidence that check touched that workspace. scan and candidate scans keep onboarding failures as unavailable evidence rather than continuing to freshness probes.

Cross-workspace limits

Two separate Microsoft limits apply:

Scope Platform limit deadair behavior
workspaces in an analytics-rule query 20; Microsoft recommends no more than five for performance counts the home workspace plus distinct verified remotes; more than 20 is incompatible
Azure regions in the Logs query scope blocked at 20 or more; warning at five counts the home region plus verified remote regions; 20 or more is incompatible; missing location evidence is unavailable

The five-workspace recommendation and five-region warning are platform guidance, not deadair findings. Textual aliases must pass their catalog and identity proof before either count is conclusive. See Microsoft’s cross-workspace analytics-rule limits and Log Analytics query-scope limits.

For mappings within the rule’s subscription, positive ARM and Logs evidence can conclusively prove source availability under the scanner identity. That does not extend to execution identity across subscriptions or tenants. Microsoft applies the rule creator’s credentials to those analytics rules, so a scanner-positive query can coexist with a rule that stopped running after its creator lost access. For an eligible installed rule that references another subscription, deadair accepts only an exact, successful SentinelHealth record matching that rule identity, recorded after the latest rule change and within the rule’s expected run cadence plus Microsoft’s scheduling delay. It does not generalize that evidence to another rule or workspace. Candidate rules and absent, old, ambiguous, mismatched, or non-successful health records remain unassessed. deadair does not separately identify tenant boundaries; Azure Lighthouse and other cross-tenant topologies have not been live-validated. See Microsoft’s analytics-rule access model and failure guidance.

These runtime tables are opt-in. _SentinelHealth() requires Sentinel auditing and health monitoring. LASummaryLogs requires the Summary Logs diagnostic setting and appears only after a summary event is emitted. Missing runtime evidence does not create a finding. With table-level RBAC, the custom role also needs Microsoft.OperationalInsights/workspaces/query/SentinelHealth/read and Microsoft.OperationalInsights/workspaces/query/LASummaryLogs/read, or the broader Microsoft.OperationalInsights/workspaces/query/*/read action.

Read the findings

The scan lists affected detections first, then sources and the policy result. The disposable demo exits 1 because its seeded problems match the gate. Exit 2 means the assessment could not complete safely.

Terminal colours mark failures and warnings; the labels carry the same meaning without colour. Set NO_COLOR=1 to disable styling. Redirected output is always plain text.

A finding states what deadair observed. If several rules point to the same source, start there:

deadair inspect --source CommonSecurityLog report.json

This reads a saved scan. It shows the source’s observations and enabled consumers in severity order, with a first check based on the evidence. See Investigate a telemetry gap for a worked Sentinel feed example.

Terms used in reports:

Term Meaning
rule input backend expression configured on the detection, such as an index selector, data view, or KQL table dependency
source concrete index, data stream, or Sentinel table visible to deadair, such as winlogbeat-2026.07 or SigninLogs
resolved source concrete source returned by the backend’s native input-resolution API
dead detection enabled rule with no usable source, or with only stale/empty matched sources
impaired detection enabled rule with live input but positive evidence of reduced field, timing, or source coverage
partial input coverage the complete input resolves, but one positive selector inside a multi-selector expression resolves empty

The source inventory is credential-scoped. deadair records whether each input was resolved, empty, incompatible, unsupported, unavailable, remote, or ambiguous. Incompatible means the source was identified but its configuration cannot be used by that rule type. Permission failures and unsupported query types stay visible as unassessed inputs; they are never silently treated as healthy or dead.

For a rule with several positive selectors, deadair also resolves each selector with the rule’s exclusions retained. If the complete expression resolves but one selector does not, the report records partial input coverage. This is useful during source migrations, but it is not a dead-rule verdict: selectors may be alternatives or fallbacks. It affects exit status only when a policy includes the partial-input finding class.

No matching source

Human reports say no matching source; JSON uses the stable reason code disconnected. It means the backend understood the enabled rule’s input and positively resolved it to no concrete index, alias target, data stream, or Sentinel table visible to the credential.

It does not mean the SIEM, agent, or network connection is disconnected. A connection or authentication failure makes the scan or fleet instance fail and returns exit 2; it is not a rule verdict.

A finding from the checked-in lab report:

Evidence Value
Rule Sysmon registry run-key modification
Configured patterns winlogbeat-sysmon-*
Matched sources none
Impact the rule currently has no source to query
Lab explanation the disposable lab intentionally does not create a matching Sysmon source

That is an expected coverage gap in the lab. A production regression looks similar but has a change behind it: a Windows rule still queries winlogbeat-*, telemetry moves to logs-windows.sysmon_operational-*, and the old Winlogbeat indices age out. The rule remains enabled, but its configured input no longer names the source carrying the events.

Common explanations:

Situation What to verify
integration not onboarded whether the rule is intentionally enabled before its data source is available
source renamed or migrated current data-stream names after an agent, package, or pipeline change
rule copied from another tenant whether that tenant has the same integrations and naming conventions
pattern typo or stale override the rule’s configured index patterns or data view
credential cannot see the source index permissions and Kibana space for the deadair credential

First response:

  1. Read dead_detections[].patterns and the rule’s input_resolutions evidence in the JSON report.
  2. Confirm whether the native resolver returned the expected alias, data stream, concrete index, or Sentinel table.
  3. Confirm the deadair credential can resolve and read that source.
  4. Classify the finding as expected scope, onboarding work, or regression.
  5. Update the rule pattern, restore the integration, or disable the intentionally out-of-scope rule.
deadair scan --json --json-out report.json
jq '.dead_detections[] | select(.reason == "disconnected") | {name, patterns}' report.json

All matching sources stale or empty

Human reports spell this out; JSON uses starved. The rule patterns resolve correctly, but every matched source is degraded. A source is stale when it has documents but no recent event inside --max-stale; it is empty when it exists with zero documents.

For example, a rule queries logs-system.auth-* and resolves to logs-system.auth-default, but the newest event is three days old. The pattern is not the problem. The ingest path stopped, the source is intentionally quiet, or the stale threshold does not match its cadence.

Inspect dead_detections[].sources, then find those names under sources[] for document count, age, status, and consumer count. Check the agent or connector, forwarder, ingest pipeline, upstream system, expected cadence, and downtime configuration.

Incompatible source configuration

JSON uses source-plan-incompatible when a rule’s known sources cannot be queried under their current configuration. If every input is incompatible, the rule is dead. If another input remains usable, the rule is impaired and incompatible_sources names the lost coverage. Change the source configuration or move the affected query to a rule type that supports it.

Impaired detections

Impaired rules still have live input. deadair has evidence that part of their effective coverage is reduced.

Finding Evidence Example First response
missing-fields a best-effort rule-declared field is absent or non-searchable in at least one concrete matched source, with complete field_caps evidence for every matched source a parser upgrade stops mapping process.command_line while a rule declares it as required compare rule metadata, package version, pipeline, and mapping
lag-blind-window paired-event p95 ingest lag exceeds the rule’s lookback-minus-interval margin cloud audit events arrive 12 minutes late while a five-minute rule looks back six minutes reduce delivery lag, widen lookback, or use the appropriate ingest timestamp
source-plan-incompatible a known source configuration cannot be queried by this rule type while another input remains usable one table moves to a plan that scheduled analytics rules cannot query change the source plan or split the query into a supported rule path

required_fields is informational metadata, and field_caps proves mapping/searchability rather than field population in recent events. deadair records present, missing, and incomplete evidence per concrete source and does not make a rule-level claim when any matched-source mapping read fails. Treat missing-fields as strong triage evidence, not proof that every event is missing the value. Sentinel does not expose authoritative rule-required fields, so this check is unavailable there. Lag findings are also a timing model; validate them against the source’s real delivery behavior and the rule type.

Source findings

Finding What deadair observed First response
stale source has documents but no recent event inside --max-stale compare expected cadence, then check collector, connector, forwarder, and pipeline health
empty source exists with zero documents finish onboarding, fix routing, or remove an unused template/source
unknown freshness could not be measured check @timestamp mapping and read privileges; unknown does not make rules dead
maintenance a downtime window currently suppresses stale/empty classification confirm the declared window still matches the operating schedule
low volume source is below its own weekday/hour baseline after warmup and hysteresis compare known business cycles and upstream volume before paging
schema drift fields were added, removed, or changed type since the prior snapshot correlate with package, parser, and pipeline releases
unused telemetry source has data and every enabled local input was assessed, but none resolves to it confirm intentional collection, disabled rules, and planned coverage before changing ingest

remote_rules contains cross-cluster patterns such as cluster:index-* and Sentinel remote inputs that were not explicitly mapped or cannot be assessed. A verified, mapped workspace() table is a qualified resolved source instead. unmapped_rules retains unsupported, unavailable, and ambiguous input assessments, including query types deadair cannot safely interpret yet. These are informational because deadair does not turn uncertainty into a finding.

If an enabled local input is unsupported, unavailable, or ambiguous, unused telemetry is marked unavailable and withheld rather than turning incomplete coverage into a cost finding. Candidate reports mark it not-applicable. Sentinel also withholds unused telemetry because its workspace table inventory does not provide the bounded, authoritative per-table event and storage evidence required by that finding.

The full source evidence is in input_resolutions. Each record includes the declared selector or ordered expression, status, resolution method, observation time, logical aliases, and resolved sources. Sentinel reports can also include:

  • dependency_evidence for literal watchlists, native ASIM probes, and other prerequisites that do not necessarily become source-health edges;
  • source_lineage for the ARM-described path from a summary-rule input through its transform to an Analytics destination table consumed by an enabled detection;
  • rule_source_freshness for an eligible rule’s bounded, predicate-qualified local table slice;
  • summary_rule_runs for the latest bounded completed LASummaryLogs run associated with a relevant summary rule; and
  • rule_provenance for exact native template and Content Hub template/package IDs and versions.

dependency_evidence explains dependency-resolution outcomes. A required dependency can affect the rule verdict through authoritative input resolution. source_lineage, rule_provenance, and rule_source_freshness add context without changing the gate. Explicit producer expectations and summary-runtime failures have stable producer-stale and summary-pipeline findings. Add those classes to the policy to gate on them.

Summary lineage records ARM structure. summary_rule_runs records the latest completed native run inside its bounded window. In-progress rows do not replace the last completed run, and an overdue success is retained as incomplete evidence with health_status: "late". The validation record describes the live filtered-source and summary-runtime cases.

--include and --exclude do not change detection verdicts. They do scope source-level reporting and source-level findings such as degraded, low-volume, schema-drifted, or unused sources, so a policy that gates those classes can produce a different exit result.

Gate detection changes

Use scan --rule in detection-as-code pull requests:

deadair scan --rule candidate-rule.json

For Elastic, the candidate can be one rule object, an array, or an ndjson rule export. For OpenSearch, pass a Security Analytics detector create/update object, get response, search hit, or an array of those objects. Sentinel accepts a direct Scheduled or NRT ARM JSON body, one Azure-Sentinel analytic-rule YAML document, or an ARM deployment template containing alert-rule resources whose name, query, and timing resolve from literals, parameter defaults, or simple variables. Compiled Bicep JSON works within those limits. Raw Bicep, ARM parameter-only documents, and alert-rule templates are rejected.

deadair evaluates the candidate against the live environment without installing it. Sentinel candidates are treated as enabled for the gate, and requiredDataConnectors metadata does not create table dependencies; the KQL query is the evidence source. deadair does not execute the candidate’s full KQL. It runs only the same bounded table and dependency probes used for installed rules. Summary-lineage and Content Hub enrichment are skipped for candidates. Exit 1 means the candidate is dead or impaired; exit 2 means its inputs were unsupported, unavailable, ambiguous, remote, or otherwise not safely assessed. Existing source-health findings do not block the candidate gate. A fleet candidate scan must use one backend because the accepted candidate formats differ by backend.

Elastic indicator-match rules and ES|QL pipelines with lookup, enrichment, or subquery dependencies currently exit 2 in candidate mode. Their event source alone is not enough to assess the rule.

The official GitHub Action wraps single-instance candidate gates for Elastic, OpenSearch, and Sentinel. It keeps the JSON evidence as a workflow artifact and writes the useful counts to the job summary. For Elastic or OpenSearch, pass the connection details through repository or environment secrets:

permissions:
  contents: read

steps:
  - name: Check out detection rules
    uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
    with:
      persist-credentials: false
  - name: Check candidate coverage
    uses: alephnull-sh/deadair@v0.8.0
    with:
      backend: elastic
      elasticsearch-url: $
      kibana-url: $
      api-key: $
      candidate-rule: detections/new-rule.json
      policy-file: deadair-policy.json

For Sentinel, authenticate the runner before invoking deadair. The recommended GitHub-hosted path uses Azure OIDC: azure/login exchanges the GitHub token and creates an Azure CLI session, then deadair’s DefaultAzureCredential reuses that session through AzureCLICredential. The deadair Action defines no Azure credential inputs and does not call azure/login itself.

permissions:
  contents: read
  id-token: write

jobs:
  sentinel-gate:
    runs-on: ubuntu-latest
    environment: sentinel-read

    steps:
      - name: Check out detection rules
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
        with:
          persist-credentials: false
      - name: Sign in to Azure with OIDC
        uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
        with:
          client-id: $
          tenant-id: $
          subscription-id: $
      - name: Check Sentinel candidate coverage
        uses: alephnull-sh/deadair@v0.8.0
        env:
          AZURE_TOKEN_CREDENTIALS: AzureCLICredential
        with:
          backend: sentinel
          azure-subscription-id: $
          azure-resource-group: $
          sentinel-workspace: $
          candidate-rule: detections/new-rule.yaml
          policy-file: deadair-policy.json

Define the Azure identifiers in the protected sentinel-read GitHub Environment, and make the Entra federated credential’s subject match that environment. id-token: write lets the job ask GitHub for an OIDC token; Azure RBAC still decides what the identity can read or write. Give it the read-only workspace roles from the Sentinel credential guide, leave Azure Login’s cleanup enabled, and run only trusted, pinned actions after the login step. An Azure CLI login is shared by later steps in the same job. The hosted OIDC path passed a live test against two disposable UK South workspaces using branch-scoped trust. That test covered candidate gates, read-only access, redacted artifacts and session cleanup; it did not test GitHub Environment approval rules. See the validation record.

sentinel-workspace-id can supply the optional Log Analytics customer-ID override, and sentinel-remotes-file can point to the explicit JSON mapping used by cross-workspace rules. The Action still verifies the customer ID through ARM and applies the same remote-evidence limits as the CLI. This path targets Azure public cloud; Azure Login settings for another cloud do not change deadair’s endpoints.

Treat the remote mapping as part of the scanner’s authority boundary. It decides which workspace aliases deadair may resolve and query with the job’s Azure identity. Do not read it from an untrusted pull-request checkout. Load it from a protected base branch or restricted runner path, or require CODEOWNERS review for changes.

The Action always fails on a scan error. Findings also fail the step unless fail-on-findings: "false" is set, which is useful while establishing a baseline. Reports are redacted before upload by default. Set redact-key-file to a secret file when redacted names must remain stable across runs; setting redact-report: "false" can expose sensitive blind-spot evidence in the job log and artifact. Redaction covers terminal and JSON output, not a --state-file; state files remain raw and sensitive.

If the Action runs more than once in a job or across a matrix, give each upload a unique artifact-name, such as deadair-report-$.

The policy is part of the gate. Do not let a pull request weaken the policy that evaluates it. Read the policy from a protected base branch or restricted runner path, or require CODEOWNERS review for policy changes.

The Action does not emit SARIF because live operational findings do not map to source-file locations. It accepts one SIEM instance and rejects --fleet; use the CLI directly for fleet files.

A local policy can set source-specific freshness, choose which finding classes gate, and accept a known finding until a fixed expiry:

{
  "version": 1,
  "severity_threshold": "high",
  "gate_classes": ["dead-detection", "impaired-detection", "source-degraded"],
  "sources": [{"pattern": "logs-cloudtrail-*", "max_stale": "2h"}],
  "accepted": [{
    "finding_id": "finding-0123456789abcdef0123",
    "reason": "source migration",
    "expires_at": "2035-01-15T00:00:00Z"
  }]
}

The expiry in this example is illustrative; choose a short, reviewed exception window. Run it with deadair scan --policy deadair-policy.json. Copy acceptance IDs from a restricted, unredacted report. Policy matching happens before optional report redaction, which replaces finding IDs along with the other identifiers. An expired acceptance remains visible as expired, stops suppressing its finding, and gates normally. Source entries use the first matching pattern. The severity threshold applies to rule findings; source findings are controlled by their class. Malformed policy files fail the scan.

Sentinel uses each source’s max_stale policy when checking its 24-hour freshness evidence. An empty 24-hour result proves a source is stale under a 1-hour policy, but cannot settle a 48-hour policy. That longer threshold leaves the assessment incomplete and returns 2 in candidate mode.

A policy can name source patterns, accepted finding IDs, and the reason an exception exists. Check one in only when repository access matches the sensitivity of that information. For a public repository, keep sensitive entries in a restricted file created on the runner and pass that path to policy-file.

Use diff for scheduled checks while the backlog is still being worked down:

deadair scan --json --json-out today.json
deadair diff yesterday.json today.json

diff first checks schema, backend, instance, target, redaction key, source filters, candidate rule set, and assessment configuration. The normalized Sentinel remote-workspace mapping set contributes to target and assessment identity, so changing it requires a new baseline. Incomparable reports exit 2. Comparable reports diff stable reason-level finding IDs, so one newly missing field does not make every impairment on that rule look new. Recoveries are shown but do not fail the command. Candidate comparisons also exit 2 if either scan could not assess the candidate safely. Rerun that scan after resolving the access or input problem before using it in a comparison.

Reports created before v0.6.0 do not contain the comparison identity fields. After upgrading, create a new baseline with v0.6.0 or later.

Add history-based checks

Stateful checks need a local state file:

deadair scan --state-file deadair-state.json

With a state file, deadair records source history for:

  • volume baselines by weekday and hour
  • warmup before low-volume findings can fire
  • hysteresis, so one low scan does not page by itself
  • active and recovered finding history

Ingest lag is sampled from paired timestamps during each eligible Elastic or Sentinel Scheduled scan; it does not depend on the state file. Sentinel NRT rules use ingestion-time freshness and do not receive the Scheduled-rule lag-window finding.

The default thresholds wait for local history before reporting low volume:

Flag Default Meaning
--volume-warmup 24h minimum observation time before volume findings can fire
--volume-hysteresis 2 consecutive low scans required
--volume-min-samples 4 same weekday/hour samples required before evaluation
--volume-z-threshold 3 low-volume z-score threshold

After you have real history, ask deadair for tuning suggestions:

deadair tune --state-file deadair-state.json

Track schema drift

Schema drift uses field_caps snapshots over time:

deadair scan --state-file deadair-state.json --schema

The first scan records a snapshot. Later scans report fields added, removed, or changed in type. This is useful after agent upgrades, package upgrades, parser changes, and pipeline releases.

Schema drift is source-level evidence. It does not automatically prove a rule is broken. Rule impairment is reported when a required field is absent or non-searchable in one or more matched sources and every needed source mapping was read successfully.

Declare maintenance windows

Use downtime windows for expected source quiet periods. This keeps the source visible, suppresses the finding during the window, and prevents the quiet period from poisoning volume baselines.

{"windows": [
  {
    "name": "weekly patch window",
    "sources": ["winlogbeat-*"],
    "days": ["sat"],
    "start": "22:00",
    "end": "02:00",
    "timezone": "Europe/London"
  }
]}

Run with:

deadair scan --state-file deadair-state.json --downtime-file downtime.json

Prefer downtime windows over exclusions. Exclusions hide sources from the listing, which can hide real coverage gaps.

Scan fleets

Use --fleet for multiple SIEM instances:

deadair check --fleet fleet.json
deadair scan --fleet fleet.json
deadair serve --fleet fleet.json

Fleet scans are sequential. One failed tenant is recorded as an instance error and the command exits 2; successful tenants still appear in the same report. With --state-file, deadair writes one state file per instance. The fleet summary carries unused_telemetry_assessment; it is unavailable if any instance failed or could not establish complete local consumer coverage, so a partial unused_bytes total is not presented as a fleet-wide measurement.

Use check --fleet after onboarding a tenant or rotating a tenant secret.

For MSSP operating guidance, including secret layout, redaction, Alertmanager routing, and fleet operations, see mssp.md.

Run the exporter

deadair serve --interval 5m --state-file deadair-state.json

Metrics are exposed on 127.0.0.1:9317 by default. Prometheus scrapes the cached last scan; a scrape does not trigger a SIEM API call. Grafana and Alertmanager examples are in the repository’s contrib directory.

For fleets, route failed scans by deadair_instance_up. No-match findings usually go to detection engineering or onboarding after credential scope is checked. Findings where all matched sources are stale or empty usually belong to telemetry pipeline owners.

Share reports safely

Use --redact for anything leaving the restricted SOC workspace:

deadair scan --json --redact --json-out redacted-report.json
deadair serve --redact
deadair tune --state-file deadair-state.json --redact

Redaction replaces tenant, source, rule, pattern, field, dependency, lineage, provenance, workspace, watchlist, template, and package identifiers with HMAC pseudonyms. Validated dependency probe expressions and their KQL arguments are never serialized. Values use a process-local key by default. For diffs and trend reporting across separate runs, generate a random key in a restricted directory and pass it by path:

umask 077
mkdir -p ~/.config/deadair
openssl rand -hex 32 > ~/.config/deadair/redaction.key
export DEADAIR_REDACT_KEY_FILE="$HOME/.config/deadair/redaction.key"
deadair scan --json-out redacted-report.json

The report records a non-secret key identifier so consumers can check whether two reports are correlatable. It never records the key. Redacted fleet reports also replace raw scan failures, which may contain hosts or response details, with fixed error categories.

Unredacted reports map blind detections, unused telemetry, workspace relationships, dependency aliases, data transformations, and installed content. Treat them like sensitive SOC artifacts. Redaction is not declassification: evidence kinds, statuses, versions, counts, timing, lag, and coverage information remain visible.