python bindings - 1-3-7/disrobe GitHub Wiki

Python bindings

Use Disrobe's recovery engines from Python to decompile bytecode, analyze native code, deobfuscate scripts, and extract packaged files. The disrobe module uses the same Rust implementations as the CLI and provides typed report objects. It supports Python 3.9+ through PyO3's stable ABI.

Most analysis calls leave file I/O to the caller. The optional Flutter engine-symbol cache reads and writes the directory passed as cache_dir. Build the bindings from source; wheels are not published to PyPI.

The WebAssembly example shows an addition function recovered as WAT. The 0.10.5 compatibility results cover 60 API checks for typed results, error payloads, artifact round trips, and CLI surface mappings.

Install

Install Python 3.9+, the repository's Rust toolchain, and the target's native compiler and linker. Windows MSVC builds require the Visual C++ build tools, Windows SDK, and a Python installation that includes python3.lib. See the source build prerequisites.

Create a virtual environment:

git clone https://github.com/1-3-7/disrobe
cd disrobe/bindings/python
python -m venv .venv

Activate it with source .venv/bin/activate on Linux or macOS, or .\.venv\Scripts\Activate.ps1 in Windows PowerShell. Then install the build tool and compile the extension into that environment:

python -m pip install "maturin>=1.5,<2.0"
maturin develop --release

For a redistributable wheel:

maturin build --release --out dist
python -m pip install --no-index --find-links dist disrobe

The pyproject.toml requires maturin>=1.5,<2.0, takes the version from the Rust workspace, sets module-name = "disrobe.disrobe", and points at crates/disrobe-python/Cargo.toml. On Windows the crate's build.rs searches PYO3_PYTHON, an active VIRTUAL_ENV, and standard install locations; set PYO3_PYTHON=<path-to-python.exe> if none is found. A py.typed marker and a .pyi stub provide type information for the documented report attributes.

from __future__ import annotations

import disrobe

version: str = disrobe.__version__

Report model

Analysis functions generally return concrete report classes. Some functions return text, bytes, collections, or tuples; the function table gives each return type. The stub's _Report base describes the serialization methods shared by report objects.

_Report

Member Signature Notes
raw @property -> dict[str, Any] Full underlying record; no detail dropped
to_json () -> str Compact JSON string
from_json_str classmethod(text: str) -> Self Rebuild from a to_json string
from_obj classmethod(obj: dict[str, Any]) -> Self Wrap an already-decoded dict

Reports compare equal when their underlying records are equal (== / !=).

_LlmReport

Subclasses _Report. Adds one property:

Member Signature Notes
llm @property -> LlmBundle | None Populated when metadata output was requested; None otherwise

Metadata-wired passes: py_decompile, py_disasm, py_deob, pyarmor_detect, pyarmor_unpack. Functions that build a metadata bundle accept pack: Pack | None where Pack = Literal["pack-1", "pack-2", "pack-3", "pack-4"].

LlmBundle

A TypedDict mirroring the disrobe.metadata.llm.v2 on-disk schema. The type name is stable; the schema id changes only when the bundle shape breaks compatibility. The payload is deterministic metadata, not model output. Every top-level key except generated_at is always present, and the categories present depend on which pack was requested. Pipeline steps record no durations. generated_at appears only when SOURCE_DATE_EPOCH is set, as an RFC 3339 UTC timestamp; a value that is not a whole number of seconds up to 9999-12-31T23:59:59Z raises DisrobeError.

The LlmBundle name describes a type in the stub; it is not a runtime export. To retain a complete report and its metadata, serialize the report:

from __future__ import annotations

from pathlib import Path

import disrobe

pyc_bytes: bytes = Path("module.pyc").read_bytes()
report: disrobe.PyDisasmReport = disrobe.py_disasm(pyc_bytes, pack="pack-2")
metadata_json: str = report.to_json()

print(metadata_json)

Literal type aliases

These names are defined in the type stub, not exported by the runtime module. Pass the documented string values to functions; do not import the aliases at runtime.

Alias Values
Pack "pack-1", "pack-2", "pack-3", "pack-4"
RoundtripStatus "perfect", "semantic", "code-diff", "no-interpreter", "recompile-failed", "skipped"
PyarmorUnpackStatus "functional", "bcc-partial", "detect-only", "skeleton"
ChainVerdict "ok", "complete", "fan-out", "fan-out-partial", "stalled", "cycle", "cap-reached", "extracted", "error", "dry-run"
ChainVerdictGrade "ok", "incomplete", "failed"
ContainerListing "enumerated", "requires-extraction", "unreadable"
SymbolKind "function", "data", "label", "export", "import"
InstructionFlow "sequential", "call", "indirect-call", "conditional-branch", "unconditional-branch", "indirect-branch", "return", "interrupt"
SourceLanguage "python", "py", "python3"
JsLanguage "javascript", "js", "typescript", "ts"
ByteLanguage "python-bytecode", "pyc", "jvm-class", "class", "dex", "beam", "hermes", "hermes-bundle", "hbc", "wasm"
ParseByteLanguage "go", "swift", "objc", "objective-c", "kotlin", "ruby", "lua", "php"
DisasmByteLanguage "ruby", "ruby-bytecode", "yarv", "mruby", "php", "php-bytecode"
DecompileLanguage "python-bytecode", "pyc", "jvm-class", "class", "java", "kotlin", "lua", "ruby", "php", "php-bytecode", "javascript", "js", "typescript", "ts"

Exception hierarchy

Class Base Raised when
DisrobeError Exception An underlying recovery or analysis operation fails
UnsupportedLanguage DisrobeError disasm/parse/compile/decompile for a language with no backing implementation; message includes a hint

Invalid Python argument types can also raise standard Python exceptions.

Module-level functions: full surface

