CLI Reference
xgrep flags and subcommands.
CLI Reference
xgrep [flags] -f <rules> <targets...>
Flags:
-f, --rules string path to rule file or directory
-c, --config string path to rule file or directory (alias for --rules)
--json output results as JSON
--stream with --json: emit findings as newline-delimited JSON (NDJSON) during the scan
--sarif output results as SARIF
--gitlab output results as a GitLab SAST report (gl-sast-report.json)
--fex output results as Mondoo FEX finding documents (JSON) — the same documents reported to Mondoo Platform (inspect what gets uploaded; no service account needed)
--project-root string repository root for report file paths (default: auto-detected git root)
--sarif-category string SARIF automationDetails.id / GitHub Code Scanning category (default: xgrep)
--sarif-include-suppressed include inline-suppressed (# nogrep/# nosemgrep) findings in SARIF as result.suppressions; omitted by default because GitHub code scanning ignores that field and imports them as open alerts
--error exit with code 1 if findings are found (default: exit 0 even with findings)
--max-findings int exit 1 only when active findings exceed N (gradual rollout); takes precedence over --error; -1 disables (default)
--strict exit with code 4 if any file was skipped by a scan error (e.g. the per-file --timeout), so an incomplete scan does not pass as a clean one
--disable-nosemgrep ignore inline nosemgrep/nogrep suppression comments and report all findings
-j, --jobs int number of parallel workers (default: half the CPUs, so an interactive scan leaves the workstation usable; `xgrep ci` defaults to all CPUs)
--priority string CPU priority: low (desktop-friendly default — yields to foreground apps) or high (take the whole box; xgrep ci's default). Env: XGREP_SCAN_PRIORITY
--severity string minimum severity to report (LOW, MEDIUM, HIGH, CRITICAL; Semgrep names INFO/WARNING/ERROR also accepted)
--category string only run rules in this category (e.g. security, correctness); default: security,secrets for built-in rules
--subcategory string only run rules in this subcategory tier (e.g. vuln for exploitable-only)
--exclude-subcategory string skip rules in this subcategory tier (e.g. audit to drop hardening/advisory rules)
--with-builtin string with -f/--rules, also run the built-in rules from the given categories (comma-separated)
--include-opt-in also run rules marked metadata.opt-in: true (off by default)
--include-tests keep security findings in test/spec/fixture/example paths (dropped by default; secrets are always kept)
--all-files also scan untracked files (default: git-tracked only)
--include-vendored scan vendored third-party code: dependency directories (deps/, vendor/, third_party/, ...) and web assets
--no-ignore scan everything: disable the default-on ignores (node_modules, vendor, dist, minified, lockfiles)
--include string include only files matching glob pattern
--exclude string exclude files matching glob pattern
--max-target-bytes int skip files larger than N bytes (default 1000000; 0 disables). Compiled artifacts are exempt — see below
-o, --output string write output to file instead of stdout
--rule-id string only run rules with matching IDs (comma-separated)
--skip-rule string skip rules with matching IDs (comma-separated)
--no-cache do not write the .xgrep/findings.json cache that `xgrep fix` reads
--no-dep-scan skip dependency vulnerability scanning; report code (SAST) findings only
--no-code-scan skip the rule/SAST code scan; report dependency vulnerability findings only
--redact hide detected secret values in all output and in the Mondoo upload; keeps the rule ID, location, and fingerprint
--sbom string scan an existing SBOM file (CycloneDX/SPDX/Mondoo JSON) for dependency vulnerabilities instead of walking a code tree
--incognito local-only scan: do not report findings to Mondoo Platform (reporting is otherwise automatic when a service account is configured)
--scope-mrn string target Mondoo space/org MRN for reporting (defaults to the scope in the service-account config)
--mondoo-config string path to the Mondoo service-account config (default: MONDOO_CONFIG_PATH or ~/.config/mondoo/mondoo.yml)
--timeout int per-file timeout in seconds (default 60; 0 disables) — a pathological file is skipped and recorded, not left to hang the scan
--verbose show the full per-finding list (default output is a concise summary) and enable debug logging
--owasp print an OWASP Top 10 coverage scoreboard at the end of the run
--cwe print a CWE distribution table (top weakness types) at the end of the run
--owasp-edition string OWASP Top 10 edition for --owasp: 2021 or 2025 (default 2025)Flags shown with a type (string, int) take an argument; the rest
(--include-opt-in, --include-tests, --no-cache, --no-ignore, …) are booleans that take none.
For the complete, current flag list (including --baseline-commit, --history,
--decode, --max-memory, --lang, --metrics,
--stdin, and profiling flags), run xgrep --help.
Default output vs. --verbose
On an interactive terminal, xgrep scan prints a concise executive summary —
a scan headline, the top findings (most-severe first, with location), a
severity breakdown (CRITICAL/HIGH/MEDIUM/LOW), and a → xgrep fix next step that
works from the cached results. Pass --verbose to stream every finding inline
(high-severity first; lower severities collapse to a hint unless you lower
--severity). Piped/redirected output and every machine format
(--json/--sarif/--gitlab) are unaffected. Severity is shown on the
security-standard ladder (CRITICAL/HIGH/MEDIUM/LOW) in human output; the
Semgrep-compatible names (ERROR/WARNING/INFO) are retained in --json and SARIF.
OWASP Top 10 and CWE summaries
Every finding is tagged with its OWASP Top 10 category and CWE. Two flags turn that tagging into an end-of-run summary:
--owaspprints an OWASP Top 10 coverage scoreboard: all ten categories, with a filled marker and count where findings mapped and a hollow marker where none did, plus aN/10 categories with findingsheader.--owasp-editionselects the 2021 or 2025 edition (default 2025).--cweprints a CWE distribution — the most frequent weakness types by finding count.
xgrep scan ./src --owasp
xgrep scan ./src --cwe
xgrep scan ./src --owasp --cwe --owasp-edition 2021The tables summarize the findings of that run, so they respect --severity,
--subcategory, and the other selection flags. In interactive output they print
after the executive summary; alongside a machine format
(--json/--sarif/…) they print to stderr so the document on stdout stays valid,
and --quiet suppresses them. The same OWASP Top 10 and CWE tables are shown by
default in xgrep analytics.
Controlling CPU usage
A scan is CPU-bound — it parses and matches files in parallel — so by default xgrep is deliberately polite on a workstation and goes full speed only where the machine is dedicated to the job.
Two independent controls:
-j,--jobssets the worker count and caps the whole process to that many cores. This is a real budget, not just a worker count: background garbage collection is held to the same cap, so-j 4means "use about four cores," not "four workers plus whatever the runtime spreads across the rest." The interactive default is half your CPUs (so a scan leaves the machine usable);xgrep ciraises it to all CPUs.--prioritysets how xgrep competes for the CPU it does use:low(the default) lowers the process's scheduling priority so a foreground editor or browser always wins contention. A scan runs in the background without making the rest of the machine feel sluggish; it may take marginally longer under load.highapplies no such yield — use it when nothing else needs the machine.xgrep ciselectshighby default.
Both can also be set with the XGREP_SCAN_PRIORITY=low|high environment
variable, which is handy for wrappers, CI images, or editor integrations that
launch xgrep and can't easily pass a flag. The environment variable wins over
--priority.
Examples:
xgrep scan -f rules/ . # half your cores, low priority (default)
xgrep scan -j 2 -f rules/ . # hard cap at ~2 cores
xgrep scan --priority high -f rules/ . # take the whole box on this run
XGREP_SCAN_PRIORITY=high xgrep scan -f rules/ .The same controls apply to the background servers (xgrep lsp, xgrep mcp) —
see Resource usage for how this keeps the
editor integration from pegging your machine.
Rule selection
The built-in corpus is filtered by category and tier before it runs:
--category(defaultsecurity,secrets) picks the top-level rule sets:security,secrets,correctness,performance,concurrency— plus the inventory sets selected by their own flags (--cbom,--aibom,--dspm). Secrets scanning is on by default, so a committed credential is reported without any extra flag. A name no rule carries selects nothing, so xgrep warns and lists the categories it did find rather than reporting a clean scan.--subcategory/--exclude-subcategorynarrow by exploitability tier (vulnvsaudit, below).--rule-id/--skip-rule(and the Semgrep-compatible--exclude-rulealias) include or exclude individual rule IDs.--include-opt-inadditionally runs rules markedmetadata.opt-in: true— higher-noise or situational rules that are off by default.--skip-rulestill excludes them.
To run your own rules alongside the built-ins, pass -f/--rules together
with --with-builtin <categories>; a custom rule overrides a built-in rule with
the same ID, and an explicit --category still filters the whole merged set:
xgrep scan -f rules/ --with-builtin security,secrets src/Exploitability tier (--subcategory)
Every built-in security rule carries a metadata.subcategory tier. It is the
second of the two axes every rule is classified on — the first is
--category, which says what the finding is:
vuln— exploitable / attacker-reachable impact: injection, eval/dynamic exec, deserialization, SSRF, path traversal, auth bypass, open redirect, hardcoded credentials/secret exposure, and insecure-TLS / disabled cert validation.audit— hardening / best-practice / advisory with no direct exploit: missing security headers, cookie flags, weak hashing/ciphers, timing attacks on constants, info disclosure (stacktraces, cleartext logging), availability DoS (ReDoS), and regex/validation smells.
Secrets are not tiered, and neither filter removes them. A committed
credential is its own category rather than a tier of security finding, so
secrets rules carry no subcategory and both --subcategory vuln and
--exclude-subcategory audit keep them. Drop them with --category security
if you want the tier alone.
Filter on it to get an exploitable-only scan (high signal, fewer advisories):
xgrep scan --category security --subcategory vuln <target>--exclude-subcategory audit is the inverse and yields the same set. Combine
with --xgrepignore to also drop non-source trees — the
recommended setup for scanning a focused executable surface (e.g. an AI-agent
skill):
xgrep scan --category security --subcategory vuln --xgrepignore <target>Both flags accept a comma-separated list and compose with --category,
--severity, and --rule-id/--skip-rule.
Subcommands
scan find security issues: code (SAST) and dependency vulnerabilities
(implied when targets are given without a subcommand)
ci CI-optimized diff-aware scan (auto-detects the CI environment)
fix verify and apply fixes through the verification harness
triage record triage verdicts (true-positive / false-positive) on findings
sbom generate a Software Bill of Materials, or a Crypto/AI BOM with --include cbom/aibom
deps query a project's dependencies: inventory, tree, usage, reachability, licenses
dspm code DSPM: inventory the sensitive data a codebase handles
inspect code intelligence: search symbols, navigate definitions, assess impact
graph build and query the code graph
analytics behavioral code-risk analytics: hotspots, coupling, ownership
guard client-side hook for coding agents: block secrets/PII leaving via prompt or tool call
mcp run as an MCP server over stdio (for AI agents)
lsp start an LSP server over stdio
login authenticate with Mondoo Platform using a registration token
status show version, platform authentication, and configuration
telemetry manage anonymous usage telemetry (on / off / status)
test <path> run tests on rule files in a directory
validate <path> validate rule files without scanning
version print version and exitfixandtriageare documented under Remediation — fixing code, fixing dependencies, and triage.depsis documented under Querying dependencies, with itsreachability,licenseandnoticesubcommands under Reachability, License compliance and Attribution.dspmis documented under Code DSPM.loginand reporting to the platform (automatic when a service account is configured;--incognitoto disable) are documented under Mondoo Platform.statusis documented under Checking your setup below.inspectandgraphare documented under Code intelligence;analyticsunder Code-risk analytics.guardis documented under AI agents → Guard hooks. The AI-agent skills built on xgrep are published separately — see AI agents → Skills.lspis documented under IDE integration.mcpis documented under Integrations.testis documented under Testing rules.sbomgenerates a dependency SBOM, or a Cryptography/AI Bill of Materials withsbom --include cbom/--include aibom(combine them to merge into one document) — see CBOM.
Checking your setup (xgrep status)
xgrep status answers "what is this install doing?" without running a scan — the
build you are on, whether findings would reach Mondoo Platform and which space,
the telemetry setting and why, and whether update checks are enabled:
$ xgrep status
xgrep 1.4.0
commit 9f2c1ab · built 2026-09-02 · go1.26.6 · linux/amd64 · release build
Mondoo Platform
✔ authenticated — scans report findings to this space
space keen-shockley-123456
mrn //captain.api.mondoo.app/spaces/keen-shockley-123456
config /home/you/.config/mondoo/mondoo.yml
reporting runs on the default branch only; --incognito turns it off
Telemetry
disabled
posture off
reason opted out with `xgrep telemetry off`
config /home/you/.config/xgrep/config.json
Update check
enabled — release build
endpoint https://install.mondoo.comEverything is read locally: no credential is validated against the platform
and no version lookup is made, so status works offline and never blocks on a
slow network. It prints whether a service account resolves and the space it
targets — never the key itself.
| Flag | Effect |
|---|---|
--json | emit the same report as JSON, for support tooling and scripts. |
--mondoo-config <path> | check a specific service-account config instead of $MONDOO_CONFIG_PATH / ~/.config/mondoo/mondoo.yml. |
--scope-mrn <mrn> | resolve reporting against a specific space/org MRN, as a scan would. |
When no service account resolves, status says not configured and marks the
config path (not present) — so "never set up" is never confused with
"set up and rejected". Scanning itself is unaffected: xgrep runs fully local
without credentials.
Fixing findings
scan discovers and never modifies your source. Fixing is a separate command,
xgrep fix, which applies deterministic edits itself, delegates the judgement calls
to your coding agent, and re-scans every change before writing it. It upgrades
vulnerable dependencies too.
See Remediation — fixing code, fixing dependencies, and triage.
xgrep scan . # find
xgrep fix # review and fixScan targets
A scan target is more than a single file. xgrep accepts any of these as
<targets...>, and you can pass several at once:
-
A file —
xgrep scan app.pyscans just that file. -
A directory —
xgrep scan .scans it recursively. By default xgrep scans the git-tracked files under the directory (respecting.gitignore), falling back to a filesystem walk when the target isn't a git repository. See File filtering for what is and isn't included. -
Multiple targets —
xgrep scan src/ lib/ config.yamlscans the union. -
A remote git repository —
xgrep scan github.com/expressjs/expressclones and scans it, no manual checkout needed. See Scanning a remote repository below. -
An archive or build artifact —
xgrep scan app.jarscans what is inside a.zip,.tar,.tar.gz, a Java.jar/.war/.ear(including the compiled classes), a Python.whl/.egg, a Ruby.gem, a NuGet.nupkg, a PHP.phar, or annpm packtarball. See Scanning a build artifact below. -
A deployed serverless function —
xgrep scan lambda://checkout-api,xgrep scan gcpfn://checkout-api, orxgrep scan /subscriptions/<id>/resourceGroups/<group>/providers/Microsoft.Web/sites/<app>fetches and scans the code that is actually deployed. See Scanning a deployed function below. -
stdin — pipe a single source with
--stdin, or a multi-file JSON manifest with--stdin-files. See Scanning from stdin below. -
No target —
xgrep scan(with no path) scans the current directory, so you don't have to typexgrep scan .. A rules path with no target (xgrep -f rules.yaml) does the same. A barexgrepwith no subcommand and nothing to scan prints a welcome screen instead (so it never silently scans a large directory you didn't ask for).
Scanning from stdin
When the code to scan isn't on disk — an editor buffer, a generated snippet, a file pulled from an API — feed it on stdin instead of a path:
# A single source. --lang is required (there's no filename to detect from).
cat app.py | xgrep scan --stdin --lang python
# A set of files in one call, as a JSON manifest [{path, content}, ...].
# Cross-file taint is preserved across the set.
xgrep scan --stdin-files < files.json--stdinreads one source from stdin and requires--langto set the language. Findings are reported against a synthetic path.--stdin-filesreads a JSON array of{path, content}objects and scans them together, preserving the relative paths (so cross-file dataflow works).- stdin input is mutually exclusive with
--historyand--baseline-commit.
Scanning a remote repository
A scan target can be a remote git repository instead of a local path; xgrep
clones it (shallowly, default branch) into a temp directory, scans it, and
reports repo-relative paths. Cloning uses a built-in git client — no
git binary required.
xgrep scan github.com/expressjs/express # host/owner/repo shorthand
xgrep scan https://github.com/expressjs/express # explicit https
xgrep scan git@github.com:expressjs/express.git # SSH
xgrep scan github.com/expressjs/express --ref 4.18.2 # a tag, branch, or commit
xgrep scan github.com/expressjs/express --full-clone # full history instead of shallow- A target is treated as remote only when it isn't an existing local path, so a local directory always scans in place.
--ref <branch|tag|commit>checks out a specific ref before scanning (default: the repo's default branch). A commit SHA implies a full clone.--depth <n>sets the shallow clone depth (default1);--full-cloneclones full history. The two are mutually exclusive.- Private repositories use your usual git credentials: SSH targets via the
ssh-agent; HTTPS targets via a token in the environment —
GITHUB_TOKEN,GITLAB_TOKEN, or the genericXGREP_GIT_TOKEN. - Diff-aware scanning of a remote works: combine
--baseline-commitwith a remote target and xgrep clones it with full history automatically (a shallow clone has none to diff a range against), then diffs and reports only changed lines, repo-relative — the same behavior as a local diff-aware scan. Only a single remote target is supported in this mode.xgrep scan github.com/acme/app --baseline-commit v1.0.0..v1.1.0 - Remote scanning needs outbound network access (governed by your environment's network policy in hosted/CI setups).
Scanning a build artifact
Point xgrep at the thing you ship. A Java .jar, .war or .ear, a Python
.whl or .egg, a Ruby .gem, a NuGet .nupkg, a PHP .phar, or the tarball
npm pack writes is scanned in place — no unzipping, no hunting for the source
repository:
xgrep scan app.jar
xgrep scan target/myservice.war
xgrep scan dist/checkout_api-2.1.0-py3-none-any.whl
xgrep scan pkg/rack-3.1.8.gem
xgrep scan bin/Release/Checkout.Api.2.1.0.nupkg
xgrep scan build/checkout-api.phar
xgrep scan checkout-api-2.1.0.tgzFindings name the path inside the artifact, so you can find what they refer to:
app.war!WEB-INF/classes/AppServlet.class:9:1: Command injection HTTP request
app.war!index.jsp:1:11: JSP scriptlet XSSWhat you get:
- Findings from compiled classes. An ordinary jar holds
.classfiles rather than source, and xgrep reads them: string constants go through the full secret corpus, and an untrusted value reaching a dangerous call is reported as the same rule the source scan reports, at the same source line when the class was compiled with debug information. A jar with no source anywhere near it still produces real findings. Confidence on a taint finding is capped at MEDIUM, because the guard and sanitizer checks behind a rule's own confidence have no syntax to match in bytecode; a rule decided by the call the code names rather than by a flow — a broken cipher or digest — keeps its own confidence, because the class file states that algorithm as plainly as the source does. - Kotlin and Scala classes are reported as Kotlin and Scala. A
.classfile is not necessarily Java — Kotlin, Scala, Groovy and Clojure all compile to the same format. xgrep reads which compiler produced each class and reports under that language's rules, so scanning a jar built from Kotlin gives youkotlin-command-injection, exactly as scanning the.ktsources does. It is decided per class, so a mixed Java/Kotlin jar reports each class under its own language. - Findings from everything else in the archive — JSPs, bundled JavaScript, configuration, SQL, shell scripts.
- Findings from a wheel's Python source. A wheel ships
.pyfiles, so scanning one reports exactly what scanning the project's sources reports — the same rules at the same lines, named by their path inside the wheel. - Findings from a gem's Ruby, and from an npm tarball's JavaScript. Both
ship source, so the same rules fire at the same lines. A
.gemkeeps its code in an innerdata.tar.gz, which xgrep unpacks for you — pointing a scanner at the outer container alone finds three compressed files and no Ruby. - Findings from a phar's PHP. A phar is how PHP ships an application as one
file, and everything inside it is ordinary PHP source, so the same rules fire
at the same lines. All three shapes a phar can take are read — the usual
stub-and-manifest file, and the tar- and zip-based forms
Phar::convertTo*writes — as are the whole-file-compressed.phar.gzand.phar.bz2thatPhar::compress()produces. - Findings from compiled .NET assemblies. A
.dllor.exeholds CIL rather than source, and xgrep reads it: string constants go through the full secret corpus, and an untrusted value reaching a dangerous call is reported as the same rule the C# source scan reports. A deployed .NET service is a directory of assemblies with no source anywhere near it, and it still produces real findings. A.nupkgextracts to exactly such a directory, so its assemblies are analysed along with the content files and scripts it ships. - Findings from compiled Python modules. A
.pycholds CPython bytecode rather than source, and xgrep reads it: string constants go through the full secret corpus, and an untrusted value reaching a dangerous call is reported as the same rule the Python source scan reports. A deployment that compiled its sources and then deleted them still produces real findings. A__pycache__standing beside its.pyfiles is skipped, since those sources are scanned already; one whose sources are gone is scanned. - Findings from WebAssembly modules. A
.wasmholds no source and names no library calls, and xgrep reads it for the two things it does carry: the string literals in its data segments, through the full secret corpus, and the host functions it imports, which bound what the module can reach. A finding carries that capability list, so a credential in a module that can open sockets reads differently from the same credential in one that provably cannot. - Dependencies. The artifact's own coordinates are read from its metadata,
so they are reported with no build file anywhere: a jar's Maven coordinates
and those of the jars it bundles, a wheel's name, version and declared
license from the
METADATAit ships, a gem's from the gemspec inside it, and a NuGet package's from its.nuspec. Those licenses are the ones the registry shows, and nothing beside the artifact states them — arequirements.txtor aGemfile.locklists what a project depends on, not what the project is, and no .NET manifest format carries a license at all. Artifacts whose metadata is stripped or absent cannot be identified, and the scan says how many rather than reporting a clean result.
Modules versus dependencies. An enterprise archive's modules — the wars and
jars an .ear contains — hold your own code, so they are scanned. Jars in a
dependency directory (WEB-INF/lib, BOOT-INF/lib, lib) are libraries: their
coordinates are inventoried and their internals are not reported, so an
application's own bugs are not buried under its dependencies'.
The inventory commands accept the same targets:
xgrep sbom app.jar
xgrep deps list app.war
xgrep deps license app.jar
xgrep deps license dist/checkout_api-2.1.0-py3-none-any.whl
xgrep deps license pkg/rack-3.1.8.gem
xgrep deps license bin/Release/Checkout.Api.2.1.0.nupkg
xgrep deps list build/checkout-api.pharA compiled assembly reports its method, not a line. A .NET assembly carries no line table — the mapping lives in a PDB, which is a separate file and is usually not deployed — so a finding names the method it is in and the byte offset of the instruction, rather than a line number the artifact cannot support. Everything else is the rule's: its severity, its message and its fix. Confidence is capped at MEDIUM, because the guard and sanitizer checks behind a C# rule's own confidence have no syntax to match in CIL. The cap can only lower a rule's confidence, never raise it: a rule that declares LOW still reports LOW.
A compiled Python module reports its function and its original source path.
A .pyc has no lines of its own, so a finding gives the byte offset of the
instruction — but the module remembers what it was compiled from, so the finding
also names that source file and the function, which is enough to find the code
in a repository even when the deployment carries none. Confidence is capped at
LOW — a rung below a compiled Java or .NET finding, and for a reason particular
to CPython. A .class or an assembly names the method each call goes to, so the
analysis reads its call targets; CPython names nothing, so the target has to be
reconstructed from the instruction stream. On top of the guard and sanitizer
checks that have no bytecode syntax to match, the identity of the dangerous call
is itself inferred, and the confidence says so.
A WebAssembly finding names a byte offset and what the module can reach. A
module has no lines, so a finding gives the offset of the literal it was found
in. It also carries wasm-capabilities — read from the import section, which in
WebAssembly is a hard bound rather than a hint, since a module has no ambient
authority. wasm-host: wasi marks a module whose capability list is a complete
account; a module on a custom host instead names those host namespaces, so an
empty capability list is never ambiguous.
Four Python rules have a compiled counterpart — command injection, code injection, path traversal and SQL injection — and CPython 3.10 through 3.13 are read. A module compiled by a release xgrep has no table for is reported as unreadable rather than decoded with another release's table.
Not every C# rule has a compiled counterpart. Five do — command injection, SQL injection, path traversal, reflected XSS and code injection. A rule with no counterpart simply does not report on an assembly; it is not silently approximated by a neighbouring one.
A phar with a broken signature is refused, not scanned. Most phars carry a digest of their own contents, and xgrep checks it before extracting anything — exactly as the PHP runtime does — for the stub-and-manifest form and for the tar-based form:
$ xgrep scan composer.phar
error: extracting archive: phar signature does not verify: the file has been
altered or truncated since it was signedFindings from a file that no longer matches what its publisher signed would describe something nobody shipped, so the scan stops and says so. Every entry is also checked against the checksum the phar records for it, which catches corruption in a phar that carries no signature at all. Two cases are read without a verdict: a phar signed with OpenSSL, whose check needs the publisher's public key, and the zip-based form, whose digest does not cover a region of the shipped file. Their contents are still checked entry by entry.
A phar's dependencies come from what it bundles. A phar states no name,
version or license of its own — it carries an alias and nothing a registry would
show — so xgrep deps list composer.phar reports the packages inside it, read
from the composer.lock and installed-package metadata the build wrote in.
An npm tarball is recognised by its contents, not its name. npm pack
writes <name>-<version>.tgz, an extension that describes gzip and tar and
nothing else. xgrep reads the layout npm writes — package/package.json — so a
real npm package is treated as a build artifact while your backup tarball stays
an ordinary archive.
A compiled Go binary states its own dependencies. xgrep sbom ./checkout-api
and xgrep deps list ./checkout-api read the module graph the Go linker writes
into the executable, which survives stripping — so a deployed Go service is
inventoried with no source, no go.mod and no lockfile. See
Generating an SBOM.
It is inventory only: xgrep does not analyse compiled machine code for
vulnerabilities.
A Rust binary states its dependencies only if it was built to. cargo auditable build writes the crate graph into a linker section, and xgrep reads
it out of ELF, Mach-O and PE images alike. A binary built without it carries no
such data, and xgrep reports that the data is absent rather than returning an
empty inventory or guessing a crate list from the binary's strings. See
Generating an SBOM.
Source distributions are not build artifacts here. A Python sdist is also a
.tar.gz, and xgrep already scans one as an archive — but it is not treated as
a Python package, because claiming that extension would label every tarball you
scan as one.
A NuGet package's .nuspec is read only from a real package. A project that
packs with nuget pack keeps an authored .nuspec beside its project file; it
is the same XML a shipped package carries, so xgrep tells them apart by the
packaging NuGet writes around it. Your project is never listed as a dependency
of itself.
Scanning a deployed function
The repository a function was built from is a proxy for what is running. The build may have bundled different versions than the lockfile pins, the deployed revision may predate the last year of commits, and some function code has no repository at all. Point xgrep at the function instead:
# Region and account come from your AWS credentials.
xgrep scan fn aws --name checkout-api
# A version or alias of it.
xgrep scan fn aws --name checkout-api --qualifier PROD
# Or the full ARN, when the region or account is not the ambient one.
xgrep scan fn aws --arn arn:aws:lambda:us-east-1:123456789012:function:checkout-apiA target string names one function, and works anywhere a target is accepted
— including xgrep sbom and xgrep deps, which have no fn subcommand:
xgrep scan lambda://checkout-api
xgrep scan arn:aws:lambda:us-east-1:123456789012:function:checkout-api:PRODDiscovery is not available this way. xgrep scan lambda://,
xgrep scan azurefn://<subscription> and xgrep scan gcpfn:// are refused, with
the scan fn command to use instead: how many functions a command scans should be visible in the
command, and the difference between one function and a whole region is too large
to hide in a suffix.
xgrep downloads the deployed package, extracts it, and scans it like any other target. Findings name the function:
lambda:checkout-api!Handler.class:18:1: Command injection HTTP request- Layers are fetched too. A function's dependencies often live in a layer
rather than in its own package, so scanning the package alone would report no
dependencies for a function that has plenty. Layer contents are reported
against the layer they came from
(
lambda:checkout-api!layer:common-deps:3!python/requirements.txt), because a layer is shared between functions. - Container-packaged functions work too. The image is pulled from its registry and flattened. Registry credentials come from your existing setup — the ECR helper for Amazon registries, the ACR helper for Azure, otherwise your Docker configuration.
- Application files only. Base operating-system paths in an image (
/usr,/etc,/var/lib, …) are skipped: xgrep answers whether your code and its dependencies are exploitable. Operating-system packages are a different question, answered by cnspec.
Scan every function in the region, or those matching a name prefix:
xgrep scan fn aws --all
xgrep scan fn aws --prefix checkout-
xgrep scan fn aws --region eu-west-1 --allThese scan a region, not an account: Lambda is a regional service, so the functions listed are the ones in the region your credentials resolve to. Scanning several regions means running this once per region.
Deployed packages are cached between runs, keyed on the code identity AWS
reports (CodeSha256, and a layer's immutable version), so re-scanning an
estate transfers only what changed. Pass --no-package-cache to fetch
everything again.
Credentials come from the usual AWS chain (environment, profile, SSO, instance
role). Every API xgrep calls is read-only; the permissions needed are
lambda:GetFunction, lambda:GetLayerVersion, and for container-packaged
functions ecr:GetAuthorizationToken, ecr:BatchGetImage and
ecr:GetDownloadUrlForLayer.
Azure Functions
The simplest way is xgrep scan fn, which selects the target with flags:
xgrep scan fn azure --subscription <id> --group <rg> --name checkout-fn
xgrep scan fn azure --subscription <id> --group <rg> --name checkout-fn --function HttpTrigger
xgrep scan fn azure --subscription <id> --all # the whole subscription
xgrep scan fn azure --subscription <id> --group <rg> --all # a resource group
xgrep scan fn azure --subscription <id> --group <rg> --prefix checkout-
xgrep scan fn aws --name checkout-api
xgrep scan fn aws --arn arn:aws:lambda:us-east-1:123456789012:function:checkout-api:PRODEvery xgrep scan flag works here too (--json, --severity, …), and
xgrep scan fn <cloud> --help lists the selectors. --prefix takes no *, so
your shell cannot expand it.
The selectors are the same on every cloud — exactly one of --name, --prefix
or --all — and only the scope flags differ, because the clouds do. Scanning a
whole scope is always explicit: --all, rather than the absence of narrower
flags, so a forgotten --name cannot quietly turn a one-function scan into an
estate scan.
Or point xgrep at a Function App with the azurefn:// form, which is what you
need for xgrep sbom and xgrep deps:
xgrep scan azurefn://<subscription>/<group>/checkout-fn
# Narrow it to one function in the app.
xgrep scan azurefn://<subscription>/<group>/checkout-fn/HttpTriggerThe full ARM resource ID works too, so you can paste one straight from the portal:
xgrep scan /subscriptions/<id>/resourceGroups/<group>/providers/Microsoft.Web/sites/checkout-fnPrefer azurefn:// in scripts and CI. A resource ID starts with /, so it is
indistinguishable from an absolute path: if a directory of that name exists,
the directory wins. azurefn:// always means Azure.
Findings name the app: azurefn:checkout-fn!HttpTrigger/__init__.py:12:5: ....
Scan a whole estate by naming a scope instead of one app:
xgrep scan azurefn://<subscription> # every function app in the subscription
xgrep scan azurefn://<subscription>/<group> # every one in a resource group
xgrep scan azurefn://<subscription>/<group>/checkout-* # those matching a name prefixThe equivalent resource-ID forms (/subscriptions/<id>,
/subscriptions/<id>/resourceGroups/<group>, and a sites/checkout-* suffix)
work as well.
Each app is fetched and scanned as its own target, so one app that cannot be fetched is one failed target rather than a failed run — and the scan warns that it happened, because findings that cover only the apps it could reach describe a smaller estate than the one you asked about. A scope naming more than 500 apps is refused rather than truncated; narrow it to a resource group or a name prefix.
-
The app is the unit that is deployed, not the individual function. One package holds
host.json, a directory per function, and — for a custom-handler app — a single executable shared by all of them. Naming a function selects what you meant, not which bytes are downloaded; naming one the package does not contain is an error rather than a whole-app report under that name. -
Two deployment shapes are fetched. An app deployed run-from-package is read from the package URL its settings name; an app deployed by zip-deploy (or with
WEBSITE_RUN_FROM_PACKAGE=1, which says a package is mounted without saying where from) is read from the app's SCM endpoint. -
A custom-handler app ships a compiled executable and no readable source. The scan still fetches and inventories it, but code findings come from what is readable — for these apps that is the dependency manifests, not the handler.
-
Container-hosted apps work too. An app that runs a container image is pulled from its registry and flattened, using your existing registry credentials — the Azure helper for
*.azurecr.io, otherwise your Docker configuration. -
Some packages are SquashFS, not zips. Azure stores a run-from-package deployment as a blob named
scm-latest-<app>.zipthat is really a SquashFS image. xgrep decides the format from the file's contents rather than its name and reads either. -
Flex Consumption apps are not supported yet. Their deployment package lives somewhere the SCM endpoint does not serve, and the scan fails saying so rather than reporting a clean app.
-
Linux Consumption apps work too. Their code is not served by the app's SCM endpoint at all — it is mounted from an Azure Files share — so xgrep reads it from there. An app that was created but never deployed is reported as having no deployed content, not as a failed scan.
-
Third-party assemblies are not scanned as your code. In a published .NET app the project's own assembly sits beside every NuGet dependency, all
.dll. xgrep reads the app's own.deps.jsonto tell them apart and reports findings from your assembly, not from the libraries it restored. Pass--include-vendoredto scan everything.
Deployed content is cached between runs, keyed on the version Azure reports, so
re-scanning transfers only what changed. This covers a package named by
WEBSITE_RUN_FROM_PACKAGE, one read from the app's own storage account (the
Flex Consumption path), and a Linux Consumption app's content share — the share
is keyed on a fingerprint over every file's ETag, so a single changed file
re-fetches the tree. A reissued SAS on the package URL does not defeat the
cache. Content whose version xgrep cannot read is fetched every time rather
than cached, and the SCM endpoint is never cached because its zip is generated
per request. Pass --no-package-cache to fetch everything again.
Credentials come from the standard Azure chain (environment, Azure CLI, managed
identity). The permissions needed are read access to the site and its
configuration — Microsoft.Web/sites/read and
Microsoft.Web/sites/config/list/action. If the deployment package has to be
read from the app's storage account, that account additionally needs the
Storage Blob Data Reader role: blob data access is separate from your
subscription role, so Owner alone is not enough.
GCP Cloud Run functions
A function is named by the short form or by its full resource name:
# Project and region come from your gcloud context.
xgrep scan fn gcp --name checkout-api
# Name the region, or both the project and the region.
xgrep scan fn gcp --region us-central1 --name checkout-api
xgrep scan fn gcp --project acme-prod --region us-central1 --name checkout-apiScan a whole project, a region, or the functions matching a name prefix:
xgrep scan fn gcp --all # every region of the project
xgrep scan fn gcp --region us-central1 --all
xgrep scan fn gcp --prefix checkout-Unlike AWS, --all here covers every region of the project, because Google's
API can list them in one call. Adding --region narrows it.
- What is scanned is the source you deployed. GCP stores the archive you uploaded and builds a container from it separately; xgrep reads the stored archive, so findings are in the code somebody wrote rather than in build output. The caveat is the pipeline: if your deploy bundles or minifies before uploading, the bundle is what was uploaded and the bundle is what is scanned.
- Dependencies your deploy excluded are not there to scan.
gcloud's default.gcloudignoreskipsnode_modules, so a Node function's archive names its dependencies inpackage.jsonwithout carrying their code. The inventory is complete; the dependency source is not. - Both generations work. A 1st-gen function's archive is a zip and a
2nd-gen one's is a
.tar.gz; xgrep decides from the file's contents rather than from the generation the API reports. - Container images are a fallback. A Cloud Run function has no image field —
it is always built from source — so if the deployed source cannot be fetched,
xgrep falls back to the image of the Cloud Run service behind the function.
That pull uses your Docker credential helper: run
gcloud auth configure-docker <region>-docker.pkg.devfirst, since Artifact Registry is not read through application default credentials.
Deployed source archives are cached between runs, keyed on the GCS object
generation of the stored archive, so re-scanning a project transfers only what
changed — and a cached function needs no signed download URL either, so a warm
re-scan makes no generateDownloadUrl call for it. A redeploy changes the
generation and re-fetches. A function deployed from a repository rather than an
uploaded archive has no generation to key on and is fetched every time. Pass
--no-package-cache to fetch everything again.
Credentials come from Google application default credentials. The permissions
needed are cloudfunctions.functions.get and
cloudfunctions.functions.sourceCodeGet, plus cloudfunctions.functions.list
to scan a whole project or region, and run.services.get with Artifact Registry
read access for the container fallback.
Findings from a function are not reported to Mondoo Platform yet: a function does not have a platform asset identity, and the scan says so rather than uploading findings that would be attributed to the wrong thing.
Sweeping a function's environment variables
A function's environment variables are not read by default, on any cloud.
They are configuration rather than code, and a code report is not where you
expect secrets to turn up. --include-env opts in:
xgrep scan --include-env fn aws --allWith it, the function's configured variables are written into the scanned tree
as .xgrep-function-environment and read by the same secret rules that read a
committed .env file. A finding against that path is a finding about the
function's configuration, not about its code — the file was never deployed,
and the file itself says so in its first lines.
Every variable is swept and nothing is filtered out, including the settings
xgrep itself read to locate the package: WEBSITE_RUN_FROM_PACKAGE holds a SAS
often enough that skipping it would leave the likeliest credential the one thing
never looked at.
Some environments cannot be read and report nothing rather than reporting partially: a Lambda whose variables are encrypted with a customer managed key returns no values, and a GCP function that references Secret Manager exposes the secret's name rather than its value — which is the right answer for a function that handles its secrets properly.
Passing --include-env to a scan with no function target in it warns and
changes nothing, so a clean report never silently means "your configuration was
swept and was fine".
Suppressing findings (nosemgrep)
Add a nosemgrep (or nogrep) comment on the matched line or the line directly
above it to suppress a finding. Scope it to specific rules with nosemgrep: <id>;
a bare nosemgrep suppresses every rule on that line.
dangerous(user_input) # nosemgrep
dangerous(user_input) # nosemgrep: python-command-injectionSuppressed findings are retained, not deleted, and surface differently per output so CI behaves predictably:
| Output | Suppressed finding |
|---|---|
| Console text | hidden |
| JSON | included, extra.is_ignored: true |
| SARIF | included with suppressions[].kind: "inSource" → GitHub shows it dismissed |
| GitLab SAST | omitted |
Exit code / --error | not counted (a file with only suppressed findings exits 0) |
Pass --disable-nosemgrep to ignore all suppression comments and report every
finding as active — useful for auditing what suppressions are hiding.
Diff-aware scanning (--baseline-commit)
For pull-request / CI checks, --baseline-commit scopes the scan to the files
changed since a baseline so xgrep only parses and runs rules over what changed,
and only reports findings on changed lines. This matches
Semgrep/Opengrep, which expose diff-aware scanning under the same flag.
xgrep --baseline-commit HEAD # changes in the working tree vs HEAD
xgrep --baseline-commit origin/main # changes since origin/main (use the merge-base in CI)
xgrep --baseline-commit origin/main..HEAD # changes in a commit range- The spec is a single ref (diffed against the working tree) or a
<base>..<head>/<base>...<head>range. - Only changed files are scanned; findings on unchanged lines of changed files
are dropped.
--errorexits 1 only when a changed line has a finding. - Content scanned is the working tree, and paths are repository-root
relative — run xgrep from the repo root (the standard CI setup). For an
accurate range, the head should be the working tree /
HEAD. - Because unchanged files are not scanned, interfile (cross-file) analysis sees
only the changed files; omit
--baseline-commitfor a full-context scan.
Re-scanning faster (--incremental)
A second scan of a tree usually re-analyzes files that did not change. --incremental
reuses the per-file results cached in .xgrep/scan-cache.json (written by the previous
run), so a re-scan only analyzes what actually changed:
xgrep scan . # first run — populates the cache
xgrep scan --incremental . # re-scan; unchanged files are served from the cacheIt is opt-in because reuse must never hide a finding. A cached result is reused only
when the file's content and the exact ruleset, engine and scan semantics that
produced it are unchanged — any mismatch discards the cache and the file is
re-analyzed. With cross-file analysis (--graph) a file's findings can depend on
other files, so reuse additionally requires the whole tree to be unchanged. A fully
cached run therefore reports exactly what a cold run reports.
The cache is keyed on stable on-disk paths, so --incremental does not apply to
scans that have none: stdin (--stdin-files), --history, archives, and remote
repository targets are always analyzed in full.
Scanning git history for secrets (--history)
A secret that was committed and later deleted still lives in the repository's
git history, so it is still compromised. A normal scan only sees the working
tree, and even a diff-aware scan (--baseline-commit) misses an add-then-remove.
--history walks the full commit history and scans the content each commit
introduced, so it catches secrets that no longer exist in the current tree.
# Scan a repo's whole history for secrets (pass the repo path)
xgrep --history --category secrets .
# Bound the walk for speed on large repos
xgrep --history --category secrets --since 2024-01-01 .
xgrep --history --category secrets --max-commits 5000 .Each finding carries commit provenance — who introduced the secret and when —
so you can rotate the credential and purge the history. In JSON/SARIF output this
appears as a commit object:
{
"check_id": "aws-access-key-id",
"path": "config/app.yaml",
"commit": {
"sha": "b719f81a0c…",
"author": "Jane Dev",
"email": "jane@example.com",
"date": "2024-03-02T11:07:14Z"
}
}Notes:
- Pair it with
--category secrets. History scanning runs whatever rules are selected; secrets are the use case it exists for. (The built-in default category issecurity, so without--category secretsyou would scan history with the code-vulnerability rules instead.) - Additions only. Each commit is compared to its first parent and only the lines it added are attributed to it, so a secret is reported at the commit that introduced it — with its true line number in that commit's file.
- Reported once. The same secret touched by many commits is de-duplicated to its earliest introducing commit.
--since <date>acceptsYYYY-MM-DDor an RFC3339 timestamp;--max-commits <n>caps how many commits are walked (0= no limit). Both require--history.- Hermetic. It reads only the local
.gitobject store — no network. To scan a remote repository's history, clone it with full history first (xgrep scan <url> --full-clone) or check it out locally, then run--history. - Mutually exclusive with
--baseline-commitand stdin input.
Decoding encoded payloads (--decode)
Secrets are often committed one encoding layer deep — a base64-wrapped .env, a
gzip'd Kubernetes secret, a token inside a percent-encoded URL. To a normal scan
that outer blob is opaque, so the credential inside it is missed. --decode
decodes encoded payloads and re-runs the secret/generic rules over the decoded
content, so the hidden token is found and reported at the encoded span in the
original file.
# Find secrets hidden inside base64 / hex / url / gzip payloads
xgrep --decode --category secrets .
# Combine with history to sweep encoded secrets across deleted code too
xgrep --history --decode --category secrets .Each decoded finding records the decode chain in metadata.decoded-from, and its
position points at the encoded blob you can see in the file:
{
"check_id": "aws-access-key-id",
"path": "config/app.yaml",
"start": { "line": 12, "col": 14 },
"extra": {
"metadata": { "decoded-from": "base64 > gzip" }
}
}Notes:
- Opt-in. Off by default; a scan without
--decodeis byte-for-byte unchanged. Decoders supported today:base64(standard + URL-safe),hex,url(percent-encoding), andgzip/zlib(peeled when nested). - Bounded. Nesting depth, per-payload input/output size, and per-file budgets are all capped, and an over-sized inflation is abandoned — so a decompression bomb cannot blow up a scan.
- Precise. Decoded bytes that are not printable text are dropped before matching, so decoding never manufactures false positives; it only adds findings on content that really decodes to a credential.
- Hermetic. Decoding is a local, deterministic transform — no network, no temp files. Decoded content is scanned as text, never parsed back into the language/AST engine.
- Pair it with
--category secretsfor the intended use case (the built-in default category issecurity).
Selecting what to scan (code vs. dependencies)
A scan has two independent halves: the rule/SAST scan (code + secrets,
narrowed by --category) and the dependency vulnerability scan (the SBOM).
Both run by default; two flags turn either half off:
--no-dep-scan— run the rule scan only (no dependency findings).--no-code-scan— run the dependency scan only (no code/secret findings).
xgrep --no-code-scan <target> # dependency vulnerabilities only
xgrep --no-dep-scan <target> # code + secrets onlySetting both leaves nothing to scan and is rejected with an error.
--no-code-scan cannot be combined with --history (a code/secrets walk).
Validating secrets (--validate)
Detection finds strings that look like credentials; --validate confirms
whether one is actually live by probing its provider. It turns a
finding's validation_state into:
confirmed— the provider accepted the secret; it is live (act now).unconfirmed— the provider rejected it; revoked/invalid (lower urgency).error— could not determine (network issue / unexpected response).unvalidated— not probed (the default; no--validate, or the rule has no validation endpoint).
xgrep --validate --category secrets <target>{ "check_id": "github-personal-access-token", "extra": { "validation_state": "confirmed" } }This is opt-in and off by default — it is the one mode that makes outbound network calls, so the hermetic default is preserved unless you ask for it:
- The candidate secret is sent only to that rule's fixed provider endpoint (built into the rule, not anything from the scanned content), and is never logged or written to disk — it is held in memory only for the probe.
- Validation runs after detection; it never changes which findings are reported,
only their
validation_state. Probes are bounded by a small concurrency limit and short timeouts to stay gentle on provider APIs. - Only rules with a provider introspection endpoint are validated; the rest stay
unvalidated. Validators currently ship for GitHub, GitLab, Slack, and Stripe tokens (more to follow). - Severity follows the result. A validatable secret is reported at its
reduced
unvalidated-severityuntil proven live; a confirmed finding is raised to the rule's full severity, while unconfirmed/error keep the reduced one. So--validatesharpens the signal — live keys rise to the top, dead ones stay low — rather than just adding a field.
Validating GitHub Action pins (--validate)
The same --validate flag also verifies GitHub Action SHA pins. Pinning an
action to a commit SHA with a version comment is the recommended practice:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1The static rules (in the default scan, no flag needed) check the shape of this
line — that a SHA pin has a version comment, uses a full-length SHA, and isn't
annotated with a mutable ref like # latest. What they cannot check offline
is whether the comment tells the truth: that b4ffde6… really is the commit the
v4.1.1 tag points to. A comment that lies — through drift or a malicious PR —
slips a wrong or hostile commit past a reviewer who trusts the # v4.1.1 they
see.
You must pass --validate to get this check — like secret validation, it is
the one mode that makes outbound calls (it resolves the tag through the GitHub
REST API), so it is off by default. Without --validate, the
actions-sha-pin-version-mismatch rule stays silent: a version-commented pin is
the good state, so it is only ever surfaced as a finding when the API proves the
comment is wrong.
xgrep --validate <target>When enabled, each pin's comment is checked and a mismatch is reported at a severity that reflects how wrong it is:
CRITICAL— the pinned SHA matches no published release tag of the action: an off-release (arbitrary, fork, or unreleased) commit posing as a trusted version.HIGH— the version named in the comment does not exist (deleted/yanked or fabricated), though the SHA is some other real release.MEDIUM— the comment names a real but different release than the SHA.- Honest pins, and pins that can't be checked (network/rate-limit), produce no finding.
Notes:
- Set
GITHUB_TOKENto raise the GitHub API rate limit (the same token pinact uses). - For GitHub Enterprise Server, point the resolver at your instance with
XGREP_GITHUB_API_BASE=https://ghe.example.com/api/v3. - Honest pins cost a single tag lookup; the heavier release-tag listing runs only when a comment doesn't match, so validation stays cheap on a clean repo.
- The version comment must be
v-prefixed (# v4.1.1) — the form pinact, Dependabot, and Renovate emit. Bare-semver comments (# 4.0.2) are left to the static shape checks.
Redacting secret values (--redact)
By default a secret finding prints the matched line verbatim, which includes the
detected credential — fine for a local review, but a leak if the output is piped
into CI logs or another system. --redact replaces every detected secret value
with [REDACTED] while keeping everything a triage needs: the rule ID, file,
line, and fingerprint.
xgrep --redact --category secrets <target>{
"check_id": "github-personal-access-token",
"extra": { "lines": "github_token = \"[REDACTED]\"", "fingerprint": "0e1eb049fd5121aa" }
}- Opt-in, off by default — output is unchanged unless you pass
--redact. - Covers every output format and the Mondoo upload. The masking runs once
over the assembled report before rendering and before any upload, so the raw
value never leaves the process — in text,
--json,--sarif,--gitlab,--fex, or the platform report. The matched line, captured metavariables, and any dataflow-trace snippets are all scrubbed. - The fingerprint is unchanged. It is a one-way hash computed before redaction, so deduplication and cross-run stability are unaffected — a redacted and an unredacted scan of the same code produce the same fingerprints.
- Not compatible with
--streamor--sqlite, which emit or persist matched content during the scan, before the redaction pass runs. Combining them errors rather than silently leaking. Works with--validate: the secret is still probed (validation reads the raw match at scan time) and thevalidation_stateis reported with the value hidden.
Overview
Run xgrep's SAST (static application security testing) engine against your code — taint and dataflow analysis that traces untrusted input to dangerous sinks, plus output formats, supported languages, Semgrep parity, and custom rules.
Output formats
xgrep emits human-readable text, Semgrep-compatible JSON, SARIF 2.1.0, a GitLab SAST report, or a CycloneDX 1.6 CBOM.