diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c96389ec..a2327a44 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -88,6 +88,12 @@ jobs: .\tools\generate-dotnet-notices.ps1 -PublishDirectory artifacts/win-x64 + - name: Generate SBOM and review dependency licenses + shell: pwsh + run: >- + .\tools\generate-sbom.ps1 + -PublishDirectory artifacts/win-x64 + - name: Create and verify Windows alpha archive shell: pwsh run: | diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index e0c757c4..4a7587d3 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -1,6 +1,6 @@ # SightAdapt dependency inventory -This inventory distinguishes components shipped in a SightAdapt binary distribution from build, test and operating-system dependencies. The machine-readable exact release evidence is stored in `DOTNET-NOTICE-METADATA.json` inside the final package. +This repository copy documents the currently reviewed dependency set. Release builds regenerate `DEPENDENCIES.md` inside the final package from `release/dependency-policy.json`, the actual restore graph, exact .NET notice metadata, project references and workflow actions. The same evaluated inventory is written to `SBOM.spdx.json` and `LICENSE-REPORT.json`. ## Components shipped in the Windows x64 package @@ -36,6 +36,7 @@ These components are required at runtime but are not copied into the SightAdapt | Component | Current repository version | Purpose | |---|---|---| | .NET SDK | `8.0.423` | Restore, build, test, publish and authoritative legal-text source for the matching release train | +| Microsoft Windows SDK .NET reference package | `10.0.19041.56` | Compile-time Windows API references | | Microsoft.NET.Test.Sdk | `17.11.1` | Test host | | MSTest.TestAdapter | `3.6.4` | Test discovery and execution | | MSTest.TestFramework | `3.6.4` | Test framework | @@ -43,7 +44,7 @@ These components are required at runtime but are not copied into the SightAdapt | GitHub Actions .NET setup | `actions/setup-dotnet@v4` | CI SDK installation | | GitHub Actions artifact upload | `actions/upload-artifact@v4` | CI artifact publication | -Build and test dependencies are development infrastructure and are not part of the final SightAdapt binary archive unless explicitly listed in the shipped-components table. +Build and test dependencies are development infrastructure and are not part of the final SightAdapt binary archive unless explicitly marked as shipped by the generated inventory. ## Release maintenance @@ -52,5 +53,6 @@ For every official binary release, maintainers must: 1. keep `global.json` and the expected .NET properties in `Directory.Build.props` synchronized; 2. restore and review the exact runtime-pack inventory in `project.assets.json`; 3. verify the official SDK ZIP SHA-512 and import its license and third-party notice files; -4. review `DOTNET-NOTICE-METADATA.json` and any newly mapped runtime component; -5. verify the final archive against `release/required-files.txt`. +4. update `release/dependency-policy.json` for every dependency, version, supplier or license change; +5. generate and review `SBOM.spdx.json`, `LICENSE-REPORT.json` and the package `DEPENDENCIES.md`; +6. verify the final archive against `release/required-files.txt`. diff --git a/docs/PACKAGING.md b/docs/PACKAGING.md index 93c7177c..3923c6c0 100644 --- a/docs/PACKAGING.md +++ b/docs/PACKAGING.md @@ -20,7 +20,9 @@ Every package must place the following files at the package root so they can be | `DOTNET-LICENSE-NOTICE.txt` | Exact-version Microsoft .NET license text and source metadata | | `DOTNET-NOTICE-METADATA.json` | Machine-readable SDK, runtime, RID, runtime-pack, source and checksum evidence | | `MICROSOFT-DOTNET-REDISTRIBUTION.txt` | End-user and downstream-distributor conditions that keep Microsoft components separate from SightAdapt's MIT license | -| `DEPENDENCIES.md` | Human-readable inventory of shipped, platform and development dependencies | +| `DEPENDENCIES.md` | Human-readable inventory generated from the evaluated dependency list | +| `SBOM.spdx.json` | SPDX 2.3 component and shipped-file inventory | +| `LICENSE-REPORT.json` | Automated dependency-license policy result | | `PRIVACY.md` | Application privacy and local-data notice | A package is incomplete if a required file is missing, empty, unreadable, stale, inconsistent with the pinned build inputs or nested only inside another container that the recipient cannot inspect directly. @@ -42,7 +44,7 @@ Release mirrors must copy the verified archive without removing, renaming or rep ## Publish behavior -`src/SightAdapt/SightAdapt.csproj` copies the legal baseline and redistribution notice into the publish directory. `tools/generate-dotnet-notices.ps1` then replaces the baseline .NET files with exact-version material and adds the metadata record before packaging. +`src/SightAdapt/SightAdapt.csproj` copies the legal baseline and redistribution notice into the publish directory. `tools/generate-dotnet-notices.ps1` replaces the baseline .NET files with exact-version material. `tools/generate-sbom.ps1` then evaluates dependency licenses and generates `DEPENDENCIES.md`, `SBOM.spdx.json` and `LICENSE-REPORT.json` before packaging. The expected publish layout begins with: @@ -55,6 +57,8 @@ artifacts/win-x64/ ├── DOTNET-NOTICE-METADATA.json ├── MICROSOFT-DOTNET-REDISTRIBUTION.txt ├── DEPENDENCIES.md +├── SBOM.spdx.json +├── LICENSE-REPORT.json └── PRIVACY.md ``` @@ -76,26 +80,21 @@ Compress-Archive ` .\tools\test-release-package.ps1 -ArchivePath $archive ``` -The validation script opens the final ZIP and checks the entries listed in `release/required-files.txt`. Required documents must be readable UTF-8 text. The .NET metadata must match the exact release inputs in `Directory.Build.props`, contain valid checksums and map both required runtime packs. The redistribution notice must identify the same product, SDK, runtime, target framework, RID and publish mode, clearly separate Microsoft's terms from MIT, prohibit standalone redistribution and disclaim Microsoft endorsement. +The validation script opens the final ZIP and checks the entries listed in `release/required-files.txt`. Required documents must be readable UTF-8 text. The exact .NET metadata must match the pinned release inputs and map the required runtime packs. SBOM generation has already failed the workflow if a dependency is absent from the reviewed policy, uses a different version or has a denied/unreviewed license. ## Microsoft .NET notices and redistribution position SightAdapt is a self-contained Windows application. `global.json` pins the SDK, while `Directory.Build.props` records the expected SDK/runtime release mapping. The actual restore graph in `project.assets.json` identifies the exact runtime packs selected by the build. -The generator obtains the matching official .NET SDK ZIP through Microsoft's release metadata, verifies its published SHA-512 hash and imports `LICENSE.txt` and `ThirdPartyNotices.txt` from that archive. The SDK ZIP is used because the standalone Windows Desktop Runtime ZIP does not contain those legal files; the restore graph, not the SDK ZIP contents, remains the authority for which runtime packs are associated with the SightAdapt publication. +The notice generator obtains the matching official .NET SDK ZIP through Microsoft's release metadata, verifies its published SHA-512 hash and imports `LICENSE.txt` and `ThirdPartyNotices.txt` from that archive. The SDK ZIP is used because the standalone Windows Desktop Runtime ZIP does not contain those legal files; the restore graph, not the SDK ZIP contents, remains the authority for which runtime packs are associated with the SightAdapt publication. -The package record identifies: +The reviewed redistribution position is documented in [Microsoft .NET redistribution analysis](legal/DOTNET-REDISTRIBUTION.md). `MICROSOFT-DOTNET-REDISTRIBUTION.txt` implements the recipient and downstream-distributor notice in each binary package. See also [Exact-version .NET notice generation](legal/DOTNET-NOTICE-GENERATION.md). -- exact .NET SDK version; -- exact runtime and Windows Desktop Runtime versions; -- runtime identifier and publish mode; -- restored runtime packages and framework; -- authoritative release-metadata and SDK-package URLs; -- official SDK package SHA-512; -- SHA-256 values for the imported license and notice text; -- generation time and product version. +## SBOM and license review -The reviewed redistribution position is documented in [Microsoft .NET redistribution analysis](legal/DOTNET-REDISTRIBUTION.md). `MICROSOFT-DOTNET-REDISTRIBUTION.txt` implements the recipient and downstream-distributor notice in each binary package. See also [Exact-version .NET notice generation](legal/DOTNET-NOTICE-GENERATION.md). +`release/dependency-policy.json` records the reviewed component versions, suppliers, sources, scopes and license decisions. `tools/generate-sbom.ps1` combines that policy with the actual restore graph, project references, workflow actions, .NET notice metadata and final publish files. + +The generated SPDX document contains component relationships and SHA-256 checksums for every separately shipped file. `SightAdapt.exe` is the documented single-file container for embedded managed and native runtime components. See [SBOM and dependency-license review](legal/SBOM-AND-LICENSE-REVIEW.md). ## Release checklist @@ -106,10 +105,11 @@ Before publishing or mirroring a binary package: 3. restore, build and test the application; 4. publish into a clean staging directory; 5. generate exact-version .NET notices from the hash-verified official SDK package and actual restore graph; -6. inspect the generated notice metadata and any newly mapped runtime component; -7. create the final archive or platform package; -8. validate the final archive with `tools/test-release-package.ps1`; -9. inspect the package manually to confirm that every legal document opens without running SightAdapt; -10. publish the same verified bytes to every official mirror. - -Do not publish an official binary release when the legal bundle, redistribution notice, exact-version generation, package checksum, runtime mapping or final-archive validation is incomplete. Production or paid distribution additionally remains blocked until the qualified legal review required by Issue #93 is recorded. +6. generate the SPDX SBOM, license report and human-readable dependency inventory; +7. resolve every component or license-policy failure; +8. create the final archive or platform package; +9. validate the final archive with `tools/test-release-package.ps1`; +10. inspect the package manually to confirm that every legal document opens without running SightAdapt; +11. publish the same verified bytes to every official mirror. + +Do not publish an official binary release when the legal bundle, redistribution notice, exact-version generation, SBOM, license report, package checksum, runtime mapping or final-archive validation is incomplete. Production or paid distribution additionally remains blocked until the qualified legal review required by Issue #93 is recorded. diff --git a/docs/README.md b/docs/README.md index 14f36329..a6f49ee7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,9 +4,10 @@ The documentation describes the current SightAdapt implementation and the canoni - [Product identity and brand usage](BRAND.md) — canonical name, `™` notation, descriptions, publisher attribution, website URL, and asset rules. - [Release naming and attribution](RELEASING.md) — public release-title, tag, artifact, website, publisher, and mark conventions. -- [Binary packaging standard](PACKAGING.md) — required legal files, redistribution notice, exact-version notices, archive validation and distribution-format rules. +- [Binary packaging standard](PACKAGING.md) — required legal files, redistribution notice, SBOM, exact-version notices, archive validation and distribution-format rules. - [Microsoft .NET redistribution analysis](legal/DOTNET-REDISTRIBUTION.md) — Microsoft-origin component inventory, applicable terms, package implementation, review triggers and professional-review gate. - [Exact-version .NET notice generation](legal/DOTNET-NOTICE-GENERATION.md) — pinned SDK/runtime inputs, official package verification, notice import, checksums and update triggers. +- [SBOM and dependency-license review](legal/SBOM-AND-LICENSE-REVIEW.md) — SPDX inventory, license policy, generated reports and release failure rules. - [Complete functionality](FEATURES.md) — user-visible behavior, profiles, application assignments, overlay scopes, switching, settings, safety, and limitations. - [Current architecture](ARCHITECTURE.md) — authorities, sources of truth, settings transaction, foreground switching, overlay lifecycle, geometry, UI boundaries, and failure behavior. - [Build and package a standalone EXE](BUILD.md) — step-by-step local self-contained Windows x64 publication and archive verification. diff --git a/docs/legal/SBOM-AND-LICENSE-REVIEW.md b/docs/legal/SBOM-AND-LICENSE-REVIEW.md new file mode 100644 index 00000000..d18c2e0c --- /dev/null +++ b/docs/legal/SBOM-AND-LICENSE-REVIEW.md @@ -0,0 +1,58 @@ +# SBOM and dependency-license review + +## Scope + +Every SightAdapt release candidate produces a machine-readable SPDX 2.3 software bill of materials and a machine-readable license-policy report. These files describe the published application container, exact .NET runtime packs, application/test/build dependencies and every file placed in the final publish directory. + +This process is supply-chain and release documentation. It does not change SightAdapt application behavior. + +## Authoritative inputs + +`tools/generate-sbom.ps1` reads: + +- `Directory.Build.props` for the product, SDK, runtime and RID versions; +- `src/SightAdapt/obj/project.assets.json` for the actual application restore graph; +- `DOTNET-NOTICE-METADATA.json` for exact runtime-pack and Microsoft source-package evidence; +- application and test project files for direct NuGet references; +- `.github/workflows/build.yml` for GitHub Actions build dependencies; +- `release/dependency-policy.json` for reviewed suppliers, licenses, versions, scopes and allow/deny/review decisions; +- the final publish directory for file names and SHA-256 checksums. + +A component not present in the policy is unresolved and fails the release step. A component version different from the reviewed version also fails. Denied or unreviewed license expressions fail before archive creation. + +## Generated files + +The generator writes these files at the package root: + +| File | Purpose | +|---|---| +| `SBOM.spdx.json` | SPDX 2.3 document containing packages, scopes, suppliers, versions, declared/concluded licenses, source references, file inventory, relationships and checksums | +| `LICENSE-REPORT.json` | License-policy result with allowed, denied and review lists plus the evaluated component inventory and failures | +| `DEPENDENCIES.md` | Human-readable summary generated from the same evaluated component list | + +The SBOM intentionally treats `SightAdapt.exe` as the single-file publication container. Runtime packs embedded by self-contained single-file publication are represented as package components and related to the SightAdapt package. Every separately shipped file is represented with its SHA-256 checksum. The SBOM excludes only itself from its file list because a file cannot contain a stable checksum of its own final bytes. + +## License policy + +`release/dependency-policy.json` is the review source of truth. It defines: + +- allowed license expressions; +- explicitly denied expressions; +- expressions requiring review; +- expected component versions or version sources; +- supplier and source information; +- whether a component is shipped, restore-only, test-only or build-only. + +Custom Microsoft `LicenseRef` expressions point to the exact Microsoft license, redistribution and third-party notice files included in the same package. They do not relicense Microsoft components under MIT. + +## Release behavior + +The maintained workflow runs SBOM generation after self-contained publication and exact-version .NET notice generation, but before ZIP creation. A failed component or license review prevents archive publication. `release/required-files.txt` requires the SBOM and report in every final distribution, so GitHub artifacts, manual ZIPs, installers, store packages, portable packages and mirrors must preserve them. + +The final ZIP retained for a release therefore contains the exact SBOM, license report and human-readable dependency inventory associated with the shipped bytes. + +## Maintenance + +Update the dependency policy when any package, runtime pack, SDK, GitHub Action, supplier, license or version changes. Do not add an unknown component with an assumed license. Review the authoritative package/repository terms, record the result in the policy and preserve any required notices. + +The SBOM is evidence of composition. It does not replace legal review of ambiguous, custom or conflicting terms. diff --git a/release/dependency-policy.json b/release/dependency-policy.json new file mode 100644 index 00000000..5b69e6b0 --- /dev/null +++ b/release/dependency-policy.json @@ -0,0 +1,146 @@ +{ + "schemaVersion": 1, + "allowedLicenseExpressions": [ + "MIT", + "LicenseRef-Microsoft-DotNet-Distribution", + "LicenseRef-Microsoft-Windows-SDK-Reference-Terms" + ], + "deniedLicenseExpressions": [ + "GPL-2.0-only", + "GPL-3.0-only", + "AGPL-3.0-only", + "CC-BY-NC-4.0", + "NOASSERTION", + "UNKNOWN" + ], + "components": { + "SightAdapt": { + "expectedVersionSource": "product", + "scope": "shipped", + "shipped": true, + "supplier": "Organization: KeyffMS / aiteracja.pl", + "license": "MIT", + "source": "https://github.com/KeyffMS/SightAdapt", + "purpose": "APPLICATION", + "expectedVersion": "" + }, + "Microsoft.NETCore.App.Runtime.win-x64": { + "expectedVersionSource": "runtime", + "scope": "shipped", + "shipped": true, + "supplier": "Organization: Microsoft Corporation", + "license": "LicenseRef-Microsoft-DotNet-Distribution", + "source": "https://github.com/dotnet/runtime", + "purpose": "LIBRARY", + "expectedVersion": "" + }, + "Microsoft.WindowsDesktop.App.Runtime.win-x64": { + "expectedVersionSource": "runtime", + "scope": "shipped", + "shipped": true, + "supplier": "Organization: Microsoft Corporation", + "license": "LicenseRef-Microsoft-DotNet-Distribution", + "source": "https://github.com/dotnet/windowsdesktop", + "purpose": "LIBRARY", + "expectedVersion": "" + }, + "Microsoft.AspNetCore.App.Runtime.win-x64": { + "expectedVersionSource": "runtime", + "scope": "restore-only", + "shipped": false, + "supplier": "Organization: Microsoft Corporation", + "license": "LicenseRef-Microsoft-DotNet-Distribution", + "source": "https://github.com/dotnet/aspnetcore", + "purpose": "LIBRARY", + "expectedVersion": "" + }, + "Microsoft.NET.Test.Sdk": { + "expectedVersion": "17.11.1", + "scope": "test", + "shipped": false, + "supplier": "Organization: Microsoft Corporation", + "license": "MIT", + "source": "https://github.com/microsoft/vstest", + "purpose": "OTHER", + "expectedVersionSource": "" + }, + "MSTest.TestAdapter": { + "expectedVersion": "3.6.4", + "scope": "test", + "shipped": false, + "supplier": "Organization: Microsoft Corporation", + "license": "MIT", + "source": "https://github.com/microsoft/testfx", + "purpose": "OTHER", + "expectedVersionSource": "" + }, + "MSTest.TestFramework": { + "expectedVersion": "3.6.4", + "scope": "test", + "shipped": false, + "supplier": "Organization: Microsoft Corporation", + "license": "MIT", + "source": "https://github.com/microsoft/testfx", + "purpose": "OTHER", + "expectedVersionSource": "" + }, + "actions/checkout": { + "expectedVersion": "v4", + "scope": "build", + "shipped": false, + "supplier": "Organization: GitHub, Inc.", + "license": "MIT", + "source": "https://github.com/actions/checkout", + "purpose": "OTHER", + "expectedVersionSource": "" + }, + "actions/setup-dotnet": { + "expectedVersion": "v4", + "scope": "build", + "shipped": false, + "supplier": "Organization: GitHub, Inc.", + "license": "MIT", + "source": "https://github.com/actions/setup-dotnet", + "purpose": "OTHER", + "expectedVersionSource": "" + }, + "actions/upload-artifact": { + "expectedVersion": "v4", + "scope": "build", + "shipped": false, + "supplier": "Organization: GitHub, Inc.", + "license": "MIT", + "source": "https://github.com/actions/upload-artifact", + "purpose": "OTHER", + "expectedVersionSource": "" + }, + "Microsoft .NET SDK": { + "expectedVersion": "", + "expectedVersionSource": "sdk", + "scope": "build", + "shipped": false, + "supplier": "Organization: Microsoft Corporation", + "license": "LicenseRef-Microsoft-DotNet-Distribution", + "source": "https://github.com/dotnet/sdk", + "purpose": "OTHER" + }, + "Microsoft.Windows.SDK.NET.Ref": { + "expectedVersion": "10.0.19041.56", + "expectedVersionSource": "", + "scope": "build-reference", + "shipped": false, + "supplier": "Organization: Microsoft Corporation", + "license": "LicenseRef-Microsoft-Windows-SDK-Reference-Terms", + "source": "https://www.nuget.org/packages/Microsoft.Windows.SDK.NET.Ref", + "purpose": "LIBRARY" + } + }, + "reviewLicenseExpressions": [ + "LGPL-2.1-only", + "LGPL-3.0-only", + "MPL-2.0", + "EPL-2.0", + "CDDL-1.0", + "LicenseRef-*" + ] +} diff --git a/release/required-files.txt b/release/required-files.txt index 74c414cd..cad12497 100644 --- a/release/required-files.txt +++ b/release/required-files.txt @@ -7,4 +7,6 @@ DOTNET-LICENSE-NOTICE.txt DOTNET-NOTICE-METADATA.json MICROSOFT-DOTNET-REDISTRIBUTION.txt DEPENDENCIES.md +SBOM.spdx.json +LICENSE-REPORT.json PRIVACY.md diff --git a/tools/generate-sbom.ps1 b/tools/generate-sbom.ps1 new file mode 100644 index 00000000..79005092 --- /dev/null +++ b/tools/generate-sbom.ps1 @@ -0,0 +1,427 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$PublishDirectory, + + [string]$AssetsPath, + + [string]$AppProjectPath, + + [string]$PolicyPath, + + [string]$TestProjectPath, + + [string]$WorkflowPath +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$root = Split-Path -Parent $PSScriptRoot +if ([string]::IsNullOrWhiteSpace($AssetsPath)) { + $AssetsPath = Join-Path $root 'src\SightAdapt\obj\project.assets.json' +} +if ([string]::IsNullOrWhiteSpace($AppProjectPath)) { + $AppProjectPath = Join-Path $root 'src\SightAdapt\SightAdapt.csproj' +} +if ([string]::IsNullOrWhiteSpace($PolicyPath)) { + $PolicyPath = Join-Path $root 'release\dependency-policy.json' +} +if ([string]::IsNullOrWhiteSpace($TestProjectPath)) { + $TestProjectPath = Join-Path $root 'tests\SightAdapt.Tests\SightAdapt.Tests.csproj' +} +if ([string]::IsNullOrWhiteSpace($WorkflowPath)) { + $WorkflowPath = Join-Path $root '.github\workflows\build.yml' +} + +$publish = [System.IO.Path]::GetFullPath($PublishDirectory) +if (-not [System.IO.Directory]::Exists($publish)) { + throw "Publish directory does not exist: $publish" +} + +$resolvedAssets = (Resolve-Path -LiteralPath $AssetsPath).Path +$resolvedAppProject = (Resolve-Path -LiteralPath $AppProjectPath).Path +$resolvedPolicy = (Resolve-Path -LiteralPath $PolicyPath).Path +$resolvedTestProject = (Resolve-Path -LiteralPath $TestProjectPath).Path +$resolvedWorkflow = (Resolve-Path -LiteralPath $WorkflowPath).Path + +[xml]$props = Get-Content -LiteralPath (Join-Path $root 'Directory.Build.props') +$group = $props.Project.PropertyGroup +$productVersion = [string]$group.SightAdaptProductVersion +$sdkVersion = [string]$group.SightAdaptDotNetSdkVersion +$runtimeVersion = [string]$group.SightAdaptDotNetRuntimeVersion +$rid = [string]$group.SightAdaptRuntimeIdentifier + +$noticeMetadataPath = Join-Path $publish 'DOTNET-NOTICE-METADATA.json' +if (-not [System.IO.File]::Exists($noticeMetadataPath)) { + throw 'DOTNET-NOTICE-METADATA.json must be generated before SBOM generation.' +} + +$noticeMetadata = Get-Content -LiteralPath $noticeMetadataPath -Raw | ConvertFrom-Json +if (-not [System.IO.File]::Exists((Join-Path $publish 'SightAdapt.exe'))) { + throw 'SightAdapt.exe is missing from the publish directory.' +} +$assets = Get-Content -LiteralPath $resolvedAssets -Raw | ConvertFrom-Json +$policy = Get-Content -LiteralPath $resolvedPolicy -Raw | ConvertFrom-Json +$allowedLicenses = @($policy.allowedLicenseExpressions) +$deniedLicenses = @($policy.deniedLicenseExpressions) +$reviewLicenses = @($policy.reviewLicenseExpressions) +$failures = [System.Collections.Generic.List[string]]::new() +$components = [System.Collections.Generic.List[object]]::new() +$seen = @{} + +function Get-PolicyEntry([string]$Name) { + $property = @( + $policy.components.PSObject.Properties | + Where-Object { [string]$_.Name -eq $Name } + ) | Select-Object -First 1 + if ($null -eq $property) { + return $null + } + return $property.Value +} + +function Add-Component( + [string]$Name, + [string]$Version, + [string]$DetectedScope) { + if ([string]::IsNullOrWhiteSpace($Name) -or + [string]::IsNullOrWhiteSpace($Version)) { + $failures.Add("A dependency has an empty name or version: '$Name' '$Version'.") + return + } + + $key = "$Name@$Version" + if ($seen.ContainsKey($key)) { + return + } + + $entry = Get-PolicyEntry $Name + if ($null -eq $entry) { + $failures.Add("Dependency '$Name@$Version' has no reviewed policy entry.") + return + } + + $expectedVersion = [string]$entry.expectedVersion + $versionSource = [string]$entry.expectedVersionSource + if ($versionSource -eq 'product') { + $expectedVersion = $productVersion + } + elseif ($versionSource -eq 'sdk') { + $expectedVersion = $sdkVersion + } + elseif ($versionSource -eq 'runtime') { + $expectedVersion = $runtimeVersion + } + + if (-not [string]::IsNullOrWhiteSpace($expectedVersion) -and + $Version -ne $expectedVersion) { + $failures.Add( + "Dependency '$Name' has version '$Version'; reviewed version is '$expectedVersion'.") + } + + $license = [string]$entry.license + if ($deniedLicenses -contains $license) { + $failures.Add("Dependency '$Name@$Version' uses denied license '$license'.") + } + elseif ($allowedLicenses -notcontains $license) { + $failures.Add( + "Dependency '$Name@$Version' uses unreviewed license '$license'.") + } + + $scope = [string]$entry.scope + if ([string]::IsNullOrWhiteSpace($scope)) { + $scope = $DetectedScope + } + + $component = [pscustomobject]@{ + name = $Name + version = $Version + scope = $scope + shipped = [bool]$entry.shipped + supplier = [string]$entry.supplier + license = $license + source = [string]$entry.source + purpose = [string]$entry.purpose + status = if (($allowedLicenses -contains $license) -and + ($Version -eq $expectedVersion -or + [string]::IsNullOrWhiteSpace($expectedVersion))) { + 'approved' + } + else { + 'failed' + } + } + $components.Add($component) + $seen[$key] = $true +} + +Add-Component 'SightAdapt' $productVersion 'shipped' +Add-Component 'Microsoft .NET SDK' $sdkVersion 'build' + +foreach ($runtimePackage in @($noticeMetadata.runtimePackages)) { + $parts = ([string]$runtimePackage) -split '/', 2 + if ($parts.Count -ne 2) { + $failures.Add("Invalid runtime package identity '$runtimePackage'.") + continue + } + Add-Component $parts[0] $parts[1] 'runtime' +} + +$assetsFramework = @( + $assets.project.frameworks.PSObject.Properties +) | Select-Object -First 1 +if ($null -eq $assetsFramework) { + $failures.Add('The application restore graph has no framework metadata.') +} +else { + foreach ($dependency in @($assetsFramework.Value.downloadDependencies)) { + $dependencyName = [string]$dependency.name + $versionRange = [string]$dependency.version + $versionMatch = [regex]::Match( + $versionRange, + '^\[([^,\]]+),\s*\1\]$') + if (-not $versionMatch.Success) { + $failures.Add( + "Restore dependency '$dependencyName' does not use an exact version range: '$versionRange'.") + continue + } + Add-Component $dependencyName $versionMatch.Groups[1].Value 'restore' + } +} + +[xml]$appProject = Get-Content -LiteralPath $resolvedAppProject +foreach ($reference in @($appProject.SelectNodes('/Project/ItemGroup/PackageReference'))) { + Add-Component ([string]$reference.Include) ([string]$reference.Version) 'runtime' +} + +[xml]$testProject = Get-Content -LiteralPath $resolvedTestProject +foreach ($reference in @($testProject.SelectNodes('/Project/ItemGroup/PackageReference'))) { + Add-Component ([string]$reference.Include) ([string]$reference.Version) 'test' +} + +$workflowText = Get-Content -LiteralPath $resolvedWorkflow -Raw +$actionMatches = [regex]::Matches( + $workflowText, + 'uses:\s+([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)@([^\s#]+)') +foreach ($match in $actionMatches) { + Add-Component ([string]$match.Groups[1].Value) ([string]$match.Groups[2].Value) 'build' +} + +$summaryLines = [System.Collections.Generic.List[string]]::new() +$summaryLines.Add('# SightAdapt dependency inventory') +$summaryLines.Add('') +$summaryLines.Add( + "Generated from the release dependency policy, the actual restore graph and exact .NET notice metadata for SightAdapt $productVersion ($rid).") +$summaryLines.Add('') +$summaryLines.Add('| Component | Version | Scope | Shipped | Supplier | License | Source |') +$summaryLines.Add('|---|---|---|---|---|---|---|') +foreach ($component in @($components | Sort-Object scope, name)) { + $source = if ([string]::IsNullOrWhiteSpace($component.source)) { + 'not recorded' + } + else { + $component.source + } + $summaryLines.Add( + "| $($component.name) | `$($component.version)` | $($component.scope) | $($component.shipped) | $($component.supplier) | `$($component.license)` | $source |") +} +$summaryLines.Add('') +$summaryLines.Add('`SBOM.spdx.json` contains the machine-readable component and file inventory. `LICENSE-REPORT.json` records the policy result. Build and test dependencies are not included in the binary package unless `shipped` is `true`.') +$summaryText = $summaryLines -join [Environment]::NewLine +[System.IO.File]::WriteAllText( + (Join-Path $publish 'DEPENDENCIES.md'), + $summaryText + [Environment]::NewLine, + [System.Text.UTF8Encoding]::new($false)) + +$created = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ') +$report = [ordered]@{ + schemaVersion = 1 + generatedAtUtc = $created + productVersion = $productVersion + sdkVersion = $sdkVersion + runtimeVersion = $runtimeVersion + runtimeIdentifier = $rid + policy = 'release/dependency-policy.json' + result = if ($failures.Count -eq 0) { 'pass' } else { 'fail' } + allowedLicenseExpressions = $allowedLicenses + deniedLicenseExpressions = $deniedLicenses + components = @($components | Sort-Object scope, name) + reviewLicenseExpressions = $reviewLicenses + failures = @($failures) +} +[System.IO.File]::WriteAllText( + (Join-Path $publish 'LICENSE-REPORT.json'), + ($report | ConvertTo-Json -Depth 8) + [Environment]::NewLine, + [System.Text.UTF8Encoding]::new($false)) + +function Get-SpdxId([string]$Prefix, [string]$Value) { + $safe = [regex]::Replace($Value, '[^A-Za-z0-9.-]', '-') + return "SPDXRef-$Prefix-$safe" +} + +$files = [System.Collections.Generic.List[object]]::new() +$fileRelationships = [System.Collections.Generic.List[object]]::new() +$sightAdaptId = Get-SpdxId 'Package' 'SightAdapt' +foreach ($file in @(Get-ChildItem -LiteralPath $publish -File -Recurse | Sort-Object FullName)) { + $relative = [System.IO.Path]::GetRelativePath($publish, $file.FullName).Replace('\', '/') + if ($relative -eq 'SBOM.spdx.json') { + continue + } + $fileId = Get-SpdxId 'File' $relative + $hash = (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash + $files.Add([ordered]@{ + fileName = "./$relative" + SPDXID = $fileId + checksums = @( + [ordered]@{ + algorithm = 'SHA256' + checksumValue = $hash + } + ) + licenseConcluded = 'NOASSERTION' + copyrightText = 'NOASSERTION' + }) + $fileRelationships.Add([ordered]@{ + spdxElementId = $sightAdaptId + relationshipType = 'CONTAINS' + relatedSpdxElement = $fileId + }) +} + +$packages = [System.Collections.Generic.List[object]]::new() +$relationships = [System.Collections.Generic.List[object]]::new() +foreach ($component in @($components | Sort-Object name)) { + $packageId = Get-SpdxId 'Package' $component.name + $package = [ordered]@{ + name = $component.name + SPDXID = $packageId + versionInfo = $component.version + supplier = $component.supplier + downloadLocation = if ([string]::IsNullOrWhiteSpace($component.source)) { + 'NOASSERTION' + } + else { + $component.source + } + filesAnalyzed = $false + licenseConcluded = $component.license + licenseDeclared = $component.license + copyrightText = 'NOASSERTION' + primaryPackagePurpose = $component.purpose + comment = "Scope: $($component.scope); shipped: $($component.shipped)." + } + + if ($component.name -eq 'SightAdapt') { + $exePath = Join-Path $publish 'SightAdapt.exe' + if ([System.IO.File]::Exists($exePath)) { + $package['checksums'] = @( + [ordered]@{ + algorithm = 'SHA256' + checksumValue = (Get-FileHash -LiteralPath $exePath -Algorithm SHA256).Hash + } + ) + } + } + elseif ($component.name -eq 'Microsoft .NET SDK' -and + [string]$noticeMetadata.source.packageSha512 -match '^[0-9A-Fa-f]{128}$') { + $package['checksums'] = @( + [ordered]@{ + algorithm = 'SHA512' + checksumValue = [string]$noticeMetadata.source.packageSha512 + } + ) + } + + if ($component.name -like 'actions/*') { + $package['externalRefs'] = @( + [ordered]@{ + referenceCategory = 'PACKAGE-MANAGER' + referenceType = 'purl' + referenceLocator = "pkg:github/$($component.name)@$($component.version)" + } + ) + } + elseif ($component.name -ne 'SightAdapt' -and + $component.name -ne 'Microsoft .NET SDK') { + $package['externalRefs'] = @( + [ordered]@{ + referenceCategory = 'PACKAGE-MANAGER' + referenceType = 'purl' + referenceLocator = "pkg:nuget/$($component.name)@$($component.version)" + } + ) + } + + $packages.Add($package) + if ($component.name -eq 'SightAdapt') { + $relationships.Add([ordered]@{ + spdxElementId = 'SPDXRef-DOCUMENT' + relationshipType = 'DESCRIBES' + relatedSpdxElement = $packageId + }) + } + else { + $relationships.Add([ordered]@{ + spdxElementId = $sightAdaptId + relationshipType = 'DEPENDS_ON' + relatedSpdxElement = $packageId + comment = "Dependency scope: $($component.scope); shipped: $($component.shipped)." + }) + } +} +foreach ($relationship in $fileRelationships) { + $relationships.Add($relationship) +} + +$namespaceId = [Guid]::NewGuid().ToString('N') +$sbom = [ordered]@{ + spdxVersion = 'SPDX-2.3' + dataLicense = 'CC0-1.0' + SPDXID = 'SPDXRef-DOCUMENT' + name = "SightAdapt-$productVersion-$rid" + documentNamespace = "https://github.com/KeyffMS/SightAdapt/sbom/$productVersion/$namespaceId" + creationInfo = [ordered]@{ + created = $created + creators = @( + 'Tool: SightAdapt tools/generate-sbom.ps1', + 'Organization: KeyffMS / aiteracja.pl' + ) + } + documentDescribes = @($sightAdaptId) + packages = @($packages) + files = @($files) + relationships = @($relationships) + hasExtractedLicensingInfos = @( + [ordered]@{ + licenseId = 'LicenseRef-Microsoft-DotNet-Distribution' + name = 'Microsoft .NET distribution terms and component notices' + extractedText = 'See DOTNET-LICENSE-NOTICE.txt, THIRD-PARTY-NOTICES.txt, MICROSOFT-DOTNET-REDISTRIBUTION.txt and DOTNET-NOTICE-METADATA.json in the same package.' + seeAlsos = @( + 'https://dotnet.microsoft.com/en-us/dotnet_library_license.htm', + 'https://github.com/dotnet/core/blob/main/license-information.md' + ) + } + [ordered]@{ + licenseId = 'LicenseRef-Microsoft-Windows-SDK-Reference-Terms' + name = 'Microsoft Windows SDK .NET reference package terms' + extractedText = 'Build-time reference package only; not shipped. Review the package terms and metadata at the recorded source URL.' + seeAlsos = @( + 'https://www.nuget.org/packages/Microsoft.Windows.SDK.NET.Ref' + ) + } + ) +} + +[System.IO.File]::WriteAllText( + (Join-Path $publish 'SBOM.spdx.json'), + ($sbom | ConvertTo-Json -Depth 12) + [Environment]::NewLine, + [System.Text.UTF8Encoding]::new($false)) + +if ($failures.Count -gt 0) { + $details = $failures | ForEach-Object { " - $_" } + throw "Dependency license review failed:`n$($details -join "`n")" +} + +Write-Host "Generated SPDX 2.3 SBOM and approved license report for $($components.Count) components and $($files.Count) packaged files."