Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
<PackageVersion Include="Tomlyn.Signed" Version="0.20.0" />
<PackageVersion Include="yamldotnet" Version="16.3.0" />
<PackageVersion Include="Faker.net" Version="2.0.163" />
<PackageVersion Include="Valleysoft.DockerfileModel" Version="1.2.0" />
<PackageVersion Include="Valleysoft.DockerfileModel" Version="2.0.0" />
Comment thread
dcristoloveanu marked this conversation as resolved.
<!-- Fix security alerts -->
<PackageVersion Include="System.Formats.Asn1" Version="9.0.13" />
</ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions docs/detectors/dockerfile.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<EOF` or `COPY <<EOF /path`) are supported. A heredoc body is treated as opaque content, so a `FROM` line inside it is not reported as an image reference.

### FROM Instruction Detection
The detector extracts base image references from `FROM` instructions and resolves multi-stage build references:
- Direct image references (e.g., `FROM ubuntu:22.04`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public DockerfileComponentDetector(

public override IEnumerable<ComponentType> SupportedComponentTypes { get; } = [ComponentType.DockerReference];

public override int Version => 1;
public override int Version => 2;

protected override IList<string> SkippedFolders => ["node_modules"];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<EOF
set -eux
apt-get update
EOF
";

var loggerMock = new Mock<ILogger<DockerfileComponentDetector>>();

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<LogLevel>(level => level >= LogLevel.Warning),
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Never);
}

[TestMethod]
public async Task TestDockerfile_RunHeredocWithInterpreterAndSyntaxDirectiveAsync()
{
var dockerfile = @"# syntax=docker/dockerfile:1.4
FROM ubuntu:noble-20260113

RUN <<END_OF_SCRIPT bash
set -e
apt-get update
END_OF_SCRIPT
";

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/ubuntu");
dockerRef.Tag.Should().Be("noble-20260113");
}

[TestMethod]
public async Task TestDockerfile_HeredocMultiStageWithCopyFromStageAsync()
{
var dockerfile = @"
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
RUN <<EOF
set -eux
dotnet publish -c Release -o /out
EOF

FROM mcr.microsoft.com/dotnet/runtime-deps:8.0
COPY <<EOF /etc/app.conf
setting=value
EOF
COPY --from=build /out /app
";

var (scanResult, componentRecorder) = await this.DetectorTestUtility
.WithFile("Dockerfile", dockerfile)
.ExecuteDetectorAsync();

scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
var components = componentRecorder.GetDetectedComponents();
components.Should().HaveCount(2);

var repos = components
.Select(c => 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 <<EOF cat > /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");
}
}