@localnerve/csp-hashes 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.eslintignore +5 -0
  2. package/.eslintrc.json +22 -0
  3. package/.github/workflows/verify.yml +32 -0
  4. package/.vscode/launch.json +14 -0
  5. package/__tests__/fixtures/multiple-scripts-attr.html +17 -0
  6. package/__tests__/fixtures/multiple-scripts-attr.sha256 +7 -0
  7. package/__tests__/fixtures/multiple-scripts-attr.sha384 +7 -0
  8. package/__tests__/fixtures/multiple-scripts-attr.sha512 +7 -0
  9. package/__tests__/fixtures/multiple-scripts-styles-script.sha256 +7 -0
  10. package/__tests__/fixtures/multiple-scripts-styles-script.sha384 +7 -0
  11. package/__tests__/fixtures/multiple-scripts-styles-script.sha512 +7 -0
  12. package/__tests__/fixtures/multiple-scripts-styles-style.sha256 +6 -0
  13. package/__tests__/fixtures/multiple-scripts-styles-style.sha384 +6 -0
  14. package/__tests__/fixtures/multiple-scripts-styles-style.sha512 +6 -0
  15. package/__tests__/fixtures/multiple-scripts-styles.html +20 -0
  16. package/__tests__/fixtures/multiple-scripts.html +13 -0
  17. package/__tests__/fixtures/multiple-scripts.sha256 +5 -0
  18. package/__tests__/fixtures/multiple-scripts.sha384 +5 -0
  19. package/__tests__/fixtures/multiple-scripts.sha512 +5 -0
  20. package/__tests__/fixtures/multiple-style-attr.html +12 -0
  21. package/__tests__/fixtures/multiple-style-attr.sha256 +6 -0
  22. package/__tests__/fixtures/multiple-style-attr.sha384 +6 -0
  23. package/__tests__/fixtures/multiple-style-attr.sha512 +6 -0
  24. package/__tests__/fixtures/multiple-style.html +9 -0
  25. package/__tests__/fixtures/multiple-style.sha256 +5 -0
  26. package/__tests__/fixtures/multiple-style.sha384 +5 -0
  27. package/__tests__/fixtures/multiple-style.sha512 +5 -0
  28. package/__tests__/fixtures/script-src.html +7 -0
  29. package/__tests__/fixtures/script-src.sha256 +0 -0
  30. package/__tests__/fixtures/script-src.sha384 +0 -0
  31. package/__tests__/fixtures/script-src.sha512 +0 -0
  32. package/__tests__/fixtures/single-script.html +7 -0
  33. package/__tests__/fixtures/single-script.sha256 +4 -0
  34. package/__tests__/fixtures/single-script.sha384 +4 -0
  35. package/__tests__/fixtures/single-script.sha512 +4 -0
  36. package/__tests__/fixtures/single-style.html +8 -0
  37. package/__tests__/fixtures/single-style.sha256 +4 -0
  38. package/__tests__/fixtures/single-style.sha384 +4 -0
  39. package/__tests__/fixtures/single-style.sha512 +4 -0
  40. package/__tests__/index.test.js +195 -0
  41. package/babel.config.js +11 -0
  42. package/dist/index.js +15 -0
  43. package/dist/lib/index.js +119 -0
  44. package/index.js +11 -0
  45. package/jest.config.js +10 -0
  46. package/lib/index.js +113 -0
  47. package/license.md +8 -0
  48. package/package.json +46 -0
  49. package/readme.md +129 -0
