diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aa49c59..f82b69e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -420,3 +420,40 @@ jobs: # popd > /dev/null - name: Remove components run: gu remove espresso llvm-toolchain nodejs python ruby wasm + test-sbom: + name: test 'native-image-enable-sbom' option + runs-on: ${{ matrix.os }} + permissions: + contents: write + strategy: + matrix: + java-version: ['24-ea', 'latest-ea'] + distribution: ['graalvm'] + os: [macos-latest, windows-latest, ubuntu-latest] + set-gds-token: [false] + components: [''] + steps: + - uses: actions/checkout@v4 + - name: Run setup-graalvm action + uses: ./ + with: + java-version: ${{ matrix.java-version }} + distribution: ${{ matrix.distribution }} + github-token: ${{ secrets.GITHUB_TOKEN }} + components: ${{ matrix.components }} + gds-token: ${{ matrix.set-gds-token && secrets.GDS_TOKEN || '' }} + native-image-enable-sbom: 'true' + - name: Build Maven project and verify that SBOM was generated and its contents + run: | + cd __tests__/sbom/main-test-app + mvn --no-transfer-progress -Pnative package + bash verify-sbom.sh + shell: bash + if: runner.os != 'Windows' + - name: Build Maven project and verify that SBOM was generated and its contents (Windows) + run: | + cd __tests__\sbom\main-test-app + mvn --no-transfer-progress -Pnative package + cmd /c verify-sbom.cmd + shell: cmd + if: runner.os == 'Windows' \ No newline at end of file diff --git a/.gitignore b/.gitignore index 18e337d..ed52253 100644 --- a/.gitignore +++ b/.gitignore @@ -96,4 +96,7 @@ Thumbs.db # Ignore built ts files __tests__/runner/* -lib/**/* \ No newline at end of file +lib/**/* + +# Ignore target directory in __tests__ +__tests__/**/target diff --git a/README.md b/README.md index 58a589a..a8590ee 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,7 @@ This actions can be configured with the following options: | `native-image-job-reports` *) | `'false'` | If set to `'true'`, post a job summary containing a Native Image build report. | | `native-image-pr-reports` *) | `'false'` | If set to `'true'`, post a comment containing a Native Image build report on pull requests. Requires `write` permissions for the [`pull-requests` scope][gha-permissions]. | | `native-image-pr-reports-update-existing` *) | `'false'` | Instead of posting another comment, update an existing PR comment with the latest Native Image build report. Requires `native-image-pr-reports` to be `true`. | +| `native-image-enable-sbom` | `'false'` | If set to `'true'`, generate a minimal SBOM based on the Native Image static analysis and submit it to GitHub's dependency submission API. This enables the [dependency graph feature](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-the-dependency-graph) for dependency tracking and vulnerability analysis. Requires `write` permissions for the [`contents` scope][gha-permissions] and the dependency graph to be actived (on by default for public repositories - see [how to activate](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/configuring-the-dependency-graph#enabling-and-disabling-the-dependency-graph-for-a-private-repository)). Only available in Oracle GraalVM for JDK 24 or later. | | `components` | `''` | Comma-separated list of GraalVM components (e.g., `native-image` or `ruby,nodejs`) that will be installed by the [GraalVM Updater][gu]. | | `version` | `''` | `X.Y.Z` (e.g., `22.3.0`) for a specific [GraalVM release][releases] up to `22.3.2`
`mandrel-X.Y.Z.W` or `X.Y.Z.W-Final` (e.g., `mandrel-21.3.0.0-Final` or `21.3.0.0-Final`) for a specific [Mandrel release][mandrel-releases],
`mandrel-latest` or `latest` for the latest Mandrel stable release. | | `gds-token` | `''` Download token for the GraalVM Download Service. If a non-empty token is provided, the action will set up Oracle GraalVM (see [Oracle GraalVM via GDS template](#template-for-oracle-graalvm-via-graalvm-download-service)) or GraalVM Enterprise Edition (see [GraalVM EE template](#template-for-graalvm-enterprise-edition)) via GDS. | diff --git a/__tests__/cleanup.test.ts b/__tests__/cleanup.test.ts index b43d151..9a70f2a 100644 --- a/__tests__/cleanup.test.ts +++ b/__tests__/cleanup.test.ts @@ -49,7 +49,7 @@ describe('cleanup', () => { resetState() }) - it('does not fail nor warn even when the save provess throws a ReserveCacheError', async () => { + it('does not fail nor warn even when the save process throws a ReserveCacheError', async () => { spyCacheSave.mockImplementation((paths: string[], key: string) => Promise.reject( new cache.ReserveCacheError( diff --git a/__tests__/sbom.test.ts b/__tests__/sbom.test.ts new file mode 100644 index 0000000..f0fabf6 --- /dev/null +++ b/__tests__/sbom.test.ts @@ -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> + let spyWarning: jest.SpyInstance> + let spyExportVariable: jest.SpyInstance< + void, + Parameters + > + 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 { + 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: '20241224', + purl: 'pkg:maven/org.json/json@20241224', + 'bom-ref': 'pkg:maven/org.json/json@20241224', + properties: [ + { + name: 'syft:cpe23', + value: 'cpe:2.3:a:json:json:20241224:*:*:*:*:*:*:*' + } + ] + }, + { + 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@20241224'] + }, + { + ref: 'pkg:maven/org.json/json@20241224', + 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@20241224') + expect(spyInfo).toHaveBeenCalledWith( + '- pkg:maven/com.oracle/main-test-app@1.0-SNAPSHOT' + ) + expect(spyInfo).toHaveBeenCalledWith( + ' depends on: pkg:maven/org.json/json@20241224' + ) + 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': 'no-purl-package@1.0.0' + } + ] + } + await setUpAndProcessSBOM(sbomWithoutPurl) + + expect(spyInfo).toHaveBeenCalledWith('=== SBOM Content ===') + expect(spyInfo).toHaveBeenCalledWith('- no-purl-package@1.0.0') + 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@20241224', + dependencies: [] + }), + 'main-test-app': expect.objectContaining({ + package_url: + 'pkg:maven/com.oracle/main-test-app@1.0-SNAPSHOT', + dependencies: ['pkg:maven/org.json/json@20241224'] + }) + }) + }) + }) + }) + ) + 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 + ) + }) + }) +}) diff --git a/__tests__/sbom/main-test-app/pom.xml b/__tests__/sbom/main-test-app/pom.xml new file mode 100644 index 0000000..ec9ae9f --- /dev/null +++ b/__tests__/sbom/main-test-app/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + com.oracle + main-test-app + 1.0.0 + + + 17 + 17 + + + + + org.json + json + 20241224 + + + + + + native + + + + org.graalvm.buildtools + native-maven-plugin + 0.10.3 + + + + compile-no-fork + + package + + + + com.oracle.sbom.SBOMTestApplication + + -Ob + --no-fallback + -H:+ReportExceptionStackTraces + + + + + + + + \ No newline at end of file diff --git a/__tests__/sbom/main-test-app/src/main/java/com/oracle/sbom/SBOMTestApplication.java b/__tests__/sbom/main-test-app/src/main/java/com/oracle/sbom/SBOMTestApplication.java new file mode 100644 index 0000000..5164f7b --- /dev/null +++ b/__tests__/sbom/main-test-app/src/main/java/com/oracle/sbom/SBOMTestApplication.java @@ -0,0 +1,12 @@ +package com.oracle.sbom; + +import org.json.JSONObject; + +public class SBOMTestApplication { + public static void main(String argv[]) { + JSONObject jo = new JSONObject(); + jo.put("lorem", "ipsum"); + jo.put("dolor", "sit amet"); + System.out.println(jo); + } +} diff --git a/__tests__/sbom/main-test-app/verify-sbom.cmd b/__tests__/sbom/main-test-app/verify-sbom.cmd new file mode 100644 index 0000000..de5cd0c --- /dev/null +++ b/__tests__/sbom/main-test-app/verify-sbom.cmd @@ -0,0 +1,14 @@ +@echo off +set "SCRIPT_DIR=%~dp0" + +for %%p in ( + "\"pkg:maven/org.json/json@20241224\"" + "\"main-test-app\"" + "\"svm\"" + "\"nativeimage\"" +) do ( + echo Checking for %%p + findstr /c:%%p "%SCRIPT_DIR%target\main-test-app.sbom.json" || exit /b 1 +) + +echo SBOM was successfully generated and contained the expected components \ No newline at end of file diff --git a/__tests__/sbom/main-test-app/verify-sbom.sh b/__tests__/sbom/main-test-app/verify-sbom.sh new file mode 100644 index 0000000..c9c2f7d --- /dev/null +++ b/__tests__/sbom/main-test-app/verify-sbom.sh @@ -0,0 +1,19 @@ +#!/bin/bash +script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +required_patterns=( + '"pkg:maven/org.json/json@20241224"' + '"main-test-app"' + '"svm"' + '"nativeimage"' +) + +for pattern in "${required_patterns[@]}"; do + echo "Checking for $pattern" + if ! grep -q "$pattern" "$script_dir/target/main-test-app.sbom.json"; then + echo "Pattern not found: $pattern" + exit 1 + fi +done + +echo "SBOM was successfully generated and contained the expected components" \ No newline at end of file diff --git a/action.yml b/action.yml index 47fa5b6..a261d35 100644 --- a/action.yml +++ b/action.yml @@ -51,6 +51,10 @@ inputs: required: false description: 'Instead of posting another comment, update an existing PR comment with the latest Native Image build report.' default: 'false' + native-image-enable-sbom: + required: false + description: 'Automatically generate an SBOM and submit it to the GitHub dependency submission API for vulnerability and dependency tracking.' + default: 'false' version: required: false description: 'GraalVM version (release, latest, dev).' diff --git a/dist/cleanup/index.js b/dist/cleanup/index.js index c6bc466..76f4a5d 100644 --- a/dist/cleanup/index.js +++ b/dist/cleanup/index.js @@ -98011,6 +98011,7 @@ const core = __importStar(__nccwpck_require__(2186)); const constants = __importStar(__nccwpck_require__(9042)); const cache_1 = __nccwpck_require__(9179); const reports_1 = __nccwpck_require__(2046); +const sbom_1 = __nccwpck_require__(9181); /** * Check given input and run a save process for the specified package manager * @returns Promise that will be resolved when the save process finishes @@ -98043,6 +98044,7 @@ function ignoreErrors(promise) { function run() { return __awaiter(this, void 0, void 0, function* () { yield ignoreErrors((0, reports_1.generateReports)()); + yield ignoreErrors((0, sbom_1.processSBOM)()); yield ignoreErrors(saveCache()); }); } @@ -98064,7 +98066,7 @@ else { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.ERROR_HINT = exports.ERROR_REQUEST = exports.EVENT_NAME_PULL_REQUEST = exports.ENV_GITHUB_EVENT_NAME = exports.GDS_GRAALVM_PRODUCT_ID = exports.GDS_BASE = exports.MANDREL_NAMESPACE = exports.GRAALVM_RELEASES_REPO = exports.GRAALVM_PLATFORM = exports.GRAALVM_GH_USER = exports.GRAALVM_FILE_EXTENSION = exports.GRAALVM_ARCH = exports.JDK_HOME_SUFFIX = exports.JDK_PLATFORM = exports.JDK_ARCH = exports.VERSION_LATEST = exports.VERSION_DEV = exports.DISTRIBUTION_LIBERICA = exports.DISTRIBUTION_MANDREL = exports.DISTRIBUTION_GRAALVM_COMMUNITY = exports.DISTRIBUTION_GRAALVM = exports.EXECUTABLE_SUFFIX = exports.IS_WINDOWS = exports.IS_MACOS = exports.IS_LINUX = exports.INPUT_NI_MUSL = exports.INPUT_CHECK_FOR_UPDATES = exports.INPUT_CACHE = exports.INPUT_SET_JAVA_HOME = exports.INPUT_GITHUB_TOKEN = exports.INPUT_COMPONENTS = exports.INPUT_DISTRIBUTION = exports.INPUT_JAVA_PACKAGE = exports.INPUT_JAVA_VERSION = exports.INPUT_GDS_TOKEN = exports.INPUT_VERSION = exports.ACTION_VERSION = void 0; +exports.ERROR_HINT = exports.ERROR_REQUEST = exports.EVENT_NAME_PULL_REQUEST = exports.ENV_GITHUB_EVENT_NAME = exports.GDS_GRAALVM_PRODUCT_ID = exports.GDS_BASE = exports.MANDREL_NAMESPACE = exports.GRAALVM_RELEASES_REPO = exports.GRAALVM_PLATFORM = exports.GRAALVM_GH_USER = exports.GRAALVM_FILE_EXTENSION = exports.GRAALVM_ARCH = exports.JDK_HOME_SUFFIX = exports.JDK_PLATFORM = exports.JDK_ARCH = exports.VERSION_LATEST = exports.VERSION_DEV = exports.DISTRIBUTION_LIBERICA = exports.DISTRIBUTION_MANDREL = exports.DISTRIBUTION_GRAALVM_COMMUNITY = exports.DISTRIBUTION_GRAALVM = exports.EXECUTABLE_SUFFIX = exports.IS_WINDOWS = exports.IS_MACOS = exports.IS_LINUX = exports.NATIVE_IMAGE_OPTIONS_ENV = exports.INPUT_NI_MUSL = exports.INPUT_CHECK_FOR_UPDATES = exports.INPUT_CACHE = exports.INPUT_SET_JAVA_HOME = exports.INPUT_GITHUB_TOKEN = exports.INPUT_COMPONENTS = exports.INPUT_DISTRIBUTION = exports.INPUT_JAVA_PACKAGE = exports.INPUT_JAVA_VERSION = exports.INPUT_GDS_TOKEN = exports.INPUT_VERSION = exports.ACTION_VERSION = void 0; exports.ACTION_VERSION = '1.2.7'; exports.INPUT_VERSION = 'version'; exports.INPUT_GDS_TOKEN = 'gds-token'; @@ -98077,6 +98079,7 @@ exports.INPUT_SET_JAVA_HOME = 'set-java-home'; exports.INPUT_CACHE = 'cache'; exports.INPUT_CHECK_FOR_UPDATES = 'check-for-updates'; exports.INPUT_NI_MUSL = 'native-image-musl'; +exports.NATIVE_IMAGE_OPTIONS_ENV = 'NATIVE_IMAGE_OPTIONS'; exports.IS_LINUX = process.platform === 'linux'; exports.IS_MACOS = process.platform === 'darwin'; exports.IS_WINDOWS = process.platform === 'win32'; @@ -98420,10 +98423,8 @@ const core = __importStar(__nccwpck_require__(2186)); const fs = __importStar(__nccwpck_require__(7147)); const github = __importStar(__nccwpck_require__(5438)); const semver = __importStar(__nccwpck_require__(1383)); -const path_1 = __nccwpck_require__(1017); -const os_1 = __nccwpck_require__(2037); const utils_1 = __nccwpck_require__(1314); -const BUILD_OUTPUT_JSON_PATH = (0, path_1.join)((0, os_1.tmpdir)(), 'native-image-build-output.json'); +const BUILD_OUTPUT_JSON_PATH = (0, utils_1.tmpfile)('native-image-build-output.json'); const BYTES_TO_KiB = 1024; const BYTES_TO_MiB = 1024 * 1024; const BYTES_TO_GiB = 1024 * 1024 * 1024; @@ -98431,9 +98432,6 @@ const DOCS_BASE = 'https://github.com/oracle/graal/blob/master/docs/reference-ma const INPUT_NI_JOB_REPORTS = 'native-image-job-reports'; const INPUT_NI_PR_REPORTS = 'native-image-pr-reports'; const INPUT_NI_PR_REPORTS_UPDATE = 'native-image-pr-reports-update-existing'; -const NATIVE_IMAGE_CONFIG_FILE = (0, path_1.join)((0, os_1.tmpdir)(), 'native-image-options.properties'); -const NATIVE_IMAGE_OPTIONS_ENV = 'NATIVE_IMAGE_OPTIONS'; -const NATIVE_IMAGE_CONFIG_FILE_ENV = 'NATIVE_IMAGE_CONFIG_FILE'; const PR_COMMENT_TITLE = '## GraalVM Native Image Build Report'; function setUpNativeImageBuildReports(isGraalVMforJDK17OrLater, javaVersionOrDev, graalVMVersion) { return __awaiter(this, void 0, void 0, function* () { @@ -98450,7 +98448,7 @@ function setUpNativeImageBuildReports(isGraalVMforJDK17OrLater, javaVersionOrDev core.warning(`Build reports for PRs and job summaries are only available in GraalVM 22.2.0 or later. This build job uses GraalVM ${graalVMVersion}.`); return; } - setNativeImageOption(javaVersionOrDev, `-H:BuildOutputJSONFile=${BUILD_OUTPUT_JSON_PATH.replace(/\\/g, '\\\\')}`); // Escape backslashes for Windows + (0, utils_1.setNativeImageOption)(javaVersionOrDev, `-H:BuildOutputJSONFile=${BUILD_OUTPUT_JSON_PATH.replace(/\\/g, '\\\\')}`); // Escape backslashes for Windows }); } exports.setUpNativeImageBuildReports = setUpNativeImageBuildReports; @@ -98492,38 +98490,6 @@ function arePRReportsEnabled() { function arePRReportsUpdateEnabled() { return (0, utils_1.isPREvent)() && core.getInput(INPUT_NI_PR_REPORTS_UPDATE) === 'true'; } -function setNativeImageOption(javaVersionOrDev, optionValue) { - const coercedJavaVersionOrDev = semver.coerce(javaVersionOrDev); - if ((coercedJavaVersionOrDev && - semver.gte(coercedJavaVersionOrDev, '22.0.0')) || - javaVersionOrDev === c.VERSION_DEV || - javaVersionOrDev.endsWith('-ea')) { - /* NATIVE_IMAGE_OPTIONS was introduced in GraalVM for JDK 22 (so were EA builds). */ - let newOptionValue = optionValue; - const existingOptions = process.env[NATIVE_IMAGE_OPTIONS_ENV]; - if (existingOptions) { - newOptionValue = `${existingOptions} ${newOptionValue}`; - } - core.exportVariable(NATIVE_IMAGE_OPTIONS_ENV, newOptionValue); - } - else { - const optionsFile = getNativeImageOptionsFile(); - if (fs.existsSync(optionsFile)) { - fs.appendFileSync(optionsFile, ` ${optionValue}`); - } - else { - fs.writeFileSync(optionsFile, `NativeImageArgs = ${optionValue}`); - } - } -} -function getNativeImageOptionsFile() { - let optionsFile = process.env[NATIVE_IMAGE_CONFIG_FILE_ENV]; - if (optionsFile === undefined) { - optionsFile = NATIVE_IMAGE_CONFIG_FILE; - core.exportVariable(NATIVE_IMAGE_CONFIG_FILE_ENV, optionsFile); - } - return optionsFile; -} function createReport(data) { const context = github.context; const info = data.general_info; @@ -98751,6 +98717,239 @@ function secondsToHuman(seconds) { } +/***/ }), + +/***/ 9181: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.processSBOM = exports.setUpSBOMSupport = void 0; +const c = __importStar(__nccwpck_require__(9042)); +const core = __importStar(__nccwpck_require__(2186)); +const fs = __importStar(__nccwpck_require__(7147)); +const github = __importStar(__nccwpck_require__(5438)); +const glob = __importStar(__nccwpck_require__(8090)); +const path_1 = __nccwpck_require__(1017); +const semver = __importStar(__nccwpck_require__(1383)); +const utils_1 = __nccwpck_require__(1314); +const INPUT_NI_SBOM = 'native-image-enable-sbom'; +const SBOM_FILE_SUFFIX = '.sbom.json'; +const MIN_JAVA_VERSION = '24.0.0'; +let javaVersionOrLatestEA = null; +function setUpSBOMSupport(javaVersionOrDev, distribution) { + if (!isFeatureEnabled()) { + return; + } + validateJavaVersionAndDistribution(javaVersionOrDev, distribution); + javaVersionOrLatestEA = javaVersionOrDev; + (0, utils_1.setNativeImageOption)(javaVersionOrLatestEA, '--enable-sbom=export'); + core.info('Enabled SBOM generation for Native Image build'); +} +exports.setUpSBOMSupport = setUpSBOMSupport; +function validateJavaVersionAndDistribution(javaVersionOrDev, distribution) { + if (distribution !== c.DISTRIBUTION_GRAALVM) { + throw new Error(`The '${INPUT_NI_SBOM}' option is only supported for Oracle GraalVM (distribution '${c.DISTRIBUTION_GRAALVM}'), but found distribution '${distribution}'.`); + } + if (javaVersionOrDev === 'dev') { + throw new Error(`The '${INPUT_NI_SBOM}' option is not supported for java-version 'dev'.`); + } + if (javaVersionOrDev === 'latest-ea') { + return; + } + const coercedJavaVersion = semver.coerce(javaVersionOrDev); + if (!coercedJavaVersion || semver.gt(MIN_JAVA_VERSION, coercedJavaVersion)) { + throw new Error(`The '${INPUT_NI_SBOM}' option is only supported for GraalVM for JDK ${MIN_JAVA_VERSION} or later, but found java-version '${javaVersionOrDev}'.`); + } +} +function processSBOM() { + return __awaiter(this, void 0, void 0, function* () { + if (!isFeatureEnabled()) { + return; + } + if (javaVersionOrLatestEA === null) { + throw new Error('setUpSBOMSupport must be called before processSBOM'); + } + const sbomPath = yield findSBOMFilePath(); + try { + const sbomContent = fs.readFileSync(sbomPath, 'utf8'); + const sbomData = parseSBOM(sbomContent); + const components = mapToComponentsWithDependencies(sbomData); + printSBOMContent(components); + const snapshot = convertSBOMToSnapshot(sbomPath, components); + yield submitDependencySnapshot(snapshot); + } + catch (error) { + throw new Error(`Failed to process and submit SBOM to the GitHub dependency submission API: ${error instanceof Error ? error.message : String(error)}`); + } + }); +} +exports.processSBOM = processSBOM; +function isFeatureEnabled() { + return core.getInput(INPUT_NI_SBOM) === 'true'; +} +function findSBOMFilePath() { + return __awaiter(this, void 0, void 0, function* () { + const globber = yield glob.create(`**/*${SBOM_FILE_SUFFIX}`); + const sbomFiles = yield globber.glob(); + if (sbomFiles.length === 0) { + throw new Error('No SBOM found. Make sure native-image build completed successfully.'); + } + if (sbomFiles.length > 1) { + throw new Error(`Expected one SBOM but found multiple: ${sbomFiles.join(', ')}.`); + } + core.info(`Found SBOM: ${sbomFiles[0]}`); + return sbomFiles[0]; + }); +} +function parseSBOM(jsonString) { + try { + const sbomData = JSON.parse(jsonString); + return sbomData; + } + catch (error) { + throw new Error(`Failed to parse SBOM JSON: ${error instanceof Error ? error.message : String(error)}`); + } +} +// Maps the SBOM to a list of components with their dependencies +function mapToComponentsWithDependencies(sbom) { + if (!sbom || sbom.components.length === 0) { + throw new Error('Invalid SBOM data or no components found.'); + } + return sbom.components.map((component) => { + var _a, _b; + const dependencies = ((_b = (_a = sbom.dependencies) === null || _a === void 0 ? void 0 : _a.find((dep) => dep.ref === component['bom-ref'])) === null || _b === void 0 ? void 0 : _b.dependsOn) || []; + return { + name: component.name, + version: component.version, + purl: component.purl, + dependencies, + 'bom-ref': component['bom-ref'] + }; + }); +} +function printSBOMContent(components) { + core.info('=== SBOM Content ==='); + for (const component of components) { + core.info(`- ${component['bom-ref']}`); + if (component.dependencies && component.dependencies.length > 0) { + core.info(` depends on: ${component.dependencies.join(', ')}`); + } + } + core.info('=================='); +} +function convertSBOMToSnapshot(sbomPath, components) { + const context = github.context; + const sbomFileName = (0, path_1.basename)(sbomPath); + if (!sbomFileName.endsWith(SBOM_FILE_SUFFIX)) { + throw new Error(`Invalid SBOM file name: ${sbomFileName}. Expected a file ending with ${SBOM_FILE_SUFFIX}.`); + } + return { + version: 0, + sha: context.sha, + ref: context.ref, + job: { + correlator: `${context.workflow}_${context.job}`, + id: context.runId.toString(), + html_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` + }, + detector: { + name: 'Oracle GraalVM', + version: javaVersionOrLatestEA !== null && javaVersionOrLatestEA !== void 0 ? javaVersionOrLatestEA : '', + url: 'https://www.graalvm.org/' + }, + scanned: new Date().toISOString(), + manifests: { + [sbomFileName]: { + name: sbomFileName, + resolved: mapComponentsToGithubAPIFormat(components), + metadata: { + generated_by: 'SBOM generated by GraalVM Native Image', + action_version: c.ACTION_VERSION + } + } + } + }; +} +function mapComponentsToGithubAPIFormat(components) { + return Object.fromEntries(components + .filter(component => { + if (!component.purl) { + core.info(`Component ${component.name} does not have a valid package URL (purl). Skipping.`); + } + return component.purl; + }) + .map(component => [ + component.name, + { + package_url: component.purl, + dependencies: component.dependencies || [] + } + ])); +} +function submitDependencySnapshot(snapshotData) { + return __awaiter(this, void 0, void 0, function* () { + const token = core.getInput(c.INPUT_GITHUB_TOKEN, { required: true }); + const octokit = github.getOctokit(token); + const context = github.context; + try { + yield octokit.request('POST /repos/{owner}/{repo}/dependency-graph/snapshots', { + owner: context.repo.owner, + repo: context.repo.repo, + version: snapshotData.version, + sha: snapshotData.sha, + ref: snapshotData.ref, + job: snapshotData.job, + detector: snapshotData.detector, + metadata: {}, + scanned: snapshotData.scanned, + manifests: snapshotData.manifests, + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }); + core.info('Dependency snapshot submitted successfully.'); + } + catch (error) { + throw new Error(`Failed to submit dependency snapshot for SBOM: ${error instanceof Error ? error.message : String(error)}`); + } + }); +} + + /***/ }), /***/ 1314: @@ -98791,18 +98990,20 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.createPRComment = exports.updatePRComment = exports.findExistingPRCommentId = exports.isPREvent = exports.toSemVer = exports.calculateSHA256 = exports.downloadExtractAndCacheJDK = exports.downloadAndExtractJDK = exports.getMatchingTags = exports.getTaggedRelease = exports.getContents = exports.getLatestRelease = exports.exec = void 0; +exports.setNativeImageOption = exports.tmpfile = exports.createPRComment = exports.updatePRComment = exports.findExistingPRCommentId = exports.isPREvent = exports.toSemVer = exports.calculateSHA256 = exports.downloadExtractAndCacheJDK = exports.downloadAndExtractJDK = exports.getMatchingTags = exports.getTaggedRelease = exports.getContents = exports.getLatestRelease = exports.exec = void 0; const c = __importStar(__nccwpck_require__(9042)); const core = __importStar(__nccwpck_require__(2186)); const github = __importStar(__nccwpck_require__(5438)); const httpClient = __importStar(__nccwpck_require__(6255)); const semver = __importStar(__nccwpck_require__(1383)); const tc = __importStar(__nccwpck_require__(7784)); +const fs = __importStar(__nccwpck_require__(7147)); const exec_1 = __nccwpck_require__(1514); const fs_1 = __nccwpck_require__(7147); const core_1 = __nccwpck_require__(6762); const crypto_1 = __nccwpck_require__(6113); const path_1 = __nccwpck_require__(1017); +const os_1 = __nccwpck_require__(2037); // Set up Octokit for github.com only and in the same way as @actions/github (see https://git.io/Jy9YP) const baseUrl = 'https://api.github.com'; const GitHubDotCom = core_1.Octokit.defaults({ @@ -98999,6 +99200,45 @@ function createPRComment(content) { }); } exports.createPRComment = createPRComment; +function tmpfile(fileName) { + return (0, path_1.join)((0, os_1.tmpdir)(), fileName); +} +exports.tmpfile = tmpfile; +function setNativeImageOption(javaVersionOrDev, optionValue) { + const coercedJavaVersionOrDev = semver.coerce(javaVersionOrDev); + if ((coercedJavaVersionOrDev && + semver.gte(coercedJavaVersionOrDev, '22.0.0')) || + javaVersionOrDev === c.VERSION_DEV || + javaVersionOrDev.endsWith('-ea')) { + /* NATIVE_IMAGE_OPTIONS was introduced in GraalVM for JDK 22 (so were EA builds). */ + let newOptionValue = optionValue; + const existingOptions = process.env[c.NATIVE_IMAGE_OPTIONS_ENV]; + if (existingOptions) { + newOptionValue = `${existingOptions} ${newOptionValue}`; + } + core.exportVariable(c.NATIVE_IMAGE_OPTIONS_ENV, newOptionValue); + } + else { + const optionsFile = getNativeImageOptionsFile(); + if (fs.existsSync(optionsFile)) { + fs.appendFileSync(optionsFile, ` ${optionValue}`); + } + else { + fs.writeFileSync(optionsFile, `NativeImageArgs = ${optionValue}`); + } + } +} +exports.setNativeImageOption = setNativeImageOption; +const NATIVE_IMAGE_CONFIG_FILE = tmpfile('native-image-options.properties'); +const NATIVE_IMAGE_CONFIG_FILE_ENV = 'NATIVE_IMAGE_CONFIG_FILE'; +function getNativeImageOptionsFile() { + let optionsFile = process.env[NATIVE_IMAGE_CONFIG_FILE_ENV]; + if (optionsFile === undefined) { + optionsFile = NATIVE_IMAGE_CONFIG_FILE; + core.exportVariable(NATIVE_IMAGE_CONFIG_FILE_ENV, optionsFile); + } + return optionsFile; +} /***/ }), diff --git a/dist/main/index.js b/dist/main/index.js index 74a4102..f0e2eae 100644 --- a/dist/main/index.js +++ b/dist/main/index.js @@ -97949,7 +97949,7 @@ function wrappy (fn, cb) { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.ERROR_HINT = exports.ERROR_REQUEST = exports.EVENT_NAME_PULL_REQUEST = exports.ENV_GITHUB_EVENT_NAME = exports.GDS_GRAALVM_PRODUCT_ID = exports.GDS_BASE = exports.MANDREL_NAMESPACE = exports.GRAALVM_RELEASES_REPO = exports.GRAALVM_PLATFORM = exports.GRAALVM_GH_USER = exports.GRAALVM_FILE_EXTENSION = exports.GRAALVM_ARCH = exports.JDK_HOME_SUFFIX = exports.JDK_PLATFORM = exports.JDK_ARCH = exports.VERSION_LATEST = exports.VERSION_DEV = exports.DISTRIBUTION_LIBERICA = exports.DISTRIBUTION_MANDREL = exports.DISTRIBUTION_GRAALVM_COMMUNITY = exports.DISTRIBUTION_GRAALVM = exports.EXECUTABLE_SUFFIX = exports.IS_WINDOWS = exports.IS_MACOS = exports.IS_LINUX = exports.INPUT_NI_MUSL = exports.INPUT_CHECK_FOR_UPDATES = exports.INPUT_CACHE = exports.INPUT_SET_JAVA_HOME = exports.INPUT_GITHUB_TOKEN = exports.INPUT_COMPONENTS = exports.INPUT_DISTRIBUTION = exports.INPUT_JAVA_PACKAGE = exports.INPUT_JAVA_VERSION = exports.INPUT_GDS_TOKEN = exports.INPUT_VERSION = exports.ACTION_VERSION = void 0; +exports.ERROR_HINT = exports.ERROR_REQUEST = exports.EVENT_NAME_PULL_REQUEST = exports.ENV_GITHUB_EVENT_NAME = exports.GDS_GRAALVM_PRODUCT_ID = exports.GDS_BASE = exports.MANDREL_NAMESPACE = exports.GRAALVM_RELEASES_REPO = exports.GRAALVM_PLATFORM = exports.GRAALVM_GH_USER = exports.GRAALVM_FILE_EXTENSION = exports.GRAALVM_ARCH = exports.JDK_HOME_SUFFIX = exports.JDK_PLATFORM = exports.JDK_ARCH = exports.VERSION_LATEST = exports.VERSION_DEV = exports.DISTRIBUTION_LIBERICA = exports.DISTRIBUTION_MANDREL = exports.DISTRIBUTION_GRAALVM_COMMUNITY = exports.DISTRIBUTION_GRAALVM = exports.EXECUTABLE_SUFFIX = exports.IS_WINDOWS = exports.IS_MACOS = exports.IS_LINUX = exports.NATIVE_IMAGE_OPTIONS_ENV = exports.INPUT_NI_MUSL = exports.INPUT_CHECK_FOR_UPDATES = exports.INPUT_CACHE = exports.INPUT_SET_JAVA_HOME = exports.INPUT_GITHUB_TOKEN = exports.INPUT_COMPONENTS = exports.INPUT_DISTRIBUTION = exports.INPUT_JAVA_PACKAGE = exports.INPUT_JAVA_VERSION = exports.INPUT_GDS_TOKEN = exports.INPUT_VERSION = exports.ACTION_VERSION = void 0; exports.ACTION_VERSION = '1.2.7'; exports.INPUT_VERSION = 'version'; exports.INPUT_GDS_TOKEN = 'gds-token'; @@ -97962,6 +97962,7 @@ exports.INPUT_SET_JAVA_HOME = 'set-java-home'; exports.INPUT_CACHE = 'cache'; exports.INPUT_CHECK_FOR_UPDATES = 'check-for-updates'; exports.INPUT_NI_MUSL = 'native-image-musl'; +exports.NATIVE_IMAGE_OPTIONS_ENV = 'NATIVE_IMAGE_OPTIONS'; exports.IS_LINUX = process.platform === 'linux'; exports.IS_MACOS = process.platform === 'darwin'; exports.IS_WINDOWS = process.platform === 'win32'; @@ -98519,10 +98520,8 @@ const core = __importStar(__nccwpck_require__(42186)); const fs = __importStar(__nccwpck_require__(57147)); const github = __importStar(__nccwpck_require__(95438)); const semver = __importStar(__nccwpck_require__(11383)); -const path_1 = __nccwpck_require__(71017); -const os_1 = __nccwpck_require__(22037); const utils_1 = __nccwpck_require__(71314); -const BUILD_OUTPUT_JSON_PATH = (0, path_1.join)((0, os_1.tmpdir)(), 'native-image-build-output.json'); +const BUILD_OUTPUT_JSON_PATH = (0, utils_1.tmpfile)('native-image-build-output.json'); const BYTES_TO_KiB = 1024; const BYTES_TO_MiB = 1024 * 1024; const BYTES_TO_GiB = 1024 * 1024 * 1024; @@ -98530,9 +98529,6 @@ const DOCS_BASE = 'https://github.com/oracle/graal/blob/master/docs/reference-ma const INPUT_NI_JOB_REPORTS = 'native-image-job-reports'; const INPUT_NI_PR_REPORTS = 'native-image-pr-reports'; const INPUT_NI_PR_REPORTS_UPDATE = 'native-image-pr-reports-update-existing'; -const NATIVE_IMAGE_CONFIG_FILE = (0, path_1.join)((0, os_1.tmpdir)(), 'native-image-options.properties'); -const NATIVE_IMAGE_OPTIONS_ENV = 'NATIVE_IMAGE_OPTIONS'; -const NATIVE_IMAGE_CONFIG_FILE_ENV = 'NATIVE_IMAGE_CONFIG_FILE'; const PR_COMMENT_TITLE = '## GraalVM Native Image Build Report'; function setUpNativeImageBuildReports(isGraalVMforJDK17OrLater, javaVersionOrDev, graalVMVersion) { return __awaiter(this, void 0, void 0, function* () { @@ -98549,7 +98545,7 @@ function setUpNativeImageBuildReports(isGraalVMforJDK17OrLater, javaVersionOrDev core.warning(`Build reports for PRs and job summaries are only available in GraalVM 22.2.0 or later. This build job uses GraalVM ${graalVMVersion}.`); return; } - setNativeImageOption(javaVersionOrDev, `-H:BuildOutputJSONFile=${BUILD_OUTPUT_JSON_PATH.replace(/\\/g, '\\\\')}`); // Escape backslashes for Windows + (0, utils_1.setNativeImageOption)(javaVersionOrDev, `-H:BuildOutputJSONFile=${BUILD_OUTPUT_JSON_PATH.replace(/\\/g, '\\\\')}`); // Escape backslashes for Windows }); } exports.setUpNativeImageBuildReports = setUpNativeImageBuildReports; @@ -98591,38 +98587,6 @@ function arePRReportsEnabled() { function arePRReportsUpdateEnabled() { return (0, utils_1.isPREvent)() && core.getInput(INPUT_NI_PR_REPORTS_UPDATE) === 'true'; } -function setNativeImageOption(javaVersionOrDev, optionValue) { - const coercedJavaVersionOrDev = semver.coerce(javaVersionOrDev); - if ((coercedJavaVersionOrDev && - semver.gte(coercedJavaVersionOrDev, '22.0.0')) || - javaVersionOrDev === c.VERSION_DEV || - javaVersionOrDev.endsWith('-ea')) { - /* NATIVE_IMAGE_OPTIONS was introduced in GraalVM for JDK 22 (so were EA builds). */ - let newOptionValue = optionValue; - const existingOptions = process.env[NATIVE_IMAGE_OPTIONS_ENV]; - if (existingOptions) { - newOptionValue = `${existingOptions} ${newOptionValue}`; - } - core.exportVariable(NATIVE_IMAGE_OPTIONS_ENV, newOptionValue); - } - else { - const optionsFile = getNativeImageOptionsFile(); - if (fs.existsSync(optionsFile)) { - fs.appendFileSync(optionsFile, ` ${optionValue}`); - } - else { - fs.writeFileSync(optionsFile, `NativeImageArgs = ${optionValue}`); - } - } -} -function getNativeImageOptionsFile() { - let optionsFile = process.env[NATIVE_IMAGE_CONFIG_FILE_ENV]; - if (optionsFile === undefined) { - optionsFile = NATIVE_IMAGE_CONFIG_FILE; - core.exportVariable(NATIVE_IMAGE_CONFIG_FILE_ENV, optionsFile); - } - return optionsFile; -} function createReport(data) { const context = github.context; const info = data.general_info; @@ -98850,6 +98814,239 @@ function secondsToHuman(seconds) { } +/***/ }), + +/***/ 69181: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.processSBOM = exports.setUpSBOMSupport = void 0; +const c = __importStar(__nccwpck_require__(69042)); +const core = __importStar(__nccwpck_require__(42186)); +const fs = __importStar(__nccwpck_require__(57147)); +const github = __importStar(__nccwpck_require__(95438)); +const glob = __importStar(__nccwpck_require__(28090)); +const path_1 = __nccwpck_require__(71017); +const semver = __importStar(__nccwpck_require__(11383)); +const utils_1 = __nccwpck_require__(71314); +const INPUT_NI_SBOM = 'native-image-enable-sbom'; +const SBOM_FILE_SUFFIX = '.sbom.json'; +const MIN_JAVA_VERSION = '24.0.0'; +let javaVersionOrLatestEA = null; +function setUpSBOMSupport(javaVersionOrDev, distribution) { + if (!isFeatureEnabled()) { + return; + } + validateJavaVersionAndDistribution(javaVersionOrDev, distribution); + javaVersionOrLatestEA = javaVersionOrDev; + (0, utils_1.setNativeImageOption)(javaVersionOrLatestEA, '--enable-sbom=export'); + core.info('Enabled SBOM generation for Native Image build'); +} +exports.setUpSBOMSupport = setUpSBOMSupport; +function validateJavaVersionAndDistribution(javaVersionOrDev, distribution) { + if (distribution !== c.DISTRIBUTION_GRAALVM) { + throw new Error(`The '${INPUT_NI_SBOM}' option is only supported for Oracle GraalVM (distribution '${c.DISTRIBUTION_GRAALVM}'), but found distribution '${distribution}'.`); + } + if (javaVersionOrDev === 'dev') { + throw new Error(`The '${INPUT_NI_SBOM}' option is not supported for java-version 'dev'.`); + } + if (javaVersionOrDev === 'latest-ea') { + return; + } + const coercedJavaVersion = semver.coerce(javaVersionOrDev); + if (!coercedJavaVersion || semver.gt(MIN_JAVA_VERSION, coercedJavaVersion)) { + throw new Error(`The '${INPUT_NI_SBOM}' option is only supported for GraalVM for JDK ${MIN_JAVA_VERSION} or later, but found java-version '${javaVersionOrDev}'.`); + } +} +function processSBOM() { + return __awaiter(this, void 0, void 0, function* () { + if (!isFeatureEnabled()) { + return; + } + if (javaVersionOrLatestEA === null) { + throw new Error('setUpSBOMSupport must be called before processSBOM'); + } + const sbomPath = yield findSBOMFilePath(); + try { + const sbomContent = fs.readFileSync(sbomPath, 'utf8'); + const sbomData = parseSBOM(sbomContent); + const components = mapToComponentsWithDependencies(sbomData); + printSBOMContent(components); + const snapshot = convertSBOMToSnapshot(sbomPath, components); + yield submitDependencySnapshot(snapshot); + } + catch (error) { + throw new Error(`Failed to process and submit SBOM to the GitHub dependency submission API: ${error instanceof Error ? error.message : String(error)}`); + } + }); +} +exports.processSBOM = processSBOM; +function isFeatureEnabled() { + return core.getInput(INPUT_NI_SBOM) === 'true'; +} +function findSBOMFilePath() { + return __awaiter(this, void 0, void 0, function* () { + const globber = yield glob.create(`**/*${SBOM_FILE_SUFFIX}`); + const sbomFiles = yield globber.glob(); + if (sbomFiles.length === 0) { + throw new Error('No SBOM found. Make sure native-image build completed successfully.'); + } + if (sbomFiles.length > 1) { + throw new Error(`Expected one SBOM but found multiple: ${sbomFiles.join(', ')}.`); + } + core.info(`Found SBOM: ${sbomFiles[0]}`); + return sbomFiles[0]; + }); +} +function parseSBOM(jsonString) { + try { + const sbomData = JSON.parse(jsonString); + return sbomData; + } + catch (error) { + throw new Error(`Failed to parse SBOM JSON: ${error instanceof Error ? error.message : String(error)}`); + } +} +// Maps the SBOM to a list of components with their dependencies +function mapToComponentsWithDependencies(sbom) { + if (!sbom || sbom.components.length === 0) { + throw new Error('Invalid SBOM data or no components found.'); + } + return sbom.components.map((component) => { + var _a, _b; + const dependencies = ((_b = (_a = sbom.dependencies) === null || _a === void 0 ? void 0 : _a.find((dep) => dep.ref === component['bom-ref'])) === null || _b === void 0 ? void 0 : _b.dependsOn) || []; + return { + name: component.name, + version: component.version, + purl: component.purl, + dependencies, + 'bom-ref': component['bom-ref'] + }; + }); +} +function printSBOMContent(components) { + core.info('=== SBOM Content ==='); + for (const component of components) { + core.info(`- ${component['bom-ref']}`); + if (component.dependencies && component.dependencies.length > 0) { + core.info(` depends on: ${component.dependencies.join(', ')}`); + } + } + core.info('=================='); +} +function convertSBOMToSnapshot(sbomPath, components) { + const context = github.context; + const sbomFileName = (0, path_1.basename)(sbomPath); + if (!sbomFileName.endsWith(SBOM_FILE_SUFFIX)) { + throw new Error(`Invalid SBOM file name: ${sbomFileName}. Expected a file ending with ${SBOM_FILE_SUFFIX}.`); + } + return { + version: 0, + sha: context.sha, + ref: context.ref, + job: { + correlator: `${context.workflow}_${context.job}`, + id: context.runId.toString(), + html_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` + }, + detector: { + name: 'Oracle GraalVM', + version: javaVersionOrLatestEA !== null && javaVersionOrLatestEA !== void 0 ? javaVersionOrLatestEA : '', + url: 'https://www.graalvm.org/' + }, + scanned: new Date().toISOString(), + manifests: { + [sbomFileName]: { + name: sbomFileName, + resolved: mapComponentsToGithubAPIFormat(components), + metadata: { + generated_by: 'SBOM generated by GraalVM Native Image', + action_version: c.ACTION_VERSION + } + } + } + }; +} +function mapComponentsToGithubAPIFormat(components) { + return Object.fromEntries(components + .filter(component => { + if (!component.purl) { + core.info(`Component ${component.name} does not have a valid package URL (purl). Skipping.`); + } + return component.purl; + }) + .map(component => [ + component.name, + { + package_url: component.purl, + dependencies: component.dependencies || [] + } + ])); +} +function submitDependencySnapshot(snapshotData) { + return __awaiter(this, void 0, void 0, function* () { + const token = core.getInput(c.INPUT_GITHUB_TOKEN, { required: true }); + const octokit = github.getOctokit(token); + const context = github.context; + try { + yield octokit.request('POST /repos/{owner}/{repo}/dependency-graph/snapshots', { + owner: context.repo.owner, + repo: context.repo.repo, + version: snapshotData.version, + sha: snapshotData.sha, + ref: snapshotData.ref, + job: snapshotData.job, + detector: snapshotData.detector, + metadata: {}, + scanned: snapshotData.scanned, + manifests: snapshotData.manifests, + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }); + core.info('Dependency snapshot submitted successfully.'); + } + catch (error) { + throw new Error(`Failed to submit dependency snapshot for SBOM: ${error instanceof Error ? error.message : String(error)}`); + } + }); +} + + /***/ }), /***/ 19543: @@ -99684,6 +99881,7 @@ const musl_1 = __nccwpck_require__(10316); const msvc_1 = __nccwpck_require__(31165); const reports_1 = __nccwpck_require__(92046); const exec_1 = __nccwpck_require__(71514); +const sbom_1 = __nccwpck_require__(69181); function run() { return __awaiter(this, void 0, void 0, function* () { try { @@ -99795,6 +99993,7 @@ function run() { yield (0, cache_2.restore)(cache); } (0, reports_1.setUpNativeImageBuildReports)(isGraalVMforJDK17OrLater, javaVersion, graalVMVersion); + (0, sbom_1.setUpSBOMSupport)(javaVersion, distribution); core.startGroup(`Successfully set up '${(0, path_1.basename)(graalVMHome)}'`); yield (0, exec_1.exec)((0, path_1.join)(graalVMHome, 'bin', `java${c.EXECUTABLE_SUFFIX}`), [ javaVersion.startsWith('8') ? '-version' : '--version' @@ -100117,18 +100316,20 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.createPRComment = exports.updatePRComment = exports.findExistingPRCommentId = exports.isPREvent = exports.toSemVer = exports.calculateSHA256 = exports.downloadExtractAndCacheJDK = exports.downloadAndExtractJDK = exports.getMatchingTags = exports.getTaggedRelease = exports.getContents = exports.getLatestRelease = exports.exec = void 0; +exports.setNativeImageOption = exports.tmpfile = exports.createPRComment = exports.updatePRComment = exports.findExistingPRCommentId = exports.isPREvent = exports.toSemVer = exports.calculateSHA256 = exports.downloadExtractAndCacheJDK = exports.downloadAndExtractJDK = exports.getMatchingTags = exports.getTaggedRelease = exports.getContents = exports.getLatestRelease = exports.exec = void 0; const c = __importStar(__nccwpck_require__(69042)); const core = __importStar(__nccwpck_require__(42186)); const github = __importStar(__nccwpck_require__(95438)); const httpClient = __importStar(__nccwpck_require__(96255)); const semver = __importStar(__nccwpck_require__(11383)); const tc = __importStar(__nccwpck_require__(27784)); +const fs = __importStar(__nccwpck_require__(57147)); const exec_1 = __nccwpck_require__(71514); const fs_1 = __nccwpck_require__(57147); const core_1 = __nccwpck_require__(76762); const crypto_1 = __nccwpck_require__(6113); const path_1 = __nccwpck_require__(71017); +const os_1 = __nccwpck_require__(22037); // Set up Octokit for github.com only and in the same way as @actions/github (see https://git.io/Jy9YP) const baseUrl = 'https://api.github.com'; const GitHubDotCom = core_1.Octokit.defaults({ @@ -100325,6 +100526,45 @@ function createPRComment(content) { }); } exports.createPRComment = createPRComment; +function tmpfile(fileName) { + return (0, path_1.join)((0, os_1.tmpdir)(), fileName); +} +exports.tmpfile = tmpfile; +function setNativeImageOption(javaVersionOrDev, optionValue) { + const coercedJavaVersionOrDev = semver.coerce(javaVersionOrDev); + if ((coercedJavaVersionOrDev && + semver.gte(coercedJavaVersionOrDev, '22.0.0')) || + javaVersionOrDev === c.VERSION_DEV || + javaVersionOrDev.endsWith('-ea')) { + /* NATIVE_IMAGE_OPTIONS was introduced in GraalVM for JDK 22 (so were EA builds). */ + let newOptionValue = optionValue; + const existingOptions = process.env[c.NATIVE_IMAGE_OPTIONS_ENV]; + if (existingOptions) { + newOptionValue = `${existingOptions} ${newOptionValue}`; + } + core.exportVariable(c.NATIVE_IMAGE_OPTIONS_ENV, newOptionValue); + } + else { + const optionsFile = getNativeImageOptionsFile(); + if (fs.existsSync(optionsFile)) { + fs.appendFileSync(optionsFile, ` ${optionValue}`); + } + else { + fs.writeFileSync(optionsFile, `NativeImageArgs = ${optionValue}`); + } + } +} +exports.setNativeImageOption = setNativeImageOption; +const NATIVE_IMAGE_CONFIG_FILE = tmpfile('native-image-options.properties'); +const NATIVE_IMAGE_CONFIG_FILE_ENV = 'NATIVE_IMAGE_CONFIG_FILE'; +function getNativeImageOptionsFile() { + let optionsFile = process.env[NATIVE_IMAGE_CONFIG_FILE_ENV]; + if (optionsFile === undefined) { + optionsFile = NATIVE_IMAGE_CONFIG_FILE; + core.exportVariable(NATIVE_IMAGE_CONFIG_FILE_ENV, optionsFile); + } + return optionsFile; +} /***/ }), diff --git a/package-lock.json b/package-lock.json index 3c98fd1..da52382 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@actions/tool-cache": "^2.0.2", "@octokit/core": "^5.2.0", "@octokit/types": "^12.6.0", + "@github/dependency-submission-toolkit": "^2.0.4", "semver": "^7.6.3", "uuid": "^11.0.5" }, @@ -1111,6 +1112,22 @@ "integrity": "sha512-gIhjdJp/c2beaIWWIlsXdqXVRUz3r2BxBCpfz/F3JXHvSAQ1paMYjLH+maEATtENg+k5eLV7gA+9yPp762ieuw==", "dev": true }, + "node_modules/@github/dependency-submission-toolkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@github/dependency-submission-toolkit/-/dependency-submission-toolkit-2.0.4.tgz", + "integrity": "sha512-uQia1YSLTrVmy+f6XpAzy/MEFDvjMg/VOm9pdROxVKQA5SvLXDvXeGgxLwy9fH+sXHqtDWRnVOI1+UAcQ4pi/w==", + "license": "MIT", + "workspaces": [ + "example" + ], + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/github": "^6.0.0", + "@octokit/request-error": "^5.0.1", + "@octokit/webhooks-types": "^7.3.1", + "packageurl-js": "^1.2.1" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -1796,6 +1813,12 @@ "@octokit/openapi-types": "^20.0.0" } }, + "node_modules/@octokit/webhooks-types": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.6.1.tgz", + "integrity": "sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw==", + "license": "MIT" + }, "node_modules/@opentelemetry/api": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", @@ -6648,6 +6671,12 @@ "node": ">=6" } }, + "node_modules/packageurl-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/packageurl-js/-/packageurl-js-1.2.1.tgz", + "integrity": "sha512-cZ6/MzuXaoFd16/k0WnwtI298UCaDHe/XlSh85SeOKbGZ1hq0xvNbx3ILyCMyk7uFQxl6scF3Aucj6/EO9NwcA==", + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7998,10 +8027,11 @@ } }, "node_modules/typescript": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", - "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 5ca9135..6dea35a 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@actions/tool-cache": "^2.0.2", "@octokit/core": "^5.2.0", "@octokit/types": "^12.6.0", + "@github/dependency-submission-toolkit": "^2.0.4", "semver": "^7.6.3", "uuid": "^11.0.5" }, diff --git a/src/cleanup.ts b/src/cleanup.ts index 3ed1b0c..4ebd75a 100644 --- a/src/cleanup.ts +++ b/src/cleanup.ts @@ -28,6 +28,7 @@ import * as core from '@actions/core' import * as constants from './constants' import {save} from './features/cache' import {generateReports} from './features/reports' +import {processSBOM} from './features/sbom' /** * Check given input and run a save process for the specified package manager @@ -58,6 +59,7 @@ async function ignoreErrors(promise: Promise): Promise { export async function run(): Promise { await ignoreErrors(generateReports()) + await ignoreErrors(processSBOM()) await ignoreErrors(saveCache()) } diff --git a/src/constants.ts b/src/constants.ts index acb6aaf..519700f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -14,6 +14,8 @@ export const INPUT_CACHE = 'cache' export const INPUT_CHECK_FOR_UPDATES = 'check-for-updates' export const INPUT_NI_MUSL = 'native-image-musl' +export const NATIVE_IMAGE_OPTIONS_ENV = 'NATIVE_IMAGE_OPTIONS' + export const IS_LINUX = process.platform === 'linux' export const IS_MACOS = process.platform === 'darwin' export const IS_WINDOWS = process.platform === 'win32' diff --git a/src/features/reports.ts b/src/features/reports.ts index fd21fd3..3793b31 100644 --- a/src/features/reports.ts +++ b/src/features/reports.ts @@ -3,17 +3,17 @@ import * as core from '@actions/core' import * as fs from 'fs' import * as github from '@actions/github' import * as semver from 'semver' -import {join} from 'path' -import {tmpdir} from 'os' import { createPRComment, findExistingPRCommentId, isPREvent, toSemVer, - updatePRComment + updatePRComment, + tmpfile, + setNativeImageOption } from '../utils' -const BUILD_OUTPUT_JSON_PATH = join(tmpdir(), 'native-image-build-output.json') +const BUILD_OUTPUT_JSON_PATH = tmpfile('native-image-build-output.json') const BYTES_TO_KiB = 1024 const BYTES_TO_MiB = 1024 * 1024 const BYTES_TO_GiB = 1024 * 1024 * 1024 @@ -22,12 +22,6 @@ const DOCS_BASE = const INPUT_NI_JOB_REPORTS = 'native-image-job-reports' const INPUT_NI_PR_REPORTS = 'native-image-pr-reports' const INPUT_NI_PR_REPORTS_UPDATE = 'native-image-pr-reports-update-existing' -const NATIVE_IMAGE_CONFIG_FILE = join( - tmpdir(), - 'native-image-options.properties' -) -const NATIVE_IMAGE_OPTIONS_ENV = 'NATIVE_IMAGE_OPTIONS' -const NATIVE_IMAGE_CONFIG_FILE_ENV = 'NATIVE_IMAGE_CONFIG_FILE' const PR_COMMENT_TITLE = '## GraalVM Native Image Build Report' interface AnalysisResult { @@ -169,43 +163,6 @@ function arePRReportsUpdateEnabled(): boolean { return isPREvent() && core.getInput(INPUT_NI_PR_REPORTS_UPDATE) === 'true' } -function setNativeImageOption( - javaVersionOrDev: string, - optionValue: string -): void { - const coercedJavaVersionOrDev = semver.coerce(javaVersionOrDev) - if ( - (coercedJavaVersionOrDev && - semver.gte(coercedJavaVersionOrDev, '22.0.0')) || - javaVersionOrDev === c.VERSION_DEV || - javaVersionOrDev.endsWith('-ea') - ) { - /* NATIVE_IMAGE_OPTIONS was introduced in GraalVM for JDK 22 (so were EA builds). */ - let newOptionValue = optionValue - const existingOptions = process.env[NATIVE_IMAGE_OPTIONS_ENV] - if (existingOptions) { - newOptionValue = `${existingOptions} ${newOptionValue}` - } - core.exportVariable(NATIVE_IMAGE_OPTIONS_ENV, newOptionValue) - } else { - const optionsFile = getNativeImageOptionsFile() - if (fs.existsSync(optionsFile)) { - fs.appendFileSync(optionsFile, ` ${optionValue}`) - } else { - fs.writeFileSync(optionsFile, `NativeImageArgs = ${optionValue}`) - } - } -} - -function getNativeImageOptionsFile(): string { - let optionsFile = process.env[NATIVE_IMAGE_CONFIG_FILE_ENV] - if (optionsFile === undefined) { - optionsFile = NATIVE_IMAGE_CONFIG_FILE - core.exportVariable(NATIVE_IMAGE_CONFIG_FILE_ENV, optionsFile) - } - return optionsFile -} - function createReport(data: BuildOutput): string { const context = github.context const info = data.general_info diff --git a/src/features/sbom.ts b/src/features/sbom.ts new file mode 100644 index 0000000..125e077 --- /dev/null +++ b/src/features/sbom.ts @@ -0,0 +1,300 @@ +import * as c from '../constants' +import * as core from '@actions/core' +import * as fs from 'fs' +import * as github from '@actions/github' +import * as glob from '@actions/glob' +import {basename} from 'path' +import * as semver from 'semver' +import {setNativeImageOption} from '../utils' + +const INPUT_NI_SBOM = 'native-image-enable-sbom' +const SBOM_FILE_SUFFIX = '.sbom.json' +const MIN_JAVA_VERSION = '24.0.0' + +let javaVersionOrLatestEA: string | null = null + +interface SBOM { + components: Component[] + dependencies: Dependency[] +} + +interface Component { + name: string + version?: string + purl?: string + dependencies?: string[] + 'bom-ref': string +} + +interface Dependency { + ref: string + dependsOn: string[] +} + +interface DependencySnapshot { + version: number + sha: string + ref: string + job: { + correlator: string + id: string + html_url?: string + } + detector: { + name: string + version: string + url: string + } + scanned: string + manifests: Record< + string, + { + name: string + metadata?: Record + // Not including the 'file' property because we cannot specify any reasonable value for 'source_location' + // since the SBOM will not necessarily be saved in the repository of the user. + // GitHub docs: https://docs.github.com/en/rest/dependency-graph/dependency-submission?apiVersion=2022-11-28#create-a-snapshot-of-dependencies-for-a-repository + resolved: Record< + string, + { + package_url: string + relationship?: 'direct' + scope?: 'runtime' + dependencies?: string[] + } + > + } + > +} + +export function setUpSBOMSupport( + javaVersionOrDev: string, + distribution: string +): void { + if (!isFeatureEnabled()) { + return + } + + validateJavaVersionAndDistribution(javaVersionOrDev, distribution) + javaVersionOrLatestEA = javaVersionOrDev + setNativeImageOption(javaVersionOrLatestEA, '--enable-sbom=export') + core.info('Enabled SBOM generation for Native Image build') +} + +function validateJavaVersionAndDistribution( + javaVersionOrDev: string, + distribution: string +): void { + if (distribution !== c.DISTRIBUTION_GRAALVM) { + throw new Error( + `The '${INPUT_NI_SBOM}' option is only supported for Oracle GraalVM (distribution '${c.DISTRIBUTION_GRAALVM}'), but found distribution '${distribution}'.` + ) + } + + if (javaVersionOrDev === 'dev') { + throw new Error( + `The '${INPUT_NI_SBOM}' option is not supported for java-version 'dev'.` + ) + } + + if (javaVersionOrDev === 'latest-ea') { + return + } + + const coercedJavaVersion = semver.coerce(javaVersionOrDev) + if (!coercedJavaVersion || semver.gt(MIN_JAVA_VERSION, coercedJavaVersion)) { + throw new Error( + `The '${INPUT_NI_SBOM}' option is only supported for GraalVM for JDK ${MIN_JAVA_VERSION} or later, but found java-version '${javaVersionOrDev}'.` + ) + } +} + +export async function processSBOM(): Promise { + if (!isFeatureEnabled()) { + return + } + + if (javaVersionOrLatestEA === null) { + throw new Error('setUpSBOMSupport must be called before processSBOM') + } + + const sbomPath = await findSBOMFilePath() + try { + const sbomContent = fs.readFileSync(sbomPath, 'utf8') + const sbomData = parseSBOM(sbomContent) + const components = mapToComponentsWithDependencies(sbomData) + printSBOMContent(components) + const snapshot = convertSBOMToSnapshot(sbomPath, components) + await submitDependencySnapshot(snapshot) + } catch (error) { + throw new Error( + `Failed to process and submit SBOM to the GitHub dependency submission API: ${error instanceof Error ? error.message : String(error)}` + ) + } +} + +function isFeatureEnabled(): boolean { + return core.getInput(INPUT_NI_SBOM) === 'true' +} + +async function findSBOMFilePath(): Promise { + const globber = await glob.create(`**/*${SBOM_FILE_SUFFIX}`) + const sbomFiles = await globber.glob() + + if (sbomFiles.length === 0) { + throw new Error( + 'No SBOM found. Make sure native-image build completed successfully.' + ) + } + + if (sbomFiles.length > 1) { + throw new Error( + `Expected one SBOM but found multiple: ${sbomFiles.join(', ')}.` + ) + } + + core.info(`Found SBOM: ${sbomFiles[0]}`) + return sbomFiles[0] +} + +function parseSBOM(jsonString: string): SBOM { + try { + const sbomData: SBOM = JSON.parse(jsonString) + return sbomData + } catch (error) { + throw new Error( + `Failed to parse SBOM JSON: ${error instanceof Error ? error.message : String(error)}` + ) + } +} + +// Maps the SBOM to a list of components with their dependencies +function mapToComponentsWithDependencies(sbom: SBOM): Component[] { + if (!sbom || sbom.components.length === 0) { + throw new Error('Invalid SBOM data or no components found.') + } + + return sbom.components.map((component: Component) => { + const dependencies = + sbom.dependencies?.find( + (dep: Dependency) => dep.ref === component['bom-ref'] + )?.dependsOn || [] + + return { + name: component.name, + version: component.version, + purl: component.purl, + dependencies, + 'bom-ref': component['bom-ref'] + } + }) +} + +function printSBOMContent(components: Component[]): void { + core.info('=== SBOM Content ===') + for (const component of components) { + core.info(`- ${component['bom-ref']}`) + if (component.dependencies && component.dependencies.length > 0) { + core.info(` depends on: ${component.dependencies.join(', ')}`) + } + } + core.info('==================') +} + +function convertSBOMToSnapshot( + sbomPath: string, + components: Component[] +): DependencySnapshot { + const context = github.context + const sbomFileName = basename(sbomPath) + + if (!sbomFileName.endsWith(SBOM_FILE_SUFFIX)) { + throw new Error( + `Invalid SBOM file name: ${sbomFileName}. Expected a file ending with ${SBOM_FILE_SUFFIX}.` + ) + } + + return { + version: 0, + sha: context.sha, + ref: context.ref, + job: { + correlator: `${context.workflow}_${context.job}`, + id: context.runId.toString(), + html_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` + }, + detector: { + name: 'Oracle GraalVM', + version: javaVersionOrLatestEA ?? '', + url: 'https://www.graalvm.org/' + }, + scanned: new Date().toISOString(), + manifests: { + [sbomFileName]: { + name: sbomFileName, + resolved: mapComponentsToGithubAPIFormat(components), + metadata: { + generated_by: 'SBOM generated by GraalVM Native Image', + action_version: c.ACTION_VERSION + } + } + } + } +} + +function mapComponentsToGithubAPIFormat( + components: Component[] +): Record { + return Object.fromEntries( + components + .filter(component => { + if (!component.purl) { + core.info( + `Component ${component.name} does not have a valid package URL (purl). Skipping.` + ) + } + return component.purl + }) + .map(component => [ + component.name, + { + package_url: component.purl as string, + dependencies: component.dependencies || [] + } + ]) + ) +} + +async function submitDependencySnapshot( + snapshotData: DependencySnapshot +): Promise { + const token = core.getInput(c.INPUT_GITHUB_TOKEN, {required: true}) + const octokit = github.getOctokit(token) + const context = github.context + + try { + await octokit.request( + 'POST /repos/{owner}/{repo}/dependency-graph/snapshots', + { + owner: context.repo.owner, + repo: context.repo.repo, + version: snapshotData.version, + sha: snapshotData.sha, + ref: snapshotData.ref, + job: snapshotData.job, + detector: snapshotData.detector, + metadata: {}, + scanned: snapshotData.scanned, + manifests: snapshotData.manifests, + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + } + ) + core.info('Dependency snapshot submitted successfully.') + } catch (error) { + throw new Error( + `Failed to submit dependency snapshot for SBOM: ${error instanceof Error ? error.message : String(error)}` + ) + } +} diff --git a/src/main.ts b/src/main.ts index 7f32b82..7a27c13 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,6 +14,7 @@ import {setUpNativeImageMusl} from './features/musl' import {setUpWindowsEnvironment} from './msvc' import {setUpNativeImageBuildReports} from './features/reports' import {exec} from '@actions/exec' +import {setUpSBOMSupport} from './features/sbom' async function run(): Promise { try { @@ -148,7 +149,6 @@ async function run(): Promise { if (setJavaHome) { core.exportVariable('JAVA_HOME', graalVMHome) } - await setUpGUComponents( javaVersion, graalVMVersion, @@ -165,6 +165,7 @@ async function run(): Promise { javaVersion, graalVMVersion ) + setUpSBOMSupport(javaVersion, distribution) core.startGroup(`Successfully set up '${basename(graalVMHome)}'`) await exec(join(graalVMHome, 'bin', `java${c.EXECUTABLE_SUFFIX}`), [ diff --git a/src/utils.ts b/src/utils.ts index 655d1d9..5273b54 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,11 +4,13 @@ import * as github from '@actions/github' import * as httpClient from '@actions/http-client' import * as semver from 'semver' import * as tc from '@actions/tool-cache' +import * as fs from 'fs' import {ExecOptions, exec as e} from '@actions/exec' import {readFileSync, readdirSync} from 'fs' import {Octokit} from '@octokit/core' import {createHash} from 'crypto' import {join} from 'path' +import {tmpdir} from 'os' // Set up Octokit for github.com only and in the same way as @actions/github (see https://git.io/Jy9YP) const baseUrl = 'https://api.github.com' @@ -247,3 +249,47 @@ export async function createPRComment(content: string): Promise { ) } } + +export function tmpfile(fileName: string) { + return join(tmpdir(), fileName) +} + +export function setNativeImageOption( + javaVersionOrDev: string, + optionValue: string +): void { + const coercedJavaVersionOrDev = semver.coerce(javaVersionOrDev) + if ( + (coercedJavaVersionOrDev && + semver.gte(coercedJavaVersionOrDev, '22.0.0')) || + javaVersionOrDev === c.VERSION_DEV || + javaVersionOrDev.endsWith('-ea') + ) { + /* NATIVE_IMAGE_OPTIONS was introduced in GraalVM for JDK 22 (so were EA builds). */ + let newOptionValue = optionValue + const existingOptions = process.env[c.NATIVE_IMAGE_OPTIONS_ENV] + if (existingOptions) { + newOptionValue = `${existingOptions} ${newOptionValue}` + } + core.exportVariable(c.NATIVE_IMAGE_OPTIONS_ENV, newOptionValue) + } else { + const optionsFile = getNativeImageOptionsFile() + if (fs.existsSync(optionsFile)) { + fs.appendFileSync(optionsFile, ` ${optionValue}`) + } else { + fs.writeFileSync(optionsFile, `NativeImageArgs = ${optionValue}`) + } + } +} + +const NATIVE_IMAGE_CONFIG_FILE = tmpfile('native-image-options.properties') +const NATIVE_IMAGE_CONFIG_FILE_ENV = 'NATIVE_IMAGE_CONFIG_FILE' + +function getNativeImageOptionsFile(): string { + let optionsFile = process.env[NATIVE_IMAGE_CONFIG_FILE_ENV] + if (optionsFile === undefined) { + optionsFile = NATIVE_IMAGE_CONFIG_FILE + core.exportVariable(NATIVE_IMAGE_CONFIG_FILE_ENV, optionsFile) + } + return optionsFile +}