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¶
- The agent polls the VirtualSutra API for pending scan jobs (
POST /scan-jobs/claim) - When a job is claimed, it clones the repository locally using credentials from your environment
- Scanner containers are spawned via the local Docker socket and run against the clone
- Findings are batch-posted back to the VirtualSutra API
- 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
tmpfsat/tmp. - The agent runs as non-root (UID 1000) with a read-only root filesystem.
- The
scan:agentcredential is bound per job and per organization - see What the agent may do.