插件开发,是一件即快乐又痛苦的事情。快乐的是你可以根据自己的需求通过插件来进行实现,比如经常看到的 Chrome 的插件开发。插件对于应用的原生生态有着很大的益处,往往那些特别优秀的插件甚至会被官方收编或者在正式功能中加入插件的功能。痛苦的是你需要去看文档,看插件开发的各种文档,如果文档不详细的话,痛苦加倍。程序猿最讨厌的事就是看别人的文档以及自己写文档。当然,除了文档,作为小白你还会踩到各种各样的坑。

先吐槽

五一期间,疫情实在是憋得无趣,于是就成生了编写一款 Goland 上的 SCA 检测的插件的想法。Jetbrains 作为一个 IDE 开发公司,通过 Java 的语言生态开发出 IDEA 全家桶系列如此精美并且功能强大的 IDE 产品。其背后的技术能力不得不让人折服。IDE 是程序猿开发的生产力,而 Jetbrains 公司则是生产力的生产力。这几天,笔者就在着力开发一款针对 Goland 的第一款 SCA 检测插件。相较于以往 Chrome 或者 Burp 的插件开发而言,Jetbrains 插件开发的难度大大提升,主要是因为以下几点原因:

API 文档过于简单

IntelliJ 只提供了官方的文档地址。这里面包含了一些 API 的实现以及介绍,但是太简单了。全篇中几乎找不到相关实现的示例代码,通常只有寥寥数语的介绍。举一个例子,希望能够通过插件能够创建文件,在找遍了官方的文档后,只发现了以下内容:

image.png

文档里面提到可以使用 PsiDirectory 中的 add 方法来保存 PSI 文件,但它没说 HOW!那怎么办,只能去 Github 中去搜索代码关键字,然后扒别人的代码去看别人是如何实现的,这绝对是一个非常痛苦的过程,尤其是你看的是一个实现很糟糕的插件。

API 复杂性

由于 IDEA 强大的生态,其 API 要考虑到兼容性以及很多特性,所以 API 中很多的含义不好理解。其本身也是包含了很多复杂的配置项,同时还需要综合考虑插件是通过什么样的形式去实现。

太“强大的”官方模板

官方提供了一个创建插件的模板。首先承认的一点是这个模板的功能非常强大,涵盖插件开发、单元测试、质量检查、发布的整个生命周期,并且与 Github 无缝集成。不过作为模板,它包含的内容是不是太多了呢?这个模板的 README 几乎看了3遍之后才知道里面包含了哪些内容。实际上,对于一个小白来说,这个过程挺痛苦的,甚至可能有的人看了一下就萌发了退意。里面的一些模块,比如单元测试模块以及覆盖率检查这些模块,可以作为可选项,并不一定要默认就包含进去。

Bug 有一点点多

目前尚未确定是否这是一个 Bug,但是笔者严重怀疑这是一个 Bug。上面提到的模板,通过 Gradle 实现了一系列的任务。在 Run Verifications 中,有个小任务是 ./gradlew listProductsReleases,它会在 build 文件中生成一个 listProductsReleases.txt 的文件。而这个文件中的版本应该适用于 IDE 兼容型的检查。但是在运行这个 task 的过程中,反复遇到下面的报错:

image.png

这个任务的报错是由于无法下载 GO-2021.1.4 版本的 IDE。在我理解,这个版本应该是对应到 Goland 中的版本,而 IDEA 的版本可以参考官方的版本列表页面。报错中的链接则是则是各个 IDEA 的发布信息。

image.png

在 Go 的 release 信息中,的确没有看到 2021.1.4 版本信息,而这个版本号则是 listProductsRelases 生成的。并且无法得知这个生成的原理是什么。在一个下午的持续尝试中,终于发现了这个版本信息与 gradle.properties 中的 pluginSinceBuild 以及 pluginUntilBuild 相关,并且最终定位到是由于 pluginSinceBuild = 211 这个配置项导致的,最终将这个版本号改为212,则没有产生 2021.1.4 这个版本。而这个版本信息配置是官方的模板配置提供的,在 Github 也看到一些插件使用了同样的配置,所以目前怀疑这是官方的一个 Bug 导致的。

目前这个已经被官方确认为一个 bug。不过这个 bug 的影响还比较大,因为插件发布之前需要做兼容性检查,但是目前又没法通过 Goland 最新版的的兼容性检查,就会导致在最新版本上无法使用插件。

后面有遇到一个非常奇怪的 bug,一开始是发现 map 在代码中无法获取。经过折腾了一段时间才发现原来是每次 Run Plugin 的时候,并没有重新编译插件。这导致的后果是,修改代码后,修改的代码并没有生效。这个问题也被确定为 gradle-intellij-plugin 的 bug。截止目前,这个插件模板已经有两个 bug 了,其实 changelog 方面也有一个小 bug,不过不是特别影响使用就暂且不提了。

image.png

当然 Jetrains YYDS,尽管有以上的这些槽点,但瑕不掩瑜,IDEA 生态的插件的功能还是非常强大。而且,在踩过坑之后,官方模板给你带来的则是各种各样的便捷性,让你享受从开发到发布的一条龙服务。下面,则开始本次 Goland 插件开发的真正旅途。

SCA

