A security scanner for 22 languages. It follows untrusted input through your code, and for every finding it explains why it thinks the finding is real.
There are two parts. A small Go engine builds one graph of your program and
answers questions about it. Everything the engine knows about frameworks, sinks,
sanitizers and vulnerability classes lives in vyql/ as plain text you can read
and edit.
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 add up to a vulnerability. To cover a new framework you usually
write a binding, not Go.
curl -fsSL https://dl.vyprsec.ai/vyql/install.sh | shPicks 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 comes from the same host as the archive. This catches a corrupted
download, but it does not catch a compromised release. If you need more than
that, pin VYQL_VERSION and compare the checksum with a second source.
Linux and macOS, amd64 and arm64. On Windows it tells you so, instead of failing obscurely. Use WSL, or the GitHub Action.
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.5; 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 .Requires Go 1.26+ and a C toolchain (the parsers are C).
go install github.com/vyprai/vyql/cmd/vyql@latestThe binary finds its data in the module cache, so no further setup is needed.
docker run --rm -v "$PWD:/work" ghcr.io/vyprai/vyql scan .About 270 MB, and most of that is the security knowledge base. It runs as a
non-root user, so it does not leave root-owned files in a mounted tree. The base
is debian:stable-slim and not Alpine, because the parsers are C linked against
glibc.
git clone https://github.com/vyprai/vyql
cd vyql
make build # -> bin/vyqlFor 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 normal way. If the vyql binary is missing,
the skill installs it, but it asks you first. A security tool should not download
and run binaries without asking.
It lives in vyprai/claude-plugins and not here, so installing it copies 32 KB. Cloning this repository would copy a knowledge base of 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.
vyql versionvyql v0.2.4
commit: 6906231d71f36f0f7f93f6301870efff4ae333ad
built: 2026-08-07T01:43:38Z
go: go1.26.4
platform: darwin/arm64
Quote this in a bug report, because findings depend on the version of the security knowledge as much as on the engine.
The same shape whether you drive the CLI yourself or ask an agent.
scope → scan → coverage → list → verify → reproduce → fix
Scope. Whole repo, or just what a change introduced. These are different
questions. On a codebase that already has a backlog, scanning everything at
review time mixes the two findings your branch added into the two hundred that
were there before. For a diff, scan both sides and use vyql diff.
Scan. vyql scan . with no configuration. There is nothing to pick.
Coverage. Read the scanned line before the findings. A clean report over a
tree that was mostly skipped looks the same as a clean report over a tree that
was fully read. That line is the only way to tell them apart.
List. Everything it found, by severity, each with a location.
Verify. Pick one, or all the high ones. Findings are grouped by rule family and verified one agent at a time, four families at most, because a systematic false positive usually appears as a whole family. This is static verification: it shows that the path holds up, not that the bug can be exploited.
Reproduce. Optionally, boot the app from a clean worktree and exploit that. If it will not boot cold, a failing test instead. Local only, never the instance you are already running.
Fix. Optionally, and only if you ask. The default is to name the control and leave the edit to you.
You can stop at any point where you have your answer. A list is a complete answer to "what is wrong here", and most scans end there.
The same flow, without remembering the commands:
/plugin marketplace add vyprai/claude-plugins
/plugin install vyql@vypr
Then it is a conversation, and each turn builds on the last:
you audit this repo for security problems
Scanning the whole tree. vendor/, node_modules/ and testdata/ are
skipped already. On top of those I would skip:
**/*_templ.go generated templates, one huge function each
examples/ sample code, not shipped
Anything else to leave out, or shall I go?
you go
scanned python:14 javascript:3, 2 files unread (.kt)
7 findings. 2 critical, 3 high, 2 medium.
1. VYQL-INJ-002 critical api/users.py:88 request body reaches a shell
2. VYQL-PATH-001 high api/files.py:23 query param reaches open()
...
you verify the high ones
4 families: INJ (2), PATH (3), CRY (1), CFG (1). Verifying all four,
one agent each. Nothing deferred.
INJ 1 real, 1 false positive
PATH 3 false positives, all through the same startswith() guard
CRY 1 real
CFG 1 unresolved, needs to know whether debug mode ships
The three PATH findings share a cause. That guard is a containment
check VyQL does not model, so it is a binding gap, not three bugs.
you what about the INJ one
Real. api/users.py:88 is a request handler and the path carries the
value with no escaping. Fix is core.ShellEscape: subprocess with a
list rather than shell=True.
you prove it
Booting from a clean worktree at the scanned commit.
POST /users/import with a payload of "; id" returns uid=0(root).
Torn down.
you fix it then
One file, api/users.py, introducing subprocess with an argument list.
Shall I?
You do not need to repeat which finding you mean, because the list is still in the conversation. A single question also works, but the flow above is what the skill is built for.
A reproduction runs only against your own machine, and the skill asks you before it runs one. Writing the reproduction is the result. Whether to run it is your decision.
The skill runs the right commands and reports coverage before findings. It also keeps a strict meaning for a verdict. It asks you before it installs anything, before it scans, and before it writes a reproduction.
It asks about scope because scope decides what the scan cannot find. A directory skipped there is one that the report calls clean without having read it. It also never shortens a list of findings without telling you. Past about 25 findings it shows the critical and high ones, states how many of each severity it is holding back, and offers you the rest.
skills/vyql-security-scan/ in that repository is a plain
SKILL.md, which is an open format. Any agent that reads
it can follow the same flow. Claude Code is only the one that has an installer
for it.
vyql scan ./my-project
vyql scan # no path: scans the working directory, and says soanalysis 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
controls that would have stopped it. If you already have one of those controls
in place, explain tells you why it did not apply here.
vyql scan -format sarif ./my-project > results.sarif
vyql scan -format json ./my-project | jq '.[].rule'scan exits 3 when it finds anything HIGH or CRITICAL. If you put it in a
pipeline, it gates that pipeline, and you do not have to configure anything.
vyql scan . # exit 3 on HIGH or CRITICAL (the default)
vyql scan -fail-on critical . # exit 3 only on CRITICAL
vyql scan -fail-on none . # report everything, always exit 0Severities, lowest to highest: info low medium high critical. -fail-on
takes any of them, or none.
Every command reports the same four codes, so a pipeline can act on the status alone without parsing output:
| code | meaning |
|---|---|
0 |
the command run successfully |
1 |
VyQL could not complete: bad path, unreadable file, rules that do not compile |
2 |
usage error: unknown command or flag, missing path, a value outside its set |
3 |
the check ran and did not pass: findings at or above -fail-on, a corpus that does not validate, a finding set that changed |
A baseline records what you have already triaged, keyed on the finding fingerprint. The fingerprint is anchored to rule and location rather than line number, so your 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 newApplying a baseline lowers the gate to any new finding. Without one, scan
asks "is anything in this code wrong". A severity floor is how a team decides not
to fix everything at once. With a baseline it asks "did this change add
anything", and then every addition counts: a new medium is something this branch
introduced, and not part of a backlog someone already accepted. With the normal
default, the build would pass on exactly those findings, while the report above
it listed them.
The run states this on stderr, so you do not have to work it out yourself, and it names how to get the normal threshold back:
vyql: warning: baseline applied; gating on any new finding
pass -fail-on high to keep the usual threshold, or -fail-on none to report only
A threshold you name always wins, so a pipeline that only wants new criticals can ask for that:
vyql scan -baseline .vyql-baseline.json -fail-on critical .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" }A recording run still reports. -baseline-write writes the file and then prints
the findings it recorded, in whatever -format you asked for, so the run that
adopts the scanner can still publish its SARIF and you can see what was accepted
on your behalf:
vyql scan -baseline-write .vyql-baseline.json -format sarif . > results.sarifThat run does not fail the build. Everything it reported was accepted at the same time, and failing on the backlog would make it impossible to adopt the scanner in the pipeline that needs it. It does fail, before it prints anything, if the baseline cannot be written.
Rolling a baseline forward. Give both flags, with different paths, and the run applies the old baseline and records the next one:
vyql scan -baseline .vyql-baseline.json -baseline-write next.json .This run reports and gates on what the old baseline does not cover, because that
is what is new. The file it writes carries three things: every finding the old
baseline covered, keeping its verdict and reason, so triage survives the roll;
every new finding below -fail-on, as accepted; and nothing else. A new
finding that meets -fail-on is left out on purpose. If it were recorded, the
next run would be green, the finding would be absorbed, and nobody would be told.
Entries in the old baseline that match no current finding are dropped, so a
rolled baseline does not keep suppressions for code that is gone.
Pointing both flags at the same file is refused. Recording writes every finding
as accepted with an empty reason, so it would overwrite the verdicts you
triaged. Record to a different path and diff it.
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
Without this, a suppression lives longer than the code it was written for: the code moves, the excuse stays, and nobody looks again.
A malformed baseline, an unknown verdict or a missing file is an error, and not an empty baseline. It is better to fail with a message than to silently suppress everything, or nothing.
-exclude takes one pattern and may be repeated. One rule decides what a
pattern means:
vyql scan -exclude node_modules . # that directory, at any depth
vyql scan -exclude '*_templ.go' . # that file, at any depth
vyql scan -exclude 'src/gen/**' . # rooted, because it has a slash
vyql scan -exclude '**/*.{test,spec}.ts' . # brace alternation
vyql scan -exclude node_modules -exclude vendor . # repeat for moreA value with no slash and no glob character names a directory. A value with no
slash but with a glob character names a file at any depth, because * does not
cross a slash: written literally, a bare suffix pattern would match only at the
scan root. A value that contains a slash is anchored at the scan root and matches
as written.
An excluded directory is never entered, so its files are not read and not even
listed. A malformed pattern is rejected before the scan starts, and -coverage
reports how much each pattern excluded.
One pattern per flag: a comma is rejected, because it would be ambiguous with a valid glob pattern.
"No findings" only means something if you know what was read. Anything left unanalysed gets 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) and 1 director(ies) dropped by -exclude
node_modules 1 dir(s)
**/*_templ.go 5 file(s)
*.min.js 0 file(s) ← matched nothing
oversized 2 file(s) skipped over the -max-file-size ceiling;
raise it or pass 0 to scan them
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
Source files above 2 MiB are skipped during tree walks by default. A file that
large is almost never written by hand, and parsing one costs more than the whole
rest of the tree. -max-file-size 8MB raises the ceiling and 0 turns it off.
Naming a file directly always scans it, whatever its size.
A clean report over a tree that was mostly skipped looks the same as a clean
report over a tree that was fully read. The warning is what separates them, and
that is why you cannot turn it off. 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.
The diagnostic commands are why the design works this way: you ask the analysis
questions instead of guessing. Each one takes paths in the same way as scan, so . scans
the current directory.
explain prints 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 filematch lists 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 shows 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 accessquery searches 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 manygraph dumps the whole graph, or taint reachability per source. Verbose, and
definitive when the others all look right.
vyql graph .Two more that are useful to know:
vyql definitions -kind all # what concepts, rules and bindings loaded
vyql definitions explain code.SqlExecution # which binding produced a label
vyql definitions -kind bindings -lang python # one language's source/sink/check vocabularyA filter that matches no known concept is an error, and not an empty result, because "0 sources reach a sink" and "you typed the name wrong" must 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
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.jsonbefore: 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 almost always one of three things, and match, resolve and
explain separate them in that order: nothing was labelled, the call did not
resolve, or an unless clause was satisfied.
vyql help lists the commands; vyql help <command> prints that command's
flags. This section is the same information with the reason for each one.
Four flags are shared. -data points at the vyql/ data directory when it is
not where the binary would look, -profile picks the analysis profile, and
-cpuprofile / -memprofile write pprof files.
Every command that reads the data directory takes -data, including each
subcommand of definitions. -profile is on the commands that analyse source.
diff compares two reports and cache manages files on disk, so neither takes
any of the four:
vyql scan -data /opt/vyql . # data directory somewhere else
vyql definitions -data /opt/vyql -kind concepts
vyql scan -profile api . # skip auto-detection
vyql scan -cpuprofile cpu.prof -memprofile heap.prof .$VYQL_HOME, $VYQL_CPUPROFILE and $VYQL_MEMPROFILE do the same and are the
fallback when the flag is not given.
| flag | default | what it does |
|---|---|---|
-rules |
vyql/packs |
load rules from a .vyql file or directory |
-bindings |
a repo-local binding overlay directory | |
-format |
text |
text, sarif, json or graph-json |
-fail-on |
high, or any new finding with -baseline |
exit 3 at or above this severity, or none |
-exclude |
skip paths matching this pattern; repeatable | |
-baseline |
apply triaged findings, and report only what is new | |
-baseline-write |
record the current findings to this path | |
-coverage |
off | what was parsed, excluded and left unread |
-stats |
off | graph counts, taint hubs, per-phase timing |
-flags |
off |
review flags: off, with (findings and flags), only |
-flag-category |
all |
filter review flags by category |
-flag-kind |
all |
all, attention, target or check |
-flag-loc |
filter review flags by location substring | |
-cache |
auto |
auto, off, or a directory |
-cache-incremental |
off | also cache per-file parses, for an edit loop |
-max-ram |
80% of RAM | soft ceiling, e.g. 8GB or 16GiB |
-max-file-size |
2MiB |
skip larger source files; 0 disables |
Combinations that are useful to know:
# CI: machine output, gated, with the coverage account on stderr
vyql scan -format sarif -fail-on high -coverage . > results.sarif
# CI on a codebase with a backlog: fail only on what this branch added
vyql scan -baseline .vyql-baseline.json -format sarif . > results.sarif
# roll the baseline forward while still gating on what it does not cover
vyql scan -baseline .vyql-baseline.json -baseline-write next.json .
# adopt: record everything, gate on nothing
vyql scan -fail-on none -baseline-write .vyql-baseline.json .
# review flags instead of findings, narrowed to one category and location
vyql scan -flags only -flag-kind attention -flag-loc handlers/ .
# findings and flags together, as one JSON document
vyql scan -flags with -format json .
# a big or slow tree: bound the memory, skip generated files, keep the cache warm
vyql scan -max-ram 8GB -exclude '**/*_templ.go' -exclude node_modules .
vyql scan -cache-incremental . # second run after an edit is faster
# one pack, one profile, no cache: use this when a rule misbehaves
vyql scan -rules vyql/packs/injection -profile api -cache off -stats .-flags selects the mode and the three -flag-* flags filter it. Setting a
filter while -flags is off is a usage error, because the filter could not
reach the output.
| flag | what it does |
|---|---|
-from |
only sources whose concept contains this substring |
-to |
only sinks whose concept contains this substring |
-brief |
one line per connected pair, with a hop count |
-count |
the number of connected pairs, and nothing else |
vyql trace . # every source
vyql trace -from HttpInput -to SqlExecution . # one question
vyql trace -to FilePathAccess . # every path into file access
vyql trace -from HttpInput -to SqlExecution -brief .
vyql trace -from HttpInput -count .-from and -to are checked against the ontology before the scan runs, so a
typo is an error rather than a report of zero. Every mode reports how many
sources dead-ended, because a list of only the sources that reached a sink looks
the same as a clean result.
| flag | what it does |
|---|---|
-type |
match node type substring |
-concept |
match concept label substring |
-call |
match callee path or method |
-loc |
match location substring |
-edges |
also print each match's outgoing edges |
-count |
print only how many matched |
vyql query -type code.Call .
vyql query -concept HttpInput .
vyql query -call db.Query .
vyql query -loc handlers.go .
vyql query -concept SqlExecution -edges . # with outgoing edges
vyql query -concept HttpInput -count . # just the number
vyql query -type code.Call -loc handlers/ -call exec . # filters combineFilters combine with AND. Reachability is trace, not query.
vyql explain .
vyql explain -rules vyql/packs/injection . # narrow to one pack or file
vyql match .
vyql resolve .
vyql graph . # the whole USG, nodes and edges| flag | default | what it does |
|---|---|---|
-kind |
all |
all, concepts, rules, bindings, reviews, packs |
-lang |
binding language filter | |
-query |
case-insensitive substring across names, patterns, packages, CWE | |
-limit |
80 |
maximum rows per section |
-format |
text |
text or json |
vyql definitions # everything that loaded
vyql definitions -kind concepts # the concept vocabulary
vyql definitions -kind rules -query injection # rules mentioning injection
vyql definitions -kind bindings -lang python # one language's vocabulary
vyql definitions -kind bindings -lang go -format json
vyql definitions -kind concepts -limit 500 # raise the per-section capSubcommands:
vyql definitions search sql injection # search every kind at once
vyql definitions explain code.SqlExecution # which binding produced a label
vyql definitions refs core.SqlParameterization # what references a definition
vyql definitions show-policy resultIdentity.default
vyql definitions show-mechanic ruleVerb.taint
vyql definitions validate # validate the loaded corpus
vyql definitions validate vyql/packs/injection # or one path
vyql definitions validate -unstable # report quarantined uses
vyql definitions validate-binding vyql/bindings/python/python/558.vyqlvalidate exits 3 when the corpus does not validate.
refs and explain take the path as -in <path> and not as a positional
argument, because their positional argument is the definition you are asking
about:
vyql definitions refs -in vyql/ontology/concepts core.SqlParameterization
vyql definitions explain -in vyql/bindings/python code.SqlExecutionvyql diff before.json after.json # two `scan -format json` outputs
vyql cache path # the directory this build would use
vyql cache clear # remove it, reporting the space freed
vyql cache clear -cache /tmp/mine # a cache somewhere else
vyql version
vyql help
vyql help scandiff exits 3 when the finding set changed, so a pipeline can gate on "this
branch changed the findings" without parsing the output.
cache clear removes the directory instead of emptying it, and reports how much
space it freed. The next scan creates it again.
scan -max-ram puts the graph in a store under the system temporary directory,
removed when the scan ends, and also when you interrupt it. A kill -9 cannot be
caught, so only that case leaves the directory behind. Remove it with
rm -rf $TMPDIR/vyql-graph-*.
Java, Python, JavaScript/TypeScript, C#, PHP, Ruby, Go, Rust, Kotlin, Scala, Swift, Dart, Groovy, Elixir, Lua, Perl, C, C++, Objective-C, Solidity, Bash, PowerShell.
The depth is not the same everywhere. Java, Python and JavaScript are the
reference frontends and have the most complete modelling. The others range from
full taint tracking down to call-and-concat coverage. vyql definitions -kind all
reports what is actually
loaded.
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 into the binary. It is loaded at startup from the
directory that VYQL_HOME names, or from a vyql/ directory found above the
working directory. Point VYQL_HOME at your own copy, and your edits apply on
the next run.
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 definitions validate-binding vyql/bindings/python/python/558.vyql{
"ok": true,
"bindings": [
{ "name": "python", "mapping_count": 1, "mappings": [ ... ] }
]
}Then check that it attaches what you expect on real code. match lists what was
labelled, and definitions explain names the binding that did it:
vyql match ./some-project
vyql definitions explain code.HttpInputdocs/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.
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 are renamed and clauses are added,
so a rule pack you write today may need edits later to keep working. The specs in
vyql/tests/ are what tell you when something breaks.
The knowledge base keeps changing. A newer release can report findings that an older one did not. That is the purpose of it, but it also means that pinning a version is the only way to get identical output twice.
Some documents describe design rather than behaviour. The reference series in
docs/ also 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 what vyql definitions -kind all reports as loaded.
See CONTRIBUTING.md. Two things to know before your first change:
go testmust always bego test -count=1. The Go test cache keys on source and does not track the.vyqldata files, so a cached pass can hide a change to the security knowledge base completely.CGO_ENABLED=1is required. The parsers are C; without cgo the build constraints exclude them silently rather than failing.
make ci runs exactly what the pipeline runs.
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 |
Run them yourself: benchmarks/fetch-corpora.sh pulls the corpora, which are
GPL and not vendored here. benchmarks/RESULTS.md has the
method, per-category numbers and known corpus defects. Scores from our own
language ports live there too, and are not comparable to these.
Apache-2.0. See LICENSE, NOTICE and THIRD_PARTY_NOTICES.md. The last one covers the vendored tree-sitter grammars, the MITRE CWE/CAPEC taxonomies, and the ecosyste.ms package snapshot, which is CC-BY-SA 4.0.