Skip to content

Latest commit

 

History

History
1473 lines (1264 loc) · 73.2 KB

user.adoc

File metadata and controls

1473 lines (1264 loc) · 73.2 KB

check-script — User Guide

1. Overview

check-script evaluates one or more shell or Python 3 scripts and produces a ranking across five categories, each scored out of 10:

Category What is evaluated

Security

Command injection, hard-coded secrets, downloaded code that gets executed, dangerous permissions, disabled TLS, unsafe PATH, modified sensitive files…

Robustness

Error handling (set -e, unchecked cd, ignored return codes), quoting, syntax errors, CRLF…

Maintainability

Readability, style, formatting, function size, nesting, comments…

Portability

Bashisms in a /bin/sh script (including those that break under dash), shebang, obsolete or distribution-specific commands…

Performance

Needless processes (cat | grep, expr, external commands in loops…)

For each category, the report gives the number of issues by severity (Critical / High / Medium / Low) and their total, then an overall score and a level from A to E. Each issue comes with a fix suggestion and, for external tools, a link to their documentation.

The tool uses the reference analyzers when they are installed (ShellCheck, shfmt, bashate, checkbashisms, gitleaks, trufflehog, bash -n for shell; Ruff, Bandit, Semgrep, mypy, Radon, Vermin — and optionally Pylint, Pyright — for Python, see Python scripts) and complements them with built-in rules that always work (82 for shell, 10 for Python), and with the project’s custom rules. It also analyzes the shell embedded in GitHub Actions and GitLab CI workflows, Dockerfiles, Makefiles and Ansible tasks. It can also fix safe defects (--fix), compare two analyses (baseline) and integrates with continuous integration (SARIF, GitLab Code Quality, pre-commit hook). A graphical interface (check-script-gui) reuses the same engine.

The interface and the reports are available in French and in English.

2. Installation

2.1. From the archive

tar xzf check_script-VERSION-linux-x86_64.tar.gz
cd check_script-VERSION-linux-x86_64
./install.sh              # user installation (~/.local)
sudo ./install.sh         # system-wide installation (/usr/local)
./install.sh --prefix=/opt/tools

The installation puts in place the check-script command, the check-script-gui interface (with its menu entry), the manual page (man check-script), the bash and zsh completions and the documentation. To uninstall: ./uninstall.sh (same options).

The CLI binary is self-contained; the interface requires GTK 3.

2.2. RPM packages

sudo dnf install ./check-script-VERSION-1.fc44.x86_64.rpm       # CLI
sudo dnf install ./check-script-gui-VERSION-1.fc44.x86_64.rpm   # interface

The CLI package recommends ShellCheck, shfmt and checkbashisms, which are installed automatically if weak dependencies are enabled.

Each version published on GitHub (Releases) provides the archive, the RPMs, the AppImage and the Flatpak.

2.3. AppImage

A single file, no installation, for any recent distribution (GTK 3 required, present on any desktop):

chmod +x CheckScript-VERSION-x86_64.AppImage
./CheckScript-VERSION-x86_64.AppImage [script.sh|folder]    # interface
./CheckScript-VERSION-x86_64.AppImage --cli script.sh         # command line
ln -s "$PWD/CheckScript-VERSION-x86_64.AppImage" ~/.local/bin/check-script  # direct CLI

Without FUSE (container…), add APPIMAGE_EXTRACT_AND_RUN=1. The analysis tools are those of the system.

2.4. Flatpak

flatpak install --user ./CheckScript-VERSION-x86_64.flatpak
flatpak run fr.chla28.check_script_gui                                # interface
flatpak run --command=check-script fr.chla28.check_script_gui s.sh    # CLI

The Flatpak runs the analysis tools of the host (flatpak-spawn --host, org.freedesktop.Flatpak permission): install ShellCheck, shfmt, Ruff… on the system as for the native version. It has access to the whole file system (--filesystem=host) so that it can analyze and fix scripts wherever they are; its settings, cache and history are in ~/.var/app/fr.chla28.check_script_gui/.

Without them, only the built-in rules apply and the report says so.

Tool Contribution Installation (Fedora/RHEL — Debian/Ubuntu)

shellcheck

Reference for shell static analysis (quoting, errors, POSIX portability…); suggests fixes

dnf install ShellCheck — apt install shellcheck

shfmt

Canonical formatting, syntax tree for accurate built-in rules, detection of constructs outside the dialect

dnf install shfmt — apt install shfmt

bashate

Style rules (OpenStack conventions)

pip install --user bashate

checkbashisms

Bashisms in /bin/sh scripts

dnf install devscripts-checkbashisms — apt install devscripts

gitleaks

Secret detection (optional)

https://github.com/gitleaks/gitleaks/releases

trufflehog

Secret detection (optional, run without network verification)

https://github.com/trufflesecurity/trufflehog/releases

bash / sh

Syntax check (bash -n)

present on every distribution

hadolint

Dockerfile: best practices and security (pinned versions, USER, caches…); the ShellCheck results it relays are discarded

https://github.com/hadolint/hadolint/releases

actionlint

GitHub Actions workflows: syntax, expressions, injections

https://github.com/rhysd/actionlint/releases

zizmor

GitHub Actions workflows: security audit (unpinned actions, dangerous triggers, injections, credential persistence…), without network access (--offline)

pipx install zizmor

For Python scripts:

Tool Contribution Installation

ruff

Reference linter (errors, likely bugs, security, modernization, performance); formatting; suggests fixes

pipx install ruff

bandit

Security (injections, eval, pickle, disabled TLS, secrets…)

pipx install bandit

semgrep

Security and correctness rules from the p/python registry (network access to semgrep.dev)

pipx install semgrep

mypy

Typing: call and operand errors detected without running the code

pipx install mypy

radon

Cyclomatic complexity and maintainability index

pipx install radon

vermin

Minimum Python version required (portability)

pipx install vermin

pylint

Full linter, redundant with Ruff: disabled by default (--with pylint)

pipx install pylint

pyright

Typing, redundant with mypy: disabled by default (--with pyright)

pipx install pyright

pip-audit

Known vulnerabilities of the packages declared in the project’s requirements*.txt (network access to PyPI / OSV)

pipx install pip-audit

python3

Syntax check (compilation, writing nothing to disk) and syntax-tree facts for the built-in rules

present on every distribution

Check what is detected:

