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 positiveacceptable_risk— the risk is acknowledged and acceptedwont_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/settingsand 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 thescanner.tool.missinglog), assert that the inlinebinary not found on PATHstatus renders next to the SCA toggle withrole="status"and thetext-vetrix-dangercolor, 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 whileavailable: 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:
- Confirm the capability is advertised — see Capability discovery below. The
scarow should appear with the tool reported asgovulncheck+trivy(the composite presents both). - Trigger a scan from the Security tab on any repository, or via
POST /api/v1/repos/:owner/:repo/security/scanswith{"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
availablefield is alwaystruefor any registered scanner. The boot-time probe reports binary presence at startup; there is no per-request gate that flips the field tofalsefor 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:
PATHmismatch between the operator's interactive shell and the server process. Confirm withwhich govulncheckandwhich trivyfrom the same shell that launches the server (e.g. inside the systemd unit'sExecStartcontext, or viadocker execinto the running container).- Wrong binary names — the probe specifically invokes
govulncheck -versionandtrivy --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.missingWARN. 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/capabilitiesand inspect the row'savailablefield directly. - Check the boot log for
scanner.tool.missingon the corresponding backend — that is the most likely upstream cause of anavailable: falserow. - File a bug if the dropdown shows the disabled tooltip but
/capabilitiesreportsavailable: true; the FE state has drifted from the BE response.