Set up Surface Discovery for GitHub
Surface Discovery for GitHub finds the APIs in every repository of a GitHub organization and registers each one as an APIsec application. One repository in your organization runs the APIsec Surface org scan workflow every night. The workflow runs in your organization, on your runners, with your secrets. With the settings in this guide, your source code stays on your runners. APIsec receives only the results of the Surface Code Discovery analysis: facts and observations about the code, such as routes, called functions, dependency versions, and masked secrets.
Your repository holds only a short caller workflow. The scan logic lives in a reusable workflow that APIsec hosts in the public repository apisec-inc/apisec-orchestrator. Fixes reach you on the next nightly run, with no change on your side.
Each run scans only the repositories that someone pushed to since their last successful scan. The first run scans every repository.
Unlike the single-repository workflow, Surface Discovery covers the whole organization from one repository.
Prerequisites
- An organization owner account on the GitHub organization. You need it to set organization-level Actions secrets and variables.
- 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. - An APIsec user account whose platform role is
ADMINorUSER, to create the APIsec token. - A runner pool for the workflow. Use one of these:
- GitHub-hosted runners. This is the default. No setup is needed.
- Self-hosted runners registered to the organization with the custom label
apisec. GitHub adds theself-hostedlabel automatically. The runners need Python 3.10 or later,git, and outbound access to GitHub,pypi.org,files.pythonhosted.org, andhttps://api.apisecapps.com.
Step 1: Create the scan repository
- Open the template repository
apisec-inc/apisec-surface-scan-templateand click Use this template › Create a new repository. - Set Owner to your organization and give the repository a name, such as
apisec-surface-scan. - Open the new repository's Actions tab and confirm that the APIsec Surface org scan workflow is listed. If Actions are disabled for the repository, enable them.
The workflow scans the organization that owns the repository, so you don't need to change the workflow file.
To use an existing repository instead, add the file templates/apisec-scan.yml to it as .github/workflows/apisec-scan.yml. The template repository contains exactly this file.
The rest of this guide calls this repository the scan repository.
If your organization's Actions policy allows only workflows from your own organization, the reusable workflow can't run. Use the standalone workflow instead. See If your organization blocks external workflows.
Step 2: Configure secrets and variables
You can add secrets and variables at the organization level or on the scan repository. These steps use the organization level. Open your organization's Settings › Secrets and variables › Actions. For each secret and variable you add, set Repository access so that it includes the scan repository.
-
On the Secrets tab, add two secrets:
Secret Value APISEC_TOKENAn APIsec Personal Access Token with the Create App and Get App scopes, created by a user whose platform role is ADMINorUSER. A token from a Viewer is rejected. Create it on the API Credentials page.ORG_READ_TOKENA GitHub fine-grained personal access token with your organization as Resource owner. Grant Contents: Read-only and Metadata: Read-only. -
On the Variables tab, add three variables:
Variable Value APISEC_ENDPOINThttps://api.apisecapps.comAPISEC_REGISTRATION_CODEYour registration code, in the format ###-###APISEC_LLM_ENRICHMENT0. This keeps your source code on your runners. If this variable is not set, the workflow also sends selected source snippets to APIsec for LLM-assisted route discovery. -
Add any optional variables you need. See Optional variables.
The Repository access setting of ORG_READ_TOKEN decides which private repositories the workflow scans. Without ORG_READ_TOKEN, the workflow scans only public repositories.
Fine-grained tokens belong to the GitHub user who creates them, and they expire. If your organization uses a service account for automation, create the token from that account. Renew the token before its expiration date, or the nightly run fails. If your organization requires approval for fine-grained tokens, an owner must approve the token before the workflow can use it.
Optional variables
| Variable | Default | Value |
|---|---|---|
ORCHESTRATOR_RUNNER | GitHub-hosted (ubuntu-latest) | self-hosted runs every job on runners with both the self-hosted and apisec labels. Any other value uses GitHub-hosted runners. |
THROUGHPUT | 2 | The maximum number of repositories scanned in parallel, from 1 to 10. |
TARGET_ORG | The organization that owns the scan repository | The login of a different organization to scan. ORG_READ_TOKEN must then have that organization as Resource owner. |
EXCLUDE_REPOS | None | A comma-separated list of repositories to skip, as owner/repo. |
APISEC_CLI_VERSION | 1.1.0 | A different version of the Surface CLI (apisec-surface). |
Step 3: Run the workflow
To start a run, open the scan repository and go to Actions › APIsec Surface org scan › Run workflow. The runner pool comes from ORCHESTRATOR_RUNNER for both manual and nightly runs.
| Input | Default | Effect |
|---|---|---|
dry_run | false | Analyzes repositories but uploads nothing, and doesn't record them as scanned. |
force_full_scan | false | Ignores the scan record and scans every repository. |
max_repos | 0 | The maximum number of repositories in this run. 0 means no limit. |
Start with two runs:
- Dry run. Set
dry_runtotrueandmax_reposto5. The workflow lists repositories and analyzes up to five of them, but uploads nothing. Confirm that every job completes. Nothing appears in APIsec after a dry run. - First full run. Run the workflow with the default inputs. It scans every repository, up to 256 per run, starting with the most recently pushed, and uploads the results.
The workflow also runs every night at 02:00 UTC. The schedule is active as soon as the workflow file is in the repository, so add the secrets and variables before the first nightly run. To change the schedule, edit the cron line in the scan repository's workflow file. Only one run happens at a time. A new run waits until the current run finishes.
Each run scans at most 256 repositories, whatever the value of max_repos. When a run reaches this cap, the Plan job log shows Reached cap of N repos; re-run for the remainder. Run the workflow again to scan the rest. The workflow skips the repositories it already scanned.
For a first scan of a large organization, set max_repos to a lower value, such as 100. Run the workflow again until the Plan job log shows Scan plan: 0 repo(s).
Verify
- The workflow run finishes with every job green.
- In Applications, each discovered API appears as an application with its endpoints.
- Run the workflow again before anyone pushes to a repository. The Plan job log shows
Scan plan: 0 repo(s), and the workflow skips the scan jobs. This confirms that incremental scanning works.
How runs choose repositories
Each run compares every repository's last push time with the time of its last successful scan. It scans only the repositories pushed to since then. The workflow keeps this record as an Actions artifact named push-ts-store in the scan repository. Each run that scans a repository refreshes the record, and GitHub keeps it for 90 days. If the record has expired or can't be restored, the run logs a warning and scans every repository.
A run always skips these repositories:
- Archived and disabled repositories
- Empty repositories, with no commits
- The scan repository itself
- The repositories in
EXCLUDE_REPOS
When a scan finds no API routes in a repository, the workflow uploads nothing for it, and no application is created for it.
When a repository's scan fails, the workflow doesn't record that repository, so the next run retries it.
APIsec matches each application to its repository by the repository's GitHub ID. Later runs update the same application instead of creating a duplicate.
Control updates
The workflow file calls apisec-inc/apisec-orchestrator/.github/workflows/scan.yml@v1. The v1 tag moves to each new 1.x release, so you get fixes automatically. Breaking changes ship as v2, which you adopt by editing the tag.
To stay on one release, replace @v1 with a release tag, such as @v1.0.1.
If your organization blocks external workflows
If your organization's Actions policy blocks workflows from apisec-inc, replace the contents of .github/workflows/apisec-scan.yml with templates/apisec-scan-standalone.yml. Keep only one of the two workflows in the repository. It runs the same jobs inside your repository. Each run installs the latest 1.x release of the apisec-orgscan package from PyPI, so you still get fixes without editing the file.
The standalone workflow uses the same secrets and variables. Its Run workflow form adds two inputs:
| Input | Effect |
|---|---|
throughput | Overrides THROUGHPUT for this run. |
runner | github-hosted or self-hosted for this run. Leave it blank to use ORCHESTRATOR_RUNNER. |
Move from the archive version
If you set up an earlier version from apisec-orchestrator.zip, move to the new workflow in one of two ways. Either way, APIsec matches applications by GitHub repository ID, so neither way creates duplicates.
- Keep your repository. In your existing orchestrator repository, replace
.github/workflows/orchestrator.ymlwithtemplates/apisec-scan.yml, and delete thescripts/folder. Your secrets, variables (includingAPISEC_ENDPOINT), and scan record carry over, so the next run doesn't rescan repositories that haven't changed. Add theAPISEC_REGISTRATION_CODEandAPISEC_LLM_ENRICHMENTvariables from Step 2 if you don't have them yet. - Start fresh. Delete the old orchestrator repository and follow this guide from Step 1. The first run scans every repository once.
The variables ORCHESTRATOR_RUNNER and THROUGHPUT keep working. The default runner pool is now GitHub-hosted. If you use self-hosted runners, set ORCHESTRATOR_RUNNER to self-hosted. The new workflow doesn't use APISEC_SERVER_URL or the Variables: Read-only permission on ORG_READ_TOKEN.
If APIsec created applications from OpenAPI specifications for your repositories before you used the orchestrator, ask APIsec support to link those applications to their repositories before your first full scan.
Optional: Show the repository inventory in APIsec
The scan workflow doesn't use or need a GitHub App. Add the APIsec Surface Discovery GitHub App only if you want a live inventory of your organization's repositories in the APIsec console. The app has read-only access to repository metadata and no access to code.
Create the GitHub App
-
In GitHub, open your organization's Settings › Developer settings › GitHub Apps and click New GitHub App.
-
Fill in the General fields:
Field Value GitHub App name APIsec Surface DiscoveryHomepage URL https://apisec.aiCallback URL Leave empty. Webhook › Active Selected Webhook URL https://api.apisecapps.com/v1/integrations/github/webhookWebhook secret A random string. Generate one with the command after this table. SSL verification Enabled openssl rand -hex 32 -
Under Repository permissions, set Metadata to Read-only. Leave every other permission at No access.
-
Under Subscribe to events, select Repository, Installation, and Installation repositories.
-
Under Where can this GitHub App be installed?, select Only on this account, then click Create GitHub App.
-
On the app's General page, copy the App ID.
-
Under Private keys, click Generate a private key. GitHub downloads a
.pemfile.
Store the webhook secret and the .pem file in your password manager now. You paste both into APIsec when you connect the app, and GitHub doesn't show the webhook secret again.
Install the app on your organization
- On the app's settings page, click Install App, then click Install next to your organization.
- Choose All repositories, or Only select repositories to start with a subset. You can change this selection later.
- After installation, copy the Installation ID from the end of the page URL. For example,
https://github.com/organizations/example-org/settings/installations/12345678has the Installation ID12345678.
The repositories you select here appear in the APIsec inventory. The repositories that get scanned are set separately, by ORG_READ_TOKEN.
Connect the app in APIsec
-
In the APIsec console, click Integrations.
-
On the GitHub App tile, click View Inventories, then create a new connection.
-
Fill in the connection form:
Field Value Name Any unique name, such as APIsec Surface DiscoveryGitHub Organization Your organization's login, such as example-orgApp ID The App ID from the app's General page Installation ID The Installation ID from the installation page URL Private Key The full contents of the .pemfile, including theBEGINandENDlinesWebhook Secret The webhook secret you generated -
Leave Make connection private cleared. It restricts the connection to your user only.
-
Click Test Connection. When the test succeeds, click Save.
-
Open the connection and click Inventory. Your repositories appear with their visibility, default branch, and an APIsec column that shows Application exists or No application. The first sync runs in the background and usually finishes within a few minutes.
After a scan run, the repositories that have an application show Application exists. To test the lifecycle webhooks, archive or rename a test repository in GitHub. The inventory usually shows the change within a few minutes.
Troubleshoot
| Symptom | Cause | Fix |
|---|---|---|
A scan job fails with Set the APISEC_ENDPOINT org variable to your APIsec API URL. | APISEC_ENDPOINT is missing, or the scan repository can't access it | Add the variable and include the scan repository in its Repository access. |
A scan job fails with SSL certificate verify failed | APISEC_ENDPOINT points at the wrong host | Set it to https://api.apisecapps.com. |
The run fails with Set the APISEC_TOKEN secret, or run with dry_run. | APISEC_TOKEN is missing, or the scan repository can't access it | Add the secret and include the scan repository in its Repository access. |
A scan job fails with HTTP 403 | APISEC_TOKEN lacks Create App or Get App, or its creator has the Viewer role | Create a new token with both scopes from an ADMIN or USER account. |
The run logs ORG_READ_TOKEN is not set; only public repos and this repo can be scanned. | ORG_READ_TOKEN is missing, or the scan repository can't access it | Add the secret and include the scan repository in its Repository access. |
The run fails with TARGET_ORG is empty; refusing to guess which org to scan. | The workflow couldn't determine which organization to scan | Set the TARGET_ORG variable to your organization's login. |
A scan job fails with <path> was not uploaded (exit N); it is retried next run. | The analysis or upload of that repository failed | Check the job log for the cause. The next run retries the repository. |
| Jobs stay queued with "Waiting for a runner" | ORCHESTRATOR_RUNNER is self-hosted, and no runner has the apisec label | Add the apisec label to your runners. To use GitHub-hosted runners instead, delete ORCHESTRATOR_RUNNER. |
| The run is green, but no application appears | The run was a dry run | Run the workflow again with dry_run set to false. |
| A repository is never scanned | The repository is outside the Repository access of ORG_READ_TOKEN, is archived or empty, or is in EXCLUDE_REPOS | In the token's settings in GitHub, add the repository to Repository access, or remove it from EXCLUDE_REPOS. |
| A repository isn't scanned again | Nobody pushed to it since its last successful scan | Run the workflow with force_full_scan set to true to scan every repository. |
| A run scans every repository and logs that every repo counts as changed | The scan record expired, because no run scanned anything for 90 days, or the record couldn't be restored | No action needed. This run saves a new record, and later runs scan only changed repositories. |
| Test Connection fails with "GitHub credentials validation failed" | The App ID, Installation ID, or private key is wrong | Copy each value again from GitHub. Paste the whole .pem file. |
| Saving the connection fails with "already connected under a different connection" | Another APIsec connection already uses this installation | Edit the existing connection instead. |
| The inventory doesn't reflect repository changes | Webhook deliveries fail | In the GitHub App's Advanced › Recent Deliveries, a 403 means the webhook secrets don't match, and a 404 means the webhook URL is wrong. |
Related
- Surface Code Discovery: what it does, ways to run, and supported frameworks
- API Credentials: create and scope APIsec tokens
- Configure an authenticated scan: scan the applications that Surface Discovery registers