Run Surface Code Discovery in Azure Pipelines
This guide adds Surface Code Discovery to one repository in Azure DevOps, using an Azure Pipelines YAML pipeline. Each run on main registers or updates the repository's APIs in APIsec. It also analyzes the code's dependencies, authentication, and committed secrets. With the settings in this pipeline, your source code stays on the build agent; only facts and observations about it reach APIsec.
Prerequisites
- A repository in Azure Repos, or a GitHub repository connected to Azure Pipelines, that contains your API's source code in a supported framework. The pipeline runs on pushes to
main; change the branch name if yours differs. - A Microsoft-hosted Linux agent, such as
ubuntu-latest, or a self-hosted Linux or macOS agent with Python 3.11 or later andgit. - An APIsec Personal Access Token with the Create App and Get App scopes, created by a user whose platform role is
ADMINorUSER. Create it on the API Credentials page. - Your APIsec registration code, in the format
###-###, given to you by your APIsec Support resource. Each customer has their own code. If you don't have it, contact support. - Permission to edit the pipeline and its variables.
Step 1: Add the pipeline
Create azure-pipelines.yml in the repository root:
trigger:
branches:
include:
- main
pool:
vmImage: ubuntu-latest
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: "3.12"
- script: |
pip install "apisec-surface==1.2.0"
apisec-surface telemetry off
displayName: Install the Surface CLI
- script: apisec-surface analyze . --no-detect-monorepo --api-url https://api.apisecapps.com
displayName: Discover and analyze APIs
env:
APISEC_PAT: $(APISEC_TOKEN)
APISEC_REGISTRATION_CODE: $(APISEC_REGISTRATION_CODE)
APISEC_SURFACE_LLM_EXTRACTION: "0"
Commit the file, then create the pipeline from it: in Pipelines, select New pipeline, choose the repository, and select Existing Azure Pipelines YAML file. Don't run it yet; it needs the variables from step 2.
The pipeline runs on every push to main. It doesn't run on pull requests: a pull request's unmerged code would replace the application's inventory. For a repository in Azure Repos, pull request builds happen only through a Build Validation branch policy, so don't add this pipeline to one. For a GitHub repository, add pr: none at the top of the file.
Always pass --api-url: the CLI reads the APIsec URL only from this option, and the run fails without it. Use https://api.apisecapps.com unless APIsec gave you a different API URL.
What each environment variable does:
| Variable | Purpose |
|---|---|
APISEC_PAT | Authenticates the upload to APIsec. |
APISEC_REGISTRATION_CODE | Identifies your installation. Every fresh agent is a new installation, so the CLI needs the code on every run, even with --dry-run. If the secret is missing or empty, the log shows "Please enter code (###-###):" followed by "Aborted!", and the step fails with exit code 1. |
APISEC_SURFACE_LLM_EXTRACTION | "0" keeps all source code on the build agent. The CLI sends APIsec only facts and observations about the code, such as routes, called functions, dependency versions, and masked secrets. Endpoints come from static analysis of the code. |
The CLI identifies the repository from the BUILD_REPOSITORY_URI variable that Azure Pipelines sets on every run, as azure:<organization>/<project>/<repository>, so each run updates the same application in APIsec. The log shows this ID in the Cloud connection panel. The same panel shows the branch as HEAD, because Azure Pipelines checks out a detached commit; this doesn't affect the application. --no-detect-monorepo keeps the repository as one application. See Monorepos for the alternative.
apisec-surface telemetry off stops the CLI from sending anonymous usage data. The CLI does not commit files or create pull requests, so the pipeline needs the default read access to the repository only. The pipeline pins version 1.2.0; change the pin to upgrade. Versions before 1.2.0 can't identify the repository and register a new application on every run unless you set APISEC_APPLICATION_ID as in Step 3.
Step 2: Add the variables
- In Pipelines, open the pipeline and select Edit › Variables.
- Add these variables, and select Keep this value secret for each:
| Name | Value |
|---|---|
APISEC_TOKEN | Your APIsec Personal Access Token |
APISEC_REGISTRATION_CODE | Your registration code. Each customer has their own; the CLI needs it on every run. |
- Save, then run the pipeline.
Secret variables reach a script only through the step's env mapping, which the pipeline in step 1 includes. If a variable doesn't exist, Azure Pipelines passes the text $(APISEC_TOKEN) to the step unchanged, and the upload fails with HTTP 401.
Step 3: Attach to an existing application (optional)
Skip this step for a new repository. The first run registers an application in APIsec, and every later run updates it.
Set APISEC_APPLICATION_ID when the pipeline must update an application that already exists, such as one registered by an earlier run with a CLI older than 1.2.0. The pinned application takes precedence over the repository identity.
- Find the application's ID. In the APIsec console, open the application; the ID is in the page URL after
/application/. The View Results in APIsec link at the end of a pipeline log ends with it too. - In Edit › Variables, add
APISEC_APPLICATION_IDwith that ID. It's not secret. - Add this step before Discover and analyze APIs:
- script: |
mkdir -p apisec-code-bolt
printf 'applicationId: %s\n' "$APISEC_APPLICATION_ID" > apisec-code-bolt/state.yaml
displayName: Pin the APIsec application
env:
APISEC_APPLICATION_ID: $(APISEC_APPLICATION_ID)
- Run the pipeline. In Applications, that application updates and no new one appears.
Monorepos
For a repository with several services, choose one of these:
- Let the CLI split the repository. Remove
--no-detect-monorepofrom the analyze command. The CLI detects workspace boundaries, such as pnpm, npm, Maven, Gradle, .NET, Go, uv, and Poetry workspaces, and registers one application per sub-project, named after the repository and the sub-project's folder. Each run updates the same applications. The split ignoresAPISEC_APPLICATION_ID. - Run one step per service. Give each step the service's folder as the analyze path, keep
--no-detect-monorepo, and pin a different application in each step, with the pin written to that folder (mkdir -p services/orders/apisec-code-bolt). The pin is required here: every step identifies the repository the same way, so without pins all the steps update one application.
If your services import shared workspace packages, such as route factories or authentication middleware in packages/shared, a step that analyzes only one service's folder can't resolve those imports, and it can miss routes or their authentication. For these repositories, analyze from the repository root as one application, or compare a per-service run with a root run before you rely on it.
Step 4: Adjust the scan (optional)
The analyze command takes the same options as in GitHub Actions: a subfolder path, --exclude, --timeout, --dry-run, and --fail-on secrets. See Adjust the scan.
Verify
- Open the pipeline run. The Discover and analyze APIs step finishes green and the log lists the routes and frameworks it found, followed by a Verification Result table and a View Results in APIsec link. Replace
<tenant_name>in the link with your APIsec tenant name. - In the APIsec console, open Applications. The repository (or, with one step per service, each service) appears as an application with its endpoints, frameworks, and libraries.
- In the application, dependency vulnerabilities that the analysis traced to an endpoint appear as predicted exploits. An APIsec scan of the running API later confirms or clears each one.
- Run the pipeline again. The same application updates and no new one appears.
To test the setup without uploading, add --dry-run to the analyze command and run the pipeline once.
For large applications, verification can take longer than the run waits. The run then logs "Stopped waiting for verification" and still finishes green. The analysis continues in APIsec, and the results appear at the link in the log.
Troubleshoot
Use the exit code and the error in the log to find the cause.
| Exit code or symptom | Cause | Fix |
|---|---|---|
1 with "Please enter code" and "Aborted!", or 4 with "No registration code found" | The APISEC_REGISTRATION_CODE secret variable is missing or empty, or the step's env doesn't map it | Add the secret variable and keep the env mapping as in step 1. |
4, with "Cannot register application in CI" | The CLI couldn't read the repository identity from BUILD_REPOSITORY_URI | Run the CLI in a script step on the agent, as in step 1. If the step runs in a container, pass BUILD_REPOSITORY_URI into it. |
| Each run creates a new application | The pipeline pins a CLI older than 1.2.0 and APISEC_APPLICATION_ID is not set | Change the pin to 1.2.0, or set APISEC_APPLICATION_ID as in step 3. With one step per service, every step needs its own pin. |
1, analysis error | The CLI couldn't analyze the code | Check the path you passed, and that the code uses a supported framework. |
2, with "Upload failed: Client error '401 Unauthorized'" after the analysis tables | The token is invalid or expired, or the APISEC_TOKEN variable doesn't exist and the step received the text $(APISEC_TOKEN) | Create a new token and update the variable. Check the variable name matches the env mapping exactly. |
2, with HTTP 403 in the log | The token lacks a scope, or its creator lacks a role | Use a token with Create App and Get App, created by an ADMIN or USER. |
2, with SSL certificate verify failed | --api-url is wrong | Use https://api.apisecapps.com, or the API URL APIsec gave you. |
3, policy violation | --fail-on secrets found a committed credential | Remove the credential from the repository. |
4, configuration error | A required setting is missing or invalid | Read the error in the log. It names the missing setting. |
6, memory limit | The repository is too large for the agent | Scan a subfolder, or exclude generated and vendored code with --exclude. |
requires-python error during install | The agent's Python is older than 3.11 | Keep the UsePythonVersion@0 task, or install Python 3.11 or later on a self-hosted agent. |
| Green run, no endpoints | No supported framework was found | Point the analyze command at the folder that contains the API code. |
Related
- Surface Code Discovery: what it does, ways to run, and supported frameworks
- Run Surface Code Discovery in GitHub Actions: the same setup for a GitHub repository
- Gate CI/CD builds on APIsec scan results: fail a build on scan findings