diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..38a8c8d --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,67 @@ +name: Publish to PyPI + +# Runs when a version tag is pushed, e.g. `git tag v0.2.1 && git push origin v0.2.1`. +# The tag must match __version__ in src/tocode/__init__.py. +on: + push: + tags: + - "v*" + +permissions: + contents: read + +jobs: + build: + name: Test and build + runs-on: ubuntu-24.04 + steps: + # actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + + - name: Install uv + # astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@d0d8abe699bfb85fec6de9f7adb5ae17292296ff + + - name: Check the tag matches the package version + run: | + version="$(sed -n 's/^__version__ = "\(.*\)"$/\1/p' src/tocode/__init__.py)" + if [[ "${GITHUB_REF_NAME}" != "v${version}" ]]; then + echo "::error::tag ${GITHUB_REF_NAME} does not match __version__ ${version} (expected tag v${version})" + exit 1 + fi + echo "Publishing tocode-cli ${version}" + + - name: Run tests + run: uv run --locked --extra dev pytest -q + + - name: Build sdist and wheel + run: uv build + + - name: Check the distributions + run: uvx twine==6.2.0 check --strict dist/* + + # actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: dist + path: dist/ + if-no-files-found: error + + publish: + name: Publish + needs: build + runs-on: ubuntu-24.04 + environment: + name: pypi + url: https://pypi.org/project/tocode-cli/ + permissions: + id-token: write # PyPI trusted publishing (no API token stored) + steps: + # actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: dist + path: dist/ + + # pypa/gh-action-pypi-publish@release/v1 + - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 diff --git a/README.md b/README.md index c024798..463b594 100644 --- a/README.md +++ b/README.md @@ -112,21 +112,16 @@ Other disassemblers may be added in the future. ### .NET assemblies -`tocode App.dll` (also `.exe`, an apphost launcher next to its `.dll`, single-file bundles, and `.nupkg` packages) uses a built-in .NET backend: [dnlib](https://github.com/0xd4d/dnlib) for metadata and [ICSharpCode.Decompiler](https://github.com/icsharpcode/ILSpy) (ILSpy's engine) for C#, loaded through [pythonnet](https://pypi.org/project/pythonnet/). No dnSpy/ILSpy install is needed, only a **.NET 9+ runtime** (e.g. `sudo apt install dotnet-runtime-10.0`, or the SDK; `DOTNET_ROOT` is honoured). +ToCode decompiles .NET assemblies (`.dll`, `.exe`), single-file bundles, apphost launchers, and NuGet packages to C#, and writes the IL of each type next to it. You need a .NET 9 or newer runtime. -ToCode does not ship these two libraries (both MIT). The first .NET export asks once whether to download them from nuget.org and remembers the answer. Non-interactive runs (agents, CI) are never prompted: run `tocode --setup-dotnet` once instead, which downloads without asking, or re-verifies and repairs an existing install. Each download is checked against SHA-256 hashes pinned in `src/tocode/backends/dotnet_libs.py`, for both the `.nupkg` and the extracted DLL, and the files are re-hashed after being written. They are stored per user in `~/.local/share/tocode/dotnet` (macOS: `~/Library/Application Support/tocode/dotnet`, Windows: `%LOCALAPPDATA%\tocode\dotnet`, override: `TOCODE_DOTNET_LIB_DIR`). Delete that folder to undo the install or reset the remembered answer. +Decompilation uses [dnlib](https://github.com/0xd4d/dnlib) and [ICSharpCode.Decompiler](https://github.com/icsharpcode/ILSpy), which ToCode doesn't include. The first .NET export asks once whether to download them from nuget.org and checks them against pinned SHA-256 hashes. Scripts and agents can't answer that prompt, so run `tocode --setup-dotnet` beforehand. The files are stored per user (`~/.local/share/tocode/dotnet` on Linux, or `TOCODE_DOTNET_LIB_DIR`); delete that folder to reset. -- `src/raw///.../.cs`: C# per top-level type, folders follow assemblies and namespaces like a dnSpy/ILSpy project export; `.il` next to it keeps the bytecode (IL with RVA, raw bytes, and metadata tokens per instruction). -- Per-method C# and IL line ranges in `functions.json`/`function-index.json` (address `!0x`), with lambdas and async/iterator state machines linked to the method that owns them. -- `assemblies.json`, `types.json`, `strings.json` (user strings with `ldstr` xrefs), `imports.json` (cross-assembly members and P/Invoke), `exports.json`, `reachable.json`, `namespace-graph.json`, `resources.json`, `container.json`, `triage.json` (P/Invoke, suspicious API families, obfuscation hints, strings of interest). -- Native code goes through the regular native backends on a background thread, like APK `.so` files: mixed-mode (C++/CLI) assemblies, native libraries inside bundles/packages, and P/Invoke targets shipped next to the input. `--no-native` skips it. -- Single-file bundles and packages carry the .NET runtime/framework; those assemblies and runtime native libraries are listed but only decompiled with `--include-framework`. NuGet packages decompile each assembly once, from its newest target framework. -- `--as-native` exports a managed PE with the native backend instead (e.g. to look at a ReadyToRun or mixed-mode image in IDA). +Native code in the program (mixed-mode assemblies, bundled libraries, P/Invoke libraries next to the input) goes to the native backend unless you pass `--no-native`. Bundles and packages carry the .NET runtime itself, which is skipped unless you add `--include-framework`. `--as-native` treats a .NET file as a plain PE. ```bash -tocode App.dll # assembly (+ P/Invoke libraries next to it) -tocode publish/App # single-file bundle or apphost launcher -tocode Vendor.Lib.1.0.0.nupkg # NuGet package +tocode App.dll +tocode publish/App # single-file bundle or apphost +tocode Vendor.Lib.1.0.0.nupkg ``` ### Android APKs @@ -151,7 +146,16 @@ tocode app.apks --no-native -j 4 # bundle, DEX/Android side only ToCode supports Windows, Linux, and macOS with Python 3.10 or newer. -On Windows PowerShell: +Install it from PyPI: + +```bash +pip install tocode-cli # or: uv tool install tocode-cli +pip install "tocode-cli[angr]" # also install the angr fallback backend +``` + +The command is `tocode`. You still need a backend (IDA, radare2, angr, or Binary Ninja); see [Supported backends](https://github.com/buzzer-re/ToCode#supported-backends). + +To install from a clone of the repository instead, on Windows PowerShell: ```powershell powershell -ExecutionPolicy Bypass -File .\install.ps1 @@ -246,4 +250,4 @@ export_from_binaryview(bv, "out/") ## Development -See [DEVELOPMENT.md](DEVELOPMENT.md) for setup, tests, the local quality gate, and release steps. +See [DEVELOPMENT.md](https://github.com/buzzer-re/ToCode/blob/main/DEVELOPMENT.md) for setup, tests, the local quality gate, and release steps. diff --git a/src/tocode/__init__.py b/src/tocode/__init__.py index e4bed5a..72e9618 100644 --- a/src/tocode/__init__.py +++ b/src/tocode/__init__.py @@ -7,7 +7,7 @@ __all__ = ["__version__", "export_from_binaryview"] -__version__ = "0.1.0" +__version__ = "0.2.1" def export_from_binaryview( diff --git a/src/tocode/backends/dotnet.py b/src/tocode/backends/dotnet.py index 3a3ef1b..323fc08 100644 --- a/src/tocode/backends/dotnet.py +++ b/src/tocode/backends/dotnet.py @@ -540,10 +540,18 @@ def load_runtime() -> dict[str, Any]: raise ToCodeError(f"cannot load the .NET runtime: {exc}") from exc import clr # type: ignore[import-not-found] - for spec in dotnet_libs.LIBRARIES: - clr.AddReference(str(library_dir / spec.file_name)) import System # type: ignore[import-not-found] + # Reference by name from a sys.path folder: pythonnet 3.0 (Python 3.10) + # rejects full paths in AddReference; 3.1+ accepts both. + if str(library_dir) not in sys.path: + sys.path.append(str(library_dir)) + for spec in dotnet_libs.LIBRARIES: + try: + clr.AddReference(spec.file_name.removesuffix(".dll")) + except Exception: + System.Reflection.Assembly.LoadFrom(str(library_dir / spec.file_name)) + actual = str(System.Environment.Version) if _version_tuple(actual)[:1] < (MIN_RUNTIME_MAJOR,): raise ToCodeError(