From 6c21728a5ada558d55eb37eab0070515ac9f48dd Mon Sep 17 00:00:00 2001 From: ostridm Date: Fri, 2 Dec 2022 11:26:59 +0300 Subject: [PATCH] feat(scan): provide a default scan factory closes #68 --- src/SecTester.Scan/DefaultScanFactory.cs | 95 ++++++ src/SecTester.Scan/Models/ScanConfig.cs | 2 +- src/SecTester.Scan/Scan.cs | 9 +- src/SecTester.Scan/ScanDefaults.cs | 38 +++ src/SecTester.Scan/ScanFactory.cs | 2 +- src/SecTester.Scan/ScanSettings.cs | 173 +++++++++++ src/SecTester.Scan/Target/Har/Cache.cs | 6 + src/SecTester.Scan/Target/Har/Content.cs | 6 + src/SecTester.Scan/Target/Har/Creator.cs | 3 + src/SecTester.Scan/Target/Har/Entry.cs | 6 + src/SecTester.Scan/Target/Har/Har.cs | 2 +- src/SecTester.Scan/Target/Har/HarDefaults.cs | 6 + src/SecTester.Scan/Target/Har/Log.cs | 5 + src/SecTester.Scan/Target/Har/Response.cs | 10 + src/SecTester.Scan/Target/Har/Timings.cs | 6 + .../Commands/CreateScanTests.cs | 58 ++-- .../Commands/UploadHarTests.cs | 27 +- .../DefaultScanFactoryTests.cs | 101 +++++++ .../SecTester.Scan.Tests/DefaultScansTests.cs | 117 +++++--- .../Extensions/HttpContentExtensions.cs | 11 + .../Fixtures/ScanFixture.cs | 55 ---- test/SecTester.Scan.Tests/Mocks/MockLogger.cs | 19 ++ .../SecTester.Scan.Tests/Models/IssueTests.cs | 1 + .../Models/UploadHarOptionsTests.cs | 22 +- .../SecTester.Scan.Tests/ScanSettingsTests.cs | 273 ++++++++++++++++++ test/SecTester.Scan.Tests/ScanTests.cs | 46 +-- .../SecTester.Scan.Tests.csproj | 2 - test/SecTester.Scan.Tests/Usings.cs | 7 + 28 files changed, 942 insertions(+), 166 deletions(-) create mode 100644 src/SecTester.Scan/DefaultScanFactory.cs create mode 100644 src/SecTester.Scan/ScanDefaults.cs create mode 100644 src/SecTester.Scan/ScanSettings.cs create mode 100644 src/SecTester.Scan/Target/Har/Cache.cs create mode 100644 src/SecTester.Scan/Target/Har/Content.cs create mode 100644 src/SecTester.Scan/Target/Har/Creator.cs create mode 100644 src/SecTester.Scan/Target/Har/Entry.cs create mode 100644 src/SecTester.Scan/Target/Har/HarDefaults.cs create mode 100644 src/SecTester.Scan/Target/Har/Log.cs create mode 100644 src/SecTester.Scan/Target/Har/Response.cs create mode 100644 src/SecTester.Scan/Target/Har/Timings.cs create mode 100644 test/SecTester.Scan.Tests/DefaultScanFactoryTests.cs create mode 100644 test/SecTester.Scan.Tests/Extensions/HttpContentExtensions.cs delete mode 100644 test/SecTester.Scan.Tests/Fixtures/ScanFixture.cs create mode 100644 test/SecTester.Scan.Tests/Mocks/MockLogger.cs create mode 100644 test/SecTester.Scan.Tests/ScanSettingsTests.cs diff --git a/src/SecTester.Scan/DefaultScanFactory.cs b/src/SecTester.Scan/DefaultScanFactory.cs new file mode 100644 index 0000000..5e473ca --- /dev/null +++ b/src/SecTester.Scan/DefaultScanFactory.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using SecTester.Core; +using SecTester.Core.Utils; +using SecTester.Scan.Models; +using SecTester.Scan.Target.Har; +using Response = SecTester.Scan.Target.Har.Response; + +namespace SecTester.Scan; + +public class DefaultScanFactory : ScanFactory +{ + private static readonly IEnumerable DefaultDiscoveryTypes = new List { Discovery.Archive }; + private readonly Scans _scans; + private readonly ILogger _logger; + private readonly Configuration _configuration; + private readonly SystemTimeProvider _systemTimeProvider; + + public DefaultScanFactory(Configuration configuration, Scans scans, SystemTimeProvider systemTimeProvider, + ILogger logger) + { + _scans = scans ?? throw new ArgumentNullException(nameof(scans)); + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + _systemTimeProvider = systemTimeProvider ?? throw new ArgumentNullException(nameof(systemTimeProvider)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task CreateScan(ScanSettingsOptions settingsOptions, ScanOptions? options) + { + var scanConfig = await BuildScanConfig(new ScanSettings(settingsOptions)).ConfigureAwait(false); + var scanId = await _scans.CreateScan(scanConfig).ConfigureAwait(false); + + return new Scan(scanId, _scans, _logger, options ?? new ScanOptions()); + } + + private async Task BuildScanConfig(ScanSettingsOptions settingsOptions) + { + var target = new Target.Target(settingsOptions.Target); + var fileId = await CreateAndUploadHar(target).ConfigureAwait(false); + + return new ScanConfig(settingsOptions.Name!) + { + FileId = fileId, + Smart = settingsOptions.Smart, + PoolSize = settingsOptions.PoolSize, + SkipStaticParams = settingsOptions.SkipStaticParams, + Module = Module.Dast, + DiscoveryTypes = DefaultDiscoveryTypes, + AttackParamLocations = settingsOptions.AttackParamLocations, + Tests = settingsOptions.Tests, + Repeaters = settingsOptions.RepeaterId is null ? default : new List { settingsOptions.RepeaterId }, + SlowEpTimeout = + settingsOptions.SlowEpTimeout is null ? default : (int)settingsOptions.SlowEpTimeout.Value.TotalSeconds, + TargetTimeout = + settingsOptions.TargetTimeout is null ? default : (int)settingsOptions.TargetTimeout.Value.TotalSeconds + }; + } + + private async Task CreateAndUploadHar(Target.Target target) + { + var filename = GenerateFileName(target.Url); + var har = await CreateHar(target).ConfigureAwait(false); + + return await _scans.UploadHar(new UploadHarOptions(har, filename, true)).ConfigureAwait(false); + } + + private static string GenerateFileName(string url) + { + var host = new Uri(url).Host; + + host = host.Length <= HarDefaults.MaxHostLength ? host : host.Substring(0, HarDefaults.MaxHostLength); + + return $"{host.TrimEnd('-')}-{Guid.NewGuid().ToString()}.har"; + } + + private async Task CreateHarEntry(Target.Target target) + { + var request = await target.ToHarRequest().ConfigureAwait(false); + return new Entry(_systemTimeProvider.Now, 0, Timings.Default, Cache.Default, request, Response.Default); + } + + private async Task CreateHar(Target.Target target) + { + var entry = await CreateHarEntry(target).ConfigureAwait(false); + + return new Har( + new Log( + new Creator(_configuration.Name, _configuration.Version), + new List { entry } + ) + ); + } +} diff --git a/src/SecTester.Scan/Models/ScanConfig.cs b/src/SecTester.Scan/Models/ScanConfig.cs index 5b96e27..22d10f2 100644 --- a/src/SecTester.Scan/Models/ScanConfig.cs +++ b/src/SecTester.Scan/Models/ScanConfig.cs @@ -5,7 +5,7 @@ namespace SecTester.Scan.Models; public record ScanConfig(string Name) { - public string Name { get; init; } = Name ?? throw new ArgumentNullException(nameof(Name)); + public string Name { get; } = Name ?? throw new ArgumentNullException(nameof(Name)); public Module? Module { get; init; } public IEnumerable? Tests { get; init; } public IEnumerable? DiscoveryTypes { get; init; } diff --git a/src/SecTester.Scan/Scan.cs b/src/SecTester.Scan/Scan.cs index b170b2c..9117210 100644 --- a/src/SecTester.Scan/Scan.cs +++ b/src/SecTester.Scan/Scan.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using SecTester.Scan.Models; namespace SecTester.Scan; @@ -22,14 +23,18 @@ public class Scan : IDisposable }; private readonly ScanOptions _options; - + private readonly ILogger _logger; private readonly Scans _scans; private readonly ScanState _scanState = new(ScanStatus.Pending); - public Scan(Scans scans, ScanOptions options) + public string Id { get; } + + public Scan(string id, Scans scans, ILogger logger, ScanOptions options) { + Id = id ?? throw new ArgumentNullException(nameof(id)); _scans = scans ?? throw new ArgumentNullException(nameof(scans)); _options = options ?? throw new ArgumentNullException(nameof(options)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public bool Active => ActiveStatuses.Contains(_scanState.Status); diff --git a/src/SecTester.Scan/ScanDefaults.cs b/src/SecTester.Scan/ScanDefaults.cs new file mode 100644 index 0000000..93ee298 --- /dev/null +++ b/src/SecTester.Scan/ScanDefaults.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SecTester.Scan.Models; + +namespace SecTester.Scan; + +internal static class ScanDefaults +{ + public const int MinNameLength = 1; + public const int MaxNameLength = 200; + public const int MinPoolSize = 1; + public const int MaxPoolSize = 50; + + public static readonly TimeSpan MinTargetTimeout = TimeSpan.FromSeconds(1); + public static readonly TimeSpan MaxTargetTimeout = TimeSpan.FromSeconds(120); + public static readonly TimeSpan MinSlowEpTimeout = TimeSpan.FromSeconds(100); + + public const bool DefaultSmart = true; + public const bool DefaultSkipStaticParams = true; + public const int DefaultPoolSize = 10; + + public static readonly TimeSpan DefaultTargetTimeout = TimeSpan.FromSeconds(5); + public static readonly TimeSpan DefaultSlowEpTimeout = TimeSpan.FromSeconds(1000); + + public static readonly IEnumerable DefaultAttackParamLocations = new List + { + AttackParamLocation.Body, AttackParamLocation.Query, AttackParamLocation.Fragment + }; + + public static readonly IEnumerable TestTypeWhiteList = Enum + .GetValues(typeof(TestType)) + .Cast(); + + public static readonly IEnumerable AttackParamLocationWhiteList = Enum + .GetValues(typeof(AttackParamLocation)) + .Cast(); +} diff --git a/src/SecTester.Scan/ScanFactory.cs b/src/SecTester.Scan/ScanFactory.cs index 6ae642e..18bfce1 100644 --- a/src/SecTester.Scan/ScanFactory.cs +++ b/src/SecTester.Scan/ScanFactory.cs @@ -4,5 +4,5 @@ namespace SecTester.Scan; public interface ScanFactory { - Task CreateScan(ScanSettingsOptions settingsOptions, ScanOptions? options); + Task CreateScan(ScanSettingsOptions settingsOptions, ScanOptions? options = default); } diff --git a/src/SecTester.Scan/ScanSettings.cs b/src/SecTester.Scan/ScanSettings.cs new file mode 100644 index 0000000..af19f85 --- /dev/null +++ b/src/SecTester.Scan/ScanSettings.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using SecTester.Scan.Models; +using SecTester.Scan.Target; + +namespace SecTester.Scan; + +public record ScanSettings : ScanSettingsOptions +{ + private Target.Target _target = null!; + private IEnumerable _tests = null!; + + private string? _name; + private TimeSpan? _targetTimeout; + private TimeSpan? _slowEpTimeout; + private int? _poolSize; + private IEnumerable? _attackParamLocations; + + public TargetOptions Target + { + get { return _target; } + set + { + _target = new Target.Target(value); + } + } + + public string? Name + { + get { return _name; } + set + { + if (string.IsNullOrWhiteSpace(value) || + value!.Length is < ScanDefaults.MinNameLength or > ScanDefaults.MaxNameLength) + { + throw new ArgumentException($"Name must be less than {ScanDefaults.MaxNameLength} characters."); + } + + _name = value; + } + } + + public string? RepeaterId { get; set; } + + public bool? Smart { get; set; } + + public int? PoolSize + { + get { return _poolSize; } + set + { + if (value is null or < ScanDefaults.MinPoolSize or > ScanDefaults.MaxPoolSize) + { + throw new ArgumentException("Invalid pool size."); + } + + _poolSize = value; + } + } + + public TimeSpan? SlowEpTimeout + { + get { return _slowEpTimeout; } + set + { + if (value is null || value < ScanDefaults.MinSlowEpTimeout) + { + throw new ArgumentException("Invalid slow entry point timeout."); + } + + _slowEpTimeout = value; + } + } + + public TimeSpan? TargetTimeout + { + get { return _targetTimeout; } + set + { + if (value is null || (value < ScanDefaults.MinTargetTimeout || value > ScanDefaults.MaxTargetTimeout)) + { + throw new ArgumentException("Invalid target connection timeout."); + } + + _targetTimeout = value; + } + } + + public bool? SkipStaticParams { get; set; } + + public IEnumerable Tests + { + get { return _tests; } + set + { + Assert(value, ScanDefaults.TestTypeWhiteList, + "Unknown test type supplied.", + "Please provide at least one test."); + + _tests = value.Distinct(); + } + } + + public IEnumerable? AttackParamLocations + { + get { return _attackParamLocations; } + set + { + Assert(value, ScanDefaults.AttackParamLocationWhiteList, + "Unknown attack param location supplied.", + "Please provide at least one attack parameter location."); + + _attackParamLocations = value!.Distinct(); + } + } + + public ScanSettings(TargetOptions targetOptions, IEnumerable tests, string? repeaterId = default, + string? name = default) + : this(targetOptions, tests, repeaterId, name, ScanDefaults.DefaultSmart, ScanDefaults.DefaultPoolSize, + ScanDefaults.DefaultTargetTimeout, + ScanDefaults.DefaultSlowEpTimeout, ScanDefaults.DefaultSkipStaticParams, ScanDefaults.DefaultAttackParamLocations) + { + } + + internal ScanSettings(ScanSettingsOptions scanSettingsOptions) + : this(scanSettingsOptions.Target, scanSettingsOptions.Tests, scanSettingsOptions.RepeaterId, + scanSettingsOptions.Name, scanSettingsOptions.Smart, scanSettingsOptions.PoolSize, + scanSettingsOptions.TargetTimeout, scanSettingsOptions.SlowEpTimeout, scanSettingsOptions.SkipStaticParams, + scanSettingsOptions.AttackParamLocations) + { + } + + private ScanSettings(TargetOptions targetOptions, IEnumerable tests, string? repeaterId, + string? name, + bool? smart, int? poolSize, TimeSpan? targetTimeout, + TimeSpan? slowEpTimeout, bool? skipStaticParams, + IEnumerable? attackParamLocations) + { + Target = targetOptions; + Tests = tests; + Name = name ?? CreateDefaultName(targetOptions); + RepeaterId = repeaterId; + Smart = smart ?? ScanDefaults.DefaultSmart; + PoolSize = poolSize ?? ScanDefaults.DefaultPoolSize; + TargetTimeout = targetTimeout ?? ScanDefaults.DefaultTargetTimeout; + SlowEpTimeout = slowEpTimeout ?? ScanDefaults.DefaultSlowEpTimeout; + SkipStaticParams = skipStaticParams ?? ScanDefaults.DefaultSkipStaticParams; + AttackParamLocations = attackParamLocations ?? ScanDefaults.DefaultAttackParamLocations; + } + + private static string CreateDefaultName(TargetOptions target) + { + var uri = new Uri(target.Url); + var name = $"{target.Method ?? HttpMethod.Get} {uri.Host}"; + + return name.Length <= ScanDefaults.MaxNameLength ? name : name.Substring(0, ScanDefaults.MaxNameLength); + } + + private static void Assert(IEnumerable? value, IEnumerable whiteList, + string unknownEntry, string emptyList) + { + if (value is null || !value.All(whiteList.Contains)) + { + throw new ArgumentException(unknownEntry); + } + if (!value.Distinct().Any()) + { + throw new ArgumentException(emptyList); + } + } +} diff --git a/src/SecTester.Scan/Target/Har/Cache.cs b/src/SecTester.Scan/Target/Har/Cache.cs new file mode 100644 index 0000000..72eb307 --- /dev/null +++ b/src/SecTester.Scan/Target/Har/Cache.cs @@ -0,0 +1,6 @@ +namespace SecTester.Scan.Target.Har; + +public record Cache() +{ + public static readonly Cache Default = new(); +}; diff --git a/src/SecTester.Scan/Target/Har/Content.cs b/src/SecTester.Scan/Target/Har/Content.cs new file mode 100644 index 0000000..8fd199a --- /dev/null +++ b/src/SecTester.Scan/Target/Har/Content.cs @@ -0,0 +1,6 @@ +namespace SecTester.Scan.Target.Har; + +public record Content(int Size, string MimeType) +{ + public static readonly Content Default = new(-1, "text/plain"); +} diff --git a/src/SecTester.Scan/Target/Har/Creator.cs b/src/SecTester.Scan/Target/Har/Creator.cs new file mode 100644 index 0000000..8c639b0 --- /dev/null +++ b/src/SecTester.Scan/Target/Har/Creator.cs @@ -0,0 +1,3 @@ +namespace SecTester.Scan.Target.Har; + +public record Creator(string Name, string Version); diff --git a/src/SecTester.Scan/Target/Har/Entry.cs b/src/SecTester.Scan/Target/Har/Entry.cs new file mode 100644 index 0000000..1297509 --- /dev/null +++ b/src/SecTester.Scan/Target/Har/Entry.cs @@ -0,0 +1,6 @@ +using System; + +namespace SecTester.Scan.Target.Har; + +public record Entry(DateTime StartedDateTime, int Time, Timings Timings, Cache Cache, Request Request, + Response Response); diff --git a/src/SecTester.Scan/Target/Har/Har.cs b/src/SecTester.Scan/Target/Har/Har.cs index 1475d08..a810777 100644 --- a/src/SecTester.Scan/Target/Har/Har.cs +++ b/src/SecTester.Scan/Target/Har/Har.cs @@ -1,3 +1,3 @@ namespace SecTester.Scan.Target.Har; -public record Har; +public record Har(Log Log); diff --git a/src/SecTester.Scan/Target/Har/HarDefaults.cs b/src/SecTester.Scan/Target/Har/HarDefaults.cs new file mode 100644 index 0000000..e0a627e --- /dev/null +++ b/src/SecTester.Scan/Target/Har/HarDefaults.cs @@ -0,0 +1,6 @@ +namespace SecTester.Scan.Target.Har; + +public static class HarDefaults +{ + public const int MaxHostLength = 200; +} diff --git a/src/SecTester.Scan/Target/Har/Log.cs b/src/SecTester.Scan/Target/Har/Log.cs new file mode 100644 index 0000000..3495102 --- /dev/null +++ b/src/SecTester.Scan/Target/Har/Log.cs @@ -0,0 +1,5 @@ +using System.Collections.Generic; + +namespace SecTester.Scan.Target.Har; + +public record Log(Creator Creator, IEnumerable Entries, string Version = "1.2"); diff --git a/src/SecTester.Scan/Target/Har/Response.cs b/src/SecTester.Scan/Target/Har/Response.cs new file mode 100644 index 0000000..352f358 --- /dev/null +++ b/src/SecTester.Scan/Target/Har/Response.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace SecTester.Scan.Target.Har; + +public record Response(int Status, string StatusText, string HttpVersion, IEnumerable Cookies, + IEnumerable
Headers, string RedirectUrl, int HeaderSize, int BodySize, Content Content) +{ + public static readonly Response Default = new(200, "OK", "HTTP/1.1", new List(), + new List
(), "", -1, -1, Content.Default); +} diff --git a/src/SecTester.Scan/Target/Har/Timings.cs b/src/SecTester.Scan/Target/Har/Timings.cs new file mode 100644 index 0000000..17ec6f3 --- /dev/null +++ b/src/SecTester.Scan/Target/Har/Timings.cs @@ -0,0 +1,6 @@ +namespace SecTester.Scan.Target.Har; + +public record Timings(int Wait, int Receive, int? Send) +{ + public static readonly Timings Default = new(0, 0, 0); +} diff --git a/test/SecTester.Scan.Tests/Commands/CreateScanTests.cs b/test/SecTester.Scan.Tests/Commands/CreateScanTests.cs index 273d2ab..dee9ae6 100644 --- a/test/SecTester.Scan.Tests/Commands/CreateScanTests.cs +++ b/test/SecTester.Scan.Tests/Commands/CreateScanTests.cs @@ -1,29 +1,49 @@ -using SecTester.Scan.Tests.Fixtures; - namespace SecTester.Scan.Tests.Commands; -public class CreateScanTests : ScanFixture +public class CreateScanTests { + private const string ScanName = "Scan Name"; + private const string ProjectId = "e9a2eX46EkidKhn3uqdYvE"; + private const string RepeaterId = "g5MvgM74sweGcK1U6hvs76"; + private const string FileId = "6aJa25Yd8DdXEcZg3QFoi8"; + + private readonly ScanConfig _scanConfig = new(ScanName) + { + Module = Module.Dast, + Repeaters = new[] { RepeaterId }, + Smart = true, + Tests = new[] { TestType.Csrf, TestType.Jwt }, + DiscoveryTypes = new[] { Discovery.Crawler }, + FileId = FileId, + HostsFilter = new[] { "example.com" }, + PoolSize = 2, + ProjectId = ProjectId, + TargetTimeout = 10, + AttackParamLocations = new[] { AttackParamLocation.Body, AttackParamLocation.Header }, + SkipStaticParams = true, + SlowEpTimeout = 20 + }; + [Fact] public void Constructor_ConstructsInstance() { // arrange var expectedPayload = new { - ScanConfig.Name, - ScanConfig.Module, - ScanConfig.Tests, - ScanConfig.DiscoveryTypes, - ScanConfig.PoolSize, - ScanConfig.AttackParamLocations, - ScanConfig.FileId, - ScanConfig.HostsFilter, - ScanConfig.Repeaters, - ScanConfig.Smart, - ScanConfig.SkipStaticParams, - ScanConfig.ProjectId, - ScanConfig.SlowEpTimeout, - ScanConfig.TargetTimeout, + _scanConfig.Name, + _scanConfig.Module, + _scanConfig.Tests, + _scanConfig.DiscoveryTypes, + _scanConfig.PoolSize, + _scanConfig.AttackParamLocations, + _scanConfig.FileId, + _scanConfig.HostsFilter, + _scanConfig.Repeaters, + _scanConfig.Smart, + _scanConfig.SkipStaticParams, + _scanConfig.ProjectId, + _scanConfig.SlowEpTimeout, + _scanConfig.TargetTimeout, Info = new { Source = "utlib", @@ -33,7 +53,7 @@ public void Constructor_ConstructsInstance() }; // act - var command = new CreateScan(ScanConfig, "Configuration Name", "Configuration Version", "Some CI"); + var command = new CreateScan(_scanConfig, "Configuration Name", "Configuration Version", "Some CI"); // assert command.Should() @@ -48,7 +68,7 @@ public void Constructor_ConstructsInstance() }, config => config.IncludingNestedObjects() .Using(ctx => { - ReadHttpContentAsString(ctx.Subject).Should().Be(ReadHttpContentAsString(ctx.Expectation)); + ctx.Subject.ReadHttpContentAsString().Should().Be(ctx.Expectation.ReadHttpContentAsString()); ctx.Subject.Headers.ContentType.Should().Be(ctx.Expectation.Headers.ContentType); }) .When(info => info.Path.EndsWith(nameof(CreateScan.Body))) diff --git a/test/SecTester.Scan.Tests/Commands/UploadHarTests.cs b/test/SecTester.Scan.Tests/Commands/UploadHarTests.cs index e0d6717..bbcc643 100644 --- a/test/SecTester.Scan.Tests/Commands/UploadHarTests.cs +++ b/test/SecTester.Scan.Tests/Commands/UploadHarTests.cs @@ -1,21 +1,28 @@ -using SecTester.Scan.Tests.Fixtures; - namespace SecTester.Scan.Tests.Commands; -public class UploadHarTests : ScanFixture +public class UploadHarTests { + private const string HarFileName = "filename.har"; + + private static readonly Har Har = new( + new Log( + new Creator("Configuration_Name", "Configuration_Version"), + new List() + ) + ); + [Fact] public void Constructor_ConstructsInstance() { // arrange - var options = new UploadHarOptions(new Har(), "filename.har"); + var options = new UploadHarOptions(Har, HarFileName); var expectedContent = new MultipartFormDataContent { { new StringContent(MessageSerializer.Serialize(options.Har), Encoding.UTF8, "application/json"), "file", - "filename.har" + HarFileName } }; @@ -34,11 +41,11 @@ public void Constructor_ConstructsInstance() }, config => config.IncludingNestedObjects() .Using(ctx => { - ReadHttpContentAsString(ctx.Subject.First()).Should() - .BeEquivalentTo(ReadHttpContentAsString(ctx.Expectation.First())); + ctx.Subject.First().ReadHttpContentAsString().Should() + .Be(ctx.Expectation.First().ReadHttpContentAsString()); ctx.Subject.First().Headers.ContentType.Should() - .BeEquivalentTo(ctx.Expectation.First().Headers.ContentType); - ctx.Subject.Headers.ContentDisposition.Should().BeEquivalentTo(ctx.Expectation.Headers.ContentDisposition); + .Be(ctx.Expectation.First().Headers.ContentType); + ctx.Subject.Headers.ContentDisposition.Should().Be(ctx.Expectation.Headers.ContentDisposition); }) .When(info => info.Path.EndsWith(nameof(UploadHar.Body))) ); @@ -48,7 +55,7 @@ public void Constructor_ConstructsInstance() public void Constructor_DiscardIsTrue_ConstructsInstance() { // arrange - var options = new UploadHarOptions(new Har(), "filename.har", true); + var options = new UploadHarOptions(Har, HarFileName, true); // act var command = new UploadHar(options); diff --git a/test/SecTester.Scan.Tests/DefaultScanFactoryTests.cs b/test/SecTester.Scan.Tests/DefaultScanFactoryTests.cs new file mode 100644 index 0000000..1104a79 --- /dev/null +++ b/test/SecTester.Scan.Tests/DefaultScanFactoryTests.cs @@ -0,0 +1,101 @@ +using System.Text.RegularExpressions; + +namespace SecTester.Scan.Tests; + +public class DefaultScanFactoryTests : IDisposable +{ + private const string FileId = "6aJa25Yd8DdXEcZg3QFoi8"; + private const string ScanId = "roMq1UVuhPKkndLERNKnA8"; + + private readonly Configuration _configuration = new("app.neuralegion.com"); + private readonly ScanSettingsOptions _options = Substitute.For(); + private readonly Scans _scans = Substitute.For(); + private readonly MockLogger _logger = Substitute.For(); + private readonly SystemTimeProvider _systemTimeProvider = Substitute.For(); + private readonly ScanFactory _sut; + + public DefaultScanFactoryTests() + { + _sut = new DefaultScanFactory(_configuration, _scans, _systemTimeProvider, _logger); + } + + public void Dispose() + { + _options.ClearSubstitute(); + _scans.ClearSubstitute(); + _logger.ClearSubstitute(); + _systemTimeProvider.ClearSubstitute(); + GC.SuppressFinalize(this); + } + + [Fact] + public async Task CreateScan_CreatesScan() + { + // arrange + _options.Name.ReturnsForAnyArgs(null as string); + _options.AttackParamLocations.ReturnsForAnyArgs(null as IEnumerable); + _options.Target.Returns(new SecTester.Scan.Target.Target("https://example.com")); + _options.Tests.Returns(new List { TestType.DomXss }); + + _scans.UploadHar(Arg.Any()).Returns(FileId); + _scans.CreateScan(Arg.Any()).Returns(ScanId); + + // act + var result = await _sut.CreateScan(_options); + + // assert + result.Id.Should().Be(ScanId); + await _scans.Received(1).CreateScan(Arg.Is(x => + x.Name == "GET example.com" && + x.FileId == FileId && + x.Module == Module.Dast && + x.Tests.Contains(TestType.DomXss) && x.Tests.Count() == 1 && + x.DiscoveryTypes.Contains(Discovery.Archive) && x.DiscoveryTypes.Count() == 1 + )); + } + + [Fact] + public async Task CreateScan_GeneratesUploadHarFile() + { + // arrange + _options.Name.ReturnsForAnyArgs(null as string); + _options.AttackParamLocations.ReturnsForAnyArgs(null as IEnumerable); + _options.Target.Returns(new SecTester.Scan.Target.Target("https://example.com")); + _options.Tests.Returns(new List { TestType.DomXss }); + + _scans.UploadHar(Arg.Any()).Returns(FileId); + _scans.CreateScan(Arg.Any()).Returns(ScanId); + + // act + await _sut.CreateScan(_options); + + // assert + await _scans.Received(1).UploadHar(Arg.Is(x => + x.Discard && Regex.IsMatch(x.FileName, @"^example\.com-[a-z\d-]+\.har$") && + x.Har.Log.Creator.Version == _configuration.Version && + x.Har.Log.Creator.Name == _configuration.Name && + x.Har.Log.Version == "1.2" + )); + } + + [Fact] + public async Task CreateScan_TruncatesHarFilename() + { + // arrange + _options.Name.ReturnsForAnyArgs(null as string); + _options.AttackParamLocations.ReturnsForAnyArgs(null as IEnumerable); + _options.Target.Returns(new SecTester.Scan.Target.Target($"https://{new string('a', 201)}.example.com")); + _options.Tests.Returns(new List { TestType.DomXss }); + + _scans.UploadHar(Arg.Any()).Returns(FileId); + _scans.CreateScan(Arg.Any()).Returns(ScanId); + + // act + await _sut.CreateScan(_options); + + // assert + await _scans.Received(1).UploadHar(Arg.Is(x => + Regex.IsMatch(x.FileName, @"^.{200}-[a-z\d-]+\.har$") + )); + } +} diff --git a/test/SecTester.Scan.Tests/DefaultScansTests.cs b/test/SecTester.Scan.Tests/DefaultScansTests.cs index 723b175..073f4da 100644 --- a/test/SecTester.Scan.Tests/DefaultScansTests.cs +++ b/test/SecTester.Scan.Tests/DefaultScansTests.cs @@ -1,32 +1,90 @@ -using SecTester.Scan.Tests.Fixtures; using Request = SecTester.Scan.Models.Request; namespace SecTester.Scan.Tests; -public class DefaultScansTests : ScanFixture +public class DefaultScansTests : IDisposable { private const string NullResultMessage = "Something went wrong. Please try again later."; + private const string BaseUrl = "https://example.com/api/v1"; + private const string ScanName = "Scan Name"; + private const string ProjectId = "e9a2eX46EkidKhn3uqdYvE"; + private const string RepeaterId = "g5MvgM74sweGcK1U6hvs76"; + private const string FileId = "6aJa25Yd8DdXEcZg3QFoi8"; + + private const string ScanId = "roMq1UVuhPKkndLERNKnA8"; + private const string IssueId = "pDzxcEXQC8df1fcz1QwPf9"; + private const string HarId = "gwycPnxzQihoeGP141pvDe"; + private const string HarFileName = "filename.har"; + + private readonly ScanConfig _scanConfig = new(ScanName) + { + Module = Module.Dast, + Repeaters = new[] { RepeaterId }, + Smart = true, + Tests = new[] { TestType.Csrf, TestType.Jwt }, + DiscoveryTypes = new[] { Discovery.Crawler }, + FileId = FileId, + HostsFilter = new[] { "example.com" }, + PoolSize = 2, + ProjectId = ProjectId, + TargetTimeout = 10, + AttackParamLocations = new[] { AttackParamLocation.Body, AttackParamLocation.Header }, + SkipStaticParams = true, + SlowEpTimeout = 20 + }; + + private readonly Issue _issue = new(IssueId, + "Cross-site request forgery is a type of malicious website exploit.", + "Database connection crashed", + "The best way to protect against those kind of issues is making sure the Database resources are sufficient", + new Request("https://brokencrystals.com/") { Method = HttpMethod.Get }, + new Request("https://brokencrystals.com/") { Method = HttpMethod.Get }, + $"{BaseUrl}/scans/{ScanId}/issues/{IssueId}", + 1, + Severity.Medium, + Protocol.Http, + DateTime.UtcNow) + { Cvss = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L" }; + + private readonly Configuration _configuration = new("app.neuralegion.com"); + private readonly CommandDispatcher _commandDispatcher = Substitute.For(); + private readonly CiDiscovery _ciDiscovery = Substitute.For(); + + private static readonly Har Har = new( + new Log( + new Creator("Configuration_Name", "Configuration_Version"), + new List() + ) + ); private readonly Scans _sut; public DefaultScansTests() { - _sut = new DefaultScans(Configuration, CommandDispatcher, CiDiscovery); + _sut = new DefaultScans(_configuration, _commandDispatcher, _ciDiscovery); + } + + public void Dispose() + { + _ciDiscovery.ClearSubstitute(); + _commandDispatcher.ClearSubstitute(); + + GC.SuppressFinalize(this); } [Fact] public async Task CreateScan_CreatesNewScan() { // arrange - CommandDispatcher.Execute(Arg.Any()) + _commandDispatcher.Execute(Arg.Any()) .Returns(Task.FromResult?>(new Identifiable(ScanId))); // act - var result = await _sut.CreateScan(ScanConfig); + var result = await _sut.CreateScan(_scanConfig); // assert result.Should().Be(ScanId); - await CommandDispatcher.Received(1) + await _commandDispatcher.Received(1) .Execute(Arg.Any()); } @@ -34,11 +92,11 @@ await CommandDispatcher.Received(1) public async Task CreateScan_ResultIsNull_ThrowError() { // arrange - CommandDispatcher.Execute(Arg.Any()) + _commandDispatcher.Execute(Arg.Any()) .Returns(Task.FromResult?>(null)); // act - var act = () => _sut.CreateScan(ScanConfig); + var act = () => _sut.CreateScan(_scanConfig); // assert await act.Should().ThrowAsync().WithMessage(NullResultMessage); @@ -48,22 +106,9 @@ public async Task CreateScan_ResultIsNull_ThrowError() public async Task ListIssues_ReturnListOfIssues() { // arrange - var issues = new List - { - new(IssueId, - "Cross-site request forgery is a type of malicious website exploit.", - "Database connection crashed", - "The best way to protect against those kind of issues is making sure the Database resources are sufficient", - new Request("https://brokencrystals.com/") { Method = HttpMethod.Get }, - new Request("https://brokencrystals.com/") { Method = HttpMethod.Get }, - $"{Configuration.Api}/scans/{ScanId}/issues/{IssueId}", - 1, - Severity.Medium, - Protocol.Http, - DateTime.UtcNow) { Cvss = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L" } - }; - - CommandDispatcher.Execute(Arg.Any()) + var issues = new List { _issue }; + + _commandDispatcher.Execute(Arg.Any()) .Returns(Task.FromResult?>(issues)); // act @@ -71,7 +116,7 @@ public async Task ListIssues_ReturnListOfIssues() // assert result.Should().BeEquivalentTo(issues); - await CommandDispatcher.Received(1) + await _commandDispatcher.Received(1) .Execute(Arg.Any()); } @@ -79,7 +124,7 @@ await CommandDispatcher.Received(1) public async Task ListIssues_ResultIsNull_ThrowError() { // arrange - CommandDispatcher.Execute(Arg.Any()) + _commandDispatcher.Execute(Arg.Any()) .Returns(Task.FromResult?>(null)); // act @@ -96,7 +141,7 @@ public async Task StopScan_StopsScan() await _sut.StopScan(ScanId); // assert - await CommandDispatcher.Received(1) + await _commandDispatcher.Received(1) .Execute(Arg.Any()); } @@ -107,7 +152,7 @@ public async Task DeleteScan_DeletesScan() await _sut.DeleteScan(ScanId); // assert - await CommandDispatcher.Received(1) + await _commandDispatcher.Received(1) .Execute(Arg.Any()); } @@ -117,7 +162,7 @@ public async Task GetScan_ReturnsScanState() // arrange var scanState = new ScanState(ScanStatus.Done); - CommandDispatcher.Execute(Arg.Any()) + _commandDispatcher.Execute(Arg.Any()) .Returns(Task.FromResult(scanState)); // act @@ -125,7 +170,7 @@ public async Task GetScan_ReturnsScanState() // assert result.Should().Be(scanState); - await CommandDispatcher.Received(1) + await _commandDispatcher.Received(1) .Execute(Arg.Any()); } @@ -133,7 +178,7 @@ await CommandDispatcher.Received(1) public async Task GetScan_ResultIsNull_ThrowError() { // arrange - CommandDispatcher.Execute(Arg.Any()) + _commandDispatcher.Execute(Arg.Any()) .Returns(Task.FromResult(null)); // act @@ -147,9 +192,9 @@ public async Task GetScan_ResultIsNull_ThrowError() public async Task UploadHar_CreatesNewHar() { // arrange - var options = new UploadHarOptions(new Har(), "filename.har"); + var options = new UploadHarOptions(Har, HarFileName); - CommandDispatcher.Execute(Arg.Any()) + _commandDispatcher.Execute(Arg.Any()) .Returns(Task.FromResult?>(new Identifiable(HarId))); // act @@ -157,7 +202,7 @@ public async Task UploadHar_CreatesNewHar() // assert result.Should().Be(HarId); - await CommandDispatcher.Received(1) + await _commandDispatcher.Received(1) .Execute(Arg.Any()); } @@ -165,9 +210,9 @@ await CommandDispatcher.Received(1) public async Task UploadHar_ResultIsNull_ThrowError() { // arrange - var options = new UploadHarOptions(new Har(), "filename.har"); + var options = new UploadHarOptions(Har, HarFileName); - CommandDispatcher.Execute(Arg.Any()) + _commandDispatcher.Execute(Arg.Any()) .Returns(Task.FromResult?>(null)); // act diff --git a/test/SecTester.Scan.Tests/Extensions/HttpContentExtensions.cs b/test/SecTester.Scan.Tests/Extensions/HttpContentExtensions.cs new file mode 100644 index 0000000..5719a77 --- /dev/null +++ b/test/SecTester.Scan.Tests/Extensions/HttpContentExtensions.cs @@ -0,0 +1,11 @@ +namespace SecTester.Scan.Tests.Extensions; + +internal static class HttpContentExtensions +{ + public static string? ReadHttpContentAsString(this HttpContent? content) + { + return content is null + ? default + : Task.Run(content.ReadAsStringAsync).GetAwaiter().GetResult(); + } +} diff --git a/test/SecTester.Scan.Tests/Fixtures/ScanFixture.cs b/test/SecTester.Scan.Tests/Fixtures/ScanFixture.cs deleted file mode 100644 index f2ac22e..0000000 --- a/test/SecTester.Scan.Tests/Fixtures/ScanFixture.cs +++ /dev/null @@ -1,55 +0,0 @@ -using NSubstitute.ClearExtensions; -using SecTester.Core; -using SecTester.Core.Bus; -using SecTester.Scan.CI; -using SecTester.Scan.Models; - -namespace SecTester.Scan.Tests.Fixtures; - -public class ScanFixture : IDisposable -{ - private const string ScanName = "Scan Name"; - private const string ProjectId = "e9a2eX46EkidKhn3uqdYvE"; - private const string RepeaterId = "g5MvgM74sweGcK1U6hvs76"; - private const string FileId = "6aJa25Yd8DdXEcZg3QFoi8"; - - protected const string ScanId = "roMq1UVuhPKkndLERNKnA8"; - protected const string IssueId = "pDzxcEXQC8df1fcz1QwPf9"; - protected const string HarId = "gwycPnxzQihoeGP141pvDe"; - - protected readonly ScanConfig ScanConfig = new(ScanName) - { - Module = Module.Dast, - Repeaters = new[] { RepeaterId }, - Smart = true, - Tests = new[] { TestType.Csrf, TestType.Jwt }, - DiscoveryTypes = new[] { Discovery.Crawler }, - FileId = FileId, - HostsFilter = new[] { "example.com" }, - PoolSize = 2, - ProjectId = ProjectId, - TargetTimeout = 10, - AttackParamLocations = new[] { AttackParamLocation.Body, AttackParamLocation.Header }, - SkipStaticParams = true, - SlowEpTimeout = 20 - }; - - protected readonly Configuration Configuration = new("app.neuralegion.com"); - protected readonly CommandDispatcher CommandDispatcher = Substitute.For(); - protected readonly CiDiscovery CiDiscovery = Substitute.For(); - - public void Dispose() - { - CiDiscovery.ClearSubstitute(); - CommandDispatcher.ClearSubstitute(); - - GC.SuppressFinalize(this); - } - - protected static string? ReadHttpContentAsString(HttpContent? content) - { - return content is null - ? default - : Task.Run(content.ReadAsStringAsync).GetAwaiter().GetResult(); - } -} diff --git a/test/SecTester.Scan.Tests/Mocks/MockLogger.cs b/test/SecTester.Scan.Tests/Mocks/MockLogger.cs new file mode 100644 index 0000000..96df4e8 --- /dev/null +++ b/test/SecTester.Scan.Tests/Mocks/MockLogger.cs @@ -0,0 +1,19 @@ +namespace SecTester.Scan.Tests.Mocks; + +internal abstract class MockLogger : ILogger +{ + void ILogger.Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, + Func formatter) + { + Log(logLevel, formatter(state, exception)); + } + + public virtual bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public abstract IDisposable BeginScope(TState state); + + public abstract void Log(LogLevel logLevel, string message); +} diff --git a/test/SecTester.Scan.Tests/Models/IssueTests.cs b/test/SecTester.Scan.Tests/Models/IssueTests.cs index 73c6e44..37fdbd2 100644 --- a/test/SecTester.Scan.Tests/Models/IssueTests.cs +++ b/test/SecTester.Scan.Tests/Models/IssueTests.cs @@ -1,4 +1,5 @@ using Request = SecTester.Scan.Models.Request; +using Response = SecTester.Scan.Models.Response; namespace SecTester.Scan.Tests.Models; diff --git a/test/SecTester.Scan.Tests/Models/UploadHarOptionsTests.cs b/test/SecTester.Scan.Tests/Models/UploadHarOptionsTests.cs index d6be351..d2cab86 100644 --- a/test/SecTester.Scan.Tests/Models/UploadHarOptionsTests.cs +++ b/test/SecTester.Scan.Tests/Models/UploadHarOptionsTests.cs @@ -1,21 +1,27 @@ namespace SecTester.Scan.Tests.Models; -public class UploadHarContentOptionsTests +public class UploadHarOptionsTests { - private const string FileName = "file.har"; - private readonly Har _har = new(); + private const string HarFileName = "file.har"; + + private static readonly Har Har = new Har( + new Log( + new Creator("Configuration_Name", "Configuration_Version"), + new List() + ) + ); [Fact] public void Constructor_WithAllParameters_AssignProperties() { // act - var options = new UploadHarOptions(_har, FileName, true); + var options = new UploadHarOptions(Har, HarFileName, true); // assert options.Should().BeEquivalentTo(new { - Har = _har, - FileName, + FileName = HarFileName, + Har, Discard = true }); } @@ -24,7 +30,7 @@ public void Constructor_WithAllParameters_AssignProperties() public void Constructor_GivenNullHarContent_ThrowError() { // act - var act = () => new UploadHarOptions(null!, FileName); + var act = () => new UploadHarOptions(null!, HarFileName); // assert act.Should().Throw().WithMessage("*Har*"); @@ -34,7 +40,7 @@ public void Constructor_GivenNullHarContent_ThrowError() public void Constructor_GivenNullFileName_ThrowError() { // act - var act = () => new UploadHarOptions(_har, null!); + var act = () => new UploadHarOptions(Har, null!); // assert act.Should().Throw().WithMessage("*Filename*"); diff --git a/test/SecTester.Scan.Tests/ScanSettingsTests.cs b/test/SecTester.Scan.Tests/ScanSettingsTests.cs new file mode 100644 index 0000000..439965a --- /dev/null +++ b/test/SecTester.Scan.Tests/ScanSettingsTests.cs @@ -0,0 +1,273 @@ +namespace SecTester.Scan.Tests; + +public class ScanSettingsTests : IDisposable +{ + private const string ScanName = "Scan Name"; + private const string RepeaterId = "g5MvgM74sweGcK1U6hvs76"; + private const string TargetUrl = "https://example.com/api/v1/info"; + + private readonly TargetOptions _targetOptions = Substitute.For(); + private readonly ScanSettingsOptions _scanSettingsOptions = Substitute.For(); + + public ScanSettingsTests() + { + _targetOptions.Url.Returns(TargetUrl); + _targetOptions.Body.Returns(new StringContent("{}", Encoding.UTF8, "application/json")); + _targetOptions.Headers.Returns(new List>>()); + _targetOptions.Method.Returns(HttpMethod.Get); + _targetOptions.Query.Returns(new List>()); + + _scanSettingsOptions.Name.Returns(ScanName); + _scanSettingsOptions.Smart.Returns(false); + _scanSettingsOptions.Target.Returns(_targetOptions); + _scanSettingsOptions.RepeaterId.Returns(RepeaterId); + _scanSettingsOptions.PoolSize.Returns(1); + _scanSettingsOptions.TargetTimeout.Returns(TimeSpan.FromSeconds(5)); + _scanSettingsOptions.SlowEpTimeout.Returns(TimeSpan.FromSeconds(200)); + _scanSettingsOptions.SkipStaticParams.Returns(false); + _scanSettingsOptions.Tests.Returns(new List { TestType.Csrf }); + _scanSettingsOptions.AttackParamLocations.Returns(new List { AttackParamLocation.Query }); + } + + public void Dispose() + { + _targetOptions.ClearSubstitute(); + _scanSettingsOptions.ClearSubstitute(); + } + + public static IEnumerable SetterInvalidInput() + { + yield return new object[] + { + "Unknown attack param location supplied.", (ScanSettings x) => + { + x.AttackParamLocations = new List { (AttackParamLocation)1024 }; + } + }; + yield return new object[] + { + "Please provide at least one attack parameter location.", (ScanSettings x) => + { + x.AttackParamLocations = new List(); + } + }; + yield return new object[] + { + "Unknown test type supplied.", (ScanSettings x) => + { + x.Tests = new List { (TestType)1024 }; + } + }; + yield return new object[] + { + "Please provide at least one test.", (ScanSettings x) => + { + x.Tests = new List(); + } + }; + yield return new object[] + { + "Invalid target connection timeout.", (ScanSettings x) => + { + x.TargetTimeout = null; + } + }; + yield return new object[] + { + "Invalid target connection timeout.", (ScanSettings x) => + { + x.TargetTimeout = TimeSpan.FromSeconds(0); + } + }; + yield return new object[] + { + "Invalid target connection timeout.", (ScanSettings x) => + { + x.TargetTimeout = TimeSpan.FromSeconds(121); + } + }; + yield return new object[] + { + "Invalid slow entry point timeout.", (ScanSettings x) => + { + x.SlowEpTimeout = null; + } + }; + yield return new object[] + { + "Invalid slow entry point timeout.", (ScanSettings x) => + { + x.SlowEpTimeout = TimeSpan.FromSeconds(99); + } + }; + yield return new object[] + { + "Invalid pool size.", (ScanSettings x) => + { + x.PoolSize = null; + } + }; + yield return new object[] + { + "Invalid pool size.", (ScanSettings x) => + { + x.PoolSize = 0; + } + }; + yield return new object[] + { + "Invalid pool size.", (ScanSettings x) => + { + x.PoolSize = 51; + } + }; + yield return new object[] + { + "Name must be less than 200 characters.", (ScanSettings x) => + { + x.Name = null; + } + }; + yield return new object[] + { + "Name must be less than 200 characters.", (ScanSettings x) => + { + x.Name = " "; + } + }; + yield return new object[] + { + "Name must be less than 200 characters.", (ScanSettings x) => + { + x.Name = new string('a', 201); + } + }; + } + + [Fact] + public void PublicConstructor_CreatesInstanceWithDefaultOptions() + { + // act + var result = new ScanSettings(_targetOptions, new List { TestType.Csrf }); + + // assert + result.Should().BeEquivalentTo(new + { + RepeaterId = null as string, + Name = "GET example.com", + PoolSize = 10, + TargetTimeout = TimeSpan.FromSeconds(5), + SlowEpTimeout = TimeSpan.FromSeconds(1000), + Smart = true, + SkipStaticParams = true, + Tests = new List { TestType.Csrf }, + AttackParamLocations = new List + { + AttackParamLocation.Body, AttackParamLocation.Query, AttackParamLocation.Fragment + }, + Target = new + { + Url = TargetUrl + } + }); + } + + [Fact] + public void InternalConstructor_CreatesInstance() + { + // act + var result = new ScanSettings(_scanSettingsOptions); + + // assert + result.Should().BeEquivalentTo(new + { + RepeaterId, + Name = ScanName, + PoolSize = 1, + TargetTimeout = TimeSpan.FromSeconds(5), + SlowEpTimeout = TimeSpan.FromSeconds(200), + Smart = false, + SkipStaticParams = false, + Tests = new List { TestType.Csrf }, + AttackParamLocations = new List { AttackParamLocation.Query }, + Target = new + { + Url = TargetUrl + } + }); + } + + [Fact] + public void InternalConstructor_NameIsNull_CreatesInstanceWithDefaultName() + { + // arrange + _scanSettingsOptions.Name.ReturnsForAnyArgs(_ => null!); + + // act + var result = new ScanSettings(_scanSettingsOptions); + + // assert + result.Should().BeEquivalentTo(new { Name = "GET example.com" }); + } + + [Fact] + public void InternalConstructor_NameIsNull_CreatesInstanceTruncatedHost() + { + // arrange + _scanSettingsOptions.Name.ReturnsForAnyArgs(_ => null); + _scanSettingsOptions.Target.Url.ReturnsForAnyArgs($"https://{new string('a', 200)}.example.com/api/v1/info"); + + // act + var result = new ScanSettings(_scanSettingsOptions); + + // assert + result.Should().BeEquivalentTo(new { Name = $"GET {new string('a', 196)}" }); + } + + [Fact] + public void InternalConstructor_CreatesInstanceWithUniqueAttackParamLocations() + { + // arrange + _scanSettingsOptions.AttackParamLocations.ReturnsForAnyArgs( + new List { AttackParamLocation.Header, AttackParamLocation.Header }); + + // act + var result = new ScanSettings(_scanSettingsOptions); + + // assert + result.Should().BeEquivalentTo(new + { + AttackParamLocations = new List { AttackParamLocation.Header } + }); + } + + [Fact] + public void InternalConstructor_CreatesInstanceWithUniqueTests() + { + // arrange + _scanSettingsOptions.Tests.ReturnsForAnyArgs( + new List { TestType.Csrf, TestType.Csrf, TestType.Hrs }); + + // act + var result = new ScanSettings(_scanSettingsOptions); + + // assert + result.Should().BeEquivalentTo(new { Tests = new List { TestType.Csrf, TestType.Hrs } }); + } + + + [Theory] + [MemberData(nameof(SetterInvalidInput))] + public void Setter_GivenInvalidInput_ThrowError(string expectedMessage, + Action action) + { + // arrange + var sut = new ScanSettings(_scanSettingsOptions); + + // act + var act = () => action(sut); + + // assert + act.Should().Throw().WithMessage($"*{expectedMessage}*"); + } +} diff --git a/test/SecTester.Scan.Tests/ScanTests.cs b/test/SecTester.Scan.Tests/ScanTests.cs index fab7316..971d58b 100644 --- a/test/SecTester.Scan.Tests/ScanTests.cs +++ b/test/SecTester.Scan.Tests/ScanTests.cs @@ -1,53 +1,37 @@ namespace SecTester.Scan.Tests; -public class ScanTests +public class ScanTests : IDisposable { - [Fact] - public void Constructor_GivenNullScans_ThrowError() - { - // act - var act = () => new Scan(null!, new ScanOptions()); + private const string ScanId = "roMq1UVuhPKkndLERNKnA8"; - // assert - act.Should().Throw().WithMessage("*scans*"); - } + private readonly Scans _scans = Substitute.For(); + private readonly MockLogger _logger = Substitute.For(); + private readonly Scan _sut; - [Fact] - public void Constructor_GivenNullScanOptions_ThrowError() + public ScanTests() { - // arrange - var scans = Substitute.For(); + _sut = new Scan(ScanId, _scans, _logger, new ScanOptions(TimeSpan.Zero, PollingInterval: TimeSpan.Zero)); + } - // act - var act = () => new Scan(scans, null!); + public void Dispose() + { + _scans.ClearSubstitute(); + _logger.ClearSubstitute(); - // assert - act.Should().Throw().WithMessage("*options*"); + GC.SuppressFinalize(this); } [Fact] public void Active_DefaultValue_ReturnTrue() { - // arrange - var scans = Substitute.For(); - - // act - using var scan = new Scan(scans, new ScanOptions()); - // assert - scan.Active.Should().BeTrue(); + _sut.Active.Should().BeTrue(); } [Fact] public void Done_DefaultValue_ReturnTrue() { - // arrange - var scans = Substitute.For(); - - // act - using var scan = new Scan(scans, new ScanOptions()); - // assert - scan.Done.Should().BeFalse(); + _sut.Done.Should().BeFalse(); } } diff --git a/test/SecTester.Scan.Tests/SecTester.Scan.Tests.csproj b/test/SecTester.Scan.Tests/SecTester.Scan.Tests.csproj index ab2cdd3..876df70 100644 --- a/test/SecTester.Scan.Tests/SecTester.Scan.Tests.csproj +++ b/test/SecTester.Scan.Tests/SecTester.Scan.Tests.csproj @@ -16,9 +16,7 @@ - - diff --git a/test/SecTester.Scan.Tests/Usings.cs b/test/SecTester.Scan.Tests/Usings.cs index 700fd63..1c6ad66 100644 --- a/test/SecTester.Scan.Tests/Usings.cs +++ b/test/SecTester.Scan.Tests/Usings.cs @@ -3,14 +3,21 @@ global using System.Text; global using FluentAssertions; global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; global using NSubstitute; +global using NSubstitute.ClearExtensions; global using SecTester.Bus.Dispatchers; global using SecTester.Bus.Extensions; global using SecTester.Core; +global using SecTester.Core.Bus; global using SecTester.Core.Exceptions; +global using SecTester.Core.Utils; global using SecTester.Scan.CI; global using SecTester.Scan.Commands; global using SecTester.Scan.Extensions; global using SecTester.Scan.Models; +global using SecTester.Scan.Target; global using SecTester.Scan.Target.Har; +global using SecTester.Scan.Tests.Extensions; +global using SecTester.Scan.Tests.Mocks; global using Xunit;