<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: David Moya</title>
    <description>The latest articles on DEV Community by David Moya (@david_moya_5b6cdd8b4e1219).</description>
    <link>https://dev.to/david_moya_5b6cdd8b4e1219</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4032193%2F6c1dfc67-6557-42b7-99a6-b04d1e784fcc.png</url>
      <title>DEV Community: David Moya</title>
      <link>https://dev.to/david_moya_5b6cdd8b4e1219</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/david_moya_5b6cdd8b4e1219"/>
    <language>en</language>
    <item>
      <title>ELF 101: Linux Executable Format for Malware Analysts</title>
      <dc:creator>David Moya</dc:creator>
      <pubDate>Tue, 01 Sep 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/david_moya_5b6cdd8b4e1219/elf-101-linux-executable-format-for-malware-analysts-59nm</link>
      <guid>https://dev.to/david_moya_5b6cdd8b4e1219/elf-101-linux-executable-format-for-malware-analysts-59nm</guid>
      <description>&lt;p&gt;If you come from Windows malware analysis, PE is second nature. On Linux, the equivalent is &lt;strong&gt;ELF&lt;/strong&gt; (Executable and Linkable Format).&lt;/p&gt;

&lt;p&gt;With Linux malware surging (ESXi ransomware, IoT botnets, cloud cryptominers), analysts need ELF fluency at the same level as PE. This guide covers the structure, tools, and indicators that separate a legitimate ELF binary from a malicious one.&lt;/p&gt;

&lt;h2&gt;
  
  
  ELF Structure at a Glance
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Offset 0x0000
┌─────────────────────────┐
│      ELF Header         │  52/64 bytes (32/64-bit)
│  Magic: 7f 45 4c 46     │  "\x7fELF"
├─────────────────────────┤
│  Program Header Table   │  Segments (for loading)
│  (PHT)                  │  Used by the kernel loader
├─────────────────────────┤
│                         │
│       Sections          │  .text, .data, .rodata,
│   (actual content)      │  .dynsym, .got, .plt ...
│                         │
├─────────────────────────┤
│  Section Header Table   │  Sections (for linking)
│  (SHT)                  │  Used by linker/debugger
└─────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key difference vs PE:&lt;/strong&gt; ELF has two views of the same content:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Segments&lt;/strong&gt; (Program Headers): how the kernel loads the binary into memory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sections&lt;/strong&gt; (Section Headers): how the linker organizes content&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A binary needs segments to execute but does &lt;strong&gt;not&lt;/strong&gt; need sections. Malware often strips the Section Header Table to hinder analysis, and the binary still runs because the kernel only uses Program Headers.&lt;/p&gt;

&lt;h2&gt;
  
  
  ELF Header: What to Look For
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Offset&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;e_ident[EI_MAG]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0x00&lt;/td&gt;
&lt;td&gt;Magic: &lt;code&gt;\x7fELF&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;e_ident[EI_CLASS]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0x04&lt;/td&gt;
&lt;td&gt;1=32-bit, 2=64-bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;e_type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0x10&lt;/td&gt;
&lt;td&gt;2=Executable, 3=Shared Object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;e_machine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0x12&lt;/td&gt;
&lt;td&gt;0x03=x86, 0x3E=x86-64, 0x28=ARM, 0xB7=AArch64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;e_entry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0x18&lt;/td&gt;
&lt;td&gt;Entry point address&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;e_shoff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0x20&lt;/td&gt;
&lt;td&gt;Section Header Table offset (0 if stripped)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;e_shnum&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0x3C&lt;/td&gt;
&lt;td&gt;Number of Section Headers (0 if stripped)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;readelf &lt;span class="nt"&gt;-h&lt;/span&gt; malware_sample

ELF Header:
  Class:                  ELF64
  Type:                   EXEC &lt;span class="o"&gt;(&lt;/span&gt;Executable file&lt;span class="o"&gt;)&lt;/span&gt;
  Machine:                Advanced Micro Devices X86-64
  Entry point address:    0x400a30
  Start of section headers: 0 &lt;span class="o"&gt;(&lt;/span&gt;bytes into file&lt;span class="o"&gt;)&lt;/span&gt;  ← STRIPPED!
  Number of section headers: 0                    ← STRIPPED!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Malware Indicators in the ELF Header
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Indicator&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;e_shoff = 0&lt;/code&gt;, &lt;code&gt;e_shnum = 0&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Section headers stripped. Common in malware to block analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;e_machine = ARM/MIPS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Possible IoT malware (Mirai variants)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;e_type = ET_DYN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PIE executable, not suspicious per se&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entry point at unusual address&lt;/td&gt;
&lt;td&gt;Possible packing or manipulation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Sections That Matter for Malware Analysis
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.text&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Executable code&lt;/td&gt;
&lt;td&gt;The malware's main logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.rodata&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Read-only data&lt;/td&gt;
&lt;td&gt;Strings: C2 URLs, messages, keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Initialized globals&lt;/td&gt;
&lt;td&gt;Config, encryption keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.dynsym&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dynamic symbol table&lt;/td&gt;
&lt;td&gt;Imported/exported functions (like IAT/EAT)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.got&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Global Offset Table&lt;/td&gt;
&lt;td&gt;Function pointers (hooking target)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.plt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Procedure Linkage Table&lt;/td&gt;
&lt;td&gt;Dynamic call stubs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.init_array&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Constructor functions&lt;/td&gt;
&lt;td&gt;Code that runs &lt;strong&gt;before&lt;/strong&gt; main()&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;.init_array&lt;/code&gt; is particularly interesting for malware: code here executes before &lt;code&gt;main()&lt;/code&gt;, making it a common persistence and anti-analysis point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Essential Tools
&lt;/h2&gt;

&lt;h3&gt;
  
  
  readelf: Header and section inspection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Full headers&lt;/span&gt;
readelf &lt;span class="nt"&gt;-a&lt;/span&gt; sample

&lt;span class="c"&gt;# Just program headers (segments)&lt;/span&gt;
readelf &lt;span class="nt"&gt;-l&lt;/span&gt; sample

&lt;span class="c"&gt;# Dynamic symbols (imports)&lt;/span&gt;
readelf &lt;span class="nt"&gt;--dyn-syms&lt;/span&gt; sample
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  objdump: Disassembly
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Disassemble .text section&lt;/span&gt;
objdump &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-M&lt;/span&gt; intel sample

&lt;span class="c"&gt;# All sections with content&lt;/span&gt;
objdump &lt;span class="nt"&gt;-s&lt;/span&gt; sample
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  strings + FLOSS: String extraction
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Basic strings&lt;/span&gt;
strings &lt;span class="nt"&gt;-a&lt;/span&gt; sample | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'http|\.onion|/bin/|socket|connect'&lt;/span&gt;

&lt;span class="c"&gt;# FLOSS for obfuscated strings&lt;/span&gt;
floss sample
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  file + checksec: Quick triage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;file sample
sample: ELF 64-bit LSB executable, x86-64, statically linked, stripped

&lt;span class="nv"&gt;$ &lt;/span&gt;checksec &lt;span class="nt"&gt;--file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sample
RELRO    STACK CANARY  NX       PIE
No RELRO No canary     NX disabled  No PIE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No RELRO + No canary + NX disabled + No PIE = likely malware. Legitimate software enables these protections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static vs Dynamic Linking: Why It Matters
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Indicator&lt;/th&gt;
&lt;th&gt;Malware implication&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dynamically linked&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Has &lt;code&gt;.dynsym&lt;/code&gt;, &lt;code&gt;.plt&lt;/code&gt;, &lt;code&gt;.got&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Imports visible, easier to analyze&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Statically linked&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;file&lt;/code&gt; says "statically linked"&lt;/td&gt;
&lt;td&gt;All library code embedded, harder to identify functions, common in Go/Rust malware&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Go and Rust malware is almost always statically linked, producing large binaries (2-10 MB) with thousands of functions. Tools like &lt;code&gt;GoReSym&lt;/code&gt; or &lt;code&gt;redress&lt;/code&gt; help recover function names in stripped Go binaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Red Flags Checklist
&lt;/h2&gt;

