Skip to content

Repository files navigation

Scanii CLI

The Scanii CLI (sc) helps you build, test, and manage your Scanii integration right from the terminal.

With the CLI, you can:

  • Interact with the Scanii API: scan files, manage auth tokens, and check account info
  • Start a local server that simulates the Scanii API for integration testing
  • Process single files or entire directories with concurrent workers and byte-level upload progress

Installation

Homebrew (macOS, Linux)

brew trust --formula scanii/tap/scanii-cli
brew install scanii/tap/scanii-cli

From Homebrew 6.0, formulae in third-party taps must be trusted before Homebrew will load them, so without the first command the install fails with Refusing to load formula scanii/tap/scanii-cli from untrusted tap scanii/tap. Trust is recorded locally in ~/.homebrew/trust.json — it is your decision to make, and nobody can grant it on your behalf. Use brew trust scanii/tap instead to trust everything in the tap rather than just this formula.

brew trust prompts for nothing, so it drops straight into a CI script. It has existed since Homebrew 5.1.15; on anything older, skip it and just install.

Trust persists, so upgrades need nothing extra:

brew upgrade scanii-cli

Shell installer (macOS, Linux)

curl -fsSL https://raw.githubusercontent.com/scanii/scanii-cli/main/install.sh | sh

The installer downloads the matching release archive from GitHub, verifies it against checksums.txt, and installs sc to ~/.local/bin (override with SCANII_CLI_BIN_DIR). Pin a specific version with SCANII_CLI_VERSION=1.6.0.

Docker

A container image is published to the GitHub Container Registry:

docker run ghcr.io/scanii/scanii-cli:latest

Previous versions are listed here.

Binary releases

Pre-built binaries for macOS, Windows, and Linux are available on the releases page.

On macOS, you may need to remove the quarantine attribute before running:

xattr -d com.apple.quarantine /path/to/sc

Quick start

1. Configure a profile

Set up your API credentials and endpoint:

sc profile create --endpoint api-us1.scanii.com --credentials YOUR_KEY:YOUR_SECRET

This creates a default profile stored in ~/.config/scanii-cli/. You can create named profiles for different environments:

sc profile create staging --endpoint localhost:4000 --credentials key:secret

List configured profiles:

sc profile list

2. Test connectivity

sc ping

Use a non-default profile with the -p flag:

sc -p staging ping

3. Scan a file

Synchronous scan (blocks until the result is ready):

sc files process /path/to/file.pdf

Delete a processed result once you no longer need it:

sc files delete RESULT_ID

Progress is measured in bytes as they go out on the wire, so a large file advances smoothly while it uploads:

Using endpoint: api-us1.scanii.com and API key: mykey
Processing file /path/to/backup.tar
Uploading backup.tar  [█████████████████░░░░░░░░░░░░░]  58% (183.2 MB/314.6 MB)

Once the last byte is sent, the wait is on the server, so the bar gives way to a spinner until the result lands:

⠹ Analyzing backup.tar
# /path/to/backup.tar:

  id:             ff03467da11f417aa99845c91793ce0c
  checksum/sha1:  0c4f4f069728d13bf481738ac926381477fb8975
  content type:   application/octet-stream
  content length:  314.6 MB
  creation date:   Sat, 08 Aug 2026 08:41:43 EDT
  findings:       none
  metadata:       none

✔ Completed in 4.2 s, 1 file(s) analyzed. Throughput 74.9 MB/s
✔ Files with findings: 0, unable to process: 0 and successfully processed: 1

Every request records the X-Scanii-Request-Id the API returned — the id support needs to look a specific scan up on the server side. A request that failed quotes it in the error; otherwise --perf prints it, and -v logs it for every request:

2026-08-08 08:41:43.508 DEBUG 24068 internal/commands/file/common.go:38                : processed file path=/path/to/backup.tar status=201 request_id=req_9f3a1c

