Skip to content

Commit 06947d5

Browse files
committed
Add support for application-level component support in containers
Key changes: - Add IArtifactComponentFactory interface with implementations for Linux, npm, and pip components - Add IArtifactFilter interface with Mariner2ArtifactFilter to handle distro-specific artifact filtering - Refactor LinuxScanner to use factories and filters via dependency injection - Rename LinuxComponents to Components in LayerMappedLinuxComponents for generic support - Update telemetry to track component types alongside names and versions - Add test coverage for multi-type artifact scanning
1 parent d8c0daa commit 06947d5

File tree

16 files changed

+648
-152
lines changed

16 files changed

+648
-152
lines changed
Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
namespace Microsoft.ComponentDetection.Contracts.BcdeModels;
22

3+
using System;
34
using System.Collections.Generic;
5+
using System.Linq;
46
using Microsoft.ComponentDetection.Contracts.TypedComponent;
57

8+
/// <summary>
9+
/// Represents a mapping between a Docker layer and the components detected within that layer.
10+
/// This class associates components (both Linux system packages and application-level packages) with their corresponding Docker layer.
11+
/// </summary>
612
public class LayerMappedLinuxComponents
713
{
8-
public IEnumerable<LinuxComponent> LinuxComponents { get; set; }
14+
/// <summary>
15+
/// Gets or sets the components detected in this layer.
16+
/// This can include system packages (LinuxComponent) as well as application-level packages (NpmComponent, PipComponent, etc.).
17+
/// </summary>
18+
public IEnumerable<TypedComponent> Components { get; set; }
919

20+
/// <summary>
21+
/// Gets or sets the Docker layer associated with these components.
22+
/// </summary>
1023
public DockerLayer DockerLayer { get; set; }
1124
}

src/Microsoft.ComponentDetection.Detectors/linux/Contracts/ImageScanningResult.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,18 @@ namespace Microsoft.ComponentDetection.Detectors.Linux.Contracts;
44
using Microsoft.ComponentDetection.Contracts;
55
using Microsoft.ComponentDetection.Contracts.BcdeModels;
66

7+
/// <summary>
8+
/// Represents the result of scanning a container image for components.
9+
/// </summary>
710
internal class ImageScanningResult
811
{
12+
/// <summary>
13+
/// Gets or sets the container details associated with the image scanning result.
14+
/// </summary>
915
public ContainerDetails ContainerDetails { get; set; }
1016

17+
/// <summary>
18+
/// Gets or sets the collection of components detected during the image scanning process.
19+
/// </summary>
1120
public IEnumerable<DetectedComponent> Components { get; set; }
1221
}

src/Microsoft.ComponentDetection.Detectors/linux/Exceptions/MissingContainerDetailException.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,32 @@ namespace Microsoft.ComponentDetection.Detectors.Linux.Exceptions;
22

33
using System;
44

