diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/InvalidParseVersionTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/InvalidParseVersionTelemetryRecord.cs new file mode 100644 index 000000000..341b0b69b --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/InvalidParseVersionTelemetryRecord.cs @@ -0,0 +1,14 @@ +namespace Microsoft.ComponentDetection.Common.Telemetry.Records; + +public class InvalidParseVersionTelemetryRecord : BaseDetectionTelemetryRecord +{ + public override string RecordName => "InvalidParseVersion"; + + public string DetectorId { get; set; } + + public string FilePath { get; set; } + + public string Version { get; set; } + + public string MaxVersion { get; set; } +} diff --git a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/PipReportVersionTelemetryRecord.cs b/src/Microsoft.ComponentDetection.Common/Telemetry/Records/PipReportVersionTelemetryRecord.cs deleted file mode 100644 index 26242d459..000000000 --- a/src/Microsoft.ComponentDetection.Common/Telemetry/Records/PipReportVersionTelemetryRecord.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Microsoft.ComponentDetection.Common.Telemetry.Records; - -public class PipReportVersionTelemetryRecord : BaseDetectionTelemetryRecord -{ - public override string RecordName => "PipReportVersion"; - - public string Version { get; set; } - - public string MaxVersion { get; set; } -} diff --git a/src/Microsoft.ComponentDetection.Detectors/pip/PipReportComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/pip/PipReportComponentDetector.cs index 2bb07e1b9..5e074786e 100644 --- a/src/Microsoft.ComponentDetection.Detectors/pip/PipReportComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/pip/PipReportComponentDetector.cs @@ -96,8 +96,10 @@ protected override async Task OnFileFoundAsync(ProcessRequest processRequest, ID report.Version, MaxReportVersion); - using var versionRecord = new PipReportVersionTelemetryRecord + using var versionRecord = new InvalidParseVersionTelemetryRecord { + DetectorId = this.Id, + FilePath = file.Location, Version = report.Version, MaxVersion = MaxReportVersion.ToString(), }; diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/IPnpmDetector.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/IPnpmDetector.cs new file mode 100644 index 000000000..4c2695dc3 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/IPnpmDetector.cs @@ -0,0 +1,16 @@ +namespace Microsoft.ComponentDetection.Detectors.Pnpm; + +using Microsoft.ComponentDetection.Contracts; + +/// +/// Interface that represents a version of the pnpm detector. +/// +public interface IPnpmDetector +{ + /// + /// Parses a yaml file content in pnmp format into the dependecy graph. + /// + /// Content of the yaml file that contains the pnpm dependencies. + /// Component recorder to which to write the dependency graph. + public void RecordDependencyGraphFromFile(string yamlFileContent, ISingleFileComponentRecorder singleFileComponentRecorder); +} diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm5Detector.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm5Detector.cs new file mode 100644 index 000000000..b91403ffe --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm5Detector.cs @@ -0,0 +1,58 @@ +namespace Microsoft.ComponentDetection.Detectors.Pnpm; + +using System.Collections.Generic; +using System.Linq; +using Microsoft.ComponentDetection.Contracts; + +public class Pnpm5Detector : IPnpmDetector +{ + public const string MajorVersion = "5"; + + public void RecordDependencyGraphFromFile(string yamlFileContent, ISingleFileComponentRecorder singleFileComponentRecorder) + { + var yaml = PnpmParsingUtilities.DeserializePnpmYamlV5File(yamlFileContent); + + foreach (var packageKeyValue in yaml.Packages ?? Enumerable.Empty>()) + { + // Ignore file: as these are local packages. + if (packageKeyValue.Key.StartsWith("file:")) + { + continue; + } + + var parentDetectedComponent = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV5(pnpmPackagePath: packageKeyValue.Key); + var isDevDependency = packageKeyValue.Value != null && PnpmParsingUtilities.IsPnpmPackageDevDependency(packageKeyValue.Value); + singleFileComponentRecorder.RegisterUsage(parentDetectedComponent, isDevelopmentDependency: isDevDependency); + parentDetectedComponent = singleFileComponentRecorder.GetComponent(parentDetectedComponent.Component.Id); + + if (packageKeyValue.Value.Dependencies != null) + { + foreach (var dependency in packageKeyValue.Value.Dependencies) + { + // Ignore local packages. + if (PnpmParsingUtilities.IsLocalDependency(dependency)) + { + continue; + } + + var childDetectedComponent = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV5( + pnpmPackagePath: PnpmParsingUtilities.CreatePnpmPackagePathFromDependencyV5(dependency.Key, dependency.Value)); + + // Older code used the root's dev dependency value. We're leaving this null until we do a second pass to look at each components' top level referrers. + singleFileComponentRecorder.RegisterUsage(childDetectedComponent, parentComponentId: parentDetectedComponent.Component.Id, isDevelopmentDependency: null); + } + } + } + + // PNPM doesn't know at the time of RegisterUsage being called for a dependency whether something is a dev dependency or not, so after building up the graph we look at top level referrers. + foreach (var component in singleFileComponentRecorder.GetDetectedComponents()) + { + var graph = singleFileComponentRecorder.DependencyGraph; + var explicitReferences = graph.GetExplicitReferencedDependencyIds(component.Key); + foreach (var explicitReference in explicitReferences) + { + singleFileComponentRecorder.RegisterUsage(component.Value, isDevelopmentDependency: graph.IsDevelopmentDependency(explicitReference)); + } + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6ComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6Detector.cs similarity index 63% rename from src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6ComponentDetector.cs rename to src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6Detector.cs index 088f42201..0a3e5a99e 100644 --- a/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6ComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6Detector.cs @@ -1,82 +1,17 @@ namespace Microsoft.ComponentDetection.Detectors.Pnpm; -using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Threading.Tasks; using Microsoft.ComponentDetection.Contracts; -using Microsoft.ComponentDetection.Contracts.Internal; -using Microsoft.ComponentDetection.Contracts.TypedComponent; -using Microsoft.Extensions.Logging; -public class Pnpm6ComponentDetector : FileComponentDetector, IExperimentalDetector +public class Pnpm6Detector : IPnpmDetector { - public Pnpm6ComponentDetector( - IComponentStreamEnumerableFactory componentStreamEnumerableFactory, - IObservableDirectoryWalkerFactory walkerFactory, - ILogger logger) - { - this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory; - this.Scanner = walkerFactory; - this.Logger = logger; - } - - public override string Id { get; } = "Pnpm6"; - - public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.Npm) }; - - public override IList SearchPatterns { get; } = new List { "shrinkwrap.yaml", "pnpm-lock.yaml" }; - - public override IEnumerable SupportedComponentTypes { get; } = new[] { ComponentType.Npm }; + public const string MajorVersion = "6"; - public override int Version { get; } = 1; - - public override bool NeedsAutomaticRootDependencyCalculation => true; - - /// - protected override IList SkippedFolders => new List { "node_modules", "pnpm-store" }; - - protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary detectorArgs) + public void RecordDependencyGraphFromFile(string yamlFileContent, ISingleFileComponentRecorder singleFileComponentRecorder) { - var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; - var file = processRequest.ComponentStream; + var yaml = PnpmParsingUtilities.DeserializePnpmYamlV6File(yamlFileContent); - var skippedFolder = this.SkippedFolders.FirstOrDefault(folder => file.Location.Contains(folder)); - if (!string.IsNullOrEmpty(skippedFolder)) - { - this.Logger.LogDebug("Skipping found file, it was detected as being within a {SkippedFolder} folder.", skippedFolder); - } - - try - { - var fileContent = await new StreamReader(file.Stream).ReadToEndAsync(); - var version = PnpmParsingUtilities.DeserializePnpmYamlFileVersion(fileContent); - this.RecordLockfileVersion(version); - var majorVersion = version?.Split(".")[0]; - switch (majorVersion) - { - case null: - case "5": - // Handled in the non-experimental detector. No-op here. - break; - case "6": - var pnpmYamlV6 = PnpmParsingUtilities.DeserializePnpmYamlV6File(fileContent); - this.RecordDependencyGraphFromFileV6(pnpmYamlV6, singleFileComponentRecorder); - break; - default: - // Handled in the non-experimental detector. No-op here. - break; - } - } - catch (Exception e) - { - this.Logger.LogError(e, "Failed to read pnpm yaml file {File}", file.Location); - } - } - - private void RecordDependencyGraphFromFileV6(PnpmYamlV6 yaml, ISingleFileComponentRecorder singleFileComponentRecorder) - { // There may be multiple instance of the same package (even at the same version) in pnpm differentiated by other aspects of the pnpm dependency path. // Therefor all DetectedComponents are tracked by the same full string pnpm uses, the pnpm dependency path, which is used as the key in this dictionary. // Some documentation about pnpm dependency paths can be found at https://github.com/pnpm/spec/blob/master/dependency-path.md. diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetector.cs deleted file mode 100644 index d8bb95354..000000000 --- a/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetector.cs +++ /dev/null @@ -1,128 +0,0 @@ -namespace Microsoft.ComponentDetection.Detectors.Pnpm; - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.ComponentDetection.Contracts; -using Microsoft.ComponentDetection.Contracts.Internal; -using Microsoft.ComponentDetection.Contracts.TypedComponent; -using Microsoft.Extensions.Logging; - -public class PnpmComponentDetector : FileComponentDetector -{ - public PnpmComponentDetector( - IComponentStreamEnumerableFactory componentStreamEnumerableFactory, - IObservableDirectoryWalkerFactory walkerFactory, - ILogger logger) - { - this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory; - this.Scanner = walkerFactory; - this.Logger = logger; - } - - public override string Id { get; } = "Pnpm"; - - public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.Npm) }; - - public override IList SearchPatterns { get; } = new List { "shrinkwrap.yaml", "pnpm-lock.yaml" }; - - public override IEnumerable SupportedComponentTypes { get; } = new[] { ComponentType.Npm }; - - public override int Version { get; } = 5; - - public override bool NeedsAutomaticRootDependencyCalculation => true; - - /// - protected override IList SkippedFolders => new List { "node_modules", "pnpm-store" }; - - protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary detectorArgs) - { - var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; - var file = processRequest.ComponentStream; - - this.Logger.LogDebug("Found yaml file: {YamlFile}", file.Location); - var skippedFolder = this.SkippedFolders.FirstOrDefault(folder => file.Location.Contains(folder)); - if (!string.IsNullOrEmpty(skippedFolder)) - { - this.Logger.LogDebug("Skipping found file, it was detected as being within a {SkippedFolder} folder.", skippedFolder); - } - - try - { - var fileContent = await new StreamReader(file.Stream).ReadToEndAsync(); - var version = PnpmParsingUtilities.DeserializePnpmYamlFileVersion(fileContent); - this.RecordLockfileVersion(version); - var majorVersion = version?.Split(".")[0]; - switch (majorVersion) - { - case null: - // The null case falls through to version 5 to preserver the behavior of this scanner from before version specific logic was added. - // This allows files versioned with "shrinkwrapVersion" (such as one included in some of the tests) to be used. - // Given that "shrinkwrapVersion" is a concept from file format version 4 https://github.com/pnpm/spec/blob/master/lockfile/4.md) - // this case might not be robust. - case "5": - var pnpmYamlV5 = PnpmParsingUtilities.DeserializePnpmYamlV5File(fileContent); - this.RecordDependencyGraphFromFileV5(pnpmYamlV5, singleFileComponentRecorder); - break; - case "6": - // Handled in the experimental detector. No-op here. - break; - default: - this.Logger.LogWarning("Unsupported lockfileVersion in pnpm yaml file {File}", file.Location); - break; - } - } - catch (Exception e) - { - this.Logger.LogError(e, "Failed to read pnpm yaml file {File}", file.Location); - } - } - - private void RecordDependencyGraphFromFileV5(PnpmYamlV5 yaml, ISingleFileComponentRecorder singleFileComponentRecorder) - { - foreach (var packageKeyValue in yaml.Packages ?? Enumerable.Empty>()) - { - // Ignore file: as these are local packages. - if (packageKeyValue.Key.StartsWith("file:")) - { - continue; - } - - var parentDetectedComponent = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV5(pnpmPackagePath: packageKeyValue.Key); - var isDevDependency = packageKeyValue.Value != null && PnpmParsingUtilities.IsPnpmPackageDevDependency(packageKeyValue.Value); - singleFileComponentRecorder.RegisterUsage(parentDetectedComponent, isDevelopmentDependency: isDevDependency); - parentDetectedComponent = singleFileComponentRecorder.GetComponent(parentDetectedComponent.Component.Id); - - if (packageKeyValue.Value.Dependencies != null) - { - foreach (var dependency in packageKeyValue.Value.Dependencies) - { - // Ignore local packages. - if (PnpmParsingUtilities.IsLocalDependency(dependency)) - { - continue; - } - - var childDetectedComponent = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV5( - pnpmPackagePath: PnpmParsingUtilities.CreatePnpmPackagePathFromDependencyV5(dependency.Key, dependency.Value)); - - // Older code used the root's dev dependency value. We're leaving this null until we do a second pass to look at each components' top level referrers. - singleFileComponentRecorder.RegisterUsage(childDetectedComponent, parentComponentId: parentDetectedComponent.Component.Id, isDevelopmentDependency: null); - } - } - } - - // PNPM doesn't know at the time of RegisterUsage being called for a dependency whether something is a dev dependency or not, so after building up the graph we look at top level referrers. - foreach (var component in singleFileComponentRecorder.GetDetectedComponents()) - { - var graph = singleFileComponentRecorder.DependencyGraph; - var explicitReferences = graph.GetExplicitReferencedDependencyIds(component.Key); - foreach (var explicitReference in explicitReferences) - { - singleFileComponentRecorder.RegisterUsage(component.Value, isDevelopmentDependency: graph.IsDevelopmentDependency(explicitReference)); - } - } - } -} diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetectorFactory.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetectorFactory.cs new file mode 100644 index 000000000..32750124e --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetectorFactory.cs @@ -0,0 +1,115 @@ +namespace Microsoft.ComponentDetection.Detectors.Pnpm; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Common.Telemetry.Records; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.Internal; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.Extensions.Logging; + +/// +/// Factory responsible for constructing the proper and recording its dependency +/// graph based on the file found during file component detection. +/// +public class PnpmComponentDetectorFactory : FileComponentDetector +{ + /// + /// The maximum version of the report specification that this detector can handle. + /// + private static readonly Version MaxLockfileVersion = new(6, 0); + + public PnpmComponentDetectorFactory( + IComponentStreamEnumerableFactory componentStreamEnumerableFactory, + IObservableDirectoryWalkerFactory walkerFactory, + ILogger logger) + { + this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory; + this.Scanner = walkerFactory; + this.Logger = logger; + } + + public override string Id { get; } = "Pnpm"; + + public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.Npm) }; + + public override IList SearchPatterns { get; } = new List { "shrinkwrap.yaml", "pnpm-lock.yaml" }; + + public override IEnumerable SupportedComponentTypes { get; } = new[] { ComponentType.Npm }; + + public override int Version { get; } = 6; + + public override bool NeedsAutomaticRootDependencyCalculation => true; + + protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary detectorArgs) + { + var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; + var file = processRequest.ComponentStream; + this.Logger.LogDebug("Found yaml file: {YamlFile}", file.Location); + var skippedFolder = this.SkippedFolders.FirstOrDefault(folder => file.Location.Contains(folder)); + if (!string.IsNullOrEmpty(skippedFolder)) + { + this.Logger.LogDebug("Skipping found file, it was detected as being within a {SkippedFolder} folder.", skippedFolder); + } + + try + { + var fileContent = await new StreamReader(file.Stream).ReadToEndAsync(); + var detector = this.GetPnpmComponentDetector(fileContent, out var detectedVersion); + if (detector == null) + { + this.Logger.LogWarning("Unsupported lockfileVersion in pnpm yaml file {File}", file.Location); + using var unsupportedVersionRecord = new InvalidParseVersionTelemetryRecord + { + DetectorId = this.Id, + FilePath = file.Location, + Version = detectedVersion, + MaxVersion = MaxLockfileVersion.ToString(), + }; + } + else + { + this.Logger.LogDebug( + "Found Pnmp yaml file '{Location}' with version '{Version}' so using PnpmDetector of type '{Type}'.", + file.Location, + detectedVersion ?? "null", + detector.GetType().Name); + + detector.RecordDependencyGraphFromFile(fileContent, singleFileComponentRecorder); + } + } + catch (Exception e) + { + this.Logger.LogError(e, "Failed to read pnpm yaml file {File}", file.Location); + + using var failedParsingRecord = new FailedParsingFileRecord + { + DetectorId = this.Id, + FilePath = file.Location, + ExceptionMessage = e.Message, + StackTrace = e.StackTrace, + }; + } + } + + private IPnpmDetector GetPnpmComponentDetector(string fileContent, out string detectedVersion) + { + detectedVersion = PnpmParsingUtilities.DeserializePnpmYamlFileVersion(fileContent); + this.RecordLockfileVersion(detectedVersion); + var majorVersion = detectedVersion?.Split(".")[0]; + return majorVersion switch + { + // The null case falls through to version 5 to preserve the behavior of this scanner from before version specific logic was added. + // This allows files versioned with "shrinkwrapVersion" (such as one included in some of the tests) to be used. + // Given that "shrinkwrapVersion" is a concept from file format version 4 https://github.com/pnpm/spec/blob/master/lockfile/4.md) + // this case might not be robust. + null => new Pnpm5Detector(), + Pnpm5Detector.MajorVersion => new Pnpm5Detector(), + Pnpm6Detector.MajorVersion => new Pnpm6Detector(), + _ => null, + }; + } +} diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/Pnpm6Experiment.cs b/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/Pnpm6Experiment.cs deleted file mode 100644 index 32f9b9b92..000000000 --- a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/Pnpm6Experiment.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Microsoft.ComponentDetection.Orchestrator.Experiments.Configs; - -using Microsoft.ComponentDetection.Contracts; -using Microsoft.ComponentDetection.Detectors.Pnpm; - -/// -/// Validating the . -/// -public class Pnpm6Experiment : IExperimentConfiguration -{ - public string Name => "Pnpm6"; - - public bool IsInControlGroup(IComponentDetector componentDetector) => componentDetector is PnpmComponentDetector; - - public bool IsInExperimentGroup(IComponentDetector componentDetector) => componentDetector is Pnpm6ComponentDetector; - - public bool ShouldRecord(IComponentDetector componentDetector, int numComponents) => true; -} diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs index e819f515e..182ed884e 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs +++ b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs @@ -64,7 +64,6 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); // Detectors @@ -121,8 +120,7 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s services.AddSingleton(); // pnpm - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); // Poetry services.AddSingleton(); diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/Pnpm6DetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/Pnpm6DetectorTests.cs deleted file mode 100644 index 8381acf45..000000000 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/Pnpm6DetectorTests.cs +++ /dev/null @@ -1,188 +0,0 @@ -namespace Microsoft.ComponentDetection.Detectors.Tests; - -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.ComponentDetection.Common.DependencyGraph; -using Microsoft.ComponentDetection.Contracts; -using Microsoft.ComponentDetection.Contracts.TypedComponent; -using Microsoft.ComponentDetection.Detectors.Pnpm; -using Microsoft.ComponentDetection.Detectors.Tests.Utilities; -using Microsoft.ComponentDetection.TestsUtilities; -using Microsoft.Extensions.Logging; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; - -[TestClass] -[TestCategory("Governance/All")] -[TestCategory("Governance/ComponentDetection")] -public class Pnpm6DetectorTests : BaseDetectorTest -{ - public Pnpm6DetectorTests() - { - var componentRecorder = new ComponentRecorder(enableManualTrackingOfExplicitReferences: false); - this.DetectorTestUtility.WithScanRequest( - new ScanRequest( - new DirectoryInfo(Path.GetTempPath()), - null, - null, - new Dictionary(), - null, - componentRecorder)); - this.DetectorTestUtility.AddServiceMock(new Mock>()); - } - - [TestMethod] - public async Task TestPnpmDetector_V6Async() - { - var yamlFile = @" -lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false -dependencies: - minimist: - specifier: 1.2.8 - version: 1.2.8 -packages: - /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(); - detectedComponents.Should().ContainSingle(); - - var minimist = detectedComponents.Single(component => ((NpmComponent)component.Component).Name.Contains("minimist")); - componentRecorder.AssertAllExplicitlyReferencedComponents( - minimist.Component.Id, - parentComponent => parentComponent.Name == "minimist"); - - componentRecorder.ForAllComponents(grouping => grouping.AllFileLocations.First().Should().Contain("pnpm-lock.yaml")); - - foreach (var component in detectedComponents) - { - component.Component.Type.Should().Be(ComponentType.Npm); - } - } - - [TestMethod] - public async Task TestPnpmDetector_V6WorkspaceAsync() - { - var yamlFile = @" -lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false -importers: - .: - dependencies: - minimist: - specifier: 1.2.8 - version: 1.2.8 -packages: - /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(); - detectedComponents.Should().ContainSingle(); - - var minimist = detectedComponents.Single(component => ((NpmComponent)component.Component).Name.Contains("minimist")); - componentRecorder.AssertAllExplicitlyReferencedComponents( - minimist.Component.Id, - parentComponent => parentComponent.Name == "minimist"); - - componentRecorder.ForAllComponents(grouping => grouping.AllFileLocations.First().Should().Contain("pnpm-lock.yaml")); - - foreach (var component in detectedComponents) - { - component.Component.Type.Should().Be(ComponentType.Npm); - } - } - - // Test that renamed package is handled correctly, and that resolved version gets used (not specifier) - [TestMethod] - public async Task TestPnpmDetector_V6RenamedAsync() - { - var yamlFile = @" -lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false -dependencies: - renamed: - specifier: npm:minimist@* - version: /minimist@1.2.8 -packages: - /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(); - detectedComponents.Should().ContainSingle(); - - var minimist = detectedComponents.Single(component => ((NpmComponent)component.Component).Name.Equals("minimist")); - componentRecorder.AssertAllExplicitlyReferencedComponents( - minimist.Component.Id, - parentComponent => parentComponent.Name == "minimist"); - ((NpmComponent)minimist.Component).Version.Should().BeEquivalentTo("1.2.8"); - - componentRecorder.ForAllComponents(grouping => grouping.AllFileLocations.First().Should().Contain("pnpm-lock.yaml")); - - foreach (var component in detectedComponents) - { - component.Component.Type.Should().Be(ComponentType.Npm); - } - } - - [TestMethod] - public async Task TestPnpmDetector_V6_BadLockVersion_EmptyAsync() - { - var yamlFile = @" -lockfileVersion: '5.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false -dependencies: - renamed: - specifier: npm:minimist@* - version: /minimist@1.2.8 -packages: - /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(); - detectedComponents.Should().BeEmpty(); - } -} diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs index 3b6b8e839..c43b019be 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs @@ -18,7 +18,7 @@ namespace Microsoft.ComponentDetection.Detectors.Tests; [TestClass] [TestCategory("Governance/All")] [TestCategory("Governance/ComponentDetection")] -public class PnpmDetectorTests : BaseDetectorTest +public class PnpmDetectorTests : BaseDetectorTest { public PnpmDetectorTests() { @@ -394,10 +394,190 @@ public async Task TestPnpmDetector_DependenciesRefeToLocalPaths_DependenciesAreI } [TestMethod] - public async Task TestPnpmDetector_V5_BadLockVersion_EmptyAsync() + public async Task TestPnpmDetector_BadLockVersion_EmptyAsync() + { + var yamlFile = @" +lockfileVersion: '4.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false +dependencies: + renamed: + specifier: npm:minimist@* + version: /minimist@1.2.8 +packages: + /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(); + detectedComponents.Should().BeEmpty(); + } + + [TestMethod] + public async Task TestPnpmDetector_V5_GoodLockVersion_ParsedDependenciesAsync() + { + var yamlFile = @" +lockfileVersion: '5.0' +dependencies: + 'query-string': 4.3.4, + 'strict-uri-encode': 1.1.0 +packages: + /query-string/4.3.4: + dev: false + /strict-uri-encode/1.1.0: + dev: true"; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("pnpm-lock.yaml", yamlFile) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + var detectedComponents = componentRecorder.GetDetectedComponents(); + var noDevDependencyComponent = detectedComponents.Select(x => new { Component = x.Component as NpmComponent, DetectedComponent = x }).FirstOrDefault(x => x.Component.Name.Contains("query-string")); + var devDependencyComponent = detectedComponents.Select(x => new { Component = x.Component as NpmComponent, DetectedComponent = x }).FirstOrDefault(x => x.Component.Name.Contains("strict-uri-encode")); + + componentRecorder.GetEffectiveDevDependencyValue(noDevDependencyComponent.Component.Id).Should().BeFalse(); + componentRecorder.GetEffectiveDevDependencyValue(devDependencyComponent.Component.Id).Should().BeTrue(); + } + + [TestMethod] + public async Task TestPnpmDetector_V6_SuccessAsync() { var yamlFile = @" lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false +dependencies: + minimist: + specifier: 1.2.8 + version: 1.2.8 +packages: + /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(); + detectedComponents.Should().ContainSingle(); + + var minimist = detectedComponents.Single(component => ((NpmComponent)component.Component).Name.Contains("minimist")); + componentRecorder.AssertAllExplicitlyReferencedComponents( + minimist.Component.Id, + parentComponent => parentComponent.Name == "minimist"); + + componentRecorder.ForAllComponents(grouping => grouping.AllFileLocations.First().Should().Contain("pnpm-lock.yaml")); + + foreach (var component in detectedComponents) + { + component.Component.Type.Should().Be(ComponentType.Npm); + } + } + + [TestMethod] + public async Task TestPnpmDetector_V6_WorkspaceAsync() + { + var yamlFile = @" +lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false +importers: + .: + dependencies: + minimist: + specifier: 1.2.8 + version: 1.2.8 +packages: + /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(); + detectedComponents.Should().ContainSingle(); + + var minimist = detectedComponents.Single(component => ((NpmComponent)component.Component).Name.Contains("minimist")); + componentRecorder.AssertAllExplicitlyReferencedComponents( + minimist.Component.Id, + parentComponent => parentComponent.Name == "minimist"); + + componentRecorder.ForAllComponents(grouping => grouping.AllFileLocations.First().Should().Contain("pnpm-lock.yaml")); + + foreach (var component in detectedComponents) + { + component.Component.Type.Should().Be(ComponentType.Npm); + } + } + + // Test that renamed package is handled correctly, and that resolved version gets used (not specifier) + [TestMethod] + public async Task TestPnpmDetector_V6_RenamedAsync() + { + var yamlFile = @" +lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false +dependencies: + renamed: + specifier: npm:minimist@* + version: /minimist@1.2.8 +packages: + /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(); + detectedComponents.Should().ContainSingle(); + + var minimist = detectedComponents.Single(component => ((NpmComponent)component.Component).Name.Equals("minimist")); + componentRecorder.AssertAllExplicitlyReferencedComponents( + minimist.Component.Id, + parentComponent => parentComponent.Name == "minimist"); + ((NpmComponent)minimist.Component).Version.Should().BeEquivalentTo("1.2.8"); + + componentRecorder.ForAllComponents(grouping => grouping.AllFileLocations.First().Should().Contain("pnpm-lock.yaml")); + + foreach (var component in detectedComponents) + { + component.Component.Type.Should().Be(ComponentType.Npm); + } + } + + [TestMethod] + public async Task TestPnpmDetector_V6_BadLockVersion_EmptyAsync() + { + var yamlFile = @" +lockfileVersion: '5.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false