Asynchronous scan (returns immediately with a pending result ID):

sc files async /path/to/file.pdf

Retrieve the result of an async scan:

sc files retrieve RESULT_ID

Retrieve the processing trace for a result:

sc files trace RESULT_ID

4. Scan a remote URL

Submit a URL for server-side fetch and scan:

sc files fetch https://example.com/document.pdf

Wait for the result instead of returning immediately:

sc files fetch --wait 30 https://example.com/document.pdf

5. Scan an entire directory

Process all files in a directory with concurrent workers:

sc files process /path/to/directory

Skip hidden files and attach metadata:

sc files process --ignore-hidden --metadata env=production,scan_type=nightly /path/to/directory

Empty files are skipped rather than uploaded — the API rejects empty content, so sending it only buys a 400 — and the CLI reports how many it passed over:

Processing recursive directory /path/to/directory with ~12 files | ~50.3 MB
Skipping 3 empty file(s)

Directories are tracked the same way. The bar fills with the bytes uploaded so far across every file, and the label counts the files that have come back:

Using endpoint: api-us1.scanii.com and API key: mykey
Processing recursive directory /path/to/directory with ~12 files | ~50.3 MB
Files 7/12  [████████████████████████░░░░░░░░░░░░░░░░░░]  58% (29.7 MB/50.3 MB)

On completion, any file that came back with findings is listed, so a directory scan tells you where the malware was and not just how much of it there was:

Using endpoint: api-us1.scanii.com and API key: mykey
Processing recursive directory /path/to/directory with ~12 files | ~50.3 MB
Files 12/12  [████████████████████████████████████████]  100% (50.3 MB/50.3 MB)

## Files with findings

# /path/to/directory/quarantine/sample.txt:

  id:             e353d97476fe40c6abd20418efe82b96
  checksum/sha1:  7da9d3b0c68b1d0543acb65af4220a4745607557
  content type:   text/plain; charset=utf-8
  content length:  36 B
  creation date:   Sat, 08 Aug 2026 09:07:46 EDT
  findings:       content.malicious.eicar-test-signature
  metadata:       none

✔ Completed in 3.1 s, 12 file(s) analyzed. Throughput 16.2 MB/s
✔ Files with findings: 1, unable to process: 0 and successfully processed: 12

Files the API rejected are reported as they happen, with the reason the API gave and the request id to quote in a support ticket. When more than a handful fail, the ones that scrolled past are listed again at the end of the run:

error: /path/to/directory/huge.iso — status 413: File is too large (request id req_9f3a1c)

✔ Completed in 3.1 s, 11 file(s) analyzed. Throughput 16.2 MB/s
warning: Files with findings: 1, unable to process: 1 and successfully processed: 11
error: 1 of 12 file(s) could not be processed

sc files process and sc files async exit non-zero if any file could not be processed, so a scan that half-failed does not pass for a clean one in CI.

6. Measure where the time went

--perf prints a breakdown of the API requests a command made, which is how to tell a slow network apart from a slow scan. It works on every sc files command:

sc files process --perf /path/to/backup.tar
sc files retrieve --perf RESULT_ID
sc files fetch --perf --wait 30 https://example.com/document.pdf
## Performance
  request id:          req_9f3a1c
  dns:                 24 ms
  tcp connect:         31 ms
  tls handshake:       58 ms
  request transfer:    3.4 s
  server processing:   612 ms
  response transfer:   1.2 ms
  total:               4.1 s
  client overhead:     104 ms
  connection:          new

The phases run in that order and, give or take the wait for a free connection, add up to total:

Phase What it covers
dns Resolving the endpoint's host name
tcp connect Opening the socket
tls handshake Negotiating TLS
request transfer Sending the request — for a scan, the upload
server processing Last request byte out to first response byte back — the API's work plus the round trip carrying the question and the answer
response transfer First response byte to the last byte of the body
total The whole exchange
client overhead The rest of the run — reading and hashing the file, building the request, printing the result

