对于一个软件开发团队,可以通过哪些代码质量指标和扫描方法让团队产出规范、安全、高质量的代码?让开发团队运行的安全、透明、可靠?本文总结了其中一些实践和工具,包含常见代码质量扫描工具、代码质量指标、第三方依赖管理、安全运维等几个方面,主要适用于 Java/JavaScript 技术栈的 web 项目,希望对于想要规范化自己的项目的 Tech Lead 有所帮助。
代码扫描和常见质量指标
“祸患常积于忽微”,往往一些奇怪的 bug 都是一些不规范的小问题造成的。德国飞机涡轮机的发明者帕布斯·海恩提出的一个在航空界关于飞行安全的法则,法则指出: 每一起严重事故的背后,必然有 29 次轻微事故和 300 起未遂先兆以及 1000 起事故隐患。应用于软件开发中,如果项目中代码混乱不堪,必然会在某个时候最终爆发大量的问题。
这里整理了一些常见的扫描工具和代码质量指标,可以在搭建项目基础设施时引入,用于自动化的检查代码中潜在的问题,达到控制代码产出质量的目的。
扫描工具
(1) checkstyle:checkstyle 是常用于 java 项目的扫描工具,检查源代码是否与代码规范相符,检查项目主要包括:Javadoc 注释、imports、过长的类和方法、空格、重复文件、圈复杂度等,默认使用 sun 的代码规则,也可以配置自定义的代码规则,例如阿里就发布了相应的检查规则。
(2) findbugs:通过 Bug Patterns 的概念,寻找代码中可能出现的 bug,检查项目主要包括:不良编程习惯导致的问题、性能问题、安全问题、线程问题等。例如,应使用 equals 判断相等,而不是 “ =” 操作符、流需要关闭、线程资源需要释放等问题。findbugs 的模式库对编程经验也有较好的提升作用。还可以导入和编写自己的 Bug Patterns 完善检查机制。
(3) simian:simian 是一个用于检查重复和相似代码的工具,它的重复检查类似于论文查重,会提示一定的相似度。可以单独运行,也可以作为 checkstyle 插件来使用,相对来来说比较小众。
(4) pmd:pmd 是一款跨语言的通用静态扫描工具,具备一部分 checkstyle、findbugs 的功能,不再赘述。
(5) ESlint/TSlint:前端界的 checkstyle , TSlint 设计用来做 TypeScript 类型检查,ESlint 作为代码风格检查工具。不过现在 ESlint 也提供了TypeScript 类型检查功能,基本上 ESlint 能整合这两个功能。由于性能问题, TypeScript 也采用了 ESLint 作为 TSlint替代的检查工具。
(6) SonarQube:SonarQube 是一款用于代码质量管理的开源工具,它主要用于管理源代码的质量。SonarQube 和上面的工具不太一样,SonarQube 设计目的是提供一个平台,通过插件的方式提供对各个语言进行支持,也可以和 checkstyle、pmd、simian 等工具进行集成。SonarQube 一般需要单独部署成一个服务,提供数据库,可以记录扫描结果等信息。
(7) npm audit:npm audit 是 npm 6 之后的版本 自带的一个前端安全扫描工具,可以扫描 npm 依赖中的潜在的漏洞威胁。这些引入的漏洞可能威胁用户开发的机,另外也可能被带入 bundle 文件发布到线上,带来安全问题。目前 npm audit 会在 npm install 完成后自动执行,需要留意安全威胁报告。
(8) Fortify SCA:Fortify SCA(Source Code Analyzer) 是一款非常优秀的代码安全扫描工具,用于分析代码中潜在的安全问题。通过调用语言的编译器或者解释器把代码(Java、C、C++等源代码)转换成一种中间媒体文件 NST(Normal Syntax Trcc),然后通过模式匹配相关的方式抓取存在于漏洞库中的漏洞。例如,上传的文件没有做检查等 XSS 攻击。
(9) OWASP Dependency-Track:开放式 Web 应用程序安全项目(OWASP)是一个非营利组织,提供了很多安全标准、数据库、社区和培训。其中一个工具就是 OWASP Dependency-Track,可以对第三方依赖包中的知名漏洞进行检查,扫描结果受到漏洞数据库的更新影响。
(10) archunit 架构规范检查:前面的检查是代码层面,archunit 可以用于代码架构检查,可以定义规则检查每个包中的实现是否符合规范。例如,controller 包中的类不能实现 service 的接口,repository 下的类必须实现 Repository 接口。通过 archunit 可以减少 codereview 的工作量,避免项目的结构被破坏。
统计工具
sloccount、sourcemointor 这两个工具可以用于统计代码数量,包括行数、文件数、注释等。除了在项目中扫描 bug 之外,配置代码统计工具可以对项目有一个整体的认知。其他的扫描工具还很多,例如 coverity、codemars、binscope、synk、appscan、retire.js 等工具,不再一一列举。
最佳搭配
这几款工具之间的功能有所重叠,在实际工作中,我们可以根据上面推荐的关注的点,重点清除这些问题。这些扫描工具全部用上除了会带来团队压力和维护成本之外,代码质量不会随着引入的插件增多。除开有质量团队的大厂提供这些扫描平台外,敏捷团队往往不会太大,团队持续关注一个精简的扫描组合更好。
Java 后端:
- checkstyle Java 代码风格守护,Java 项目至少应该配置一个默认的 checkstyle 规则。至少让项目干净,没有无用、重复的代码,以及超大的类和方法。建议做到每次提交代码前检查。
- findbugs 常见不规范的代码检查,一些空指针、equals 检查非常有用,而且 IDE 的插件也很好用。
前端:
- eslint 守护 JavaScript 代码风格,eslint 搭配一个 .editorconfig ,可以方便的让编辑器保持同 eslint 一致的代码风格。
- npm audit 项目中第三方包的威胁扫描,npm 自带无需额外安装,npm 6 以后自运行,需要关注并修复报出的安全问题。
安全:
- fortify 扫描代码中的漏洞,用它检查出来的大部分安全问题都是注入攻击、XSS 等攻击,这些问题明显可以在开发过程中避免。可以作为 Jenkins 插件配置,和单元测试作为同一阶段运行。
- OWASP 插件 用来扫描第三方依赖漏洞,因为项目中的依赖不会像源代码一样频繁变化,推荐使用 Jekins 插件,定期执行即可。
为什么不用 SonarQube 呢,SonarQube 是一个非常优秀的代码质量开放平台,需要单独的配置安装,需要花费额外的时间维护,对于小团队来说成本较高,如果有专门的质量团队可以考虑维护一套。
常用代码质量指标参考
- 编译告警数,大部分程序员基本上忽略 warning,但是编译器出现了告警是一种不好的体现,意味着软件可能工作,但是存在不好的实践,而这种不确定性,会带来不确定的 bug 最终让人一头雾水。编译过程中的告警,尽量消除掉,编译告警的值推荐消除到 0。
- 平均函数代码行数,过大的函数会导致阅读困难,而且往往过大的函数职责不够单一,一般将一个方法代码行数控制到 30 - 50 行。
- 平均文件代码行,和平均函数代码行一样,过长的文件一样难以维护,一般一个文件10多个方法,因此文件的代码行数一般控制到 300 - 500 行。
- 冗余代码,有时候我们代码中可能存在未使用的方法、变量等代码,这让维护者一头雾水,通常需要清零。
- 总文件重复率,出现重复文件的次数。除了编写单元测试的情况下,业务代码不应该出现重复代码,推荐值为 0。
- 总代码重复度,代码的重复度检查,限于扫描工具的识别模式,需要有一定的容忍度,推荐值在 5% - 10%
- 平均函数圈复杂度,圈复杂度用来衡量一个模块判定结构的复杂程度。如果一个方法内部有大量的 if 语句嵌套,意味着这个方法的实现质量低下,且程序复杂度高不利于维护,推荐值小于 5%。
- 安全告警,如果配置了安全扫描工具,例如 Fortify,安全威胁应该被清零。
- 代码缺陷,如果配置了缺陷扫描工具,例如 Findbus,需要清零。
第三方依赖规范化
软件开发过程中,不可避免的需要引入第三方或者开源软件包作为库或者框架引入。“第三方” 其实不是一个软件工程术语,现今在软件行业里面的理解是:第一方为自研的软件,第二方为内部发布的软件,第三方为从社区或者外部商业途径引入的软件包。对于个人开发者而言,面向“搜索引擎”编程往往将来源不明的代码片段和程序包引入到项目中。对于企业来说,考虑到的不仅仅是功能是否能实现,还要考虑引入时带来的成本和问题,例如是否需要授权、开源协议是否合理、是否会带来安全威胁。企业对于第三方依赖的引入分为几种情况:
- 作为开发工具引入,例如 gcc、Jenkins,基本没有开源协议问题,但是需要注意开发机、CI 会有安全风险。Jenkins 曾出现过漏洞,CI 服务器被当做远程矿机使用。
- 作为服务部署使用(SaaS),部分开源协议会限制这种使用方式,第三方依赖的安全问题会威胁服务器。
- 通过软件包再发布,大部分开源软件对这种使用方式有较多要求,例如 GPL 开源协议具有传染性,要求使用了 GPL 的项目也要开源。
- 拷贝源代码引入项目,非常不推荐这种方式,尽量通过包管理的方式引入。
引入第三方依赖需要充分考虑,尽可能最小成本的引入。在一个 React 的前端项目中,有不熟悉的工程师,为了使用一个简单的手风琴效果,引入了整套 bootstrap。不仅破坏了使用 React 的最佳实践,而且让输出的 bundle 文件大小激增数倍,造成首屏加载的性能问题。
常见商业友好的开源协议
商业用户常用的开源协议实际上只有6种左右,即 LGPL、Mozilla、GPL、BSD、MIT、Apache,另外还有极其宽松的 The Unlicense,但采用的开源软件不多。GitHub 提供了一个 license 清单的列表 https://choosealicense.com/licenses/,我根据开源协议的宽松程度,整理了一个列表,方便查看:
几乎所有的开源协议有一个共同的注意事项:采用该开源协议的软件项目,不提供任何责任转移和质量保证。也就是说采用开源软件造成的法律问题和开源项目无关,另外需要使用者承担因质量问题造成的所有后果。另外,除了引入的程序包之外,字体、图片、特效音、手册等媒体资源也算广义上的“软件”需要考虑开源协议和使用场景。
第三方依赖管理
对项目中出现的任何第三方依赖有效的管理有非常重要的意义,通过扫描工具,识别出项目中是否有源码、jar包、二进制文件是否来源于某个开源项目。任何的第三方软件需要申请入库管理(内部其他团队申请通过可以直接使用),质量团队对申请的软件进行评估:
- 是否有开源义务需要履行
- 引入的第三方依赖是否有 CVEs等漏洞
- 第三方开源软件是否仍然在维护
质量团队根据上面的一些条件,决定出申请的软件能否在项目中使用,允许被采用的软件会定义出优选级别,优先推荐团队使用较为优选的软件,并对项目整体的优选率有一定要求。如果项目中出现了无法识别的二进制文件、非约定目录下的代码片段,需要报备。通过良好的依赖管理和规范化,能减少不良第三方依赖的引入,让软件项目透明、可信。一些商业公司提供这些完整的服务,例如 fossid、blackduck、code-climate 等。
运维安全
大的软件公司,往往有一堆流程和要求。虽然一线开发对堡垒机、防火墙、各种安全规范显得不耐烦,但这些安全措施也在保护开发者。
1. 防火墙用于环境隔离
往往开发者理解的防火墙用于防止网络入侵、审计、入侵检测等功能,除此之外,防火墙还可以用于各个环境的隔离。一般来说,企业对于生产环境的数据控制比较严格,不会将生产环境的权限交给团队所有开发者,但网络连接有可能疏漏。曾经出现过一次线上事故,由于配置文件错误,将原本应该连接到测试的数据库连接到了生产环境,造成大量脏数据写入。如果通过防火墙规则对各个环境进行隔离,这类问题将不会出现。另外也可以设计 DMZ 区,将面向用户侧的网关部署到 DMZ 区,仅仅开放必要的端口给网关,实现内外网的物理隔离。同时,对整个系统的防火墙策略应该清晰地记录,否则在做大的基础设施更新时,梳理出所有的防火墙策略,是一件比较困难的事情。
2. 凭据管理
项目中会用到大量的凭据,例如数据库、第三方系统对接的 key,使用明文不是一件好事。理想的情况下,对项目中所有的密码信息进行掩盖(mask),避免 CI、日志中敏感信息的泄露。有很多种方法可以掩盖项目中的密码信息:
- 使用环境变量对密码信息进行覆盖。
- 使用Spring boot 的项目可以配置 jasypt,使用 jasypt 将密码加密,将生成的加密串配置 ENC(加密串) 到工程的配置文件中。加密过程可以加盐作为解密的凭据,“盐” 可以不存放到工程中,在工程部署的时候注入即可。
- 如果使用 Jenkins 等 CI/CD 工具,可以使用构建平台提供的凭证管理工具。
- 如果使用 Spring cloud,可以使用 spring cloud vault 组件部署一个凭证管理服务
另外,建议不要用任何个人凭据用作系统对接,应该使用一个公共的应用凭据。
3. 堡垒机
一般来说我们管理服务器,所有的运维操作需要通过堡垒机进行操作。开放 22 等高危端口,允许开发者直接登录到服务器是一种不安全的做法。堡垒机,通俗的来说是跳板机 + 监控。最初使用的跳板机配置了两张网卡,用于连接开发环境和生产环境,并没有监控功能。在此基础上,堡垒机增加了统一运维管理的功能,往往需要两步验证(SMS 或 Email),并对所有的操作进行记录和监控。在需要团队参与运维工作的场景中,非常有必要部署一套堡垒机服务,并使用 LDAP 对接到团队成员的 ID 上,便于集中运维管理。
4. 定期对系统软件扫描
Linux 系统往往有云厂商推送安全补丁和风险提示,但是安装到服务器上的软件,例如 JDK、nodejs,需要自己检查安全问题。因此需要在系统中安装并定期运行 CVEs 检查并及时更新。有一款 cvechecker 可以帮助运维人员,编写一个脚本定期运行 cvechecker 检查系统中已知的软件是否存在 CVEs 漏洞,并提醒开发者及时更新。
写在后面
刚开始工作时候,喜欢动态的、灵活的编程语言,讨厌的死板的、套路化的编程语言,然而需要很长一段时间,才能意识到 “约束是程序员的朋友”。对一些安全知识了解的来源大多来自修复 SonarQube 的经历,使用 findbugs 也让我对 Java 基础认识的更加深刻。
类似的,在使用一些框架、平台的时候往往存在大量的限制,有时候开发者难以意识到 “限制” 正是框架、平台的作者 “保护” 应用开发者的一种方式。有一些开发者以 Hack 框架、平台为乐,但是这样会带来潜在的隐患,在用户量上来之后负面效应表现的尤为明显。
项目的规范化对于 Tech Lead来说可以减少程序的运行事故和 codereview 时间,对于团队来说也许可以少加班吧。
【本文是51CTO专栏作者“ThoughtWorks”的原创稿件,微信公众号:思特沃克,转载请联系原作者】