denimoll/appsec-compose
GitHub: denimoll/appsec-compose
一个轻量级、无状态的开源应用安全编排工具,通过一条命令整合多种开源扫描引擎对代码仓库和容器镜像进行安全扫描与去重报告。
Stars: 0 | Forks: 0
# appsec-compose
只需一条命令,即可对任何代码仓库进行开源应用安全(OSS AppSec)静态扫描。它通过久经考验的开源引擎运行 **SAST、SCA、密钥扫描和 IaC** 检查,随后将结果收集为原生报告,并附带人类可读的摘要和 CycloneDX SBOM —— 方便您自己查阅或上传至 ASPM/ASOC(如 DefectDojo、DependencyTrack 等)。
它是一个**轻量级、无状态的编排器**:没有数据库,没有 Web UI,也没有 worker。
每个扫描器都作为一次性的 Docker Compose 服务运行;一个小型 Python 收集器会汇总报告,并生成对 CI 有意义的退出码。
后续计划(多项目配置、任务调度、极简 UI、ASPM 推送)已在 [ROADMAP.md](ROADMAP.md) 中追踪记录。
## 引擎
| 类别 | 工具 | 默认开启 | 输出 |
|---|---|---|---|
| SAST | [Semgrep](https://semgrep.dev) | 开启 | `semgrep.sarif` |
| SCA | [Trivy](https://trivy.dev) | 开启 | `trivy-fs.sarif` |
| SCA (备选) | [Grype](https://github.com/anchore/grype) | 关闭 | `grype.sarif` |
| SCA (备选) | [OSV-Scanner](https://github.com/google/osv-scanner) | 关闭 | `osv.sarif` (需要网络) |
| IaC | [Trivy config](https://trivy.dev) | 开启 | `trivy-config.sarif` |
| IaC | [Checkov](https://www.checkov.io) | 开启 | `checkov.sarif` |
| IaC (备选) | [Hadolint](https://github.com/hadolint/hadolint) | 关闭 | `hadolint.sarif` (针对 Dockerfile) |
| 密钥 | [Gitleaks](https://github.com/gitleaks/gitleaks) | 开启 | `gitleaks.sarif` |
| 密钥 (备选) | [TruffleHog](https://github.com/trufflesecurity/trufflehog) | 关闭 | `trufflehog.json` + `trufflehog.sarif`\* |
| SBOM | [Trivy](https://trivy.dev) | 开启 | `sbom.trivy.cdx.json` |
| SBOM (备选) | [Syft](https://github.com/anchore/syft) | 关闭 | `sbom.syft.cdx.json` |
覆盖同一类别的工具是**互补**的 —— 您可以启用一个或多个;每个启用的工具都会输出其各自的原生报告,并且所有报告都会被汇总。
\* TruffleHog 没有原生的 SARIF 输出,因此收集器会保留其原始 JSON **并**据此生成一份 SARIF,这样仅支持 SARIF 的 ASPM 工具也能对其进行提取。我们的 `findings.json` 规范化是内部的(用于生成摘要);您上传到 ASPM 的应当是各个工具的原生报告。
## 用法
```
./run.sh /path/to/your/repo
# 默认在 high+ findings 上失败(exit 1);可在每次运行时覆盖:
./run.sh /path/to/your/repo --fail-on critical
# 扫描容器镜像而非 repo(Trivy / Grype / Syft):
./run.sh --image nginx:1.27
# 私有镜像(或在 env 中设置 REGISTRY_USER / REGISTRY_PASS):
./run.sh --image ghcr.io/me/app:1.0 --registry-user me --registry-pass "$TOKEN"
# 从 Dockerfile 构建并扫描结果:
./run.sh --build ./path/to/context [--dockerfile Dockerfile.prod]
```
## 配置 —— 单个文件
所有配置都保存在 [`scan-config.yml`](scan-config.yml) 中:包括运行哪些检查、锁定的**版本**(在此处更新版本号以进行升级)、策略门禁以及离线开关。`run.sh` 会将其渲染到 `.env`(由 Compose 加载)中,并且只运行已启用的扫描器。
```
scanners:
semgrep: { enabled: true, version: "1.97.0" } # SAST
trivy: { enabled: true, version: "0.58.0" } # SCA + IaC + SBOM
gitleaks: { enabled: true, version: "v8.21.2" } # secrets
checkov: { enabled: true, version: "3.2.334" } # IaC
sbom: true # SBOM via Trivy
sbom_formats: [cyclonedx] # any of: cyclonedx, spdx
fail_on: high # critical|high|medium|low|none (none = report-only)
unknown_severity: medium
offline: false # use ./preload.sh cache, no network during scan
```
设置 `enabled: false` 即可禁用某项检查 —— 该扫描器将不会运行,且收集器也不会预期获取它的报告。修改工具的 `version` 字段即可实现升级。
设置 `secrets_history: true` 可让 Gitleaks 扫描完整的 **git 提交历史**(要求仓库中存在 `.git` 目录),而不仅仅是扫描工作区。
## 离线 / 物理隔离
首先在**有**网络的环境下运行一次,将数据库和规则集快照抓取到 `./cache/` 中:
```
./preload.sh # Semgrep ruleset + Trivy vulnerability DB
```
然后在 `scan-config.yml` 中设置 `offline: true`。现在扫描将在没有任何网络访问的情况下运行(Semgrep 使用缓存的规则集;Trivy 使用缓存的 DB 并配合 `--skip-db-update --offline-scan` 运行)。Gitleaks 和 Checkov 将其规则打包在镜像中,因此不需要预加载。
## 输出 (`./reports/`)
```
reports/
├── native/ # raw per-tool reports (SARIF) — feed these to ASPM
│ ├── semgrep.sarif
│ ├── trivy-fs.sarif
│ ├── trivy-config.sarif
│ ├── gitleaks.sarif
│ ├── checkov.sarif
│ ├── grype.sarif # + grype.json (carries CVE/GHSA aliases for dedup)
│ └── trufflehog.json # + generated trufflehog.sarif
├── findings.json # consolidated, de-duplicated findings + counts
├── sbom.trivy.cdx.json # CycloneDX SBOM (Trivy; .spdx.json if enabled)
├── sbom.syft.cdx.json # Syft SBOM (CycloneDX + SPDX, if enabled)
├── summary.md # human summary
└── summary.html # human summary (styled)
```
`summary.html` 会显示严重程度卡片/条形图、按工具划分的明细以及一个漏洞发现表格,包含**类别筛选器**、可展开的**详情**(每个漏洞的描述和公告链接),并且 —— 在基线模式下 —— 会在自上次接受的快照以来新增的漏洞上标记 **NEW** 标志。它是完全独立的(不依赖任何外部资源)。
## 策略 / 退出码
**收集器**决定通过或失败:如果任何漏洞发现的严重程度达到或超过 `fail_on`,则退出码为 `1`,否则为 `0`。扫描器本身始终以 `0` 退出,因此嘈杂的漏洞发现绝不会中断扫描阶段 —— 只有策略门禁会。`run.sh` 会传递收集器的退出码,这使得它能安全地嵌入到 CI 门禁中。
## CI/CD
包含了开箱即用的模板:
- **GitHub Actions** — [`.github/workflows/appsec-scan.yml`](.github/workflows/appsec-scan.yml)
运行扫描,将每个 SARIF 上传到 **code scanning**,将 `reports/` 作为构件保留,缓存漏洞数据库,并在违反策略时使任务失败。
- **GitLab CI** — [`.gitlab-ci.yml`](.gitlab-ci.yml) 运行扫描,将 CycloneDX SBOM 暴露给 GitLab Dependency Scanning,并缓存数据库。(需要带有宿主机 Docker socket 的 runner —— 请参阅文件头部说明。)
- **发布收集器镜像** — [`.github/workflows/publish-collector.yml`](.github/workflows/publish-collector.yml)
在发布时将收集器推送到 GHCR,这样 CI 就可以跳过本地构建。
## 去重
当多个工具覆盖同一类别时,它们会报告相同的问题。在 `dedup: true`(默认)的情况下,收集器会在 `findings.json` 和摘要中合并重复项(原始的原生报告保持不变,以便导入到 ASPM):
- **密钥** — 相同的 `(file, line)` 被视为同一个密钥,与具体工具无关。
- **SCA** — 相同的 `package@version` + 文件,根据重叠的漏洞 ID 进行聚类。
Trivy 的 `CVE-2018-1000656` 和 Grype 的 `GHSA-562c-5r94-xh97` 会合并,因为 Grype 的 `relatedVulnerabilities` 列出了该 CVE —— 因此 CVE/GHSA 的别名会被折叠合并。
- **SAST/IaC** — 在相同的 `(file, line)` 处触发的相同规则。
合并后的漏洞发现会保留最高的严重程度,记录报告该漏洞的**每一个工具**,并将等效的 ID 列为别名。`findings.json` 会报告 `raw_findings`、`unique_findings` 和 `duplicates_removed`。各工具的计数依然反映了每个工具真实的原始检出量。
## 抑制
通过在 `scan-config.yml` 中配置 `ignore:` 列表,将已知/接受的漏洞发现从门禁和计数中剔除。只有当条目中包含的所有字段都匹配时,该条目才会匹配成功;`rule` 是针对漏洞 ID **及其别名**进行全局匹配(glob)的(因此,单个 CVE 条目也能捕获等效的 GHSA):
```
ignore:
- rule: CVE-2018-1000656
reason: "no fix available, mitigated at the proxy"
- file: "tests/**"
reason: "test fixtures"
- rule: "generic.*"
category: secrets
reason: "false positives in sample data"
```
被抑制的漏洞发现会从 `fail_on` 中排除,但会记录在 `findings.json` 中的 `suppressed` 字段下(附带原因),以供审计追踪。
## 基线(仅针对新发现的门禁)
接受当前的漏洞发现,此后仅针对**新**发现执行失败拦截:
```
# 1. 在 scan-config.yml 中设置 `baseline: true`,然后对已接受的状态进行 snapshot:
./run.sh /path/to/repo --update-baseline # writes appsec-baseline.json
# 2. 提交 appsec-baseline.json;后续运行仅针对新发现的 findings 进行 gate:
./run.sh /path/to/repo
```
每个漏洞发现都会获得一个稳定的指纹 `(category, id, file, package, line)`。
运行时会报告 `new` / `known` / `fixed` 的计数(显示在控制台、摘要和 `findings.json` 中),并且 `fail_on` 仅适用于**新**漏洞发现。重新运行 `--update-baseline` 即可重新接受当前状态。
## 扫描容器镜像
`./run.sh --image ` 扫描容器镜像而不是代码仓库。只有支持镜像的工具会运行 —— **Trivy** 和 **Grype**(操作系统 + 语言包漏洞)以及 **Syft**(SBOM);仅限源代码的扫描器(Semgrep、Checkov、Gitleaks、Hadolint)将被跳过。去重、抑制、基线、SBOM 格式以及策略门禁的工作方式均相同。
- **私有镜像仓库** — 传入 `--registry-user/--registry-pass`(或设置
`REGISTRY_USER`/`REGISTRY_PASS`)。扫描器使用生成的 docker auth 配置自行拉取镜像;不需要挂载 Docker socket。
- **构建 & 扫描** — `./run.sh --build [--dockerfile ]` 使用宿主机的 Docker 构建镜像,将其导出为 tar 包(`docker save`)并扫描该归档文件 —— 这样就可以在扫描器容器中没有镜像仓库或 Docker socket 的情况下扫描本地构建的镜像。
## 可重现 / 锁定镜像
默认情况下,扫描器从锁定的 **tag**(版本号在 `scan-config.yml` 中定义)运行。为了实现防篡改、完全可重现的运行,请将它们锁定到不可变的 digest:
```
./pin.sh # resolves each repo:tag -> repo@sha256:... into image-digests.lock
```
提交 `image-digests.lock`;随后 `render-env.py` 将按 digest 运行每个扫描器。
在升级版本号后请重新运行 `./pin.sh`(如果升级了版本号但没有匹配的锁定条目,系统会安全地回退到其 tag)。删除该锁定文件即可恢复使用 tag。
如果要手动锁定**单个**工具,请在其 `version` 字段中填入 digest,而不是 tag —— `version: "sha256:abc..."` 会解析为 `repo@sha256:abc...`。
## 工作原理
1. `run.sh` 将 `scan-config.yml` 渲染到 `.env` 中,并将目标代码仓库以**只读**方式绑定挂载到 `/code`。
2. 已启用的扫描器服务运行一次(并行执行),每个服务将各自的原生 SARIF 报告写入共享的 `reports/` 数据卷中,并退出(退出码为 `0`)。
3. `collector` 将每个 SARIF 规范化为统一模型,写入 `findings.json` 和摘要,并以策略退出码退出。
无需 `docker.sock`,也不需要 docker-in-docker。
## 目录结构
```
docker-compose.yml # scanners + collector (versions/flags from .env)
run.sh # entrypoint: render config, run scanners + collector
preload.sh # offline cache populator
scan-config.yml # the single file you edit
scripts/render-env.py # scan-config.yml -> .env
orchestrator/ # Python collector (config, normalize, policy, summary)
```
标签:AI应用开发, Docker Compose, FTP漏洞扫描, SBOM, 安全编排, 版权保护, 硬件无关, 逆向工具, 静态代码扫描