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()), + Times.Once); + } + + [TestMethod] + public async Task RunSyftAsync_CancellationToken_PropagatedToCommand() + { + using var cts = new CancellationTokenSource(); + await cts.CancelAsync(); + + this.mockCommandLineService.Setup(service => + service.ExecuteCommandAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + cts.Token, + It.IsAny())) + .ThrowsAsync(new System.OperationCanceledException()); + + var runner = new BinarySyftRunner( + "/usr/local/bin/syft", + this.mockCommandLineService.Object, + this.mockLogger.Object); + + System.Func action = async () => await runner.RunSyftAsync( + new ImageReference { OriginalInput = "fake_hash", Reference = "fake_hash", Kind = ImageReferenceKind.DockerImage }, + ["--quiet", "--output", "json"], + cts.Token); + + await action.Should().ThrowAsync(); + } + + [TestMethod] + [DataRow(ImageReferenceKind.OciLayout, "/path/to/oci", "oci-dir:/path/to/oci")] + [DataRow(ImageReferenceKind.OciArchive, "/path/to/image.tar", "oci-archive:/path/to/image.tar")] + [DataRow(ImageReferenceKind.DockerArchive, "/path/to/save.tar", "docker-archive:/path/to/save.tar")] + [DataRow(ImageReferenceKind.DockerImage, "ubuntu:22.04", "ubuntu:22.04")] + public async Task RunSyftAsync_ConstructsCorrectSourceForImageKind( + ImageReferenceKind kind, + string reference, + string expectedSource) + { + this.mockCommandLineService.Setup(service => + service.ExecuteCommandAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(new CommandLineExecutionResult + { + StdOut = "{}", + StdErr = string.Empty, + ExitCode = 0, + }); + + var runner = new BinarySyftRunner( + "/usr/local/bin/syft", + this.mockCommandLineService.Object, + this.mockLogger.Object); + + var imageRef = new ImageReference + { + OriginalInput = reference, + Reference = reference, + Kind = kind, + }; + + await runner.RunSyftAsync(imageRef, ["--quiet", "--output", "json"]); + + this.mockCommandLineService.Verify( + service => service.ExecuteCommandAsync( + "/usr/local/bin/syft", + null, + null, + It.IsAny(), + It.Is(args => args[0] == expectedSource)), + Times.Once); + } +} diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/DockerSyftRunnerTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/DockerSyftRunnerTests.cs new file mode 100644 index 000000000..6eb211a5d --- /dev/null +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/DockerSyftRunnerTests.cs @@ -0,0 +1,211 @@ +namespace Microsoft.ComponentDetection.Detectors.Tests; + +using System.Collections.Generic; +using System.IO; +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 DockerSyftRunnerTests +{ + private readonly Mock mockDockerService; + private readonly Mock> mockLogger; + + public DockerSyftRunnerTests() + { + this.mockDockerService = new Mock(); + this.mockLogger = new Mock>(); + } + + [TestMethod] + public async Task CanRunAsync_ReturnsTrueWhenDockerCanRunLinuxContainers() + { + this.mockDockerService.Setup(s => + s.CanRunLinuxContainersAsync(It.IsAny())) + .ReturnsAsync(true); + + var runner = new DockerSyftRunner(this.mockDockerService.Object, this.mockLogger.Object); + + var result = await runner.CanRunAsync(); + + result.Should().BeTrue(); + } + + [TestMethod] + public async Task CanRunAsync_ReturnsFalseWhenDockerCannotRunLinuxContainers() + { + this.mockDockerService.Setup(s => + s.CanRunLinuxContainersAsync(It.IsAny())) + .ReturnsAsync(false); + + var runner = new DockerSyftRunner(this.mockDockerService.Object, this.mockLogger.Object); + + var result = await runner.CanRunAsync(); + + result.Should().BeFalse(); + } + + [TestMethod] + public async Task RunSyftAsync_DockerImage_PassesReferenceDirectly() + { + this.mockDockerService.Setup(s => + s.CreateAndRunContainerAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(("{}", string.Empty)); + + var runner = new DockerSyftRunner(this.mockDockerService.Object, this.mockLogger.Object); + var imageRef = new ImageReference + { + OriginalInput = "ubuntu:22.04", + Reference = "ubuntu:22.04", + Kind = ImageReferenceKind.DockerImage, + }; + + await runner.RunSyftAsync(imageRef, ["--quiet", "--output", "json"]); + + this.mockDockerService.Verify( + s => s.CreateAndRunContainerAsync( + It.IsAny(), + It.Is>(cmd => cmd[0] == "ubuntu:22.04"), + It.Is>(binds => binds.Count == 0), + It.IsAny()), + Times.Once); + } + + [TestMethod] + public async Task RunSyftAsync_OciLayout_MountsDirectoryAndUsesMountPoint() + { + this.mockDockerService.Setup(s => + s.CreateAndRunContainerAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(("{}", string.Empty)); + + var runner = new DockerSyftRunner(this.mockDockerService.Object, this.mockLogger.Object); + var imageRef = new ImageReference + { + OriginalInput = "oci-dir:/path/to/oci", + Reference = "/path/to/oci", + Kind = ImageReferenceKind.OciLayout, + }; + + await runner.RunSyftAsync(imageRef, ["--quiet", "--output", "json"]); + + this.mockDockerService.Verify( + s => s.CreateAndRunContainerAsync( + It.IsAny(), + It.Is>(cmd => cmd[0] == "oci-dir:/image"), + It.Is>(binds => + binds.Count == 1 && binds[0] == "/path/to/oci:/image:ro"), + It.IsAny()), + Times.Once); + } + + [TestMethod] + public async Task RunSyftAsync_OciArchive_MountsParentDirAndUsesFileName() + { + this.mockDockerService.Setup(s => + s.CreateAndRunContainerAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(("{}", string.Empty)); + + var runner = new DockerSyftRunner(this.mockDockerService.Object, this.mockLogger.Object); + var imageRef = new ImageReference + { + OriginalInput = "oci-archive:/archives/image.tar", + Reference = "/archives/image.tar", + Kind = ImageReferenceKind.OciArchive, + }; + + await runner.RunSyftAsync(imageRef, ["--quiet", "--output", "json"]); + + var expectedDir = Path.GetDirectoryName("/archives/image.tar"); + this.mockDockerService.Verify( + s => s.CreateAndRunContainerAsync( + It.IsAny(), + It.Is>(cmd => cmd[0] == "oci-archive:/image/image.tar"), + It.Is>(binds => + binds.Count == 1 && binds[0] == $"{expectedDir}:/image:ro"), + It.IsAny()), + Times.Once); + } + + [TestMethod] + public async Task RunSyftAsync_DockerArchive_MountsParentDirAndUsesFileName() + { + this.mockDockerService.Setup(s => + s.CreateAndRunContainerAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(("{}", string.Empty)); + + var runner = new DockerSyftRunner(this.mockDockerService.Object, this.mockLogger.Object); + var imageRef = new ImageReference + { + OriginalInput = "docker-archive:/saves/myimage.tar", + Reference = "/saves/myimage.tar", + Kind = ImageReferenceKind.DockerArchive, + }; + + await runner.RunSyftAsync(imageRef, ["--quiet", "--output", "json"]); + + var expectedDir = Path.GetDirectoryName("/saves/myimage.tar"); + this.mockDockerService.Verify( + s => s.CreateAndRunContainerAsync( + It.IsAny(), + It.Is>(cmd => cmd[0] == "docker-archive:/image/myimage.tar"), + It.Is>(binds => + binds.Count == 1 && binds[0] == $"{expectedDir}:/image:ro"), + It.IsAny()), + Times.Once); + } + + [TestMethod] + public async Task RunSyftAsync_OciLayout_PreservesCaseSensitivePath() + { + this.mockDockerService.Setup(s => + s.CreateAndRunContainerAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(("{}", string.Empty)); + + var runner = new DockerSyftRunner(this.mockDockerService.Object, this.mockLogger.Object); + var imageRef = new ImageReference + { + OriginalInput = "oci-dir:/Path/To/MyImage", + Reference = "/Path/To/MyImage", + Kind = ImageReferenceKind.OciLayout, + }; + + await runner.RunSyftAsync(imageRef, ["--quiet", "--output", "json"]); + + this.mockDockerService.Verify( + s => s.CreateAndRunContainerAsync( + It.IsAny(), + It.IsAny>(), + It.Is>(binds => + binds.Count == 1 && binds[0] == "/Path/To/MyImage:/image:ro"), + It.IsAny()), + Times.Once); + } +} diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxContainerDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxContainerDetectorTests.cs index 12308a383..54f7a5b23 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxContainerDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxContainerDetectorTests.cs @@ -38,15 +38,13 @@ public class LinuxContainerDetectorTests private readonly Mock mockDockerService; private readonly Mock mockLogger; private readonly Mock> mockLinuxContainerDetectorLogger; + private readonly Mock mockDockerSyftRunner; + private readonly Mock mockBinarySyftRunnerFactory; private readonly Mock mockSyftLinuxScanner; public LinuxContainerDetectorTests() { this.mockDockerService = new Mock(); - this.mockDockerService.Setup(service => - service.CanRunLinuxContainersAsync(It.IsAny()) - ) - .ReturnsAsync(true); this.mockDockerService.Setup(service => service.TryPullImageAsync(It.IsAny(), It.IsAny()) ) @@ -67,15 +65,22 @@ public LinuxContainerDetectorTests() this.mockLogger = new Mock(); this.mockLinuxContainerDetectorLogger = new Mock>(); + this.mockDockerSyftRunner = new Mock(); + this.mockDockerSyftRunner.Setup(runner => + runner.CanRunAsync(It.IsAny()) + ) + .ReturnsAsync(true); + this.mockBinarySyftRunnerFactory = new Mock(); this.mockSyftLinuxScanner = new Mock(); this.mockSyftLinuxScanner.Setup(scanner => scanner.ScanLinuxAsync( - It.IsAny(), + It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny>(), It.IsAny(), + It.IsAny(), It.IsAny() ) ) @@ -98,6 +103,8 @@ public async Task TestLinuxContainerDetectorAsync() var linuxContainerDetector = new LinuxContainerDetector( this.mockSyftLinuxScanner.Object, + this.mockDockerSyftRunner.Object, + this.mockBinarySyftRunnerFactory.Object, this.mockDockerService.Object, this.mockLinuxContainerDetectorLogger.Object ); @@ -138,13 +145,15 @@ public async Task TestLinuxContainerDetector_CantRunLinuxContainersAsync() componentRecorder ); - this.mockDockerService.Setup(service => - service.CanRunLinuxContainersAsync(It.IsAny()) + this.mockDockerSyftRunner.Setup(runner => + runner.CanRunAsync(It.IsAny()) ) .ReturnsAsync(false); var linuxContainerDetector = new LinuxContainerDetector( this.mockSyftLinuxScanner.Object, + this.mockDockerSyftRunner.Object, + this.mockBinarySyftRunnerFactory.Object, this.mockDockerService.Object, this.mockLinuxContainerDetectorLogger.Object ); @@ -183,6 +192,8 @@ public async Task TestLinuxContainerDetector_TestNullAsync() var linuxContainerDetector = new LinuxContainerDetector( this.mockSyftLinuxScanner.Object, + this.mockDockerSyftRunner.Object, + this.mockBinarySyftRunnerFactory.Object, this.mockDockerService.Object, this.mockLinuxContainerDetectorLogger.Object ); @@ -221,6 +232,8 @@ public async Task TestLinuxContainerDetector_VerifyLowerCaseAsync() var linuxContainerDetector = new LinuxContainerDetector( this.mockSyftLinuxScanner.Object, + this.mockDockerSyftRunner.Object, + this.mockBinarySyftRunnerFactory.Object, this.mockDockerService.Object, this.mockLinuxContainerDetectorLogger.Object ); @@ -256,6 +269,8 @@ public async Task TestLinuxContainerDetector_SameImagePassedMultipleTimesAsync() var linuxContainerDetector = new LinuxContainerDetector( this.mockSyftLinuxScanner.Object, + this.mockDockerSyftRunner.Object, + this.mockBinarySyftRunnerFactory.Object, this.mockDockerService.Object, this.mockLinuxContainerDetectorLogger.Object ); @@ -276,11 +291,12 @@ public async Task TestLinuxContainerDetector_SameImagePassedMultipleTimesAsync() this.mockSyftLinuxScanner.Verify( scanner => scanner.ScanLinuxAsync( - It.IsAny(), + It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny>(), It.IsAny(), + It.IsAny(), It.IsAny() ), Times.Once @@ -302,6 +318,8 @@ public async Task TestLinuxContainerDetector_TimeoutParameterSpecifiedAsync() var linuxContainerDetector = new LinuxContainerDetector( this.mockSyftLinuxScanner.Object, + this.mockDockerSyftRunner.Object, + this.mockBinarySyftRunnerFactory.Object, this.mockDockerService.Object, this.mockLinuxContainerDetectorLogger.Object ); @@ -333,6 +351,8 @@ public async Task TestLinuxContainerDetector_ImageScanScopeParameterSpecifiedAsy var linuxContainerDetector = new LinuxContainerDetector( this.mockSyftLinuxScanner.Object, + this.mockDockerSyftRunner.Object, + this.mockBinarySyftRunnerFactory.Object, this.mockDockerService.Object, this.mockLinuxContainerDetectorLogger.Object ); @@ -342,11 +362,12 @@ public async Task TestLinuxContainerDetector_ImageScanScopeParameterSpecifiedAsy this.mockSyftLinuxScanner.Verify( scanner => scanner.ScanLinuxAsync( - It.IsAny(), + It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny>(), expectedScope, + It.IsAny(), It.IsAny() ), Times.Once @@ -429,9 +450,9 @@ public async Task TestLinuxContainerDetector_OciLayoutImage_DetectsComponentsAsy this.mockSyftLinuxScanner.Setup(scanner => scanner.GetSyftOutputAsync( - It.IsAny(), - It.IsAny>(), + It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny() ) ) @@ -456,10 +477,12 @@ public async Task TestLinuxContainerDetector_OciLayoutImage_DetectsComponentsAsy .Returns(layerMappedComponents); var linuxContainerDetector = new LinuxContainerDetector( - this.mockSyftLinuxScanner.Object, - this.mockDockerService.Object, - this.mockLinuxContainerDetectorLogger.Object - ); + this.mockSyftLinuxScanner.Object, + this.mockDockerSyftRunner.Object, + this.mockBinarySyftRunnerFactory.Object, + this.mockDockerService.Object, + this.mockLinuxContainerDetectorLogger.Object + ); var scanResult = await linuxContainerDetector.ExecuteDetectorAsync(scanRequest); @@ -484,10 +507,9 @@ public async Task TestLinuxContainerDetector_OciLayoutImage_DetectsComponentsAsy this.mockSyftLinuxScanner.Verify( scanner => scanner.GetSyftOutputAsync( - It.Is(s => s.StartsWith("oci-dir:")), - It.Is>(binds => - binds.Count == 1 && binds[0].Contains(ociDir)), + It.Is(r => r.Kind == ImageReferenceKind.OciLayout), It.IsAny(), + It.IsAny(), It.IsAny() ), Times.Once @@ -563,9 +585,9 @@ public async Task TestLinuxContainerDetector_OciLayoutImage_DoesNotLowercasePath this.mockSyftLinuxScanner.Setup(scanner => scanner.GetSyftOutputAsync( - It.IsAny(), - It.IsAny>(), + It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny() ) ) @@ -581,10 +603,12 @@ public async Task TestLinuxContainerDetector_OciLayoutImage_DoesNotLowercasePath .Returns([]); var linuxContainerDetector = new LinuxContainerDetector( - this.mockSyftLinuxScanner.Object, - this.mockDockerService.Object, - this.mockLinuxContainerDetectorLogger.Object - ); + this.mockSyftLinuxScanner.Object, + this.mockDockerSyftRunner.Object, + this.mockBinarySyftRunnerFactory.Object, + this.mockDockerService.Object, + this.mockLinuxContainerDetectorLogger.Object + ); await linuxContainerDetector.ExecuteDetectorAsync(scanRequest); @@ -592,10 +616,9 @@ public async Task TestLinuxContainerDetector_OciLayoutImage_DoesNotLowercasePath this.mockSyftLinuxScanner.Verify( scanner => scanner.GetSyftOutputAsync( - It.Is(s => s.StartsWith("oci-dir:")), - It.Is>(binds => - binds.Count == 1 && binds[0].Contains(ociDir)), + It.Is(r => r.Kind == ImageReferenceKind.OciLayout && r.Reference == ociDir), It.IsAny(), + It.IsAny(), It.IsAny() ), Times.Once @@ -651,9 +674,9 @@ public async Task TestLinuxContainerDetector_OciLayoutImage_NormalizesPathAsync( this.mockSyftLinuxScanner.Setup(scanner => scanner.GetSyftOutputAsync( - It.IsAny(), - It.IsAny>(), + It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny() ) ) @@ -669,20 +692,21 @@ public async Task TestLinuxContainerDetector_OciLayoutImage_NormalizesPathAsync( .Returns([]); var linuxContainerDetector = new LinuxContainerDetector( - this.mockSyftLinuxScanner.Object, - this.mockDockerService.Object, - this.mockLinuxContainerDetectorLogger.Object - ); + this.mockSyftLinuxScanner.Object, + this.mockDockerSyftRunner.Object, + this.mockBinarySyftRunnerFactory.Object, + this.mockDockerService.Object, + this.mockLinuxContainerDetectorLogger.Object + ); await linuxContainerDetector.ExecuteDetectorAsync(scanRequest); this.mockSyftLinuxScanner.Verify( scanner => scanner.GetSyftOutputAsync( - It.Is(s => s.StartsWith("oci-dir:")), - It.Is>(binds => - binds.Count == 1 && binds[0].Contains(ociDir) && !binds[0].Contains(ociDirWithExtraComponents)), + It.Is(r => r.Kind == ImageReferenceKind.OciLayout && r.Reference.Contains(ociDir) && !r.Reference.Contains(ociDirWithExtraComponents)), It.IsAny(), + It.IsAny(), It.IsAny() ), Times.Once @@ -739,9 +763,9 @@ public async Task TestLinuxContainerDetector_MixedDockerAndOciImages_BothProcess this.mockSyftLinuxScanner.Setup(scanner => scanner.GetSyftOutputAsync( - It.IsAny(), - It.IsAny>(), + It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny() ) ) @@ -766,10 +790,12 @@ public async Task TestLinuxContainerDetector_MixedDockerAndOciImages_BothProcess .Returns(ociLayerMappedComponents); var linuxContainerDetector = new LinuxContainerDetector( - this.mockSyftLinuxScanner.Object, - this.mockDockerService.Object, - this.mockLinuxContainerDetectorLogger.Object - ); + this.mockSyftLinuxScanner.Object, + this.mockDockerSyftRunner.Object, + this.mockBinarySyftRunnerFactory.Object, + this.mockDockerService.Object, + this.mockLinuxContainerDetectorLogger.Object + ); var scanResult = await linuxContainerDetector.ExecuteDetectorAsync(scanRequest); @@ -824,9 +850,9 @@ public async Task TestLinuxContainerDetector_OciLayoutImage_NoMetadata_DetectsCo this.mockSyftLinuxScanner.Setup(scanner => scanner.GetSyftOutputAsync( - It.IsAny(), - It.IsAny>(), + It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny() ) ) @@ -851,10 +877,12 @@ public async Task TestLinuxContainerDetector_OciLayoutImage_NoMetadata_DetectsCo .Returns(layerMappedComponents); var linuxContainerDetector = new LinuxContainerDetector( - this.mockSyftLinuxScanner.Object, - this.mockDockerService.Object, - this.mockLinuxContainerDetectorLogger.Object - ); + this.mockSyftLinuxScanner.Object, + this.mockDockerSyftRunner.Object, + this.mockBinarySyftRunnerFactory.Object, + this.mockDockerService.Object, + this.mockLinuxContainerDetectorLogger.Object + ); var scanResult = await linuxContainerDetector.ExecuteDetectorAsync(scanRequest); @@ -937,9 +965,9 @@ public async Task TestLinuxContainerDetector_OciLayoutImage_IncompatibleMetadata this.mockSyftLinuxScanner.Setup(scanner => scanner.GetSyftOutputAsync( - It.IsAny(), - It.IsAny>(), + It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny() ) ) @@ -964,10 +992,12 @@ public async Task TestLinuxContainerDetector_OciLayoutImage_IncompatibleMetadata .Returns(layerMappedComponents); var linuxContainerDetector = new LinuxContainerDetector( - this.mockSyftLinuxScanner.Object, - this.mockDockerService.Object, - this.mockLinuxContainerDetectorLogger.Object - ); + this.mockSyftLinuxScanner.Object, + this.mockDockerSyftRunner.Object, + this.mockBinarySyftRunnerFactory.Object, + this.mockDockerService.Object, + this.mockLinuxContainerDetectorLogger.Object + ); var scanResult = await linuxContainerDetector.ExecuteDetectorAsync(scanRequest); @@ -1045,9 +1075,9 @@ public async Task TestLinuxContainerDetector_OciArchiveImage_DetectsComponentsAs this.mockSyftLinuxScanner.Setup(scanner => scanner.GetSyftOutputAsync( - It.IsAny(), - It.IsAny>(), + It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny() ) ) @@ -1072,10 +1102,12 @@ public async Task TestLinuxContainerDetector_OciArchiveImage_DetectsComponentsAs .Returns(layerMappedComponents); var linuxContainerDetector = new LinuxContainerDetector( - this.mockSyftLinuxScanner.Object, - this.mockDockerService.Object, - this.mockLinuxContainerDetectorLogger.Object - ); + this.mockSyftLinuxScanner.Object, + this.mockDockerSyftRunner.Object, + this.mockBinarySyftRunnerFactory.Object, + this.mockDockerService.Object, + this.mockLinuxContainerDetectorLogger.Object + ); var scanResult = await linuxContainerDetector.ExecuteDetectorAsync(scanRequest); @@ -1098,10 +1130,9 @@ public async Task TestLinuxContainerDetector_OciArchiveImage_DetectsComponentsAs this.mockSyftLinuxScanner.Verify( scanner => scanner.GetSyftOutputAsync( - It.Is(s => s.StartsWith("oci-archive:") && s.Contains(ociArchiveName)), - It.Is>(binds => - binds.Count == 1 && binds[0].Contains(ociArchiveDir)), + It.Is(r => r.Kind == ImageReferenceKind.OciArchive && r.Reference == ociArchive), It.IsAny(), + It.IsAny(), It.IsAny() ), Times.Once @@ -1177,9 +1208,9 @@ public async Task TestLinuxContainerDetector_DockerArchiveImage_DetectsComponent this.mockSyftLinuxScanner.Setup(scanner => scanner.GetSyftOutputAsync( - It.IsAny(), - It.IsAny>(), + It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny() ) ) @@ -1204,10 +1235,12 @@ public async Task TestLinuxContainerDetector_DockerArchiveImage_DetectsComponent .Returns(layerMappedComponents); var linuxContainerDetector = new LinuxContainerDetector( - this.mockSyftLinuxScanner.Object, - this.mockDockerService.Object, - this.mockLinuxContainerDetectorLogger.Object - ); + this.mockSyftLinuxScanner.Object, + this.mockDockerSyftRunner.Object, + this.mockBinarySyftRunnerFactory.Object, + this.mockDockerService.Object, + this.mockLinuxContainerDetectorLogger.Object + ); var scanResult = await linuxContainerDetector.ExecuteDetectorAsync(scanRequest); @@ -1230,10 +1263,9 @@ public async Task TestLinuxContainerDetector_DockerArchiveImage_DetectsComponent this.mockSyftLinuxScanner.Verify( scanner => scanner.GetSyftOutputAsync( - It.Is(s => s.StartsWith("docker-archive:") && s.Contains(dockerArchiveName)), - It.Is>(binds => - binds.Count == 1 && binds[0].Contains(dockerArchiveDir)), + It.Is(r => r.Kind == ImageReferenceKind.DockerArchive && r.Reference == dockerArchive), It.IsAny(), + It.IsAny(), It.IsAny() ), Times.Once @@ -1262,6 +1294,8 @@ public async Task TestLinuxContainerDetector_ImageParseFailure_ContinuesScanning var linuxContainerDetector = new LinuxContainerDetector( this.mockSyftLinuxScanner.Object, + this.mockDockerSyftRunner.Object, + this.mockBinarySyftRunnerFactory.Object, this.mockDockerService.Object, this.mockLinuxContainerDetectorLogger.Object ); diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxScannerTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxScannerTests.cs index 5178eec0b..64195148e 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxScannerTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxScannerTests.cs @@ -6,7 +6,6 @@ namespace Microsoft.ComponentDetection.Detectors.Tests; using System.Threading; using System.Threading.Tasks; using AwesomeAssertions; -using Microsoft.ComponentDetection.Contracts; using Microsoft.ComponentDetection.Contracts.BcdeModels; using Microsoft.ComponentDetection.Contracts.TypedComponent; using Microsoft.ComponentDetection.Detectors.Linux; @@ -221,21 +220,14 @@ public class LinuxScannerTests """; private readonly LinuxScanner linuxScanner; - private readonly Mock mockDockerService; + private readonly Mock mockSyftRunner; private readonly Mock> mockLogger; private readonly List componentFactories; private readonly List artifactFilters; public LinuxScannerTests() { - this.mockDockerService = new Mock(); - this.mockDockerService.Setup(service => - service.CanPingDockerAsync(It.IsAny()) - ) - .ReturnsAsync(true); - this.mockDockerService.Setup(service => - service.TryPullImageAsync(It.IsAny(), It.IsAny()) - ); + this.mockSyftRunner = new Mock(); this.mockLogger = new Mock>(); @@ -250,7 +242,6 @@ public LinuxScannerTests() this.artifactFilters = [new Mariner2ArtifactFilter()]; this.linuxScanner = new LinuxScanner( - this.mockDockerService.Object, this.mockLogger.Object, this.componentFactories, this.artifactFilters @@ -262,10 +253,9 @@ public LinuxScannerTests() [DataRow(SyftOutputLicenseFieldAndMaintainer)] public async Task TestLinuxScannerAsync(string syftOutput) { - this.mockDockerService.Setup(service => - service.CreateAndRunContainerAsync( - It.IsAny(), - It.IsAny>(), + this.mockSyftRunner.Setup(runner => + runner.RunSyftAsync( + It.IsAny(), It.IsAny>(), It.IsAny() ) @@ -280,7 +270,7 @@ public async Task TestLinuxScannerAsync(string syftOutput) }; var result = ( await this.linuxScanner.ScanLinuxAsync( - "fake_hash", + new ImageReference { OriginalInput = "fake_hash", Reference = "fake_hash", Kind = ImageReferenceKind.DockerImage }, [ new DockerLayer { @@ -291,7 +281,8 @@ await this.linuxScanner.ScanLinuxAsync( ], 0, enabledTypes, - LinuxScannerScope.AllLayers + LinuxScannerScope.AllLayers, + this.mockSyftRunner.Object ) ) .First() @@ -313,10 +304,9 @@ await this.linuxScanner.ScanLinuxAsync( [DataRow(SyftOutputNoAuthorOrLicense)] public async Task TestLinuxScanner_ReturnsNullAuthorAndLicense_Async(string syftOutput) { - this.mockDockerService.Setup(service => - service.CreateAndRunContainerAsync( - It.IsAny(), - It.IsAny>(), + this.mockSyftRunner.Setup(runner => + runner.RunSyftAsync( + It.IsAny(), It.IsAny>(), It.IsAny() ) @@ -331,7 +321,7 @@ public async Task TestLinuxScanner_ReturnsNullAuthorAndLicense_Async(string syft }; var result = ( await this.linuxScanner.ScanLinuxAsync( - "fake_hash", + new ImageReference { OriginalInput = "fake_hash", Reference = "fake_hash", Kind = ImageReferenceKind.DockerImage }, [ new DockerLayer { @@ -342,7 +332,8 @@ await this.linuxScanner.ScanLinuxAsync( ], 0, enabledTypes, - LinuxScannerScope.AllLayers + LinuxScannerScope.AllLayers, + this.mockSyftRunner.Object ) ) .First() @@ -366,10 +357,9 @@ public async Task TestLinuxScanner_SyftOutputIgnoreInvalidMarinerPackages_Async( string syftOutput ) { - this.mockDockerService.Setup(service => - service.CreateAndRunContainerAsync( - It.IsAny(), - It.IsAny>(), + this.mockSyftRunner.Setup(runner => + runner.RunSyftAsync( + It.IsAny(), It.IsAny>(), It.IsAny() ) @@ -384,7 +374,7 @@ string syftOutput }; var result = ( await this.linuxScanner.ScanLinuxAsync( - "fake_hash", + new ImageReference { OriginalInput = "fake_hash", Reference = "fake_hash", Kind = ImageReferenceKind.DockerImage }, [ new DockerLayer { @@ -395,7 +385,8 @@ await this.linuxScanner.ScanLinuxAsync( ], 0, enabledTypes, - LinuxScannerScope.AllLayers + LinuxScannerScope.AllLayers, + this.mockSyftRunner.Object ) ) .First() @@ -419,10 +410,9 @@ public async Task TestLinuxScanner_SyftOutputKeepNonduplicatedMarinerPackages_As string syftOutput ) { - this.mockDockerService.Setup(service => - service.CreateAndRunContainerAsync( - It.IsAny(), - It.IsAny>(), + this.mockSyftRunner.Setup(runner => + runner.RunSyftAsync( + It.IsAny(), It.IsAny>(), It.IsAny() ) @@ -437,7 +427,7 @@ string syftOutput }; var result = ( await this.linuxScanner.ScanLinuxAsync( - "fake_hash", + new ImageReference { OriginalInput = "fake_hash", Reference = "fake_hash", Kind = ImageReferenceKind.DockerImage }, [ new DockerLayer { @@ -448,7 +438,8 @@ await this.linuxScanner.ScanLinuxAsync( ], 0, enabledTypes, - LinuxScannerScope.AllLayers + LinuxScannerScope.AllLayers, + this.mockSyftRunner.Object ) ) .First() @@ -515,10 +506,9 @@ public async Task TestLinuxScanner_SupportsMultipleComponentTypes_Async() } """; - this.mockDockerService.Setup(service => - service.CreateAndRunContainerAsync( - It.IsAny(), - It.IsAny>(), + this.mockSyftRunner.Setup(runner => + runner.RunSyftAsync( + It.IsAny(), It.IsAny>(), It.IsAny() ) @@ -532,14 +522,15 @@ public async Task TestLinuxScanner_SupportsMultipleComponentTypes_Async() ComponentType.Pip, }; var layers = await this.linuxScanner.ScanLinuxAsync( - "fake_hash", + new ImageReference { OriginalInput = "fake_hash", Reference = "fake_hash", Kind = ImageReferenceKind.DockerImage }, [ new DockerLayer { LayerIndex = 0, DiffId = "sha256:layer1" }, new DockerLayer { LayerIndex = 1, DiffId = "sha256:layer2" }, ], 0, enabledTypes, - LinuxScannerScope.AllLayers + LinuxScannerScope.AllLayers, + this.mockSyftRunner.Object ); var allComponents = layers.SelectMany(l => l.Components).ToList(); @@ -621,10 +612,9 @@ public async Task TestLinuxScanner_FiltersComponentsByEnabledTypes_OnlyLinux_Asy } """; - this.mockDockerService.Setup(service => - service.CreateAndRunContainerAsync( - It.IsAny(), - It.IsAny>(), + this.mockSyftRunner.Setup(runner => + runner.RunSyftAsync( + It.IsAny(), It.IsAny>(), It.IsAny() ) @@ -634,14 +624,15 @@ public async Task TestLinuxScanner_FiltersComponentsByEnabledTypes_OnlyLinux_Asy // Only enable Linux component type var enabledTypes = new HashSet { ComponentType.Linux }; var layers = await this.linuxScanner.ScanLinuxAsync( - "fake_hash", + new ImageReference { OriginalInput = "fake_hash", Reference = "fake_hash", Kind = ImageReferenceKind.DockerImage }, [ new DockerLayer { LayerIndex = 0, DiffId = "sha256:layer1" }, new DockerLayer { LayerIndex = 1, DiffId = "sha256:layer2" }, ], 0, enabledTypes, - LinuxScannerScope.AllLayers + LinuxScannerScope.AllLayers, + this.mockSyftRunner.Object ); var allComponents = layers.SelectMany(l => l.Components).ToList(); @@ -708,10 +699,9 @@ public async Task TestLinuxScanner_FiltersComponentsByEnabledTypes_OnlyNpmAndPip } """; - this.mockDockerService.Setup(service => - service.CreateAndRunContainerAsync( - It.IsAny(), - It.IsAny>(), + this.mockSyftRunner.Setup(runner => + runner.RunSyftAsync( + It.IsAny(), It.IsAny>(), It.IsAny() ) @@ -721,14 +711,15 @@ public async Task TestLinuxScanner_FiltersComponentsByEnabledTypes_OnlyNpmAndPip // Only enable Npm and Pip component types (exclude Linux) var enabledTypes = new HashSet { ComponentType.Npm, ComponentType.Pip }; var layers = await this.linuxScanner.ScanLinuxAsync( - "fake_hash", + new ImageReference { OriginalInput = "fake_hash", Reference = "fake_hash", Kind = ImageReferenceKind.DockerImage }, [ new DockerLayer { LayerIndex = 0, DiffId = "sha256:layer1" }, new DockerLayer { LayerIndex = 1, DiffId = "sha256:layer2" }, ], 0, enabledTypes, - LinuxScannerScope.AllLayers + LinuxScannerScope.AllLayers, + this.mockSyftRunner.Object ); var allComponents = layers.SelectMany(l => l.Components).ToList(); @@ -752,10 +743,9 @@ public async Task TestLinuxScanner_ScopeParameter_IncludesCorrectFlagAsync( string expectedFlag ) { - this.mockDockerService.Setup(service => - service.CreateAndRunContainerAsync( - It.IsAny(), - It.IsAny>(), + this.mockSyftRunner.Setup(runner => + runner.RunSyftAsync( + It.IsAny(), It.IsAny>(), It.IsAny() ) @@ -764,21 +754,21 @@ string expectedFlag var enabledTypes = new HashSet { ComponentType.Linux }; await this.linuxScanner.ScanLinuxAsync( - "fake_hash", + new ImageReference { OriginalInput = "fake_hash", Reference = "fake_hash", Kind = ImageReferenceKind.DockerImage }, [new DockerLayer { LayerIndex = 0, DiffId = "sha256:layer1" }], 0, enabledTypes, - scope + scope, + this.mockSyftRunner.Object ); - this.mockDockerService.Verify( - service => - service.CreateAndRunContainerAsync( - It.IsAny(), - It.Is>(cmd => - cmd.Contains("--scope") && cmd.Contains(expectedFlag) + this.mockSyftRunner.Verify( + runner => + runner.RunSyftAsync( + It.IsAny(), + It.Is>(args => + args.Contains("--scope") && args.Contains(expectedFlag) ), - It.IsAny>(), It.IsAny() ), Times.Once @@ -793,11 +783,12 @@ public async Task TestLinuxScanner_InvalidScopeParameter_ThrowsArgumentOutOfRang Func action = async () => await this.linuxScanner.ScanLinuxAsync( - "fake_hash", + new ImageReference { OriginalInput = "fake_hash", Reference = "fake_hash", Kind = ImageReferenceKind.DockerImage }, [new DockerLayer { LayerIndex = 0, DiffId = "sha256:layer1" }], 0, enabledTypes, - invalidScope + invalidScope, + this.mockSyftRunner.Object ); await action.Should().ThrowAsync(); @@ -865,21 +856,20 @@ public async Task TestLinuxScanner_ScanLinuxSyftOutputAsync_ReturnsParsedSyftOut } """; - this.mockDockerService.Setup(service => - service.CreateAndRunContainerAsync( - It.IsAny(), - It.IsAny>(), + this.mockSyftRunner.Setup(runner => + runner.RunSyftAsync( + It.IsAny(), It.IsAny>(), It.IsAny() ) ) .ReturnsAsync((syftOutputWithSource, string.Empty)); - var additionalBinds = new List { "/some/oci/path:/oci-image:ro" }; + var ociRef = new ImageReference { OriginalInput = "oci-dir:/oci-image", Reference = "/host/path/to/oci", Kind = ImageReferenceKind.OciLayout }; var syftOutput = await this.linuxScanner.GetSyftOutputAsync( - "oci-dir:/oci-image", - additionalBinds, - LinuxScannerScope.AllLayers + ociRef, + LinuxScannerScope.AllLayers, + this.mockSyftRunner.Object ); syftOutput.Should().NotBeNull(); @@ -936,10 +926,9 @@ public async Task TestLinuxScanner_ScanLinuxSyftOutputAsync_PassesAdditionalBind } """; - this.mockDockerService.Setup(service => - service.CreateAndRunContainerAsync( - It.IsAny(), - It.IsAny>(), + this.mockSyftRunner.Setup(runner => + runner.RunSyftAsync( + It.IsAny(), It.IsAny>(), It.IsAny() ) @@ -947,21 +936,19 @@ public async Task TestLinuxScanner_ScanLinuxSyftOutputAsync_PassesAdditionalBind .ReturnsAsync((syftOutput, string.Empty)); var additionalBinds = new List { "/host/path/to/oci:/oci-image:ro" }; + var ociRef = new ImageReference { OriginalInput = "oci-dir:/oci-image", Reference = "/host/path/to/oci", Kind = ImageReferenceKind.OciLayout }; await this.linuxScanner.GetSyftOutputAsync( - "oci-dir:/oci-image", - additionalBinds, - LinuxScannerScope.AllLayers + ociRef, + LinuxScannerScope.AllLayers, + this.mockSyftRunner.Object ); // Verify the Syft command uses oci-dir: scheme and passes binds - this.mockDockerService.Verify( - service => - service.CreateAndRunContainerAsync( - It.IsAny(), - It.Is>(cmd => cmd[0] == "oci-dir:/oci-image"), - It.Is>(binds => - binds.Count == 1 && binds[0] == "/host/path/to/oci:/oci-image:ro" - ), + this.mockSyftRunner.Verify( + runner => + runner.RunSyftAsync( + It.Is(r => r.Kind == ImageReferenceKind.OciLayout && r.Reference == "/host/path/to/oci"), + It.IsAny>(), It.IsAny() ), Times.Once diff --git a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/LinuxApplicationLayerExperimentTests.cs b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/LinuxApplicationLayerExperimentTests.cs index fdfc38443..62c74f230 100644 --- a/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/LinuxApplicationLayerExperimentTests.cs +++ b/test/Microsoft.ComponentDetection.Orchestrator.Tests/Experiments/LinuxApplicationLayerExperimentTests.cs @@ -16,7 +16,7 @@ public class LinuxApplicationLayerExperimentTests [TestMethod] public void IsInControlGroup_LinuxContainerDetector_ReturnsTrue() { - var linuxDetector = new LinuxContainerDetector(null!, null!, null!); + var linuxDetector = new LinuxContainerDetector(null!, null!, null!, null!, null!); this.experiment.IsInControlGroup(linuxDetector).Should().BeTrue(); } @@ -63,21 +63,21 @@ public void IsInControlGroup_FileBasedDetectors_ReturnsTrue() [TestMethod] public void IsInControlGroup_LinuxApplicationLayerDetector_ReturnsFalse() { - var experimentalDetector = new LinuxApplicationLayerDetector(null!, null!, null!); + var experimentalDetector = new LinuxApplicationLayerDetector(null!, null!, null!, null!, null!); this.experiment.IsInControlGroup(experimentalDetector).Should().BeFalse(); } [TestMethod] public void IsInExperimentGroup_LinuxApplicationLayerDetector_ReturnsTrue() { - var experimentalDetector = new LinuxApplicationLayerDetector(null!, null!, null!); + var experimentalDetector = new LinuxApplicationLayerDetector(null!, null!, null!, null!, null!); this.experiment.IsInExperimentGroup(experimentalDetector).Should().BeTrue(); } [TestMethod] public void IsInExperimentGroup_LinuxContainerDetector_ReturnsFalse() { - var linuxDetector = new LinuxContainerDetector(null!, null!, null!); + var linuxDetector = new LinuxContainerDetector(null!, null!, null!, null!, null!); this.experiment.IsInExperimentGroup(linuxDetector).Should().BeFalse(); } @@ -124,28 +124,28 @@ public void IsInExperimentGroup_FileBasedDetectors_ReturnsTrue() [TestMethod] public void ShouldRecord_ExperimentGroup_ReturnsTrue_WhenNumComponentsGreaterThanZero() { - var experimentalDetector = new LinuxApplicationLayerDetector(null!, null!, null!); + var experimentalDetector = new LinuxApplicationLayerDetector(null!, null!, null!, null!, null!); this.experiment.ShouldRecord(experimentalDetector, 1).Should().BeTrue(); } [TestMethod] public void ShouldRecord_ExperimentGroup_ReturnsFalse_WhenNumComponentsIsZero() { - var experimentalDetector = new LinuxApplicationLayerDetector(null!, null!, null!); + var experimentalDetector = new LinuxApplicationLayerDetector(null!, null!, null!, null!, null!); this.experiment.ShouldRecord(experimentalDetector, 0).Should().BeFalse(); } [TestMethod] public void ShouldRecord_ControlGroup_ReturnsTrue_WhenNumComponentsGreaterThanZero() { - var linuxDetector = new LinuxContainerDetector(null!, null!, null!); + var linuxDetector = new LinuxContainerDetector(null!, null!, null!, null!, null!); this.experiment.ShouldRecord(linuxDetector, 1).Should().BeTrue(); } [TestMethod] public void ShouldRecord_ControlGroup_ReturnsFalse_WhenNumComponentsIsZero() { - var linuxDetector = new LinuxContainerDetector(null!, null!, null!); + var linuxDetector = new LinuxContainerDetector(null!, null!, null!, null!, null!); this.experiment.ShouldRecord(linuxDetector, 0).Should().BeFalse(); }