forked from graalvm/setup-graalvm
-
Notifications
You must be signed in to change notification settings - Fork 0
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
16 changed files
with
1,583 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,285 @@ | ||
import * as c from '../src/constants' | ||
import { | ||
setUpSBOMSupport, | ||
processSBOM, | ||
mapToComponentsWithDependencies, | ||
INPUT_NI_SBOM | ||
} 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' | ||
} | ||
})) | ||
|
||
// sbom.ts uses glob to find the SBOM file | ||
// This helper function mocks the glob module to return 'files' | ||
function mockGlobResult(files: string[]) { | ||
const mockCreate = jest.fn().mockResolvedValue({ | ||
glob: jest.fn().mockResolvedValue(files) | ||
}) | ||
;(glob.create as jest.Mock).mockImplementation(mockCreate) | ||
} | ||
|
||
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 | ||
|
||
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-')) | ||
;(github.getOctokit as jest.Mock).mockReturnValue({ | ||
request: jest.fn().mockResolvedValue(undefined) | ||
}) | ||
|
||
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 === INPUT_NI_SBOM) { | ||
return 'true' | ||
} | ||
if (name === 'github-token') { | ||
return 'fake-token' | ||
} | ||
return '' | ||
}) | ||
}) | ||
|
||
afterEach(() => { | ||
// Restore original env | ||
process.env = originalEnv | ||
|
||
jest.clearAllMocks() | ||
rmSync(workspace, {recursive: true, force: true}) | ||
}) | ||
|
||
describe('setup', () => { | ||
it('should set the SBOM option flag when activated', () => { | ||
setUpSBOMSupport() | ||
expect(spyExportVariable).toHaveBeenCalledWith( | ||
c.NATIVE_IMAGE_OPTIONS_ENV, | ||
expect.stringContaining('--enable-sbom=export') | ||
) | ||
expect(spyInfo).toHaveBeenCalledWith( | ||
'Enabled SBOM generation for Native Image builds' | ||
) | ||
}) | ||
|
||
it('should not set the SBOM option flag when not activated', () => { | ||
jest.spyOn(core, 'getInput').mockReturnValue('false') | ||
setUpSBOMSupport() | ||
expect(spyExportVariable).not.toHaveBeenCalled() | ||
expect(spyInfo).not.toHaveBeenCalled() | ||
}) | ||
}) | ||
|
||
describe('process', () => { | ||
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 file and display components', async () => { | ||
setUpSBOMSupport() | ||
spyInfo.mockClear() | ||
|
||
// Mock 'native-image' invocation by creating the SBOM file | ||
const sbomPath = join(workspace, 'test.sbom.json') | ||
writeFileSync(sbomPath, JSON.stringify(sampleSBOM, null, 2)) | ||
|
||
mockGlobResult([sbomPath]) | ||
|
||
await processSBOM() | ||
|
||
expect(spyInfo).toHaveBeenCalledWith('Found SBOM file: ' + sbomPath) | ||
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 () => { | ||
setUpSBOMSupport() | ||
spyInfo.mockClear() | ||
|
||
const sbomWithoutPurl = { | ||
...sampleSBOM, | ||
components: [ | ||
{ | ||
type: 'library', | ||
name: 'no-purl-package', | ||
version: '1.0.0', | ||
'bom-ref': '[email protected]' | ||
} | ||
] | ||
} | ||
|
||
const sbomPath = join(workspace, 'test.sbom.json') | ||
writeFileSync(sbomPath, JSON.stringify(sbomWithoutPurl, null, 2)) | ||
|
||
mockGlobResult([sbomPath]) | ||
|
||
await processSBOM() | ||
|
||
expect(spyInfo).toHaveBeenCalledWith('=== SBOM Content ===') | ||
expect(spyInfo).toHaveBeenCalledWith('- [email protected]') | ||
expect(spyWarning).not.toHaveBeenCalled() | ||
}) | ||
|
||
it('should handle missing SBOM file', async () => { | ||
setUpSBOMSupport() | ||
spyInfo.mockClear() | ||
|
||
mockGlobResult([]) | ||
|
||
await processSBOM() | ||
expect(spyWarning).toHaveBeenCalledWith( | ||
'No SBOM file found. Make sure native-image build completed successfully. Skipping submission to GitHub Dependency API.' | ||
) | ||
}) | ||
|
||
it('should submit dependency snapshot when processing valid SBOM', async () => { | ||
setUpSBOMSupport() | ||
spyInfo.mockClear() | ||
|
||
const mockOctokit = { | ||
request: jest.fn().mockResolvedValue(undefined) | ||
} | ||
;(github.getOctokit as jest.Mock).mockReturnValue(mockOctokit) | ||
|
||
const sbomPath = join(workspace, 'test.sbom.json') | ||
writeFileSync(sbomPath, JSON.stringify(sampleSBOM, null, 2)) | ||
|
||
mockGlobResult([sbomPath]) | ||
|
||
await processSBOM() | ||
|
||
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'] | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) | ||
) | ||
}) | ||
|
||
it('should handle GitHub API submission errors gracefully', async () => { | ||
setUpSBOMSupport() | ||
spyInfo.mockClear() | ||
|
||
const mockOctokit = { | ||
request: jest.fn().mockRejectedValue(new Error('API submission failed')) | ||
} | ||
;(github.getOctokit as jest.Mock).mockReturnValue(mockOctokit) | ||
|
||
const sbomPath = join(workspace, 'test.sbom.json') | ||
writeFileSync(sbomPath, JSON.stringify(sampleSBOM, null, 2)) | ||
|
||
mockGlobResult([sbomPath]) | ||
|
||
await processSBOM() | ||
|
||
expect(spyWarning).toHaveBeenCalledWith( | ||
expect.stringContaining('Failed to submit dependency snapshot') | ||
) | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.