5+
/// <summary>
6+
/// Exception thrown when container details information cannot be found for a specified image.
7+
/// </summary>
58
public class MissingContainerDetailException : Exception
69
{
10+
/// <summary>
11+
/// Initializes a new instance of the <see cref="MissingContainerDetailException"/> class with the specified image ID.
12+
/// </summary>
13+
/// <param name="imageId">The ID of the container image for which details could not be found.</param>
714
public MissingContainerDetailException(string imageId)
8-
: base($"No container details information could be found for image ${imageId}")
15+
: base($"No container details information could be found for image {imageId}")
916
{
1017
}
1118

19+
/// <summary>
20+
/// Initializes a new instance of the <see cref="MissingContainerDetailException"/> class.
21+
/// </summary>
1222
public MissingContainerDetailException()
1323
{
1424
}
1525

26+
/// <summary>
27+
/// Initializes a new instance of the <see cref="MissingContainerDetailException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
28+
/// </summary>
29+
/// <param name="message">The error message that explains the reason for the exception.</param>
30+
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
1631
public MissingContainerDetailException(string message, Exception innerException)
1732
: base(message, innerException)
1833
{
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
namespace Microsoft.ComponentDetection.Detectors.Linux.Factories;
2+
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using Microsoft.ComponentDetection.Contracts.TypedComponent;
6+
using Microsoft.ComponentDetection.Detectors.Linux.Contracts;
7+
8+
/// <summary>
9+
/// Abstract base class for artifact component factories that provides common functionality
10+
/// for extracting license and author information from Syft artifacts.
11+
/// </summary>
12+
public abstract class ArtifactComponentFactoryBase : IArtifactComponentFactory
13+
{
14+
/// <inheritdoc/>
15+
public abstract IEnumerable<string> SupportedArtifactTypes { get; }
16+
17+
/// <inheritdoc/>
18+
public abstract TypedComponent CreateComponent(ArtifactElement artifact, Distro distro);
19+
20+
/// <summary>
21+
/// Extracts license information from the artifact, checking both metadata and top-level licenses array.
22+
/// </summary>
23+
/// <param name="artifact">The artifact element from Syft output.</param>
24+
/// <returns>A comma-separated string of license values, or null if no licenses are found.</returns>
25+
protected static string GetLicenseFromArtifact(ArtifactElement artifact)
26+
{
27+
// First try metadata.License which may be a string
28+
var license = artifact.Metadata?.License?.String;
29+
if (license != null)
30+
{
31+
return license;
32+
}
33+
34+
// Fall back to top-level Licenses array
35+
var licenses = artifact.Licenses;
36+
if (licenses != null && licenses.Length != 0)
37+
{
38+
return string.Join(", ", licenses.Select(l => l.Value));
39+
}
40+
41+
return null;
42+
}
43+
44+
/// <summary>
45+
/// Extracts author information from the artifact metadata, checking both Author and Maintainer fields.
46+
/// </summary>
47+
/// <param name="artifact">The artifact element from Syft output.</param>
48+
/// <returns>The author or maintainer string, or null if neither is found.</returns>
49+
protected static string GetAuthorFromArtifact(ArtifactElement artifact)
50+
{
51+
var author = artifact.Metadata?.Author;
52+
if (!string.IsNullOrEmpty(author))
53+
{
54+
return author;
55+
}
56+
57+
var maintainer = artifact.Metadata?.Maintainer;
58+
if (!string.IsNullOrEmpty(maintainer))
59+
{
60+
return maintainer;
61+
}
62+
63+
return null;
64+
}
65+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace Microsoft.ComponentDetection.Detectors.Linux.Factories;
2+
3+
using System.Collections.Generic;
4+
using Microsoft.ComponentDetection.Contracts.TypedComponent;
5+
using Microsoft.ComponentDetection.Detectors.Linux.Contracts;
6+
7+
/// <summary>
8+
/// Factory interface for creating TypedComponent instances from Syft artifacts.
9+
/// </summary>
10+
public interface IArtifactComponentFactory
11+
{
12+
/// <summary>
13+
/// Gets the artifact types (e.g., "npm", "apk", "deb") that this factory can handle.
14+
/// </summary>
15+
public IEnumerable<string> SupportedArtifactTypes { get; }
16+
17+
/// <summary>
18+
/// Creates a TypedComponent from a Syft artifact element.
19+
/// </summary>
20+
/// <param name="artifact">The artifact element from Syft output.</param>
21+
/// <param name="distro">The distribution information from Syft output.</param>
22+
/// <returns>A TypedComponent instance, or null if the artifact cannot be processed.</returns>
23+
public TypedComponent CreateComponent(ArtifactElement artifact, Distro distro);
24+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
namespace Microsoft.ComponentDetection.Detectors.Linux.Factories;
2+
3+
using System.Collections.Generic;
4+
using Microsoft.ComponentDetection.Contracts.TypedComponent;
5+
using Microsoft.ComponentDetection.Detectors.Linux.Contracts;
6+
7+
/// <summary>
8+
/// Factory for creating LinuxComponent instances from system package artifacts (apk, deb, rpm).
9+
/// </summary>
10+
public class LinuxComponentFactory : ArtifactComponentFactoryBase
11+
{
12+
/// <inheritdoc/>
13+
public override IEnumerable<string> SupportedArtifactTypes => ["apk", "deb", "rpm"];
14+
15+
/// <inheritdoc/>
16+
public override TypedComponent CreateComponent(ArtifactElement artifact, Distro distro)
17+
{
18+
if (artifact == null || distro == null)
19+
{
20+
return null;
21+
}
22+
23+
if (string.IsNullOrWhiteSpace(artifact.Name) || string.IsNullOrWhiteSpace(artifact.Version))
24+
{
25+
return null;
26+
}
27+
28+
var license = GetLicenseFromArtifact(artifact);
29+
var supplier = GetAuthorFromArtifact(artifact);
30+
31+
return new LinuxComponent(
32+
distribution: distro.Id,
33+
release: distro.VersionId,
34+
name: artifact.Name,
35+
version: artifact.Version,
36+
license: license,
37+
author: supplier);
38+
}
39+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
namespace Microsoft.ComponentDetection.Detectors.Linux.Factories;
2+
3+
using System.Collections.Generic;
4+
using Microsoft.ComponentDetection.Contracts.Internal;
5+
using Microsoft.ComponentDetection.Contracts.TypedComponent;
6+
using Microsoft.ComponentDetection.Detectors.Linux.Contracts;
7+
8+
/// <summary>
9+
/// Factory for creating NpmComponent instances from npm package artifacts.
10+
/// </summary>
11+
public class NpmComponentFactory : ArtifactComponentFactoryBase
12+
{
13+
/// <inheritdoc/>
14+
public override IEnumerable<string> SupportedArtifactTypes => ["npm"];
15+
16+
/// <inheritdoc/>
17+
public override TypedComponent CreateComponent(ArtifactElement artifact, Distro distro)
18+
{
19+
if (artifact == null)
20+
{
21+
return null;
22+
}
23+
24+
if (string.IsNullOrWhiteSpace(artifact.Name) || string.IsNullOrWhiteSpace(artifact.Version))
25+
{
26+
return null;
27+
}
28+
29+
var author = GetNpmAuthorFromArtifact(artifact);
30+
var hash = GetHashFromArtifact(artifact);
31+
32+
return new NpmComponent(
33+
name: artifact.Name,
34+
version: artifact.Version,
35+
hash: hash,
36+
author: author);
37+
}
38+
39+
private static NpmAuthor GetNpmAuthorFromArtifact(ArtifactElement artifact)
40+
{
41+
var authorString = artifact.Metadata?.Author;
42+
if (!string.IsNullOrWhiteSpace(authorString))
43+
{
44+
return new NpmAuthor(authorString);
45+
}
46+
47+
return null;
48+
}
49+
50+
private static string GetHashFromArtifact(ArtifactElement artifact)
51+
{
52+
if (!string.IsNullOrWhiteSpace(artifact.Metadata?.Integrity))
53+
{
54+
return artifact.Metadata.Integrity;
55+
}
56+
57+
return null;
58+
}
59+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
namespace Microsoft.ComponentDetection.Detectors.Linux.Factories;
2+
3+
using System.Collections.Generic;
4+
using Microsoft.ComponentDetection.Contracts.TypedComponent;
5+
using Microsoft.ComponentDetection.Detectors.Linux.Contracts;
6+
7+
/// <summary>
8+
/// Factory for creating PipComponent instances from Python package artifacts.
9+
/// </summary>
10+
public class PipComponentFactory : ArtifactComponentFactoryBase
11+
{
12+
/// <inheritdoc/>
13+
public override IEnumerable<string> SupportedArtifactTypes => ["python"];
14+
15+
/// <inheritdoc/>
16+
public override TypedComponent CreateComponent(ArtifactElement artifact, Distro distro)
17+
{
18+
if (artifact == null)
19+
{
20+
return null;
21+
}
22+
23+
if (string.IsNullOrWhiteSpace(artifact.Name) || string.IsNullOrWhiteSpace(artifact.Version))
24+
{
25+
return null;
26+
}
27+
28+
var author = GetAuthorFromArtifact(artifact);
29+
var license = GetLicenseFromArtifact(artifact);
30+
31+
return new PipComponent(
32+
name: artifact.Name,
33+
version: artifact.Version,
34+
author: author,
35+
license: license);
36+
}
37+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace Microsoft.ComponentDetection.Detectors.Linux.Filters;
2+
3+
using System.Collections.Generic;
4+
using Microsoft.ComponentDetection.Detectors.Linux.Contracts;
5+
6+
/// <summary>
7+
/// Interface for filtering or transforming Syft artifacts before component creation.
8+
/// Useful for handling distribution-specific workarounds or edge cases.
9+
/// </summary>
10+
public interface IArtifactFilter
11+
{
12+
/// <summary>
13+
/// Filters the provided artifacts and returns the filtered collection.
14+
/// </summary>
15+
/// <param name="artifacts">The artifacts to filter.</param>
16+
/// <param name="distro">The distribution information from Syft output.</param>
17+
/// <returns>The filtered collection of artifacts.</returns>
18+
public IEnumerable<ArtifactElement> Filter(IEnumerable<ArtifactElement> artifacts, Distro distro);
19+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
namespace Microsoft.ComponentDetection.Detectors.Linux.Filters;
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using Microsoft.ComponentDetection.Common.Telemetry.Records;
7+
using Microsoft.ComponentDetection.Detectors.Linux.Contracts;
8+
using Newtonsoft.Json;
9+
10+
/// <summary>
11+
/// Filters out invalid ELF binary packages from Mariner 2.0 images that lack proper release/epoch version fields.
12+
/// This workaround addresses an issue where Syft's elf-binary-package-cataloger detects packages without complete
13+
/// version information. The issue was fixed in Azure Linux 3.0 (https://github.com/microsoft/azurelinux/pull/10405),
14+
/// but Mariner 2.0 no longer receives non-security updates and will be deprecated in July 2025.
15+
/// Related Syft PR: https://github.com/anchore/syft/pull/3008.
16+
/// </summary>
17+
public class Mariner2ArtifactFilter : IArtifactFilter
18+
{
19+
/// <inheritdoc/>
20+
public IEnumerable<ArtifactElement> Filter(IEnumerable<ArtifactElement> artifacts, Distro distro)
21+
{
22+
if (artifacts == null || distro == null)
23+
{
24+
return artifacts ?? [];
25+
}
26+
27+
// Only apply this filter to Mariner 2.0
28+
if (distro.Id != "mariner" || distro.VersionId != "2.0")
29+
{
30+
return artifacts;
31+
}
32+
33+
using var syftTelemetryRecord = new LinuxScannerSyftTelemetryRecord();
34+
35+
var artifactsList = artifacts.ToList();
36+
37+
// Find ELF packages that lack release version (indicated by missing dash in version string)
38+
var elfVersionsWithoutRelease = artifactsList
39+
.Where(artifact =>
40+
artifact.FoundBy == "elf-binary-package-cataloger" && // Specific cataloger with invalid results
41+
!artifact.Version.Contains('-', StringComparison.OrdinalIgnoreCase)) // Missing release version
42+
.ToList();
43+
44+
if (elfVersionsWithoutRelease.Count > 0)
45+
{
46+
var removedComponents = new List<string>();
47+
foreach (var elfArtifact in elfVersionsWithoutRelease)
48+
{
49+
removedComponents.Add($"{elfArtifact.Name} {elfArtifact.Version}");
50+
artifactsList.Remove(elfArtifact);
51+
}
52+
53+
syftTelemetryRecord.Mariner2ComponentsRemoved = JsonConvert.SerializeObject(removedComponents);
54+
}
55+
56+
return artifactsList;
57+
}
58+
}

0 commit comments

Comments
 (0)