Vetrix Docs

Security Scanning

Vetrix runs static analysis and dependency auditing on every pipeline. Results surface in merge requests and the repository's Security tab.

Scan types

SAST (Static Application Security Testing)

SAST analyzes source code for common vulnerability patterns without executing it. Supported languages: Go, Python, JavaScript, TypeScript, Java, Ruby, PHP, C, C++.

Findings are classified by CWE (Common Weakness Enumeration) and assigned a severity:

Severity Description
critical Direct code execution, auth bypass, SQL injection
high Privilege escalation, sensitive data exposure
medium Indirect injection, insecure defaults
low Best-practice deviations, informational

Dependency scanning

Checks go.sum, package-lock.json, requirements.txt, Gemfile.lock, pom.xml, Cargo.lock, and similar lock files against the OSV (Open Source Vulnerabilities) database.

Reports include CVE ID, CVSS score, affected version range, and fixed version if available.

Secret detection

Detects accidentally committed credentials. See secrets.md for details.

Container scanning

When a pipeline produces a Docker image, Vetrix can scan it for OS and application layer CVEs. Requires the runner to have access to the image after build.

Enable per-repo in Settings → Security → Container Scanning.

Viewing results

After a pipeline runs, security findings appear in:

  • Merge request — as review comments on the affected lines, and as a summary widget below the diff
  • Security tab — consolidated view across all branches with filter by severity, scanner, and status
  • Pipeline view — per-job findings in the job log

Dismissing a finding

Findings can be dismissed with a reason:

  • not_applicable — the code path is unreachable or the pattern is a false positive
  • acceptable_risk — the risk is acknowledged and accepted
  • wont_fix — intentional behaviour

Dismissals are recorded in the audit log with the dismissing user and timestamp.

MR (merge request) security gates

Configure the pipeline to block merges when unresolved findings exceed a threshold.

In vetrix.toml or via admin settings:

[security]
block_on_critical = true   # block PR merge if any critical finding is unresolved
block_on_high     = false

Or via admin API:

curl -X PUT https://<vetrix-host>/api/v1/admin/settings/security.block_on_critical \
  -H "Authorization: Bearer <admin-token>" \
  -H "Content-Type: application/json" \
  -d '"true"'

Overriding the block

Repository admins can bypass the security gate on individual PRs by clicking Override security gate in the PR security widget. The override is recorded in the audit log.

Pipeline integration

Security scans run as implicit jobs injected into every pipeline. They do not need to be declared in .ci/pipeline.yml. To disable scanning for a specific repo:

curl -X PATCH https://<vetrix-host>/api/v1/repos/<owner>/<repo>/settings \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"security_scanning": false}'

Requires maintain role or higher.

API reference

List findings

GET /api/v1/repos/:owner/:repo/security/findings?severity=high&status=open&page=1

Get finding

GET /api/v1/repos/:owner/:repo/security/findings/:id

Dismiss finding

POST /api/v1/repos/:owner/:repo/security/findings/:id/dismiss
Content-Type: application/json

{"reason": "not_applicable", "note": "dead code path, validated by manual review"}

Reopen dismissed finding

POST /api/v1/repos/:owner/:repo/security/findings/:id/reopen

Enabling scanners from the admin UI

For routine operator use, the canonical place to enable or disable any of the four scanner backends is /admin/settings → Security scanners. The section lists one toggle per scan type — Container (Trivy), SAST (Gosec), SCA (Govulncheck + Trivy-fs), Secrets (Gitleaks) — each bound to a security.<type>.enabled setting key.

Each toggle calls PUT /api/v1/admin/settings/security.<type>.enabled with a JSON body of {"value":"true"} or {"value":"false"}. The backend SettingsListener picks the change up off its change channel and re-runs the scanner registry sync within ≤2 seconds, without a server restart. The capability advertised on GET /security/capabilities updates on the same tick, so the FE Trigger-scan dropdown reflects the new state on the next refresh.

The VETRIX_SCA_ENABLED environment variable (and its VETRIX_SAST_ENABLED / VETRIX_CONTAINER_ENABLED / VETRIX_SECRETS_ENABLED siblings) remain available, but only as a bootstrap-only seed: when set in the server's startup environment, they pre-populate the matching security.<type>.enabled row on first boot. After that the admin UI is the source of truth — flipping the env var without restarting has no effect, and once the row exists the env var is ignored on subsequent boots.

