diff --git a/docs/detectors/linux.md b/docs/detectors/linux.md
index d6acc11e4..87789f8a9 100644
--- a/docs/detectors/linux.md
+++ b/docs/detectors/linux.md
@@ -11,6 +11,20 @@ Linux detection depends on the following:
Linux package detection is performed by running [Syft](https://github.com/anchore/syft) and parsing the output.
The output contains the package name, version, and the layer of the container in which it was found.
+### Scanner Scope
+
+By default, this detector invokes Syft with the `all-layers` scanning scope (i.e. the Syft argument `--scope all-layers`).
+
+Syft has another scope, `squashed`, which can be used to scan only files accessible from the final layer of an image.
+
+The detector argument `Linux.ImageScanScope` can be used to configure this option as `squashed` or `all-layers` when invoking Component Detection.
+
+For example:
+
+```sh
+--DetectorArgs Linux.ImageScanScope=squashed
+```
+
## Known limitations
- Windows container scanning is not supported
diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/ILinuxScanner.cs b/src/Microsoft.ComponentDetection.Detectors/linux/ILinuxScanner.cs
index adbf67a9c..078211863 100644
--- a/src/Microsoft.ComponentDetection.Detectors/linux/ILinuxScanner.cs
+++ b/src/Microsoft.ComponentDetection.Detectors/linux/ILinuxScanner.cs
@@ -19,6 +19,7 @@ public interface ILinuxScanner
/// 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.
/// 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(
@@ -26,6 +27,7 @@ public Task> ScanLinuxAsync(
IEnumerable containerLayers,
int baseImageLayerCount,
ISet enabledComponentTypes,
+ LinuxScannerScope scope,
CancellationToken cancellationToken = default
);
}
diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/LinuxContainerDetector.cs b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxContainerDetector.cs
index fdf19a4bf..81ca7ac57 100644
--- a/src/Microsoft.ComponentDetection.Detectors/linux/LinuxContainerDetector.cs
+++ b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxContainerDetector.cs
@@ -28,6 +28,8 @@ ILogger logger
{
private const string TimeoutConfigKey = "Linux.ScanningTimeoutSec";
private const int DefaultTimeoutMinutes = 10;
+ private const string ScanScopeConfigKey = "Linux.ImageScanScope";
+ private const LinuxScannerScope DefaultScanScope = LinuxScannerScope.AllLayers;
private readonly ILinuxScanner linuxScanner = linuxScanner;
private readonly IDockerService dockerService = dockerService;
@@ -77,6 +79,8 @@ public async Task ExecuteDetectorAsync(
return EmptySuccessfulScan();
}
+ var scannerScope = GetScanScope(request.DetectorArgs);
+
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(GetTimeout(request.DetectorArgs));
@@ -96,6 +100,7 @@ public async Task ExecuteDetectorAsync(
results = await this.ProcessImagesAsync(
imagesToProcess,
request.ComponentRecorder,
+ scannerScope,
timeoutCts.Token
);
}
@@ -137,6 +142,26 @@ private static TimeSpan GetTimeout(IDictionary detectorArgs)
: defaultTimeout;
}
+ ///
+ /// Extracts and returns the scan scope from detector arguments.
+ ///
+ /// The arguments provided by the user.
+ /// The to use for scanning. Defaults to if not specified.
+ private static LinuxScannerScope GetScanScope(IDictionary detectorArgs)
+ {
+ if (detectorArgs == null || !detectorArgs.TryGetValue(ScanScopeConfigKey, out var scopeValue))
+ {
+ return DefaultScanScope;
+ }
+
+ return scopeValue?.ToUpperInvariant() switch
+ {
+ "ALL-LAYERS" => LinuxScannerScope.AllLayers,
+ "SQUASHED" => LinuxScannerScope.Squashed,
+ _ => DefaultScanScope,
+ };
+ }
+
private static IndividualDetectorScanResult EmptySuccessfulScan() =>
new() { ResultCode = ProcessingResultCode.Success };
@@ -179,6 +204,7 @@ private static void RecordImageDetectionFailure(Exception exception, string imag
private async Task> ProcessImagesAsync(
IEnumerable imagesToProcess,
IComponentRecorder componentRecorder,
+ LinuxScannerScope scannerScope,
CancellationToken cancellationToken = default
)
{
@@ -249,6 +275,7 @@ await this.dockerService.InspectImageAsync(image, cancellationToken)
internalContainerDetails.Layers,
baseImageLayerCount,
enabledComponentTypes,
+ scannerScope,
cancellationToken
);
diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/LinuxScanner.cs b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxScanner.cs
index 38817916c..f9bdd864a 100644
--- a/src/Microsoft.ComponentDetection.Detectors/linux/LinuxScanner.cs
+++ b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxScanner.cs
@@ -27,12 +27,14 @@ public class LinuxScanner : ILinuxScanner
private static readonly IList CmdParameters =
[
"--quiet",
- "--scope",
- "all-layers",
"--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(
@@ -96,6 +98,7 @@ public async Task> ScanLinuxAsync(
IEnumerable containerLayers,
int baseImageLayerCount,
ISet enabledComponentTypes,
+ LinuxScannerScope scope,
CancellationToken cancellationToken = default
)
{
@@ -109,6 +112,16 @@ public async Task> ScanLinuxAsync(
var stdout = string.Empty;
var stderr = string.Empty;
+ var scopeParameters = scope switch
+ {
+ LinuxScannerScope.AllLayers => ScopeAllLayersParameter,
+ LinuxScannerScope.Squashed => ScopeSquashedParameter,
+ _ => throw new ArgumentOutOfRangeException(
+ nameof(scope),
+ $"Unsupported scope value: {scope}"
+ ),
+ };
+
using var syftTelemetryRecord = new LinuxScannerSyftTelemetryRecord();
try
@@ -120,6 +133,7 @@ public async Task> ScanLinuxAsync(
{
var command = new List { imageHash }
.Concat(CmdParameters)
+ .Concat(scopeParameters)
.ToList();
(stdout, stderr) = await this.dockerService.CreateAndRunContainerAsync(
ScannerImage,
diff --git a/src/Microsoft.ComponentDetection.Detectors/linux/LinuxScannerScope.cs b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxScannerScope.cs
new file mode 100644
index 000000000..164bd71ee
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/linux/LinuxScannerScope.cs
@@ -0,0 +1,17 @@
+namespace Microsoft.ComponentDetection.Detectors.Linux;
+
+///
+/// Defines the scope for scanning Linux container images.
+///
+public enum LinuxScannerScope
+{
+ ///
+ /// Scan files from all layers of the image.
+ ///
+ AllLayers,
+
+ ///
+ /// Scan only the files accessible from the final layer of the image.
+ ///
+ Squashed,
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxContainerDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxContainerDetectorTests.cs
index 07aa3235d..472afa29c 100644
--- a/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxContainerDetectorTests.cs
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxContainerDetectorTests.cs
@@ -73,6 +73,7 @@ public LinuxContainerDetectorTests()
It.IsAny>(),
It.IsAny(),
It.IsAny>(),
+ It.IsAny(),
It.IsAny()
)
)
@@ -277,6 +278,7 @@ public async Task TestLinuxContainerDetector_SameImagePassedMultipleTimesAsync()
It.IsAny>(),
It.IsAny(),
It.IsAny>(),
+ It.IsAny(),
It.IsAny()
),
Times.Once
@@ -307,6 +309,48 @@ public async Task TestLinuxContainerDetector_TimeoutParameterSpecifiedAsync()
await action.Should().NotThrowAsync();
}
+ [TestMethod]
+ [DataRow("all-layers", LinuxScannerScope.AllLayers)]
+ [DataRow("squashed", LinuxScannerScope.Squashed)]
+ [DataRow("ALL-LAYERS", LinuxScannerScope.AllLayers)]
+ [DataRow("SQUASHED", LinuxScannerScope.Squashed)]
+ [DataRow(null, LinuxScannerScope.AllLayers)] // Test default behavior
+ [DataRow("", LinuxScannerScope.AllLayers)] // Test empty string default
+ [DataRow("invalid-value", LinuxScannerScope.AllLayers)] // Test invalid input defaults to AllLayers
+ public async Task TestLinuxContainerDetector_ImageScanScopeParameterSpecifiedAsync(string scopeValue, LinuxScannerScope expectedScope)
+ {
+ var detectorArgs = new Dictionary { { "Linux.ImageScanScope", scopeValue } };
+ var scanRequest = new ScanRequest(
+ new DirectoryInfo(Path.GetTempPath()),
+ (_, __) => false,
+ this.mockLogger.Object,
+ detectorArgs,
+ [NodeLatestImage],
+ new ComponentRecorder()
+ );
+
+ var linuxContainerDetector = new LinuxContainerDetector(
+ this.mockSyftLinuxScanner.Object,
+ this.mockDockerService.Object,
+ this.mockLinuxContainerDetectorLogger.Object
+ );
+
+ await linuxContainerDetector.ExecuteDetectorAsync(scanRequest);
+
+ this.mockSyftLinuxScanner.Verify(
+ scanner =>
+ scanner.ScanLinuxAsync(
+ It.IsAny(),
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny>(),
+ expectedScope,
+ It.IsAny()
+ ),
+ Times.Once
+ );
+ }
+
[TestMethod]
public async Task TestLinuxContainerDetector_HandlesScratchBaseAsync()
{
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxScannerTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxScannerTests.cs
index eaa2c2bb4..6a1e98515 100644
--- a/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxScannerTests.cs
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/LinuxScannerTests.cs
@@ -1,6 +1,7 @@
#nullable disable
namespace Microsoft.ComponentDetection.Detectors.Tests;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -288,7 +289,8 @@ await this.linuxScanner.ScanLinuxAsync(
},
],
0,
- enabledTypes
+ enabledTypes,
+ LinuxScannerScope.AllLayers
)
)
.First()
@@ -335,7 +337,8 @@ await this.linuxScanner.ScanLinuxAsync(
},
],
0,
- enabledTypes
+ enabledTypes,
+ LinuxScannerScope.AllLayers
)
)
.First()
@@ -384,7 +387,8 @@ await this.linuxScanner.ScanLinuxAsync(
},
],
0,
- enabledTypes
+ enabledTypes,
+ LinuxScannerScope.AllLayers
)
)
.First()
@@ -433,7 +437,8 @@ await this.linuxScanner.ScanLinuxAsync(
},
],
0,
- enabledTypes
+ enabledTypes,
+ LinuxScannerScope.AllLayers
)
)
.First()
@@ -522,7 +527,8 @@ public async Task TestLinuxScanner_SupportsMultipleComponentTypes_Async()
new DockerLayer { LayerIndex = 1, DiffId = "sha256:layer2" },
],
0,
- enabledTypes
+ enabledTypes,
+ LinuxScannerScope.AllLayers
);
var allComponents = layers.SelectMany(l => l.Components).ToList();
@@ -622,7 +628,8 @@ public async Task TestLinuxScanner_FiltersComponentsByEnabledTypes_OnlyLinux_Asy
new DockerLayer { LayerIndex = 1, DiffId = "sha256:layer2" },
],
0,
- enabledTypes
+ enabledTypes,
+ LinuxScannerScope.AllLayers
);
var allComponents = layers.SelectMany(l => l.Components).ToList();
@@ -707,7 +714,8 @@ public async Task TestLinuxScanner_FiltersComponentsByEnabledTypes_OnlyNpmAndPip
new DockerLayer { LayerIndex = 1, DiffId = "sha256:layer2" },
],
0,
- enabledTypes
+ enabledTypes,
+ LinuxScannerScope.AllLayers
);
var allComponents = layers.SelectMany(l => l.Components).ToList();
@@ -722,4 +730,61 @@ public async Task TestLinuxScanner_FiltersComponentsByEnabledTypes_OnlyNpmAndPip
var pipComponent = allComponents.OfType().Single();
pipComponent.Name.Should().Be("requests");
}
+
+ [TestMethod]
+ [DataRow(LinuxScannerScope.AllLayers, "all-layers")]
+ [DataRow(LinuxScannerScope.Squashed, "squashed")]
+ public async Task TestLinuxScanner_ScopeParameter_IncludesCorrectFlagAsync(
+ LinuxScannerScope scope,
+ string expectedFlag
+ )
+ {
+ this.mockDockerService.Setup(service =>
+ service.CreateAndRunContainerAsync(
+ It.IsAny(),
+ It.IsAny>(),
+ It.IsAny()
+ )
+ )
+ .ReturnsAsync((SyftOutputNoAuthorOrLicense, string.Empty));
+
+ var enabledTypes = new HashSet { ComponentType.Linux };
+ await this.linuxScanner.ScanLinuxAsync(
+ "fake_hash",
+ [new DockerLayer { LayerIndex = 0, DiffId = "sha256:layer1" }],
+ 0,
+ enabledTypes,
+ scope
+ );
+
+ this.mockDockerService.Verify(
+ service =>
+ service.CreateAndRunContainerAsync(
+ It.IsAny(),
+ It.Is>(cmd =>
+ cmd.Contains("--scope") && cmd.Contains(expectedFlag)
+ ),
+ It.IsAny()
+ ),
+ Times.Once
+ );
+ }
+
+ [TestMethod]
+ public async Task TestLinuxScanner_InvalidScopeParameter_ThrowsArgumentOutOfRangeExceptionAsync()
+ {
+ var enabledTypes = new HashSet { ComponentType.Linux };
+ var invalidScope = (LinuxScannerScope)999; // Invalid enum value
+
+ Func action = async () =>
+ await this.linuxScanner.ScanLinuxAsync(
+ "fake_hash",
+ [new DockerLayer { LayerIndex = 0, DiffId = "sha256:layer1" }],
+ 0,
+ enabledTypes,
+ invalidScope
+ );
+
+ await action.Should().ThrowAsync();
+ }
}