Skip to content

Scan Agent (Customer-Hosted)

The VirtualSutra Scan Agent is a lightweight, customer-hosted polling agent that runs inside your network boundary. It enables code scanning for organizations that cannot grant VirtualSutra direct access to their source code repositories.

How It Works

  1. The agent polls the VirtualSutra API for pending scan jobs (POST /scan-jobs/claim)
  2. When a job is claimed, it clones the repository locally using credentials from your environment
  3. Scanner containers are spawned via the local Docker socket and run against the clone
  4. Findings are batch-posted back to the VirtualSutra API
  5. The clone directory is deleted - code never leaves your network

Credentials

The agent authenticates as a service account carrying exactly the scan:agent scope - a dedicated, least-privilege scope covering only the scan-job queue.

Issue it from the console under Settings → Agents → "Add hosted agent". That page creates the service account, shows the client id and secret once, and renders ready-to-paste docker run and helm install snippets pre-filled with your API URL. There is no need to hand-craft an API key, and no scope list to choose from - the scope is fixed.

Existing agents must be re-issued a credential

If you deployed the agent before this change, its credential holds a broader, older scope and is refused at claim. Re-issue it from Settings → Agents; the agent picks up the new credential on the next poll. Nothing else about your deployment changes.

What the agent may do

The scan:agent credential can only claim a job, heartbeat it, fail it, upload findings for it, and complete it - and only for jobs it claimed itself. Every one of those calls additionally verifies that the job belongs to this agent's own credential and to this agent's own organization; anything else answers "scan job not found". A stolen agent credential cannot read, take over, or write findings against another agent's job, and it cannot write findings against a job in another organization.

Availability

scan:agent, the per-job ownership checks, and the Settings → Agents page that issues the credential all ship together. Until that release is deployed to your environment, the console cannot issue this scope; existing agent credentials keep working unchanged, and an agent credential should be treated as organization-wide.

Variable Required Meaning
VSUTRA_API_URL yes Base URL of the VirtualSutra API
VSUTRA_CLIENT_ID preferred Service-account client id (scan:agent scope)
VSUTRA_CLIENT_SECRET preferred Service-account client secret
VSUTRA_API_KEY fallback Static API key - still supported, also needs the scan:agent scope

When both VSUTRA_CLIENT_ID and VSUTRA_CLIENT_SECRET are set, the agent uses the OAuth2 client-credentials grant (short-lived JWT, refreshed before expiry, centrally revocable) and ignores VSUTRA_API_KEY.

Scanners

The agent runs eight scanners. Which of them run on a given job is decided server-side from the repository group's configuration and arrives on the claimed job.

Scanner Finds
Secrets Committed credentials (Gitleaks + TruffleHog)
IaC Infrastructure-as-code misconfiguration (Checkov)
SAST Static application security issues (Semgrep)
Dependencies Vulnerable dependencies (pip-audit, osv-scanner, govulncheck)
SBOM Software bill of materials, CVE matching and license violations (Syft + Grype)
Best practices Language-idiomatic Semgrep rulesets plus package-health manifest checks
PII Personal data in the repository
Repository posture Per-repository source-control posture - branch protection, visibility (GitHub only)

Every scanner except repository posture runs with --network none and a read-only bind mount of the clone. Repository posture is the one exception: it reads the source-control API rather than the working tree, so it is the only scanner given network access - and correspondingly the only one the repository is not mounted into. It is skipped with a warning when the job supplies no organization, repository name or GitHub token, including for GitLab, Bitbucket and Azure DevOps repositories, whose credentials are never released to it.

Current coverage - read this before relying on a clean report

Three of the eight scanners produce no findings on the agent today

The agent dispatches all eight, but only five of them currently return findings - secrets, IaC, PII, repository posture, and the package-health half of best practices. A clean SAST or dependency result from the hosted agent does not mean the repository is clean. Run those scanners through a cloud-hosted scan until this is fixed.

SAST, dependencies and SBOM cannot work inside the --network none, read-only sandbox the agent enforces: their rule bundles and vulnerability databases are fetched at scan time and there is no egress. SAST and dependencies exit successfully with zero findings rather than failing loudly; SBOM still produces the bill of materials but matches no CVEs. Tracked internally as codebase-analyzer#39.

