Python bindings

disrobe ships a typed Python library that mirrors the full CLI surface. The importable disrobe module is built from crates/disrobe-python with pyo3 (abi3, Python 3.9+) and wraps the same Rust library the CLI uses. Bytes go in; a concrete typed report object comes out. Output is deterministic: the same input produces the same report bytes.

The library does not read or write the filesystem; the caller owns all I/O. Wheels are not published to PyPI; build from source.

Install

git clone https://github.com/1-3-7/disrobe
cd disrobe/bindings/python
pip install maturin
maturin develop --release

For a redistributable wheel:

maturin build --release
pip install target/wheels/disrobe-*.whl

The pyproject.toml pins maturin>=1.5,<2.0, 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 is shipped so pyright and mypy resolve every attribute from the .pyi stub.

import disrobe

version: str = disrobe.__version__

Report model

Every analysis function returns a concrete subclass of _Report. The base carries the full serialization surface every report shares.

_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(total=False) mirroring the disrobe.metadata.llm.v1 on-disk schema. The type name and schema id are stable API names; the payload is deterministic metadata, not model output. Keys present depend on which pack was requested.

from disrobe import LlmBundle
from typing import Any

bundle: LlmBundle = {
    "schema": "disrobe.metadata.llm.v1",
    "schema_version": "1",
    "generated_at": "2026-06-16T00:00:00Z",
    "tool": {},
    "selection": {},
    "input": {},
    "pipeline": [],
    "categories": {},
}

