Security questionnaire

The answers, written once. Every claim here is checkable against the running product or against an artefact that ships with it; where the honest answer is "no" or "not yet", it says so rather than being left out.

Deployment and data

Where does our source code go? Nowhere. The product runs on your hardware and clones your repositories into a directory you name (instance.dataDir). Nothing is sent anywhere.

Is there a SaaS component? No. There is no hosted control plane, no vendor-side account and no registration step.

Does it phone home? No — not on any tier, not for licensing, not for telemetry, not for updates. Beyond that, the process makes no outbound connection unless you configure one. Exactly three things can configure one, all of them absent or off in a default install: a code-host connection, which you create and which names exactly one host; OTLP tracing to your own collector; and a remote syslog audit sink. It never connects to us under any circumstances, on any tier.

Then what does a code-host connection reach? One host, the one you named. The client is pinned to it at startup, a redirect to anywhere else is refused rather than followed, and nothing derives a destination from a response — so a compromised or misconfigured code host cannot hand your token to a third party by setting one header. If your code host is inside your own network — GitHub Enterprise Server, self-managed GitLab, Bitbucket Data Center — the socket never leaves your network and you lose nothing relative to any prior release. Setting connections.egress: internal-only makes that a control rather than a promise: a connection naming a public code host then fails at config load and the chart refuses to render one.

Prove it. The Helm chart renders a NetworkPolicy generated from your configuration: the egress ports come from the repository URLs you declared and from the collector you named, and nothing else is permitted. Or run it with --network none against a pre-seeded data directory and watch it serve. See Air-gapped install.

What egress exists at all? Four connections, three of which are not made by this process:

Connection Made by
git clone / git fetch the git binary, in a subprocess
Image pull your container runtime
SSO to your identity provider your auth proxy
OTLP spans searchcode — only if you enable it

Does it need a privileged code-host token? No. Repository access uses whatever credential you would give any other clone client — a deploy key, a read-only token. Enumerating a code host to build the repository list is searchcode discover, a CLI you run; the server never calls a code host.

Runtime posture

What does it run as? uid 65532, non-root, on a read-only root filesystem, with all Linux capabilities dropped and no-new-privileges set. Every write goes to the data volume or to a /tmp emptyDir.

Are images signed? Yes — cosign, keyless. Verification instructions are in Air-gapped install.

Is there an SBOM? Yes, CycloneDX, attached to every release as sbom.json. It is generated from go version -m against the built binary, so it lists what the linker actually included rather than the whole module graph.

Third-party licensing dependency? None. Licence verification is a signature check against a public key compiled into the binary. No licence server, no activation, no check-in.

Dependencies? One process. No database to provision, no cache, no message queue, no model, no GPU. State is one directory; the only durable file that cannot be reconstructed is subjects.db.

Authentication and authorisation

How do people sign in? One of three modes, exactly one active, never combined:

Why can't we have a local break-glass admin alongside SSO? Because a permanently password-authenticated administrator behind an SSO-protected instance is a standing bypass of the control you believe is in force. It is refused rather than unsupported.

Do you support OIDC natively? No, and this is deliberate. In proxy mode your proxy is the OIDC client. That is what makes the egress answer above true without qualification: the searchcode process never talks to an identity provider, in any mode.

How is authorisation modelled? Declarative bindings in configuration: identity groups on one side, repository groups on the other, granting a role. Allow-only — no deny rules, no exceptions, no priority — so evaluation is order-independent and access is the union of every matching binding. Validated by searchcode config validate in CI.

Where is it enforced? One function, called by every surface. Filtering is at query time, so a repository you cannot see is never read and neither result counts nor response timings leak its existence. A repository a principal cannot see returns 404, never 403 — a 403 confirms existence.

Is authorisation affected by the licence? No. Nothing is licence-gated on any tier, and past grace pauses indexing and nothing else — search, the API, MCP, audit and above all authorisation keep working.

Password policy? Argon2id at the OWASP-recommended parameters (19456 KiB / 2 passes / 1 lane by default), tunable, with the parameters stored alongside each hash so raising them invalidates nothing. Login is rate-limited before the hash runs — otherwise an expensive hash is a denial-of-service amplifier. Brute force produces backoff, not lockout: usernames appear in the commit history this product indexes, so account lockout would ship a working denial of service against any named engineer.

Password reset? searchcode user passwd, run on the box, audited. There is no SMTP configuration, no reset link and no self-service recovery — this product sends no mail.

