Skip to main content

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-surface

The apisec-code-bolt command still works as a deprecated alias, and existing apisec-code-bolt/state.yaml files and saved credentials are reused unchanged.

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 install fails with a requires-python error, your default python is older than 3.11. The simplest fix is uv, 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-monorepo or detect_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}/shared for 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 bare repo_name as 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 (including ApplicationController-inherited callbacks), Django view classes and decorators, and route-level auth dependencies — so two services that each define, say, an articles controller 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 shared partition 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_MB sets the ceiling, in MB (an optional mb suffix 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 of 1, 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 a scan_failed record with reason: memory_limit_exceeded plus the limit, observed RSS, stage, file count and repo; the scan_start record 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 (or off / 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).

  1. Bump the version. Edit version in pyproject.toml (single source of truth; --version reads 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 to build.

  2. 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; only build has the bumped version) → Publish. This fires the same workflow via release: [published].

    gh release create vX.Y.Z --target build --title "vX.Y.Z" --notes "…"
    
  3. 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 build after 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 0.1.13

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for apisec-surface 0.1.13
File Size Uploaded
apisec_surface-0.1.13.tar.gz 1.3 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for apisec-surface 0.1.13
File Interpreter ABI Platform
apisec_surface-0.1.13-py3-none-any.whl Python 3 none any Details

Total release size: 2.2 MB

Release files / apisec_surface-0.1.13.tar.gz

Download URL apisec_surface-0.1.13.tar.gz
Size 1.3 MB
Tags Source
SHA-256 checksum
How to use checksums
af3f4200a7d53e51bb8cc18ba55bd74a55b6958b456ac9a3306437cdd826e350
BLAKE2b-256 checksum
How to use checksums
d62f5ff85b514bc20fe5642b439cdf693ac33fbf1c7c1df7637f61188e78ff9c
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-0.1.13-py3-none-any.whl

Download URL apisec_surface-0.1.13-py3-none-any.whl
Size 907.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b8fa708fc558b8ed2a10349301bfeddbc3d7f991e14837677d490a638eac855b
BLAKE2b-256 checksum
How to use checksums
efdd608b8000904a86c9b88101f7a4872481337304fb75abd26abd16509596d0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

1.0.0

2 release files

This release

0.1.13 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page