From ae04c5cccf41932b7a512c977519b2591a49b7a8 Mon Sep 17 00:00:00 2001 From: Alex Stevens Date: Tue, 22 Oct 2024 20:14:13 +0100 Subject: [PATCH] Add normalisation logic to VCS URL Normalise the contents of the VCS URL to handle SCP-like Git URLs Signed-off-by: Alex Stevens --- CycloneDX.Tests/NugetV3ServiceTests.cs | 71 ++++++++++++++++++++++++ CycloneDX/Services/NugetV3Service.cs | 75 +++++++++++++++++++++++++- 2 files changed, 145 insertions(+), 1 deletion(-) diff --git a/CycloneDX.Tests/NugetV3ServiceTests.cs b/CycloneDX.Tests/NugetV3ServiceTests.cs index 909ac6a8..2f6d3cde 100644 --- a/CycloneDX.Tests/NugetV3ServiceTests.cs +++ b/CycloneDX.Tests/NugetV3ServiceTests.cs @@ -188,6 +188,77 @@ public async Task GetComponent_FromCachedNugetFile_DoNotReturnsHash_WhenDisabled Assert.Null(component.Hashes); } + public static IEnumerable VcsUrlNormalization + { + get + { + return new List + { + new object[] { null, null }, + + // Blank + new object[] { "", null }, + new object[] { " ", null }, + + // Leading and trailing whitespace + new object[] { " git@github.com:LordVeovis/xmlrpc.git", "https://git:@github.com/LordVeovis/xmlrpc.git" }, + new object[] { "git@github.com:LordVeovis/xmlrpc.git ", "https://git:@github.com/LordVeovis/xmlrpc.git" }, + new object[] { " git@github.com: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[] { "git@gitlab.dontcare.com: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[] { "git@gitlab.dontcare.com:/group/repo.git", "https://git:@gitlab.dontcare.com/group/repo.git" }, + + // Colon in path + new object[] { "git@gitlab.dontcare.com:/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:password@github.com:443/CycloneDX/cyclonedx-dotnet.git", "https://user:password@github.com/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 = $@" + + + testpackage + + + "; + var mockFileSystem = new MockFileSystem(new Dictionary + { + { XFS.Path($@"c:\nugetcache\testpackage\1.0.0\testpackage.nuspec"), new MockFileData(nuspecFileContents) }, + }); + + var nugetService = new NugetV3Service(null, + mockFileSystem, + new List { XFS.Path(@"c:\nugetcache") }, + new Mock().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() { diff --git a/CycloneDX/Services/NugetV3Service.cs b/CycloneDX/Services/NugetV3Service.cs index 6fa4bcc5..365fca0a 100644 --- a/CycloneDX/Services/NugetV3Service.cs +++ b/CycloneDX/Services/NugetV3Service.cs @@ -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; @@ -114,6 +115,78 @@ private string NormalizeVersion(string version) return version; } + /// + /// Converts Git SCP-like URLs to IRI / .NET URI parse-able equivalent. + /// + /// The VCS URI to normalize. + /// A string parseable by Uri.Parse, otherwise null. + 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) || @@ -255,7 +328,7 @@ public async Task 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