Matching modes and confidence
Matching modes and confidence
Every active indicator in Mimir is checked against your fleet through three independent paths: a real-time watchlist that sweeps every connected agent on a steady cadence, a retroactive historical scan that fires once when the indicator is first added, and hunts an operator launches on demand. This page explains what each path does and why feed-imported indicators come with a confidence score that decays over time.
The three matching paths
Real-time watchlist
A leader-elected worker on the Mimir server compiles every active
IOC into osquery SQL and dispatches it to every connected agent
once every 15 minutes. The agent runs the query, returns
matches, and Mimir creates an alert tagged watchlist_pack for
each result.
What the watchlist sees depends on the indicator family:
- File hashes / filenames / paths. A query against osquery’s
file_events+hashjoin, scoped to events in the last 20 minutes (time > now - 1200s). - Registry keys (Windows). Querying the
registrytable for the indicator’s path. Pure-snapshot, not evented. - IPv4 + domain. Two queries per tick — a snapshot against
process_open_sockets/ current DNS state, plus an evented query againstsocket_events/dns_lookup_eventsto catch connections that opened and closed inside the tick window.
The watchlist sees activity going forward: every new file event, DNS lookup, or socket open is checked against the active IOC set once per tick. An indicator added at 09:01 starts being checked on the next tick (within 15 minutes).
Retroactive historical scan
When you add a new indicator, Mimir kicks off a one-shot scan of
the last 90 days of stored query results, looking for any host
that already saw the indicator. The window is set by
MaxHistoricalWindow and capped at 500 distinct hosts per scan to
keep the database load bounded.
Any match found is alerted with the source historical_match. The
evidence row includes the original timestamp the activity was
recorded — so an indicator you added today can produce an alert
that says “this host ran the binary 47 days ago.” This is the
most-loved feature for incident response: a vendor publishes a
fresh hash and you can immediately answer “did any of our hosts
ever see this?”
The scan runs asynchronously in a goroutine. For a single
hand-added indicator it usually completes within seconds. For a
large bulk import the scan can take up to 30 minutes — the budget
is matchHistoricalBulkTimeout = 30m precisely because the 90-day
window scans far more rows than the older 7-day cutoff did.
Hunts (on-demand)
A hunt is an operator-driven sweep:
“don’t wait 15 minutes, ask every host right now.” The compiler
takes the same IOC set, builds the same osquery SQL, and
dispatches it directly to every targeted agent, then streams the
results back into a hunt detail view. Hunt matches are alerted
with the source hunt.
Hunts are the only path you control directly. The other two run automatically.
How feed-imported confidence works
Every indicator has a confidence score from 0 to 100. Hand-added indicators default to 100 — you said it’s bad, Mimir trusts you. Feed-imported indicators default to 70 — the source is reputable but not authoritative for your environment, so the score leaves room to deactivate noisy entries automatically.
A leader-elected worker runs once a day to decay feed-imported confidence by 5 points per day. Any feed IOC whose post-decay score drops below 20 is automatically deactivated (kept in the database, not matched against agent activity). New IOCs get a 7-day grace period before the first decay tick.
The two thresholds are pinned constants in
internal/server/ioc_decay.go:
const ConfidenceDecayPerDay = 5const ConfidenceDeactivateBelow = 20A worked example. A URLhaus indicator imported on day 0 starts at confidence 70. With no further activity it tracks like this:
| Day | Confidence | Active? |
|---|---|---|
| 0 | 70 | yes (newly imported) |
| 7 | 70 | yes (still in grace period) |
| 8 | 65 | yes (first decay tick after grace) |
| 17 | 20 | yes (one decay step from threshold) |
| 18 | 15 | no — auto-deactivated |
A successful re-poll of the source feed that re-emits the same indicator value boosts the confidence back up (and re-activates the IOC if it had been auto-deactivated). So an indicator abuse.ch keeps emitting stays hot indefinitely; one they drop goes inactive on its own about 10 days later.
The Confidence column on the indicators table is colored by band: green over 70, yellow 31–70, red 30 and below. The narrow bar visualizes the current score so you can scan the page for indicators that are about to age out.
Why hand-added indicators don’t decay
Decay only runs against feed-imported IOCs (source_id IS NOT NULL and not the magic manual-source UUID). Hand-added IOCs
stay at confidence 100 forever unless you change them. The
intuition: an analyst typing a hash into the form is making a
deliberate decision that Mimir shouldn’t second-guess. A feed
publishing 200,000 hashes is statistical, and statistics deserve
decay.
Activating and deactivating manually
Every indicator row has an Active / Inactive button in the last column. Clicking it toggles the row.
- Active → Inactive. Stops matching immediately. The watchlist’s next tick (within 15 minutes) drops the IOC from its query set. The indicator stays in the database — you can reactivate later, and feed re-polls won’t override your decision (the deactivation is preserved).
- Inactive → Active. Resumes matching. No retroactive 90-day scan is re-run on reactivation; if you want one, delete and re-create the indicator.
Use Inactive as the right answer for “this feed indicator is producing noise in my environment.” Don’t delete the row — keep it inactive so a future feed poll won’t silently re-import the same value and start matching again.
Source attribution and the filter chip
Every alert carries its source tag (watchlist_pack,
historical_match, hunt, or campaign) so you can tell which
path produced it. The Alerts page exposes
the first three as filter chips — useful for “show me only the
late matches from this morning’s bulk import” or “show me only
the alerts my last hunt produced.” Campaign-sourced alerts (from
offline-host responses arriving after the live hunt closed) are
reachable from the campaign detail page
rather than via a toolbar chip.
Troubleshooting
An indicator I added is showing confidence 100 but never
matching. The watchlist runs every 15 minutes against
connected agents only. If the only host that has the artifact
hasn’t checked in since you added the indicator, you won’t see
a match yet. Open Hosts to confirm
the agent is online; if it isn’t, the indicator catches it
either on reconnect (via the campaign path, when
offline_campaigns_enabled is on — see
Campaigns) or when you run a
hunt once it’s back.
Feed indicators keep auto-deactivating before I get to investigate them. That means the feed isn’t re-emitting the values on subsequent polls — the upstream source has dropped them, which is itself information (“the campaign aged out”). If you genuinely care about preserving feed indicators past their natural lifetime, copy the value into a manual indicator; hand-added IOCs don’t decay.
Historical scan finished but I don’t see late matches I
expected. Two checks: confirm the indicator’s type matches
what the host actually has (a SHA256 indicator can’t match a
host that only logged the filename — see
Indicator types), and confirm the activity was
inside the 90-day window (MaxHistoricalWindow). Older activity
is not in the scan range.
Where to next
- Indicator types — what each indicator type matches against on the agent side.
- Add an indicator — the form path for hand-added IOCs that don’t decay.
- The alert feed — where matches surface, and how to filter by source tag.
- Launch a hunt — the on-demand matching path.