SecurityCustomize SecurityExceptions for Findings

Manage Exceptions in Your Code Repositories

Request exceptions in a mondoo.yml file next to your infrastructure as code, route their review with CODEOWNERS, and let cnspec submit them to Mondoo Platform after merge.

An exception created in Mondoo Platform is reviewed away from the code it excuses. For infrastructure as code, you can keep the exception with the code instead. An engineer writes the exception in a mondoo.yml file in the repository, justifies it in a pull request, and the people who own that code review it before it merges. cnspec reads the file on every scan. After the change merges, the next CI scan of the default branch submits the exception to Mondoo Platform, where it appears with the rest of your space's exceptions.

This approach fits teams that already review everything through pull requests:

  • Four-eyes review where the code lives. Branch protection blocks a merge until someone other than the author approves the change.
  • Owners files route the review. A CODEOWNERS entry for mondoo.yml automatically requests review from the right team, such as your security team or the team that owns a module.
  • A complete audit trail. The repository history records who asked for each exception, who approved it, and why.
  • Exceptions expire and retire on their own. An entry can carry an expiration date, and deleting it from the file ends the exception on the next default branch scan.

Preview

Exceptions in code repositories are in preview and require cnspec 14.4 or later. The mondoo.yml exception format, the --exceptions-submit flag, and the reported outcomes may still change.

How it works

  1. An engineer adds an entry to mondoo.yml in the pull request that needs it, with a justification and, usually, an expiration date.
  2. CODEOWNERS requests a review from the owning team, and branch protection blocks the merge until that team approves.
  3. The pull request's CI scan reads the file and reports the exception, but doesn't submit it to Mondoo Platform. A feature branch never turns into a shared exception.
  4. After the merge, the CI scan of the default branch submits the exception to Mondoo Platform.
  5. Mondoo Platform applies the exception right away, or holds it for a reviewer, depending on your space's exception settings.
  6. Every scan's output lists each exception from the file and whether it's in effect.

Write the exceptions file

Add an exceptions list to a file named mondoo.yml:

mondoo.yml
exceptions:
  - title: Central CloudTrail covers this
    checks:
      - mondoo-aws-security-s3-bucket-logging-enabled
    action: risk-accepted
    justification: Access logging is handled centrally by the organization-wide CloudTrail trail.
    valid_until: 2026-11-01

  - checks:
      - mondoo-aws-security-s3-bucket-public-read-prohibited
    paths:
      - infra/staging
    action: risk-accepted
    justification: Staging buckets serve public test fixtures.
    valid_until: 2026-12-31

Each entry accepts these fields:

FieldRequiredDescription
checksYesOne or more check UIDs or MRNs. Each check is its own exception. Globs aren't supported.
actionYesThe exception type: risk-accepted, workaround, false-positive, or disable.
justificationYesWhy the exception exists. Reviewers see it in the pull request and in Mondoo Platform.
valid_untilNoThe expiration date, as an RFC 3339 date (2026-11-01) or date and time. A date is valid through the end of that day, UTC. Leave it out for a non-expiring entry.
pathsNoPath prefixes that limit the entry to part of the repository. To learn more, read Scope entries with paths.
titleNoA short label that's shown in scan output and carried into Mondoo Platform.

The actions match the four exception types:

actionException typeEffect
risk-acceptedRisk AcceptedThe check runs but doesn't affect scores.
workaroundWorkaroundThe check runs but doesn't affect scores.
false-positiveFalse PositiveThe check runs but doesn't affect scores.
disableDisableThe check doesn't run.

valid_until has no effect on a disable entry, and cnspec warns if you set one. Exceptions in the file apply to checks only. You can't declare a check out of scope, and you can't except compliance controls, vulnerabilities, or advisories from a file.

cnspec validates every entry when it scans. Unknown fields are errors, so a misspelled valid-until is reported instead of silently never expiring. An entry with an error doesn't apply, and cnspec logs a warning that names the file and the entry's position, such as mondoo.yml: exception 2: has no justification.

Find a check's UID

A check's UID is the last segment of its MRN. For example, the check with the MRN //policy.api.mondoo.app/queries/mondoo-aws-security-s3-bucket-logging-enabled has the UID mondoo-aws-security-s3-bucket-logging-enabled. To list every check in a policy with its UID and MRN, download the policy. You can name a check by its full MRN instead of its UID.

Where to put the file

cnspec reads the mondoo.yml at the root of what you scan. Which file that is depends on the scan command:

ScanFile cnspec reads
cnspec scan iac <dir><dir>/mondoo.yml. It governs every asset the scan finds below that directory.
cnspec scan terraform <dir><dir>/mondoo.yml
cnspec scan terraform plan or statemondoo.yml in the directory that holds the plan or state file
cnspec scan github org or github repomondoo.yml at the root of each repository's default branch. It governs the repository asset and the Terraform asset that discovery finds in it.

