Java是现在使用最多的语言之一,创建Java应用程序相对来说会更加迅捷,因为开发人员有许多现成的可用的开源资源。为了加速开发与迭代,许多开发者会使用开源框架、库、组件甚至代码来完成一些复杂且琐碎的构建任务。在当今Java应用程序中,几乎所有应用程序都包含来自其他人开发的库的依赖项。
据不完全统计,在一个Java应用程序中,依赖项约占二进制文件的80%-90%。因此,依赖的安全性和可靠性对于Java开发来说,是一个无法规避且十分重要的考量。在本文中,我们将为您提供一些处理Java依赖关系的建议以及自动化工具的最佳实践。
一、管理Java依赖项的重要性
当涉及到管理代码贡献时,特别是在将新代码合并到我们的主分支之前,我们通常会使用一些代码审计工具来确保代码质量,例如一些SAST工具,像Snyk Code、Graudit、CodeAnt等等都是不错的自动化工具。
但是,我们对待依赖项的方式与管理自己代码的方式会有很大的不同。在很多情况下,引入依赖关系时不需要任何形式的验证。在许多情况下,这些顶层依赖关系会引入传递依赖关系这就可能导致关系网络异常庞大。例如,一个有5个直接依赖项的200行Spring应用程序最终可能总共使用了60个依赖项,这相当于将约50万行代码直接发送到生产环境。
更新历史项目中的Java依赖关系可能更具有挑战性。如果它们已经过时,那么最终将导致兼容性问题的多米诺骨牌效应,更新一个库可能意味着有bug或安全问题而更新多个库。如果这些Java依赖项更改了它们的API,则可能需要完全重写整个程序。
此外,在许多企业中,依赖项依旧保留在清单文件中,即使它们不再在代码中使用,这些未剔除的依赖项在应用程序中却处于仍然可用状态。
这一切的一切,将会导致:
1.使用更多资源或启动时间的较大二进制文件
2.在库中添加新的依赖项时可能发生冲突
3.包含bug或安全问题的过时库
4.更新库时的兼容性问题等等
二、依赖准入原则
管理依赖首先要做的就是从源头解决问题——确立依赖准入原则。目前常用的依赖准入管理方法有两种:
1.使用制品仓进行管理:
在开发构建阶段,由于大量依赖外部组件,这时出现了两个重要问题:一是下载外部依赖费时费力;二是防止下载的组件由漏洞提心吊胆,有些组件可能还有法律风险。于是,制品仓的概念应运而生。
制品仓通常分为三类:制品仓库、镜像仓库、依赖仓库。制品仓库是存放流水线构建的通用文件类型的仓库,例如.zip、APK、.exe等格式。镜像仓库是用来拉取镜像的,比如docker、helm。依赖仓库则就是用来引入和管理依赖的,在依赖仓库中的依赖项是经过安全检测的、无漏洞与法律风险的“纯净”制品。开发者可以放心的引用依赖仓库中的依赖项,而不用担惊受怕。
现在常用的制品仓是Jfrog artifactory。
2.集成进IDE工具,在引入时进行检测:
中小型企业可能由于开发团队小,资金不充裕等各种原因,无法构建完备的制品仓,此时的替代方案就是使用能集成进IDE的自动化安全检测工具,在code阶段就对依赖进行扫描,以绝后患。同时,这种方法也是现在使用最多的,工具同样也很多。大部分SCA工具目前都支持IDE插件形式的集成,包括IDEA、Eclipse、VS Code等等。例如UniSCA、Black duck、Mend SCA。
举个例子,在IDEA中集成了UniSCA plugin,可以实时检测依赖项中的漏洞、cvss分数、许可证信息,还能给出修复建议,非常实用。
三、如何管理Java依赖项
使用存储库(如MavenCentral)的最佳实践之一是设置您自己的存储库管理器。这是一个介于你的内部开发和公共存储库之间的专用代理服务器——它不仅提供更快、更稳定的构建而且还允许你为Java包设置策略,例如,你可以阻止某些版本的进入,这样它们就无法在你的应用程序中下载和使用。
有关存储库管理器和可能产品列表的更多信息,请参见Maven文档。
Maven – Best Practice - Using a Repository Manager
四、Java项目中的依赖项
在引入新的依赖之前,每个开发者都应该问自己几个问题:
1.这能解决问题吗?
引入组件的目的是为了解决问题。如果引入的组件带来的新的问题,这一切还值得吗?
2.我需要整个包吗?
如果你只需要一个函数,那么是否值得导入一个包含许多函数和数据类型的大依赖项?有时候,自己编写该函数可能更容易,也更易于管理。例如,如果我只想使用Tuple数据类型,那么包含整个Eclipse集合就显得意义不大。
3.依赖有多少贡献者?
如果你使用的Java依赖只有一个或几个维护者,如果维护人员决定退出,或者没有时间修复bug,会发生什么情况?在项目中引入依赖之前,一定要检查核心存储库并查看有多少活动维护人员。
4.还在维护吗?
如果一个包不再维护,你肯定不像引用它。在集成依赖之前,检查GitHub存储库是否有新的更新,并检查软件包的发布周期,这将让你对该依赖的维护情况有一个大致的了解。
5.依赖的最新版本是什么?
代码示例可以让你深入了解特定的Java依赖关系。但是,这些示例可能已经过时,并且有最新版本的更新。如果Java依赖具有限定符GA或final,则通常可以将其视为稳定版本。
另外,像Mend SCA、UniSCA这样成熟的工具,可以提供最新版本以及最小修复版本,最大程度的节约开发者的宝贵时间。
6.有什么安全漏洞与风险吗?
在主动依赖Java包之前,请确保对其进行扫描以查找已知的漏洞,除非你是从公司的制品仓中下载的。工具有很多,普通的SCA开源工具就可以做到这一点。但对于Java的二进制文件检测则很少有工具能做到,支持二进制SCA检测的工具有Black duck、Mend SCA、UniSCA等。
五、更新Java依赖项
手动检查每个Java依赖项以确定是否有更新的版本可用实在是太麻烦了。幸运的是,有更简单的方法可以做到这一点。
1.Maven用例
在Maven中,亦可以像下面这样使用版本插件:
mvn versions:display-dependency-updates
2.Gradle用例
对于Gradel,我们必须包含一个插件,比如ben-manes的版本插件:
plugins {
id "com.github.ben-manes.versions" version "0.42.0"
}
现在我们可以运行一个类似的命令来显示库的新版本:
gradle dependencyUpdates -Drevision=release
3.IDEA用例
如果你正在使用IDEA,那么较新的版本可以高亮可以更新的依赖项,这对Maven和Gradle都适用:
六、从项目中移除Java依赖项
1.Maven用例
对于Maven,我可以适用依赖插件来分析我的依赖,在这种情况下,我不想被提供的依赖项或测试依赖项所困扰,所以我可以使用如下命令:
mvn dependency:analyze –DignoreNonCompile
2.Gradle用例
在Gradle中,我们需要添加另一个插件来分析依赖关系。这里,我们使用nebula.lint插件,这个Gradle lint可以分析包含的Java依赖项并查看是否有未使用的依赖项。
plugins {
id "nebula.lint" version "17.7.0"
}
我必须相应的配置插件来设置gradlelint.rules。你可以在gradle文件中执行此操作,或者作为命令行参数执行。例如:
gradle lintGradle -PgradleLint.rules=unused-dependency
七、创建依赖管理策略
在开发Java应用程序和使用依赖项时,最明智的做法是创建一个策略来处理它们。了解如何从应用程序中选择、更新和删除Java依赖项对于软件供应链安全至关重要。通过创建明确的策略或使用类似UniSCA这样的软件供应链安全管理平台,可以有效的管理依赖,识别许可证风险,保障软件资产安全,维护软件供应链的稳定。