├── .gitignore ├── Build.ps1 ├── BuildTestPack.ps1 ├── DotnetHttpSecurityCheck.sln ├── Functions.ps1 ├── LICENSE.txt ├── Pack.ps1 ├── README.md ├── Shared.Dependencies.Test.props ├── Shared.Dependencies.props ├── Shared.Package.props ├── Shared.props ├── Test.ps1 ├── src ├── CodeTherapy.HttpSecurityCheck │ ├── CodeTherapy.HttpSecurityCheck.csproj │ ├── ContentSecurityPolicySecurityHeaderCheck.cs │ ├── Core │ │ ├── HttpResponseMessageExtensions.cs │ │ └── IEnumerableStringExteions.cs │ ├── Data │ │ ├── HeaderValueCheck.cs │ │ ├── HttpSecurityCheckSummary.cs │ │ ├── SecurityCheckExecutionResult.cs │ │ ├── SecurityCheckInfo.cs │ │ ├── SecurityCheckPiplineResult.cs │ │ ├── SecurityCheckState.cs │ │ └── ServerCertificateValidation.cs │ ├── DisclosureHeaderValueSecurityCheck.cs │ ├── HTTPSCheck.cs │ ├── HttpHeaderCheckBase.cs │ ├── HttpSecurityCheckBase.cs │ ├── ISecurityCheck.cs │ ├── MissingHeaderValueSecurityCheck.cs │ ├── Properties │ │ └── launchSettings.json │ ├── ReferrerPolicySecurityHeaderCheck.cs │ ├── Report │ │ ├── ISecurityCheckReportWriter.cs │ │ └── SecurityCheckReportWriterBase.cs │ ├── ServerCertificateCheck.cs │ ├── ServerDisclosureHeaderValueSecurityCheck.cs │ ├── Services │ │ ├── ISecurityCheckPipline.cs │ │ ├── SecurityCheckPipeline.cs │ │ └── UriHelper.cs │ ├── SingleHeaderValueSecurityCheck.cs │ ├── StrictTransportSecuritySecurityHeaderCheck.cs │ ├── XContentTypeOptionsSecurityHeaderCheck.cs │ ├── XFrameOptionsSecurityHeaderCheck.cs │ ├── XPoweredByDisclosureHeaderSecurityCheck.cs │ └── XXSSProtectionSecurityHeaderCheck.cs └── DotnetHttpSecurityCheck │ ├── Command.cs │ ├── ConsoleExtensions.cs │ ├── DotnetHttpSecurityCheck.csproj │ ├── Program.cs │ ├── Report │ ├── ConsoleSecurityCheckReportWriter.cs │ ├── JsonHttpSecurityCheckReportWriter.cs │ └── TextWriterSecurityCheckReportWriter.cs │ ├── ReportFormat.cs │ └── VerbosityLevel.cs └── test └── CodeTherapy.HttpSecurityCheck.Tests ├── CodeTherapy.HttpSecurityCheck.Tests.csproj ├── ContentSecurityPolicySecurityHeaderCheckTests.cs ├── DisclosureHeaderSecurityCheckTestBase.cs ├── HeaderSecurityCheckTestBase.cs ├── HttpsSecurityHeaderCheckTests.cs ├── ReferrerPolicySecurityHeaderCheckTests.cs ├── SecurityCheckExecutionResultTests.cs ├── SecurityCheckPipelineTests.cs ├── SecurityCheckTestBase.cs ├── SecurityHeaderCheckBaseTests.cs ├── ServerCertificateSecurityHeaderCheckTests.cs ├── ServerDisclosureHeaderSecurityCheckTests.cs ├── StrictTransportSecuritySecurityHeaderCheckTests.cs ├── XContentTypeOptionsSecurityHeaderCheckTests.cs ├── XFrameOptionsSecurityHeaderCheckTests.cs ├── XPoweredByDisclosureHeaderSecurityCheckTests.cs └── XXSSProtectionSecurityHeaderCheckTests.cs /.gitignore: -------------------------------------------------------------------------------- 1 | [Oo]bj/ 2 | [Bb]in/ 3 | TestResults/ 4 | .nuget/ 5 | .testPublish/ 6 | *.sln.ide/ 7 | _ReSharper.*/ 8 | packages/ 9 | artifacts/ 10 | PublishProfiles/ 11 | .vs/ 12 | bower_components/ 13 | node_modules/ 14 | **/wwwroot/lib/ 15 | debugSettings.json 16 | project.lock.json 17 | *.user 18 | *.suo 19 | *.cache 20 | *.docstates 21 | _ReSharper.* 22 | *.exe 23 | *.psess 24 | *.vsp 25 | *.pidb 26 | *.userprefs 27 | *DS_Store 28 | *.ncrunchsolution 29 | *.*sdf 30 | *.ipch 31 | .settings 32 | *.sln.ide 33 | node_modules/ 34 | **/[Cc]ompiler/[Rr]esources/**/*.js 35 | .vscode/ 36 | .idea/ 37 | *.iml 38 | .build/ 39 | **/coverage.json -------------------------------------------------------------------------------- /Build.ps1: -------------------------------------------------------------------------------- 1 | 2 | 3 | [CmdletBinding(PositionalBinding = $false)] 4 | param( 5 | [ValidateSet('Debug', 'Release')] 6 | $Configuration = 'Release' 7 | ) 8 | 9 | Set-StrictMode -Version 1 10 | $ErrorActionPreference = 'Stop' 11 | 12 | . .\Functions.ps1 13 | 14 | $arguments = New-Object System.Collections.ArrayList 15 | 16 | [void]$arguments.Add("-p:Configuration=$Configuration") 17 | 18 | exec dotnet build @arguments 19 | 20 | write-host -f green 'Build Done!' 21 | -------------------------------------------------------------------------------- /BuildTestPack.ps1: -------------------------------------------------------------------------------- 1 | 2 | 3 | [CmdletBinding(PositionalBinding = $false)] 4 | param( 5 | [ValidateSet('Debug', 'Release')] 6 | $Configuration = 'Release', 7 | [switch] 8 | $CollectCoverage, 9 | [switch] 10 | $NoIntegrationTests, 11 | [switch] 12 | $Pack 13 | ) 14 | 15 | Set-StrictMode -Version 1 16 | $ErrorActionPreference = 'Stop' 17 | 18 | write-host -f Magenta '*** Build ***' 19 | .\Build.ps1 -Configuration $Configuration 20 | 21 | write-host -f Magenta '*** Test ***' 22 | .\Test.ps1 -Configuration $Configuration -NoBuild -CollectCoverage:$CollectCoverage -NoIntegrationTests:$NoIntegrationTests 23 | 24 | if($Pack) { 25 | write-host -f Magenta '*** Pack ***' 26 | .\Pack.ps1 -Configuration $Configuration -NoBuild 27 | } -------------------------------------------------------------------------------- /DotnetHttpSecurityCheck.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28010.2041 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotnetHttpSecurityCheck", "src\DotnetHttpSecurityCheck\DotnetHttpSecurityCheck.csproj", "{9D958890-3DC5-4D27-B2AD-E83371613C69}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodeTherapy.HttpSecurityCheck", "src\CodeTherapy.HttpSecurityCheck\CodeTherapy.HttpSecurityCheck.csproj", "{C4B04422-F371-4DF7-9BC9-14D11DC5F18C}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodeTherapy.HttpSecurityCheck.Tests", "test\CodeTherapy.HttpSecurityCheck.Tests\CodeTherapy.HttpSecurityCheck.Tests.csproj", "{A8D12B50-0714-42EE-8775-18317319D777}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9E9D6CE9-8219-4DB3-8957-70088AD00855}" 13 | ProjectSection(SolutionItems) = preProject 14 | Build.ps1 = Build.ps1 15 | BuildTestPack.ps1 = BuildTestPack.ps1 16 | Functions.ps1 = Functions.ps1 17 | gitignore.txt = gitignore.txt 18 | Pack.ps1 = Pack.ps1 19 | README.md = README.md 20 | Shared.Dependencies.props = Shared.Dependencies.props 21 | Shared.Dependencies.Test.props = Shared.Dependencies.Test.props 22 | Shared.Package.props = Shared.Package.props 23 | Shared.props = Shared.props 24 | Test.ps1 = Test.ps1 25 | EndProjectSection 26 | EndProject 27 | Global 28 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 29 | Debug|Any CPU = Debug|Any CPU 30 | Release|Any CPU = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 33 | {9D958890-3DC5-4D27-B2AD-E83371613C69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {9D958890-3DC5-4D27-B2AD-E83371613C69}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {9D958890-3DC5-4D27-B2AD-E83371613C69}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {9D958890-3DC5-4D27-B2AD-E83371613C69}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {C4B04422-F371-4DF7-9BC9-14D11DC5F18C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {C4B04422-F371-4DF7-9BC9-14D11DC5F18C}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {C4B04422-F371-4DF7-9BC9-14D11DC5F18C}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {C4B04422-F371-4DF7-9BC9-14D11DC5F18C}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {A8D12B50-0714-42EE-8775-18317319D777}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {A8D12B50-0714-42EE-8775-18317319D777}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {A8D12B50-0714-42EE-8775-18317319D777}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {A8D12B50-0714-42EE-8775-18317319D777}.Release|Any CPU.Build.0 = Release|Any CPU 45 | EndGlobalSection 46 | GlobalSection(SolutionProperties) = preSolution 47 | HideSolutionNode = FALSE 48 | EndGlobalSection 49 | GlobalSection(ExtensibilityGlobals) = postSolution 50 | SolutionGuid = {8A2C5A5C-0EDB-4CF9-A34F-3CA550A0BD4E} 51 | EndGlobalSection 52 | EndGlobal 53 | -------------------------------------------------------------------------------- /Functions.ps1: -------------------------------------------------------------------------------- 1 | 2 | 3 | function exec([string]$_cmd) { 4 | write-host -ForegroundColor White " $_cmd $args" 5 | $ErrorActionPreference = 'Continue' 6 | & $_cmd @args 7 | $ErrorActionPreference = 'Stop' 8 | if ($LASTEXITCODE -ne 0) { 9 | write-error "Failed with exit code $LASTEXITCODE" 10 | exit 1 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) CodeTherapist 4 | 5 | All rights reserved. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. -------------------------------------------------------------------------------- /Pack.ps1: -------------------------------------------------------------------------------- 1 | 2 | 3 | [CmdletBinding(PositionalBinding = $false)] 4 | param( 5 | [ValidateSet('Debug', 'Release')] 6 | $Configuration = 'Release', 7 | [switch] 8 | $NoBuild 9 | ) 10 | 11 | Set-StrictMode -Version 1 12 | $ErrorActionPreference = 'Stop' 13 | 14 | . .\Functions.ps1 15 | 16 | $arguments = New-Object System.Collections.ArrayList 17 | 18 | [void]$arguments.Add("-p:Configuration=$Configuration") 19 | 20 | if ($NoBuild) { 21 | [void]$arguments.Add('--no-build') 22 | } 23 | 24 | $artifacts = "$PSScriptRoot\artifacts\" 25 | 26 | Remove-Item -Recurse $artifacts -ErrorAction Ignore 27 | 28 | exec dotnet pack -o $artifacts @arguments 29 | 30 | write-host -f green 'Pack done!' 31 | 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dotnet http-security-check 2 | 3 | A .NET Core global tool to do a http request/response security assessment. 4 | You can also find some information on my [Blog](https://codetherapist.github.io/blog/dotnet-http-security-check/). 5 | 6 | ## Installation 7 | 8 | Download and install the [.NET Core 2.2 SDK](https://www.microsoft.com/net/download) or newer. Once installed, run the following command: 9 | 10 | ```text 11 | dotnet tool install -g DotnetHttpSecurityCheck 12 | ``` 13 | 14 | If you already have a previous version of [DotnetHttpSecurityCheck](https://www.nuget.org/packages/DotnetHttpSecurityCheck/) installed, you can upgrade to the latest version using the following command: 15 | 16 | ```text 17 | dotnet tool update -g DotnetHttpSecurityCheck 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```text 23 | 1.0.0 24 | 25 | A dotnet tool to do a http request/response security assessment. 26 | 27 | Usage: http-security-check [arguments] [options] 28 | 29 | Arguments: 30 | Url A absolute URL for the security assessment (required). 31 | 32 | Options: 33 | --version Show version information 34 | -?|-h|--help Show help information 35 | -o|--output The report output file path (optional). 36 | -f|--format Format of the report (optional). Default is Text. 37 | -v|--verbose Set the console verbosity level (optional). Default is normal. Allowed values are n[normal], q[uiet], d[etailed]. 38 | 39 | All Security Checks: 40 | 41 | > Header 42 | 43 | 1# X-Content-Type-Options: Checks the response header value of the 'X-Content-Type-Options' header. 44 | Recommended is "X-Content-Type-Options: nosniff". 45 | 46 | 2# X-Frame-Options: Checks the response header value of the 'X-Frame-Options' header. 47 | Recommended is "X-Frame-Options: deny". 48 | 49 | 3# X-XSS-Protection: Checks the response header value of the 'X-XSS-Protection' header. 50 | Recommended is "X-XSS-Protection: 1;". 51 | 52 | 4# Referrer-Policy: Checks the response header value of the 'Referrer-Policy' header. 53 | Recommended values: "strict-origin", "strict-origin-when-cross-origin" or "no-referrer". 54 | 55 | 5# Content-Security-Policy: Checks the response header value of the 'Content-Security-Policy' header. 56 | Whitelisting sources of approved content. Example: "Content-Security-Policy: default-src 'self'" 57 | 58 | 6# Strict-Transport-Security: Checks the response header value of the 'Strict-Transport-Security' header. 59 | Recommended is "Strict-Transport-Security: max-age=31536000; includeSubDomains". 60 | 61 | 7# X-Powered-By: Checks the response header value of the 'X-Powered-By' header. 62 | Technology information should be removed. 63 | 64 | 8# Server: Checks the response header value of the 'Server' header. 65 | Server information should be removed. 66 | 67 | > Request 68 | 69 | 1# Server Certificate: Checks for server certificate validation errors. 70 | Recommended is a valid certificate that has no errors. 71 | 72 | 2# HTTPS: Checks that the request/response is HTTPS. 73 | Recommended is to use HTTPS. 74 | ``` 75 | 76 | ### Usage examples 77 | 78 | Simplest usage: 79 | 80 | ```text 81 | dotnet http-security-check https://example.com 82 | ``` 83 | 84 | Same as above, but writes a textual report: 85 | 86 | ```text 87 | dotnet http-security-check https://example.com -o=.\Report.txt 88 | ``` 89 | 90 | The tool has basic support for reporting. 91 | By default the report is written to the console. 92 | 93 | ## Contribution 94 | 95 | Feel free to create issues and PR's (pull requests) to improve this tool - any help is appreciated! 96 | The project is splitted in three projects: 97 | 98 | **Core** 99 | 100 | _CodeTherapy.HttpSecurityCheck.csproj_ is the core library with all the security checks and infrastructure. 101 | 102 | **Tool** 103 | 104 | _DotnetHttpSecurityCheck.csproj_ is the dotnet tool console command - a lightweight wrapper around the core. 105 | 106 | **Tests** 107 | 108 | _CodeTherapy.HttpSecurityCheck.Tests.csproj_ contains unit tests and integration tests. 109 | 110 | Code Coverage (powered by [Coverlet](https://github.com/tonerdo/coverlet)) 111 | 112 | | Module | Line | Branch | Method | 113 | |-------------------------------|--------|--------|--------| 114 | | CodeTherapy.HttpSecurityCheck | 85.2% | 78.4% | 83.3% | 115 | 116 | ## Build, Test & Pack 117 | 118 | The project separates the three concerns _Building_, _Testing_ and _Packaging_. 119 | All of these steps could be executed individually. 120 | 121 | **BuildTestPack.ps1** 122 | 123 | This command calls internally **Build.ps1**, **Test.ps1** and **Pack.ps1** and supports following parameters: 124 | 125 | Parameter|Description|Type| 126 | ---------|-----------|----| 127 | Configuration|Can be set to "Release" or "Debug"|string 128 | CollectCoverage|When set, code coverage is calculated|switch 129 | NoIntegrationTests|When set, integration tests are skipped|switch 130 | Pack|When set, nuget packages are created (call to Pack.ps1)|switch 131 | 132 | **Build.ps1** 133 | 134 | Builds all projects. Supports the _Configuration_ parameter. 135 | 136 | **Test.ps1** 137 | 138 | Runs all tests. Supports _Configuration_, _CollectCoverage_, _NoIntegrationTests_ and _NoBuild_ parameters. 139 | 140 | Parameter|Description|Type 141 | ---------|-----------|---- 142 | NoBuild|The projects are not re-build|switch 143 | 144 | **Pack.ps1** 145 | 146 | Creates the nuget packages into the `.\artifacts` directory. Supports _Configuration_ and _NoBuild_ parameters. 147 | 148 | ## License 149 | 150 | This project is licensed under the MIT License - see the LICENSE.md file for details. 151 | -------------------------------------------------------------------------------- /Shared.Dependencies.Test.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | runtime; build; native; contentfiles; analyzers 8 | 9 | 10 | 11 | runtime; build; native; contentfiles; analyzers 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers 16 | 17 | 18 | -------------------------------------------------------------------------------- /Shared.Dependencies.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Shared.Package.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | https://opensource.org/licenses/MIT 5 | https://github.com/CodeTherapist/DotnetHttpSecurityCheck 6 | https://github.com/CodeTherapist/DotnetHttpSecurityCheck.git 7 | git 8 | true 9 | 10 | -------------------------------------------------------------------------------- /Shared.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | CodeTherapist 4 | Copyright © CodeTherapist 5 | 1.0.0 6 | Latest 7 | none 8 | true 9 | portable 10 | true 11 | 12 | -------------------------------------------------------------------------------- /Test.ps1: -------------------------------------------------------------------------------- 1 | 2 | 3 | [CmdletBinding(PositionalBinding = $false)] 4 | param( 5 | [ValidateSet('Debug', 'Release')] 6 | $Configuration = 'Debug', 7 | [switch] 8 | $CollectCoverage, 9 | [switch] 10 | $NoBuild, 11 | [switch] 12 | $NoIntegrationTests 13 | ) 14 | 15 | Set-StrictMode -Version 1 16 | $ErrorActionPreference = 'Stop' 17 | 18 | . .\Functions.ps1 19 | 20 | $arguments = New-Object System.Collections.ArrayList 21 | 22 | if ($NoBuild) { 23 | [void]$arguments.Add('--no-build') 24 | } 25 | 26 | if($NoIntegrationTests) { 27 | [void]$arguments.Add('--filter="Category!=Integration"') 28 | } 29 | 30 | [void]$arguments.Add("-p:Configuration=$Configuration") 31 | 32 | if ($CollectCoverage) { 33 | [void]$arguments.Add("-p:CollectCoverage=true") 34 | } 35 | 36 | exec dotnet test "$PSScriptRoot\test\CodeTherapy.HttpSecurityCheck.Tests\CodeTherapy.HttpSecurityCheck.Tests.csproj" ` 37 | @arguments 38 | 39 | write-host -f green 'Test Done!' 40 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/CodeTherapy.HttpSecurityCheck.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | netstandard2.0 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/ContentSecurityPolicySecurityHeaderCheck.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Data; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace CodeTherapy.HttpSecurityChecks 6 | { 7 | public sealed class ContentSecurityPolicySecurityHeaderCheck : MissingHeaderValueSecurityCheckBase 8 | { 9 | public override string Name => HeaderName; 10 | 11 | public override string HeaderName => "Content-Security-Policy"; 12 | 13 | public override string Recommendation => $"Whitelisting sources of approved content. Example: \"{HeaderName}: default-src 'self'\""; 14 | 15 | public IReadOnlyCollection ValidSrcs => new[]{ 16 | "default-src", 17 | "child-src", 18 | "connect-src", 19 | "font-src", 20 | "frame-src", 21 | "img-src", 22 | "manifest-src", 23 | "media-src", 24 | "object-src", 25 | "prefetch-src", 26 | "script-src", 27 | "style-src", 28 | "worker-src", 29 | }; 30 | 31 | public override IReadOnlyCollection HeaderValueChecks => new[] 32 | { 33 | HeaderValueCheck.IsBest(when: value => ValidSrcs.Any(vs => value.Contains(vs))) 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/Core/HttpResponseMessageExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Net.Http.Headers; 6 | 7 | namespace CodeTherapy.HttpSecurityChecks.Core 8 | { 9 | public static class HttpResponseMessageExtensions 10 | { 11 | 12 | public static IEnumerable GetHeaderValuesOrEmpty(this HttpResponseMessage httpResponseMessage, string headerName) 13 | { 14 | if (httpResponseMessage.Headers.TryGetValues(headerName, out IEnumerable values)) 15 | { 16 | return values; 17 | } 18 | return Array.Empty(); 19 | } 20 | 21 | public static bool TryGetHeaderValue(this HttpResponseHeaders headers, string headerName, out string value) 22 | { 23 | value = null; 24 | var hasHeader = headers.TryGetValues(headerName, out IEnumerable values); 25 | if (hasHeader) 26 | { 27 | value = values.Single(); 28 | } 29 | return hasHeader; 30 | } 31 | 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/Core/IEnumerableStringExteions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace CodeTherapy.HttpSecurityChecks.Core 6 | { 7 | public static class IEnumerableStringExteions 8 | { 9 | public static bool AnyOrdinalIgnoreCase(this IEnumerable enumerable, string value) 10 | { 11 | return enumerable.Any(source => string.Equals(source, value, StringComparison.OrdinalIgnoreCase)); 12 | } 13 | 14 | public static bool EqualsOrdinalIgnoreCase(this string s, string value) 15 | { 16 | return string.Equals(s, value, StringComparison.OrdinalIgnoreCase); 17 | } 18 | 19 | public static bool StartsWithOrdinalIgnoreCase(this string s, string value) 20 | { 21 | if (string.IsNullOrWhiteSpace(s)) 22 | { 23 | return false; 24 | } 25 | return s.StartsWith(value, StringComparison.OrdinalIgnoreCase); 26 | } 27 | 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/Data/HeaderValueCheck.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CodeTherapy.HttpSecurityChecks.Data 4 | { 5 | public sealed class HeaderValueCheck 6 | { 7 | private readonly Predicate _when; 8 | 9 | public HeaderValueCheck() 10 | { 11 | } 12 | 13 | public HeaderValueCheck(SecurityCheckState securityCheckState, Predicate when, string recommendation = "") 14 | { 15 | _when = when; 16 | SecurityCheckState = securityCheckState; 17 | Recommendation = recommendation; 18 | } 19 | 20 | 21 | public bool Matches(string value) 22 | { 23 | return _when(value); 24 | } 25 | 26 | public SecurityCheckState SecurityCheckState { get; private set; } 27 | 28 | public string Recommendation { get; private set; } 29 | 30 | public static HeaderValueCheck IsBad(Predicate when, string recommandation) 31 | { 32 | return new HeaderValueCheck(SecurityCheckState.Bad, when, recommandation); 33 | } 34 | 35 | public static HeaderValueCheck IsGood(Predicate when, string recommandation) 36 | { 37 | return new HeaderValueCheck(SecurityCheckState.Good, when, recommandation); 38 | } 39 | 40 | public static HeaderValueCheck IsBest(Predicate when) 41 | { 42 | return new HeaderValueCheck(SecurityCheckState.Best, when); 43 | } 44 | 45 | 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/Data/HttpSecurityCheckSummary.cs: -------------------------------------------------------------------------------- 1 | namespace CodeTherapy.HttpSecurityChecks.Data 2 | { 3 | public sealed class HttpSecurityCheckSummary 4 | { 5 | public string Url { get; set; } 6 | 7 | public string HttpMethod { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/Data/SecurityCheckExecutionResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CodeTherapy.HttpSecurityChecks.Data 4 | { 5 | public sealed class SecurityCheckExecutionResult 6 | { 7 | public SecurityCheckExecutionResult(ISecurityCheck securityCheck, SecurityCheckResult securityCheckInfo, Exception exception = null) 8 | { 9 | SecurityCheck = securityCheck ?? throw new ArgumentNullException(nameof(securityCheck)); 10 | SecurityCheckResult = securityCheckInfo ?? throw new ArgumentNullException(nameof(securityCheckInfo)); 11 | Exception = exception; 12 | } 13 | 14 | public ISecurityCheck SecurityCheck { get; } 15 | 16 | public SecurityCheckResult SecurityCheckResult { get; } 17 | 18 | public bool HasError => !(Exception is null); 19 | 20 | public Exception Exception { get; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/Data/SecurityCheckInfo.cs: -------------------------------------------------------------------------------- 1 | namespace CodeTherapy.HttpSecurityChecks.Data 2 | { 3 | public sealed class SecurityCheckResult 4 | { 5 | public static SecurityCheckResult Empty = new SecurityCheckResult(SecurityCheckState.None, string.Empty); 6 | 7 | private SecurityCheckResult(SecurityCheckState state, string recommandation, string value = "") 8 | { 9 | State = state; 10 | Recommandation = recommandation; 11 | Value = value; 12 | } 13 | 14 | public SecurityCheckState State { get; } 15 | 16 | public string Recommandation { get; } 17 | 18 | public bool HasRecommandation => !string.IsNullOrWhiteSpace(Recommandation); 19 | 20 | public string Value { get; } 21 | 22 | public static SecurityCheckResult Create(SecurityCheckState state, string recommandation = "", string value = "") => new SecurityCheckResult(state, recommandation, value); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/Data/SecurityCheckPiplineResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | 5 | namespace CodeTherapy.HttpSecurityChecks.Data 6 | { 7 | public sealed class SecurityCheckPiplineResult : IEnumerable 8 | { 9 | public static SecurityCheckPiplineResult Empty => new SecurityCheckPiplineResult(); 10 | 11 | public SecurityCheckPiplineResult() 12 | { 13 | Results = new List(); 14 | } 15 | 16 | public DateTimeOffset DateTime { get; set; } 17 | 18 | public string Url { get; set; } 19 | 20 | private List Results { get; } 21 | 22 | public int Count => Results.Count; 23 | 24 | 25 | public void Add(ISecurityCheck securityCheck, SecurityCheckResult info) 26 | { 27 | Results.Add(new SecurityCheckExecutionResult(securityCheck, info)); 28 | } 29 | 30 | public void Add(ISecurityCheck securityCheck, Exception exception) 31 | { 32 | Results.Add(new SecurityCheckExecutionResult(securityCheck, SecurityCheckResult.Empty, exception)); 33 | } 34 | 35 | public IEnumerator GetEnumerator() 36 | { 37 | return Results.GetEnumerator(); 38 | } 39 | 40 | IEnumerator IEnumerable.GetEnumerator() 41 | { 42 | return GetEnumerator(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/Data/SecurityCheckState.cs: -------------------------------------------------------------------------------- 1 | namespace CodeTherapy.HttpSecurityChecks.Data 2 | { 3 | public enum SecurityCheckState 4 | { 5 | None, 6 | Skipped, 7 | Bad, 8 | Good, 9 | Best, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/Data/ServerCertificateValidation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Security; 3 | using System.Security.Cryptography.X509Certificates; 4 | 5 | namespace CodeTherapy.HttpSecurityChecks.Data 6 | { 7 | public sealed class ServerCertificateInfo 8 | { 9 | public ServerCertificateInfo(string x509CertificateName, X509Chain x509Chain, SslPolicyErrors sslPolicyErrors) 10 | { 11 | X509CertificateName = x509CertificateName ?? throw new ArgumentNullException(nameof(x509CertificateName)); 12 | X509Chain = x509Chain ?? throw new ArgumentNullException(nameof(x509Chain)); 13 | SslPolicyErrors = sslPolicyErrors; 14 | } 15 | 16 | public string X509CertificateName { get; } 17 | 18 | public X509Chain X509Chain { get; } 19 | 20 | public SslPolicyErrors SslPolicyErrors { get; } 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/DisclosureHeaderValueSecurityCheck.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using CodeTherapy.HttpSecurityChecks.Data; 3 | 4 | namespace CodeTherapy.HttpSecurityChecks 5 | { 6 | public abstract class DisclosureHeaderValueSecurityCheck : SingleHeaderValueSecurityCheck 7 | { 8 | 9 | protected override SecurityCheckResult CheckHeaderValue(string value) 10 | { 11 | return SecurityCheckResult.Create(SecurityCheckState.Bad, Recommendation, value); 12 | } 13 | 14 | protected override SecurityCheckResult CreateForMissingHeader() 15 | { 16 | return SecurityCheckResult.Create(SecurityCheckState.Best); 17 | } 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/HTTPSCheck.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using CodeTherapy.HttpSecurityChecks.Data; 4 | 5 | namespace CodeTherapy.HttpSecurityChecks 6 | { 7 | public sealed class HttpsCheck : HttpSecurityCheckBase 8 | { 9 | public override string Name => "HTTPS"; 10 | 11 | public override string Description => "Checks that the request/response is HTTPS."; 12 | 13 | public override string Category => "Request"; 14 | 15 | public override string Recommendation => "Recommended is to use HTTPS."; 16 | 17 | protected override SecurityCheckResult CheckCore(HttpResponseMessage httpResponseMessage) 18 | { 19 | if (string.Equals(httpResponseMessage.RequestMessage.RequestUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) 20 | { 21 | return SecurityCheckResult.Create(SecurityCheckState.Best, string.Empty, httpResponseMessage.RequestMessage.RequestUri.Scheme); 22 | } 23 | return SecurityCheckResult.Create(SecurityCheckState.Bad, Recommendation, Uri.UriSchemeHttp); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/HttpHeaderCheckBase.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Net.Http.Headers; 3 | using CodeTherapy.HttpSecurityChecks.Data; 4 | 5 | namespace CodeTherapy.HttpSecurityChecks 6 | { 7 | public abstract class HttpHeaderCheckBase : HttpSecurityCheckBase 8 | { 9 | public abstract string HeaderName { get; } 10 | 11 | public override string Category => "Header"; 12 | 13 | public override string Description => $"Checks the response header value of the '{HeaderName}' header."; 14 | 15 | protected override SecurityCheckResult CheckCore(HttpResponseMessage httpResponseMessage) 16 | { 17 | return CheckHeader(httpResponseMessage.Headers); 18 | } 19 | 20 | protected abstract SecurityCheckResult CheckHeader(HttpResponseHeaders headers); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/HttpSecurityCheckBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using CodeTherapy.HttpSecurityChecks.Data; 5 | 6 | namespace CodeTherapy.HttpSecurityChecks 7 | { 8 | public abstract class HttpSecurityCheckBase : ISecurityCheck, IEquatable 9 | { 10 | public abstract string Name { get; } 11 | 12 | public abstract string Description { get; } 13 | 14 | public abstract string Category { get; } 15 | 16 | public abstract string Recommendation { get; } 17 | 18 | public virtual bool HttpsOnly => false; 19 | 20 | public SecurityCheckResult Check(HttpResponseMessage httpResponseMessage) 21 | { 22 | if (httpResponseMessage is null) 23 | { 24 | throw new ArgumentNullException(nameof(httpResponseMessage)); 25 | } 26 | 27 | if (HttpsOnly && httpResponseMessage.RequestMessage.RequestUri.Scheme != Uri.UriSchemeHttps) 28 | { 29 | return SecurityCheckResult.Create(SecurityCheckState.Skipped, $"{Name} is HTTPS only."); 30 | } 31 | 32 | return CheckCore(httpResponseMessage); 33 | } 34 | 35 | 36 | 37 | protected abstract SecurityCheckResult CheckCore(HttpResponseMessage httpResponseMessage); 38 | 39 | public bool Equals(HttpSecurityCheckBase other) 40 | { 41 | if (other is null) 42 | { 43 | return false; 44 | } 45 | 46 | if (ReferenceEquals(this, other)) 47 | { 48 | return true; 49 | } 50 | 51 | return string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase); 52 | } 53 | 54 | public sealed override bool Equals(object obj) 55 | { 56 | return Equals(obj as HttpSecurityCheckBase); 57 | } 58 | 59 | public override int GetHashCode() 60 | { 61 | unchecked 62 | { 63 | return 539060726 + EqualityComparer.Default.GetHashCode(Name); 64 | } 65 | } 66 | 67 | public static bool operator ==(HttpSecurityCheckBase obj1, HttpSecurityCheckBase obj2) 68 | { 69 | return EqualityComparer.Default.Equals(obj1, obj2); 70 | } 71 | 72 | public static bool operator !=(HttpSecurityCheckBase obj1, HttpSecurityCheckBase obj2) 73 | { 74 | return !(obj1 == obj2); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/ISecurityCheck.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using CodeTherapy.HttpSecurityChecks.Data; 3 | 4 | namespace CodeTherapy.HttpSecurityChecks 5 | { 6 | public interface ISecurityCheck 7 | { 8 | string Name { get; } 9 | 10 | string Description { get; } 11 | 12 | string Category { get; } 13 | 14 | string Recommendation { get; } 15 | 16 | bool HttpsOnly { get; } 17 | 18 | SecurityCheckResult Check(HttpResponseMessage httpRequestMessage); 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/MissingHeaderValueSecurityCheck.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using CodeTherapy.HttpSecurityChecks.Data; 4 | 5 | namespace CodeTherapy.HttpSecurityChecks 6 | { 7 | public abstract class MissingHeaderValueSecurityCheckBase : SingleHeaderValueSecurityCheck 8 | { 9 | public abstract IReadOnlyCollection HeaderValueChecks { get; } 10 | 11 | 12 | protected override SecurityCheckResult CheckHeaderValue(string value) 13 | { 14 | var trimedValue = value.Trim(); 15 | var headerValueCheck = HeaderValueChecks.FirstOrDefault(check => check.Matches(trimedValue)); 16 | 17 | if (!(headerValueCheck is null)) 18 | { 19 | return SecurityCheckResult.Create(headerValueCheck.SecurityCheckState, headerValueCheck.Recommendation, value); 20 | } 21 | else 22 | { 23 | return CreateForValue(value); 24 | } 25 | } 26 | 27 | protected override SecurityCheckResult CreateForMissingHeader() 28 | { 29 | return SecurityCheckResult.Create(SecurityCheckState.Bad, $"The header {HeaderName} is missing. {Recommendation}"); 30 | } 31 | 32 | protected virtual SecurityCheckResult CreateForValue(string value) 33 | { 34 | return SecurityCheckResult.Create(SecurityCheckState.Bad, $"Invalid or unknown value for {HeaderName}. {Recommendation}", value); 35 | } 36 | } 37 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "CodeTherapy.HttpSecurityChecks": { 4 | "commandName": "Project" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/ReferrerPolicySecurityHeaderCheck.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using CodeTherapy.HttpSecurityChecks.Core; 3 | using CodeTherapy.HttpSecurityChecks.Data; 4 | 5 | namespace CodeTherapy.HttpSecurityChecks 6 | { 7 | public sealed class ReferrerPolicySecurityHeaderCheck : MissingHeaderValueSecurityCheckBase 8 | { 9 | public override string Name => HeaderName; 10 | 11 | public override string HeaderName => "Referrer-Policy"; 12 | 13 | public override string Recommendation => "Recommended values: \"strict-origin\", \"strict-origin-when-cross-origin\" or \"no-referrer\"."; 14 | 15 | public override IReadOnlyCollection HeaderValueChecks => new[] 16 | { 17 | HeaderValueCheck.IsBest(when: value => value.EqualsOrdinalIgnoreCase("strict-origin-when-cross-origin")), 18 | HeaderValueCheck.IsBest(when: value => value.EqualsOrdinalIgnoreCase("strict-origin")), 19 | HeaderValueCheck.IsBest(when: value => value.EqualsOrdinalIgnoreCase("no-referrer")), 20 | 21 | HeaderValueCheck.IsGood(when: value => value.EqualsOrdinalIgnoreCase("origin-when-cross-origin"), recommandation: $"Recommended value \"strict-origin-when-cross-origin\" to prevent leaking referrer data over an insecure connection."), 22 | HeaderValueCheck.IsGood(when: value => value.EqualsOrdinalIgnoreCase("origin"), recommandation: $"Recommended value \"strict-origin\" to prevent leaking referrer data over an insecure connection."), 23 | HeaderValueCheck.IsGood(when: value => value.EqualsOrdinalIgnoreCase("no-referrer-when-downgrade"), recommandation: Recommendation), 24 | HeaderValueCheck.IsGood(when: value => value.EqualsOrdinalIgnoreCase("same-origin"), recommandation: Recommendation), 25 | 26 | HeaderValueCheck.IsBad(when: value => value.EqualsOrdinalIgnoreCase("unsafe-url"), recommandation: $"This policy will leak origins and paths from TLS-protected resources to insecure origins. {Recommendation}."), 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/Report/ISecurityCheckReportWriter.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Data; 2 | using System; 3 | 4 | namespace CodeTherapy.HttpSecurityChecks.Report 5 | { 6 | public interface IHttpSecurityCheckReportWriter 7 | { 8 | void Write(SecurityCheckPiplineResult securityCheckExecutionResults); 9 | } 10 | } -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/Report/SecurityCheckReportWriterBase.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Data; 2 | using System; 3 | 4 | namespace CodeTherapy.HttpSecurityChecks.Report 5 | { 6 | public abstract class HttpSecurityCheckReportWriterBase : IHttpSecurityCheckReportWriter 7 | { 8 | public HttpSecurityCheckReportWriterBase() 9 | { 10 | } 11 | 12 | public void Write(SecurityCheckPiplineResult securityCheckExecutionResults) 13 | { 14 | if (securityCheckExecutionResults is null) 15 | { 16 | throw new ArgumentNullException(nameof(securityCheckExecutionResults)); 17 | } 18 | WriteCore(securityCheckExecutionResults); 19 | } 20 | 21 | protected abstract void WriteCore(SecurityCheckPiplineResult securityCheckExecutionResults); 22 | 23 | protected string GetText(SecurityCheckState securityCheckState) 24 | { 25 | switch (securityCheckState) 26 | { 27 | case SecurityCheckState.None: 28 | return "None"; 29 | case SecurityCheckState.Skipped: 30 | return "Skipped"; 31 | case SecurityCheckState.Bad: 32 | return "Bad"; 33 | case SecurityCheckState.Good: 34 | return "Good"; 35 | case SecurityCheckState.Best: 36 | return "Best Practice"; 37 | default: 38 | throw new ArgumentOutOfRangeException(); 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/ServerCertificateCheck.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using CodeTherapy.HttpSecurityChecks.Data; 4 | 5 | namespace CodeTherapy.HttpSecurityChecks 6 | { 7 | public sealed class ServerCertificateCheck : HttpSecurityCheckBase 8 | { 9 | public override string Name => "Server Certificate"; 10 | 11 | public override string Description => "Checks for server certificate validation errors."; 12 | 13 | public override string Category => "Request"; 14 | 15 | public override bool HttpsOnly => true; 16 | 17 | public override string Recommendation => "Recommended is a valid certificate that has no errors."; 18 | 19 | protected override SecurityCheckResult CheckCore(HttpResponseMessage httpResponseMessage) 20 | { 21 | var request = httpResponseMessage.RequestMessage; 22 | 23 | if (request.Properties.TryGetValue(nameof(ServerCertificateInfo), out object obj)) 24 | { 25 | var serverCertificateValidation = (ServerCertificateInfo)obj; 26 | 27 | if (serverCertificateValidation.SslPolicyErrors == System.Net.Security.SslPolicyErrors.None) 28 | { 29 | return SecurityCheckResult.Create(SecurityCheckState.Best, value: serverCertificateValidation.X509CertificateName); 30 | } 31 | else 32 | { 33 | return SecurityCheckResult.Create(SecurityCheckState.Bad, $"The server certificate has '{serverCertificateValidation.SslPolicyErrors}'. Ensure that the certificate has no erros.", serverCertificateValidation.X509CertificateName); 34 | } 35 | 36 | } 37 | else 38 | { 39 | throw new InvalidOperationException($"No {nameof(ServerCertificateInfo)} found on the request."); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/ServerDisclosureHeaderValueSecurityCheck.cs: -------------------------------------------------------------------------------- 1 | namespace CodeTherapy.HttpSecurityChecks 2 | { 3 | public sealed class ServerDisclosureHeaderValueSecurityCheck : DisclosureHeaderValueSecurityCheck 4 | { 5 | public override string Name => HeaderName; 6 | 7 | public override string HeaderName => "Server"; 8 | 9 | public override string Recommendation => "Server information should be removed."; 10 | } 11 | 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/Services/ISecurityCheckPipline.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Data; 2 | using System; 3 | using System.Net.Http; 4 | 5 | namespace CodeTherapy.HttpSecurityChecks.Services 6 | { 7 | public interface ISecurityCheckPipeline 8 | { 9 | SecurityCheckPiplineResult Analyze(HttpResponseMessage httpResponse); 10 | 11 | SecurityCheckPiplineResult Run(Uri uri, Action beforeRequest = null, Action afterRequest = null); 12 | 13 | } 14 | } -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/Services/SecurityCheckPipeline.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Data; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Net.Http; 5 | using System.Net.Security; 6 | using System.Security.Cryptography.X509Certificates; 7 | 8 | namespace CodeTherapy.HttpSecurityChecks.Services 9 | { 10 | public sealed class SecurityCheckPipeline : ISecurityCheckPipeline 11 | { 12 | private readonly Lazy _httpClient; 13 | 14 | public SecurityCheckPipeline(IEnumerable securityChecks) 15 | { 16 | if (securityChecks is null) 17 | { 18 | throw new ArgumentNullException(nameof(securityChecks)); 19 | } 20 | 21 | _httpClient = new Lazy(CreateHttpClient); 22 | SecurityChecks = new HashSet(securityChecks); 23 | } 24 | 25 | private HttpClient HttpClient => _httpClient.Value; 26 | 27 | private HashSet SecurityChecks { get; } 28 | 29 | /// 30 | /// Analyzes a HTTP response. 31 | /// 32 | /// The target response. 33 | /// The results from the scan. 34 | public SecurityCheckPiplineResult Analyze(HttpResponseMessage httpResponse) 35 | { 36 | if (httpResponse is null) 37 | { 38 | throw new ArgumentNullException(nameof(httpResponse)); 39 | } 40 | 41 | var result = new SecurityCheckPiplineResult(); 42 | foreach (var securityCheck in SecurityChecks) 43 | { 44 | try 45 | { 46 | var info = securityCheck.Check(httpResponse); 47 | result.Add(securityCheck, info); 48 | } 49 | catch (Exception ex) 50 | { 51 | result.Add(securityCheck, ex); 52 | } 53 | } 54 | return result; 55 | } 56 | 57 | /// 58 | /// Runs a scan. 59 | /// 60 | /// The target uri. 61 | /// Before the request is sent. 62 | /// After the request is sent. 63 | /// The results from the scan. 64 | public SecurityCheckPiplineResult Run(Uri uri, Action beforeRequest = null, Action afterRequest =null) 65 | { 66 | UriHelper.ValidateUri(uri); 67 | var startTime = DateTimeOffset.Now; 68 | using (var request = new HttpRequestMessage(HttpMethod.Get, uri)) 69 | { 70 | beforeRequest?.Invoke(request); 71 | using (var response = HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) 72 | .ConfigureAwait(false) 73 | .GetAwaiter() 74 | .GetResult()) 75 | { 76 | afterRequest?.Invoke(response); 77 | 78 | var result = Analyze(response); 79 | result.DateTime = startTime; 80 | result.Url = uri.ToString(); 81 | return result; 82 | } 83 | } 84 | } 85 | 86 | 87 | private HttpClient CreateHttpClient() 88 | { 89 | var httpClientHandler = new HttpClientHandler 90 | { 91 | CheckCertificateRevocationList = true, 92 | ClientCertificateOptions = ClientCertificateOption.Automatic, 93 | ServerCertificateCustomValidationCallback = 94 | (HttpRequestMessage httpRequestMessage, 95 | X509Certificate2 certificate, 96 | X509Chain chain, 97 | SslPolicyErrors sslPolicyErrors) => 98 | { 99 | httpRequestMessage.Properties[nameof(ServerCertificateInfo)] 100 | = CreateServerCertificateValidationInfo(certificate, chain, sslPolicyErrors); 101 | return true; 102 | } 103 | }; 104 | return new HttpClient(httpClientHandler); 105 | } 106 | 107 | private static ServerCertificateInfo CreateServerCertificateValidationInfo(X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) 108 | { 109 | string friendlyName = string.Empty; 110 | if (!(certificate is null)) 111 | if (!string.IsNullOrWhiteSpace(certificate.FriendlyName)) 112 | { 113 | friendlyName = certificate.FriendlyName; 114 | } 115 | else 116 | { 117 | friendlyName = certificate.Subject; 118 | } 119 | return new ServerCertificateInfo(friendlyName, chain, sslPolicyErrors); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/Services/UriHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CodeTherapy.HttpSecurityChecks.Services 4 | { 5 | public sealed class UriHelper 6 | { 7 | public bool TryCreateUri(string uriString, out Uri uri, out string message) 8 | { 9 | message = null; 10 | if (Uri.TryCreate(uriString, UriKind.Absolute, out uri)) 11 | { 12 | return ValidateUri(uri, out message); 13 | } 14 | else 15 | { 16 | message = $"{uriString ?? "null"} is not a valid absolute uri."; 17 | } 18 | return false; 19 | } 20 | 21 | public static void ValidateUri(Uri uri) 22 | { 23 | if (uri is null) 24 | { 25 | throw new ArgumentNullException(nameof(uri)); 26 | } 27 | 28 | if (!ValidateUri(uri, out string message)) 29 | { 30 | throw new ArgumentException(message); 31 | } 32 | } 33 | 34 | public static bool ValidateUri(Uri uri, out string message) 35 | { 36 | message = null; 37 | if (!uri.IsAbsoluteUri) 38 | { 39 | message = "The uri cannot be a relative uri."; 40 | return false; 41 | } 42 | if (uri.IsFile) 43 | { 44 | message = "The uri cannot be a file."; 45 | return false; 46 | } 47 | if (uri.IsUnc) 48 | { 49 | message = "The uri cannot be a UNC path."; 50 | return false; 51 | } 52 | return true; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/SingleHeaderValueSecurityCheck.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Net.Http.Headers; 3 | using CodeTherapy.HttpSecurityChecks.Core; 4 | using CodeTherapy.HttpSecurityChecks.Data; 5 | 6 | namespace CodeTherapy.HttpSecurityChecks 7 | { 8 | public abstract class SingleHeaderValueSecurityCheck : HttpHeaderCheckBase 9 | { 10 | 11 | protected override SecurityCheckResult CheckHeader(HttpResponseHeaders headers) 12 | { 13 | if (headers.TryGetHeaderValue(HeaderName, out string value)) 14 | { 15 | if (!string.IsNullOrWhiteSpace(value)) 16 | { 17 | return CheckHeaderValue(value); 18 | } 19 | } 20 | return CreateForMissingHeader(); 21 | } 22 | 23 | protected abstract SecurityCheckResult CheckHeaderValue(string value); 24 | 25 | protected abstract SecurityCheckResult CreateForMissingHeader(); 26 | 27 | 28 | } 29 | 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/StrictTransportSecuritySecurityHeaderCheck.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using CodeTherapy.HttpSecurityChecks.Core; 3 | using CodeTherapy.HttpSecurityChecks.Data; 4 | 5 | namespace CodeTherapy.HttpSecurityChecks 6 | { 7 | public sealed class StrictTransportSecuritySecurityHeaderCheck : MissingHeaderValueSecurityCheckBase 8 | { 9 | public override string Name => HeaderName; 10 | 11 | public override string HeaderName => "Strict-Transport-Security"; 12 | 13 | public override string Recommendation => $"Recommended is \"{HeaderName}: max-age=31536000; includeSubDomains\"."; 14 | 15 | public override bool HttpsOnly => true; 16 | 17 | public override IReadOnlyCollection HeaderValueChecks => new[] 18 | { 19 | HeaderValueCheck.IsBest(when: value => value.StartsWithOrdinalIgnoreCase("max-age=") && value.Contains("includeSubDomains")), 20 | HeaderValueCheck.IsGood(when: value => value.StartsWithOrdinalIgnoreCase("max-age=") && value.Contains("preload"), recommandation: $".The optional value 'preload' is not part of the HSTS specification and should not be treated as official. {Recommendation}"), 21 | HeaderValueCheck.IsGood(when: value => value.StartsWithOrdinalIgnoreCase("max-age="), recommandation: $"All sub domains should be included. {Recommendation}"), 22 | }; 23 | } 24 | 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/XContentTypeOptionsSecurityHeaderCheck.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using CodeTherapy.HttpSecurityChecks.Core; 3 | using CodeTherapy.HttpSecurityChecks.Data; 4 | 5 | namespace CodeTherapy.HttpSecurityChecks 6 | { 7 | public sealed class XContentTypeOptionsSecurityHeaderCheck : MissingHeaderValueSecurityCheckBase 8 | { 9 | public override string Name => HeaderName; 10 | 11 | public override string HeaderName => "X-Content-Type-Options"; 12 | 13 | public override string Recommendation => $"Recommended is \"{HeaderName}: nosniff\"."; 14 | 15 | public override IReadOnlyCollection HeaderValueChecks => new[] 16 | { 17 | HeaderValueCheck.IsBest(when: value => value.EqualsOrdinalIgnoreCase("nosniff")), 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/XFrameOptionsSecurityHeaderCheck.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using CodeTherapy.HttpSecurityChecks.Core; 3 | using CodeTherapy.HttpSecurityChecks.Data; 4 | 5 | namespace CodeTherapy.HttpSecurityChecks 6 | { 7 | public sealed class XFrameOptionsSecurityHeaderCheck : MissingHeaderValueSecurityCheckBase 8 | { 9 | public override string Name => HeaderName; 10 | 11 | public override string HeaderName => "X-Frame-Options"; 12 | 13 | public override string Recommendation => $"Recommended is \"{HeaderName}: deny\"."; 14 | 15 | public override IReadOnlyCollection HeaderValueChecks => new[] 16 | { 17 | HeaderValueCheck.IsBest(when: value => value.EqualsOrdinalIgnoreCase("deny")), 18 | HeaderValueCheck.IsGood(when: value => value.EqualsOrdinalIgnoreCase("sameorigin"), recommandation: $"The page can only be displayed in a frame on the same origin as the page itself. Some browsers do not ensure that all ancestors are also in the same origin. {Recommendation}"), 19 | HeaderValueCheck.IsGood(when: value => value.StartsWithOrdinalIgnoreCase("allow-from"), recommandation: $"All ancestors are also in the same origin. Some browsers do not ensure that all ancestors are also in the same origin. {Recommendation}"), 20 | }; 21 | } 22 | 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/XPoweredByDisclosureHeaderSecurityCheck.cs: -------------------------------------------------------------------------------- 1 | namespace CodeTherapy.HttpSecurityChecks 2 | { 3 | public sealed class XPoweredByDisclosureHeaderSecurityCheck : DisclosureHeaderValueSecurityCheck 4 | { 5 | public override string Name => HeaderName; 6 | 7 | public override string HeaderName => "X-Powered-By"; 8 | 9 | public override string Recommendation => "Technology information should be removed."; 10 | } 11 | 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/CodeTherapy.HttpSecurityCheck/XXSSProtectionSecurityHeaderCheck.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using CodeTherapy.HttpSecurityChecks.Core; 4 | using CodeTherapy.HttpSecurityChecks.Data; 5 | 6 | namespace CodeTherapy.HttpSecurityChecks 7 | { 8 | public sealed class XXSSProtectionSecurityHeaderCheck : MissingHeaderValueSecurityCheckBase, IEquatable 9 | { 10 | public override string Name => HeaderName; 11 | 12 | public override string HeaderName => "X-XSS-Protection"; 13 | 14 | public override string Recommendation => "Recommended is \"X-XSS-Protection: 1;\"."; 15 | 16 | public override IReadOnlyCollection HeaderValueChecks => new[] 17 | { 18 | HeaderValueCheck.IsBest(when: value => value.EqualsOrdinalIgnoreCase("1; mode=block")), 19 | HeaderValueCheck.IsGood(when: value => value.EqualsOrdinalIgnoreCase("1"), recommandation: $"By abusing false-positives, attackers can selectively disable innocent scripts on the page. {Recommendation}"), 20 | HeaderValueCheck.IsBad(when: value => value.StartsWithOrdinalIgnoreCase("1; report="), recommandation: $"This is only supported on Chromium. By abusing false-positives, attackers can selectively disable innocent scripts on the page. {Recommendation}"), 21 | HeaderValueCheck.IsBad(when: value => value.EqualsOrdinalIgnoreCase("0"), recommandation: $"Value 0 disables XSS filtering. {Recommendation}") 22 | }; 23 | 24 | public bool Equals(XXSSProtectionSecurityHeaderCheck other) 25 | { 26 | return base.Equals(other); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/DotnetHttpSecurityCheck/Command.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Report; 2 | using CodeTherapy.HttpSecurityChecks.Services; 3 | using DotnetHttpSecurityCheck.Report; 4 | using McMaster.Extensions.CommandLineUtils; 5 | using System; 6 | using System.ComponentModel.DataAnnotations; 7 | using System.IO; 8 | using System.Net.Http; 9 | using System.Reflection; 10 | 11 | namespace DotnetHttpSecurityCheck 12 | { 13 | [Command( 14 | Name = "http-security-check", 15 | Description = "A dotnet tool to do a http request/response security assessment.")] 16 | [HelpOption()] 17 | [VersionOptionFromMember(MemberName = nameof(GetVersion))] 18 | public sealed class HttpSecuityCheckCommand 19 | { 20 | 21 | public HttpSecuityCheckCommand(ISecurityCheckPipeline securityCheckPipline, IReporter reporter, UriHelper uriHelper) 22 | { 23 | SecurityCheckPipeline = securityCheckPipline ?? throw new ArgumentNullException(nameof(securityCheckPipline)); 24 | Reporter = reporter ?? throw new ArgumentNullException(nameof(reporter)); 25 | UriHelper = uriHelper ?? throw new ArgumentNullException(nameof(uriHelper)); 26 | } 27 | 28 | [Argument(0, Description = "A absolute URL for the security assessment (required).")] 29 | [Required(AllowEmptyStrings = false)] 30 | public (bool HasValue, Uri Value) Url { get; set; } 31 | 32 | [Option(CommandOptionType.SingleOrNoValue, 33 | Template = "-o|--output ", 34 | Description = "The report output file path (optional).")] 35 | [LegalFilePath()] 36 | public (bool HasValue, string Value) ReportOutput { get; set; } 37 | 38 | [Option(CommandOptionType.SingleOrNoValue, 39 | Template = "-f|--format ", 40 | Description = "Format of the report (optional). Default is Text.")] 41 | public (bool HasValue, ReportFormat Value) ReportFormat { get; set; } 42 | 43 | [Option(CommandOptionType.SingleValue, 44 | Template = "-v|--verbose ", 45 | Description = "Set the console verbosity level (optional). Default is normal. Allowed values are n[normal], q[uiet], d[etailed].")] 46 | public VerbosityLevel Verbosity { get; set; } 47 | 48 | private IReporter Reporter { get; } 49 | 50 | private ISecurityCheckPipeline SecurityCheckPipeline { get; } 51 | 52 | private UriHelper UriHelper { get; } 53 | 54 | public int OnExecute() 55 | { 56 | if (ReportFormat.HasValue) 57 | { 58 | if (!ReportOutput.HasValue) 59 | { 60 | Reporter.Error("Option 'output' is required when the report format is not the console."); 61 | return 1; 62 | } 63 | } 64 | 65 | if (Url.HasValue) 66 | { 67 | Uri uri = Url.Value; 68 | string message; 69 | if (UriHelper.ValidateUri(uri, out message)) 70 | { 71 | Reporter.Verbose($"Starting assessment for {uri.AbsoluteUri}."); 72 | var result = SecurityCheckPipeline.Run(uri, afterRequest: OnAfterRequest); 73 | var securityCheckReportWriter = CreateReportWriter(); 74 | 75 | Reporter.Verbose("Generating report."); 76 | securityCheckReportWriter.Write(result); 77 | if (ReportOutput.HasValue && ReportFormat.HasValue) 78 | { 79 | Reporter.Output($"Report written to '{Path.GetFullPath(ReportOutput.Value)}'."); 80 | } 81 | return 0; 82 | } 83 | else 84 | { 85 | Reporter.Error(message); 86 | } 87 | } 88 | 89 | return 1; 90 | } 91 | 92 | 93 | private void OnAfterRequest(HttpResponseMessage response) 94 | { 95 | var request = response.RequestMessage; 96 | var message = $"GET {request.RequestUri.AbsoluteUri} {(int)response.StatusCode}: {response.ReasonPhrase}"; 97 | if (response.IsSuccessStatusCode) 98 | { 99 | Reporter.Output(message); 100 | } 101 | else 102 | { 103 | Reporter.Error(message); 104 | } 105 | } 106 | 107 | private IHttpSecurityCheckReportWriter CreateReportWriter() 108 | { 109 | if (ReportOutput.HasValue) 110 | { 111 | var filePath = ReportOutput.Value; 112 | switch (ReportFormat.Value) 113 | { 114 | case DotnetHttpSecurityCheck.ReportFormat.Json: 115 | return new JsonHttpSecurityCheckReportWriter(filePath); 116 | case DotnetHttpSecurityCheck.ReportFormat.Text: 117 | default: 118 | return new TextHttpSecurityCheckReportWriter(filePath); 119 | } 120 | } 121 | return new ConsoleSecurityCheckReportWriter(PhysicalConsole.Singleton); 122 | } 123 | 124 | 125 | private static string GetVersion() => typeof(Program) 126 | .Assembly 127 | .GetCustomAttribute() 128 | .InformationalVersion; 129 | 130 | 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/DotnetHttpSecurityCheck/ConsoleExtensions.cs: -------------------------------------------------------------------------------- 1 | using McMaster.Extensions.CommandLineUtils; 2 | using System; 3 | 4 | namespace DotnetHttpSecurityCheck 5 | { 6 | public static class ConsoleExtensions 7 | { 8 | 9 | public static void Write(this IConsole console, string value, ConsoleColor color) 10 | { 11 | WriteInternal(console, color, () => console.Write(value)); 12 | } 13 | 14 | public static void WriteLine(this IConsole console, string value, ConsoleColor color) 15 | { 16 | WriteInternal(console, color, () => console.WriteLine(value)); 17 | } 18 | public static void WriteIntent(this IConsole console, int intent = 2) 19 | { 20 | console.Write(new string(' ', intent)); 21 | } 22 | 23 | public static void Write(this IConsole console, string value, ConsoleColor? color) 24 | { 25 | ConsoleColor? tempColor = null; 26 | if (color.HasValue) 27 | { 28 | tempColor = console.ForegroundColor; 29 | console.ForegroundColor = color.Value; 30 | } 31 | console.Write(value); 32 | 33 | if (tempColor.HasValue) 34 | { 35 | console.ForegroundColor = tempColor.Value; 36 | } 37 | } 38 | 39 | public static void WriteInternal(IConsole console, ConsoleColor foregroundColor, Action action) 40 | { 41 | ConsoleColor tempColor = console.ForegroundColor; 42 | console.ForegroundColor = foregroundColor; 43 | action(); 44 | console.ForegroundColor = tempColor; 45 | } 46 | 47 | 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/DotnetHttpSecurityCheck/DotnetHttpSecurityCheck.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Exe 10 | netcoreapp2.2 11 | 12 | 13 | 14 | dotnet-http-security-check 15 | true 16 | true 17 | dotnet-http-security-check 18 | A dotnet tool to do a http request/response security assessment. 19 | CodeTherapist 20 | HTTP, Tool, Security, Headers 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/DotnetHttpSecurityCheck/Program.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks; 2 | using CodeTherapy.HttpSecurityChecks.Services; 3 | using McMaster.Extensions.CommandLineUtils; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using System; 6 | using System.Linq; 7 | using System.Text; 8 | 9 | namespace DotnetHttpSecurityCheck 10 | { 11 | internal sealed class Program 12 | { 13 | static Program() 14 | { 15 | } 16 | 17 | public static int Main(string[] args) 18 | { 19 | using (var services = new ServiceCollection() 20 | .AddSingleton() 21 | .AddSingleton(provider => new ConsoleReporter(provider.GetService())) 22 | .AddSingleton() 23 | .AddSingleton() 24 | 25 | .AddSingleton() 26 | .AddSingleton() 27 | .AddSingleton() 28 | .AddSingleton() 29 | .AddSingleton() 30 | .AddSingleton() 31 | 32 | .AddSingleton() 33 | .AddSingleton() 34 | 35 | .AddSingleton() 36 | .AddSingleton() 37 | .BuildServiceProvider()) 38 | { 39 | var app = new CommandLineApplication(); 40 | 41 | app.Conventions 42 | .UseDefaultConventions() 43 | .UseConstructorInjection(services); 44 | 45 | app.ThrowOnUnexpectedArgument = false; 46 | app.ExtendedHelpText = GetExtendedHelpText(services); 47 | 48 | return app.Execute(args); 49 | } 50 | } 51 | 52 | private static string GetExtendedHelpText(IServiceProvider serviceProvider) 53 | { 54 | var checks = serviceProvider.GetServices(); 55 | 56 | if (checks.Any()) 57 | { 58 | var stringBuilder = new StringBuilder(); 59 | stringBuilder 60 | .AppendLine() 61 | .AppendLine("All Security Checks:") 62 | .AppendLine(); 63 | 64 | foreach (var group in checks.GroupBy(c => c.Category)) 65 | { 66 | stringBuilder.AppendLine($" > {group.Key}").AppendLine(); 67 | var i = 0; 68 | foreach (var item in group) 69 | { 70 | i++; 71 | var s = $" {i}# "; 72 | stringBuilder.Append(s).AppendLine($"{item.Name}: {item.Description}"); 73 | stringBuilder.Append(new string(' ', s.Length)).AppendLine($"{item.Recommendation}").AppendLine(); 74 | } 75 | } 76 | 77 | return stringBuilder.ToString(); 78 | } 79 | return string.Empty; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/DotnetHttpSecurityCheck/Report/ConsoleSecurityCheckReportWriter.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Data; 2 | using CodeTherapy.HttpSecurityChecks.Report; 3 | using McMaster.Extensions.CommandLineUtils; 4 | using System; 5 | using System.Linq; 6 | 7 | namespace DotnetHttpSecurityCheck 8 | { 9 | public sealed class ConsoleSecurityCheckReportWriter : HttpSecurityCheckReportWriterBase 10 | { 11 | public ConsoleSecurityCheckReportWriter(IConsole console) 12 | { 13 | Console = console ?? throw new ArgumentNullException(nameof(console)); 14 | } 15 | 16 | private IConsole Console { get; } 17 | 18 | protected override void WriteCore(SecurityCheckPiplineResult securityCheckExecutionResult) 19 | { 20 | Console.WriteLine(); 21 | Console.WriteIntent(2); 22 | Console.WriteLine("Report Summary"); 23 | 24 | var checkNameColumnSize = securityCheckExecutionResult.Max(s => s.SecurityCheck.Name.Length); 25 | var valueColumnSize = securityCheckExecutionResult.Max(s => s.SecurityCheckResult.Value.Length); 26 | 27 | var groupedByType = securityCheckExecutionResult.GroupBy(r => r.SecurityCheck.Category); 28 | 29 | foreach (var group in groupedByType) 30 | { 31 | Console.WriteLine(); 32 | Console.WriteIntent(4); 33 | Console.WriteLine($"> {group.Key} checks"); 34 | Console.WriteLine(); 35 | foreach (var item in group) 36 | { 37 | Write(item, valueColumnSize, checkNameColumnSize); 38 | } 39 | } 40 | } 41 | 42 | private void Write(SecurityCheckExecutionResult executionResult, int typeIntent, int nameIntent) 43 | { 44 | if (executionResult is null) 45 | { 46 | throw new ArgumentNullException(nameof(executionResult)); 47 | } 48 | 49 | Console.WriteIntent(8); 50 | Console.WriteLine($"Check {executionResult.SecurityCheck.Name.PadRight(nameIntent)} : {executionResult.SecurityCheckResult.Value}"); 51 | Console.WriteIntent(8); 52 | if (!executionResult.HasError) 53 | { 54 | var (Text, Color) = GetTextAndColor(executionResult.SecurityCheckResult.State); 55 | Console.WriteIntent(6 + nameIntent); 56 | Console.Write($" -> {Text}", Color); 57 | if (executionResult.SecurityCheckResult.HasRecommandation) 58 | { 59 | Console.Write($": {executionResult.SecurityCheckResult.Recommandation}", ConsoleColor.White); 60 | } 61 | } 62 | else 63 | { 64 | Console.Write($"Exception occured! {Environment.NewLine}{executionResult.Exception.ToString()}", ConsoleColor.Yellow); 65 | } 66 | 67 | Console.WriteLine(); 68 | Console.WriteLine(); 69 | } 70 | 71 | private (string Text, ConsoleColor Color) GetTextAndColor(SecurityCheckState securityCheckState) 72 | { 73 | switch (securityCheckState) 74 | { 75 | case SecurityCheckState.None: 76 | return (GetText(securityCheckState), Console.ForegroundColor); 77 | case SecurityCheckState.Skipped: 78 | return (GetText(securityCheckState), ConsoleColor.Blue); 79 | case SecurityCheckState.Bad: 80 | return (GetText(securityCheckState), ConsoleColor.Red); 81 | case SecurityCheckState.Good: 82 | return (GetText(securityCheckState), ConsoleColor.Green); 83 | case SecurityCheckState.Best: 84 | return (GetText(securityCheckState), ConsoleColor.Green); 85 | default: 86 | throw new ArgumentOutOfRangeException(); 87 | } 88 | } 89 | 90 | } 91 | } -------------------------------------------------------------------------------- /src/DotnetHttpSecurityCheck/Report/JsonHttpSecurityCheckReportWriter.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Data; 2 | using CodeTherapy.HttpSecurityChecks.Report; 3 | using Newtonsoft.Json; 4 | using System; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Text; 8 | 9 | namespace DotnetHttpSecurityCheck.Report 10 | { 11 | public sealed class JsonHttpSecurityCheckReportWriter : HttpSecurityCheckReportWriterBase 12 | { 13 | private readonly string _path; 14 | 15 | public JsonHttpSecurityCheckReportWriter(string path) 16 | { 17 | if (string.IsNullOrWhiteSpace(path)) 18 | { 19 | throw new ArgumentException("message", nameof(path)); 20 | } 21 | 22 | _path = path; 23 | } 24 | 25 | 26 | protected override void WriteCore(SecurityCheckPiplineResult securityCheckExecutionResults) 27 | { 28 | var obj = JsonConvert.SerializeObject(new 29 | { 30 | Url = securityCheckExecutionResults.Url, 31 | DateTime = securityCheckExecutionResults.DateTime.ToString(), 32 | Results = securityCheckExecutionResults.Select(r => new 33 | { 34 | CheckName = r.SecurityCheck.Name, 35 | Category = r.SecurityCheck.Category, 36 | HttpsOnly = r.SecurityCheck.HttpsOnly, 37 | State = GetText(r.SecurityCheckResult.State), 38 | Recommandation = r.SecurityCheckResult.Recommandation, 39 | Value = r.SecurityCheckResult.Value, 40 | HasError = r.HasError, 41 | Error = r.Exception?.ToString() 42 | }) 43 | }, new JsonSerializerSettings() { 44 | ReferenceLoopHandling = ReferenceLoopHandling.Ignore, 45 | Formatting = Formatting.Indented, 46 | TypeNameHandling = TypeNameHandling.None }); 47 | 48 | File.WriteAllText(_path , obj); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/DotnetHttpSecurityCheck/Report/TextWriterSecurityCheckReportWriter.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Data; 2 | using CodeTherapy.HttpSecurityChecks.Report; 3 | using System; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | 8 | namespace DotnetHttpSecurityCheck 9 | { 10 | public sealed class TextHttpSecurityCheckReportWriter : HttpSecurityCheckReportWriterBase 11 | { 12 | public TextHttpSecurityCheckReportWriter(string path) 13 | : this(() => new StreamWriter(path, append: false, Encoding.UTF8)) 14 | { 15 | 16 | } 17 | 18 | public TextHttpSecurityCheckReportWriter(Func textWriterFactoy) 19 | { 20 | TextWriterFactory = textWriterFactoy ?? throw new ArgumentNullException(nameof(textWriterFactoy)); 21 | } 22 | 23 | private Func TextWriterFactory { get; } 24 | 25 | protected override void WriteCore(SecurityCheckPiplineResult securityCheckExecutionResult) 26 | { 27 | using (var textWriter = TextWriterFactory()) 28 | { 29 | textWriter.WriteLine(securityCheckExecutionResult.Url); 30 | textWriter.WriteLine(securityCheckExecutionResult.DateTime); 31 | textWriter.WriteLine("Report Summary"); 32 | 33 | var maxNameLenght = securityCheckExecutionResult.Max(r => r.SecurityCheck.Name.Length); 34 | var groupedByType = securityCheckExecutionResult.GroupBy(r => r.SecurityCheck.Category); 35 | 36 | foreach (var group in groupedByType) 37 | { 38 | textWriter.WriteLine(); 39 | textWriter.WriteLine($"> {group.Key} checks"); 40 | textWriter.WriteLine(); 41 | foreach (var item in group) 42 | { 43 | Write(textWriter, item, maxNameLenght); 44 | } 45 | } 46 | textWriter.Flush(); 47 | 48 | } 49 | 50 | } 51 | 52 | private void Write(TextWriter textWriter, SecurityCheckExecutionResult executionResult, int nameIntent) 53 | { 54 | if (executionResult is null) 55 | { 56 | throw new ArgumentNullException(nameof(executionResult)); 57 | } 58 | 59 | textWriter.WriteLine($"Check {executionResult.SecurityCheck.Name.PadRight(nameIntent)} : {executionResult.SecurityCheckResult.Value}"); 60 | if (!executionResult.HasError) 61 | { 62 | var text = GetText(executionResult.SecurityCheckResult.State); 63 | textWriter.Write($"-> {text}"); 64 | if (executionResult.SecurityCheckResult.HasRecommandation) 65 | { 66 | textWriter.Write($": {executionResult.SecurityCheckResult.Recommandation}", ConsoleColor.White); 67 | } 68 | } 69 | else 70 | { 71 | textWriter.Write($"Exception occured! {Environment.NewLine}{executionResult.Exception.ToString()}", ConsoleColor.Yellow); 72 | } 73 | 74 | textWriter.WriteLine(); 75 | textWriter.WriteLine(); 76 | } 77 | 78 | 79 | 80 | } 81 | } -------------------------------------------------------------------------------- /src/DotnetHttpSecurityCheck/ReportFormat.cs: -------------------------------------------------------------------------------- 1 | namespace DotnetHttpSecurityCheck 2 | { 3 | /// 4 | /// Defines the report format values. 5 | /// 6 | public enum ReportFormat 7 | { 8 | /// 9 | /// Plain text format. 10 | /// 11 | Text, 12 | /// 13 | /// Json text format 14 | /// 15 | Json 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/DotnetHttpSecurityCheck/VerbosityLevel.cs: -------------------------------------------------------------------------------- 1 | namespace DotnetHttpSecurityCheck 2 | { 3 | public enum VerbosityLevel 4 | { 5 | Normal, 6 | Quiet, 7 | Detailed 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /test/CodeTherapy.HttpSecurityCheck.Tests/CodeTherapy.HttpSecurityCheck.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | net472 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/CodeTherapy.HttpSecurityCheck.Tests/ContentSecurityPolicySecurityHeaderCheckTests.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Data; 2 | using Xunit; 3 | 4 | namespace CodeTherapy.HttpSecurityChecks.Test 5 | { 6 | public class ContentSecurityPolicySecurityHeaderCheckTests : MissingHeaderSecurityCheckTestBase 7 | { 8 | public override string HeaderName => "content-security-policy"; 9 | 10 | [Theory] 11 | [InlineData("default-src 'self'", SecurityCheckState.Best)] 12 | [InlineData("default-src 'self'; script-src https://example.com", SecurityCheckState.Best)] 13 | public void CheckValue(string value, SecurityCheckState securityCheckState) 14 | { 15 | AssertSecurityCheckState(HeaderName, value, securityCheckState); 16 | } 17 | 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/CodeTherapy.HttpSecurityCheck.Tests/DisclosureHeaderSecurityCheckTestBase.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Data; 2 | using Xunit; 3 | 4 | namespace CodeTherapy.HttpSecurityChecks.Test 5 | { 6 | public abstract class DisclosureHeaderSecurityCheckTestBase : HeaderSecurityCheckTestBase where TSecurityCheck : HttpHeaderCheckBase 7 | { 8 | 9 | 10 | [Theory] 11 | [InlineData("Value", SecurityCheckState.Bad)] 12 | public void CheckInvalidValue(string value, SecurityCheckState securityCheckState) 13 | { 14 | AssertSecurityCheckState(HeaderName, value, securityCheckState); 15 | } 16 | 17 | [Fact] 18 | public void CheckNoValue() 19 | { 20 | AssertSecurityCheckStateWhenNoHeaderIsSet(HeaderName, SecurityCheckState.Best); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/CodeTherapy.HttpSecurityCheck.Tests/HeaderSecurityCheckTestBase.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Data; 2 | using System.Net; 3 | using Xunit; 4 | 5 | namespace CodeTherapy.HttpSecurityChecks.Test 6 | { 7 | public abstract class HeaderSecurityCheckTestBase : SecurityCheckTestBase where TSecurityCheck : HttpHeaderCheckBase 8 | { 9 | 10 | public abstract string HeaderName { get; } 11 | 12 | 13 | [Fact] 14 | public void DoesNotThrow() 15 | { 16 | var exception = Record.Exception(() => RunCheck()); 17 | Assert.Null(exception); 18 | } 19 | 20 | [Fact] 21 | public void HeaderNameIsNotNull() 22 | { 23 | var check = CreateSecurityCheck(); 24 | Assert.NotNull(check.HeaderName); 25 | } 26 | 27 | 28 | 29 | protected void AssertSecurityCheckState(string headerName, string value, SecurityCheckState securityCheckState) 30 | { 31 | var securityCheck = CreateSecurityCheck(); 32 | var response = CreateResponseMessage(); 33 | response.Headers.Add(headerName, value); 34 | var result = securityCheck.Check(response); 35 | Assert.True(result.State == securityCheckState, $"{nameof(securityCheckState)}:{result.State}"); 36 | } 37 | 38 | protected void AssertSecurityCheckStateWhenNoHeaderIsSet(string headerName, SecurityCheckState securityCheckState = SecurityCheckState.Bad) 39 | { 40 | var securityCheck = CreateSecurityCheck(); 41 | var response = CreateResponseMessage(); 42 | var result = securityCheck.Check(response); 43 | Assert.True(result.State == securityCheckState, $"{nameof(securityCheckState)}:{result.State}"); 44 | } 45 | 46 | 47 | 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/CodeTherapy.HttpSecurityCheck.Tests/HttpsSecurityHeaderCheckTests.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Data; 2 | using Xunit; 3 | 4 | namespace CodeTherapy.HttpSecurityChecks.Test 5 | { 6 | public class HttpsSecurityHeaderCheckTests : SecurityCheckTestBase 7 | { 8 | 9 | 10 | [Theory] 11 | [InlineData(true, SecurityCheckState.Best)] 12 | [InlineData(false, SecurityCheckState.Bad)] 13 | public void CheckState(bool https, SecurityCheckState expectedSecurityCheckState) 14 | { 15 | var result = RunCheck(https); 16 | Assert.Equal(expectedSecurityCheckState, result.State); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/CodeTherapy.HttpSecurityCheck.Tests/ReferrerPolicySecurityHeaderCheckTests.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Data; 2 | using Xunit; 3 | 4 | namespace CodeTherapy.HttpSecurityChecks.Test 5 | { 6 | public class ReferrerPolicySecurityHeaderCheckTests : MissingHeaderSecurityCheckTestBase 7 | { 8 | public override string HeaderName => "referrer-policy"; 9 | 10 | [Theory] 11 | [InlineData("strict-origin-when-cross-origin", SecurityCheckState.Best)] 12 | [InlineData("strict-origin", SecurityCheckState.Best)] 13 | [InlineData("no-referrer", SecurityCheckState.Best)] 14 | [InlineData("origin-when-cross-origin", SecurityCheckState.Good)] 15 | [InlineData("origin", SecurityCheckState.Good)] 16 | [InlineData("no-referrer-when-downgrade", SecurityCheckState.Good)] 17 | [InlineData("same-origin", SecurityCheckState.Good)] 18 | [InlineData("unsafe-url", SecurityCheckState.Bad)] 19 | public void CheckValidValues(string value, SecurityCheckState securityCheckState) 20 | { 21 | AssertSecurityCheckState(HeaderName, value, securityCheckState); 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /test/CodeTherapy.HttpSecurityCheck.Tests/SecurityCheckExecutionResultTests.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks; 2 | using CodeTherapy.HttpSecurityChecks.Data; 3 | using System; 4 | using System.Net.Http; 5 | using Xunit; 6 | 7 | namespace CodeTherapy.HttpSecurityCheck.Tests 8 | { 9 | public class SecurityCheckExecutionResultTests 10 | { 11 | public void MustThrowWhenSecurityCheckIsNull() 12 | { 13 | var ex = Record.Exception(() => new SecurityCheckExecutionResult(securityCheck: null, SecurityCheckResult.Empty)); 14 | 15 | Assert.NotNull(ex); 16 | Assert.IsType(ex); 17 | } 18 | 19 | public void MustThrowWhenSecurityCheckResultIsNull() 20 | { 21 | var ex = Record.Exception(() => new SecurityCheckExecutionResult(new FakeSecurityCheck(), null)); 22 | Assert.NotNull(ex); 23 | Assert.IsType(ex); 24 | } 25 | 26 | private class FakeSecurityCheck : ISecurityCheck 27 | { 28 | public string Name { get; } 29 | public string Description { get; } 30 | public string Category { get; } 31 | public string Recommendation { get; } 32 | public bool HttpsOnly { get; } 33 | 34 | public SecurityCheckResult Check(HttpResponseMessage httpRequestMessage) 35 | { 36 | throw new NotImplementedException(); 37 | } 38 | } 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/CodeTherapy.HttpSecurityCheck.Tests/SecurityCheckPipelineTests.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Data; 2 | using CodeTherapy.HttpSecurityChecks.Services; 3 | using System; 4 | using System.Net.Http; 5 | using Xunit; 6 | 7 | namespace CodeTherapy.HttpSecurityChecks.Test 8 | { 9 | public class SecurityCheckPipelineTests 10 | { 11 | 12 | [Fact] 13 | public void RunDoesNotAcceptNull() 14 | { 15 | var pipeline = new SecurityCheckPipeline(Array.Empty()); 16 | 17 | Assert.Throws(() => pipeline.Run(null)); 18 | } 19 | 20 | [Fact] 21 | public void AnalyzeDoesNotAcceptNull() 22 | { 23 | var pipeline = new SecurityCheckPipeline(Array.Empty()); 24 | Assert.Throws(() => pipeline.Analyze(null)); 25 | } 26 | 27 | [Fact] 28 | public void NoSecurityChecksDoesNotThrow() 29 | { 30 | var pipeline = new SecurityCheckPipeline(Array.Empty()); 31 | 32 | HttpResponseMessage response = CreateResponse(); 33 | 34 | var result = pipeline.Analyze(response); 35 | Assert.DoesNotContain(result, r => r.HasError); 36 | } 37 | 38 | private static HttpResponseMessage CreateResponse() 39 | { 40 | var request = new HttpRequestMessage(HttpMethod.Get, "http://www.example.com"); 41 | var response = new HttpResponseMessage() { RequestMessage = request }; 42 | return response; 43 | } 44 | 45 | [Fact] 46 | public void HasExceptionFromSecurrityCheck() 47 | { 48 | var pipeline = new SecurityCheckPipeline(new[] { new ActionSecurityCheck((e) => throw new Exception())}); 49 | var response = CreateResponse(); 50 | 51 | var result = pipeline.Analyze(response); 52 | Assert.Contains(result, r => r.HasError); 53 | } 54 | 55 | 56 | [Theory] 57 | [InlineData("file:///c:/path/to/the%20file.txt")] 58 | [InlineData("/relative/uri")] 59 | [InlineData(@"\\Unc\path\")] 60 | public void UriCannotBeAFile(string uri) 61 | { 62 | var pipeline = new SecurityCheckPipeline(Array.Empty()); 63 | var ex = Record.Exception(() => pipeline.Run(new Uri(uri, UriKind.RelativeOrAbsolute))); 64 | Assert.NotNull(ex); 65 | Assert.IsType(ex); 66 | } 67 | 68 | 69 | private class ActionSecurityCheck : ISecurityCheck 70 | { 71 | private readonly Func _check; 72 | 73 | public ActionSecurityCheck(Func check) 74 | { 75 | _check = check ?? throw new ArgumentNullException(nameof(check)); 76 | } 77 | 78 | public string Name => "Name"; 79 | public string Description => "Description"; 80 | public string Category => "Category"; 81 | public string Recommendation => "Recommendation"; 82 | public bool HttpsOnly => false; 83 | 84 | public SecurityCheckResult Check(HttpResponseMessage httpRequestMessage) 85 | { 86 | return _check(httpRequestMessage); 87 | } 88 | } 89 | 90 | 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /test/CodeTherapy.HttpSecurityCheck.Tests/SecurityCheckTestBase.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Data; 2 | using System; 3 | using System.Net; 4 | using System.Net.Http; 5 | using Xunit; 6 | 7 | namespace CodeTherapy.HttpSecurityChecks.Test 8 | { 9 | public abstract class SecurityCheckTestBase where TSecurityCheck : class, ISecurityCheck 10 | { 11 | protected TSecurityCheck CreateSecurityCheck() 12 | { 13 | return Activator.CreateInstance(); 14 | } 15 | 16 | 17 | 18 | [Fact] 19 | public void RecommendationIsNotNull() 20 | { 21 | var check = CreateSecurityCheck(); 22 | Assert.NotNull(check.Recommendation); 23 | } 24 | 25 | [Fact] 26 | public void NameIsNotNull() 27 | { 28 | var check = CreateSecurityCheck(); 29 | Assert.NotNull(check.Name); 30 | } 31 | 32 | 33 | [Fact] 34 | public void CategoryIsNotNull() 35 | { 36 | var check = CreateSecurityCheck(); 37 | Assert.NotNull(check.Category); 38 | } 39 | 40 | 41 | [Fact] 42 | public void DescriptionIsNotNull() 43 | { 44 | var check = CreateSecurityCheck(); 45 | Assert.NotNull(check.Description); 46 | } 47 | 48 | 49 | [Fact] 50 | public void CheckIsEquals() 51 | { 52 | var check1 = CreateSecurityCheck(); 53 | var check2 = CreateSecurityCheck(); 54 | 55 | Assert.NotSame(check1, check2); 56 | Assert.Equal(check1, check2); 57 | } 58 | 59 | [Fact] 60 | public void NullEquality() 61 | { 62 | var check1 = CreateSecurityCheck(); 63 | Assert.False(check1.Equals(null)); 64 | } 65 | 66 | [Fact] 67 | public void RefenceEquality() 68 | { 69 | var check1 = CreateSecurityCheck(); 70 | var check2 = check1; 71 | Assert.True(check1.Equals(check2)); 72 | } 73 | 74 | 75 | [Fact] 76 | public void ThrowsWhenParameterIsNull() 77 | { 78 | var check = CreateSecurityCheck(); 79 | 80 | 81 | var exception = Record.Exception(() => check.Check(null)); 82 | 83 | Assert.NotNull(exception); 84 | Assert.IsType(exception); 85 | } 86 | 87 | 88 | 89 | protected SecurityCheckResult RunCheck(bool https = true) 90 | { 91 | var response = CreateResponseMessage(https); 92 | return RunCheck(response); 93 | } 94 | 95 | protected SecurityCheckResult RunCheck(HttpResponseMessage httpResponseMessage) 96 | { 97 | var securityCheck = CreateSecurityCheck(); 98 | var result = securityCheck.Check(httpResponseMessage); 99 | return result; 100 | } 101 | 102 | 103 | protected HttpResponseMessage CreateResponseMessage(bool https = true) 104 | { 105 | var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK) 106 | { 107 | RequestMessage = new HttpRequestMessage(HttpMethod.Get, https ? "https://example.com" : "http://example.com") 108 | }; 109 | 110 | return httpResponseMessage; 111 | } 112 | 113 | 114 | } 115 | 116 | 117 | 118 | } 119 | -------------------------------------------------------------------------------- /test/CodeTherapy.HttpSecurityCheck.Tests/SecurityHeaderCheckBaseTests.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Data; 2 | using Xunit; 3 | 4 | namespace CodeTherapy.HttpSecurityChecks.Test 5 | { 6 | public abstract class MissingHeaderSecurityCheckTestBase : HeaderSecurityCheckTestBase where TSecurityCheck : HttpHeaderCheckBase 7 | { 8 | 9 | [Theory] 10 | [InlineData("", SecurityCheckState.Bad)] 11 | [InlineData("A invalid value", SecurityCheckState.Bad)] 12 | public void CheckInvalidValue(string value, SecurityCheckState securityCheckState) 13 | { 14 | AssertSecurityCheckState(HeaderName, value, securityCheckState); 15 | } 16 | 17 | [Fact] 18 | public void CheckNoValue() 19 | { 20 | AssertSecurityCheckStateWhenNoHeaderIsSet(HeaderName, SecurityCheckState.Bad); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/CodeTherapy.HttpSecurityCheck.Tests/ServerCertificateSecurityHeaderCheckTests.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Data; 2 | using CodeTherapy.HttpSecurityChecks.Services; 3 | using System; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Net.Security; 7 | using System.Security.Cryptography.X509Certificates; 8 | using Xunit; 9 | 10 | namespace CodeTherapy.HttpSecurityChecks.Test 11 | { 12 | public class ServerCertificateSecurityHeaderCheckTests : SecurityCheckTestBase 13 | { 14 | 15 | 16 | [Theory] 17 | [InlineData(SslPolicyErrors.None, SecurityCheckState.Best)] 18 | [InlineData(SslPolicyErrors.RemoteCertificateChainErrors, SecurityCheckState.Bad)] 19 | [InlineData(SslPolicyErrors.RemoteCertificateNameMismatch, SecurityCheckState.Bad)] 20 | [InlineData(SslPolicyErrors.RemoteCertificateNotAvailable, SecurityCheckState.Bad)] 21 | public void NoCertificateError(SslPolicyErrors sslPolicyErrors, SecurityCheckState expected) 22 | { 23 | var response = CreateResponseMessage(); 24 | AddServerCertificateInfo(response, sslPolicyErrors); 25 | 26 | var result = RunCheck(response); 27 | Assert.Equal(expected, result.State); 28 | } 29 | 30 | private static void AddServerCertificateInfo(HttpResponseMessage response, SslPolicyErrors sslPolicyErrors) 31 | { 32 | response.RequestMessage.Properties[nameof(ServerCertificateInfo)] 33 | = new ServerCertificateInfo("X509CertificateName", 34 | X509Chain.Create(), 35 | sslPolicyErrors); 36 | } 37 | 38 | 39 | [Theory, Trait("Category", "Integration")] 40 | [InlineData("https://expired.badssl.com/", SecurityCheckState.Bad)] 41 | [InlineData("https://wrong.host.badssl.com/", SecurityCheckState.Bad)] 42 | [InlineData("https://self-signed.badssl.com/", SecurityCheckState.Bad)] 43 | [InlineData("https://untrusted-root.badssl.com/", SecurityCheckState.Bad)] 44 | [InlineData("https://revoked.badssl.com/", SecurityCheckState.Bad)] 45 | public void BadSSLTests(string uri, SecurityCheckState expected) 46 | { 47 | var pipeline = new SecurityCheckPipeline(new[] { CreateSecurityCheck() }); 48 | var result = pipeline.Run(new System.Uri(uri)); 49 | Assert.Equal(expected, result.First().SecurityCheckResult.State); 50 | } 51 | 52 | [Fact] 53 | public void NoServerCertificateInfoThrowsException() 54 | { 55 | var response = CreateResponseMessage(); 56 | var ex = Record.Exception(() => RunCheck(response)); 57 | Assert.NotNull(ex); 58 | Assert.IsType(ex); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/CodeTherapy.HttpSecurityCheck.Tests/ServerDisclosureHeaderSecurityCheckTests.cs: -------------------------------------------------------------------------------- 1 | namespace CodeTherapy.HttpSecurityChecks.Test 2 | { 3 | public class ServerDisclosureHeaderSecurityCheckTests : DisclosureHeaderSecurityCheckTestBase 4 | { 5 | public override string HeaderName => "server"; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/CodeTherapy.HttpSecurityCheck.Tests/StrictTransportSecuritySecurityHeaderCheckTests.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Data; 2 | using Xunit; 3 | 4 | namespace CodeTherapy.HttpSecurityChecks.Test 5 | { 6 | public class StrictTransportSecuritySecurityHeaderCheckTests : MissingHeaderSecurityCheckTestBase 7 | { 8 | public override string HeaderName => "strict-transport-security"; 9 | 10 | [Theory] 11 | [InlineData("max-age=31536000; includeSubDomains", SecurityCheckState.Best)] 12 | [InlineData("max-age=31536000", SecurityCheckState.Good)] 13 | [InlineData("max-age=31536000; preload", SecurityCheckState.Good)] 14 | public void CheckValidValues(string value, SecurityCheckState securityCheckState) 15 | { 16 | AssertSecurityCheckState(HeaderName, value, securityCheckState); 17 | } 18 | 19 | } 20 | 21 | 22 | } 23 | -------------------------------------------------------------------------------- /test/CodeTherapy.HttpSecurityCheck.Tests/XContentTypeOptionsSecurityHeaderCheckTests.cs: -------------------------------------------------------------------------------- 1 | namespace CodeTherapy.HttpSecurityChecks.Test 2 | { 3 | public class XContentTypeOptionsSecurityHeaderCheckTests : MissingHeaderSecurityCheckTestBase 4 | { 5 | public override string HeaderName => "x-content-type-options"; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/CodeTherapy.HttpSecurityCheck.Tests/XFrameOptionsSecurityHeaderCheckTests.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Data; 2 | using Xunit; 3 | 4 | namespace CodeTherapy.HttpSecurityChecks.Test 5 | { 6 | public class XFrameOptionsSecurityHeaderCheckTests : MissingHeaderSecurityCheckTestBase 7 | { 8 | public override string HeaderName => "x-frame-options"; 9 | 10 | [Theory] 11 | [InlineData("deny", SecurityCheckState.Best)] 12 | [InlineData("sameorigin", SecurityCheckState.Good)] 13 | [InlineData("allow-from https://example.com/", SecurityCheckState.Good)] 14 | [InlineData("", SecurityCheckState.Bad)] 15 | [InlineData("A invalid value", SecurityCheckState.Bad)] 16 | public void CheckValue(string value, SecurityCheckState securityCheckState) 17 | { 18 | AssertSecurityCheckState(HeaderName, value, securityCheckState); 19 | } 20 | 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/CodeTherapy.HttpSecurityCheck.Tests/XPoweredByDisclosureHeaderSecurityCheckTests.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Data; 2 | using Xunit; 3 | 4 | namespace CodeTherapy.HttpSecurityChecks.Test 5 | { 6 | public class XPoweredByDisclosureHeaderSecurityCheckTests : DisclosureHeaderSecurityCheckTestBase 7 | { 8 | public override string HeaderName => "x-powered-by"; 9 | 10 | 11 | [Fact] 12 | public void CheckEmptyValue() 13 | { 14 | AssertSecurityCheckState(HeaderName, string.Empty, SecurityCheckState.Best); 15 | } 16 | 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/CodeTherapy.HttpSecurityCheck.Tests/XXSSProtectionSecurityHeaderCheckTests.cs: -------------------------------------------------------------------------------- 1 | using CodeTherapy.HttpSecurityChecks.Data; 2 | using Xunit; 3 | 4 | namespace CodeTherapy.HttpSecurityChecks.Test 5 | { 6 | public class XXSSProtectionSecurityHeaderCheckTests : MissingHeaderSecurityCheckTestBase 7 | { 8 | public override string HeaderName => "x-xss-protection"; 9 | 10 | [Theory] 11 | [InlineData("1; mode=block", SecurityCheckState.Best)] 12 | [InlineData("1", SecurityCheckState.Good)] 13 | [InlineData("1; report=http://example.com/report_URI", SecurityCheckState.Bad)] 14 | [InlineData("0", SecurityCheckState.Bad)] 15 | public void CheckValidValues(string value, SecurityCheckState securityCheckState) 16 | { 17 | AssertSecurityCheckState(HeaderName, value, securityCheckState); 18 | } 19 | } 20 | 21 | } 22 | --------------------------------------------------------------------------------