Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
fbe7db6
Add support for NuGet license files
mus65 Mar 1, 2026
129f6f2
fix typo
mus65 Mar 14, 2026
c1c19f6
build(deps): bump actions/checkout from 6.0.1 to 6.0.2 (#1008)
dependabot[bot] Mar 1, 2026
ba9a9b3
build(deps): bump actions/setup-dotnet from 5.0.1 to 5.1.0 (#1003)
dependabot[bot] Mar 1, 2026
d46dc61
fix: use case-insensitive comparison when pruning unresolved transiti…
mtsfoni Mar 1, 2026
74b59ef
Fix/metadata import overrides (#1041)
mtsfoni Mar 1, 2026
d6ebdfc
build(deps): upgrade CycloneDX.Core from 10.0.1 to 11.0.0 (#1042)
mtsfoni Mar 1, 2026
cbdca24
fix: use tools/components instead of deprecated tools/tool in BOM met…
mtsfoni Mar 1, 2026
03a9e96
build(deps): bump actions/setup-dotnet from 5.0.1 to 5.2.0 (#1052)
dependabot[bot] Mar 7, 2026
94270ba
Use trusted publishing for the .NET tool package (#1054)
coderpatros Mar 7, 2026
4d75d0d
build(deps): bump actions/checkout from 6.0.1 to 6.0.2 (#1045)
dependabot[bot] Mar 7, 2026
96dcb07
Fixed resolving of project name for classis .NET framework (#1051)
dridders Mar 7, 2026
81d3460
test: verify AssemblyName resolution for classic .NET Framework proje…
mtsfoni Mar 7, 2026
431e259
build(deps): upgrade CycloneDX.Core from 11.0.0 to 12.0.1 (#1055)
mtsfoni Mar 7, 2026
9418097
chore: bump version & update changelog
mtsfoni Mar 18, 2026
5f54d2d
Revert "Use trusted publishing for the .NET tool package (#1054)"
mtsfoni Mar 18, 2026
7bc7d0a
Reapply "Use trusted publishing for the .NET tool package (#1054)"
coderpatros Mar 19, 2026
02977cb
WIP: gate license file embedding behind --include-license-text flag
mtsfoni Apr 8, 2026
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
16 changes: 8 additions & 8 deletions .github/workflows/dotnetcore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup dotnet
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: |
9.x
Expand Down Expand Up @@ -51,17 +51,17 @@ jobs:
timeout-minutes: 30

steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup dotnet 10
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: '10.x'
- name: Setup dotnet 9
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: '9.x'
- name: Setup dotnet 8
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: '8.x'
- name: Tests
Expand Down Expand Up @@ -97,9 +97,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup dotnet 10
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: '10.x'
- name: Locked restore
Expand Down
14 changes: 11 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write # needed GitHub OIDC token issuance
timeout-minutes: 30
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup dotnet
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: |
9.x
Expand Down Expand Up @@ -75,11 +76,18 @@ jobs:
- name: Generate JSON SBOM
run: docker run --rm --user $(id -u):$(id -g) -v ${GITHUB_WORKSPACE}:/usr/src/project cyclonedx/cyclonedx-dotnet:${{ steps.package_release.outputs.version }} /usr/src/project/CycloneDX.sln --output-format json -o /usr/src/project

# Get a short-lived NuGet API key
- name: NuGet login (OIDC → temp API key)
uses: NuGet/login@v1
id: login
with:
user: ${{ secrets.NUGET_USER }}

- name: Publish package to NuGet
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
run: |
dotnet nuget push --source https://api.nuget.org/v3/index.json --api-key "$NUGET_API_KEY" ${{ steps.package_release.outputs.package_filename }}
dotnet nuget push --source https://api.nuget.org/v3/index.json --api-key ${{steps.login.outputs.NUGET_API_KEY}} ${{ steps.package_release.outputs.package_filename }}

- name: Publish Docker image to Docker Hub
env:
Expand Down
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [6.1.0]

### Added

- **CycloneDX spec 1.7 support** — upgraded CycloneDX.Core from 11.0.0 to 12.0.1; generated BOMs now use the `bom/1.7` schema namespace
- **Allow credentials via environment variables** (#1036) — NuGet feed credentials can now be passed through environment variables
- **Allow exclude filter without version specifier** (#1014) — the `--exclude` filter no longer requires a version to be specified
- **Recursive scan warning** (#1037) — a warning is now emitted when scanning project references recursively to make the behavior more visible
- **End-to-end test suite** (#1032) — added E2E tests using Testcontainers and Verify snapshots

### Fixed

- **Fix project name resolution for classic .NET Framework projects** (#1051) — correctly resolve `AssemblyName` in projects using the default XML namespace
- **Fix case-insensitive comparison when pruning transitive deps** (#1025, #1040) — package names are now compared case-insensitively when removing unresolved transitive dependencies
- **Fix metadata import overrides** (#1041) — metadata values imported from project properties are no longer incorrectly overridden
- **Use `tools/components` instead of deprecated `tools/tool`** (#1043) — BOM metadata now uses the non-deprecated CycloneDX structure for recording tool information
- **Validate GitHub API redirect destination** (#1030) — redirect URLs from the GitHub API are now validated before following

### Security

- **Sanitize untrusted URL inputs from NuGet feed metadata** (#1033) — URLs from NuGet package metadata are now sanitized before use
- **Rootless container** (#1035) — Docker image now runs as a non-root user by default
- **Trusted publishing for .NET tool package** (#1054) — NuGet package publishing now uses trusted publishing

### Changed

- **Upgrade CycloneDX.Core from 10.0.1 to 12.0.1** (#1042) — via intermediate upgrade to 11.0.0; enables CycloneDX spec 1.7 output
- **Dependency updates**
- actions/checkout: 6.0.1 → 6.0.2 (#1008, #1045)
- actions/setup-dotnet: 5.0.1 → 5.2.0 (#1003, #1052)
- actions/upload-artifact: 5.0.0 → 7.0.0 (#1031)

### Documentation

- Add security trust model (#1029)
- Move threat model and add architecture reference (#1034)
- Link NuGet and Docker Hub in README (#1019)
- Streamline README shields and links (#1018)
- Fix CI link in README (#1015)

## [6.0.0] - 2026-02-08

> **⚠️ WARNING: This is a MAJOR release with breaking changes.**
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 @@ -112,6 +112,43 @@ await PushPackageAsync(NupkgBuilder.Build(

// TestPkg.Transitive 1.0.0 — used only as a transitive dep
await PushPackageAsync(NupkgBuilder.Build("TestPkg.Transitive", "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 };
}
}
29 changes: 24 additions & 5 deletions CycloneDX.E2ETests/Infrastructure/VerifyConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,14 @@ public static class VerifyConfig
[ModuleInitializer]
public static void Initialize()
{
// Auto-accept snapshots when they don't exist yet (first run).
// Set VERIFY_DISABLE_CLIP=1 in CI to prevent clipboard usage.
VerifierSettings.AutoVerify();
// Auto-accept snapshots locally so the first run creates them without failing.
// In CI (detected via the CI environment variable) snapshots must already match;
// any divergence is a real test failure.
var isCI = !string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable("CI"));
if (!isCI)
{
VerifierSettings.AutoVerify();
}

// Store snapshots in the Snapshots/ subfolder of the project directory
Verifier.DerivePathInfo(
Expand Down Expand Up @@ -70,10 +75,24 @@ public static void Initialize()
@"(<version>)\d+\.\d+\.\d+(?:\.\d+)?(?:-[^<]+)?(</version>)",
"$1{scrubbed-version}$2");

// SHA-512 hashes (base64, ~88 chars ending in ==)
// Hex-encoded hash values as emitted by CycloneDX in <hash> elements.
// SHA-512 = 128 hex chars, SHA-256 = 64, SHA-1 = 40, MD5 = 32.
// Match uppercase or lowercase hex strings of those lengths (longest first).
line = Regex.Replace(
line,
@"\b[0-9A-Fa-f]{128}\b",
"{scrubbed-hash}");
line = Regex.Replace(
line,
@"\b[0-9A-Fa-f]{64}\b",
"{scrubbed-hash}");
line = Regex.Replace(
line,
@"\b[0-9A-Fa-f]{40}\b",
"{scrubbed-hash}");
line = Regex.Replace(
line,
@"(?:[A-Za-z0-9+/]{86,88}={0,2})",
@"\b[0-9A-Fa-f]{32}\b",
"{scrubbed-hash}");

return line;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
<?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" serialNumber="urn:uuid:{scrubbed}" version="1" xmlns="http://cyclonedx.org/schema/bom/1.6">
<bom xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" serialNumber="urn:uuid:{scrubbed}" version="1" xmlns="http://cyclonedx.org/schema/bom/1.7">
<metadata>
<timestamp>{scrubbed-timestamp}</timestamp>
<tools>
<tool>
<vendor>CycloneDX</vendor>
<name>CycloneDX module for .NET</name>
<version>{scrubbed-version}</version>
</tool>
<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="DevDepExcludeSln@0.0.0">
<name>DevDepExcludeSln</name>
Expand All @@ -26,7 +37,7 @@
<description>Test package TestPkg.A</description>
<scope>required</scope>
<hashes>
<hash alg="SHA-512">{scrubbed-hash}531C6F999C3D7E54935AF23D43D1F34E34F13C64</hash>
<hash alg="SHA-512">{scrubbed-hash}</hash>
</hashes>
<licenses>
<license>
Expand Down
Loading