Skip to content

Commit 5fa9cb1

Browse files
jpinzCopilot
andcommitted
Add DockerfileComponentDetectorTests
Co-authored-by: Copilot <copilot@github.com>
1 parent e880b90 commit 5fa9cb1

1 file changed

Lines changed: 260 additions & 0 deletions

File tree

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
#nullable enable
2+
namespace Microsoft.ComponentDetection.Detectors.Tests;
3+
4+
using System.Linq;
5+
using System.Threading.Tasks;
6+
using AwesomeAssertions;
7+
using Microsoft.ComponentDetection.Contracts;
8+
using Microsoft.ComponentDetection.Contracts.TypedComponent;
9+
using Microsoft.ComponentDetection.Detectors.Dockerfile;
10+
using Microsoft.ComponentDetection.TestsUtilities;
11+
using Microsoft.VisualStudio.TestTools.UnitTesting;
12+
using Moq;
13+
14+
[TestClass]
15+
[TestCategory("Governance/All")]
16+
[TestCategory("Governance/ComponentDetection")]
17+
public class DockerfileComponentDetectorTests : BaseDetectorTest<DockerfileComponentDetector>
18+
{
19+
public DockerfileComponentDetectorTests() =>
20+
this.DetectorTestUtility
21+
.AddServiceMock(new Mock<ICommandLineInvocationService>())
22+
.AddServiceMock(new Mock<IEnvironmentVariableService>());
23+
24+
[TestMethod]
25+
public async Task TestDockerfile_SingleFromInstructionAsync()
26+
{
27+
var dockerfile = @"
28+
FROM nginx:1.21
29+
";
30+
31+
var (scanResult, componentRecorder) = await this.DetectorTestUtility
32+
.WithFile("Dockerfile", dockerfile)
33+
.ExecuteDetectorAsync();
34+
35+
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
36+
var components = componentRecorder.GetDetectedComponents();
37+
components.Should().ContainSingle();
38+
39+
var dockerRef = components.First().Component as DockerReferenceComponent;
40+
dockerRef.Should().NotBeNull();
41+
dockerRef!.Repository.Should().Be("library/nginx");
42+
dockerRef.Tag.Should().Be("1.21");
43+
dockerRef.Digest.Should().BeNull();
44+
}
45+
46+
[TestMethod]
47+
public async Task TestDockerfile_FromWithRegistryAsync()
48+
{
49+
var dockerfile = @"
50+
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
51+
WORKDIR /app
52+
";
53+
54+
var (scanResult, componentRecorder) = await this.DetectorTestUtility
55+
.WithFile("Dockerfile", dockerfile)
56+
.ExecuteDetectorAsync();
57+
58+
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
59+
var components = componentRecorder.GetDetectedComponents();
60+
components.Should().ContainSingle();
61+
62+
var dockerRef = components.First().Component as DockerReferenceComponent;
63+
dockerRef.Should().NotBeNull();
64+
dockerRef!.Domain.Should().Be("mcr.microsoft.com");
65+
dockerRef.Repository.Should().Be("dotnet/sdk");
66+
dockerRef.Tag.Should().Be("8.0");
67+
dockerRef.Digest.Should().BeNull();
68+
}
69+
70+
[TestMethod]
71+
public async Task TestDockerfile_FromWithDigestAsync()
72+
{
73+
var dockerfile = @"
74+
FROM nginx@sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abc1
75+
";
76+
77+
var (scanResult, componentRecorder) = await this.DetectorTestUtility
78+
.WithFile("Dockerfile", dockerfile)
79+
.ExecuteDetectorAsync();
80+
81+
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
82+
var components = componentRecorder.GetDetectedComponents();
83+
components.Should().ContainSingle();
84+
85+
var dockerRef = components.First().Component as DockerReferenceComponent;
86+
dockerRef.Should().NotBeNull();
87+
dockerRef!.Digest.Should().Be("sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abc1");
88+
dockerRef.Tag.Should().BeNull();
89+
}
90+
91+
[TestMethod]
92+
public async Task TestDockerfile_MultiStageBuildAsync()
93+
{
94+
var dockerfile = @"
95+
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
96+
WORKDIR /app
97+
COPY . .
98+
RUN dotnet publish -c Release -o out
99+
100+
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0 AS runtime
101+
WORKDIR /app
102+
COPY --from=build /app/out ./
103+
ENTRYPOINT [""/app/MyApp""]
104+
";
105+
106+
var (scanResult, componentRecorder) = await this.DetectorTestUtility
107+
.WithFile("Dockerfile", dockerfile)
108+
.ExecuteDetectorAsync();
109+
110+
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
111+
var components = componentRecorder.GetDetectedComponents();
112+
components.Should().HaveCount(2);
113+
114+
var repos = components
115+
.Select(c => c.Component as DockerReferenceComponent)
116+
.Where(c => c != null)
117+
.Select(c => c!.Repository)
118+
.ToList();
119+
repos.Should().Contain("dotnet/sdk");
120+
repos.Should().Contain("dotnet/runtime-deps");
121+
}
122+
123+
[TestMethod]
124+
public async Task TestDockerfile_CopyFromStageNameDoesNotCreateExtraComponentAsync()
125+
{
126+
// COPY --from=<stage> references a previous build stage and should not yield a separate image component.
127+
var dockerfile = @"
128+
FROM nginx:1.21 AS build
129+
FROM alpine:3.18 AS runtime
130+
COPY --from=build /etc/nginx/nginx.conf /etc/nginx/nginx.conf
131+
";
132+
133+
var (scanResult, componentRecorder) = await this.DetectorTestUtility
134+
.WithFile("Dockerfile", dockerfile)
135+
.ExecuteDetectorAsync();
136+
137+
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
138+
var components = componentRecorder.GetDetectedComponents().ToList();
139+
140+
// Two FROM instructions => two images. The COPY --from=build should resolve back to nginx:1.21,
141+
// which is already registered, so no new component is added.
142+
components.Should().HaveCount(2);
143+
var repos = components
144+
.Select(c => (c.Component as DockerReferenceComponent)!.Repository)
145+
.ToList();
146+
repos.Should().Contain("library/nginx");
147+
repos.Should().Contain("library/alpine");
148+
}
149+
150+
[TestMethod]
151+
public async Task TestDockerfile_CopyFromExternalImageAsync()
152+
{
153+
// COPY --from=<image> references an image directly and should produce a component.
154+
var dockerfile = @"
155+
FROM alpine:3.18
156+
COPY --from=busybox:1.36 /bin/busybox /usr/local/bin/busybox
157+
";
158+
159+
var (scanResult, componentRecorder) = await this.DetectorTestUtility
160+
.WithFile("Dockerfile", dockerfile)
161+
.ExecuteDetectorAsync();
162+
163+
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
164+
var components = componentRecorder.GetDetectedComponents().ToList();
165+
components.Should().HaveCount(2);
166+
167+
var repos = components
168+
.Select(c => (c.Component as DockerReferenceComponent)!.Repository)
169+
.ToList();
170+
repos.Should().Contain("library/alpine");
171+
repos.Should().Contain("library/busybox");
172+
}
173+
174+
[TestMethod]
175+
public async Task TestDockerfile_LowercaseFilenameAsync()
176+
{
177+
var dockerfile = @"FROM redis:7-alpine";
178+
179+
var (scanResult, componentRecorder) = await this.DetectorTestUtility
180+
.WithFile("dockerfile", dockerfile)
181+
.ExecuteDetectorAsync();
182+
183+
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
184+
componentRecorder.GetDetectedComponents().Should().ContainSingle();
185+
}
186+
187+
[TestMethod]
188+
public async Task TestDockerfile_ExtensionFilenameAsync()
189+
{
190+
var dockerfile = @"FROM redis:7-alpine";
191+
192+
var (scanResult, componentRecorder) = await this.DetectorTestUtility
193+
.WithFile("app.dockerfile", dockerfile)
194+
.ExecuteDetectorAsync();
195+
196+
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
197+
componentRecorder.GetDetectedComponents().Should().ContainSingle();
198+
}
199+
200+
[TestMethod]
201+
public async Task TestDockerfile_PrefixedFilenameAsync()
202+
{
203+
var dockerfile = @"FROM redis:7-alpine";
204+
205+
var (scanResult, componentRecorder) = await this.DetectorTestUtility
206+
.WithFile("Dockerfile.prod", dockerfile)
207+
.ExecuteDetectorAsync();
208+
209+
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
210+
componentRecorder.GetDetectedComponents().Should().ContainSingle();
211+
}
212+
213+
[TestMethod]
214+
public async Task TestDockerfile_NoFromInstructionsAsync()
215+
{
216+
var dockerfile = @"
217+
# This Dockerfile has no FROM instructions
218+
ARG BUILD_VERSION=1.0
219+
";
220+
221+
var (scanResult, componentRecorder) = await this.DetectorTestUtility
222+
.WithFile("Dockerfile", dockerfile)
223+
.ExecuteDetectorAsync();
224+
225+
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
226+
componentRecorder.GetDetectedComponents().Should().BeEmpty();
227+
}
228+
229+
[TestMethod]
230+
public async Task TestDockerfile_MalformedContentAsync()
231+
{
232+
// Garbage content should not crash the detector.
233+
var dockerfile = "this is not a dockerfile at all { ] : >";
234+
235+
var (scanResult, componentRecorder) = await this.DetectorTestUtility
236+
.WithFile("Dockerfile", dockerfile)
237+
.ExecuteDetectorAsync();
238+
239+
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
240+
componentRecorder.GetDetectedComponents().Should().BeEmpty();
241+
}
242+
243+
[TestMethod]
244+
public async Task TestDockerfile_FromWithUnresolvedArgVariableIsSkippedAsync()
245+
{
246+
// References containing unresolved variable placeholders (e.g. ${BASE_TAG}) cannot be parsed
247+
// into a concrete image identity and are skipped by DockerReferenceUtility.
248+
var dockerfile = @"
249+
ARG BASE_TAG=1.21
250+
FROM nginx:${BASE_TAG}
251+
";
252+
253+
var (scanResult, componentRecorder) = await this.DetectorTestUtility
254+
.WithFile("Dockerfile", dockerfile)
255+
.ExecuteDetectorAsync();
256+
257+
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
258+
componentRecorder.GetDetectedComponents().Should().BeEmpty();
259+
}
260+
}

0 commit comments

Comments
 (0)