Code Security
Three kinds of finding share one list (GET /v1/projects/{prj}/vulnerabilities, filter type=):
Type | Source | How it gets in |
|---|---|---|
| A package your service uses has an OSV advisory |
|
| A rule matched your source |
|
| Request input reached a SQL, shell or file sink | An opt-in SDK hook posts to |
Mute or force-fix a finding with PATCH /v1/projects/{prj}/vulnerabilities/{id} and a reason.
wtctl scan
wtctl scan --dir . --project $PRJ --repo owner/name --sha $(git rev-parse HEAD) --service checkoutRuns about 30 line-level rules (secrets, SQL built by concatenation, f-string SQL in Python, command injection, unsafe deserialization, disabled TLS verification, JDBC, XXE, path traversal, open redirect) and a taint pass for Go, JavaScript and Python.
--sarif out.sarifkeeps the SARIF.Reports dependencies too (
--deps, default on). Lockfiles are read first:package-lock.json,npm-shrinkwrap.json,yarn.lock,poetry.lock,Pipfile.lock,Cargo.lock,go.sum, thengo.mod,package.jsonandrequirements.txt. So a dependency in a service that never started is still checked.Exit code 1 when the quality gate blocks on new high-severity findings (
--fail-on-gate=falseto only report), 2 on a usage or API error.
Limits: the rules are single-line regex, a statement split over lines is missed. Python taint skips files the 3.4-grammar parser cannot read (f-strings, async, annotations), which the f-string rules cover at line level. Semgrep or CodeQL SARIF can be uploaded to the same endpoint for deeper coverage.
IAST
Opt-in, off until started. It matches request inputs (4+ characters, URL-decoded too) against the text of a sink, so a parameterised query never fires. Each type and code location is reported once per process.
Node:
startIast,iastMiddleware,guardSink(sdks/node/src/iast.ts).Python:
watchtower.iast.start_iast,IastMiddleware(WSGI) orIastASGIMiddleware,guard_sink.Java:
Iast.start, registerIast.filter()as a servlet filter, callIast.checkSinkwhere you run a sink.
Limits: substring matching, not byte-level taint, so transformed input is missed; Java and Python need you to wrap your sinks (no agent yet).
Reachability
Each library finding says how sure we are the vulnerable code runs, in reachability_level:
import: the package is loaded. The default.reference: the advisory names functions and your source references one of them (wtctl deps upload).profile: a CPU profile of that service, last 7 days, has a frame matching the advisory's function (or, when the advisory names none, the package).profile_shareis the fraction of the service's CPU samples with that frame on the stack, andreachability_evidencereads, for example, "vulnerable functionparseseen in 3.2% of CPU incheckoutin the last 7 days".
A service with profiles but no match keeps its static verdict and says "not seen in the CPU profiles ...". A missed sample is not proof the code is unreachable, so a profile never lowers reachable. A service with no profiles shows profile_window_days: 0 and no evidence. The CPU share is inclusive over all matching frames and capped at 100%.
Offline use
WT_OSV_MIRROR=<folder> makes the sweep read a local OSV export instead of api.osv.dev; see air-gapped install. WT_OSV_SYNC=off disables the sweep.