@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,86 @@
|
|
|
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 { lineAtOffset, MAX_FINDINGS_PER_DETECTOR, MAX_SOURCE_BYTES, namedCallAlternation, } from "../internal/patterns.js";
|
|
10
|
+
import { truncateSnippet } from "../internal/text.js";
|
|
11
|
+
// Path fragments that strongly indicate a secrets read.
|
|
12
|
+
const SECRET_PATH_RE = /(\.npmrc|\.aws[\/\\]credentials|\.aws[\/\\]config|\.ssh[\/\\]id_[a-z0-9_]+|\.docker[\/\\]config|\.gitconfig|\.netrc|GITHUB_TOKEN|NPM_TOKEN|AWS_ACCESS_KEY)/g;
|
|
13
|
+
const cache = new WeakMap();
|
|
14
|
+
function compile(config) {
|
|
15
|
+
const cached = cache.get(config);
|
|
16
|
+
if (cached)
|
|
17
|
+
return cached;
|
|
18
|
+
const net = config.network_io ?? [];
|
|
19
|
+
const sec = config.secrets_io ?? [];
|
|
20
|
+
const shell = config.shell_exec ?? [];
|
|
21
|
+
const compiled = {
|
|
22
|
+
network: net.length ? new RegExp(`(?:${namedCallAlternation(net)})\\s*\\(`, "g") : null,
|
|
23
|
+
secretsIo: sec.length ? new RegExp(`(?:${namedCallAlternation(sec)})\\s*\\(`, "g") : null,
|
|
24
|
+
shellExec: shell.length ? new RegExp(`(?:${namedCallAlternation(shell)})\\s*\\(`, "g") : null,
|
|
25
|
+
};
|
|
26
|
+
cache.set(config, compiled);
|
|
27
|
+
return compiled;
|
|
28
|
+
}
|
|
29
|
+
export const suspiciousIoCluster = {
|
|
30
|
+
id: "obf.suspicious-io-cluster",
|
|
31
|
+
docsUrl: "https://github.com/bytebardorg/obfuscan/blob/main/docs/detectors.md#obfsuspicious-io-cluster",
|
|
32
|
+
applies(ctx) {
|
|
33
|
+
return (ctx.config !== null &&
|
|
34
|
+
ctx.source.length > 0 &&
|
|
35
|
+
ctx.source.length < MAX_SOURCE_BYTES);
|
|
36
|
+
},
|
|
37
|
+
run(ctx) {
|
|
38
|
+
if (!ctx.config)
|
|
39
|
+
return [];
|
|
40
|
+
const cfg = ctx.config;
|
|
41
|
+
const src = ctx.source;
|
|
42
|
+
const { network, shellExec } = compile(cfg);
|
|
43
|
+
if (!network)
|
|
44
|
+
return [];
|
|
45
|
+
// 1. Find at least one secret-path reference.
|
|
46
|
+
const secretPathMatch = SECRET_PATH_RE.exec(src);
|
|
47
|
+
SECRET_PATH_RE.lastIndex = 0;
|
|
48
|
+
const hasSecretPath = !!secretPathMatch;
|
|
49
|
+
// 2. Find at least one network call.
|
|
50
|
+
const netRe = new RegExp(network.source, network.flags);
|
|
51
|
+
const netMatch = netRe.exec(src);
|
|
52
|
+
if (!netMatch)
|
|
53
|
+
return [];
|
|
54
|
+
// Secondary cluster: shell + network in the same file. This catches
|
|
55
|
+
// setup.py and similar install-time droppers that fetch and execute.
|
|
56
|
+
const shellMatch = shellExec ? new RegExp(shellExec.source, shellExec.flags).exec(src) : null;
|
|
57
|
+
if (!hasSecretPath && !shellMatch)
|
|
58
|
+
return [];
|
|
59
|
+
const findings = [];
|
|
60
|
+
if (findings.length >= MAX_FINDINGS_PER_DETECTOR)
|
|
61
|
+
return findings;
|
|
62
|
+
// Anchor the finding on whichever appears earlier — gives the user a
|
|
63
|
+
// single jump-to location for the cluster.
|
|
64
|
+
const offset = hasSecretPath
|
|
65
|
+
? Math.min(secretPathMatch.index, netMatch.index)
|
|
66
|
+
: Math.min(shellMatch.index, netMatch.index);
|
|
67
|
+
findings.push({
|
|
68
|
+
ruleId: `obf.suspicious-io-cluster.${cfg.id}`,
|
|
69
|
+
severity: "warn",
|
|
70
|
+
score: 8,
|
|
71
|
+
file: ctx.path,
|
|
72
|
+
line: lineAtOffset(src, offset),
|
|
73
|
+
snippet: truncateSnippet(src.slice(offset, Math.min(src.length, offset + 200))),
|
|
74
|
+
reason: `File reads from a known secrets location AND makes a network call. ` +
|
|
75
|
+
`This is the data-exfil cluster shape that defines supply-chain malware.`,
|
|
76
|
+
evidence: {
|
|
77
|
+
language: cfg.id,
|
|
78
|
+
secretMarker: secretPathMatch?.[0] ?? null,
|
|
79
|
+
shellCall: shellMatch?.[0] ?? null,
|
|
80
|
+
networkCall: netMatch[0],
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
return findings;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=suspicious-io-cluster.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"suspicious-io-cluster.js","sourceRoot":"","sources":["../../src/detectors/suspicious-io-cluster.ts"],"names":[],"mappings":"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"}
|
package/dist/diff.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified-diff parser → DiffFile[].
|
|
3
|
+
*
|
|
4
|
+
* Pure, dependency-free, line-based parser. Handles:
|
|
5
|
+
* - added / modified / deleted file headers (`new file mode`, `deleted file mode`, `/dev/null`)
|
|
6
|
+
* - hunk headers `@@ -a,b +c,d @@`
|
|
7
|
+
* - paths normalized to forward slashes (no Windows backslashes leak through)
|
|
8
|
+
*
|
|
9
|
+
* `addedRanges` is a list of inclusive 1-based [startLine, endLine] tuples
|
|
10
|
+
* pointing into the *post-image* (the file as it exists after the diff is
|
|
11
|
+
* applied), covering only the lines marked with `+` in each hunk.
|
|
12
|
+
*/
|
|
13
|
+
export interface DiffFile {
|
|
14
|
+
/** Workspace-relative POSIX path. */
|
|
15
|
+
path: string;
|
|
16
|
+
status: "added" | "modified" | "deleted";
|
|
17
|
+
/** 1-based inclusive line ranges of added lines in the post-image. Empty for deleted files. */
|
|
18
|
+
addedRanges: ReadonlyArray<readonly [number, number]>;
|
|
19
|
+
}
|
|
20
|
+
export declare function parseDiffToFiles(diff: string): DiffFile[];
|
|
21
|
+
/** True if `line` (1-based) falls within any of the given inclusive ranges. */
|
|
22
|
+
export declare function lineInRanges(line: number, ranges: ReadonlyArray<readonly [number, number]>): boolean;
|
|
23
|
+
//# sourceMappingURL=diff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,QAAQ;IACvB,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC;IACzC,+FAA+F;IAC/F,WAAW,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACvD;AAUD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,CAmHzD;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAC/C,OAAO,CAKT"}
|
package/dist/diff.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified-diff parser → DiffFile[].
|
|
3
|
+
*
|
|
4
|
+
* Pure, dependency-free, line-based parser. Handles:
|
|
5
|
+
* - added / modified / deleted file headers (`new file mode`, `deleted file mode`, `/dev/null`)
|
|
6
|
+
* - hunk headers `@@ -a,b +c,d @@`
|
|
7
|
+
* - paths normalized to forward slashes (no Windows backslashes leak through)
|
|
8
|
+
*
|
|
9
|
+
* `addedRanges` is a list of inclusive 1-based [startLine, endLine] tuples
|
|
10
|
+
* pointing into the *post-image* (the file as it exists after the diff is
|
|
11
|
+
* applied), covering only the lines marked with `+` in each hunk.
|
|
12
|
+
*/
|
|
13
|
+
const FILE_HEADER = /^diff --git a\/(.+?) b\/(.+?)$/;
|
|
14
|
+
const HUNK_HEADER = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/;
|
|
15
|
+
export function parseDiffToFiles(diff) {
|
|
16
|
+
if (!diff || diff.length === 0)
|
|
17
|
+
return [];
|
|
18
|
+
const out = [];
|
|
19
|
+
const lines = diff.split(/\r?\n/);
|
|
20
|
+
let i = 0;
|
|
21
|
+
while (i < lines.length) {
|
|
22
|
+
const line = lines[i] ?? "";
|
|
23
|
+
const header = FILE_HEADER.exec(line);
|
|
24
|
+
if (!header) {
|
|
25
|
+
i++;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
// Default to the b-side path; overridden below if /dev/null appears.
|
|
29
|
+
let path = (header[2] ?? "").replace(/\\/g, "/");
|
|
30
|
+
let status = "modified";
|
|
31
|
+
const addedRanges = [];
|
|
32
|
+
i++;
|
|
33
|
+
// Parse meta lines until we hit the first hunk or the next file header.
|
|
34
|
+
while (i < lines.length) {
|
|
35
|
+
const meta = lines[i] ?? "";
|
|
36
|
+
if (FILE_HEADER.test(meta))
|
|
37
|
+
break;
|
|
38
|
+
if (meta.startsWith("@@"))
|
|
39
|
+
break;
|
|
40
|
+
if (meta.startsWith("new file mode"))
|
|
41
|
+
status = "added";
|
|
42
|
+
else if (meta.startsWith("deleted file mode"))
|
|
43
|
+
status = "deleted";
|
|
44
|
+
else if (meta.startsWith("--- ")) {
|
|
45
|
+
// No-op; b-side path drives reporting.
|
|
46
|
+
}
|
|
47
|
+
else if (meta.startsWith("+++ ")) {
|
|
48
|
+
const rhs = meta.slice(4).trim();
|
|
49
|
+
if (rhs === "/dev/null")
|
|
50
|
+
status = "deleted";
|
|
51
|
+
else if (rhs.startsWith("b/"))
|
|
52
|
+
path = rhs.slice(2).replace(/\\/g, "/");
|
|
53
|
+
else if (rhs !== "/dev/null")
|
|
54
|
+
path = rhs.replace(/\\/g, "/");
|
|
55
|
+
}
|
|
56
|
+
i++;
|
|
57
|
+
}
|
|
58
|
+
// Parse hunks.
|
|
59
|
+
while (i < lines.length) {
|
|
60
|
+
const hunkLine = lines[i] ?? "";
|
|
61
|
+
if (FILE_HEADER.test(hunkLine))
|
|
62
|
+
break;
|
|
63
|
+
if (!hunkLine.startsWith("@@")) {
|
|
64
|
+
i++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const m = HUNK_HEADER.exec(hunkLine);
|
|
68
|
+
if (!m) {
|
|
69
|
+
i++;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const hunk = {
|
|
73
|
+
postStart: parseInt(m[1] ?? "0", 10) || 0,
|
|
74
|
+
};
|
|
75
|
+
i++;
|
|
76
|
+
// Walk hunk body. Lines starting with "+" advance the post-image cursor
|
|
77
|
+
// and contribute to addedRanges. " " lines advance both. "-" lines do
|
|
78
|
+
// not advance the post cursor. "" is ignored.
|
|
79
|
+
let postCursor = hunk.postStart;
|
|
80
|
+
let runStart = null;
|
|
81
|
+
while (i < lines.length) {
|
|
82
|
+
const body = lines[i] ?? "";
|
|
83
|
+
if (FILE_HEADER.test(body) || body.startsWith("@@"))
|
|
84
|
+
break;
|
|
85
|
+
if (body.startsWith("+")) {
|
|
86
|
+
if (runStart === null)
|
|
87
|
+
runStart = postCursor;
|
|
88
|
+
postCursor++;
|
|
89
|
+
i++;
|
|
90
|
+
}
|
|
91
|
+
else if (body.startsWith(" ")) {
|
|
92
|
+
if (runStart !== null) {
|
|
93
|
+
addedRanges.push([runStart, postCursor - 1]);
|
|
94
|
+
runStart = null;
|
|
95
|
+
}
|
|
96
|
+
postCursor++;
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
else if (body.startsWith("-")) {
|
|
100
|
+
if (runStart !== null) {
|
|
101
|
+
addedRanges.push([runStart, postCursor - 1]);
|
|
102
|
+
runStart = null;
|
|
103
|
+
}
|
|
104
|
+
i++;
|
|
105
|
+
}
|
|
106
|
+
else if (body.startsWith("\\")) {
|
|
107
|
+
// "" — skip.
|
|
108
|
+
i++;
|
|
109
|
+
}
|
|
110
|
+
else if (body === "") {
|
|
111
|
+
// Blank line: treat as context unless it's the trailing blank from split.
|
|
112
|
+
if (i + 1 >= lines.length) {
|
|
113
|
+
i++;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
if (runStart !== null) {
|
|
117
|
+
addedRanges.push([runStart, postCursor - 1]);
|
|
118
|
+
runStart = null;
|
|
119
|
+
}
|
|
120
|
+
postCursor++;
|
|
121
|
+
i++;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (runStart !== null)
|
|
128
|
+
addedRanges.push([runStart, postCursor - 1]);
|
|
129
|
+
}
|
|
130
|
+
if (status !== "deleted") {
|
|
131
|
+
out.push({ path, status, addedRanges });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
/** True if `line` (1-based) falls within any of the given inclusive ranges. */
|
|
137
|
+
export function lineInRanges(line, ranges) {
|
|
138
|
+
for (const [start, end] of ranges) {
|
|
139
|
+
if (line >= start && line <= end)
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=diff.js.map
|
package/dist/diff.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.js","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-source suppression directives.
|
|
3
|
+
*
|
|
4
|
+
* Three forms recognized, mirroring eslint's syntax. Comment style is
|
|
5
|
+
* autodetected — anything between `#`, `//`, `/* ... *\/`, `--`, `;`, or `'`
|
|
6
|
+
* is treated as a comment context.
|
|
7
|
+
*
|
|
8
|
+
* // obfuscan-disable-next-line obf.high-entropy-literal
|
|
9
|
+
* const k = "...";
|
|
10
|
+
*
|
|
11
|
+
* const k = "..."; // obfuscan-disable-line obf.high-entropy-literal
|
|
12
|
+
*
|
|
13
|
+
* /* obfuscan-disable-line obf.foo, obf.bar *\/
|
|
14
|
+
*
|
|
15
|
+
* Multiple rule ids may be comma-separated. Omitting the ids disables ALL
|
|
16
|
+
* detectors for that line — discouraged, but supported.
|
|
17
|
+
*
|
|
18
|
+
* The aggregator calls `extractDisableDirectives(source)` once per file and
|
|
19
|
+
* filters findings whose `(line, ruleId)` matches.
|
|
20
|
+
*/
|
|
21
|
+
export interface DisableDirective {
|
|
22
|
+
/** 1-based line number that this directive suppresses. */
|
|
23
|
+
line: number;
|
|
24
|
+
/** Empty array means "all rules". */
|
|
25
|
+
ruleIds: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
export declare function extractDisableDirectives(source: string): DisableDirective[];
|
|
28
|
+
/**
|
|
29
|
+
* True if `(line, ruleId)` is suppressed by any directive in `directives`.
|
|
30
|
+
* A directive with an empty ruleIds array suppresses every rule on its line.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isSuppressedByDirectives(line: number, ruleId: string, directives: readonly DisableDirective[]): boolean;
|
|
33
|
+
//# sourceMappingURL=directives.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"directives.d.ts","sourceRoot":"","sources":["../src/directives.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B;AAKD,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAsB3E;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,SAAS,gBAAgB,EAAE,GACtC,OAAO,CAST"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-source suppression directives.
|
|
3
|
+
*
|
|
4
|
+
* Three forms recognized, mirroring eslint's syntax. Comment style is
|
|
5
|
+
* autodetected — anything between `#`, `//`, `/* ... *\/`, `--`, `;`, or `'`
|
|
6
|
+
* is treated as a comment context.
|
|
7
|
+
*
|
|
8
|
+
* // obfuscan-disable-next-line obf.high-entropy-literal
|
|
9
|
+
* const k = "...";
|
|
10
|
+
*
|
|
11
|
+
* const k = "..."; // obfuscan-disable-line obf.high-entropy-literal
|
|
12
|
+
*
|
|
13
|
+
* /* obfuscan-disable-line obf.foo, obf.bar *\/
|
|
14
|
+
*
|
|
15
|
+
* Multiple rule ids may be comma-separated. Omitting the ids disables ALL
|
|
16
|
+
* detectors for that line — discouraged, but supported.
|
|
17
|
+
*
|
|
18
|
+
* The aggregator calls `extractDisableDirectives(source)` once per file and
|
|
19
|
+
* filters findings whose `(line, ruleId)` matches.
|
|
20
|
+
*/
|
|
21
|
+
const DIRECTIVE_RE = /obfuscan-disable-(next-line|line)\b\s*([A-Za-z0-9_.,\- *]*)/g;
|
|
22
|
+
export function extractDisableDirectives(source) {
|
|
23
|
+
const out = [];
|
|
24
|
+
if (!source)
|
|
25
|
+
return out;
|
|
26
|
+
const lines = source.split("\n");
|
|
27
|
+
for (let i = 0; i < lines.length; i++) {
|
|
28
|
+
const line = lines[i] ?? "";
|
|
29
|
+
DIRECTIVE_RE.lastIndex = 0;
|
|
30
|
+
let m;
|
|
31
|
+
while ((m = DIRECTIVE_RE.exec(line)) !== null) {
|
|
32
|
+
const kind = m[1];
|
|
33
|
+
const idsRaw = (m[2] ?? "").trim();
|
|
34
|
+
const ruleIds = idsRaw
|
|
35
|
+
.split(/[\s,]+/)
|
|
36
|
+
.map(s => s.trim())
|
|
37
|
+
.filter(s => s.length > 0 && /^[A-Za-z0-9_.\-]+$/.test(s));
|
|
38
|
+
const targetLine = kind === "next-line" ? i + 2 : i + 1;
|
|
39
|
+
out.push({ line: targetLine, ruleIds });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* True if `(line, ruleId)` is suppressed by any directive in `directives`.
|
|
46
|
+
* A directive with an empty ruleIds array suppresses every rule on its line.
|
|
47
|
+
*/
|
|
48
|
+
export function isSuppressedByDirectives(line, ruleId, directives) {
|
|
49
|
+
for (const d of directives) {
|
|
50
|
+
if (d.line !== line)
|
|
51
|
+
continue;
|
|
52
|
+
if (d.ruleIds.length === 0)
|
|
53
|
+
return true;
|
|
54
|
+
if (d.ruleIds.some(id => ruleId === id || ruleId.startsWith(id + "."))) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=directives.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"directives.js","sourceRoot":"","sources":["../src/directives.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AASH,MAAM,YAAY,GAChB,8DAA8D,CAAC;AAEjE,MAAM,UAAU,wBAAwB,CAAC,MAAc;IACrD,MAAM,GAAG,GAAuB,EAAE,CAAC;IACnC,IAAI,CAAC,MAAM;QAAE,OAAO,GAAG,CAAC;IACxB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAEjC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC5B,YAAY,CAAC,SAAS,GAAG,CAAC,CAAC;QAC3B,IAAI,CAAyB,CAAC;QAC9B,OAAO,CAAC,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YAC9C,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAClB,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YACnC,MAAM,OAAO,GAAG,MAAM;iBACnB,KAAK,CAAC,QAAQ,CAAC;iBACf,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;iBAClB,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;YAE7D,MAAM,UAAU,GAAG,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YACxD,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,wBAAwB,CACtC,IAAY,EACZ,MAAc,EACd,UAAuC;IAEvC,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI;YAAE,SAAS;QAC9B,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACxC,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,MAAM,KAAK,EAAE,IAAI,MAAM,CAAC,UAAU,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,EAAE,CAAC;YACvE,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error classes — hoisted to a separate module to avoid circular imports
|
|
3
|
+
* between rules.ts, scan.ts, and index.ts.
|
|
4
|
+
*/
|
|
5
|
+
export declare class InvalidScanInputError extends Error {
|
|
6
|
+
readonly name = "InvalidScanInputError";
|
|
7
|
+
}
|
|
8
|
+
export declare class InvalidRuleSetError extends Error {
|
|
9
|
+
readonly details: ReadonlyArray<{
|
|
10
|
+
file: string;
|
|
11
|
+
problem: string;
|
|
12
|
+
}>;
|
|
13
|
+
readonly name = "InvalidRuleSetError";
|
|
14
|
+
constructor(message: string, details: ReadonlyArray<{
|
|
15
|
+
file: string;
|
|
16
|
+
problem: string;
|
|
17
|
+
}>);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,SAAkB,IAAI,2BAA2B;CAClD;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAI1C,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAHpE,SAAkB,IAAI,yBAAyB;gBAE7C,OAAO,EAAE,MAAM,EACN,OAAO,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAIrE"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error classes — hoisted to a separate module to avoid circular imports
|
|
3
|
+
* between rules.ts, scan.ts, and index.ts.
|
|
4
|
+
*/
|
|
5
|
+
export class InvalidScanInputError extends Error {
|
|
6
|
+
name = "InvalidScanInputError";
|
|
7
|
+
}
|
|
8
|
+
export class InvalidRuleSetError extends Error {
|
|
9
|
+
details;
|
|
10
|
+
name = "InvalidRuleSetError";
|
|
11
|
+
constructor(message, details) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.details = details;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAC5B,IAAI,GAAG,uBAAuB,CAAC;CAClD;AAED,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAIjC;IAHO,IAAI,GAAG,qBAAqB,CAAC;IAC/C,YACE,OAAe,EACN,OAAyD;QAElE,KAAK,CAAC,OAAO,CAAC,CAAC;QAFN,YAAO,GAAP,OAAO,CAAkD;IAGpE,CAAC;CACF"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree-sitter query runner — internal facade.
|
|
3
|
+
*
|
|
4
|
+
* STATUS: stub. The real implementation will:
|
|
5
|
+
* 1. Resolve the per-grammar query file `queries/per-grammar/<lang>.scm`
|
|
6
|
+
* 2. Concatenate it with the shared template `queries/shared/<name>.scm`
|
|
7
|
+
* 3. Compile via `web-tree-sitter` Query, cached per (grammar, name)
|
|
8
|
+
* 4. Run against the parsed tree and yield captures
|
|
9
|
+
*
|
|
10
|
+
* Detectors depend only on the `QueryMatch` / `QueryNode` shapes below, so
|
|
11
|
+
* the engine implementation can evolve without rewriting detectors.
|
|
12
|
+
*/
|
|
13
|
+
import type { GrammarHandle } from "../types.js";
|
|
14
|
+
/** A captured node from a tree-sitter query. */
|
|
15
|
+
export interface QueryNode {
|
|
16
|
+
/** Source text of the node. */
|
|
17
|
+
readonly text: string;
|
|
18
|
+
/** 0-based byte offset of the node start in the source. */
|
|
19
|
+
readonly startIndex: number;
|
|
20
|
+
/** 0-based byte offset of the node end (exclusive). */
|
|
21
|
+
readonly endIndex: number;
|
|
22
|
+
/** 0-based row/column of node start. */
|
|
23
|
+
readonly startPosition: {
|
|
24
|
+
readonly row: number;
|
|
25
|
+
readonly column: number;
|
|
26
|
+
};
|
|
27
|
+
/** 0-based row/column of node end. */
|
|
28
|
+
readonly endPosition: {
|
|
29
|
+
readonly row: number;
|
|
30
|
+
readonly column: number;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** A single match from a query — a map from capture name to node. */
|
|
34
|
+
export interface QueryMatch {
|
|
35
|
+
readonly captures: ReadonlyMap<string, QueryNode>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Run a named query against a parsed tree and return all matches.
|
|
39
|
+
*
|
|
40
|
+
* Throws if the query name is unknown for this grammar — detectors should
|
|
41
|
+
* catch and treat as "this language hasn't been ported to this query yet".
|
|
42
|
+
*/
|
|
43
|
+
export declare function runQuery(_grammar: GrammarHandle, _tree: unknown, _queryName: string): readonly QueryMatch[];
|
|
44
|
+
//# sourceMappingURL=query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/grammar/query.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,gDAAgD;AAChD,MAAM,WAAW,SAAS;IACxB,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,wCAAwC;IACxC,QAAQ,CAAC,aAAa,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1E,sCAAsC;IACtC,QAAQ,CAAC,WAAW,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACzE;AAED,qEAAqE;AACrE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACnD;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CACtB,QAAQ,EAAE,aAAa,EACvB,KAAK,EAAE,OAAO,EACd,UAAU,EAAE,MAAM,GACjB,SAAS,UAAU,EAAE,CAIvB"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree-sitter query runner — internal facade.
|
|
3
|
+
*
|
|
4
|
+
* STATUS: stub. The real implementation will:
|
|
5
|
+
* 1. Resolve the per-grammar query file `queries/per-grammar/<lang>.scm`
|
|
6
|
+
* 2. Concatenate it with the shared template `queries/shared/<name>.scm`
|
|
7
|
+
* 3. Compile via `web-tree-sitter` Query, cached per (grammar, name)
|
|
8
|
+
* 4. Run against the parsed tree and yield captures
|
|
9
|
+
*
|
|
10
|
+
* Detectors depend only on the `QueryMatch` / `QueryNode` shapes below, so
|
|
11
|
+
* the engine implementation can evolve without rewriting detectors.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Run a named query against a parsed tree and return all matches.
|
|
15
|
+
*
|
|
16
|
+
* Throws if the query name is unknown for this grammar — detectors should
|
|
17
|
+
* catch and treat as "this language hasn't been ported to this query yet".
|
|
18
|
+
*/
|
|
19
|
+
export function runQuery(_grammar, _tree, _queryName) {
|
|
20
|
+
// Reference stub: returns no matches. Replace with a real implementation
|
|
21
|
+
// that compiles `.scm` files and walks the tree.
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.js","sourceRoot":"","sources":["../../src/grammar/query.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAuBH;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CACtB,QAAuB,EACvB,KAAc,EACd,UAAkB;IAElB,yEAAyE;IACzE,iDAAiD;IACjD,OAAO,EAAE,CAAC;AACZ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @obfuscan/core — public entry point.
|
|
3
|
+
*
|
|
4
|
+
* Stable API surface. Anything not re-exported here is internal and may
|
|
5
|
+
* change without a major version bump.
|
|
6
|
+
*
|
|
7
|
+
* ## Quick start
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { scan } from "@obfuscan/core";
|
|
11
|
+
* import * as fs from "node:fs/promises";
|
|
12
|
+
*
|
|
13
|
+
* const result = await scan(
|
|
14
|
+
* { diff: await fs.readFile("pr.diff", "utf8") },
|
|
15
|
+
* { fileResolver: (p) => fs.readFile(p, "utf8") },
|
|
16
|
+
* );
|
|
17
|
+
*
|
|
18
|
+
* for (const f of result.findings) {
|
|
19
|
+
* console.log(`${f.severity.toUpperCase()} ${f.file}:${f.line} ${f.reason}`);
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* ## Custom rules
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { scan, loadRuleSet } from "@obfuscan/core";
|
|
27
|
+
*
|
|
28
|
+
* const rules = await loadRuleSet({
|
|
29
|
+
* languageDir: "./my-rules/languages",
|
|
30
|
+
* queryDir: "./my-rules/queries",
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* const result = await scan({ dir: "./src" }, { fileResolver, rules });
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* ## Custom detectors
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* import { scan, defaultDetectors, Detector } from "@obfuscan/core";
|
|
40
|
+
*
|
|
41
|
+
* const myDetector: Detector = {
|
|
42
|
+
* id: "my-org.no-fetch-in-tests",
|
|
43
|
+
* applies: (ctx) => /\.test\./.test(ctx.path),
|
|
44
|
+
* run: (ctx) => /* ... *\/ [],
|
|
45
|
+
* };
|
|
46
|
+
*
|
|
47
|
+
* const result = await scan(input, {
|
|
48
|
+
* fileResolver,
|
|
49
|
+
* detectors: [...defaultDetectors(), myDetector],
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export { scan } from "./scan.js";
|
|
54
|
+
export type { Severity, Finding, LanguageConfig, RuleSet, GrammarHandle, PathAllowlistEntry, SnippetAllowlistEntry, Allowlist, ScanInput, ScanOptions, ScanProgress, ScanResult, FileResolver, SymbolResolver, Logger, Detector, FileContext, } from "./types.js";
|
|
55
|
+
/**
|
|
56
|
+
* Load a custom rule set from disk. Use this to point obfuscan at a fork of
|
|
57
|
+
* `@obfuscan/rules` or at an internal extension. If you only need the
|
|
58
|
+
* defaults, omit `rules` from `ScanOptions` and the built-in pack is used.
|
|
59
|
+
*/
|
|
60
|
+
export { loadRuleSet, defaultRuleSet } from "./rules.js";
|
|
61
|
+
/**
|
|
62
|
+
* The shipped detector set. Use this as a base when you want to add or
|
|
63
|
+
* filter detectors:
|
|
64
|
+
*
|
|
65
|
+
* ```ts
|
|
66
|
+
* const dets = defaultDetectors().filter(d => d.id !== "obf.high-entropy-literal");
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export { defaultDetectors } from "./detectors/index.js";
|
|
70
|
+
/**
|
|
71
|
+
* Read/write `.obfuscan/allowlist.json`. The CLI uses these directly.
|
|
72
|
+
* Programmatic users typically pass an inline `allowlist` to `scan()`
|
|
73
|
+
* instead of touching disk.
|
|
74
|
+
*/
|
|
75
|
+
export { loadAllowlist, saveAllowlist, hashSnippet, matchesAllowlist, } from "./allowlist.js";
|
|
76
|
+
/**
|
|
77
|
+
* Parse a unified diff into the shape `scan()` consumes internally.
|
|
78
|
+
* Exposed for hosts (e.g. a Git client) that already have a structured diff
|
|
79
|
+
* representation and want to skip re-serializing.
|
|
80
|
+
*/
|
|
81
|
+
export { parseDiffToFiles } from "./diff.js";
|
|
82
|
+
export type { DiffFile } from "./diff.js";
|
|
83
|
+
/**
|
|
84
|
+
* Parses `// obfuscan-disable-next-line <ruleId>` and similar comment
|
|
85
|
+
* directives. Returns the set of (line, ruleId) pairs to suppress.
|
|
86
|
+
* Used internally by the aggregator; exposed for tooling that wants to
|
|
87
|
+
* surface them in a UI.
|
|
88
|
+
*/
|
|
89
|
+
export { extractDisableDirectives } from "./directives.js";
|
|
90
|
+
export type { DisableDirective } from "./directives.js";
|
|
91
|
+
/** SemVer of the engine. Replaced at build time via `__ENGINE_VERSION__`. */
|
|
92
|
+
export { ENGINE_VERSION } from "./version.js";
|
|
93
|
+
/**
|
|
94
|
+
* `InvalidScanInputError` — thrown when `ScanInput` is missing all of
|
|
95
|
+
* `diff`, `paths`, `dir`, or specifies more than one.
|
|
96
|
+
*
|
|
97
|
+
* `InvalidRuleSetError` — thrown when a custom rule set fails validation.
|
|
98
|
+
* Includes a `details` array describing each problem.
|
|
99
|
+
*/
|
|
100
|
+
export { InvalidScanInputError, InvalidRuleSetError } from "./errors.js";
|
|
101
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAIH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAIjC,YAAY,EAEV,QAAQ,EACR,OAAO,EAEP,cAAc,EACd,OAAO,EACP,aAAa,EACb,kBAAkB,EAClB,qBAAqB,EACrB,SAAS,EAET,SAAS,EACT,WAAW,EACX,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,cAAc,EACd,MAAM,EAEN,QAAQ,EACR,WAAW,GACZ,MAAM,YAAY,CAAC;AAIpB;;;;GAIG;AACH,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAIzD;;;;;;;GAOG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAIxD;;;;GAIG;AACH,OAAO,EACL,aAAa,EACb,aAAa,EACb,WAAW,EACX,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AAIxB;;;;GAIG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,YAAY,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAI1C;;;;;GAKG;AACH,OAAO,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAC3D,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAIxD,6EAA6E;AAC7E,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAI9C;;;;;;GAMG;AACH,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC"}
|