IOS 代码扫描从放弃到入门

2021-09-23 17:06:55 浏览数 (1)

一、前言

我司今年开始尝试一些代码质量相关建设,比如组织 codereview、修复代码扫描漏洞.这是一个很好的现象,当我们为了快速迭代,往往为了让需求上线,导致代码并不是很规范,时间长了就留下了一堆技术债.

前日的一天,iOS 老哥找我说让看看能不能弄弄 IOS 代码扫描,扫描出一些代码漏洞,尝试去修复漏洞和 bug.

于是乎,下面就是我记录一下折腾了几天完成的 IOS 代码扫描初探的过程.

二、工具选择

从去年开始,就一直研究 IOS 代码扫描这款.无奈乎,IOS 在代码扫描这个领域能选的工具其实不算太多.

这次主要介绍如下几个工具:

  1. oclint
  2. infer
  3. sonar-swift

1、Oclint SonarQube方案

所需安装工具一览

下面是在mac机器上安装的工具

  • homebrew(mac命令管理软件工具)
  • Java JDK(推荐jdk而不是jre,最新的即可)
  • maven
  • xcode(通过appstore下面)
  • xcpretty(用于对xcodebuild的输出进行格式化)
  • sonarqube(代码扫描平台)
  • sonar-scanner(扫描工具)
  • oclint
  • SonarQube Plugin for Objective C(扫描插件)
oclint

OCLint是基于Clang Tooling开发的静态分析工具,主要用来发现编译器检查不到的那些潜在的关键技术问题.

命令安装

代码语言:javascript复制
brew tap oclint/formulae
brew install oclint

下载安装包安装

代码语言:javascript复制
https://github.com/oclint/oclint/releases

配置环境变量

代码语言:javascript复制
OCLint_PATH=/Users/xinxi/Documents/oclint/build/oclint-release
export PATH=$OCLint_PATH/bin:$PATH

source .bash_profile

验证是否安装成功。在终端输入 oclint --version

xcpretty

用于对xcodebuild的输出进行格式化

代码语言:javascript复制
gem install xcpretty

用法:

紧跟在xcodebuild 相关语句后面,比如:

代码语言:javascript复制
xcodebuild [flags] | xcpretty

可以结合tee进行日志收集

代码语言:javascript复制
xcodebuild [flags] | tee xcodebuild.log | xcpretty
SonarQube安装

官网地址,sonarqube分社区版本和商业化版本,能扫描多种语言并且开源

代码语言:javascript复制
https://www.sonarqube.org/downloads/

docker 安装

代码语言:javascript复制
docker pull sonarqube:8.6-community

二进制文件安装

在bin/macosx-universal-64目录下的输入:

代码语言:javascript复制
sh sonar.sh start

控制台输出"Started SonarQube"说明启动成功.

在浏览器访问,能打开页面说明启动成功.

代码语言:javascript复制
http://127.0.0.1:9000/

需要说明的是SonarQube如果想持久化保存数据,是需要依赖mysql数据库的.

SonarQube 默认提供H2存储,只能暂时存储一些小项目结果,仅为了演示使用.

在 conf/sonar.properties 下配置数据库地址即可.

可选 MySQL、Oracle、PostgreSQL

sonar-objective-c插件

sonarqube 默认没有扫描 oc 的检查,sonarqube 官方的 sonar-objective-c 插件是收费的.

需要在找一个免费的插件,在github找到两个项目

插件一

代码语言:javascript复制
https://github.com/Backelite/sonar-objective-c

这个插件在三年前没有修改了,在使用中发现有些扫描规则并没有.

插件二

这个项目稍微更新的时间短一些,有些规则适当的更新了

代码语言:javascript复制
https://github.com/raatiniemi/sonar-objective-c

下载插件放到 /extensions/plugins 目录下

sonar-scanner

sonar-scanner用来扫描本地代码,并且上传到SonarQube平台中.

下载地址: https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/

按照不同的操作系统选择不同安装包即可.

配置环境变量:

代码语言:javascript复制
vim /etc/profile

export SONAR_HOME=/usr/local/sonarqube-6.7.5
export SONAR_SCANNER_HOME=/usr/local/sonar-scanner
PATH=$PATH:$SONAR_HOME/bin:$SONAR_RUNNER_HOME/bin

source /etc/profile

sonar-scanner分为两种使用方式:

配置文件方式:

在项目根目录下新建 sonar-project.properties 文件,内容如下:

代码语言:javascript复制
//项目的key

sonar.projectKey=projectKey

//项目的名字

sonar.projectName=projectName

//项目的版本

sonar.projectVersion=1.0.0

//需要分析的源码的目录,多个目录用英文逗号隔开
sonar.sources=D:/workspace/Demo/src

进入项目根目录下,然后输入“sonar-scanner”命令,执行代码分析

命令行方式:

在命令中设置了参数

代码语言:javascript复制
sonar-scanner -Dsonar.host.url=http://sonarqube -Dsonar.projectKey=app -Dsonar.sources=.

项目实验

