feat(ql3): export redacted cluster evidence bundle

This commit is contained in:
whyour
2026-08-16 07:07:32 +08:00
parent 7955d55629
commit 4f62a86d15
17 changed files with 1308 additions and 25 deletions
@@ -260,14 +260,22 @@ child.stdout.on('data', (chunk) => {
response.on('data', (chunk) => chunks.push(chunk));
response.once('end', () => {
const body = Buffer.concat(chunks).toString('utf8');
if (response.statusCode !== 200 || !body.includes('Cluster field ledger') || !body.includes('/app.css') || !body.includes('/app.js')) { finish(45); return; }
child.once('close', (status, signal) => {
if (status !== 0 || signal !== null) { finish(46); return; }
settled = true;
clearTimeout(timeout);
process.stdout.write(JSON.stringify({ loopback: true, assets: true, cleanShutdown: true }));
});
child.kill('SIGTERM');
if (response.statusCode !== 200 || !body.includes('Cluster field ledger') || !body.includes('/app.css') || !body.includes('/evidence-bundle.js') || !body.includes('/app.js')) { finish(45); return; }
get(started.origin + '/evidence-bundle.js', (assetResponse) => {
const assetChunks = [];
assetResponse.on('data', (chunk) => assetChunks.push(chunk));
assetResponse.once('end', () => {
const asset = Buffer.concat(assetChunks).toString('utf8');
if (assetResponse.statusCode !== 200 || !asset.includes('qinglong/cluster-console-redacted-evidence-bundle@v1') || !asset.includes('createClusterConsoleEvidenceBundle')) { finish(48); return; }
child.once('close', (status, signal) => {
if (status !== 0 || signal !== null) { finish(46); return; }
settled = true;
clearTimeout(timeout);
process.stdout.write(JSON.stringify({ loopback: true, assets: true, evidenceBundle: true, cleanShutdown: true }));
});
child.kill('SIGTERM');
});
}).once('error', () => finish(49));
});
}).once('error', () => finish(47));
});
@@ -307,6 +315,7 @@ child.stdout.on('data', (chunk) => {
if (
result?.loopback !== true ||
result?.assets !== true ||
result?.evidenceBundle !== true ||
result?.cleanShutdown !== true
) {
fail('Console live contract drifted');
@@ -442,13 +451,17 @@ process.once('SIGINT', () => child.kill('SIGINT'));
process.execPath,
[
'-e',
"require('node:http').get(process.argv[1],(r)=>{const c=[];r.on('data',(x)=>c.push(x));r.on('end',()=>{const b=Buffer.concat(c).toString('utf8');if(r.statusCode!==200||!b.includes('Cluster field ledger'))process.exit(2);process.stdout.write(JSON.stringify({status:r.statusCode,assets:b.includes('/app.css')&&b.includes('/app.js')}));});}).on('error',()=>process.exit(3));",
"const h=require('node:http');h.get(process.argv[1],(r)=>{const c=[];r.on('data',(x)=>c.push(x));r.on('end',()=>{const b=Buffer.concat(c).toString('utf8');if(r.statusCode!==200||!b.includes('Cluster field ledger')||!b.includes('/evidence-bundle.js'))process.exit(2);h.get(process.argv[1]+'/evidence-bundle.js',(a)=>{const d=[];a.on('data',(x)=>d.push(x));a.on('end',()=>{const s=Buffer.concat(d).toString('utf8');if(a.statusCode!==200||!s.includes('createClusterConsoleEvidenceBundle'))process.exit(4);process.stdout.write(JSON.stringify({status:r.statusCode,assets:b.includes('/app.css')&&b.includes('/app.js'),evidenceBundle:true}));});}).on('error',()=>process.exit(5));});}).on('error',()=>process.exit(3));",
origin,
],
{ encoding: 'utf8', timeout: 5_000 },
);
const probeFact = JSON.parse(probe);
if (probeFact.status !== 200 || probeFact.assets !== true) {
if (
probeFact.status !== 200 ||
probeFact.assets !== true ||
probeFact.evidenceBundle !== true
) {
fail('published Console host read drifted');
}
@@ -536,6 +549,7 @@ function main() {
contextReadiness: true,
consoleLoopback: true,
consoleAssets: true,
consoleEvidenceBundle: true,
consolePublishedHostAddress: '127.0.0.1',
consoleDistributionEmbedded: true,
isolation: Object.freeze({
+54 -1
View File
@@ -15,6 +15,7 @@ const REQUIRED_FILES = Object.freeze([
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',
@@ -130,6 +131,8 @@ function auditClusterCopilotConsole(options = {}) {
"'/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'",
@@ -168,12 +171,17 @@ function auditClusterCopilotConsole(options = {}) {
'读取 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', [
@@ -188,6 +196,35 @@ function auditClusterCopilotConsole(options = {}) {
'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)',
@@ -202,6 +239,11 @@ function auditClusterCopilotConsole(options = {}) {
'--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',
@@ -222,6 +264,8 @@ function auditClusterCopilotConsole(options = {}) {
'runPublishedConsoleContract(image);',
'consoleLoopback: true',
'consoleAssets: true',
'consoleEvidenceBundle: true',
"started.origin + '/evidence-bundle.js'",
"consolePublishedHostAddress: '127.0.0.1'",
'consoleDistributionEmbedded: true',
]);
@@ -341,7 +385,16 @@ function auditClusterCopilotConsole(options = {}) {
]),
legacyUiCoupled: false,
kubernetesResident: false,
assetCount: 3,
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',
}),
sourceFileCount: 4,
findings: Object.freeze(findings),
compatible: findings.length === 0,