@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.
- package/.eslintignore +5 -0
- package/.eslintrc.json +22 -0
- package/.github/workflows/verify.yml +32 -0
- package/.vscode/launch.json +14 -0
- package/__tests__/fixtures/multiple-scripts-attr.html +17 -0
- package/__tests__/fixtures/multiple-scripts-attr.sha256 +7 -0
- package/__tests__/fixtures/multiple-scripts-attr.sha384 +7 -0
- package/__tests__/fixtures/multiple-scripts-attr.sha512 +7 -0
- package/__tests__/fixtures/multiple-scripts-styles-script.sha256 +7 -0
- package/__tests__/fixtures/multiple-scripts-styles-script.sha384 +7 -0
- package/__tests__/fixtures/multiple-scripts-styles-script.sha512 +7 -0
- package/__tests__/fixtures/multiple-scripts-styles-style.sha256 +6 -0
- package/__tests__/fixtures/multiple-scripts-styles-style.sha384 +6 -0
- package/__tests__/fixtures/multiple-scripts-styles-style.sha512 +6 -0
- package/__tests__/fixtures/multiple-scripts-styles.html +20 -0
- package/__tests__/fixtures/multiple-scripts.html +13 -0
- package/__tests__/fixtures/multiple-scripts.sha256 +5 -0
- package/__tests__/fixtures/multiple-scripts.sha384 +5 -0
- package/__tests__/fixtures/multiple-scripts.sha512 +5 -0
- package/__tests__/fixtures/multiple-style-attr.html +12 -0
- package/__tests__/fixtures/multiple-style-attr.sha256 +6 -0
- package/__tests__/fixtures/multiple-style-attr.sha384 +6 -0
- package/__tests__/fixtures/multiple-style-attr.sha512 +6 -0
- package/__tests__/fixtures/multiple-style.html +9 -0
- package/__tests__/fixtures/multiple-style.sha256 +5 -0
- package/__tests__/fixtures/multiple-style.sha384 +5 -0
- package/__tests__/fixtures/multiple-style.sha512 +5 -0
- package/__tests__/fixtures/script-src.html +7 -0
- package/__tests__/fixtures/script-src.sha256 +0 -0
- package/__tests__/fixtures/script-src.sha384 +0 -0
- package/__tests__/fixtures/script-src.sha512 +0 -0
- package/__tests__/fixtures/single-script.html +7 -0
- package/__tests__/fixtures/single-script.sha256 +4 -0
- package/__tests__/fixtures/single-script.sha384 +4 -0
- package/__tests__/fixtures/single-script.sha512 +4 -0
- package/__tests__/fixtures/single-style.html +8 -0
- package/__tests__/fixtures/single-style.sha256 +4 -0
- package/__tests__/fixtures/single-style.sha384 +4 -0
- package/__tests__/fixtures/single-style.sha512 +4 -0
- package/__tests__/index.test.js +195 -0
- package/babel.config.js +11 -0
- package/dist/index.js +15 -0
- package/dist/lib/index.js +119 -0
- package/index.js +11 -0
- package/jest.config.js +10 -0
- package/lib/index.js +113 -0
- package/license.md +8 -0
- package/package.json +46 -0
- package/readme.md +129 -0
package/.eslintignore
ADDED
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
|
+
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
|
+
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
|
+
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,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
|
+
#
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -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
|
+
});
|
package/babel.config.js
ADDED
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
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
|
+
[](https://badge.fury.io/js/%40localnerve%2Fcsp-hashes)
|
|
6
|
+

|
|
7
|
+
[](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)
|