Category Function Returns
Auto chain auto(input, *, max_depth=8, path_hint=None) ChainReport
Generic dispatch decompile(language, source) CanonicalSource
disasm(language, source) str
parse(language, source) typed report or dict[str, Any]
compile(language, source, *, version=None) bytes
Custom pass register_pass(name, callable) None
register_consumer(name, callable) None
registered_passes() list[str]
registered_consumers() list[str]
unregister(name) bool
run_pass(name, data) Any
run_chain(names, data) Any
emit(name, result, **context) Any
Analysis strings_extract(data, *, min_len=4, decode=True) StringsReport
ioc_extract(data) IocReport
behavior_analyze(data) BehaviorReport
identify(data) IdentifyReport
secret_scan(data, *, redact=False) SecretScanReport
capabilities(binary_bytes) Capabilities
extract(data, out_dir) ExtractionResult
extract_recursive(data, *, source_label='inline', max_depth=8) OverlayReport
yara_parse(ruleset_source) YaraReport
yara_generate(data, *, name=None) YaraReport
Native native_symbols(data) SymbolsReport
native_disasm(data) DisasmPayload
native_callgraph(data) CallGraph
native_imports_dot(data) str
native_entropy(data) EntropyReport
native_sbom(data) SbomReport
native_fingerprint(data, *, flirt=None) FingerprintReport
native_signatures(data, *, flirt=None) SignatureReport
native_sigmaker(data, at) SigmakerReport
native_diff(a, b) DiffReport
native_match(a, b, *, stage=None, function=None, limit=None) NativeMatch
native_patch(data, *, at, replacement=None, nop_start=None, nop_end=None) tuple[bytes, PatchReport]
native_format(binary_bytes) NativeFormat
native_detect(binary_bytes) DetectionList
native_probe_backends() BackendList
native_deobfuscate(code, *, bits=64, base=0, entry=0) NativeDeobfuscation
Query IR query_functions(dr_bytes) FunctionList
query_calls_to(dr_bytes, target) QueryReport
query_xrefs_to(dr_bytes, symbol) QueryReport
query_string_decoders(dr_bytes) QueryReport
query_complexity_over(dr_bytes, threshold) QueryReport
query_capability_sites(dr_bytes, capability) QueryReport
query_call_graph(dr_bytes) CallGraph
Envelope envelope_create(payload, *, source_label='inline', produced_by=None, detected_format=None) bytes
envelope_verify(envelope_bytes) EnvelopeReport
LLM renders agents_md(result) str
skill_md(result) str
provenance(result) Provenance
Python decompile py_decompile(pyc_bytes, *, roundtrip=False, pack=None) PyDecompileReport
py_disasm(pyc_bytes, *, pack=None) PyDisasmReport
Python deobfuscate py_deob(source, *, cleanup=True, pack=None) PyDeobReport
py_deob_detect(source) PyDeobDetection
py_deob_list_passes() list[ObfuscatorPass]
py_deob_detect_pass(source, pass_id) PyDeobDetection
PyArmor pyarmor_detect(source, *, pack=None) PyarmorDetection
pyarmor_unpack(payload, *, runtime=None, pack=None) PyarmorUnpack
pyarmor_classify(source, payload) PyarmorClassification
PyInstaller pyinstaller_extract(image_bytes) PyInstallerArchive
pyinstaller_entry_bytes(image_bytes, entry_name) bytes
Nuitka nuitka_detect(image_bytes) NuitkaDetection
nuitka_extract(image_bytes) NuitkaExtraction
Hermes hermes_disasm(bundle_bytes) HermesDisassembly
hermes_lift(bundle_bytes) HermesLift
hermes_info(bundle_bytes) HermesInfo
Flutter flutter_engine_symbols(input, engine_symbol_map=None, *, source=None, cache_dir=None, no_cache=False) FlutterEngineSymbols
Mach-O macho_dump(macho_bytes) MachoReport
swift_analyze(macho_bytes) SwiftReport
JVM / Android jvm_parse_class(class_bytes) JvmClass
jvm_parse_dex(dex_bytes) DexFileReport
jvm_decompile_class(class_bytes) JvmDecompiledClass
jvm_decompile_dex(dex_bytes) JvmDecompiledDex
jvm_detect(class_bytes) DetectionList
jvm_backends() JvmBackends
apk_resources(apk_bytes) ApkResources
jvm_jni_link(dex_bytes, native_libs) JniLink
.NET dotnet_parse_pe(pe_bytes) DotnetPe
dotnet_parse_metadata(pe_bytes) DotnetMetadata
dotnet_detect(pe_bytes) DotnetDetection
dotnet_analyze(pe_bytes) DotnetAnalysis
dotnet_decompile(pe_bytes) DotnetDecompilation
dotnet_recover_decoders(pe_bytes) DotnetDecoders
dotnet_native_aot(image_bytes) DotnetNativeAot
dotnet_backends() BackendList
WebAssembly wasm_analyze(wasm_bytes) WasmAnalysis
wasm_detect(wasm_bytes) WasmDetection
wasm_lift(wasm_bytes, *, target) WasmLift
JavaScript js_detect(js_source) JsDetection
js_unminify(js_source) JsUnminify
js_unbundle(js_source, *, bundler=None) JsUnbundle
Lua lua_detect(bytecode) LuaDetection
lua_decompile(bytecode) LuaDecompilation
lua_deobfuscate(source, *, authorize=False) LuaDeobfuscation
Go go_analyze(binary_bytes) GoAnalysis
go_symbols(binary_bytes) GoSymbols
go_pclntab(binary_bytes) GoPclntab
go_garble(binary_bytes) GarbleReport
Ruby ruby_detect(ruby_bytes, *, source_path=None) RubyDetection
ruby_decompile(ruby_bytes, *, source_path=None) RubyAnalysis
PHP php_detect(php_bytes) PhpDetection
php_scan(php_bytes) PhpScan
php_decode(php_bytes, *, max_depth=None) PhpDecode
Shell batch_deobfuscate(script, *, args=None) BatchDeobReport
powershell_detect(script) PowershellDetection
powershell_deobfuscate(script) PowershellDeobfuscation
Containers byte_coverage(image_bytes) ByteCoverage
container_detect(container_bytes) ContainerDetection
container_members(container_bytes) ContainerMembers
Pickle pickle_disasm(pickle_bytes) str
pickle_decompile(pickle_bytes) PickleDecompilation
pickle_safety(pickle_bytes) PickleSafety
pickle_trace(pickle_bytes) PickleTrace
pickle_polyglot(file_bytes) PicklePolyglot
pickle_ml_detect(file_bytes) PickleMlReport

Auto chain

from __future__ import annotations

from pathlib import Path

import disrobe

chain: disrobe.ChainReport = disrobe.auto(Path("sample.bin").read_bytes(), max_depth=8)

passes: list[str] = chain.passes
verdict: disrobe.ChainVerdict | None = chain.verdict
grade: disrobe.ChainVerdictGrade | None = chain.verdict_grade
print(chain.node_count, chain.pass_count, passes, verdict, grade)

full_plan_json: str = chain.to_json()
print(full_plan_json)

auto runs the chain detector against raw bytes and returns a ChainReport carrying the full chain.json plan the CLI produces; it does not write stage outputs to disk. max_depth must be 1-16. Other values in the unsigned-byte range raise DisrobeError; values below 0 or above 255 raise OverflowError. The registered pass tree covers pyarmor, pyinstaller, nuitka, py-decompile, py-deob, container, js, jvm, dotnet, wasm, mobile, swift-objc, and the native packer detector.

The accessors read the disrobe.chain/v2 document. to_json() returns the complete plan, including per-node hashes, detector picks, and statistics. The document carries no clock and no duration, so one input gives the same JSON on every call.

Accessor Value
spec: str | None The raw chain specification, such as "auto:8"
node_count: int Length of nodes, including the root input node and extracted children
pass_count: int Number of nodes whose pass is set
passes: list[str] Those pass ids in node order, such as ["native.packer-unpack"]
verdict: ChainVerdict | None The chain-level verdict
verdict_grade: ChainVerdictGrade | None The grade the core assigns to that verdict; None for an unrecognised verdict
final_format: str | None The final format tag, when the chain reached one

The earlier terminated accessor was removed because the document has no termination field; read verdict and verdict_grade instead.

Generic dispatch

Language-keyed entry points that fan out to the per-language passes.

decompile

def decompile(language: str, source: str | bytes) -> CanonicalSource: ...

Wired families: python-bytecode/pyc (py-decompile), jvm-class/class/java/kotlin (JVM lifter), lua (register lifter), ruby (YARV/mruby recovery), php/php-bytecode (eval-chain peel/op-array skeleton), javascript/js/typescript/ts (unminify). Binary-only targets (go, swift, wasm) have no single source body; call their structural binding or parse instead.

from __future__ import annotations

from pathlib import Path

import disrobe

recovered: disrobe.CanonicalSource = disrobe.decompile(
    "python-bytecode", Path("module.pyc").read_bytes()
)

source: str | None = recovered.source
language: str | None = recovered.language
produced_by: str | None = recovered.produced_by
confidence: float | None = recovered.confidence

CanonicalSource accessors

Property Type
source str | None
language str | None
produced_by str | None
confidence float | None

disasm

def disasm(language: str, source: str | bytes) -> str: ...

Returns a rendered instruction listing as text. Wired: python/pyc, jvm-class/class, dex, beam, hermes, wasm, ruby/yarv/mruby, and php/php-bytecode. For Lua use decompile('lua', ...) or parse('lua', ...) instead.