上面的软件安装完成后,基本上具备的代码扫描的条件.找一个开源项目实验下

使用网络库AFNetworking项目:https://github.com/AFNetworking/AFNetworking

脚本
  • 首先clone代码到本地,然后再清理项目工程
代码语言:javascript复制
xcodebuild -workspace AFNetworking.xcworkspace -scheme AFNetworking iOS -sdk iphonesimulator11.2 -configuration Debug clean
  • 生成compile_commands.json
代码语言:javascript复制
xcodebuild -workspace AFNetworking.xcworkspace -scheme AFNetworking iOS -sdk iphonesimulator11.2 -configuration Debug COMPILER_INDEX_STORE_ENABLE=NO | xcpretty -r json-compilation-database -o compile_commands.json
  • 生成oclint.xml
代码语言:javascript复制
oclint-json-compilation-database -- -report-type pmd -o oclint.xml -max-priority-1 100000  -max-priority-2 100000  -max-priority-3 100000
  • 处理oclint.xml

oclint生成的报告中如下形式的规则会导致Objective-c分析插件出错(ERROR: The rule 'OCLint:compiler warning' does not exist, 刚才上面提到的sonar-objective-c插件并没有处理warning这些规则. 所以需要通过脚本删除这个结果.

脚本

代码语言:javascript复制
#!/usr/bin/python

import xml.etree.ElementTree as ET
import os

os.system('mv oclint.xml oclint.xml.origin')
tree = ET.ElementTree(file='oclint.xml.origin')
root = tree.getroot()
del_items = []
for child in root:
    for one in child:
        if one.attrib['ruleset'] == 'clang':
            print child.attrib['name']
            del_items.append(child)
            break

for del_item in del_items:
    root.remove(del_item)

tree.write('oclint.xml')

在和 oclint.xml 一个目录下,执行该脚本

  • 生成 Sonar 报告

将如下内容保存为 sonar-project.properties 文件,放到 AFNetworking 目录下

代码语言:javascript复制
sonar.projectKey=AFNetworking
sonar.host.url=http://localhost:9000
sonar.login=admin
sonar.password=admin

sonar.language=objc
sonar.objectivec.workspace=AFNetworking.xcworkspace
sonar.objectivec.appScheme=AFNetworking iOS
sonar.sources=AFNetworking

sonar.objectivec.oclint.report=oclint.xml
  • Sonar平台展示

扫描结果图一:

扫描结果图二:

问题记录

在使用demo中非常顺滑,没什么问题.但是接入了实际项目,出现了如下问题.

  • 问题一:编译项目失败

解决方案:

命令行编译的问题,必须携带参数" COMPILER_INDEX_STORE_ENABLE=NO"

  • 问题二:oclint: error: violations exceed threshold

解决方案:

代码语言:javascript复制
maxPriority=15000
代码语言:javascript复制

${oclint_in} $${oclint_ex} -- -o=$$BUILD_WORK_DIR/oclint/lint.xml -report-type=pmd -stats -max-priority-1=$$maxPriority -max-priority-2=$$maxPriority -max-priority-3=$maxPriority -rc LONG_LINE=500 -rc LONG_VARIABLE_NAME=10
  • 问题三:如果扫描的生成的 compile_commands.json 文件过大,oclint-json-compilation-database会提示出错“OSError: [Errno 7] Argument list too long”

这个问题在网上看了很多帖子都是如下解决方案,但是实际中使用根本没有解决问题.

https://github.com/oclint/oclint/issues/233有网友给出解决方案https://github.com/wuwen1030/oclint_argument_list_too_long_solution/tree/master

解决方案:

oclint-json-compilation-database可以过滤不想扫描的文件和需要扫描的文件夹

-e忽略扫描和-i是指定扫描路径

代码语言:javascript复制
oclint-json-compilation-database -e pods -i build

虽然使用上面的命令扫描,不报错误,但是在平台中扫描的bug数是0,这个问题目前一直未解决.

  • 问题四:mysql存储问题

2、infer sonar-swift

基于上面失败方案一度想放弃,但是无意中在社区中,看到了好未来开源的iOS代码扫描的帖子"我们开源了一款SonarQube iOS代码扫描插件",https://testerhome.com/topics/26967, 又激起了我想重新尝试的勇气.

github地址:

代码语言:javascript复制
https://github.com/tal-tech/sonar-swift

简单看了一下需要工具,需要infer、xcpretty、sonar、sonar-swift插件.

扫描规则:

代码语言:javascript复制
https://github.com/tal-tech/sonar-swift/blob/master/docs/rule.md

这次尝试并没有急于着手干,看到帖子下面有个微信群并加了群,询问了开发者一些细节,确认是可以扫描 oc 项目的.

sonar-swift

插件地址:

代码语言:javascript复制
https://github.com/tal-tech/sonar-swift/releases

当时我下载的是v1.0.2版本,把插件放到 /extensions/plugins 目录下,重启sonar

脚本

官方提供的脚本

代码语言:javascript复制
xcodebuild clean build -workspace app.xcworkspace -scheme scheme -destination 'generic/platform=iOS' COMPILER_INDEX_STORE_ENABLE=NO | tee xcodebuild.log > /dev/null
xcpretty -r json-compilation-database -o compile_commands.json < xcodebuild.log > /dev/null
# --skip-analysis-in-path 是忽略扫描目录
infer run --skip-analysis-in-path Pods --compilation-database compile_commands.json

# 可选,如果有 swift 语言使用
swiftlint lint > swiftlint.txt

lizard --xml > lizard-report.xml
sonar-scanner -Dsonar.host.url=http://sonarqube -Dsonar.projectKey=app -Dsonar.sources=. -Dsonar.swift.swiftlint.report=swiftlint.txt -Dsonar.swift.lizard.report=lizard-report.xml -Dsonar.swift.infer.report=infer-out/report.json

infer

infer是facebook开源的一款代码扫描软件,可以分析 Objective-C, Java 或者 C 代码,报告潜在的问题

在releases页面中下载二进制文件

代码语言:javascript复制
https://github.com/facebook/infer/releases

设置环境变量

代码语言:javascript复制
tar xf infer-osx-vXX.tar.xz
# this assumes you use bash, adapt to your needs in case you use
# another shell
echo "export PATH=$PATH:`pwd`/infer-osx/infer/infer/bin" 
     >> ~/.bashrc && source ~/.bashrc

通过infer --version查看infer版本信息,说明安装成功.

扫描iOS命令:

代码语言:javascript复制
infer -- xcodebuild -workspace "test.xcworkspace" -scheme "scheme"

扫描出的结果会在工程目录下的infer-out文件中,其中具体的代码会以csv,txt,json的格式分别存在对应的文件中。可以供我们分析.

扫描过程

infer扫描阶段

扫描的bug数量

扫描规则

结果上传成功

sonar 平台展示数据

扫描结果图三:

扫描结果图四:

从下载代码到上传扫描结果,大概1小时30分支,和项目规模成正比.

问题记录

问题1

解决方案: lizard 这个报告不要了,暂时去掉

问题2:java包中没有这个规则

解决方案:

1、用 -Dsonar.exclusions=文件路径这个排除 2、在report.json中删除这个规则

问题3:没有这个规则

解决方案:使用新版本的jar包

问题4:有个异常,去掉-Dsonar.swift.swiftlint.report=swiftlint.txt
问题5

因为每次扫描都是增量扫描,如果使用多个分支同一个项目扫描,结果会被覆盖,sonar本身也不支持多个分支扫描. 每次扫描的时候想知道是扫描的哪个版本的数据,通过参数-Dsonar.projectVersion参数可以上传版本号.

shell中获取版本号

代码语言:javascript复制
version_number=`sed -n '/MARKETING_VERSION/{s/MARKETING_VERSION = //;s/;//;s/^[[:space:]]*//;p;q;}' ./PROJECTNAME.xcodeproj/project.pbxproj`

shell中获取版本号构建号

代码语言:javascript复制
build_number=`sed -n '/CURRENT_PROJECT_VERSION/{s/CURRENT_PROJECT_VERSION = //;s/;//;s/^[[:space:]]*//;p;q;}' ./PROJECTNAME.xcodeproj/project.pbxproj`

在活动页面展示了版本号

结语

经过折腾了几天,也算顺利的完成了基础环境搭建,能正常扫描出结果来了.

给我最大的启发是: 做事需要有专业的"社区",如果我没有去社区中有浏览的习惯,也很难找到不错的工具. 做事需要有专业的"圈子",专业的人做专业事,方可事半功倍.

参考资料:

  • [1]:如何使用脚本读取Xcode 11中的当前应用程序版本(https://stackoom.com/question/3q09t/如何使用脚本读取Xcode-中的当前应用程序版本)
  • [2]:iOS Jenkins持续构建-代码扫描(https://www.jianshu.com/p/c0d49bcefeb0)
  • [3]:使用Jenkins OCLint SonarCube对iOS项目进行代码分析(https://juejin.cn/post/6844903575680729102)
  • [4]:iOS 静态代码扫描平台 Sonarqube 实战 Objective-C、Swift(https://testerhome.com/topics/13158)
  • [5]:ios fastline sonarqube(https://medium.com/@aamir.ali/sonarqube-integration-with-fastlane-in-ios-3cd33e5abdc8)
  • [6]:oclint_argument_list_too_long_solution解决方案(https://github.com/wuwen1030/oclint_argument_list_too_long_solution)
  • [7]:OCLint静态代码检测实践(https://juejin.cn/post/6844904017424809998)
  • [8]:OCLint基本使用(https://www.jianshu.com/p/b2513f16d246)
  • [9]:iOS 静态代码扫描平台 Sonarqube 实战 Objective-C、Swift(https://testerhome.com/topics/13158)
  • [10]:Sonarqube & ObjectiveC 环境搭建(https://www.jianshu.com/p/5a01e56176bf)

0 人点赞