vyql

module
v0.2.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: Apache-2.0

README

VyQL

ci detection data security Go Reference License

A multi-language security scanner that follows tainted data through your code and tells you why each finding is a finding.

VyQL is two things: a small Go engine, and a large body of security knowledge written as data. The engine builds one language-agnostic graph of your program and answers questions about it. Everything it knows about frameworks, sinks, sanitizers and vulnerability classes lives in vyql/ as text you can read, edit and add to without touching Go.

pattern  →  concept  →  binding  →  rule

A pattern is a shape in the code. A binding attaches a concept (code.SqlExecution, core.HtmlEscape) to it. A rule says which combinations of concepts are a vulnerability. Adding coverage for a new framework is usually a new binding, not new Go.

Install

curl -fsSL https://dl.vyprsec.ai/vyql/install.sh | sh

Picks the right build for your platform, verifies its published SHA-256, unpacks it under ~/.local/share/vyql, and puts vyql in ~/.local/bin. Set VYQL_VERSION, VYQL_INSTALL_DIR or VYQL_BIN_DIR to change any of that.

Release assets come from the GitHub release, which is where the checksums and build provenance live. VYQL_INSTALL_BASE_URL points the download at a mirror serving the same <base>/<version>/<asset> layout.

The checksum is served from the same host as the archive, so it catches a corrupted download rather than a compromised release. Pin VYQL_VERSION and check the sum against a second source if you need more than that.

Linux and macOS, amd64 and arm64. On Windows it says so rather than failing obscurely — use WSL, or the GitHub Action.

Download a release manually

The scanner loads its security knowledge from a vyql/ directory at startup. The release archive carries both halves, so it works on a machine that has never seen VyQL:

# platforms: linux_amd64, linux_arm64, darwin_arm64, darwin_amd64
V=v0.2.3; P=darwin_arm64
curl -fsSLO https://github.com/vyprai/vyql/releases/download/$V/vyql_${V}_${P}.tar.gz
curl -fsSLO https://github.com/vyprai/vyql/releases/download/$V/vyql_${V}_${P}.tar.gz.sha256
shasum -a 256 -c vyql_${V}_${P}.tar.gz.sha256

tar -xzf vyql_${V}_${P}.tar.gz
cd vyql_${V}_${P}
./bin/vyql scan .
go install

Requires Go 1.26+ and a C toolchain (the parsers are C).

go install github.com/vyprai/vyql/cmd/vyql@latest

The binary finds its data in the module cache, so no further setup is needed.

Container
docker run --rm -v "$PWD:/work" ghcr.io/vyprai/vyql scan .

About 270 MB, most of it the security knowledge base. Runs as a non-root user, so it will not leave root-owned files in a mounted tree. debian:stable-slim rather than Alpine: the parsers are C linked against glibc.

From source
git clone https://github.com/vyprai/vyql
cd vyql
make build          # -> bin/vyql
As an agent skill

For Claude Code, VyQL ships as a plugin that teaches the agent to run a scan and triage what comes back:

/plugin marketplace add vyprai/claude-plugins
/plugin install vyql@vypr

Then ask for a security scan in the ordinary way. The skill installs the vyql binary if it is missing, after asking — a security tool that downloads and runs binaries unprompted has the wrong instincts.

It lives in vyprai/claude-plugins rather than here, so installing it copies 32 KB instead of cloning a repository whose knowledge base is several hundred megabytes.

skills/vyql-security-scan/ there follows the Agent Skills format, which is not Claude-specific: any tool that reads a SKILL.md can use it directly, without the plugin manifests.

Check what you have
vyql version
vyql v0.2.3
commit: 337710842c77d1155e6691372c0f2aeeaa704702
built:  2026-08-06T13:00:00Z
go:     go1.26.4
platform: darwin/arm64

Worth quoting in a bug report: findings depend on the version of the security knowledge as much as on the engine.

Scan something

vyql scan ./my-project
vyql scan                  # no path: scans the working directory, and says so
analysis profile: HTTP API server (api)

1 finding(s):

[P3] [HIGH] VYQL-PATH-001  (conf=medium, fp=fd2b76a631e27d16)
    source: code.HttpInput @ app.js:5   <- code.HttpInput by javascript.input@resolved
    sink: code.FilePathAccess @ app.js:6
    taint path: app.jsAttr#85 -> app.jsArg#86
    unless path coveredBy core.PathCanonicalization: not satisfied
    unless endpoint coveredBy core.PathAccessCheck: not satisfied

