Skip to content

Commit 9eb6bc3

Browse files
authored
Merge branch 'main' into user/aamaini/dependency-graph-identity-reconciliation
2 parents e0fa8cd + 9582bca commit 9eb6bc3

20 files changed

Lines changed: 738 additions & 380 deletions

File tree

.github/workflows/smoke-test.yml

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ on:
88
branches:
99
- main
1010
pull_request:
11+
paths:
12+
- "src/**"
13+
- "Directory.Build.props"
14+
- "Directory.Build.targets"
15+
- "Directory.Packages.props"
16+
- "global.json"
17+
- ".github/workflows/smoke-test.yml"
1118
schedule:
1219
- cron: "0 0 * * *" # every day at midnight
1320

@@ -16,28 +23,9 @@ permissions:
1623

1724
jobs:
1825
smoke-test:
19-
if: github.repository == 'microsoft/component-detection'
26+
if: github.repository == 'microsoft/component-detection' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false)
2027
runs-on: ["self-hosted", "1ES.Pool=1ES-OSE-GH-Pool"]
21-
strategy:
22-
matrix:
23-
language:
24-
[
25-
{ name: "CocoaPods", repo: "realm/realm-swift" },
26-
{ name: "Gradle", repo: "microsoft/ApplicationInsights-Java" },
27-
{ name: "Go", repo: "kubernetes/kubernetes" },
28-
{ name: "Maven", repo: "apache/kafka" },
29-
{ name: "NPM", repo: "axios/axios" },
30-
{ name: "NuGet", repo: "Radarr/Radarr" },
31-
{ name: "Pip", repo: "django/django" },
32-
{ name: "Pnpm", repo: "pnpm/pnpm" },
33-
{ name: "Poetry", repo: "Textualize/rich" },
34-
{ name: "Ruby", repo: "rails/rails" },
35-
{ name: "Rust", repo: "alacritty/alacritty" },
36-
{ name: "Yarn", repo: "gatsbyjs/gatsby" },
37-
]
38-
fail-fast: false
39-
max-parallel: 4 # limit the total number of running jobs to avoid rate limiting
40-
name: ${{ matrix.language.name }}
28+
name: Smoke Test
4129
steps:
4230
- name: Harden Runner
4331
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
@@ -67,23 +55,39 @@ jobs:
6755
sudo chmod 777 /usr/share/ant/lib
6856
curl https://downloads.apache.org/ant/ivy/2.5.2/apache-ivy-2.5.2-bin.tar.gz | tar xOz apache-ivy-2.5.2/ivy-2.5.2.jar > /usr/share/ant/lib/ivy.jar
6957
70-
- name: Checkout Smoke Test Repo
71-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
72-
with:
73-
persist-credentials: false
74-
repository: ${{ matrix.language.repo }}
75-
path: smoke-test-repo
58+
- name: Checkout Smoke Test Repos
59+
run: |
60+
mkdir -p smoke-test-repos
61+
repos=(
62+
"realm/realm-swift" # CocoaPods
63+
"microsoft/ApplicationInsights-Java" # Gradle
64+
"kubernetes/kubernetes" # Go
65+
"apache/kafka" # Maven
66+
"axios/axios" # NPM
67+
"Radarr/Radarr" # NuGet
68+
"django/django" # Pip
69+
"pnpm/pnpm" # Pnpm
70+
"Textualize/rich" # Poetry
71+
"rails/rails" # Ruby
72+
"alacritty/alacritty" # Rust
73+
"gatsbyjs/gatsby" # Yarn
74+
)
75+
for repo in "${repos[@]}"; do
76+
dir="smoke-test-repos/$(basename "$repo")"
77+
echo "Cloning $repo into $dir..."
78+
git clone --depth 1 "https://github.com/$repo.git" "$dir"
79+
done
7680
7781
- name: Restore Smoke Test NuGet Packages
78-
if: ${{ matrix.language.name == 'NuGet'}}
79-
working-directory: smoke-test-repo/src
82+
working-directory: smoke-test-repos/Radarr/src
8083
run: dotnet restore
8184

