mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 22:21:20 +00:00
527 lines
13 KiB
JavaScript
527 lines
13 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const ROOT_DIR = path.resolve(__dirname, '..');
|
|
const SRC_DIR = path.join(ROOT_DIR, 'src');
|
|
const REPORT_DIR = path.join(ROOT_DIR, 'docs', 'reports');
|
|
const JSON_REPORT_PATH = path.join(REPORT_DIR, 'hardening-inventory.json');
|
|
const MD_REPORT_PATH = path.join(REPORT_DIR, 'hardening-inventory.md');
|
|
|
|
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
|
|
const HOTPATH_PATTERNS = [
|
|
/^src\/web-server\//,
|
|
/^src\/commands\//,
|
|
/^src\/cliproxy\//,
|
|
/^src\/management\//,
|
|
/^src\/auth\//,
|
|
/^src\/delegation\//,
|
|
/^src\/utils\//,
|
|
/^src\/ccs\.ts$/,
|
|
];
|
|
|
|
const SYNC_CALL_NAMES = [
|
|
'accessSync',
|
|
'appendFileSync',
|
|
'chmodSync',
|
|
'chownSync',
|
|
'closeSync',
|
|
'copyFileSync',
|
|
'cpSync',
|
|
'existsSync',
|
|
'fstatSync',
|
|
'fsyncSync',
|
|
'ftruncateSync',
|
|
'futimesSync',
|
|
'lchmodSync',
|
|
'lchownSync',
|
|
'linkSync',
|
|
'lstatSync',
|
|
'mkdirSync',
|
|
'mkdtempSync',
|
|
'openSync',
|
|
'opendirSync',
|
|
'readFileSync',
|
|
'readdirSync',
|
|
'readlinkSync',
|
|
'readSync',
|
|
'readvSync',
|
|
'realpathSync',
|
|
'renameSync',
|
|
'rmSync',
|
|
'rmdirSync',
|
|
'statSync',
|
|
'symlinkSync',
|
|
'truncateSync',
|
|
'unlinkSync',
|
|
'utimesSync',
|
|
'writeFileSync',
|
|
'writeSync',
|
|
'writevSync',
|
|
];
|
|
const SYNC_CALL_CAPTURE_REGEX = new RegExp(
|
|
`(?:\\bfs(?:\\s*\\?\\.)?\\s*\\.\\s*|(?<![\\w$.]))(${SYNC_CALL_NAMES.join('|')})\\s*\\(`,
|
|
'g'
|
|
);
|
|
const LEGACY_MARKER_REGEX =
|
|
/(?:\blegacy\b|\bshim\b|backward compatibility|backwards compatibility|compatibility layer|deprecated.*re-export|re-export.*compatibility)/i;
|
|
const REGEX_LITERAL_KEYWORDS = new Set([
|
|
'return',
|
|
'throw',
|
|
'case',
|
|
'else',
|
|
'do',
|
|
'delete',
|
|
'void',
|
|
'typeof',
|
|
'instanceof',
|
|
'in',
|
|
'of',
|
|
'yield',
|
|
'await',
|
|
'new',
|
|
]);
|
|
|
|
function toPosixPath(filePath) {
|
|
return filePath.split(path.sep).join('/');
|
|
}
|
|
|
|
function relativePath(filePath) {
|
|
return toPosixPath(path.relative(ROOT_DIR, filePath));
|
|
}
|
|
|
|
function isSourceFile(filePath) {
|
|
return SOURCE_EXTENSIONS.has(path.extname(filePath));
|
|
}
|
|
|
|
function isHotpath(filePath) {
|
|
return HOTPATH_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
}
|
|
|
|
function walkFiles(dirPath) {
|
|
const output = [];
|
|
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(dirPath, entry.name);
|
|
if (entry.isDirectory()) {
|
|
output.push(...walkFiles(fullPath));
|
|
continue;
|
|
}
|
|
|
|
if (entry.isFile() && isSourceFile(fullPath)) {
|
|
output.push(fullPath);
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
function uniqueSorted(values) {
|
|
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
}
|
|
|
|
function sortByCountDesc(items) {
|
|
return [...items].sort((a, b) => {
|
|
if (b.count !== a.count) return b.count - a.count;
|
|
return a.file.localeCompare(b.file);
|
|
});
|
|
}
|
|
|
|
function summarize(items, limit = 10) {
|
|
return sortByCountDesc(items)
|
|
.slice(0, limit)
|
|
.map((item) => ({
|
|
file: item.file,
|
|
count: item.count,
|
|
calls: uniqueSorted(item.calls || []),
|
|
markers: uniqueSorted(item.markers || []),
|
|
}));
|
|
}
|
|
|
|
function isRegexLiteralStart(previousSignificantChar, previousIdentifier) {
|
|
return (
|
|
previousSignificantChar === '' ||
|
|
'([{:;,=!?+-*%^&|~<>'.includes(previousSignificantChar) ||
|
|
REGEX_LITERAL_KEYWORDS.has(previousIdentifier)
|
|
);
|
|
}
|
|
|
|
function stripComments(sourceText) {
|
|
let output = '';
|
|
let index = 0;
|
|
let inLineComment = false;
|
|
let inBlockComment = false;
|
|
let inSingleQuote = false;
|
|
let inDoubleQuote = false;
|
|
let inTemplateLiteral = false;
|
|
let inRegexLiteral = false;
|
|
let inRegexCharClass = false;
|
|
let previousSignificantChar = '';
|
|
let previousIdentifier = '';
|
|
|
|
while (index < sourceText.length) {
|
|
const current = sourceText[index];
|
|
const next = sourceText[index + 1];
|
|
|
|
if (inLineComment) {
|
|
if (current === '\n' || current === '\r') {
|
|
inLineComment = false;
|
|
output += current;
|
|
} else {
|
|
output += ' ';
|
|
}
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (inBlockComment) {
|
|
if (current === '*' && next === '/') {
|
|
output += ' ';
|
|
index += 2;
|
|
inBlockComment = false;
|
|
continue;
|
|
}
|
|
|
|
output += current === '\n' || current === '\r' ? current : ' ';
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (inRegexLiteral) {
|
|
if (current === '\n' || current === '\r') {
|
|
output += current;
|
|
index += 1;
|
|
inRegexLiteral = false;
|
|
inRegexCharClass = false;
|
|
continue;
|
|
}
|
|
|
|
output += ' ';
|
|
|
|
if (current === '\\') {
|
|
output += next === '\n' || next === '\r' ? next : ' ';
|
|
index += 2;
|
|
continue;
|
|
}
|
|
|
|
if (!inRegexCharClass && current === '[') {
|
|
inRegexCharClass = true;
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (inRegexCharClass && current === ']') {
|
|
inRegexCharClass = false;
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (!inRegexCharClass && current === '/') {
|
|
index += 1;
|
|
while (index < sourceText.length && /[a-z]/i.test(sourceText[index])) {
|
|
output += ' ';
|
|
index += 1;
|
|
}
|
|
inRegexLiteral = false;
|
|
previousSignificantChar = 'r';
|
|
previousIdentifier = '';
|
|
continue;
|
|
}
|
|
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (inSingleQuote) {
|
|
output += current === '\n' || current === '\r' ? current : ' ';
|
|
if (current === '\\') {
|
|
output += next === '\n' || next === '\r' ? next : ' ';
|
|
index += 2;
|
|
continue;
|
|
}
|
|
if (current === "'") {
|
|
inSingleQuote = false;
|
|
previousSignificantChar = 's';
|
|
previousIdentifier = '';
|
|
}
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (inDoubleQuote) {
|
|
output += current === '\n' || current === '\r' ? current : ' ';
|
|
if (current === '\\') {
|
|
output += next === '\n' || next === '\r' ? next : ' ';
|
|
index += 2;
|
|
continue;
|
|
}
|
|
if (current === '"') {
|
|
inDoubleQuote = false;
|
|
previousSignificantChar = 's';
|
|
previousIdentifier = '';
|
|
}
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (inTemplateLiteral) {
|
|
output += current === '\n' || current === '\r' ? current : ' ';
|
|
if (current === '\\') {
|
|
output += next === '\n' || next === '\r' ? next : ' ';
|
|
index += 2;
|
|
continue;
|
|
}
|
|
if (current === '`') {
|
|
inTemplateLiteral = false;
|
|
previousSignificantChar = 's';
|
|
previousIdentifier = '';
|
|
}
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (current === '/' && next === '/') {
|
|
output += ' ';
|
|
index += 2;
|
|
inLineComment = true;
|
|
continue;
|
|
}
|
|
|
|
if (current === '/' && next === '*') {
|
|
output += ' ';
|
|
index += 2;
|
|
inBlockComment = true;
|
|
continue;
|
|
}
|
|
|
|
if (current === '/' && isRegexLiteralStart(previousSignificantChar, previousIdentifier)) {
|
|
output += ' ';
|
|
index += 1;
|
|
inRegexLiteral = true;
|
|
inRegexCharClass = false;
|
|
continue;
|
|
}
|
|
|
|
if (/[A-Za-z_$]/.test(current)) {
|
|
let tokenEnd = index + 1;
|
|
while (tokenEnd < sourceText.length && /[A-Za-z0-9_$]/.test(sourceText[tokenEnd])) {
|
|
tokenEnd += 1;
|
|
}
|
|
const token = sourceText.slice(index, tokenEnd);
|
|
output += token;
|
|
previousSignificantChar = 'i';
|
|
previousIdentifier = token;
|
|
index = tokenEnd;
|
|
continue;
|
|
}
|
|
|
|
if (current === "'") {
|
|
inSingleQuote = true;
|
|
output += ' ';
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (current === '"') {
|
|
inDoubleQuote = true;
|
|
output += ' ';
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (current === '`') {
|
|
inTemplateLiteral = true;
|
|
output += ' ';
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
output += current;
|
|
if (!/\s/.test(current)) {
|
|
previousSignificantChar = current;
|
|
previousIdentifier = '';
|
|
}
|
|
index += 1;
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
function collectSyncCallSites(sourceText) {
|
|
const sanitizedSource = stripComments(sourceText);
|
|
const captureRegex = new RegExp(SYNC_CALL_CAPTURE_REGEX.source, SYNC_CALL_CAPTURE_REGEX.flags);
|
|
const calls = [];
|
|
|
|
for (const match of sanitizedSource.matchAll(captureRegex)) {
|
|
calls.push(match[1]);
|
|
}
|
|
|
|
return {
|
|
count: calls.length,
|
|
calls,
|
|
};
|
|
}
|
|
|
|
function buildReport() {
|
|
const files = walkFiles(SRC_DIR);
|
|
const syncEntries = [];
|
|
const legacyEntries = [];
|
|
|
|
for (const fullPath of files) {
|
|
const file = relativePath(fullPath);
|
|
const sourceText = fs.readFileSync(fullPath, 'utf8');
|
|
const lines = sourceText.split(/\r?\n/);
|
|
const { count: syncCount, calls: syncCalls } = collectSyncCallSites(sourceText);
|
|
let legacyCount = 0;
|
|
const legacyMarkers = [];
|
|
|
|
for (const line of lines) {
|
|
if (LEGACY_MARKER_REGEX.test(line)) {
|
|
legacyCount += 1;
|
|
const normalized = line.trim();
|
|
if (normalized.length > 0) {
|
|
legacyMarkers.push(normalized);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (syncCount > 0) {
|
|
syncEntries.push({
|
|
file,
|
|
count: syncCount,
|
|
calls: syncCalls,
|
|
hotpath: isHotpath(file),
|
|
});
|
|
}
|
|
|
|
if (legacyCount > 0) {
|
|
legacyEntries.push({
|
|
file,
|
|
count: legacyCount,
|
|
markers: legacyMarkers,
|
|
});
|
|
}
|
|
}
|
|
|
|
const syncHotpathEntries = syncEntries.filter((entry) => entry.hotpath);
|
|
const totalSyncCount = syncEntries.reduce((acc, entry) => acc + entry.count, 0);
|
|
const totalSyncHotpathCount = syncHotpathEntries.reduce((acc, entry) => acc + entry.count, 0);
|
|
const totalLegacyMarkers = legacyEntries.reduce((acc, entry) => acc + entry.count, 0);
|
|
|
|
return {
|
|
scope: 'src/**/*.{ts,tsx,js,jsx,mjs,cjs}',
|
|
syncFs: {
|
|
totalOccurrences: totalSyncCount,
|
|
filesAffected: syncEntries.length,
|
|
hotpathOccurrences: totalSyncHotpathCount,
|
|
hotpathFilesAffected: syncHotpathEntries.length,
|
|
topHotpathFiles: summarize(syncHotpathEntries),
|
|
topFilesOverall: summarize(syncEntries),
|
|
},
|
|
legacyShim: {
|
|
totalMarkers: totalLegacyMarkers,
|
|
filesAffected: legacyEntries.length,
|
|
topFiles: summarize(legacyEntries),
|
|
explicitShimFiles: uniqueSorted(
|
|
legacyEntries
|
|
.map((entry) => entry.file)
|
|
.filter((file) => /shim|re-export|compat/i.test(path.basename(file)))
|
|
),
|
|
},
|
|
};
|
|
}
|
|
|
|
function renderMarkdown(report) {
|
|
const lines = [];
|
|
|
|
lines.push('# Hardening Inventory Report');
|
|
lines.push('');
|
|
lines.push(`Scope: \`${report.scope}\``);
|
|
lines.push('');
|
|
lines.push('## Summary');
|
|
lines.push('');
|
|
lines.push('| Metric | Value |');
|
|
lines.push('|---|---:|');
|
|
lines.push(`| Sync fs occurrences (all) | ${report.syncFs.totalOccurrences} |`);
|
|
lines.push(`| Sync fs files affected (all) | ${report.syncFs.filesAffected} |`);
|
|
lines.push(`| Sync fs occurrences (runtime hotpaths) | ${report.syncFs.hotpathOccurrences} |`);
|
|
lines.push(`| Sync fs files affected (runtime hotpaths) | ${report.syncFs.hotpathFilesAffected} |`);
|
|
lines.push(`| Legacy shim markers | ${report.legacyShim.totalMarkers} |`);
|
|
lines.push(`| Legacy shim files affected | ${report.legacyShim.filesAffected} |`);
|
|
lines.push('');
|
|
|
|
lines.push('## Top Runtime Hotpath Sync fs Files');
|
|
lines.push('');
|
|
lines.push('| File | Sync Calls | API Names |');
|
|
lines.push('|---|---:|---|');
|
|
|
|
for (const item of report.syncFs.topHotpathFiles) {
|
|
lines.push(`| \`${item.file}\` | ${item.count} | ${item.calls.join(', ')} |`);
|
|
}
|
|
|
|
if (report.syncFs.topHotpathFiles.length === 0) {
|
|
lines.push('| _none_ | 0 | - |');
|
|
}
|
|
|
|
lines.push('');
|
|
lines.push('## Top Legacy Shim Marker Files');
|
|
lines.push('');
|
|
lines.push('| File | Marker Count |');
|
|
lines.push('|---|---:|');
|
|
|
|
for (const item of report.legacyShim.topFiles) {
|
|
lines.push(`| \`${item.file}\` | ${item.count} |`);
|
|
}
|
|
|
|
if (report.legacyShim.topFiles.length === 0) {
|
|
lines.push('| _none_ | 0 |');
|
|
}
|
|
|
|
lines.push('');
|
|
lines.push('## Explicit Shim/Re-export Files');
|
|
lines.push('');
|
|
for (const file of report.legacyShim.explicitShimFiles) {
|
|
lines.push(`- \`${file}\``);
|
|
}
|
|
if (report.legacyShim.explicitShimFiles.length === 0) {
|
|
lines.push('- _none_');
|
|
}
|
|
|
|
lines.push('');
|
|
return lines.join('\n');
|
|
}
|
|
|
|
function main() {
|
|
const report = buildReport();
|
|
|
|
fs.mkdirSync(REPORT_DIR, { recursive: true });
|
|
fs.writeFileSync(JSON_REPORT_PATH, JSON.stringify(report, null, 2) + '\n', 'utf8');
|
|
fs.writeFileSync(MD_REPORT_PATH, renderMarkdown(report), 'utf8');
|
|
|
|
const relJson = relativePath(JSON_REPORT_PATH);
|
|
const relMd = relativePath(MD_REPORT_PATH);
|
|
|
|
console.log(`[hardening-inventory] generatedAt=${new Date().toISOString()}`);
|
|
console.log(
|
|
`[hardening-inventory] sync-fs total=${report.syncFs.totalOccurrences}, hotpath=${report.syncFs.hotpathOccurrences}`
|
|
);
|
|
console.log(
|
|
`[hardening-inventory] legacy markers total=${report.legacyShim.totalMarkers}, files=${report.legacyShim.filesAffected}`
|
|
);
|
|
console.log(`[hardening-inventory] wrote ${relJson}`);
|
|
console.log(`[hardening-inventory] wrote ${relMd}`);
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main();
|
|
}
|
|
|
|
module.exports = {
|
|
buildReport,
|
|
collectSyncCallSites,
|
|
renderMarkdown,
|
|
stripComments,
|
|
};
|