应用安全扫描实战——SAST、DAST、SCA 在 CI/CD 中的落地
应用安全扫描实战——SAST、DAST、SCA 在 CI/CD 中的落地
适读人群:关注应用安全的开发和 DevSecOps 工程师 | 阅读时长:约20分钟 | 核心价值:把三类安全扫描集成到 CI/CD,让安全问题在代码合并前就被发现
我们有一次因为用了一个老版本的 Log4j,在 Log4Shell 漏洞爆发的时候,被迫紧急停服修复,影响了凌晨的业务。
那个 Log4j 版本其实已经有已知高危漏洞好几个月了,但我们没有自动化的依赖漏洞扫描,完全靠人工关注安全公告,结果漏掉了。
事后我们把 SCA(Software Composition Analysis)工具集成进了所有项目的 CI/CD,现在每次构建都会扫描依赖漏洞。如果出现新的高危漏洞,会在 24 小时内收到告警。
但 SCA 只是安全扫描的三分之一。这篇文章讲的是完整的三类扫描:SAST、DAST、SCA 如何在 CI/CD 里落地。
三类扫描的分工
SAST(Static Application Security Testing,静态应用安全测试):分析源代码,找代码里的安全漏洞(SQL 注入、XSS、硬编码密码等)。不需要运行程序,在编译阶段就能发现问题。
DAST(Dynamic Application Security Testing,动态应用安全测试):向运行中的应用发送攻击请求,测试应用对攻击的抵御能力。需要部署运行的应用。
SCA(Software Composition Analysis,软件成分分析):分析项目依赖的第三方库,检测已知的 CVE 漏洞。
三者互补,每种都有自己的检测盲区:
- SAST 不能发现运行时的逻辑问题
- DAST 不能发现代码层面的静态问题
- SCA 只关注依赖,不关注你自己写的代码
SAST:Semgrep 实战
Semgrep 是我目前最推荐的开源 SAST 工具,规则丰富、支持自定义规则、速度快。
接入 CI/CD
# GitHub Actions
security-sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Semgrep SAST
uses: returntocorp/semgrep-action@v1
with:
config: >-
p/java
p/owasp-top-ten
p/secrets
publishToken: ${{ secrets.SEMGREP_APP_TOKEN }} # 可选,上传到 Semgrep Cloud
# 或者直接用命令行
- name: Run Semgrep
run: |
pip install semgrep
semgrep scan \
--config "p/java" \
--config "p/owasp-top-ten" \
--config "p/secrets" \
--sarif \
--output semgrep-results.sarif \
--error # 发现 HIGH 以上问题就返回非零退出码
- name: Upload SARIF results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: semgrep-results.sarif自定义 Semgrep 规则
Semgrep 的强大之处在于可以写自定义规则。比如,你们团队有一个内部的 SQL 执行方式,Semgrep 不知道如何检测它,你可以自己写规则:
# rules/custom-java-rules.yml
rules:
- id: hardcoded-password-in-config
patterns:
- pattern: |
@Value("${...}")
private String password = "$VALUE";
- pattern-not: |
@Value("${...}")
private String password = "${...}";
message: |
Hardcoded password detected. Use Spring's value injection
from environment variables or configuration server instead.
severity: ERROR
languages: [java]
- id: sql-concatenation
patterns:
- pattern: |
String query = "..." + $VAR + "...";
... $JDBCTEMPLATE.query(query, ...);
message: "Potential SQL injection via string concatenation"
severity: ERROR
languages: [java]
- id: log-sensitive-data
patterns:
- pattern: log.$METHOD(..., ...$ARGS, $SENSITIVE, ...)
- metavariable-regex:
metavariable: $SENSITIVE
regex: (password|token|secret|key|credential)
message: "Potential sensitive data logging"
severity: WARNING
languages: [java]SpotBugs + FindSecBugs:深度字节码分析
Semgrep 分析源代码,SpotBugs 分析字节码,两者的检测侧重点不同,可以互补:
<!-- pom.xml -->
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.8.3.1</version>
<configuration>
<effort>Max</effort>
<threshold>Low</threshold>
<failOnError>true</failOnError>
<includeFilterFile>spotbugs-security-include.xml</includeFilterFile>
<plugins>
<plugin>
<groupId>com.h3xstream.findsecbugs</groupId>
<artifactId>findsecbugs-plugin</artifactId>
<version>1.12.0</version>
</plugin>
</plugins>
</configuration>
</plugin>spotbugs-security-include.xml(只扫描安全相关规则,不扫描代码质量问题,减少噪音):
<FindBugsFilter>
<Match>
<Bug category="SECURITY"/>
</Match>
</FindBugsFilter>SCA:依赖漏洞扫描
OWASP Dependency Check
# GitLab CI
security-sca:
stage: security
image: maven:3.9-eclipse-temurin-17
cache:
key: nvd-cache-$CI_COMMIT_WEEK
paths: [.owasp-nvd/]
script:
- mvn dependency-check:check
-DnvdDatafeedUrl=https://nvd.nist.gov/feeds/json/cve/1.1/
-DdataDirectory=.owasp-nvd/
-DfailBuildOnCVSS=7
-DsuppressionFile=owasp-suppressions.xml
-Dformat=ALL
artifacts:
when: always
paths:
- target/dependency-check-report.html
- target/dependency-check-report.xml
expire_in: 1 weekTrivy:容器镜像 + 依赖双扫描
Trivy 可以同时扫描容器镜像里的 OS 包漏洞和应用依赖漏洞:
security-trivy:
stage: security
image:
name: aquasec/trivy:latest
entrypoint: [""]
variables:
TRIVY_CACHE_DIR: ".trivycache"
cache:
paths: [.trivycache/]
key: trivy-$CI_COMMIT_WEEK # 每周刷新一次漏洞数据库缓存
script:
# 扫描 pom.xml 里的依赖漏洞(不需要构建)
- trivy fs
--security-checks vuln
--exit-code 0
--severity LOW,MEDIUM
--format sarif
--output trivy-deps.sarif
.
# HIGH/CRITICAL 漏洞必须修复
- trivy fs
--security-checks vuln
--exit-code 1
--severity HIGH,CRITICAL
.
# 扫描镜像(如果本 stage 有镜像)
- |
if docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA 2>/dev/null; then
trivy image
--exit-code 1
--severity HIGH,CRITICAL
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
fiDAST:OWASP ZAP 动态扫描
DAST 需要有运行中的应用,通常在 Staging 环境部署后执行:
security-dast:
stage: security-dast # 在 deploy-staging 之后
needs: [deploy-staging]
image:
name: owasp/zap2docker-stable:latest
entrypoint: [""]
variables:
TARGET_URL: "http://staging.company.internal"
script:
# 基线扫描(被动扫描,不发送攻击请求)
- zap-baseline.py
-t $TARGET_URL
-r zap-baseline-report.html
-x zap-baseline-report.xml
-J zap-baseline-report.json
-I # 忽略告警,只做信息收集
|| true # 即使有发现也不失败
# API 扫描(需要 OpenAPI 规格文件)
- zap-api-scan.py
-t $TARGET_URL/v3/api-docs
-f openapi
-r zap-api-report.html
-x zap-api-report.xml
-l WARN # 只报 WARN 以上
artifacts:
when: always
paths:
- zap-*.html
- zap-*.xmlZAP 的局限性:ZAP 会发送大量攻击性请求,可能触发 WAF 或者被误认为攻击,建议在 Staging 环境里专门配置白名单,排除 ZAP 的 IP。同时,有些接口(如批量删除)不适合 ZAP 扫描,需要在 ZAP 配置里排除这些接口。
踩坑实录
踩坑一:安全扫描报告太多让研发不知所措
我们第一次把三类扫描全部跑起来,输出了一份 Excel,里面有 300+ 个安全问题。研发团队看到这份报告,不知道从哪里下手,最后这份报告就被束之高阁了。
正确的做法是分级处理:
# 在 CI 里分两个步骤:一个强制性(高危),一个建议性(中低危)
# 强制性:HIGH/CRITICAL 必须修复,阻塞合并
security-critical:
script:
- trivy fs --severity HIGH,CRITICAL --exit-code 1 .
allow_failure: false
# 建议性:MEDIUM/LOW 记录但不阻塞
security-advisory:
script:
- trivy fs --severity LOW,MEDIUM --exit-code 0 .
allow_failure: true # 不阻塞流水线,但结果会显示踩坑二:Dependency Check 更新 NVD 数据库太慢
OWASP Dependency Check 每次运行需要从 NVD 下载漏洞数据库,这个过程在网络不好的情况下要 10-20 分钟。
解决方案:
- 使用 CI 缓存存储 NVD 数据库(缓存 key 按周更新,不需要每次下载)
- 如果公司有 nexus/artifactory,可以在内网搭建一个 NVD 数据库镜像
- 用 Trivy 替代(Trivy 的漏洞数据库更新机制更友好,可以离线使用)
踩坑三:DAST 扫描时误删了 Staging 数据
ZAP 的 Active Scan 会对所有发现的接口发送攻击请求,其中包括 DELETE 请求。有一次它把 Staging 环境的测试数据全部删除了,测试团队一脸懵逼。
ZAP DAST 的注意事项:
- 绝对不要在 Production 环境跑 Active Scan
- Staging 环境里,要么在 ZAP 配置里排除破坏性操作(DELETE、POST 某些接口)
- 扫描前备份 Staging 数据,或者用只读的数据快照
深度解析:OWASP Top 10 与 Java 应用的关联
OWASP Top 10 是应用安全领域最权威的漏洞分类,每几年更新一次。SAST 工具的规则基本上都是围绕 OWASP Top 10 设计的,理解这 10 类漏洞,能帮助你更好地评估 SAST 扫描结果。
对于 Java 后端工程师,最常见的几类:
A01:2021 – 访问控制失效(Broken Access Control)
这是 2021 年排名第一的漏洞类型。典型场景:接口没有鉴权(忘记加 @PreAuthorize),或者水平越权(用户 A 能访问用户 B 的数据)。
SAST 工具可以检测到缺少访问控制注解的接口,但无法检测业务逻辑上的越权,需要代码审查和测试来覆盖。
A02:2021 – 加密失败(Cryptographic Failures)
使用弱加密算法(MD5、SHA1 用于密码哈希)、硬编码密钥、HTTP 而不是 HTTPS 传输敏感数据。
SAST 工具非常擅长检测这类问题——SpotBugs 的 FindSecBugs 对 Java 加密相关的 API 误用有很全面的规则。
A03:2021 – 注入(Injection)
SQL 注入、命令注入、LDAP 注入……注入类漏洞是最经典的。用字符串拼接构造 SQL 是 Java 代码里最常见的漏洞来源。
// 危险:SQL 注入
String sql = "SELECT * FROM users WHERE name = '" + username + "'";
jdbcTemplate.query(sql, ...);
// 安全:用参数化查询
String sql = "SELECT * FROM users WHERE name = ?";
jdbcTemplate.query(sql, username);SAST 工具对这类模式有很好的检测能力,Semgrep 的 p/java 规则集包含了 SQL 注入的检测。
A07:2021 – 认证和验证失败(Identification and Authentication Failures)
弱密码策略、没有多因素认证、session 管理问题、JWT 使用不当(比如接受算法为 none 的 JWT)。
对于 JWT 的安全使用,SAST 工具可以检测到一些典型的错误用法,但业务层面的认证逻辑需要人工 review。
深度解析:安全扫描结果的分类处理
安全扫描工具会产生大量发现,并不是所有的发现都是真正的漏洞,需要有效的分类机制:
True Positive(真阳性):真正的安全漏洞,需要修复。
False Positive(假阳性):工具误报,实际上不是问题。比如 OWASP Dependency Check 把一个测试依赖(只在编译时存在)报告为运行时漏洞;或者 SpotBugs 检测到一个"硬编码密码",但实际上那是一个无害的占位符字符串。
True Negative(真阴性):没有漏洞,工具也没报告。
False Negative(假阴性):有漏洞,但工具没检测到。这是最危险的,但工具本身做不到保证 100% 覆盖。
处理扫描结果的流程:
- 对每个发现分类:是真阳性还是假阳性?
- 真阳性按严重程度处理:CRITICAL/HIGH 立刻修复,MEDIUM/LOW 放入 backlog
- 假阳性做文档化的抑制(suppression):记录原因、审批人、复查日期
- 定期复查抑制规则:防止"为了通过检查而抑制"成为习惯
维护一个 security-suppressions.xml(OWASP)或 .semgrepignore 文件,记录所有抑制规则,让这些决策有据可查,而不是默默地把漏洞扫描关掉。
完整的安全流水线
把三类扫描整合到一个完整的 CI 安全流水线:
# 分为两个阶段:快速安全(阻塞)和深度安全(不阻塞)
stages:
- build
- test
- security-fast # 代码提交时运行,阻塞合并
- deploy-staging
- security-deep # 部署后运行,不阻塞但发告警
security-sast:
stage: security-fast
script:
- semgrep scan --config "p/java" --config "p/owasp-top-ten" --error
allow_failure: false
security-sca:
stage: security-fast
script:
- trivy fs --severity HIGH,CRITICAL --exit-code 1 .
allow_failure: false
security-container:
stage: security-fast
script:
- trivy image --severity HIGH,CRITICAL --exit-code 1 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
needs: [build-image]
allow_failure: false
security-dast:
stage: security-deep
script:
- zap-baseline.py -t $STAGING_URL -r report.html
needs: [deploy-staging]
allow_failure: true # 不阻塞,但结果上传并发告警
artifacts:
when: always
paths: [report.html]深度解析:安全扫描工具的局限性
无论多么先进的安全扫描工具,都有它无法覆盖的场景。理解这些局限,才能对安全扫描的结果有正确的预期,不过度依赖,也不忽视。
SAST 的假阳性问题
SAST(静态分析)最大的问题是假阳性(False Positive)——工具报告的问题,实际上不是真正的漏洞。典型的例子:SAST 扫描出一个 SQL 注入风险,但实际上代码已经用了参数化查询,根本不可能被注入;工具不理解这个上下文,仍然报告了风险。
假阳性率高的扫描工具会产生严重的"告警疲劳"。工程师一看报告里 200 个问题,大部分是假阳性,就开始不认真对待真正的问题。解决方案是:选择假阳性率低的工具(Semgrep 的规则精确度较高),或者投入时间调优规则(关闭不适合你的代码库的规则)。宁可少报告一些问题,也不要让团队被假阳性淹没。
DAST 的覆盖盲区
DAST(动态测试)只能测试"能从 HTTP 接口到达的功能"。对于内部的业务逻辑漏洞(比如越权访问:A 用户能访问 B 用户的数据),DAST 很难自动发现,因为工具不知道哪些数据应该对哪些用户可见,只能靠手工测试或者人工 code review。
DAST 也不擅长测试认证和授权逻辑的复杂场景——它可以测试"是否有 JWT 签名验证",但测试"这套权限控制逻辑是否完备"需要人来做渗透测试。DAST 工具是"辅助",不是"替代"安全测试工程师。
SCA 的 Reachability 问题
SCA 的一个常见批评是:报了很多漏洞,但很多都在"你根本没有用到的代码路径"里。比如某个日志库有一个 RCE(远程代码执行)漏洞,但这个漏洞只有在使用了某个特定的日志格式化功能时才能触发,而你根本没有用那个功能。
现代的 SCA 工具开始引入 Reachability Analysis——不只报告"依赖有漏洞",还判断"漏洞代码路径是否可达"。这大幅减少了需要处理的问题数量,提高了结果的可操作性。Snyk、GitHub Advanced Security 等工具都开始支持这个功能,在评估 SCA 工具时值得关注。
深度解析:安全扫描的结果处理策略
扫描工具跑完,生成了一份安全报告,然后呢?这才是安全工程的真正挑战——如何系统地处理扫描结果,而不是陷入"发现了问题但不知道怎么处理"的困境。
漏洞分级与处理时限
建立一套清晰的漏洞处理 SLA,是把安全扫描落地的关键:
Critical(CVSS 9.0+):24 小时内必须处理或有明确的缓解方案
High(CVSS 7.0-8.9):7 天内处理
Medium(CVSS 4.0-6.9):30 天内纳入计划
Low(CVSS < 4.0):纳入技术债管理,按季度回顾
这个 SLA 需要和管理层达成共识,并且有配套的例外申请流程——当一个 High 级别的漏洞因为依赖第三方升级进度无法在 7 天内修复时,有正式的记录和风险承担流程,而不是无声无息地超期。
建立漏洞修复的优先级模型
仅仅按 CVSS 分数排优先级是不够的。一个 CVSS 9.0 的漏洞,如果在你的网络边界上根本无法从外部触达,实际风险远低于一个 CVSS 7.0 但直接暴露在公网接口上的漏洞。
实际优先级应该考虑四个维度:漏洞严重性(CVSS)× 漏洞可利用性(有无公开利用代码)× 资产重要性(数据库比测试工具重要)× 暴露面(能从外部触达吗)。这四个因素综合判断,才能得出真正合理的处理优先级。
与研发流程集成,而不是对抗
安全扫描的结果不应该只在安全团队内部消化,要直接进入研发团队的工作流。有几种集成方式:把高危漏洞自动创建为 Jira 工单,分配给对应服务的负责人;在 PR 评论里直接指出新引入的安全问题(SonarQube 和 Semgrep 都支持);在 Sprint 计划里留出安全修复的时间(通常 10-15% 的 Sprint 容量)。
安全工作融入日常研发节奏,比独立的"安全冲刺"更可持续。一次集中的"安全清理 Sprint"往往很难彻底,而且修复质量不高(为了赶工期,fix 可能只是绕过扫描,不是真正修复)。把安全修复变成和 bug 修复同等重要的日常工作,才能持续改善安全状态。
总结
SAST 找代码里的安全缺陷,DAST 测运行时的安全防御,SCA 管依赖库的已知漏洞。三者在 CI/CD 里的最佳实践是:
- SAST + SCA(高危部分):放在代码提交时,阻塞合并,快速反馈
- DAST:放在 Staging 部署后,不阻塞但记录并告警
- SCA(中低危):记录,有专门的 tech debt 处理机制
安全是工程问题,不是"安全团队的事"。把安全扫描变成日常开发流程的一部分,是 DevSecOps 最核心的实践。工具只是手段,让工程师养成"每次提交都关注安全"的意识,才是目的。三类扫描工具协同工作,加上合理的结果处理机制和团队文化,安全才能真正融入到产品开发的每个环节,而不是上线前的最后一道"安全关卡"。安全工程的终极目标不是零漏洞,而是"发现漏洞的成本足够低、修复漏洞的速度足够快"——在攻击者利用漏洞之前,你已经修复了它。扫描工具只是达到这个目标的手段,持续改善的流程和文化才是真正的护城河。真正成熟的 DevSecOps 团队,最终会把安全扫描做到"无感"——工程师提交代码,扫描在后台自动运行,高危问题自动创建工单,整个流程透明但不打断开发节奏。这个状态的实现需要时间和迭代,但每一步的改进都在降低整体的安全风险。
