sentinel-scanner 2.4.1 → 2.5.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.
Files changed (63) hide show
  1. package/.cspell.json +19 -51
  2. package/.github/ISSUE_TEMPLATE/config.yml +1 -1
  3. package/.github/PULL_REQUEST_TEMPLATE.md +2 -2
  4. package/.github/workflows/stale.yaml +20 -0
  5. package/.github/workflows/webapp-scanner.yml +31 -19
  6. package/.github/workflows/welcome.yaml +9 -55
  7. package/.husky/pre-commit +35 -0
  8. package/.vscode/extensions.json +7 -0
  9. package/.vscode/launch.json +20 -0
  10. package/.vscode/settings.json +32 -0
  11. package/.vscode/tasks.json +24 -0
  12. package/CHANGELOG.md +7 -3
  13. package/CODE_OF_CONDUCT.md +4 -1
  14. package/CONTRIBUTING.md +2 -2
  15. package/README.md +5 -0
  16. package/api-extractor.json +30 -30
  17. package/biome.json +6 -32
  18. package/build/index.d.ts +0 -147
  19. package/build/index.js +111 -2633
  20. package/package.json +69 -102
  21. package/scripts/build.ts +68 -78
  22. package/scripts/test.ts +55 -0
  23. package/src/__tests__/spider.test.ts +44 -0
  24. package/src/commands/spider.ts +61 -126
  25. package/src/index.ts +23 -26
  26. package/src/spider/index.ts +345 -0
  27. package/src/spider/types/index.ts +21 -0
  28. package/src/spider/types/schema.ts +54 -0
  29. package/src/utils/index.ts +199 -3
  30. package/tsconfig.json +19 -18
  31. package/.github/assets/header.png +0 -0
  32. package/.github/dependabot.yml +0 -11
  33. package/.github/workflows/pr.yaml +0 -64
  34. package/.nsprc +0 -3
  35. package/build/bin.js +0 -2679
  36. package/build/xhr-sync-worker.js +0 -59
  37. package/docs/CNAME +0 -1
  38. package/docs/disclaimer.md +0 -68
  39. package/docs/headers/details.md +0 -114
  40. package/docs/headers/index.md +0 -73
  41. package/docs/index.md +0 -82
  42. package/docs/ports/index.md +0 -86
  43. package/docs/scoring.md +0 -91
  44. package/docs/spider/index.md +0 -61
  45. package/docs/sql-injection/details.md +0 -109
  46. package/docs/sql-injection/index.md +0 -73
  47. package/docs/xss/details.md +0 -92
  48. package/docs/xss/index.md +0 -73
  49. package/scripts/extras/document-shim.js +0 -4
  50. package/src/bin.ts +0 -29
  51. package/src/commands/header.ts +0 -150
  52. package/src/commands/ports.ts +0 -175
  53. package/src/commands/sqli.ts +0 -150
  54. package/src/commands/xss.ts +0 -149
  55. package/src/modules/headers/headers.ts +0 -161
  56. package/src/modules/headers/index.ts +0 -179
  57. package/src/modules/ports/index.ts +0 -311
  58. package/src/modules/spider/index.ts +0 -178
  59. package/src/modules/sqli/index.ts +0 -486
  60. package/src/modules/sqli/payloads.json +0 -156
  61. package/src/modules/xss/index.ts +0 -401
  62. package/src/modules/xss/payloads.json +0 -2692
  63. package/src/utils/types.ts +0 -7
