Skip to content

Repository files navigation

shellscan

shellscan

Find and lint every shell in a project — .sh files, shebangs, and scripts embedded in GitLab CI and GitHub Actions YAML.

An Alpine-based image wraps shellcheck with four discovery modes: .sh files, files with a shell shebang (/bin/bash and env bash forms), and shell scripts embedded in CI YAML — GitLab CI before_script / script / after_script keys and GitHub Actions run steps — extracted via yq, with YAML anchors expanded. File discovery uses fd. Findings render as terminal output, GitLab Code Quality JSON for the merge request widget, or SARIF 2.1.0 for GitHub code scanning; embedded-script findings map back to their YAML source lines. Opt-in security rules flag remote code piped into a shell, secrets echoed to job logs, unquoted attacker-controllable CI variables, and GitHub expressions injecting untrusted data into run scripts.

latestpipelineghcr.ioDocker Hublicensestarscoverageskillscoroboros.com

Contents

Requirements

Docker, BuildKit, or any OCI runtime able to pull from the GitHub Container Registry (or the Docker Hub mirror).

Image

ghcr.io/coroboros/shellscan:{tag} — mirrored to docker.io/coroboros/shellscan:{tag}, and in the GitLab Container Registry at registry.gitlab.com/coroboros/security/infrastructure/shellscan:{tag}.

  • Architectures: linux/amd64, linux/arm64
  • Working directory: /shellscan
  • Entrypoint: shellscan
  • User: non-root shellscan (uid 10000)

Tags

TagBaseArchitecturesSizeSourceMutability
<version>koalaman/shellcheck-alpine:v0.11.0amd64, arm6418.6 MB (amd64), 27.0 MB (arm64)SemVer git tagimmutable
mainsame as <version>same as <version>varies by buildlatest green main buildrolling
<sha>same as <version>same as <version>varies by buildevery buildimmutable

Sizes are compressed layer sums from GHCR. The base image source of truth is the FROM line in the Dockerfile. Pin a digest for reproducible builds; see Provenance.

Commands

shellscan [mode] [fd_options]

mode

Discovery mode passed as the first positional argument.

ModeBehavior
allDefault. Scan every situation below combined.
gitlab-ciScan scripts embedded in .yml / .yaml files under before_script / script / after_script keys. YAML anchors are expanded.
github-actionsScan run scripts embedded in .yml / .yaml files under jobs.<id>.steps[].run (workflows) and runs.steps[].run (composite actions). ${{ }} expressions are neutralized before linting; shellcheck runs with the step's bash/sh dialect and skips steps whose effective shell: resolves to pwsh, python, cmd, or node — Windows runners and windows-only matrices default to pwsh. The security rules run on every run script regardless. YAML anchors are expanded.
shebangScan files with a shell shebang (sh, bash, dash, ksh).
.shScan .sh files.
-hPrint help and exit.
fd_options

Extra options for the fd invocation, passed as one quoted argument (reference). Options are whitespace-split and passed verbatim — fd matches its own glob patterns, the shell never expands them. Command-execution flags (-x, -X, --exec, --exec-batch) are refused.

Environment variables
VariableDefaultPurpose
SHELLCHECK_OPTS--color=alwaysOptions passed to shellcheck.
SHELLSCAN_JOBS1Parallel scan workers. Above 1 speeds up large scans; shellcheck output interleaves across files, final counts and exit code stay correct.
SHELLSCAN_FORMAThumanOutput format: human, codequality (GitLab Code Quality JSON), or sarif (SARIF 2.1.0). Machine formats own stdout; progress moves to stderr.
SHELLSCAN_BASELINE.shellscanignoreBaseline file of finding fingerprints suppressed in machine formats — one per line, # comments allowed.
SHELLSCAN_SECURITY0Set to 1 to enable the security rules on scripts embedded in CI YAML — GitLab CI and GitHub Actions.
Exit codes
CodeMeaning
0Scan succeeded, no findings.
1Findings reported.
2Usage error, refused option, or discovery failure. A failed discovery aborts — it never reads as a clean scan of zero files.
Examples
shellscan gitlab-ci
SHELLCHECK_OPTS="--severity=info --color=never" \
shellscan gitlab-ci '--exclude *.yaml --exclude .gitlab-ci.yml'
SHELLSCAN_SECURITY=1 SHELLSCAN_FORMAT=codequality \
shellscan gitlab-ci > gl-code-quality-report.json
SHELLSCAN_SECURITY=1 SHELLSCAN_FORMAT=sarif \
shellscan github-actions > shellscan.sarif

