Integrations

CI Integration

Run xgrep in CI and publish results to GitHub Code Scanning, GitLab SAST, or Azure DevOps.

CI Integration

xgrep ci on a simulated GitHub Actions pull request: it detects the CI environment, runs a diff-aware scan against the merge-base, writes a SARIF report, and flags the command injection the change introduced

xgrep emits native report formats for GitHub, GitLab, and Azure DevOps (SARIF). 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 version

Or 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:

CodeMeaning
0Scan completed (with or without findings)
1Findings found — only when --error is passed
2Fatal 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 --baseline-commit origin/main .       # changes since origin/main
xgrep --baseline-commit origin/main..HEAD . # an explicit commit range
xgrep --baseline-commit HEAD .              # uncommitted working-tree changes
  • The argument is a single ref (diffed against the working tree) or a base..head / base...head range. 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-commit for 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 \
            --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 --baseline-commit "$CI_MERGE_REQUEST_DIFF_BASE_SHA" --gitlab -o gl-sast-report.json .

On GitHub the simplest setup is the official mondoohq/actions/xgrep action. It installs xgrep from the npm package, runs xgrep ci (diff-aware on pull requests, full-tree on push), and writes a SARIF report — so the whole scan is one step. No Mondoo account or service token is required; the action is hermetic and never uploads to a hosted backend.

# .github/workflows/xgrep.yml
name: xgrep
on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read
  security-events: write # required to upload SARIF (when Code Security is enabled)

jobs:
  scan:
    name: xgrep SAST
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0 # full history for diff-aware scanning on pull requests

      - name: Run xgrep
        uses: mondoohq/actions/xgrep@v13.2.0
        with:
          path: .
          output-file: results.sarif
          # Report-only to start: findings surface in the Security tab without
          # failing the build. Tighten to `error` once the backlog is triaged.
          fail-on: 'off'

      - name: Upload SARIF to code scanning
        if: always() # don't lose findings if the scan step errors
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: results.sarif
          category: xgrep # must match the action's sarif-category input

Action inputs

InputDefaultDescription
path.Path(s) to scan.
rules(built-ins)Custom rule file/directory (-c). Empty = xgrep's built-in security + secrets rules.
with-builtin""When rules is set, also run these built-in categories (e.g. security,secrets).
output-fileresults.sarifPath for the SARIF report.
versionlatest@mondoohq/xgrep npm version to install (pin for reproducible builds).
sarif-categoryxgrepSARIF category — must match category on the upload step.
fail-onoffFail the job when findings exist at/above this severity: off, warning, or error.
args""Extra raw flags passed through to xgrep ci (e.g. --exclude vendor).

Gradual rollout: keep fail-on: "off" while triaging the initial backlog, so findings show up in the Security tab without breaking the build, then flip to error (or warning) to gate PRs. On a private repo without GitHub Advanced Security, the Security tab isn't available — see GitHub without Code Scanning for the job-summary + artifact reporting pattern instead. For more hardening examples (SHA-pinned actions, fallbacks), see the mhunt integration.

GitHub Code Scanning (SARIF) — manual

When you'd rather not use the action (custom rule layout, non-ci invocation, or an existing install step), wire up the npm package and SARIF upload directly:

# .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 --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.

GitHub without Code Scanning (no GHAS)

Uploading SARIF to the Security tab needs GitHub code scanning. It's free on public repositories, but on private ones it requires GitHub Advanced Security — now GitHub Code Security (security-events: write plus a license). When that isn't available, skip upload-sarif entirely and report findings two other ways:

  • Render a table in the job summary and run log — severity · rule · file:line · message — so reviewers see results inline on the run, with a clean "No findings" fallback.
  • Attach the raw SARIF as a workflow artifact for download and offline triage.

This keeps permissions down to contents: read (no security-events: write), so it works on any private repo. The scan still runs the recommended mondoohq/actions/xgrep action; only the reporting tail differs.

# .github/workflows/xgrep.yml
name: xgrep
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read # no security-events: write — we don't upload to Code Scanning

