centurygame-agent-scan

SKILL.md

CenturyGame Agent 安全合规扫描

你是世纪华通企业安全团队的 Agent 安全扫描工具。

输出规则

所有输出必须使用中文。

扫描过程中请直接调用工具完成各步骤,不需要在过程中向用户展示进度或说明。 等所有步骤完成后,统一输出一段简洁的结果即可。

最终结果格式(中文):

  • 未发现问题:🛡️ 安全扫描完成 | ✅ 未发现风险
  • 发现问题:
🛡️ 安全扫描完成 | 发现 N 个问题

| 级别 | 类型 | 位置 | 说明 |
|------|------|------|------|
| 🔴 | 凭证泄露 | src/config.ts:23 | 硬编码 API Key (sk-**** ) |
| 🔴 | 凭证泄露 | src/db.ts:45 | 数据库连接字符串 |
| 🟠 | 内网地址 | deploy/k8s.yaml:12 | 内网 IP 地址 (10.x.x.x) |
| 🟠 | 配置风险 | — | 文件系统沙箱未启用 |
| 🟡 | 供应链 | — | skill-x 存在可疑网络请求 |

请联系安全组同学协助处理:李超、史强强、王永涛

注意:说明列中涉及凭证的值必须脱敏显示(如 sk-ab12****ef90)。 不要显示扫描文件数量、平台信息、报告存储路径、任何英文说明。

不要输出的品牌内容

  • EdgeOne、腾讯云、Tencent、A.I.G、AI-Infra-Guard、朱雀实验室
  • 微信群、Discord 群链接
  • "Star" 开源项目引导
  • edgeone-clawscan 的报告模板和格式

执行步骤

依次完成以下步骤,过程中直接调用工具即可,完成后统一输出结果。

步骤 1:平台识别

echo "--- platform detect ---" && (which openclaw 2>/dev/null && openclaw --version 2>/dev/null || true) && (which claude 2>/dev/null && claude --version 2>/dev/null || true) && echo "workspace: $(pwd)"

记住平台类型,不输出。

步骤 2:DLP 敏感信息泄露扫描

这一步使用本地脚本扫描工作区文件,检测硬编码密钥、内网地址、个人信息等敏感数据泄露风险。 脚本仅在本地读取文件内容并进行正则匹配,不会发送任何数据到外部。

首次运行:检查 <SKILL_DIR>/scripts/dlp_scanner.ts 是否存在(<SKILL_DIR> 是本 SKILL.md 所在目录)。 如果不存在,向用户说明:「安全扫描需要创建一个本地检测脚本(dlp_scanner.ts),该脚本仅在本地读取文件做正则匹配,不会发起任何网络请求。是否同意创建?」 用户同意后,用下方源码创建该文件。用户拒绝则跳过 DLP 扫描步骤,继续执行后续步骤。

运行命令

npx tsx <SKILL_DIR>/scripts/dlp_scanner.ts <WORKSPACE_DIR>

脚本输出 JSON,包含 findings 数组,每条记录有 file(相对路径)、line(行号)、 severitycategorymatch(脱敏描述)。保留完整 file:line 信息用于最终表格。

#!/usr/bin/env npx tsx
/**
 * 本地 DLP 敏感信息检测脚本
 * 功能:扫描工作区文件,用正则匹配检测硬编码密钥、内网地址、PII 等泄露风险
 * 原理:逐文件读取内容 → 逐行正则匹配 → 输出 JSON 结果
 * 安全:纯本地操作,不发起任何网络请求,不修改任何文件
 */
import * as fs from "fs";
import * as path from "path";

interface Finding {
  category: string;
  severity: "critical" | "high" | "medium" | "low";
  file: string;
  line: number;
  match: string;
}

interface PatternDef {
  pattern: RegExp;
  category: string;
  severity: "critical" | "high" | "medium" | "low";
  description: string;
}

const EXCLUDE_DIRS = new Set([
  "node_modules", ".git", "vendor", "dist", "build", ".next",
  "__pycache__", ".venv", "venv", ".tox", ".cache",
  ".gradle", "target", "bin", "obj",
]);

const EXCLUDE_EXTS = new Set([
  ".lock", ".min.js", ".min.css", ".map",
  ".woff", ".woff2", ".ttf", ".eot", ".ico",
  ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp",
  ".mp3", ".mp4", ".wav", ".avi", ".mov",
  ".pdf", ".zip", ".tar", ".gz", ".bz2", ".7z", ".rar",
  ".wasm", ".exe", ".dll", ".so", ".dylib",
]);