parse

def parse(language: str, source: str | bytes) -> (
    dict[str, Any]
    | GoAnalysis
    | SwiftReport
    | JvmClass
    | RubyAnalysis
    | LuaDecompilation
    | PhpDecode
    | JsUnminify
): ...

Returns a typed report for structural-recovery languages: go -> GoAnalysis, swift/objc/objective-c -> SwiftReport, kotlin -> JvmClass, ruby -> RubyAnalysis, lua -> LuaDecompilation, php -> PhpDecode, javascript/js/typescript/ts -> JsUnminify. Container and bytecode formats (pyc, jvm-class, dex, wasm, hermes, beam) return a nested dict[str, Any] because their full parse records have no single typed shape.

For a classfile report with typed attributes, use the dedicated parser:

from __future__ import annotations

from pathlib import Path

import disrobe

class_bytes: bytes = Path("Hello.class").read_bytes()
parsed: disrobe.JvmClass = disrobe.jvm_parse_class(class_bytes)
method_count: int = parsed.method_count

print(method_count)

compile

def compile(language: str, source: str, *, version: str | None = None) -> bytes: ...

Implemented for Python only; returns raw marshal.dumps bytes (no .pyc header) via the host interpreter. lua, ruby, and php raise UnsupportedLanguage with a hint pointing at the CLI subcommand or toolchain.

from __future__ import annotations

import disrobe

blob: bytes = disrobe.compile("python", "x: int = 1 + 2\n")
listing: str = disrobe.disasm("python", "x: int = 1 + 2\n")

Custom pass plugin protocol

Register and compose named passes and output consumers in the host process.

Named passes can return different Python objects. Check a registry result before using operations specific to its expected type.

from __future__ import annotations

import disrobe


def reverse_bytes(data: bytes) -> bytes:
    return data[::-1]


def print_result(result: object, **context: object) -> None:
    print(result, context)


disrobe.register_pass("reverse", reverse_bytes)
disrobe.register_consumer("print", print_result)

try:
    output: object = disrobe.run_chain(["reverse", "reverse"], b"hello")
    if not isinstance(output, bytes):
        raise TypeError("the reverse chain must return bytes")

    restored: bytes = output
    disrobe.emit("print", restored, source="example")
finally:
    disrobe.unregister("reverse")
    disrobe.unregister("print")

Pass and OutputConsumer protocols

These protocols describe callables in the stub. They are not exported as runtime classes; register ordinary Python functions with the corresponding signatures.

Protocol Signature
Pass __call__(self, data: Any) -> Any
OutputConsumer __call__(self, result: Any, **context: Any) -> Any

Analysis

strings_extract

def strings_extract(data: bytes, *, min_len: int = 4, decode: bool = True) -> StringsReport: ...

Extracts ASCII and UTF-16 strings from a binary blob.

ioc_extract

def ioc_extract(data: bytes) -> IocReport: ...

Harvests indicators of compromise from bytes and recovered strings.

behavior_analyze

def behavior_analyze(data: bytes) -> BehaviorReport: ...

Behavioral summary by category with MITRE ATT&CK IDs.

identify

def identify(data: bytes) -> IdentifyReport: ...

Compiler/linker/packer/protector/installer fingerprint. A signed PE's Authenticode certificate validity is judged at the time SOURCE_DATE_EPOCH names, or at the current time when it is unset; a value that is not a whole number of seconds raises DisrobeError.

secret_scan

def secret_scan(data: bytes, *, redact: bool = False) -> SecretScanReport: ...

Set redact=True to replace detected values, previews, messages, and URI credentials with stable redaction tokens. The default report retains the matched values. Redaction preserves the finding count and byte offsets.

Leaked-credential scan over raw bytes.

capabilities

def capabilities(binary_bytes: bytes) -> Capabilities: ...

Capability rule-set matches for a native binary.

extract

def extract(data: bytes, out_dir: str) -> ExtractionResult: ...

Carves container/firmware members to out_dir.

extract_recursive

def extract_recursive(
    data: bytes, *, source_label: str = "inline", max_depth: int = 8
) -> OverlayReport: ...

Recursive multi-magic carve; classifies every chunk by entropy and nesting.

yara_parse / yara_generate

def yara_parse(ruleset_source: str) -> YaraReport: ...
def yara_generate(data: bytes, *, name: str | None = None) -> YaraReport: ...

Parse a YARA ruleset AST or generate a candidate rule from a binary blob.

from __future__ import annotations

from pathlib import Path

import disrobe

data: bytes = Path("suspect.bin").read_bytes()

strings: disrobe.StringsReport = disrobe.strings_extract(data, min_len=6)
string_count: int = strings.string_count

iocs: disrobe.IocReport = disrobe.ioc_extract(data)
indicator_count: int = iocs.indicator_count

behavior: disrobe.BehaviorReport = disrobe.behavior_analyze(data)
category_count: int = behavior.category_count

ident: disrobe.IdentifyReport = disrobe.identify(data)
fmt: str | None = ident.format
finding_count: int = ident.finding_count

caps: disrobe.Capabilities = disrobe.capabilities(data)
match_count: int = caps.match_count

overlay: disrobe.OverlayReport = disrobe.extract_recursive(data, max_depth=4)
chunks_total: int | None = overlay.chunks_total
bytes_carved: int | None = overlay.bytes_carved

rule: disrobe.YaraReport = disrobe.yara_generate(data, name="suspect")
rule_count: int = rule.rule_count

Analysis report classes

Class Notable typed accessors
StringsReport string_count: int
IocReport indicator_count: int
BehaviorReport category_count: int
IdentifyReport format: str | None, finding_count: int
SecretScanReport finding_count: int
Capabilities match_count: int (entries in capabilities), matched_rules: int
ExtractionResult kind: str | None, entry_count: int, integrity_violation_count: int
OverlayReport max_depth: int | None, nodes_visited: int | None, chunks_total: int | None, bytes_carved: int | None
YaraReport rule_count: int

Native binary

Functions

Function Returns Notes
native_symbols(data) SymbolsReport Symbols, sections, imports, debug info
native_disasm(data) DisasmPayload Full disassembly: functions, stream, symbols
native_callgraph(data) CallGraph Whole-program call graph
native_imports_dot(data) str GraphViz DOT of the import graph
native_entropy(data) EntropyReport Sliding-window Shannon entropy map
native_sbom(data) SbomReport CycloneDX 1.5 SBOM from cargo-auditable section
native_fingerprint(data, *, flirt=None) FingerprintReport Crypto-constant + FLIRT + string-xref sidecar
native_signatures(data, *, flirt=None) SignatureReport Crypto-primitive signatures and FLIRT matches
native_sigmaker(data, at) SigmakerReport Wildcarded byte signature for a VA
native_diff(a, b) DiffReport Function-level diff of two binaries
native_patch(data, *, at, ...) tuple[bytes, PatchReport] Rewrite bytes and revalidate
native_format(binary_bytes) NativeFormat Format: kind, bitness, subsystem
native_detect(binary_bytes) DetectionList Packer/protector detection hits
native_probe_backends() BackendList Probe for installed external tools
native_deobfuscate(code, *, bits=64, base=0, entry=0) NativeDeobfuscation x86 OLLVM/Tigress deflattening
from __future__ import annotations

from pathlib import Path

import disrobe

data: bytes = Path("binary.elf").read_bytes()

syms: disrobe.SymbolsReport = disrobe.native_symbols(data)
symbol_count: int = syms.symbol_count
section_count: int = syms.section_count
import_count: int = syms.import_count

