@q32/signal-scanner 0.1.0 → 0.2.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 (100) hide show
  1. package/COPYING +674 -0
  2. package/COPYING.LESSER +165 -0
  3. package/README.md +57 -9
  4. package/dist/cli.d.ts +26 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +592 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/dynamic.d.ts +43 -0
  9. package/dist/dynamic.d.ts.map +1 -0
  10. package/{src/dynamic.ts → dist/dynamic.js} +133 -156
  11. package/dist/dynamic.js.map +1 -0
  12. package/dist/feeds.d.ts +66 -0
  13. package/dist/feeds.d.ts.map +1 -0
  14. package/dist/feeds.js +259 -0
  15. package/dist/feeds.js.map +1 -0
  16. package/dist/index.d.ts +110 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +1251 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/intel.d.ts +72 -0
  21. package/dist/intel.d.ts.map +1 -0
  22. package/dist/intel.js +480 -0
  23. package/dist/intel.js.map +1 -0
  24. package/dist/node-tls.d.ts +8 -0
  25. package/dist/node-tls.d.ts.map +1 -0
  26. package/dist/node-tls.js +48 -0
  27. package/dist/node-tls.js.map +1 -0
  28. package/dist/render-isolate/entry.d.ts +2 -0
  29. package/dist/render-isolate/entry.d.ts.map +1 -0
  30. package/dist/render-isolate/entry.js +3 -0
  31. package/dist/render-isolate/entry.js.map +1 -0
  32. package/dist/render-isolate/polyfills.d.ts +2 -0
  33. package/dist/render-isolate/polyfills.d.ts.map +1 -0
  34. package/dist/render-isolate/polyfills.js +41 -0
  35. package/dist/render-isolate/polyfills.js.map +1 -0
  36. package/dist/render-isolate/run.d.ts +3 -0
  37. package/dist/render-isolate/run.d.ts.map +1 -0
  38. package/dist/render-isolate/run.js +88 -0
  39. package/dist/render-isolate/run.js.map +1 -0
  40. package/dist/render.d.ts +26 -0
  41. package/dist/render.d.ts.map +1 -0
  42. package/dist/render.js +248 -0
  43. package/dist/render.js.map +1 -0
  44. package/dist/rules/packs/binary.d.ts +4 -0
  45. package/dist/rules/packs/binary.d.ts.map +1 -0
  46. package/dist/rules/packs/binary.js +101 -0
  47. package/dist/rules/packs/binary.js.map +1 -0
  48. package/dist/rules/packs/css.d.ts +3 -0
  49. package/dist/rules/packs/css.d.ts.map +1 -0
  50. package/dist/rules/packs/css.js +43 -0
  51. package/dist/rules/packs/css.js.map +1 -0
  52. package/dist/rules/packs/decoders.d.ts +3 -0
  53. package/dist/rules/packs/decoders.d.ts.map +1 -0
  54. package/dist/rules/packs/decoders.js +46 -0
  55. package/dist/rules/packs/decoders.js.map +1 -0
  56. package/dist/rules/packs/html.d.ts +4 -0
  57. package/dist/rules/packs/html.d.ts.map +1 -0
  58. package/dist/rules/packs/html.js +227 -0
  59. package/dist/rules/packs/html.js.map +1 -0
  60. package/dist/rules/packs/index.d.ts +24 -0
  61. package/dist/rules/packs/index.d.ts.map +1 -0
  62. package/dist/rules/packs/index.js +75 -0
  63. package/dist/rules/packs/index.js.map +1 -0
  64. package/dist/rules/packs/script-risk.d.ts +4 -0
  65. package/dist/rules/packs/script-risk.d.ts.map +1 -0
  66. package/dist/rules/packs/script-risk.js +231 -0
  67. package/dist/rules/packs/script-risk.js.map +1 -0
  68. package/dist/rules/packs/source-code.d.ts +3 -0
  69. package/dist/rules/packs/source-code.d.ts.map +1 -0
  70. package/dist/rules/packs/source-code.js +179 -0
  71. package/dist/rules/packs/source-code.js.map +1 -0
  72. package/dist/rules/packs/urls.d.ts +3 -0
  73. package/dist/rules/packs/urls.d.ts.map +1 -0
  74. package/dist/rules/packs/urls.js +123 -0
  75. package/dist/rules/packs/urls.js.map +1 -0
  76. package/dist/rules/types.d.ts +34 -0
  77. package/dist/rules/types.d.ts.map +1 -0
  78. package/dist/rules/types.js +2 -0
  79. package/dist/rules/types.js.map +1 -0
  80. package/package.json +33 -18
  81. package/scripts/check-coverage.ts +0 -33
  82. package/scripts/eval.ts +0 -311
  83. package/scripts/render-isolate/entry.ts +0 -2
  84. package/scripts/render-isolate/polyfills.ts +0 -33
  85. package/scripts/render-isolate/run.ts +0 -63
  86. package/scripts/scan.ts +0 -612
  87. package/src/feeds.ts +0 -334
  88. package/src/index.ts +0 -1366
  89. package/src/intel.ts +0 -561
  90. package/src/node-tls.ts +0 -55
  91. package/src/render.ts +0 -233
  92. package/src/rules/packs/binary.ts +0 -103
  93. package/src/rules/packs/css.ts +0 -44
  94. package/src/rules/packs/decoders.ts +0 -47
  95. package/src/rules/packs/html.ts +0 -255
  96. package/src/rules/packs/index.ts +0 -76
  97. package/src/rules/packs/script-risk.ts +0 -236
  98. package/src/rules/packs/source-code.ts +0 -180
  99. package/src/rules/packs/urls.ts +0 -138
  100. package/src/rules/types.ts +0 -56
