Teach ESLint the classic renderer and mixed CommonJS harness runtimes while retaining actionable rules. Add deterministic credential, lockfile, release-manifest, and lint contracts plus equivalent GitHub and Gitea Windows pipelines. Gate clean installs, focused offline smoke coverage, builds, packaged launches, and silent installer verification without enabling authenticated network tests.
122 lines
4.9 KiB
JavaScript
122 lines
4.9 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const textExtensions = new Set([
|
|
'', '.cjs', '.css', '.html', '.js', '.json', '.md', '.mjs', '.nsh', '.ps1', '.ts', '.tsx', '.txt', '.yaml', '.yml'
|
|
]);
|
|
|
|
const sensitivePatterns = [
|
|
['github-token', /\b(?:gh[pousr]_[A-Za-z0-9]{36,255}|github_pat_[A-Za-z0-9_]{20,255})\b/g],
|
|
['aws-access-key', /\bAKIA[0-9A-Z]{16}\b/g],
|
|
['slack-token', /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g],
|
|
['discord-webhook', /https:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d{8,}\/[A-Za-z0-9._-]{20,}/gi],
|
|
['url-credentials', /https?:\/\/[^\s/@:]+:[^\s/@]+@/gi],
|
|
['windows-user-path', /\b[A-Za-z]:\\Users\\[^\\/\s]+\\/g]
|
|
];
|
|
|
|
function lineNumberAt(source, index) {
|
|
return source.slice(0, index).split('\n').length;
|
|
}
|
|
|
|
function scanText(relativePath, source) {
|
|
const findings = [];
|
|
for (const header of ['PRIVATE KEY', 'RSA PRIVATE KEY', 'EC PRIVATE KEY', 'DSA PRIVATE KEY', 'OPENSSH PRIVATE KEY', 'ENCRYPTED PRIVATE KEY']) {
|
|
const marker = `-----BEGIN ${header}-----`;
|
|
let index = source.indexOf(marker);
|
|
while (index >= 0) {
|
|
findings.push({ file: relativePath, line: lineNumberAt(source, index), rule: 'private-key' });
|
|
index = source.indexOf(marker, index + marker.length);
|
|
}
|
|
}
|
|
for (const [rule, pattern] of sensitivePatterns) {
|
|
pattern.lastIndex = 0;
|
|
for (const match of source.matchAll(pattern)) {
|
|
findings.push({ file: relativePath, line: lineNumberAt(source, match.index ?? 0), rule });
|
|
}
|
|
}
|
|
return findings;
|
|
}
|
|
|
|
function isContainedPath(root, target) {
|
|
const relative = path.relative(root, target);
|
|
return relative !== '' && !relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative);
|
|
}
|
|
|
|
function inspectPublicFiles(root, entries) {
|
|
const findings = [];
|
|
const resolvedRoot = path.resolve(root);
|
|
for (const rawEntry of entries) {
|
|
const entry = String(rawEntry || '').replace(/\\/g, '/');
|
|
const absolutePath = path.resolve(resolvedRoot, ...entry.split('/'));
|
|
if (!entry || path.isAbsolute(entry) || !isContainedPath(resolvedRoot, absolutePath)) {
|
|
findings.push({ file: entry || '<empty>', line: 0, rule: 'manifest-path' });
|
|
continue;
|
|
}
|
|
if (!fs.existsSync(absolutePath)) {
|
|
findings.push({ file: entry, line: 0, rule: 'manifest-missing' });
|
|
continue;
|
|
}
|
|
const stat = fs.lstatSync(absolutePath);
|
|
if (stat.isSymbolicLink()) {
|
|
findings.push({ file: entry, line: 0, rule: 'manifest-symlink' });
|
|
continue;
|
|
}
|
|
if (!stat.isFile()) {
|
|
findings.push({ file: entry, line: 0, rule: 'manifest-file-type' });
|
|
continue;
|
|
}
|
|
if (!textExtensions.has(path.extname(entry).toLowerCase())) continue;
|
|
findings.push(...scanText(entry, fs.readFileSync(absolutePath, 'utf8')));
|
|
}
|
|
return findings;
|
|
}
|
|
|
|
function inspectLockfile(lockfile) {
|
|
const findings = [];
|
|
if (!Number.isInteger(lockfile?.lockfileVersion) || lockfile.lockfileVersion < 3) {
|
|
findings.push({ file: 'package-lock.json', line: 0, rule: 'lockfile-version' });
|
|
}
|
|
const rootPackage = lockfile?.packages?.[''] || {};
|
|
for (const [name, specifier] of Object.entries({
|
|
...(rootPackage.dependencies || {}),
|
|
...(rootPackage.devDependencies || {})
|
|
})) {
|
|
if (/^(?:file:|git(?:\+|:)|https?:)/i.test(String(specifier))) {
|
|
findings.push({ file: 'package-lock.json', line: 0, rule: 'dependency-source', package: name });
|
|
}
|
|
}
|
|
for (const [packagePath, metadata] of Object.entries(lockfile?.packages || {})) {
|
|
if (!packagePath || metadata?.link) continue;
|
|
const resolved = typeof metadata?.resolved === 'string' ? metadata.resolved : '';
|
|
if (/^https:\/\/registry\.npmjs\.org\//i.test(resolved) && !/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(String(metadata.integrity || ''))) {
|
|
findings.push({ file: 'package-lock.json', line: 0, rule: 'dependency-integrity', package: packagePath });
|
|
}
|
|
if (resolved && !/^https:\/\/registry\.npmjs\.org\//i.test(resolved)) {
|
|
findings.push({ file: 'package-lock.json', line: 0, rule: 'dependency-source', package: packagePath });
|
|
}
|
|
}
|
|
return findings;
|
|
}
|
|
|
|
function run(root = process.cwd()) {
|
|
const manifestPath = path.join(root, 'scripts', 'public-release-files.json');
|
|
const lockfilePath = path.join(root, 'package-lock.json');
|
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
const lockfile = JSON.parse(fs.readFileSync(lockfilePath, 'utf8'));
|
|
const entries = Array.isArray(manifest.files) ? manifest.files : [];
|
|
return [...inspectPublicFiles(root, entries), ...inspectLockfile(lockfile)];
|
|
}
|
|
|
|
if (require.main === module) {
|
|
try {
|
|
const failures = run();
|
|
console.log(JSON.stringify({ failures }, null, 2));
|
|
if (failures.length) process.exitCode = 1;
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : String(error));
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
module.exports = { inspectLockfile, inspectPublicFiles, run, scanText };
|