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.
- package/.cspell.json +19 -51
- package/.github/ISSUE_TEMPLATE/config.yml +1 -1
- package/.github/PULL_REQUEST_TEMPLATE.md +2 -2
- package/.github/workflows/stale.yaml +20 -0
- package/.github/workflows/webapp-scanner.yml +31 -19
- package/.github/workflows/welcome.yaml +9 -55
- package/.husky/pre-commit +35 -0
- package/.vscode/extensions.json +7 -0
- package/.vscode/launch.json +20 -0
- package/.vscode/settings.json +32 -0
- package/.vscode/tasks.json +24 -0
- package/CHANGELOG.md +7 -3
- package/CODE_OF_CONDUCT.md +4 -1
- package/CONTRIBUTING.md +2 -2
- package/README.md +5 -0
- package/api-extractor.json +30 -30
- package/biome.json +6 -32
- package/build/index.d.ts +0 -147
- package/build/index.js +111 -2633
- package/package.json +69 -102
- package/scripts/build.ts +68 -78
- package/scripts/test.ts +55 -0
- package/src/__tests__/spider.test.ts +44 -0
- package/src/commands/spider.ts +61 -126
- package/src/index.ts +23 -26
- package/src/spider/index.ts +345 -0
- package/src/spider/types/index.ts +21 -0
- package/src/spider/types/schema.ts +54 -0
- package/src/utils/index.ts +199 -3
- package/tsconfig.json +19 -18
- package/.github/assets/header.png +0 -0
- package/.github/dependabot.yml +0 -11
- package/.github/workflows/pr.yaml +0 -64
- package/.nsprc +0 -3
- package/build/bin.js +0 -2679
- package/build/xhr-sync-worker.js +0 -59
- package/docs/CNAME +0 -1
- package/docs/disclaimer.md +0 -68
- package/docs/headers/details.md +0 -114
- package/docs/headers/index.md +0 -73
- package/docs/index.md +0 -82
- package/docs/ports/index.md +0 -86
- package/docs/scoring.md +0 -91
- package/docs/spider/index.md +0 -61
- package/docs/sql-injection/details.md +0 -109
- package/docs/sql-injection/index.md +0 -73
- package/docs/xss/details.md +0 -92
- package/docs/xss/index.md +0 -73
- package/scripts/extras/document-shim.js +0 -4
- package/src/bin.ts +0 -29
- package/src/commands/header.ts +0 -150
- package/src/commands/ports.ts +0 -175
- package/src/commands/sqli.ts +0 -150
- package/src/commands/xss.ts +0 -149
- package/src/modules/headers/headers.ts +0 -161
- package/src/modules/headers/index.ts +0 -179
- package/src/modules/ports/index.ts +0 -311
- package/src/modules/spider/index.ts +0 -178
- package/src/modules/sqli/index.ts +0 -486
- package/src/modules/sqli/payloads.json +0 -156
- package/src/modules/xss/index.ts +0 -401
- package/src/modules/xss/payloads.json +0 -2692
- package/src/utils/types.ts +0 -7
package/src/modules/xss/index.ts
DELETED
@@ -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
|
-
}
|