AI agent 在真实业务中开始承担越来越多“主动决策”的工作之后,安全检查的对象不再只是代码仓库里的源代码。一个 agent 可以调用外部工具、读写文件、访问网络、执行命令,而这些能力往往来自 MCP 服务器,或者被封装成可复用的 agent skill。传统 SAST 扫描器盯着函数调用和数据流看,却没有回答一个问题:这个 agent 到底被授权做了什么,它暴露了多少能力给未知输入。安全扫描器(Security scanner for AI agents, MCP servers and agent skills)要解决的,正是这套新问题:审计 agent 的权限边界、工具暴露面、指令注入风险和可执行资产的供应链风险。
本文围绕“AI agent、MCP 服务器、agent skills 的安全扫描”这条主线展开。先说明为什么要独立分层扫描,再给出一套可落地的扫描器数据模型和最小 Python 实现,然后把它接入 CI/CD 作为质量门槛,最后整理排查路径和生产基线。适合正在搭建 AI 应用平台的研发、运维和安全同学阅读,也适合对 MCP 机制感兴趣的开发者作为上手参考。
1. 为什么 AI 代理需要独立的安全扫描层
1.1 从传统代码扫描到智能体安全扫描的差异
传统安全扫描聚焦在代码缺陷上,比如 SQL 注入、XSS、命令拼接、硬编码密钥。它的对象是“代码逻辑”,判断依据是语法、数据流和已知漏洞模式。AI agent 场景有一个本质区别:核心逻辑由模型动态生成,开发者无法在提交代码时确定模型最终会执行哪条工具调用路径。代码里可能根本不存在“注入点”,因为危险动作由模型根据上下文自行决定。
安全扫描器转向“能力审计”和“暴露面分析”。它检查的是:
- agent 配置里声明了哪些 MCP 服务器;
- 每个 MCP 服务器暴露了哪些工具、资源和提示词;
- 每个 agent skill 包允许哪些命令、网络访问和文件操作;
- 工具定义是否缺少输入校验、是否允许执行任意命令;
- 密钥、令牌、内部地址是否被打包进可分发资产。
| 维度 | 传统代码扫描 | AI agent 安全扫描 |
|---|---|---|
| 扫描对象 | 源代码、依赖、配置 | agent 配置、MCP 工具定义、技能包、提示模板、权限声明 |
| 主要风险 | 注入、越权、密钥泄露、依赖漏洞 | 提示注入、工具滥用、权限过大、供应链投毒 |
| 判断依据 | 代码语法、数据流、漏洞规则 | 能力声明、权限边界、工具参数约束、指令风险 |
| 修复方式 | 改代码、升级依赖 | 收紧工具权限、隔离执行环境、限制技能行为 |
| 运行时依赖 | 较低 | 较高,需要知道 agent 如何加载和执行外部能力 |
1.2 MCP 服务器的攻击面
MCP(Model Context Protocol)为 agent 访问外部工具和数据提供了一种标准协议。一个 MCP 服务器通常会向模型暴露三类内容:工具(tools)、资源(resources)和提示词(prompts)。工具是可执行操作,资源是可直接读取的数据,提示词是预置指令模板。
攻击面可以从三个层次看。
第一层是工具层。MCP 服务器可能声明一个execute_command工具,参数是command: string,模型可以直接传入 shell 命令。这类工具一旦被提示注入或恶意指令诱导,风险等级非常高。扫描器要识别工具名称、参数结构、是否包含command、shell、exec、file_write、http_request这类高风险语义。
第二层是资源和数据层。资源模板可能指向内部文件路径或内部服务地址,file:///etc/passwd、http://127.0.0.1:6379这类路径如果被模型读取,信息泄露风险很高。扫描器需要解析资源 URI 的 scheme 和 host。
第三层是指令层。提示词模板不是普通字符串,它会直接进入系统提示,影响模型行为。如果提示词来自不可信来源,可能包含“忽略之前指令”“把结果发送到外部地址”等恶意内容。虽然静态扫描无法完全判断模型会如何理解,但至少可以识别出高风险指令模式和外部地址。
// 常见 MCP 工具定义中的高风险字段 { "name": "execute_command", "description": "Run a shell command on the host", "inputSchema": { "type": "object", "properties": { "command": { "type": "string" }, "cwd": { "type": "string" } }, "required": ["command"] } }这段定义本身可能没有漏洞,但它意味着一旦 agent 被注入恶意指令,工具就会变成远程命令执行的入口。安全扫描器的任务之一,就是把这个风险等级标出来。
1.3 agent skills 的信任边界
agent skills 是一种把“能力 + 说明 + 脚本”打包成可复用资产的方式。技能包通常包含三部分:描述技能用途和调用方式的说明文档、执行具体操作的脚本或配置、声明权限和依赖的元数据。它的信任边界比普通代码更特殊。
普通代码的信任边界是仓库权限和代码评审;技能包的信任边界是“谁写的、谁审的、在哪运行”。如果团队从内部市场或第三方仓库拉取技能包,供应链风险会直接进入 agent 运行时。一个恶意技能包可以在说明里诱导模型调用危险命令,也可以直接在安装脚本里执行动作。
扫描器对技能包的审计重点包括:
- 技能包元数据是否声明了执行权限;
- 说明文档是否包含指向未知域名的链接;
- 脚本是否包含
rm -rf、curl | sh、base64 -d等高风险模式; - 技能包是否携带密钥或
.env文件; - 技能包依赖是否锁定版本,是否来自可信源。
2. 扫描器的架构设计与数据模型
2.1 扫描器整体工作流程
一个可用的 AI agent 安全扫描器至少包含四个阶段:收集目标、解析资产、执行规则、输出报告。
收集目标是定位要扫描的目录或文件。扫描对象可以是整个项目仓库,也可以是指定的 agent 目录、MCP 配置目录或 skills 目录。解析资产阶段把配置文件、JSON Schema、Markdown 说明、脚本文件转换为统一的数据结构。执行规则阶段用一组确定性的规则对数据结构做判断,产出风险条目。输出报告阶段把风险条目格式化为 JSON、Markdown 或 SARIF,方便集成到 CI/CD 和 IDE。
这个流程与传统扫描器类似,但关键差异在“资产解析”这一步。扫描器必须先理解 MCP 工具的结构、技能包的元数据,才能把风险规则命中在正确的位置。跳过解析阶段,直接用正则匹配全部文本,会产生大量误报。
2.2 用数据结构描述“AI 资产”
为了让扫描规则有稳定的执行基础,建议先用 Python dataclass 定义核心数据模型。下面是一个最小可用的模型,覆盖 MCP 服务器和技能包。
# models.py from dataclasses import dataclass, field from typing import Optional @dataclass class ToolDefinition: name: str description: str = "" input_schema: dict = field(default_factory=dict) annotations: dict = field(default_factory=dict) def parameters(self) -> dict: return self.input_schema.get("properties", {}) @dataclass class MCPResource: uri: str = "" name: str = "" mime_type: str = "" @dataclass class MCPPrompt: name: str = "" description: str = "" arguments: list = field(default_factory=list) @dataclass class MCPServerDeclaration: name: str = "" command: str = "" args: list = field(default_factory=list) env: dict = field(default_factory=dict) config_path: str = "" tools: list = field(default_factory=list) resources: list = field(default_factory=list) prompts: list = field(default_factory=list) @dataclass class SkillAction: command: str = "" allow_list: list = field(default_factory=list) network_access: bool = False filesystem_write: bool = False @dataclass class SkillPackage: name: str = "" version: str = "" path: str = "" description: str = "" allowed_commands: list = field(default_factory=list) actions: list = field(default_factory=list) files: list = field(default_factory=list)这几个数据结构决定了扫描规则能读取哪些信息。比如规则要判断一个 MCP 工具是否允许任意命令执行,只需要读取ToolDefinition.name和parameters()。规则要判断技能包是否联网,只需要读取SkillPackage.actions里的network_access字段。
2.3 风险分级
扫描结果不能只给一个“有问题/没问题”的结论。需要风险分级,否则团队无法确定修复优先级。这里建议分四级。
| 级别 | 名称 | 典型场景 | 处理策略 |
|---|---|---|---|
| L1 | 阻断 | 允许任意命令执行、包含明文密钥、技能包来源不可信 | CI 必须失败,禁止合并 |
| L2 | 高 | MCP 工具可访问内网地址、文件写权限未限制、提示词指向外部域名 | 需安全评审后合并 |
| L3 | 中 | 工具描述缺少输入约束、依赖版本未固定 | 记录风险,限期整改 |
| L4 | 提示 | 权限声明不完整、缺少安全说明 | 线上报告可见即可 |
分级要尽量可判定,不能依赖人的主观感觉。allow arbitrary command execution可以翻译成“工具名包含execute或shell,且参数中存在未约束的命令字符串”这样的规则。“来源不可信”可以翻译成“技能包元数据中没有source字段,或source不在可信域名列表中”。
2.4 为什么先建立数据模型,再写检测规则
如果直接对 MCP 配置文件做字符串匹配,规则会非常脆弱。比如工具名从exec_command改成run_cmd,正则匹配就失效了。如果先解析成结构体,规则写的是“读取所有 tools 的 name,只要包含exec或cmd就提示高险”,后续字段改名只需要调整一条解析映射。
另一个原因是方便扩展。今天扫描 MCP 工具,明天要扫描 MCP 资源、后天要扫描技能包,只要数据模型稳定,增加规则的成本会明显下降。
3. 用 Python 实现一个最小可用安全扫描器
3.1 环境准备
下面的示例用一个独立的 Python 项目演示,不依赖复杂框架。实际项目落地时,可以根据团队技术栈改写成 Go、Java 或 Rust,扫描器的核心思路是一样的。
mkdir ai-agent-security-scanner cd ai-agent-security-scanner python3 -m venv .venv source .venv/bin/activate pip install pyyaml jsonschemapyyaml用于解析 YAML 配置,jsonschema用于校验 MCP 工具参数。前者是为了处理技能包和扫描配置,后者是为了在解析工具 schema 时避免结构异常导致规则崩溃。
3.2 实现规则引擎
规则引擎的核心是:输入一个数据模型,输出若干风险条目。下面定义一个基础规则接口和两条可执行规则。
# rules.py from dataclasses import dataclass from typing import List from models import MCPServerDeclaration, SkillPackage @dataclass class RiskFinding: rule_id: str severity: str message: str file_path: str target: str class BaseRule: rule_id = "BASE" severity = "L4" def check_servers(self, servers) -> List[RiskFinding]: return [] def check_skills(self, skills: List[SkillPackage]) -> List[RiskFinding]: return [] class ArbitraryCommandToolRule(BaseRule): """检测 MCP 工具是否允许任意命令执行或参数中暴露命令入口。""" rule_id = "MCP-001" severity = "L1" dangerous_keywords = ["execute", "exec", "shell", "command", "cmd"] def check_servers(self, servers) -> List[RiskFinding]: findings = [] for server in servers: for tool in server.tools: name = tool.name.lower() if any(k in name for k in self.dangerous_keywords): params = tool.parameters() for param_name, param_schema in params.items(): if any(k in param_name.lower() for k in self.dangerous_keywords): findings.append(RiskFinding( rule_id=self.rule_id, severity=self.severity, message=f"Tool {tool.name} 允许通过参数 {param_name} 执行命令", file_path=server.config_path, target=server.name + "/" + tool.name )) return findings class SecretFileRule(BaseRule): """检测技能包目录下是否出现密钥文件或环境变量文件。""" rule_id = "SKILL-002" severity = "L1" secret_filename_keywords = [".env", "id_rsa", "credential", "secret", "token"] def check_skills(self, skills: List[SkillPackage]) -> List[RiskFinding]: findings = [] for skill in skills: for file_path in skill.files: lower_path = file_path.lower() if any(k in lower_path for k in self.secret_filename_keywords): findings.append(RiskFinding( rule_id=self.rule_id, severity=self.severity, message=f"技能包包含疑似密钥文件: {file_path}", file_path=file_path, target=skill.name )) return findings规则接口本身不复杂,关键是每个规则都只访问数据模型,不直接处理原始字符串。这样排查问题时,可以单独测试某一个规则,不会因为文件格式变化导致整条规则链崩溃。
3.3 解析 MCP 服务器配置
MCP 服务器声明可能出现在不同位置。常见场景是根目录下的mcp.json、.mcp.json,或者 agent 平台内置的servers配置。下面用一份通用示例说明解析思路。
{ "servers": [ { "name": "local-shell", "command": "python", "args": ["-m", "mcp_server_shell"], "env": { "ALLOWED_PATH": "/tmp/work" } }, { "name": "internal-db", "command": "python", "args": ["-m", "mcp_server_db"], "env": { "DB_HOST": "127.0.0.1" } } ] }解析器把 JSON 转成MCPServerDeclaration对象,然后根据工具定义填充tools。工具定义往往通过tools/list接口在运行时获取,但安全扫描器通常只做静态扫描,因此可以依赖一份导出的工具描述文件。如果项目中没有工具描述文件,扫描器至少需要检查服务器本身。
# mcp_parser.py import json from pathlib import Path from models import MCPServerDeclaration, ToolDefinition, MCPResource, MCPPrompt def load_mcp_servers(path: str) -> list: content = Path(path).read_text(encoding="utf-8") data = json.loads(content) servers = [] for item in data.get("servers", []): server = MCPServerDeclaration( name=item.get("name", ""), command=item.get("command", ""), args=item.get("args", []), env=item.get("env", {}), config_path=path, ) tools_export = item.get("tools", []) for tool_data in tools_export: server.tools.append(ToolDefinition( name=tool_data.get("name", ""), description=tool_data.get("description", ""), input_schema=tool_data.get("inputSchema", {}), )) servers.append(server) return servers这里要注意,tools字段不是 MCP 协议标准配置,而是扫描器使用的“工具描述导出文件”约定。实际项目里,可以通过脚本从 MCP 服务器调用tools/list后导出 JSON,作为扫描输入。
3.4 扫描 agent skills 目录
技能包目录结构可以按自己的约定设计。下面是一种通用结构:
skills/ fetch-url/ SKILL.md skill.yaml fetch.py local-db-query/ SKILL.md skill.yaml query.pyskill.yaml用于声明技能名称、权限和命令白名单。
# skills/fetch-url/skill.yaml name: fetch-url version: 1.0.0 description: Fetch URL content and extract text permissions: network: true filesystem_write: false commands: - python fetch.py {url}扫描器解析skill.yaml后,把该目录下的所有文件加入SkillPackage.files,然后交给规则检查。
# skill_parser.py import os import yaml from pathlib import Path from models import SkillPackage def load_skill_packages(base_dir: str) -> list: packages = [] for root, dirs, files in os.walk(base_dir): if "skill.yaml" not in files: continue meta_path = Path(root) / "skill.yaml" meta = yaml.safe_load(meta_path.read_text(encoding="utf-8")) skill = SkillPackage( name=meta.get("name", root), version=meta.get("version", ""), path=root, description=meta.get("description", ""), allowed_commands=meta.get("commands", []), ) skill.actions.append({ "network": meta.get("permissions", {}).get("network", False), "filesystem_write": meta.get("permissions", {}).get("filesystem_write", False), }) for file_name in files: skill.files.append(str((Path(root) / file_name).resolve())) packages.append(skill) return packages这里有个容易出错的地方:filesystem_write: false只是声明,扫描器无法证明脚本实际不会写入文件。因此技能包扫描只是第一道关卡,不是运行时保证。对于生产环境,应该配合沙箱或 seccomp 策略。
3.5 生成评分和报告
规则执行完成后,把所有RiskFinding汇总到一个报告里。报告不追求复杂的展示,关键是能直接被 CI 解析。
# scanner.py import json import sys from mcp_parser import load_mcp_servers from skill_parser import load_skill_packages from rules import ArbitraryCommandToolRule, SecretFileRule def scan(project_path: str) -> dict: findings = [] rules = [ArbitraryCommandToolRule(), SecretFileRule()] mcp_files = [project_path + "/mcp.json", project_path + "/.mcp.json"] servers = [] for mcp_file in mcp_files: if os.path.exists(mcp_file): servers.extend(load_mcp_servers(mcp_file)) skills_dir = os.path.join(project_path, "skills") skills = load_skill_packages(skills_dir) if os.path.exists(skills_dir) else [] for rule in rules: findings.extend(rule.check_servers(servers)) findings.extend(rule.check_skills(skills)) finding_dicts = [finding.__dict__ for finding in findings] return { "summary": { "total": len(finding_dicts), "blocking": len([f for f in finding_dicts if f["severity"] == "L1"]), "high": len([f for f in finding_dicts if f["severity"] == "L2"]), }, "findings": finding_dicts, } if __name__ == "__main__": project_path = sys.argv[1] if len(sys.argv) > 1 else "." result = scan(project_path) print(json.dumps(result, ensure_ascii=False, indent=2))一个简单报告输出如下:
{ "summary": { "total": 2, "blocking": 1, "high": 0 }, "findings": [ { "rule_id": "MCP-001", "severity": "L1", "message": "Tool execute_command 允许通过参数 command 执行命令", "file_path": "/repo/mcp.json", "target": "local-shell/execute_command" }, { "rule_id": "SKILL-002", "severity": "L1", "message": "技能包包含疑似密钥文件: /repo/skills/fetch-url/.env", "file_path": "/repo/skills/fetch-url/.env", "target": "fetch-url" } ] }这样一份报告可以直接合并到 MR 评论里,也可以上传为 CI artifact。blocking数量一旦大于 0,流水线就应该失败。
4. 将扫描器接入 CI/CD,形成可执行门槛
4.1 常用命令行参数设计
只有扫描器没有流程门槛,扫描结果很快会变成没人看的报告。建议从一开始就设计好命令行参数,便于接入各种 CI 系统。
python scanner.py scan \ --path ./agents \ --block-level L1 \ --ignore-file .security-ignore.yml \ --format json \ --debug参数含义:
| 参数 | 作用 | 示例 |
|---|---|---|
--path | 指定要扫描的目录 | ./agents |
--block-level | 达到该级别即阻断 CI | L1 |
--ignore-file | 指定忽略规则文件 | .security-ignore.yml |
--format | 报告输出格式 | json、markdown、sarif |
--debug | 输出解析日志和内部变量 | 无 |
--block-level的设计很关键。它让不同团队按自己的承受能力设置门槛:平台组可以严格要求 L1 阻断,独立小型项目临时放宽到 L2,但报告仍然保留。
4.2 在 GitHub Actions 中执行
下面是一段 CI 集成示例,在每次 Pull Request 上运行扫描,并上传报告。
name: agent-security-scan on: pull_request: paths: - "agents/**" - "skills/**" - "mcp.json" - ".github/workflows/agent-security-scan.yml" jobs: scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install dependencies run: pip install pyyaml jsonschema - name: Run security scanner run: python scanner.py scan --path . --block-level L1 --format json --debug - name: Upload report uses: actions/upload-artifact@v4 with: name: agent-security-report path: report.json触发路径要尽量精确。只影响agents/、skills/和 MCP 配置文件的改动才需要扫描,否则每轮 CI 都扫描全部仓库,时间和噪声成本都会上升。
4.3 用 ignore 配置降低噪声
任何安全扫描器都会产生误报。与其在代码里硬编码跳过,不如提供一份显式的忽略文件,保留审计记录。
# .security-ignore.yml rules: - rule_id: MCP-001 targets: - "local-shell/execute_command" reason: "execute_command 只允许在本地开发容器中运行,容器不挂载生产密钥" expires: "2026-01-01"忽略文件包含三个关键点:目标 ID、原因、过期时间。给“原因”和“过期时间”的目的是让忽略决策可追溯。没有过期时间的忽略规则,容易变成永久例外。
5. 实际项目中的排错与误报治理
5.1 高频问题排查表
扫描器接入后,最常见的不是扫描器本身报错,而是解析失败和误报。
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| MCP 配置解析后工具列表为空 | 配置里没有tools导出字段,或工具描述文件未生成 | 用--debug打印加载后的MCPServerDeclaration对象 | 增加导出流程,扫描前调用tools/list生成描述文件 |
| 技能包目录没有被扫描 | 扫描器只查找skill.yaml,目录命名不一致 | 检查目录下是否存在skill.yaml,文件大小写是否正确 | 统一技能包固定文件名,区分入口文件与说明文档 |
| 密钥扫描误报测试 token | 规则对整个目录的.env文件敏感 | 查看SkillPackage.files中记录的路径,确认是否为测试 fixture | 在.security-ignore.yml中忽略测试目录,或让种子数据放在fixtures/目录 |
| CI 无法运行扫描器,因为依赖下载失败 | 内网环境无法访问公共 Python 源 | 检查流水线日志中的 pip 错误 | 使用内部镜像源,或在依赖安装阶段前缓存依赖 |
| 扫描器版本和 MCP 配置结构不匹配 | 协议或平台升级后,字段名发生调整 | 用--debug导出解析后的数据模型 | 保持在 CI 中锁定扫描器版本,升级时先验证全量规则 |
5.2 误报治理的四个步骤
第一步,先确认规则命中的对象是否真实存在于目标目录。很多误报来自模板目录、测试目录和历史遗留文件。
第二步,判断规则逻辑是否过时。比如规则把http一律判为风险,但业务中https://也可能被误写。需要调整规则,而不是直接忽略。
第三步,分析数据模型是否解析正确。误报常常不是规则写错,而是解析器把描述文本塞进了错误字段,导致规则命中了错误内容。
第四步,用忽略文件记录经过人工确认的例外。忽略时写清原因和过期时间,每周组织一次 review。
5.3 日志与调试
扫描器调试时,建议保留三层日志:解析日志、规则命中日志、排除日志。--debug开关负责打开解析日志,展示每个输入文件被解析成了什么结构;--list-rules输出当前启用的规则 ID 和严重级别;排除日志会在忽略规则生效时打印原因,避免开发者误认为“扫描器坏了”。
python scanner.py scan --path ./demo --list-rules python scanner.py scan --path ./demo --debug第一行用于确认规则集合是否符合预期,第二行用于查看具体文件被解析后的数据模型。实际排查顺序是:先确认输入文件存在,再确认解析结果不为空,再确认规则命中逻辑,最后确认忽略规则是否干扰。
6. 生产环境落地的安全基线
6.1 学习环境、开发环境与生产环境的差异
同一个扫描器在不同环境里的用途和严格度要区分开。
| 环境 | 主要目的 | 建议做法 |
|---|---|---|
| 学习环境 | 快速跑通开发流程 | 扫描结果仅展示,不阻断;规则可放宽 |
| 开发环境 | 尽早发现高风险配置 | 开启 L1、L2 阻断,L3 提示 |
| 测试环境 | 验证安全策略本身 | 加入密钥轮换、模拟注入攻击等验证用例 |
| 生产环境 | 防止风险变更上线 | 强制阻断,扫描器版本固定,报告归档至少 180 天 |
扫描器只是生产环境的一层闸门,不能替代运行时隔离。生产环境还需要考虑日志、监控、回滚、权限边界和异常处理。比如 MCP 服务器运行在独立容器里,文件系统只读挂载,网络只允许白名单出口,这些是扫描器无法静态保证的。
6.2 最小可行安全基线清单
发布前检查清单:
- MCP 服务器是否只声明了最小必要工具;
- 工具参数是否避免“任意字符串命令”形式;
- MCP 服务器运行账号是否具备写权限;
- 技能包是否来自可信源,依赖版本是否锁定;
- 技能包目录是否包含密钥文件;
- 提示词模板是否包含外部链接或“忽略指令”类内容;
- 网络出口是否限制为白名单;
- 文件系统写入路径是否限制在临时目录;
- CI 是否强制运行扫描器,报告是否保存;
- 忽略规则是否有过期时间,是否经过评审。
这份清单可以贴到团队发布规范里。它不是安全最佳实践的全部,却是最容易落地的第一层。
6.3 扩展方向
当前实现是静态扫描,能力边界很清楚:它能看到声明,看不到运行时真实调用。后续扩展可以沿下面几个方向发展。
一是运行时审计。给 MCP 工具调用加一层代理,记录模型实际调用了哪些工具、传入了哪些参数,形成调用日志,再与静态扫描结果对比,发现“声明之外的实际行为”。
二是沙箱验证。把 MCP 服务器或技能包放入受限沙箱运行,观察它尝试访问的地址、文件和进程,用动态行为补充静态规则判断。
三是供应链签名。技能包和 MCP 服务器插件来自不同维护者,如果统一增加签名校验,扫描器可以把“来源可信”从不可判定项变成可判定项。
四是策略即代码。把“不能使用任意命令工具”“不能访问内网地址”“技能包不能携带密钥”写成策略文件,扫描器只是策略执行器。这样规则可以独立于扫描器代码维护,业务侧也更愿意参与评审。
6.4 实践建议与下一步
AI agent 的安全扫描是一个新事物,但它依然遵循老原则:最小权限、可信来源、闭环审计。如果你的团队刚开始做,不要急着写上百条规则,先把 MCP 服务器和技能包的数据模型建好,把“任意命令执行”“密钥泄露”“来源不可信”这三点查清楚,接入 CI 后观察一到两周误报率,再逐步扩充规则库。
从一个最小扫描器开始,让每一次工具调用、每一个技能包、每一段提示词都在上线前被检查一遍,是当前成本最低且收益最稳定的安全投入方向。下一步可以把扫描报告接入内部安全平台,把风险数据沉淀成趋势分析,让安全扫描从一次性工具变成持续治理机制。