@obfuscan/core 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.
Files changed (131) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +224 -0
  3. package/dist/allowlist.d.ts +25 -0
  4. package/dist/allowlist.d.ts.map +1 -0
  5. package/dist/allowlist.js +138 -0
  6. package/dist/allowlist.js.map +1 -0
  7. package/dist/detectors/bidi-control.d.ts +14 -0
  8. package/dist/detectors/bidi-control.d.ts.map +1 -0
  9. package/dist/detectors/bidi-control.js +67 -0
  10. package/dist/detectors/bidi-control.js.map +1 -0
  11. package/dist/detectors/cargo-build-rs-network.d.ts +12 -0
  12. package/dist/detectors/cargo-build-rs-network.d.ts.map +1 -0
  13. package/dist/detectors/cargo-build-rs-network.js +54 -0
  14. package/dist/detectors/cargo-build-rs-network.js.map +1 -0
  15. package/dist/detectors/decode-then-exec.d.ts +20 -0
  16. package/dist/detectors/decode-then-exec.d.ts.map +1 -0
  17. package/dist/detectors/decode-then-exec.js +189 -0
  18. package/dist/detectors/decode-then-exec.js.map +1 -0
  19. package/dist/detectors/deserializer-untrusted.d.ts +15 -0
  20. package/dist/detectors/deserializer-untrusted.d.ts.map +1 -0
  21. package/dist/detectors/deserializer-untrusted.js +99 -0
  22. package/dist/detectors/deserializer-untrusted.js.map +1 -0
  23. package/dist/detectors/dockerfile-curl-pipe-shell.d.ts +10 -0
  24. package/dist/detectors/dockerfile-curl-pipe-shell.d.ts.map +1 -0
  25. package/dist/detectors/dockerfile-curl-pipe-shell.js +42 -0
  26. package/dist/detectors/dockerfile-curl-pipe-shell.js.map +1 -0
  27. package/dist/detectors/dynamic-exec-non-literal.d.ts +17 -0
  28. package/dist/detectors/dynamic-exec-non-literal.d.ts.map +1 -0
  29. package/dist/detectors/dynamic-exec-non-literal.js +104 -0
  30. package/dist/detectors/dynamic-exec-non-literal.js.map +1 -0
  31. package/dist/detectors/encoded-array-fingerprint.d.ts +11 -0
  32. package/dist/detectors/encoded-array-fingerprint.d.ts.map +1 -0
  33. package/dist/detectors/encoded-array-fingerprint.js +60 -0
  34. package/dist/detectors/encoded-array-fingerprint.js.map +1 -0
  35. package/dist/detectors/gha-curl-pipe-shell.d.ts +11 -0
  36. package/dist/detectors/gha-curl-pipe-shell.d.ts.map +1 -0
  37. package/dist/detectors/gha-curl-pipe-shell.js +42 -0
  38. package/dist/detectors/gha-curl-pipe-shell.js.map +1 -0
  39. package/dist/detectors/high-entropy-literal.d.ts +19 -0
  40. package/dist/detectors/high-entropy-literal.d.ts.map +1 -0
  41. package/dist/detectors/high-entropy-literal.js +90 -0
  42. package/dist/detectors/high-entropy-literal.js.map +1 -0
  43. package/dist/detectors/homoglyph-identifier.d.ts +16 -0
  44. package/dist/detectors/homoglyph-identifier.d.ts.map +1 -0
  45. package/dist/detectors/homoglyph-identifier.js +76 -0
  46. package/dist/detectors/homoglyph-identifier.js.map +1 -0
  47. package/dist/detectors/index.d.ts +31 -0
  48. package/dist/detectors/index.d.ts.map +1 -0
  49. package/dist/detectors/index.js +60 -0
  50. package/dist/detectors/index.js.map +1 -0
  51. package/dist/detectors/library-load-non-literal.d.ts +10 -0
  52. package/dist/detectors/library-load-non-literal.d.ts.map +1 -0
  53. package/dist/detectors/library-load-non-literal.js +72 -0
  54. package/dist/detectors/library-load-non-literal.js.map +1 -0
  55. package/dist/detectors/long-line.d.ts +12 -0
  56. package/dist/detectors/long-line.d.ts.map +1 -0
  57. package/dist/detectors/long-line.js +53 -0
  58. package/dist/detectors/long-line.js.map +1 -0
  59. package/dist/detectors/manifest-install-script.d.ts +54 -0
  60. package/dist/detectors/manifest-install-script.d.ts.map +1 -0
  61. package/dist/detectors/manifest-install-script.js +272 -0
  62. package/dist/detectors/manifest-install-script.js.map +1 -0
  63. package/dist/detectors/network-then-exec.d.ts +17 -0
  64. package/dist/detectors/network-then-exec.d.ts.map +1 -0
  65. package/dist/detectors/network-then-exec.js +140 -0
  66. package/dist/detectors/network-then-exec.js.map +1 -0
  67. package/dist/detectors/perl-makefile-side-effect.d.ts +17 -0
  68. package/dist/detectors/perl-makefile-side-effect.d.ts.map +1 -0
  69. package/dist/detectors/perl-makefile-side-effect.js +72 -0
  70. package/dist/detectors/perl-makefile-side-effect.js.map +1 -0
  71. package/dist/detectors/python-setup-side-effect.d.ts +10 -0
  72. package/dist/detectors/python-setup-side-effect.d.ts.map +1 -0
  73. package/dist/detectors/python-setup-side-effect.js +87 -0
  74. package/dist/detectors/python-setup-side-effect.js.map +1 -0
  75. package/dist/detectors/shell-untrusted-input.d.ts +10 -0
  76. package/dist/detectors/shell-untrusted-input.d.ts.map +1 -0
  77. package/dist/detectors/shell-untrusted-input.js +76 -0
  78. package/dist/detectors/shell-untrusted-input.js.map +1 -0
  79. package/dist/detectors/string-array-decoder.d.ts +15 -0
  80. package/dist/detectors/string-array-decoder.d.ts.map +1 -0
  81. package/dist/detectors/string-array-decoder.js +70 -0
  82. package/dist/detectors/string-array-decoder.js.map +1 -0
  83. package/dist/detectors/suspicious-io-cluster.d.ts +11 -0
  84. package/dist/detectors/suspicious-io-cluster.d.ts.map +1 -0
  85. package/dist/detectors/suspicious-io-cluster.js +86 -0
  86. package/dist/detectors/suspicious-io-cluster.js.map +1 -0
  87. package/dist/diff.d.ts +23 -0
  88. package/dist/diff.d.ts.map +1 -0
  89. package/dist/diff.js +144 -0
  90. package/dist/diff.js.map +1 -0
  91. package/dist/directives.d.ts +33 -0
  92. package/dist/directives.d.ts.map +1 -0
  93. package/dist/directives.js +60 -0
  94. package/dist/directives.js.map +1 -0
  95. package/dist/errors.d.ts +19 -0
  96. package/dist/errors.d.ts.map +1 -0
  97. package/dist/errors.js +16 -0
  98. package/dist/errors.js.map +1 -0
  99. package/dist/grammar/query.d.ts +44 -0
  100. package/dist/grammar/query.d.ts.map +1 -0
  101. package/dist/grammar/query.js +24 -0
  102. package/dist/grammar/query.js.map +1 -0
  103. package/dist/index.d.ts +101 -0
  104. package/dist/index.d.ts.map +1 -0
  105. package/dist/index.js +106 -0
  106. package/dist/index.js.map +1 -0
  107. package/dist/internal/patterns.d.ts +48 -0
  108. package/dist/internal/patterns.d.ts.map +1 -0
  109. package/dist/internal/patterns.js +95 -0
  110. package/dist/internal/patterns.js.map +1 -0
  111. package/dist/internal/text.d.ts +14 -0
  112. package/dist/internal/text.d.ts.map +1 -0
  113. package/dist/internal/text.js +20 -0
  114. package/dist/internal/text.js.map +1 -0
  115. package/dist/rules.d.ts +25 -0
  116. package/dist/rules.d.ts.map +1 -0
  117. package/dist/rules.js +195 -0
  118. package/dist/rules.js.map +1 -0
  119. package/dist/scan.d.ts +26 -0
  120. package/dist/scan.d.ts.map +1 -0
  121. package/dist/scan.js +287 -0
  122. package/dist/scan.js.map +1 -0
  123. package/dist/types.d.ts +215 -0
  124. package/dist/types.d.ts.map +1 -0
  125. package/dist/types.js +8 -0
  126. package/dist/types.js.map +1 -0
  127. package/dist/version.d.ts +10 -0
  128. package/dist/version.d.ts.map +1 -0
  129. package/dist/version.js +50 -0
  130. package/dist/version.js.map +1 -0
  131. package/package.json +74 -0
@@ -0,0 +1,104 @@
1
+ /**
2
+ * obf.dynamic-exec-with-non-literal.<lang> — Layer B.
3
+ *
4
+ * Fires when a dynamic-exec sink is called with an argument that is not a
5
+ * string/numeric literal. `eval("1+1")` is uninteresting; `eval(s)` is.
6
+ *
7
+ * The "is the argument a literal?" check is a syntactic approximation: we
8
+ * peek at the first non-whitespace character after the sink's `(`. If it's
9
+ * a quote or a digit, we treat the call as literal. Anything else — including
10
+ * template strings with `${}` interpolation — is non-literal.
11
+ *
12
+ * Pure template strings without interpolation (e.g. `eval(\`1+1\`)`) are
13
+ * treated as literal and not flagged.
14
+ */
15
+ import { lineAtOffset, MAX_FINDINGS_PER_DETECTOR, MAX_SOURCE_BYTES, namedCallAlternation, } from "../internal/patterns.js";
16
+ import { truncateSnippet } from "../internal/text.js";
17
+ const cache = new WeakMap();
18
+ function compile(config) {
19
+ const cached = cache.get(config);
20
+ if (cached)
21
+ return cached;
22
+ const sinks = namedCallAlternation(config.dynamic_exec_sinks);
23
+ // Capture the sink name and the first ~6 chars after `(` to inspect.
24
+ const call = new RegExp(`(?:^|[^A-Za-z0-9_$])((?:${sinks}))\\s*\\(([\\s\\S]{0,12})`, "g");
25
+ const compiled = { call };
26
+ cache.set(config, compiled);
27
+ return compiled;
28
+ }
29
+ function isLiteralPeek(peek) {
30
+ const trimmed = peek.replace(/^\s+/, "");
31
+ if (trimmed.length === 0)
32
+ return false;
33
+ const c = trimmed[0];
34
+ // Numeric, single/double quote — definitely literal
35
+ if (c === '"' || c === "'")
36
+ return true;
37
+ if (c >= "0" && c <= "9")
38
+ return true;
39
+ if (c === "-" && trimmed[1] && trimmed[1] >= "0" && trimmed[1] <= "9")
40
+ return true;
41
+ // Pure template literal with no `${`: still literal.
42
+ if (c === "`") {
43
+ return !trimmed.includes("${");
44
+ }
45
+ return false;
46
+ }
47
+ export const dynamicExecNonLiteral = {
48
+ id: "obf.dynamic-exec-with-non-literal",
49
+ docsUrl: "https://github.com/bytebardorg/obfuscan/blob/main/docs/detectors.md#obfdynamic-exec-with-non-literal",
50
+ applies(ctx) {
51
+ return (ctx.config !== null &&
52
+ ctx.config.dynamic_exec_sinks.length > 0 &&
53
+ ctx.source.length > 0 &&
54
+ ctx.source.length < MAX_SOURCE_BYTES);
55
+ },
56
+ run(ctx) {
57
+ if (!ctx.config)
58
+ return [];
59
+ const cfg = ctx.config;
60
+ const src = ctx.source;
61
+ const { call } = compile(cfg);
62
+ const findings = [];
63
+ const seen = new Set();
64
+ const re = new RegExp(call.source, call.flags);
65
+ let m;
66
+ while ((m = re.exec(src)) !== null) {
67
+ if (findings.length >= MAX_FINDINGS_PER_DETECTOR)
68
+ break;
69
+ const sinkName = m[1] ?? "";
70
+ const peek = m[2] ?? "";
71
+ if (isLiteralPeek(peek))
72
+ continue;
73
+ // Skip when the next thing looks like another decoder call — that's
74
+ // the decode-then-exec detector's job.
75
+ if (/^[A-Za-z_$][\w$.]*\s*\(/.test(peek.replace(/^\s+/, ""))) {
76
+ // It's a function call argument; only flag if the function name is
77
+ // not in this language's decoders list (otherwise decode-then-exec
78
+ // owns the finding).
79
+ const fnNameMatch = /^([A-Za-z_$][\w$.]*)/.exec(peek.replace(/^\s+/, ""));
80
+ const fnName = fnNameMatch?.[1] ?? "";
81
+ if (cfg.decoders.some(d => fnName === d || fnName.endsWith("." + d)))
82
+ continue;
83
+ }
84
+ const offset = m.index + (m[0].length - peek.length);
85
+ const line = lineAtOffset(src, offset);
86
+ if (seen.has(line))
87
+ continue;
88
+ seen.add(line);
89
+ findings.push({
90
+ ruleId: `obf.dynamic-exec-with-non-literal.${cfg.id}`,
91
+ severity: "warn",
92
+ score: 7,
93
+ file: ctx.path,
94
+ line,
95
+ snippet: truncateSnippet(`${sinkName}(${peek}`),
96
+ reason: `Dynamic-exec sink \`${sinkName}\` called with a non-literal argument. ` +
97
+ `Confirm the input cannot be attacker-influenced.`,
98
+ evidence: { language: cfg.id, sink: sinkName },
99
+ });
100
+ }
101
+ return findings;
102
+ },
103
+ };
104
+ //# sourceMappingURL=dynamic-exec-non-literal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dynamic-exec-non-literal.js","sourceRoot":"","sources":["../../src/detectors/dynamic-exec-non-literal.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * obf.encoded-array-fingerprint — Layer A.
3
+ *
4
+ * Detects the lexical fingerprint of obfuscator.io / javascript-obfuscator
5
+ * output: a long array of high-entropy strings, often assigned to a hex-style
6
+ * identifier (`_0xabc12`). This fires before any AST analysis and is one of
7
+ * the strongest single signals for packed JS payloads.
8
+ */
9
+ import type { Detector } from "../types.js";
10
+ export declare const encodedArrayFingerprint: Detector;
11
+ //# sourceMappingURL=encoded-array-fingerprint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encoded-array-fingerprint.d.ts","sourceRoot":"","sources":["../../src/detectors/encoded-array-fingerprint.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAwB,MAAM,aAAa,CAAC;AAkBlE,eAAO,MAAM,uBAAuB,EAAE,QAuCrC,CAAC"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * obf.encoded-array-fingerprint — Layer A.
3
+ *
4
+ * Detects the lexical fingerprint of obfuscator.io / javascript-obfuscator
5
+ * output: a long array of high-entropy strings, often assigned to a hex-style
6
+ * identifier (`_0xabc12`). This fires before any AST analysis and is one of
7
+ * the strongest single signals for packed JS payloads.
8
+ */
9
+ import { lineAtOffset, MAX_SOURCE_BYTES } from "../internal/patterns.js";
10
+ import { truncateSnippet } from "../internal/text.js";
11
+ // `[ "...", "...", ... ]` arrays with at least N quoted-string elements.
12
+ // Backquotes excluded — template literals don't show up in obfuscator arrays.
13
+ const ARRAY_RE = /\[\s*((?:"[^"\n]{4,}"|'[^'\n]{4,}')(?:\s*,\s*(?:"[^"\n]{4,}"|'[^'\n]{4,}')){15,})\s*\]/g;
14
+ const MIN_BASE64_RATIO = 0.6;
15
+ const BASE64_CHAR = /[A-Za-z0-9+/=]/;
16
+ function looksBase64ish(s) {
17
+ if (s.length < 8)
18
+ return false;
19
+ let hits = 0;
20
+ for (const c of s)
21
+ if (BASE64_CHAR.test(c))
22
+ hits++;
23
+ return hits / s.length >= MIN_BASE64_RATIO;
24
+ }
25
+ export const encodedArrayFingerprint = {
26
+ id: "obf.encoded-array-fingerprint",
27
+ docsUrl: "https://github.com/bytebardorg/obfuscan/blob/main/docs/detectors.md#obfencoded-array-fingerprint",
28
+ applies(ctx) {
29
+ return ctx.source.length > 0 && ctx.source.length < MAX_SOURCE_BYTES;
30
+ },
31
+ run(ctx) {
32
+ const findings = [];
33
+ const src = ctx.source;
34
+ const re = new RegExp(ARRAY_RE.source, ARRAY_RE.flags);
35
+ let m;
36
+ while ((m = re.exec(src)) !== null) {
37
+ const elements = (m[1] ?? "")
38
+ .split(/,/)
39
+ .map(s => s.trim().replace(/^["']|["']$/g, ""));
40
+ const base64ish = elements.filter(looksBase64ish).length;
41
+ if (base64ish / elements.length < MIN_BASE64_RATIO)
42
+ continue;
43
+ const line = lineAtOffset(src, m.index);
44
+ findings.push({
45
+ ruleId: encodedArrayFingerprint.id,
46
+ severity: "warn",
47
+ score: 7,
48
+ file: ctx.path,
49
+ line,
50
+ snippet: truncateSnippet(m[0]),
51
+ reason: `Large array of encoded-looking strings (${elements.length} entries, ` +
52
+ `${base64ish} base64-shaped). This is the obfuscator.io / ` +
53
+ `javascript-obfuscator string-table fingerprint.`,
54
+ evidence: { length: elements.length, base64ishCount: base64ish },
55
+ });
56
+ }
57
+ return findings;
58
+ },
59
+ };
60
+ //# sourceMappingURL=encoded-array-fingerprint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encoded-array-fingerprint.js","sourceRoot":"","sources":["../../src/detectors/encoded-array-fingerprint.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AACzE,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,yEAAyE;AACzE,8EAA8E;AAC9E,MAAM,QAAQ,GAAG,yFAAyF,CAAC;AAE3G,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAC7B,MAAM,WAAW,GAAG,gBAAgB,CAAC;AAErC,SAAS,cAAc,CAAC,CAAS;IAC/B,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAC/B,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,CAAC,IAAI,CAAC;QAAE,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;YAAE,IAAI,EAAE,CAAC;IACnD,OAAO,IAAI,GAAG,CAAC,CAAC,MAAM,IAAI,gBAAgB,CAAC;AAC7C,CAAC;AAED,MAAM,CAAC,MAAM,uBAAuB,GAAa;IAC/C,EAAE,EAAE,+BAA+B;IACnC,OAAO,EAAE,kGAAkG;IAE3G,OAAO,CAAC,GAAgB;QACtB,OAAO,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,gBAAgB,CAAC;IACvE,CAAC;IAED,GAAG,CAAC,GAAgB;QAClB,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC;QACvB,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;QAEvD,IAAI,CAAyB,CAAC;QAC9B,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YACnC,MAAM,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;iBAC1B,KAAK,CAAC,GAAG,CAAC;iBACV,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,CAAC;YAElD,MAAM,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,MAAM,CAAC;YACzD,IAAI,SAAS,GAAG,QAAQ,CAAC,MAAM,GAAG,gBAAgB;gBAAE,SAAS;YAE7D,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;YACxC,QAAQ,CAAC,IAAI,CAAC;gBACZ,MAAM,EAAE,uBAAuB,CAAC,EAAE;gBAClC,QAAQ,EAAE,MAAM;gBAChB,KAAK,EAAE,CAAC;gBACR,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,IAAI;gBACJ,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC9B,MAAM,EACJ,2CAA2C,QAAQ,CAAC,MAAM,YAAY;oBACtE,GAAG,SAAS,+CAA+C;oBAC3D,iDAAiD;gBACnD,QAAQ,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,cAAc,EAAE,SAAS,EAAE;aACjE,CAAC,CAAC;QACL,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * obf.gha-curl-pipe-shell — Manifest detector for GitHub Actions YAML.
3
+ *
4
+ * Flags any `run:` step in `.github/workflows/*.yml|yaml` whose command
5
+ * pipes a network download directly into a shell. This is one of the
6
+ * highest-yield supply-chain attack vectors — a CI run pulling and
7
+ * executing remote code on every push.
8
+ */
9
+ import type { Detector } from "../types.js";
10
+ export declare const ghaCurlPipeShell: Detector;
11
+ //# sourceMappingURL=gha-curl-pipe-shell.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gha-curl-pipe-shell.d.ts","sourceRoot":"","sources":["../../src/detectors/gha-curl-pipe-shell.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAwB,MAAM,aAAa,CAAC;AAWlE,eAAO,MAAM,gBAAgB,EAAE,QA6B9B,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * obf.gha-curl-pipe-shell — Manifest detector for GitHub Actions YAML.
3
+ *
4
+ * Flags any `run:` step in `.github/workflows/*.yml|yaml` whose command
5
+ * pipes a network download directly into a shell. This is one of the
6
+ * highest-yield supply-chain attack vectors — a CI run pulling and
7
+ * executing remote code on every push.
8
+ */
9
+ import { lineAtOffset } from "../internal/patterns.js";
10
+ import { truncateSnippet } from "../internal/text.js";
11
+ function isWorkflow(p) {
12
+ return /(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/.test(p);
13
+ }
14
+ const CURL_PIPE_SHELL_RE = /(?:curl|wget|fetch)\b[^\n]{0,200}\|\s*(?:bash|sh|zsh|python|node|perl|powershell|pwsh)\b/g;
15
+ export const ghaCurlPipeShell = {
16
+ id: "obf.gha-curl-pipe-shell",
17
+ docsUrl: "https://github.com/bytebardorg/obfuscan/blob/main/docs/detectors.md#obfgha-curl-pipe-shell",
18
+ applies(ctx) {
19
+ return isWorkflow(ctx.path);
20
+ },
21
+ run(ctx) {
22
+ const findings = [];
23
+ const src = ctx.source;
24
+ const re = new RegExp(CURL_PIPE_SHELL_RE.source, CURL_PIPE_SHELL_RE.flags);
25
+ let m;
26
+ while ((m = re.exec(src)) !== null) {
27
+ findings.push({
28
+ ruleId: ghaCurlPipeShell.id,
29
+ severity: "block",
30
+ score: 9,
31
+ file: ctx.path,
32
+ line: lineAtOffset(src, m.index),
33
+ snippet: truncateSnippet(m[0]),
34
+ reason: `GitHub Actions step pipes a network download into a shell. ` +
35
+ `Pin the artifact (sha256) or fetch + verify before executing.`,
36
+ evidence: { command: m[0] },
37
+ });
38
+ }
39
+ return findings;
40
+ },
41
+ };
42
+ //# sourceMappingURL=gha-curl-pipe-shell.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gha-curl-pipe-shell.js","sourceRoot":"","sources":["../../src/detectors/gha-curl-pipe-shell.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,SAAS,UAAU,CAAC,CAAS;IAC3B,OAAO,0CAA0C,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,kBAAkB,GACtB,2FAA2F,CAAC;AAE9F,MAAM,CAAC,MAAM,gBAAgB,GAAa;IACxC,EAAE,EAAE,yBAAyB;IAC7B,OAAO,EAAE,4FAA4F;IAErG,OAAO,CAAC,GAAgB;QACtB,OAAO,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED,GAAG,CAAC,GAAgB;QAClB,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC;QACvB,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC;QAC3E,IAAI,CAAyB,CAAC;QAC9B,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YACnC,QAAQ,CAAC,IAAI,CAAC;gBACZ,MAAM,EAAE,gBAAgB,CAAC,EAAE;gBAC3B,QAAQ,EAAE,OAAO;gBACjB,KAAK,EAAE,CAAC;gBACR,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,IAAI,EAAE,YAAY,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC;gBAChC,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC9B,MAAM,EACJ,6DAA6D;oBAC7D,+DAA+D;gBACjE,QAAQ,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE;aAC5B,CAAC,CAAC;QACL,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * obf.high-entropy-literal — Layer A (source-level regex).
3
+ *
4
+ * Flags long string literals whose Shannon entropy exceeds a threshold tuned
5
+ * against the Datadog malicious-packages corpus. Packed payloads, base64
6
+ * blobs, and hex-encoded shellcode all surface here.
7
+ *
8
+ * Known false positives:
9
+ * - Hex-encoded SHA-256 hashes in tests (mean ~4.0 bits/char, length 64)
10
+ * - Base64 SVG data URIs in CSS-in-JS (often legitimate)
11
+ * - Long license keys / JWTs in fixtures
12
+ *
13
+ * Tune via the `MIN_LEN` and `ENTROPY_THRESHOLD` constants. Per-project
14
+ * suppression should be done via `.obfuscan/allowlist.json`, not by
15
+ * lowering the threshold globally.
16
+ */
17
+ import type { Detector } from "../types.js";
18
+ export declare const highEntropyLiteral: Detector;
19
+ //# sourceMappingURL=high-entropy-literal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"high-entropy-literal.d.ts","sourceRoot":"","sources":["../../src/detectors/high-entropy-literal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAwB,MAAM,aAAa,CAAC;AAiClE,eAAO,MAAM,kBAAkB,EAAE,QAiDhC,CAAC"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * obf.high-entropy-literal — Layer A (source-level regex).
3
+ *
4
+ * Flags long string literals whose Shannon entropy exceeds a threshold tuned
5
+ * against the Datadog malicious-packages corpus. Packed payloads, base64
6
+ * blobs, and hex-encoded shellcode all surface here.
7
+ *
8
+ * Known false positives:
9
+ * - Hex-encoded SHA-256 hashes in tests (mean ~4.0 bits/char, length 64)
10
+ * - Base64 SVG data URIs in CSS-in-JS (often legitimate)
11
+ * - Long license keys / JWTs in fixtures
12
+ *
13
+ * Tune via the `MIN_LEN` and `ENTROPY_THRESHOLD` constants. Per-project
14
+ * suppression should be done via `.obfuscan/allowlist.json`, not by
15
+ * lowering the threshold globally.
16
+ */
17
+ import { truncateSnippet } from "../internal/text.js";
18
+ /** Matches `"..."`, `'...'`, or backtick strings of at least MIN_LEN chars. */
19
+ const STRING_LITERAL_RE = /(["'`])((?:\\.|(?!\1).){40,}?)\1/g;
20
+ const MIN_LEN = 40;
21
+ const ENTROPY_THRESHOLD = 4.5; // bits/char
22
+ const MAX_SOURCE_BYTES = 2_000_000; // skip files larger than 2 MB
23
+ const MAX_FINDINGS_PER_FILE = 50; // protect the report from minified bundles
24
+ /** Shannon entropy in bits/char. */
25
+ function shannon(s) {
26
+ if (s.length === 0)
27
+ return 0;
28
+ const freq = new Map();
29
+ for (const c of s)
30
+ freq.set(c, (freq.get(c) ?? 0) + 1);
31
+ let h = 0;
32
+ for (const n of freq.values()) {
33
+ const p = n / s.length;
34
+ h -= p * Math.log2(p);
35
+ }
36
+ return h;
37
+ }
38
+ /** Convert a 0-based character offset to a 1-based line number. */
39
+ function lineAt(source, offset) {
40
+ let line = 1;
41
+ for (let i = 0; i < offset && i < source.length; i++) {
42
+ if (source.charCodeAt(i) === 10 /* \n */)
43
+ line++;
44
+ }
45
+ return line;
46
+ }
47
+ export const highEntropyLiteral = {
48
+ id: "obf.high-entropy-literal",
49
+ docsUrl: "https://github.com/bytebardorg/obfuscan/blob/main/docs/detectors.md#obfhigh-entropy-literal",
50
+ applies(ctx) {
51
+ // Source-level — runs on any text file under the size cap.
52
+ return ctx.source.length > 0 && ctx.source.length < MAX_SOURCE_BYTES;
53
+ },
54
+ run(ctx) {
55
+ const findings = [];
56
+ const src = ctx.source;
57
+ let match;
58
+ // Local regex copy so we don't share lastIndex across calls.
59
+ const re = new RegExp(STRING_LITERAL_RE.source, STRING_LITERAL_RE.flags);
60
+ while ((match = re.exec(src)) !== null) {
61
+ if (findings.length >= MAX_FINDINGS_PER_FILE)
62
+ break;
63
+ const body = match[2];
64
+ if (!body || body.length < MIN_LEN)
65
+ continue;
66
+ const entropy = shannon(body);
67
+ if (entropy < ENTROPY_THRESHOLD)
68
+ continue;
69
+ const line = lineAt(src, match.index);
70
+ const score = Math.min(10, Math.round(entropy * 1.5));
71
+ findings.push({
72
+ ruleId: highEntropyLiteral.id,
73
+ severity: "warn",
74
+ score,
75
+ file: ctx.path,
76
+ line,
77
+ snippet: truncateSnippet(body),
78
+ reason: `High-entropy string literal ` +
79
+ `(Shannon ${entropy.toFixed(2)} bits/char, length ${body.length}) — ` +
80
+ `possible packed payload.`,
81
+ evidence: {
82
+ entropy: Number(entropy.toFixed(3)),
83
+ length: body.length,
84
+ },
85
+ });
86
+ }
87
+ return findings;
88
+ },
89
+ };
90
+ //# sourceMappingURL=high-entropy-literal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"high-entropy-literal.js","sourceRoot":"","sources":["../../src/detectors/high-entropy-literal.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * obf.homoglyph-identifier — Layer A.
3
+ *
4
+ * Flags identifiers that mix scripts in a way associated with homoglyph
5
+ * attacks. The narrow definition: an identifier with a *majority* of ASCII
6
+ * Latin letters but containing at least one confusable from another script.
7
+ *
8
+ * Negative cases:
9
+ * - Pure non-ASCII identifiers (e.g. all Cyrillic, all CJK) — legitimate
10
+ * non-English code.
11
+ * - Pure ASCII identifiers — by construction.
12
+ * - Strings, comments — not identifier contexts.
13
+ */
14
+ import type { Detector } from "../types.js";
15
+ export declare const homoglyphIdentifier: Detector;
16
+ //# sourceMappingURL=homoglyph-identifier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"homoglyph-identifier.d.ts","sourceRoot":"","sources":["../../src/detectors/homoglyph-identifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAwB,MAAM,aAAa,CAAC;AAgBlE,eAAO,MAAM,mBAAmB,EAAE,QAkDjC,CAAC"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * obf.homoglyph-identifier — Layer A.
3
+ *
4
+ * Flags identifiers that mix scripts in a way associated with homoglyph
5
+ * attacks. The narrow definition: an identifier with a *majority* of ASCII
6
+ * Latin letters but containing at least one confusable from another script.
7
+ *
8
+ * Negative cases:
9
+ * - Pure non-ASCII identifiers (e.g. all Cyrillic, all CJK) — legitimate
10
+ * non-English code.
11
+ * - Pure ASCII identifiers — by construction.
12
+ * - Strings, comments — not identifier contexts.
13
+ */
14
+ import { lineAtOffset, MAX_SOURCE_BYTES } from "../internal/patterns.js";
15
+ import { truncateSnippet } from "../internal/text.js";
16
+ // Identifier tokenizer: ASCII-letter or non-space, non-punct unicode letters.
17
+ // We use a Unicode property escape here; supported in Node 12+.
18
+ const IDENT_RE = /[A-Za-z_$][\p{L}\p{N}_$]{2,}/gu;
19
+ // "Confusable script ranges" — characters that visually resemble Latin but
20
+ // belong to other scripts. Subset focused on Cyrillic + Greek which produce
21
+ // the highest-yield homoglyphs.
22
+ const CONFUSABLE_RE = /[\u0400-\u04FF\u0370-\u03FF]/;
23
+ // ASCII letters
24
+ const ASCII_LETTER_RE = /[A-Za-z]/;
25
+ export const homoglyphIdentifier = {
26
+ id: "obf.homoglyph-identifier",
27
+ docsUrl: "https://github.com/bytebardorg/obfuscan/blob/main/docs/detectors.md#obfhomoglyph-identifier",
28
+ applies(ctx) {
29
+ return ctx.source.length > 0 && ctx.source.length < MAX_SOURCE_BYTES;
30
+ },
31
+ run(ctx) {
32
+ const findings = [];
33
+ const src = ctx.source;
34
+ const seen = new Set();
35
+ const re = new RegExp(IDENT_RE.source, IDENT_RE.flags);
36
+ let m;
37
+ while ((m = re.exec(src)) !== null) {
38
+ const ident = m[0];
39
+ if (seen.has(ident))
40
+ continue;
41
+ if (!CONFUSABLE_RE.test(ident))
42
+ continue;
43
+ // Count ASCII vs confusable letters
44
+ let ascii = 0;
45
+ let confusable = 0;
46
+ for (const c of ident) {
47
+ if (ASCII_LETTER_RE.test(c))
48
+ ascii++;
49
+ else if (CONFUSABLE_RE.test(c))
50
+ confusable++;
51
+ }
52
+ if (ascii === 0)
53
+ continue; // pure non-ASCII identifier
54
+ if (confusable === 0)
55
+ continue;
56
+ // Only flag mixed identifiers where ASCII is the majority.
57
+ if (ascii < confusable)
58
+ continue;
59
+ seen.add(ident);
60
+ const line = lineAtOffset(src, m.index);
61
+ findings.push({
62
+ ruleId: homoglyphIdentifier.id,
63
+ severity: "block",
64
+ score: 9,
65
+ file: ctx.path,
66
+ line,
67
+ snippet: truncateSnippet(ident),
68
+ reason: `Identifier mixes Latin letters with confusable characters from ` +
69
+ `another script (Cyrillic/Greek). This is a homoglyph attack pattern.`,
70
+ evidence: { identifier: ident, asciiCount: ascii, confusableCount: confusable },
71
+ });
72
+ }
73
+ return findings;
74
+ },
75
+ };
76
+ //# sourceMappingURL=homoglyph-identifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"homoglyph-identifier.js","sourceRoot":"","sources":["../../src/detectors/homoglyph-identifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AACzE,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,8EAA8E;AAC9E,gEAAgE;AAChE,MAAM,QAAQ,GAAG,gCAAgC,CAAC;AAElD,2EAA2E;AAC3E,4EAA4E;AAC5E,gCAAgC;AAChC,MAAM,aAAa,GAAG,8BAA8B,CAAC;AAErD,gBAAgB;AAChB,MAAM,eAAe,GAAG,UAAU,CAAC;AAEnC,MAAM,CAAC,MAAM,mBAAmB,GAAa;IAC3C,EAAE,EAAE,0BAA0B;IAC9B,OAAO,EAAE,6FAA6F;IAEtG,OAAO,CAAC,GAAgB;QACtB,OAAO,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,gBAAgB,CAAC;IACvE,CAAC;IAED,GAAG,CAAC,GAAgB;QAClB,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC;QACvB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;QAC/B,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;QAEvD,IAAI,CAAyB,CAAC;QAC9B,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YACnC,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YACnB,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC;gBAAE,SAAS;YAE9B,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC;gBAAE,SAAS;YAEzC,oCAAoC;YACpC,IAAI,KAAK,GAAG,CAAC,CAAC;YACd,IAAI,UAAU,GAAG,CAAC,CAAC;YACnB,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;gBACtB,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;oBAAE,KAAK,EAAE,CAAC;qBAChC,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC;oBAAE,UAAU,EAAE,CAAC;YAC/C,CAAC;YACD,IAAI,KAAK,KAAK,CAAC;gBAAE,SAAS,CAAC,4BAA4B;YACvD,IAAI,UAAU,KAAK,CAAC;gBAAE,SAAS;YAC/B,2DAA2D;YAC3D,IAAI,KAAK,GAAG,UAAU;gBAAE,SAAS;YAEjC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAChB,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;YACxC,QAAQ,CAAC,IAAI,CAAC;gBACZ,MAAM,EAAE,mBAAmB,CAAC,EAAE;gBAC9B,QAAQ,EAAE,OAAO;gBACjB,KAAK,EAAE,CAAC;gBACR,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,IAAI;gBACJ,OAAO,EAAE,eAAe,CAAC,KAAK,CAAC;gBAC/B,MAAM,EACJ,iEAAiE;oBACjE,sEAAsE;gBACxE,QAAQ,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE;aAChF,CAAC,CAAC;QACL,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Built-in detector registry.
3
+ *
4
+ * `defaultDetectors()` returns the canonical, ordered list shipped with
5
+ * `@obfuscan/core`. Order is meaningful for stable output: when two detectors
6
+ * fire on the same line and severity, the earlier-listed one's finding sorts
7
+ * first.
8
+ */
9
+ import type { Detector } from "../types.js";
10
+ import { highEntropyLiteral } from "./high-entropy-literal.js";
11
+ import { bidiControlChar } from "./bidi-control.js";
12
+ import { homoglyphIdentifier } from "./homoglyph-identifier.js";
13
+ import { longLine } from "./long-line.js";
14
+ import { encodedArrayFingerprint } from "./encoded-array-fingerprint.js";
15
+ import { decodeThenExec } from "./decode-then-exec.js";
16
+ import { dynamicExecNonLiteral } from "./dynamic-exec-non-literal.js";
17
+ import { networkThenExec } from "./network-then-exec.js";
18
+ import { deserializerUntrusted } from "./deserializer-untrusted.js";
19
+ import { suspiciousIoCluster } from "./suspicious-io-cluster.js";
20
+ import { stringArrayDecoder } from "./string-array-decoder.js";
21
+ import { shellUntrustedInput } from "./shell-untrusted-input.js";
22
+ import { libraryLoadNonLiteral } from "./library-load-non-literal.js";
23
+ import { manifestInstallScript } from "./manifest-install-script.js";
24
+ import { pythonSetupSideEffect } from "./python-setup-side-effect.js";
25
+ import { perlMakefileSideEffect } from "./perl-makefile-side-effect.js";
26
+ import { cargoBuildRsNetwork } from "./cargo-build-rs-network.js";
27
+ import { ghaCurlPipeShell } from "./gha-curl-pipe-shell.js";
28
+ import { dockerfileCurlPipeShell } from "./dockerfile-curl-pipe-shell.js";
29
+ export declare function defaultDetectors(): readonly Detector[];
30
+ export { highEntropyLiteral, bidiControlChar, homoglyphIdentifier, longLine, encodedArrayFingerprint, decodeThenExec, dynamicExecNonLiteral, networkThenExec, deserializerUntrusted, suspiciousIoCluster, stringArrayDecoder, shellUntrustedInput, libraryLoadNonLiteral, manifestInstallScript, pythonSetupSideEffect, perlMakefileSideEffect, cargoBuildRsNetwork, ghaCurlPipeShell, dockerfileCurlPipeShell, };
31
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/detectors/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAG5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAGzE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAGtE,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAE1E,wBAAgB,gBAAgB,IAAI,SAAS,QAAQ,EAAE,CAEtD;AA8BD,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,mBAAmB,EACnB,QAAQ,EACR,uBAAuB,EACvB,cAAc,EACd,qBAAqB,EACrB,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,sBAAsB,EACtB,mBAAmB,EACnB,gBAAgB,EAChB,uBAAuB,GACxB,CAAC"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Built-in detector registry.
3
+ *
4
+ * `defaultDetectors()` returns the canonical, ordered list shipped with
5
+ * `@obfuscan/core`. Order is meaningful for stable output: when two detectors
6
+ * fire on the same line and severity, the earlier-listed one's finding sorts
7
+ * first.
8
+ */
9
+ // Layer A — universal source-level detectors
10
+ import { highEntropyLiteral } from "./high-entropy-literal.js";
11
+ import { bidiControlChar } from "./bidi-control.js";
12
+ import { homoglyphIdentifier } from "./homoglyph-identifier.js";
13
+ import { longLine } from "./long-line.js";
14
+ import { encodedArrayFingerprint } from "./encoded-array-fingerprint.js";
15
+ // Layer B — config-driven semantic detectors
16
+ import { decodeThenExec } from "./decode-then-exec.js";
17
+ import { dynamicExecNonLiteral } from "./dynamic-exec-non-literal.js";
18
+ import { networkThenExec } from "./network-then-exec.js";
19
+ import { deserializerUntrusted } from "./deserializer-untrusted.js";
20
+ import { suspiciousIoCluster } from "./suspicious-io-cluster.js";
21
+ import { stringArrayDecoder } from "./string-array-decoder.js";
22
+ import { shellUntrustedInput } from "./shell-untrusted-input.js";
23
+ import { libraryLoadNonLiteral } from "./library-load-non-literal.js";
24
+ // Manifest — ecosystem-specific detectors
25
+ import { manifestInstallScript } from "./manifest-install-script.js";
26
+ import { pythonSetupSideEffect } from "./python-setup-side-effect.js";
27
+ import { perlMakefileSideEffect } from "./perl-makefile-side-effect.js";
28
+ import { cargoBuildRsNetwork } from "./cargo-build-rs-network.js";
29
+ import { ghaCurlPipeShell } from "./gha-curl-pipe-shell.js";
30
+ import { dockerfileCurlPipeShell } from "./dockerfile-curl-pipe-shell.js";
31
+ export function defaultDetectors() {
32
+ return DEFAULTS;
33
+ }
34
+ const DEFAULTS = Object.freeze([
35
+ // Layer A
36
+ highEntropyLiteral,
37
+ bidiControlChar,
38
+ homoglyphIdentifier,
39
+ longLine,
40
+ encodedArrayFingerprint,
41
+ // Layer B
42
+ decodeThenExec,
43
+ networkThenExec,
44
+ dynamicExecNonLiteral,
45
+ deserializerUntrusted,
46
+ suspiciousIoCluster,
47
+ stringArrayDecoder,
48
+ shellUntrustedInput,
49
+ libraryLoadNonLiteral,
50
+ // Manifest
51
+ manifestInstallScript,
52
+ pythonSetupSideEffect,
53
+ perlMakefileSideEffect,
54
+ cargoBuildRsNetwork,
55
+ ghaCurlPipeShell,
56
+ dockerfileCurlPipeShell,
57
+ ]);
58
+ // Named re-exports for tests and advanced consumers.
59
+ export { highEntropyLiteral, bidiControlChar, homoglyphIdentifier, longLine, encodedArrayFingerprint, decodeThenExec, dynamicExecNonLiteral, networkThenExec, deserializerUntrusted, suspiciousIoCluster, stringArrayDecoder, shellUntrustedInput, libraryLoadNonLiteral, manifestInstallScript, pythonSetupSideEffect, perlMakefileSideEffect, cargoBuildRsNetwork, ghaCurlPipeShell, dockerfileCurlPipeShell, };
60
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/detectors/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,6CAA6C;AAC7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAEzE,6CAA6C;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAEtE,0CAA0C;AAC1C,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAE1E,MAAM,UAAU,gBAAgB;IAC9B,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,QAAQ,GAAwB,MAAM,CAAC,MAAM,CAAC;IAClD,UAAU;IACV,kBAAkB;IAClB,eAAe;IACf,mBAAmB;IACnB,QAAQ;IACR,uBAAuB;IAEvB,UAAU;IACV,cAAc;IACd,eAAe;IACf,qBAAqB;IACrB,qBAAqB;IACrB,mBAAmB;IACnB,kBAAkB;IAClB,mBAAmB;IACnB,qBAAqB;IAErB,WAAW;IACX,qBAAqB;IACrB,qBAAqB;IACrB,sBAAsB;IACtB,mBAAmB;IACnB,gBAAgB;IAChB,uBAAuB;CACxB,CAAC,CAAC;AAEH,qDAAqD;AACrD,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,mBAAmB,EACnB,QAAQ,EACR,uBAAuB,EACvB,cAAc,EACd,qBAAqB,EACrB,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,sBAAsB,EACtB,mBAAmB,EACnB,gBAAgB,EAChB,uBAAuB,GACxB,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * obf.library-load-non-literal.<lang> — Layer B.
3
+ *
4
+ * Fires when a dynamic-library-load function (`require`, `importlib.import_module`,
5
+ * `Assembly.Load`, `libloading::Library::new`, …) is called with a non-literal
6
+ * argument.
7
+ */
8
+ import type { Detector } from "../types.js";
9
+ export declare const libraryLoadNonLiteral: Detector;
10
+ //# sourceMappingURL=library-load-non-literal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"library-load-non-literal.d.ts","sourceRoot":"","sources":["../../src/detectors/library-load-non-literal.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAwC,MAAM,aAAa,CAAC;AAiClF,eAAO,MAAM,qBAAqB,EAAE,QA4CnC,CAAC"}