apisec-surface
Static analysis probe for extracting architectural metadata from codebases.
Overview
apisec-surface analyzes source code to extract:
- Routes/Endpoints — HTTP routes, parameters, request/response types
- Data Flows — How data moves from entry points to sinks
- Authentication — Auth schemes, dependencies, role requirements
- Integrations — External services, databases, APIs
- Dependencies — Package dependencies and versions
The output is a structured manifest that can be uploaded to the APIsec cloud for vulnerability analysis. Raw source code never leaves your environment.
Requirements
- Python 3.11 or newer (3.11 and 3.12 are supported). Check with
python --version. - No JDK, Node, Ruby, or .NET runtime required — all parsers are pure-Python
(Java via
javalang, C#/JS/TS/Ruby via tree-sitter grammars). You can analyze a Java or Ruby project without those toolchains installed.
Installation
The CLI is published on PyPI as apisec-surface.
Upgrading from
apisec-code-bolt? The package was renamed in 0.1.13; the old PyPI project gets no further releases. Both packages install the same Python module, so remove the old one first:pipx uninstall apisec-code-bolt && pipx install apisec-surface # or: pip uninstall -y apisec-code-bolt && pip install apisec-surfaceThe
apisec-code-boltcommand still works as a deprecated alias, and existingapisec-code-bolt/state.yamlfiles and saved credentials are reused unchanged.
Recommended: isolated install (pipx or uv)
Installing a CLI into an isolated environment avoids dependency conflicts with other tools and sidesteps system-Python issues:
# Using pipx
pipx install apisec-surface
# Or using uv (also handles the Python version for you)
uv tool install apisec-surface
Plain pip
pip install apisec-surface
On an older or mismatched Python? If
pip installfails with arequires-pythonerror, your defaultpythonis older than 3.11. The simplest fix isuv, which fetches a compatible interpreter automatically:uv tool install apisec-surface # install the CLI, or uv run --python 3.12 apisec-surface ... # run ad hoc under 3.12
Verify the install
apisec-surface --version
Getting Started (end to end)
A full run is three steps: register → authenticate → analyze.
1. Register (first run only)
The first time you run the CLI it asks for the registration code APIsec
provided during onboarding (format ###-###):
apisec-surface analyze . # prompts: "Please enter code (###-###)"
For non-interactive environments (CI, scripts), supply it via the environment instead of typing it at a prompt:
export APISEC_REGISTRATION_CODE=123-456
2. Authenticate
Store your APIsec API key so uploads are authorized:
# Interactive (prompts for the key)
apisec-surface auth
# Or pass the key directly
apisec-surface auth sk_live_abc123...
# Confirm you're authenticated
apisec-surface auth --check
3. Analyze
# Analyze the current project and upload the manifest to the cloud
apisec-surface analyze .
On a successful upload the CLI prints a "View Results in APIsec" panel with a direct link to your results in the console.
Working offline / inspecting the manifest
# Analyze and save the manifest locally, no upload
apisec-surface analyze . --output manifest.json --no-upload
# Analyze but write nothing — just print a summary (great for a first look)
apisec-surface analyze . --dry-run
# Emit manifest JSON to stdout for piping into other tools
apisec-surface analyze . --stdout --no-upload | jq .
# Give the extractor framework hints
apisec-surface analyze . --frameworks fastapi,sqlalchemy
Supported Languages & Frameworks
| Language | Frameworks |
|---|---|
| Python | FastAPI, Flask, Django, GraphQL (Strawberry / Graphene / Ariadne), Celery, Click, Prefect |
| Java | Spring Boot, Micronaut, JAX-RS (Quarkus), GraphQL (Spring for GraphQL / graphql-java-kickstart) |
| JavaScript / TypeScript | Express, Fastify, NestJS, GraphQL (NestJS GraphQL / TypeGraphQL) |
| Ruby | Rails, Grape, Sinatra, GraphQL (graphql-ruby) |
| C# / .NET | ASP.NET Core, legacy ASP.NET (MVC/Web API), WCF, gRPC, Refit |
Framework coverage is validated end-to-end against real-world repositories in
the benchmark suite (benchmark/).
Configuration
Scaffold a config file with sensible defaults:
apisec-surface init # writes .surface.yaml
.surface.yaml in your project root is picked up automatically:
analysis:
file_discovery:
exclude_patterns:
- "tests/**"
- "**/migrations/**"
max_files: 10000
detect_workspace_boundaries: true # see "Monorepo support" below
data_flow:
mode: inter_procedural
max_depth: 10
cloud:
enabled: true
api_url: https://api.apisec.ai
output:
format: json
Monorepo support
When a scanned tree contains multiple independently-deployed sub-projects
(a pnpm/npm workspace, a Maven or Gradle multi-module build, a .sln with
several .csprojs, a go.work, or a uv/Poetry workspace — or, absent any of
those, any directory with its own recognised dependency manifest file),
analyze automatically detects each one as a separate boundary and uploads
its own manifest as its own Application, instead of one flat manifest mixing
every sub-project's routes, dependencies, and secrets together.
- Activation is automatic and safe by default. Detection only takes
effect when 2 or more real boundaries are found; a single-project repo
is completely unaffected — same manifest, same canonical id, same
state.yaml. Disable it entirely with--no-detect-monorepoordetect_workspace_boundaries: false. - Identity. Each sub-project uploads under
{repo_canonical_id}/{relative/path/to/sub-project}; code that doesn't belong to any detected sub-project (root-level config, CI files, a shared library with no manifest of its own) uploads under{repo_canonical_id}/shared. - Display name. Each sub-project's Application is named
{repo_name}/{relative/path/to/sub-project}(and{repo_name}/sharedfor the catch-all), so sibling services are distinguishable in the APIsec console's Applications list instead of all showing up under the identical bare repo name. A single-project repo is unaffected — same barerepo_nameas always. - Auth-middleware scoping rides on the same gate — and this one is
security-material. A globally-registered auth middleware is attributed
only to routes inside its own boundary, so one service's middleware can no
longer mark a sibling service's routes as authenticated (which hides
genuinely-exposed endpoints). Because it is gated on the same "2 or more
boundaries" condition, a polyglot repo that presents only a single
boundary — e.g. one root manifest and no per-service marker file or
workspace config — falls back to the legacy path and gets repo-wide
attribution. If a repo mixes languages or services, give each one its own
dependency manifest (or declare a workspace) so boundary detection can see
them; otherwise auth attribution is repo-wide and may over-report routes as
protected.
Scoping covers every auth path that resolves by name rather than by file:
Spring Security filter chains, Rails
before_action(includingApplicationController-inherited callbacks), Django view classes and decorators, and route-level auth dependencies — so two services that each define, say, anarticlescontroller no longer share each other's guards. - Known v1 limitations:
- A response/request schema referenced by routes in more than one sub-project is duplicated into each of them; only one level of nested model references is followed.
- The
sharedpartition does not get the LLM-based surface-enrichment pass (it has no single directory to scope that scan to). - A cross-partition reference (e.g. a route depending on an auth guard defined in another sub-project) is preserved as-is and surfaced as a warning in that partition's manifest, not silently dropped or rewritten.
Memory limits
A very large repository can need more memory than the machine — or, more often,
the container the scan runs in — will grant the process. When that happens the
kernel's OOM killer ends the run with SIGKILL: exit code -9, no message, no
manifest. From the outside that is indistinguishable from a target that simply
has nothing in it. analyze therefore watches its own memory and stops
before the kernel can, with a diagnostic naming the limit it hit:
Analysis failed: analysis aborted: memory limit 4096 MB exceeded after 8213
files (stage 'call_graph', resident 4111 MB, repo /src/app). This stop replaces
the OS kill it preempts (rc=-9, no message, empty result): raise
APISEC_MEMORY_LIMIT_MB to scan a repository this large, or set
APISEC_MEMORY_LIMIT_MB=0 to disable the guard.
- Threshold.
APISEC_MEMORY_LIMIT_MBsets the ceiling, in MB (an optionalmbsuffix is accepted). Unset, it defaults to 90% of the memory the OS would actually grant the process — the cgroup limit when containerised (that is what the OOM killer charges against), otherwise physical RAM. That default never interrupts a scan that fits, and a scan that crosses it was about to be killed anyway. - Exit code. A guard stop exits
6(RESOURCE_EXCEEDED) instead of1, so CI can tell a resource abort apart from a broken manifest, and leaves no manifest behind to be mistaken for an empty scan. --log-format json. The same information arrives as ascan_failedrecord withreason: memory_limit_exceededplus the limit, observed RSS, stage, file count and repo; thescan_startrecord carries the ceiling the run started with, so even a run killed mid-flight leaves its memory state behind.- Opt out.
APISEC_MEMORY_LIMIT_MB=0(oroff/none) switches the guard off, for a runner whose limit is misdetected.
Commands
Global options (before the subcommand): --version, -v/--verbose,
-q/--quiet, --debug, --log-format [text|json].
analyze
Analyze a codebase and generate/upload a manifest.
apisec-surface analyze [PATH] [OPTIONS]
Options:
-o, --output FILE Save manifest to file instead of uploading
--no-upload Skip uploading to cloud (implies --output if not set)
--api-key TEXT Override stored API key
--api-url TEXT Applicationsservice base URL (all analysis traffic is proxied here)
--format [json|yaml] Output format
--config FILE Path to configuration file
--frameworks TEXT Comma-separated framework hints
--exclude TEXT Glob patterns to exclude (repeatable)
--max-files INTEGER Maximum files to analyze
--timeout INTEGER Analysis timeout in seconds
--dry-run Analyze and print a summary; write/upload nothing
--stdout Write manifest JSON to stdout (for pipelines)
--detect-monorepo / --no-detect-monorepo
Detect monorepo sub-project boundaries and upload one
Application per sub-project (default: on; see
"Monorepo support" below)
auth
Authenticate with the APIsec cloud.
apisec-surface auth [API_KEY] [OPTIONS]
Options:
--api-url TEXT APIsec API URL
--check Check if already authenticated
--logout Remove stored credentials
init
Scaffold a .surface.yaml configuration file.
apisec-surface init [OPTIONS]
Options:
-o, --output FILE Output file path (default: .surface.yaml)
--force Overwrite an existing file
validate
Validate a manifest file against the schema.
apisec-surface validate MANIFEST_FILE
answer
Answer verification queries (for air-gapped environments where the manifest was uploaded separately and the cloud generated questions).
apisec-surface answer [OPTIONS]
Options:
-q, --questions FILE Input questions file (JSON) [required]
-o, --output FILE Output answers file
-r, --repo DIRECTORY Repository path
--timeout INTEGER Query timeout in seconds
telemetry
Manage anonymous usage telemetry (opt-out; on by default, disable any time with
telemetry off; never includes code, paths, or credentials).
apisec-surface telemetry on|off|status
Architecture
src/apisec_code_bolt/
├── cli/ # Command-line interface
├── core/ # Types, config, manifest schema
├── parsing/ # Language-specific parsers
│ ├── python/ # LibCST-based Python parser
│ └── jvm/ # Java via the pure-Python javalang library
├── frameworks/ # Framework plugins
│ ├── python/ # FastAPI, Flask, Django, GraphQL, Celery, Click, Prefect
│ ├── java/ # Spring Boot, Micronaut, JAX-RS, GraphQL
│ ├── js/ # Express, Fastify, NestJS, GraphQL
│ ├── ruby/ # Rails, Grape, Sinatra, GraphQL
│ └── dotnet/ # ASP.NET Core, legacy ASP.NET, WCF, gRPC, Refit
├── analysis/ # Call graph, data flow
├── fingerprinting/ # Integration detection
├── query/ # Query API executor
└── cloud/ # Cloud communication
Development
Setup
# Clone and install in development mode
git clone https://github.com/apisec-inc/ApisecSurfaceCore.git
cd ApisecSurfaceCore
pip install -e ".[dev]"
Running Tests
pytest
Type Checking
mypy src/apisec_code_bolt
Linting & Formatting
CI runs ruff check and ruff format --check as separate steps. Run both
before you push (formatting alone does not fix lint, and lint fixes do not format).
One command (same as CI):
make ci
Manual equivalent:
ruff check .
ruff format --check .
pytest -x --tb=short
Auto-fix locally:
make fix # ruff format + ruff check --fix
Pre-commit (recommended): hooks run ruff lint + format on every commit.
pip install -e ".[dev]"
make pre-commit-install # or: pre-commit install
pre-commit run --all-files # once, to verify the repo
Push org PRs to the github remote (apisec-inc/ApisecSurfaceCore), not a
personal origin fork, unless you intend to.
Publishing a release (maintainers)
The CLI is published to PyPI by the
Publish to PyPI GitHub Action. Merging to build does NOT publish — the
workflow only runs on workflow_dispatch or a published GitHub Release, and it
builds from whatever ref it runs on.
Prerequisites (one-time): the PYPI_API_TOKEN repository secret must be set
(Settings → Secrets and variables → Actions).
-
Bump the version. Edit
versioninpyproject.toml(single source of truth;--versionreads it via package metadata). PyPI rejects re-uploads of an existing version, so the number must be higher than the current PyPI release. Open a PR and merge it tobuild. -
Publish — dispatch the workflow (the standard path we use): Actions → Publish to PyPI → Run workflow → select branch
build.gh workflow run "Publish to PyPI" --ref build
Alternative: publish via a GitHub Release
New release → create tag
vX.Y.Z→ set Target:build(it defaults to the default branch; onlybuildhas the bumped version) → Publish. This fires the same workflow viarelease: [published].gh release create vX.Y.Z --target build --title "vX.Y.Z" --notes "…"
-
Verify. Confirm the new version appears on PyPI, then upgrade an install:
uv tool upgrade apisec-surface # or: pipx upgrade apisec-surface
Always release from
buildafter the version bump has merged there — releasing from a ref that still carries the old version will fail the PyPI upload.
Privacy
apisec-surface is designed with privacy as a core principle:
- No raw code egress — Source code never leaves your environment
- Metadata only — The manifest contains structural information, not code
- Outbound only — Only makes outbound HTTPS calls to upload manifests
- Air-gapped support — Can run completely offline with file-based workflow
License
Proprietary. Copyright © APIsec.
Metadata
Release files for apisec-surface 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| apisec_surface-1.0.0.tar.gz | 1.3 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| apisec_surface-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 2.2 MB
Release files / apisec_surface-1.0.0.tar.gz
| Download URL | apisec_surface-1.0.0.tar.gz |
|---|---|
| Size | 1.3 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b17684446d1d4e12a6359c19313a13c39ad75cddf8a831fc40be582caa9769e0
|
|
BLAKE2b-256 checksum How to use checksums |
0be7cfa793d5862841eabaed2bbf65ae6480031a467d1b440f6a92dbc99fe842
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / apisec_surface-1.0.0-py3-none-any.whl
| Download URL | apisec_surface-1.0.0-py3-none-any.whl |
|---|---|
| Size | 909.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
dbdf8c3e229b1b688edb4da7da09198dd6a46eb648f2c2bc8a1325ce48945e36
|
|
BLAKE2b-256 checksum How to use checksums |
844f53c3b9b4b6951b1955ade838586ffc2c851b60be8e07fa0c4e047cee19c9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|