0x01 前景提要
Veinmind 在最初设计时,采用了插件系统的模式,目的是为了在尽可能多的场景下复用公共功能部分的代码,让工具的开发者尽可能的减少非关键业务的代码编写。
举个例子,在 1.0 版本时,我们提供了本地扫描的整体逻辑,插件仅需要通过注册命令服务,即可生成一个扫描本地镜像/本地容器的 cli 工具, 如:
func scanImage(ctx context.Context, api.Image) {
// do some thing...
}
scanCommand.AddCommand(
cmd.MapImageCommand(scanImageCommand, scanImage)
)
这样,使用时只需关心具体的扫描逻辑,也就是scanImage
函数内如何对镜像进行操作即可,让开发者的注意力集中在安全扫描能力的建设上去。
同样的,为了实现在各种场景下被复用,我们在 runner 内扩展了扫描对象,这样设计不仅能支持扫描本地镜像,还可以直接扫描远程仓库内的镜像信息;对于 IaC 文件配置,同样支持直接扫描 git 仓库、kubernetes 集群。对于插件来说,无需做出任何修改,解决了每个插件独立的去做认证、下载、存储、删除等等一系列重复行为的问题。
基于这个思路,在 2.0 版本中,我们期望能够帮助开发者和使用者做更多的事情。
我们发现,各插件为了满足能够独立使用,都实现了一个report
包用于数据的展示和输出。于是,我们提供了一个报告器模块,让用户只需要将安全事件进行上报,无需再多考虑输出模式和结果的渲染。进一步将开发者的注意力聚焦到 “如何扫描镜像” 的业务上。
0x02 小试牛刀
要实现一个报告器,首先面临两个场景:
- 插件独立运行时,报告器应该独立输出。
- 插件被 runner 宿主调用时,报告器应该汇总所有插件的事件结果,统一输出。
对于场景1,我们可以提供一个对象来存储所有被上报的安全事件,并监听到程序运行结束时,运行输出函数,根据用户指定的输出格式参数,进行事件渲染。
由此,我们实现了MapReportCmd
方法,通过该方法将报告器相关的操作注入到cmd
指令的调用流程,伪代码如下:
func MapReportCmd(c *cmd.Command, service *Service){
// inject flags
c.Flags().StringP("format", "f" ...)
c.Flags().BoolP("verbose", "v" ...)
c.PreRun = func() {
// init service struct...
}
if !libService.Hosted() {
c.PostRun = func(c *cmd.Command, args []string) {
// do output ...
}
}
}
可以发现,我们通过sdk
的Hosted()
方法,来判断插件在运行时是否为宿主调用。依此来解决当插件被宿主调用时会被重复输出的问题。
实现了独立运行的输出后,报告器还需要解决场景2:宿主统一输出。
我们通过sdk
的服务机制,将上报事件的服务注册到Registry
中进行索引,为所有运行的插件提供 report.Service
服务,由此,插件与宿主之间即可进行安全事件的上报通信。
0x03 报告器功能如何使用?
当我们独立使用插件进行扫描时,可以发现,所有插件均包含了 -f/--format
参数(即使插件自身代码内没有指定接收该参数),此时可以通过指定该参数快速输出想要的报告格式:(目前支持 cli/html/json)
./veinmind-plugins scan image xxxx -f cli
扫描结果将会输出在控制台。
报告器还可以指定多个输出格式,同时生成多份报表:
./veinmind-plugins scan image xxxx -f cli,html,json
0x04 自定义插件如何快速接入报告器?
对于插件的开发者,目前仅需要通过MapRerportCmd
函数即可帮助插件快速生成Service
实例,并将格式化参数注入到插件的命令中。通过该函数,插件将不再需要关心任何与输出相关的问题。
talk is easy, show me the code
import(
"github.com/chaitin/veinmind-common-go/service/report"
"github.com/chaitin/veinmind-common-go/service/report/event"
"github.com/chaitin/veinmind-common-go/service/report/service"
)
var (
reportService = &report.Service{}
rootCommand = &cmd.Command{}
scanCommand = &cmd.Command{
Use: "scan",
Short: "scan mode",
}
scanImageCommand = &cmd.Command{
Use: "image",
Short: "scan mode",
}
)
func scanImage(c *cmd.Command, image api.Image) error {
// ...do some scan
evt := &event.Event{}
// now you only need report evt to service,
// report.Service will render
err = reportService.Client.Report(evt)
}
func init() {
rootCommand.AddCommand(scanCommand)
// use MapReportCmd inject preRun/postRun
// which init reportSerivce and reportOoutput at end
scanCommand.AddCommand(report.MapReportCmd(
cmd.MapImageCommand(scanImageCommand, scanImage),
reportService
))
}