Documentation

Technical reference for how ADOScore works, what data it handles, and how to integrate with its API.

Overview

ADOScore connects to your Azure DevOps organization using a scoped Personal Access Token (PAT) you provide. It reads commit history, pull request records, and file change metadata from the ADO REST API, then runs a scoring pipeline to produce per-developer activity and security risk scores. Results are stored historically so you can track trends over time.

No source code is stored. ADOScore only reads file paths, commit metadata, and — in Deep Scan mode — fetches file content transiently to run pattern matching. File content is never persisted.

How It Works

Each scoring run executes the following pipeline stages in order. All ADO API calls are read-only.

1

List Repositories

Fetches all Git repositories in the selected ADO project.

GET {orgUrl}/{project}/_apis/git/repositories
2

Fetch Commits

Fetches commits within the configured look-back window (default 30 days). Paginated in batches of 200, capped at 5,000 commits per repo.

GET …/commits?searchCriteria.fromDate={since}
3

Fetch File Change Paths

For each commit, fetches the list of changed file paths. Used to detect sensitive file commits. No file content is fetched in this step.

GET …/commits/{commitId}/changes
4

Deep Scan (optional)

If Deep Scan is enabled, fetches text file content for each changed file and runs regex patterns against it to detect secrets. Binary files are skipped. Content is never stored.

GET …/items?path={filePath}&versionDescriptor.version={commitId}
5

Fetch Pull Requests

Fetches all completed pull requests within the look-back window. Used to attribute PR creation and review activity to developers.

GET …/pullrequests?searchCriteria.status=completed
6

Detect Direct Pushes

Fetches commits on each configured protected branch and cross-references them against PR merge commits. Commits that aren't associated with a closed PR are flagged as direct pushes.

GET …/commits?searchCriteria.itemVersion.version={branch}
7

Calculate Scores

Aggregates per-developer metrics in memory and computes Activity and Risk scores. Results are written to the database as a scored run snapshot.

— (in-process)

Data Collection

ADOScore reads the following fields from the Azure DevOps REST API. All calls are authenticated with the PAT you provide and are strictly read-only — ADOScore never writes to your ADO organization.

Commit metadata

FieldSourcePurpose
commitIdADO Commits APIUnique identifier for cross-referencing
author.emailADO Commits APIDeveloper identity (normalized to lowercase)
author.nameADO Commits APIDisplay name
author.dateADO Commits APITimestamp for recency and weekly bucketing
changeCounts.Add/Edit/DeleteADO Commits APINot stored — used transiently

File change paths

FieldSourcePurpose
item.pathADO Commit Changes APIChecked against sensitive file patterns
changeTypeADO Commit Changes APINot stored

Pull request metadata

FieldSourcePurpose
createdBy.uniqueNameADO Pull Requests APIPR author identity
creationDate / closedDateADO Pull Requests APIWindow filtering and direct push detection
reviewers[].uniqueName + voteADO Pull Requests APIPR review attribution (non-zero vote only)
sourceRefName / targetRefNameADO Pull Requests APIBranch diversity and protected branch checks

File content (Deep Scan only)

In Deep Scan mode, text file content is fetched transiently and scanned with regex patterns. The content is processed in memory and immediately discarded — it is never written to disk, logs, or the database. Binary file extensions are blocklisted and skipped entirely.

Data in Transit

All communication between ADOScore and Azure DevOps uses HTTPS. All communication between your browser and ADOScore uses HTTPS. The decrypted PAT is only present in server memory during an active scoring run and is never sent to the client.

ADOScore → Azure DevOps

  • • HTTPS GET requests to ADO REST API
  • • PAT sent as HTTP Basic auth header (in-flight only)
  • • Paginated reads, 200 items per page
  • • Hard cap of 5,000 commits per repo per run

Browser → ADOScore

  • • PAT submitted once at onboarding (HTTPS POST)
  • • Encrypted server-side before database write
  • • Never returned to the browser after submission
  • • Only the last 4 characters are shown as a hint

What does NOT leave your ADO organization

Commit message bodies, code diffs, file content (except transiently during Deep Scan), branch names beyond what is stored in PR metadata, user profile photos, and any ADO work items (tickets, boards) are never fetched or transmitted.

Data at Rest

ADOScore stores the minimum data required to display dashboards and historical trends. All data is stored in a PostgreSQL database hosted on Neon (serverless Postgres).

