sentinel-scanner 1.0.1-alpha.1 → 1.1.0-alpha.1

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/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