disasm_payload: disrobe.DisasmPayload = disrobe.native_disasm(data)
function_count: int = disasm_payload.function_count
instruction_count: int = disasm_payload.instruction_count

entropy: disrobe.EntropyReport = disrobe.native_entropy(data)
mean: float | None = entropy.mean

sig: disrobe.SigmakerReport = disrobe.native_sigmaker(data, at=0x1000)
ida_pattern: str | None = sig.ida_pattern

patched_bytes: bytes
patch_report: disrobe.PatchReport
patched_bytes, patch_report = disrobe.native_patch(
    data, at=0x1234, nop_start=0x1234, nop_end=0x1240
)
bytes_written: int | None = patch_report.bytes_written
edit_count: int = patch_report.edit_count

code: bytes = Path("function.x86.bin").read_bytes()
deob: disrobe.NativeDeobfuscation = disrobe.native_deobfuscate(
    code, bits=64, base=0x400000
)
recovered_blocks: int | None = deob.recovered_blocks
covered_states: int | None = deob.covered_states
dispatcher_states: int | None = deob.dispatcher_states
fully_recovered: bool = deob.fully_recovered

Native report classes

Class Notable typed accessors
SymbolsReport symbol_count: int, section_count: int, import_count: int
DisasmPayload function_count: int, instruction_count: int
CallGraph node_count: int, edge_count: int
EntropyReport window_count: int, mean: float | None, min: float | None, max: float | None
SbomReport component_count: int, bom_format: str | None, spec_version: str | None
FingerprintReport crypto_hit_count: int
SignatureReport signature_count: int
SigmakerReport ida_pattern: str | None, byte_count: int
DiffReport added: int, removed: int, changed: int
PatchReport at: int | None (first applied edit), bytes_written: int | None, edit_count: int, format: str | None, image_base: int | None
NativeFormat kind: str | None, bits: int | None, subsystem: str | None
DetectionList count: int
BackendList count: int, available_count: int
NativeDeobfuscation bits: int | None, recovered_blocks: int | None, original_blocks: int | None, dispatcher_states: int | None, covered_states: int | None, fully_recovered: bool

Query IR

The query functions operate on a Disasm- or Mir-rung .dr envelope (raw bytes). See Editable IR objects for how to produce and consume .dr envelopes programmatically.

Function Returns Notes
query_functions(dr_bytes) FunctionList All recovered functions
query_calls_to(dr_bytes, target) QueryReport Call sites targeting a symbol name
query_xrefs_to(dr_bytes, symbol) QueryReport Data/code cross-references to a symbol
query_string_decoders(dr_bytes) QueryReport Functions with string-decode patterns
query_complexity_over(dr_bytes, threshold) QueryReport Functions with cyclomatic complexity above threshold
query_capability_sites(dr_bytes, capability) QueryReport Sites exercising a named capability
query_call_graph(dr_bytes) CallGraph Whole-program call graph from IR
from __future__ import annotations

from pathlib import Path

import disrobe

dr: bytes = Path("module.dr").read_bytes()

functions: disrobe.FunctionList = disrobe.query_functions(dr)
fn_count: int = functions.count

callers: disrobe.QueryReport = disrobe.query_calls_to(dr, "malloc")
match_count: int = callers.match_count

complex_fns: disrobe.QueryReport = disrobe.query_complexity_over(dr, threshold=20)
graph: disrobe.CallGraph = disrobe.query_call_graph(dr)
edge_count: int = graph.edge_count

Query report classes

Class Notable typed accessors
FunctionList kind: str | None, count: int
QueryReport kind: str | None, match_count: int
CallGraph node_count: int, edge_count: int

Envelope

envelope_create wraps a payload as a Raw-rung .dr envelope and returns the encoded bytes. envelope_verify decodes and verifies, returning an EnvelopeReport.

from __future__ import annotations

import disrobe

envelope: bytes = disrobe.envelope_create(
    b"payload",
    source_label="inline",
    produced_by="my-tool",
)
report: disrobe.EnvelopeReport = disrobe.envelope_verify(envelope)
ok: bool = report.verified
root_hash: str | None = report.root_hash
rung: str | None = report.rung
hot_bytes: int | None = report.hot_bytes
cold_bytes: int | None = report.cold_bytes
version: int | None = report.version

EnvelopeReport accessors

Property Type
verified bool
rung str | None
version int | None
hot_bytes int | None
cold_bytes int | None
root_hash str | None

The sidecar DrEnvelope TypedDict (bindings/python/dr-envelope.pyi) mirrors the raw on-disk header shape: magic, version, rung, flags, hot_len, cold_len, root_hash.

Metadata renders

agents_md and skill_md render the AGENTS.md and SKILL.md reconstruction briefs for a report with metadata attached (or a bare bundle dict), returning a str. provenance extracts tool/selection/input metadata as a typed Provenance. Each accepts a typed report or its raw dict and returns the same result for both. Passing a report whose llm slot is None raises DisrobeError; passing any other kind of object raises TypeError.

from __future__ import annotations

from pathlib import Path

import disrobe

report: disrobe.PyDecompileReport = disrobe.py_decompile(
    Path("module.pyc").read_bytes(), pack="pack-2"
)

agents_brief: str = disrobe.agents_md(report)
skill_brief: str = disrobe.skill_md(report)

prov: disrobe.Provenance = disrobe.provenance(report)
generated_at: str | None = prov.generated_at
schema: str | None = prov.schema
schema_version: str | None = prov.schema_version

Provenance accessors

Property Type
schema str | None
schema_version str | None
generated_at str | None

generated_at is None unless SOURCE_DATE_EPOCH was set when the bundle was built.

Python passes

See also Python decompiler for the full decompiler design.

py_decompile

Decompiles a .pyc with its header to source. Normalized opcode-structure agreement on the fixed 574-module CPython 3.14 core population is

96.5% (17637 of 18276 code objects); the pinned

200-module corpus is 97.66% (6139 of 6286, regression threshold 97.66%).

The 574-module population excludes idlelib and turtledemo. The structural comparison omits jump targets, most operands and __annotate__ code objects; see the comparison rules.

from __future__ import annotations

from pathlib import Path

import disrobe

report: disrobe.PyDecompileReport = disrobe.py_decompile(
    Path("module.pyc").read_bytes(), roundtrip=True
)

source: str | None = report.source
marshal_version: str | None = report.marshal_version
decompile_version: str | None = report.decompile_version
recovered_directly: bool = report.recovered_directly
fallback_reason: str | None = report.fallback_reason
status: disrobe.RoundtripStatus | None = report.roundtrip_status
roundtrip_detail: str | None = report.roundtrip_detail
interpreter_path: str | None = report.interpreter_path
interpreter_version: str | None = report.interpreter_version

if status == "perfect":
    print("recompiled bytecode matched")

Round-tripping (when roundtrip=True) invokes a matching host interpreter to compile the recovered source and compare bytecode. It does not execute the recovered program.

py_disasm

from __future__ import annotations

from pathlib import Path

import disrobe

result: disrobe.PyDisasmReport = disrobe.py_disasm(Path("module.pyc").read_bytes())

marshal_version: str | None = result.marshal_version
instruction_count: int = result.instruction_count
text: str | None = result.text

py_deob, py_deob_detect, py_deob_list_passes, py_deob_detect_pass

from __future__ import annotations

import disrobe

obfuscated: str = "exec(__import__('base64').b64decode('cHJpbnQoMSk='))\n"

deob: disrobe.PyDeobReport = disrobe.py_deob(obfuscated, cleanup=True)
peeled_source: str | None = deob.peeled_source
cleanup_source: str | None = deob.cleanup_source
layer_count: int = deob.layer_count