&lt;p&gt;When triaging an ELF binary, check for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Stripped sections (&lt;code&gt;e_shnum = 0&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] UPX or custom packing (&lt;code&gt;strings sample | grep UPX&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] Anti-debug: &lt;code&gt;ptrace(PTRACE_TRACEME)&lt;/code&gt; in imports&lt;/li&gt;
&lt;li&gt;[ ] Network functions: &lt;code&gt;socket&lt;/code&gt;, &lt;code&gt;connect&lt;/code&gt;, &lt;code&gt;send&lt;/code&gt;, &lt;code&gt;recv&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Process manipulation: &lt;code&gt;fork&lt;/code&gt;, &lt;code&gt;execve&lt;/code&gt;, &lt;code&gt;prctl&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] File operations in &lt;code&gt;/proc/&lt;/code&gt;, &lt;code&gt;/etc/cron&lt;/code&gt;, &lt;code&gt;/tmp/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Self-deletion: &lt;code&gt;unlink&lt;/code&gt; on own path&lt;/li&gt;
&lt;li&gt;[ ] Crypto imports: functions from OpenSSL, mbedTLS&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;.init_array&lt;/code&gt; with suspicious entries&lt;/li&gt;
&lt;li&gt;[ ] Abnormally large &lt;code&gt;.rodata&lt;/code&gt; (embedded configs/payloads)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;This is Part 1 of our &lt;a href="https://malwareintel.es/blog/malware-linux/" rel="noopener noreferrer"&gt;Linux Malware series&lt;/a&gt;. Coming up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Part 2&lt;/strong&gt;: ELF Packers and Obfuscation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 3&lt;/strong&gt;: Linux Rootkits (Diamorphine, Reptile, BPFDoor)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 4&lt;/strong&gt;: Forensics on Linux Endpoints&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://malwareintel.es/blog/malware-linux/elf-101-ejecutables-linux/" rel="noopener noreferrer"&gt;MalwareIntel&lt;/a&gt;. MalwareIntel is a free threat intelligence platform monitoring 13 public CTI sources.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free resources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://malwareintel.es/defense-kit/" rel="noopener noreferrer"&gt;Defense Kit&lt;/a&gt; — Sigma + YARA + IOCs per malware family&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://malwareintel.es/tools/detection-gap/" rel="noopener noreferrer"&gt;Detection Gap Analyzer&lt;/a&gt; — Check your MITRE ATT&amp;amp;CK coverage&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://malwareintel.es/assessment/" rel="noopener noreferrer"&gt;CTI Assessment&lt;/a&gt; — Rate your threat intel maturity in 5 min&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>analysis</category>
      <category>cybersecurity</category>
      <category>linux</category>
      <category>security</category>
    </item>
    <item>
      <title>AI-Powered Cyber Attacks Are Here: What Your SOC Needs to Change Now</title>
      <dc:creator>David Moya</dc:creator>
      <pubDate>Tue, 18 Aug 2026 10:53:11 +0000</pubDate>
      <link>https://dev.to/david_moya_5b6cdd8b4e1219/ai-powered-cyber-attacks-are-here-what-your-soc-needs-to-change-now-3c15</link>
      <guid>https://dev.to/david_moya_5b6cdd8b4e1219/ai-powered-cyber-attacks-are-here-what-your-soc-needs-to-change-now-3c15</guid>
      <description>&lt;h2&gt;
  
  
  The attack surface just changed
&lt;/h2&gt;

&lt;p&gt;In August 2026, Taiwan confirmed that government agencies were targeted by attacks combining human operators with AI agents. Researchers documented an environment where up to eight sub-agents worked in parallel: mapping systems, extracting configurations, scoring vulnerabilities, and adjusting tactics across multiple intrusion waves.&lt;/p&gt;

&lt;p&gt;This is not a theoretical exercise. The architecture showed agents capable of investigating alternatives when a technique was blocked and reorganizing offensive effort without constant human intervention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for every SOC
&lt;/h2&gt;

&lt;p&gt;Most incident response plans are designed around a sequential attacker. One human, one thread, limited parallelism. AI agents break that assumption.&lt;/p&gt;

&lt;p&gt;Consider what changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reconnaissance&lt;/strong&gt; happens across dozens of endpoints simultaneously&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vulnerability analysis&lt;/strong&gt; is scored and prioritized automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credential harvesting&lt;/strong&gt; feeds results back into the next attack wave&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lateral movement&lt;/strong&gt; paths are evaluated in parallel, not one at a time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The human operator still sets objectives and makes critical decisions, but the operational capacity multiplied.&lt;/p&gt;

&lt;h2&gt;
  
  
  The response time compression problem
&lt;/h2&gt;

&lt;p&gt;For a CISO, the real question is: how much did the attack timeline compress?&lt;/p&gt;

&lt;p&gt;If your SOC detects a compromised identity with high confidence but needs three approvals before blocking it, while parallel agents are testing access paths across your infrastructure, that administrative delay becomes part of your attack surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four things to review today
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Detection-to-containment time
&lt;/h3&gt;

&lt;p&gt;Measure the actual time between a high-confidence detection and the first effective containment action. Separate technical limitations from governance friction.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Automate low-risk defensive actions
&lt;/h3&gt;

&lt;p&gt;Not everything needs human approval. Isolating a workstation, revoking a session token, blocking a known-bad IP, these can be automated with clear boundaries and rollback mechanisms.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Elevate observability
&lt;/h3&gt;

&lt;p&gt;APIs, SSO tokens, service accounts, exposed applications. These were the surfaces documented in the Taiwan campaign. If you cannot see lateral movement across these, you cannot respond to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Train for parallel pressure
&lt;/h3&gt;

&lt;p&gt;Your tabletop exercises probably simulate one attack thread. Start simulating multiple simultaneous signals, because a multi-agent adversary creates pressure on several points at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The detection stack that helps
&lt;/h2&gt;

&lt;p&gt;Platforms that combine IOCs with TTPs mapped to MITRE ATT&amp;amp;CK give your SOC behavioral detection, not just artifact matching. When an attacker changes tools, your rules based on technique patterns still fire.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://malwareintel.es" rel="noopener noreferrer"&gt;MalwareIntel&lt;/a&gt;, we aggregate 1.65M+ IOCs from 108 public feeds, map 4,293 malware families to MITRE ATT&amp;amp;CK, and provide Defense Kits with Sigma + YARA rules ready to import into any SIEM.&lt;/p&gt;

&lt;p&gt;The free tier includes IOC search, Knowledge Graph, and 10 queries per day. No credit card required.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;AI-powered attacks are not replacing human operators. They are amplifying them. The question for your organization is whether your detection, escalation, and containment processes are still designed for pre-AI attack speeds.&lt;/p&gt;

&lt;p&gt;The organizations that will fare best are those that automated their defensive responses before they needed to.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is based on publicly available research and official government statements. It does not constitute attribution of specific campaigns to specific actors.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;About MalwareIntel&lt;/strong&gt;: Free Cyber Threat Intelligence platform. 4,293 malware families, 1.65M+ IOCs, MITRE ATT&amp;amp;CK mapping, Sigma/YARA rules. &lt;a href="https://malwareintel.es" rel="noopener noreferrer"&gt;malwareintel.es&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>cybersecurity</category>
      <category>ai</category>
      <category>soc</category>
    </item>
    <item>
      <title>Your AI agent says 'done' but nothing works: an open-source framework to fix it</title>
      <dc:creator>David Moya</dc:creator>
      <pubDate>Fri, 14 Aug 2026 16:44:55 +0000</pubDate>
      <link>https://dev.to/david_moya_5b6cdd8b4e1219/your-ai-agent-says-done-but-nothing-works-an-open-source-framework-to-fix-it-3joc</link>
      <guid>https://dev.to/david_moya_5b6cdd8b4e1219/your-ai-agent-says-done-but-nothing-works-an-open-source-framework-to-fix-it-3joc</guid>
      <description>&lt;p&gt;I've been working with AI agents in production (Claude Code, Codex, Cursor) for over a year. The pattern is always the same:1. You ask the agent for something2. It says "done"3. It doesn't work4. You switch models5. Still doesn't workThe problem isn't the model. It's the environment.## The gapThere's a huge gap between "demo that works" and "system that produces consistent results." Models are capable, but they fail because of:- &lt;strong&gt;Vague specifications&lt;/strong&gt;: the agent can only guess- &lt;strong&gt;No verification&lt;/strong&gt;: says "done" without running tests- &lt;strong&gt;No state&lt;/strong&gt;: every session starts from scratch- &lt;strong&gt;No observability&lt;/strong&gt;: you don't know the cost or if quality drops## The solution: 6 progressive tiers&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEQKICAgIFQxWyJUaWVyIDE6IEhhcm5lc3MgwrcgMTAgbWluIl0gLS0-IFQyWyJUaWVyIDI6IEV2YWwgwrcgMzAgbWluIl0KICAgIFQyIC0tPiBUM1siVGllciAzOiBPYnNlcnZlIMK3IDFoIl0KICAgIFQzIC0tPiBUNFsiVGllciA0OiBIb29rcyDCtyAxNSBtaW4iXQogICAgVDQgLS0-IFQ1WyJUaWVyIDU6IFBhdHRlcm5zIl0KICAgIFQ1IC0tPiBUNlsiVGllciA2OiBDSS9DRCDCtyAxaCJdCiAgICBzdHlsZSBUMSBmaWxsOiNFRjQ0NDQsY29sb3I6I2ZmZgogICAgc3R5bGUgVDIgZmlsbDojZmI5MjNjLGNvbG9yOiNmZmYKICAgIHN0eWxlIFQzIGZpbGw6I2E3OGJmYSxjb2xvcjojZmZmCiAgICBzdHlsZSBUNCBmaWxsOiMyMmQzZWUsY29sb3I6IzFBMTkxNgogICAgc3R5bGUgVDUgZmlsbDojMzRkMzk5LGNvbG9yOiMxQTE5MTYKICAgIHN0eWxlIFQ2IGZpbGw6I2U4NzlmOSxjb2xvcjojZmZm%3FbgColor%3D1A1916" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEQKICAgIFQxWyJUaWVyIDE6IEhhcm5lc3MgwrcgMTAgbWluIl0gLS0-IFQyWyJUaWVyIDI6IEV2YWwgwrcgMzAgbWluIl0KICAgIFQyIC0tPiBUM1siVGllciAzOiBPYnNlcnZlIMK3IDFoIl0KICAgIFQzIC0tPiBUNFsiVGllciA0OiBIb29rcyDCtyAxNSBtaW4iXQogICAgVDQgLS0-IFQ1WyJUaWVyIDU6IFBhdHRlcm5zIl0KICAgIFQ1IC0tPiBUNlsiVGllciA2OiBDSS9DRCDCtyAxaCJdCiAgICBzdHlsZSBUMSBmaWxsOiNFRjQ0NDQsY29sb3I6I2ZmZgogICAgc3R5bGUgVDIgZmlsbDojZmI5MjNjLGNvbG9yOiNmZmYKICAgIHN0eWxlIFQzIGZpbGw6I2E3OGJmYSxjb2xvcjojZmZmCiAgICBzdHlsZSBUNCBmaWxsOiMyMmQzZWUsY29sb3I6IzFBMTkxNgogICAgc3R5bGUgVDUgZmlsbDojMzRkMzk5LGNvbG9yOiMxQTE5MTYKICAgIHN0eWxlIFQ2IGZpbGw6I2U4NzlmOSxjb2xvcjojZmZm%3FbgColor%3D1A1916" alt="6-tier Architecture" width="246" height="590"&gt;&lt;/a&gt;I published &lt;a href="https://github.com/pedri77/ia-engineer-framework" rel="noopener noreferrer"&gt;ia-engineer-framework&lt;/a&gt;, an open-source framework (MIT) that closes this gap with 6 layers:### Tier 1: Harness (10 min)Templates you copy to your project. &lt;code&gt;CLAUDE.md&lt;/code&gt; with instructions, &lt;code&gt;feature_list.json&lt;/code&gt; with machine-readable features, &lt;code&gt;init.sh&lt;/code&gt; to verify the environment.&lt;br&gt;
