OSPulse

Enforce policy gates

This guide takes a policy from idea to enforced build gate: author it, test it against your real dependency estate, roll it out in warn mode, then enforce it in CI — without blocking every engineer on day one.

Before you start

You need an API token and at least one imported repository that has completed a scan. If you want the concepts first, read policies and gates.

The rollout that works

The failure mode with policy enforcement is turning on a strict rule against a codebase that has never been measured, discovering four hundred violations, and having the gate disabled by lunchtime. Do it in this order instead:

Measure before you decide

Look at what a candidate rule would actually catch before you write it. Test the policy against live data (step 3) and read the violation count. A rule that fires on 5 packages is enforceable this week; one that fires on 400 is a roadmap item.

Create it disabled or in warn mode

Land the policy so it records violations without failing anything. You get the reporting immediately and the pressure later.

Burn down the backlog

Fix or grant time-boxed exceptions for existing violations. The count should approach zero.

Switch to blocking

Only once the backlog is clear. From then on the gate only ever fires on new risk, which is the thing the team can actually act on.

Create a policy

Policies are created with POST /api/v1/policies. The rule itself is carried in conditionsJson, what happens when it matches in actionsJson, and what it covers in appliesToJson. Scope is set by the workspaceId, teamId, and repositoryId fields — leave them null to apply tenant-wide.

bash
curl -X POST https://api.ospulse.app/api/v1/policies \
  -H "Authorization: Bearer $OSPULSE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "Block critical vulnerabilities",
        "description": "No new dependencies with critical advisories.",
        "policyType": "vulnerability",
        "severity": "critical",
        "conditionsJson": "{\"maxSeverity\":\"critical\"}",
        "actionsJson": "{\"mode\":\"warn\"}",
        "appliesToJson": "{\"ecosystems\":[\"Npm\",\"PyPI\"]}",
        "workspaceId": null,
        "teamId": null,
        "repositoryId": null,
        "enabled": true
      }'
json
{
  "policyId": "b41f8c2e-91a7-4d63-8f2a-0c7e5d914b33",
  "name": "Block critical vulnerabilities",
  "policyType": "vulnerability",
  "scope": "tenant",
  "severity": "critical",
  "enabled": true,
  "createdAtUtc": "2026-07-28T09:14:22Z",
  "updatedAtUtc": "2026-07-28T09:14:22Z"
}

Condition and action shapes vary by policy type

conditionsJson, actionsJson, and appliesToJson are JSON strings, and the schema inside each depends on policyType. The example above is illustrative — check the interactive reference under Policies for the exact shape your policy type expects, and use the test endpoint to confirm it behaves as you intended before relying on it.

What to gate on

Start with rules that are unambiguous and cheap to comply with:

RuleWhy it is a good first gate
No new critical advisoriesNobody argues with it, and fixes usually exist
No packages flagged as compromisedZero tolerance is the correct posture
Banned licences (AGPL, SSPL, unlicensed)Binary, legally motivated, easy to check
No deprecated packagesThe author has explicitly told you to stop

Leave subjective thresholds — minimum health score, maximum maintainer concentration — until the team trusts the gate. Those generate debate, and debate during a release is what gets gates switched off.

Test before you enforce

POST /api/v1/policies/{policyId}/test evaluates a policy against real data without recording violations or blocking anything. This is the single most useful endpoint in the whole workflow — always run it before enabling enforcement.

bash
curl -X POST "https://api.ospulse.app/api/v1/policies/$POLICY_ID/test" \
  -H "Authorization: Bearer $OSPULSE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'

Read the result as a blast-radius estimate. If it matches far more than you expected, the condition is broader than you intended — narrow appliesToJson, or raise the threshold, and test again. Iterate here, where it is free.

Review violations

Violations are listed with GET /api/v1/policy-violations and paginate with take/skip like every other collection:

bash
curl "https://api.ospulse.app/api/v1/policy-violations?take=50" \
  -H "Authorization: Bearer $OSPULSE_TOKEN"
