A raw IOC feed is not intelligence — it is data. The analyst’s job is to transform data into actionable, prioritized, contextualized indicators.
IOC Quality Assessment Framework
IOC QUALITY SCORECARD
======================
For each indicator, score 1-5 on each dimension:
1. ATTRIBUTION CONFIDENCE (1=unknown source, 5=confirmed from own IR)
Attribution chain: OSINT feed → vendor report → ISAC → own IR
2. FRESHNESS (1=6+ months old, 5=<7 days old)
Apply decay rates by type:
- IP addresses: max useful life ~60 days after exposure
- Domains: max useful life ~90 days
- File hashes: max useful life ~30 days for specific samples
- TTPs: indefinitely useful
3. ENVIRONMENTAL RELEVANCE (1=irrelevant sector/platform, 5=exact match)
- Does this indicator relate to threats against our industry?
- Does it involve our actual technology stack?
- Have we seen this actor target organizations like ours?
4. SPECIFICITY (1=shared CDN/cloud IP, 5=dedicated attacker infrastructure)
- Is the IP dedicated to malicious activity or shared hosting?
- Is the domain a registered typosquat or a compromised legitimate domain?
5. CORROBORATION (1=single uncorroborated source, 5=multiple independent sources)
- How many independent sources confirm this indicator?
- Was it observed in actual victim environments or only in reports?
DEPLOYMENT DECISION MATRIX:
Score 20-25: Deploy as BLOCK in network controls + SIEM alert
Score 15-19: Deploy as SIEM alert-only, review in 30 days
Score 10-14: Add to CTI database, watch list only
Score <10: Do not operationalize, document for future reference
EXAMPLE SCORING:
Fresh IOC from our own IR (C2 IP seen this week): 5+5+5+5+5 = 25 → BLOCK
90-day-old Cloudflare IP from unknown OSINT feed: 1+1+3+1+1 = 7 → Do not deploy
ISAC-shared domain targeting our industry (2 weeks old): 4+4+5+4+3 = 20 → BLOCK
Indicator Lifecycle Management
# IOC lifecycle tracker pseudocode
# Demonstrates the decisions in IOC management
class Indicator:
def __init__(self, value, type, source, first_seen, confidence):
self.value = value
self.type = type # ip, domain, hash, yara
self.source = source
self.first_seen = first_seen
self.confidence = confidence # 0-100
self.hit_count = 0
self.last_hit = None
self.status = 'active'
def decay_period_days(self):
"""Return expected useful lifetime by indicator type"""
decay_map = {
'ip': 60,
'domain': 90,
'md5': 30,
'sha256': 30,
'url': 45,
'yara': 180,
'sigma': 365, # Behavioral rules decay much slower
}
return decay_map.get(self.type, 60)
def review_status(self, current_date):
"""Determine if indicator should be retired, renewed, or kept"""
age_days = (current_date - self.first_seen).days
if age_days > self.decay_period_days():
if self.hit_count == 0:
# Never fired — either never matched (clean) or already decayed
return 'RETIRE: No hits, exceeded decay period'
elif self.last_hit and (current_date - self.last_hit).days > 30:
# Had hits but not recently
return 'REVIEW: No recent hits — may be decayed'
else:
# Still hitting — likely still active
return 'RENEW: Indicator still generating matches'
else:
return 'ACTIVE: Within decay period'
# Quarterly review process
def quarterly_ioc_review(indicator_list, current_date):
retire_list = []
review_list = []
for ioc in indicator_list:
status = ioc.review_status(current_date)
if 'RETIRE' in status:
retire_list.append(ioc)
elif 'REVIEW' in status:
review_list.append(ioc)
print(f"Quarterly Review: {len(retire_list)} to retire, {len(review_list)} require human review")
return retire_list, review_list