cnspec reads only that one file. It doesn't look in parent directories or merge files from subdirectories. As a result, the same Terraform module can be governed by different files depending on how you scan it: cnspec scan iac . reads the repository root's file, while cnspec scan terraform infra/prod reads infra/prod/mondoo.yml. Scan output always names the file that governed each exception.

cnspec scan gitlab and cnspec scan azuredevops don't read mondoo.yml. To use exceptions in a GitLab or Azure Repos repository, scan a checkout of it in CI with cnspec scan iac or cnspec scan terraform.

To use exceptions with CloudFormation, Helm, Kubernetes manifests, Kustomize, Bicep, Ansible, or Dockerfiles, scan them with cnspec scan iac. Scanned directly with their own commands, those tools don't read mondoo.yml.

A repository file can only carry exceptions

Anyone who can open a pull request can edit mondoo.yml, so cnspec treats it as untrusted input. It reads only the exceptions key and ignores everything else. If the file contains credentials or an API endpoint, cnspec ignores them and warns, naming the file, because those values don't belong in version control. cnspec only reads a regular file of 1 MiB or less. It doesn't follow a mondoo.yml symlink.

Scope entries with paths

One file at the root of a repository governs every module in it. Without paths, accepting a risk for infra/staging would also accept it for infra/prod. paths limits an entry to the listed path prefixes, relative to the directory that holds mondoo.yml:

  • infra/staging matches the asset at infra/staging and every asset below it. It doesn't match infra/staging-2.
  • An entry without paths applies to every asset the file governs.
  • When two matching entries give the same check different actions, the entry with the longer prefix wins. If both prefixes are the same length, cnspec reports a conflict and neither entry applies.
  • Absolute paths and paths that contain .. are errors. A file can only speak for its own directory tree.
  • An asset with no path below the file, such as the repository asset in a cnspec scan github scan, is governed only by entries without paths.

cnspec scan github discovers one Terraform asset for the whole repository, so paths can't tell its modules apart. To scope entries to individual modules, scan a checkout of the repository with cnspec scan iac.

Route reviews with CODEOWNERS

An owners file decides who must approve a change to mondoo.yml. Choose a layout first, because cnspec reads only one file per scan:

  • One file at the repository root, scanned with cnspec scan iac .. One set of owners reviews every exception, and paths scopes each entry to a module.
  • One file per module, with each module scanned directly, for example cnspec scan terraform infra/prod. Each module's owners review that module's exceptions.

GitHub

Add the exceptions files to .github/CODEOWNERS. When several patterns match a file, the last match wins, so put the general rule first:

.github/CODEOWNERS
# The security team reviews every exceptions file
mondoo.yml                @lunalectric/security

# Production exceptions also need the platform team
/infra/prod/mondoo.yml    @lunalectric/platform @lunalectric/security

# Only the security team can change who reviews exceptions
/.github/CODEOWNERS       @lunalectric/security

Then protect the default branch with a ruleset or branch protection rule that turns on:

  • Require a pull request before merging, with at least one required approval. GitHub never counts the author's own approval, so every exception gets a second pair of eyes.
  • Require review from Code Owners, so the approval must come from the team the owners file names.
  • Require approval of the most recent reviewable push, so the author can't change an entry after it's approved.

GitLab

GitLab reads a CODEOWNERS file in the repository root, docs/, or .gitlab/, with the same pattern syntax and group names such as @lunalectric/security. To require the code owners' approval, turn on Code owner approval for the default branch in the project's Settings > Repository > Protected branches. Code owner approval requires GitLab Premium or Ultimate.

Azure Repos

Azure Repos has no owners file. Branch policies do the same job: a reviewer policy with a path filter requires the right team's approval whenever a pull request changes mondoo.yml. In Project settings > Repositories, select the repository, open Policies, and select the default branch under Branch Policies. Then:

  • Turn on Require a minimum number of reviewers with at least one reviewer. Clear Allow requestors to approve their own changes, select Prohibit the most recent pusher from approving their own changes, and under When new changes are pushed, select Reset all approval votes, so the author can't change an entry after it's approved.
  • Under Automatically included reviewers, add the team that reviews exceptions, such as your security team. Set Policy requirement to Required and set Filter by path to */mondoo.yml, so the team's approval is required only when a pull request changes an exceptions file. To have a second team review production exceptions, add another reviewer policy with the path filter /infra/prod/mondoo.yml.