server processing is measured from this end, so it cannot separate the API's work from the round trip it takes to ask and be answered; against a distant endpoint it reads higher than what the server reports for the same request, by about one round trip. It is the floor for a scan — client overhead and the three connection phases are the parts a caller can do something about.

A phase that did not happen reads n/a: a pooled connection resolves no name and shakes no hands, and a plaintext endpoint never reaches the TLS phase. So does a phase that finished inside the clock's resolution, which on Windows is a millisecond — not a distinction that matters for the latencies this is for.

A command that makes more than one request reports the mean instead, and counts how many of those requests rode on a connection that was already open. A directory scan sends one request per file; retrieve --wait and fetch --wait each poll until the result lands:

## Performance (mean of 128 requests)
  dns:                 1 ms
  tcp connect:         2 ms
  tls handshake:       12 ms
  request transfer:    184 ms
  server processing:   397 ms
  response transfer:   1.1 ms
  total:               597 ms
  connections:         96 of 128 reused

connections is worth watching on a directory scan. The pool is sized to --concurrency, so once the first wave of files has opened its connections the rest of the run should reuse them; a low reuse count means the run is paying a connect and a TLS handshake per file, and none of that shows up in the API's own timings.

A scan keeps 32 requests in flight by default. That number belongs to the link rather than to the machine: once the uplink is full, more requests in flight buy no throughput and cost a connection — and a TLS handshake — each. If --perf shows request transfer is not what the run is waiting on, there is headroom to raise --concurrency; if a directory scan reports few reused connections, it is already higher than the link can use.

7. Manage auth tokens

Create a short-lived auth token (default timeout: 300 seconds):

sc auth-token create --timeout 600

Retrieve or revoke a token:

sc auth-token retrieve TOKEN_ID
sc auth-token delete TOKEN_ID

Local server

The local server is the primary way to integration-test code that talks to the Scanii API. It implements the full v2.2 API surface including file processing, auth tokens, callbacks, and fetch-by-URL -- all without requiring real credentials or network access to Scanii servers.

Starting the server

sc server

Output:

Scanii local server starting
API Key:       key
API Secret:    secret
Engine Rules:  5
Callback Wait: 100ms
Address:       http://localhost:4000

Sample usage: curl -u key:secret http://localhost:4000/v2.2/ping

We also provide fake sample files you can use to trigger findings:
  content.image.nsfw.nudity:         http://localhost:4000/static/samples/image.jpg
  content.en.language.nsfw.0:        http://localhost:4000/static/samples/language.txt
  content.malicious.local-test-file: http://localhost:4000/static/samples/malware

Default credentials are key / secret. Override them with flags:

sc server --key my-key --secret my-secret --address 0.0.0.0:8080

Server options

Flag Default Description
-a, --address localhost:4000 Listen address
-k, --key key API key
-s, --secret secret API secret
-e, --engine built-in Path to a custom engine rules JSON file
-d, --data temp dir Directory for storing processing results
-w, --callback-wait 100ms Delay before firing callbacks; overrides callback_wait in the engine config

API endpoints

All endpoints are under the /v2.2/ prefix and require HTTP Basic Auth:

Method Path Description
GET /v2.2/ping Health check
GET /v2.2/account.json Account info (returns mock data)
POST /v2.2/files Synchronous file scan
POST /v2.2/files/async Async file scan (returns pending ID)
POST /v2.2/files/fetch Fetch remote URL and scan
GET /v2.2/files/{id} Retrieve scan result
DELETE /v2.2/files/{id} Delete scan result
GET /v2.2/files/{id}/trace Retrieve processing trace
DELETE /v2.2/files/{id}/trace Delete processing trace
POST /v2.2/auth/tokens Create auth token
GET /v2.2/auth/tokens/{id} Retrieve auth token
DELETE /v2.2/auth/tokens/{id} Delete auth token

