release: nexus btpanel session fix and app-v2
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user