Files
Ncurator/scripts/package-content-policy.mjs
T
hy a6bf082e0a feat: enhance web crawler sidecar build process and add package content policy
- Updated the build_web_crawler_sidecar.py script to remove full Chromium from bundled browsers and added a function to prune non-runtime files.
- Enhanced create-release-fragment.mjs to validate package security audit data.
- Introduced package-content-policy.mjs to enforce restrictions on forbidden file types and directories in packages.
- Added prepare-package-stage.mjs for staging package preparation, including runtime dependency installation and resource pruning.
- Implemented print-package-audit-summary.mjs to generate a summary of package security audits.
- Created runtime-bundle-graph.mjs to analyze runtime dependencies and collect package names.
- Refactored verify-runtime-bundles.mjs to utilize the new runtime-bundle-graph module for better dependency management.
- Updated release-fragment-contract.test.ts to include tests for package audit integration and validation.
2026-07-15 09:39:19 +08:00

308 lines
8.5 KiB
JavaScript

/* eslint-disable @typescript-eslint/explicit-function-return-type */
import fs from 'node:fs'
import path from 'node:path'
const FORBIDDEN_EXTENSIONS = new Set([
'.adoc',
'.bat',
'.map',
'.ts',
'.tsx',
'.mts',
'.cts',
'.tsbuildinfo',
'.jsx',
'.vue',
'.svelte',
'.py',
'.pyc',
'.pyo',
'.c',
'.cc',
'.cpp',
'.h',
'.hpp',
'.cs',
'.fs',
'.fsx',
'.go',
'.java',
'.kt',
'.kts',
'.m',
'.mm',
'.php',
'.rb',
'.rs',
'.scala',
'.swift',
'.vb',
'.gyp',
'.gypi',
'.gradle',
'.sln',
'.vcxproj',
'.sh',
'.ps1',
'.cmd',
'.flow',
'.jsdoc',
'.patch',
'.orig',
'.md',
'.markdown',
'.rst',
'.key',
'.p12',
'.pfx',
'.pdb'
])
const FORBIDDEN_SEGMENTS = new Set([
'.changeset',
'.circleci',
'.claude',
'.codex',
'.github',
'.git',
'.history',
'.husky',
'.idea',
'.vscode',
'__tests__',
'docs',
'doc',
'coverage',
'examples',
'example',
'patchright',
'tests',
'test',
'scripts'
])
const FORBIDDEN_ROOT_SEGMENTS = new Set(['src', 'tests', 'scripts', 'doc'])
const FORBIDDEN_NAMES = new Set([
'.ds_store',
'.editorconfig',
'.eslintignore',
'.flowconfig',
'.gitattributes',
'.gitignore',
'.gitkeep',
'.keep',
'.nojekyll',
'.npmignore',
'.nycrc',
'.prettierignore',
'.taprc',
'pnpm-lock.yaml',
'pnpm-workspace.yaml',
'package-lock.json',
'yarn.lock',
'tsconfig.json',
'tsconfig.node.json',
'tsconfig.web.json'
])
const SECRET_PATTERNS = [
{
label: 'private key',
pattern:
/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----\r?\n[A-Za-z0-9+/=\r\n]{80,}-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/u
},
{ label: 'AWS access key', pattern: /\bAKIA[0-9A-Z]{16}\b/u },
{
label: 'GitHub token',
pattern: /\b(?:github_pat_[A-Za-z0-9_]{20,}|gh[opusr]_[A-Za-z0-9]{30,})\b/u
},
{ label: 'Stripe live secret', pattern: /\bsk_live_[A-Za-z0-9]{20,}\b/u }
]
const normalizeRelativePath = (value) => value.replaceAll('\\', '/').replace(/^\/+|\/+$/gu, '')
const isLegalNotice = (name) =>
/^(?:license|licence|notice|copying)(?:[._-].*)?$/u.test(name) ||
/^(?:apache2?|bsd|mit)$/u.test(name)
export const assertSafePackagePaths = (paths) => {
const violations = []
const targetPrebuildDirectory = `${process.platform}-${process.arch}`
for (const rawPath of paths) {
const relativePath = normalizeRelativePath(rawPath)
if (!relativePath) continue
const segments = relativePath.split('/')
const lowerSegments = segments.map((segment) => segment.toLowerCase())
const name = lowerSegments.at(-1) || ''
const extension = path.extname(name)
const legalNotice = isLegalNotice(name)
const prebuildsIndex = lowerSegments.indexOf('prebuilds')
const prebuildDirectory = prebuildsIndex >= 0 ? lowerSegments[prebuildsIndex + 1] : undefined
if (
prebuildDirectory &&
/^(?:darwin|linux|win32)-(?:arm64|ia32|x64)$/u.test(prebuildDirectory) &&
prebuildDirectory !== targetPrebuildDirectory
) {
violations.push(`${relativePath}: foreign native prebuild ${prebuildDirectory}`)
continue
}
if (lowerSegments.some((segment) => segment.startsWith('chromium-'))) {
violations.push(`${relativePath}: full Chromium is forbidden; only headless shell is allowed`)
continue
}
if (FORBIDDEN_ROOT_SEGMENTS.has(lowerSegments[0])) {
violations.push(`${relativePath}: forbidden project directory ${lowerSegments[0]}`)
continue
}
if (lowerSegments.some((segment) => segment.startsWith('.env'))) {
violations.push(`${relativePath}: environment file`)
continue
}
const forbiddenSegment = lowerSegments.find((segment) => FORBIDDEN_SEGMENTS.has(segment))
if (forbiddenSegment) {
violations.push(`${relativePath}: forbidden directory ${forbiddenSegment}`)
continue
}
if (
FORBIDDEN_NAMES.has(name) ||
name.startsWith('.eslintrc') ||
name.startsWith('.prettierrc')
) {
violations.push(`${relativePath}: forbidden build file`)
continue
}
if (!legalNotice && FORBIDDEN_EXTENSIONS.has(extension)) {
violations.push(`${relativePath}: forbidden extension ${extension}`)
continue
}
if (/\.(?:spec|test)\.[^.]+$/u.test(name)) {
violations.push(`${relativePath}: test file`)
continue
}
if (name === 'trace.zip' || name.endsWith('.trace')) {
violations.push(`${relativePath}: test trace`)
}
}
if (violations.length > 0) {
throw new Error(`package content policy failed:\n${violations.slice(0, 100).join('\n')}`)
}
}
export const listDirectoryFiles = (rootDirectory) => {
const files = []
const symlinks = []
const walk = (directory) => {
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
const absolutePath = path.join(directory, entry.name)
const relativePath = normalizeRelativePath(path.relative(rootDirectory, absolutePath))
if (entry.isSymbolicLink()) {
symlinks.push({ absolutePath, relativePath, target: fs.readlinkSync(absolutePath) })
} else if (entry.isDirectory()) {
walk(absolutePath)
} else if (entry.isFile()) {
files.push({ absolutePath, relativePath, size: fs.statSync(absolutePath).size })
}
}
}
walk(rootDirectory)
return { files, symlinks }
}
export const assertSafeSymlinks = (rootDirectory, symlinks) => {
const root = path.resolve(rootDirectory)
const violations = []
for (const item of symlinks) {
if (path.isAbsolute(item.target)) {
violations.push(`${item.relativePath}: absolute symlink`)
continue
}
const resolved = path.resolve(path.dirname(item.absolutePath), item.target)
if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) {
violations.push(`${item.relativePath}: symlink escapes package root`)
}
}
if (violations.length > 0) {
throw new Error(`unsafe package symlinks:\n${violations.join('\n')}`)
}
}
export const assertMinimalPackageJson = (packageJson, sourceLabel) => {
const allowedKeys = new Set([
'name',
'version',
'description',
'main',
'author',
'homepage',
'private',
'dependencies'
])
const extraKeys = Object.keys(packageJson).filter((key) => !allowedKeys.has(key))
if (extraKeys.length > 0) {
throw new Error(`${sourceLabel} contains development metadata: ${extraKeys.join(', ')}`)
}
if (packageJson.main !== './out/main/index.js') {
throw new Error(`${sourceLabel} has an unexpected main entry`)
}
if (!packageJson.private || !packageJson.name || !packageJson.version) {
throw new Error(`${sourceLabel} is missing required production metadata`)
}
}
export const scanTextForSecrets = (source, sourceLabel) => {
for (const item of SECRET_PATTERNS) {
if (item.pattern.test(source)) {
throw new Error(`${sourceLabel} contains a possible ${item.label}`)
}
}
if (/(?:\/\/[#@]\s*sourceMappingURL\s*=|\/\*[#@]\s*sourceMappingURL\s*=)/u.test(source)) {
throw new Error(`${sourceLabel} references a source map`)
}
}
export const auditPackageDirectory = (rootDirectory, options = {}) => {
const root = path.resolve(rootDirectory)
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
throw new Error(`package directory not found: ${root}`)
}
const { files, symlinks } = listDirectoryFiles(root)
assertSafePackagePaths([
...files.map((item) => item.relativePath),
...symlinks.map((item) => item.relativePath)
])
assertSafeSymlinks(root, symlinks)
if (options.allowedTopLevel) {
const allowed = new Set(options.allowedTopLevel)
const unexpected = fs.readdirSync(root).filter((name) => !allowed.has(name))
if (unexpected.length > 0) {
throw new Error(`unexpected package root entries: ${unexpected.join(', ')}`)
}
}
const packageJsonPath = path.join(root, 'package.json')
if (options.requireMinimalPackageJson) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
assertMinimalPackageJson(packageJson, packageJsonPath)
}
for (const item of files) {
if (
item.size <= 5 * 1024 * 1024 &&
/\.(?:c?js|mjs|json|html|css|ya?ml|toml|xml|pem|txt)$/u.test(item.relativePath)
) {
scanTextForSecrets(fs.readFileSync(item.absolutePath, 'utf8'), item.relativePath)
}
if (!item.relativePath.startsWith('out/') || item.size > 25 * 1024 * 1024) continue
if (!/\.(?:js|cjs|mjs|json|html|css)$/u.test(item.relativePath)) continue
scanTextForSecrets(fs.readFileSync(item.absolutePath, 'utf8'), item.relativePath)
}
return { files, symlinks }
}