diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/BinarySyftRunner.cs b/src/Microsoft.ComponentDetection.Detectors/linux/BinarySyftRunner.cs
new file mode 100644
index 000000000..dab3e06a6
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/linux/BinarySyftRunner.cs
@@ -0,0 +1,133 @@
+namespace Microsoft.ComponentDetection.Detectors.Linux;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.Extensions.Logging;
+
+///
+/// Runs Syft by invoking a local Syft binary.
+///
+internal class BinarySyftRunner : ISyftRunner
+{
+ private static readonly SemaphoreSlim BinarySemaphore = new(2);
+
+ private static readonly int SemaphoreTimeout = Convert.ToInt32(
+ TimeSpan.FromHours(1).TotalMilliseconds);
+
+ private readonly string syftBinaryPath;
+ private readonly ICommandLineInvocationService commandLineInvocationService;
+ private readonly ILogger logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The path to the Syft binary.
+ /// The command line invocation service.
+ /// The logger.
+ public BinarySyftRunner(
+ string syftBinaryPath,
+ ICommandLineInvocationService commandLineInvocationService,
+ ILogger logger)
+ {
+ this.syftBinaryPath = syftBinaryPath;
+ this.commandLineInvocationService = commandLineInvocationService;
+ this.logger = logger;
+ }
+
+ ///
+ public async Task CanRunAsync(CancellationToken cancellationToken = default)
+ {
+ var result = await this.commandLineInvocationService.ExecuteCommandAsync(
+ this.syftBinaryPath,
+ null,
+ null,
+ cancellationToken,
+ "--version");
+
+ if (result.ExitCode != 0)
+ {
+ this.logger.LogInformation(
+ "Syft binary at {SyftBinaryPath} failed version check with exit code {ExitCode}. Stderr: {StdErr}",
+ this.syftBinaryPath,
+ result.ExitCode,
+ result.StdErr);
+ return false;
+ }
+
+ this.logger.LogInformation(
+ "Using Syft binary at {SyftBinaryPath}: {SyftVersion}",
+ this.syftBinaryPath,
+ result.StdOut?.Trim());
+ return true;
+ }
+
+ ///
+ public async Task<(string Stdout, string Stderr)> RunSyftAsync(
+ ImageReference imageReference,
+ IList arguments,
+ CancellationToken cancellationToken = default)
+ {
+ var syftSource = GetSyftSource(imageReference);
+ var acquired = false;
+
+ try
+ {
+ acquired = await BinarySemaphore.WaitAsync(SemaphoreTimeout, cancellationToken);
+ if (!acquired)
+ {
+ this.logger.LogWarning(
+ "Failed to enter the binary semaphore for image {ImageReference}",
+ imageReference.Reference);
+ return (string.Empty, string.Empty);
+ }
+
+ var parameters = new[] { syftSource }
+ .Concat(arguments)
+ .ToArray();
+
+ var result = await this.commandLineInvocationService.ExecuteCommandAsync(
+ this.syftBinaryPath,
+ null,
+ null,
+ cancellationToken,
+ parameters);
+
+ if (result.ExitCode != 0)
+ {
+ this.logger.LogError(
+ "Syft binary exited with code {ExitCode}. Stderr: {StdErr}",
+ result.ExitCode,
+ result.StdErr);
+ }
+
+ return (result.StdOut, result.StdErr);
+ }
+ finally
+ {
+ if (acquired)
+ {
+ BinarySemaphore.Release();
+ }
+ }
+ }
+
+ ///
+ /// Constructs the Syft source argument from an image reference.
+ /// For local images, the host path is used directly with the appropriate scheme prefix.
+ ///
+ private static string GetSyftSource(ImageReference imageReference) =>
+ imageReference.Kind switch
+ {
+ ImageReferenceKind.DockerImage => imageReference.Reference,
+ ImageReferenceKind.OciLayout => $"oci-dir:{imageReference.Reference}",
+ ImageReferenceKind.OciArchive => $"oci-archive:{imageReference.Reference}",
+ ImageReferenceKind.DockerArchive => $"docker-archive:{imageReference.Reference}",
+ _ => throw new ArgumentOutOfRangeException(
+ nameof(imageReference),
+ $"Unsupported image reference kind '{imageReference.Kind}'."),
+ };
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/BinarySyftRunnerFactory.cs b/src/Microsoft.ComponentDetection.Detectors/linux/BinarySyftRunnerFactory.cs
new file mode 100644
index 000000000..7d3c1dbbb
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/linux/BinarySyftRunnerFactory.cs
@@ -0,0 +1,33 @@
+namespace Microsoft.ComponentDetection.Detectors.Linux;
+
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.Extensions.Logging;
+
+///
+/// Factory for creating instances.
+///
+internal class BinarySyftRunnerFactory : IBinarySyftRunnerFactory
+{
+ private readonly ICommandLineInvocationService commandLineInvocationService;
+ private readonly ILoggerFactory loggerFactory;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The command line invocation service.
+ /// The logger factory.
+ public BinarySyftRunnerFactory(
+ ICommandLineInvocationService commandLineInvocationService,
+ ILoggerFactory loggerFactory)
+ {
+ this.commandLineInvocationService = commandLineInvocationService;
+ this.loggerFactory = loggerFactory;
+ }
+
+ ///
+ public ISyftRunner Create(string binaryPath) =>
+ new BinarySyftRunner(
+ binaryPath,
+ this.commandLineInvocationService,
+ this.loggerFactory.CreateLogger());
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/DockerSyftRunner.cs b/src/Microsoft.ComponentDetection.Detectors/linux/DockerSyftRunner.cs
new file mode 100644
index 000000000..e9d63a87d
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/linux/DockerSyftRunner.cs
@@ -0,0 +1,141 @@
+namespace Microsoft.ComponentDetection.Detectors.Linux;
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.ComponentDetection.Common.Telemetry.Records;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.Extensions.Logging;
+
+///
+/// Runs Syft by executing a Docker container with the Syft image.
+///
+internal class DockerSyftRunner : IDockerSyftRunner
+{
+ internal const string ScannerImage =
+ "governancecontainerregistry.azurecr.io/syft:v1.37.0@sha256:48d679480c6d272c1801cf30460556959c01d4826795be31d4fd8b53750b7d91";
+
+ private const string LocalImageMountPoint = "/image";
+
+ private static readonly SemaphoreSlim ContainerSemaphore = new(2);
+
+ private static readonly int SemaphoreTimeout = Convert.ToInt32(
+ TimeSpan.FromHours(1).TotalMilliseconds);
+
+ private readonly IDockerService dockerService;
+ private readonly ILogger logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The docker service.
+ /// The logger.
+ public DockerSyftRunner(IDockerService dockerService, ILogger logger)
+ {
+ this.dockerService = dockerService;
+ this.logger = logger;
+ }
+
+ ///
+ public async Task CanRunAsync(CancellationToken cancellationToken = default)
+ {
+ if (await this.dockerService.CanRunLinuxContainersAsync(cancellationToken))
+ {
+ return true;
+ }
+
+ using var record = new LinuxContainerDetectorUnsupportedOs
+ {
+ Os = RuntimeInformation.OSDescription,
+ };
+ this.logger.LogInformation("Linux containers are not available on this host.");
+ return false;
+ }
+
+ ///
+ public async Task<(string Stdout, string Stderr)> RunSyftAsync(
+ ImageReference imageReference,
+ IList arguments,
+ CancellationToken cancellationToken = default)
+ {
+ var (syftSource, additionalBinds) = GetSyftSourceAndBinds(imageReference);
+ var acquired = false;
+
+ try
+ {
+ acquired = await ContainerSemaphore.WaitAsync(SemaphoreTimeout, cancellationToken);
+ if (!acquired)
+ {
+ this.logger.LogWarning(
+ "Failed to enter the container semaphore for image {ImageReference}",
+ imageReference.Reference);
+ return (string.Empty, string.Empty);
+ }
+
+ var command = new List { syftSource }
+ .Concat(arguments)
+ .ToList();
+
+ return await this.dockerService.CreateAndRunContainerAsync(
+ ScannerImage,
+ command,
+ additionalBinds,
+ cancellationToken);
+ }
+ finally
+ {
+ if (acquired)
+ {
+ ContainerSemaphore.Release();
+ }
+ }
+ }
+
+ ///
+ /// Constructs the Syft source argument and any required Docker bind mounts from an image reference.
+ /// For Docker images, no additional binds are needed. For local images (OCI/archives),
+ /// the host path is mounted into the container and the source uses the container-relative path.
+ ///
+ private static (string SyftSource, IList AdditionalBinds) GetSyftSourceAndBinds(ImageReference imageReference)
+ {
+ switch (imageReference.Kind)
+ {
+ case ImageReferenceKind.DockerImage:
+ return (imageReference.Reference, []);
+
+ case ImageReferenceKind.OciLayout:
+ return (
+ $"oci-dir:{LocalImageMountPoint}",
+ [$"{imageReference.Reference}:{LocalImageMountPoint}:ro"]);
+
+ case ImageReferenceKind.OciArchive:
+ {
+ var dir = Path.GetDirectoryName(imageReference.Reference)
+ ?? throw new InvalidOperationException($"Could not determine parent directory for OCI archive path '{imageReference.Reference}'.");
+ var fileName = Path.GetFileName(imageReference.Reference);
+ return (
+ $"oci-archive:{LocalImageMountPoint}/{fileName}",
+ [$"{dir}:{LocalImageMountPoint}:ro"]);
+ }
+
+ case ImageReferenceKind.DockerArchive:
+ {
+ var dir = Path.GetDirectoryName(imageReference.Reference)
+ ?? throw new InvalidOperationException($"Could not determine parent directory for Docker archive path '{imageReference.Reference}'.");
+ var fileName = Path.GetFileName(imageReference.Reference);
+ return (
+ $"docker-archive:{LocalImageMountPoint}/{fileName}",
+ [$"{dir}:{LocalImageMountPoint}:ro"]);
+ }
+
+ default:
+ throw new ArgumentOutOfRangeException(
+ nameof(imageReference),
+ $"Unsupported image reference kind '{imageReference.Kind}'.");
+ }
+ }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/IBinarySyftRunnerFactory.cs b/src/Microsoft.ComponentDetection.Detectors/linux/IBinarySyftRunnerFactory.cs
new file mode 100644
index 000000000..b31db859c
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/linux/IBinarySyftRunnerFactory.cs
@@ -0,0 +1,14 @@
+namespace Microsoft.ComponentDetection.Detectors.Linux;
+
+///
+/// Factory for creating binary-based Syft runners.
+///
+public interface IBinarySyftRunnerFactory
+{
+ ///
+ /// Creates a binary Syft runner configured to use the specified binary path.
+ ///
+ /// The path to the Syft binary.
+ /// An that invokes the specified Syft binary.
+ ISyftRunner Create(string binaryPath);
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/IDockerSyftRunner.cs b/src/Microsoft.ComponentDetection.Detectors/linux/IDockerSyftRunner.cs
new file mode 100644
index 000000000..a4d54650d
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/linux/IDockerSyftRunner.cs
@@ -0,0 +1,9 @@
+namespace Microsoft.ComponentDetection.Detectors.Linux;
+
+///
+/// Marker interface for the Docker-based Syft runner.
+/// Runs Syft by executing a Docker container with the Syft image.
+///
+public interface IDockerSyftRunner : ISyftRunner
+{
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/ILinuxScanner.cs b/src/Microsoft.ComponentDetection.Detectors/linux/ILinuxScanner.cs
index 3064a6013..4ae0f738b 100644
--- a/src/Microsoft.ComponentDetection.Detectors/linux/ILinuxScanner.cs
+++ b/src/Microsoft.ComponentDetection.Detectors/linux/ILinuxScanner.cs
@@ -16,19 +16,21 @@ public interface ILinuxScanner
/// Scans a Linux container image for components and maps them to their respective layers.
/// Runs Syft and processes the output in a single step.
///
- /// The hash identifier of the container image to scan.
+ /// The image reference to scan.
/// The collection of Docker layers that make up the container image.
/// The number of layers that belong to the base image, used to distinguish base image layers from application layers.
/// The set of component types to include in the scan results. Only components matching these types will be returned.
/// The scope for scanning the image. See for values.
+ /// The Syft runner to use for executing the scan.
/// A token to monitor for cancellation requests. The default value is .
/// A task that represents the asynchronous operation. The task result contains a collection of representing the components found in the image and their associated layers.
public Task> ScanLinuxAsync(
- string imageHash,
+ ImageReference imageReference,
IEnumerable containerLayers,
int baseImageLayerCount,
ISet enabledComponentTypes,
LinuxScannerScope scope,
+ ISyftRunner syftRunner,
CancellationToken cancellationToken = default
);
@@ -36,15 +38,15 @@ public Task> ScanLinuxAsync(
/// Runs the Syft scanner and returns the raw parsed output without processing components.
/// Use this when the caller needs access to the full Syft output (e.g., to extract source metadata for OCI images).
///
- /// The source argument passed to Syft (e.g., an image hash or "oci-dir:/oci-image").
- /// Additional volume bind mounts for the Syft container (e.g., for mounting OCI directories).
+ /// The image reference to scan.
/// The scope for scanning the image.
+ /// The Syft runner to use for executing the scan.
/// A token to monitor for cancellation requests.
/// A task that represents the asynchronous operation. The task result contains the parsed .
public Task GetSyftOutputAsync(
- string syftSource,
- IList additionalBinds,
+ ImageReference imageReference,
LinuxScannerScope scope,
+ ISyftRunner syftRunner,
CancellationToken cancellationToken = default
);
diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/ISyftRunner.cs b/src/Microsoft.ComponentDetection.Detectors/linux/ISyftRunner.cs
new file mode 100644
index 000000000..6aff06def
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/linux/ISyftRunner.cs
@@ -0,0 +1,31 @@
+namespace Microsoft.ComponentDetection.Detectors.Linux;
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+///
+/// Interface for executing Syft scans against container images.
+/// Implementations may invoke Syft via Docker container or as a local binary.
+///
+public interface ISyftRunner
+{
+ ///
+ /// Checks whether this runner is able to execute Syft scans in the current environment.
+ ///
+ /// A token to monitor for cancellation requests.
+ /// True if the runner is ready to execute scans, false otherwise.
+ Task CanRunAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Runs Syft against a container image and returns the raw output.
+ ///
+ /// The image reference to scan. Each runner implementation handles this differently based on the image kind.
+ /// The command-line arguments to pass to Syft (e.g., --quiet, --output json, --scope).
+ /// A token to monitor for cancellation requests.
+ /// A tuple containing the standard output and standard error from the Syft execution.
+ Task<(string Stdout, string Stderr)> RunSyftAsync(
+ ImageReference imageReference,
+ IList arguments,
+ CancellationToken cancellationToken = default);
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/ImageReference.cs b/src/Microsoft.ComponentDetection.Detectors/linux/ImageReference.cs
index fcb8c1c34..24ea33521 100644
--- a/src/Microsoft.ComponentDetection.Detectors/linux/ImageReference.cs
+++ b/src/Microsoft.ComponentDetection.Detectors/linux/ImageReference.cs
@@ -5,7 +5,7 @@ namespace Microsoft.ComponentDetection.Detectors.Linux;
///
/// Specifies the type of image reference.
///
-internal enum ImageReferenceKind
+public enum ImageReferenceKind
{
///
/// A Docker image reference (e.g., "node:latest", "sha256:abc123").
@@ -31,7 +31,7 @@ internal enum ImageReferenceKind
///
/// Represents a parsed image reference from the scan input, with its type and cleaned reference string.
///
-internal class ImageReference
+public class ImageReference
{
private const string OciDirPrefix = "oci-dir:";
private const string OciArchivePrefix = "oci-archive:";
diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/LinuxApplicationLayerDetector.cs b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxApplicationLayerDetector.cs
index f883a75a8..bdb3daeed 100644
--- a/src/Microsoft.ComponentDetection.Detectors/linux/LinuxApplicationLayerDetector.cs
+++ b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxApplicationLayerDetector.cs
@@ -12,13 +12,17 @@ namespace Microsoft.ComponentDetection.Detectors.Linux;
/// Linux detector (which only scans system packages).
///
/// The Linux scanner service.
+/// The Docker-based Syft runner.
+/// The factory for creating binary Syft runners.
/// The Docker service.
/// The logger.
public class LinuxApplicationLayerDetector(
ILinuxScanner linuxScanner,
+ IDockerSyftRunner dockerSyftRunner,
+ IBinarySyftRunnerFactory binarySyftRunnerFactory,
IDockerService dockerService,
ILogger logger
-) : LinuxContainerDetector(linuxScanner, dockerService, logger), IExperimentalDetector
+) : LinuxContainerDetector(linuxScanner, dockerSyftRunner, binarySyftRunnerFactory, dockerService, logger), IExperimentalDetector
{
///
public new string Id => "LinuxApplicationLayer";
diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/LinuxContainerDetector.cs b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxContainerDetector.cs
index 8b3fff13e..412604efc 100644
--- a/src/Microsoft.ComponentDetection.Detectors/linux/LinuxContainerDetector.cs
+++ b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxContainerDetector.cs
@@ -5,7 +5,6 @@ namespace Microsoft.ComponentDetection.Detectors.Linux;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.ComponentDetection.Common.Exceptions;
@@ -22,6 +21,8 @@ namespace Microsoft.ComponentDetection.Detectors.Linux;
///
public class LinuxContainerDetector(
ILinuxScanner linuxScanner,
+ IDockerSyftRunner dockerSyftRunner,
+ IBinarySyftRunnerFactory binarySyftRunnerFactory,
IDockerService dockerService,
ILogger logger
) : IComponentDetector
@@ -31,13 +32,15 @@ ILogger logger
private const string ScanScopeConfigKey = "Linux.ImageScanScope";
private const LinuxScannerScope DefaultScanScope = LinuxScannerScope.AllLayers;
- private const string LocalImageMountPoint = "/image";
+ private const string SyftBinaryPathConfigKey = "Linux.SyftBinaryPath";
// Base image annotations from ADO dockerTask
private const string BaseImageRefAnnotation = "image.base.ref.name";
private const string BaseImageDigestAnnotation = "image.base.digest";
private readonly ILinuxScanner linuxScanner = linuxScanner;
+ private readonly IDockerSyftRunner dockerSyftRunner = dockerSyftRunner;
+ private readonly IBinarySyftRunnerFactory binarySyftRunnerFactory = binarySyftRunnerFactory;
private readonly IDockerService dockerService = dockerService;
private readonly ILogger logger = logger;
@@ -95,17 +98,14 @@ public async Task ExecuteDetectorAsync(
}
var scannerScope = GetScanScope(request.DetectorArgs);
+ var syftRunner = this.GetSyftRunner(request.DetectorArgs);
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(GetTimeout(request.DetectorArgs));
- if (!await this.dockerService.CanRunLinuxContainersAsync(timeoutCts.Token))
+ if (!await syftRunner.CanRunAsync(timeoutCts.Token))
{
- using var record = new LinuxContainerDetectorUnsupportedOs
- {
- Os = RuntimeInformation.OSDescription,
- };
- this.logger.LogInformation("Linux containers are not available on this host.");
+ this.logger.LogInformation("Syft scanner is not available. Skipping Linux container scanning.");
return EmptySuccessfulScan();
}
@@ -116,6 +116,7 @@ public async Task ExecuteDetectorAsync(
allImages,
request.ComponentRecorder,
scannerScope,
+ syftRunner,
timeoutCts.Token
);
}
@@ -143,7 +144,7 @@ public async Task ExecuteDetectorAsync(
///
/// The arguments provided by the user.
/// Time interval representing the timeout defined by the user, or a default value if one is not provided.
- private static TimeSpan GetTimeout(IDictionary detectorArgs)
+ private static TimeSpan GetTimeout(IDictionary? detectorArgs)
{
var defaultTimeout = TimeSpan.FromMinutes(DefaultTimeoutMinutes);
@@ -219,10 +220,35 @@ private static void RecordImageDetectionFailure(Exception exception, string imag
};
}
+ ///
+ /// Gets the appropriate Syft runner based on detector arguments.
+ /// When Linux.SyftBinaryPath is provided, returns a binary runner via the factory;
+ /// otherwise returns the default Docker-based runner.
+ ///
+ /// The arguments provided by the user.
+ /// An to use for scanning.
+ private ISyftRunner GetSyftRunner(IDictionary detectorArgs)
+ {
+ if (
+ detectorArgs != null
+ && detectorArgs.TryGetValue(SyftBinaryPathConfigKey, out var syftBinaryPath)
+ && !string.IsNullOrWhiteSpace(syftBinaryPath)
+ )
+ {
+ this.logger.LogInformation(
+ "Using Syft binary at {SyftBinaryPath} for Linux container scanning",
+ syftBinaryPath);
+ return this.binarySyftRunnerFactory.Create(syftBinaryPath);
+ }
+
+ return this.dockerSyftRunner;
+ }
+
private async Task> ProcessImagesAsync(
IEnumerable imageReferences,
IComponentRecorder componentRecorder,
LinuxScannerScope scannerScope,
+ ISyftRunner syftRunner,
CancellationToken cancellationToken = default
)
{
@@ -232,7 +258,7 @@ private async Task> ProcessImagesAsync(
var processedDockerImages = new ConcurrentDictionary();
// Local images will be validated for existence and tracked by their file path.
- var localImages = new ConcurrentDictionary();
+ var localImages = new ConcurrentDictionary();
var resolveTasks = imageReferences.Select(imageRef =>
this.ResolveImageAsync(imageRef, processedDockerImages, localImages, componentRecorder, cancellationToken));
@@ -243,11 +269,11 @@ private async Task> ProcessImagesAsync(
var scanTasks = new List>();
scanTasks.AddRange(processedDockerImages.Select(kvp =>
- this.ScanDockerImageAsync(kvp.Key, kvp.Value, scannerScope, componentRecorder, cancellationToken)));
+ this.ScanDockerImageAsync(kvp.Key, kvp.Value, scannerScope, syftRunner, componentRecorder, cancellationToken)));
scanTasks.AddRange(localImages
.Select(kvp =>
- this.ScanLocalImageAsync(kvp.Key, kvp.Value, scannerScope, componentRecorder, cancellationToken)));
+ this.ScanLocalImageAsync(kvp.Value, scannerScope, syftRunner, componentRecorder, cancellationToken)));
return await Task.WhenAll(scanTasks);
}
@@ -262,7 +288,7 @@ private async Task> ProcessImagesAsync(
private async Task ResolveImageAsync(
ImageReference imageRef,
ConcurrentDictionary resolvedDockerImages,
- ConcurrentDictionary localImages,
+ ConcurrentDictionary localImages,
IComponentRecorder componentRecorder,
CancellationToken cancellationToken)
{
@@ -277,7 +303,13 @@ private async Task ResolveImageAsync(
case ImageReferenceKind.OciArchive:
case ImageReferenceKind.DockerArchive:
var fullPath = this.ValidateLocalImagePath(imageRef);
- localImages.TryAdd(fullPath, imageRef.Kind);
+ var resolvedRef = new ImageReference
+ {
+ OriginalInput = imageRef.OriginalInput,
+ Reference = fullPath,
+ Kind = imageRef.Kind,
+ };
+ localImages.TryAdd(fullPath, resolvedRef);
break;
default:
throw new InvalidUserInputException(
@@ -355,6 +387,7 @@ private async Task ScanDockerImageAsync(
string imageId,
ContainerDetails containerDetails,
LinuxScannerScope scannerScope,
+ ISyftRunner syftRunner,
IComponentRecorder componentRecorder,
CancellationToken cancellationToken)
{
@@ -377,14 +410,21 @@ private async Task ScanDockerImageAsync(
).ToList();
var enabledComponentTypes = this.GetEnabledComponentTypes();
+ var dockerImageRef = new ImageReference
+ {
+ OriginalInput = containerDetails.ImageId,
+ Reference = containerDetails.ImageId,
+ Kind = ImageReferenceKind.DockerImage,
+ };
var layers = await this.linuxScanner.ScanLinuxAsync(
- containerDetails.ImageId,
+ dockerImageRef,
containerDetails.Layers,
baseImageLayerCount,
enabledComponentTypes,
scannerScope,
- cancellationToken
- ) ?? throw new InvalidOperationException($"Failed to scan image layers for image {containerDetails.ImageId}");
+ syftRunner,
+ cancellationToken)
+ ?? throw new InvalidOperationException($"Failed to scan image layers for image {containerDetails.ImageId}");
return this.RecordComponents(containerDetails, layers, componentRecorder);
}
@@ -402,54 +442,25 @@ private async Task ScanDockerImageAsync(
}
///
- /// Scans a local image (OCI layout directory or archive file) by invoking Syft with a volume
- /// mount, extracting metadata from the Syft output to build ContainerDetails, and processing
+ /// Scans a local image (OCI layout directory or archive file) by invoking Syft,
+ /// extracting metadata from the Syft output to build ContainerDetails, and processing
/// detected components.
///
private async Task ScanLocalImageAsync(
- string localImagePath,
- ImageReferenceKind imageRefKind,
+ ImageReference imageReference,
LinuxScannerScope scannerScope,
+ ISyftRunner syftRunner,
IComponentRecorder componentRecorder,
CancellationToken cancellationToken)
{
- string hostPathToBind;
- string syftContainerPath;
- switch (imageRefKind)
- {
- case ImageReferenceKind.OciLayout:
- hostPathToBind = localImagePath;
- syftContainerPath = $"oci-dir:{LocalImageMountPoint}";
- break;
- case ImageReferenceKind.OciArchive:
- hostPathToBind = Path.GetDirectoryName(localImagePath)
- ?? throw new InvalidOperationException($"Could not determine parent directory for OCI archive path '{localImagePath}'.");
- syftContainerPath = $"oci-archive:{LocalImageMountPoint}/{Path.GetFileName(localImagePath)}";
- break;
- case ImageReferenceKind.DockerArchive:
- hostPathToBind = Path.GetDirectoryName(localImagePath)
- ?? throw new InvalidOperationException($"Could not determine parent directory for Docker archive path '{localImagePath}'.");
- syftContainerPath = $"docker-archive:{LocalImageMountPoint}/{Path.GetFileName(localImagePath)}";
- break;
- case ImageReferenceKind.DockerImage:
- default:
- throw new InvalidUserInputException(
- $"Unsupported image reference kind '{imageRefKind}' for local image at path '{localImagePath}'."
- );
- }
+ var localImagePath = imageReference.Reference;
try
{
- var additionalBinds = new List
- {
- // Bind the local image path into the Syft container as read-only
- $"{hostPathToBind}:{LocalImageMountPoint}:ro",
- };
-
var syftOutput = await this.linuxScanner.GetSyftOutputAsync(
- syftContainerPath,
- additionalBinds,
+ imageReference,
scannerScope,
+ syftRunner,
cancellationToken
);
diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/LinuxScanner.cs b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxScanner.cs
index 6482958b9..a05f55762 100644
--- a/src/Microsoft.ComponentDetection.Detectors/linux/LinuxScanner.cs
+++ b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxScanner.cs
@@ -7,7 +7,6 @@ namespace Microsoft.ComponentDetection.Detectors.Linux;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.ComponentDetection.Common.Telemetry.Records;
-using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Contracts.BcdeModels;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
using Microsoft.ComponentDetection.Detectors.Linux.Contracts;
@@ -20,22 +19,12 @@ namespace Microsoft.ComponentDetection.Detectors.Linux;
///
internal class LinuxScanner : ILinuxScanner
{
- private const string ScannerImage =
- "governancecontainerregistry.azurecr.io/syft:v1.37.0@sha256:48d679480c6d272c1801cf30460556959c01d4826795be31d4fd8b53750b7d91";
-
private static readonly IList CmdParameters = ["--quiet", "--output", "json"];
private static readonly IList ScopeAllLayersParameter = ["--scope", "all-layers"];
private static readonly IList ScopeSquashedParameter = ["--scope", "squashed"];
- private static readonly SemaphoreSlim ContainerSemaphore = new SemaphoreSlim(2);
-
- private static readonly int SemaphoreTimeout = Convert.ToInt32(
- TimeSpan.FromHours(1).TotalMilliseconds
- );
-
- private readonly IDockerService dockerService;
private readonly ILogger logger;
private readonly IEnumerable componentFactories;
private readonly IEnumerable artifactFilters;
@@ -48,18 +37,15 @@ private readonly Dictionary<
///
/// Initializes a new instance of the class.
///
- /// The docker service.
/// The logger.
/// The component factories.
/// The artifact filters.
public LinuxScanner(
- IDockerService dockerService,
ILogger logger,
IEnumerable componentFactories,
IEnumerable artifactFilters
)
{
- this.dockerService = dockerService;
this.logger = logger;
this.componentFactories = componentFactories;
this.artifactFilters = artifactFilters;
@@ -79,21 +65,22 @@ IEnumerable artifactFilters
///
public async Task> ScanLinuxAsync(
- string imageHash,
+ ImageReference imageReference,
IEnumerable containerLayers,
int baseImageLayerCount,
ISet enabledComponentTypes,
LinuxScannerScope scope,
+ ISyftRunner syftRunner,
CancellationToken cancellationToken = default
)
{
using var record = new LinuxScannerTelemetryRecord
{
- ImageToScan = imageHash,
- ScannerVersion = ScannerImage,
+ ImageToScan = imageReference.Reference,
+ ScannerVersion = DockerSyftRunner.ScannerImage,
};
using var syftTelemetryRecord = new LinuxScannerSyftTelemetryRecord();
- var stdout = await this.RunSyftAsync(imageHash, scope, additionalBinds: [], record, syftTelemetryRecord, cancellationToken);
+ var stdout = await this.RunSyftAsync(imageReference, scope, syftRunner, record, syftTelemetryRecord, cancellationToken);
try
{
@@ -103,26 +90,26 @@ public async Task> ScanLinuxAsync(
catch (Exception e)
{
record.FailedDeserializingScannerOutput = e.ToString();
- this.logger.LogError(e, "Failed to deserialize Syft output for image {ImageHash}", imageHash);
+ this.logger.LogError(e, "Failed to deserialize Syft output for image {ImageReference}", imageReference.Reference);
return [];
}
}
///
public async Task GetSyftOutputAsync(
- string syftSource,
- IList additionalBinds,
+ ImageReference imageReference,
LinuxScannerScope scope,
+ ISyftRunner syftRunner,
CancellationToken cancellationToken = default
)
{
using var record = new LinuxScannerTelemetryRecord
{
- ImageToScan = syftSource,
- ScannerVersion = ScannerImage,
+ ImageToScan = imageReference.Reference,
+ ScannerVersion = DockerSyftRunner.ScannerImage,
};
using var syftTelemetryRecord = new LinuxScannerSyftTelemetryRecord();
- var stdout = await this.RunSyftAsync(syftSource, scope, additionalBinds, record, syftTelemetryRecord, cancellationToken);
+ var stdout = await this.RunSyftAsync(imageReference, scope, syftRunner, record, syftTelemetryRecord, cancellationToken);
try
{
return SyftOutput.FromJson(stdout);
@@ -130,7 +117,7 @@ public async Task GetSyftOutputAsync(
catch (Exception e)
{
record.FailedDeserializingScannerOutput = e.ToString();
- this.logger.LogError(e, "Failed to deserialize Syft output for source {SyftSource}", syftSource);
+ this.logger.LogError(e, "Failed to deserialize Syft output for source {ImageReference}", imageReference.Reference);
throw;
}
}
@@ -252,17 +239,16 @@ private IEnumerable ProcessSyftOutputWithTelemetry(
}
///
- /// Runs the Syft scanner container and returns the stdout output.
+ /// Runs the Syft scanner and returns the stdout output.
///
private async Task RunSyftAsync(
- string syftSource,
+ ImageReference imageReference,
LinuxScannerScope scope,
- IList additionalBinds,
+ ISyftRunner syftRunner,
LinuxScannerTelemetryRecord record,
LinuxScannerSyftTelemetryRecord syftTelemetryRecord,
CancellationToken cancellationToken)
{
- var acquired = false;
var stdout = string.Empty;
var stderr = string.Empty;
@@ -278,44 +264,20 @@ private async Task RunSyftAsync(
try
{
- acquired = await ContainerSemaphore.WaitAsync(SemaphoreTimeout, cancellationToken);
- if (acquired)
- {
- try
- {
- var command = new List { syftSource }
- .Concat(CmdParameters)
- .Concat(scopeParameters)
- .ToList();
- (stdout, stderr) = await this.dockerService.CreateAndRunContainerAsync(
- ScannerImage,
- command,
- additionalBinds,
- cancellationToken
- );
- }
- catch (Exception e)
- {
- syftTelemetryRecord.Exception = JsonSerializer.Serialize(e);
- this.logger.LogError(e, "Failed to run syft");
- throw;
- }
- }
- else
- {
- record.SemaphoreFailure = true;
- this.logger.LogWarning(
- "Failed to enter the container semaphore for image {SyftSource}",
- syftSource
- );
- }
+ var arguments = CmdParameters
+ .Concat(scopeParameters)
+ .ToList();
+ (stdout, stderr) = await syftRunner.RunSyftAsync(
+ imageReference,
+ arguments,
+ cancellationToken
+ );
}
- finally
+ catch (Exception e)
{
- if (acquired)
- {
- ContainerSemaphore.Release();
- }
+ syftTelemetryRecord.Exception = JsonSerializer.Serialize(e);
+ this.logger.LogError(e, "Failed to run syft");
+ throw;
}
record.ScanStdErr = stderr;
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs
index 9a9a2b2c3..29c51697b 100644
--- a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs
@@ -43,6 +43,7 @@ public static class ServiceCollectionExtensions
public static IServiceCollection AddComponentDetection(this IServiceCollection services)
{
// Shared services
+ services.AddLogging();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
@@ -102,6 +103,8 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s
// Linux
services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/BinarySyftRunnerTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/BinarySyftRunnerTests.cs
new file mode 100644
index 000000000..b75b81488
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/BinarySyftRunnerTests.cs
@@ -0,0 +1,242 @@
+namespace Microsoft.ComponentDetection.Detectors.Tests;
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using AwesomeAssertions;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Detectors.Linux;
+using Microsoft.Extensions.Logging;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+[TestClass]
+[TestCategory("Governance/All")]
+[TestCategory("Governance/ComponentDetection")]
+public class BinarySyftRunnerTests
+{
+ private readonly Mock mockCommandLineService;
+ private readonly Mock> mockLogger;
+
+ public BinarySyftRunnerTests()
+ {
+ this.mockCommandLineService = new Mock();
+ this.mockLogger = new Mock>();
+ }
+
+ [TestMethod]
+ public async Task CanRunAsync_ReturnsTrueWhenVersionCheckSucceeds()
+ {
+ this.mockCommandLineService.Setup(service =>
+ service.ExecuteCommandAsync(
+ "/usr/local/bin/syft",
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny(),
+ "--version"))
+ .ReturnsAsync(new CommandLineExecutionResult
+ {
+ StdOut = "syft 1.37.0",
+ StdErr = string.Empty,
+ ExitCode = 0,
+ });
+
+ var runner = new BinarySyftRunner(
+ "/usr/local/bin/syft",
+ this.mockCommandLineService.Object,
+ this.mockLogger.Object);
+
+ var result = await runner.CanRunAsync();
+
+ result.Should().BeTrue();
+ }
+
+ [TestMethod]
+ public async Task CanRunAsync_ReturnsFalseWhenVersionCheckFails()
+ {
+ this.mockCommandLineService.Setup(service =>
+ service.ExecuteCommandAsync(
+ "/usr/local/bin/syft",
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny(),
+ "--version"))
+ .ReturnsAsync(new CommandLineExecutionResult
+ {
+ StdOut = string.Empty,
+ StdErr = "not a valid syft binary",
+ ExitCode = 1,
+ });
+
+ var runner = new BinarySyftRunner(
+ "/usr/local/bin/syft",
+ this.mockCommandLineService.Object,
+ this.mockLogger.Object);
+
+ var result = await runner.CanRunAsync();
+
+ result.Should().BeFalse();
+ }
+
+ [TestMethod]
+ public async Task RunSyftAsync_ConstructsCorrectCommandLine()
+ {
+ var expectedOutput = """{"artifacts":[],"distro":{"id":"ubuntu","versionID":"22.04"}}""";
+ this.mockCommandLineService.Setup(service =>
+ service.ExecuteCommandAsync(
+ It.IsAny(),
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .ReturnsAsync(new CommandLineExecutionResult
+ {
+ StdOut = expectedOutput,
+ StdErr = string.Empty,
+ ExitCode = 0,
+ });
+
+ var runner = new BinarySyftRunner(
+ "/usr/local/bin/syft",
+ this.mockCommandLineService.Object,
+ this.mockLogger.Object);
+
+ var (stdout, stderr) = await runner.RunSyftAsync(
+ new ImageReference { OriginalInput = "fake_hash", Reference = "fake_hash", Kind = ImageReferenceKind.DockerImage },
+ ["--quiet", "--output", "json", "--scope", "all-layers"]);
+
+ stdout.Should().Be(expectedOutput);
+ stderr.Should().BeEmpty();
+
+ this.mockCommandLineService.Verify(
+ service => service.ExecuteCommandAsync(
+ "/usr/local/bin/syft",
+ null,
+ null,
+ It.IsAny(),
+ It.Is(args =>
+ args.Length == 6
+ && args[0] == "fake_hash"
+ && args[1] == "--quiet"
+ && args[2] == "--output"
+ && args[3] == "json"
+ && args[4] == "--scope"
+ && args[5] == "all-layers")),
+ Times.Once);
+ }
+
+ [TestMethod]
+ public async Task RunSyftAsync_NonZeroExitCode_LogsErrorAndReturnsOutput()
+ {
+ this.mockCommandLineService.Setup(service =>
+ service.ExecuteCommandAsync(
+ It.IsAny(),
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .ReturnsAsync(new CommandLineExecutionResult
+ {
+ StdOut = string.Empty,
+ StdErr = "error: image not found",
+ ExitCode = 1,
+ });
+
+ var runner = new BinarySyftRunner(
+ "/usr/local/bin/syft",
+ this.mockCommandLineService.Object,
+ this.mockLogger.Object);
+
+ var (stdout, stderr) = await runner.RunSyftAsync(
+ new ImageReference { OriginalInput = "fake_hash", Reference = "fake_hash", Kind = ImageReferenceKind.DockerImage },
+ ["--quiet", "--output", "json"]);
+
+ stdout.Should().BeEmpty();
+ stderr.Should().Be("error: image not found");
+
+ this.mockLogger.Verify(
+ logger => logger.Log(
+ LogLevel.Error,
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ (System.Func)It.IsAny