Run

GitLab CI
check-sh-files:
image:
name: registry.gitlab.com/coroboros/security/infrastructure/shellscan:<tag>entrypoint: [""]stage: checkvariables:
SHELLCHECK_OPTS: >- --severity=warning --color=neverscript:
- shellscan .sh
check-ci-yaml-files:
image:
name: registry.gitlab.com/coroboros/security/infrastructure/shellscan:<tag>entrypoint: [""]stage: checkscript:
- shellscan gitlab-ci
check-files-with-shebang:
image:
name: registry.gitlab.com/coroboros/security/infrastructure/shellscan:<tag>entrypoint: [""]stage: checkscript:
- cd ..
- shellscan shebang '--exclude *.sh'
parallel-scan:
image:
name: registry.gitlab.com/coroboros/security/infrastructure/shellscan:<tag>entrypoint: [""]stage: checkvariables:
SHELLSCAN_JOBS: "4"script:
- shellscan all
CI/CD component

One include runs the scan and publishes a Code Quality report for the merge request widget. Findings are non-blocking by default; set fail_on_findings: true to gate the pipeline.

include:
- component: gitlab.com/coroboros/security/infrastructure/shellscan/shellscan@<version>inputs:
security: true

The final /shellscan segment is the component name from templates/shellscan.yml; GitLab component refs are <fqdn>/<project-path>/<component-name>@<version>.

Inputs: job_name, stage, mode, fd_options, security, fail_on_findings, jobs, shellcheck_opts, baseline, image — see templates/shellscan.yml.

GitHub Actions

The same image scans a GitHub repository — github-actions mode lints the workflows themselves, and the SARIF report lands in code scanning:

jobs:
shellscan:
runs-on: ubuntu-latestpermissions:
security-events: writesteps:
- uses: actions/checkout@v4
- name: scanrun: | docker run --rm -v "$PWD:/shellscan" \ -e SHELLSCAN_SECURITY=1 -e SHELLSCAN_FORMAT=sarif \ ghcr.io/coroboros/shellscan:<tag> all > shellscan.sarif || [ $? -eq 1 ] - uses: github/codeql-action/upload-sarif@v3with:
sarif_file: shellscan.sarif
pre-commit

The hook builds from the pinned repo rev — Docker is the only local runtime dependency.

repos:
- repo: https://gitlab.com/coroboros/security/infrastructure/shellscanrev: <version>hooks:
- id: shellscan
Docker

Mount the project as the /shellscan volume and run any mode:

docker run --rm \
-v "$PWD:/shellscan" \
registry.gitlab.com/coroboros/security/infrastructure/shellscan:<tag>
docker run --rm \
-v "$PWD:/shellscan" \
registry.gitlab.com/coroboros/security/infrastructure/shellscan:<tag> \
gitlab-ci '--exclude "*.yaml"'

Reports

SHELLSCAN_FORMAT=codequality emits GitLab Code Quality JSON; SHELLSCAN_FORMAT=sarif emits SARIF 2.1.0 for GitHub code scanning. Both formats write the report to stdout and move progress to stderr. Findings from scripts embedded in CI YAML carry the YAML source line and their selector — before_script / script / after_script for GitLab CI, jobs.<id>.steps[].run for GitHub Actions — so annotations land on the YAML line a reviewer actually reads.

Wire the Code Quality report into a job and findings surface in the merge request widget:

shellscan:
image:
name: registry.gitlab.com/coroboros/security/infrastructure/shellscan:<tag>entrypoint: [""]stage: checkvariables:
SHELLSCAN_FORMAT: codequalityscript:
- shellscan gitlab-ci > gl-code-quality-report.json || [ $? -eq 1 ]artifacts:
when: alwaysreports:
codequality: gl-code-quality-report.json

|| [ $? -eq 1 ] keeps findings non-blocking while discovery failures (exit 2) still fail the job. Drop it to gate the pipeline on findings.