Use the env-var seed for first-boot provisioning in IaC. Use the admin UI for ongoing operator changes.

Inline binary not found on PATH status

When the server's boot-time probe (see §3 below) reports that a registered scanner's binary is not reachable on PATH, the GET /security/capabilities response carries a binary_missing: true flag on the matching row. The admin Security-scanners panel surfaces this inline next to the toggle:

[x] SAST (Gosec)        binary not found on PATH

The inline status renders as a <span role="status" class="text-vetrix-danger"> so screen-reader users hear the signal when they reach the toggle. The toggle itself stays clickable regardless — operators may pre-enable a scanner in anticipation of installing the binary on the runner image, and the row will simply continue to surface the status until the next boot probe reports the binary present.

The signal is best-effort progressive enhancement: when the capabilities endpoint is unreachable or returns rows without the binary_missing field (e.g. against a backend that predates the boot probe), the panel falls back to rendering the toggle without the inline status. Operator action is never gated on the probe.

Note: the admin Settings page is instance-scoped, but GET /security/capabilities is per-repo. Capabilities themselves are instance-wide, so the page calls the endpoint against the canonical bootstrap repo (vetrix/vetrix) to retrieve the same row set the per-repo Security tab would see. A dedicated /api/v1/admin/security/capabilities endpoint is a future refinement; for now the per-repo path is the single source of truth.

Verifying enablement

End-to-end coverage of the admin toggle → repo Security-tab affordance round-trip lives in vetrix-frontend/src/__tests__/e2e/security-scanner-enablement.spec.ts (Playwright, @slow @scannerenablement). The spec exercises both flows operators care about:

  • Flow 1 — full enablement. Authenticate as the super-admin, toggle the SCA scanner on at /admin/settings → Security scanners, navigate to /{owner}/{repo}/security, assert that the outer Run scan button is enabled, click it, assert that SCA is the default selection in the Trigger-scan dropdown, then toggle SCA off back at /admin/settings and assert that the outer Run scan button flips back to disabled with the documented tooltip.
  • Flow 2 — binary-missing. Stage a capabilities response where SCA carries binary_missing: true (mirroring the boot-probe signal from the scanner.tool.missing log), assert that the inline binary not found on PATH status renders next to the SCA toggle with role="status" and the text-vetrix-danger color, that the toggle remains clickable so operators can pre-enable a scanner in anticipation of installing the binary, and that the Security-tab Run scan button stays disabled while available: false.

The spec mocks /api/v1/admin/settings and /api/v1/repos/{owner}/{repo}/security/capabilities with page.route so the test is deterministic regardless of the local stack's scanner registry or boot-probe state. The per-scan POST /security/scans round-trip is deliberately out of scope here — that is covered by security-scannerfix.spec.ts.

To run the verification locally:

# Inside vetrix-frontend/
npx playwright test src/__tests__/e2e/security-scanner-enablement.spec.ts

SCA enablement runbook

This runbook walks an operator through turning on Software Composition Analysis (SCA) — dependency vulnerability scanning — for a Vetrix deployment. SCA is registered as a composite scanner that picks govulncheck for Go modules (any target with a go.mod at the root) and trivy fs for everything else; a single registration covers polyglot deployments.

The pieces below are what the operator changes on the server host.

1. Set the env-var gate

The SCA composite is gated behind a single environment variable. Set it on the server process (compose file, systemd unit, Kubernetes deployment, etc.):

VETRIX_SCA_ENABLED=1

When the variable is unset (or any value other than 1), the SCA composite is not registered and the capabilities endpoint will not advertise sca. This is the intended "feature disabled" pattern — no scanner row is created.

Note: VETRIX_SCA_ENABLED and its siblings function as a bootstrap-only seed — they pre-populate the security.sca.enabled setting row on first boot, after which the admin UI's Security scanners panel is the source of truth. See Enabling scanners from the admin UI above.

2. Install the scanner binaries

The two backends must be on the runner image's PATH with their canonical names:

Tool Binary name Verifies via
govulncheck govulncheck govulncheck -version
trivy trivy trivy --version

