SonarQube Java自定义规则编写分享

背景

当今的大部分公司都有对自己的业务代码进行安全性审计的需求,来保证业务代码的安全性,同时代码审计作为SDL中重要的一环,可有效保证业务的CIA。但是人工审计存在严重的性能瓶颈,单纯的使用代码扫描器效果也不尽如意,误报问题较多。目前较好的方法:结合业务,自定义规则,结合两者优势。但是网上关于这方面的介绍较少,希望本文章能帮助到有需求的同学。选择的扫描为SonarQube,这款扫描器是开源扫描器中较为出色的一款,有丰富的图像化界面和强大的语法解析能力。

准备工作

  1. 下载并运行SonarQube,具体步骤请参考官网教程。
  2. 下载sonar-java插件源代码,这也是Java扫描规则集,我们会基于这个规则集编写我们自己的规则,下载地址:
    https://github.com/SonarSource/sonar-java

sonar-java插件关键结构

java-checks模块:该模块包含最重要的JAVA扫描规则集
java-frontend模块:该模块提供JAVA语法解析类,是该插件的基础

一条规则的必要构成

  1. java-check中添加一条规则
  2. java-check test模块中添加测试用例
  3. java-check resource模块中添加规则描述,包括一个html和一个json文件
  4. 在org.sonar.java.checks.CheckList中注册规则

示例解析

我们先使用java-check中的一条扫描规则作为示例,先了解下如何编写和注册规则,规则路径如下:
org.sonar.java.checks.spring.RequestMappingMethodPublicCheck

先看规则本体:
package org.sonar.java.checks.spring;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.sonar.check.Rule;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.Tree;

@Rule(key = "S3751")
public class RequestMappingMethodPublicCheck extends IssuableSubscriptionVisitor {

  @Override
  public List<Tree.Kind> nodesToVisit() {
    return Collections.singletonList(Tree.Kind.METHOD);
  }

  private static final List<String> CONTROLLER_ANNOTATIONS = Arrays.asList(
    "org.springframework.stereotype.Controller",
    "org.springframework.web.bind.annotation.RestController"
  );

  private static final List<String> REQUEST_ANNOTATIONS = Arrays.asList(
    "org.springframework.web.bind.annotation.RequestMapping",
    "org.springframework.web.bind.annotation.GetMapping",
    "org.springframework.web.bind.annotation.PostMapping",
    "org.springframework.web.bind.annotation.PutMapping",
    "org.springframework.web.bind.annotation.DeleteMapping",
    "org.springframework.web.bind.annotation.PatchMapping"
  );

  @Override
  public void visitNode(Tree tree) {
    if (!hasSemantic()) {
      return;
    }

    MethodTree methodTree = (MethodTree) tree;
    Symbol.MethodSymbol methodSymbol = methodTree.symbol();

    if (isClassController(methodSymbol)
      && isRequestMappingAnnotated(methodSymbol)
      && !methodSymbol.isPublic()) {
      reportIssue(methodTree.simpleName(), "Make this method \"public\".");
    }
  }

  private static boolean isClassController(Symbol.MethodSymbol methodSymbol) {
    return CONTROLLER_ANNOTATIONS.stream().anyMatch(methodSymbol.owner().metadata()::isAnnotatedWith);
  }

  private static boolean isRequestMappingAnnotated(Symbol.MethodSymbol methodSymbol) {
    return REQUEST_ANNOTATIONS.stream().anyMatch(methodSymbol.metadata()::isAnnotatedWith);
  }

}

该规则的核心是visitNode,通过重写该方法,来遍历被扫描Java文件的语法树,该方法会在扫描任务运行时自动被调用。

该规则的运行流程:

  1. 扫描器加载插件进行扫描,解析被扫描文件的语法树
  2. visitNode被调用,并将解析好的语法树传入
  3. 运行自定义方法isClassControllerisRequestMappingAnnotated
  4. 使用reportIssue上报问题

但是只有一个规则文件是无法被扫描器正常加载的,还需要一个规则定义文件和一个规则详情描述文件:

规则定义文件 S3751_java.json

