CI Integration
Run xgrep in CI and publish results to GitHub Code Scanning or GitLab SAST.
CI Integration

xgrep emits native report formats for both major platforms. Run it from the
repository root (or pass --project-root) so file paths in the report match
the repo and findings attach to the right lines. Combine with
--baseline-commit to scan only what a
pull request changed. See Output formats.
Installing xgrep in CI
The workflows below assume xgrep is on the runner's PATH. The simplest way to
get it there is the npm package,
which ships prebuilt binaries (Linux, macOS, and Windows, amd64 and arm64) and
works on any runner with Node.js:
npm install -g @mondoohq/xgrep # adds the `xgrep` command to PATH
xgrep versionOr run it without a global install via npx @mondoohq/xgrep … — drop-in for
xgrep … in any of the steps below. Pin a version (@mondoohq/xgrep@<version>)
for reproducible builds instead of floating on the latest release.
GitHub-hosted and GitLab node:* runners ship Node.js, so no extra setup step is
needed; on other images add Node first (actions/setup-node on GitHub, a
node:lts image on GitLab — both shown below). See Installation
for details.
Exit codes
xgrep follows the Semgrep/Opengrep convention:
| Code | Meaning |
|---|---|
0 | Scan completed (with or without findings) |
1 | Findings found — only when --error is passed |
2 | Fatal error (e.g. rules failed to load) |
A completed scan exits 0 even when it reports findings, so the
upload/report step still runs. Add --error to hard-fail the job on any
finding (scope it with --severity, e.g. --severity ERROR --error). Let the
platform (GitHub Code Scanning, GitLab) gate via policy when you prefer
upload-then-gate over a hard CLI gate.
For gradual rollout, --max-findings N fails the job (exit 1) only when the
active finding count exceeds N — so you can adopt a ruleset on a repo with
known findings, gate at the current count, and tighten over time. When set it
takes precedence over --error and gates on its own (--max-findings 0 is
equivalent to --error).
Diff-aware scanning (pull requests)
For pull-request checks, --baseline-commit scopes the scan to the files changed
since a baseline and reports only findings on changed lines — so a PR check
parses and runs rules over just what changed, not the whole repository. This is
the same flag (and behavior) Semgrep/Opengrep use for diff-aware scanning.
xgrep -f rules/ --baseline-commit origin/main . # changes since origin/main
xgrep -f rules/ --baseline-commit origin/main..HEAD . # an explicit commit range
xgrep -f rules/ --baseline-commit HEAD . # uncommitted working-tree changes- The argument is a single ref (diffed against the working tree) or a
base..head/base...headrange. In CI, use the merge-base between the PR branch and its target — that is what should count as "new." - Only changed files are scanned; findings on unchanged lines of changed files
are dropped. With
--error, the job fails only when a changed line has a finding. - Run from the repository root; the content scanned is the working tree, so
the head should be the working tree /
HEAD. - Because unchanged files are skipped, interfile (cross-file) analysis sees only
the changed files. Drop
--baseline-commitfor a full-context scan (e.g. a scheduled scan of the default branch).
GitHub Actions PR example (combine with the SARIF upload below):
on: pull_request
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # need history so the baseline ref is available
- run: npm install -g @mondoohq/xgrep
- name: Diff-aware scan
run: |
xgrep -f rules/ \
--baseline-commit "${{ github.event.pull_request.base.sha }}" \
--sarif -o results.sarif .GitLab CI resolves the target branch via CI_MERGE_REQUEST_DIFF_BASE_SHA:
xgrep-mr:
image: node:lts
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
before_script:
- npm install -g @mondoohq/xgrep
script:
- xgrep -f rules/ --baseline-commit "$CI_MERGE_REQUEST_DIFF_BASE_SHA" --gitlab -o gl-sast-report.json .GitHub Code Scanning (SARIF)
# .github/workflows/xgrep.yml
name: xgrep
on: [push, pull_request]
permissions:
contents: read
security-events: write # required to upload SARIF
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install -g @mondoohq/xgrep
- name: Run xgrep
run: xgrep -f rules/ --sarif -o results.sarif .
- name: Upload SARIF
if: always() # upload even when findings make xgrep exit non-zero
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
category: xgrep # matches automationDetails.id (set via --sarif-category)xgrep fills in partialFingerprints (stable alert tracking), security-severity
(severity ranking), CWE/OWASP tags, automationDetails.id, and
versionControlProvenance automatically.
GitLab SAST
# .gitlab-ci.yml
xgrep-sast:
stage: test
image: node:lts
before_script:
- npm install -g @mondoohq/xgrep
script:
- xgrep -f rules/ --gitlab -o gl-sast-report.json .
artifacts:
reports:
sast: gl-sast-report.jsonThe report appears in the merge request security widget and the Vulnerability
Report. scan.primary_identifiers lets GitLab mark findings resolved once they
disappear from a later scan.
xgrep ci — one-command CI scan
xgrep ci wraps scan with CI-aware defaults, so most pipelines need
a single line. It:
- Auto-detects the CI provider (GitHub Actions, GitLab CI, or a generic
CIenvironment). - Scans diff-aware by default against the pull/merge-request base — on
GitHub from
GITHUB_BASE_REF, refined to the merge-base withHEAD(the fork point) so changes the base branch picked up after the PR forked aren't blamed on the PR; needs full history, e.g.actions/checkoutwithfetch-depth: 0(it falls back to the base ref when the merge-base can't be computed). On GitLab it usesCI_MERGE_REQUEST_DIFF_BASE_SHA, which is already the merge-base. On a push / when no base is available it runs a full scan.--no-baselineforces a full scan;--baseline-commitoverrides the auto-derived base. - Defaults to SARIF (GitLab SAST under GitLab CI); override with
--json/--sarif/--gitlaband-o. - Exits per the table above (0 clean/findings, 1 with
--error, 2 fatal). - Honors
SEMGREP_BASELINE_REF/SEMGREP_REPO_URL(andXGREP_*equivalents), so an existing Semgrep/Opengrep CI step can switch by binary name. - Stays hermetic — it writes a report file for the platform to ingest and never uploads to a hosted backend.
# GitHub Actions
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- run: npm install -g @mondoohq/xgrep
- run: xgrep ci -f rules/ -o results.sarif
- if: always()
uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: results.sarif }Baseline precedence: --baseline-commit > SEMGREP_BASELINE_REF >
XGREP_BASELINE_REF > provider diff-base. An explicitly-set env var overrides the
auto-detected provider base (so a custom ref can override GITHUB_BASE_REF). The
auto-derived baseline (env var or provider ref, but not an explicit
--baseline-commit) is refined to its merge-base with HEAD; a base..head
range is used verbatim.
--max-findings (see Exit codes) gates gradual rollout. Parity
status is tracked in Semgrep compatibility.