A KICS 2.2 scan in CI exits with status 40 — what does that mean, and how do `--fail-on` and `--ignore-on-exit` change it?
answer
- severity-coded exit status
- the worst counted severity wins
- a list, not a threshold
- results versus engine errors
- 60 arrived with KICS 2.0
basics
~20 sStatus 40 means the worst counted finding was MEDIUM, with no counted CRITICAL (60) or HIGH (50). --fail-on lists which severities may set the status, and --ignore-on-exit can mute result codes, engine-error codes, or both.
solid answer
~40 sKICS sets its exit status from the most severe result among the severities `--fail-on` counts: 60 for CRITICAL, 50 HIGH, 40 MEDIUM, 30 LOW, 20 INFO, and 0 when nothing counted was found; 126 is an engine error, 130 a signal interrupt and 70 a failed `remediate`. So 40 means at least one MEDIUM finding and no counted CRITICAL or HIGH. `--fail-on` (default `critical,high,medium,low,info`) is a list, not a threshold: `--fail-on high` on its own lets a run whose only findings are CRITICAL exit 0. `--ignore-on-exit` takes `none` (the default), `results`, `errors` or `all`; with `results`, only engine errors fail the job. Either way the report is written in full before KICS exits.
code
bash · 9 linesset +e
kics scan -p . -o ./kics-out --report-formats json,sarif --fail-on critical,high
status=$?
set -e
case "$status" in
0) echo "no critical or high findings" ;;
50|60) echo "blocking findings, exit $status"; exit 1 ;;
*) echo "KICS did not finish cleanly, exit $status"; exit "$status" ;;
esacgo deeper
Remember that KICS's exit status encodes the worst finding: 60 critical, 50 high, 40 medium, 30 low, 20 info, and 0 when nothing counted was found.
Explain how --fail-on selects which severities count, why it is a list rather than a threshold, and what each --ignore-on-exit value hides.
Show a gate that blocks on exactly the codes you mean, still fails on engine errors, and survived the KICS 2.0 change that introduced status 60.
Discuss whether the scanner's exit status or a separate step reading the report should own the merge decision, given that tool upgrades can change the codes.
## The exit-status table KICS documents two groups of exit statuses. The first encodes the **worst result found**; the second reports that something went wrong in the tool itself. | Status | Meaning | |---|---| | `0` | no counted results | | `60` | at least one `CRITICAL` result | | `50` | at least one `HIGH` result | | `40` | at least one `MEDIUM` result | | `30` | at least one `LOW` result | | `20` | at least one `INFO` result | | `70` | remediation error (the `remediate` command) | | `126` | engine error | | `130` | signal interrupt | ## How KICS picks the status After the scan, KICS has already printed the results and written every requested report. It then computes the status: 1. Walk the severities in order: `CRITICAL`, `HIGH`, `MEDIUM`, `LOW`, `INFO`. 2. Skip any severity that is **not in the `--fail-on` list**. 3. Return the code of the first remaining severity with a count above zero. 4. If none qualifies, return 0. `TRACE` results — the bill-of-materials entries `-m, --bom` adds — never set a status. So status 40 says: at least one `MEDIUM` result, and no `CRITICAL` or `HIGH` result among the counted severities. It says nothing about how many findings there were; the counts live in `severity_counters` in the JSON report. ## `--fail-on` is a list, not a threshold `--fail-on` accepts any of `critical`, `high`, `medium`, `low` and `info`, and defaults to all five. An unknown value stops KICS with an error. Because step 2 skips unlisted severities, the flag selects severities rather than drawing a line: | `--fail-on` | Findings in the scan | Exit status | |---|---|---| | default (all five) | 2 MEDIUM, 5 LOW | 40 | | `critical,high` | 2 MEDIUM, 5 LOW | 0 | | `critical,high` | 1 CRITICAL, 3 HIGH | 60 | | `high` | 1 CRITICAL only | 0 | | `low` | 1 HIGH, 2 LOW | 30 | The fourth row is the classic surprise: someone writes `--fail-on high` meaning "high or worse", and a `CRITICAL` finding passes the gate. Write every severity you mean, for example `--fail-on critical,high`. `--fail-on` changes only the status. Every finding still appears in the console and in every report. That is the difference from `--exclude-severities`, which removes queries of those severities before evaluation so their findings never exist. ## `--ignore-on-exit` separates findings from failures | Value | Result codes (20-60) | Error codes | |---|---|---| | `none` (default) | returned | returned | | `results` | suppressed, exit 0 | returned | | `errors` | returned | suppressed | | `all` | suppressed | suppressed | `results` is the setting for a **report-only** stage: findings are published, yet a broken run still turns the job red. `all` makes a crashed or interrupted scan look exactly like a clean one, which is rarely what anybody wants. ## KICS 2.0 and the CRITICAL code KICS 2.0 added the `CRITICAL` severity with its own exit status, 60, and listed that as a **breaking change**. Some queries were re-rated at the same time; their `metadata.json` keeps the earlier rating in `oldSeverity`, and `--old-severities` reports those older ratings, which never include `CRITICAL`. A script written for KICS 1.x that blocks only when the status equals 50 lets a 60 straight through after the upgrade. ## Wiring it into a CI job - Capture the status explicitly rather than relying on the shell to stop at the first non-zero exit. - Test the codes you mean, and treat any unexpected status as a broken run, not as a pass. - Avoid `|| true` after the command: it hides 126 and 130 along with the findings. - Keep the JSON report as an artefact, so the reason for a red job is one click away. ## The status is a summary, the report is the evidence The exit status compresses a whole scan into one number, and it loses information on purpose: it cannot say how many findings there were, which files they sit in, or whether a query failed to run. When a decision needs more than "worst severity found" — say, blocking only when the HIGH count rises — the step reads `severity_counters` and `total_counter` from the JSON report instead of the status. The report is written before KICS exits, whatever the status turns out to be, so that step always has its input.
- How is `--exclude-severities` different from leaving a severity off `--fail-on`?`--fail-on` only decides which severities may set the exit status; every finding still appears in the report. `--exclude-severities` removes queries of those severities before evaluation, so their findings never exist in any report or counter. Use `--fail-on` to stop blocking on LOW while keeping it visible, and `--exclude-severities` only when nobody should see those results at all.
- Why is `--ignore-on-exit all` a poor way to make a KICS stage non-blocking?`all` suppresses error codes as well as result codes, so an engine error (126) or an interrupted run (130) also ends with status 0, and a partial scan looks like a clean one. `results` gives the same non-blocking behaviour for findings while still failing the job when the tool itself breaks. Pair it with a check on `queries_failed_to_execute` in the JSON report.
- Where does a KICS result's severity come from?From the `severity` field in the matching query's `metadata.json`; KICS does not rescore it for your environment. Queries re-rated in KICS 2.x keep their earlier rating in `oldSeverity`, and `--old-severities` reports those older values, which never include CRITICAL.
--fail-on works like a guest list at a door rather than a height bar: only the severities named on the list can set off the alarm, so leaving CRITICAL off the list lets a critical finding walk straight through.
saying these in an interview costs you the question
- The KICS exit status is the number of findings.
- --fail-on high means fail on HIGH or anything worse.
- --ignore-on-exit all is the safe way to make the scan non-blocking.
- A non-zero KICS exit means the report was never written.
- Status 50 is still the most severe result code KICS can return.
- Leaving LOW off --fail-on removes LOW findings from the report.