How the OpenCTI Indicator Decay Manager Works: A Technical Deep Dive
The OpenCTI indicator decay manager automatically ages Indicators by running a scheduled background job that selects stale indicators, recomputes their scores based on Decay Rules, and updates their validity status.
OpenCTI, an open-source cyber threat intelligence platform, implements automatic indicator lifecycle management through the indicator decay manager. This system ensures that Indicators (IP addresses, file hashes, domains, etc.) automatically lose relevance over time according to configurable decay rules, reducing false positives from outdated threat intelligence.
What Is the Indicator Decay Manager?
The indicator decay manager is a background service registered with OpenCTI's generic manager framework. It operates on a scheduled interval to process batches of Indicators whose decay_next_reaction_date has passed. For each selected Indicator, the manager:
- Retrieves the associated Decay Rule (containing score points, revoke thresholds, and reaction dates)
- Calculates the next stable score based on the decay curve
- Determines if the Indicator should be revoked
- Updates the Indicator's score, history, and next reaction timestamp
The implementation resides primarily in opencti-platform/opencti-graphql/src/manager/indicatorDecayManager.ts with core logic delegated to src/modules/indicator/indicator-domain.ts.
Configuration and Initialization
Manager Settings
The indicator decay manager is controlled through configuration keys loaded at startup in indicatorDecayManager.ts:
| Config Key | Default | Purpose |
|---|---|---|
indicator_decay_manager:enabled |
true |
Master switch to enable/disable the manager |
indicator_decay_manager:lock_key |
indicator_decay_manager_lock |
Distributed lock key preventing parallel execution across cluster nodes |
indicator_decay_manager:interval |
60000 (1 minute) |
Milliseconds between scheduler invocations |
indicator_decay_manager:batch_size |
10000 |
Maximum indicators processed per execution cycle |
const INDICATOR_DECAY_MANAGER_ENABLED = booleanConf('indicator_decay_manager:enabled', true);
const INDICATOR_DECAY_MANAGER_KEY = conf.get('indicator_decay_manager:lock_key') || 'indicator_decay_manager_lock';
const SCHEDULE_TIME = conf.get('indicator_decay_manager:interval') || 60000;
const BATCH_SIZE = conf.get('indicator_decay_manager:batch_size') || 10000;
Registration with the Manager Framework
The manager registers itself using the registerManager function, providing a cron-like handler that the framework invokes on the defined interval:
const INDICATOR_DECAY_MANAGER_DEFINITION: ManagerDefinition = {
id: 'INDICATOR_DECAY_MANAGER',
label: 'Indicator decay manager',
executionContext: 'indicator_decay_manager',
cronSchedulerHandler: {
handler: indicatorDecayHandler,
interval: SCHEDULE_TIME,
lockKey: INDICATOR_DECAY_MANAGER_KEY,
},
enabledByConfig: INDICATOR_DECAY_MANAGER_ENABLED,
enabledToStart() { return this.enabledByConfig; },
enabled() { return this.enabledByConfig; },
};
registerManager(INDICATOR_DECAY_MANAGER_DEFINITION);
How the Decay Process Works
Step 1: Selecting Stale Indicators
The indicatorDecayHandler function creates an execution context and queries for indicators requiring decay using findIndicatorsForDecay from src/modules/indicator/indicator-domain.ts:
const context = executionContext('indicator_decay_manager');
const indicatorsToUpdate = await findIndicatorsForDecay(context, DECAY_MANAGER_USER, BATCH_SIZE);
The selector queries the database with specific filters:
export const findIndicatorsForDecay = (context, user, maxSize) => {
const filters = {
orderBy: 'decay_next_reaction_date',
orderMode: OrderingMode.Asc,
mode: FilterMode.And,
filters: [
{ key: ['decay_next_reaction_date'], values: [prepareDate()], operator: FilterOperator.Lt },
{ key: ['revoked'], values: ['false'] },
],
filterGroups: [],
};
const args = { filters, maxSize };
return fullEntitiesList(context, user, [ENTITY_TYPE_INDICATOR], args);
};
This query retrieves up to BATCH_SIZE indicators where:
decay_next_reaction_dateis less than the current time (stale)revokedisfalse(not already revoked)- Results are ordered by reaction date (oldest first)
Step 2: Computing the Next Decay Step
For each selected indicator, the handler invokes updateIndicatorDecayScore, which delegates to computeIndicatorDecayPatch in indicator-domain.ts. This function calculates the new score based on the associated Decay Rule's decay_points array:
const newStableScore = model.decay_points.find(p => (p || indicator.x_opencti_score) < indicator.x_opencti_score) || model.decay_revoke_score;
if (newStableScore) {
const newDecayHistoryPoint = { updated_at: new Date(), score: newStableScore, updated_by: user.id };
const decayHistory = computeIndicatorDecayHistory([...indicator.decay_history ?? []], newDecayHistoryPoint);
patch = { x_opencti_score: newStableScore, decay_history: decayHistory };
if (newStableScore <= model.decay_revoke_score) {
patch = { ...patch, revoked: true, x_opencti_detection: false };
} else {
const nextScoreReactionDate = computeNextScoreReactionDate(indicator.decay_base_score, newStableScore, model, moment(indicator.valid_from));
if (nextScoreReactionDate) {
patch = { ...patch, decay_next_reaction_date: nextScoreReactionDate };
}
}
}
The logic follows this decision tree:
- Find the next lower score in the decay rule's points array
- If the new score is at or below
decay_revoke_score, mark the indicator as revoked and disable detection - Otherwise, calculate the next reaction date using
computeNextScoreReactionDateand schedule future processing
Step 3: Persisting Updates
The computed patch is persisted using patchAttribute:
return patchAttribute(context, user, indicator.id, ENTITY_TYPE_INDICATOR, patch);
This updates the indicator's document in Elasticsearch with the new score, history, revocation status, and next reaction date.
Error Handling and Resilience
The indicator decay manager implements robust error isolation. Within the processing loop, individual indicator failures are caught and logged without aborting the batch:
try {
await updateIndicatorDecayScore(context, DECAY_MANAGER_USER, indicator);
} catch (e) {
logApp.error('[OPENCTI-MODULE] Error when processing decay, skipping.', { cause: e, id: indicatorsToUpdate[i].id });
errorCount += 1;
}
After processing, the manager emits a summary log:
if (errorCount > 0) {
logApp.error('[OPENCTI-MODULE] Indicator decay manager got errors.', { errors_count: errorCount, indicators_count: indicatorsToUpdate.length });
} else {
logApp.debug('[OPENCTI-MODULE] Indicator decay manager updated', { indicators_count: indicatorsToUpdate.length });
}
This design ensures that a corrupted indicator or transient database error cannot stall the decay process for thousands of other indicators.
Code Example: Manual Trigger
For testing or administrative purposes, you can manually invoke the decay handler. This respects the same configuration and batch size limits defined in the manager:
import { indicatorDecayHandler } from '../src/manager/indicatorDecayManager';
(async () => {
// Directly invoke the handler – useful for unit tests or forced runs
await indicatorDecayHandler();
console.log('Decay run finished');
})();
In production, the cron scheduler automatically invokes this handler according to the indicator_decay_manager:interval configuration. Manual invocation is primarily used in integration tests located at tests/03-integration/04-manager/indicatorDecayManager-test.ts.
Summary
- The indicator decay manager is a scheduled background service in OpenCTI that automatically ages Indicators based on configurable Decay Rules.
- Configuration in
indicatorDecayManager.tscontrols enablement, interval (default 60 seconds), batch size (default 10,000), and distributed locking. - The manager queries for stale indicators using
findIndicatorsForDecayinindicator-domain.ts, selecting only non-revoked indicators whosedecay_next_reaction_datehas passed. - Each indicator is processed via
updateIndicatorDecayScore, which calculates the next score step, updates history, determines revocation status, and schedules the next reaction date. - Error isolation ensures individual processing failures are logged without stopping the batch, maintaining system stability.
Frequently Asked Questions
How often does the indicator decay manager run?
By default, the indicator decay manager runs every 60 seconds (60,000 milliseconds). This interval is controlled by the indicator_decay_manager:interval configuration key in indicatorDecayManager.ts. You can adjust this value based on your performance requirements and the volume of indicators in your deployment.
What happens when an indicator's score reaches the revoke threshold?
When the computed newStableScore is less than or equal to the Decay Rule's decay_revoke_score, the manager automatically sets revoked: true and x_opencti_detection: false on the indicator. This marks the indicator as invalid and removes it from active detection rules, effectively retiring the threat intelligence once it has aged sufficiently.
Can the indicator decay manager process indicators in parallel across multiple OpenCTI instances?
Yes, the manager supports distributed deployments through the indicator_decay_manager:lock_key configuration. This lock key (default: indicator_decay_manager_lock) prevents multiple OpenCTI instances from running the decay process simultaneously, ensuring that the same indicator is not processed twice in a clustered environment.
Where can I find the integration tests for the indicator decay manager?
The integration tests are located at tests/03-integration/04-manager/indicatorDecayManager-test.ts in the OpenCTI repository. These tests demonstrate the manager's behavior by manually invoking indicatorDecayHandler and verifying that indicators transition through decay states correctly, including score reductions and eventual revocation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →