Skip to content
Open
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
73 changes: 72 additions & 1 deletion CycloneDX.Tests/NugetV3ServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,42 @@ public async Task GetComponent_GitHubLicenseLookup_FromProjectUrl_ReturnsCompone
Assert.Equal("LicenseId", component.Licenses.First().License.Id);
}

[Fact]
public async Task GetComponent_GitHubLicenseLookup_FallsBackToLicenseFile()
{
var nuspecFileContents = @"<?xml version=""1.0"" encoding=""utf-8""?>
<package xmlns=""http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"">
<metadata>
<id>testpackage</id>
<license type=""file"">subdir/LICENSE.MD</license>
</metadata>
</package>";

byte[] licenseContents = Encoding.UTF8.GetBytes("The license");

var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ XFS.Path(@"c:\nugetcache\testpackage\1.0.0\testpackage.nuspec"), new MockFileData(nuspecFileContents) },
{ XFS.Path(@"c:\nugetcache\testpackage\1.0.0\subdir\LICENSE.MD"), new MockFileData(licenseContents) },
});

var mockGitHubService = new Mock<IGithubService>();

var nugetService = new NugetV3Service(null,
mockFileSystem,
new List<string> { XFS.Path(@"c:\nugetcache") },
mockGitHubService.Object,
new NullLogger(), false);

var component = await nugetService.GetComponentAsync("testpackage", "1.0.0", Component.ComponentScope.Required).ConfigureAwait(true);

Assert.Single(component.Licenses);
Assert.Equal("testpackage License", component.Licenses.First().License.Name);
Assert.Equal(Convert.ToBase64String(licenseContents), component.Licenses.First().License.Text.Content);
Assert.Equal("base64", component.Licenses.First().License.Text.Encoding);
Assert.Equal("text/markdown", component.Licenses.First().License.Text.ContentType);
}

[Fact]
public async Task GetComponent_GitHubLicenseLookup_FromRepository_WhenLicenseInvalid_ReturnsComponent()
{
Expand Down Expand Up @@ -578,7 +614,7 @@ public async Task GetComponent_GitHubLicenseLookup_MaliciousCommitWithFragment_D
}

[Fact]
public async Task GetComponent_WhenGitHubServiceIsNull_UsesLicenseUrl()
public async Task GetComponent_WhenGitHubServiceIsNullAndHasNoLicenseFile_UsesLicenseUrl()
{
var nuspecFileContents = @"<?xml version=""1.0"" encoding=""utf-8""?>
<package xmlns=""http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"">
Expand All @@ -605,5 +641,40 @@ public async Task GetComponent_WhenGitHubServiceIsNull_UsesLicenseUrl()
Assert.Equal("https://not-licence.url", component.Licenses.First().License.Url);
Assert.Equal("Unknown - See URL", component.Licenses.First().License.Name);
}

[Fact]
public async Task GetComponent_WhenGitHubServiceIsNull_UsesLicenseFile()
{
var nuspecFileContents = @"<?xml version=""1.0"" encoding=""utf-8""?>
<package xmlns=""http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"">
<metadata>
<id>testpackage</id>
<license type=""file"">subdir/LICENSE.MD</license>
<repository url=""https://licence.url"" />
</metadata>
</package>";

byte[] licenseContents = Encoding.UTF8.GetBytes("The license");

var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ XFS.Path(@"c:\nugetcache\testpackage\1.0.0\testpackage.nuspec"), new MockFileData(nuspecFileContents) },
{ XFS.Path(@"c:\nugetcache\testpackage\1.0.0\subdir\LICENSE.MD"), new MockFileData(licenseContents) },
});

var nugetService = new NugetV3Service(null,
mockFileSystem,
new List<string> { XFS.Path(@"c:\nugetcache") },
null,
new NullLogger(), false);

var component = await nugetService.GetComponentAsync("testpackage", "1.0.0", Component.ComponentScope.Required).ConfigureAwait(true);

