Skip to content

Launching a campaign

Launching a campaign

A campaign is a durable record of a fleet-wide sweep. When the offline campaigns feature is enabled, two operator surfaces create campaigns:

  • The Hunts page — every hunt writes a campaign with a frozen IOC snapshot and assigns the compiled queries to every targeted host.
  • The Queries page — tick Launch as campaign on a free-form or saved query to send raw osquery through the same pipeline. See Run a query as a campaign.

Either way, hosts that were offline at launch time pick up their assignment when they reconnect, run it, and report results back — so a campaign that started yesterday can still catch the laptop that came back online this morning.

If offline campaigns are disabled, both the hunt path and the Launch as campaign toggle on the Queries page dispatch live-only (or are disabled), and this page is empty. Toggling the feature on creates campaigns going forward; it doesn’t retroactively backfill past hunts.

Two kinds of campaigns

Every campaign carries a kind discriminator that distinguishes IOC-hunt campaigns from osquery campaigns. The Campaigns list and detail UI mix both kinds together — the lifecycle is identical — and a Kind column on the list page tells them apart.

ioc_huntosquery
Created byLaunching a huntLaunching as campaign on the Queries or Saved queries surface
PayloadFrozen IOC snapshot; IOC compiler emits per-type SQLOperator-typed SQL (free-form or from a saved query)
Per-row outputMatches against the frozen IOC snapshotRaw osquery rows
Alerts producedYes — source='campaign' IOC alerts for every matchNone — there’s nothing to match against, just data
Detail page frozen cardFrozen IOCs (snapshot of indicators)Frozen SQL (the literal query text)
Detail page late cardLate Matches (IOC type, value, host, time)Late Responses (host, row count, time, “view rows”)

Same campaign list, same coverage card, same pending-hosts card, same Stop button, same audit trail, same feature flag. The cards that hold the run-specific data change shape because the data is necessarily different.

What you get

The Campaigns page is a single table, one row per campaign, sorted newest first. Each row shows:

  • Name — the campaign label (auto-generated from the hunt name or supplied at launch time for osquery campaigns).
  • Kindioc_hunt or osquery. Optional filter chip at the top of the table lets you narrow to one kind.
  • Statusactive, stopped, or archived.
  • Assigned — how many hosts the campaign was dispatched to at launch.
  • Responded — how many of those have reported in.
  • Pending — how many haven’t responded yet (still offline, or still working).
  • Hits — for ioc_hunt, the count of hosts whose results matched the frozen IOC snapshot. For osquery, the count of hosts that returned at least one row. The column header is shared; the underlying number is kind-appropriate.
  • Created — relative timestamp of the launch.
  • Stop — admin-only action button on active rows (see Reading a campaign for the stop semantics).

Click any row to open its detail view. Admin users get a Stop button inline on each active row; non-admins get a read-only table.

Nag highlights

A campaign that’s still active long after launch is usually a mistake — most of the offline hosts will never reconnect, and the row clutters the table. Mimir colors active rows by age:

AgeColorMeaning
< 30 daysnonenormal
30–90 daysyellow”consider reviewing pending hosts”
90–180 daysorange”many hosts may be decommissioned”
> 180 daysred”review and stop if no longer needed”

Hover the row to see the full nag message. None of these automatically stop the campaign; they’re a triage hint, not an expiry.

Enabling and disabling

Offline campaigns are gated by the runtime feature flag offline_campaigns_enabled. When the flag is off (the default in a fresh deployment), the Campaigns page shows an empty state:

Campaigns are disabled. Hunts dispatch directly to currently connected agents and don’t create a durable record for offline hosts to pick up later.

Enable Campaigns [button]

Click the button to flip the flag on. Subsequent hunts will create campaign rows. The page shows a small Last changed N ago by username footer so the toggle is auditable.

When the flag is on with no campaigns yet:

No campaigns yet. Launch a hunt and a campaign row appears here.

Campaigns are enabled. [Disable button]

Disabling does not delete existing campaigns. It just stops future hunts from creating new ones; hunts revert to direct live dispatch.

Flipping the flag requires the admin role — PUT /api/v1/feature-flags/offline_campaigns_enabled is gated by withAdminAuth. Reading the current state (and seeing the toggle) is open to any signed-in user. The button shows the last-flipped attribution under the toggle (Last changed Nm ago by alice@example.com) so a team can see who turned it on or off without leaving the page.

How campaigns are actually created

You don’t launch a campaign from this page directly — campaigns are derived from the surface that produced them: a hunt on the Hunts page, or a query launched with Launch as campaign on the Queries page. Either path produces a row on this page when the flag is on.

From a hunt (kind='ioc_hunt')

