Skip to content

Azure DevOps Sensor ​

What it ingests ​

Four object kinds from the Azure DevOps REST API:

Azure DevOps objectenabled_objects nameOntology entitySignal
Git repositories (Azure Repos)repositoriesRepository, BELONGS_TO an Organizationrepository
Pull requests, with reviewer votespull_requestsPullRequestpull_request
Completed pipeline builds (Azure Pipelines)buildsBuildbuild
Work items (Azure Boards): bugs, user stories, tasks, features, epics, and custom typeswork_itemsIssuework_item

How the graph is wired:

  • Names follow Azure DevOps' mention syntax. A work item is contoso#1234 and a pull request is contoso/Platform/api!42, where contoso is the organization (or, on Azure DevOps Server, the collection). A repository is contoso/Platform/api. Titles, states, and paths are properties.
  • The organization is an Organization. Each Repository BELONGS_TO it, with the project as a property. If the same org also runs GitHub under the same name, both resolve to one Organization, while their repositories stay distinct.
  • Pull requests. A PullRequest BELONGS_TO its Repository. Its creator gets OPENED. Whoever completed it gets MERGED, or CLOSED if they abandoned it. Each reviewer who voted gets APPROVED for Approved or Approved with suggestions, or REVIEWED for Wait for author or Rejected. The vote is a property on the edge. Group and team reviewers aren't people, so they get no edge.
  • Builds. A Build is AUTHORED by the person it ran for. When the source is an Azure Repos repository, the build BELONGS_TO that Repository. A pull-request validation build is also RELATED_TO its PullRequest. A build of a GitHub or Bitbucket repository keeps the repository name and commit as properties instead.
  • Work items. The assignee gets ASSIGNED_TO the Issue and the creator gets OPENED. A child item is PART_OF its parent, and every pull request linked from the item's Development section REFERENCES it. Type, state, reason, area and iteration paths, tags, priority, and severity are properties.
  • People resolve to Person by email and display name, the same identity contract as GitHub, Jira, and Slack. On Azure DevOps Services backed by Microsoft Entra ID, a user's unique name is their email address, so an assignee lands on the same Person as their Teams and Outlook activity. On Azure DevOps Server, unique names are often DOMAIN\user. Those identities resolve by display name only.

Each object kind is its own signal, so you can disable one stream from the source's bindings without disabling the source.

Read-only and poll-based.

Credential ​

References an azure_devops_credentials credential by id. The secret is a JSON object holding exactly one of two forms:

Personal access token (PAT) — works with Azure DevOps Services and Azure DevOps Server:

bash
curl -sS -X POST https://your-corveil-host/api/credentials \
  -H "Authorization: Bearer <admin>" -H "Content-Type: application/json" \
  -d '{"type":"azure_devops_credentials","name":"contoso-ado","secret":"{\"pat\":\"<personal access token>\"}"}'

Microsoft Entra ID service principal — Azure DevOps Services only:

bash
curl -sS -X POST https://your-corveil-host/api/credentials \
  -H "Authorization: Bearer <admin>" -H "Content-Type: application/json" \
  -d '{"type":"azure_devops_credentials","name":"contoso-ado-sp","secret":"{\"tenant_id\":\"<directory (tenant) id>\",\"client_id\":\"<application (client) id>\",\"client_secret\":\"<client secret value>\"}"}'

With a service principal, Corveil mints a token from https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token for the Azure DevOps resource (499b84ac-1321-427f-aa17-267ca6975798/.default). It reuses the token until it expires and mints a new one if Azure DevOps rejects it early. Moving from a PAT to a service principal is a credential rotation, and the sensor doesn't change.

The vault rejects the common paste mistakes when you save: a bare token, both forms at once, a partial service principal, a user:token pair or Basic … header in pat, and a client_id that isn't the application's GUID.

Config fields ​

