@networkpro/web 1.7.9 → 1.9.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 +238 -75
- package/cspell.json +3 -0
- package/netlify/edge-functions/csp-report.js +151 -0
- package/netlify.toml +3 -3
- package/package.json +6 -6
- package/scripts/generateTest.js +61 -0
- package/src/hooks.server.js +1 -1
- package/src/lib/components/foss/FossItemContent.svelte +28 -30
- package/src/lib/components/layout/HeaderDefault.svelte +1 -1
- package/src/lib/components/layout/HeaderHome.svelte +1 -1
- package/src/lib/pages/AboutContent.svelte +15 -5
- package/src/lib/pages/FossContent.svelte +1 -2
- package/src/lib/pages/LicenseContent.svelte +2 -2
- package/src/lib/styles/css/default.css +11 -2
- package/src/lib/styles/global.min.css +1 -1
- package/src/lib/types/fossTypes.js +23 -0
- package/src/lib/utils/purify.js +74 -0
- package/src/routes/status/+page.server.js +32 -0
- package/tests/internal/auditCoverage.test.js +99 -0
- package/tests/unit/csp-report.test.js +81 -0
- package/tests/unit/lib/utils/purify.test.js +50 -0
- package/vitest.config.client.js +5 -1
- package/vitest.config.server.js +1 -0
- package/netlify-functions/cspReport.js +0 -76
- package/tests/unit/auditScripts.test.js +0 -43
- package/tests/unit/cspReport.test.js +0 -81
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
tests/unit/lib/utils/purify.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
|
+
* @file purify.test.js
|
|
11
|
+
* @description Unit test for src/lib/utils/purify.js
|
|
12
|
+
* @module tests/unit/lib/util
|
|
13
|
+
* @author SunDevil311
|
|
14
|
+
* @updated 2025-06-01
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, expect, it } from "vitest";
|
|
18
|
+
import { sanitizeHtml } from "../../../../src/lib/utils/purify.js";
|
|
19
|
+
|
|
20
|
+
describe("sanitizeHtml", () => {
|
|
21
|
+
it("removes dangerous tags like <script>", async () => {
|
|
22
|
+
const dirty = `<div>Hello <script>alert("XSS")</script> world!</div>`;
|
|
23
|
+
const clean = await sanitizeHtml(dirty);
|
|
24
|
+
expect(clean).toBe("<div>Hello world!</div>");
|
|
25
|
+
}); // timeout in ms
|
|
26
|
+
|
|
27
|
+
it("preserves safe markup like <strong>", async () => {
|
|
28
|
+
const dirty = `<p>This is <strong>important</strong>.</p>`;
|
|
29
|
+
const clean = await sanitizeHtml(dirty);
|
|
30
|
+
expect(clean).toBe("<p>This is <strong>important</strong>.</p>");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("removes dangerous attributes like onerror", async () => {
|
|
34
|
+
const dirty = `<img src="x" onerror="alert(1)">`;
|
|
35
|
+
const clean = await sanitizeHtml(dirty);
|
|
36
|
+
expect(clean).toBe('<img src="x">');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("keeps valid external links", async () => {
|
|
40
|
+
const dirty = `<a href="https://example.com">Click</a>`;
|
|
41
|
+
const clean = await sanitizeHtml(dirty);
|
|
42
|
+
expect(clean).toBe('<a href="https://example.com">Click</a>');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("blocks javascript: URLs", async () => {
|
|
46
|
+
const dirty = `<a href="javascript:alert('XSS')">bad</a>`;
|
|
47
|
+
const clean = await sanitizeHtml(dirty);
|
|
48
|
+
expect(clean).toBe("<a>bad</a>");
|
|
49
|
+
});
|
|
50
|
+
});
|
package/vitest.config.client.js
CHANGED
|
@@ -26,10 +26,14 @@ export default defineConfig({
|
|
|
26
26
|
name: "client",
|
|
27
27
|
environment: "jsdom",
|
|
28
28
|
clearMocks: true,
|
|
29
|
-
include: [
|
|
29
|
+
include: [
|
|
30
|
+
"tests/unit/**/*.test.{js,mjs,svelte}",
|
|
31
|
+
"tests/internal/**/*.test.{js,mjs,svelte}",
|
|
32
|
+
],
|
|
30
33
|
exclude: [],
|
|
31
34
|
setupFiles: ["./vitest-setup-client.js"],
|
|
32
35
|
reporters: ["default", "json"],
|
|
36
|
+
testTimeout: 10000,
|
|
33
37
|
outputFile: {
|
|
34
38
|
json: "./reports/client/results.json",
|
|
35
39
|
},
|
package/vitest.config.server.js
CHANGED
|
@@ -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,43 +0,0 @@
|
|
|
1
|
-
/* ==========================================================================
|
|
2
|
-
tests/unit/auditScripts.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
|
-
* Unit test for scripts/auditScripts.js
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import fs from "fs";
|
|
14
|
-
import path from "path";
|
|
15
|
-
import { describe, expect, it } from "vitest";
|
|
16
|
-
|
|
17
|
-
describe("auditScripts.js", () => {
|
|
18
|
-
it("should identify untested scripts correctly", () => {
|
|
19
|
-
const scriptsDir = path.resolve("./scripts");
|
|
20
|
-
const testsDir = path.resolve("./tests");
|
|
21
|
-
|
|
22
|
-
const allowList = new Set(["checkNode.js", "auditScripts.js"]);
|
|
23
|
-
|
|
24
|
-
const scriptFiles = fs
|
|
25
|
-
.readdirSync(scriptsDir)
|
|
26
|
-
.filter((file) => file.endsWith(".js"));
|
|
27
|
-
|
|
28
|
-
const testFiles = fs
|
|
29
|
-
.readdirSync(testsDir)
|
|
30
|
-
.filter((file) => file.endsWith(".test.js") || file.endsWith(".spec.js"));
|
|
31
|
-
|
|
32
|
-
const testedModules = new Set(
|
|
33
|
-
testFiles.map((f) => f.replace(/\.test\.js$|\.spec\.js$/, "")),
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
const untested = scriptFiles.filter((file) => {
|
|
37
|
-
const base = file.replace(/\.js$/, "");
|
|
38
|
-
return !allowList.has(file) && !testedModules.has(base);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
expect(untested).not.toContain("auditScripts.js");
|
|
42
|
-
});
|
|
43
|
-
});
|
|
@@ -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
|
-
});
|