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

MemberSignatureNotes
raw@property -> dict[str, Any]Full underlying record; no detail dropped
to_json() -> strCompact JSON string
from_json_strclassmethod(text: str) -> SelfRebuild from a to_json string
from_objclassmethod(obj: dict[str, Any]) -> SelfWrap an already-decoded dict

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

_LlmReport

Subclasses _Report. Adds one property:

MemberSignatureNotes
llm@property -> LlmBundle | NonePopulated 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.

AliasValues
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

ClassBaseRaised when
DisrobeErrorExceptionAn underlying recovery or analysis operation fails
UnsupportedLanguageDisrobeErrordisasm/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

CategoryFunctionReturns
Auto chainauto(input, *, max_depth=8, path_hint=None)ChainReport
Generic dispatchdecompile(language, source)CanonicalSource
disasm(language, source)str
parse(language, source)typed report or dict[str, Any]
compile(language, source, *, version=None)bytes
Custom passregister_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
Analysisstrings_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
Nativenative_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 IRquery_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
Envelopeenvelope_create(payload, *, source_label='inline', produced_by=None, detected_format=None)bytes
envelope_verify(envelope_bytes)EnvelopeReport
LLM rendersagents_md(result)str
skill_md(result)str
provenance(result)Provenance
Python decompilepy_decompile(pyc_bytes, *, roundtrip=False, pack=None)PyDecompileReport
py_disasm(pyc_bytes, *, pack=None)PyDisasmReport
Python deobfuscatepy_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
PyArmorpyarmor_detect(source, *, pack=None)PyarmorDetection
pyarmor_unpack(payload, *, runtime=None, pack=None)PyarmorUnpack
pyarmor_classify(source, payload)PyarmorClassification
PyInstallerpyinstaller_extract(image_bytes)PyInstallerArchive
pyinstaller_entry_bytes(image_bytes, entry_name)bytes
Nuitkanuitka_detect(image_bytes)NuitkaDetection
nuitka_extract(image_bytes)NuitkaExtraction
Hermeshermes_disasm(bundle_bytes)HermesDisassembly
hermes_lift(bundle_bytes)HermesLift
hermes_info(bundle_bytes)HermesInfo
Flutterflutter_engine_symbols(input, engine_symbol_map=None, *, source=None, cache_dir=None, no_cache=False)FlutterEngineSymbols
Mach-Omacho_dump(macho_bytes)MachoReport
swift_analyze(macho_bytes)SwiftReport
JVM / Androidjvm_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
.NETdotnet_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
WebAssemblywasm_analyze(wasm_bytes)WasmAnalysis
wasm_detect(wasm_bytes)WasmDetection
wasm_lift(wasm_bytes, *, target)WasmLift
JavaScriptjs_detect(js_source)JsDetection
js_unminify(js_source)JsUnminify
js_unbundle(js_source, *, bundler=None)JsUnbundle
Lualua_detect(bytecode)LuaDetection
lua_decompile(bytecode)LuaDecompilation
lua_deobfuscate(source, *, authorize=False)LuaDeobfuscation
Gogo_analyze(binary_bytes)GoAnalysis
go_symbols(binary_bytes)GoSymbols
go_pclntab(binary_bytes)GoPclntab
go_garble(binary_bytes)GarbleReport
Rubyruby_detect(ruby_bytes, *, source_path=None)RubyDetection
ruby_decompile(ruby_bytes, *, source_path=None)RubyAnalysis
PHPphp_detect(php_bytes)PhpDetection
php_scan(php_bytes)PhpScan
php_decode(php_bytes, *, max_depth=None)PhpDecode
Shellbatch_deobfuscate(script, *, args=None)BatchDeobReport
powershell_detect(script)PowershellDetection
powershell_deobfuscate(script)PowershellDeobfuscation
Containersbyte_coverage(image_bytes)ByteCoverage
container_detect(container_bytes)ContainerDetection
container_members(container_bytes)ContainerMembers
Picklepickle_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.

AccessorValue
spec: str | NoneThe raw chain specification, such as "auto:8"
node_count: intLength of nodes, including the root input node and extracted children
pass_count: intNumber of nodes whose pass is set
passes: list[str]Those pass ids in node order, such as ["native.packer-unpack"]
verdict: ChainVerdict | NoneThe chain-level verdict
verdict_grade: ChainVerdictGrade | NoneThe grade the core assigns to that verdict; None for an unrecognised verdict
final_format: str | NoneThe 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

PropertyType
sourcestr | None
languagestr | None
produced_bystr | None
confidencefloat | 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.

ProtocolSignature
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

ClassNotable typed accessors
StringsReportstring_count: int
IocReportindicator_count: int
BehaviorReportcategory_count: int
IdentifyReportformat: str | None, finding_count: int
SecretScanReportfinding_count: int
Capabilitiesmatch_count: int (entries in capabilities), matched_rules: int
ExtractionResultkind: str | None, entry_count: int, integrity_violation_count: int
OverlayReportmax_depth: int | None, nodes_visited: int | None, chunks_total: int | None, bytes_carved: int | None
YaraReportrule_count: int

