Featured image of post Automated Threat Intelligence Report Generator

Automated Threat Intelligence Report Generator

A pipeline that pulls IOCs, TTPs, and threat actor profiles from MISP, VirusTotal, and open sources, then automatically generates structured HTML and PDF threat intelligence reports.

Overview

Threat intelligence reports take hours to write manually — pulling data from multiple sources, formatting IOC tables, mapping TTPs to ATT&CK, writing the narrative. This pipeline automates all of it.

Given a threat actor name, a campaign tag, or a time window, the generator:

  1. Pulls all relevant events and attributes from MISP
  2. Enriches IOCs with VirusTotal, Shodan, and WHOIS
  3. Maps observed behaviours to MITRE ATT&CK
  4. Renders a structured HTML/PDF report using a Jinja2 template
  5. Distributes the report via email and Teams

Reports are produced in two formats:

  • Technical report — full IOC tables, raw ATT&CK mappings, analyst-facing
  • Executive summary — threat landscape, business impact, recommended actions (no raw data)

Architecture

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Data Sources
  MISP Events ──────┐
  VirusTotal API ───┤
  Shodan API ───────┼──► Collector → Enricher → ATT&CK Mapper
  AlienVault OTX ───┤                                │
  CISA Advisories ──┘                                ▼
                                              Report Generator
                                           (Jinja2 HTML template)
                                          ┌──────────┼──────────┐
                                          ▼          ▼          ▼
                                       HTML PDF  Executive   MISP
                                      Report   Summary     Event
                                    Email / Teams

Tech Stack

Component Tool
Language Python 3.11
Threat Intel PyMISP, OTXv2, VirusTotal API v3
ATT&CK Mapping mitreattack-python library
Templating Jinja2
PDF Rendering weasyprint
Distribution SMTP, Teams Webhook

Replication Guide

Step 1 — Install dependencies

1
pip install pymisp OTXv2 vt-py mitreattack-python jinja2 weasyprint requests

Step 2 — Collect data from MISP

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from pymisp import PyMISP

def collect_misp_data(misp_url, misp_key, tags=None, actor=None, days_back=30):
    misp = PyMISP(misp_url, misp_key)

    search_params = {
        'publish_timestamp': f'{days_back}d',
        'to_ids': True,
        'pythonify': True
    }
    if tags:
        search_params['tags'] = tags
    if actor:
        search_params['value'] = actor

    events = misp.search('events', **search_params)

    iocs = {'ip': [], 'domain': [], 'url': [], 'hash': [], 'email': []}
    ttps = []
    references = []

    for event in events:
        # Extract IOCs
        for attr in event.attributes:
            if attr.type in ['ip-dst', 'ip-src']:
                iocs['ip'].append({'value': attr.value, 'comment': attr.comment, 'event': event.info})
            elif attr.type == 'domain':
                iocs['domain'].append({'value': attr.value, 'comment': attr.comment})
            elif attr.type in ['url', 'link']:
                iocs['url'].append({'value': attr.value})
            elif attr.type in ['md5', 'sha1', 'sha256']:
                iocs['hash'].append({'type': attr.type, 'value': attr.value, 'comment': attr.comment})
            elif attr.type in ['email-src', 'email-dst']:
                iocs['email'].append({'value': attr.value})

        # Extract ATT&CK tags
        for tag in event.tags:
            if 'mitre-attack' in tag.name.lower() or 'attack.t' in tag.name.lower():
                ttps.append(tag.name)

        # Extract references
        for obj in event.objects:
            if obj.name == 'link':
                for a in obj.attributes:
                    if a.type == 'url':
                        references.append(a.value)

    return iocs, list(set(ttps)), references

Step 3 — Enrich IOCs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import vt
import time