detection: disrobe.PyDeobDetection = disrobe.py_deob_detect(obfuscated)
match_count: int = detection.match_count

passes: list[disrobe.ObfuscatorPass] = disrobe.py_deob_list_passes()
first_id: str | None = passes[0].id if passes else None

if first_id is not None:
    per_pass: disrobe.PyDeobDetection = disrobe.py_deob_detect_pass(
        obfuscated, first_id
    )
    print(per_pass.match_count)

py_deob_detect_pass raises DisrobeError for an unknown pass_id.

Python pass report classes

Class Notable typed accessors
PyDecompileReport source, marshal_version, decompile_version, recovered_directly, fallback_reason, roundtrip_status, roundtrip_detail, interpreter_path, interpreter_version, llm
PyDisasmReport marshal_version: str | None, instruction_count: int, text: str | None, llm
PyDeobReport peeled_source: str | None, cleanup_source: str | None, layer_count: int, llm
PyDeobDetection match_count: int (matched markers), confidence: float | None, llm
ObfuscatorPass id: str | None

PyArmor

pyarmor_detect

Parses a PyArmor wrapper from source text.

from __future__ import annotations

from pathlib import Path

import disrobe

detection: disrobe.PyarmorDetection = disrobe.pyarmor_detect(
    Path("wrapped.py").read_text(encoding="utf-8")
)
version: str | None = detection.version
protection: str | None = detection.protection
confidence: str | None = detection.confidence
serial: str | None = detection.serial
python_major: int | None = detection.python_major
python_minor: int | None = detection.python_minor
payload_offset: int | None = detection.payload_offset
payload_size: int | None = detection.payload_size

pyarmor_unpack

Accepts the binary payload extracted from a PyArmor wrapper: the bytes literal passed as the third argument of __pyarmor__, not the wrapper source. The earlier keyword wrapper_bytes= is accepted as an alias for payload; passing both raises TypeError.

Without runtime, v8/v9 payloads return detection metadata and status "detect-only". Pass the bytes of the matching pyarmor_runtime extension module as runtime to decrypt the payload statically; the runtime is parsed, not loaded or executed. A runtime the core cannot parse raises DisrobeError. The report describes the result and does not expose plaintext bytes. There is no --allow-dynamic equivalent.

The core static path, supplied with the matching runtime, decrypts each body and decodes one complete root CodeObject from its header-anchored marshal stream on the 72 / 72 manifest-named v8/v9 default-trial wrappers. That structural check does not compare source, emitted .pyc bytes, semantic or execution behavior, or external-tool output.

from __future__ import annotations

from pathlib import Path

import disrobe

unpacked: disrobe.PyarmorUnpack = disrobe.pyarmor_unpack(
    Path("payload.bin").read_bytes(),
    runtime=Path("pyarmor_runtime_000000/pyarmor_runtime.pyd").read_bytes(),
)

status: disrobe.PyarmorUnpackStatus | None = unpacked.status
pyarmor_version: str | None = unpacked.pyarmor_version
protection_kind: str | None = unpacked.protection_kind
plaintext_len: int | None = unpacked.plaintext_len
digest: str | None = unpacked.plaintext_blake3_hex
bcc_blob_count: int | None = unpacked.bcc_blob_count
inner_cipher_recovered_co: int | None = unpacked.inner_cipher_recovered_co

pyarmor_classify

from __future__ import annotations

from pathlib import Path

import disrobe

payload: bytes = Path("payload.bin").read_bytes()

classification: disrobe.PyarmorClassification = disrobe.pyarmor_classify(
    Path("wrapped.py").read_text(encoding="utf-8"), payload
)
script_type: str | None = classification.script_type
bootstrap_import: str | None = classification.bootstrap_import
disposition: str | None = classification.disposition
rft_enabled: bool = classification.rft_enabled
ecc_enabled: bool = classification.ecc_enabled

The sidecar PyarmorDetection TypedDict (bindings/python/pyarmor-detection.pyi) names the confidence and protection Literal values used in the raw dict.

PyArmor report classes

Class Notable typed accessors
PyarmorDetection version, protection, confidence, serial, python_major, python_minor, payload_offset, payload_size, llm
PyarmorUnpack status, pyarmor_version, protection_kind, plaintext_len, plaintext_blake3_hex, bcc_blob_count, inner_cipher_recovered_co, llm
PyarmorClassification script_type, bootstrap_import, disposition, rft_enabled, ecc_enabled

PyInstaller and Nuitka

pyinstaller_extract / pyinstaller_entry_bytes

from __future__ import annotations

from pathlib import Path

import disrobe

image: bytes = Path("app.exe").read_bytes()

archive: disrobe.PyInstallerArchive = disrobe.pyinstaller_extract(image)
entry_count: int = archive.entry_count
encrypted: bool = archive.encrypted
encryption_key_present: bool = archive.encryption_key_present
python_major: int | None = archive.python_major
python_minor: int | None = archive.python_minor

archive_json: str = archive.to_json()
print(archive_json)

Use an entry name from the archive report to recover its bytes:

from __future__ import annotations

from pathlib import Path

import disrobe

image: bytes = Path("app.exe").read_bytes()
entry_name: str = "main"
main_payload: bytes = disrobe.pyinstaller_entry_bytes(image, entry_name)
print(len(main_payload))

nuitka_detect / nuitka_extract

from __future__ import annotations

from pathlib import Path

import disrobe

image: bytes = Path("app.exe").read_bytes()

det: disrobe.NuitkaDetection = disrobe.nuitka_detect(image)
flavor: str | None = det.flavor
version: str | None = det.version
wheel_marker: str | None = det.wheel_marker
onefile_payload_offset: int | None = det.onefile_payload_offset
onefile_payload_compressed: bool = det.onefile_payload_compressed

extraction: disrobe.NuitkaExtraction = disrobe.nuitka_extract(image)
variant: str | None = extraction.variant

The sidecar FreezerManifest TypedDict (bindings/python/freezer-manifest.pyi) describes the manifest schema for cx-freeze/py2exe/shiv/pex/py-oxidizer/briefcase freezer families; reach it via report.raw.

PyInstaller/Nuitka report classes

Class Notable typed accessors
PyInstallerArchive entry_count: int, encrypted: bool, encryption_key_present: bool, python_major: int | None, python_minor: int | None, llm
NuitkaDetection flavor: str | None, version: str | None, wheel_marker: str | None, onefile_payload_offset: int | None, onefile_payload_compressed: bool, llm
NuitkaExtraction variant: str | None, llm

Hermes (React Native)

All 8 functions in the committed hermesc-built HBC v96 sample lift at 100% op-coverage with 0 fallback ops. A 122,633-function production React Native bundle parses with no module-parse failure in the local scale harness.

from __future__ import annotations

from pathlib import Path

import disrobe

bundle: bytes = Path("index.android.bundle").read_bytes()

disasm_result: disrobe.HermesDisassembly = disrobe.hermes_disasm(bundle)
function_count: int = disasm_result.function_count
identifier_count: int = disasm_result.identifier_count
string_count: int = disasm_result.string_count

lift: disrobe.HermesLift = disrobe.hermes_lift(bundle)
function_surface_count: int = lift.function_surface_count

info: disrobe.HermesInfo = disrobe.hermes_info(bundle)
version: int | None = info.version
header_size: int | None = info.header_size

Hermes report classes

Class Notable typed accessors
HermesDisassembly function_count: int, identifier_count: int, string_count: int, llm
HermesLift function_surface_count: int, string_count: int, identifier_count: int, llm
HermesInfo version: int | None, function_count: int | None, string_count: int | None, header_size: int | None, llm

Mach-O and Swift