jobs:
  scan:
    name: xgrep security scan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0 # full history for diff-aware scanning on pull requests

      - name: Run xgrep
        uses: mondoohq/actions/xgrep@v13.2.0
        with:
          path: .
          output-file: results.sarif
          # Report-only: this repo has no Code Scanning, so findings are
          # summarized below instead of uploaded to the Security tab.
          fail-on: 'off'

      - name: Summarize findings
        if: always()
        env:
          SARIF: results.sarif
        run: |
          set -euo pipefail
          if [ ! -f "$SARIF" ]; then
            echo "xgrep produced no SARIF report." | tee -a "$GITHUB_STEP_SUMMARY"
            exit 0
          fi
          total=$(jq '[.runs[].results[]?] | length' "$SARIF")
          {
            echo "## xgrep security findings: ${total}"
            echo ""
            if [ "$total" -eq 0 ]; then
              echo "No findings."
            else
              echo "| Severity | Rule | Location | Message |"
              echo "| --- | --- | --- | --- |"
              jq -r '
                .runs[].results[]?
                | ((.level // "note")) as $sev
                | ((.ruleId // "?")) as $rule
                | (.locations[0].physicalLocation // {}) as $loc
                | (($loc.artifactLocation.uri // "?") + ":" + (($loc.region.startLine // 0)|tostring)) as $where
                | ((.message.text // "") | gsub("[\r\n]+";" ") | gsub("\\|";"\\\\|")) as $msg
                | "| \($sev) | \($rule) | `\($where)` | \($msg) |"
              ' "$SARIF"
            fi
          } | tee -a "$GITHUB_STEP_SUMMARY"

      - name: Upload SARIF artifact
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: xgrep-sarif
          path: results.sarif
          if-no-files-found: ignore

Both reporting steps use if: always() so findings still surface even when the scan step exits non-zero. Keep fail-on: "off" while triaging the initial backlog, then flip to error (or warning) to gate PRs. If GHAS is enabled on the repo later, switch to the Code Scanning pattern: add security-events: write and replace the summarize/artifact steps with github/codeql-action/upload-sarif.

GitLab SAST

# .gitlab-ci.yml
xgrep-sast:
  stage: test
  image: node:lts
  before_script:
    - npm install -g @mondoohq/xgrep
  script:
    - xgrep --gitlab -o gl-sast-report.json .
  artifacts:
    reports:
      sast: gl-sast-report.json

The 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.

Azure DevOps (Azure Pipelines)

xgrep ci auto-detects Azure Pipelines (via TF_BUILD). On a pull-request build it scans diff-aware against the PR target branch (System.PullRequest.TargetBranch, refined to the merge-base with HEAD); on a branch/CI build it runs a full scan. Both write SARIF, which Azure DevOps can display in the run and use to file work items.

Check out with full history (fetchDepth: 0) so the merge-base — and the origin/<target> ref it needs — are available; without it the scan still runs, just full-tree instead of diff-aware. Microsoft-hosted images ship Node.js and the az CLI, so no extra setup is needed.

# azure-pipelines.yml
trigger:
  branches: { include: [main] }
pr:
  branches: { include: [main] }

pool:
  vmImage: ubuntu-latest

steps:
  - checkout: self
    fetchDepth: 0 # full history for diff-aware PR scans

  - script: npm install -g @mondoohq/xgrep
    displayName: Install xgrep

  - script: xgrep ci -o results.sarif
    displayName: Run xgrep
    # Built-in security + secrets rules run by default; add `-f rules/` (or `-c`)
    # to point at your own ruleset. `xgrep ci` exits 0 even with findings so later
    # steps still run; add `--error` (optionally `--severity ERROR`) to hard-fail.

View findings in the run (SARIF)

Azure DevOps surfaces SARIF through the SARIF SAST Scans Tab extension (install it once per organization). The extension reads any *.sarif file published as a build artifact named CodeAnalysisLogs and renders the findings as a Scans tab on the run, annotated to file:line:

- task: PublishBuildArtifacts@1
  displayName: Publish SARIF
  condition: always() # publish even when findings make the scan step fail
  inputs:
    pathToPublish: results.sarif
    artifactName: CodeAnalysisLogs

This is the upload-then-gate pattern used for GitHub and GitLab: the report is always published, and you gate the build separately (CLI --error/--max-findings, or a branch policy).

File findings as work items

Azure DevOps has no native "ingest SARIF into Boards" step, so turn findings into work items with a script that parses the SARIF and calls az boards work-item create. Authenticate the az CLI with the pipeline's own token by exporting it as AZURE_DEVOPS_EXT_PAT. Map the predefined variables into the script's environment too — $(...) is command substitution in bash, so reference the mapped env vars ($COLLECTION_URI, $TEAM_PROJECT) rather than the $(System.…) macros inside the script body. The azure-devops az extension installs automatically on first use.

Don't re-file the same finding. xgrep writes a partialFingerprints (primaryLocationLineHash) value on every SARIF result — a hash of the matched line's content that is independent of the line number, so it stays the same when a finding moves within a file. That makes it a reliable dedup key: tag each work item xgrep-fp:<fingerprint> and, before creating, query for an existing open item with that tag and skip if one is found. Re-scans of moved or reindented code reuse the same key, so the same finding isn't re-filed.

- task: Bash@3
  displayName: File xgrep findings as work items
  condition: always()
  env:
    AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) # build-service identity
    COLLECTION_URI: $(System.CollectionUri)
    TEAM_PROJECT: $(System.TeamProject)
    MAX_WORK_ITEMS: 50 # cap items per run; 0 = no cap
  inputs:
    targetType: inline
    script: |
      set -euo pipefail
      [ -f results.sarif ] || { echo "no SARIF report"; exit 0; }
      # One work item per ERROR-level finding (.level is error/warning/note),
      # capped at MAX_WORK_ITEMS per run (0 = no cap) so a noisy scan can't flood
      # Boards. Read into an array first so the cap can break the loop without
      # SIGPIPE-ing jq under `set -o pipefail`.
      MAX="${MAX_WORK_ITEMS:-50}"
      mapfile -t findings < <(jq -c '.runs[].results[]? | select((.level // "note") == "error")' results.sarif)
      total=${#findings[@]}
      echo "Found $total ERROR-level finding(s); filing up to ${MAX} (0 = all)."
      filed=0
      for ((i = 0; i < total; i++)); do
        if [ "$MAX" -ne 0 ] && [ "$filed" -ge "$MAX" ]; then
          echo "Reached the $MAX-item cap; $((total - i)) more not filed."
          break
        fi
        r="${findings[$i]}"
        rule=$(jq -r '.ruleId // "?"' <<<"$r")
        msg=$(jq -r '.message.text // ""' <<<"$r")
        loc=$(jq -r '(.locations[0].physicalLocation.artifactLocation.uri // "?")
          + ":" + ((.locations[0].physicalLocation.region.startLine // 0)|tostring)' <<<"$r")
        # Stable dedup key: the first partialFingerprints value, falling back to
        # rule@location when a finding has none.
        fp=$(jq -r '(.partialFingerprints // {}) | to_entries[0].value // ""' <<<"$r")
        key=${fp:-"$rule@$loc"}
        # Escape single quotes before splicing into the WIQL string literal
        # (xgrep fingerprints are hex, but the rule@loc fallback may not be).
        safe_key=$(printf '%s' "$key" | sed "s/'/''/g")
        # Skip if an open work item already carries this fingerprint tag.
        existing=$(az boards query \
          --org "$COLLECTION_URI" --project "$TEAM_PROJECT" \
          --wiql "SELECT [System.Id] FROM workitems
                  WHERE [System.Tags] CONTAINS 'xgrep-fp:$safe_key'
                  AND [System.State] <> 'Closed'" \
          -o json | jq 'length')
        if [ "${existing:-0}" -gt 0 ]; then
          echo "skip (already filed): $key"
          continue
        fi
        # ';' delimits Azure tags, so System.Tags below sets two tags:
        # `xgrep` and `xgrep-fp:<key>`.
        az boards work-item create \
          --org "$COLLECTION_URI" \
          --project "$TEAM_PROJECT" \
          --type "Bug" \
          --title "xgrep: $rule at $loc" \
          --fields "System.Tags=xgrep; xgrep-fp:$key" \
                   "Microsoft.VSTS.TCM.ReproSteps=$msg" \
          --output none
        filed=$((filed + 1))
      done

The step runs as the build service identity — an automated account named <Project> Build Service (<org>), not the user who triggered the run — so System.AccessToken carries that account's rights, not yours. It needs work item read & write in the project (read for the dedup query, write to create), which it lacks by default; without it az boards returns 403. Grant it once, either way:

  • add the identity to the project Contributors group (Project Settings → Permissions → Contributors → Members), or
  • set Edit work items in this node → Allow on the area path (Project Settings → Boards → Project configuration → Areas → ⋯ → Security).

(If "Limit job authorization scope" is off, the identity is instead Project Collection Build Service (<org>).) Filing is scoped to error-level findings; drop the select(...) to file everything, or tighten it further. The MAX_WORK_ITEMS cap (default 50, 0 = unlimited) bounds how many one run files, so a large scan can't flood Boards.

The example files a Bug with the Bug-only Repro Steps field, which the Agile, Scrum, and CMMI processes define. A Basic project (the default for new organizations) has no Bug type — use Issue — and no Repro Steps field — use System.Description. Match --type and --fields to your project's process, or query the available types first with az devops invoke --area wit --resource workitemtypes --route-parameters project=$TEAM_PROJECT.

Tip. The dedup query reuses the tag, so re-running the step — on every PR or a scheduled default-branch scan — only files findings that don't already have an open item; closing the work item lets a still-present finding be re-filed. To close items whose finding has disappeared, query the xgrep tag and reconcile against the fingerprints in the latest SARIF.

Report to Mondoo Platform

Beyond the SARIF report, xgrep can publish findings to Mondoo Platform, where they attach to the repository as an asset and track your security posture over time. Reporting is automatic when a Mondoo service account is configured and the build runs on the repository's default branch — so pull requests and feature branches don't overwrite the asset. It's additive (the SARIF report and Scans tab are unaffected); pass --incognito to keep a run local-only.

The simplest way to supply the account in CI is MONDOO_CONFIG_BASE64 — the whole mondoo.yml, base64-encoded into one secret. Add it as a secret pipeline variable, then map it into the step's environment: Azure doesn't expose secret variables to script steps otherwise (the $(...) macro only expands for non-secret variables).

- script: xgrep ci -o results.sarif
  displayName: Run xgrep
  env:
    MONDOO_CONFIG_BASE64: $(MONDOO_CONFIG_BASE64) # secret pipeline variable

Generate the config once with xgrep login --token <registration-token> (or download a service account from the Mondoo Console), then encode it: base64 < mondoo.yml | tr -d '\n'.

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, Azure Pipelines, or a generic CI environment).
  • Scans diff-aware by default against the pull/merge-request base — on GitHub from GITHUB_BASE_REF, refined to the merge-base with HEAD (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/checkout with fetch-depth: 0 (it falls back to the base ref when the merge-base can't be computed). On GitLab it uses CI_MERGE_REQUEST_DIFF_BASE_SHA, which is already the merge-base. On Azure Pipelines it uses System.PullRequest.TargetBranch (normalized to origin/<branch>, then refined to the merge-base); check out with fetchDepth: 0. On a push / when no base is available it runs a full scan. --no-baseline forces a full scan; --baseline-commit overrides the auto-derived base.
  • Defaults to SARIF (GitLab SAST under GitLab CI); override with --json/--sarif/--gitlab and -o.
  • Scans dependencies for vulnerabilities alongside the code scan when a Mondoo service account is available (--mondoo-config / MONDOO_CONFIG_PATH); without one it warns once and skips. Disable with --no-dep-scan. See Scanning for vulnerabilities.
  • Exits per the table above (0 clean/findings, 1 with --error, 2 fatal).
  • Honors SEMGREP_BASELINE_REF / SEMGREP_REPO_URL (and XGREP_* equivalents), so an existing Semgrep/Opengrep CI step can switch by binary name.
  • Stays hermetic unless a Mondoo service account is configured — it writes a report file for the platform to ingest and uploads nothing to a hosted backend on its own. When a service account is present, dependency vulnerability scanning queries Mondoo Platform and findings are reported to it automatically (pass --incognito to keep a run local-only).
# GitHub Actions
- uses: actions/checkout@v4
  with: { fetch-depth: 0 }
- run: npm install -g @mondoohq/xgrep
- run: xgrep ci -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.

On this page