Public Access
generated from Philipp/template-git
refactor(action): replace bash blocks with zero-dependency Node.js modules
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 1m53s
Philipp/Global-Workflows: Pre-commit Lint (Scoped) / pre-commit (pull_request) Successful in 1m53s
Extract ~450 lines of complex bash from action.yaml into modular, unit-tested Node.js scripts in lib/. All logic is covered by 26 native node:test unit tests (node --test, no external dependencies). Modules added: - lib/platforms.js - platform parsing and OCI manifest auto-discovery - lib/distro.js - SPDX PURL distro detection (os_name/os_version) - lib/verifier.js - Trivy arch mapping and SHA256 verification - lib/diagnostics.js - CI terminal diagnostic formatters - lib/install-trivy.js - Trivy install/verify entrypoint (replaces bash step) - lib/extract-sboms.js - SBOM extraction entrypoint (replaces bash step) - lib/scan-sboms.js - SBOM scan + diff entrypoint (replaces bash step) Tests added (all pass, auto-discovered by ode --test): - test/platforms.test.js - test/distro.test.js - test/verifier.test.js - test/diagnostics.test.js - test/orchestration.test.js action.yaml shrinks from 554 to 107 lines. Inputs are bridged to Node scripts via INPUT_* env vars so modules are fully decoupled from the Actions runner and testable in isolation.
This commit is contained in:
13 files changed
+1145
-347
No files matched your search
@@ -0,0 +1,76 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
formatSbomOverview,
|
||||
formatScanOverview,
|
||||
countVulnerabilities,
|
||||
} = require('../lib/diagnostics.js');
|
||||
|
||||
test('formatSbomOverview formats successful extraction banner', () => {
|
||||
const overview = formatSbomOverview({
|
||||
headImage: 'my-head:latest',
|
||||
targetImage: 'my-target:latest',
|
||||
headFiles: [{ name: 'sbom-amd64.spdx.json', size: 1024 }],
|
||||
targetFiles: [{ name: 'sbom-amd64.spdx.json', size: 2048 }],
|
||||
headError: null,
|
||||
targetError: null,
|
||||
});
|
||||
|
||||
assert.ok(overview.includes('SBOM EXTRACTION DIAGNOSTIC OVERVIEW'));
|
||||
assert.ok(overview.includes('HEAD Image (my-head:latest): ✅ SUCCESS'));
|
||||
assert.ok(overview.includes('TARGET Baseline Image (my-target:latest): ✅ SUCCESS'));
|
||||
assert.ok(overview.includes('sbom-amd64.spdx.json (1024 bytes)'));
|
||||
});
|
||||
|
||||
test('formatSbomOverview formats failure banner when error present', () => {
|
||||
const overview = formatSbomOverview({
|
||||
headImage: 'my-head:latest',
|
||||
targetImage: 'my-target:latest',
|
||||
headFiles: [],
|
||||
targetFiles: [],
|
||||
headError: 'Image not found',
|
||||
targetError: null,
|
||||
});
|
||||
|
||||
assert.ok(overview.includes('HEAD Image (my-head:latest): ❌ FAILED - Image not found'));
|
||||
});
|
||||
|
||||
test('formatScanOverview formats scan summary with strategy', () => {
|
||||
const overview = formatScanOverview({
|
||||
headImage: 'debian:bookworm-slim',
|
||||
targetImage: 'debian:bullseye-slim',
|
||||
targetBranch: 'main',
|
||||
strategy: 'Direct Trivy Image Scan',
|
||||
headSummary: [' - amd64: ✅ OK (104 vulnerability findings)'],
|
||||
targetSummary: [' - amd64: ✅ OK (130 vulnerability findings)'],
|
||||
});
|
||||
|
||||
assert.ok(overview.includes('CVE VULNERABILITY SCAN DIAGNOSTIC OVERVIEW'));
|
||||
assert.ok(overview.includes('Scan Strategy: Direct Trivy Image Scan'));
|
||||
assert.ok(overview.includes('HEAD Image: debian:bookworm-slim'));
|
||||
assert.ok(overview.includes('TARGET Baseline Image: debian:bullseye-slim (branch: \'main\')'));
|
||||
assert.ok(overview.includes('104 vulnerability findings'));
|
||||
});
|
||||
|
||||
test('countVulnerabilities counts all vulnerabilities in a Trivy JSON report', () => {
|
||||
const report = {
|
||||
Results: [
|
||||
{
|
||||
Vulnerabilities: [
|
||||
{ VulnerabilityID: 'CVE-2023-0001' },
|
||||
{ VulnerabilityID: 'CVE-2023-0002' },
|
||||
],
|
||||
},
|
||||
{
|
||||
Vulnerabilities: [
|
||||
{ VulnerabilityID: 'CVE-2023-0003' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert.equal(countVulnerabilities(report), 3);
|
||||
assert.equal(countVulnerabilities(JSON.stringify(report)), 3);
|
||||
assert.equal(countVulnerabilities({}), 0);
|
||||
assert.equal(countVulnerabilities(null), 0);
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { detectDistro } = require('../lib/distro.js');
|
||||
|
||||
test('detectDistro returns null for null, undefined, or empty input', () => {
|
||||
assert.equal(detectDistro(null), null);
|
||||
assert.equal(detectDistro(undefined), null);
|
||||
assert.equal(detectDistro(''), null);
|
||||
assert.equal(detectDistro('{}'), null);
|
||||
});
|
||||
|
||||
test('detectDistro extracts distro from parsed SPDX object with package PURLs', () => {
|
||||
const spdx = {
|
||||
packages: [
|
||||
{
|
||||
name: 'acl',
|
||||
externalRefs: [
|
||||
{
|
||||
referenceType: 'purl',
|
||||
referenceLocator: 'pkg:deb/debian/acl@2.3.1-3?os_distro=bookworm&os_name=debian&os_version=12',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert.equal(detectDistro(spdx), 'debian/12');
|
||||
});
|
||||
|
||||
test('detectDistro extracts distro from JSON string containing escaped unicode \\u0026', () => {
|
||||
const jsonStr = JSON.stringify({
|
||||
packages: [
|
||||
{
|
||||
name: 'adduser',
|
||||
externalRefs: [
|
||||
{
|
||||
referenceType: 'purl',
|
||||
referenceLocator: 'pkg:deb/debian/adduser@3.134?os_distro=bullseye&os_name=debian&os_version=11',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}).replace(/&/g, '\\u0026');
|
||||
|
||||
assert.ok(jsonStr.includes('\\u0026'));
|
||||
assert.equal(detectDistro(jsonStr), 'debian/11');
|
||||
});
|
||||
|
||||
test('detectDistro handles non-debian distributions (e.g. ubuntu, alpine)', () => {
|
||||
const spdx = {
|
||||
packages: [
|
||||
{
|
||||
externalRefs: [
|
||||
{
|
||||
referenceType: 'purl',
|
||||
referenceLocator: 'pkg:apk/alpine/alpine-baselayout@3.4.3-r2?os_name=alpine&os_version=3.20',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert.equal(detectDistro(spdx), 'alpine/3.20');
|
||||
});
|
||||
|
||||
test('detectDistro returns null when PURLs lack os_name or os_version', () => {
|
||||
const spdx = {
|
||||
packages: [
|
||||
{
|
||||
externalRefs: [
|
||||
{
|
||||
referenceType: 'purl',
|
||||
referenceLocator: 'pkg:generic/foo@1.0.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert.equal(detectDistro(spdx), null);
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { parseArgs: parseExtractArgs, cleanupInvalidFile } = require('../lib/extract-sboms.js');
|
||||
const { parseArgs: parseScanArgs } = require('../lib/scan-sboms.js');
|
||||
|
||||
test('extract-sboms parseArgs parses command line arguments and defaults', () => {
|
||||
const argv = [
|
||||
'node',
|
||||
'extract-sboms.js',
|
||||
'--head-image', 'my-repo/head:1.0',
|
||||
'--target-image', 'my-repo/target:1.0',
|
||||
'--architectures', 'amd64,arm64',
|
||||
'--use-inbuild-sbom', 'true',
|
||||
];
|
||||
|
||||
const args = parseExtractArgs(argv);
|
||||
assert.equal(args.headImage, 'my-repo/head:1.0');
|
||||
assert.equal(args.targetImage, 'my-repo/target:1.0');
|
||||
assert.equal(args.architectures, 'amd64,arm64');
|
||||
assert.equal(args.useInbuildSbom, 'true');
|
||||
});
|
||||
|
||||
test('extract-sboms cleanupInvalidFile removes empty files and files containing null', () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sbom-clean-'));
|
||||
const emptyFile = path.join(tmpDir, 'empty.json');
|
||||
const nullFile = path.join(tmpDir, 'null.json');
|
||||
const validFile = path.join(tmpDir, 'valid.json');
|
||||
|
||||
fs.writeFileSync(emptyFile, '');
|
||||
fs.writeFileSync(nullFile, 'null\n');
|
||||
fs.writeFileSync(validFile, '{"spdxVersion": "SPDX-2.3"}');
|
||||
|
||||
cleanupInvalidFile(emptyFile);
|
||||
cleanupInvalidFile(nullFile);
|
||||
cleanupInvalidFile(validFile);
|
||||
|
||||
assert.equal(fs.existsSync(emptyFile), false);
|
||||
assert.equal(fs.existsSync(nullFile), false);
|
||||
assert.equal(fs.existsSync(validFile), true);
|
||||
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('scan-sboms parseArgs parses scan arguments and defaults', () => {
|
||||
const argv = [
|
||||
'node',
|
||||
'scan-sboms.js',
|
||||
'--head-image', 'my-head:latest',
|
||||
'--target-image', 'my-target:latest',
|
||||
'--target-branch', 'feature',
|
||||
'--output-delta', 'custom-delta.md',
|
||||
'--output-summary', 'custom-summary.md',
|
||||
];
|
||||
|
||||
const args = parseScanArgs(argv);
|
||||
assert.equal(args.headImage, 'my-head:latest');
|
||||
assert.equal(args.targetImage, 'my-target:latest');
|
||||
assert.equal(args.targetBranch, 'feature');
|
||||
assert.equal(args.outputDelta, 'custom-delta.md');
|
||||
assert.equal(args.outputSummary, 'custom-summary.md');
|
||||
assert.equal(args.useInbuildSbom, 'false');
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { parsePlatforms, discoverPlatforms, toArchTag } = require('../lib/platforms.js');
|
||||
|
||||
test('parsePlatforms handles empty and undefined input', () => {
|
||||
assert.deepEqual(parsePlatforms(''), []);
|
||||
assert.deepEqual(parsePlatforms(null), []);
|
||||
assert.deepEqual(parsePlatforms(undefined), []);
|
||||
assert.deepEqual(parsePlatforms(' '), []);
|
||||
});
|
||||
|
||||
test('parsePlatforms parses comma-separated architectures and adds linux/ prefix if omitted', () => {
|
||||
const result = parsePlatforms('amd64, arm64, linux/arm/v7');
|
||||
assert.deepEqual(result, ['linux/amd64', 'linux/arm64', 'linux/arm/v7']);
|
||||
});
|
||||
|
||||
test('discoverPlatforms extracts unique architectures from OCI image manifests', () => {
|
||||
const headManifest = JSON.stringify({
|
||||
manifests: [
|
||||
{ platform: { architecture: 'amd64', os: 'linux' } },
|
||||
{ platform: { architecture: 'arm64', os: 'linux' } },
|
||||
{ platform: { architecture: 'unknown', os: 'unknown' } },
|
||||
],
|
||||
});
|
||||
|
||||
const targetManifest = JSON.stringify({
|
||||
manifests: [
|
||||
{ platform: { architecture: 'amd64', os: 'linux' } },
|
||||
{ platform: { architecture: 'arm', os: 'linux', variant: 'v7' } },
|
||||
],
|
||||
});
|
||||
|
||||
const result = discoverPlatforms(headManifest, targetManifest);
|
||||
assert.deepEqual(result, ['linux/amd64', 'linux/arm/v7', 'linux/arm64']);
|
||||
});
|
||||
|
||||
test('discoverPlatforms safely handles invalid or missing manifest JSON', () => {
|
||||
assert.deepEqual(discoverPlatforms('invalid json', null), []);
|
||||
assert.deepEqual(discoverPlatforms('', '{}'), []);
|
||||
});
|
||||
|
||||
test('toArchTag converts platform strings to filesystem-safe arch tags', () => {
|
||||
assert.equal(toArchTag('linux/amd64'), 'amd64');
|
||||
assert.equal(toArchTag('linux/arm64'), 'arm64');
|
||||
assert.equal(toArchTag('linux/arm/v7'), 'arm-v7');
|
||||
assert.equal(toArchTag('amd64'), 'amd64');
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const crypto = require('node:crypto');
|
||||
const { mapArch, parseChecksums, verifyFileSha256 } = require('../lib/verifier.js');
|
||||
|
||||
test('mapArch maps uname architecture to Trivy release arch', () => {
|
||||
assert.equal(mapArch('x86_64'), '64bit');
|
||||
assert.equal(mapArch('aarch64'), 'ARM64');
|
||||
assert.equal(mapArch('arm64'), 'ARM64');
|
||||
assert.throws(() => mapArch('mips'), /Unsupported architecture/);
|
||||
});
|
||||
|
||||
test('parseChecksums parses sha256 checksum lockfile into filename map', () => {
|
||||
const content = `
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 trivy_0.74.0_Linux-64bit.tar.gz
|
||||
a1b2c3d4e5f60000000000000000000000000000000000000000000000000000 trivy_0.74.0_Linux-ARM64.tar.gz
|
||||
`;
|
||||
|
||||
const map = parseChecksums(content);
|
||||
assert.equal(map.get('trivy_0.74.0_Linux-64bit.tar.gz'), 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855');
|
||||
assert.equal(map.get('trivy_0.74.0_Linux-ARM64.tar.gz'), 'a1b2c3d4e5f60000000000000000000000000000000000000000000000000000');
|
||||
assert.equal(map.has('nonexistent'), false);
|
||||
});
|
||||
|
||||
test('verifyFileSha256 verifies actual sha256 hash of a file', () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cve-test-'));
|
||||
const testFile = path.join(tmpDir, 'test.txt');
|
||||
const fileContent = 'hello world\n';
|
||||
fs.writeFileSync(testFile, fileContent);
|
||||
|
||||
const expectedHash = crypto.createHash('sha256').update(fileContent).digest('hex');
|
||||
assert.equal(verifyFileSha256(testFile, expectedHash), true);
|
||||
assert.equal(verifyFileSha256(testFile, 'wronghash'), false);
|
||||
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
Reference in new issue
Block a user