// Package security 提供引擎层的安全能力:秘密扫描与审计日志. // // 设计定位: // // 这是一个零外部依赖的基础包,被 pkg/tools/builtin(工具层) // 和 pkg/engine(引擎层)同时引用,不引入循环依赖. // // 核心能力: // 1. SecretGuard - 写入前扫描内容,检测 API key,密码等敏感信息 // 2. AuditSink - 操作审计落地接口(本地文件,远端等实现由外部注入) // // 升华改进(ELEVATED): 早期设计只保护 TeamMem 同步路径, // 设计出发点是"防止秘密被同步给团队成员". // 我们把安全边界翻转:默认全路径扫描,调用方显式豁免(而不是显式开启). // 这符合"安全默认,显式降级"原则,与 Go httptest.NewServer 等标准库哲学一致. // 替代方案:<路径白名单--只扫描指定路径> - 否决原因:遗漏路径比豁免路径危险得多. // // 升华改进(ELEVATED): 早期设计用 ANT_KEY_PFX = ['sk','ant','api'].join('-') 在运行时 // 拼接 Anthropic key 前缀,目的是防止 JS bundle 被 gitleaks 扫描时误报. // Go 编译产物无此问题(二进制不含源码),直接写明前缀即可,反而更易理解和审计. // 替代方案:<保留运行时拼接> - 否决原因:在 Go 库中无意义,徒增阅读成本. package security // SecretRule 描述一条秘密检测规则. // 规则 ID 来自 gitleaks(https://github.com/gitleaks/gitleaks,MIT 协议), // 只使用高置信度,前缀可辨识的规则. // 通用关键词上下文规则(如 api_key=xxx)被排除--误报率过高,不适合阻断写入. type SecretRule struct { // ID 是规则的唯一标识,对应 gitleaks rule ID(kebab-case). // 例:"github-pat","aws-access-token" ID string // Source 是正则表达式源字符串(Go re2 语法). // 精妙之处(CLEVER): 存储 source 而非编译后的 *regexp.Regexp, // 支持按需惰性编译(只在首次扫描时编译),也方便序列化和热更新. Source string // Flags 是正则 flag,通常为空(大多数规则区分大小写). // 仅当规则需要大小写不敏感时设置为 "i". Flags string } // SecretMatch 是一次扫描命中的结果. // // 精妙之处(CLEVER): 故意不包含实际匹配内容(matched text). // 只返回"发现了什么类型的秘密",不返回"秘密的具体值". // 这保证了即使日志系统出现问题,秘密值也不会意外泄露. // 早期设计同样遵守此原则,我们继承并明确记录原因. type SecretMatch struct { // RuleID 是触发的规则 ID,如 "github-pat". RuleID string // Label 是人类可读的标签,如 "GitHub PAT". // 用于错误消息和审计日志(永远不包含实际秘密值). Label string } // ─── 内置规则集 ────────────────────────────────────────────────────────────── // // 来源:gitleaks 公开配置(MIT 协议),只保留高置信度规则. // 排除原则:没有独特前缀的通用规则(generic-api-key,password 等)一律不入库. // Go 正则注意事项: // - gitleaks 使用 Go re2 语法,直接兼容; // - 边界字符组 (?:[\x60'"\s;]|\\[nr]|$) 已保留(匹配引号/空格/分号/行尾) // // builtinRules 是包级私有变量,外部通过 BuiltinRules() 或 DefaultSecretGuard 访问. var builtinRules = []SecretRule{ // ── 云服务商 ──────────────────────────────────────────────────────────── { ID: "aws-access-token", Source: `\b((?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16})\b`, }, { ID: "gcp-api-key", Source: `\b(AIza[\w-]{35})(?:[\x60'"\s;]|\\[nr]|$)`, }, { ID: "azure-ad-client-secret", Source: `(?:^|[\\'"` + "`" + `\s>=:(,)])([a-zA-Z0-9_~.]{3}\dQ~[a-zA-Z0-9_~.-]{31,34})(?:$|[\\'"` + "`" + `\s<),])`, }, { ID: "digitalocean-pat", Source: `\b(dop_v1_[a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)`, }, { ID: "digitalocean-access-token", Source: `\b(doo_v1_[a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)`, }, // ── AI API ────────────────────────────────────────────────────────────── // 精妙之处(CLEVER): 此处直接写明 Anthropic API key 前缀(sk-ant-api03-), // 而非早期设计的运行时拼接技巧.早期设计是为了防止 JS bundle 被 gitleaks 扫描时 // 误报 bundle 本身含有 Anthropic key--Go 二进制无此问题. { ID: "anthropic-api-key", Source: `\b(sk-ant-api03-[a-zA-Z0-9_\-]{93}AA)(?:[\x60'"\s;]|\\[nr]|$)`, }, { ID: "anthropic-admin-api-key", Source: `\b(sk-ant-admin01-[a-zA-Z0-9_\-]{93}AA)(?:[\x60'"\s;]|\\[nr]|$)`, }, { ID: "openai-api-key", Source: `\b(sk-(?:proj|svcacct|admin)-(?:[A-Za-z0-9_-]{74}|[A-Za-z0-9_-]{58})T3BlbkFJ(?:[A-Za-z0-9_-]{74}|[A-Za-z0-9_-]{58})\b|sk-[a-zA-Z0-9]{20}T3BlbkFJ[a-zA-Z0-9]{20})(?:[\x60'"\s;]|\\[nr]|$)`, }, { ID: "huggingface-access-token", Source: `\b(hf_[a-zA-Z]{34})(?:[\x60'"\s;]|\\[nr]|$)`, }, // ── 版本控制 ──────────────────────────────────────────────────────────── { ID: "github-pat", Source: `ghp_[0-9a-zA-Z]{36}`, }, { ID: "github-fine-grained-pat", Source: `github_pat_\w{82}`, }, { ID: "github-app-token", Source: `(?:ghu|ghs)_[0-9a-zA-Z]{36}`, }, { ID: "github-oauth", Source: `gho_[0-9a-zA-Z]{36}`, }, { ID: "github-refresh-token", Source: `ghr_[0-9a-zA-Z]{36}`, }, { ID: "gitlab-pat", Source: `glpat-[\w-]{20}`, }, { ID: "gitlab-deploy-token", Source: `gldt-[0-9a-zA-Z_\-]{20}`, }, // ── 通信服务 ──────────────────────────────────────────────────────────── { ID: "slack-bot-token", Source: `xoxb-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*`, }, { ID: "slack-user-token", Source: `xox[pe](?:-[0-9]{10,13}){3}-[a-zA-Z0-9-]{28,34}`, }, { ID: "slack-app-token", Source: `xapp-\d-[A-Z0-9]+-\d+-[a-z0-9]+`, Flags: "i", }, { ID: "twilio-api-key", Source: `SK[0-9a-fA-F]{32}`, }, { ID: "sendgrid-api-token", Source: `\b(SG\.[a-zA-Z0-9=_\-.]{66})(?:[\x60'"\s;]|\\[nr]|$)`, }, // ── 开发工具 ──────────────────────────────────────────────────────────── { ID: "npm-access-token", Source: `\b(npm_[a-zA-Z0-9]{36})(?:[\x60'"\s;]|\\[nr]|$)`, }, { ID: "pypi-upload-token", Source: `pypi-AgEIcHlwaS5vcmc[\w-]{50,1000}`, }, { ID: "databricks-api-token", Source: `\b(dapi[a-f0-9]{32}(?:-\d)?)(?:[\x60'"\s;]|\\[nr]|$)`, }, { ID: "hashicorp-tf-api-token", Source: `[a-zA-Z0-9]{14}\.atlasv1\.[a-zA-Z0-9\-_=]{60,70}`, }, { ID: "pulumi-api-token", Source: `\b(pul-[a-f0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)`, }, { ID: "postman-api-token", Source: `\b(PMAK-[a-fA-F0-9]{24}-[a-fA-F0-9]{34})(?:[\x60'"\s;]|\\[nr]|$)`, }, // ── 可观测性平台 ───────────────────────────────────────────────────────── { ID: "grafana-api-key", Source: `\b(eyJrIjoi[A-Za-z0-9+/]{70,400}={0,3})(?:[\x60'"\s;]|\\[nr]|$)`, }, { ID: "grafana-cloud-api-token", Source: `\b(glc_[A-Za-z0-9+/]{32,400}={0,3})(?:[\x60'"\s;]|\\[nr]|$)`, }, { ID: "grafana-service-account-token", Source: `\b(glsa_[A-Za-z0-9]{32}_[A-Fa-f0-9]{8})(?:[\x60'"\s;]|\\[nr]|$)`, }, { ID: "sentry-user-token", Source: `\b(sntryu_[a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)`, }, { ID: "sentry-org-token", Source: `\bsntrys_eyJpYXQiO[a-zA-Z0-9+/]{10,200}(?:LCJyZWdpb25fdXJs|InJlZ2lvbl91cmwi|cmVnaW9uX3VybCI6)[a-zA-Z0-9+/]{10,200}={0,2}_[a-zA-Z0-9+/]{43}`, }, // ── 支付/电商 ──────────────────────────────────────────────────────────── { ID: "stripe-access-token", Source: `\b((?:sk|rk)_(?:test|live|prod)_[a-zA-Z0-9]{10,99})(?:[\x60'"\s;]|\\[nr]|$)`, }, { ID: "shopify-access-token", Source: `shpat_[a-fA-F0-9]{32}`, }, { ID: "shopify-shared-secret", Source: `shpss_[a-fA-F0-9]{32}`, }, // ── 加密/证书 ──────────────────────────────────────────────────────────── { ID: "private-key", Source: `-----BEGIN[ A-Z0-9_-]{0,100}PRIVATE KEY(?: BLOCK)?-----[\s\S-]{64,}?-----END[ A-Z0-9_-]{0,100}PRIVATE KEY(?: BLOCK)?-----`, Flags: "i", }, } // BuiltinRules 返回内置规则集的副本. // 外部可以在此基础上追加自定义规则(仓储,金融等行业特有格式). func BuiltinRules() []SecretRule { cp := make([]SecretRule, len(builtinRules)) copy(cp, builtinRules) return cp } // ruleIDToLabel 将 gitleaks rule ID(kebab-case)转换为人类可读标签. // 例:"github-pat" → "GitHub PAT","aws-access-token" → "AWS Access Token" func ruleIDToLabel(id string) string { // 需要特殊大写处理的词(与 title case 不同) specialCase := map[string]string{ "aws": "AWS", "gcp": "GCP", "api": "API", "pat": "PAT", "ad": "AD", "tf": "TF", "oauth": "OAuth", "npm": "NPM", "pypi": "PyPI", "jwt": "JWT", "github": "GitHub", "gitlab": "GitLab", "openai": "OpenAI", "digitalocean": "DigitalOcean", "huggingface": "HuggingFace", "hashicorp": "HashiCorp", "sendgrid": "SendGrid", "anthropic": "Anthropic", } parts := splitKebab(id) result := make([]string, len(parts)) for i, p := range parts { if special, ok := specialCase[p]; ok { result[i] = special } else { result[i] = capitalize(p) } } return joinWords(result) } // splitKebab 将 kebab-case 字符串按 "-" 分割. func splitKebab(s string) []string { var parts []string start := 0 for i := 0; i < len(s); i++ { if s[i] == '-' { if i > start { parts = append(parts, s[start:i]) } start = i + 1 } } if start < len(s) { parts = append(parts, s[start:]) } return parts } // capitalize 将字符串首字母大写. func capitalize(s string) string { if s == "" { return s } // 精妙之处(CLEVER): 直接操作字节(ASCII 范围内的字母), // 避免引入 unicode 包.规则 ID 均为 ASCII. b := []byte(s) if b[0] >= 'a' && b[0] <= 'z' { b[0] -= 32 } return string(b) } // joinWords 将单词列表用空格连接. func joinWords(words []string) string { if len(words) == 0 { return "" } n := len(words) - 1 for _, w := range words { n += len(w) } b := make([]byte, 0, n) for i, w := range words { if i > 0 { b = append(b, ' ') } b = append(b, w...) } return string(b) }