Skip to content

Commit 1b91aaa

Browse files
ronens88claude
andcommitted
feat: enrich SBOM summary with component/relationship counts
- Parse components, relationships, artifacts counts from cimon log - Show totals in overview line and per-SBOM stats in table columns - Handle 3 cases: SBOMs generated, SBOM enabled but none produced, SBOM not enabled (silent) - Graceful fallback for older cimon versions without stats fields - 31 unit tests covering all scenarios Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d686854 commit 1b91aaa

4 files changed

Lines changed: 376 additions & 129 deletions

File tree

dist/post/index.js

Lines changed: 95 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3637,12 +3637,11 @@ async function run(config) {
36373637
}
36383638

36393639
// Parse and display SBOM summary regardless of stop exit code.
3640-
const sbomEntries = (0,_sbom_summary_js__WEBPACK_IMPORTED_MODULE_4__/* .parseSBOMEntries */ .w)(stopOutput);
3641-
if (sbomEntries.length > 0) {
3642-
const reportJobSummary = _actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput('report-job-summary');
3643-
if (reportJobSummary) {
3644-
await (0,_sbom_summary_js__WEBPACK_IMPORTED_MODULE_4__/* .writeSBOMSummary */ .k)(_actions_core__WEBPACK_IMPORTED_MODULE_0__, sbomEntries);
3645-
}
3640+
const reportJobSummary = _actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput('report-job-summary');
3641+
if (reportJobSummary) {
3642+
const sbomEntries = (0,_sbom_summary_js__WEBPACK_IMPORTED_MODULE_4__/* .parseSBOMEntries */ .ws)(stopOutput);
3643+
const sbomEnabled = (0,_sbom_summary_js__WEBPACK_IMPORTED_MODULE_4__/* .isSBOMEnabled */ .iE)(stopOutput);
3644+
await (0,_sbom_summary_js__WEBPACK_IMPORTED_MODULE_4__/* .writeSBOMSummary */ .kq)(_actions_core__WEBPACK_IMPORTED_MODULE_0__, sbomEntries, { sbomEnabled });
36463645
}
36473646

36483647
if (retval !== 0) {
@@ -3677,8 +3676,9 @@ __webpack_handle_async_dependencies__();
36773676
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __nccwpck_require__) => {
36783677

36793678
/* harmony export */ __nccwpck_require__.d(__webpack_exports__, {
3680-
/* harmony export */ "w": () => (/* binding */ parseSBOMEntries),
3681-
/* harmony export */ "k": () => (/* binding */ writeSBOMSummary)
3679+
/* harmony export */ "ws": () => (/* binding */ parseSBOMEntries),
3680+
/* harmony export */ "iE": () => (/* binding */ isSBOMEnabled),
3681+
/* harmony export */ "kq": () => (/* binding */ writeSBOMSummary)
36823682
/* harmony export */ });
36833683
/**
36843684
* Pure helpers for parsing and displaying SBOM results in the
@@ -3689,10 +3689,11 @@ __webpack_handle_async_dependencies__();
36893689
/**
36903690
* Parses SBOM file entries from cimon agent stop output.
36913691
* Each JSON log line that contains `"SBOM files written"` is expected
3692-
* to carry `cyclonedx` and/or `spdx` path fields.
3692+
* to carry `cyclonedx` and/or `spdx` path fields, and optionally
3693+
* `components`, `relationships`, and `artifacts` counts.
36933694
*
36943695
* @param {string} output - Combined stdout+stderr from `cimon agent stop`.
3695-
* @returns {Array<{cyclonedx: string, spdx: string}>}
3696+
* @returns {Array<{cyclonedx: string, spdx: string, components: number, relationships: number, artifacts: number}>}
36963697
*/
36973698
function parseSBOMEntries(output) {
36983699
const entries = [];
@@ -3704,6 +3705,9 @@ function parseSBOMEntries(output) {
37043705
entries.push({
37053706
cyclonedx: parsed.cyclonedx || '',
37063707
spdx: parsed.spdx || '',
3708+
components: parsed.components || 0,
3709+
relationships: parsed.relationships || 0,
3710+
artifacts: parsed.artifacts || 0,
37073711
});
37083712
}
37093713
} catch {
@@ -3714,29 +3718,96 @@ function parseSBOMEntries(output) {
37143718
}
37153719

37163720
/**
3717-
* Builds a GitHub Actions job summary table from SBOM entries.
3721+
* Checks whether the cimon stop output indicates that SBOM generation
3722+
* was enabled (i.e. the feature was active, regardless of whether any
3723+
* SBOMs were actually produced).
3724+
*
3725+
* @param {string} output - Combined stdout+stderr from `cimon agent stop`.
3726+
* @returns {boolean}
3727+
*/
3728+
function isSBOMEnabled(output) {
3729+
// The SBOM feature logs at least one of these markers when active.
3730+
return (
3731+
output.includes('"SBOM files written"') ||
3732+
output.includes('"SBOM generation"') ||
3733+
output.includes('CIMON_SBOM_ENABLED')
3734+
);
3735+
}
3736+
3737+
/**
3738+
* Builds a GitHub Actions job summary with SBOM results.
3739+
* Handles three scenarios:
3740+
* 1. SBOMs generated → table with details per SBOM
3741+
* 2. SBOM enabled but none generated → informational notice
3742+
* 3. SBOM not enabled → no summary (silent)
37183743
*
37193744
* @param {import('@actions/core')} core - The @actions/core module.
3720-
* @param {Array<{cyclonedx: string, spdx: string}>} sbomEntries
3745+
* @param {Array<{cyclonedx: string, spdx: string, components: number, relationships: number, artifacts: number}>} sbomEntries
3746+
* @param {{sbomEnabled: boolean}} options
37213747
*/
3722-
async function writeSBOMSummary(core, sbomEntries) {
3723-
if (sbomEntries.length === 0) return;
3748+
async function writeSBOMSummary(core, sbomEntries, options = {}) {
3749+
const { sbomEnabled = false } = options;
37243750

3725-
const rows = [['Format', 'Path']];
3726-
for (const entry of sbomEntries) {
3727-
if (entry.cyclonedx) {
3728-
rows.push(['CycloneDX', `\`${entry.cyclonedx}\``]);
3729-
}
3730-
if (entry.spdx) {
3731-
rows.push(['SPDX', `\`${entry.spdx}\``]);
3751+
// Case 3: SBOM not enabled — nothing to report.
3752+
if (sbomEntries.length === 0 && !sbomEnabled) return;
3753+
3754+
// Case 2: SBOM was enabled but produced no output.
3755+
if (sbomEntries.length === 0 && sbomEnabled) {
3756+
await core.summary
3757+
.addHeading('Cimon SBOM Report', 2)
3758+
.addRaw(
3759+
'SBOM generation was enabled but no SBOMs were produced. ' +
3760+
'This can happen when no build artifacts (executables or libraries) were detected during the build.\n'
3761+
)
3762+
.write();
3763+
return;
3764+
}
3765+
3766+
// Case 1: SBOMs generated — build a rich summary.
3767+
const totalComponents = sbomEntries.reduce(
3768+
(sum, e) => sum + e.components,
3769+
0
3770+
);
3771+
const totalRelationships = sbomEntries.reduce(
3772+
(sum, e) => sum + e.relationships,
3773+
0
3774+
);
3775+
3776+
// Overview line.
3777+
const parts = [
3778+
`**${sbomEntries.length}** SBOM${sbomEntries.length > 1 ? 's' : ''} generated during build`,
3779+
];
3780+
if (totalComponents > 0) {
3781+
parts.push(
3782+
`covering **${totalComponents}** component${totalComponents > 1 ? 's' : ''}` +
3783+
` and **${totalRelationships}** relationship${totalRelationships !== 1 ? 's' : ''}`
3784+
);
3785+
}
3786+
3787+
// Detail table: one row per SBOM entry.
3788+
const hasStats = totalComponents > 0;
3789+
const header = hasStats
3790+
? ['#', 'CycloneDX', 'SPDX', 'Components', 'Relationships']
3791+
: ['#', 'CycloneDX', 'SPDX'];
3792+
3793+
const rows = [header];
3794+
for (let i = 0; i < sbomEntries.length; i++) {
3795+
const entry = sbomEntries[i];
3796+
const row = [
3797+
`${i + 1}`,
3798+
entry.cyclonedx ? `\`${entry.cyclonedx}\`` : '-',
3799+
entry.spdx ? `\`${entry.spdx}\`` : '-',
3800+
];
3801+
if (hasStats) {
3802+
row.push(`${entry.components}`);
3803+
row.push(`${entry.relationships}`);
37323804
}
3805+
rows.push(row);
37333806
}
37343807

37353808
await core.summary
37363809
.addHeading('Cimon SBOM Report', 2)
3737-
.addRaw(
3738-
`**${sbomEntries.length}** SBOM${sbomEntries.length > 1 ? 's' : ''} generated during build.\n\n`
3739-
)
3810+
.addRaw(parts.join(', ') + '.\n\n')
37403811
.addTable(rows)
37413812
.write();
37423813
}

src/post/index.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import core from '@actions/core';
22
import exec from '@actions/exec';
33
import fs from 'fs';
44
import * as http from '@actions/http-client';
5-
import { parseSBOMEntries, writeSBOMSummary } from './sbom-summary.js';
5+
import {
6+
parseSBOMEntries,
7+
isSBOMEnabled,
8+
writeSBOMSummary,
9+
} from './sbom-summary.js';
610

711
const CIMON_SCRIPT_DOWNLOAD_URL =
812
'https://cimon-releases.s3.amazonaws.com/install.sh';
@@ -114,12 +118,11 @@ async function run(config) {
114118
}
115119

116120
// Parse and display SBOM summary regardless of stop exit code.
117-
const sbomEntries = parseSBOMEntries(stopOutput);
118-
if (sbomEntries.length > 0) {
119-
const reportJobSummary = core.getBooleanInput('report-job-summary');
120-
if (reportJobSummary) {
121-
await writeSBOMSummary(core, sbomEntries);
122-
}
121+
const reportJobSummary = core.getBooleanInput('report-job-summary');
122+
if (reportJobSummary) {
123+
const sbomEntries = parseSBOMEntries(stopOutput);
124+
const sbomEnabled = isSBOMEnabled(stopOutput);
125+
await writeSBOMSummary(core, sbomEntries, { sbomEnabled });
123126
}
124127

125128
if (retval !== 0) {

src/post/sbom-summary.js

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
/**
88
* Parses SBOM file entries from cimon agent stop output.
99
* Each JSON log line that contains `"SBOM files written"` is expected
10-
* to carry `cyclonedx` and/or `spdx` path fields.
10+
* to carry `cyclonedx` and/or `spdx` path fields, and optionally
11+
* `components`, `relationships`, and `artifacts` counts.
1112
*
1213
* @param {string} output - Combined stdout+stderr from `cimon agent stop`.
13-
* @returns {Array<{cyclonedx: string, spdx: string}>}
14+
* @returns {Array<{cyclonedx: string, spdx: string, components: number, relationships: number, artifacts: number}>}
1415
*/
1516
export function parseSBOMEntries(output) {
1617
const entries = [];
@@ -22,6 +23,9 @@ export function parseSBOMEntries(output) {
2223
entries.push({
2324
cyclonedx: parsed.cyclonedx || '',
2425
spdx: parsed.spdx || '',
26+
components: parsed.components || 0,
27+
relationships: parsed.relationships || 0,
28+
artifacts: parsed.artifacts || 0,
2529
});
2630
}
2731
} catch {
@@ -32,29 +36,96 @@ export function parseSBOMEntries(output) {
3236
}
3337

3438
/**
35-
* Builds a GitHub Actions job summary table from SBOM entries.
39+
* Checks whether the cimon stop output indicates that SBOM generation
40+
* was enabled (i.e. the feature was active, regardless of whether any
41+
* SBOMs were actually produced).
42+
*
43+
* @param {string} output - Combined stdout+stderr from `cimon agent stop`.
44+
* @returns {boolean}
45+
*/
46+
export function isSBOMEnabled(output) {
47+
// The SBOM feature logs at least one of these markers when active.
48+
return (
49+
output.includes('"SBOM files written"') ||
50+
output.includes('"SBOM generation"') ||
51+
output.includes('CIMON_SBOM_ENABLED')
52+
);
53+
}
54+
55+
/**
56+
* Builds a GitHub Actions job summary with SBOM results.
57+
* Handles three scenarios:
58+
* 1. SBOMs generated → table with details per SBOM
59+
* 2. SBOM enabled but none generated → informational notice
60+
* 3. SBOM not enabled → no summary (silent)
3661
*
3762
* @param {import('@actions/core')} core - The @actions/core module.
38-
* @param {Array<{cyclonedx: string, spdx: string}>} sbomEntries
63+
* @param {Array<{cyclonedx: string, spdx: string, components: number, relationships: number, artifacts: number}>} sbomEntries
64+
* @param {{sbomEnabled: boolean}} options
3965
*/
40-
export async function writeSBOMSummary(core, sbomEntries) {
41-
if (sbomEntries.length === 0) return;
66+
export async function writeSBOMSummary(core, sbomEntries, options = {}) {
67+
const { sbomEnabled = false } = options;
4268

43-
const rows = [['Format', 'Path']];
44-
for (const entry of sbomEntries) {
45-
if (entry.cyclonedx) {
46-
rows.push(['CycloneDX', `\`${entry.cyclonedx}\``]);
47-
}
48-
if (entry.spdx) {
49-
rows.push(['SPDX', `\`${entry.spdx}\``]);
69+
// Case 3: SBOM not enabled — nothing to report.
70+
if (sbomEntries.length === 0 && !sbomEnabled) return;
71+
72+
// Case 2: SBOM was enabled but produced no output.
73+
if (sbomEntries.length === 0 && sbomEnabled) {
74+
await core.summary
75+
.addHeading('Cimon SBOM Report', 2)
76+
.addRaw(
77+
'SBOM generation was enabled but no SBOMs were produced. ' +
78+
'This can happen when no build artifacts (executables or libraries) were detected during the build.\n'
79+
)
80+
.write();
81+
return;
82+
}
83+
84+
// Case 1: SBOMs generated — build a rich summary.
85+
const totalComponents = sbomEntries.reduce(
86+
(sum, e) => sum + e.components,
87+
0
88+
);
89+
const totalRelationships = sbomEntries.reduce(
90+
(sum, e) => sum + e.relationships,
91+
0
92+
);
93+
94+
// Overview line.
95+
const parts = [
96+
`**${sbomEntries.length}** SBOM${sbomEntries.length > 1 ? 's' : ''} generated during build`,
97+
];
98+
if (totalComponents > 0) {
99+
parts.push(
100+
`covering **${totalComponents}** component${totalComponents > 1 ? 's' : ''}` +
101+
` and **${totalRelationships}** relationship${totalRelationships !== 1 ? 's' : ''}`
102+
);
103+
}
104+
105+
// Detail table: one row per SBOM entry.
106+
const hasStats = totalComponents > 0;
107+
const header = hasStats
108+
? ['#', 'CycloneDX', 'SPDX', 'Components', 'Relationships']
109+
: ['#', 'CycloneDX', 'SPDX'];
110+
111+
const rows = [header];
112+
for (let i = 0; i < sbomEntries.length; i++) {
113+
const entry = sbomEntries[i];
114+
const row = [
115+
`${i + 1}`,
116+
entry.cyclonedx ? `\`${entry.cyclonedx}\`` : '-',
117+
entry.spdx ? `\`${entry.spdx}\`` : '-',
118+
];
119+
if (hasStats) {
120+
row.push(`${entry.components}`);
121+
row.push(`${entry.relationships}`);
50122
}
123+
rows.push(row);
51124
}
52125

53126
await core.summary
54127
.addHeading('Cimon SBOM Report', 2)
55-
.addRaw(
56-
`**${sbomEntries.length}** SBOM${sbomEntries.length > 1 ? 's' : ''} generated during build.\n\n`
57-
)
128+
.addRaw(parts.join(', ') + '.\n\n')
58129
.addTable(rows)
59130
.write();
60131
}

0 commit comments

Comments
 (0)