The API reports this machine-readably so your tooling never has to infer it. GET /scan-jobs/capabilities returns, per runner backend:

Field Meaning
scanners What the backend dispatches - the eight names above for the agent
degraded What it dispatches but cannot deliver today, each with a reason
effective scanners minus degraded - the honest answer to "what will I actually get"

The console's run wizard renders both kinds of gap: a scanner the selected backend cannot run at all, and a scanner it runs but cannot deliver.

Deployment

The scan agent is deployed as a single container (ghcr.io/virtual-sutra/scan-agent) with access to the Docker socket. It can run:

  • Docker Compose - standalone container with Docker socket mount
  • Kubernetes - Helm chart at charts/helm/scan-agent/; mounts the host Docker socket

Settings → Agents renders both snippets for you with your own API URL filled in. The shapes are:

docker run -d --name vsutra-scan-agent \
  -e VSUTRA_API_URL=https://api.example.com \
  -e VSUTRA_CLIENT_ID=... \
  -e VSUTRA_CLIENT_SECRET=... \
  -v /var/run/docker.sock:/var/run/docker.sock \
  ghcr.io/virtual-sutra/scan-agent:latest

The agent needs a Docker socket because it spawns the scanner containers itself.

Optional Configuration

Env Var Default Description
AGENT_ID auto-generated Stable identifier for this agent instance
POLL_INTERVAL_SECONDS 15 Seconds between poll attempts
POLL_BACKOFF_SECONDS 60 Seconds between polls when queue is empty
HEARTBEAT_INTERVAL_SECONDS 30 Heartbeat frequency during active scans
SCANNER_TIMEOUT_SECONDS 1200 Max time per scanner container
DOCKER_HOST unix:///var/run/docker.sock Docker daemon socket

Air-Gapped / Private Registry

For environments without access to ghcr.io, configure a private registry:

Env Var Description
SCANNER_REGISTRY_PREFIX Registry prefix (e.g. registry.corp.internal/vsutra) - rewrites all default scanner image references
SCANNER_REGISTRY_USERNAME Registry username - triggers docker login at startup
SCANNER_REGISTRY_PASSWORD Registry password (passed via stdin, never logged)

Per-scanner image overrides (SCANNER_SECRETS_IMAGE, SCANNER_IAC_IMAGE, SCANNER_SAST_IMAGE, SCANNER_DEPS_IMAGE, SCANNER_SBOM_IMAGE, SCANNER_PII_IMAGE, SCANNER_REPO_POSTURE_IMAGE) take highest priority over the prefix.

Helm Values

scan-agent:
  enabled: true
  api:
    url: "https://api.example.com"
  auth:
    clientId: "..."       # service account with the scan:agent scope
    clientSecret: "..."   # delivered via a Kubernetes Secret
  registry:
    prefix: "registry.corp.internal/vsutra"  # optional
    username: ""  # optional
    password: ""  # optional

The OAuth2 client-credentials flow needs POST /auth/token mounted on the API, which requires the External API to be enabled on your deployment.

Security

  • Outbound HTTPS only. The agent opens no inbound ports; it polls out.
  • Source never leaves your network. Repositories are cloned to the agent's own host, mounted read-only into the scanners, and the clone directory is deleted when the job finishes.
  • Only findings are sent back, and a finding never carries a raw secret. Where a secret scanner matches a credential, the matched value is replaced by a fixed mask plus a short hash prefix of it, and where the scanner emits a surrounding line of context the captured secret is masked out of that too. The scanner's own claim to have redacted a value is not trusted - the mask is applied regardless. The raw value never enters a finding's title, description, evidence or tags.
  • Scanner containers run with --network none - with the single documented exception of repository posture, above.
  • Repository clones are bind-mounted read-only, and every scanner container runs read-only with a single writable tmpfs at /tmp.
  • The agent runs as non-root (UID 1000) with a read-only root filesystem.
  • The scan:agent credential is bound per job and per organization - see What the agent may do.