From c7af6ec803430e012d5ac7147cd9bdb1fa008938 Mon Sep 17 00:00:00 2001 From: Georg Rottensteiner Date: Thu, 16 Jan 2025 07:46:49 +0100 Subject: [PATCH 01/10] Added NugetConfigFileService, parsing nuget.config for package sources Allow package lookup from a list of repositories --- .../FunctionalTests/FunctionalTestHelper.cs | 5 +- CycloneDX.Tests/FunctionalTests/Issue758.cs | 3 +- CycloneDX.Tests/Helpers.cs | 12 +++ .../NugetConfigFileServiceTests.cs | 86 +++++++++++++++ CycloneDX.Tests/ProgramTests.cs | 6 +- CycloneDX.Tests/ValidationTests.cs | 2 +- .../Interfaces/INugetConfigFileService.cs | 29 +++++ CycloneDX/Interfaces/INugetServiceFactory.cs | 2 +- CycloneDX/Models/NugetInputModel.cs | 8 +- CycloneDX/Runner.cs | 15 ++- CycloneDX/Services/FileDiscoveryService.cs | 10 ++ CycloneDX/Services/NugetConfigFileService.cs | 101 ++++++++++++++++++ CycloneDX/Services/NugetV3Service.cs | 63 +++++++++-- CycloneDX/Services/NugetV3ServiceFactory.cs | 19 +++- Directory.Packages.props | 4 +- 15 files changed, 338 insertions(+), 27 deletions(-) create mode 100644 CycloneDX.Tests/NugetConfigFileServiceTests.cs create mode 100644 CycloneDX/Interfaces/INugetConfigFileService.cs create mode 100644 CycloneDX/Services/NugetConfigFileService.cs diff --git a/CycloneDX.Tests/FunctionalTests/FunctionalTestHelper.cs b/CycloneDX.Tests/FunctionalTests/FunctionalTestHelper.cs index 15c46613..dd48e80b 100644 --- a/CycloneDX.Tests/FunctionalTests/FunctionalTestHelper.cs +++ b/CycloneDX.Tests/FunctionalTests/FunctionalTestHelper.cs @@ -39,7 +39,8 @@ private static INugetServiceFactory CreateMockNugetServiceFactory() It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny>())) + It.IsAny>(), + It.IsAny>())) .Returns(nugetService); return mockNugetServiceFactory.Object; @@ -79,7 +80,7 @@ public static async Task Test(RunOptions options, INugetServiceFactory nuge options.SolutionOrProjectFile ??= MockUnixSupport.Path("c:/ProjectPath/Project.csproj"); options.disablePackageRestore = true; - Runner runner = new Runner(mockFileSystem, null, null, null, null, null, null, nugetService); + Runner runner = new Runner(mockFileSystem, null, null, null, null, null, null, null, nugetService); int exitCode = await runner.HandleCommandAsync(options); Assert.Equal((int)ExitCode.OK, exitCode); diff --git a/CycloneDX.Tests/FunctionalTests/Issue758.cs b/CycloneDX.Tests/FunctionalTests/Issue758.cs index 24bf8ff7..5b4c7233 100644 --- a/CycloneDX.Tests/FunctionalTests/Issue758.cs +++ b/CycloneDX.Tests/FunctionalTests/Issue758.cs @@ -37,7 +37,8 @@ public Issue758() It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny>())) + It.IsAny>(), + It.IsAny>())) .Returns(nugetService); nugetServiceFactory = mockNugetServiceFactory.Object; diff --git a/CycloneDX.Tests/Helpers.cs b/CycloneDX.Tests/Helpers.cs index b71a0329..2f35e102 100644 --- a/CycloneDX.Tests/Helpers.cs +++ b/CycloneDX.Tests/Helpers.cs @@ -103,6 +103,18 @@ public static MockFileData GetPackagesFileWithPackageReferences(IEnumerable sources) + { + var fileData = ""; + foreach (var source in sources) + { + fileData += @""; + } + fileData += ""; + return new MockFileData(fileData); + + } + public static DotnetCommandResult GetDotnetListPackagesResult(IEnumerable<(string projectName, (string packageName, string version)[] packages)> projects) { StringBuilder stdout = new StringBuilder(); diff --git a/CycloneDX.Tests/NugetConfigFileServiceTests.cs b/CycloneDX.Tests/NugetConfigFileServiceTests.cs new file mode 100644 index 00000000..2153b250 --- /dev/null +++ b/CycloneDX.Tests/NugetConfigFileServiceTests.cs @@ -0,0 +1,86 @@ +// This file is part of CycloneDX Tool for .NET +// +// Licensed under the Apache License, Version 2.0 (the “License”); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an “AS IS” BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) OWASP Foundation. All Rights Reserved. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; +using System.IO.Abstractions.TestingHelpers; +using XFS = System.IO.Abstractions.TestingHelpers.MockUnixSupport; +using CycloneDX.Models; +using CycloneDX.Services; +using System.Linq; + +namespace CycloneDX.Tests +{ + public class NugetConfigFileServiceTests + { + [Fact] + public async Task GetDotnetDependencys_ReturnsDotnetDependency() + { + var mockFileSystem = new MockFileSystem(new Dictionary + { + { XFS.Path(@"c:\Project\nuget.config"), Helpers.GetNugetConfigFileWithSources( + new List { + new NugetInputModel( "https://www.contoso.com" ) { nugetFeedName = "Contoso" } + }) + }, + }); + var configFileService = new NugetConfigFileService(mockFileSystem); + + var sources = await configFileService.GetPackageSourcesAsync(XFS.Path(@"c:\Project\nuget.config")).ConfigureAwait(true); + + Assert.Collection(sources, + item => { + Assert.Equal("https://www.contoso.com", item.nugetFeedUrl); + }); + Assert.Collection(sources, + item => { + Assert.Equal("Contoso", item.nugetFeedName); + }); + } + + [Fact] + public async Task GetDotnetDependencys_ReturnsMultipleDotnetDependencys() + { + var mockFileSystem = new MockFileSystem(new Dictionary + { + { XFS.Path(@"c:\Project\nuget.config"), Helpers.GetNugetConfigFileWithSources( + new List { + new NugetInputModel( "https://www.contoso.com" ) { nugetFeedName = "Contoso" }, + new NugetInputModel( "https://www.contoso2.com" ) { nugetFeedName = "Contoso2" }, + new NugetInputModel( "https://www.contoso3.com" ) { nugetFeedName = "Contoso3" }, + }) + }, + }); + var configFileService = new NugetConfigFileService(mockFileSystem); + + var sources = await configFileService.GetPackageSourcesAsync(XFS.Path(@"c:\Project\nuget.config")).ConfigureAwait(true); + var sortedPackages = new List(sources); + sortedPackages.OrderBy(nim => nim.nugetFeedName); + + Assert.Collection(sortedPackages, + item => Assert.Equal("https://www.contoso.com", item.nugetFeedUrl), + item => Assert.Equal("https://www.contoso2.com", item.nugetFeedUrl), + item => Assert.Equal("https://www.contoso3.com", item.nugetFeedUrl)); + Assert.Collection(sortedPackages, + item => Assert.Equal("Contoso", item.nugetFeedName), + item => Assert.Equal("Contoso2", item.nugetFeedName), + item => Assert.Equal("Contoso3", item.nugetFeedName)); + } + + } +} diff --git a/CycloneDX.Tests/ProgramTests.cs b/CycloneDX.Tests/ProgramTests.cs index 0e1ca4a3..8d0233b3 100755 --- a/CycloneDX.Tests/ProgramTests.cs +++ b/CycloneDX.Tests/ProgramTests.cs @@ -50,7 +50,7 @@ public async Task CallingCycloneDX_CreatesOutputDirectory() .Setup(s => s.GetSolutionDotnetDependencys(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new HashSet()); - Runner runner = new Runner(fileSystem: mockFileSystem, null, null, null, null, null, solutionFileService: mockSolutionFileService.Object, null); + Runner runner = new Runner(fileSystem: mockFileSystem, null, null, null, null, null, null, solutionFileService: mockSolutionFileService.Object, null); RunOptions runOptions = new RunOptions { @@ -75,7 +75,7 @@ public async Task CallingCycloneDX_WithOutputFilename_CreatesOutputFilename() .Setup(s => s.GetSolutionDotnetDependencys(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new HashSet()); - Runner runner = new Runner(fileSystem: mockFileSystem, null, null, null, null, null, solutionFileService: mockSolutionFileService.Object, null); + Runner runner = new Runner(fileSystem: mockFileSystem, null, null, null, null, null, null, solutionFileService: mockSolutionFileService.Object, null); RunOptions runOptions = new RunOptions { @@ -116,7 +116,7 @@ public async Task CallingCycloneDX_WithSolutionOrProjectFileThatDoesntExistsRetu .Setup(s => s.GetSolutionDotnetDependencys(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new HashSet()); - Runner runner = new Runner(fileSystem: mockFileSystem, null, null, null, null, null, solutionFileService: mockSolutionFileService.Object, null); + Runner runner = new Runner(fileSystem: mockFileSystem, null, null, null, null, null, null, solutionFileService: mockSolutionFileService.Object, null); RunOptions runOptions = new RunOptions { diff --git a/CycloneDX.Tests/ValidationTests.cs b/CycloneDX.Tests/ValidationTests.cs index 802cec92..72cbc1dc 100644 --- a/CycloneDX.Tests/ValidationTests.cs +++ b/CycloneDX.Tests/ValidationTests.cs @@ -41,7 +41,7 @@ public async Task Validation(string fileFormat, bool disableGitHubLicenses) mock.GetProjectDotnetDependencysAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) ).ReturnsAsync(packages); - Runner runner = new Runner(fileSystem: mockFileSystem, null, null, null, null, projectFileService: mockProjectFileService.Object, solutionFileService: null, null); + Runner runner = new Runner(fileSystem: mockFileSystem, null, null, null, null, null, projectFileService: mockProjectFileService.Object, solutionFileService: null, null); RunOptions runOptions = new RunOptions diff --git a/CycloneDX/Interfaces/INugetConfigFileService.cs b/CycloneDX/Interfaces/INugetConfigFileService.cs new file mode 100644 index 00000000..bb65c25f --- /dev/null +++ b/CycloneDX/Interfaces/INugetConfigFileService.cs @@ -0,0 +1,29 @@ +// This file is part of CycloneDX Tool for .NET +// +// Licensed under the Apache License, Version 2.0 (the “License”); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an “AS IS” BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) OWASP Foundation. All Rights Reserved. + +using System.Collections.Generic; +using System.Threading.Tasks; +using CycloneDX.Models; + +namespace CycloneDX.Interfaces +{ + public interface INugetConfigFileService + { + Task> GetPackageSourcesAsync(string configFilePath); + Task> RecursivelyGetPackageSourcesAsync(string directoryPath); + } +} diff --git a/CycloneDX/Interfaces/INugetServiceFactory.cs b/CycloneDX/Interfaces/INugetServiceFactory.cs index 8bf2c729..b48a055f 100644 --- a/CycloneDX/Interfaces/INugetServiceFactory.cs +++ b/CycloneDX/Interfaces/INugetServiceFactory.cs @@ -11,6 +11,6 @@ namespace CycloneDX.Interfaces { public interface INugetServiceFactory { - INugetService Create(RunOptions option, IFileSystem fileSystem, IGithubService githubService, List packageCachePaths); + INugetService Create(RunOptions option, IFileSystem fileSystem, IGithubService githubService, List packageCachePaths, HashSet nugetInputModels); } } diff --git a/CycloneDX/Models/NugetInputModel.cs b/CycloneDX/Models/NugetInputModel.cs index 9487a3af..e0089caa 100644 --- a/CycloneDX/Models/NugetInputModel.cs +++ b/CycloneDX/Models/NugetInputModel.cs @@ -20,7 +20,7 @@ namespace CycloneDX.Models public static class NugetInputFactory { public static NugetInputModel Create(string baseUrl, string baseUrlUserName, string baseUrlUserPassword, - bool isPasswordClearText) + bool isPasswordClearText, string feedName = "Unnamed Source" ) { if (string.IsNullOrEmpty(baseUrl)) { @@ -29,7 +29,7 @@ public static NugetInputModel Create(string baseUrl, string baseUrlUserName, str if (!string.IsNullOrEmpty(baseUrlUserName) && !string.IsNullOrEmpty(baseUrlUserPassword)) { - return new NugetInputModel(baseUrl, baseUrlUserName, baseUrlUserPassword, isPasswordClearText); + return new NugetInputModel(baseUrl, baseUrlUserName, baseUrlUserPassword, isPasswordClearText, feedName); } return new NugetInputModel(baseUrl); @@ -39,6 +39,7 @@ public static NugetInputModel Create(string baseUrl, string baseUrlUserName, str public class NugetInputModel { + public string nugetFeedName { get; set; } public string nugetFeedUrl { get; set; } public string nugetUsername { get; set; } public string nugetPassword { get; set; } @@ -50,12 +51,13 @@ public NugetInputModel(string baseUrl) } public NugetInputModel(string baseUrl, string baseUrlUserName, string baseUrlUserPassword, - bool isPasswordClearText) + bool isPasswordClearText, string feedName) { nugetFeedUrl = baseUrl; nugetUsername = baseUrlUserName; nugetPassword = baseUrlUserPassword; IsPasswordClearText = isPasswordClearText; + nugetFeedName = nugetFeedName; } } } diff --git a/CycloneDX/Runner.cs b/CycloneDX/Runner.cs index 10c0e23e..26e98836 100644 --- a/CycloneDX/Runner.cs +++ b/CycloneDX/Runner.cs @@ -37,6 +37,7 @@ public class Runner readonly IDotnetCommandService dotnetCommandService; readonly IDotnetUtilsService dotnetUtilsService; readonly IPackagesFileService packagesFileService; + readonly INugetConfigFileService configFileService; readonly IProjectFileService projectFileService; readonly ISolutionFileService solutionFileService; readonly INugetServiceFactory nugetServiceFactory; @@ -46,6 +47,7 @@ public Runner(IFileSystem fileSystem, IProjectAssetsFileService projectAssetsFileService, IDotnetUtilsService dotnetUtilsService, IPackagesFileService packagesFileService, + INugetConfigFileService configFileService, IProjectFileService projectFileService, ISolutionFileService solutionFileService, INugetServiceFactory nugetServiceFactory) @@ -55,11 +57,12 @@ public Runner(IFileSystem fileSystem, projectAssetsFileService ??= new ProjectAssetsFileService(this.fileSystem, () => new AssetFileReader()); this.dotnetUtilsService = dotnetUtilsService ?? new DotnetUtilsService(this.fileSystem, this.dotnetCommandService); this.packagesFileService = packagesFileService ?? new PackagesFileService(this.fileSystem); + this.configFileService = configFileService ?? new NugetConfigFileService(this.fileSystem); this.projectFileService = projectFileService ?? new ProjectFileService(this.fileSystem, this.dotnetUtilsService, this.packagesFileService, projectAssetsFileService); this.solutionFileService = solutionFileService ?? new SolutionFileService(this.fileSystem, this.projectFileService); this.nugetServiceFactory = nugetServiceFactory ?? new NugetV3ServiceFactory(); } - public Runner() : this(null, null, null, null, null, null, null, null) { } + public Runner() : this(null, null, null, null, null, null, null, null, null) { } public async Task HandleCommandAsync(RunOptions options) { @@ -130,9 +133,8 @@ public async Task HandleCommandAsync(RunOptions options) } } - var nugetService = nugetServiceFactory.Create(options, fileSystem, githubService, packageCachePathsResult.Result); - var packages = new HashSet(); + var sources = new HashSet(); // determine what we are analyzing and do the analysis var fullSolutionOrProjectFilePath = this.fileSystem.Path.GetFullPath(SolutionOrProjectFile); @@ -170,6 +172,7 @@ public async Task HandleCommandAsync(RunOptions options) return (int)ExitCode.InvalidOptions; } packages = await solutionFileService.GetSolutionDotnetDependencys(fullSolutionOrProjectFilePath, baseIntermediateOutputPath, excludetestprojects, framework, runtime).ConfigureAwait(false); + sources = await configFileService.RecursivelyGetPackageSourcesAsync( fileSystem.Path.GetDirectoryName(fullSolutionOrProjectFilePath)).ConfigureAwait(false); topLevelComponent.Name = fileSystem.Path.GetFileNameWithoutExtension(SolutionOrProjectFile); } else if (Utils.IsSupportedProjectType(SolutionOrProjectFile) && scanProjectReferences) @@ -180,6 +183,7 @@ public async Task HandleCommandAsync(RunOptions options) return (int)ExitCode.InvalidOptions; } packages = await projectFileService.RecursivelyGetProjectDotnetDependencysAsync(fullSolutionOrProjectFilePath, baseIntermediateOutputPath, excludetestprojects, framework, runtime).ConfigureAwait(false); + sources = await configFileService.RecursivelyGetPackageSourcesAsync(fileSystem.Path.GetDirectoryName(fullSolutionOrProjectFilePath)).ConfigureAwait(false); topLevelComponent.Name = fileSystem.Path.GetFileNameWithoutExtension(SolutionOrProjectFile); } else if (Utils.IsSupportedProjectType(SolutionOrProjectFile)) @@ -190,6 +194,7 @@ public async Task HandleCommandAsync(RunOptions options) return (int)ExitCode.InvalidOptions; } packages = await projectFileService.GetProjectDotnetDependencysAsync(fullSolutionOrProjectFilePath, baseIntermediateOutputPath, excludetestprojects, framework, runtime).ConfigureAwait(false); + sources = await configFileService.RecursivelyGetPackageSourcesAsync(fileSystem.Path.GetDirectoryName(fullSolutionOrProjectFilePath)).ConfigureAwait(false); topLevelComponent.Name = fileSystem.Path.GetFileNameWithoutExtension(SolutionOrProjectFile); } else if (fileSystem.Path.GetFileName(SolutionOrProjectFile).ToLowerInvariant().Equals("packages.config", StringComparison.OrdinalIgnoreCase)) @@ -200,11 +205,13 @@ public async Task HandleCommandAsync(RunOptions options) return (int)ExitCode.InvalidOptions; } packages = await packagesFileService.GetDotnetDependencysAsync(fullSolutionOrProjectFilePath).ConfigureAwait(false); + sources = await configFileService.RecursivelyGetPackageSourcesAsync(fileSystem.Path.GetDirectoryName(fullSolutionOrProjectFilePath)).ConfigureAwait(false); topLevelComponent.Name = fileSystem.Path.GetDirectoryName(fullSolutionOrProjectFilePath); } else if (fileSystem.Directory.Exists(fullSolutionOrProjectFilePath)) { packages = await packagesFileService.RecursivelyGetDotnetDependencysAsync(fullSolutionOrProjectFilePath).ConfigureAwait(false); + sources = await configFileService.RecursivelyGetPackageSourcesAsync(fileSystem.Path.GetDirectoryName(fullSolutionOrProjectFilePath)).ConfigureAwait(false); topLevelComponent.Name = fileSystem.Path.GetDirectoryName(fullSolutionOrProjectFilePath); } else @@ -219,7 +226,7 @@ public async Task HandleCommandAsync(RunOptions options) } await Console.Out.WriteLineAsync($"Found {packages.Count()} packages"); - + var nugetService = nugetServiceFactory.Create(options, fileSystem, githubService, packageCachePathsResult.Result, sources); if (!string.IsNullOrEmpty(setName)) { diff --git a/CycloneDX/Services/FileDiscoveryService.cs b/CycloneDX/Services/FileDiscoveryService.cs index 70f8129f..a9b44b40 100644 --- a/CycloneDX/Services/FileDiscoveryService.cs +++ b/CycloneDX/Services/FileDiscoveryService.cs @@ -40,5 +40,15 @@ public IEnumerable GetPackagesConfigFiles(string directory) return _fileSystem.Directory.GetFiles(directory, "packages.config", SearchOption.AllDirectories); } + /// + /// Recursively searches a directory for nuget.config files. + /// + /// Directory path to search + /// List of full file paths + public IEnumerable GetNugetConfigFiles(string directory) + { + return _fileSystem.Directory.GetFiles(directory, "nuget.config", SearchOption.AllDirectories); + } + } } diff --git a/CycloneDX/Services/NugetConfigFileService.cs b/CycloneDX/Services/NugetConfigFileService.cs new file mode 100644 index 00000000..c4188e19 --- /dev/null +++ b/CycloneDX/Services/NugetConfigFileService.cs @@ -0,0 +1,101 @@ +// This file is part of CycloneDX Tool for .NET +// +// Licensed under the Apache License, Version 2.0 (the “License”); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an “AS IS” BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) OWASP Foundation. All Rights Reserved. + +using System.Collections.Generic; +using System.Xml; +using System.IO; +using System.IO.Abstractions; +using System.Threading.Tasks; +using CycloneDX.Interfaces; +using CycloneDX.Models; +using System.Linq; +using System; + +namespace CycloneDX.Services +{ + public class NugetConfigFileService : INugetConfigFileService + { + private XmlReaderSettings _xmlReaderSettings = new XmlReaderSettings + { + Async = true + }; + + private IFileSystem _fileSystem; + private FileDiscoveryService _fileDiscoveryService; + + public NugetConfigFileService(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + _fileDiscoveryService = new FileDiscoveryService(fileSystem); + } + + /// + /// Analyzes a single nuget.config file for package sources. + /// + /// + /// + public async Task> GetPackageSourcesAsync(string configFilePath) + { + var sources = new HashSet(); + using (StreamReader fileReader = _fileSystem.File.OpenText(configFilePath)) + { + using (XmlReader reader = XmlReader.Create(fileReader, _xmlReaderSettings)) + { + while (await reader.ReadAsync().ConfigureAwait(false)) + { + if (reader.IsStartElement() && reader.Name == "add") + { + string packageSource = reader["value"]; + if (packageSource.StartsWith("https://api.nuget.org/v3")) + { + // normalize default URL + packageSource = "https://api.nuget.org/v3/index.json"; + } + var newSource = new NugetInputModel(packageSource); + newSource.nugetFeedName = reader["key"]; + + // TODO - user, password + + await Console.Out.WriteLineAsync($"\tFound Package Source:{newSource.nugetFeedName}"); + sources.Add(newSource); + } + } + } + } + return sources; + } + + /// + /// Recursively searches a directory and analyzes all packages.config files for NuGet package references. + /// + /// + /// + public async Task> RecursivelyGetPackageSourcesAsync(string directoryPath) + { + var sources = new HashSet(); + var configFiles = _fileDiscoveryService.GetNugetConfigFiles(directoryPath); + + foreach (var configFile in configFiles) + { + var newsources = await GetPackageSourcesAsync(configFile).ConfigureAwait(false); + sources.UnionWith(newsources); + } + + return sources; + } + } +} diff --git a/CycloneDX/Services/NugetV3Service.cs b/CycloneDX/Services/NugetV3Service.cs index 6fa4bcc5..402065c7 100644 --- a/CycloneDX/Services/NugetV3Service.cs +++ b/CycloneDX/Services/NugetV3Service.cs @@ -40,7 +40,7 @@ namespace CycloneDX.Services /// public class NugetV3Service : INugetService { - private readonly SourceRepository _sourceRepository; + private readonly List _sourceRepositories; private readonly SourceCacheContext _sourceCacheContext; private readonly CancellationToken _cancellationToken; private readonly ILogger _logger; @@ -56,7 +56,7 @@ public class NugetV3Service : INugetService private const string _sha512Extension = ".nupkg.sha512"; public NugetV3Service( - NugetInputModel nugetInput, + HashSet nugetInputModels, IFileSystem fileSystem, List packageCachePaths, IGithubService githubService, @@ -70,7 +70,22 @@ bool disableHashComputation _disableHashComputation = disableHashComputation; _logger = logger; - _sourceRepository = SetupNugetRepository(nugetInput); + _sourceRepositories = new List(); + if (nugetInputModels != null) + { + foreach (var nugetInput in nugetInputModels) + { + var repo = SetupNugetRepository(nugetInput); + if (repo != null) + { + _sourceRepositories.Add(repo); + } + } + } + else + { + _sourceRepositories.Add(SetupNugetRepository(null)); + } _sourceCacheContext = new SourceCacheContext(); _cancellationToken = CancellationToken.None; } @@ -167,11 +182,15 @@ public async Task GetComponentAsync(string name, string version, Comp if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(version)) { return null; } // https://docs.microsoft.com/en-us/nuget/reference/nuget-client-sdk - Download a package - var resource = await _sourceRepository.GetResourceAsync(); + var resources = new List(); + foreach (var repo in _sourceRepositories) + { + resources.Add( await repo.GetResourceAsync() ); + } var component = SetupComponent(name, version, scope); var nuspecFilename = GetCachedNuspecFilename(name, version); - var nuspecModel = await GetNuspec(name, version, nuspecFilename, resource).ConfigureAwait(false); + var nuspecModel = await GetNuspec(name, version, nuspecFilename, resources).ConfigureAwait(false); if (nuspecModel.hashBytes != null) { var hex = BitConverter.ToString(nuspecModel.hashBytes).Replace("-", string.Empty); @@ -306,15 +325,43 @@ private static Component SetupComponentProperties(Component component, NuspecMod } private async Task GetNuspec(string name, string version, string nuspecFilename, - FindPackageByIdResource resource) + List resources) { var nuspecModel = new NuspecModel(); if (nuspecFilename == null) { var packageVersion = new NuGetVersion(version); await using MemoryStream packageStream = new MemoryStream(); - await resource.CopyNupkgToStreamAsync(name, packageVersion, packageStream, _sourceCacheContext, - _logger, _cancellationToken); + Console.WriteLine($"Looking for {name} {version}"); + bool foundResource = false; + foreach (var resource in resources) + { + try + { + await resource.CopyNupkgToStreamAsync(name, packageVersion, packageStream, _sourceCacheContext, + _logger, _cancellationToken); + if ((packageStream != null) + && (packageStream.Length == 0)) + { + continue; + } + if ((packageStream != null) + && (packageStream.Length > 0)) + { + foundResource = true; + Console.WriteLine($" found"); + break; + } + } + catch (Exception ex) + { + Console.WriteLine($" Error looking for package {name} {version}: {ex.ToString()}"); + } + } + if (!foundResource) + { + throw new Exception( $"Did not find package {name} {version}"); + } using PackageArchiveReader packageReader = new PackageArchiveReader(packageStream); nuspecModel.nuspecReader = await packageReader.GetNuspecReaderAsync(_cancellationToken); diff --git a/CycloneDX/Services/NugetV3ServiceFactory.cs b/CycloneDX/Services/NugetV3ServiceFactory.cs index 46479336..abc7bf8d 100644 --- a/CycloneDX/Services/NugetV3ServiceFactory.cs +++ b/CycloneDX/Services/NugetV3ServiceFactory.cs @@ -6,16 +6,31 @@ using System.Threading.Tasks; using CycloneDX.Interfaces; using CycloneDX.Models; +using JetBrains.Annotations; +using NuGet.Common; namespace CycloneDX.Services { public class NugetV3ServiceFactory : INugetServiceFactory { - public INugetService Create(RunOptions option, IFileSystem fileSystem, IGithubService githubService, List packageCachePaths ) + public INugetService Create(RunOptions option, IFileSystem fileSystem, IGithubService githubService, List packageCachePaths, HashSet nugetInputModels) { var nugetLogger = new NuGet.Common.NullLogger(); var nugetInput = NugetInputFactory.Create(option.baseUrl, option.baseUrlUserName, option.baseUrlUSP, option.isPasswordClearText); - return new NugetV3Service(nugetInput, fileSystem, packageCachePaths, githubService, nugetLogger, option.disableHashComputation); + + if (nugetInputModels == null) + { + nugetInputModels = new HashSet(); + } + if (nugetInput != null) + { + nugetInputModels.Add(nugetInput); + } + if (!nugetInputModels.Any()) + { + nugetInputModels.Add(new NugetInputModel("https://api.nuget.org/v3/index.json")); + } + return new NugetV3Service(nugetInputModels, fileSystem, packageCachePaths, githubService, nugetLogger, option.disableHashComputation); } } } diff --git a/Directory.Packages.props b/Directory.Packages.props index 1c7c2074..1b33329d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,8 +7,8 @@ - - + + From defa3ac1fa89ce0fe67f045573ef4d100883d058 Mon Sep 17 00:00:00 2001 From: Georg Rottensteiner Date: Tue, 21 Jan 2025 15:02:03 +0100 Subject: [PATCH 02/10] Fix Codacy "issues" Signed-off-by: Georg Rottensteiner --- CycloneDX/Models/NugetInputModel.cs | 12 ++++++++++-- CycloneDX/Runner.cs | 2 +- CycloneDX/Services/NugetConfigFileService.cs | 6 +++--- CycloneDX/Services/NugetV3Service.cs | 11 ++++++++++- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/CycloneDX/Models/NugetInputModel.cs b/CycloneDX/Models/NugetInputModel.cs index e0089caa..76e8b256 100644 --- a/CycloneDX/Models/NugetInputModel.cs +++ b/CycloneDX/Models/NugetInputModel.cs @@ -15,12 +15,20 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (c) OWASP Foundation. All Rights Reserved. +using System.Transactions; + namespace CycloneDX.Models { public static class NugetInputFactory { public static NugetInputModel Create(string baseUrl, string baseUrlUserName, string baseUrlUserPassword, - bool isPasswordClearText, string feedName = "Unnamed Source" ) + bool isPasswordClearText) + { + return Create(baseUrl, baseUrlUserName, baseUrlUserPassword, isPasswordClearText, "Unnamed Source"); + } + + public static NugetInputModel Create(string baseUrl, string baseUrlUserName, string baseUrlUserPassword, + bool isPasswordClearText, string feedName ) { if (string.IsNullOrEmpty(baseUrl)) { @@ -57,7 +65,7 @@ public NugetInputModel(string baseUrl, string baseUrlUserName, string baseUrlUse nugetUsername = baseUrlUserName; nugetPassword = baseUrlUserPassword; IsPasswordClearText = isPasswordClearText; - nugetFeedName = nugetFeedName; + nugetFeedName = feedName; } } } diff --git a/CycloneDX/Runner.cs b/CycloneDX/Runner.cs index 26e98836..24f8667e 100644 --- a/CycloneDX/Runner.cs +++ b/CycloneDX/Runner.cs @@ -134,7 +134,7 @@ public async Task HandleCommandAsync(RunOptions options) } var packages = new HashSet(); - var sources = new HashSet(); + HashSet sources = null; // determine what we are analyzing and do the analysis var fullSolutionOrProjectFilePath = this.fileSystem.Path.GetFullPath(SolutionOrProjectFile); diff --git a/CycloneDX/Services/NugetConfigFileService.cs b/CycloneDX/Services/NugetConfigFileService.cs index c4188e19..db001a8c 100644 --- a/CycloneDX/Services/NugetConfigFileService.cs +++ b/CycloneDX/Services/NugetConfigFileService.cs @@ -29,13 +29,13 @@ namespace CycloneDX.Services { public class NugetConfigFileService : INugetConfigFileService { - private XmlReaderSettings _xmlReaderSettings = new XmlReaderSettings + private readonly XmlReaderSettings _xmlReaderSettings = new XmlReaderSettings { Async = true }; - private IFileSystem _fileSystem; - private FileDiscoveryService _fileDiscoveryService; + private readonly IFileSystem _fileSystem; + private readonly FileDiscoveryService _fileDiscoveryService; public NugetConfigFileService(IFileSystem fileSystem) { diff --git a/CycloneDX/Services/NugetV3Service.cs b/CycloneDX/Services/NugetV3Service.cs index 402065c7..c7fe4e2c 100644 --- a/CycloneDX/Services/NugetV3Service.cs +++ b/CycloneDX/Services/NugetV3Service.cs @@ -40,6 +40,15 @@ namespace CycloneDX.Services /// public class NugetV3Service : INugetService { + public class PackageNotFoundException : Exception + { + public PackageNotFoundException(string message) : base(message) { } + + public PackageNotFoundException(string message, Exception innerException) : base(message, innerException) { } + } + + + private readonly List _sourceRepositories; private readonly SourceCacheContext _sourceCacheContext; private readonly CancellationToken _cancellationToken; @@ -360,7 +369,7 @@ await resource.CopyNupkgToStreamAsync(name, packageVersion, packageStream, _sour } if (!foundResource) { - throw new Exception( $"Did not find package {name} {version}"); + throw new PackageNotFoundException( $"Did not find package {name} {version}"); } using PackageArchiveReader packageReader = new PackageArchiveReader(packageStream); From 9a25946debaa549883799f61c60ee591f90e51f6 Mon Sep 17 00:00:00 2001 From: Georg Rottensteiner Date: Wed, 22 Jan 2025 08:40:15 +0100 Subject: [PATCH 03/10] Add logging --- CycloneDX/Services/NugetConfigFileService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CycloneDX/Services/NugetConfigFileService.cs b/CycloneDX/Services/NugetConfigFileService.cs index db001a8c..d4cde9a1 100644 --- a/CycloneDX/Services/NugetConfigFileService.cs +++ b/CycloneDX/Services/NugetConfigFileService.cs @@ -50,6 +50,7 @@ public NugetConfigFileService(IFileSystem fileSystem) /// public async Task> GetPackageSourcesAsync(string configFilePath) { + Console.WriteLine( $"Reading NuGet config file: {configFilePath}"); var sources = new HashSet(); using (StreamReader fileReader = _fileSystem.File.OpenText(configFilePath)) { @@ -69,7 +70,6 @@ public async Task> GetPackageSourcesAsync(string config newSource.nugetFeedName = reader["key"]; // TODO - user, password - await Console.Out.WriteLineAsync($"\tFound Package Source:{newSource.nugetFeedName}"); sources.Add(newSource); } @@ -86,6 +86,7 @@ public async Task> GetPackageSourcesAsync(string config /// public async Task> RecursivelyGetPackageSourcesAsync(string directoryPath) { + Console.WriteLine($"Scanning for nuget.config files in {directoryPath}"); var sources = new HashSet(); var configFiles = _fileDiscoveryService.GetNugetConfigFiles(directoryPath); From 726420179eee5e4e35e52d371048f7275c148734 Mon Sep 17 00:00:00 2001 From: Georg Rottensteiner Date: Wed, 29 Jan 2025 08:13:22 +0100 Subject: [PATCH 04/10] Update packages --- CycloneDX.Tests/CycloneDX.Tests.csproj | 1 + CycloneDX/CycloneDX.csproj | 1 + Directory.Packages.props | 1 + 3 files changed, 3 insertions(+) diff --git a/CycloneDX.Tests/CycloneDX.Tests.csproj b/CycloneDX.Tests/CycloneDX.Tests.csproj index 802f25da..86210790 100644 --- a/CycloneDX.Tests/CycloneDX.Tests.csproj +++ b/CycloneDX.Tests/CycloneDX.Tests.csproj @@ -13,6 +13,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CycloneDX/CycloneDX.csproj b/CycloneDX/CycloneDX.csproj index d113aaa3..5eb75cb1 100644 --- a/CycloneDX/CycloneDX.csproj +++ b/CycloneDX/CycloneDX.csproj @@ -38,6 +38,7 @@ + diff --git a/Directory.Packages.props b/Directory.Packages.props index 1b33329d..3bf97784 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,6 +11,7 @@ + From b632f223c1530fa5b7036b6e07b7137a93870a76 Mon Sep 17 00:00:00 2001 From: Georg Rottensteiner Date: Wed, 29 Jan 2025 08:38:11 +0100 Subject: [PATCH 05/10] Update packages again --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3bf97784..b0ddee44 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,7 +11,7 @@ - + From 77b6f93e858e5d987ef6024ceaf99532406f94cf Mon Sep 17 00:00:00 2001 From: Georg Rottensteiner Date: Thu, 16 Jan 2025 07:46:49 +0100 Subject: [PATCH 06/10] Added NugetConfigFileService, parsing nuget.config for package sources Allow package lookup from a list of repositories Modified commit message to include signoff: Signed-off-by: Georg Rottensteiner --- .../FunctionalTests/FunctionalTestHelper.cs | 5 +- CycloneDX.Tests/FunctionalTests/Issue758.cs | 3 +- CycloneDX.Tests/Helpers.cs | 12 +++ .../NugetConfigFileServiceTests.cs | 86 +++++++++++++++ CycloneDX.Tests/ProgramTests.cs | 6 +- CycloneDX.Tests/ValidationTests.cs | 2 +- .../Interfaces/INugetConfigFileService.cs | 29 +++++ CycloneDX/Interfaces/INugetServiceFactory.cs | 2 +- CycloneDX/Models/NugetInputModel.cs | 8 +- CycloneDX/Runner.cs | 15 ++- CycloneDX/Services/FileDiscoveryService.cs | 10 ++ CycloneDX/Services/NugetConfigFileService.cs | 101 ++++++++++++++++++ CycloneDX/Services/NugetV3Service.cs | 63 +++++++++-- CycloneDX/Services/NugetV3ServiceFactory.cs | 19 +++- Directory.Packages.props | 4 +- 15 files changed, 338 insertions(+), 27 deletions(-) create mode 100644 CycloneDX.Tests/NugetConfigFileServiceTests.cs create mode 100644 CycloneDX/Interfaces/INugetConfigFileService.cs create mode 100644 CycloneDX/Services/NugetConfigFileService.cs diff --git a/CycloneDX.Tests/FunctionalTests/FunctionalTestHelper.cs b/CycloneDX.Tests/FunctionalTests/FunctionalTestHelper.cs index 15c46613..dd48e80b 100644 --- a/CycloneDX.Tests/FunctionalTests/FunctionalTestHelper.cs +++ b/CycloneDX.Tests/FunctionalTests/FunctionalTestHelper.cs @@ -39,7 +39,8 @@ private static INugetServiceFactory CreateMockNugetServiceFactory() It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny>())) + It.IsAny>(), + It.IsAny>())) .Returns(nugetService); return mockNugetServiceFactory.Object; @@ -79,7 +80,7 @@ public static async Task Test(RunOptions options, INugetServiceFactory nuge options.SolutionOrProjectFile ??= MockUnixSupport.Path("c:/ProjectPath/Project.csproj"); options.disablePackageRestore = true; - Runner runner = new Runner(mockFileSystem, null, null, null, null, null, null, nugetService); + Runner runner = new Runner(mockFileSystem, null, null, null, null, null, null, null, nugetService); int exitCode = await runner.HandleCommandAsync(options); Assert.Equal((int)ExitCode.OK, exitCode); diff --git a/CycloneDX.Tests/FunctionalTests/Issue758.cs b/CycloneDX.Tests/FunctionalTests/Issue758.cs index 24bf8ff7..5b4c7233 100644 --- a/CycloneDX.Tests/FunctionalTests/Issue758.cs +++ b/CycloneDX.Tests/FunctionalTests/Issue758.cs @@ -37,7 +37,8 @@ public Issue758() It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny>())) + It.IsAny>(), + It.IsAny>())) .Returns(nugetService); nugetServiceFactory = mockNugetServiceFactory.Object; diff --git a/CycloneDX.Tests/Helpers.cs b/CycloneDX.Tests/Helpers.cs index b71a0329..2f35e102 100644 --- a/CycloneDX.Tests/Helpers.cs +++ b/CycloneDX.Tests/Helpers.cs @@ -103,6 +103,18 @@ public static MockFileData GetPackagesFileWithPackageReferences(IEnumerable sources) + { + var fileData = ""; + foreach (var source in sources) + { + fileData += @""; + } + fileData += ""; + return new MockFileData(fileData); + + } + public static DotnetCommandResult GetDotnetListPackagesResult(IEnumerable<(string projectName, (string packageName, string version)[] packages)> projects) { StringBuilder stdout = new StringBuilder(); diff --git a/CycloneDX.Tests/NugetConfigFileServiceTests.cs b/CycloneDX.Tests/NugetConfigFileServiceTests.cs new file mode 100644 index 00000000..2153b250 --- /dev/null +++ b/CycloneDX.Tests/NugetConfigFileServiceTests.cs @@ -0,0 +1,86 @@ +// This file is part of CycloneDX Tool for .NET +// +// Licensed under the Apache License, Version 2.0 (the “License”); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an “AS IS” BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) OWASP Foundation. All Rights Reserved. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; +using System.IO.Abstractions.TestingHelpers; +using XFS = System.IO.Abstractions.TestingHelpers.MockUnixSupport; +using CycloneDX.Models; +using CycloneDX.Services; +using System.Linq; + +namespace CycloneDX.Tests +{ + public class NugetConfigFileServiceTests + { + [Fact] + public async Task GetDotnetDependencys_ReturnsDotnetDependency() + { + var mockFileSystem = new MockFileSystem(new Dictionary + { + { XFS.Path(@"c:\Project\nuget.config"), Helpers.GetNugetConfigFileWithSources( + new List { + new NugetInputModel( "https://www.contoso.com" ) { nugetFeedName = "Contoso" } + }) + }, + }); + var configFileService = new NugetConfigFileService(mockFileSystem); + + var sources = await configFileService.GetPackageSourcesAsync(XFS.Path(@"c:\Project\nuget.config")).ConfigureAwait(true); + + Assert.Collection(sources, + item => { + Assert.Equal("https://www.contoso.com", item.nugetFeedUrl); + }); + Assert.Collection(sources, + item => { + Assert.Equal("Contoso", item.nugetFeedName); + }); + } + + [Fact] + public async Task GetDotnetDependencys_ReturnsMultipleDotnetDependencys() + { + var mockFileSystem = new MockFileSystem(new Dictionary + { + { XFS.Path(@"c:\Project\nuget.config"), Helpers.GetNugetConfigFileWithSources( + new List { + new NugetInputModel( "https://www.contoso.com" ) { nugetFeedName = "Contoso" }, + new NugetInputModel( "https://www.contoso2.com" ) { nugetFeedName = "Contoso2" }, + new NugetInputModel( "https://www.contoso3.com" ) { nugetFeedName = "Contoso3" }, + }) + }, + }); + var configFileService = new NugetConfigFileService(mockFileSystem); + + var sources = await configFileService.GetPackageSourcesAsync(XFS.Path(@"c:\Project\nuget.config")).ConfigureAwait(true); + var sortedPackages = new List(sources); + sortedPackages.OrderBy(nim => nim.nugetFeedName); + + Assert.Collection(sortedPackages, + item => Assert.Equal("https://www.contoso.com", item.nugetFeedUrl), + item => Assert.Equal("https://www.contoso2.com", item.nugetFeedUrl), + item => Assert.Equal("https://www.contoso3.com", item.nugetFeedUrl)); + Assert.Collection(sortedPackages, + item => Assert.Equal("Contoso", item.nugetFeedName), + item => Assert.Equal("Contoso2", item.nugetFeedName), + item => Assert.Equal("Contoso3", item.nugetFeedName)); + } + + } +} diff --git a/CycloneDX.Tests/ProgramTests.cs b/CycloneDX.Tests/ProgramTests.cs index 0e1ca4a3..8d0233b3 100755 --- a/CycloneDX.Tests/ProgramTests.cs +++ b/CycloneDX.Tests/ProgramTests.cs @@ -50,7 +50,7 @@ public async Task CallingCycloneDX_CreatesOutputDirectory() .Setup(s => s.GetSolutionDotnetDependencys(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new HashSet()); - Runner runner = new Runner(fileSystem: mockFileSystem, null, null, null, null, null, solutionFileService: mockSolutionFileService.Object, null); + Runner runner = new Runner(fileSystem: mockFileSystem, null, null, null, null, null, null, solutionFileService: mockSolutionFileService.Object, null); RunOptions runOptions = new RunOptions { @@ -75,7 +75,7 @@ public async Task CallingCycloneDX_WithOutputFilename_CreatesOutputFilename() .Setup(s => s.GetSolutionDotnetDependencys(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new HashSet()); - Runner runner = new Runner(fileSystem: mockFileSystem, null, null, null, null, null, solutionFileService: mockSolutionFileService.Object, null); + Runner runner = new Runner(fileSystem: mockFileSystem, null, null, null, null, null, null, solutionFileService: mockSolutionFileService.Object, null); RunOptions runOptions = new RunOptions { @@ -116,7 +116,7 @@ public async Task CallingCycloneDX_WithSolutionOrProjectFileThatDoesntExistsRetu .Setup(s => s.GetSolutionDotnetDependencys(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new HashSet()); - Runner runner = new Runner(fileSystem: mockFileSystem, null, null, null, null, null, solutionFileService: mockSolutionFileService.Object, null); + Runner runner = new Runner(fileSystem: mockFileSystem, null, null, null, null, null, null, solutionFileService: mockSolutionFileService.Object, null); RunOptions runOptions = new RunOptions { diff --git a/CycloneDX.Tests/ValidationTests.cs b/CycloneDX.Tests/ValidationTests.cs index 802cec92..72cbc1dc 100644 --- a/CycloneDX.Tests/ValidationTests.cs +++ b/CycloneDX.Tests/ValidationTests.cs @@ -41,7 +41,7 @@ public async Task Validation(string fileFormat, bool disableGitHubLicenses) mock.GetProjectDotnetDependencysAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) ).ReturnsAsync(packages); - Runner runner = new Runner(fileSystem: mockFileSystem, null, null, null, null, projectFileService: mockProjectFileService.Object, solutionFileService: null, null); + Runner runner = new Runner(fileSystem: mockFileSystem, null, null, null, null, null, projectFileService: mockProjectFileService.Object, solutionFileService: null, null); RunOptions runOptions = new RunOptions diff --git a/CycloneDX/Interfaces/INugetConfigFileService.cs b/CycloneDX/Interfaces/INugetConfigFileService.cs new file mode 100644 index 00000000..bb65c25f --- /dev/null +++ b/CycloneDX/Interfaces/INugetConfigFileService.cs @@ -0,0 +1,29 @@ +// This file is part of CycloneDX Tool for .NET +// +// Licensed under the Apache License, Version 2.0 (the “License”); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an “AS IS” BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) OWASP Foundation. All Rights Reserved. + +using System.Collections.Generic; +using System.Threading.Tasks; +using CycloneDX.Models; + +namespace CycloneDX.Interfaces +{ + public interface INugetConfigFileService + { + Task> GetPackageSourcesAsync(string configFilePath); + Task> RecursivelyGetPackageSourcesAsync(string directoryPath); + } +} diff --git a/CycloneDX/Interfaces/INugetServiceFactory.cs b/CycloneDX/Interfaces/INugetServiceFactory.cs index 8bf2c729..b48a055f 100644 --- a/CycloneDX/Interfaces/INugetServiceFactory.cs +++ b/CycloneDX/Interfaces/INugetServiceFactory.cs @@ -11,6 +11,6 @@ namespace CycloneDX.Interfaces { public interface INugetServiceFactory { - INugetService Create(RunOptions option, IFileSystem fileSystem, IGithubService githubService, List packageCachePaths); + INugetService Create(RunOptions option, IFileSystem fileSystem, IGithubService githubService, List packageCachePaths, HashSet nugetInputModels); } } diff --git a/CycloneDX/Models/NugetInputModel.cs b/CycloneDX/Models/NugetInputModel.cs index 9487a3af..e0089caa 100644 --- a/CycloneDX/Models/NugetInputModel.cs +++ b/CycloneDX/Models/NugetInputModel.cs @@ -20,7 +20,7 @@ namespace CycloneDX.Models public static class NugetInputFactory { public static NugetInputModel Create(string baseUrl, string baseUrlUserName, string baseUrlUserPassword, - bool isPasswordClearText) + bool isPasswordClearText, string feedName = "Unnamed Source" ) { if (string.IsNullOrEmpty(baseUrl)) { @@ -29,7 +29,7 @@ public static NugetInputModel Create(string baseUrl, string baseUrlUserName, str if (!string.IsNullOrEmpty(baseUrlUserName) && !string.IsNullOrEmpty(baseUrlUserPassword)) { - return new NugetInputModel(baseUrl, baseUrlUserName, baseUrlUserPassword, isPasswordClearText); + return new NugetInputModel(baseUrl, baseUrlUserName, baseUrlUserPassword, isPasswordClearText, feedName); } return new NugetInputModel(baseUrl); @@ -39,6 +39,7 @@ public static NugetInputModel Create(string baseUrl, string baseUrlUserName, str public class NugetInputModel { + public string nugetFeedName { get; set; } public string nugetFeedUrl { get; set; } public string nugetUsername { get; set; } public string nugetPassword { get; set; } @@ -50,12 +51,13 @@ public NugetInputModel(string baseUrl) } public NugetInputModel(string baseUrl, string baseUrlUserName, string baseUrlUserPassword, - bool isPasswordClearText) + bool isPasswordClearText, string feedName) { nugetFeedUrl = baseUrl; nugetUsername = baseUrlUserName; nugetPassword = baseUrlUserPassword; IsPasswordClearText = isPasswordClearText; + nugetFeedName = nugetFeedName; } } } diff --git a/CycloneDX/Runner.cs b/CycloneDX/Runner.cs index 10c0e23e..26e98836 100644 --- a/CycloneDX/Runner.cs +++ b/CycloneDX/Runner.cs @@ -37,6 +37,7 @@ public class Runner readonly IDotnetCommandService dotnetCommandService; readonly IDotnetUtilsService dotnetUtilsService; readonly IPackagesFileService packagesFileService; + readonly INugetConfigFileService configFileService; readonly IProjectFileService projectFileService; readonly ISolutionFileService solutionFileService; readonly INugetServiceFactory nugetServiceFactory; @@ -46,6 +47,7 @@ public Runner(IFileSystem fileSystem, IProjectAssetsFileService projectAssetsFileService, IDotnetUtilsService dotnetUtilsService, IPackagesFileService packagesFileService, + INugetConfigFileService configFileService, IProjectFileService projectFileService, ISolutionFileService solutionFileService, INugetServiceFactory nugetServiceFactory) @@ -55,11 +57,12 @@ public Runner(IFileSystem fileSystem, projectAssetsFileService ??= new ProjectAssetsFileService(this.fileSystem, () => new AssetFileReader()); this.dotnetUtilsService = dotnetUtilsService ?? new DotnetUtilsService(this.fileSystem, this.dotnetCommandService); this.packagesFileService = packagesFileService ?? new PackagesFileService(this.fileSystem); + this.configFileService = configFileService ?? new NugetConfigFileService(this.fileSystem); this.projectFileService = projectFileService ?? new ProjectFileService(this.fileSystem, this.dotnetUtilsService, this.packagesFileService, projectAssetsFileService); this.solutionFileService = solutionFileService ?? new SolutionFileService(this.fileSystem, this.projectFileService); this.nugetServiceFactory = nugetServiceFactory ?? new NugetV3ServiceFactory(); } - public Runner() : this(null, null, null, null, null, null, null, null) { } + public Runner() : this(null, null, null, null, null, null, null, null, null) { } public async Task HandleCommandAsync(RunOptions options) { @@ -130,9 +133,8 @@ public async Task HandleCommandAsync(RunOptions options) } } - var nugetService = nugetServiceFactory.Create(options, fileSystem, githubService, packageCachePathsResult.Result); - var packages = new HashSet(); + var sources = new HashSet(); // determine what we are analyzing and do the analysis var fullSolutionOrProjectFilePath = this.fileSystem.Path.GetFullPath(SolutionOrProjectFile); @@ -170,6 +172,7 @@ public async Task HandleCommandAsync(RunOptions options) return (int)ExitCode.InvalidOptions; } packages = await solutionFileService.GetSolutionDotnetDependencys(fullSolutionOrProjectFilePath, baseIntermediateOutputPath, excludetestprojects, framework, runtime).ConfigureAwait(false); + sources = await configFileService.RecursivelyGetPackageSourcesAsync( fileSystem.Path.GetDirectoryName(fullSolutionOrProjectFilePath)).ConfigureAwait(false); topLevelComponent.Name = fileSystem.Path.GetFileNameWithoutExtension(SolutionOrProjectFile); } else if (Utils.IsSupportedProjectType(SolutionOrProjectFile) && scanProjectReferences) @@ -180,6 +183,7 @@ public async Task HandleCommandAsync(RunOptions options) return (int)ExitCode.InvalidOptions; } packages = await projectFileService.RecursivelyGetProjectDotnetDependencysAsync(fullSolutionOrProjectFilePath, baseIntermediateOutputPath, excludetestprojects, framework, runtime).ConfigureAwait(false); + sources = await configFileService.RecursivelyGetPackageSourcesAsync(fileSystem.Path.GetDirectoryName(fullSolutionOrProjectFilePath)).ConfigureAwait(false); topLevelComponent.Name = fileSystem.Path.GetFileNameWithoutExtension(SolutionOrProjectFile); } else if (Utils.IsSupportedProjectType(SolutionOrProjectFile)) @@ -190,6 +194,7 @@ public async Task HandleCommandAsync(RunOptions options) return (int)ExitCode.InvalidOptions; } packages = await projectFileService.GetProjectDotnetDependencysAsync(fullSolutionOrProjectFilePath, baseIntermediateOutputPath, excludetestprojects, framework, runtime).ConfigureAwait(false); + sources = await configFileService.RecursivelyGetPackageSourcesAsync(fileSystem.Path.GetDirectoryName(fullSolutionOrProjectFilePath)).ConfigureAwait(false); topLevelComponent.Name = fileSystem.Path.GetFileNameWithoutExtension(SolutionOrProjectFile); } else if (fileSystem.Path.GetFileName(SolutionOrProjectFile).ToLowerInvariant().Equals("packages.config", StringComparison.OrdinalIgnoreCase)) @@ -200,11 +205,13 @@ public async Task HandleCommandAsync(RunOptions options) return (int)ExitCode.InvalidOptions; } packages = await packagesFileService.GetDotnetDependencysAsync(fullSolutionOrProjectFilePath).ConfigureAwait(false); + sources = await configFileService.RecursivelyGetPackageSourcesAsync(fileSystem.Path.GetDirectoryName(fullSolutionOrProjectFilePath)).ConfigureAwait(false); topLevelComponent.Name = fileSystem.Path.GetDirectoryName(fullSolutionOrProjectFilePath); } else if (fileSystem.Directory.Exists(fullSolutionOrProjectFilePath)) { packages = await packagesFileService.RecursivelyGetDotnetDependencysAsync(fullSolutionOrProjectFilePath).ConfigureAwait(false); + sources = await configFileService.RecursivelyGetPackageSourcesAsync(fileSystem.Path.GetDirectoryName(fullSolutionOrProjectFilePath)).ConfigureAwait(false); topLevelComponent.Name = fileSystem.Path.GetDirectoryName(fullSolutionOrProjectFilePath); } else @@ -219,7 +226,7 @@ public async Task HandleCommandAsync(RunOptions options) } await Console.Out.WriteLineAsync($"Found {packages.Count()} packages"); - + var nugetService = nugetServiceFactory.Create(options, fileSystem, githubService, packageCachePathsResult.Result, sources); if (!string.IsNullOrEmpty(setName)) { diff --git a/CycloneDX/Services/FileDiscoveryService.cs b/CycloneDX/Services/FileDiscoveryService.cs index 70f8129f..a9b44b40 100644 --- a/CycloneDX/Services/FileDiscoveryService.cs +++ b/CycloneDX/Services/FileDiscoveryService.cs @@ -40,5 +40,15 @@ public IEnumerable GetPackagesConfigFiles(string directory) return _fileSystem.Directory.GetFiles(directory, "packages.config", SearchOption.AllDirectories); } + /// + /// Recursively searches a directory for nuget.config files. + /// + /// Directory path to search + /// List of full file paths + public IEnumerable GetNugetConfigFiles(string directory) + { + return _fileSystem.Directory.GetFiles(directory, "nuget.config", SearchOption.AllDirectories); + } + } } diff --git a/CycloneDX/Services/NugetConfigFileService.cs b/CycloneDX/Services/NugetConfigFileService.cs new file mode 100644 index 00000000..c4188e19 --- /dev/null +++ b/CycloneDX/Services/NugetConfigFileService.cs @@ -0,0 +1,101 @@ +// This file is part of CycloneDX Tool for .NET +// +// Licensed under the Apache License, Version 2.0 (the “License”); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an “AS IS” BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) OWASP Foundation. All Rights Reserved. + +using System.Collections.Generic; +using System.Xml; +using System.IO; +using System.IO.Abstractions; +using System.Threading.Tasks; +using CycloneDX.Interfaces; +using CycloneDX.Models; +using System.Linq; +using System; + +namespace CycloneDX.Services +{ + public class NugetConfigFileService : INugetConfigFileService + { + private XmlReaderSettings _xmlReaderSettings = new XmlReaderSettings + { + Async = true + }; + + private IFileSystem _fileSystem; + private FileDiscoveryService _fileDiscoveryService; + + public NugetConfigFileService(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + _fileDiscoveryService = new FileDiscoveryService(fileSystem); + } + + /// + /// Analyzes a single nuget.config file for package sources. + /// + /// + /// + public async Task> GetPackageSourcesAsync(string configFilePath) + { + var sources = new HashSet(); + using (StreamReader fileReader = _fileSystem.File.OpenText(configFilePath)) + { + using (XmlReader reader = XmlReader.Create(fileReader, _xmlReaderSettings)) + { + while (await reader.ReadAsync().ConfigureAwait(false)) + { + if (reader.IsStartElement() && reader.Name == "add") + { + string packageSource = reader["value"]; + if (packageSource.StartsWith("https://api.nuget.org/v3")) + { + // normalize default URL + packageSource = "https://api.nuget.org/v3/index.json"; + } + var newSource = new NugetInputModel(packageSource); + newSource.nugetFeedName = reader["key"]; + + // TODO - user, password + + await Console.Out.WriteLineAsync($"\tFound Package Source:{newSource.nugetFeedName}"); + sources.Add(newSource); + } + } + } + } + return sources; + } + + /// + /// Recursively searches a directory and analyzes all packages.config files for NuGet package references. + /// + /// + /// + public async Task> RecursivelyGetPackageSourcesAsync(string directoryPath) + { + var sources = new HashSet(); + var configFiles = _fileDiscoveryService.GetNugetConfigFiles(directoryPath); + + foreach (var configFile in configFiles) + { + var newsources = await GetPackageSourcesAsync(configFile).ConfigureAwait(false); + sources.UnionWith(newsources); + } + + return sources; + } + } +} diff --git a/CycloneDX/Services/NugetV3Service.cs b/CycloneDX/Services/NugetV3Service.cs index 6fa4bcc5..402065c7 100644 --- a/CycloneDX/Services/NugetV3Service.cs +++ b/CycloneDX/Services/NugetV3Service.cs @@ -40,7 +40,7 @@ namespace CycloneDX.Services /// public class NugetV3Service : INugetService { - private readonly SourceRepository _sourceRepository; + private readonly List _sourceRepositories; private readonly SourceCacheContext _sourceCacheContext; private readonly CancellationToken _cancellationToken; private readonly ILogger _logger; @@ -56,7 +56,7 @@ public class NugetV3Service : INugetService private const string _sha512Extension = ".nupkg.sha512"; public NugetV3Service( - NugetInputModel nugetInput, + HashSet nugetInputModels, IFileSystem fileSystem, List packageCachePaths, IGithubService githubService, @@ -70,7 +70,22 @@ bool disableHashComputation _disableHashComputation = disableHashComputation; _logger = logger; - _sourceRepository = SetupNugetRepository(nugetInput); + _sourceRepositories = new List(); + if (nugetInputModels != null) + { + foreach (var nugetInput in nugetInputModels) + { + var repo = SetupNugetRepository(nugetInput); + if (repo != null) + { + _sourceRepositories.Add(repo); + } + } + } + else + { + _sourceRepositories.Add(SetupNugetRepository(null)); + } _sourceCacheContext = new SourceCacheContext(); _cancellationToken = CancellationToken.None; } @@ -167,11 +182,15 @@ public async Task GetComponentAsync(string name, string version, Comp if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(version)) { return null; } // https://docs.microsoft.com/en-us/nuget/reference/nuget-client-sdk - Download a package - var resource = await _sourceRepository.GetResourceAsync(); + var resources = new List(); + foreach (var repo in _sourceRepositories) + { + resources.Add( await repo.GetResourceAsync() ); + } var component = SetupComponent(name, version, scope); var nuspecFilename = GetCachedNuspecFilename(name, version); - var nuspecModel = await GetNuspec(name, version, nuspecFilename, resource).ConfigureAwait(false); + var nuspecModel = await GetNuspec(name, version, nuspecFilename, resources).ConfigureAwait(false); if (nuspecModel.hashBytes != null) { var hex = BitConverter.ToString(nuspecModel.hashBytes).Replace("-", string.Empty); @@ -306,15 +325,43 @@ private static Component SetupComponentProperties(Component component, NuspecMod } private async Task GetNuspec(string name, string version, string nuspecFilename, - FindPackageByIdResource resource) + List resources) { var nuspecModel = new NuspecModel(); if (nuspecFilename == null) { var packageVersion = new NuGetVersion(version); await using MemoryStream packageStream = new MemoryStream(); - await resource.CopyNupkgToStreamAsync(name, packageVersion, packageStream, _sourceCacheContext, - _logger, _cancellationToken); + Console.WriteLine($"Looking for {name} {version}"); + bool foundResource = false; + foreach (var resource in resources) + { + try + { + await resource.CopyNupkgToStreamAsync(name, packageVersion, packageStream, _sourceCacheContext, + _logger, _cancellationToken); + if ((packageStream != null) + && (packageStream.Length == 0)) + { + continue; + } + if ((packageStream != null) + && (packageStream.Length > 0)) + { + foundResource = true; + Console.WriteLine($" found"); + break; + } + } + catch (Exception ex) + { + Console.WriteLine($" Error looking for package {name} {version}: {ex.ToString()}"); + } + } + if (!foundResource) + { + throw new Exception( $"Did not find package {name} {version}"); + } using PackageArchiveReader packageReader = new PackageArchiveReader(packageStream); nuspecModel.nuspecReader = await packageReader.GetNuspecReaderAsync(_cancellationToken); diff --git a/CycloneDX/Services/NugetV3ServiceFactory.cs b/CycloneDX/Services/NugetV3ServiceFactory.cs index 46479336..abc7bf8d 100644 --- a/CycloneDX/Services/NugetV3ServiceFactory.cs +++ b/CycloneDX/Services/NugetV3ServiceFactory.cs @@ -6,16 +6,31 @@ using System.Threading.Tasks; using CycloneDX.Interfaces; using CycloneDX.Models; +using JetBrains.Annotations; +using NuGet.Common; namespace CycloneDX.Services { public class NugetV3ServiceFactory : INugetServiceFactory { - public INugetService Create(RunOptions option, IFileSystem fileSystem, IGithubService githubService, List packageCachePaths ) + public INugetService Create(RunOptions option, IFileSystem fileSystem, IGithubService githubService, List packageCachePaths, HashSet nugetInputModels) { var nugetLogger = new NuGet.Common.NullLogger(); var nugetInput = NugetInputFactory.Create(option.baseUrl, option.baseUrlUserName, option.baseUrlUSP, option.isPasswordClearText); - return new NugetV3Service(nugetInput, fileSystem, packageCachePaths, githubService, nugetLogger, option.disableHashComputation); + + if (nugetInputModels == null) + { + nugetInputModels = new HashSet(); + } + if (nugetInput != null) + { + nugetInputModels.Add(nugetInput); + } + if (!nugetInputModels.Any()) + { + nugetInputModels.Add(new NugetInputModel("https://api.nuget.org/v3/index.json")); + } + return new NugetV3Service(nugetInputModels, fileSystem, packageCachePaths, githubService, nugetLogger, option.disableHashComputation); } } } diff --git a/Directory.Packages.props b/Directory.Packages.props index 1c7c2074..1b33329d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,8 +7,8 @@ - - + + From f8ac92eb19f11b004265e1def07e65f83af4ed5f Mon Sep 17 00:00:00 2001 From: Georg Rottensteiner Date: Tue, 21 Jan 2025 15:02:03 +0100 Subject: [PATCH 07/10] Fix Codacy "issues" Signed-off-by: Georg Rottensteiner --- CycloneDX/Models/NugetInputModel.cs | 12 ++++++++++-- CycloneDX/Runner.cs | 2 +- CycloneDX/Services/NugetConfigFileService.cs | 6 +++--- CycloneDX/Services/NugetV3Service.cs | 11 ++++++++++- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/CycloneDX/Models/NugetInputModel.cs b/CycloneDX/Models/NugetInputModel.cs index e0089caa..76e8b256 100644 --- a/CycloneDX/Models/NugetInputModel.cs +++ b/CycloneDX/Models/NugetInputModel.cs @@ -15,12 +15,20 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (c) OWASP Foundation. All Rights Reserved. +using System.Transactions; + namespace CycloneDX.Models { public static class NugetInputFactory { public static NugetInputModel Create(string baseUrl, string baseUrlUserName, string baseUrlUserPassword, - bool isPasswordClearText, string feedName = "Unnamed Source" ) + bool isPasswordClearText) + { + return Create(baseUrl, baseUrlUserName, baseUrlUserPassword, isPasswordClearText, "Unnamed Source"); + } + + public static NugetInputModel Create(string baseUrl, string baseUrlUserName, string baseUrlUserPassword, + bool isPasswordClearText, string feedName ) { if (string.IsNullOrEmpty(baseUrl)) { @@ -57,7 +65,7 @@ public NugetInputModel(string baseUrl, string baseUrlUserName, string baseUrlUse nugetUsername = baseUrlUserName; nugetPassword = baseUrlUserPassword; IsPasswordClearText = isPasswordClearText; - nugetFeedName = nugetFeedName; + nugetFeedName = feedName; } } } diff --git a/CycloneDX/Runner.cs b/CycloneDX/Runner.cs index 26e98836..24f8667e 100644 --- a/CycloneDX/Runner.cs +++ b/CycloneDX/Runner.cs @@ -134,7 +134,7 @@ public async Task HandleCommandAsync(RunOptions options) } var packages = new HashSet(); - var sources = new HashSet(); + HashSet sources = null; // determine what we are analyzing and do the analysis var fullSolutionOrProjectFilePath = this.fileSystem.Path.GetFullPath(SolutionOrProjectFile); diff --git a/CycloneDX/Services/NugetConfigFileService.cs b/CycloneDX/Services/NugetConfigFileService.cs index c4188e19..db001a8c 100644 --- a/CycloneDX/Services/NugetConfigFileService.cs +++ b/CycloneDX/Services/NugetConfigFileService.cs @@ -29,13 +29,13 @@ namespace CycloneDX.Services { public class NugetConfigFileService : INugetConfigFileService { - private XmlReaderSettings _xmlReaderSettings = new XmlReaderSettings + private readonly XmlReaderSettings _xmlReaderSettings = new XmlReaderSettings { Async = true }; - private IFileSystem _fileSystem; - private FileDiscoveryService _fileDiscoveryService; + private readonly IFileSystem _fileSystem; + private readonly FileDiscoveryService _fileDiscoveryService; public NugetConfigFileService(IFileSystem fileSystem) { diff --git a/CycloneDX/Services/NugetV3Service.cs b/CycloneDX/Services/NugetV3Service.cs index 402065c7..c7fe4e2c 100644 --- a/CycloneDX/Services/NugetV3Service.cs +++ b/CycloneDX/Services/NugetV3Service.cs @@ -40,6 +40,15 @@ namespace CycloneDX.Services /// public class NugetV3Service : INugetService { + public class PackageNotFoundException : Exception + { + public PackageNotFoundException(string message) : base(message) { } + + public PackageNotFoundException(string message, Exception innerException) : base(message, innerException) { } + } + + + private readonly List _sourceRepositories; private readonly SourceCacheContext _sourceCacheContext; private readonly CancellationToken _cancellationToken; @@ -360,7 +369,7 @@ await resource.CopyNupkgToStreamAsync(name, packageVersion, packageStream, _sour } if (!foundResource) { - throw new Exception( $"Did not find package {name} {version}"); + throw new PackageNotFoundException( $"Did not find package {name} {version}"); } using PackageArchiveReader packageReader = new PackageArchiveReader(packageStream); From 784b7f8687a1b814af93b7964e3f93e5eec529e5 Mon Sep 17 00:00:00 2001 From: Georg Rottensteiner Date: Mon, 10 Feb 2025 06:17:05 +0100 Subject: [PATCH 08/10] Fixed StringBuilder recommendation for unit test Signed-off-by: Georg Rottensteiner --- CycloneDX.Tests/Helpers.cs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/CycloneDX.Tests/Helpers.cs b/CycloneDX.Tests/Helpers.cs index 2f35e102..c6401697 100644 --- a/CycloneDX.Tests/Helpers.cs +++ b/CycloneDX.Tests/Helpers.cs @@ -104,14 +104,20 @@ public static MockFileData GetPackagesFileWithPackageReferences(IEnumerable sources) - { - var fileData = ""; + { + var sb = new StringBuilder(); + + sb.Append( "" ); foreach (var source in sources) - { - fileData += @""; - } - fileData += ""; - return new MockFileData(fileData); + { + sb.Append(@""); + } + sb.Append( "" ); + return new MockFileData(sb.ToString()); } From 3f8ff399370c4a2825b42d48e18c322f286bae63 Mon Sep 17 00:00:00 2001 From: Georg Rottensteiner Date: Mon, 19 May 2025 14:20:49 +0200 Subject: [PATCH 09/10] Fix potential nullreference access --- CycloneDX/Services/GithubService.cs | 2 +- CycloneDX/Services/NugetConfigFileService.cs | 200 +++++++++---------- 2 files changed, 101 insertions(+), 101 deletions(-) diff --git a/CycloneDX/Services/GithubService.cs b/CycloneDX/Services/GithubService.cs index ca6780ac..bbbfa5aa 100644 --- a/CycloneDX/Services/GithubService.cs +++ b/CycloneDX/Services/GithubService.cs @@ -134,7 +134,7 @@ private async Task GetLicenseForCacheAsync(string licenseUrl) } // License is not on GitHub, we need to abort - if (!match.Success) return null; + if (match == null || !match.Success) { return null; } var repositoryId = match.Groups["repositoryId"].Value; string refSpec = null; diff --git a/CycloneDX/Services/NugetConfigFileService.cs b/CycloneDX/Services/NugetConfigFileService.cs index a87ea480..ec3cfdc7 100644 --- a/CycloneDX/Services/NugetConfigFileService.cs +++ b/CycloneDX/Services/NugetConfigFileService.cs @@ -1,101 +1,101 @@ -// This file is part of CycloneDX Tool for .NET -// -// Licensed under the Apache License, Version 2.0 (the “License”); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an “AS IS” BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 -// Copyright (c) OWASP Foundation. All Rights Reserved. - -using System.Collections.Generic; -using System.Xml; -using System.IO; -using System.IO.Abstractions; -using System.Threading.Tasks; -using CycloneDX.Interfaces; -using CycloneDX.Models; -using System.Linq; -using System; - -namespace CycloneDX.Services -{ - public class NugetConfigFileService : INugetConfigFileService - { - private readonly XmlReaderSettings _xmlReaderSettings = new XmlReaderSettings - { - Async = true - }; - - private readonly IFileSystem _fileSystem; - private readonly FileDiscoveryService _fileDiscoveryService; - - public NugetConfigFileService(IFileSystem fileSystem) - { - _fileSystem = fileSystem; - _fileDiscoveryService = new FileDiscoveryService(fileSystem); - } - - /// - /// Analyzes a single nuget.config file for package sources. - /// - /// - /// - public async Task> GetPackageSourcesAsync(string configFilePath) - { - var sources = new HashSet(); - using (StreamReader fileReader = _fileSystem.File.OpenText(configFilePath)) - { - using (XmlReader reader = XmlReader.Create(fileReader, _xmlReaderSettings)) - { - while (await reader.ReadAsync().ConfigureAwait(false)) - { - if (reader.IsStartElement() && reader.Name == "add") - { - string packageSource = reader["value"]; - if (packageSource.StartsWith("https://api.nuget.org/v3")) - { - // normalize default URL - packageSource = "https://api.nuget.org/v3/index.json"; - } - var newSource = new NugetInputModel(packageSource); - newSource.nugetFeedName = reader["key"]; - +// This file is part of CycloneDX Tool for .NET +// +// Licensed under the Apache License, Version 2.0 (the “License”); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an “AS IS” BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) OWASP Foundation. All Rights Reserved. + +using System.Collections.Generic; +using System.Xml; +using System.IO; +using System.IO.Abstractions; +using System.Threading.Tasks; +using CycloneDX.Interfaces; +using CycloneDX.Models; +using System.Linq; +using System; + +namespace CycloneDX.Services +{ + public class NugetConfigFileService : INugetConfigFileService + { + private readonly XmlReaderSettings _xmlReaderSettings = new XmlReaderSettings + { + Async = true + }; + + private readonly IFileSystem _fileSystem; + private readonly FileDiscoveryService _fileDiscoveryService; + + public NugetConfigFileService(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + _fileDiscoveryService = new FileDiscoveryService(fileSystem); + } + + /// + /// Analyzes a single nuget.config file for package sources. + /// + /// + /// + public async Task> GetPackageSourcesAsync(string configFilePath) + { + var sources = new HashSet(); + using (StreamReader fileReader = _fileSystem.File.OpenText(configFilePath)) + { + using (XmlReader reader = XmlReader.Create(fileReader, _xmlReaderSettings)) + { + while (await reader.ReadAsync().ConfigureAwait(false)) + { + if (reader.IsStartElement() && reader.Name == "add") + { + string packageSource = reader["value"]; + if (packageSource.StartsWith("https://api.nuget.org/v3")) + { + // normalize default URL + packageSource = "https://api.nuget.org/v3/index.json"; + } + var newSource = new NugetInputModel(packageSource); + newSource.nugetFeedName = reader["key"]; + // TODO - user, password - - await Console.Out.WriteLineAsync($"\tFound Package Source:{newSource.nugetFeedName}"); - sources.Add(newSource); - } - } - } - } - return sources; - } - - /// - /// Recursively searches a directory and analyzes all packages.config files for NuGet package references. - /// - /// - /// - public async Task> RecursivelyGetPackageSourcesAsync(string directoryPath) - { - var sources = new HashSet(); - var configFiles = _fileDiscoveryService.GetNugetConfigFiles(directoryPath); - - foreach (var configFile in configFiles) - { - var newsources = await GetPackageSourcesAsync(configFile).ConfigureAwait(false); - sources.UnionWith(newsources); - } - - return sources; - } - } -} + + await Console.Out.WriteLineAsync($"\tFound Package Source:{newSource.nugetFeedName}"); + sources.Add(newSource); + } + } + } + } + return sources; + } + + /// + /// Recursively searches a directory and analyzes all packages.config files for NuGet package references. + /// + /// + /// + public async Task> RecursivelyGetPackageSourcesAsync(string directoryPath) + { + var sources = new HashSet(); + var configFiles = _fileDiscoveryService.GetNugetConfigFiles(directoryPath); + + foreach (var configFile in configFiles) + { + var newsources = await GetPackageSourcesAsync(configFile).ConfigureAwait(false); + sources.UnionWith(newsources); + } + + return sources; + } + } +} From 827f139e463a4ec0741bf00b11ea4262705bcca9 Mon Sep 17 00:00:00 2001 From: Georg Rottensteiner Date: Tue, 19 Aug 2025 14:30:29 +0200 Subject: [PATCH 10/10] Deny parallel build, runs into The process cannot access the file obj\DotnetToolSettings.xml issues otherwise --- CycloneDX/CycloneDX.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/CycloneDX/CycloneDX.csproj b/CycloneDX/CycloneDX.csproj index 5eb75cb1..93ee02b9 100644 --- a/CycloneDX/CycloneDX.csproj +++ b/CycloneDX/CycloneDX.csproj @@ -11,6 +11,7 @@ dotnet-CycloneDX <_SkipUpgradeNetAnalyzersNuGetWarning>true net8.0;net7.0;net6.0 + false Major