TableWhat is storedRetention
usersEmail, hashed password (bcrypt), plan, Stripe IDsUntil account deleted
orgsDisplay name, ADO org URL, encrypted PAT, IV, last-4 hintUntil org deleted
projectsADO project name, scoring config, scheduleUntil project deleted
runsStatus, timestamps, aggregate counts (devs, high-risk count)Indefinite
developer_scoresEmail (lowercase), display name, scores, risk factors (string array)Tied to run lifecycle

File content, commit diffs, and commit messages are never stored. Risk factors stored in developer_scores.risk_factors contain only structured strings such as Direct push to main in repo-name or Sensitive file: .env — never raw file content.

PAT Security

Encryption

Personal Access Tokens are encrypted with AES-256-GCM before being written to the database. A unique 16-byte random IV is generated per encryption operation. The authentication tag produced by GCM mode is stored alongside the ciphertext, providing integrity verification on decryption.

// Encryption model
algorithm:  AES-256-GCM
key:        32-byte random key (hex-encoded in environment variable)
iv:         16-byte random, generated per-PAT, stored in orgs.pat_iv
auth_tag:   appended to ciphertext as hex, verified on decrypt
plaintext:  never written to disk, logs, or database

PAT Lifecycle

SubmissionPAT is received over HTTPS, encrypted in memory, written to DB as ciphertext. The plaintext is immediately discarded.
At restOnly the ciphertext and IV are stored. The encryption key lives in Vercel environment variables, never in source code or the database.
During a runPAT is decrypted in-memory within the scoring function scope only. It is never passed to client components, logged, or included in API responses.
RotationRe-entering a PAT replaces the ciphertext and IV atomically after a connection test. The old ciphertext is deleted.
DisplayOnly the last 4 characters of the original PAT are stored (pat_hint) for display. There is no reveal feature.

Required PAT Scopes

ADOScore only requires read-only scopes. Grant the minimum permissions when creating your PAT in Azure DevOps.

ScopePermissionWhy
CodeReadList repos, commits, file paths, file content (Deep Scan)
Pull Request ThreadsReadList PRs and reviewer votes

Scoring Methodology

Activity Score (0–100, higher = more active)

SignalMax pointsFormula
Commit frequency30min(commits_30d, 20) / 20 × 30
Recency20max(0, 20 − max(0, days_since_last − 7))
PR participation25(min(prs_created, 15) + min(prs_reviewed, 10)) / 25 × 25
Consistency15max(0, 15 − (weekly_std_dev / 10 × 15))
Branch diversity10min(unique_branches, 5) / 5 × 10

Security Risk Score (0–100, higher = more risk)

SignalPoints eachCapRequires
Secret/credential in file content+35×3 maxDeep Scan enabled
Sensitive file committed+20×2 maxStandard scan
Branch policy bypass+25×2 maxStandard scan
Direct push to protected branch+20×2 maxStandard scan

Total is clamped to 100.

Risk Tiers

Low0–30
Medium31–60
High61–85
Critical86–100

Secret Detection Patterns

The following patterns are scanned during Deep Scan runs. Matching is performed against file content in memory — no content is stored regardless of whether a match is found.

Pattern nameExample match target
AWS access keyAKIA + 16 uppercase alphanumerics
Private key header-----BEGIN PRIVATE KEY-----
Azure SAS tokensig= followed by base64 encoded signature
GitHub tokenghp_, gho_, ghu_, ghs_, ghr_ prefixes
Generic passwordpassword=, passwd=, pwd= with 8+ char value
Generic API keyapi_key=, apikey= with 16+ char value
Connection stringServer=…;Password=… style MSSQL/ADO.NET strings
Bearer tokenAuthorization: Bearer with 20+ char token
SendGrid keySG. prefix with standard key format
Slack tokenxoxb-, xoxa-, xoxp-, xoxr-, xoxs- prefixes
Azure storage keyDefaultEndpointsProtocol=https;AccountName=

Sensitive File Patterns

These file path patterns trigger a risk finding regardless of file content. Deep Scan is not required.

PatternExamples
.env files.env, .env.local, .env.production
Private keysid_rsa, id_ed25519, id_ecdsa, id_dsa
Certificate files*.pem, *.key, *.pfx, *.p12, *.cer, *.jks
Secret storescredentials (no extension), kdbx
Secret config filessecrets.json, secrets.yaml, secrets.yml
Infrastructure secretsterraform.tfvars, kubeconfig
Package manager auth.npmrc, .pypirc

