# Pipelines, dependencies and supply chain

What a read-only reviewer can actually check in a diff. Verified 2026-09 against learn.microsoft.com/azure/devops/pipelines and OWASP Top 10:2025 A03.

## Dependency and lockfile diffs

- **Read the lockfile, not only the manifest.** A one-line manifest bump can add dozens of transitive packages; the lockfile diff shows them. Flag new top-level *and* new transitive names in the review.
- **Check the source.** A dependency pointing at a git URL, a fork, a personal registry or a tarball instead of the official registry is a finding until someone explains it.
- **Check the jump.** A major-version bump in a security-relevant library (auth, crypto, serialization, templating) needs the changelog read; call it out when the PR does not mention it.
- **Postinstall and build scripts** in a newly added package run on every developer machine and build agent. Name them.
- **Typosquats:** compare the new name against the package it resembles (`lodahs`, `reqeusts`, `@types/node-fetch` vs `node-fetch`). Confirm the publisher and the repository link.
- **Removed pins** — a lockfile deleted, a range widened from `1.2.3` to `^1.2.3`, `--no-save` installs in a script — reduce reproducibility; treat as `medium` when the dependency reaches a sink.

## `azure-pipelines.yml` and templates

| Look for | Why it matters | Fix |
|---|---|---|
| Secrets as plain `variables:` values, or echoed in a script | Plain variables are visible in logs and to anyone who can read the repository | Variable group linked to Key Vault; reference as `$(name)`; never `echo` it |
| A secret used in a script without an `env:` mapping | Secret variables are deliberately not injected into the environment, so the script silently gets an empty value — or the author works around it by making the variable non-secret | Map explicitly: `env: { API_KEY: $(apiKey) }` |
| `persistCredentials: true` on `checkout` | Leaves a usable repository token in the work folder for every later step, including third-party tasks | Omit it (the default is `false`); when a step really must push, scope it to that job |
| Unpinned task or container versions (`@0`, `:latest`, an unpinned action) | The build's behaviour changes without a commit | Pin the major version (`UseDotNet@2`) and digest-pin images |
| `trigger:` or `pr:` widened to all branches, or a `pr` trigger on a fork with secrets available | A fork PR that runs with secrets is remote code execution against your pipeline | Explicit branch lists; no secrets for fork builds; deployment behind an environment with approvals |
| Scripts fetched from the internet and piped to a shell | Unreviewed code runs on the agent with repository credentials | Vendor the script or pin it by digest |
| `System.AccessToken` handed to a script, or a service connection widened | The token acts as the build identity across the project | Grant per job, only where needed, and say which permission the connection holds |
| Deployment steps without an environment or approval | Anyone who can merge can reach production | Use environments with checks; keep the approval in the pipeline, not in convention |

Self-hosted agents add one more: anything written to the work folder or global tool cache persists for the next pipeline that runs on that agent.

## What a review cannot settle

Runtime configuration, network egress rules, secret-store contents and agent-pool membership are invisible in a diff. Put them under **Needs verification** with the check that would settle them — "confirm the `prod-deploy` service connection is scoped to the resource group, not the subscription" — rather than asserting either way.