KeyRequiredNotes
organization_urlyesAzure DevOps Services: https://dev.azure.com/contoso or https://contoso.visualstudio.com (both spellings name the same organization, and switching between them doesn't duplicate entities). Azure DevOps Server: the project collection URL, such as https://tfs.agency.gov/tfs/DefaultCollection. Must be https://. A pasted page link (…/_git/api, …/_workitems) is trimmed at the first _ route segment.
projectsnoComma-separated project names. Blank reads every project the credential can see. Test connection checks that each name exists.
enabled_objectsnoComma-separated object kinds. Blank polls every supported kind. Unknown names are rejected at configure time.
api_versionnoREST API version. Blank uses 6.0, which Azure DevOps Services and Azure DevOps Server 2020 or later accept. Set 5.1 for Azure DevOps Server 2019.

Supported object names: repositories, pull_requests, builds, work_items (also accepted: repos, prs, pipelines, workitems).

There is no default_entity_type. This source emits several primary types, so a single override would mistype the rest.

Any host is allowed in organization_url, because Azure DevOps Server lives on your own domain. Corveil's sensor network guard still applies. On Corveil Cloud, a URL that resolves to a private or loopback address is refused when the sensor connects, so an on-premises server must be reachable from the internet. A self-hosted Corveil deployment that reaches the server over a private network sets CORVEIL_SENSOR_ALLOW_PRIVATE_NETWORKS=true.

Provider-side setup ​

Option A: personal access token ​

Create the token as a dedicated account rather than a person, so it survives staff changes. On Azure DevOps Server, a local or domain service account works.

  1. Add the account to the organization (Services) or collection (Server) with Basic access. Add it to the Readers group of every project you want ingested. Corveil never writes.

  2. Sign in as that account and open User settings → Personal access tokens → New Token.

  3. Set Organization to the one you're connecting (Services), and pick an expiry your policy allows. Azure DevOps Services caps PATs at one year.

  4. Choose Custom defined scopes and grant only read access:

    ScopeNeeded for
    Project and Team: ReadListing projects (always)
    Work Items: Readwork_items
    Code: Readrepositories, pull_requests, and resolving work item pull request links
    Build: Readbuilds
  5. Copy the token and store it as {"pat":"…"}. Rotate the credential before the token expires. An expired token fails as an authentication error, and the sensor backs off until the credential is fixed.

If your organization blocks PAT creation or restricts PAT lifetime by policy, use a service principal.

Option B: Microsoft Entra ID service principal (Services only) ​

The organization must be connected to your Entra ID tenant (Organization settings → Microsoft Entra).

  1. In the Entra admin center, go to App registrations → New registration. Name it Corveil Azure DevOps and register it. No redirect URI or API permissions are needed. Azure DevOps grants access through its own groups, not through Entra consent.
  2. Under Certificates & secrets, add a client secret. Copy its Value, plus the app's Application (client) ID and Directory (tenant) ID.
  3. In Azure DevOps, go to Organization settings → Users → Add users. Search for the app registration by name and add it with Basic access.
  4. Add it to the Readers group of every project you want ingested.
  5. Store {"tenant_id","client_id","client_secret"} as the credential. Rotate the secret before it expires.

Azure DevOps Server can't accept Entra tokens. Pairing a service principal with a Server URL fails when the sensor is created, with an error saying so.

Rate limits ​

Azure DevOps throttles each identity on a sliding budget of throughput units, not a fixed request count. The sensor paces itself to at most 10 requests per second and backs off on a 429 or 503. A dedicated identity keeps its usage from counting against a person's budget.

Wire-up ​

POST /api/sensors

json
{
  "sensor_type": "azure_devops",
  "name": "Contoso Azure DevOps",
  "config": {
    "organization_url": "https://dev.azure.com/contoso",
    "projects": "Platform, Mobile",
    "enabled_objects": "work_items, repositories, pull_requests, builds"
  },
  "credentials": { "credentials": "<azure_devops_credentials credential id>" }
}

For Azure DevOps Server:

json
{
  "sensor_type": "azure_devops",
  "name": "Agency TFS",
  "config": {
    "organization_url": "https://tfs.agency.gov/tfs/DefaultCollection",
    "api_version": "6.0"
  },
  "credentials": { "credentials": "<azure_devops_credentials credential id>" }
}

Verify ​

