From b6c1239be74847f05f9a0e06f2de28228e817aad Mon Sep 17 00:00:00 2001 From: RKS Date: Wed, 16 Sep 2026 23:41:19 -0400 Subject: [PATCH 1/2] Ignore link and file versions in Pnpm 6 package dependencies In the PNPM v6 lockfile format, package dependencies may reference local or workspace packages using link: or file: version paths. Previously, Pnpm6Detector only ignored file: packages in top-level package keys and did not check for local dependencies (file: or link:) when traversing package.Dependencies. This led to KeyNotFoundException lookups in the components map and failed dependency graph construction. This change: 1. Skips both file: and link: dependency paths when discovering packages in yaml.Packages. 2. Skips local package dependencies (file: and link:) when building dependency edges. 3. Bumps PnpmComponentDetectorFactory.Version from 8 to 9. --- .../pnpm/Pnpm6Detector.cs | 10 +++- .../pnpm/PnpmComponentDetectorFactory.cs | 2 +- .../PnpmDetectorTests.cs | 47 +++++++++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6Detector.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6Detector.cs index d915c6dea..7cfe0f968 100644 --- a/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6Detector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6Detector.cs @@ -23,10 +23,10 @@ public void RecordDependencyGraphFromFile(string yamlFileContent, ISingleFileCom // This includes all directly and transitively referenced dependencies. foreach (var (pnpmDependencyPath, package) in yaml.Packages ?? Enumerable.Empty>()) { - // Ignore "file:" as these are local packages. + // Ignore "file:" and "link:" as these are local packages. // Such local packages should only be referenced at the top level (via ProcessDependencyList) which also skips them or from other local packages (which this skips). // There should be no cases where a non-local package references a local package, so skipping them here should not result in failed lookups below when adding all the graph references. - if (pnpmDependencyPath.StartsWith(PnpmConstants.PnpmFileDependencyPath)) + if (pnpmDependencyPath.StartsWith(PnpmConstants.PnpmFileDependencyPath) || pnpmDependencyPath.StartsWith(PnpmConstants.PnpmLinkDependencyPath)) { continue; } @@ -46,6 +46,12 @@ public void RecordDependencyGraphFromFile(string yamlFileContent, ISingleFileCom { foreach (var (name, version) in package.Dependencies ?? Enumerable.Empty>()) { + // Ignore local packages. + if (this.pnpmParsingUtilities.IsLocalDependency(new KeyValuePair(name, version))) + { + continue; + } + var pnpmDependencyPath = this.pnpmParsingUtilities.ReconstructPnpmDependencyPath(name, version); // If this lookup fails, then pnpmDependencyPath was either parsed incorrectly or constructed incorrectly. diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetectorFactory.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetectorFactory.cs index 796016b73..9e8937f86 100644 --- a/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetectorFactory.cs +++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetectorFactory.cs @@ -42,7 +42,7 @@ public PnpmComponentDetectorFactory( public override IEnumerable SupportedComponentTypes { get; } = [ComponentType.Npm]; - public override int Version { get; } = 8; + public override int Version { get; } = 9; public override bool NeedsAutomaticRootDependencyCalculation => true; diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs index de0f977e2..ea6a224d2 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs @@ -580,6 +580,53 @@ public async Task TestPnpmDetector_V6_RenamedAsync() } } + [TestMethod] + public async Task TestPnpmDetector_V6_IgnoresLinkAndFilePackageDependenciesAsync() + { + var yamlFile = @" +lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false +importers: + .: + dependencies: + pkg-a: + specifier: 1.0.0 + version: 1.0.0 +packages: + /pkg-a@1.0.0: + resolution: {integrity: sha512-mock=} + dependencies: + pkg-link: link:../pkg-link + pkg-file: file:../pkg-file + minimist: 1.2.8 + dev: false + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: false +"; + + var (scanResult, componentRecorder) = await this.detectorTestUtility + .WithFile("pnpm-lock.yaml", yamlFile) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + var detectedComponents = componentRecorder.GetDetectedComponents().ToList(); + detectedComponents.Should().HaveCount(2); + + var names = detectedComponents.Select(c => ((NpmComponent)c.Component).Name); + names.Should().BeEquivalentTo(["pkg-a", "minimist"]); + + var pkgA = detectedComponents.Single(c => ((NpmComponent)c.Component).Name == "pkg-a"); + var minimist = detectedComponents.Single(c => ((NpmComponent)c.Component).Name == "minimist"); + + var graph = componentRecorder.GetDependencyGraphsByLocation().Values.First(); + graph.IsComponentExplicitlyReferenced(pkgA.Component.Id).Should().BeTrue(); + graph.GetDependenciesForComponent(pkgA.Component.Id).Should().Contain(minimist.Component.Id); + } + [TestMethod] public async Task TestPnpmDetector_V6_BadLockVersion_EmptyAsync() { From 1ec35739bf4c9a289f5e4fbb952cecd48f1825cb Mon Sep 17 00:00:00 2001 From: RKS Date: Wed, 16 Sep 2026 23:58:14 -0400 Subject: [PATCH 2/2] test: add local package entries under packages in regression test --- .../PnpmDetectorTests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs index ea6a224d2..d9ba406d9 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs @@ -605,6 +605,10 @@ public async Task TestPnpmDetector_V6_IgnoresLinkAndFilePackageDependenciesAsync /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: false + link:../pkg-link: + dev: false + file:../pkg-file: + dev: false "; var (scanResult, componentRecorder) = await this.detectorTestUtility