Assert.Single(component.Licenses);
Assert.Equal("testpackage License", component.Licenses.First().License.Name);
Assert.Equal(Convert.ToBase64String(licenseContents), component.Licenses.First().License.Text.Content);
Assert.Equal("base64", component.Licenses.First().License.Text.Encoding);
Assert.Equal("text/markdown", component.Licenses.First().License.Text.ContentType);
}
}
}
77 changes: 69 additions & 8 deletions CycloneDX/Services/NugetV3Service.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
using NuGet.Common;
using NuGet.Configuration;
using NuGet.Packaging;
using NuGet.Packaging.Core;
using NuGet.Packaging.Licenses;
using NuGet.Protocol;
using NuGet.Protocol.Core.Types;
Expand Down Expand Up @@ -77,25 +78,28 @@ bool disableHashComputation
}

internal string GetCachedNuspecFilename(string name, string version)
{
return GetCachedNupkgFilename(name, version, name.ToLowerInvariant() + _nuspecExtension);
}

internal string GetCachedNupkgFilename(string name, string version, string fileName)
{
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(version)) { return null; }

var lowerName = name.ToLowerInvariant();
var lowerVersion = version.ToLowerInvariant();
string nuspecFilename = null;

foreach (var packageCachePath in _packageCachePaths)
{
var currentDirectory = _fileSystem.Path.Combine(packageCachePath, lowerName, NormalizeVersion(lowerVersion));
var currentFilename = _fileSystem.Path.Combine(currentDirectory, lowerName + _nuspecExtension);
var currentFilename = _fileSystem.Path.Combine(currentDirectory, fileName);
if (_fileSystem.File.Exists(currentFilename))
{
nuspecFilename = currentFilename;
break;
return currentFilename;
}
}

return nuspecFilename;
return null;
}

/// <summary>
Expand Down Expand Up @@ -272,9 +276,19 @@ public async Task<Component> GetComponentAsync(string name, string version, Comp
}
else if (_githubService == null)
{
var licenseUrl = nuspecModel.nuspecReader.GetLicenseUrl();
var license = new License { Name = "Unknown - See URL", Url = licenseUrl?.Trim() };
component.Licenses = new List<LicenseChoice> { new LicenseChoice { License = license } };
License license = await TryGetLicenseFileAsync(licenseMetadata, name, version).ConfigureAwait(false);

if (license == null)
{
var licenseUrl = nuspecModel.nuspecReader.GetLicenseUrl();
license = new License { Name = "Unknown - See URL", Url = licenseUrl?.Trim() };
}

if (license != null)
{
component.Licenses ??= new List<LicenseChoice>();
component.Licenses.Add(new LicenseChoice { License = license });
}
}
else
{
Expand Down Expand Up @@ -310,6 +324,11 @@ public async Task<Component> GetComponentAsync(string name, string version, Comp
}
}

if (license == null)
{
license = await TryGetLicenseFileAsync(licenseMetadata, name, version).ConfigureAwait(false);
}

if (license != null)
{
component.Licenses = new List<LicenseChoice> { new LicenseChoice { License = license } };
Expand Down Expand Up @@ -350,6 +369,48 @@ public async Task<Component> GetComponentAsync(string name, string version, Comp
return component;
}

private async Task<License> TryGetLicenseFileAsync(LicenseMetadata licenseMetadata, string name, string version)
{
if (licenseMetadata == null || licenseMetadata.Type != LicenseType.File || string.IsNullOrEmpty(licenseMetadata.License))
{
return null;
}

string licensePath = GetCachedNupkgFilename(name, version, licenseMetadata.License);

if (licensePath == null)
{
return null;
}

if (!_fileSystem.File.Exists(licensePath))
{
return null;
}

string extension = _fileSystem.Path.GetExtension(licensePath)?.ToLowerInvariant();

string contentType = extension switch
{
".md" => "text/markdown",
".txt" or "" or null => "text/plain",
_ => "application/octet-stream",
};

byte[] licenseContent = await _fileSystem.File.ReadAllBytesAsync(licensePath).ConfigureAwait(false);

return new License
{
Name = $"{name} License",
Text = new AttachedText
{
ContentType = contentType,
Encoding = "base64",
Content = Convert.ToBase64String(licenseContent),
}
};
}

private static Component SetupComponentProperties(Component component, NuspecModel nuspecModel)
{
component.Authors = new List<OrganizationalContact> { new OrganizationalContact { Name = nuspecModel.nuspecReader.GetAuthors() } };
Expand Down