package/.eslintignore ADDED
@@ -0,0 +1,5 @@
1
+ __tests__/lib
2
+ coverage
3
+ dist
4
+ private
5
+ tmp
package/.eslintrc.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "root": true,
3
+ "env": {
4
+ "node": true,
5
+ "es2020": true
6
+ },
7
+ "parserOptions": {
8
+ "sourceType": "module",
9
+ "ecmaVersion": 2020
10
+ },
11
+ "extends": [
12
+ "eslint:recommended"
13
+ ],
14
+ "rules": {
15
+ "indent": [2, 2, {
16
+ "SwitchCase": 1,
17
+ "MemberExpression": 1
18
+ }],
19
+ "quotes": [2, "single"],
20
+ "dot-notation": [2, {"allowKeywords": true}]
21
+ }
22
+ }
@@ -0,0 +1,32 @@
1
+ name: Verify
2
+ on:
3
+ push:
4
+ branches: [ main ]
5
+ pull_request:
6
+ branches: [ main ]
7
+
8
+ jobs:
9
+ verify:
10
+
11
+ runs-on: ubuntu-latest
12
+
13
+ strategy:
14
+ matrix:
15
+ node-version: [14.x, 16.x]
16
+ # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
17
+
18
+ steps:
19
+ - uses: actions/checkout@v3
20
+ - name: Use Node.js ${{ matrix.node-version }}
21
+ uses: actions/setup-node@v3.0.0
22
+ with:
23
+ node-version: ${{ matrix.node-version }}
24
+ - run: npm ci
25
+ - name: Run Lint and Test
26
+ run: npm run lint && npm test
27
+ - name: Coverage Upload
28
+ if: ${{ success() }}
29
+ uses: coverallsapp/github-action@master
30
+ with:
31
+ github-token: ${{ secrets.GITHUB_TOKEN }}
32
+ path-to-lcov: ./coverage/lcov.info
@@ -0,0 +1,14 @@
1
+ {
2
+ // Use IntelliSense to learn about possible attributes.
3
+ // Hover to view descriptions of existing attributes.
4
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
+ "version": "0.2.0",
6
+ "configurations": [
7
+ {
8
+ "type": "node",
9
+ "request": "attach",
10
+ "name": "Attach to Tests",
11
+ "port": 9229
12
+ }
13
+ ]
14
+ }
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <script>
5
+ console.log("foo");
6
+
7
+
8
+ </script>
9
+ </head>
10
+ <body>
11
+ <script>"hello world"</script>
12
+ <div class="decoy"></div>
13
+ <button onclick="alert(0);"></button>
14
+ <a href="javascript:void(0)">link</a>
15
+ <div data-attr="decoy"></div>
16
+ </body>
17
+ </html>
@@ -0,0 +1,7 @@
1
+ # element hashes
2
+ sha256-rExzpKR08DmzriumM64x74bd6TG79uFw0RlSMdXFzO4=
3
+ sha256-nd7+RDWyHZAUOeVG1UoUoXWjSTuf2PvzjZ6m08v3CCY=
4
+ # attribute hashes
5
+ sha256-d3ii1Pel57UO62xosCMNgTaZJhJa87Gd/X6e7UdlEU8=
6
+ sha256-97l24HYIWEdSIQ8PoMHzpxiGCZuyBDXtN19RPKFsOgk=
7
+ #
@@ -0,0 +1,7 @@
1
+ # element hashes
2
+ sha384-O60SQD+smnMjcJ8RHL7sAbSOyPUWgRHBovbIAphGqPN98/Iu8mtniAogp4wIsOhW
3
+ sha384-WmpTK6zbDiu5hx1ojbwG5VsNrqhW+OahJWEgoWt05o63pvZvCdwNIanHJLoyr3SD
4
+ # attribute hashes
5
+ sha384-bukTLq2o+W5IQrLDTC6PHPqfJ4hmAZxNVytFjb4OuGCjwVgz4fWKrwLEuGwMGN3+
6
+ sha384-Hs4TMINoOqtUudRVK7cWbO9tOQB1sxVWZcY9wrHsnyMIvr0urtjvW2Tl48Td9XHX
7
+ #
@@ -0,0 +1,7 @@
1
+ # element hashes
2
+ sha512-MqQ+zvBvxknlz+ZyHsCJfMSsAwYLwpr4REOSr7Q6QhvkqbJFvjlbN6mHacwH7sS6GkbgoC5j+DWFzWh6coeP0g==
3
+ sha512-qAJOafTHO8uHcv2M4vRRoaHnkd7h/xuiv+DkboPHj8WwHNcevfKTc4Wt0OqnaGuRpeKnxLXBv4VByLKSvuGZqQ==
4
+ # attribute hashes
5
+ sha512-d9VZk4RVMuB9zbaCdtPmoi0jg3Q/ENmcbczKvg9eF1Km6v4jO658wc17JPo2V9eP59CGLG1Qtnj9KBA3pvFFdw==
6
+ sha512-95nit2a0nErfKAcXliTb4gREVstYj5n71nrjGBiyu9LTOQ0tLgNNIAYImzWBYUP5H7MjJz//7XfNfirAJKoyuA==
7
+ #
@@ -0,0 +1,7 @@
1
+ # element hashes
2
+ sha256-rExzpKR08DmzriumM64x74bd6TG79uFw0RlSMdXFzO4=
3
+ sha256-nd7+RDWyHZAUOeVG1UoUoXWjSTuf2PvzjZ6m08v3CCY=
4
+ # attribute hashes
5
+ sha256-d3ii1Pel57UO62xosCMNgTaZJhJa87Gd/X6e7UdlEU8=
6
+ sha256-97l24HYIWEdSIQ8PoMHzpxiGCZuyBDXtN19RPKFsOgk=
7
+ #
@@ -0,0 +1,7 @@
1
+ # element hashes
2
+ sha384-O60SQD+smnMjcJ8RHL7sAbSOyPUWgRHBovbIAphGqPN98/Iu8mtniAogp4wIsOhW
3
+ sha384-WmpTK6zbDiu5hx1ojbwG5VsNrqhW+OahJWEgoWt05o63pvZvCdwNIanHJLoyr3SD
4
+ # attribute hashes
5
+ sha384-bukTLq2o+W5IQrLDTC6PHPqfJ4hmAZxNVytFjb4OuGCjwVgz4fWKrwLEuGwMGN3+
6
+ sha384-Hs4TMINoOqtUudRVK7cWbO9tOQB1sxVWZcY9wrHsnyMIvr0urtjvW2Tl48Td9XHX
7
+ #
@@ -0,0 +1,7 @@
1
+ # element hashes
2
+ sha512-MqQ+zvBvxknlz+ZyHsCJfMSsAwYLwpr4REOSr7Q6QhvkqbJFvjlbN6mHacwH7sS6GkbgoC5j+DWFzWh6coeP0g==
3
+ sha512-qAJOafTHO8uHcv2M4vRRoaHnkd7h/xuiv+DkboPHj8WwHNcevfKTc4Wt0OqnaGuRpeKnxLXBv4VByLKSvuGZqQ==
4
+ # attribute hashes
5
+ sha512-d9VZk4RVMuB9zbaCdtPmoi0jg3Q/ENmcbczKvg9eF1Km6v4jO658wc17JPo2V9eP59CGLG1Qtnj9KBA3pvFFdw==
6
+ sha512-95nit2a0nErfKAcXliTb4gREVstYj5n71nrjGBiyu9LTOQ0tLgNNIAYImzWBYUP5H7MjJz//7XfNfirAJKoyuA==
7
+ #
@@ -0,0 +1,6 @@
1
+ # element hashes
2
+ sha256-ZRXDQAaaiAOOtUbmDL5Ty2JR8Zf1AonXn6DCmchNhvk=
3
+ sha256-n5JetA0VmZdQj4zgyixeiOgx9wfnK2oa9/zb8+xvZks=
4
+ # attribute hashes
5
+ sha256-iYwYhiMcsGmXCUzLEpEzZNz5dINrlkqf1sLbLhEcqGM=
6
+ #
@@ -0,0 +1,6 @@
1
+ # element hashes
2
+ sha384-fR76DAgrmWR4v7rpv4JBvCihgpwA2GRb4b6e4o+ThYYMJ4FQLX3l+EwzySAPxzbN
3
+ sha384-vSmTBkQfDGIq3/4cHWOHpAwAHxMhwPY/5vxKe4XCcKVmQaAN0BYDoZeZu5n0qHjS
4
+ # attribute hashes
5
+ sha384-YHRSKUJbAwb1DUQ5EcSZ+IOjmjTr9Bcrv8E2oHbMS45tF1M/tNHACnjgCr6L+GXc
6
+ #
@@ -0,0 +1,6 @@
1
+ # element hashes
2
+ sha512-dtOFpumNJPKES8S72UuTAXS6daEMYQKpRpCzlFAto/DHdx7fH0KK8sO9LjLpOjVWrDXE4G+MtisGdWa19wieSg==
3
+ sha512-mZQHDGDpL2tkHm3IWxwlZQUEwzH27l1bGvq4qo/9T/Ef52z0J2TGEEeOgp8INb/fGFHk4NjteTNwDTeuqb9rrw==
4
+ # attribute hashes
5
+ sha512-aOZs3PhtrZbzR6+CPKnKpStcJBq1Y4lw/+SwV5pKL52hpcq5pj+tTLv7x2uegYLFIqZ89KnfhRsrVSrQ3V3XDw==
6
+ #
@@ -0,0 +1,20 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <script>
5
+ console.log("foo");
6
+
7
+
8
+ </script>
9
+ <style>* { display: none; }</style>
10
+ </head>
11
+ <body>
12
+ <style>div { background: red; }</style>
13
+ <script>"hello world"</script>
14
+ <div class="decoy"></div>
15
+ <button onclick="alert(0);"></button>
16
+ <a href="javascript:void(0)">link</a>
17
+ <div data-attr="decoy"></div>
18
+ <div style="position:relative;"></div>
19
+ </body>
20
+ </html>
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <script>
5
+ console.log("foo");
6
+
7
+
8
+ </script>
9
+ </head>
10
+ <body>
11
+ <script>"hello world"</script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,5 @@
1
+ # element hashes
2
+ sha256-rExzpKR08DmzriumM64x74bd6TG79uFw0RlSMdXFzO4=
3
+ sha256-nd7+RDWyHZAUOeVG1UoUoXWjSTuf2PvzjZ6m08v3CCY=
4
+ # attribute hashes
5
+ #
@@ -0,0 +1,5 @@
1
+ # element hashes
2
+ sha384-O60SQD+smnMjcJ8RHL7sAbSOyPUWgRHBovbIAphGqPN98/Iu8mtniAogp4wIsOhW
3
+ sha384-WmpTK6zbDiu5hx1ojbwG5VsNrqhW+OahJWEgoWt05o63pvZvCdwNIanHJLoyr3SD
4
+ # attribute hashes
5
+ #
@@ -0,0 +1,5 @@
1
+ # element hashes
2
+ sha512-MqQ+zvBvxknlz+ZyHsCJfMSsAwYLwpr4REOSr7Q6QhvkqbJFvjlbN6mHacwH7sS6GkbgoC5j+DWFzWh6coeP0g==
3
+ sha512-qAJOafTHO8uHcv2M4vRRoaHnkd7h/xuiv+DkboPHj8WwHNcevfKTc4Wt0OqnaGuRpeKnxLXBv4VByLKSvuGZqQ==
4
+ # attribute hashes
5
+ #
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <style>* { display: none; }</style>
5
+ </head>
6
+ <body>
7
+ <style>div { background: red; }</style>
8
+ <div class="decoy"></div>
9
+ <div style="position:relative;"></div>
10
+ <div data-attr="decoy"></div>
11
+ </body>
12
+ </html>
@@ -0,0 +1,6 @@
1
+ # element hashes
2
+ sha256-ZRXDQAaaiAOOtUbmDL5Ty2JR8Zf1AonXn6DCmchNhvk=
3
+ sha256-n5JetA0VmZdQj4zgyixeiOgx9wfnK2oa9/zb8+xvZks=
4
+ # attribute hashes
5
+ sha256-iYwYhiMcsGmXCUzLEpEzZNz5dINrlkqf1sLbLhEcqGM=
6
+ #
@@ -0,0 +1,6 @@
1
+ # element hashes
2
+ sha384-fR76DAgrmWR4v7rpv4JBvCihgpwA2GRb4b6e4o+ThYYMJ4FQLX3l+EwzySAPxzbN
3
+ sha384-vSmTBkQfDGIq3/4cHWOHpAwAHxMhwPY/5vxKe4XCcKVmQaAN0BYDoZeZu5n0qHjS
4
+ # attribute hashes
5
+ sha384-YHRSKUJbAwb1DUQ5EcSZ+IOjmjTr9Bcrv8E2oHbMS45tF1M/tNHACnjgCr6L+GXc
6
+ #
@@ -0,0 +1,6 @@
1
+ # element hashes
2
+ sha512-dtOFpumNJPKES8S72UuTAXS6daEMYQKpRpCzlFAto/DHdx7fH0KK8sO9LjLpOjVWrDXE4G+MtisGdWa19wieSg==
3
+ sha512-mZQHDGDpL2tkHm3IWxwlZQUEwzH27l1bGvq4qo/9T/Ef52z0J2TGEEeOgp8INb/fGFHk4NjteTNwDTeuqb9rrw==
4
+ # attribute hashes
5
+ sha512-aOZs3PhtrZbzR6+CPKnKpStcJBq1Y4lw/+SwV5pKL52hpcq5pj+tTLv7x2uegYLFIqZ89KnfhRsrVSrQ3V3XDw==
6
+ #
@@ -0,0 +1,9 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <style>* { display: none; }</style>
5
+ </head>
6
+ <body>
7
+ <style>div { background: red; }</style>
8
+ </body>
9
+ </html>
@@ -0,0 +1,5 @@
1
+ # element hashes
2
+ sha256-ZRXDQAaaiAOOtUbmDL5Ty2JR8Zf1AonXn6DCmchNhvk=
3
+ sha256-n5JetA0VmZdQj4zgyixeiOgx9wfnK2oa9/zb8+xvZks=
4
+ # attribute hashes
5
+ #
@@ -0,0 +1,5 @@
1
+ # element hashes
2
+ sha384-fR76DAgrmWR4v7rpv4JBvCihgpwA2GRb4b6e4o+ThYYMJ4FQLX3l+EwzySAPxzbN
3
+ sha384-vSmTBkQfDGIq3/4cHWOHpAwAHxMhwPY/5vxKe4XCcKVmQaAN0BYDoZeZu5n0qHjS
4
+ # attribute hashes
5
+ #
@@ -0,0 +1,5 @@
1
+ # element hashes
2
+ sha512-dtOFpumNJPKES8S72UuTAXS6daEMYQKpRpCzlFAto/DHdx7fH0KK8sO9LjLpOjVWrDXE4G+MtisGdWa19wieSg==
3
+ sha512-mZQHDGDpL2tkHm3IWxwlZQUEwzH27l1bGvq4qo/9T/Ef52z0J2TGEEeOgp8INb/fGFHk4NjteTNwDTeuqb9rrw==
4
+ # attribute hashes
5
+ #
@@ -0,0 +1,7 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head></head>
4
+ <body>
5
+ <script src="test.js"></script>
6
+ </body>
7
+ </html>
File without changes
File without changes
File without changes
@@ -0,0 +1,7 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head></head>
4
+ <body>
5
+ <script>"hello world"</script>
6
+ </body>
7
+ </html>
@@ -0,0 +1,4 @@
1
+ # element hashes
2
+ sha256-nd7+RDWyHZAUOeVG1UoUoXWjSTuf2PvzjZ6m08v3CCY=
3
+ # attribute hashes
4
+ #
@@ -0,0 +1,4 @@
1
+ # element hashes
2
+ sha384-WmpTK6zbDiu5hx1ojbwG5VsNrqhW+OahJWEgoWt05o63pvZvCdwNIanHJLoyr3SD
3
+ # attribute hashes
4
+ #
@@ -0,0 +1,4 @@
1
+ # element hashes
2
+ sha512-qAJOafTHO8uHcv2M4vRRoaHnkd7h/xuiv+DkboPHj8WwHNcevfKTc4Wt0OqnaGuRpeKnxLXBv4VByLKSvuGZqQ==
3
+ # attribute hashes
4
+ #
@@ -0,0 +1,8 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <style>* { display: none; }</style>
5
+ </head>
6
+ <body>
7
+ </body>
8
+ </html>
@@ -0,0 +1,4 @@
1
+ # element hashes
2
+ sha256-ZRXDQAaaiAOOtUbmDL5Ty2JR8Zf1AonXn6DCmchNhvk=
3
+ # attribute hashes
4
+ #
@@ -0,0 +1,4 @@
1
+ # element hashes
2
+ sha384-fR76DAgrmWR4v7rpv4JBvCihgpwA2GRb4b6e4o+ThYYMJ4FQLX3l+EwzySAPxzbN
3
+ # attribute hashes
4
+ #
@@ -0,0 +1,4 @@
1
+ # element hashes
2
+ sha512-dtOFpumNJPKES8S72UuTAXS6daEMYQKpRpCzlFAto/DHdx7fH0KK8sO9LjLpOjVWrDXE4G+MtisGdWa19wieSg==
3
+ # attribute hashes
4
+ #
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Test entry
3
+ */
4
+ /* eslint-env jest */
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+ const hashstream = require('./lib').default;
8
+ const Vinyl = require('vinyl');
9
+
10
+ require('@babel/register');
11
+
12
+ function fixtures (glob) {
13
+ return path.join(__dirname, 'fixtures', glob);
14
+ }
15
+
16
+ function parseHashFixture (fixtureFilename) {
17
+ const result = {
18
+ elements: [],
19
+ attributes: [],
20
+ get all () {
21
+ return this.elements.concat(this.attributes);
22
+ }
23
+ };
24
+
25
+ try {
26
+ const re = /#\s*element hashes\s*(?<elements>[^#]+)#\s*attribute hashes\s*(?<attributes>[^#]+)\s*/im;
27
+ const m = fs.readFileSync(fixtureFilename, { encoding: 'utf8' }).match(re);
28
+ const elements = m?.groups?.elements;
29
+ const attributes = m?.groups?.attributes;
30
+ if (elements) {
31
+ result.elements.push(...elements.replace(/\s+/g, ' ').split(/\s+/).filter(h => h.length > 0));
32
+ }
33
+ if (attributes) {
34
+ result.attributes.push(...attributes.replace(/\s+/g, ' ').split(/\s+/).filter(h => h.length > 0));
35
+ }
36
+ }
37
+ catch (e) { /* ignore */ }
38
+
39
+ return result;
40
+ }
41
+
42
+ function onStreamError (err) {
43
+ throw new Error(err.message);
44
+ }
45
+
46
+ function onStreamFinish (expectedHashes, actualHashes, done) {
47
+ Object.keys(expectedHashes).forEach(what => {
48
+ expect(actualHashes[what].all.join(' ')).toEqual(expectedHashes[what].all.join(' '));
49
+ Object.keys(expectedHashes[what]).forEach(which => {
50
+ expect(actualHashes[what][which].length).toEqual(expectedHashes[what][which].length);
51
+ for (let i = 0; i < expectedHashes[what][which].length; ++i) {
52
+ expect(actualHashes[what][which][i]).toEqual(expectedHashes[what][which][i]);
53
+ }
54
+ });
55
+ });
56
+ done();
57
+ }
58
+
59
+ function run (name, algo, replace, {
60
+ hashFixtureScript = 'none',
61
+ hashFixtureStyle = 'none'
62
+ } = {}, done) {
63
+ const srcFile = new Vinyl({
64
+ path: fixtures(`${name}.html`),
65
+ cwd: 'test/',
66
+ base: fixtures(''),
67
+ contents: fs.readFileSync(fixtures(`${name}.html`))
68
+ });
69
+
70
+ const scriptFixture = fixtures(`${hashFixtureScript}.${algo}`);
71
+ const styleFixture = fixtures(`${hashFixtureStyle}.${algo}`);
72
+ const expectedHashes = {
73
+ script: parseHashFixture(scriptFixture),
74
+ style: parseHashFixture(styleFixture)
75
+ };
76
+ const actualHashes = {
77
+ script: {
78
+ elements: [],
79
+ attributes: []
80
+ },
81
+ style: {
82
+ elements: [],
83
+ attributes: []
84
+ }
85
+ };
86
+
87
+ const stream = hashstream({
88
+ algo,
89
+ replace,
90
+ callback: (path, hashes, contents) => {
91
+ expect(path).toEqual(fixtures(`${name}.html`));
92
+ if (replace) {
93
+ expect(contents).toEqual(srcFile.contents.toString());
94
+ }
95
+
96
+ Object.keys(hashes).forEach(what => {
97
+ Object.defineProperty(actualHashes[what], 'all', {
98
+ get: Object.getOwnPropertyDescriptor(hashes[what], 'all').get
99
+ });
100
+ Object.keys(hashes[what]).forEach(which => {
101
+ actualHashes[what][which].push(...hashes[what][which].map(x => x.replace(/'/g, '')));
102
+ });
103
+ });
104
+
105
+ return contents;
106
+ }
107
+ });
108
+
109
+ stream.on('error', onStreamError);
110
+ stream.on('finish', onStreamFinish.bind(null, expectedHashes, actualHashes, done));
111
+
112
+ stream.write(srcFile);
113
+ stream.end();
114
+ }
115
+
116
+ describe('should hash scripts correctly', () => {
117
+ const name = 'single-script';
118
+ const hashFixtureScript = name;
119
+ it('#sha256', done => { run(name, 'sha256', false, { hashFixtureScript }, done); });
120
+ it('#sha384', done => { run(name, 'sha384', false, { hashFixtureScript }, done); });
121
+ it('#sha512', done => { run(name, 'sha512', false, { hashFixtureScript }, done); });
122
+ });
123
+
124
+ describe('should hash styles correctly', () => {
125
+ const name = 'single-style';
126
+ const hashFixtureStyle = name;
127
+ it('#sha256', done => { run(name, 'sha256', false, { hashFixtureStyle }, done); });
128
+ it('#sha384', done => { run(name, 'sha384', false, { hashFixtureStyle }, done); });
129
+ it('#sha512', done => { run(name, 'sha512', false, { hashFixtureStyle }, done); });
130
+ });
131
+
132
+ describe('should hash multiple script tags', () => {
133
+ const name = 'multiple-scripts';
134
+ const hashFixtureScript = name;
135
+ it('#sha256', done => { run(name, 'sha256', false, { hashFixtureScript }, done); });
136
+ it('#sha384', done => { run(name, 'sha384', false, { hashFixtureScript }, done); });
137
+ it('#sha512', done => { run(name, 'sha512', false, { hashFixtureScript }, done); });
138
+ });
139
+
140
+ describe('should hash multiple style tags', () => {
141
+ const name = 'multiple-style';
142
+ const hashFixtureStyle = name;
143
+ it('#sha256', done => { run(name, 'sha256', false, { hashFixtureStyle }, done); });
144
+ it('#sha384', done => { run(name, 'sha384', false, { hashFixtureStyle }, done); });
145
+ it('#sha512', done => { run(name, 'sha512', false, { hashFixtureStyle }, done); });
146
+ });
147
+
148
+ describe('should ignore scripts with src attribute', () => {
149
+ const name = 'script-src';
150
+ const hashFixtureScript = name;
151
+ it('#sha256', done => { run(name, 'sha256', false, { hashFixtureScript }, done); });
152
+ it('#sha384', done => { run(name, 'sha384', false, { hashFixtureScript }, done); });
153
+ it('#sha512', done => { run(name, 'sha512', false, { hashFixtureScript }, done); });
154
+ });
155
+
156
+ it('should throw an exception on invalid algo', () => {
157
+ expect(() => hashstream({ algo: 'invalid' })).toThrow();
158
+ });
159
+
160
+ it('should throw an exception on invalid callbacks', () => {
161
+ expect(() => hashstream({})).toThrow();
162
+ });
163
+
164
+ describe('should hash multiple script tags and attributes', () => {
165
+ const name = 'multiple-scripts-attr';
166
+ const hashFixtureScript = name;
167
+ it('#sha256', done => { run(name, 'sha256', false, { hashFixtureScript }, done); });
168
+ it('#sha384', done => { run(name, 'sha384', false, { hashFixtureScript }, done); });
169
+ it('#sha512', done => { run(name, 'sha512', false, { hashFixtureScript }, done); });
170
+ });
171
+
172
+ describe('should hash multiple style tags and attributes', () => {
173
+ const name = 'multiple-style-attr';
174
+ const hashFixtureStyle = name;
175
+ it('#sha256', done => { run(name, 'sha256', false, { hashFixtureStyle }, done); });
176
+ it('#sha384', done => { run(name, 'sha384', false, { hashFixtureStyle }, done); });
177
+ it('#sha512', done => { run(name, 'sha512', false, { hashFixtureStyle }, done); });
178
+ });
179
+
180
+ describe('should hash multiple style tags and attributes (REPLACE OPTION)', () => {
181
+ const name = 'multiple-style-attr';
182
+ const hashFixtureStyle = name;
183
+ it('#sha256', done => { run(name, 'sha256', true, { hashFixtureStyle }, done); });
184
+ it('#sha384', done => { run(name, 'sha384', true, { hashFixtureStyle }, done); });
185
+ it('#sha512', done => { run(name, 'sha512', true, { hashFixtureStyle }, done); });
186
+ });
187
+
188
+ describe('should hash multiple scripts and styles, elements and attributes', () => {
189
+ const name = 'multiple-scripts-styles';
190
+ const hashFixtureScript = `${name}-script`;
191
+ const hashFixtureStyle = `${name}-style`;
192
+ it('#sha256', done => { run (name, 'sha256', false, { hashFixtureScript, hashFixtureStyle }, done); });
193
+ it('#sha384', done => { run (name, 'sha384', false, { hashFixtureScript, hashFixtureStyle }, done); });
194
+ it('#sha512', done => { run (name, 'sha512', false, { hashFixtureScript, hashFixtureStyle }, done); });
195
+ });
@@ -0,0 +1,11 @@
1
+ module.exports = {
2
+ presets: [
3
+ [
4
+ '@babel/preset-env', {
5
+ targets: {
6
+ node: '16'
7
+ }
8
+ }
9
+ ]
10
+ ]
11
+ };
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "default", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _index.default;
10
+ }
11
+ });
12
+
13
+ var _index = _interopRequireDefault(require("./lib/index"));
14
+
15
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = hashstream;
7
+
8
+ var _cheerio = _interopRequireDefault(require("cheerio"));
9
+
10
+ var _through = _interopRequireDefault(require("through2"));
11
+
12
+ var _crypto = _interopRequireDefault(require("crypto"));
13
+
14
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
+
16
+ /**
17
+ * CSP Hashes.
18
+ *
19
+ * Return a Vinyl transform object stream to process html files for
20
+ * generating the required CSP hashes for inline and attribute scripts, styles.
21
+ *
22
+ * Copyright (c) 2022 Alex Grant (@localnerve), LocalNerve LLC
23
+ * Licensed under the MIT license.
24
+ */
25
+
26
+ /**
27
+ * Collect all CSP Hashes and fill the given `hashes` structure.
28
+ *
29
+ * @param {Function} hashFn - Creates and formats a csp hash
30
+ * @param {Buffer} html - The html content
31
+ * @param {Object} hashes - The hash structure to fill
32
+ */
33
+ function collectHashes(hashFn, html, hashes) {
34
+ const $ = _cheerio.default.load(html);
35
+
36
+ Object.keys(hashes).forEach(what => {
37
+ hashes[what].elements = $(`${what}:not([src])`).map((i, el) => hashFn($(el).html())).toArray();
38
+ });
39
+ hashes.style.attributes.push(...$('[style]').map((i, el) => hashFn($(el).attr('style'))).toArray());
40
+ const eventHandlerRe = /^on/i;
41
+ const jsUrlRe = /^javascript:/i;
42
+ $('*').each(function (i, el) {
43
+ for (const attrName in el.attribs) {
44
+ if (eventHandlerRe.test(attrName)) {
45
+ hashes.script.attributes.push(hashFn(el.attribs[attrName]));
46
+ }
47
+
48
+ if (jsUrlRe.test(el.attribs[attrName])) {
49
+ hashes.script.attributes.push(hashFn(el.attribs[attrName].split(jsUrlRe)[1]));
50
+ }
51
+ }
52
+ });
53
+ }
54
+ /**
55
+ * hashstream
56
+ * Accepts the processing options and returns the Vinyl transform object stream.
57
+ *
58
+ * @param {Object} options
59
+ * @param {Function} options.callback - Function to call to process the csp hashes.
60
+ * @param {String} [options.algo] - hash algorithm, default sha256. Can be sha384, sha512.
61
+ * @param {Boolean} [options.replace] - True if callback is used for meta html replacements, defaults to false.
62
+ * @returns Transform object stream to process Vinyl objects.
63
+ */
64
+
65
+
66
+ function hashstream({
67
+ algo = 'sha256',
68
+ replace = false,
69
+ callback = null
70
+ } = {}) {
71
+ if (!/^sha(256|384|512)$/.test(algo)) {
72
+ throw new Error('algo option must be one of "sha256", "sha384", or "sha512" only.');
73
+ }
74
+
75
+ if (typeof callback !== 'function') {
76
+ throw new Error('callback option must be a valid function.');
77
+ }
78
+
79
+ const createHash = r => _crypto.default.createHash(algo).update(r).digest('base64');
80
+
81
+ const formatHash = h => `'${algo}-${h}'`;
82
+
83
+ const makeCSPHash = s => formatHash(createHash(s));
84
+
85
+ return _through.default.obj((vinyl, enc, done) => {
86
+ const path = vinyl.path;
87
+ const content = vinyl.contents;
88
+ const hashes = {
89
+ script: {
90
+ elements: [],
91
+ attributes: [],
92
+
93
+ get all() {
94
+ return this.elements.concat(this.attributes);
95
+ }
96
+
97
+ },
98
+ style: {
99
+ elements: [],
100
+ attributes: [],
101
+
102
+ get all() {
103
+ return this.elements.concat(this.attributes);
104
+ }
105
+
106
+ }
107
+ };
108
+ collectHashes(makeCSPHash, content, hashes);
109
+
110
+ if (replace) {
111
+ const s = callback(path, hashes, content.toString());
112
+ vinyl.contents = Buffer.from(s, enc);
113
+ } else {
114
+ callback(path, hashes);
115
+ }
116
+
117
+ done(null, vinyl);
118
+ });
119
+ }
package/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * CSP Hashes.
3
+ *
4
+ * Return a Vinyl transform object stream to process html files for
5
+ * generating the required CSP hashes for inline and attribute scripts, styles.
6
+ *
7
+ * Copyright (c) 2022 Alex Grant (@localnerve), LocalNerve LLC
8
+ * Licensed under the MIT license.
9
+ */
10
+ /* eslint-env node */
11
+ export { default } from './lib/index';
package/jest.config.js ADDED
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+ collectCoverage: true,
3
+ coverageDirectory: 'coverage',
4
+ verbose: true,
5
+ testEnvironment: 'node',
6
+ testPathIgnorePatterns: [
7
+ '/node_modules/',
8
+ '/tmp'
9
+ ]
10
+ };
package/lib/index.js ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * CSP Hashes.
3
+ *
4
+ * Return a Vinyl transform object stream to process html files for
5
+ * generating the required CSP hashes for inline and attribute scripts, styles.
6
+ *
7
+ * Copyright (c) 2022 Alex Grant (@localnerve), LocalNerve LLC
8
+ * Licensed under the MIT license.
9
+ */
10
+ import cheerio from 'cheerio';
11
+ import through2 from 'through2';
12
+ import crypto from 'crypto';
13
+
14
+ /**
15
+ * Collect all CSP Hashes and fill the given `hashes` structure.
16
+ *
17
+ * @param {Function} hashFn - Creates and formats a csp hash
18
+ * @param {Buffer} html - The html content
19
+ * @param {Object} hashes - The hash structure to fill
20
+ */
21
+ function collectHashes (hashFn, html, hashes) {
22
+ const $ = cheerio.load(html);
23
+
24
+ Object.keys(hashes).forEach(what => {
25
+ hashes[what].elements = $(`${what}:not([src])`).map(
26
+ (i, el) => hashFn($(el).html())
27
+ ).toArray();
28
+ });
29
+
30
+ hashes.style.attributes.push(
31
+ ...$('[style]').map((i, el) => hashFn($(el).attr('style'))).toArray()
32
+ );
33
+
34
+ const eventHandlerRe = /^on/i;
35
+ const jsUrlRe = /^javascript:/i;
36
+
37
+ $('*').each(function (i, el) {
38
+ for (const attrName in el.attribs) {
39
+ if (eventHandlerRe.test(attrName)) {
40
+ hashes.script.attributes.push(
41
+ hashFn(el.attribs[attrName])
42
+ );
43
+ }
44
+ if (jsUrlRe.test(el.attribs[attrName])) {
45
+ hashes.script.attributes.push(
46
+ hashFn(el.attribs[attrName].split(jsUrlRe)[1])
47
+ );
48
+ }
49
+ }
50
+ });
51
+ }
52
+
53
+ /**
54
+ * hashstream
55
+ * Accepts the processing options and returns the Vinyl transform object stream.
56
+ *
57
+ * @param {Object} options
58
+ * @param {Function} options.callback - Function to call to process the csp hashes.
59
+ * @param {String} [options.algo] - hash algorithm, default sha256. Can be sha384, sha512.
60
+ * @param {Boolean} [options.replace] - True if callback is used for meta html replacements, defaults to false.
61
+ * @returns Transform object stream to process Vinyl objects.
62
+ */
63
+ export default function hashstream ({
64
+ algo = 'sha256',
65
+ replace = false,
66
+ callback = null
67
+ } = {}) {
68
+
69
+ if (!/^sha(256|384|512)$/.test(algo)) {
70
+ throw new Error('algo option must be one of "sha256", "sha384", or "sha512" only.');
71
+ }
72
+
73
+ if (typeof callback !== 'function') {
74
+ throw new Error('callback option must be a valid function.');
75
+ }
76
+
77
+ const createHash = r => crypto.createHash(algo).update(r).digest('base64');
78
+ const formatHash = h => `'${algo}-${h}'`;
79
+ const makeCSPHash = s => formatHash(createHash(s));
80
+
81
+ return through2.obj((vinyl, enc, done) => {
82
+ const path = vinyl.path;
83
+ const content = vinyl.contents;
84
+
85
+ const hashes = {
86
+ script: {
87
+ elements: [],
88
+ attributes: [],
89
+ get all () {
90
+ return this.elements.concat(this.attributes);
91
+ }
92
+ },
93
+ style: {
94
+ elements: [],
95
+ attributes: [],
96
+ get all () {
97
+ return this.elements.concat(this.attributes);
98
+ }
99
+ }
100
+ };
101
+
102
+ collectHashes(makeCSPHash, content, hashes);
103
+
104
+ if (replace) {
105
+ const s = callback(path, hashes, content.toString());
106
+ vinyl.contents = Buffer.from(s, enc);
107
+ } else {
108
+ callback(path, hashes);
109
+ }
110
+
111
+ done(null, vinyl);
112
+ });
113
+ }
package/license.md ADDED
@@ -0,0 +1,8 @@
1
+ Copyright 2022, Alex Grant, LocalNerve, LLC
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@localnerve/csp-hashes",
3
+ "version": "0.1.3",
4
+ "description": "Flexible library to handle CSP hashes at build time",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "lint": "eslint .",
8
+ "transpile": "rimraf ./dist && babel --out-dir ./dist index.js && babel --out-dir ./dist/lib ./lib",
9
+ "prepublishOnly": "npm run transpile",
10
+ "pretest": "node -e 'try{require(\"fs\").symlinkSync(\"../lib\", \"./__tests__/lib\");}catch(e){}'",
11
+ "test": "jest",
12
+ "test:debug": "node --inspect-brk ./node_modules/.bin/jest"
13
+ },
14
+ "devDependencies": {
15
+ "@babel/cli": "^7.17.6",
16
+ "@babel/preset-env": "^7.16.11",
17
+ "@babel/register": "^7.17.7",
18
+ "coveralls": "^3.1.1",
19
+ "eslint": "^8.11.0",
20
+ "jest": "^27.5.1",
21
+ "rimraf": "^3.0.2",
22
+ "vinyl": "^2.2.1"
23
+ },
24
+ "dependencies": {
25
+ "cheerio": "^1.0.0-rc.3",
26
+ "through2": "^4.0.2"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/localnerve/csp-hashes.git"
31
+ },
32
+ "keywords": [
33
+ "CSP",
34
+ "hashes",
35
+ "gulp"
36
+ ],
37
+ "author": "Alex Grant <alex@localnerve.com>",
38
+ "license": "MIT",
39
+ "bugs": {
40
+ "url": "https://github.com/localnerve/csp-hashes/issues"
41
+ },
42
+ "homepage": "https://github.com/localnerve/csp-hashes#readme",
43
+ "engines": {
44
+ "node": "14 - 16"
45
+ }
46
+ }
package/readme.md ADDED
@@ -0,0 +1,129 @@
1
+ # csp-hashes
2
+
3
+ > Flexible build library to generate script and style hashes for CSP headers or meta tags
4
+
5
+ [![npm version](https://badge.fury.io/js/%40localnerve%2Fcsp-hashes.svg)](https://badge.fury.io/js/%40localnerve%2Fcsp-hashes)
6
+ ![Verify](https://github.com/localnerve/csp-hashes/workflows/Verify/badge.svg)
7
+ [![Coverage Status](https://coveralls.io/repos/github/localnerve/csp-hashes/badge.svg?branch=main)](https://coveralls.io/github/localnerve/csp-hashes?branch=main)
8
+
9
+ ## Contents
10
+ + [Overview](#overview)
11
+ + [API](#API)
12
+ + [Options](#options)
13
+ + [Callback Function](#callback-function)
14
+ + [Callback Hashes Object](#callback-hashes-object)
15
+ + [Example Usage](#example-usage)
16
+ + [CSP Headers](#build-step-to-maintain-csp-headers)
17
+ + [Meta Tag](#build-step-to-maintain-csp-meta-tags)
18
+ + [MIT License](#license)
19
+
20
+ ## Overview
21
+ This library generates script and style inline element and attribute hashes. It is for use in the generation of HTTP content security policy (CSP) headers or to replace/update Meta tags as a website build step.
22
+
23
+ ## API
24
+ This library exports a single function that takes options and returns a transform stream in object mode that operates on [Vinyl](https://github.com/gulpjs/vinyl) objects in [Gulp](https://github.com/gulpjs/gulp). The only required option is a [`callback`](#callback-function) function.
25
+
26
+ ```
27
+ Stream hashstream ({
28
+ callback,
29
+ replace = false,
30
+ algo = 'sha256'
31
+ })
32
+ ```
33
+
34
+ ### Options
35
+
36
+ + {Function} **callback** - Required - A [function](#callback-function) to process the hashes. Receives file contents and must return new file contents if `replace` option is true.
37
+ + {Boolean} **\[replace\]** - Optional - Defaults to `false`, set to true to indicate your `callback` function returns new file contents to replace the original.
38
+ + {String} **\[algo\]** - Optional - Defaults to `'sha256'`, can be one of 'sha256', 'sha384' or 'sha512'.
39
+
40
+
41
+ #### Callback Function
42
+ A callback function is required to process the CSP hashes collected by this library for your build.
43
+
44
+ ```
45
+ callback(path, hashes[, contents])
46
+ ```
47
+
48
+ + {String} **path** - The local filesystem path to the original file. Use to create your own rules and/or a path to the web resource for writing header rules.
49
+ + {Object} **hashes** - The script and style inline element and attribute hashes for the current file. See object [format](callback-hashes-object) for details.
50
+ + {String} **\[contents\]** - The original file contents. Only sent if the `replace` option is true, in which case you **must** return new file contents.
51
+
52
+ ##### Callback Hashes Object
53
+ The callback hashes object contains all of the inline element and attribute hashes for scripts and styles in the current file being processed. The object has the following format:
54
+
55
+ ```javascript
56
+ // `hashes` object:
57
+ {
58
+ script: {
59
+ elements: [],
60
+ attributes: [],
61
+ get all () {
62
+ return this.elements.concat(this.attributes);
63
+ }
64
+ },
65
+ style: {
66
+ elements: [],
67
+ attributes: [],
68
+ get all () {
69
+ return this.elements.concat(this.attributes);
70
+ }
71
+ }
72
+ }
73
+ ```
74
+ The object structure allows you to direct the hashes to any CSP header directive layout you might use. You can use `script-src` or `style-src` alone and concatenate the element and attribute hashes together into one list using the `all` getter property, or you can use `script-src-attr` and `style-src-attr` separately, whatever is a more secure/optimal policy for your situation.
75
+ **NOTE**
76
+ The `hashes` object structure is always the same. If there are no elements or attributes of script or style in the current file, the arrays are just empty (not null).
77
+
78
+ ## Example Usage
79
+
80
+ ### Build Step to Maintain CSP Headers
81
+ In this example, a build step gets the hashes for every html file under the `dist` directory, then for each html file, updates the header rules for the host service being deployed to.
82
+
83
+ ```javascript
84
+ import gulp from 'gulp';
85
+ import hashstream from '@localnerve/csp-hashes';
86
+ import { cspHeaderRules } from './host-header-rules';
87
+
88
+ export function cspHeaders (settings) {
89
+ const { dist } = settings;
90
+
91
+ return gulp.src(`${dist}/**/*.html`)
92
+ .pipe(hashstream({
93
+ callback: (path, hashes) => {
94
+ const webPath = path.replace(dist, '');
95
+ cspHeaderRules.updateHashes(webPath, 'script-src', hashes.script.elements.join(' '));
96
+ cspHeaderRules.updateHashes(webPath, 'script-src-attr', hashes.script.attributes.join(' '));
97
+ cspHeaderRules.updateHashes(webPath, 'style-src', hashes.style.elements.join(' '));
98
+ cspHeaderRules.updateHashes(webPath, 'style-src-attr', hashes.style.attributes.join(' '));
99
+ }
100
+ }))
101
+ }
102
+ ```
103
+
104
+ ### Build Step to Maintain CSP Meta Tags
105
+ In this example, a build step gets the hashes for every html file under the `dist` directory, then updates each html file's meta tags to include the hashes after 'self', preserving any other rules before it. This example uses the `all` property to get the combined element and attribute hashes together.
106
+
107
+ ```javascript
108
+ import gulp from 'gulp';
109
+ import hashstream from '@localnerve/csp-hashes';
110
+
111
+ export function cspMetaTags (settings) {
112
+ const { dist } = settings;
113
+
114
+ return gulp.src(`${dist}/**/*.html`)
115
+ .pipe(hashstream({
116
+ replace: true,
117
+ callback: (p /* not used */, hashes, contents) => {
118
+ return contents
119
+ .replace(/script-src (.+) 'self'/, `script-src $1 'self' ${hashes.script.all.join(' ')}`)
120
+ .replace(/style-src (.+) 'self'/, `style-src $1 'self' ${hashes.style.all.join(' ')}`);
121
+ }
122
+ }))
123
+ .pipe(gulp.dest(dist));
124
+ }
125
+ ```
126
+
127
+ ## LICENSE
128
+
129
+ * [MIT, Alex Grant, LocalNerve, LLC](license.md)