- 1. Overview
- 2. Installation
- 3. Usage
- 4. Reading the report
- 5. Profiles
- 6. Execution contexts
- 7. Directives in the script
- 8. Automatic fix
- 9. Baseline and evolution
- 10. Comparing two reports:
check-script diff - 11. External tools: what is used
- 12. Built-in rules
- 13. Python scripts
- 14. Excluding paths
- 15. Starting a project:
check-script init - 16. Understanding a rule:
check-script explain - 17. Configuration
- 18. Embedded scripts (CI, Dockerfile, Makefile, Ansible)
- 19. PDF report
- 20. External commands
- 21. Frameworks: CWE, OWASP, ANSSI
- 22. Editors (VS Code, Eclipse, Geany…)
- 23. Continuous integration
- 24. Graphical interface
- 25. Frequently asked questions
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 ( |
Maintainability |
Readability, style, formatting, function size, nesting, comments… |
Portability |
Bashisms in a |
Performance |
Needless processes ( |
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.
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/toolsThe 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.
sudo dnf install ./check-script-VERSION-1.fc44.x86_64.rpm # CLI
sudo dnf install ./check-script-gui-VERSION-1.fc44.x86_64.rpm # interfaceThe 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.
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 CLIWithout FUSE (container…), add APPIMAGE_EXTRACT_AND_RUN=1. The analysis
tools are those of the system.
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 # CLIThe 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) |
|---|---|---|
|
Reference for shell static analysis (quoting, errors, POSIX portability…); suggests fixes |
|
|
Canonical formatting, syntax tree for accurate built-in rules, detection of constructs outside the dialect |
|
|
Style rules (OpenStack conventions) |
|
|
Bashisms in |
|
|
Secret detection (optional) |
|
|
Secret detection (optional, run without network verification) |
|
|
Syntax check ( |
present on every distribution |
|
Dockerfile: best practices and security (pinned versions, |
|
|
GitHub Actions workflows: syntax, expressions, injections |
|
|
GitHub Actions workflows: security audit (unpinned actions, dangerous triggers, injections, credential persistence…), without network access ( |
|
For Python scripts:
| Tool | Contribution | Installation |
|---|---|---|
|
Reference linter (errors, likely bugs, security, modernization, performance); formatting; suggests fixes |
|
|
Security (injections, |
|
|
Security and correctness rules from the |
|
|
Typing: call and operand errors detected without running the code |
|
|
Cyclomatic complexity and maintainability index |
|
|
Minimum Python version required (portability) |
|
|
Full linter, redundant with Ruff: disabled by default ( |
|
|
Typing, redundant with mypy: disabled by default ( |
|
|
Known vulnerabilities of the packages declared in the project’s |
|
|
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.
|
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| Option | Effect |
|---|---|
|
Writes the report to FILE; format according to the extension: |
|
|
|
Report language. Default: |
|
Forces the dialect instead of inferring it from the shebang (or from the |
|
Minimum Python version the scripts must support (default: |
|
Scoring profile (see Profiles). |
|
Execution context (see Execution contexts). Repeatable. |
|
Follows sourced files ( |
|
YAML configuration file (see [config]). |
|
Enables a tool disabled by default ( |
|
Disables a tool ( |
|
Uses only the built-in rules. |
|
In a folder, ignores embedded scripts (see Embedded scripts (CI, Dockerfile, Makefile, Ansible)). |
|
Shows only the issues attached to this normative reference, by prefix and case-insensitively: |
|
After the analysis, watches the targets: each script saved (or created) is re-analyzed and its report displayed; with |
|
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. |
|
Where to record the history of analyzed folders (default with |
|
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 ( |
|
Walks the sub-folders of the given folders (by default only the scripts of the folder itself are analysed). With a pattern without |
|
Excludes from a folder analysis the paths matching the pattern ( |
|
Reuses the report of an unchanged script (default; |
|
Terminal: what each rule costs (points removed from its category, gain on the overall score if fixed, automatic fix available). |
|
Terminal: issues by category (default) or by quick win (impact on the score, automatic fixes favored). |
|
Number of scripts of a folder analyzed simultaneously (default: depends on the number of processors, at most 4; |
|
Baseline: only new issues are detailed, the evolution of the scores is displayed (see Baseline and evolution). |
|
With |
|
With |
|
Fixes safe defects in the file (see Automatic fix). |
|
With |
|
With |
|
With |
|
Terminal: every issue, with its fix suggestion (default: 10 per category). |
|
Terminal: summary only. |
|
Terminal: shows under each issue the line of code concerned, with a |
|
Forces or disables color (default: automatic, |
|
No report on the standard output. |
|
Exit code 1 if the overall score, or that of a category, is below the threshold. Repeatable; several thresholds separated by commas ( |
|
Detected tools / built-in rules. |
|
All known rules: built-in rules and tool codes classified by check-script (identifier, tool, language, category, severity, title). |
|
Version / help. |
--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.
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.
| Severity | Meaning |
|---|---|
Critical |
Probable compromise or destruction (downloaded code executed, hard-coded secret, |
High |
Serious exploitable defect or one with major side effects (eval of a variable, |
Medium |
Real defect with limited behavior (unquoted variable, no |
Low |
Style, readability, performance, good practices. |
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).
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.
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.
| Profile | Usage |
|---|---|
|
New scripts: weights Critical 5, High 3, Medium 1, Low 0.4; attenuation from 50 lines; lines ≤ 100, functions ≤ 40 lines, nesting ≤ 3. |
|
Settings described above. |
|
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.
Declaring how the script is run enables specific rules and raises some severities (they are never lowered):
| Context | Rules enabled | Severities raised |
|---|---|---|
|
SEC017 (PATH not set) |
predictable temporary file, cleartext download, untrusted |
|
ROB014 (no lock), ROB015 (interactive input), ROB016 (PATH not set) |
no |
|
SEC017, ROB015 |
no |
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--fix applies only safe transformations:
-
the fixes proposed by ShellCheck (variable quoting,
cd … || exit, backticks…); -
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); -
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.origWith --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.shFor 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 -ncheck, 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).
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.
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.
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-worseCompares 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…).
- 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…).SC1091is 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 (awhichin a string is not a command; amkdirin the condition of anifis checked). - bashate
-
Style; E003 ignored for a script indented otherwise than with 4 spaces.
- checkbashisms
-
/bin/shscripts (or without a shebang) only. - gitleaks, trufflehog
-
Secret detection; trufflehog is run with
--no-verificationso that no secret is sent over the network. - bash -n
-
Syntax check with the dialect’s interpreter: any error is Critical.
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 ( |
SEC002 |
Security |
Critical |
Hard-coded secret (password, token, AWS/GitHub key, private key) |
SEC003 |
Security |
High |
|
SEC004 |
Security |
High |
World-writable permissions ( |
SEC005 |
Security |
High |
TLS / host key verification disabled |
SEC006 |
Security |
Medium |
Cleartext download ( |
SEC007 |
Security |
High |
|
SEC008 |
Security |
Critical |
Recursive deletion of |
SEC009 |
Security |
Medium |
Temporary file with a predictable name in |
SEC010 |
Security |
High |
Password on the command line ( |
SEC011 |
Security |
Medium |
|
SEC012 |
Security |
Low |
|
SEC013 |
Security |
High |
setuid/setgid bit |
SEC014 |
Security |
Medium |
Secret typed without masking ( |
SEC015 |
Security |
Low |
|
SEC016 |
Security |
High |
PATH containing |
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 ( |
SEC020 |
Security |
Medium |
Risky SSH option ( |
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 ( |
SEC024 |
Security |
Medium |
Package installed without a pinned version: |
SEC025 |
Security |
High |
Repository or package without signature verification: |
SEC026 |
Security |
Low |
|
ROB001 |
Robustness |
Medium |
No stop on error ( |
ROB002 |
Robustness |
Low |
Pipelines without |
ROB003 |
Robustness |
Low |
No |
ROB004 |
Robustness |
Low |
|
ROB005 |
Robustness |
Medium |
|
ROB006 |
Robustness |
Medium |
Unquoted variable in |
ROB007 |
Robustness |
Low |
|
ROB008 |
Robustness |
Medium |
Loop over the output of |
ROB009 |
Robustness |
High |
CRLF line endings |
ROB010 |
Robustness |
Medium |
Unquoted |
ROB011 |
Robustness |
Low |
Return code of a critical command ignored (without |
ROB012 |
Robustness |
Low |
IFS modified globally without restoration |
ROB013 |
Robustness |
Low |
Cleanup |
ROB014 |
Robustness |
Medium |
Scheduled task without a lock ( |
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 ( |
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 |
POR003 |
Portability |
Medium |
Bash construct in a |
POR004 |
Portability |
Low |
|
POR005 |
Portability |
Low |
Obsolete |
POR006 |
Portability |
Low |
A single package manager without an alternative |
POR007 |
Portability |
Low |
Obsolete net-tools utilities |
PERF001 |
Performance |
Low |
Useless |
PERF002 |
Performance |
Low |
|
PERF003 |
Performance |
Low |
|
PERF004 |
Performance |
Low |
External command on every loop iteration |
PERF005 |
Performance |
Low |
Chained filters that can be reduced |
PERF006 |
Performance |
Low |
|
PERF007 |
Performance |
Low |
|
PERF008 |
Performance |
Low |
Useless |
PERF009 |
Performance |
Low |
|
|
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).
|
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).
- Ruff
-
tools.ruff.config: projectfollows the Ruff configuration of the script’s project (ruff.toml,.ruff.tomlorpyproject.tomlwith[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) andS603are 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 (F821undefined name → High,S602shell=True→ High…).ruff format --diffreports 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, unsafeyaml.load, SQL built by concatenation) as High, like SEC003. A secret’s value is never copied into the report.B404andB603(any import or call of subprocess) are excluded by default. - Semgrep
-
p/pythonrules, 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.C0103andC0114–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…
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.
| Rule | Category | Severity | Description |
|---|---|---|---|
PYSEC001 |
Security |
Medium |
High-entropy string: potential secret (line never copied), like SEC022 |
PYROB001 |
Robustness |
Low |
Call to |
PYROB002 |
Robustness |
Medium |
|
PYROB003 |
Robustness |
Medium |
Scheduled task without a lock ( |
PYROB004 |
Robustness |
Medium |
Return code of |
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 |
PYPOR001 |
Portability |
High |
|
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).
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.
--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.
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 PATTERNoption (repeatable); -
the
exclude:key of the configuration (.checkscript.yaml); -
a
.checkscriptignorefile 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 fileA 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.
check-script init # writes ./.checkscript.yaml
check-script init scripts/ --exclude vendor/ --context cron
check-script init --baseline .checkscript-baseline.jsoninit 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.
check-script explain SEC003 # built-in rule
check-script explain SC2086 B602 # tool codesPrints 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.
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: 150rules.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 ignoredmessage 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.
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 |
|
|
GitLab CI |
|
|
Dockerfile |
|
|
Makefile |
|
Recipes ( |
Ansible |
YAML containing a |
|
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 throughenv:and use"$VAR". -
SEC007:
rm -ron 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: |
DKR002 |
Security |
High |
Dockerfile: no unprivileged |
DKR003 |
Security |
Medium |
Dockerfile: |
DKR004 |
Maintainability |
Low |
Dockerfile: |
DKR005 |
Security |
Critical |
Dockerfile: secret in |
DKR006 |
Security |
Low |
Dockerfile: |
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 |
CI002 |
Security |
Low/High |
GitHub Actions: no |
CI003 |
Security |
Critical |
GitHub Actions: |
CI004 |
Security |
Critical |
Cleartext secret in |
CI005 |
Security |
Medium |
CI container image not pinned ( |
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.
-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.
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.
Each issue carries its normative references, when the correspondence is direct:
-
CWE (MITRE), e.g.
CWE-494Download of Code Without Integrity Check; -
OWASP Top 10 2021, e.g.
OWASP A08:2021Software and Data Integrity Failures, and OWASP Top 10 CI/CD for workflows, e.g.OWASP CICD-SEC-4Poisoned 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.
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.vsixfile 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.zipfile 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/.mkand.gitlab-ci.ymlopen 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.pyfiles, 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 optionslang,profile,analyzeOnType,typingDelay(ms); notificationcheckScript/score(uri,score,grade,findings) after each full analysis.
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 githubwrites 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.
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, likerules.disabledin 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.yamlusable 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 --detailssummarizes it by rule to enrich the corpus; -
with no configuration file chosen, the project’s
.checkscript.yamlof 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
.checkscriptignorefile of the folder and theexcludekey 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
.origcopy 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
.origcopy, 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.
- 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-toolsgives 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/shis dash (Debian, Ubuntu):function,[[,&>,<<<or arrays are errors there, not style details. Declare#!/usr/bin/env bashif the script is written for bash.