Code Security

Embed
Preview of the card
<a href="https://watchtowerops.net/docs/code-security"><img src="https://watchtowerops.net/docs/_api/public/orgs/aa5959cb-1a0b-4a9a-bc9a-913e7ac31cd9/spaces/b7aac181-9916-4805-b253-becc5fd2ca62/pages/6a3ee0d9-972c-49a3-ae63-e28d56062064/social-card.svg?v=3e7ae33eec8b" alt="Code Security" width="600" height="315"></a>

Three kinds of finding share one list (GET /v1/projects/{prj}/vulnerabilities, filter type=):

Type

Source

How it gets in

library (SCA)

A package your service uses has an OSV advisory

wtctl deps upload or wtctl scan reports packages; the worker matches them daily

code (SAST)

A rule matched your source

wtctl scan uploads SARIF (any SARIF scanner works)

runtime (IAST)

Request input reached a SQL, shell or file sink

An opt-in SDK hook posts to /code-security/runtime

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 checkout
  • Runs 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.sarif keeps 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, then go.mod, package.json and requirements.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=false to 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) or IastASGIMiddleware, guard_sink.

  • Java: Iast.start, register Iast.filter() as a servlet filter, call Iast.checkSink where 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_share is the fraction of the service's CPU samples with that frame on the stack, and reachability_evidence reads, for example, "vulnerable function parse seen in 3.2% of CPU in checkout in 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.

Updated