json
{
  "items": [
    {
      "violationId": "7c2a9e13-4b8f-4a02-9d5e-1f6b3c8a0d27",
      "policyId": "b41f8c2e-91a7-4d63-8f2a-0c7e5d914b33",
      "repositoryId": "3e91b7d4-2c6a-4f85-b1d0-9a4e2f7c5b18",
      "severity": "critical",
      "status": "open",
      "detectedAtUtc": "2026-07-28T09:20:11Z"
    }
  ],
  "totalEstimate": 12
}

Fetching a single violation with GET /api/v1/policy-violations/{violationId} returns the detail you need to act — including evidenceJson, a recommendedAction, and createdTicketUrl if one has been raised. Update triage state with PATCH /api/v1/policy-violations/{violationId}.

Grant a time-boxed exception

Sometimes a violation cannot be fixed now. Grant an exception rather than weakening the policy for everyone:

bash
curl -X POST "https://api.ospulse.app/api/v1/policy-violations/$VIOLATION_ID/exception" \
  -H "Authorization: Bearer $OSPULSE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "reason": "Upstream fix merged, awaiting release. Tracked in SEC-4192.",
        "expiresAtUtc": "2026-09-30T00:00:00Z"
      }'

Always set an expiry

An exception record carries expiresAtUtc, approvedByUserId, a reason, and a review reminder. Use all of them. A permanent suppression is indistinguishable from having no policy at all — and six months later nobody remembers why it was granted. A dated exception with a written justification is an auditable decision; a silent suppression is a hole.

Exceptions can be revoked, and expiry brings the violation back automatically — which is the point. The risk did not disappear, you deferred it.

Wire it into CI

Once the backlog is clear, switch the policy to blocking by updating actionsJson via PATCH /api/v1/policies/{policyId}, then fail the build on open violations.

The simplest gate queries violations for the repository after a scan completes and exits non-zero if any remain:

bash
#!/usr/bin/env bash
set -euo pipefail
 
: "${OSPULSE_TOKEN:?OSPULSE_TOKEN is required}"
: "${REPOSITORY_ID:?REPOSITORY_ID is required}"
 
API="https://api.ospulse.app/api/v1"
AUTH=(-H "Authorization: Bearer $OSPULSE_TOKEN")
 
# Only count violations that are still open for this repository.
violations=$(curl -sf "${API}/policy-violations?take=100" "${AUTH[@]}")
 
open_count=$(echo "$violations" | jq --arg repo "$REPOSITORY_ID" \
  '[.items[] | select(.repositoryId == $repo and .status == "open")] | length')
 
if [ "$open_count" -gt 0 ]; then
  echo "✗ ${open_count} open policy violation(s) — failing the build."
  echo "$violations" | jq --arg repo "$REPOSITORY_ID" -r \
    '.items[] | select(.repositoryId == $repo and .status == "open")
     | "  [\(.severity)] violation \(.violationId) detected \(.detectedAtUtc)"'
  exit 1
fi
 
echo "✓ No open policy violations."

Wire that into your pipeline after the scan step:

yaml
- name: Enforce OSPulse policy
  env:
    OSPULSE_TOKEN: ${{ secrets.OSPULSE_TOKEN }}
    REPOSITORY_ID: ${{ vars.OSPULSE_REPOSITORY_ID }}
  run: ./scripts/ospulse-gate.sh

See scan in CI/CD for triggering the scan itself and waiting for it to finish before this gate runs.

Common pitfalls

  • Enforcing before measuring. Always run the test endpoint first. See above.
  • Gating on the whole estate from one pipeline. Filter violations to the repository being built, or every team's pipeline fails on every other team's debt.
  • No exception path. If the only way past a gate is to disable it, engineers will disable it. Make the documented exception route faster than the workaround.
  • Treating a 401 as a policy failure. Distinguish "the gate says no" from "the gate could not run". A token problem should be a loud infrastructure error, not a silent pass — and definitely not a build failure blamed on dependencies.
  • Ignoring pagination. The example above takes the first 100 violations. If you expect more, page through them properly — see pagination.

Next steps