8285
- name: Run Smoke Test
8386
working-directory: src/Microsoft.ComponentDetection
8487
run: |
85-
for i in $(seq 1 10); do
86-
dotnet run -c Release -- scan --SourceDirectory ${{ github.workspace }}/smoke-test-repo --Verbosity Verbose || exit 1
88+
ITERATIONS=${{ github.event_name == 'schedule' && 10 || 1 }}
89+
for i in $(seq 1 $ITERATIONS); do
90+
dotnet run -c Release -- scan --SourceDirectory ${{ github.workspace }}/smoke-test-repos --Verbosity Verbose || exit 1
8791
done
8892
8993
create-issue:

Directory.Packages.props

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
</PackageReference>
77
</ItemDefinitionGroup>
88
<ItemGroup>
9-
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
109
<PackageVersion Include="Docker.DotNet" Version="3.125.15" />
1110
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
1211
<PackageVersion Include="AwesomeAssertions.Analyzers" Version="9.0.8" />
@@ -44,7 +43,6 @@
4443
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
4544
<PackageVersion Include="System.Memory" Version="4.6.3" />
4645
<PackageVersion Include="System.Reactive" Version="6.1.0" />
47-
<PackageVersion Include="System.Runtime.Loader" Version="4.3.0" />
4846
<PackageVersion Include="System.Text.Json" Version="9.0.13" />
4947
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
5048
<PackageVersion Include="Tomlyn.Signed" Version="0.20.0" />

docs/detectors/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818
| -------------------------- | ---------- |
1919
| CondaLockComponentDetector | DefaultOff |
2020

21+
- [Docker Compose](dockercompose.md)
22+
23+
| Detector | Status |
24+
| ------------------------------ | ---------- |
25+
| DockerComposeComponentDetector | DefaultOff |
26+
2127
- [Dockerfile](dockerfile.md)
2228

2329
| Detector | Status |

