diff --git a/Directory.Packages.props b/Directory.Packages.props
index d1cf38c0c..21e67f6a1 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -48,7 +48,7 @@
-
+
diff --git a/docs/detectors/dockerfile.md b/docs/detectors/dockerfile.md
index 2da4fa3a4..febfad138 100644
--- a/docs/detectors/dockerfile.md
+++ b/docs/detectors/dockerfile.md
@@ -12,6 +12,8 @@ The `DockerfileComponentDetector` is an **Experimental** detector. It runs autom
The Dockerfile detector parses Dockerfile syntax to extract Docker image references from `FROM` and `COPY --from` instructions. It uses the [Valleysoft.DockerfileModel](https://github.com/mthalman/DockerfileModel) library to parse Dockerfile syntax.
+BuildKit [here-documents](https://docs.docker.com/reference/dockerfile/#here-documents) (for example `RUN < SupportedComponentTypes { get; } = [ComponentType.DockerReference];
- public override int Version => 1;
+ public override int Version => 2;
protected override IList SkippedFolders => ["node_modules"];
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/DockerfileComponentDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/DockerfileComponentDetectorTests.cs
index 90d685172..0baa8430f 100644
--- a/test/Microsoft.ComponentDetection.Detectors.Tests/DockerfileComponentDetectorTests.cs
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/DockerfileComponentDetectorTests.cs
@@ -305,4 +305,128 @@ public async Task TestDockerfile_FromWithUnresolvedArgVariableIsSkippedAsync()
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
componentRecorder.GetDetectedComponents().Should().BeEmpty();
}
+
+ [TestMethod]
+ public async Task TestDockerfile_RunHeredocAsync()
+ {
+ // BuildKit heredoc syntax: https://docs.docker.com/reference/dockerfile/#here-documents
+ var dockerfile = @"
+FROM ubuntu:noble
+
+RUN <>();
+
+ var (scanResult, componentRecorder) = await this.DetectorTestUtility
+ .WithFile("Dockerfile", dockerfile)
+ .AddServiceMock(loggerMock)
+ .ExecuteDetectorAsync();
+
+ scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+ var components = componentRecorder.GetDetectedComponents();
+ components.Should().ContainSingle();
+
+ var dockerRef = components.First().Component as DockerReferenceComponent;
+ dockerRef.Should().NotBeNull();
+ dockerRef!.Repository.Should().Be("library/ubuntu");
+ dockerRef.Tag.Should().Be("noble");
+
+ loggerMock.Verify(
+ logger => logger.Log(
+ It.Is(level => level >= LogLevel.Warning),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny>()),
+ Times.Never);
+ }
+
+ [TestMethod]
+ public async Task TestDockerfile_RunHeredocWithInterpreterAndSyntaxDirectiveAsync()
+ {
+ var dockerfile = @"# syntax=docker/dockerfile:1.4
+FROM ubuntu:noble-20260113
+
+RUN < c.Component as DockerReferenceComponent)
+ .Where(c => c != null)
+ .Select(c => c!.Repository)
+ .ToList();
+ repos.Should().Contain("dotnet/sdk");
+ repos.Should().Contain("dotnet/runtime-deps");
+ }
+
+ [TestMethod]
+ public async Task TestDockerfile_HeredocBodyIsNotParsedAsInstructionsAsync()
+ {
+ // A heredoc body is opaque content, so a FROM line inside it is not a base image.
+ var dockerfile = @"
+FROM alpine:3.20
+RUN < /tmp/Dockerfile.inner
+FROM inner/image:1.0
+EOF
+";
+
+ var (scanResult, componentRecorder) = await this.DetectorTestUtility
+ .WithFile("Dockerfile", dockerfile)
+ .ExecuteDetectorAsync();
+
+ scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+ var components = componentRecorder.GetDetectedComponents();
+ components.Should().ContainSingle();
+
+ var dockerRef = components.First().Component as DockerReferenceComponent;
+ dockerRef.Should().NotBeNull();
+ dockerRef!.Repository.Should().Be("library/alpine");
+ dockerRef.Tag.Should().Be("3.20");
+ }
}