def enrich_iocs(iocs, vt_api_key):
    enriched = {'ip': [], 'domain': [], 'hash': []}
    client = vt.Client(vt_api_key)

    for ip in iocs['ip'][:50]:  # Respect rate limits
        try:
            obj = client.get_object(f"/ip_addresses/{ip['value']}")
            stats = obj.last_analysis_stats
            enriched['ip'].append({
                **ip,
                'vt_malicious': stats.get('malicious', 0),
                'vt_total': sum(stats.values()),
                'country': getattr(obj, 'country', 'Unknown'),
                'asn': getattr(obj, 'asn', 'Unknown'),
                'as_owner': getattr(obj, 'as_owner', 'Unknown'),
            })
        except Exception:
            enriched['ip'].append({**ip, 'vt_malicious': 0, 'vt_total': 0})
        time.sleep(0.5)

    for h in iocs['hash'][:50]:
        try:
            obj = client.get_object(f"/files/{h['value']}")
            stats = obj.last_analysis_stats
            enriched['hash'].append({
                **h,
                'vt_malicious': stats.get('malicious', 0),
                'vt_total': sum(stats.values()),
                'file_type': getattr(obj, 'type_description', 'Unknown'),
                'file_name': getattr(obj, 'meaningful_name', h['value'][:16] + '...'),
            })
        except Exception:
            enriched['hash'].append({**h, 'vt_malicious': 0, 'vt_total': 0})
        time.sleep(0.5)

    client.close()
    return enriched

Step 4 — Map TTPs to ATT&CK

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from mitreattack.stix20 import MitreAttackData

def map_ttps_to_attack(ttp_tags):
    """
    Convert MISP ATT&CK tags like 'mitre-attack:attack-pattern="T1059.001"'
    to full technique names and descriptions.
    """
    mitre = MitreAttackData('enterprise-attack.json')
    mapped = []

    for tag in ttp_tags:
        # Extract technique ID from tag
        import re
        match = re.search(r'T\d{4}(?:\.\d{3})?', tag)
        if not match:
            continue
        tech_id = match.group()

        technique = mitre.get_technique_by_id(tech_id)
        if technique:
            tactic = technique.get('kill_chain_phases', [{}])[0].get('phase_name', 'Unknown')
            mapped.append({
                'id': tech_id,
                'name': technique['name'],
                'tactic': tactic.replace('-', ' ').title(),
                'url': f"https://attack.mitre.org/techniques/{tech_id.replace('.', '/')}",
            })

    # Deduplicate
    seen = set()
    unique = []
    for t in mapped:
        if t['id'] not in seen:
            seen.add(t['id'])
            unique.append(t)

    return sorted(unique, key=lambda x: x['tactic'])

Step 5 — Render the HTML report

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
from jinja2 import Environment, FileSystemLoader
import datetime

