Skip to content

CI

A pipeline can submit a sample to a running Maljan deployment, wait for the analysis and act on what it publishes, through the REST API and an API key. This page is a recipe built from those endpoints; Maljan ships no CI plug-in of its own. The checks this repository runs on itself are described in Development.

Keep samples off shared runners

A CI job that submits malware carries the sample through the runner. Use a runner you control, on a network you are authorised to use for this, and keep the deployment off the public internet.

An API key for the pipeline

A key acts as the account that minted it, so mint it signed in as the account the pipeline should act as:

curl -s -X POST http://maljan.internal:8000/api/v1/audit/api-keys \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name": "ci", "expires_in_days": 90}'

The answer's raw_key is shown once; store it as a CI secret. Keys are stored only as a SHA-256 hash, can carry an expiry, and an unknown, revoked or expired key is refused with the same 401. last_used_at is stamped on every accepted call. See Security.

Submit, wait, act

  1. Upload the sample.

    SAMPLE=$(curl -s -X POST "$MALJAN/api/v1/samples/upload" \
      -H "X-API-Key: $MALJAN_API_KEY" -F "file=@$SAMPLE_PATH" | jq -r .id)
    
  2. Start the job, naming the team if it is not the deployment's active one.

    JOB=$(curl -s -X POST "$MALJAN/api/v1/jobs" \
      -H "X-API-Key: $MALJAN_API_KEY" -H 'Content-Type: application/json' \
      -d "{\"sample_id\":\"$SAMPLE\",\"config\":{\"profile\":\"default\"}}" | jq -r .id)
    

    A 422 here is a refusal to start a job that could only fail — a model no probe has reached, a static provider that is not ready, an unknown team — and its detail says which.

  3. Wait for a terminal status — completed, failed or cancelled.

    while :; do
      STATUS=$(curl -s "$MALJAN/api/v1/jobs/$JOB" -H "X-API-Key: $MALJAN_API_KEY" | jq -r .status)
      case "$STATUS" in completed|failed|cancelled) break ;; esac
      sleep 30
    done
    
  4. Read the verdict and fetch what to act on.

    REPORT=$(curl -s "$MALJAN/api/v1/reports/job/$JOB" -H "X-API-Key: $MALJAN_API_KEY")
    echo "$REPORT" | jq '{verdict, verdict_reading, overall_confidence}'
    REPORT_ID=$(echo "$REPORT" | jq -r .id)
    
    curl -s "$MALJAN/api/v1/reports/$REPORT_ID/iocs" -H "X-API-Key: $MALJAN_API_KEY" > iocs.json
    curl -s "$MALJAN/api/v1/reports/$REPORT_ID/stix" -H "X-API-Key: $MALJAN_API_KEY" > bundle.json
    

What to gate on

  • verdict is Malware, Suspicious or Benign. Gate on it together with verdict_reading: stated means the judge's structured answer stated it, fallback that it was read from an answer that was not a bundle, and unrecognised and unstated that the judge's conclusion could not be read, so the inconclusive Suspicious was published.
  • The IOC feed's default is only what the publish rule publishes, each row with publish_answer saying why. That is the set to push to a blocklist; see The IOC feed.
  • A failed job can still have a report, with incomplete_reason saying where the run failed. Treat it as incomplete rather than as a verdict.

Runs take minutes to hours depending on the model and the team, so give the waiting step a timeout that suits the deployment. Requests are rate-limited per client address and path; see Operations.