Surface radare2's r2ghidra (pdg) Ghidra decompiler as a deep-tier registry tool tagged pe/elf/decompilation. It is requiresUserArgs (function-scoped, empty on packed/stripped samples), so analyze_file surfaces the exact invocation in tools_skipped rather than auto-running it, and suggest_tools teaches the pdg/afl/pdc recipe in the PE/ELF hints; function-level targeting rides on run_tool. Also tags cfr/jadx with the decompilation marker and clarifies the requiresUserArgs doc comment.
Invocation verified on REMnux radare2 6.1.6.
upload_from_host accepted any absolute host_path with no base-directory
confinement. In docker/ssh mode it reads from the host where the server
runs (the analyst's workstation), outside the container/VM isolation that
bounds the rest of the server, so a prompt-injected client could stage a
host file such as ~/.ssh/id_rsa into REMnux.
Add opt-in confinement: with --sandbox, the source is realpath-resolved
and must reside under --ingest-root (defaults to the samples dir). The
resolved path is used for the read and the copy, so the validated and read
paths match (closes the check-vs-read race) and a symlinked parent cannot
redirect the read. In docker/ssh mode --ingest-root is required when
--sandbox is set; the server fails closed at startup otherwise.
Default behavior (no --sandbox) is unchanged: local mode already grants
arbitrary read via run_tool by design, so confinement stays opt-in.
Documents the connector-mode boundary in the Security Model. Verified end
to end in local, docker, and ssh modes.
The runtime loads data/tools-index.json from the package root, but the files
allowlist omitted data/, so npm published without it (since 0.1.43). The server
degraded gracefully but additional_tools discovery was unavailable. Add the
catalog to files; verified present via npm pack --dry-run.
- Detect extracted symlinks whose resolved target escapes outputDir
(find -type l + findEscapingSymlinks); the prior name-based check could not
see a symlink whose target leaves the dir. Wired into the existing rm-rf+fail
path. Benign internal symlinks are preserved.
- Reject extract_archive passwords starting with '-' (parsed by unzip as an
option) and shell metacharacters, surfaced up front in the handler.
- Add 15 unit tests; validated end-to-end against real samples in remnux-distro.
Real `diec --json` nests the file type inside detects[i].filetype; the parser
read top-level data.filetype, so metadata.filetype was always undefined for
real output. Read it from the first typed detect, falling back to top-level for
the synthetic/legacy shape. Verified live (now resolves, e.g. ELF64).
`diec --json` prepends an informational line (e.g. "[!] Heuristic scan is
disabled. Use '--heuristicscan' to enable") to stdout before the JSON body,
so the parser's bare JSON.parse failed and diec findings were never extracted
via run_tool. Try the clean output first, then fall back to the JSON object
between the first '{' and last '}'. The warning itself starts with '[', so
naive bracket-trimming is unsafe.
Verified against real container output; adds a test using the actual warning prefix.
The catalog (data/tools-index.json) is a discovery index: its `command` is a
display-name slug (Detect-It-Easy->detect-it-easy; real binary diec), not a
runnable binary. additional_tools surfaced that slug as `command` and told the
model to "use run_tool to invoke manually" -- the issue #1 bug class on a
secondary surface.
Drop the misleading slug from the model-facing entry (keep it internal for
dedup) and surface name + description + website with an honest note pointing to
the docs for the real command. Tests updated to assert the new shape and the
dedup invariant via a name->command mapping.
Making the catalog itself runnable is a salt-states update-docs.py change
(~192 tools lack a Command: field) -- tracked separately.
The old end-anchored regex only matched a recursive rm when "/" was the last
token, so the actually-destructive form on modern coreutils --
`rm -rf / --no-preserve-root` -- slipped through, while the harmless bare
`rm -rf /` (a no-op without --no-preserve-root) was blocked.
Replace it with a token/segment-aware isRootWipe() in isCommandSafe that
detects a recursive rm targeting / or /* regardless of flag order, combined or
long flags, trailing options, comments, a leading sudo, or a quoted root
operand. Targeted deletes (rm -rf subdir/, the session output dirs) stay
allowed.
Found during adversarial review of the prior change; session-preservation
guardrail only -- container isolation remains the security boundary.
Issue #1 — "the model runs invocations, not names":
- suggest_tools surfaces a full `invocation` per tool (real command + args +
<file>), via a shared assembleCommand() that can't drift from the executor.
- get_tool_help resolves a registry name to its real binary + returns the
canonical invocation.
- run_tool silently rewrites a bare ".py" name (emldump -> emldump.py); aliases
pass through and fail naturally (no false-positive blocking).
Issue #2 — host /tmp leakage:
- The four /tmp output paths become an inert "%OUTPUT%/" sentinel, resolved to
the per-session output dir by one shared resolveOutputPath() used by the
command builder, analyze_file's mkdir, and run_tool.
- mkdir target == command target; fails one tool, not the whole run, when no
output dir is configured.
Adds 6 test files. Reimplements the approach proposed in #3 by @tlium.
Co-authored-by: tlium <6695612+tlium@users.noreply.github.com>
resolveSamplePath only bypassed the samples-dir prefix for absolute paths in
local mode. In docker/ssh mode an absolute path (e.g. the `extracted_to` that
extract_archive returns) was re-rooted under samplesDir, producing
'/samples//home/.../sample' and a confusing 'file not found' — exactly what an
AI chaining extract_archive -> analyze_file would hit. Absolute paths are now
resolved as-is on the target system in all modes. Sandbox enforcement is
unchanged: validateFilePath() still gates args.file in each handler.
Add two read-only MCP tools and matching resources that return a locally
bundled copy of Lenny Zeltser's malware analysis report template (CC BY 4.0)
and writing guidelines digest, so air-gapped/offline analysts can draft a
report after analysis with zero extra config:
- get_report_template / get_report_guidance (with `topic` narrowing)
- remnux://report/template and remnux://report/guidelines resources
- analyze_file suggests drafting a report when results are substantive
Content is synced from the canonical public zeltser.com sources into a
generated TS module (ships via dist/) by 'pnpm run sync:report-guidance'
(deterministic; '--check' drift mode). Online users are pointed to the
richer zeltser-website MCP server for interactive review/scoring.
NODE_AUTH_TOKEN is the repo secret name (not NPM_TOKEN). Restore
npm install -g npm@latest from the original workflow — the runner's
bundled npm may not handle auth correctly with corepack active.
Add --provenance for OIDC attestation, explicit NODE_AUTH_TOKEN mapping,
and COREPACK_ENABLE_STRICT=0 to prevent corepack from interfering with
npm publish when packageManager declares pnpm.
The live integration test called nonexistent tool "upload_sample" with
wrong parameters. Fix to use "upload_from_host" with host_path, matching
the actual tool schema.
Switch to pnpm for project-level security policies: onlyBuiltDependencies
allowlist, 7-day minimumReleaseAge quarantine, and blockExoticSubdeps.
Preserve npm publish with OIDC provenance in CI.
When the MCP server runs inside REMnux with HTTP transport (Scenario 3),
upload_from_host reads from the REMnux filesystem, not the remote client.
The previous description said "host filesystem" which is ambiguous.
- Tool description now distinguishes HTTP vs stdio transport in local mode
- Parameter description clarifies "machine where the MCP server runs"
- Error hint for HTTP transport explains path resolves on REMnux and
suggests scp/sftp or download_from_url as alternatives
- Added transport field to HandlerConfig for transport-aware error messages
Extract resolveSamplePath and checkFileExists utilities shared across
analyze_file, get_file_info, and suggest_tools handlers. Fixes two bugs:
- file="samples/sample.exe" caused doubled path /samples/samples/sample.exe
- Nonexistent files returned success: true with matched_category: "Unknown"
Also fixes suggest_tools passing raw args.file instead of normalizedFile
to matchFileType for consistency with the other handlers.
Add tool definitions for redress (Go binary analysis), uncompyle6
(Python decompiler), pyinstxtractor-ng, and apkid (Android packer
detection). Update pyinstxtractor preprocessor to prefer -ng variant
with fallback.
suggest_tools now queries the tool catalog and returns additional_tools
for the detected file type, deduplicated against the registry via .py
normalization and an alias map (e.g., oletools → oleid/olevba/rtfobj).
Catalog loading is now graceful — a missing or corrupt tools-index.json
logs a warning instead of crashing the server. New category mappings
for Go→ELF and Python. Updated analysis hints for redress, uncompyle6,
apkid.
Container/VM isolation is the security boundary, not in-band command
filtering. These patterns were inconsistent with eval, exec, source,
and pipe-to-interpreter already being allowed — blocking $(whoami)
while allowing eval 'whoami' is security theater. Unblocking enables
legitimate workflows like file $(which python3) and echo ${PATH}.
Null byte injection and catastrophic command guards (rm -rf /, mkfs)
remain in place.