mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
551 lines
16 KiB
JavaScript
551 lines
16 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('node:assert/strict');
|
|
const crypto = require('node:crypto');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const test = require('node:test');
|
|
const {
|
|
auditClusterOciLayout,
|
|
} = require('../../scripts/ql3-cluster-oci-layout-audit.cjs');
|
|
const {
|
|
mergeNativeLayouts,
|
|
nativeEvidenceRecord,
|
|
} = require('../../scripts/ql3-image-release-bundle.cjs');
|
|
const {
|
|
createClusterImageSbom,
|
|
} = require('../../scripts/ql3-cluster-image-sbom.cjs');
|
|
|
|
const root = path.resolve(__dirname, '../..');
|
|
const revision = 'fixture-revision';
|
|
|
|
function createFixture(t, options = {}) {
|
|
const image = options.image || 'control';
|
|
const isControl = image === 'control' || image === 'control-ai';
|
|
const isControlAi = image === 'control-ai';
|
|
const isLocal = image === 'local';
|
|
const layoutRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-oci-layout-'));
|
|
t.after(() => fs.rmSync(layoutRoot, { recursive: true, force: true }));
|
|
const blobDirectory = path.join(layoutRoot, 'blobs', 'sha256');
|
|
fs.mkdirSync(blobDirectory, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(layoutRoot, 'oci-layout'),
|
|
JSON.stringify({ imageLayoutVersion: '1.0.0' }),
|
|
);
|
|
|
|
function blob(value, mediaType) {
|
|
const content = Buffer.isBuffer(value)
|
|
? value
|
|
: Buffer.from(JSON.stringify(value));
|
|
const digest = `sha256:${crypto
|
|
.createHash('sha256')
|
|
.update(content)
|
|
.digest('hex')}`;
|
|
fs.writeFileSync(
|
|
path.join(blobDirectory, digest.slice('sha256:'.length)),
|
|
content,
|
|
);
|
|
return { mediaType, digest, size: content.length };
|
|
}
|
|
|
|
const expectedComponents = createClusterImageSbom({
|
|
root,
|
|
image,
|
|
}).components;
|
|
const applicationPackages = expectedComponents.map((component) => ({
|
|
name: component.name,
|
|
versionInfo: component.version,
|
|
sourceInfo: `acquired package info from installed node module manifest file: /opt/qinglong/node_modules/${component.name}/package.json`,
|
|
externalRefs: [
|
|
{
|
|
referenceCategory: 'PACKAGE-MANAGER',
|
|
referenceType: 'purl',
|
|
referenceLocator: component.purl,
|
|
},
|
|
],
|
|
}));
|
|
if (options.addDevelopmentPackage) {
|
|
applicationPackages.push({
|
|
name: '@types/node',
|
|
versionInfo: '24.13.3',
|
|
sourceInfo:
|
|
'acquired package info from installed node module manifest file: /opt/qinglong/node_modules/@types/node/package.json',
|
|
externalRefs: [
|
|
{
|
|
referenceCategory: 'PACKAGE-MANAGER',
|
|
referenceType: 'purl',
|
|
referenceLocator: 'pkg:npm/%40types/node@24.13.3',
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
const descriptors = [];
|
|
const imageDescriptors = [];
|
|
for (const architecture of options.onlyArchitecture
|
|
? [options.onlyArchitecture]
|
|
: options.omitArm64
|
|
? ['amd64']
|
|
: ['amd64', 'arm64']) {
|
|
const config = blob(
|
|
{
|
|
architecture,
|
|
os: 'linux',
|
|
config: {
|
|
User:
|
|
options.rootArm64 && architecture === 'arm64'
|
|
? '0:0'
|
|
: isLocal
|
|
? '65532:65532'
|
|
: '10001:10001',
|
|
...(isControl ? { ExposedPorts: { '5800/tcp': {} } } : {}),
|
|
Env: [
|
|
'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
|
|
'NODE_VERSION=24.18.0',
|
|
'YARN_VERSION=1.22.22',
|
|
'NODE_ENV=production',
|
|
],
|
|
Entrypoint: [
|
|
'node',
|
|
isLocal
|
|
? '/opt/qinglong/node_modules/@qinglong/local-application/dist/cli.js'
|
|
: isControl
|
|
? isControlAi
|
|
? '/opt/qinglong/node_modules/@qinglong/cluster-control/dist/aiCli.js'
|
|
: '/opt/qinglong/node_modules/@qinglong/cluster-control/dist/cli.js'
|
|
: '/opt/qinglong/node_modules/@qinglong/cluster-admin/dist/product-cli/cli.js',
|
|
],
|
|
WorkingDir: '/opt/qinglong',
|
|
Labels: {
|
|
...(isLocal
|
|
? {
|
|
'io.qinglong.ai': 'excluded',
|
|
'io.qinglong.local.application-config': '2',
|
|
'io.qinglong.local.compose-selection': '1',
|
|
'io.qinglong.local.sqlite-contract-max': '41',
|
|
'io.qinglong.local.sqlite-contract-min': '41',
|
|
'io.qinglong.local.sqlite-write-contract': '41',
|
|
'io.qinglong.profile': 'edge,standalone',
|
|
}
|
|
: {}),
|
|
'org.opencontainers.image.description': isLocal
|
|
? 'QingLong 3.0 AI-excluded Edge and Standalone runtime'
|
|
: isControl
|
|
? isControlAi
|
|
? 'Optional QingLong 3.0 AI-enabled cluster control plane'
|
|
: 'QingLong 3.0 PostgreSQL-backed cluster control plane'
|
|
: 'QingLong 3.0 cluster operations and bounded Copilot surfaces',
|
|
'org.opencontainers.image.licenses': 'Apache-2.0',
|
|
'org.opencontainers.image.revision': revision,
|
|
'org.opencontainers.image.source':
|
|
'https://github.com/whyour/qinglong',
|
|
'org.opencontainers.image.title': isLocal
|
|
? 'QingLong 3.0 Local Application'
|
|
: isControl
|
|
? isControlAi
|
|
? 'QingLong 3.0 Cluster Control AI'
|
|
: 'QingLong 3.0 Cluster Control'
|
|
: 'QingLong 3.0 Cluster Admin',
|
|
...(isLocal
|
|
? {
|
|
'org.opencontainers.image.version': '3.0.0-alpha.0',
|
|
}
|
|
: {}),
|
|
},
|
|
},
|
|
rootfs: {
|
|
type: 'layers',
|
|
diff_ids: [
|
|
`sha256:${(architecture === 'amd64' ? 'a' : 'b').repeat(64)}`,
|
|
],
|
|
},
|
|
},
|
|
'application/vnd.oci.image.config.v1+json',
|
|
);
|
|
const layer = blob(
|
|
Buffer.from(`fixture-${architecture}`),
|
|
'application/vnd.oci.image.layer.v1.tar+gzip',
|
|
);
|
|
const manifest = blob(
|
|
{
|
|
schemaVersion: 2,
|
|
mediaType: 'application/vnd.oci.image.manifest.v1+json',
|
|
config,
|
|
layers: [layer],
|
|
},
|
|
'application/vnd.oci.image.manifest.v1+json',
|
|
);
|
|
const descriptor = {
|
|
...manifest,
|
|
platform: { architecture, os: 'linux' },
|
|
};
|
|
imageDescriptors.push(descriptor);
|
|
descriptors.push(descriptor);
|
|
}
|
|
|
|
for (const imageDescriptor of imageDescriptors) {
|
|
const spdx = blob(
|
|
{
|
|
_type: 'https://in-toto.io/Statement/v1',
|
|
subject: [],
|
|
predicateType: 'https://spdx.dev/Document',
|
|
predicate: {
|
|
spdxVersion: 'SPDX-2.3',
|
|
packages: applicationPackages,
|
|
},
|
|
},
|
|
'application/vnd.in-toto+json',
|
|
);
|
|
spdx.annotations = {
|
|
'in-toto.io/predicate-type': 'https://spdx.dev/Document',
|
|
};
|
|
const provenance = blob(
|
|
{
|
|
_type: 'https://in-toto.io/Statement/v1',
|
|
subject: [],
|
|
predicateType: 'https://slsa.dev/provenance/v1',
|
|
predicate: {
|
|
buildDefinition: {
|
|
buildType:
|
|
'https://github.com/moby/buildkit/blob/master/docs/attestations/slsa-definitions.md',
|
|
externalParameters: {
|
|
request: {
|
|
frontend: 'dockerfile.v0',
|
|
args: {
|
|
'build-arg:SOURCE_REVISION': revision,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
runDetails: { builder: { id: '' } },
|
|
},
|
|
},
|
|
'application/vnd.in-toto+json',
|
|
);
|
|
provenance.annotations = {
|
|
'in-toto.io/predicate-type': 'https://slsa.dev/provenance/v1',
|
|
};
|
|
const attestationLayers = options.omitProvenance
|
|
? [spdx]
|
|
: [spdx, provenance];
|
|
const attestationConfig = blob(
|
|
{
|
|
architecture: 'unknown',
|
|
os: 'unknown',
|
|
config: {},
|
|
rootfs: {
|
|
type: 'layers',
|
|
diff_ids: attestationLayers.map((layer) => layer.digest),
|
|
},
|
|
},
|
|
'application/vnd.oci.image.config.v1+json',
|
|
);
|
|
const attestationManifest = blob(
|
|
{
|
|
schemaVersion: 2,
|
|
mediaType: 'application/vnd.oci.image.manifest.v1+json',
|
|
config: attestationConfig,
|
|
layers: attestationLayers,
|
|
},
|
|
'application/vnd.oci.image.manifest.v1+json',
|
|
);
|
|
descriptors.push({
|
|
...attestationManifest,
|
|
annotations: {
|
|
'vnd.docker.reference.digest': options.unboundAttestation
|
|
? `sha256:${'f'.repeat(64)}`
|
|
: imageDescriptor.digest,
|
|
'vnd.docker.reference.type': 'attestation-manifest',
|
|
},
|
|
platform: { architecture: 'unknown', os: 'unknown' },
|
|
});
|
|
}
|
|
|
|
if (!options.onlyArchitecture) {
|
|
while (descriptors.length < 4) {
|
|
descriptors.push({
|
|
...descriptors[descriptors.length - 1],
|
|
digest: `sha256:${'0'.repeat(64)}`,
|
|
});
|
|
}
|
|
}
|
|
const imageIndex = blob(
|
|
{
|
|
schemaVersion: 2,
|
|
mediaType: 'application/vnd.oci.image.index.v1+json',
|
|
manifests: descriptors,
|
|
},
|
|
'application/vnd.oci.image.index.v1+json',
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(layoutRoot, 'index.json'),
|
|
JSON.stringify({
|
|
schemaVersion: 2,
|
|
mediaType: 'application/vnd.oci.image.index.v1+json',
|
|
manifests: [imageIndex],
|
|
}),
|
|
);
|
|
return layoutRoot;
|
|
}
|
|
|
|
test('accepts two exact images with bound SBOM and provenance', (t) => {
|
|
const report = auditClusterOciLayout({
|
|
root,
|
|
layoutRoot: createFixture(t),
|
|
expectedRevision: revision,
|
|
});
|
|
assert.equal(report.platforms.length, 2);
|
|
assert.deepEqual(
|
|
report.platforms.map((entry) => entry.platform),
|
|
['linux/amd64', 'linux/arm64'],
|
|
);
|
|
assert.deepEqual(
|
|
report.platforms.map((entry) => entry.spdxApplicationPackages),
|
|
[46, 46],
|
|
);
|
|
});
|
|
|
|
test('accepts the independent cluster-admin image and attestation closure', (t) => {
|
|
const report = auditClusterOciLayout({
|
|
root,
|
|
layoutRoot: createFixture(t, { image: 'admin' }),
|
|
expectedRevision: revision,
|
|
image: 'admin',
|
|
});
|
|
assert.equal(report.image, 'admin');
|
|
assert.deepEqual(
|
|
report.platforms.map((entry) => entry.spdxApplicationPackages),
|
|
[91, 91],
|
|
);
|
|
});
|
|
|
|
test('accepts the optional Cluster AI image and attestation closure', (t) => {
|
|
const report = auditClusterOciLayout({
|
|
root,
|
|
layoutRoot: createFixture(t, { image: 'control-ai' }),
|
|
expectedRevision: revision,
|
|
image: 'control-ai',
|
|
});
|
|
assert.equal(report.image, 'control-ai');
|
|
assert.deepEqual(
|
|
report.platforms.map((entry) => entry.spdxApplicationPackages),
|
|
[47, 47],
|
|
);
|
|
});
|
|
|
|
test('accepts the AI-excluded local image and attestation closure', (t) => {
|
|
const report = auditClusterOciLayout({
|
|
root,
|
|
layoutRoot: createFixture(t, { image: 'local' }),
|
|
expectedRevision: revision,
|
|
image: 'local',
|
|
});
|
|
assert.equal(report.image, 'local');
|
|
assert.equal(report.maximumPlatformBytes, 128 * 1024 * 1024);
|
|
assert.deepEqual(
|
|
report.platforms.map((entry) => entry.spdxApplicationPackages),
|
|
[10, 10],
|
|
);
|
|
});
|
|
|
|
test('rejects cluster-control config presented as cluster-admin evidence', (t) => {
|
|
assert.throws(
|
|
() =>
|
|
auditClusterOciLayout({
|
|
root,
|
|
layoutRoot: createFixture(t),
|
|
expectedRevision: revision,
|
|
image: 'admin',
|
|
}),
|
|
/SPDX application closure differs|image config differs/,
|
|
);
|
|
});
|
|
|
|
test('rejects an absent arm64 image', (t) => {
|
|
assert.throws(
|
|
() =>
|
|
auditClusterOciLayout({
|
|
root,
|
|
layoutRoot: createFixture(t, { omitArm64: true }),
|
|
expectedRevision: revision,
|
|
}),
|
|
/platform set/,
|
|
);
|
|
});
|
|
|
|
test('rejects a root runtime config on either architecture', (t) => {
|
|
assert.throws(
|
|
() =>
|
|
auditClusterOciLayout({
|
|
root,
|
|
layoutRoot: createFixture(t, { rootArm64: true }),
|
|
expectedRevision: revision,
|
|
}),
|
|
/image config differs/,
|
|
);
|
|
});
|
|
|
|
test('rejects an attestation that is not bound to its image digest', (t) => {
|
|
assert.throws(
|
|
() =>
|
|
auditClusterOciLayout({
|
|
root,
|
|
layoutRoot: createFixture(t, { unboundAttestation: true }),
|
|
expectedRevision: revision,
|
|
}),
|
|
/one bound attestation/,
|
|
);
|
|
});
|
|
|
|
test('rejects an incomplete predicate set', (t) => {
|
|
assert.throws(
|
|
() =>
|
|
auditClusterOciLayout({
|
|
root,
|
|
layoutRoot: createFixture(t, { omitProvenance: true }),
|
|
expectedRevision: revision,
|
|
}),
|
|
/exactly SBOM and provenance/,
|
|
);
|
|
});
|
|
|
|
test('rejects a development package in the image SBOM closure', (t) => {
|
|
assert.throws(
|
|
() =>
|
|
auditClusterOciLayout({
|
|
root,
|
|
layoutRoot: createFixture(t, { addDevelopmentPackage: true }),
|
|
expectedRevision: revision,
|
|
}),
|
|
/SPDX application closure differs/,
|
|
);
|
|
});
|
|
|
|
test('accepts one native layout with one bound attestation', (t) => {
|
|
const report = auditClusterOciLayout({
|
|
root,
|
|
layoutRoot: createFixture(t, { onlyArchitecture: 'arm64' }),
|
|
expectedRevision: revision,
|
|
expectedPlatforms: ['linux/arm64'],
|
|
});
|
|
assert.deepEqual(
|
|
report.platforms.map((entry) => entry.platform),
|
|
['linux/arm64'],
|
|
);
|
|
});
|
|
|
|
test('merges two scanned native layouts into the exact audited multiarch digest', (t) => {
|
|
const amd64Layout = createFixture(t, { onlyArchitecture: 'amd64' });
|
|
const arm64Layout = createFixture(t, { onlyArchitecture: 'arm64' });
|
|
const outputParent = fs.realpathSync(
|
|
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-release-bundle-')),
|
|
);
|
|
t.after(() => fs.rmSync(outputParent, { recursive: true, force: true }));
|
|
const amd64Evidence = path.join(outputParent, 'amd64-evidence.json');
|
|
const arm64Evidence = path.join(outputParent, 'arm64-evidence.json');
|
|
fs.writeFileSync(
|
|
amd64Evidence,
|
|
JSON.stringify(
|
|
nativeEvidenceRecord({
|
|
root,
|
|
layoutRoot: amd64Layout,
|
|
expectedRevision: revision,
|
|
image: 'control',
|
|
platform: 'linux/amd64',
|
|
}),
|
|
),
|
|
);
|
|
fs.writeFileSync(
|
|
arm64Evidence,
|
|
JSON.stringify(
|
|
nativeEvidenceRecord({
|
|
root,
|
|
layoutRoot: arm64Layout,
|
|
expectedRevision: revision,
|
|
image: 'control',
|
|
platform: 'linux/arm64',
|
|
}),
|
|
),
|
|
);
|
|
const outputRoot = path.join(outputParent, 'merged');
|
|
const predicatePath = path.join(outputParent, 'predicate.json');
|
|
const reportPath = path.join(outputParent, 'report.json');
|
|
const report = mergeNativeLayouts({
|
|
root,
|
|
image: 'control',
|
|
expectedRevision: revision,
|
|
amd64Layout,
|
|
amd64Evidence,
|
|
arm64Layout,
|
|
arm64Evidence,
|
|
outputRoot,
|
|
predicatePath,
|
|
reportPath,
|
|
});
|
|
assert.match(report.rootIndexDigest, /^sha256:[0-9a-f]{64}$/);
|
|
assert.equal(
|
|
JSON.parse(fs.readFileSync(predicatePath, 'utf8')).subjectDigest,
|
|
report.rootIndexDigest,
|
|
);
|
|
assert.equal(
|
|
auditClusterOciLayout({
|
|
root,
|
|
layoutRoot: outputRoot,
|
|
expectedRevision: revision,
|
|
image: 'control',
|
|
}).rootIndexDigest,
|
|
report.rootIndexDigest,
|
|
);
|
|
});
|
|
|
|
test('rejects native evidence that does not exactly describe its OCI layout', (t) => {
|
|
const amd64Layout = createFixture(t, { onlyArchitecture: 'amd64' });
|
|
const arm64Layout = createFixture(t, { onlyArchitecture: 'arm64' });
|
|
const outputParent = fs.realpathSync(
|
|
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-release-bundle-mismatch-')),
|
|
);
|
|
t.after(() => fs.rmSync(outputParent, { recursive: true, force: true }));
|
|
const amd64Evidence = path.join(outputParent, 'amd64-evidence.json');
|
|
const arm64Evidence = path.join(outputParent, 'arm64-evidence.json');
|
|
const amd64Record = nativeEvidenceRecord({
|
|
root,
|
|
layoutRoot: amd64Layout,
|
|
expectedRevision: revision,
|
|
image: 'control',
|
|
platform: 'linux/amd64',
|
|
});
|
|
fs.writeFileSync(
|
|
amd64Evidence,
|
|
JSON.stringify({ ...amd64Record, sourceRevision: 'different' }),
|
|
);
|
|
fs.writeFileSync(
|
|
arm64Evidence,
|
|
JSON.stringify(
|
|
nativeEvidenceRecord({
|
|
root,
|
|
layoutRoot: arm64Layout,
|
|
expectedRevision: revision,
|
|
image: 'control',
|
|
platform: 'linux/arm64',
|
|
}),
|
|
),
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
mergeNativeLayouts({
|
|
root,
|
|
image: 'control',
|
|
expectedRevision: revision,
|
|
amd64Layout,
|
|
amd64Evidence,
|
|
arm64Layout,
|
|
arm64Evidence,
|
|
outputRoot: path.join(outputParent, 'merged'),
|
|
predicatePath: path.join(outputParent, 'predicate.json'),
|
|
reportPath: path.join(outputParent, 'report.json'),
|
|
}),
|
|
/native vulnerability evidence differs for linux\/amd64/,
|
|
);
|
|
});
|