
简介Fortify SCA 20.1.1 代码审计工具安装包面向开发者、安全团队及代码审计学习者用于在编码与测试阶段静态扫描源码提前发现 SQL 注入、XSS、缓冲区溢出和权限配置等安全隐患。压缩包共 41 个文件约 986.97MB主要由 34 个 bin 规则与核心组件、2 个 txt 说明文件以及 xml 元数据、properties 配置、exe 安装程序、jar 组件和 license 授权文件构成覆盖 Java、C#、C、Python、JavaScript 等多种语言的审计规则。已有 922 人学习下载适合用来搭建本地静态分析环境、对照内置规则理解漏洞类别并借助报告与 IDE、版本控制及 CI/CD 集成思路梳理从扫描到修复的代码安全审查流程。1. 手上这份 Fortify SCA 20.1.1 镜像先弄清它在链路里的位置上个月接手一个跑了七年的订单系统甲方安全评审只提一条硬要求上线前交一份静态代码审计报告。代码审计工具 Fortify SCA 20.1.1 的下载包就是这时候翻出来的——它不是那种拿正则扫全家的轻量检查器而是先把源码翻译成中间模型、再跑数据流和语义分析的重型工具最终产出一个.fpr文件。它解决的是“人工看不完几十万行代码”这个问题适合做交付验收、DevSecOps 流水线门禁以及接手遗留系统时先摸一遍底的人。下面按我实际拆包的顺序讲链路、参数、结果、坑。2. 扫描链路拆开看translate、scan、audit 三段各干什么很多人第一次用 Fortify 会觉得它“慢得没道理”扫一个十万行的 Java 工程要四十分钟。慢的根源不在扫描在于它把活分成了三段每段的输入输出都不一样。搞不清这三段的边界后面所有参数调整都是瞎调。2.1 为什么它一定要先 translate 而不是直接扫Fortify SCA 的核心不是模式匹配。translate 阶段会把每个源文件解析成一套中间表示行内叫 NST——一棵带类型信息、调用关系和符号表的规范化语法树产物落在用户目录下的 build 目录里按 build id 分文件夹。scan 阶段才在这棵树上跑五类分析器数据流污点从 source 追到 sink、控制流、语义、结构、配置。这个设计带来一个很硬的结果翻译阶段漏掉的文件、解析不出来的类型扫描阶段一定查不到而且它不会报错只会安静地少一条污点传播链。我见过最典型的翻车是 Java 项目 classpath 少给了一个 jar跨模块的request.getParameter到executeQuery的连接断在中间报告干干净净评审一问就露馅。所以调 Fortify 的正确姿势是先把 translate 做全看 build 目录里的产物体量对不对再去动扫描参数。反过来先调-Xmx、调分析器开关基本是给自己找心理安慰。2.2 20.1.1 的环境依赖与版本对应装之前先把这几个数对一遍省得装完发现跑不动再回滚项目建议值说明运行 sourceanalyzer 的 JDK8 或 11安装包自带 JRE手改JAVA_HOME反而容易启动失败被分析 Java 代码的 source level按项目实际一般到 11超出支持范围会退化成“解析成功但类型不全”分析机内存≥16 GB-Xmx给 68 GB堆不够的表现是分析到一半直接 OOM没有中间态磁盘预留 20 GBbuild 目录 FPR 中间产物大工程能吃掉几个 G字符集显式-encoding UTF-8中文注释多的项目不设这一项解析报错会莫名其妙授权文件放在安装根目录用sourceanalyzer -version验证无效会直接提示表里“被分析代码的 source level”这一行最容易被忽略。它和运行工具的 JDK 是两回事前者决定你的语法树能不能完整建起来后者只决定工具自己能不能启动。用 JDK 8 跑工具去分析 Java 11 的代码是可行的反过来把-source写小了var、文本块这类语法直接解析失败。2.3 装完先跑一次最小样例把四段链路验通正式扫业务代码之前一定先拿一个三十行的小文件把链路走通。这一步省不得因为 license、规则包、翻译、扫描任何一段出问题在真实工程上的表现都是“扫完了但结果是空的”你很难判断是哪一环。# 1) 解压后设置环境变量版本号目录按实际解压结果替换 export FORTIFY_HOME/opt/Fortify/Fortify_SCA_and_Apps_20.1.1 export PATH$FORTIFY_HOME/bin:$PATH # 2) 验证可执行与授权状态license 无效在这里就会报出来 sourceanalyzer -version # 3) 造一个必然命中的样例字符串拼接 SQL mkdir -p /tmp/sca-demo/src cat /tmp/sca-demo/src/UserDao.java EOF public class UserDao { public void find(java.sql.Connection conn, String name) throws Exception { // 故意拼接 SQL用来验证数据流分析器是否正常工作 String sql select * from users where name name ; conn.createStatement().executeQuery(sql); } } EOF cd /tmp/sca-demo sourceanalyzer -b demo -clean sourceanalyzer -b demo -encoding UTF-8 src/UserDao.java sourceanalyzer -b demo -scan -f demo.fpr ls -lh demo.fpr-b是 build id一个工程一个所有阶段靠它串起来-clean清掉同名 build id 的历史产物不清会带着上一轮的翻译结果一起扫translate 阶段只接源文件路径不产生 FPR-scan才是真正调分析器-f指定输出文件名。跑完拿 Audit Workbench 打开demo.fpr能看到一条 SQL Injection说明 license、规则包、翻译、扫描四段全通。这条链验不通就先去查授权和规则包别急着扫业务代码——这是最省时间的后悔药。3. sourceanalyzer 命令行实战按语言配翻译参数真正耗时间的不是敲命令是想清楚每个参数为什么这么给。这一章按语言拆Java 和 C/C 的差异大到几乎是两个工具。3.1 build id 的生命周期与清理时机build id 是个很容易被轻视的东西。它对应的目录在~/.fortify/sca/版本/build/build-id里面存的是翻译产物。日常有三个动作要固定下来# 每轮扫描前清空避免上一轮的残留文件被重复计入 sourceanalyzer -b order-svc -clean # 翻译完成后看一眼产物体量几百 KB 基本说明只翻译进去了很少的文件 du -sh ~/.fortify/sca/20.1.1/build/order-svc # 工程彻底不扫了连目录一起删不然磁盘会慢慢被吃满 rm -rf ~/.fortify/sca/20.1.1/build/order-svc第二步那个du是我必做的一步。正常情况下一个中型 Java 工程的翻译产物在几十到几百 MB如果只有几百 KB说明find出来的文件列表是空的或者-exclude写得太狠把所有东西都排掉了。这个信号比看日志快得多。另外 CI 上跑的时候多个流水线并发共用一个 build id 会互相覆盖产物表现是扫描结果随机缺失。稳妥做法是把 build id 拼上流水线号或分支名。3.2 Java 工程classpath、-source、-encoding 三个参数决定召回率Java 是 Fortify 支持最成熟的场景也是最容易配错的。下面这段是我现在的标准模板cd /data/src/order-svc # 1) 收集依赖 jar。classpath 缺一个包就可能断掉一条跨模块的污点传播链 CP$(find . -name *.jar -path */lib/* | tr \n :) # 2) 清空历史产物 sourceanalyzer -b order-svc -clean # 3) 翻译显式声明源级别、编码并排除测试目录 sourceanalyzer -b order-svc \ -source 11 \ -encoding UTF-8 \ -cp $CP \ -exclude **/src/test/** \ $(find src/main/java -name *.java) # 4) 扫描并输出 FPR sourceanalyzer -b order-svc -scan -f /data/report/order-svc.fpr -Xmx8G-source 11是给解析器看的语法级别写小了遇到新语法会整文件解析失败-encoding UTF-8影响中文注释和字符串常量不写就跟随系统 locale容器镜像里默认常常是 POSIX-cp接的是被分析代码的依赖不是工具自己的-exclude用通配符排除路径测试代码里的断言和 mock 常常制造大量误报排掉能省一轮人工审计。-Xmx8G放在 scan 阶段是给分析器 JVM 的堆上限。这个值不是越大越好超过物理内存会触发换页反而慢得离谱。我的经验值是物理内存的一半且不超过 8G。还有一点$(find ...)在文件数量过万时会撞上命令行长度上限。工程大了就改用-filelist把路径写进一个文本文件再传进去。3.3 C/C 与脚本语言build 包装比手写参数可靠C/C 项目不要手写 include 路径直接用 build 包装。原理是把编译命令套一层工具从真实的编译过程里抓头文件搜索路径和宏定义这比你自己猜-I准确得多。cd /data/src/native-core sourceanalyzer -b native-core -clean # 把 make 整体包进来编译命令里出现的 -I / -D 都会被抓到 sourceanalyzer -b native-core make clean all -j4 # 只翻译不扫描时产物同样落在 build 目录里可以先看体量再决定要不要扫 du -sh ~/.fortify/sca/20.1.1/build/native-core sourceanalyzer -b native-core -scan -f /data/report/native-core.fpr -Xmx8G脚本语言Python、JavaScript、Go 之类走的是自动识别直接给目录就行写法简单但坑在版本解释器版本对不上动态类型的推断会大面积退化。# 脚本类项目给目录用 -exclude 剔掉虚拟环境和第三方依赖 sourceanalyzer -b web-api -clean sourceanalyzer -b web-api \ -encoding UTF-8 \ -exclude **/venv/** \ -exclude **/node_modules/** \ ./src sourceanalyzer -b web-api -scan -f /data/report/web-api.fpr -Xmx6Gnode_modules和venv一定要排不排的话扫描时间能翻三倍而且报出来的问题九成在第三方库里修不了也不该你修。真要查依赖走软件成分分析那条线别混在静态代码扫描里。3.4 扫描阶段的规则包与并发控制规则包决定能查出哪些类型的缺陷。默认加载全部核心规则代价是时间长、误报多。按项目技术栈裁剪是常规操作# -rules 指定规则目录或规则文件只留 Java 相关的核心规则 sourceanalyzer -b order-svc -scan \ -f /data/report/order-svc.fpr \ -Xmx8G \ -rules $FORTIFY_HOME/Core/config/rules/java.rules # 规则包需要更新时用 fortifyupdate离线环境则把规则包文件拷进对应目录 fortifyupdate -acceptKey-rules后面跟的路径在不同小版本里目录结构略有差异拿不准就先ls $FORTIFY_HOME/Core/config/rules看一眼再写别照抄网上的路径。fortifyupdate需要能连到更新源隔离环境里跑不了只能走离线规则包替换替换完记得用 2.3 那个小样例复验一遍确认规则确实生效了。并发方面一台机器上同时跑多个-scan不会自动限流各自按-Xmx抢内存最后集体 OOM。我一般用串行队列或者用taskset绑核后错开启动。4. 结果侧从 FPR 里挑出真问题接进工单FPR 这个格式第一次接触会有点懵——双击打不开用文本编辑器打开是乱码。它其实就是个 zip。4.1 FPR 是个 zipaudit.fvdl 才是数据源# 看一眼内部结构核心数据都在 audit.fvdl 这个 XML 里 unzip -l /data/report/order-svc.fpr | head -20里面通常还有源码快照、规则信息、扫描日志。audit.fvdl记录每一条 issue 的规则 ID、类别、文件、行号、严重级别还有分析器给出的数据流路径。想在流水线里做自动化门禁就得解析它——Audit Workbench 适合人工审不适合批量。4.2 用 Python 把 FPR 拆成 CSV 接进工单系统下面这段我用了两年多字段名在不同小版本间有出入所以做了命名空间剥离和容错import csv import sys import zipfile import xml.etree.ElementTree as ET def strip_ns(tag): 去掉 XML 命名空间前缀不同版本前缀不一样不去掉匹配不上 return tag.split(})[-1] def parse_fpr(fpr_path, out_csv): with zipfile.ZipFile(fpr_path) as z: with z.open(audit.fvdl) as f: tree ET.parse(f) rows [] for vuln in tree.iter(): if strip_ns(vuln.tag) ! Vulnerability: continue rec {rule: , category: , file: , line: , severity: } for node in vuln.iter(): name strip_ns(node.tag) text (node.text or ).strip() if name RuleID and text and not rec[rule]: rec[rule] text elif name Type and text and not rec[category]: rec[category] text elif name FileName and text and not rec[file]: rec[file] text # 只取第一个实例多实例需展开 InstanceInfo elif name LineNumber and text and not rec[line]: rec[line] text elif name in (DefaultSeverity, Severity) and text and not rec[severity]: rec[severity] text if rec[rule] or rec[category]: rows.append(rec) with open(out_csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[rule, category, file, line, severity]) writer.writeheader() writer.writerows(rows) print(f{len(rows)} issues - {out_csv}) if __name__ __main__: parse_fpr(sys.argv[1], sys.argv[2])用法是python fpr2csv.py order-svc.fpr issues.csv。几个关键点说清楚strip_ns必须做Fortify 不同版本用的 XML 命名空间前缀变过硬编码{ns}Vulnerability换版本就崩一个Vulnerability节点下可能有多个InstanceInfo这里只取了第一个文件名和行号如果一个规则命中多处要改成在外层循环里再遍历实例输出用utf-8-sig是为了让 Excel 直接打开不乱码这一点在很多团队里能省掉一轮沟通。拿到 CSV 之后按rule聚合成各团队的问题分布按file的前缀映射到代码库负责人就是一张能直接派单的表。4.3 上传平台做基线比对与误报抑制单机扫出来的 FPR 只能看单次快照真正有价值的是“这次比上次多了哪些新问题”。这个能力要靠平台端做版本比对# 上传 FPR 到平台project / version 用分支和提交号方便回溯 fortifyclient -url $SSC_URL -authtoken $SSC_TOKEN \ uploadFPR -f /data/report/order-svc.fpr \ -project order-svc \ -version $GIT_COMMIT-project保持稳定-version换成提交号或构建号平台就能自动算出新增、已修、遗留三类。门禁只看新增存量走单独的治理计划——这是让评审能过的关键一上来就要求“零高危”在遗留系统上不可能实现。误报抑制在 Audit Workbench 里做对确认是误报的 issue 打 Not an Issue导出成 filter 文件下一轮扫描时带上。不用把过滤规则想得太复杂把审计结论沉淀下来这件事本身就是价值。4.4 严重级别与修复优先级对照级别典型问题类型处理策略CriticalSQL 注入、命令执行、反序列化阻塞发布当天定位High路径穿越、SSRF、硬编码凭据当版本内修复MediumXSS、日志注入、权限校验缺失排期修复可带条件上线Low信息泄露、异常栈外抛批量处理不单独排期这张表的价值不在级别本身而在于让业务方接受“分级放行”。全按最高级别卡项目会绕过你直接上线全放过门禁形同虚设。5. 常见问题排查装完跑不起来的五类翻车现场5.1 扫描跑完FPR 里一条问题都没有现象命令全部返回 0FPR 也生成了打开一看是空的。原因多半是翻译阶段没进去文件。find的结果为空、-exclude写成了**/*、工作目录不对都可能导致。也可能规则包路径给错-rules指向了不存在的文件而工具不报错。解决先du -sh ~/.fortify/sca/20.1.1/build/id看产物体量只有几百 KB 就是翻译没进去再把find src/main/java -name *.java | wc -l的输出和文件数对一遍最后去掉-rules用默认规则重跑一次排除规则包问题。5.2 分析到一半直接 OutOfMemoryError现象scan 阶段跑了十几分钟日志突然中断抛 OOM。原因-Xmx给小了或者工程里有自动生成的大文件协议代码、模板生成的 Java把单个文件撑到几万行。解决-Xmx提到物理内存的一半先试同时把生成代码目录用-exclude排掉这类文件本身不承载业务逻辑扫出来也没人修。还不行就按模块拆成多个 build id 分批扫最后合并结果。5.3 中文注释导致解析报错日志里一堆编码异常现象translate 阶段报解析错误行号指向的是中文注释行。原因没指定-encoding工具跟随系统 locale容器镜像里LANG常常没设默认落到 POSIX。解决命令里显式加-encoding UTF-8同时在容器里把LANG设成C.UTF-8。顺带检查源文件里有没有 GBK 编码的历史文件混编码的项目在解析时会零星失败很难一眼看出来。5.4 报告里几百条高危评审直接被打回现象第一次扫描出了四百多条安全团队要求全部修复项目组认为根本修不完。原因没有基线。全量结果里包含大量测试代码、框架样板代码和历史遗留问题。解决先做一轮人工审计把确认误报的打 Not an Issue 并导出 filter再用平台做版本比对门禁只卡新增问题存量问题按模块拆成治理任务给一个季度窗口。这三步做完报告数量通常能降到两位数。5.5 CI 上并发扫描结果随机缺失现象同一份代码本地扫得全流水线上时好时坏。原因多个流水线共用了一个 build id翻译产物目录互相覆盖。解决build id 拼上分支名或流水线号跑完在post阶段-clean。另外注意 CI 的 workspace 是临时的用户目录下的 build 目录如果没挂缓存每次都是全新翻译时间成本会明显上升可以只缓存依赖不变的部分。6. 把它塞进流水线门禁收敛靠的是基线不是规则单机跑通只是开始。真要落地扫描脚本得能无人值守地跑结果得能自动判断红绿。下面这个脚本是我们现在用的骨架去掉业务相关的部分#!/usr/bin/env bash set -euo pipefail BUILD_ID${BUILD_ID:-order-svc}-${CI_PIPELINE_ID:-local} FPR/data/report/${BUILD_ID}.fpr sourceanalyzer -b $BUILD_ID -clean sourceanalyzer -b $BUILD_ID \ -encoding UTF-8 \ -cp $(cat /data/report/cp.txt) \ -exclude **/src/test/** \ $(find src/main/java -name *.java) sourceanalyzer -b $BUILD_ID -scan -f $FPR -Xmx6G # 上传平台做基线比对流水线只对新增问题置红 fortifyclient -url $SSC_URL -authtoken $SSC_TOKEN \ uploadFPR -f $FPR -project order-svc -version $GIT_COMMITBUILD_ID拼流水线号解决并发覆盖cp.txt是提前算好的依赖列表避免在 CI 上重复find上万个 jar最后一步上传之后红绿判定交给平台的策略脚本本身不写阈值判断——阈值写死在脚本里后面每改一次都要发一次版本成本太高。一个具体技巧把-exclude的清单抽成单独文件用-exclude多次传参从文件里读代码库新增模块时只改配置不改脚本。我们靠这个把扫描配置和构建脚本彻底解耦安全团队自己就能维护。血泪经验是——别指望第一次就配出干净的扫描结果。我现在的习惯是每个新工程接入时强制先做一次“只翻译不扫描”确认产物体量和文件数对得上再跑全量扫描最后人工审前五十条。跳过这三步直接看报表数字后面的每一轮沟通都在还债。希望帮到你。本文还有配套的精品资源点击获取