---
title: Security
description: Automate scan policies, CVE suppressions, and vulnerability scans from CI. Includes the deploy 409 response shape and admin bypass flow.
---

The Security API lets you manage scan policies, CVE suppressions, and trigger vulnerability scans from CI pipelines and automation scripts. Every endpoint in this reference is intended for external automation; internal frontend-only endpoints (finding listings, SARIF downloads) are not documented here.

All endpoints require [Bearer token authentication](/api-reference/overview#authentication). Every endpoint in this reference is available on every tier, including scan policies with `block_on_deploy` enforcement. See the per-endpoint **Role** row for the admin requirement on writes.

## Scan policies

Scan policies define risk conditions that Sencho evaluates on every post-deploy and scheduled scan: a known-exploited CVE (CISA KEV), a fixable Critical/High finding, and an optional severity threshold. When a policy has `block_on_deploy=1`, a deploy whose images match any enabled condition is rejected at the pre-flight stage with an HTTP 409 response. See [Deploy Enforcement](/features/deploy-enforcement) for the full flow.

Writes are admin-only and rejected on replica nodes (policies are managed on the control instance and replicate fleet-wide).

### List policies

**`GET /api/security/policies`**

**License:** Community

```bash
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  https://your-sencho-instance:1852/api/security/policies
```

**Response:**

```json
[
  {
    "id": 1,
    "name": "prod-high-gate",
    "node_id": null,
    "node_identity": null,
    "stack_pattern": "prod-*",
    "max_severity": "HIGH",
    "block_on_deploy": 1,
    "enabled": 1,
    "block_on_severity": 1,
    "block_on_kev": 0,
    "block_on_fixable": 0,
    "replicated_from_control": 0,
    "created_at": 1745107200000,
    "updated_at": 1745107200000
  }
]
```

### Create policy

**`POST /api/security/policies`**

**License:** Community · **Role:** Admin

| Field | Type | Required | Description |
|-------|------|:--------:|-------------|
| `name` | string | yes | Human-readable name shown in the UI. |
| `stack_pattern` | string or `null` | no | Glob against stack names (e.g. `prod-*`). `null` matches every stack. |
| `max_severity` | string | yes | One of `CRITICAL`, `HIGH`, `MEDIUM`, `LOW`. The threshold used when `block_on_severity` is on. Always stored for context. |
| `block_on_deploy` | `0` or `1` | yes | When `1`, pre-flight matches reject deploys with HTTP 409. When `0`, post-deploy and scheduled scans dispatch warning alerts instead. |
| `enabled` | `0` or `1` | no | Defaults to `1`. Disabled policies are never evaluated. |
| `block_on_severity` | `0` or `1` | no | Block when an image's highest non-suppressed finding meets or exceeds `max_severity`. Defaults to `0`. |
| `block_on_kev` | `0` or `1` | no | Block when an image carries a CVE on the CISA known-exploited (KEV) list. Defaults to `1`. |
| `block_on_fixable` | `0` or `1` | no | Block when an image has a Critical/High finding with a fix available. Defaults to `1`. |
| `node_id` | number or `null` | no | Scope the policy to one node. `null` applies the policy fleet-wide. |

A blocking policy (`block_on_deploy=1`) must enable at least one of `block_on_severity`, `block_on_kev`, or `block_on_fixable`. When the three input fields are omitted, they default risk-first (`block_on_kev=1`, `block_on_fixable=1`, `block_on_severity=0`).

```bash
curl -X POST https://your-sencho-instance:1852/api/security/policies \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "prod-high-gate",
    "stack_pattern": "prod-*",
    "max_severity": "HIGH",
    "block_on_deploy": 1,
    "enabled": 1
  }'
```

**Response:** `201 Created` with the full policy object.

**Errors:**

- `400` `max_severity must be CRITICAL, HIGH, MEDIUM, or LOW`
- `400` `Policy name is required`
- `400` `A blocking policy must enable at least one of: severity threshold, KEV, or fixable.`
- `403` when called on a replica node

### Update policy

**`PUT /api/security/policies/{id}`**

**License:** Community · **Role:** Admin

Any of the create fields can be updated individually. Omitted fields are left unchanged.

```bash
curl -X PUT https://your-sencho-instance:1852/api/security/policies/1 \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "block_on_deploy": 0 }'
```

**Response:** `200 OK` with the updated policy.

**Errors:** `400 Invalid policy id`, `404 Policy not found`, `403` on replicas.

### Delete policy

**`DELETE /api/security/policies/{id}`**

**License:** Community · **Role:** Admin

```bash
curl -X DELETE https://your-sencho-instance:1852/api/security/policies/1 \
  -H "Authorization: Bearer YOUR_API_TOKEN"
```

**Response:** `200 OK` `{"success": true}`. The call is idempotent; deleting a non-existent id returns the same shape.

## CVE suppressions

Suppressions let you mark individual CVEs as acknowledged so scan reads, comparisons, and exports downgrade them to a dimmed state. See [CVE Suppressions](/features/cve-suppressions) for the full flow.

### List suppressions

**`GET /api/security/suppressions`**

**License:** Community

Response rows include an `active` boolean computed from the `expires_at` timestamp.

```json
[
  {
    "id": 3,
    "cve_id": "CVE-2024-12345",
    "pkg_name": "openssl",
    "image_pattern": "alpine:*",
    "reason": "Awaiting upstream patch. Reviewed by security on 2026-04-10.",
    "created_by": "ops@example.com",
    "created_at": 1744000000000,
    "expires_at": 1746592000000,
    "replicated_from_control": 0,
    "active": true
  }
]
```

### Create suppression

**`POST /api/security/suppressions`**

**License:** Community · **Role:** Admin

| Field | Type | Required | Description |
|-------|------|:--------:|-------------|
| `cve_id` | string | yes | `CVE-YYYY-NNNN` or `GHSA-xxxx-xxxx-xxxx` format. |
| `pkg_name` | string or `null` | no | Package name to narrow the scope (e.g. `openssl`). `null` suppresses every package match. |
| `image_pattern` | string or `null` | no | Glob against image references. `null` applies fleet-wide. |
| `reason` | string | yes | Required justification, up to 2000 characters. Surfaced on every dimmed row. |
| `expires_at` | number or `null` | no | Unix timestamp in milliseconds. `null` for an indefinite suppression. |
| `status` | string | no | Triage decision. Defaults to `accepted`. One of `accepted`, `not_affected`, `false_positive`, `affected`, `needs_review`, `fixed`, `ignored`. |
| `justification` | string or `null` | no | OpenVEX justification code, prompted for by the Sencho UI when `status` is `not_affected` or `false_positive`. One of `vulnerable_code_not_present`, `vulnerable_code_not_in_execute_path`, `component_not_present`, `inline_mitigations_already_exist`. |

```bash
curl -X POST https://your-sencho-instance:1852/api/security/suppressions \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "cve_id": "CVE-2024-12345",
    "pkg_name": "openssl",
    "image_pattern": "alpine:*",
    "reason": "Awaiting upstream patch.",
    "expires_at": 1746592000000
  }'
```

**Response:** `201 Created` with the full suppression.

**Errors:** `400` on invalid shape (missing `reason`, malformed `cve_id`, over-length fields), `409` when a suppression already exists for the same `(cve_id, pkg_name, image_pattern)` triple, `403` on replicas.

### Delete suppression

**`DELETE /api/security/suppressions/{id}`**

**License:** Community · **Role:** Admin

```bash
curl -X DELETE https://your-sencho-instance:1852/api/security/suppressions/3 \
  -H "Authorization: Bearer YOUR_API_TOKEN"
```

## Vulnerability scans

### Trigger a scan

**`POST /api/security/scan`**

**License:** Community · **Role:** Admin

Accepts an image reference and starts an asynchronous scan. The response returns immediately with a `scanId` that you can poll.

| Field | Type | Required | Description |
|-------|------|:--------:|-------------|
| `imageRef` | string | yes | Image reference Trivy will scan (must match Sencho's validator; `/`, `:`, `@`, alphanumerics, `-`, `_`, `.`). |
| `stackName` | string | no | Associates the scan with a stack for display purposes. |
| `force` | boolean | no | Default `false`. When `true`, ignores the 24-hour digest cache and runs Trivy again. |
| `scanners` | `["vuln"]` or `["vuln","secret"]` | no | Omit for vuln-only. Pass `["vuln","secret"]` to include secret detection. |

```bash
curl -X POST https://your-sencho-instance:1852/api/security/scan \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "imageRef": "nginx:1.27" }'
```

**Response:** `202 Accepted` `{ "scanId": 142 }`. The scan continues in the background.

**Errors:** `400 imageRef is required`, `400 Invalid imageRef format`, `409 Already scanning this image`, `503 Trivy is not available on this host`.

### List scans

**`GET /api/security/scans`**

**License:** Community

Supports filters: `imageRef`, `imageRefLike` (reference-only substring), `imageDigest` (exact), `imageIdentityLike` (substring match on reference **or** digest), `status`, `limit`, `offset`.

Response is paginated with `items`, `total`, `perImageLimit`, and `cappedIdentities` (digest-or-ref buckets at the retention cap). Exact `imageRef` or `imageDigest` deep-dives bypass the per-identity cap.

```bash
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  "https://your-sencho-instance:1852/api/security/scans?imageIdentityLike=nginx&limit=20"
```

Legacy clients may still use `imageRefLike` for reference-only search:

```bash
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  "https://your-sencho-instance:1852/api/security/scans?imageRefLike=nginx&limit=20"
```

## Stack deploy with policy

Every stack deploy endpoint evaluates the scan policy gate before `docker compose up` runs. When a policy blocks the deploy, the endpoint returns HTTP 409.

**Affected endpoints:** `POST /api/stacks/{stackName}/deploy`, `POST /api/stacks/{stackName}/update`, `POST /api/stacks/{stackName}/recreate`, `POST /api/stacks/from-git`, `POST /api/stacks/git-source/{sourceId}/apply`, `POST /api/stacks/deploy-template`.

### Blocked-deploy response

HTTP 409 Conflict. The response body is parseable JSON so CI pipelines can branch on it:

```json
{
  "error": "Policy \"prod-high-gate\" blocked deploy: 2 image(s) exceed HIGH",
  "policy": {
    "id": 3,
    "name": "prod-high-gate",
    "maxSeverity": "HIGH"
  },
  "violations": [
    {
      "imageRef": "myapp:v1",
      "severity": "CRITICAL",
      "criticalCount": 2,
      "highCount": 5,
      "scanId": 142
    },
    {
      "imageRef": "nginx:1.14",
      "severity": "HIGH",
      "criticalCount": 0,
      "highCount": 3,
      "scanId": 143
    }
  ]
}
```

### Bypassing a block

Admins can bypass the gate on a per-deploy basis by passing `?ignorePolicy=true` on the deploy URL. The flag is only honored when the calling session resolves to an admin user; API tokens with read-only or deploy-only scope cannot bypass a policy.

```bash
curl -X POST "https://your-sencho-instance:1852/api/stacks/myapp/deploy?ignorePolicy=true" \
  -H "Authorization: Bearer ADMIN_API_TOKEN"
```

Every bypass is recorded in the [Audit Log](/features/audit-log) with the originating path and method, the actor's username, the policy name, and the list of bypassed images.

**Response when bypass is honored:** `200 OK` with the normal deploy success payload and `bypassedPolicy: true`.

**Response when bypass is denied:** the original `409 Conflict` payload is returned (the caller was not admin, or the token scope disallowed bypass).

### Fail-open when Trivy is missing

If Trivy is not installed on the target node, the gate fails open: the deploy proceeds with an HTTP 200 response and Sencho dispatches a warning notification `Pre-deploy scan for "<stack>" skipped: Trivy not installed on this node`. This keeps teams from being locked out when the scanner binary is unavailable.

Install Trivy on the affected node to enforce the policy; see [Installing Trivy](/operations/trivy-setup).
