OSPulse

Policies & gates

A policy is your risk appetite written down in a form a machine can check. Without one, every dependency decision is re-argued from first principles by whoever happens to be reviewing the pull request. With one, the argument happens once, in the open, and the answer is applied consistently thereafter.

Why codify risk appetite at all

Every organisation already has dependency policy. It is usually held informally: a senior engineer who "doesn't like AGPL", a tribal rule that you never take a package with fewer than two maintainers, a vague sense that things more than two majors behind are a problem.

Informal policy has three failure modes. It is inconsistent — the rule applies when the person who holds it happens to review. It is unauditable — you cannot show a regulator or a customer what your standard was six months ago. And it is unenforceable at scale — it does not survive a thousand transitive dependencies.

A written policy converts a judgement call into a repeatable check. That does not make the judgement correct; it makes it visible, versioned, and arguable. Most of the value comes from the argument the policy forces you to have before it ever blocks a build.

What a policy can assert

Policies evaluate against the enriched facts OSPulse holds for every package and repository. The useful assertions fall into a handful of families.

Minimum health scoreassertionOptional

A floor on the composite health score, optionally per ecosystem or per business-criticality band. The blunt instrument, and the one most likely to need an exception process behind it.

Licence allow / deny listsassertionOptional

Which licences are permitted, which are forbidden, and which require review. The most common first policy anyone writes, because the answer is usually already known and just needs recording. Copyleft handling normally differs between what you ship and what you build with.

Maximum vulnerability severityassertionOptional

The severity band above which a dependency may not be introduced. Pair it with a tolerance for existing findings — a policy that forbids all known highs on a legacy monolith will simply be disabled on day two.

Package age and abandonmentassertionOptional

Rules against packages whose last release is older than a threshold, or whose maintenance signals have gone flat. This is where policy catches the risks that never produce a CVE: the unmaintained transitive dependency nobody has looked at since 2019.

Maintainer thresholdsassertionOptional

A floor on maintainer count or bus factor. Single-maintainer packages are not automatically bad — a great deal of critical infrastructure is single-maintainer — but they are a different risk shape, and a policy makes you decide deliberately rather than by accident.

Ecosystem restrictionsassertionOptional

Which of the supported ecosystems — npm, NuGet, PyPI, Maven, Go, Cargo, RubyGems, Composer, Docker — a given repository or team may pull from. Useful where a platform team has deliberately standardised, and useful for catching a new runtime sneaking in through a build container.

Assertions compose. A realistic production policy is rarely one rule; it is a small set of rules with different severities, some of which warn and one or two of which block.

Creating and inspecting policies

bash
curl https://api.ospulse.app/api/v1/policies \
  -H "Authorization: Bearer $OSPULSE_TOKEN"
json
{
  "items": [
    {
      "id": "0a91f2c4-7b3d-4c8e-9f21-6d5a0b7e1c33",
      "name": "Production licence policy",
      "severity": "High",
      "enabled": true
    }
  ],
  "totalEstimate": 4,
  "take": 20,
  "skip": 0
}

Create with POST /api/v1/policies, read one with GET /api/v1/policies/{policyId}, amend with PATCH /api/v1/policies/{policyId}, and remove with DELETE /api/v1/policies/{policyId}.

Policies are tenant-scoped

Like everything else in OSPulse, policies live inside a tenant and apply to the repositories in it. A token can only see and change the policies of the tenant it belongs to.

Warn versus block

Every policy runs in one of two enforcement modes, and choosing between them is the single most consequential decision you will make about it.

Warn records a violation, raises an alert, and lets the build through. It answers "how much of this do we have?" — which is the question you actually have on day one, when nobody knows whether the new rule matches five dependencies or five hundred.

Block fails the gate. It answers "stop this getting worse". Block is only safe once you know the existing volume, because a block rule applied to an unmeasured estate does not enforce a standard — it halts delivery until someone disables it, and a policy that gets disabled under pressure is worse than no policy, because it taught everyone that policies are negotiable.

The reliable rollout order

Write the rule → run it in warn mode → measure the violation count → fix or except the existing backlog → switch to block for new violations. Skipping the measurement step is how policy programmes die.