&lt;br&gt;
&lt;code&gt;bashcp -r ia-engineer-framework/harness/ your-project/# Edit CLAUDE.md with your commands# Edit feature_list.json with your features# Run your agent — it reads the files automatically&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier 2: Eval (30 min)JSONL datasets with scenarios and expected answers. A runner that tests against any LLM. A scorer that measures accuracy. A gate that blocks CI if it drops below the threshold.
&lt;/h3&gt;

&lt;p&gt;&lt;br&gt;
&lt;code&gt;bashpython3 run-evals.py --dataset prompting-basics.jsonl --output results.jsonlpython3 score-evals.py --results results.jsonl# Accuracy: 87.5% — PASS&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier 3: Observe (1h)Logger that records 10 fields per interaction (tokens, cost, latency, tools, errors). Cost tracker that aggregates by session/model/day. Importable Grafana dashboard.### Tier 4: Hooks (15 min)Claude Code automation: pre-commit lint, post-task tests, session cost guard, security scan.### Tier 5: Patterns (reading)8 reference documents: diagnostic loop, multi-session, graph engineering, circuit breaker, HITL, model routing, prompt versioning, agent testing.### Tier 6: CI/CD (1h)GitHub Action that runs evals on every PR. Quality gate that blocks merge if they fail.## Diagnostic Loop: the most useful pattern&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBBW0FnZW50IGZhaWxlZF0gLS0-IEJ7RGlkIHNvbWV0aGluZyBkaWZmZXJlbnQKdGhhbiBleHBlY3RlZD99CiAgICBCIC0tPnxZZXN8IENbTGF5ZXIgMTogU1BFQ0lGSUNBVElPTgpXcml0ZSBEZWZpbml0aW9uIG9mIERvbmVdCiAgICBCIC0tPnxOb3wgRHtXcm9uZyBwYXR0ZXJuL3ZlcnNpb24vCnN0eWxlP30KICAgIEQgLS0-fFllc3wgRVtMYXllciAyOiBDT05URVhUClVwZGF0ZSBDTEFVREUubWRdCiAgICBEIC0tPnxOb3wgRntEZXBlbmRlbmN5L3Rvb2xzCmVycm9yP30KICAgIEYgLS0-fFllc3wgR1tMYXllciAzOiBFTlZJUk9OTUVOVApGaXggaW5pdC5zaF0KICAgIEYgLS0-fE5vfCBIe1NhaWQgZG9uZSBidXQKZG9lc24ndCB3b3JrP30KICAgIEggLS0-fFllc3wgSVtMYXllciA0OiBWRVJJRklDQVRJT04KUmVxdWlyZSB0ZXN0cyArIGV2aWRlbmNlXQogICAgSCAtLT58Tm98IEpbTGF5ZXIgNTogU1RBVEUKcHJvZ3Jlc3MubWQgKyBoYW5kb2ZmXQogICAgc3R5bGUgQyBmaWxsOiNFRjQ0NDQsY29sb3I6I2ZmZgogICAgc3R5bGUgRSBmaWxsOiNmYjkyM2MsY29sb3I6I2ZmZgogICAgc3R5bGUgRyBmaWxsOiNhNzhiZmEsY29sb3I6I2ZmZgogICAgc3R5bGUgSSBmaWxsOiMyMmQzZWUsY29sb3I6IzFBMTkxNgogICAgc3R5bGUgSiBmaWxsOiMzNGQzOTksY29sb3I6IzFBMTkxNg%3D%3D%3FbgColor%3D1A1916" alt="Diagnostic Loop" width="937" height="1359"&gt;When your agent fails, don't switch models. Diagnose the layer:1. &lt;strong&gt;Specification&lt;/strong&gt; — was the task clear?2. &lt;strong&gt;Context&lt;/strong&gt; — did the agent have the info?3. &lt;strong&gt;Environment&lt;/strong&gt; — correct dependencies?4. &lt;strong&gt;Verification&lt;/strong&gt; — were there tests?5. &lt;strong&gt;State&lt;/strong&gt; — did it remember previous work?Fix the layer that failed. Repeat. After 3-5 rounds your environment is robust. Without spending more money.&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fc2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBEZXYgYXMgRGV2ZWxvcGVyCiAgICBwYXJ0aWNpcGFudCBEUyBhcyBEYXRhc2V0IEpTT05MCiAgICBwYXJ0aWNpcGFudCBSdW4gYXMgcnVuLWV2YWxzLnB5CiAgICBwYXJ0aWNpcGFudCBMTE0gYXMgTExNCiAgICBwYXJ0aWNpcGFudCBTY29yZSBhcyBzY29yZS1ldmFscy5weQogICAgcGFydGljaXBhbnQgR2F0ZSBhcyBjaS1nYXRlLnB5CiAgICBEZXYtPj5EUzogQ3JlYXRlIHNjZW5hcmlvcwogICAgRGV2LT4-UnVuOiAtLWRhdGFzZXQgZXZhbC5qc29ubAogICAgbG9vcCBFYWNoIHNjZW5hcmlvCiAgICAgICAgUnVuLT4-TExNOiBTZW5kIGlucHV0CiAgICAgICAgTExNLS0-PlJ1bjogUmVzcG9uc2UKICAgIGVuZAogICAgUnVuLT4-UnVuOiBTYXZlIHJlc3VsdHMuanNvbmwKICAgIERldi0-PlNjb3JlOiAtLXJlc3VsdHMgcmVzdWx0cy5qc29ubAogICAgU2NvcmUtLT4-RGV2OiBBY2N1cmFjeSA4Ny41JQogICAgRGV2LT4-R2F0ZTogLS10aHJlc2hvbGQgMC44CiAgICBHYXRlLS0-PkRldjogUEFTUw%3D%3D%3FbgColor%3D1A1916" alt="Eval Pipeline" width="1250" height="664"&gt;## Comparison with Learn Harness Engineering (11.3K stars)| | LHE | ia-engineer-framework ||---|---|---|| Scope | Harness only | Harness + eval + observe + CI || Eval pipeline | No | Runner + scorer + CI gate || Observability | No | Logger + cost tracker + alerts || CI/CD | No | GitHub Action + quality gate || Format | Course | Copy-paste to production || Lock-in | Specific | Claude Code, Codex, Cursor |## Principles1. &lt;strong&gt;Automation over AI&lt;/strong&gt; — if a test solves it, don't use an LLM2. &lt;strong&gt;Verifiable &amp;gt; smart&lt;/strong&gt; — 8 eval scenarios &amp;gt; 1 "perfect" prompt3. &lt;strong&gt;Copy-paste to production&lt;/strong&gt; — every file works standalone4. &lt;strong&gt;Progressive&lt;/strong&gt; — Tier 1 in 10 min, Tier 6 when you have a team5. &lt;strong&gt;No lock-in&lt;/strong&gt; — Claude Code, Codex, Cursor, anything## Get started in 5 minutes
&lt;/h3&gt;

