代码安全扫描实践——使用自定义lint检查
1、代码安全扫描与lint工具
Android Studio 提供了一个名为 lint 的代码扫描工具,可帮助发现并更正代码结构质量的问题,而无需实际执行应用,也不必编写测试用例。系统会报告该工具检测到的每个问题并提供问题的描述消息和严重级别,以便可以快速确定需要优先进行的关键改进。此外,还可以降低问题的严重级别以忽略与项目无关的问题,或者提高严重级别以突出特定问题。
lint 工具可以检查的 Android 项目源文件是否有潜在的错误,以及在正确性、安全性、性能、易用性、无障碍性和国际化方面是否需要优化改进。使用 Android Studio 时,无论何时构建应用,都会运行配置的 lint 和 IDE 检查。
图 1. lint 工具的代码扫描工作流
lint工具进行代码检查的优点:
lint工具是Google提供的Android静态代码检查工具,可以扫描并发现代码中潜在的问题。除了提供的几百个lint规则,还可以开发自定义lint规则以满足定制化需求。
相比于其他工具,lint优势如下:
1.功能强大,支持java源文件、class文件、资源文件、Gradle等文件的检查;
2.扩展性强,支持开发自定义lint规则;
3.配套工具完善,Android Studio、Android Gradle插件支持lint工具;
4.lint专为Android设计,原生提供了几百个实用的Android相关检查规则;
5.有Google官方的支持,会和Android开发工具一起升级完善。
2、lint实现原理与检查效果
2.1实现原理
Android lint 的工作过程比较简单,一个基础的 lint 过程由 Lint Tool(检测工具),Source Files(项目源文件) 和 lint.xml(配置文件) 三个部分组成,Lint Tool 读取 Source Files,根据 lint.xml 配置的规则(issue)输出结果
lint.xml的配置
在Android工程中,可以创建XML文件配置lint规则的优先级和忽略路径,示例如下。配置文件默认路径为模块目录下的lint.xml,手动创建该文件,也可以通过LintOptions指定。
lint发现的问题可以分为如下几个大类:
Accessibility 辅助选项,例如 ImageView 缺少 ContentDescription 描述,String 编码字符串等问题。
Compliance 合规性,违反了Google Play的要求,比如使用了过期的库版本,性能、安全性、API等级等没有遵循新系统的要求等。
Correctness 不够完美的编码,比如硬编码、使用过时API等。
Internationalization 国际化,如直接使用汉字,没有使用资源引用等。
Interoperability 互操作性,比如和Kotlin的交互等。
Performance 性能,例如:静态引用,循环引用等。
Security 安全性,例如没有使用 HTTPS 连接 Gradle,AndroidManifest 中的权限问题等。
Usability 易用性,有更好的替换的,例如缺少某些倍数的切图,排版、图标格式建议.png格式等等。
2.2 检查效果
现在lint是直接集成到Android Studio中的,所以可以直接使用,无需再安装。
主要有三种检查触发方式:
1)编码时实时触发
当用户在Android Studio中编写出触发规则的代码时,会实时对该问题进行标识。当鼠标放到该行时会有详细的问题提醒。
2)通过菜单栏Analyze-Inspect Code选项触发
用户也可以通过手动的方式主动触发扫描,选择【Analyze】菜单,点击【Inspect Code】,然后选择扫描的范围。如下图所示:
扫描完成后,会在Android Studio底部出现扫描结果的树形显示。如下图:
3) 通过gradle lint或者添加lint编译依赖时触发
在Gradle命令行环境下可以使用命令执行检查,Windows下在工程的命令行执行gradlew lint,或者在Gradle菜单里面选择执行lint命令。
运行完成后会在各个模块的\build\reports目录生成lint-results-debug.html的lint报告文件。
生成的lint检查报告样式如下图:
3、实现自定义lint检查的方法
自定义lint开发需要调用lint提供的API,最主要的几个API如下。
• Issue:表示一个lint规则。例如调用Toast.makeText()方法后,没有调用Toast.show()方法将其显示。
• IssueRegistry:用于注册要检查的Issue列表。自定义lint需要生成一个jar文件,其Manifest指向IssueRegistry类。
• Detector:用于检测并报告代码中的Issue。每个Issue包含一个Detector。
• Scope:声明Detector要扫描的代码范围,例如Java源文件、XML资源文件、Gradle文件等。每个Issue可包含多个Scope。
• Scanner:用于扫描并发现代码中的Issue。每个Detector可以实现一到多个Scanner。自定义lint开发过程中最主要的工作就是实现Scanner。
lint中包括多种类型的Scanner如下,其中最常用的是扫描Java源文件和XML文件的Scanner。
• JavaScanner / JavaPsiScanner / UastScanner:扫描Java源文件
• XmlScanner:扫描XML文件
• ClassScanner:扫描class文件
• BinaryResourceScanner:扫描二进制资源文件
• ResourceFolderScanner:扫描资源文件夹
• GradleScanner:扫描Gradle脚本
• OtherFileScanner:扫描其他类型文件
完成自定义 lint 的编写后,编译 Gradle 可以得到对应的 jar 文件,那么 jar 应该如何接入项目,使得执行项目 lint 时可以识别到这些自定义的规则呢?有下面3种接入方式。
3.1 全局模式
把自定义规则 jar 文件放到 ~/.android/lint/目录下面,如果本地没有 lint 目录可以自行创建,这个使用方式较为简单,使得 Android lint 作用于本地所有的项目。
3.2 在项目中依赖包含自定义规则的aar包
前面的使用方式,自定义lint必须保存在电脑中的特定文件夹。实际应用时,往往希望自定义lint和工程关联,而不是和电脑关联,因此需要创建lint.aar包,并在需要执行自定义lint检查的工程中依赖这个aar。
依赖关系:Java模块 –> 包含lint.jar的lint.aar模块 –> 实际Android项目
具体步骤如下。
1.)在Android Studio中创建一个空的Java模块lintjar,和一个空的Android Library模块lintaar。
2)lintjar模块中的配置和前面相同,用于编写实际的lint规则。
3)lintaar模块依赖lintjar模块,build.gradle如下,主要是把jar文件改成了lint.jar并打包到aar里。lintaar模块编译生成的aar即为需要的lint.aar。
3.3文件单独扫描
在lint实现原理里已经知道,lint检测的文件,默认是Project的javaSourceFolders和resourceFolders,但是这样会造成每次lint检测的时间很长,我们pipeline的效率就很低。还有一种方法就是针对单独的文件扫描。
它可以用于对增量代码进行检查,有以下几个好处:
1.避免修改“祖传”代码的一些问题。如果全量扫描,之前老代码的问题一大堆就会暴露出来,这样就大大增加了工作量,开发人员也没有那么多的精力全部修改;
2.增加了一些代码规范的强制性。增量扫描代码,如果代码有问题,就回滚,可以强制开发人员规范代码;
通过阅读lint源码我们知道lint框架中有下面主要类:
LintCliClient:lint客户端,作用是集成lint检查的操作、相关配置以及lint检查的入口。
LintCliFlags:lint标志位管理类,提供了lint操作的标志位。lint代码检查工具主要使用了该类中生成日志的配置,通过加入不同实现的报告生成类可以实现不同的输出格式(比如TXT、XML、HTML等)。
LintRequest:执行lint操作时的一个请求类,主要作用是存储lint将要扫描的文件。在lint工具中重写LintRequest初始化方法可以实现增量文件的检查。
LintDriver:执行lint规则检查逻辑的类。
IssueRegistry:自定义lint规则管理类。用于添加lint自定义规则。
上述对于lint框架中类的介绍是在实现lint增量代码检查中主要用到的类。
具体针对单个文件扫描可以参考下面的代码:
4、 一个自定义规则的示例
扫描Java源文件的Scanner先后经历了三个版本。
1. 最开始使用的是JavaScanner,lint通过Lombok库将Java源码解析成AST(抽象语法树),然后由JavaScanner扫描。
2. 在Android Studio 2.2和lint-api 25.2.0版本中,lint工具将Lombok AST替换为PSI,同时弃用JavaScanner,推荐使用JavaPsiScanner。
PSI是JetBrains在IDEA中解析Java源码生成语法树后提供的API。相比之前的Lombok AST,可以支持Java 1.8、类型解析等。使用JavaPsiScanner实现的自定义lint规则,可以被加载到Android Studio 2.2+版本中,在编写Android代码时实时执行。
3. 在Android Studio 3.0和lint-api 25.4.0版本中,lint工具将PSI替换为UAST,同时推荐使用新的UastScanner。
UAST是JetBrains在IDEA新版本中用于替换PSI的API。UAST更加语言无关,除了支持Java,还可以支持Kotlin。
下面我们介绍一下通过UAST完成自定义规则的编写。
(1)引入依赖
首先,我们需要创建一个java的module,用来实现自定义lint规则,需要引入lint相关的依赖。
然后,我们新建一个规则类,需要继承Detector并实现Detector.UastScanner接口,这是实现代码检测的入口。
Detector是检测的基类
Detector.UastScanner是扫描Java和kotlin代码的接口类
(2)定义规则
接着,我们需要实现一些方法完成我们的规则检测。
a.需要检测的方法名
重写getApplicableMethodNames()方法,返回要检测的方法名,我们要检测的是,对android.util.Log类的i、e、w、d等方法的调用。
b.检测指定方法的调用
重写visitMethod()方法,该方法会在上述方法名的方法被调用时调用,所以在此方法内,我们需要判断,只有是android.util.Log的这些方法被调用时,才会进行警告。
c.规则配置
通过定义Issue完成规则的定义,检测到该问题时,通过context.report()方法抛出。
Issue主要包括问题id、名称、描述、规则组、优先级和警告级别等内容。
(3)注册规则
现在规则的实现和配置都已完成,要注册到lint我们需要继承IssueRegistry实现一个注册类。在getIssues方法中,注册我们的规则配置,即上面的ISSUE对象。
(4)生成jar包
最后,我们需要在gradle中配置生成jar包。其中Lint-Registry(gradle3.0-lint26.0以后,可以使用Lint-Registry-v2)的值,就是我们的注册类。
生成的jar包可以按照第3章中介绍的方法使之生效。
5、总结
本文介绍了lint代码扫描工具的特点、原理和检查效果,并阐述了实现自定义lint检查的方法的三种方式。最后通过一个自定义规则的编写示例演示了自定义lint规则的编写方法。通过自定义lint规则的编写,可以实现如安全编码规范、不安全函数、配置使用等安全检查。
参考资料
《使用 lint 检查改进您的代码》https://developer.android.com/studio/write/lint
《美团外卖Android Lint代码检查实践》https://tech.meituan.com/2018/04/13/waimai-android-lint.html
《Android Lint:自定义Lint调试与开发》https://www.paincker.com/android-lint-2-implements
《【工利其器】必会工具之(九)Android Lint篇——为Android量身定做的代码审查利器》https://www.1024sou.com/article/203066.html
《Android Lint基本使用和自定义规则》https://codeantenna.com/a/cTnAo5oo3H
《Android自定义Lint增量代码检查工具》https://juejin.cn/post/6844903849036103688
精彩文章推荐