From 0059cf249626a88ff0cbd7b90640ab222b0845fe Mon Sep 17 00:00:00 2001 From: Julian Pinzer Date: Fri, 17 Apr 2026 16:37:33 +0000 Subject: [PATCH 1/6] Add the DockerComposeComponentDetector and tests Co-authored-by: Copilot --- docs/detectors/README.md | 6 + docs/detectors/dockercompose.md | 48 +++ .../DockerReference/DockerReferenceUtility.cs | 47 +++ .../DetectorClass.cs | 3 + .../DockerComposeComponentDetector.cs | 124 ++++++++ .../Extensions/ServiceCollectionExtensions.cs | 4 + .../DockerComposeComponentDetectorTests.cs | 276 ++++++++++++++++++ .../dockercompose/docker-compose.override.yml | 5 + .../dockercompose/docker-compose.yml | 37 +++ 9 files changed, 550 insertions(+) create mode 100644 docs/detectors/dockercompose.md create mode 100644 src/Microsoft.ComponentDetection.Detectors/dockercompose/DockerComposeComponentDetector.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/DockerComposeComponentDetectorTests.cs create mode 100644 test/Microsoft.ComponentDetection.VerificationTests/resources/dockercompose/docker-compose.override.yml create mode 100644 test/Microsoft.ComponentDetection.VerificationTests/resources/dockercompose/docker-compose.yml diff --git a/docs/detectors/README.md b/docs/detectors/README.md index f15c1a72a..5465830db 100644 --- a/docs/detectors/README.md +++ b/docs/detectors/README.md @@ -18,6 +18,12 @@ | -------------------------- | ---------- | | CondaLockComponentDetector | DefaultOff | +- [Docker Compose](dockercompose.md) + +| Detector | Status | +| ------------------------------- | ---------- | +| DockerComposeComponentDetector | DefaultOff | + - [Dockerfile](dockerfile.md) | Detector | Status | diff --git a/docs/detectors/dockercompose.md b/docs/detectors/dockercompose.md new file mode 100644 index 000000000..f05e026ad --- /dev/null +++ b/docs/detectors/dockercompose.md @@ -0,0 +1,48 @@ +# Docker Compose Detection + +## Requirements + +Docker Compose detection depends on the following to successfully run: + +- One or more Docker Compose files matching the patterns: `docker-compose.yml`, `docker-compose.yaml`, `docker-compose.*.yml`, `docker-compose.*.yaml`, `compose.yml`, `compose.yaml`, `compose.*.yml`, `compose.*.yaml` + +The `DockerComposeComponentDetector` is a **DefaultOff** detector and must be explicitly enabled via the `--DetectorArgs` parameter. + +## Detection strategy + +The Docker Compose detector parses YAML compose files to extract Docker image references from service definitions. + +### Service Image Detection + +The detector looks for the `services` section and extracts the `image` field from each service: + +```yaml +services: + web: + image: nginx:1.21 + db: + image: postgres:14 +``` + +Services that only define a `build` directive without an `image` field are skipped, as they do not reference external Docker images. + +### Full Registry References + +The detector supports full registry image references: + +```yaml +services: + app: + image: ghcr.io/myorg/myapp:v2.0 +``` + +### Variable Resolution + +Images containing unresolved variables (e.g., `${TAG}` or `${REGISTRY:-docker.io}`) are skipped to avoid reporting incomplete or incorrect references. The detector checks for `$`, `{`, or `}` characters in image references. + +## Known limitations + +- **DefaultOff Status**: This detector must be explicitly enabled using `--DetectorArgs DockerCompose=EnableIfDefaultOff` +- **Variable Resolution**: Image references containing unresolved environment variables or template expressions are not reported, which may lead to under-reporting in compose files that heavily use variable substitution +- **Build-Only Services**: Services that only specify a `build` directive without an `image` field are not reported +- **No Dependency Graph**: All detected images are registered as independent components without parent-child relationships \ No newline at end of file diff --git a/src/Microsoft.ComponentDetection.Common/DockerReference/DockerReferenceUtility.cs b/src/Microsoft.ComponentDetection.Common/DockerReference/DockerReferenceUtility.cs index 022f8b612..63ce6f04a 100644 --- a/src/Microsoft.ComponentDetection.Common/DockerReference/DockerReferenceUtility.cs +++ b/src/Microsoft.ComponentDetection.Common/DockerReference/DockerReferenceUtility.cs @@ -38,6 +38,53 @@ public static class DockerReferenceUtility private const string LEGACYDEFAULTDOMAIN = "index.docker.io"; private const string OFFICIALREPOSITORYNAME = "library"; + /// + /// Returns true if the reference contains unresolved variable placeholders (e.g., ${VAR}, {{ .Values.tag }}). + /// Such references should be skipped before calling or . + /// + /// The image reference string to check. + /// true if the reference contains variable placeholder characters; otherwise false. + public static bool HasUnresolvedVariables(string reference) => + reference.IndexOfAny(['$', '{', '}']) >= 0; + + /// + /// Attempts to parse an image reference string into a . + /// Returns null if the reference contains unresolved variables or cannot be parsed. + /// + /// The image reference string to parse. + /// A if parsing succeeds; otherwise null. + public static DockerReference? TryParseImageReference(string imageReference) + { + if (HasUnresolvedVariables(imageReference)) + { + return null; + } + + try + { + return ParseFamiliarName(imageReference); + } + catch + { + return null; + } + } + + /// + /// Parses an image reference and registers it with the recorder if valid. + /// Silently skips references with unresolved variables or that cannot be parsed. + /// + /// The image reference string to parse. + /// The component recorder to register the image with. + public static void TryRegisterImageReference(string imageReference, ISingleFileComponentRecorder recorder) + { + var dockerRef = TryParseImageReference(imageReference); + if (dockerRef != null) + { + recorder.RegisterUsage(new DetectedComponent(dockerRef.ToTypedDockerReferenceComponent())); + } + } + public static DockerReference ParseQualifiedName(string qualifiedName) { var regexp = DockerRegex.ReferenceRegexp; diff --git a/src/Microsoft.ComponentDetection.Contracts/DetectorClass.cs b/src/Microsoft.ComponentDetection.Contracts/DetectorClass.cs index becfa4f9f..6b0b91cd4 100644 --- a/src/Microsoft.ComponentDetection.Contracts/DetectorClass.cs +++ b/src/Microsoft.ComponentDetection.Contracts/DetectorClass.cs @@ -47,4 +47,7 @@ public enum DetectorClass /// Indicates a detector applies to Swift packages. Swift, + + /// Indicates a detector applies to Docker Compose image references. + DockerCompose, } diff --git a/src/Microsoft.ComponentDetection.Detectors/dockercompose/DockerComposeComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/dockercompose/DockerComposeComponentDetector.cs new file mode 100644 index 000000000..c948c6a34 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/dockercompose/DockerComposeComponentDetector.cs @@ -0,0 +1,124 @@ +#nullable enable +namespace Microsoft.ComponentDetection.Detectors.DockerCompose; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Common; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.Internal; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.Extensions.Logging; +using YamlDotNet.RepresentationModel; + +public class DockerComposeComponentDetector : FileComponentDetector, IDefaultOffComponentDetector +{ + public DockerComposeComponentDetector( + IComponentStreamEnumerableFactory componentStreamEnumerableFactory, + IObservableDirectoryWalkerFactory walkerFactory, + ILogger logger) + { + this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory; + this.Scanner = walkerFactory; + this.Logger = logger; + } + + public override string Id => "DockerCompose"; + + public override IList SearchPatterns { get; } = + [ + "docker-compose.yml", "docker-compose.yaml", + "docker-compose.*.yml", "docker-compose.*.yaml", + "compose.yml", "compose.yaml", + "compose.*.yml", "compose.*.yaml", + ]; + + public override IEnumerable SupportedComponentTypes => [ComponentType.DockerReference]; + + public override int Version => 1; + + public override IEnumerable Categories => [nameof(DetectorClass.DockerCompose)]; + + protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary detectorArgs, CancellationToken cancellationToken = default) + { + var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; + var file = processRequest.ComponentStream; + + try + { + this.Logger.LogInformation("Discovered Docker Compose file: {Location}", file.Location); + + string contents; + using (var reader = new StreamReader(file.Stream)) + { + contents = await reader.ReadToEndAsync(cancellationToken); + } + + var yaml = new YamlStream(); + yaml.Load(new StringReader(contents)); + + if (yaml.Documents.Count == 0) + { + return; + } + + foreach (var document in yaml.Documents) + { + if (document.RootNode is YamlMappingNode rootMapping) + { + this.ExtractImageReferences(rootMapping, singleFileComponentRecorder); + } + } + } + catch (Exception e) + { + this.Logger.LogError(e, "Failed to parse Docker Compose file: {Location}", file.Location); + } + } + + private static YamlMappingNode? GetMappingChild(YamlMappingNode parent, string key) + { + foreach (var entry in parent.Children) + { + if (entry.Key is YamlScalarNode scalarKey && string.Equals(scalarKey.Value, key, StringComparison.OrdinalIgnoreCase)) + { + return entry.Value as YamlMappingNode; + } + } + + return null; + } + + private void ExtractImageReferences(YamlMappingNode rootMapping, ISingleFileComponentRecorder recorder) + { + var services = GetMappingChild(rootMapping, "services"); + if (services == null) + { + return; + } + + foreach (var serviceEntry in services.Children) + { + if (serviceEntry.Value is not YamlMappingNode serviceMapping) + { + continue; + } + + // Extract direct image: references + foreach (var entry in serviceMapping.Children) + { + var key = (entry.Key as YamlScalarNode)?.Value; + if (string.Equals(key, "image", StringComparison.OrdinalIgnoreCase)) + { + var imageRef = (entry.Value as YamlScalarNode)?.Value; + if (!string.IsNullOrWhiteSpace(imageRef)) + { + DockerReferenceUtility.TryRegisterImageReference(imageRef, recorder); + } + } + } + } + } +} diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs index 0e0e30360..37206fdf3 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs +++ b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ namespace Microsoft.ComponentDetection.Orchestrator.Extensions; using Microsoft.ComponentDetection.Contracts; using Microsoft.ComponentDetection.Detectors.CocoaPods; using Microsoft.ComponentDetection.Detectors.Conan; +using Microsoft.ComponentDetection.Detectors.DockerCompose; using Microsoft.ComponentDetection.Detectors.Dockerfile; using Microsoft.ComponentDetection.Detectors.DotNet; using Microsoft.ComponentDetection.Detectors.Go; @@ -84,6 +85,9 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s // Conda services.AddSingleton(); + // Docker Compose + services.AddSingleton(); + // Dockerfile services.AddSingleton(); diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/DockerComposeComponentDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/DockerComposeComponentDetectorTests.cs new file mode 100644 index 000000000..64fcbd56a --- /dev/null +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/DockerComposeComponentDetectorTests.cs @@ -0,0 +1,276 @@ +#nullable enable +namespace Microsoft.ComponentDetection.Detectors.Tests; + +using System.Linq; +using System.Threading.Tasks; +using AwesomeAssertions; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.ComponentDetection.Detectors.DockerCompose; +using Microsoft.ComponentDetection.TestsUtilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +[TestCategory("Governance/All")] +[TestCategory("Governance/ComponentDetection")] +public class DockerComposeComponentDetectorTests : BaseDetectorTest +{ + [TestMethod] + public async Task TestCompose_SingleServiceImageAsync() + { + var composeYaml = @" +version: '3' +services: + web: + image: nginx:1.21 + ports: + - ""80:80"" +"; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("docker-compose.yml", composeYaml) + .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/nginx"); + dockerRef.Tag.Should().Be("1.21"); + } + + [TestMethod] + public async Task TestCompose_MultipleServicesAsync() + { + var composeYaml = @" +version: '3' +services: + web: + image: nginx:1.21 + db: + image: postgres:15 + cache: + image: redis:7-alpine +"; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("docker-compose.yml", composeYaml) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + var components = componentRecorder.GetDetectedComponents(); + components.Should().HaveCount(3); + } + + [TestMethod] + public async Task TestCompose_FullRegistryImageAsync() + { + var composeYaml = @" +services: + app: + image: ghcr.io/myorg/myapp:v2.0 +"; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("compose.yaml", composeYaml) + .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.Domain.Should().Be("ghcr.io"); + dockerRef.Repository.Should().Be("myorg/myapp"); + dockerRef.Tag.Should().Be("v2.0"); + } + + [TestMethod] + public async Task TestCompose_BuildOnlyServiceIgnoredAsync() + { + var composeYaml = @" +version: '3' +services: + app: + build: ./app + ports: + - ""3000:3000"" +"; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("docker-compose.yml", composeYaml) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + + [TestMethod] + public async Task TestCompose_MixedBuildAndImageAsync() + { + var composeYaml = @" +version: '3' +services: + app: + build: ./app + db: + image: postgres:15 +"; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("docker-compose.yml", composeYaml) + .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/postgres"); + dockerRef.Tag.Should().Be("15"); + } + + [TestMethod] + public async Task TestCompose_NoServicesKeyAsync() + { + var composeYaml = @" +version: '3' +networks: + frontend: +"; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("docker-compose.yml", composeYaml) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + + [TestMethod] + public async Task TestCompose_ImageWithDigestAsync() + { + var composeYaml = @" +services: + app: + image: nginx@sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abc1 +"; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("docker-compose.yaml", composeYaml) + .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.Digest.Should().Be("sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abc1"); + } + + [TestMethod] + public async Task TestCompose_ImageWithTagAndDigestAsync() + { + var composeYaml = @" +services: + app: + image: nginx:1.21@sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abc1 +"; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("docker-compose.yaml", composeYaml) + .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.Tag.Should().Be("1.21"); + dockerRef.Digest.Should().Be("sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abc1"); + } + + [TestMethod] + public async Task TestCompose_OverrideFileAsync() + { + var composeYaml = @" +services: + web: + image: myregistry.io/web:latest +"; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("docker-compose.override.yml", composeYaml) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + var components = componentRecorder.GetDetectedComponents(); + components.Should().ContainSingle(); + } + + [TestMethod] + public async Task TestCompose_UnresolvedVariableSkippedAsync() + { + var composeYaml = @" +services: + app: + image: ${REGISTRY}/app:${TAG} + db: + image: postgres:15 +"; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("docker-compose.yml", composeYaml) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + var components = componentRecorder.GetDetectedComponents(); + + // Only the literal image reference (postgres:15) should be registered; + // the variable-interpolated image (${REGISTRY}/app:${TAG}) should be silently skipped. + components.Should().ContainSingle(); + var dockerRef = components.First().Component as DockerReferenceComponent; + dockerRef.Should().NotBeNull(); + dockerRef.Repository.Should().Be("library/postgres"); + } + + [TestMethod] + public async Task TestCompose_ComposeOverrideFileAsync() + { + var composeYaml = @" +services: + web: + image: nginx:1.21 +"; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("compose.override.yml", composeYaml) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().ContainSingle(); + } + + [TestMethod] + public async Task TestCompose_ComposeOverrideYamlAsync() + { + var composeYaml = @" +services: + db: + image: postgres:15 +"; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("compose.prod.yaml", composeYaml) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().ContainSingle(); + } +} diff --git a/test/Microsoft.ComponentDetection.VerificationTests/resources/dockercompose/docker-compose.override.yml b/test/Microsoft.ComponentDetection.VerificationTests/resources/dockercompose/docker-compose.override.yml new file mode 100644 index 000000000..6075b6789 --- /dev/null +++ b/test/Microsoft.ComponentDetection.VerificationTests/resources/dockercompose/docker-compose.override.yml @@ -0,0 +1,5 @@ +version: '3.8' +services: + debug: + image: busybox:1.35 + command: ['sh', '-c', 'sleep 3600'] \ No newline at end of file diff --git a/test/Microsoft.ComponentDetection.VerificationTests/resources/dockercompose/docker-compose.yml b/test/Microsoft.ComponentDetection.VerificationTests/resources/dockercompose/docker-compose.yml new file mode 100644 index 000000000..c5ce18f94 --- /dev/null +++ b/test/Microsoft.ComponentDetection.VerificationTests/resources/dockercompose/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3.8' +services: + web: + image: nginx:1.21 + ports: + - "8080:80" + depends_on: + - api + - db + + api: + image: ghcr.io/myorg/myapp:v2.0 + environment: + - DATABASE_URL=postgres://db:5432/app + depends_on: + - db + + db: + image: postgres:15 + volumes: + - db-data:/var/lib/postgresql/data + environment: + - POSTGRES_PASSWORD=secret + + cache: + image: redis:7-alpine + ports: + - "6379:6379" + + digest-only: + image: nginx@sha256:0d17b565c37bcbd895e9d92315a05c1c3c9a29f762b011a10c54a66cd53c9b31 + + tag-and-digest: + image: redis:7-alpine@sha256:0d17b565c37bcbd895e9d92315a05c1c3c9a29f762b011a10c54a66cd53c9b31 + +volumes: + db-data: \ No newline at end of file From a5a66f104a28bf4eb6cad2ba3ac04dc508363702 Mon Sep 17 00:00:00 2001 From: Julian Pinzer Date: Fri, 17 Apr 2026 16:38:20 +0000 Subject: [PATCH 2/6] Update the DockerfileComponentDetector to enable nullability and use the updated utilities to reduce code duplication. --- .../dockerfile/DockerfileComponentDetector.cs | 54 +++++-------------- 1 file changed, 12 insertions(+), 42 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs index d1d19f3a9..a06366fa6 100644 --- a/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs @@ -1,11 +1,9 @@ -#nullable disable +#nullable enable namespace Microsoft.ComponentDetection.Detectors.Dockerfile; using System; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.ComponentDetection.Common; @@ -36,7 +34,7 @@ public DockerfileComponentDetector( public override string Id { get; } = "DockerReference"; - public override IEnumerable Categories => [Enum.GetName(typeof(DetectorClass), DetectorClass.DockerReference)]; + public override IEnumerable Categories => [nameof(DetectorClass.DockerReference)]; public override IList SearchPatterns { get; } = ["dockerfile", "dockerfile.*", "*.dockerfile"]; @@ -84,12 +82,12 @@ private Task ParseDockerFileAsync(string fileContents, string fileLocation, ISin return Task.CompletedTask; } - private DockerReference ProcessDockerfileConstruct(DockerfileConstruct construct, char escapeChar, Dictionary stageNameMap) + private DockerReference? ProcessDockerfileConstruct(DockerfileConstruct construct, char escapeChar, Dictionary stageNameMap) { try { var instructionKeyword = construct.Type; - DockerReference baseImage = null; + DockerReference? baseImage = null; if (instructionKeyword == ConstructType.Instruction) { var constructType = construct.GetType().Name; @@ -115,10 +113,9 @@ private DockerReference ProcessDockerfileConstruct(DockerfileConstruct construct } } - private DockerReference ParseFromInstruction(DockerfileConstruct construct, char escapeChar, Dictionary stageNameMap) + private DockerReference? ParseFromInstruction(DockerfileConstruct construct, char escapeChar, Dictionary stageNameMap) { - var tokens = construct.Tokens.ToArray(); - var resolvedFromStatement = construct.ResolveVariables(escapeChar).TrimEnd(); + var resolvedFromStatement = construct.ResolveVariables(escapeChar)?.TrimEnd(); var fromInstruction = (FromInstruction)construct; var reference = fromInstruction.ImageName; if (string.IsNullOrWhiteSpace(resolvedFromStatement) || string.IsNullOrEmpty(reference)) @@ -143,25 +140,15 @@ private DockerReference ParseFromInstruction(DockerfileConstruct construct, char if (!string.IsNullOrEmpty(stageNameReference)) { - if (this.HasUnresolvedVariables(stageNameReference)) - { - return null; - } - - return DockerReferenceUtility.ParseFamiliarName(stageNameReference); - } - - if (this.HasUnresolvedVariables(reference)) - { - return null; + return DockerReferenceUtility.TryParseImageReference(stageNameReference); } - return DockerReferenceUtility.ParseFamiliarName(reference); + return DockerReferenceUtility.TryParseImageReference(reference); } - private DockerReference ParseCopyInstruction(DockerfileConstruct construct, char escapeChar, Dictionary stageNameMap) + private DockerReference? ParseCopyInstruction(DockerfileConstruct construct, char escapeChar, Dictionary stageNameMap) { - var resolvedCopyStatement = construct.ResolveVariables(escapeChar).TrimEnd(); + var resolvedCopyStatement = construct.ResolveVariables(escapeChar)?.TrimEnd(); var copyInstruction = (CopyInstruction)construct; var reference = copyInstruction.FromStageName; if (string.IsNullOrWhiteSpace(resolvedCopyStatement) || string.IsNullOrWhiteSpace(reference)) @@ -172,26 +159,9 @@ private DockerReference ParseCopyInstruction(DockerfileConstruct construct, char stageNameMap.TryGetValue(reference, out var stageNameReference); if (!string.IsNullOrEmpty(stageNameReference)) { - if (this.HasUnresolvedVariables(stageNameReference)) - { - return null; - } - else - { - return DockerReferenceUtility.ParseFamiliarName(stageNameReference); - } + return DockerReferenceUtility.TryParseImageReference(stageNameReference); } - if (this.HasUnresolvedVariables(reference)) - { - return null; - } - - return DockerReferenceUtility.ParseFamiliarName(reference); - } - - private bool HasUnresolvedVariables(string reference) - { - return new Regex("[${}]").IsMatch(reference); + return DockerReferenceUtility.TryParseImageReference(reference); } } From 330d5fc9656ee48cadbe573d85e8a6ee423320ce Mon Sep 17 00:00:00 2001 From: Julian Pinzer Date: Fri, 17 Apr 2026 17:38:15 +0000 Subject: [PATCH 3/6] Address copilot PR comments. And add new tests for DockerReferenceUtility Co-authored-by: Copilot --- docs/detectors/README.md | 6 +- .../dockerfile/DockerfileComponentDetector.cs | 21 +--- .../DockerReferenceUtilityTests.cs | 95 +++++++++++++++++++ 3 files changed, 103 insertions(+), 19 deletions(-) diff --git a/docs/detectors/README.md b/docs/detectors/README.md index 5465830db..5d96ec22f 100644 --- a/docs/detectors/README.md +++ b/docs/detectors/README.md @@ -20,9 +20,9 @@ - [Docker Compose](dockercompose.md) -| Detector | Status | -| ------------------------------- | ---------- | -| DockerComposeComponentDetector | DefaultOff | +| Detector | Status | +| ------------------------------ | ---------- | +| DockerComposeComponentDetector | DefaultOff | - [Dockerfile](dockerfile.md) diff --git a/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs index a06366fa6..204dc6fdb 100644 --- a/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs @@ -86,23 +86,12 @@ private Task ParseDockerFileAsync(string fileContents, string fileLocation, ISin { try { - var instructionKeyword = construct.Type; - DockerReference? baseImage = null; - if (instructionKeyword == ConstructType.Instruction) + var baseImage = construct switch { - var constructType = construct.GetType().Name; - switch (constructType) - { - case "FromInstruction": - baseImage = this.ParseFromInstruction(construct, escapeChar, stageNameMap); - break; - case "CopyInstruction": - baseImage = this.ParseCopyInstruction(construct, escapeChar, stageNameMap); - break; - default: - break; - } - } + FromInstruction => this.ParseFromInstruction(construct, escapeChar, stageNameMap), + CopyInstruction => this.ParseCopyInstruction(construct, escapeChar, stageNameMap), + _ => null, + }; return baseImage; } diff --git a/test/Microsoft.ComponentDetection.Common.Tests/DockerReferenceUtilityTests.cs b/test/Microsoft.ComponentDetection.Common.Tests/DockerReferenceUtilityTests.cs index bc4f4f1d4..c0e9d49ff 100644 --- a/test/Microsoft.ComponentDetection.Common.Tests/DockerReferenceUtilityTests.cs +++ b/test/Microsoft.ComponentDetection.Common.Tests/DockerReferenceUtilityTests.cs @@ -4,7 +4,9 @@ namespace Microsoft.ComponentDetection.Common.Tests; using System; using AwesomeAssertions; using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.BcdeModels; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; [TestClass] [TestCategory("Governance/All")] @@ -268,4 +270,97 @@ public void ParseAll_ParsesFamiliarNames() result.Should().NotBeNull(); result.Should().BeAssignableTo(); } + + [TestMethod] + public void HasUnresolvedVariables_ReturnsTrueForDollarSign() + { + DockerReferenceUtility.HasUnresolvedVariables("${MY_IMAGE}:latest").Should().BeTrue(); + } + + [TestMethod] + public void HasUnresolvedVariables_ReturnsTrueForBraces() + { + DockerReferenceUtility.HasUnresolvedVariables("{{ .Values.image }}").Should().BeTrue(); + } + + [TestMethod] + public void HasUnresolvedVariables_ReturnsFalseForPlainReference() + { + DockerReferenceUtility.HasUnresolvedVariables("docker.io/library/nginx:latest").Should().BeFalse(); + } + + [TestMethod] + public void TryParseImageReference_ReturnsNullForUnresolvedVariables() + { + DockerReferenceUtility.TryParseImageReference("${IMAGE}:latest").Should().BeNull(); + } + + [TestMethod] + public void TryParseImageReference_ReturnsNullForInvalidReference() + { + DockerReferenceUtility.TryParseImageReference("docker.io/library/Nginx").Should().BeNull(); + } + + [TestMethod] + public void TryParseImageReference_ReturnsParsedReferenceForValidInput() + { + var result = DockerReferenceUtility.TryParseImageReference("nginx:latest"); + + result.Should().NotBeNull(); + result.Should().BeAssignableTo(); + } + + [TestMethod] + public void TryParseImageReference_ReturnsParsedReferenceForDigest() + { + var digest = $"sha256:{new string('a', 64)}"; + var result = DockerReferenceUtility.TryParseImageReference($"nginx@{digest}"); + + result.Should().NotBeNull(); + result.Should().BeAssignableTo(); + ((CanonicalReference)result).Digest.Should().Be(digest); + } + + [TestMethod] + public void TryParseImageReference_ReturnsParsedReferenceForTagAndDigest() + { + var digest = $"sha256:{new string('a', 64)}"; + var result = DockerReferenceUtility.TryParseImageReference($"nginx:latest@{digest}"); + + result.Should().NotBeNull(); + result.Should().BeAssignableTo(); + var dualRef = (DualReference)result; + dualRef.Tag.Should().Be("latest"); + dualRef.Digest.Should().Be(digest); + } + + [TestMethod] + public void TryRegisterImageReference_RegistersValidReference() + { + var recorder = new Mock(); + + DockerReferenceUtility.TryRegisterImageReference("nginx:latest", recorder.Object); + + recorder.Verify(r => r.RegisterUsage(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + } + + [TestMethod] + public void TryRegisterImageReference_SkipsUnresolvedVariables() + { + var recorder = new Mock(); + + DockerReferenceUtility.TryRegisterImageReference("${IMAGE}", recorder.Object); + + recorder.Verify(r => r.RegisterUsage(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [TestMethod] + public void TryRegisterImageReference_SkipsInvalidReference() + { + var recorder = new Mock(); + + DockerReferenceUtility.TryRegisterImageReference("docker.io/library/Nginx", recorder.Object); + + recorder.Verify(r => r.RegisterUsage(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } } From fbfec9032337625c4afcc37ed3faecab611056a5 Mon Sep 17 00:00:00 2001 From: Julian Pinzer Date: Fri, 17 Apr 2026 17:58:43 +0000 Subject: [PATCH 4/6] Address PR comments. Co-authored-by: Copilot --- .../DockerReference/DockerReferenceUtility.cs | 13 +++---------- .../dockercompose/DockerComposeComponentDetector.cs | 1 - .../dockerfile/DockerfileComponentDetector.cs | 1 - .../DockerReferenceUtilityTests.cs | 11 +++++++---- 4 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Common/DockerReference/DockerReferenceUtility.cs b/src/Microsoft.ComponentDetection.Common/DockerReference/DockerReferenceUtility.cs index 63ce6f04a..3c9919b21 100644 --- a/src/Microsoft.ComponentDetection.Common/DockerReference/DockerReferenceUtility.cs +++ b/src/Microsoft.ComponentDetection.Common/DockerReference/DockerReferenceUtility.cs @@ -49,10 +49,10 @@ public static bool HasUnresolvedVariables(string reference) => /// /// Attempts to parse an image reference string into a . - /// Returns null if the reference contains unresolved variables or cannot be parsed. + /// Returns null if the reference contains unresolved variables. /// /// The image reference string to parse. - /// A if parsing succeeds; otherwise null. + /// A if parsing succeeds; otherwise null if it has unresolved variables, or an exception is thrown. public static DockerReference? TryParseImageReference(string imageReference) { if (HasUnresolvedVariables(imageReference)) @@ -60,14 +60,7 @@ public static bool HasUnresolvedVariables(string reference) => return null; } - try - { - return ParseFamiliarName(imageReference); - } - catch - { - return null; - } + return ParseFamiliarName(imageReference); } /// diff --git a/src/Microsoft.ComponentDetection.Detectors/dockercompose/DockerComposeComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/dockercompose/DockerComposeComponentDetector.cs index c948c6a34..5930735c0 100644 --- a/src/Microsoft.ComponentDetection.Detectors/dockercompose/DockerComposeComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/dockercompose/DockerComposeComponentDetector.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Microsoft.ComponentDetection.Detectors.DockerCompose; using System; diff --git a/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs index 204dc6fdb..0e5c8c9b2 100644 --- a/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Microsoft.ComponentDetection.Detectors.Dockerfile; using System; diff --git a/test/Microsoft.ComponentDetection.Common.Tests/DockerReferenceUtilityTests.cs b/test/Microsoft.ComponentDetection.Common.Tests/DockerReferenceUtilityTests.cs index c0e9d49ff..214a535ca 100644 --- a/test/Microsoft.ComponentDetection.Common.Tests/DockerReferenceUtilityTests.cs +++ b/test/Microsoft.ComponentDetection.Common.Tests/DockerReferenceUtilityTests.cs @@ -296,9 +296,11 @@ public void TryParseImageReference_ReturnsNullForUnresolvedVariables() } [TestMethod] - public void TryParseImageReference_ReturnsNullForInvalidReference() + public void TryParseImageReference_ThrowsForInvalidReference() { - DockerReferenceUtility.TryParseImageReference("docker.io/library/Nginx").Should().BeNull(); + var func = () => DockerReferenceUtility.TryParseImageReference("docker.io/library/Nginx"); + + func.Should().Throw(); } [TestMethod] @@ -355,12 +357,13 @@ public void TryRegisterImageReference_SkipsUnresolvedVariables() } [TestMethod] - public void TryRegisterImageReference_SkipsInvalidReference() + public void TryRegisterImageReference_ThrowsForInvalidReference() { var recorder = new Mock(); - DockerReferenceUtility.TryRegisterImageReference("docker.io/library/Nginx", recorder.Object); + var func = () => DockerReferenceUtility.TryRegisterImageReference("docker.io/library/Nginx", recorder.Object); + func.Should().Throw(); recorder.Verify(r => r.RegisterUsage(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } } From 7e8f1ef30ad2996e8a1eb95adeec7d9ad092b8ee Mon Sep 17 00:00:00 2001 From: Julian Pinzer Date: Fri, 17 Apr 2026 19:07:09 +0000 Subject: [PATCH 5/6] Add logging to some of the utils in DockerReferenceUtility for parse failures Co-authored-by: Copilot --- .../DockerReference/DockerReferenceUtility.cs | 40 ++++++++++++++----- .../DockerComposeComponentDetector.cs | 2 +- .../dockerfile/DockerfileComponentDetector.cs | 9 +---- .../DockerReferenceUtilityTests.cs | 31 ++++++++++---- 4 files changed, 58 insertions(+), 24 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Common/DockerReference/DockerReferenceUtility.cs b/src/Microsoft.ComponentDetection.Common/DockerReference/DockerReferenceUtility.cs index 3c9919b21..fe3122647 100644 --- a/src/Microsoft.ComponentDetection.Common/DockerReference/DockerReferenceUtility.cs +++ b/src/Microsoft.ComponentDetection.Common/DockerReference/DockerReferenceUtility.cs @@ -29,6 +29,7 @@ namespace Microsoft.ComponentDetection.Common; using System; using System.Diagnostics.CodeAnalysis; using Microsoft.ComponentDetection.Contracts; +using Microsoft.Extensions.Logging; public static class DockerReferenceUtility { @@ -49,32 +50,53 @@ public static bool HasUnresolvedVariables(string reference) => /// /// Attempts to parse an image reference string into a . - /// Returns null if the reference contains unresolved variables. + /// Returns null if the reference contains unresolved variables or cannot be parsed. /// /// The image reference string to parse. - /// A if parsing succeeds; otherwise null if it has unresolved variables, or an exception is thrown. - public static DockerReference? TryParseImageReference(string imageReference) + /// Optional logger for recording parse failures. + /// A if parsing succeeds; otherwise null. + public static DockerReference? TryParseImageReference(string imageReference, ILogger? logger = null) { if (HasUnresolvedVariables(imageReference)) { return null; } - return ParseFamiliarName(imageReference); + try + { + return ParseFamiliarName(imageReference); + } + catch (DockerReferenceException ex) + { + logger?.LogWarning(ex, "Failed to parse image reference '{ImageReference}'.", imageReference); + return null; + } } /// /// Parses an image reference and registers it with the recorder if valid. - /// Silently skips references with unresolved variables or that cannot be parsed. + /// Skips references with unresolved variables or that cannot be parsed, + /// logging a warning for parse failures so that remaining entries continue to be processed. /// /// The image reference string to parse. /// The component recorder to register the image with. - public static void TryRegisterImageReference(string imageReference, ISingleFileComponentRecorder recorder) + /// Optional logger for recording parse failures. + public static void TryRegisterImageReference(string imageReference, ISingleFileComponentRecorder recorder, ILogger? logger = null) + { + var dockerRef = TryParseImageReference(imageReference, logger); + TryRegisterImageReference(dockerRef, recorder); + } + + /// + /// Registers a pre-parsed with the recorder if non-null. + /// + /// The parsed docker reference, or null to skip. + /// The component recorder to register the image with. + public static void TryRegisterImageReference(DockerReference? dockerReference, ISingleFileComponentRecorder recorder) { - var dockerRef = TryParseImageReference(imageReference); - if (dockerRef != null) + if (dockerReference != null) { - recorder.RegisterUsage(new DetectedComponent(dockerRef.ToTypedDockerReferenceComponent())); + recorder.RegisterUsage(new DetectedComponent(dockerReference.ToTypedDockerReferenceComponent())); } } diff --git a/src/Microsoft.ComponentDetection.Detectors/dockercompose/DockerComposeComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/dockercompose/DockerComposeComponentDetector.cs index 5930735c0..a582728a1 100644 --- a/src/Microsoft.ComponentDetection.Detectors/dockercompose/DockerComposeComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/dockercompose/DockerComposeComponentDetector.cs @@ -114,7 +114,7 @@ private void ExtractImageReferences(YamlMappingNode rootMapping, ISingleFileComp var imageRef = (entry.Value as YamlScalarNode)?.Value; if (!string.IsNullOrWhiteSpace(imageRef)) { - DockerReferenceUtility.TryRegisterImageReference(imageRef, recorder); + DockerReferenceUtility.TryRegisterImageReference(imageRef, recorder, this.Logger); } } } diff --git a/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs index 0e5c8c9b2..a80aa72f3 100644 --- a/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs @@ -72,10 +72,7 @@ private Task ParseDockerFileAsync(string fileContents, string fileLocation, ISin foreach (var instruction in instructions) { var imageReference = this.ProcessDockerfileConstruct(instruction, dockerfileModel.EscapeChar, stageNameMap); - if (imageReference != null) - { - singleFileComponentRecorder.RegisterUsage(new DetectedComponent(imageReference.ToTypedDockerReferenceComponent())); - } + DockerReferenceUtility.TryRegisterImageReference(imageReference, singleFileComponentRecorder); } return Task.CompletedTask; @@ -85,14 +82,12 @@ private Task ParseDockerFileAsync(string fileContents, string fileLocation, ISin { try { - var baseImage = construct switch + return construct switch { FromInstruction => this.ParseFromInstruction(construct, escapeChar, stageNameMap), CopyInstruction => this.ParseCopyInstruction(construct, escapeChar, stageNameMap), _ => null, }; - - return baseImage; } catch (Exception e) { diff --git a/test/Microsoft.ComponentDetection.Common.Tests/DockerReferenceUtilityTests.cs b/test/Microsoft.ComponentDetection.Common.Tests/DockerReferenceUtilityTests.cs index 214a535ca..d50c65be0 100644 --- a/test/Microsoft.ComponentDetection.Common.Tests/DockerReferenceUtilityTests.cs +++ b/test/Microsoft.ComponentDetection.Common.Tests/DockerReferenceUtilityTests.cs @@ -5,6 +5,7 @@ namespace Microsoft.ComponentDetection.Common.Tests; using AwesomeAssertions; using Microsoft.ComponentDetection.Contracts; using Microsoft.ComponentDetection.Contracts.BcdeModels; +using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -296,11 +297,9 @@ public void TryParseImageReference_ReturnsNullForUnresolvedVariables() } [TestMethod] - public void TryParseImageReference_ThrowsForInvalidReference() + public void TryParseImageReference_ReturnsNullForInvalidReference() { - var func = () => DockerReferenceUtility.TryParseImageReference("docker.io/library/Nginx"); - - func.Should().Throw(); + DockerReferenceUtility.TryParseImageReference("docker.io/library/Nginx").Should().BeNull(); } [TestMethod] @@ -357,13 +356,31 @@ public void TryRegisterImageReference_SkipsUnresolvedVariables() } [TestMethod] - public void TryRegisterImageReference_ThrowsForInvalidReference() + public void TryRegisterImageReference_SkipsInvalidReference() { var recorder = new Mock(); - var func = () => DockerReferenceUtility.TryRegisterImageReference("docker.io/library/Nginx", recorder.Object); + DockerReferenceUtility.TryRegisterImageReference("docker.io/library/Nginx", recorder.Object); + + recorder.Verify(r => r.RegisterUsage(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [TestMethod] + public void TryRegisterImageReference_LogsWarningForInvalidReference() + { + var recorder = new Mock(); + var logger = new Mock(); + + DockerReferenceUtility.TryRegisterImageReference("docker.io/library/Nginx", recorder.Object, logger.Object); - func.Should().Throw(); recorder.Verify(r => r.RegisterUsage(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + logger.Verify( + l => l.Log( + LogLevel.Warning, + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>()), + Times.Once); } } From 6d054fced4c6b2e1c121977db3eb9626cbc8872c Mon Sep 17 00:00:00 2001 From: Julian Pinzer Date: Mon, 20 Apr 2026 17:49:19 +0000 Subject: [PATCH 6/6] I missed passing in the logger to the DockerReference parsing from the Dockerfile detector --- .../dockerfile/DockerfileComponentDetector.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs index a80aa72f3..e314fb82d 100644 --- a/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/dockerfile/DockerfileComponentDetector.cs @@ -123,10 +123,10 @@ private Task ParseDockerFileAsync(string fileContents, string fileLocation, ISin if (!string.IsNullOrEmpty(stageNameReference)) { - return DockerReferenceUtility.TryParseImageReference(stageNameReference); + return DockerReferenceUtility.TryParseImageReference(stageNameReference, this.Logger); } - return DockerReferenceUtility.TryParseImageReference(reference); + return DockerReferenceUtility.TryParseImageReference(reference, this.Logger); } private DockerReference? ParseCopyInstruction(DockerfileConstruct construct, char escapeChar, Dictionary stageNameMap) @@ -142,9 +142,9 @@ private Task ParseDockerFileAsync(string fileContents, string fileLocation, ISin stageNameMap.TryGetValue(reference, out var stageNameReference); if (!string.IsNullOrEmpty(stageNameReference)) { - return DockerReferenceUtility.TryParseImageReference(stageNameReference); + return DockerReferenceUtility.TryParseImageReference(stageNameReference, this.Logger); } - return DockerReferenceUtility.TryParseImageReference(reference); + return DockerReferenceUtility.TryParseImageReference(reference, this.Logger); } }