OSPulse

Scans & the scan lifecycle

A scan is the unit of work that turns a repository into intelligence. It reads what you declared, resolves what you actually get, enriches that against registries and threat feeds, scores it, evaluates your policies, and raises whatever alerts fall out. Knowing what happens in each stage tells you why a result looks the way it does — and what a result is not.

What a scan actually does

The pipeline runs in a fixed order, because each stage depends on the output of the one before it.

Manifest and lockfile discovery

The scanner walks the repository looking for the declaration files of every supported ecosystempackage.json and its lockfiles, *.csproj and packages.lock.json, pyproject.toml, pom.xml, go.mod, Cargo.toml, Gemfile.lock, composer.lock, Dockerfile. A monorepo commonly yields several, in more than one ecosystem, and each is treated as its own root.

Discovery decides everything downstream. A dependency declared in a file OSPulse cannot find is a dependency that does not exist as far as the rest of the pipeline is concerned.

Dependency graph resolution

Manifests state intent; lockfiles state fact. The scanner resolves the full transitive graph — every package, every version, and the edges that explain why each one is present.

This is the step that makes the difference between a list and a graph. A list tells you a vulnerable package is present. A graph tells you that it arrived through a single direct dependency three levels up, which is the only fact that lets you plan a fix. Read the result with GET /api/v1/repositories/{repositoryId}/dependency-graph or the flat view at GET /api/v1/repositories/{repositoryId}/dependencies.

Where no lockfile exists, resolution falls back to range interpretation, and the result is necessarily less certain — the graph reflects what would resolve, not what your build last produced.

Registry enrichment

Each resolved package is matched against its upstream registry to collect the facts that manifests never contain: release history and cadence, maintainer set and changes to it, deprecation status, repository provenance, declared licence, download and adoption signals.

Enrichment is where a package identifier becomes a package record. It is also the slowest stage on a first scan and the fastest on subsequent ones, because most of the estate is already known.

Threat-feed correlation

Enriched packages are correlated against vulnerability advisories and threat intelligence — matching not just by name but by version range, so a fixed version is not reported as vulnerable and an unfixed one is not missed. Correlation produces exposures: the intersection of a global vulnerability and your specific dependency graph.

Scoring

With enrichment and correlation complete, each package receives a health score and the repository receives an aggregate risk picture, weighted by your tenant's scoring configuration. Read the roll-up with GET /api/v1/repositories/{repositoryId}/risk-summary.

Policy evaluation

Your policies are evaluated against the scored graph, producing violations. This is the same evaluation the CI gate performs, so the dashboard and the build agree by construction.

Alert generation

Finally, the scan's results are diffed against the previous state and the differences are matched against your alert rules. A new exposure raises an alert; an exposure that was already there and already acknowledged does not raise it again.

The ordering explains most surprises. If policy evaluation looks wrong, the cause is usually enrichment or resolution, not the policy. If a package is missing entirely, the cause is almost always discovery.

The asynchronous model

Scanning a large monorepo across several ecosystems is not a request-response operation. OSPulse therefore separates asking for a scan from getting its result.

Triggering returns immediately with 202 Accepted and an identifier:

bash
curl -X POST https://api.ospulse.app/api/v1/repositories/{repositoryId}/scan \
  -H "Authorization: Bearer $OSPULSE_TOKEN"
json
{
  "scanId": "c47b0a1e-9d33-4f21-9a8c-2b6e5f7d0142",
  "state": "Queued"
}

You then poll the scan record until it reaches a terminal state:

bash
curl https://api.ospulse.app/api/v1/scans/{scanId} \
  -H "Authorization: Bearer $OSPULSE_TOKEN"

Poll politely

Back off between polls rather than hammering the endpoint — a short initial interval widening to something like thirty seconds is plenty. Rate limits are reported in the RateLimit-* response headers; see Rate limits. If you would rather not poll at all, configure an outbound webhook integration and let OSPulse tell you when the scan finishes.

Scan states

A scan is a state machine. The exact enumeration is in the API reference, but the shape is:

QueuedstateOptional

Accepted and waiting for a worker. A repository that already has a scan in flight will normally not start a second one — the trigger endpoint can reject a duplicate request rather than queue redundant work.

RunningstateOptional

Working through the pipeline stages above. Progress is visible in the scan logs.

CompletedstateOptional

Terminal and successful. Results — dependencies, exposures, scores, violations — are published atomically, so you never read a half-updated repository.

FailedstateOptional

Terminal and unsuccessful. The previous successful scan's results remain in place; a failure never blanks your data. The logs carry the reason.

CancelledstateOptional

Terminal, stopped on request. As with failure, prior results are retained.

List scans, optionally narrowed to one repository or one state:

bash
curl "https://api.ospulse.app/api/v1/scans?repositoryId=8c1b2e77-4b1a-4d5f-a2c9-9e3f70d51a44&state=Completed&take=10" \
  -H "Authorization: Bearer $OSPULSE_TOKEN"

Cancel a scan that is no longer wanted — a superseded push, a misconfigured repository — with POST /api/v1/scans/{scanId}/cancel. Cancellation of a scan that has already reached a terminal state returns 409, which is information rather than an error: something else finished it first.

Reading scan logs

When a scan behaves unexpectedly, the logs are where the pipeline explains itself — which manifests were discovered, which registries responded, where resolution gave up.

bash
curl "https://api.ospulse.app/api/v1/scans/{scanId}/logs?take=100" \
  -H "Authorization: Bearer $OSPULSE_TOKEN"

The sinceUtc parameter returns only entries after a timestamp, which makes it practical to tail a running scan without re-fetching everything you already have.

Failures are usually access, not analysis

The most common causes of a failed scan are a source-control connection whose credentials have lapsed, a private registry OSPulse cannot reach, or a repository whose lockfiles are absent or unparseable. The logs name which.

Data freshness: two clocks, not one

This is the most important thing to understand about scan results, and the least obvious.

What you see for a repository is the product of two independently moving inputs:

  1. Your dependency graph, which is a snapshot — it is exactly what the last successful scan found and does not change until the next one runs.
  2. The intelligence layered on top of it, which is continuous — advisories, compromise signals, maintainer changes and health scores keep updating for packages already in your graph.

So a repository that has not been scanned since Monday can still acquire a new critical exposure on Thursday. Nothing in your code changed; the world's knowledge about a package you already had did. That is the intended behaviour, and it is why alerts can fire against repositories with no recent commits.

The corollary is the failure mode: if a repository stops being scanned, its graph goes stale silently. Newly added dependencies are invisible, and no amount of fresh threat intelligence will find a package OSPulse does not know you have. Scan coverage and scan recency are worth monitoring in their own right — see Continuous monitoring.

Incremental and full scans

Not every scan does the same amount of work.

An incremental scan is the normal case. The repository is already known, most of the dependency graph is unchanged, and most of the enrichment data is current. The scan resolves the graph, identifies what actually differs from the previous state, and does expensive upstream work only for packages that are new or stale. This is why a re-scan after a small change completes far faster than the first scan of the same repository.

A full scan re-derives everything from scratch, ignoring cached enrichment. It costs substantially more and is the right choice in a narrow set of circumstances: the first scan of a repository, after a large structural change such as a migration or a monorepo split, or when you have reason to believe cached state is wrong.

The distinction is an optimisation, not a difference in results. An incremental scan and a full scan of the same commit produce the same graph — the incremental one just avoids asking questions it already knows the answers to.

Webhook-triggered re-scans

Manual triggering does not scale, and scheduled scanning alone leaves a window between a merge and the moment anyone knows what it introduced. The right default is to scan on push.

OSPulse accepts inbound webhooks from all four supported providers:

text
POST /api/v1/webhooks/github
POST /api/v1/webhooks/gitlab
POST /api/v1/webhooks/azure-devops
POST /api/v1/webhooks/bitbucket

Each returns 202 Accepted — the webhook is acknowledged and the scan is queued, following the same asynchronous model as a manual trigger. Delivery is authenticated per provider, and an unverified payload is rejected with 401 rather than silently ignored. See Webhooks for signature verification and payload handling.

In practice most teams run all three triggers together: webhooks for responsiveness on change, a scheduled cadence to catch repositories that are quiet, and manual triggers for one-off investigation.

Push events are not the only source of new risk

Webhook-driven scanning covers changes you make. It does not cover changes made to packages you already depend on — those arrive through the continuously updated intelligence layer described above, independently of whether you commit anything.

Next steps