@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,140 @@
1
+ /**
2
+ * obf.network-then-exec.<lang> — Layer B.
3
+ *
4
+ * Fires when network IO output flows into a dynamic-exec sink. The shape:
5
+ *
6
+ * eval(await (await fetch(url)).text())
7
+ * exec(requests.get(url).text)
8
+ * IEX (New-Object Net.WebClient).DownloadString($u)
9
+ * eval "$(curl -s $URL)"
10
+ *
11
+ * Unlike decode-then-exec, the source of the executed string is *external*
12
+ * — strictly worse, because the attacker doesn't even need to be in the
13
+ * source tree. Always blocks.
14
+ */
15
+ import { escapeRegex, 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 candidateTokens(names) {
19
+ const out = [];
20
+ const seen = new Set();
21
+ for (const n of names) {
22
+ if (!n)
23
+ continue;
24
+ const tail = n.split(/[.\/:\s]+/).filter(Boolean).pop() ?? n;
25
+ const token = tail.replace(/[^A-Za-z0-9_]/g, "");
26
+ if (token.length < 3)
27
+ continue;
28
+ if (seen.has(token))
29
+ continue;
30
+ seen.add(token);
31
+ out.push(token);
32
+ }
33
+ return out;
34
+ }
35
+ function maybeRelevantSource(source, config) {
36
+ const networkTokens = candidateTokens(config.network_io ?? []);
37
+ const sinkTokens = candidateTokens(config.dynamic_exec_sinks);
38
+ const hasNetwork = networkTokens.some(t => source.includes(t));
39
+ if (!hasNetwork)
40
+ return false;
41
+ return sinkTokens.some(t => source.includes(t));
42
+ }
43
+ function compile(config) {
44
+ const cached = cache.get(config);
45
+ if (cached)
46
+ return cached;
47
+ const network = namedCallAlternation(config.network_io ?? []);
48
+ const sinks = namedCallAlternation(config.dynamic_exec_sinks);
49
+ // Direct: sink( ... network( ... ) ... )
50
+ // Allow some method chaining (`.text()`, `.read()`) and up to two nested
51
+ // call groups between the sink and the network call — e.g.
52
+ // `eval(await (await fetch(url)).text())`. We use [\s\S] with a bounded
53
+ // length cap to keep this O(n) and avoid catastrophic backtracking.
54
+ const direct = new RegExp(`(?:${sinks})\\s*[("\\s][\\s\\S]{0,400}?(?:${network})\\s*[("\\s]`, "g");
55
+ // Bash-specific: `sink "$( ... pipe-decoder/network ... )"` where the inner
56
+ // shell expansion contains a network command. The direct regex above can
57
+ // miss this because the network name is a bare word (`curl`), not a call.
58
+ const bashSubshell = new RegExp(`(?:${sinks})\\s+"?\\$\\([\\s\\S]{0,400}?(?:${network})\\b`, "g");
59
+ // Reuse `direct` as the union: tests use `direct` only, but if the language
60
+ // is bash we want to fall back to the subshell shape below.
61
+ const isShell = (config.id === "bash" || config.aliases?.includes("sh"));
62
+ const directUnion = isShell
63
+ ? new RegExp(`${direct.source}|${bashSubshell.source}`, "g")
64
+ : direct;
65
+ // Indirect: `var X = network(...);` followed by `sink(X)`.
66
+ const indirectAssign = new RegExp(`(?:(?:const|let|var|my|local|\\$)\\s+)?` +
67
+ `([A-Za-z_$][\\w$]*)` +
68
+ `\\s*[:=]\\s*` +
69
+ `(?:await\\s+)?` +
70
+ `(?:${network})\\s*\\(`, "g");
71
+ const sinkUse = (v) => new RegExp(`(?:${sinks})\\s*[("\\s][^()]{0,200}\\b${escapeRegex(v)}\\b`, "g");
72
+ const compiled = { direct: directUnion, indirectAssign, sinkUse };
73
+ cache.set(config, compiled);
74
+ return compiled;
75
+ }
76
+ export const networkThenExec = {
77
+ id: "obf.network-then-exec",
78
+ docsUrl: "https://github.com/bytebardorg/obfuscan/blob/main/docs/detectors.md#obfnetwork-then-exec",
79
+ applies(ctx) {
80
+ return (ctx.config !== null &&
81
+ (ctx.config.network_io?.length ?? 0) > 0 &&
82
+ ctx.config.dynamic_exec_sinks.length > 0 &&
83
+ ctx.source.length > 0 &&
84
+ ctx.source.length < MAX_SOURCE_BYTES &&
85
+ maybeRelevantSource(ctx.source, ctx.config));
86
+ },
87
+ run(ctx) {
88
+ if (!ctx.config)
89
+ return [];
90
+ const cfg = ctx.config;
91
+ const src = ctx.source;
92
+ const { direct, indirectAssign, sinkUse } = compile(cfg);
93
+ const findings = [];
94
+ const seen = new Set();
95
+ let m;
96
+ const directRe = new RegExp(direct.source, direct.flags);
97
+ while ((m = directRe.exec(src)) !== null) {
98
+ if (findings.length >= MAX_FINDINGS_PER_DETECTOR)
99
+ break;
100
+ const line = lineAtOffset(src, m.index);
101
+ if (seen.has(line))
102
+ continue;
103
+ seen.add(line);
104
+ findings.push(make(ctx, cfg.id, m[0], m.index, "direct"));
105
+ }
106
+ const assignRe = new RegExp(indirectAssign.source, indirectAssign.flags);
107
+ while ((m = assignRe.exec(src)) !== null) {
108
+ if (findings.length >= MAX_FINDINGS_PER_DETECTOR)
109
+ break;
110
+ const v = m[1];
111
+ if (!v)
112
+ continue;
113
+ const after = src.slice(m.index + m[0].length);
114
+ const u = sinkUse(v).exec(after);
115
+ if (!u)
116
+ continue;
117
+ const offset = m.index + m[0].length + u.index;
118
+ const line = lineAtOffset(src, offset);
119
+ if (seen.has(line))
120
+ continue;
121
+ seen.add(line);
122
+ findings.push(make(ctx, cfg.id, u[0], offset, "indirect"));
123
+ }
124
+ return findings;
125
+ },
126
+ };
127
+ function make(ctx, langId, rawSnippet, offset, flow) {
128
+ return {
129
+ ruleId: `obf.network-then-exec.${langId}`,
130
+ severity: "block",
131
+ score: 10,
132
+ file: ctx.path,
133
+ line: lineAtOffset(ctx.source, offset),
134
+ snippet: truncateSnippet(rawSnippet),
135
+ reason: `Network IO result flows into a dynamic-exec sink (${flow}). The ` +
136
+ `executed code is fully attacker-controlled.`,
137
+ evidence: { language: langId, flow },
138
+ };
139
+ }
140
+ //# sourceMappingURL=network-then-exec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network-then-exec.js","sourceRoot":"","sources":["../../src/detectors/network-then-exec.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * obf.perl-makefile-side-effect — Manifest detector for `Makefile.PL` and
3
+ * `Build.PL`.
4
+ *
5
+ * CPAN distributions ship a `Makefile.PL` (or `Build.PL`) which is executed
6
+ * verbatim by the user during `cpan install` / `cpanm`. The expected shape is
7
+ * a small declarative call to `WriteMakefile(...)` (ExtUtils::MakeMaker) or
8
+ * `Module::Build->new(...)->create_build_script` (Module::Build). Anything
9
+ * else at module scope runs on the user's machine.
10
+ *
11
+ * This detector is the Perl counterpart to `obf.python-setup-side-effect`.
12
+ *
13
+ * Severity: `block` (score 9) — same calculus as setup.py.
14
+ */
15
+ import type { Detector } from "../types.js";
16
+ export declare const perlMakefileSideEffect: Detector;
17
+ //# sourceMappingURL=perl-makefile-side-effect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"perl-makefile-side-effect.d.ts","sourceRoot":"","sources":["../../src/detectors/perl-makefile-side-effect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAwB,MAAM,aAAa,CAAC;AAalE,eAAO,MAAM,sBAAsB,EAAE,QAwDpC,CAAC"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * obf.perl-makefile-side-effect — Manifest detector for `Makefile.PL` and
3
+ * `Build.PL`.
4
+ *
5
+ * CPAN distributions ship a `Makefile.PL` (or `Build.PL`) which is executed
6
+ * verbatim by the user during `cpan install` / `cpanm`. The expected shape is
7
+ * a small declarative call to `WriteMakefile(...)` (ExtUtils::MakeMaker) or
8
+ * `Module::Build->new(...)->create_build_script` (Module::Build). Anything
9
+ * else at module scope runs on the user's machine.
10
+ *
11
+ * This detector is the Perl counterpart to `obf.python-setup-side-effect`.
12
+ *
13
+ * Severity: `block` (score 9) — same calculus as setup.py.
14
+ */
15
+ import { truncateSnippet } from "../internal/text.js";
16
+ function isPerlInstaller(p) {
17
+ const norm = p.replace(/\\/g, "/");
18
+ const base = norm.slice(norm.lastIndexOf("/") + 1);
19
+ return base === "Makefile.PL" || base === "Build.PL";
20
+ }
21
+ // Suspicious shapes at module scope: network, shell-out, eval-like, decoders.
22
+ const SUSPICIOUS_RE = /(\bsystem\s*\(|\bexec\s*\(|`[^`]*`|qx[\s({\[]|LWP::|HTTP::Tiny|IO::Socket|Net::|MIME::Base64|decode_base64|\beval\s*\{|\beval\s*['"]|use\s+inline\b)/i;
23
+ export const perlMakefileSideEffect = {
24
+ id: "obf.perl-makefile-side-effect",
25
+ docsUrl: "https://github.com/bytebardorg/obfuscan/blob/main/docs/detectors.md#obfperl-makefile-side-effect",
26
+ applies(ctx) {
27
+ return isPerlInstaller(ctx.path);
28
+ },
29
+ run(ctx) {
30
+ const findings = [];
31
+ const lines = ctx.source.split("\n");
32
+ for (let i = 0; i < lines.length; i++) {
33
+ const raw = lines[i] ?? "";
34
+ // Strip trailing comments before testing.
35
+ const code = raw.replace(/(?<!\\)#.*$/, "");
36
+ if (code.trim().length === 0)
37
+ continue;
38
+ // Skip indented lines — only flag module-scope side effects. (Code
39
+ // inside a sub {} or block is indented in any reasonable style; this
40
+ // mirrors the python-setup heuristic.)
41
+ if (/^\s/.test(raw))
42
+ continue;
43
+ // Skip pragmas, package declarations, simple use/require, and
44
+ // declarative WriteMakefile / Module::Build calls.
45
+ if (/^\s*(?:use|no|require|package|our|my)\b/.test(code) ||
46
+ /^\s*(?:WriteMakefile|Module::Build)\b/.test(code) ||
47
+ /^\s*\)/.test(code) // tail of a multi-line call
48
+ ) {
49
+ continue;
50
+ }
51
+ if (SUSPICIOUS_RE.test(code)) {
52
+ findings.push({
53
+ ruleId: perlMakefileSideEffect.id,
54
+ severity: "block",
55
+ score: 9,
56
+ file: ctx.path,
57
+ line: i + 1,
58
+ snippet: truncateSnippet(code.trim()),
59
+ reason: `${ctx.path} contains code outside the declarative ` +
60
+ `\`WriteMakefile\` / \`Module::Build\` call that performs network, ` +
61
+ `shell, or eval-like side effects. CPAN clients execute this file ` +
62
+ `on the user's machine during \`cpan install\`.`,
63
+ evidence: {},
64
+ });
65
+ // First match per file is enough to surface the issue.
66
+ break;
67
+ }
68
+ }
69
+ return findings;
70
+ },
71
+ };
72
+ //# sourceMappingURL=perl-makefile-side-effect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"perl-makefile-side-effect.js","sourceRoot":"","sources":["../../src/detectors/perl-makefile-side-effect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,SAAS,eAAe,CAAC,CAAS;IAChC,MAAM,IAAI,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACnC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IACnD,OAAO,IAAI,KAAK,aAAa,IAAI,IAAI,KAAK,UAAU,CAAC;AACvD,CAAC;AAED,8EAA8E;AAC9E,MAAM,aAAa,GACjB,uJAAuJ,CAAC;AAE1J,MAAM,CAAC,MAAM,sBAAsB,GAAa;IAC9C,EAAE,EAAE,+BAA+B;IACnC,OAAO,EACL,kGAAkG;IAEpG,OAAO,CAAC,GAAgB;QACtB,OAAO,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC;IAED,GAAG,CAAC,GAAgB;QAClB,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAErC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtC,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAC3B,0CAA0C;YAC1C,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;YAC5C,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YAEvC,mEAAmE;YACnE,qEAAqE;YACrE,uCAAuC;YACvC,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC;gBAAE,SAAS;YAE9B,8DAA8D;YAC9D,mDAAmD;YACnD,IACE,yCAAyC,CAAC,IAAI,CAAC,IAAI,CAAC;gBACpD,uCAAuC,CAAC,IAAI,CAAC,IAAI,CAAC;gBAClD,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,4BAA4B;cAChD,CAAC;gBACD,SAAS;YACX,CAAC;YAED,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC7B,QAAQ,CAAC,IAAI,CAAC;oBACZ,MAAM,EAAE,sBAAsB,CAAC,EAAE;oBACjC,QAAQ,EAAE,OAAO;oBACjB,KAAK,EAAE,CAAC;oBACR,IAAI,EAAE,GAAG,CAAC,IAAI;oBACd,IAAI,EAAE,CAAC,GAAG,CAAC;oBACX,OAAO,EAAE,eAAe,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;oBACrC,MAAM,EACJ,GAAG,GAAG,CAAC,IAAI,yCAAyC;wBACpD,oEAAoE;wBACpE,mEAAmE;wBACnE,gDAAgD;oBAClD,QAAQ,EAAE,EAAE;iBACb,CAAC,CAAC;gBACH,uDAAuD;gBACvD,MAAM;YACR,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * obf.python-setup-side-effect — Manifest detector for `setup.py`.
3
+ *
4
+ * Flags top-level executable code in `setup.py` other than the import,
5
+ * `setup(...)` call, and a small allowlist of bookkeeping. Real-world
6
+ * malicious `setup.py` files run network/shell side effects at install time.
7
+ */
8
+ import type { Detector } from "../types.js";
9
+ export declare const pythonSetupSideEffect: Detector;
10
+ //# sourceMappingURL=python-setup-side-effect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"python-setup-side-effect.d.ts","sourceRoot":"","sources":["../../src/detectors/python-setup-side-effect.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAwB,MAAM,aAAa,CAAC;AAelE,eAAO,MAAM,qBAAqB,EAAE,QA0EnC,CAAC"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * obf.python-setup-side-effect — Manifest detector for `setup.py`.
3
+ *
4
+ * Flags top-level executable code in `setup.py` other than the import,
5
+ * `setup(...)` call, and a small allowlist of bookkeeping. Real-world
6
+ * malicious `setup.py` files run network/shell side effects at install time.
7
+ */
8
+ import { truncateSnippet } from "../internal/text.js";
9
+ function isSetupPy(p) {
10
+ return p === "setup.py" || p.endsWith("/setup.py");
11
+ }
12
+ // Lines that are allowed at module scope without flagging.
13
+ const ALLOWED_LINE_RE = /^(?:\s*$|\s*#|from\s+\S+\s+import\s+|import\s+\S+|setup\s*\(|\)\s*$|\s*[\w]+\s*=\s*[^=].*$)/;
14
+ // Suspicious side-effect markers at module scope.
15
+ const SUSPICIOUS_RE = /(urllib\.request|requests\.|httpx\.|urlretrieve|os\.system|subprocess\.|Popen|socket\.|exec\s*\(|eval\s*\(|base64\.b64decode\s*\()/;
16
+ export const pythonSetupSideEffect = {
17
+ id: "obf.python-setup-side-effect",
18
+ docsUrl: "https://github.com/bytebardorg/obfuscan/blob/main/docs/detectors.md#obfpython-setup-side-effect",
19
+ applies(ctx) {
20
+ return isSetupPy(ctx.path);
21
+ },
22
+ run(ctx) {
23
+ const findings = [];
24
+ const lines = ctx.source.split("\n");
25
+ let inSetupCall = false;
26
+ let parenDepth = 0;
27
+ for (let i = 0; i < lines.length; i++) {
28
+ const raw = lines[i] ?? "";
29
+ const line = raw;
30
+ if (inSetupCall) {
31
+ for (const c of line) {
32
+ if (c === "(")
33
+ parenDepth++;
34
+ else if (c === ")") {
35
+ parenDepth--;
36
+ if (parenDepth <= 0) {
37
+ inSetupCall = false;
38
+ parenDepth = 0;
39
+ break;
40
+ }
41
+ }
42
+ }
43
+ continue;
44
+ }
45
+ if (/^\s*setup\s*\(/.test(line)) {
46
+ inSetupCall = true;
47
+ parenDepth = 0;
48
+ for (const c of line) {
49
+ if (c === "(")
50
+ parenDepth++;
51
+ else if (c === ")")
52
+ parenDepth--;
53
+ }
54
+ if (parenDepth <= 0)
55
+ inSetupCall = false;
56
+ continue;
57
+ }
58
+ // Indented lines are inside def/class/if blocks — skip; we only flag
59
+ // module-scope side-effects.
60
+ if (/^\s/.test(line))
61
+ continue;
62
+ if (SUSPICIOUS_RE.test(line)) {
63
+ findings.push({
64
+ ruleId: pythonSetupSideEffect.id,
65
+ severity: "block",
66
+ score: 9,
67
+ file: ctx.path,
68
+ line: i + 1,
69
+ snippet: truncateSnippet(line.trim()),
70
+ reason: `setup.py contains code outside the \`setup()\` call that performs ` +
71
+ `network, shell, or eval-like side effects at install time. This is ` +
72
+ `the canonical \`pip install\` malware shape.`,
73
+ evidence: {},
74
+ });
75
+ // First match per file is enough.
76
+ break;
77
+ }
78
+ // Otherwise: only flag if it's a function call that's NOT in our
79
+ // allowlist (imports, simple assignments, comments).
80
+ if (!ALLOWED_LINE_RE.test(line) && /\(/.test(line)) {
81
+ // Skip — too noisy. The SUSPICIOUS_RE branch above is the real signal.
82
+ }
83
+ }
84
+ return findings;
85
+ },
86
+ };
87
+ //# sourceMappingURL=python-setup-side-effect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"python-setup-side-effect.js","sourceRoot":"","sources":["../../src/detectors/python-setup-side-effect.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * obf.shell-with-untrusted-input.<lang> — Layer B.
3
+ *
4
+ * Fires when a shell-exec sink (`child_process.exec`, `os.system`,
5
+ * `Runtime.exec`, …) receives an argument built via string interpolation
6
+ * or concatenation. Static-string commands are not flagged.
7
+ */
8
+ import type { Detector } from "../types.js";
9
+ export declare const shellUntrustedInput: Detector;
10
+ //# sourceMappingURL=shell-untrusted-input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-untrusted-input.d.ts","sourceRoot":"","sources":["../../src/detectors/shell-untrusted-input.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAwC,MAAM,aAAa,CAAC;AAmClF,eAAO,MAAM,mBAAmB,EAAE,QA6CjC,CAAC"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * obf.shell-with-untrusted-input.<lang> — Layer B.
3
+ *
4
+ * Fires when a shell-exec sink (`child_process.exec`, `os.system`,
5
+ * `Runtime.exec`, …) receives an argument built via string interpolation
6
+ * or concatenation. Static-string commands are not flagged.
7
+ */
8
+ import { lineAtOffset, MAX_FINDINGS_PER_DETECTOR, MAX_SOURCE_BYTES, namedCallAlternation, } from "../internal/patterns.js";
9
+ import { truncateSnippet } from "../internal/text.js";
10
+ // Heuristic markers that the argument is built dynamically.
11
+ // ${...} — JS template / shell expansion
12
+ // `..${` — JS template start
13
+ // f"..{..}" — Python f-string with placeholder
14
+ // "..%s.." — printf-style
15
+ // .. + .. — concatenation (followed by an identifier)
16
+ // $variable — shell/perl/php interpolation
17
+ const DYNAMIC_ARG_RE = /(\$\{[^}]+\}|`[^`]*\$\{|f["'][^"']*\{[^}]+\}|"[^"]*%[sdif]"|\+\s*[A-Za-z_$][\w$]*|\$[A-Za-z_]\w*)/;
18
+ const cache = new WeakMap();
19
+ function compile(config) {
20
+ if (cache.has(config))
21
+ return cache.get(config) ?? null;
22
+ const list = config.shell_exec ?? [];
23
+ if (list.length === 0) {
24
+ cache.set(config, null);
25
+ return null;
26
+ }
27
+ const alt = namedCallAlternation(list);
28
+ // Capture sink + the first 80 chars of arguments
29
+ const re = new RegExp(`(?:^|[^A-Za-z0-9_$])((?:${alt}))\\s*\\(([^)\\n]{0,200})`, "g");
30
+ cache.set(config, re);
31
+ return re;
32
+ }
33
+ export const shellUntrustedInput = {
34
+ id: "obf.shell-with-untrusted-input",
35
+ docsUrl: "https://github.com/bytebardorg/obfuscan/blob/main/docs/detectors.md#obfshell-with-untrusted-input",
36
+ applies(ctx) {
37
+ return (ctx.config !== null &&
38
+ (ctx.config.shell_exec?.length ?? 0) > 0 &&
39
+ ctx.source.length > 0 &&
40
+ ctx.source.length < MAX_SOURCE_BYTES);
41
+ },
42
+ run(ctx) {
43
+ if (!ctx.config)
44
+ return [];
45
+ const cfg = ctx.config;
46
+ const re = compile(cfg);
47
+ if (!re)
48
+ return [];
49
+ const findings = [];
50
+ const local = new RegExp(re.source, re.flags);
51
+ let m;
52
+ while ((m = local.exec(ctx.source)) !== null) {
53
+ if (findings.length >= MAX_FINDINGS_PER_DETECTOR)
54
+ break;
55
+ const name = m[1] ?? "";
56
+ const args = m[2] ?? "";
57
+ if (!DYNAMIC_ARG_RE.test(args))
58
+ continue;
59
+ const offset = m.index + (m[0].length - args.length);
60
+ findings.push({
61
+ ruleId: `obf.shell-with-untrusted-input.${cfg.id}`,
62
+ severity: "warn",
63
+ score: 7,
64
+ file: ctx.path,
65
+ line: lineAtOffset(ctx.source, offset),
66
+ snippet: truncateSnippet(`${name}(${args}`),
67
+ reason: `Shell-exec sink \`${name}\` called with an interpolated/concatenated ` +
68
+ `argument. Confirm any user input is escaped or routed through an ` +
69
+ `arg-array form.`,
70
+ evidence: { language: cfg.id, sink: name },
71
+ });
72
+ }
73
+ return findings;
74
+ },
75
+ };
76
+ //# sourceMappingURL=shell-untrusted-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-untrusted-input.js","sourceRoot":"","sources":["../../src/detectors/shell-untrusted-input.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * obf.string-array-decoder.<lang> — Layer B.
3
+ *
4
+ * Two-step pattern: a high-entropy string array AND a decoder/sink wired to
5
+ * it in the same file. This is the structural fingerprint of obfuscator.io
6
+ * output beyond the lexical fingerprint of `encoded-array-fingerprint`.
7
+ *
8
+ * To fire, the file must contain BOTH:
9
+ * - A long array of base64-shaped string literals (≥16 elements), AND
10
+ * - A decoder call from the language config in the same file
11
+ * - A dynamic-exec sink call from the language config in the same file
12
+ */
13
+ import type { Detector } from "../types.js";
14
+ export declare const stringArrayDecoder: Detector;
15
+ //# sourceMappingURL=string-array-decoder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"string-array-decoder.d.ts","sourceRoot":"","sources":["../../src/detectors/string-array-decoder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAwC,MAAM,aAAa,CAAC;AA8BlF,eAAO,MAAM,kBAAkB,EAAE,QA8ChC,CAAC"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * obf.string-array-decoder.<lang> — Layer B.
3
+ *
4
+ * Two-step pattern: a high-entropy string array AND a decoder/sink wired to
5
+ * it in the same file. This is the structural fingerprint of obfuscator.io
6
+ * output beyond the lexical fingerprint of `encoded-array-fingerprint`.
7
+ *
8
+ * To fire, the file must contain BOTH:
9
+ * - A long array of base64-shaped string literals (≥16 elements), AND
10
+ * - A decoder call from the language config in the same file
11
+ * - A dynamic-exec sink call from the language config in the same file
12
+ */
13
+ import { lineAtOffset, MAX_FINDINGS_PER_DETECTOR, MAX_SOURCE_BYTES, namedCallAlternation, } from "../internal/patterns.js";
14
+ import { truncateSnippet } from "../internal/text.js";
15
+ const ARRAY_RE = /\[\s*((?:"[^"\n]{4,}"|'[^'\n]{4,}')(?:\s*,\s*(?:"[^"\n]{4,}"|'[^'\n]{4,}')){15,})\s*\]/g;
16
+ const cache = new WeakMap();
17
+ function compile(config) {
18
+ const cached = cache.get(config);
19
+ if (cached)
20
+ return cached;
21
+ const compiled = {
22
+ decoder: new RegExp(`(?:${namedCallAlternation(config.decoders)})\\s*\\(`, "g"),
23
+ sink: new RegExp(`(?:${namedCallAlternation(config.dynamic_exec_sinks)})\\s*\\(`, "g"),
24
+ };
25
+ cache.set(config, compiled);
26
+ return compiled;
27
+ }
28
+ export const stringArrayDecoder = {
29
+ id: "obf.string-array-decoder",
30
+ docsUrl: "https://github.com/bytebardorg/obfuscan/blob/main/docs/detectors.md#obfstring-array-decoder",
31
+ applies(ctx) {
32
+ return (ctx.config !== null &&
33
+ ctx.config.decoders.length > 0 &&
34
+ ctx.config.dynamic_exec_sinks.length > 0 &&
35
+ ctx.source.length > 0 &&
36
+ ctx.source.length < MAX_SOURCE_BYTES);
37
+ },
38
+ run(ctx) {
39
+ if (!ctx.config)
40
+ return [];
41
+ const src = ctx.source;
42
+ const { decoder, sink } = compile(ctx.config);
43
+ const arrRe = new RegExp(ARRAY_RE.source, ARRAY_RE.flags);
44
+ const arrayMatch = arrRe.exec(src);
45
+ if (!arrayMatch)
46
+ return [];
47
+ const decRe = new RegExp(decoder.source, decoder.flags);
48
+ if (!decRe.exec(src))
49
+ return [];
50
+ const sinkRe = new RegExp(sink.source, sink.flags);
51
+ if (!sinkRe.exec(src))
52
+ return [];
53
+ const findings = [];
54
+ if (findings.length >= MAX_FINDINGS_PER_DETECTOR)
55
+ return findings;
56
+ findings.push({
57
+ ruleId: `obf.string-array-decoder.${ctx.config.id}`,
58
+ severity: "block",
59
+ score: 9,
60
+ file: ctx.path,
61
+ line: lineAtOffset(src, arrayMatch.index),
62
+ snippet: truncateSnippet(arrayMatch[0]),
63
+ reason: `String-array + decoder + dynamic-exec sink present in the same file. ` +
64
+ `This is the obfuscator.io / javascript-obfuscator structural fingerprint.`,
65
+ evidence: { language: ctx.config.id },
66
+ });
67
+ return findings;
68
+ },
69
+ };
70
+ //# sourceMappingURL=string-array-decoder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"string-array-decoder.js","sourceRoot":"","sources":["../../src/detectors/string-array-decoder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * obf.suspicious-io-cluster.<lang> — Layer B.
3
+ *
4
+ * Fires when a single file *both* reads from a known-secrets path
5
+ * (~/.npmrc, ~/.aws/credentials, ~/.ssh/id_*, etc.) *and* makes an
6
+ * outbound network call. The cluster is the signal — either alone is
7
+ * usually benign.
8
+ */
9
+ import type { Detector } from "../types.js";
10
+ export declare const suspiciousIoCluster: Detector;
11
+ //# sourceMappingURL=suspicious-io-cluster.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suspicious-io-cluster.d.ts","sourceRoot":"","sources":["../../src/detectors/suspicious-io-cluster.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAwC,MAAM,aAAa,CAAC;AAoClF,eAAO,MAAM,mBAAmB,EAAE,QA+DjC,CAAC"}