$ check-script --list-tools
Tool            Language  Status              Version
syntax          all       built in            bash -n / sh -n / python3 compile()
shellcheck      shell     available           0.11.0
bandit          python    available           1.9.4
ruff            python    available           0.16.8
semgrep         python    available           1.178.0
mypy            python    available           2.3.1
pyright         python    disabled
pylint          python    disabled
vermin          python    available           1.8.0
radon           python    available           6.0.1
gitleaks        all       not installed       (install : https://github.com/gitleaks/gitleaks/releases)
trufflehog      all       not installed       (install : https://github.com/trufflesecurity/trufflehog/releases)
checkbashisms   shell     available           2.26.5
pydeps          python    available           3.14.7
pip-audit       python    not installed       (install : pipx install pip-audit)
custom          all       built in            0 custom rule(s)
builtin         all       built in            82 + 10 rules
shfmt           shell     available           ?
bashate         shell     available           2.1.1
Note
? means the tool is present but does not display a version number (the case of Fedora’s shfmt package). The absence of gitleaks and trufflehog is not reported in the reports: the built-in rules SEC002 and SEC022 already cover secrets.

3. Usage

check-script my_script.sh                           # report in the terminal
check-script --details my_script.sh                 # + every issue and its fix
check-script my_script.sh -o report.html            # standalone HTML report
check-script scripts/ -o folder.html                # folder: summary, frequent rules
check-script -q deploy.sh -o r.md -o r.adoc         # files only, nothing on screen
check-script scripts/                               # all the scripts of a folder
cat script.sh | check-script -                      # standard input
check-script --lang en build.sh                     # report in English
check-script --profile strict --context root install.sh
check-script --fix --dry-run deploy.sh              # proposed fixes (diff)
check-script --fix --backup deploy.sh               # fixes applied
check-script -b baseline.json scripts/              # new issues only
check-script tool.py                                # Python script
check-script --python-target 3.6 --with pylint tool.py
check-script Dockerfile .github/workflows/ci.yml    # embedded scripts
check-script --watch scripts/                       # re-analyze on every save
check-script -o report.xml scripts/                 # JUnit XML (Jenkins, GitLab)

A folder is analysed at its own level only; --recursive (-r) also walks its sub-folders. The files kept are .sh, .bash, .ksh, .dash, .zsh, .py, .pyw and files without an extension whose shebang designates a shell or Python. Hidden folders (.git, .venv…) and dependency folders (venv, site-packages, pycache, node_modules) are ignored, except .github, .gitlab and .gitlab-ci.yml. Files that contain embedded scripts are added (see Embedded scripts (CI, Dockerfile, Makefile, Ansible)), except with --no-embedded.

A pattern ('.sh', 'scripts/.py', '/.sh', 'backup-[0-9]?.sh') is expanded by check-script itself, which is useful when the shell does not do it (quotes, Windows, CI configuration files). and ? stay within one folder level, crosses folders, [abc] is a character class. Without a /, the pattern applies to the current folder, or to the whole tree with --recursive (like find -name). Only scripts and folders are kept, hidden files only when the pattern names them (.*), and a pattern without a match is reported as not found. The GUI accepts the same patterns as start-up arguments.

check-script '*.sh'                  # scripts of the current folder
check-script -r '*.py'               # all the Python scripts of the tree
check-script scripts/                # scripts/ only, without sub-folders
check-script -r scripts/             # scripts/ and its sub-folders

3.1. Options

Option Effect

-o, --output FILE

Writes the report to FILE; format according to the extension: .md, .adoc, .html, .pdf (see PDF report), .json, .sarif, .codeclimate.json (or gl-code-quality-report.json), .xml (JUnit), .txt. Repeatable.

-f, --format FORMAT

terminal, md, adoc, html, pdf, json, sarif, codeclimate (alias gitlab: GitLab Code Quality), junit (JUnit XML), github (GitHub Actions annotations): format of the standard output, and of -o files without a recognized extension (the extension takes precedence).

-l, --lang fr|en

Report language. Default: LC_ALL, LC_MESSAGES then LANG.

-s, --shell sh|bash|dash|ksh|zsh|python

Forces the dialect instead of inferring it from the shebang (or from the .py extension).

--python-target VERSION

Minimum Python version the scripts must support (default: 3.9, see Python scripts).

-p, --profile strict|default|legacy

Scoring profile (see Profiles).

--context root|cron|systemd|interactive

Execution context (see Execution contexts). Repeatable.

--follow-source

Follows sourced files (shellcheck -x).

-c, --config FILE

YAML configuration file (see [config]).

--with TOOL

Enables a tool disabled by default (pylint, pyright); repeatable or comma-separated.

--without TOOL

Disables a tool (shellcheck, shfmt, bashate, checkbashisms, ruff, bandit, semgrep, mypy, radon, vermin, gitleaks, trufflehog, syntax, builtin…); repeatable or comma-separated.

--no-external

Uses only the built-in rules.

--no-embedded

In a folder, ignores embedded scripts (see Embedded scripts (CI, Dockerfile, Makefile, Ansible)).

--ref REFERENCE

Shows only the issues attached to this normative reference, by prefix and case-insensitively: CWE-78, CWE-7 (all CWE-7xx), A08 (OWASP A08:2021), CICD-SEC, ANSSI, ANSSI-BP-028 R59 (see Frameworks: CWE, OWASP, ANSSI). Repeatable; the scores remain those of the full analysis.

-w, --watch

After the analysis, watches the targets: each script saved (or created) is re-analyzed and its report displayed; with -o files, all targets are re-analyzed and the reports rewritten (the cache makes this fast). Ctrl+C to stop; incompatible with --fix and standard input.

--dashboard FILE.html

Team dashboard: each target (folder) is a repository; average score, trend (history curve), serious issues, levels, most frequent rules, weakest scripts. The history is recorded on each run.

--history-dir FOLDER

Where to record the history of analyzed folders (default with --dashboard: ~/.local/share/check-script/history); a versioned folder shares it within the team (same analysis paths, e.g. a nightly CI job).

--changed-since REF

Analyzes only the scripts modified since the git reference REF (commits, work in progress, new files), in the repository of the first target; no modified script: exit code 0. Ideal in CI (--changed-since origin/main) and in pre-commit.

-r, --recursive

Walks the sub-folders of the given folders (by default only the scripts of the folder itself are analysed). With a pattern without / ('.sh'), also looks for it in the sub-folders. In the GUI: *Sub-folders option of the Folder screen.

--exclude PATTERN

Excludes from a folder analysis the paths matching the pattern (.gitignore syntax: vendor/, *.min.sh, /gen/); repeatable. Adds to the exclude key of the configuration and to the .checkscriptignore file of the folder (see Excluding paths).

--[no-]cache

Reuses the report of an unchanged script (default; ~/.cache/check-script/results). The key covers the script’s content and path, the configuration, the language, the versions of check-script and of the tools (and the Semgrep rules); an analysis where a tool failed is not cached; disabled with --follow-source.

--explain

Terminal: what each rule costs (points removed from its category, gain on the overall score if fixed, automatic fix available).

--sort category|impact

Terminal: issues by category (default) or by quick win (impact on the score, automatic fixes favored).

-j, --jobs N

Number of scripts of a folder analyzed simultaneously (default: depends on the number of processors, at most 4; 1: one after the other). The tools of a single script always run in parallel.

-b, --baseline REPORT.json

Baseline: only new issues are detailed, the evolution of the scores is displayed (see Baseline and evolution).

--fail-on-new SEVERITY

With --baseline: exit code 1 if a new issue reaches this severity.

--baseline-update

With --baseline: rewrites the baseline file without the issues that were fixed, so the tolerated debt can only shrink (see [baseline]).

--fix

Fixes safe defects in the file (see Automatic fix).

--dry-run

With --fix: prints the unified diff without modifying the file.

-i, --interactive

With --fix: offers each fix one at a time (y / n / a all the rest / q quit), like git add -p (see Automatic fix).

--backup

With --fix: keeps the original as FILE.orig.

--details

Terminal: every issue, with its fix suggestion (default: 10 per category).

--summary

Terminal: summary only.

--[no-]source

Terminal: shows under each issue the line of code concerned, with a ^ marker under the flagged column (default). A line where a secret is detected is never copied.

--[no-]color

Forces or disables color (default: automatic, NO_COLOR respected).

-q, --quiet

No report on the standard output.

--fail-under SCORE / --fail-under CATEGORY=SCORE

Exit code 1 if the overall score, or that of a category, is below the threshold. Repeatable; several thresholds separated by commas (security=8,robustness=6.5); decimals with a dot.

--list-tools / --list-rules

Detected tools / built-in rules.

--list-rules --all

All known rules: built-in rules and tool codes classified by check-script (identifier, tool, language, category, severity, title).

-v, --version / -h, --help

Version / help.

3.2. Exit codes

Code Meaning

0

Analysis performed.

1

Threshold not met (--fail-under, --fail-on-new).

2

Usage error (invalid option, no script, invalid configuration or reference).

3

Script not found or unreadable, or report file impossible to write.

4. Reading the report

Terminal output excerpt (--details)
═══ Script assessment: deploy.sh ═══
Dialect: bash    Lines: 16 (code: 15, comments: 0)
Tools: syntax (run), shellcheck 0.11.0 (run), …

Category         Score /10            Critical     High   Medium      Low   Total
Security           0.0 ░░░░░░░░░░            3        3        3        1      10
Robustness         1.3 █░░░░░░░░░            0        1        8        4      13
Maintainability    8.8 █████████░            0        0        0        5       5
Portability       10.0 ██████████            0        0        0        0       0
Performance        9.0 █████████░            0        0        0        4       4

Overall score: 1.5/10 (E)

Issue details
▶ Security (10)
  L3       Critical SEC002 [builtin]          Hard-coded secret (password, token, key) in the script
        3 │ (line not shown: potential secret)
                                              → Read the secret from the environment, a protected file…
  L8       Critical SC2115 [shellcheck]       Use "${var:?}" to ensure this never expands to /* .
        8 │ rm -rf $APP_DIR/*
          │        ^
                                              → https://www.shellcheck.net/wiki/SC2115
  …

Each issue shows its line (file if it concerns the whole script), its severity, its rule, the tool that detected it, the line of code (with a ^ marker under the flagged column when the tool provides it; --no-source to hide it) and, with --details, its fix suggestion or the link to the rule’s documentation. A line where a secret is detected is never copied into the report. The messages of external tools remain in English; those of the built-in rules follow --lang. Detected secrets are never copied into the report.

4.1. Understanding and improving the score

When possible, the report tells you how to gain a grade: → Grade B (7.6) by fixing: SC2164, ROB001. With --explain (terminal), and always in Markdown, HTML, JSON (explanation) and in the interface, a table gives for each rule the points removed from its category and the gain on the overall score if all its occurrences are fixed. A zero gain means the score is capped by another, weaker category (the overall score does not exceed the worst + 1.5): that one has to be fixed first. --sort impact (interface: By quick win) ranks the issues from the most to the least profitable; a rule that can be fixed automatically counts 1.5 times its gain.

4.2. Severities

Severity Meaning

Critical

Probable compromise or destruction (downloaded code executed, hard-coded secret, rm -rf /, syntactically invalid script).

High

Serious exploitable defect or one with major side effects (eval of a variable, chmod 777, disabled TLS, unchecked cd, bashism that breaks under dash).

Medium

Real defect with limited behavior (unquoted variable, no set -e, predictable temporary file).

Low

Style, readability, performance, good practices.

4.3. Score computation

For each category:

score = 10 − Σ penalties          (floor 0, rounded to the tenth)

penalty of a rule triggered n times = weight × (1 + log2 n)
weight (standard profile): Critical 4 · High 2 · Medium 0.75 · Low 0.25
Medium and Low are multiplied by √(100 / max(100, lines of code))
  • Repeated occurrences: a rule triggered 8 times costs 4 times its weight, not 8 times — repetitions reflect a single habit.

  • Script size: beyond 100 lines of code, Medium/Low penalties are attenuated. Critical and High never are.

The overall score is the weighted mean of the categories (Security 1.5; Robustness 1.25; Maintainability 1; Portability 0.75; Performance 0.5), capped at the lowest category score + 1.5: a weak category cannot be hidden by the others (weakest-link principle).

Levels: A ≥ 9 · B ≥ 7.5 · C ≥ 6 · D ≥ 4 · E < 4.

These settings were calibrated on a corpus of real, typical scripts (see the developer guide).

4.4. Duplicates between tools

When several tools report the same defect on the same line (for example ShellCheck SC2002 and the built-in rule PERF001), a single issue is kept, that of the most specialized tool: bash -n, ShellCheck, gitleaks, trufflehog, checkbashisms, built-in rules, shfmt, bashate. In Python: compilation, Bandit, Ruff, Semgrep, mypy, Pyright, Pylint, Vermin, Radon, then gitleaks, trufflehog and built-in rules.

A statement spread over several lines counts as a whole: Bandit locates a call on its first line, Ruff on the line of the faulty argument; both reports are merged (statements of at most 6 lines, so that a rule covering an entire function does not absorb the issues it contains).

Analyses run in parallel (tools of a single script, scripts of a folder); the priority order above is kept, and the result is identical to a sequential analysis.

4.5. Disabled rules

A disabled rule (rules.disabled, Rules tab of the interface) is also disabled under the code of the same rule in another tool: Ruff S602 ↔ Bandit B602, Ruff PLW0602 ↔ Pylint W0602, Ruff E722 ↔ Pylint W0702, ShellCheck SC2164 ↔ ROB005… It is passed on to the tools that accept it without error (ShellCheck, bashate, Bandit: they no longer compute it), and --fix no longer fixes it. A neighboring detection by another tool (e.g. the Semgrep rule subprocess-shell-true for B602) is still reported: disable it in turn if needed.

5. Profiles

Profile Usage

strict

New scripts: weights Critical 5, High 3, Medium 1, Low 0.4; attenuation from 50 lines; lines ≤ 100, functions ≤ 40 lines, nesting ≤ 3.

default

Settings described above.

legacy

Old existing code: High 1.5, Medium 0.5, Low 0.1; relaxed thresholds; pure style rules disabled (MNT001, MNT004, MNT005, MNT010, shfmt and Ruff formatting, bashate’s E001–E006; in Python PYMNT001, E501, W291, W293, C0301, C0303).

The profile is chosen with --profile or with the profile: key of the configuration; the other configuration keys apply on top of it.

6. Execution contexts

Declaring how the script is run enables specific rules and raises some severities (they are never lowered):

Context Rules enabled Severities raised

root

SEC017 (PATH not set)

predictable temporary file, cleartext download, untrusted source, unverified archive → High; chmod 777, rm -rf "$VAR/", unchecked cd → Critical; no set -e → High

cron

ROB014 (no lock), ROB015 (interactive input), ROB016 (PATH not set)

no set -e, predictable temporary file → High; set -x → Medium

systemd

SEC017, ROB015

no set -e → High; set -x → Medium

7. Directives in the script

A false positive or a deliberate choice can be neutralized in the script itself:

#!/bin/bash
# check-script disable-file=MNT005,E003        # the whole file

# check-script disable=SEC005                  # comment alone: next line of code
curl -k "$INTERNAL_URL"

eval "$SAFE_CMD"  # check-script disable=SEC003   # this line
# check-script disable-next-line=SEC022
PUBLIC_KEY="AAAAC3NzaC1lZDI1NTE5AAAAI…"

Identifiers accept a trailing wildcard (SC20*) and all. The number of neutralized issues appears in the report. # shellcheck disable=… directives are still handled by ShellCheck.

A # check-script directive that no longer neutralizes anything (issue fixed, rule changed) is reported by MNT011 at its line, so that it can be removed. As a precaution, it is not reported if a targeted tool is absent or failed (nothing then proves that it is useless). Useless # noqa comments are covered by the Ruff rule RUF100.

In a Python script, the native directives apply to all tools, and a code also targets the same rule in the other tools:

import os  # noqa                          # the whole line
x = eval(expr)  # noqa: S307               # S307 (Ruff), B307 (Bandit), W0123 (Pylint)
subprocess.call(cmd, shell=True)  # nosec  # security issues of the line
subprocess.call(cmd, shell=True)  # nosec B602

8. Automatic fix

--fix applies only safe transformations:

  1. the fixes proposed by ShellCheck (variable quoting, cd … || exit, backticks…);

  2. the built-in fixes: CRLF line endings, trailing spaces, backticks → $(…), egrep/fgrep → grep -E/-F, which → command -v, read → read -r (never in a heredoc, a string or a comment);

  3. shfmt formatting, in the script’s indentation style.

The result is checked with bash -n (or sh -n): if the script was valid and would no longer be, no fix is written. The report displayed afterwards covers the fixed version.

check-script --fix --dry-run deploy.sh       # diff on standard output, nothing is written
check-script --fix --backup deploy.sh        # writes deploy.sh, keeps deploy.sh.orig

With --interactive (-i), each fix is shown (lines before / after) and applied only if you accept it, like git add -p: y applies it, n skips it, a applies it and all the following ones, q stops (the fixes already accepted are applied), ? reminds the keys. Only the line-level fixes (ShellCheck, built-in) are offered; the shfmt reformatting stays reserved to the non-interactive mode. --dry-run and --backup work as usual.

check-script --fix --interactive deploy.sh

8.1. Fix code for each issue

For each issue, the graphical interface (by expanding the issue) and the HTML report show the code to write:

  • Suggested fix: the lines concerned before and after the fix, when a safe fix exists for this precise issue (ShellCheck suggestion, or built-in fix limited to the line); in the interface, Apply this fix modifies only those lines (bash -n check, refused if the file has changed since the analysis);

  • otherwise, Fix example: a generic "avoid / write" example for the rule (all built-in rules and the most common ShellCheck codes).

Any fix from the interface (Fix… or Apply this fix) saves the script as .orig on the first fix since launch; later fixes keep this copy, which therefore remains the script as it was before the first fix.

The Copy button puts the fixed code on the clipboard. A line where a secret is detected never receives a concrete fix (its preview would copy the secret).

9. Baseline and evolution

A previous JSON report serves as a baseline: only new issues are detailed, and the evolution of the scores is displayed (new: 1 · fixed: 2 · unchanged: 30, Overall score: 6.1 → 7.4 (+1.3)). Issues are matched by a fingerprint independent of the line number: adding code above a known issue does not make it look new.

check-script -q -o baseline.json scripts/           # on the main branch
check-script -b baseline.json --fail-on-new high scripts/

This is the way to adopt the tool on existing code: the current debt is tolerated, any new debt blocks.

9.1. Tightening the baseline

As issues get fixed, the baseline should shrink with them, otherwise a fixed issue could silently come back. --baseline-update rewrites the baseline file without the issues that are no longer there:

check-script -b baseline.json --baseline-update --fail-on-new high scripts/

A script is tightened only if it has no new issue: its current report then replaces its entry (scores included). A script with new issues keeps its entry, so they are never absorbed into the tolerated debt (they are listed in the message). Scripts absent from the baseline are not added. Not usable with --ref (filtered report). A typical use is a CI job on the main branch that commits the updated file.

10. Comparing two reports: check-script diff

check-script diff before.json after.json              # text
check-script diff before.json after.json -f md        # merge request comment
check-script diff before.json after.json --fail-on-new high --fail-on-worse

Compares two JSON reports (-o report.json) without analyzing anything: for each script, the new, fixed and unchanged issues (matched by fingerprint, so independent of the line number), the evolution of the overall score and of each category, plus the added and removed scripts. --summary hides the issue details, -f json gives a machine-readable result, --fail-on-new SEVERITY exits with code 1 if a new issue reaches that severity, and --fail-on-worse if the score of a script dropped. The same comparison is available in the interface (Compare…).

11. External tools: what is used

ShellCheck

JSON with the optional checks add-default-case, avoid-nullary-conditions, check-set-e-suppressed, check-unassigned-uppercase, deprecate-which, useless-use-of-cat. Each SCxxxx code is classified (SC2115 → Security/Critical, SC2164 → Robustness/High, SC3xxx → Portability, bashisms that break under dash → High…). SC1091 is excluded by default. Its fixes feed --fix.

shfmt

Formatting checked in the script’s indentation style (the choice of style is not penalized, its inconsistencies are). Its syntax tree (--to-json) makes the built-in rules about commands accurate (a which in a string is not a command; a mkdir in the condition of an if is checked).

bashate

Style; E003 ignored for a script indented otherwise than with 4 spaces.

checkbashisms

/bin/sh scripts (or without a shebang) only.

gitleaks, trufflehog

Secret detection; trufflehog is run with --no-verification so that no secret is sent over the network.

bash -n

Syntax check with the dialect’s interpreter: any error is Critical.

12. Built-in rules

Full list: check-script --list-rules. Each rule has a fix suggestion (displayed with --details and in the report files). In square brackets: rule active only in that context.

Rule Category Severity Description

SEC001

Security

Critical

Downloaded content executed directly by a shell (curl | sh, bash <(curl …))

SEC002

Security

Critical

Hard-coded secret (password, token, AWS/GitHub key, private key)

SEC003

Security

High

eval applied to an expansion: injection risk

SEC004

Security

High

World-writable permissions (chmod 777/666, o+w)

SEC005

Security

High

TLS / host key verification disabled

SEC006

Security

Medium

Cleartext download (http://)

SEC007

Security

High

rm -r on a path built with an unguarded variable

SEC008

Security

Critical

Recursive deletion of / or of a system directory

SEC009

Security

Medium

Temporary file with a predictable name in /tmp

SEC010

Security

High

Password on the command line (sudo -S, sshpass -p, mysql -pXXX…)

SEC011

Security

Medium

source of a file from /tmp or a URL

SEC012

Security

Low

set -x: secrets in the logs

SEC013

Security

High

setuid/setgid bit

SEC014

Security

Medium

Secret typed without masking (read without -s)

SEC015

Security

Low

sudo with a relative command, PATH not set

SEC016

Security

High

PATH containing . or an empty entry

SEC017

Security

Medium

Privileged script without a fixed PATH [root, systemd]

SEC018

Security

High

Modification of sudoers, authorized_keys, passwd, shadow or cron

SEC019

Security

Medium

Downloaded archive extracted without verification (curl | tar)

SEC020

Security

Medium

Risky SSH option (-A, ForwardAgent=yes, PermitLocalCommand)

SEC021

Security

Low

Secret exported to the environment

SEC022

Security

Medium

High-entropy string (potential secret; checksums excluded)

SEC023

Security

Critical

GitHub workflow: untrusted expression (${{ github.event.issue.title }}, github.head_ref…) inserted in a run: — command injection

SEC024

Security

Medium

Package installed without a pinned version: pip/pipx install package, npm install -g, go install …@latest, gem install without -v, cargo install without --version (not an installation from a dependency file, a path or a URL)

SEC025

Security

High

Repository or package without signature verification: [trusted=yes], --allow-unauthenticated, gpgcheck=0, --nogpgcheck, rpm --nosignature, apk --allow-untrusted

SEC026

Security

Low

git clone without a pinned revision (neither --branch v1.2, nor git checkout <sha> afterwards)

ROB001

Robustness

Medium

No stop on error (set -e)

ROB002

Robustness

Low

Pipelines without set -o pipefail

ROB003

Robustness

Low

No set -u

ROB004

Robustness

Low

mktemp without a cleanup trap

ROB005

Robustness

Medium

cd without failure check (without set -e)

ROB006

Robustness

Medium

Unquoted variable in [ ]

ROB007

Robustness

Low

read without -r

ROB008

Robustness

Medium

Loop over the output of ls

ROB009

Robustness

High

CRLF line endings

ROB010

Robustness

Medium

Unquoted $@ / $*

ROB011

Robustness

Low

Return code of a critical command ignored (without set -e)

ROB012

Robustness

Low

IFS modified globally without restoration

ROB013

Robustness

Low

Cleanup trap without EXIT

ROB014

Robustness

Medium

Scheduled task without a lock (flock) [cron]

ROB015

Robustness

Medium

Interactive input without a terminal [cron, systemd]

ROB016

Robustness

Medium

Cron task without a fixed PATH [cron]

ROB017

Robustness

Medium

External command not found on the analysis machine, which the script does not check (command -v, type, hash); dnf / apt packages indicated (see External commands)

MNT001

Maintainability

Low

Line too long

MNT002

Maintainability

Low/Medium

Function too long

MNT003

Maintainability

Medium

Nesting too deep

MNT004

Maintainability

Low

No header comment

MNT005

Maintainability

Low

Very few comments

MNT006

Maintainability

Low

TODO/FIXME/XXX/HACK marker

MNT007

Maintainability

Low

Backticks instead of $(…)

MNT008

Maintainability

Medium

Long script without a function

MNT009

Maintainability

Low

Indentation mixing tabs and spaces

MNT010

Maintainability

Low

Trailing whitespace

POR001

Portability

Medium

No shebang

POR002

Portability

Low

Interpreter outside /bin, /usr/bin (or env)

POR003

Portability

Medium

Bash construct in a /bin/sh script

POR004

Portability

Low

which instead of command -v

POR005

Portability

Low

Obsolete egrep / fgrep

POR006

Portability

Low

A single package manager without an alternative

POR007

Portability

Low

Obsolete net-tools utilities

PERF001

Performance

Low

Useless cat

PERF002

Performance

Low

grep | wc -l

PERF003

Performance

Low

expr

PERF004

Performance

Low

External command on every loop iteration

PERF005

Performance

Low

Chained filters that can be reduced

PERF006

Performance

Low

ps | grep

PERF007

Performance

Low

for … in $(seq …) (bash)

PERF008

Performance

Low

Useless echo in a substitution

PERF009

Performance

Low

$(cat f) instead of $(< f) (bash)

Important
The built-in rules remain heuristic when shfmt is absent (regular expressions on a light splitting of the script). A false positive is neutralized with a directive (Directives in the script) or through the configuration (rules.disabled).

13. Python scripts

A script is handled as Python 3 when its shebang designates python, python3 or python3.N (including through env), or, without a shebang, when its extension is .py / .pyw; --shell python forces it. The shell tools are then not run, and vice versa: the report lists only the tools of the script’s language (plus gitleaks, trufflehog and the built-in rules, common to both).

13.1. Tools and classification

Ruff

tools.ruff.config: project follows the Ruff configuration of the script’s project (ruff.toml, .ruff.toml or pyproject.toml with [tool.ruff], up to the git root; interface setting); a path designates a configuration file. Otherwise, families retained: E4, E7, E9, E501, W, F, B, S, UP, PERF, C4, SIM, BLE, PLE, PLW, PLR, PIE, RUF, EXE, T10, ISC, A, ASYNC (pure style families — docstrings, annotations, naming — are discarded). PLR2004 ("magic" constants) and S603 are excluded by default. Run with --isolated: the project’s Ruff configuration does not influence the score. Each family is classified (S → Security, F → Robustness, PERF / C4 → Performance, EXE → Portability…), with reclassifications (F821 undefined name → High, S602 shell=True → High…). ruff format --diff reports formatting deviations. Only the fixes that Ruff qualifies as safe are proposed.

Bandit

Bandit’s severity, lowered by one notch if its confidence is low, with reclassifications aligned with Ruff and with the shell rules: hard-coded secrets (B105–B107) as High, and as Critical when the value looks like a real secret (at least 8 characters, not an example or a template), like SEC002; execution of code or data (eval, exec, pickle, unsafe yaml.load, SQL built by concatenation) as High, like SEC003. A secret’s value is never copied into the report. B404 and B603 (any import or call of subprocess) are excluded by default.

Semgrep

p/python rules, run with --metrics=off. They are downloaded on first access to ${XDG_CACHE_HOME:-~/.cache}/check-script/, refreshed every week, and the local copy is used offline (report: p/python (cache) or (stale cache)). With neither a copy nor a network, the tool is marked "failed" and the analysis continues without it. On an isolated machine, provide a rules file or folder: tools.semgrep.config: /opt/rules/python.yml.

mypy

Run with --check-untyped-defs (unannotated functions are checked too) and --ignore-missing-imports, without a disk cache. Typing errors are classified Robustness / Medium. mypy 2 no longer accepts a target below 3.10: it then checks with the interpreter’s version, Vermin covering portability.

Radon

Functions with cyclomatic complexity ≥ 11 (rank C → Low, D → Medium, E-F → High); maintainability index of rank B (Low) or C (Medium).

Vermin

Each construct that requires a Python version higher than the target (e.g. match → 3.10): Portability / High, the script would not run.

Pylint, Pyright

Disabled by default because they are redundant with Ruff and mypy; --with pylint, --with pyright, the YAML configuration or the interface settings enable them. C0103 and C0114–C0116 (naming, docstrings) are excluded from Pylint by default.

Duplicates between tools are merged on the same line: S602 (Ruff) = B602 (Bandit) = subprocess-shell-true (Semgrep), PLW0602 (Ruff) = W0602 (Pylint), mypy errors = Pyright errors…

13.2. Target version

pythonTarget (default 3.9, the Python of RHEL / Rocky 9; option --python-target, interface setting) is passed to Vermin, Ruff (--target-version), Pylint and Pyright. In the YAML, the version is written in quotes: 3.10 without quotes would be read as the number 3.1.

13.3. Python built-in rules

Rule Category Severity Description

PYSEC001

Security

Medium

High-entropy string: potential secret (line never copied), like SEC022

PYROB001

Robustness

Low

Call to subprocess.run/call/check_call/check_output without timeout (Medium in cron / systemd)

PYROB002

Robustness

Medium

input() in a script without a terminal [cron, systemd]

PYROB003

Robustness

Medium

Scheduled task without a lock (fcntl.flock…): concurrent runs possible [cron]

PYROB004

Robustness

Medium

Return code of main() ignored (call without sys.exit(…​)): the script always exits with 0

PYROB005

Robustness

High

Imported module not found: neither installed nor declared in the dependencies (see Dependencies)

PYMNT001

Maintainability

Low

Neither a docstring nor a header comment describing the script

PYMNT002

Maintainability

Low

Code executed at module load, without an if name == "main": guard

PYPOR001

Portability

High

python / python2 shebang: absent from RHEL 8+ and Debian 11+ (the script does not start); use python3

PYPOR002

Portability

Medium

Third-party package installed but missing from the project’s declared dependencies

They rely on Python’s ast module (calls spread over several lines, from subprocess import run imports included); without python3, a line-by-line splitting takes over (PYROB004 is then not evaluated).

Docstrings count as documentation, not as code: a well-documented script is not treated as a longer script.

Python scoring is calibrated on a corpus of 12 scripts (test/corpus/scripts/py_*, see the developer guide).

13.4. Dependencies

The pydeps analyzer has the interpreter classify each import, without running it (importlib.util.find_spec): standard library, local module (next to the script), installed package or not found. It compares it with the declared dependencies: the script’s PEP 723 header (# /// script) if it exists, otherwise the files of the nearest project (going up to the git root): requirements*.txt (and requirements/*.txt, -r followed), pyproject.toml ([project], extras, [dependency-groups], Poetry), setup.cfg, Pipfile.

  • PYROB005: module not found and not declared (typo, forgotten dependency). An import placed in try: … except ImportError: is optional and is not reported; neither is a module declared but absent from the analysis environment (that is not a defect of the script).

  • PYPOR002: package installed but not declared (the script will not work elsewhere). Without any dependency file, nothing is reported.

Module and package names are matched through the metadata of installed packages, a table of common cases (yaml → PyYAML, PIL → Pillow, cv2 → opencv-python…) and variants (psycopg2 covered by psycopg2-binary).

The interpreter used is that of the active virtual environment (VIRTUAL_ENV), otherwise python3, or tools.pydeps.path from the configuration. The one of a .venv in the analyzed project is never run by default (the repository may be foreign): activate the environment before the analysis.

pip-audit (optional) audits the project’s requirements*.txt, once for all its scripts: directly if the versions are pinned (==), otherwise after resolution by pip in a temporary environment (slower). Each vulnerability is reported (High, Security, OSV link) on the import of the package concerned, with the fixed version. Packages the script does not import are ignored.

13.5. Fix

--fix (and Fix… in the interface) applies the safe fixes of ruff check --fix then ruff format; the result is checked by Python compilation; a disabled rule is not fixed. The directives # check-script disable=…, # noqa and # nosec are used as described in Directives in the script.

14. Excluding paths

In a folder, hidden folders (.git, .venv…) and dependency folders (venv, site-packages, node_modules, pycache) are never scanned. To exclude more, use .gitignore-style patterns, from three sources that add up:

  • the --exclude PATTERN option (repeatable);

  • the exclude: key of the configuration (.checkscript.yaml);

  • a .checkscriptignore file at the root of the analyzed folder, one pattern per line (# comments).

vendor/            # a folder, at any level
generated/**       # everything under a folder
*.min.sh           # by name, at any level
/build             # only at the root of the analyzed folder
legacy/*.sh        # a path (contains a /: relative to the root)
!legacy/keep.sh    # re-includes a file

A script given explicitly on the command line is always analyzed. The interface (folder view) reads the same .checkscriptignore, and the --watch mode honors the patterns.

15. Starting a project: check-script init

check-script init                       # writes ./.checkscript.yaml
check-script init scripts/ --exclude vendor/ --context cron
check-script init --baseline .checkscript-baseline.json

init analyzes the scripts of the folder and writes a commented .checkscript.yaml with a suggested profile: strict if the average score is at least 8.5 (code already clean), legacy below 6 (old code, to be improved gradually), default otherwise. --profile forces it (no analysis), --context and --exclude are written into the file, --baseline FILE.json also writes the baseline report: the current debt is then tolerated and any new debt blocks (check-script -b FILE.json --fail-on-new high, see the baseline section). An existing file is never replaced without --force; --stdout prints the result instead.

16. Understanding a rule: check-script explain

check-script explain SEC003             # built-in rule
check-script explain SC2086 B602        # tool codes

Prints the rule’s category, severity, tool, CWE / OWASP / ANSSI references, the avoid / write instead example, the equivalent codes in other tools and the three ways to ignore it (directive on the line, on the file, rules.disabled). It also knows the custom rules of the configuration (--config). An unknown identifier ends with exit code 2 and suggestions.

17. Configuration

YAML file read, in order: --config FILE; otherwise, for each script, the nearest .checkscript.yaml (its folder, then the parent folders up to the root of the git repository) — per-subproject settings in a monorepo; otherwise ./.checkscript.yaml, then ${XDG_CONFIG_HOME:-~/.config}/check-script/config.yaml. Command-line options apply on top. All keys are optional; commented example: doc/checkscript.example.yaml. The graphical interface can export it with its own choices (rules, tools, Python version): the CLI and the CI then score as it does.

profile: strict                        # base: strict | default | legacy
context: [root]                        # execution contexts
followSource: true                     # shellcheck -x
pythonTarget: "3.11"                   # minimum Python (in quotes)
tools:
  shellcheck:
    path: /opt/shellcheck/shellcheck
    exclude: [SC1091, SC2034]
  bashate: false                       # disables the tool
  semgrep: false                       # machine without network access
  pylint: true                         # enables a tool disabled by default
  ruff:
    exclude: [PLR2004, S603, E501]     # passed to ruff --ignore
rules:
  disabled: [MNT005, E006]             # rules ignored, whatever the tool
  overrides:
    SC2086: { category: security, severity: medium }
  custom:                              # custom rules (see below)
    - id: ACME001
      pattern: 'set -euo pipefail'
      absent: true
      message: Always set -euo pipefail
scoring:
  weights: { critical: 4, high: 2, medium: 0.75, low: 0.25 }
  referenceLines: 100
  categoryWeights: { security: 1.5, performance: 0.5 }
thresholds:
  maxLineLength: 100
  maxFunctionLines: 60
  maxNesting: 4
  maxLinesWithoutFunction: 150

17.1. Custom rules

rules.custom declares project-specific rules (house conventions), checked line by line with a regular expression (Dart syntax, close to PCRE). They appear in the reports (tool custom), in the Rules tab of the interface and in --list-rules, and can be disabled and neutralized (# check-script disable=ACME001) like the others.

rules:
  custom:
    - id: ACME001                      # uppercase, digits, _ and -; not a built-in code
      pattern: '\bcurl\b[^|]*\|\s*(ba)?sh\b'
      message: No curl | sh in our scripts
      severity: critical               # critical, high, medium (default), low
      category: security               # default: maintainability
      fix: Download, verify the signature, then run.
      url: https://wiki.example.com/scripts#curl
    - id: ACME002
      pattern: 'set -euo pipefail'
      absent: true                     # reported (line 0) if the pattern appears nowhere
      language: shell                  # shell, python or any (default)
      message: {fr: Toujours set -euo pipefail, en: Always set -euo pipefail}
    - id: ACME003
      pattern: 'mkdir (?!-p )(\S+)'
      replace: 'mkdir -p $1'           # automatic fix ($0, $1… : groups)
      message: mkdir without -p
      ignoreCase: false                # default: case-sensitive
      comments: false                  # default: comment lines ignored

message and fix accept a single text or {fr: …, en: …}. With replace, the fix is offered in the interface and applied by --fix (after the other fixes, with the same syntax check). An invalid rule (identifier, expression, severity…) stops the analysis with an explicit message, like any configuration error.

18. Embedded scripts (CI, Dockerfile, Makefile, Ansible)

Shell written in other files is analyzed as a script, with the same tools; the issues point to the lines of the original file.

File Recognized by Shell extracted

GitHub Actions

.github/workflows/*.yml, action.yml

run: of the steps (bash by default, shell: sh; pwsh, python and Windows jobs ignored)

GitLab CI

.gitlab-ci.yml, .gitlab-ci.yml, .gitlab/*.yml

before_script, script, after_script (!reference ignored)

Dockerfile

Dockerfile, Containerfile, .dockerfile, Dockerfile.

RUN (continuations, --mount… options, heredoc; exec form ignored; SHELL respected)

Makefile

Makefile, GNUmakefile, *.mk

Recipes (@ - + prefixes, $$ → $, SHELL respected; define ignored)

Ansible

YAML containing a shell: task

shell: / ansible.builtin.shell: (string or cmd:, executable)

The host file’s expressions (${{ … }}, Jinja’s {{ … }}, make’s $(VAR)) are replaced by a neutral word. Specific rules:

  • SEC023 (Critical): in a GitHub workflow, an expression controllable by a third party (issue or pull request title or body, comment, commit message, branch name…) inserted as is in run:: command injection. Pass it through env: and use "$VAR".

  • SEC007: rm -r on a path that starts with a make variable (rm -rf $(BUILD)/* targets the root if it is empty).

The following do not apply to embedded scripts: shebang (POR001/002), set -e/-u options (ROB001/003, supplied or not by the host), pipefail when the host enables it (GitHub Actions, SHELL [… "pipefail" …]), PATH and lock (SEC015/017, ROB014/016), header and comments (MNT004/005/008/009), ROB011, POR006, formatting (shfmt, Bashate), and SC2148, SC2154, SC2317 (blocks are independent and the host defines variables: ENV, env:, variables:). Secret detectors scan the whole file.

Rules specific to host files (in addition to the shell they contain):

Rule Category Severity Description

DKR001

Security

Medium

Dockerfile: latest or unversioned base image (named stages and scratch are excluded)

DKR002

Security

High

Dockerfile: no unprivileged USER instruction in the final stage (or USER root)

DKR003

Security

Medium

Dockerfile: ADD of a URL without --checksum

DKR004

Maintainability

Low

Dockerfile: ADD of a local file (other than a tar archive): prefer COPY

DKR005

Security

Critical

Dockerfile: secret in ENV or ARG (Medium for an ARG without a value, which remains in the history)

DKR006

Security

Low

Dockerfile: apt-get install without --no-install-recommends

DKR007

Performance

Low

Dockerfile: apt, dnf/yum or apk cache left in the image

CI001

Security

Medium

GitHub Actions: action not pinned by its SHA (Low for actions/ and github/)

CI002

Security

Low/High

GitHub Actions: no permissions: (Low); permissions: write-all (High)

CI003

Security

Critical

GitHub Actions: pull_request_target or workflow_run that checks out the pull request’s code (Poisoned Pipeline Execution)

CI004

Security

Critical

Cleartext secret in env: (GitHub) or variables: (GitLab)

CI005

Security

Medium

CI container image not pinned (container:, services:, docker://; GitLab’s image: and services:)

hadolint, actionlint and zizmor, if installed, complement these rules; an issue they share with a built-in rule is counted only once (the built-in rule is kept, with its suggestion and references).

A # check-script disable=… directive is written in the shell or as a comment of the host file, on the preceding line. There is no automatic fix in a host file: --fix leaves it untouched.

19. PDF report

-o audit.pdf (or Export… in the interface) produces an audit deliverable:

  • cover page: date, check-script version, number of scripts, profile and contexts, average score and level, issues by severity;

  • summary: scripts from the weakest to the best (score, level, issues by severity);

  • frameworks: each CWE, OWASP or ANSSI reference with its title, the number of issues, the highest severity and the number of scripts (see Frameworks: CWE, OWASP, ANSSI);

  • packages to install (dnf, apt) for the scripts' external commands;

  • one section per script: scores by category, external commands, then each issue with its line, severity, rule, the code concerned (never a line containing a secret), the fix suggestion and its references.

The PDF uses a system TrueType font (Noto Sans, DejaVu Sans, Liberation Sans or Bitstream Vera, and their monospaced variant for code); without any of them, the standard PDF fonts, with non-Latin characters replaced.

20. External commands

For each shell script, check-script draws up the inventory of the external commands it runs (jq, rsync, curl… ; shell built-ins and the script’s functions excluded; the command run by sudo, env, timeout, xargs… is retained too), with the lines, their presence on the analysis machine (path) and, for common commands, the package that provides them on Fedora/RHEL (dnf) and Debian/Ubuntu (apt).

  • terminal: External commands line, ✗ for a command not found;

  • Markdown, AsciiDoc: list, packages of the missing commands;

  • HTML, PDF: table per script, and for a folder the dnf install … / apt install … line of the packages needed;

  • JSON: commands (name, lines, found, path, dnf, apt, checked).

The ROB017 rule reports a command not found that the script does not check itself; basic commands (coreutils, grep, sed, awk, find, tar…) are not subject to it. The analysis machine is not necessarily the target machine: ROB017 can be disabled like any rule. Embedded scripts (Dockerfile, CI) have no inventory: their commands run elsewhere.

21. Frameworks: CWE, OWASP, ANSSI

Each issue carries its normative references, when the correspondence is direct:

  • CWE (MITRE), e.g. CWE-494 Download of Code Without Integrity Check;

  • OWASP Top 10 2021, e.g. OWASP A08:2021 Software and Data Integrity Failures, and OWASP Top 10 CI/CD for workflows, e.g. OWASP CICD-SEC-4 Poisoned Pipeline Execution;

  • ANSSI: ANSSI-BP-028 Rnn (configuration recommendations for a GNU/Linux system, v2.0), ANSSI-FT-082 Rnn (Docker container deployment), ANSSI OpenSSH Rnn (secure use of OpenSSH).

The built-in rules have their references; those of Bandit (CWE) and of Semgrep (CWE, OWASP 2021) come from the tool; a rule of another tool receives those of the equivalent built-in rule (ShellCheck SC2164 ↔ ROB005, hadolint DL3007 ↔ DKR001, zizmor unpinned-uses ↔ CI001…). Style or performance rules generally have no reference.

Rule References

SEC001, SEC019

CWE-494, OWASP A08:2021, ANSSI-BP-028 R59 (trusted repositories)

SEC002, SEC022, PYSEC001

CWE-798, OWASP A07:2021

SEC003

CWE-95, CWE-78, OWASP A03:2021

SEC004

CWE-732, OWASP A01:2021, ANSSI-BP-028 R50 and R54

SEC005

CWE-295, OWASP A07:2021, ANSSI OpenSSH R6 (server legitimacy)

SEC009

CWE-377, CWE-59, ANSSI-BP-028 R55 (temporary directories)

SEC012

CWE-532, OWASP A09:2021

SEC013

CWE-250, ANSSI-BP-028 R56 (setuid / setgid)

SEC015, SEC016

CWE-426 / CWE-427, ANSSI-BP-028 R39 (sudo directives)

SEC020

CWE-668, ANSSI OpenSSH R20 (trusted relays)

SEC023

CWE-78, CWE-94, OWASP A03:2021, OWASP CICD-SEC-4

SEC024, SEC026, DKR001, CI001, CI005

CWE-1357 or CWE-829, OWASP A08:2021, OWASP CICD-SEC-3

SEC025

CWE-347, CWE-494, OWASP A08:2021, OWASP CICD-SEC-3, ANSSI-BP-028 R59

DKR002

CWE-250, OWASP A05:2021, ANSSI-FT-082 R7

DKR005, CI004

CWE-798, OWASP A07:2021, OWASP CICD-SEC-6

DKR006

ANSSI-BP-028 R58 (strictly necessary packages)

CI002

CWE-250, CWE-272, OWASP A01:2021, OWASP CICD-SEC-5

CI003

CWE-829, CWE-94, OWASP A08:2021, OWASP CICD-SEC-4

The full list is given by check-script --list-rules. The references appear in all reports: terminal (--details), Markdown, AsciiDoc, JSON (refs), JUnit, SARIF (external/cwe/cwe-NNN tags, recognized by GitHub Code Scanning) and HTML, which adds a Frameworks view: each reference with its title, the number of issues, the highest severity and the number of scripts concerned — the basis of a compliance audit report. --ref restricts the display to one reference or family; the Rules tab of the interface also searches in the references, and the hover in an editor (LSP) shows them as links.

22. Editors (VS Code, Eclipse, Geany…)

check-script lsp is an LSP server: the analysis appears directly in the editor, with the project’s configuration (nearest .checkscript.yaml), as on the command line.

  • Diagnostics: issues underlined and listed (severity, rule, suggestion, link to the documentation). Full analysis (ShellCheck, Ruff…) on open and on save; while typing, after a pause, the built-in rules, syntax and custom rules on the current text (the tools' results remain displayed as long as their line has not changed).

  • Quick fixes: fix the issue or all occurrences of the rule, ignore the rule on the line (# check-script disable=… above) or in the whole file (disable-file).

  • Hover: message, suggestion, "avoid / write" example, link.

  • Format document: shfmt (shell) or ruff format (Python).

  • Script score in the status bar (VS Code) or the status line (Eclipse).

Shell and Python scripts, and embedded scripts (Dockerfile, Makefile, CI, Ansible, see Embedded scripts (CI, Dockerfile, Makefile, Ansible)). The extensions use the installed check-script (PATH, or configurable path): version 0.16 or newer.

VS Code

check-script-VERSION.vsix file from the release:

code --install-extension check-script-VERSION.vsix

Settings (checkScript.): path (executable), lang, profile, analyzeOnType, typingDelay, trace.server. Commands (their labels are in French): *CheckScript: Analyser le fichier (analyze the file; also by clicking the score), CheckScript: Redémarrer le serveur (restart the server).

Eclipse (recent version, run with Java 21 or newer)

check-script-eclipse-VERSION.zip file from the release: Help → Install New Software… → Add… → Archive…, then tick CheckScript (LSP4E and the generic editor, present in the Eclipse IDE packages, are installed if needed). Scripts (.sh, .bash, .ksh, .zsh, .py), Dockerfile, Makefile/.mk and .gitlab-ci.yml open in the generic editor; for a file without an extension, *Open With → Generic Text Editor. Settings: Window → Preferences → CheckScript (executable, language, profile, analysis while typing). If another plugin (PyDev…) already claims .py files, Eclipse may give it priority.

Geany (2.1 or newer)

With the LSP Client plugin (geany-plugins): Tools → LSP Client → User configuration, then paste the sections of editors/geany/lsp.conf (Sh, Python, Dockerfile, Make, YAML). Geany has only one server per file type: for Python, CheckScript then replaces pyright or pylsp.

Other editors

Any LSP client: command check-script lsp, initialization options lang, profile, analyzeOnType, typingDelay (ms); notification checkScript/score (uri, score, grade, findings) after each full analysis.

23. Continuous integration

A dedicated guide, Continuous integration guide, covers the three adoption levels (threshold, baseline, ratchet), the pull request comparison and complete examples for GitHub, GitLab, Jenkins and pre-commit (doc/ci/).

GitLab

The "Code Quality" format is displayed in the merge request widget. Complete example: doc/ci/gitlab-ci.yml.

check-script --summary -o gl-code-quality-report.json -o check-script.html \
  --fail-under 6 --fail-under security=8 .
GitHub

The repository’s action builds check-script, installs ShellCheck and shfmt, annotates the lines concerned (visible in pull requests), adds the report to the job summary and produces a SARIF for "Security → Code scanning". Complete example: doc/ci/github-actions.yml.

- uses: actions/checkout@v7
- id: cs
  uses: chla28/CheckScript@v0.24.1
  with:
    paths: scripts .github          # default: .
    fail-under: '6'                 # or security=8,global=6; default: no threshold
    changed-since: ''               # e.g. origin/main (checkout with fetch-depth: 0)
    args: --profile strict          # additional options
    lang: en                        # fr (default) or en
    annotations: 'true'
    summary: 'true'
    sarif: check-script.sarif       # empty: no SARIF
    install-tools: 'true'           # ShellCheck and shfmt through apt
- uses: github/codeql-action/upload-sarif@v4
  if: always()
  with:
    sarif_file: check-script.sarif
- run: echo "Average score ${{ steps.cs.outputs.score }} (${{ steps.cs.outputs.grade }})"

Outputs: score (average score), grade (lowest level), scripts (number analyzed), sarif (path). The action’s repository must be accessible to the repository that uses it (public, or same organization with access to actions allowed). Without the action: --format github writes the annotations to the standard output.

Jenkins, GitLab (Tests tab)

-o report.xml (or --format junit) writes a JUnit XML report: one suite per script, one failed case per issue (type: severity).

check-script:
  script: check-script -q -o check-script.xml -o gl-code-quality-report.json .
  artifacts:
    when: always
    reports:
      junit: check-script.xml
      codequality: gl-code-quality-report.json
pre-commit

With the https://pre-commit.com framework, in .pre-commit-config.yaml:

repos:
  - repo: https://github.com/chla28/CheckScript
    rev: v0.24.1
    hooks:
      - id: check-script        # or check-script-fix to fix

Without the framework, the standalone hook checks the staged version of the modified scripts:

ln -s /path/to/CheckScript/scripts/pre-commit-hook.sh .git/hooks/pre-commit
# variables: CHECK_SCRIPT_MIN (default 6), CHECK_SCRIPT_ARGS, CHECK_SCRIPT_SKIP=1

The JSON format (-o report.json) is stable: scores by category, counters, issues with fingerprint, suggestion and link, tools and versions, comparison with the baseline.

24. Graphical interface

check-script-gui ("CheckScript" menu, or the MainGUI launcher of the AuditTools suite) reuses the same engine, the same rules and the same reports:

  • Analysis: open or drop a script; annotated source (severity mark in the margin, tooltip of the issues, syntax highlighting of shell, Python and host files — YAML, Dockerfile, Makefile), in a monospaced font: JetBrains Mono, embedded, or any installed monospaced font (Settings → Code font, fontconfig list or typed name); text size through the A− / A+ buttons above the code (click on the size: 13 pt), Ctrl+plus / Ctrl+minus / Ctrl+0, Ctrl+wheel on the code or the settings slider; font and size also apply to the diffs and the fix examples, and are remembered; Summary tab (overall score, radar of the five categories, counters by severity) and Issues tab (filters by category and severity, suggestion, link to the documentation; a click moves the source to the line and expands the code to write, see Fix code for each issue); the bar between the code and the results can be moved with the mouse (double-click: default split), its arrows collapse either panel; the split is remembered, separately for a wide window (side by side) and a narrow one (one above the other);

  • Fix…: diff preview then application; from an expanded issue, Apply this fix (this occurrence) or Fix the N occurrences (the whole rule, if there are several); in the Issues tab, fixable issues have a checkbox (and Fixable shown ticks them all, according to the filters): Fix selection shows the global diff before applying;

  • Rules: all detection rules — built-in rules, tool codes classified by the tool (ShellCheck, Ruff, Bandit, Pylint…) and codes encountered during analyses —, each with a checkbox: an unticked rule is no longer reported from the next analysis (Re-run analysis button). Search by code, text or tool; filters by language, tool and category; field to disable any other code (e.g. SC2317); Enable all. Rules disabled by the profile or the YAML file appear unticked and greyed out. In the list of issues, an expanded issue also offers Stop reporting its rule. A rule is disabled by its identifier, whatever the tool, like rules.disabled in the configuration; the choice is remembered;

  • Folder: sortable table of all the scripts of a folder; with a baseline loaded, Trend column (evolution of the score); scripts saved or created in the folder are re-analyzed (same setting as the watching of the open script);

  • Recent (clock icon under the tabs): the last 10 scripts and folders analyzed; dragging and dropping a script or a folder onto the window also analyzes it;

  • Export…: HTML, Markdown, AsciiDoc, JSON, SARIF; a folder’s HTML report starts with a summary (average score, levels, issues by severity), a sortable table of scripts and the most frequent rules;

  • Settings: language, theme, profile, contexts, tools grouped by language (with version detection; Pylint and Pyright can be enabled), minimum Python version, following of sourced files, YAML configuration file: Import… (the file becomes the reference: its profile and contexts are taken over, the interface’s choices reset) and Export… (writes the effective configuration — file and interface choices — to a .checkscript.yaml usable by the CLI and the CI).

Folder analyses process several scripts at once.

Also in the interface:

  • Summary: "What weighs on the score" (most costly rules, gain by fixing them, attainable level); Issues: sorting By category or By quick win;

  • Open in editor (toolbar, or from an expanded issue at its line): detected editor (VS Code, VSCodium, GNOME Text Editor, gedit, Kate, KWrite, Xed, Geany) or command from the Settings, with {file} and {line} (e.g. code -g {file}:{line});

  • the open script is watched: on each save, the analysis is re-run (setting Re-run the analysis when a script is saved);

  • Folder: History of the folder’s analyses (average score curve; ~/.local/share/check-script/history, 200 analyses per folder);

  • Report a false positive (expanded issue): the case is recorded on the machine, anonymized (string contents and long tokens masked, secret line never copied), with a comment, in ~/.local/share/check-script/false-positives.jsonl; dart run tool/false_positives.dart --details summarizes it by rule to enrich the corpus;

  • with no configuration file chosen, the project’s .checkscript.yaml of the script or folder analyzed applies; Reuse the results of an unchanged script enables the cache, like the CLI.

  • Several scripts: Open a script (Ctrl+O) accepts several files, and several scripts can be dropped on the window; each one gets a tab (shown from two scripts on) with its own analysis, kept while it is up to date. Ctrl+Tab / Ctrl+Shift+Tab switch tabs, Ctrl+W closes one;

  • Search: Ctrl+F searches the code (magnifier above it: matching lines are tinted, Enter / Shift+Enter go to the next / previous one) or, with the Issues tab shown, the issues (rule code, text, tool, reference or line, e.g. SEC003, eval, L12); on the Rules and Help screens it focuses their search field;

  • Set as baseline… (Analysis and Folder screens): writes the displayed analysis as a JSON report and loads it as the baseline in one click; the file can be shared with the CI (check-script -b FILE.json);

  • the folder view honors the .checkscriptignore file of the folder and the exclude key of the configuration (see Excluding paths);

  • keyboard shortcuts: F1 help, Ctrl+O open scripts, Ctrl+Shift+O open a folder, F5 / Ctrl+R re-run, Ctrl+E export, Ctrl+F search, Ctrl+Tab / Ctrl+W tabs, Ctrl+1 … Ctrl+5 screens, Ctrl+plus / minus / 0 code size; tooltips recall them;

  • Compare… (Analysis and Folder screens): puts a "before" state (a JSON report, or the loaded baseline) and an "after" state (the displayed analysis, or another JSON report) side by side: fixed issues in green on the left, new ones in red on the right, score evolution per category, regressions first; Copy (Markdown) gives a merge request comment (same engine as check-script diff, see Comparing two reports: check-script diff);

  • Rule details (the ⓘ icon of a rule in the Rules screen): description, category, severity, references, "avoid / write instead" example, equivalent codes in other tools, occurrences in the displayed script and folder, enable switch and ways to ignore it (same as check-script explain);

  • Editing: the pencil above the code opens a text editor in the panel; Ctrl+S (or Save) writes the file — a .orig copy keeps the original, line endings are kept — and re-runs the analysis. Unsaved changes form a draft that survives tab changes (● before the name, banner) and a confirmation is asked before closing a modified tab; saving is refused if the file changed on disk since the analysis;

  • Undo: every fix, save from the editor and restore is recorded in the session history (50 entries). Undo (toolbar) puts the displayed script back into its state before its last change, byte for byte, and re-runs the analysis; it is refused if the file changed since (nothing is overwritten). History… lists the changes (only the most recent of a script can be undone, in order), and Restore the original puts the script back as it was before the first change of the session, from its .orig copy, after confirmation (the restore can itself be undone);

  • Accessibility: the colors of severities and scores have a contrast of at least 4.5:1 (WCAG AA) on all backgrounds in both themes; color is never the only cue (labels, tooltips); the code margin, the radar and the history curve are described to screen readers;

  • Help: the last tab is an online help — getting started, each screen, how the score is computed, fixes, baseline, rules, settings, profiles and contexts, tools, YAML file, directives, false positives, exports, shortcuts —, with a search field and the same content in French and in English. F1 opens the help of the screen being displayed, and the ? icons next to the toolbars, the summary, the issue filters and each group of settings show a short summary of their topic with a link to the full page. Every button, filter, column header, tool and setting also has a tooltip (hover it);

Tool-by-tool progress is displayed during the analysis, which can be cancelled. A path passed as an argument (check-script-gui deploy.sh or a folder) is analyzed at startup.

25. Frequently asked questions

The report says "Missing tools"

The analysis was limited to the built-in rules for these tools; install them for a complete result (check-script --list-tools gives the commands).

My script is bash but has no shebang

Use --shell bash; without a shebang, the dialect is unknown, POR001 is reported and checkbashisms is run in forced mode.

How do I ignore a rule for a single script?

With a # check-script disable=… / disable-file=… directive (see Directives in the script), or # shellcheck disable=… for ShellCheck codes.

Why is a POSIX script with a few bashisms scored so low?

Because it will really fail on systems where /bin/sh is dash (Debian, Ubuntu): function, [[, &>, <<< or arrays are errors there, not style details. Declare #!/usr/bin/env bash if the script is written for bash.