Skip to content

Commit a6c9b5b

Browse files
committed
Add ability to use syft binary instead of container for Linux detector
1 parent c3ef82c commit a6c9b5b

17 files changed

Lines changed: 1115 additions & 298 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
namespace Microsoft.ComponentDetection.Detectors.Linux;
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Microsoft.ComponentDetection.Contracts;
9+
using Microsoft.Extensions.Logging;
10+
11+
/// <summary>
12+
/// Runs Syft by invoking a local Syft binary.
13+
/// </summary>
14+
internal class BinarySyftRunner : ISyftRunner
15+
{
16+
private static readonly SemaphoreSlim BinarySemaphore = new(2);
17+
18+
private static readonly int SemaphoreTimeout = Convert.ToInt32(
19+
TimeSpan.FromHours(1).TotalMilliseconds);
20+
21+
private readonly string syftBinaryPath;
22+
private readonly ICommandLineInvocationService commandLineInvocationService;
23+
private readonly ILogger<BinarySyftRunner> logger;
24+
25+
/// <summary>
26+
/// Initializes a new instance of the <see cref="BinarySyftRunner"/> class.
27+
/// </summary>
28+
/// <param name="syftBinaryPath">The path to the Syft binary.</param>
29+
/// <param name="commandLineInvocationService">The command line invocation service.</param>
30+
/// <param name="logger">The logger.</param>
31+
public BinarySyftRunner(
32+
string syftBinaryPath,
33+
ICommandLineInvocationService commandLineInvocationService,
34+
ILogger<BinarySyftRunner> logger)
35+
{
36+
this.syftBinaryPath = syftBinaryPath;
37+
this.commandLineInvocationService = commandLineInvocationService;
38+
this.logger = logger;
39+
}
40+
41+
/// <inheritdoc/>
42+
public async Task<bool> CanRunAsync(CancellationToken cancellationToken = default)
43+
{
44+
var result = await this.commandLineInvocationService.ExecuteCommandAsync(
45+
this.syftBinaryPath,
46+
null,
47+
null,
48+
cancellationToken,
49+
"--version");
50+
51+
if (result.ExitCode != 0)
52+
{
53+
this.logger.LogInformation(
54+
"Syft binary at {SyftBinaryPath} failed version check with exit code {ExitCode}. Stderr: {StdErr}",
55+
this.syftBinaryPath,
56+
result.ExitCode,
57+
result.StdErr);
58+
return false;
59+
}
60+
61+
this.logger.LogInformation(
62+
"Using Syft binary at {SyftBinaryPath}: {SyftVersion}",
63+
this.syftBinaryPath,
64+
result.StdOut?.Trim());
65+
return true;
66+
}
67+
68+
/// <inheritdoc/>
69+
public async Task<(string Stdout, string Stderr)> RunSyftAsync(
70+
ImageReference imageReference,
71+
IList<string> arguments,
72+
CancellationToken cancellationToken = default)
73+
{
74+
var syftSource = GetSyftSource(imageReference);
75+
var acquired = false;
76+
77+
try
78+
{
79+
acquired = await BinarySemaphore.WaitAsync(SemaphoreTimeout, cancellationToken);
80+
if (!acquired)
81+
{
82+
this.logger.LogWarning(
83+
"Failed to enter the binary semaphore for image {ImageReference}",
84+
imageReference.Reference);
85+
return (string.Empty, string.Empty);
86+
}
87+
88+
var parameters = new[] { syftSource }
89+
.Concat(arguments)
90+
.ToArray();
91+
92+
var result = await this.commandLineInvocationService.ExecuteCommandAsync(
93+
this.syftBinaryPath,
94+
null,
95+
null,
96+
cancellationToken,
97+
parameters);
98+
99+
if (result.ExitCode != 0)
100+
{
101+
this.logger.LogError(
102+
"Syft binary exited with code {ExitCode}. Stderr: {StdErr}",
103+
result.ExitCode,
104+
result.StdErr);
105+
}
106+
107+
return (result.StdOut, result.StdErr);
108+
}
109+
finally
110+
{
111+
if (acquired)
112+
{
113+
BinarySemaphore.Release();
114+
}
115+
}
116+
}
117+
118+
/// <summary>
119+
/// Constructs the Syft source argument from an image reference.
120+
/// For local images, the host path is used directly with the appropriate scheme prefix.
121+
/// </summary>
122+
private static string GetSyftSource(ImageReference imageReference) =>
123+
imageReference.Kind switch
124+
{
125+
ImageReferenceKind.DockerImage => imageReference.Reference,
126+
ImageReferenceKind.OciLayout => $"oci-dir:{imageReference.Reference}",
127+
ImageReferenceKind.OciArchive => $"oci-archive:{imageReference.Reference}",
128+
ImageReferenceKind.DockerArchive => $"docker-archive:{imageReference.Reference}",
129+
_ => throw new ArgumentOutOfRangeException(
130+
nameof(imageReference),
131+
$"Unsupported image reference kind '{imageReference.Kind}'."),
132+
};
133+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
namespace Microsoft.ComponentDetection.Detectors.Linux;
2+
3+
using Microsoft.ComponentDetection.Contracts;
4+
using Microsoft.Extensions.Logging;
5+
6+
/// <summary>
7+
/// Factory for creating <see cref="BinarySyftRunner"/> instances.
8+
/// </summary>
9+
internal class BinarySyftRunnerFactory : IBinarySyftRunnerFactory
10+
{
11+
private readonly ICommandLineInvocationService commandLineInvocationService;
12+
private readonly ILoggerFactory loggerFactory;
13+
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="BinarySyftRunnerFactory"/> class.
16+
/// </summary>
17+
/// <param name="commandLineInvocationService">The command line invocation service.</param>
18+
/// <param name="loggerFactory">The logger factory.</param>
19+
public BinarySyftRunnerFactory(
20+
ICommandLineInvocationService commandLineInvocationService,
21+
ILoggerFactory loggerFactory)
22+
{
23+
this.commandLineInvocationService = commandLineInvocationService;
24+
this.loggerFactory = loggerFactory;
25+
}
26+
27+
/// <inheritdoc/>
28+
public ISyftRunner Create(string binaryPath) =>
29+
new BinarySyftRunner(
30+
binaryPath,
31+
this.commandLineInvocationService,
32+
this.loggerFactory.CreateLogger<BinarySyftRunner>());
33+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
namespace Microsoft.ComponentDetection.Detectors.Linux;
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.IO;
6+
using System.Linq;
7+
using System.Runtime.InteropServices;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Microsoft.ComponentDetection.Common.Telemetry.Records;
11+
using Microsoft.ComponentDetection.Contracts;
12+
using Microsoft.Extensions.Logging;
13+
14+
/// <summary>
15+
/// Runs Syft by executing a Docker container with the Syft image.
16+
/// </summary>
17+
internal class DockerSyftRunner : IDockerSyftRunner
18+
{
19+
internal const string ScannerImage =
20+
"governancecontainerregistry.azurecr.io/syft:v1.37.0@sha256:48d679480c6d272c1801cf30460556959c01d4826795be31d4fd8b53750b7d91";
21+
22+
private const string LocalImageMountPoint = "/image";
23+
24+
private static readonly SemaphoreSlim ContainerSemaphore = new(2);
25+
26+
private static readonly int SemaphoreTimeout = Convert.ToInt32(
27+
TimeSpan.FromHours(1).TotalMilliseconds);
28+
29+
private readonly IDockerService dockerService;
30+
private readonly ILogger<DockerSyftRunner> logger;
31+
32+
/// <summary>
33+
/// Initializes a new instance of the <see cref="DockerSyftRunner"/> class.
34+
/// </summary>
35+
/// <param name="dockerService">The docker service.</param>
36+
/// <param name="logger">The logger.</param>
37+
public DockerSyftRunner(IDockerService dockerService, ILogger<DockerSyftRunner> logger)
38+
{
39+
this.dockerService = dockerService;
40+
this.logger = logger;
41+
}
42+
43+
/// <inheritdoc/>
44+
public async Task<bool> CanRunAsync(CancellationToken cancellationToken = default)
45+
{
46+
if (await this.dockerService.CanRunLinuxContainersAsync(cancellationToken))
47+
{
48+
return true;
49+
}
50+
51+
using var record = new LinuxContainerDetectorUnsupportedOs
52+
{
53+
Os = RuntimeInformation.OSDescription,
54+
};
55+
this.logger.LogInformation("Linux containers are not available on this host.");
56+
return false;
57+
}
58+
59+
/// <inheritdoc/>
60+
public async Task<(string Stdout, string Stderr)> RunSyftAsync(
61+
ImageReference imageReference,
62+
IList<string> arguments,
63+
CancellationToken cancellationToken = default)
64+
{
65+
var (syftSource, additionalBinds) = GetSyftSourceAndBinds(imageReference);
66+
var acquired = false;
67+
68+
try
69+
{
70+
acquired = await ContainerSemaphore.WaitAsync(SemaphoreTimeout, cancellationToken);
71+
if (!acquired)
72+
{
73+
this.logger.LogWarning(
74+
"Failed to enter the container semaphore for image {ImageReference}",
75+
imageReference.Reference);
76+
return (string.Empty, string.Empty);
77+
}
78+
79+
var command = new List<string> { syftSource }
80+
.Concat(arguments)
81+
.ToList();
82+
83+
return await this.dockerService.CreateAndRunContainerAsync(
84+
ScannerImage,
85+
command,
86+
additionalBinds,
87+
cancellationToken);
88+
}
89+
finally
90+
{
91+
if (acquired)
92+
{
93+
ContainerSemaphore.Release();
94+
}
95+
}
96+
}
97+
98+
/// <summary>
99+
/// Constructs the Syft source argument and any required Docker bind mounts from an image reference.
100+
/// For Docker images, no additional binds are needed. For local images (OCI/archives),
101+
/// the host path is mounted into the container and the source uses the container-relative path.
102+
/// </summary>
103+
private static (string SyftSource, IList<string> AdditionalBinds) GetSyftSourceAndBinds(ImageReference imageReference)
104+
{
105+
switch (imageReference.Kind)
106+
{
107+
case ImageReferenceKind.DockerImage:
108+
return (imageReference.Reference, []);
109+
110+
case ImageReferenceKind.OciLayout:
111+
return (
112+
$"oci-dir:{LocalImageMountPoint}",
113+
[$"{imageReference.Reference}:{LocalImageMountPoint}:ro"]);
114+
115+
case ImageReferenceKind.OciArchive:
116+
{
117+
var dir = Path.GetDirectoryName(imageReference.Reference)
118+
?? throw new InvalidOperationException($"Could not determine parent directory for OCI archive path '{imageReference.Reference}'.");
119+
var fileName = Path.GetFileName(imageReference.Reference);
120+
return (
121+
$"oci-archive:{LocalImageMountPoint}/{fileName}",
122+
[$"{dir}:{LocalImageMountPoint}:ro"]);
123+
}
124+
125+
case ImageReferenceKind.DockerArchive:
126+
{
127+
var dir = Path.GetDirectoryName(imageReference.Reference)
128+
?? throw new InvalidOperationException($"Could not determine parent directory for Docker archive path '{imageReference.Reference}'.");
129+
var fileName = Path.GetFileName(imageReference.Reference);
130+
return (
131+
$"docker-archive:{LocalImageMountPoint}/{fileName}",
132+
[$"{dir}:{LocalImageMountPoint}:ro"]);
133+
}
134+
135+
default:
136+
throw new ArgumentOutOfRangeException(
137+
nameof(imageReference),
138+
$"Unsupported image reference kind '{imageReference.Kind}'.");
139+
}
140+
}
141+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace Microsoft.ComponentDetection.Detectors.Linux;
2+
3+
/// <summary>
4+
/// Factory for creating binary-based Syft runners.
5+
/// </summary>
6+
public interface IBinarySyftRunnerFactory
7+
{
8+
/// <summary>
9+
/// Creates a binary Syft runner configured to use the specified binary path.
10+
/// </summary>
11+
/// <param name="binaryPath">The path to the Syft binary.</param>
12+
/// <returns>An <see cref="ISyftRunner"/> that invokes the specified Syft binary.</returns>
13+
ISyftRunner Create(string binaryPath);
14+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Microsoft.ComponentDetection.Detectors.Linux;
2+
3+
/// <summary>
4+
/// Marker interface for the Docker-based Syft runner.
5+
/// Runs Syft by executing a Docker container with the Syft image.
6+
/// </summary>
7+
public interface IDockerSyftRunner : ISyftRunner
8+
{
9+
}

src/Microsoft.ComponentDetection.Detectors/linux/ILinuxScanner.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,35 +16,37 @@ public interface ILinuxScanner
1616
/// Scans a Linux container image for components and maps them to their respective layers.
1717
/// Runs Syft and processes the output in a single step.
1818
/// </summary>
19-
/// <param name="imageHash">The hash identifier of the container image to scan.</param>
19+
/// <param name="imageReference">The image reference to scan.</param>
2020
/// <param name="containerLayers">The collection of Docker layers that make up the container image.</param>
2121
/// <param name="baseImageLayerCount">The number of layers that belong to the base image, used to distinguish base image layers from application layers.</param>
2222
/// <param name="enabledComponentTypes">The set of component types to include in the scan results. Only components matching these types will be returned.</param>
2323
/// <param name="scope">The scope for scanning the image. See <see cref="LinuxScannerScope"/> for values.</param>
24+
/// <param name="syftRunner">The Syft runner to use for executing the scan.</param>
2425
/// <param name="cancellationToken">A token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None"/>.</param>
2526
/// <returns>A task that represents the asynchronous operation. The task result contains a collection of <see cref="LayerMappedLinuxComponents"/> representing the components found in the image and their associated layers.</returns>
2627
public Task<IEnumerable<LayerMappedLinuxComponents>> ScanLinuxAsync(
27-
string imageHash,
28+
ImageReference imageReference,
2829
IEnumerable<DockerLayer> containerLayers,
2930
int baseImageLayerCount,
3031
ISet<ComponentType> enabledComponentTypes,
3132
LinuxScannerScope scope,
33+
ISyftRunner syftRunner,
3234
CancellationToken cancellationToken = default
3335
);
3436

3537
/// <summary>
3638
/// Runs the Syft scanner and returns the raw parsed output without processing components.
3739
/// Use this when the caller needs access to the full Syft output (e.g., to extract source metadata for OCI images).
3840
/// </summary>
39-
/// <param name="syftSource">The source argument passed to Syft (e.g., an image hash or "oci-dir:/oci-image").</param>
40-
/// <param name="additionalBinds">Additional volume bind mounts for the Syft container (e.g., for mounting OCI directories).</param>
41+
/// <param name="imageReference">The image reference to scan.</param>
4142
/// <param name="scope">The scope for scanning the image.</param>
43+
/// <param name="syftRunner">The Syft runner to use for executing the scan.</param>
4244
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
4345
/// <returns>A task that represents the asynchronous operation. The task result contains the parsed <see cref="SyftOutput"/>.</returns>
4446
public Task<SyftOutput> GetSyftOutputAsync(
45-
string syftSource,
46-
IList<string> additionalBinds,
47+
ImageReference imageReference,
4748
LinuxScannerScope scope,
49+
ISyftRunner syftRunner,
4850
CancellationToken cancellationToken = default
4951
);
5052

0 commit comments

Comments
 (0)