Malspark

Usage

Running an analysis

Samples, jobs, the per-job overrides, the sandbox choice and following a run.

An analysis is a job: one team run over one uploaded sample. This page walks the lifecycle from the operator's side — upload, submit, follow, read — and the choices a job can make for itself. The console does each step with a button; the API calls are shown so the same thing can be scripted. The worker's side of the same lifecycle is in Architecture.

Upload the sample.

curl -X POST http://localhost:8000/api/v1/samples/upload \
  -H "Authorization: Bearer $TOKEN" -F '[email protected]'

The answer carries the sample's id. No sample is refused for its format: routing reads the file type from the bytes and picks the path, as described in format routing.

Optionally attach a sandbox report produced elsewhere.

curl -X POST http://localhost:8000/api/v1/samples/<sample id>/sandbox-reports \
  -H "Authorization: Bearer $TOKEN" -F '[email protected]'

The format is sniffed from the content against the declared report formats; CAPEv2, Cuckoo and Triage reports are accepted by default (sandbox.upload.allowed_formats), and a format of your own is declared as data in sandbox.report_formats (see Report formats are declarations), up to 64 MiB (sandbox.upload.max_report_bytes). The upload records whether the report's own target hash matches the sample.

Submit the job.

curl -X POST http://localhost:8000/api/v1/jobs \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"sample_id":"<sample id>","config":{"profile":"default"}}'

Follow the run on the console's analysis page, or over the WebSocket at /ws/analysis/{job_id}.

Read the report once the job is completed; see Reports and exports.

What a job may choose for itself

config is optional. The keys below are checked when the job is submitted, with the same choices and bounds the settings have, so a bad value is a 422 at submit rather than a failed job minutes later. Unknown keys pass through.

KeyEffect
profileThe team this job runs. Checked against the teams the deployment holds; an unknown team, or one that lists a disabled analyst, is refused. See Teams and profiles.
llm_provideropenai, anthropic, ollama or gemini for this job, in place of llm.provider.
static_providerghidra, r2, capa_yara, generic_mcp or none for this job, in place of static.provider.
sandbox_providermock, cape2, upload, triage or rest for this job, in place of sandbox.provider.
sandbox_report_idAn uploaded report to attach. It forces sandbox.provider to upload for this job, and only a report attached to this job's own sample is read.
max_iterationsThe debate's round limit for this job (negotiation.max_iterations).
mock_modeAsks for the pipeline's mock path. It runs only when an administrator has also turned on api.mock_mode_allowed; a mock run cannot consume an uploaded report.
ignore_earlier_runstrue starts the job without an earlier run's memory of its sample: no agent is shown the "AN EARLIER RUN ON THIS SAMPLE" block and the judge's bundle records no earlier run. Nothing stored is read for it. Once it completes, its own case is stored as any job's is, which replaces the case its owner (the account that submitted it) held for the sample. The run summary says so (earlier_runs_ignored: true). Off by default; every benchmark run sets it.
isolated_memorytrue gives the job long-term memory of its own: it is filed under the job alone rather than under its owner, so no case, similar sample or function-hash family another job stored is read for it (the judge's similar cases, the attribution's similar samples, the function-hash families and the earlier run alike), for its own sample or any other (a case corpus mined from the memory, scripts/knowledge/build_attck_case_kb.py, is not owner-scoped: the job still reads it; it holds unscoped cases alone). Once it completes, its own case is stored under the job, never replacing the case its owner held for the sample; no later job reads it. The run summary says so (isolated_memory: true). Off by default; every benchmark run sets it.

The checks made before a job is accepted

POST /api/v1/jobs refuses, with a 422 and a sentence naming what is wrong, three kinds of job that could only fail later:

  • A model no probe has reached. With llm.require_probe on (the default), every model the job calls must have answered a connection test: every agent of the team and every agent it can ask, the mediator and summariser when they run, and the judge and (while reporting is on) the reporter on the global judge model unless they have an entry. See a model is probed before a job may name it.
  • A static provider that is not ready. Ghidra does not degrade, so a team that needs it waits for it. See a team that needs Ghidra waits for it.
  • An unknown team, or a team that lists a disabled analyst.

Choosing a sandbox

The sandbox produces the dynamic evidence. The default is mock, which executes nothing.

No detonation. The mock returns a recorded fixture for a sample it has one for, and an empty stand-in marked synthetic for any other. Where no sandbox ran, the report says so in one sentence, the dynamic and network analysts are skipped, and a stand-in's empty sections are never rendered as "0 processes". See no sandbox observation where no sandbox ran.

A CAPEv2 instance over its REST API (sandbox.cape2.*). A file type is mapped to a CAPE analysis package with package_by_format; further form fields go in submit_options.

Hatching Triage's cloud API (sandbox.triage.*). A file type is mapped to a VM profile with profile_by_format; analysis_seconds sets the VM's run time and timeout_seconds, which must be longer, how long the platform waits for the report.

No detonation of Malspark's own: a report produced elsewhere and attached to the sample (step 2 above) is read instead.

Any HTTP sandbox, described rather than coded: the submission, the poll, the report's location and a JSONPath per channel (sandbox.rest.*).

Each provider's settings, and the options each format is submitted with, are on the Sandboxes page.

Following a run

The console's CONVERSATION tab draws the run as the exchange it is: every agent's messages, its tool calls and the verdict, while the analysis header draws the team's stages, each filling in as it starts and finishes. The EVIDENCE tab is the ledger of every tool call the run made. See Console.

Programmatically:

SurfaceWhat it gives
GET /api/v1/jobs/{job_id}The job's status — pending, running, completed, failed or cancelled — with its timing and error message.
/ws/analysis/{job_id}The run's events as they happen, including stage_started, stage_skipped and stage_finished once per stage.
GET /api/v1/jobs/{job_id}/eventsThe same events, in sequence order, for a tab that opens mid-run or resumes with since. They are read first from a live buffer (24 hours, at most 1,000 events) and then from the job_events table, kept for core.events.retention_days (30 days by default). Each model call's model_usage event is in the table alone and is merged in by seq either way.
GET /api/v1/jobs/{job_id}/usageThe job's usage so far, from its model_usage events: calls, retries, failed_attempts, tokens (the run summary's block: input, output, cache reads and writes, reasoning, per agent) and spend (spent_usd, the sum of what the spend meter charged each call when it was settled, where each price came from, calls that could not be priced, repriced_calls for older records without a charge, per agent). Answers during a run and after a worker that died.
GET /api/v1/jobs/{job_id}/evidenceThe evidence ledger, one entry per tool call, filterable by stage, agent and tool.
DELETE /api/v1/jobs/{job_id}Cancels a pending or running job; its model calls stop.

The event and evidence formats are in the REST API reference.

Running without the stack

The standalone CLI runs the pipeline without the API, the worker or any of the backing services:

uv run maljan analyze sample_1 --mock --name test.exe

It builds the core Settings model directly, so it reads the process environment (and a .env in the working directory) rather than the settings store. --ignore-earlier-runs is the job option ignore_earlier_runs for a command-line run. The command line has no isolated_memory option: its runs have no account and use the unscoped memory.

Edit on GitHub

On this page