Literal type aliases

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"
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
DisrobeErrorExceptionAny binding fails
UnsupportedLanguageDisrobeErrordisasm/parse/compile/decompile for a language with no backing implementation; message includes a hint

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_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(wrapper_bytes, *, 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
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
.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
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, strict=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
Containerscontainer_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

import disrobe
from disrobe import ChainReport

with open("sample.bin", "rb") as fh:
    chain: ChainReport = disrobe.auto(fh.read(), max_depth=8)

spec: str | None = chain.spec
pass_count: int = chain.pass_count
terminated: bool = chain.terminated
full_plan: dict[str, object] = chain.raw

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; out-of-range values raise DisrobeError. 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.

ChainReport accessors

PropertyType
specstr | None
pass_countint
terminatedbool

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/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.

import disrobe
from disrobe import CanonicalSource

with open("module.pyc", "rb") as fh:
    recovered: CanonicalSource = disrobe.decompile("python-bytecode", fh.read())

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.

import disrobe
from typing import Any

with open("Hello.class", "rb") as fh:
    parsed: dict[str, Any] = disrobe.parse("jvm-class", fh.read())
method_count: int = len(parsed["methods"])

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.

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.

from typing import Any
import disrobe
from disrobe import Pass, OutputConsumer

def my_pass(data: Any) -> Any:
    return data[::-1]

def my_consumer(result: Any, **context: Any) -> Any:
    print(result, context)

disrobe.register_pass("reverse", my_pass)
disrobe.register_consumer("print", my_consumer)

names: list[str] = disrobe.registered_passes()
consumer_names: list[str] = disrobe.registered_consumers()

output: Any = disrobe.run_pass("reverse", b"hello")
chained: Any = disrobe.run_chain(["reverse", "reverse"], b"hello")
disrobe.emit("print", chained, source="example")

removed: bool = disrobe.unregister("reverse")

Pass and OutputConsumer protocols

Both are @runtime_checkable protocols.

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.

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.

import disrobe
from disrobe import (
    StringsReport, IocReport, BehaviorReport, IdentifyReport,
    SecretScanReport, Capabilities, OverlayReport, YaraReport,
)

with open("suspect.bin", "rb") as fh:
    data: bytes = fh.read()

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

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

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

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

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

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

rule: 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, format: str | None
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
import disrobe
from disrobe import (
    SymbolsReport, DisasmPayload, CallGraph, EntropyReport,
    SbomReport, FingerprintReport, SignatureReport, SigmakerReport,
    DiffReport, PatchReport, NativeFormat, DetectionList,
    BackendList, NativeDeobfuscation,
)

with open("binary.elf", "rb") as fh:
    data: bytes = fh.read()

syms: 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: DisasmPayload = disrobe.native_disasm(data)
instruction_count: int = disasm_payload.instruction_count
source_hash: str | None = disasm_payload.source_hash

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

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

patched_bytes: bytes
patch_report: PatchReport
patched_bytes, patch_report = disrobe.native_patch(data, at=0x1234, nop_start=0x1234, nop_end=0x1240)
revalidated: bool = patch_report.revalidated

deob: NativeDeobfuscation = disrobe.native_deobfuscate(data, 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
DisasmPayloadinstruction_count: int, symbol_count: int, source_hash: str | None
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, bytes_written: int | None, revalidated: bool
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
import disrobe
from disrobe import FunctionList, QueryReport, CallGraph

with open("module.dr", "rb") as fh:
    dr: bytes = fh.read()

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

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

complex_fns: QueryReport = disrobe.query_complexity_over(dr, threshold=20)
graph: 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.

import disrobe
from disrobe import EnvelopeReport

envelope: bytes = disrobe.envelope_create(
    b"payload",
    source_label="inline",
    produced_by="my-tool",
    detected_format="elf64",
)
report: 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. Passing a report whose llm slot is None raises DisrobeError.

import disrobe
from disrobe import PyDecompileReport, Provenance

with open("module.pyc", "rb") as fh:
    report: PyDecompileReport = disrobe.py_decompile(fh.read(), pack="pack-2")

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

prov: 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

Python passes

See also Python decompiler for the full decompiler design.

py_decompile

Decompiles a .pyc (with header) to source. Full CPython 3.14 stdlib coverage is

95.09% per-code-object recompile equivalence (17378 of 18276); the pinned

200-module corpus is 96.53% (6068 of 6286, CI floor 96.51%). Legacy CPython 1.0-3.7: 78.5% proven-correct (CI floor 150 of 191; 166 of 191 with the full interpreter zoo present).

import disrobe
from disrobe import PyDecompileReport, RoundtripStatus

with open("module.pyc", "rb") as fh:
    report: PyDecompileReport = disrobe.py_decompile(fh.read(), 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: 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) shells out to a matching host interpreter; it is the one binding that may run an external python.

py_disasm

import disrobe
from disrobe import PyDisasmReport

with open("module.pyc", "rb") as fh:
    result: PyDisasmReport = disrobe.py_disasm(fh.read())

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

import disrobe
from disrobe import ObfuscatorPass, PyDeobDetection, PyDeobReport

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

deob: 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: PyDeobDetection = disrobe.py_deob_detect(obfuscated)
match_count: int = detection.match_count

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

per_pass: PyDeobDetection = disrobe.py_deob_detect_pass(obfuscated, "base64-exec")

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, llm
ObfuscatorPassid: str | None

PyArmor

pyarmor_detect

Parses a PyArmor wrapper from source text.

import disrobe
from disrobe import PyarmorDetection

detection: PyarmorDetection = disrobe.pyarmor_detect(open("wrapped.py").read())
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

Statically unpacks a PyArmor wrapper image. On the 72 / 72 manifest-named v8/v9 default-trial wrappers, the static path decrypts each body and decodes one complete root CodeObject from its header-anchored marshal stream. This structural check does not compare source, emitted .pyc bytes, semantic or execution behavior, or external-tool output. The bindings expose only the static path; there is no --allow-dynamic equivalent.

import disrobe
from disrobe import PyarmorUnpack, PyarmorUnpackStatus

with open("wrapper.pyc", "rb") as fh:
    unpacked: PyarmorUnpack = disrobe.pyarmor_unpack(fh.read())

status: 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

import disrobe
from disrobe import PyarmorClassification

with open("payload.bin", "rb") as fh:
    payload: bytes = fh.read()

classification: PyarmorClassification = disrobe.pyarmor_classify(open("wrapped.py").read(), 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

import disrobe
from disrobe import PyInstallerArchive

with open("app.exe", "rb") as fh:
    image: bytes = fh.read()

archive: 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

entries: list[dict[str, object]] = archive.raw["entries"]
main_payload: bytes = disrobe.pyinstaller_entry_bytes(image, str(entries[0]["name"]))

nuitka_detect / nuitka_extract

import disrobe
from disrobe import NuitkaDetection, NuitkaExtraction

with open("app.exe", "rb") as fh:
    image: bytes = fh.read()

det: 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: 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.

import disrobe
from disrobe import HermesDisassembly, HermesLift, HermesInfo

with open("index.android.bundle", "rb") as fh:
    bundle: bytes = fh.read()

disasm_result: 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: HermesLift = disrobe.hermes_lift(bundle)
function_surface_count: int = lift.function_surface_count

info: 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

import disrobe
from disrobe import MachoReport, SwiftReport

with open("universal.dylib", "rb") as fh:
    data: bytes = fh.read()

macho: 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: 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.

import disrobe
from disrobe import (
    JvmClass, DexFileReport, JvmDecompiledClass, JvmDecompiledDex,
    DetectionList, JvmBackends, ApkResources,
)

with open("Hello.class", "rb") as fh:
    cls: JvmClass = disrobe.jvm_parse_class(fh.read())
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

with open("classes.dex", "rb") as fh:
    dex: DexFileReport = disrobe.jvm_parse_dex(fh.read())
class_count: int = dex.class_count
dex_method_count: int = dex.method_count

with open("Hello.class", "rb") as fh:
    decompiled: JvmDecompiledClass = disrobe.jvm_decompile_class(fh.read())
source: str | None = decompiled.source
fully_lifted_methods: int = decompiled.fully_lifted_methods
fallback_methods: int = decompiled.fallback_methods

with open("classes.dex", "rb") as fh:
    decompiled_dex: JvmDecompiledDex = disrobe.jvm_decompile_dex(fh.read())
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: DetectionList = disrobe.jvm_detect(open("obf.class", "rb").read())
detection_count: int = detections.count

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

with open("app.apk", "rb") as fh:
    apk: ApkResources = disrobe.apk_resources(fh.read())
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

import disrobe
from disrobe import (
    DotnetPe, DotnetMetadata, DotnetDetection,
    DotnetAnalysis, DotnetDecompilation, DotnetDecoders, DotnetNativeAot, BackendList,
)

with open("Sample.dll", "rb") as fh:
    pe_bytes: bytes = fh.read()

pe: 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: 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: DotnetDetection = disrobe.dotnet_detect(pe_bytes)
primary: str | None = detection.primary
match_count: int = detection.match_count

analysis: DotnetAnalysis = disrobe.dotnet_analyze(pe_bytes)
pe_bitness: str | None = analysis.pe_bitness
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: 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: DotnetDecoders = disrobe.dotnet_recover_decoders(pe_bytes)
pure_decoders_found: int | None = decoders.pure_decoders_found
constants_recovered: int = decoders.constants_recovered

with open("HelloAppAot.exe", "rb") as fh:
    image_bytes: bytes = fh.read()

native_aot: 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: 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.

import disrobe
from disrobe import WasmAnalysis, WasmDetection

with open("module.wasm", "rb") as fh:
    wasm_bytes: bytes = fh.read()

analysis: 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: 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.

import disrobe
from disrobe import JsDetection, JsUnminify, JsUnbundle

source: str = open("main.js").read()

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

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

bundle_source: str = open("bundle.js").read()
unbundled: JsUnbundle = disrobe.js_unbundle(bundle_source)
module_count: int = unbundled.module_count
bundler: str | None = unbundled.bundler

unbundled_hinted: 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.

import disrobe
from disrobe import LuaDetection, LuaDecompilation, LuaDeobfuscation

with open("chunk.luac", "rb") as fh:
    bytecode: bytes = fh.read()

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

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

deob: LuaDeobfuscation = disrobe.lua_deobfuscate(open("obf.lua").read(), 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.
import disrobe
from disrobe import GoAnalysis, GoSymbols, GoPclntab, GarbleReport

with open("binary", "rb") as fh:
    go_bytes: bytes = fh.read()

analysis: 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: 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: GoPclntab = disrobe.go_pclntab(go_bytes)
version: str | None = pclntab.version
func_count: int | None = pclntab.func_count

garble: GarbleReport = disrobe.go_garble(go_bytes)
quality: str | None = garble.quality
detection_score: int | None = garble.detection_score
seed_recoverable: bool = garble.seed_recoverable
seed_hash: str | None = garble.seed_hash
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, seed_recoverable: bool, seed_hash: str | None, recovered_string_count: int, llm

Ruby

import disrobe
from disrobe import RubyDetection, RubyAnalysis

with open("hello.rb.enc", "rb") as fh:
    ruby_bytes: bytes = fh.read()

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

analysis: 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

import disrobe
from disrobe import PhpDetection, PhpScan, PhpDecode

with open("obfuscated.php", "rb") as fh:
    php_bytes: bytes = fh.read()

det: 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: PhpScan = disrobe.php_scan(php_bytes)
hit_count: int = scan.hit_count
family_count: int = scan.family_count

decoded: 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

import disrobe
from disrobe import BatchDeobReport, PowershellDetection, PowershellDeobfuscation

batch_script: str = open("dropper.bat").read()
batch_result: 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 = open("obf.ps1").read()
ps_det: 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: 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

102 container families are detected. Of these, 101 carry a generic in-tree extractor and LUKS1 carries a bounded raw-volume-key extraction route. 41 of them are driven to member

bytes by an input this repository commits. See container docs for the full family list.

import disrobe
from disrobe import ContainerDetection, ContainerMembers, ContainerListing

with open("archive.zip", "rb") as fh:
    container_bytes: bytes = fh.read()

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

members: ContainerMembers = disrobe.container_members(container_bytes)
fmt: str | None = members.format
size: int | None = members.size
listing: 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.

import disrobe
from disrobe import (
    PickleDecompilation, PickleSafety, PickleTrace,
    PicklePolyglot, PickleMlReport,
)

with open("model.pkl", "rb") as fh:
    pkl: bytes = fh.read()

listing: str = disrobe.pickle_disasm(pkl)

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

safety: 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: 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: PicklePolyglot = disrobe.pickle_polyglot(pkl)
is_pickle: bool = polyglot.is_pickle
is_polyglot: bool = polyglot.is_polyglot
kind_count: int = polyglot.kind_count

with open("model.pt", "rb") as fh:
    ml_report: PickleMlReport = disrobe.pickle_ml_detect(fh.read())
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 disrobe import Instruction, InstructionFlow

instr: Instruction = Instruction(
    offset=0,
    mnemonic="mov",
    operands=["rax", "rbx"],
    bytes=b"\x48\x89\xd8",
)
instr.branch_target = None
flow: 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 disrobe import Symbol, SymbolKind

sym: Symbol = Symbol(address=0x1000, name="entry", kind="function")
sym.name = "main"
sym.kind = "export"
MemberTypeNotes
addressintMutable
namestrMutable
kindSymbolKindMutable

CodeObject

import disrobe
from disrobe import CodeObject, Instruction, Symbol

with open("module.dr", "rb") as fh:
    co: CodeObject = CodeObject.from_dr(fh.read())

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[Instruction] = co.instructions
syms: list[Symbol] = co.symbols
metadata: dict[str, str] = co.metadata
capabilities: list[str] = co.capabilities
llm_bundle: dict[str, object] | None = co.llm

new_sym: Symbol = 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()
with open("module_patched.dr", "wb") as fh:
    fh.write(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

  • No file or directory handling: no --out trees, no --capture-stages, no container extraction to disk. auto returns the plan document only.
  • External backend tools (jvm_backends, dotnet_backends, native_probe_backends) are probed for availability but never executed.
  • AS3, Flutter, 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.