Lesson 1 of 1 · 20 min read

IOC Types, Quality Assessment, Lifecycle, and Decay

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