csprefabricate 0.2.3 → 0.3.1
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/README.md +93 -7
- package/dist/helpers.d.ts +9 -0
- package/dist/helpers.js +56 -0
- package/dist/test/helpers.test.js +109 -9
- package/dist/test/utils.test.js +54 -15
- package/dist/types.d.ts +1 -1
- package/dist/utils.d.ts +10 -3
- package/dist/utils.js +31 -11
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -14,20 +14,106 @@ Currently `csprefabricate`:
|
|
|
14
14
|
|
|
15
15
|
- Validates directive names
|
|
16
16
|
- Supports providing a list of TLDs for a given domain name
|
|
17
|
+
- Provides warnings for insecure or incomplete CSP configurations, with options to disable specific warnings
|
|
18
|
+
|
|
19
|
+
## Common CSP Issues
|
|
20
|
+
|
|
21
|
+
By default, `csprefabricate` will warn you about common CSP issues, such as:
|
|
22
|
+
|
|
23
|
+
- Overly permissive sources (e.g. using `*`)
|
|
24
|
+
- Missing recommended directives (i.e. `object-src`, `base-uri`, `form-action`)
|
|
25
|
+
- Use of `'unsafe-inline'` in `script-src`, even if nonces or hashes are present
|
|
26
|
+
- Missing nonces or hashes when using `'unsafe-inline'` in `script-src`
|
|
27
|
+
- Allowing `data:` in `img-src` or `media-src`
|
|
28
|
+
|
|
29
|
+
You can control which warnings are shown by passing an optional `WarningOptions` object to the `create` function:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import {
|
|
33
|
+
create,
|
|
34
|
+
Directive,
|
|
35
|
+
ContentSecurityPolicy,
|
|
36
|
+
WarningOptions,
|
|
37
|
+
} from "csprefabricate";
|
|
38
|
+
|
|
39
|
+
const csp: ContentSecurityPolicy = {
|
|
40
|
+
[Directive.SCRIPT_SRC]: ["*"],
|
|
41
|
+
[Directive.IMG_SRC]: ["data:"],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Disable all warnings
|
|
45
|
+
const warningOptions: WarningOptions = {
|
|
46
|
+
overlyPermissive: false,
|
|
47
|
+
missingDirectives: false,
|
|
48
|
+
unsafeInline: false,
|
|
49
|
+
missingNonceOrHash: false,
|
|
50
|
+
dataUri: false,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
create(csp, warningOptions);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
You can selectively enable or disable specific warnings as needed.
|
|
57
|
+
|
|
58
|
+
## Real World Examples
|
|
59
|
+
|
|
60
|
+
### Example 1: Basic Strict Policy
|
|
17
61
|
|
|
18
62
|
```typescript
|
|
19
|
-
import {create} from "csprefabricate";
|
|
63
|
+
import {create, Directive, ContentSecurityPolicy} from "csprefabricate";
|
|
20
64
|
|
|
21
|
-
const
|
|
65
|
+
const csp: ContentSecurityPolicy = {
|
|
66
|
+
[Directive.DEFAULT_SRC]: ["'self'"],
|
|
67
|
+
[Directive.SCRIPT_SRC]: ["'self'"],
|
|
68
|
+
[Directive.STYLE_SRC]: ["'self'"],
|
|
69
|
+
[Directive.IMG_SRC]: ["'self'"],
|
|
70
|
+
[Directive.OBJECT_SRC]: ["'none'"],
|
|
71
|
+
[Directive.BASE_URI]: ["'self'"],
|
|
72
|
+
[Directive.FORM_ACTION]: ["'self'"],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const cspString = create(csp);
|
|
76
|
+
// "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self';"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Example 2: Allowing Google Analytics
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import {create, Directive, ContentSecurityPolicy} from "csprefabricate";
|
|
83
|
+
|
|
84
|
+
const csp: ContentSecurityPolicy = {
|
|
22
85
|
[Directive.DEFAULT_SRC]: ["self"],
|
|
23
|
-
[Directive.
|
|
24
|
-
|
|
86
|
+
[Directive.SCRIPT_SRC]: ["self", "*.googletagmanager.com"],
|
|
87
|
+
[Directive.IMG_SRC]: [
|
|
88
|
+
"self",
|
|
89
|
+
"*.google-analytics.com",
|
|
90
|
+
"https://*.googletagmanager.com",
|
|
91
|
+
],
|
|
92
|
+
[Directive.CONNECT_SRC]: [
|
|
93
|
+
"self",
|
|
94
|
+
"https://*.google-analytics.com",
|
|
95
|
+
"https://*.analytics.google.com",
|
|
96
|
+
"https://*.googletagmanager.com",
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const cspString = create(csp);
|
|
101
|
+
// "default-src 'self'; script-src 'self' *.googletagmanager.com; img-src 'self' *.google-analytics.com https://*.googletagmanager.com; connect-src 'self' https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com;"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Example 3: Using TLD Expansion for Multiple Domains
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import {create, Directive, ContentSecurityPolicy} from "csprefabricate";
|
|
108
|
+
|
|
109
|
+
const csp: ContentSecurityPolicy = {
|
|
110
|
+
[Directive.IMG_SRC]: ["self", {"*.example": [".com", ".co.uk", ".net"]}],
|
|
111
|
+
};
|
|
25
112
|
|
|
26
|
-
const
|
|
27
|
-
//
|
|
113
|
+
const cspString = create(csp);
|
|
114
|
+
// "img-src 'self' *.example.com *.example.co.uk *.example.net;"
|
|
28
115
|
```
|
|
29
116
|
|
|
30
117
|
## Future
|
|
31
118
|
|
|
32
119
|
- Generate baseline recommended CSPs (for example, Google Analytics)
|
|
33
|
-
- Warnings for insecure configurations
|
package/dist/helpers.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
|
+
import { ContentSecurityPolicy } from "./types";
|
|
2
|
+
export interface WarningOptions {
|
|
3
|
+
overlyPermissive?: boolean;
|
|
4
|
+
missingDirectives?: boolean;
|
|
5
|
+
unsafeInline?: boolean;
|
|
6
|
+
missingNonceOrHash?: boolean;
|
|
7
|
+
dataUri?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function warnOnCspIssues(csp: ContentSecurityPolicy, overrides?: WarningOptions): void;
|
|
1
10
|
export declare const isValidDirective: (directive: string) => boolean;
|
|
2
11
|
export declare const formatRule: (rule: string) => string;
|
package/dist/helpers.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import { Directive } from "./types";
|
|
2
|
+
const DEFAULT_WARNINGS = {
|
|
3
|
+
overlyPermissive: true,
|
|
4
|
+
missingDirectives: true,
|
|
5
|
+
unsafeInline: true,
|
|
6
|
+
missingNonceOrHash: true,
|
|
7
|
+
dataUri: true,
|
|
8
|
+
};
|
|
1
9
|
const validDirectives = [
|
|
2
10
|
"default-src",
|
|
3
11
|
"script-src",
|
|
@@ -33,5 +41,53 @@ const specialRules = [
|
|
|
33
41
|
"strict-dynamic",
|
|
34
42
|
"unsafe-hashes",
|
|
35
43
|
];
|
|
44
|
+
export function warnOnCspIssues(csp, overrides = {}) {
|
|
45
|
+
const options = { ...DEFAULT_WARNINGS, ...overrides };
|
|
46
|
+
// 1. Overly permissive: * in script-src, style-src, etc.
|
|
47
|
+
if (options.overlyPermissive) {
|
|
48
|
+
[Directive.SCRIPT_SRC, Directive.STYLE_SRC, Directive.IMG_SRC, Directive.CONNECT_SRC].forEach(directive => {
|
|
49
|
+
const rules = csp[directive];
|
|
50
|
+
if (Array.isArray(rules) && rules.includes("*")) {
|
|
51
|
+
console.warn(`[CSPrefabricate] Overly permissive: '*' found in ${directive}`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// 2. Missing important directives
|
|
56
|
+
if (options.missingDirectives) {
|
|
57
|
+
[Directive.OBJECT_SRC, Directive.BASE_URI, Directive.FORM_ACTION].forEach(directive => {
|
|
58
|
+
if (!(directive in csp)) {
|
|
59
|
+
console.warn(`[CSPrefabricate] Missing recommended directive: ${directive}`);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// 3. Unsafe inline
|
|
64
|
+
if (options.unsafeInline) {
|
|
65
|
+
[Directive.SCRIPT_SRC, Directive.STYLE_SRC].forEach(directive => {
|
|
66
|
+
const rules = csp[directive];
|
|
67
|
+
if (Array.isArray(rules) && rules.includes("'unsafe-inline'")) {
|
|
68
|
+
console.warn(`[CSPrefabricate] 'unsafe-inline' found in ${directive}`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// 4. Missing nonce or hash in script-src if 'unsafe-inline' is present
|
|
73
|
+
if (options.missingNonceOrHash) {
|
|
74
|
+
const rules = csp[Directive.SCRIPT_SRC];
|
|
75
|
+
if (Array.isArray(rules) && rules.includes("'unsafe-inline'")) {
|
|
76
|
+
const hasNonceOrHash = rules.some((r) => typeof r === "string" && (r.startsWith("'nonce-") || r.startsWith("'sha")));
|
|
77
|
+
if (!hasNonceOrHash) {
|
|
78
|
+
console.warn(`[CSPrefabricate] 'unsafe-inline' in script-src without nonce or hash`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// 5. Permitting data: in img-src or media-src
|
|
83
|
+
if (options.dataUri) {
|
|
84
|
+
[Directive.IMG_SRC, Directive.MEDIA_SRC].forEach(directive => {
|
|
85
|
+
const rules = csp[directive];
|
|
86
|
+
if (Array.isArray(rules) && rules.includes("data:")) {
|
|
87
|
+
console.warn(`[CSPrefabricate] 'data:' allowed in ${directive}`);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
36
92
|
export const isValidDirective = (directive) => validDirectives.includes(directive);
|
|
37
93
|
export const formatRule = (rule) => specialRules.includes(rule) ? `'${rule}'` : rule;
|
|
@@ -1,23 +1,123 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
1
|
+
import { describe, it, beforeEach, after } from "node:test";
|
|
2
2
|
import assert from "node:assert";
|
|
3
|
-
import { formatRule, isValidDirective } from "../helpers";
|
|
3
|
+
import { formatRule, isValidDirective, warnOnCspIssues } from "../helpers";
|
|
4
4
|
import { Directive } from "../types";
|
|
5
|
-
describe("Helpers tests", () => {
|
|
6
|
-
describe("isValidDirective", () => {
|
|
7
|
-
it("Returns true if directive is valid", () => {
|
|
5
|
+
void describe("Helpers tests", () => {
|
|
6
|
+
void describe("isValidDirective", () => {
|
|
7
|
+
void it("Returns true if directive is valid", () => {
|
|
8
8
|
assert.strictEqual(isValidDirective(Directive.BASE_URI), true);
|
|
9
9
|
assert.strictEqual(isValidDirective("default-src"), true);
|
|
10
10
|
});
|
|
11
|
-
it("Returns false if directive is invalid", () => {
|
|
11
|
+
void it("Returns false if directive is invalid", () => {
|
|
12
12
|
assert.strictEqual(isValidDirective("some-src"), false);
|
|
13
13
|
});
|
|
14
14
|
});
|
|
15
|
-
describe("formatRule", () => {
|
|
16
|
-
it("Formats special rules with single quotes", () => {
|
|
15
|
+
void describe("formatRule", () => {
|
|
16
|
+
void it("Formats special rules with single quotes", () => {
|
|
17
17
|
assert.strictEqual(formatRule("self"), `'self'`);
|
|
18
18
|
});
|
|
19
|
-
it("Returns non-special rules", () => {
|
|
19
|
+
void it("Returns non-special rules", () => {
|
|
20
20
|
assert.strictEqual(formatRule("google.com"), `google.com`);
|
|
21
21
|
});
|
|
22
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
|
+
});
|
|
23
123
|
});
|
package/dist/test/utils.test.js
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
1
|
+
import { describe, it, afterEach, mock, beforeEach } from "node:test";
|
|
2
2
|
import assert from "node:assert";
|
|
3
3
|
import { create, processRules } from "../utils";
|
|
4
4
|
import { Directive } from "../types";
|
|
5
|
-
describe("Utils tests", () => {
|
|
6
|
-
|
|
7
|
-
|
|
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)", () => {
|
|
8
16
|
const rules = ["self", "*.google.com", "*.google.com.au"];
|
|
9
17
|
assert.strictEqual(processRules(rules), `'self' *.google.com *.google.com.au`);
|
|
10
18
|
});
|
|
11
|
-
it("Processes rules provided a complex list of tlds", () => {
|
|
19
|
+
void it("Processes rules provided a complex list of tlds", () => {
|
|
12
20
|
const rules = ["self", { "*.google": [".com", ".com.au"] }];
|
|
13
21
|
assert.strictEqual(processRules(rules), `'self' *.google.com *.google.com.au`);
|
|
14
22
|
});
|
|
15
23
|
});
|
|
16
|
-
describe("create", () => {
|
|
17
|
-
it("Formats a CSP string with all rules", () => {
|
|
24
|
+
void describe("create", () => {
|
|
25
|
+
void it("Formats a CSP string with all rules", () => {
|
|
18
26
|
const csp = {
|
|
19
27
|
[Directive.DEFAULT_SRC]: ["self"],
|
|
20
28
|
[Directive.SCRIPT_SRC]: ["self", "js.example.com"],
|
|
@@ -48,14 +56,7 @@ describe("Utils tests", () => {
|
|
|
48
56
|
const cspString = create(csp);
|
|
49
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;");
|
|
50
58
|
});
|
|
51
|
-
it("
|
|
52
|
-
const csp = {
|
|
53
|
-
[Directive.SANDBOX]: [],
|
|
54
|
-
};
|
|
55
|
-
const cspString = create(csp);
|
|
56
|
-
assert.strictEqual(cspString, "sandbox;");
|
|
57
|
-
});
|
|
58
|
-
it("Ignores invalid directives", () => {
|
|
59
|
+
void it("Ignores invalid directives", () => {
|
|
59
60
|
const csp = {
|
|
60
61
|
[Directive.DEFAULT_SRC]: ["self"],
|
|
61
62
|
// @ts-expect-error deliberate testing of invalid directive
|
|
@@ -65,5 +66,43 @@ describe("Utils tests", () => {
|
|
|
65
66
|
const cspString = create(csp);
|
|
66
67
|
assert.strictEqual(cspString, "default-src 'self'; img-src my.domain.com;");
|
|
67
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
|
+
});
|
|
68
107
|
});
|
|
69
108
|
});
|
package/dist/types.d.ts
CHANGED
|
@@ -55,4 +55,4 @@ interface ContentSecurityPolicy {
|
|
|
55
55
|
[Directive.UPGRADE_INSECURE_REQUESTS]?: BlankDirectiveRule;
|
|
56
56
|
[Directive.BLOCK_ALL_MIXED_CONTENT]?: BlankDirectiveRule;
|
|
57
57
|
}
|
|
58
|
-
export { ContentSecurityPolicy, Rules, Directive };
|
|
58
|
+
export { ContentSecurityPolicy, Rules, Directive, BasicDirectiveRule };
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export declare const
|
|
1
|
+
import { WarningOptions } from "./helpers";
|
|
2
|
+
import { ContentSecurityPolicy, BasicDirectiveRule } from "./types";
|
|
3
|
+
export declare const processRules: (rules: BasicDirectiveRule) => string;
|
|
4
|
+
/**
|
|
5
|
+
* Creates a CSP string from a ContentSecurityPolicy object.
|
|
6
|
+
* Filters out invalid directives and formats the CSP string.
|
|
7
|
+
* @param obj - The ContentSecurityPolicy object.
|
|
8
|
+
* @returns The formatted CSP string.
|
|
9
|
+
*/
|
|
10
|
+
export declare const create: (obj: ContentSecurityPolicy, warningOptions?: WarningOptions) => string;
|
package/dist/utils.js
CHANGED
|
@@ -1,26 +1,46 @@
|
|
|
1
|
-
import { formatRule, isValidDirective } from "./helpers";
|
|
1
|
+
import { formatRule, isValidDirective, warnOnCspIssues } from "./helpers";
|
|
2
2
|
export const processRules = (rules) => {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
// Flatten and deduplicate rules
|
|
4
|
+
const seen = new Set();
|
|
5
|
+
for (const rule of rules) {
|
|
5
6
|
if (typeof rule === "object") {
|
|
6
|
-
|
|
7
|
+
for (const [domain, tlds] of Object.entries(rule)) {
|
|
8
|
+
for (const tld of tlds) {
|
|
9
|
+
seen.add(`${domain}${tld}`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
7
12
|
}
|
|
8
13
|
else {
|
|
9
|
-
|
|
14
|
+
seen.add(formatRule(rule));
|
|
10
15
|
}
|
|
11
|
-
}
|
|
12
|
-
|
|
16
|
+
}
|
|
17
|
+
return Array.from(seen).join(" ");
|
|
13
18
|
};
|
|
14
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Creates a CSP string from a ContentSecurityPolicy object.
|
|
21
|
+
* Filters out invalid directives and formats the CSP string.
|
|
22
|
+
* @param obj - The ContentSecurityPolicy object.
|
|
23
|
+
* @returns The formatted CSP string.
|
|
24
|
+
*/
|
|
25
|
+
export const create = (obj, warningOptions) => {
|
|
26
|
+
warnOnCspIssues(obj, warningOptions);
|
|
15
27
|
const entries = Object.entries(obj);
|
|
16
28
|
const cspString = entries
|
|
17
29
|
.filter(([directive, _rules]) => {
|
|
18
30
|
const isValid = isValidDirective(directive);
|
|
19
31
|
if (!isValid) {
|
|
20
|
-
console.warn(`"${directive}" is not a valid CSP directive and has been ignored.`);
|
|
32
|
+
console.warn(`[CSPrefabricate] "${directive}" is not a valid CSP directive and has been ignored.`);
|
|
21
33
|
}
|
|
22
34
|
return isValid;
|
|
23
35
|
})
|
|
24
|
-
.map(([directive, rules]) =>
|
|
25
|
-
|
|
36
|
+
.map(([directive, rules]) => {
|
|
37
|
+
if (Array.isArray(rules)) {
|
|
38
|
+
// Filter out non-string/object values at runtime
|
|
39
|
+
const filtered = rules.filter((r) => typeof r === "string" || (typeof r === "object" && r !== null));
|
|
40
|
+
const processed = processRules(filtered);
|
|
41
|
+
return processed ? `${directive} ${processed}` : `${directive}`;
|
|
42
|
+
}
|
|
43
|
+
return `${directive}`;
|
|
44
|
+
});
|
|
45
|
+
return cspString.length > 0 ? `${cspString.join("; ")};` : "";
|
|
26
46
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "csprefabricate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"packageManager": "yarn@4.5.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"devDependencies": {
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
"prepack": "yarn typecheck && yarn test && yarn build",
|
|
25
25
|
"prettier": "prettier . --write",
|
|
26
26
|
"prepublish": "yarn version check",
|
|
27
|
-
"publish": "npm publish",
|
|
28
27
|
"test": "tsx --test src/test/**/*test.ts",
|
|
29
28
|
"typecheck": "tsc --noEmit",
|
|
30
29
|
"lint": "eslint ."
|