diff --git a/src/Microsoft.ComponentDetection.Detectors/npm/NpmComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/npm/NpmComponentDetector.cs index 1ed41a721..354b81ae8 100644 --- a/src/Microsoft.ComponentDetection.Detectors/npm/NpmComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/npm/NpmComponentDetector.cs @@ -40,7 +40,7 @@ public NpmComponentDetector( public override IEnumerable SupportedComponentTypes { get; } = [ComponentType.Npm]; - public override int Version { get; } = 2; + public override int Version { get; } = 3; protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary detectorArgs, CancellationToken cancellationToken = default) { @@ -79,6 +79,7 @@ protected virtual bool ProcessIndividualPackageJTokens(string filePath, ISingleF var name = packageJToken["name"].ToString(); var version = packageJToken["version"].ToString(); var authorToken = packageJToken["author"]; + var enginesToken = packageJToken["engines"]; if (!SemanticVersion.TryParse(version, out _)) { @@ -87,6 +88,12 @@ protected virtual bool ProcessIndividualPackageJTokens(string filePath, ISingleF return false; } + if (enginesToken != null && enginesToken["vscode"] != null) + { + this.Logger.LogInformation("{NpmPackageName} found at path {NpmPackageLocation} represents a built-in VS Code extension. This package will not be registered.", name, filePath); + return false; + } + var npmComponent = new NpmComponent(name, version, author: this.GetAuthor(authorToken, name, filePath)); singleFileComponentRecorder.RegisterUsage(new DetectedComponent(npmComponent)); diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/NpmDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/NpmDetectorTests.cs index bce143d44..b75299933 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/NpmDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/NpmDetectorTests.cs @@ -78,9 +78,9 @@ public async Task TestNpmDetector_AuthorNameAndAuthorEmailDetected_WhenAuthorNam { var authorName = GetRandomString(); var authorEmail = GetRandomString(); - var authroUrl = GetRandomString(); + var authorUrl = GetRandomString(); var (packageJsonName, packageJsonContents, packageJsonPath) = - NpmTestUtilities.GetPackageJsonNoDependenciesForAuthorAndEmailAsSingleString(authorName, authorEmail, authroUrl); + NpmTestUtilities.GetPackageJsonNoDependenciesForAuthorAndEmailAsSingleString(authorName, authorEmail, authorUrl); var (scanResult, componentRecorder) = await this.DetectorTestUtility .WithFile(packageJsonName, packageJsonContents, this.packageJsonSearchPattern, fileLocation: packageJsonPath) @@ -97,9 +97,9 @@ public async Task TestNpmDetector_AuthorNameAndAuthorEmailDetected_WhenAuthorNam public async Task TestNpmDetector_AuthorNameDetected_WhenEmailNotPresentAndUrlIsPresent_AuthorAsSingleStringAsync() { var authorName = GetRandomString(); - var authroUrl = GetRandomString(); + var authorUrl = GetRandomString(); var (packageJsonName, packageJsonContents, packageJsonPath) = - NpmTestUtilities.GetPackageJsonNoDependenciesForAuthorAndEmailAsSingleString(authorName, null, authroUrl); + NpmTestUtilities.GetPackageJsonNoDependenciesForAuthorAndEmailAsSingleString(authorName, null, authorUrl); var (scanResult, componentRecorder) = await this.DetectorTestUtility .WithFile(packageJsonName, packageJsonContents, this.packageJsonSearchPattern, fileLocation: packageJsonPath) @@ -116,10 +116,10 @@ public async Task TestNpmDetector_AuthorNameDetected_WhenEmailNotPresentAndUrlIs public async Task TestNpmDetector_AuthorNull_WhenAuthorMalformed_AuthorAsSingleStringAsync() { var authorName = GetRandomString(); - var authroUrl = GetRandomString(); + var authorUrl = GetRandomString(); var authorEmail = GetRandomString(); var (packageJsonName, packageJsonContents, packageJsonPath) = - NpmTestUtilities.GetPackageJsonNoDependenciesMalformedAuthorAsSingleString(authorName, authorEmail, authroUrl); + NpmTestUtilities.GetPackageJsonNoDependenciesMalformedAuthorAsSingleString(authorName, authorEmail, authorUrl); var (scanResult, componentRecorder) = await this.DetectorTestUtility .WithFile(packageJsonName, packageJsonContents, this.packageJsonSearchPattern, fileLocation: packageJsonPath) @@ -135,7 +135,6 @@ public async Task TestNpmDetector_AuthorNull_WhenAuthorMalformed_AuthorAsSingleS public async Task TestNpmDetector_AuthorNameDetected_WhenEmailNotPresentAndUrlNotPresent_AuthorAsSingleStringAsync() { var authorName = GetRandomString(); - var authroUrl = GetRandomString(); var (packageJsonName, packageJsonContents, packageJsonPath) = NpmTestUtilities.GetPackageJsonNoDependenciesForAuthorAndEmailAsSingleString(authorName); @@ -208,6 +207,38 @@ public async Task TestNpmDetector_NullAuthor_WhenAuthorNameIsNullOrEmpty_AuthorA ((NpmComponent)detectedComponents.First().Component).Author.Should().BeNull(); } + [TestMethod] + public async Task TestNpmDetector_NodeEngineDoesNotCauseSkippedPackageAsync() + { + var componentName = GetRandomString(); + var version = NewRandomVersion(); + var (packageJsonName, packageJsonContents, packageJsonPath) = + NpmTestUtilities.GetPackageJsonNoDependenciesForNameAndVersionWithNodeEngine(componentName, version); + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile(packageJsonName, packageJsonContents, this.packageJsonSearchPattern, fileLocation: packageJsonPath) + .ExecuteDetectorAsync(); + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().ContainSingle(); + } + + [TestMethod] + public async Task TestNpmDetector_VSCodeEngineCausesSkippedPackageAsync() + { + var componentName = GetRandomString(); + var version = NewRandomVersion(); + var (packageJsonName, packageJsonContents, packageJsonPath) = + NpmTestUtilities.GetPackageJsonNoDependenciesForNameAndVersionWithVSCodeEngine(componentName, version); + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile(packageJsonName, packageJsonContents, this.packageJsonSearchPattern, fileLocation: packageJsonPath) + .ExecuteDetectorAsync(); + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().BeEmpty(); + } + private static void AssertDetectedComponentCount(IEnumerable detectedComponents, int expectedCount) { detectedComponents.Should().HaveCount(expectedCount); diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/NpmTestUtilities.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/NpmTestUtilities.cs index 224493655..7d2267d48 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/NpmTestUtilities.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/NpmTestUtilities.cs @@ -117,6 +117,32 @@ public static (string PackageJsonName, string PackageJsonContents, string Packag return ("package.json", packageJsonTemplate, Path.Combine(Path.GetTempPath(), "package.json")); } + public static (string PackageJsonName, string PackageJsonContents, string PackageJsonPath) GetPackageJsonNoDependenciesForNameAndVersionWithNodeEngine(string packageName, string packageVersion) + { + var packagejson = @"{{ + ""name"": ""{0}"", + ""version"": ""{1}"", + ""engines"": {{ + ""node"": ""^20.0.0"" + }} + }}"; + var packageJsonTemplate = string.Format(packagejson, packageName, packageVersion); + return ("package.json", packageJsonTemplate, Path.Combine(Path.GetTempPath(), "package.json")); + } + + public static (string PackageJsonName, string PackageJsonContents, string PackageJsonPath) GetPackageJsonNoDependenciesForNameAndVersionWithVSCodeEngine(string packageName, string packageVersion) + { + var packagejson = @"{{ + ""name"": ""{0}"", + ""version"": ""{1}"", + ""engines"": {{ + ""vscode"": ""^1.0.0"" + }} + }}"; + var packageJsonTemplate = string.Format(packagejson, packageName, packageVersion); + return ("package.json", packageJsonTemplate, Path.Combine(Path.GetTempPath(), "package.json")); + } + public static (string PackageJsonName, string PackageJsonContents, string PackageJsonPath) GetPackageJsonNoDependenciesForAuthorAndEmailInJsonFormat( string authorName, string authorEmail = null) {