diff --git a/src/Microsoft.ComponentDetection.Detectors/conda/CondaDependencyResolver.cs b/src/Microsoft.ComponentDetection.Detectors/conda/CondaDependencyResolver.cs index c84637640..45c49858a 100644 --- a/src/Microsoft.ComponentDetection.Detectors/conda/CondaDependencyResolver.cs +++ b/src/Microsoft.ComponentDetection.Detectors/conda/CondaDependencyResolver.cs @@ -18,7 +18,7 @@ public static class CondaDependencyResolver /// The full condaLock object. /// The SingleFileComponentRecorder. public static void RecordDependencyGraphFromFile(CondaLock condaLock, ISingleFileComponentRecorder singleFileComponentRecorder) - => GetPackages(condaLock).ForEach(package => RegisterPackageWithDependencies(package, null, condaLock, singleFileComponentRecorder)); + => GetPackages(condaLock).ForEach(package => RegisterPackageWithDependencies(package, null, condaLock, singleFileComponentRecorder, [])); /// /// Updates all registered packages that don't have any ancestors. @@ -60,7 +60,8 @@ public static void UpdateDirectlyReferencedPackages(ISingleFileComponentRecorder /// The id of the parent package. /// The full condaLock object. /// The SingleFileComponentRecorder. - private static void RegisterPackageWithDependencies(CondaPackage package, string parentId, CondaLock condaLock, ISingleFileComponentRecorder singleFileComponentRecorder) + /// The component ids in the current dependency path. + private static void RegisterPackageWithDependencies(CondaPackage package, string parentId, CondaLock condaLock, ISingleFileComponentRecorder singleFileComponentRecorder, HashSet currentPath) { if (package == null) { @@ -72,13 +73,22 @@ private static void RegisterPackageWithDependencies(CondaPackage package, string //// Register the package itself. RegisterPackage(component, parentId, false, singleFileComponentRecorder); + //// Conda lockfiles can contain dependency cycles; retain the edge above but do not traverse the same path indefinitely. + if (!currentPath.Add(component.Id)) + { + return; + } + //// Register all dependencies of the package. package.Dependencies.Keys.ToList().ForEach(dependency => RegisterPackageWithDependencies( condaLock?.Package.FirstOrDefault(condaPackage => condaPackage.Name == dependency && condaPackage.Platform == package.Platform), component.Id, condaLock, - singleFileComponentRecorder)); + singleFileComponentRecorder, + currentPath)); + + currentPath.Remove(component.Id); } /// diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/CondaLockComponentDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/CondaLockComponentDetectorTests.cs index 4902b205a..35223105c 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/CondaLockComponentDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/CondaLockComponentDetectorTests.cs @@ -104,6 +104,48 @@ public async Task CondaComponentDetector_TestCondaLockFileAsync() detectedComponents.Should().HaveCount(4); } + [TestMethod] + public async Task CondaComponentDetector_CircularDependenciesDoNotOverflowAsync() + { + var condaLockContent = +@"version: 1 +metadata: + platforms: + - linux-64 +package: +- name: python + version: 3.12.13 + manager: conda + platform: linux-64 + dependencies: + pip: '' + category: main + optional: false +- name: pip + version: 26.2.1 + manager: conda + platform: linux-64 + dependencies: + python: '' + category: main + optional: false +"; + + var (scanResult, componentRecorder) = await this.detectorTestUtility + .WithFile("conda-lock.yml", condaLockContent) + .ExecuteDetectorAsync(); + + var detectedComponents = componentRecorder.GetDetectedComponents(); + var dependencyGraph = componentRecorder.GetDependencyGraphsByLocation().Values.First(); + var pythonId = detectedComponents.Single(component => component.Component is CondaComponent { Name: "python" }).Component.Id; + var pipId = detectedComponents.Single(component => component.Component is PipComponent { Name: "pip" }).Component.Id; + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + detectedComponents.Should().HaveCount(2); + dependencyGraph.GetDependenciesForComponent(pythonId).Should().Contain(pipId); + dependencyGraph.GetDependenciesForComponent(pipId).Should().Contain(pythonId); + } + private void AssertCondaLockComponentNameAndVersion(IEnumerable detectedComponents, string name, string version) { detectedComponents.SingleOrDefault(c =>