@networkpro/web 1.7.6 β 1.8.3
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 +178 -28
- package/_redirects +3 -0
- package/cspell.json +3 -0
- package/netlify/edge-functions/csp-report.js +151 -0
- package/netlify.toml +3 -3
- package/package.json +3 -3
- package/src/hooks.server.js +3 -6
- package/src/lib/components/layout/HeaderDefault.svelte +1 -1
- package/src/lib/components/layout/HeaderHome.svelte +1 -1
- package/src/lib/pages/FossContent.svelte +2 -3
- package/src/lib/styles/css/default.css +11 -2
- package/src/lib/styles/global.min.css +1 -1
- package/src/routes/status/+page.server.js +32 -0
- package/tests/unit/csp-report.test.js +81 -0
- package/netlify-functions/cspReport.js +0 -76
- package/tests/unit/cspReport.test.js +0 -81
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@ This file is part of Network Pro.
|
|
|
17
17
|
[](https://github.com/prettier/prettier) [](https://stylelint.io/)
|
|
18
18
|
[](https://github.com/netwk-pro/netwk-pro.github.io/blob/master/CODE_OF_CONDUCT.md)
|
|
19
19
|
|
|
20
|
+
<section id="top">
|
|
21
|
+
|
|
20
22
|
## π Project Overview
|
|
21
23
|
|
|
22
24
|
This GitHub repository powers the official web presence of **[Network Pro Strategies](https://netwk.pro/about)** β a privacy-first consultancy specializing in cybersecurity, network engineering, and information security. We also lead public advocacy efforts promoting digital privacy and responsible cyber policy.
|
|
@@ -26,7 +28,27 @@ Built with [SvelteKit](https://kit.svelte.dev/) and deployed via [Netlify](https
|
|
|
26
28
|
|
|
27
29
|
All infrastructure and data flows are designed with **maximum transparency, self-hosting, and user privacy** in mind.
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
</section>
|
|
32
|
+
|
|
33
|
+
### Table of Contents
|
|
34
|
+
|
|
35
|
+
- [Repository Structure](#structure)
|
|
36
|
+
- [Getting Started](#getting-started)
|
|
37
|
+
- [Configuration](#configuration)
|
|
38
|
+
- [Service Worker Utilities](#sw-utilities)
|
|
39
|
+
- [CSP Report Handler](#cspreport)
|
|
40
|
+
- [Testing](#testing)
|
|
41
|
+
- [Recommended Toolchain](#toolchain)
|
|
42
|
+
- [Tooling Configuration](#toolconfig)
|
|
43
|
+
- [Available Scripts](#scripts)
|
|
44
|
+
- [License](#license)
|
|
45
|
+
- [Questions](#questions)
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
<section id="structure">
|
|
50
|
+
|
|
51
|
+
## π Repository Structure
|
|
30
52
|
|
|
31
53
|
```bash
|
|
32
54
|
.
|
|
@@ -36,8 +58,9 @@ All infrastructure and data flows are designed with **maximum transparency, self
|
|
|
36
58
|
β βββ extensions.json # Recommended VSCodium / VS Code extensions
|
|
37
59
|
β βββ extensions.jsonc # Commented version of extensions.json for reference
|
|
38
60
|
β βββ settings.json # User settings for VSCodium / VS Code
|
|
39
|
-
βββ netlify
|
|
40
|
-
β βββ
|
|
61
|
+
βββ netlify/
|
|
62
|
+
β βββ edge-functions/ # Netlify Edge Functions directory
|
|
63
|
+
β βββ csp-report.js # Edge Function to receive and log CSP violation reports
|
|
41
64
|
βββ scripts/ # General utility scripts
|
|
42
65
|
βββ src/
|
|
43
66
|
β βββ lib/ # Reusable components, styles, utilities
|
|
@@ -74,8 +97,14 @@ tests/
|
|
|
74
97
|
βββ ...
|
|
75
98
|
```
|
|
76
99
|
|
|
100
|
+
</section>
|
|
101
|
+
|
|
102
|
+
<sub>[Back to top](#top)</sub>
|
|
103
|
+
|
|
77
104
|
---
|
|
78
105
|
|
|
106
|
+
<section id="getting-started">
|
|
107
|
+
|
|
79
108
|
## π Getting Started
|
|
80
109
|
|
|
81
110
|
### π¦ Environment Setup
|
|
@@ -119,9 +148,9 @@ npm install
|
|
|
119
148
|
> You can also use `bootstrap.local.sh` to automate the steps above and more (optional).
|
|
120
149
|
> `ENV_MODE` controls local tooling behavior β it is not used by the app runtime directly.
|
|
121
150
|
|
|
122
|
-
|
|
151
|
+
|
|
123
152
|
|
|
124
|
-
|
|
153
|
+
### πΎ Version Enforcement
|
|
125
154
|
|
|
126
155
|
To ensure consistent environments across contributors and CI systems, this project enforces specific Node.js and npm versions via the `"engines"` field in `package.json`:
|
|
127
156
|
|
|
@@ -185,7 +214,13 @@ node -v # Should fall within engines.node
|
|
|
185
214
|
npm -v # Should fall within engines.npm
|
|
186
215
|
```
|
|
187
216
|
|
|
188
|
-
|
|
217
|
+
</section>
|
|
218
|
+
|
|
219
|
+
<sub>[Back to top](#top)</sub>
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
<section id="configuration">
|
|
189
224
|
|
|
190
225
|
## π‘οΈ Configuration
|
|
191
226
|
|
|
@@ -208,36 +243,117 @@ To re-enable nonce generation for inline scripts in the future:
|
|
|
208
243
|
|
|
209
244
|
> π‘ The `[headers]` block in `netlify.toml` has been deprecated β all headers are now set dynamically from within SvelteKit.
|
|
210
245
|
|
|
211
|
-
|
|
246
|
+
|
|
212
247
|
|
|
213
248
|
### π§ `hooks.client.ts`
|
|
214
249
|
|
|
215
|
-
|
|
250
|
+
Located at `src/hooks.client.ts`, this file is currently limited to handling uncaught client-side errors via the `handleError()` lifecycle hook.
|
|
216
251
|
|
|
217
|
-
-
|
|
218
|
-
- Provides a `handleError()` hook that logs uncaught client-side errors
|
|
252
|
+
Client-side PWA logic (such as handling the `beforeinstallprompt` event, checking browser compatibility, and registering the service worker) has been moved to `src/lib/registerServiceWorker.js` for better modularity and testability.
|
|
219
253
|
|
|
220
|
-
|
|
254
|
+
> π‘ This separation ensures that error handling is isolated from PWA lifecycle logic, making both concerns easier to maintain.
|
|
255
|
+
|
|
256
|
+
</section>
|
|
257
|
+
|
|
258
|
+
<sub>[Back to top](#top)</sub>
|
|
221
259
|
|
|
222
260
|
---
|
|
223
261
|
|
|
224
|
-
|
|
262
|
+
<section id="sw-utilities">
|
|
263
|
+
|
|
264
|
+
## βοΈ Service Worker Utilities
|
|
265
|
+
|
|
266
|
+
This project includes modular service worker management to support PWA functionality, update lifecycles, and debugging workflows.
|
|
267
|
+
|
|
268
|
+
### β
`registerServiceWorker.js`
|
|
269
|
+
|
|
270
|
+
Located at `src/lib/registerServiceWorker.js`, this module handles:
|
|
271
|
+
|
|
272
|
+
- **Service worker registration** (`service-worker.js`)
|
|
273
|
+
- **Update lifecycle**: prompts users when new content is available
|
|
274
|
+
- **Cache hygiene**: removes unexpected caches not prefixed with `cache-`
|
|
275
|
+
- **Install prompt support**: dispatches a `pwa-install-available` event for custom handling
|
|
276
|
+
- **Firefox compatibility**: skips registration in Firefox during localhost development
|
|
277
|
+
|
|
278
|
+
This function is typically called during client boot from `+layout.svelte` or another root-level component.
|
|
279
|
+
|
|
280
|
+
> βΉοΈ The service worker will not register if the `?nosw` flag is present or if `window.__DISABLE_SW__` is set (see below).
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
### π§Ή `unregisterServiceWorker.js`
|
|
285
|
+
|
|
286
|
+
Located at `src/lib/unregisterServiceWorker.js`, this utility allows for manual deactivation of service workers during debugging or user opt-out flows.
|
|
287
|
+
|
|
288
|
+
It unregisters **all active service worker registrations** and logs the result.
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
### π« `disableSw.js`
|
|
293
|
+
|
|
294
|
+
Located at `static/disableSw.js`, this file sets a global flag if the URL contains the `?nosw` query parameter:
|
|
295
|
+
|
|
296
|
+
```js
|
|
297
|
+
if (location.search.includes("nosw")) {
|
|
298
|
+
window.__DISABLE_SW__ = true;
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
This flag is used by `registerServiceWorker.js` to bypass registration. It's helpful for testing environments, browser compatibility checks, or simulating first-load conditions without service worker interference.
|
|
303
|
+
|
|
304
|
+
To use:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
https://netwk.pro/?nosw
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
> π‘ `disableSw.js` is loaded from the static directory and runs early, ensuring the `__DISABLE_SW__` flag is available before service worker logic executes.
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
#### π§ Example Usage
|
|
315
|
+
|
|
316
|
+
To register the service worker conditionally, call the function from client code:
|
|
317
|
+
|
|
318
|
+
```js
|
|
319
|
+
import { registerServiceWorker } from "$lib/registerServiceWorker.js";
|
|
320
|
+
|
|
321
|
+
registerServiceWorker();
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
You can optionally import unregisterServiceWorker() in a debug menu or settings panel to let users opt out of offline behavior.
|
|
325
|
+
|
|
326
|
+
</section>
|
|
327
|
+
|
|
328
|
+
<sub>[Back to top](#top)</sub>
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
<section id="cspreport">
|
|
333
|
+
|
|
334
|
+
## π£ CSP Report Handler
|
|
225
335
|
|
|
226
336
|
To receive and inspect CSP violation reports in development or production, the repo includes a Netlify-compatible function at:
|
|
227
337
|
|
|
228
338
|
```bash
|
|
229
|
-
netlify-functions/
|
|
339
|
+
netlify/edge-functions/csp-report.js
|
|
230
340
|
```
|
|
231
341
|
|
|
232
|
-
This
|
|
342
|
+
This Edge Function receives Content Security Policy (CSP) violation reports at `/api/csp-report` and logs relevant details to the console. High-risk violations (e.g., `script-src`, `form-action`) also trigger real-time alerts via `ntfy`. You can further integrate with logging tools, SIEM platforms, or notification systems as needed.
|
|
233
343
|
|
|
234
344
|
Make sure to include the `report-uri` directive in your CSP header:
|
|
235
345
|
|
|
236
346
|
```bash
|
|
237
|
-
Content-Security-Policy: ...; report-uri
|
|
347
|
+
Content-Security-Policy: ...; report-uri /api/csp-report;
|
|
238
348
|
```
|
|
239
349
|
|
|
240
|
-
|
|
350
|
+
</section>
|
|
351
|
+
|
|
352
|
+
<sub>[Back to top](#top)</sub>
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
<section id="testing">
|
|
241
357
|
|
|
242
358
|
## π§ͺ Testing
|
|
243
359
|
|
|
@@ -332,8 +448,14 @@ You can also audit locally using Chrome DevTools β Lighthouse tab for on-the-f
|
|
|
332
448
|
|
|
333
449
|
<!-- markdownlint-disable MD028 -->
|
|
334
450
|
|
|
451
|
+
</section>
|
|
452
|
+
|
|
453
|
+
<sub>[Back to top](#top)</sub>
|
|
454
|
+
|
|
335
455
|
---
|
|
336
456
|
|
|
457
|
+
<section id="toolchain">
|
|
458
|
+
|
|
337
459
|
## π Recommended Toolchain
|
|
338
460
|
|
|
339
461
|
To streamline development and align with project conventions, we recommend the following setup β especially for contributors without a strong existing preference.
|
|
@@ -371,8 +493,14 @@ npm run lint:fix
|
|
|
371
493
|
npm run format:fix
|
|
372
494
|
```
|
|
373
495
|
|
|
496
|
+
</section>
|
|
497
|
+
|
|
498
|
+
<sub>[Back to top](#top)</sub>
|
|
499
|
+
|
|
374
500
|
---
|
|
375
501
|
|
|
502
|
+
<section id="toolconfig">
|
|
503
|
+
|
|
376
504
|
## βοΈ Tooling Configuration
|
|
377
505
|
|
|
378
506
|
All linting, formatting, and version settings are defined in versioned project config files:
|
|
@@ -395,8 +523,14 @@ These are the same rules used by CI and automation, so aligning your local setup
|
|
|
395
523
|
|
|
396
524
|
> Note: `.vscode/extensions.json` defines a minimal recommended dev stack for VSCodium / VS Code. These extensions are **optional but thoughtfully curated** to improve developer experience without introducing bloat.
|
|
397
525
|
|
|
526
|
+
</section>
|
|
527
|
+
|
|
528
|
+
<sub>[Back to top](#top)</sub>
|
|
529
|
+
|
|
398
530
|
---
|
|
399
531
|
|
|
532
|
+
<section id="scripts">
|
|
533
|
+
|
|
400
534
|
## π Available Scripts
|
|
401
535
|
|
|
402
536
|
The following CLI commands are available via `npm run <script>` or `pnpm run <script>`.
|
|
@@ -412,7 +546,7 @@ The following CLI commands are available via `npm run <script>` or `pnpm run <sc
|
|
|
412
546
|
| `build:netlify` | Build using Netlify CLI |
|
|
413
547
|
| `css:bundle` | Bundle and minify CSS |
|
|
414
548
|
|
|
415
|
-
|
|
549
|
+
|
|
416
550
|
|
|
417
551
|
### β
Pre-check / Sync
|
|
418
552
|
|
|
@@ -425,7 +559,7 @@ The following CLI commands are available via `npm run <script>` or `pnpm run <sc
|
|
|
425
559
|
| `checkout` | Full local validation: check versions, test, lint, typecheck |
|
|
426
560
|
| `verify` | Alias for `checkout` |
|
|
427
561
|
|
|
428
|
-
|
|
562
|
+
|
|
429
563
|
|
|
430
564
|
### π§Ή Cleanup & Maintenance
|
|
431
565
|
|
|
@@ -435,7 +569,7 @@ The following CLI commands are available via `npm run <script>` or `pnpm run <sc
|
|
|
435
569
|
| `clean` | Fully reset environment and reinstall |
|
|
436
570
|
| `upgrade` | Update all dependencies via `npm-check-updates` |
|
|
437
571
|
|
|
438
|
-
|
|
572
|
+
|
|
439
573
|
|
|
440
574
|
<!-- markdownlint-disable MD024 -->
|
|
441
575
|
|
|
@@ -453,7 +587,7 @@ The following CLI commands are available via `npm run <script>` or `pnpm run <sc
|
|
|
453
587
|
| `test:coverage` | Collect coverage from both client and server |
|
|
454
588
|
| `test:e2e` | Runs E2E tests with up to 1 automatic retry on failure |
|
|
455
589
|
|
|
456
|
-
|
|
590
|
+
|
|
457
591
|
|
|
458
592
|
### π§Ό Linting & Formatting
|
|
459
593
|
|
|
@@ -468,7 +602,7 @@ The following CLI commands are available via `npm run <script>` or `pnpm run <sc
|
|
|
468
602
|
| `format` | Run Prettier formatting check |
|
|
469
603
|
| `format:fix` | Auto-format code using Prettier |
|
|
470
604
|
|
|
471
|
-
|
|
605
|
+
|
|
472
606
|
|
|
473
607
|
### π‘ Lighthouse / Performance
|
|
474
608
|
|
|
@@ -477,7 +611,7 @@ The following CLI commands are available via `npm run <script>` or `pnpm run <sc
|
|
|
477
611
|
| `lhci` | Alias for Lighthouse CI |
|
|
478
612
|
| `lhci:run` | Run Lighthouse CI autorun |
|
|
479
613
|
|
|
480
|
-
|
|
614
|
+
|
|
481
615
|
|
|
482
616
|
### π Audits / Validation
|
|
483
617
|
|
|
@@ -487,7 +621,7 @@ The following CLI commands are available via `npm run <script>` or `pnpm run <sc
|
|
|
487
621
|
| `head:flatten` | Flatten headers for Netlify |
|
|
488
622
|
| `head:validate` | Validate headers file against project config |
|
|
489
623
|
|
|
490
|
-
|
|
624
|
+
|
|
491
625
|
|
|
492
626
|
### π Lifecycle Hooks
|
|
493
627
|
|
|
@@ -495,10 +629,14 @@ The following CLI commands are available via `npm run <script>` or `pnpm run <sc
|
|
|
495
629
|
| ------------- | ----------------------------------- |
|
|
496
630
|
| `postinstall` | Ensures version check after install |
|
|
497
631
|
|
|
498
|
-
|
|
632
|
+
</section>
|
|
633
|
+
|
|
634
|
+
<sub>[Back to top](#top)</sub>
|
|
499
635
|
|
|
500
636
|
---
|
|
501
637
|
|
|
638
|
+
<section id="license">
|
|
639
|
+
|
|
502
640
|
## π§Ύ License
|
|
503
641
|
|
|
504
642
|
This project is licensed under:
|
|
@@ -509,11 +647,21 @@ This project is licensed under:
|
|
|
509
647
|
|
|
510
648
|
Source code, branding, and visual assets are subject to reuse and distribution terms specified on our [Legal, Copyright, and Licensing page](https://netwk.pro/license).
|
|
511
649
|
|
|
512
|
-
|
|
650
|
+
</section>
|
|
651
|
+
|
|
652
|
+
<sub>[Back to top](#top)</sub>
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
<section id="questions">
|
|
513
657
|
|
|
514
658
|
## πββοΈQuestions?
|
|
515
659
|
|
|
516
|
-
Reach out via [
|
|
660
|
+
Reach out via our [Contact Form](https://netwk.pro/contact), open an issue on this repo, or email us directly at `support (at) neteng.pro`.
|
|
661
|
+
|
|
662
|
+
</section>
|
|
663
|
+
|
|
664
|
+
<sub>[Back to top](#top)</sub>
|
|
517
665
|
|
|
518
666
|
|
|
519
667
|
|
|
@@ -522,7 +670,7 @@ _Designed for professionals. Hardened for privacy. Built with intent._
|
|
|
522
670
|
|
|
523
671
|
---
|
|
524
672
|
|
|
525
|
-
<
|
|
673
|
+
<span style="font-size: 12px; text-align: center;">
|
|
526
674
|
|
|
527
675
|
Copyright © 2025
|
|
528
676
|
**[Network Pro Strategies](https://netwk.pro) (Network Pro™)**
|
|
@@ -531,4 +679,6 @@ Network Pro™, the shield logo, and the "Locking Down Networks™" slog
|
|
|
531
679
|
|
|
532
680
|
Licensed under **[CC BY 4.0](https://netwk.pro/license#cc-by)** and the **[GNU GPL](https://netwk.pro/license#gnu-gpl)**, as published by the [Free Software Foundation](https://www.fsf.org), either version 3 of the License, or (at your option) any later version.
|
|
533
681
|
|
|
534
|
-
</
|
|
682
|
+
</span>
|
|
683
|
+
|
|
684
|
+
<!-- cspell:ignore cspreport toolconfig -->
|
package/_redirects
CHANGED
package/cspell.json
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"homescreen",
|
|
20
20
|
"Izzy",
|
|
21
21
|
"lhci",
|
|
22
|
+
"lifecycles",
|
|
22
23
|
"lighthouseci",
|
|
23
24
|
"lighthouserc",
|
|
24
25
|
"lightningcss",
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
"nosniff",
|
|
36
37
|
"nosw",
|
|
37
38
|
"npmjs",
|
|
39
|
+
"ntfy",
|
|
38
40
|
"obtainium",
|
|
39
41
|
"posthog",
|
|
40
42
|
"SIEM",
|
|
@@ -45,6 +47,7 @@
|
|
|
45
47
|
"subsites",
|
|
46
48
|
"Supercookie",
|
|
47
49
|
"supercookies",
|
|
50
|
+
"unregisters",
|
|
48
51
|
"urlcheck",
|
|
49
52
|
"vcard",
|
|
50
53
|
"vite",
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
edge-functions/csp-report.js
|
|
3
|
+
|
|
4
|
+
Copyright Β© 2025 Network Pro Strategies (Network Proβ’)
|
|
5
|
+
SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
|
|
6
|
+
This file is part of Network Pro.
|
|
7
|
+
========================================================================== */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @file csp-report.js
|
|
11
|
+
* @description Netlify Edge Function to handle CSP violation reports.
|
|
12
|
+
*
|
|
13
|
+
* Accepts POST requests to /api/csp-report and logs relevant CSP reports.
|
|
14
|
+
* Filters out common low-value reports (e.g., img-src) to reduce invocation
|
|
15
|
+
* cost. Alerts on high-risk violations via ntfy topic.
|
|
16
|
+
*
|
|
17
|
+
* @module netlify/edge-functions
|
|
18
|
+
* @author SunDevil311
|
|
19
|
+
* @updated 2025-05-31
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Netlify Edge Function entry point for CSP reporting.
|
|
24
|
+
*
|
|
25
|
+
* @param {Request} request - The incoming HTTP request object
|
|
26
|
+
* @param {import('@netlify/edge-functions').Context} _context - The Netlify Edge Function context (unused)
|
|
27
|
+
* @returns {Promise<Response>} HTTP Response with status 204 or 405
|
|
28
|
+
*/
|
|
29
|
+
export default async (request, _context) => {
|
|
30
|
+
if (request.method !== "POST") {
|
|
31
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const body = await request.json();
|
|
36
|
+
const report = body["csp-report"];
|
|
37
|
+
|
|
38
|
+
// Ignore if report is missing or malformed
|
|
39
|
+
if (!report || typeof report !== "object") {
|
|
40
|
+
return new Response(null, { status: 204 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const violated = report["violated-directive"] ?? "";
|
|
44
|
+
const blockedUri = report["blocked-uri"] ?? "";
|
|
45
|
+
|
|
46
|
+
// Filter: Skip img-src violations and empty URIs
|
|
47
|
+
const ignored = [
|
|
48
|
+
violated.startsWith("img-src"),
|
|
49
|
+
blockedUri === "",
|
|
50
|
+
blockedUri === "about",
|
|
51
|
+
blockedUri.startsWith("chrome-extension://"),
|
|
52
|
+
blockedUri.startsWith("moz-extension://"),
|
|
53
|
+
].some(Boolean);
|
|
54
|
+
|
|
55
|
+
if (ignored) {
|
|
56
|
+
return new Response(null, { status: 204 });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Send alert for high-risk directives
|
|
60
|
+
await sendToNtfy(violated, blockedUri, report);
|
|
61
|
+
|
|
62
|
+
// Log useful violations
|
|
63
|
+
console.log("[CSP-Edge] Violation:", {
|
|
64
|
+
directive: violated,
|
|
65
|
+
uri: blockedUri,
|
|
66
|
+
referrer: report["referrer"],
|
|
67
|
+
source: report["source-file"],
|
|
68
|
+
line: report["line-number"],
|
|
69
|
+
});
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.warn("[CSP-Edge] Failed to parse CSP report:", err.message);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return new Response(null, { status: 204 });
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const recentViolations = new Map();
|
|
78
|
+
const VIOLATION_TTL_MS = 60_000;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Sends a high-priority alert to your ntfy topic for high-risk CSP violations.
|
|
82
|
+
* Applies rate-limiting to avoid sending duplicate alerts within 60 seconds.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} violated - The violated CSP directive
|
|
85
|
+
* @param {string} blockedUri - The URI that was blocked
|
|
86
|
+
* @param {Record<string, any>} report - The full CSP report object
|
|
87
|
+
*/
|
|
88
|
+
async function sendToNtfy(violated, blockedUri, report) {
|
|
89
|
+
const highRiskDirectives = [
|
|
90
|
+
"script-src",
|
|
91
|
+
"form-action",
|
|
92
|
+
"frame-ancestors",
|
|
93
|
+
"base-uri",
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const directiveKey = violated.split(" ")[0]; // strip fallback values or sources
|
|
97
|
+
const isHighRisk = highRiskDirectives.includes(directiveKey);
|
|
98
|
+
console.log(`[CSP-Edge] Checking directive: ${directiveKey}`);
|
|
99
|
+
if (!isHighRisk) return;
|
|
100
|
+
|
|
101
|
+
const key = `${violated}|${blockedUri}`;
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
|
|
104
|
+
// Skip and log if violation was reported recently
|
|
105
|
+
if (
|
|
106
|
+
recentViolations.has(key) &&
|
|
107
|
+
now - recentViolations.get(key) < VIOLATION_TTL_MS
|
|
108
|
+
) {
|
|
109
|
+
console.log(`[CSP-Edge] Skipped duplicate alert for ${key}`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Record the current timestamp
|
|
114
|
+
recentViolations.set(key, now);
|
|
115
|
+
|
|
116
|
+
// Cleanup old entries (memory-safe for low volume)
|
|
117
|
+
for (const [k, t] of recentViolations.entries()) {
|
|
118
|
+
if (now - t > VIOLATION_TTL_MS) {
|
|
119
|
+
recentViolations.delete(k);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const topicUrl = "https://ntfy.neteng.pro/csp-alerts";
|
|
124
|
+
|
|
125
|
+
const message = [
|
|
126
|
+
`π¨ CSP Violation Detected`,
|
|
127
|
+
`Directive: ${violated}`,
|
|
128
|
+
`Blocked URI: ${blockedUri}`,
|
|
129
|
+
`Referrer: ${report.referrer || "N/A"}`,
|
|
130
|
+
`Source: ${report["source-file"] || "N/A"}`,
|
|
131
|
+
`Line: ${report["line-number"] || "N/A"}`,
|
|
132
|
+
].join("\n");
|
|
133
|
+
|
|
134
|
+
await fetch(topicUrl, {
|
|
135
|
+
method: "POST",
|
|
136
|
+
headers: {
|
|
137
|
+
"Content-Type": "text/plain",
|
|
138
|
+
"X-Title": "High-Risk CSP Violation",
|
|
139
|
+
"X-Priority": "5",
|
|
140
|
+
},
|
|
141
|
+
body: message,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Configuration block for the Edge Function.
|
|
147
|
+
* This sets the endpoint route to /api/csp-report
|
|
148
|
+
*/
|
|
149
|
+
export const config = {
|
|
150
|
+
path: "/api/csp-report",
|
|
151
|
+
};
|
package/netlify.toml
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
targetPort = 5173
|
|
12
12
|
port = 8888
|
|
13
13
|
|
|
14
|
-
[
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
[[edge_functions]]
|
|
15
|
+
path = "/api/csp-report"
|
|
16
|
+
function = "csp-report"
|
|
17
17
|
|
|
18
18
|
[[plugins]]
|
|
19
19
|
package = "netlify-plugin-submit-sitemap"
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"sideEffects": [
|
|
5
5
|
"./.netlify/shims.js"
|
|
6
6
|
],
|
|
7
|
-
"version": "1.
|
|
7
|
+
"version": "1.8.3",
|
|
8
8
|
"description": "Locking Down Networks, Unlocking Confidence | Security, Networking, Privacy β Network Pro Strategies",
|
|
9
9
|
"keywords": [
|
|
10
10
|
"advisory",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"nodemailer": "^7.0.3",
|
|
81
81
|
"posthog-js": "^1.249.0",
|
|
82
82
|
"semver": "^7.7.2",
|
|
83
|
-
"svelte": "5.33.
|
|
83
|
+
"svelte": "5.33.11"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
86
|
"@eslint/compat": "^1.2.9",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"browserslist": "^4.25.0",
|
|
98
98
|
"eslint": "^9.28.0",
|
|
99
99
|
"eslint-config-prettier": "^10.1.5",
|
|
100
|
-
"eslint-plugin-jsdoc": "^50.
|
|
100
|
+
"eslint-plugin-jsdoc": "^50.7.0",
|
|
101
101
|
"eslint-plugin-svelte": "^3.9.0",
|
|
102
102
|
"globals": "^16.2.0",
|
|
103
103
|
"jsdom": "^26.1.0",
|
package/src/hooks.server.js
CHANGED
|
@@ -21,14 +21,11 @@ export async function handle({ event, resolve }) {
|
|
|
21
21
|
const isProdEnvironment =
|
|
22
22
|
process.env.NODE_ENV === "production" || process.env.ENV_MODE === "prod";
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
24
|
+
console.log("[CSP Debug] NODE_ENV:", process.env.NODE_ENV);
|
|
25
|
+
console.log("[CSP Debug] ENV_MODE:", process.env.ENV_MODE);
|
|
27
26
|
|
|
28
27
|
// Determine report URI
|
|
29
|
-
const reportUri = isProdEnvironment
|
|
30
|
-
? "/.netlify/functions/cspReport"
|
|
31
|
-
: "/api/mock-csp";
|
|
28
|
+
const reportUri = isProdEnvironment ? "/api/csp-report" : "/api/mock-csp";
|
|
32
29
|
|
|
33
30
|
// Construct base policy
|
|
34
31
|
const cspDirectives = [
|
|
@@ -46,7 +46,7 @@ This file is part of Network Pro.
|
|
|
46
46
|
*/
|
|
47
47
|
const pageInfo = {
|
|
48
48
|
title: "FOSS Spotlight",
|
|
49
|
-
lastUpdated: "May
|
|
49
|
+
lastUpdated: "May 30, 2025",
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
/** @type {any} */
|
|
@@ -67,8 +67,7 @@ This file is part of Network Pro.
|
|
|
67
67
|
href="https://spdx.dev/learn/handling-license-info"
|
|
68
68
|
target={constants.targetBlank}>
|
|
69
69
|
SPDX License Identifier
|
|
70
|
-
</a>
|
|
71
|
-
: <code>CC-BY-4.0 OR GPL-3.0-or-later</code>
|
|
70
|
+
</a>: <code>CC-BY-4.0 OR GPL-3.0-or-later</code>
|
|
72
71
|
</span>
|
|
73
72
|
</section>
|
|
74
73
|
|
|
@@ -439,8 +439,8 @@ footer .container {
|
|
|
439
439
|
|
|
440
440
|
.center-nav {
|
|
441
441
|
padding: 5px;
|
|
442
|
-
font-size:
|
|
443
|
-
line-height: 1.
|
|
442
|
+
font-size: 1rem;
|
|
443
|
+
line-height: 1.5rem;
|
|
444
444
|
text-align: center;
|
|
445
445
|
}
|
|
446
446
|
|
|
@@ -497,3 +497,12 @@ footer .container {
|
|
|
497
497
|
font-family: inherit; /* Ensure it uses the same font-family as normal text */
|
|
498
498
|
font-style: normal; /* Remove italic for the description */
|
|
499
499
|
}
|
|
500
|
+
|
|
501
|
+
.goldseparator {
|
|
502
|
+
margin: 0 0.5rem;
|
|
503
|
+
color: #FFC627;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.gold {
|
|
507
|
+
color: #FFC627;
|
|
508
|
+
}
|
|
@@ -3,4 +3,4 @@ Copyright Β© 2025 Network Pro Strategies (Network Proβ’)
|
|
|
3
3
|
SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
|
|
4
4
|
This file is part of Network Pro.
|
|
5
5
|
========================================================================== */
|
|
6
|
-
html{-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{margin:.67em 0;font-size:2em}hr{box-sizing:content-box}pre{font-family:monospace;font-size:1em}a{background-color:#0000}abbr[title]{border-bottom:none;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted buttontext}fieldset{padding:.35em .75em .625em}legend{color:inherit;box-sizing:border-box;white-space:normal;max-width:100%;padding:0;display:table}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}details{display:block}summary{display:list-item}template{display:none}html{color:#222;scroll-behavior:smooth;font-size:1em;line-height:1.4}::-moz-selection{text-shadow:none;background:#191919}::selection{text-shadow:none;background:#191919}hr{border:0;border-top:1px solid #ccc;height:1px;margin:1em 0;padding:0;display:block;overflow:visible}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}body{color:#fafafa;background-color:#191919;margin:10px;font-family:Arial,Helvetica,sans-serif}a{text-decoration:none}a:link{color:#ffc627}a:hover,a:active{color:#ffc627;text-decoration:underline}a:focus{color:#191919;background-color:#ffc627}a:visited,a:visited:hover{color:#7f6227}.hidden,[hidden]{display:none!important}.visually-hidden{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.visually-hidden.focusable:active,.visually-hidden.focusable:focus{clip:auto;width:auto;height:auto;white-space:inherit;margin:0;position:static;overflow:visible}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:"";display:table}.clearfix:after{clear:both}@media print{*,:before,:after{color:#000!important;box-shadow:none!important;text-shadow:none!important;background:#fff!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^=\#]:after,a[href^=javascript\:]:after{content:""}pre{white-space:pre-wrap!important}pre,blockquote{page-break-inside:avoid;border:1px solid #999}tr,img{page-break-inside:avoid}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}.full-width-section{background-position:50%;background-size:cover;width:100%;max-width:1920px;margin:0 auto}.container{max-width:1200px;margin:0 auto;padding:0 12px}.readable{max-width:900px;margin:0 auto}header,footer{width:100%}header .container,footer .container{max-width:1200px;margin:0 auto;padding:20px 12px}.gh{border-collapse:collapse;border-spacing:0;margin:0 auto}.gh td,.gh th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.gh .gh-tcell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.gh,.gh col{width:auto!important}.gh-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.soc{border-collapse:collapse;border-spacing:0;margin:0 auto}.soc td,.soc th{border-collapse:collapse;word-break:normal;padding:8px;overflow:hidden}.soc .soc-fa{text-align:center;vertical-align:middle}@media screen and (width<=767px){.soc,.soc col{width:auto!important}.soc-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.foss{border-collapse:collapse;border-spacing:0}.foss td,.foss th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.foss .foss-cell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.foss,.foss col{width:auto!important}.foss-wrap{-webkit-overflow-scrolling:touch;overflow-x:auto}}.bnav{text-align:center;border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav td,.bnav th{text-align:center;vertical-align:middle;word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav .bnav-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav,.bnav col{width:auto!important}.bnav-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.bnav2{border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav2 td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav2 th{word-break:normal;border-style:none;padding:12px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.bnav2 .bnav2-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav2,.bnav2 col{width:auto!important}.bnav2-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.pgp{border-collapse:collapse;border-spacing:0;margin:0 auto}.pgp td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp th{word-break:normal;border:1px solid #000;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp .pgp-col1{text-align:right;vertical-align:middle;padding-right:1rem}.pgp .pgp-col2{text-align:left;vertical-align:middle;padding-left:1rem}@media screen and (width<=767px){.pgp,.pgp col{width:auto!important}.pgp-wrap{-webkit-overflow-scrolling:touch;margin:2rem 0 auto;overflow-x:auto}}.logo{margin-left:auto;margin-right:auto;display:block}.index-title1{text-align:center;font-style:italic;font-weight:700}.index-title2{letter-spacing:-.015em;text-align:center;font-variant:small-caps;font-size:1.25rem;line-height:1.625rem}.index1{letter-spacing:-.035em;text-align:center;font-style:italic;font-weight:700;line-height:2.125rem}.index2{letter-spacing:-.035em;text-align:center;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.index3{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem}.index4{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem;text-decoration:underline}.subhead{letter-spacing:-.035em;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.bold{font-weight:700}.emphasis{font-style:italic}.uline{text-decoration:underline}.bolditalic{font-style:italic;font-weight:700}.bquote{border-left:3px solid #9e9e9e;margin-left:30px;padding-left:10px;font-style:italic}.small-text{font-size:.75rem;line-height:1.125rem}.large-text-center{text-align:center;font-size:1.25rem;line-height:1.75rem}.prewrap{white-space:pre-wrap;display:block}.hr-styled{width:75%;margin:auto}.center-text{text-align:center}.copyright{text-align:center;font-size:.75rem;line-height:1.125rem}.visited{color:#7f6227}.center-nav{text-align:center;padding:5px;font-size
|
|
6
|
+
html{-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{margin:.67em 0;font-size:2em}hr{box-sizing:content-box}pre{font-family:monospace;font-size:1em}a{background-color:#0000}abbr[title]{border-bottom:none;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted buttontext}fieldset{padding:.35em .75em .625em}legend{color:inherit;box-sizing:border-box;white-space:normal;max-width:100%;padding:0;display:table}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}details{display:block}summary{display:list-item}template{display:none}html{color:#222;scroll-behavior:smooth;font-size:1em;line-height:1.4}::-moz-selection{text-shadow:none;background:#191919}::selection{text-shadow:none;background:#191919}hr{border:0;border-top:1px solid #ccc;height:1px;margin:1em 0;padding:0;display:block;overflow:visible}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}body{color:#fafafa;background-color:#191919;margin:10px;font-family:Arial,Helvetica,sans-serif}a{text-decoration:none}a:link{color:#ffc627}a:hover,a:active{color:#ffc627;text-decoration:underline}a:focus{color:#191919;background-color:#ffc627}a:visited,a:visited:hover{color:#7f6227}.hidden,[hidden]{display:none!important}.visually-hidden{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.visually-hidden.focusable:active,.visually-hidden.focusable:focus{clip:auto;width:auto;height:auto;white-space:inherit;margin:0;position:static;overflow:visible}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:"";display:table}.clearfix:after{clear:both}@media print{*,:before,:after{color:#000!important;box-shadow:none!important;text-shadow:none!important;background:#fff!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^=\#]:after,a[href^=javascript\:]:after{content:""}pre{white-space:pre-wrap!important}pre,blockquote{page-break-inside:avoid;border:1px solid #999}tr,img{page-break-inside:avoid}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}.full-width-section{background-position:50%;background-size:cover;width:100%;max-width:1920px;margin:0 auto}.container{max-width:1200px;margin:0 auto;padding:0 12px}.readable{max-width:900px;margin:0 auto}header,footer{width:100%}header .container,footer .container{max-width:1200px;margin:0 auto;padding:20px 12px}.gh{border-collapse:collapse;border-spacing:0;margin:0 auto}.gh td,.gh th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.gh .gh-tcell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.gh,.gh col{width:auto!important}.gh-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.soc{border-collapse:collapse;border-spacing:0;margin:0 auto}.soc td,.soc th{border-collapse:collapse;word-break:normal;padding:8px;overflow:hidden}.soc .soc-fa{text-align:center;vertical-align:middle}@media screen and (width<=767px){.soc,.soc col{width:auto!important}.soc-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.foss{border-collapse:collapse;border-spacing:0}.foss td,.foss th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.foss .foss-cell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.foss,.foss col{width:auto!important}.foss-wrap{-webkit-overflow-scrolling:touch;overflow-x:auto}}.bnav{text-align:center;border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav td,.bnav th{text-align:center;vertical-align:middle;word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav .bnav-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav,.bnav col{width:auto!important}.bnav-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.bnav2{border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav2 td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav2 th{word-break:normal;border-style:none;padding:12px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.bnav2 .bnav2-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav2,.bnav2 col{width:auto!important}.bnav2-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.pgp{border-collapse:collapse;border-spacing:0;margin:0 auto}.pgp td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp th{word-break:normal;border:1px solid #000;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp .pgp-col1{text-align:right;vertical-align:middle;padding-right:1rem}.pgp .pgp-col2{text-align:left;vertical-align:middle;padding-left:1rem}@media screen and (width<=767px){.pgp,.pgp col{width:auto!important}.pgp-wrap{-webkit-overflow-scrolling:touch;margin:2rem 0 auto;overflow-x:auto}}.logo{margin-left:auto;margin-right:auto;display:block}.index-title1{text-align:center;font-style:italic;font-weight:700}.index-title2{letter-spacing:-.015em;text-align:center;font-variant:small-caps;font-size:1.25rem;line-height:1.625rem}.index1{letter-spacing:-.035em;text-align:center;font-style:italic;font-weight:700;line-height:2.125rem}.index2{letter-spacing:-.035em;text-align:center;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.index3{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem}.index4{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem;text-decoration:underline}.subhead{letter-spacing:-.035em;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.bold{font-weight:700}.emphasis{font-style:italic}.uline{text-decoration:underline}.bolditalic{font-style:italic;font-weight:700}.bquote{border-left:3px solid #9e9e9e;margin-left:30px;padding-left:10px;font-style:italic}.small-text{font-size:.75rem;line-height:1.125rem}.large-text-center{text-align:center;font-size:1.25rem;line-height:1.75rem}.prewrap{white-space:pre-wrap;display:block}.hr-styled{width:75%;margin:auto}.center-text{text-align:center}.copyright{text-align:center;font-size:.75rem;line-height:1.125rem}.visited{color:#7f6227}.center-nav{text-align:center;padding:5px;font-size:1rem;line-height:1.5rem}.block{resize:none;background:0 0;border:none;border-radius:0;outline:none;width:100%;font-size:.75rem;line-height:1.125rem}.fingerprint{white-space:pre-line;font-weight:700;display:block}.pgp-image{width:150px;height:150px}.spacer{margin:2rem 0}.separator{margin:0 .5rem}.emoji{margin-right:8px}.headline{margin-bottom:4px;font-style:italic;font-weight:700;display:block}.label{font-family:inherit;font-weight:700}.description{font-family:inherit;font-style:normal;font-weight:400;display:inline}.goldseparator{color:#ffc627;margin:0 .5rem}.gold{color:#ffc627}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
src/routes/status/+page.server.js
|
|
3
|
+
|
|
4
|
+
Copyright Β© 2025 Network Pro Strategies (Network Proβ’)
|
|
5
|
+
SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
|
|
6
|
+
This file is part of Network Pro.
|
|
7
|
+
========================================================================== */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Server-side load function for /status route.
|
|
11
|
+
*
|
|
12
|
+
* Logs runtime environment variables for debugging purposes.
|
|
13
|
+
* This function executes on the server during SSR and is used to verify
|
|
14
|
+
* that environment variables (e.g., ENV_MODE and NODE_ENV) are properly
|
|
15
|
+
* injected and available during runtime in Netlify's SSR context.
|
|
16
|
+
*
|
|
17
|
+
* @module src/routes/status
|
|
18
|
+
* @author SunDevil311
|
|
19
|
+
* @updated 2025-05-31
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export const prerender = false;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @function load
|
|
26
|
+
* @returns {{ ok: boolean }} Response indicating successful execution
|
|
27
|
+
*/
|
|
28
|
+
export function load() {
|
|
29
|
+
console.log("[Status Debug] ENV_MODE:", process.env.ENV_MODE);
|
|
30
|
+
console.log("[Status Debug] NODE_ENV:", process.env.NODE_ENV);
|
|
31
|
+
return { ok: true };
|
|
32
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
tests/unit/csp-report.test.js
|
|
3
|
+
|
|
4
|
+
Copyright Β© 2025 Network Pro Strategies (Network Proβ’)
|
|
5
|
+
SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
|
|
6
|
+
This file is part of Network Pro.
|
|
7
|
+
========================================================================== */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Tests the edge-functions/csp-report.js CSP reporting endpoint
|
|
11
|
+
*
|
|
12
|
+
* @module tests/unit
|
|
13
|
+
* @author SunDevil311
|
|
14
|
+
* @updated 2025-05-31
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** @file Unit tests for edge-functions/csp-report.js using Vitest */
|
|
18
|
+
/** @typedef {import('vitest').TestContext} TestContext */
|
|
19
|
+
|
|
20
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
21
|
+
import handler from "../../netlify/edge-functions/csp-report.js";
|
|
22
|
+
|
|
23
|
+
// π§ͺ Mock fetch used by sendToNtfy inside the Edge Function
|
|
24
|
+
global.fetch = vi.fn(() =>
|
|
25
|
+
Promise.resolve({ ok: true, status: 200, text: () => "OK" }),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
describe("csp-report.js", () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vi.clearAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should handle a valid CSP report", async () => {
|
|
34
|
+
const req = new Request("http://localhost/api/csp-report", {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "Content-Type": "application/json" },
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
"csp-report": {
|
|
39
|
+
"document-uri": "https://example.com",
|
|
40
|
+
"violated-directive": "script-src",
|
|
41
|
+
"blocked-uri": "https://malicious.site",
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const res = await handler(req, {});
|
|
47
|
+
expect(res.status).toBe(204);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should reject non-POST requests", async () => {
|
|
51
|
+
const req = new Request("http://localhost/api/csp-report", {
|
|
52
|
+
method: "GET",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const res = await handler(req, {});
|
|
56
|
+
const text = await res.text();
|
|
57
|
+
expect(res.status).toBe(405);
|
|
58
|
+
expect(text).toContain("Method Not Allowed");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should handle malformed JSON", async () => {
|
|
62
|
+
const badJson = "{ invalid json }";
|
|
63
|
+
const req = new Request("http://localhost/api/csp-report", {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "Content-Type": "application/json" },
|
|
66
|
+
body: badJson,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const res = await handler(req, {});
|
|
70
|
+
expect(res.status).toBe(204); // The current handler swallows errors silently
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should handle missing body", async () => {
|
|
74
|
+
const req = new Request("http://localhost/api/csp-report", {
|
|
75
|
+
method: "POST",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const res = await handler(req, {});
|
|
79
|
+
expect(res.status).toBe(204); // No body is also treated silently
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/* ==========================================================================
|
|
2
|
-
netlify-functions/cspReport.js
|
|
3
|
-
|
|
4
|
-
Copyright Β© 2025 Network Pro Strategies (Network Proβ’)
|
|
5
|
-
SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
|
|
6
|
-
This file is part of Network Pro.
|
|
7
|
-
========================================================================== */
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @file cspReport.js
|
|
11
|
-
* @description Sets up a CSP reporting endpoint with email notifications, to be deployed as a Netlify function.
|
|
12
|
-
* @module netlify-functions
|
|
13
|
-
* @author SunDevil311
|
|
14
|
-
* @updated 2025-05-29
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import nodemailer from "nodemailer";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Netlify Function: CSP violation report handler
|
|
21
|
-
*
|
|
22
|
-
* @param {import('@netlify/functions').HandlerEvent} event - Incoming Netlify request
|
|
23
|
-
* @returns {Promise<import('@netlify/functions').HandlerResponse>} - Netlify-compatible HTTP response
|
|
24
|
-
*/
|
|
25
|
-
export async function handler(event) {
|
|
26
|
-
try {
|
|
27
|
-
if (event.httpMethod !== "POST") {
|
|
28
|
-
return {
|
|
29
|
-
statusCode: 405,
|
|
30
|
-
body: "Method Not Allowed",
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (!event.body) {
|
|
35
|
-
return {
|
|
36
|
-
statusCode: 400,
|
|
37
|
-
body: "No body provided",
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** @type {Record<string, any>} */
|
|
42
|
-
const report = JSON.parse(event.body);
|
|
43
|
-
const violation = report["csp-report"] || report;
|
|
44
|
-
|
|
45
|
-
const shouldSendEmail =
|
|
46
|
-
process.env.MAIL_ENABLED !== "false" && process.env.NODE_ENV !== "test";
|
|
47
|
-
|
|
48
|
-
if (shouldSendEmail) {
|
|
49
|
-
const transporter = nodemailer.createTransport({
|
|
50
|
-
host: process.env.SMTP_HOST,
|
|
51
|
-
port: 465,
|
|
52
|
-
secure: true,
|
|
53
|
-
auth: {
|
|
54
|
-
user: process.env.SMTP_USER,
|
|
55
|
-
pass: process.env.SMTP_PASS,
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
await transporter.sendMail({
|
|
60
|
-
from: `"CSP Reporter" <${process.env.SMTP_USER}>`,
|
|
61
|
-
to: process.env.NOTIFY_EMAIL,
|
|
62
|
-
subject: "π¨ CSP Violation Detected",
|
|
63
|
-
text: JSON.stringify(violation, null, 2),
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
statusCode: 204,
|
|
69
|
-
};
|
|
70
|
-
} catch (error) {
|
|
71
|
-
return {
|
|
72
|
-
statusCode: 400,
|
|
73
|
-
body: `Invalid JSON: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/* ==========================================================================
|
|
2
|
-
tests/unit/cspReport.test.js
|
|
3
|
-
|
|
4
|
-
Copyright Β© 2025 Network Pro Strategies (Network Proβ’)
|
|
5
|
-
SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
|
|
6
|
-
This file is part of Network Pro.
|
|
7
|
-
========================================================================== */
|
|
8
|
-
|
|
9
|
-
/** @file Unit tests for netlify-functions/cspReport.js using Vitest */
|
|
10
|
-
/** @typedef {import('vitest').TestContext} TestContext */
|
|
11
|
-
|
|
12
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
13
|
-
import { handler } from "../../netlify-functions/cspReport.js";
|
|
14
|
-
|
|
15
|
-
// π§ͺ Force test mode
|
|
16
|
-
process.env.NODE_ENV = "test";
|
|
17
|
-
process.env.MAIL_ENABLED = "true"; // Still ignored due to NODE_ENV === test
|
|
18
|
-
|
|
19
|
-
// π§ͺ Mock nodemailer to prevent real email sending
|
|
20
|
-
vi.mock("nodemailer", async () => {
|
|
21
|
-
return {
|
|
22
|
-
default: {
|
|
23
|
-
createTransport: () => ({
|
|
24
|
-
sendMail: vi.fn().mockResolvedValue({}),
|
|
25
|
-
}),
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe("cspReport.js", () => {
|
|
31
|
-
beforeEach(() => {
|
|
32
|
-
vi.clearAllMocks(); // reset mocks if needed
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("should handle valid CSP report", async () => {
|
|
36
|
-
/** @type {import('netlify/functions').HandlerEvent} */
|
|
37
|
-
const event = {
|
|
38
|
-
httpMethod: "POST",
|
|
39
|
-
body: JSON.stringify({
|
|
40
|
-
"csp-report": {
|
|
41
|
-
"document-uri": "https://example.com",
|
|
42
|
-
"violated-directive": "script-src",
|
|
43
|
-
},
|
|
44
|
-
}),
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const response = await handler(event);
|
|
48
|
-
expect(response.statusCode).toBe(204);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("should reject GET requests", async () => {
|
|
52
|
-
/** @type {import('netlify/functions').HandlerEvent} */
|
|
53
|
-
const event = { httpMethod: "GET" };
|
|
54
|
-
const response = await handler(event);
|
|
55
|
-
expect(response.statusCode).toBe(405);
|
|
56
|
-
expect(response.body).toContain("Method Not Allowed");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("should handle malformed JSON", async () => {
|
|
60
|
-
/** @type {import('netlify/functions').HandlerEvent} */
|
|
61
|
-
const event = {
|
|
62
|
-
httpMethod: "POST",
|
|
63
|
-
body: "{ bad json }",
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const response = await handler(event);
|
|
67
|
-
expect(response.statusCode).toBe(400);
|
|
68
|
-
expect(response.body).toContain("Invalid JSON");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("should handle missing body", async () => {
|
|
72
|
-
/** @type {import('netlify/functions').HandlerEvent} */
|
|
73
|
-
const event = {
|
|
74
|
-
httpMethod: "POST",
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const response = await handler(event);
|
|
78
|
-
expect(response.statusCode).toBe(400);
|
|
79
|
-
expect(response.body).toContain("No body provided");
|
|
80
|
-
});
|
|
81
|
-
});
|