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.
List Repositories
Fetches all Git repositories in the selected ADO project.
GET {orgUrl}/{project}/_apis/git/repositoriesFetch 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}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}/changesDeep 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}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=completedDetect 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}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
| Field | Source | Purpose |
|---|---|---|
| commitId | ADO Commits API | Unique identifier for cross-referencing |
| author.email | ADO Commits API | Developer identity (normalized to lowercase) |
| author.name | ADO Commits API | Display name |
| author.date | ADO Commits API | Timestamp for recency and weekly bucketing |
| changeCounts.Add/Edit/Delete | ADO Commits API | Not stored — used transiently |
File change paths
| Field | Source | Purpose |
|---|---|---|
| item.path | ADO Commit Changes API | Checked against sensitive file patterns |
| changeType | ADO Commit Changes API | Not stored |
Pull request metadata
| Field | Source | Purpose |
|---|---|---|
| createdBy.uniqueName | ADO Pull Requests API | PR author identity |
| creationDate / closedDate | ADO Pull Requests API | Window filtering and direct push detection |
| reviewers[].uniqueName + vote | ADO Pull Requests API | PR review attribution (non-zero vote only) |
| sourceRefName / targetRefName | ADO Pull Requests API | Branch 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).
| Table | What is stored | Retention |
|---|---|---|
| users | Email, hashed password (bcrypt), plan, Stripe IDs | Until account deleted |
| orgs | Display name, ADO org URL, encrypted PAT, IV, last-4 hint | Until org deleted |
| projects | ADO project name, scoring config, schedule | Until project deleted |
| runs | Status, timestamps, aggregate counts (devs, high-risk count) | Indefinite |
| developer_scores | Email (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
Required PAT Scopes
ADOScore only requires read-only scopes. Grant the minimum permissions when creating your PAT in Azure DevOps.
| Scope | Permission | Why |
|---|---|---|
| Code | Read | List repos, commits, file paths, file content (Deep Scan) |
| Pull Request Threads | Read | List PRs and reviewer votes |
Scoring Methodology
Activity Score (0–100, higher = more active)
| Signal | Max points | Formula |
|---|---|---|
| Commit frequency | 30 | min(commits_30d, 20) / 20 × 30 |
| Recency | 20 | max(0, 20 − max(0, days_since_last − 7)) |
| PR participation | 25 | (min(prs_created, 15) + min(prs_reviewed, 10)) / 25 × 25 |
| Consistency | 15 | max(0, 15 − (weekly_std_dev / 10 × 15)) |
| Branch diversity | 10 | min(unique_branches, 5) / 5 × 10 |
Security Risk Score (0–100, higher = more risk)
| Signal | Points each | Cap | Requires |
|---|---|---|---|
| Secret/credential in file content | +35 | ×3 max | Deep Scan enabled |
| Sensitive file committed | +20 | ×2 max | Standard scan |
| Branch policy bypass | +25 | ×2 max | Standard scan |
| Direct push to protected branch | +20 | ×2 max | Standard scan |
Total is clamped to 100.
Risk Tiers
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 name | Example match target |
|---|---|
| AWS access key | AKIA + 16 uppercase alphanumerics |
| Private key header | -----BEGIN PRIVATE KEY----- |
| Azure SAS token | sig= followed by base64 encoded signature |
| GitHub token | ghp_, gho_, ghu_, ghs_, ghr_ prefixes |
| Generic password | password=, passwd=, pwd= with 8+ char value |
| Generic API key | api_key=, apikey= with 16+ char value |
| Connection string | Server=…;Password=… style MSSQL/ADO.NET strings |
| Bearer token | Authorization: Bearer with 20+ char token |
| SendGrid key | SG. prefix with standard key format |
| Slack token | xoxb-, xoxa-, xoxp-, xoxr-, xoxs- prefixes |
| Azure storage key | DefaultEndpointsProtocol=https;AccountName= |
Sensitive File Patterns
These file path patterns trigger a risk finding regardless of file content. Deep Scan is not required.
| Pattern | Examples |
|---|---|
| .env files | .env, .env.local, .env.production |
| Private keys | id_rsa, id_ed25519, id_ecdsa, id_dsa |
| Certificate files | *.pem, *.key, *.pfx, *.p12, *.cer, *.jks |
| Secret stores | credentials (no extension), kdbx |
| Secret config files | secrets.json, secrets.yaml, secrets.yml |
| Infrastructure secrets | terraform.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
/api/auth/callback/credentialsAuth: NoneExchange 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
/api/orgsAuth: SessionCreate 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": "…" }
}/api/orgs/[orgId]Auth: Session + org ownerFetch a single org and its projects. Never returns the PAT or ciphertext.
Response
{
"id": "…",
"displayName": "…",
"orgUrl": "…",
"patHint": "abcd",
"projects": [ { "id": "…", "adoProject": "…", "scheduleHours": 24 } ]
}/api/orgs/[orgId]Auth: Session + org ownerUpdate 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 }/api/orgs/[orgId]Auth: Session + org ownerDelete the org and all associated projects, runs, and developer scores. Cascades via foreign key constraints.
Response
{ "ok": true }/api/orgs/[orgId]/testAuth: Session + org ownerValidate 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
/api/orgs/[orgId]/projectsAuth: Session + org ownerAdd 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", … }/api/orgs/[orgId]/projects/[projectId]/scoreAuth: Session + org ownerManually 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": "…" }/api/orgs/[orgId]/projects/[projectId]/runsAuth: Session + org ownerList 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)
/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
| Status | Code | Meaning |
|---|---|---|
| 400 | BAD_REQUEST | Missing or invalid request body fields |
| 401 | UNAUTHORIZED | No valid session or invalid cron secret |
| 403 | FORBIDDEN | Authenticated but does not own this org |
| 404 | NOT_FOUND | Org or project does not exist |
| 429 | RATE_LIMITED | Manual score trigger exceeds 1/5 min limit |
| 500 | INTERNAL_ERROR | Unhandled server error — check run error_message field |