Hanzo AI

Security

Secret scanning for your code: submit sources, get findings, masked never raw.

Also for this capability: API · CLI · MCP · SDKs

Secret scanning for your code: submit sources, get findings, masked never raw.

Base URLhttps://api.hanzo.ai
Operations7
AuthAuthorization: Bearer $HANZO_API_KEY

Specification

HIP-1047 · Security Scanning — Draft · read the specification →

/v1/security scans submitted source for hardcoded secrets and keeps the findings. It is served by apps/security in hanzoai/cloud.

One rule shapes everything else: the submitted content is NEVER stored, and a finding never carries the secret it found. What persists is a masked preview and the digest of the raw secret — enough to recognise and to triage, not enough to use.

Motivation

A scanner that stores what it scanned is a repository of every secret its users ever leaked, held by a service whose whole purpose is telling them not to do that. The interesting design question is therefore not detection; it is what a finding is allowed to remember.

Specification

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.

Content is read, never kept

Submitted files are scanned IN MEMORY. A finding persists its rule, its path and line, a MASKED preview — first and last characters kept, the middle starred — and the SHA-256 of the raw secret (apps/security/security.go:336, apps/security/detect/detect.go:190-199).

The digest is what makes the same secret recognisable across scans and after rotation WITHOUT the secret ever being written down. It is a correlation key, not a recovery path.

Everything stored is org-scoped, and every miss is a 404

A scan and every finding on it are filed under the caller's validated org, and a caller with no org is refused. An identifier belonging to another org is the SAME 404 as an identifier that never existed (apps/security/security.go:451), so the surface cannot be used to learn what exists elsewhere.

Bounded submissions

One submission is bounded in file count and in total content (apps/security/security.go:44-45). A caller with more source splits it across scans. The bound is on the REQUEST, so a single call can neither exhaust the process nor wedge the engine.

Prepaid, at a resolved fee

A scan is one metered unit, billed under the meter kind security.scan (apps/security/security.go:26). The caller's balance MUST cover it BEFORE the engine runs (apps/security/security.go:375): a check downstream of the work is a bill for compute already spent. The debit lands through the fleet's one resource meter on the caller's own commerce ledger (apps/security/security.go:413).

The fee resolves through the platform's own policy default — CLOUD_SECURITY_FEE_CENTS, defaulting to the fleet-wide $1.00 provision fee, zero making scanning free and un-gated (apps/security/security.go:28-31) — never a number invented by this subsystem. This is stated because the failure is quiet: the surface declared itself metered from the start and passed a literal zero as the amount, and a zero debit posts no ledger entry — so the platform required standing to scan and then charged for none of them.

The METERED amount MUST be the same value the balance was checked against, read once, so a charge can never exceed what was authorized.

Off the request path there is no payer, and that is a refusal rather than an unbilled scan.

The audit record carries the tally, not the findings

A scan is recorded as an audited action with who ran it and what it found by count. The redacted findings are the evidence and live in the store; the tally is the outcome the audit log carries (AU-3).

Unauthenticated reads are the ones that disclose nothing

Liveness and the detection catalog take no org: the first measures this process and answers while any tenant state is cold, and the second is the same for everyone. Everything that touches stored results requires a validated org.

Every operation is typed

Each operation is a typed input and answer, so the schema, the prose, the tool, the CLI command and every generated SDK method are projections of the handler itself. apps/security/typed_wire_test.go holds the exceptions as a CLOSED list that is currently EMPTY, and fails on an operation that is neither typed nor named there — so the next route added is typed by default and dropping one out takes a deliberate edit with a reason.

A filter value outside its vocabulary is REFUSED rather than ignored, so a typo in a severity filter cannot read as "no findings".

Store, addresses, events, stage, upstreams

The one store this capability owns is its findings database: an org-scoped SQLite file holding scans and their redacted findings and nothing else, encrypted at rest where the build carries the cipher (apps/security/store.go:13-27). The addresses are /v1/security/scans (POST and GET, and GET of one by id), /v1/security/findings (GET, and GET of one by id), /v1/security/rules and /v1/security/health (plugin/security/openapi.json) — nothing else answers under the prefix.

It publishes no event, so a customer's webhooks receive nothing from it; the record of a scan is §5's audit entry. Beyond the request span every route already gets it emits nothing to observability. Its stage is beta (manifest/apps.go:338, Stage: Beta), so it is reached by flag until promoted. The detection engine derives from no outside project — it is a dependency-free leaf over the standard library, no scanner binary embedded or shelled out to (apps/security/detect/detect.go:1-13).

Rationale

The alternative to masking is storing the match and encrypting it, which turns every finding into a key-management problem and makes the blast radius of this store the union of its users' secrets. A digest plus a mask supports the two things a reviewer actually does — recognise which secret this is, and confirm it stopped appearing — with nothing to steal.

Gating before the engine rather than after costs a balance read on every submission and removes the case where an unfunded caller consumes the compute and then learns they could not pay for it.

Security Considerations

The threat this capability creates is itself: a service that receives source code containing live credentials. The mitigations are that the content is not persisted, the finding cannot reconstruct the secret, and the store is partitioned by org with denials that disclose nothing.

The digest deserves care. SHA-256 of a raw secret is reversible by guessing when the secret is low-entropy — a short password, a well-known test key — so the digest MUST be treated as sensitive within the tenant's own scope, and MUST NOT be published across tenants as a shared correlation key.

Submission bounds are a denial-of-service control, not tidiness: the engine runs regular expressions over caller-supplied text, so the size of that text is the size of the work a single request can buy.

Four surfaces

SurfaceReaches this capability asCoverage
RESTsecurity at its own prefix7 operations
CLIhanzo security …7 of 7
SDKSecurityApi in every published client7 methods
MCPtool security on https://api.hanzo.ai/v1/mcp7 operations, 1 under the document's own id — ask describe for the rest

Quickstart

export HANZO_API_KEY=sk-...   # console.hanzo.ai → API keys

Then the first call — a read that needs nothing but the key. GET /v1/security/rules, operation get_security_rules:

hanzo security rules

Answers 200 with object — ok.

Endpoints

EndpointWhat it does
GET /v1/security/findings/{id}Returns a single finding: which rule fired, where (path and line), the masked preview and the SHA-256 fingerprint of the secret — the raw secret is…
GET /v1/security/findingsIs the org's findings — rule, severity, path, line, masked preview and fingerprint — newest first, across scans or within one.
GET /v1/security/healthReports that the scanning subsystem is serving and how many secret-detection rules the engine holds.
GET /v1/security/rulesIs the secret-detection catalog the engine scans with.
GET /v1/security/scans/{id}Returns one scan together with every finding on it, so the detail view is one round-trip rather than a list call per scan.
GET /v1/security/scansIs the org's scan history, newest first, each as the same summary the submission answered — files read, findings fired, tally by severity.
POST /v1/security/scansRuns the detection engine over a batch of files and answers 201 with the scan summary: how many files were read, how many findings fired, and the…

All Hanzo APIs · Interactive reference

How is this guide?