Anyone who holds the Bypass policies when completing pull requests or Bypass policies when pushing permission on the repository can skip this review, so grant those permissions sparingly. To check who can bypass branch policies across your organization, scan Azure DevOps with cnspec.

Choose who approves in Mondoo Platform

When the default branch scan submits an exception, Mondoo Platform handles it like any other exception in the space. The space's Require exception approvals setting decides whether the repository review is the only approval or the first of two:

Require exception approvalsWhat happens after merge
On (default for new spaces)The exception waits on the space's Exceptions page until a team member with the Owner, Exception Reviewer, or Exception Manager role approves it. Scan output lists it as awaiting approval. Use this when your security team wants the final say.
OffThe exception applies as soon as it's submitted. The pull request review is the approval, and scan output reports the exception as auto-accepted.

Require exception approvals applies to every exception in the space, including those created in Mondoo Platform. If you want repository review to be the only approval for repository exceptions but not for others, scan your repositories into their own space. If the space doesn't allow non-expiring exceptions, give every entry a valid_until date. To learn about these settings, read Space-level exception settings.

A few rules keep the file and Mondoo Platform in step:

  • Deleting an entry retires the exception. Each submission is the complete set of entries in the file, so an entry that's gone from the file stops applying after the next default branch scan. There's nothing to revoke by hand.
  • A file only speaks for its own exceptions. It can't remove, weaken, or override an exception created in Mondoo Platform or by another file.
  • An exception from a file applies to the assets the file governs, not to the whole space.
  • Expired entries stay in the file. They no longer apply, and every scan reports them. cnspec warns about entries that expire within 14 days.
  • A rejected exception doesn't apply. Scan output reports it as rejected, with the reviewer's reason when there is one, and the check counts toward scores again.

Run the scans in CI

Decide which runs submit exceptions

The --exceptions-submit flag of cnspec scan decides whether a connected scan submits exceptions to Mondoo Platform:

ValueSubmits exceptions
auto (default)Only from a CI run that builds the default branch
alwaysFrom every connected scan
neverNever

With auto, cnspec decides whether a run builds the default branch from the variables your CI system sets. GitHub Actions and GitLab CI/CD report the default branch themselves. Azure Pipelines and CircleCI don't, so name it in the MONDOO_DEFAULT_BRANCH environment variable. Jenkins needs the variable outside multibranch pipelines.

CI systemSubmits fromNever submits from
GitHub ActionsA run on the repository's default branch, such as a pushPull requests, pull_request_target, merge queues, tags
GitLab CI/CDA pipeline where CI_COMMIT_BRANCH equals CI_DEFAULT_BRANCHMerge request pipelines, tags
Azure PipelinesA build where Build.SourceBranch is the branch that MONDOO_DEFAULT_BRANCH namesPull request builds, tags
CircleCIA job where CIRCLE_BRANCH is the branch that MONDOO_DEFAULT_BRANCH namesTags, pull requests from forks
JenkinsA multibranch pipeline build of the primary branch (BRANCH_IS_PRIMARY). Otherwise, a build where BRANCH_NAME or GIT_BRANCH is the branch MONDOO_DEFAULT_BRANCH namesChange requests, tags

Set MONDOO_DEFAULT_BRANCH to the branch name, such as main, or to refs/heads/main. When it's unset on Azure Pipelines, CircleCI, or a Jenkins job outside a multibranch pipeline, auto never submits. The variable has no effect on GitHub Actions and GitLab CI/CD, which report the default branch themselves, or outside CI, so a scan on a laptop never submits.

On every other CI system, auto never submits. Add --exceptions-submit always to a job that runs only on your default branch. Scheduled cnspec scan github scans that don't run as a default branch CI job also need --exceptions-submit always. They read mondoo.yml from each repository's default branch, so they always see merged entries.

How each kind of scan treats the file

ScanExceptions in the file
Connected, submitting (default branch CI)Submitted to Mondoo Platform, which decides whether each one applies.
Connected, not submitting (pull requests, feature branches, laptops)Reported as not submitted. They aren't in effect, and the checks score as usual.
Disconnected, or with --incognitoApplied locally with no approval step. Results aren't sent to Mondoo Platform.

Gate pull requests on the proposed exceptions

A connected pull request scan doesn't apply exceptions, so a pull request that adds an exception for a failing check still fails --risk-threshold. To let the pull request scan reflect the exceptions it proposes, run that scan with --incognito. cnspec then applies every valid entry locally and keeps the results out of Mondoo Platform. The merge still requires the code owners' approval, and the default branch scan submits the exception for Mondoo Platform to decide.

This GitHub Actions workflow scans pull requests with --incognito and submits exceptions from pushes to main:

.github/workflows/iac-scan.yml
name: mondoo-iac-scan