Run Test connection. It lists projects, checks that every configured project exists, and reads from each enabled object kind in the first project. A missing PAT scope is reported against the kind it blocks (cannot read builds in project "Platform" (needs Build (Read))) at save time, not on the first poll. A 401 or 403, or Azure DevOps answering with its sign-in page, is reported as an authentication failure.

Once polling, each kind's count appears in the per-poll summary log (work_items_collected, pull_requests_collected, builds_collected, repositories_collected). A kind that stays at zero while the others move usually means the identity can't read it.

In the ontology, search for a work item (contoso#1234). It should be an Issue with ASSIGNED_TO and OPENED edges to the right people, and a REFERENCES edge from any pull request linked to it.

How incremental sync works ​

  • The cadence is the worker's poll interval. Each poll walks every enabled kind forward from where the last one stopped.
  • The first poll records a starting point. For work items, pull requests, and builds, it emits nothing. History comes from the worker's one-time Backfill (SENSOR_BACKFILL_SINCE, default 72h), so connecting a large organization doesn't send its whole history on the first poll. A project or kind you add later starts the same way. Repositories are current structure rather than history, so the first poll lists them.
  • Work items use a (ChangedDate, Id) keyset over WIQL with time precision: ChangedDate > T, or = T with a higher id, ordered by both. A bulk edit that stamps many items with one instant is read across pages without skips or repeats, and no offsets are used. Each page of ids is then fetched in one batch as of the instant the query ran, so an item edited between the two calls is read as the query saw it and picked up again on a later page. Up to 200 items per page and 10 pages per poll.
  • Pull requests have no "changed since" filter, so each project keeps two things: the highest pull request id seen, and a fingerprint of every active pull request. The fingerprint covers status, title, description, branches, head commit, and reviewer votes. Each poll reads the active list and emits those whose fingerprint changed. It reads the newest pull requests until it reaches the high-water id, which catches one opened and completed between polls. A pull request that left the active list is fetched once for its final state. Merge-status churn isn't part of the fingerprint, so a push to the target branch doesn't re-ingest every open pull request.
  • Builds use a finish-time watermark per project. Only completed builds are read, oldest first. Builds that finish at the same instant are tracked by id, so none is emitted twice.
  • Repositories are listed once every 6 hours. An unchanged repository is deduplicated.
  • Only items older than one minute are read, so an item that commits a moment late is still ahead of the cursor.
  • Edits arrive as new events and update the same entity. The event id carries a version (the work item revision, the pull request fingerprint), so a changed item is ingested again. The entity id (azuredevops:<organization>:<kind>:<id>) stays the same, so the existing node is updated rather than duplicated. The organization in the id keeps two organizations, or a Services org and a Server collection, from colliding in one Corveil org.
  • Rate limits. A 429 or 503 is retried up to three times, honoring Retry-After (capped at 30s), then 1s/2s/4s. If it still fails, the poll stops with its cursor where it was, and the next poll tries again.
  • Free text is scrubbed. Work item descriptions, repro steps, acceptance criteria, and pull request descriptions go through Corveil's secret redaction before they leave the sensor, so a pasted key or connection string never reaches the event store. Rich-text HTML is converted to plain text. Content is capped at 64 KiB per event.

Known limits ​

  • Discussion comments are not ingested. That covers work item discussion and pull request comment threads. Descriptions and reviewer votes are ingested.
  • Commits are not ingested as Commit entities. A completed pull request carries its merge commit, and a build its source commit, as properties.
  • Only completed builds. Queued and running builds appear once they finish. Release pipelines (classic Releases) and pipeline stage detail are not read.
  • Deletes aren't detected. A work item moved to the Recycle Bin, a deleted repository, or a deleted pull request leaves its entity in place.
  • Removed links aren't retracted. If a work item's parent changes or a pull request link is removed, the old edge stays.
  • Wikis, test plans, and Artifacts feeds aren't read in v1.
  • Very large projects. A poll reads at most 2,000 active pull requests per project. Past that, closed pull requests are detected on a later poll rather than the next one.
  • Polling only. Service hooks could push changes, but they need per-project subscriptions an administrator creates. Polling needs only read access.