Every finding shows the source, the sink, the path between them, and the neutralizing controls it looked for and did not find. If a scanner tells you something is vulnerable, it should be able to tell you what would have made it safe.

vyql scan --format sarif ./my-project > results.sarif
vyql scan --format json  ./my-project | jq '.[].rule'
Failing a build

scan exits 1 when it finds anything HIGH or CRITICAL. Dropping it into a pipeline gates that pipeline, with no extra configuration.

vyql scan .                        # exit 1 on HIGH or CRITICAL  (the default)
vyql scan -fail-on critical .      # exit 1 only on CRITICAL
vyql scan -fail-on none .          # report everything, always exit 0

Severities, lowest to highest: info low medium high critical. -fail-on takes any of them, or none.

A failed scan also exits 1, so if your pipeline needs to tell "found something" apart from "could not run", give -exit-code a distinct value:

vyql scan -exit-code 3 .   # 3 = findings, 1 = VyQL failed, 2 = bad usage
Adopting it on a codebase that already has findings

Triage is worth nothing if it evaporates. A baseline records what you have already looked at, keyed on the finding fingerprint — which is anchored to rule and location, not line number, so a verdict survives edits elsewhere in the file.

vyql scan -baseline-write .vyql-baseline.json .   # take the backlog as given
vyql scan -baseline .vyql-baseline.json .         # fail only on what is new

Entries are written as accepted with an empty reason; fill them in as you triage, and change the verdict to false-positive where the finding is wrong:

{ "fp": "cfb54bfb4024aa90", "verdict": "false-positive",
  "reason": "source is a build-time constant, not request data",
  "rule": "VYQL-INJ-002", "loc": "server.js:5" }

Baselined findings are kept out of both the report and the gate. Anything not in the file is new and reported normally.

Entries that stop matching are reported, not forgotten:

warning: 4 baseline entries match nothing in this scan
         the code they excused may have changed; re-triage or remove them:
           27d5f6e6503511f5  VYQL-INJ-004  app.py:12

A suppression that outlives the code it excused is how these files turn dangerous — the code moved, the excuse did not, and nobody looked again.

A malformed baseline, an unknown verdict or a missing file is an error rather than an empty baseline. Silently suppressing nothing buries you in findings you thought were triaged; silently suppressing everything is worse.

What was actually read

"No findings" is only as good as what the scan looked at, so anything left unanalysed is reported whether you ask or not:

scanned python:1 textpattern:1 — 9 finding(s)
warning: 15 file(s) matched no frontend and were not analysed (.zig 12, .cob 3)
         run with -coverage for the breakdown

-coverage gives the full account:

vyql scan -coverage .
coverage
  parsed    python 1 · textpattern 1
  excluded  5 file(s) dropped by -exclude
  unread    15 file(s) matched no frontend: .zig 12, .cob 3
  depth     java, python, javascript are the reference frontends;
            other languages range down to call-and-concat coverage
  note      a parse that partially fails still counts as parsed;
            this does not yet report that

A clean report over a tree that was mostly skipped reads exactly like a clean report over a tree that was fully read. The warning is the difference, which is why it is not optional. The note is there because the gap is real: tree-sitter recovers from syntax errors, so a file that parsed badly still counts as parsed.

Understand a finding

The diagnostic commands are the point of the design: you interrogate the analysis instead of guessing at it. Every one takes paths just as scan does, so . scans the current directory.

explain — each finding's full proof tree, including the negation evidence: every unless clause the rule carries and whether it was satisfied. Usually the fastest answer to "why did this fire".

vyql explain .
vyql explain -rules vyql/packs/injection .   # narrow to one pack or file

match — every node a binding attached a concept to. If your source or sink is not listed, no rule can fire, because rules match concepts.

vyql match .

resolve — which calls resolved to a body and which did not. Taint stops at an unresolved call, so this is where a missing cross-function flow shows up.

vyql resolve .

trace — follows taint from source to sink, or shows where it stops. Both filters match on a substring of the concept name.

vyql trace .
vyql trace -from HttpInput -to SqlExecution .
vyql trace -to FilePathAccess .              # every path into file access

query — the graph by predicate, for when you want to look rather than be told.

vyql query -type code.Call .                 # by node type
vyql query -concept HttpInput .              # by concept label
vyql query -call db.Query .                  # by callee path or method
vyql query -loc handlers.go .                # by location substring
vyql query -concept SqlExecution -edges .    # include outgoing edges
vyql query -concept HttpInput -count .       # just how many
vyql query -from HttpInput -to SqlExecution .  # reachability between concepts

