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 +394 -0
- package/dist/hasher.d.ts +4 -0
- package/dist/hasher.d.ts.map +1 -0
- package/dist/hasher.js +14 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/injector.d.ts +10 -0
- package/dist/injector.d.ts.map +1 -0
- package/dist/injector.js +40 -0
- package/dist/integration.d.ts +19 -0
- package/dist/integration.d.ts.map +1 -0
- package/dist/integration.js +166 -0
- package/dist/nginx.d.ts +4 -0
- package/dist/nginx.d.ts.map +1 -0
- package/dist/nginx.js +37 -0
- package/dist/presets.d.ts +11 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +68 -0
- package/dist/scanner.d.ts +3 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +51 -0
- package/dist/types.d.ts +96 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +56 -0
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
|
+
[](https://www.npmjs.com/package/gila-astro-csp)
|
|
6
|
+
[](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.
|
package/dist/hasher.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|
package/dist/injector.js
ADDED
|
@@ -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
|
+
}
|
package/dist/nginx.d.ts
ADDED
|
@@ -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"}
|
package/dist/presets.js
ADDED
|
@@ -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 @@
|
|
|
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"}
|
package/dist/scanner.js
ADDED
|
@@ -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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|