sentinel-scanner 1.0.1 → 1.1.0-alpha.1

Sign up to get free protection for your applications and to get access to all the features.
package/src/index.ts CHANGED
@@ -1,27 +1,86 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { Command } from "commander";
4
- // @ts-ignore: For TypeScript compatibility when importing JSON files
5
- import packageData from "../package.json";
3
+ import yargs from "yargs";
4
+ import { hideBin } from "yargs/helpers";
5
+ import { SpiderScanner } from "./modules";
6
6
 
7
- // Create a new Command object
8
- const program = new Command();
7
+ const commandHandler = yargs(hideBin(process.argv));
9
8
 
10
- // Set version, name, and description from the package.json
11
- program
12
- .version(packageData.version)
13
- .name(packageData.name)
14
- .description(packageData.description);
9
+ /**
10
+ * Command to scan for XSS vulnerabilities
11
+ *
12
+ * @param {string} url - URL to scan
13
+ * @param {string} wordlist - Path to wordlist file
14
+ * @returns {void}
15
+ *
16
+ * @example
17
+ * npx sentinel-scanner xss --url https://example.com
18
+ */
19
+ commandHandler.command(
20
+ "xss",
21
+ "Scan for XSS vulnerabilities",
22
+ {
23
+ url: {
24
+ describe: "URL to scan",
25
+ demandOption: true,
26
+ type: "string",
27
+ coerce: (value) => {
28
+ try {
29
+ new URL(value);
30
+ return value;
31
+ } catch (err) {
32
+ throw new Error("Invalid URL format");
33
+ }
34
+ },
35
+ },
36
+ wordlist: {
37
+ describe: "Path to wordlist file",
38
+ type: "string",
39
+ },
40
+ },
41
+ (argv) => {
42
+ console.log("Scanning for XSS vulnerabilities...");
43
+ console.log(`URL: ${argv.url}`);
44
+ console.log(`Wordlist: ${argv.wordlist || "Default"}`);
45
+ },
46
+ );
15
47
 
16
- // Add a help command explicitly if needed
17
- program.helpOption("-h, --help", "Display help for command");
48
+ // Command to Spider a website
49
+ commandHandler.command(
50
+ "spider",
51
+ "Scan a website for vulnerabilities",
52
+ {
53
+ url: {
54
+ describe: "URL to scan",
55
+ demandOption: true,
56
+ type: "string",
57
+ coerce: (value) => {
58
+ try {
59
+ new URL(value);
60
+ return value;
61
+ } catch (err) {
62
+ throw new Error("Invalid URL format");
63
+ }
64
+ },
65
+ },
66
+ },
67
+ (argv) => {
68
+ const spider = new SpiderScanner(argv.url);
18
69
 
19
- // Parse command-line arguments
20
- program.parse(process.argv);
70
+ spider.crawl().then((output) => {
71
+ console.log(
72
+ JSON.stringify(
73
+ {
74
+ forms: output.forms,
75
+ links: output.links,
76
+ },
77
+ null,
78
+ 2,
79
+ ),
80
+ );
81
+ });
82
+ },
83
+ );
21
84
 
