Skip to the content.

How-to: Create Intelligence Reports

Produce structured, lifecycle-managed intelligence products using gnat.reporting — with STIX 2.1 export and AI-assisted drafting.

Note: gnat.reporting manages analyst intelligence products (finished intelligence, incident reports, threat actor profiles). For operational PDF/HTML/DOCX generation (daily briefs, scheduled dashboards) see How-to: Generate Reports.


Setup

ReportStore uses SQLAlchemy and is included in the [analysis] or [reporting] extras:

pip install "gnat[reporting]"   # or gnat[all]
import os
from gnat.reporting import Report, ReportService, ReportStore, ReportType, ReportStatus

store   = ReportStore("sqlite:///" + os.path.expanduser("~/.gnat/gnat.db"))
store.create_all()                # zero-migration schema init
service = ReportService(store)

Create a report

from gnat.analysis.tlp import TLPLevel

report = service.create(
    title       = "BLACKCAT Ransomware — April 2026",
    report_type = ReportType.INCIDENT_REPORT,
    authors     = ["analyst@example.com"],
    tlp         = TLPLevel.AMBER,
    tags        = ["ransomware", "blackcat", "healthcare"],
)

print(report.id)       # UUID
print(report.status)   # ReportStatus.DRAFT
print(report.version)  # 1

Available report types: INCIDENT_REPORT, THREAT_ACTOR_PROFILE, CAMPAIGN_ANALYSIS, DAILY_BRIEF, VULNERABILITY_ADVISORY, FINISHED_INTELLIGENCE.


Add findings and sections

from gnat.reporting import EvidenceLinkType

# Add key findings (each gets a confidence score)
service.add_finding(
    report.id,
    statement  = "Threat actor reused C2 infrastructure from the March 2026 campaign.",
    confidence = ConfidenceScore.high(rationale="Confirmed by two independent analysts."),
)

service.add_finding(
    report.id,
    statement  = "Initial access vector was spear-phishing with malicious ISO attachment.",
)

# Link evidence to a finding
finding = service.get(report.id).key_findings[0]
service.add_evidence_link(
    report.id,
    finding_id    = finding.id,
    artifact_ref  = "indicator--abc123",
    link_type     = EvidenceLinkType.SUPPORTS,
    description   = "C2 IP observed in March campaign network log.",
)

# Add report body sections
service.add_section(
    report.id,
    title   = "Executive Summary",
    content = "This report details a BLACKCAT ransomware intrusion...",
    order   = 1,
)
service.add_section(
    report.id,
    title   = "Technical Analysis",
    content = "Initial access was achieved via spear-phishing...",
    order   = 2,
)

Add attribution

service.add_attribution(
    report.id,
    threat_actor_name  = "BLACKCAT / ALPHV",
    threat_actor_ref   = "threat-actor--alphv-id",
    confidence         = ConfidenceScore.medium(),
    mitre_attack_ids   = ["T1566.001", "T1486"],
    notes              = "Attribution based on tooling overlap with previous campaign.",
)

Lifecycle transitions

# DRAFT → REVIEW
service.submit_for_review(report.id)

# REVIEW → DRAFT (rejection)
service.reject(report.id, reviewer="manager@example.com",
               reason="Missing technical IOC section.")

# REVIEW → APPROVED
service.approve(report.id, reviewer="manager@example.com")

# APPROVED → PUBLISHED (generates STIX bundle automatically)
published = service.publish(report.id, changed_by="manager@example.com")

print(published.status)           # ReportStatus.PUBLISHED
print(published.stix_report_ref)  # "report--<uuid>" — the STIX SDO ID

Lifecycle state machine:

DRAFT ──► REVIEW ──► APPROVED ──► PUBLISHED
  ▲           │                        │
  └───────────┘                        ▼
  (reject → DRAFT)                 ARCHIVED

Published reports are immutable — content fields cannot be modified after publication.


Create a revision from a published report

# Produces a new DRAFT with parent_report_id set and version incremented
revision = service.create_revision(published.id, changed_by="analyst@example.com")
print(revision.version)         # 2
print(revision.parent_report_id) # original report ID

Export to STIX bundle

publish() triggers STIX generation automatically. You can also call the export function directly:

from gnat.reporting import report_to_stix_bundle

bundle = report_to_stix_bundle(published)
print(bundle)   # {"type": "bundle", "id": "bundle--...", "objects": [...]}
# Objects: report SDO + identity SDO + threat-actor SDO (if attribution set)
#          + attributed-to Relationship SRO; all objects carry TLP marking refs

AI-assisted drafting

Generate a draft executive summary and key-findings narrative before submitting for review:

from gnat.agents.llm import LLMClient
from gnat.analysis.copilot.drafting import ReportDraftingAssistant

assistant = ReportDraftingAssistant(llm_client=LLMClient.from_ini())
result    = assistant.draft_full(report)

print(result.executive_summary)
print(result.key_findings_narrative)

# Apply after analyst review
service.update_summary(report.id, result.executive_summary)

List and query reports

# All reports
all_reports = service.list()

# By status
drafts    = service.list(status=ReportStatus.DRAFT)
published = service.list(status=ReportStatus.PUBLISHED)

# By type
incidents = service.list(report_type=ReportType.INCIDENT_REPORT)

# Get a single report
report = service.get(report_id)

# Delete (only DRAFT and ARCHIVED)
service.delete(draft_report.id)

Archive a report

service.archive(report.id, changed_by="manager@example.com")

See Also


Licensed under the Apache License, Version 2.0