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 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 input = {
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.IMG_SRC]: ["self", {"*.google": [".com", ".com.au"]}],
24
- } satisfies ContentSecurityPolicy;
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 output = create(csp);
27
- // > "default-src 'self'; img-src 'self' *.google.com *.google.com.au;",
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
  });
@@ -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
- describe("processRules", () => {
7
- it("Processes rules provided as an array of strings (simple)", () => {
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("Handles blank directives", () => {
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 { ContentSecurityPolicy } from "./types";
2
- export declare const processRules: (rules: Array<string> | Array<string | Record<string, Array<string>>>) => string;
3
- export declare const create: (obj: ContentSecurityPolicy) => string;
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
- return rules
4
- .map((rule) => {
3
+ // Flatten and deduplicate rules
4
+ const seen = new Set();
5
+ for (const rule of rules) {
5
6
  if (typeof rule === "object") {
6
- return Object.entries(rule).map(([domain, tlds]) => tlds.map((tld) => `${domain}${tld}`).join(" "));
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
- return formatRule(rule);
14
+ seen.add(formatRule(rule));
10
15
  }
11
- })
12
- .join(" ");
16
+ }
17
+ return Array.from(seen).join(" ");
13
18
  };
14
- export const create = (obj) => {
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]) => `${directive}${rules && rules.length > 0 ? " " + processRules(rules) : ""}`);
25
- return `${cspString.join("; ")};`;
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.2.3",
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 ."