Arm 把内部安全研究团队打磨多年的漏洞发现框架 Metis 完全开源了。这不是又一个"AI 扫描器"的 demo——它在 Arm 内部 130 多个软件项目里已经跑出真阳性率提升 10 倍、误报降低 50% 的硬数据,并计划 2026 年底前覆盖 Arm 全系产品。对于做安全审计、代码扫描的工程师来说,这个架构值得拆开看一遍。
为什么传统静态分析撞墙了
静态分析工具(Coverity、CodeQL、Semgrep 等)的核心瓶颈不是速度,而是信号噪声比。一个中型项目跑完一轮扫描,几百上千条告警里真正可利用的漏洞可能不到 5 条。安全研究员的时间被大量消耗在"这条告警到底是不是真的"上。
问题的根源在于:传统工具靠规则匹配和抽象路径分析,缺乏对代码语义上下文的理解。它知道 memcpy(dst, src, len) 可能溢出,但不知道在当前调用链里 len 的上游约束是否已经限定了安全范围。
Metis 的切入点正是这个语义缺口——用 RAG 把项目上下文喂给 LLM,再让 Agent 自主规划审计路径,逐层深入,而不是一次性撒网。
Metis 的架构:RAG 为底,Agent 为舵
Metis 的核心思路可以拆成两层:
第一层:RAG 知识库。 把目标项目的源码、提交历史、文档、已知 CVE 描述等切片后存入向量数据库。当 Agent 需要判断某个函数是否危险时,RAG 检索会拉回该函数的完整定义、调用者、调用者上下文、相关 commit message——相当于给 LLM 配了一个"项目级记忆"。
第二层:Agentic 规划。 Agent 不是一次性问 LLM "这段代码有没有漏洞",而是分步推理:
- 入口识别——先找出暴露给外部输入的函数(网络解析、IPC handler、syscall entry)。
- 路径追踪——从入口沿数据流追踪到危险 sink(memcpy、alloc、format string)。
- 约束验证——在每条路径上检查是否存在边界检查、sanitizer、不可达条件。
- 可利用性评估——综合判断路径是否真的可触发、影响范围多大。
每一步的中间结果都写回上下文,下一步的 prompt 可以引用前一步的结论。这种链式推理大幅减少了"猜"的成分,也让误报有了被逐层淘汰的机会。
10 倍真阳性、50% 误报削减是怎么来的
Arm 公布的数据背后,有几个关键设计决策:
- 拒绝一次性输出。 Agent 不直接输出"漏洞列表",而是先输出推理链,再由验证模块逐条复核。推理链本身成为可审计的证据,也让安全研究员能快速定位 Agent 的判断依据。
- RAG 的检索粒度控制。 不是整文件丢给 LLM,而是按函数 + 调用链切片,控制每次推理的 token 窗口在有效范围内,避免 LLM 在长上下文里"迷失"。
- 项目专属知识注入。 把项目的历史漏洞模式、安全编码规范作为 RAG 的补充文档,让 Agent 学会"这个团队过去常犯的错误类型"。
这三点叠加的效果:Agent 不再对每条可疑路径都报高危,而是能区分"理论上有路径但实际不可达"和"有路径且缺少约束",前者被自然淘汰,后者被保留——这就是误报下降和真阳性上升同时发生的机制。
实践:用 RAG + Agent 搭一个最小化漏洞审计原型
Metis 完整代码已在 GitHub 开源,但如果你想在自己的项目上快速验证这个思路,可以先搭一个最小原型。下面是一个用 LangChain + Chroma + OpenAI API 实现的简化版 RAG Agent 漏洞审计器,核心逻辑与 Metis 同源:
# minivuln_audit.py — 最小化 RAG Agent 漏洞审计原型
# 运行前安装依赖:pip install langchain chromadb openai tiktoken tree-sitter tree-sitter-python
import os
import json
from pathlib import Path
from langchain_openai import ChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_core.documents import Document
# ── 1. 项目源码切片 & 入库 ──────────────────────────────
def ingest_project(project_dir: str, persist_dir: str = "./chroma_db"):
"""把项目源码按函数粒度切片,存入 Chroma 向量库"""
splitter = RecursiveCharacterTextSplitter(
chunk_size=800, # 控制每次检索的上下文大小
chunk_overlap=100,
separators=["\ndef ", "\nclass ", "\nif ", "\n"],
)
docs = []
for path in Path(project_dir).rglob("*.py"): # 可扩展 c/h 等后缀
content = path.read_text(encoding="utf-8", errors="ignore")
# 附带文件路径作为元数据,方便溯源
chunks = splitter.split_text(content)
for chunk in chunks:
docs.append(Document(page_content=chunk, metadata={"source": str(path)}))
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectordb = Chroma.from_documents(docs, embeddings, persist_directory=persist_dir)
vectordb.persist()
print(f"已入库 {len(docs)} 个代码片段 → {persist_dir}")
return vectordb
# ── 2. Agent 推理链定义 ──────────────────────────────────
ENTRY_PROMPT = """你是一个安全审计 Agent。以下是项目的代码片段:
{context}
请识别所有可能接收外部输入的"入口函数"(如网络请求处理、IPC、文件解析入口)。
对每个入口,列出:函数名、文件路径、输入来源类型。只输出 JSON 数组。"""
PATH_PROMPT = """基于已识别的入口函数:
{entries}
以下是相关代码上下文:
{context}
请从每个入口追踪数据流到危险 sink(memcpy/alloc/format/print 等无边界检查的调用)。
对每条路径,输出:入口→中间节点→sink,以及路径上是否有边界检查。JSON 数组格式。"""
VERIFY_PROMPT = """以下是候选漏洞路径:
{paths}
相关上下文:
{context}
逐条验证:路径是否真的可达?缺少的约束是否可被外部输入绕过?影响范围?
输出最终确认的漏洞列表,每条包含:路径、可利用性(高/中/低)、简要推理。JSON 格式。"""
# ── 3. 运行 Agent 链 ────────────────────────────────────
def run_audit(vectordb, project_name: str = "my_project"):
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# Step 1: 入口识别
# 检索项目顶层模块和 handler 相关代码
entry_docs = vectordb.similarity_search("handler request parse entry point", k=15)
entry_context = "\n---\n".join(d.page_content for d in entry_docs)
entries_raw = llm.invoke(ENTRY_PROMPT.format(context=entry_context)).content
entries = json.loads(entries_raw)
print(f"▸ 识别到 {len(entries)} 个入口函数")
# Step 2: 路径追踪 — 对每个入口检索其调用链上下文
all_paths = []
for entry in entries:
query = f"{entry['function_name']} call flow sink"
path_docs = vectordb.similarity_search(query, k=10)
path_context = "\n---\n".join(d.page_content for d in path_docs)
paths_raw = llm.invoke(PATH_PROMPT.format(
entries=json.dumps(entries, ensure_ascii=False),
context=path_context
)).content
paths = json.loads(paths_raw)
all_paths.extend(paths)
print(f"▸ 发现 {len(all_paths)} 条候选路径")
# Step 3: 可利用性验证
verify_docs = vectordb.similarity_search("boundary check sanitizer validation", k=10)
verify_context = "\n---\n".join(d.page_content for d in verify_docs)
vulns_raw = llm.invoke(VERIFY_PROMPT.format(
paths=json.dumps(all_paths, ensure_ascii=False),
context=verify_context
)).content
vulns = json.loads(vulns_raw)
print(f"▸ 最终确认 {len(vulns)} 个可利用漏洞")
return vulns
# ── 4. 主流程 ────────────────────────────────────────────
if __name__ == "__main__":
PROJECT_DIR = "./target_project" # ← 替换为你要审计的项目路径
os.environ.setdefault("OPENAI_API_KEY", "sk-xxx") # ← 替换为你的 key
vectordb = ingest_project(PROJECT_DIR)
results = run_audit(vectordb)
for v in results:
print(f"\n🚨 {v.get('exploitability', '?')} | {v.get('path', '?')}")
print(f" 推理: {v.get('reasoning', '?')}")
运行前你需要做的:
- 把
PROJECT_DIR指向你要审计的源码目录。 - 设置
OPENAI_API_KEY环境变量。 - 如果审计 C/C++ 项目,把
*.py改为*.c/*.h,并调整separators为["\nvoid ", "\nint ", "\nstatic ", "\n"]。 chunk_size=800是刻意偏小的值——Metis 的经验表明,控制每次检索的上下文窗口在 800–1200 token 能让 LLM 的推理质量最稳定。
这个原型缺少 Metis 的几个关键增强(项目历史漏洞知识注入、Agent 自主决定检索策略、多轮对话式验证),但核心的"RAG 检索 → 分步推理 → 逐层过滤"链路已经搭通。你可以在此基础上逐步叠加。
落地前需要想清楚的几件事
成本问题。 Metis 的 Agent 链每审计一个项目会调用数十到上百次 LLM,按 GPT-4o 的定价,一个中型项目(10 万行 C 代码)单次完整审计的 API 成本大约在 $20–$80。如果你要跑 CI 每次提交都审计,需要做增量策略——只审计变更函数及其调用者。
RAG 切片质量决定上限。 上面原型用 RecursiveCharacterTextSplitter 是最简方案。Metis 实际使用了基于 tree-sitter 的语义切片——按函数/结构体边界切,而不是按字符数硬切。如果你发现检索结果经常"断在函数中间",就该换语义切片了。
LLM 的安全幻觉。 Agent 有时会"发明"一条看起来合理但实际不存在的数据流路径。Metis 用了二次验证模块(独立 LLM 调用 + 代码路径可达性静态检查)来拦截这类幻觉。你的原型至少应该在 Step 3 的验证环节加一条规则:如果 Agent 声称的路径中某个函数名在项目源码里搜不到,直接剔除。
开源生态的对接。 Metis 开源后,最值得关注的不是框架本身,而是它积累的安全知识库——Arm 内部整理的漏洞模式、C/C++ 常见 sink 列表、架构级攻击面分类。这些数据如果随框架一起发布,会直接提升任何 RAG 审计系统的起点水位。去 GitHub 看看 arm-software/metis 的 data 目录,这是第一个该翻的地方。
Metis 的意义不在于"又一个 AI 安全工具",而在于它用生产级数据验证了一个架构判断:把 LLM 当成推理引擎而不是分类器,把 RAG 当成项目记忆而不是搜索框,把 Agent 当成审计流程而不是一次性问答——这三层组合能把漏洞发现的信号噪声比拉到一个传统静态分析够不到的水位。现在框架开源了,最实际的下一步是:拿你自己的项目跑一遍原型,看看 10 倍真阳性的曲线在你的代码上是不是同样成立。