Skip to content

Commit

Permalink
feat(repeater): implement a repeater factory (#79)
Browse files Browse the repository at this point in the history
closes #78
  • Loading branch information
derevnjuk authored Dec 1, 2022
1 parent dabf2a5 commit d4d3d1a
Show file tree
Hide file tree
Showing 13 changed files with 411 additions and 13 deletions.
47 changes: 47 additions & 0 deletions src/SecTester.Repeater/DefaultRepeaterFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SecTester.Core;
using SecTester.Core.Utils;
using SecTester.Repeater.Api;
using SecTester.Repeater.Bus;

namespace SecTester.Repeater;

public class DefaultRepeaterFactory : RepeaterFactory
{
private readonly Configuration _configuration;
private readonly EventBusFactory _eventBusFactory;
private readonly ILogger _logger;
private readonly Repeaters _repeaters;
private readonly IServiceScopeFactory _serviceScopeFactory;

public DefaultRepeaterFactory(IServiceScopeFactory serviceScopeFactory, Repeaters repeaters, EventBusFactory eventBusFactory, Configuration configuration, ILogger logger)
{
_serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory));
_repeaters = repeaters ?? throw new ArgumentNullException(nameof(repeaters));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_eventBusFactory = eventBusFactory ?? throw new ArgumentNullException(nameof(eventBusFactory));
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}

public Task<Repeater> CreateRepeater()
{
return CreateRepeater(new RepeaterOptions());
}

public async Task<Repeater> CreateRepeater(RepeaterOptions options)
{
Version version = new(_configuration.Version);

string repeaterId = await _repeaters.CreateRepeater($"{options.NamePrefix}-{Guid.NewGuid()}", options.Description).ConfigureAwait(false);
var eventBus = await _eventBusFactory.Create(repeaterId).ConfigureAwait(false);

var scope = _serviceScopeFactory.CreateAsyncScope();
await using var _ = scope.ConfigureAwait(false);
var timerProvider = scope.ServiceProvider.GetRequiredService<TimerProvider>();

return new Repeater(repeaterId, eventBus, version, _logger, timerProvider);
}
}
25 changes: 25 additions & 0 deletions src/SecTester.Repeater/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using SecTester.Core.Utils;
using SecTester.Repeater.Api;

namespace SecTester.Repeater.Extensions;