Audit

What is logged? A separate audit stream, versioned, additive-only, over a closed action taxonomy. It records denials as well as grants — outcome: denied — which is the half most audit implementations omit and the half a reviewer actually asks about.

Can it be turned off? No. There is no licence check, no enabled flag and no output setting that opens a discarding sink. That is a security property rather than a pricing one: a gated audit log means a licence lapse silently stops recording security events.

Where does it go? stdout, stderr, a file, or syslog (RFC 5424). Retention defaults to 400 days — a 13-month cycle plus a month — and 0 keeps everything. Records are pruned by whole UTC day files, so the effective window is up to a day longer than configured; erring long is the right direction for an audit trail.

Can we exclude search query text? Yes, audit.logSearchQueries: false. Usage counts keep working.

What if the sink stalls? Emission is non-blocking onto a bounded queue. If it fills, the drop is counted, published on /api/v1/status and logged — an audit log with an unrecorded gap is worse than one with a recorded gap. Failure to open the sink at all is fatal at startup.

Does it record who READ the audit log? No, and it cannot. This is asked in reviews and the honest answer is short: the stream is written to a file or to stdout precisely so that getting logs out needs no API call, so reading it is cat, tail or a forwarder against a file you own, in a directory you own, and this process is not in that conversation. There is nothing for it to observe.

If "who read the audit log" has to be answerable in your environment, it is answerable where the read actually happens: auditd or a comparable file-access policy on dataDir/audit/, the mount policy on the volume, or your SIEM's own access log once the records are forwarded into it.

The taxonomy carries a reserved audit.exported action for the day an export path exists. No shipped release emits it, and its description in the taxonomy says so, because a reviewer who sees a rule for "the audit log was exported" never fire could otherwise conclude nobody has read it.

Application logs vs audit logs. Different streams, different retention, different stability guarantee, never mixed. Application logs are pure JSON on stdout with ts, level, msg, component and uniqueCode — see Log codes.

Offboarding

Asked in every review. The truth has three parts, and only two of them are automatic.

1. Browser access dies with the identity provider session. In auth.mode: proxy, once your IdP stops issuing a session, your proxy stops forwarding identity headers and there is nothing for searchcode to trust. In auth.mode: local, searchcode user disable alice takes effect on a running instance — the account directory refreshes from storage on a one-second tick — and kills that subject's existing sessions.

2. OAuth-based agent access dies at a token refresh. A refresh never passes your proxy — the token endpoint is on the agent listener — so it is checked against what this instance knows. In auth.mode: local that is the account: a disabled user's next refresh fails, and mcp.auth.tokenTtl (default 8h) is the upper bound on that latency. In auth.mode: proxy it is the last time your proxy vouched for the person in a browser: a refresh is refused once that is more than seven days old, and the refreshed token carries the groups the proxy asserted then. So after an identity-provider disable, an agent keeps working for at most seven days after the person's last browser visit plus tokenTtl — or not at all once searchcode user disable is run, which refuses the next refresh in either mode.

3. Static API keys survive until they are revoked or expire. This product does not poll a directory and deliberately does not integrate with one, so nothing external can invalidate an issued key. Disabling a user is therefore a two-step, and the second step is manual: revoke that user's keys.

The admin credentials view lists any user's keys with last-used times, which is what makes step two a thirty-second task rather than an archaeology exercise. Put it in your offboarding runbook next to the identity provider step.

Bound the exposure with api.apiKeys.ttl.max. It defaults to 90 days, and that ceiling is what actually limits how long a missed step can matter. Raising it makes the manual step more load-bearing, not less — that is the trade, and it is yours to make. api.apiKeys.allowNoExpiry defaults to false for the same reason: a non-expiring credential is a finding in any audit.

Vendor continuity

searchcode is a small vendor and you should ask about the bus factor. Three answers are on the table — source escrow, a source-available licence, or a contractual release-on-cessation clause — and which one applies to your contract is a commercial question to settle during it rather than something this page should pretend is already decided.

What is true today regardless of which: the product runs entirely on your hardware with no vendor-side component, licence verification is offline, and no feature is gated on a vendor service. An instance that stops receiving updates keeps working indefinitely. That is a materially different continuity risk profile from a SaaS dependency, whatever the contract says.

What is not true

Stated here because a questionnaire answered only in the affirmative is one nobody believes:

Generated from the documentation that ships with searchcode enterprise. Questions: ben@boyter.org