-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Integrate Native Image SBOM with GitHub's Dependency Submission API
- Loading branch information
Showing
19 changed files
with
1,638 additions
and
213 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,306 @@ | ||
import * as c from '../src/constants' | ||
import {setUpSBOMSupport, processSBOM} from '../src/features/sbom' | ||
import * as core from '@actions/core' | ||
import * as github from '@actions/github' | ||
import * as glob from '@actions/glob' | ||
import {join} from 'path' | ||
import {tmpdir} from 'os' | ||
import {mkdtempSync, writeFileSync, rmSync} from 'fs' | ||
|
||
jest.mock('@actions/glob') | ||
jest.mock('@actions/github', () => ({ | ||
getOctokit: jest.fn(() => ({ | ||
request: jest.fn().mockResolvedValue(undefined) | ||
})), | ||
context: { | ||
repo: { | ||
owner: 'test-owner', | ||
repo: 'test-repo' | ||
}, | ||
sha: 'test-sha', | ||
ref: 'test-ref', | ||
workflow: 'test-workflow', | ||
job: 'test-job', | ||
runId: '12345' | ||
} | ||
})) | ||
|
||
function mockFindSBOM(files: string[]) { | ||
const mockCreate = jest.fn().mockResolvedValue({ | ||
glob: jest.fn().mockResolvedValue(files) | ||
}) | ||
;(glob.create as jest.Mock).mockImplementation(mockCreate) | ||
} | ||
|
||
// Mocks the GitHub dependency submission API return value | ||
// 'undefined' is treated as a successful request | ||
function mockGithubAPIReturnValue(returnValue: Error | undefined = undefined) { | ||
const mockOctokit = { | ||
request: | ||
returnValue === undefined | ||
? jest.fn().mockResolvedValue(returnValue) | ||
: jest.fn().mockRejectedValue(returnValue) | ||
} | ||
;(github.getOctokit as jest.Mock).mockReturnValue(mockOctokit) | ||
return mockOctokit | ||
} | ||
|
||
describe('sbom feature', () => { | ||
let spyInfo: jest.SpyInstance<void, Parameters<typeof core.info>> | ||
let spyWarning: jest.SpyInstance<void, Parameters<typeof core.warning>> | ||
let spyExportVariable: jest.SpyInstance< | ||
void, | ||
Parameters<typeof core.exportVariable> | ||
> | ||
let workspace: string | ||
let originalEnv: NodeJS.ProcessEnv | ||
const javaVersion = '24.0.0' | ||
const distribution = c.DISTRIBUTION_GRAALVM | ||
|
||
beforeEach(() => { | ||
originalEnv = process.env | ||
|
||
process.env = { | ||
...process.env, | ||
GITHUB_REPOSITORY: 'test-owner/test-repo', | ||
GITHUB_TOKEN: 'fake-token' | ||
} | ||
|
||
workspace = mkdtempSync(join(tmpdir(), 'setup-graalvm-sbom-')) | ||
mockGithubAPIReturnValue() | ||
|
||
spyInfo = jest.spyOn(core, 'info').mockImplementation(() => null) | ||
spyWarning = jest.spyOn(core, 'warning').mockImplementation(() => null) | ||
spyExportVariable = jest | ||
.spyOn(core, 'exportVariable') | ||
.mockImplementation(() => null) | ||
jest.spyOn(core, 'getInput').mockImplementation((name: string) => { | ||
if (name === 'native-image-enable-sbom') { | ||
return 'true' | ||
} | ||
if (name === 'github-token') { | ||
return 'fake-token' | ||
} | ||
return '' | ||
}) | ||
}) | ||
|
||
afterEach(() => { | ||
process.env = originalEnv | ||
jest.clearAllMocks() | ||
spyInfo.mockRestore() | ||
spyWarning.mockRestore() | ||
spyExportVariable.mockRestore() | ||
rmSync(workspace, {recursive: true, force: true}) | ||
}) | ||
|
||
describe('setup', () => { | ||
it('should throw an error when the distribution is not Oracle GraalVM', () => { | ||
const not_supported_distributions = [ | ||
c.DISTRIBUTION_GRAALVM_COMMUNITY, | ||
c.DISTRIBUTION_MANDREL, | ||
c.DISTRIBUTION_LIBERICA, | ||
'' | ||
] | ||
for (const distribution of not_supported_distributions) { | ||
expect(() => setUpSBOMSupport(javaVersion, distribution)).toThrow() | ||
} | ||
}) | ||
|
||
it('should throw an error when the java-version is not supported', () => { | ||
const not_supported_versions = ['23', '23-ea', '21.0.3', 'dev', '17', ''] | ||
for (const version of not_supported_versions) { | ||
expect(() => setUpSBOMSupport(version, distribution)).toThrow() | ||
} | ||
}) | ||
|
||
it('should not throw an error when the java-version is supported', () => { | ||
const supported_versions = ['24', '24-ea', '24.0.2', 'latest-ea'] | ||
for (const version of supported_versions) { | ||
expect(() => setUpSBOMSupport(version, distribution)).not.toThrow() | ||
} | ||
}) | ||
|
||
it('should set the SBOM option when activated', () => { | ||
setUpSBOMSupport(javaVersion, distribution) | ||
|
||
expect(spyExportVariable).toHaveBeenCalledWith( | ||
c.NATIVE_IMAGE_OPTIONS_ENV, | ||
expect.stringContaining('--enable-sbom=export') | ||
) | ||
expect(spyInfo).toHaveBeenCalledWith( | ||
'Enabled SBOM generation for Native Image build' | ||
) | ||
expect(spyWarning).not.toHaveBeenCalled() | ||
}) | ||
|
||
it('should not set the SBOM option when not activated', () => { | ||
jest.spyOn(core, 'getInput').mockReturnValue('false') | ||
setUpSBOMSupport(javaVersion, distribution) | ||
|
||
expect(spyExportVariable).not.toHaveBeenCalled() | ||
expect(spyInfo).not.toHaveBeenCalled() | ||
expect(spyWarning).not.toHaveBeenCalled() | ||
}) | ||
}) | ||
|
||
describe('process', () => { | ||
async function setUpAndProcessSBOM(sbom: object): Promise<void> { | ||
setUpSBOMSupport(javaVersion, distribution) | ||
spyInfo.mockClear() | ||
|
||
// Mock 'native-image' invocation by creating the SBOM file | ||
const sbomPath = join(workspace, 'test.sbom.json') | ||
writeFileSync(sbomPath, JSON.stringify(sbom, null, 2)) | ||
|
||
mockFindSBOM([sbomPath]) | ||
|
||
await processSBOM() | ||
} | ||
|
||
const sampleSBOM = { | ||
bomFormat: 'CycloneDX', | ||
specVersion: '1.5', | ||
version: 1, | ||
serialNumber: 'urn:uuid:52c977f8-6d04-3c07-8826-597a036d61a6', | ||
components: [ | ||
{ | ||
type: 'library', | ||
group: 'org.json', | ||
name: 'json', | ||
version: '20211205', | ||
purl: 'pkg:maven/org.json/json@20211205', | ||
'bom-ref': 'pkg:maven/org.json/json@20211205', | ||
properties: [ | ||
{ | ||
name: 'syft:cpe23', | ||
value: 'cpe:2.3:a:json:json:20211205:*:*:*:*:*:*:*' | ||
} | ||
] | ||
}, | ||
{ | ||
type: 'library', | ||
group: 'com.oracle', | ||
name: 'main-test-app', | ||
version: '1.0-SNAPSHOT', | ||
purl: 'pkg:maven/com.oracle/[email protected]', | ||
'bom-ref': 'pkg:maven/com.oracle/[email protected]' | ||
} | ||
], | ||
dependencies: [ | ||
{ | ||
ref: 'pkg:maven/com.oracle/[email protected]', | ||
dependsOn: ['pkg:maven/org.json/json@20211205'] | ||
}, | ||
{ | ||
ref: 'pkg:maven/org.json/json@20211205', | ||
dependsOn: [] | ||
} | ||
] | ||
} | ||
|
||
it('should process SBOM and display components', async () => { | ||
await setUpAndProcessSBOM(sampleSBOM) | ||
|
||
expect(spyInfo).toHaveBeenCalledWith( | ||
'Found SBOM: ' + join(workspace, 'test.sbom.json') | ||
) | ||
expect(spyInfo).toHaveBeenCalledWith('=== SBOM Content ===') | ||
expect(spyInfo).toHaveBeenCalledWith('- pkg:maven/org.json/json@20211205') | ||
expect(spyInfo).toHaveBeenCalledWith( | ||
'- pkg:maven/com.oracle/[email protected]' | ||
) | ||
expect(spyInfo).toHaveBeenCalledWith( | ||
' depends on: pkg:maven/org.json/json@20211205' | ||
) | ||
expect(spyWarning).not.toHaveBeenCalled() | ||
}) | ||
|
||
it('should handle components without purl', async () => { | ||
const sbomWithoutPurl = { | ||
...sampleSBOM, | ||
components: [ | ||
{ | ||
type: 'library', | ||
name: 'no-purl-package', | ||
version: '1.0.0', | ||
'bom-ref': '[email protected]' | ||
} | ||
] | ||
} | ||
await setUpAndProcessSBOM(sbomWithoutPurl) | ||
|
||
expect(spyInfo).toHaveBeenCalledWith('=== SBOM Content ===') | ||
expect(spyInfo).toHaveBeenCalledWith('- [email protected]') | ||
expect(spyWarning).not.toHaveBeenCalled() | ||
}) | ||
|
||
it('should handle missing SBOM file', async () => { | ||
setUpSBOMSupport(javaVersion, distribution) | ||
spyInfo.mockClear() | ||
|
||
mockFindSBOM([]) | ||
|
||
await expect(processSBOM()).rejects.toBeInstanceOf(Error) | ||
}) | ||
|
||
it('should throw when JSON contains an invalid SBOM', async () => { | ||
const invalidSBOM = { | ||
'out-of-spec-field': {} | ||
} | ||
try { | ||
await setUpAndProcessSBOM(invalidSBOM) | ||
fail('Expected an error since invalid JSON was passed') | ||
} catch (error) { | ||
expect(error).toBeInstanceOf(Error) | ||
} | ||
}) | ||
|
||
it('should submit dependencies when processing valid SBOM', async () => { | ||
const mockOctokit = mockGithubAPIReturnValue(undefined) | ||
await setUpAndProcessSBOM(sampleSBOM) | ||
|
||
expect(mockOctokit.request).toHaveBeenCalledWith( | ||
'POST /repos/{owner}/{repo}/dependency-graph/snapshots', | ||
expect.objectContaining({ | ||
owner: 'test-owner', | ||
repo: 'test-repo', | ||
version: expect.any(Number), | ||
sha: 'test-sha', | ||
ref: 'test-ref', | ||
job: expect.objectContaining({ | ||
correlator: 'test-workflow_test-job', | ||
id: '12345' | ||
}), | ||
manifests: expect.objectContaining({ | ||
'test.sbom.json': expect.objectContaining({ | ||
name: 'test.sbom.json', | ||
resolved: expect.objectContaining({ | ||
json: expect.objectContaining({ | ||
package_url: 'pkg:maven/org.json/json@20211205', | ||
dependencies: [] | ||
}), | ||
'main-test-app': expect.objectContaining({ | ||
package_url: | ||
'pkg:maven/com.oracle/[email protected]', | ||
dependencies: ['pkg:maven/org.json/json@20211205'] | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) | ||
) | ||
expect(spyInfo).toHaveBeenCalledWith( | ||
'Dependency snapshot submitted successfully.' | ||
) | ||
}) | ||
|
||
it('should handle GitHub API submission errors gracefully', async () => { | ||
mockGithubAPIReturnValue(new Error('API submission failed')) | ||
|
||
await expect(setUpAndProcessSBOM(sampleSBOM)).rejects.toBeInstanceOf( | ||
Error | ||
) | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.