&lt;p&gt;&lt;br&gt;
&lt;code&gt;bashgit clone https://github.com/pedri77/ia-engineer-framework.gitcp -r ia-engineer-framework/harness/ your-project/&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
Edit &lt;code&gt;CLAUDE.md&lt;/code&gt; and &lt;code&gt;feature_list.json&lt;/code&gt; with your data. Run your agent.&lt;a href="https://github.com/pedri77/ia-engineer-framework" rel="noopener noreferrer"&gt;GitHub: pedri77/ia-engineer-framework&lt;/a&gt;MIT. Use, modify, distribute freely.---&lt;em&gt;This is part of &lt;a href="https://iacademy.es" rel="noopener noreferrer"&gt;IAcademy&lt;/a&gt;, an applied AI academy for real work.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Tu agente IA dice 'listo' pero no funciona: un framework open-source para arreglarlo</title>
      <dc:creator>David Moya</dc:creator>
      <pubDate>Fri, 14 Aug 2026 16:40:07 +0000</pubDate>
      <link>https://dev.to/david_moya_5b6cdd8b4e1219/tu-agente-ia-dice-listo-pero-no-funciona-un-framework-open-source-para-arreglarlo-37op</link>
      <guid>https://dev.to/david_moya_5b6cdd8b4e1219/tu-agente-ia-dice-listo-pero-no-funciona-un-framework-open-source-para-arreglarlo-37op</guid>
      <description>&lt;p&gt;Llevo más de un año trabajando con agentes IA en producción (Claude Code, Codex, Cursor). El patrón siempre es el mismo: 1. Le pides algo al agente 2. Dice "listo" 3. No funciona4. Cambias de modelo5. Sigue sin funcionarEl problema no es el modelo. Es el entorno.## El gapHay un gap enorme entre "demo que funciona" y "sistema que produce resultados consistentes". Los modelos son capaces, pero fallan por:- &lt;strong&gt;Especificaciónes vagas&lt;/strong&gt;: el agente adivina- &lt;strong&gt;Sin verificación&lt;/strong&gt;: dice "listo" sin ejecutar tests- &lt;strong&gt;Sin estado&lt;/strong&gt;: cada sesión empieza de cero- &lt;strong&gt;Sin observabilidad&lt;/strong&gt;: no sabes cuánto cuesta ni si la calidad baja## La solución: 6 tiers progresivos&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEQKICAgIFQxWyJUaWVyIDE6IEhhcm5lc3MgwrcgMTAgbWluIl0gLS0-IFQyWyJUaWVyIDI6IEV2YWwgwrcgMzAgbWluIl0KICAgIFQyIC0tPiBUM1siVGllciAzOiBPYnNlcnZlIMK3IDFoIl0KICAgIFQzIC0tPiBUNFsiVGllciA0OiBIb29rcyDCtyAxNSBtaW4iXQogICAgVDQgLS0-IFQ1WyJUaWVyIDU6IFBhdHRlcm5zIl0KICAgIFQ1IC0tPiBUNlsiVGllciA2OiBDSS9DRCDCtyAxaCJdCiAgICBzdHlsZSBUMSBmaWxsOiNFRjQ0NDQsY29sb3I6I2ZmZgogICAgc3R5bGUgVDIgZmlsbDojZmI5MjNjLGNvbG9yOiNmZmYKICAgIHN0eWxlIFQzIGZpbGw6I2E3OGJmYSxjb2xvcjojZmZmCiAgICBzdHlsZSBUNCBmaWxsOiMyMmQzZWUsY29sb3I6IzFBMTkxNgogICAgc3R5bGUgVDUgZmlsbDojMzRkMzk5LGNvbG9yOiMxQTE5MTYKICAgIHN0eWxlIFQ2IGZpbGw6I2U4NzlmOSxjb2xvcjojZmZm%3FbgColor%3D1A1916" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEQKICAgIFQxWyJUaWVyIDE6IEhhcm5lc3MgwrcgMTAgbWluIl0gLS0-IFQyWyJUaWVyIDI6IEV2YWwgwrcgMzAgbWluIl0KICAgIFQyIC0tPiBUM1siVGllciAzOiBPYnNlcnZlIMK3IDFoIl0KICAgIFQzIC0tPiBUNFsiVGllciA0OiBIb29rcyDCtyAxNSBtaW4iXQogICAgVDQgLS0-IFQ1WyJUaWVyIDU6IFBhdHRlcm5zIl0KICAgIFQ1IC0tPiBUNlsiVGllciA2OiBDSS9DRCDCtyAxaCJdCiAgICBzdHlsZSBUMSBmaWxsOiNFRjQ0NDQsY29sb3I6I2ZmZgogICAgc3R5bGUgVDIgZmlsbDojZmI5MjNjLGNvbG9yOiNmZmYKICAgIHN0eWxlIFQzIGZpbGw6I2E3OGJmYSxjb2xvcjojZmZmCiAgICBzdHlsZSBUNCBmaWxsOiMyMmQzZWUsY29sb3I6IzFBMTkxNgogICAgc3R5bGUgVDUgZmlsbDojMzRkMzk5LGNvbG9yOiMxQTE5MTYKICAgIHN0eWxlIFQ2IGZpbGw6I2U4NzlmOSxjb2xvcjojZmZm%3FbgColor%3D1A1916" alt="Arquitectura 6 tiers" width="246" height="590"&gt;&lt;/a&gt;He publicado &lt;a href="https://github.com/pedri77/ia-engineer-framework" rel="noopener noreferrer"&gt;ia-engineer-framework&lt;/a&gt;, un framework open-source (MIT, español nativo) que cierra este gap con 6 capas:### Tier 1: Harness (10 min)Templates que copias a tu proyecto. &lt;code&gt;CLAUDE.md&lt;/code&gt; con instrucciones, &lt;code&gt;feature_list.json&lt;/code&gt; con features machine-readable, &lt;code&gt;init.sh&lt;/code&gt; para verificar entorno.&lt;br&gt;
&lt;br&gt;
&lt;code&gt;bashcp -r ia-engineer-framework/harness/ tu-proyecto/# Edita CLAUDE.md con tus comandos# Edita feature_list.json con tus features# Ejecuta tu agente — lee los archivos automáticamente&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier 2: Eval (30 min)Datasets JSONL con escenarios y respuestas esperadas. Un runner que ejecuta contra cualquier LLM. Un scorer que mide accuracy. Un gate que bloquea CI si baja del umbral.
&lt;/h3&gt;

