diff --git a/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs index 031358105..00760e336 100644 --- a/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs @@ -3,6 +3,8 @@ namespace Microsoft.ComponentDetection.Detectors.Dockerfile; using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.ComponentDetection.Common; @@ -10,6 +12,7 @@ namespace Microsoft.ComponentDetection.Detectors.Dockerfile; using Microsoft.ComponentDetection.Contracts.Internal; using Microsoft.ComponentDetection.Contracts.TypedComponent; using Microsoft.Extensions.Logging; +using Sprache; using Valleysoft.DockerfileModel; public class DockerfileComponentDetector : FileComponentDetector, IExperimentalDetector @@ -41,6 +44,15 @@ public DockerfileComponentDetector( public override int Version => 1; + protected override IList SkippedFolders => ["node_modules"]; + + protected override Task> OnPrepareDetectionAsync( + IObservable processRequests, + IDictionary detectorArgs, + CancellationToken cancellationToken = default) => + Task.FromResult(processRequests.Where(processRequest => + !this.IsInSkippedFolder(processRequest.ComponentStream.Location))); + protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary detectorArgs, CancellationToken cancellationToken = default) { var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; @@ -59,12 +71,22 @@ protected override async Task OnFileFoundAsync(ProcessRequest processRequest, ID var stageNameMap = new Dictionary(); var dockerFileComponent = this.ParseDockerFileAsync(contents, file.Location, singleFileComponentRecorder, stageNameMap); } + catch (ParseException e) + { + this.Logger.LogWarning(e, "Ignoring file that doesn't appear to be a Dockerfile: {Location}", filePath); + } catch (Exception e) { - this.Logger.LogError(e, "The file doesn't appear to be a Dockerfile: {Location}", filePath); + this.Logger.LogError(e, "Failed to process Dockerfile: {Location}", filePath); } } + private bool IsInSkippedFolder(string filePath) + { + var pathSegments = filePath.Split(['/', '\\'], StringSplitOptions.RemoveEmptyEntries); + return this.SkippedFolders.Any(skippedFolder => pathSegments.Contains(skippedFolder, StringComparer.OrdinalIgnoreCase)); + } + private Task ParseDockerFileAsync(string fileContents, string fileLocation, ISingleFileComponentRecorder singleFileComponentRecorder, Dictionary stageNameMap) { var dockerfileModel = Dockerfile.Parse(fileContents); diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/DockerfileComponentDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/DockerfileComponentDetectorTests.cs index 62bf38b24..90d685172 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/DockerfileComponentDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/DockerfileComponentDetectorTests.cs @@ -1,6 +1,8 @@ #nullable enable namespace Microsoft.ComponentDetection.Detectors.Tests; +using System; +using System.IO; using System.Linq; using System.Threading.Tasks; using AwesomeAssertions; @@ -8,6 +10,7 @@ namespace Microsoft.ComponentDetection.Detectors.Tests; using Microsoft.ComponentDetection.Contracts.TypedComponent; using Microsoft.ComponentDetection.Detectors.Dockerfile; using Microsoft.ComponentDetection.TestsUtilities; +using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -210,6 +213,51 @@ public async Task TestDockerfile_PrefixedFilenameAsync() componentRecorder.GetDetectedComponents().Should().ContainSingle(); } + [TestMethod] + [DataRow("dockerfile.mjs")] + [DataRow("dockerfile.d.mts")] + public async Task TestDockerfile_ShikiLanguageDefinitionIsIgnoredAsync(string fileName) + { + var loggerMock = new Mock>(); + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile(fileName, "export default Object.freeze({ name: 'dockerfile' });") + .AddServiceMock(loggerMock) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + loggerMock.Verify( + logger => logger.Log( + LogLevel.Warning, + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>()), + Times.Once); + loggerMock.Verify( + logger => logger.Log( + LogLevel.Error, + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>()), + Times.Never); + } + + [TestMethod] + public async Task TestDockerfile_UnderNodeModulesIsIgnoredAsync() + { + var fileLocation = Path.Combine(Path.GetTempPath(), "node_modules", "shiki", "dist", "langs", "dockerfile.mjs"); + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("dockerfile.mjs", "FROM redis:7-alpine", fileLocation: fileLocation) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + [TestMethod] public async Task TestDockerfile_NoFromInstructionsAsync() {