功能概述
Skill 安全检测是 Web 应用防火墙(WAF)大模型安全模块在 SDK/API 接入模式下提供的 Skill 插件安全扫描能力。当 AI Agent 需要加载外部 Skill 插件时,您可以通过 API 上传 Skill ZIP 包触发异步安全扫描,系统将对 Skill 代码进行安全分析,检测供应链风险、命令注入、数据外传、提示词注入等威胁,并返回风险等级和详细的安全审计结果。
您可以根据检测结果的风险等级,决定是否允许该 Skill 安装或加载到 AI Agent 中,有助于在加载前识别风险较高的 Skill。
说明:
适用场景
AI Agent 从外部市场或第三方来源安装 Skill 插件前,进行安全准入检测。
企业内部 Skill 开发完成后,在发布上线前进行安全审计。
定期对已上线的 Skill 进行安全复检,及时发现新增的依赖风险或样本特征变化。
检测流程
Skill 安全检测为异步检测,整体流程如下:
1. 按照 规范要求 将 Skill 目录打包为按规范生成的 ZIP 文件。
2. 调用 UploadSkillSecScan 接口上传 ZIP 文件(需先进行 Base64 编码)。上传成功后接口立即返回该文件的 ContentHash(SHA256),后台异步执行扫描。
说明:
当前
UploadSkillSecScan 接口限制单个 Skill ZIP 文件大小上限为 10 MB,超过限制将被拒绝。建议精简 Skill 包内容,例如剔除非必要的二进制资源、模型权重等,将文件控制在 10 MB 以内。3. 使用上一步返回的 ContentHash,调用 DescribeSkillSecScanResult 接口轮询查询检测结果。
一般情况下,单次检测可在 5 分钟以内完成,建议轮询间隔设置为 5~10 秒。
对于可疑或恶意样本,因需进行更深度的分析,检测耗时通常为 5~10 分钟,最长约 90 分钟。
如果检测失败,可重新触发检测。
4. 检测完成后,系统将返回:
风险等级:
malicious(恶意)/ suspicious(可疑)/ benign(可信)。命中规则详情:按检测引擎分组,展示每条风险的文件位置、行为特征等具体信息。
处置建议:针对检测结果的综合修复建议。
规范要求
Skill 安全检测会基于文件内容的 SHA256 Hash 复用相同 Skill 的历史检测结果,避免重复扫描。但 ZIP 文件格式会在文件头中记录修改时间、文件系统属性等元信息,即使源文件内容完全相同,不同环境下打包产出的 ZIP 二进制也可能不同,导致 Hash 不一致、无法复用结果。因此,在调用上传接口前,需按以下规范打包生成 ZIP 文件,使同一份 Skill 目录产出的 ContentHash 保持一致。
请统一使用同一种语言的内置 ZIP 库进行打包,并严格按照下表参数执行,使 Hash 保持一致。
项目 | 要求 |
打包方式 | 必须使用编程语言内置的 ZIP 库,例如 Go archive/zip、Python zipfile;不得使用系统 zip 命令 |
文件排序 | 按路径字节序排序,其效果等价于在 Shell 中执行 LC_ALL=C sort。 |
时间戳 | 固定为 2000-01-01 00:00:00 UTC |
压缩算法 | DEFLATE,压缩级别固定为 6 |
文件权限 | 固定为 0644 |
Extra Field / Comment | 不写入 |
目录条目 | 不单独写入目录条目,目录结构由文件路径自身体现 |
文件过滤 | Skill 目录下的所有文件均需打包,不进行任何过滤或排除 |
注意:
系统
zip 工具(即 Info-ZIP 项目)的不同版本,以及不同编程语言的 DEFLATE 实现,对相同输入也可能产出不同的压缩字节流。示例
Go 语言
以下示例展示如何使用 Go 将 Skill 目录按规范打包为 ZIP,并生成可作为
UploadSkillSecScan 接口 FileData 参数的 Base64 编码字符串。1. 将
/path/to/skill_dir 替换为待检测的 Skill 目录路径。2. 运行程序,获取 Base64 编码字符串及 ContentHash。
3. 将 Base64 字符串作为
FileData 参数传入 UploadSkillSecScan 接口。package mainimport ("archive/zip""bytes""compress/flate""crypto/sha256""encoding/base64""encoding/hex""fmt""io""os""path/filepath""sort""strings""time")// 固定时间戳:2000-01-01 00:00:00 UTCvar fixedModTime = time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)func packSkillDir(skillDir string) ([]byte, error) {// 1. 收集所有文件的相对路径var files []stringerr := filepath.Walk(skillDir, func(path string, info os.FileInfo, err error) error {if err != nil {return err}if info.IsDir() {return nil // 不写入目录条目}rel, err := filepath.Rel(skillDir, path)if err != nil {return err}// 统一使用 / 作为 ZIP 内路径分隔符files = append(files, filepath.ToSlash(rel))return nil})if err != nil {return nil, err}// 2. 按路径字节序排序(等价于 LC_ALL=C sort)sort.Strings(files)// 3. 写入 ZIP(压缩级别 6,固定时间戳,固定权限 0644)buf := new(bytes.Buffer)zw := zip.NewWriter(buf)// 显式注册压缩级别 6 的 deflate writer,确保跨 Go 版本输出稳定zw.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) {return flate.NewWriter(out, 6)})for _, rel := range files {header := &zip.FileHeader{Name: rel,Method: zip.Deflate,Modified: fixedModTime,}header.SetMode(0644)// 不写入 Extra Field / Commentheader.Extra = nilheader.Comment = ""w, err := zw.CreateHeader(header)if err != nil {return nil, err}f, err := os.Open(filepath.Join(skillDir, filepath.FromSlash(rel)))if err != nil {return nil, err}if _, err := io.Copy(w, f); err != nil {f.Close()return nil, err}f.Close()}if err := zw.Close(); err != nil {return nil, err}return buf.Bytes(), nil}func main() {if len(os.Args) < 2 {fmt.Println("Usage: skill_package_to_base64 <skill_dir>")os.Exit(1)}skillDir := strings.TrimRight(os.Args[1], string(os.PathSeparator))zipBytes, err := packSkillDir(skillDir)if err != nil {fmt.Fprintf(os.Stderr, "pack failed: %v\\n", err)os.Exit(1)}sum := sha256.Sum256(zipBytes)fmt.Println("ContentHash:", hex.EncodeToString(sum[:]))fmt.Println("FileData (Base64):")fmt.Println(base64.StdEncoding.EncodeToString(zipBytes))}
Python 语言
以下示例展示如何使用 Python 将 Skill 目录按规范打包为 ZIP,并生成可作为
UploadSkillSecScan 接口 FileData 参数的 Base64 编码字符串。说明:
运行环境要求 Python 3.7 或更高版本。本示例仅依赖 Python 标准库,无需安装任何第三方包。
1. 将命令行参数替换为待检测的 Skill 目录路径:
python3 skill_package_to_base64.py /path/to/skill_dir
2. 运行程序,获取 Base64 编码字符串和 ContentHash。
3. 将 Base64 字符串作为
FileData 参数传入 UploadSkillSecScan 接口。#!/usr/bin/env python3# skill_package_to_base64.py# 按规范打包 Skill 目录为 ZIP,并输出 Base64 与 ContentHash(SHA256)import base64import hashlibimport ioimport osimport sysimport zipfile# 固定时间戳:2000-01-01 00:00:00 UTCFIXED_DATE_TIME = (2000, 1, 1, 0, 0, 0)# 文件权限 0644UNIX_FILE_MODE = 0o644def collect_files(skill_dir: str):"""递归收集 skill_dir 下所有文件的相对路径,按字节序排序。"""files = []for root, _dirs, filenames in os.walk(skill_dir):for name in filenames:abs_path = os.path.join(root, name)rel = os.path.relpath(abs_path, skill_dir)# 统一使用 / 作为 ZIP 内路径分隔符rel = rel.replace(os.sep, "/")files.append(rel)# 按路径字节序排序(等价于 LC_ALL=C sort)files.sort(key=lambda s: s.encode("utf-8"))return filesdef pack_skill_dir(skill_dir: str) -> bytes:buf = io.BytesIO()# 压缩级别固定为 6(Python 3.7+ 支持 compresslevel)with zipfile.ZipFile(buf, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=6) as zf:for rel in collect_files(skill_dir):abs_path = os.path.join(skill_dir, rel.replace("/", os.sep))with open(abs_path, "rb") as f:data = f.read()zi = zipfile.ZipInfo(filename=rel, date_time=FIXED_DATE_TIME)zi.compress_type = zipfile.ZIP_DEFLATED# 文件权限 0644 写入 external_attr 高 16 位zi.external_attr = (UNIX_FILE_MODE & 0xFFFF) << 16# 不写入 Extra Field / Commentzi.extra = b""zi.comment = b""zf.writestr(zi, data)return buf.getvalue()def main():if len(sys.argv) < 2:print("Usage: python3 skill_package_to_base64.py <skill_dir>", file=sys.stderr)sys.exit(1)skill_dir = sys.argv[1].rstrip(os.sep)if not os.path.isdir(skill_dir):print(f"Not a directory: {skill_dir}", file=sys.stderr)sys.exit(1)zip_bytes = pack_skill_dir(skill_dir)content_hash = hashlib.sha256(zip_bytes).hexdigest()print("ContentHash:", content_hash)print("FileData (Base64):")print(base64.b64encode(zip_bytes).decode("ascii"))if __name__ == "__main__":main()
4. 如果您使用其他编程语言,可参考下表对应的 ZIP 库与压缩级别设置方式,并参照上述打包规范的所有参数。
语言 | ZIP 库 | 设置压缩级别的方式 |
Node.js | fflate | zipSync(data, { level: 6 }) |
Java | java.util.zip | ZipOutputStream.setLevel(6) |
Rust | zip crate + flate2 crate | ZipWriter::start_file(name, FileOptions::default().compression_level(Some(6))) |