Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **NuGet license file support** (#1011) — packages declaring `<license type="file">` now have their license file embedded as base64-encoded text in the BOM when `--include-license-text` is specified; without the flag the license is still detected but not embedded

### Fixed

- **Suppress `aka.ms/deprecateLicenseUrl` stub URL** (#1011) — NuGet auto-injects `https://aka.ms/deprecateLicenseUrl` into `<licenseUrl>` for packages packed with `<license type="file">`; this URL is now correctly ignored rather than being emitted as a license entry in the BOM (see [NuGet spec](https://github.com/NuGet/Home/wiki/Packaging-License-within-the-nupkg))
- **Fix null-URL license stub** (#1011) — packages with no `<licenseUrl>` no longer produce a spurious `License { Name="Unknown - See URL", Url=null }` node in the BOM
- **Fix `UNLICENSED` emitted as SPDX id** (#1004, fixes #915) — `UNLICENSED` is a NuGet-specific token that is not a valid SPDX identifier; it is now emitted as `license.name` instead of `license.id` to keep BOM output valid

## [6.1.1] - 2026-04-08

### Fixed
Expand Down
6 changes: 6 additions & 0 deletions CycloneDX.E2ETests/Infrastructure/CycloneDxRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ private static string BuildArgs(string projectOrSolutionPath, string outputDir,
sb.Append(" --disable-hash-computation");
}

if (options.IncludeLicenseText)
{
sb.Append(" --include-license-text");
}

if (options.NuGetFeedUrl != null)
{
sb.Append($" --url \"{options.NuGetFeedUrl}\"");
Expand Down Expand Up @@ -183,6 +188,7 @@ internal sealed class ToolRunOptions
public bool Recursive { get; set; }
public bool NoSerialNumber { get; set; }
public bool DisableHashComputation { get; set; }
public bool IncludeLicenseText { get; set; }
public string NuGetFeedUrl { get; set; }
public string SetName { get; set; }
public string SetVersion { get; set; }
Expand Down
37 changes: 37 additions & 0 deletions CycloneDX.E2ETests/Infrastructure/NuGetServerFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,43 @@ await PushPackageAsync(NupkgBuilder.Build(
"TestPkg.Consumer", "1.0.0",
dependencies: new[] { new NupkgDependency("TestPkg.Shared", "[1.0.0, 1.0.0]") }
)).ConfigureAwait(false);

// TestPkg.SpdxLicense 1.0.0 — declares license via SPDX expression
await PushPackageAsync(NupkgBuilder.Build(
"TestPkg.SpdxLicense", "1.0.0",
license: NupkgLicense.Spdx("MIT")
)).ConfigureAwait(false);

// TestPkg.FileLicense 1.0.0 — declares license via embedded file (LICENSE.txt)
await PushPackageAsync(NupkgBuilder.Build(
"TestPkg.FileLicense", "1.0.0",
license: NupkgLicense.File("LICENSE.txt", System.Text.Encoding.UTF8.GetBytes(
"MIT License\n\nCopyright (c) CycloneDX E2E Tests\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software."))
)).ConfigureAwait(false);

// TestPkg.FileLicenseMd 1.0.0 — license file with .md extension
await PushPackageAsync(NupkgBuilder.Build(
"TestPkg.FileLicenseMd", "1.0.0",
license: NupkgLicense.File("LICENSE.md", System.Text.Encoding.UTF8.GetBytes(
"# MIT License\n\nCopyright (c) CycloneDX E2E Tests"))
)).ConfigureAwait(false);

// TestPkg.UrlLicense 1.0.0 — declares license via deprecated <licenseUrl>
await PushPackageAsync(NupkgBuilder.Build(
"TestPkg.UrlLicense", "1.0.0",
license: NupkgLicense.LicenseUrl("https://opensource.org/licenses/MIT")
)).ConfigureAwait(false);

// TestPkg.NoLicense 1.0.0 — no license metadata at all
await PushPackageAsync(NupkgBuilder.Build("TestPkg.NoLicense", "1.0.0")).ConfigureAwait(false);

// TestPkg.FileLicenseDeprecatedUrl 1.0.0 — <license type="file"> with the aka.ms stub
// URL that NuGet auto-inserts when packing. Phase 4 must NOT fall back to this URL.
await PushPackageAsync(NupkgBuilder.Build(
"TestPkg.FileLicenseDeprecatedUrl", "1.0.0",
license: NupkgLicense.FileWithDeprecatedUrl("LICENSE.txt", System.Text.Encoding.UTF8.GetBytes(
"MIT License\n\nCopyright (c) CycloneDX E2E Tests\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software."))
)).ConfigureAwait(false);
}

public async ValueTask DisposeAsync()
Expand Down
66 changes: 63 additions & 3 deletions CycloneDX.E2ETests/Infrastructure/NupkgBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ public static byte[] Build(
string id,
string version,
string description = null,
NupkgDependency[] dependencies = null)
NupkgDependency[] dependencies = null,
NupkgLicense license = null)
{
description ??= $"Test package {id}";

var nuspec = BuildNuspec(id, version, description, dependencies);
var nuspec = BuildNuspec(id, version, description, dependencies, license);

using var ms = new MemoryStream();
using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
Expand All @@ -53,6 +54,15 @@ public static byte[] Build(
dllStream.Write(placeholder, 0, placeholder.Length);
}

// If a file license is specified, embed the license file in the .nupkg
if ((license?.Type == NupkgLicenseType.File || license?.Type == NupkgLicenseType.FileWithDeprecatedUrl)
&& license.FileContent != null)
{
var licenseEntry = archive.CreateEntry(license.FilePath, CompressionLevel.Optimal);
using var licenseStream = licenseEntry.Open();
licenseStream.Write(license.FileContent, 0, license.FileContent.Length);
}

// [Content_Types].xml
var contentTypesEntry = archive.CreateEntry("[Content_Types].xml", CompressionLevel.Optimal);
using (var writer = new StreamWriter(contentTypesEntry.Open(), Encoding.UTF8))
Expand All @@ -66,7 +76,8 @@ private static string BuildNuspec(
string id,
string version,
string description,
NupkgDependency[] dependencies)
NupkgDependency[] dependencies,
NupkgLicense license)
{
var sb = new StringBuilder();
sb.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
Expand All @@ -77,6 +88,28 @@ private static string BuildNuspec(
sb.AppendLine(" <authors>CycloneDX E2E Tests</authors>");
sb.AppendLine($" <description>{description}</description>");

if (license != null)
{
switch (license.Type)
{
case NupkgLicenseType.Expression:
sb.AppendLine($" <license type=\"expression\">{license.Expression}</license>");
break;
case NupkgLicenseType.File:
sb.AppendLine($" <license type=\"file\">{license.FilePath}</license>");
break;
case NupkgLicenseType.FileWithDeprecatedUrl:
// Mirrors what `dotnet pack` does: emits both <license type="file"> and
// the NuGet deprecation stub URL so consumers can test the filtering logic.
sb.AppendLine($" <license type=\"file\">{license.FilePath}</license>");
sb.AppendLine($" <licenseUrl>https://aka.ms/deprecateLicenseUrl</licenseUrl>");
break;
case NupkgLicenseType.Url:
sb.AppendLine($" <licenseUrl>{license.Url}</licenseUrl>");
break;
}
}

if (dependencies != null && dependencies.Length > 0)
{
sb.AppendLine(" <dependencies>");
Expand Down Expand Up @@ -116,4 +149,31 @@ public NupkgDependency(string id, string version)
Version = version;
}
}

internal enum NupkgLicenseType { Expression, File, FileWithDeprecatedUrl, Url }

internal sealed class NupkgLicense
{
public NupkgLicenseType Type { get; private set; }
public string Expression { get; private set; }
public string FilePath { get; private set; }
public byte[] FileContent { get; private set; }
public string Url { get; private set; }

public static NupkgLicense Spdx(string expression) =>
new NupkgLicense { Type = NupkgLicenseType.Expression, Expression = expression };

public static NupkgLicense File(string path, byte[] content) =>
new NupkgLicense { Type = NupkgLicenseType.File, FilePath = path, FileContent = content };

/// <summary>
/// Mirrors real NuGet pack output: <c>&lt;license type="file"&gt;</c> plus the auto-inserted
/// <c>&lt;licenseUrl&gt;https://aka.ms/deprecateLicenseUrl&lt;/licenseUrl&gt;</c> stub.
/// </summary>
public static NupkgLicense FileWithDeprecatedUrl(string path, byte[] content) =>
new NupkgLicense { Type = NupkgLicenseType.FileWithDeprecatedUrl, FilePath = path, FileContent = content };

public static NupkgLicense LicenseUrl(string url) =>
new NupkgLicense { Type = NupkgLicenseType.Url, Url = url };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,6 @@
<hashes>
<hash alg="SHA-512">{scrubbed-hash}</hash>
</hashes>
<licenses>
<license>
<name>Unknown - See URL</name>
</license>
</licenses>
<purl>pkg:nuget/TestPkg.A@1.0.0</purl>
</component>
</components>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<bom xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" version="1" xmlns="http://cyclonedx.org/schema/bom/1.7">
<metadata>
<timestamp>{scrubbed-timestamp}</timestamp>
<tools>
<components>
<component type="application">
<authors>
<author>
<name>CycloneDX</name>
</author>
</authors>
<name>CycloneDX module for .NET</name>
<version>{scrubbed-version}</version>
<externalReferences>
<reference type="website">
<url>https://github.com/CycloneDX/cyclonedx-dotnet</url>
</reference>
</externalReferences>
</component>
</components>
</tools>
<component type="application" bom-ref="LicenseSnapshotFileFlagOnSln@0.0.0">
<name>LicenseSnapshotFileFlagOnSln</name>
<version>{scrubbed-version}</version>
</component>
</metadata>
<components>
<component type="library" bom-ref="pkg:nuget/TestPkg.FileLicense@1.0.0">
<authors>
<author>
<name>CycloneDX E2E Tests</name>
</author>
</authors>
<name>TestPkg.FileLicense</name>
<version>{scrubbed-version}</version>
<description>Test package TestPkg.FileLicense</description>
<scope>required</scope>
<hashes>
<hash alg="SHA-512">{scrubbed-hash}</hash>
</hashes>
<licenses>
<license>
<name>TestPkg.FileLicense License</name>
<text content-type="text/plain" encoding="base64">TUlUIExpY2Vuc2UKCkNvcHlyaWdodCAoYykgQ3ljbG9uZURYIEUyRSBUZXN0cwoKUGVybWlzc2lvbiBpcyBoZXJlYnkgZ3JhbnRlZCwgZnJlZSBvZiBjaGFyZ2UsIHRvIGFueSBwZXJzb24gb2J0YWluaW5nIGEgY29weSBvZiB0aGlzIHNvZnR3YXJlLg==</text>
</license>
</licenses>
<purl>pkg:nuget/TestPkg.FileLicense@1.0.0</purl>
</component>
</components>
<dependencies>
<dependency ref="LicenseSnapshotFileFlagOnSln@0.0.0">
<dependency ref="pkg:nuget/TestPkg.FileLicense@1.0.0" />
</dependency>
<dependency ref="pkg:nuget/TestPkg.FileLicense@1.0.0" />
</dependencies>
</bom>
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<bom xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" version="1" xmlns="http://cyclonedx.org/schema/bom/1.7">
<metadata>
<timestamp>{scrubbed-timestamp}</timestamp>
<tools>
<components>
<component type="application">
<authors>
<author>
<name>CycloneDX</name>
</author>
</authors>
<name>CycloneDX module for .NET</name>
<version>{scrubbed-version}</version>
<externalReferences>
<reference type="website">
<url>https://github.com/CycloneDX/cyclonedx-dotnet</url>
</reference>
</externalReferences>
</component>
</components>
</tools>
<component type="application" bom-ref="LicenseSnapshotSpdxSln@0.0.0">
<name>LicenseSnapshotSpdxSln</name>
<version>{scrubbed-version}</version>
</component>
</metadata>
<components>
<component type="library" bom-ref="pkg:nuget/TestPkg.SpdxLicense@1.0.0">
<authors>
<author>
<name>CycloneDX E2E Tests</name>
</author>
</authors>
<name>TestPkg.SpdxLicense</name>
<version>{scrubbed-version}</version>
<description>Test package TestPkg.SpdxLicense</description>
<scope>required</scope>
<hashes>
<hash alg="SHA-512">{scrubbed-hash}</hash>
</hashes>
<licenses>
<license>
<id>MIT</id>
</license>
</licenses>
<purl>pkg:nuget/TestPkg.SpdxLicense@1.0.0</purl>
</component>
</components>
<dependencies>
<dependency ref="LicenseSnapshotSpdxSln@0.0.0">
<dependency ref="pkg:nuget/TestPkg.SpdxLicense@1.0.0" />
</dependency>
<dependency ref="pkg:nuget/TestPkg.SpdxLicense@1.0.0" />
</dependencies>
</bom>
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<bom xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" version="1" xmlns="http://cyclonedx.org/schema/bom/1.7">
<metadata>
<timestamp>{scrubbed-timestamp}</timestamp>
<tools>
<components>
<component type="application">
<authors>
<author>
<name>CycloneDX</name>
</author>
</authors>
<name>CycloneDX module for .NET</name>
<version>{scrubbed-version}</version>
<externalReferences>
<reference type="website">
<url>https://github.com/CycloneDX/cyclonedx-dotnet</url>
</reference>
</externalReferences>
</component>
</components>
</tools>
<component type="application" bom-ref="LicenseSnapshotUrlSln@0.0.0">
<name>LicenseSnapshotUrlSln</name>
<version>{scrubbed-version}</version>
</component>
</metadata>
<components>
<component type="library" bom-ref="pkg:nuget/TestPkg.UrlLicense@1.0.0">
<authors>
<author>
<name>CycloneDX E2E Tests</name>
</author>
</authors>
<name>TestPkg.UrlLicense</name>
<version>{scrubbed-version}</version>
<description>Test package TestPkg.UrlLicense</description>
<scope>required</scope>
<hashes>
<hash alg="SHA-512">{scrubbed-hash}</hash>
</hashes>
<licenses>
<license>
<name>Unknown - See URL</name>
<url>https://opensource.org/licenses/MIT</url>
</license>
</licenses>
<purl>pkg:nuget/TestPkg.UrlLicense@1.0.0</purl>
</component>
</components>
<dependencies>
<dependency ref="LicenseSnapshotUrlSln@0.0.0">
<dependency ref="pkg:nuget/TestPkg.UrlLicense@1.0.0" />
</dependency>
<dependency ref="pkg:nuget/TestPkg.UrlLicense@1.0.0" />
</dependencies>
</bom>
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,6 @@
<hashes>
<hash alg="SHA-512">{scrubbed-hash}</hash>
</hashes>
<licenses>
<license>
<name>Unknown - See URL</name>
</license>
</licenses>
<purl>pkg:nuget/TestPkg.A@1.0.0</purl>
</component>
</components>
Expand All @@ -53,4 +48,4 @@
<dependency ref="pkg:nuget/TestPkg.A@1.0.0" />
</dependency>
</dependencies>
</bom>
</bom>
Loading
Loading