尧图精选

Fortify SCA静态应用安全测试:安装扫描、CI集成与自定义规则实战

🕒 发布时间:2026/10/2 4:53:04 📁 来源:尧图网络
1. 先想清楚Fortify 到底解决什么问题值不值得上手Fortify 这套东西在第一接触的时候最容易产生的误解就是它是个杀毒软件。不是。它属于SAST也就是静态应用安全测试工具核心场景是你还没把代码编译上线跑起来它先把源码和字节码读一遍从里面找出可能被利用的安全缺陷——SQL 注入、跨站脚本、路径穿越、硬编码口令、不安全的加密算法、越权访问等等。它输出的不是能不能跑通而是这段代码被恶意输入怼过来的时候会不会出事。我第一次接触它是在一个后台管理系统做等保自查的时候。当时团队已经跑通了 SonarQube代码异味、重复率、覆盖率都挺好看但一到安全项就抓瞎Sonar 的规则偏代码质量安全类规则覆盖面有限很多逻辑漏洞根本报不出来。后来换成 Fortify SCA 扫一遍一口气冒出来两百多条问题其中有十几条是真正的高危数据流——请求参数一路裸奔进到 SQL 拼接里。那种原来这里真有问题的冲击感是纯代码质量工具给不了的。这篇内容适合三类人看。第一类是刚接手公司代码审计工作、被安排去装 Fortify 的运维或者安全工程师第二类是想把安全扫描塞进 CI 流水线的开发负责人第三类是做外包交付、甲方要求出具安全测试报告的团队。无论你是零基础还是已经用过 Sonar、Checkmarx 之类的工具这篇都会把 Fortify 从安装、翻译、扫描、审计到自定义规则的完整链路讲透同时把那些文档里不会写的坑一并交代清楚。有一点先说明白下面涉及的具体版本号、目录路径、参数取值都是基于业界这些年比较通用的实践整理出来的不同版本之间会有细微差异落到你自己环境里时务必以随包发布的官方文档和实际报错为准。2. 搞懂这几个名词后面才不至于看得云里雾里2.1 规则包与漏洞分类体系Fortify 的检测能力全部来自规则包Rulepack。规则包是一堆 XML 描述文件的集合里面用模式匹配加数据流建模的方式定义了什么样的代码形态算漏洞。举个最直白的例子规则里会写明java.sql.Statement.executeQuery是危险方法任何能污染它入参的外部输入都可能导致 SQL 注入。这样一来工具不需要理解你的业务只靠语法结构和调用链就能判定风险。规则包对它报出的每条问题都会打上分类标签同时映射到CWE 编号和OWASP Top 10。这一点在交付场景里特别重要因为甲方验收时通常不看你报了多少条而是看高危类别覆盖到没有。所以拿到报告第一件事不是数条数是先按 CWE 分类把高危项筛出来。常见分类典型 CWE危害直观描述SQL InjectionCWE-89拼接入参数据库被拖走Cross-Site ScriptingCWE-79输出未转义前端被挂脚本Path ManipulationCWE-22文件名可控读写任意路径Hardcoded PasswordCWE-798密钥写死在代码里Insecure RandomnessCWE-330随机数可预测令牌可猜2.2 五类分析引擎各管什么很多人以为 Fortify 就是正则匹配关键词其实它内部有好几套分析引擎协同工作。**数据流分析Data Flow**负责追踪污点从输入点传播到危险点的完整路径这是它最有价值的部分**控制流分析Control Flow**看分支和异常路径会不会绕过校验**语义分析Semantic**理解方法调用的实际行为**结构分析Structural**盯代码结构和配置写法**内容分析Content**处理字符串、配置文件这类静态内容。为什么要理解这个层次因为当你遇到明明有问题却不报的情况第一反应应该是判断这是数据流断了还是规则压根不认识这个框架。这两种情况的解决路径完全不同——前者去查中间有没有被 String 拼接或集合容器打断后者就得去写自定义规则。2.3 从 Source 到 Sink把污点传播想成自来水我习惯用一个类比给新人解释把外部输入想象成水源Source比如 HTTP 请求参数、命令行参数、文件读取内容把危险操作想象成出水口Sink比如执行 SQL、写文件、反射调用。中间的水管就是数据流路径。如果从水源到出水口一路畅通中间没有任何净化装置Sanitizer比如参数化查询、转义函数、白名单校验那这条路径就会被标记为漏洞。Fortify 报出的每条问题都带一条 Trace也就是这条水管的完整走向。审计的时候你不必逐行读代码直接看 Trace很快就能判断是真漏洞还是误报。这个技巧能省掉大量时间尤其是面对几千行的大文件。2.4 FPR、SSC、Audit Workbench 三者关系刚上手最容易搞混的是输出物。sourceanalyzer扫描完成后会生成一个FPR文件全称 Fortify Project Results本质上是个压缩包里面装着所有原始分析结果。FPR 本身不是给人看的要打开得用Audit Workbench这是本地图形化审计工具负责人工确认、标记误报、调整严重级别。如果团队规模大就会用到SSCSoftware Security Center一个 Web 端集中管理平台。开发把 FPR 上传上去SSC 统一做合并、去重、趋势统计、版本对比和权限分发。简单说FPR 是原料Audit Workbench 是个人厨房SSC 是中央食堂。3. 安装与初始化把地基打稳3.1 内存和硬盘到底要留多少这是最容易翻车的地方。Fortify 翻译和扫描阶段对内存的胃口相当大因为它要在内存里构建整个项目的抽象语法树和调用图。我见过太多人机器上装完就跑结果扫描跑到一半 JVM 直接 OOM 崩掉日志里只留一句莫名其妙的堆栈。估算方法其实不复杂。粗略的经验值是每 10 万行代码扫描阶段建议预留 2GB 到 4GB 堆内存再叠加基础开销 2GB。比如一个 50 万行的 Java 单体应用-Xmx给到 12GB 到 16GB 是比较稳妥的。硬盘方面工作目录会缓存翻译中间结果建议至少留出项目源码体积的 5 到 10 倍空间。一个 200MB 的源码仓库中间产物轻松吃掉 2GB。另外提一句线程数。现代机器核多扫描阶段可以并行但并行不等于越快越好。我实测下来线程数给到物理核心数的 60% 到 75%比较舒服比如 16 核机器给-j 10。给满容易和构建进程抢资源反而拖慢。3.2 安装流程与目录约定安装本身没什么玄学但目录规划值得说两句。默认安装路径通常带空格这在 Linux 下配合 Shell 脚本容易出问题所以我一般建议装到没有空格、没有中文的路径比如/opt/fortify或者D:\Fortify。安装完成后主要认识三个目录# 工具可执行文件 /opt/fortify/bin/sourceanalyzer /opt/fortify/bin/auditworkbench /opt/fortify/bin/fortifyupdate /opt/fortify/bin/ReportGenerator # 配置目录 /opt/fortify/Core/config/ # 规则与插件目录 /opt/fortify/Core/plugins/ /opt/fortify/plugins/bin目录建议加进PATH否则每次敲全路径会疯。加完之后用sourceanalyzer -version验证一下能打印出版本号说明基本环境通了。3.3 规则包更新与授权文件放置授权文件通常叫fortify.license一般放在安装根目录下或者通过环境变量FORTIFY_LICENSE_FILE指定路径。放错位置的表现是命令能跑但一执行翻译就提示找不到有效授权。这个报错很直白看到就知道是授权问题。规则包更新用fortifyupdate命令它会连到官方更新源拉取最新规则。这里有个现实问题很多内网环境没有外网出口那就需要提前把规则包离线下载好然后手动放到Core/config/rules目录或者用fortifyupdate -acceptKey之类的方式导入。内网部署时这一步通常由安全团队统一分发别自己乱改目录结构。注意规则包版本会直接影响扫描结果。同一个代码库用 19.1 的规则和用 22.x 的规则扫出来条数可能差三成以上。做前后对比测试时必须固定规则包版本否则结论没有意义。3.4 全局配置项该调哪些配置文件主要是fortify-sca.properties这个文件决定了扫描器的默认行为。我不建议一上来就大改但有几项确实值得调# 堆内存上限配合命令行 -Xmx 使用 com.fortify.sca.Xmx8G # 单文件最大分析行数超大文件会拖慢整体速度 com.fortify.sca.fileExtensionsjava,xml,properties # 扫描线程数默认值 com.fortify.sca.Parallel8 # 是否启用详细日志排查问题时才打开 com.fortify.sca.Verbosefalse每改一项都要记录原因和改动时间因为这类全局配置一旦被后人随手改动很容易造成昨天还扫得好好的今天结果完全不一样的诡异现象。我个人的做法是把它纳入版本管理跟项目配置一起提交谁改的、为什么改一目了然。4. 第一次扫描从 translate 到 scan 的完整链路4.1 为什么必须先翻译再扫描Fortify 的执行模型分两大步翻译Translate和扫描Scan。这两步必须串行而且中间产物会缓存在一个以项目 ID 命名的目录里。翻译这一步的作用是把源码转换成工具内部能理解的中间表示。它其实是在调用你项目真实的构建过程把编译器看到的每一个类、每一个方法调用都记录下来。这也是为什么它敢说自己看得懂代码——它不是文本匹配是真的拿着编译产物在做分析。所以翻译阶段本质上是用 Fortify 的编译器包装器跑一遍你的构建。理解这一点非常关键因为后面所有构建失败的问题根源都在这里。你的项目平时怎么编译就怎么喂给 Fortify一旦构建脚本里有特殊处理比如动态生成的代码、代码生成插件、注解处理器翻译阶段都必须完整走一遍否则分析结果就是残缺的。翻译前第一步是清理旧缓存这个习惯一定要养成sourceanalyzer -b myproject clean-b后面跟的是项目 ID随便起名但要保持前后一致。不清理的话上一次的中间结果会混进来导致报出一些源码里已经删掉的幽灵漏洞。4.2 Java 与 Maven 项目的实操写法对于 Maven 项目最省心的方式是把 Maven 命令直接交给 Fortify 包装# 清理缓存 sourceanalyzer -b order-service clean # 翻译让 Fortify 包住 mvn 的编译过程 sourceanalyzer -b order-service -Xmx8G mvn -DskipTests clean compile # 如果项目有测试代码也需要分析就去掉 -DskipTests sourceanalyzer -b order-service -Xmx8G mvn clean test-compile这里有个细节clean要小心。如果你在 translate 命令里带了mvn cleanMaven 会清掉target目录这是正常的不影响 Fortify 的缓存。但如果你用了sourceanalyzer -b xxx clean又把 Maven 的 clean 写在一起顺序错了可能会有意外我一般分两条命令写清晰不容易出错。对 Gradle 项目思路一样把gradle build换成对应命令即可。纯手工的 Java 项目就显式写 javacsourceanalyzer -b legacy-app -Xmx4G \ javac -cp lib/*:src -d build/classes $(find src -name *.java)类路径一定要写全缺了依赖会导致 Fortify 无法解析方法签名翻译日志里会出现大量 Unable to resolve 警告。这些警告可以直接理解为这段代码我读不懂可能会漏报。4.3 C/C 项目要额外注意什么C/C 项目相对麻烦因为要处理头文件路径和编译宏。核心思路是让 Fortify 接管gcc或makesourceanalyzer -b c-project clean sourceanalyzer -b c-project -Xmx6G make clean all如果项目用 CMake可以先正常生成 Makefile再让 Fortify 包住make。关键点是宏定义和头文件路径必须和真实构建完全一致。很多 C 项目靠宏做条件编译如果宏没定义对Fortify 看到的是另一半代码漏报就出现了。排查方法很简单翻译完成后执行sourceanalyzer -b c-project -show-build-warnings把警告日志导出来重点看有没有找不到头文件这一类。4.4 扫描与生成 FPR翻译成功后进入扫描阶段。这一步可以理解为在已经建好的模型上跑规则sourceanalyzer -b order-service -scan \ -f order-service.fpr \ -Xmx8G \ -j 8 \ -logfile scan.log几个参数值得说明。-f指定输出的 FPR 文件名用项目名加日期比较清晰。-j是并行线程数。-logfile把扫描日志落盘这一步千万别省出问题时这是唯一线索。扫描时间完全取决于代码规模和规则集。我实测过一个 30 万行的 Java 项目8 核 32G 的机器翻译大概 4 分钟扫描大概 25 分钟。如果扫了两三个小时还没完八成是某几类分析跑疯了可以在日志里看到卡在哪个规则组必要时用-rules参数只加载安全类规则临时降速。4.5 用 Audit Workbench 做审计与误报标记FPR 生成后用auditworkbench打开auditworkbench order-service.fpr界面里左侧是问题列表中间是代码下方是 Trace 路径。审计的核心动作只有三个标记误报Not an Issue、调整严重级别、写说明。这三步做完FPR 才算是一份可以交付的结果。这里必须强调一个现实首轮扫描的误报率通常在三成到六成之间尤其是业务逻辑复杂、框架用得比较偏的项目。所以不要看到数字就慌也不要把原始条数直接发给老板。正确的做法是先按严重级别排序优先确认高危项逐个看 Trace 判断真假把确认过的误报打标记。注意审计结论一定要写理由。我见过不少团队只点了误报没写一句话半年后接手的人完全不知道为什么被判成误报只能重新审一遍等于白干。5. 让扫描跑进流水线CI 集成的三种姿势5.1 全量扫描放在夜间跑全量扫描是一次完整的 translate scan耗时最长但结果最全。适合放在每天凌晨的定时任务里跑一次第二天早上出报告。之所以不放在每次提交上是因为全量扫描动辄二三十分钟会严重拖慢开发节奏最后的结果一定是有人偷偷把 CI 门禁关掉。Jenkins 里大概长这样pipeline { agent any triggers { cron(H 2 * * *) } stages { stage(Fortify Translate) { steps { sh sourceanalyzer -b ${JOB_NAME} clean sh sourceanalyzer -b ${JOB_NAME} -Xmx8G mvn -DskipTests clean compile } } stage(Fortify Scan) { steps { sh sourceanalyzer -b ${JOB_NAME} -scan -f ${JOB_NAME}.fpr -Xmx8G -j 8 } } stage(Upload) { steps { sh java -jar ssc-client.jar -url $SSC_URL -f ${JOB_NAME}.fpr } } } }5.2 增量扫描与基线对比增量扫描的关键参数是baseline。它的思路是拿当前代码和上一次的基准 FPR 做对比只报出新增的问题。这样做有两个好处一是速度快因为复用了缓存二是有效——开发只关心自己这次改动引入的新问题历史遗留的存量问题不会天天刷屏。sourceanalyzer -b order-service -scan \ -f current.fpr \ -baseline previous.fpr \ -Xmx8G基线的选取是个技术活。我的经验是每周固定更新一次基线用当周经过人工审计、误报已清理干净的 FPR 作为下一周的基准。不清理就直接当基线会让误报持续被继承时间久了没人愿意看。5.3 质量门禁怎么设才合理门禁设置是最考验团队协作的一环。一刀切地规定高危必须为零听起来很美实际执行时会把所有人的提交卡死最后必然演变成集体找绕过办法。我推荐的渐进策略是这样的阶段门禁规则适用场景起步期新增高危为 0中危不拦截存量问题多的老项目稳定期新增高危和中危均为 0已清理过一轮的项目成熟期全量高危为 0中危环比不增加新立项项目特殊期只对特定 CWE 类别设卡有明确合规要求的项目门禁的判定依据不要用原始条数而要用经过审计确认的条数。这就是为什么审计环节不能省它直接决定门禁的可信度。6. 自定义规则让 Fortify 认识你家的框架6.1 什么时候非写自定义规则不可用久了你会发现一个规律Fortify 对主流开源框架的支持很好对自研框架几乎无能为力。比如你们公司封装了一套BaseDao内部用反射拼 SQL又或者用自研的模板引擎渲染页面。这些场景下Fortify 看不到熟悉的 Sink 方法名自然报不出来漏报就产生了。判断是否需要自研规则有个简单方法看看你的代码里有没有自研的危险方法也就是内部分装了一层、本质还是执行数据库操作或文件操作的公共方法。如果有而且它被到处调用那就值得写规则。6.2 用 Custom Rules Editor 写一条 Sink 规则工具自带一个图形化规则编辑器能显著降低门槛。写规则的核心是定义三个东西Source输入点、Sink危险点、Sanitizer净化点。举个常见例子假设自研框架里有个方法com.corp.dao.BaseDao.executeRaw(String sql)会直接执行拼接 SQL需要把它声明为 SinkRuleDefinitions formatVersion1.0 SinkDefinitions SinkDefinition idcorp-sql-sink DescriptionCorp BaseDao raw SQL execution/Description Typejava.lang.String/Type MethodSignature Method nameexecuteRaw ClassNamecom.corp.dao.BaseDao/ClassName ParamTypes ParamTypejava.lang.String/ParamType /ParamTypes /Method /MethodSignature /SinkDefinition /SinkDefinitions /RuleDefinitions写完之后把规则打包成.xml或.bin放到Core/config/rules下重新扫描即可生效。规则写多了之后建议按业务模块拆成多个文件别堆在一个大文件里否则维护起来想哭。6.3 规则调试与生效验证规则写完不等于生效验证步骤不能跳。我一般用一个最小的测试类来验证public class RuleTest { public void testRawSql(String userInput) { BaseDao dao new BaseDao(); dao.executeRaw(SELECT * FROM t WHERE name userInput ); } }把这个类单独翻译、单独扫描看能不能报出问题。报不出来就查三件事规则文件是否被加载、类名方法签名是否完全匹配大小写、包路径一个字符都不能差、参数类型是否写对。这三项里最常错的是包路径因为 IDE 里显示的是简名实际全限定名可能带着一长串前缀。7. 常见问题速查与排查实录7.1 翻译阶段报错怎么办翻译失败是最高频的问题几乎每个人都会遇到。整理了一张速查表覆盖我这些年遇到过的大部分情况现象大概率原因处理办法提示找不到有效授权授权文件路径错误或已过期检查FORTIFY_LICENSE_FILE环境变量大量 Unable to resolve 警告依赖缺失或类路径不全补全-cp确认依赖已下载命令执行成功但扫描报 0 条翻译未真正编译到源码查看 translate 日志确认类文件数量报no build files found构建命令没被执行检查构建脚本退出码扫描结果里有已删除的代码旧缓存未清理重新执行-b xxx clean再翻译这里重点说扫描 0 条这个坑。它特别迷惑人因为命令全部返回成功。原因通常是构建过程被跳过了——比如 Maven 用了-o离线模式或者构建脚本里有条件判断在 CI 环境下走了另一条分支。排查方法很简单翻 translate 日志看它到底调用了多少次编译器如果是个位数那肯定不对。7.2 扫描结果飘忽不定同一份代码两次扫描条数差很多先别怀疑工具按顺序查这几项规则包版本是否一致、缓存是否清理干净、构建环境JDK 版本、依赖版本是否一致。这三项里任意一项变了结果都会变。我踩过一个很隐蔽的坑本地扫描用的 JDK 8CI 用的是 JDK 11。因为部分 API 在 11 里签名变了导致某些规则匹配不上两边结果差了 40 多条。所以做前后对比时务必把 JDK 版本写进脚本里固定住。7.3 性能与内存问题OOM 是另一个高频问题。除了前面说的增大-Xmx还有几个小技巧。一是拆分扫描把单体应用按模块拆成多个子项目分别扫最后在 SSC 里合并。这样做虽然麻烦但对超大项目的效果立竿见影。二是排除无关目录sourceanalyzer -b order-service -scan \ -exclude **/test/** \ -exclude **/generated/** \ -f order-service.fpr测试代码和自动生成的代码通常不是审计重点排除掉能省不少时间。但要注意别把真正风险最高的目录也排掉了排除规则要经过确认。7.4 误报和漏报怎么治误报的治理靠审计漏报的治理靠规则。误报多的时候我一般做三件事给确认的误报打上统一的过滤标记、把高频误报的规则参数调优、在 SSC 里建立过滤集Filter Set让后续扫描自动过滤。过滤集这个东西特别好用相当于一次人工判断永久生效。漏报更麻烦因为它不会主动告诉你。我自己的做法是定期做对照测试手工构造几个已知漏洞的样例扔进扫描流程里看能不能被报出来。这个动作叫哨兵用例每季度跑一次能有效发现规则失效或配置漂移。8. 我踩过的坑和几条实操建议聊几个纯经验层面的东西文档里一般不会写。第一条关于别追求一次扫干净。刚接手的人容易犯的错是想把首轮报告里的所有问题都审完结果几千条看到崩溃最后不了了之。正确的节奏是先按严重级别切一刀高危先审中危抽样低危批量确认为误报或延期。一轮下来能清掉七八成剩下的慢慢磨。第二条关于报告怎么写。给甲方的报告不要直接贴 FPR 导出的原始表格那张表又长又乱。我习惯做三件事按 CWE 分类汇总一张统计表、挑出高危项写清楚文件行号和修复建议、附上修复前后的复扫对比。最后这一项特别加分它证明你不只是发现问题还验证了修复有效。第三条关于环境隔离。Fortify 的版本、规则包、JDK 这三个变量最好在团队内部统一用容器镜像或者统一安装包分发。我经历过一次因为三个人的规则包版本不同同一份代码扫出三份完全不同的报告排查了一整天才定位到是规则包时间戳差了两周。最后分享一个提效的小配置。如果团队用的是 IDE可以装 Fortify 的插件让开发在写代码的时候就看到问题而不是等 CI 跑完再返工。这个改动看起来小但它把安全左移了一步修复成本能从生产环境的一小时降到编码时的五分钟。装了插件之后我明显感觉到团队里因为安全扫描返工的次数少了一大半讨论也从事后追责变成了事前商量。关于自定义规则还有个后续可以扩展的方向把公司内部的编码规范里那些安全相关但工具不认识的写法逐条翻译成规则。比如内部约定String.format不能用来拼 SQL那就可以写一条规则专门盯这个。这件事慢但一旦积累起来扫描的准确率会明显上一个台阶长期看比买任何商业规则包都值。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →