Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/detectors/linux.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ public interface ILinuxScanner
/// <param name="containerLayers">The collection of Docker layers that make up the container image.</param>
/// <param name="baseImageLayerCount">The number of layers that belong to the base image, used to distinguish base image layers from application layers.</param>
/// <param name="enabledComponentTypes">The set of component types to include in the scan results. Only components matching these types will be returned.</param>
/// <param name="scope">The scope for scanning the image. See <see cref="LinuxScannerScope"/> for values.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None"/>.</param>
/// <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>
public Task<IEnumerable<LayerMappedLinuxComponents>> ScanLinuxAsync(
string imageHash,
IEnumerable<DockerLayer> containerLayers,
int baseImageLayerCount,
ISet<ComponentType> enabledComponentTypes,
LinuxScannerScope scope,
CancellationToken cancellationToken = default
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ ILogger<LinuxContainerDetector> 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;
Expand Down Expand Up @@ -77,6 +79,8 @@ public async Task<IndividualDetectorScanResult> ExecuteDetectorAsync(
return EmptySuccessfulScan();
}

var scannerScope = GetScanScope(request.DetectorArgs);

using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(GetTimeout(request.DetectorArgs));

Expand All @@ -96,6 +100,7 @@ public async Task<IndividualDetectorScanResult> ExecuteDetectorAsync(
results = await this.ProcessImagesAsync(
imagesToProcess,
request.ComponentRecorder,
scannerScope,
timeoutCts.Token
);
}
Expand Down Expand Up @@ -137,6 +142,26 @@ private static TimeSpan GetTimeout(IDictionary<string, string> detectorArgs)
: defaultTimeout;
}

/// <summary>
/// Extracts and returns the scan scope from detector arguments.
/// </summary>
/// <param name="detectorArgs">The arguments provided by the user.</param>
/// <returns>The <see cref="LinuxScannerScope"/> to use for scanning. Defaults to <see cref="DefaultScanScope"/> if not specified.</returns>
private static LinuxScannerScope GetScanScope(IDictionary<string, string> 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 };

