gila-astro-csp 0.1.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/README.md ADDED
@@ -0,0 +1,394 @@
1
+ # gila-astro-csp
2
+
3
+ Astro 5+ integration for automatic **SRI (Subresource Integrity)** and **CSP (Content-Security-Policy)** generation with nginx support.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/gila-astro-csp.svg)](https://www.npmjs.com/package/gila-astro-csp)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Why gila-astro-csp?
9
+
10
+ Modern web security requires strict CSP headers to prevent XSS attacks. However, frameworks like Astro generate inline scripts that break when using CSP without `unsafe-inline`.
11
+
12
+ **gila-astro-csp solves this by:**
13
+
14
+ 1. Scanning all HTML files after build
15
+ 2. Calculating SHA-256 hashes for inline scripts and styles
16
+ 3. Adding `integrity` attributes to HTML elements
17
+ 4. Generating nginx-ready CSP configuration
18
+
19
+ **Result:** A+ security rating without `unsafe-inline` or `unsafe-eval`.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install gila-astro-csp
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ Add to your `astro.config.mjs`:
30
+
31
+ ```javascript
32
+ import { defineConfig } from 'astro/config';
33
+ import { gilaCSP } from 'gila-astro-csp';
34
+
35
+ export default defineConfig({
36
+ integrations: [
37
+ gilaCSP()
38
+ ]
39
+ });
40
+ ```
41
+
42
+ Run your build:
43
+
44
+ ```bash
45
+ npm run build
46
+ ```
47
+
48
+ That's it! Check `dist/_csp/` for the generated files.
49
+
50
+ ## Configuration
51
+
52
+ ### Full Example
53
+
54
+ ```javascript
55
+ import { gilaCSP } from 'gila-astro-csp';
56
+
57
+ export default defineConfig({
58
+ integrations: [
59
+ gilaCSP({
60
+ // Pre-configured external services
61
+ presets: ['google-analytics', 'cloudflare-insights', 'google-fonts'],
62
+
63
+ // Nginx output configuration
64
+ nginx: {
65
+ outputPath: './dist/_csp/nginx.conf',
66
+ includeComments: true,
67
+ },
68
+
69
+ // JSON output for custom integrations
70
+ json: {
71
+ outputPath: './dist/_csp/hashes.json',
72
+ pretty: true,
73
+ },
74
+
75
+ // Additional CSP directives
76
+ directives: {
77
+ 'img-src': ["'self'", 'data:', 'https:'],
78
+ 'connect-src': ['https://api.example.com'],
79
+ },
80
+ })
81
+ ]
82
+ });
83
+ ```
84
+
85
+ ### Options
86
+
87
+ | Option | Type | Default | Description |
88
+ |--------|------|---------|-------------|
89
+ | `presets` | `string[]` | `[]` | Pre-configured services (see below) |
90
+ | `nginx` | `object \| false` | `{ outputPath: './dist/_csp/nginx.conf' }` | Nginx config output |
91
+ | `json` | `object \| false` | `{ outputPath: './dist/_csp/hashes.json' }` | JSON output |
92
+ | `directives` | `object` | `{}` | Additional CSP directives |
93
+
94
+ ### Available Presets
95
+
96
+ | Preset | Description | Adds |
97
+ |--------|-------------|------|
98
+ | `google-analytics` | Google Analytics & GTM | script-src, connect-src |
99
+ | `cloudflare-insights` | Cloudflare Web Analytics | script-src, connect-src |
100
+ | `google-fonts` | Google Fonts | style-src, font-src |
101
+
102
+ ## Output Files
103
+
104
+ ### nginx.conf
105
+
106
+ Ready-to-include nginx configuration:
107
+
108
+ ```nginx
109
+ # Content-Security-Policy Header
110
+ # Generated by gila-astro-csp
111
+
112
+ add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-abc123...' 'sha256-def456...'; style-src 'self' 'sha256-xyz789...'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; form-action 'self'; base-uri 'self'" always;
113
+ ```
114
+
115
+ ### hashes.json
116
+
117
+ JSON file for custom integrations:
118
+
119
+ ```json
120
+ {
121
+ "scripts": [
122
+ "sha256-abc123...",
123
+ "sha256-def456..."
124
+ ],
125
+ "styles": [
126
+ "sha256-xyz789..."
127
+ ],
128
+ "externalScripts": [
129
+ "https://www.googletagmanager.com"
130
+ ],
131
+ "externalStyles": [
132
+ "https://fonts.googleapis.com"
133
+ ],
134
+ "directives": {
135
+ "default-src": ["'self'"],
136
+ "script-src": ["'self'", "'sha256-abc123...'"]
137
+ }
138
+ }
139
+ ```
140
+
141
+ ## Usage with Nginx
142
+
143
+ ### Option 1: Include the generated file
144
+
145
+ ```nginx
146
+ server {
147
+ listen 443 ssl;
148
+ server_name example.com;
149
+
150
+ include /path/to/dist/_csp/nginx.conf;
151
+
152
+ # ... rest of config
153
+ }
154
+ ```
155
+
156
+ ### Option 2: Use with nginx-proxy (Docker)
157
+
158
+ Copy to your vhost.d folder:
159
+
160
+ ```bash
161
+ cp dist/_csp/nginx.conf /etc/nginx/vhost.d/example.com_location
162
+ ```
163
+
164
+ ### Automation Script
165
+
166
+ Create a script to update nginx after build:
167
+
168
+ ```bash
169
+ #!/bin/bash
170
+ # scripts/update-csp.sh
171
+
172
+ CSP_CONF="./dist/_csp/nginx.conf"
173
+ VHOST_DIR="/etc/nginx/vhost.d"
174
+
175
+ # Extract CSP line
176
+ CSP_LINE=$(grep 'add_header Content-Security-Policy' "$CSP_CONF")
177
+
178
+ # Update vhost files
179
+ for file in "$VHOST_DIR/example.com" "$VHOST_DIR/example.com_location"; do
180
+ if [ -f "$file" ]; then
181
+ sed -i '/^add_header Content-Security-Policy/c\'"$CSP_LINE" "$file"
182
+ fi
183
+ done
184
+
185
+ # Reload nginx
186
+ nginx -s reload
187
+ ```
188
+
189
+ ## How It Works
190
+
191
+ ### Build Process
192
+
193
+ ```
194
+ ┌─────────────────────────────────────────────────────────────┐
195
+ │ Astro Build │
196
+ └─────────────────────────────────────────────────────────────┘
197
+
198
+
199
+ ┌─────────────────────────────────────────────────────────────┐
200
+ │ 1. Scanner │
201
+ │ - Find all HTML files in dist/ │
202
+ │ - Extract inline <script> and <style> content │
203
+ │ - Detect external scripts (https://) │
204
+ └─────────────────────────────────────────────────────────────┘
205
+
206
+
207
+ ┌─────────────────────────────────────────────────────────────┐
208
+ │ 2. Hasher │
209
+ │ - Calculate SHA-256 hash for each inline element │
210
+ │ - Format as 'sha256-{base64}' │
211
+ └─────────────────────────────────────────────────────────────┘
212
+
213
+
214
+ ┌─────────────────────────────────────────────────────────────┐
215
+ │ 3. Injector │
216
+ │ - Add integrity="sha256-..." to HTML elements │
217
+ │ - Update HTML files in place │
218
+ └─────────────────────────────────────────────────────────────┘
219
+
220
+
221
+ ┌─────────────────────────────────────────────────────────────┐
222
+ │ 4. Generator │
223
+ │ - Merge hashes with presets and custom directives │
224
+ │ - Generate nginx.conf with CSP header │
225
+ │ - Generate hashes.json for custom use │
226
+ └─────────────────────────────────────────────────────────────┘
227
+ ```
228
+
229
+ ### Security Benefits
230
+
231
+ | Without gila-astro-csp | With gila-astro-csp |
232
+ |------------------------|---------------------|
233
+ | `script-src 'unsafe-inline'` | `script-src 'sha256-...'` |
234
+ | Vulnerable to XSS | Protected against XSS |
235
+ | Security grade: C/D | Security grade: A+ |
236
+
237
+ ## Examples
238
+
239
+ ### Basic Static Site
240
+
241
+ ```javascript
242
+ // astro.config.mjs
243
+ import { gilaCSP } from 'gila-astro-csp';
244
+
245
+ export default defineConfig({
246
+ integrations: [
247
+ gilaCSP()
248
+ ]
249
+ });
250
+ ```
251
+
252
+ ### With Google Analytics
253
+
254
+ ```javascript
255
+ import { gilaCSP } from 'gila-astro-csp';
256
+
257
+ export default defineConfig({
258
+ integrations: [
259
+ gilaCSP({
260
+ presets: ['google-analytics'],
261
+ })
262
+ ]
263
+ });
264
+ ```
265
+
266
+ ### With Multiple Services
267
+
268
+ ```javascript
269
+ import { gilaCSP } from 'gila-astro-csp';
270
+
271
+ export default defineConfig({
272
+ integrations: [
273
+ gilaCSP({
274
+ presets: ['google-analytics', 'cloudflare-insights', 'google-fonts'],
275
+ directives: {
276
+ 'img-src': ["'self'", 'data:', 'https:', 'blob:'],
277
+ 'connect-src': ['https://api.myservice.com'],
278
+ },
279
+ })
280
+ ]
281
+ });
282
+ ```
283
+
284
+ ### Custom Output Paths
285
+
286
+ ```javascript
287
+ import { gilaCSP } from 'gila-astro-csp';
288
+
289
+ export default defineConfig({
290
+ integrations: [
291
+ gilaCSP({
292
+ nginx: {
293
+ outputPath: '../nginx/csp.conf',
294
+ includeComments: false,
295
+ },
296
+ json: {
297
+ outputPath: '../config/csp-hashes.json',
298
+ },
299
+ })
300
+ ]
301
+ });
302
+ ```
303
+
304
+ ### Disable JSON Output
305
+
306
+ ```javascript
307
+ import { gilaCSP } from 'gila-astro-csp';
308
+
309
+ export default defineConfig({
310
+ integrations: [
311
+ gilaCSP({
312
+ json: false, // Only generate nginx config
313
+ })
314
+ ]
315
+ });
316
+ ```
317
+
318
+ ## TypeScript Support
319
+
320
+ Full TypeScript support with exported types:
321
+
322
+ ```typescript
323
+ import { gilaCSP } from 'gila-astro-csp';
324
+ import type { GilaCSPOptions, PresetName, CSPDirectives } from 'gila-astro-csp';
325
+
326
+ const options: GilaCSPOptions = {
327
+ presets: ['google-analytics'] as PresetName[],
328
+ directives: {
329
+ 'img-src': ["'self'", 'data:'],
330
+ } as Partial<CSPDirectives>,
331
+ };
332
+
333
+ export default defineConfig({
334
+ integrations: [gilaCSP(options)]
335
+ });
336
+ ```
337
+
338
+ ## Troubleshooting
339
+
340
+ ### Scripts still blocked after build
341
+
342
+ 1. Clear browser cache
343
+ 2. Verify nginx reloaded: `nginx -s reload`
344
+ 3. Check CSP header in DevTools > Network > Response Headers
345
+
346
+ ### Hash mismatch errors
347
+
348
+ Hashes are calculated from exact content. If you see mismatches:
349
+
350
+ 1. Ensure no post-build modifications to HTML
351
+ 2. Check for whitespace differences
352
+ 3. Rebuild: `npm run build`
353
+
354
+ ### External scripts not working
355
+
356
+ Add the domain to presets or directives:
357
+
358
+ ```javascript
359
+ gilaCSP({
360
+ directives: {
361
+ 'script-src': ['https://cdn.example.com'],
362
+ },
363
+ })
364
+ ```
365
+
366
+ ## Contributing
367
+
368
+ Contributions are welcome! Please read our contributing guidelines.
369
+
370
+ ```bash
371
+ # Clone
372
+ git clone https://github.com/petryx/gila-astro-csp.git
373
+
374
+ # Install
375
+ npm install
376
+
377
+ # Test
378
+ npm test
379
+
380
+ # Build
381
+ npm run build
382
+ ```
383
+
384
+ ## License
385
+
386
+ MIT License - see [LICENSE](LICENSE) for details.
387
+
388
+ ## Credits
389
+
390
+ Developed by [Gila Security](https://gilasecurity.com) - Cybersecurity specialists helping companies protect their digital assets.
391
+
392
+ ---
393
+
394
+ **Need help securing your web application?** [Contact Gila Security](https://gilasecurity.com/contato) for professional cybersecurity services.
@@ -0,0 +1,4 @@
1
+ import type { InlineElement, HashResult } from './types.js';
2
+ export declare function calculateHash(content: string): string;
3
+ export declare function hashInlineElements(elements: InlineElement[], type: 'script' | 'style'): HashResult[];
4
+ //# sourceMappingURL=hasher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hasher.d.ts","sourceRoot":"","sources":["../src/hasher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE5D,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKrD;AAED,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,aAAa,EAAE,EACzB,IAAI,EAAE,QAAQ,GAAG,OAAO,GACvB,UAAU,EAAE,CAMd"}
package/dist/hasher.js ADDED
@@ -0,0 +1,14 @@
1
+ import { createHash } from 'node:crypto';
2
+ export function calculateHash(content) {
3
+ const hash = createHash('sha256')
4
+ .update(content, 'utf8')
5
+ .digest('base64');
6
+ return `sha256-${hash}`;
7
+ }
8
+ export function hashInlineElements(elements, type) {
9
+ return elements.map(element => ({
10
+ hash: calculateHash(element.content),
11
+ content: element.content,
12
+ type
13
+ }));
14
+ }
@@ -0,0 +1,3 @@
1
+ export { gilaCSP } from './integration.js';
2
+ export type { GilaCSPOptions, NginxOptions, JsonOptions, PresetName, CSPDirectives } from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,YAAY,EACV,cAAc,EACd,YAAY,EACZ,WAAW,EACX,UAAU,EACV,aAAa,EACd,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { gilaCSP } from './integration.js';
@@ -0,0 +1,10 @@
1
+ import type { HashResult, ProcessedHashes } from './types.js';
2
+ export interface ProcessResult {
3
+ html: string;
4
+ hashes: ProcessedHashes;
5
+ externalScripts: string[];
6
+ externalStyles: string[];
7
+ }
8
+ export declare function injectIntegrity(html: string, hashes: HashResult[]): string;
9
+ export declare function processHtml(html: string): ProcessResult;
10
+ //# sourceMappingURL=injector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"injector.d.ts","sourceRoot":"","sources":["../src/injector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAI9D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,eAAe,CAAC;IACxB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,CA2B1E;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAkBvD"}
@@ -0,0 +1,40 @@
1
+ import { extractInlineElements } from './scanner.js';
2
+ import { hashInlineElements } from './hasher.js';
3
+ export function injectIntegrity(html, hashes) {
4
+ if (hashes.length === 0) {
5
+ return html;
6
+ }
7
+ let result = html;
8
+ for (const hashResult of hashes) {
9
+ const { content, hash, type } = hashResult;
10
+ const escapedContent = escapeRegex(content);
11
+ if (type === 'script') {
12
+ const regex = new RegExp(`(<script)(?![^>]*\\bsrc=)([^>]*>)(${escapedContent})(</script>)`, 'g');
13
+ result = result.replace(regex, `$1 integrity="${hash}"$2$3$4`);
14
+ }
15
+ else {
16
+ const regex = new RegExp(`(<style)([^>]*>)(${escapedContent})(</style>)`, 'g');
17
+ result = result.replace(regex, `$1 integrity="${hash}"$2$3$4`);
18
+ }
19
+ }
20
+ return result;
21
+ }
22
+ export function processHtml(html) {
23
+ const scanResult = extractInlineElements(html);
24
+ const scriptHashes = hashInlineElements(scanResult.scripts, 'script');
25
+ const styleHashes = hashInlineElements(scanResult.styles, 'style');
26
+ const allHashes = [...scriptHashes, ...styleHashes];
27
+ const processedHtml = injectIntegrity(html, allHashes);
28
+ return {
29
+ html: processedHtml,
30
+ hashes: {
31
+ scripts: scriptHashes.map(h => h.hash),
32
+ styles: styleHashes.map(h => h.hash)
33
+ },
34
+ externalScripts: scanResult.externalScripts,
35
+ externalStyles: scanResult.externalStyles
36
+ };
37
+ }
38
+ function escapeRegex(str) {
39
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
40
+ }
@@ -0,0 +1,19 @@
1
+ import type { AstroIntegration } from 'astro';
2
+ import type { GilaCSPOptions, CSPDirectives } from './types.js';
3
+ export interface DirectivesInput {
4
+ scriptHashes: string[];
5
+ styleHashes: string[];
6
+ externalScripts: string[];
7
+ externalStyles: string[];
8
+ }
9
+ export interface ProcessResult {
10
+ processedFiles: number;
11
+ scriptHashes: string[];
12
+ styleHashes: string[];
13
+ externalScripts: string[];
14
+ externalStyles: string[];
15
+ }
16
+ export declare function buildDirectives(input: DirectivesInput, options?: GilaCSPOptions): Partial<CSPDirectives>;
17
+ export declare function processDirectory(dir: string): ProcessResult;
18
+ export declare function gilaCSP(options?: GilaCSPOptions): AstroIntegration;
19
+ //# sourceMappingURL=integration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAG9C,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAKhE,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,eAAe,EACtB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,aAAa,CAAC,CA4ExB;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CA8B3D;AAED,wBAAgB,OAAO,CAAC,OAAO,CAAC,EAAE,cAAc,GAAG,gBAAgB,CAwDlE"}
@@ -0,0 +1,166 @@
1
+ import { readFileSync, writeFileSync, readdirSync, statSync, mkdirSync, existsSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { processHtml } from './injector.js';
4
+ import { applyPresets } from './presets.js';
5
+ import { generateNginxConfig } from './nginx.js';
6
+ export function buildDirectives(input, options) {
7
+ const directives = {
8
+ 'default-src': ["'self'"],
9
+ 'script-src': ["'self'"],
10
+ 'style-src': ["'self'"],
11
+ 'img-src': ["'self'", 'data:'],
12
+ 'font-src': ["'self'"],
13
+ 'connect-src': ["'self'"],
14
+ 'frame-ancestors': ["'none'"],
15
+ 'form-action': ["'self'"],
16
+ 'base-uri': ["'self'"]
17
+ };
18
+ for (const hash of input.scriptHashes) {
19
+ directives['script-src'].push(`'${hash}'`);
20
+ }
21
+ for (const hash of input.styleHashes) {
22
+ directives['style-src'].push(`'${hash}'`);
23
+ }
24
+ for (const url of input.externalScripts) {
25
+ const origin = extractOrigin(url);
26
+ if (!directives['script-src'].includes(origin)) {
27
+ directives['script-src'].push(origin);
28
+ }
29
+ }
30
+ for (const url of input.externalStyles) {
31
+ const origin = extractOrigin(url);
32
+ if (!directives['style-src'].includes(origin)) {
33
+ directives['style-src'].push(origin);
34
+ }
35
+ }
36
+ if (options?.presets) {
37
+ const presetResources = applyPresets(options.presets);
38
+ for (const url of presetResources.scripts) {
39
+ if (!directives['script-src'].includes(url)) {
40
+ directives['script-src'].push(url);
41
+ }
42
+ }
43
+ for (const url of presetResources.styles) {
44
+ if (!directives['style-src'].includes(url)) {
45
+ directives['style-src'].push(url);
46
+ }
47
+ }
48
+ for (const url of presetResources.fonts) {
49
+ if (!directives['font-src'].includes(url)) {
50
+ directives['font-src'].push(url);
51
+ }
52
+ }
53
+ for (const url of presetResources.connect) {
54
+ if (!directives['connect-src'].includes(url)) {
55
+ directives['connect-src'].push(url);
56
+ }
57
+ }
58
+ }
59
+ if (options?.directives) {
60
+ for (const [key, values] of Object.entries(options.directives)) {
61
+ const directive = key;
62
+ if (values) {
63
+ directives[directive] = [
64
+ ...(directives[directive] || []),
65
+ ...values.filter(v => !directives[directive]?.includes(v))
66
+ ];
67
+ }
68
+ }
69
+ }
70
+ return directives;
71
+ }
72
+ export function processDirectory(dir) {
73
+ const result = {
74
+ processedFiles: 0,
75
+ scriptHashes: [],
76
+ styleHashes: [],
77
+ externalScripts: [],
78
+ externalStyles: []
79
+ };
80
+ const htmlFiles = findHtmlFiles(dir);
81
+ for (const file of htmlFiles) {
82
+ const html = readFileSync(file, 'utf-8');
83
+ const processed = processHtml(html);
84
+ writeFileSync(file, processed.html);
85
+ result.processedFiles++;
86
+ result.scriptHashes.push(...processed.hashes.scripts);
87
+ result.styleHashes.push(...processed.hashes.styles);
88
+ result.externalScripts.push(...processed.externalScripts);
89
+ result.externalStyles.push(...processed.externalStyles);
90
+ }
91
+ result.scriptHashes = [...new Set(result.scriptHashes)];
92
+ result.styleHashes = [...new Set(result.styleHashes)];
93
+ result.externalScripts = [...new Set(result.externalScripts)];
94
+ result.externalStyles = [...new Set(result.externalStyles)];
95
+ return result;
96
+ }
97
+ export function gilaCSP(options) {
98
+ return {
99
+ name: 'gila-astro-csp',
100
+ hooks: {
101
+ 'astro:build:done': async ({ dir }) => {
102
+ const distPath = dir.pathname;
103
+ const result = processDirectory(distPath);
104
+ const directives = buildDirectives({
105
+ scriptHashes: result.scriptHashes,
106
+ styleHashes: result.styleHashes,
107
+ externalScripts: result.externalScripts,
108
+ externalStyles: result.externalStyles
109
+ }, options);
110
+ if (options?.nginx !== false) {
111
+ const nginxOpts = typeof options?.nginx === 'object' ? options.nginx : {};
112
+ const outputPath = nginxOpts.outputPath || './dist/_csp/nginx.conf';
113
+ const config = generateNginxConfig(directives, nginxOpts);
114
+ const outputDir = dirname(outputPath);
115
+ if (!existsSync(outputDir)) {
116
+ mkdirSync(outputDir, { recursive: true });
117
+ }
118
+ writeFileSync(outputPath, config);
119
+ }
120
+ if (options?.json !== false) {
121
+ const jsonOpts = typeof options?.json === 'object' ? options.json : {};
122
+ const outputPath = jsonOpts.outputPath || './dist/_csp/hashes.json';
123
+ const pretty = jsonOpts.pretty !== false;
124
+ const outputDir = dirname(outputPath);
125
+ if (!existsSync(outputDir)) {
126
+ mkdirSync(outputDir, { recursive: true });
127
+ }
128
+ const jsonContent = {
129
+ scripts: result.scriptHashes,
130
+ styles: result.styleHashes,
131
+ externalScripts: result.externalScripts,
132
+ externalStyles: result.externalStyles,
133
+ directives
134
+ };
135
+ writeFileSync(outputPath, pretty ? JSON.stringify(jsonContent, null, 2) : JSON.stringify(jsonContent));
136
+ }
137
+ console.log(`[gila-astro-csp] Processed ${result.processedFiles} HTML files`);
138
+ console.log(`[gila-astro-csp] Found ${result.scriptHashes.length} script hashes, ${result.styleHashes.length} style hashes`);
139
+ }
140
+ }
141
+ };
142
+ }
143
+ function findHtmlFiles(dir) {
144
+ const files = [];
145
+ const entries = readdirSync(dir);
146
+ for (const entry of entries) {
147
+ const fullPath = join(dir, entry);
148
+ const stat = statSync(fullPath);
149
+ if (stat.isDirectory()) {
150
+ files.push(...findHtmlFiles(fullPath));
151
+ }
152
+ else if (entry.endsWith('.html')) {
153
+ files.push(fullPath);
154
+ }
155
+ }
156
+ return files;
157
+ }
158
+ function extractOrigin(url) {
159
+ try {
160
+ const parsed = new URL(url);
161
+ return `${parsed.protocol}//${parsed.host}`;
162
+ }
163
+ catch {
164
+ return url;
165
+ }
166
+ }
@@ -0,0 +1,4 @@
1
+ import type { CSPDirectives, NginxOptions } from './types.js';
2
+ export declare function generateCSPHeader(directives: Partial<CSPDirectives>): string;
3
+ export declare function generateNginxConfig(directives: Partial<CSPDirectives>, options?: Partial<NginxOptions>): string;
4
+ //# sourceMappingURL=nginx.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nginx.d.ts","sourceRoot":"","sources":["../src/nginx.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAO9D,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,MAAM,CAuB5E;AAED,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,OAAO,CAAC,aAAa,CAAC,EAClC,OAAO,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAC9B,MAAM,CAeR"}
package/dist/nginx.js ADDED
@@ -0,0 +1,37 @@
1
+ const DEFAULT_OPTIONS = {
2
+ outputPath: './dist/_csp/nginx.conf',
3
+ includeComments: true
4
+ };
5
+ export function generateCSPHeader(directives) {
6
+ const parts = [];
7
+ const directiveOrder = [
8
+ 'default-src',
9
+ 'script-src',
10
+ 'style-src',
11
+ 'img-src',
12
+ 'font-src',
13
+ 'connect-src',
14
+ 'frame-ancestors',
15
+ 'form-action',
16
+ 'base-uri'
17
+ ];
18
+ for (const directive of directiveOrder) {
19
+ const values = directives[directive];
20
+ if (values && values.length > 0) {
21
+ parts.push(`${directive} ${values.join(' ')}`);
22
+ }
23
+ }
24
+ return parts.join('; ');
25
+ }
26
+ export function generateNginxConfig(directives, options) {
27
+ const opts = { ...DEFAULT_OPTIONS, ...options };
28
+ const cspHeader = generateCSPHeader(directives);
29
+ const lines = [];
30
+ if (opts.includeComments) {
31
+ lines.push('# Content-Security-Policy Header');
32
+ lines.push('# Generated by gila-astro-csp');
33
+ lines.push('');
34
+ }
35
+ lines.push(`add_header Content-Security-Policy "${cspHeader}" always;`);
36
+ return lines.join('\n');
37
+ }
@@ -0,0 +1,11 @@
1
+ import type { PresetName, Preset } from './types.js';
2
+ export interface PresetResources {
3
+ scripts: string[];
4
+ styles: string[];
5
+ fonts: string[];
6
+ connect: string[];
7
+ }
8
+ export declare const PRESETS: Record<PresetName, Preset>;
9
+ export declare function getPreset(name: PresetName): Preset;
10
+ export declare function applyPresets(presetNames: PresetName[]): PresetResources;
11
+ //# sourceMappingURL=presets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../src/presets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAErD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CA+B9C,CAAC;AAEF,wBAAgB,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAMlD;AAED,wBAAgB,YAAY,CAAC,WAAW,EAAE,UAAU,EAAE,GAAG,eAAe,CA+BvE"}
@@ -0,0 +1,68 @@
1
+ export const PRESETS = {
2
+ 'google-analytics': {
3
+ name: 'google-analytics',
4
+ scripts: [
5
+ 'https://www.googletagmanager.com',
6
+ 'https://www.google-analytics.com'
7
+ ],
8
+ connect: [
9
+ 'https://www.google-analytics.com',
10
+ 'https://analytics.google.com',
11
+ 'https://stats.g.doubleclick.net'
12
+ ]
13
+ },
14
+ 'cloudflare-insights': {
15
+ name: 'cloudflare-insights',
16
+ scripts: [
17
+ 'https://static.cloudflareinsights.com'
18
+ ],
19
+ connect: [
20
+ 'https://cloudflareinsights.com'
21
+ ]
22
+ },
23
+ 'google-fonts': {
24
+ name: 'google-fonts',
25
+ styles: [
26
+ 'https://fonts.googleapis.com'
27
+ ],
28
+ fonts: [
29
+ 'https://fonts.gstatic.com'
30
+ ]
31
+ }
32
+ };
33
+ export function getPreset(name) {
34
+ const preset = PRESETS[name];
35
+ if (!preset) {
36
+ throw new Error(`Unknown preset: ${name}`);
37
+ }
38
+ return preset;
39
+ }
40
+ export function applyPresets(presetNames) {
41
+ const result = {
42
+ scripts: [],
43
+ styles: [],
44
+ fonts: [],
45
+ connect: []
46
+ };
47
+ for (const name of presetNames) {
48
+ const preset = getPreset(name);
49
+ if (preset.scripts) {
50
+ result.scripts.push(...preset.scripts);
51
+ }
52
+ if (preset.styles) {
53
+ result.styles.push(...preset.styles);
54
+ }
55
+ if (preset.fonts) {
56
+ result.fonts.push(...preset.fonts);
57
+ }
58
+ if (preset.connect) {
59
+ result.connect.push(...preset.connect);
60
+ }
61
+ }
62
+ return {
63
+ scripts: [...new Set(result.scripts)],
64
+ styles: [...new Set(result.styles)],
65
+ fonts: [...new Set(result.fonts)],
66
+ connect: [...new Set(result.connect)]
67
+ };
68
+ }
@@ -0,0 +1,3 @@
1
+ import type { ScanResult } from './types.js';
2
+ export declare function extractInlineElements(html: string): ScanResult;
3
+ //# sourceMappingURL=scanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAiB,MAAM,YAAY,CAAC;AAO5D,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAmD9D"}
@@ -0,0 +1,51 @@
1
+ const INLINE_SCRIPT_REGEX = /<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/gi;
2
+ const EXTERNAL_SCRIPT_REGEX = /<script[^>]*\bsrc=["']([^"']+)["'][^>]*>/gi;
3
+ const INLINE_STYLE_REGEX = /<style[^>]*>([\s\S]*?)<\/style>/gi;
4
+ const EXTERNAL_STYLE_REGEX = /<link[^>]*\brel=["']stylesheet["'][^>]*\bhref=["']([^"']+)["'][^>]*>/gi;
5
+ export function extractInlineElements(html) {
6
+ const scripts = [];
7
+ const styles = [];
8
+ const externalScripts = [];
9
+ const externalStyles = [];
10
+ // Extract inline scripts
11
+ let match;
12
+ const scriptRegex = new RegExp(INLINE_SCRIPT_REGEX.source, 'gi');
13
+ while ((match = scriptRegex.exec(html)) !== null) {
14
+ scripts.push({
15
+ content: match[1],
16
+ startIndex: match.index,
17
+ endIndex: match.index + match[0].length
18
+ });
19
+ }
20
+ // Extract external scripts (only https://)
21
+ const extScriptRegex = new RegExp(EXTERNAL_SCRIPT_REGEX.source, 'gi');
22
+ while ((match = extScriptRegex.exec(html)) !== null) {
23
+ const url = match[1];
24
+ if (url.startsWith('https://')) {
25
+ externalScripts.push(url);
26
+ }
27
+ }
28
+ // Extract inline styles
29
+ const styleRegex = new RegExp(INLINE_STYLE_REGEX.source, 'gi');
30
+ while ((match = styleRegex.exec(html)) !== null) {
31
+ styles.push({
32
+ content: match[1],
33
+ startIndex: match.index,
34
+ endIndex: match.index + match[0].length
35
+ });
36
+ }
37
+ // Extract external styles (only https://)
38
+ const extStyleRegex = new RegExp(EXTERNAL_STYLE_REGEX.source, 'gi');
39
+ while ((match = extStyleRegex.exec(html)) !== null) {
40
+ const url = match[1];
41
+ if (url.startsWith('https://')) {
42
+ externalStyles.push(url);
43
+ }
44
+ }
45
+ return {
46
+ scripts,
47
+ styles,
48
+ externalScripts,
49
+ externalStyles
50
+ };
51
+ }
@@ -0,0 +1,96 @@
1
+ import type { AstroIntegration } from 'astro';
2
+ export interface GilaCSPOptions {
3
+ /**
4
+ * Presets for common external services
5
+ * @example ['google-analytics', 'cloudflare-insights', 'google-fonts']
6
+ */
7
+ presets?: PresetName[];
8
+ /**
9
+ * Additional external scripts and styles
10
+ */
11
+ external?: {
12
+ scripts?: string[];
13
+ styles?: string[];
14
+ };
15
+ /**
16
+ * Additional CSP directives
17
+ */
18
+ directives?: Partial<CSPDirectives>;
19
+ /**
20
+ * Nginx output configuration
21
+ * Set to false to disable nginx output
22
+ * @default { outputPath: './dist/_csp/nginx.conf' }
23
+ */
24
+ nginx?: NginxOptions | false;
25
+ /**
26
+ * JSON output configuration
27
+ * Set to false to disable JSON output
28
+ * @default { outputPath: './dist/_csp/hashes.json' }
29
+ */
30
+ json?: JsonOptions | false;
31
+ }
32
+ export interface NginxOptions {
33
+ /**
34
+ * Path to output nginx config file
35
+ * @default './dist/_csp/nginx.conf'
36
+ */
37
+ outputPath?: string;
38
+ /**
39
+ * Include explanatory comments in output
40
+ * @default true
41
+ */
42
+ includeComments?: boolean;
43
+ }
44
+ export interface JsonOptions {
45
+ /**
46
+ * Path to output JSON file
47
+ * @default './dist/_csp/hashes.json'
48
+ */
49
+ outputPath?: string;
50
+ /**
51
+ * Pretty print JSON
52
+ * @default true
53
+ */
54
+ pretty?: boolean;
55
+ }
56
+ export type PresetName = 'google-analytics' | 'cloudflare-insights' | 'google-fonts';
57
+ export interface Preset {
58
+ name: PresetName;
59
+ scripts?: string[];
60
+ styles?: string[];
61
+ connect?: string[];
62
+ fonts?: string[];
63
+ }
64
+ export interface CSPDirectives {
65
+ 'default-src': string[];
66
+ 'script-src': string[];
67
+ 'style-src': string[];
68
+ 'img-src': string[];
69
+ 'font-src': string[];
70
+ 'connect-src': string[];
71
+ 'frame-ancestors': string[];
72
+ 'form-action': string[];
73
+ 'base-uri': string[];
74
+ }
75
+ export interface HashResult {
76
+ hash: string;
77
+ content: string;
78
+ type: 'script' | 'style';
79
+ }
80
+ export interface ScanResult {
81
+ scripts: InlineElement[];
82
+ styles: InlineElement[];
83
+ externalScripts: string[];
84
+ externalStyles: string[];
85
+ }
86
+ export interface InlineElement {
87
+ content: string;
88
+ startIndex: number;
89
+ endIndex: number;
90
+ }
91
+ export interface ProcessedHashes {
92
+ scripts: string[];
93
+ styles: string[];
94
+ }
95
+ export type GilaCSPIntegration = (options?: GilaCSPOptions) => AstroIntegration;
96
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAE9C,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;IAEvB;;OAEG;IACH,QAAQ,CAAC,EAAE;QACT,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;QACnB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;KACnB,CAAC;IAEF;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IAEpC;;;;OAIG;IACH,KAAK,CAAC,EAAE,YAAY,GAAG,KAAK,CAAC;IAE7B;;;;OAIG;IACH,IAAI,CAAC,EAAE,WAAW,GAAG,KAAK,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,MAAM,UAAU,GAClB,kBAAkB,GAClB,qBAAqB,GACrB,cAAc,CAAC;AAEnB,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,CAAC,EAAE,cAAc,KAAK,gBAAgB,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "gila-astro-csp",
3
+ "version": "0.1.0",
4
+ "description": "Astro 5+ integration for SRI (Subresource Integrity) and CSP (Content-Security-Policy) with nginx support",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "dev": "tsc --watch",
20
+ "test": "vitest",
21
+ "test:run": "vitest run",
22
+ "test:coverage": "vitest run --coverage",
23
+ "lint": "eslint src --ext .ts",
24
+ "prepare": "npm run build",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "keywords": [
28
+ "astro",
29
+ "astro-integration",
30
+ "csp",
31
+ "content-security-policy",
32
+ "sri",
33
+ "subresource-integrity",
34
+ "security",
35
+ "nginx"
36
+ ],
37
+ "author": "Gila Security <contato@gilasecurity.com> (https://gilasecurity.com)",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/petryx/gila-astro-csp.git"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/petryx/gila-astro-csp/issues"
45
+ },
46
+ "homepage": "https://gilasecurity.com",
47
+ "peerDependencies": {
48
+ "astro": "^5.0.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^22.0.0",
52
+ "astro": "^5.16.0",
53
+ "typescript": "^5.7.0",
54
+ "vitest": "^2.1.0"
55
+ }
56
+ }