&lt;p&gt;&lt;br&gt;
&lt;code&gt;bashpython3 run-evals.py --dataset prompting-basics.jsonl --output results.jsonlpython3 score-evals.py --results results.jsonl# Accuracy: 87.5% — PASS&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier 3: Observe (1h)Logger que registra 10 campos por interacción (tokens, coste, latencia, tools, errores). Cost tracker que agrega por sesión/modelo/dia. Dashboard Grafana importable.### Tier 4: Hooks (15 min)Automatización para Claude Code: lint pre-commit, tests post-tarea, guard de coste por sesión, scan de seguridad.### Tier 5: Patterns (lectura)8 documentos de referencia: díagnostic loop, multi-session, graph engineering, circuit breaker, HITL, model routing, prompt versioning, agent testing.### Tier 6: CI/CD (1h)GitHub Action que ejecuta evals en cada PR. Quality gate que bloquea merge si fallan.## Diagnostic Loop: el patrón más útil&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBBW0FnZW50IGZhaWxlZF0gLS0-IEJ7RGlkIHNvbWV0aGluZyBkaWZmZXJlbnQKdGhhbiBleHBlY3RlZD99CiAgICBCIC0tPnxZZXN8IENbTGF5ZXIgMTogU1BFQ0lGSUNBVElPTgpXcml0ZSBEZWZpbml0aW9uIG9mIERvbmVdCiAgICBCIC0tPnxOb3wgRHtXcm9uZyBwYXR0ZXJuL3ZlcnNpb24vCnN0eWxlP30KICAgIEQgLS0-fFllc3wgRVtMYXllciAyOiBDT05URVhUClVwZGF0ZSBDTEFVREUubWRdCiAgICBEIC0tPnxOb3wgRntEZXBlbmRlbmN5L3Rvb2xzCmVycm9yP30KICAgIEYgLS0-fFllc3wgR1tMYXllciAzOiBFTlZJUk9OTUVOVApGaXggaW5pdC5zaF0KICAgIEYgLS0-fE5vfCBIe1NhaWQgZG9uZSBidXQKZG9lc24ndCB3b3JrP30KICAgIEggLS0-fFllc3wgSVtMYXllciA0OiBWRVJJRklDQVRJT04KUmVxdWlyZSB0ZXN0cyArIGV2aWRlbmNlXQogICAgSCAtLT58Tm98IEpbTGF5ZXIgNTogU1RBVEUKcHJvZ3Jlc3MubWQgKyBoYW5kb2ZmXQogICAgc3R5bGUgQyBmaWxsOiNFRjQ0NDQsY29sb3I6I2ZmZgogICAgc3R5bGUgRSBmaWxsOiNmYjkyM2MsY29sb3I6I2ZmZgogICAgc3R5bGUgRyBmaWxsOiNhNzhiZmEsY29sb3I6I2ZmZgogICAgc3R5bGUgSSBmaWxsOiMyMmQzZWUsY29sb3I6IzFBMTkxNgogICAgc3R5bGUgSiBmaWxsOiMzNGQzOTksY29sb3I6IzFBMTkxNg%3D%3D%3FbgColor%3D1A1916" alt="Diagnostic Loop" width="937" height="1359"&gt;Cuando tu agente falla, no cambies de modelo. Diagnostica la capa: 1. &lt;strong&gt;Especificación&lt;/strong&gt; — la tarea era clara?2. &lt;strong&gt;Contexto&lt;/strong&gt; — el agente tenía la info? 3. &lt;strong&gt;Entorno&lt;/strong&gt; — dependencias correctas? 4. &lt;strong&gt;Verificacion&lt;/strong&gt; — había tests? 5. &lt;strong&gt;Estado&lt;/strong&gt; — recordaba el trabajo previo?Corrige la capa que falló. Repite. Tras 3-5 rondas tu entorno es robusto. Sin gastar más dinero.&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fc2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBEZXYgYXMgRGV2ZWxvcGVyCiAgICBwYXJ0aWNpcGFudCBEUyBhcyBEYXRhc2V0IEpTT05MCiAgICBwYXJ0aWNpcGFudCBSdW4gYXMgcnVuLWV2YWxzLnB5CiAgICBwYXJ0aWNpcGFudCBMTE0gYXMgTExNCiAgICBwYXJ0aWNpcGFudCBTY29yZSBhcyBzY29yZS1ldmFscy5weQogICAgcGFydGljaXBhbnQgR2F0ZSBhcyBjaS1nYXRlLnB5CiAgICBEZXYtPj5EUzogQ3JlYXRlIHNjZW5hcmlvcwogICAgRGV2LT4-UnVuOiAtLWRhdGFzZXQgZXZhbC5qc29ubAogICAgbG9vcCBFYWNoIHNjZW5hcmlvCiAgICAgICAgUnVuLT4-TExNOiBTZW5kIGlucHV0CiAgICAgICAgTExNLS0-PlJ1bjogUmVzcG9uc2UKICAgIGVuZAogICAgUnVuLT4-UnVuOiBTYXZlIHJlc3VsdHMuanNvbmwKICAgIERldi0-PlNjb3JlOiAtLXJlc3VsdHMgcmVzdWx0cy5qc29ubAogICAgU2NvcmUtLT4-RGV2OiBBY2N1cmFjeSA4Ny41JQogICAgRGV2LT4-R2F0ZTogLS10aHJlc2hvbGQgMC44CiAgICBHYXRlLS0-PkRldjogUEFTUw%3D%3D%3FbgColor%3D1A1916" alt="Eval Pipeline" width="1250" height="664"&gt;## Comparativa con Learn Harness Engineering (11.3K stars)| | LHE | ia-engineer-framework ||---|---|---|| Scope | Solo harness | Harness + eval + observe + CI || Eval pipeline | No | Runner + scorer + CI gate || Observabilidad | No | Logger + cost tracker + alertas || CI/CD | No | GitHub Action + quality gate || Idioma | Inglés | Español nativo || Formato | Curso | Copy-paste to production |## Principios1. &lt;strong&gt;Automatización antes que IA&lt;/strong&gt; — si un test lo resuelve, no uses LLM2. &lt;strong&gt;Verificable &amp;gt; inteligente&lt;/strong&gt; — un eval dataset de 8 preguntas &amp;gt; un prompt "perfecto"3. &lt;strong&gt;Copy-paste to production&lt;/strong&gt; — cada archivo funciona standalone4. &lt;strong&gt;Progresivo&lt;/strong&gt; — Tier 1 en 10 min, Tier 6 cuando tengas equipo5. &lt;strong&gt;Sin lock-in&lt;/strong&gt; — Claude Code, Codex, Cursor, cualquiera## Empieza en 5 minutos
&lt;/h3&gt;

&lt;p&gt;&lt;br&gt;
&lt;code&gt;bashgit clone https://github.com/pedri77/ia-engineer-framework.gitcp -r ia-engineer-framework/harness/ tu-proyecto/&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
Edita &lt;code&gt;CLAUDE.md&lt;/code&gt; y &lt;code&gt;feature_list.json&lt;/code&gt; con tus datos. Ejecuta tu agente.&lt;a href="https://github.com/pedri77/ia-engineer-framework" rel="noopener noreferrer"&gt;GitHub: pedri77/ia-engineer-framework&lt;/a&gt;MIT. Usa, modifica, distribuye.---&lt;em&gt;Esto es parte de &lt;a href="https://iacademy.es" rel="noopener noreferrer"&gt;IAcademy&lt;/a&gt;, academia de IA aplicada al trabajo real.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Building a Malware Intelligence Platform: 13 Public Feeds, Knowledge Graph, and Zero Budget</title>
      <dc:creator>David Moya</dc:creator>
      <pubDate>Thu, 13 Aug 2026 09:43:59 +0000</pubDate>
      <link>https://dev.to/david_moya_5b6cdd8b4e1219/building-a-malware-intelligence-platform-13-public-feeds-knowledge-graph-and-zero-budget-18gc</link>
      <guid>https://dev.to/david_moya_5b6cdd8b4e1219/building-a-malware-intelligence-platform-13-public-feeds-knowledge-graph-and-zero-budget-18gc</guid>
      <description>&lt;h1&gt;
  
  
  Building a Malware Intelligence Platform: 13 Public Feeds, Knowledge Graph, and Zero Budget
&lt;/h1&gt;

&lt;p&gt;I spent the last 6 months building &lt;a href="https://malwareintel.es" rel="noopener noreferrer"&gt;MalwareIntel&lt;/a&gt;, an open CTI (Cyber Threat Intelligence) platform that aggregates data from 13 public feeds into a unified knowledge graph. Here is the architecture, what worked, and what almost broke everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;SOC analysts and threat hunters need context. When an alert fires on a SHA256 hash, they need to know: what malware family is this? What APT group uses it? What MITRE ATT&amp;amp;CK techniques does it employ? What are the defensive mitigations?&lt;/p&gt;

&lt;p&gt;Commercial platforms (Recorded Future, ThreatConnect, Anomali) solve this for $30,000-100,000/year. Most small and mid-size security teams cannot afford that. They end up checking 5-10 different free tools manually.&lt;/p&gt;

&lt;p&gt;MalwareIntel aggregates all of that into one platform with a unified data model, for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;13 Public Feeds
    |
    v
Celery Workers (scheduled ingestion)
    |
    v
Validation + Normalization Pipeline
    |
    v
PostgreSQL (Supabase) ──→ Qdrant (semantic search)
    |
    v
FastAPI REST API
    |
    v
Next.js Frontend (Cloudflare Pages)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Backend:&lt;/strong&gt; FastAPI + Python 3.12, SQLAlchemy 2.0 async, Celery + Redis for scheduled ingestion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend:&lt;/strong&gt; Next.js 14 App Router, TypeScript strict, Tailwind + shadcn/ui. Dark mode only (SOC analysts work in the dark).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database:&lt;/strong&gt; Supabase PostgreSQL with full-text search via GIN indexes. Qdrant for semantic search over malware family descriptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hosting:&lt;/strong&gt; Cloudflare Pages (frontend), Hetzner VPS (backend + workers), Supabase (managed Postgres).&lt;/p&gt;

&lt;h2&gt;
  
  
  The 13 Feeds
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feed&lt;/th&gt;
&lt;th&gt;What it provides&lt;/th&gt;
&lt;th&gt;Schedule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MITRE ATT&amp;amp;CK (TAXII)&lt;/td&gt;
&lt;td&gt;TTPs, threat actors, software&lt;/td&gt;
&lt;td&gt;Weekly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MITRE D3FEND&lt;/td&gt;
&lt;td&gt;Defensive mitigations&lt;/td&gt;
&lt;td&gt;Weekly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MalwareBazaar (abuse.ch)&lt;/td&gt;
&lt;td&gt;Malware hashes + families&lt;/td&gt;
&lt;td&gt;Every 6h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ThreatFox (abuse.ch)&lt;/td&gt;
&lt;td&gt;C&amp;amp;C IOCs&lt;/td&gt;
&lt;td&gt;Every 6h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URLhaus (abuse.ch)&lt;/td&gt;
&lt;td&gt;Malware distribution URLs&lt;/td&gt;
&lt;td&gt;Every 6h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feodo Tracker (abuse.ch)&lt;/td&gt;
&lt;td&gt;Botnet C&amp;amp;C IPs&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OTX AlienVault&lt;/td&gt;
&lt;td&gt;Pulses, IOCs, TTPs&lt;/td&gt;
&lt;td&gt;Every 12h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MISP public feeds&lt;/td&gt;
&lt;td&gt;IOCs, events, galaxies&lt;/td&gt;
&lt;td&gt;Every 12h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CISA KEV&lt;/td&gt;
&lt;td&gt;Exploited vulnerabilities&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ransomware.live&lt;/td&gt;
&lt;td&gt;Ransomware victims + groups&lt;/td&gt;
&lt;td&gt;Every 6h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ANY.RUN feeds&lt;/td&gt;
&lt;td&gt;Sandbox behaviors&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HoneyDB&lt;/td&gt;
&lt;td&gt;Honeypot IOCs&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub (SigmaHQ)&lt;/td&gt;
&lt;td&gt;Detection rules&lt;/td&gt;
&lt;td&gt;Weekly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Data Model
&lt;/h2&gt;

&lt;p&gt;The core insight: &lt;strong&gt;everything in CTI is a graph&lt;/strong&gt;. A malware family uses TTPs. TTPs have mitigations. Actors use families. Campaigns deploy families against targets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MalwareFamily ──[uses]──────→ TTP (ATT&amp;amp;CK)
MalwareFamily ──[attributed]─→ ThreatActor
MalwareFamily ──[generates]──→ IOC (IP, hash, domain, URL)
MalwareFamily ──[exploits]───→ CVE
TTP ──[mitigated by]─────────→ D3FEND Mitigation
Campaign ──[uses]────────────→ MalwareFamily
Campaign ──[attributed]──────→ ThreatActor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We model this with 12 PostgreSQL tables and 5 M2M junction tables. Key tables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;malware_families&lt;/strong&gt;: 4,000+ families with type taxonomy (ransomware, RAT, infostealer, loader...)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iocs&lt;/strong&gt;: 1.7M+ indicators with type, confidence, severity, TLP classification&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ttps&lt;/strong&gt;: Full MITRE ATT&amp;amp;CK matrix with subtechniques&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;d3fend_mitigations&lt;/strong&gt;: Defensive countermeasures mapped to TTPs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Hardest Part: Normalization
&lt;/h2&gt;

&lt;p&gt;Every feed has its own format. MalwareBazaar calls it "signature", MITRE calls it "software", Malpedia uses different naming. Emotet vs Heodo vs Geodo are the same family.&lt;/p&gt;

&lt;p&gt;Our normalization pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fetch&lt;/strong&gt; raw data from feed API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate&lt;/strong&gt; format (IPs via , hashes by length, domains via RFC 1035)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reject&lt;/strong&gt; private IPs (RFC 1918), localhost, future dates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normalize&lt;/strong&gt; family names against our canonical taxonomy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deduplicate&lt;/strong&gt; by unique index on &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link&lt;/strong&gt; IOCs to families by matching signatures against aliases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score&lt;/strong&gt; confidence based on source reliability and corroboration count
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@celery_app.task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate_limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10/m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ingest_malwarebazaar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lookback_hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;get_or_create_source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MalwareBazaar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch_malwarebazaar_recent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lookback_hours&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;validated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MalwareBazaarSample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize_to_ioc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ioc_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# NEVER download the binary, only metadata
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Knowledge Graph
&lt;/h2&gt;

&lt;p&gt;The crown jewel. An interactive force-directed graph that visualizes relationships between families, actors, TTPs, IOCs, campaigns, and mitigations.&lt;/p&gt;

&lt;p&gt;Built with Canvas API (not D3 force simulation, which is too slow for 500+ nodes). Custom physics engine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node repulsion: F = 14000 / dist²&lt;/li&gt;
&lt;li&gt;Edge attraction: F = (dist - ideal) × 0.006 × strength&lt;/li&gt;
&lt;li&gt;Central gravity: F = pos × 0.0008&lt;/li&gt;
&lt;li&gt;Damping: 0.82 per frame&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;6 node types with semantic colors: purple (families), red (actors), green (TTPs), blue (IOCs), amber (campaigns), lime (mitigations).&lt;/p&gt;

&lt;h2&gt;
  
  
  Defense Kits
&lt;/h2&gt;

&lt;p&gt;The most popular feature: downloadable defense kits per malware family. Each kit includes IOCs, YARA rules, Sigma rules, ATT&amp;amp;CK mappings, and D3FEND mitigations. One click to get everything you need to detect and defend against a specific threat.&lt;/p&gt;

&lt;p&gt;4,251 families have defense kits available. Cobalt Strike alone generates a 621KB kit with 45 files.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Feed reliability varies wildly.&lt;/strong&gt; abuse.ch feeds are rock solid. Some MISP feeds have 30% duplicate data. Build retry logic and dedup from day one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Anti-prompt-injection in enrichment.&lt;/strong&gt; When using LLMs to enrich family descriptions, IOC values can contain adversarial text. Sanitize everything before feeding to the model.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Redis cache saves your API.&lt;/strong&gt; Without caching, the stats endpoints were hitting the database on every request. Redis with 5-minute TTL reduced DB load by 80%.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Spanish CTI content is an empty niche.&lt;/strong&gt; 95% of threat intelligence is in English. The blog section with CTI articles in Spanish gets more organic traffic than any English content we tried.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Numbers (6 months in)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Malware families indexed&lt;/td&gt;
&lt;td&gt;4,251&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IOCs in database&lt;/td&gt;
&lt;td&gt;1.7M+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public feeds integrated&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blog articles (ES)&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Defense kits available&lt;/td&gt;
&lt;td&gt;4,251&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly cost&lt;/td&gt;
&lt;td&gt;~50 EUR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revenue&lt;/td&gt;
&lt;td&gt;Pre-revenue (subscription model launching)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Stack Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Next.js 14, TypeScript, Tailwind, shadcn/ui&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;FastAPI, Python 3.12, SQLAlchemy 2.0 async&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workers&lt;/td&gt;
&lt;td&gt;Celery + Redis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;Supabase PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector search&lt;/td&gt;
&lt;td&gt;Qdrant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;Cloudflare Pages + Hetzner VPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD&lt;/td&gt;
&lt;td&gt;GitHub Actions (1027/1027 tests green)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://malwareintel.es" rel="noopener noreferrer"&gt;MalwareIntel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://malwareintel.es/defense-kit/" rel="noopener noreferrer"&gt;Defense Kit example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://malwareintel.es/blog/" rel="noopener noreferrer"&gt;Blog (Spanish CTI content)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Building in public. If you work in security and want to contribute feeds or integrations, reach out on &lt;a href="https://www.linkedin.com/in/dmgmoya/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>python</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How We Built a 160-Article AI Education Platform with Next.js and Static HTML</title>
      <dc:creator>David Moya</dc:creator>
      <pubDate>Thu, 13 Aug 2026 09:38:56 +0000</pubDate>
      <link>https://dev.to/david_moya_5b6cdd8b4e1219/how-we-built-a-160-article-ai-education-platform-with-nextjs-and-static-html-565h</link>
      <guid>https://dev.to/david_moya_5b6cdd8b4e1219/how-we-built-a-160-article-ai-education-platform-with-nextjs-and-static-html-565h</guid>
      <description>&lt;h1&gt;
  
  
  How We Built a 160-Article AI Education Platform with Next.js and Static HTML
&lt;/h1&gt;

&lt;p&gt;Three months ago, I launched &lt;a href="https://iacedemy.com" rel="noopener noreferrer"&gt;IAcademy&lt;/a&gt; — an AI education platform in Spanish with 160+ free guides covering everything from prompting basics to autonomous agents, LLM deployment, and MCP servers.&lt;/p&gt;

&lt;p&gt;Here's what worked, what didn't, and the architecture behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Spanish AI Education is Underserved
&lt;/h2&gt;

&lt;p&gt;The AI education space is dominated by English content. Coursera, Udemy, DeepLearning.AI — all English-first. Spanish-speaking professionals (500M+ people) get translated scraps or nothing.&lt;/p&gt;

&lt;p&gt;The opportunity: &lt;strong&gt;0% competition&lt;/strong&gt; on keywords like "agentes ia" (400 monthly searches), "herramientas ia" (400), "formación ia" (250). In English, these keywords have 30-50% competition. In Spanish, nobody's writing quality content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: Why Static HTML, Not a CMS
&lt;/h2&gt;

&lt;p&gt;Each blog post is a standalone &lt;code&gt;index.html&lt;/code&gt; file. No WordPress, no Gatsby, no MDX compilation step.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;site/blog/
├── agentes-ia-que-son/
│   └── index.html
├── herramientas-ia-guia/
│   └── index.html
├── formacion-ia/
│   └── index.html
└── ... (160+ directories)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this approach:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zero build time.&lt;/strong&gt; Adding an article = creating a directory + file. No compilation, no hydration errors, no framework upgrades breaking 160 pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Perfect SEO control.&lt;/strong&gt; Every &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt;, JSON-LD schema, internal link, and heading hierarchy is hand-crafted per page. No CMS template imposing its structure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instant deploy.&lt;/strong&gt; Push to GitHub → Cloudflare Pages deploys in ~30 seconds. No build queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No JavaScript required for content.&lt;/strong&gt; Google indexes immediately. Core Web Vitals are perfect — there's nothing to load.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The dynamic parts (auth, course portal, labs) use Supabase + vanilla JS. But the blog — which is the SEO engine — is pure static HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Content Strategy: Niche Prompts Beat Head Terms
&lt;/h2&gt;