from __future__ import annotations

from pathlib import Path

import disrobe

data: bytes = Path("universal.dylib").read_bytes()

macho: disrobe.MachoReport = disrobe.macho_dump(data)
kind: str | None = macho.kind
fat_entry_count: int = macho.fat_entry_count
slice_count: int = macho.slice_count

swift: disrobe.SwiftReport = disrobe.swift_analyze(data)
container: str | None = swift.container
swift_fat_entry_count: int = swift.fat_entry_count
swift_slice_count: int = swift.slice_count

Mach-O report classes

Class Notable typed accessors
MachoReport kind: str | None, fat_entry_count: int, slice_count: int, llm
SwiftReport container: str | None, fat_entry_count: int, slice_count: int, llm

JVM and Android

131 of 131 JVM methods recompile error-free under javac (CI floor 131 of 131, JDK 25). 100% of verifier-presented committed DEX classes pass -Xverify:all, with 37 of 155 classes link-skipped and never reaching the verifier.

from __future__ import annotations

from pathlib import Path

import disrobe

cls: disrobe.JvmClass = disrobe.jvm_parse_class(Path("Hello.class").read_bytes())
major_version: int | None = cls.major_version
minor_version: int | None = cls.minor_version
method_count: int = cls.method_count
field_count: int = cls.field_count
constant_pool_count: int = cls.constant_pool_count

dex: disrobe.DexFileReport = disrobe.jvm_parse_dex(Path("classes.dex").read_bytes())
class_count: int = dex.class_count
dex_method_count: int = dex.method_count

decompiled: disrobe.JvmDecompiledClass = disrobe.jvm_decompile_class(
    Path("Hello.class").read_bytes()
)
source: str | None = decompiled.source
fully_lifted_methods: int = decompiled.fully_lifted_methods
fallback_methods: int = decompiled.fallback_methods

decompiled_dex: disrobe.JvmDecompiledDex = disrobe.jvm_decompile_dex(
    Path("classes.dex").read_bytes()
)
dex_source: str | None = decompiled_dex.source
source_count: int = decompiled_dex.source_count
dex_fully_lifted_methods: int = decompiled_dex.fully_lifted_methods
dex_fallback_methods: int = decompiled_dex.fallback_methods

detections: disrobe.DetectionList = disrobe.jvm_detect(Path("obf.class").read_bytes())
detection_count: int = detections.count

backends: disrobe.JvmBackends = disrobe.jvm_backends()
jvm_count: int = backends.jvm_count
android_count: int = backends.android_count

apk: disrobe.ApkResources = disrobe.apk_resources(Path("app.apk").read_bytes())
package: str | None = apk.package
manifest_xml: str | None = apk.manifest_xml
resource_entry_count: int = apk.resource_entry_count
certificate_count: int = apk.certificate_count
dex_count: int = apk.dex_count

jvm_backends and dotnet_backends probe the host for installed external tools but never shell out to them. Counts are informational only.

JVM/Android report classes

Class Notable typed accessors
JvmClass major_version: int | None, minor_version: int | None, method_count: int, field_count: int, constant_pool_count: int, llm
DexFileReport string_count: int, type_count: int, class_count: int, method_count: int, llm
JvmDecompiledClass source: str | None, method_count: int, field_count: int, fully_lifted_methods: int, fallback_methods: int
JvmDecompiledDex source: str | None, source_count: int, class_count: int, method_count: int, fully_lifted_methods: int, fallback_methods: int
DetectionList count: int
JvmBackends jvm_count: int, android_count: int, llm
ApkResources package: str | None, manifest_xml: str | None, resource_entry_count: int, certificate_count: int, dex_count: int, llm

.NET

from __future__ import annotations

from pathlib import Path

import disrobe

pe_bytes: bytes = Path("Sample.dll").read_bytes()

pe: disrobe.DotnetPe = disrobe.dotnet_parse_pe(pe_bytes)
bitness: str | None = pe.bitness
machine: int | None = pe.machine
section_count: int = pe.section_count
entry_point_rva: int | None = pe.entry_point_rva

metadata: disrobe.DotnetMetadata = disrobe.dotnet_parse_metadata(pe_bytes)
version: str | None = metadata.version
major_runtime_version: int | None = metadata.major_runtime_version
stream_count: int = metadata.stream_count

detection: disrobe.DotnetDetection = disrobe.dotnet_detect(pe_bytes)
primary: str | None = detection.primary
match_count: int = detection.match_count

analysis: disrobe.DotnetAnalysis = disrobe.dotnet_analyze(pe_bytes)
pe_bitness: str | None = analysis.pe_bitness
is_native_aot: bool = analysis.native_aot
primary_protector: str | None = analysis.primary_protector
opcode_spec_coverage_pct: int | None = analysis.opcode_spec_coverage_pct

decompilation: disrobe.DotnetDecompilation = disrobe.dotnet_decompile(pe_bytes)
module_name: str | None = decompilation.module_name
methods_decompiled: int | None = decompilation.methods_decompiled
methods_bodyless: int | None = decompilation.methods_bodyless
methods_failed: int | None = decompilation.methods_failed

decoders: disrobe.DotnetDecoders = disrobe.dotnet_recover_decoders(pe_bytes)
pure_decoders_found: int | None = decoders.pure_decoders_found
constants_recovered: int = decoders.constants_recovered

image_bytes: bytes = Path("HelloAppAot.exe").read_bytes()

native_aot: disrobe.DotnetNativeAot = disrobe.dotnet_native_aot(image_bytes)
runtime_label: str | None = native_aot.runtime_label
method_count: int = native_aot.method_count
methods_with_code_range: int = native_aot.methods_with_code_range
managed_signature_count: int = native_aot.managed_signature_count
register_signature_count: int = native_aot.register_signature_count

backend_list: disrobe.BackendList = disrobe.dotnet_backends()
available_count: int = backend_list.available_count

dotnet_native_aot reads an ahead-of-time compiled .NET image rather than a managed assembly, so it accepts PE, ELF and Mach-O input. It never raises on a file that is not a NativeAOT image: is_native_aot is then false and every count is zero.

managed_signature_count and register_signature_count partition the method bodies disrobe recovered. A body counts as managed when its prototype was reattached from the image's own managed signature. It counts as registers when reattachment abstained and the body kept the types the machine-code lifter inferred from register use, which happens for generics, varargs, a hidden struct return, and the other cases the pass declines rather than guesses. The two counts separate a prototype disrobe sourced from metadata from one it could only infer, so read them as a pair.

.NET report classes

Class Notable typed accessors
DotnetPe bitness: str | None, machine: int | None, section_count: int, entry_point_rva: int | None, llm
DotnetMetadata version: str | None, major_runtime_version: int | None, stream_count: int, llm
DotnetDetection primary: str | None, match_count: int, llm
DotnetAnalysis pe_bitness: str | None, clr_runtime_version: str | None, native_aot: bool, primary_protector: str | None, opcode_spec_coverage_pct: int | None, llm
DotnetDecompilation module_name: str | None, methods_decompiled: int | None, methods_bodyless: int | None, methods_failed: int | None, llm
DotnetDecoders pure_decoders_found: int | None, constants_recovered: int, llm
DotnetNativeAot is_native_aot: bool, runtime_label: str | None, recovered_symbol_count: int, recovered_name_count: int, eager_class_constructors: int | None, type_count: int, method_count: int, methods_with_entrypoint: int, methods_with_code_range: int, methods_with_body: int, managed_signature_count: int, register_signature_count: int, llm

WebAssembly

