diff --git a/.github/workflows/reusable-generate-sbom.yml b/.github/workflows/reusable-generate-sbom.yml new file mode 100644 index 0000000..3974388 --- /dev/null +++ b/.github/workflows/reusable-generate-sbom.yml @@ -0,0 +1,148 @@ +name: Reusable - Generate SBOM + +on: + workflow_call: + inputs: + lvproj_path: + description: 'Path to the .lvproj file, relative to the caller repository.' + required: true + type: string + labview_version: + description: 'Linux container release (YYYYq1 or YYYYq3; minimum 2025q3).' + required: false + type: string + default: '2026q1' + labview_bitness: + required: true + type: string + build_spec_name: + required: true + type: string + target_name: + required: false + type: string + default: 'My Computer' + product_name: + required: true + type: string + product_version: + required: true + type: string + output_path: + description: 'SBOM output path, relative to the caller repository.' + required: true + type: string + format: + required: false + type: string + default: 'cyclonedx' + schema_version: + required: false + type: string + default: '1.5' + artifact_name: + description: 'Name for the uploaded SBOM artifact.' + required: false + type: string + default: 'sbom' + +permissions: + contents: read + +jobs: + generate-sbom: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 + - name: Validate LabVIEW Version for Linux Docker + id: validate-version + shell: pwsh + env: + LABVIEW_VERSION_INPUT: ${{ inputs.labview_version }} + run: | + $lvVersion = $env:LABVIEW_VERSION_INPUT + if ($lvVersion -match '^(\d{4})q([13])$') { + $year = [int]$Matches[1] + $quarter = [int]$Matches[2] + } else { + throw "Invalid LabVIEW version format: $lvVersion. Expected YYYYq1 or YYYYq3 (for example, 2026q1)." + } + + $isValid = ($year -gt 2025) -or (($year -eq 2025) -and ($quarter -ge 3)) + if (-not $isValid) { + throw "LabVIEW version $lvVersion is not available in Linux Docker. Minimum supported version is 2025q3." + } + + "year=$year" >> $env:GITHUB_OUTPUT + - name: Generate SBOM in LabVIEW Linux container + shell: pwsh + env: + LABVIEW_CONTAINER_VERSION: ${{ inputs.labview_version }} + LABVIEW_YEAR: ${{ steps.validate-version.outputs.year }} + LVPROJ_PATH: ${{ inputs.lvproj_path }} + LABVIEW_BITNESS: ${{ inputs.labview_bitness }} + BUILD_SPEC_NAME: ${{ inputs.build_spec_name }} + TARGET_NAME: ${{ inputs.target_name }} + PRODUCT_NAME: ${{ inputs.product_name }} + PRODUCT_VERSION: ${{ inputs.product_version }} + OUTPUT_PATH: ${{ inputs.output_path }} + SBOM_FORMAT: ${{ inputs.format }} + SBOM_SCHEMA_VERSION: ${{ inputs.schema_version }} + run: | + $containerScript = @' + set -e + apt-get update && apt-get install -y wget + wget --no-check-certificate -O /tmp/vipm.deb https://traffic.libsyn.com/secure/jkinc/vipm_26.3.1-4025_amd64.deb + dpkg -i /tmp/vipm.deb + vipm --version + output_file="/workspace/$OUTPUT_PATH" + mkdir -p "$(dirname "$output_file")" + timeout 300 xvfb-run -a vipm sbom "/workspace/$LVPROJ_PATH" \ + --format "$SBOM_FORMAT" \ + --schema-version "$SBOM_SCHEMA_VERSION" \ + --labview-version "$LABVIEW_YEAR" \ + --labview-bitness "$LABVIEW_BITNESS" \ + --lvproj-build-spec "$BUILD_SPEC_NAME" \ + --lvproj-target "$TARGET_NAME" \ + --product-name "$PRODUCT_NAME" \ + --product-version "$PRODUCT_VERSION" \ + --output "$output_file" + '@ + + $workspacePath = $env:GITHUB_WORKSPACE + $labviewImage = "nationalinstruments/labview:$($env:LABVIEW_CONTAINER_VERSION)-linux" + docker run --rm ` + --volume "${workspacePath}:/workspace" ` + --env LVPROJ_PATH ` + --env LABVIEW_YEAR ` + --env LABVIEW_BITNESS ` + --env BUILD_SPEC_NAME ` + --env TARGET_NAME ` + --env PRODUCT_NAME ` + --env PRODUCT_VERSION ` + --env OUTPUT_PATH ` + --env SBOM_FORMAT ` + --env SBOM_SCHEMA_VERSION ` + $labviewImage ` + bash -c $containerScript + + if ($LASTEXITCODE -ne 0) { + throw "SBOM generation inside the LabVIEW Linux container failed with exit code $LASTEXITCODE" + } + - name: Verify SBOM exists + shell: pwsh + env: + OUTPUT_PATH: ${{ inputs.output_path }} + run: | + $sbomPath = Join-Path $env:GITHUB_WORKSPACE $env:OUTPUT_PATH + if (-not (Test-Path -Path $sbomPath -PathType Leaf)) { + throw "No SBOM file found at '$sbomPath'. Ensure vipm sbom completed successfully." + } + Write-Host "SBOM verified: $sbomPath" + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + if: success() + with: + name: ${{ inputs.artifact_name }} + path: ${{ inputs.output_path }} + retention-days: 90 + if-no-files-found: error \ No newline at end of file diff --git a/actions/OpenSourceActions.psd1 b/actions/OpenSourceActions.psd1 index 5aa36b9..841a9aa 100644 --- a/actions/OpenSourceActions.psd1 +++ b/actions/OpenSourceActions.psd1 @@ -22,6 +22,7 @@ 'Invoke-CloseLabVIEW' 'Invoke-ConfigureLabview' 'Invoke-GenerateReleaseNotes' + 'Invoke-GenerateSbom' 'Invoke-MissingInProject' 'Invoke-ModifyVIPBDisplayInfo' 'Invoke-PrepareLabVIEWSource' @@ -43,7 +44,8 @@ PSData = @{ ReleaseNotes = @( '1.0.0 - Initial release: adapters for apply-vipc, build-lvlibp, missing-in-project, run-unit-tests; Format-UnitTestReport helper; discovery flags; DryRun; cross-platform g-cli support.' - '1.1.1 - Fix: update-vipm-dependencies now fails the job (instead of warning and continuing) when any package cannot be resolved/updated, so a partial dependency bump never reaches vipm.lock or a PR.' + '1.1.0 - Add generate-sbom adapter: minimal CycloneDX SBOM generation via vipm sbom, assuming VIPM is already installed/activated on the runner.' + '1.1.1 - Fix Invoke-GenerateSbom dispatcher DryRun compatibility with aliases and optional product metadata; make update-vipm-dependencies fail when a package cannot be resolved or updated.' ) } } diff --git a/actions/OpenSourceActions.psm1 b/actions/OpenSourceActions.psm1 index 9c787f0..1f31399 100644 --- a/actions/OpenSourceActions.psm1 +++ b/actions/OpenSourceActions.psm1 @@ -930,4 +930,55 @@ function Invoke-ViaLvDocker { DockerImage = $DockerImage } return Invoke-OpenSourceActionScript -ScriptSegments @('via-lv-docker','RunViaLvDocker.ps1') -Arguments $args -DryRun:$DryRun -gcliPath $gcliPath +} + +# Generates a CycloneDX SBOM for a LabVIEW build spec using `vipm sbom`. +# Assumes VIPM CLI is already installed/activated and LabVIEW is available on +# the runner; the caller is responsible for that (same as ni/labview-icon-editor#540's +# own "Install VIPM CLI" step). +# LvprojPath: Path to the .lvproj file to scan. +# LabVIEWVersion: LabVIEW version (YYYY). +# LabVIEWBitness: "32" or "64". +# BuildSpecName: Name of the build specification to scope the SBOM to. +# TargetName: LabVIEW project target containing the build spec (default: "My Computer"). +# ProductName: Name recorded in the SBOM's metadata.component. +# ProductVersion: Version recorded in the SBOM's metadata.component. +# OutputPath: Output file path for the generated SBOM. +# Format: SBOM output format (default: "cyclonedx"). +# SchemaVersion: CycloneDX schema version (default: "1.5"). +# DryRun: If set, prints the command instead of executing it. +function Invoke-GenerateSbom { + [CmdletBinding()] + param( + [Parameter(Mandatory)][Alias('ProjectPath', 'ProjectFile')] [string] $LvprojPath, + [Parameter(Mandatory)][Alias('LVVersion', 'MinimumSupportedLVVersion')] [string] $LabVIEWVersion, + [Parameter(Mandatory)][Alias('LVBitness', 'SupportedBitness')] [string] $LabVIEWBitness, + [Parameter(Mandatory)][Alias('Build_Spec')] [string] $BuildSpecName, + [Parameter()] [string] $TargetName = "My Computer", + [Parameter()] [string] $ProductName = "", + [Parameter()] [string] $ProductVersion = "", + [Parameter(Mandatory)] [string] $OutputPath, + [Parameter()] [string] $Format = "cyclonedx", + [Parameter()] [string] $SchemaVersion = "1.5", + [switch] $DryRun + ) + Write-Information "Invoking GenerateSbom" -InformationAction Continue + + $result = Invoke-OpenSourceActionScript ` + -ScriptSegments @('generate-sbom', 'GenerateSbom.ps1') ` + -Arguments @{ + LvprojPath = $LvprojPath + LabVIEWVersion = $LabVIEWVersion + LabVIEWBitness = $LabVIEWBitness + BuildSpecName = $BuildSpecName + TargetName = $TargetName + ProductName = $ProductName + ProductVersion = $ProductVersion + OutputPath = $OutputPath + Format = $Format + SchemaVersion = $SchemaVersion + } ` + -DryRun:$DryRun + + return $result } \ No newline at end of file diff --git a/artifacts/linux/action-docs.json b/artifacts/linux/action-docs.json index 778b61c..06f2ede 100644 --- a/artifacts/linux/action-docs.json +++ b/artifacts/linux/action-docs.json @@ -585,6 +585,71 @@ } } }, + "Invoke-GenerateSbom": { + "description": "Generates a CycloneDX SBOM for a LabVIEW build spec using `vipm sbom`. Assumes VIPM CLI is already installed/activated and LabVIEW is available on the runner; the caller is responsible for that (same as ni/labview-icon-editor#540's own \"Install VIPM CLI\" step). LvprojPath: Path to the .lvproj file to scan. LabVIEWVersion: LabVIEW version (YYYY). LabVIEWBitness: \"32\" or \"64\". BuildSpecName: Name of the build specification to scope the SBOM to. TargetName: LabVIEW project target containing the build spec (default: \"My Computer\"). ProductName: Name recorded in the SBOM's metadata.component. ProductVersion: Version recorded in the SBOM's metadata.component. OutputPath: Output file path for the generated SBOM. Format: SBOM output format (default: \"cyclonedx\"). SchemaVersion: CycloneDX schema version (default: \"1.5\"). DryRun: If set, prints the command instead of executing it.", + "parameters": { + "BuildSpecName": { + "type": "string", + "required": true, + "description": "Name of the build specification to scope the SBOM to" + }, + "DryRun": { + "type": "boolean", + "required": false, + "description": "If set, prints the command instead of executing it" + }, + "Format": { + "type": "string", + "required": false, + "default": "cyclonedx", + "description": "SBOM output format (default: \"cyclonedx\")" + }, + "LabVIEWBitness": { + "type": "string", + "required": true, + "description": "\"32\" or \"64\"" + }, + "LabVIEWVersion": { + "type": "string", + "required": true, + "description": "LabVIEW version (YYYY)" + }, + "LvprojPath": { + "type": "string", + "required": true, + "description": "Path to the" + }, + "OutputPath": { + "type": "string", + "required": true, + "description": "Output file path for the generated SBOM" + }, + "ProductName": { + "type": "string", + "required": false, + "default": "", + "description": "Name recorded in the SBOM's metadata" + }, + "ProductVersion": { + "type": "string", + "required": false, + "default": "", + "description": "Version recorded in the SBOM's metadata" + }, + "SchemaVersion": { + "type": "string", + "required": false, + "default": "1.5", + "description": "CycloneDX schema version (default: \"1" + }, + "TargetName": { + "type": "string", + "required": false, + "default": "My Computer", + "description": "LabVIEW project target containing the build spec (default: \"My Computer\")" + } + } + }, "Invoke-MissingInProject": { "description": "Lists files referenced in a LabVIEW project that are missing on disk. LVVersion: LabVIEW version of the project. SupportedBitness: Target LabVIEW bitness (32- or 64-bit). ProjectFile: Path to the .lvproj file to analyze. DryRun: If set, prints the command instead of executing it. gcliPath: Optional path prepended to PATH for locating the g CLI.", "parameters": { @@ -1892,6 +1957,99 @@ "type": "string" } ], + "generate-sbom": [ + { + "name": "lvproj_path", + "description": "Path to the .lvproj file to scan.", + "required": true, + "default": "", + "type": "string" + }, + { + "name": "labview_version", + "description": "LabVIEW version (YYYY).", + "required": true, + "default": "", + "type": "string" + }, + { + "name": "labview_bitness", + "description": "\"32\" or \"64\".", + "required": true, + "default": "", + "type": "string" + }, + { + "name": "build_spec_name", + "description": "Name of the build specification to scope the SBOM to.", + "required": true, + "default": "", + "type": "string" + }, + { + "name": "target_name", + "description": "LabVIEW project target containing the build spec.", + "required": false, + "default": "My Computer", + "type": "string" + }, + { + "name": "product_name", + "description": "Name recorded in the SBOM metadata.component.", + "required": true, + "default": "", + "type": "string" + }, + { + "name": "product_version", + "description": "Version recorded in the SBOM metadata.component.", + "required": true, + "default": "", + "type": "string" + }, + { + "name": "output_path", + "description": "Output file path for the generated SBOM.", + "required": true, + "default": "", + "type": "string" + }, + { + "name": "format", + "description": "SBOM output format.", + "required": false, + "default": "cyclonedx", + "type": "string" + }, + { + "name": "schema_version", + "description": "CycloneDX schema version.", + "required": false, + "default": "1.5", + "type": "string" + }, + { + "name": "working_directory", + "description": "Working directory where the action will run.", + "required": false, + "default": "", + "type": "string" + }, + { + "name": "log_level", + "description": "Verbosity level (ERROR|WARN|INFO|DEBUG).", + "required": false, + "default": "INFO", + "type": "string" + }, + { + "name": "dry_run", + "description": "If true, simulate the action without side effects.", + "required": false, + "default": false, + "type": "string" + } + ], "missing-in-project": [ { "name": "lv_version", diff --git a/artifacts/linux/action-docs.md b/artifacts/linux/action-docs.md index 1509afd..1ac1f01 100644 --- a/artifacts/linux/action-docs.md +++ b/artifacts/linux/action-docs.md @@ -203,6 +203,26 @@ Generates a release notes file from the project's metadata. OutputPath: Path whe pwsh ./actions/Invoke-OSAction.ps1 -ActionName Invoke-GenerateReleaseNotes -ArgsJson '{}' ``` +#### Invoke-GenerateSbom +Generates a CycloneDX SBOM for a LabVIEW build spec using `vipm sbom`. Assumes VIPM CLI is already installed/activated and LabVIEW is available on the runner; the caller is responsible for that (same as ni/labview-icon-editor#540's own "Install VIPM CLI" step). LvprojPath: Path to the .lvproj file to scan. LabVIEWVersion: LabVIEW version (YYYY). LabVIEWBitness: "32" or "64". BuildSpecName: Name of the build specification to scope the SBOM to. TargetName: LabVIEW project target containing the build spec (default: "My Computer"). ProductName: Name recorded in the SBOM's metadata.component. ProductVersion: Version recorded in the SBOM's metadata.component. OutputPath: Output file path for the generated SBOM. Format: SBOM output format (default: "cyclonedx"). SchemaVersion: CycloneDX schema version (default: "1.5"). DryRun: If set, prints the command instead of executing it. +| Parameter | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| BuildSpecName | string | true | | Name of the build specification to scope the SBOM to | +| DryRun | boolean | false | | If set, prints the command instead of executing it | +| Format | string | false | cyclonedx | SBOM output format (default: "cyclonedx") | +| LabVIEWBitness | string | true | | "32" or "64" | +| LabVIEWVersion | string | true | | LabVIEW version (YYYY) | +| LvprojPath | string | true | | Path to the | +| OutputPath | string | true | | Output file path for the generated SBOM | +| ProductName | string | false | | Name recorded in the SBOM's metadata | +| ProductVersion | string | false | | Version recorded in the SBOM's metadata | +| SchemaVersion | string | false | 1.5 | CycloneDX schema version (default: "1 | +| TargetName | string | false | My Computer | LabVIEW project target containing the build spec (default: "My Computer") | + +```powershell +pwsh ./actions/Invoke-OSAction.ps1 -ActionName Invoke-GenerateSbom -ArgsJson '{}' +``` + #### Invoke-MissingInProject Lists files referenced in a LabVIEW project that are missing on disk. LVVersion: LabVIEW version of the project. SupportedBitness: Target LabVIEW bitness (32- or 64-bit). ProjectFile: Path to the .lvproj file to analyze. DryRun: If set, prints the command instead of executing it. gcliPath: Optional path prepended to PATH for locating the g CLI. | Parameter | Type | Required | Default | Description | @@ -579,6 +599,23 @@ server.viscripting.ShowScriptingOperationsInEditor=TRUE | log_level | string | false | INFO | Verbosity level (ERROR|WARN|INFO|DEBUG). | | dry_run | string | false | false | If true, simulate the action without side effects. | +#### generate-sbom +| Name | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| lvproj_path | string | true | | Path to the .lvproj file to scan. | +| labview_version | string | true | | LabVIEW version (YYYY). | +| labview_bitness | string | true | | "32" or "64". | +| build_spec_name | string | true | | Name of the build specification to scope the SBOM to. | +| target_name | string | false | My Computer | LabVIEW project target containing the build spec. | +| product_name | string | true | | Name recorded in the SBOM metadata.component. | +| product_version | string | true | | Version recorded in the SBOM metadata.component. | +| output_path | string | true | | Output file path for the generated SBOM. | +| format | string | false | cyclonedx | SBOM output format. | +| schema_version | string | false | 1.5 | CycloneDX schema version. | +| working_directory | string | false | | Working directory where the action will run. | +| log_level | string | false | INFO | Verbosity level (ERROR|WARN|INFO|DEBUG). | +| dry_run | string | false | false | If true, simulate the action without side effects. | + #### missing-in-project | Name | Type | Required | Default | Description | | --- | --- | --- | --- | --- | diff --git a/ci_evidence.txt b/ci_evidence.txt index dd38ea0..5fbaf88 100644 --- a/ci_evidence.txt +++ b/ci_evidence.txt @@ -1 +1 @@ -{"pipeline":"Unknown","git_sha":"5fbaea9b62f424424f5c5608977e4a5489f405be","req_status":{"REQ-023":"PASS","REQ-024":"PASS","REQ-025":"PASS","REQ-026":"PASS","REQ-027":"PASS","REQ-028":"PASS","REQ-029":"PASS","REQ-030":"PASS","REQ-031":"PASS","REQ-032":"PASS","REQ-033":"PASS"}} \ No newline at end of file +{"pipeline":"Unknown","git_sha":"e92713aec46744b7f7e2f148f3e447e3d5a5db27","req_status":{"REQ-023":"PASS","REQ-024":"PASS","REQ-025":"PASS","REQ-026":"PASS","REQ-027":"PASS","REQ-028":"PASS","REQ-029":"PASS","REQ-030":"PASS","REQ-031":"PASS","REQ-032":"PASS","REQ-033":"PASS"}} \ No newline at end of file diff --git a/dispatchers.json b/dispatchers.json index 836760e..502c977 100644 --- a/dispatchers.json +++ b/dispatchers.json @@ -583,6 +583,71 @@ } } }, + "Invoke-GenerateSbom": { + "description": "Generates a CycloneDX SBOM for a LabVIEW build spec using `vipm sbom`. Assumes VIPM CLI is already installed/activated and LabVIEW is available on the runner; the caller is responsible for that (same as ni/labview-icon-editor#540's own \"Install VIPM CLI\" step). LvprojPath: Path to the .lvproj file to scan. LabVIEWVersion: LabVIEW version (YYYY). LabVIEWBitness: \"32\" or \"64\". BuildSpecName: Name of the build specification to scope the SBOM to. TargetName: LabVIEW project target containing the build spec (default: \"My Computer\"). ProductName: Name recorded in the SBOM's metadata.component. ProductVersion: Version recorded in the SBOM's metadata.component. OutputPath: Output file path for the generated SBOM. Format: SBOM output format (default: \"cyclonedx\"). SchemaVersion: CycloneDX schema version (default: \"1.5\"). DryRun: If set, prints the command instead of executing it.", + "parameters": { + "BuildSpecName": { + "type": "string", + "required": true, + "description": "Name of the build specification to scope the SBOM to" + }, + "DryRun": { + "type": "boolean", + "required": false, + "description": "If set, prints the command instead of executing it" + }, + "Format": { + "type": "string", + "required": false, + "default": "cyclonedx", + "description": "SBOM output format (default: \"cyclonedx\")" + }, + "LabVIEWBitness": { + "type": "string", + "required": true, + "description": "\"32\" or \"64\"" + }, + "LabVIEWVersion": { + "type": "string", + "required": true, + "description": "LabVIEW version (YYYY)" + }, + "LvprojPath": { + "type": "string", + "required": true, + "description": "Path to the" + }, + "OutputPath": { + "type": "string", + "required": true, + "description": "Output file path for the generated SBOM" + }, + "ProductName": { + "type": "string", + "required": false, + "default": "", + "description": "Name recorded in the SBOM's metadata" + }, + "ProductVersion": { + "type": "string", + "required": false, + "default": "", + "description": "Version recorded in the SBOM's metadata" + }, + "SchemaVersion": { + "type": "string", + "required": false, + "default": "1.5", + "description": "CycloneDX schema version (default: \"1" + }, + "TargetName": { + "type": "string", + "required": false, + "default": "My Computer", + "description": "LabVIEW project target containing the build spec (default: \"My Computer\")" + } + } + }, "Invoke-MissingInProject": { "description": "Lists files referenced in a LabVIEW project that are missing on disk. LVVersion: LabVIEW version of the project. SupportedBitness: Target LabVIEW bitness (32- or 64-bit). ProjectFile: Path to the .lvproj file to analyze. DryRun: If set, prints the command instead of executing it. gcliPath: Optional path prepended to PATH for locating the g CLI.", "parameters": { diff --git a/docs/actions/generate-sbom.md b/docs/actions/generate-sbom.md new file mode 100644 index 0000000..de937ae --- /dev/null +++ b/docs/actions/generate-sbom.md @@ -0,0 +1,110 @@ +# generate-sbom + +## Purpose + +Generate a CycloneDX Software Bill of Materials (SBOM) for a LabVIEW build specification using `vipm sbom`. The reusable workflow runs on `ubuntu-latest` and invokes VIPM inside the matching `nationalinstruments/labview:-linux` container, following the Linux flow validated in [ni/labview-icon-editor#540](https://github.com/ni/labview-icon-editor/pull/540). The standalone composite action remains available for callers that already have VIPM and LabVIEW configured. + +## Parameters + +Common parameters are described in [Common parameters](../common-parameters.md). + +### Required + +- **LvprojPath** (`string`): Path to the `.lvproj` file to scan. +- **LabVIEWVersion** (`string`): LabVIEW version (`YYYY`). +- **LabVIEWBitness** (`string`): `"32"` or `"64"`. +- **BuildSpecName** (`string`): Name of the build specification to scope the SBOM to. +- **ProductName** (`string`): Name recorded in the SBOM's `metadata.component`. +- **ProductVersion** (`string`): Version recorded in the SBOM's `metadata.component`. +- **OutputPath** (`string`): Output file path for the generated SBOM. + +### Optional + +- **TargetName** (`string`): LabVIEW project target containing the build spec (default: `"My Computer"`). +- **Format** (`string`): SBOM output format (default: `"cyclonedx"`). +- **SchemaVersion** (`string`): CycloneDX schema version (default: `"1.5"`). + +### GitHub Action inputs + +| Input | CLI parameter | Description | +| --- | --- | --- | +| `lvproj_path` | `LvprojPath` | Path to the `.lvproj` file to scan. | +| `labview_version` | `LabVIEWVersion` | LabVIEW version (`YYYY`). | +| `labview_bitness` | `LabVIEWBitness` | `"32"` or `"64"`. | +| `build_spec_name` | `BuildSpecName` | Build specification to scope the SBOM to. | +| `target_name` | `TargetName` | LabVIEW project target (default: `"My Computer"`). | +| `product_name` | `ProductName` | Name recorded in the SBOM. | +| `product_version` | `ProductVersion` | Version recorded in the SBOM. | +| `output_path` | `OutputPath` | Output file path for the generated SBOM. | +| `format` | `Format` | SBOM output format (default: `"cyclonedx"`). | +| `schema_version` | `SchemaVersion` | CycloneDX schema version (default: `"1.5"`). | +| `log_level` | `LogLevel` | Verbosity level (ERROR\|WARN\|INFO\|DEBUG). | +| `dry_run` | `DryRun` | If true, simulate the action without side effects. | + +## Prerequisites + +The standalone composite action does **not** install or activate VIPM, and does not set up LabVIEW. The reusable Linux workflow is self-contained: it checks out the caller repository, starts the LabVIEW Linux container, installs VIPM from its Debian package, and generates the SBOM there. Its `labview_version` input is the container release (`YYYYq1` or `YYYYq3`, minimum `2025q3`); the workflow derives the year passed to VIPM. + +## Examples + +### CLI + +```powershell +pwsh -File actions/Invoke-OSAction.ps1 -ActionName generate-sbom -ArgsJson '{ + "LvprojPath": "lv_icon_editor.lvproj", + "LabVIEWVersion": "2026", + "LabVIEWBitness": "64", + "BuildSpecName": "Editor Packed Library", + "ProductName": "Icon Editor Packed Library", + "ProductVersion": "1.0.0.0", + "OutputPath": "builds/lv_icon_editor_Editor Packed Library.cdx.sbom.json" +}' +``` + +### GitHub Action (direct, as a step in an existing job) + +```yaml +- name: Install VIPM CLI + # ... existing install/activate steps ... +- name: Generate SBOM + uses: ni/open-source/generate-sbom@v1 + with: + lvproj_path: lv_icon_editor.lvproj + labview_version: '2026' + labview_bitness: '64' + build_spec_name: 'Editor Packed Library' + product_name: 'Icon Editor Packed Library' + product_version: '1.0.0.0' + output_path: 'builds/lv_icon_editor_Editor Packed Library.cdx.sbom.json' +``` + +### Reusable workflow (Linux container) + +Actor Framework, Icon Editor, and other consumer repositories can call the reusable workflow. It uploads the generated SBOM as an artifact retained for 90 days: + +```yaml +name: Generate SBOM + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + generate-sbom: + uses: ni/open-source/.github/workflows/reusable-generate-sbom.yml@v1 + with: + lvproj_path: lv_icon_editor.lvproj + labview_version: '2026q1' + labview_bitness: '64' + build_spec_name: 'Editor Packed Library' + product_name: 'Icon Editor Packed Library' + product_version: '1.0.0.0' + output_path: 'builds/lv_icon_editor_Editor Packed Library.cdx.sbom.json' + artifact_name: 'sbom' +``` + +## Return Codes + +- `0` - success +- non-zero - `vipm` not found on `PATH`, `vipm sbom` failure, or no SBOM file produced diff --git a/docs/index.md b/docs/index.md index 44c5aab..ef832c4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,6 +30,7 @@ Open Source LabVIEW Actions unifies LabVIEW [CI/CD](glossary.md#ci-cd) scripts b | [build-vi-package](actions/build-vi-package.md) | Update VIPB display information and build a VI package using g-cli. | | [close-labview](actions/close-labview.md) | Gracefully close a running LabVIEW instance via g-cli. | | [generate-release-notes](actions/generate-release-notes.md) | Generate release notes from the git history and write them to a markdown file. | +| [generate-sbom](actions/generate-sbom.md) | Generate a CycloneDX SBOM for a LabVIEW build spec using `vipm sbom`. | | [missing-in-project](actions/missing-in-project.md) | Check that all files in a LabVIEW project are present by scanning for items missing from the `.lvproj`. | | [modify-vipb-display-info](actions/modify-vipb-display-info.md) | Update display information in a VIPB file and rebuild the VI package. | | [prepare-labview-source](actions/prepare-labview-source.md) | Run PrepareIESource.vi via g-cli to unzip components and configure LabVIEW for building. | diff --git a/generate-sbom/action.yml b/generate-sbom/action.yml new file mode 100644 index 0000000..619d9b4 --- /dev/null +++ b/generate-sbom/action.yml @@ -0,0 +1,76 @@ +name: 'Generate SBOM' +description: 'Generates a CycloneDX SBOM for a LabVIEW build spec using vipm sbom. Requires VIPM CLI to be available on the runner; the reusable workflow installs it automatically.' +inputs: + lvproj_path: + description: 'Path to the .lvproj file to scan.' + required: true + labview_version: + description: 'LabVIEW version (YYYY).' + required: true + labview_bitness: + description: '"32" or "64".' + required: true + build_spec_name: + description: 'Name of the build specification to scope the SBOM to.' + required: true + target_name: + description: 'LabVIEW project target containing the build spec.' + default: 'My Computer' + required: false + product_name: + description: 'Name recorded in the SBOM metadata.component.' + required: true + product_version: + description: 'Version recorded in the SBOM metadata.component.' + required: true + output_path: + description: 'Output file path for the generated SBOM.' + required: true + format: + description: 'SBOM output format.' + default: 'cyclonedx' + required: false + schema_version: + description: 'CycloneDX schema version.' + default: '1.5' + required: false + working_directory: + description: 'Working directory where the action will run.' + required: false + log_level: + description: 'Verbosity level (ERROR|WARN|INFO|DEBUG).' + default: 'INFO' + required: false + dry_run: + description: 'If true, simulate the action without side effects.' + default: false + required: false +runs: + using: 'composite' + steps: + - name: Dispatch generate-sbom + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $args = @{ + LvprojPath = '${{ inputs.lvproj_path }}' + LabVIEWVersion = '${{ inputs.labview_version }}' + LabVIEWBitness = '${{ inputs.labview_bitness }}' + BuildSpecName = '${{ inputs.build_spec_name }}' + TargetName = '${{ inputs.target_name }}' + ProductName = '${{ inputs.product_name }}' + ProductVersion = '${{ inputs.product_version }}' + OutputPath = '${{ inputs.output_path }}' + Format = '${{ inputs.format }}' + SchemaVersion = '${{ inputs.schema_version }}' + } + $params = @{ + ActionName = 'generate-sbom' + Args = $args + LogLevel = '${{ inputs.log_level }}' + } + if ('${{ inputs.dry_run }}' -eq 'true') { $params['DryRun'] = $true } + if ('${{ inputs.working_directory }}') { $params['WorkingDirectory'] = '${{ inputs.working_directory }}' } + $script = Join-Path $env:GITHUB_ACTION_PATH '..' 'actions' 'common-dispatch.ps1' + & $script @params + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/mkdocs.yml b/mkdocs.yml index 09448f7..9873dc2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -57,6 +57,7 @@ nav: - Build VI Package: actions/build-vi-package.md - Close LabVIEW: actions/close-labview.md - Generate Release Notes: actions/generate-release-notes.md + - Generate SBOM: actions/generate-sbom.md - Missing in Project: actions/missing-in-project.md - Modify VIPB Display Info: actions/modify-vipb-display-info.md - Prepare LabVIEW Source: actions/prepare-labview-source.md diff --git a/requirements.json b/requirements.json index ab7ecd1..ecdb9fc 100644 --- a/requirements.json +++ b/requirements.json @@ -317,6 +317,14 @@ "BuildSpecGithubHostedWindows.Workflow.Tests" ] }, + { + "id": "REQ-042", + "description": "Workflow tests the composite action defined in generate-sbom/action.yml on the GitHub-hosted Ubuntu runner labeled ubuntu-latest.", + "runner": "ubuntu-latest", + "tests": [ + "GenerateSbom.Workflow.Tests" + ] + }, { "id": "REQ-043", "description": "Workflow tests the composite action defined in refresh-vipm-lock/action.yml on the GitHub-hosted Ubuntu runner labeled ubuntu-latest.", diff --git a/scripts/generate-sbom/GenerateSbom.ps1 b/scripts/generate-sbom/GenerateSbom.ps1 new file mode 100644 index 0000000..031e82a --- /dev/null +++ b/scripts/generate-sbom/GenerateSbom.ps1 @@ -0,0 +1,115 @@ +<# +.SYNOPSIS + Generates a CycloneDX SBOM for a LabVIEW build spec using `vipm sbom`. + +.DESCRIPTION + Minimal wrapper: requires VIPM CLI to be available on the runner and LabVIEW + to be available for the target project. The reusable workflow installs VIPM + before invoking this script; callers that invoke the action directly are + responsible for installing VIPM themselves. Just runs `vipm sbom` with the + given flags and verifies the output file exists. See + docs.vipm.io/cli/command-reference#vipm-sbom. + +.PARAMETER LvprojPath + Path to the .lvproj file to scan. + +.PARAMETER LabVIEWVersion + LabVIEW version (YYYY). + +.PARAMETER LabVIEWBitness + "32" or "64". + +.PARAMETER BuildSpecName + Name of the build specification within the .lvproj to scope the SBOM to. + +.PARAMETER TargetName + LabVIEW project target containing the build spec. Defaults to "My Computer". + +.PARAMETER ProductName + Name recorded in the SBOM's metadata.component. + +.PARAMETER ProductVersion + Version recorded in the SBOM's metadata.component. + +.PARAMETER OutputPath + Output file path for the generated SBOM. + +.PARAMETER Format + SBOM output format. Defaults to "cyclonedx". + +.PARAMETER SchemaVersion + CycloneDX schema version. Defaults to "1.5". + +.NOTES + Leaf script for the 'generate-sbom' dispatcher action. +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$LvprojPath, + + [Parameter(Mandatory = $true)] + [string]$LabVIEWVersion, + + [Parameter(Mandatory = $true)] + [string]$LabVIEWBitness, + + [Parameter(Mandatory = $true)] + [string]$BuildSpecName, + + [Parameter(Mandatory = $false)] + [string]$TargetName = "My Computer", + + [Parameter(Mandatory = $true)] + [string]$ProductName, + + [Parameter(Mandatory = $true)] + [string]$ProductVersion, + + [Parameter(Mandatory = $true)] + [string]$OutputPath, + + [Parameter(Mandatory = $false)] + [string]$Format = "cyclonedx", + + [Parameter(Mandatory = $false)] + [string]$SchemaVersion = "1.5" +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +try { + if (-not (Get-Command vipm -ErrorAction SilentlyContinue)) { + throw "vipm CLI not found on PATH. Install and activate VIPM before calling generate-sbom." + } + + Write-Information "Generating SBOM for '$LvprojPath' (build spec '$BuildSpecName')..." -InformationAction Continue + + vipm sbom $LvprojPath ` + --format $Format ` + --schema-version $SchemaVersion ` + --labview-version $LabVIEWVersion ` + --labview-bitness $LabVIEWBitness ` + --lvproj-build-spec $BuildSpecName ` + --lvproj-target $TargetName ` + --product-name $ProductName ` + --product-version $ProductVersion ` + --output $OutputPath + + if ($LASTEXITCODE -ne 0) { + throw "vipm sbom failed with exit code $LASTEXITCODE" + } + + if (-not (Test-Path $OutputPath)) { + throw "No SBOM file found at $OutputPath. Ensure vipm sbom completed successfully." + } + + Write-Information "SBOM verified: $OutputPath" -InformationAction Continue + exit 0 +} +catch { + Write-Error "GenerateSbom failed: $_" + exit 1 +} diff --git a/tests/pester/GenerateSbom.Workflow.Tests.ps1 b/tests/pester/GenerateSbom.Workflow.Tests.ps1 new file mode 100644 index 0000000..7dd951d --- /dev/null +++ b/tests/pester/GenerateSbom.Workflow.Tests.ps1 @@ -0,0 +1,55 @@ +#requires -Version 7.0 +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +Describe 'GenerateSbom.Workflow' { + $meta = @{ + requirement = 'REQ-042' + Owner = 'NI' + Evidence = 'tests/pester/GenerateSbom.Workflow.Tests.ps1' + } + + BeforeAll { + $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..' '..')).Path + Import-Module (Join-Path $repoRoot 'actions' 'OpenSourceActions.psd1') -Force + } + + It 'validates action.yml exists [REQ-042]' -Tag 'REQ-042' { + $actionPath = Join-Path $repoRoot 'generate-sbom' 'action.yml' + Test-Path $actionPath | Should -Be $true + } + + It 'validates implementation script exists [REQ-042]' -Tag 'REQ-042' { + $scriptPath = Join-Path $repoRoot 'scripts' 'generate-sbom' 'GenerateSbom.ps1' + Test-Path $scriptPath | Should -Be $true + } + + It 'uses the tested Linux LabVIEW container flow [REQ-042]' -Tag 'REQ-042' { + $workflowPath = Join-Path $repoRoot '.github' 'workflows' 'reusable-generate-sbom.yml' + $workflow = Get-Content $workflowPath -Raw + + $workflow | Should -Match 'runs-on:\s*ubuntu-latest' + $workflow | Should -Match 'vipm_26\.3\.1-4025_amd64\.deb' + $workflow | Should -Match 'dpkg -i /tmp/vipm\.deb' + $workflow | Should -Match 'timeout 300 xvfb-run -a vipm sbom' + $workflow | Should -Match 'actions/upload-artifact@' + } + + It 'executes dry-run with only required parameters [REQ-042]' -Tag 'REQ-042' { + $result = & "$repoRoot/actions/Invoke-OSAction.ps1" ` + -ActionName 'generate-sbom' ` + -ArgsJson '{"LvprojPath":"test.lvproj","LabVIEWVersion":"2026","LabVIEWBitness":"64","BuildSpecName":"TestBuild","ProductName":"Test Product","ProductVersion":"1.0.0","OutputPath":"sbom.json"}' ` + -DryRun + + $LASTEXITCODE | Should -Be 0 + } + + It 'executes dry-run with all parameters [REQ-042]' -Tag 'REQ-042' { + $result = & "$repoRoot/actions/Invoke-OSAction.ps1" ` + -ActionName 'generate-sbom' ` + -ArgsJson '{"LvprojPath":"test.lvproj","LabVIEWVersion":"2026","LabVIEWBitness":"64","BuildSpecName":"TestBuild","TargetName":"My Computer","ProductName":"Test Product","ProductVersion":"1.0.0","OutputPath":"sbom.json","Format":"cyclonedx","SchemaVersion":"1.5"}' ` + -DryRun + + $LASTEXITCODE | Should -Be 0 + } +}