Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add normalisation logic to VCS URL #910

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
71 changes: 71 additions & 0 deletions CycloneDX.Tests/NugetV3ServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,77 @@ public async Task GetComponent_FromCachedNugetFile_DoNotReturnsHash_WhenDisabled
Assert.Null(component.Hashes);
}

public static IEnumerable<object[]> VcsUrlNormalization
{
get
{
return new List<object[]>
{
new object[] { null, null },

// Blank
new object[] { "", null },
new object[] { " ", null },

// Leading and trailing whitespace
new object[] { " [email protected]:LordVeovis/xmlrpc.git", "https://git:@github.com/LordVeovis/xmlrpc.git" },
new object[] { "[email protected]:LordVeovis/xmlrpc.git ", "https://git:@github.com/LordVeovis/xmlrpc.git" },
new object[] { " [email protected]:LordVeovis/xmlrpc.git ", "https://git:@github.com/LordVeovis/xmlrpc.git" },

// Relative
new object[] { "gitlab.dontcare.com:group/repo.git", "gitlab.dontcare.com:group/repo.git" },
new object[] { "[email protected]:group/repo.git", "https://git:@gitlab.dontcare.com/group/repo.git" },

// Absolute
new object[] { "gitlab.dontcare.com:/group/repo.git", "gitlab.dontcare.com:/group/repo.git" },
new object[] { "[email protected]:/group/repo.git", "https://git:@gitlab.dontcare.com/group/repo.git" },

// Colon in path
new object[] { "[email protected]:/group:with:colons/repo.git", "https://git:@gitlab.dontcare.com/group:with:colons/repo.git" },

// Invalid
new object[] { " + ", null },
new object[] { "user@@gitlab.com:/rooted/Thinktecture.Logging.Configuration.git", null },

// Port number
new object[] { "https://github.com:443/CycloneDX/cyclonedx-dotnet.git", "https://github.com/CycloneDX/cyclonedx-dotnet.git" },
new object[] { "https://user:@github.com:443/CycloneDX/cyclonedx-dotnet.git", "https://user:@github.com/CycloneDX/cyclonedx-dotnet.git" },
new object[] { "https://user:[email protected]:443/CycloneDX/cyclonedx-dotnet.git", "https://user:[email protected]/CycloneDX/cyclonedx-dotnet.git" },

// Valid
new object[] { "https://github.com/CycloneDX/cyclonedx-dotnet.git", "https://github.com/CycloneDX/cyclonedx-dotnet.git" }
};
}
}

[Theory]
[MemberData(nameof(VcsUrlNormalization))]
public async Task GetComponent_FromCachedNuspecFile_UsesNormalizedVcsUrl(string rawVcsUrl, string normalizedVcsUrl)
{
var nuspecFileContents = $@"<?xml version=""1.0"" encoding=""utf-8""?>
<package xmlns=""http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"">
<metadata>
<id>testpackage</id>
<repository type=""git"" url=""{rawVcsUrl}"" />
</metadata>
</package>";
var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ XFS.Path($@"c:\nugetcache\testpackage\1.0.0\testpackage.nuspec"), new MockFileData(nuspecFileContents) },
});

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

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

Assert.Equal("testpackage", component.Name);
Assert.Equal(normalizedVcsUrl, component.ExternalReferences?.FirstOrDefault()?.Url);
}

[Fact]
public async Task GetComponentFromNugetOrgReturnsComponent()
{
Expand Down
75 changes: 74 additions & 1 deletion CycloneDX/Services/NugetV3Service.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
using System.IO;
using System.IO.Abstractions;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CycloneDX.Interfaces;
Expand Down Expand Up @@ -114,6 +115,78 @@ private string NormalizeVersion(string version)
return version;
}

/// <summary>
/// Converts Git SCP-like URLs to IRI / .NET URI parse-able equivalent.
/// </summary>
/// <param name="input">The VCS URI to normalize.</param>
/// <returns>A string parseable by Uri.Parse, otherwise null.</returns>
private string NormalizeUri(string input)
{
const string FALLBACK_SCHEME = "https://";

if (string.IsNullOrWhiteSpace(input)) { return null; }

UriCreationOptions ops = new UriCreationOptions();

input = input.Trim();

if (Uri.TryCreate(input, ops, out Uri? result))
{
return result.ToString();
}

// Locate the main parts of the 'SCP-like' Git URL
// https://git-scm.com/docs/git-clone#_git_urls
// 1. Optional user
// 2. Host
// 3. Path
int colonLocation = input.IndexOf(':');
if (colonLocation == -1)
{
// Uri.Parse can fail in the absense of colons AND the absense of a scheme.
// Add the fallback scheme to see if Uri.Parse can then interpret.
return NormalizeUri($"{FALLBACK_SCHEME}{input}");
}

var userAndHostPart = input.Substring(0, colonLocation);
var pathPart = input.Substring(colonLocation + 1, input.Length - 1 - userAndHostPart.Length);

var tokens = userAndHostPart.Split('@');
if (tokens.Length != 2)
{
// More than 1 @ would be invalid. No @ would probably have parsed ok by .NET.
return null;
}

var user = tokens[0];
var host = tokens[1];

var sb = new StringBuilder();
sb.Append(FALLBACK_SCHEME); // Assume this is the scheme which caused the parsing issue.

if (!string.IsNullOrEmpty(user))
{
sb.Append(user);
sb.Append(":@");
}

sb.Append(host);

if (!pathPart.StartsWith('/'))
{
sb.Append("/");
}

sb.Append(pathPart);

if (Uri.TryCreate(sb.ToString(), ops, out var adapted))
{
return adapted.ToString();
}

return null;
}

private SourceRepository SetupNugetRepository(NugetInputModel nugetInput)
{
if (nugetInput == null || string.IsNullOrEmpty(nugetInput.nugetFeedUrl) ||
Expand Down Expand Up @@ -255,7 +328,7 @@ public async Task<Component> GetComponentAsync(string name, string version, Comp

// Source: https://docs.microsoft.com/de-de/nuget/reference/nuspec#repository
var repoMeta = nuspecModel.nuspecReader.GetRepositoryMetadata();
var vcsUrl = repoMeta?.Url;
var vcsUrl = NormalizeUri(repoMeta?.Url);
if (!string.IsNullOrEmpty(vcsUrl))
{
var externalReference = new ExternalReference
Expand Down