22
- const options = program.opts();
23
-
24
- // If no arguments are provided, display help
25
- if (Object.keys(options).length === 0) {
26
- program.help();
27
- }
85
+ // Parse arguments and handle commands
86
+ commandHandler.parse();
@@ -0,0 +1,43 @@
1
+ export default class Logger {
2
+ private moduleName: string;
3
+ private colors = {
4
+ error: "\x1b[31m",
5
+ info: "\x1b[32m",
6
+ warn: "\x1b[33m",
7
+ debug: "\x1b[35m",
8
+ reset: "\x1b[0m",
9
+ module: "\x1b[46m",
10
+ };
11
+
12
+ constructor(moduleName: string) {
13
+ this.moduleName = moduleName;
14
+ }
15
+
16
+ private formatMessage(
17
+ level: keyof typeof this.colors,
18
+ ...message: string[]
19
+ ): string {
20
+ const timestamp = new Date().toTimeString().split(" ")[0];
21
+ return `[${level}] ${this.colors[level]}${this.colors.reset}${this.colors[level]}[${timestamp}]${this.colors.reset} ${this.colors.module}[${this.moduleName}]${this.colors.reset} ${this.colors[level]}${message}${this.colors.reset}`;
22
+ }
23
+
24
+ public error(...message: string[]): void {
25
+ console.error(this.formatMessage("error", ...message));
26
+ }
27
+
28
+ public info(...message: string[]): void {
29
+ console.info(this.formatMessage("info", ...message));
30
+ }
31
+
32
+ public warn(...message: string[]): void {
33
+ console.warn(this.formatMessage("warn", ...message));
34
+ }
35
+
36
+ public log(...message: string[]): void {
37
+ console.log(this.formatMessage("info", ...message));
38
+ }
39
+
40
+ public debug(...message: string[]): void {
41
+ console.debug(this.formatMessage("debug", ...message));
42
+ }
43
+ }
@@ -0,0 +1,3 @@
1
+ import SpiderScanner from "./spider";
2
+
3
+ export { SpiderScanner };
@@ -0,0 +1,175 @@
1
+ import fetch from "isomorphic-fetch";
2
+ import jsdom from "jsdom";
3
+ import UserAgent from "user-agents";
4
+ import Logger from "../../lib/logger";
5
+
6
+ export type FormOutput = {
7
+ id: number;
8
+ url: string;
9
+ fields: Array<{ name: string; id: string; class: string; type: string }>;
10
+ };
11
+
12
+ export type CrawlOutput = {
13
+ links: string[];
14
+ forms: FormOutput[];
15
+ };
16
+
17
+ export default class SpiderScanner {
18
+ private header: Record<string, string> = {
19
+ "User-Agent": new UserAgent().toString(),
20
+ };
21
+ private url: URL;
22
+ private logger = new Logger("Spider");
23
+
24
+ constructor(url: string) {
25
+ try {
26
+ this.url = new URL(url);
27
+ this.logger.info(
28
+ `Initialized with URL: ${url} & User-Agent: ${this.header["User-Agent"]}`,
29
+ );
30
+ } catch (error) {
31
+ if (error instanceof TypeError) {
32
+ this.logger.error("Invalid URL");
33
+ throw new Error("Invalid URL");
34
+ }
35
+ this.logger.error(`Unexpected error in constructor: ${error}`);
36
+ throw error;
37
+ }
38
+ }
39
+
40
+ // Normalize domains (removes 'www.')
41
+ private normalizeDomain(domain: string): string {
42
+ return domain.startsWith("www.") ? domain.slice(4) : domain;
43
+ }
44
+
45
+ private convertRelativeUrlToAbsolute(url: string): string {
46
+ return new URL(url, this.url.toString()).toString();
47
+ }
48
+
49
+ private isInternalLink(url: string): boolean {
50
+ try {
51
+ const parsedUrl = new URL(url, this.url.href);
52
+ if (!["http:", "https:"].includes(parsedUrl.protocol)) {
53
+ return false;
54
+ }
55
+ const baseDomain = this.normalizeDomain(this.url.hostname);
56
+ const parsedDomain = this.normalizeDomain(parsedUrl.hostname);
57
+ return parsedDomain === baseDomain;
58
+ } catch (error) {
59
+ this.logger.warn(`Error parsing URL: ${url} - ${error}`);
60
+ return false;
61
+ }
62
+ }
63
+
64
+ private async fetchUrl(url: string): Promise<string | null> {
65
+ try {
66
+ this.logger.debug(`Fetching URL: ${url}`);
67
+ const response = await fetch(url, { headers: this.header });
68
+ if (!response.ok) {
69
+ this.logger.warn(`Failed to fetch URL (${response.status}): ${url}`);
70
+ return null;
71
+ }
72
+ this.logger.info(`Successfully fetched URL: ${url}`);
73
+ return await response.text();
74
+ } catch (error) {
75
+ this.logger.error(`Error fetching URL: ${url} - ${error}`);
76
+ return null;
77
+ }
78
+ }
79
+
80
+ private extractLinks(html: string): string[] {
81
+ const { JSDOM } = jsdom;
82
+ const dom = new JSDOM(html);
83
+ const links = Array.from(dom.window.document.querySelectorAll("a"));
84
+ const hrefs = links.map((link) => link.href);
85
+ const internalLinks = hrefs.filter((href) => this.isInternalLink(href));
86
+ this.logger.debug(
87
+ `Extracted ${internalLinks.length} internal links from HTML content`,
88
+ );
89
+ return internalLinks.map((link) => this.convertRelativeUrlToAbsolute(link));
90
+ }
91
+
92
+ private extractForms(html: string): FormOutput[] {
93
+ const { JSDOM } = jsdom;
94
+ const dom = new JSDOM(html);
95
+ const forms = Array.from(dom.window.document.querySelectorAll("form"));
96
+ this.logger.debug(`Extracted ${forms.length} forms from HTML content`);
97
+
98
+ return forms.map((form, index) => {
99
+ const fields = Array.from(form.querySelectorAll("input")).map(
100
+ (input) => ({
101
+ name: input.name,
102
+ id: input.id,
103
+ class: input.className,
104
+ type: input.type,
105
+ }),
106
+ );
107
+
108
+ return {
109
+ id: index,
110
+ url: this.url.href,
111
+ fields,
112
+ };
113
+ });
114
+ }
115
+
116
+ // Main function to scan the website with concurrency support and return both links and forms
117
+ public async crawl(depth = 250, concurrency = 5): Promise<CrawlOutput> {
118
+ const visited = new Set<string>();
119
+ const queue = new Set<string>([this.url.href]);
120
+ const resultLinks = new Set<string>();
121
+ const resultForms = new Set<FormOutput>();
122
+
123
+ const fetchAndExtract = async (currentUrl: string) => {
124
+ if (visited.has(currentUrl)) {
125
+ this.logger.debug(`Skipping already visited URL: ${currentUrl}`);
126
+ return;
127
+ }
128
+ visited.add(currentUrl);
129
+ this.logger.info(`Visiting URL: ${currentUrl}`);
130
+
131
+ const html = await this.fetchUrl(currentUrl);
132
+ if (!html) return;
133
+
134
+ // Extract links and forms
135
+ const links = this.extractLinks(html);
136
+ const forms = this.extractForms(html);
137
+
138
+ for (const form of forms) {
139
+ resultForms.add(form);
140
+ }
141
+
142
+ for (const link of links) {
143
+ if (!visited.has(link) && queue.size < depth) {
144
+ queue.add(link);
145
+ this.logger.debug(`Added to queue: ${link}`);
146
+ }
147
+ }
148
+ resultLinks.add(currentUrl);
149
+ };
150
+
151
+ const processBatch = async () => {
152
+ const batch = Array.from(queue).slice(0, concurrency);
153
+ for (const url of batch) {
154
+ queue.delete(url);
155
+ }
156
+ await Promise.allSettled(batch.map((url) => fetchAndExtract(url)));
157
+ };
158
+
159
+ this.logger.info(
160
+ `Starting crawl with depth: ${depth}, concurrency: ${concurrency}`,
161
+ );
162
+ while (queue.size > 0 && visited.size < depth) {
163
+ await processBatch();
164
+ }
165
+
166
+ this.logger.info(
167
+ `Crawling completed. Total pages visited: ${resultLinks.size}, Total forms found: ${resultForms.size}`,
168
+ );
169
+
170
+ return {
171
+ links: Array.from(resultLinks),
172
+ forms: Array.from(resultForms),
173
+ };
174
+ }
175
+ }
package/tsconfig.json CHANGED
@@ -2,13 +2,12 @@
2
2
  "include": ["./src/**/*.ts"],
3
3
  "compilerOptions": {
4
4
  "lib": ["es2023"],
5
- "module": "nodenext",
5
+ "module": "ESNext",
6
6
  "target": "es2022",
7
- "moduleResolution": "nodenext",
7
+ "moduleResolution": "node",
8
8
 
9
9
  "rootDir": "./src",
10
10
  "outDir": "build",
11
- "resolvePackageJsonImports": true,
12
11
 
13
12
  "strict": true,
14
13
  "noUncheckedIndexedAccess": true,
@@ -19,6 +18,12 @@
19
18
  "declaration": true,
20
19
  "resolveJsonModule": true,
21
20
  "emitDeclarationOnly": true,
21
+
22
+ // These two options can help resolve ESM issues
23
+ "allowSyntheticDefaultImports": true,
24
+ "isolatedModules": true,
25
+
26
+ // Ensure TypeScript recognizes .ts file extensions in ESM
22
27
  "allowImportingTsExtensions": true
23
28
  }
24
29
  }
File without changes