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 @@
-
-
+
+
+