The hunt machinery routes through the campaign path when the flag is on. From the user’s perspective, Launch a hunt is the same form either way. What changes under the hood:

  1. The hunt’s IOC list is frozen into a snapshot stored on the campaign row. Future deactivation of an IOC doesn’t affect campaign matching — it sees the IOCs as they were at launch.
  2. The IOC compiler turns the snapshot into per-type osquery.
  3. Every targeted host gets a campaign assignment row, not a one-shot RPC. Offline hosts still get the assignment, with a lease window (default 30 minutes) per assignment slot.
  4. When an offline host reconnects, the reconnect dispatcher claims its assignments (FOR UPDATE SKIP LOCKED plus a token-bucket and per-host jitter), dispatches the SQL, and ingests the results.
  5. Matches from the late-arriving host produce IOC alerts written with source='campaign', so they’re attributable back to the originating campaign on the Alerts page. Mimir’s source-filter chips currently surface Watchlist, Historical, and Hunt; campaign-sourced alerts appear in unfiltered views and are reachable via the campaign detail page’s late-matches table.

From a query (kind='osquery')

Tick Launch as campaign on the Queries page, or open the per-row action menu on a row in the Saved queries rail. What’s different from the hunt path:

  1. The campaign carries the operator’s literal SQL in a Frozen SQL field rather than an IOC snapshot.
  2. There’s no IOC compilation step — the campaign dispatches one query per assignment, the SQL operators typed.
  3. The deny-list runs at three surfaces (launch, live-drain enqueue, reconnect dispatch) so operator-typed SQL can’t slip past the safety net at any of them.
  4. Per-host row caps replace the live-only path’s shared per-query cap, so a fleet-scale campaign doesn’t silently lose late-arriving hosts’ rows.
  5. No alerts are produced — the data lands as rows on the campaign’s detail page (Late Responses card) and as raw osquery rows queryable from the campaign’s query_id.

Full detail in Run a query as a campaign.

Why offline campaigns matter

This is the load-bearing reason for offline campaigns: incident response shouldn’t lose hosts to a coffee break. A laptop that closed its lid mid-hunt — or mid-query — still answers when it wakes up. The campaign carries the work durably so a transient disconnect doesn’t turn into a silent under-count.

When to enable

Three patterns:

  1. Active IR engagements. Turn the flag on at the start of an incident-response window. Every hunt for the duration captures late responders. Turn it off afterwards if you don’t want the ongoing book-keeping.
  2. Distributed remote fleets. Field laptops, traveling workstations, anything that’s intermittently connected. The live-only path consistently underreports because the host you care about is offline at launch.
  3. Compliance evidence trails. A campaign is a durable record of “we asked every host about X on day Y” — useful for auditors who want to see proof of completion, not just a point-in-time count of online hosts.

When to leave off

Three patterns where the live-only path is fine:

  1. Static fleets where every endpoint is online effectively 100% of the time (e.g., server fleets in a single datacenter).
  2. Speed-of-light-only hunts where late matches would actively confuse downstream automation (an IR pipeline that pages on every campaign alert would get spurious pages from campaigns days old).
  3. Database / storage-constrained tenants. Campaign rows plus assignments add steady write load proportional to fleet size × hunt frequency. A small tenant on a shared database may prefer the lighter live-only path.

What this page won’t do

A few deliberate non-features:

  • There’s no “launch a custom campaign” form on this page. Campaigns are launched from their originating surface — a hunt on the Hunts page for ioc_hunt campaigns, or Launch as campaign on the Queries page for osquery campaigns. The campaigns page is the read-and-stop view; the launch UI lives where the work was defined.
  • No editing of an active campaign. The frozen payload (IOC snapshot or SQL text) is locked at launch. To change it, stop the campaign and re-launch from the originating surface.
  • No re-targeting. A campaign’s assigned-host set is locked at launch. A host that wasn’t in the fleet at launch time won’t pick up the assignment when it enrolls later.
  • OS-filtered targets aren’t supported yet for osquery campaigns. A saved query whose stored scope is os_in is rejected at launch with a clear modal error — pick Fleet or a specific host list instead. Tracked as a follow-up.

For those workflows, the originating surface is the right tool — campaigns are the durability layer underneath, not a separate UI.

Permissions

Listing campaigns (GET /api/v1/campaigns) is gated by withAnyAuth. Stopping a campaign requires the admin role. Reading the offline_campaigns_enabled flag (GET /api/v1/feature-flags) is open to any signed-in user; toggling it (PUT /api/v1/feature-flags/{key}) requires the admin role because feature flags can gate security-sensitive behavior. The toggle’s last-flipped attribution (Last changed Nm ago by alice@example.com) is shown to every user who can see the page, not just the admin who flipped it.

Where to next

  • Reading a campaign — the per-campaign detail view, pending hosts, late matches and late responses, and the stop flow.
  • Launch a hunt — the form that creates an ioc_hunt campaign when the flag is on.
  • Run a query as a campaign — the toggle on the Queries page that creates an osquery campaign.
  • Matching modes — how the three live matching paths work, and how campaign late matches land as campaign-source alerts.