on:
  pull_request:
  push:
    branches: [main]

jobs:
  scan:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v7
      - name: Install cnspec
        run: bash -c "$(curl -sSL https://install.mondoo.com/sh)"
      - name: Scan infrastructure as code with Mondoo
        env:
          MONDOO_CONFIG_BASE64: ${{ secrets.MONDOO_CONFIG_BASE64 }}
          # pull requests apply the proposed exceptions locally; pushes to main submit them
          INCOGNITO: ${{ github.event_name == 'pull_request' && '--incognito' || '' }}
        run: |
          cnspec scan iac . \
            --discover auto,terraform \
            --risk-threshold 90 \
            $INCOGNITO

The same pattern in GitLab CI/CD:

.gitlab-ci.yml
mondoo-iac:
  stage: test
  image:
    name: mondoo/cnspec:14
    entrypoint: ['']
  script:
    - cnspec scan iac . --discover auto,terraform --risk-threshold 90 $CNSPEC_FLAGS
  rules:
    # merge requests apply the proposed exceptions locally
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      variables:
        CNSPEC_FLAGS: --incognito
    # the default branch submits them to Mondoo Platform
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

The same pattern in Azure Pipelines. Azure Pipelines can't tell cnspec which branch is the default, so the pipeline names it in MONDOO_DEFAULT_BRANCH:

azure-pipelines.yml
trigger:
  - main
pr:
  - main

pool:
  vmImage: ubuntu-latest

variables:
  # tells cnspec which branch submits exceptions to Mondoo Platform
  MONDOO_DEFAULT_BRANCH: main

steps:
  - script: bash -c "$(curl -sSL https://install.mondoo.com/sh)"
    displayName: Install cnspec

  - script: |
      # pull request builds apply the proposed exceptions locally
      if [ "$BUILD_REASON" = "PullRequest" ]; then CNSPEC_FLAGS="--incognito"; fi
      cnspec scan iac . --discover auto,terraform --risk-threshold 90 $CNSPEC_FLAGS
    displayName: Scan infrastructure as code with Mondoo
    env:
      MONDOO_CONFIG_BASE64: $(MONDOO_CONFIG_BASE64)

Azure Pipelines runs pull request builds from the pr trigger only for repositories in GitHub or Bitbucket Cloud. For an Azure Repos repository, add a Build validation branch policy on the default branch that runs this pipeline.

For complete pipeline setup, including how to store Mondoo credentials, read Integrate Mondoo with CI/CD Platforms.

Read the results

cnspec's default output lists every exception from a mondoo.yml file for each asset, grouped by whether it's in effect. Each exception names the file and the paths prefix that matched, its status, its expiration, and its justification:

Exceptions from config files [preview]:
  •  risk accepted  Ensure S3 bucket logging is enabled (Central CloudTrail covers this)
     from mondoo.yml · approved by sam@lunalectric.com · valid until 2026-11-01
     "Access logging is handled centrally by the organization-wide CloudTrail trail."

Exceptions awaiting approval (not in effect) [preview]:
  •  risk accepted  Ensure S3 buckets do not allow public read access via ACLs
     from mondoo.yml (infra/staging) · submitted, pending review · valid until 2026-12-31
     "Staging buckets serve public test fixtures."

Each exception has one of these outcomes:

OutcomeIn effectMeaning
appliedYesA disconnected or --incognito scan applied it locally.
acceptedYesA reviewer approved it in Mondoo Platform. The output names the approver.
auto-acceptedYesThe space doesn't require exception approvals, so it applied when submitted.
pendingNoIt's waiting for a reviewer in Mondoo Platform.
rejectedNoA reviewer rejected it. The output includes the reason when there is one.
expiredNoIts valid_until date has passed.
not-submittedNoA connected scan that doesn't submit read it. Mondoo Platform hasn't seen it, so it doesn't apply.
unknown-checkNoNo policy that scans the asset has this check. Look for a typo, or a policy that isn't enabled.

JSON output (--output json) adds a top-level exceptions key, keyed by asset MRN, with the same details for each exception. The key appears only when a scan read exceptions from a file. SARIF output shows each excepted check's resulting status, skipped or disabled, without the exception behind it.

If a check still fails after you commit an exception, look for it in the Exceptions not in effect or Exceptions awaiting approval section of the output. The status explains why it doesn't apply.

Exceptions in the cnspec configuration file

The cnspec configuration file, such as ~/.config/mondoo/mondoo.yml, accepts the same exceptions list. There, the entries govern every asset that cnspec installation scans. They can't use paths, and a repository file's entry for the same check takes precedence. A connected scan that submits sends these entries once per run, for the whole space.

Learn more

On this page