Skip to content

Commit

Permalink
Started implementing CSP Nonce (#30)
Browse files Browse the repository at this point in the history
* Started implementing CSP Nonce

* Removed nonce definition

* Removed Helpers

* Changed to update header to include nonce only if called by the TagHelper

* Reworked with service

* Removed as unrequired

* Changed to check context

* Fixed incorrect key

* Reversed check

* Added ability to output data-attribute nonce

* Removed unused class

* Removed unused interface

* Corrected check

* Updated readme

* Moved to constant

* Updated more places to use constants

* Branch update and some code changes

* Code changes reordered CSP directives

* code clean up and testing fix

* Build fixes

* Build pipeline permissions

* Test reporter doesnt support external PRS so need to split into two actions

* fix yaml formatting

* Fix test run report uploading

* Fix report yaml

* Fixing reporting

* Fixing reporting

* Update step versions

---------

Co-authored-by: Matthew Wise <6782865+Matthew-Wise@users.noreply.github.com>
  • Loading branch information
AaronSadlerUK and Matthew-Wise authored Mar 27, 2024
1 parent d67bea2 commit d293004
Show file tree
Hide file tree
Showing 20 changed files with 435 additions and 157 deletions.
28 changes: 11 additions & 17 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,19 @@ jobs:
build:
env:
BUILD_CONFIG: "Release"
LogFileName: "test-results.trx"
PROJECT: "./src/Umbraco.Community.CSPManager/Umbraco.Community.CSPManager.csproj"
TESTPROJECT: "./src/Umbraco.Community.CSPManager.Tests/Umbraco.Community.CSPManager.Tests.csproj"
TESTLOGPATH: "./src/Umbraco.Community.CSPManager.Tests/TestResults/"
BUILD_VERSION: ${{ github.event.release.tag_name || '0.0.0' }}

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Setup NuGet
uses: NuGet/setup-nuget@v1
with:
nuget-version: "6.x"
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
6.x
Expand All @@ -48,17 +45,14 @@ jobs:
run: dotnet build $TESTPROJECT -c $BUILD_CONFIG -p:Version=$BUILD_VERSION --no-restore

- name: Run tests
run: dotnet test $TESTPROJECT -c $BUILD_CONFIG --no-build --verbosity normal --filter "Category!=LongRunning" --logger "trx;LogFileName=test-results.trx"

- name: Test Report
uses: dorny/test-reporter@v1
if: always()
run: dotnet test $TESTPROJECT -c $BUILD_CONFIG --no-build --verbosity normal --filter "Category!=LongRunning" --logger "trx;LogFileName=$LogFileName"

- uses: actions/upload-artifact@v4 # upload test results
if: success() || failure() # run this step even if previous step failed
with:
name: DotNET Tests
path: "**/test-results.trx"
reporter: dotnet-trx
fail-on-error: true

name: test-results
path: "${{env.TESTLOGPATH}}${{env.LogFileName}}"

- name: Publish
if: github.event_name == 'release'
run: nuget push **\*.nupkg -Source 'https://api.nuget.org/v3/index.json' -ApiKey ${{secrets.NUGET_API_KEY}}
20 changes: 20 additions & 0 deletions .github/workflows/test-report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: 'Test Report'
on:
workflow_run:
workflows: [Build] # runs after Build workflow
types:
- completed
permissions:
contents: read
actions: read
checks: write
jobs:
report:
runs-on: ubuntu-latest
steps:
- uses: dorny/test-reporter@v1
with:
artifact: test-results # artifact name
name: .NET Tests # Name of the check run which will be created
path: "**/test-results.trx" # Path to test results (inside artifact .zip)
reporter: dotnet-trx
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,27 @@ dotnet add package Umbraco.Community.CSPManager
![CSP Evaluation section](https://raw.githubusercontent.com/Matthew-Wise/Umbraco-CSP-manager/main/images/evaluate-screen.png "Csp Evaluation section")

You will also need to give access via the users section to the CSP Manager section.

## Nonce Tag Helper

To use CSP nonce you can make use of the Tag Helper.

First you will need to include the namespace in the `ViewImports.cshtml`

```
@addTagHelper *, Umbraco.Community.CSPManager
```

To use the nonce add the following to your `<script>` or `<style>` tags:

```
csp-manager-add-nonce="true"
```

When this is added it will include the nonce in the CSP header and output in the page.

If you need to access the nonce within a data attribute you can use the following:

```
csp-manager-add-nonce-data-attribute="true"
```
1 change: 1 addition & 0 deletions src/DemoSite/Views/_ViewImports.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
@using Umbraco.Cms.Core.Models.PublishedContent
@using Microsoft.AspNetCore.Html
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Umbraco.Community.CSPManager
@addTagHelper *, Smidge
@inject Smidge.SmidgeHelper SmidgeHelper
6 changes: 6 additions & 0 deletions src/DemoSite/Views/master.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.13.1/jquery.validate.min.js"></script>
<script src="https://ajax.aspnetcdn.com/ajax/mvc/5.2.3/jquery.validate.unobtrusive.min.js"></script>
<script src="@Url.Content("~/scripts/umbraco-starterkit-app.js")"></script>
<script>
console.log("this should not run with nonce")
</script>
<script csp-manager-add-nonce csp-manager-add-nonce-data-attribute>
console.log("this should run with nonce", document.currentScript.attributes)
</script>

</body>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,25 @@

using System.Collections.Generic;
using System.Threading.Tasks;
using Cms.Core;
using Cms.Core.Configuration.Models;
using Cms.Core.Events;
using Cms.Core.Routing;
using Cms.Core.Services;
using Cms.Tests.Integration.Implementations;
using CSPManager.Middleware;
using CSPManager.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Models;
using Notifications;
using IHostingEnvironment = Cms.Core.Hosting.IHostingEnvironment;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Tests.Integration.Implementations;
using Umbraco.Community.CSPManager.Middleware;
using Umbraco.Community.CSPManager.Services;
using Umbraco.Community.CSPManager.Models;
using Umbraco.Community.CSPManager.Notifications;

using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;

[TestFixture]
public class CspMiddlewareTests
Expand Down Expand Up @@ -79,20 +80,20 @@ public void SetUp()
}

[Test]
[TestCaseSource(nameof(CspMiddlewareOnlyRunsWithRuntimeRunCases))]
[TestCaseSource(typeof(MiddlewareTestCases), nameof(MiddlewareTestCases.CspMiddlewareOnlyRunsWithRuntimeRunCases))]
public async Task CspMiddleware_OnlyRunsWithRuntimeRun(RuntimeLevel runtimeLevel, Times verifyCalls)
{
Mock.Get(Services.GetRequiredService<IRuntimeState>())
.SetupGet(x => x.Level).Returns(runtimeLevel);

await _host.GetTestClient().GetAsync("/");
await _host.GetTestClient().GetAsync("/", HttpCompletionOption.ResponseHeadersRead);
Mock.Get(_cspService).Verify(x => x.GetCachedCspDefinition(It.IsAny<bool>()), verifyCalls);
Mock.Get(_eventAggregator).Verify(x => x.PublishAsync(It.IsAny<CspWritingNotification>(),
It.IsAny<CancellationToken>()), verifyCalls);
}

[Test]
[TestCaseSource(nameof(CspMiddlewareReturnsExpectedCspWhenEnabledCases))]
[TestCaseSource(typeof(MiddlewareTestCases), nameof(MiddlewareTestCases.CspMiddlewareReturnsExpectedCspWhenEnabledCases))]
public async Task CspMiddleware_ReturnsExpectedCspWhenEnabled(string uri, CspDefinition definition)
{
Mock.Get(_cspService).Setup(x => x.GetCachedCspDefinition(It.IsAny<bool>())).Returns(definition);
Expand All @@ -101,65 +102,16 @@ public async Task CspMiddleware_ReturnsExpectedCspWhenEnabled(string uri, CspDef
if (definition.Enabled)
{
await Verify(response.Headers)
.UseDirectory(nameof(CspMiddleware_ReturnsExpectedCspWhenEnabled))
.UseFileName(
$"{nameof(CspMiddleware_ReturnsExpectedCspWhenEnabled)}_{TestContext.CurrentContext.Test.Name}");
$"{TestContext.CurrentContext.Test.Name}");
}
else
{
response.Headers.Should().NotContainKey(CspConstants.HeaderName);
}
}

private static IEnumerable<TestCaseData> CspMiddlewareReturnsExpectedCspWhenEnabledCases
{
get
{
yield return new TestCaseData("/umbraco",
new CspDefinition
{
Id = CspConstants.DefaultBackofficeId,
Enabled = true,
IsBackOffice = true,
Sources = CspConstants.DefaultBackOfficeCsp
})
{ TestName = "Backoffice enabled" };

yield return new TestCaseData("/umbraco",
new CspDefinition
{
Id = CspConstants.DefaultBackofficeId,
Enabled = true,
IsBackOffice = true,
ReportOnly = true,
Sources = CspConstants.DefaultBackOfficeCsp
})
{ TestName = "Backoffice Report Only" };

yield return new TestCaseData("/umbraco",
new CspDefinition
{
Id = CspConstants.DefaultBackofficeId,
Enabled = false,
IsBackOffice = true,
Sources = CspConstants.DefaultBackOfficeCsp
})
{ TestName = "Backoffice disabled" };
}
}

public static IEnumerable<TestCaseData> CspMiddlewareOnlyRunsWithRuntimeRunCases
{
get
{
yield return new TestCaseData(RuntimeLevel.Run, Times.Once());
yield return new TestCaseData(RuntimeLevel.Install, Times.Never());
yield return new TestCaseData(RuntimeLevel.Upgrade, Times.Never());
yield return new TestCaseData(RuntimeLevel.Boot, Times.Never());
yield return new TestCaseData(RuntimeLevel.BootFailed, Times.Never());
yield return new TestCaseData(RuntimeLevel.Unknown, Times.Never());
}
}

[TearDown]
public void TearDownAsync() => _host.StopAsync();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
namespace Umbraco.Community.CSPManager.Tests.Middleware;

using System.Collections.Generic;
using Umbraco.Cms.Core;
using Umbraco.Community.CSPManager.Models;

internal static class MiddlewareTestCases
{
public static IEnumerable<TestCaseData> CspMiddlewareReturnsExpectedCspWhenEnabledCases
{
get
{
yield return new TestCaseData("/umbraco",
new CspDefinition
{
Id = CspConstants.DefaultBackofficeId,
Enabled = true,
IsBackOffice = true,
Sources = CspConstants.DefaultBackOfficeCsp
})
{ TestName = "Backoffice enabled" };

yield return new TestCaseData("/umbraco",
new CspDefinition
{
Id = CspConstants.DefaultBackofficeId,
Enabled = true,
IsBackOffice = true,
ReportOnly = true,
Sources = CspConstants.DefaultBackOfficeCsp
})
{ TestName = "Backoffice Report Only" };

yield return new TestCaseData("/umbraco",
new CspDefinition
{
Id = CspConstants.DefaultBackofficeId,
Enabled = false,
IsBackOffice = true,
Sources = CspConstants.DefaultBackOfficeCsp
})
{ TestName = "Backoffice disabled" };
}
}

public static IEnumerable<TestCaseData> CspMiddlewareOnlyRunsWithRuntimeRunCases
{
get
{
yield return new TestCaseData(RuntimeLevel.Run, Times.Once());
yield return new TestCaseData(RuntimeLevel.Install, Times.Never());
yield return new TestCaseData(RuntimeLevel.Upgrade, Times.Never());
yield return new TestCaseData(RuntimeLevel.Boot, Times.Never());
yield return new TestCaseData(RuntimeLevel.BootFailed, Times.Never());
yield return new TestCaseData(RuntimeLevel.Unknown, Times.Never());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
namespace Umbraco.Community.CSPManager.Tests.Services;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Community.CSPManager.Models;

internal class CspServiceTestCases
{
public static IEnumerable<TestCaseData> SaveCspDefinitionSource
{
get
{
var oneLessSource = new CspDefinition
{
Id = CspConstants.DefaultBackofficeId,
Enabled = true,
IsBackOffice = true,
Sources = CspConstants.DefaultBackOfficeCsp.GetRange(0, CspConstants.DefaultBackOfficeCsp.Count - 1)
};

yield return new TestCaseData(oneLessSource) { TestName = "Remove a CSP Source from a Definition" };

var additionalSource = CspConstants.DefaultBackOfficeCsp.ToList();
additionalSource.Add(new CspDefinitionSource
{
DefinitionId = CspConstants.DefaultBackofficeId,
Directives = new() { CspConstants.Directives.BaseUri },
Source = "test"
});

yield return new TestCaseData(new CspDefinition
{
Id = CspConstants.DefaultBackofficeId,
Enabled = true,
IsBackOffice = true,
Sources = additionalSource
})
{ TestName = "Add a CSP Source to a Definition" };


var longSource = CspConstants.DefaultBackOfficeCsp.ToList();
longSource.Add(new CspDefinitionSource
{
DefinitionId = CspConstants.DefaultBackofficeId,
Directives = new() { CspConstants.Directives.BaseUri },
Source = new string('a', 300)
});


yield return new TestCaseData(new CspDefinition
{
Id = CspConstants.DefaultBackofficeId,
Enabled = true,
IsBackOffice = true,
Sources = additionalSource
})
{ TestName = "Add a CSP Long Source to a Definition" };
}
}
}
Loading

0 comments on commit d293004

Please sign in to comment.