< Summary

Information
Class: ProjectTemplate.Web.Extensions.StartupSecurityPostureExtensions
Assembly: ProjectTemplate.Web
File(s): /home/runner/work/NetCoreApplicationTemplate/NetCoreApplicationTemplate/src/ProjectTemplate.Web/Extensions/StartupSecurityPostureExtensions.cs
Line coverage
100%
Covered lines: 80
Uncovered lines: 0
Coverable lines: 80
Total lines: 161
Line coverage: 100%
Branch coverage
95%
Covered branches: 21
Total branches: 22
Branch coverage: 95.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
LogApplicationSecurityPosture(...)100%11100%
LogApplicationSecurityPosture(...)100%66100%
LogHostFilteringPosture(...)100%66100%
LogDataProtectionPosture(...)90%1010100%

File(s)

/home/runner/work/NetCoreApplicationTemplate/NetCoreApplicationTemplate/src/ProjectTemplate.Web/Extensions/StartupSecurityPostureExtensions.cs

#LineLine coverage
 1using ProjectTemplate.Web.Authentication.Options;
 2using ProjectTemplate.Web.Options;
 3
 4namespace ProjectTemplate.Web.Extensions;
 5
 6/// <summary>
 7/// Provides startup-only diagnostics for security-relevant supported deployment postures.
 8/// </summary>
 9public static class StartupSecurityPostureExtensions
 10{
 11    private const string _authenticationEnabledConfigurationKey =
 12        ApplicationAuthenticationOptions.SectionName + ":Enabled";
 13
 14    private const string _anonymousHealthEndpoints =
 15        "/health, /health/ready, /health/live";
 16
 17    private const string _dataProtectionKeyRingPathConfigurationKey =
 18        ApplicationDataProtectionOptions.SectionName + ":" + nameof(ApplicationDataProtectionOptions.KeyRingPath);
 19
 20    private const string _dataProtectionKeyEncryptionCertificatePathConfigurationKey =
 21        ApplicationDataProtectionOptions.SectionName + ":" + nameof(ApplicationDataProtectionOptions.KeyEncryptionCertif
 22
 23    private const string _allowedHostsConfigurationKey = "AllowedHosts";
 24
 125    private static readonly Action<ILogger, string, Exception?> _logPermissiveAllowedHosts =
 126        LoggerMessage.Define<string>(
 127            LogLevel.Warning,
 128            new EventId(1005, "PermissiveAllowedHosts"),
 129            "Security posture: {ConfigurationKey} allows every host, so ASP.NET Core host filtering accepts any Host " +
 130            "header. Set it to the public host names this deployment serves, such as \"app.example.com;www.example.com\"
 31
 132    private static readonly Action<ILogger, string, string, Exception?> _logDataProtectionKeyRingUnderContentRoot =
 133        LoggerMessage.Define<string, string>(
 134            LogLevel.Warning,
 135            new EventId(1003, "DataProtectionKeyRingRelativePath"),
 136            "Security posture: {ConfigurationKey} is the relative path '{KeyRingPath}', so the Data Protection key ring 
 137            "is stored under the application content root. In containers and orchestrated deployments that location is "
 138            "usually replaced with the application, which invalidates authentication cookies and antiforgery tokens. " +
 139            "Configure an absolute path on durable, access-restricted storage shared by every replica.");
 40
 141    private static readonly Action<ILogger, string, Exception?> _logDataProtectionKeysNotEncrypted =
 142        LoggerMessage.Define<string>(
 143            LogLevel.Warning,
 144            new EventId(1004, "DataProtectionKeysNotEncryptedAtRest"),
 145            "Security posture: {ConfigurationKey} is not set, so Data Protection key-ring files are not encrypted by the
 146            "application. On Linux and macOS they are written in plain text; on Windows they are protected with DPAPI fo
 147            "the current user and cannot be shared across machines. Configure a key-encryption certificate or confirm " 
 148            "that storage-level encryption and access controls protect the key ring.");
 49
 150    private static readonly Action<ILogger, string, Exception?> _logAuthenticationDisabled =
 151        LoggerMessage.Define<string>(
 152            LogLevel.Warning,
 153            new EventId(1001, "AuthenticationDisabled"),
 154            "Security posture: authentication is intentionally disabled by {ConfigurationKey}. " +
 155            "This is a supported configuration, not an authentication framework failure. " +
 156            "Review deployment exposure and authorization expectations before production use.");
 57
 158    private static readonly Action<ILogger, string, Exception?> _logAnonymousProductionHealthEndpoints =
 159        LoggerMessage.Define<string>(
 160            LogLevel.Warning,
 161            new EventId(1002, "AnonymousProductionHealthEndpoints"),
 162            "Security posture: health endpoints {HealthEndpoints} are intentionally mapped with anonymous access in Prod
 163            "Anonymous probes are supported for infrastructure health checks; confirm reverse-proxy, ingress, firewall, 
 164            "or service-mesh routing limits external reachability as intended.");
 65
 66    /// <summary>
 67    /// Emits structured startup diagnostics for supported security postures that require
 68    /// operator awareness.
 69    /// </summary>
 70    /// <param name="app">The application whose startup posture is being reported.</param>
 71    /// <returns>The original <see cref="WebApplication"/> for chaining.</returns>
 72    public static WebApplication LogApplicationSecurityPosture(this WebApplication app)
 73    {
 9174        ArgumentNullException.ThrowIfNull(app);
 75
 9176        LogApplicationSecurityPosture(app.Logger, app.Configuration, app.Environment);
 77
 9178        return app;
 79    }
 80
 81    /// <summary>
 82    /// Emits structured startup diagnostics using the supplied application services.
 83    /// This overload keeps the posture rules independently testable.
 84    /// </summary>
 85    /// <param name="logger">The startup logger.</param>
 86    /// <param name="configuration">The application configuration.</param>
 87    /// <param name="environment">The host environment.</param>
 88    public static void LogApplicationSecurityPosture(
 89        ILogger logger,
 90        IConfiguration configuration,
 91        IHostEnvironment environment)
 92    {
 10193        ArgumentNullException.ThrowIfNull(logger);
 10194        ArgumentNullException.ThrowIfNull(configuration);
 10195        ArgumentNullException.ThrowIfNull(environment);
 96
 10197        bool authenticationEnabled = configuration.GetValue<bool>(
 10198            _authenticationEnabledConfigurationKey);
 99
 101100        if (!authenticationEnabled)
 101        {
 2102            _logAuthenticationDisabled(
 2103                logger,
 2104                _authenticationEnabledConfigurationKey,
 2105                null);
 106        }
 107
 101108        if (environment.IsProduction())
 109        {
 2110            _logAnonymousProductionHealthEndpoints(
 2111                logger,
 2112                _anonymousHealthEndpoints,
 2113                null);
 114        }
 115
 101116        if (!environment.IsDevelopment())
 117        {
 96118            LogHostFilteringPosture(logger, configuration);
 96119            LogDataProtectionPosture(logger, configuration);
 120        }
 101121    }
 122
 123    private static void LogHostFilteringPosture(ILogger logger, IConfiguration configuration)
 124    {
 96125        string? allowedHosts = configuration[_allowedHostsConfigurationKey]?.Trim();
 126
 127        // An absent value and "*" both allow every Host header, because host filtering defaults to allowing all hosts.
 96128        bool allowsEveryHost = string.IsNullOrEmpty(allowedHosts) ||
 96129            allowedHosts.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
 96130                .Any(host => string.Equals(host, "*", StringComparison.Ordinal));
 131
 96132        if (allowsEveryHost)
 133        {
 93134            _logPermissiveAllowedHosts(logger, _allowedHostsConfigurationKey, null);
 135        }
 96136    }
 137
 138    private static void LogDataProtectionPosture(ILogger logger, IConfiguration configuration)
 139    {
 96140        string keyRingPath = configuration[_dataProtectionKeyRingPathConfigurationKey]?.Trim() is { Length: > 0 } config
 96141            ? configuredPath
 96142            : new ApplicationDataProtectionOptions().KeyRingPath;
 143
 96144        if (!Path.IsPathFullyQualified(keyRingPath))
 145        {
 93146            _logDataProtectionKeyRingUnderContentRoot(
 93147                logger,
 93148                _dataProtectionKeyRingPathConfigurationKey,
 93149                keyRingPath,
 93150                null);
 151        }
 152
 96153        if (string.IsNullOrWhiteSpace(configuration[_dataProtectionKeyEncryptionCertificatePathConfigurationKey]))
 154        {
 92155            _logDataProtectionKeysNotEncrypted(
 92156                logger,
 92157                _dataProtectionKeyEncryptionCertificatePathConfigurationKey,
 92158                null);
 159        }
 96160    }
 161}