Thanks for considering a contribution. This project lives or dies on rule quality and rule churn, so the most valuable contributions are usually small.
The fastest paths to merged PRs, in order:
- Add a sink/decoder to an existing language config (1-line PR).
- Add a new language config from
_template.json(~50-line PR). - Add a fixture to the regression corpus showing a real attack we missed (~10-line PR + a comment with the source).
- Report a false positive with a minimal reproducer (issue, not PR).
- Add a new manifest detector (e.g. for a CI system we don't cover).
- Engine work — last because it changes API surface.
You spotted a sink we don't catch in language X. Open packages/rules/languages/X.json, add the fully-qualified name to the relevant array, run the tests, open a PR.
git clone https://github.com/ByteBardOrg/obfuscan
cd obfuscan
cd packages/core
npm ci --legacy-peer-deps
npm run test:allIf the core test suite passes, your PR is in good shape.
The lists in each language config are deliberately narrow. Use this as the test:
| List | Test |
|---|---|
dynamic_exec_sinks |
Does this function execute its first non-literal argument as code or bytecode? |
decoders |
Does this function turn opaque bytes/strings back into something an attacker would want to execute? Base64, hex, gunzip, decrypt, deserialize-to-string. |
deserializers |
Does this function materialize attacker-controlled objects/code? pickle.loads, BinaryFormatter.Deserialize, ObjectInputStream.readObject, Marshal.load, yaml.load. |
network_io |
Does this function make an outbound network connection? |
secrets_io |
Does this function read environment, credentials, keychains, or browser stores? |
shell_exec |
Does this function spawn an OS process? Distinct from in-process eval. |
library_load |
Does this function load a dynamic library or in-memory assembly? |
If you're unsure which list, err on the side of leaving it out rather than adding it to two. The detector framework boosts scores when sinks combine with decoders or network calls — duplication double-counts.
- Standard library functions that are merely common.
console.log,print,String.prototype.replace. These produce noise without signal. - Functions that can be misused but require a specific argument shape. Those belong in language-specific detectors under
detectors/lang-specific/<lang>.ts. - Functions only dangerous in old runtimes. If
assert($s)was deprecated in PHP 7.2 and removed in 8.0, document it but keep the rule — old shells still appear in audits.
Use the fully qualified name as users actually write it in source, not the runtime name. For example:
- Python:
base64.b64decode(notb64decodeor_pybase64.b64decode). - Node:
child_process.exec(notexecorcp.exec). - .NET:
System.Convert.FromBase64String(full namespace). - PowerShell:
[System.Convert]::FromBase64Stringand[Convert]::FromBase64Stringif the short form is commonly used.
The FQN resolver handles import aliases, destructuring (const { exec } = require('child_process')), using directives, and from x import y as z. You don't need to enumerate aliases.
For the default scanner, adding a language is primarily a rules-and-fixtures change:
packages/rules/languages/<id>.json— copy_template.jsonand fill in the lists. Theidmust match the tree-sitter grammar's package name (tree-sitter-<id>).- Fixtures — add at least one malicious and one benign fixture under
packages/core/test/fixtures/.
Tree-sitter query support is optional and host-provided. If you are maintaining a host integration or custom parser-backed detector, also add packages/rules/queries/per-grammar/<id>.scm to map that grammar's node names onto the normalized captures (@call, @callee, @first-arg, @decoder, @netcall, @array). @obfuscan/rules ships query text, not parser grammars, and the default @obfuscan/core engine does not execute these .scm queries by itself.
A complete checklist:
-
languages/<id>.jsonfollowspackages/rules/languages/_schema.json. - At least one malicious fixture shows the language's canonical malware shape.
- At least one benign-but-tricky fixture prevents obvious false positives.
-
cd packages/core && npm run test:allpasses. - Add the language to the coverage table in
README.md.
If the language has obfuscation patterns that don't fit the generic detectors (PowerShell backtick-escapes, bash ${!var}, PHP assert($dyn), Python cmdclass, Rust build.rs), open an issue or PR that explains the shape and includes fixtures. Keep language-specific detector code rare — every detector is maintenance debt.
Every detector change needs a fixture. Two kinds:
fixtures/
malicious/
<lang>/
<case>/
input.<ext> # the malicious source
expected.json # findings we expect
SOURCE.md # where this came from (URL, CVE, advisory)
benign/
<lang>/
<case>/
input.<ext>
expected.json
SOURCE.md # why this looks suspicious but is benign
expected.json declares rule-id prefixes with mustFlag, mustNotFlag, mustBlock, or expectClean. Extra findings are tolerated unless a benign fixture emits a block finding.
For real-world malware samples, never commit the live payload. Truncate or replace the executable portion with a stub that preserves the syntactic shape. The detector's job is to flag the shape, not to run the malware.
False positives kill adoption faster than missed detections. To report one:
- Open an issue with the minimal source snippet that triggers the wrong finding.
- Include the
ruleIdandobfuscanversion. - Explain why this code is legitimate — be specific, "it's our internal tool" is not enough. "It's the WebAssembly polyfill loader from
@stdlib/wasm, used at module init" is.
We treat FPs as severity-1 bugs. The fix is usually one of:
- Tighten the sink list: remove an entry that fires on too much legitimate code.
- Tighten a query: add a path predicate (
#not-in-test-file?) or scope the match. - Add a built-in suppression: e.g. "skip
obf.high-entropy-literalinside acryptographytest vector file with a known-good hash." - Lower the score: demote a rule from
blocktowarn.
Detector framework, scoring, aggregation, suppression, the flow walker. Higher bar than rule changes:
- API surface changes need an RFC issue first.
- All changes must be diff-aware: a finding on a line not in
addedRangesis a bug. - Performance: a 10k-line file should scan in < 500ms on a 2020 laptop. Add a benchmark fixture if you change anything in the parse / query / flow path.
- Memory: trees and queries are cached per-process; new caches need explicit eviction policy.
- TypeScript strict mode, no
anyunless documented. - Keep formatting consistent with the surrounding file. Dedicated lint/format tooling has not landed yet.
- Public API is exported only from
packages/core/src/index.ts. Anything not re-exported there is internal and may change without notice. - Detectors never throw to the pipeline — catch and log internally. One bad detector must not kill a scan.
- Conventional Commits (
feat(rules): add Marshal.load to ruby deserializers). - One logical change per PR.
- Rule-only PRs: title prefix
rules:. They go through a faster review path and ship in the next@obfuscan/rulespatch. - Engine PRs: title prefix
core:. They wait for a@obfuscan/coreminor release.
We follow the Contributor Covenant 2.1. Be kind. Security communities are small and reputations are forever.
By submitting a PR you agree your contribution is licensed under Apache-2.0 (same as the project). No CLA required.
Open a discussion. Issues are for tracked work; discussions are for "is this even the right approach."