Native binary

Functions

FunctionReturnsNotes
native_symbols(data)SymbolsReportSymbols, sections, imports, debug info
native_disasm(data)DisasmPayloadFull disassembly: functions, stream, symbols
native_callgraph(data)CallGraphWhole-program call graph
native_imports_dot(data)strGraphViz DOT of the import graph
native_entropy(data)EntropyReportSliding-window Shannon entropy map
native_sbom(data)SbomReportCycloneDX 1.5 SBOM from cargo-auditable section
native_fingerprint(data, *, flirt=None)FingerprintReportCrypto-constant + FLIRT + string-xref sidecar
native_signatures(data, *, flirt=None)SignatureReportCrypto-primitive signatures and FLIRT matches
native_sigmaker(data, at)SigmakerReportWildcarded byte signature for a VA
native_diff(a, b)DiffReportFunction-level diff of two binaries
native_patch(data, *, at, ...)tuple[bytes, PatchReport]Rewrite bytes and revalidate
native_format(binary_bytes)NativeFormatFormat: kind, bitness, subsystem
native_detect(binary_bytes)DetectionListPacker/protector detection hits
native_probe_backends()BackendListProbe for installed external tools
native_deobfuscate(code, *, bits=64, base=0, entry=0)NativeDeobfuscationx86 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

ClassNotable typed accessors
SymbolsReportsymbol_count: int, section_count: int, import_count: int
DisasmPayloadfunction_count: int, instruction_count: int
CallGraphnode_count: int, edge_count: int
EntropyReportwindow_count: int, mean: float | None, min: float | None, max: float | None
SbomReportcomponent_count: int, bom_format: str | None, spec_version: str | None
FingerprintReportcrypto_hit_count: int
SignatureReportsignature_count: int
SigmakerReportida_pattern: str | None, byte_count: int
DiffReportadded: int, removed: int, changed: int
PatchReportat: int | None (first applied edit), bytes_written: int | None, edit_count: int, format: str | None, image_base: int | None
NativeFormatkind: str | None, bits: int | None, subsystem: str | None
DetectionListcount: int
BackendListcount: int, available_count: int
NativeDeobfuscationbits: 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.

FunctionReturnsNotes
query_functions(dr_bytes)FunctionListAll recovered functions
query_calls_to(dr_bytes, target)QueryReportCall sites targeting a symbol name
query_xrefs_to(dr_bytes, symbol)QueryReportData/code cross-references to a symbol
query_string_decoders(dr_bytes)QueryReportFunctions with string-decode patterns
query_complexity_over(dr_bytes, threshold)QueryReportFunctions with cyclomatic complexity above threshold
query_capability_sites(dr_bytes, capability)QueryReportSites exercising a named capability
query_call_graph(dr_bytes)CallGraphWhole-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

ClassNotable typed accessors
FunctionListkind: str | None, count: int
QueryReportkind: str | None, match_count: int
CallGraphnode_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

PropertyType
verifiedbool
rungstr | None
versionint | None
hot_bytesint | None
cold_bytesint | None
root_hashstr | 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

PropertyType
schemastr | None
schema_versionstr | None
generated_atstr | 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

