Files
Nexus/frontend-v2/scripts/scan-app-v2-sensitive.mjs
T
2026-07-08 22:31:31 +08:00

146 lines
4.9 KiB
JavaScript

#!/usr/bin/env node
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const appRoot = path.resolve(__dirname, '..')
const defaultTarget = path.resolve(appRoot, '..', 'web', 'app-v2')
const targetDir = path.resolve(process.argv[2] || defaultTarget)
const TEXT_FILE_EXTENSIONS = new Set(['.html', '.js', '.css', '.json', '.txt', '.svg', '.map'])
const MAX_CONTEXT = 110
const checks = [
{
id: 'forbidden-global-api-key-copy',
severity: 'error',
pattern: /全局\s*API\s*Key|Global\s+API\s+Key/gi,
description: 'UI/build output must not expose the global API Key copy.',
},
{
id: 'private-key-block',
severity: 'error',
pattern: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]{0,4096}?-----END [A-Z0-9 ]*PRIVATE KEY-----/g,
description: 'Private key material must never be present in app-v2 output.',
},
{
id: 'certificate-request-private-material',
severity: 'error',
pattern: /-----BEGIN CERTIFICATE REQUEST-----[\s\S]{0,4096}?-----END CERTIFICATE REQUEST-----/g,
description: 'CSR/PEM material should not be bundled into app-v2 output.',
},
{
id: 'jwt-like-token',
severity: 'error',
pattern: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
description: 'JWT-like token detected in app-v2 output.',
},
{
id: 'openai-or-generic-sk-token',
severity: 'error',
pattern: /\bsk-[A-Za-z0-9_-]{20,}\b/g,
description: 'High-confidence secret key token detected.',
},
{
id: 'github-token',
severity: 'error',
pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b|\bgithub_pat_[A-Za-z0-9_]{30,}\b/g,
description: 'GitHub token detected.',
},
{
id: 'aws-access-key',
severity: 'error',
pattern: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g,
description: 'AWS access key id detected.',
},
{
id: 'bearer-token',
severity: 'error',
pattern: /\bBearer\s+(?!\[REDACTED\]|REDACTED|<redacted>)[A-Za-z0-9._~+\/-]{24,}/gi,
description: 'Bearer token detected.',
},
{
id: 'tmp-login-token-value',
severity: 'error',
pattern: /\btmp_(?:login|token)=(?!\[REDACTED\]|REDACTED|<redacted>)[^&\s"'<>]{8,}/gi,
description: 'Baota temporary login token value detected.',
},
{
id: 'url-basic-auth-credentials',
severity: 'error',
pattern: /https?:\/\/[^\s\/:'"<>@]{1,80}:[^\s\/"'<>@]{1,120}@/gi,
description: 'URL contains embedded username/password credentials.',
},
{
id: 'inline-secret-assignment',
severity: 'error',
pattern: /\b(?:password|passwd|pwd|api[_-]?key|access[_-]?key|secret|private[_-]?key|bt[_-]?key)\b["'\s:=]{1,8}(?!\[REDACTED\]|REDACTED|<redacted>|隐藏|脱敏|null|false|true|undefined)[A-Za-z0-9_./+=:@-]{16,}/gi,
description: 'High-confidence inline secret assignment detected.',
},
]
function walk(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true })
const files = []
for (const entry of entries) {
const full = path.join(dir, entry.name)
if (entry.isDirectory()) files.push(...walk(full))
else if (entry.isFile() && TEXT_FILE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full)
}
return files
}
function contextOf(text, index, length) {
const start = Math.max(0, index - MAX_CONTEXT)
const end = Math.min(text.length, index + length + MAX_CONTEXT)
return text
.slice(start, end)
.replace(/[\r\n\t]+/g, ' ')
.replace(/\s{2,}/g, ' ')
}
function redactMatch(match) {
if (match.length <= 18) return match
return match.slice(0, 8) + '...[' + match.length + ' chars]...' + match.slice(-6)
}
if (!fs.existsSync(targetDir)) {
console.error('[app-v2-sensitive-scan] target directory does not exist: ' + targetDir)
process.exit(2)
}
const files = walk(targetDir)
const findings = []
for (const file of files) {
const rel = path.relative(targetDir, file).replace(/\\/g, '/')
const text = fs.readFileSync(file, 'utf8')
for (const check of checks) {
check.pattern.lastIndex = 0
for (const match of text.matchAll(check.pattern)) {
findings.push({
file: rel,
check: check.id,
severity: check.severity,
description: check.description,
offset: match.index ?? 0,
match: redactMatch(match[0]),
context: contextOf(text, match.index ?? 0, match[0].length),
})
}
}
}
if (findings.length > 0) {
console.error('[app-v2-sensitive-scan] FAILED: ' + findings.length + ' finding(s) in ' + targetDir)
for (const finding of findings) {
console.error('- [' + finding.severity + '] ' + finding.check + ' ' + finding.file + '@' + finding.offset)
console.error(' ' + finding.description)
console.error(' match: ' + finding.match)
console.error(' context: ' + finding.context)
}
process.exit(1)
}
console.log('[app-v2-sensitive-scan] OK: scanned ' + files.length + ' file(s), no high-confidence secrets found in ' + targetDir)