Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1a3f651
Add Docker Compose, Helm, and Kubernetes component detectors
jpinz Apr 2, 2026
077d1d0
Add tests for Docker Compose, Dockerfile, Helm, and Kubernetes compon…
jpinz Apr 2, 2026
fd74cb5
Add `#nullable disable` to the new files
jpinz Apr 2, 2026
d5c91ed
Fix helm file patterns, and update tests
jpinz Apr 2, 2026
a111856
Add tests for Docker, Helm, and Kubernetes images with tag and digest
jpinz Apr 3, 2026
70776f8
Add support for additional Docker Compose file patterns and new tests…
jpinz Apr 3, 2026
a099d56
Refactor DockerCompose and Kubernetes component detectors to address …
jpinz Apr 3, 2026
0bbc3db
Refine HelmComponentDetector to process only relevant values files an…
jpinz Apr 3, 2026
5c4e487
Add default to switch cases
jpinz Apr 3, 2026
b5d3fbf
Enhance KubernetesComponentDetector to skip non-manifest files and im…
jpinz Apr 3, 2026
5158b75
Move DockerCompose ordering in DetectorClass enum
jpinz Apr 3, 2026
a26f434
Merge branch 'main' into jupinzer/add_container_detectors
jpinz Apr 3, 2026
0873316
Fix build issues
jpinz Apr 3, 2026
ea3b0a4
Skip image references with unresolved variables in DockerCompose, Hel…
Copilot Apr 3, 2026
5984b1c
Remove accidentally committed .nuget/nuget.exe and add to .gitignore
Copilot Apr 3, 2026
9d56f38
Fix failing tests
jpinz Apr 3, 2026
8bce22b
Merge branch 'main' into jupinzer/add_container_detectors
jpinz Apr 3, 2026
ad592b2
Add docs for the new detectors
jpinz Apr 3, 2026
9ae1dd4
Add a new Containers DetectorCategory that enabled Dockerfile, Docker…
jpinz Apr 3, 2026
70762f1
Use shared HasUnresolvedVariables in Dockerfile detector, add Helm co…
Copilot Apr 3, 2026
d220257
Fix AllowedDetectorIds+Categories intersection bug in DetectorRestric…
Copilot Apr 3, 2026
dff7d5f
Rename test variables for clarity in DetectorRestrictionServiceTests
Copilot Apr 3, 2026
569babe
Refactor DockerfileComponentDetector to use DockerReferenceUtility fo…
jpinz Apr 3, 2026
f651b13
Revert Containers DetectorCategory support
jpinz Apr 3, 2026
7f08f43
Fix Helm detector file ordering via two-pass OnPrepareDetectionAsync
Copilot Apr 3, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ launchSettings.json

# Dev nupkgs
dev-source
.nuget/
18 changes: 18 additions & 0 deletions docs/detectors/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
| -------------------------- | ---------- |
| CondaLockComponentDetector | DefaultOff |

- [Docker Compose](dockercompose.md)

| Detector | Status |
| ------------------------------- | ---------- |
| DockerComposeComponentDetector | DefaultOff |

- [Dockerfile](dockerfile.md)

| Detector | Status |
Expand All @@ -42,12 +48,24 @@
| ----------------------- | ------ |
| GradleComponentDetector | Stable |

- [Helm](helm.md)

| Detector | Status |
| ---------------------- | ---------- |
| HelmComponentDetector | DefaultOff |

- [Ivy](ivy.md)

| Detector | Status |
| ----------- | ------------ |
| IvyDetector | Experimental |

- [Kubernetes](kubernetes.md)

| Detector | Status |
| ------------------------------ | ---------- |
| KubernetesComponentDetector | DefaultOff |

- [Linux](linux.md)

| Detector | Status |
Expand Down
48 changes: 48 additions & 0 deletions docs/detectors/dockercompose.md
Original file line number Diff line number Diff line change
@@ -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 `{{ .Values.tag }}`) 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
50 changes: 50 additions & 0 deletions docs/detectors/helm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Helm Detection

## Requirements

Helm detection depends on the following to successfully run:

- One or more Helm values files matching the patterns: `*values*.yaml`, `*values*.yml`
- Chart metadata files (`Chart.yaml`, `Chart.yml`, `chart.yaml`, `chart.yml`) are matched for file discovery but only values files are parsed for image references

The `HelmComponentDetector` is a **DefaultOff** detector and must be explicitly enabled via the `--DetectorArgs` parameter.

## Detection strategy

The Helm detector parses Helm values YAML files to extract Docker image references. It recursively walks the YAML tree looking for `image` keys.

### Direct Image Strings

The detector recognizes image references specified as simple strings:

```yaml
image: nginx:1.21
```

### Structured Image Objects

The detector also supports the common Helm chart pattern of structured image definitions:

```yaml
image:
registry: ghcr.io
repository: org/myimage
tag: v1.0
```

The `registry` and `tag` fields are optional. When present, the detector reconstructs the full image reference. The `digest` field is also supported.

### Recursive Search

The detector recursively traverses all nested mappings and sequences in the values file, detecting image references at any depth in the YAML structure.

### Variable Resolution

Images containing unresolved variables (e.g., `{{ .Values.tag }}`) 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 Helm=EnableIfDefaultOff`
- **Values Files Only**: Only files with `values` in the name are parsed for image references. Chart.yaml files are matched but not processed
- **Variable Resolution**: Image references containing unresolved Helm template expressions are not reported
- **No Dependency Graph**: All detected images are registered as independent components without parent-child relationships
58 changes: 58 additions & 0 deletions docs/detectors/kubernetes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Kubernetes Detection

## Requirements

Kubernetes detection depends on the following to successfully run:

- One or more Kubernetes manifest files matching the patterns: `*.yaml`, `*.yml`
- Manifests must contain both `apiVersion` and `kind` fields to be recognized as Kubernetes resources

The `KubernetesComponentDetector` is a **DefaultOff** detector and must be explicitly enabled via the `--DetectorArgs` parameter.

## Detection strategy

The Kubernetes detector parses Kubernetes manifest YAML files to extract Docker image references from container specifications.

### Supported Resource Kinds

The detector recognizes the following Kubernetes resource kinds:

- `Pod`
- `Deployment`
- `StatefulSet`
- `DaemonSet`
- `ReplicaSet`
- `Job`
- `CronJob`
- `ReplicationController`

Files with an unrecognized `kind` or missing `apiVersion`/`kind` fields are skipped.

### Container Image Detection

The detector extracts image references from all container types within pod specifications:

- **containers**: Main application containers
- **initContainers**: Initialization containers that run before app containers
- **ephemeralContainers**: Ephemeral debugging containers

### Pod Spec Locations

The detector handles different pod spec locations depending on the resource kind:

- **Pod**: `spec.containers`
- **Deployment, StatefulSet, DaemonSet, ReplicaSet, ReplicationController**: `spec.template.spec.containers`
- **Job**: `spec.template.spec.containers`
- **CronJob**: `spec.jobTemplate.spec.template.spec.containers`

### Variable Resolution

Images containing unresolved variables (e.g., `${TAG}`) 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 Kubernetes=EnableIfDefaultOff`
- **Broad File Matching**: The `*.yaml` and `*.yml` search patterns match all YAML files, so the detector relies on content-based filtering (`apiVersion` and `kind` fields) to identify Kubernetes manifests
- **Variable Resolution**: Image references containing unresolved template variables are not reported
- **Limited Resource Kinds**: Only the eight resource kinds listed above are supported. Custom resources (CRDs) or other workload types are not detected
- **No Dependency Graph**: All detected images are registered as independent components without parent-child relationships
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ public static class DockerReferenceUtility
private const string LEGACYDEFAULTDOMAIN = "index.docker.io";
private const string OFFICIALREPOSITORYNAME = "library";

/// <summary>
/// Returns true if the reference contains unresolved variable placeholders (e.g., ${VAR}, {{ .Values.tag }}).
/// Such references should be skipped before calling <see cref="ParseFamiliarName"/> or <see cref="ParseQualifiedName"/>.
/// </summary>
/// <param name="reference">The image reference string to check.</param>
Comment on lines +41 to +45
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says unresolved-variable skipping logic was consolidated across all Docker-related detectors, but DockerfileComponentDetector still has its own regex-based HasUnresolvedVariables helper and does not use this new shared method. Either update the Dockerfile detector to use DockerReferenceUtility.HasUnresolvedVariables or adjust the PR description to match the implemented scope.

This issue also appears on line 47 of the same file.

Copilot uses AI. Check for mistakes.
/// <returns><c>true</c> if the reference contains variable placeholder characters; otherwise <c>false</c>.</returns>
public static bool HasUnresolvedVariables(string reference) =>
reference.IndexOfAny(['$', '{', '}']) >= 0;
Comment on lines +41 to +48
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new helper duplicates the Dockerfile detector’s existing HasUnresolvedVariables implementation (Regex("[${}]")). To actually consolidate the logic across Docker-related detectors (as described in the PR), consider switching the Dockerfile detector to call this shared helper and removing the duplicate method so the criteria can’t drift over time.

Copilot uses AI. Check for mistakes.

public static DockerReference ParseQualifiedName(string qualifiedName)
{
var regexp = DockerRegex.ReferenceRegexp;
Expand Down
9 changes: 9 additions & 0 deletions src/Microsoft.ComponentDetection.Contracts/DetectorClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,13 @@ public enum DetectorClass

/// <summary> Indicates a detector applies to Swift packages.</summary>
Swift,

/// <summary>Indicates a detector applies to Docker Compose image references.</summary>
DockerCompose,

/// <summary>Indicates a detector applies to Helm chart image references.</summary>
Helm,

/// <summary>Indicates a detector applies to Kubernetes manifest image references.</summary>
Kubernetes,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#nullable disable
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enable nullability for new files please

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<DockerComposeComponentDetector> logger)
{
this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory;
this.Scanner = walkerFactory;
this.Logger = logger;
}

public override string Id => "DockerCompose";

public override IList<string> SearchPatterns { get; } =
[
"docker-compose.yml", "docker-compose.yaml",
"docker-compose.*.yml", "docker-compose.*.yaml",
"compose.yml", "compose.yaml",
"compose.*.yml", "compose.*.yaml",
];
Comment on lines +30 to +36
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SearchPatterns contains docker-compose.*.yml / compose.*.yaml variants, but the repo’s PathUtilityService.MatchesPattern only supports * at the beginning or end of a pattern. As written, override/env-specific compose files like docker-compose.override.yml or compose.prod.yaml won’t be discovered in real scans. Use patterns the matcher can handle (e.g., docker-compose.* / compose.*) or broaden to *.yml/*.yaml and filter by filename.

Copilot uses AI. Check for mistakes.

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

public override int Version => 1;

public override IEnumerable<string> Categories => [Enum.GetName(typeof(DetectorClass), DetectorClass.DockerCompose)];

protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary<string, string> 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, file.Location);
}
}
}
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, string fileLocation)
{
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))
{
this.TryRegisterImageReference(imageRef, recorder, fileLocation);
}
}
}
}
}

private void TryRegisterImageReference(string imageReference, ISingleFileComponentRecorder recorder, string fileLocation)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TryRegisterImageReference is copy-pasted across all three detectors. Consider pulling it into a shared static method on DockerReferenceUtility. Something like TryParseImageReference(string) -> DockerReference? that handles the unresolved-variable check, parsing, and exception swallowing. The Dockerfile detector does the same work inline too, so it'd consolidate four copies.

{
if (DockerReferenceUtility.HasUnresolvedVariables(imageReference))
{
return;
}
Comment on lines +126 to +130
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The detector skips unresolved variable placeholders via DockerReferenceUtility.HasUnresolvedVariables, but there’s no unit test covering this Docker Compose behavior (e.g., ${REGISTRY}/app:${TAG}) even though it’s called out in the PR description. Add a test to prevent regressions.

Copilot uses AI. Check for mistakes.

try
{
var dockerRef = DockerReferenceUtility.ParseFamiliarName(imageReference);
if (dockerRef != null)
{
recorder.RegisterUsage(new DetectedComponent(dockerRef.ToTypedDockerReferenceComponent()));
}
}
catch (Exception e)
{
this.Logger.LogWarning(e, "Failed to parse image reference '{ImageReference}' in {Location}", imageReference, fileLocation);
}
Comment on lines +125 to +143
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docker Compose files commonly use variable interpolation in image (e.g., ${REGISTRY}/app:${TAG}). DockerReferenceUtility.ParseFamiliarName throws on these, which will generate warnings for otherwise valid compose files. Consider skipping registration when the image reference contains unresolved variables (similar to DockerfileComponentDetector.HasUnresolvedVariables) instead of logging a warning for each occurrence.

Copilot uses AI. Check for mistakes.
}
}
Loading
Loading