Skip to content

Commit 02e9dbd

Browse files
JamieMageejpinz
andauthored
Add support for application-level component support in containers (#1529)
* 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 * PR comments * Update src/Microsoft.ComponentDetection.Detectors/linux/Filters/Mariner2ArtifactFilter.cs Co-authored-by: Julian <[email protected]> * PR review comment changes --------- Co-authored-by: Julian <[email protected]>
1 parent b2822ac commit 02e9dbd

File tree

17 files changed

+646
-162
lines changed

17 files changed

+646
-162
lines changed

src/Microsoft.ComponentDetection.Common/Telemetry/Records/LinuxScannerSyftTelemetryRecord.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ public class LinuxScannerSyftTelemetryRecord : BaseDetectionTelemetryRecord
44
{
55
public override string RecordName => "LinuxScannerSyftTelemetry";
66

7-
public string LinuxComponents { get; set; }
7+
public string Components { get; set; }
88

99
public string Exception { get; set; }
1010

11-
public string Mariner2ComponentsRemoved { get; set; }
11+
public string ComponentsRemoved { get; set; }
1212
}

src/Microsoft.ComponentDetection.Contracts/BcdeModels/LayerMappedLinuxComponents.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,20 @@ namespace Microsoft.ComponentDetection.Contracts.BcdeModels;
33
using System.Collections.Generic;
44
using Microsoft.ComponentDetection.Contracts.TypedComponent;
55

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

18+
/// <summary>
19+
/// Gets or sets the Docker layer associated with these components.
20+
/// </summary>
1021
public DockerLayer DockerLayer { get; set; }
1122
}

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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
/// <remarks>
16+
/// For a complete list of Syft artifact types, see:
17+
/// https://github.com/anchore/syft/blob/main/syft/pkg/type.go.
18+
/// </remarks>
19+
public IEnumerable<string> SupportedArtifactTypes { get; }
20+
21+
/// <summary>
22+
/// Creates a TypedComponent from a Syft artifact element.
23+
/// </summary>
24+
/// <param name="artifact">The artifact element from Syft output.</param>
25+
/// <param name="distro">The distribution information from Syft output.</param>
26+
/// <returns>A TypedComponent instance, or null if the artifact cannot be processed.</returns>
27+
public TypedComponent CreateComponent(ArtifactElement artifact, Distro distro);
28+
}
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 <see cref="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 <see cref="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 <see cref="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+
}

0 commit comments

Comments
 (0)