public static class ServiceCollectionExtensions
{
public static IServiceCollection AddSecTesterRepeater(this IServiceCollection collection)
{
return AddSecTesterRepeater(collection, () => new RepeaterOptions());
}

public static IServiceCollection AddSecTesterRepeater(this IServiceCollection collection, Func<RepeaterOptions> configure)
{
collection
.AddScoped<RepeaterFactory, DefaultRepeaterFactory>()
.AddScoped(_ => configure())
.AddScoped<Repeaters, DefaultRepeaters>()
.AddScoped<TimerProvider, SystemTimerProvider>();

return collection;
}
}
117 changes: 117 additions & 0 deletions src/SecTester.Repeater/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,123 @@ More info about [repeaters](https://docs.brightsec.com/docs/on-premises-repeater
$ dotnet add package SecTester.Repeater
```

## Usage

To establish a secure connection between the Bright cloud engine and a target on a local network, you just need to use the `RepeaterFactory` constructed with [`Configuration` instance](https://github.com/NeuraLegion/sectester-js/tree/master/packages/core#configuration).

```csharp
var repeaterFactory = serviceProvider.GetService<RepeaterFactory>();
```

The factory exposes the `CreateRepeater` method that returns a new `Repeater` instance:

```csharp
await using var repeater = await repeaterFactory.CreateRepeater();
```

You can customize some properties, e.g. name prefix or description, passing options as follows:

```csharp
await using var repeater = await repeaterFactory.CreateRepeater(new RepeaterOptions {
NamePrefix = 'my-repeater',
Description = 'My repeater'
});
```

The `CreateRepeater` method accepts the options described below:

| Option | Description |
| :--------------------- | ----------------------------------------------------------------------------------------------------- |
| `namePrefix` | Enter a name prefix that will be used as a constant part of the unique name. By default, `sectester`. |
| `description` | Set a short description of the Repeater. |
| `requestRunnerOptions` | Custom the request runner settings that will be used to execute requests to your application. |

The default `requestRunnerOptions` is as follows:

```js
{
timeout: 30000,
maxContentLength: 100,
reuseConnection: false,
allowedMimes: [
'text/html',
'text/plain',
'text/css',
'text/javascript',
'text/markdown',
'text/xml',
'application/javascript',
'application/x-javascript',
'application/json',
'application/xml',
'application/x-www-form-urlencoded',
'application/msgpack',
'application/ld+json',
'application/graphql'
]
};
```

The `RequestRunnerOptions` exposes the following options that can used to customize the request runner's behavior: [RequestRunnerOptions.cs](https://github.com/NeuraLegion/sectester-net/blob/master/src/SecTester.Repeater/Runners/RequestRunnerOptions.cs)

The `Repeater` instance provides the `Start` method. This method is required to establish a connection with the Bright cloud engine and interact with other services.

```csharp
await repeater.Start();
```

To dispose of the connection, stop accepting any incoming commands, and handle events, you can call the `Stop` method if the `Repeater` instance is started:

```csharp
await repeater.Stop();
```

`Repeater` instance also has a `RepeaterId` field, that is required to start a new scan for local targets.

### Usage in unit tests

There are multiple strategies of how to run a repeater: before-all or before-each (recommended).
The two most viable options are running before all the tests vs running before every single test.

Below you can find the implementation of before-each strategy:

```csharp
public class ScanTests: IAsyncDisposable, IAsyncLifetime
{
// ...
private readonly Repeater _repeater;

public ScanTests()
{
// ...
var repeaterFactory = serviceProvider.GetService<RepeaterFactory>();
_repeater = repeaterFactory.CreateRepeater();
}

public async Task InitializeAsync()
{
await _repeater.Start();
}

public async ValueTask DisposeAsync()
{
await _repeater.DisposeAsync();

GC.SuppressFinalize(this);
}

[Fact]
public void BeNotVulnerable()
{
// run scan of local target passing `repeater.repeaterId` to scan config
}
}
```

## Limitations

Custom scripts and self-signed certificates (see [NexPloit CLI](https://www.npmjs.com/package/@neuralegion/nexploit-cli)) are not supported yet.

## License

Copyright © 2022 [Bright Security](https://brightsec.com/).
Expand Down
8 changes: 8 additions & 0 deletions src/SecTester.Repeater/RepeaterFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Threading.Tasks;

namespace SecTester.Repeater;

public interface RepeaterFactory
{
public Task<Repeater> CreateRepeater(RepeaterOptions options);
}
20 changes: 17 additions & 3 deletions src/SecTester.Repeater/RepeaterOptions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
using SecTester.Repeater.Runners;
using System;

namespace SecTester.Repeater;

public record RepeaterOptions
{
public string NamePrefix { get; init; } = "sectester";
private const int MaxPrefixLength = 44;
private readonly string _namePrefix = "sectester";

public string NamePrefix
{
get => _namePrefix;
init
{
if (string.IsNullOrEmpty(value) || value.Length > MaxPrefixLength)
{
throw new ArgumentOutOfRangeException($"Name prefix must be less than {MaxPrefixLength} characters.");
}
_namePrefix = value;
}
}

public string? Description { get; init; }
public RequestRunnerOptions? RequestRunnerOptions { get; init; }
}
18 changes: 18 additions & 0 deletions src/SecTester.Repeater/Runners/RequestRunnerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,24 @@ namespace SecTester.Repeater.Runners;

public record RequestRunnerOptions
{
/// <summary>
/// Time to wait for a server to send response headers (and start the response body) before aborting the request.
/// </summary>
public TimeSpan? Timeout { get; init; } = TimeSpan.FromSeconds(30);

/// <summary>
/// SOCKS4 or SOCKS5 URL to proxy all traffic.
/// </summary>
public Uri? ProxyUrl { get; init; }

/// <summary>
/// The headers, which is initially empty and consists of zero or more name and value pairs.
/// </summary>
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? Headers { get; init; }

/// <summary>
/// The list of allowed mimes, the HTTP response content will be truncated up <see cref="MaxContentLength"/> if its content type does not consist in the list.
/// </summary>
public IEnumerable<string>? AllowedMimes { get; init; } = new List<string>
{
"text/html",
Expand All @@ -29,7 +41,13 @@ public record RequestRunnerOptions
"application/graphql"
};

/// <summary>
/// The max size of the HTTP response content in bytes allowed for mimes different from <see cref="AllowedMimes"/>.
/// </summary>
public int? MaxContentLength { get; init; } = 1024;

/// <summary>
/// Configure experimental support for TCP connections reuse.
/// </summary>
public bool? ReuseConnection { get; init; } = false;
}
79 changes: 79 additions & 0 deletions test/SecTester.Repeater.Tests/DefaultRepeaterFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using SecTester.Repeater.Tests.Mocks;

namespace SecTester.Repeater.Tests;

public class DefaultRepeaterFactoryTests : IDisposable
{
private const string Id = "99138d92-69db-44cb-952a-1cd9ec031e20";
private const string DefaultNamePrefix = "sectester";
private const string Hostname = "app.neuralegion.com";

private readonly IServiceScopeFactory _serviceScopeFactory = Substitute.For<IServiceScopeFactory>();
private readonly EventBusFactory _eventBusFactory = Substitute.For<EventBusFactory>();
private readonly Configuration _configuration = new(Hostname);

private readonly Repeaters _repeaters = Substitute.For<Repeaters>();
private readonly MockLogger _logger = Substitute.For<MockLogger>();
private readonly TimerProvider _timerProvider = Substitute.For<TimerProvider>();
private readonly DefaultRepeaterFactory _sut;

public DefaultRepeaterFactoryTests()
{
// ADHOC: since GetRequiredService is part of extension we should explicitly mock an instance method
_serviceScopeFactory.CreateAsyncScope().ServiceProvider.GetService(typeof(TimerProvider)).Returns(_timerProvider);
_sut = new DefaultRepeaterFactory(_serviceScopeFactory, _repeaters, _eventBusFactory, _configuration, _logger);
}

public void Dispose()
{
_timerProvider.ClearSubstitute();
_serviceScopeFactory.ClearSubstitute();
_eventBusFactory.ClearSubstitute();
_repeaters.ClearSubstitute();
_logger.ClearSubstitute();
GC.SuppressFinalize(this);
}

[Fact]
public async Task CreateRepeater_CreatesRepeater()
{
// arrange
_repeaters.CreateRepeater(Arg.Is<string>(s => s.Contains(DefaultNamePrefix)))
.Returns(Id);

// act
await using var repeater = await _sut.CreateRepeater();

// assert
repeater.Should().BeOfType<Repeater>();
repeater.Should().BeEquivalentTo(
new
{
RepeaterId = Id
});
}

[Fact]
public async Task CreateRepeater_GivenOptions_CreatesRepeater()
{
// arrange
var options = new RepeaterOptions
{
NamePrefix = "foo",
Description = "bar"
};
_repeaters.CreateRepeater(Arg.Is<string>(s => s.Contains(options.NamePrefix)), options.Description)
.Returns(Id);

// act
await using var repeater = await _sut.CreateRepeater(options);

// assert
repeater.Should().BeOfType<Repeater>();
repeater.Should().BeEquivalentTo(
new
{
RepeaterId = Id
});
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using SecTester.Repeater.Extensions;

namespace SecTester.Repeater.Tests.Extensions;

public class SemaphoreSlimExtensionsTests
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
namespace SecTester.Repeater.Tests.Extensions;

public class ServiceCollectionExtensionsTests
{
private readonly IServiceCollection _sut = Substitute.ForPartsOf<ServiceCollection>();

[Fact]
public void AddSecTesterRepeater_RegistersRepeaterFactory()
{
// act
_sut.AddSecTesterRepeater();

// assert
_sut.Received().AddScoped<RepeaterFactory, DefaultRepeaterFactory>();
}

[Fact]
public void AddSecTesterRepeater_RegistersRepeaters()
{
// act
_sut.AddSecTesterRepeater();

// assert
_sut.Received().AddScoped<Repeaters, DefaultRepeaters>();
}

[Fact]
public void AddSecTesterRepeater_RegistersTimerProvider()
{
// act
_sut.AddSecTesterRepeater();

// assert
_sut.Received().AddScoped<TimerProvider, SystemTimerProvider>();
}
}
Loading

0 comments on commit d4d3d1a

Please sign in to comment.