Expand Down Expand Up @@ -179,6 +204,7 @@ private static void RecordImageDetectionFailure(Exception exception, string imag
private async Task<IEnumerable<ImageScanningResult>> ProcessImagesAsync(
IEnumerable<string> imagesToProcess,
IComponentRecorder componentRecorder,
LinuxScannerScope scannerScope,
CancellationToken cancellationToken = default
)
{
Expand Down Expand Up @@ -249,6 +275,7 @@ await this.dockerService.InspectImageAsync(image, cancellationToken)
internalContainerDetails.Layers,
baseImageLayerCount,
enabledComponentTypes,
scannerScope,
cancellationToken
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ public class LinuxScanner : ILinuxScanner
private static readonly IList<string> CmdParameters =
[
"--quiet",
"--scope",
"all-layers",
"--output",
"json",
];

private static readonly IList<string> ScopeAllLayersParameter = ["--scope", "all-layers"];

private static readonly IList<string> ScopeSquashedParameter = ["--scope", "squashed"];

private static readonly SemaphoreSlim ContainerSemaphore = new SemaphoreSlim(2);

private static readonly int SemaphoreTimeout = Convert.ToInt32(
Expand Down Expand Up @@ -96,6 +98,7 @@ public async Task<IEnumerable<LayerMappedLinuxComponents>> ScanLinuxAsync(
IEnumerable<DockerLayer> containerLayers,
int baseImageLayerCount,
ISet<ComponentType> enabledComponentTypes,
LinuxScannerScope scope,
CancellationToken cancellationToken = default
)
{
Expand All @@ -109,6 +112,16 @@ public async Task<IEnumerable<LayerMappedLinuxComponents>> 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
Expand All @@ -120,6 +133,7 @@ public async Task<IEnumerable<LayerMappedLinuxComponents>> ScanLinuxAsync(
{
var command = new List<string> { imageHash }
.Concat(CmdParameters)
.Concat(scopeParameters)
.ToList();
(stdout, stderr) = await this.dockerService.CreateAndRunContainerAsync(
ScannerImage,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Microsoft.ComponentDetection.Detectors.Linux;

/// <summary>
/// Defines the scope for scanning Linux container images.
/// </summary>
public enum LinuxScannerScope
{
/// <summary>
/// Scan files from all layers of the image.
/// </summary>
AllLayers,

/// <summary>
/// Scan only the files accessible from the final layer of the image.
/// </summary>
Squashed,
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public LinuxContainerDetectorTests()
It.IsAny<IEnumerable<DockerLayer>>(),
It.IsAny<int>(),
It.IsAny<ISet<ComponentType>>(),
It.IsAny<LinuxScannerScope>(),
It.IsAny<CancellationToken>()
)
)
Expand Down Expand Up @@ -277,6 +278,7 @@ public async Task TestLinuxContainerDetector_SameImagePassedMultipleTimesAsync()
It.IsAny<IEnumerable<DockerLayer>>(),
It.IsAny<int>(),
It.IsAny<ISet<ComponentType>>(),
It.IsAny<LinuxScannerScope>(),
It.IsAny<CancellationToken>()
),
Times.Once
Expand Down Expand Up @@ -307,6 +309,48 @@ public async Task TestLinuxContainerDetector_TimeoutParameterSpecifiedAsync()
await action.Should().NotThrowAsync<OperationCanceledException>();
}

[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<string, string> { { "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<string>(),
It.IsAny<IEnumerable<DockerLayer>>(),
It.IsAny<int>(),
It.IsAny<ISet<ComponentType>>(),
expectedScope,
It.IsAny<CancellationToken>()
),
Times.Once
);
}

[TestMethod]
public async Task TestLinuxContainerDetector_HandlesScratchBaseAsync()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#nullable disable
namespace Microsoft.ComponentDetection.Detectors.Tests;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
Expand Down Expand Up @@ -288,7 +289,8 @@ await this.linuxScanner.ScanLinuxAsync(
},
],
0,
enabledTypes
enabledTypes,
LinuxScannerScope.AllLayers
)
)
.First()
Expand Down Expand Up @@ -335,7 +337,8 @@ await this.linuxScanner.ScanLinuxAsync(
},
],
0,
enabledTypes
enabledTypes,
LinuxScannerScope.AllLayers
)
)
.First()
Expand Down Expand Up @@ -384,7 +387,8 @@ await this.linuxScanner.ScanLinuxAsync(
},
],
0,
enabledTypes
enabledTypes,
LinuxScannerScope.AllLayers
)
)
.First()
Expand Down Expand Up @@ -433,7 +437,8 @@ await this.linuxScanner.ScanLinuxAsync(
},
],
0,
enabledTypes
enabledTypes,
LinuxScannerScope.AllLayers
)
)
.First()
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand All @@ -722,4 +730,61 @@ public async Task TestLinuxScanner_FiltersComponentsByEnabledTypes_OnlyNpmAndPip
var pipComponent = allComponents.OfType<PipComponent>().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<string>(),
It.IsAny<List<string>>(),
It.IsAny<CancellationToken>()
)
)
.ReturnsAsync((SyftOutputNoAuthorOrLicense, string.Empty));

var enabledTypes = new HashSet<ComponentType> { 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<string>(),
It.Is<List<string>>(cmd =>
cmd.Contains("--scope") && cmd.Contains(expectedFlag)
),
It.IsAny<CancellationToken>()
),
Times.Once
);
}

[TestMethod]
public async Task TestLinuxScanner_InvalidScopeParameter_ThrowsArgumentOutOfRangeExceptionAsync()
{
var enabledTypes = new HashSet<ComponentType> { ComponentType.Linux };
var invalidScope = (LinuxScannerScope)999; // Invalid enum value

Func<Task> action = async () =>
await this.linuxScanner.ScanLinuxAsync(
"fake_hash",
[new DockerLayer { LayerIndex = 0, DiffId = "sha256:layer1" }],
0,
enabledTypes,
invalidScope
);

await action.Should().ThrowAsync<ArgumentOutOfRangeException>();
}
}
Loading