csprefabricate 0.3.1 → 1.0.0
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/CHANGELOG.md +41 -0
- package/README.md +32 -12
- package/dist/baseline.d.ts +8 -0
- package/dist/baseline.js +245 -0
- package/dist/helpers.js +15 -5
- package/dist/index.d.ts +5 -2
- package/dist/index.js +2 -0
- package/dist/utils.js +3 -2
- package/package.json +50 -14
- package/dist/test/helpers.test.d.ts +0 -1
- package/dist/test/helpers.test.js +0 -123
- package/dist/test/utils.test.d.ts +0 -1
- package/dist/test/utils.test.js +0 -108
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.0.0](https://github.com/JamesToohey/csprefabricate/compare/v0.4.0...v1.0.0) (2025-06-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* updates for 1.0 release ([#29](https://github.com/JamesToohey/csprefabricate/issues/29))
|
|
9
|
+
|
|
10
|
+
### Documentation
|
|
11
|
+
|
|
12
|
+
* updates for 1.0 release ([#29](https://github.com/JamesToohey/csprefabricate/issues/29)) ([1629ea1](https://github.com/JamesToohey/csprefabricate/commit/1629ea1e415142a54f6bd538832dbca4cdf1f179))
|
|
13
|
+
|
|
14
|
+
## [0.4.0](https://github.com/JamesToohey/csprefabricate/compare/v0.3.1...v0.4.0) (2025-05-30)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* add baseline CSPs ([#25](https://github.com/JamesToohey/csprefabricate/issues/25)) ([6dd8588](https://github.com/JamesToohey/csprefabricate/commit/6dd858853d143de66979e74ba981844c2b1e28d3))
|
|
20
|
+
|
|
21
|
+
## [0.3.1](https://github.com/JamesToohey/csprefabricate/compare/v0.3.0...v0.3.1) (2025-05-27)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Bug Fixes
|
|
25
|
+
|
|
26
|
+
* fix publish command ([#23](https://github.com/JamesToohey/csprefabricate/issues/23)) ([7b537f3](https://github.com/JamesToohey/csprefabricate/commit/7b537f337e20c188a89ddb3dd3f2ab5ddefade04))
|
|
27
|
+
|
|
28
|
+
## [0.3.0](https://github.com/JamesToohey/csprefabricate/compare/v0.2.3...v0.3.0) (2025-05-26)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### Features
|
|
32
|
+
|
|
33
|
+
* Add warnings for common misconfigurations ([#21](https://github.com/JamesToohey/csprefabricate/issues/21)) ([d47858a](https://github.com/JamesToohey/csprefabricate/commit/d47858a04b777edec738b0f8ead23845795597a5))
|
|
34
|
+
|
|
35
|
+
## [0.2.3](https://github.com/JamesToohey/csprefabricate/compare/0.2.2...v0.2.3) (2025-05-25)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
### Bug Fixes
|
|
39
|
+
|
|
40
|
+
* give release please access to github actions ([#17](https://github.com/JamesToohey/csprefabricate/issues/17)) ([456d933](https://github.com/JamesToohey/csprefabricate/commit/456d933dcbb746943f1f0a921e96a5b54c6055e5))
|
|
41
|
+
* update contributing ([4588b6a](https://github.com/JamesToohey/csprefabricate/commit/4588b6a09731a08121f3a26612147518fbf50b05))
|
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# csprefabricate
|
|
1
|
+
# csprefabricate
|
|
2
2
|
|
|
3
|
-
**Generate a valid CSP with
|
|
3
|
+
**Generate a valid CSP with TypeScript.**
|
|
4
4
|
|
|
5
|
-
Content Security Policies (CSPs) are cumbersome strings that are
|
|
5
|
+
Content Security Policies (CSPs) are cumbersome strings that are frustrating to work with:
|
|
6
6
|
|
|
7
7
|
- Fickle syntax
|
|
8
|
-
-
|
|
8
|
+
- Duplication when multiple TLDs are required
|
|
9
9
|
- Easy to allow insecure configuration
|
|
10
10
|
|
|
11
11
|
This project aims to make creating useful and secure CSPs a more pleasant experience.
|
|
@@ -82,15 +82,19 @@ const cspString = create(csp);
|
|
|
82
82
|
import {create, Directive, ContentSecurityPolicy} from "csprefabricate";
|
|
83
83
|
|
|
84
84
|
const csp: ContentSecurityPolicy = {
|
|
85
|
-
[Directive.DEFAULT_SRC]: ["self"],
|
|
86
|
-
[Directive.SCRIPT_SRC]: ["self", "*.googletagmanager.com"],
|
|
85
|
+
[Directive.DEFAULT_SRC]: ["'self'"],
|
|
86
|
+
[Directive.SCRIPT_SRC]: ["'self'", "*.googletagmanager.com"],
|
|
87
|
+
[Directive.STYLE_SRC]: ["'self'"],
|
|
87
88
|
[Directive.IMG_SRC]: [
|
|
88
|
-
"self",
|
|
89
|
-
"
|
|
89
|
+
"'self'",
|
|
90
|
+
"https://*.google-analytics.com",
|
|
90
91
|
"https://*.googletagmanager.com",
|
|
91
92
|
],
|
|
93
|
+
[Directive.OBJECT_SRC]: ["'none'"],
|
|
94
|
+
[Directive.BASE_URI]: ["'self'"],
|
|
95
|
+
[Directive.FORM_ACTION]: ["'self'"],
|
|
92
96
|
[Directive.CONNECT_SRC]: [
|
|
93
|
-
"self",
|
|
97
|
+
"'self'",
|
|
94
98
|
"https://*.google-analytics.com",
|
|
95
99
|
"https://*.analytics.google.com",
|
|
96
100
|
"https://*.googletagmanager.com",
|
|
@@ -98,7 +102,7 @@ const csp: ContentSecurityPolicy = {
|
|
|
98
102
|
};
|
|
99
103
|
|
|
100
104
|
const cspString = create(csp);
|
|
101
|
-
// "default-src 'self'; script-src 'self' *.googletagmanager.com; img-src 'self'
|
|
105
|
+
// "default-src 'self'; script-src 'self' *.googletagmanager.com; style-src 'self'; img-src 'self' https://*.google-analytics.com https://*.googletagmanager.com; object-src 'none'; base-uri 'self'; form-action 'self'; connect-src 'self' https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com;"
|
|
102
106
|
```
|
|
103
107
|
|
|
104
108
|
### Example 3: Using TLD Expansion for Multiple Domains
|
|
@@ -114,6 +118,22 @@ const cspString = create(csp);
|
|
|
114
118
|
// "img-src 'self' *.example.com *.example.co.uk *.example.net;"
|
|
115
119
|
```
|
|
116
120
|
|
|
117
|
-
##
|
|
121
|
+
## Baseline Recommended CSPs
|
|
122
|
+
|
|
123
|
+
You can quickly generate a recommended Content Security Policy for common use cases using built-in baselines.
|
|
118
124
|
|
|
119
|
-
|
|
125
|
+
Available Baselines:
|
|
126
|
+
|
|
127
|
+
- BASELINE_STRICT_CSP
|
|
128
|
+
- GOOGLE_ANALYTICS_CSP
|
|
129
|
+
- GOOGLE_ANALYTICS_WITH_SIGNALS_CSP
|
|
130
|
+
|
|
131
|
+
### Google Analytics Baseline CSP
|
|
132
|
+
|
|
133
|
+
Allow Google Analytics and Tag Manager:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import {create, Baseline} from "csprefabricate";
|
|
137
|
+
|
|
138
|
+
const cspString = create(Baseline.GOOGLE_ANALYTICS_CSP);
|
|
139
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ContentSecurityPolicy } from "./types";
|
|
2
|
+
export declare const BASELINE_STRICT_CSP: ContentSecurityPolicy;
|
|
3
|
+
/**
|
|
4
|
+
* Google Analytics Content Security Policy based on the official guidelines.
|
|
5
|
+
* https://developers.google.com/tag-platform/security/guides/csp#google_analytics_4_google_analytics
|
|
6
|
+
*/
|
|
7
|
+
export declare const GOOGLE_ANALYTICS_CSP: ContentSecurityPolicy;
|
|
8
|
+
export declare const GOOGLE_ANALYTICS_WITH_SIGNALS_CSP: ContentSecurityPolicy;
|
package/dist/baseline.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { Directive } from "./types";
|
|
2
|
+
// List of supported domains for Google Signals from https://www.google.com/supported_domains
|
|
3
|
+
const googleSupportedTLDs = [
|
|
4
|
+
".com",
|
|
5
|
+
".ad",
|
|
6
|
+
".ae",
|
|
7
|
+
".com.af",
|
|
8
|
+
".com.ag",
|
|
9
|
+
".al",
|
|
10
|
+
".am",
|
|
11
|
+
".co.ao",
|
|
12
|
+
".com.ar",
|
|
13
|
+
".as",
|
|
14
|
+
".at",
|
|
15
|
+
".com.au",
|
|
16
|
+
".az",
|
|
17
|
+
".ba",
|
|
18
|
+
".com.bd",
|
|
19
|
+
".be",
|
|
20
|
+
".bf",
|
|
21
|
+
".bg",
|
|
22
|
+
".com.bh",
|
|
23
|
+
".bi",
|
|
24
|
+
".bj",
|
|
25
|
+
".com.bn",
|
|
26
|
+
".com.bo",
|
|
27
|
+
".com.br",
|
|
28
|
+
".bs",
|
|
29
|
+
".bt",
|
|
30
|
+
".co.bw",
|
|
31
|
+
".by",
|
|
32
|
+
".com.bz",
|
|
33
|
+
".ca",
|
|
34
|
+
".cd",
|
|
35
|
+
".cf",
|
|
36
|
+
".cg",
|
|
37
|
+
".ch",
|
|
38
|
+
".ci",
|
|
39
|
+
".co.ck",
|
|
40
|
+
".cl",
|
|
41
|
+
".cm",
|
|
42
|
+
".cn",
|
|
43
|
+
".com.co",
|
|
44
|
+
".co.cr",
|
|
45
|
+
".com.cu",
|
|
46
|
+
".cv",
|
|
47
|
+
".com.cy",
|
|
48
|
+
".cz",
|
|
49
|
+
".de",
|
|
50
|
+
".dj",
|
|
51
|
+
".dk",
|
|
52
|
+
".dm",
|
|
53
|
+
".com.do",
|
|
54
|
+
".dz",
|
|
55
|
+
".com.ec",
|
|
56
|
+
".ee",
|
|
57
|
+
".com.eg",
|
|
58
|
+
".es",
|
|
59
|
+
".com.et",
|
|
60
|
+
".fi",
|
|
61
|
+
".com.fj",
|
|
62
|
+
".fm",
|
|
63
|
+
".fr",
|
|
64
|
+
".ga",
|
|
65
|
+
".ge",
|
|
66
|
+
".gg",
|
|
67
|
+
".com.gh",
|
|
68
|
+
".com.gi",
|
|
69
|
+
".gl",
|
|
70
|
+
".gm",
|
|
71
|
+
".gr",
|
|
72
|
+
".com.gt",
|
|
73
|
+
".gy",
|
|
74
|
+
".com.hk",
|
|
75
|
+
".hn",
|
|
76
|
+
".hr",
|
|
77
|
+
".ht",
|
|
78
|
+
".hu",
|
|
79
|
+
".co.id",
|
|
80
|
+
".ie",
|
|
81
|
+
".co.il",
|
|
82
|
+
".im",
|
|
83
|
+
".co.in",
|
|
84
|
+
".iq",
|
|
85
|
+
".is",
|
|
86
|
+
".it",
|
|
87
|
+
".je",
|
|
88
|
+
".com.jm",
|
|
89
|
+
".jo",
|
|
90
|
+
".co.jp",
|
|
91
|
+
".co.ke",
|
|
92
|
+
".com.kh",
|
|
93
|
+
".ki",
|
|
94
|
+
".kg",
|
|
95
|
+
".co.kr",
|
|
96
|
+
".com.kw",
|
|
97
|
+
".kz",
|
|
98
|
+
".la",
|
|
99
|
+
".com.lb",
|
|
100
|
+
".li",
|
|
101
|
+
".lk",
|
|
102
|
+
".co.ls",
|
|
103
|
+
".lt",
|
|
104
|
+
".lu",
|
|
105
|
+
".lv",
|
|
106
|
+
".com.ly",
|
|
107
|
+
".co.ma",
|
|
108
|
+
".md",
|
|
109
|
+
".me",
|
|
110
|
+
".mg",
|
|
111
|
+
".mk",
|
|
112
|
+
".ml",
|
|
113
|
+
".com.mm",
|
|
114
|
+
".mn",
|
|
115
|
+
".com.mt",
|
|
116
|
+
".mu",
|
|
117
|
+
".mv",
|
|
118
|
+
".mw",
|
|
119
|
+
".com.mx",
|
|
120
|
+
".com.my",
|
|
121
|
+
".co.mz",
|
|
122
|
+
".com.na",
|
|
123
|
+
".com.ng",
|
|
124
|
+
".com.ni",
|
|
125
|
+
".ne",
|
|
126
|
+
".nl",
|
|
127
|
+
".no",
|
|
128
|
+
".com.np",
|
|
129
|
+
".nr",
|
|
130
|
+
".nu",
|
|
131
|
+
".co.nz",
|
|
132
|
+
".com.om",
|
|
133
|
+
".com.pa",
|
|
134
|
+
".com.pe",
|
|
135
|
+
".com.pg",
|
|
136
|
+
".com.ph",
|
|
137
|
+
".com.pk",
|
|
138
|
+
".pl",
|
|
139
|
+
".pn",
|
|
140
|
+
".com.pr",
|
|
141
|
+
".ps",
|
|
142
|
+
".pt",
|
|
143
|
+
".com.py",
|
|
144
|
+
".com.qa",
|
|
145
|
+
".ro",
|
|
146
|
+
".ru",
|
|
147
|
+
".rw",
|
|
148
|
+
".com.sa",
|
|
149
|
+
".com.sb",
|
|
150
|
+
".sc",
|
|
151
|
+
".se",
|
|
152
|
+
".com.sg",
|
|
153
|
+
".sh",
|
|
154
|
+
".si",
|
|
155
|
+
".sk",
|
|
156
|
+
".com.sl",
|
|
157
|
+
".sn",
|
|
158
|
+
".so",
|
|
159
|
+
".sm",
|
|
160
|
+
".sr",
|
|
161
|
+
".st",
|
|
162
|
+
".com.sv",
|
|
163
|
+
".td",
|
|
164
|
+
".tg",
|
|
165
|
+
".co.th",
|
|
166
|
+
".com.tj",
|
|
167
|
+
".tl",
|
|
168
|
+
".tm",
|
|
169
|
+
".tn",
|
|
170
|
+
".to",
|
|
171
|
+
".com.tr",
|
|
172
|
+
".tt",
|
|
173
|
+
".com.tw",
|
|
174
|
+
".co.tz",
|
|
175
|
+
".com.ua",
|
|
176
|
+
".co.ug",
|
|
177
|
+
".co.uk",
|
|
178
|
+
".com.uy",
|
|
179
|
+
".co.uz",
|
|
180
|
+
".com.vc",
|
|
181
|
+
".co.ve",
|
|
182
|
+
".co.vi",
|
|
183
|
+
".com.vn",
|
|
184
|
+
".vu",
|
|
185
|
+
".ws",
|
|
186
|
+
".rs",
|
|
187
|
+
".co.za",
|
|
188
|
+
".co.zm",
|
|
189
|
+
".co.zw",
|
|
190
|
+
".cat",
|
|
191
|
+
];
|
|
192
|
+
export const BASELINE_STRICT_CSP = {
|
|
193
|
+
[Directive.DEFAULT_SRC]: ["'self'"],
|
|
194
|
+
[Directive.SCRIPT_SRC]: ["'self'"],
|
|
195
|
+
[Directive.STYLE_SRC]: ["'self'"],
|
|
196
|
+
[Directive.IMG_SRC]: ["'self'"],
|
|
197
|
+
[Directive.OBJECT_SRC]: ["'none'"],
|
|
198
|
+
[Directive.BASE_URI]: ["'self'"],
|
|
199
|
+
[Directive.FORM_ACTION]: ["'self'"],
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* Google Analytics Content Security Policy based on the official guidelines.
|
|
203
|
+
* https://developers.google.com/tag-platform/security/guides/csp#google_analytics_4_google_analytics
|
|
204
|
+
*/
|
|
205
|
+
export const GOOGLE_ANALYTICS_CSP = {
|
|
206
|
+
...BASELINE_STRICT_CSP,
|
|
207
|
+
[Directive.DEFAULT_SRC]: ["'self'"],
|
|
208
|
+
[Directive.SCRIPT_SRC]: ["'self'", "*.googletagmanager.com"],
|
|
209
|
+
[Directive.IMG_SRC]: [
|
|
210
|
+
"'self'",
|
|
211
|
+
"https://*.google-analytics.com",
|
|
212
|
+
"https://*.googletagmanager.com",
|
|
213
|
+
],
|
|
214
|
+
[Directive.CONNECT_SRC]: [
|
|
215
|
+
"'self'",
|
|
216
|
+
"https://*.google-analytics.com",
|
|
217
|
+
"https://*.analytics.google.com",
|
|
218
|
+
"https://*.googletagmanager.com",
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
export const GOOGLE_ANALYTICS_WITH_SIGNALS_CSP = {
|
|
222
|
+
...BASELINE_STRICT_CSP,
|
|
223
|
+
...GOOGLE_ANALYTICS_CSP,
|
|
224
|
+
[Directive.IMG_SRC]: [
|
|
225
|
+
"'self'",
|
|
226
|
+
"https://*.google-analytics.com",
|
|
227
|
+
"https://*.googletagmanager.com",
|
|
228
|
+
"https://*.g.doubleclick.net",
|
|
229
|
+
"https://*.google.com",
|
|
230
|
+
{ "https://*.google.": googleSupportedTLDs },
|
|
231
|
+
],
|
|
232
|
+
[Directive.CONNECT_SRC]: [
|
|
233
|
+
"'self'",
|
|
234
|
+
"https://*.google-analytics.com",
|
|
235
|
+
"https://*.googletagmanager.com",
|
|
236
|
+
"https://*.g.doubleclick.net",
|
|
237
|
+
"https://pagead2.googlesyndication.com",
|
|
238
|
+
{ "https://*.google": googleSupportedTLDs },
|
|
239
|
+
],
|
|
240
|
+
[Directive.FRAME_SRC]: [
|
|
241
|
+
"'self'",
|
|
242
|
+
"https://td.doubleclick.net",
|
|
243
|
+
"https://www.googletagmanager.com",
|
|
244
|
+
],
|
|
245
|
+
};
|
package/dist/helpers.js
CHANGED
|
@@ -45,7 +45,12 @@ export function warnOnCspIssues(csp, overrides = {}) {
|
|
|
45
45
|
const options = { ...DEFAULT_WARNINGS, ...overrides };
|
|
46
46
|
// 1. Overly permissive: * in script-src, style-src, etc.
|
|
47
47
|
if (options.overlyPermissive) {
|
|
48
|
-
[
|
|
48
|
+
[
|
|
49
|
+
Directive.SCRIPT_SRC,
|
|
50
|
+
Directive.STYLE_SRC,
|
|
51
|
+
Directive.IMG_SRC,
|
|
52
|
+
Directive.CONNECT_SRC,
|
|
53
|
+
].forEach((directive) => {
|
|
49
54
|
const rules = csp[directive];
|
|
50
55
|
if (Array.isArray(rules) && rules.includes("*")) {
|
|
51
56
|
console.warn(`[CSPrefabricate] Overly permissive: '*' found in ${directive}`);
|
|
@@ -54,7 +59,11 @@ export function warnOnCspIssues(csp, overrides = {}) {
|
|
|
54
59
|
}
|
|
55
60
|
// 2. Missing important directives
|
|
56
61
|
if (options.missingDirectives) {
|
|
57
|
-
[
|
|
62
|
+
[
|
|
63
|
+
Directive.OBJECT_SRC,
|
|
64
|
+
Directive.BASE_URI,
|
|
65
|
+
Directive.FORM_ACTION,
|
|
66
|
+
].forEach((directive) => {
|
|
58
67
|
if (!(directive in csp)) {
|
|
59
68
|
console.warn(`[CSPrefabricate] Missing recommended directive: ${directive}`);
|
|
60
69
|
}
|
|
@@ -62,7 +71,7 @@ export function warnOnCspIssues(csp, overrides = {}) {
|
|
|
62
71
|
}
|
|
63
72
|
// 3. Unsafe inline
|
|
64
73
|
if (options.unsafeInline) {
|
|
65
|
-
[Directive.SCRIPT_SRC, Directive.STYLE_SRC].forEach(directive => {
|
|
74
|
+
[Directive.SCRIPT_SRC, Directive.STYLE_SRC].forEach((directive) => {
|
|
66
75
|
const rules = csp[directive];
|
|
67
76
|
if (Array.isArray(rules) && rules.includes("'unsafe-inline'")) {
|
|
68
77
|
console.warn(`[CSPrefabricate] 'unsafe-inline' found in ${directive}`);
|
|
@@ -73,7 +82,8 @@ export function warnOnCspIssues(csp, overrides = {}) {
|
|
|
73
82
|
if (options.missingNonceOrHash) {
|
|
74
83
|
const rules = csp[Directive.SCRIPT_SRC];
|
|
75
84
|
if (Array.isArray(rules) && rules.includes("'unsafe-inline'")) {
|
|
76
|
-
const hasNonceOrHash = rules.some((r) => typeof r === "string" &&
|
|
85
|
+
const hasNonceOrHash = rules.some((r) => typeof r === "string" &&
|
|
86
|
+
(r.startsWith("'nonce-") || r.startsWith("'sha")));
|
|
77
87
|
if (!hasNonceOrHash) {
|
|
78
88
|
console.warn(`[CSPrefabricate] 'unsafe-inline' in script-src without nonce or hash`);
|
|
79
89
|
}
|
|
@@ -81,7 +91,7 @@ export function warnOnCspIssues(csp, overrides = {}) {
|
|
|
81
91
|
}
|
|
82
92
|
// 5. Permitting data: in img-src or media-src
|
|
83
93
|
if (options.dataUri) {
|
|
84
|
-
[Directive.IMG_SRC, Directive.MEDIA_SRC].forEach(directive => {
|
|
94
|
+
[Directive.IMG_SRC, Directive.MEDIA_SRC].forEach((directive) => {
|
|
85
95
|
const rules = csp[directive];
|
|
86
96
|
if (Array.isArray(rules) && rules.includes("data:")) {
|
|
87
97
|
console.warn(`[CSPrefabricate] 'data:' allowed in ${directive}`);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Directive } from "./types";
|
|
2
2
|
import { create } from "./utils";
|
|
3
|
-
|
|
3
|
+
import * as Baseline from "./baseline";
|
|
4
|
+
export { Baseline };
|
|
5
|
+
export { create, Directive };
|
|
6
|
+
export type { ContentSecurityPolicy } from "./types";
|
package/dist/index.js
CHANGED
package/dist/utils.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatRule, isValidDirective, warnOnCspIssues } from "./helpers";
|
|
1
|
+
import { formatRule, isValidDirective, warnOnCspIssues, } from "./helpers";
|
|
2
2
|
export const processRules = (rules) => {
|
|
3
3
|
// Flatten and deduplicate rules
|
|
4
4
|
const seen = new Set();
|
|
@@ -36,7 +36,8 @@ export const create = (obj, warningOptions) => {
|
|
|
36
36
|
.map(([directive, rules]) => {
|
|
37
37
|
if (Array.isArray(rules)) {
|
|
38
38
|
// Filter out non-string/object values at runtime
|
|
39
|
-
const filtered = rules.filter((r) => typeof r === "string" ||
|
|
39
|
+
const filtered = rules.filter((r) => typeof r === "string" ||
|
|
40
|
+
(typeof r === "object" && r !== null));
|
|
40
41
|
const processed = processRules(filtered);
|
|
41
42
|
return processed ? `${directive} ${processed}` : `${directive}`;
|
|
42
43
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "csprefabricate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Generate valid and secure Content Security Policies (CSP) with TypeScript.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"csp",
|
|
7
|
+
"content-security-policy",
|
|
8
|
+
"security",
|
|
9
|
+
"web-security",
|
|
10
|
+
"xss-protection",
|
|
11
|
+
"typescript"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/jamestoohey/csprefabricate#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/jamestoohey/csprefabricate/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/jamestoohey/csprefabricate.git"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": {
|
|
23
|
+
"name": "James Toohey",
|
|
24
|
+
"url": "https://github.com/jamestoohey"
|
|
25
|
+
},
|
|
4
26
|
"packageManager": "yarn@4.5.3",
|
|
5
27
|
"type": "module",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"main": "dist/index.js",
|
|
35
|
+
"types": "dist/index.d.ts",
|
|
36
|
+
"files": [
|
|
37
|
+
"dist/",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"CHANGELOG.md"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc --project tsconfig.build.json",
|
|
44
|
+
"functional-test": "yarn build && tsx --test src/test/functional/functional.test.js",
|
|
45
|
+
"lint": "eslint .",
|
|
46
|
+
"pack": "npm pack",
|
|
47
|
+
"prepack": "yarn typecheck && yarn test && yarn build",
|
|
48
|
+
"prepublish": "yarn version check",
|
|
49
|
+
"test": "tsx --test src/test/**/*test.ts",
|
|
50
|
+
"test:watch": "tsx --test --watch src/test/**/*test.ts",
|
|
51
|
+
"typecheck": "tsc --noEmit"
|
|
52
|
+
},
|
|
6
53
|
"devDependencies": {
|
|
7
54
|
"@tsconfig/node-lts": "^22.0.1",
|
|
8
55
|
"@types/node": "^22.13.5",
|
|
@@ -14,18 +61,7 @@
|
|
|
14
61
|
"tsx": "^4.19.3",
|
|
15
62
|
"typescript": "^5.7.3"
|
|
16
63
|
},
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
"dist/"
|
|
20
|
-
],
|
|
21
|
-
"scripts": {
|
|
22
|
-
"build": "tsc --project tsconfig.build.json",
|
|
23
|
-
"pack": "npm pack",
|
|
24
|
-
"prepack": "yarn typecheck && yarn test && yarn build",
|
|
25
|
-
"prettier": "prettier . --write",
|
|
26
|
-
"prepublish": "yarn version check",
|
|
27
|
-
"test": "tsx --test src/test/**/*test.ts",
|
|
28
|
-
"typecheck": "tsc --noEmit",
|
|
29
|
-
"lint": "eslint ."
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=18"
|
|
30
66
|
}
|
|
31
67
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { describe, it, beforeEach, after } from "node:test";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
import { formatRule, isValidDirective, warnOnCspIssues } from "../helpers";
|
|
4
|
-
import { Directive } from "../types";
|
|
5
|
-
void describe("Helpers tests", () => {
|
|
6
|
-
void describe("isValidDirective", () => {
|
|
7
|
-
void it("Returns true if directive is valid", () => {
|
|
8
|
-
assert.strictEqual(isValidDirective(Directive.BASE_URI), true);
|
|
9
|
-
assert.strictEqual(isValidDirective("default-src"), true);
|
|
10
|
-
});
|
|
11
|
-
void it("Returns false if directive is invalid", () => {
|
|
12
|
-
assert.strictEqual(isValidDirective("some-src"), false);
|
|
13
|
-
});
|
|
14
|
-
});
|
|
15
|
-
void describe("formatRule", () => {
|
|
16
|
-
void it("Formats special rules with single quotes", () => {
|
|
17
|
-
assert.strictEqual(formatRule("self"), `'self'`);
|
|
18
|
-
});
|
|
19
|
-
void it("Returns non-special rules", () => {
|
|
20
|
-
assert.strictEqual(formatRule("google.com"), `google.com`);
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
void describe("warnOnCspIssues", () => {
|
|
24
|
-
let warnings = [];
|
|
25
|
-
const originalWarn = console.warn;
|
|
26
|
-
void beforeEach(() => {
|
|
27
|
-
warnings = [];
|
|
28
|
-
console.warn = (msg) => { warnings.push(msg); };
|
|
29
|
-
});
|
|
30
|
-
void after(() => {
|
|
31
|
-
console.warn = originalWarn;
|
|
32
|
-
});
|
|
33
|
-
void it("Warns on overly permissive *", () => {
|
|
34
|
-
const csp = {
|
|
35
|
-
[Directive.SCRIPT_SRC]: ["*"]
|
|
36
|
-
};
|
|
37
|
-
warnOnCspIssues(csp);
|
|
38
|
-
assert(warnings.some(w => w.includes("Overly permissive")));
|
|
39
|
-
});
|
|
40
|
-
void it("Warns on missing important directives", () => {
|
|
41
|
-
const csp = {
|
|
42
|
-
[Directive.DEFAULT_SRC]: ["'self'"]
|
|
43
|
-
};
|
|
44
|
-
warnOnCspIssues(csp);
|
|
45
|
-
assert(warnings.some(w => w.includes("Missing recommended directive: object-src")));
|
|
46
|
-
assert(warnings.some(w => w.includes("Missing recommended directive: base-uri")));
|
|
47
|
-
assert(warnings.some(w => w.includes("Missing recommended directive: form-action")));
|
|
48
|
-
});
|
|
49
|
-
void it("Warns on 'unsafe-inline' in script-src", () => {
|
|
50
|
-
const csp = {
|
|
51
|
-
[Directive.SCRIPT_SRC]: ["'unsafe-inline'"]
|
|
52
|
-
};
|
|
53
|
-
warnOnCspIssues(csp);
|
|
54
|
-
assert(warnings.some(w => w.includes("'unsafe-inline' found in script-src")));
|
|
55
|
-
});
|
|
56
|
-
void it("Warns on 'unsafe-inline' in script-src without nonce or hash", () => {
|
|
57
|
-
const csp = {
|
|
58
|
-
[Directive.SCRIPT_SRC]: ["'unsafe-inline'"]
|
|
59
|
-
};
|
|
60
|
-
warnOnCspIssues(csp);
|
|
61
|
-
assert(warnings.some(w => w.includes("'unsafe-inline' in script-src without nonce or hash")));
|
|
62
|
-
});
|
|
63
|
-
void it("Does not warn if nonce is present with 'unsafe-inline'", () => {
|
|
64
|
-
const csp = {
|
|
65
|
-
[Directive.SCRIPT_SRC]: ["'unsafe-inline'", "'nonce-abc'"]
|
|
66
|
-
};
|
|
67
|
-
warnOnCspIssues(csp);
|
|
68
|
-
assert(!warnings.some(w => w.includes("without nonce or hash")));
|
|
69
|
-
});
|
|
70
|
-
void it("Does not warn if hash is present with 'unsafe-inline'", () => {
|
|
71
|
-
const csp = {
|
|
72
|
-
[Directive.SCRIPT_SRC]: ["'unsafe-inline'", "'sha256-xyz'"]
|
|
73
|
-
};
|
|
74
|
-
warnOnCspIssues(csp);
|
|
75
|
-
assert(!warnings.some(w => w.includes("without nonce or hash")));
|
|
76
|
-
});
|
|
77
|
-
void it("Warns on data: in img-src", () => {
|
|
78
|
-
const csp = {
|
|
79
|
-
[Directive.IMG_SRC]: ["data:"]
|
|
80
|
-
};
|
|
81
|
-
warnOnCspIssues(csp);
|
|
82
|
-
assert(warnings.some(w => w.includes("'data:' allowed in img-src")));
|
|
83
|
-
});
|
|
84
|
-
void it("Respects warning options to opt out of specific warnings", () => {
|
|
85
|
-
const csp = {
|
|
86
|
-
[Directive.SCRIPT_SRC]: ["*"]
|
|
87
|
-
};
|
|
88
|
-
const opts = { overlyPermissive: false };
|
|
89
|
-
warnOnCspIssues(csp, opts);
|
|
90
|
-
assert(!warnings.some(w => w.includes("Overly permissive")));
|
|
91
|
-
});
|
|
92
|
-
void it("Does not warn if all warnings are disabled", () => {
|
|
93
|
-
const csp = {
|
|
94
|
-
[Directive.SCRIPT_SRC]: ["*"],
|
|
95
|
-
[Directive.IMG_SRC]: ["data:"]
|
|
96
|
-
};
|
|
97
|
-
const opts = {
|
|
98
|
-
overlyPermissive: false,
|
|
99
|
-
missingDirectives: false,
|
|
100
|
-
unsafeInline: false,
|
|
101
|
-
missingNonceOrHash: false,
|
|
102
|
-
dataUri: false
|
|
103
|
-
};
|
|
104
|
-
warnOnCspIssues(csp, opts);
|
|
105
|
-
assert.strictEqual(warnings.length, 0);
|
|
106
|
-
});
|
|
107
|
-
void it("Warns only for enabled warnings", () => {
|
|
108
|
-
const csp = {
|
|
109
|
-
[Directive.SCRIPT_SRC]: ["*", "'unsafe-inline'"],
|
|
110
|
-
[Directive.IMG_SRC]: ["data:"]
|
|
111
|
-
};
|
|
112
|
-
const opts = {
|
|
113
|
-
overlyPermissive: false,
|
|
114
|
-
missingDirectives: false,
|
|
115
|
-
unsafeInline: true
|
|
116
|
-
};
|
|
117
|
-
warnOnCspIssues(csp, opts);
|
|
118
|
-
assert(warnings.some(w => w.includes("'unsafe-inline' found in script-src")));
|
|
119
|
-
assert(!warnings.some(w => w.includes("Overly permissive")));
|
|
120
|
-
assert(!warnings.some(w => w.includes("Missing recommended directive")));
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/test/utils.test.js
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { describe, it, afterEach, mock, beforeEach } from "node:test";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
import { create, processRules } from "../utils";
|
|
4
|
-
import { Directive } from "../types";
|
|
5
|
-
void describe("Utils tests", () => {
|
|
6
|
-
let mockWarn;
|
|
7
|
-
const originalWarn = console.warn;
|
|
8
|
-
void beforeEach(() => {
|
|
9
|
-
mockWarn = mock.method(console, "warn", () => { });
|
|
10
|
-
});
|
|
11
|
-
void afterEach(() => {
|
|
12
|
-
console.warn = originalWarn;
|
|
13
|
-
});
|
|
14
|
-
void describe("processRules", () => {
|
|
15
|
-
void it("Processes rules provided as an array of strings (simple)", () => {
|
|
16
|
-
const rules = ["self", "*.google.com", "*.google.com.au"];
|
|
17
|
-
assert.strictEqual(processRules(rules), `'self' *.google.com *.google.com.au`);
|
|
18
|
-
});
|
|
19
|
-
void it("Processes rules provided a complex list of tlds", () => {
|
|
20
|
-
const rules = ["self", { "*.google": [".com", ".com.au"] }];
|
|
21
|
-
assert.strictEqual(processRules(rules), `'self' *.google.com *.google.com.au`);
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
void describe("create", () => {
|
|
25
|
-
void it("Formats a CSP string with all rules", () => {
|
|
26
|
-
const csp = {
|
|
27
|
-
[Directive.DEFAULT_SRC]: ["self"],
|
|
28
|
-
[Directive.SCRIPT_SRC]: ["self", "js.example.com"],
|
|
29
|
-
[Directive.STYLE_SRC]: ["self", "css.example.com"],
|
|
30
|
-
[Directive.IMG_SRC]: [
|
|
31
|
-
"self",
|
|
32
|
-
{ "*.google": [".com", ".com.au"] },
|
|
33
|
-
],
|
|
34
|
-
[Directive.CONNECT_SRC]: ["self"],
|
|
35
|
-
[Directive.FONT_SRC]: ["self", "font.example.com"],
|
|
36
|
-
[Directive.OBJECT_SRC]: ["none"],
|
|
37
|
-
[Directive.MEDIA_SRC]: ["self", "media.example.com"],
|
|
38
|
-
[Directive.FRAME_SRC]: ["self"],
|
|
39
|
-
[Directive.SANDBOX]: ["allow-scripts"],
|
|
40
|
-
[Directive.REPORT_URI]: ["/my-report-uri"],
|
|
41
|
-
[Directive.CHILD_SRC]: ["self"],
|
|
42
|
-
[Directive.FORM_ACTION]: ["self"],
|
|
43
|
-
[Directive.FRAME_ANCESTORS]: ["none"],
|
|
44
|
-
[Directive.PLUGIN_TYPES]: ["application/pdf"],
|
|
45
|
-
[Directive.BASE_URI]: ["self"],
|
|
46
|
-
[Directive.REPORT_TO]: ["myGroupName"],
|
|
47
|
-
[Directive.WORKER_SRC]: ["none"],
|
|
48
|
-
[Directive.MANIFEST_SRC]: ["none"],
|
|
49
|
-
[Directive.PREFETCH_SRC]: ["none"],
|
|
50
|
-
[Directive.NAVIGATE_TO]: ["example.com"],
|
|
51
|
-
[Directive.REQUIRE_TRUSTED_TYPES_FOR]: ["script"],
|
|
52
|
-
[Directive.TRUSTED_TYPES]: ["none"],
|
|
53
|
-
[Directive.UPGRADE_INSECURE_REQUESTS]: null,
|
|
54
|
-
[Directive.BLOCK_ALL_MIXED_CONTENT]: null,
|
|
55
|
-
};
|
|
56
|
-
const cspString = create(csp);
|
|
57
|
-
assert.strictEqual(cspString, "default-src 'self'; script-src 'self' js.example.com; style-src 'self' css.example.com; img-src 'self' *.google.com *.google.com.au; connect-src 'self'; font-src 'self' font.example.com; object-src 'none'; media-src 'self' media.example.com; frame-src 'self'; sandbox allow-scripts; report-uri /my-report-uri; child-src 'self'; form-action 'self'; frame-ancestors 'none'; plugin-types application/pdf; base-uri 'self'; report-to myGroupName; worker-src 'none'; manifest-src 'none'; prefetch-src 'none'; navigate-to example.com; require-trusted-types-for script; trusted-types 'none'; upgrade-insecure-requests; block-all-mixed-content;");
|
|
58
|
-
});
|
|
59
|
-
void it("Ignores invalid directives", () => {
|
|
60
|
-
const csp = {
|
|
61
|
-
[Directive.DEFAULT_SRC]: ["self"],
|
|
62
|
-
// @ts-expect-error deliberate testing of invalid directive
|
|
63
|
-
["invalid-directive"]: ["self"],
|
|
64
|
-
[Directive.IMG_SRC]: ["my.domain.com"]
|
|
65
|
-
};
|
|
66
|
-
const cspString = create(csp);
|
|
67
|
-
assert.strictEqual(cspString, "default-src 'self'; img-src my.domain.com;");
|
|
68
|
-
});
|
|
69
|
-
void it("Calls warning helper when invoked", () => {
|
|
70
|
-
const csp = {
|
|
71
|
-
[Directive.DEFAULT_SRC]: ["self"],
|
|
72
|
-
};
|
|
73
|
-
create(csp);
|
|
74
|
-
assert.equal(mockWarn.mock.calls.length, 3);
|
|
75
|
-
const args = mockWarn.mock.calls.map((call) => call.arguments);
|
|
76
|
-
assert.equal(args[0][0], "[CSPrefabricate] Missing recommended directive: object-src");
|
|
77
|
-
assert.equal(args[1][0], "[CSPrefabricate] Missing recommended directive: base-uri");
|
|
78
|
-
assert.equal(args[2][0], "[CSPrefabricate] Missing recommended directive: form-action");
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
void describe("Edge cases", () => {
|
|
82
|
-
void it("Handles empty rules array", () => {
|
|
83
|
-
const csp = {
|
|
84
|
-
[Directive.DEFAULT_SRC]: [],
|
|
85
|
-
};
|
|
86
|
-
const cspString = create(csp);
|
|
87
|
-
assert.strictEqual(cspString, "default-src;");
|
|
88
|
-
});
|
|
89
|
-
void it("Handles completely empty policy object", () => {
|
|
90
|
-
const csp = {};
|
|
91
|
-
const cspString = create(csp);
|
|
92
|
-
assert.strictEqual(cspString, "");
|
|
93
|
-
});
|
|
94
|
-
void it("Handles duplicate rules in an array", () => {
|
|
95
|
-
const csp = {
|
|
96
|
-
[Directive.DEFAULT_SRC]: ["self", "self", "example.com", "example.com"],
|
|
97
|
-
};
|
|
98
|
-
const cspString = create(csp);
|
|
99
|
-
assert.strictEqual(cspString, "default-src 'self' example.com;");
|
|
100
|
-
});
|
|
101
|
-
void it("Ignores non-string, non-object values in rules array at runtime", () => {
|
|
102
|
-
const csp = {
|
|
103
|
-
[Directive.DEFAULT_SRC]: ["self", 123, false, null, undefined],
|
|
104
|
-
};
|
|
105
|
-
assert.doesNotThrow(() => create(csp));
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
});
|