graph — the whole graph, or taint reachability per source. Verbose, and definitive when the others all look right.

vyql graph .
vyql graph -taint .

Two more worth knowing:

vyql definitions -kind all                   # what concepts, rules and bindings loaded
vyql definitions explain code.SqlExecution   # which binding produced a label
vyql bindings -lang python                   # one language's source/sink/check vocabulary

A filter that matches no known concept is an error rather than an empty result, because "0 sources reach a sink" and "you typed the name wrong" should not look the same:

$ vyql trace -from HttpInpt .
vyql: -from "HttpInpt" matches no concept
  did you mean: code.HttpInput, code.HttpHeader, code.HttpPersistentAuthReuse?
  list them with: vyql definitions -kind concepts
Comparing two scans

diff compares two --format json runs by finding fingerprint. Fingerprints are anchored to rule and location rather than line number, so the comparison survives edits elsewhere in the file:

vyql scan --format json . > before.json
# ... change something ...
vyql scan --format json . > after.json
vyql diff before.json after.json
before: 9 findings   after: 7 findings
- removed: 2   + added: 0   (= 7 unchanged)
  - [VYQL-INJ-004] app.py:18 → app.py:19
  - [VYQL-INJ-202]  → app.py:19

This is how to ask "did this branch introduce anything new" without failing on a backlog that was already there.

A missed finding is nearly always one of three things, and match, resolve and explain distinguish them in that order: nothing was labelled, the call did not resolve, or an unless clause was satisfied.

Languages

Java, Python, JavaScript/TypeScript, C#, PHP, Ruby, Go, Rust, Kotlin, Scala, Swift, Dart, Groovy, Elixir, Lua, Perl, C, C++, Objective-C, Solidity, Bash, PowerShell.

Depth is not uniform. Java, Python and JavaScript are the reference frontends and carry the most complete modelling; the rest range from full taint tracking to call-and-concat coverage. vyql definitions -kind all reports what is actually loaded.

Where the knowledge lives

vyql/
  ontology/   concepts and threat kinds -- the vocabulary
  bindings/   what in each language means which concept
  packs/      rules: which concept combinations are vulnerabilities
  taxonomy/   CWE and CAPEC
  tests/      executable specs, one per rule

None of this is compiled in. The binary loads it at startup from the directory VYQL_HOME names, or by finding vyql/ above the working directory. Point VYQL_HOME at your own copy and your edits take effect on the next run.

Adding coverage

A binding says "this shape in this language is this concept":

binding requestJson {
  requires { language("python") }
  query pattern callExpr where callee.path ~= "request.json"
  emit source code.HttpInput at call.result
}

Write it, add a spec in vyql/tests/, run go test -count=1 ./.... See docs/07-adapters-and-patterns.md for the binding language.

Check a binding file parses, and what it emits, without running a scan. Bindings live under vyql/bindings/<language>/, one module per file:

vyql validate-binding -file vyql/bindings/python/python/558.vyql
{
  "ok": true,
  "bindings": [
    { "name": "python", "mapping_count": 1, "mappings": [ ... ] }
  ]
}

Then confirm it attaches what you expect on real code — match lists what was labelled, and definitions explain names the binding responsible:

vyql match ./some-project
vyql definitions explain code.HttpInput

Documentation

docs/README.md is the index. The design series (docs/03 through docs/20) is the reference for how the engine works; read it before changing the engine or the language.

Stability

The CLI is stable. Commands, flags and output formats follow semantic versioning: scan, its --format values, the JSON and SARIF shapes, and the finding fingerprint will not change incompatibly within a major version. Build tooling against them.

The rule and binding language is not. It is still moving, and a future version changes parts of the syntax. Concepts get renamed and clauses get added, so a rule pack written today may need edits to keep working. The specs in vyql/tests/ are what tell you when something breaks.

The knowledge base evolves. A newer release can report findings an older one did not -- that is the point of it -- but it means pinning a version is the only way to get identical output twice.

Some documents describe design rather than behaviour. The reference series in docs/ includes work that is not implemented; those documents say so at the top, and the index lists them separately. What a scan actually does is whatever vyql definitions -kind all reports as loaded.

Contributing

See CONTRIBUTING.md. Two things worth knowing before your first change:

  • go test must always be go test -count=1. The Go test cache keys on source and does not track the .vyql data files, so a cached pass can hide a change to the security knowledge base entirely.
  • CGO_ENABLED=1 is required. The parsers are C; without cgo the build constraints exclude them silently rather than failing.

