├── demo-site ├── .eslintignore ├── static │ ├── favicon │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── apple-icon-114x114.png │ └── index.html ├── scripts │ └── prepend-licenses.js ├── src │ ├── icons │ │ ├── DashIcon.jsx │ │ ├── PlusIcon.jsx │ │ └── ShuffleIcon.jsx │ ├── index.js │ ├── components │ │ ├── ErrorToast.jsx │ │ ├── Footer.jsx │ │ ├── PolicyBreakdown.jsx │ │ ├── PolicyBox.jsx │ │ ├── OutputPanel.jsx │ │ └── CSPValidationForm.jsx │ ├── App.jsx │ └── csp-list.json ├── babel.config.js ├── README.md ├── webpack.config.js ├── package.json └── .eslintrc.js ├── .gitignore ├── .github └── workflows │ ├── demo-build-and-deploy.yml │ └── ci.yml ├── src ├── main │ └── java │ │ └── com │ │ └── shapesecurity │ │ └── salvation2 │ │ ├── PolicyList.java │ │ ├── URLs │ │ ├── GUID.java │ │ ├── URLWithScheme.java │ │ └── URI.java │ │ ├── Directives │ │ ├── FrameAncestorsDirective.java │ │ ├── ReportUriDirective.java │ │ ├── PluginTypesDirective.java │ │ ├── HostSourceDirective.java │ │ ├── SourceExpressionDirective.java │ │ └── SandboxDirective.java │ │ ├── Values │ │ ├── RFC7230Token.java │ │ ├── Scheme.java │ │ ├── Nonce.java │ │ ├── MediaType.java │ │ ├── Hash.java │ │ └── Host.java │ │ ├── Utils.java │ │ ├── JSInterface.java │ │ ├── PolicyInOrigin.java │ │ ├── Directive.java │ │ ├── Constants.java │ │ └── FetchDirectiveKind.java └── test │ ├── javascript │ └── salvation.test.js │ └── java │ └── com │ └── shapesecurity │ └── salvation2 │ ├── TestBase.java │ ├── LowLevelPolicyManipulationTest.java │ └── ParserTest.java ├── README.md ├── style.xml ├── LICENSE └── pom.xml /demo-site/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | logs 4 | deployment 5 | static 6 | public 7 | -------------------------------------------------------------------------------- /demo-site/static/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shapesecurity/salvation/HEAD/demo-site/static/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /demo-site/static/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shapesecurity/salvation/HEAD/demo-site/static/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /demo-site/static/favicon/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shapesecurity/salvation/HEAD/demo-site/static/favicon/apple-icon-114x114.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.class 3 | target 4 | .idea 5 | .editorconfig 6 | node_modules 7 | demo/dist 8 | build 9 | npm-debug.log 10 | key.pem 11 | cert.pem 12 | 13 | # Eclipse 14 | # ------- 15 | *.classpath 16 | *.project 17 | *.settings 18 | 19 | dist 20 | .vscode 21 | demo-site/public 22 | -------------------------------------------------------------------------------- /demo-site/scripts/prepend-licenses.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const mainJsPath = __dirname + '/../public/main.js'; 4 | const mainJsContent = fs.readFileSync(mainJsPath).toString(); 5 | 6 | fs.writeFileSync(mainJsPath, '// Licenses available at https://cspvalidator.org/licenses.json\n' + mainJsContent); 7 | -------------------------------------------------------------------------------- /demo-site/src/icons/DashIcon.jsx: -------------------------------------------------------------------------------- 1 | // From bootstrap-icons 2 | import React from 'react'; 3 | const styles = { display: 'flex' }; 4 | const DashIcon = () => ( 5 | 6 | 7 | 8 | ); 9 | 10 | export { DashIcon }; 11 | -------------------------------------------------------------------------------- /demo-site/src/icons/PlusIcon.jsx: -------------------------------------------------------------------------------- 1 | // From bootstrap-icons 2 | import React from 'react'; 3 | const styles = { display: 'flex' }; 4 | const PlusIcon = () => ( 5 | 6 | 7 | 8 | ); 9 | 10 | export { PlusIcon }; 11 | -------------------------------------------------------------------------------- /demo-site/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | ['@babel/preset-react', { 5 | runtime: 'automatic', 6 | development: process.env.NODE_ENV === 'development', 7 | }], 8 | ], 9 | targets: { 10 | browsers: '> 0.5%, last 10 versions', 11 | }, 12 | plugins: [ 13 | '@babel/plugin-transform-runtime', 14 | [ 15 | '@babel/plugin-transform-react-jsx', 16 | { 17 | runtime: 'automatic', 18 | importSource: 'preact', 19 | }, 20 | ], 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /demo-site/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { StrictMode } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { App } from './App'; 4 | 5 | if (process.env.NODE_ENV !== 'production') { 6 | (async () => { 7 | const ReactDOM = await import('react-dom'); 8 | const axe = await import('@axe-core/react'); 9 | axe.default(React, ReactDOM); 10 | })(); 11 | } 12 | 13 | const container = document.getElementById('root'); 14 | const root = createRoot(container); 15 | root.render( 16 | 17 | 18 | , 19 | ); 20 | -------------------------------------------------------------------------------- /.github/workflows/demo-build-and-deploy.yml: -------------------------------------------------------------------------------- 1 | # https://shapesecurity.github.io/salvation/index.html 2 | name: Build and Deploy Demo Site 3 | on: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | build-and-deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 🛎️ 12 | uses: actions/checkout@v4 13 | - name: Install and Build 🔧 14 | working-directory: ./demo-site 15 | run: | 16 | npm install 17 | npm run build 18 | - name: Deploy 🚀 19 | if: | 20 | !cancelled() && !failure() 21 | uses: JamesIves/github-pages-deploy-action@v4 22 | with: 23 | branch: gh-pages 24 | folder: demo-site/public 25 | -------------------------------------------------------------------------------- /src/main/java/com/shapesecurity/salvation2/PolicyList.java: -------------------------------------------------------------------------------- 1 | package com.shapesecurity.salvation2; 2 | 3 | import java.util.List; 4 | 5 | public class PolicyList { 6 | public final List policies; 7 | 8 | public PolicyList(List policies) { 9 | this.policies = policies; 10 | } 11 | 12 | @Override 13 | public String toString() { 14 | StringBuilder out = new StringBuilder(); 15 | boolean first = true; 16 | for (Policy policy : this.policies) { 17 | if (!first) { 18 | out.append(", "); // The whitespace is not strictly necessary but is probably valuable 19 | } 20 | first = false; 21 | out.append(policy.toString()); 22 | } 23 | return out.toString(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /demo-site/README.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | ```sh 3 | npm install 4 | ``` 5 | 6 | ## Development Build 7 | ```sh 8 | npm run dev 9 | ``` 10 | 11 | ## Production Build 12 | ```sh 13 | npm run build 14 | ``` 15 | 16 | # Note on salvation.min.js 17 | 18 | This comes from [Salvation](https://github.com/shapesecurity/salvation). It is automatically generated at compile time using [TeaVM](https://teavm.org/docs/intro/getting-started.html). And needs to be manually copied to `demo-site/static/`. 19 | 20 | To update the version of Salvation used in this demo website, follow the instructions provided in the README located at the root of this repo. Copy the generated file to `demo-site/static/`, then rename it to `salvation.min.js`. 21 | -------------------------------------------------------------------------------- /demo-site/src/components/ErrorToast.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Toast } from 'react-bootstrap'; 3 | import { string, func, number } from 'prop-types'; 4 | 5 | const ErrorToast = ({ stack, dismiss, id }) => { 6 | const [show, setShow] = useState(true); 7 | 8 | const removeError = () => { 9 | setShow(false); 10 | dismiss({ stack, id }); 11 | }; 12 | 13 | return ( 14 | 15 | 16 | Error 17 | 18 | 19 |
{stack}
20 |
21 |
22 | ); 23 | }; 24 | 25 | ErrorToast.propTypes = { 26 | stack: string.isRequired, 27 | id: number.isRequired, 28 | dismiss: func.isRequired, 29 | }; 30 | 31 | export { ErrorToast }; 32 | -------------------------------------------------------------------------------- /src/main/java/com/shapesecurity/salvation2/URLs/GUID.java: -------------------------------------------------------------------------------- 1 | package com.shapesecurity.salvation2.URLs; 2 | 3 | import com.shapesecurity.salvation2.Constants; 4 | 5 | import javax.annotation.Nonnull; 6 | import java.util.Optional; 7 | import java.util.regex.Matcher; 8 | 9 | public class GUID extends URLWithScheme { 10 | // See https://url.spec.whatwg.org/#example-url-components 11 | public GUID(@Nonnull String scheme, @Nonnull String value) { 12 | super(scheme, null, null, value); 13 | } 14 | 15 | public static Optional parseGUID(String value) { 16 | Matcher matcher = Constants.schemePattern.matcher(value); 17 | if (!matcher.find()) { 18 | return Optional.empty(); 19 | } 20 | String scheme = matcher.group(1); 21 | scheme = scheme.substring(0, scheme.length() - 1); // + 1 for the trailing ":" 22 | return Optional.of(new GUID(scheme, value.substring(scheme.length() + 1))); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/shapesecurity/salvation2/Directives/FrameAncestorsDirective.java: -------------------------------------------------------------------------------- 1 | package com.shapesecurity.salvation2.Directives; 2 | 3 | import com.shapesecurity.salvation2.Policy; 4 | 5 | import java.util.List; 6 | import java.util.Locale; 7 | 8 | public class FrameAncestorsDirective extends HostSourceDirective { 9 | public FrameAncestorsDirective(List values, DirectiveErrorConsumer errors) { 10 | super(values); 11 | 12 | int index = 0; 13 | for (String token : values) { 14 | String lowcaseToken = token.toLowerCase(Locale.ENGLISH); 15 | this._addHostOrSchemeDuringConstruction(token, lowcaseToken, "ancestor-source", index, errors); 16 | } 17 | 18 | if (this.none != null && values.size() > 1) { 19 | errors.add(Policy.Severity.Error, "'none' must not be combined with any other ancestor-source", index); 20 | } 21 | 22 | if (values.isEmpty()) { 23 | errors.add(Policy.Severity.Error, "Ancestor-source lists cannot be empty (use 'none' instead)", -1); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /demo-site/src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from 'react-bootstrap'; 3 | 4 | const Footer = () => { 5 | const viewLicenses = () => { 6 | window.location.pathname = '/licenses.json'; 7 | }; 8 | return ( 9 |
10 |

11 | Send your feedback! 12 |

13 |

14 | CSP Validator was built by Sergey Shekyan, Michael Ficarra, Lewis Ellis, 15 | Ben Vinegar, and the fine folks at{' '} 16 | F5, Inc.. 17 |

18 |

19 | Powered by{' '} 20 | Salvation{' '} 21 | v3.0.1, a Java library for working with CSP policies. 22 |

23 | 24 |
25 | ); 26 | }; 27 | export { Footer }; 28 | -------------------------------------------------------------------------------- /demo-site/src/icons/ShuffleIcon.jsx: -------------------------------------------------------------------------------- 1 | // From bootstrap-icons 2 | import React from 'react'; 3 | const styles = { display: 'flex' }; 4 | const ShuffleIcon = () => ( 5 | 6 | 7 | 8 | 9 | ); 10 | 11 | export { ShuffleIcon }; 12 | -------------------------------------------------------------------------------- /src/main/java/com/shapesecurity/salvation2/Values/RFC7230Token.java: -------------------------------------------------------------------------------- 1 | package com.shapesecurity.salvation2.Values; 2 | 3 | import com.shapesecurity.salvation2.Constants; 4 | 5 | import javax.annotation.Nonnull; 6 | import java.util.Objects; 7 | import java.util.Optional; 8 | import java.util.regex.Matcher; 9 | 10 | public class RFC7230Token { 11 | @Nonnull 12 | public final String value; 13 | 14 | private RFC7230Token(@Nonnull String value) { 15 | this.value = value; 16 | } 17 | 18 | public static Optional parseRFC7230Token(String value) { 19 | Matcher matcher = Constants.rfc7230TokenPattern.matcher(value); 20 | if (matcher.find()) { 21 | return Optional.of(new RFC7230Token(value)); 22 | } else { 23 | return Optional.empty(); 24 | } 25 | } 26 | 27 | @Override 28 | public boolean equals(Object o) { 29 | if (this == o) return true; 30 | if (!(o instanceof RFC7230Token)) return false; 31 | RFC7230Token that = (RFC7230Token) o; 32 | return value.equals(that.value); 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | return Objects.hash(value); 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return this.value; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/shapesecurity/salvation2/URLs/URLWithScheme.java: -------------------------------------------------------------------------------- 1 | package com.shapesecurity.salvation2.URLs; 2 | 3 | 4 | import javax.annotation.Nonnull; 5 | import javax.annotation.Nullable; 6 | import java.util.Locale; 7 | import java.util.Objects; 8 | 9 | public abstract class URLWithScheme { 10 | @Nonnull 11 | public final String scheme; 12 | @Nullable 13 | public final String host; 14 | @Nullable 15 | public final Integer port; 16 | @Nonnull 17 | public final String path; 18 | 19 | 20 | protected URLWithScheme(@Nonnull String scheme, @Nullable String host, @Nullable Integer port, @Nonnull String path) { 21 | this.scheme = scheme.toLowerCase(Locale.ENGLISH); 22 | this.host = host == null ? host : host.toLowerCase(Locale.ENGLISH); 23 | this.port = port; 24 | this.path = path; 25 | } 26 | 27 | @Override 28 | public boolean equals(Object o) { 29 | if (this == o) return true; 30 | if (!(o instanceof URLWithScheme)) return false; 31 | URLWithScheme that = (URLWithScheme) o; 32 | return scheme.equals(that.scheme) && 33 | Objects.equals(host, that.host) && 34 | Objects.equals(port, that.port) && 35 | path.equals(that.path); 36 | } 37 | 38 | @Override 39 | public int hashCode() { 40 | return Objects.hash(scheme, host, port, path); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/shapesecurity/salvation2/Values/Scheme.java: -------------------------------------------------------------------------------- 1 | package com.shapesecurity.salvation2.Values; 2 | 3 | import com.shapesecurity.salvation2.Constants; 4 | 5 | import javax.annotation.Nonnull; 6 | import java.util.Locale; 7 | import java.util.Objects; 8 | import java.util.Optional; 9 | 10 | public class Scheme { 11 | @Nonnull 12 | public final String value; 13 | 14 | private Scheme(@Nonnull String value) { 15 | this.value = value; 16 | } 17 | 18 | public static Optional parseScheme(String value) { 19 | if (value.matches("^" + Constants.schemePart + ":$")) { 20 | // https://tools.ietf.org/html/rfc3986#section-3.1 21 | // "Although schemes are case-insensitive, the canonical form is lowercase" 22 | return Optional.of(new Scheme(value.substring(0, value.length() - 1).toLowerCase(Locale.ENGLISH))); 23 | } 24 | return Optional.empty(); 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return this.value + ":"; 30 | } 31 | 32 | @Override 33 | public boolean equals(Object o) { 34 | if (this == o) return true; 35 | if (o == null || getClass() != o.getClass()) return false; 36 | Scheme scheme = (Scheme) o; 37 | return value.equals(scheme.value); 38 | } 39 | 40 | @Override 41 | public int hashCode() { 42 | return Objects.hash(value); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/shapesecurity/salvation2/Values/Nonce.java: -------------------------------------------------------------------------------- 1 | package com.shapesecurity.salvation2.Values; 2 | 3 | import com.shapesecurity.salvation2.Utils; 4 | 5 | import javax.annotation.Nonnull; 6 | import java.util.Locale; 7 | import java.util.Objects; 8 | import java.util.Optional; 9 | 10 | public class Nonce { 11 | @Nonnull 12 | public final String base64ValuePart; 13 | 14 | private Nonce(@Nonnull String base64Valuepart) { 15 | this.base64ValuePart = base64Valuepart; 16 | } 17 | 18 | public static Optional parseNonce(String value) { 19 | String lowcaseValue = value.toLowerCase(Locale.ENGLISH); 20 | if (lowcaseValue.startsWith("'nonce-") && lowcaseValue.endsWith("'")) { 21 | String nonce = value.substring(7, value.length() - 1); 22 | if (Utils.IS_BASE64_VALUE.test(nonce)) { 23 | // Note that nonces _are_ case-sensitive, even though the grammar is not 24 | return Optional.of(new Nonce(nonce)); 25 | } 26 | } 27 | return Optional.empty(); 28 | } 29 | 30 | @Override 31 | public String toString() { 32 | return "'nonce-" + base64ValuePart + "'"; 33 | } 34 | 35 | @Override 36 | public boolean equals(Object o) { 37 | if (this == o) return true; 38 | if (o == null || getClass() != o.getClass()) return false; 39 | Nonce nonce = (Nonce) o; 40 | return base64ValuePart.equals(nonce.base64ValuePart); 41 | } 42 | 43 | @Override 44 | public int hashCode() { 45 | return Objects.hash(base64ValuePart); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/shapesecurity/salvation2/Utils.java: -------------------------------------------------------------------------------- 1 | package com.shapesecurity.salvation2; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.io.UnsupportedEncodingException; 5 | import java.net.URLDecoder; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.function.Predicate; 9 | import java.util.regex.Pattern; 10 | 11 | public class Utils { 12 | private static final Pattern BASE64_PATTERN = Pattern.compile("[a-zA-Z0-9+/\\-_]+=?=?"); 13 | public static final Predicate IS_BASE64_VALUE = s -> BASE64_PATTERN.matcher(s).matches(); 14 | // https://infra.spec.whatwg.org/#split-on-ascii-whitespace 15 | static List splitOnAsciiWhitespace(String input) { 16 | ArrayList out = new ArrayList<>(); 17 | for (String value : input.split("[" + Constants.WHITESPACE_CHARS + "]")) { 18 | if (value.isEmpty()) { 19 | continue; 20 | } 21 | out.add(value); 22 | } 23 | return out; 24 | } 25 | 26 | // https://infra.spec.whatwg.org/#strictly-split 27 | static List strictlySplit(@Nonnull String s, char delim) { 28 | int off = 0; 29 | int next; 30 | ArrayList list = new ArrayList<>(); 31 | while ((next = s.indexOf(delim, off)) != -1) { 32 | list.add(s.substring(off, next)); 33 | off = next + 1; 34 | } 35 | 36 | list.add(s.substring(off)); 37 | return list; 38 | } 39 | 40 | static String decodeString(@Nonnull String s) { 41 | try { 42 | return URLDecoder.decode(s, "UTF-8"); 43 | } catch (UnsupportedEncodingException e) { 44 | return s; 45 | } 46 | } 47 | 48 | private Utils() { 49 | // Utility class 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/shapesecurity/salvation2/Values/MediaType.java: -------------------------------------------------------------------------------- 1 | package com.shapesecurity.salvation2.Values; 2 | 3 | import com.shapesecurity.salvation2.Constants; 4 | 5 | import javax.annotation.Nonnull; 6 | import java.util.Locale; 7 | import java.util.Objects; 8 | import java.util.Optional; 9 | import java.util.regex.Matcher; 10 | 11 | public class MediaType { 12 | @Nonnull 13 | public final String type; 14 | @Nonnull 15 | public final String subtype; 16 | 17 | private MediaType(String type, String subtype) { 18 | this.type = type; 19 | this.subtype = subtype; 20 | } 21 | 22 | public static Optional parseMediaType(String value) { 23 | Matcher matcher = Constants.mediaTypePattern.matcher(value); 24 | if (matcher.find()) { 25 | // plugin type matching is ASCII case-insensitive 26 | // https://w3c.github.io/webappsec-csp/#plugin-types-post-request-check 27 | String type = matcher.group(1).toLowerCase(Locale.ENGLISH); 28 | String subtype = matcher.group(2).toLowerCase(Locale.ENGLISH); 29 | return Optional.of(new MediaType(type, subtype)); 30 | } 31 | return Optional.empty(); 32 | } 33 | 34 | @Override 35 | public boolean equals(Object o) { 36 | if (this == o) return true; 37 | if (o == null || getClass() != o.getClass()) return false; 38 | MediaType mediaType = (MediaType) o; 39 | return type.equals(mediaType.type) && 40 | subtype.equals(mediaType.subtype); 41 | } 42 | 43 | @Override 44 | public int hashCode() { 45 | return Objects.hash(type, subtype); 46 | } 47 | 48 | @Override 49 | public String toString() { 50 | return this.type + "/" + this.subtype; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/javascript/salvation.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const test = require('node:test'); 3 | const assert = require('node:assert'); 4 | const salvation = require('../../../target/javascript/salvation.min.js'); 5 | 6 | test('salvation initialization', () => { 7 | assert.notStrictEqual(salvation.main, undefined); 8 | salvation.main(); 9 | assert.notStrictEqual(getErrorsForSerializedCSP, undefined); 10 | assert.notStrictEqual(getErrorsForSerializedCSPList, undefined); 11 | }); 12 | 13 | test('.getErrorsForSerializedCSP() gives no errors for a valid CSP', () => { 14 | salvation.main(); 15 | const result = getErrorsForSerializedCSP('default-src \'none\';'); 16 | assert.strictEqual(result.length, 0, 'No errors should be found'); 17 | }); 18 | 19 | test('.getErrorsForSerializedCSP() provides feedback', () => { 20 | salvation.main(); 21 | const result = getErrorsForSerializedCSP('hello world'); 22 | assert.strictEqual(result, 'Warning at directive 0: Unrecognized directive hello'); 23 | }); 24 | 25 | test('.getErrorsForSerializedCSPList() gives no errors for a valid CSP', () => { 26 | salvation.main(); 27 | const result = getErrorsForSerializedCSPList('default-src \'none\',plugin-types image/png application/pdf; sandbox,style-src https: \'self\''); 28 | assert.strictEqual(result, ''); 29 | }); 30 | 31 | test('.getErrorsForSerializedCSPList() provides feedback', () => { 32 | salvation.main(); 33 | const result = getErrorsForSerializedCSPList('hello,foobar,script-src \'self\'; style-src \'self\''); 34 | assert.strictEqual(result, 'Warning at directive 0: Unrecognized directive hello\n' 35 | + 'Warning at directive 0: Unrecognized directive foobar'); 36 | }); 37 | -------------------------------------------------------------------------------- /demo-site/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useReducer } from 'react'; 2 | import { Container } from 'react-bootstrap'; 3 | import { Footer } from './components/Footer'; 4 | import { CSPValidationForm } from './components/CSPValidationForm'; 5 | import { OutputPanel } from './components/OutputPanel'; 6 | 7 | const policyReducer = (currentPolicies, action) => { 8 | if (action.type === 'update') { 9 | const newPolicies = [...currentPolicies]; 10 | const policyIndex = newPolicies.findIndex(p => p.id === action.pId); 11 | newPolicies[policyIndex].policy = action.updatedPolicy; 12 | return newPolicies; 13 | } 14 | if (action.type === 'add') { 15 | return [...currentPolicies, { id: action.pId, policy: '' }]; 16 | } 17 | if (action.type === 'remove') { 18 | const newPolicies = [...currentPolicies]; 19 | const policyIndex = newPolicies.findIndex(p => p.id === action.pId); 20 | newPolicies.splice(policyIndex, 1); 21 | return newPolicies; 22 | } 23 | if (action.type === 'overwriteAll') { 24 | return [...action.policies]; 25 | } 26 | throw new Error(`Unknown action type: ${action.type}`); 27 | }; 28 | 29 | const App = () => { 30 | window.main(); // Need this to initialize the CSP parsing functions. 31 | 32 | const [policies, setPolicies] = useReducer(policyReducer, [{ id: 0, policy: '' }]); 33 | 34 | return ( 35 | 36 |
37 |

Content Security Policy (CSP) Validator

38 |

Validate/Manipulate CSP Strings

39 | 40 | 41 |
42 |
43 |