From 0d18df753c32afa8af462485417437bd45545d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Tue, 14 Nov 2023 01:40:40 +0100 Subject: [PATCH 001/129] Adding basics of installing ZAP from Docker --- Lombiq.Tests.UI.Samples/Readme.md | 1 + .../Tests/SecurityScanningTests.cs | 26 ++++++++++ .../SecurityScanning/ZapManager.cs | 51 +++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs create mode 100644 Lombiq.Tests.UI/SecurityScanning/ZapManager.cs diff --git a/Lombiq.Tests.UI.Samples/Readme.md b/Lombiq.Tests.UI.Samples/Readme.md index 15a4ac422..4babb9526 100644 --- a/Lombiq.Tests.UI.Samples/Readme.md +++ b/Lombiq.Tests.UI.Samples/Readme.md @@ -27,6 +27,7 @@ For general details about and on using the Toolbox see the [root Readme](../Read - [Basic visual verification tests](Tests/BasicVisualVerificationTests.cs) - [Testing in tenants](Tests/TenantTests.cs) - [Interactive mode](Tests/InteractiveModeTests.cs) +- [Security scanning](Tests/SecurityScanningTests.cs) ## Adding new tutorials diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs new file mode 100644 index 000000000..08b8e3c23 --- /dev/null +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -0,0 +1,26 @@ +using Lombiq.Tests.UI.Attributes; +using Lombiq.Tests.UI.SecurityScanning; +using Lombiq.Tests.UI.Services; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Lombiq.Tests.UI.Samples.Tests; + +public class SecurityScanningTests : UITestBase +{ + public SecurityScanningTests(ITestOutputHelper testOutputHelper) + : base(testOutputHelper) + { + } + + [Theory, Chrome] + public Task SecurityScanShouldPass(Browser browser) => + ExecuteTestAfterSetupAsync( + async context => + { + var zapManager = new ZapManager(); + await zapManager.StartAsync(); + }, + browser); +} diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs new file mode 100644 index 000000000..ee0357e22 --- /dev/null +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -0,0 +1,51 @@ +using Lombiq.HelpfulLibraries.Cli; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Lombiq.Tests.UI.SecurityScanning; + +public sealed class ZapManager : IAsyncDisposable +{ + private static readonly SemaphoreSlim _restoreSemaphore = new(1, 1); + private static readonly CliProgram _docker = new("docker"); + + private static bool _wasPulled; + + private CancellationTokenSource _cancellationTokenSource; + + public async Task StartAsync() + { + _cancellationTokenSource = new CancellationTokenSource(); + var token = _cancellationTokenSource.Token; + + try + { + await _restoreSemaphore.WaitAsync(token); + + if (!_wasPulled) + { + // Need to use the weekly release because that's the one that has packaged scans migrated to Automation + // Framework. + await _docker.ExecuteAsync(token, "pull", "softwaresecurityproject/zap-weekly:20231113"); + + _wasPulled = true; + } + } + finally + { + _restoreSemaphore.Release(); + } + } + + public ValueTask DisposeAsync() + { + if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested) + { + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.Dispose(); + } + + return ValueTask.CompletedTask; + } +} From 338811b83d66621a2fdd2bc0e11bf75821607b65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Tue, 14 Nov 2023 03:05:30 +0100 Subject: [PATCH 002/129] Adding ZAP baseline scan run as a prototype --- .../Tests/SecurityScanningTests.cs | 2 +- .../SecurityScanning/ZapManager.cs | 40 ++++++++++++++++--- Lombiq.Tests.UI/Services/SmtpService.cs | 2 +- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 08b8e3c23..49438c529 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -20,7 +20,7 @@ public Task SecurityScanShouldPass(Browser browser) => async context => { var zapManager = new ZapManager(); - await zapManager.StartAsync(); + await zapManager.StartInstanceAsync(); }, browser); } diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index ee0357e22..685801c01 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -7,6 +7,9 @@ namespace Lombiq.Tests.UI.SecurityScanning; public sealed class ZapManager : IAsyncDisposable { + // Need to use the weekly release because that's the one that has packaged scans migrated to Automation Framework. + private const string _zapImage = "softwaresecurityproject/zap-weekly:20231113"; + private static readonly SemaphoreSlim _restoreSemaphore = new(1, 1); private static readonly CliProgram _docker = new("docker"); @@ -14,8 +17,13 @@ public sealed class ZapManager : IAsyncDisposable private CancellationTokenSource _cancellationTokenSource; - public async Task StartAsync() + public async Task StartInstanceAsync() { + if (_cancellationTokenSource != null) + { + throw new InvalidOperationException("The ZAP instance was already started."); + } + _cancellationTokenSource = new CancellationTokenSource(); var token = _cancellationTokenSource.Token; @@ -25,10 +33,7 @@ public async Task StartAsync() if (!_wasPulled) { - // Need to use the weekly release because that's the one that has packaged scans migrated to Automation - // Framework. - await _docker.ExecuteAsync(token, "pull", "softwaresecurityproject/zap-weekly:20231113"); - + await _docker.ExecuteAsync(token, "pull", _zapImage); _wasPulled = true; } } @@ -36,6 +41,31 @@ public async Task StartAsync() { _restoreSemaphore.Release(); } + + // Explanation on the arguments: + // - --add-host: Lets us connect to the host OS's localhost, where the OC app runs, with https://localhost. See + // https://stackoverflow.com/a/24326540/220230. --network="host" serves the same, but only works under Linux. + // See https://docs.docker.com/engine/reference/commandline/run/#network and + // https://docs.docker.com/network/drivers/host/. + + // Running a ZAP desktop in the browser with Webswing with the same config: +#pragma warning disable S103 // Lines should not be too long + // docker run --add-host localhost:host-gateway -u zap -p 8080:8080 -p 8090:8090 -i softwaresecurityproject/zap-weekly:20231113 zap-webswing.sh +#pragma warning restore S103 // Lines should not be too long + + var result = await _docker.ExecuteAndGetOutputAsync( + new object[] + { + "run", + "--add-host", + "localhost:host-gateway", + _zapImage, + "zap-baseline.py", + "-t", + "https://localhost:44335/", + }, + additionalExceptionText: null, + token); } public ValueTask DisposeAsync() diff --git a/Lombiq.Tests.UI/Services/SmtpService.cs b/Lombiq.Tests.UI/Services/SmtpService.cs index 166176861..d198932c8 100644 --- a/Lombiq.Tests.UI/Services/SmtpService.cs +++ b/Lombiq.Tests.UI/Services/SmtpService.cs @@ -110,7 +110,7 @@ public async Task StartAsync() // An empty db parameter means an in-memory DB. For all possible command line arguments see: // https://github.com/rnwood/smtp4dev/blob/master/Rnwood.Smtp4dev/Program.cs#L132. await CliProgram.DotNet.GetCommand( - new object[] { "tool", "run", "smtp4dev", "--db", string.Empty, "--smtpport", _smtpPort, "--urls", webUIUri }) + "tool", "run", "smtp4dev", "--db", string.Empty, "--smtpport", _smtpPort, "--urls", webUIUri) .ExecuteDotNetApplicationAsync( stdErr => throw new IOException( From 32f9b549137d5ec57617c3cd8a2311beede7a82f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Tue, 14 Nov 2023 13:57:54 +0100 Subject: [PATCH 003/129] Inserting SecurityScanningTests into the sample walkthrough --- Lombiq.Tests.UI.Samples/Tests/InteractiveModeTests.cs | 1 + Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Lombiq.Tests.UI.Samples/Tests/InteractiveModeTests.cs b/Lombiq.Tests.UI.Samples/Tests/InteractiveModeTests.cs index bba0b7399..3d376e045 100644 --- a/Lombiq.Tests.UI.Samples/Tests/InteractiveModeTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/InteractiveModeTests.cs @@ -74,3 +74,4 @@ await Task.WhenAll( } // END OF TRAINING SECTION: Interactive mode. +// NEXT STATION: Head over to Tests/SecurityScanningTests.cs. diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 49438c529..aa203cb65 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -24,3 +24,5 @@ public Task SecurityScanShouldPass(Browser browser) => }, browser); } + +// END OF TRAINING SECTION: Security scanning. From 2d288e3485172409effe97f020dc9dd1ef4e641d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Tue, 14 Nov 2023 14:33:30 +0100 Subject: [PATCH 004/129] Interactive mode docs and making internally used methods internal --- .../ShortcutsUITestContextExtensions.cs | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs index 3d09bb55b..e2a6f0478 100644 --- a/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs @@ -590,12 +590,26 @@ await UsingScopeAsync( return eventUrl; } + /// + /// Switches to an interactive mode where control from the test is handed over and you can use the web app as an + /// ordinary user from the browser or access its web APIs. To switch back to the test, click the button + /// that'll be displayed in the browser, or call open . + /// + public static async Task SwitchToInteractiveAsync(this UITestContext context) + { + await context.EnterInteractiveModeAsync(); + await context.WaitInteractiveModeAsync(); + + context.Driver.Close(); + context.SwitchToLastWindow(); + } + /// /// Opens a new tab with the /// page. Visiting this page enables the interactive mode flag so it can be awaited with the extension method. /// - public static Task EnterInteractiveModeAsync(this UITestContext context) + internal static Task EnterInteractiveModeAsync(this UITestContext context) { context.Driver.SwitchTo().NewWindow(WindowType.Tab); context.Driver.SwitchTo().Window(context.Driver.WindowHandles[^1]); @@ -607,7 +621,7 @@ public static Task EnterInteractiveModeAsync(this UITestContext context) /// Periodically polls the and waits half a second if it's /// . /// - public static async Task WaitInteractiveModeAsync(this UITestContext context) + internal static async Task WaitInteractiveModeAsync(this UITestContext context) { var client = context.GetApi(); while (await client.IsInteractiveModeEnabledAsync()) @@ -616,15 +630,6 @@ public static async Task WaitInteractiveModeAsync(this UITestContext context) } } - public static async Task SwitchToInteractiveAsync(this UITestContext context) - { - await context.EnterInteractiveModeAsync(); - await context.WaitInteractiveModeAsync(); - - context.Driver.Close(); - context.SwitchToLastWindow(); - } - private static bool IsAdminTheme(IManifestInfo manifest) => manifest.Tags.Any(tag => tag.EqualsOrdinalIgnoreCase(ManifestConstants.AdminTag)); From 290aa3da56f2d4b58fa7bba53c02e498243d4b2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Tue, 14 Nov 2023 18:53:27 +0100 Subject: [PATCH 005/129] Running ZAP scans for the current base URL --- Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs | 4 +++- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index aa203cb65..59a637b1a 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -19,8 +19,10 @@ public Task SecurityScanShouldPass(Browser browser) => ExecuteTestAfterSetupAsync( async context => { + //await context.SwitchToInteractiveAsync(); var zapManager = new ZapManager(); - await zapManager.StartInstanceAsync(); + //await zapManager.StartInstanceAsync("https://localhost:44335/"); + await zapManager.StartInstanceAsync(context.Scope.BaseUri); }, browser); } diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 685801c01..71e23846a 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -17,7 +17,7 @@ public sealed class ZapManager : IAsyncDisposable private CancellationTokenSource _cancellationTokenSource; - public async Task StartInstanceAsync() + public async Task StartInstanceAsync(Uri startUri) { if (_cancellationTokenSource != null) { @@ -62,7 +62,7 @@ public async Task StartInstanceAsync() _zapImage, "zap-baseline.py", "-t", - "https://localhost:44335/", + startUri.ToString(), }, additionalExceptionText: null, token); From 62c42339e188c4c8fe36ad4f693f92d1a121f433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Tue, 14 Nov 2023 19:13:07 +0100 Subject: [PATCH 006/129] Temporarily not running retries --- Lombiq.Tests.UI.Samples/UITestBase.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lombiq.Tests.UI.Samples/UITestBase.cs b/Lombiq.Tests.UI.Samples/UITestBase.cs index 3fe59f397..4510e4aa5 100644 --- a/Lombiq.Tests.UI.Samples/UITestBase.cs +++ b/Lombiq.Tests.UI.Samples/UITestBase.cs @@ -42,6 +42,8 @@ protected override Task ExecuteTestAsync( setupOperation, async configuration => { + configuration.MaxRetryCount = 0; + // You should always set the window size of the browser, otherwise the size will be random based on the // settings of the given machine. However this is already handled as long as the // context.Configuration.BrowserConfiguration.DefaultBrowserSize option is properly set. You can change From ed318326758c1cdecdce79e10d3a38dceb2efc75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Tue, 14 Nov 2023 19:19:38 +0100 Subject: [PATCH 007/129] Fixing ZAP network access under Linux --- .../SecurityScanning/ZapManager.cs | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 71e23846a..f2afc2404 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -1,5 +1,7 @@ using Lombiq.HelpfulLibraries.Cli; using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -42,10 +44,10 @@ public async Task StartInstanceAsync(Uri startUri) _restoreSemaphore.Release(); } - // Explanation on the arguments: - // - --add-host: Lets us connect to the host OS's localhost, where the OC app runs, with https://localhost. See - // https://stackoverflow.com/a/24326540/220230. --network="host" serves the same, but only works under Linux. - // See https://docs.docker.com/engine/reference/commandline/run/#network and + // Explanation on the arguments used below: + // - --add-host and --network host: Lets us connect to the host OS's localhost, where the OC app runs, with + // https://localhost. See https://stackoverflow.com/a/24326540/220230. --network host serves the same, but + // only works under Linux. See https://docs.docker.com/engine/reference/commandline/run/#network and // https://docs.docker.com/network/drivers/host/. // Running a ZAP desktop in the browser with Webswing with the same config: @@ -53,19 +55,28 @@ public async Task StartInstanceAsync(Uri startUri) // docker run --add-host localhost:host-gateway -u zap -p 8080:8080 -p 8090:8090 -i softwaresecurityproject/zap-weekly:20231113 zap-webswing.sh #pragma warning restore S103 // Lines should not be too long - var result = await _docker.ExecuteAndGetOutputAsync( - new object[] - { - "run", - "--add-host", - "localhost:host-gateway", - _zapImage, - "zap-baseline.py", - "-t", - startUri.ToString(), - }, - additionalExceptionText: null, - token); + var cliParameters = new List { "run" }; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + cliParameters.Add("--network"); + cliParameters.Add("host"); + } + else + { + cliParameters.Add("--add-host"); + cliParameters.Add("localhost:host-gateway"); + } + + cliParameters.AddRange(new object[] + { + _zapImage, + "zap-baseline.py", + "-t", + startUri.ToString(), + }); + + var result = await _docker.ExecuteAndGetOutputAsync(cliParameters, additionalExceptionText: null, token); } public ValueTask DisposeAsync() From 8af3f0d1d99338b1ec70fbbe354816f030d728b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Tue, 14 Nov 2023 21:34:31 +0100 Subject: [PATCH 008/129] Managing the ZapManager instance centrally --- .../Tests/SecurityScanningTests.cs | 4 +- .../SecurityScanning/ZapManager.cs | 53 ++++++++++--------- Lombiq.Tests.UI/Services/UITestContext.cs | 11 +++- .../Services/UITestExecutionSession.cs | 8 ++- 4 files changed, 47 insertions(+), 29 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 59a637b1a..6104ce8c7 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -1,5 +1,4 @@ using Lombiq.Tests.UI.Attributes; -using Lombiq.Tests.UI.SecurityScanning; using Lombiq.Tests.UI.Services; using System.Threading.Tasks; using Xunit; @@ -20,9 +19,8 @@ public Task SecurityScanShouldPass(Browser browser) => async context => { //await context.SwitchToInteractiveAsync(); - var zapManager = new ZapManager(); //await zapManager.StartInstanceAsync("https://localhost:44335/"); - await zapManager.StartInstanceAsync(context.Scope.BaseUri); + await context.ZapManager.RunSecurityScanAsync(context.Scope.BaseUri); }, browser); } diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index f2afc2404..87190e14e 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -19,30 +19,9 @@ public sealed class ZapManager : IAsyncDisposable private CancellationTokenSource _cancellationTokenSource; - public async Task StartInstanceAsync(Uri startUri) + public async Task RunSecurityScanAsync(Uri startUri) { - if (_cancellationTokenSource != null) - { - throw new InvalidOperationException("The ZAP instance was already started."); - } - - _cancellationTokenSource = new CancellationTokenSource(); - var token = _cancellationTokenSource.Token; - - try - { - await _restoreSemaphore.WaitAsync(token); - - if (!_wasPulled) - { - await _docker.ExecuteAsync(token, "pull", _zapImage); - _wasPulled = true; - } - } - finally - { - _restoreSemaphore.Release(); - } + await EnsureInitializedAsync(); // Explanation on the arguments used below: // - --add-host and --network host: Lets us connect to the host OS's localhost, where the OC app runs, with @@ -76,7 +55,10 @@ public async Task StartInstanceAsync(Uri startUri) startUri.ToString(), }); - var result = await _docker.ExecuteAndGetOutputAsync(cliParameters, additionalExceptionText: null, token); + var result = await _docker.ExecuteAndGetOutputAsync( + cliParameters, + additionalExceptionText: null, + _cancellationTokenSource.Token); } public ValueTask DisposeAsync() @@ -89,4 +71,27 @@ public ValueTask DisposeAsync() return ValueTask.CompletedTask; } + + private async Task EnsureInitializedAsync() + { + if (_cancellationTokenSource != null) return; + + _cancellationTokenSource = new CancellationTokenSource(); + var token = _cancellationTokenSource.Token; + + try + { + await _restoreSemaphore.WaitAsync(token); + + if (!_wasPulled) + { + await _docker.ExecuteAsync(token, "pull", _zapImage); + _wasPulled = true; + } + } + finally + { + _restoreSemaphore.Release(); + } + } } diff --git a/Lombiq.Tests.UI/Services/UITestContext.cs b/Lombiq.Tests.UI/Services/UITestContext.cs index e9806ec4b..5e51039ea 100644 --- a/Lombiq.Tests.UI/Services/UITestContext.cs +++ b/Lombiq.Tests.UI/Services/UITestContext.cs @@ -1,6 +1,7 @@ using Lombiq.Tests.UI.Exceptions; using Lombiq.Tests.UI.Extensions; using Lombiq.Tests.UI.Models; +using Lombiq.Tests.UI.SecurityScanning; using OpenQA.Selenium; using System; using System.Collections.Generic; @@ -61,6 +62,12 @@ public class UITestContext /// public AzureBlobStorageRunningContext AzureBlobStorageRunningContext { get; } + /// + /// Gets the service to manage Zed Attack Proxy instances for security scanning. Usually, it's recommended to use + /// the ZAP extension methods instead. + /// + public ZapManager ZapManager { get; } + /// /// Gets a cumulative log of browser console entries. /// @@ -105,7 +112,8 @@ public UITestContext( OrchardCoreUITestExecutorConfiguration configuration, IWebApplicationInstance application, AtataScope scope, - RunningContextContainer runningContextContainer) + RunningContextContainer runningContextContainer, + ZapManager zapManager) { Id = id; TestManifest = testManifest; @@ -115,6 +123,7 @@ public UITestContext( Scope = scope; SmtpServiceRunningContext = runningContextContainer.SmtpServiceRunningContext; AzureBlobStorageRunningContext = runningContextContainer.AzureBlobStorageRunningContext; + ZapManager = zapManager; } /// diff --git a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs index c66926f65..2be6bb0ac 100644 --- a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs +++ b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs @@ -6,6 +6,7 @@ using Lombiq.Tests.UI.Extensions; using Lombiq.Tests.UI.Helpers; using Lombiq.Tests.UI.Models; +using Lombiq.Tests.UI.SecurityScanning; using Lombiq.Tests.UI.Services.GitHub; using Microsoft.VisualBasic.FileIO; using Mono.Unix; @@ -39,6 +40,7 @@ internal sealed class UITestExecutionSession : IAsyncDisposable private SqlServerManager _sqlServerManager; private SmtpService _smtpService; private AzureBlobStorageManager _azureBlobStorageManager; + private ZapManager _zapManager; private IWebApplicationInstance _applicationInstance; private UITestContext _context; private DockerConfiguration _dockerConfiguration; @@ -178,6 +180,7 @@ private async ValueTask ShutdownAsync() if (_smtpService != null) await _smtpService.DisposeAsync(); if (_azureBlobStorageManager != null) await _azureBlobStorageManager.DisposeAsync(); + if (_zapManager != null) await _zapManager.DisposeAsync(); _screenshotCount = 0; @@ -584,6 +587,8 @@ private async Task CreateContextAsync() if (_configuration.UseAzureBlobStorage) azureBlobStorageContext = await SetUpAzureBlobStorageAsync(); if (_configuration.UseSmtpService) smtpContext = await StartSmtpServiceAsync(); + _zapManager = new ZapManager(); + Task UITestingBeforeAppStartHandlerAsync(string contentRootPath, InstanceCommandLineArgumentsBuilder arguments) { _configuration.OrchardCoreConfiguration.BeforeAppStart -= UITestingBeforeAppStartHandlerAsync; @@ -640,7 +645,8 @@ Task UITestingBeforeAppStartHandlerAsync(string contentRootPath, InstanceCommand _configuration, _applicationInstance, atataScope, - new RunningContextContainer(sqlServerContext, smtpContext, azureBlobStorageContext)); + new RunningContextContainer(sqlServerContext, smtpContext, azureBlobStorageContext), + _zapManager); } private string GetSetupHashCode() => From 19cc147ccad5ee07f57fadcf5191d6cb28c3dced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Tue, 14 Nov 2023 21:35:38 +0100 Subject: [PATCH 009/129] Redirecting ZAP output to the test output --- .../Tests/SecurityScanningTests.cs | 2 +- .../SecurityScanning/ZapManager.cs | 23 +++++++++++++++---- .../Services/UITestExecutionSession.cs | 2 +- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 6104ce8c7..395efcad7 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -20,7 +20,7 @@ public Task SecurityScanShouldPass(Browser browser) => { //await context.SwitchToInteractiveAsync(); //await zapManager.StartInstanceAsync("https://localhost:44335/"); - await context.ZapManager.RunSecurityScanAsync(context.Scope.BaseUri); + await context.ZapManager.RunSecurityScanAsync(context, context.Scope.BaseUri); }, browser); } diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 87190e14e..7c9568de8 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -1,9 +1,13 @@ +using CliWrap; using Lombiq.HelpfulLibraries.Cli; +using Lombiq.Tests.UI.Services; using System; using System.Collections.Generic; using System.Runtime.InteropServices; +using System.Text; using System.Threading; using System.Threading.Tasks; +using Xunit.Abstractions; namespace Lombiq.Tests.UI.SecurityScanning; @@ -15,11 +19,15 @@ public sealed class ZapManager : IAsyncDisposable private static readonly SemaphoreSlim _restoreSemaphore = new(1, 1); private static readonly CliProgram _docker = new("docker"); + private readonly ITestOutputHelper _testOutputHelper; + private static bool _wasPulled; private CancellationTokenSource _cancellationTokenSource; - public async Task RunSecurityScanAsync(Uri startUri) + internal ZapManager(ITestOutputHelper testOutputHelper) => _testOutputHelper = testOutputHelper; + + public async Task RunSecurityScanAsync(UITestContext context, Uri startUri) { await EnsureInitializedAsync(); @@ -55,10 +63,15 @@ public async Task RunSecurityScanAsync(Uri startUri) startUri.ToString(), }); - var result = await _docker.ExecuteAndGetOutputAsync( - cliParameters, - additionalExceptionText: null, - _cancellationTokenSource.Token); + var stdErrBuffer = new StringBuilder(); + + var result = await _docker + .GetCommand(cliParameters) + .WithStandardOutputPipe(PipeTarget.ToDelegate(line => _testOutputHelper.WriteLineTimestampedAndDebug(line))) + .WithStandardErrorPipe(PipeTarget.ToStringBuilder(stdErrBuffer)) + // This is so no exception is thrown by CliWrap if the exit code is not 0. + .WithValidation(CommandResultValidation.None) + .ExecuteAsync(_cancellationTokenSource.Token); } public ValueTask DisposeAsync() diff --git a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs index 2be6bb0ac..9e60a6cc0 100644 --- a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs +++ b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs @@ -587,7 +587,7 @@ private async Task CreateContextAsync() if (_configuration.UseAzureBlobStorage) azureBlobStorageContext = await SetUpAzureBlobStorageAsync(); if (_configuration.UseSmtpService) smtpContext = await StartSmtpServiceAsync(); - _zapManager = new ZapManager(); + _zapManager = new ZapManager(_testOutputHelper); Task UITestingBeforeAppStartHandlerAsync(string contentRootPath, InstanceCommandLineArgumentsBuilder arguments) { From f174c2f0f7631ef99127a90362f5a1a5173f37e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Tue, 14 Nov 2023 21:50:02 +0100 Subject: [PATCH 010/129] Making SecurityScanShouldPass pass with simplified setup --- .../Tests/SecurityScanningTests.cs | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 395efcad7..5851c1847 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -1,5 +1,9 @@ using Lombiq.Tests.UI.Attributes; +using Lombiq.Tests.UI.Extensions; +using Lombiq.Tests.UI.Pages; using Lombiq.Tests.UI.Services; +using OpenQA.Selenium; +using System; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -18,11 +22,39 @@ public Task SecurityScanShouldPass(Browser browser) => ExecuteTestAfterSetupAsync( async context => { - //await context.SwitchToInteractiveAsync(); - //await zapManager.StartInstanceAsync("https://localhost:44335/"); await context.ZapManager.RunSecurityScanAsync(context, context.Scope.BaseUri); }, browser); + + // Overriding the default setup so we can have a simpler site, simplifying the security scan for the purpose of this + // demo. + protected override Task ExecuteTestAfterSetupAsync( + Func testAsync, + Browser browser, + Func changeConfigurationAsync) => + ExecuteTestAsync( + testAsync, + browser, + async context => + { + var homepageUri = await context.GoToSetupPageAndSetupOrchardCoreAsync( + new OrchardCoreSetupParameters(context) + { + SiteName = "Lombiq's OSOCE - UI Testing", + RecipeId = "ComingSoon", + TablePrefix = "OSOCE", + SiteTimeZoneValue = "Europe/Budapest", + }); + + context.Exists(By.ClassName("masthead-content")); + + return homepageUri; + }, + async configuration => + { + configuration.HtmlValidationConfiguration.RunHtmlValidationAssertionOnAllPageChanges = false; + await changeConfigurationAsync(configuration); + }); } // END OF TRAINING SECTION: Security scanning. From d0ba4a8f1209e04df6c8985a51a8b53f5d40e575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 15 Nov 2023 00:49:43 +0100 Subject: [PATCH 011/129] Docs --- Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs | 2 ++ Lombiq.Tests.UI/Docs/CreatingTests.md | 1 + Lombiq.Tests.UI/Docs/Tools.md | 1 + 3 files changed, 4 insertions(+) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 5851c1847..b663571d6 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -10,6 +10,8 @@ namespace Lombiq.Tests.UI.Samples.Tests; +// Note that security scanning has cross-platform support, but due to the limitations of virtualization under Windows in +// GitHub Actions, these tests won't work there. public class SecurityScanningTests : UITestBase { public SecurityScanningTests(ITestOutputHelper testOutputHelper) diff --git a/Lombiq.Tests.UI/Docs/CreatingTests.md b/Lombiq.Tests.UI/Docs/CreatingTests.md index d9f31a1ac..4871da817 100644 --- a/Lombiq.Tests.UI/Docs/CreatingTests.md +++ b/Lombiq.Tests.UI/Docs/CreatingTests.md @@ -14,6 +14,7 @@ We also recommend always running some highly automated tests that need very litt - The suite of tests for checking that all the basic Orchard Core features work, like login, registration, and content management. Use `context.TestBasicOrchardFeatures()` to run all such tests but see the other, more granular tests too. This is also demonstrated in `Lombiq.Tests.UI.Samples` and in [this video](https://www.youtube.com/watch?v=jmhq63sRZrI). - [Monkey tests](https://en.wikipedia.org/wiki/Monkey_testing) can also be useful. Use `context.TestCurrentPageAsMonkeyRecursively()` to run a monkey testing process. This walks through site pages and does random interactions with pages, like clicking, scrolling, form filling, etc. It's recommended to have at least 3 monkey tests that execute with different user states: As an admin, as a regular registered user and as an anonymous user. The admin test can start execution on admin dashboard page, while other tests can start on home page. This is also demonstrated in `Lombiq.Tests.UI.Samples` and in [this video](https://www.youtube.com/watch?v=pZbEsEz3tuE). +- Security scans with [Zed Attack Proxy (ZAP)](https://www.zaproxy.org/). `Lombiq.Tests.UI.Samples` contains a demonstration of how to use it. ## Steps for creating a test class diff --git a/Lombiq.Tests.UI/Docs/Tools.md b/Lombiq.Tests.UI/Docs/Tools.md index 6fbdd0fc2..e0b87edc6 100644 --- a/Lombiq.Tests.UI/Docs/Tools.md +++ b/Lombiq.Tests.UI/Docs/Tools.md @@ -14,3 +14,4 @@ - Monkey testing is implemented using [Gremlins.js](https://github.com/marmelab/gremlins.js/) library. - Visual verification is implemented using [ImageSharpCompare](https://github.com/Codeuctivity/ImageSharp.Compare). - [Ben.Demystifier](https://github.com/benaadams/Ben.Demystifier) is used to simplify stack traces, mainly around async methods. +- Security scans are done with [Zed Attack Proxy (ZAP)](https://www.zaproxy.org/). From dd3ff19d430e05ee1672b80136d865e7ed34aba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 15 Nov 2023 01:22:20 +0100 Subject: [PATCH 012/129] Basics of using YAML config files --- .../Tests/SecurityScanningTests.cs | 2 +- Lombiq.Tests.UI/Lombiq.Tests.UI.csproj | 18 +++++ .../AutomationFrameworkYamls/Baseline.yml | 73 ++++++++++++++++++ .../AutomationFrameworkYamls/FullScan.yml | 74 +++++++++++++++++++ .../AutomationFrameworkYamls/GraphQL.yml | 61 +++++++++++++++ .../AutomationFrameworkYamls/OpenAPI.yml | 52 +++++++++++++ .../SecurityScanning/ZapManager.cs | 42 +++++++++-- 7 files changed, 315 insertions(+), 7 deletions(-) create mode 100644 Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml create mode 100644 Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml create mode 100644 Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml create mode 100644 Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index b663571d6..5fe9d9c03 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -11,7 +11,7 @@ namespace Lombiq.Tests.UI.Samples.Tests; // Note that security scanning has cross-platform support, but due to the limitations of virtualization under Windows in -// GitHub Actions, these tests won't work there. +// GitHub Actions, these tests won't work there. They'll work on a Windows desktop though. public class SecurityScanningTests : UITestBase { public SecurityScanningTests(ITestOutputHelper testOutputHelper) diff --git a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj index 1e4e5c038..6cad5ae8b 100644 --- a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj +++ b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj @@ -44,6 +44,18 @@ PreserveNewest true + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest true @@ -113,5 +125,11 @@ + + + + + + diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml new file mode 100644 index 000000000..f0e9dc90f --- /dev/null +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml @@ -0,0 +1,73 @@ +--- +env: + contexts: + - name: "Default Context" + urls: + - "https://localhost:44335/" + includePaths: + - "https://localhost:44335/.*" + excludePaths: [] + authentication: + parameters: {} + verification: + method: "response" + pollFrequency: 60 + pollUnits: "requests" + sessionManagement: + method: "cookie" + parameters: {} + technology: + exclude: [] + parameters: + failOnError: true + failOnWarning: false + progressToStdout: true + vars: {} +jobs: +- parameters: + scanOnlyInScope: true + enableTags: false + disableAllRules: false + rules: [] + name: "passiveScan-config" + type: "passiveScan-config" +- parameters: {} + name: "spider" + type: "spider" + tests: + - onFail: "INFO" + statistic: "automation.spider.urls.added" + site: "" + operator: ">=" + value: 100 + name: "At least 100 URLs found" + type: "stats" +- parameters: + context: "" + user: "" + url: "" + maxDuration: 60 + maxCrawlDepth: 10 + numberOfBrowsers: 64 + inScopeOnly: true + name: "spiderAjax" + type: "spiderAjax" + tests: + - onFail: "INFO" + statistic: "spiderAjax.urls.added" + site: "" + operator: ">=" + value: 100 + name: "At least 100 URLs found" + type: "stats" +- parameters: {} + name: "passiveScan-wait" + type: "passiveScan-wait" +- parameters: + template: "risk-confidence-html" + reportDir: "/zap/wrk/reports" + reportTitle: "ZAP Scanning Report" + reportDescription: "" + name: "report" + type: "report" + diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml new file mode 100644 index 000000000..89d65aa46 --- /dev/null +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml @@ -0,0 +1,74 @@ +--- +env: + contexts: + - name: "Default Context" + urls: + - "https://localhost:44335/" + includePaths: [] + excludePaths: [] + authentication: + parameters: {} + verification: + method: "response" + pollFrequency: 60 + pollUnits: "requests" + sessionManagement: + method: "cookie" + parameters: {} + technology: + exclude: [] + parameters: + failOnError: true + failOnWarning: false + progressToStdout: true + vars: {} +jobs: +- parameters: + scanOnlyInScope: true + enableTags: false + disableAllRules: false + rules: [] + name: "passiveScan-config" + type: "passiveScan-config" +- parameters: {} + name: "spider" + type: "spider" + tests: + - onFail: "INFO" + statistic: "automation.spider.urls.added" + site: "" + operator: ">=" + value: 100 + name: "At least 100 URLs found" + type: "stats" +- parameters: + maxDuration: 60 + maxCrawlDepth: 10 + numberOfBrowsers: 64 + inScopeOnly: true + name: "spiderAjax" + type: "spiderAjax" + tests: + - onFail: "INFO" + statistic: "spiderAjax.urls.added" + site: "" + operator: ">=" + value: 100 + name: "At least 100 URLs found" + type: "stats" +- parameters: {} + name: "passiveScan-wait" + type: "passiveScan-wait" +- parameters: {} + policyDefinition: + rules: [] + name: "activeScan" + type: "activeScan" +- parameters: + template: "risk-confidence-html" + reportDir: "/zap/wrk/reports" + reportTitle: "ZAP Scanning Report" + reportDescription: "" + name: "report" + type: "report" + diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml new file mode 100644 index 000000000..62939c8a5 --- /dev/null +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml @@ -0,0 +1,61 @@ +--- +env: + contexts: + - name: "Default Context" + urls: + - "https://localhost:44335/" + includePaths: + - "https://localhost:44335/.*" + excludePaths: [] + authentication: + parameters: {} + verification: + method: "response" + pollFrequency: 60 + pollUnits: "requests" + sessionManagement: + method: "cookie" + parameters: {} + technology: + exclude: [] + parameters: + failOnError: true + failOnWarning: false + progressToStdout: true + vars: {} +jobs: +- parameters: + scanOnlyInScope: true + enableTags: false + disableAllRules: false + rules: [] + name: "passiveScan-config" + type: "passiveScan-config" +- parameters: + queryGenEnabled: true + maxQueryDepth: 5 + lenientMaxQueryDepthEnabled: true + maxAdditionalQueryDepth: 5 + maxArgsDepth: 5 + optionalArgsEnabled: true + argsType: "both" + querySplitType: "leaf" + requestMethod: "post_json" + name: "graphql" + type: "graphql" +- parameters: {} + name: "passiveScan-wait" + type: "passiveScan-wait" +- parameters: {} + policyDefinition: + rules: [] + name: "activeScan" + type: "activeScan" +- parameters: + template: "risk-confidence-html" + reportDir: "/zap/wrk/reports" + reportTitle: "ZAP Scanning Report" + reportDescription: "" + name: "report" + type: "report" + diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml new file mode 100644 index 000000000..5a2fa9c81 --- /dev/null +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml @@ -0,0 +1,52 @@ +--- +env: + contexts: + - name: "Default Context" + urls: + - "https://localhost:44335/" + includePaths: + - "https://localhost:44335/.*" + excludePaths: [] + authentication: + parameters: {} + verification: + method: "response" + pollFrequency: 60 + pollUnits: "requests" + sessionManagement: + method: "cookie" + parameters: {} + technology: + exclude: [] + parameters: + failOnError: true + failOnWarning: false + progressToStdout: true + vars: {} +jobs: +- parameters: + scanOnlyInScope: true + enableTags: false + disableAllRules: false + rules: [] + name: "passiveScan-config" + type: "passiveScan-config" +- parameters: {} + name: "openapi" + type: "openapi" +- parameters: {} + name: "passiveScan-wait" + type: "passiveScan-wait" +- parameters: {} + policyDefinition: + rules: [] + name: "activeScan" + type: "activeScan" +- parameters: + template: "risk-confidence-html" + reportDir: "/zap/wrk/reports" + reportTitle: "ZAP Scanning Report" + reportDescription: "" + name: "report" + type: "report" + diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 7c9568de8..aee769299 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -1,8 +1,10 @@ using CliWrap; using Lombiq.HelpfulLibraries.Cli; +using Lombiq.Tests.UI.Constants; using Lombiq.Tests.UI.Services; using System; using System.Collections.Generic; +using System.IO; using System.Runtime.InteropServices; using System.Text; using System.Threading; @@ -27,21 +29,45 @@ public sealed class ZapManager : IAsyncDisposable internal ZapManager(ITestOutputHelper testOutputHelper) => _testOutputHelper = testOutputHelper; - public async Task RunSecurityScanAsync(UITestContext context, Uri startUri) + public async Task RunSecurityScanAsync( + UITestContext context, + Uri startUri, + string automationFrameworkYamlPath = null) { await EnsureInitializedAsync(); - // Explanation on the arguments used below: + if (string.IsNullOrEmpty(automationFrameworkYamlPath)) + { + automationFrameworkYamlPath = Path.Combine("SecurityScanning", "AutomationFrameworkYamls", "Baseline.yml"); + } + + // Explanation on the CLI arguments used below: // - --add-host and --network host: Lets us connect to the host OS's localhost, where the OC app runs, with // https://localhost. See https://stackoverflow.com/a/24326540/220230. --network host serves the same, but // only works under Linux. See https://docs.docker.com/engine/reference/commandline/run/#network and // https://docs.docker.com/network/drivers/host/. + // - --volume: Mounts the given host folder as a volume under the given container path. This is to pass files + // back and forth between the host and the container. + // - --tty: Allocates a pseudo-teletypewriter, i.e. redirects the output of ZAP to the CLI's output. + // - zap.sh: The entry point of ZAP. Everything that comes after this is executed in the container. + + // Also see https://www.zaproxy.org/docs/docker/about/#automation-framework. - // Running a ZAP desktop in the browser with Webswing with the same config: + // Running a ZAP desktop in the browser with Webswing with the same config under Windows: #pragma warning disable S103 // Lines should not be too long // docker run --add-host localhost:host-gateway -u zap -p 8080:8080 -p 8090:8090 -i softwaresecurityproject/zap-weekly:20231113 zap-webswing.sh #pragma warning restore S103 // Lines should not be too long + var mountedDirectoryPath = DirectoryPaths.GetTempSubDirectoryPath(context.Id, "Zap"); + Directory.CreateDirectory(mountedDirectoryPath); + + var yamlFileName = Path.GetFileName(automationFrameworkYamlPath); + + File.Copy( + automationFrameworkYamlPath, + Path.Combine(mountedDirectoryPath, yamlFileName), + overwrite: true); + var cliParameters = new List { "run" }; if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) @@ -57,10 +83,14 @@ public async Task RunSecurityScanAsync(UITestContext context, Uri startUri) cliParameters.AddRange(new object[] { + "--volume", + mountedDirectoryPath + ":/zap/wrk/:rw", + "--tty", _zapImage, - "zap-baseline.py", - "-t", - startUri.ToString(), + "zap.sh", + "-cmd", + "-autorun", + "/zap/wrk/" + yamlFileName, }); var stdErrBuffer = new StringBuilder(); From 8c703cc9a54a673b8ac4073f7a2cd479984d81de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 15 Nov 2023 02:14:16 +0100 Subject: [PATCH 013/129] Adding support for YAML config files --- Lombiq.Tests.UI/Lombiq.Tests.UI.csproj | 1 + .../AutomationFrameworkYamls/Baseline.yml | 4 +- .../AutomationFrameworkYamls/FullScan.yml | 2 +- .../AutomationFrameworkYamls/GraphQL.yml | 4 +- .../AutomationFrameworkYamls/OpenAPI.yml | 4 +- .../SecurityScanning/ZapManager.cs | 44 ++++++++++++++++--- 6 files changed, 45 insertions(+), 14 deletions(-) diff --git a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj index 6cad5ae8b..3bc64d1c0 100644 --- a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj +++ b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj @@ -86,6 +86,7 @@ + diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml index f0e9dc90f..276b3608c 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml @@ -3,9 +3,9 @@ env: contexts: - name: "Default Context" urls: - - "https://localhost:44335/" + - "" includePaths: - - "https://localhost:44335/.*" + - ".*" excludePaths: [] authentication: parameters: {} diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml index 89d65aa46..d1d7a0eba 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml @@ -3,7 +3,7 @@ env: contexts: - name: "Default Context" urls: - - "https://localhost:44335/" + - "" includePaths: [] excludePaths: [] authentication: diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml index 62939c8a5..7b876b6c0 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml @@ -3,9 +3,9 @@ env: contexts: - name: "Default Context" urls: - - "https://localhost:44335/" + - "" includePaths: - - "https://localhost:44335/.*" + - ".*" excludePaths: [] authentication: parameters: {} diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml index 5a2fa9c81..a9cd48558 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml @@ -3,9 +3,9 @@ env: contexts: - name: "Default Context" urls: - - "https://localhost:44335/" + - "" includePaths: - - "https://localhost:44335/.*" + - ".*" excludePaths: [] authentication: parameters: {} diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index aee769299..b4e04f738 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -16,9 +16,11 @@ namespace Lombiq.Tests.UI.SecurityScanning; public sealed class ZapManager : IAsyncDisposable { // Need to use the weekly release because that's the one that has packaged scans migrated to Automation Framework. + // When updating this version, also regenerate the Automation Framework YAML config files so we don't miss any + // changes to those. private const string _zapImage = "softwaresecurityproject/zap-weekly:20231113"; - private static readonly SemaphoreSlim _restoreSemaphore = new(1, 1); + private static readonly SemaphoreSlim _pullSemaphore = new(1, 1); private static readonly CliProgram _docker = new("docker"); private readonly ITestOutputHelper _testOutputHelper; @@ -29,6 +31,15 @@ public sealed class ZapManager : IAsyncDisposable internal ZapManager(ITestOutputHelper testOutputHelper) => _testOutputHelper = testOutputHelper; + /// + /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against the app. + /// + /// The of the currently executing test. + /// The under the app where to start the scan from. + /// + /// File system path to the YAML configuration file of ZAP's Automationat Framework. See + /// for details. + /// public async Task RunSecurityScanAsync( UITestContext context, Uri startUri, @@ -62,11 +73,11 @@ public async Task RunSecurityScanAsync( Directory.CreateDirectory(mountedDirectoryPath); var yamlFileName = Path.GetFileName(automationFrameworkYamlPath); + var yamlFileCopyPath = Path.Combine(mountedDirectoryPath, yamlFileName); - File.Copy( - automationFrameworkYamlPath, - Path.Combine(mountedDirectoryPath, yamlFileName), - overwrite: true); + File.Copy(automationFrameworkYamlPath, yamlFileCopyPath, overwrite: true); + + await PrepareYamlAsync(yamlFileCopyPath, startUri); var cliParameters = new List { "run" }; @@ -124,7 +135,7 @@ private async Task EnsureInitializedAsync() try { - await _restoreSemaphore.WaitAsync(token); + await _pullSemaphore.WaitAsync(token); if (!_wasPulled) { @@ -134,7 +145,26 @@ private async Task EnsureInitializedAsync() } finally { - _restoreSemaphore.Release(); + _pullSemaphore.Release(); } } + + private async Task PrepareYamlAsync(string yamlFilePath, Uri startUri) + { + var yaml = await File.ReadAllTextAsync(yamlFilePath, _cancellationTokenSource.Token); + + // Setting URLs: + yaml = yaml.Replace("", startUri.ToString()); + + //var deserializer = new DeserializerBuilder().Build(); + + //// Deseralizing into a free-form object, not to potentially break unknown fields during reserialization. + //dynamic configuration = deserializer.Deserialize(yaml); + + + //var contexts = configuration["env"]["contexts"]; + //contexts["urls"] + + await File.WriteAllTextAsync(yamlFilePath, yaml, _cancellationTokenSource.Token); + } } From 1c6667afdd37d8531c0815e117f7fe0a8844f88f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 15 Nov 2023 02:19:56 +0100 Subject: [PATCH 014/129] Running spiderAjax only for "modern" apps --- .../SecurityScanning/AutomationFrameworkYamls/Baseline.yml | 4 +--- .../SecurityScanning/AutomationFrameworkYamls/FullScan.yml | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml index 276b3608c..c18c72414 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml @@ -43,13 +43,11 @@ jobs: name: "At least 100 URLs found" type: "stats" - parameters: - context: "" - user: "" - url: "" maxDuration: 60 maxCrawlDepth: 10 numberOfBrowsers: 64 inScopeOnly: true + runOnlyIfModern: true name: "spiderAjax" type: "spiderAjax" tests: diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml index d1d7a0eba..37d6249a8 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml @@ -46,6 +46,7 @@ jobs: maxCrawlDepth: 10 numberOfBrowsers: 64 inScopeOnly: true + runOnlyIfModern: true name: "spiderAjax" type: "spiderAjax" tests: From 540dc9e8e861fd57bed03b2f888818dcdd2c807d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 15 Nov 2023 03:44:20 +0100 Subject: [PATCH 015/129] Switching to the Blog recipe since ZAP has a bug with Coming Soon --- Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 5fe9d9c03..3ae427d4f 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -43,12 +43,12 @@ protected override Task ExecuteTestAfterSetupAsync( new OrchardCoreSetupParameters(context) { SiteName = "Lombiq's OSOCE - UI Testing", - RecipeId = "ComingSoon", + RecipeId = "Blog", TablePrefix = "OSOCE", SiteTimeZoneValue = "Europe/Budapest", }); - context.Exists(By.ClassName("masthead-content")); + context.Exists(By.ClassName("site-heading")); return homepageUri; }, From 51f429df931a8ad5caf5b2bd81cf2479d1574e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 15 Nov 2023 03:59:37 +0100 Subject: [PATCH 016/129] Link to ZAP bug report --- Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 3ae427d4f..f2c2eb183 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -43,6 +43,8 @@ protected override Task ExecuteTestAfterSetupAsync( new OrchardCoreSetupParameters(context) { SiteName = "Lombiq's OSOCE - UI Testing", + // We can't use the even simpler Coming Soon recipe due to this ZAP bug: + // https://github.com/zaproxy/zaproxy/issues/8191. RecipeId = "Blog", TablePrefix = "OSOCE", SiteTimeZoneValue = "Europe/Budapest", From 06d55607a668866aea8592500318dce80798017e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 15 Nov 2023 15:50:41 +0100 Subject: [PATCH 017/129] Centralizing YAML paths --- .../AutomationFrameworkYamlPaths.cs | 13 +++++++++++++ Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamlPaths.cs diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamlPaths.cs b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamlPaths.cs new file mode 100644 index 000000000..f31bdba52 --- /dev/null +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamlPaths.cs @@ -0,0 +1,13 @@ +using System.IO; + +namespace Lombiq.Tests.UI.SecurityScanning; + +public static class AutomationFrameworkYamlPaths +{ + private static readonly string AutomationFrameworkYamlsPath = Path.Combine("SecurityScanning", "AutomationFrameworkYamls"); + + public static readonly string BaselineYamlPath = Path.Combine(AutomationFrameworkYamlsPath, "Baseline.yml"); + public static readonly string FullScanYamlPath = Path.Combine(AutomationFrameworkYamlsPath, "FullScan.yml"); + public static readonly string GraphQLYamlPath = Path.Combine(AutomationFrameworkYamlsPath, "GraphQL.yml"); + public static readonly string OpenAPIYamlPath = Path.Combine(AutomationFrameworkYamlsPath, "OpenAPI.yml"); +} diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index b4e04f738..a9445aaf4 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -49,7 +49,7 @@ public async Task RunSecurityScanAsync( if (string.IsNullOrEmpty(automationFrameworkYamlPath)) { - automationFrameworkYamlPath = Path.Combine("SecurityScanning", "AutomationFrameworkYamls", "Baseline.yml"); + automationFrameworkYamlPath = AutomationFrameworkYamlPaths.BaselineYamlPath; } // Explanation on the CLI arguments used below: From 4fa49b987b8b39fa42eb51077af2e48961aab30b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 15 Nov 2023 18:17:50 +0100 Subject: [PATCH 018/129] More flexible YML manipulation --- .../AutomationFrameworkYamls/Baseline.yml | 2 - .../AutomationFrameworkYamls/FullScan.yml | 1 - .../AutomationFrameworkYamls/GraphQL.yml | 2 - .../AutomationFrameworkYamls/OpenAPI.yml | 2 - .../SecurityScanning/ZapManager.cs | 73 ++++++++++++++----- 5 files changed, 53 insertions(+), 27 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml index c18c72414..f6dfde001 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml @@ -4,8 +4,6 @@ env: - name: "Default Context" urls: - "" - includePaths: - - ".*" excludePaths: [] authentication: parameters: {} diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml index 37d6249a8..8760037a5 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml @@ -4,7 +4,6 @@ env: - name: "Default Context" urls: - "" - includePaths: [] excludePaths: [] authentication: parameters: {} diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml index 7b876b6c0..c53c8f3a5 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml @@ -4,8 +4,6 @@ env: - name: "Default Context" urls: - "" - includePaths: - - ".*" excludePaths: [] authentication: parameters: {} diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml index a9cd48558..2caeb326a 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml @@ -4,8 +4,6 @@ env: - name: "Default Context" urls: - "" - includePaths: - - ".*" excludePaths: [] authentication: parameters: {} diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index a9445aaf4..19101322d 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -5,11 +5,13 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; using Xunit.Abstractions; +using YamlDotNet.Serialization; namespace Lombiq.Tests.UI.SecurityScanning; @@ -37,7 +39,7 @@ public sealed class ZapManager : IAsyncDisposable /// The of the currently executing test. /// The under the app where to start the scan from. /// - /// File system path to the YAML configuration file of ZAP's Automationat Framework. See + /// File system path to the YAML configuration file of ZAP's Automation Framework. See /// for details. /// public async Task RunSecurityScanAsync( @@ -52,6 +54,16 @@ public async Task RunSecurityScanAsync( automationFrameworkYamlPath = AutomationFrameworkYamlPaths.BaselineYamlPath; } + var mountedDirectoryPath = DirectoryPaths.GetTempSubDirectoryPath(context.Id, "Zap"); + Directory.CreateDirectory(mountedDirectoryPath); + + var yamlFileName = Path.GetFileName(automationFrameworkYamlPath); + var yamlFileCopyPath = Path.Combine(mountedDirectoryPath, yamlFileName); + + File.Copy(automationFrameworkYamlPath, yamlFileCopyPath, overwrite: true); + + await PrepareYamlAsync(yamlFileCopyPath, startUri); + // Explanation on the CLI arguments used below: // - --add-host and --network host: Lets us connect to the host OS's localhost, where the OC app runs, with // https://localhost. See https://stackoverflow.com/a/24326540/220230. --network host serves the same, but @@ -69,16 +81,6 @@ public async Task RunSecurityScanAsync( // docker run --add-host localhost:host-gateway -u zap -p 8080:8080 -p 8090:8090 -i softwaresecurityproject/zap-weekly:20231113 zap-webswing.sh #pragma warning restore S103 // Lines should not be too long - var mountedDirectoryPath = DirectoryPaths.GetTempSubDirectoryPath(context.Id, "Zap"); - Directory.CreateDirectory(mountedDirectoryPath); - - var yamlFileName = Path.GetFileName(automationFrameworkYamlPath); - var yamlFileCopyPath = Path.Combine(mountedDirectoryPath, yamlFileName); - - File.Copy(automationFrameworkYamlPath, yamlFileCopyPath, overwrite: true); - - await PrepareYamlAsync(yamlFileCopyPath, startUri); - var cliParameters = new List { "run" }; if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) @@ -151,20 +153,51 @@ private async Task EnsureInitializedAsync() private async Task PrepareYamlAsync(string yamlFilePath, Uri startUri) { - var yaml = await File.ReadAllTextAsync(yamlFilePath, _cancellationTokenSource.Token); + var originalYaml = await File.ReadAllTextAsync(yamlFilePath, _cancellationTokenSource.Token); + + var deserializer = new DeserializerBuilder().Build(); + // Deseralizing into a free-form object, not to potentially break unknown fields during reserialization. + dynamic configuration = deserializer.Deserialize(originalYaml); + + List contexts = configuration["env"]["contexts"]; + + if (!contexts.Any()) + { + throw new ArgumentException("The supplied ZAP Automation Framework YAML file should contain at least one context."); + } - // Setting URLs: - yaml = yaml.Replace("", startUri.ToString()); + var context = (Dictionary)contexts[0]; - //var deserializer = new DeserializerBuilder().Build(); + if (contexts.Count > 1) + { + context = + (Dictionary)contexts + .Find(context => + ((Dictionary)context).TryGetValue("name", out var name) && (string)name == "Default Context") ?? + context; + } + + // Setting URLs in the context. + // Setting includePaths in the context is not necessary because by default everything under urls will be scanned. + + if (!context.ContainsKey("urls")) context["urls"] = new List(); + + var urls = (List)context["urls"]; + + if (urls.Count > 1) + { + throw new ArgumentException( + "The context in the ZAP Automation Framework YAML file should contain at most a single url in the urls section."); + } - //// Deseralizing into a free-form object, not to potentially break unknown fields during reserialization. - //dynamic configuration = deserializer.Deserialize(yaml); + if (urls.Count == 1) urls.Clear(); + urls.Add(startUri.ToString()); - //var contexts = configuration["env"]["contexts"]; - //contexts["urls"] + // Serializing the results. + var serializer = new SerializerBuilder().WithQuotingNecessaryStrings().Build(); + var updatedYaml = serializer.Serialize(configuration); - await File.WriteAllTextAsync(yamlFilePath, yaml, _cancellationTokenSource.Token); + await File.WriteAllTextAsync(yamlFilePath, updatedYaml, _cancellationTokenSource.Token); } } From e25d515e07cf98fd7f50f61b0f2e3d57eb22293f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 15 Nov 2023 18:50:56 +0100 Subject: [PATCH 019/129] Adding UITestContext extensions for security scans --- .../Tests/SecurityScanningTests.cs | 3 +- ...SecurityScanningUITestContextExtensions.cs | 97 +++++++++++++++++++ .../SecurityScanning/ZapManager.cs | 62 +++++++++--- 3 files changed, 148 insertions(+), 14 deletions(-) create mode 100644 Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index f2c2eb183..bdf34c9df 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -1,6 +1,7 @@ using Lombiq.Tests.UI.Attributes; using Lombiq.Tests.UI.Extensions; using Lombiq.Tests.UI.Pages; +using Lombiq.Tests.UI.SecurityScanning; using Lombiq.Tests.UI.Services; using OpenQA.Selenium; using System; @@ -24,7 +25,7 @@ public Task SecurityScanShouldPass(Browser browser) => ExecuteTestAfterSetupAsync( async context => { - await context.ZapManager.RunSecurityScanAsync(context, context.Scope.BaseUri); + await context.RunBaselineSecurityScanAsync(); }, browser); diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs new file mode 100644 index 000000000..bd383174e --- /dev/null +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs @@ -0,0 +1,97 @@ +using Lombiq.Tests.UI.Extensions; +using Lombiq.Tests.UI.Services; +using System; +using System.Threading.Tasks; + +namespace Lombiq.Tests.UI.SecurityScanning; + +public static class SecurityScanningUITestContextExtensions +{ + /// + /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app with the Baseline + /// Automation Framework profile (see for the + /// official docs on the legacy version of this scan). + /// + /// + /// The under the app where to start the scan from. If not provided, defaults to the current URL. + /// + /// + /// A delegate that may optionally modify the deserialized representation of the ZAP Automation Framework YAML. + /// + public static Task RunBaselineSecurityScanAsync( + this UITestContext context, + Uri startUri = null, + Func modifyYaml = null) => + context.RunSecurityScanAsync(AutomationFrameworkYamlPaths.BaselineYamlPath, startUri, modifyYaml); + + /// + /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app with the Full Scan + /// Automation Framework profile (see for the + /// official docs on the legacy version of this scan). + /// + /// + /// The under the app where to start the scan from. If not provided, defaults to the current URL. + /// + /// + /// A delegate that may optionally modify the deserialized representation of the ZAP Automation Framework YAML. + /// + public static Task RunFullSecurityScanAsync( + this UITestContext context, + Uri startUri = null, + Func modifyYaml = null) => + context.RunSecurityScanAsync(AutomationFrameworkYamlPaths.FullScanYamlPath, startUri, modifyYaml); + + /// + /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app with the GraphQL + /// Automation Framework profile (see for + /// the official docs on ZAP's GraphQL support). + /// + /// + /// The under the app where to start the scan from. If not provided, defaults to the current URL. + /// + /// + /// A delegate that may optionally modify the deserialized representation of the ZAP Automation Framework YAML. + /// + public static Task RunGraphQLSecurityScanAsync( + this UITestContext context, + Uri startUri = null, + Func modifyYaml = null) => + context.RunSecurityScanAsync(AutomationFrameworkYamlPaths.GraphQLYamlPath, startUri, modifyYaml); + + /// + /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app with the OpenAPI + /// Automation Framework profile (see for + /// the official docs on ZAP's GraphQL support). + /// + /// + /// The under the app where to start the scan from. If not provided, defaults to the current URL. + /// + /// + /// A delegate that may optionally modify the deserialized representation of the ZAP Automation Framework YAML. + /// + public static Task RunOpenApiSecurityScanAsync( + this UITestContext context, + Uri startUri = null, + Func modifyYaml = null) => + context.RunSecurityScanAsync(AutomationFrameworkYamlPaths.BaselineYamlPath, startUri, modifyYaml); + + /// + /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app. + /// + /// + /// File system path to the YAML configuration file of ZAP's Automation Framework. See + /// for details. + /// + /// + /// The under the app where to start the scan from. If not provided, defaults to the current URL. + /// + /// + /// A delegate that may optionally modify the deserialized representation of the ZAP Automation Framework YAML. + /// + public static Task RunSecurityScanAsync( + this UITestContext context, + string automationFrameworkYamlPath, + Uri startUri = null, + Func modifyYaml = null) => + context.ZapManager.RunSecurityScanAsync(context, automationFrameworkYamlPath, startUri ?? context.GetCurrentUri(), modifyYaml); +} diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 19101322d..3823a3a46 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -15,6 +15,9 @@ namespace Lombiq.Tests.UI.SecurityScanning; +/// +/// Service to manage Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) instances and security scans. +/// public sealed class ZapManager : IAsyncDisposable { // Need to use the weekly release because that's the one that has packaged scans migrated to Automation Framework. @@ -34,18 +37,46 @@ public sealed class ZapManager : IAsyncDisposable internal ZapManager(ITestOutputHelper testOutputHelper) => _testOutputHelper = testOutputHelper; /// - /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against the app. + /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app. /// /// The of the currently executing test. + /// + /// File system path to the YAML configuration file of ZAP's Automation Framework. See + /// for details. + /// /// The under the app where to start the scan from. + /// + /// A delegate that may optionally modify the deserialized representation of the ZAP Automation Framework YAML. + /// + public Task RunSecurityScanAsync( + UITestContext context, + string automationFrameworkYamlPath, + Uri startUri, + Func modifyYaml = null) => + RunSecurityScanAsync( + context, + automationFrameworkYamlPath, + async configuration => + { + SetStartUrlInYaml(configuration, startUri); + if (modifyYaml != null) await modifyYaml(configuration); + }); + + /// + /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app. + /// + /// The of the currently executing test. /// /// File system path to the YAML configuration file of ZAP's Automation Framework. See /// for details. /// + /// + /// A delegate that may optionally modify the deserialized representation of the ZAP Automation Framework YAML. + /// public async Task RunSecurityScanAsync( UITestContext context, - Uri startUri, - string automationFrameworkYamlPath = null) + string automationFrameworkYamlPath, + Func modifyYaml = null) { await EnsureInitializedAsync(); @@ -62,7 +93,7 @@ public async Task RunSecurityScanAsync( File.Copy(automationFrameworkYamlPath, yamlFileCopyPath, overwrite: true); - await PrepareYamlAsync(yamlFileCopyPath, startUri); + await PrepareYamlAsync(yamlFileCopyPath, modifyYaml); // Explanation on the CLI arguments used below: // - --add-host and --network host: Lets us connect to the host OS's localhost, where the OC app runs, with @@ -151,15 +182,26 @@ private async Task EnsureInitializedAsync() } } - private async Task PrepareYamlAsync(string yamlFilePath, Uri startUri) + private async Task PrepareYamlAsync(string yamlFilePath, Func modifyYaml) { var originalYaml = await File.ReadAllTextAsync(yamlFilePath, _cancellationTokenSource.Token); var deserializer = new DeserializerBuilder().Build(); // Deseralizing into a free-form object, not to potentially break unknown fields during reserialization. - dynamic configuration = deserializer.Deserialize(originalYaml); + var configuration = deserializer.Deserialize(originalYaml); - List contexts = configuration["env"]["contexts"]; + if (modifyYaml != null) await modifyYaml(configuration); + + // Serializing the results. + var serializer = new SerializerBuilder().WithQuotingNecessaryStrings().Build(); + var updatedYaml = serializer.Serialize(configuration); + + await File.WriteAllTextAsync(yamlFilePath, updatedYaml, _cancellationTokenSource.Token); + } + + private static void SetStartUrlInYaml(object configuration, Uri startUri) + { + List contexts = ((dynamic)configuration)["env"]["contexts"]; if (!contexts.Any()) { @@ -193,11 +235,5 @@ private async Task PrepareYamlAsync(string yamlFilePath, Uri startUri) if (urls.Count == 1) urls.Clear(); urls.Add(startUri.ToString()); - - // Serializing the results. - var serializer = new SerializerBuilder().WithQuotingNecessaryStrings().Build(); - var updatedYaml = serializer.Serialize(configuration); - - await File.WriteAllTextAsync(yamlFilePath, updatedYaml, _cancellationTokenSource.Token); } } From 1ba4af9ceb2222509bd46c9bf40dfd19bcc2f7bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 15 Nov 2023 19:07:55 +0100 Subject: [PATCH 020/129] Nicer-looking reports --- .../AutomationFrameworkYamls/Baseline.yml | 21 +++++++++++++++++-- .../AutomationFrameworkYamls/FullScan.yml | 21 +++++++++++++++++-- .../AutomationFrameworkYamls/GraphQL.yml | 21 +++++++++++++++++-- .../AutomationFrameworkYamls/OpenAPI.yml | 21 +++++++++++++++++-- 4 files changed, 76 insertions(+), 8 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml index f6dfde001..7bb057d02 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml @@ -60,10 +60,27 @@ jobs: name: "passiveScan-wait" type: "passiveScan-wait" - parameters: - template: "risk-confidence-html" reportDir: "/zap/wrk/reports" + template: "modern" + theme: "technology" reportTitle: "ZAP Scanning Report" reportDescription: "" + risks: + - "low" + - "medium" + - "high" + confidences: + - "low" + - "medium" + - "high" + - "confirmed" + sections: + - "passingrules" + - "instancecount" + - "alertdetails" + - "alertcount" + - "params" + - "chart" + - "statistics" name: "report" type: "report" - diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml index 8760037a5..26f3e6fa1 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml @@ -65,10 +65,27 @@ jobs: name: "activeScan" type: "activeScan" - parameters: - template: "risk-confidence-html" reportDir: "/zap/wrk/reports" + template: "modern" + theme: "technology" reportTitle: "ZAP Scanning Report" reportDescription: "" + risks: + - "low" + - "medium" + - "high" + confidences: + - "low" + - "medium" + - "high" + - "confirmed" + sections: + - "passingrules" + - "instancecount" + - "alertdetails" + - "alertcount" + - "params" + - "chart" + - "statistics" name: "report" type: "report" - diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml index c53c8f3a5..81a5ad611 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml @@ -50,10 +50,27 @@ jobs: name: "activeScan" type: "activeScan" - parameters: - template: "risk-confidence-html" reportDir: "/zap/wrk/reports" + template: "modern" + theme: "technology" reportTitle: "ZAP Scanning Report" reportDescription: "" + risks: + - "low" + - "medium" + - "high" + confidences: + - "low" + - "medium" + - "high" + - "confirmed" + sections: + - "passingrules" + - "instancecount" + - "alertdetails" + - "alertcount" + - "params" + - "chart" + - "statistics" name: "report" type: "report" - diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml index 2caeb326a..f3254afe6 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml @@ -41,10 +41,27 @@ jobs: name: "activeScan" type: "activeScan" - parameters: - template: "risk-confidence-html" reportDir: "/zap/wrk/reports" + template: "modern" + theme: "technology" reportTitle: "ZAP Scanning Report" reportDescription: "" + risks: + - "low" + - "medium" + - "high" + confidences: + - "low" + - "medium" + - "high" + - "confirmed" + sections: + - "passingrules" + - "instancecount" + - "alertdetails" + - "alertcount" + - "params" + - "chart" + - "statistics" name: "report" type: "report" - From 821bf4a78f0b4cb72425f18413b713fc433d540f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 15 Nov 2023 19:13:24 +0100 Subject: [PATCH 021/129] Adding basics of separate docs page --- .../Docs/Attachments/ZapReportScreenshot.png | Bin 0 -> 70089 bytes Lombiq.Tests.UI/Docs/CreatingTests.md | 2 +- Lombiq.Tests.UI/Docs/SecurityScanning.md | 7 +++++++ Readme.md | 1 + 4 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 Lombiq.Tests.UI/Docs/Attachments/ZapReportScreenshot.png create mode 100644 Lombiq.Tests.UI/Docs/SecurityScanning.md diff --git a/Lombiq.Tests.UI/Docs/Attachments/ZapReportScreenshot.png b/Lombiq.Tests.UI/Docs/Attachments/ZapReportScreenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..39a5438c0312bf2dc2ccfb4a17046838022ee902 GIT binary patch literal 70089 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 Date: Wed, 15 Nov 2023 19:20:00 +0100 Subject: [PATCH 022/129] Generating SARIF reports too --- .../AutomationFrameworkYamls/Baseline.yml | 22 +++++++++++++++++++ .../AutomationFrameworkYamls/FullScan.yml | 22 +++++++++++++++++++ .../AutomationFrameworkYamls/GraphQL.yml | 22 +++++++++++++++++++ .../AutomationFrameworkYamls/OpenAPI.yml | 22 +++++++++++++++++++ 4 files changed, 88 insertions(+) diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml index 7bb057d02..a65874fce 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml @@ -84,3 +84,25 @@ jobs: - "statistics" name: "report" type: "report" +- parameters: + template: "sarif-json" + reportDir: "/zap/wrk/reports" + reportFile: "" + reportTitle: "ZAP Scanning Report" + reportDescription: "" + displayReport: false + risks: + - "info" + - "low" + - "medium" + - "high" + confidences: + - "falsepositive" + - "low" + - "medium" + - "high" + - "confirmed" + sites: [] + name: "sarifReport" + type: "report" + diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml index 26f3e6fa1..dee1b59f2 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml @@ -89,3 +89,25 @@ jobs: - "statistics" name: "report" type: "report" +- parameters: + template: "sarif-json" + reportDir: "/zap/wrk/reports" + reportFile: "" + reportTitle: "ZAP Scanning Report" + reportDescription: "" + displayReport: false + risks: + - "info" + - "low" + - "medium" + - "high" + confidences: + - "falsepositive" + - "low" + - "medium" + - "high" + - "confirmed" + sites: [] + name: "sarifReport" + type: "report" + diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml index 81a5ad611..6e32aea75 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml @@ -74,3 +74,25 @@ jobs: - "statistics" name: "report" type: "report" +- parameters: + template: "sarif-json" + reportDir: "/zap/wrk/reports" + reportFile: "" + reportTitle: "ZAP Scanning Report" + reportDescription: "" + displayReport: false + risks: + - "info" + - "low" + - "medium" + - "high" + confidences: + - "falsepositive" + - "low" + - "medium" + - "high" + - "confirmed" + sites: [] + name: "sarifReport" + type: "report" + diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml index f3254afe6..e4f7f3c12 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml @@ -65,3 +65,25 @@ jobs: - "statistics" name: "report" type: "report" +- parameters: + template: "sarif-json" + reportDir: "/zap/wrk/reports" + reportFile: "" + reportTitle: "ZAP Scanning Report" + reportDescription: "" + displayReport: false + risks: + - "info" + - "low" + - "medium" + - "high" + confidences: + - "falsepositive" + - "low" + - "medium" + - "high" + - "confirmed" + sites: [] + name: "sarifReport" + type: "report" + From 754bd4c00a52e049f42d84aea492d819ab806ec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 15 Nov 2023 20:28:05 +0100 Subject: [PATCH 023/129] Automatically setting report directories to the conventional one --- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 3823a3a46..e6dc1387a 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -190,6 +190,18 @@ private async Task PrepareYamlAsync(string yamlFilePath, Func modi // Deseralizing into a free-form object, not to potentially break unknown fields during reserialization. var configuration = deserializer.Deserialize(originalYaml); + // Setting report directories to the conventional one. + List jobs = ((dynamic)configuration)["jobs"]; + foreach (var job in jobs) + { + var jobDictionary = (Dictionary)job; + + if (!jobDictionary.TryGetValue("type", out var typeValue) || (string)typeValue != "report") continue; + + var parameters = (Dictionary)jobDictionary["parameters"]; + parameters["reportDir"] = "/zap/wrk/reports"; + } + if (modifyYaml != null) await modifyYaml(configuration); // Serializing the results. From 694557f6e5b3df8f44d4cd9e69f5c8a71aa15568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 15 Nov 2023 20:54:31 +0100 Subject: [PATCH 024/129] Docs --- Lombiq.Tests.UI/Docs/SecurityScanning.md | 7 ++++++- Lombiq.Tests.UI/Services/UITestContext.cs | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Lombiq.Tests.UI/Docs/SecurityScanning.md b/Lombiq.Tests.UI/Docs/SecurityScanning.md index 2abed8aee..16be1773d 100644 --- a/Lombiq.Tests.UI/Docs/SecurityScanning.md +++ b/Lombiq.Tests.UI/Docs/SecurityScanning.md @@ -1,7 +1,12 @@ # Security scanning with ZAP +## Overview + You an create detailed security scans of your app with [Zed Attack Proxy (ZAP)](https://www.zaproxy.org/) right from the Lombiq UI Testing Toolbox, with detailed reports. ![Sample ZAP security scan report](Attachments/ZapReportScreenshot.png) -`Lombiq.Tests.UI.Samples` contains a demonstration of how to use security scanning. +- `Lombiq.Tests.UI.Samples` contains a demonstration of how to use security scanning. Check that out for code examples. +- The most important default ZAP scans, Baseline, Full, GraphQL, and OpenAPI scans are included and readily usable. +- Since we use [ZAP's Automation Framework](https://www.zaproxy.org/docs/automate/automation-framework/) for configuration, you have complete and detailed control over how the scans are configured. +- [SARIF](https://sarifweb.azurewebsites.net/) reports are available to integrate with other InfoSec tools. diff --git a/Lombiq.Tests.UI/Services/UITestContext.cs b/Lombiq.Tests.UI/Services/UITestContext.cs index 5e51039ea..7f14ac2e9 100644 --- a/Lombiq.Tests.UI/Services/UITestContext.cs +++ b/Lombiq.Tests.UI/Services/UITestContext.cs @@ -83,7 +83,7 @@ public class UITestContext public Dictionary CustomContext { get; } = new(); /// - /// Gets a dictionary storing some custom data for collecting in failure dump. + /// Gets a dictionary storing some custom data for collecting in the failure dump. /// public IDictionary FailureDumpContainer { get; } = new Dictionary(); From 06a7e02835590cba2ee085e4adb340f92bd4b324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 15 Nov 2023 21:15:03 +0100 Subject: [PATCH 025/129] Changing report theme to the less extravagant "corporate" one --- .../SecurityScanning/AutomationFrameworkYamls/Baseline.yml | 2 +- .../SecurityScanning/AutomationFrameworkYamls/FullScan.yml | 2 +- .../SecurityScanning/AutomationFrameworkYamls/GraphQL.yml | 2 +- .../SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml index a65874fce..7bb3b8bc6 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml @@ -62,7 +62,7 @@ jobs: - parameters: reportDir: "/zap/wrk/reports" template: "modern" - theme: "technology" + theme: "corporate" reportTitle: "ZAP Scanning Report" reportDescription: "" risks: diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml index dee1b59f2..bc52140dc 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml @@ -67,7 +67,7 @@ jobs: - parameters: reportDir: "/zap/wrk/reports" template: "modern" - theme: "technology" + theme: "corporate" reportTitle: "ZAP Scanning Report" reportDescription: "" risks: diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml index 6e32aea75..8e5e712af 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml @@ -52,7 +52,7 @@ jobs: - parameters: reportDir: "/zap/wrk/reports" template: "modern" - theme: "technology" + theme: "corporate" reportTitle: "ZAP Scanning Report" reportDescription: "" risks: diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml index e4f7f3c12..08a04ad7e 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml @@ -43,7 +43,7 @@ jobs: - parameters: reportDir: "/zap/wrk/reports" template: "modern" - theme: "technology" + theme: "corporate" reportTitle: "ZAP Scanning Report" reportDescription: "" risks: From 06bbfe0b9b130c3ce2fd0b6c0c648e162f0d78a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 15 Nov 2023 22:01:56 +0100 Subject: [PATCH 026/129] Saving ZAP reports into the Failure Dump --- .../FailureDumpUITestContextExtensions.cs | 53 ++++++++++++++-- Lombiq.Tests.UI/Lombiq.Tests.UI.csproj | 1 + .../SecurityScanningException.cs | 20 ++++++ .../SecurityScanning/ZapManager.cs | 62 ++++++++++++++++--- 4 files changed, 122 insertions(+), 14 deletions(-) create mode 100644 Lombiq.Tests.UI/SecurityScanning/SecurityScanningException.cs diff --git a/Lombiq.Tests.UI/Extensions/FailureDumpUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/FailureDumpUITestContextExtensions.cs index fdeca8613..19872250f 100644 --- a/Lombiq.Tests.UI/Extensions/FailureDumpUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/FailureDumpUITestContextExtensions.cs @@ -11,10 +11,34 @@ namespace Lombiq.Tests.UI.Extensions; public static class FailureDumpUITestContextExtensions { + /// + /// Appends a local directory's whole content to be collected on failure dump. + /// + /// The full file system path of the directory. + /// A message to display in case the desired file already exists in the dump. + public static void AppendDirectoryToFailureDump( + this UITestContext context, + string directoryPath, + string messageIfExists = null) => + RecursivelyAppendFolderContent(context, directoryPath, string.Empty, messageIfExists); + + /// + /// Appends a local file's content to be collected on failure dump. + /// + /// The full file system path of the file. + /// A message to display in case the desired file already exists in the dump. + public static void AppendFailureDump( + this UITestContext context, + string filePath, + string messageIfExists = null) => + context.AppendFailureDump( + Path.GetFileName(filePath), + context => Task.FromResult((Stream)File.OpenRead(filePath)), + messageIfExists); + /// /// Appends stream as file content to be collected on failure dump. /// - /// instance. /// The name of the file. /// Gets called in failure dump collection. /// A message to display in case the desired file already exists in the dump. @@ -31,7 +55,6 @@ public static void AppendFailureDump( /// /// Appends string as file content to be collected on failure dump. /// - /// instance. /// The name of the file. /// File content. /// A message to display in case the desired file already exists in the dump. @@ -51,7 +74,6 @@ public static void AppendFailureDump( /// /// Appends generic content as file content to be collected on failure dump. /// - /// instance. /// The name of the file. /// File content. /// Function to get a new from content. @@ -72,7 +94,6 @@ public static void AppendFailureDump( /// /// Appends as file content to be collected on failure dump. /// - /// instance. /// The name of the file. /// File content. The will be disposed at the end. /// A message to display in case the desired file already exists in the dump. @@ -100,4 +121,28 @@ private static void AppendFailureDumpInternal( context.FailureDumpContainer.Add(fileName, item); } + + private static void RecursivelyAppendFolderContent( + UITestContext context, + string directoryPath, + string failureDumpDirectoryPath, + string messageIfExists = null) + { + foreach (var filePath in Directory.GetFiles(directoryPath)) + { + context.AppendFailureDump( + Path.Combine(failureDumpDirectoryPath, Path.GetFileName(filePath)), + context => Task.FromResult((Stream)File.OpenRead(filePath)), + messageIfExists); + } + + foreach (var subDirectoryPath in Directory.GetDirectories(directoryPath)) + { + RecursivelyAppendFolderContent( + context, + subDirectoryPath, + Path.Combine(failureDumpDirectoryPath, Path.GetFileName(subDirectoryPath)), + messageIfExists); + } + } } diff --git a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj index 3bc64d1c0..bf12db5d3 100644 --- a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj +++ b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj @@ -81,6 +81,7 @@ + diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningException.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningException.cs new file mode 100644 index 000000000..c78eef82a --- /dev/null +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningException.cs @@ -0,0 +1,20 @@ +using System; + +namespace Lombiq.Tests.UI.SecurityScanning; + +public class SecurityScanningException : Exception +{ + public SecurityScanningException() + { + } + + public SecurityScanningException(string message) + : base(message) + { + } + + public SecurityScanningException(string message, Exception innerException) + : base(message, innerException) + { + } +} diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index e6dc1387a..2f538e142 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -1,7 +1,9 @@ using CliWrap; using Lombiq.HelpfulLibraries.Cli; using Lombiq.Tests.UI.Constants; +using Lombiq.Tests.UI.Extensions; using Lombiq.Tests.UI.Services; +using Microsoft.CodeAnalysis.Sarif; using System; using System.Collections.Generic; using System.IO; @@ -24,6 +26,8 @@ public sealed class ZapManager : IAsyncDisposable // When updating this version, also regenerate the Automation Framework YAML config files so we don't miss any // changes to those. private const string _zapImage = "softwaresecurityproject/zap-weekly:20231113"; + private const string _zapWorkingDirectoryPath = "/zap/wrk/"; + private const string _zapReportsDirectoryName = "reports"; private static readonly SemaphoreSlim _pullSemaphore = new(1, 1); private static readonly CliProgram _docker = new("docker"); @@ -48,7 +52,8 @@ public sealed class ZapManager : IAsyncDisposable /// /// A delegate that may optionally modify the deserialized representation of the ZAP Automation Framework YAML. /// - public Task RunSecurityScanAsync( + /// The SARIF () report of the scan. + public Task RunSecurityScanAsync( UITestContext context, string automationFrameworkYamlPath, Uri startUri, @@ -73,7 +78,7 @@ public Task RunSecurityScanAsync( /// /// A delegate that may optionally modify the deserialized representation of the ZAP Automation Framework YAML. /// - public async Task RunSecurityScanAsync( + public async Task RunSecurityScanAsync( UITestContext context, string automationFrameworkYamlPath, Func modifyYaml = null) @@ -128,24 +133,50 @@ public async Task RunSecurityScanAsync( cliParameters.AddRange(new object[] { "--volume", - mountedDirectoryPath + ":/zap/wrk/:rw", + $"{mountedDirectoryPath}:{_zapWorkingDirectoryPath}:rw", "--tty", _zapImage, "zap.sh", "-cmd", "-autorun", - "/zap/wrk/" + yamlFileName, + _zapWorkingDirectoryPath + yamlFileName, }); var stdErrBuffer = new StringBuilder(); - var result = await _docker + // The result of the call is not interesting, since we don't need the exit code: Assertions should check if the + // app failed security scanning, and if the scan itself fails then there won't be a report, what's checked below. + await _docker .GetCommand(cliParameters) .WithStandardOutputPipe(PipeTarget.ToDelegate(line => _testOutputHelper.WriteLineTimestampedAndDebug(line))) .WithStandardErrorPipe(PipeTarget.ToStringBuilder(stdErrBuffer)) // This is so no exception is thrown by CliWrap if the exit code is not 0. .WithValidation(CommandResultValidation.None) .ExecuteAsync(_cancellationTokenSource.Token); + + var reportsDirectoryPath = Path.Combine(mountedDirectoryPath, _zapReportsDirectoryName); + + var jsonReports = Directory.EnumerateFiles(reportsDirectoryPath, "*.json").ToList(); + + if (jsonReports.Count > 1) + { + throw new SecurityScanningException( + "There were more than one JSON reports generated for the ZAP scan. The supplied ZAP Automation " + + "Framework YAML file should contain exactly one JSON report job, generating a SARIF report."); + } + + if (jsonReports.Count != 1) + { + throw new SecurityScanningException( + "No SARIF JSON report was generated for the ZAP scan. This indicates that the scan couldn't finish. " + + "Check the test output for details."); + } + + context.AppendDirectoryToFailureDump(reportsDirectoryPath); + + throw new Exception(); + return SarifLog.Load(jsonReports[0]); + //log.Runs.First().Results.Any(result => result.Kind == ResultKind.Fail); } public ValueTask DisposeAsync() @@ -190,8 +221,10 @@ private async Task PrepareYamlAsync(string yamlFilePath, Func modi // Deseralizing into a free-form object, not to potentially break unknown fields during reserialization. var configuration = deserializer.Deserialize(originalYaml); - // Setting report directories to the conventional one. + // Setting report directories to the conventional one and verifying that there's exactly one SARIF report. List jobs = ((dynamic)configuration)["jobs"]; + var sarifReportCount = 0; + foreach (var job in jobs) { var jobDictionary = (Dictionary)job; @@ -199,7 +232,15 @@ private async Task PrepareYamlAsync(string yamlFilePath, Func modi if (!jobDictionary.TryGetValue("type", out var typeValue) || (string)typeValue != "report") continue; var parameters = (Dictionary)jobDictionary["parameters"]; - parameters["reportDir"] = "/zap/wrk/reports"; + parameters["reportDir"] = _zapWorkingDirectoryPath + _zapReportsDirectoryName; + + if ((string)parameters["template"] == "sarif-json") sarifReportCount++; + } + + if (sarifReportCount != 1) + { + throw new ArgumentException( + "The supplied ZAP Automation Framework YAML file should contain exactly one SARIF report job."); } if (modifyYaml != null) await modifyYaml(configuration); @@ -217,7 +258,8 @@ private static void SetStartUrlInYaml(object configuration, Uri startUri) if (!contexts.Any()) { - throw new ArgumentException("The supplied ZAP Automation Framework YAML file should contain at least one context."); + throw new ArgumentException( + "The supplied ZAP Automation Framework YAML file should contain at least one context."); } var context = (Dictionary)contexts[0]; @@ -227,8 +269,8 @@ private static void SetStartUrlInYaml(object configuration, Uri startUri) context = (Dictionary)contexts .Find(context => - ((Dictionary)context).TryGetValue("name", out var name) && (string)name == "Default Context") ?? - context; + ((Dictionary)context).TryGetValue("name", out var name) && (string)name == "Default Context") + ?? context; } // Setting URLs in the context. From c5fc6636f1510cd2ab041abcc5712d985eaf7823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 15 Nov 2023 22:53:48 +0100 Subject: [PATCH 027/129] Adding the ability to assert on security scan results --- .../Tests/SecurityScanningTests.cs | 2 +- .../SecurityScanning/SecurityScanResult.cs | 15 ++ .../SecurityScanningAssertionException.cs | 27 ++++ .../SecurityScanningConfiguration.cs | 23 +++ ...SecurityScanningUITestContextExtensions.cs | 149 +++++++++++++++--- .../SecurityScanning/ZapManager.cs | 28 ++-- .../OrchardCoreUITestExecutorConfiguration.cs | 3 + 7 files changed, 207 insertions(+), 40 deletions(-) create mode 100644 Lombiq.Tests.UI/SecurityScanning/SecurityScanResult.cs create mode 100644 Lombiq.Tests.UI/SecurityScanning/SecurityScanningAssertionException.cs create mode 100644 Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index bdf34c9df..f37098b2c 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -25,7 +25,7 @@ public Task SecurityScanShouldPass(Browser browser) => ExecuteTestAfterSetupAsync( async context => { - await context.RunBaselineSecurityScanAsync(); + await context.RunAndAssertBaselineSecurityScanAsync(); }, browser); diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanResult.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanResult.cs new file mode 100644 index 000000000..269e1d044 --- /dev/null +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanResult.cs @@ -0,0 +1,15 @@ +using Microsoft.CodeAnalysis.Sarif; + +namespace Lombiq.Tests.UI.SecurityScanning; + +public class SecurityScanResult +{ + public string ReportsDirectoryPath { get; } + public SarifLog SarifLog { get; } + + public SecurityScanResult(string reportsDirectoryPath, SarifLog sarifLog) + { + ReportsDirectoryPath = reportsDirectoryPath; + SarifLog = sarifLog; + } +} diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningAssertionException.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningAssertionException.cs new file mode 100644 index 000000000..138907dd3 --- /dev/null +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningAssertionException.cs @@ -0,0 +1,27 @@ +using System; + +namespace Lombiq.Tests.UI.Exceptions; + +public class SecurityScanningAssertionException : Exception +{ + public SecurityScanningAssertionException(Exception innerException) + : base( + "Asserting the security scan result failed. Check the security scan report in the failure dump for details.", + innerException) + { + } + + public SecurityScanningAssertionException() + { + } + + public SecurityScanningAssertionException(string message) + : base(message) + { + } + + public SecurityScanningAssertionException(string message, Exception innerException) + : base(message, innerException) + { + } +} diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs new file mode 100644 index 000000000..688274722 --- /dev/null +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs @@ -0,0 +1,23 @@ +using Lombiq.Tests.UI.Services; +using Microsoft.CodeAnalysis.Sarif; +using Shouldly; +using System; +using System.Threading.Tasks; + +namespace Lombiq.Tests.UI.SecurityScanning; + +public class SecurityScanningConfiguration +{ + /// + /// Gets or sets a delegate that may modify the deserialized representation of the ZAP Automation Framework YAML. + /// + public Func ZapAutomationFrameworkYamlModifier { get; set; } + + /// + /// Gets or sets a delegate to run assertions on the when security scanning happens. + /// + public Action AssertSecurityScanResult { get; set; } = AssertSecurityScanHasNoFails; + + public static readonly Action AssertSecurityScanHasNoFails = + (_, sarifLog) => sarifLog.Runs[0].Results.ShouldNotContain(result => result.Kind == ResultKind.Fail); +} diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs index bd383174e..2bf88f7c8 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs @@ -1,5 +1,7 @@ +using Lombiq.Tests.UI.Exceptions; using Lombiq.Tests.UI.Extensions; using Lombiq.Tests.UI.Services; +using Microsoft.CodeAnalysis.Sarif; using System; using System.Threading.Tasks; @@ -9,71 +11,162 @@ public static class SecurityScanningUITestContextExtensions { /// /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app with the Baseline - /// Automation Framework profile (see for the - /// official docs on the legacy version of this scan). + /// Automation Framework profile and runs assertions on the result (see for the official docs on the legacy version of this + /// scan). /// /// /// The under the app where to start the scan from. If not provided, defaults to the current URL. /// /// - /// A delegate that may optionally modify the deserialized representation of the ZAP Automation Framework YAML. + /// A delegate to modify the deserialized representation of the ZAP Automation Framework YAML. /// - public static Task RunBaselineSecurityScanAsync( + /// + /// A delegate to run assertions on the one the scan finishes. + /// + public static Task RunAndAssertBaselineSecurityScanAsync( this UITestContext context, Uri startUri = null, - Func modifyYaml = null) => - context.RunSecurityScanAsync(AutomationFrameworkYamlPaths.BaselineYamlPath, startUri, modifyYaml); + Func modifyYaml = null, + Action assertSecurityScanResult = null) => + context.RunAndAssertSecurityScanAsync( + AutomationFrameworkYamlPaths.BaselineYamlPath, + startUri, + modifyYaml, + assertSecurityScanResult); /// /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app with the Full Scan - /// Automation Framework profile (see for the - /// official docs on the legacy version of this scan). + /// Automation Framework profile and runs assertions on the result (see for the official docs on the legacy version of this + /// scan). /// /// /// The under the app where to start the scan from. If not provided, defaults to the current URL. /// /// - /// A delegate that may optionally modify the deserialized representation of the ZAP Automation Framework YAML. + /// A delegate to modify the deserialized representation of the ZAP Automation Framework YAML. /// - public static Task RunFullSecurityScanAsync( + /// + /// A delegate to run assertions on the one the scan finishes. + /// + public static Task RunAndAssertFullSecurityScanAsync( this UITestContext context, Uri startUri = null, - Func modifyYaml = null) => - context.RunSecurityScanAsync(AutomationFrameworkYamlPaths.FullScanYamlPath, startUri, modifyYaml); + Func modifyYaml = null, + Action assertSecurityScanResult = null) => + context.RunAndAssertSecurityScanAsync( + AutomationFrameworkYamlPaths.FullScanYamlPath, + startUri, + modifyYaml, + assertSecurityScanResult); /// /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app with the GraphQL - /// Automation Framework profile (see for - /// the official docs on ZAP's GraphQL support). + /// Automation Framework profile and runs assertions on the result (see for the official docs on ZAP's GraphQL + /// support). /// /// /// The under the app where to start the scan from. If not provided, defaults to the current URL. /// /// - /// A delegate that may optionally modify the deserialized representation of the ZAP Automation Framework YAML. + /// A delegate to modify the deserialized representation of the ZAP Automation Framework YAML. /// - public static Task RunGraphQLSecurityScanAsync( + /// + /// A delegate to run assertions on the one the scan finishes. + /// + public static Task RunAndAssertGraphQLSecurityScanAsync( this UITestContext context, Uri startUri = null, - Func modifyYaml = null) => - context.RunSecurityScanAsync(AutomationFrameworkYamlPaths.GraphQLYamlPath, startUri, modifyYaml); + Func modifyYaml = null, + Action assertSecurityScanResult = null) => + context.RunAndAssertSecurityScanAsync( + AutomationFrameworkYamlPaths.GraphQLYamlPath, + startUri, + modifyYaml, + assertSecurityScanResult); /// /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app with the OpenAPI - /// Automation Framework profile (see for - /// the official docs on ZAP's GraphQL support). + /// Automation Framework profile and runs assertions on the result (see for the official docs on ZAP's GraphQL + /// support). /// /// /// The under the app where to start the scan from. If not provided, defaults to the current URL. /// /// - /// A delegate that may optionally modify the deserialized representation of the ZAP Automation Framework YAML. + /// A delegate to modify the deserialized representation of the ZAP Automation Framework YAML. + /// + /// + /// A delegate to run assertions on the one the scan finishes. /// - public static Task RunOpenApiSecurityScanAsync( + public static Task RunAndAssertOpenApiSecurityScanAsync( this UITestContext context, Uri startUri = null, - Func modifyYaml = null) => - context.RunSecurityScanAsync(AutomationFrameworkYamlPaths.BaselineYamlPath, startUri, modifyYaml); + Func modifyYaml = null, + Action assertSecurityScanResult = null) => + context.RunAndAssertSecurityScanAsync( + AutomationFrameworkYamlPaths.OpenAPIYamlPath, + startUri, + modifyYaml, + assertSecurityScanResult); + + /// + /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app and runs assertions on + /// the result. + /// + /// + /// File system path to the YAML configuration file of ZAP's Automation Framework. See for details. + /// + /// + /// The under the app where to start the scan from. If not provided, defaults to the current URL. + /// + /// + /// A delegate to modify the deserialized representation of the ZAP Automation Framework YAML. + /// + /// + /// A delegate to run assertions on the one the scan finishes. + /// + /// + /// A instance containing the SARIF () report of the scan. + /// + public static async Task RunAndAssertSecurityScanAsync( + this UITestContext context, + string automationFrameworkYamlPath, + Uri startUri = null, + Func modifyYaml = null, + Action assertSecurityScanResult = null) + { + var securityScanningConfiguration = context.Configuration.SecurityScanningConfiguration; + + async Task CompositeModifyYaml(object configuration) + { + if (securityScanningConfiguration.ZapAutomationFrameworkYamlModifier != null) + { + await securityScanningConfiguration.ZapAutomationFrameworkYamlModifier(context, configuration); + } + + if (modifyYaml != null) await modifyYaml(configuration); + } + + SecurityScanResult result = null; + try + { + result = await context.RunSecurityScanAsync(automationFrameworkYamlPath, startUri, CompositeModifyYaml); + + if (assertSecurityScanResult != null) assertSecurityScanResult(result.SarifLog); + else securityScanningConfiguration.AssertSecurityScanResult(context, result.SarifLog); + } + catch (Exception ex) + { + if (result != null) context.AppendDirectoryToFailureDump(result.ReportsDirectoryPath); + throw new SecurityScanningAssertionException(ex); + } + } /// /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app. @@ -86,9 +179,13 @@ public static Task RunOpenApiSecurityScanAsync( /// The under the app where to start the scan from. If not provided, defaults to the current URL. /// /// - /// A delegate that may optionally modify the deserialized representation of the ZAP Automation Framework YAML. + /// A delegate to modify the deserialized representation of the ZAP Automation Framework YAML. /// - public static Task RunSecurityScanAsync( + /// + /// A instance containing the SARIF () report of the scan. + /// + public static Task RunSecurityScanAsync( this UITestContext context, string automationFrameworkYamlPath, Uri startUri = null, diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 2f538e142..cae954c64 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -1,7 +1,6 @@ using CliWrap; using Lombiq.HelpfulLibraries.Cli; using Lombiq.Tests.UI.Constants; -using Lombiq.Tests.UI.Extensions; using Lombiq.Tests.UI.Services; using Microsoft.CodeAnalysis.Sarif; using System; @@ -50,10 +49,13 @@ public sealed class ZapManager : IAsyncDisposable /// /// The under the app where to start the scan from. /// - /// A delegate that may optionally modify the deserialized representation of the ZAP Automation Framework YAML. + /// A delegate to modify the deserialized representation of the ZAP Automation Framework YAML. /// - /// The SARIF () report of the scan. - public Task RunSecurityScanAsync( + /// + /// A instance containing the SARIF () report of the scan. + /// + public Task RunSecurityScanAsync( UITestContext context, string automationFrameworkYamlPath, Uri startUri, @@ -72,13 +74,17 @@ public Task RunSecurityScanAsync( /// /// The of the currently executing test. /// - /// File system path to the YAML configuration file of ZAP's Automation Framework. See - /// for details. + /// File system path to the YAML configuration file of ZAP's Automation Framework. See for details. /// /// - /// A delegate that may optionally modify the deserialized representation of the ZAP Automation Framework YAML. + /// A delegate to modify the deserialized representation of the ZAP Automation Framework YAML. /// - public async Task RunSecurityScanAsync( + /// + /// A instance containing the SARIF () report of the scan. + /// + public async Task RunSecurityScanAsync( UITestContext context, string automationFrameworkYamlPath, Func modifyYaml = null) @@ -172,11 +178,7 @@ await _docker "Check the test output for details."); } - context.AppendDirectoryToFailureDump(reportsDirectoryPath); - - throw new Exception(); - return SarifLog.Load(jsonReports[0]); - //log.Runs.First().Results.Any(result => result.Kind == ResultKind.Fail); + return new SecurityScanResult(reportsDirectoryPath, SarifLog.Load(jsonReports[0])); } public ValueTask DisposeAsync() diff --git a/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs b/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs index 9aa85452c..9894159a0 100644 --- a/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs +++ b/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs @@ -1,4 +1,5 @@ using Lombiq.Tests.UI.Extensions; +using Lombiq.Tests.UI.SecurityScanning; using Lombiq.Tests.UI.Services.GitHub; using OpenQA.Selenium; using Shouldly; @@ -140,6 +141,8 @@ public class OrchardCoreUITestExecutorConfiguration public HtmlValidationConfiguration HtmlValidationConfiguration { get; set; } = new(); + public SecurityScanningConfiguration SecurityScanningConfiguration { get; set; } = new(); + /// /// Gets or sets a value indicating whether the test should verify the Orchard Core logs and the browser logs for /// errors after every page load. When enabled and there is an error the test is failed immediately which prevents From 4fbd32a9016715c006f9ef79404890e66ebdf0f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 15 Nov 2023 23:04:25 +0100 Subject: [PATCH 028/129] Making SecurityScanShouldPass pass --- Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index f37098b2c..9f569ef7a 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -4,6 +4,7 @@ using Lombiq.Tests.UI.SecurityScanning; using Lombiq.Tests.UI.Services; using OpenQA.Selenium; +using Shouldly; using System; using System.Threading.Tasks; using Xunit; @@ -23,10 +24,8 @@ public SecurityScanningTests(ITestOutputHelper testOutputHelper) [Theory, Chrome] public Task SecurityScanShouldPass(Browser browser) => ExecuteTestAfterSetupAsync( - async context => - { - await context.RunAndAssertBaselineSecurityScanAsync(); - }, + async context => await context.RunAndAssertBaselineSecurityScanAsync( + assertSecurityScanResult: sarifLog => sarifLog.Runs[0].Results.Count.ShouldBe(118)), browser); // Overriding the default setup so we can have a simpler site, simplifying the security scan for the purpose of this From 2c48541681b9d88b71b7860d29f7f69d9d25a224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 16 Nov 2023 01:40:02 +0100 Subject: [PATCH 029/129] Docs --- .../Tests/SecurityScanningTests.cs | 19 ++++++++++++++++++- Lombiq.Tests.UI/Docs/SecurityScanning.md | 16 ++++++++++++++-- Lombiq.Tests.UI/Docs/Troubleshooting.md | 4 ++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 9f569ef7a..179d024fd 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -12,6 +12,12 @@ namespace Lombiq.Tests.UI.Samples.Tests; +// Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) is the world's most widely used web app security scanner, and a +// fellow open-source project we can recommend. And you can use it right from UI tests, on the same app that's run for +// the tests! This is useful to find all kinds of security issues with your app. In this sample we'll see how, but be +// sure to also check out the corresponding documentation page: +// https://github.com/Lombiq/UI-Testing-Toolbox/blob/dev/Lombiq.Tests.UI/Docs/SecurityScanning.md. + // Note that security scanning has cross-platform support, but due to the limitations of virtualization under Windows in // GitHub Actions, these tests won't work there. They'll work on a Windows desktop though. public class SecurityScanningTests : UITestBase @@ -21,6 +27,16 @@ public SecurityScanningTests(ITestOutputHelper testOutputHelper) { } + // Let's see simple use case first: Running a built-in ZAP scan. + + // We're running one of ZAP's built-in scans, the Baseline scan. This, as the name suggests, provides some + // rudimentary security checks. While you can start with this, we recommend running the Full Scan, for which there + // similarly is an extension method as well. + + // Note how we specify an assertion too. This is because ZAP actually notices a few security issues with vanilla + // Orchard Core. These, however, are more like artifacts of running the app locally and without any real + // configuration. So, to make the test pass, we need to substitute the default assertion that would fail the test if + // any issue is found. [Theory, Chrome] public Task SecurityScanShouldPass(Browser browser) => ExecuteTestAfterSetupAsync( @@ -29,7 +45,8 @@ public Task SecurityScanShouldPass(Browser browser) => browser); // Overriding the default setup so we can have a simpler site, simplifying the security scan for the purpose of this - // demo. + // demo. For a real app's security scan you needn't (shouldn't) do this though; always run the scan on the actual + // app with everything set up how you run it in production. protected override Task ExecuteTestAfterSetupAsync( Func testAsync, Browser browser, diff --git a/Lombiq.Tests.UI/Docs/SecurityScanning.md b/Lombiq.Tests.UI/Docs/SecurityScanning.md index 16be1773d..d29d4a035 100644 --- a/Lombiq.Tests.UI/Docs/SecurityScanning.md +++ b/Lombiq.Tests.UI/Docs/SecurityScanning.md @@ -2,11 +2,23 @@ ## Overview -You an create detailed security scans of your app with [Zed Attack Proxy (ZAP)](https://www.zaproxy.org/) right from the Lombiq UI Testing Toolbox, with detailed reports. +You an create detailed security scans of your app with [Zed Attack Proxy (ZAP)](https://www.zaproxy.org/) right from the Lombiq UI Testing Toolbox, with nice reports. ZAP is the world's most widely used web app security scanner, and a fellow open-source project we can recommend. ![Sample ZAP security scan report](Attachments/ZapReportScreenshot.png) -- `Lombiq.Tests.UI.Samples` contains a demonstration of how to use security scanning. Check that out for code examples. - The most important default ZAP scans, Baseline, Full, GraphQL, and OpenAPI scans are included and readily usable. +- You can assert on scan results and thus fail the test if there are security warnings. - Since we use [ZAP's Automation Framework](https://www.zaproxy.org/docs/automate/automation-framework/) for configuration, you have complete and detailed control over how the scans are configured. - [SARIF](https://sarifweb.azurewebsites.net/) reports are available to integrate with other InfoSec tools. + +## Working with ZAP in the Lombiq UI Testing Toolbox + +- We recommend you first check out the related samples in the [`Lombiq.Tests.UI.Samples` project](../../Lombiq.Tests.UI.Samples). +- If you're new to ZAP, you can start learning by checking out [ZAP's getting started guide](https://www.zaproxy.org/getting-started/), as well as the [ZAP Chat Videos](https://www.zaproxy.org/zap-chat/). +- ZAP scans run with an internally managed browser instance, not in the browser launched by the test. +- While ZAP is fully managed for you, Docker needs to be available and running to host the ZAP instance. On your development machine you can install [Docker Desktop](https://www.docker.com/products/docker-desktop/). +- The scan of a website with even just 1-200 pages can take 15-30 minutes. So, be careful to fine-tune the ZAP configuration to make it suitable for your app. + +## Troubleshooting + + diff --git a/Lombiq.Tests.UI/Docs/Troubleshooting.md b/Lombiq.Tests.UI/Docs/Troubleshooting.md index cca05a2e2..3e66603e5 100644 --- a/Lombiq.Tests.UI/Docs/Troubleshooting.md +++ b/Lombiq.Tests.UI/Docs/Troubleshooting.md @@ -65,3 +65,7 @@ - The last monkey testing interaction before a failure is logged. You can correlate with the coordinates of it with the last page screenshot. - If you want to test the failed page granularly, you can write a test that navigates to that page and executes `context.TestCurrentPageAsMonkey(_monkeyTestingOptions, 12345);`, where `12345` is the random seed number that can be found in a failed test log. - It is also possible to set a larger time value to the `MonkeyTestingOptions.GremlinsAttackDelay` property in order to make gremlin interaction slower, thus allowing you to watch what's happening. + +## Security scanning + +Check out the [security scanning docs](SecurityScanning.md). From 868763a114ca527b8d27f76d583e1d1cdb6d86a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 16 Nov 2023 01:40:42 +0100 Subject: [PATCH 030/129] Adding assertion delegate that doesn't fail on HSTS not being set --- .../SecurityScanning/SecurityScanningConfiguration.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs index 688274722..212001de0 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs @@ -20,4 +20,11 @@ public class SecurityScanningConfiguration public static readonly Action AssertSecurityScanHasNoFails = (_, sarifLog) => sarifLog.Runs[0].Results.ShouldNotContain(result => result.Kind == ResultKind.Fail); + + // When running the app locally, HSTS is never set, so we'd get a "Strict-Transport-Security Header Not Set" fail. + // The rule is disabled in the default configs though. + public static readonly Action AssertSecurityScanHasNoFailsExceptHsts = + (_, sarifLog) => + sarifLog.Runs[0].Results.ShouldNotContain(result => + result.Kind == ResultKind.Fail && result.RuleId != "10035"); } From 0a5f60856fdd54a5399aeb1ab8bdc317ec96045f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 16 Nov 2023 02:57:26 +0100 Subject: [PATCH 031/129] Changing ugly object-based YAML manipulation to nicer YamlDocument-based one --- .../SecurityScanningConfiguration.cs | 3 +- ...SecurityScanningUITestContextExtensions.cs | 15 +-- .../SecurityScanning/ZapManager.cs | 103 ++++++++++-------- 3 files changed, 65 insertions(+), 56 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs index 212001de0..45d115b80 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs @@ -3,6 +3,7 @@ using Shouldly; using System; using System.Threading.Tasks; +using YamlDotNet.RepresentationModel; namespace Lombiq.Tests.UI.SecurityScanning; @@ -11,7 +12,7 @@ public class SecurityScanningConfiguration /// /// Gets or sets a delegate that may modify the deserialized representation of the ZAP Automation Framework YAML. /// - public Func ZapAutomationFrameworkYamlModifier { get; set; } + public Func ZapAutomationFrameworkYamlModifier { get; set; } /// /// Gets or sets a delegate to run assertions on the when security scanning happens. diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs index 2bf88f7c8..82ce686de 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs @@ -4,6 +4,7 @@ using Microsoft.CodeAnalysis.Sarif; using System; using System.Threading.Tasks; +using YamlDotNet.RepresentationModel; namespace Lombiq.Tests.UI.SecurityScanning; @@ -27,7 +28,7 @@ public static class SecurityScanningUITestContextExtensions public static Task RunAndAssertBaselineSecurityScanAsync( this UITestContext context, Uri startUri = null, - Func modifyYaml = null, + Func modifyYaml = null, Action assertSecurityScanResult = null) => context.RunAndAssertSecurityScanAsync( AutomationFrameworkYamlPaths.BaselineYamlPath, @@ -53,7 +54,7 @@ public static Task RunAndAssertBaselineSecurityScanAsync( public static Task RunAndAssertFullSecurityScanAsync( this UITestContext context, Uri startUri = null, - Func modifyYaml = null, + Func modifyYaml = null, Action assertSecurityScanResult = null) => context.RunAndAssertSecurityScanAsync( AutomationFrameworkYamlPaths.FullScanYamlPath, @@ -79,7 +80,7 @@ public static Task RunAndAssertFullSecurityScanAsync( public static Task RunAndAssertGraphQLSecurityScanAsync( this UITestContext context, Uri startUri = null, - Func modifyYaml = null, + Func modifyYaml = null, Action assertSecurityScanResult = null) => context.RunAndAssertSecurityScanAsync( AutomationFrameworkYamlPaths.GraphQLYamlPath, @@ -105,7 +106,7 @@ public static Task RunAndAssertGraphQLSecurityScanAsync( public static Task RunAndAssertOpenApiSecurityScanAsync( this UITestContext context, Uri startUri = null, - Func modifyYaml = null, + Func modifyYaml = null, Action assertSecurityScanResult = null) => context.RunAndAssertSecurityScanAsync( AutomationFrameworkYamlPaths.OpenAPIYamlPath, @@ -138,12 +139,12 @@ public static async Task RunAndAssertSecurityScanAsync( this UITestContext context, string automationFrameworkYamlPath, Uri startUri = null, - Func modifyYaml = null, + Func modifyYaml = null, Action assertSecurityScanResult = null) { var securityScanningConfiguration = context.Configuration.SecurityScanningConfiguration; - async Task CompositeModifyYaml(object configuration) + async Task CompositeModifyYaml(YamlDocument configuration) { if (securityScanningConfiguration.ZapAutomationFrameworkYamlModifier != null) { @@ -189,6 +190,6 @@ public static Task RunSecurityScanAsync( this UITestContext context, string automationFrameworkYamlPath, Uri startUri = null, - Func modifyYaml = null) => + Func modifyYaml = null) => context.ZapManager.RunSecurityScanAsync(context, automationFrameworkYamlPath, startUri ?? context.GetCurrentUri(), modifyYaml); } diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index cae954c64..60c59926d 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -12,7 +12,7 @@ using System.Threading; using System.Threading.Tasks; using Xunit.Abstractions; -using YamlDotNet.Serialization; +using YamlDotNet.RepresentationModel; namespace Lombiq.Tests.UI.SecurityScanning; @@ -59,7 +59,7 @@ public Task RunSecurityScanAsync( UITestContext context, string automationFrameworkYamlPath, Uri startUri, - Func modifyYaml = null) => + Func modifyYaml = null) => RunSecurityScanAsync( context, automationFrameworkYamlPath, @@ -87,7 +87,7 @@ public Task RunSecurityScanAsync( public async Task RunSecurityScanAsync( UITestContext context, string automationFrameworkYamlPath, - Func modifyYaml = null) + Func modifyYaml = null) { await EnsureInitializedAsync(); @@ -215,81 +215,88 @@ private async Task EnsureInitializedAsync() } } - private async Task PrepareYamlAsync(string yamlFilePath, Func modifyYaml) + private static void SetStartUrlInYaml(YamlDocument configuration, Uri startUri) { - var originalYaml = await File.ReadAllTextAsync(yamlFilePath, _cancellationTokenSource.Token); + var rootNode = (YamlMappingNode)configuration.RootNode; - var deserializer = new DeserializerBuilder().Build(); - // Deseralizing into a free-form object, not to potentially break unknown fields during reserialization. - var configuration = deserializer.Deserialize(originalYaml); + var contexts = (YamlSequenceNode)rootNode["env"]["contexts"]; - // Setting report directories to the conventional one and verifying that there's exactly one SARIF report. - List jobs = ((dynamic)configuration)["jobs"]; - var sarifReportCount = 0; + if (!contexts.Any()) + { + throw new ArgumentException( + "The supplied ZAP Automation Framework YAML file should contain at least one context."); + } - foreach (var job in jobs) + var currentContext = (YamlMappingNode)contexts[0]; + + if (contexts.Count() > 1) { - var jobDictionary = (Dictionary)job; + currentContext = (YamlMappingNode)contexts.FirstOrDefault(context => context["Name"].ToString() == "Default Context") + ?? currentContext; + } - if (!jobDictionary.TryGetValue("type", out var typeValue) || (string)typeValue != "report") continue; + // Setting URLs in the context. + // Setting includePaths in the context is not necessary because by default everything under urls will be scanned. - var parameters = (Dictionary)jobDictionary["parameters"]; - parameters["reportDir"] = _zapWorkingDirectoryPath + _zapReportsDirectoryName; + if (!currentContext.Children.ContainsKey("urls")) currentContext.Add("urls", new YamlSequenceNode()); - if ((string)parameters["template"] == "sarif-json") sarifReportCount++; - } + var urls = (YamlSequenceNode)currentContext["urls"]; + var urlsCount = urls.Count(); - if (sarifReportCount != 1) + if (urlsCount > 1) { throw new ArgumentException( - "The supplied ZAP Automation Framework YAML file should contain exactly one SARIF report job."); + "The context in the ZAP Automation Framework YAML file should contain at most a single url in the urls section."); } - if (modifyYaml != null) await modifyYaml(configuration); - - // Serializing the results. - var serializer = new SerializerBuilder().WithQuotingNecessaryStrings().Build(); - var updatedYaml = serializer.Serialize(configuration); + if (urlsCount == 1) urls.Children.Clear(); - await File.WriteAllTextAsync(yamlFilePath, updatedYaml, _cancellationTokenSource.Token); + urls.Add(startUri.ToString()); } - private static void SetStartUrlInYaml(object configuration, Uri startUri) + private static async Task PrepareYamlAsync(string yamlFilePath, Func modifyYaml) { - List contexts = ((dynamic)configuration)["env"]["contexts"]; + YamlDocument yamlDocument; - if (!contexts.Any()) + using (var streamReader = new StreamReader(yamlFilePath)) { - throw new ArgumentException( - "The supplied ZAP Automation Framework YAML file should contain at least one context."); + var yamlStream = new YamlStream(); + yamlStream.Load(streamReader); + // Using a free-form object instead of deserializing into a statically defined object, not to potentially + // break unknown fields during reserialization. + yamlDocument = yamlStream.Documents[0]; } - var context = (Dictionary)contexts[0]; + var rootNode = (YamlMappingNode)yamlDocument.RootNode; - if (contexts.Count > 1) - { - context = - (Dictionary)contexts - .Find(context => - ((Dictionary)context).TryGetValue("name", out var name) && (string)name == "Default Context") - ?? context; - } + // Setting report directories to the conventional one and verifying that there's exactly one SARIF report. - // Setting URLs in the context. - // Setting includePaths in the context is not necessary because by default everything under urls will be scanned. + var jobs = (IEnumerable)rootNode["jobs"]; + + var sarifReportCount = 0; + + foreach (var job in jobs) + { + if ((string)job["type"] != "report") continue; - if (!context.ContainsKey("urls")) context["urls"] = new List(); + var parameters = (YamlMappingNode)job["parameters"]; + ((YamlScalarNode)parameters["reportDir"]).Value = _zapWorkingDirectoryPath + _zapReportsDirectoryName; - var urls = (List)context["urls"]; + if ((string)parameters["template"] == "sarif-json") sarifReportCount++; + } - if (urls.Count > 1) + if (sarifReportCount != 1) { throw new ArgumentException( - "The context in the ZAP Automation Framework YAML file should contain at most a single url in the urls section."); + "The supplied ZAP Automation Framework YAML file should contain exactly one SARIF report job."); } - if (urls.Count == 1) urls.Clear(); + if (modifyYaml != null) await modifyYaml(yamlDocument); - urls.Add(startUri.ToString()); + using (var streamWriter = new StreamWriter(yamlFilePath)) + { + var yamlStream = new YamlStream(yamlDocument); + yamlStream.Save(streamWriter, assignAnchors: false); + } } } From f2b5407f35a3c94f3364c217e89c5b8ea05b73a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 16 Nov 2023 03:22:21 +0100 Subject: [PATCH 032/129] Simpler sample assertion --- Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 179d024fd..27614e296 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -41,7 +41,7 @@ public SecurityScanningTests(ITestOutputHelper testOutputHelper) public Task SecurityScanShouldPass(Browser browser) => ExecuteTestAfterSetupAsync( async context => await context.RunAndAssertBaselineSecurityScanAsync( - assertSecurityScanResult: sarifLog => sarifLog.Runs[0].Results.Count.ShouldBe(118)), + assertSecurityScanResult: sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeGreaterThan(100)), browser); // Overriding the default setup so we can have a simpler site, simplifying the security scan for the purpose of this From d1c851a26323ca90928f84fa519997bb8199edd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 16 Nov 2023 19:30:58 +0100 Subject: [PATCH 033/129] Docs --- Lombiq.Tests.UI/Docs/SecurityScanning.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lombiq.Tests.UI/Docs/SecurityScanning.md b/Lombiq.Tests.UI/Docs/SecurityScanning.md index d29d4a035..159d8fd32 100644 --- a/Lombiq.Tests.UI/Docs/SecurityScanning.md +++ b/Lombiq.Tests.UI/Docs/SecurityScanning.md @@ -14,11 +14,11 @@ You an create detailed security scans of your app with [Zed Attack Proxy (ZAP)]( ## Working with ZAP in the Lombiq UI Testing Toolbox - We recommend you first check out the related samples in the [`Lombiq.Tests.UI.Samples` project](../../Lombiq.Tests.UI.Samples). -- If you're new to ZAP, you can start learning by checking out [ZAP's getting started guide](https://www.zaproxy.org/getting-started/), as well as the [ZAP Chat Videos](https://www.zaproxy.org/zap-chat/). -- ZAP scans run with an internally managed browser instance, not in the browser launched by the test. +- If you're new to ZAP, you can start learning by checking out [ZAP's getting started guide](https://www.zaproxy.org/getting-started/), as well as the [ZAP Chat Videos](https://www.zaproxy.org/zap-chat/) (especially [ZAP Chat 06 Automation Introduction](https://www.youtube.com/watch?v=PnCbIAnauD8)). +- Be aware that ZAP scans run with an internally managed browser instance, not in the browser launched by the test. - While ZAP is fully managed for you, Docker needs to be available and running to host the ZAP instance. On your development machine you can install [Docker Desktop](https://www.docker.com/products/docker-desktop/). - The scan of a website with even just 1-200 pages can take 15-30 minutes. So, be careful to fine-tune the ZAP configuration to make it suitable for your app. ## Troubleshooting - +- If you're unsure what happens in a scan, run the [ZAP desktop app](https://www.zaproxy.org/download/) and load the Automation Framework plan's YAML file into it. If you use the default scans, then these will be available under the build output directory (like _bin/Debug_) under _SecurityScanning/AutomationFrameworkPlans_. Then, you can open and run them as demonstrated [in this video](https://youtu.be/PnCbIAnauD8?si=u0vi63Uvv9wZINzb&t=1173). From 35f2e8e1b432183802e542faa05504bab5638019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 16 Nov 2023 19:33:12 +0100 Subject: [PATCH 034/129] Fixing that Automation Framework plan YAMLs were called "configuration files" --- Lombiq.Tests.UI/Lombiq.Tests.UI.csproj | 16 ++--- .../AutomationFrameworkPlanPaths.cs | 13 ++++ .../Baseline.yml | 44 ++++---------- .../FullScan.yml | 0 .../GraphQL.yml | 0 .../OpenAPI.yml | 0 .../AutomationFrameworkYamlPaths.cs | 13 ---- .../SecurityScanningConfiguration.cs | 2 +- ...SecurityScanningUITestContextExtensions.cs | 60 +++++++++---------- .../SecurityScanning/ZapManager.cs | 22 +++---- 10 files changed, 76 insertions(+), 94 deletions(-) create mode 100644 Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanPaths.cs rename Lombiq.Tests.UI/SecurityScanning/{AutomationFrameworkYamls => AutomationFrameworkPlans}/Baseline.yml (78%) rename Lombiq.Tests.UI/SecurityScanning/{AutomationFrameworkYamls => AutomationFrameworkPlans}/FullScan.yml (100%) rename Lombiq.Tests.UI/SecurityScanning/{AutomationFrameworkYamls => AutomationFrameworkPlans}/GraphQL.yml (100%) rename Lombiq.Tests.UI/SecurityScanning/{AutomationFrameworkYamls => AutomationFrameworkPlans}/OpenAPI.yml (100%) delete mode 100644 Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamlPaths.cs diff --git a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj index bf12db5d3..253cd56f0 100644 --- a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj +++ b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj @@ -44,16 +44,16 @@ PreserveNewest true - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest @@ -128,10 +128,10 @@ - - - - + + + + diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanPaths.cs b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanPaths.cs new file mode 100644 index 000000000..09a2cc65f --- /dev/null +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanPaths.cs @@ -0,0 +1,13 @@ +using System.IO; + +namespace Lombiq.Tests.UI.SecurityScanning; + +public static class AutomationFrameworkPlanPaths +{ + private static readonly string AutomationFrameworkPlansPath = Path.Combine("SecurityScanning", "AutomationFrameworkPlans"); + + public static readonly string BaselinePlanPath = Path.Combine(AutomationFrameworkPlansPath, "Baseline.yml"); + public static readonly string FullScanPlanPath = Path.Combine(AutomationFrameworkPlansPath, "FullScan.yml"); + public static readonly string GraphQLPlanPath = Path.Combine(AutomationFrameworkPlansPath, "GraphQL.yml"); + public static readonly string OpenAPIPlanPath = Path.Combine(AutomationFrameworkPlansPath, "OpenAPI.yml"); +} diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml similarity index 78% rename from Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml rename to Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml index 7bb3b8bc6..74305624a 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/Baseline.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml @@ -3,8 +3,10 @@ env: contexts: - name: "Default Context" urls: - - "" - excludePaths: [] + - "https://localhost:44335/" + includePaths: [] + excludePaths: + - ".*blog.*" authentication: parameters: {} verification: @@ -30,41 +32,42 @@ jobs: name: "passiveScan-config" type: "passiveScan-config" - parameters: {} - name: "spider" - type: "spider" tests: - onFail: "INFO" statistic: "automation.spider.urls.added" site: "" operator: ">=" value: 100 - name: "At least 100 URLs found" type: "stats" + name: "At least 100 URLs found" + name: "spider" + type: "spider" - parameters: maxDuration: 60 maxCrawlDepth: 10 numberOfBrowsers: 64 inScopeOnly: true - runOnlyIfModern: true - name: "spiderAjax" - type: "spiderAjax" tests: - onFail: "INFO" statistic: "spiderAjax.urls.added" site: "" operator: ">=" value: 100 - name: "At least 100 URLs found" type: "stats" + name: "At least 100 URLs found" + name: "spiderAjax" + type: "spiderAjax" - parameters: {} name: "passiveScan-wait" type: "passiveScan-wait" - parameters: - reportDir: "/zap/wrk/reports" template: "modern" theme: "corporate" + reportDir: "C:\\Users\\lehoc\\Downloads" + reportFile: "" reportTitle: "ZAP Scanning Report" reportDescription: "" + displayReport: false risks: - "low" - "medium" @@ -84,25 +87,4 @@ jobs: - "statistics" name: "report" type: "report" -- parameters: - template: "sarif-json" - reportDir: "/zap/wrk/reports" - reportFile: "" - reportTitle: "ZAP Scanning Report" - reportDescription: "" - displayReport: false - risks: - - "info" - - "low" - - "medium" - - "high" - confidences: - - "falsepositive" - - "low" - - "medium" - - "high" - - "confirmed" - sites: [] - name: "sarifReport" - type: "report" diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/FullScan.yml similarity index 100% rename from Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/FullScan.yml rename to Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/FullScan.yml diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/GraphQL.yml similarity index 100% rename from Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/GraphQL.yml rename to Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/GraphQL.yml diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/OpenAPI.yml similarity index 100% rename from Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamls/OpenAPI.yml rename to Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/OpenAPI.yml diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamlPaths.cs b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamlPaths.cs deleted file mode 100644 index f31bdba52..000000000 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkYamlPaths.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.IO; - -namespace Lombiq.Tests.UI.SecurityScanning; - -public static class AutomationFrameworkYamlPaths -{ - private static readonly string AutomationFrameworkYamlsPath = Path.Combine("SecurityScanning", "AutomationFrameworkYamls"); - - public static readonly string BaselineYamlPath = Path.Combine(AutomationFrameworkYamlsPath, "Baseline.yml"); - public static readonly string FullScanYamlPath = Path.Combine(AutomationFrameworkYamlsPath, "FullScan.yml"); - public static readonly string GraphQLYamlPath = Path.Combine(AutomationFrameworkYamlsPath, "GraphQL.yml"); - public static readonly string OpenAPIYamlPath = Path.Combine(AutomationFrameworkYamlsPath, "OpenAPI.yml"); -} diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs index 45d115b80..22f92bcd1 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs @@ -10,7 +10,7 @@ namespace Lombiq.Tests.UI.SecurityScanning; public class SecurityScanningConfiguration { /// - /// Gets or sets a delegate that may modify the deserialized representation of the ZAP Automation Framework YAML. + /// Gets or sets a delegate that may modify the deserialized representation of the ZAP Automation Framework plan in YAML. /// public Func ZapAutomationFrameworkYamlModifier { get; set; } diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs index 82ce686de..a5807c6b6 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs @@ -19,8 +19,8 @@ public static class SecurityScanningUITestContextExtensions /// /// The under the app where to start the scan from. If not provided, defaults to the current URL. /// - /// - /// A delegate to modify the deserialized representation of the ZAP Automation Framework YAML. + /// + /// A delegate to modify the deserialized representation of the ZAP Automation Framework plan in YAML. /// /// /// A delegate to run assertions on the one the scan finishes. @@ -28,12 +28,12 @@ public static class SecurityScanningUITestContextExtensions public static Task RunAndAssertBaselineSecurityScanAsync( this UITestContext context, Uri startUri = null, - Func modifyYaml = null, + Func modifyPlan = null, Action assertSecurityScanResult = null) => context.RunAndAssertSecurityScanAsync( - AutomationFrameworkYamlPaths.BaselineYamlPath, + AutomationFrameworkPlanPaths.BaselinePlanPath, startUri, - modifyYaml, + modifyPlan, assertSecurityScanResult); /// @@ -45,8 +45,8 @@ public static Task RunAndAssertBaselineSecurityScanAsync( /// /// The under the app where to start the scan from. If not provided, defaults to the current URL. /// - /// - /// A delegate to modify the deserialized representation of the ZAP Automation Framework YAML. + /// + /// A delegate to modify the deserialized representation of the ZAP Automation Framework plan in YAML. /// /// /// A delegate to run assertions on the one the scan finishes. @@ -54,12 +54,12 @@ public static Task RunAndAssertBaselineSecurityScanAsync( public static Task RunAndAssertFullSecurityScanAsync( this UITestContext context, Uri startUri = null, - Func modifyYaml = null, + Func modifyPlan = null, Action assertSecurityScanResult = null) => context.RunAndAssertSecurityScanAsync( - AutomationFrameworkYamlPaths.FullScanYamlPath, + AutomationFrameworkPlanPaths.FullScanPlanPath, startUri, - modifyYaml, + modifyPlan, assertSecurityScanResult); /// @@ -71,8 +71,8 @@ public static Task RunAndAssertFullSecurityScanAsync( /// /// The under the app where to start the scan from. If not provided, defaults to the current URL. /// - /// - /// A delegate to modify the deserialized representation of the ZAP Automation Framework YAML. + /// + /// A delegate to modify the deserialized representation of the ZAP Automation Framework plan in YAML. /// /// /// A delegate to run assertions on the one the scan finishes. @@ -80,12 +80,12 @@ public static Task RunAndAssertFullSecurityScanAsync( public static Task RunAndAssertGraphQLSecurityScanAsync( this UITestContext context, Uri startUri = null, - Func modifyYaml = null, + Func modifyPlan = null, Action assertSecurityScanResult = null) => context.RunAndAssertSecurityScanAsync( - AutomationFrameworkYamlPaths.GraphQLYamlPath, + AutomationFrameworkPlanPaths.GraphQLPlanPath, startUri, - modifyYaml, + modifyPlan, assertSecurityScanResult); /// @@ -97,8 +97,8 @@ public static Task RunAndAssertGraphQLSecurityScanAsync( /// /// The under the app where to start the scan from. If not provided, defaults to the current URL. /// - /// - /// A delegate to modify the deserialized representation of the ZAP Automation Framework YAML. + /// + /// A delegate to modify the deserialized representation of the ZAP Automation Framework plan in YAML. /// /// /// A delegate to run assertions on the one the scan finishes. @@ -106,12 +106,12 @@ public static Task RunAndAssertGraphQLSecurityScanAsync( public static Task RunAndAssertOpenApiSecurityScanAsync( this UITestContext context, Uri startUri = null, - Func modifyYaml = null, + Func modifyPlan = null, Action assertSecurityScanResult = null) => context.RunAndAssertSecurityScanAsync( - AutomationFrameworkYamlPaths.OpenAPIYamlPath, + AutomationFrameworkPlanPaths.OpenAPIPlanPath, startUri, - modifyYaml, + modifyPlan, assertSecurityScanResult); /// @@ -125,8 +125,8 @@ public static Task RunAndAssertOpenApiSecurityScanAsync( /// /// The under the app where to start the scan from. If not provided, defaults to the current URL. /// - /// - /// A delegate to modify the deserialized representation of the ZAP Automation Framework YAML. + /// + /// A delegate to modify the deserialized representation of the ZAP Automation Framework plan in YAML. /// /// /// A delegate to run assertions on the one the scan finishes. @@ -139,25 +139,25 @@ public static async Task RunAndAssertSecurityScanAsync( this UITestContext context, string automationFrameworkYamlPath, Uri startUri = null, - Func modifyYaml = null, + Func modifyPlan = null, Action assertSecurityScanResult = null) { var securityScanningConfiguration = context.Configuration.SecurityScanningConfiguration; - async Task CompositeModifyYaml(YamlDocument configuration) + async Task CompositemodifyPlan(YamlDocument configuration) { if (securityScanningConfiguration.ZapAutomationFrameworkYamlModifier != null) { await securityScanningConfiguration.ZapAutomationFrameworkYamlModifier(context, configuration); } - if (modifyYaml != null) await modifyYaml(configuration); + if (modifyPlan != null) await modifyPlan(configuration); } SecurityScanResult result = null; try { - result = await context.RunSecurityScanAsync(automationFrameworkYamlPath, startUri, CompositeModifyYaml); + result = await context.RunSecurityScanAsync(automationFrameworkYamlPath, startUri, CompositemodifyPlan); if (assertSecurityScanResult != null) assertSecurityScanResult(result.SarifLog); else securityScanningConfiguration.AssertSecurityScanResult(context, result.SarifLog); @@ -179,8 +179,8 @@ async Task CompositeModifyYaml(YamlDocument configuration) /// /// The under the app where to start the scan from. If not provided, defaults to the current URL. /// - /// - /// A delegate to modify the deserialized representation of the ZAP Automation Framework YAML. + /// + /// A delegate to modify the deserialized representation of the ZAP Automation Framework plan in YAML. /// /// /// A instance containing the SARIF ( RunSecurityScanAsync( this UITestContext context, string automationFrameworkYamlPath, Uri startUri = null, - Func modifyYaml = null) => - context.ZapManager.RunSecurityScanAsync(context, automationFrameworkYamlPath, startUri ?? context.GetCurrentUri(), modifyYaml); + Func modifyPlan = null) => + context.ZapManager.RunSecurityScanAsync(context, automationFrameworkYamlPath, startUri ?? context.GetCurrentUri(), modifyPlan); } diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 60c59926d..2cbe55cd3 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -48,8 +48,8 @@ public sealed class ZapManager : IAsyncDisposable /// for details. /// /// The under the app where to start the scan from. - /// - /// A delegate to modify the deserialized representation of the ZAP Automation Framework YAML. + /// + /// A delegate to modify the deserialized representation of the ZAP Automation Framework plan in YAML. /// /// /// A instance containing the SARIF ( RunSecurityScanAsync( UITestContext context, string automationFrameworkYamlPath, Uri startUri, - Func modifyYaml = null) => + Func modifyPlan = null) => RunSecurityScanAsync( context, automationFrameworkYamlPath, async configuration => { SetStartUrlInYaml(configuration, startUri); - if (modifyYaml != null) await modifyYaml(configuration); + if (modifyPlan != null) await modifyPlan(configuration); }); /// @@ -77,8 +77,8 @@ public Task RunSecurityScanAsync( /// File system path to the YAML configuration file of ZAP's Automation Framework. See for details. /// - /// - /// A delegate to modify the deserialized representation of the ZAP Automation Framework YAML. + /// + /// A delegate to modify the deserialized representation of the ZAP Automation Framework plan in YAML. /// /// /// A instance containing the SARIF ( RunSecurityScanAsync( public async Task RunSecurityScanAsync( UITestContext context, string automationFrameworkYamlPath, - Func modifyYaml = null) + Func modifyPlan = null) { await EnsureInitializedAsync(); if (string.IsNullOrEmpty(automationFrameworkYamlPath)) { - automationFrameworkYamlPath = AutomationFrameworkYamlPaths.BaselineYamlPath; + automationFrameworkYamlPath = AutomationFrameworkPlanPaths.BaselinePlanPath; } var mountedDirectoryPath = DirectoryPaths.GetTempSubDirectoryPath(context.Id, "Zap"); @@ -104,7 +104,7 @@ public async Task RunSecurityScanAsync( File.Copy(automationFrameworkYamlPath, yamlFileCopyPath, overwrite: true); - await PrepareYamlAsync(yamlFileCopyPath, modifyYaml); + await PrepareYamlAsync(yamlFileCopyPath, modifyPlan); // Explanation on the CLI arguments used below: // - --add-host and --network host: Lets us connect to the host OS's localhost, where the OC app runs, with @@ -254,7 +254,7 @@ private static void SetStartUrlInYaml(YamlDocument configuration, Uri startUri) urls.Add(startUri.ToString()); } - private static async Task PrepareYamlAsync(string yamlFilePath, Func modifyYaml) + private static async Task PrepareYamlAsync(string yamlFilePath, Func modifyPlan) { YamlDocument yamlDocument; @@ -291,7 +291,7 @@ private static async Task PrepareYamlAsync(string yamlFilePath, Func Date: Thu, 16 Nov 2023 19:53:52 +0100 Subject: [PATCH 035/129] Making sure Automation Framework Plans are copied to the NuGet package too --- Lombiq.Tests.UI/Lombiq.Tests.UI.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj index 253cd56f0..0786f8b4e 100644 --- a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj +++ b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj @@ -46,15 +46,19 @@ PreserveNewest + true PreserveNewest + true PreserveNewest + true PreserveNewest + true PreserveNewest From 5ed1520a5b37545df1042f5024957020d64d6e3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 16 Nov 2023 19:54:26 +0100 Subject: [PATCH 036/129] Restoring the Baseline plan --- .../AutomationFrameworkPlans/Baseline.yml | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml index 74305624a..7bb3b8bc6 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml @@ -3,10 +3,8 @@ env: contexts: - name: "Default Context" urls: - - "https://localhost:44335/" - includePaths: [] - excludePaths: - - ".*blog.*" + - "" + excludePaths: [] authentication: parameters: {} verification: @@ -32,42 +30,41 @@ jobs: name: "passiveScan-config" type: "passiveScan-config" - parameters: {} + name: "spider" + type: "spider" tests: - onFail: "INFO" statistic: "automation.spider.urls.added" site: "" operator: ">=" value: 100 - type: "stats" name: "At least 100 URLs found" - name: "spider" - type: "spider" + type: "stats" - parameters: maxDuration: 60 maxCrawlDepth: 10 numberOfBrowsers: 64 inScopeOnly: true + runOnlyIfModern: true + name: "spiderAjax" + type: "spiderAjax" tests: - onFail: "INFO" statistic: "spiderAjax.urls.added" site: "" operator: ">=" value: 100 - type: "stats" name: "At least 100 URLs found" - name: "spiderAjax" - type: "spiderAjax" + type: "stats" - parameters: {} name: "passiveScan-wait" type: "passiveScan-wait" - parameters: + reportDir: "/zap/wrk/reports" template: "modern" theme: "corporate" - reportDir: "C:\\Users\\lehoc\\Downloads" - reportFile: "" reportTitle: "ZAP Scanning Report" reportDescription: "" - displayReport: false risks: - "low" - "medium" @@ -87,4 +84,25 @@ jobs: - "statistics" name: "report" type: "report" +- parameters: + template: "sarif-json" + reportDir: "/zap/wrk/reports" + reportFile: "" + reportTitle: "ZAP Scanning Report" + reportDescription: "" + displayReport: false + risks: + - "info" + - "low" + - "medium" + - "high" + confidences: + - "falsepositive" + - "low" + - "medium" + - "high" + - "confirmed" + sites: [] + name: "sarifReport" + type: "report" From 361d165bc75f85eb7c9e296bf46179843917f961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 16 Nov 2023 19:59:04 +0100 Subject: [PATCH 037/129] Less oblivious assertion in sample test --- Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 27614e296..289babc39 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -41,7 +41,7 @@ public SecurityScanningTests(ITestOutputHelper testOutputHelper) public Task SecurityScanShouldPass(Browser browser) => ExecuteTestAfterSetupAsync( async context => await context.RunAndAssertBaselineSecurityScanAsync( - assertSecurityScanResult: sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeGreaterThan(100)), + assertSecurityScanResult: sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeLessThan(200)), browser); // Overriding the default setup so we can have a simpler site, simplifying the security scan for the purpose of this From 74693fa25174edcc9db095b87873870a3d849eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 16 Nov 2023 20:15:00 +0100 Subject: [PATCH 038/129] Disabling the Strict-Transport-Security Header Not Set rule since it's not applicable for a localhost app --- Lombiq.Tests.UI/Docs/SecurityScanning.md | 2 +- .../SecurityScanning/AutomationFrameworkPlans/Baseline.yml | 5 ++++- .../SecurityScanning/AutomationFrameworkPlans/FullScan.yml | 5 ++++- .../SecurityScanning/AutomationFrameworkPlans/GraphQL.yml | 5 ++++- .../SecurityScanning/AutomationFrameworkPlans/OpenAPI.yml | 5 ++++- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Lombiq.Tests.UI/Docs/SecurityScanning.md b/Lombiq.Tests.UI/Docs/SecurityScanning.md index 159d8fd32..609936d32 100644 --- a/Lombiq.Tests.UI/Docs/SecurityScanning.md +++ b/Lombiq.Tests.UI/Docs/SecurityScanning.md @@ -6,7 +6,7 @@ You an create detailed security scans of your app with [Zed Attack Proxy (ZAP)]( ![Sample ZAP security scan report](Attachments/ZapReportScreenshot.png) -- The most important default ZAP scans, Baseline, Full, GraphQL, and OpenAPI scans are included and readily usable. +- The most important default ZAP scans, Baseline, Full, GraphQL, and OpenAPI scans are included and readily usable. Note that these are modified to be more applicable to Orchard Core apps run on localhost during a UI testing scenario; if you want to scan remote (and especially production) apps, then you'll need to create your own scans based on ZAP's default ones. - You can assert on scan results and thus fail the test if there are security warnings. - Since we use [ZAP's Automation Framework](https://www.zaproxy.org/docs/automate/automation-framework/) for configuration, you have complete and detailed control over how the scans are configured. - [SARIF](https://sarifweb.azurewebsites.net/) reports are available to integrate with other InfoSec tools. diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml index 7bb3b8bc6..e8756ba29 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml @@ -26,7 +26,10 @@ jobs: scanOnlyInScope: true enableTags: false disableAllRules: false - rules: [] + rules: + - id: 10035 + name: "Strict-Transport-Security Header" + threshold: "off" name: "passiveScan-config" type: "passiveScan-config" - parameters: {} diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/FullScan.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/FullScan.yml index bc52140dc..5c968761e 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/FullScan.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/FullScan.yml @@ -26,7 +26,10 @@ jobs: scanOnlyInScope: true enableTags: false disableAllRules: false - rules: [] + rules: + - id: 10035 + name: "Strict-Transport-Security Header" + threshold: "off" name: "passiveScan-config" type: "passiveScan-config" - parameters: {} diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/GraphQL.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/GraphQL.yml index 8e5e712af..07e4362d6 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/GraphQL.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/GraphQL.yml @@ -26,7 +26,10 @@ jobs: scanOnlyInScope: true enableTags: false disableAllRules: false - rules: [] + rules: + - id: 10035 + name: "Strict-Transport-Security Header" + threshold: "off" name: "passiveScan-config" type: "passiveScan-config" - parameters: diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/OpenAPI.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/OpenAPI.yml index 08a04ad7e..ff260ba84 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/OpenAPI.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/OpenAPI.yml @@ -26,7 +26,10 @@ jobs: scanOnlyInScope: true enableTags: false disableAllRules: false - rules: [] + rules: + - id: 10035 + name: "Strict-Transport-Security Header" + threshold: "off" name: "passiveScan-config" type: "passiveScan-config" - parameters: {} From 0e4f77b823f631d255fe1b5a56522de6e7074346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 16 Nov 2023 22:26:01 +0100 Subject: [PATCH 039/129] Removing the spiderAjax job from plans by default but adding configuration to re-add it, as well as configure excludePaths --- .../Tests/SecurityScanningTests.cs | 39 +++++++++++--- Lombiq.Tests.UI/Docs/SecurityScanning.md | 2 +- Lombiq.Tests.UI/Lombiq.Tests.UI.csproj | 13 ++--- .../SpiderAjax.yml | 8 +++ .../AutomationFrameworkPlanFragmentsPaths.cs | 11 ++++ .../AutomationFrameworkPlans/Baseline.yml | 16 ------ .../AutomationFrameworkPlans/FullScan.yml | 16 ------ ...SecurityScanningUITestContextExtensions.cs | 4 +- .../YamlDocumentExtensions.cs | 52 +++++++++++++++++++ .../SecurityScanning/YamlHelper.cs | 38 ++++++++++++++ .../SecurityScanning/ZapManager.cs | 49 ++++------------- 11 files changed, 158 insertions(+), 90 deletions(-) create mode 100644 Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragments/SpiderAjax.yml create mode 100644 Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragmentsPaths.cs create mode 100644 Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs create mode 100644 Lombiq.Tests.UI/SecurityScanning/YamlHelper.cs diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 289babc39..d5fbc8501 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -33,15 +33,33 @@ public SecurityScanningTests(ITestOutputHelper testOutputHelper) // rudimentary security checks. While you can start with this, we recommend running the Full Scan, for which there // similarly is an extension method as well. - // Note how we specify an assertion too. This is because ZAP actually notices a few security issues with vanilla - // Orchard Core. These, however, are more like artifacts of running the app locally and without any real - // configuration. So, to make the test pass, we need to substitute the default assertion that would fail the test if - // any issue is found. + // If you're new to security scanning, starting with exactly this is probably a good idea. Most possibly your app + // will fail the scan, but don't worry! You'll get a nice report about the findings in the failure dump. [Theory, Chrome] - public Task SecurityScanShouldPass(Browser browser) => + public Task BasicSecurityScanShouldPass(Browser browser) => + ExecuteTestAfterSetupAsync( + async context => await context.RunAndAssertBaselineSecurityScanAsync(), + browser); + + // Time for some custom configuration! While this scan also runs the Baseline scan, it does this with several + // adjustments: + // - The assertion on the scan results is custom. Use this if you (conditionally) want to assert on the results + // differently from the default "no scanning alert is allowed" assertion. + // - The plan is modified to also run ZAP's Ajax Spider + // (https://www.zaproxy.org/docs/desktop/addons/ajax-spider/automation/). This is usually not just unnecessary for + // a website that's not an SPA, but also slows the scan down by a lot. + // - The plan is also modified with an exclusion regex pattern. You can use this to exclude certain URLs from the + // scan. + [Theory, Chrome] + public Task SecurityScanWithCustomConfigurationShouldPass(Browser browser) => ExecuteTestAfterSetupAsync( async context => await context.RunAndAssertBaselineSecurityScanAsync( - assertSecurityScanResult: sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeLessThan(200)), + assertSecurityScanResult: sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeLessThan(2), + modifyPlan: plan => + { + plan.AddSpiderAjaxAfterSpider().AddExcludePathsRegex(".*blog.*"); + return Task.CompletedTask; + }), browser); // Overriding the default setup so we can have a simpler site, simplifying the security scan for the purpose of this @@ -74,6 +92,15 @@ protected override Task ExecuteTestAfterSetupAsync( async configuration => { configuration.HtmlValidationConfiguration.RunHtmlValidationAssertionOnAllPageChanges = false; + + // Note how we specify an assertion too. This is because ZAP actually notices a few security issues with + // vanilla Orchard Core. These, however, are more like artifacts of running the app locally and out of + // the box without any real configuration. So, to make the tests pass, we need to override the default + // assertion that would fail the test if any issue is found. + + // Don't do this at home! Fix the issues instead. This is only here to have a smoother demo. + configuration.SecurityScanningConfiguration.AssertSecurityScanResult = (_, _) => { }; + await changeConfigurationAsync(configuration); }); } diff --git a/Lombiq.Tests.UI/Docs/SecurityScanning.md b/Lombiq.Tests.UI/Docs/SecurityScanning.md index 609936d32..49c7dfef7 100644 --- a/Lombiq.Tests.UI/Docs/SecurityScanning.md +++ b/Lombiq.Tests.UI/Docs/SecurityScanning.md @@ -6,7 +6,7 @@ You an create detailed security scans of your app with [Zed Attack Proxy (ZAP)]( ![Sample ZAP security scan report](Attachments/ZapReportScreenshot.png) -- The most important default ZAP scans, Baseline, Full, GraphQL, and OpenAPI scans are included and readily usable. Note that these are modified to be more applicable to Orchard Core apps run on localhost during a UI testing scenario; if you want to scan remote (and especially production) apps, then you'll need to create your own scans based on ZAP's default ones. +- The most important default ZAP scans, Baseline, Full, GraphQL, and OpenAPI scans are included and readily usable. Note that these are modified to be more applicable to Orchard Core apps run on localhost during a UI testing scenario (notably, [`ajaxSpider`](https://www.zaproxy.org/docs/desktop/addons/ajax-spider/automation/) is removed, since most Orchard Core apps don't need it but it takes a lot of time). If you want to scan remote (and especially production) apps, then you'll need to create your own scans based on ZAP's default ones. These can then be run from inside UI tests too. - You can assert on scan results and thus fail the test if there are security warnings. - Since we use [ZAP's Automation Framework](https://www.zaproxy.org/docs/automate/automation-framework/) for configuration, you have complete and detailed control over how the scans are configured. - [SARIF](https://sarifweb.azurewebsites.net/) reports are available to integrate with other InfoSec tools. diff --git a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj index 0786f8b4e..158cfa98e 100644 --- a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj +++ b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj @@ -44,19 +44,11 @@ PreserveNewest true - + PreserveNewest true - - PreserveNewest - true - - - PreserveNewest - true - - + PreserveNewest true @@ -132,6 +124,7 @@ + diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragments/SpiderAjax.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragments/SpiderAjax.yml new file mode 100644 index 000000000..aa35251d8 --- /dev/null +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragments/SpiderAjax.yml @@ -0,0 +1,8 @@ +parameters: + maxDuration: 60 + maxCrawlDepth: 10 + numberOfBrowsers: 64 + inScopeOnly: true + runOnlyIfModern: true +name: "spiderAjax" +type: "spiderAjax" diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragmentsPaths.cs b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragmentsPaths.cs new file mode 100644 index 000000000..1bb41f121 --- /dev/null +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragmentsPaths.cs @@ -0,0 +1,11 @@ +using System.IO; + +namespace Lombiq.Tests.UI.SecurityScanning; + +public static class AutomationFrameworkPlanFragmentsPaths +{ + private static readonly string AutomationFrameworkPlanFragmentsPath = + Path.Combine("SecurityScanning", "AutomationFrameworkPlanFragments"); + + public static readonly string SpiderAjaxPath = Path.Combine(AutomationFrameworkPlanFragmentsPath, "SpiderAjax.yml"); +} diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml index e8756ba29..0b2a99a2a 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml @@ -43,22 +43,6 @@ jobs: value: 100 name: "At least 100 URLs found" type: "stats" -- parameters: - maxDuration: 60 - maxCrawlDepth: 10 - numberOfBrowsers: 64 - inScopeOnly: true - runOnlyIfModern: true - name: "spiderAjax" - type: "spiderAjax" - tests: - - onFail: "INFO" - statistic: "spiderAjax.urls.added" - site: "" - operator: ">=" - value: 100 - name: "At least 100 URLs found" - type: "stats" - parameters: {} name: "passiveScan-wait" type: "passiveScan-wait" diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/FullScan.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/FullScan.yml index 5c968761e..562b14879 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/FullScan.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/FullScan.yml @@ -43,22 +43,6 @@ jobs: value: 100 name: "At least 100 URLs found" type: "stats" -- parameters: - maxDuration: 60 - maxCrawlDepth: 10 - numberOfBrowsers: 64 - inScopeOnly: true - runOnlyIfModern: true - name: "spiderAjax" - type: "spiderAjax" - tests: - - onFail: "INFO" - statistic: "spiderAjax.urls.added" - site: "" - operator: ">=" - value: 100 - name: "At least 100 URLs found" - type: "stats" - parameters: {} name: "passiveScan-wait" type: "passiveScan-wait" diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs index a5807c6b6..ed9a8bfb9 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs @@ -12,7 +12,7 @@ public static class SecurityScanningUITestContextExtensions { /// /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app with the Baseline - /// Automation Framework profile and runs assertions on the result (see for the official docs on the legacy version of this /// scan). /// @@ -38,7 +38,7 @@ public static Task RunAndAssertBaselineSecurityScanAsync( /// /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app with the Full Scan - /// Automation Framework profile and runs assertions on the result (see for the official docs on the legacy version of this /// scan). /// diff --git a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs new file mode 100644 index 000000000..62da89274 --- /dev/null +++ b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; +using YamlDotNet.RepresentationModel; + +namespace Lombiq.Tests.UI.SecurityScanning; + +public static class YamlDocumentExtensions +{ + /// + /// Adds the ZAP Ajax Spider + /// to the ZAP Automation Framework plan, just after the "spider" job. + /// + /// + /// If no job named "spider" is found in the ZAP Automation Framework plan. + /// + public static YamlDocument AddSpiderAjaxAfterSpider(this YamlDocument yamlDocument) + { + var jobs = (YamlSequenceNode)yamlDocument.GetRootNode()["jobs"]; + + var spiderJob = + jobs.FirstOrDefault(job => (string)job["name"] == "spider") ?? + throw new ArgumentException( + "No job named \"spider\" found in the Automation Framework Plan. We can only add ajaxSpider immediately after it."); + + var spiderIndex = jobs.Children.IndexOf(spiderJob); + var spiderAjax = YamlHelper.LoadDocument(AutomationFrameworkPlanFragmentsPaths.SpiderAjaxPath); + jobs.Children.Insert(spiderIndex + 1, spiderAjax.GetRootNode()); + + return yamlDocument; + } + + /// + /// Adds one or more regex patterns to the ZAP Automation Framework plan's excludePaths config under the current + /// context. + /// + public static YamlDocument AddExcludePathsRegex(this YamlDocument yamlDocument, params string[] excludePathsPatterns) + { + var currentContext = YamlHelper.GetCurrentContext(yamlDocument); + + if (!currentContext.Children.ContainsKey("excludePaths")) currentContext.Add("excludePaths", new YamlSequenceNode()); + + var excludePaths = (YamlSequenceNode)currentContext["excludePaths"]; + foreach (var pattern in excludePathsPatterns) + { + excludePaths.Add(pattern); + } + + return yamlDocument; + } + + public static YamlMappingNode GetRootNode(this YamlDocument yamlDocument) => (YamlMappingNode)yamlDocument.RootNode; +} diff --git a/Lombiq.Tests.UI/SecurityScanning/YamlHelper.cs b/Lombiq.Tests.UI/SecurityScanning/YamlHelper.cs new file mode 100644 index 000000000..c76e01538 --- /dev/null +++ b/Lombiq.Tests.UI/SecurityScanning/YamlHelper.cs @@ -0,0 +1,38 @@ +using System; +using System.IO; +using System.Linq; +using YamlDotNet.RepresentationModel; + +namespace Lombiq.Tests.UI.SecurityScanning; + +internal static class YamlHelper +{ + public static YamlDocument LoadDocument(string yamlFilePath) + { + using var streamReader = new StreamReader(yamlFilePath); + var yamlStream = new YamlStream(); + yamlStream.Load(streamReader); + return yamlStream.Documents[0]; + } + + public static YamlMappingNode GetCurrentContext(YamlDocument yamlDocument) + { + var contexts = (YamlSequenceNode)yamlDocument.GetRootNode()["env"]["contexts"]; + + if (!contexts.Any()) + { + throw new ArgumentException( + "The supplied ZAP Automation Framework YAML file should contain at least one context."); + } + + var currentContext = (YamlMappingNode)contexts[0]; + + if (contexts.Count() > 1) + { + currentContext = (YamlMappingNode)contexts.FirstOrDefault(context => context["Name"].ToString() == "Default Context") + ?? currentContext; + } + + return currentContext; + } +} diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 2cbe55cd3..edb102ad7 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -65,7 +65,7 @@ public Task RunSecurityScanAsync( automationFrameworkYamlPath, async configuration => { - SetStartUrlInYaml(configuration, startUri); + SetStartUrlInPlan(configuration, startUri); if (modifyPlan != null) await modifyPlan(configuration); }); @@ -104,7 +104,7 @@ public async Task RunSecurityScanAsync( File.Copy(automationFrameworkYamlPath, yamlFileCopyPath, overwrite: true); - await PrepareYamlAsync(yamlFileCopyPath, modifyPlan); + await PreparePlanAsync(yamlFileCopyPath, modifyPlan); // Explanation on the CLI arguments used below: // - --add-host and --network host: Lets us connect to the host OS's localhost, where the OC app runs, with @@ -215,25 +215,9 @@ private async Task EnsureInitializedAsync() } } - private static void SetStartUrlInYaml(YamlDocument configuration, Uri startUri) + private static void SetStartUrlInPlan(YamlDocument yamlDocument, Uri startUri) { - var rootNode = (YamlMappingNode)configuration.RootNode; - - var contexts = (YamlSequenceNode)rootNode["env"]["contexts"]; - - if (!contexts.Any()) - { - throw new ArgumentException( - "The supplied ZAP Automation Framework YAML file should contain at least one context."); - } - - var currentContext = (YamlMappingNode)contexts[0]; - - if (contexts.Count() > 1) - { - currentContext = (YamlMappingNode)contexts.FirstOrDefault(context => context["Name"].ToString() == "Default Context") - ?? currentContext; - } + var currentContext = YamlHelper.GetCurrentContext(yamlDocument); // Setting URLs in the context. // Setting includePaths in the context is not necessary because by default everything under urls will be scanned. @@ -254,24 +238,13 @@ private static void SetStartUrlInYaml(YamlDocument configuration, Uri startUri) urls.Add(startUri.ToString()); } - private static async Task PrepareYamlAsync(string yamlFilePath, Func modifyPlan) + private static async Task PreparePlanAsync(string yamlFilePath, Func modifyPlan) { - YamlDocument yamlDocument; - - using (var streamReader = new StreamReader(yamlFilePath)) - { - var yamlStream = new YamlStream(); - yamlStream.Load(streamReader); - // Using a free-form object instead of deserializing into a statically defined object, not to potentially - // break unknown fields during reserialization. - yamlDocument = yamlStream.Documents[0]; - } - - var rootNode = (YamlMappingNode)yamlDocument.RootNode; + var yamlDocument = YamlHelper.LoadDocument(yamlFilePath); // Setting report directories to the conventional one and verifying that there's exactly one SARIF report. - var jobs = (IEnumerable)rootNode["jobs"]; + var jobs = (IEnumerable)yamlDocument.GetRootNode()["jobs"]; var sarifReportCount = 0; @@ -293,10 +266,8 @@ private static async Task PrepareYamlAsync(string yamlFilePath, Func Date: Thu, 16 Nov 2023 22:26:40 +0100 Subject: [PATCH 040/129] Fixing SecurityScanWithCustomConfigurationShouldPass --- Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index d5fbc8501..ed0e9e014 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -54,7 +54,7 @@ public Task BasicSecurityScanShouldPass(Browser browser) => public Task SecurityScanWithCustomConfigurationShouldPass(Browser browser) => ExecuteTestAfterSetupAsync( async context => await context.RunAndAssertBaselineSecurityScanAsync( - assertSecurityScanResult: sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeLessThan(2), + assertSecurityScanResult: sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeLessThan(200), modifyPlan: plan => { plan.AddSpiderAjaxAfterSpider().AddExcludePathsRegex(".*blog.*"); From 61f162d53564b88a4120878fb600e5d02d4881c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 16 Nov 2023 23:24:15 +0100 Subject: [PATCH 041/129] Better configurability --- .../Tests/SecurityScanningTests.cs | 10 ++-- .../YamlDocumentExtensions.cs | 53 ++++++++++++++++++- .../SecurityScanning/ZapManager.cs | 4 +- 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index ed0e9e014..615244aa4 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -50,16 +50,16 @@ public Task BasicSecurityScanShouldPass(Browser browser) => // a website that's not an SPA, but also slows the scan down by a lot. // - The plan is also modified with an exclusion regex pattern. You can use this to exclude certain URLs from the // scan. + // - We disable the "Server Leaks Information via "X-Powered-By" HTTP Response Header Field(s)" alert of ZAP. This + // is because by default, Orchard Core sends an "X-Powered-By: OrchardCore" header. If you want airtight security, + // you might want to turn this off, but for the sake of example we just ignore the alert here. [Theory, Chrome] public Task SecurityScanWithCustomConfigurationShouldPass(Browser browser) => ExecuteTestAfterSetupAsync( async context => await context.RunAndAssertBaselineSecurityScanAsync( - assertSecurityScanResult: sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeLessThan(200), + assertSecurityScanResult: sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeLessThan(2), modifyPlan: plan => - { - plan.AddSpiderAjaxAfterSpider().AddExcludePathsRegex(".*blog.*"); - return Task.CompletedTask; - }), + plan.AddSpiderAjaxAfterSpider().AddExcludePathsRegex(".*blog.*").DisableScanRule("10037", "asdfaldfalsdflasldf").CompletedTaskAsync()), browser); // Overriding the default setup so we can have a simpler site, simplifying the security scan for the purpose of this diff --git a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs index 62da89274..2e7ff66e9 100644 --- a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Threading.Tasks; using YamlDotNet.RepresentationModel; namespace Lombiq.Tests.UI.SecurityScanning; @@ -15,7 +16,7 @@ public static class YamlDocumentExtensions /// public static YamlDocument AddSpiderAjaxAfterSpider(this YamlDocument yamlDocument) { - var jobs = (YamlSequenceNode)yamlDocument.GetRootNode()["jobs"]; + var jobs = yamlDocument.GetJobs(); var spiderJob = jobs.FirstOrDefault(job => (string)job["name"] == "spider") ?? @@ -48,5 +49,55 @@ public static YamlDocument AddExcludePathsRegex(this YamlDocument yamlDocument, return yamlDocument; } + /// + /// Disable a certain ZAP scan rule. + /// + /// The ID of the rule. In the scan report, this is usually displayed as "Plugin Id". + /// + /// The human-readable name of the rule. Not required to turn off the rule, and its value doesn't matter. It's just + /// useful for the readability of the method call. + /// + /// + /// Thrown if no job with the type "passiveScan-config" is found in the Automation Framework Plan. + /// + public static YamlDocument DisableScanRule(this YamlDocument yamlDocument, string id, string name = "") + { + var jobs = yamlDocument.GetJobs(); + + var passiveScanConfigJob = + (YamlMappingNode)jobs.FirstOrDefault(job => (string)job["type"] == "passiveScan-config") ?? + throw new ArgumentException( + "No job with the type \"passiveScan-config\" found in the Automation Framework Plan so the rule can't be added."); + + if (!passiveScanConfigJob.Children.ContainsKey("rules")) passiveScanConfigJob.Add("rules", new YamlSequenceNode()); + + var newRule = new YamlMappingNode + { + { "id", id }, + { "name", name }, + { "threshold", "off" }, + }; + + ((YamlSequenceNode)passiveScanConfigJob["rules"]).Add(newRule); + + return yamlDocument; + } + + /// + /// Gets cast to . + /// public static YamlMappingNode GetRootNode(this YamlDocument yamlDocument) => (YamlMappingNode)yamlDocument.RootNode; + + /// + /// Gets the "jobs" section of the . + /// + public static YamlSequenceNode GetJobs(this YamlDocument yamlDocument) => + (YamlSequenceNode)yamlDocument.GetRootNode()["jobs"]; + + /// + /// Shortcuts to to be able to chain extensions in an + /// async method/delegate. + /// + /// . + public static Task CompletedTaskAsync(this YamlDocument yamlDocument) => Task.CompletedTask; } diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index edb102ad7..1f6268406 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -244,11 +244,9 @@ private static async Task PreparePlanAsync(string yamlFilePath, Func)yamlDocument.GetRootNode()["jobs"]; - var sarifReportCount = 0; - foreach (var job in jobs) + foreach (var job in yamlDocument.GetJobs()) { if ((string)job["type"] != "report") continue; From 6ee61e1d6dfac36d38c29f14b2062298835e8add Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 16 Nov 2023 23:26:28 +0100 Subject: [PATCH 042/129] Fixing debug code --- Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 615244aa4..81e88f55e 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -57,9 +57,13 @@ public Task BasicSecurityScanShouldPass(Browser browser) => public Task SecurityScanWithCustomConfigurationShouldPass(Browser browser) => ExecuteTestAfterSetupAsync( async context => await context.RunAndAssertBaselineSecurityScanAsync( - assertSecurityScanResult: sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeLessThan(2), + assertSecurityScanResult: sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeLessThan(200), modifyPlan: plan => - plan.AddSpiderAjaxAfterSpider().AddExcludePathsRegex(".*blog.*").DisableScanRule("10037", "asdfaldfalsdflasldf").CompletedTaskAsync()), + plan + .AddSpiderAjaxAfterSpider() + .AddExcludePathsRegex(".*blog.*") + .DisableScanRule("10037", "Server Leaks Information via \"X-Powered-By\" HTTP Response Header Field(s)") + .CompletedTaskAsync()), browser); // Overriding the default setup so we can have a simpler site, simplifying the security scan for the purpose of this From 0980b9d957741ae2f2f2e6354156dbd5c223b431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 16 Nov 2023 23:28:23 +0100 Subject: [PATCH 043/129] Docs formatting --- ...SecurityScanningUITestContextExtensions.cs | 24 +++++++++---------- .../SecurityScanning/ZapManager.cs | 6 ++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs index ed9a8bfb9..fefc3ff44 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs @@ -11,8 +11,8 @@ namespace Lombiq.Tests.UI.SecurityScanning; public static class SecurityScanningUITestContextExtensions { /// - /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app with the Baseline - /// Automation Framework profile except for the spiderAjax job, and runs assertions on the result (see Zed Attack Proxy (ZAP) security scan against an app with the + /// Baseline Automation Framework profile except for the spiderAjax job, and runs assertions on the result (see for the official docs on the legacy version of this /// scan). /// @@ -37,9 +37,9 @@ public static Task RunAndAssertBaselineSecurityScanAsync( assertSecurityScanResult); /// - /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app with the Full Scan - /// Automation Framework profile except for the spiderAjax job, and runs assertions on the result (see for the official docs on the legacy version of this + /// Run a Zed Attack Proxy (ZAP) security scan against an app with the + /// Full Scan Automation Framework profile except for the spiderAjax job, and runs assertions on the result (see + /// for the official docs on the legacy version of this /// scan). /// /// @@ -63,8 +63,8 @@ public static Task RunAndAssertFullSecurityScanAsync( assertSecurityScanResult); /// - /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app with the GraphQL - /// Automation Framework profile and runs assertions on the result (see Zed Attack Proxy (ZAP) security scan against an app with the + /// GraphQL Automation Framework profile and runs assertions on the result (see for the official docs on ZAP's GraphQL /// support). /// @@ -89,8 +89,8 @@ public static Task RunAndAssertGraphQLSecurityScanAsync( assertSecurityScanResult); /// - /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app with the OpenAPI - /// Automation Framework profile and runs assertions on the result (see Zed Attack Proxy (ZAP) security scan against an app with the + /// OpenAPI Automation Framework profile and runs assertions on the result (see for the official docs on ZAP's GraphQL /// support). /// @@ -115,8 +115,8 @@ public static Task RunAndAssertOpenApiSecurityScanAsync( assertSecurityScanResult); /// - /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app and runs assertions on - /// the result. + /// Run a Zed Attack Proxy (ZAP) security scan against an app and runs + /// assertions on the result. /// /// /// File system path to the YAML configuration file of ZAP's Automation Framework. See - /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app. + /// Run a Zed Attack Proxy (ZAP) security scan against an app. /// /// /// File system path to the YAML configuration file of ZAP's Automation Framework. See diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 1f6268406..4b1ab306f 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -17,7 +17,7 @@ namespace Lombiq.Tests.UI.SecurityScanning; /// -/// Service to manage Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) instances and security scans. +/// Service to manage Zed Attack Proxy (ZAP) instances and security scans. /// public sealed class ZapManager : IAsyncDisposable { @@ -40,7 +40,7 @@ public sealed class ZapManager : IAsyncDisposable internal ZapManager(ITestOutputHelper testOutputHelper) => _testOutputHelper = testOutputHelper; /// - /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app. + /// Run a Zed Attack Proxy (ZAP) security scan against an app. /// /// The of the currently executing test. /// @@ -70,7 +70,7 @@ public Task RunSecurityScanAsync( }); /// - /// Run a Zed Attack Proxy (ZAP, see https://www.zaproxy.org/) security scan against an app. + /// Run a Zed Attack Proxy (ZAP) security scan against an app. /// /// The of the currently executing test. /// From 2a046146f411d89f4353b7032a28e2faa3debe42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 16 Nov 2023 23:38:41 +0100 Subject: [PATCH 044/129] Rule IDs are actually ints --- Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs | 2 +- Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 81e88f55e..e760bf32c 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -62,7 +62,7 @@ public Task SecurityScanWithCustomConfigurationShouldPass(Browser browser) => plan .AddSpiderAjaxAfterSpider() .AddExcludePathsRegex(".*blog.*") - .DisableScanRule("10037", "Server Leaks Information via \"X-Powered-By\" HTTP Response Header Field(s)") + .DisableScanRule(10037, "Server Leaks Information via \"X-Powered-By\" HTTP Response Header Field(s)") .CompletedTaskAsync()), browser); diff --git a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs index 2e7ff66e9..5bee31166 100644 --- a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs @@ -60,7 +60,7 @@ public static YamlDocument AddExcludePathsRegex(this YamlDocument yamlDocument, /// /// Thrown if no job with the type "passiveScan-config" is found in the Automation Framework Plan. /// - public static YamlDocument DisableScanRule(this YamlDocument yamlDocument, string id, string name = "") + public static YamlDocument DisableScanRule(this YamlDocument yamlDocument, int id, string name = "") { var jobs = yamlDocument.GetJobs(); @@ -73,7 +73,7 @@ public static YamlDocument DisableScanRule(this YamlDocument yamlDocument, strin var newRule = new YamlMappingNode { - { "id", id }, + { "id", id.ToTechnicalString() }, { "name", name }, { "threshold", "off" }, }; From 3f1f974ba414f59347a64c22e579a7a7f97a040f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 16 Nov 2023 23:47:02 +0100 Subject: [PATCH 045/129] Fixing assertion for when there are no alerts --- .../SecurityScanningConfiguration.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs index 22f92bcd1..f16e096d7 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs @@ -17,15 +17,9 @@ public class SecurityScanningConfiguration /// /// Gets or sets a delegate to run assertions on the when security scanning happens. /// - public Action AssertSecurityScanResult { get; set; } = AssertSecurityScanHasNoFails; + public Action AssertSecurityScanResult { get; set; } = AssertSecurityScanHasNoAlerts; - public static readonly Action AssertSecurityScanHasNoFails = - (_, sarifLog) => sarifLog.Runs[0].Results.ShouldNotContain(result => result.Kind == ResultKind.Fail); - - // When running the app locally, HSTS is never set, so we'd get a "Strict-Transport-Security Header Not Set" fail. - // The rule is disabled in the default configs though. - public static readonly Action AssertSecurityScanHasNoFailsExceptHsts = - (_, sarifLog) => - sarifLog.Runs[0].Results.ShouldNotContain(result => - result.Kind == ResultKind.Fail && result.RuleId != "10035"); + public static readonly Action AssertSecurityScanHasNoAlerts = + (_, sarifLog) => sarifLog.Runs[0].Results.ShouldNotContain(result => + result.Kind == ResultKind.Fail && result.Level != FailureLevel.None && result.Level != FailureLevel.Note); } From 805d7316b0c2dc6f2ef9a958dffdbce62d217103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 16 Nov 2023 23:54:47 +0100 Subject: [PATCH 046/129] Helper to merge YAMLs --- .../YamlDocumentExtensions.cs | 29 +++++++++++++++++++ .../SecurityScanning/YamlHelper.cs | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs index 5bee31166..7a63b3266 100644 --- a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs @@ -100,4 +100,33 @@ public static YamlSequenceNode GetJobs(this YamlDocument yamlDocument) => /// /// . public static Task CompletedTaskAsync(this YamlDocument yamlDocument) => Task.CompletedTask; + + /// + /// Merge the given into the current one. + /// + /// + /// The to merge from, which overrides the current one. + /// + /// The merged . + public static YamlDocument MergeFrom(this YamlDocument baseDocument, YamlDocument overrideDocument) + { + var baseMapping = baseDocument.GetRootNode(); + var overrideMapping = overrideDocument.GetRootNode(); + + foreach (var entry in overrideMapping.Children) + { + if (baseMapping.Children.ContainsKey(entry.Key)) + { + // Override existing property. + baseMapping.Children[entry.Key] = entry.Value; + } + else + { + // Add new property. + baseMapping.Children.Add(entry.Key, entry.Value); + } + } + + return baseDocument; + } } diff --git a/Lombiq.Tests.UI/SecurityScanning/YamlHelper.cs b/Lombiq.Tests.UI/SecurityScanning/YamlHelper.cs index c76e01538..db4c70513 100644 --- a/Lombiq.Tests.UI/SecurityScanning/YamlHelper.cs +++ b/Lombiq.Tests.UI/SecurityScanning/YamlHelper.cs @@ -5,7 +5,7 @@ namespace Lombiq.Tests.UI.SecurityScanning; -internal static class YamlHelper +public static class YamlHelper { public static YamlDocument LoadDocument(string yamlFilePath) { From 8ac11f182002a13da904c9a0ae351f7f571aa561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Fri, 17 Nov 2023 00:01:38 +0100 Subject: [PATCH 047/129] Fixing method name --- Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs | 2 +- Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index e760bf32c..606914ddb 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -62,7 +62,7 @@ public Task SecurityScanWithCustomConfigurationShouldPass(Browser browser) => plan .AddSpiderAjaxAfterSpider() .AddExcludePathsRegex(".*blog.*") - .DisableScanRule(10037, "Server Leaks Information via \"X-Powered-By\" HTTP Response Header Field(s)") + .DisablePassiveScanRule(10037, "Server Leaks Information via \"X-Powered-By\" HTTP Response Header Field(s)") .CompletedTaskAsync()), browser); diff --git a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs index 7a63b3266..f5bdf0bb9 100644 --- a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs @@ -50,7 +50,7 @@ public static YamlDocument AddExcludePathsRegex(this YamlDocument yamlDocument, } /// - /// Disable a certain ZAP scan rule. + /// Disable a certain ZAP passive scan rule. /// /// The ID of the rule. In the scan report, this is usually displayed as "Plugin Id". /// @@ -60,7 +60,7 @@ public static YamlDocument AddExcludePathsRegex(this YamlDocument yamlDocument, /// /// Thrown if no job with the type "passiveScan-config" is found in the Automation Framework Plan. /// - public static YamlDocument DisableScanRule(this YamlDocument yamlDocument, int id, string name = "") + public static YamlDocument DisablePassiveScanRule(this YamlDocument yamlDocument, int id, string name = "") { var jobs = yamlDocument.GetJobs(); From 4f7ca1fa7b24bd0b87f7aab3c45f94afd9e1235c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Fri, 17 Nov 2023 00:19:23 +0100 Subject: [PATCH 048/129] Docs --- Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs | 6 +++--- Lombiq.Tests.UI/Docs/SecurityScanning.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 606914ddb..0b99a0cac 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -50,9 +50,9 @@ public Task BasicSecurityScanShouldPass(Browser browser) => // a website that's not an SPA, but also slows the scan down by a lot. // - The plan is also modified with an exclusion regex pattern. You can use this to exclude certain URLs from the // scan. - // - We disable the "Server Leaks Information via "X-Powered-By" HTTP Response Header Field(s)" alert of ZAP. This - // is because by default, Orchard Core sends an "X-Powered-By: OrchardCore" header. If you want airtight security, - // you might want to turn this off, but for the sake of example we just ignore the alert here. + // - We disable the "Server Leaks Information via "X-Powered-By" HTTP Response Header Field(s)" alert of ZAP's + // passive scan. This is because by default, Orchard Core sends an "X-Powered-By: OrchardCore" header. If you want + // airtight security, you might want to turn this off, but for the sake of example we just ignore the alert here. [Theory, Chrome] public Task SecurityScanWithCustomConfigurationShouldPass(Browser browser) => ExecuteTestAfterSetupAsync( diff --git a/Lombiq.Tests.UI/Docs/SecurityScanning.md b/Lombiq.Tests.UI/Docs/SecurityScanning.md index 49c7dfef7..45c5d7f04 100644 --- a/Lombiq.Tests.UI/Docs/SecurityScanning.md +++ b/Lombiq.Tests.UI/Docs/SecurityScanning.md @@ -17,7 +17,7 @@ You an create detailed security scans of your app with [Zed Attack Proxy (ZAP)]( - If you're new to ZAP, you can start learning by checking out [ZAP's getting started guide](https://www.zaproxy.org/getting-started/), as well as the [ZAP Chat Videos](https://www.zaproxy.org/zap-chat/) (especially [ZAP Chat 06 Automation Introduction](https://www.youtube.com/watch?v=PnCbIAnauD8)). - Be aware that ZAP scans run with an internally managed browser instance, not in the browser launched by the test. - While ZAP is fully managed for you, Docker needs to be available and running to host the ZAP instance. On your development machine you can install [Docker Desktop](https://www.docker.com/products/docker-desktop/). -- The scan of a website with even just 1-200 pages can take 15-30 minutes. So, be careful to fine-tune the ZAP configuration to make it suitable for your app. +- The full scan of a website with even just 1-200 pages can take 15-30 minutes. So, be careful to fine-tune the ZAP configuration to make it suitable for your app. ## Troubleshooting From e4e479b94df9d5d4d77036564ee32f29546e37a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Fri, 17 Nov 2023 00:22:50 +0100 Subject: [PATCH 049/129] Preventing running more than one scan in the same test --- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 4b1ab306f..1a81af1f2 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -89,6 +89,17 @@ public async Task RunSecurityScanAsync( string automationFrameworkYamlPath, Func modifyPlan = null) { + // Being able to run more than one scan in a test would complicate report generation and processing the SARIF + // report so rather just preventing it here (really nobody should want it anyway). + const string customContextKey = "ZapManager.ScanWasRun"; + + if (context.CustomContext.ContainsKey(customContextKey)) + { + throw new NotSupportedException("You may only run a single ZAP scan in a given test."); + } + + context.CustomContext.Add(customContextKey, string.Empty); + await EnsureInitializedAsync(); if (string.IsNullOrEmpty(automationFrameworkYamlPath)) From a61497c4fca27026268a19e1fb2bd5be258ca21a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Fri, 17 Nov 2023 00:42:24 +0100 Subject: [PATCH 050/129] Adding config to always create a scan report but it's not working now --- .../Tests/SecurityScanningTests.cs | 1 + .../SecurityScanningConfiguration.cs | 10 ++++++++++ .../SecurityScanningUITestContextExtensions.cs | 14 ++++++++------ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 0b99a0cac..6afb948b7 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -104,6 +104,7 @@ protected override Task ExecuteTestAfterSetupAsync( // Don't do this at home! Fix the issues instead. This is only here to have a smoother demo. configuration.SecurityScanningConfiguration.AssertSecurityScanResult = (_, _) => { }; + // Check out the rest of SecurityScanningConfiguration too! await changeConfigurationAsync(configuration); }); diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs index f16e096d7..fdf831f66 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs @@ -9,6 +9,16 @@ namespace Lombiq.Tests.UI.SecurityScanning; public class SecurityScanningConfiguration { + /// + /// Gets or sets a value indicating whether to save a report to the failure dump for every scan, even passing ones. + /// + /// + /// + /// Won't work until https://github.com/Lombiq/UI-Testing-Toolbox/issues/323 is implemented, hence it's internal. + /// + /// + internal bool CreateReportAlways { get; set; } + /// /// Gets or sets a delegate that may modify the deserialized representation of the ZAP Automation Framework plan in YAML. /// diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs index fefc3ff44..7df200af8 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs @@ -142,16 +142,16 @@ public static async Task RunAndAssertSecurityScanAsync( Func modifyPlan = null, Action assertSecurityScanResult = null) { - var securityScanningConfiguration = context.Configuration.SecurityScanningConfiguration; + var configuration = context.Configuration.SecurityScanningConfiguration; - async Task CompositemodifyPlan(YamlDocument configuration) + async Task CompositemodifyPlan(YamlDocument plan) { - if (securityScanningConfiguration.ZapAutomationFrameworkYamlModifier != null) + if (configuration.ZapAutomationFrameworkYamlModifier != null) { - await securityScanningConfiguration.ZapAutomationFrameworkYamlModifier(context, configuration); + await configuration.ZapAutomationFrameworkYamlModifier(context, plan); } - if (modifyPlan != null) await modifyPlan(configuration); + if (modifyPlan != null) await modifyPlan(plan); } SecurityScanResult result = null; @@ -160,7 +160,9 @@ async Task CompositemodifyPlan(YamlDocument configuration) result = await context.RunSecurityScanAsync(automationFrameworkYamlPath, startUri, CompositemodifyPlan); if (assertSecurityScanResult != null) assertSecurityScanResult(result.SarifLog); - else securityScanningConfiguration.AssertSecurityScanResult(context, result.SarifLog); + else configuration.AssertSecurityScanResult(context, result.SarifLog); + + if (configuration.CreateReportAlways) context.AppendDirectoryToFailureDump(result.ReportsDirectoryPath); } catch (Exception ex) { From e6cfe4662931655767be845f4ebe6cb9ccca1aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Fri, 17 Nov 2023 01:02:49 +0100 Subject: [PATCH 051/129] Adding DisableActiveScanRule() --- .../YamlDocumentExtensions.cs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs index f5bdf0bb9..433831fcc 100644 --- a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs @@ -83,6 +83,48 @@ public static YamlDocument DisablePassiveScanRule(this YamlDocument yamlDocument return yamlDocument; } + /// + /// Disable a certain ZAP active scan rule. + /// + /// The ID of the rule. In the scan report, this is usually displayed as "Plugin Id". + /// + /// The human-readable name of the rule. Not required to turn off the rule, and its value doesn't matter. It's just + /// useful for the readability of the method call. + /// + /// + /// Thrown if no job with the type "activeScan" is found in the Automation Framework Plan, or if it doesn't have a + /// policyDefinition property. + /// + public static YamlDocument DisableActiveScanRule(this YamlDocument yamlDocument, int id, string name = "") + { + var jobs = yamlDocument.GetJobs(); + + var activeScanConfigJob = + (YamlMappingNode)jobs.FirstOrDefault(job => (string)job["type"] == "activeScan") ?? + throw new ArgumentException( + "No job with the type \"activeScan\" found in the Automation Framework Plan so the rule can't be added."); + + if (!activeScanConfigJob.Children.ContainsKey("policyDefinition")) + { + throw new ArgumentException("The \"activeScan\" job should contain a policyDefinition."); + } + + var policyDefinition = (YamlMappingNode)activeScanConfigJob["policyDefinition"]; + + if (!policyDefinition.Children.ContainsKey("rules")) policyDefinition.Add("rules", new YamlSequenceNode()); + + var newRule = new YamlMappingNode + { + { "id", id.ToTechnicalString() }, + { "name", name }, + { "threshold", "off" }, + }; + + ((YamlSequenceNode)policyDefinition["rules"]).Add(newRule); + + return yamlDocument; + } + /// /// Gets cast to . /// From 5befd0f72b401e11929c86c5ac936d5293f7a1e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Fri, 17 Nov 2023 01:10:08 +0100 Subject: [PATCH 052/129] Updating NuGet metadata --- Lombiq.Tests.UI/Lombiq.Tests.UI.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj index 158cfa98e..b3eb7d28f 100644 --- a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj +++ b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj @@ -18,7 +18,7 @@ Lombiq Technologies Copyright © 2020, Lombiq Technologies Ltd. Lombiq UI Testing Toolbox for Orchard Core: Web UI testing toolbox mostly for Orchard Core applications. Everything you need to do UI testing with Selenium for an Orchard app is here. See the project website for detailed documentation. - OrchardCore;Lombiq;AspNetCore;Selenium;Atata;Shouldly;xUnit;Axe;AccessibilityTesting;UITesting;Testing;Automation + OrchardCore;Lombiq;AspNetCore;Selenium;Atata;Shouldly;xUnit;Axe;AccessibilityTesting;UITesting;Testing;Automation;ZAP;Zed Attack Proxy;Security;Scanning;OWASP NuGetIcon.png https://github.com/Lombiq/UI-Testing-Toolbox https://github.com/Lombiq/UI-Testing-Toolbox From fe76c3a58e2cd917bf751cc66a4fd1000023083d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Sun, 19 Nov 2023 23:32:16 +0100 Subject: [PATCH 053/129] We can actually also use the current stable version of ZAP --- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 1a81af1f2..4fe7fb6ae 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -21,10 +21,11 @@ namespace Lombiq.Tests.UI.SecurityScanning; /// public sealed class ZapManager : IAsyncDisposable { - // Need to use the weekly release because that's the one that has packaged scans migrated to Automation Framework. + // Using the then-latest stable release of ZAP. You can check for newer version tags here: + // https://hub.docker.com/r/softwaresecurityproject/zap-stable/tags. // When updating this version, also regenerate the Automation Framework YAML config files so we don't miss any // changes to those. - private const string _zapImage = "softwaresecurityproject/zap-weekly:20231113"; + private const string _zapImage = "softwaresecurityproject/zap-stable:2.14.0"; private const string _zapWorkingDirectoryPath = "/zap/wrk/"; private const string _zapReportsDirectoryName = "reports"; @@ -130,9 +131,7 @@ public async Task RunSecurityScanAsync( // Also see https://www.zaproxy.org/docs/docker/about/#automation-framework. // Running a ZAP desktop in the browser with Webswing with the same config under Windows: -#pragma warning disable S103 // Lines should not be too long - // docker run --add-host localhost:host-gateway -u zap -p 8080:8080 -p 8090:8090 -i softwaresecurityproject/zap-weekly:20231113 zap-webswing.sh -#pragma warning restore S103 // Lines should not be too long + // docker run --add-host localhost:host-gateway -u zap -p 8080:8080 -p 8090:8090 -i softwaresecurityproject/zap-stable zap-webswing.sh var cliParameters = new List { "run" }; From 80ddf4a2b54db6c0d8c5e26cdd0336f44b732d3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Mon, 20 Nov 2023 00:46:03 +0100 Subject: [PATCH 054/129] Better ZAP error handling --- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 4fe7fb6ae..a5157cfb6 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -160,9 +160,7 @@ public async Task RunSecurityScanAsync( var stdErrBuffer = new StringBuilder(); - // The result of the call is not interesting, since we don't need the exit code: Assertions should check if the - // app failed security scanning, and if the scan itself fails then there won't be a report, what's checked below. - await _docker + var result = await _docker .GetCommand(cliParameters) .WithStandardOutputPipe(PipeTarget.ToDelegate(line => _testOutputHelper.WriteLineTimestampedAndDebug(line))) .WithStandardErrorPipe(PipeTarget.ToStringBuilder(stdErrBuffer)) @@ -170,6 +168,11 @@ await _docker .WithValidation(CommandResultValidation.None) .ExecuteAsync(_cancellationTokenSource.Token); + if (result.ExitCode == 1) + { + throw new SecurityScanningException("Security scanning failed to complete. Check the test's output log for details."); + } + var reportsDirectoryPath = Path.Combine(mountedDirectoryPath, _zapReportsDirectoryName); var jsonReports = Directory.EnumerateFiles(reportsDirectoryPath, "*.json").ToList(); From 81bba3bb437073d4d7d3909090c226b59b83ed87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Mon, 20 Nov 2023 00:46:25 +0100 Subject: [PATCH 055/129] Typo --- .../SecurityScanningUITestContextExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs index 7df200af8..6ceb52b29 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs @@ -144,7 +144,7 @@ public static async Task RunAndAssertSecurityScanAsync( { var configuration = context.Configuration.SecurityScanningConfiguration; - async Task CompositemodifyPlan(YamlDocument plan) + async Task CompositeModifyPlan(YamlDocument plan) { if (configuration.ZapAutomationFrameworkYamlModifier != null) { @@ -157,7 +157,7 @@ async Task CompositemodifyPlan(YamlDocument plan) SecurityScanResult result = null; try { - result = await context.RunSecurityScanAsync(automationFrameworkYamlPath, startUri, CompositemodifyPlan); + result = await context.RunSecurityScanAsync(automationFrameworkYamlPath, startUri, CompositeModifyPlan); if (assertSecurityScanResult != null) assertSecurityScanResult(result.SarifLog); else configuration.AssertSecurityScanResult(context, result.SarifLog); From 74c2b43d03a642d261f1f13acfc7acc4500501b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Mon, 20 Nov 2023 00:58:02 +0100 Subject: [PATCH 056/129] Configurability for disabling a rule just for a single URL --- .../Tests/SecurityScanningTests.cs | 10 ++- Lombiq.Tests.UI/Docs/SecurityScanning.md | 1 + .../YamlDocumentExtensions.cs | 87 ++++++++++++++++--- 3 files changed, 85 insertions(+), 13 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 6afb948b7..f0f0c6f8d 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -47,12 +47,15 @@ public Task BasicSecurityScanShouldPass(Browser browser) => // differently from the default "no scanning alert is allowed" assertion. // - The plan is modified to also run ZAP's Ajax Spider // (https://www.zaproxy.org/docs/desktop/addons/ajax-spider/automation/). This is usually not just unnecessary for - // a website that's not an SPA, but also slows the scan down by a lot. + // a website that's not an SPA, but also slows the scan down by a lot. However, if you have an SPA, you need to + // use it. // - The plan is also modified with an exclusion regex pattern. You can use this to exclude certain URLs from the // scan. // - We disable the "Server Leaks Information via "X-Powered-By" HTTP Response Header Field(s)" alert of ZAP's - // passive scan. This is because by default, Orchard Core sends an "X-Powered-By: OrchardCore" header. If you want - // airtight security, you might want to turn this off, but for the sake of example we just ignore the alert here. + // passive scan for the whole scan. This is because by default, Orchard Core sends an "X-Powered-By: OrchardCore" + // header. If you want airtight security, you might want to turn this off, but for the sake of example we just + // ignore the alert here. + // - We also disable the "Content Security Policy (CSP) Header Not Set" rule but only for the /about page. [Theory, Chrome] public Task SecurityScanWithCustomConfigurationShouldPass(Browser browser) => ExecuteTestAfterSetupAsync( @@ -63,6 +66,7 @@ public Task SecurityScanWithCustomConfigurationShouldPass(Browser browser) => .AddSpiderAjaxAfterSpider() .AddExcludePathsRegex(".*blog.*") .DisablePassiveScanRule(10037, "Server Leaks Information via \"X-Powered-By\" HTTP Response Header Field(s)") + .DisableRuleForUrl(10038, ".*/about", "Content Security Policy (CSP) Header Not Set") .CompletedTaskAsync()), browser); diff --git a/Lombiq.Tests.UI/Docs/SecurityScanning.md b/Lombiq.Tests.UI/Docs/SecurityScanning.md index 45c5d7f04..e623ebed4 100644 --- a/Lombiq.Tests.UI/Docs/SecurityScanning.md +++ b/Lombiq.Tests.UI/Docs/SecurityScanning.md @@ -22,3 +22,4 @@ You an create detailed security scans of your app with [Zed Attack Proxy (ZAP)]( ## Troubleshooting - If you're unsure what happens in a scan, run the [ZAP desktop app](https://www.zaproxy.org/download/) and load the Automation Framework plan's YAML file into it. If you use the default scans, then these will be available under the build output directory (like _bin/Debug_) under _SecurityScanning/AutomationFrameworkPlans_. Then, you can open and run them as demonstrated [in this video](https://youtu.be/PnCbIAnauD8?si=u0vi63Uvv9wZINzb&t=1173). +- If an alert is a false positive, follow [the official docs](https://www.zaproxy.org/faq/how-do-i-handle-a-false-positive/). You can use the [`alertFilter` job](https://www.zaproxy.org/docs/desktop/addons/alert-filters/automation/) to ignore alerts in very specific conditions. diff --git a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs index 433831fcc..c49651e55 100644 --- a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs @@ -34,14 +34,19 @@ public static YamlDocument AddSpiderAjaxAfterSpider(this YamlDocument yamlDocume /// Adds one or more regex patterns to the ZAP Automation Framework plan's excludePaths config under the current /// context. /// - public static YamlDocument AddExcludePathsRegex(this YamlDocument yamlDocument, params string[] excludePathsPatterns) + /// + /// One or more regex patterns to be added to the ZAP Automation Framework plan's excludePaths config under the + /// current context. These should be regex patterns that match the whole absolute URL, so something like ".*blog.*" + /// to match /blog, /blog/my-post, etc. + /// + public static YamlDocument AddExcludePathsRegex(this YamlDocument yamlDocument, params string[] excludePathsRegexPatterns) { var currentContext = YamlHelper.GetCurrentContext(yamlDocument); if (!currentContext.Children.ContainsKey("excludePaths")) currentContext.Add("excludePaths", new YamlSequenceNode()); var excludePaths = (YamlSequenceNode)currentContext["excludePaths"]; - foreach (var pattern in excludePathsPatterns) + foreach (var pattern in excludePathsRegexPatterns) { excludePaths.Add(pattern); } @@ -50,7 +55,8 @@ public static YamlDocument AddExcludePathsRegex(this YamlDocument yamlDocument, } /// - /// Disable a certain ZAP passive scan rule. + /// Disable a certain ZAP passive scan rule for the whole scan. If you only want to disable a rule for a given page, + /// use instead. /// /// The ID of the rule. In the scan report, this is usually displayed as "Plugin Id". /// @@ -62,12 +68,7 @@ public static YamlDocument AddExcludePathsRegex(this YamlDocument yamlDocument, /// public static YamlDocument DisablePassiveScanRule(this YamlDocument yamlDocument, int id, string name = "") { - var jobs = yamlDocument.GetJobs(); - - var passiveScanConfigJob = - (YamlMappingNode)jobs.FirstOrDefault(job => (string)job["type"] == "passiveScan-config") ?? - throw new ArgumentException( - "No job with the type \"passiveScan-config\" found in the Automation Framework Plan so the rule can't be added."); + var passiveScanConfigJob = GetPassiveScanConfigJobOrThrow(yamlDocument); if (!passiveScanConfigJob.Children.ContainsKey("rules")) passiveScanConfigJob.Add("rules", new YamlSequenceNode()); @@ -84,7 +85,8 @@ public static YamlDocument DisablePassiveScanRule(this YamlDocument yamlDocument } /// - /// Disable a certain ZAP active scan rule. + /// Disable a certain ZAP active scan rule for the whole scan. If you only want to disable a rule for a given page, + /// use instead. /// /// The ID of the rule. In the scan report, this is usually displayed as "Plugin Id". /// @@ -125,6 +127,62 @@ public static YamlDocument DisableActiveScanRule(this YamlDocument yamlDocument, return yamlDocument; } + /// + /// Disables a rule (can be any rule, including e.g. both active or passive scan rules) for just URLs matching the + /// given regular expression pattern. + /// + /// The ID of the rule. In the scan report, this is usually displayed as "Plugin Id". + /// + /// A regular expression pattern to match URLs against. This should be a regex pattern that matches the whole + /// absolute URL, so something like ".*blog.*" to match /blog, /blog/my-post, etc. + /// + /// + /// The human-readable name of the rule. Not required to turn off the rule, and its value doesn't matter. It's just + /// useful for the readability of the method call. + /// + /// + /// If you disable the rule because it's a false positive, then set this to . This helps the + /// development of ZAP by collecting which rules have the highest false positive rate (see ). + /// + public static YamlDocument DisableRuleForUrl( + this YamlDocument yamlDocument, + int ruleId, + string urlMatchingRegexPattern, + string ruleName = "", + bool isFalsePositive = false) + { + var jobs = yamlDocument.GetJobs(); + + if (jobs.FirstOrDefault(job => (string)job["type"] == "alertFilter") is not YamlMappingNode alertFilterJob) + { + alertFilterJob = new YamlMappingNode + { + { "type", "alertFilter" }, + { "name", "alertFilter" }, + }; + + var passiveScanConfigJob = GetPassiveScanConfigJobOrThrow(yamlDocument); + var passiveScanConfigIndex = jobs.Children.IndexOf(passiveScanConfigJob); + jobs.Children.Insert(passiveScanConfigIndex + 1, alertFilterJob); + } + + if (!alertFilterJob.Children.ContainsKey("alertFilters")) alertFilterJob.Add("alertFilters", new YamlSequenceNode()); + + var newRule = new YamlMappingNode + { + { "ruleId", ruleId.ToTechnicalString() }, + { "ruleName", ruleName }, + { "url", urlMatchingRegexPattern }, + { "urlRegex", "true" }, + { "newRisk", isFalsePositive ? "False Positive" : "Info" }, + }; + + ((YamlSequenceNode)alertFilterJob["alertFilters"]).Add(newRule); + + return yamlDocument; + } + /// /// Gets cast to . /// @@ -171,4 +229,13 @@ public static YamlDocument MergeFrom(this YamlDocument baseDocument, YamlDocumen return baseDocument; } + + private static YamlMappingNode GetPassiveScanConfigJobOrThrow(YamlDocument yamlDocument) + { + var jobs = yamlDocument.GetJobs(); + + return (YamlMappingNode)jobs.FirstOrDefault(job => (string)job["type"] == "passiveScan-config") ?? + throw new ArgumentException( + "No job with the type \"passiveScan-config\" found in the Automation Framework Plan so the rule can't be added."); + } } From b983c30717794c7e1d1ecf3d7ffd87aaf0e20d1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Mon, 20 Nov 2023 20:37:17 +0100 Subject: [PATCH 057/129] Adding simplified fluent configuration --- .../Tests/SecurityScanningTests.cs | 36 ++-- Lombiq.Tests.UI/Docs/SecurityScanning.md | 2 +- .../SecurityScanConfiguration.cs | 172 ++++++++++++++++++ .../SecurityScanningConfiguration.cs | 5 +- ...SecurityScanningUITestContextExtensions.cs | 100 ++++------ .../YamlDocumentExtensions.cs | 48 ++++- .../SecurityScanning/ZapManager.cs | 53 ------ 7 files changed, 266 insertions(+), 150 deletions(-) create mode 100644 Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index f0f0c6f8d..67e50f74a 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -43,31 +43,29 @@ public Task BasicSecurityScanShouldPass(Browser browser) => // Time for some custom configuration! While this scan also runs the Baseline scan, it does this with several // adjustments: + // - Also runs ZAP's Ajax Spider (https://www.zaproxy.org/docs/desktop/addons/ajax-spider/automation/). This is + // usually not just unnecessary for a website that's not an SPA, but also slows the scan down by a lot. However, + // if you have an SPA, you need to use it. + // - Excludes certain URLs from the scan completely. Use this if you don't want ZAP to process certain URLs at all. + // - Disables the "Server Leaks Information via "X-Powered-By" HTTP Response Header Field(s)" alert of ZAP's passive + // scan for the whole scan. This is because by default, Orchard Core sends an "X-Powered-By: OrchardCore" header. + // If you want airtight security, you might want to turn this off, but for the sake of example we just ignore the + // alert here. + // - Also disables the "Content Security Policy (CSP) Header Not Set" rule but only for the /about page. Use this to + // disable rules more specifically instead of the whole scan. // - The assertion on the scan results is custom. Use this if you (conditionally) want to assert on the results - // differently from the default "no scanning alert is allowed" assertion. - // - The plan is modified to also run ZAP's Ajax Spider - // (https://www.zaproxy.org/docs/desktop/addons/ajax-spider/automation/). This is usually not just unnecessary for - // a website that's not an SPA, but also slows the scan down by a lot. However, if you have an SPA, you need to - // use it. - // - The plan is also modified with an exclusion regex pattern. You can use this to exclude certain URLs from the - // scan. - // - We disable the "Server Leaks Information via "X-Powered-By" HTTP Response Header Field(s)" alert of ZAP's - // passive scan for the whole scan. This is because by default, Orchard Core sends an "X-Powered-By: OrchardCore" - // header. If you want airtight security, you might want to turn this off, but for the sake of example we just - // ignore the alert here. - // - We also disable the "Content Security Policy (CSP) Header Not Set" rule but only for the /about page. + // differently from the global context.Configuration.SecurityScanningConfiguration.AssertSecurityScanResult. The + // default there is "no scanning alert is allowed". [Theory, Chrome] public Task SecurityScanWithCustomConfigurationShouldPass(Browser browser) => ExecuteTestAfterSetupAsync( async context => await context.RunAndAssertBaselineSecurityScanAsync( - assertSecurityScanResult: sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeLessThan(200), - modifyPlan: plan => - plan - .AddSpiderAjaxAfterSpider() - .AddExcludePathsRegex(".*blog.*") + configuration => configuration + ////.UseAjaxSpider() // This is quite slow so just showing you here but not running it. + .ExcludeUrlWithRegex(".*blog.*") .DisablePassiveScanRule(10037, "Server Leaks Information via \"X-Powered-By\" HTTP Response Header Field(s)") - .DisableRuleForUrl(10038, ".*/about", "Content Security Policy (CSP) Header Not Set") - .CompletedTaskAsync()), + .DisableScanRuleForUrlWithRegex(".*/about", 10038, "Content Security Policy (CSP) Header Not Set"), + sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeLessThan(200)), browser); // Overriding the default setup so we can have a simpler site, simplifying the security scan for the purpose of this diff --git a/Lombiq.Tests.UI/Docs/SecurityScanning.md b/Lombiq.Tests.UI/Docs/SecurityScanning.md index e623ebed4..c19244407 100644 --- a/Lombiq.Tests.UI/Docs/SecurityScanning.md +++ b/Lombiq.Tests.UI/Docs/SecurityScanning.md @@ -8,7 +8,7 @@ You an create detailed security scans of your app with [Zed Attack Proxy (ZAP)]( - The most important default ZAP scans, Baseline, Full, GraphQL, and OpenAPI scans are included and readily usable. Note that these are modified to be more applicable to Orchard Core apps run on localhost during a UI testing scenario (notably, [`ajaxSpider`](https://www.zaproxy.org/docs/desktop/addons/ajax-spider/automation/) is removed, since most Orchard Core apps don't need it but it takes a lot of time). If you want to scan remote (and especially production) apps, then you'll need to create your own scans based on ZAP's default ones. These can then be run from inside UI tests too. - You can assert on scan results and thus fail the test if there are security warnings. -- Since we use [ZAP's Automation Framework](https://www.zaproxy.org/docs/automate/automation-framework/) for configuration, you have complete and detailed control over how the scans are configured. +- Since we use [ZAP's Automation Framework](https://www.zaproxy.org/docs/automate/automation-framework/) for configuration, you have complete and detailed control over how the scans are configured, but you can also start with a simple configuration available in the .NET API. - [SARIF](https://sarifweb.azurewebsites.net/) reports are available to integrate with other InfoSec tools. ## Working with ZAP in the Lombiq UI Testing Toolbox diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs new file mode 100644 index 000000000..e35aab452 --- /dev/null +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using YamlDotNet.RepresentationModel; + +namespace Lombiq.Tests.UI.SecurityScanning; + +/// +/// High-level configuration for a security scan with Zed Attack Proxy (ZAP). +/// +/// +/// +/// This class and intentionally use different terminology, the latter assuming you +/// know ZAP. Here, we provide a simplified configuration for people who just want to use security scans without having +/// to understand ZAP's configuration too much. +/// +/// +public class SecurityScanConfiguration +{ + public Uri StartUri { get; private set; } + public bool AjaxSpiderIsUsed { get; private set; } + public IList ExcludedUrlRegexPatterns { get; } = new List(); + public IList DisabledActiveScanRules { get; } = new List(); + public IList DisabledPassiveScanRules { get; } = new List(); + public IDictionary DisabledRulesForUrls { get; } = new Dictionary(); + public IList> ZapPlanModifiers { get; } = new List>(); + + internal SecurityScanConfiguration() + { + } + + /// + /// Sets the start URL under the app where to start the scan from. + /// + /// The under the app where to start the scan from. + public SecurityScanConfiguration StartAtUri(Uri startUri) + { + StartUri = startUri; + return this; + } + + /// + /// Enables the ZAP Ajax + /// Spider. This is useful if you have an SPA; it unnecessarily slows down the scan otherwise. + /// + public SecurityScanConfiguration UseAjaxSpider() + { + AjaxSpiderIsUsed = true; + return this; + } + + /// + /// Excludes a given URL from the scan completely. + /// + /// + /// The regex pattern to match URLs against to exclude them. These should be patterns that match the whole absolute + /// URL, so something like ".*blog.*" to match /blog, /blog/my-post, etc. + /// + public SecurityScanConfiguration ExcludeUrlWithRegex(string excludedUrlRegex) + { + ExcludedUrlRegexPatterns.Add(excludedUrlRegex); + return this; + } + + /// + /// Disable a certain active scan rule for the whole scan. If you only want to disable a rule for a given page, use + /// instead. + /// + /// The ID of the rule. In the scan report, this is usually displayed as "Plugin Id". + /// + /// The human-readable name of the rule. Not required to turn off the rule, and its value doesn't matter. It's just + /// useful for the readability of the method call. + /// + public SecurityScanConfiguration DisableActiveScanRule(int id, string name = "") + { + DisabledActiveScanRules.Add(new ScanRule(id, name)); + return this; + } + + /// + /// Disable a certain passive scan rule for the whole scan. If you only want to disable a rule for a given page, use + /// instead. + /// + /// The ID of the rule. In the scan report, this is usually displayed as "Plugin Id". + /// + /// The human-readable name of the rule. Not required to turn off the rule, and its value doesn't matter. It's just + /// useful for the readability of the method call. + /// + public SecurityScanConfiguration DisablePassiveScanRule(int id, string name = "") + { + DisabledPassiveScanRules.Add(new ScanRule(id, name)); + return this; + } + + /// + /// Disables a rule (can be any rule, including e.g. both active or passive scan rules) for just URLs matching the + /// given regular expression pattern. + /// + /// + /// A regular expression pattern to match URLs against. This should be a regex pattern that matches the whole + /// absolute URL, so something like ".*blog.*" to match /blog, /blog/my-post, etc. + /// + /// The ID of the rule. In the scan report, this is usually displayed as "Plugin Id". + /// + /// The human-readable name of the rule. Not required to turn off the rule, and its value doesn't matter. It's just + /// useful for the readability of the method call. + /// + public SecurityScanConfiguration DisableScanRuleForUrlWithRegex(string urlRegex, int ruleId, string ruleName = "") + { + DisabledRulesForUrls[urlRegex] = new ScanRule(ruleId, ruleName); + return this; + } + + /// + /// Modifies the Automation Framework + /// plan of Zed Attack Proxy (ZAP), the tool used for the security scan. + /// You can use this to do any arbitrary ZAP configuration. + /// + /// + /// A delegate to modify the deserialized representation of the ZAP Automation Framework plan in YAML. + /// + public SecurityScanConfiguration ModifyZapPlan(Func modifyPlan) + { + ZapPlanModifiers.Add(modifyPlan); + return this; + } + + /// + /// Modifies the Automation Framework + /// plan of Zed Attack Proxy (ZAP), the tool used for the security scan. + /// You can use this to do any arbitrary ZAP configuration. + /// + /// + /// A delegate to modify the deserialized representation of the ZAP Automation Framework plan in YAML. + /// + public SecurityScanConfiguration ModifyZapPlan(Action modifyPlan) + { + ZapPlanModifiers.Add(yamlDocument => + { + modifyPlan(yamlDocument); + return Task.CompletedTask; + }); + + return this; + } + + internal async Task ApplyToPlanAsync(YamlDocument yamlDocument) + { + yamlDocument.SetStartUrl(StartUri); + if (AjaxSpiderIsUsed) yamlDocument.AddSpiderAjaxAfterSpider(); + yamlDocument.AddExcludePathsRegex(ExcludedUrlRegexPatterns.ToArray()); + foreach (var rule in DisabledActiveScanRules) yamlDocument.DisableActiveScanRule(rule.Id, rule.Name); + foreach (var rule in DisabledPassiveScanRules) yamlDocument.DisablePassiveScanRule(rule.Id, rule.Name); + foreach (var kvp in DisabledRulesForUrls) yamlDocument.AddAlertFilter(kvp.Key, kvp.Value.Id, kvp.Value.Name); + foreach (var modifier in ZapPlanModifiers) await modifier(yamlDocument); + } +} + +public class ScanRule +{ + public int Id { get; set; } + public string Name { get; set; } + + public ScanRule(int id, string name) + { + Id = id; + Name = name; + } +} diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs index fdf831f66..0bf3df4f7 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningConfiguration.cs @@ -20,9 +20,10 @@ public class SecurityScanningConfiguration internal bool CreateReportAlways { get; set; } /// - /// Gets or sets a delegate that may modify the deserialized representation of the ZAP Automation Framework plan in YAML. + /// Gets or sets a delegate that may modify the deserialized representation of the ZAP Automation Framework plan in + /// YAML. /// - public Func ZapAutomationFrameworkYamlModifier { get; set; } + public Func ZapAutomationFrameworkPlanModifier { get; set; } /// /// Gets or sets a delegate to run assertions on the when security scanning happens. diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs index 6ceb52b29..e6c40a7a8 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs @@ -4,7 +4,6 @@ using Microsoft.CodeAnalysis.Sarif; using System; using System.Threading.Tasks; -using YamlDotNet.RepresentationModel; namespace Lombiq.Tests.UI.SecurityScanning; @@ -16,24 +15,17 @@ public static class SecurityScanningUITestContextExtensions /// href="https://www.zaproxy.org/docs/docker/baseline-scan/"/> for the official docs on the legacy version of this /// scan). /// - /// - /// The under the app where to start the scan from. If not provided, defaults to the current URL. - /// - /// - /// A delegate to modify the deserialized representation of the ZAP Automation Framework plan in YAML. - /// + /// A delegate to configure the security scan in detail. /// /// A delegate to run assertions on the one the scan finishes. /// public static Task RunAndAssertBaselineSecurityScanAsync( this UITestContext context, - Uri startUri = null, - Func modifyPlan = null, + Action configure = null, Action assertSecurityScanResult = null) => context.RunAndAssertSecurityScanAsync( AutomationFrameworkPlanPaths.BaselinePlanPath, - startUri, - modifyPlan, + configure, assertSecurityScanResult); /// @@ -42,24 +34,18 @@ public static Task RunAndAssertBaselineSecurityScanAsync( /// for the official docs on the legacy version of this /// scan). /// - /// - /// The under the app where to start the scan from. If not provided, defaults to the current URL. - /// - /// - /// A delegate to modify the deserialized representation of the ZAP Automation Framework plan in YAML. - /// + /// A delegate to configure the security scan in detail. /// /// A delegate to run assertions on the one the scan finishes. /// public static Task RunAndAssertFullSecurityScanAsync( this UITestContext context, Uri startUri = null, - Func modifyPlan = null, + Action configure = null, Action assertSecurityScanResult = null) => context.RunAndAssertSecurityScanAsync( AutomationFrameworkPlanPaths.FullScanPlanPath, - startUri, - modifyPlan, + configure, assertSecurityScanResult); /// @@ -68,24 +54,18 @@ public static Task RunAndAssertFullSecurityScanAsync( /// href="https://www.zaproxy.org/docs/desktop/addons/graphql-support/"/> for the official docs on ZAP's GraphQL /// support). /// - /// - /// The under the app where to start the scan from. If not provided, defaults to the current URL. - /// - /// - /// A delegate to modify the deserialized representation of the ZAP Automation Framework plan in YAML. - /// + /// A delegate to configure the security scan in detail. /// /// A delegate to run assertions on the one the scan finishes. /// public static Task RunAndAssertGraphQLSecurityScanAsync( this UITestContext context, Uri startUri = null, - Func modifyPlan = null, + Action configure = null, Action assertSecurityScanResult = null) => context.RunAndAssertSecurityScanAsync( AutomationFrameworkPlanPaths.GraphQLPlanPath, - startUri, - modifyPlan, + configure, assertSecurityScanResult); /// @@ -94,24 +74,18 @@ public static Task RunAndAssertGraphQLSecurityScanAsync( /// href="https://www.zaproxy.org/docs/desktop/addons/openapi-support/"/> for the official docs on ZAP's GraphQL /// support). /// - /// - /// The under the app where to start the scan from. If not provided, defaults to the current URL. - /// - /// - /// A delegate to modify the deserialized representation of the ZAP Automation Framework plan in YAML. - /// + /// A delegate to configure the security scan in detail. /// /// A delegate to run assertions on the one the scan finishes. /// public static Task RunAndAssertOpenApiSecurityScanAsync( this UITestContext context, Uri startUri = null, - Func modifyPlan = null, + Action configure = null, Action assertSecurityScanResult = null) => context.RunAndAssertSecurityScanAsync( AutomationFrameworkPlanPaths.OpenAPIPlanPath, - startUri, - modifyPlan, + configure, assertSecurityScanResult); /// @@ -122,12 +96,7 @@ public static Task RunAndAssertOpenApiSecurityScanAsync( /// File system path to the YAML configuration file of ZAP's Automation Framework. See for details. /// - /// - /// The under the app where to start the scan from. If not provided, defaults to the current URL. - /// - /// - /// A delegate to modify the deserialized representation of the ZAP Automation Framework plan in YAML. - /// + /// A delegate to configure the security scan in detail. /// /// A delegate to run assertions on the one the scan finishes. /// @@ -138,29 +107,18 @@ public static Task RunAndAssertOpenApiSecurityScanAsync( public static async Task RunAndAssertSecurityScanAsync( this UITestContext context, string automationFrameworkYamlPath, - Uri startUri = null, - Func modifyPlan = null, + Action configure = null, Action assertSecurityScanResult = null) { var configuration = context.Configuration.SecurityScanningConfiguration; - async Task CompositeModifyPlan(YamlDocument plan) - { - if (configuration.ZapAutomationFrameworkYamlModifier != null) - { - await configuration.ZapAutomationFrameworkYamlModifier(context, plan); - } - - if (modifyPlan != null) await modifyPlan(plan); - } - SecurityScanResult result = null; try { - result = await context.RunSecurityScanAsync(automationFrameworkYamlPath, startUri, CompositeModifyPlan); + result = await context.RunSecurityScanAsync(automationFrameworkYamlPath, configure); if (assertSecurityScanResult != null) assertSecurityScanResult(result.SarifLog); - else configuration.AssertSecurityScanResult(context, result.SarifLog); + else configuration?.AssertSecurityScanResult(context, result.SarifLog); if (configuration.CreateReportAlways) context.AppendDirectoryToFailureDump(result.ReportsDirectoryPath); } @@ -178,12 +136,7 @@ async Task CompositeModifyPlan(YamlDocument plan) /// File system path to the YAML configuration file of ZAP's Automation Framework. See /// for details. /// - /// - /// The under the app where to start the scan from. If not provided, defaults to the current URL. - /// - /// - /// A delegate to modify the deserialized representation of the ZAP Automation Framework plan in YAML. - /// + /// A delegate to configure the security scan in detail. /// /// A instance containing the SARIF () report of the scan. @@ -191,7 +144,20 @@ async Task CompositeModifyPlan(YamlDocument plan) public static Task RunSecurityScanAsync( this UITestContext context, string automationFrameworkYamlPath, - Uri startUri = null, - Func modifyPlan = null) => - context.ZapManager.RunSecurityScanAsync(context, automationFrameworkYamlPath, startUri ?? context.GetCurrentUri(), modifyPlan); + Action configure = null) + { + var configuration = new SecurityScanConfiguration(); + + configuration.StartAtUri(context.GetCurrentUri()); + + if (context.Configuration.SecurityScanningConfiguration.ZapAutomationFrameworkPlanModifier != null) + { + configuration.ModifyZapPlan(async plan => + await context.Configuration.SecurityScanningConfiguration.ZapAutomationFrameworkPlanModifier(context, plan)); + } + + configure?.Invoke(configuration); + + return context.ZapManager.RunSecurityScanAsync(context, automationFrameworkYamlPath, configuration.ApplyToPlanAsync); + } } diff --git a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs index c49651e55..291b05e18 100644 --- a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs @@ -7,6 +7,36 @@ namespace Lombiq.Tests.UI.SecurityScanning; public static class YamlDocumentExtensions { + /// + /// Sets the start URL under the app where to start the scan from. + /// + /// The under the app where to start the scan from. + /// + /// Thrown when the ZAP Automation Framework plan contains more than a single URL in the "urls" section. + /// + public static void SetStartUrl(this YamlDocument yamlDocument, Uri startUri) + { + var currentContext = YamlHelper.GetCurrentContext(yamlDocument); + + // Setting includePaths in the context is not necessary because by default everything under "urls" will be + // scanned. + + if (!currentContext.Children.ContainsKey("urls")) currentContext.Add("urls", new YamlSequenceNode()); + + var urls = (YamlSequenceNode)currentContext["urls"]; + var urlsCount = urls.Count(); + + if (urlsCount > 1) + { + throw new ArgumentException( + "The context in the ZAP Automation Framework YAML file should contain at most a single URL in the \"urls\" section."); + } + + if (urlsCount == 1) urls.Children.Clear(); + + urls.Add(startUri.ToString()); + } + /// /// Adds the ZAP Ajax Spider /// to the ZAP Automation Framework plan, just after the "spider" job. @@ -55,8 +85,9 @@ public static YamlDocument AddExcludePathsRegex(this YamlDocument yamlDocument, } /// - /// Disable a certain ZAP passive scan rule for the whole scan. If you only want to disable a rule for a given page, - /// use instead. + /// Disable a certain ZAP passive scan rule for the whole scan in the ZAP Automation Framework plan. If you only + /// want to disable a rule for a given page, use + /// instead. /// /// The ID of the rule. In the scan report, this is usually displayed as "Plugin Id". /// @@ -85,8 +116,9 @@ public static YamlDocument DisablePassiveScanRule(this YamlDocument yamlDocument } /// - /// Disable a certain ZAP active scan rule for the whole scan. If you only want to disable a rule for a given page, - /// use instead. + /// Disable a certain ZAP active scan rule for the whole scan in the ZAP Automation Framework plan. If you only want + /// to disable a rule for a given page, use + /// instead. /// /// The ID of the rule. In the scan report, this is usually displayed as "Plugin Id". /// @@ -128,8 +160,8 @@ public static YamlDocument DisableActiveScanRule(this YamlDocument yamlDocument, } /// - /// Disables a rule (can be any rule, including e.g. both active or passive scan rules) for just URLs matching the - /// given regular expression pattern. + /// Adds an Alert Filter to the + /// ZAP Automation Framework plan. /// /// The ID of the rule. In the scan report, this is usually displayed as "Plugin Id". /// @@ -145,10 +177,10 @@ public static YamlDocument DisableActiveScanRule(this YamlDocument yamlDocument, /// development of ZAP by collecting which rules have the highest false positive rate (see ). /// - public static YamlDocument DisableRuleForUrl( + public static YamlDocument AddAlertFilter( this YamlDocument yamlDocument, - int ruleId, string urlMatchingRegexPattern, + int ruleId, string ruleName = "", bool isFalsePositive = false) { diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index a5157cfb6..59f0a9d13 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -40,36 +40,6 @@ public sealed class ZapManager : IAsyncDisposable internal ZapManager(ITestOutputHelper testOutputHelper) => _testOutputHelper = testOutputHelper; - /// - /// Run a Zed Attack Proxy (ZAP) security scan against an app. - /// - /// The of the currently executing test. - /// - /// File system path to the YAML configuration file of ZAP's Automation Framework. See - /// for details. - /// - /// The under the app where to start the scan from. - /// - /// A delegate to modify the deserialized representation of the ZAP Automation Framework plan in YAML. - /// - /// - /// A instance containing the SARIF () report of the scan. - /// - public Task RunSecurityScanAsync( - UITestContext context, - string automationFrameworkYamlPath, - Uri startUri, - Func modifyPlan = null) => - RunSecurityScanAsync( - context, - automationFrameworkYamlPath, - async configuration => - { - SetStartUrlInPlan(configuration, startUri); - if (modifyPlan != null) await modifyPlan(configuration); - }); - /// /// Run a Zed Attack Proxy (ZAP) security scan against an app. /// @@ -228,29 +198,6 @@ private async Task EnsureInitializedAsync() } } - private static void SetStartUrlInPlan(YamlDocument yamlDocument, Uri startUri) - { - var currentContext = YamlHelper.GetCurrentContext(yamlDocument); - - // Setting URLs in the context. - // Setting includePaths in the context is not necessary because by default everything under urls will be scanned. - - if (!currentContext.Children.ContainsKey("urls")) currentContext.Add("urls", new YamlSequenceNode()); - - var urls = (YamlSequenceNode)currentContext["urls"]; - var urlsCount = urls.Count(); - - if (urlsCount > 1) - { - throw new ArgumentException( - "The context in the ZAP Automation Framework YAML file should contain at most a single url in the urls section."); - } - - if (urlsCount == 1) urls.Children.Clear(); - - urls.Add(startUri.ToString()); - } - private static async Task PreparePlanAsync(string yamlFilePath, Func modifyPlan) { var yamlDocument = YamlHelper.LoadDocument(yamlFilePath); From c03f54a4330a70d78d9391244fa2ac7b150f0943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Mon, 20 Nov 2023 23:30:37 +0100 Subject: [PATCH 058/129] Making current user shortcut controller more explicit --- Lombiq.Tests.UI.Shortcuts/Controllers/CurrentUserController.cs | 2 +- Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI.Shortcuts/Controllers/CurrentUserController.cs b/Lombiq.Tests.UI.Shortcuts/Controllers/CurrentUserController.cs index 926580e3b..b4ef7c4e5 100644 --- a/Lombiq.Tests.UI.Shortcuts/Controllers/CurrentUserController.cs +++ b/Lombiq.Tests.UI.Shortcuts/Controllers/CurrentUserController.cs @@ -9,5 +9,5 @@ public class CurrentUserController : Controller { // Needs to return a string even if there's no user, otherwise it'd return an HTTP 204 without a body, see: // https://weblog.west-wind.com/posts/2020/Feb/24/Null-API-Responses-and-HTTP-204-Results-in-ASPNET-Core. - public string Index() => "UserName: " + User?.Identity?.Name; + public string Index() => User != null ? "UserName: " + User?.Identity?.Name : "Unauthenticated"; } diff --git a/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs index e2a6f0478..20eb13d95 100644 --- a/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs @@ -108,6 +108,7 @@ public static async Task GetCurrentUserNameAsync(this UITestContext cont { await context.GoToAsync(controller => controller.Index()); var userNameContainer = context.Get(By.CssSelector("pre")).Text; + if (userNameContainer == "Unauthenticated") return string.Empty; return userNameContainer["UserName: ".Length..]; } From 0b6f0c237ec539a45a78e2326791faafecf63963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Mon, 20 Nov 2023 23:56:36 +0100 Subject: [PATCH 059/129] Removing useless "At least 100 URLs found" sample test from scans --- .../AutomationFrameworkPlans/Baseline.yml | 8 -------- .../AutomationFrameworkPlans/FullScan.yml | 8 -------- 2 files changed, 16 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml index 0b2a99a2a..cae880358 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml @@ -35,14 +35,6 @@ jobs: - parameters: {} name: "spider" type: "spider" - tests: - - onFail: "INFO" - statistic: "automation.spider.urls.added" - site: "" - operator: ">=" - value: 100 - name: "At least 100 URLs found" - type: "stats" - parameters: {} name: "passiveScan-wait" type: "passiveScan-wait" diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/FullScan.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/FullScan.yml index 562b14879..46162ecd6 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/FullScan.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/FullScan.yml @@ -35,14 +35,6 @@ jobs: - parameters: {} name: "spider" type: "spider" - tests: - - onFail: "INFO" - statistic: "automation.spider.urls.added" - site: "" - operator: ">=" - value: 100 - name: "At least 100 URLs found" - type: "stats" - parameters: {} name: "passiveScan-wait" type: "passiveScan-wait" From 1e2fca69b84e315f34112c9a3d1ca3f5dd2ebaed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Tue, 21 Nov 2023 00:02:55 +0100 Subject: [PATCH 060/129] Fixing CurrentUserController user retrieval logic --- Lombiq.Tests.UI.Shortcuts/Controllers/CurrentUserController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI.Shortcuts/Controllers/CurrentUserController.cs b/Lombiq.Tests.UI.Shortcuts/Controllers/CurrentUserController.cs index b4ef7c4e5..08f46b510 100644 --- a/Lombiq.Tests.UI.Shortcuts/Controllers/CurrentUserController.cs +++ b/Lombiq.Tests.UI.Shortcuts/Controllers/CurrentUserController.cs @@ -9,5 +9,5 @@ public class CurrentUserController : Controller { // Needs to return a string even if there's no user, otherwise it'd return an HTTP 204 without a body, see: // https://weblog.west-wind.com/posts/2020/Feb/24/Null-API-Responses-and-HTTP-204-Results-in-ASPNET-Core. - public string Index() => User != null ? "UserName: " + User?.Identity?.Name : "Unauthenticated"; + public string Index() => User.Identity.IsAuthenticated ? "UserName: " + User.Identity.Name : "Unauthenticated"; } From 9e027f4389c1e43f8e1c07b0b2ac7b691ca658e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Tue, 21 Nov 2023 02:09:40 +0100 Subject: [PATCH 061/129] Support login --- .../Tests/SecurityScanningTests.cs | 7 +- Lombiq.Tests.UI/Docs/SecurityScanning.md | 5 +- .../TypedRouteUITestContextExtensions.cs | 35 +++++- Lombiq.Tests.UI/Lombiq.Tests.UI.csproj | 3 +- .../RequestorJob.yml | 12 ++ .../{SpiderAjax.yml => SpiderAjaxJob.yml} | 0 .../AutomationFrameworkPlanFragmentsPaths.cs | 4 +- .../SecurityScanConfiguration.cs | 58 +++++++-- ...SecurityScanningUITestContextExtensions.cs | 5 +- .../YamlDocumentExtensions.cs | 110 +++++++++++++++--- .../SecurityScanning/YamlHelper.cs | 23 ---- 11 files changed, 203 insertions(+), 59 deletions(-) create mode 100644 Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragments/RequestorJob.yml rename Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragments/{SpiderAjax.yml => SpiderAjaxJob.yml} (100%) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 67e50f74a..c85c3c28d 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -53,6 +53,10 @@ public Task BasicSecurityScanShouldPass(Browser browser) => // alert here. // - Also disables the "Content Security Policy (CSP) Header Not Set" rule but only for the /about page. Use this to // disable rules more specifically instead of the whole scan. + // - Configures sign in with a user account. This is what the scan will start with. With the Blog recipe it doesn't + // matter too much, since nothing on the frontend will change, but you can use this to scan authenticated features + // too. Note that since ZAP uses its own spider, not the browser accessed by the test, user sessions are not + // shared, so such an explicit sign in is necessary. // - The assertion on the scan results is custom. Use this if you (conditionally) want to assert on the results // differently from the global context.Configuration.SecurityScanningConfiguration.AssertSecurityScanResult. The // default there is "no scanning alert is allowed". @@ -64,7 +68,8 @@ public Task SecurityScanWithCustomConfigurationShouldPass(Browser browser) => ////.UseAjaxSpider() // This is quite slow so just showing you here but not running it. .ExcludeUrlWithRegex(".*blog.*") .DisablePassiveScanRule(10037, "Server Leaks Information via \"X-Powered-By\" HTTP Response Header Field(s)") - .DisableScanRuleForUrlWithRegex(".*/about", 10038, "Content Security Policy (CSP) Header Not Set"), + .DisableScanRuleForUrlWithRegex(".*/about", 10038, "Content Security Policy (CSP) Header Not Set") + .SignIn(), sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeLessThan(200)), browser); diff --git a/Lombiq.Tests.UI/Docs/SecurityScanning.md b/Lombiq.Tests.UI/Docs/SecurityScanning.md index c19244407..469b11116 100644 --- a/Lombiq.Tests.UI/Docs/SecurityScanning.md +++ b/Lombiq.Tests.UI/Docs/SecurityScanning.md @@ -15,11 +15,12 @@ You an create detailed security scans of your app with [Zed Attack Proxy (ZAP)]( - We recommend you first check out the related samples in the [`Lombiq.Tests.UI.Samples` project](../../Lombiq.Tests.UI.Samples). - If you're new to ZAP, you can start learning by checking out [ZAP's getting started guide](https://www.zaproxy.org/getting-started/), as well as the [ZAP Chat Videos](https://www.zaproxy.org/zap-chat/) (especially [ZAP Chat 06 Automation Introduction](https://www.youtube.com/watch?v=PnCbIAnauD8)). -- Be aware that ZAP scans run with an internally managed browser instance, not in the browser launched by the test. +- Be aware that ZAP scans run its own spider or with an internally managed browser instance, not in the browser launched by the test. - While ZAP is fully managed for you, Docker needs to be available and running to host the ZAP instance. On your development machine you can install [Docker Desktop](https://www.docker.com/products/docker-desktop/). - The full scan of a website with even just 1-200 pages can take 15-30 minutes. So, be careful to fine-tune the ZAP configuration to make it suitable for your app. ## Troubleshooting - If you're unsure what happens in a scan, run the [ZAP desktop app](https://www.zaproxy.org/download/) and load the Automation Framework plan's YAML file into it. If you use the default scans, then these will be available under the build output directory (like _bin/Debug_) under _SecurityScanning/AutomationFrameworkPlans_. Then, you can open and run them as demonstrated [in this video](https://youtu.be/PnCbIAnauD8?si=u0vi63Uvv9wZINzb&t=1173). -- If an alert is a false positive, follow [the official docs](https://www.zaproxy.org/faq/how-do-i-handle-a-false-positive/). You can use the [`alertFilter` job](https://www.zaproxy.org/docs/desktop/addons/alert-filters/automation/) to ignore alerts in very specific conditions. +- If an alert is a false positive, follow [the official docs](https://www.zaproxy.org/faq/how-do-i-handle-a-false-positive/). You can use the [`alertFilter` job](https://www.zaproxy.org/docs/desktop/addons/alert-filters/automation/) to ignore alerts in very specific conditions. You can also access this via the .NET configuration API. +- ZAP didn't find everything in your app? By default, ZAP has a crawl depth of 5 for its standard spider and 10 for its AJAX spider. Set `maxDepth` (and `maxChildren`) [for `spider`](https://www.zaproxy.org/docs/desktop/addons/automation-framework/job-spider/) and `maxCrawlDepth` [for `spiderAjax`](https://www.zaproxy.org/docs/desktop/addons/ajax-spider/automation/). diff --git a/Lombiq.Tests.UI/Extensions/TypedRouteUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/TypedRouteUITestContextExtensions.cs index 7467663bb..cd2033e44 100644 --- a/Lombiq.Tests.UI/Extensions/TypedRouteUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/TypedRouteUITestContextExtensions.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using OrchardCore.Admin; using System; +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Threading.Tasks; @@ -20,9 +21,7 @@ public static Task GoToAsync( Expression> actionExpression, params (string Key, object Value)[] additionalArguments) where TController : ControllerBase => - context.GoToRelativeUrlAsync(TypedRoute - .CreateFromExpression(actionExpression, additionalArguments, CreateServiceProvider(context)) - .ToString()); + context.GoToRelativeUrlAsync(context.GetRelativeUrlOfAction(actionExpression, additionalArguments)); /// /// Navigates to the relative URL generated by for the ( Expression> actionExpressionAsync, params (string Key, object Value)[] additionalArguments) where TController : ControllerBase => - context.GoToRelativeUrlAsync(TypedRoute + context.GoToRelativeUrlAsync(context.GetRelativeUrlOfAction(actionExpressionAsync, additionalArguments)); + + /// + /// Gets the relative URL generated by for the in the + /// . + /// + [SuppressMessage("Design", "CA1055:URI-like return values should not be strings", Justification = "Other APIs need it as a string.")] + public static string GetRelativeUrlOfAction( + this UITestContext context, + Expression> actionExpression, + params (string Key, object Value)[] additionalArguments) + where TController : ControllerBase => + TypedRoute + .CreateFromExpression(actionExpression, additionalArguments, CreateServiceProvider(context)) + .ToString(); + + /// + /// Gets the relative URL generated by for the in + /// the . + /// + [SuppressMessage("Design", "CA1055:URI-like return values should not be strings", Justification = "Other APIs need it as a string.")] + public static string GetRelativeUrlOfAction( + this UITestContext context, + Expression> actionExpressionAsync, + params (string Key, object Value)[] additionalArguments) + where TController : ControllerBase => + TypedRoute .CreateFromExpression(actionExpressionAsync.StripResult(), additionalArguments, CreateServiceProvider(context)) - .ToString()); + .ToString(); private static IServiceProvider CreateServiceProvider(UITestContext context) { diff --git a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj index b3eb7d28f..12335b7d1 100644 --- a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj +++ b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj @@ -124,7 +124,8 @@ - + + diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragments/RequestorJob.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragments/RequestorJob.yml new file mode 100644 index 000000000..b1693febf --- /dev/null +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragments/RequestorJob.yml @@ -0,0 +1,12 @@ +parameters: + user: "" +requests: +- url: "" + name: "" + method: "" + httpVersion: "" + headers: [] + data: "" + responseCode: 200 +name: "requestor" +type: "requestor" diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragments/SpiderAjax.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragments/SpiderAjaxJob.yml similarity index 100% rename from Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragments/SpiderAjax.yml rename to Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragments/SpiderAjaxJob.yml diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragmentsPaths.cs b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragmentsPaths.cs index 1bb41f121..a31ae4ecd 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragmentsPaths.cs +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragmentsPaths.cs @@ -7,5 +7,7 @@ public static class AutomationFrameworkPlanFragmentsPaths private static readonly string AutomationFrameworkPlanFragmentsPath = Path.Combine("SecurityScanning", "AutomationFrameworkPlanFragments"); - public static readonly string SpiderAjaxPath = Path.Combine(AutomationFrameworkPlanFragmentsPath, "SpiderAjax.yml"); + public static readonly string SpiderAjaxJobPath = Path.Combine(AutomationFrameworkPlanFragmentsPath, "SpiderAjaxJob.yml"); + public static readonly string RequestorJobPath = + Path.Combine(AutomationFrameworkPlanFragmentsPath, "RequestorJob.yml"); } diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs index e35aab452..b3f71a459 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs @@ -1,3 +1,8 @@ +using Lombiq.HelpfulLibraries.OrchardCore.Mvc; +using Lombiq.Tests.UI.Constants; +using Lombiq.Tests.UI.Extensions; +using Lombiq.Tests.UI.Services; +using Lombiq.Tests.UI.Shortcuts.Controllers; using System; using System.Collections.Generic; using System.Linq; @@ -20,6 +25,7 @@ public class SecurityScanConfiguration { public Uri StartUri { get; private set; } public bool AjaxSpiderIsUsed { get; private set; } + public string SignInUserName { get; private set; } public IList ExcludedUrlRegexPatterns { get; } = new List(); public IList DisabledActiveScanRules { get; } = new List(); public IList DisabledPassiveScanRules { get; } = new List(); @@ -50,6 +56,17 @@ public SecurityScanConfiguration UseAjaxSpider() return this; } + /// + /// Signs in directly (see ) with the given user at the start + /// of the scan. + /// + /// The name of the user to sign in with directly. + public SecurityScanConfiguration SignIn(string userName = DefaultUser.UserName) + { + SignInUserName = userName; + return this; + } + /// /// Excludes a given URL from the scan completely. /// @@ -147,26 +164,47 @@ public SecurityScanConfiguration ModifyZapPlan(Action modifyPlan) return this; } - internal async Task ApplyToPlanAsync(YamlDocument yamlDocument) + internal async Task ApplyToPlanAsync(YamlDocument yamlDocument, UITestContext context) { yamlDocument.SetStartUrl(StartUri); if (AjaxSpiderIsUsed) yamlDocument.AddSpiderAjaxAfterSpider(); + + if (!string.IsNullOrEmpty(SignInUserName)) + { + yamlDocument.AddRequestor( + context.GetAbsoluteUri( + context.GetRelativeUrlOfAction(controller => controller.SignInDirectly(SignInUserName))) + .ToString()); + + // It might be that later such a verification for the login state will need to be needed, but this seems + // unnecessary now. + // verification: + // method: "response" + // method: "poll" + // loggedInRegex: "Unauthenticated" + // loggedOutRegex: "UserName: .*" + // pollFrequency: 60 + // pollUnits: "requests" + // pollUrl: "https://localhost:44335/Lombiq.Tests.UI.Shortcuts/CurrentUser/Index" + // pollPostData: "" + } + yamlDocument.AddExcludePathsRegex(ExcludedUrlRegexPatterns.ToArray()); foreach (var rule in DisabledActiveScanRules) yamlDocument.DisableActiveScanRule(rule.Id, rule.Name); foreach (var rule in DisabledPassiveScanRules) yamlDocument.DisablePassiveScanRule(rule.Id, rule.Name); foreach (var kvp in DisabledRulesForUrls) yamlDocument.AddAlertFilter(kvp.Key, kvp.Value.Id, kvp.Value.Name); foreach (var modifier in ZapPlanModifiers) await modifier(yamlDocument); } -} -public class ScanRule -{ - public int Id { get; set; } - public string Name { get; set; } - - public ScanRule(int id, string name) + public class ScanRule { - Id = id; - Name = name; + public int Id { get; } + public string Name { get; } + + public ScanRule(int id, string name) + { + Id = id; + Name = name; + } } } diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs index e6c40a7a8..dc80caa62 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs @@ -158,6 +158,9 @@ public static Task RunSecurityScanAsync( configure?.Invoke(configuration); - return context.ZapManager.RunSecurityScanAsync(context, automationFrameworkYamlPath, configuration.ApplyToPlanAsync); + return context.ZapManager.RunSecurityScanAsync( + context, + automationFrameworkYamlPath, + async plan => await configuration.ApplyToPlanAsync(plan, context)); } } diff --git a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs index 291b05e18..4b165818c 100644 --- a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs @@ -8,22 +8,19 @@ namespace Lombiq.Tests.UI.SecurityScanning; public static class YamlDocumentExtensions { /// - /// Sets the start URL under the app where to start the scan from. + /// Sets the start URL under the app where to start the scan from in the current context of the ZAP Automation + /// Framework plan. /// - /// The under the app where to start the scan from. + /// The absolute to start the scan from. /// /// Thrown when the ZAP Automation Framework plan contains more than a single URL in the "urls" section. /// - public static void SetStartUrl(this YamlDocument yamlDocument, Uri startUri) + public static YamlDocument SetStartUrl(this YamlDocument yamlDocument, Uri startUri) { - var currentContext = YamlHelper.GetCurrentContext(yamlDocument); - // Setting includePaths in the context is not necessary because by default everything under "urls" will be // scanned. - if (!currentContext.Children.ContainsKey("urls")) currentContext.Add("urls", new YamlSequenceNode()); - - var urls = (YamlSequenceNode)currentContext["urls"]; + var urls = yamlDocument.GetUrls(); var urlsCount = urls.Count(); if (urlsCount > 1) @@ -35,6 +32,22 @@ public static void SetStartUrl(this YamlDocument yamlDocument, Uri startUri) if (urlsCount == 1) urls.Children.Clear(); urls.Add(startUri.ToString()); + + return yamlDocument; + } + + /// + /// Adds a URL to the "urls" section of the current context of the ZAP Automation Framework plan. + /// + /// + /// The to add to the "urls" section of the current context of the ZAP Automation Framework + /// plan. + /// + public static YamlDocument AddUrl(this YamlDocument yamlDocument, Uri uri) + { + var urls = yamlDocument.GetUrls(); + urls.Add(uri.ToString()); + return yamlDocument; } /// @@ -51,11 +64,12 @@ public static YamlDocument AddSpiderAjaxAfterSpider(this YamlDocument yamlDocume var spiderJob = jobs.FirstOrDefault(job => (string)job["name"] == "spider") ?? throw new ArgumentException( - "No job named \"spider\" found in the Automation Framework Plan. We can only add ajaxSpider immediately after it."); + "No job named \"spider\" found in the Automation Framework Plan. We can only add the ajaxSpider job " + + "immediately after it."); var spiderIndex = jobs.Children.IndexOf(spiderJob); - var spiderAjax = YamlHelper.LoadDocument(AutomationFrameworkPlanFragmentsPaths.SpiderAjaxPath); - jobs.Children.Insert(spiderIndex + 1, spiderAjax.GetRootNode()); + var spiderAjaxJob = YamlHelper.LoadDocument(AutomationFrameworkPlanFragmentsPaths.SpiderAjaxJobPath); + jobs.Children.Insert(spiderIndex + 1, spiderAjaxJob.GetRootNode()); return yamlDocument; } @@ -71,7 +85,7 @@ public static YamlDocument AddSpiderAjaxAfterSpider(this YamlDocument yamlDocume /// public static YamlDocument AddExcludePathsRegex(this YamlDocument yamlDocument, params string[] excludePathsRegexPatterns) { - var currentContext = YamlHelper.GetCurrentContext(yamlDocument); + var currentContext = yamlDocument.GetCurrentContext(); if (!currentContext.Children.ContainsKey("excludePaths")) currentContext.Add("excludePaths", new YamlSequenceNode()); @@ -160,8 +174,8 @@ public static YamlDocument DisableActiveScanRule(this YamlDocument yamlDocument, } /// - /// Adds an Alert Filter to the - /// ZAP Automation Framework plan. + /// Adds an Alert Filter to the ZAP + /// Automation Framework plan. /// /// The ID of the rule. In the scan report, this is usually displayed as "Plugin Id". /// @@ -215,17 +229,83 @@ public static YamlDocument AddAlertFilter( return yamlDocument; } + /// + /// Adds a "requestor" job to the ZAP Automation Framework plan. + /// + /// The URL the requestor job will access. + /// + /// If no job named "spider" is found in the ZAP Automation Framework plan. + /// + public static YamlDocument AddRequestor(this YamlDocument yamlDocument, string url) + { + var jobs = yamlDocument.GetJobs(); + + var spiderJob = + jobs.FirstOrDefault(job => (string)job["name"] == "spider") ?? + throw new ArgumentException( + "No job named \"spider\" found in the Automation Framework Plan. We can only add the requestor job " + + "immediately before it."); + + var requestorJob = YamlHelper.LoadDocument(AutomationFrameworkPlanFragmentsPaths.RequestorJobPath).GetRootNode(); + + ((YamlScalarNode)((YamlSequenceNode)requestorJob["requests"]).Children[0]["url"]).Value = url; + + var spiderIndex = jobs.Children.IndexOf(spiderJob); + jobs.Children.Insert(spiderIndex, requestorJob); + + return yamlDocument; + } + /// /// Gets cast to . /// public static YamlMappingNode GetRootNode(this YamlDocument yamlDocument) => (YamlMappingNode)yamlDocument.RootNode; /// - /// Gets the "jobs" section of the . + /// Gets the "jobs" section of the ZAP Automation Framework plan. /// public static YamlSequenceNode GetJobs(this YamlDocument yamlDocument) => (YamlSequenceNode)yamlDocument.GetRootNode()["jobs"]; + /// + /// Gets the "urls" section of the current context in the ZAP Automation Framework plan. + /// + public static YamlSequenceNode GetUrls(this YamlDocument yamlDocument) + { + var currentContext = yamlDocument.GetCurrentContext(); + + if (!currentContext.Children.ContainsKey("urls")) currentContext.Add("urls", new YamlSequenceNode()); + + return (YamlSequenceNode)currentContext["urls"]; + } + + /// + /// Gets the first context or the one named "Default Context" from the ZAP Automation Framework plan. + /// + /// + /// Thrown if the ZAP Automation Framework plan doesn't contain a context. + /// + public static YamlMappingNode GetCurrentContext(this YamlDocument yamlDocument) + { + var contexts = (YamlSequenceNode)yamlDocument.GetRootNode()["env"]["contexts"]; + + if (!contexts.Any()) + { + throw new ArgumentException( + "The supplied ZAP Automation Framework plan YAML file should contain at least one context."); + } + + var currentContext = (YamlMappingNode)contexts[0]; + + if (contexts.Count() > 1) + { + currentContext = (YamlMappingNode)contexts.FirstOrDefault(context => context["Name"].ToString() == "Default Context") + ?? currentContext; + } + + return currentContext; + } + /// /// Shortcuts to to be able to chain extensions in an /// async method/delegate. diff --git a/Lombiq.Tests.UI/SecurityScanning/YamlHelper.cs b/Lombiq.Tests.UI/SecurityScanning/YamlHelper.cs index db4c70513..f0fadff09 100644 --- a/Lombiq.Tests.UI/SecurityScanning/YamlHelper.cs +++ b/Lombiq.Tests.UI/SecurityScanning/YamlHelper.cs @@ -1,6 +1,4 @@ -using System; using System.IO; -using System.Linq; using YamlDotNet.RepresentationModel; namespace Lombiq.Tests.UI.SecurityScanning; @@ -14,25 +12,4 @@ public static YamlDocument LoadDocument(string yamlFilePath) yamlStream.Load(streamReader); return yamlStream.Documents[0]; } - - public static YamlMappingNode GetCurrentContext(YamlDocument yamlDocument) - { - var contexts = (YamlSequenceNode)yamlDocument.GetRootNode()["env"]["contexts"]; - - if (!contexts.Any()) - { - throw new ArgumentException( - "The supplied ZAP Automation Framework YAML file should contain at least one context."); - } - - var currentContext = (YamlMappingNode)contexts[0]; - - if (contexts.Count() > 1) - { - currentContext = (YamlMappingNode)contexts.FirstOrDefault(context => context["Name"].ToString() == "Default Context") - ?? currentContext; - } - - return currentContext; - } } From 2c9a9bc1cbb7b205d3c8c3eaa7ead013956e7516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Tue, 21 Nov 2023 13:58:13 +0100 Subject: [PATCH 062/129] Docs --- Lombiq.Tests.UI/Docs/SecurityScanning.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lombiq.Tests.UI/Docs/SecurityScanning.md b/Lombiq.Tests.UI/Docs/SecurityScanning.md index 469b11116..f608d11b0 100644 --- a/Lombiq.Tests.UI/Docs/SecurityScanning.md +++ b/Lombiq.Tests.UI/Docs/SecurityScanning.md @@ -14,10 +14,10 @@ You an create detailed security scans of your app with [Zed Attack Proxy (ZAP)]( ## Working with ZAP in the Lombiq UI Testing Toolbox - We recommend you first check out the related samples in the [`Lombiq.Tests.UI.Samples` project](../../Lombiq.Tests.UI.Samples). -- If you're new to ZAP, you can start learning by checking out [ZAP's getting started guide](https://www.zaproxy.org/getting-started/), as well as the [ZAP Chat Videos](https://www.zaproxy.org/zap-chat/) (especially [ZAP Chat 06 Automation Introduction](https://www.youtube.com/watch?v=PnCbIAnauD8)). +- If you're new to ZAP, you can start learning by checking out [ZAP's getting started guide](https://www.zaproxy.org/getting-started/), as well as the [ZAP Chat Videos](https://www.zaproxy.org/zap-chat/). The [documentation on ZAP's Automation Framework](https://www.zaproxy.org/docs/automate/automation-framework/) and the [ZAP Chat 06 Automation Introduction vide](https://www.youtube.com/watch?v=PnCbIAnauD8) will help you understand what we use under the hood to instruct ZAP, and will allow you to use your completely custom Automation Framework plans too. - Be aware that ZAP scans run its own spider or with an internally managed browser instance, not in the browser launched by the test. - While ZAP is fully managed for you, Docker needs to be available and running to host the ZAP instance. On your development machine you can install [Docker Desktop](https://www.docker.com/products/docker-desktop/). -- The full scan of a website with even just 1-200 pages can take 15-30 minutes. So, be careful to fine-tune the ZAP configuration to make it suitable for your app. +- The full scan of a website with even just 1-200 pages can take 10-15 minutes. So, be careful to fine-tune the ZAP configuration to make it suitable for your app. ## Troubleshooting From 1c16a3f6da9d01ab6ca1d33b6da2f6d0f759fe93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Tue, 21 Nov 2023 14:00:52 +0100 Subject: [PATCH 063/129] More docs --- Lombiq.Tests.UI/Docs/SecurityScanning.md | 2 +- .../SecurityScanning/SecurityScanConfiguration.cs | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Lombiq.Tests.UI/Docs/SecurityScanning.md b/Lombiq.Tests.UI/Docs/SecurityScanning.md index f608d11b0..c947d7c4e 100644 --- a/Lombiq.Tests.UI/Docs/SecurityScanning.md +++ b/Lombiq.Tests.UI/Docs/SecurityScanning.md @@ -13,7 +13,7 @@ You an create detailed security scans of your app with [Zed Attack Proxy (ZAP)]( ## Working with ZAP in the Lombiq UI Testing Toolbox -- We recommend you first check out the related samples in the [`Lombiq.Tests.UI.Samples` project](../../Lombiq.Tests.UI.Samples). +- We recommend you first check out the [related samples in the `Lombiq.Tests.UI.Samples` project](../../Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs). - If you're new to ZAP, you can start learning by checking out [ZAP's getting started guide](https://www.zaproxy.org/getting-started/), as well as the [ZAP Chat Videos](https://www.zaproxy.org/zap-chat/). The [documentation on ZAP's Automation Framework](https://www.zaproxy.org/docs/automate/automation-framework/) and the [ZAP Chat 06 Automation Introduction vide](https://www.youtube.com/watch?v=PnCbIAnauD8) will help you understand what we use under the hood to instruct ZAP, and will allow you to use your completely custom Automation Framework plans too. - Be aware that ZAP scans run its own spider or with an internally managed browser instance, not in the browser launched by the test. - While ZAP is fully managed for you, Docker needs to be available and running to host the ZAP instance. On your development machine you can install [Docker Desktop](https://www.docker.com/products/docker-desktop/). diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs index b3f71a459..8ebb9d613 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs @@ -176,8 +176,12 @@ internal async Task ApplyToPlanAsync(YamlDocument yamlDocument, UITestContext co context.GetRelativeUrlOfAction(controller => controller.SignInDirectly(SignInUserName))) .ToString()); - // It might be that later such a verification for the login state will need to be needed, but this seems - // unnecessary now. + // With such direct sign in we don't need to utilize ZAP's authentication and user managements mechanisms + // (see https://www.zaproxy.org/docs/desktop/start/features/authmethods/ and + // https://www.zaproxy.org/docs/desktop/addons/automation-framework/authentication/). + + // Also, it might be that later such a verification for the login state will need to be needed, but this + // seems unnecessary now. // verification: // method: "response" // method: "poll" From 2c5711dae65800708223332badf3b92a4c7e4aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Tue, 21 Nov 2023 14:16:24 +0100 Subject: [PATCH 064/129] Fixing potential race condition in ZAP initialization --- .../SecurityScanning/ZapManager.cs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 59f0a9d13..88c03e7a8 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -33,11 +33,10 @@ public sealed class ZapManager : IAsyncDisposable private static readonly CliProgram _docker = new("docker"); private readonly ITestOutputHelper _testOutputHelper; + private readonly CancellationTokenSource _cancellationTokenSource = new(); private static bool _wasPulled; - private CancellationTokenSource _cancellationTokenSource; - internal ZapManager(ITestOutputHelper testOutputHelper) => _testOutputHelper = testOutputHelper; /// @@ -177,20 +176,16 @@ public ValueTask DisposeAsync() private async Task EnsureInitializedAsync() { - if (_cancellationTokenSource != null) return; - - _cancellationTokenSource = new CancellationTokenSource(); - var token = _cancellationTokenSource.Token; - try { + var token = _cancellationTokenSource.Token; + await _pullSemaphore.WaitAsync(token); - if (!_wasPulled) - { - await _docker.ExecuteAsync(token, "pull", _zapImage); - _wasPulled = true; - } + if (_wasPulled) return; + + await _docker.ExecuteAsync(token, "pull", _zapImage); + _wasPulled = true; } finally { From 176b1c16ff6796c338906f3b3613044f3c1c19c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Tue, 21 Nov 2023 14:16:39 +0100 Subject: [PATCH 065/129] Fixing docker pull failing due to hint --- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 88c03e7a8..ddf3e3903 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -184,7 +184,9 @@ private async Task EnsureInitializedAsync() if (_wasPulled) return; - await _docker.ExecuteAsync(token, "pull", _zapImage); + // Without --quiet, "What's Next?" hints will be written to stderr by Docker. See + // https://github.com/docker/for-mac/issues/6904. + await _docker.ExecuteAsync(token, "pull", _zapImage, "--quiet"); _wasPulled = true; } finally From cd99756e9ce2fc9898f2b64ebbb956da4558a412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Tue, 21 Nov 2023 16:05:46 +0100 Subject: [PATCH 066/129] Low-level config sample --- .../Lombiq.Tests.UI.Samples.csproj | 4 + .../CustomZapAutomationFrameworkPlan.yml | 99 +++++++++++++++++++ .../Tests/SecurityScanningTests.cs | 44 +++++++++ .../YamlDocumentExtensions.cs | 60 +++++++---- .../SecurityScanning/YamlNodeExtensions.cs | 25 +++++ .../SecurityScanning/ZapManager.cs | 2 +- 6 files changed, 214 insertions(+), 20 deletions(-) create mode 100644 Lombiq.Tests.UI.Samples/Tests/CustomZapAutomationFrameworkPlan.yml create mode 100644 Lombiq.Tests.UI/SecurityScanning/YamlNodeExtensions.cs diff --git a/Lombiq.Tests.UI.Samples/Lombiq.Tests.UI.Samples.csproj b/Lombiq.Tests.UI.Samples/Lombiq.Tests.UI.Samples.csproj index 524752739..2f30ec9a8 100644 --- a/Lombiq.Tests.UI.Samples/Lombiq.Tests.UI.Samples.csproj +++ b/Lombiq.Tests.UI.Samples/Lombiq.Tests.UI.Samples.csproj @@ -10,6 +10,7 @@ + @@ -17,6 +18,9 @@ Always + + PreserveNewest + PreserveNewest diff --git a/Lombiq.Tests.UI.Samples/Tests/CustomZapAutomationFrameworkPlan.yml b/Lombiq.Tests.UI.Samples/Tests/CustomZapAutomationFrameworkPlan.yml new file mode 100644 index 000000000..ff35bfc73 --- /dev/null +++ b/Lombiq.Tests.UI.Samples/Tests/CustomZapAutomationFrameworkPlan.yml @@ -0,0 +1,99 @@ +--- +env: + contexts: + - name: "Default Context" + urls: + - "" + excludePaths: [] + authentication: + parameters: {} + verification: + method: "response" + pollFrequency: 60 + pollUnits: "requests" + sessionManagement: + method: "cookie" + parameters: {} + technology: + exclude: [] + parameters: + failOnError: true + failOnWarning: false + progressToStdout: true + vars: {} +jobs: +- parameters: + scanOnlyInScope: true + enableTags: false + disableAllRules: false + rules: + - id: 10035 + name: "Strict-Transport-Security Header" + threshold: "off" + - id: 10038 + name: "Content Security Policy (CSP) Header Not Set" + threshold: "off" + - id: 10020 + name: "Anti-clickjacking Header" + threshold: "off" + - id: 10037 + name: "Server Leaks Information via \"X-Powered-By\" HTTP Response Header Field(s)" + threshold: "off" + - id: 10021 + name: "X-Content-Type-Options Header Missing" + threshold: "off" + name: "passiveScan-config" + type: "passiveScan-config" +- parameters: {} + name: "spider" + type: "spider" +- parameters: {} + name: "passiveScan-wait" + type: "passiveScan-wait" +- parameters: + reportDir: "/zap/wrk/reports" + template: "modern" + theme: "corporate" + reportTitle: "ZAP Scanning Report" + reportDescription: "" + risks: + - "low" + - "medium" + - "high" + confidences: + - "low" + - "medium" + - "high" + - "confirmed" + sections: + - "passingrules" + - "instancecount" + - "alertdetails" + - "alertcount" + - "params" + - "chart" + - "statistics" + name: "report" + type: "report" +- parameters: + template: "sarif-json" + reportDir: "/zap/wrk/reports" + reportFile: "" + reportTitle: "ZAP Scanning Report" + reportDescription: "" + displayReport: false + risks: + - "info" + - "low" + - "medium" + - "high" + confidences: + - "falsepositive" + - "low" + - "medium" + - "high" + - "confirmed" + sites: [] + name: "sarifReport" + type: "report" + diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index c85c3c28d..8e5b0ad4e 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; +using YamlDotNet.RepresentationModel; namespace Lombiq.Tests.UI.Samples.Tests; @@ -73,6 +74,49 @@ public Task SecurityScanWithCustomConfigurationShouldPass(Browser browser) => sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeLessThan(200)), browser); + // Let's get low-level into ZAP's configuration now. While the .NET configuration API of the Lombiq UI Testing + // Toolbox covers the most important ways to configure ZAP, sometimes you need more. For this, you have complete + // control over ZAP's configuration via its Automation Framework (see + // https://www.zaproxy.org/docs/automate/automation-framework/ and https://www.youtube.com/watch?v=PnCbIAnauD8 for + // an introduction), what all the packaged scans and .NET configuration uses under the hood too. This way, if you + // know what you want to do in ZAP, you can just directly run in as a UI test! + + // We run a completely custom Automation Framework plan here. It's almost the same as the plan used by the Baseline + // scan, but has some rules disabled by default, so we can assert on no alerts. Note that it has the Content build + // action to copy it to the build output folder. + + // You can also create and configure such plans from the ZAP desktop app, following the guides linked above. The + // plan doesn't need anything special, apart from having at least one context defined, as well as having a + // "sarif-json" report job so assertions can work with it. If something is missing in it, you'll get exceptions + // telling you what the problem is anyway. + + // Then, you can see an example of modifying the ZAP plan from code. You can also do this with the built-in plans to + // customize them if something you need is not surfaced as configuration. + [Theory, Chrome] + public Task SecurityScanWithCustomAutomationFrameworkPlanShouldPass(Browser browser) => + ExecuteTestAfterSetupAsync( + async context => await context.RunAndAssertSecurityScanAsync( + "Tests/CustomZapAutomationFrameworkPlan.yml", + configuration => configuration + .ModifyZapPlan(plan => + { + // "plan" here is a representation of the YAML document containing the plan. It's a low-level + // representation, but you can do anything with it. + + // We'll change a parameter for ZAP's spider. This of course could be done right in our custom + // plan, but we wanted to demo this too; furthermore, from code, you can change the plan even + // based on the context dynamically, so it's more flexible than trying to configure everything + // in the plan's YAML file. + + var spiderJob = plan.GetSpiderJob(); + var spiderParameters = (YamlMappingNode)spiderJob["parameters"]; + // The default maxDepth is 5. 8 will let the spider run for a bit more, potentially discovering + // more pages to be scanned. + spiderParameters.Add("maxDepth", "8"); + }), + sarifLog => SecurityScanningConfiguration.AssertSecurityScanHasNoAlerts(context, sarifLog)), + browser); + // Overriding the default setup so we can have a simpler site, simplifying the security scan for the purpose of this // demo. For a real app's security scan you needn't (shouldn't) do this though; always run the scan on the actual // app with everything set up how you run it in production. diff --git a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs index 4b165818c..67a9c4bf4 100644 --- a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs @@ -60,9 +60,8 @@ public static YamlDocument AddUrl(this YamlDocument yamlDocument, Uri uri) public static YamlDocument AddSpiderAjaxAfterSpider(this YamlDocument yamlDocument) { var jobs = yamlDocument.GetJobs(); - var spiderJob = - jobs.FirstOrDefault(job => (string)job["name"] == "spider") ?? + yamlDocument.GetSpiderJob() ?? throw new ArgumentException( "No job named \"spider\" found in the Automation Framework Plan. We can only add the ajaxSpider job " + "immediately after it."); @@ -113,7 +112,7 @@ public static YamlDocument AddExcludePathsRegex(this YamlDocument yamlDocument, /// public static YamlDocument DisablePassiveScanRule(this YamlDocument yamlDocument, int id, string name = "") { - var passiveScanConfigJob = GetPassiveScanConfigJobOrThrow(yamlDocument); + var passiveScanConfigJob = yamlDocument.GetPassiveScanConfigJobOrThrow(); if (!passiveScanConfigJob.Children.ContainsKey("rules")) passiveScanConfigJob.Add("rules", new YamlSequenceNode()); @@ -145,10 +144,8 @@ public static YamlDocument DisablePassiveScanRule(this YamlDocument yamlDocument /// public static YamlDocument DisableActiveScanRule(this YamlDocument yamlDocument, int id, string name = "") { - var jobs = yamlDocument.GetJobs(); - var activeScanConfigJob = - (YamlMappingNode)jobs.FirstOrDefault(job => (string)job["type"] == "activeScan") ?? + (YamlMappingNode)yamlDocument.GetJobByType("activeScan") ?? throw new ArgumentException( "No job with the type \"activeScan\" found in the Automation Framework Plan so the rule can't be added."); @@ -200,7 +197,7 @@ public static YamlDocument AddAlertFilter( { var jobs = yamlDocument.GetJobs(); - if (jobs.FirstOrDefault(job => (string)job["type"] == "alertFilter") is not YamlMappingNode alertFilterJob) + if (yamlDocument.GetJobByType("alertFilter") is not YamlMappingNode alertFilterJob) { alertFilterJob = new YamlMappingNode { @@ -208,7 +205,7 @@ public static YamlDocument AddAlertFilter( { "name", "alertFilter" }, }; - var passiveScanConfigJob = GetPassiveScanConfigJobOrThrow(yamlDocument); + var passiveScanConfigJob = yamlDocument.GetPassiveScanConfigJobOrThrow(); var passiveScanConfigIndex = jobs.Children.IndexOf(passiveScanConfigJob); jobs.Children.Insert(passiveScanConfigIndex + 1, alertFilterJob); } @@ -241,14 +238,14 @@ public static YamlDocument AddRequestor(this YamlDocument yamlDocument, string u var jobs = yamlDocument.GetJobs(); var spiderJob = - jobs.FirstOrDefault(job => (string)job["name"] == "spider") ?? + yamlDocument.GetSpiderJob() ?? throw new ArgumentException( "No job named \"spider\" found in the Automation Framework Plan. We can only add the requestor job " + "immediately before it."); var requestorJob = YamlHelper.LoadDocument(AutomationFrameworkPlanFragmentsPaths.RequestorJobPath).GetRootNode(); - ((YamlScalarNode)((YamlSequenceNode)requestorJob["requests"]).Children[0]["url"]).Value = url; + ((YamlSequenceNode)requestorJob["requests"]).Children[0]["url"].SetValue(url); var spiderIndex = jobs.Children.IndexOf(spiderJob); jobs.Children.Insert(spiderIndex, requestorJob); @@ -267,6 +264,25 @@ public static YamlDocument AddRequestor(this YamlDocument yamlDocument, string u public static YamlSequenceNode GetJobs(this YamlDocument yamlDocument) => (YamlSequenceNode)yamlDocument.GetRootNode()["jobs"]; + /// + /// Gets the job from the "jobs" section of the ZAP Automation Framework with the name "spider". + /// + public static YamlNode GetSpiderJob(this YamlDocument yamlDocument) => yamlDocument.GetJobByName("spider"); + + /// + /// Gets a job from the "jobs" section of the ZAP Automation Framework plan by its name. + /// + /// The "name" field of the job to search for. + public static YamlNode GetJobByName(this YamlDocument yamlDocument, string jobName) => + yamlDocument.GetJobs().FirstOrDefault(job => (string)job["name"] == jobName); + + /// + /// Gets a job from the "jobs" section of the ZAP Automation Framework plan by its type. + /// + /// The "type" field of the job to search for. + public static YamlNode GetJobByType(this YamlDocument yamlDocument, string jobType) => + yamlDocument.GetJobs().FirstOrDefault(job => (string)job["type"] == jobType); + /// /// Gets the "urls" section of the current context in the ZAP Automation Framework plan. /// @@ -306,6 +322,21 @@ public static YamlMappingNode GetCurrentContext(this YamlDocument yamlDocument) return currentContext; } + /// + /// Gets the job with the type "passiveScan-config" from the ZAP Automation Framework plan. + /// + /// + /// Thrown if the ZAP Automation Framework plan doesn't contain a job with the type "passiveScan-config". + /// + public static YamlMappingNode GetPassiveScanConfigJobOrThrow(this YamlDocument yamlDocument) + { + var jobs = yamlDocument.GetJobs(); + + return (YamlMappingNode)yamlDocument.GetJobByType("passiveScan-config") ?? + throw new ArgumentException( + "No job with the type \"passiveScan-config\" found in the Automation Framework Plan."); + } + /// /// Shortcuts to to be able to chain extensions in an /// async method/delegate. @@ -341,13 +372,4 @@ public static YamlDocument MergeFrom(this YamlDocument baseDocument, YamlDocumen return baseDocument; } - - private static YamlMappingNode GetPassiveScanConfigJobOrThrow(YamlDocument yamlDocument) - { - var jobs = yamlDocument.GetJobs(); - - return (YamlMappingNode)jobs.FirstOrDefault(job => (string)job["type"] == "passiveScan-config") ?? - throw new ArgumentException( - "No job with the type \"passiveScan-config\" found in the Automation Framework Plan so the rule can't be added."); - } } diff --git a/Lombiq.Tests.UI/SecurityScanning/YamlNodeExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/YamlNodeExtensions.cs new file mode 100644 index 000000000..df567c6cc --- /dev/null +++ b/Lombiq.Tests.UI/SecurityScanning/YamlNodeExtensions.cs @@ -0,0 +1,25 @@ +using System; +using YamlDotNet.RepresentationModel; + +namespace Lombiq.Tests.UI.SecurityScanning; + +public static class YamlNodeExtensions +{ + /// + /// Sets to the given value. + /// + /// The value to set to. + /// + /// Thrown if the supplied YamlNode can't be cast to YamlScalarNode and thus can't have a value set. + /// + public static void SetValue(this YamlNode yamlNode, string value) + { + if (yamlNode is not YamlScalarNode) + { + throw new ArgumentException( + "The supplied YamlNode can't be cast to YamlScalarNode and thus can't have a value set.", nameof(yamlNode)); + } + + ((YamlScalarNode)yamlNode).Value = value; + } +} diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index ddf3e3903..63e1c7fb7 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -208,7 +208,7 @@ private static async Task PreparePlanAsync(string yamlFilePath, Func Date: Tue, 21 Nov 2023 16:30:11 +0100 Subject: [PATCH 067/129] Docs --- Lombiq.Tests.UI/Docs/Troubleshooting.md | 2 +- Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs | 4 +++- Readme.md | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Lombiq.Tests.UI/Docs/Troubleshooting.md b/Lombiq.Tests.UI/Docs/Troubleshooting.md index 3e66603e5..ffe94dca0 100644 --- a/Lombiq.Tests.UI/Docs/Troubleshooting.md +++ b/Lombiq.Tests.UI/Docs/Troubleshooting.md @@ -66,6 +66,6 @@ - If you want to test the failed page granularly, you can write a test that navigates to that page and executes `context.TestCurrentPageAsMonkey(_monkeyTestingOptions, 12345);`, where `12345` is the random seed number that can be found in a failed test log. - It is also possible to set a larger time value to the `MonkeyTestingOptions.GremlinsAttackDelay` property in order to make gremlin interaction slower, thus allowing you to watch what's happening. -## Security scanning +## Security scanning with ZAP Check out the [security scanning docs](SecurityScanning.md). diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs index 8ebb9d613..7e7715862 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs @@ -178,7 +178,9 @@ internal async Task ApplyToPlanAsync(YamlDocument yamlDocument, UITestContext co // With such direct sign in we don't need to utilize ZAP's authentication and user managements mechanisms // (see https://www.zaproxy.org/docs/desktop/start/features/authmethods/ and - // https://www.zaproxy.org/docs/desktop/addons/automation-framework/authentication/). + // https://www.zaproxy.org/docs/desktop/addons/automation-framework/authentication/). If using the standard + // Orchard Core login screen, that would also require using a (headless) browser, bringing all kinds of + // WebDriver compatibility issues we've already solved here. // Also, it might be that later such a verification for the login state will need to be needed, but this // seems unnecessary now. diff --git a/Readme.md b/Readme.md index 80e840f33..4bc2e6f85 100644 --- a/Readme.md +++ b/Readme.md @@ -26,6 +26,7 @@ Highlights: - Visual verification testing: You can make the test fail if the look of the app changes. Demo video [here](https://www.youtube.com/watch?v=a-1zKjxTKkk). - If your app uses a camera, a fake video capture source in Chrome is supported. [Here's a demo video of the feature](https://www.youtube.com/watch?v=sGcD0eJ2ytc), and check out the docs [here](Lombiq.Tests.UI/Docs/FakeVideoCaptureSource.md). - Interactive mode for debugging the app while the test is paused. [Here's a demo of the feature](Lombiq.Tests.UI.Samples/Tests/InteractiveModeTests.cs), and a [demo video here](https://www.youtube.com/watch?v=ItNltaruWTY). +- Security scanning with [Zed Attack Proxy (ZAP)](https://www.zaproxy.org/), the world's most widely used web app security scanner. See a demo video of the project [here](https://www.youtube.com/watch?v=mEUg6-pad-E), and the Orchard Harvest 2023 conference talk about automated QA in Orchard Core [here](https://youtu.be/CHdhwD2NHBU). Also, see our [Testing Toolbox](https://github.com/Lombiq/Testing-Toolbox) for similar features for lower-level tests. From 8ab559014fd6563c7fc08e1fdd3e1770e88a5eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Tue, 21 Nov 2023 16:34:35 +0100 Subject: [PATCH 068/129] Code styling --- .../SecurityScanning/YamlDocumentExtensions.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs index 67a9c4bf4..2864d22e9 100644 --- a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs @@ -328,14 +328,10 @@ public static YamlMappingNode GetCurrentContext(this YamlDocument yamlDocument) /// /// Thrown if the ZAP Automation Framework plan doesn't contain a job with the type "passiveScan-config". /// - public static YamlMappingNode GetPassiveScanConfigJobOrThrow(this YamlDocument yamlDocument) - { - var jobs = yamlDocument.GetJobs(); - - return (YamlMappingNode)yamlDocument.GetJobByType("passiveScan-config") ?? + public static YamlMappingNode GetPassiveScanConfigJobOrThrow(this YamlDocument yamlDocument) => + (YamlMappingNode)yamlDocument.GetJobByType("passiveScan-config") ?? throw new ArgumentException( "No job with the type \"passiveScan-config\" found in the Automation Framework Plan."); - } /// /// Shortcuts to to be able to chain extensions in an From e9ac2f6cc1d5a817a553b0b25e1727754362b283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Tue, 21 Nov 2023 16:38:51 +0100 Subject: [PATCH 069/129] Removing leftover arguments --- .../SecurityScanningUITestContextExtensions.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs index dc80caa62..95c5c790c 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanningUITestContextExtensions.cs @@ -40,7 +40,6 @@ public static Task RunAndAssertBaselineSecurityScanAsync( /// public static Task RunAndAssertFullSecurityScanAsync( this UITestContext context, - Uri startUri = null, Action configure = null, Action assertSecurityScanResult = null) => context.RunAndAssertSecurityScanAsync( @@ -60,7 +59,6 @@ public static Task RunAndAssertFullSecurityScanAsync( /// public static Task RunAndAssertGraphQLSecurityScanAsync( this UITestContext context, - Uri startUri = null, Action configure = null, Action assertSecurityScanResult = null) => context.RunAndAssertSecurityScanAsync( @@ -80,7 +78,6 @@ public static Task RunAndAssertGraphQLSecurityScanAsync( /// public static Task RunAndAssertOpenApiSecurityScanAsync( this UITestContext context, - Uri startUri = null, Action configure = null, Action assertSecurityScanResult = null) => context.RunAndAssertSecurityScanAsync( From 751f0314ecb6dd0e3c5e75aaacc75815648b2c90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 22 Nov 2023 13:28:28 +0100 Subject: [PATCH 070/129] Docs formatting --- Lombiq.Tests.UI/Docs/Configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/Docs/Configuration.md b/Lombiq.Tests.UI/Docs/Configuration.md index a5291028a..933dc2d2c 100644 --- a/Lombiq.Tests.UI/Docs/Configuration.md +++ b/Lombiq.Tests.UI/Docs/Configuration.md @@ -20,7 +20,7 @@ Note that since the tests are xUnit tests you can configure general parameters o ``` -Note also that some projects' _xunit.runner.json_ files may include the flag [stopOnFail](https://xunit.net/docs/configuration-files#stopOnFail) set to `true`, which makes further tests stop once a failing test is encountered. +Note also that some projects' _xunit.runner.json_ files may include the flag [`stopOnFail`](https://xunit.net/docs/configuration-files#stopOnFail) set to `true`, which makes further tests stop once a failing test is encountered. Certain test execution parameters can be configured externally too, the ones retrieved via the `TestConfigurationManager` class. All configuration options are basic key-value pairs and can be provided in one of the two ways: From e3a15c9a7e1395773352b08c6758ca47331c4ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 22 Nov 2023 13:29:40 +0100 Subject: [PATCH 071/129] Removing link to now fixed ZAP bug --- Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 8e5b0ad4e..8c96d12de 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -133,8 +133,6 @@ protected override Task ExecuteTestAfterSetupAsync( new OrchardCoreSetupParameters(context) { SiteName = "Lombiq's OSOCE - UI Testing", - // We can't use the even simpler Coming Soon recipe due to this ZAP bug: - // https://github.com/zaproxy/zaproxy/issues/8191. RecipeId = "Blog", TablePrefix = "OSOCE", SiteTimeZoneValue = "Europe/Budapest", From acb9f009e3ded4143b773188f0906b012287f966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 22 Nov 2023 15:41:55 +0100 Subject: [PATCH 072/129] Excluding irrelevant technologies from the scans, making them faster --- Lombiq.Tests.UI/Docs/SecurityScanning.md | 2 +- .../AutomationFrameworkPlans/Baseline.yml | 17 ++++++++++++++++- .../AutomationFrameworkPlans/FullScan.yml | 17 ++++++++++++++++- .../AutomationFrameworkPlans/GraphQL.yml | 17 ++++++++++++++++- .../AutomationFrameworkPlans/OpenAPI.yml | 17 ++++++++++++++++- 5 files changed, 65 insertions(+), 5 deletions(-) diff --git a/Lombiq.Tests.UI/Docs/SecurityScanning.md b/Lombiq.Tests.UI/Docs/SecurityScanning.md index c947d7c4e..17ffe76a7 100644 --- a/Lombiq.Tests.UI/Docs/SecurityScanning.md +++ b/Lombiq.Tests.UI/Docs/SecurityScanning.md @@ -6,7 +6,7 @@ You an create detailed security scans of your app with [Zed Attack Proxy (ZAP)]( ![Sample ZAP security scan report](Attachments/ZapReportScreenshot.png) -- The most important default ZAP scans, Baseline, Full, GraphQL, and OpenAPI scans are included and readily usable. Note that these are modified to be more applicable to Orchard Core apps run on localhost during a UI testing scenario (notably, [`ajaxSpider`](https://www.zaproxy.org/docs/desktop/addons/ajax-spider/automation/) is removed, since most Orchard Core apps don't need it but it takes a lot of time). If you want to scan remote (and especially production) apps, then you'll need to create your own scans based on ZAP's default ones. These can then be run from inside UI tests too. +- The most important default ZAP scans, Baseline, Full, GraphQL, and OpenAPI scans are included and readily usable. Note that these are modified to be more applicable to Orchard Core apps run on localhost during a UI testing scenario (notably, [`ajaxSpider`](https://www.zaproxy.org/docs/desktop/addons/ajax-spider/automation/) is removed, since most Orchard Core apps don't need it but it takes a lot of time, and rules as well as selected technologies are adjusted). If you want to scan remote (and especially production) apps, then you'll need to create your own scans based on ZAP's default ones. These can then be run from inside UI tests too. - You can assert on scan results and thus fail the test if there are security warnings. - Since we use [ZAP's Automation Framework](https://www.zaproxy.org/docs/automate/automation-framework/) for configuration, you have complete and detailed control over how the scans are configured, but you can also start with a simple configuration available in the .NET API. - [SARIF](https://sarifweb.azurewebsites.net/) reports are available to integrate with other InfoSec tools. diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml index cae880358..3ebde1b96 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/Baseline.yml @@ -15,7 +15,22 @@ env: method: "cookie" parameters: {} technology: - exclude: [] + exclude: + - "C" + - "IBM DB2" + - "PHP" + - "CouchDB" + - "Oracle" + - "JSP/Servlet" + - "Firebird" + - "HypersonicSQL" + - "SAP MaxDB" + - "Ruby" + - "Microsoft Access" + - "Java" + - "Tomcat" + - "Sybase" + - "Python" parameters: failOnError: true failOnWarning: false diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/FullScan.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/FullScan.yml index 46162ecd6..19bf336b2 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/FullScan.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/FullScan.yml @@ -15,7 +15,22 @@ env: method: "cookie" parameters: {} technology: - exclude: [] + exclude: + - "C" + - "IBM DB2" + - "PHP" + - "CouchDB" + - "Oracle" + - "JSP/Servlet" + - "Firebird" + - "HypersonicSQL" + - "SAP MaxDB" + - "Ruby" + - "Microsoft Access" + - "Java" + - "Tomcat" + - "Sybase" + - "Python" parameters: failOnError: true failOnWarning: false diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/GraphQL.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/GraphQL.yml index 07e4362d6..24f14d902 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/GraphQL.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/GraphQL.yml @@ -15,7 +15,22 @@ env: method: "cookie" parameters: {} technology: - exclude: [] + exclude: + - "C" + - "IBM DB2" + - "PHP" + - "CouchDB" + - "Oracle" + - "JSP/Servlet" + - "Firebird" + - "HypersonicSQL" + - "SAP MaxDB" + - "Ruby" + - "Microsoft Access" + - "Java" + - "Tomcat" + - "Sybase" + - "Python" parameters: failOnError: true failOnWarning: false diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/OpenAPI.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/OpenAPI.yml index ff260ba84..9327dc09a 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/OpenAPI.yml +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlans/OpenAPI.yml @@ -15,7 +15,22 @@ env: method: "cookie" parameters: {} technology: - exclude: [] + exclude: + - "C" + - "IBM DB2" + - "PHP" + - "CouchDB" + - "Oracle" + - "JSP/Servlet" + - "Firebird" + - "HypersonicSQL" + - "SAP MaxDB" + - "Ruby" + - "Microsoft Access" + - "Java" + - "Tomcat" + - "Sybase" + - "Python" parameters: failOnError: true failOnWarning: false From 033e23bcecfad20cc71129b35914223856216ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 22 Nov 2023 15:42:30 +0100 Subject: [PATCH 073/129] Comment on an alternative to separate ZAP Docker container instances for each scan --- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 63e1c7fb7..c71d0e969 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -129,6 +129,10 @@ public async Task RunSecurityScanAsync( var stdErrBuffer = new StringBuilder(); + // Here we use a new container instance for every scan. This is viable and is not an overhead big enough to + // worry about, but an optimization would be to run multiple scans (also possible simultaneously with background + // commands) with the same instance. This needs the -dir option to configure a different home directory per + // scan, see https://www.zaproxy.org/docs/desktop/cmdline/#options. var result = await _docker .GetCommand(cliParameters) .WithStandardOutputPipe(PipeTarget.ToDelegate(line => _testOutputHelper.WriteLineTimestampedAndDebug(line))) From e8ac89716b343984a3cd6bcd55f447c856df286e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Wed, 22 Nov 2023 23:32:16 +0100 Subject: [PATCH 074/129] Removing now unnecessary browser configs --- .../Tests/SecurityScanningTests.cs | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 8c96d12de..5bdfc4e90 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -1,4 +1,3 @@ -using Lombiq.Tests.UI.Attributes; using Lombiq.Tests.UI.Extensions; using Lombiq.Tests.UI.Pages; using Lombiq.Tests.UI.SecurityScanning; @@ -36,11 +35,10 @@ public SecurityScanningTests(ITestOutputHelper testOutputHelper) // If you're new to security scanning, starting with exactly this is probably a good idea. Most possibly your app // will fail the scan, but don't worry! You'll get a nice report about the findings in the failure dump. - [Theory, Chrome] - public Task BasicSecurityScanShouldPass(Browser browser) => + [Fact] + public Task BasicSecurityScanShouldPass() => ExecuteTestAfterSetupAsync( - async context => await context.RunAndAssertBaselineSecurityScanAsync(), - browser); + async context => await context.RunAndAssertBaselineSecurityScanAsync()); // Time for some custom configuration! While this scan also runs the Baseline scan, it does this with several // adjustments: @@ -61,8 +59,8 @@ public Task BasicSecurityScanShouldPass(Browser browser) => // - The assertion on the scan results is custom. Use this if you (conditionally) want to assert on the results // differently from the global context.Configuration.SecurityScanningConfiguration.AssertSecurityScanResult. The // default there is "no scanning alert is allowed". - [Theory, Chrome] - public Task SecurityScanWithCustomConfigurationShouldPass(Browser browser) => + [Fact] + public Task SecurityScanWithCustomConfigurationShouldPass() => ExecuteTestAfterSetupAsync( async context => await context.RunAndAssertBaselineSecurityScanAsync( configuration => configuration @@ -71,8 +69,7 @@ public Task SecurityScanWithCustomConfigurationShouldPass(Browser browser) => .DisablePassiveScanRule(10037, "Server Leaks Information via \"X-Powered-By\" HTTP Response Header Field(s)") .DisableScanRuleForUrlWithRegex(".*/about", 10038, "Content Security Policy (CSP) Header Not Set") .SignIn(), - sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeLessThan(200)), - browser); + sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeLessThan(200))); // Let's get low-level into ZAP's configuration now. While the .NET configuration API of the Lombiq UI Testing // Toolbox covers the most important ways to configure ZAP, sometimes you need more. For this, you have complete @@ -92,8 +89,8 @@ public Task SecurityScanWithCustomConfigurationShouldPass(Browser browser) => // Then, you can see an example of modifying the ZAP plan from code. You can also do this with the built-in plans to // customize them if something you need is not surfaced as configuration. - [Theory, Chrome] - public Task SecurityScanWithCustomAutomationFrameworkPlanShouldPass(Browser browser) => + [Fact] + public Task SecurityScanWithCustomAutomationFrameworkPlanShouldPass() => ExecuteTestAfterSetupAsync( async context => await context.RunAndAssertSecurityScanAsync( "Tests/CustomZapAutomationFrameworkPlan.yml", @@ -114,8 +111,7 @@ public Task SecurityScanWithCustomAutomationFrameworkPlanShouldPass(Browser brow // more pages to be scanned. spiderParameters.Add("maxDepth", "8"); }), - sarifLog => SecurityScanningConfiguration.AssertSecurityScanHasNoAlerts(context, sarifLog)), - browser); + sarifLog => SecurityScanningConfiguration.AssertSecurityScanHasNoAlerts(context, sarifLog))); // Overriding the default setup so we can have a simpler site, simplifying the security scan for the purpose of this // demo. For a real app's security scan you needn't (shouldn't) do this though; always run the scan on the actual From 9c4ad2d5f765cdefa664ad82c898bf50979036d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 18:45:26 +0100 Subject: [PATCH 075/129] Resetting debug code --- Lombiq.Tests.UI.Samples/UITestBase.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/UITestBase.cs b/Lombiq.Tests.UI.Samples/UITestBase.cs index 4510e4aa5..3fe59f397 100644 --- a/Lombiq.Tests.UI.Samples/UITestBase.cs +++ b/Lombiq.Tests.UI.Samples/UITestBase.cs @@ -42,8 +42,6 @@ protected override Task ExecuteTestAsync( setupOperation, async configuration => { - configuration.MaxRetryCount = 0; - // You should always set the window size of the browser, otherwise the size will be random based on the // settings of the given machine. However this is already handled as long as the // context.Configuration.BrowserConfiguration.DefaultBrowserSize option is properly set. You can change From 5281dd81191cd694253c26585179d32cc4a5b40a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 18:45:34 +0100 Subject: [PATCH 076/129] Docs --- Lombiq.Tests.UI/Docs/SecurityScanning.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/Docs/SecurityScanning.md b/Lombiq.Tests.UI/Docs/SecurityScanning.md index 17ffe76a7..582d59da1 100644 --- a/Lombiq.Tests.UI/Docs/SecurityScanning.md +++ b/Lombiq.Tests.UI/Docs/SecurityScanning.md @@ -17,10 +17,11 @@ You an create detailed security scans of your app with [Zed Attack Proxy (ZAP)]( - If you're new to ZAP, you can start learning by checking out [ZAP's getting started guide](https://www.zaproxy.org/getting-started/), as well as the [ZAP Chat Videos](https://www.zaproxy.org/zap-chat/). The [documentation on ZAP's Automation Framework](https://www.zaproxy.org/docs/automate/automation-framework/) and the [ZAP Chat 06 Automation Introduction vide](https://www.youtube.com/watch?v=PnCbIAnauD8) will help you understand what we use under the hood to instruct ZAP, and will allow you to use your completely custom Automation Framework plans too. - Be aware that ZAP scans run its own spider or with an internally managed browser instance, not in the browser launched by the test. - While ZAP is fully managed for you, Docker needs to be available and running to host the ZAP instance. On your development machine you can install [Docker Desktop](https://www.docker.com/products/docker-desktop/). -- The full scan of a website with even just 1-200 pages can take 10-15 minutes. So, be careful to fine-tune the ZAP configuration to make it suitable for your app. +- The full scan of a website with even just 1-200 pages can take 5-10 minutes. So, be careful to fine-tune the ZAP configuration to make it suitable for your app. ## Troubleshooting - If you're unsure what happens in a scan, run the [ZAP desktop app](https://www.zaproxy.org/download/) and load the Automation Framework plan's YAML file into it. If you use the default scans, then these will be available under the build output directory (like _bin/Debug_) under _SecurityScanning/AutomationFrameworkPlans_. Then, you can open and run them as demonstrated [in this video](https://youtu.be/PnCbIAnauD8?si=u0vi63Uvv9wZINzb&t=1173). - If an alert is a false positive, follow [the official docs](https://www.zaproxy.org/faq/how-do-i-handle-a-false-positive/). You can use the [`alertFilter` job](https://www.zaproxy.org/docs/desktop/addons/alert-filters/automation/) to ignore alerts in very specific conditions. You can also access this via the .NET configuration API. - ZAP didn't find everything in your app? By default, ZAP has a crawl depth of 5 for its standard spider and 10 for its AJAX spider. Set `maxDepth` (and `maxChildren`) [for `spider`](https://www.zaproxy.org/docs/desktop/addons/automation-framework/job-spider/) and `maxCrawlDepth` [for `spiderAjax`](https://www.zaproxy.org/docs/desktop/addons/ajax-spider/automation/). +- Do you sometimes get slightly different scan results? This is normal, and ZAP can be inconsistent/appear random within limits, see [the official docs page](https://www.zaproxy.org/faq/why-can-zap-scans-be-inconsistent/). From dc9e849ebe1abd94b873747f56f514bea899eefe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 19:00:08 +0100 Subject: [PATCH 077/129] Docs --- Lombiq.Tests.UI/Docs/SecurityScanning.md | 6 +++--- .../Extensions/ShortcutsUITestContextExtensions.cs | 2 +- Lombiq.Tests.UI/Services/UITestContext.cs | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Lombiq.Tests.UI/Docs/SecurityScanning.md b/Lombiq.Tests.UI/Docs/SecurityScanning.md index 582d59da1..94083ec09 100644 --- a/Lombiq.Tests.UI/Docs/SecurityScanning.md +++ b/Lombiq.Tests.UI/Docs/SecurityScanning.md @@ -2,7 +2,7 @@ ## Overview -You an create detailed security scans of your app with [Zed Attack Proxy (ZAP)](https://www.zaproxy.org/) right from the Lombiq UI Testing Toolbox, with nice reports. ZAP is the world's most widely used web app security scanner, and a fellow open-source project we can recommend. +You can create detailed security scans of your app with [Zed Attack Proxy (ZAP)](https://www.zaproxy.org/) right from the Lombiq UI Testing Toolbox, with nice reports. ZAP is the world's most widely used web app security scanner, and a fellow open-source project we can recommend. ![Sample ZAP security scan report](Attachments/ZapReportScreenshot.png) @@ -14,9 +14,9 @@ You an create detailed security scans of your app with [Zed Attack Proxy (ZAP)]( ## Working with ZAP in the Lombiq UI Testing Toolbox - We recommend you first check out the [related samples in the `Lombiq.Tests.UI.Samples` project](../../Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs). -- If you're new to ZAP, you can start learning by checking out [ZAP's getting started guide](https://www.zaproxy.org/getting-started/), as well as the [ZAP Chat Videos](https://www.zaproxy.org/zap-chat/). The [documentation on ZAP's Automation Framework](https://www.zaproxy.org/docs/automate/automation-framework/) and the [ZAP Chat 06 Automation Introduction vide](https://www.youtube.com/watch?v=PnCbIAnauD8) will help you understand what we use under the hood to instruct ZAP, and will allow you to use your completely custom Automation Framework plans too. +- If you're new to ZAP, you can start learning by checking out [ZAP's getting started guide](https://www.zaproxy.org/getting-started/), as well as the [ZAP Chat Videos](https://www.zaproxy.org/zap-chat/). The [documentation on ZAP's Automation Framework](https://www.zaproxy.org/docs/automate/automation-framework/) and the [ZAP Chat 06 Automation Introduction video](https://www.youtube.com/watch?v=PnCbIAnauD8) will help you understand what we use under the hood to instruct ZAP, and will allow you to use your completely custom Automation Framework plans too. - Be aware that ZAP scans run its own spider or with an internally managed browser instance, not in the browser launched by the test. -- While ZAP is fully managed for you, Docker needs to be available and running to host the ZAP instance. On your development machine you can install [Docker Desktop](https://www.docker.com/products/docker-desktop/). +- While ZAP is fully managed for you, Docker needs to be available and running to host the ZAP instance. On your development machine, you can install [Docker Desktop](https://www.docker.com/products/docker-desktop/). - The full scan of a website with even just 1-200 pages can take 5-10 minutes. So, be careful to fine-tune the ZAP configuration to make it suitable for your app. ## Troubleshooting diff --git a/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs index 20eb13d95..5a41fd2e8 100644 --- a/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs @@ -594,7 +594,7 @@ await UsingScopeAsync( /// /// Switches to an interactive mode where control from the test is handed over and you can use the web app as an /// ordinary user from the browser or access its web APIs. To switch back to the test, click the button - /// that'll be displayed in the browser, or call open . + /// that'll be displayed in the browser, or open . /// public static async Task SwitchToInteractiveAsync(this UITestContext context) { diff --git a/Lombiq.Tests.UI/Services/UITestContext.cs b/Lombiq.Tests.UI/Services/UITestContext.cs index 7f14ac2e9..89a5ee4ad 100644 --- a/Lombiq.Tests.UI/Services/UITestContext.cs +++ b/Lombiq.Tests.UI/Services/UITestContext.cs @@ -63,8 +63,9 @@ public class UITestContext public AzureBlobStorageRunningContext AzureBlobStorageRunningContext { get; } /// - /// Gets the service to manage Zed Attack Proxy instances for security scanning. Usually, it's recommended to use - /// the ZAP extension methods instead. + /// Gets the service to manage Zed Attack Proxy (ZAP) instances for + /// security scanning. Usually, it's recommended to use the hihger-level ZAP extension methods instead. /// public ZapManager ZapManager { get; } From 30f9c6ab72946d8af09ccdd1a9110bc36e4d26d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 19:05:19 +0100 Subject: [PATCH 078/129] Spelling --- .../SecurityScanning/SecurityScanConfiguration.cs | 2 +- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs index 7e7715862..863c787ee 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs @@ -198,7 +198,7 @@ internal async Task ApplyToPlanAsync(YamlDocument yamlDocument, UITestContext co yamlDocument.AddExcludePathsRegex(ExcludedUrlRegexPatterns.ToArray()); foreach (var rule in DisabledActiveScanRules) yamlDocument.DisableActiveScanRule(rule.Id, rule.Name); foreach (var rule in DisabledPassiveScanRules) yamlDocument.DisablePassiveScanRule(rule.Id, rule.Name); - foreach (var kvp in DisabledRulesForUrls) yamlDocument.AddAlertFilter(kvp.Key, kvp.Value.Id, kvp.Value.Name); + foreach (var urlToRule in DisabledRulesForUrls) yamlDocument.AddAlertFilter(urlToRule.Key, urlToRule.Value.Id, urlToRule.Value.Name); foreach (var modifier in ZapPlanModifiers) await modifier(yamlDocument); } diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index c71d0e969..17076ace1 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -25,7 +25,7 @@ public sealed class ZapManager : IAsyncDisposable // https://hub.docker.com/r/softwaresecurityproject/zap-stable/tags. // When updating this version, also regenerate the Automation Framework YAML config files so we don't miss any // changes to those. - private const string _zapImage = "softwaresecurityproject/zap-stable:2.14.0"; + private const string _zapImage = "softwaresecurityproject/zap-stable:2.14.0"; // #spell-check-ignore-line private const string _zapWorkingDirectoryPath = "/zap/wrk/"; private const string _zapReportsDirectoryName = "reports"; @@ -99,8 +99,10 @@ public async Task RunSecurityScanAsync( // Also see https://www.zaproxy.org/docs/docker/about/#automation-framework. - // Running a ZAP desktop in the browser with Webswing with the same config under Windows: - // docker run --add-host localhost:host-gateway -u zap -p 8080:8080 -p 8090:8090 -i softwaresecurityproject/zap-stable zap-webswing.sh + // Running a ZAP desktop in the browser with Webswing with the same config under Windows: #spell-check-ignore-line +#pragma warning disable S103 // Lines should not be too long + // docker run --add-host localhost:host-gateway -u zap -p 8080:8080 -p 8090:8090 -i softwaresecurityproject/zap-stable zap-webswing.sh #spell-check-ignore-line +#pragma warning restore S103 // Lines should not be too long var cliParameters = new List { "run" }; From 2fffa22c0820b1b8e615a848ac1949602910b8dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 19:08:34 +0100 Subject: [PATCH 079/129] More spelling --- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 2 +- Lombiq.Tests.UI/Services/UITestContext.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 17076ace1..35a691a9c 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -26,7 +26,7 @@ public sealed class ZapManager : IAsyncDisposable // When updating this version, also regenerate the Automation Framework YAML config files so we don't miss any // changes to those. private const string _zapImage = "softwaresecurityproject/zap-stable:2.14.0"; // #spell-check-ignore-line - private const string _zapWorkingDirectoryPath = "/zap/wrk/"; + private const string _zapWorkingDirectoryPath = "/zap/wrk/"; // #spell-check-ignore-line private const string _zapReportsDirectoryName = "reports"; private static readonly SemaphoreSlim _pullSemaphore = new(1, 1); diff --git a/Lombiq.Tests.UI/Services/UITestContext.cs b/Lombiq.Tests.UI/Services/UITestContext.cs index 89a5ee4ad..3dbc878ed 100644 --- a/Lombiq.Tests.UI/Services/UITestContext.cs +++ b/Lombiq.Tests.UI/Services/UITestContext.cs @@ -64,7 +64,7 @@ public class UITestContext /// /// Gets the service to manage Zed Attack Proxy (ZAP) instances for - /// security scanning. Usually, it's recommended to use the hihger-level ZAP extension methods instead. /// public ZapManager ZapManager { get; } From 600b0db80ec3575157003745c2c95e90da7608c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 19:16:37 +0100 Subject: [PATCH 080/129] Updating Helpful Libraries NuGet references to latest --- Lombiq.Tests.UI.Shortcuts/Lombiq.Tests.UI.Shortcuts.csproj | 2 +- Lombiq.Tests.UI/Lombiq.Tests.UI.csproj | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lombiq.Tests.UI.Shortcuts/Lombiq.Tests.UI.Shortcuts.csproj b/Lombiq.Tests.UI.Shortcuts/Lombiq.Tests.UI.Shortcuts.csproj index bd6f9b01f..fd2d7525d 100644 --- a/Lombiq.Tests.UI.Shortcuts/Lombiq.Tests.UI.Shortcuts.csproj +++ b/Lombiq.Tests.UI.Shortcuts/Lombiq.Tests.UI.Shortcuts.csproj @@ -50,7 +50,7 @@ - + diff --git a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj index 12335b7d1..ee6eb3391 100644 --- a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj +++ b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj @@ -103,9 +103,9 @@ - - - + + + From 7212085fef6506875ce27768dcaee4fdcfc983d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 19:30:54 +0100 Subject: [PATCH 081/129] Removing unnecessary code --- Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 5bdfc4e90..0bac66ab0 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -38,7 +38,7 @@ public SecurityScanningTests(ITestOutputHelper testOutputHelper) [Fact] public Task BasicSecurityScanShouldPass() => ExecuteTestAfterSetupAsync( - async context => await context.RunAndAssertBaselineSecurityScanAsync()); + context => context.RunAndAssertBaselineSecurityScanAsync()); // Time for some custom configuration! While this scan also runs the Baseline scan, it does this with several // adjustments: @@ -62,7 +62,7 @@ public Task BasicSecurityScanShouldPass() => [Fact] public Task SecurityScanWithCustomConfigurationShouldPass() => ExecuteTestAfterSetupAsync( - async context => await context.RunAndAssertBaselineSecurityScanAsync( + context => context.RunAndAssertBaselineSecurityScanAsync( configuration => configuration ////.UseAjaxSpider() // This is quite slow so just showing you here but not running it. .ExcludeUrlWithRegex(".*blog.*") @@ -92,7 +92,7 @@ public Task SecurityScanWithCustomConfigurationShouldPass() => [Fact] public Task SecurityScanWithCustomAutomationFrameworkPlanShouldPass() => ExecuteTestAfterSetupAsync( - async context => await context.RunAndAssertSecurityScanAsync( + context => context.RunAndAssertSecurityScanAsync( "Tests/CustomZapAutomationFrameworkPlan.yml", configuration => configuration .ModifyZapPlan(plan => From 71b0967aa594dc7398d17f2c935837e7134ed3d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 21:10:06 +0100 Subject: [PATCH 082/129] Trying to fix report creation issue under GitHub-hosted GHA runners --- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 35a691a9c..4e8fa3aff 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -79,6 +79,9 @@ public async Task RunSecurityScanAsync( var mountedDirectoryPath = DirectoryPaths.GetTempSubDirectoryPath(context.Id, "Zap"); Directory.CreateDirectory(mountedDirectoryPath); + // Pre-creating the reports folder to avoid write permission issues under GitHub-hosted runners in GitHub + // Actions (BuildJet ones work without this too). + Directory.CreateDirectory(Path.Combine(mountedDirectoryPath, _zapReportsDirectoryName)); var yamlFileName = Path.GetFileName(automationFrameworkYamlPath); var yamlFileCopyPath = Path.Combine(mountedDirectoryPath, yamlFileName); From 3724507fe3245452f4cd5a7f9154b44bb68b6b54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 21:10:23 +0100 Subject: [PATCH 083/129] MaxRetryCount = 0 not to waste time --- Lombiq.Tests.UI.Samples/UITestBase.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lombiq.Tests.UI.Samples/UITestBase.cs b/Lombiq.Tests.UI.Samples/UITestBase.cs index 3fe59f397..4510e4aa5 100644 --- a/Lombiq.Tests.UI.Samples/UITestBase.cs +++ b/Lombiq.Tests.UI.Samples/UITestBase.cs @@ -42,6 +42,8 @@ protected override Task ExecuteTestAsync( setupOperation, async configuration => { + configuration.MaxRetryCount = 0; + // You should always set the window size of the browser, otherwise the size will be random based on the // settings of the given machine. However this is already handled as long as the // context.Configuration.BrowserConfiguration.DefaultBrowserSize option is properly set. You can change From 8ceaf8d62ee7498e14a89a56818fb2d3d60cec6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 21:33:28 +0100 Subject: [PATCH 084/129] Another attempt to fix report creation issue under GitHub-hosted GHA runners --- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 4e8fa3aff..680561257 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis.Sarif; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Runtime.InteropServices; @@ -79,9 +80,10 @@ public async Task RunSecurityScanAsync( var mountedDirectoryPath = DirectoryPaths.GetTempSubDirectoryPath(context.Id, "Zap"); Directory.CreateDirectory(mountedDirectoryPath); - // Pre-creating the reports folder to avoid write permission issues under GitHub-hosted runners in GitHub + // Pre-creating the report's folder to avoid write permission issues under GitHub-hosted runners in GitHub // Actions (BuildJet ones work without this too). - Directory.CreateDirectory(Path.Combine(mountedDirectoryPath, _zapReportsDirectoryName)); + var reportSubFolder = $"{DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}-ZAP-Report-localhost"; + Directory.CreateDirectory(Path.Combine(mountedDirectoryPath, _zapReportsDirectoryName, reportSubFolder)); var yamlFileName = Path.GetFileName(automationFrameworkYamlPath); var yamlFileCopyPath = Path.Combine(mountedDirectoryPath, yamlFileName); From eca957d401ef1f9b5984eb51bb45c7770057d9cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 21:56:13 +0100 Subject: [PATCH 085/129] Another attempt to fix report creation issue under GitHub-hosted GHA runners with chmod --- .../SecurityScanning/ZapManager.cs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 680561257..50f8d4a61 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -2,10 +2,10 @@ using Lombiq.HelpfulLibraries.Cli; using Lombiq.Tests.UI.Constants; using Lombiq.Tests.UI.Services; +using Lombiq.Tests.UI.Services.GitHub; using Microsoft.CodeAnalysis.Sarif; using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Runtime.InteropServices; @@ -79,11 +79,17 @@ public async Task RunSecurityScanAsync( } var mountedDirectoryPath = DirectoryPaths.GetTempSubDirectoryPath(context.Id, "Zap"); - Directory.CreateDirectory(mountedDirectoryPath); - // Pre-creating the report's folder to avoid write permission issues under GitHub-hosted runners in GitHub - // Actions (BuildJet ones work without this too). - var reportSubFolder = $"{DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}-ZAP-Report-localhost"; - Directory.CreateDirectory(Path.Combine(mountedDirectoryPath, _zapReportsDirectoryName, reportSubFolder)); + var reportsDirectoryPath = Path.Combine(mountedDirectoryPath, _zapReportsDirectoryName); + Directory.CreateDirectory(reportsDirectoryPath); + + // Giving write permission to all users to the reports folder. This is to avoid issues under GitHub-hosted + // runners in GitHub Actions (BuildJet ones work without this too). + // Pre-creating the report's folder would just prompt ZAP to try another folder name suffixed with "2". + if (GitHubHelper.IsGitHubEnvironment) + { + await new CliProgram("chmod").ExecuteAndGetOutputAsync( + new[] { "a+w", reportsDirectoryPath }, additionalExceptionText: null, _cancellationTokenSource.Token); + } var yamlFileName = Path.GetFileName(automationFrameworkYamlPath); var yamlFileCopyPath = Path.Combine(mountedDirectoryPath, yamlFileName); @@ -153,8 +159,6 @@ public async Task RunSecurityScanAsync( throw new SecurityScanningException("Security scanning failed to complete. Check the test's output log for details."); } - var reportsDirectoryPath = Path.Combine(mountedDirectoryPath, _zapReportsDirectoryName); - var jsonReports = Directory.EnumerateFiles(reportsDirectoryPath, "*.json").ToList(); if (jsonReports.Count > 1) From 1328958bc43ca26b610cbf13dc7aa095711d883d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 22:15:34 +0100 Subject: [PATCH 086/129] Attempting to fix test temp directory cleanup failing due to something related to ZAP keeping a handle on it --- Lombiq.Tests.UI/Helpers/DirectoryHelper.cs | 2 +- Lombiq.Tests.UI/Services/UITestExecutionSession.cs | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/Helpers/DirectoryHelper.cs b/Lombiq.Tests.UI/Helpers/DirectoryHelper.cs index fdaaa0166..8af0780d7 100644 --- a/Lombiq.Tests.UI/Helpers/DirectoryHelper.cs +++ b/Lombiq.Tests.UI/Helpers/DirectoryHelper.cs @@ -18,7 +18,7 @@ public static void SafelyDeleteDirectoryIfExists(string path, int maxTryCount = { Directory.Delete(path, recursive: true); // Even after the delete seemingly succeeding the folder can remain there with some empty subfolders. - // Perhaps this happens when one opens it in Explorer and that keeps a handle open. + // Perhaps this happens when one opens it in Windows Explorer and that keeps a handle open. if (!Directory.Exists(path)) return; } catch (DirectoryNotFoundException) diff --git a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs index 9e60a6cc0..007ca6f1b 100644 --- a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs +++ b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs @@ -163,8 +163,11 @@ private async ValueTask ShutdownAsync() if (_applicationInstance != null) await _applicationInstance.DisposeAsync(); + string contextId = null; + if (_context != null) { + contextId = _context.Id; _context.Scope?.Dispose(); DirectoryHelper.SafelyDeleteDirectoryIfExists(DirectoryPaths.GetTempSubDirectoryPath(_context.Id)); @@ -182,6 +185,13 @@ private async ValueTask ShutdownAsync() if (_azureBlobStorageManager != null) await _azureBlobStorageManager.DisposeAsync(); if (_zapManager != null) await _zapManager.DisposeAsync(); + // First the context needs to be disposed before anything else, and then, once the other services free up any + // handles to the temp folder, that can be cleaned up too. + if (!string.IsNullOrEmpty(contextId)) + { + DirectoryHelper.SafelyDeleteDirectoryIfExists(DirectoryPaths.GetTempSubDirectoryPath(contextId)); + } + _screenshotCount = 0; _context = null; From a9bf935fb66df4d57c3807157a61276a9e3afaef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 22:46:09 +0100 Subject: [PATCH 087/129] How about chmod a+x? --- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 50f8d4a61..37e285512 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -88,7 +88,7 @@ public async Task RunSecurityScanAsync( if (GitHubHelper.IsGitHubEnvironment) { await new CliProgram("chmod").ExecuteAndGetOutputAsync( - new[] { "a+w", reportsDirectoryPath }, additionalExceptionText: null, _cancellationTokenSource.Token); + new[] { "a+x", reportsDirectoryPath }, additionalExceptionText: null, _cancellationTokenSource.Token); } var yamlFileName = Path.GetFileName(automationFrameworkYamlPath); From 3645b937008afaba94cb3b884091cc61cc7a4e2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 22:50:02 +0100 Subject: [PATCH 088/129] Trying to restore the original permissions of the reports folder --- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 37e285512..490cf8512 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -85,10 +85,13 @@ public async Task RunSecurityScanAsync( // Giving write permission to all users to the reports folder. This is to avoid issues under GitHub-hosted // runners in GitHub Actions (BuildJet ones work without this too). // Pre-creating the report's folder would just prompt ZAP to try another folder name suffixed with "2". + string originalReportsFolderPermissions = null; if (GitHubHelper.IsGitHubEnvironment) { - await new CliProgram("chmod").ExecuteAndGetOutputAsync( - new[] { "a+x", reportsDirectoryPath }, additionalExceptionText: null, _cancellationTokenSource.Token); + originalReportsFolderPermissions = await new CliProgram("stat").ExecuteAndGetOutputAsync( + _cancellationTokenSource.Token, "-c", "%a", reportsDirectoryPath); + + await new CliProgram("chmod").ExecuteAsync(_cancellationTokenSource.Token, "a+w", reportsDirectoryPath); } var yamlFileName = Path.GetFileName(automationFrameworkYamlPath); @@ -154,9 +157,11 @@ public async Task RunSecurityScanAsync( .WithValidation(CommandResultValidation.None) .ExecuteAsync(_cancellationTokenSource.Token); + _testOutputHelper.WriteLineTimestampedAndDebug("Security scanning completed with the exit code {0}.", result.ExitCode); + if (result.ExitCode == 1) { - throw new SecurityScanningException("Security scanning failed to complete. Check the test's output log for details."); + throw new SecurityScanningException("Security scanning didn't successfully finish. Check the test's output log for details."); } var jsonReports = Directory.EnumerateFiles(reportsDirectoryPath, "*.json").ToList(); @@ -175,6 +180,12 @@ public async Task RunSecurityScanAsync( "Check the test output for details."); } + // Restoring original permissions, otherwise the post-test clean-up wouldn't be able to delete the folder. + if (!string.IsNullOrEmpty(originalReportsFolderPermissions)) + { + await new CliProgram("chmod").ExecuteAsync(_cancellationTokenSource.Token, originalReportsFolderPermissions, reportsDirectoryPath); + } + return new SecurityScanResult(reportsDirectoryPath, SarifLog.Load(jsonReports[0])); } From 1a9868c8cd9c971c1c114e59260bd9f66e8e7538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 22:50:15 +0100 Subject: [PATCH 089/129] Debug code --- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 490cf8512..6da501099 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -159,6 +159,9 @@ public async Task RunSecurityScanAsync( _testOutputHelper.WriteLineTimestampedAndDebug("Security scanning completed with the exit code {0}.", result.ExitCode); + var psOutput = _docker.ExecuteAndGetOutputAsync(_cancellationTokenSource.Token, "ps"); + _testOutputHelper.WriteLineTimestampedAndDebug("Are there any Docker processes running? {0}", psOutput); + if (result.ExitCode == 1) { throw new SecurityScanningException("Security scanning didn't successfully finish. Check the test's output log for details."); From 26aa5ecdf614ceb83619e2e5b89a2ff320634d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 23:03:33 +0100 Subject: [PATCH 090/129] Fixing debug code --- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 6da501099..b447b9e10 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -159,7 +159,7 @@ public async Task RunSecurityScanAsync( _testOutputHelper.WriteLineTimestampedAndDebug("Security scanning completed with the exit code {0}.", result.ExitCode); - var psOutput = _docker.ExecuteAndGetOutputAsync(_cancellationTokenSource.Token, "ps"); + var psOutput = await _docker.ExecuteAndGetOutputAsync(_cancellationTokenSource.Token, "ps"); _testOutputHelper.WriteLineTimestampedAndDebug("Are there any Docker processes running? {0}", psOutput); if (result.ExitCode == 1) From 8459d69383a8979bba348dd4598c8fb30a45c19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 23:03:54 +0100 Subject: [PATCH 091/129] Simplifying ExecuteAndGetOutputAsync() call with new overload --- Lombiq.Tests.UI/Services/SqlServerManager.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Lombiq.Tests.UI/Services/SqlServerManager.cs b/Lombiq.Tests.UI/Services/SqlServerManager.cs index 57cb7fd5b..3fdaed8f3 100644 --- a/Lombiq.Tests.UI/Services/SqlServerManager.cs +++ b/Lombiq.Tests.UI/Services/SqlServerManager.cs @@ -269,10 +269,7 @@ private Task DockerExecuteAsync(string containerName, params object[] command) = CancellationToken.None); private Task DockerExecuteAndGetOutputAsync(string containerName, params object[] command) => - _docker.ExecuteAndGetOutputAsync( - CreateArguments(containerName, command), - additionalExceptionText: null, - CancellationToken.None); + _docker.ExecuteAndGetOutputAsync(CancellationToken.None, CreateArguments(containerName, command)); private static List CreateArguments(string containerName, params object[] command) { From 691ff510addf66d63648c2773996ed3497832691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 23:05:55 +0100 Subject: [PATCH 092/129] Debug output --- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index b447b9e10..a0920b9aa 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -91,6 +91,8 @@ public async Task RunSecurityScanAsync( originalReportsFolderPermissions = await new CliProgram("stat").ExecuteAndGetOutputAsync( _cancellationTokenSource.Token, "-c", "%a", reportsDirectoryPath); + _testOutputHelper.WriteLineTimestampedAndDebug("Original permissions: {0}", originalReportsFolderPermissions); + await new CliProgram("chmod").ExecuteAsync(_cancellationTokenSource.Token, "a+w", reportsDirectoryPath); } From 2eafae1c04f501ce3b5ef66ca3457f9c8580b4e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 23:06:59 +0100 Subject: [PATCH 093/129] Attempting additional chmod --- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index a0920b9aa..f21a1ea6b 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -94,6 +94,9 @@ public async Task RunSecurityScanAsync( _testOutputHelper.WriteLineTimestampedAndDebug("Original permissions: {0}", originalReportsFolderPermissions); await new CliProgram("chmod").ExecuteAsync(_cancellationTokenSource.Token, "a+w", reportsDirectoryPath); + // If we don't change permissions for the parent too, then the clean-up after the test wouldn't be able to + // delete the temp folder. + await new CliProgram("chmod").ExecuteAsync(_cancellationTokenSource.Token, "a+w", mountedDirectoryPath); } var yamlFileName = Path.GetFileName(automationFrameworkYamlPath); From db617f2e99dde71dc484b18bc0751c210231ed9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 23:07:34 +0100 Subject: [PATCH 094/129] Fail-safe for a clean-up fail under GHA --- Lombiq.Tests.UI/Services/UITestExecutionSession.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs index 007ca6f1b..45844e3a4 100644 --- a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs +++ b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs @@ -189,7 +189,17 @@ private async ValueTask ShutdownAsync() // handles to the temp folder, that can be cleaned up too. if (!string.IsNullOrEmpty(contextId)) { - DirectoryHelper.SafelyDeleteDirectoryIfExists(DirectoryPaths.GetTempSubDirectoryPath(contextId)); + try + { + DirectoryHelper.SafelyDeleteDirectoryIfExists(DirectoryPaths.GetTempSubDirectoryPath(contextId)); + } + catch (Exception ex) when (GitHubHelper.IsGitHubEnvironment) + { + _testOutputHelper.WriteLineTimestampedAndDebug( + "Cleaning up the temporary directory failed with the following exception. Due to using ephemeral " + + "GitHub Actions runners, this is not a fatal error. Exception details: {0}", + ex); + } } _screenshotCount = 0; From b65ccd92927d029f4e9f234eeabcb161d7afa420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 23:23:42 +0100 Subject: [PATCH 095/129] Fixing restoring the original folder permission --- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index f21a1ea6b..8f1c7d527 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -90,6 +90,7 @@ public async Task RunSecurityScanAsync( { originalReportsFolderPermissions = await new CliProgram("stat").ExecuteAndGetOutputAsync( _cancellationTokenSource.Token, "-c", "%a", reportsDirectoryPath); + originalReportsFolderPermissions = originalReportsFolderPermissions.Trim(); _testOutputHelper.WriteLineTimestampedAndDebug("Original permissions: {0}", originalReportsFolderPermissions); From 3a9b0f6243ce5ccfe0309ab520a0a16f06a92d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 23:25:36 +0100 Subject: [PATCH 096/129] Removing leftover directory deletion --- Lombiq.Tests.UI/Services/UITestExecutionSession.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs index 45844e3a4..753a1c8d6 100644 --- a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs +++ b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs @@ -170,8 +170,6 @@ private async ValueTask ShutdownAsync() contextId = _context.Id; _context.Scope?.Dispose(); - DirectoryHelper.SafelyDeleteDirectoryIfExists(DirectoryPaths.GetTempSubDirectoryPath(_context.Id)); - _context.FailureDumpContainer.Values.ForEach(value => value.Dispose()); _context.FailureDumpContainer.Clear(); } From 7ddb051f01737592fcc71cc24bb0e6f81a7bb5c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 23:40:08 +0100 Subject: [PATCH 097/129] Removing useless code --- .../SecurityScanning/ZapManager.cs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 8f1c7d527..33f13e065 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -85,19 +85,9 @@ public async Task RunSecurityScanAsync( // Giving write permission to all users to the reports folder. This is to avoid issues under GitHub-hosted // runners in GitHub Actions (BuildJet ones work without this too). // Pre-creating the report's folder would just prompt ZAP to try another folder name suffixed with "2". - string originalReportsFolderPermissions = null; if (GitHubHelper.IsGitHubEnvironment) { - originalReportsFolderPermissions = await new CliProgram("stat").ExecuteAndGetOutputAsync( - _cancellationTokenSource.Token, "-c", "%a", reportsDirectoryPath); - originalReportsFolderPermissions = originalReportsFolderPermissions.Trim(); - - _testOutputHelper.WriteLineTimestampedAndDebug("Original permissions: {0}", originalReportsFolderPermissions); - await new CliProgram("chmod").ExecuteAsync(_cancellationTokenSource.Token, "a+w", reportsDirectoryPath); - // If we don't change permissions for the parent too, then the clean-up after the test wouldn't be able to - // delete the temp folder. - await new CliProgram("chmod").ExecuteAsync(_cancellationTokenSource.Token, "a+w", mountedDirectoryPath); } var yamlFileName = Path.GetFileName(automationFrameworkYamlPath); @@ -165,9 +155,6 @@ public async Task RunSecurityScanAsync( _testOutputHelper.WriteLineTimestampedAndDebug("Security scanning completed with the exit code {0}.", result.ExitCode); - var psOutput = await _docker.ExecuteAndGetOutputAsync(_cancellationTokenSource.Token, "ps"); - _testOutputHelper.WriteLineTimestampedAndDebug("Are there any Docker processes running? {0}", psOutput); - if (result.ExitCode == 1) { throw new SecurityScanningException("Security scanning didn't successfully finish. Check the test's output log for details."); @@ -189,12 +176,6 @@ public async Task RunSecurityScanAsync( "Check the test output for details."); } - // Restoring original permissions, otherwise the post-test clean-up wouldn't be able to delete the folder. - if (!string.IsNullOrEmpty(originalReportsFolderPermissions)) - { - await new CliProgram("chmod").ExecuteAsync(_cancellationTokenSource.Token, originalReportsFolderPermissions, reportsDirectoryPath); - } - return new SecurityScanResult(reportsDirectoryPath, SarifLog.Load(jsonReports[0])); } From 7b9e58ddd288849214526db302b15606283cb9c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 23:41:10 +0100 Subject: [PATCH 098/129] Adding debug code to see if the chmod alone breaks clean-up --- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 33f13e065..094eafd3f 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -90,6 +90,8 @@ public async Task RunSecurityScanAsync( await new CliProgram("chmod").ExecuteAsync(_cancellationTokenSource.Token, "a+w", reportsDirectoryPath); } + throw new Exception("Debug exception."); + var yamlFileName = Path.GetFileName(automationFrameworkYamlPath); var yamlFileCopyPath = Path.Combine(mountedDirectoryPath, yamlFileName); From 624bd3cf66f797fda32ef527046bae6377e25c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 23:42:00 +0100 Subject: [PATCH 099/129] Removing chmod to see if anything else breaks clean-up --- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 094eafd3f..511551028 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -2,7 +2,6 @@ using Lombiq.HelpfulLibraries.Cli; using Lombiq.Tests.UI.Constants; using Lombiq.Tests.UI.Services; -using Lombiq.Tests.UI.Services.GitHub; using Microsoft.CodeAnalysis.Sarif; using System; using System.Collections.Generic; @@ -85,10 +84,7 @@ public async Task RunSecurityScanAsync( // Giving write permission to all users to the reports folder. This is to avoid issues under GitHub-hosted // runners in GitHub Actions (BuildJet ones work without this too). // Pre-creating the report's folder would just prompt ZAP to try another folder name suffixed with "2". - if (GitHubHelper.IsGitHubEnvironment) - { - await new CliProgram("chmod").ExecuteAsync(_cancellationTokenSource.Token, "a+w", reportsDirectoryPath); - } + throw new Exception("Debug exception."); From aa20e16e37034a9fce2c03e43e84807fcd30b0ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 23 Nov 2023 23:57:27 +0100 Subject: [PATCH 100/129] Removing debug code --- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 511551028..33f13e065 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -2,6 +2,7 @@ using Lombiq.HelpfulLibraries.Cli; using Lombiq.Tests.UI.Constants; using Lombiq.Tests.UI.Services; +using Lombiq.Tests.UI.Services.GitHub; using Microsoft.CodeAnalysis.Sarif; using System; using System.Collections.Generic; @@ -84,9 +85,10 @@ public async Task RunSecurityScanAsync( // Giving write permission to all users to the reports folder. This is to avoid issues under GitHub-hosted // runners in GitHub Actions (BuildJet ones work without this too). // Pre-creating the report's folder would just prompt ZAP to try another folder name suffixed with "2". - - - throw new Exception("Debug exception."); + if (GitHubHelper.IsGitHubEnvironment) + { + await new CliProgram("chmod").ExecuteAsync(_cancellationTokenSource.Token, "a+w", reportsDirectoryPath); + } var yamlFileName = Path.GetFileName(automationFrameworkYamlPath); var yamlFileCopyPath = Path.Combine(mountedDirectoryPath, yamlFileName); From acffc2b35aac867eeb9818f944a34d639b4bacd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Fri, 24 Nov 2023 00:00:00 +0100 Subject: [PATCH 101/129] Docs --- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 2 +- Lombiq.Tests.UI/Services/UITestExecutionSession.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 33f13e065..181c34710 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -83,7 +83,7 @@ public async Task RunSecurityScanAsync( Directory.CreateDirectory(reportsDirectoryPath); // Giving write permission to all users to the reports folder. This is to avoid issues under GitHub-hosted - // runners in GitHub Actions (BuildJet ones work without this too). + // runners in GitHub Actions (BuildJet ones work without this too) at ZAP not being able to create the report. // Pre-creating the report's folder would just prompt ZAP to try another folder name suffixed with "2". if (GitHubHelper.IsGitHubEnvironment) { diff --git a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs index 753a1c8d6..95e1a9aef 100644 --- a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs +++ b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs @@ -193,6 +193,7 @@ private async ValueTask ShutdownAsync() } catch (Exception ex) when (GitHubHelper.IsGitHubEnvironment) { + // This can be caused by running a security scan via ZapManager. _testOutputHelper.WriteLineTimestampedAndDebug( "Cleaning up the temporary directory failed with the following exception. Due to using ephemeral " + "GitHub Actions runners, this is not a fatal error. Exception details: {0}", From a26715199137ee9978839193264d43701b37531f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Fri, 24 Nov 2023 00:00:27 +0100 Subject: [PATCH 102/129] Intentionally failing security scan to test artifacts --- Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 0bac66ab0..8964f19e8 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -69,7 +69,7 @@ public Task SecurityScanWithCustomConfigurationShouldPass() => .DisablePassiveScanRule(10037, "Server Leaks Information via \"X-Powered-By\" HTTP Response Header Field(s)") .DisableScanRuleForUrlWithRegex(".*/about", 10038, "Content Security Policy (CSP) Header Not Set") .SignIn(), - sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeLessThan(200))); + sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeLessThan(2))); // Let's get low-level into ZAP's configuration now. While the .NET configuration API of the Lombiq UI Testing // Toolbox covers the most important ways to configure ZAP, sometimes you need more. For this, you have complete From 879570dfbd46a10a874afd7b4a3a52ea6d3937da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Fri, 24 Nov 2023 00:01:34 +0100 Subject: [PATCH 103/129] Removing MaxRetryCount = 0 --- Lombiq.Tests.UI.Samples/UITestBase.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/UITestBase.cs b/Lombiq.Tests.UI.Samples/UITestBase.cs index 4510e4aa5..3fe59f397 100644 --- a/Lombiq.Tests.UI.Samples/UITestBase.cs +++ b/Lombiq.Tests.UI.Samples/UITestBase.cs @@ -42,8 +42,6 @@ protected override Task ExecuteTestAsync( setupOperation, async configuration => { - configuration.MaxRetryCount = 0; - // You should always set the window size of the browser, otherwise the size will be random based on the // settings of the given machine. However this is already handled as long as the // context.Configuration.BrowserConfiguration.DefaultBrowserSize option is properly set. You can change From c16dcc9f809dd0fcb60102a0f0b052e9a153f5ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Fri, 24 Nov 2023 00:22:45 +0100 Subject: [PATCH 104/129] Revert "Intentionally failing security scan to test artifacts" This reverts commit a26715199137ee9978839193264d43701b37531f. --- Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 8964f19e8..0bac66ab0 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -69,7 +69,7 @@ public Task SecurityScanWithCustomConfigurationShouldPass() => .DisablePassiveScanRule(10037, "Server Leaks Information via \"X-Powered-By\" HTTP Response Header Field(s)") .DisableScanRuleForUrlWithRegex(".*/about", 10038, "Content Security Policy (CSP) Header Not Set") .SignIn(), - sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeLessThan(2))); + sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeLessThan(200))); // Let's get low-level into ZAP's configuration now. While the .NET configuration API of the Lombiq UI Testing // Toolbox covers the most important ways to configure ZAP, sometimes you need more. For this, you have complete From 2122a9916dfc22e970a100b3064ff137d23c8d05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Fri, 24 Nov 2023 00:26:11 +0100 Subject: [PATCH 105/129] Updating Helpful Libraries references --- Lombiq.Tests.UI.Shortcuts/Lombiq.Tests.UI.Shortcuts.csproj | 2 +- Lombiq.Tests.UI/Lombiq.Tests.UI.csproj | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lombiq.Tests.UI.Shortcuts/Lombiq.Tests.UI.Shortcuts.csproj b/Lombiq.Tests.UI.Shortcuts/Lombiq.Tests.UI.Shortcuts.csproj index fd2d7525d..d062c5111 100644 --- a/Lombiq.Tests.UI.Shortcuts/Lombiq.Tests.UI.Shortcuts.csproj +++ b/Lombiq.Tests.UI.Shortcuts/Lombiq.Tests.UI.Shortcuts.csproj @@ -50,7 +50,7 @@ - + diff --git a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj index ee6eb3391..d47c3128c 100644 --- a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj +++ b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj @@ -103,9 +103,9 @@ - - - + + + From 7316bd646f66b1e8dd5b8b4819aa015cd9196739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Fri, 24 Nov 2023 00:49:47 +0100 Subject: [PATCH 106/129] Docs --- Lombiq.Tests.UI/Docs/SecurityScanning.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/Docs/SecurityScanning.md b/Lombiq.Tests.UI/Docs/SecurityScanning.md index 94083ec09..e0e932683 100644 --- a/Lombiq.Tests.UI/Docs/SecurityScanning.md +++ b/Lombiq.Tests.UI/Docs/SecurityScanning.md @@ -14,7 +14,7 @@ You can create detailed security scans of your app with [Zed Attack Proxy (ZAP)] ## Working with ZAP in the Lombiq UI Testing Toolbox - We recommend you first check out the [related samples in the `Lombiq.Tests.UI.Samples` project](../../Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs). -- If you're new to ZAP, you can start learning by checking out [ZAP's getting started guide](https://www.zaproxy.org/getting-started/), as well as the [ZAP Chat Videos](https://www.zaproxy.org/zap-chat/). The [documentation on ZAP's Automation Framework](https://www.zaproxy.org/docs/automate/automation-framework/) and the [ZAP Chat 06 Automation Introduction video](https://www.youtube.com/watch?v=PnCbIAnauD8) will help you understand what we use under the hood to instruct ZAP, and will allow you to use your completely custom Automation Framework plans too. +- If you're new to ZAP, you can start learning by checking out [ZAP's getting started guide](https://www.zaproxy.org/getting-started/), as well as the [ZAP Chat Videos](https://www.zaproxy.org/zap-chat/). The [documentation on ZAP's Automation Framework](https://www.zaproxy.org/docs/automate/automation-framework/) and the [ZAP Chat 06 Automation Introduction video](https://www.youtube.com/watch?v=PnCbIAnauD8) (as well as the subsequent videos about it in the series) will help you understand what we use under the hood to instruct ZAP, and will allow you to use your completely custom Automation Framework plans too. - Be aware that ZAP scans run its own spider or with an internally managed browser instance, not in the browser launched by the test. - While ZAP is fully managed for you, Docker needs to be available and running to host the ZAP instance. On your development machine, you can install [Docker Desktop](https://www.docker.com/products/docker-desktop/). - The full scan of a website with even just 1-200 pages can take 5-10 minutes. So, be careful to fine-tune the ZAP configuration to make it suitable for your app. From d5dd603d636caffba29acabb00b2483e76bd08be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Fri, 24 Nov 2023 00:55:10 +0100 Subject: [PATCH 107/129] Fixing SQL Docker operations --- Lombiq.Tests.UI/Services/SqlServerManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/Services/SqlServerManager.cs b/Lombiq.Tests.UI/Services/SqlServerManager.cs index 3fdaed8f3..055526f82 100644 --- a/Lombiq.Tests.UI/Services/SqlServerManager.cs +++ b/Lombiq.Tests.UI/Services/SqlServerManager.cs @@ -269,7 +269,7 @@ private Task DockerExecuteAsync(string containerName, params object[] command) = CancellationToken.None); private Task DockerExecuteAndGetOutputAsync(string containerName, params object[] command) => - _docker.ExecuteAndGetOutputAsync(CancellationToken.None, CreateArguments(containerName, command)); + _docker.ExecuteAndGetOutputAsync(CancellationToken.None, CreateArguments(containerName, command).ToArray()); private static List CreateArguments(string containerName, params object[] command) { From 6272d305cbd253b089c71b2b010e6269fa9b1e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Fri, 24 Nov 2023 00:57:15 +0100 Subject: [PATCH 108/129] Simplifying SQL Server Docker CLI calls --- Lombiq.Tests.UI/Services/SqlServerManager.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Lombiq.Tests.UI/Services/SqlServerManager.cs b/Lombiq.Tests.UI/Services/SqlServerManager.cs index 055526f82..b0df97248 100644 --- a/Lombiq.Tests.UI/Services/SqlServerManager.cs +++ b/Lombiq.Tests.UI/Services/SqlServerManager.cs @@ -263,20 +263,17 @@ public async Task RestoreSnapshotAsync( } private Task DockerExecuteAsync(string containerName, params object[] command) => - _docker.ExecuteAsync( - CreateArguments(containerName, command), - additionalExceptionText: null, - CancellationToken.None); + _docker.ExecuteAsync(CancellationToken.None, CreateArguments(containerName, command)); private Task DockerExecuteAndGetOutputAsync(string containerName, params object[] command) => - _docker.ExecuteAndGetOutputAsync(CancellationToken.None, CreateArguments(containerName, command).ToArray()); + _docker.ExecuteAndGetOutputAsync(CancellationToken.None, CreateArguments(containerName, command)); - private static List CreateArguments(string containerName, params object[] command) + private static object[] CreateArguments(string containerName, params object[] command) { var arguments = new List { "exec", "-u", 0, containerName }; arguments.AddRange(command); - return arguments; + return arguments.ToArray(); } public async ValueTask DisposeAsync() From f36a553360e98df26a12241c8ca1102cf8922bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Fri, 24 Nov 2023 14:19:43 +0100 Subject: [PATCH 109/129] Adding script to display display the runtime of scan rules --- Lombiq.Tests.UI/Docs/SecurityScanning.md | 1 + Lombiq.Tests.UI/Lombiq.Tests.UI.csproj | 1 + .../DisplayActiveScanRuleRuntimesScript.yml | 25 ++++++++++++ .../AutomationFrameworkPlanFragmentsPaths.cs | 5 ++- .../YamlDocumentExtensions.cs | 40 +++++++++++++++++-- 5 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragments/DisplayActiveScanRuleRuntimesScript.yml diff --git a/Lombiq.Tests.UI/Docs/SecurityScanning.md b/Lombiq.Tests.UI/Docs/SecurityScanning.md index e0e932683..605219f8f 100644 --- a/Lombiq.Tests.UI/Docs/SecurityScanning.md +++ b/Lombiq.Tests.UI/Docs/SecurityScanning.md @@ -25,3 +25,4 @@ You can create detailed security scans of your app with [Zed Attack Proxy (ZAP)] - If an alert is a false positive, follow [the official docs](https://www.zaproxy.org/faq/how-do-i-handle-a-false-positive/). You can use the [`alertFilter` job](https://www.zaproxy.org/docs/desktop/addons/alert-filters/automation/) to ignore alerts in very specific conditions. You can also access this via the .NET configuration API. - ZAP didn't find everything in your app? By default, ZAP has a crawl depth of 5 for its standard spider and 10 for its AJAX spider. Set `maxDepth` (and `maxChildren`) [for `spider`](https://www.zaproxy.org/docs/desktop/addons/automation-framework/job-spider/) and `maxCrawlDepth` [for `spiderAjax`](https://www.zaproxy.org/docs/desktop/addons/ajax-spider/automation/). - Do you sometimes get slightly different scan results? This is normal, and ZAP can be inconsistent/appear random within limits, see [the official docs page](https://www.zaproxy.org/faq/why-can-zap-scans-be-inconsistent/). +- Is the active scan too slow? You can find out which rules take the most time by adding a script displaying each rules' runtime with `YamlDocumentExtensions.AddDisplayActiveScanRuleRuntimesScriptAfterActiveScan()`. diff --git a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj index d47c3128c..8ae1cc497 100644 --- a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj +++ b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj @@ -124,6 +124,7 @@ + diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragments/DisplayActiveScanRuleRuntimesScript.yml b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragments/DisplayActiveScanRuleRuntimesScript.yml new file mode 100644 index 000000000..464dc14cd --- /dev/null +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragments/DisplayActiveScanRuleRuntimesScript.yml @@ -0,0 +1,25 @@ +- parameters: + action: "add" + type: "active" + engine: "ECMAScript : Graal.js" + name: "displayRuleRuntimes" + target: "" + inline: "var extAscan = control.getExtensionLoader().getExtension(\n org.zaproxy.zap.extension.ascan.ExtensionActiveScan.NAME);\n\ + \nif (extAscan != null) {\n var lastScan = extAscan.getLastScan();\n if (lastScan\ + \ != null) {\n var hps = lastScan.getHostProcesses().toArray();\n for\ + \ (var i=0; i < hps.length; i++) {\n var hp = hps[i];\n var plugins\ + \ = hp.getCompleted().toArray();\n for (var j=0; j < plugins.length; j++)\ + \ {\n var plugin = plugins[j];\n var timeTaken = plugin.getTimeFinished().getTime()\n\ + \ - plugin.getTimeStarted().getTime();\n print(plugin.getName()\ + \ + \"\\t\" + timeTaken);\n }\n }\n }\n}\n" + name: "script" + type: "script" +- parameters: + action: "run" + type: "standalone" + engine: "" + name: "displayRuleRuntimes" + target: "" + inline: "" + name: "script" + type: "script" diff --git a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragmentsPaths.cs b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragmentsPaths.cs index a31ae4ecd..b9fe60f43 100644 --- a/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragmentsPaths.cs +++ b/Lombiq.Tests.UI/SecurityScanning/AutomationFrameworkPlanFragmentsPaths.cs @@ -7,7 +7,8 @@ public static class AutomationFrameworkPlanFragmentsPaths private static readonly string AutomationFrameworkPlanFragmentsPath = Path.Combine("SecurityScanning", "AutomationFrameworkPlanFragments"); + public static readonly string DisplayActiveScanRuleRuntimesScriptPath = + Path.Combine(AutomationFrameworkPlanFragmentsPath, "DisplayActiveScanRuleRuntimesScript.yml"); + public static readonly string RequestorJobPath = Path.Combine(AutomationFrameworkPlanFragmentsPath, "RequestorJob.yml"); public static readonly string SpiderAjaxJobPath = Path.Combine(AutomationFrameworkPlanFragmentsPath, "SpiderAjaxJob.yml"); - public static readonly string RequestorJobPath = - Path.Combine(AutomationFrameworkPlanFragmentsPath, "RequestorJob.yml"); } diff --git a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs index 2864d22e9..92e900a7c 100644 --- a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs @@ -52,10 +52,10 @@ public static YamlDocument AddUrl(this YamlDocument yamlDocument, Uri uri) /// /// Adds the ZAP Ajax Spider - /// to the ZAP Automation Framework plan, just after the "spider" job. + /// to the ZAP Automation Framework plan, just after the job named "spider". /// /// - /// If no job named "spider" is found in the ZAP Automation Framework plan. + /// Thrown if no job named "spider" is found in the ZAP Automation Framework plan. /// public static YamlDocument AddSpiderAjaxAfterSpider(this YamlDocument yamlDocument) { @@ -68,7 +68,41 @@ public static YamlDocument AddSpiderAjaxAfterSpider(this YamlDocument yamlDocume var spiderIndex = jobs.Children.IndexOf(spiderJob); var spiderAjaxJob = YamlHelper.LoadDocument(AutomationFrameworkPlanFragmentsPaths.SpiderAjaxJobPath); - jobs.Children.Insert(spiderIndex + 1, spiderAjaxJob.GetRootNode()); + jobs.Children.Insert(spiderIndex + 1, spiderAjaxJob.RootNode); + + return yamlDocument; + } + + /// + /// Adds a script to the ZAP Automation Framework plan that displays the runtime of each active scan rule, in milliseconds, + /// just after the first job with the type "activeScan". + /// + /// + /// Thrown if no job with the type "activeScan" is found in the ZAP Automation Framework plan. + /// + /// + /// + /// Script code taken from . + /// + /// + public static YamlDocument AddDisplayActiveScanRuleRuntimesScriptAfterActiveScan(this YamlDocument yamlDocument) + { + var jobs = yamlDocument.GetJobs(); + var activeScanJob = + yamlDocument.GetJobByType("activeScan") ?? + throw new ArgumentException( + "No job with the type \"activeScan\" found in the Automation Framework Plan. We can only add the " + + "active scan rule runtime-displaying script immediately after it."); + + var activeScanIndex = jobs.Children.IndexOf(activeScanJob); + var scriptJobs = ((YamlSequenceNode)YamlHelper + .LoadDocument(AutomationFrameworkPlanFragmentsPaths.DisplayActiveScanRuleRuntimesScriptPath).RootNode) + .Children; + + for (int i = scriptJobs.Count - 1; i >= 0; i--) + { + jobs.Children.Insert(activeScanIndex + 1, scriptJobs[i]); + } return yamlDocument; } From 44b117b0919acadc67b718b82741cad85ecceaa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Fri, 24 Nov 2023 14:19:57 +0100 Subject: [PATCH 110/129] Excluding technologies also in CustomZapAutomationFrameworkPlan --- .../Tests/CustomZapAutomationFrameworkPlan.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/CustomZapAutomationFrameworkPlan.yml b/Lombiq.Tests.UI.Samples/Tests/CustomZapAutomationFrameworkPlan.yml index ff35bfc73..ba2162096 100644 --- a/Lombiq.Tests.UI.Samples/Tests/CustomZapAutomationFrameworkPlan.yml +++ b/Lombiq.Tests.UI.Samples/Tests/CustomZapAutomationFrameworkPlan.yml @@ -15,7 +15,22 @@ env: method: "cookie" parameters: {} technology: - exclude: [] + exclude: + - "C" + - "IBM DB2" + - "PHP" + - "CouchDB" + - "Oracle" + - "JSP/Servlet" + - "Firebird" + - "HypersonicSQL" + - "SAP MaxDB" + - "Ruby" + - "Microsoft Access" + - "Java" + - "Tomcat" + - "Sybase" + - "Python" parameters: failOnError: true failOnWarning: false From e4cadd444b57006c392f795d1da8f7e90a8bd447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 30 Nov 2023 19:20:35 +0100 Subject: [PATCH 111/129] Typo Co-authored-by: Benedek Farkas --- Lombiq.Tests.UI/Services/UITestContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/Services/UITestContext.cs b/Lombiq.Tests.UI/Services/UITestContext.cs index 3dbc878ed..a1da23400 100644 --- a/Lombiq.Tests.UI/Services/UITestContext.cs +++ b/Lombiq.Tests.UI/Services/UITestContext.cs @@ -63,7 +63,7 @@ public class UITestContext public AzureBlobStorageRunningContext AzureBlobStorageRunningContext { get; } /// - /// Gets the service to manage Zed Attack Proxy (ZAP) instances for + /// Gets the service to manage Zed Attack Proxy (ZAP) instances for /// security scanning. Usually, it's recommended to use the higher-level ZAP extension methods instead. /// From bed71dedde930bcc3cc4cd8fcf7d7ae9b1e9c2b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 30 Nov 2023 19:21:31 +0100 Subject: [PATCH 112/129] Grammar Co-authored-by: Benedek Farkas --- Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs index 863c787ee..59611de03 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs @@ -68,7 +68,7 @@ public SecurityScanConfiguration SignIn(string userName = DefaultUser.UserName) } /// - /// Excludes a given URL from the scan completely. + /// Excludes URLs from the scan that are matched by the supplied regex. /// /// /// The regex pattern to match URLs against to exclude them. These should be patterns that match the whole absolute From 56dddac66c14498ef055c8bc00c18ad38dd142e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 30 Nov 2023 19:25:43 +0100 Subject: [PATCH 113/129] SecurityScanConfiguration docs clarification and reference fixes Co-authored-by: Benedek Farkas --- .../SecurityScanning/SecurityScanConfiguration.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs index 59611de03..190a87653 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs @@ -81,8 +81,8 @@ public SecurityScanConfiguration ExcludeUrlWithRegex(string excludedUrlRegex) } /// - /// Disable a certain active scan rule for the whole scan. If you only want to disable a rule for a given page, use - /// instead. + /// Disable a certain active scan rule for the whole scan. If you only want to disable a rule for specific pages + /// matched by a regex, use instead. /// /// The ID of the rule. In the scan report, this is usually displayed as "Plugin Id". /// @@ -96,8 +96,8 @@ public SecurityScanConfiguration DisableActiveScanRule(int id, string name = "") } /// - /// Disable a certain passive scan rule for the whole scan. If you only want to disable a rule for a given page, use - /// instead. + /// Disable a certain passive scan rule for the whole scan. If you only want to disable a rule for specific pages + /// matched by a regex, use instead. /// /// The ID of the rule. In the scan report, this is usually displayed as "Plugin Id". /// From 8c2c3ee21d464f0e7bdba34760ded136c04b223b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 30 Nov 2023 19:29:26 +0100 Subject: [PATCH 114/129] Fixing docs method reference --- Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs index 190a87653..89a0fada7 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs @@ -82,7 +82,7 @@ public SecurityScanConfiguration ExcludeUrlWithRegex(string excludedUrlRegex) /// /// Disable a certain active scan rule for the whole scan. If you only want to disable a rule for specific pages - /// matched by a regex, use instead. + /// matched by a regex, use instead. /// /// The ID of the rule. In the scan report, this is usually displayed as "Plugin Id". /// @@ -97,7 +97,7 @@ public SecurityScanConfiguration DisableActiveScanRule(int id, string name = "") /// /// Disable a certain passive scan rule for the whole scan. If you only want to disable a rule for specific pages - /// matched by a regex, use instead. + /// matched by a regex, use instead. /// /// The ID of the rule. In the scan report, this is usually displayed as "Plugin Id". /// From 145c0adf2ef1b49f76a496cb997f3e298bd0ad48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 30 Nov 2023 19:31:45 +0100 Subject: [PATCH 115/129] Clarifying URL regex configurations --- .../SecurityScanning/SecurityScanConfiguration.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs index 89a0fada7..1f5455bb3 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs @@ -71,8 +71,8 @@ public SecurityScanConfiguration SignIn(string userName = DefaultUser.UserName) /// Excludes URLs from the scan that are matched by the supplied regex. /// /// - /// The regex pattern to match URLs against to exclude them. These should be patterns that match the whole absolute - /// URL, so something like ".*blog.*" to match /blog, /blog/my-post, etc. + /// The regex pattern to match URLs against. It will be matched against the whole absolute URL, e.g., ".*blog.*" + /// will match https://example.com/blog, https://example.com/blog/my-post, etc. /// public SecurityScanConfiguration ExcludeUrlWithRegex(string excludedUrlRegex) { @@ -115,8 +115,8 @@ public SecurityScanConfiguration DisablePassiveScanRule(int id, string name = "" /// given regular expression pattern. /// /// - /// A regular expression pattern to match URLs against. This should be a regex pattern that matches the whole - /// absolute URL, so something like ".*blog.*" to match /blog, /blog/my-post, etc. + /// The regex pattern to match URLs against. It will be matched against the whole absolute URL, e.g., ".*blog.*" + /// will match https://example.com/blog, https://example.com/blog/my-post, etc. /// /// The ID of the rule. In the scan report, this is usually displayed as "Plugin Id". /// From e122facf82bb23d5ca78259fbc45184e7430a099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 30 Nov 2023 19:43:09 +0100 Subject: [PATCH 116/129] Simplifying AF plan context URL management --- .../SecurityScanning/YamlDocumentExtensions.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs index 92e900a7c..1f42c5ac0 100644 --- a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs @@ -21,16 +21,7 @@ public static YamlDocument SetStartUrl(this YamlDocument yamlDocument, Uri start // scanned. var urls = yamlDocument.GetUrls(); - var urlsCount = urls.Count(); - - if (urlsCount > 1) - { - throw new ArgumentException( - "The context in the ZAP Automation Framework YAML file should contain at most a single URL in the \"urls\" section."); - } - - if (urlsCount == 1) urls.Children.Clear(); - + urls.Children.Clear(); urls.Add(startUri.ToString()); return yamlDocument; From 1e2993d4a6e10dceca1f8d449afb7185bd59dad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 30 Nov 2023 20:34:45 +0100 Subject: [PATCH 117/129] Cleaning up the ZAP container after completion --- Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs | 4 ++-- Lombiq.Tests.UI/SecurityScanning/ZapManager.cs | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 0bac66ab0..55811d4d8 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -58,7 +58,7 @@ public Task BasicSecurityScanShouldPass() => // shared, so such an explicit sign in is necessary. // - The assertion on the scan results is custom. Use this if you (conditionally) want to assert on the results // differently from the global context.Configuration.SecurityScanningConfiguration.AssertSecurityScanResult. The - // default there is "no scanning alert is allowed". + // default there is "no scanning alert is allowed"; we expect some alerts here. [Fact] public Task SecurityScanWithCustomConfigurationShouldPass() => ExecuteTestAfterSetupAsync( @@ -69,7 +69,7 @@ public Task SecurityScanWithCustomConfigurationShouldPass() => .DisablePassiveScanRule(10037, "Server Leaks Information via \"X-Powered-By\" HTTP Response Header Field(s)") .DisableScanRuleForUrlWithRegex(".*/about", 10038, "Content Security Policy (CSP) Header Not Set") .SignIn(), - sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeLessThan(200))); + sarifLog => sarifLog.Runs[0].Results.Count.ShouldBeLessThan(34))); // Let's get low-level into ZAP's configuration now. While the .NET configuration API of the Lombiq UI Testing // Toolbox covers the most important ways to configure ZAP, sometimes you need more. For this, you have complete diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 181c34710..addea5713 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -18,7 +18,8 @@ namespace Lombiq.Tests.UI.SecurityScanning; /// -/// Service to manage Zed Attack Proxy (ZAP) instances and security scans. +/// Service to manage Zed Attack Proxy (ZAP) instances and security scans +/// for a given test. /// public sealed class ZapManager : IAsyncDisposable { @@ -102,6 +103,8 @@ public async Task RunSecurityScanAsync( // https://localhost. See https://stackoverflow.com/a/24326540/220230. --network host serves the same, but // only works under Linux. See https://docs.docker.com/engine/reference/commandline/run/#network and // https://docs.docker.com/network/drivers/host/. + // - --rm: Removes the container after completion. Otherwise, unused containers would pile up in Docker. See + // https://docs.docker.com/engine/reference/run/#clean-up---rm for the official docs. // - --volume: Mounts the given host folder as a volume under the given container path. This is to pass files // back and forth between the host and the container. // - --tty: Allocates a pseudo-teletypewriter, i.e. redirects the output of ZAP to the CLI's output. @@ -129,6 +132,7 @@ public async Task RunSecurityScanAsync( cliParameters.AddRange(new object[] { + "--rm", "--volume", $"{mountedDirectoryPath}:{_zapWorkingDirectoryPath}:rw", "--tty", From 116bd83ffd26fb9ca4785014145d7677858e6e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 30 Nov 2023 20:35:48 +0100 Subject: [PATCH 118/129] Cleaning up the ZAP image too after completion --- .../SecurityScanning/ZapManager.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index addea5713..8a1150a14 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -183,7 +183,7 @@ public async Task RunSecurityScanAsync( return new SecurityScanResult(reportsDirectoryPath, SarifLog.Load(jsonReports[0])); } - public ValueTask DisposeAsync() + public async ValueTask DisposeAsync() { if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested) { @@ -191,7 +191,23 @@ public ValueTask DisposeAsync() _cancellationTokenSource.Dispose(); } - return ValueTask.CompletedTask; + try + { + using var cleanupCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + var cleanupCancellationToken = cleanupCancellationTokenSource.Token; + var imagesOutput = await _docker.ExecuteAndGetOutputAsync( + cleanupCancellationToken, "images", _zapImage, "--format", "{{.ID}}"); + + if (!string.IsNullOrEmpty(imagesOutput)) + { + await _docker.ExecuteAsync(cleanupCancellationToken, "image", "rm", _zapImage); + } + } + catch (Exception ex) + { + _testOutputHelper.WriteLineTimestampedAndDebug( + "Removing the Docker image {0} for ZAP failed with the following exception: {1}", _zapImage, ex); + } } private async Task EnsureInitializedAsync() From 462d7b13e8dca4d8d703d56333ae2717e968e6c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 30 Nov 2023 20:38:19 +0100 Subject: [PATCH 119/129] Revert "Cleaning up the ZAP image too after completion" This reverts commit 116bd83ffd26fb9ca4785014145d7677858e6e37. It'd actually delete the image after every single test, making a new pull necessary for every test, which is wasteful. We can't reliably run something only after all tests finished, so we can't do this properly. --- .../SecurityScanning/ZapManager.cs | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs index 8a1150a14..addea5713 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapManager.cs @@ -183,7 +183,7 @@ public async Task RunSecurityScanAsync( return new SecurityScanResult(reportsDirectoryPath, SarifLog.Load(jsonReports[0])); } - public async ValueTask DisposeAsync() + public ValueTask DisposeAsync() { if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested) { @@ -191,23 +191,7 @@ public async ValueTask DisposeAsync() _cancellationTokenSource.Dispose(); } - try - { - using var cleanupCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); - var cleanupCancellationToken = cleanupCancellationTokenSource.Token; - var imagesOutput = await _docker.ExecuteAndGetOutputAsync( - cleanupCancellationToken, "images", _zapImage, "--format", "{{.ID}}"); - - if (!string.IsNullOrEmpty(imagesOutput)) - { - await _docker.ExecuteAsync(cleanupCancellationToken, "image", "rm", _zapImage); - } - } - catch (Exception ex) - { - _testOutputHelper.WriteLineTimestampedAndDebug( - "Removing the Docker image {0} for ZAP failed with the following exception: {1}", _zapImage, ex); - } + return ValueTask.CompletedTask; } private async Task EnsureInitializedAsync() From f0706cfc95c8380a291881c3d48d0a6430aec9aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Sat, 2 Dec 2023 01:25:58 +0100 Subject: [PATCH 120/129] Adding shortcuts for configuring active scan rules --- .../SecurityScanConfiguration.cs | 34 +++++++++++++++++++ .../YamlDocumentExtensions.cs | 33 ++++++++++++++++-- Lombiq.Tests.UI/SecurityScanning/ZapEnums.cs | 27 +++++++++++++++ 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 Lombiq.Tests.UI/SecurityScanning/ZapEnums.cs diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs index 1f5455bb3..304e88ed2 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs @@ -28,6 +28,8 @@ public class SecurityScanConfiguration public string SignInUserName { get; private set; } public IList ExcludedUrlRegexPatterns { get; } = new List(); public IList DisabledActiveScanRules { get; } = new List(); + public IDictionary ConfiguredActiveScanRules { get; } = + new Dictionary(); public IList DisabledPassiveScanRules { get; } = new List(); public IDictionary DisabledRulesForUrls { get; } = new Dictionary(); public IList> ZapPlanModifiers { get; } = new List>(); @@ -95,6 +97,28 @@ public SecurityScanConfiguration DisableActiveScanRule(int id, string name = "") return this; } + /// + /// Configures a certain active scan rule for the whole scan. + /// + /// The ID of the rule. In the scan report, this is usually displayed as "Plugin Id". + /// + /// Controls how likely ZAP is to report potential vulnerabilities. See the official docs. + /// + /// + /// Controls the number of attacks that ZAP will perform. See the official docs. + /// + /// + /// The human-readable name of the rule. Not required to turn off the rule, and its value doesn't matter. It's just + /// useful for the readability of the method call. + /// + public SecurityScanConfiguration ConfigureActiveScanRule(int id, ScanRuleThreshold threshold, ScanRuleStrength strength, string name = "") + { + ConfiguredActiveScanRules.Add(new ScanRule(id, name), (threshold, strength)); + return this; + } + /// /// Disable a certain passive scan rule for the whole scan. If you only want to disable a rule for specific pages /// matched by a regex, use instead. @@ -197,6 +221,16 @@ internal async Task ApplyToPlanAsync(YamlDocument yamlDocument, UITestContext co yamlDocument.AddExcludePathsRegex(ExcludedUrlRegexPatterns.ToArray()); foreach (var rule in DisabledActiveScanRules) yamlDocument.DisableActiveScanRule(rule.Id, rule.Name); + + foreach (var ruleConfiguration in ConfiguredActiveScanRules) + { + yamlDocument.ConfigureActiveScanRule( + ruleConfiguration.Key.Id, + ruleConfiguration.Value.Threshold, + ruleConfiguration.Value.Strength, + ruleConfiguration.Key.Name); + } + foreach (var rule in DisabledPassiveScanRules) yamlDocument.DisablePassiveScanRule(rule.Id, rule.Name); foreach (var urlToRule in DisabledRulesForUrls) yamlDocument.AddAlertFilter(urlToRule.Key, urlToRule.Value.Id, urlToRule.Value.Name); foreach (var modifier in ZapPlanModifiers) await modifier(yamlDocument); diff --git a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs index 1f42c5ac0..adf969d44 100644 --- a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs @@ -167,7 +167,35 @@ public static YamlDocument DisablePassiveScanRule(this YamlDocument yamlDocument /// Thrown if no job with the type "activeScan" is found in the Automation Framework Plan, or if it doesn't have a /// policyDefinition property. /// - public static YamlDocument DisableActiveScanRule(this YamlDocument yamlDocument, int id, string name = "") + public static YamlDocument DisableActiveScanRule(this YamlDocument yamlDocument, int id, string name = "") => + yamlDocument.ConfigureActiveScanRule(id, ScanRuleThreshold.Off, ScanRuleStrength.Default, name); + + /// + /// Configures a certain ZAP active scan rule for the whole scan in the ZAP Automation Framework plan. + /// + /// The ID of the rule. In the scan report, this is usually displayed as "Plugin Id". + /// + /// Controls how likely ZAP is to report potential vulnerabilities. See the official docs. + /// + /// + /// Controls the number of attacks that ZAP will perform. See the official docs. + /// + /// + /// The human-readable name of the rule. Not required to configure the rule, and its value doesn't matter. It's just + /// useful for the readability of the method call. + /// + /// + /// Thrown if no job with the type "activeScan" is found in the Automation Framework Plan, or if it doesn't have a + /// policyDefinition property. + /// + public static YamlDocument ConfigureActiveScanRule( + this YamlDocument yamlDocument, + int id, + ScanRuleThreshold threshold, + ScanRuleStrength strength, + string name = "") { var activeScanConfigJob = (YamlMappingNode)yamlDocument.GetJobByType("activeScan") ?? @@ -187,7 +215,8 @@ public static YamlDocument DisableActiveScanRule(this YamlDocument yamlDocument, { { "id", id.ToTechnicalString() }, { "name", name }, - { "threshold", "off" }, + { "threshold", threshold.ToString() }, + { "strength", strength.ToString() }, }; ((YamlSequenceNode)policyDefinition["rules"]).Add(newRule); diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapEnums.cs b/Lombiq.Tests.UI/SecurityScanning/ZapEnums.cs new file mode 100644 index 000000000..862a29a53 --- /dev/null +++ b/Lombiq.Tests.UI/SecurityScanning/ZapEnums.cs @@ -0,0 +1,27 @@ +namespace Lombiq.Tests.UI.SecurityScanning; + +/// +/// Controls how likely ZAP is to report potential vulnerabilities. See the official docs. +/// +public enum ScanRuleThreshold +{ + Off, + Default, + Low, + Medium, + High, +} + +/// +/// Controls the number of attacks that ZAP will perform. See the official docs. +/// +public enum ScanRuleStrength +{ + Default, + Low, + Medium, + High, + Insane, +} From c97657a2d38f5630e56c43f831395722f1ca6067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Sat, 2 Dec 2023 01:31:51 +0100 Subject: [PATCH 121/129] AddRequestor() docs clarification --- Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs index adf969d44..7753cc22d 100644 --- a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs @@ -281,7 +281,7 @@ public static YamlDocument AddAlertFilter( } /// - /// Adds a "requestor" job to the ZAP Automation Framework plan. + /// Adds a "requestor" job to the ZAP Automation Framework plan just before the job named "spider". /// /// The URL the requestor job will access. /// From 631b50216b2b6b971b6579feef4d1f2e87e4ebd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Sat, 2 Dec 2023 01:32:10 +0100 Subject: [PATCH 122/129] Adding SecurityScanConfiguration.AddAdditionalUri() --- .../SecurityScanning/SecurityScanConfiguration.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs index 304e88ed2..29e39e14c 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs @@ -24,6 +24,7 @@ namespace Lombiq.Tests.UI.SecurityScanning; public class SecurityScanConfiguration { public Uri StartUri { get; private set; } + public IList AdditionalUris { get; } = new List(); public bool AjaxSpiderIsUsed { get; private set; } public string SignInUserName { get; private set; } public IList ExcludedUrlRegexPatterns { get; } = new List(); @@ -48,6 +49,17 @@ public SecurityScanConfiguration StartAtUri(Uri startUri) return this; } + /// + /// Adds an additional URL to visit during the scan. This is useful if you want to scan URLs that are otherwise + /// unreachable from . + /// + /// The under the app to also cover during the scan. + public SecurityScanConfiguration AddAdditionalUri(Uri additionalUri) + { + AdditionalUris.Add(additionalUri); + return this; + } + /// /// Enables the ZAP Ajax /// Spider. This is useful if you have an SPA; it unnecessarily slows down the scan otherwise. From 558b6ad3ab110cafe936ce8a655eed1e552525a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Sat, 2 Dec 2023 01:32:56 +0100 Subject: [PATCH 123/129] Docs on the "Cross Site Scripting (DOM Based)" active scan rule --- Lombiq.Tests.UI/Docs/SecurityScanning.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/Docs/SecurityScanning.md b/Lombiq.Tests.UI/Docs/SecurityScanning.md index 605219f8f..f8819315d 100644 --- a/Lombiq.Tests.UI/Docs/SecurityScanning.md +++ b/Lombiq.Tests.UI/Docs/SecurityScanning.md @@ -25,4 +25,6 @@ You can create detailed security scans of your app with [Zed Attack Proxy (ZAP)] - If an alert is a false positive, follow [the official docs](https://www.zaproxy.org/faq/how-do-i-handle-a-false-positive/). You can use the [`alertFilter` job](https://www.zaproxy.org/docs/desktop/addons/alert-filters/automation/) to ignore alerts in very specific conditions. You can also access this via the .NET configuration API. - ZAP didn't find everything in your app? By default, ZAP has a crawl depth of 5 for its standard spider and 10 for its AJAX spider. Set `maxDepth` (and `maxChildren`) [for `spider`](https://www.zaproxy.org/docs/desktop/addons/automation-framework/job-spider/) and `maxCrawlDepth` [for `spiderAjax`](https://www.zaproxy.org/docs/desktop/addons/ajax-spider/automation/). - Do you sometimes get slightly different scan results? This is normal, and ZAP can be inconsistent/appear random within limits, see [the official docs page](https://www.zaproxy.org/faq/why-can-zap-scans-be-inconsistent/). -- Is the active scan too slow? You can find out which rules take the most time by adding a script displaying each rules' runtime with `YamlDocumentExtensions.AddDisplayActiveScanRuleRuntimesScriptAfterActiveScan()`. +- Is the active scan too slow? + - You can find out which rules take the most time by adding a script displaying each rules' runtime with `YamlDocumentExtensions.AddDisplayActiveScanRuleRuntimesScriptAfterActiveScan()`. + - The ["Cross Site Scripting (DOM Based)" active scan rule](https://www.zaproxy.org/docs/desktop/addons/dom-xss-active-scan-rule/), unlike other rules, launches browsers and thus will take 1-2 orders of magnitude more time than other scans, usually causing the bulk of an active scan's runtime. Also see [the official docs](https://www.zaproxy.org/docs/desktop/addons/dom-xss-active-scan-rule/). You can tune it so it completes faster but still produces acceptable results for your app. You can do this from the Automation Framework plan's YAML file (see the samples on how you can use a custom one), or with `SecurityScanConfiguration.ConfigureActiveScanRule()`. From b39e37e981b84f1dc183940e2ef1405621aa72ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Sat, 2 Dec 2023 01:35:29 +0100 Subject: [PATCH 124/129] Fixing that AdditionalUris wasn't applied to the YAML --- Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs index 29e39e14c..4aad3938e 100644 --- a/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs +++ b/Lombiq.Tests.UI/SecurityScanning/SecurityScanConfiguration.cs @@ -203,6 +203,9 @@ public SecurityScanConfiguration ModifyZapPlan(Action modifyPlan) internal async Task ApplyToPlanAsync(YamlDocument yamlDocument, UITestContext context) { yamlDocument.SetStartUrl(StartUri); + + foreach (var uri in AdditionalUris) yamlDocument.AddUrl(uri); + if (AjaxSpiderIsUsed) yamlDocument.AddSpiderAjaxAfterSpider(); if (!string.IsNullOrEmpty(SignInUserName)) From 5f748052466b2f977a50853f9f0f73617f896d89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Sat, 2 Dec 2023 01:43:39 +0100 Subject: [PATCH 125/129] MD, C# linting fixes --- Lombiq.Tests.UI/Docs/SecurityScanning.md | 4 ++-- Lombiq.Tests.UI/SecurityScanning/ZapEnums.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lombiq.Tests.UI/Docs/SecurityScanning.md b/Lombiq.Tests.UI/Docs/SecurityScanning.md index f8819315d..c0cb81c38 100644 --- a/Lombiq.Tests.UI/Docs/SecurityScanning.md +++ b/Lombiq.Tests.UI/Docs/SecurityScanning.md @@ -26,5 +26,5 @@ You can create detailed security scans of your app with [Zed Attack Proxy (ZAP)] - ZAP didn't find everything in your app? By default, ZAP has a crawl depth of 5 for its standard spider and 10 for its AJAX spider. Set `maxDepth` (and `maxChildren`) [for `spider`](https://www.zaproxy.org/docs/desktop/addons/automation-framework/job-spider/) and `maxCrawlDepth` [for `spiderAjax`](https://www.zaproxy.org/docs/desktop/addons/ajax-spider/automation/). - Do you sometimes get slightly different scan results? This is normal, and ZAP can be inconsistent/appear random within limits, see [the official docs page](https://www.zaproxy.org/faq/why-can-zap-scans-be-inconsistent/). - Is the active scan too slow? - - You can find out which rules take the most time by adding a script displaying each rules' runtime with `YamlDocumentExtensions.AddDisplayActiveScanRuleRuntimesScriptAfterActiveScan()`. - - The ["Cross Site Scripting (DOM Based)" active scan rule](https://www.zaproxy.org/docs/desktop/addons/dom-xss-active-scan-rule/), unlike other rules, launches browsers and thus will take 1-2 orders of magnitude more time than other scans, usually causing the bulk of an active scan's runtime. Also see [the official docs](https://www.zaproxy.org/docs/desktop/addons/dom-xss-active-scan-rule/). You can tune it so it completes faster but still produces acceptable results for your app. You can do this from the Automation Framework plan's YAML file (see the samples on how you can use a custom one), or with `SecurityScanConfiguration.ConfigureActiveScanRule()`. + - You can find out which rules take the most time by adding a script displaying each rules' runtime with `YamlDocumentExtensions.AddDisplayActiveScanRuleRuntimesScriptAfterActiveScan()`. + - The ["Cross Site Scripting (DOM Based)" active scan rule](https://www.zaproxy.org/docs/desktop/addons/dom-xss-active-scan-rule/), unlike other rules, launches browsers and thus will take 1-2 orders of magnitude more time than other scans, usually causing the bulk of an active scan's runtime. Also see [the official docs](https://www.zaproxy.org/docs/desktop/addons/dom-xss-active-scan-rule/). You can tune it so it completes faster but still produces acceptable results for your app. You can do this from the Automation Framework plan's YAML file (see the samples on how you can use a custom one), or with `SecurityScanConfiguration.ConfigureActiveScanRule()`. diff --git a/Lombiq.Tests.UI/SecurityScanning/ZapEnums.cs b/Lombiq.Tests.UI/SecurityScanning/ZapEnums.cs index 862a29a53..d3f3612c7 100644 --- a/Lombiq.Tests.UI/SecurityScanning/ZapEnums.cs +++ b/Lombiq.Tests.UI/SecurityScanning/ZapEnums.cs @@ -15,7 +15,7 @@ public enum ScanRuleThreshold /// /// Controls the number of attacks that ZAP will perform. See the official docs. +/// href="https://www.zaproxy.org/docs/desktop/ui/dialogs/scanpolicy/#strength">the official docs. /// public enum ScanRuleStrength { From 61d16f8ffaa4959adf74c8f7896c80bc37cb3553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Sat, 2 Dec 2023 01:55:50 +0100 Subject: [PATCH 126/129] Fixing MD indentation again --- Lombiq.Tests.UI/Docs/SecurityScanning.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lombiq.Tests.UI/Docs/SecurityScanning.md b/Lombiq.Tests.UI/Docs/SecurityScanning.md index c0cb81c38..b26c1acf6 100644 --- a/Lombiq.Tests.UI/Docs/SecurityScanning.md +++ b/Lombiq.Tests.UI/Docs/SecurityScanning.md @@ -26,5 +26,5 @@ You can create detailed security scans of your app with [Zed Attack Proxy (ZAP)] - ZAP didn't find everything in your app? By default, ZAP has a crawl depth of 5 for its standard spider and 10 for its AJAX spider. Set `maxDepth` (and `maxChildren`) [for `spider`](https://www.zaproxy.org/docs/desktop/addons/automation-framework/job-spider/) and `maxCrawlDepth` [for `spiderAjax`](https://www.zaproxy.org/docs/desktop/addons/ajax-spider/automation/). - Do you sometimes get slightly different scan results? This is normal, and ZAP can be inconsistent/appear random within limits, see [the official docs page](https://www.zaproxy.org/faq/why-can-zap-scans-be-inconsistent/). - Is the active scan too slow? - - You can find out which rules take the most time by adding a script displaying each rules' runtime with `YamlDocumentExtensions.AddDisplayActiveScanRuleRuntimesScriptAfterActiveScan()`. - - The ["Cross Site Scripting (DOM Based)" active scan rule](https://www.zaproxy.org/docs/desktop/addons/dom-xss-active-scan-rule/), unlike other rules, launches browsers and thus will take 1-2 orders of magnitude more time than other scans, usually causing the bulk of an active scan's runtime. Also see [the official docs](https://www.zaproxy.org/docs/desktop/addons/dom-xss-active-scan-rule/). You can tune it so it completes faster but still produces acceptable results for your app. You can do this from the Automation Framework plan's YAML file (see the samples on how you can use a custom one), or with `SecurityScanConfiguration.ConfigureActiveScanRule()`. + - You can find out which rules take the most time by adding a script displaying each rules' runtime with `YamlDocumentExtensions.AddDisplayActiveScanRuleRuntimesScriptAfterActiveScan()`. + - The ["Cross Site Scripting (DOM Based)" active scan rule](https://www.zaproxy.org/docs/desktop/addons/dom-xss-active-scan-rule/), unlike other rules, launches browsers and thus will take 1-2 orders of magnitude more time than other scans, usually causing the bulk of an active scan's runtime. Also see [the official docs](https://www.zaproxy.org/docs/desktop/addons/dom-xss-active-scan-rule/). You can tune it so it completes faster but still produces acceptable results for your app. You can do this from the Automation Framework plan's YAML file (see the samples on how you can use a custom one), or with `SecurityScanConfiguration.ConfigureActiveScanRule()`. From b9fb086c7b1892d65e8a76655ce5245b3dfa08ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Mon, 4 Dec 2023 14:17:35 +0100 Subject: [PATCH 127/129] Code styling --- Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs index 55811d4d8..14dbbf4e4 100644 --- a/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/SecurityScanningTests.cs @@ -37,8 +37,7 @@ public SecurityScanningTests(ITestOutputHelper testOutputHelper) // will fail the scan, but don't worry! You'll get a nice report about the findings in the failure dump. [Fact] public Task BasicSecurityScanShouldPass() => - ExecuteTestAfterSetupAsync( - context => context.RunAndAssertBaselineSecurityScanAsync()); + ExecuteTestAfterSetupAsync(context => context.RunAndAssertBaselineSecurityScanAsync()); // Time for some custom configuration! While this scan also runs the Baseline scan, it does this with several // adjustments: From 72496e9f40e9cbeb19f6824c0daf8ce4e7ab5463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Mon, 4 Dec 2023 22:44:30 +0100 Subject: [PATCH 128/129] Removing outdated comment Co-authored-by: Benedek Farkas --- Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs index 7753cc22d..9966e63ad 100644 --- a/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs +++ b/Lombiq.Tests.UI/SecurityScanning/YamlDocumentExtensions.cs @@ -12,9 +12,6 @@ public static class YamlDocumentExtensions /// Framework plan. /// /// The absolute to start the scan from. - /// - /// Thrown when the ZAP Automation Framework plan contains more than a single URL in the "urls" section. - /// public static YamlDocument SetStartUrl(this YamlDocument yamlDocument, Uri startUri) { // Setting includePaths in the context is not necessary because by default everything under "urls" will be From ba9010a6dd57e460cdd105d9ea1f9274ec07c13b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Tue, 5 Dec 2023 17:34:19 +0100 Subject: [PATCH 129/129] Updating Lombiq.HelpfulLibraries package references --- Lombiq.Tests.UI.Shortcuts/Lombiq.Tests.UI.Shortcuts.csproj | 2 +- Lombiq.Tests.UI/Lombiq.Tests.UI.csproj | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lombiq.Tests.UI.Shortcuts/Lombiq.Tests.UI.Shortcuts.csproj b/Lombiq.Tests.UI.Shortcuts/Lombiq.Tests.UI.Shortcuts.csproj index d062c5111..cc4504bda 100644 --- a/Lombiq.Tests.UI.Shortcuts/Lombiq.Tests.UI.Shortcuts.csproj +++ b/Lombiq.Tests.UI.Shortcuts/Lombiq.Tests.UI.Shortcuts.csproj @@ -50,7 +50,7 @@ - + diff --git a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj index 8ae1cc497..28dae6c07 100644 --- a/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj +++ b/Lombiq.Tests.UI/Lombiq.Tests.UI.csproj @@ -103,9 +103,9 @@ - - - + + +