make ci runs exactly what the pipeline runs.

Accuracy

Measured on the public OWASP Benchmark suites, scored by Youden index (J = TPR − FPR, macro-averaged over categories):

Suite Cases Score
BenchmarkJava 2,740 +1.00
BenchmarkPython 1,230 +0.90

Reproduce them yourself — benchmarks/fetch-corpora.sh fetches the corpora (they are GPL and are not vendored here), and benchmarks/RESULTS.md records the method, the per-category numbers, and the known corpus defects. Scores from our synthetic language ports are recorded separately there and are not comparable to these.

License

Apache-2.0. See LICENSE, NOTICE and THIRD_PARTY_NOTICES.md — the last covers the vendored tree-sitter grammars, the MITRE CWE/CAPEC taxonomies, and the ecosyste.ms package snapshot, which is CC-BY-SA 4.0.

Directories

Path Synopsis
cmd
gen-ontology-json command
Command gen-ontology-json dumps the ontology content (concepts + threat kinds) to JSON, the source-of-truth format the ontology package loads.
Command gen-ontology-json dumps the ontology content (concepts + threat kinds) to JSON, the source-of-truth format the ontology package loads.
vyql command
Command vyql is a multi-language security scanner.
Command vyql is a multi-language security scanner.
internal
attackpath
Package attackpath composes typed step relations into cross-domain attack Paths (docs/13).
Package attackpath composes typed step relations into cross-domain attack Paths (docs/13).
bindings
Package bindings applies compiled v2 binding labelers to graph nodes.
Package bindings applies compiled v2 binding labelers to graph nodes.
datadir
Package datadir resolves the VyQL data root — the standalone `vyql/` directory (rule packs + ontology + taxonomy) that lives OUTSIDE the Go source tree and is loaded from disk at runtime (no go:embed).
Package datadir resolves the VyQL data root — the standalone `vyql/` directory (rule packs + ontology + taxonomy) that lives OUTSIDE the Go source tree and is loaded from disk at runtime (no go:embed).
definitions
Package definitions inspects the shipped VyQL definition corpus -- concepts, bindings, rule packs and reviews -- and reports what it contains.
Package definitions inspects the shipped VyQL definition corpus -- concepts, bindings, rule packs and reviews -- and reports what it contains.
engine
Package engine compiles and evaluates VyQL rules (docs/03, /05).
Package engine compiles and evaluates VyQL rules (docs/03, /05).
extract/frontend
Package frontend turns extracted code.* graphs into concept labels using framework bindings (docs/07).
Package frontend turns extracted code.* graphs into concept labels using framework bindings (docs/07).
extract/frontend/config
Package config is a non-tree-sitter frontend for declarative project files.
Package config is a non-tree-sitter frontend for declarative project files.
extract/frontend/golang
Package golang is a NATIVE Go-source extraction frontend: it parses real .go files with the standard go/parser + go/ast and lowers them to the shared NIR (docs/20).
Package golang is a NATIVE Go-source extraction frontend: it parses real .go files with the standard go/parser + go/ast and lowers them to the shared NIR (docs/20).
extract/frontend/textpattern
Package textpattern runs a data-defined text-pattern profile over files that do not need a language parser.
Package textpattern runs a data-defined text-pattern profile over files that do not need a language parser.
extract/frontend/treesitter
Package treesitter holds REAL parser frontends built on tree-sitter (docs/20: the recommended production parser — a uniform node API across 100+ languages, error recovery, build-free).
Package treesitter holds REAL parser frontends built on tree-sitter (docs/20: the recommended production parser — a uniform node API across 100+ languages, error recovery, build-free).
extract/frontend/treesitter/grammars/dart
Package dart provides the tree-sitter Dart language.
Package dart provides the tree-sitter Dart language.
extract/frontend/treesitter/grammars/elixir
Package elixir provides the tree-sitter Elixir language.
Package elixir provides the tree-sitter Elixir language.
extract/frontend/treesitter/grammars/groovy
Package groovy provides the tree-sitter Groovy language.
Package groovy provides the tree-sitter Groovy language.
extract/frontend/treesitter/grammars/objc
Package objc provides the tree-sitter Objective-C language (vendored from tree-sitter-grammars/tree-sitter-objc, committed parser.c, no Go binding).
Package objc provides the tree-sitter Objective-C language (vendored from tree-sitter-grammars/tree-sitter-objc, committed parser.c, no Go binding).
extract/frontend/treesitter/grammars/perl
Package perl provides the tree-sitter Perl language.
Package perl provides the tree-sitter Perl language.
extract/frontend/treesitter/grammars/powershell
Package powershell provides the tree-sitter PowerShell language.
Package powershell provides the tree-sitter PowerShell language.
extract/frontend/treesitter/grammars/solidity
Package solidity provides the tree-sitter Solidity language.
Package solidity provides the tree-sitter Solidity language.
extract/frontend/treesitter/grammars/swift
Package swift provides the tree-sitter Swift language.
Package swift provides the tree-sitter Swift language.
extract/lowering
Package lowering is the shared, language-AGNOSTIC tier (docs/20): it lowers NIR into the shared graph, owning the function/class registries, per-file import tables, the type map (self, constructors, class/static receivers), call resolution (import -> type -> guarded unique-name fallback), and dataflow construction (scopes, assignments, FLOWS edges).
Package lowering is the shared, language-AGNOSTIC tier (docs/20): it lowers NIR into the shared graph, owning the function/class registries, per-file import tables, the type map (self, constructors, class/static receivers), call resolution (import -> type -> guarded unique-name fallback), and dataflow construction (scopes, assignments, FLOWS edges).
extract/nir
Package nir is the Normalized IR (docs/20) — the coherent representation Every language frontend targets.
Package nir is the Normalized IR (docs/20) — the coherent representation Every language frontend targets.
extract/parsecache
Package parsecache is a content-addressed, BadgerDB-backed cache of per-file NIR parse results.
Package parsecache is a content-addressed, BadgerDB-backed cache of per-file NIR parse results.
extract/regexambig
Package regexambig decides whether a regular expression can backtrack catastrophically.
Package regexambig decides whether a regular expression can backtrack catastrophically.
extract/sca
Package sca implements the dependency/SBOM path and the vulnerable-library Entrypoint projection (docs/20, docs/11).
Package sca implements the dependency/SBOM path and the vulnerable-library Entrypoint projection (docs/20, docs/11).
findings
Package findings is the Finding + proof tree (docs/14).
Package findings is the Finding + proof tree (docs/14).
graphsync
Package graphsync turns a VyQL analysis graph into a stable, content-addressed change-feed for syncing to an external database.
Package graphsync turns a VyQL analysis graph into a stable, content-addressed change-feed for syncing to an external database.
nexus
Package nexus emits full-fidelity finding JSON for the Nexus API (docs/14) — finding + proof tree + node refs, so UIs can pivot from a finding to its graph neighborhood.
Package nexus emits full-fidelity finding JSON for the Nexus API (docs/14) — finding + proof tree + node refs, so UIs can pivot from a finding to its graph neighborhood.
ontology
Package ontology is the VyQL security ontology + type system (docs/06).
Package ontology is the VyQL security ontology + type system (docs/06).
pack
Package pack is the rule-pack manifest + version pinning (docs/05 §module System, docs/15).
Package pack is the rule-pack manifest + version pinning (docs/05 §module System, docs/15).
parser
Package parser implements the VyQL v2 definition parser and compiles authored definitions into the scanner IR consumed by the engine and graph labeler.
Package parser implements the VyQL v2 definition parser and compiles authored definitions into the scanner IR consumed by the engine and graph labeler.
profile
Package profile implements application-archetype analysis profiles.
Package profile implements application-archetype analysis profiles.
resultpolicy
Package resultpolicy loads v2 result policies and applies them to scanner outputs.
Package resultpolicy loads v2 result policies and applies them to scanner outputs.
risk
Package risk is the derived prioritization layer over findings + graph context.
Package risk is the derived prioritization layer over findings + graph context.
sarif
Package sarif emits VyQL findings as SARIF 2.1.0 (docs/14, /16) — the Table-stakes output for code findings and CI.
Package sarif emits VyQL findings as SARIF 2.1.0 (docs/14, /16) — the Table-stakes output for code findings and CI.
solvers
Package solvers implements the flow solvers (docs/08).
Package solvers implements the flow solvers (docs/08).
taxonomy
Package taxonomy embeds the full MITRE CWE and CAPEC catalogs as reference data (docs/16).
Package taxonomy embeds the full MITRE CWE and CAPEC catalogs as reference data (docs/16).
usg
Package usg implements the Universal Security Graph (docs/04) for the Go production build of VyQL.
Package usg implements the Universal Security Graph (docs/04) for the Go production build of VyQL.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL