@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.
- package/LICENSE +201 -0
- package/README.md +224 -0
- package/dist/allowlist.d.ts +25 -0
- package/dist/allowlist.d.ts.map +1 -0
- package/dist/allowlist.js +138 -0
- package/dist/allowlist.js.map +1 -0
- package/dist/detectors/bidi-control.d.ts +14 -0
- package/dist/detectors/bidi-control.d.ts.map +1 -0
- package/dist/detectors/bidi-control.js +67 -0
- package/dist/detectors/bidi-control.js.map +1 -0
- package/dist/detectors/cargo-build-rs-network.d.ts +12 -0
- package/dist/detectors/cargo-build-rs-network.d.ts.map +1 -0
- package/dist/detectors/cargo-build-rs-network.js +54 -0
- package/dist/detectors/cargo-build-rs-network.js.map +1 -0
- package/dist/detectors/decode-then-exec.d.ts +20 -0
- package/dist/detectors/decode-then-exec.d.ts.map +1 -0
- package/dist/detectors/decode-then-exec.js +189 -0
- package/dist/detectors/decode-then-exec.js.map +1 -0
- package/dist/detectors/deserializer-untrusted.d.ts +15 -0
- package/dist/detectors/deserializer-untrusted.d.ts.map +1 -0
- package/dist/detectors/deserializer-untrusted.js +99 -0
- package/dist/detectors/deserializer-untrusted.js.map +1 -0
- package/dist/detectors/dockerfile-curl-pipe-shell.d.ts +10 -0
- package/dist/detectors/dockerfile-curl-pipe-shell.d.ts.map +1 -0
- package/dist/detectors/dockerfile-curl-pipe-shell.js +42 -0
- package/dist/detectors/dockerfile-curl-pipe-shell.js.map +1 -0
- package/dist/detectors/dynamic-exec-non-literal.d.ts +17 -0
- package/dist/detectors/dynamic-exec-non-literal.d.ts.map +1 -0
- package/dist/detectors/dynamic-exec-non-literal.js +104 -0
- package/dist/detectors/dynamic-exec-non-literal.js.map +1 -0
- package/dist/detectors/encoded-array-fingerprint.d.ts +11 -0
- package/dist/detectors/encoded-array-fingerprint.d.ts.map +1 -0
- package/dist/detectors/encoded-array-fingerprint.js +60 -0
- package/dist/detectors/encoded-array-fingerprint.js.map +1 -0
- package/dist/detectors/gha-curl-pipe-shell.d.ts +11 -0
- package/dist/detectors/gha-curl-pipe-shell.d.ts.map +1 -0
- package/dist/detectors/gha-curl-pipe-shell.js +42 -0
- package/dist/detectors/gha-curl-pipe-shell.js.map +1 -0
- package/dist/detectors/high-entropy-literal.d.ts +19 -0
- package/dist/detectors/high-entropy-literal.d.ts.map +1 -0
- package/dist/detectors/high-entropy-literal.js +90 -0
- package/dist/detectors/high-entropy-literal.js.map +1 -0
- package/dist/detectors/homoglyph-identifier.d.ts +16 -0
- package/dist/detectors/homoglyph-identifier.d.ts.map +1 -0
- package/dist/detectors/homoglyph-identifier.js +76 -0
- package/dist/detectors/homoglyph-identifier.js.map +1 -0
- package/dist/detectors/index.d.ts +31 -0
- package/dist/detectors/index.d.ts.map +1 -0
- package/dist/detectors/index.js +60 -0
- package/dist/detectors/index.js.map +1 -0
- package/dist/detectors/library-load-non-literal.d.ts +10 -0
- package/dist/detectors/library-load-non-literal.d.ts.map +1 -0
- package/dist/detectors/library-load-non-literal.js +72 -0
- package/dist/detectors/library-load-non-literal.js.map +1 -0
- package/dist/detectors/long-line.d.ts +12 -0
- package/dist/detectors/long-line.d.ts.map +1 -0
- package/dist/detectors/long-line.js +53 -0
- package/dist/detectors/long-line.js.map +1 -0
- package/dist/detectors/manifest-install-script.d.ts +54 -0
- package/dist/detectors/manifest-install-script.d.ts.map +1 -0
- package/dist/detectors/manifest-install-script.js +272 -0
- package/dist/detectors/manifest-install-script.js.map +1 -0
- package/dist/detectors/network-then-exec.d.ts +17 -0
- package/dist/detectors/network-then-exec.d.ts.map +1 -0
- package/dist/detectors/network-then-exec.js +140 -0
- package/dist/detectors/network-then-exec.js.map +1 -0
- package/dist/detectors/perl-makefile-side-effect.d.ts +17 -0
- package/dist/detectors/perl-makefile-side-effect.d.ts.map +1 -0
- package/dist/detectors/perl-makefile-side-effect.js +72 -0
- package/dist/detectors/perl-makefile-side-effect.js.map +1 -0
- package/dist/detectors/python-setup-side-effect.d.ts +10 -0
- package/dist/detectors/python-setup-side-effect.d.ts.map +1 -0
- package/dist/detectors/python-setup-side-effect.js +87 -0
- package/dist/detectors/python-setup-side-effect.js.map +1 -0
- package/dist/detectors/shell-untrusted-input.d.ts +10 -0
- package/dist/detectors/shell-untrusted-input.d.ts.map +1 -0
- package/dist/detectors/shell-untrusted-input.js +76 -0
- package/dist/detectors/shell-untrusted-input.js.map +1 -0
- package/dist/detectors/string-array-decoder.d.ts +15 -0
- package/dist/detectors/string-array-decoder.d.ts.map +1 -0
- package/dist/detectors/string-array-decoder.js +70 -0
- package/dist/detectors/string-array-decoder.js.map +1 -0
- package/dist/detectors/suspicious-io-cluster.d.ts +11 -0
- package/dist/detectors/suspicious-io-cluster.d.ts.map +1 -0
- package/dist/detectors/suspicious-io-cluster.js +86 -0
- package/dist/detectors/suspicious-io-cluster.js.map +1 -0
- package/dist/diff.d.ts +23 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +144 -0
- package/dist/diff.js.map +1 -0
- package/dist/directives.d.ts +33 -0
- package/dist/directives.d.ts.map +1 -0
- package/dist/directives.js +60 -0
- package/dist/directives.js.map +1 -0
- package/dist/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +16 -0
- package/dist/errors.js.map +1 -0
- package/dist/grammar/query.d.ts +44 -0
- package/dist/grammar/query.d.ts.map +1 -0
- package/dist/grammar/query.js +24 -0
- package/dist/grammar/query.js.map +1 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +106 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/patterns.d.ts +48 -0
- package/dist/internal/patterns.d.ts.map +1 -0
- package/dist/internal/patterns.js +95 -0
- package/dist/internal/patterns.js.map +1 -0
- package/dist/internal/text.d.ts +14 -0
- package/dist/internal/text.d.ts.map +1 -0
- package/dist/internal/text.js +20 -0
- package/dist/internal/text.js.map +1 -0
- package/dist/rules.d.ts +25 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +195 -0
- package/dist/rules.js.map +1 -0
- package/dist/scan.d.ts +26 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +287 -0
- package/dist/scan.js.map +1 -0
- package/dist/types.d.ts +215 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +10 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +50 -0
- package/dist/version.js.map +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* obf.bidi-control — Layer A.
|
|
3
|
+
*
|
|
4
|
+
* Flags Unicode bidirectional control characters in source code (Trojan
|
|
5
|
+
* Source, CVE-2021-42574). These are invisible characters that can make
|
|
6
|
+
* source code render differently than it executes.
|
|
7
|
+
*
|
|
8
|
+
* Always blocks: there is essentially no legitimate reason for these to
|
|
9
|
+
* appear in source. They come up in localized strings rarely, but those
|
|
10
|
+
* should be in resource files, not code.
|
|
11
|
+
*/
|
|
12
|
+
import { lineAtOffset, MAX_SOURCE_BYTES } from "../internal/patterns.js";
|
|
13
|
+
import { truncateSnippet } from "../internal/text.js";
|
|
14
|
+
// Listed individually for clarity. Same set as RFC + Rust's lint.
|
|
15
|
+
const BIDI_CHARS = [
|
|
16
|
+
"\u202A", // LRE — Left-to-Right Embedding
|
|
17
|
+
"\u202B", // RLE — Right-to-Left Embedding
|
|
18
|
+
"\u202C", // PDF — Pop Directional Formatting
|
|
19
|
+
"\u202D", // LRO — Left-to-Right Override
|
|
20
|
+
"\u202E", // RLO — Right-to-Left Override
|
|
21
|
+
"\u2066", // LRI — Left-to-Right Isolate
|
|
22
|
+
"\u2067", // RLI — Right-to-Left Isolate
|
|
23
|
+
"\u2068", // FSI — First Strong Isolate
|
|
24
|
+
"\u2069", // PDI — Pop Directional Isolate
|
|
25
|
+
];
|
|
26
|
+
const BIDI_RE = new RegExp(`[${BIDI_CHARS.join("")}]`, "g");
|
|
27
|
+
const NAMES = {
|
|
28
|
+
"\u202A": "LRE", "\u202B": "RLE", "\u202C": "PDF",
|
|
29
|
+
"\u202D": "LRO", "\u202E": "RLO", "\u2066": "LRI",
|
|
30
|
+
"\u2067": "RLI", "\u2068": "FSI", "\u2069": "PDI",
|
|
31
|
+
};
|
|
32
|
+
export const bidiControlChar = {
|
|
33
|
+
id: "obf.bidi-control",
|
|
34
|
+
docsUrl: "https://github.com/bytebardorg/obfuscan/blob/main/docs/detectors.md#obfbidi-control",
|
|
35
|
+
applies(ctx) {
|
|
36
|
+
return ctx.source.length > 0 && ctx.source.length < MAX_SOURCE_BYTES;
|
|
37
|
+
},
|
|
38
|
+
run(ctx) {
|
|
39
|
+
const findings = [];
|
|
40
|
+
const src = ctx.source;
|
|
41
|
+
let m;
|
|
42
|
+
BIDI_RE.lastIndex = 0;
|
|
43
|
+
while ((m = BIDI_RE.exec(src)) !== null) {
|
|
44
|
+
const ch = m[0];
|
|
45
|
+
const codePoint = ch.codePointAt(0);
|
|
46
|
+
const line = lineAtOffset(src, m.index);
|
|
47
|
+
// Take a small window of context around the bidi char for the snippet.
|
|
48
|
+
const winStart = Math.max(0, m.index - 20);
|
|
49
|
+
const winEnd = Math.min(src.length, m.index + 20);
|
|
50
|
+
const snippet = src.slice(winStart, winEnd);
|
|
51
|
+
findings.push({
|
|
52
|
+
ruleId: bidiControlChar.id,
|
|
53
|
+
severity: "block",
|
|
54
|
+
score: 10,
|
|
55
|
+
file: ctx.path,
|
|
56
|
+
line,
|
|
57
|
+
snippet: truncateSnippet(snippet),
|
|
58
|
+
reason: `Unicode bidirectional control character ` +
|
|
59
|
+
`${NAMES[ch] ?? "?"} (U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}) ` +
|
|
60
|
+
`found in source. This is a Trojan Source attack vector (CVE-2021-42574).`,
|
|
61
|
+
evidence: { codePoint, name: NAMES[ch] ?? "unknown" },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return findings;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=bidi-control.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bidi-control.js","sourceRoot":"","sources":["../../src/detectors/bidi-control.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* obf.cargo-build-rs-network — Manifest detector for Cargo `build.rs`.
|
|
3
|
+
*
|
|
4
|
+
* Flags `build.rs` files that perform network IO. `build.rs` runs at compile
|
|
5
|
+
* time on every machine that builds the crate; network-fetching binaries
|
|
6
|
+
* here is a supply-chain malware delivery shape.
|
|
7
|
+
*
|
|
8
|
+
* Allowed: `println!("cargo:...")` directives and pure-compute build logic.
|
|
9
|
+
*/
|
|
10
|
+
import type { Detector } from "../types.js";
|
|
11
|
+
export declare const cargoBuildRsNetwork: Detector;
|
|
12
|
+
//# sourceMappingURL=cargo-build-rs-network.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cargo-build-rs-network.d.ts","sourceRoot":"","sources":["../../src/detectors/cargo-build-rs-network.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAwB,MAAM,aAAa,CAAC;AAgBlE,eAAO,MAAM,mBAAmB,EAAE,QAqCjC,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* obf.cargo-build-rs-network — Manifest detector for Cargo `build.rs`.
|
|
3
|
+
*
|
|
4
|
+
* Flags `build.rs` files that perform network IO. `build.rs` runs at compile
|
|
5
|
+
* time on every machine that builds the crate; network-fetching binaries
|
|
6
|
+
* here is a supply-chain malware delivery shape.
|
|
7
|
+
*
|
|
8
|
+
* Allowed: `println!("cargo:...")` directives and pure-compute build logic.
|
|
9
|
+
*/
|
|
10
|
+
import { lineAtOffset } from "../internal/patterns.js";
|
|
11
|
+
import { truncateSnippet } from "../internal/text.js";
|
|
12
|
+
function isBuildRs(p) {
|
|
13
|
+
return p === "build.rs" || p.endsWith("/build.rs");
|
|
14
|
+
}
|
|
15
|
+
// Network markers in Rust build scripts
|
|
16
|
+
const NETWORK_RE = /\b(?:reqwest::|ureq::|isahc::|hyper::|surf::|attohttpc::|curl::|tokio::net|std::net::TcpStream|std::net::UdpSocket)/g;
|
|
17
|
+
// Process-spawn markers (curl|wget shelled out from build.rs)
|
|
18
|
+
const PROCESS_NETWORK_RE = /Command::new\s*\(\s*"(?:curl|wget|powershell|pwsh)"/g;
|
|
19
|
+
export const cargoBuildRsNetwork = {
|
|
20
|
+
id: "obf.cargo-build-rs-network",
|
|
21
|
+
docsUrl: "https://github.com/bytebardorg/obfuscan/blob/main/docs/detectors.md#obfcargo-build-rs-network",
|
|
22
|
+
applies(ctx) {
|
|
23
|
+
return isBuildRs(ctx.path);
|
|
24
|
+
},
|
|
25
|
+
run(ctx) {
|
|
26
|
+
const findings = [];
|
|
27
|
+
const src = ctx.source;
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
for (const re of [NETWORK_RE, PROCESS_NETWORK_RE]) {
|
|
30
|
+
const local = new RegExp(re.source, re.flags);
|
|
31
|
+
let m;
|
|
32
|
+
while ((m = local.exec(src)) !== null) {
|
|
33
|
+
const line = lineAtOffset(src, m.index);
|
|
34
|
+
if (seen.has(line))
|
|
35
|
+
continue;
|
|
36
|
+
seen.add(line);
|
|
37
|
+
findings.push({
|
|
38
|
+
ruleId: cargoBuildRsNetwork.id,
|
|
39
|
+
severity: "block",
|
|
40
|
+
score: 9,
|
|
41
|
+
file: ctx.path,
|
|
42
|
+
line,
|
|
43
|
+
snippet: truncateSnippet(m[0]),
|
|
44
|
+
reason: `\`build.rs\` performs network IO at compile time. Fetching code or ` +
|
|
45
|
+
`binaries from the network during a build is a supply-chain ` +
|
|
46
|
+
`malware delivery vector.`,
|
|
47
|
+
evidence: { marker: m[0] },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return findings;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=cargo-build-rs-network.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cargo-build-rs-network.js","sourceRoot":"","sources":["../../src/detectors/cargo-build-rs-network.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,SAAS,SAAS,CAAC,CAAS;IAC1B,OAAO,CAAC,KAAK,UAAU,IAAI,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AACrD,CAAC;AAED,wCAAwC;AACxC,MAAM,UAAU,GACd,sHAAsH,CAAC;AAEzH,8DAA8D;AAC9D,MAAM,kBAAkB,GACtB,sDAAsD,CAAC;AAEzD,MAAM,CAAC,MAAM,mBAAmB,GAAa;IAC3C,EAAE,EAAE,4BAA4B;IAChC,OAAO,EAAE,+FAA+F;IAExG,OAAO,CAAC,GAAgB;QACtB,OAAO,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC7B,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;QAE/B,KAAK,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,kBAAkB,CAAC,EAAE,CAAC;YAClD,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC;YAC9C,IAAI,CAAyB,CAAC;YAC9B,OAAO,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;gBACtC,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;gBACxC,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;oBAAE,SAAS;gBAC7B,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACf,QAAQ,CAAC,IAAI,CAAC;oBACZ,MAAM,EAAE,mBAAmB,CAAC,EAAE;oBAC9B,QAAQ,EAAE,OAAO;oBACjB,KAAK,EAAE,CAAC;oBACR,IAAI,EAAE,GAAG,CAAC,IAAI;oBACd,IAAI;oBACJ,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;oBAC9B,MAAM,EACJ,qEAAqE;wBACrE,6DAA6D;wBAC7D,0BAA0B;oBAC5B,QAAQ,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE;iBAC3B,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* obf.decode-then-exec.<lang> — Layer B.
|
|
3
|
+
*
|
|
4
|
+
* Fires when a value flowing out of a *decoder* (base64, hex, gzip, …) flows
|
|
5
|
+
* into a *dynamic-exec sink* (eval, exec, Function, …) — either as a direct
|
|
6
|
+
* argument or via one intermediate variable.
|
|
7
|
+
*
|
|
8
|
+
* Implementation note: the bundled engine is regex-driven over `ctx.source`.
|
|
9
|
+
* Each language config supplies the decoder + sink names; we build two
|
|
10
|
+
* patterns per file:
|
|
11
|
+
*
|
|
12
|
+
* 1. Direct: sink( ... decoder( ... ) ... )
|
|
13
|
+
* 2. Indirect: var = decoder( ... ); ... sink(var)
|
|
14
|
+
*
|
|
15
|
+
* Hosts that want full scope analysis can replace this with a tree-sitter
|
|
16
|
+
* implementation by supplying their own `Detector` via `ScanOptions.detectors`.
|
|
17
|
+
*/
|
|
18
|
+
import type { Detector } from "../types.js";
|
|
19
|
+
export declare const decodeThenExec: Detector;
|
|
20
|
+
//# sourceMappingURL=decode-then-exec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decode-then-exec.d.ts","sourceRoot":"","sources":["../../src/detectors/decode-then-exec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAwC,MAAM,aAAa,CAAC;AAkHlF,eAAO,MAAM,cAAc,EAAE,QA0E5B,CAAC"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* obf.decode-then-exec.<lang> — Layer B.
|
|
3
|
+
*
|
|
4
|
+
* Fires when a value flowing out of a *decoder* (base64, hex, gzip, …) flows
|
|
5
|
+
* into a *dynamic-exec sink* (eval, exec, Function, …) — either as a direct
|
|
6
|
+
* argument or via one intermediate variable.
|
|
7
|
+
*
|
|
8
|
+
* Implementation note: the bundled engine is regex-driven over `ctx.source`.
|
|
9
|
+
* Each language config supplies the decoder + sink names; we build two
|
|
10
|
+
* patterns per file:
|
|
11
|
+
*
|
|
12
|
+
* 1. Direct: sink( ... decoder( ... ) ... )
|
|
13
|
+
* 2. Indirect: var = decoder( ... ); ... sink(var)
|
|
14
|
+
*
|
|
15
|
+
* Hosts that want full scope analysis can replace this with a tree-sitter
|
|
16
|
+
* implementation by supplying their own `Detector` via `ScanOptions.detectors`.
|
|
17
|
+
*/
|
|
18
|
+
import { escapeRegex, lineAtOffset, MAX_FINDINGS_PER_DETECTOR, MAX_SOURCE_BYTES, namedCallAlternation, } from "../internal/patterns.js";
|
|
19
|
+
import { truncateSnippet } from "../internal/text.js";
|
|
20
|
+
const cache = new WeakMap();
|
|
21
|
+
function candidateTokens(names) {
|
|
22
|
+
const out = [];
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
for (const n of names) {
|
|
25
|
+
if (!n)
|
|
26
|
+
continue;
|
|
27
|
+
const tail = n.split(/[.\/:\s]+/).filter(Boolean).pop() ?? n;
|
|
28
|
+
const token = tail.replace(/[^A-Za-z0-9_]/g, "");
|
|
29
|
+
if (token.length < 3)
|
|
30
|
+
continue;
|
|
31
|
+
if (seen.has(token))
|
|
32
|
+
continue;
|
|
33
|
+
seen.add(token);
|
|
34
|
+
out.push(token);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
function maybeRelevantSource(source, config) {
|
|
39
|
+
const sinkTokens = candidateTokens(config.dynamic_exec_sinks);
|
|
40
|
+
const decoderTokens = candidateTokens(config.decoders);
|
|
41
|
+
const hasSink = sinkTokens.some(t => source.includes(t));
|
|
42
|
+
if (!hasSink)
|
|
43
|
+
return false;
|
|
44
|
+
return decoderTokens.some(t => source.includes(t));
|
|
45
|
+
}
|
|
46
|
+
function compile(config) {
|
|
47
|
+
const cached = cache.get(config);
|
|
48
|
+
if (cached)
|
|
49
|
+
return cached;
|
|
50
|
+
const decoders = namedCallAlternation(config.decoders);
|
|
51
|
+
const sinks = namedCallAlternation(config.dynamic_exec_sinks);
|
|
52
|
+
// Direct: sink( ... decoder( ... ) ... )
|
|
53
|
+
// We allow up to ~400 chars between the sink open and the decoder call to
|
|
54
|
+
// accommodate chained method calls (`.toString()`, `.text`, etc.).
|
|
55
|
+
const direct = new RegExp(`(?:${sinks})\\s*\\(([^()]{0,400}(?:\\([^()]*\\)[^()]{0,200}){0,3}(?:${decoders})\\s*\\()`, "g");
|
|
56
|
+
// Bash-specific: `sink "$( ... | base64 -d)"`. Decoders are pipeline words,
|
|
57
|
+
// not function calls. Require the sink to be followed (within ~400 chars)
|
|
58
|
+
// by a `$( ... decoder ... )` subshell; the decoder appears as a bare word
|
|
59
|
+
// possibly with args, e.g. `base64 -d`, `xxd -r -p`.
|
|
60
|
+
const isShell = (config.id === "bash" || (config.aliases ?? []).includes("sh"));
|
|
61
|
+
const bashSubshell = isShell
|
|
62
|
+
? new RegExp(`(?:${sinks})\\s+"?\\$\\([\\s\\S]{0,400}?(?:${decoders})(?:\\s|\\b)`, "g")
|
|
63
|
+
: null;
|
|
64
|
+
const directUnion = bashSubshell
|
|
65
|
+
? new RegExp(`${direct.source}|${bashSubshell.source}`, "g")
|
|
66
|
+
: direct;
|
|
67
|
+
// Indirect-assign: `var X = decoder(...);` and tuple-style assignments.
|
|
68
|
+
const indirectAssign = new RegExp(`([A-Za-z_$][\\w$]*)` +
|
|
69
|
+
`\\s*(?:,\\s*[A-Za-z_$][\\w$]*)*\\s*` +
|
|
70
|
+
`(?::=|=)\\s*` +
|
|
71
|
+
`(?:await\\s+)?` +
|
|
72
|
+
`(?:${decoders})\\s*\\(`, "g");
|
|
73
|
+
const sinkUse = (varName) => new RegExp(`(?:${sinks})\\s*(?:\\(|\\s+)\\s*(?:[A-Za-z_][\\w$]*\\s*\\(\\s*){0,2}[&*]*\\s*${escapeRegex(varName)}\\b`, "g");
|
|
74
|
+
const sinkCall = new RegExp(`(?:${sinks})\\s*(?:\\(|\\s+)([\\s\\S]{0,24})`, "g");
|
|
75
|
+
const decoderCall = new RegExp(`(?:${decoders})\\s*\\(`, "g");
|
|
76
|
+
const compiled = {
|
|
77
|
+
direct: directUnion,
|
|
78
|
+
indirectAssign,
|
|
79
|
+
sinkUse,
|
|
80
|
+
sinkCall,
|
|
81
|
+
decoderCall,
|
|
82
|
+
};
|
|
83
|
+
cache.set(config, compiled);
|
|
84
|
+
return compiled;
|
|
85
|
+
}
|
|
86
|
+
function isLiteralPeek(peek) {
|
|
87
|
+
const trimmed = peek.replace(/^\s+/, "");
|
|
88
|
+
if (trimmed.length === 0)
|
|
89
|
+
return false;
|
|
90
|
+
const c = trimmed[0];
|
|
91
|
+
if (c === '"' || c === "'")
|
|
92
|
+
return true;
|
|
93
|
+
if (c >= "0" && c <= "9")
|
|
94
|
+
return true;
|
|
95
|
+
if (c === "-" && trimmed[1] && trimmed[1] >= "0" && trimmed[1] <= "9")
|
|
96
|
+
return true;
|
|
97
|
+
if (c === "`")
|
|
98
|
+
return !trimmed.includes("${");
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
export const decodeThenExec = {
|
|
102
|
+
id: "obf.decode-then-exec",
|
|
103
|
+
docsUrl: "https://github.com/bytebardorg/obfuscan/blob/main/docs/detectors.md#obfdecode-then-exec",
|
|
104
|
+
applies(ctx) {
|
|
105
|
+
return (ctx.config !== null &&
|
|
106
|
+
ctx.config.dynamic_exec_sinks.length > 0 &&
|
|
107
|
+
ctx.config.decoders.length > 0 &&
|
|
108
|
+
ctx.source.length > 0 &&
|
|
109
|
+
ctx.source.length < MAX_SOURCE_BYTES &&
|
|
110
|
+
maybeRelevantSource(ctx.source, ctx.config));
|
|
111
|
+
},
|
|
112
|
+
run(ctx) {
|
|
113
|
+
if (!ctx.config)
|
|
114
|
+
return [];
|
|
115
|
+
const cfg = ctx.config;
|
|
116
|
+
const src = ctx.source;
|
|
117
|
+
const { direct, indirectAssign, sinkUse, sinkCall, decoderCall } = compile(cfg);
|
|
118
|
+
const findings = [];
|
|
119
|
+
const seen = new Set(); // (line) dedupe
|
|
120
|
+
// Direct flow.
|
|
121
|
+
let m;
|
|
122
|
+
const directRe = new RegExp(direct.source, direct.flags);
|
|
123
|
+
while ((m = directRe.exec(src)) !== null) {
|
|
124
|
+
if (findings.length >= MAX_FINDINGS_PER_DETECTOR)
|
|
125
|
+
break;
|
|
126
|
+
const line = lineAtOffset(src, m.index);
|
|
127
|
+
if (seen.has(line))
|
|
128
|
+
continue;
|
|
129
|
+
seen.add(line);
|
|
130
|
+
findings.push(buildFinding(ctx, cfg.id, m[0], m.index, "direct"));
|
|
131
|
+
}
|
|
132
|
+
// Indirect flow: find decoder assignments, then look for sink(var) later.
|
|
133
|
+
const assignRe = new RegExp(indirectAssign.source, indirectAssign.flags);
|
|
134
|
+
while ((m = assignRe.exec(src)) !== null) {
|
|
135
|
+
if (findings.length >= MAX_FINDINGS_PER_DETECTOR)
|
|
136
|
+
break;
|
|
137
|
+
const varName = m[1];
|
|
138
|
+
if (!varName)
|
|
139
|
+
continue;
|
|
140
|
+
const after = src.slice(m.index + m[0].length);
|
|
141
|
+
const useRe = sinkUse(varName);
|
|
142
|
+
const useMatch = useRe.exec(after);
|
|
143
|
+
if (!useMatch)
|
|
144
|
+
continue;
|
|
145
|
+
const useOffset = m.index + m[0].length + useMatch.index;
|
|
146
|
+
const line = lineAtOffset(src, useOffset);
|
|
147
|
+
if (seen.has(line))
|
|
148
|
+
continue;
|
|
149
|
+
seen.add(line);
|
|
150
|
+
findings.push(buildFinding(ctx, cfg.id, useMatch[0], useOffset, "indirect"));
|
|
151
|
+
}
|
|
152
|
+
// Fallback: if a file contains both a decoder call and a dynamic-exec sink
|
|
153
|
+
// with a non-literal argument, flag it. This catches helper wrappers and
|
|
154
|
+
// statement-style sinks that don't fit the simple assignment model.
|
|
155
|
+
const hasDecoder = new RegExp(decoderCall.source, decoderCall.flags).test(src);
|
|
156
|
+
if (hasDecoder) {
|
|
157
|
+
const sinkRe = new RegExp(sinkCall.source, sinkCall.flags);
|
|
158
|
+
while ((m = sinkRe.exec(src)) !== null) {
|
|
159
|
+
if (findings.length >= MAX_FINDINGS_PER_DETECTOR)
|
|
160
|
+
break;
|
|
161
|
+
const peek = m[1] ?? "";
|
|
162
|
+
if (isLiteralPeek(peek))
|
|
163
|
+
continue;
|
|
164
|
+
const offset = m.index + (m[0].length - peek.length);
|
|
165
|
+
const line = lineAtOffset(src, offset);
|
|
166
|
+
if (seen.has(line))
|
|
167
|
+
continue;
|
|
168
|
+
seen.add(line);
|
|
169
|
+
findings.push(buildFinding(ctx, cfg.id, m[0], offset, "co-located"));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return findings;
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
function buildFinding(ctx, langId, rawSnippet, offset, flow) {
|
|
176
|
+
const line = lineAtOffset(ctx.source, offset);
|
|
177
|
+
return {
|
|
178
|
+
ruleId: `obf.decode-then-exec.${langId}`,
|
|
179
|
+
severity: "block",
|
|
180
|
+
score: 9,
|
|
181
|
+
file: ctx.path,
|
|
182
|
+
line,
|
|
183
|
+
snippet: truncateSnippet(rawSnippet),
|
|
184
|
+
reason: `Decoded value flows into a dynamic-exec sink (${flow}). This is the ` +
|
|
185
|
+
`canonical decode-then-exec obfuscation pattern.`,
|
|
186
|
+
evidence: { language: langId, flow },
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=decode-then-exec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decode-then-exec.js","sourceRoot":"","sources":["../../src/detectors/decode-then-exec.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* obf.deserializer-untrusted.<lang> — Layer B.
|
|
3
|
+
*
|
|
4
|
+
* Fires when an unsafe deserializer (`pickle.loads`, `Marshal.load`,
|
|
5
|
+
* `unserialize`, `BinaryFormatter.Deserialize`, `ObjectInputStream.readObject`)
|
|
6
|
+
* is called with a non-literal argument. These are the canonical RCE-by-
|
|
7
|
+
* deserialization sinks.
|
|
8
|
+
*
|
|
9
|
+
* Safe deserializers (e.g. `JSON.parse`, `json_decode`, `JSON.stringify`)
|
|
10
|
+
* are skipped via an internal allowlist even if a `LanguageConfig` lists them,
|
|
11
|
+
* so authors who include them for completeness don't pay an FP tax.
|
|
12
|
+
*/
|
|
13
|
+
import type { Detector } from "../types.js";
|
|
14
|
+
export declare const deserializerUntrusted: Detector;
|
|
15
|
+
//# sourceMappingURL=deserializer-untrusted.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deserializer-untrusted.d.ts","sourceRoot":"","sources":["../../src/detectors/deserializer-untrusted.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAwC,MAAM,aAAa,CAAC;AAiDlF,eAAO,MAAM,qBAAqB,EAAE,QA8CnC,CAAC"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* obf.deserializer-untrusted.<lang> — Layer B.
|
|
3
|
+
*
|
|
4
|
+
* Fires when an unsafe deserializer (`pickle.loads`, `Marshal.load`,
|
|
5
|
+
* `unserialize`, `BinaryFormatter.Deserialize`, `ObjectInputStream.readObject`)
|
|
6
|
+
* is called with a non-literal argument. These are the canonical RCE-by-
|
|
7
|
+
* deserialization sinks.
|
|
8
|
+
*
|
|
9
|
+
* Safe deserializers (e.g. `JSON.parse`, `json_decode`, `JSON.stringify`)
|
|
10
|
+
* are skipped via an internal allowlist even if a `LanguageConfig` lists them,
|
|
11
|
+
* so authors who include them for completeness don't pay an FP tax.
|
|
12
|
+
*/
|
|
13
|
+
import { lineAtOffset, MAX_FINDINGS_PER_DETECTOR, MAX_SOURCE_BYTES, namedCallAlternation, } from "../internal/patterns.js";
|
|
14
|
+
import { truncateSnippet } from "../internal/text.js";
|
|
15
|
+
/**
|
|
16
|
+
* Names that are deserializers but are *safe* against untrusted input. If a
|
|
17
|
+
* `LanguageConfig` happens to list them (some do, for completeness), the
|
|
18
|
+
* detector still skips them.
|
|
19
|
+
*/
|
|
20
|
+
const SAFE_DESERIALIZERS = new Set([
|
|
21
|
+
"JSON.parse",
|
|
22
|
+
"v8.deserialize",
|
|
23
|
+
"node:v8.deserialize",
|
|
24
|
+
"json_decode",
|
|
25
|
+
]);
|
|
26
|
+
const cache = new WeakMap();
|
|
27
|
+
function compile(config) {
|
|
28
|
+
const cached = cache.get(config);
|
|
29
|
+
if (cached)
|
|
30
|
+
return cached;
|
|
31
|
+
const list = (config.deserializers ?? []).filter((d) => !SAFE_DESERIALIZERS.has(d));
|
|
32
|
+
if (list.length === 0)
|
|
33
|
+
return null;
|
|
34
|
+
const alt = namedCallAlternation(list);
|
|
35
|
+
// Match `deserializer( ... )` and capture a peek at the first argument.
|
|
36
|
+
const re = new RegExp(`(?:^|[^A-Za-z0-9_$])((?:${alt}))\\s*\\(([\\s\\S]{0,16})`, "g");
|
|
37
|
+
cache.set(config, re);
|
|
38
|
+
return re;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Skip definition contexts: `def name(`, `function name(`, `fn name(`,
|
|
42
|
+
* `sub name`, `func name(`. The detector is otherwise too eager when a
|
|
43
|
+
* deserializer's bare tail (e.g. `load`) collides with a function name.
|
|
44
|
+
*/
|
|
45
|
+
const DEFINITION_PREFIX_RE = /(?:\b(?:def|function|fn|func|sub)\s+|class\s+)$/;
|
|
46
|
+
function looksLikeLiteralCall(peek) {
|
|
47
|
+
const t = peek.replace(/^\s+/, "");
|
|
48
|
+
if (t.length === 0)
|
|
49
|
+
return false;
|
|
50
|
+
const c = t[0];
|
|
51
|
+
return c === '"' || c === "'" || (c >= "0" && c <= "9");
|
|
52
|
+
}
|
|
53
|
+
export const deserializerUntrusted = {
|
|
54
|
+
id: "obf.deserializer-untrusted",
|
|
55
|
+
docsUrl: "https://github.com/bytebardorg/obfuscan/blob/main/docs/detectors.md#obfdeserializer-untrusted",
|
|
56
|
+
applies(ctx) {
|
|
57
|
+
return (ctx.config !== null &&
|
|
58
|
+
(ctx.config.deserializers?.length ?? 0) > 0 &&
|
|
59
|
+
ctx.source.length > 0 &&
|
|
60
|
+
ctx.source.length < MAX_SOURCE_BYTES);
|
|
61
|
+
},
|
|
62
|
+
run(ctx) {
|
|
63
|
+
if (!ctx.config)
|
|
64
|
+
return [];
|
|
65
|
+
const cfg = ctx.config;
|
|
66
|
+
const re = compile(cfg);
|
|
67
|
+
if (!re)
|
|
68
|
+
return [];
|
|
69
|
+
const findings = [];
|
|
70
|
+
const local = new RegExp(re.source, re.flags);
|
|
71
|
+
let m;
|
|
72
|
+
while ((m = local.exec(ctx.source)) !== null) {
|
|
73
|
+
if (findings.length >= MAX_FINDINGS_PER_DETECTOR)
|
|
74
|
+
break;
|
|
75
|
+
const name = m[1] ?? "";
|
|
76
|
+
const peek = m[2] ?? "";
|
|
77
|
+
if (looksLikeLiteralCall(peek))
|
|
78
|
+
continue;
|
|
79
|
+
// Skip `def load(...)`, `function load(...)`, etc. — definitions, not calls.
|
|
80
|
+
const lookback = ctx.source.slice(Math.max(0, m.index - 20), m.index + 1);
|
|
81
|
+
if (DEFINITION_PREFIX_RE.test(lookback))
|
|
82
|
+
continue;
|
|
83
|
+
const offset = m.index + (m[0].length - peek.length);
|
|
84
|
+
findings.push({
|
|
85
|
+
ruleId: `obf.deserializer-untrusted.${cfg.id}`,
|
|
86
|
+
severity: "block",
|
|
87
|
+
score: 9,
|
|
88
|
+
file: ctx.path,
|
|
89
|
+
line: lineAtOffset(ctx.source, offset),
|
|
90
|
+
snippet: truncateSnippet(`${name}(${peek}`),
|
|
91
|
+
reason: `Unsafe deserializer \`${name}\` called with a non-literal argument. ` +
|
|
92
|
+
`Untrusted input here is RCE.`,
|
|
93
|
+
evidence: { language: cfg.id, deserializer: name },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return findings;
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
//# sourceMappingURL=deserializer-untrusted.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deserializer-untrusted.js","sourceRoot":"","sources":["../../src/detectors/deserializer-untrusted.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* obf.dockerfile-curl-pipe-shell — Manifest detector for Dockerfiles.
|
|
3
|
+
*
|
|
4
|
+
* Flags `RUN curl|bash` and equivalents in Dockerfiles. Same shape as the
|
|
5
|
+
* GHA detector — a network download piped into a shell — but in the image
|
|
6
|
+
* build path.
|
|
7
|
+
*/
|
|
8
|
+
import type { Detector } from "../types.js";
|
|
9
|
+
export declare const dockerfileCurlPipeShell: Detector;
|
|
10
|
+
//# sourceMappingURL=dockerfile-curl-pipe-shell.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dockerfile-curl-pipe-shell.d.ts","sourceRoot":"","sources":["../../src/detectors/dockerfile-curl-pipe-shell.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAwB,MAAM,aAAa,CAAC;AAYlE,eAAO,MAAM,uBAAuB,EAAE,QA6BrC,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* obf.dockerfile-curl-pipe-shell — Manifest detector for Dockerfiles.
|
|
3
|
+
*
|
|
4
|
+
* Flags `RUN curl|bash` and equivalents in Dockerfiles. Same shape as the
|
|
5
|
+
* GHA detector — a network download piped into a shell — but in the image
|
|
6
|
+
* build path.
|
|
7
|
+
*/
|
|
8
|
+
import { lineAtOffset } from "../internal/patterns.js";
|
|
9
|
+
import { truncateSnippet } from "../internal/text.js";
|
|
10
|
+
function isDockerfile(p) {
|
|
11
|
+
const base = p.slice(p.lastIndexOf("/") + 1);
|
|
12
|
+
return base === "Dockerfile" || base.startsWith("Dockerfile.");
|
|
13
|
+
}
|
|
14
|
+
const RUN_CURL_PIPE_RE = /^\s*RUN\b[^\n]{0,500}(?:curl|wget|fetch)\b[^\n]{0,200}\|\s*(?:bash|sh|zsh|python|node|perl|powershell|pwsh)\b/gm;
|
|
15
|
+
export const dockerfileCurlPipeShell = {
|
|
16
|
+
id: "obf.dockerfile-curl-pipe-shell",
|
|
17
|
+
docsUrl: "https://github.com/bytebardorg/obfuscan/blob/main/docs/detectors.md#obfdockerfile-curl-pipe-shell",
|
|
18
|
+
applies(ctx) {
|
|
19
|
+
return isDockerfile(ctx.path);
|
|
20
|
+
},
|
|
21
|
+
run(ctx) {
|
|
22
|
+
const findings = [];
|
|
23
|
+
const src = ctx.source;
|
|
24
|
+
const re = new RegExp(RUN_CURL_PIPE_RE.source, RUN_CURL_PIPE_RE.flags);
|
|
25
|
+
let m;
|
|
26
|
+
while ((m = re.exec(src)) !== null) {
|
|
27
|
+
findings.push({
|
|
28
|
+
ruleId: dockerfileCurlPipeShell.id,
|
|
29
|
+
severity: "block",
|
|
30
|
+
score: 9,
|
|
31
|
+
file: ctx.path,
|
|
32
|
+
line: lineAtOffset(src, m.index),
|
|
33
|
+
snippet: truncateSnippet(m[0].trim()),
|
|
34
|
+
reason: `Dockerfile RUN pipes a network download into a shell. ` +
|
|
35
|
+
`Pin the artifact (sha256) or fetch + verify before executing.`,
|
|
36
|
+
evidence: { command: m[0].trim() },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return findings;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=dockerfile-curl-pipe-shell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dockerfile-curl-pipe-shell.js","sourceRoot":"","sources":["../../src/detectors/dockerfile-curl-pipe-shell.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,SAAS,YAAY,CAAC,CAAS;IAC7B,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7C,OAAO,IAAI,KAAK,YAAY,IAAI,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;AACjE,CAAC;AAED,MAAM,gBAAgB,GACpB,iHAAiH,CAAC;AAEpH,MAAM,CAAC,MAAM,uBAAuB,GAAa;IAC/C,EAAE,EAAE,gCAAgC;IACpC,OAAO,EAAE,mGAAmG;IAE5G,OAAO,CAAC,GAAgB;QACtB,OAAO,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChC,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,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,CAAC,KAAK,CAAC,CAAC;QACvE,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,uBAAuB,CAAC,EAAE;gBAClC,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,IAAI,EAAE,CAAC;gBACrC,MAAM,EACJ,wDAAwD;oBACxD,+DAA+D;gBACjE,QAAQ,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE;aACnC,CAAC,CAAC;QACL,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
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 type { Detector } from "../types.js";
|
|
16
|
+
export declare const dynamicExecNonLiteral: Detector;
|
|
17
|
+
//# sourceMappingURL=dynamic-exec-non-literal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-exec-non-literal.d.ts","sourceRoot":"","sources":["../../src/detectors/dynamic-exec-non-literal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAwC,MAAM,aAAa,CAAC;AAyClF,eAAO,MAAM,qBAAqB,EAAE,QA0DnC,CAAC"}
|