coroboros/shellscan
GitHub: coroboros/shellscan
一个基于 shellcheck 的容器化 shell 脚本检查工具,支持扫描 .sh 文件、shebang 文件以及嵌入在 GitLab CI 和 GitHub Actions YAML 中的 shell 脚本。
Stars: 0 | Forks: 0

# shellscan
**查找并 lint 项目中的每一个 shell —— `.sh` 文件、shebang 以及嵌入在 GitLab CI 和 GitHub Actions YAML 中的脚本。**
一个基于 Alpine 的镜像封装了 [`shellcheck`](https://github.com/koalaman/shellcheck) 并提供四种发现模式:`.sh` 文件、带有 shell shebang 的文件(`/bin/bash` 和 `env bash` 形式),以及嵌入在 CI YAML 中的 shell 脚本 —— 通过 [`yq`](https://github.com/mikefarah/yq) 提取 GitLab CI 的 `before_script` / `script` / `after_script` 键和 GitHub Actions 的 `run` 步骤,同时展开了 YAML 锚点。文件发现使用 [`fd`](https://github.com/sharkdp/fd)。检查结果以终端输出、[用于合并请求小部件的 GitLab Code Quality JSON,或用于 GitHub 代码扫描的 SARIF 2.1.0](#reports) 格式呈现;嵌入脚本的检查结果会映射回其原始的 YAML 行。可选的[安全规则](#security-rules) 可标记通过管道传递给 shell 的远程代码、输出到作业日志的密钥、未加引号且受攻击者控制的 CI 变量,以及将不受信任的数据注入到 `run` 脚本中的 GitHub 表达式。
[](https://gitlab.com/coroboros/security/infrastructure/shellscan/-/releases)
[](https://gitlab.com/coroboros/security/infrastructure/shellscan/-/pipelines)
[](https://github.com/orgs/coroboros/packages/container/package/shellscan)
[](https://hub.docker.com/r/coroboros/shellscan)
[](LICENSE.md)
[](https://gitlab.com/coroboros/security/infrastructure/shellscan)
[](https://gitlab.com/coroboros/security/infrastructure/shellscan/-/jobs/artifacts/main/file/coverage/index.html?job=test-shell-coverage)
[](https://github.com/coroboros/agent-skills)
[](https://coroboros.com)
## 目录
- [要求](#requirements)
- [镜像](#image)
- [标签](#tags)
- [命令](#commands)
- [运行](#run)
- [报告](#reports)
- [安全规则](#security-rules)
- [代理](#agents)
- [包](#packages)
- [出处](#provenance)
- [与替代方案比较](#compared-to-alternatives)
- [安全](#security)
- [贡献](#contributing)
- [许可证](#license)
## 要求
Docker、BuildKit,或任何能够从 GitHub Container Registry(或 Docker Hub 镜像)拉取的 OCI runtime。
## 镜像
`ghcr.io/coroboros/shellscan:{tag}` —— 镜像同步至 `docker.io/coroboros/shellscan:{tag}`,以及位于 GitLab Container Registry 的 `registry.gitlab.com/coroboros/security/infrastructure/shellscan:{tag}`。
- **架构**: `linux/amd64`, `linux/arm64`
- **工作目录**: `/shellscan`
- **Entrypoint**: `shellscan`
- **用户**: 非特权用户 `shellscan` (uid 10000)
## 标签
| 标签 | 基础镜像 | 架构 | 大小 | 来源 | 可变性 |
| --- | --- | --- | --- | --- | --- |
| `
` | [`koalaman/shellcheck-alpine:v0.11.0`](Dockerfile#L3) | `amd64`, `arm64` | 18.6 MB (amd64), 27.0 MB (arm64) | SemVer git tag | 不可变 |
| `main` | 同 `` | 同 `` | 视构建而定 | 最新的绿色 `main` 构建 | 滚动更新 |
| `` | 同 `` | 同 `` | 视构建而定 | 每次构建 | 不可变 |
大小是来自 GHCR 的压缩层总和。基础镜像的可靠来源是 `Dockerfile` 中的 `FROM` 行。固定 digest 以实现可重现的构建;参见[出处](#provenance)。
## 命令
`shellscan [mode] [fd_options]`
mode
作为第一个位置参数传递的发现模式。
| 模式 | 行为 |
| --- | --- |
| `all` | 默认值。扫描以下所有情况的组合。 |
| `gitlab-ci` | 扫描 `.yml` / `.yaml` 文件中 `before_script` / `script` / `after_script` 键下嵌入的脚本。YAML 锚点会被展开。 |
| `github-actions` | 扫描 `jobs..steps[].run`(workflows)和 `runs.steps[].run`(composite actions)下嵌入在 `.yml` / `.yaml` 文件中的 `run` 脚本。`${{ }}` 表达式在 lint 前会被中和;shellcheck 会以该步骤的 bash/sh 方言运行,并跳过其有效 `shell:` 解析为 pwsh、python、cmd 或 node 的步骤 —— Windows runner 和仅限 windows 的矩阵默认使用 pwsh。[安全规则](#security-rules) 会在每个 `run` 脚本上运行,无论其为何种 shell。YAML 锚点会被展开。 |
| `shebang` | 扫描带有 shell shebang(`sh`, `bash`, `dash`, `ksh`)的文件。 |
| `.sh` | 扫描 `.sh` 文件。 |
| `-h` | 打印帮助并退出。 |
fd_options
`fd` 调用的额外选项,作为一个带引号的参数传递([参考](https://github.com/sharkdp/fd#how-to-use))。选项按空白字符拆分并原样传递 —— `fd` 匹配其自身的 glob 模式,shell 永远不会展开它们。命令执行标志(`-x`, `-X`, `--exec`, `--exec-batch`)会被拒绝。
环境变量
| 变量 | 默认值 | 用途 |
| --- | --- | --- |
| `SHELLCHECK_OPTS` | `--color=always` | 传递给 [`shellcheck`](https://github.com/koalaman/shellcheck/blob/master/shellcheck.1.md#environment-variables) 的选项。 |
| `SHELLSCAN_JOBS` | `1` | 并行扫描 worker。大于 `1` 可加快大规模扫描速度;shellcheck 输出会跨文件交错,但最终计数和退出码会保持正确。 |
| `SHELLSCAN_FORMAT` | `human` | 输出格式:`human`、`codequality` (GitLab Code Quality JSON) 或 `sarif` (SARIF 2.1.0)。机器格式独占 stdout;进度信息移动到 stderr。 |
| `SHELLSCAN_BASELINE` | `.shellscanignore` | 在机器格式中被抑制的检查结果指纹基线文件 —— 每行一个,允许 `#` 注释。 |
| `SHELLSCAN_SECURITY` | `0` | 设为 `1` 以在嵌入 CI YAML —— GitLab CI 和 GitHub Actions 的脚本上启用[安全规则](#security-rules)。 |
退出码
| 代码 | 含义 |
| --- | --- |
| `0` | 扫描成功,无检查结果。 |
| `1` | 报告了检查结果。 |
| `2` | 用法错误、被拒绝的选项或发现失败。失败的发现会中止操作 —— 它永远不会显示为扫描了零个文件的成功扫描。 |
示例
```
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
```
## 运行
GitLab CI
```
check-sh-files:
image:
name: registry.gitlab.com/coroboros/security/infrastructure/shellscan:
entrypoint: [""]
stage: check
variables:
SHELLCHECK_OPTS: >-
--severity=warning
--color=never
script:
- shellscan .sh
```
```
check-ci-yaml-files:
image:
name: registry.gitlab.com/coroboros/security/infrastructure/shellscan:
entrypoint: [""]
stage: check
script:
- shellscan gitlab-ci
```
```
check-files-with-shebang:
image:
name: registry.gitlab.com/coroboros/security/infrastructure/shellscan:
entrypoint: [""]
stage: check
script:
- cd ..
- shellscan shebang '--exclude *.sh'
```
```
parallel-scan:
image:
name: registry.gitlab.com/coroboros/security/infrastructure/shellscan:
entrypoint: [""]
stage: check
variables:
SHELLSCAN_JOBS: "4"
script:
- shellscan all
```
CI/CD 组件
一个 include 即可运行扫描并为合并请求小部件发布 Code Quality 报告。默认情况下,检查结果是非阻塞的;设置 `fail_on_findings: true` 以对流水线进行门控。
```
include:
- component: gitlab.com/coroboros/security/infrastructure/shellscan/shellscan@
inputs:
security: true
```
最后的 `/shellscan` 片段是来自 `templates/shellscan.yml` 的组件名称;GitLab 组件的引用格式为 `//@`。
输入项:`job_name`, `stage`, `mode`, `fd_options`, `security`, `fail_on_findings`, `jobs`, `shellcheck_opts`, `baseline`, `image` —— 参见 [`templates/shellscan.yml`](templates/shellscan.yml)。
GitHub Actions
同一个镜像扫描 GitHub 代码库 —— `github-actions` 模式会 lint workflows 本身,并且 SARIF 报告会汇入代码扫描:
```
jobs:
shellscan:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- uses: actions/checkout@v4
- name: scan
run: |
docker run --rm -v "$PWD:/shellscan" \
-e SHELLSCAN_SECURITY=1 -e SHELLSCAN_FORMAT=sarif \
ghcr.io/coroboros/shellscan: all > shellscan.sarif || [ $? -eq 1 ]
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: shellscan.sarif
```
pre-commit
该 hook 基于固定的代码库 `rev` 构建 —— Docker 是唯一的本地 runtime 依赖。
```
repos:
- repo: https://gitlab.com/coroboros/security/infrastructure/shellscan
rev:
hooks:
- id: shellscan
```
Docker
将项目挂载为 `/shellscan` 卷并运行任意模式:
```
docker run --rm \
-v "$PWD:/shellscan" \
registry.gitlab.com/coroboros/security/infrastructure/shellscan:
```
```
docker run --rm \
-v "$PWD:/shellscan" \
registry.gitlab.com/coroboros/security/infrastructure/shellscan: \
gitlab-ci '--exclude "*.yaml"'
```
## 报告
`SHELLSCAN_FORMAT=codequality` 输出 [GitLab Code Quality JSON](https://docs.gitlab.com/ci/testing/code_quality/);`SHELLSCAN_FORMAT=sarif` 输出用于 GitHub 代码扫描的 [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html)。这两种格式都将报告写入 stdout,并将进度信息移动到 stderr。来自嵌入在 CI YAML 中脚本的检查结果带有 YAML 源代码行及其选择器 —— GitLab CI 的 `before_script` / `script` / `after_script`,GitHub Actions 的 `jobs..steps[].run` —— 因此标注会落在审查者实际阅读的 YAML 行上。
将 Code Quality 报告接入到作业中,检查结果就会显示在合并请求小部件中:
```
shellscan:
image:
name: registry.gitlab.com/coroboros/security/infrastructure/shellscan:
entrypoint: [""]
stage: check
variables:
SHELLSCAN_FORMAT: codequality
script:
- shellscan gitlab-ci > gl-code-quality-report.json || [ $? -eq 1 ]
artifacts:
when: always
reports:
codequality: gl-code-quality-report.json
```
`|| [ $? -eq 1 ]` 保持检查结果处于非阻塞状态,而发现失败(退出码 `2`)仍会导致作业失败。去掉它可以在发现检查结果时对流水线进行门控。
每个检查结果都带有其文件、规则和消息的稳定 SHA-256 指纹。在 `.shellscanignore` 中列出指纹(每行一个,允许 `#` 注释)以抑制已知的检查结果 —— 在不首先修复遗留树中数十年历史的 shell 的情况下采用 shellscan,并让门控仅捕获新内容。
## 安全规则
`SHELLSCAN_SECURITY=1` 添加了针对 CI YAML —— GitLab CI 和 GitHub Actions —— 内部脚本的规则,这是 shellcheck 无法提供意见的注入面。
| 规则 | 严重性 | 标记内容 |
| --- | --- | --- |
| `SHELLSCAN-CURL-PIPE` | critical | 通过管道传递给 shell 的 `curl` 或 `wget` —— 未经验证即执行远程代码。 |
| `SHELLSCAN-EVAL` | major | 对展开的值使用 `eval` —— 动态输入作为代码运行。 |
| `SHELLSCAN-SECRET-ECHO` | major | 对以 secret 命名的变量使用 `echo` / `printf` —— 密钥会出现在作业日志中。 |
| `SHELLSCAN-CI-INJECTION` | major | 未加引号且受攻击者控制的 CI 变量(`CI_COMMIT_MESSAGE`、`CI_MERGE_REQUEST_TITLE`、分支和标签名称) —— 精心构造的提交会将 shell 语法注入到作业中。 |
| `SHELLSCAN-GHA-INJECTION` | critical | `run` 脚本内包含受攻击者控制数据(`github.event.pull_request.title`、`github.head_ref`、提交消息)的 `${{ }}` 表达式 —— 在任何 shell 解析之前被替换,因此引号无济于事;请通过环境变量传递它。 |
每个检查结果都会报告 YAML 源代码行。这些规则仅在提取的脚本上运行 —— `.sh` 和 shebang 文件已经获得了完整的 shellcheck 处理。注入规则是针对特定平台的:`SHELLSCAN-CI-INJECTION` 在 GitLab CI 脚本上触发,`SHELLSCAN-G-INJECTION` 在 GitHub Actions `run` 脚本上触发 —— GitLab 脚本中的字面量 `${{ }}` 是惰性文本,永远不会被标记。
## 代理
shellscan 为编码代理提供了一个代理技能(agent skill)—— 它自己的扫描和分流指南。将其安装到代理中:
```
npx skills add https://gitlab.com/coroboros/security/infrastructure/shellscan
```
或者在不安装的情况下阅读:[`skills/shellscan/SKILL.md`](skills/shellscan/SKILL.md)。
## 包
在所有 shellscan 标签中相同。
| 包 | 用途 |
| --- | --- |
| `shellcheck` | linter —— 由 `koalaman/shellcheck-alpine` 基础镜像提供。 |
| `bash` | shell —— `src/shellscan.sh` 在 bash 上运行。 |
| `fd` | 快速文件发现 —— 用于枚举要扫描的文件。 |
| `yq` | YAML 处理器 —— 从 `before_script` / `script` / `after_script` 和 `run` 键中提取脚本并展开锚点。 |
| `jq` | JSON 处理器 —— 根据 shellcheck `json1` 输出渲染 Code Quality 和 SARIF 报告。 |
| `ca-certificates` | TLS 证书包。 |
## 出处
通过共享的 [`coroboros/ci`](https://gitlab.com/coroboros/ci) `container-images` 模板发布的每个镜像都:
- **多架构** —— BuildKit,`linux/amd64,linux/arm64`;
- **门控** —— 通过 `gitleaks` 获取源代码密钥,通过 Trivy 在已发布的 `:sha` 上检查镜像 CVE,并在 tag 提升前对镜像进行冒烟测试;
- **签名** —— 在不可变的 digest 上进行 cosign 无密钥签名,并带有 **CycloneDX SBOM** 证明。
签名的 digest 发布到 `ghcr.io/coroboros/shellscan` 并在版本标签上镜像到 `docker.io/coroboros/shellscan`。在下游固定 `@sha256` digest 以实现字节可重现的扫描。
cosign 签名绑定到 digest,而不是 tag —— 验证固定的 digest:
```
cosign verify \
--certificate-identity-regexp 'https://gitlab.com/coroboros/security/infrastructure/shellscan//.*' \
--certificate-oidc-issuer https://gitlab.com \
ghcr.io/coroboros/shellscan@sha256:
```
## 与替代方案比较
| 功能 | 直接使用 `shellcheck` | `yamllint` | `pre-commit` + shellcheck | GitLab CI Lint | `super-linter` | `actionlint` | **`shellscan`** |
| --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| Lint `.sh` 文件 | 是 | 否 | 是 | 否 | 是 | 否 | 是 |
| 通过 shebang 检测 Lint 文件 | 否 | 否 | 否 | 否 | 是 | 否 | 是 |
| 提取 GitLab CI YAML 中嵌入的 shell | 否 | 否 | 否 | 否 | 否 | 否 | 是 |
| 提取 GitHub Actions YAML 中嵌入的 shell | 否 | 否 | 否 | 否 | 通过 `actionlint` | 是 | 是 |
| 扫描前展开 YAML 锚点 | 否 | 否 | 否 | 否 | 否 | 是 (workflows) | 是 |
| 单个二进制文件 / 无需编排设置 | 是 | 是 | 否 | 是 | 否 | 是 | 是 (镜像) |
| 可配置的文件发现(`fd` 选项) | 否 | 否 | 否 | 否 | 否 | 否 | 是 |
| 并行扫描(可配置 worker) | 否 | 否 | 否 | 否 | 否 | 自动 | 是 |
| GitLab Code Quality + SARIF 输出 | 否 | 否 | 否 | 否 | 否 | 通过模板输出 SARIF | 是 |
| 针对嵌入 CI 的 shell 的安全规则 | 否 | 否 | 否 | 否 | 否 | GH 不受信任的输入 | 是 |
| 用于增量采用的基线文件 | 否 | 否 | 否 | 否 | 否 | 否 | 是 |
独特之处在于:shellscan 能找到 shell 在项目中的**任何藏身之处** —— 包括隐藏在 CI YAML 中经常被忽视的脚本 —— 在展开 YAML 锚点的情况下通过标准的 `shellcheck` linter 运行它,并在审查者查看的地方报告检查结果:GitLab Code Quality、GitHub 代码扫描或终端。[`actionlint`](https://github.com/rhysd/actionlint) 证明了对 GitHub Actions 上的 CI 配置内的 shell 进行 lint 的需求;shellscan 在一个扫描器中涵盖了两个平台,并在顶层添加了具备平台感知能力的安全规则。
## 安全
通过[安全策略](SECURITY.md) 私下报告漏洞 —— **ob@coroboros.com**,请勿使用公开 issue。
## 贡献
欢迎提交 Bug 报告和合并请求。
- 在提交非简单的合并请求之前,请先创建一个 issue。
- 提交遵循 [Conventional Commits](https://www.conventionalcommits.org/)。
- 签署每个提交 (DCO):`git commit -s`。
- 目标分支为 `main`。
在本地运行单元测试:
```
bash test/unit.sh
```
覆盖率报告(需要 Ruby + Bundler):
```
bundle install
bundle exec bashcov --command-name shellscan --mute test/coverage.sh
```
## 许可证
[Apache 2.0](LICENSE.md)标签:Cutter, DevSecOps, Shell, 上游代理, 自动化检查, 请求拦截, 错误基检测, 静态代码分析