docs/detectors/dockercompose.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Docker Compose Detection
2+
3+
## Requirements
4+
5+
Docker Compose detection depends on the following to successfully run:
6+
7+
- 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`
8+
9+
The `DockerComposeComponentDetector` is a **DefaultOff** detector and must be explicitly enabled via the `--DetectorArgs` parameter.
10+
11+
## Detection strategy
12+
13+
The Docker Compose detector parses YAML compose files to extract Docker image references from service definitions.
14+
15+
### Service Image Detection
16+
17+
The detector looks for the `services` section and extracts the `image` field from each service:
18+
19+
```yaml
20+
services:
21+
web:
22+
image: nginx:1.21
23+
db:
24+
image: postgres:14
25+
```
26+
27+
Services that only define a `build` directive without an `image` field are skipped, as they do not reference external Docker images.
28+
29+
### Full Registry References
30+
31+
The detector supports full registry image references:
32+
33+
```yaml
34+
services:
35+
app:
36+
image: ghcr.io/myorg/myapp:v2.0
37+
```
38+
39+
### Variable Resolution
40+
41+
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.
42+
43+
## Known limitations
44+
45+
- **DefaultOff Status**: This detector must be explicitly enabled using `--DetectorArgs DockerCompose=EnableIfDefaultOff`
46+
- **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
47+
- **Build-Only Services**: Services that only specify a `build` directive without an `image` field are not reported
48+
- **No Dependency Graph**: All detected images are registered as independent components without parent-child relationships

src/Microsoft.ComponentDetection.Common/DockerReference/DockerReferenceUtility.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ namespace Microsoft.ComponentDetection.Common;
2929
using System;
3030
using System.Diagnostics.CodeAnalysis;
3131
using Microsoft.ComponentDetection.Contracts;
32+
using Microsoft.Extensions.Logging;
3233

3334
public static class DockerReferenceUtility
3435
{
@@ -38,6 +39,67 @@ public static class DockerReferenceUtility
3839
private const string LEGACYDEFAULTDOMAIN = "index.docker.io";
3940
private const string OFFICIALREPOSITORYNAME = "library";
4041

42+
/// <summary>
43+
/// Returns true if the reference contains unresolved variable placeholders (e.g., ${VAR}, {{ .Values.tag }}).
44+
/// Such references should be skipped before calling <see cref="ParseFamiliarName"/> or <see cref="ParseQualifiedName"/>.
45+
/// </summary>
46+
/// <param name="reference">The image reference string to check.</param>
47+
/// <returns><c>true</c> if the reference contains variable placeholder characters; otherwise <c>false</c>.</returns>
48+
public static bool HasUnresolvedVariables(string reference) =>
49+
reference.IndexOfAny(['$', '{', '}']) >= 0;
50+
51+
/// <summary>
52+
/// Attempts to parse an image reference string into a <see cref="DockerReference"/>.
53+
/// Returns <c>null</c> if the reference contains unresolved variables or cannot be parsed.
54+
/// </summary>
55+
/// <param name="imageReference">The image reference string to parse.</param>
56+
/// <param name="logger">Optional logger for recording parse failures.</param>
57+
/// <returns>A <see cref="DockerReference"/> if parsing succeeds; otherwise <c>null</c>.</returns>
58+
public static DockerReference? TryParseImageReference(string imageReference, ILogger? logger = null)
59+
{
60+
if (HasUnresolvedVariables(imageReference))
61+
{
62+
return null;
63+
}
64+
65+
try
66+
{
67+
return ParseFamiliarName(imageReference);
68+
}
69+
catch (DockerReferenceException ex)
70+
{
71+
logger?.LogWarning(ex, "Failed to parse image reference '{ImageReference}'.", imageReference);
72+
return null;
73+
}
74+
}
75+
76+
/// <summary>
77+
/// Parses an image reference and registers it with the recorder if valid.
78+
/// Skips references with unresolved variables or that cannot be parsed,
79+
/// logging a warning for parse failures so that remaining entries continue to be processed.
80+
/// </summary>
81+
/// <param name="imageReference">The image reference string to parse.</param>
82+
/// <param name="recorder">The component recorder to register the image with.</param>
83+
/// <param name="logger">Optional logger for recording parse failures.</param>
84+
public static void TryRegisterImageReference(string imageReference, ISingleFileComponentRecorder recorder, ILogger? logger = null)
85+
{
86+
var dockerRef = TryParseImageReference(imageReference, logger);
87+
TryRegisterImageReference(dockerRef, recorder);
88+
}
89+
90+
/// <summary>
91+
/// Registers a pre-parsed <see cref="DockerReference"/> with the recorder if non-null.
92+
/// </summary>
93+
/// <param name="dockerReference">The parsed docker reference, or <c>null</c> to skip.</param>
94+
/// <param name="recorder">The component recorder to register the image with.</param>
95+
public static void TryRegisterImageReference(DockerReference? dockerReference, ISingleFileComponentRecorder recorder)
96+
{
97+
if (dockerReference != null)
98+
{
99+
recorder.RegisterUsage(new DetectedComponent(dockerReference.ToTypedDockerReferenceComponent()));
100+
}
101+
}
102+
41103
public static DockerReference ParseQualifiedName(string qualifiedName)
42104
{
43105
var regexp = DockerRegex.ReferenceRegexp;

src/Microsoft.ComponentDetection.Common/Microsoft.ComponentDetection.Common.csproj

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,4 @@
2121
<ProjectReference Include="..\Microsoft.ComponentDetection.Contracts\Microsoft.ComponentDetection.Contracts.csproj" />
2222
</ItemGroup>
2323

24-
<ItemGroup>
25-
<Compile Update="Resources.Designer.cs">
26-
<DependentUpon>Resources.resx</DependentUpon>
27-
<DesignTime>True</DesignTime>
28-
<AutoGen>True</AutoGen>
29-
</Compile>
30-
<EmbeddedResource Update="Resources.resx">
31-
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
32-
<Generator>ResXFileCodeGenerator</Generator>
33-
</EmbeddedResource>
34-
</ItemGroup>
35-
3624
</Project>

src/Microsoft.ComponentDetection.Common/Resources.Designer.cs

Lines changed: 0 additions & 82 deletions
This file was deleted.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Microsoft.ComponentDetection.Common;
2+
3+
internal static class Resources
4+
{
5+
internal const string MissingComponentId = "The component object does not have a componentId specified";
6+
7+
internal const string MissingNodeInDependencyGraph = "Node with id {0} has not be inserted in the dependency graph";
8+
}

0 commit comments

Comments
 (0)