Test before you enforce

Because that measurement step matters so much, policies can be evaluated without being enforced at all. POST /api/v1/policies/{policyId}/test runs a policy against real data and returns what it would have done.

bash
curl -X POST https://api.ospulse.app/api/v1/policies/{policyId}/test \
  -H "Authorization: Bearer $OSPULSE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "repositoryId": "8c1b2e77-4b1a-4d5f-a2c9-9e3f70d51a44" }'

Use it for three things:

  1. Sizing — before enabling anything, learn how many violations exist today.
  2. Regression checking — after amending a rule, confirm the change does what you intended and nothing else.
  3. Review evidence — attach the test output to whatever change process you use, so the person approving the policy can see its blast radius rather than guessing.

Violations

When a policy evaluates against a dependency and the assertion fails, the result is a violation — a durable record of which policy, which package, and which repository.

bash
curl https://api.ospulse.app/api/v1/policy-violations \
  -H "Authorization: Bearer $OSPULSE_TOKEN"

Fetch one with GET /api/v1/policy-violations/{violationId}. Move a violation through your workflow with a PATCH:

bash
curl -X PATCH https://api.ospulse.app/api/v1/policy-violations/{violationId} \
  -H "Authorization: Bearer $OSPULSE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "status": "InProgress" }'

Violations resolve themselves when the underlying condition goes away — the next scan finds the dependency upgraded, replaced or removed, and the violation closes. You do not need to close them by hand, and you should be suspicious of any workflow that requires it.

Exceptions, and why they must expire

Some violations are correct findings that you nevertheless accept. A GPL-licensed tool in your build pipeline that never ships. A package below your health floor that has no viable replacement. A high-severity advisory on a code path you have proven unreachable.

Grant an exception against the specific violation:

bash
curl -X POST https://api.ospulse.app/api/v1/policy-violations/{violationId}/exception \
  -H "Authorization: Bearer $OSPULSE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "reason": "Build-time only; not present in shipped artefact",
        "expiresAtUtc": "2026-10-31T00:00:00Z"
      }'

List every live exception with GET /api/v1/policy-exceptions and revoke one early with DELETE /api/v1/policy-exceptions/{exceptionId}.

The expiry is the important part. A permanent suppression is a decision made once, under one set of circumstances, that then silently outlives every one of them — the replacement package that finally shipped, the reachability analysis that stopped being true, the person who made the call and left. Nobody ever reviews a permanent suppression, because nothing ever asks them to.

A time-boxed exception inverts that. It expires, the violation reappears, and someone has to look again. If the reasoning still holds, renewing costs a minute. If it no longer holds, you have just caught a risk that permanent suppression would have hidden indefinitely.

Treat the exception list as a risk register

GET /api/v1/policy-exceptions is the honest inventory of everything you have knowingly accepted. If that list is long, growing, or full of expiries nobody remembers granting, your policy is stricter than your organisation's actual appetite — and the right fix is to change the policy, not to keep issuing exceptions against it.

How policies power CI gates

A gate is a policy evaluated at a moment where you can still say no.

In continuous integration, the OSPulse CLI evaluates the policies applicable to the repository against the dependency set of the branch under test, and exits non-zero when a blocking policy is violated. That exit code is the whole mechanism — your CI system already knows what to do with it.

The gate is deliberately the same evaluation as the background scan, not a parallel implementation. This matters more than it sounds: if the gate and the dashboard could disagree, engineers would learn to trust neither. One evaluation, three surfaces:

  • In CI, as a pass/fail gate on the change — see CI/CD integration.
  • In the platform, as the violation list you triage on a cadence.
  • In the editor, through POST /api/v1/ide/verdicts:batch, which returns verdicts for a batch of dependencies so tooling can surface a policy answer before the dependency is even committed.

The earlier the gate fires, the cheaper the answer. A blocked merge costs an hour; a package discovered to be non-compliant after it has spread through four services costs a quarter.

Gate new risk, not accumulated risk

The most sustainable gate configuration blocks on newly introduced violations and warns on pre-existing ones. Your backlog gets a deadline instead of a roadblock, and the line still holds.

Next steps