Skip to the content.

How to build the Linux static-analysis guest

The Linux guest runs the pre-detonation static-analysis stage: PE/ELF parsing, fuzzy hashing (ssdeep, TLSH), deep YARA, CAPA capability detection, strings + entropy, and byte/opcode trigram MinHashing. No sample execution, ever.

Prerequisites

Base image

Install the VM with the minimal netinst image. Apt the basics:

apt-get update
apt-get install -y \
    python3.11 python3-pip python3-venv \
    yara libyara-dev \
    ssdeep libfuzzy-dev \
    libtlsh-dev \
    git ca-certificates

Install CAPA from upstream releases (it’s Go, so just one binary):

curl -LO https://github.com/mandiant/capa/releases/latest/download/capa-linux.zip
unzip capa-linux.zip -d /usr/local/bin/
chmod +x /usr/local/bin/capa
capa --version  # verify

Deploy the guest package

Unlike the Windows guest, the Linux guest doesn’t need a freeze step — it runs under the system Python interpreter.

# Clone and install.
git clone https://github.com/wrhalpin/SandGNAT /opt/sandgnat
cd /opt/sandgnat
python3.11 -m venv /opt/sandgnat/venv
/opt/sandgnat/venv/bin/pip install -e '.[static]'

The [static] extra pulls in pefile, pyelftools, ssdeep, python-tlsh, capstone, and yara-python. Missing ones degrade to skipped at runtime — the guest won’t crash without them.

Mount the staging share

The guest needs /srv/sandgnat/staging to point at the same SMB/NFS share the orchestrator writes to.

# /etc/fstab entry for SMB:
//orchestrator.internal/sandgnat  /srv/sandgnat/staging  cifs  credentials=/etc/sandgnat/smb.cred,uid=sandgnat,gid=sandgnat,_netdev  0 0

# Or NFS:
orchestrator.internal:/srv/sandgnat  /srv/sandgnat/staging  nfs  defaults,_netdev  0 0

Verify read+write:

sudo -u sandgnat ls /srv/sandgnat/staging
sudo -u sandgnat touch /srv/sandgnat/staging/completed/.write-test && \
    rm /srv/sandgnat/staging/completed/.write-test

Deep-YARA rules

Mount or sync the deep ruleset onto the guest — conventionally at /etc/sandgnat/yara-deep. See configure-yara.md.

Environment

/etc/sandgnat/env:

LINUX_GUEST_STAGING_ROOT=/srv/sandgnat/staging
LINUX_GUEST_POLL_INTERVAL=2.0
LINUX_GUEST_CAPA_EXE=/usr/local/bin/capa
LINUX_GUEST_YARA_DEEP_RULES_DIR=/etc/sandgnat/yara-deep
LINUX_GUEST_MAX_STRINGS_BYTES=1048576

systemd unit

/etc/systemd/system/sandgnat-static.service:

[Unit]
Description=SandGNAT Linux static-analysis guest
After=network-online.target remote-fs.target
Wants=network-online.target remote-fs.target

[Service]
Type=simple
User=sandgnat
Group=sandgnat
EnvironmentFile=/etc/sandgnat/env
ExecStart=/opt/sandgnat/venv/bin/python -m linux_guest_agent
Restart=on-failure
RestartSec=5s
# Tightened sandboxing: we only read samples and write artifacts.
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/srv/sandgnat/staging
PrivateTmp=true
ProtectHome=true

[Install]
WantedBy=multi-user.target

Enable:

useradd -r -s /bin/false sandgnat
chown -R sandgnat:sandgnat /opt/sandgnat
systemctl daemon-reload
systemctl enable --now sandgnat-static.service
journalctl -u sandgnat-static -f

Take the clean snapshot

Same flow as the Windows guest:

  1. Stop the service: systemctl stop sandgnat-static.service.
  2. Clean any pending samples: rm -rf /srv/sandgnat/staging/in-flight/* (on the host; shouldn’t be any if you only used this VM for prep).
  3. From the Proxmox host:

    qm snapshot 9001 clean --description "Debian 12 + SandGNAT static guest ready"
    

The Linux pool clones from vmid 9001 (LINUX_TEMPLATE_VMID) and reverts to clean after each job.

Test the template

# On the orchestrator host:
from uuid import uuid4
from pathlib import Path
from orchestrator.schema import MODE_STATIC_ANALYSIS, StaticAnalysisOptions
from orchestrator.guest_driver import submit_job, wait_for_result

job_id = uuid4()
# Stage a benign test sample first — e.g. /bin/ls from the guest or any small ELF.
# Put it at {staging}/samples/{job_id}/ls
submit_job(
    Path("/srv/sandgnat/staging"), job_id,
    sample_name="ls", sample_sha256="<sha256>",
    timeout_seconds=60, mode=MODE_STATIC_ANALYSIS,
    static=StaticAnalysisOptions(),
)
art = wait_for_result(Path("/srv/sandgnat/staging"), job_id, timeout_seconds=120)
print(art.envelope.status, art.envelope.static_summary)

Expect status=="completed" and static_summary["file_format"] == "elf64" for a typical binary.

Troubleshooting

Upgrading

The Linux guest doesn’t need a refreeze — it’s interpreted. For a code update:

cd /opt/sandgnat
git pull
/opt/sandgnat/venv/bin/pip install -e '.[static]'
systemctl restart sandgnat-static

Re-take the snapshot after verifying it comes up clean.