fingerprint-mcp 0.1.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/LICENSE +663 -0
- package/README.ar.md +767 -0
- package/README.bn.md +767 -0
- package/README.bs.md +767 -0
- package/README.da.md +767 -0
- package/README.de.md +770 -0
- package/README.el.md +767 -0
- package/README.es.md +769 -0
- package/README.fr.md +769 -0
- package/README.hi.md +767 -0
- package/README.it.md +767 -0
- package/README.ja.md +767 -0
- package/README.ko.md +772 -0
- package/README.md +767 -0
- package/README.no.md +767 -0
- package/README.pl.md +767 -0
- package/README.pt-BR.md +767 -0
- package/README.ru.md +767 -0
- package/README.th.md +767 -0
- package/README.tr.md +767 -0
- package/README.uk.md +767 -0
- package/README.vi.md +767 -0
- package/README.zh-TW.md +768 -0
- package/README.zh.md +768 -0
- package/dist/app/index.d.ts +3 -0
- package/dist/app/index.d.ts.map +1 -0
- package/dist/app/index.js +614 -0
- package/dist/app/index.js.map +1 -0
- package/dist/composite/analyze.d.ts +3 -0
- package/dist/composite/analyze.d.ts.map +1 -0
- package/dist/composite/analyze.js +59 -0
- package/dist/composite/analyze.js.map +1 -0
- package/dist/composite/correlate.d.ts +3 -0
- package/dist/composite/correlate.d.ts.map +1 -0
- package/dist/composite/correlate.js +184 -0
- package/dist/composite/correlate.js.map +1 -0
- package/dist/composite/enumerate.d.ts +3 -0
- package/dist/composite/enumerate.d.ts.map +1 -0
- package/dist/composite/enumerate.js +94 -0
- package/dist/composite/enumerate.js.map +1 -0
- package/dist/composite/helpers.d.ts +22 -0
- package/dist/composite/helpers.d.ts.map +1 -0
- package/dist/composite/helpers.js +111 -0
- package/dist/composite/helpers.js.map +1 -0
- package/dist/composite/index.d.ts +3 -0
- package/dist/composite/index.d.ts.map +1 -0
- package/dist/composite/index.js +29 -0
- package/dist/composite/index.js.map +1 -0
- package/dist/composite/meta.d.ts +3 -0
- package/dist/composite/meta.d.ts.map +1 -0
- package/dist/composite/meta.js +23 -0
- package/dist/composite/meta.js.map +1 -0
- package/dist/composite/osint.d.ts +3 -0
- package/dist/composite/osint.d.ts.map +1 -0
- package/dist/composite/osint.js +40 -0
- package/dist/composite/osint.js.map +1 -0
- package/dist/composite/recon.d.ts +3 -0
- package/dist/composite/recon.d.ts.map +1 -0
- package/dist/composite/recon.js +131 -0
- package/dist/composite/recon.js.map +1 -0
- package/dist/composite/scan-dns.d.ts +3 -0
- package/dist/composite/scan-dns.d.ts.map +1 -0
- package/dist/composite/scan-dns.js +44 -0
- package/dist/composite/scan-dns.js.map +1 -0
- package/dist/composite/scan-http.d.ts +3 -0
- package/dist/composite/scan-http.d.ts.map +1 -0
- package/dist/composite/scan-http.js +68 -0
- package/dist/composite/scan-http.js.map +1 -0
- package/dist/composite/scan-paths.d.ts +3 -0
- package/dist/composite/scan-paths.d.ts.map +1 -0
- package/dist/composite/scan-paths.js +32 -0
- package/dist/composite/scan-paths.js.map +1 -0
- package/dist/composite/scan-ports.d.ts +3 -0
- package/dist/composite/scan-ports.d.ts.map +1 -0
- package/dist/composite/scan-ports.js +79 -0
- package/dist/composite/scan-ports.js.map +1 -0
- package/dist/composite/scan-services.d.ts +3 -0
- package/dist/composite/scan-services.d.ts.map +1 -0
- package/dist/composite/scan-services.js +111 -0
- package/dist/composite/scan-services.js.map +1 -0
- package/dist/composite/scan-tls.d.ts +3 -0
- package/dist/composite/scan-tls.d.ts.map +1 -0
- package/dist/composite/scan-tls.js +32 -0
- package/dist/composite/scan-tls.js.map +1 -0
- package/dist/composite/scan-waf.d.ts +3 -0
- package/dist/composite/scan-waf.d.ts.map +1 -0
- package/dist/composite/scan-waf.js +35 -0
- package/dist/composite/scan-waf.js.map +1 -0
- package/dist/correlation/index.d.ts +3 -0
- package/dist/correlation/index.d.ts.map +1 -0
- package/dist/correlation/index.js +1044 -0
- package/dist/correlation/index.js.map +1 -0
- package/dist/data/analytics-patterns.d.ts +21 -0
- package/dist/data/analytics-patterns.d.ts.map +1 -0
- package/dist/data/analytics-patterns.js +449 -0
- package/dist/data/analytics-patterns.js.map +1 -0
- package/dist/data/app-signatures.d.ts +52 -0
- package/dist/data/app-signatures.d.ts.map +1 -0
- package/dist/data/app-signatures.js +1143 -0
- package/dist/data/app-signatures.js.map +1 -0
- package/dist/data/c2-signatures.d.ts +54 -0
- package/dist/data/c2-signatures.d.ts.map +1 -0
- package/dist/data/c2-signatures.js +256 -0
- package/dist/data/c2-signatures.js.map +1 -0
- package/dist/data/cloud-ranges.d.ts +34 -0
- package/dist/data/cloud-ranges.d.ts.map +1 -0
- package/dist/data/cloud-ranges.js +628 -0
- package/dist/data/cloud-ranges.js.map +1 -0
- package/dist/data/cookie-patterns.d.ts +28 -0
- package/dist/data/cookie-patterns.d.ts.map +1 -0
- package/dist/data/cookie-patterns.js +225 -0
- package/dist/data/cookie-patterns.js.map +1 -0
- package/dist/data/error-signatures.d.ts +19 -0
- package/dist/data/error-signatures.d.ts.map +1 -0
- package/dist/data/error-signatures.js +321 -0
- package/dist/data/error-signatures.js.map +1 -0
- package/dist/data/favicon-hashes.d.ts +25 -0
- package/dist/data/favicon-hashes.d.ts.map +1 -0
- package/dist/data/favicon-hashes.js +249 -0
- package/dist/data/favicon-hashes.js.map +1 -0
- package/dist/data/h2-signatures.d.ts +50 -0
- package/dist/data/h2-signatures.d.ts.map +1 -0
- package/dist/data/h2-signatures.js +211 -0
- package/dist/data/h2-signatures.js.map +1 -0
- package/dist/data/header-order.d.ts +38 -0
- package/dist/data/header-order.d.ts.map +1 -0
- package/dist/data/header-order.js +185 -0
- package/dist/data/header-order.js.map +1 -0
- package/dist/data/jarm-signatures.d.ts +25 -0
- package/dist/data/jarm-signatures.d.ts.map +1 -0
- package/dist/data/jarm-signatures.js +213 -0
- package/dist/data/jarm-signatures.js.map +1 -0
- package/dist/data/saas-patterns.d.ts +23 -0
- package/dist/data/saas-patterns.d.ts.map +1 -0
- package/dist/data/saas-patterns.js +139 -0
- package/dist/data/saas-patterns.js.map +1 -0
- package/dist/data/sensitive-paths.d.ts +12 -0
- package/dist/data/sensitive-paths.d.ts.map +1 -0
- package/dist/data/sensitive-paths.js +1539 -0
- package/dist/data/sensitive-paths.js.map +1 -0
- package/dist/data/service-banners.d.ts +59 -0
- package/dist/data/service-banners.d.ts.map +1 -0
- package/dist/data/service-banners.js +323 -0
- package/dist/data/service-banners.js.map +1 -0
- package/dist/data/smtp-signatures.d.ts +12 -0
- package/dist/data/smtp-signatures.d.ts.map +1 -0
- package/dist/data/smtp-signatures.js +350 -0
- package/dist/data/smtp-signatures.js.map +1 -0
- package/dist/data/takeover-patterns.d.ts +37 -0
- package/dist/data/takeover-patterns.d.ts.map +1 -0
- package/dist/data/takeover-patterns.js +249 -0
- package/dist/data/takeover-patterns.js.map +1 -0
- package/dist/data/tech-patterns.d.ts +44 -0
- package/dist/data/tech-patterns.d.ts.map +1 -0
- package/dist/data/tech-patterns.js +690 -0
- package/dist/data/tech-patterns.js.map +1 -0
- package/dist/data/waf-signatures.d.ts +21 -0
- package/dist/data/waf-signatures.d.ts.map +1 -0
- package/dist/data/waf-signatures.js +318 -0
- package/dist/data/waf-signatures.js.map +1 -0
- package/dist/dns/index.d.ts +3 -0
- package/dist/dns/index.d.ts.map +1 -0
- package/dist/dns/index.js +1067 -0
- package/dist/dns/index.js.map +1 -0
- package/dist/enum/index.d.ts +3 -0
- package/dist/enum/index.d.ts.map +1 -0
- package/dist/enum/index.js +818 -0
- package/dist/enum/index.js.map +1 -0
- package/dist/h2/index.d.ts +3 -0
- package/dist/h2/index.d.ts.map +1 -0
- package/dist/h2/index.js +414 -0
- package/dist/h2/index.js.map +1 -0
- package/dist/http/index.d.ts +3 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +2444 -0
- package/dist/http/index.js.map +1 -0
- package/dist/identify/index.d.ts +3 -0
- package/dist/identify/index.d.ts.map +1 -0
- package/dist/identify/index.js +447 -0
- package/dist/identify/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +165 -0
- package/dist/index.js.map +1 -0
- package/dist/infra/index.d.ts +3 -0
- package/dist/infra/index.d.ts.map +1 -0
- package/dist/infra/index.js +989 -0
- package/dist/infra/index.js.map +1 -0
- package/dist/iot/index.d.ts +3 -0
- package/dist/iot/index.d.ts.map +1 -0
- package/dist/iot/index.js +610 -0
- package/dist/iot/index.js.map +1 -0
- package/dist/meta/index.d.ts +3 -0
- package/dist/meta/index.d.ts.map +1 -0
- package/dist/meta/index.js +442 -0
- package/dist/meta/index.js.map +1 -0
- package/dist/osint/index.d.ts +3 -0
- package/dist/osint/index.d.ts.map +1 -0
- package/dist/osint/index.js +687 -0
- package/dist/osint/index.js.map +1 -0
- package/dist/passive/index.d.ts +3 -0
- package/dist/passive/index.d.ts.map +1 -0
- package/dist/passive/index.js +944 -0
- package/dist/passive/index.js.map +1 -0
- package/dist/path/index.d.ts +3 -0
- package/dist/path/index.d.ts.map +1 -0
- package/dist/path/index.js +878 -0
- package/dist/path/index.js.map +1 -0
- package/dist/protocol/mcp-server.d.ts +4 -0
- package/dist/protocol/mcp-server.d.ts.map +1 -0
- package/dist/protocol/mcp-server.js +32 -0
- package/dist/protocol/mcp-server.js.map +1 -0
- package/dist/protocol/tools.d.ts +3 -0
- package/dist/protocol/tools.d.ts.map +1 -0
- package/dist/protocol/tools.js +5 -0
- package/dist/protocol/tools.js.map +1 -0
- package/dist/service/index.d.ts +3 -0
- package/dist/service/index.d.ts.map +1 -0
- package/dist/service/index.js +1053 -0
- package/dist/service/index.js.map +1 -0
- package/dist/smtp/index.d.ts +3 -0
- package/dist/smtp/index.d.ts.map +1 -0
- package/dist/smtp/index.js +437 -0
- package/dist/smtp/index.js.map +1 -0
- package/dist/ssh/index.d.ts +3 -0
- package/dist/ssh/index.d.ts.map +1 -0
- package/dist/ssh/index.js +676 -0
- package/dist/ssh/index.js.map +1 -0
- package/dist/tcp/index.d.ts +3 -0
- package/dist/tcp/index.d.ts.map +1 -0
- package/dist/tcp/index.js +369 -0
- package/dist/tcp/index.js.map +1 -0
- package/dist/timing/index.d.ts +3 -0
- package/dist/timing/index.d.ts.map +1 -0
- package/dist/timing/index.js +425 -0
- package/dist/timing/index.js.map +1 -0
- package/dist/tls/index.d.ts +3 -0
- package/dist/tls/index.d.ts.map +1 -0
- package/dist/tls/index.js +1332 -0
- package/dist/tls/index.js.map +1 -0
- package/dist/types/index.d.ts +26 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/cache.d.ts +11 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +35 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/murmurhash3.d.ts +3 -0
- package/dist/utils/murmurhash3.d.ts.map +1 -0
- package/dist/utils/murmurhash3.js +57 -0
- package/dist/utils/murmurhash3.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +10 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +35 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/require-key.d.ts +2 -0
- package/dist/utils/require-key.d.ts.map +1 -0
- package/dist/utils/require-key.js +8 -0
- package/dist/utils/require-key.js.map +1 -0
- package/dist/waf/index.d.ts +3 -0
- package/dist/waf/index.d.ts.map +1 -0
- package/dist/waf/index.js +767 -0
- package/dist/waf/index.js.map +1 -0
- package/dist/web/index.d.ts +3 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +1366 -0
- package/dist/web/index.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { json } from "../types/index.js";
|
|
3
|
+
import { CLOUD_PATTERNS } from "../data/cloud-ranges.js";
|
|
4
|
+
import { HEADER_ORDER_SIGNATURES, computeHeaderOrderHash } from "../data/header-order.js";
|
|
5
|
+
import { WAF_SIGNATURES } from "../data/waf-signatures.js";
|
|
6
|
+
import { COOKIE_PATTERNS } from "../data/cookie-patterns.js";
|
|
7
|
+
import { TECH_PATTERNS } from "../data/tech-patterns.js";
|
|
8
|
+
import { ANALYTICS_PATTERNS } from "../data/analytics-patterns.js";
|
|
9
|
+
import { SERVICE_BANNERS } from "../data/service-banners.js";
|
|
10
|
+
import * as cheerio from "cheerio";
|
|
11
|
+
// ─── Helpers ───
|
|
12
|
+
/** Safely test a regex pattern against a string, returning false on invalid regex. */
|
|
13
|
+
function safeRegexTest(pattern, value) {
|
|
14
|
+
try {
|
|
15
|
+
return new RegExp(pattern, "i").test(value);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Safely extract first capture group using a regex pattern. */
|
|
22
|
+
function safeRegexExtract(pattern, value) {
|
|
23
|
+
try {
|
|
24
|
+
const match = new RegExp(pattern, "i").exec(value);
|
|
25
|
+
return match?.[1] ?? match?.[2] ?? null;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Decode an F5 BIG-IP persistence cookie value to extract internal IP:port.
|
|
33
|
+
*
|
|
34
|
+
* The decimal cookie value encodes backend IP and port:
|
|
35
|
+
* - Split by '.': first part is encoded IP, second is encoded port
|
|
36
|
+
* - IP: convert decimal to hex, reverse byte pairs, convert each pair to decimal octets
|
|
37
|
+
* - Port: convert decimal to hex, reverse byte pair, convert to decimal
|
|
38
|
+
*/
|
|
39
|
+
function decodeBigIpCookie(cookieValue) {
|
|
40
|
+
try {
|
|
41
|
+
const parts = cookieValue.split(".");
|
|
42
|
+
if (parts.length < 2)
|
|
43
|
+
return null;
|
|
44
|
+
const encodedIp = parseInt(parts[0], 10);
|
|
45
|
+
const encodedPort = parseInt(parts[1], 10);
|
|
46
|
+
if (isNaN(encodedIp) || isNaN(encodedPort))
|
|
47
|
+
return null;
|
|
48
|
+
// Decode IP: convert to hex, reverse byte pairs
|
|
49
|
+
const ipHex = encodedIp.toString(16).padStart(8, "0");
|
|
50
|
+
const ip = [
|
|
51
|
+
parseInt(ipHex.substring(6, 8), 16),
|
|
52
|
+
parseInt(ipHex.substring(4, 6), 16),
|
|
53
|
+
parseInt(ipHex.substring(2, 4), 16),
|
|
54
|
+
parseInt(ipHex.substring(0, 2), 16),
|
|
55
|
+
].join(".");
|
|
56
|
+
// Decode port: convert to hex, reverse byte pair
|
|
57
|
+
const portHex = encodedPort.toString(16).padStart(4, "0");
|
|
58
|
+
const port = parseInt(portHex.substring(2, 4) + portHex.substring(0, 2), 16);
|
|
59
|
+
return { ip, port };
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// ─── Security Header Constants ───
|
|
66
|
+
const SECURITY_HEADERS = [
|
|
67
|
+
"strict-transport-security",
|
|
68
|
+
"content-security-policy",
|
|
69
|
+
"x-frame-options",
|
|
70
|
+
"x-content-type-options",
|
|
71
|
+
"referrer-policy",
|
|
72
|
+
"permissions-policy",
|
|
73
|
+
"x-xss-protection",
|
|
74
|
+
"cross-origin-opener-policy",
|
|
75
|
+
"cross-origin-embedder-policy",
|
|
76
|
+
"cross-origin-resource-policy",
|
|
77
|
+
];
|
|
78
|
+
// ─── Port-to-Protocol Map ───
|
|
79
|
+
const PORT_PROTOCOL_MAP = {
|
|
80
|
+
21: "ftp",
|
|
81
|
+
22: "ssh",
|
|
82
|
+
25: "smtp",
|
|
83
|
+
80: "http",
|
|
84
|
+
110: "pop3",
|
|
85
|
+
143: "imap",
|
|
86
|
+
443: "http",
|
|
87
|
+
465: "smtp",
|
|
88
|
+
587: "smtp",
|
|
89
|
+
3306: "mysql",
|
|
90
|
+
5432: "postgresql",
|
|
91
|
+
6379: "redis",
|
|
92
|
+
8080: "http",
|
|
93
|
+
8443: "http",
|
|
94
|
+
11211: "memcached",
|
|
95
|
+
27017: "mongodb",
|
|
96
|
+
};
|
|
97
|
+
// ─── Tool 1: fp_analyze_headers ───
|
|
98
|
+
async function analyzeHeaders(args) {
|
|
99
|
+
const headers = args.headers;
|
|
100
|
+
const url = args.url;
|
|
101
|
+
// Normalize header keys to lowercase for consistent matching
|
|
102
|
+
const normalizedHeaders = {};
|
|
103
|
+
const originalHeaderOrder = [];
|
|
104
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
105
|
+
normalizedHeaders[key.toLowerCase()] = value;
|
|
106
|
+
originalHeaderOrder.push(key);
|
|
107
|
+
}
|
|
108
|
+
// 1. Server identification
|
|
109
|
+
const claimedServer = normalizedHeaders["server"] ?? null;
|
|
110
|
+
// Compute header order hash and match against known signatures
|
|
111
|
+
const headerOrderHash = computeHeaderOrderHash(originalHeaderOrder);
|
|
112
|
+
let headerOrderServer = null;
|
|
113
|
+
for (const sig of HEADER_ORDER_SIGNATURES) {
|
|
114
|
+
if (sig.hash === headerOrderHash) {
|
|
115
|
+
headerOrderServer = sig.server;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const server = {
|
|
120
|
+
claimed: claimedServer,
|
|
121
|
+
headerOrderMatch: headerOrderServer,
|
|
122
|
+
headerOrderHash,
|
|
123
|
+
headerOrderMismatch: claimedServer && headerOrderServer
|
|
124
|
+
? !claimedServer.toLowerCase().includes(headerOrderServer.toLowerCase()) &&
|
|
125
|
+
!headerOrderServer.toLowerCase().includes(claimedServer.toLowerCase())
|
|
126
|
+
: false,
|
|
127
|
+
};
|
|
128
|
+
// 2. CDN/Cloud detection
|
|
129
|
+
const cloudDetections = [];
|
|
130
|
+
for (const cloud of CLOUD_PATTERNS) {
|
|
131
|
+
const matchedHeaders = [];
|
|
132
|
+
for (const [headerName, pattern] of Object.entries(cloud.headerPatterns)) {
|
|
133
|
+
const headerValue = normalizedHeaders[headerName.toLowerCase()];
|
|
134
|
+
if (headerValue !== undefined && safeRegexTest(pattern, headerValue)) {
|
|
135
|
+
matchedHeaders.push({ header: headerName, pattern });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (matchedHeaders.length > 0) {
|
|
139
|
+
cloudDetections.push({
|
|
140
|
+
provider: cloud.provider,
|
|
141
|
+
matchedHeaders,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// 3. WAF detection
|
|
146
|
+
const wafDetections = [];
|
|
147
|
+
// Extract cookie names from set-cookie headers
|
|
148
|
+
const setCookieValue = normalizedHeaders["set-cookie"] ?? "";
|
|
149
|
+
const cookieHeader = normalizedHeaders["cookie"] ?? "";
|
|
150
|
+
const allCookieText = setCookieValue + " " + cookieHeader;
|
|
151
|
+
// Extract cookie names from Set-Cookie (name=value; ...) patterns
|
|
152
|
+
const cookieNames = [];
|
|
153
|
+
const cookieNameMatches = allCookieText.match(/(?:^|[,;]\s*)([^=\s]+)=/g);
|
|
154
|
+
if (cookieNameMatches) {
|
|
155
|
+
for (const match of cookieNameMatches) {
|
|
156
|
+
const name = match.replace(/^[,;]\s*/, "").replace(/=$/, "");
|
|
157
|
+
if (name)
|
|
158
|
+
cookieNames.push(name);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const waf of WAF_SIGNATURES) {
|
|
162
|
+
const matchedHeaders = [];
|
|
163
|
+
const matchedCookies = [];
|
|
164
|
+
// Check header patterns
|
|
165
|
+
if (waf.patterns.headers) {
|
|
166
|
+
for (const [headerName, pattern] of Object.entries(waf.patterns.headers)) {
|
|
167
|
+
const headerValue = normalizedHeaders[headerName.toLowerCase()];
|
|
168
|
+
if (headerValue !== undefined && safeRegexTest(pattern, headerValue)) {
|
|
169
|
+
matchedHeaders.push(headerName);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Check cookie patterns
|
|
174
|
+
if (waf.patterns.cookies) {
|
|
175
|
+
for (const cookiePattern of waf.patterns.cookies) {
|
|
176
|
+
for (const cookieName of cookieNames) {
|
|
177
|
+
if (safeRegexTest(cookiePattern, cookieName)) {
|
|
178
|
+
matchedCookies.push(cookieName);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (matchedHeaders.length > 0 || matchedCookies.length > 0) {
|
|
184
|
+
wafDetections.push({
|
|
185
|
+
name: waf.name,
|
|
186
|
+
vendor: waf.vendor,
|
|
187
|
+
confidence: waf.confidence,
|
|
188
|
+
matchedHeaders,
|
|
189
|
+
matchedCookies,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// 4. Security header audit
|
|
194
|
+
const securityHeaderResults = [];
|
|
195
|
+
let securityHeaderCount = 0;
|
|
196
|
+
let securityScore = 0;
|
|
197
|
+
for (const header of SECURITY_HEADERS) {
|
|
198
|
+
const value = normalizedHeaders[header] ?? null;
|
|
199
|
+
const present = value !== null;
|
|
200
|
+
let assessment = "Missing";
|
|
201
|
+
if (present) {
|
|
202
|
+
securityHeaderCount++;
|
|
203
|
+
securityScore += 10;
|
|
204
|
+
assessment = "Present";
|
|
205
|
+
// Additional quality checks
|
|
206
|
+
if (header === "strict-transport-security") {
|
|
207
|
+
const maxAgeMatch = value.match(/max-age=(\d+)/i);
|
|
208
|
+
const maxAge = maxAgeMatch ? parseInt(maxAgeMatch[1], 10) : 0;
|
|
209
|
+
if (maxAge >= 31536000) {
|
|
210
|
+
assessment = "Strong (max-age >= 1 year)";
|
|
211
|
+
}
|
|
212
|
+
else if (maxAge >= 2592000) {
|
|
213
|
+
assessment = "Moderate (max-age >= 30 days)";
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
assessment = "Weak (max-age < 30 days)";
|
|
217
|
+
securityScore -= 3;
|
|
218
|
+
}
|
|
219
|
+
if (/includesubdomains/i.test(value))
|
|
220
|
+
assessment += ", includeSubDomains";
|
|
221
|
+
if (/preload/i.test(value))
|
|
222
|
+
assessment += ", preload";
|
|
223
|
+
}
|
|
224
|
+
else if (header === "x-content-type-options") {
|
|
225
|
+
assessment = value.toLowerCase() === "nosniff" ? "Correct (nosniff)" : `Unexpected value: ${value}`;
|
|
226
|
+
}
|
|
227
|
+
else if (header === "x-frame-options") {
|
|
228
|
+
if (/^deny$/i.test(value)) {
|
|
229
|
+
assessment = "Strong (DENY)";
|
|
230
|
+
}
|
|
231
|
+
else if (/^sameorigin$/i.test(value)) {
|
|
232
|
+
assessment = "Good (SAMEORIGIN)";
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
assessment = `Custom: ${value}`;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else if (header === "referrer-policy") {
|
|
239
|
+
if (/no-referrer$/i.test(value) || /strict-origin-when-cross-origin/i.test(value)) {
|
|
240
|
+
assessment = `Good (${value})`;
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
assessment = `Present (${value})`;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
securityHeaderResults.push({ header, present, value, assessment });
|
|
248
|
+
}
|
|
249
|
+
// Grade calculation: A-F based on count (10 headers total, 10 points each = 100 max)
|
|
250
|
+
const securityGrade = securityScore >= 90
|
|
251
|
+
? "A"
|
|
252
|
+
: securityScore >= 70
|
|
253
|
+
? "B"
|
|
254
|
+
: securityScore >= 50
|
|
255
|
+
? "C"
|
|
256
|
+
: securityScore >= 30
|
|
257
|
+
? "D"
|
|
258
|
+
: securityScore >= 10
|
|
259
|
+
? "E"
|
|
260
|
+
: "F";
|
|
261
|
+
const securityHeaders = {
|
|
262
|
+
results: securityHeaderResults,
|
|
263
|
+
present: securityHeaderCount,
|
|
264
|
+
total: SECURITY_HEADERS.length,
|
|
265
|
+
grade: securityGrade,
|
|
266
|
+
score: securityScore,
|
|
267
|
+
};
|
|
268
|
+
// 5. Infrastructure cookie decode
|
|
269
|
+
const cookieFindings = [];
|
|
270
|
+
for (const cookieName of cookieNames) {
|
|
271
|
+
for (const pattern of COOKIE_PATTERNS) {
|
|
272
|
+
if (safeRegexTest(pattern.pattern, cookieName)) {
|
|
273
|
+
const finding = {
|
|
274
|
+
cookieName,
|
|
275
|
+
technology: pattern.technology,
|
|
276
|
+
category: pattern.category,
|
|
277
|
+
name: pattern.name,
|
|
278
|
+
};
|
|
279
|
+
// Decode BIGipServer cookies
|
|
280
|
+
if (cookieName.startsWith("BIGipServer")) {
|
|
281
|
+
// Try to extract the cookie value from the raw set-cookie header
|
|
282
|
+
const bigIpMatch = allCookieText.match(new RegExp(`${cookieName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}=([^;\\s]+)`));
|
|
283
|
+
if (bigIpMatch) {
|
|
284
|
+
finding.decoded = decodeBigIpCookie(bigIpMatch[1]);
|
|
285
|
+
}
|
|
286
|
+
finding.note = "F5 BIG-IP persistence cookie - may reveal internal IP:port";
|
|
287
|
+
}
|
|
288
|
+
else if (cookieName.startsWith("AWSALB")) {
|
|
289
|
+
finding.note = "AWS Application Load Balancer cookie";
|
|
290
|
+
}
|
|
291
|
+
else if (cookieName.startsWith("NSC_")) {
|
|
292
|
+
finding.note = "Citrix NetScaler persistence cookie - may encode VIP address";
|
|
293
|
+
}
|
|
294
|
+
cookieFindings.push(finding);
|
|
295
|
+
break; // First match per cookie name
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// 6. Technology detection
|
|
300
|
+
const technologyDetections = [];
|
|
301
|
+
const techHeaders = {
|
|
302
|
+
"x-powered-by": "Server-side technology",
|
|
303
|
+
"x-aspnet-version": "ASP.NET version",
|
|
304
|
+
"x-generator": "Site generator",
|
|
305
|
+
"x-drupal-cache": "Drupal CMS",
|
|
306
|
+
"x-drupal-dynamic-cache": "Drupal CMS (dynamic cache)",
|
|
307
|
+
"x-varnish": "Varnish cache",
|
|
308
|
+
"x-magento-vary": "Magento e-commerce",
|
|
309
|
+
"x-shopify-stage": "Shopify",
|
|
310
|
+
"x-wix-request-id": "Wix",
|
|
311
|
+
"x-envoy-upstream-service-time": "Envoy proxy",
|
|
312
|
+
"x-litespeed-cache": "LiteSpeed Cache",
|
|
313
|
+
"x-turbo-charged-by": "LiteSpeed (Turbo)",
|
|
314
|
+
"x-pingback": "WordPress (xmlrpc)",
|
|
315
|
+
"x-redirect-by": "WordPress redirect",
|
|
316
|
+
"x-runtime": "Ruby on Rails / Rack",
|
|
317
|
+
};
|
|
318
|
+
for (const [header, tech] of Object.entries(techHeaders)) {
|
|
319
|
+
const value = normalizedHeaders[header];
|
|
320
|
+
if (value !== undefined) {
|
|
321
|
+
technologyDetections.push({ header, value, technology: tech });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// 7. Cache strategy
|
|
325
|
+
const cacheStrategy = {
|
|
326
|
+
cacheControl: normalizedHeaders["cache-control"] ?? null,
|
|
327
|
+
etag: normalizedHeaders["etag"] ?? null,
|
|
328
|
+
age: normalizedHeaders["age"] ?? null,
|
|
329
|
+
xCache: normalizedHeaders["x-cache"] ?? null,
|
|
330
|
+
vary: normalizedHeaders["vary"] ?? null,
|
|
331
|
+
cdnCache: normalizedHeaders["cf-cache-status"] ??
|
|
332
|
+
normalizedHeaders["x-vercel-cache"] ??
|
|
333
|
+
normalizedHeaders["x-cache-hits"] ??
|
|
334
|
+
null,
|
|
335
|
+
strategy: "unknown",
|
|
336
|
+
};
|
|
337
|
+
// Determine caching strategy
|
|
338
|
+
const cc = cacheStrategy.cacheControl?.toLowerCase() ?? "";
|
|
339
|
+
if (cc.includes("no-store") || cc.includes("no-cache")) {
|
|
340
|
+
cacheStrategy.strategy = "no-cache";
|
|
341
|
+
}
|
|
342
|
+
else if (cc.includes("public") && cc.includes("max-age")) {
|
|
343
|
+
cacheStrategy.strategy = "aggressive-caching";
|
|
344
|
+
}
|
|
345
|
+
else if (cc.includes("private")) {
|
|
346
|
+
cacheStrategy.strategy = "private-caching";
|
|
347
|
+
}
|
|
348
|
+
else if (cc.includes("must-revalidate") || cacheStrategy.etag) {
|
|
349
|
+
cacheStrategy.strategy = "revalidation-based";
|
|
350
|
+
}
|
|
351
|
+
else if (cacheStrategy.xCache || cacheStrategy.cdnCache) {
|
|
352
|
+
cacheStrategy.strategy = "cdn-managed";
|
|
353
|
+
}
|
|
354
|
+
else if (cacheStrategy.cacheControl) {
|
|
355
|
+
cacheStrategy.strategy = "custom";
|
|
356
|
+
}
|
|
357
|
+
// Intelligence summary
|
|
358
|
+
const intelligence = [];
|
|
359
|
+
if (server.headerOrderMismatch) {
|
|
360
|
+
intelligence.push(`Server header claims "${server.claimed}" but header ordering matches "${server.headerOrderMatch}" - possible header spoofing`);
|
|
361
|
+
}
|
|
362
|
+
if (cloudDetections.length > 0) {
|
|
363
|
+
intelligence.push(`Cloud infrastructure detected: ${cloudDetections.map((c) => c.provider).join(", ")}`);
|
|
364
|
+
}
|
|
365
|
+
if (wafDetections.length > 0) {
|
|
366
|
+
intelligence.push(`WAF detected: ${wafDetections.map((w) => `${w.name} (${w.vendor})`).join(", ")}`);
|
|
367
|
+
}
|
|
368
|
+
if (securityGrade === "F" || securityGrade === "E") {
|
|
369
|
+
intelligence.push(`Poor security header posture (grade ${securityGrade}) - ${securityHeaderCount}/${SECURITY_HEADERS.length} headers present`);
|
|
370
|
+
}
|
|
371
|
+
if (cookieFindings.some((c) => c.decoded)) {
|
|
372
|
+
intelligence.push("Infrastructure cookies leak internal IP addresses");
|
|
373
|
+
}
|
|
374
|
+
if (technologyDetections.length > 0) {
|
|
375
|
+
intelligence.push(`Technologies revealed via headers: ${technologyDetections.map((t) => t.value || t.technology).join(", ")}`);
|
|
376
|
+
}
|
|
377
|
+
const result = {
|
|
378
|
+
url: url ?? null,
|
|
379
|
+
server,
|
|
380
|
+
cloud: cloudDetections,
|
|
381
|
+
waf: wafDetections,
|
|
382
|
+
securityHeaders,
|
|
383
|
+
cookies: cookieFindings,
|
|
384
|
+
technology: technologyDetections,
|
|
385
|
+
cacheStrategy,
|
|
386
|
+
intelligence,
|
|
387
|
+
};
|
|
388
|
+
return json(result);
|
|
389
|
+
}
|
|
390
|
+
// ─── Tool 2: fp_analyze_html ───
|
|
391
|
+
async function analyzeHtml(args) {
|
|
392
|
+
const html = args.html;
|
|
393
|
+
const url = args.url;
|
|
394
|
+
const $ = cheerio.load(html);
|
|
395
|
+
// 1. Technology detection
|
|
396
|
+
const technologies = [];
|
|
397
|
+
// Collect all script src values
|
|
398
|
+
const scriptSrcs = [];
|
|
399
|
+
$("script[src]").each((_i, el) => {
|
|
400
|
+
const src = $(el).attr("src");
|
|
401
|
+
if (src)
|
|
402
|
+
scriptSrcs.push(src);
|
|
403
|
+
});
|
|
404
|
+
// Collect all inline script content
|
|
405
|
+
const inlineScripts = [];
|
|
406
|
+
$("script:not([src])").each((_i, el) => {
|
|
407
|
+
const content = $(el).html();
|
|
408
|
+
if (content)
|
|
409
|
+
inlineScripts.push(content);
|
|
410
|
+
});
|
|
411
|
+
const inlineScriptText = inlineScripts.join("\n");
|
|
412
|
+
// Collect all meta tags as raw HTML
|
|
413
|
+
const metaTags = [];
|
|
414
|
+
$("meta").each((_i, el) => {
|
|
415
|
+
const outerHtml = $.html(el);
|
|
416
|
+
if (outerHtml)
|
|
417
|
+
metaTags.push(outerHtml);
|
|
418
|
+
});
|
|
419
|
+
// Collect all HTML comments
|
|
420
|
+
const htmlComments = [];
|
|
421
|
+
const commentRegex = /<!--([\s\S]*?)-->/g;
|
|
422
|
+
let commentMatch;
|
|
423
|
+
while ((commentMatch = commentRegex.exec(html)) !== null) {
|
|
424
|
+
htmlComments.push(commentMatch[1].trim());
|
|
425
|
+
}
|
|
426
|
+
const htmlBody = $.html();
|
|
427
|
+
for (const tech of TECH_PATTERNS) {
|
|
428
|
+
let detected = false;
|
|
429
|
+
let matchType = "";
|
|
430
|
+
// Check script patterns
|
|
431
|
+
if (tech.detection.script) {
|
|
432
|
+
for (const pattern of tech.detection.script) {
|
|
433
|
+
for (const src of scriptSrcs) {
|
|
434
|
+
if (safeRegexTest(pattern, src)) {
|
|
435
|
+
detected = true;
|
|
436
|
+
matchType = "script-src";
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (detected)
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// Check DOM patterns
|
|
445
|
+
if (!detected && tech.detection.dom) {
|
|
446
|
+
for (const pattern of tech.detection.dom) {
|
|
447
|
+
if (safeRegexTest(pattern, htmlBody)) {
|
|
448
|
+
detected = true;
|
|
449
|
+
matchType = "dom";
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// Check meta patterns
|
|
455
|
+
if (!detected && tech.detection.meta) {
|
|
456
|
+
for (const pattern of tech.detection.meta) {
|
|
457
|
+
for (const metaTag of metaTags) {
|
|
458
|
+
if (safeRegexTest(pattern, metaTag)) {
|
|
459
|
+
detected = true;
|
|
460
|
+
matchType = "meta";
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (detected)
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// Check comment patterns
|
|
469
|
+
if (!detected && tech.detection.comment) {
|
|
470
|
+
for (const pattern of tech.detection.comment) {
|
|
471
|
+
for (const comment of htmlComments) {
|
|
472
|
+
if (safeRegexTest(pattern, comment)) {
|
|
473
|
+
detected = true;
|
|
474
|
+
matchType = "comment";
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (detected)
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (detected) {
|
|
483
|
+
// Try to extract version
|
|
484
|
+
let version = null;
|
|
485
|
+
if (tech.versionExtract) {
|
|
486
|
+
// Search across all available text sources
|
|
487
|
+
version =
|
|
488
|
+
safeRegexExtract(tech.versionExtract, html) ??
|
|
489
|
+
safeRegexExtract(tech.versionExtract, inlineScriptText);
|
|
490
|
+
// Also check script src values for version info
|
|
491
|
+
if (!version) {
|
|
492
|
+
for (const src of scriptSrcs) {
|
|
493
|
+
version = safeRegexExtract(tech.versionExtract, src);
|
|
494
|
+
if (version)
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// Check CVEs if version found
|
|
500
|
+
const matchedCves = [];
|
|
501
|
+
if (version && tech.cves) {
|
|
502
|
+
for (const cveEntry of tech.cves) {
|
|
503
|
+
// Simple version comparison: if the CVE applies to versions below a threshold
|
|
504
|
+
// we include it as potentially applicable (exact semver comparison is complex,
|
|
505
|
+
// so we flag all known CVEs for manual review when a version is detected)
|
|
506
|
+
matchedCves.push({
|
|
507
|
+
cve: cveEntry.cve,
|
|
508
|
+
description: cveEntry.description,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
else if (!version && tech.cves) {
|
|
513
|
+
// If no version extracted but CVEs exist, flag them as "version unknown"
|
|
514
|
+
for (const cveEntry of tech.cves) {
|
|
515
|
+
matchedCves.push({
|
|
516
|
+
cve: cveEntry.cve,
|
|
517
|
+
description: `${cveEntry.description} (version not determined - manual check needed for ${cveEntry.version})`,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
technologies.push({
|
|
522
|
+
name: tech.name,
|
|
523
|
+
category: tech.category,
|
|
524
|
+
version,
|
|
525
|
+
cves: matchedCves,
|
|
526
|
+
matchType,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// 2. Analytics detection
|
|
531
|
+
const analytics = [];
|
|
532
|
+
for (const ap of ANALYTICS_PATTERNS) {
|
|
533
|
+
let detected = false;
|
|
534
|
+
let matchType = "";
|
|
535
|
+
// Check script patterns
|
|
536
|
+
if (ap.patterns.script) {
|
|
537
|
+
for (const pattern of ap.patterns.script) {
|
|
538
|
+
for (const src of scriptSrcs) {
|
|
539
|
+
if (safeRegexTest(pattern, src)) {
|
|
540
|
+
detected = true;
|
|
541
|
+
matchType = "script-src";
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (detected)
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// Check inline patterns
|
|
550
|
+
if (!detected && ap.patterns.inline) {
|
|
551
|
+
for (const pattern of ap.patterns.inline) {
|
|
552
|
+
if (safeRegexTest(pattern, inlineScriptText)) {
|
|
553
|
+
detected = true;
|
|
554
|
+
matchType = "inline-script";
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (detected) {
|
|
560
|
+
// Extract tracking ID
|
|
561
|
+
let trackingId = null;
|
|
562
|
+
if (ap.idExtract) {
|
|
563
|
+
trackingId =
|
|
564
|
+
safeRegexExtract(ap.idExtract, html) ??
|
|
565
|
+
safeRegexExtract(ap.idExtract, inlineScriptText);
|
|
566
|
+
if (!trackingId) {
|
|
567
|
+
for (const src of scriptSrcs) {
|
|
568
|
+
trackingId = safeRegexExtract(ap.idExtract, src);
|
|
569
|
+
if (trackingId)
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
analytics.push({
|
|
575
|
+
name: ap.name,
|
|
576
|
+
category: ap.category,
|
|
577
|
+
trackingId,
|
|
578
|
+
crossRefValue: ap.crossRefValue,
|
|
579
|
+
matchType,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// 3. Meta generator
|
|
584
|
+
const generatorMeta = $('meta[name="generator"]').attr("content") ?? null;
|
|
585
|
+
// 4. Framework indicators
|
|
586
|
+
const frameworkIndicators = [];
|
|
587
|
+
if (html.includes("__NEXT_DATA__") || $("script#__NEXT_DATA__").length > 0) {
|
|
588
|
+
frameworkIndicators.push({ framework: "Next.js", indicator: "__NEXT_DATA__" });
|
|
589
|
+
}
|
|
590
|
+
if (html.includes("__NUXT__") || html.includes("$nuxt")) {
|
|
591
|
+
frameworkIndicators.push({ framework: "Nuxt.js", indicator: "__NUXT__" });
|
|
592
|
+
}
|
|
593
|
+
if ($("[ng-version]").length > 0) {
|
|
594
|
+
const ngVersion = $("[ng-version]").attr("ng-version") ?? "unknown";
|
|
595
|
+
frameworkIndicators.push({ framework: "Angular", indicator: `ng-version="${ngVersion}"` });
|
|
596
|
+
}
|
|
597
|
+
if ($("[data-reactroot]").length > 0 || html.includes("data-reactroot")) {
|
|
598
|
+
frameworkIndicators.push({ framework: "React", indicator: "data-reactroot" });
|
|
599
|
+
}
|
|
600
|
+
if (/\bdata-v-[a-f0-9]+/.test(html)) {
|
|
601
|
+
frameworkIndicators.push({ framework: "Vue.js", indicator: "data-v-* scoped attributes" });
|
|
602
|
+
}
|
|
603
|
+
if (/\bclass="svelte-[a-z0-9]+"/i.test(html) || html.includes("svelte-")) {
|
|
604
|
+
frameworkIndicators.push({ framework: "Svelte", indicator: "svelte-* class names" });
|
|
605
|
+
}
|
|
606
|
+
if ($("astro-island").length > 0 || html.includes("astro-island")) {
|
|
607
|
+
frameworkIndicators.push({ framework: "Astro", indicator: "astro-island element" });
|
|
608
|
+
}
|
|
609
|
+
// 5. Source maps
|
|
610
|
+
const sourceMaps = [];
|
|
611
|
+
$("script[src]").each((_i, el) => {
|
|
612
|
+
const src = $(el).attr("src");
|
|
613
|
+
if (src && src.endsWith(".map")) {
|
|
614
|
+
sourceMaps.push(src);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
$("link[href]").each((_i, el) => {
|
|
618
|
+
const href = $(el).attr("href");
|
|
619
|
+
if (href && href.endsWith(".map")) {
|
|
620
|
+
sourceMaps.push(href);
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
// Check inline scripts for sourceMappingURL
|
|
624
|
+
const sourceMappingRegex = /\/\/[#@]\s*sourceMappingURL\s*=\s*(\S+)/g;
|
|
625
|
+
let smMatch;
|
|
626
|
+
while ((smMatch = sourceMappingRegex.exec(html)) !== null) {
|
|
627
|
+
sourceMaps.push(smMatch[1]);
|
|
628
|
+
}
|
|
629
|
+
// 6. HTML comments analysis
|
|
630
|
+
const interestingComments = [];
|
|
631
|
+
for (const comment of htmlComments) {
|
|
632
|
+
// Skip empty or very short comments
|
|
633
|
+
if (comment.length < 3)
|
|
634
|
+
continue;
|
|
635
|
+
// Version strings
|
|
636
|
+
if (/v?\d+\.\d+\.\d+/.test(comment)) {
|
|
637
|
+
interestingComments.push({ content: comment.substring(0, 200), type: "version" });
|
|
638
|
+
}
|
|
639
|
+
// TODO/FIXME markers
|
|
640
|
+
else if (/\b(TODO|FIXME|HACK|BUG|XXX|WARN)\b/i.test(comment)) {
|
|
641
|
+
interestingComments.push({ content: comment.substring(0, 200), type: "developer-note" });
|
|
642
|
+
}
|
|
643
|
+
// Internal URLs
|
|
644
|
+
else if (/https?:\/\/(?:localhost|127\.0\.0\.1|192\.168\.|10\.|172\.(?:1[6-9]|2\d|3[01])\.)/i.test(comment)) {
|
|
645
|
+
interestingComments.push({ content: comment.substring(0, 200), type: "internal-url" });
|
|
646
|
+
}
|
|
647
|
+
// Developer notes with useful info
|
|
648
|
+
else if (comment.length > 20 && !/^\s*\[if\s/.test(comment) && !/^\s*<!\[endif\]/.test(comment)) {
|
|
649
|
+
// Skip IE conditional comments, only flag substantive comments
|
|
650
|
+
if (/\b(debug|config|password|secret|key|token|api|admin|internal|staging|test)\b/i.test(comment)) {
|
|
651
|
+
interestingComments.push({ content: comment.substring(0, 200), type: "potentially-sensitive" });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
// 7. Hardcoded endpoints
|
|
656
|
+
const endpoints = [];
|
|
657
|
+
// Scan inline scripts for URLs
|
|
658
|
+
const urlRegex = /(?:["'`])((https?:\/\/[^\s"'`<>]+))/g;
|
|
659
|
+
let urlMatch;
|
|
660
|
+
const seenUrls = new Set();
|
|
661
|
+
while ((urlMatch = urlRegex.exec(inlineScriptText)) !== null) {
|
|
662
|
+
const foundUrl = urlMatch[1];
|
|
663
|
+
if (!seenUrls.has(foundUrl)) {
|
|
664
|
+
seenUrls.add(foundUrl);
|
|
665
|
+
let type = "external-url";
|
|
666
|
+
if (/\/api\//i.test(foundUrl))
|
|
667
|
+
type = "api-endpoint";
|
|
668
|
+
else if (/\/v[1-9]\//i.test(foundUrl))
|
|
669
|
+
type = "versioned-api";
|
|
670
|
+
else if (/\/graphql/i.test(foundUrl))
|
|
671
|
+
type = "graphql-endpoint";
|
|
672
|
+
endpoints.push({ url: foundUrl, type, source: "inline-script" });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
// Scan inline scripts for IP addresses
|
|
676
|
+
const ipRegex = /(?:["'`])(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?::\d+)?)/g;
|
|
677
|
+
let ipMatch;
|
|
678
|
+
const seenIps = new Set();
|
|
679
|
+
while ((ipMatch = ipRegex.exec(inlineScriptText)) !== null) {
|
|
680
|
+
const foundIp = ipMatch[1];
|
|
681
|
+
if (!seenIps.has(foundIp) && !/^0\.0\.0\.0$/.test(foundIp) && !/^127\.0\.0\.1$/.test(foundIp)) {
|
|
682
|
+
seenIps.add(foundIp);
|
|
683
|
+
endpoints.push({ url: foundIp, type: "ip-address", source: "inline-script" });
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// Scan inline scripts for API path patterns
|
|
687
|
+
const apiPathRegex = /(?:["'`])(\/(?:api|v[1-9]|v\d+|graphql)\/[^\s"'`<>]*)/g;
|
|
688
|
+
let apiMatch;
|
|
689
|
+
const seenPaths = new Set();
|
|
690
|
+
while ((apiMatch = apiPathRegex.exec(inlineScriptText)) !== null) {
|
|
691
|
+
const foundPath = apiMatch[1];
|
|
692
|
+
if (!seenPaths.has(foundPath)) {
|
|
693
|
+
seenPaths.add(foundPath);
|
|
694
|
+
let type = "api-path";
|
|
695
|
+
if (/\/graphql/i.test(foundPath))
|
|
696
|
+
type = "graphql-path";
|
|
697
|
+
endpoints.push({ url: foundPath, type, source: "inline-script" });
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
// 8. Build tool detection
|
|
701
|
+
let buildTool = null;
|
|
702
|
+
const buildIndicators = [];
|
|
703
|
+
if (html.includes("webpackJsonp") || html.includes("__webpack_require__") || html.includes("webpackChunk")) {
|
|
704
|
+
buildIndicators.push(html.includes("webpackJsonp") ? "webpackJsonp" : "", html.includes("__webpack_require__") ? "__webpack_require__" : "", html.includes("webpackChunk") ? "webpackChunk" : "");
|
|
705
|
+
buildTool = { name: "Webpack", indicators: buildIndicators.filter(Boolean) };
|
|
706
|
+
}
|
|
707
|
+
else if (html.includes("/@vite/") || html.includes("__vite_")) {
|
|
708
|
+
buildIndicators.push(html.includes("/@vite/") ? "/@vite/ import" : "", html.includes("__vite_") ? "__vite_ global" : "");
|
|
709
|
+
buildTool = { name: "Vite", indicators: buildIndicators.filter(Boolean) };
|
|
710
|
+
}
|
|
711
|
+
else if (html.includes("parcelRequire")) {
|
|
712
|
+
buildTool = { name: "Parcel", indicators: ["parcelRequire"] };
|
|
713
|
+
}
|
|
714
|
+
// Intelligence summary
|
|
715
|
+
const intelligence = [];
|
|
716
|
+
if (technologies.length > 0) {
|
|
717
|
+
intelligence.push(`${technologies.length} technologies detected: ${technologies.map((t) => t.name + (t.version ? ` v${t.version}` : "")).join(", ")}`);
|
|
718
|
+
}
|
|
719
|
+
const vulnerableTechs = technologies.filter((t) => t.cves.length > 0);
|
|
720
|
+
if (vulnerableTechs.length > 0) {
|
|
721
|
+
const totalCves = vulnerableTechs.reduce((sum, t) => sum + t.cves.length, 0);
|
|
722
|
+
intelligence.push(`${totalCves} potential CVEs found across ${vulnerableTechs.length} technologies`);
|
|
723
|
+
}
|
|
724
|
+
if (analytics.length > 0) {
|
|
725
|
+
const crossRefAnalytics = analytics.filter((a) => a.trackingId && a.crossRefValue);
|
|
726
|
+
if (crossRefAnalytics.length > 0) {
|
|
727
|
+
intelligence.push(`Cross-referenceable tracking IDs: ${crossRefAnalytics.map((a) => `${a.name}=${a.trackingId}`).join(", ")}`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (sourceMaps.length > 0) {
|
|
731
|
+
intelligence.push(`${sourceMaps.length} source maps found - may expose original source code`);
|
|
732
|
+
}
|
|
733
|
+
if (endpoints.length > 0) {
|
|
734
|
+
intelligence.push(`${endpoints.length} hardcoded endpoints found in inline scripts`);
|
|
735
|
+
}
|
|
736
|
+
if (interestingComments.length > 0) {
|
|
737
|
+
intelligence.push(`${interestingComments.length} interesting HTML comments found`);
|
|
738
|
+
}
|
|
739
|
+
const result = {
|
|
740
|
+
url: url ?? null,
|
|
741
|
+
technologies,
|
|
742
|
+
analytics,
|
|
743
|
+
generator: generatorMeta,
|
|
744
|
+
frameworkIndicators,
|
|
745
|
+
sourceMaps,
|
|
746
|
+
comments: interestingComments,
|
|
747
|
+
endpoints,
|
|
748
|
+
buildTool,
|
|
749
|
+
intelligence,
|
|
750
|
+
};
|
|
751
|
+
return json(result);
|
|
752
|
+
}
|
|
753
|
+
// ─── Tool 3: fp_analyze_banner ───
|
|
754
|
+
async function analyzeBanner(args) {
|
|
755
|
+
const banner = args.banner;
|
|
756
|
+
const port = args.port;
|
|
757
|
+
// 1. Match against SERVICE_BANNERS database
|
|
758
|
+
let matchedService = null;
|
|
759
|
+
for (const entry of SERVICE_BANNERS) {
|
|
760
|
+
try {
|
|
761
|
+
const regex = new RegExp(entry.pattern, "i");
|
|
762
|
+
const match = regex.exec(banner);
|
|
763
|
+
if (match) {
|
|
764
|
+
let version = null;
|
|
765
|
+
// Extract version from capture groups
|
|
766
|
+
if (entry.version_extract && match.length > 1) {
|
|
767
|
+
// Use the first non-undefined capture group
|
|
768
|
+
for (let i = 1; i < match.length; i++) {
|
|
769
|
+
if (match[i]) {
|
|
770
|
+
version = match[i];
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
matchedService = {
|
|
776
|
+
service: entry.service,
|
|
777
|
+
software: entry.name,
|
|
778
|
+
version,
|
|
779
|
+
matchedPattern: entry.pattern,
|
|
780
|
+
};
|
|
781
|
+
break; // Use first match
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
catch {
|
|
785
|
+
// Invalid regex in pattern DB, skip
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
// 2. Port-based protocol heuristic as tiebreaker
|
|
789
|
+
let portProtocol = null;
|
|
790
|
+
if (port !== undefined && PORT_PROTOCOL_MAP[port]) {
|
|
791
|
+
portProtocol = PORT_PROTOCOL_MAP[port];
|
|
792
|
+
}
|
|
793
|
+
// 3. Common banner patterns not in the DB
|
|
794
|
+
const additionalDetections = [];
|
|
795
|
+
if (/^RFB\s+\d+\.\d+/i.test(banner)) {
|
|
796
|
+
additionalDetections.push({
|
|
797
|
+
pattern: "RFB protocol header",
|
|
798
|
+
protocol: "vnc",
|
|
799
|
+
software: "VNC Server",
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
if (/^\*\s+OK/i.test(banner)) {
|
|
803
|
+
additionalDetections.push({
|
|
804
|
+
pattern: "* OK (IMAP greeting)",
|
|
805
|
+
protocol: "imap",
|
|
806
|
+
software: "IMAP Server",
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
if (/^\+OK/i.test(banner)) {
|
|
810
|
+
additionalDetections.push({
|
|
811
|
+
pattern: "+OK (POP3 greeting)",
|
|
812
|
+
protocol: "pop3",
|
|
813
|
+
software: "POP3 Server",
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
if (/^HTTP\/\d+\.\d+/i.test(banner)) {
|
|
817
|
+
additionalDetections.push({
|
|
818
|
+
pattern: "HTTP/ protocol header",
|
|
819
|
+
protocol: "http",
|
|
820
|
+
software: "HTTP Server",
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
// 4. OS hints from banner
|
|
824
|
+
const osHints = [];
|
|
825
|
+
if (/ubuntu/i.test(banner))
|
|
826
|
+
osHints.push("Ubuntu");
|
|
827
|
+
if (/debian/i.test(banner))
|
|
828
|
+
osHints.push("Debian");
|
|
829
|
+
if (/centos/i.test(banner))
|
|
830
|
+
osHints.push("CentOS");
|
|
831
|
+
if (/red\s*hat/i.test(banner))
|
|
832
|
+
osHints.push("Red Hat");
|
|
833
|
+
if (/fedora/i.test(banner))
|
|
834
|
+
osHints.push("Fedora");
|
|
835
|
+
if (/alpine/i.test(banner))
|
|
836
|
+
osHints.push("Alpine Linux");
|
|
837
|
+
if (/freebsd/i.test(banner))
|
|
838
|
+
osHints.push("FreeBSD");
|
|
839
|
+
if (/windows/i.test(banner))
|
|
840
|
+
osHints.push("Windows");
|
|
841
|
+
if (/win32|win64/i.test(banner))
|
|
842
|
+
osHints.push("Windows");
|
|
843
|
+
if (/darwin|macos/i.test(banner))
|
|
844
|
+
osHints.push("macOS");
|
|
845
|
+
if (/suse/i.test(banner))
|
|
846
|
+
osHints.push("SUSE Linux");
|
|
847
|
+
if (/arch\s*linux/i.test(banner))
|
|
848
|
+
osHints.push("Arch Linux");
|
|
849
|
+
if (/amazon\s*linux/i.test(banner))
|
|
850
|
+
osHints.push("Amazon Linux");
|
|
851
|
+
// Determine final service identification
|
|
852
|
+
const service = matchedService?.service ?? additionalDetections[0]?.protocol ?? portProtocol ?? "unknown";
|
|
853
|
+
const software = matchedService?.software ?? additionalDetections[0]?.software ?? "Unknown";
|
|
854
|
+
const version = matchedService?.version ?? null;
|
|
855
|
+
// Confidence scoring
|
|
856
|
+
let confidence;
|
|
857
|
+
if (matchedService) {
|
|
858
|
+
confidence = version ? "high" : "medium";
|
|
859
|
+
}
|
|
860
|
+
else if (additionalDetections.length > 0) {
|
|
861
|
+
confidence = "medium";
|
|
862
|
+
}
|
|
863
|
+
else if (portProtocol) {
|
|
864
|
+
confidence = "low";
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
confidence = "low";
|
|
868
|
+
}
|
|
869
|
+
// Protocol determination
|
|
870
|
+
const protocol = matchedService?.service ?? additionalDetections[0]?.protocol ?? portProtocol ?? "unknown";
|
|
871
|
+
// Intelligence summary
|
|
872
|
+
const intelligence = [];
|
|
873
|
+
if (software !== "Unknown") {
|
|
874
|
+
intelligence.push(`Identified as ${software}${version ? ` version ${version}` : ""}`);
|
|
875
|
+
}
|
|
876
|
+
if (osHints.length > 0) {
|
|
877
|
+
intelligence.push(`OS hint: ${osHints.join(", ")}`);
|
|
878
|
+
}
|
|
879
|
+
if (matchedService && portProtocol && matchedService.service !== portProtocol) {
|
|
880
|
+
intelligence.push(`Service "${matchedService.service}" running on non-standard port ${port} (expected ${portProtocol})`);
|
|
881
|
+
}
|
|
882
|
+
if (version) {
|
|
883
|
+
intelligence.push("Version disclosed in banner - check for known vulnerabilities");
|
|
884
|
+
}
|
|
885
|
+
if (confidence === "low") {
|
|
886
|
+
intelligence.push("Low confidence identification - banner may be custom or obfuscated");
|
|
887
|
+
}
|
|
888
|
+
const result = {
|
|
889
|
+
banner: banner.substring(0, 500), // Truncate very long banners
|
|
890
|
+
port: port ?? null,
|
|
891
|
+
service,
|
|
892
|
+
software,
|
|
893
|
+
version,
|
|
894
|
+
osHint: osHints.length > 0 ? osHints.join(", ") : null,
|
|
895
|
+
protocol,
|
|
896
|
+
matchedPattern: matchedService?.matchedPattern ?? additionalDetections[0]?.pattern ?? null,
|
|
897
|
+
additionalDetections: additionalDetections.length > 0 && !matchedService ? additionalDetections : [],
|
|
898
|
+
confidence,
|
|
899
|
+
intelligence,
|
|
900
|
+
};
|
|
901
|
+
return json(result);
|
|
902
|
+
}
|
|
903
|
+
// ─── Tool Definitions ───
|
|
904
|
+
export const passiveTools = [
|
|
905
|
+
{
|
|
906
|
+
name: "fp_analyze_headers",
|
|
907
|
+
description: "Passive HTTP header fingerprinting. Analyzes pre-collected response headers for server identification (claimed vs header-order-based), CDN/cloud provider detection, WAF detection, security header audit with grading (A-F), infrastructure cookie decoding (F5 BIG-IP, NetScaler, AWS ALB), technology stack detection, and cache strategy analysis. No network requests are made.",
|
|
908
|
+
schema: {
|
|
909
|
+
headers: z
|
|
910
|
+
.record(z.string())
|
|
911
|
+
.describe("HTTP response headers as key-value pairs"),
|
|
912
|
+
url: z
|
|
913
|
+
.string()
|
|
914
|
+
.optional()
|
|
915
|
+
.describe("Original URL for context (not fetched)"),
|
|
916
|
+
},
|
|
917
|
+
execute: analyzeHeaders,
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
name: "fp_analyze_html",
|
|
921
|
+
description: "Passive HTML source fingerprinting. Analyzes raw HTML for client-side technology detection with version extraction and CVE cross-referencing, analytics/tracking code identification with ID extraction for cross-domain correlation, framework indicators (Next.js, Nuxt, Angular, React, Vue, Svelte, Astro), source map exposure, interesting HTML comments, hardcoded API endpoints and IP addresses, and build tool detection. No network requests are made.",
|
|
922
|
+
schema: {
|
|
923
|
+
html: z.string().describe("Raw HTML source to analyze"),
|
|
924
|
+
url: z
|
|
925
|
+
.string()
|
|
926
|
+
.optional()
|
|
927
|
+
.describe("Original URL for context (not fetched)"),
|
|
928
|
+
},
|
|
929
|
+
execute: analyzeHtml,
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
name: "fp_analyze_banner",
|
|
933
|
+
description: "Passive service banner fingerprinting. Analyzes a raw service banner string to identify the service software, extract version numbers, detect OS hints, and determine protocol. Matches against a database of known banner patterns for SSH, FTP, SMTP, HTTP, MySQL, PostgreSQL, Redis, MongoDB, Memcached, IMAP, POP3, and DNS. Uses port number as a tiebreaker for ambiguous banners. No network requests are made.",
|
|
934
|
+
schema: {
|
|
935
|
+
banner: z.string().describe("Raw service banner string to analyze"),
|
|
936
|
+
port: z
|
|
937
|
+
.number()
|
|
938
|
+
.optional()
|
|
939
|
+
.describe("Port number for protocol heuristics"),
|
|
940
|
+
},
|
|
941
|
+
execute: analyzeBanner,
|
|
942
|
+
},
|
|
943
|
+
];
|
|
944
|
+
//# sourceMappingURL=index.js.map
|