threat intelligence

Correlating Threat Campaigns

Correlates disparate security incidents, IOCs, and adversary behaviors across time and organizations to identify unified threat campaigns, attribute them to common threat actors, and extract shared indicators for improved detection. Use when multiple incidents exhibit overlapping indicators, when sector-wide attack campaigns require cross-organizational analysis, or when building campaign-level intelligence products. Activates for requests involving campaign analysis, incident clustering, cross-organizational IOC correlation, or MISP correlation engine.

att&ckcampaign-analysisclusteringcorrelationctiintrusion-setmispthreat-actor
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

Use this skill when:

  • Multiple unrelated-appearing incidents share IOCs (same C2 IP, same malware hash, similar TTPs)
  • An ISAC partner shares indicators from an incident that match your own historical events
  • Building a campaign report linking adversary activity over weeks or months to a single operation

Do not use this skill to force correlation based on weak signals — false campaign attribution misleads defenders and wastes resources on incorrect threat models.

Prerequisites

  • TIP or SIEM with historical indicator and event data (90+ days recommended)
  • MISP correlation engine enabled with event sharing configured
  • Graph analysis tool (Maltego, Neo4j, or OpenCTI) for relationship visualization
  • Reference to MITRE ATT&CK intrusion set and campaign objects for structuring output

Workflow

Step 1: Collect and Normalize Events

Gather all candidate events for correlation from:

  • Internal SIEM (raw events, alert history)
  • TIP (historical indicators and events)
  • ISAC sharing (partner-submitted events in MISP or TAXII)
  • Commercial intelligence (Recorded Future, Mandiant, CrowdStrike reports)

Normalize all events to STIX 2.1 schema with consistent timestamp (UTC), indicator types, and confidence scores. Ensure all indicators have source attribution and collection date.

Step 2: Identify Correlation Pivot Points

Apply systematic pivot analysis across four dimensions:

Infrastructure pivots:

  • Same IP address or /24 subnet across events
  • Same domain registrant email or WHOIS organization
  • Same ASN or hosting provider with same account fingerprint
  • Same SSL certificate fingerprint or serial number across C2 domains

Capability pivots:

  • Same malware hash or YARA signature match
  • Same C2 communication protocol (Cobalt Strike beacon config, Sliver implant parameters)
  • Same exploit code or weaponized document template
  • Same obfuscation method or packer fingerprint

Temporal pivots:

  • Events occurring within same time window (operational hours suggesting same timezone)
  • Sequential events with logical kill chain progression
  • Malware compilation timestamps clustering in same date range

Victimology pivots:

  • Same target sector (healthcare, energy, financial)
  • Same target geography
  • Same targeted technology (specific ERP vendor, VPN appliance brand)

Step 3: Calculate Correlation Confidence

Apply weighted scoring for campaign attribution:

def calculate_campaign_confidence(events: list) -> float:
    scores = []
 
    # Infrastructure overlap (highest weight — most discriminating)
    infra_overlap = count_shared_infra(events) / len(events)
    scores.append(infra_overlap * 40)
 
    # Capability overlap (high weight — TTPs are durable)
    capability_overlap = count_shared_ttps(events) / len(events)
    scores.append(capability_overlap * 35)
 
    # Temporal proximity (moderate weight)
    temporal_score = assess_temporal_clustering(events)
    scores.append(temporal_score * 15)
 
    # Victimology alignment (lower weight — many actors target same sector)
    victim_score = assess_victim_pattern(events)
    scores.append(victim_score * 10)
 
    total = sum(scores)
    if total >= 70: return "HIGH"
    elif total >= 45: return "MEDIUM"
    else: return "LOW"

Step 4: Build Campaign Graph

In OpenCTI or Maltego, construct campaign graph:

  • Campaign object (STIX) as central node
  • Intrusion Set → uses → Malware objects
  • Intrusion Set → uses → Infrastructure objects
  • Intrusion Set → targets → Identity objects (victim organizations/sectors)
  • Campaign → attributed-to → Threat Actor (if attribution achieved)
  • Indicators → indicates → Malware (linking technical observables to capabilities)

Label each relationship with evidence reference and confidence.

Step 5: Produce Campaign Intelligence Report

Structure the campaign report:

  1. Campaign name: Assign descriptive codename based on targeting theme or tooling
  2. Timeline: First/last observed dates with activity phases
  3. Attribution: Suspected threat actor with confidence level
  4. Target profile: Industry verticals, geographies, organization sizes
  5. TTPs summary: ATT&CK Navigator heatmap for campaign-specific techniques
  6. Shared indicators: IOCs that span multiple incidents (highest confidence for blocking)
  7. Detection guidance: Sigma/YARA rules specific to this campaign

Key Concepts

Term Definition
Campaign STIX object representing a grouping of adversarial behaviors with common objectives over a defined time period
Intrusion Set STIX object grouping related intrusion activity by common objectives, even when actor identity is uncertain
Pivot Using a single data point (IOC, infrastructure, TTP) to discover related events or adversary artifacts
Clustering Machine learning or manual grouping of incidents based on feature similarity to identify campaign boundaries
False Correlation Incorrect linking of unrelated incidents due to shared infrastructure (CDNs, shared hosting) or common tools

