mirror of
https://github.com/graalvm/setup-graalvm.git
synced 2025-03-13 14:30:15 +08:00
POC sbom integration (only printing SBOM to console, no Github integration)
This commit is contained in:
parent
4a200f28cd
commit
c7e990f07f
21
.github/workflows/test.yml
vendored
21
.github/workflows/test.yml
vendored
@ -417,3 +417,24 @@ jobs:
|
|||||||
# popd > /dev/null
|
# popd > /dev/null
|
||||||
- name: Remove components
|
- name: Remove components
|
||||||
run: gu remove espresso llvm-toolchain nodejs python ruby wasm
|
run: gu remove espresso llvm-toolchain nodejs python ruby wasm
|
||||||
|
test-sbom:
|
||||||
|
needs: test
|
||||||
|
name: test 'native-image-enable-sbom' option
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./
|
||||||
|
with:
|
||||||
|
java-version: '23'
|
||||||
|
distribution: 'graalvm'
|
||||||
|
native-image-enable-sbom: 'true'
|
||||||
|
components: 'native-image'
|
||||||
|
- name: Verify SBOM was generated
|
||||||
|
run: |
|
||||||
|
# Create a simple native image that will generate SBOM
|
||||||
|
echo 'public class Hello { public static void main(String[] args) { System.out.println("Hello"); } }' > Hello.java
|
||||||
|
javac Hello.java
|
||||||
|
# '--enable-sbom=export' should get injected into the native-image command
|
||||||
|
native-image Hello
|
||||||
|
# Verify SBOM file exists
|
||||||
|
find . -name "*.sbom.json" | grep . || exit 1
|
||||||
|
135
__tests__/sbom.test.ts
Normal file
135
__tests__/sbom.test.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import {setUpSBOMSupport, processSBOM, INPUT_NI_SBOM, NATIVE_IMAGE_OPTIONS_ENV} from '../src/features/sbom'
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as glob from '@actions/glob'
|
||||||
|
import {join} from 'path'
|
||||||
|
import {tmpdir} from 'os'
|
||||||
|
import {mkdtempSync, writeFileSync, rmSync} from 'fs'
|
||||||
|
|
||||||
|
// Module level mock
|
||||||
|
jest.mock('@actions/glob')
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
workspace = mkdtempSync(join(tmpdir(), 'setup-graalvm-sbom-'))
|
||||||
|
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
rmSync(workspace, {recursive: true, force: true})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('setup', () => {
|
||||||
|
it('should set the SBOM option flag when activated', () => {
|
||||||
|
setUpSBOMSupport()
|
||||||
|
expect(spyExportVariable).toHaveBeenCalledWith(
|
||||||
|
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/main-test-app@1.0-SNAPSHOT",
|
||||||
|
"bom-ref": "pkg:maven/com.oracle/main-test-app@1.0-SNAPSHOT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
{
|
||||||
|
ref: "pkg:maven/com.oracle/main-test-app@1.0-SNAPSHOT",
|
||||||
|
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))
|
||||||
|
|
||||||
|
const mockCreate = jest.fn().mockResolvedValue({
|
||||||
|
glob: jest.fn().mockResolvedValue([sbomPath])
|
||||||
|
})
|
||||||
|
;(glob.create as jest.Mock).mockImplementation(mockCreate)
|
||||||
|
|
||||||
|
await processSBOM()
|
||||||
|
|
||||||
|
expect(spyInfo).toHaveBeenCalledWith('Found SBOM file: ' + sbomPath)
|
||||||
|
expect(spyInfo).toHaveBeenCalledWith('=== SBOM Content ===')
|
||||||
|
expect(spyInfo).toHaveBeenCalledWith('Found 2 dependencies:')
|
||||||
|
expect(spyInfo).toHaveBeenCalledWith('- json@20211205')
|
||||||
|
expect(spyInfo).toHaveBeenCalledWith('- main-test-app@1.0-SNAPSHOT')
|
||||||
|
expect(spyWarning).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle missing SBOM file', async () => {
|
||||||
|
setUpSBOMSupport()
|
||||||
|
spyInfo.mockClear()
|
||||||
|
|
||||||
|
// Mock glob to return empty array (no files found)
|
||||||
|
const mockCreate = jest.fn().mockResolvedValue({
|
||||||
|
glob: jest.fn().mockResolvedValue([])
|
||||||
|
})
|
||||||
|
;(glob.create as jest.Mock).mockImplementation(mockCreate)
|
||||||
|
|
||||||
|
await processSBOM()
|
||||||
|
expect(spyWarning).toHaveBeenCalledWith(
|
||||||
|
'No SBOM file found. Make sure native-image build completed successfully.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -51,6 +51,10 @@ inputs:
|
|||||||
required: false
|
required: false
|
||||||
description: 'Instead of posting another comment, update an existing PR comment with the latest Native Image build report.'
|
description: 'Instead of posting another comment, update an existing PR comment with the latest Native Image build report.'
|
||||||
default: 'false'
|
default: 'false'
|
||||||
|
native-image-enable-sbom:
|
||||||
|
required: false
|
||||||
|
description: 'Enable SBOM generation for Native Image builds. The SBOM dependencies are shown in the dependency view in Github.'
|
||||||
|
default: 'false'
|
||||||
version:
|
version:
|
||||||
required: false
|
required: false
|
||||||
description: 'GraalVM version (release, latest, dev).'
|
description: 'GraalVM version (release, latest, dev).'
|
||||||
|
9
package-lock.json
generated
9
package-lock.json
generated
@ -40,7 +40,7 @@
|
|||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-eslint": "^16.3.0",
|
"prettier-eslint": "^16.3.0",
|
||||||
"ts-jest": "^29.1.2",
|
"ts-jest": "^29.1.2",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@aashutoshrathi/word-wrap": {
|
"node_modules/@aashutoshrathi/word-wrap": {
|
||||||
@ -7644,10 +7644,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.4.2",
|
"version": "5.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
|
||||||
"integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==",
|
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
@ -58,6 +58,6 @@
|
|||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-eslint": "^16.3.0",
|
"prettier-eslint": "^16.3.0",
|
||||||
"ts-jest": "^29.1.2",
|
"ts-jest": "^29.1.2",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ import * as core from '@actions/core'
|
|||||||
import * as constants from './constants'
|
import * as constants from './constants'
|
||||||
import {save} from './features/cache'
|
import {save} from './features/cache'
|
||||||
import {generateReports} from './features/reports'
|
import {generateReports} from './features/reports'
|
||||||
|
import { processSBOM } from './features/sbom'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check given input and run a save process for the specified package manager
|
* Check given input and run a save process for the specified package manager
|
||||||
@ -58,6 +59,7 @@ async function ignoreErrors(promise: Promise<void>): Promise<unknown> {
|
|||||||
|
|
||||||
export async function run(): Promise<void> {
|
export async function run(): Promise<void> {
|
||||||
await ignoreErrors(generateReports())
|
await ignoreErrors(generateReports())
|
||||||
|
await ignoreErrors(processSBOM())
|
||||||
await ignoreErrors(saveCache())
|
await ignoreErrors(saveCache())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
170
src/features/sbom.ts
Normal file
170
src/features/sbom.ts
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as github from '@actions/github'
|
||||||
|
import * as glob from '@actions/glob'
|
||||||
|
|
||||||
|
export const INPUT_NI_SBOM = 'native-image-enable-sbom'
|
||||||
|
export const NATIVE_IMAGE_OPTIONS_ENV = 'NATIVE_IMAGE_OPTIONS'
|
||||||
|
|
||||||
|
export function setUpSBOMSupport(): void {
|
||||||
|
const isSbomEnabled = core.getInput(INPUT_NI_SBOM) === 'true'
|
||||||
|
if (!isSbomEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let options = process.env[NATIVE_IMAGE_OPTIONS_ENV] || ''
|
||||||
|
if (options.length > 0) {
|
||||||
|
options += ' '
|
||||||
|
}
|
||||||
|
options += '--enable-sbom=export'
|
||||||
|
core.exportVariable(NATIVE_IMAGE_OPTIONS_ENV, options)
|
||||||
|
core.info('Enabled SBOM generation for Native Image builds')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a single SBOM file in the build directory
|
||||||
|
* @returns Path to the SBOM file or null if not found or multiple files exist
|
||||||
|
*/
|
||||||
|
async function findSBOMFile(): Promise<string | null> {
|
||||||
|
const globber = await glob.create('**/*.sbom.json')
|
||||||
|
const sbomFiles = await globber.glob()
|
||||||
|
|
||||||
|
if (sbomFiles.length === 0) {
|
||||||
|
core.warning('No SBOM file found. Make sure native-image build completed successfully.')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sbomFiles.length > 1) {
|
||||||
|
core.warning(
|
||||||
|
`Found multiple SBOM files: ${sbomFiles.join(', ')}. ` +
|
||||||
|
'Expected exactly one SBOM file. Skipping SBOM processing.'
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
core.info(`Found SBOM file: ${sbomFiles[0]}`)
|
||||||
|
return sbomFiles[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
function displaySBOMContent(sbomData: any): void {
|
||||||
|
core.info('=== SBOM Content ===')
|
||||||
|
|
||||||
|
if (sbomData.components) {
|
||||||
|
core.info(`Found ${sbomData.components.length} dependencies:`)
|
||||||
|
for (const component of sbomData.components) {
|
||||||
|
core.info(`- ${component.name}@${component.version || 'unknown'}`)
|
||||||
|
if (component.dependencies?.length > 0) {
|
||||||
|
core.info(` Dependencies: ${component.dependencies.join(', ')}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
core.info('No components found in SBOM')
|
||||||
|
}
|
||||||
|
|
||||||
|
core.info('==================')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processSBOM(): Promise<void> {
|
||||||
|
const isSbomEnabled = core.getInput(INPUT_NI_SBOM) === 'true'
|
||||||
|
if (!isSbomEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const sbomFile = await findSBOMFile()
|
||||||
|
if (!sbomFile) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sbomContent = fs.readFileSync(sbomFile, 'utf8')
|
||||||
|
const sbomData = JSON.parse(sbomContent)
|
||||||
|
displaySBOMContent(sbomData)
|
||||||
|
} catch (error) {
|
||||||
|
core.warning(`Failed to process SBOM file: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DependencySnapshot {
|
||||||
|
version: number
|
||||||
|
sha: string
|
||||||
|
ref: string
|
||||||
|
job: {
|
||||||
|
correlator: string
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
detector: {
|
||||||
|
name: string
|
||||||
|
version: string
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
scanned: string
|
||||||
|
manifests: Record<string, {
|
||||||
|
name: string
|
||||||
|
file: {
|
||||||
|
source_location: string
|
||||||
|
}
|
||||||
|
resolved: Record<string, {
|
||||||
|
package_url: string
|
||||||
|
dependencies?: string[]
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
async function convertSBOMToSnapshot(sbomData: any): Promise<DependencySnapshot> {
|
||||||
|
const context = github.context
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: 0,
|
||||||
|
sha: context.sha,
|
||||||
|
ref: context.ref,
|
||||||
|
job: {
|
||||||
|
correlator: `${context.workflow}_${context.action}`,
|
||||||
|
id: context.runId.toString()
|
||||||
|
},
|
||||||
|
detector: {
|
||||||
|
name: 'graalvm-setup-sbom',
|
||||||
|
version: '1.0.0',
|
||||||
|
url: 'https://github.com/graalvm/setup-graalvm'
|
||||||
|
},
|
||||||
|
scanned: new Date().toISOString(),
|
||||||
|
manifests: {
|
||||||
|
'native-image-sbom.json': {
|
||||||
|
name: 'native-image-sbom.json',
|
||||||
|
file: {
|
||||||
|
source_location: 'native-image-sbom.json'
|
||||||
|
},
|
||||||
|
resolved: convertSBOMDependencies(sbomData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertSBOMDependencies(sbomData: any): Record<string, {package_url: string, dependencies?: string[]}> {
|
||||||
|
const resolved: Record<string, {package_url: string, dependencies?: string[]}> = {}
|
||||||
|
|
||||||
|
if (sbomData.components) {
|
||||||
|
for (const component of sbomData.components) {
|
||||||
|
if (component.name && component.version) {
|
||||||
|
resolved[component.name] = {
|
||||||
|
package_url: `pkg:${component.type || 'maven'}/${component.name}@${component.version}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.dependencies?.length > 0) {
|
||||||
|
resolved[component.name].dependencies = component.dependencies
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitDependencySnapshot(snapshot: DependencySnapshot): Promise<void> {
|
||||||
|
const token = core.getInput('github-token')
|
||||||
|
const octokit = github.getOctokit(token)
|
||||||
|
// await octokit.rest.dependencyGraph.createSnapshot({
|
||||||
|
// owner: github.context.repo.owner,
|
||||||
|
// repo: github.context.repo.repo,
|
||||||
|
// ...snapshot
|
||||||
|
// })
|
||||||
|
}
|
@ -14,6 +14,7 @@ import {setUpNativeImageMusl} from './features/musl'
|
|||||||
import {setUpWindowsEnvironment} from './msvc'
|
import {setUpWindowsEnvironment} from './msvc'
|
||||||
import {setUpNativeImageBuildReports} from './features/reports'
|
import {setUpNativeImageBuildReports} from './features/reports'
|
||||||
import {exec} from '@actions/exec'
|
import {exec} from '@actions/exec'
|
||||||
|
import {setUpSBOMSupport} from './features/sbom'
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@ -166,6 +167,8 @@ async function run(): Promise<void> {
|
|||||||
graalVMVersion
|
graalVMVersion
|
||||||
)
|
)
|
||||||
|
|
||||||
|
setUpSBOMSupport()
|
||||||
|
|
||||||
core.startGroup(`Successfully set up '${basename(graalVMHome)}'`)
|
core.startGroup(`Successfully set up '${basename(graalVMHome)}'`)
|
||||||
await exec(join(graalVMHome, 'bin', `java${c.EXECUTABLE_SUFFIX}`), [
|
await exec(join(graalVMHome, 'bin', `java${c.EXECUTABLE_SUFFIX}`), [
|
||||||
javaVersion.startsWith('8') ? '-version' : '--version'
|
javaVersion.startsWith('8') ? '-version' : '--version'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user