本人是一个菜鸡硕士,目前做的是源代码审计,也会根据项目需求开发自定义的源代码检测工具。这时候了解一些第三方源代码分析工具就很重要了。目前已用过的有 antlr, sonarqube(除了C语言不开源其他语言的都开源), pycparser(只能分析C语言)。这些第三方库的共同点就是都有现成的函数直接把源代码文本转化成AST(抽象语法树),我们只需要根据自己的需要编写遍历AST的方法。包括生成控制流图什么的都是基于AST来分析。
在用过的3种第三方库中,pycparser是python的第三方库,能基于gcc,clang等编译器生成ast,所有能进行跨文件的分析。不过大型c工程都是通过makefile文件编译,在这上面pycparser就有些束手无策了。
antlr是一个java第三方库,可以分析C,C++,java,python等语言。不过其生成AST的速度跟sonarqube比起来确实有些感人。不过苦于sonarqube c语言插件不开源,在C语言这块作源码分析还得借助antlr。
今天主要介绍的就是如何借助sonarqube分析java源代码。先贴上官方Demo:
https://github.com/SonarSource/sonar-custom-rules-examples
这里面包含了java, python, php等语言的规则插件。包括如何打包成sonarqube插件。这里面官方也有一些说明下如何用它们的api构建自定义的检测插件,比如检测定时任务代码。
检测的代码pattern:
调用Timer类的scheduleAtFixedRate或者scheduleWithFixedDelay方法。
public static void main(String[] args) {
Timer timer = new Timer();
//timer.schedule(new TimeTest(), 2000, 3000);
timer.scheduleAtFixedRate(new TimeTask(){
public void run(){
System.out.println("time");
}
}, 2000, 3000);
}
调用ScheduledThreadPoolExecutor的scheduleWithFixedDelay方法或者类似方法以及
ScheduledThreadPool类的类似方法。
public class MyScheduledTask implements Runnable{
public void run() {
System.out.println("scheduled任务启动");
//主体代码
}
public static void main(String[] args) {
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(10);
ScheduledFuture future = executor.scheduleWithFixedDelay(new MyScheduledTask(), 1000, 2000,
TimeUnit.MILLISECONDS);
ScheduledFuture future1 = executor.scheduleWithFixedDelay(new Runnable() {
public void run() {
System.out.println("=======");
}
},
1000, 2000, TimeUnit.MILLISECONDS);
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
public void run() {
System.out.println("=========================");
}
}, 1000, 2000, TimeUnit.MILLISECONDS);
}
}
所以,检测的pattern还是比较简单,只是检测是否调用指定类的指定方法。现在就开始写检测程序吧:
首先新建一个TimeTaskRule类,继承BaseTreeVisitor类以及实现JavaFileScanner接口:
import org.sonar.check.Rule;
import org.sonar.plugins.java.api.JavaFileScanner;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.Type;
import org.sonar.plugins.java.api.tree.*;
import org.ssca.check.utils.PrinterVisitor;
import org.ssca.check.utils.WhileStatementCheck;
import java.io.IOException;
import java.util.*;
@Rule(key = "TimeTask")
public class TimeTaskRule extends BaseTreeVisitor implements JavaFileScanner {
@Override
public void scanFile(JavaFileScannerContext context) {
this.context = context;
scan(context.getTree());
System.out.println(PrinterVisitor.print(context.getTree()));
}
}
在这里还需要注意需要用Rule注解来表示这是一个检测规则,否则sonar不会加载。
重写的scanFile方法定义了遍历抽象语法树(AST)的操作,检测的时候会自动调用,无需手动调用。
PrinterVisitor类是官方提供的一个demo类,用来打印出被检测的Java文件的AST print()方法即打印AST。
这些依赖相关的类需要添加maven依赖:
<dependency>
<groupId>org.sonarsource.java</groupId>
<artifactId>sonar-java-plugin</artifactId>
<type>sonar-plugin</type>
<version>${sonarjava.version}</version>
<scope>provided</scope>
</dependency>
在定义好了检测类之后就可以定义遍历AST操作了,先打印以下下面这个代码片段的AST:
package org.sonar.samples.java;
import java.util.Timer;
import java.util.TimerTask;
/*
new Timer().schedule实现定时任务
*/
public class TimeTaskCheck{
public static void main(String[] args) {
Timer timer = new Timer();
//timer.schedule(new TimeTest(), 2000, 3000);
timer.scheduleAtFixedRate(new TimeTask(){
public void run(){
System.out.println("time");
}
}, 2000, 3000);
}
}
打印结果:
CompilationUnitTree
PackageDeclarationTree
MemberSelectExpressionTree
MemberSelectExpressionTree
MemberSelectExpressionTree
IdentifierTree
IdentifierTree
IdentifierTree
IdentifierTree : [
ImportTree
MemberSelectExpressionTree
MemberSelectExpressionTree
IdentifierTree
IdentifierTree
IdentifierTree
ImportTree
MemberSelectExpressionTree
MemberSelectExpressionTree
IdentifierTree
IdentifierTree
IdentifierTree
] : [
ClassTree
ModifiersTree
IdentifierTree
TypeParameters : [
MethodTree
ModifiersTree
TypeParameters
PrimitiveTypeTree
IdentifierTree : [
VariableTree
ModifiersTree
ArrayTypeTree
IdentifierTree
IdentifierTree
]
BlockTree : [
VariableTree
ModifiersTree
IdentifierTree
IdentifierTree
NewClassTree
IdentifierTree
Arguments
ExpressionStatementTree
MethodInvocationTree
MemberSelectExpressionTree
IdentifierTree
IdentifierTree
Arguments
IdentifierTree
Arguments
ClassTree
ModifiersTree
TypeParameters : [
MethodTree
ModifiersTree
TypeParameters
PrimitiveTypeTree
IdentifierTree
BlockTree : [
ExpressionStatementTree
MethodInvocationTree
MemberSelectExpressionTree
MemberSelectExpressionTree
IdentifierTree
IdentifierTree
IdentifierTree
Arguments
]
]
]
]
]
着实又臭又长,那就来细看下吧:
首先整个文件为一个CompilationUnitTree,下面的子节点为1个packageTree,2个ImportTree,1个ClassTree。我们关注ClassTree,其下又有个MethodTree,应该是方法定义结点,此处应该是main方法。
再来看看MethodTree中的BlockTree,那么多内容应该是方法体了,下面又有2个子结点。一个是VariableTree,一个ExpressionStatementTree. VariableTree应该是指变量定义语句。 其他应该是ExpressionStatementTree(表达式结点)。因为检测的是api调用,所以来看看ExpressionStatementTree。
ExpressionStatementTree里面有个MethodInvocationTree,这个应该是重点了,下面又有一个MemberSelectExpressionTree。所以首先来看看这部分。因为SonarSource官方并没有提供详细的api文档,所以这方面还只能自己一个一个试,试了很多次(过程省略)
接下来看看完善的程序:
@Override
public void visitMemberSelectExpression(MemberSelectExpressionTree tree) {
if (!(tree.parent() instanceof MethodInvocationTree))
return;
Symbol identifierSymbol = tree.identifier().symbol();
Type ownerType = identifierSymbol.owner().type();
String className = "unknown";
if (ownerType != null)
className = ownerType.fullyQualifiedName();
String methodName = identifierSymbol.name();
System.out.println(className + "---" + methodName);
if (class_about_time.contains(className) && method_about_time.contains(methodName)){
String line = "unknown";
if (tree.parent() instanceof MethodInvocationTree){
MethodInvocationTree method = (MethodInvocationTree)tree.parent();
line = Integer.toString(method.methodSelect().lastToken().line());
}
String msg = String.format(
"TimeTask detected %s.%s at line %s", className, methodName, line);
String currentFile = null;
try {
currentFile = context.getInputFile().file().getCanonicalPath();
} catch (IOException e) {
currentFile = context.getInputFile().file().getPath();
}
if (fileToMessages.containsKey(currentFile))
fileToMessages.get(currentFile).add(msg);
else{
List<String> msgs = new ArrayList<>();
msgs.add(msg);
fileToMessages.put(currentFile, msgs);
}
}
super.visitMemberSelectExpression(tree);
}
这里我们选择遍历MemberSelectExpressionTree, 因为这种结点出现次数很多,所以我设定只遍历MemberInvocationTree下的MemberSelectExpressionTree结点。
对于方法调用语句,MemberSelectExpressionTree类的 tree.identifier().symbol().owner().type().fullyQualifiedName()方法可以获得调用的类名。
tree.identifier().symbol().name()方法可以获得方法名。
这样,我们就可以判断只要调用了某个类的某个方法,就报问题.我们可以定义2个list来保存要检测的类和方法
private static final List<String> class_about_time = Arrays.asList("java.util.Timer",
"java.util.concurrent.ScheduledThreadPoolExecutor",
"java.util.concurrent.ScheduledExecutorService",
"org.quartz.Scheduler");
private static final List<String> method_about_time = Arrays.asList("schedule",
"scheduleAtFixedRate",
"scheduleWithFixedDelay",
"start");
完整的规则类如下:
import org.sonar.check.Rule;
import org.sonar.plugins.java.api.JavaFileScanner;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.Type;
import org.sonar.plugins.java.api.tree.*;
import org.ssca.check.utils.PrinterVisitor;
import org.ssca.check.utils.WhileStatementCheck;
import java.io.IOException;
import java.util.*;
@Rule(key = "TimeTask")
public class TimeTaskRule extends BaseTreeVisitor implements JavaFileScanner {
private static final List<String> class_about_time = Arrays.asList("java.util.Timer",
"java.util.concurrent.ScheduledThreadPoolExecutor",
"java.util.concurrent.ScheduledExecutorService",
"org.quartz.Scheduler");
private static final List<String> method_about_time = Arrays.asList("schedule",
"scheduleAtFixedRate",
"scheduleWithFixedDelay",
"start");
private static final List<String> annotation_about_time = Arrays.asList(
"org.springframework.scheduling.annotation.Scheduled"
);
private JavaFileScannerContext context;
private Map<String, List<String>> fileToMessages = new HashMap<>();
public Map<String, List<String>> getMap() {
return fileToMessages;
}
@Override
public void scanFile(JavaFileScannerContext context) {
this.context = context;
scan(context.getTree());
//System.out.println(PrinterVisitor.print(context.getTree()));
}
@Override
public void visitMemberSelectExpression(MemberSelectExpressionTree tree) {
if (!(tree.parent() instanceof MethodInvocationTree))
return;
Symbol identifierSymbol = tree.identifier().symbol();
Type ownerType = identifierSymbol.owner().type();
String className = "unknown";
if (ownerType != null)
className = ownerType.fullyQualifiedName();
String methodName = identifierSymbol.name();
System.out.println(className + "---" + methodName);
if (class_about_time.contains(className) && method_about_time.contains(methodName)){
String line = "unknown";
if (tree.parent() instanceof MethodInvocationTree){
MethodInvocationTree method = (MethodInvocationTree)tree.parent();
line = Integer.toString(method.methodSelect().lastToken().line());
}
String msg = String.format(
"TimeTask detected %s.%s at line %s", className, methodName, line);
String currentFile = null;
try {
currentFile = context.getInputFile().file().getCanonicalPath();
} catch (IOException e) {
currentFile = context.getInputFile().file().getPath();
}
if (fileToMessages.containsKey(currentFile))
fileToMessages.get(currentFile).add(msg);
else{
List<String> msgs = new ArrayList<>();
msgs.add(msg);
fileToMessages.put(currentFile, msgs);
}
}
super.visitMemberSelectExpression(tree);
}
}
接下来就先写个测试类来测试下吧,测试方法如下,这里JavaCheckVerfier类会根据源程序路径读如代码并分析。检测结果需要在规则类中定义变量(Map或者List)保存,以便打印。
import org.junit.Test;
import org.sonar.java.checks.verifier.JavaCheckVerifier;
import org.sonar.java.testing.FilesUtils;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
public class TimeTaskRuleTest {
@Test
public void TaskCheck(){
TimeTaskRule timeTaskRule = new TimeTaskRule();
String file = "src/test/files/TimeTaskCheck/TimeTaskCheck.java";
try{
JavaCheckVerifier.newVerifier().onFile(file)
.withClassPath(FilesUtils.getClassPath("target/test-jars"))
.withCheck(timeTaskRule).verifyIssues();
}catch (AssertionError error){
}
for(Map.Entry<String, List<String>> entry : timeTaskRule.getMap().entrySet()){
String fileAnylysed = entry.getKey();
List<String> msgs = entry.getValue();
System.out.println("文件为:" + fileAnylysed + "分析结果");
msgs.forEach(System.out::println);
System.out.println();
}
}
}
这里也需要添加maven依赖:
<dependency>
<groupId>org.sonarsource.java</groupId>
<artifactId>java-checks-testkit</artifactId>
<version>${sonarjava.version}</version>
<scope>compile</scope>
</dependency>
再说说这个FileUtil类,当程序只用了java自带的类(java.util.*等),并不需要FileUtil,但是当被检测程序引用了第三方库时,就需要检测程序也导入同样的第三方库,怎么导入呢?
在pom的build的plugins结点中,添加:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</version>
<executions>
<execution>
<id>copy</id>
<phase>test-compile</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
<type>jar</type>
</artifactItem>
<artifactItem>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>6.0</version>
</artifactItem>
<artifactItem>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.3.RELEASE</version>
</artifactItem>
<artifactItem>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.3.RELEASE</version>
</artifactItem>
<artifactItem>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.3.3.RELEASE</version>
</artifactItem>
<artifactItem>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.3.RELEASE</version>
</artifactItem>
<artifactItem>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.1</version>
</artifactItem>
</artifactItems>
<outputDirectory>target/test-jars</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
artifactItems为导入的第三方包,这里大部分为spring相关的。并且在编译过程中,会生成到target/test-jars目录下。
回到检测程序,我们运行以下测试文件,结果为
INFO 1/1 source files have been analyzed
文件为:/home/prophe/projects/java/sonar/java-plugin/src/test/files/TimeTaskCheck/TimeTaskCheck.java分析结果
TimeTask detected java.util.Timer.scheduleAtFixedRate at line 15
再贴一次被检测代码:
package org.sonar.samples.java;
import java.util.Timer;
import java.util.TimerTask;
/*
new Timer().schedule实现定时任务
*/
public class TimeTaskCheck{
public static void main(String[] args) {
Timer timer = new Timer();
//timer.schedule(new TimeTest(), 2000, 3000);
timer.scheduleAtFixedRate(new TimeTask(){
public void run(){
System.out.println("time");
}
}, 2000, 3000);
}
}
当然,这一套程序如果要打包成可执行jar可以写一个入口类,入口类和测试文件的检测过程相似。这里就不再展开了。
总结,这里,我用sonarjava的api简单的分析了java代码。主要写的代码是如何遍历AST,(文件读入,AST生成sonarjava可以自动进行)。不过不仅是java, sonarSource还开源了python,js,php等语言的检测插件(C语言除外)。有需要的也可打开上面的链接看看。除了AST,sonarjava还提供了控制流图相关的api,只不过我还不太会用。欢迎大佬们前来赐教。