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.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..c6401697 100644 --- a/CycloneDX.Tests/Helpers.cs +++ b/CycloneDX.Tests/Helpers.cs @@ -103,6 +103,24 @@ public static MockFileData GetPackagesFileWithPackageReferences(IEnumerable sources) + { + var sb = new StringBuilder(); + + sb.Append( "" ); + foreach (var source in sources) + { + sb.Append(@""); + } + sb.Append( "" ); + return new MockFileData(sb.ToString()); + + } + 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/CycloneDX.csproj b/CycloneDX/CycloneDX.csproj index d113aaa3..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 @@ -38,6 +39,7 @@ + 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..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) + { + 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)) { @@ -29,7 +37,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 +47,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 +59,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 = feedName; } } } diff --git a/CycloneDX/Runner.cs b/CycloneDX/Runner.cs index 10c0e23e..24f8667e 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(); + HashSet sources = null; // 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/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 new file mode 100644 index 00000000..e59943bc --- /dev/null +++ b/CycloneDX/Services/NugetConfigFileService.cs @@ -0,0 +1,102 @@ +// 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) + { + Console.WriteLine( $"Reading NuGet config file: {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) + { + Console.WriteLine($"Scanning for nuget.config files in {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..c7fe4e2c 100644 --- a/CycloneDX/Services/NugetV3Service.cs +++ b/CycloneDX/Services/NugetV3Service.cs @@ -40,7 +40,16 @@ namespace CycloneDX.Services /// public class NugetV3Service : INugetService { - private readonly SourceRepository _sourceRepository; + 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; private readonly ILogger _logger; @@ -56,7 +65,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 +79,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 +191,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 +334,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 PackageNotFoundException( $"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..b0ddee44 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,10 +7,11 @@ - - + + +