Skip to content

Commit dc9636e

Browse files
RD-977 Use new API for getting SBOM result (#3)
* RD-977 Use new API for getting SBOM result * RD-977 Use test env API base URL for testing * RD-977 Build * RD-977 Fix aritifact root directory name * RD-977 Refactor code * RD-977 Update README.md * RD-977 Refactor code * RD-977 Build * RD-977 Update sample data * RD-977 Use production config * RD-977 Cover edge case: no SBOM found after scan finished * RD-977 Add debugging log * RD-977 Refactor code * RD-977 Bump version --------- Co-authored-by: Clay Sang <[email protected]>
1 parent 177f64c commit dc9636e

File tree

11 files changed

+280
-44
lines changed

11 files changed

+280
-44
lines changed

README.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,19 @@ jobs:
4949
5050
After the scan is complete, an artifact named `DEEPBITS_SCAN_RESULTS` will be generated, which contains two files:
5151

52-
| Output | Description |
53-
| ------------------- | --------------------------------------------------------------------- |
54-
| sbom.CycloneDX.json | SBOM in CycloneDX format |
55-
| scanSummary.json | Scan result contains vulnerability and malware summary in JSON format |
52+
| Output | Description |
53+
| -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
54+
| deepbits-sbom-{{owner}}-{{repo}}-{{sha}}.zip | A ZIP file consists of the SBOM result, along with the signature of the SBOM and Deepbits’ certificate required for verifying the signature. (For example: deepbits-sbom-DeepBitsTechnology-getsbom-db3bc50.zip) |
55+
| scanSummary.json | Scan result contains vulnerability and malware summary in JSON format |
56+
57+
The structure of the `deepbits-sbom-{{owner}}-{{repo}}-{{sha}}.zip` file is as follows:
58+
59+
| FileName | Description |
60+
| ----------------------------------------- | ------------------------------------------------------------------------------------------ |
61+
| {{owner}}-{{repo}}-{{sha}}.CycloneDX.json | SBOM in CycloneDX format. (For example: DeepBitsTechnology-getsbom-db3bc50.CycloneDX.json) |
62+
| CycloneDX.signature.bin | The signature of the SBOM |
63+
| deepbits.cert | Deepbits’ certificate required for verifying the signature |
64+
| README.md | Contains instructions on how to verify the signature |
5665

5766
**We have included a sample folder called `sample_scan_results` in the repository [here](./samples/DEEPBITS_SCAN_RESULTS/).**
5867

@@ -102,4 +111,4 @@ This project is licensed under the MIT License. Please see the `LICENSE` file fo
102111

103112
## Support
104113

105-
If you encounter any issues or have any questions about the Deepbits SBOM Scanner GitHub Action, please feel free to contact us at [[email protected]](mailto:[email protected]). We are always happy to help!
114+
If you encounter any issues or have any questions about the Deepbits SBOM GitHub Action, please feel free to contact us at [[email protected]](mailto:[email protected]). We are always happy to help!

__tests__/main.test.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@ describe('Main', () => {
2727
it('should call expected functions with correct parameters if repository is public and scan result exists', async () => {
2828
jest.spyOn(deepbitsAction, 'isRepoPublic').mockResolvedValueOnce(true);
2929

30+
const mockScanResultId = 'mockScanResultId';
31+
3032
const mockScanResult = {
3133
scanResult: [
3234
{
35+
_id: mockScanResultId,
3336
finalResult: {bom: 'bom'},
3437
scanEndAt: Date.now().toString(),
3538
},
@@ -39,19 +42,30 @@ describe('Main', () => {
3942
.spyOn(deepbitsAction, 'getScanResult')
4043
.mockResolvedValueOnce(mockScanResult);
4144

45+
const mockDownloadCommitSbomZipResult =
46+
'DEEPBITS_SCAN_RESULTS/mockDownloadCommitSbomZipResult.zip';
47+
jest
48+
.spyOn(deepbitsAction, 'downloadCommitSbomZip')
49+
.mockResolvedValueOnce(mockDownloadCommitSbomZipResult);
50+
4251
// Set up the expected artifact upload parameters
4352
const expectedUploadParams = [
44-
{name: 'sbom.CycloneDX', jsonContent: 'bom'},
45-
{name: 'scanSummary', jsonContent: {bom: 'bom'}},
53+
[{name: 'scanSummary', jsonContent: {bom: 'bom'}}, ,],
54+
[mockDownloadCommitSbomZipResult],
4655
];
4756

4857
await run();
4958

5059
expect(core.setFailed).not.toHaveBeenCalled();
5160

61+
expect(deepbitsAction.downloadCommitSbomZip).toHaveBeenCalledTimes(1);
62+
expect(deepbitsAction.downloadCommitSbomZip).toHaveBeenCalledWith(
63+
mockScanResultId
64+
);
65+
5266
expect(deepbitsAction.uploadArtifacts).toHaveBeenCalledTimes(1);
5367
expect(deepbitsAction.uploadArtifacts).toHaveBeenCalledWith(
54-
expectedUploadParams
68+
...expectedUploadParams
5569
);
5670

5771
expect(deepbitsAction.setInfo).toHaveBeenCalledTimes(1);
@@ -65,17 +79,18 @@ describe('Main', () => {
6579
.mockResolvedValueOnce(undefined);
6680

6781
const expectedUploadParams = [
68-
{name: 'sbom.CycloneDX', jsonContent: {}},
69-
{name: 'scanSummary', jsonContent: {}},
82+
[{name: 'scanSummary', jsonContent: {}}],
83+
undefined,
7084
];
7185

7286
await run();
7387

7488
expect(core.setFailed).not.toHaveBeenCalled();
89+
expect(deepbitsAction.downloadCommitSbomZip).not.toHaveBeenCalled();
7590

7691
expect(deepbitsAction.uploadArtifacts).toHaveBeenCalledTimes(1);
7792
expect(deepbitsAction.uploadArtifacts).toHaveBeenCalledWith(
78-
expectedUploadParams
93+
...expectedUploadParams
7994
);
8095

8196
expect(deepbitsAction.setInfo).toHaveBeenCalledTimes(1);

__tests__/utils/DeepbitsGitHubAction.test.ts

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,32 @@ import * as artifact from '@actions/artifact';
22
import * as core from '@actions/core';
33
import * as github from '@actions/github';
44
import {afterEach, describe, expect, it, jest} from '@jest/globals';
5+
import axios from 'axios';
56
import * as fs from 'fs';
67
import {GitHubCommitDefWithPopulatedScanResult} from '../../src/types/deepbitsApi';
7-
import * as deepbitsApi from '../../src/utils/api';
88
import {
9+
downloadCommitSbomZip,
910
getScanResult,
1011
isRepoPublic,
1112
setInfo,
1213
uploadArtifacts,
1314
} from '../../src/utils/DeepbitsGitHubAction';
15+
import * as deepbitsApi from '../../src/utils/api';
1416

1517
const testToken = 'test-token';
1618

19+
jest.mock('axios', () => {
20+
return {
21+
get: jest.fn(),
22+
create: jest.fn(() => ({
23+
interceptors: {
24+
request: {use: jest.fn(), eject: jest.fn()},
25+
response: {use: jest.fn(), eject: jest.fn()},
26+
},
27+
})),
28+
};
29+
});
30+
1731
jest.mock('@actions/artifact');
1832
jest.mock('@actions/core');
1933
jest.mock('@actions/github', () => ({
@@ -202,4 +216,119 @@ describe('DeepbitsGitHubAction', () => {
202216
expect(result).toEqual({success: false});
203217
});
204218
});
219+
220+
describe('downloadCommitSbomZip', () => {
221+
const rootDirectory = 'DEEPBITS_SCAN_RESULTS';
222+
223+
const mkdirSyncMock = jest.spyOn(fs, 'mkdirSync');
224+
const writeFileSyncMock = jest.spyOn(fs, 'writeFileSync');
225+
226+
it('should return the zip file location correctly', async () => {
227+
const existsSyncMock = jest
228+
.spyOn(fs, 'existsSync')
229+
.mockReturnValueOnce(false);
230+
231+
const sbomId = 'mock_sbom_id';
232+
233+
const MOCK_FILE_NAME = 'mock_file.zip';
234+
const MOCK_CONTENT_DISPOSITION = `attachment; filename="${MOCK_FILE_NAME}"`;
235+
const MOCK_ARRAY_BUFFER = new ArrayBuffer(8);
236+
const MOCK_FILE_BUFFER = Buffer.from(MOCK_ARRAY_BUFFER);
237+
238+
const expectedUrl = `${deepbitsApi.BASE_URL}/gh/test-owner/test-repo/test-sha/sbom/${sbomId}`;
239+
const expectedFileLocation = `${rootDirectory}/${MOCK_FILE_NAME}`;
240+
241+
jest.spyOn(axios, 'get').mockResolvedValueOnce({
242+
data: MOCK_ARRAY_BUFFER,
243+
headers: {
244+
'content-disposition': MOCK_CONTENT_DISPOSITION,
245+
},
246+
});
247+
248+
const result = await downloadCommitSbomZip(sbomId);
249+
250+
expect(existsSyncMock).toHaveBeenCalledWith(rootDirectory);
251+
expect(mkdirSyncMock).toHaveBeenCalledWith(rootDirectory);
252+
253+
expect(axios.get).toHaveBeenCalledWith(expectedUrl, {
254+
responseType: 'arraybuffer',
255+
});
256+
257+
expect(writeFileSyncMock).toHaveBeenCalledWith(
258+
expectedFileLocation,
259+
MOCK_FILE_BUFFER
260+
);
261+
262+
expect(result).toEqual(expectedFileLocation);
263+
});
264+
265+
it('should return undefined if no SBOM content found', async () => {
266+
const existsSyncMock = jest
267+
.spyOn(fs, 'existsSync')
268+
.mockReturnValueOnce(false);
269+
270+
const sbomId = 'mock_sbom_id';
271+
272+
const expectedUrl = `${deepbitsApi.BASE_URL}/gh/test-owner/test-repo/test-sha/sbom/${sbomId}`;
273+
274+
const noBomContentFoundError = {
275+
response: {
276+
status: 404,
277+
data: {
278+
meta: {
279+
code: 404,
280+
message: 'Not Found',
281+
},
282+
data: {
283+
errorReason: 'No bom content found',
284+
},
285+
},
286+
},
287+
};
288+
289+
jest.spyOn(axios, 'get').mockRejectedValueOnce(noBomContentFoundError);
290+
291+
const result = await downloadCommitSbomZip(sbomId);
292+
293+
expect(existsSyncMock).toHaveBeenCalledWith(rootDirectory);
294+
expect(mkdirSyncMock).toHaveBeenCalledWith(rootDirectory);
295+
296+
expect(axios.get).toHaveBeenCalledWith(expectedUrl, {
297+
responseType: 'arraybuffer',
298+
});
299+
300+
expect(writeFileSyncMock).not.toHaveBeenCalled();
301+
302+
expect(core.info).toHaveBeenCalledWith('No SBOM found');
303+
304+
expect(result).toEqual(undefined);
305+
});
306+
307+
it('should throw error if get SBOM ZIP failed', async () => {
308+
const existsSyncMock = jest
309+
.spyOn(fs, 'existsSync')
310+
.mockReturnValueOnce(false);
311+
312+
const sbomId = 'mock_sbom_id';
313+
314+
const expectedUrl = `${deepbitsApi.BASE_URL}/gh/test-owner/test-repo/test-sha/sbom/${sbomId}`;
315+
316+
const error = new Error('Request failed with status code 500');
317+
318+
jest.spyOn(axios, 'get').mockRejectedValueOnce(error);
319+
320+
await expect(downloadCommitSbomZip(sbomId)).rejects.toThrow();
321+
322+
expect(existsSyncMock).toHaveBeenCalledWith(rootDirectory);
323+
expect(mkdirSyncMock).toHaveBeenCalledWith(rootDirectory);
324+
325+
expect(axios.get).toHaveBeenCalledWith(expectedUrl, {
326+
responseType: 'arraybuffer',
327+
});
328+
329+
expect(writeFileSyncMock).not.toHaveBeenCalled();
330+
331+
expect(core.setFailed).toHaveBeenCalledWith('Failed to download SBOM');
332+
});
333+
});
205334
});

dist/index.js

Lines changed: 50 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@deepbits/getsbom",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"private": true,
55
"description": "GitHub Action for creating and analyzing SBOM for your project to find vulnerabilities and license issues",
66
"main": "lib/main.js",
Binary file not shown.

samples/DEEPBITS_SCAN_RESULTS/sbom.CycloneDX.json

Lines changed: 0 additions & 1 deletion
This file was deleted.

samples/DEEPBITS_SCAN_RESULTS/scanSummary.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/main.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as core from '@actions/core';
22
import {
3+
downloadCommitSbomZip,
34
getScanResult,
45
isRepoPublic,
56
setInfo,
@@ -15,14 +16,20 @@ export async function run(): Promise<void> {
1516
return;
1617
}
1718

18-
const scanResult = await getScanResult();
19+
const scanResult = (await getScanResult())?.scanResult?.[0];
1920

20-
const {finalResult} = scanResult?.scanResult?.[0] ?? {};
21+
let sbomZipFileLocation: string | undefined;
2122

22-
await uploadArtifacts([
23-
{name: 'sbom.CycloneDX', jsonContent: finalResult?.bom || {}},
24-
{name: 'scanSummary', jsonContent: finalResult || {}},
25-
]);
23+
if (scanResult?._id) {
24+
sbomZipFileLocation = await downloadCommitSbomZip(scanResult._id);
25+
}
26+
27+
const {finalResult} = scanResult ?? {};
28+
29+
await uploadArtifacts(
30+
[{name: 'scanSummary', jsonContent: finalResult || {}}],
31+
sbomZipFileLocation ? [sbomZipFileLocation] : undefined
32+
);
2633

2734
await setInfo();
2835
} catch (error) {

0 commit comments

Comments
 (0)