package/src/render.ts DELETED
@@ -1,233 +0,0 @@
1
- // Render-and-scan: build a REAL DOM (linkedom), run the page's inline AND
2
- // external scripts against it with our behavioral surfaces instrumented, then
3
- // hand back the rendered HTML (to re-scan with the static rules) plus a
4
- // BehaviorReport (exfil endpoints, runtime redirects, eval'd code, surfaced
5
- // URLs). This closes the gap where a credential form is injected by an external
6
- // JS bundle — invisible to inline-only analysis.
7
- //
8
- // linkedom replaces only the DOM; the instrumentation (fetch/XHR/sendBeacon/
9
- // location/eval/Function/atob/cookie) is layered ON the linkedom window so both
10
- // `fetch(...)` and `window.fetch(...)`/`window.location.href=` are recorded.
11
- //
12
- // linkedom + new Function run in any JS isolate (Node + workerd). For UNTRUSTED
13
- // pages the caller supplies a `run` that executes in a real sandbox (node:vm with
14
- // a timeout in the CLI; a globalOutbound:null Dynamic Worker in the Worker). The
15
- // default in-process runner is for trusted/synthetic use (tests).
16
-
17
- import { parseHTML } from "linkedom";
18
- import { extractInlineScripts, extractScriptSources, type BehaviorReport, type NetworkAttempt } from "./dynamic";
19
-
20
- const MAX_EXTERNAL_SCRIPTS = 8;
21
- const MAX_SCRIPT_BYTES = 512 * 1024;
22
-
23
- /** Self-contained input for the in-isolate core: HTML + already-fetched external script bodies. */
24
- export interface RenderInput {
25
- html: string;
26
- url?: string;
27
- externalScripts?: string[];
28
- }
29
-
30
- /** Runs renderDom — in-process by default, or inside an isolate (isolated-vm / Dynamic Worker). */
31
- export type RenderInvoke = (input: RenderInput) => RenderResult | Promise<RenderResult>;
32
-
33
- export interface RenderOptions {
34
- url?: string;
35
- /** Fetch an external script body (caller provides IO + egress). Omit to skip externals. */
36
- fetchScript?: (absoluteUrl: string) => Promise<string | null>;
37
- /** Where renderDom runs (caller's isolate). Default: in-process (trusted/synthetic use only). */
38
- invoke?: RenderInvoke;
39
- maxExternalScripts?: number;
40
- }
41
-
42
- export interface RenderResult {
43
- /** Serialized DOM after scripts ran — feed this to the static scanner. */
44
- html: string;
45
- /** Behaviors recorded while scripts ran. */
46
- report: BehaviorReport;
47
- }
48
-
49
- function emptyReport(): BehaviorReport {
50
- return { redirects: [], network: [], writes: [], evals: [], decoded: [], cookies: [], errors: [] };
51
- }
52
-
53
- // Host orchestrator: pre-fetch external scripts (IO stays on the host — the
54
- // isolate has no network), then run the pure renderDom core inside the caller's
55
- // isolate (or in-process by default).
56
- export async function renderAndScan(html: string, options: RenderOptions = {}): Promise<RenderResult> {
57
- let externalScripts: string[] = [];
58
- if (options.fetchScript) {
59
- const sources = extractScriptSources(html).slice(0, options.maxExternalScripts ?? MAX_EXTERNAL_SCRIPTS);
60
- externalScripts = (
61
- await Promise.all(
62
- sources.map(async (src) => {
63
- let absolute: string;
64
- try {
65
- absolute = new URL(src, options.url ?? "https://invalid.example/").toString();
66
- } catch {
67
- return "";
68
- }
69
- if (!/^https?:/i.test(absolute)) return "";
70
- try {
71
- const body = await options.fetchScript!(absolute);
72
- return (body ?? "").slice(0, MAX_SCRIPT_BYTES);
73
- } catch {
74
- return "";
75
- }
76
- })
77
- )
78
- ).filter(Boolean);
79
- }
80
- const invoke = options.invoke ?? renderDom;
81
- return await invoke({ html, url: options.url, externalScripts });
82
- }
83
-
84
- // The pure, self-contained core: build a real DOM, run inline + provided external
85
- // scripts against it with instrumented surfaces, return rendered HTML + behaviors.
86
- // No IO, no host-global mutation — safe to run in-process or bundled into an
87
- // isolate (isolated-vm / CF Dynamic Worker).
88
- export function renderDom(input: RenderInput): RenderResult {
89
- const report = emptyReport();
90
- let parsed: { document: any; window: any };
91
- try {
92
- parsed = parseHTML(input.html);
93
- } catch {
94
- report.errors.push("linkedom parse failed");
95
- return { html: input.html, report };
96
- }
97
- const globals = instrument(parsed.window, parsed.document, input.url, report);
98
- const scripts = [...extractInlineScripts(input.html), ...(input.externalScripts ?? [])];
99
- for (const body of scripts) {
100
- try {
101
- // eslint-disable-next-line no-new-func
102
- new Function(...Object.keys(globals), body)(...Object.values(globals));
103
- } catch (error) {
104
- report.errors.push(error instanceof Error ? error.message : "script error");
105
- }
106
- }
107
- let rendered = input.html;
108
- try {
109
- rendered = parsed.document.toString();
110
- } catch {
111
- /* keep raw html */
112
- }
113
- return { html: rendered, report };
114
- }
115
-
116
- // Install instrumented behavioral surfaces on the linkedom window AND return the
117
- // matching bare globals (so `fetch(...)` and `window.fetch(...)` both record).
118
- function instrument(window: any, document: any, url: string | undefined, report: BehaviorReport): Record<string, unknown> {
119
- const resolve = (value: unknown): string => {
120
- const raw = String(value ?? "");
121
- try {
122
- return url ? new URL(raw, url).toString() : raw;
123
- } catch {
124
- return raw;
125
- }
126
- };
127
- const pushNet = (kind: NetworkAttempt["kind"], target: unknown) => report.network.push({ kind, url: resolve(target) });
128
-
129
- const fetchStub = (input: unknown) => {
130
- pushNet("fetch", typeof input === "object" && input ? (input as any).url ?? input : input);
131
- return Promise.resolve({ ok: true, status: 200, text: () => Promise.resolve(""), json: () => Promise.resolve({}), headers: { get: () => null } });
132
- };
133
- const XHRStub = function (this: any) {
134
- this.open = (_method: string, target: string) => { this._url = target; };
135
- this.send = () => { if (this._url) pushNet("xhr", this._url); };
136
- this.setRequestHeader = () => {};
137
- this.addEventListener = () => {};
138
- };
139
- const beacon = (target: unknown) => { pushNet("beacon", target); return true; };
140
- const recordEval = (code: unknown) => { report.evals.push(String(code)); return undefined; };
141
- const FunctionStub = function (...args: unknown[]) { report.evals.push(String(args[args.length - 1] ?? "")); return function () {}; };
142
- const safeAtob = (value: unknown) => {
143
- let out: string;
144
- try { out = atob(String(value)); } catch { out = String(value); }
145
- report.decoded.push(out);
146
- return out;
147
- };
148
- const safeBtoa = (value: unknown) => { try { return btoa(String(value)); } catch { return String(value); } };
149
- // Expose the URL's real components — page JS routinely builds its redirect
150
- // target from window.location.search / pathname / origin (a cloaking bouncer
151
- // does `dest + location.search`). Missing them yields "undefined" in the URL
152
- // and we'd follow the wrong destination.
153
- let parsedLocation: URL | null = null;
154
- try { parsedLocation = url ? new URL(url) : null; } catch { parsedLocation = null; }
155
- const location = new Proxy(
156
- {
157
- href: url ?? "",
158
- origin: parsedLocation?.origin ?? "",
159
- protocol: parsedLocation?.protocol ?? "",
160
- host: parsedLocation?.host ?? "",
161
- hostname: parsedLocation?.hostname ?? "",
162
- port: parsedLocation?.port ?? "",
163
- pathname: parsedLocation?.pathname ?? "/",
164
- search: parsedLocation?.search ?? "",
165
- hash: parsedLocation?.hash ?? "",
166
- assign: (u: unknown) => report.redirects.push(resolve(u)),
167
- replace: (u: unknown) => report.redirects.push(resolve(u)),
168
- reload: () => {},
169
- toString: () => url ?? ""
170
- },
171
- { set: (target, prop, value) => { if (prop === "href") report.redirects.push(resolve(value)); (target as any)[prop] = value; return true; } }
172
- );
173
-
174
- // Override the surfaces the page reaches via the window object.
175
- try { Object.defineProperty(window, "location", { value: location, configurable: true, writable: true }); } catch { /* non-configurable */ }
176
- try { window.fetch = fetchStub; } catch {}
177
- try { window.XMLHttpRequest = XHRStub; } catch {}
178
- try { if (window.navigator) window.navigator.sendBeacon = beacon; } catch {}
179
- try { Object.defineProperty(document, "cookie", { configurable: true, get: () => "", set: (v: unknown) => { report.cookies.push(String(v)); } }); } catch {}
180
- // document.write/writeln: linkedom doesn't implement them, yet phishing kits
181
- // routinely inject their credential form this way. Materialize the markup into
182
- // the real DOM so the rendered output (re-scanned by the static rules) contains
183
- // the injected form/script — not just a string buried in a <script>.
184
- const writeMarkup = (markup: unknown) => {
185
- const target = document.body ?? document.documentElement;
186
- try { target?.insertAdjacentHTML("beforeend", String(markup ?? "")); } catch {}
187
- };
188
- try { document.write = writeMarkup; } catch {}
189
- try { document.writeln = (markup: unknown) => writeMarkup(String(markup ?? "") + "\n"); } catch {}
190
-
191
- const noop = () => {};
192
- const timerRun = (fn: unknown) => { try { if (typeof fn === "function") (fn as () => void)(); } catch {} return 0; };
193
- return {
194
- window,
195
- document,
196
- self: window,
197
- globalThis: window,
198
- top: window,
199
- parent: window,
200
- location,
201
- fetch: fetchStub,
202
- XMLHttpRequest: XHRStub,
203
- navigator: window.navigator ?? { userAgent: "Mozilla/5.0", language: "en-US", platform: "Win32", sendBeacon: beacon },
204
- screen: { width: 1920, height: 1080 },
205
- history: { pushState: noop, replaceState: noop },
206
- atob: safeAtob,
207
- btoa: safeBtoa,
208
- eval: recordEval,
209
- Function: FunctionStub,
210
- setTimeout: timerRun,
211
- setInterval: () => 0,
212
- clearTimeout: noop,
213
- clearInterval: noop,
214
- requestAnimationFrame: timerRun,
215
- queueMicrotask: (fn: unknown) => { try { (fn as () => void)(); } catch {} },
216
- console: { log: noop, warn: noop, error: noop, info: noop, debug: noop },
217
- MutationObserver: function (this: any) { this.observe = noop; this.disconnect = noop; },
218
- IntersectionObserver: function (this: any) { this.observe = noop; this.disconnect = noop; }
219
- };
220
- }
221
-
222
- function defaultRun(scripts: string[], globals: Record<string, unknown>): void {
223
- const names = Object.keys(globals);
224
- const values = names.map((name) => globals[name]);
225
- for (const body of scripts) {
226
- try {
227
- // eslint-disable-next-line no-new-func
228
- new Function(...names, body)(...values);
229
- } catch {
230
- /* malformed/strict-mode-conflicting script — skip, best effort */
231
- }
232
- }
233
- }
@@ -1,103 +0,0 @@
1
- import type { PatternRule, RuleDefinition } from "../types";
2
-
3
- export const binaryRules: Record<"elf_executable_magic" | "content_type_magic_mismatch" | "elf_writable_executable_stack", RuleDefinition> = {
4
- elf_executable_magic: {
5
- id: "elf_executable_magic",
6
- pack: "binary-static",
7
- severity: "high",
8
- confidence: "high",
9
- title: "ELF executable",
10
- description: "Content begins with ELF executable magic bytes.",
11
- locationType: "binary",
12
- score: { base: 55, tags: ["binary"] }
13
- },
14
- content_type_magic_mismatch: {
15
- id: "content_type_magic_mismatch",
16
- pack: "binary-static",
17
- severity: "high",
18
- confidence: "high",
19
- title: "Content type does not match magic bytes",
20
- description: "Declared content type conflicts with executable magic bytes.",
21
- locationType: "binary",
22
- score: { base: 45, tags: ["binary", "obfuscation"] }
23
- },
24
- elf_writable_executable_stack: {
25
- id: "elf_writable_executable_stack",
26
- pack: "binary-static",
27
- severity: "high",
28
- confidence: "medium",
29
- title: "ELF requests writable executable stack",
30
- description: "ELF program headers include a GNU_STACK segment with write and execute permissions.",
31
- locationType: "binary",
32
- score: { base: 32, tags: ["binary"] }
33
- }
34
- };
35
-
36
- export const binaryStringRules: PatternRule[] = [
37
- {
38
- id: "iot_botnet_family_strings",
39
- pack: "binary-static",
40
- severity: "high",
41
- confidence: "high",
42
- title: "IoT botnet family strings",
43
- description: "Binary strings reference IoT botnet family names or architecture payload naming.",
44
- locationType: "binary",
45
- pattern: /\b(?:Mozi|mirai|gafgyt|boatnet|Mozi\.[a-z0-9])\b/i,
46
- score: { base: 70, tags: ["binary"] }
47
- },
48
- {
49
- id: "iot_device_exploit_strings",
50
- pack: "binary-static",
51
- severity: "high",
52
- confidence: "medium",
53
- title: "IoT device exploit strings",
54
- description: "Binary strings reference common router, camera, TR-064, HNAP, GPON, or Realtek exploitation paths.",
55
- locationType: "binary",
56
- pattern: /\b(?:gpon8080|gpon80|realtek|netgear8080|netgear80|huawei|tr064|hnap|camcrossweb|camjaws|dlink|vacron|setup\.cgi|SOAPAction:|AddPortMapping|SetNTPServers)\b/i,
57
- score: { base: 42, tags: ["binary"] }
58
- },
59
- {
60
- id: "iot_payload_dropper_commands",
61
- pack: "binary-static",
62
- severity: "high",
63
- confidence: "high",
64
- title: "IoT payload dropper commands",
65
- description: "Binary strings contain wget/curl, chmod, temporary directory, and shell execution payload chains.",
66
- locationType: "binary",
67
- pattern: /(?:wget|curl|busybox wget)[\s\S]{0,160}(?:chmod|\/tmp|\/var\/tmp|\/dev\/shm)[\s\S]{0,160}(?:\/bin\/sh|sh\s|\.\/|Mozi\.)/i,
68
- score: { base: 64, tags: ["binary", "source"] }
69
- },
70
- {
71
- id: "router_management_hijack_commands",
72
- pack: "binary-static",
73
- severity: "high",
74
- confidence: "high",
75
- title: "Router management hijack commands",
76
- description: "Binary strings contain TR-069 or router management-server hijack commands.",
77
- locationType: "binary",
78
- pattern: /(?:cfgtool|sendcmd)[\s\S]{0,240}(?:ManagementServer|MgtServer|Tr069Enable|ConnectionRequestPassword|acsMozi|127\.0\.0\.1)/i,
79
- score: { base: 58, tags: ["binary"] }
80
- },
81
- {
82
- id: "firewall_lockout_commands",
83
- pack: "binary-static",
84
- severity: "medium",
85
- confidence: "high",
86
- title: "Firewall lockout commands",
87
- description: "Binary strings contain iptables rules that block management, TR-069, telnet, or SSH ports.",
88
- locationType: "binary",
89
- pattern: /iptables[\s\S]{0,120}(?:DROP|--dport|--sport|--destination-port|--source-port)[\s\S]{0,80}\b(?:22|23|2323|35000|50023|7547|58000)\b/i,
90
- score: { base: 34, tags: ["binary"] }
91
- },
92
- {
93
- id: "dht_cnc_protocol_strings",
94
- pack: "binary-static",
95
- severity: "medium",
96
- confidence: "high",
97
- title: "DHT/CNC protocol strings",
98
- description: "Binary strings contain DHT peer protocol and command-and-control markers.",
99
- locationType: "binary",
100
- pattern: /(?:\[cnc\]|\[atk\]|\[ud\]|\[dip\]|1:q9:find_node|1:q9:get_peers|1:q13:announce_peer|info_hash20|nodes6)/i,
101
- score: { base: 34, tags: ["binary"] }
102
- }
103
- ];
@@ -1,44 +0,0 @@
1
- import type { RuleDefinition } from "../types";
2
-
3
- export const cssRules: Record<"hidden_link_cluster" | "unicode_bidi_trick" | "css_imports_suspicious_domain" | "invisible_form_overlay", RuleDefinition> = {
4
- hidden_link_cluster: {
5
- id: "hidden_link_cluster",
6
- pack: "seo-spam",
7
- severity: "low",
8
- confidence: "medium",
9
- title: "Hidden CSS content",
10
- description: "CSS contains hidden or offscreen content patterns.",
11
- locationType: "css",
12
- score: { base: 4, tags: ["seo"] }
13
- },
14
- unicode_bidi_trick: {
15
- id: "unicode_bidi_trick",
16
- pack: "obfuscation",
17
- severity: "medium",
18
- confidence: "high",
19
- title: "Unicode bidi CSS trick",
20
- description: "CSS uses bidi override, which can hide or reorder visible text.",
21
- locationType: "css",
22
- score: { base: 20, tags: ["obfuscation"] }
23
- },
24
- css_imports_suspicious_domain: {
25
- id: "css_imports_suspicious_domain",
26
- pack: "script-risk",
27
- severity: "medium",
28
- confidence: "medium",
29
- title: "CSS imports off-site resource",
30
- description: "CSS imports or loads an off-site URL.",
31
- locationType: "url",
32
- score: { base: 12, tags: ["script", "url"] }
33
- },
34
- invisible_form_overlay: {
35
- id: "invisible_form_overlay",
36
- pack: "phishing",
37
- severity: "medium",
38
- confidence: "medium",
39
- title: "Invisible form overlay style",
40
- description: "CSS contains fixed/absolute overlay and invisibility patterns that can hide or intercept form input.",
41
- locationType: "css",
42
- score: { base: 24, tags: ["credential", "phishing"] }
43
- }
44
- };
@@ -1,47 +0,0 @@
1
- import type { RuleDefinition } from "../types";
2
-
3
- export const decodedArtifactRules: Record<"large_base64_blob" | "javascript_hex_escapes" | "javascript_unicode_escapes" | "fromcharcode_decoded_string", RuleDefinition> = {
4
- large_base64_blob: {
5
- id: "large_base64_blob",
6
- pack: "obfuscation",
7
- severity: "medium",
8
- confidence: "medium",
9
- title: "Decoded base64 artifact",
10
- description: "Scanner decoded a base64 artifact and rescanned it.",
11
- locationType: "decoded_artifact",
12
- score: { base: 14, tags: ["decoded", "obfuscation"] }
13
- },
14
- javascript_hex_escapes: {
15
- id: "javascript_hex_escapes",
16
- pack: "obfuscation",
17
- severity: "medium",
18
- confidence: "medium",
19
- title: "Decoded JavaScript hex escapes",
20
- description: "Scanner decoded JavaScript hex escapes and rescanned the artifact.",
21
- locationType: "decoded_artifact",
22
- score: { base: 18, tags: ["decoded", "obfuscation"] }
23
- },
24
- javascript_unicode_escapes: {
25
- id: "javascript_unicode_escapes",
26
- pack: "obfuscation",
27
- severity: "low",
28
- confidence: "medium",
29
- title: "Decoded JavaScript unicode escapes",
30
- description: "Scanner decoded JavaScript unicode escapes and rescanned the artifact.",
31
- locationType: "decoded_artifact",
32
- // Unicode escapes are ubiquitous in legitimate minified/i18n JS. The mere
33
- // presence is weak — the conviction comes from rescanning the DECODED
34
- // artifact (whose own findings fire separately), not from this marker.
35
- score: { base: 8, tags: ["decoded", "obfuscation"] }
36
- },
37
- fromcharcode_decoded_string: {
38
- id: "fromcharcode_decoded_string",
39
- pack: "obfuscation",
40
- severity: "medium",
41
- confidence: "medium",
42
- title: "Decoded String.fromCharCode artifact",
43
- description: "Scanner decoded a literal String.fromCharCode artifact and rescanned it.",
44
- locationType: "decoded_artifact",
45
- score: { base: 22, tags: ["decoded", "obfuscation"] }
46
- }
47
- };
@@ -1,255 +0,0 @@
1
- import type { RuleDefinition } from "../types";
2
-
3
- export const htmlRules: Record<
4
- | "external_script_from_unrelated_domain"
5
- | "mixed_content_script"
6
- | "hidden_iframe_off_origin"
7
- | "meta_refresh_external"
8
- | "password_form_without_https"
9
- | "credential_form_posts_off_origin"
10
- | "card_fields_plus_external_script"
11
- | "excessive_external_scripts_on_login_page"
12
- | "login_page_with_punycode_links"
13
- | "credential_ui_rendered_as_image"
14
- | "crypto_wallet_login_language"
15
- | "crypto_trading_landing_language"
16
- | "seo_trademark_stuffing"
17
- | "credential_form_on_suspicious_host"
18
- | "brand_impersonation_content",
19
- RuleDefinition
20
- > = {
21
- external_script_from_unrelated_domain: {
22
- id: "external_script_from_unrelated_domain",
23
- pack: "script-risk",
24
- severity: "medium",
25
- confidence: "medium",
26
- title: "External script from unrelated domain",
27
- description: "HTML loads a script from an off-site domain.",
28
- locationType: "url",
29
- score: { base: 8, tags: ["script", "url"], repeatMultiplier: 0.1, maxRepeats: 3 }
30
- },
31
- mixed_content_script: {
32
- id: "mixed_content_script",
33
- pack: "script-risk",
34
- severity: "medium",
35
- confidence: "high",
36
- title: "Mixed-content script",
37
- description: "HTTPS page loads a script over HTTP.",
38
- locationType: "url",
39
- // A real injection vector, but a hygiene issue on its own (browsers block it)
40
- // — shouldn't convict a site as malicious without corroborating signal.
41
- score: { base: 30, tags: ["script", "url"] }
42
- },
43
- hidden_iframe_off_origin: {
44
- id: "hidden_iframe_off_origin",
45
- pack: "phishing",
46
- severity: "high",
47
- confidence: "high",
48
- title: "Hidden off-origin iframe",
49
- description: "HTML contains a hidden iframe pointed at an off-origin URL.",
50
- locationType: "url",
51
- score: { base: 70, tags: ["phishing", "url"] }
52
- },
53
- meta_refresh_external: {
54
- id: "meta_refresh_external",
55
- pack: "redirects",
56
- severity: "medium",
57
- confidence: "medium",
58
- title: "Meta refresh to external URL",
59
- description: "HTML redirects with a meta refresh to an off-site URL.",
60
- locationType: "url",
61
- score: { base: 25, tags: ["redirect", "url"] }
62
- },
63
- password_form_without_https: {
64
- id: "password_form_without_https",
65
- pack: "phishing",
66
- severity: "high",
67
- confidence: "high",
68
- title: "Password form without HTTPS",
69
- description: "Page contains a password form on an HTTP origin.",
70
- locationType: "html",
71
- score: { base: 70, tags: ["credential", "phishing"] }
72
- },
73
- credential_form_posts_off_origin: {
74
- id: "credential_form_posts_off_origin",
75
- pack: "phishing",
76
- severity: "high",
77
- confidence: "high",
78
- title: "Credential form posts off origin",
79
- description: "A form with a password field submits to an off-origin URL.",
80
- locationType: "url",
81
- score: { base: 82, tags: ["credential", "phishing", "url"] }
82
- },
83
- card_fields_plus_external_script: {
84
- id: "card_fields_plus_external_script",
85
- pack: "payment",
86
- severity: "high",
87
- confidence: "medium",
88
- title: "Payment fields with external resources",
89
- description: "Page contains payment fields and off-site resources.",
90
- locationType: "html",
91
- score: { base: 72, tags: ["payment", "script", "url"] }
92
- },
93
- excessive_external_scripts_on_login_page: {
94
- id: "excessive_external_scripts_on_login_page",
95
- pack: "phishing",
96
- severity: "medium",
97
- confidence: "medium",
98
- title: "Excessive external scripts on login/payment page",
99
- description: "Login or payment page loads many off-site scripts.",
100
- locationType: "aggregate",
101
- score: { base: 14, tags: ["phishing", "script"] }
102
- },
103
- login_page_with_punycode_links: {
104
- id: "login_page_with_punycode_links",
105
- pack: "phishing",
106
- severity: "high",
107
- confidence: "high",
108
- title: "Login page with punycode links",
109
- description: "Login-like page references punycode URLs.",
110
- locationType: "aggregate",
111
- score: { base: 76, tags: ["phishing", "url"] }
112
- },
113
- credential_ui_rendered_as_image: {
114
- id: "credential_ui_rendered_as_image",
115
- pack: "phishing",
116
- severity: "medium",
117
- confidence: "high",
118
- title: "Credential UI rendered as image",
119
- description: "Page model or markup references a screenshot/image that appears to contain a login or credential form.",
120
- locationType: "html",
121
- score: { base: 34, tags: ["credential", "phishing"] }
122
- },
123
- crypto_wallet_login_language: {
124
- id: "crypto_wallet_login_language",
125
- pack: "phishing",
126
- severity: "medium",
127
- confidence: "high",
128
- title: "Crypto wallet login language",
129
- description: "Page model or markup contains crypto/wallet language in login, account, or access context.",
130
- locationType: "html",
131
- score: { base: 22, tags: ["phishing", "wallet"] }
132
- },
133
- crypto_trading_landing_language: {
134
- id: "crypto_trading_landing_language",
135
- pack: "phishing",
136
- severity: "low",
137
- confidence: "medium",
138
- title: "Crypto or DeFi trading landing language",
139
- description: "Page model or markup contains multiple crypto, DeFi, exchange, swap, trading, or liquidity terms.",
140
- locationType: "html",
141
- score: { base: 6, tags: ["phishing", "wallet"] }
142
- },
143
- seo_trademark_stuffing: {
144
- id: "seo_trademark_stuffing",
145
- pack: "phishing",
146
- severity: "high",
147
- confidence: "medium",
148
- title: "SEO trademark stuffing",
149
- description: "Page title or SEO model overuses trademark symbols in a way commonly seen on impersonation landing pages.",
150
- locationType: "html",
151
- score: { base: 64, tags: ["phishing", "seo"] }
152
- },
153
- credential_form_on_suspicious_host: {
154
- id: "credential_form_on_suspicious_host",
155
- pack: "phishing",
156
- severity: "high",
157
- confidence: "high",
158
- title: "Credential form on suspicious host",
159
- description: "Page contains credential fields on a generated, shared-hosting, suspicious-path, or redirected host.",
160
- locationType: "html",
161
- score: { base: 72, tags: ["credential", "hosting", "phishing"] }
162
- },
163
- brand_impersonation_content: {
164
- id: "brand_impersonation_content",
165
- pack: "phishing",
166
- severity: "high",
167
- confidence: "high",
168
- title: "Page mimics a brand and captures credentials",
169
- description: "Page content prominently references a well-known brand and presents a credential field, but is served from a domain that does not belong to that brand — the core credential-phishing pattern, independent of the URL.",
170
- locationType: "html",
171
- score: { base: 68, tags: ["credential", "phishing"] }
172
- }
173
- };
174
-
175
- export const htmlTechnologyRules: Record<
176
- | "legacy_jquery_reference"
177
- | "legacy_angularjs_reference"
178
- | "legacy_bootstrap_reference"
179
- | "legacy_lodash_reference"
180
- | "wordpress_surface_reference"
181
- | "drupal_surface_reference"
182
- | "phpmyadmin_surface_reference",
183
- RuleDefinition
184
- > = {
185
- legacy_jquery_reference: {
186
- id: "legacy_jquery_reference",
187
- pack: "dependency-fingerprint",
188
- severity: "low",
189
- confidence: "medium",
190
- title: "Legacy jQuery reference",
191
- description: "A script URL or source text references a legacy jQuery major version.",
192
- locationType: "url",
193
- score: { base: 4, tags: ["dependency"] }
194
- },
195
- legacy_angularjs_reference: {
196
- id: "legacy_angularjs_reference",
197
- pack: "dependency-fingerprint",
198
- severity: "low",
199
- confidence: "medium",
200
- title: "Legacy AngularJS reference",
201
- description: "A script URL or source text references AngularJS 1.x.",
202
- locationType: "url",
203
- score: { base: 6, tags: ["dependency"] }
204
- },
205
- legacy_bootstrap_reference: {
206
- id: "legacy_bootstrap_reference",
207
- pack: "dependency-fingerprint",
208
- severity: "low",
209
- confidence: "medium",
210
- title: "Legacy Bootstrap reference",
211
- description: "A script URL or source text references Bootstrap 3.x.",
212
- locationType: "url",
213
- score: { base: 4, tags: ["dependency"] }
214
- },
215
- legacy_lodash_reference: {
216
- id: "legacy_lodash_reference",
217
- pack: "dependency-fingerprint",
218
- severity: "low",
219
- confidence: "medium",
220
- title: "Legacy lodash reference",
221
- description: "A script URL or source text references lodash versions commonly covered by dependency scanners.",
222
- locationType: "url",
223
- score: { base: 4, tags: ["dependency"] }
224
- },
225
- wordpress_surface_reference: {
226
- id: "wordpress_surface_reference",
227
- pack: "technology-fingerprint",
228
- severity: "info",
229
- confidence: "medium",
230
- title: "WordPress surface reference",
231
- description: "HTML references common WordPress paths or generator metadata.",
232
- locationType: "html",
233
- score: { base: 2, tags: ["technology"] }
234
- },
235
- drupal_surface_reference: {
236
- id: "drupal_surface_reference",
237
- pack: "technology-fingerprint",
238
- severity: "info",
239
- confidence: "medium",
240
- title: "Drupal surface reference",
241
- description: "HTML or script text references common Drupal surface fingerprints.",
242
- locationType: "html",
243
- score: { base: 2, tags: ["technology"] }
244
- },
245
- phpmyadmin_surface_reference: {
246
- id: "phpmyadmin_surface_reference",
247
- pack: "technology-fingerprint",
248
- severity: "info",
249
- confidence: "medium",
250
- title: "phpMyAdmin surface reference",
251
- description: "HTML references common phpMyAdmin surface fingerprints.",
252
- locationType: "html",
253
- score: { base: 8, tags: ["technology"] }
254
- }
255
- };