Security scan updates

This commit is contained in:
alexjanousekGSA
2025-07-31 12:47:40 -04:00
parent 55340dbc4b
commit 611ada696f
25 changed files with 114 additions and 152 deletions

View File

@@ -185,8 +185,7 @@ def _csp(config):
def create_app(application): def create_app(application):
@application.after_request @application.after_request
def add_security_headers(response): def add_security_headers(response):
# Add Cross-Origin-Embedder-Policy header response.headers['Cross-Origin-Embedder-Policy'] = 'credentialless'
response.headers["Cross-Origin-Embedder-Policy"] = "credentialless"
return response return response
@application.context_processor @application.context_processor
@@ -274,14 +273,11 @@ def create_app(application):
content_security_policy=_csp(application.config), content_security_policy=_csp(application.config),
content_security_policy_nonce_in=["style-src", "script-src"], content_security_policy_nonce_in=["style-src", "script-src"],
permissions_policy={ permissions_policy={
"accelerometer": "()", "accelerometer": "(self \"https://www.youtube-nocookie.com\")",
"ambient-light-sensor": "()", "autoplay": "(self \"https://www.youtube-nocookie.com\")",
"autoplay": "()",
"battery": "()",
"camera": "()", "camera": "()",
"document-domain": "()",
"geolocation": "()", "geolocation": "()",
"gyroscope": "()", "gyroscope": "(self \"https://www.youtube-nocookie.com\")",
"local-fonts": "()", "local-fonts": "()",
"magnetometer": "()", "magnetometer": "()",
"microphone": "()", "microphone": "()",

View File

@@ -25,30 +25,31 @@
this.bindEvents(); this.bindEvents();
}; };
ListEntry.optionalAttributes = ['aria-describedby']; ListEntry.optionalAttributes = ['aria-describedby'];
ListEntry.prototype.entryTemplate = Hogan.compile( ListEntry.prototype.renderEntry = function(data) {
'<div class="list-entry">' + return `
'<label for="{{{id}}}" class="govuk-input--numbered__label">' + <div class="list-entry">
'<span class="usa-sr-only">{{listItemName}} number </span>{{number}}.' + <label for="${data.id}" class="govuk-input--numbered__label">
'</label>' + <span class="usa-sr-only">${data.listItemName} number </span>${data.number}.
'<input' + </label>
' name="{{name}}"' + <input
' id="{{id}}"' + name="${data.name}"
' {{#value}}value="{{value}}{{/value}}"' + id="${data.id}"
' {{{sharedAttributes}}}' + ${data.value ? `value="${data.value}"` : ''}
'/>' + ${data.sharedAttributes}
'{{#button}}' + />
'<button type="button" class="usa-button input-list__button--remove">' + ${data.button ? `
'Remove<span class="usa-sr-only"> {{listItemName}} number {{number}}</span>' + <button type="button" class="usa-button input-list__button--remove">
'</button>' + Remove<span class="usa-sr-only"> ${data.listItemName} number ${data.number}</span>
'{{/button}}' + </button>
'</div>' ` : ''}
); </div>
ListEntry.prototype.addButtonTemplate = Hogan.compile( `;
'<button type="button" class="usa-button input-list__button--add">Add another {{listItemName}} ({{entriesLeft}} remaining)</button>' };
); ListEntry.prototype.renderAddButton = function(data) {
return `<button type="button" class="usa-button input-list__button--add">Add another ${data.listItemName} (${data.entriesLeft} remaining)</button>`;
};
ListEntry.prototype.getSharedAttributes = function () { ListEntry.prototype.getSharedAttributes = function () {
var $inputs = this.$wrapper.find('input'), var $inputs = this.$wrapper.find('input'),
attributeTemplate = Hogan.compile(' {{name}}="{{value}}"'),
generatedAttributes = ['id', 'name', 'value'], generatedAttributes = ['id', 'name', 'value'],
attributes = [], attributes = [],
attrIdx, attrIdx,
@@ -68,7 +69,7 @@
while (attrIdx--) { while (attrIdx--) {
// prevent duplicates // prevent duplicates
if ($.inArray(elmAttrs[attrIdx].name, existingAttributes) === -1) { if ($.inArray(elmAttrs[attrIdx].name, existingAttributes) === -1) {
attrStr += attributeTemplate.render({ 'name': elmAttrs[attrIdx].name, 'value': elmAttrs[attrIdx].value }); attrStr += ` ${elmAttrs[attrIdx].name}="${elmAttrs[attrIdx].value}"`;
existingAttributes.push(elmAttrs[attrIdx].name); existingAttributes.push(elmAttrs[attrIdx].name);
} }
} }
@@ -188,10 +189,10 @@
if (entryNumber > 1) { if (entryNumber > 1) {
dataObj.button = true; dataObj.button = true;
} }
this.$wrapper.append(this.entryTemplate.render(dataObj)); this.$wrapper.append(this.renderEntry(dataObj));
}.bind(this)); }.bind(this));
if (this.entries.length < this.maxEntries) { if (this.entries.length < this.maxEntries) {
this.$wrapper.append(this.addButtonTemplate.render({ this.$wrapper.append(this.renderAddButton({
'listItemName' : this.listItemName, 'listItemName' : this.listItemName,
'entriesLeft' : (this.maxEntries - this.entries.length) 'entriesLeft' : (this.maxEntries - this.entries.length)
})); }));

View File

@@ -3,65 +3,70 @@
"use strict"; "use strict";
var Modules = global.GOVUK.Modules; var Modules = global.GOVUK.Modules;
var Hogan = global.Hogan;
// Object holding all the states for the component's HTML // Template functions for rendering component states
let states = { let renderStates = {
'initial': Hogan.compile(` 'initial': function(data) {
{{#showNowAsDefault}} return `
${data.showNowAsDefault ? `
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" id="${data.name}-0" name="${data.name}" type="radio" value="">
<label class="usa-radio__label" for="${data.name}-0">Now</label>
</div>
</div>
` : ''}
<div class="radio-select__column margin-y-2"> <div class="radio-select__column margin-y-2">
<div class="usa-radio"> ${data.categories.map(category =>
<input class="usa-radio__input" checked="checked" id="{{name}}-0" name="{{name}}" type="radio" value=""> `<input type='button' class='usa-button usa-button--outline radio-select__button--category' aria-expanded="false" value='${category}' />`
<label class="usa-radio__label" for="{{name}}-0">Now</label> ).join('')}
</div>
</div> </div>
{{/showNowAsDefault}} `;
<div class="radio-select__column margin-y-2"> },
{{#categories}} 'choose': function(data) {
<input type='button' class='usa-button usa-button--outline radio-select__button--category' aria-expanded="false" value='{{.}}' /> return `
{{/categories}} ${data.showNowAsDefault ? `
</div> <div class="radio-select__column margin-y-2">
`), <div class="usa-radio">
'choose': Hogan.compile(` <input class="usa-radio__input" checked="checked" id="${data.name}-0" name="${data.name}" type="radio" value="">
{{#showNowAsDefault}} <label class="usa-radio__label" for="${data.name}-0">Now</label>
</div>
</div>
` : ''}
<div class="radio-select__column margin-y-2"> <div class="radio-select__column margin-y-2">
<div class="usa-radio"> ${data.choices.map(choice => `
<input class="usa-radio__input" checked="checked" id="{{name}}-0" name="{{name}}" type="radio" value=""> <div class="usa-radio js-option">
<label class="usa-radio__label" for="{{name}}-0">Now</label> <input class="usa-radio__input" type="radio" value="${choice.value}" id="${choice.id}" name="${data.name}" />
</div> <label class="usa-radio__label" for="${choice.id}">${choice.label}</label>
</div>
`).join('')}
<input type='button' class='usa-button usa-button--outline radio-select__button--done margin-top-4' aria-expanded='true' value='Back to select a new time' />
</div> </div>
{{/showNowAsDefault}} `;
<div class="radio-select__column margin-y-2"> },
{{#choices}} 'chosen': function(data) {
<div class="usa-radio js-option"> return `
<input class="usa-radio__input" type="radio" value="{{value}}" id="{{id}}" name="{{name}}" /> ${data.showNowAsDefault ? `
<label class="usa-radio__label" for="{{id}}">{{label}}</label> <div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" id="${data.name}-0" name="${data.name}" type="radio" value="">
<label class="usa-radio__label" for="${data.name}-0">Now</label>
</div>
</div> </div>
{{/choices}} ` : ''}
<input type='button' class='usa-button usa-button--outline radio-select__button--done margin-top-4' aria-expanded='true' value='Back to select a new time' />
</div>
`),
'chosen': Hogan.compile(`
{{#showNowAsDefault}}
<div class="radio-select__column margin-y-2"> <div class="radio-select__column margin-y-2">
<div class="usa-radio"> ${data.choices.map(choice => `
<input class="usa-radio__input" id="{{name}}-0" name="{{name}}" type="radio" value=""> <div class="usa-radio">
<label class="usa-radio__label" for="{{name}}-0">Now</label> <input class="usa-radio__input" checked="checked" type="radio" value="${choice.value}" id="${choice.id}" name="${data.name}" />
</div> <label class="usa-radio__label" for="${choice.id}">${choice.label}</label>
</div>
`).join('')}
</div> </div>
{{/showNowAsDefault}} <div class="radio-select__column margin-y-2">
<div class="radio-select__column margin-y-2"> <input type='button' class='usa-button usa-button--outline radio-select__button--reset' aria-expanded='false' value='Choose a different time' />
{{#choices}} </div>
<div class="usa-radio"> `;
<input class="usa-radio__input" checked="checked" type="radio" value="{{value}}" id="{{id}}" name="{{name}}" /> }
<label class="usa-radio__label" for="{{id}}">{{label}}</label>
</div>
{{/choices}}
</div>
<div class="radio-select__column margin-y-2">
<input type='button' class='usa-button usa-button--outline radio-select__button--reset' aria-expanded='false' value='Choose a different time' />
</div>
`)
}; };
let shiftFocus = function(elementToFocus, component) { let shiftFocus = function(elementToFocus, component) {
@@ -80,7 +85,7 @@
let $component = $(component); let $component = $(component);
let render = (state, data) => { let render = (state, data) => {
$component.html(states[state].render(data)); $component.html(renderStates[state](data));
}; };
// store array of all options in component // store array of all options in component
let choices = $('label', $component).toArray().map(function(element) { let choices = $('label', $component).toArray().map(function(element) {

View File

@@ -25,7 +25,7 @@
<p>Notify.gov is an easy-to-use, web-based platform. It requires no technical expertise or system integration — users <p>Notify.gov is an easy-to-use, web-based platform. It requires no technical expertise or system integration — users
can create an account and get started within minutes. We take the security and privacy of messaging data seriously can create an account and get started within minutes. We take the security and privacy of messaging data seriously
by minimizing data retention and using modern encryption methods.</p> by minimizing data retention and using modern encryption methods.</p>
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/fkcy_xbo0Bw?si=qNo94BwL4WCeusy5&amp;start=2" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> <h2 class="padding-bottom-3">Product Highlights</h2> <iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/fkcy_xbo0Bw?si=qNo94BwL4WCeusy5&amp;start=2" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen credentialless></iframe> <h2 class="padding-bottom-3">Product Highlights</h2>
{% set product_highlights = [ {% set product_highlights = [
{ {
"svg_src": "#send", "svg_src": "#send",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 158 KiB

View File

@@ -44,7 +44,6 @@ const javascripts = () => {
.pipe(buffer()) .pipe(buffer())
.pipe( .pipe(
plugins.addSrc.prepend([ plugins.addSrc.prepend([
paths.npm + 'hogan.js/dist/hogan-3.0.2.js',
paths.npm + 'jquery/dist/jquery.min.js', paths.npm + 'jquery/dist/jquery.min.js',
paths.npm + 'query-command-supported/dist/queryCommandSupported.min.js', paths.npm + 'query-command-supported/dist/queryCommandSupported.min.js',
paths.npm + 'textarea-caret/index.js', paths.npm + 'textarea-caret/index.js',

49
package-lock.json generated
View File

@@ -18,7 +18,6 @@
"govuk_frontend_toolkit": "^9.0.1", "govuk_frontend_toolkit": "^9.0.1",
"govuk-frontend": "2.13.0", "govuk-frontend": "2.13.0",
"gulp-merge": "^0.1.1", "gulp-merge": "^0.1.1",
"hogan": "1.0.2",
"jquery": "3.7.1", "jquery": "3.7.1",
"morphdom": "^2.7.7", "morphdom": "^2.7.7",
"playwright": "^1.54.1", "playwright": "^1.54.1",
@@ -4404,11 +4403,6 @@
"sass-embedded-linux-x64": "^1.89.0" "sass-embedded-linux-x64": "^1.89.0"
} }
}, },
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -8856,26 +8850,6 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/hogan": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/hogan/-/hogan-1.0.2.tgz",
"integrity": "sha512-2RV7G4f+Rt9YIYDu01r6pgZvP+XhrXi/JKlXd4b+vRybXk94ui4PQjbh/lFaH8gQtxCygy/WKkqmpm0IyZysJA==",
"dependencies": {
"hogan.js": "*"
}
},
"node_modules/hogan.js": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/hogan.js/-/hogan.js-3.0.2.tgz",
"integrity": "sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==",
"dependencies": {
"mkdirp": "0.3.0",
"nopt": "1.0.10"
},
"bin": {
"hulk": "bin/hulk"
}
},
"node_modules/homedir-polyfill": { "node_modules/homedir-polyfill": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
@@ -11489,15 +11463,6 @@
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"dev": true "dev": true
}, },
"node_modules/mkdirp": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz",
"integrity": "sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==",
"deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)",
"engines": {
"node": "*"
}
},
"node_modules/morphdom": { "node_modules/morphdom": {
"version": "2.7.7", "version": "2.7.7",
"resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.7.tgz", "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.7.tgz",
@@ -11593,20 +11558,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/nopt": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
"integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": "*"
}
},
"node_modules/normalize-path": { "node_modules/normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",

View File

@@ -34,7 +34,6 @@
"govuk_frontend_toolkit": "^9.0.1", "govuk_frontend_toolkit": "^9.0.1",
"govuk-frontend": "2.13.0", "govuk-frontend": "2.13.0",
"gulp-merge": "^0.1.1", "gulp-merge": "^0.1.1",
"hogan": "1.0.2",
"jquery": "3.7.1", "jquery": "3.7.1",
"morphdom": "^2.7.7", "morphdom": "^2.7.7",
"playwright": "^1.54.1", "playwright": "^1.54.1",

View File

@@ -1,4 +1,3 @@
"""Simple tests to verify ZAP security fixes"""
import pytest import pytest
@@ -24,11 +23,23 @@ def test_no_duplicate_form_action(client_request, mocker, mock_get_service_and_o
assert count == 1 assert count == 1
def test_cross_origin_embedder_policy_exists(client_request, mocker, mock_get_service_and_organization_counts): def test_cross_origin_embedder_policy_set_to_credentialless(client_request, mocker, mock_get_service_and_organization_counts):
"""Check that Cross-Origin-Embedder-Policy header is present""" """Check that Cross-Origin-Embedder-Policy is set to 'credentialless' for YouTube compatibility"""
mocker.patch("app.notify_client.user_api_client.UserApiClient.deactivate_user") mocker.patch("app.notify_client.user_api_client.UserApiClient.deactivate_user")
client_request.logout() client_request.logout()
response = client_request.get_response('.index') response = client_request.get_response('.index')
assert 'Cross-Origin-Embedder-Policy' in response.headers assert response.headers.get('Cross-Origin-Embedder-Policy') == 'credentialless'
assert response.headers['Cross-Origin-Embedder-Policy'] == 'credentialless'
def test_permissions_policy_allows_youtube_features(client_request, mocker, mock_get_service_and_organization_counts):
"""Check that Permissions-Policy allows necessary features for YouTube embeds"""
mocker.patch("app.notify_client.user_api_client.UserApiClient.deactivate_user")
client_request.logout()
response = client_request.get_response('.index')
permissions_policy = response.headers.get('Permissions-Policy', '')
assert 'accelerometer=(self "https://www.youtube-nocookie.com")' in permissions_policy
assert 'autoplay=(self "https://www.youtube-nocookie.com")' in permissions_policy
assert 'gyroscope=(self "https://www.youtube-nocookie.com")' in permissions_policy

20
urls.js
View File

@@ -19,17 +19,17 @@ const routes = {
authenticated: [ authenticated: [
{ {
label: 'SMS Template Preview', label: 'SMS Template Preview',
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/send/a4e530e7-cae2-4e24-a1e9-752141a0b58e/one-off/step-0', path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/send/9c2b7a55-8785-4dc6-84d6-eb0e0615590d/one-off/step-0',
}, },
// Pages with govuk buttons that need testing // Pages with govuk buttons that need testing
{ {
label: 'API Keys', label: 'API Keys',
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/api/keys', path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/api/keys',
}, },
// Pages to test radio buttons before converting govukRadios to USWDS // Pages to test radio buttons before converting govukRadios to USWDS
{ {
label: 'API Keys Create', label: 'API Keys Create',
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/api/keys/create', path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/api/keys/create',
}, },
{ {
label: 'Change User Auth', label: 'Change User Auth',
@@ -41,31 +41,31 @@ const routes = {
}, },
{ {
label: 'Service Settings', label: 'Service Settings',
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/service-settings', path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/service-settings',
}, },
{ {
label: 'Service Send Files By Email', label: 'Service Send Files By Email',
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/service-settings/send-files-by-email', path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/service-settings/send-files-by-email',
}, },
{ {
label: 'Service SMS Prefix', label: 'Service SMS Prefix',
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/service-settings/sms-prefix', path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/service-settings/sms-prefix',
}, },
{ {
label: 'Send One Off Step 2', label: 'Send One Off Step 2',
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/send/a4e530e7-cae2-4e24-a1e9-752141a0b58e/one-off/step-2', path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/send/9c2b7a55-8785-4dc6-84d6-eb0e0615590d/one-off/step-2',
}, },
{ {
label: 'Choose Template', label: 'Choose Template',
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/templates', path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/templates',
}, },
{ {
label: 'Team Members', label: 'Team Members',
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/users', path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/users',
}, },
{ {
label: 'Invite User', label: 'Invite User',
path: '/services/e6de050d-bd56-4f49-b44b-cb789c4f79fd/users/invite', path: '/services/829ac564-59e9-47c5-ad69-e91315641c31/users/invite',
}, },
// Platform admin pages with checkboxes // Platform admin pages with checkboxes
{ {