Output Formats
xgrep emits human-readable text, Semgrep-compatible JSON, SARIF 2.1.0, a GitLab SAST report, or a CycloneDX 1.6 CBOM.
Output Formats
Text (default)
src/app.py:10:my-rule: Avoid using eval()JSON
xgrep -f rules.yaml --json src/JSON output is a Semgrep-compatible ScanReport with a results array. Each result
carries check_id, path, start/end positions (each with line, col, and
byte offset), and an extra object with a short human-readable title
(schema_version 1.7), the full message, severity (INFO, WARNING, ERROR — the
Semgrep-compatible names, kept stable for machine consumers), a promoted
confidence (HIGH/MEDIUM/LOW, schema_version 1.7), captured metavars, rule
metadata (CWE, OWASP, …), and the matched lines. xgrep additionally includes
always-on context and remediation guidance per finding, plus a structured
fix_info (concrete replacement edits and/or remediation hint) whenever the
rule can offer one. Add --with-overview to prepend a code-graph summary of the
scanned tree to the report (schema_version 1.1).
Human output (the terminal summary and the plain-text format) shows severity on the security-standard ladder — CRITICAL / HIGH / MEDIUM / LOW — while the JSON
results[].extra.severity, SARIFlevel, and GitLab/LSP mappings keep their respective machine vocabularies. The two rank 1:1 (ERROR≡HIGH, WARNING≡MEDIUM, INFO≡LOW), and--severity/--max-severityaccept either.
Run summary (run)
A --json report also carries a top-level run object (schema_version 1.6) — a
machine-readable statement of what the scan did, distinct from the per-finding
results. It lets an automated caller tell "scanned and uploaded N findings to
<scope>" apart from "scanned but uploaded nothing" without inferring from the
exit code:
"run": {
"target": "https://github.com/acme/app",
"scan": {
"categories": ["secrets", "security"], "code": true, "dependencies": true,
"files_skipped_out_of_scope": 12
},
"repository": { "url": "…", "branch": "main", "revision": "9f2c…", "on_default_branch": true },
"findings": {
"total": 42,
"by_category": { "security": 30, "secrets": 12 },
"by_severity": { "CRITICAL": 2, "HIGH": 18, "MEDIUM": 15, "LOW": 7 }
},
"upload": {
"requested": true, "performed": true, "skip_reason": "",
"scope_mrn": "//…/spaces/…", "asset": "acme/app",
"findings_uploaded": 42, "findings_total": 42, "truncated": false,
"dep_vex_uploaded": 3
},
"dependencies": {
"requested": true, "performed": true, "skip_reason": "",
"packages_submitted": 20, "packages_correlated": 6,
"packages_uncorrelatable": 0, "advisories": 9,
"ecosystems": [
{ "name": "maven", "submitted": 9, "correlated": 2 },
{ "name": "npm", "submitted": 11, "correlated": 4 }
]
}
}scan.files_skipped_out_of_scope (schema_version 1.10) is how many files
production scope kept the security and correctness rules
from running on — test, spec, fixture, example, and docs paths. It is a file
count, not a finding count: those rules are skipped before they are evaluated, so
the findings they would have produced were never computed. Those files were still
read and secrets-scanned. The field is omitted when nothing was skipped (and
under --include-tests, which turns production scope off), so its presence always
means the scan covered less than the path you pointed at.
upload.performed reports whether findings actually reached Mondoo Platform, and
upload.skip_reason explains a non-upload — one of incognito, no-credentials,
not-default-branch, or error (empty when performed). truncated is true when
the upload cap kept only the highest-severity findings (findings_uploaded < findings_total). The run object is emitted for the batch --json report only
(not --stream); the human formats omit it.
Dependency coverage (run.dependencies)
dependencies (schema_version 1.11) reports what the dependency pass did, so an
empty dependency result can be read correctly. Without it, "no dependency
findings" has three indistinguishable meanings: nothing vulnerable, the pass never
ran, or the advisory service returned nothing for the packages asked about.
performedis whether the pass actually ran — packages submitted and a response received.scan.dependenciescarries the same value.skip_reasonexplains aperformed: false:disabled(--no-dep-scan),no-manifests,sbom-error,no-credentials, orerror.packages_submittedandpackages_correlatedare how many packages were sent for matching and how many of them at least one returned advisory landed on.packages_uncorrelatablecounts packages carrying no package URL. Matching is package-URL-based, so these can never match, whatever the advisory data holds — a different fact from "no advisory matched".ecosystemsrepeats the split per package-URL type. This is the row to read when a result looks short:{"name": "maven", "submitted": 9, "correlated": 0}says nine maven packages were asked about and nothing came back for any of them, which is not the same statement as "these nine packages are fine".
The counts are descriptive. A clean project correlates zero packages, and xgrep does not editorialise: it reports the question it asked and the answer it got.
Incomplete file coverage (paths.partial, paths.timed_out)
paths.scanned lists every file the scan read. Two of them can be read without
being fully analysed, because the per-file --timeout bounds how long any one
file may take, and paths names both cases (schema_version 1.13):
{
"paths": {
"scanned": ["src/app.js", "vendor/bundle.js", "src/big.java"],
"partial": [
{ "path": "src/big.java", "stopped_at_rule": "java-sql-injection", "rules_skipped": 326 }
],
"timed_out": ["vendor/bundle.js"]
}
}partial— the file's deadline expired part-way through the rule list, so the rules after that point never ran on it. Its findings are real; there are just fewer rules behind them.stopped_at_ruleis the rule being evaluated when the deadline hit andrules_skippedis how many never ran, which is the difference between a file that lost one rule and one that lost three hundred.timed_out— the file's findings were discarded. Evaluation did not stop when asked, so the whole file's results were given up and nothing was reported for it.
Both are omitted when a scan loses no coverage, so their presence always
means some of the report is incomplete. A file in either list stays in scanned
— it was read, and scanned keeps meaning "what xgrep read" — so read these two
as the caveats on it. Each affected file also gets a warning in errors
naming what was lost.
Read them whenever an empty or shrunken result matters. Without them, a file
whose results were thrown away is indistinguishable from a clean one, and a
before/after comparison of two scans can differ purely because a large file
truncated in one run and not the other. Comparing scans is the case that needs
this most: exclude the files these two lists name, and what remains is
comparable. Raise --timeout (or --timeout 0 to disable it) to shrink both
lists.
Per-rule finding volume (rule_findings)
A --json report also carries a top-level rule_findings array (schema_version
1.8): for every rule that produced at least one finding, how many findings it made
(count) and across how many distinct files (files_hit), sorted highest-count
first. It is a compact per-rule volume rollup for spotting a single rule that
dominates a scan's output, without walking the full results array:
"rule_findings": [
{ "id": "java-path-traversal-http-request", "count": 27, "files_hit": 9 },
{ "id": "generic-hardcoded-secret", "count": 3, "files_hit": 2 }
]Every code-scan finding is counted once, including any later downgraded by
severity or confidence. Suppressed (ignored) findings and
dependency-vulnerability (SCA) findings are excluded — the rollup ranks code-scan
rules, not advisories. The array is emitted for the batch --json report only
(not --stream) and is omitted when no code-scan rule fired.
Original source positions for bundled code (original_location)
A finding in a bundled or minified JavaScript file is reported at a position
nobody can act on — main.a1b2.js:1:48022 names a column in a file that is
never edited. When the scanned file carries a source map, xgrep adds the
position in the code the bundle was built from:
dist/main.js:1:489: AWS access key ID
[aws-access-key-id] (MEDIUM) confidence: HIGH
original: src/routes/user.ts:8:23 (via dist/main.js.map)
| const accessKeyId = "AKIAQYLPMN5HXTB3CDEF";
> AKIAQYLPMN5HXTB3CDEFand, in --json, an original_location object on the finding (schema_version
1.12):
"original_location": {
"path": "src/routes/user.ts", // resolved against the map's directory
"source": "../src/routes/user.ts", // exactly what the map declares
"line": 8,
"col": 23,
"name": "accessKeyId", // the identifier before minification
"snippet": "const accessKeyId = \"AKIA…\";",
"source_map": "dist/main.js.map" // or "inline" for a data: URL
}This is an addition, never a replacement. path, start and end keep
naming the generated file: they are what the byte offsets, the matched lines
excerpt and any fix contract refer to, and a source map maps single positions
rather than ranges, so it cannot re-describe a span.
The map is found the way a browser finds it: a trailing //# sourceMappingURL=
comment (the legacy //@ and the /*# … */ forms are read too) naming a path
relative to the bundle, a data:application/json;base64,… URL carrying the map
inline, or — when the file declares nothing — a <file>.map sitting beside it.
snippet and name come from the map's own sourcesContent and names; the
original file is never read from disk, so the excerpt is always the revision
that was actually built.
Three things it deliberately does not do:
- A remote
sourceMappingURLis not fetched. A map named by anhttp(s)URL is a build artifact that belongs beside the bundle; a scanner does not make network requests on the say-so of the file it is scanning. - An absolute
sourceMappingURLis not followed. It is a path from the machine that produced the build, so on yours it names a different file. - A section of an index map that names its map by URL is not read. The
sectionsform itself IS composed — see below — but a section that points at a map to fetch rather than carrying one is refused, for the same reason a remotesourceMappingURLis.
Whenever a map is absent, unreadable, or simply does not cover the finding's
position — maps are sparse, and generated runtime glue maps to nothing at all —
the finding is reported exactly as before, with no original_location. Nothing
is guessed. Run with --verbose to see, per file, how many findings were
offered to the map, how many resolved, and why the rest did not.
Both map shapes are read: the ordinary one, and the index map (sections)
that describes one output file as a series of offset sub-maps. Every mapping
form a bundler writes in practice has been checked against real output from
esbuild, rollup, webpack and a rollup→terser chain, and against the decoder the
rest of the JavaScript ecosystem reads maps with, over several hundred published
.map files.
The reported source is the map's own string. For a bundle it is usually
relative to the map (../src/routes/user.ts) and sometimes a generator's URL
(webpack://app/src/user.js); sourceRoot is applied and ./.. inside the
result resolve, matching what a browser or a stack-trace service shows for the
same position. path is the best-effort resolution of that against the
directory the map was read from, and is empty when the source is a pseudo-URL or
an absolute path from the build machine — cases where no honest local path
exists.
A bundle that carries a source map is scanned by default, which is what makes this remap reachable on a deployed front end without any flag — a vendored library's bundle stays skipped even though it ships a map of its own. See File filtering for the full rule and the flags that override it.
Streaming JSON (NDJSON)
xgrep -f rules.yaml --json --stream src/--stream turns --json into newline-delimited JSON (NDJSON), emitted as each
file is scanned rather than as one document at the end — so editors and tools can
render findings while the scan is still running. Each line is a typed envelope:
{"type":"finding","check_id":"…","path":"a.py","start":{…},"extra":{…}}
{"type":"finding","check_id":"…","path":"b.py","start":{…},"extra":{…}}
{"type":"summary","findings":2,"files":2,"errors":0}A finding line carries the same fields as an element of the batch results
array, plus a type discriminator. The single trailing summary line closes the
stream. Findings are emitted in file-completion order (not path-sorted), already
deduplicated and production-scope-filtered per file, so the streamed set matches
the batch report. --stream requires --json and cannot be combined with
--sarif/--gitlab, --validate, --baseline-commit, --history, or
--with-overview.
Consuming findings (light-mode & agent integrations)
A finding already carries everything an integration needs to render actionable output with no LLM in the loop — a review comment, a ranked triage entry, or a GitHub ```suggestion block. The fields most consumers under-read:
| You want… | Read this field | Notes |
|---|---|---|
| A short header | extra.title | Curated per rule; distinct from the machine check_id and the paragraph message. |
| The problem / "why" | extra.message | Full explanation. |
| The fix guidance ("how") | extra.fix_info.hint | Prose remediation; present whenever the rule offers remediation. |
| A ```suggestion-ready patch | extra.fix_info.patch | Whole-line unified diff, present for deterministic fixes (ADR-0435). Render a suggestion straight from it — no byte→line conversion. See "GitHub suggestions" below. |
| A concrete replacement | extra.fix / extra.fix_info.edits[] | edits[] carry start_byte/end_byte (+ line/col) and a replacement string (the replacement for that byte span — may be a partial line). Prefer fix_info.patch for suggestions; edits[] are for SARIF/LSP. Present only for rules with an authored fix:/fix-regex:. |
| The exact span to rewrite | start/end .col (and .offset) | Column-precise, so a suggestion can target the vulnerable expression, not the whole line. |
| Ranking / triage signal | extra.confidence, extra.severity | confidence is HIGH/MEDIUM/LOW; severity ranks 1:1 with the CRITICAL/HIGH/MEDIUM/LOW ladder. |
| Classification tags | extra.metadata.cwe, extra.metadata.owasp | Also category, likelihood, impact (a level, not prose), technology. |
| Taint source → sink | dataflow_trace | Source location, intermediate steps, and sink for taint-mode findings. |
Example (trimmed):
{
"check_id": "go-weak-hash",
"path": "crypto.go",
"start": { "line": 3, "col": 34, "offset": 71 },
"end": { "line": 3, "col": 45, "offset": 82 },
"extra": {
"title": "Weak hash",
"message": "Use of weak hash algorithm detected (MD5 or SHA-1) …",
"confidence": "HIGH",
"severity": "WARNING",
"fix_info": { "kind": "assisted", "hint": "Use crypto/sha256 or crypto/sha512 …" },
"metadata": { "cwe": ["CWE-328: Use of Weak Hash"], "owasp": ["A02:2021", "A04:2025"] },
},
}Not every finding carries a concrete extra.fix — only rules with an authored
fix:/fix-regex: template do (coverage is being widened; see ADR-0431). When
extra.fix is empty, fall back to title + message + fix_info.hint. Agents
that need to confirm a match or validate an authored fix in-sandbox can drive the
xgrep mcp server's read-only scan and
fix_verify tools rather than re-parsing output.
GitHub suggestions from a fix patch (ADR-0435)
Deterministic findings carry extra.fix_info.patch — a whole-line unified diff
already anchored to source lines, so a light-mode consumer renders a
```suggestion with no byte→line math of its own. The xgrep fix verify/apply
path emits the same tight patch (plus a wider, human-readable diff) on each
assisted (agent-authored) FixOutcome, with an explicit verified boolean
and the tier in kind.
Parse-free line ranges. Both surfaces also carry hunks — an array of
{start_line, end_line} (1-based, inclusive) giving the original lines each hunk
replaces, so you can anchor the suggestion without parsing the patch at all.
Use hunks[i] for the comment range and the patch's +/context lines for the
replacement text.
To turn one diff hunk into a suggestion:
- the range is
hunks[i](or, from the patch, the@@ -l,s +… @@header:l…l+s-1); - the replacement text is the hunk's
(context) and+lines with the one-character prefix stripped, in order (drop-lines); - anchor the review comment on that line range and emit the text verbatim inside a ```suggestion block.
A patch with exactly one hunk on one file (len(hunks) == 1) maps to a
single suggestion. More than one hunk, or a cross-file changeset
(FixOutcome.paths has >1 entry, ADR-0258), cannot be one suggestion — render
prose instead. A \ No newline at end of file marker means the replaced region
ends the file without a trailing newline; preserve that when applying.
SARIF
xgrep -f rules.yaml --sarif src/SARIF 2.1.0 output integrates with GitHub Code Scanning and other SARIF consumers. See CI integration.
Findings suppressed inline with a # nogrep / # nosemgrep directive are
omitted from SARIF by default. GitHub code scanning ignores the SARIF
result.suppressions array, so a suppressed finding would otherwise import as an
open alert — and a suppressed line that later moves can fail a pull-request check
as a "new" alert. When suppressed findings are present, xgrep prints a warning to
stderr noting they were omitted. To restore the old behavior and emit them with a
result.suppressions array (for consumers that honor it), pass
--sarif-include-suppressed. Other output formats (--json, text, --gitlab)
are unaffected: they always carry suppressed findings, marked is_ignored: true.
When a scan is narrowed by production scope, the run's
invocations[] entry carries the same file count as the JSON report, as the
property files-skipped-out-of-scope:
"invocations": [
{ "executionSuccessful": true, "properties": { "files-skipped-out-of-scope": 12 } }
]Like the JSON field, it counts files whose security/correctness rules did not
run, and it is absent when nothing was skipped. The same invocations[] entry
carries a toolExecutionNotifications message per scan-level error (a parse
failure, a rule that would not load), so a partial or narrowed scan is never
indistinguishable from a clean full one.
GitLab SAST
xgrep -f rules.yaml --gitlab -o gl-sast-report.json src/GitLab's Vulnerability Report consumes its own Secure SAST format rather than
SARIF, so use --gitlab (not --sarif) for GitLab. The report maps each finding
to a GitLab vulnerability with a stable id (for cross-scan tracking), a
title-cased severity (Critical/High/Medium/Low), a line-based location, and
identifiers for the rule plus any CWE/OWASP metadata. Publish it as a
reports: sast: artifact in your GitLab CI job. See
CI integration.
CBOM (Cryptography Bill of Materials)
A CBOM is a bill of materials, so it is produced by the sbom
command rather than as a scan output format:
xgrep sbom --include cbom .This emits a CycloneDX 1.6
Cryptography Bill of Materials — a machine-readable inventory of the cryptographic
assets in your code (algorithms, key material, protocols, certificates), enriched
with OID, primitive, key size, and classical/post-quantum security levels. It is
detected by the scan engine but does not add noise to a normal xgrep scan. See
CBOM for the full guide: language/library coverage, the asset
and dependency-graph structure, and how to consume the document.
Reporting to Mondoo Platform
The formats above all write locally — to stdout or a file. xgrep additionally
uploads findings to Mondoo Platform whenever a Mondoo service account is configured
(via xgrep login
or a config file), where they attach to the scanned repository as an asset.
Reporting is automatic on the repository's default branch, additive (local output
is unchanged), and best-effort (a failed upload never fails the scan):
xgrep scan . # reports automatically when logged in (default branch only)
xgrep scan --incognito . # local only; upload nothingSee Mondoo Platform for credentials, asset/severity mapping, and CI usage.