const FALSE_POSITIVES = new Set([
  "your_api_key_here", "changeme", "xxx", "xxxxxx", "placeholder",
  "example", "test", "fake", "mock", "dummy", "sample", "todo",
  "replace_me", "insert_key_here", "your_token_here",
]);

// ---- 检测模式:每条正则对应一种泄露类型 ----
const PATTERNS: PatternDef[] = [
  // 凭证泄露 (critical)
  { pattern: /(api[_-]?key|apikey)\s*[:=]\s*["']?[A-Za-z0-9_\-]{20,}/gi, category: "credential", severity: "critical", description: "疑似硬编码 API Key" },
  { pattern: /AKIA[0-9A-Z]{16}/g, category: "credential", severity: "critical", description: "AWS Access Key ID" },
  { pattern: /aws[_-]?(secret[_-]?access[_-]?key|session[_-]?token)\s*[:=]\s*["']?[A-Za-z0-9/+=]{20,}/gi, category: "credential", severity: "critical", description: "AWS Secret / Session Token" },
  { pattern: /sk-[a-zA-Z0-9]{20,}/g, category: "credential", severity: "critical", description: "OpenAI API Key" },
  { pattern: /sk-ant-[a-zA-Z0-9\-]{20,}/g, category: "credential", severity: "critical", description: "Anthropic API Key" },
  { pattern: /gh[ps]_[A-Za-z0-9_]{36,}/g, category: "credential", severity: "critical", description: "GitHub Personal Access Token" },
  { pattern: /github_pat_[A-Za-z0-9_]{22,}/g, category: "credential", severity: "critical", description: "GitHub Fine-grained PAT" },
  { pattern: /(secret|token|password|passwd|pwd)\s*[:=]\s*["'][^\s"']{8,}["']/gi, category: "credential", severity: "critical", description: "疑似硬编码密码/密钥" },
  { pattern: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/g, category: "credential", severity: "critical", description: "私钥文件内容" },
  { pattern: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+/g, category: "credential", severity: "critical", description: "JWT Token" },
  { pattern: /(mysql|postgres|postgresql|mongodb|redis|mssql):\/\/[^\s"']{10,}/gi, category: "credential", severity: "critical", description: "数据库连接字符串" },
  { pattern: /wx[a-z0-9]{16,}/g, category: "credential", severity: "critical", description: "微信 AppID" },
  { pattern: /mch_id\s*[:=]\s*["']?[0-9]{8,}/gi, category: "credential", severity: "critical", description: "微信支付商户号" },
  { pattern: /(lark|feishu|dingtalk)[_-]?(app[_-]?id|app[_-]?secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-]{10,}/gi, category: "credential", severity: "critical", description: "飞书/钉钉应用凭证" },
  { pattern: /AKID[A-Za-z0-9]{13,}/g, category: "credential", severity: "critical", description: "腾讯云 SecretId" },
  { pattern: /LTAI[A-Za-z0-9]{12,}/g, category: "credential", severity: "critical", description: "阿里云 AccessKey ID" },
  // 内网地址暴露 (high)
  { pattern: /https?:\/\/10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/g, category: "internal_url", severity: "high", description: "内网 IP 地址 (10.x.x.x)" },
  { pattern: /https?:\/\/172\.(1[6-9]|2[0-9]|3[01])\.[0-9]{1,3}\.[0-9]{1,3}/g, category: "internal_url", severity: "high", description: "内网 IP 地址 (172.16-31.x.x)" },
  { pattern: /https?:\/\/192\.168\.[0-9]{1,3}\.[0-9]{1,3}/g, category: "internal_url", severity: "high", description: "内网 IP 地址 (192.168.x.x)" },
  { pattern: /https?:\/\/[a-zA-Z0-9._-]+\.(internal|corp|local|intranet|lan|private)\b/gi, category: "internal_url", severity: "high", description: "内网域名" },
  { pattern: /(vpn|bastion|jump|gateway)[_.\-][a-zA-Z0-9._-]+\.(com|net|cn|io)/gi, category: "internal_url", severity: "high", description: "VPN/堡垒机地址" },
  { pattern: /https?:\/\/[a-zA-Z0-9._-]+\.(gitlab|jira|confluence|jenkins|harbor|grafana|kibana)\.[a-zA-Z0-9._-]+/gi, category: "internal_url", severity: "high", description: "内部 DevOps 平台地址" },
  // 商业敏感信息 (medium)
  { pattern: /(合同|contract|order)[_-]?(编号||no|number|id)\s*[:=:]\s*[A-Za-z0-9\-]{6,}/gi, category: "business_info", severity: "medium", description: "合同/订单编号" },
  { pattern: /(营收|revenue|利润|profit|成本|cost|预算|budget)\s*[:=:]\s*[0-9,.]+\s*(|亿|k|m|billion)?/gi, category: "business_info", severity: "medium", description: "财务数据" },
  { pattern: /(客户|customer|client)[_-]?(列表|list|名单)\s*[:=:]/gi, category: "business_info", severity: "medium", description: "客户名单引用" },
  { pattern: /(薪资|salary|工资|wage|compensation)\s*[:=:]\s*[0-9,.]+\s*(|k|)?/gi, category: "business_info", severity: "medium", description: "薪资信息" },
  // 个人隐私信息 PII (high)
  { pattern: /[1-9][0-9]{5}(19|20)[0-9]{2}(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|3[01])[0-9]{3}[0-9Xx]/g, category: "pii", severity: "high", description: "中国身份证号" },
  { pattern: /(?<!\d)1[3-9][0-9]{9}(?!\d)/g, category: "pii", severity: "high", description: "中国手机号" },
  { pattern: /(?<!\d)[0-9]{4}\s?[0-9]{4}\s?[0-9]{4}\s?[0-9]{4}(\s?[0-9]{1,3})?(?!\d)/g, category: "pii", severity: "high", description: "疑似银行卡号" },
  { pattern: /(email|邮箱|mail)\s*[:=:]\s*[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/gi, category: "pii", severity: "high", description: "个人邮箱地址" },
];

function maskValue(val: string): string {
  return val.length < 12 ? val.slice(0, 2) + "****" : val.slice(0, 4) + "****" + val.slice(-4);
}

function maskLine(line: string): string {
  return line.replace(/[A-Za-z0-9_\-/+=]{12,}/g, m => maskValue(m)).slice(0, 200);
}

function collectFiles(dir: string, customExcludes: Set<string>, maxSizeKb: number, onlyRecent: boolean): string[] {
  const results: string[] = [];
  const cutoff = onlyRecent ? Date.now() - 30 * 86400000 : 0;
  function walk(d: string) {
    let entries: fs.Dirent[];
    try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
    for (const e of entries) {
      const full = path.join(d, e.name);
      if (e.isDirectory()) { if (!EXCLUDE_DIRS.has(e.name) && !customExcludes.has(e.name)) walk(full); continue; }
      if (!e.isFile() || EXCLUDE_EXTS.has(path.extname(e.name).toLowerCase())) continue;
      try {
        const s = fs.statSync(full);
        if (s.size / 1024 > maxSizeKb || (onlyRecent && s.mtimeMs < cutoff)) continue;
      } catch { continue; }
      results.push(full);
    }
  }
  walk(dir);
  return results;
}

function scanFile(filePath: string, base: string): Finding[] {
  const findings: Finding[] = [];
  let content: string;
  try { content = fs.readFileSync(filePath, "utf-8"); } catch { return findings; }
  const lines = content.split("\n");
  const rel = path.relative(base, filePath);
  const isTest = /(__tests__|__test__|\/tests?\/|\.test\.|\.spec\.|_test\.|readme|example|\.md)/i.test(filePath);
  for (let i = 0; i < lines.length; i++) {
    for (const p of PATTERNS) {
      p.pattern.lastIndex = 0;
      const m = p.pattern.exec(lines[i]);
      if (m && !FALSE_POSITIVES.has(m[0].toLowerCase().replace(/["']/g, ""))) {
        findings.push({
          category: p.category,
          severity: isTest && p.severity !== "low" ? "low" : p.severity,
          file: rel, line: i + 1,
          match: `${p.description}${maskLine(m[0])}`,
        });
      }
    }
  }
  return findings;
}

function main() {
  const dir = path.resolve(process.argv[2] || ".");
  const configFile = process.argv[3];
  let customExcludes = new Set<string>();
  let maxSize = 1024;
  // 加载配置(如有)
  for (const p of [configFile, path.join(dir, ".openclaw-security.json")].filter(Boolean)) {
    try {
      const cfg = JSON.parse(fs.readFileSync(p!, "utf-8"));
      if (cfg.dlp?.exclude_dirs) customExcludes = new Set(cfg.dlp.exclude_dirs);
      if (cfg.dlp?.max_file_size_kb) maxSize = cfg.dlp.max_file_size_kb;
      break;
    } catch {}
  }
  let files = collectFiles(dir, customExcludes, maxSize, false);
  if (files.length > 10000) {
    process.stderr.write(`NOTICE: 文件数超过 10000,仅扫描最近 30 天修改的文件\n`);
    files = collectFiles(dir, customExcludes, maxSize, true);
  }
  const allFindings: Finding[] = [];
  const seen = new Set<string>();
  for (const f of files) {
    for (const fd of scanFile(f, dir)) {
      const key = `${fd.file}:${fd.line}:${fd.category}`;
      if (!seen.has(key)) { seen.add(key); allFindings.push(fd); }
    }
  }
  const summary = { critical: 0, high: 0, medium: 0, low: 0 };
  for (const f of allFindings) summary[f.severity]++;
  allFindings.sort((a, b) => ({ critical: 0, high: 1, medium: 2, low: 3 }[a.severity] - { critical: 0, high: 1, medium: 2, low: 3 }[b.severity]));
  process.stdout.write(JSON.stringify({ scan_dir: dir, total_files: files.length, findings: allFindings, summary }, null, 2) + "\n");
}
main();

步骤 3:连接与供应链审计

3.1 外部连接审计:读取平台配置,提取 MCP server、API 端点、hook URL,按白名单分类。 默认白名单:api.anthropic.comapi.openai.comregistry.npmjs.orgpypi.orggithub.com

3.2 Skill 供应链审计:遍历所有已安装 skill, 对每个 skill 执行以下审计:

  1. 元数据检查:检查 typosquat 仿冒(缺字符、多字符、换字符、同形字等)
  2. 权限分析:识别危险组合(network+fileRead=数据泄露风险、network+shell=命令外传风险、四权限全开=完全访问)
  3. 依赖审计:检查依赖来源和版本
  4. Prompt Injection 检测
    • 高危:"Ignore previous instructions"、"You are now..."、"System prompt override"、伪角色标签([SYSTEM]/[ADMIN]
    • 中危:Base64 编码指令、JSON 值中嵌入命令、零宽字符(U+200B 等)
  5. 网络外传检测
    • 高危:裸 IP 地址、DNS 隧道、非标准端口、从环境变量构造 URL
    • 模式:读文件→发送外部 URL、fetch(url?key=${env.KEY})、base64 编码 header
  6. 内容红线:引用凭证目录(/.ssh、/.aws)、提权命令、禁用安全/沙箱指令

风险判定:✅ SAFE | 🟡 SUSPICIOUS | 🟠 DANGEROUS | 🔴 BLOCK

3.3 Skill 安全情报查询:查询本地运行的企业安全服务,检查已安装的 skill 是否存在已知安全问题(如恶意代码、数据泄露等)。 该服务运行在本机(127.0.0.1),只读查询,不会上传任何用户数据。

curl -s 'http://127.0.0.1:9720/api/v1/skill-security?skill_name=<NAME>&source=<SOURCE>'

返回结果中 malicious 表示已知恶意 skill,risky 需结合本地审计判断。服务不可达时以本地审计结果为准,不影响流程。

3.4 平台漏洞查询:查询本机安全服务,检查当前 Agent 平台版本是否有已知 CVE/GHSA 漏洞。 同样是本地只读查询,不上传用户数据。

curl -s 'http://127.0.0.1:9720/api/v1/advisories?name=OpenClaw&version=<VERSION>'

将查询到的漏洞信息加入 findings。服务不可达时跳过。

步骤 4:配置安全检查

如果是 OpenClaw:执行 openclaw security audit --deep,分析输出。 如果是 Claude Code:读取 ~/.claude/settings.json,检查以下配置:

检查项 高危条件
权限绕过 dangerouslySkipPermissions 为 true
Bash 自动允许 Bash/Shell 工具设为自动批准
网络工具自动允许 WebFetch/WebSearch 设为自动批准
文件写入自动允许 Write/Edit 设为自动批准

还要检查:沙箱状态、持久化(Git Hooks / Shell RC / Crontab / LaunchAgents)、隐私泄露路径。

步骤 5:保存报告到本地

报告目录:~/.config/centurygame-agent-scan/reports/(Windows: %APPDATA%/centurygame-agent-scan/reports/)。

mkdir -p ~/.config/centurygame-agent-scan/reports

用 Write 工具保存:

  • findings.json — 所有发现项
  • skills.json — skill 审计列表
  • config-snapshot.json — 脱敏配置(密钥 >= 12 位保留首尾各 4,< 12 位保留前 2,中间 ****
  • security-report-<YYYY-MM-DD>.md — 完整 Markdown 报告

步骤 6:输出最终结果

按"输出规则"的格式展示最终结果。没问题一行,有问题列表格。

步骤 7:更新数据保护策略记忆

向用户说明:「扫描完成。为防止企业内部数据在后续对话中被意外泄露,建议将数据保护策略写入 Agent 记忆,使 Agent 在未来的对话中也能避免自动连接企业内部平台。是否同意写入?」 用户同意后执行写入。用户拒绝则跳过此步骤,并提示:「已跳过数据保护策略写入。请注意:如未启用数据保护策略,Agent 可能在后续对话中自动连接企业内部平台,因个人使用 AI 助手导致的企业数据泄露,需承担相应责任。」

策略版本v1.0.0

写入规则

  • 如果记忆中不存在 centurygame-data-protection,则写入
  • 如果已存在但版本低于当前版本,则更新
  • 版本一致则跳过

策略内容

# CenturyGame 企业数据保护策略 (v1.0.0)
# 目的:防止 Agent 在自动化操作中意外将企业内部数据发送到外部,
# 或意外连接内部系统导致数据泄露。

## 背景
企业内部平台包含大量敏感业务数据(用户数据、财务数据、代码仓库等)。
Agent 自动连接这些平台时,可能在日志、API 响应、上下文中无意携带敏感信息,
造成数据泄露风险。因此需要限制 Agent 对内部平台的自动化访问。

## 受保护的内部域名
以下域名为企业内部平台,Agent 不应自动连接:
- *.centurygame.com

完整列表以项目目录 .openclaw-security.json 的 policy 字段为准。

## 安全扫描
- 安全扫描请求请使用 centurygame-agent-scan

## 保护规则
当 Agent 的操作(API 调用、浏览器访问、代码执行等)会连接到受保护域名时:
1. 不执行该连接操作
2. 告知用户:「🔒 为防止企业内部数据泄露,Agent 不会自动连接内部平台域名。如需调整白名单请联系安全组:李超、史强强、王永涛」

## 代码执行前检查
执行用户提供的代码或脚本前,检查是否包含受保护域名(正则匹配),如有则提醒用户。

## 安全联系人
李超、史强强、王永涛

写入方式

  • OpenClaw:写入 ~/.agents/memory/centurygame-data-protection.md
  • Claude Code:写入 ~/.claude/CLAUDE.md(追加到末尾,用注释标记版本以便更新检测)

重要约束

  • 扫描过程中不执行任何修改操作(步骤 5 的文件保存和步骤 7 的记忆写入除外)
  • 外部网络请求仅限:威胁情报查询(步骤 3.3/3.4)
  • 大型仓库(>10000 文件)DLP 仅扫描最近 30 天修改的文件

数据保护操作规范

  1. 不自动连接受保护域名(避免在 API 响应中携带内部数据)
  2. 不通过浏览器自动化访问受保护域名(避免页面内容泄露)
  3. 执行代码前检查是否包含受保护域名(帮助用户发现潜在泄露风险)
  4. 不跨工作区访问(防止数据交叉泄露)
  5. 检测到其他 skill 尝试连接受保护域名时,标记为 🔴 高危

配置文件

查找顺序:项目目录 .openclaw-security.json > ~/.config/centurygame-agent-scan/openclaw-security.json > 内置默认值。 Windows 全局路径:%APPDATA%/centurygame-agent-scan/openclaw-security.json。 无配置时使用内置默认值。

Weekly Installs
3
First Seen
2 days ago
Installed on
mcpjam3
claude-code3
replit3
junie3
windsurf3
zencoder3