Every finding carries a stable SHA-256 fingerprint of its file, rule, and message. List fingerprints in .shellscanignore (one per line, # comments allowed) to suppress known findings — adopt shellscan on a legacy tree without fixing decades of shell first, and let the gate catch only what is new.

Security rules

SHELLSCAN_SECURITY=1 adds rules that target the scripts inside CI YAML — GitLab CI and GitHub Actions — the injection surface shellcheck has no opinion on.

RuleSeverityFlags
SHELLSCAN-CURL-PIPEcriticalcurl or wget piped into a shell — remote code executed unverified.
SHELLSCAN-EVALmajoreval on an expanded value — dynamic input runs as code.
SHELLSCAN-SECRET-ECHOmajorecho / printf of a secret-named variable — the secret lands in job logs.
SHELLSCAN-CI-INJECTIONmajorUnquoted attacker-controllable CI variable (CI_COMMIT_MESSAGE, CI_MERGE_REQUEST_TITLE, branch and tag names) — a crafted commit injects shell syntax into the job.
SHELLSCAN-GHA-INJECTIONcritical${{ }} expression of attacker-controllable data (github.event.pull_request.title, github.head_ref, commit messages) inside a run script — substituted before any shell parses, so quoting cannot help; pass it through an environment variable.

Each finding reports the YAML source line. The rules run on extracted scripts only — .sh and shebang files already get the full shellcheck treatment. The injection rules are platform-scoped: SHELLSCAN-CI-INJECTION fires on GitLab CI scripts, SHELLSCAN-GHA-INJECTION on GitHub Actions run scripts — a literal ${{ }} in a GitLab script is inert text and never flagged.

Agents

shellscan ships an agent skill — its own scan-and-triage guide — for coding agents. Install it into an agent:

npx skills add coroboros/shellscan

Or read it without installing: skills/shellscan/SKILL.md.

Packages

Same across all shellscan tags.

PackagePurpose
shellcheckThe linter — provided by the koalaman/shellcheck-alpine base image.
bashShell — src/shellscan.sh runs on bash.
fdFast file discovery — used to enumerate files to scan.
yqYAML processor — extracts scripts from before_script / script / after_script and run keys and expands anchors.
jqJSON processor — renders Code Quality and SARIF reports from shellcheck json1 output.
ca-certificatesTLS certificate bundle.

Provenance

Every published image, via the shared coroboros/cicontainer-images template, is:

  • multi-arch — BuildKit, linux/amd64,linux/arm64;
  • gated — source secrets via gitleaks, image CVEs via Trivy on the published :sha, and the image smoke before tag promotion;
  • signed — cosign keyless on the immutable digest, with a CycloneDX SBOM attestation.

On version tags, the promoted digest is published to the GitLab Registry, GitHub Container Registry, and Docker Hub. Each registry-local digest is signed and attested after copy. Pin the @sha256 digest downstream for byte-reproducible scans.

A cosign signature is bound to the digest, not the tag — verify the pinned digest:

cosign verify \
--certificate-identity-regexp 'https://gitlab.com/coroboros/security/infrastructure/shellscan//.*' \
--certificate-oidc-issuer https://gitlab.com \
ghcr.io/coroboros/shellscan@sha256:<manifest-list-digest>

Compared to alternatives

Capabilityshellcheck directyamllintpre-commit + shellcheckGitLab CI Lintsuper-linteractionlintshellscan
Lint .sh filesyesnoyesnoyesnoyes
Lint files via shebang detectionnonononoyesnoyes
Extract shells embedded in GitLab CI YAMLnonononononoyes
Extract shells embedded in GitHub Actions YAMLnonononovia actionlintyesyes
Expand YAML anchors before scanningnononononoyes (workflows)yes
Single binary / no orchestration setupyesyesnoyesnoyesyes (image)
Configurable file discovery (fd options)nonononononoyes
Parallel scan (configurable workers)nononononoautoyes
GitLab Code Quality + SARIF outputnononononoSARIF via templateyes
Security rules on CI-embedded shellnononononoGH untrusted inputsyes
Baseline file for incremental adoptionnonononononoyes

The unique angle: shellscan finds shell wherever it lives in a project — including the often-overlooked scripts hidden inside CI YAML — runs it through the canonical shellcheck linter with YAML anchors expanded, and reports findings where reviewers look: GitLab Code Quality, GitHub code scanning, or the terminal. actionlint proved the demand for linting shell inside CI config on GitHub Actions; shellscan covers both platforms in one scanner, with platform-aware security rules on top.

Security

Report a vulnerability privately via the security policyob@coroboros.com, never a public issue.

Contributing

Bug reports and merge requests welcome.

  • Open an issue before submitting non-trivial merge requests.
  • Commits follow Conventional Commits.
  • Sign off each commit (DCO): git commit -s.
  • Target the main branch.

Run the unit tests locally:

bash test/unit.sh

Coverage report (requires Ruby + Bundler):

bundle install
bundle exec bashcov --command-name shellscan --mute test/coverage.sh

License

Apache 2.0

About

GitLab mirror of shellscan — finds shell in files, shebangs, GitLab CI, and GitHub Actions.

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages