简介:这是一套基于Python开发的威胁情报自动化播报系统,面向网络安全从业者、SOC分析师及安全开发工程师,解决多源威胁情报(CVE等)手工收集低效、信息分散、时效性差等问题。资源共70个文件,包含25个核心Python脚本(如main.py、crawler模块、DAO数据访问层)、10个XML配置与模板、8个.dat格式情报源缓存文件、4个HTML前端播报页面,以及SQL建表与回滚脚本、邮箱/微信通知配置、GitHub Actions工作流等,整体压缩包仅738KB,轻量易部署。已有191人学习下载,项目结构清晰,src目录分层明确,支持无服务器方式一键Fork+Secrets配置即用。读者可直接获得开箱即用的邮件推送方案、TOP10情报Web展示页、本地SQLite归档机制及完整CI/CD流程,大幅降低威胁情报集成门槛。
1. threat-intelligence 不是“情报截图合集”,而是可嵌入检测 pipeline 的实时数据流:它解决的是 SOC 工程师每天手动查 IOC 效率低、TTP 更新滞后、威胁上下文缺失这三类真实翻车现场
你见过凌晨三点还在 Excel 里粘贴从 MISP 导出的 IOCs,再逐条核对是否已在 SIEM 中命中?你试过把一份 PDF 格式的 APT 组织技战术报告硬塞进 SOAR 的 playbook 触发条件里,结果因字段不一致导致误报率飙升?threat-intelligence 这个词在很多团队里,还停留在“下载一个 IOC 列表 → 手动导入防火墙 → 等下次通报再重来”的黑匣子阶段。但真正能跑进生产环境的 threat-intelligence,必须满足三个硬指标:结构化(JSON/STIX2)、可编程(Python SDK 或 REST API)、可验证(来源可信度+时效性打分)。本资源包不是 PDF 报告或静态 CSV,而是一套开箱即用的 Python 工程化实践集合——包含 STIX2 解析器、MISP/OTX/AbuseIPDB 三源自动拉取脚本、IOC 去重与置信度融合逻辑、以及直接对接 Elasticsearch 和 Suricata 的输出适配器。适合正在搭建 SOAR 基础能力、或想把威胁情报从“人工查阅”升级为“自动决策输入”的蓝队工程师、SIEM 管理员和检测规则开发人员。它不教你怎么读 APT 报告,只告诉你怎么让报告里的 TTP 自动变成 Suricata 规则里的flowbits。
2. 构建可落地的威胁情报流水线:从原始数据到可执行 IOC 的四步转化逻辑
2.1 为什么必须放弃 CSV/Excel,转向 STIX2 作为中间数据模型?
威胁情报的混乱根源,从来不是数据少,而是格式碎片化。MISP 导出的是 JSON,但字段名是attribute_uuid;OTX API 返回的是pulse结构,含indicators数组但无kill_chain_phases;AbuseIPDB 只提供 IP+分数,连恶意类型都得靠正则猜。如果直接拼接这些原始响应,你的 pipeline 会像一台用胶带粘合的发动机——某天 MISP 升级了 API 版本,attribute_uuid变成object_id,整个 IOC 同步就停摆。STIX2 是目前唯一被主流平台(MISP、AlienVault、ThreatConnect)原生支持的标准化模型,它强制定义了Indicator、Malware、AttackPattern之间的关系,并通过created_by_ref字段绑定可信来源。本资源包默认使用stix2Python 库(v3.0.1)构建统一解析层,所有外部源数据在入库前,必须转换为标准 STIX2 Bundle 对象。这不是为了“高大上”,而是为了后续做Indicator与AttackPattern的关联查询时,不用再写 20 行正则去匹配“T1059.001”和“PowerShell Execution”。
# stix2_bundle_builder.py 示例:统一入口函数 from stix2 import Bundle, Indicator, Malware, Relationship, ThreatActor def build_stix2_bundle(ioc_data: dict, source: str) -> Bundle: """ ioc_data: 来自 MISP/OTX/AbuseIPDB 的原始响应字典 source: "misp", "otx", "abuseipdb" 返回:包含 Indicator + Relationship + ThreatActor 的 Bundle 对象 """ # 步骤1:提取核心 IOC(IP/Domain/Hash) if source == "misp": pattern = f"[ipv4-addr:value = '{ioc_data['value']}']" indicator = Indicator( id=f"indicator--{uuid4()}", pattern=pattern, pattern_type="stix", valid_from=datetime.now(timezone.utc), labels=["malicious-activity"] ) elif source == "otx": # OTX 的 pulse 包含多个 indicators,需遍历 pattern = f"[file:hashes.'SHA-256' = '{ioc_data['indicator']}']" indicator = Indicator( pattern=pattern, labels=["malware"], confidence=ioc_data.get("confidence", 70) # OTX 提供置信度 ) else: # abuseipdb pattern = f"[ipv4-addr:value = '{ioc_data['ipAddress']}']" indicator = Indicator( pattern=pattern, labels=["anomalous-traffic"], confidence=int(ioc_data["abuseConfidenceScore"]) # 直接映射为 0-100 分 ) # 步骤2:绑定来源(关键!避免后续无法溯源) threat_actor = ThreatActor( name=f"{source.upper()}_Feed", identity_class="system" ) relationship = Relationship( relationship_type="identifies", source_ref=threat_actor.id, target_ref=indicator.id ) return Bundle(objects=[threat_actor, indicator, relationship])提示:
stix2库的Bundle对象是内存对象,不是文件。本包所有 STIX2 操作均在内存完成,避免频繁序列化/反序列化带来的性能损耗。若需导出.json文件供其他系统消费,调用bundle.serialize(pretty=True)即可。
2.2 三源自动拉取:MISP、OTX、AbuseIPDB 的认证与限频实战配置
MISP、OTX、AbuseIPDB 的 API 调用绝非简单requests.get()。每个平台都有自己的认证机制、速率限制策略和错误码语义。硬编码 token 或忽略429 Too Many Requests,会导致 pipeline 在高峰时段静默失败——你以为数据同步正常,其实过去 6 小时的 IOC 全丢了。本资源包采用tenacity库实现指数退避重试,并为每个源单独封装认证类:
| 平台 | 认证方式 | 限频策略 | 关键错误码处理 |
|---|---|---|---|
| MISP | X-Auth-Token Header | 每分钟 100 请求(可配) | 403:Token 失效,触发告警 |
| OTX | X-OTX-API-Key Header | 每分钟 12 次(免费版) | 404:Pulse ID 不存在,跳过 |
| AbuseIPDB | Key Query Param | 每天 1000 次(需注册) | 401:Key 无效,停止该源同步 |
# feed_puller.py 中的 MISP 拉取器(带重试与限频) from tenacity import retry, stop_after_attempt, wait_exponential import requests class MISPPuller: def __init__(self, url: str, api_key: str, rate_limit_per_minute: int = 100): self.url = url.rstrip("/") self.session = requests.Session() self.session.headers.update({ "Authorization": api_key, "Accept": "application/json" }) # 使用令牌桶算法控制速率(简化版) self.rate_limiter = RateLimiter(max_calls=rate_limit_per_minute, period=60) @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10) ) def fetch_indicators(self, last_update: str) -> list: """ last_update: ISO8601 时间字符串,如 "2024-05-20T00:00:00Z" 返回:MISP attributes 列表(已过滤 type in ["ip-dst", "domain", "md5", "sha256"]) """ self.rate_limiter.call() # 触发限频检查 resp = self.session.get( f"{self.url}/attributes/restSearch", params={ "limit": 500, "page": 1, "type": "ip-dst,domain,md5,sha256", "date_from": last_update, "enforceWarninglist": False, "includeContext": False } ) resp.raise_for_status() # 自动抛出 4xx/5xx 异常 data = resp.json() if not data.get("response"): raise ValueError("MISP response missing 'response' key") return data["response"].get("Attribute", []) # 使用示例 puller = MISPPuller("https://your-misp.example.com", "your-api-key-here") indicators = puller.fetch_indicators("2024-05-20T00:00:00Z")注意:
RateLimiter类并非第三方库,而是本包内置的轻量级实现(基于time.time()和滑动窗口),避免引入ratelimit等额外依赖。其核心逻辑是记录每秒请求数,超限则time.sleep()。你可根据实际环境调整max_calls和period。
2.3 IOC 去重与置信度融合:为什么不能简单用set()去重?
两个不同来源上报同一个恶意 IP,但置信度分别是 95%(MISP)和 30%(AbuseIPDB),你该信谁?直接set()去重会丢失所有上下文,导致高置信度情报被低置信度覆盖。本包采用加权置信度融合算法:
- 若同一 IOC(相同 value + same type)出现在多个源,取最高置信度;
- 若同一源多次上报(如 MISP 中同一 IP 出现在不同事件中),取最近一次的置信度;
- 若无显式置信度(如 MISP 默认无 confidence 字段),则按来源可信度赋默认值(MISP=85, OTX=75, AbuseIPDB=60)。
# ioc_fusion.py 核心逻辑 from collections import defaultdict from typing import Dict, List, Tuple def fuse_iocs(ioc_list: List[dict]) -> List[dict]: """ ioc_list: [{"value": "1.2.3.4", "type": "ipv4-addr", "source": "misp", "confidence": 95}, ...] 返回:去重后列表,每个元素含 fused_confidence 字段 """ # 按 (value, type) 分组 grouped = defaultdict(list) for ioc in ioc_list: key = (ioc["value"], ioc["type"]) grouped[key].append(ioc) fused = [] for key, iocs in grouped.items(): # 步骤1:提取所有置信度,无则用默认值 confidences = [] for ioc in iocs: conf = ioc.get("confidence") if conf is None: conf = {"misp": 85, "otx": 75, "abuseipdb": 60}.get(ioc["source"], 50) confidences.append(conf) # 步骤2:取最大值(保守策略:宁可信高,不信低) fused_confidence = max(confidences) # 步骤3:保留首次出现的 source(便于溯源) first_source = iocs[0]["source"] fused.append({ "value": key[0], "type": key[1], "fused_confidence": fused_confidence, "sources": [ioc["source"] for ioc in iocs], # 记录所有来源 "first_seen_source": first_source }) return fused # 示例输入 raw_iocs = [ {"value": "1.2.3.4", "type": "ipv4-addr", "source": "misp", "confidence": 95}, {"value": "1.2.3.4", "type": "ipv4-addr", "source": "abuseipdb", "confidence": 30}, {"value": "evil.com", "type": "domain", "source": "otx", "confidence": 80} ] fused = fuse_iocs(raw_iocs) # 输出: [{'value': '1.2.3.4', 'type': 'ipv4-addr', 'fused_confidence': 95, ...}]提示:
fused_confidence不是平均值,也不是加权和。实战中发现,攻击者常利用低置信度情报(如 AbuseIPDB 的扫描 IP)污染高置信度源,取最大值是最鲁棒的策略。你可以在fuse_iocs函数中轻松替换为其他策略(如加权平均),只需修改fused_confidence计算逻辑。
3. 避坑:威胁情报 pipeline 的五个血泪经验,每一个都让我重启过三次服务
3.1 现象:MISP 同步任务每天凌晨卡死,日志显示ConnectionResetError
原因:MISP 默认启用 Gzip 压缩,但某些内网代理(如 Squid)未正确透传Content-Encoding: gzip头,导致requests库解压失败。更隐蔽的是,该错误不会立即抛出,而是在resp.json()时才触发,且堆栈指向json.decoder.JSONDecodeError,完全误导排查方向。
解决:在 MISP Puller 初始化时,强制禁用自动解压:
self.session.headers.update({"Accept-Encoding": "identity"}) # 关键!3.2 现象:OTX Pulse 中的域名 IOC 同步后,在 Elasticsearch 中搜索不到
原因:OTX 返回的indicator字段值为example[.]com(用[.]替换.防止邮箱爬虫),但 STIX2pattern要求严格语法,[domain-name:value = 'example[.]com']是非法 pattern。必须先还原为example.com。
解决:在build_stix2_bundle函数中,对 domain 类型 IOC 增加预处理:
if ioc_data.get("type") == "domain": clean_value = ioc_data["value"].replace("[.]", ".") # 还原点号 pattern = f"[domain-name:value = '{clean_value}']"3.3 现象:AbuseIPDB 拉取的 IP 在 Suricata 规则中匹配失败
原因:AbuseIPDB 的ipAddress字段是字符串,但部分 IP 含前导零(如"010.002.003.004"),Suricata 的ip_proto匹配器不识别带零的格式,必须标准化为"10.2.3.4"。
解决:使用ipaddress库强制标准化:
from ipaddress import ip_address try: normalized_ip = str(ip_address(ioc_data["ipAddress"])) except ValueError: # 非法 IP,跳过 continue3.4 现象:STIX2 Bundle 导出 JSON 后,MISP 导入时报错Invalid property name "created_by_ref"
原因:MISP 的 STIX2 导入器要求created_by_ref必须指向一个已存在的 Identity 对象 ID,而本包生成的ThreatActorID 是随机 UUID,MISP 中不存在对应 Identity。
解决:不创建ThreatActor,改用Identity对象,并固定 ID:
# 替换原 ThreatActor 创建逻辑 identity = Identity( id="identity--threat-intel-pipeline", name="Threat Intel Pipeline", identity_class="system" ) # relationship.source_ref 改为 identity.id3.5 现象:Elasticsearch 输出适配器写入速度慢,CPU 占用 100%
原因:默认使用elasticsearch-py的单文档index()方法,每条 IOC 发起一次 HTTP 请求。当每分钟同步 5000+ IOC 时,网络开销成为瓶颈。
解决:切换为批量写入bulk(),并设置refresh=False:
from elasticsearch import helpers actions = [ { "_op_type": "index", "_index": "threat-iocs", "_source": ioc_dict } for ioc_dict in fused_iocs ] helpers.bulk(es_client, actions, refresh=False) # 关键:关闭实时刷新4. 输出适配:把 STIX2 Bundle 转成 Suricata 规则、Elasticsearch 文档、CSV 报表的三种落地姿势
4.1 Suricata 规则生成器:从 Indicator Pattern 到可部署规则的精准映射
Suricata 不认识 STIX2 的pattern字段,必须解析pattern字符串,提取ipv4-addr:value、domain-name:value等字段,并映射为ip_proto、http.host等关键字。本包提供suricata_rule_generator.py,支持三种模式:
| IOC Type | STIX2 Pattern 示例 | 生成的 Suricata Rule 示例 | 触发动作 |
|---|---|---|---|
| ipv4-addr | [ipv4-addr:value = '1.2.3.4'] | alert ip any any -> 1.2.3.4 any (msg:"MISP IOC: Malicious IP"; sid:1000001; rev:1;) | alert |
| domain-name | [domain-name:value = 'evil.com'] | alert http any any -> any any (msg:"OTX IOC: Malicious Domain"; content:"evil.com"; http.host; sid:1000002; rev:1;) | alert |
| file:hashes | [file:hashes.'SHA-256' = 'abc...'] | `alert http any any -> any any (msg:"OTX IOC: Malware Hash"; content:" | abc... |
# suricata_rule_generator.py 核心解析逻辑 import re def pattern_to_suricata_rule(pattern: str, confidence: int, source: str) -> str: """ pattern: STIX2 pattern string, e.g., "[ipv4-addr:value = '1.2.3.4']" confidence: fused_confidence (0-100) source: "misp", "otx", etc. 返回:Suricata rule string """ # 步骤1:提取 type 和 value match = re.search(r"\[(\w+)-addr:value\s*=\s*'([^']+)'\]", pattern) if match: addr_type, ip_value = match.groups() # 生成 IP 规则 action = "drop" if confidence >= 80 else "alert" sid = generate_sid(source) # 基于 source 生成唯一 sid return f'{action} ip any any -> {ip_value} any (msg:"{source.upper()} IOC: Malicious IP"; sid:{sid}; rev:1;)' match = re.search(r"\[domain-name:value\s*=\s*'([^']+)'\]", pattern) if match: domain = match.group(1) action = "drop" if confidence >= 80 else "alert" sid = generate_sid(source) return f'{action} http any any -> any any (msg:"{source.upper()} IOC: Malicious Domain"; content:"{domain}"; http.host; sid:{sid}; rev:1;)' # 其他类型(hash)暂略... return "" # 使用示例 rule = pattern_to_suricata_rule( "[ipv4-addr:value = '1.2.3.4']", confidence=95, source="misp" ) # 输出: 'drop ip any any -> 1.2.3.4 any (msg:"MISP IOC: Malicious IP"; sid:1000001; rev:1;)'注意:
generate_sid()函数确保同一 source 的规则 sid 连续递增(如 MISP 从 1000001 开始),避免与现有规则冲突。sid 生成逻辑存储在sid_registry.json中,每次调用后自动更新。
4.2 Elasticsearch 输出适配器:字段设计与索引模板最佳实践
直接es.index()会创建动态 mapping,导致confidence字段被识别为text而非integer,后续聚合查询失败。本包提供预定义索引模板threat_iocs_template.json,强制指定关键字段类型:
{ "mappings": { "properties": { "value": { "type": "keyword" }, "type": { "type": "keyword" }, "fused_confidence": { "type": "integer" }, "sources": { "type": "keyword" }, "first_seen_source": { "type": "keyword" }, "created_at": { "type": "date" } } } }# es_output_adapter.py 初始化逻辑 def init_threat_index(es_client: Elasticsearch, index_name: str = "threat-iocs"): """创建索引并应用模板""" if not es_client.indices.exists(index=index_name): es_client.indices.create( index=index_name, body={ "settings": {"number_of_shards": 3, "number_of_replicas": 1}, "mappings": { "properties": { "value": {"type": "keyword"}, "type": {"type": "keyword"}, "fused_confidence": {"type": "integer"}, "sources": {"type": "keyword"}, "first_seen_source": {"type": "keyword"}, "created_at": {"type": "date"} } } } ) print(f"Index {index_name} created with custom mapping") # 写入逻辑(使用 bulk) def write_to_es(es_client: Elasticsearch, iocs: List[dict]): actions = [] for ioc in iocs: action = { "_op_type": "index", "_index": "threat-iocs", "_source": { "value": ioc["value"], "type": ioc["type"], "fused_confidence": ioc["fused_confidence"], "sources": ioc["sources"], "first_seen_source": ioc["first_seen_source"], "created_at": datetime.now(timezone.utc).isoformat() } } actions.append(action) helpers.bulk(es_client, actions, refresh=False)提示:
refresh=False是性能关键。生产环境建议每 500 条 IOC 批量提交一次,并在每日凌晨执行POST /threat-iocs/_refresh保证搜索可见性。
4.3 CSV 报表生成器:给管理层看的“可读情报摘要”
安全团队常被要求每周提交“威胁情报周报”,但 raw IOC 列表毫无业务价值。本包csv_report_generator.py自动生成三张表:
| 表名 | 字段 | 用途 |
|---|---|---|
ioc_summary.csv | type,count,avg_confidence,top_source | 情报类型分布与质量概览 |
high_conf_iocs.csv | value,type,fused_confidence,sources,first_seen_source | >80 分 IOC 清单(供处置) |
source_comparison.csv | source,total_iocs,unique_iocs,overlap_rate | 各源贡献度与重叠分析 |
# csv_report_generator.py 核心逻辑 import pandas as pd def generate_csv_reports(fused_iocs: List[dict], output_dir: str): df = pd.DataFrame(fused_iocs) # 表1:汇总 summary = df.groupby("type").agg( count=("value", "count"), avg_confidence=("fused_confidence", "mean"), top_source=("first_seen_source", lambda x: x.mode().iloc[0] if not x.mode().empty else "N/A") ).round(2).reset_index() summary.to_csv(f"{output_dir}/ioc_summary.csv", index=False) # 表2:高置信度 IOC high_conf = df[df["fused_confidence"] >= 80].sort_values( by=["fused_confidence", "value"], ascending=[False, True] ) high_conf.to_csv(f"{output_dir}/high_conf_iocs.csv", index=False) # 表3:源对比(需先统计各源原始 IOC 数) # (此处省略统计逻辑,实际代码中已实现) # source_stats.to_csv(f"{output_dir}/source_comparison.csv", index=False)注意:
pandas仅用于报表生成,不影响核心 pipeline。若环境禁止安装 pandas,csv_report_generator.py提供纯csv模块备选实现(性能稍低,但零依赖)。
5. 验证 pipeline 健康度:用三个命令快速诊断数据流是否“活”着
5.1 实时监控:用curl和jq检查 STIX2 Bundle 的完整性
不要等告警才看 pipeline 是否存活。每天早会前,执行这一行命令,5 秒内确认核心数据流状态:
curl -s "http://localhost:8000/api/v1/bundle?limit=1" | jq '.objects | length'- 预期输出:
3(一个 Bundle 至少含 Indicator + Identity + Relationship) - 异常解读:
- 输出
0:API 未启动,或数据库无数据 - 输出
1:只有 Indicator,缺少 Identity/Relationship →build_stix2_bundle逻辑故障 - 输出
null:API 返回非 JSON,检查Content-Type: application/json
- 输出
提示:本包内置 FastAPI 服务(
api_server.py),端口8000,路径/api/v1/bundle返回最新 Bundle。limit=1参数确保只取最近一条,避免大体积响应拖慢诊断。
5.2 数据质量审计:用stix2-validator检查 Bundle 是否符合规范
STIX2 是强约束模型,字段缺失或类型错误会导致下游系统(如 MISP)拒绝导入。stix2-validator是官方校验工具,但直接运行stix2_validator bundle.json太重。本包提供轻量级校验函数:
# stix2_validator_lite.py from stix2 import parse def validate_bundle_json(bundle_json: str) -> bool: """ bundle_json: STIX2 Bundle 的 JSON 字符串 返回:True 表示有效,False 表示无效(打印具体错误) """ try: bundle = parse(bundle_json, allow_custom=True) # 检查必要字段 assert hasattr(bundle, 'objects') and len(bundle.objects) >= 3, "Bundle must have at least 3 objects" assert all(hasattr(obj, 'id') for obj in bundle.objects), "All objects must have 'id'" assert all(hasattr(obj, 'type') for obj in bundle.objects), "All objects must have 'type'" return True except Exception as e: print(f"STIX2 validation failed: {e}") return False # 使用示例(在 pipeline 最终步骤调用) with open("/path/to/latest_bundle.json", "r") as f: bundle_json = f.read() if not validate_bundle_json(bundle_json): send_alert("STIX2 Bundle validation failed!")注意:
parse(..., allow_custom=True)允许自定义扩展字段(如x_threat_intel_pipeline_version),避免因非标字段导致校验失败。
5.3 溯源能力验证:用 Elasticsearch 查询验证“这个 IOC 来自哪个源”
情报的价值在于可追溯。执行以下查询,应返回first_seen_source和sources字段:
curl -X GET "http://localhost:9200/threat-iocs/_search" \ -H "Content-Type: application/json" \ -d '{ "query": { "term": { "value": "1.2.3.4" } }, "fields": ["first_seen_source", "sources", "fused_confidence"] }'- 预期响应:
"hits": [{ "fields": { "first_seen_source": ["misp"], "sources": ["misp", "abuseipdb"], "fused_confidence": [95] } }] - 翻车信号:
sources字段为空 →fuse_iocs未正确填充sources列表first_seen_source为null→fuse_iocs中iocs[0]["source"]访问越界(空列表)
从那以后我每次上线新源(比如新增 VirusTotal),都强制走一遍这三步验证:
curl看 Bundle 长度是否达标;validate_bundle_json()确认 STIX2 结构合法;- Elasticsearch 查询确认溯源字段完整。
漏掉任何一步,我都宁愿停服两小时,也不让脏数据流入检测系统——因为修复一条错误 IOC 的成本,是预防它的十倍。希望帮到你。
本文还有配套的精品资源,点击获取