diff --git a/Directory.Packages.props b/Directory.Packages.props index 2623d82..fe2305d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,8 +3,8 @@ true - - + + @@ -22,7 +22,7 @@ - + diff --git a/src/DockerUpdateGuard/Images/RuntimeContainerScanOrchestrator.cs b/src/DockerUpdateGuard/Images/RuntimeContainerScanOrchestrator.cs index 63d32be..0b943b1 100644 --- a/src/DockerUpdateGuard/Images/RuntimeContainerScanOrchestrator.cs +++ b/src/DockerUpdateGuard/Images/RuntimeContainerScanOrchestrator.cs @@ -934,7 +934,7 @@ private async Task CreateDerivedBaseRuntimeFindingAsync(RuntimeFindingContext fi var imageReferenceOrId = string.IsNullOrWhiteSpace(container.LocalImageId) ? container.ImageReference - : container.LocalImageId!; + : container.LocalImageId; var historyResult = await _dockerInstanceClient.GetImageHistoryAsync(configuredInstance, imageReferenceOrId, cancellationToken) @@ -1073,7 +1073,7 @@ private async Task CreateNginxRuntimeFindingAsync(RuntimeFindingContext findingC { var imageReferenceOrId = string.IsNullOrWhiteSpace(container.LocalImageId) ? container.ImageReference - : container.LocalImageId!; + : container.LocalImageId; var inspectResult = await _dockerInstanceClient.InspectImageAsync(configuredInstance, imageReferenceOrId, cancellationToken) diff --git a/src/DockerUpdateGuard/Portainer/PortainerClient.cs b/src/DockerUpdateGuard/Portainer/PortainerClient.cs index 8354975..7f5d627 100644 --- a/src/DockerUpdateGuard/Portainer/PortainerClient.cs +++ b/src/DockerUpdateGuard/Portainer/PortainerClient.cs @@ -116,7 +116,8 @@ public PortainerClient(IHttpClientFactory httpClientFactory, ILogger(cancellationToken: cancellationToken).ConfigureAwait(false); return containers?.FirstOrDefault(c => c.Names?.Any(n => string.Equals(n, $"/{containerName}", StringComparison.OrdinalIgnoreCase) - || string.Equals(n, containerName, StringComparison.OrdinalIgnoreCase)) == true)?.Id; + || string.Equals(n, containerName, StringComparison.OrdinalIgnoreCase)) == true) + ?.Id; } #endregion // Static methods @@ -149,7 +150,7 @@ private async Task CreateAuthenticatedClientAsync(PortainerOptions o if (string.IsNullOrWhiteSpace(options.Username) == false && string.IsNullOrWhiteSpace(options.Password) == false) { - var loginBody = new PortainerLoginRequest(options.Username!, options.Password!); + var loginBody = new PortainerLoginRequest(options.Username, options.Password); using var loginResponse = await client.PostAsJsonAsync("/api/auth", loginBody, cancellationToken).ConfigureAwait(false); if (loginResponse.IsSuccessStatusCode) diff --git a/src/Tests/DockerUpdateGuard.Tests/ImageScanOrchestratorTests.cs b/src/Tests/DockerUpdateGuard.Tests/ImageScanOrchestratorTests.cs index c5454c6..afcc5b3 100644 --- a/src/Tests/DockerUpdateGuard.Tests/ImageScanOrchestratorTests.cs +++ b/src/Tests/DockerUpdateGuard.Tests/ImageScanOrchestratorTests.cs @@ -1153,113 +1153,114 @@ await seedContext.SaveChangesAsync(TestContext.CancellationToken) var dbContext2 = database.CreateDbContext(); await using (dbContext1.ConfigureAwait(false)) - - await using (dbContext2.ConfigureAwait(false)) { - var imageCatalogRepository1 = new ImageCatalogRepository(dbContext1); - var imageCatalogRepository2 = new ImageCatalogRepository(dbContext2); - var imageReferenceParser = new ImageReferenceParser(); - var baseImageResolver = Substitute.For(); - var derivedBaseRuntimeDetector = Substitute.For(); - var dotNetReleaseMetadataService = Substitute.For(); - var nginxReleaseMetadataService = Substitute.For(); - var registryMetadataService = Substitute.For(); - var firstResolveStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var releaseFirstResolve = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var resolveCallCount = 0; - var baseImageDescriptors = ExternalOperationResult>.Succeeded([ - new BaseImageDescriptor - { - Registry = "docker.io", - Repository = "library/debian", - Tag = "12.0.0", - Digest = "sha256:base", - Depth = 1, - SourceReference = "FROM debian:12.0.0", - }, - ]); - - baseImageResolver.ResolveAsync(Arg.Any(), Arg.Any()) - .Returns(async _ => - { - var callIndex = Interlocked.Increment(ref resolveCallCount); - - if (callIndex == 1) + await using (dbContext2.ConfigureAwait(false)) + { + var imageCatalogRepository1 = new ImageCatalogRepository(dbContext1); + var imageCatalogRepository2 = new ImageCatalogRepository(dbContext2); + var imageReferenceParser = new ImageReferenceParser(); + var baseImageResolver = Substitute.For(); + var derivedBaseRuntimeDetector = Substitute.For(); + var dotNetReleaseMetadataService = Substitute.For(); + var nginxReleaseMetadataService = Substitute.For(); + var registryMetadataService = Substitute.For(); + var firstResolveStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var releaseFirstResolve = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var resolveCallCount = 0; + var baseImageDescriptors = ExternalOperationResult>.Succeeded([ + new BaseImageDescriptor + { + Registry = "docker.io", + Repository = "library/debian", + Tag = "12.0.0", + Digest = "sha256:base", + Depth = 1, + SourceReference = "FROM debian:12.0.0", + }, + ]); + + baseImageResolver.ResolveAsync(Arg.Any(), Arg.Any()) + .Returns(async _ => { - firstResolveStarted.TrySetResult(true); - await releaseFirstResolve.Task.ConfigureAwait(false); - } - - return baseImageDescriptors; - }); - registryMetadataService.GetTagAsync(Arg.Any(), cancellationToken: Arg.Any()) - .Returns(callInfo => - { - var imageReference = callInfo.ArgAt(0); + var callIndex = Interlocked.Increment(ref resolveCallCount); + + if (callIndex == 1) + { + firstResolveStarted.TrySetResult(true); + await releaseFirstResolve.Task.ConfigureAwait(false); + } + + return baseImageDescriptors; + }); + registryMetadataService.GetTagAsync(Arg.Any(), cancellationToken: Arg.Any()) + .Returns(callInfo => + { + var imageReference = callInfo.ArgAt(0); + + return ExternalOperationResult.Succeeded(new DockerHubTagData + { + Tag = imageReference.Tag, + Digest = imageReference.Digest, + PublishedAtUtc = new DateTimeOffset(2025, 06, 01, 12, 00, 00, TimeSpan.Zero), + }); + }); + registryMetadataService.GetImageConfigurationAsync(Arg.Any(), Arg.Any()) + .Returns(ExternalOperationResult.NotFound("No registry config")); + + var orchestrator1 = new ImageScanOrchestrator(new ApplicationTelemetry(), + baseImageResolver, + dbContext1, + derivedBaseRuntimeDetector, + dotNetReleaseMetadataService, + nginxReleaseMetadataService, + imageCatalogRepository1, + imageReferenceParser, + NullLogger.Instance, + registryMetadataService); + var orchestrator2 = new ImageScanOrchestrator(new ApplicationTelemetry(), + baseImageResolver, + dbContext2, + derivedBaseRuntimeDetector, + dotNetReleaseMetadataService, + nginxReleaseMetadataService, + imageCatalogRepository2, + imageReferenceParser, + NullLogger.Instance, + registryMetadataService); + var observedImageId = await dbContext1.ObservedImages.Select(entity => entity.Id) + .SingleAsync(TestContext.CancellationToken) + .ConfigureAwait(false); + + var firstScanTask = orchestrator1.ScanAsync(observedImageId, ScanTriggerSource.Scheduled, CancellationToken.None); + + await firstResolveStarted.Task.ConfigureAwait(false); + + var secondScanTask = orchestrator2.ScanAsync(observedImageId, ScanTriggerSource.Scheduled, CancellationToken.None); + + await Task.Delay(100, TestContext.CancellationToken).ConfigureAwait(false); - return ExternalOperationResult.Succeeded(new DockerHubTagData - { - Tag = imageReference.Tag, - Digest = imageReference.Digest, - PublishedAtUtc = new DateTimeOffset(2025, 06, 01, 12, 00, 00, TimeSpan.Zero), - }); - }); - registryMetadataService.GetImageConfigurationAsync(Arg.Any(), Arg.Any()) - .Returns(ExternalOperationResult.NotFound("No registry config")); - - var orchestrator1 = new ImageScanOrchestrator(new ApplicationTelemetry(), - baseImageResolver, - dbContext1, - derivedBaseRuntimeDetector, - dotNetReleaseMetadataService, - nginxReleaseMetadataService, - imageCatalogRepository1, - imageReferenceParser, - NullLogger.Instance, - registryMetadataService); - var orchestrator2 = new ImageScanOrchestrator(new ApplicationTelemetry(), - baseImageResolver, - dbContext2, - derivedBaseRuntimeDetector, - dotNetReleaseMetadataService, - nginxReleaseMetadataService, - imageCatalogRepository2, - imageReferenceParser, - NullLogger.Instance, - registryMetadataService); - var observedImageId = await dbContext1.ObservedImages.Select(entity => entity.Id) - .SingleAsync(TestContext.CancellationToken) - .ConfigureAwait(false); - - var firstScanTask = orchestrator1.ScanAsync(observedImageId, ScanTriggerSource.Scheduled, CancellationToken.None); - - await firstResolveStarted.Task.ConfigureAwait(false); - - var secondScanTask = orchestrator2.ScanAsync(observedImageId, ScanTriggerSource.Scheduled, CancellationToken.None); - - await Task.Delay(100, TestContext.CancellationToken).ConfigureAwait(false); - - Assert.AreEqual(1, - Volatile.Read(ref resolveCallCount), - "Concurrent scans for the same observed image must wait for the in-flight scan before starting base-image resolution"); + Assert.AreEqual(1, + Volatile.Read(ref resolveCallCount), + "Concurrent scans for the same observed image must wait for the in-flight scan before starting base-image resolution"); - releaseFirstResolve.SetResult(true); + releaseFirstResolve.SetResult(true); - await Task.WhenAll(firstScanTask, secondScanTask).ConfigureAwait(false); + await Task.WhenAll(firstScanTask, secondScanTask).ConfigureAwait(false); - var assertionContext = database.CreateDbContext(); + var assertionContext = database.CreateDbContext(); - await using (assertionContext.ConfigureAwait(false)) - { - var relationshipCount = await assertionContext.ImageRelationships.CountAsync(TestContext.CancellationToken).ConfigureAwait(false); - var scanRunCount = await assertionContext.ScanRuns.CountAsync(TestContext.CancellationToken).ConfigureAwait(false); + await using (assertionContext.ConfigureAwait(false)) + { + var relationshipCount = await assertionContext.ImageRelationships.CountAsync(TestContext.CancellationToken).ConfigureAwait(false); + var scanRunCount = await assertionContext.ScanRuns.CountAsync(TestContext.CancellationToken).ConfigureAwait(false); - Assert.AreEqual(1, - relationshipCount, - "Serialized scans must leave a single current base-image relationship for the observed image"); - Assert.AreEqual(2, - scanRunCount, - "Both serialized scans must complete and persist their scan runs"); + Assert.AreEqual(1, + relationshipCount, + "Serialized scans must leave a single current base-image relationship for the observed image"); + Assert.AreEqual(2, + scanRunCount, + "Both serialized scans must complete and persist their scan runs"); + } } } } diff --git a/src/Tests/DockerUpdateGuard.Tests/NavMenuTests.cs b/src/Tests/DockerUpdateGuard.Tests/NavMenuTests.cs index 6db541e..28b927b 100644 --- a/src/Tests/DockerUpdateGuard.Tests/NavMenuTests.cs +++ b/src/Tests/DockerUpdateGuard.Tests/NavMenuTests.cs @@ -136,7 +136,7 @@ public async Task NavMenuRendersVulnerabilitiesNavigationEntry() var vulnerabilitiesLink = component.FindAll("a").SingleOrDefault(link => link.TextContent.Trim() == "Vulnerabilities"); Assert.IsNotNull(vulnerabilitiesLink, "The Vulnerabilities navigation entry must always be rendered"); - Assert.IsTrue(vulnerabilitiesLink!.GetAttribute("href")?.Contains("vulnerabilities", StringComparison.OrdinalIgnoreCase), + Assert.IsTrue(vulnerabilitiesLink.GetAttribute("href")?.Contains("vulnerabilities", StringComparison.OrdinalIgnoreCase), "The Vulnerabilities navigation entry must link to the vulnerabilities page"); } }