Skill 安全检测

最近更新时间:2026-05-26 10:03:32

我的收藏

功能概述

Skill 安全检测是 Web 应用防火墙(WAF)大模型安全模块在 SDK/API 接入模式下提供的 Skill 插件安全扫描能力。当 AI Agent 需要加载外部 Skill 插件时,您可以通过 API 上传 Skill ZIP 包触发异步安全扫描,系统将对 Skill 代码进行安全分析,检测供应链风险、命令注入、数据外传、提示词注入等威胁,并返回风险等级和详细的安全审计结果。
您可以根据检测结果的风险等级,决定是否允许该 Skill 安装或加载到 AI Agent 中,有助于在加载前识别风险较高的 Skill。
说明:
Skill 安全检测仅在 SDK/API 接入模式下可用。您需要先在 WAF 控制台大模型安全 > SDK 接入页面创建应用并获取接入凭证。详情请参见 SDK/API 接入

适用场景

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 main

import (
"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 UTC
var fixedModTime = time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)

func packSkillDir(skillDir string) ([]byte, error) {
// 1. 收集所有文件的相对路径
var files []string
err := 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 / Comment
header.Extra = nil
header.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 base64
import hashlib
import io
import os
import sys
import zipfile

# 固定时间戳:2000-01-01 00:00:00 UTC
FIXED_DATE_TIME = (2000, 1, 1, 0, 0, 0)
# 文件权限 0644
UNIX_FILE_MODE = 0o644


def 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 files


def 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 / Comment
zi.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)))