< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.WebApi.Monitoring.SecurityMonitoringService
Assembly: ArturRios.Heimdall.WebApi
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Presentation/ArturRios.Heimdall.WebApi/Monitoring/SecurityMonitoringService.cs
Line coverage
23%
Covered lines: 4
Uncovered lines: 13
Coverable lines: 17
Total lines: 77
Line coverage: 23.5%
Branch coverage
0%
Covered branches: 0
Total branches: 12
Branch coverage: 0%
Method coverage
66%
Covered methods: 2
Fully covered methods: 2
Total methods: 3
Method coverage: 66.6%
Full method coverage: 66.6%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_StartupDescription()100%11100%
RunAsync()0%156120%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Presentation/ArturRios.Heimdall.WebApi/Monitoring/SecurityMonitoringService.cs

#LineLine coverage
 1using ArturRios.Heimdall.Query.Handlers;
 2using ArturRios.Heimdall.Query.Input;
 3using ArturRios.Heimdall.Query.Output;
 4using ArturRios.Heimdall.Shared.Retention;
 5using ArturRios.Mediator.Query;
 6using ArturRios.Heimdall.WebApi.Retention;
 7
 8namespace ArturRios.Heimdall.WebApi.Monitoring;
 9
 10/// <summary>
 11///     Reads the security signals the system already produces and writes the ones worth acting on
 12///     to the log, at warning level, with a stable marker an alerting rule can match (NFR-26).
 13/// </summary>
 14/// <remarks>
 15///     <para>
 16///         <b>The log is the destination, deliberately.</b> This API has no opinion about where an
 17///         operator's alerts go — a mail relay, a pager, a SIEM — and building an integration for
 18///         one would be guessing. What it can do is make the signal unmistakable and machine-matchable
 19///         where the operator is already collecting: the logs, which #98 gave a retention period and
 20///         which ship off the host.
 21///     </para>
 22///     <para>
 23///         <b>Every signal is logged on every run it persists.</b> There is no suppression or
 24///         deduplication, which would be the obvious refinement and is the wrong one here: an
 25///         attack that continues is more interesting on its tenth tick than its first, and a
 26///         detector that goes quiet while the thing it detects is still happening is worse than no
 27///         detector.
 28///     </para>
 29/// </remarks>
 30public class SecurityMonitoringService(
 31    IServiceScopeFactory scopeFactory,
 32    DataRetentionOptions options,
 33    ILogger<SecurityMonitoringService> logger)
 89934    : ScheduledPassService(scopeFactory, options.MonitoringWindow, logger)
 35{
 36    /// <summary>
 37    ///     The marker every signal line carries. Stable, greppable, and the thing an alerting rule
 38    ///     should match on rather than the prose, which is written for a human reading afterwards.
 39    /// </summary>
 40    public const string Marker = "SECURITY_SIGNAL";
 41
 42    protected override string StartupDescription =>
 89943        $"Security signal detection scheduled every {options.MonitoringWindow}, alerting at "
 89944        + $"{options.RefusalThreshold} refusals by one identity or {options.LockoutThreshold} "
 89945        + "accounts locked out at once";
 46
 47    /// <remarks>
 48    ///     Resolves the query mediator rather than the command one every other pass uses: this pass
 49    ///     reads, and making it a command would put an audit entry on every tick saying "looked, saw
 50    ///     nothing", burying the writes NFR-09 exists to record.
 51    /// </remarks>
 52    protected override async Task<string?> RunAsync(IServiceProvider scope)
 53    {
 054        var result = await scope.GetRequiredService<QueryMediator>()
 055            .ExecuteQueryAsync<DetectSecuritySignalsQuery, SecuritySignalsOutput>(
 056                new DetectSecuritySignalsQuery());
 57
 058        if (!result.Success)
 59        {
 060            return $"Security signal detection failed: {string.Join("; ", result.Errors)}";
 61        }
 62
 063        var signals = result.Data?.Signals.ToList() ?? [];
 64
 065        foreach (var signal in signals)
 66        {
 067            logger.LogWarning(
 068                "{Marker} {Kind}: {Description} (count {Count}, actor {ActorId})",
 069                Marker, signal.Kind, signal.Description, signal.Count,
 070                signal.ActorId?.ToString() ?? "n/a");
 71        }
 72
 73        // Nothing extra for the caller to log: each signal has already been written above, and a
 74        // run that found nothing is not news.
 075        return null;
 076    }
 77}