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:
proxy— your existing SSO in front of the UI listener. Your proxy owns the identity provider conversation.local— built-in accounts, Argon2id password hashes in PHC string form carrying the cost parameters they were made with.open— one anonymous principal. Supported for trials; it still goes through the authoriser like any other principal.
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:
- There is no path-level authorisation. The unit is the repository, so a monorepo is all-or-nothing.
- A static API key survives an identity provider disable, until it is revoked or expires. See Offboarding above.
- We are weaker than an LLM-driven product at vague questions, and the measurements are published in ../ranking-benchmark.md rather than summarised.