95.18% (17396 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

ClassNotable typed accessors
PyDecompileReportsource, marshal_version, decompile_version, recovered_directly, fallback_reason, roundtrip_status, roundtrip_detail, interpreter_path, interpreter_version, llm
PyDisasmReportmarshal_version: str | None, instruction_count: int, text: str | None, llm
PyDeobReportpeeled_source: str | None, cleanup_source: str | None, layer_count: int, llm
PyDeobDetectionmatch_count: int (matched markers), confidence: float | None, llm
ObfuscatorPassid: 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

ClassNotable typed accessors
PyarmorDetectionversion, protection, confidence, serial, python_major, python_minor, payload_offset, payload_size, llm
PyarmorUnpackstatus, pyarmor_version, protection_kind, plaintext_len, plaintext_blake3_hex, bcc_blob_count, inner_cipher_recovered_co, llm
PyarmorClassificationscript_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

ClassNotable typed accessors
PyInstallerArchiveentry_count: int, encrypted: bool, encryption_key_present: bool, python_major: int | None, python_minor: int | None, llm
NuitkaDetectionflavor: str | None, version: str | None, wheel_marker: str | None, onefile_payload_offset: int | None, onefile_payload_compressed: bool, llm
NuitkaExtractionvariant: 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

ClassNotable typed accessors
HermesDisassemblyfunction_count: int, identifier_count: int, string_count: int, llm
HermesLiftfunction_surface_count: int, string_count: int, identifier_count: int, llm
HermesInfoversion: 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

ClassNotable typed accessors
MachoReportkind: str | None, fat_entry_count: int, slice_count: int, llm
SwiftReportcontainer: 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

ClassNotable typed accessors
JvmClassmajor_version: int | None, minor_version: int | None, method_count: int, field_count: int, constant_pool_count: int, llm
DexFileReportstring_count: int, type_count: int, class_count: int, method_count: int, llm
JvmDecompiledClasssource: str | None, method_count: int, field_count: int, fully_lifted_methods: int, fallback_methods: int
JvmDecompiledDexsource: str | None, source_count: int, class_count: int, method_count: int, fully_lifted_methods: int, fallback_methods: int
DetectionListcount: int
JvmBackendsjvm_count: int, android_count: int, llm
ApkResourcespackage: 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

ClassNotable typed accessors
DotnetPebitness: str | None, machine: int | None, section_count: int, entry_point_rva: int | None, llm
DotnetMetadataversion: str | None, major_runtime_version: int | None, stream_count: int, llm
DotnetDetectionprimary: str | None, match_count: int, llm
DotnetAnalysispe_bitness: str | None, clr_runtime_version: str | None, native_aot: bool, primary_protector: str | None, opcode_spec_coverage_pct: int | None, llm
DotnetDecompilationmodule_name: str | None, methods_decompiled: int | None, methods_bodyless: int | None, methods_failed: int | None, llm
DotnetDecoderspure_decoders_found: int | None, constants_recovered: int, llm
DotnetNativeAotis_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

ClassNotable typed accessors
WasmAnalysisimport_count: int, export_count: int, func_count: int | None, code_size_bytes: int | None, has_dwarf: bool, llm
WasmDetectionobfuscator: 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

ClassNotable typed accessors
JsDetectionfamily: str | None, confidence: float | None, marker_count: int, llm
JsUnminifysource: str | None, llm
JsUnbundlemodule_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

ClassNotable typed accessors
LuaDetectionformat: str | None, llm
LuaDecompilationsource: str | None, fidelity: str | None, warning_count: int, llm
LuaDeobfuscationobfuscator: str | None, deobfuscated: str | None, fully_recovered: bool, passes_run_count: int, recovered_string_count: int, llm

Go

85%+ type-name recovery on stripped go1.26 fixtures; 838 of 838 measured, 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

ClassNotable typed accessors
GoAnalysisimage_kind: str | None, pclntab_version: str | None, buildversion: str | None, ptr_size: int | None, llm
GoSymbolsversion_label: str | None, function_count: int, source_file_count: int, package_count: int, llm
GoPclntabversion: str | None, ptr_size: int | None, func_count: int | None, image_kind: str | None, llm
GarbleReportquality: 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

ClassNotable typed accessors
RubyDetectionflavor: str | None, llm
RubyAnalysisflavor: 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

ClassNotable typed accessors
PhpDetectionkind: str | None, confidence: str | None, open_tag_offset: int | None, has_halt_compiler: bool, llm
PhpScanhit_count: int, family_count: int, llm
PhpDecodesource: 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

ClassNotable typed accessors
BatchDeobReportoutput: str | None, embedded_payload_count: int, decrypted_stage_count: int, commands_emulated: int | None, llm
PowershellDetectionobfuscator: str | None, confidence: float | None, marker_count: int, llm
PowershellDeobfuscationoutput: str | None, level: str | None, transformation_count: int, llm

Containers

103 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

ClassNotable typed accessors
ContainerDetectiondetected: bool, kind: str | None, is_zip_family: bool, llm
ContainerMembersformat: 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

ClassNotable typed accessors
PickleDecompilationsource: str | None, llm
PickleSafetyseverity: str | None, finding_count: int, import_count: int, reduce_count: int | None, llm
PickleTraceprotocol: int | None, memo_count: int | None, max_stack_depth: int | None, global_ref_count: int, reduce_count: int | None, llm
PicklePolyglotis_pickle: bool, is_polyglot: bool, kind_count: int, llm
PickleMlReportformat: 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()
MemberTypeNotes
offsetintMutable
mnemonicstrMutable
operandslist[str]Mutable
bytesbytesMutable
branch_targetint | NoneMutable
flow@property InstructionFlowRead-only
text()-> strRendered 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"
MemberTypeNotes
addressintMutable
namestrMutable
kindSymbolKindMutable

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.

MemberSignatureNotes
from_drstaticmethod(dr_bytes: bytes) -> CodeObjectParse existing envelope
instructions@property -> list[Instruction]
set_instructions(instructions: list[Instruction]) -> NoneReplace
add_instruction(instruction: Instruction) -> NoneAppend
symbols@property -> list[Symbol]
set_symbols(symbols: list[Symbol]) -> NoneReplace
add_symbol(symbol: Symbol) -> NoneAppend
instruction_count@property -> int
symbol_count@property -> int
source_hashstrMutable attribute
produced_bystrMutable 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() -> bytesProduce 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.