鲁迅曾经说过,好的程序猿肯定会读别人的源码。在和牛顿的名言,站在巨人的肩膀上同理。在做插件开发的过程中,毫无疑问需要借鉴其它的插件是如何开发的。官方有提供一个 sample 的项目,但是里面实现的功能非常简单。

https://github.com/JetBrains/intellij-samples

对于 Go 项目依赖的解析,需要对 go.mod 文件进行解析从而获取。以 gobuster 为例,其 go.mod 定义如下:

module github.com/OJ/gobuster/v3

require (
	github.com/google/uuid v1.2.0
	github.com/spf13/cobra v1.1.3
	golang.org/x/sys v0.0.0-20210426080607-c94f62235c83 // indirect
	golang.org/x/term v0.0.0-20210422114643-f5beecf764ed
)

go 1.16

require 中定义的内容则为项目的依赖及其对应的版本。所以插件的做法就是通过去解析依赖和对应的版本。通过 https://deps.dev/ 去查询对应组件的版本是否具有安全漏洞。另外一部分就是解析结果,将结果展示。最初的想法是通过 JPanel 来进行展示,后来发现比较费劲,后来还是通过生成一个 markdown 格式的报告文件,展示效果如下:

# github.com/madneal/gshark  v0.2

## GO-2020-0048
## Overview
Source: OSV
ID: GO-2020-0048
Aliases: CVE-2020-25614
## Desciption
[`LoadURL`] does not check the Content-Type of loaded resources,
which can cause a panic due to nil pointer deference if the loaded
resource is not XML. If user supplied URLs are loaded, this may be
used as a denial of service vector.

## Impact
Severity: UNKNOWN

References:

* https://github.com/antchfx/xmlquery/commit/5648b2f39e8d5d3fc903c45a4f1274829df71821 
* https://github.com/antchfx/xmlquery/issues/39 
* https://go.googlesource.com/vulndb/+/refs/heads/master/reports/GO-2020-0048.yaml 
* https://storage.googleapis.com/go-vulndb/byID/GO-2020-0048.json 

插件开发

SCA 检测的原理基本上就是以上内容,而插件开发更复杂的部分则是插件开发的工程部分。intellij-platform-plugin-template 是插件开发的模板。通过插件的 README 可以了解这个插件所包含的内容,这个模板也包含了配置、开发、单元测试、测试股改、CI 以及发布等流程。直接通过 Github 中的 Use this template 就可以通过这个模板创建自己的项目。模板默认的开发语言是 kotlin,当然也可以选择 Java。但毫无疑问这是一次学习新语言的好机会,所以还是选择继续使用 kotlin。

image.png

功能插件实现的主要部分是通过定义一个新的 Action 来触发动作。主要是基于 PSI 的 API 来获取 go.mod 文件来进行 SCA 的检测。

var modFiles = FilenameIndex.getFilesByName(project, "go.mod", GlobalSearchScope.projectScope(project))

然后通过 directory 来进行报告文件的创建。

var psiFileFactory = PsiFileFactory.getInstance(project)
val createFileFromText = psiFileFactory.createFileFromText("sca.md", result)
var directory = modFile.containingDirectory
directory.add(createFileFromText)

action 的定义在 plugin.xml 文件中,可以新增 action 并且将其添加到菜单栏中。

<actions>
    <group id="Myplugin.CheckMenu" text="SCA Checker" description="Software component analysis">
        <add-to-group group-id="MainMenu" anchor="last"></add-to-group>
        <action class="com.github.madneal.secdog.DependencyCheck" id="Myplugin.Checker" text="SCA Check"></action>
    </group>
</actions>

.run 文件夹中可以看到以下几个任务:

image.png

Run Plugin 是开发任务中经常使用到的,在运行 task 后,会起一个 IDE 环境用于运行,同时也可以直接用于调试。调试过程也非常便利,通过打断点即可。关于 task 的定义可以参考 gradle-intellij-plugin 中的 task 的定义。buildPlugin 会在 build 文件夹中创建对应的插件 zip 文件。

gradle.properties 中的配置项也是有着比较重要的作用。模板中的默认配置项是 platformType = IC,对于 Goland 需要将其修改为 GO。而 pluginSinceBuild 以及 pluginUntilBuildRun Verifictions 比较重要的配置项。

另外 .github 中的 workflow 也值得讲一下。在 template-cleanup.yml 中,一直遇到报错:

image.png

这个 job 主要是用于将之前模板里面的默认配置项内容进行替换,后来其实这个问题主要是由于仓库中 Actions 中的设置,默认的配置并没有允许 write 权限。

image.png

在这个 job 完成之后,仓库的包名以及插件名称都会进行对应的修改。同时,如果希望通过 Github 来进行插件的发布,还需要进行以下配置项的配置。

image.png

至此,插件的开发,发布过程基本都打通了,后续的发布过程也比较简单。模板也提供了通过 Release 来进行发布以及更新 CHANGELOG.md 的功能。

插件的安装可以通过应用市场或者 releases 中的压缩包文件。插件的使用可以点击菜单栏中的 SCA Checker 中的 SCA Check 来运行,或者直接调用 action 来触发。插件的安装和使用可以参考这个教学视频

image.png

image.png

总结

毫无疑问,本次插件的开发过程还是比较曲折,在插件开发过程中遇到各种各样的问题。目前插件的状态还是非常初步的,后续还会进行插件功能的进一步优化,甚至扩大插件覆盖的功能范围,比如覆盖 Golang 中的代码安全问题。

References: