mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:38:14 +08:00
258 lines
7.3 KiB
JavaScript
258 lines
7.3 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const MAX_INPUT_BYTES = 16 * 1024 * 1024;
|
|
const MAX_ADVISORIES = 10_000;
|
|
const MAX_FINDINGS_PER_ADVISORY = 10_000;
|
|
const MAX_PATHS_PER_FINDING = 10_000;
|
|
const MAX_TEXT_LENGTH = 16_384;
|
|
const BLOCKED_SEVERITIES = new Set(['high', 'critical']);
|
|
const KNOWN_SEVERITIES = new Set([
|
|
'info',
|
|
'low',
|
|
'moderate',
|
|
'high',
|
|
'critical',
|
|
]);
|
|
const PROFILE_IMPORTERS = Object.freeze([
|
|
'packages/ql3-runtime-core',
|
|
'packages/ql3-local-command-file',
|
|
'packages/ql3-local-sqlite',
|
|
'packages/ql3-local-secret',
|
|
'packages/ql3-local-owner-console',
|
|
'packages/ql3-local-owner-cli',
|
|
'packages/ql3-local-owner-maintenance',
|
|
'packages/ql3-local-admin',
|
|
'packages/ql3-local-application',
|
|
'packages/ql3-local-execution',
|
|
'packages/ql3-local-process',
|
|
'packages/ql3-cluster-postgres',
|
|
'packages/ql3-cluster-control',
|
|
'packages/ql3-cluster-admin',
|
|
'packages/ql3-worker-runtime',
|
|
]);
|
|
|
|
class ProfileVulnerabilityAuditError extends Error {
|
|
constructor(message) {
|
|
super(`QingLong profile vulnerability audit failed: ${message}`);
|
|
this.name = 'ProfileVulnerabilityAuditError';
|
|
}
|
|
}
|
|
|
|
function boundedText(value, name) {
|
|
if (
|
|
typeof value !== 'string' ||
|
|
value.length < 1 ||
|
|
value.length > MAX_TEXT_LENGTH ||
|
|
/[\0\r\n]/.test(value)
|
|
) {
|
|
throw new ProfileVulnerabilityAuditError(`${name} is invalid`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function boundedArray(value, maximum, name) {
|
|
if (!Array.isArray(value) || value.length > maximum) {
|
|
throw new ProfileVulnerabilityAuditError(`${name} is invalid`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function emptySummary() {
|
|
return {
|
|
info: new Set(),
|
|
low: new Set(),
|
|
moderate: new Set(),
|
|
high: new Set(),
|
|
critical: new Set(),
|
|
modules: new Set(),
|
|
pathCount: 0,
|
|
};
|
|
}
|
|
|
|
function publicSummary(summary) {
|
|
return Object.freeze({
|
|
advisories: Object.freeze({
|
|
info: summary.info.size,
|
|
low: summary.low.size,
|
|
moderate: summary.moderate.size,
|
|
high: summary.high.size,
|
|
critical: summary.critical.size,
|
|
}),
|
|
modules: Object.freeze([...summary.modules].sort()),
|
|
pathCount: summary.pathCount,
|
|
compatible: summary.high.size === 0 && summary.critical.size === 0,
|
|
});
|
|
}
|
|
|
|
function importerFromPath(dependencyPath) {
|
|
const separator = dependencyPath.indexOf(' > ');
|
|
return separator === -1 ? dependencyPath : dependencyPath.slice(0, separator);
|
|
}
|
|
|
|
function advisoryIdentity(advisory) {
|
|
const value = advisory.github_advisory_id ?? advisory.id;
|
|
if (
|
|
(typeof value !== 'string' && typeof value !== 'number') ||
|
|
String(value).length > 128
|
|
) {
|
|
throw new ProfileVulnerabilityAuditError('advisory identity is invalid');
|
|
}
|
|
return String(value);
|
|
}
|
|
|
|
function auditProfileVulnerabilities(audit) {
|
|
if (!audit || typeof audit !== 'object' || Array.isArray(audit)) {
|
|
throw new ProfileVulnerabilityAuditError('audit document is invalid');
|
|
}
|
|
if (
|
|
!audit.advisories ||
|
|
typeof audit.advisories !== 'object' ||
|
|
Array.isArray(audit.advisories)
|
|
) {
|
|
throw new ProfileVulnerabilityAuditError('advisories are unavailable');
|
|
}
|
|
const advisories = Object.values(audit.advisories);
|
|
if (advisories.length > MAX_ADVISORIES) {
|
|
throw new ProfileVulnerabilityAuditError('advisory budget exceeded');
|
|
}
|
|
|
|
const summaries = new Map([
|
|
['.', emptySummary()],
|
|
...PROFILE_IMPORTERS.map((importer) => [importer, emptySummary()]),
|
|
]);
|
|
const findings = [];
|
|
const findingKeys = new Set();
|
|
const unknownImporters = new Set();
|
|
|
|
for (const advisory of advisories) {
|
|
if (!advisory || typeof advisory !== 'object' || Array.isArray(advisory)) {
|
|
throw new ProfileVulnerabilityAuditError('advisory is invalid');
|
|
}
|
|
const advisoryId = advisoryIdentity(advisory);
|
|
const moduleName = boundedText(advisory.module_name, 'module name');
|
|
const severity = boundedText(advisory.severity, 'severity');
|
|
if (!KNOWN_SEVERITIES.has(severity)) {
|
|
throw new ProfileVulnerabilityAuditError('severity is unsupported');
|
|
}
|
|
const advisoryFindings = boundedArray(
|
|
advisory.findings,
|
|
MAX_FINDINGS_PER_ADVISORY,
|
|
'advisory findings',
|
|
);
|
|
for (const finding of advisoryFindings) {
|
|
if (!finding || typeof finding !== 'object' || Array.isArray(finding)) {
|
|
throw new ProfileVulnerabilityAuditError('finding is invalid');
|
|
}
|
|
const version = boundedText(finding.version, 'finding version');
|
|
const paths = boundedArray(
|
|
finding.paths,
|
|
MAX_PATHS_PER_FINDING,
|
|
'finding paths',
|
|
);
|
|
for (const dependencyPathValue of paths) {
|
|
const dependencyPath = boundedText(
|
|
dependencyPathValue,
|
|
'dependency path',
|
|
);
|
|
const importer = importerFromPath(dependencyPath);
|
|
const summary = summaries.get(importer);
|
|
if (summary) {
|
|
summary[severity].add(advisoryId);
|
|
summary.modules.add(moduleName);
|
|
summary.pathCount += 1;
|
|
} else {
|
|
unknownImporters.add(importer);
|
|
}
|
|
if (
|
|
BLOCKED_SEVERITIES.has(severity) &&
|
|
(importer !== '.' || !summary)
|
|
) {
|
|
const code = summary
|
|
? 'PROFILE_HIGH_CRITICAL_ADVISORY'
|
|
: 'UNREVIEWED_IMPORTER_HIGH_CRITICAL_ADVISORY';
|
|
const findingKey = [
|
|
code,
|
|
importer,
|
|
advisoryId,
|
|
moduleName,
|
|
version,
|
|
].join('\0');
|
|
if (!findingKeys.has(findingKey)) {
|
|
findingKeys.add(findingKey);
|
|
findings.push({
|
|
code,
|
|
importer,
|
|
advisoryId,
|
|
moduleName,
|
|
severity,
|
|
version,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
findings.sort(
|
|
(left, right) =>
|
|
left.importer.localeCompare(right.importer) ||
|
|
left.moduleName.localeCompare(right.moduleName) ||
|
|
left.advisoryId.localeCompare(right.advisoryId),
|
|
);
|
|
const profileImporters = Object.fromEntries(
|
|
PROFILE_IMPORTERS.map((importer) => [
|
|
importer,
|
|
publicSummary(summaries.get(importer)),
|
|
]),
|
|
);
|
|
const legacyRoot = publicSummary(summaries.get('.'));
|
|
return Object.freeze({
|
|
schemaVersion: 1,
|
|
threshold: 'high',
|
|
profileImporters: Object.freeze(profileImporters),
|
|
legacyRoot,
|
|
unknownImporters: Object.freeze([...unknownImporters].sort()),
|
|
findings: Object.freeze(findings),
|
|
compatible: findings.length === 0,
|
|
});
|
|
}
|
|
|
|
function main() {
|
|
let input = '';
|
|
process.stdin.setEncoding('utf8');
|
|
process.stdin.on('data', (chunk) => {
|
|
input += chunk;
|
|
if (Buffer.byteLength(input) > MAX_INPUT_BYTES) {
|
|
process.stdin.destroy(
|
|
new ProfileVulnerabilityAuditError('input budget exceeded'),
|
|
);
|
|
}
|
|
});
|
|
process.stdin.on('end', () => {
|
|
try {
|
|
const report = auditProfileVulnerabilities(JSON.parse(input));
|
|
process.stdout.write(`${JSON.stringify(report)}\n`);
|
|
if (!report.compatible) process.exitCode = 1;
|
|
} catch (error) {
|
|
process.stderr.write(
|
|
`${error instanceof Error ? error.message : String(error)}\n`,
|
|
);
|
|
process.exitCode = 1;
|
|
}
|
|
});
|
|
process.stdin.on('error', (error) => {
|
|
process.stderr.write(
|
|
`${error instanceof Error ? error.message : String(error)}\n`,
|
|
);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
PROFILE_IMPORTERS,
|
|
ProfileVulnerabilityAuditError,
|
|
auditProfileVulnerabilities,
|
|
};
|
|
|
|
if (require.main === module) main();
|