Tools & Systems

  • MISP Correlation Engine: Automatic correlation of events sharing attribute values across the MISP instance and federated instances
  • OpenCTI Graph: Interactive relationship graph for visualizing campaign linkages with STIX object types
  • Maltego: Link analysis for infrastructure and capability pivoting across multiple data sources
  • Neo4j: Graph database with Cypher queries for large-scale campaign correlation (millions of events)

Common Pitfalls

  • CDN/Shared hosting false positives: Cloudflare, AWS CloudFront, and bulletproof hosters serve multiple threat actors. Shared IP alone does not establish campaign linkage.
  • Common malware conflation: Multiple threat actors use Cobalt Strike. Shared capability does not indicate same actor without additional corroboration.
  • Premature attribution: Forcing campaign-to-actor attribution before evidence threshold is reached produces incorrect intelligence that persists in reports.
  • Missing temporal analysis: Events from different years may share infrastructure that was recycled by a different actor, not the same campaign.
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md2.1 KB

Threat Campaign Correlation API Reference

MISP REST API

# Search attributes
curl -X POST "https://misp.example.com/attributes/restSearch" \
  -H "Authorization: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"type": "ip-src", "value": "185.220.101.42"}'
 
# Search events by tag
curl -X POST "https://misp.example.com/events/restSearch" \
  -H "Authorization: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"tags": ["apt28", "fancy-bear"], "from": "2024-01-01"}'
 
# Get event with correlations
curl "https://misp.example.com/events/view/1234" \
  -H "Authorization: YOUR_API_KEY" -H "Accept: application/json"
 
# Add attribute to event
curl -X POST "https://misp.example.com/attributes/add/1234" \
  -H "Authorization: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"type": "ip-dst", "value": "203.0.113.50", "category": "Network activity", "to_ids": true}'

MISP Correlation Engine

Correlation Type Description
Attribute match Same value in multiple events
CIDR overlap IPs in same /24 or /16 subnet
Fuzzy hash (ssdeep) Similar malware samples
Over-correlation Common values excluded (CDN IPs)

OpenCTI GraphQL API

# Query campaign relationships
query {
  campaign(id: "campaign-uuid") {
    name
    first_seen
    last_seen
    objectsOfRelationship(relationship_type: "uses") {
      edges { node { ... on Malware { name } } }
    }
    objectsOfRelationship(relationship_type: "attributed-to") {
      edges { node { ... on IntrusionSet { name aliases } } }
    }
  }
}

STIX 2.1 Campaign Object

{
  "type": "campaign",
  "spec_version": "2.1",
  "id": "campaign--uuid",
  "name": "Operation ShadowStrike",
  "first_seen": "2024-01-15T00:00:00Z",
  "last_seen": "2024-06-30T00:00:00Z",
  "objective": "Data exfiltration from financial sector"
}

STIX Relationship Types

Type Source Target
attributed-to Campaign Threat Actor
uses Intrusion Set Malware / Tool
targets Campaign Identity / Sector
indicates Indicator Malware
related-to Any Any

Scripts 1

