diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/IPnpmDetector.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/IPnpmDetector.cs index 7557c76b..cc1b96f2 100644 --- a/src/Microsoft.ComponentDetection.Detectors/pnpm/IPnpmDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/IPnpmDetector.cs @@ -23,4 +23,6 @@ public static class PnpmConstants public const string PnpmFileDependencyPath = "file:"; public const string PnpmLinkDependencyPath = "link:"; + public const string PnpmHttpDependencyPath = "http:"; + public const string PnpmHttpsDependencyPath = "https:"; } diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm9Detector.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm9Detector.cs index efb10589..a5903ab6 100644 --- a/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm9Detector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm9Detector.cs @@ -31,7 +31,7 @@ public void RecordDependencyGraphFromFile(string yamlFileContent, ISingleFileCom // 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. var (packageName, packageVersion) = this.pnpmParsingUtilities.ExtractNameAndVersionFromPnpmPackagePath(pnpmDependencyPath); - var isFileOrLink = packageVersion.StartsWith(PnpmConstants.PnpmLinkDependencyPath) || packageVersion.StartsWith(PnpmConstants.PnpmFileDependencyPath); + var isFileOrLink = this.IsFileOrLink(packageVersion) || this.IsFileOrLink(pnpmDependencyPath); var dependencyPath = pnpmDependencyPath; if (pnpmDependencyPath.StartsWith('/')) @@ -77,7 +77,7 @@ private void ProcessDependencyList(ISingleFileComponentRecorder singleFileCompon // Lockfile v9 apparently removed the tagging of dev dependencies in the lockfile, so we revert to using the dependency tree to establish dev dependency state. // At this point, the root dependencies are marked according to which dependency group they are declared in the lockfile itself. // Ignore "file:" and "link:" as these are local packages. - var isFileOrLink = dep.Version.StartsWith(PnpmConstants.PnpmLinkDependencyPath) || dep.Version.StartsWith(PnpmConstants.PnpmFileDependencyPath); + var isFileOrLink = this.IsFileOrLink(dep.Version); if (!isFileOrLink) { singleFileComponentRecorder.RegisterUsage(component, isExplicitReferencedDependency: true, isDevelopmentDependency: isDevelopmentDependency); @@ -88,6 +88,14 @@ private void ProcessDependencyList(ISingleFileComponentRecorder singleFileCompon } } + private bool IsFileOrLink(string packagePath) + { + return packagePath.StartsWith(PnpmConstants.PnpmLinkDependencyPath) || + packagePath.StartsWith(PnpmConstants.PnpmFileDependencyPath) || + packagePath.StartsWith(PnpmConstants.PnpmHttpDependencyPath) || + packagePath.StartsWith(PnpmConstants.PnpmHttpsDependencyPath); + } + private void ProcessIndirectDependencies(ISingleFileComponentRecorder singleFileComponentRecorder, Dictionary components, string parentComponentId, Dictionary dependencies, bool isDevDependency, HashSet seenDependencies) { // Now that the `components` dictionary is populated, make another pass of all components, registering all the dependency edges in the graph. diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs index 01500d13..3f212e96 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs @@ -852,6 +852,75 @@ public async Task TestPnpmDetector_V9_GoodLockVersion_FileAndLinkDependenciesAre parentComponent => parentComponent.Name == "sampleDependency"); } + [TestMethod] + public async Task TestPnpmDetector_V9_GoodLockVersion_HttpDependenciesAreNotRegistered() + { + var yamlFile = @" +lockfileVersion: '9.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false +importers: + .: + dependencies: + sampleDependency: + specifier: ^1.1.1 + version: 1.1.1 + sampleHttpDependency: + specifier: https://samplePackage/tar.gz/32f550d3b3bdb1b781aabe100683311cd982c98e + version: sample@https://samplePackage/tar.gz/32f550d3b3bdb1b781aabe100683311cd982c98e + SampleLinkDependency: + specifier: workspace:* + version: link:SampleLinkDependency +packages: + sampleDependency@1.1.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + sampleIndirectDependency2@2.2.2: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + sampleIndirectDependency@3.3.3: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + +snapshots: + sampleDependency@1.1.1: + dependencies: + sampleIndirectDependency: 3.3.3 + sampleIndirectDependency2: 2.2.2 + 'file://../sampleFile': 'link:../\\' + sampleIndirectDependency2@2.2.2: {} + sampleIndirectDependency@3.3.3: {} + sampleHttpDependency@https://samplePackage/tar.gz/32f550d3b3bdb1b781aabe100683311cd982c98e': {} +"; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("pnpm-lock.yaml", yamlFile) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().HaveCount(3); + var npmComponents = detectedComponents.Select(x => new { Component = x.Component as NpmComponent, DetectedComponent = x }); + npmComponents.Should().Contain(x => x.Component.Name == "sampleDependency" && x.Component.Version == "1.1.1"); + npmComponents.Should().Contain(x => x.Component.Name == "sampleIndirectDependency2" && x.Component.Version == "2.2.2"); + npmComponents.Should().Contain(x => x.Component.Name == "sampleIndirectDependency" && x.Component.Version == "3.3.3"); + + var noDevDependencyComponent = npmComponents.First(x => x.Component.Name == "sampleDependency"); + var indirectDependencyComponent2 = npmComponents.First(x => x.Component.Name == "sampleIndirectDependency2"); + var indirectDependencyComponent = npmComponents.First(x => x.Component.Name == "sampleIndirectDependency"); + + componentRecorder.GetEffectiveDevDependencyValue(noDevDependencyComponent.Component.Id).Should().BeFalse(); + componentRecorder.GetEffectiveDevDependencyValue(indirectDependencyComponent2.Component.Id).Should().BeFalse(); + componentRecorder.GetEffectiveDevDependencyValue(indirectDependencyComponent.Component.Id).Should().BeFalse(); + componentRecorder.AssertAllExplicitlyReferencedComponents( + indirectDependencyComponent.Component.Id, + parentComponent => parentComponent.Name == "sampleDependency"); + componentRecorder.AssertAllExplicitlyReferencedComponents( + indirectDependencyComponent2.Component.Id, + parentComponent => parentComponent.Name == "sampleDependency"); + } + [TestMethod] public async Task TestPnpmDetector_V9_GoodLockVersion_MissingSnapshotsSuccess() {