Files
qinglong/scripts/ql3-cluster-copilot-console-audit.cjs
T

474 lines
14 KiB
JavaScript

'use strict';
const fs = require('node:fs');
const path = require('node:path');
const CONSOLE_ROOT = 'packages/ql3-cluster-admin/src/copilot-console';
const CLIENT_FILE = 'packages/ql3-cluster-admin/src/copilot-client/client.ts';
const ASSET_ROOT = 'packages/ql3-cluster-admin/assets/copilot-console';
const DEPLOYMENT_ROOT = 'deploy/console/ql3-cluster-copilot';
const REQUIRED_FILES = Object.freeze([
CONSOLE_ROOT + '/assets.ts',
CONSOLE_ROOT + '/cli.ts',
CONSOLE_ROOT + '/contracts.ts',
CONSOLE_ROOT + '/evidenceVerifier.ts',
CONSOLE_ROOT + '/evidenceVerifierCli.ts',
CONSOLE_ROOT + '/server.ts',
CLIENT_FILE,
ASSET_ROOT + '/index.html',
ASSET_ROOT + '/app.css',
ASSET_ROOT + '/evidence-bundle.js',
ASSET_ROOT + '/app.js',
DEPLOYMENT_ROOT + '/README.md',
DEPLOYMENT_ROOT + '/client-config.example.json',
'deploy/containers/ql3-cluster-admin/Dockerfile',
'scripts/ql3-cluster-admin-product-live-contract.cjs',
]);
function finding(code, target, detail) {
return Object.freeze({ code, target, detail });
}
function filesBelow(root, relativeDirectory) {
const absolute = path.join(root, relativeDirectory);
const result = [];
const pending = [absolute];
while (pending.length > 0) {
const current = pending.pop();
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
const candidate = path.join(current, entry.name);
if (entry.isDirectory()) pending.push(candidate);
else if (entry.isFile()) result.push(path.relative(root, candidate));
}
}
return result.sort();
}
function auditClusterCopilotConsole(options = {}) {
const root = options.root || path.resolve(__dirname, '..');
const readFile =
options.readFile ||
((relativePath) => fs.readFileSync(path.join(root, relativePath), 'utf8'));
const findings = [];
const source = {};
for (const relativePath of REQUIRED_FILES) {
try {
source[relativePath] = readFile(relativePath);
} catch (error) {
findings.push(
finding(
'CLUSTER_COPILOT_CONSOLE_FILE_MISSING',
relativePath,
error instanceof Error ? error.name : 'Error',
),
);
}
}
const expectFragments = (relativePath, fragments) => {
const contents = source[relativePath];
if (typeof contents !== 'string') return;
for (const fragment of fragments) {
if (!contents.includes(fragment)) {
findings.push(
finding(
'CLUSTER_COPILOT_CONSOLE_CONTRACT_MISSING',
relativePath,
fragment,
),
);
}
}
};
const rejectFragments = (relativePath, fragments) => {
const contents = source[relativePath];
if (typeof contents !== 'string') return;
for (const fragment of fragments) {
if (contents.includes(fragment)) {
findings.push(
finding(
'CLUSTER_COPILOT_CONSOLE_AUTHORITY_WIDENED',
relativePath,
fragment,
),
);
}
}
};
expectFragments(CONSOLE_ROOT + '/contracts.ts', [
'CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS',
'CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA',
'clusterCopilotConsoleClientCommand',
'clusterCopilotConsoleProjectReadPath',
"'run_event_list'",
"'task_read'",
"'workflow_step_list'",
]);
rejectFragments(CONSOLE_ROOT + '/contracts.ts', [
"| 'diagnose'",
"'cancel'",
'mutationId',
'traceId',
'endpoint',
'credential',
]);
expectFragments(CLIENT_FILE, [
'executeClusterProjectApiRead',
'PROJECT_READ_PATHS',
"method: 'GET'",
'readCredentialBytes(record.credentialFile as string)',
'credentialBytes?.fill(0)',
]);
rejectFragments(CLIENT_FILE, ["method: 'DELETE'", "method: 'PATCH'"]);
expectFragments(CONSOLE_ROOT + '/server.ts', [
"networkBoundary === 'host-loopback' ? '127.0.0.1' : '0.0.0.0'",
"networkBoundary === 'container-published-loopback'",
'server.listen(record.port as number, listenAddress',
'request.headers.origin !== expectedOrigin',
"request.headers.host !== expectedOrigin.slice('http://'.length)",
'maximumConcurrentRequests: 2',
"'/api/v1/copilot/inspect': 'inspect'",
"'/api/v1/observe/run-list': 'run_list'",
"'/api/v1/observe/task-list': 'task_list'",
"'/api/v1/observe/workflow-list': 'workflow_list'",
"request.url === '/evidence-bundle.js'",
'assets.evidenceBundle as string',
"default-src 'none'",
"frame-ancestors 'none'",
"'cache-control': 'no-store'",
]);
rejectFragments(CONSOLE_ROOT + '/server.ts', [
'createSecureServer',
'WebSocket',
'set-cookie',
'diagnose',
'cancel',
'child_process',
'node:fs',
'node:net',
]);
expectFragments(CONSOLE_ROOT + '/cli.ts', [
'--session /absolute/session',
'readCanonicalFile(',
"'private'",
'validateClusterCopilotClientCredentialFile',
"clusterCredential: 'server_only'",
'networkBoundary: parsed.networkBoundary',
"publishedHostAddress: '127.0.0.1'",
'operations: CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS',
'mutation: false',
]);
rejectFragments(CONSOLE_ROOT + '/cli.ts', [
'process.env',
'0.0.0.0',
'diagnose',
'cancel',
]);
expectFragments(CONSOLE_ROOT + '/evidenceVerifier.ts', [
'qinglong/cluster-console-evidence-verification@v1',
'maximumBundleBytes: 512 * 1024',
'maximumRawBytes: 8 * 1024 * 1024',
"rawFactDigests: 'not_recomputed_without_raw_facts'",
"serverSignature: 'not_verified'",
"attestation: 'not_verified'",
"durableAudit: 'not_verified'",
"actionAuthority: 'none'",
'networkAccess: false',
'mutation: false',
'fileWrites: false',
'constants.O_RDONLY',
'constants.O_NOFOLLOW',
"createHash('sha256')",
]);
rejectFragments(CONSOLE_ROOT + '/evidenceVerifier.ts', [
'node:http',
'node:https',
'node:net',
'node:child_process',
'writeFile',
'appendFile',
'createWriteStream',
'process.env',
'process.stdin',
'fetch(',
]);
expectFragments(CONSOLE_ROOT + '/evidenceVerifierCli.ts', [
'ql3-copilot-evidence-verify --bundle=/absolute/evidence.json',
'verifyClusterConsoleEvidenceBundleFile',
'process.exitCode = 64',
'process.exitCode = 65',
'Cluster Console evidence bundle verification failed',
]);
rejectFragments(CONSOLE_ROOT + '/evidenceVerifierCli.ts', [
'process.env',
'process.stdin',
'node:http',
'node:https',
'writeFile',
]);
expectFragments(ASSET_ROOT + '/index.html', [
'沿着证据读,不替集群做决定。',
'本机只读 BFF',
'读取 Run 列表',
'读取 Workflow Runs',
'显式读取诊断内容',
'模型文本是不可信内容',
'导出脱敏包',
'/evidence-bundle.js',
]);
expectFragments(ASSET_ROOT + '/app.js', [
"credentials: 'omit'",
"cache: 'no-store'",
'output.textContent = JSON.stringify(fact, null, 2)',
'nextPage(operation, request, fact)',
'measureClusterConsoleEvidenceRecord(record)',
'createClusterConsoleEvidenceBundle',
'URL.revokeObjectURL(objectUrl)',
"sessionToken = ''",
]);
rejectFragments(ASSET_ROOT + '/app.js', [
'localStorage',
'sessionStorage',
'innerHTML',
'eval(',
'new Function',
'WebSocket',
'EventSource',
'diagnose',
'cancel',
'http://',
'https://',
'navigator.',
'setTimeout(',
]);
expectFragments(ASSET_ROOT + '/evidence-bundle.js', [
'qinglong/cluster-console-redacted-evidence-bundle@v1',
'maximumRecords: 16',
'maximumRawBytes: 8 * 1024 * 1024',
'maximumBundleBytes: 512 * 1024',
"generatedBy: 'browser_local'",
"actionAuthority: 'none'",
"attestation: 'none'",
"policy: 'fixed_allowlist_v1'",
'freeTextIncluded: false',
'copilotOutputIncluded: false',
'createClusterConsoleEvidenceBundle',
'verifyClusterConsoleEvidenceBundle',
]);
rejectFragments(ASSET_ROOT + '/evidence-bundle.js', [
'fetch(',
'XMLHttpRequest',
'WebSocket',
'EventSource',
'navigator.',
'localStorage',
'sessionStorage',
'setTimeout(',
'setInterval(',
'http://',
'https://',
]);
expectFragments(ASSET_ROOT + '/app.css', [
'@media (max-width: 520px)',
'@media (prefers-reduced-motion: reduce)',
':focus-visible',
]);
expectFragments(DEPLOYMENT_ROOT + '/README.md', [
'operator-workstation process',
'Do not deploy it as a Kubernetes workload',
'Run, Task, Workflow',
'thirteen exact operations',
'--port=0',
'TLS 1.3 `GET /readyz`',
'excluded from small router Edge/Standalone artifacts',
'Export a redacted evidence bundle',
'Export performs zero BFF or Cluster',
'8 MiB',
'512 KiB',
'server signature',
]);
rejectFragments(DEPLOYMENT_ROOT + '/README.md', [
'--host=0.0.0.0',
'kubectl apply',
'localStorage',
]);
expectFragments('deploy/containers/ql3-cluster-admin/Dockerfile', [
'COPY --from=workspace /workspace/packages/ql3-cluster-admin/assets/copilot-console',
'node_modules/@qinglong/cluster-admin/assets/copilot-console',
]);
expectFragments('scripts/ql3-cluster-admin-product-live-contract.cjs', [
'function runConsoleContract(image)',
"[facade, 'copilot-console'",
"started.event !== 'started'",
"body.includes('Cluster field ledger')",
'runConsoleContract(image);',
'function runPublishedConsoleContract(image)',
'runPublishedConsoleContract(image);',
'consoleLoopback: true',
'consoleAssets: true',
'consoleEvidenceBundle: true',
'evidenceVerifier: true',
'function runEvidenceVerifierContract(image)',
"[facade, 'evidence-verify', '--bundle=' + bundleFile]",
"started.origin + '/evidence-bundle.js'",
"consolePublishedHostAddress: '127.0.0.1'",
'consoleDistributionEmbedded: true',
]);
let manifest;
try {
manifest = JSON.parse(readFile('packages/ql3-cluster-admin/package.json'));
} catch (error) {
findings.push(
finding(
'CLUSTER_COPILOT_CONSOLE_PACKAGE_INVALID',
'packages/ql3-cluster-admin/package.json',
error instanceof Error ? error.name : 'Error',
),
);
}
if (
manifest?.bin?.['ql3-copilot-console'] !== 'dist/copilot-console/cli.js' ||
manifest?.bin?.['ql3-copilot-evidence-verify'] !==
'dist/copilot-console/evidenceVerifierCli.js' ||
manifest?.exports?.['./copilot-console']?.require !==
'./dist/copilot-console/server.js' ||
!Array.isArray(manifest?.files) ||
!manifest.files.includes('assets/copilot-console/*')
) {
findings.push(
finding(
'CLUSTER_COPILOT_CONSOLE_PACKAGE_INVALID',
'packages/ql3-cluster-admin/package.json',
'bin, export or asset packlist drifted',
),
);
}
let productCommand = '';
try {
productCommand = readFile(
'packages/ql3-cluster-admin/src/product-cli/productCommand.ts',
);
} catch {}
if (
!productCommand.includes("name: 'copilot-console'") ||
!productCommand.includes("binary: 'ql3-copilot-console'") ||
!productCommand.includes("target: 'copilot-console/cli.js'") ||
!productCommand.includes("name: 'evidence-verify'") ||
!productCommand.includes("binary: 'ql3-copilot-evidence-verify'") ||
!productCommand.includes("target: 'copilot-console/evidenceVerifierCli.js'")
) {
findings.push(
finding(
'CLUSTER_COPILOT_CONSOLE_PRODUCT_ENTRY_MISSING',
'packages/ql3-cluster-admin/src/product-cli/productCommand.ts',
'static product delegation is incomplete',
),
);
}
for (const relativePath of filesBelow(root, 'src')) {
const contents = readFile(relativePath);
if (
contents.includes('ql3-copilot-console') ||
contents.includes('cluster-copilot-console-read') ||
contents.includes('copilot/failure-diagnoses')
) {
findings.push(
finding(
'CLUSTER_COPILOT_CONSOLE_LEGACY_UI_COUPLED',
relativePath,
'legacy src imports or routes the QingLong 3.0 Console',
),
);
}
}
for (const relativePath of filesBelow(root, 'back')) {
const contents = readFile(relativePath);
if (
contents.includes('ql3-copilot-console') ||
contents.includes('cluster-copilot-console-read')
) {
findings.push(
finding(
'CLUSTER_COPILOT_CONSOLE_LEGACY_BACKEND_COUPLED',
relativePath,
'legacy backend owns the QingLong 3.0 Console',
),
);
}
}
for (const relativePath of filesBelow(root, 'deploy/kubernetes')) {
if (!/\.ya?ml$/u.test(relativePath)) continue;
const contents = readFile(relativePath);
if (contents.includes('ql3-copilot-console')) {
findings.push(
finding(
'CLUSTER_COPILOT_CONSOLE_KUBERNETES_RESIDENT',
relativePath,
'operator-workstation Console must not be a Kubernetes workload',
),
);
}
}
return Object.freeze({
schemaVersion: 1,
component: 'cluster-copilot-console',
owner: '@qinglong/cluster-admin',
lifecycle: 'operator-workstation-loopback',
operations: Object.freeze([
'inspect',
'output',
'run_list',
'run_read',
'run_event_list',
'run_step_list',
'task_list',
'task_read',
'workflow_list',
'workflow_run_list',
'workflow_run_read',
'workflow_event_list',
'workflow_step_list',
]),
legacyUiCoupled: false,
kubernetesResident: false,
assetCount: 4,
evidenceBundle: Object.freeze({
lifecycle: 'browser-local-explicit-export',
maximumRecords: 16,
maximumRawBytes: 8 * 1024 * 1024,
maximumBundleBytes: 512 * 1024,
upstreamReadsOnExport: 0,
attestation: 'none',
actionAuthority: 'none',
}),
offlineVerifier: Object.freeze({
lifecycle: 'operator-local-explicit-file-read',
bundleDigest: 'recomputed',
rawFactDigests: 'not_recomputed_without_raw_facts',
serverSignature: 'not_verified',
mutation: false,
networkAccess: false,
fileWrites: false,
}),
sourceFileCount: 6,
findings: Object.freeze(findings),
compatible: findings.length === 0,
});
}
function main() {
const report = auditClusterCopilotConsole();
process.stdout.write(JSON.stringify(report) + '\n');
if (!report.compatible) process.exitCode = 1;
}
if (require.main === module) main();
module.exports = { auditClusterCopilotConsole };