Record NativeAOT executables and bundled macOS binaries in package SBOMs - #6
Merged
Conversation
AddPackageContentComponents selected shipped binaries by extension (.dll/.so/.dylib/.wasm/.node/.a), which misses exactly the binary that matters most for a NativeAOT tool package: the application itself. It ships as app.exe on Windows and extensionless on Linux and inside the macOS .app bundle, so the package recorded no component and no hash for its primary deliverable, while the compositions entry still claimed to be a complete assembly. It matters more here than for a managed package, not less - the dependencies are statically linked into the image, so a consumer cannot inspect the binary to find them. Shipped binaries are now also classified by executable-format magic (PE, ELF, Mach-O including the universal wrapper), which spots the extensionless form that no extension list can. .exe joins the extension list as a fast path. The scan also descends into nested .zip entries. The macOS tool package ships its .app bundle re-zipped inside the .nupkg to preserve the bundle structure and the executable permission bits, so every binary it ships lives one level down; previously that package recorded no shipped binaries at all while the Windows and Linux packages recorded theirs. Names now come from BinaryName rather than Path.GetFileNameWithoutExtension, which would truncate a dotted extensionless executable - AvaloniaUI.DeveloperTools was recorded as "AvaloniaUI" - by treating the last dotted segment as an extension. Only recognised binary extensions are stripped. Verified against a synthetic package covering all three platform layouts: the zipped macOS bundle, a Windows .exe, an extensionless dotted Linux executable and a universal binary are all recorded, while ref/, the nuspec, README, icon and directory entries are still skipped.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR improves SBOM completeness for NuGet tool packages by ensuring shipped binaries are detected even when they are extensionless NativeAOT executables and when macOS .app bundles are stored inside nested .zip entries within the .nupkg.
Changes:
- Enumerate shipped binaries by executable-format magic (PE/ELF/Mach-O) in addition to extension matching, and include
.exeas a binary extension. - Descend into nested
.zipentries to detect binaries inside re-zipped macOS.appbundles. - Derive component names via
BinaryName, which strips only recognized binary extensions to avoid truncating dotted extensionless executable names.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
nuke/SbomGenerator.cs:745
- If a nested .zip entry is corrupt/invalid,
new ZipArchive(...)will throwInvalidDataExceptionwhose default message typically doesn’t identify which package entry failed. This makes SBOM failures hard to diagnose; consider catching and rethrowing withpathincluded (still failing the build, just with better context).
if (Path.GetExtension(entry.FullName).Equals(".zip", StringComparison.OrdinalIgnoreCase))
{
// Buffered into memory because ZipArchive needs a seekable stream, which the
// deflate stream of an entry is not.
using var nestedBytes = new MemoryStream(ReadEntry(entry));
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
AddPackageContentComponentscross-checks what a package actually ships against the components derived from its dependency graph, hashing each shipped binary. It selected those binaries by extension (.dll/.so/.dylib/.wasm/.node/.a), which misses exactly the binary that matters most for a NativeAOT tool package — the application itself.Raised in review of AvaloniaUI.DeveloperSuite#1927: "Do we need SBOM for native tools? I.e. Parcel/DevTools packages are NAOT compiled executables."
The answer is yes — and more so than for a managed package, not less: the dependencies are statically linked into the image, so a consumer cannot inspect the binary to find them. But the question exposed that the evidence was missing.
What was wrong
1. The NativeAOT executable was never recorded. It ships as
app.exeon Windows and extensionless on Linux and inside the macOS.appbundle. No component, no hash — while thecompositionsentry still declaredaggregate: "complete", claiming the enumeration was exhaustive.2. The macOS package recorded no shipped binaries at all. Its
.appbundle ships re-zipped inside the.nupkg(that is what preserves the bundle structure and the executable permission bits), so every binary it delivers lives one level down. Windows and Linux packages recorded theirs; macOS recorded nothing.3. A latent naming bug.
Path.GetFileNameWithoutExtensiontreats the last dotted segment as an extension, so an extensionlessAvaloniaUI.DeveloperToolswould have been recorded asAvaloniaUI.What changed
.exejoins the extension list as a fast path. Only the first 4 bytes of an entry are inflated, so rejecting non-binary entries stays cheap.EnumerateShippedBinariesdescends into nested.zipentries, reaching the macOS bundle's contents. Theref/exclusion is now applied only at the package root, since aref/directory inside a bundled app is that app's own layout.BinaryNamethat strips only recognised binary extensions.Verification
No test project exists here, so this was verified with a harness compiling this file and exercising the scan against a synthetic package covering all three platform layouts.
Recorded, as expected:
…/AvaloniaUI.DeveloperTools.zip/AvaloniaUI DeveloperTools.app/Contents/MacOS/AvaloniaUI.DeveloperTools…/AvaloniaUI.DeveloperTools.zip/…/MacOS/libHarfBuzzSharp.dylib…/win/AvaloniaUI.DeveloperTools.exe…/linux/AvaloniaUI.DeveloperTools…/linux/AvaloniaUI.DeveloperTools.Universallib/net8.0/AvaloniaUI.Core.ToolRunner.dllruntimes/linux-x64/native/libSkiaSharp.soStill skipped:
ref/net8.0/*.dll, the.nuspec,README.md,icon.png, and directory entries.BinaryNameverified on: extensionless dotted (kept whole),.exe/.so/.dll(stripped), and a dotted name with an unrecognised extension (kept whole).No regression on the managed path:
PackLibraries_DiagnosticsSupportrun end to end in AvaloniaUI.DeveloperSuite against this branch — 26 components, shipped-binary hash recorded,compositionsintact, unchanged from before.Not verified: a real NativeAOT
.nupkg. Building one needsAVALONIA_TOOLS_LICENSE_KEYand a full Parcel packaging run, so the AOT path rests on the synthetic harness rather than a real package. Worth a look at the SBOMs the next DevTools/Parcel release publishes.Note
This file is kept in sync with Avalonia's
nukebuild/SbomGenerator.cs— this change should be mirrored there.🤖 Generated with Claude Code
https://claude.ai/code/session_018E1GHyH9fht5jwsWxh1zcq