S3751为该扫描规则的Key,在规则文件里由@Rule(key = "S3751")定义

{
  "title": "\"@RequestMapping\" methods should be \"public\"",
  "type": "VULNERABILITY",
  "status": "ready",
  "remediation": {
    "func": "Constant\/Issue",
    "constantCost": "2min"
  },
  "tags": [
    "spring",
    "owasp-a6"
  ],
  "standards": [
    "OWASP Top Ten"
  ],
  "defaultSeverity": "Blocker",
  "ruleSpecification": "RSPEC-3751",
  "sqKey": "S3751",
  "scope": "Main",
  "securityStandards": {
    "OWASP": [
      "A6"
    ]
  }
}

type:为该规则的类型,包括VULNERABILITY、BUG SECURITY_HOTSPOT、CODE_SMELL等。
constantCost:代表要解决该问题大概需要花费多长时间。
scope:定义要被扫描项目的范文,包括All、Main、Test。

规则详情描述文件 S3751_java.html

规则详细描述,会在SonarQube Web端规则详情页面里展示出来

<p>A method with a <code>@RequestMapping</code> annotation part of a class annotated with <code>@Controller</code> (directly or indirectly through a
meta annotation - <code>@RestController</code> from Spring Boot is a good example) will be called to handle matching web requests. That will happen
even if the method is <code>private</code>, because Spring invokes such methods via reflection, without checking visibility. </p>
<p>So marking a sensitive method <code>private</code> may seem like a good way to control how such code is called. Unfortunately, not all Spring
frameworks ignore visibility in this way. For instance, if you've tried to control web access to your sensitive, <code>private</code>,
<code>@RequestMapping</code> method by marking it <code>@Secured</code> ... it will still be called, whether or not the user is authorized to access
it. That's because AOP proxies are not applied to non-public methods.</p>
<p>In addition to <code>@RequestMapping</code>, this rule also considers the annotations introduced in Spring Framework 4.3: <code>@GetMapping</code>,
<code>@PostMapping</code>, <code>@PutMapping</code>, <code>@DeleteMapping</code>, <code>@PatchMapping</code>.</p>
<h2>Noncompliant Code Example</h2>
<pre>
@RequestMapping("/greet", method = GET)
private String greet(String greetee) {  // Noncompliant
</pre>
<h2>Compliant Solution</h2>
<pre>
@RequestMapping("/greet", method = GET)
public String greet(String greetee) {
</pre>
<h2>See</h2>
<ul>
  <li> OWASP Top 10 2017 Category A6 - Security Misconfiguration </li>
</ul>

这两个文件都位于
sonar-java/java-checks/src/main/resources/org/sonar/l10n/java/rules/squid/目录下,
我们自定义的规则json和html文件也要放在该目录下,文件名为KEY_java.json、KEY_java.html。KEY在规则中使用@Rule注解定义。

测试文件

编写一条好的规则往往需要很多次测试,通过TDD(测试驱动开发)的方式来帮助我们写出一条好的规则。
该示例规则的测试文件:
org.sonar.java.checks.spring.RequestMappingMethodPublicCheckTest

package org.sonar.java.checks.spring;

import org.junit.Test;
import org.sonar.java.checks.verifier.JavaCheckVerifier;

public class RequestMappingMethodPublicCheckTest {

  @Test
  public void test() {
    JavaCheckVerifier.verify("src/test/files/checks/spring/RequestMappingMethodPublicCheck.java", new RequestMappingMethodPublicCheck());
    JavaCheckVerifier.verifyNoIssueWithoutSemantic("src/test/files/checks/spring/RequestMappingMethodPublicCheck.java",
      new RequestMappingMethodPublicCheck());
  }
}

通过JavaCheckVerifier类中提供的方法,来启动我们的规则扫描文件。

注册规则

org.sonar.java.checks.CheckList中进行注册,使用add方法添加需要注册的规则

自定义示例分享

Struts2 S2-057检查规则
说明:扫描项目pom.xml中是否使用包含S2-057漏洞版本的struts2依赖

package org.sonar.java.checks.xml.maven;

import org.sonar.java.checks.xml.maven.helpers.MavenDependencyCollector;
import org.sonar.java.xml.maven.PomCheck;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.apache.commons.lang.StringUtils;
import org.sonar.check.Priority;
import org.sonar.check.Rule;
import org.sonar.java.xml.maven.PomCheckContext;
import org.sonar.maven.model.LocatedAttribute;
import org.sonar.maven.model.maven2.Dependency;

import javax.annotation.Nullable;
import java.util.List;

@Rule(key = "Struts2_S2_057Check")
public class Struts2_S2_057Check implements PomCheck {

    @Override
    public void scanFile(PomCheckContext context) {
        List<Dependency> dependencies = new MavenDependencyCollector(context.getMavenProject()).allDependencies();
        for (Dependency dependency : dependencies) {
            LocatedAttribute artifactId = dependency.getArtifactId();
            LocatedAttribute version = dependency.getVersion();

            if (version != null && artifactId != null && "struts2-core".equalsIgnoreCase(artifactId.getValue()) && !strutsVerCompare(version.getValue())) {
                String message = "此版本Struts2包含高危漏洞";
                List<PomCheckContext.Location> secondaries = getSecondary(version);
                int line = version.startLocation().line();
                context.reportIssue(this, line, message, secondaries);
            }
        }
    }

    private static List<PomCheckContext.Location> getSecondary(@Nullable LocatedAttribute systemPath) {
        if (systemPath != null && StringUtils.isNotBlank(systemPath.getValue())) {
            return Lists.newArrayList(new PomCheckContext.Location("configure check", systemPath));
        }
        return ImmutableList.of();
    }

    private static boolean strutsVerCompare(String version){
        String StrutsVersion1 = "2.3.35";
        String StrutsVersion2 = "2.5.17";
        String[] versionArray1 = version.split("\\.");
        if(versionArray1[1].equalsIgnoreCase("3")){
            if(compareVersion(StrutsVersion1, version) > 0){
                return false;
            }
        }
        if(versionArray1[1].equalsIgnoreCase("5")){
            if(compareVersion(StrutsVersion2, version) > 0){
                return false;
            }
        }
        return true;
    }

    private static int compareVersion(String version1, String version2){
        String[] versionArray1 = version1.split("\\.");
        String[] versionArray2 = version2.split("\\.");
        int idx = 0;
        int minLength = Math.min(versionArray1.length, versionArray2.length);
        int diff = 0;
        while (idx < minLength
                && (diff = versionArray1[idx].length() - versionArray2[idx].length()) == 0
                && (diff = versionArray1[idx].compareTo(versionArray2[idx])) == 0) {
            ++idx;
        }
        diff = (diff != 0) ? diff : versionArray1.length - versionArray2.length;
        return diff;
    }

}

规则详细说明:

先看几个关键点:

  1. @Rule(key = "Struts2_S2_057Check")该注解声明本条规则的Key
  2. implements PomCheck
    public void scanFile(PomCheckContext context)
    本条规则实现PomCheck类,重写scanFile,这样插件和扫描器会自动解析Pom.xml文件,并将解析完后的pom文件语法树传递进来
  3. strutsVerCompare 用来定义哪些版本的Struts2依赖存在漏洞

之后在
sonar-java/java-checks/src/main/resources/org/sonar/l10n/java/rules/squid/中新建Struts2_S2_057Check_java.json和Struts2_S2_057Check_java.html两个文件,大家可以参考其他规则来编写。最后在CheckList中进行注册。

插件编译

mvn clean package -Dlicense.skip=true
-Dlicense.skip=true 跳过签名检查

可能遇到的问题

编译时提示找不到maven2相关类,在IDE中将sonar-java/java-maven-model/target/generated-sources目录设置为Generated Sources Root

2020年3月更新:

有同学在使用自己插件时,sonarqube会报如下错误:

java.lang.IllegalStateException: Name of rule [repository=squid, key=${YourRuleName}] is empty

解决方法:

将 java-checks/src/main/resources/org/sonar/l10n/java/rules/squid 目录下和自己规则对应的html以及json文件更名为${YourRuleName}_java.html和${YourRuleName}_java.json

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容