1034 of 1034 opcodes lowered across the 38 parseable corpus modules (133 of 133 functions), against an instruction inventory wasm-tools produced rather than disrobe. 57 of 57 execution-eligible functions are execution-equivalent under wasmtime.

from __future__ import annotations

from pathlib import Path

import disrobe

wasm_bytes: bytes = Path("module.wasm").read_bytes()

analysis: disrobe.WasmAnalysis = disrobe.wasm_analyze(wasm_bytes)
import_count: int = analysis.import_count
export_count: int = analysis.export_count
func_count: int | None = analysis.func_count
code_size_bytes: int | None = analysis.code_size_bytes
has_dwarf: bool = analysis.has_dwarf

detection: disrobe.WasmDetection = disrobe.wasm_detect(wasm_bytes)
obfuscator: str | None = detection.obfuscator
confidence: float | None = detection.confidence
has_name_section: bool = detection.has_name_section
function_count: int | None = detection.function_count

WebAssembly report classes

Class Notable typed accessors
WasmAnalysis import_count: int, export_count: int, func_count: int | None, code_size_bytes: int | None, has_dwarf: bool, llm
WasmDetection obfuscator: str | None, confidence: float | None, has_name_section: bool, function_count: int | None, llm

JavaScript

Detects 11 bundlers. The explicit target hint accepts auto, webpack4, webpack5/webpack, vite, rollup, esbuild, turbopack, and bun. An unrecognized hint string raises DisrobeError.

from __future__ import annotations

from pathlib import Path

import disrobe

source: str = Path("main.js").read_text(encoding="utf-8")

detection: disrobe.JsDetection = disrobe.js_detect(source)
family: str | None = detection.family
confidence: float | None = detection.confidence
marker_count: int = detection.marker_count

unminified: disrobe.JsUnminify = disrobe.js_unminify(source)
recovered_source: str | None = unminified.source

bundle_source: str = Path("bundle.js").read_text(encoding="utf-8")
unbundled: disrobe.JsUnbundle = disrobe.js_unbundle(bundle_source)
module_count: int = unbundled.module_count
bundler: str | None = unbundled.bundler

unbundled_hinted: disrobe.JsUnbundle = disrobe.js_unbundle(
    bundle_source, bundler="webpack5"
)

JavaScript report classes

Class Notable typed accessors
JsDetection family: str | None, confidence: float | None, marker_count: int, llm
JsUnminify source: str | None, llm
JsUnbundle module_count: int, bundler: str | None, llm

Lua

Detects, decompiles, and deobfuscates 14 Lua obfuscator families, with 16 catalog entries once Luau and GLua dialect detectors are included. IronBrew2 2.7.0 is reversed against real committed output with a Lua execution differential.

from __future__ import annotations

from pathlib import Path

import disrobe

bytecode: bytes = Path("chunk.luac").read_bytes()

det: disrobe.LuaDetection = disrobe.lua_detect(bytecode)
lua_format: str | None = det.format

decompiled: disrobe.LuaDecompilation = disrobe.lua_decompile(bytecode)
decompiled_source: str | None = decompiled.source
fidelity: str | None = decompiled.fidelity
warning_count: int = decompiled.warning_count

deob: disrobe.LuaDeobfuscation = disrobe.lua_deobfuscate(
    Path("obf.lua").read_text(encoding="utf-8"), authorize=True
)
obfuscator: str | None = deob.obfuscator
deobfuscated: str | None = deob.deobfuscated
fully_recovered: bool = deob.fully_recovered
passes_run_count: int = deob.passes_run_count
recovered_string_count: int = deob.recovered_string_count

Lua report classes

Class Notable typed accessors
LuaDetection format: str | None, llm
LuaDecompilation source: str | None, fidelity: str | None, warning_count: int, llm
LuaDeobfuscation obfuscator: str | None, deobfuscated: str | None, fully_recovered: bool, passes_run_count: int, recovered_string_count: int, llm

Go

100% of the independently nm-addressed type-name population on the stripped go1.26.3 linux/amd64 fixture; 115 of 115 names and addresses match, pinned by the gate.
from __future__ import annotations

from pathlib import Path

import disrobe

go_bytes: bytes = Path("binary").read_bytes()

analysis: disrobe.GoAnalysis = disrobe.go_analyze(go_bytes)
image_kind: str | None = analysis.image_kind
pclntab_version: str | None = analysis.pclntab_version
buildversion: str | None = analysis.buildversion
ptr_size: int | None = analysis.ptr_size

symbols: disrobe.GoSymbols = disrobe.go_symbols(go_bytes)
version_label: str | None = symbols.version_label
function_count: int = symbols.function_count
source_file_count: int = symbols.source_file_count
package_count: int = symbols.package_count

pclntab: disrobe.GoPclntab = disrobe.go_pclntab(go_bytes)
version: str | None = pclntab.version
func_count: int | None = pclntab.func_count

garble: disrobe.GarbleReport = disrobe.go_garble(go_bytes)
quality: str | None = garble.quality
detection_score: int | None = garble.detection_score
recovered_string_count: int = garble.recovered_string_count

Go report classes

Class Notable typed accessors
GoAnalysis image_kind: str | None, pclntab_version: str | None, buildversion: str | None, ptr_size: int | None, llm
GoSymbols version_label: str | None, function_count: int, source_file_count: int, package_count: int, llm
GoPclntab version: str | None, ptr_size: int | None, func_count: int | None, image_kind: str | None, llm
GarbleReport quality: str | None, detection_score: int | None, recovered_string_count: int, llm

Ruby

from __future__ import annotations

from pathlib import Path

import disrobe

ruby_bytes: bytes = Path("hello.rb.enc").read_bytes()

det: disrobe.RubyDetection = disrobe.ruby_detect(ruby_bytes, source_path="hello.rb.enc")
flavor: str | None = det.flavor

analysis: disrobe.RubyAnalysis = disrobe.ruby_decompile(
    ruby_bytes, source_path="hello.rb.enc"
)
ruby_flavor: str | None = analysis.flavor
source_path: str | None = analysis.source_path
input_len: int | None = analysis.input_len

Ruby report classes

Class Notable typed accessors
RubyDetection flavor: str | None, llm
RubyAnalysis flavor: str | None, source_path: str | None, input_len: int | None, llm

PHP

from __future__ import annotations

from pathlib import Path

import disrobe

php_bytes: bytes = Path("obfuscated.php").read_bytes()

det: disrobe.PhpDetection = disrobe.php_detect(php_bytes)
kind: str | None = det.kind
confidence: str | None = det.confidence
open_tag_offset: int | None = det.open_tag_offset
has_halt_compiler: bool = det.has_halt_compiler

scan: disrobe.PhpScan = disrobe.php_scan(php_bytes)
hit_count: int = scan.hit_count
family_count: int = scan.family_count

decoded: disrobe.PhpDecode = disrobe.php_decode(php_bytes, max_depth=10)
php_source: str | None = decoded.source
layer_count: int = decoded.layer_count
residual_eval: bool = decoded.residual_eval

PHP report classes

Class Notable typed accessors
PhpDetection kind: str | None, confidence: str | None, open_tag_offset: int | None, has_halt_compiler: bool, llm
PhpScan hit_count: int, family_count: int, llm
PhpDecode source: str | None, layer_count: int, residual_eval: bool, llm

Shell

from __future__ import annotations

from pathlib import Path

import disrobe

batch_script: str = Path("dropper.bat").read_text(encoding="utf-8")
batch_result: disrobe.BatchDeobReport = disrobe.batch_deobfuscate(
    batch_script, args=["/run"]
)
output: str | None = batch_result.output
embedded_payload_count: int = batch_result.embedded_payload_count
decrypted_stage_count: int = batch_result.decrypted_stage_count
commands_emulated: int | None = batch_result.commands_emulated

ps_script: str = Path("obf.ps1").read_text(encoding="utf-8")
ps_det: disrobe.PowershellDetection = disrobe.powershell_detect(ps_script)
obfuscator: str | None = ps_det.obfuscator
ps_confidence: float | None = ps_det.confidence
marker_count: int = ps_det.marker_count

ps_deob: disrobe.PowershellDeobfuscation = disrobe.powershell_deobfuscate(ps_script)
ps_output: str | None = ps_deob.output
level: str | None = ps_deob.level
transformation_count: int = ps_deob.transformation_count

Shell report classes

Class Notable typed accessors
BatchDeobReport output: str | None, embedded_payload_count: int, decrypted_stage_count: int, commands_emulated: int | None, llm
PowershellDetection obfuscator: str | None, confidence: float | None, marker_count: int, llm
PowershellDeobfuscation output: str | None, level: str | None, transformation_count: int, llm

Containers

105 container families are detected. Of these, 102 carry a generic in-tree extractor and LUKS1 carries a bounded raw-volume-key extraction route. 30 of them write member bytes

from a committed input that match an independent extractor. See container docs for the full family list.

from __future__ import annotations

from pathlib import Path

import disrobe

container_bytes: bytes = Path("archive.zip").read_bytes()

det: disrobe.ContainerDetection = disrobe.container_detect(container_bytes)
detected: bool = det.detected
kind: str | None = det.kind
is_zip_family: bool = det.is_zip_family

members: disrobe.ContainerMembers = disrobe.container_members(container_bytes)
fmt: str | None = members.format
size: int | None = members.size
listing: disrobe.ContainerListing | None = members.listing
entry_count: int = members.entry_count

Container report classes

Class Notable typed accessors
ContainerDetection detected: bool, kind: str | None, is_zip_family: bool, llm
ContainerMembers format: str | None, size: int | None, listing: ContainerListing | None, entry_count: int, llm

Pickle

Nothing is ever unpickled; the VM is symbolic.

from __future__ import annotations

from pathlib import Path

import disrobe

pkl: bytes = Path("model.pkl").read_bytes()

listing: str = disrobe.pickle_disasm(pkl)

decompilation: disrobe.PickleDecompilation = disrobe.pickle_decompile(pkl)
pkl_source: str | None = decompilation.source

safety: disrobe.PickleSafety = disrobe.pickle_safety(pkl)
severity: str | None = safety.severity
finding_count: int = safety.finding_count
import_count: int = safety.import_count
reduce_count: int | None = safety.reduce_count

trace: disrobe.PickleTrace = disrobe.pickle_trace(pkl)
protocol: int | None = trace.protocol
memo_count: int | None = trace.memo_count
max_stack_depth: int | None = trace.max_stack_depth
global_ref_count: int = trace.global_ref_count
trace_reduce_count: int | None = trace.reduce_count

polyglot: disrobe.PicklePolyglot = disrobe.pickle_polyglot(pkl)
is_pickle: bool = polyglot.is_pickle
is_polyglot: bool = polyglot.is_polyglot
kind_count: int = polyglot.kind_count

ml_report: disrobe.PickleMlReport = disrobe.pickle_ml_detect(
    Path("model.pt").read_bytes()
)
fmt: str | None = ml_report.format
framing: str | None = ml_report.framing
embedded_count: int = ml_report.embedded_count

Pickle report classes

Class Notable typed accessors
PickleDecompilation source: str | None, llm
PickleSafety severity: str | None, finding_count: int, import_count: int, reduce_count: int | None, llm
PickleTrace protocol: int | None, memo_count: int | None, max_stack_depth: int | None, global_ref_count: int, reduce_count: int | None, llm
PicklePolyglot is_pickle: bool, is_polyglot: bool, kind_count: int, llm
PickleMlReport format: str | None, framing: str | None, embedded_count: int, llm

Editable IR objects

CodeObject, Instruction, and Symbol let you load a Disasm- or Raw-rung .dr envelope, modify it in Python, and write a fresh integrity-hashed .dr.

Instruction

from __future__ import annotations

import disrobe

instr: disrobe.Instruction = disrobe.Instruction(
    offset=0,
    mnemonic="mov",
    operands=["rax", "rbx"],
    bytes=b"\x48\x89\xd8",
)
instr.branch_target = None
flow: disrobe.InstructionFlow = instr.flow
text: str = instr.text()
Member Type Notes
offset int Mutable
mnemonic str Mutable
operands list[str] Mutable
bytes bytes Mutable
branch_target int | None Mutable
flow @property InstructionFlow Read-only
text() -> str Rendered disassembly line

Symbol

from __future__ import annotations

import disrobe

sym: disrobe.Symbol = disrobe.Symbol(address=0x1000, name="entry", kind="function")
sym.name = "main"
sym.kind = "export"
Member Type Notes
address int Mutable
name str Mutable
kind SymbolKind Mutable

CodeObject

from __future__ import annotations

from pathlib import Path

import disrobe

co: disrobe.CodeObject = disrobe.CodeObject.from_dr(Path("module.dr").read_bytes())

instruction_count: int = co.instruction_count
symbol_count: int = co.symbol_count
source_hash: str = co.source_hash
produced_by: str = co.produced_by

instrs: list[disrobe.Instruction] = co.instructions
syms: list[disrobe.Symbol] = co.symbols
metadata: dict[str, str] = co.metadata
capabilities: list[str] = co.capabilities

new_sym: disrobe.Symbol = disrobe.Symbol(
    address=0x2000, name="renamed_fn", kind="function"
)
co.add_symbol(new_sym)
co.set_metadata("analysis", "patched")
co.add_capability("NETWORK_CONNECT", 1)

fresh_dr: bytes = co.to_dr()
Path("module_patched.dr").write_bytes(fresh_dr)

CodeObject.from_dr parses a Disasm- or Raw-rung .dr envelope. to_dr produces a fresh envelope with a recomputed integrity hash. The set_instructions and set_symbols methods replace the full list; add_instruction / add_symbol append. set_metadata(key, value) sets a single string key; clear_metadata resets all. set_llm(sidecar) attaches or removes the metadata sidecar dict.

Member Signature Notes
from_dr staticmethod(dr_bytes: bytes) -> CodeObject Parse existing envelope
instructions @property -> list[Instruction]
set_instructions (instructions: list[Instruction]) -> None Replace
add_instruction (instruction: Instruction) -> None Append
symbols @property -> list[Symbol]
set_symbols (symbols: list[Symbol]) -> None Replace
add_symbol (symbol: Symbol) -> None Append
instruction_count @property -> int
symbol_count @property -> int
source_hash str Mutable attribute
produced_by str Mutable attribute
metadata @property -> dict[str, str]
set_metadata (key: str, value: str) -> None
clear_metadata () -> None
capabilities @property -> list[str]
add_capability (name: str, major: int) -> None
llm @property -> dict[str, Any] | None
set_llm (sidecar: dict[str, Any] | None) -> None
to_dr () -> bytes Produce fresh integrity-hashed envelope

Scope

  • auto returns the plan document without writing stage trees. extract(data, out_dir) writes container members, and flutter_engine_symbols can use an explicit cache directory.
  • External backend tools (jvm_backends, dotnet_backends, native_probe_backends) are probed for availability but never executed.
  • AS3, BEAM (beyond disasm/parse), and the freezer family beyond PyInstaller/Nuitka have no dedicated bindings in this release.
  • No SARIF/NDJSON emitters and no serve daemon; drive the CLI or daemon directly.
⚠️ **GitHub.com Fallback** ⚠️