@@ -1,401 +0,0 @@
1
- import { cpus } from "node:os";
2
- import puppeteer, { type ElementHandle, type Page } from "puppeteer";
3
- import { createLogger, generateCVSS } from "../../utils/index.js";
4
- import type { Vulnerability } from "../../utils/types.js";
5
- import payloads from "./payloads.json";
6
-
7
- export type XSSConstructorOpts = {
8
- spiderResults: Array<string>;
9
- retries?: number;
10
- timeout?: number;
11
- concurrency?: number;
12
- };
13
-
14
- export default class XSSScanner {
15
- private logger = createLogger("XSSScanner");
16
- private spiderResults: Array<string> = [];
17
- private retries = 3;
18
- private timeout = 10000;
19
- private vulnerabilities: Array<Vulnerability> = [];
20
- private concurrency = 20;
21
-
22
- constructor(opts: XSSConstructorOpts) {
23
- try {
24
- this.validateSpiderResults(opts.spiderResults);
25
-
26
- this.spiderResults = opts.spiderResults;
27
-
28
- if (opts.retries) {
29
- this.retries = opts.retries;
30
- }
31
-
32
- if (opts.timeout) {
33
- this.timeout = opts.timeout;
34
- }
35
-
36
- if (opts.concurrency) {
37
- if (opts.concurrency < 1) {
38
- throw new Error("Concurrency must be greater than 0");
39
- }
40
-
41
- if (opts.concurrency > 100) {
42
- throw new Error("Concurrency must be less than or equal to 100");
43
- }
44
-
45
- this.concurrency = opts.concurrency;
46
- }
47
-
48
- this.logger.info(
49
- `XSSScanner initialized with ${this.spiderResults.length} URLs, ${this.retries} retries, ${this.timeout}ms timeout, and ${this.concurrency} workers`,
50
- );
51
- } catch (error) {
52
- throw new Error(`Error initializing XSSScanner: ${error}`);
53
- }
54
- }
55
-
56
- private validateSpiderResults(spiderResults: Array<string>) {
57
- if (!spiderResults) {
58
- throw new Error("Missing required spiderResults parameter");
59
- }
60
-
61
- if (!Array.isArray(spiderResults)) {
62
- throw new Error("spiderResults must be an array");
63
- }
64
-
65
- if (Array.isArray(spiderResults) && spiderResults.length === 0) {
66
- throw new Error("spiderResults array cannot be empty");
67
- }
68
-
69
- spiderResults.some((url) => {
70
- if (typeof url !== "string") {
71
- throw new Error("spiderResults array must contain only strings");
72
- }
73
- });
74
- }
75
-
76
- private async fillFormIfExists(page: Page, url: string) {
77
- try {
78
- const formsExist = await page.evaluate(() => {
79
- return document.forms.length > 0;
80
- });
81
-
82
- if (!formsExist) {
83
- this.logger.info(`No forms found on ${url}`);
84
- return;
85
- }
86
-
87
- this.logger.info(`Found forms on ${url}`);
88
-
89
- for (const payload of payloads) {
90
- if (typeof payload !== "string" || payload.length === 0) {
91
- this.logger.warn(`Skipping malformed payload: "${payload}"`);
92
- continue;
93
- }
94
-
95
- this.logger.info(`Testing payload "${payload}" on ${url}`);
96
-
97
- const forms = await page.$$("form");
98
-
99
- if (!forms || forms.length === 0) {
100
- this.logger.info(`No forms found on ${url}`);
101
- return;
102
- }
103
-
104
- for (const form of forms) {
105
- const inputs = await form.$$("input");
106
- for (const input of inputs) {
107
- const type = await input.evaluate((node) => node.type);
108
- switch (type) {
109
- case "text":
110
- case "email":
111
- case "password":
112
- case "number":
113
- case "tel":
114
- case "url":
115
- case "search":
116
- case "date":
117
- case "time":
118
- case "month":
119
- case "week":
120
- case "datetime-local":
121
- await input.type(payload);
122
- break;
123
- case "checkbox":
124
- case "radio":
125
- await input.evaluate((node) => {
126
- node.checked = true;
127
- });
128
- break;
129
- }
130
- }
131
-
132
- const textAreas = await form.$$("textarea");
133
- for (const textArea of textAreas) {
134
- await textArea.type(payload);
135
- }
136
-
137
- const selects = await form.$$("select");
138
- for (const select of selects) {
139
- select.evaluate((node) => {
140
- if (node.options.length > 0) {
141
- node.selectedIndex = 0;
142
- }
143
- });
144
- }
145
-
146
- await form.evaluate((node) => node.submit());
147
-
148
- page.removeAllListeners("dialog"); // Add this line
149
- page.removeAllListeners("console"); // Add this line
150
-
151
- page.on("dialog", async (dialog) => {
152
- this.logger.info(
153
- `XSS Potentially Found - Dialog message: ${dialog.message()}`,
154
- );
155
-
156
- const dialogMessage = dialog.message();
157
-
158
- await dialog.dismiss();
159
-
160
- const isVulnerable = payload.includes(dialogMessage);
161
-
162
- if (isVulnerable) {
163
- this.logger.info(
164
- `XSS Potentially Found - Dialog message: ${dialogMessage}`,
165
- );
166
- const description =
167
- "Payload was reflected on dialog message. People can inject malicious code into the website.";
168
-
169
- const { baseScore: score, severity: level } = generateCVSS(
170
- "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:L",
171
- );
172
-
173
- const sameVulnerability = this.vulnerabilities.find(
174
- (vulnerability) => vulnerability.description === description,
175
- );
176
-
177
- if (!sameVulnerability) {
178
- this.vulnerabilities.push({
179
- type: level,
180
- url,
181
- description,
182
- severity: score,
183
- payloads: [payload],
184
- });
185
- } else {
186
- const payloadToBeAdded = sameVulnerability.payloads
187
- ? [...sameVulnerability.payloads, payload]
188
- : [payload];
189
-
190
- sameVulnerability.payloads = Array.from(
191
- new Set(payloadToBeAdded),
192
- );
193
- }
194
- }
195
- });
196
-
197
- page.on("console", (msg) => {
198
- this.logger.info(
199
- `XSS Potentially Found - Console message: ${msg.text()}`,
200
- );
201
- const consoleMessage = msg.text();
202
-
203
- const isVulnerable = payload.includes(consoleMessage);
204
-
205
- if (isVulnerable) {
206
- this.logger.info(
207
- `XSS Potentially Found - Console message: ${consoleMessage}`,
208
- );
209
- const description =
210
- "Payload was reflected on the console. People can inject malicious code into the website.";
211
-
212
- const { baseScore: score, severity: level } = generateCVSS(
213
- "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:L",
214
- );
215
-
216
- const sameVulnerability = this.vulnerabilities.find(
217
- (vulnerability) => vulnerability.description === description,
218
- );
219
-
220
- if (!sameVulnerability) {
221
- this.vulnerabilities.push({
222
- type: level,
223
- url,
224
- description,
225
- severity: score,
226
- payloads: [payload],
227
- });
228
- } else {
229
- const payloadToBeAdded = sameVulnerability.payloads
230
- ? [...sameVulnerability.payloads, payload]
231
- : [payload];
232
-
233
- sameVulnerability.payloads = Array.from(
234
- new Set(payloadToBeAdded),
235
- );
236
- }
237
- }
238
- });
239
-
240
- // Wait for navigation to complete
241
- await page.waitForNavigation();
242
-
243
- // Check The Website Content To See If The Payload Was Reflected
244
- const content = await page.content();
245
-
246
- const isVulnerable = content.includes(payload);
247
-
248
- // Push The Vulnerability To The Array
249
- if (isVulnerable) {
250
- this.logger.info(
251
- `XSS Potentially Found - Payload "${payload}" was reflected on ${url} content as it is. People can inject malicious code into the website.`,
252
- );
253
- const description =
254
- "Payload was reflected on content as it is. People can inject malicious code into the website.";
255
-
256
- const { baseScore: score, severity: level } = generateCVSS(
257
- "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:L",
258
- );
259
-
260
- const sameVulnerability = this.vulnerabilities.find(
261
- (vulnerability) => vulnerability.description === description,
262
- );
263
-
264
- if (!sameVulnerability) {
265
- this.vulnerabilities.push({
266
- type: level,
267
- url,
268
- description,
269
- severity: score,
270
- payloads: [payload],
271
- });
272
- } else {
273
- const payloadToBeAdded = sameVulnerability.payloads
274
- ? [...sameVulnerability.payloads, payload]
275
- : [payload];
276
-
277
- sameVulnerability.payloads = Array.from(
278
- new Set(payloadToBeAdded),
279
- );
280
- }
281
- }
282
- }
283
-
284
- this.logger.info(`Payload "${payload}" submitted on ${url}`);
285
-
286
- await this.sleep(1000);
287
- }
288
- } catch (error) {
289
- this.logger.error(`Error in fillFormIfExists for ${url}: ${error}`);
290
- }
291
- }
292
-
293
- async scan() {
294
- try {
295
- this.logger.debug(
296
- `Starting XSS Scan with ${this.concurrency} workers checking ${this.spiderResults.length} URLs with ${payloads.length} payloads`,
297
- );
298
- const browser = await puppeteer.launch({
299
- headless: true,
300
- args: [
301
- "--no-sandbox",
302
- "--disable-setuid-sandbox",
303
- "--disable-web-security",
304
- "--disable-features=IsolateOrigins,site-per-process",
305
- ],
306
- });
307
-
308
- let [page] = await browser.pages();
309
-
310
- if (!page) {
311
- page = await browser.newPage();
312
- }
313
-
314
- const chunkSize = this.concurrency;
315
- const chunks = this.chunkArray(this.spiderResults, chunkSize);
316
-
317
- for (const chunk of chunks) {
318
- const batchPromises = chunk.map(async (url) => {
319
- try {
320
- if (!page) {
321
- page = await browser.newPage();
322
- }
323
-
324
- await this.retryPageNavigation(page, url);
325
- await this.retryFormFilling(page, url);
326
- } catch (error) {
327
- this.logger.error(`Error processing URL: ${url} - ${error}`);
328
- }
329
- });
330
-
331
- await Promise.allSettled(batchPromises);
332
- await this.sleep(500);
333
- }
334
-
335
- await browser.close();
336
-
337
- this.logger.info(
338
- `XSS Scan Complete - Found ${this.vulnerabilities.length} vulnerabilities`,
339
- );
340
-
341
- return this.vulnerabilities;
342
- } catch (error) {
343
- this.logger.error(`Scan error: ${error}`);
344
- }
345
- }
346
-
347
- private async retryPageNavigation(page: Page, url: string) {
348
- let attempt = 0;
349
- while (attempt < this.retries) {
350
- try {
351
- attempt++;
352
- this.logger.info(`Navigating to ${url} (Attempt ${attempt})`);
353
- await page.goto(url);
354
- return;
355
- } catch (error) {
356
- if (attempt >= this.retries) {
357
- this.logger.error(
358
- `Failed to navigate to ${url} after ${this.retries} retries`,
359
- );
360
- throw error;
361
- }
362
- const delay = 2 ** attempt * 1000;
363
- this.logger.warn(`Retrying navigation to ${url} in ${delay}ms...`);
364
- await this.sleep(delay);
365
- }
366
- }
367
- }
368
-
369
- private async retryFormFilling(page: Page, url: string) {
370
- let attempt = 0;
371
- while (attempt < this.retries) {
372
- try {
373
- attempt++;
374
- await this.fillFormIfExists(page, url);
375
- return;
376
- } catch (error) {
377
- if (attempt >= this.retries) {
378
- this.logger.error(
379
- `Failed to fill form on ${url} after ${this.retries} retries`,
380
- );
381
- throw error;
382
- }
383
- const delay = 2 ** attempt * 1000;
384
- this.logger.warn(`Retrying form fill on ${url} in ${delay}ms...`);
385
- await this.sleep(delay);
386
- }
387
- }
388
- }
389
-
390
- private chunkArray<T>(array: T[], size: number): T[][] {
391
- const result: T[][] = [];
392
- for (let i = 0; i < array.length; i += size) {
393
- result.push(array.slice(i, i + size));
394
- }
395
- return result;
396
- }
397
-
398
- private sleep(ms: number): Promise<void> {
399
- return new Promise((resolve) => setTimeout(resolve, ms));
400
- }
401
- }