agent.py7.3 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Threat campaign correlation agent using MISP and STIX."""

import json
import sys
import urllib.request
import ssl
from collections import Counter
from datetime import datetime


class MISPClient:
    """Client for MISP REST API for campaign correlation."""

    def __init__(self, url, api_key, verify_ssl=False):
        self.base_url = url.rstrip("/")
        self.headers = {
            "Authorization": api_key,
            "Content-Type": "application/json",
            "Accept": "application/json",
        }
        self.ctx = ssl.create_default_context()
        if not verify_ssl:
            self.ctx.check_hostname = False
            self.ctx.verify_mode = ssl.CERT_NONE

    def _request(self, method, path, data=None):
        url = f"{self.base_url}{path}"
        body = json.dumps(data).encode() if data else None
        req = urllib.request.Request(url, data=body, headers=self.headers, method=method)
        try:
            with urllib.request.urlopen(req, context=self.ctx, timeout=60) as resp:
                return json.loads(resp.read().decode())
        except Exception as e:
            return {"error": str(e)}

    def search_attributes(self, attr_type, value):
        """Search MISP for attributes matching type and value."""
        data = {"type": attr_type, "value": value, "searchall": 1}
        return self._request("POST", "/attributes/restSearch", data)

    def search_events(self, tags=None, date_from=None, date_to=None):
        """Search MISP events with filters."""
        data = {}
        if tags:
            data["tags"] = tags
        if date_from:
            data["from"] = date_from
        if date_to:
            data["to"] = date_to
        return self._request("POST", "/events/restSearch", data)

    def get_event(self, event_id):
        """Get full event details by ID."""
        return self._request("GET", f"/events/view/{event_id}")

    def get_correlations(self, event_id):
        """Retrieve correlation data for a MISP event."""
        event = self.get_event(event_id)
        if "error" in event:
            return event
        correlations = []
        ev = event.get("Event", event)
        for attr in ev.get("Attribute", []):
            if attr.get("RelatedAttribute"):
                for rel in attr["RelatedAttribute"]:
                    correlations.append({
                        "source_event": event_id,
                        "source_attr": attr.get("value"),
                        "source_type": attr.get("type"),
                        "related_event": rel.get("event_id"),
                        "related_value": rel.get("value"),
                    })
        return correlations


def calculate_campaign_confidence(events):
    """Calculate campaign attribution confidence from correlated events."""
    if not events or len(events) < 2:
        return {"confidence": "LOW", "score": 0, "reason": "Insufficient events"}

    all_ips = []
    all_domains = []
    all_hashes = []
    all_tags = []

    for event in events:
        ev = event.get("Event", event)
        for attr in ev.get("Attribute", []):
            atype = attr.get("type", "")
            val = attr.get("value", "")
            if atype in ("ip-src", "ip-dst"):
                all_ips.append(val)
            elif atype in ("domain", "hostname"):
                all_domains.append(val)
            elif "hash" in atype or "md5" in atype or "sha" in atype:
                all_hashes.append(val)
        for tag in ev.get("Tag", []):
            all_tags.append(tag.get("name", ""))

    ip_counts = Counter(all_ips)
    domain_counts = Counter(all_domains)
    hash_counts = Counter(all_hashes)

    shared_ips = sum(1 for c in ip_counts.values() if c > 1)
    shared_domains = sum(1 for c in domain_counts.values() if c > 1)
    shared_hashes = sum(1 for c in hash_counts.values() if c > 1)

    num_events = len(events)
    infra_score = min(40, (shared_ips + shared_domains) / max(num_events, 1) * 40)
    capability_score = min(35, shared_hashes / max(num_events, 1) * 35)
    tag_overlap = len(set(all_tags)) / max(len(all_tags), 1)
    ttp_score = min(15, tag_overlap * 15)

    total = infra_score + capability_score + ttp_score
    if total >= 70:
        confidence = "HIGH"
    elif total >= 45:
        confidence = "MEDIUM"
    else:
        confidence = "LOW"

    return {
        "confidence": confidence,
        "score": round(total, 1),
        "shared_infrastructure": {"ips": shared_ips, "domains": shared_domains},
        "shared_capabilities": {"hashes": shared_hashes},
        "events_analyzed": num_events,
    }


def extract_campaign_iocs(events):
    """Extract shared IOCs across correlated events for blocking."""
    ioc_events = {}
    for event in events:
        ev = event.get("Event", event)
        eid = ev.get("id", "unknown")
        for attr in ev.get("Attribute", []):
            val = attr.get("value", "")
            atype = attr.get("type", "")
            key = f"{atype}:{val}"
            if key not in ioc_events:
                ioc_events[key] = []
            ioc_events[key].append(eid)

    shared = {k: v for k, v in ioc_events.items() if len(v) > 1}
    return {
        "total_unique_iocs": len(ioc_events),
        "shared_iocs": len(shared),
        "shared_indicators": [
            {"type": k.split(":")[0], "value": ":".join(k.split(":")[1:]), "event_count": len(v)}
            for k, v in sorted(shared.items(), key=lambda x: len(x[1]), reverse=True)
        ][:50],
    }


def build_campaign_report(campaign_name, events, attribution=None):
    """Build a structured campaign intelligence report."""
    confidence = calculate_campaign_confidence(events)
    iocs = extract_campaign_iocs(events)

    dates = []
    targets = []
    for event in events:
        ev = event.get("Event", event)
        if ev.get("date"):
            dates.append(ev["date"])
        info = ev.get("info", "")
        if info:
            targets.append(info)

    return {
        "campaign_name": campaign_name,
        "report_date": datetime.utcnow().isoformat() + "Z",
        "timeline": {"first_seen": min(dates) if dates else None, "last_seen": max(dates) if dates else None},
        "attribution": attribution or "Unattributed",
        "confidence": confidence,
        "shared_indicators": iocs,
        "events_correlated": len(events),
        "target_summary": targets[:10],
    }


if __name__ == "__main__":
    import os
    misp_url = os.environ.get("MISP_URL", "https://misp.example.com")
    misp_key = os.environ.get("MISP_KEY", "")

    action = sys.argv[1] if len(sys.argv) > 1 else "help"
    if action == "search" and len(sys.argv) > 3:
        client = MISPClient(misp_url, misp_key)
        result = client.search_attributes(sys.argv[2], sys.argv[3])
        print(json.dumps(result, indent=2, default=str))
    elif action == "correlations" and len(sys.argv) > 2:
        client = MISPClient(misp_url, misp_key)
        result = client.get_correlations(sys.argv[2])
        print(json.dumps(result, indent=2, default=str))
    elif action == "events":
        client = MISPClient(misp_url, misp_key)
        tags = sys.argv[2] if len(sys.argv) > 2 else None
        result = client.search_events(tags=tags)
        print(json.dumps(result, indent=2, default=str))
    else:
        print("Usage: agent.py [search <type> <value>|correlations <event_id>|events [tag]]")
        print("Env: MISP_URL, MISP_KEY")
Keep exploring