TECHNICAL_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<title>Threat Intelligence Report — {{ title }}</title>
<style>
  body { font-family: Arial, sans-serif; max-width: 1000px; margin: 40px auto; color: #333; }
  h1 { color: #c0392b; border-bottom: 2px solid #c0392b; }
  h2 { color: #2c3e50; }
  table { width: 100%; border-collapse: collapse; margin: 20px 0; }
  th { background: #2c3e50; color: white; padding: 8px 12px; text-align: left; }
  td { padding: 8px 12px; border-bottom: 1px solid #ddd; font-size: 0.85em; font-family: monospace; }
  tr:nth-child(even) { background: #f9f9f9; }
  .critical { color: #c0392b; font-weight: bold; }
  .tlp-amber { background: #f39c12; color: white; padding: 4px 10px; border-radius: 4px; }
  .tlp-red { background: #c0392b; color: white; padding: 4px 10px; border-radius: 4px; }
</style>
</head>
<body>

<h1>Threat Intelligence Report</h1>
<p><strong>Title:</strong> {{ title }}</p>
<p><strong>Generated:</strong> {{ generated_at }}</p>
<p><strong>Period:</strong> Last {{ days_back }} days</p>
<p><span class="tlp-amber">TLP:AMBER</span> — For authorised recipients only</p>

<h2>Executive Summary</h2>
<p>{{ executive_summary }}</p>

<h2>MITRE ATT&CK Techniques Observed</h2>
<table>
  <tr><th>ID</th><th>Technique</th><th>Tactic</th></tr>
  {% for t in ttps %}
  <tr>
    <td><a href="{{ t.url }}">{{ t.id }}</a></td>
    <td>{{ t.name }}</td>
    <td>{{ t.tactic }}</td>
  </tr>
  {% endfor %}
</table>

<h2>Indicators of Compromise</h2>

<h3>IP Addresses ({{ iocs.ip | length }})</h3>
<table>
  <tr><th>IP</th><th>ASN / Owner</th><th>Country</th><th>VT Score</th><th>Context</th></tr>
  {% for ip in iocs.ip %}
  <tr>
    <td>{{ ip.value }}</td>
    <td>{{ ip.as_owner | default('Unknown') }}</td>
    <td>{{ ip.country | default('Unknown') }}</td>
    <td class="{{ 'critical' if ip.vt_malicious > 5 else '' }}">
      {{ ip.vt_malicious }}/{{ ip.vt_total }}
    </td>
    <td>{{ ip.comment | default('') }}</td>
  </tr>
  {% endfor %}
</table>

<h3>File Hashes ({{ iocs.hash | length }})</h3>
<table>
  <tr><th>Type</th><th>Hash</th><th>File</th><th>VT Score</th></tr>
  {% for h in iocs.hash %}
  <tr>
    <td>{{ h.type | upper }}</td>
    <td>{{ h.value }}</td>
    <td>{{ h.file_name | default('') }}</td>
    <td class="{{ 'critical' if h.vt_malicious > 5 else '' }}">
      {{ h.vt_malicious }}/{{ h.vt_total }}
    </td>
  </tr>
  {% endfor %}
</table>

<h3>Domains ({{ iocs.domain | length }})</h3>
<table>
  <tr><th>Domain</th><th>Context</th></tr>
  {% for d in iocs.domain %}
  <tr><td>{{ d.value }}</td><td>{{ d.comment | default('') }}</td></tr>
  {% endfor %}
</table>

<h2>References</h2>
<ul>
  {% for ref in references %}
  <li><a href="{{ ref }}">{{ ref }}</a></li>
  {% endfor %}
</ul>

<hr>
<p style="font-size:0.8em;color:#999;">
  Generated automatically by the CTI Report Pipeline.
  Contact: contact@malsayegh.ae
</p>
</body>
</html>
"""

def render_report(title, iocs, ttps, references, days_back=30):
    template = Environment().from_string(TECHNICAL_TEMPLATE)
    return template.render(
        title=title,
        generated_at=datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M UTC'),
        days_back=days_back,
        iocs=iocs,
        ttps=ttps,
        references=references,
        executive_summary=(
            f"During the past {days_back} days, {len(iocs['ip'])} malicious IP addresses, "
            f"{len(iocs['domain'])} domains, and {len(iocs['hash'])} file hashes were observed. "
            f"Activity maps to {len(ttps)} ATT&CK techniques across "
            f"{len(set(t['tactic'] for t in ttps))} tactics."
        )
    )

Step 6 — Export to PDF

1
2
3
4
5
from weasyprint import HTML

def export_pdf(html_content, output_path):
    HTML(string=html_content).write_pdf(output_path)
    return output_path

Step 7 — Distribute

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email import encoders

def distribute_report(pdf_path, recipients, smtp_host, smtp_user, smtp_pass, title):
    msg = MIMEMultipart()
    msg['From']    = smtp_user
    msg['To']      = ', '.join(recipients)
    msg['Subject'] = f"[TLP:AMBER] Threat Intelligence Report — {title}"

    msg.attach(MIMEText(
        f"Please find the attached threat intelligence report for: {title}\n\n"
        f"This report is classified TLP:AMBER — do not share outside authorised recipients.",
        'plain'
    ))

    with open(pdf_path, 'rb') as f:
        part = MIMEBase('application', 'octet-stream')
        part.set_payload(f.read())
        encoders.encode_base64(part)
        part.add_header('Content-Disposition', f'attachment; filename="{title}_CTI_Report.pdf"')
        msg.attach(part)

    with smtplib.SMTP_SSL(smtp_host, 465) as server:
        server.login(smtp_user, smtp_pass)
        server.sendmail(smtp_user, recipients, msg.as_string())

Schedule

1
2
3
4
5
# Weekly threat report — every Sunday at 06:00
0 6 * * 0 /usr/bin/python3 /opt/cti-reports/main.py \
  --days 7 \
  --output /reports/weekly_$(date +\%Y-\%V).pdf \
  --recipients cti-team@company.ae,ciso@company.ae

Contact me at contact@malsayegh.ae to discuss building a CTI reporting pipeline for your team.

comments powered by Disqus
All rights Reserved for malsayegh.ae
Built with Hugo
Theme Stack designed by Jimmy