&lt;p&gt;After 3 months, here's what ranks and what doesn't:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What ranks (top 10 in Google):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;prompts-ia-facturacion&lt;/code&gt; — prompts for accountants (position 8.4)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prompts-ia-logistica&lt;/code&gt; — prompts for logistics (position 4.3)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prompts-resumenes-ejecutivos&lt;/code&gt; — executive summary prompts (position 6.9)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rol-en-prompts&lt;/code&gt; — role prompting guide (position 7.2)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What doesn't rank yet:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;herramientas-ia-guia&lt;/code&gt; — "AI tools" head term&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;agentes-ia-que-son&lt;/code&gt; — "AI agents" head term&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ia-para-marketing&lt;/code&gt; — competitive keyword&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The pattern:&lt;/strong&gt; profession-specific prompt templates rank faster than generic guides. Nobody else writes "20 prompts for invoice reconciliation in Spanish." Everyone writes "best AI tools 2026."&lt;/p&gt;

&lt;h2&gt;
  
  
  SEO Schema: What Actually Matters
&lt;/h2&gt;

&lt;p&gt;Every article includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Article&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;schema&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Article"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"headline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"wordCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2800&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;FAQ&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;schema&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(drives&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;rich&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;snippets)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FAQPage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mainEntity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Breadcrumb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;schema&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BreadcrumbList"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"itemListElement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The FAQ schema is the highest-ROI structured data. Google shows your FAQ answers directly in search results → higher CTR → more clicks from the same position.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest Numbers (3 Months)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Articles published&lt;/td&gt;
&lt;td&gt;160+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total clicks (3 months)&lt;/td&gt;
&lt;td&gt;~120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily impressions&lt;/td&gt;
&lt;td&gt;0 → 200 (growing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pages in top 10&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revenue&lt;/td&gt;
&lt;td&gt;$0 (course just launched)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain Rating&lt;/td&gt;
&lt;td&gt;~0 (new domain)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;120 clicks in 3 months is painful.&lt;/strong&gt; But the trajectory matters: impressions went from 0 to 200/day. The content exists, Google is indexing it, positions are slowly improving. The bottleneck is domain authority (zero backlinks) and domain age (Google sandbox).&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with 20 niche articles, not 160.&lt;/strong&gt; I over-invested in content before having any domain authority. 20 well-linked articles on a domain with 30 backlinks would rank faster than 160 articles on a domain with 0 backlinks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Build backlinks from day one.&lt;/strong&gt; Directory submissions, guest posts, GitHub profile links — these should start before the first article, not after the 160th.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Focus on profession-specific content.&lt;/strong&gt; "Prompts for accountants" ranks. "AI tools guide" doesn't (yet). Double down on what works.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Blog:&lt;/strong&gt; Static HTML on Cloudflare Pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth + courses:&lt;/strong&gt; Supabase (PostgreSQL + RLS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics:&lt;/strong&gt; PostHog (EU)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email:&lt;/strong&gt; Brevo (transactional)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO tracking:&lt;/strong&gt; Google Search Console + Sistrix&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://iacedemy.com" rel="noopener noreferrer"&gt;IAcademy&lt;/a&gt; — the platform&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://iacedemy.com/blog/" rel="noopener noreferrer"&gt;Blog&lt;/a&gt; — 160+ free guides&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/pedri77/iacademy" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — repo&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Building in public. Follow along on &lt;a href="https://www.linkedin.com/in/dmgmoya/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; for weekly updates.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>nextjs</category>
      <category>seo</category>
      <category>education</category>
    </item>
    <item>
      <title>How I Aggregated 13 Public CTI Feeds into a Free Threat Intelligence Platform</title>
      <dc:creator>David Moya</dc:creator>
      <pubDate>Thu, 16 Jul 2026 13:20:35 +0000</pubDate>
      <link>https://dev.to/david_moya_5b6cdd8b4e1219/how-i-aggregated-13-public-cti-feeds-into-a-free-threat-intelligence-platform-g65</link>
      <guid>https://dev.to/david_moya_5b6cdd8b4e1219/how-i-aggregated-13-public-cti-feeds-into-a-free-threat-intelligence-platform-g65</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I built a free threat intelligence aggregation platform by pulling from 13 public CTI feeds (MISP, OTX, Abuse.ch, etc.), normalizing the data into a common schema, and exposing it through a REST API. This post walks through the architecture, the normalization pipeline, deduplication strategy, and a few gotchas I hit along the way.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why Build Another Threat Intel Tool?
&lt;/h2&gt;

&lt;p&gt;Commercial threat intelligence platforms are expensive. Recorded Future, ThreatConnect, and Anomali are excellent products, but their price tags put them out of reach for small security teams, indie researchers, and hobbyist defenders. At the same time, there are dozens of high-quality public feeds sitting out there—completely free—that most people never integrate properly because aggregating them is genuinely annoying.&lt;/p&gt;

&lt;p&gt;The pain points are real:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every feed has a different format (STIX, CSV, JSON, plain text)&lt;/li&gt;
&lt;li&gt;Update frequencies vary wildly (every 5 minutes to once a week)&lt;/li&gt;
&lt;li&gt;Duplicate indicators across feeds create noise&lt;/li&gt;
&lt;li&gt;Confidence scoring is inconsistent or absent entirely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I spent about three months building a pipeline to solve exactly this. The result is what became &lt;a href="https://malwareintel.es" rel="noopener noreferrer"&gt;MalwareIntel&lt;/a&gt;, a platform that aggregates, deduplicates, and scores threat indicators from 13 public sources. In this article I'll show you the technical pieces that make it work.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 13 Feeds and What They Provide
&lt;/h2&gt;

&lt;p&gt;Before writing a single line of code, I catalogued every feed and what data it actually provides. Here's the breakdown:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feed&lt;/th&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Update Frequency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Abuse.ch URLhaus&lt;/td&gt;
&lt;td&gt;CSV/JSON&lt;/td&gt;
&lt;td&gt;URLs, domains&lt;/td&gt;
&lt;td&gt;~15 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Abuse.ch MalwareBazaar&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;File hashes&lt;/td&gt;
&lt;td&gt;~1 hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Abuse.ch ThreatFox&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;IPs, domains, URLs, hashes&lt;/td&gt;
&lt;td&gt;~1 hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AlienVault OTX&lt;/td&gt;
&lt;td&gt;JSON (STIX)&lt;/td&gt;
&lt;td&gt;All IOC types&lt;/td&gt;
&lt;td&gt;Varies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MISP Default Feeds&lt;/td&gt;
&lt;td&gt;MISP JSON&lt;/td&gt;
&lt;td&gt;All IOC types&lt;/td&gt;
&lt;td&gt;Varies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feodo Tracker&lt;/td&gt;
&lt;td&gt;CSV&lt;/td&gt;
&lt;td&gt;IPs (C2)&lt;/td&gt;
&lt;td&gt;~1 hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PhishTank&lt;/td&gt;
&lt;td&gt;JSON/CSV&lt;/td&gt;
&lt;td&gt;URLs&lt;/td&gt;
&lt;td&gt;~1 hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenPhish&lt;/td&gt;
&lt;td&gt;Plain text&lt;/td&gt;
&lt;td&gt;URLs&lt;/td&gt;
&lt;td&gt;~12 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Emerging Threats (rules)&lt;/td&gt;
&lt;td&gt;Snort rules&lt;/td&gt;
&lt;td&gt;IPs, domains&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spamhaus DROP&lt;/td&gt;
&lt;td&gt;Plain text&lt;/td&gt;
&lt;td&gt;IP blocks (CIDR)&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spamhaus EDROP&lt;/td&gt;
&lt;td&gt;Plain text&lt;/td&gt;
&lt;td&gt;IP blocks (CIDR)&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CISA KEV&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;CVEs&lt;/td&gt;
&lt;td&gt;As published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Botvrij.eu&lt;/td&gt;
&lt;td&gt;Plain text sets&lt;/td&gt;
&lt;td&gt;Domains, IPs, hashes&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The diversity in formats is the core challenge. Let's look at how to handle it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building the Normalization Pipeline
&lt;/h2&gt;

&lt;p&gt;The most important architectural decision I made was defining a &lt;strong&gt;common indicator schema&lt;/strong&gt; before touching any feed. Every indicator, regardless of source, gets mapped to this structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IndicatorType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;IP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;DOMAIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;FILE_HASH_MD5&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;md5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;FILE_HASH_SHA1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;FILE_HASH_SHA256&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;CVE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;CIDR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cidr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ThreatIndicator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;itype&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IndicatorType&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;first_seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
    &lt;span class="n"&gt;last_seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
    &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;          &lt;span class="c1"&gt;# 0.0 - 1.0
&lt;/span&gt;    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;malware_family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;threat_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="c1"&gt;# original payload for debugging
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each feed gets its own &lt;strong&gt;parser class&lt;/strong&gt; that inherits from a base:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;abc&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ABC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;abstractmethod&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseFeedParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ABC&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feed_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feed_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;feed_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;feed_url&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;feed_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;feed_name&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;30.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;feed_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;

    &lt;span class="nd"&gt;@abstractmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ThreatIndicator&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Yield normalized ThreatIndicator objects.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ThreatIndicator&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's a concrete example for Abuse.ch ThreatFox, one of the richer feeds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ThreatFoxParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseFeedParser&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;CONFIDENCE_MAP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;feed_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://threatfox-api.abuse.ch/api/v1/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;feed_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;threatfox&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get_iocs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;feed_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ThreatIndicator&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query_status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
            &lt;span class="n"&gt;ioc_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_map_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ioc_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ioc_type&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nc"&gt;ThreatIndicator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ioc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;# strip port if present
&lt;/span&gt;                &lt;span class="n"&gt;itype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ioc_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;feed_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;first_seen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;first_seen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;T&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
                &lt;span class="n"&gt;last_seen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_seen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;T&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
                &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CONFIDENCE_MAP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence_level&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
                &lt;span class="n"&gt;malware_family&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;malware&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;threat_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;threat_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_map_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;IndicatorType&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;mapping&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip:port&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IndicatorType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IndicatorType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DOMAIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IndicatorType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;md5_hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IndicatorType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FILE_HASH_MD5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256_hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IndicatorType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FILE_HASH_SHA256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Spamhaus DROP feed is a totally different animal—plain text with comment lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ipaddress&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SpamhausDropParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseFeedParser&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;feed_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.spamhaus.org/drop/drop.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;feed_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spamhaus_drop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ThreatIndicator&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

            &lt;span class="c1"&gt;# Format: "1.2.3.0/24 ; SBL123456"
&lt;/span&gt;            &lt;span class="n"&gt;cidr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;ipaddress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ip_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cidr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nc"&gt;ThreatIndicator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cidr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;itype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;IndicatorType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CIDR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;feed_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;first_seen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;last_seen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Spamhaus has high reputation
&lt;/span&gt;                &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spam&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;botnet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;threat_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spam_infrastructure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Deduplication and Confidence Scoring
&lt;/h2&gt;

&lt;p&gt;This is where things get interesting. A single malicious IP might appear in Feodo Tracker, ThreatFox, AND OTX simultaneously. Naively storing all three entries creates noise. Instead, I merge duplicate indicators and use multi-source sightings to &lt;strong&gt;boost confidence&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The deduplication key is &lt;code&gt;(value, itype)&lt;/code&gt;. When the same indicator appears in multiple sources, I merge them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IndicatorAggregator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ThreatIndicator&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_counts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ingest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indicator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ThreatIndicator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indicator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indicator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;itype&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indicator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;indicator&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="c1"&gt;# Update temporal fields
&lt;/span&gt;            &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indicator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_seen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indicator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_seen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;# Merge tags
&lt;/span&gt;            &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;indicator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="c1"&gt;# Take highest individual confidence, then boost for multi-source
&lt;/span&gt;            &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indicator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Recalculate multi-source boost
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_apply_source_boost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_apply_source_boost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;indicator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;n_sources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="c1"&gt;# Each additional source adds a 5% boost, capped at 0.99
&lt;/span&gt;        &lt;span class="n"&gt;boost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;n_sources&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;indicator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indicator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;boost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.99&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Track source count as metadata
&lt;/span&gt;        &lt;span class="n"&gt;indicator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sources:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n_sources&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The multi-source boost is a simple heuristic but it works well in practice. An IP seen in three independent feeds is almost certainly malicious, and a confidence of 0.95+ reflects that appropriately.&lt;/p&gt;




&lt;h2&gt;
  
  
  Scheduling, Storage, and the API Layer
&lt;/h2&gt;

&lt;p&gt;Running all 13 parsers sequentially every hour would be inefficient. I use &lt;strong&gt;APScheduler&lt;/strong&gt; with per-feed intervals based on their actual update frequencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apscheduler.schedulers.blocking&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BlockingScheduler&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apscheduler.triggers.interval&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;IntervalTrigger&lt;/span&gt;

&lt;span class="n"&gt;scheduler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BlockingScheduler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;feed_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;URLhausParser&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;        &lt;span class="c1"&gt;# minutes
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ThreatFoxParser&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MalwareBazaarParser&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;FeodoTrackerParser&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PhishTankParser&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SpamhausDropParser&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;1440&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;# daily
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SpamhausEDropParser&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;1440&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OpenPhishParser&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;720&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;# ... etc
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;interval_minutes&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;feed_config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;run_feed_pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;trigger&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;IntervalTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;interval_minutes&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;feed_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_instances&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;coalesce&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For storage, I went with &lt;strong&gt;PostgreSQL&lt;/strong&gt; with a GIN index on the tags array and a B-tree index on &lt;code&gt;(value, itype)&lt;/code&gt; for fast lookups:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;indicators&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;       &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;itype&lt;/span&gt;       &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;confidence&lt;/span&gt;  &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;first_seen&lt;/span&gt;  &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_seen&lt;/span&gt;   &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sources&lt;/span&gt;     &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;        &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;malware_family&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;threat_type&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;itype&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_indicators_value_type&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;indicators&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;itype&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_indicators_tags&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;indicators&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_indicators_last_seen&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;indicators&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_seen&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_indicators_confidence&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;indicators&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The upsert operation handles the merge logic at the database level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;indicators&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;itype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first_seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;malware_family&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threat_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;itype&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;
    &lt;span class="n"&gt;confidence&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GREATEST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indicators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;first_seen&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LEAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indicators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_seen&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;last_seen&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GREATEST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indicators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_seen&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;sources&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;UNNEST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indicators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sources&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;UNNEST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indicators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;malware_family&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;malware_family&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indicators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;malware_family&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;threat_type&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threat_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indicators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threat_type&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The REST API is built with &lt;strong&gt;FastAPI&lt;/strong&gt; and exposes a simple lookup endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncpg&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CTI Aggregator API&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/v1/indicator/{value}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lookup_indicator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;min_confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchrow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT value, itype, confidence, first_seen, last_seen,
                   sources, tags, malware_family, threat_type
            FROM indicators
            WHERE value = $1 AND confidence &amp;gt;= $2
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_confidence&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Indicator not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/v1/search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_indicators&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;malware_family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;itype&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;min_confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;conditions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence &amp;gt;= $1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;min_confidence&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;conditions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; = ANY(tags)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;malware_family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;conditions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;malware_family ILIKE $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;malware_family&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;itype&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;conditions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;itype = $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;itype&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        SELECT value, itype, confidence, last_seen, sources, tags, malware_family
        FROM indicators
        WHERE &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; AND &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conditions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
        ORDER BY last_seen DESC, confidence DESC
        LIMIT &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What I Learned (and Would Do Differently)
&lt;/h2&gt;

&lt;p&gt;After running this pipeline for several months, a few lessons stand out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Start with fewer feeds.&lt;/strong&gt; I launched with all 13 simultaneously. That was a mistake. Each feed has its own quirks: rate limits, downtime patterns, format changes. Starting with 3-4 high-quality feeds (Abuse.ch ecosystem + OTX + CISA KEV) and adding incrementally would have saved debugging time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Deduplication at ingestion is non-negotiable.&lt;/strong&gt; Without the upsert-based merge, my database would have 10x more rows with no additional intelligence value. The multi-source confidence boost turned out to be one of the most useful features for downstream consumers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Observability matters from day one.&lt;/strong&gt; I added structured logging and feed health monitoring after the third "why did this feed stop updating?" incident. Each feed now has a heartbeat check and alerts when it misses two consecutive update cycles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The Knowledge Graph was the surprise hit.&lt;/strong&gt; I initially built &lt;a href="https://malwareintel.es" rel="noopener noreferrer"&gt;MalwareIntel&lt;/a&gt; as a simple indicator lookup tool. Adding the relationship graph between malware families, threat actors, and TTPs turned it from a search engine into an analysis platform. If I were starting over, I'd build the graph data model first.&lt;/p&gt;

&lt;p&gt;If you want to dive deeper into threat intelligence operations, I've written extensively about CTI feeds, SIEM integration, and SOC automation on the &lt;a href="https://blog.riskitera.com" rel="noopener noreferrer"&gt;Riskitera cybersecurity blog&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://malwareintel.es" rel="noopener noreferrer"&gt;MalwareIntel&lt;/a&gt; is free to use. Search any IOC, explore malware families, generate Sigma/KQL/SPL detection rules. The API is documented at &lt;code&gt;/docs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you build something similar or have questions about the architecture, find me on &lt;a href="https://linkedin.com/in/david-moya-garcia" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; or open an issue on &lt;a href="https://github.com/pedri77" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>python</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