Install both in the runner image (see the runner Dockerfile reference in builds//your runner build context). On the server host where the boot probe runs, the same binaries must also be reachable so the boot-time probe can confirm presence. A typical install snippet:

# govulncheck — installed via the Go toolchain
GOBIN=/usr/local/bin go install golang.org/x/vuln/cmd/govulncheck@latest

# trivy — distributed as a static binary
TRIVY_VERSION=0.50.0
curl -fsSL "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" \
  | tar -xz -C /usr/local/bin trivy

3. Restart the server and confirm via the boot log

Restart the Vetrix server process. On boot, the SCA registration block emits an INFO line confirming the composite is wired:

vetrix: SCA scanner registered (composite: govulncheck + trivy fs) env=VETRIX_SCA_ENABLED=1

Immediately after registration, the boot probe invokes each backend's version flag under a 3-second timeout and emits one structured log line per binary:

Log key Level Meaning
scanner.tool.detected INFO Binary present on PATH; version field carries the banner.
scanner.tool.missing WARN Binary missing or non-zero exit; error field carries the cause.

A scanner.tool.missing line is non-fatal — registration already happened, and the per-scan code path will surface its own error if a request arrives for an unavailable backend. The point of the boot probe is observability: it answers "did we register a scanner whose binary doesn't exist?" while the operator is still watching the boot output.

Sample healthy boot output:

INFO  vetrix: SCA scanner registered (composite: govulncheck + trivy fs)  env=VETRIX_SCA_ENABLED=1
INFO  scanner.tool.detected  tool=govulncheck  version=govulncheck@v1.1.3
INFO  scanner.tool.detected  tool=trivy        version=Version: 0.50.0

Sample boot output where trivy is missing:

INFO  vetrix: SCA scanner registered (composite: govulncheck + trivy fs)  env=VETRIX_SCA_ENABLED=1
INFO  scanner.tool.detected  tool=govulncheck  version=govulncheck@v1.1.3
WARN  scanner.tool.missing   tool=trivy        error=trivy --version: exec: "trivy": executable file not found in $PATH

4. Verify end-to-end

Once the server is up:

  1. Confirm the capability is advertised — see Capability discovery below. The sca row should appear with the tool reported as govulncheck+trivy (the composite presents both).
  2. Trigger a scan from the Security tab on any repository, or via POST /api/v1/repos/:owner/:repo/security/scans with {"scan_type":"sca","commit_sha":"<ref>"}. A symbolic ref (branch / tag / HEAD / short SHA) is accepted and resolved to the canonical 40-char SHA before dispatch.

Capability discovery

The capabilities endpoint advertises which scanners the current deployment has registered. It is consumed by the FE to filter the Trigger scan dropdown — users only see scan types that the deployment actually serves.

Endpoint

GET /api/v1/repos/:owner/:repo/security/capabilities

Auth: required. Same requireAuth gate as POST /security/scans — a valid bearer token is required, and the repo must be visible to the caller (404 otherwise to avoid disclosing capability details).

Response shape

200 OK with a JSON object whose only key is capabilities, an array of zero or more capability rows. The empty registry response is {"capabilities": []} (the slice is preallocated as non-nil so it marshals to [] rather than null).

Field Type Description
capabilities[].scan_type string Wire token for the scan kind. Sorted ascending. Canonical values: sca, sast, secrets, container.
capabilities[].tool string Underlying tool name (e.g. govulncheck+trivy, semgrep, gitleaks, trivy).
capabilities[].version string Best-effort version string captured at registration. May be empty if the registry does not retain a probed version.
capabilities[].available boolean Whether the scanner can currently service a scan request. This is always true for any registered scanner.
capabilities[].binary_missing boolean (optional) true when the boot-time probe (§3) reported the underlying binary missing on the server's PATH. Optional + absent-treated-as-false for back-compat with responses that predate the boot probe. Surfaced inline in /admin/settings → Security scanners.

Example request

curl -sS https://<vetrix-host>/api/v1/repos/<owner>/<repo>/security/capabilities \
  -H "Authorization: Bearer <token>"

Example response

The canonical four-scanner shape on a deployment with all backends registered:

{
  "capabilities": [
    { "scan_type": "container", "tool": "trivy",                "version": "Version: 0.50.0",       "available": true },
    { "scan_type": "sast",      "tool": "semgrep",              "version": "1.50.0",                  "available": true },
    { "scan_type": "sca",       "tool": "govulncheck+trivy",    "version": "govulncheck@v1.1.3",      "available": true },
    { "scan_type": "secrets",   "tool": "gitleaks",             "version": "v8.18.0",                 "available": true }
  ]
}

Notes for FE consumers

  • The FE Trigger-scan dropdown derives its options from this response. Users only see scan types that the deployment actually serves.
  • An empty array drives the empty-state UI in the Security tab — the dropdown is disabled and a "Configure scanners" CTA links to /system-docs/security/scanning#enablement.
  • Disabled options carry a native title="Not enabled on this instance." tooltip so a hover (or screen-reader description) explains why the option is greyed out.
  • The available field is always true for any registered scanner. The boot-time probe reports binary presence at startup; there is no per-request gate that flips the field to false for scanners whose binaries are unreachable at request time.

Troubleshooting

The five most common failure modes operators see when bringing SCA — or any scanning backend — online:

1. tool_missing (HTTP 503)

A POST /security/scans request returned a structured error envelope with error_code: "tool_missing". The scan_type is recognised but no scanner is registered for it on this instance.

What to check:

  • Confirm the env-var gate is set in the server environment (e.g. VETRIX_SCA_ENABLED=1).
  • Re-read the boot log for vetrix: SCA scanner registered …. If the line is absent, the env-var was not in scope when the server started.
  • For non-SCA scan types (sast, secrets, container), the same pattern applies — the registration is gated by the relevant feature env-var on its own backend (per-backend env vars are deployment-specific).

2. ref_not_found (HTTP 404)

A POST /security/scans request returned error_code: "ref_not_found". The symbolic ref in the commit_sha field did not resolve to a real commit.

What to check:

  • Typos in the input — branch, tag, or short SHA spelt slightly wrong.
  • Stale tags — a release tag that was deleted upstream after the caller cached it.
  • Short SHAs that have become ambiguous after subsequent pushes.

Ref resolution runs before the scanner-registry lookup, so a junk ref always surfaces as ref_not_found — not tool_missing (503) — regardless of which scan type was requested.

3. Empty /capabilities response

GET /security/capabilities returned {"capabilities": []}. The scanner registry is empty in this deployment.

What to check:

  • Every scanner backend (SCA / SAST / secrets / container) is gated by its own env-var. If none are set, the registry is empty by design.
  • The FE will render the Configure scanners empty state on the Security tab; the CTA links to the SCA enablement runbook above.
  • This is not an error — a fresh deployment with no scanners enabled is expected to return the empty list. Compare to a 503, which would mean the endpoint or service itself is down.

4. SCA tools installed but scanner.tool.missing at boot

The binaries appear to be installed (you can run them by hand) but the boot probe still emits scanner.tool.missing for govulncheck or trivy.

What to check:

  • PATH mismatch between the operator's interactive shell and the server process. Confirm with which govulncheck and which trivy from the same shell that launches the server (e.g. inside the systemd unit's ExecStart context, or via docker exec into the running container).
  • Wrong binary names — the probe specifically invokes govulncheck -version and trivy --version. If the binary was installed under an alternative name (gvc, trivy-bin, etc.), the probe will not find it.
  • The probe runs under a 3-second timeout — a binary that exits cleanly but takes longer than 3 seconds to print its version banner will surface as a scanner.tool.missing WARN. This is rare in practice but possible on heavily loaded hosts; rerun the probe by restarting the server when the host is quiet.

Note: as documented above, a scanner.tool.missing log is non-fatal — the scanner is still registered and per-scan errors will surface separately if a request arrives.

5. Disabled option in the FE dropdown with tooltip "Not enabled on this instance."

The Trigger-scan dropdown shows a scan type as disabled, with a native browser tooltip reading "Not enabled on this instance."

This means the BE registry contains the row, but its available field is false. available is always true for any registered scanner, so an available: false row implies a build with probe-driven availability behaviour. If you see this on a current build:

  • Re-fetch /security/capabilities and inspect the row's available field directly.
  • Check the boot log for scanner.tool.missing on the corresponding backend — that is the most likely upstream cause of an available: false row.
  • File a bug if the dropdown shows the disabled tooltip but /capabilities reports available: true; the FE state has drifted from the BE response.