API Reference

All authenticated endpoints require a valid session cookie issued by the NextAuth session system. Requests without a valid session return 401 Unauthorized. Org-scoped endpoints additionally verify that the authenticated user owns the requested org.

Authentication

POST/api/auth/callback/credentialsAuth: None

Exchange email and password for a session cookie. Managed by NextAuth — do not call directly; use the login form.

Request Body

{ "email": "user@example.com", "password": "…" }

Response

Set-Cookie: authjs.session-token=…

Organizations

POST/api/orgsAuth: Session

Create a new ADO organization. Validates the PAT against ADO before saving. Encrypts the PAT and stores only the ciphertext. Enforces plan org limits.

Request Body

{
  "displayName": "Contoso Engineering",
  "orgUrl": "https://dev.azure.com/contoso",
  "pat": "…",
  "adoProject": "MyProject",
  "daysBack": 30,
  "deepScan": false,
  "scheduleHours": 24
}

Response

{
  "org": { "id": "…", "displayName": "…", "orgUrl": "…", "patHint": "abcd" },
  "project": { "id": "…", "adoProject": "…" }
}
GET/api/orgs/[orgId]Auth: Session + org owner

Fetch a single org and its projects. Never returns the PAT or ciphertext.

Response

{
  "id": "…",
  "displayName": "…",
  "orgUrl": "…",
  "patHint": "abcd",
  "projects": [ { "id": "…", "adoProject": "…", "scheduleHours": 24 } ]
}
PATCH/api/orgs/[orgId]Auth: Session + org owner

Update org display name or rotate the PAT. PAT rotation triggers a connection test before replacing the existing ciphertext.

Request Body

{ "displayName": "New Name" }
// or for PAT rotation:
{ "pat": "new-pat-value" }

Response

{ "ok": true }
DELETE/api/orgs/[orgId]Auth: Session + org owner

Delete the org and all associated projects, runs, and developer scores. Cascades via foreign key constraints.

Response

{ "ok": true }
POST/api/orgs/[orgId]/testAuth: Session + org owner

Validate a PAT against the ADO org without saving it. Use this before PAT rotation or during onboarding.

Request Body

{ "pat": "…" }

Response

{
  "ok": true,
  "projects": ["ProjectAlpha", "ProjectBeta"]
}
// on failure:
{ "ok": false, "error": "Authentication failed" }

Projects & Scoring

POST/api/orgs/[orgId]/projectsAuth: Session + org owner

Add an ADO project to an org for scoring. Enforces plan project limits.

Request Body

{
  "adoProject": "MyProject",
  "daysBack": 30,
  "deepScan": false,
  "scheduleHours": 24,
  "protectedBranches": ["main", "master"],
  "alertOnCritical": true
}

Response

{ "id": "…", "adoProject": "MyProject", … }
POST/api/orgs/[orgId]/projects/[projectId]/scoreAuth: Session + org owner

Manually trigger a scoring run. Rate-limited to 1 run per project per 5 minutes. Creates a runs row and starts the pipeline. Returns immediately — poll /runs for status.

Response

{ "runId": "…" }
GET/api/orgs/[orgId]/projects/[projectId]/runsAuth: Session + org owner

List recent scoring runs for a project with status and summary statistics. Poll at 3 second intervals while a run is pending or running.

Response

[
  {
    "id": "…",
    "status": "complete",
    "triggeredBy": "manual",
    "startedAt": "2026-04-13T00:00:00Z",
    "completedAt": "2026-04-13T00:02:14Z",
    "devCount": 12,
    "highRiskCount": 2
  }
]

Cron (internal)

POST/api/cron/scoreAuth: Bearer {CRON_SECRET}

Called by Vercel Cron on an hourly schedule. Finds all projects with nextRunAt ≤ now() and sequentially runs the scoring pipeline for each. Not intended for external use — requests without the correct Authorization header are rejected with 401.

Response

{ "triggered": 3, "errors": [] }

Error Responses

StatusCodeMeaning
400BAD_REQUESTMissing or invalid request body fields
401UNAUTHORIZEDNo valid session or invalid cron secret
403FORBIDDENAuthenticated but does not own this org
404NOT_FOUNDOrg or project does not exist
429RATE_LIMITEDManual score trigger exceeds 1/5 min limit
500INTERNAL_ERRORUnhandled server error — check run error_message field