Static sample files are served without authentication under /static/.

curl examples

Ping:

curl -u key:secret http://localhost:4000/v2.2/ping
{"key":"key","message":"pong"}

Synchronous file scan:

curl -u key:secret -F "file=@test.pdf" http://localhost:4000/v2.2/files
{
  "id": "fd33128a8da445d3b8308fe6d1588829",
  "checksum": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
  "content_length": 1024,
  "content_type": "application/pdf",
  "findings": [],
  "metadata": {},
  "creation_date": "2024-02-08T13:38:02.074502Z"
}

Scan with metadata and callback:

curl -u key:secret \
  -F "file=@test.pdf" \
  -F "metadata[env]=staging" \
  -F "metadata[ticket]=JIRA-123" \
  -F "callback=https://your-app.example.com/webhook" \
  http://localhost:4000/v2.2/files/async

Fetch and scan a remote URL:

curl -u key:secret \
  -d "location=http://localhost:4000/static/eicar.txt" \
  http://localhost:4000/v2.2/files/fetch

Scan the EICAR test file (triggers a malware finding):

curl -u key:secret \
  -F "file=@-" \
  http://localhost:4000/v2.2/files < <(curl -s http://localhost:4000/static/eicar.txt)

Create and retrieve an auth token:

# Create a token valid for 600 seconds
curl -u key:secret -d "timeout=600" http://localhost:4000/v2.2/auth/tokens

# Retrieve it
curl -u key:secret http://localhost:4000/v2.2/auth/tokens/TOKEN_ID

# Delete it
curl -u key:secret -X DELETE http://localhost:4000/v2.2/auth/tokens/TOKEN_ID

How the engine works

The local server does not perform real content analysis. Instead, it computes SHA-1 and SHA-256 hashes of uploaded content and matches them against a static rule database. The built-in rules include signatures for:

Sample file Finding Trigger
/static/eicar.txt content.malicious.eicar-test-signature Standard EICAR test string
/static/samples/image.jpg content.image.nsfw.nudity Sample NSFW image
/static/samples/language.txt content.en.language.nsfw.0 Sample unsafe-language text
/static/samples/malware content.malicious.local-test-file Generic malware test file

Any file that does not match a known signature returns an empty findings list.

Custom engine rules

For more sophisticated testing, provide your own rules file:

sc server --engine /path/to/rules.json

The JSON format is:

{
  "callback_wait": "100ms",
  "rules": [
    {
      "format": "sha256",
      "content": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
      "result": "your.custom.finding"
    }
  ]
}

callback_wait is optional and must be a duration string such as "100ms" or "2s"; --callback-wait overrides it when passed.

Supported hash formats are sha1 and sha256. Generate a hash for your test file with:

shasum -a 256 /path/to/your/test-file

The rules in your file replace the built-in ones rather than adding to them, so Engine Rules: in the startup banner reflects your file alone. A config that is missing, unreadable, or not a valid rules file stops the server from starting instead of quietly falling back to the built-in rules:

% sc server --engine foo
error: opening engine config: open foo: no such file or directory

Callbacks

The local server supports callbacks. When a callback URL is included in an async or fetch request, the server POSTs a JSON payload to that URL containing the processing result (id, findings, checksum, content_type, content_length, creation_date, and metadata). The callback fires after a configurable delay: 100ms by default, callback_wait in an --engine config, or --callback-wait which overrides both. The effective value is shown as Callback Wait: in the startup banner.

Callbacks are fire-and-forget: if the target URL is unreachable, the delivery fails silently and the server continues operating normally.

Using the Docker image in CI

The Docker image is the simplest way to run the local server as a service in CI pipelines for integration testing.

GitHub Actions

Use the services block to start the local server alongside your test job:

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      scanii:
        image: ghcr.io/scanii/scanii-cli:latest
        ports:
          - 4000:4000
        options: >-
          --health-cmd "wget -qO- http://localhost:4000/v2.2/ping || exit 1"
          --health-interval 5s
          --health-timeout 3s
          --health-retries 5
    env:
      SCANII_ENDPOINT: http://localhost:4000
      SCANII_KEY: key
      SCANII_SECRET: secret
    steps:
      - uses: actions/checkout@v4

      - name: Run integration tests
        run: make test-integration

If you need the local server across a matrix of operating systems (including macOS and Windows where Docker services are not available), download the binary from GitHub Releases instead:

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    steps:
      - uses: actions/checkout@v4

      - name: Download scanii-cli
        shell: bash
        run: |
          case "${{ runner.os }}" in
            Linux)   OS=linux;   ARCH=amd64; EXT=tar.gz ;;
            macOS)   OS=darwin;  ARCH=amd64; EXT=tar.gz ;;
            Windows) OS=windows; ARCH=amd64; EXT=zip    ;;
          esac
          gh release download --repo scanii/scanii-cli \
            --pattern "scanii-cli-*-${OS}-${ARCH}.${EXT}" \
            --dir /tmp
          # Extract and add to PATH
          if [ "$EXT" = "tar.gz" ]; then
            tar -xzf /tmp/scanii-cli-*.${EXT} -C /tmp
          else
            unzip /tmp/scanii-cli-*.${EXT} -d /tmp
          fi

      - name: Start local server
        shell: bash
        run: |
          /tmp/scanii-cli-*/sc server &
          # Wait for the server to be ready
          for i in $(seq 1 30); do
            curl -sf http://localhost:4000/v2.2/ping && break
            sleep 1
          done

      - name: Run integration tests
        run: make test-integration

GitLab CI

test:
  image: your-app-image:latest
  services:
    - name: ghcr.io/scanii/scanii-cli:latest
      alias: scanii
      command: ["server", "--address", "0.0.0.0:4000"]
  variables:
    SCANII_ENDPOINT: http://scanii:4000
    SCANII_KEY: key
    SCANII_SECRET: secret
  script:
    - make test-integration

Docker Compose

For local development, add the server to your docker-compose.yml:

services:
  scanii:
    image: ghcr.io/scanii/scanii-cli:latest
    command: ["server", "--address", "0.0.0.0:4000"]
    ports:
      - "4000:4000"
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:4000/v2.2/ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  your-app:
    build: .
    depends_on:
      scanii:
        condition: service_healthy
    environment:
      SCANII_ENDPOINT: http://scanii:4000
      SCANII_KEY: key
      SCANII_SECRET: secret

Test credentials

When using the local server (Docker or binary), the default credentials are:

Setting Value
Endpoint http://localhost:4000
API Key key
API Secret secret

Global flags

Flag Description
-v, --verbose Enable debug logging
-p, --profile NAME Use a named profile (default: default)

All commands

Command Description
sc profile create [name] Create or update a profile
sc profile list [name] List profiles or show details of one
sc profile delete <name> Delete a profile
sc ping Test API connectivity
sc account Show account information
sc files process <path> Synchronous file/directory scan
sc files async <path> Asynchronous file/directory scan
sc files fetch <url> Fetch and scan a remote URL
sc files retrieve <id> Retrieve a scan result
sc files delete <id> Delete a scan result
sc files delete-trace <id> Delete a scan result's processing trace
sc files trace <id> Retrieve the processing trace for a scan result
sc auth-token create Create a temporary auth token
sc auth-token retrieve <id> Retrieve token details
sc auth-token delete <id> Revoke a token
sc server Start the local server
sc version Display version and build info

Run sc help or sc <command> --help for detailed usage of any command.

Known limitations

  • The local server engine does not perform real content analysis; it matches files by hash only
  • Requests that fail to download a remote URL via /files/fetch record an error but the result is still stored

License

See LICENSE for details.

About

Scanii.com command line interface

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages