启动初始化代码:
基础镜像的解析的初始化代码在analyzer包中。
每种基础镜像通过调用RegisterAnalyzer来将自己的实现实例注册到analyzers哈希表中。因此,如果同一种类型的Analyzer重复调用,前面注册的对象会被覆盖。
获取基础镜像的核心就是analyzer的Analyze函数,数据源来自于对应系统的配置文件,例如Ubuntu为etc/lsb-release
代码如下:
func RegisterAnalyzer(analyzer analyzer) {
analyzers[analyzer.Type()] = analyzer
}
实现的接口为:
type analyzer interface {
Type() Type//类型
Version() int//版本
Analyze(ctx context.Context, input AnalysisInput) (*AnalysisResult, error)//解析函数
Required(filePath string, info os.FileInfo) bool//基本检查,例如该文件是否为对应的os release文件名
}
以Ubuntu为例:
它所对应的基础镜像信息位于/etc/lsb-release文件中。以ubuntu18为例,内容如下:
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.2 LTS"
对应到trivy的源码文件为:trivy/pkg/fanal/analyzer/os/ubuntu/ubuntu.go
代码如下:
package ubuntu
import (
"bufio"
"context"
"os"
"strings"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
aos "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os"
"github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/fanal/utils"
)
func init() {
analyzer.RegisterAnalyzer(&ubuntuOSAnalyzer{})//程序启动时就会执行这里的注册代码,将Ubuntu的基础镜像解析器注册进去
}
const version = 1
var requiredFiles = []string{"etc/lsb-release"}//对应的基础镜像的配置文件路径,可能会有多个文件,所以是个数组
type ubuntuOSAnalyzer struct{}
/*input 包含文件信息*/
func (a ubuntuOSAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) {
isUbuntu := false
scanner := bufio.NewScanner(input.Content)//文件内容
for scanner.Scan() {
line := scanner.Text()
if line == "DISTRIB_ID=Ubuntu" {//对照前面的文件内容,第一行就是DISTRIB_ID=Ubuntu,所以为真,继续解析后面的内容
isUbuntu = true
continue
}
//第二行的内容 "DISTRIB_RELEASE=18.04" ,两个条件同时满足,直接设置AnalysisResult中的OS信息返回
if isUbuntu && strings.HasPrefix(line, "DISTRIB_RELEASE=") {
return &analyzer.AnalysisResult{
OS: &types.OS{
Family: aos.Ubuntu,
Name: strings.TrimSpace(line[16:]),//删除"DISTRIB_RELEASE="
},
}, nil
}
}
return nil, xerrors.Errorf("ubuntu: %w", aos.AnalyzeOSError)
}
func (a ubuntuOSAnalyzer) Required(filePath string, _ os.FileInfo) bool {
return utils.StringInSlice(filePath, requiredFiles)//对比文件是否为etc/lsb-release,对返回true
}
func (a ubuntuOSAnalyzer) Type() analyzer.Type {
return analyzer.TypeUbuntu//返回对应的类型,用作解析器哈希表的key
}
func (a ubuntuOSAnalyzer) Version() int {
return version
}
逻辑非常简单。
trivy的运行模式有很多种,我前面的一个例子使用的是Standalone模式。如果我们对docker镜像进行扫描的话,最后调用imageStandaloneScanner进行扫描。
整体扫描逻辑为:
创建扫描器:run=>NewImageCommand=>artifact.Run=>artifact.NewRunner
执行扫描:run=>NewImageCommand=>artifact.Run=>artifact.ScanImage=>scanArtifact=>ScanArtifact=>artifact.Inspect=>image.inspect=>image.inspectLayer=>analyzer.AnalyzeFile=>analyzer.Required=>analyzer.Analyze
这样就会调用Ubuntu的检查和分析函数最终解析到对应的基础镜像信息。
我们知道docker镜像可以有很多基础镜像,所以这些,会有很多基础镜像解析器注册进来,同时trivy是一个漏扫工具,所以有很多包管理器也会注册进来,所以这个哈希表实际上种类繁多,并不是每次都要用到,所以trivy提供了一个NewAnalyzerGroup接口给我们进行定制化扫描。
下面我们把整体代码过一遍:
入口文件trivy/cmd/trivy/main.go
main函数:
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
main=>run
func run() error {
// Trivy behaves as the specified plugin.
if runAsPlugin := os.Getenv("TRIVY_RUN_AS_PLUGIN"); runAsPlugin != "" {
if !plugin.IsPredefined(runAsPlugin) {
return xerrors.Errorf("unknown plugin: %s", runAsPlugin)
}
if err := plugin.RunWithArgs(context.Background(), runAsPlugin, os.Args[1:]); err != nil {
return xerrors.Errorf("plugin error: %w", err)
}
return nil
}
app := commands.NewApp(version)//重点函数,这里做了一系列初始化和命令行参数解析
if err := app.Execute(); err != nil {//执行命令
return err
}
return nil
}
main=>run=>NewApp
// NewApp is the factory method to return Trivy CLI
func NewApp(version string) *cobra.Command {
globalFlags := flag.NewGlobalFlagGroup()
rootCmd := NewRootCommand(version, globalFlags)
rootCmd.AddCommand(
NewImageCommand(globalFlags),//本地镜像命令创建
NewFilesystemCommand(globalFlags),
NewRootfsCommand(globalFlags),
NewRepositoryCommand(globalFlags),
NewClientCommand(globalFlags),
NewServerCommand(globalFlags),
NewConfigCommand(globalFlags),
NewPluginCommand(),
NewModuleCommand(globalFlags),
NewKubernetesCommand(globalFlags),
NewSBOMCommand(globalFlags),
NewVersionCommand(globalFlags),
)
rootCmd.AddCommand(loadPluginCommands()...)
return rootCmd
}
main=>run=>NewApp=>NewImageCommand
func NewImageCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
reportFlagGroup := flag.NewReportFlagGroup()
reportFlagGroup.DependencyTree = nil // disable '--dependency-tree'
reportFlagGroup.ReportFormat = nil // TODO: support --report summary
imageFlags := &flag.Flags{
CacheFlagGroup: flag.NewCacheFlagGroup(),
DBFlagGroup: flag.NewDBFlagGroup(),
ImageFlagGroup: flag.NewImageFlagGroup(), // container image specific
LicenseFlagGroup: flag.NewLicenseFlagGroup(),
MisconfFlagGroup: flag.NewMisconfFlagGroup(),
RemoteFlagGroup: flag.NewClientFlags(), // for client/server mode
ReportFlagGroup: reportFlagGroup,
ScanFlagGroup: flag.NewScanFlagGroup(),
SecretFlagGroup: flag.NewSecretFlagGroup(),
VulnerabilityFlagGroup: flag.NewVulnerabilityFlagGroup(),
}
cmd := &cobra.Command{
Use: "image [flags] IMAGE_NAME",
Aliases: []string{"i"},
Short: "Scan a container image",
Example: ` # Scan a container image
$ trivy image python:3.4-alpine
# Scan a container image from a tar archive
$ trivy image --input ruby-3.1.tar
# Filter by severities
$ trivy image --severity HIGH,CRITICAL alpine:3.15
# Ignore unfixed/unpatched vulnerabilities
$ trivy image --ignore-unfixed alpine:3.15
# Scan a container image in client mode
$ trivy image --server http://127.0.0.1:4954 alpine:latest
# Generate json result
$ trivy image --format json --output result.json alpine:3.15
# Generate a report in the CycloneDX format
$ trivy image --format cyclonedx --output result.cdx alpine:3.15`,
// 'Args' cannot be used since it is called before PreRunE and viper is not configured yet.
// cmd.Args -> cannot validate args here
// cmd.PreRunE -> configure viper && validate args
// cmd.RunE -> run the command
PreRunE: func(cmd *cobra.Command, args []string) error {
// viper.BindPFlag cannot be called in init(), so it is called in PreRunE.
// cf. https://github.com/spf13/cobra/issues/875
// https://github.com/spf13/viper/issues/233
if err := imageFlags.Bind(cmd); err != nil {
return xerrors.Errorf("flag bind error: %w", err)
}
return validateArgs(cmd, args)
},
RunE: func(cmd *cobra.Command, args []string) error {
options, err := imageFlags.ToOptions(cmd.Version, args, globalFlags, outputWriter)//转换命令参数至options中,如果我们指定的是本地镜像名,最终会保存在options.Target中。
if err != nil {
return xerrors.Errorf("flag error: %w", err)
}
return artifact.Run(cmd.Context(), options, artifact.TargetContainerImage)//对应的扫描执行函数
},
SilenceErrors: true,
SilenceUsage: true,
}
imageFlags.AddFlags(cmd)
cmd.SetFlagErrorFunc(flagErrorFunc)
cmd.SetUsageTemplate(fmt.Sprintf(usageTemplate, imageFlags.Usages(cmd)))
return cmd
}
main=>run=>NewApp=>NewImageCommand=>Run
// Run performs artifact scanning
func Run(ctx context.Context, opts flag.Options, targetKind TargetKind) (err error) {
ctx, cancel := context.WithTimeout(ctx, opts.Timeout)
defer cancel()
defer func() {
if xerrors.Is(err, context.DeadlineExceeded) {
log.Logger.Warn("Increase --timeout value")
}
}()
if opts.GenerateDefaultConfig {
log.Logger.Info("Writing the default config to trivy-default.yaml...")
return viper.SafeWriteConfigAs("trivy-default.yaml")
}
r, err := NewRunner(ctx, opts)//创建扫描器
if err != nil {
if errors.Is(err, SkipScan) {
return nil
}
return xerrors.Errorf("init error: %w", err)
}
defer r.Close(ctx)
var report types.Report
switch targetKind {//根据参数类型选择执行的函数,我们走第一个分支
case TargetContainerImage, TargetImageArchive:
if report, err = r.ScanImage(ctx, opts); err != nil {//扫描镜像
return xerrors.Errorf("image scan error: %w", err)
}
case TargetFilesystem:
if report, err = r.ScanFilesystem(ctx, opts); err != nil {
return xerrors.Errorf("filesystem scan error: %w", err)
}
case TargetRootfs:
if report, err = r.ScanRootfs(ctx, opts); err != nil {
return xerrors.Errorf("rootfs scan error: %w", err)
}
case TargetRepository:
if report, err = r.ScanRepository(ctx, opts); err != nil {
return xerrors.Errorf("repository scan error: %w", err)
}
case TargetSBOM:
if report, err = r.ScanSBOM(ctx, opts); err != nil {
return xerrors.Errorf("sbom scan error: %w", err)
}
}
report, err = r.Filter(ctx, opts, report)
if err != nil {
return xerrors.Errorf("filter error: %w", err)
}
if err = r.Report(opts, report); err != nil {
return xerrors.Errorf("report error: %w", err)
}
Exit(opts, report.Results.Failed())
return nil
}
main=>run=>NewApp=>NewImageCommand=>Run=>NewRunner
// NewRunner initializes Runner that provides scanning functionalities.
// It is possible to return SkipScan and it must be handled by caller.
func NewRunner(ctx context.Context, cliOptions flag.Options, opts ...runnerOption) (Runner, error) {
r := &runner{}
for _, opt := range opts {
opt(r)
}
if err := r.initCache(cliOptions); err != nil {//初始化缓存,还未研究
return nil, xerrors.Errorf("cache error: %w", err)
}
// Update the vulnerability database if needed.
if err := r.initDB(cliOptions); err != nil {//更新漏洞库,还未研究
return nil, xerrors.Errorf("DB error: %w", err)
}
// Initialize WASM modules
m, err := module.NewManager(ctx)
if err != nil {
return nil, xerrors.Errorf("WASM module error: %w", err)
}
m.Register()
r.module = m
return r, nil
}
main=>run=>NewApp=>NewImageCommand=>Run=>ScanImage
func (r *runner) ScanImage(ctx context.Context, opts flag.Options) (types.Report, error) {
// Disable the lock file scanning
opts.DisabledAnalyzers = analyzer.TypeLockfiles
var s InitializeScanner
switch {
case opts.Input != "" && opts.ServerAddr == "":
// Scan image tarball in standalone mode
s = archiveStandaloneScanner
case opts.Input != "" && opts.ServerAddr != "":
// Scan image tarball in client/server mode
s = archiveRemoteScanner
case opts.Input == "" && opts.ServerAddr == ""://我们没有指定镜像的tar包或者地址,走该分支
// Scan container image in standalone mode
s = imageStandaloneScanner
case opts.Input == "" && opts.ServerAddr != "":
// Scan container image in client/server mode
s = imageRemoteScanner
}
return r.scanArtifact(ctx, opts, s)//初始化(加载镜像。。。),启动扫描
}
main=>run=>NewApp=>NewImageCommand=>Run=>ScanImage=>imageStandaloneScanner
// imageStandaloneScanner initializes a container image scanner in standalone mode
// $ trivy image alpine:3.15
func imageStandaloneScanner(ctx context.Context, conf ScannerConfig) (scanner.Scanner, func(), error) {
dockerOpt, err := types.GetDockerOption(conf.ArtifactOption.InsecureSkipTLS)//docker选项解析
if err != nil {
return scanner.Scanner{}, nil, err
}
s, cleanup, err := initializeDockerScanner(ctx, conf.Target, conf.ArtifactCache, conf.LocalArtifactCache,
dockerOpt, conf.ArtifactOption)//加载镜像,conf.Target为镜像名或镜像id
if err != nil {
return scanner.Scanner{}, func() {}, xerrors.Errorf("unable to initialize a docker scanner: %w", err)//根据配置创建walker和analyzer(分析器)
}
return s, cleanup, nil
}
main=>run=>NewApp=>NewImageCommand=>Run=>ScanImage=>imageStandaloneScanner=>NewContainerImage
/*基于google go-containerregistry和 docker cli 进行操作*/
func NewContainerImage(ctx context.Context, imageName string, option types.DockerOption, opts ...Option) (types.Image, func(), error) {
o := &options{
dockerd: true,
podman: true,
containerd: true,
remote: true,
}
for _, opt := range opts {
opt(o)
}
var errs error
var nameOpts []name.Option
if option.NonSSL {
nameOpts = append(nameOpts, name.Insecure)
}
ref, err := name.ParseReference(imageName, nameOpts...)//解析镜像名
if err != nil {
return nil, func() {}, xerrors.Errorf("failed to parse the image name: %w", err)
}
// Try accessing Docker Daemon
if o.dockerd {//连接docker daemon,如果访问成功直接返回对应的镜像结构,后面解析时会用到这个结构
img, cleanup, err := tryDockerDaemon(imageName, ref)
if err == nil {
// Return v1.Image if the image is found in Docker Engine
return img, cleanup, nil
}
errs = multierror.Append(errs, err)
}
// Try accessing Podman
if o.podman {
img, cleanup, err := tryPodmanDaemon(imageName)
if err == nil {
// Return v1.Image if the image is found in Podman
return img, cleanup, nil
}
errs = multierror.Append(errs, err)
}
// Try containerd
if o.containerd {
img, cleanup, err := tryContainerdDaemon(ctx, imageName)
if err == nil {
// Return v1.Image if the image is found in containerd
return img, cleanup, nil
}
errs = multierror.Append(errs, err)
}
// Try accessing Docker Registry
if o.remote {
img, err := tryRemote(ctx, imageName, ref, option)
if err == nil {
// Return v1.Image if the image is found in a remote registry
return img, func() {}, nil
}
errs = multierror.Append(errs, err)
}
return nil, func() {}, errs
}
main=>run=>NewApp=>NewImageCommand=>Run=>ScanImage=>imageStandaloneScanner=>NewArtifact
func NewArtifact(img types.Image, c cache.ArtifactCache, opt artifact.Option) (artifact.Artifact, error) {
misconf := opt.MisconfScannerOption
// Register config analyzers
if err := config.RegisterConfigAnalyzers(misconf.FilePatterns); err != nil {
return nil, xerrors.Errorf("config scanner error: %w", err)
}
// Initialize handlers
handlerManager, err := handler.NewManager(opt)
if err != nil {
return nil, xerrors.Errorf("handler init error: %w", err)
}
// Register secret analyzer
if err = secret.RegisterSecretAnalyzer(opt.SecretScannerOption); err != nil {
return nil, xerrors.Errorf("secret scanner error: %w", err)
}
return Artifact{
image: img,
cache: c,
walker: walker.NewLayerTar(opt.SkipFiles, opt.SkipDirs),//遍历layer器
analyzer: analyzer.NewAnalyzerGroup(opt.AnalyzerGroup, opt.DisabledAnalyzers),//分析器
handlerManager: handlerManager,
artifactOption: opt,
}, nil
}
main=>run=>NewApp=>NewImageCommand=>Run=>ScanImage=>imageStandaloneScanner=>NewScanner
// NewScanner is the factory method of Scanner
//这里将前面创建的local scanner作为driver传进来,这个Driver定义了一个函数需要实现即Scan
func NewScanner(driver Driver, ar artifact.Artifact) Scanner {
return Scanner{driver: driver, artifact: ar}
}
//定义如下:
// Driver defines operations of scanner
type Driver interface {
Scan(ctx context.Context, target, artifactKey string, blobKeys []string, options types.ScanOptions) (
results types.Results, osFound *ftypes.OS, err error)
}
我们回过头去看localscanner的Scan函数的定义:
// Scan scans the artifact and return results.
func (s Scanner) Scan(ctx context.Context, target, artifactKey string, blobKeys []string, options types.ScanOptions) (types.Results, *ftypes.OS, error) {
artifactDetail, err := s.applier.ApplyLayers(artifactKey, blobKeys)
switch {
case errors.Is(err, analyzer.ErrUnknownOS):
log.Logger.Debug("OS is not detected.")
// If OS is not detected and repositories are detected, we'll try to use repositories as OS.
if artifactDetail.Repository != nil {
log.Logger.Debugf("Package repository: %s %s", artifactDetail.Repository.Family, artifactDetail.Repository.Release)
log.Logger.Debugf("Assuming OS is %s %s.", artifactDetail.Repository.Family, artifactDetail.Repository.Release)
artifactDetail.OS = &ftypes.OS{
Family: artifactDetail.Repository.Family,
Name: artifactDetail.Repository.Release,
}
}
case errors.Is(err, analyzer.ErrNoPkgsDetected):
log.Logger.Warn("No OS package is detected. Make sure you haven't deleted any files that contain information about the installed packages.")
log.Logger.Warn(`e.g. files under "/lib/apk/db/", "/var/lib/dpkg/" and "/var/lib/rpm"`)
case err != nil:
return nil, nil, xerrors.Errorf("failed to apply layers: %w", err)
}
var eosl bool
var results types.Results
// Scan OS packages and language-specific dependencies
if slices.Contains(options.SecurityChecks, types.SecurityCheckVulnerability) {
var vulnResults types.Results
vulnResults, eosl, err = s.checkVulnerabilities(target, artifactDetail, options)
if err != nil {
return nil, nil, xerrors.Errorf("failed to detect vulnerabilities: %w", err)
}
if artifactDetail.OS != nil {
artifactDetail.OS.Eosl = eosl
}
results = append(results, vulnResults...)
}
// Scan IaC config files
if shouldScanMisconfig(options.SecurityChecks) {
configResults := s.misconfsToResults(artifactDetail.Misconfigurations)
results = append(results, configResults...)
}
// Scan secrets
if slices.Contains(options.SecurityChecks, types.SecurityCheckSecret) {
secretResults := s.secretsToResults(artifactDetail.Secrets)
results = append(results, secretResults...)
}
// For WASM plugins and custom analyzers
if len(artifactDetail.CustomResources) != 0 {
results = append(results, types.Result{
Class: types.ClassCustom,
CustomResources: artifactDetail.CustomResources,
})
}
// Scan licenses
if slices.Contains(options.SecurityChecks, types.SecurityCheckLicense) {
licenseResults := s.scanLicenses(artifactDetail, options.LicenseCategories)
results = append(results, licenseResults...)
}
for i := range results {
// Fill vulnerability details
s.vulnClient.FillInfo(results[i].Vulnerabilities)
}
// Post scanning
results, err = post.Scan(ctx, results)
if err != nil {
return nil, nil, xerrors.Errorf("post scan error: %w", err)
}
return results, artifactDetail.OS, nil
}
我们接着往下看ScanImage,最后调用了r.scanArtifact:
main=>run=>NewApp=>NewImageCommand=>Run=>ScanImage
func (r *runner) ScanImage(ctx context.Context, opts flag.Options) (types.Report, error) {
// Disable the lock file scanning
opts.DisabledAnalyzers = analyzer.TypeLockfiles
var s InitializeScanner
switch {
case opts.Input != "" && opts.ServerAddr == "":
// Scan image tarball in standalone mode
s = archiveStandaloneScanner
case opts.Input != "" && opts.ServerAddr != "":
// Scan image tarball in client/server mode
s = archiveRemoteScanner
case opts.Input == "" && opts.ServerAddr == "":
// Scan container image in standalone mode
s = imageStandaloneScanner
case opts.Input == "" && opts.ServerAddr != "":
// Scan container image in client/server mode
s = imageRemoteScanner
}
return r.scanArtifact(ctx, opts, s)
}
main=>run=>NewApp=>NewImageCommand=>Run=>ScanImage=>scanArtifact
func (r *runner) scanArtifact(ctx context.Context, opts flag.Options, initializeScanner InitializeScanner) (types.Report, error) {
report, err := scan(ctx, opts, initializeScanner, r.cache)
if err != nil {
return types.Report{}, xerrors.Errorf("scan error: %w", err)
}
return report, nil
}
main=>run=>NewApp=>NewImageCommand=>Run=>ScanImage=>scanArtifact=>scan
func scan(ctx context.Context, opts flag.Options, initializeScanner InitializeScanner, cacheClient cache.Cache) (
types.Report, error) {
scannerConfig, scanOptions, err := initScannerConfig(opts, cacheClient)//解析配置,重点关注Target
if err != nil {
return types.Report{}, err
}
s, cleanup, err := initializeScanner(ctx, scannerConfig)//调用imageStandaloneScanner,我们前面已经分析过了
if err != nil {
return types.Report{}, xerrors.Errorf("unable to initialize a scanner: %w", err)
}
defer cleanup()
report, err := s.ScanArtifact(ctx, scanOptions)//万事俱备,执行扫描
if err != nil {
return types.Report{}, xerrors.Errorf("image scan failed: %w", err)
}
return report, nil
}
main=>run=>NewApp=>NewImageCommand=>Run=>ScanImage=>scanArtifact=>scan=>ScanArtifact
// ScanArtifact scans the artifacts and returns results
func (s Scanner) ScanArtifact(ctx context.Context, options types.ScanOptions) (types.Report, error) {
artifactInfo, err := s.artifact.Inspect(ctx)//执行扫描,基础镜像就通过此被执行的。
if err != nil {
return types.Report{}, xerrors.Errorf("failed analysis: %w", err)
}
defer func() {
if err := s.artifact.Clean(artifactInfo); err != nil {
log.Logger.Warnf("Failed to clean the artifact %q: %v", artifactInfo.Name, err)
}
}()
results, osFound, err := s.driver.Scan(ctx, artifactInfo.Name, artifactInfo.ID, artifactInfo.BlobIDs, options)//前面的local scanner的Scan在此执行
if err != nil {
return types.Report{}, xerrors.Errorf("scan failed: %w", err)
}
if osFound != nil && osFound.Eosl {
log.Logger.Warnf("This OS version is no longer supported by the distribution: %s %s", osFound.Family, osFound.Name)
log.Logger.Warnf("The vulnerability detection may be insufficient because security updates are not provided")
}
// Layer makes sense only when scanning container images
if artifactInfo.Type != ftypes.ArtifactContainerImage {
removeLayer(results)
}
return types.Report{
SchemaVersion: report.SchemaVersion,
ArtifactName: artifactInfo.Name,
ArtifactType: artifactInfo.Type,
Metadata: types.Metadata{
OS: osFound,
// Container image
ImageID: artifactInfo.ImageMetadata.ID,
DiffIDs: artifactInfo.ImageMetadata.DiffIDs,
RepoTags: artifactInfo.ImageMetadata.RepoTags,
RepoDigests: artifactInfo.ImageMetadata.RepoDigests,
ImageConfig: artifactInfo.ImageMetadata.ConfigFile,
},
CycloneDX: artifactInfo.CycloneDX,
Results: results,
}, nil//返回报告
}
main=>run=>NewApp=>NewImageCommand=>Run=>ScanImage=>scanArtifact=>scan=>ScanArtifact=>Inspect
func (a Artifact) Inspect(ctx context.Context) (types.ArtifactReference, error) {
imageID, err := a.image.ID()//镜像ID
if err != nil {
return types.ArtifactReference{}, xerrors.Errorf("unable to get the image ID: %w", err)
}
diffIDs, err := a.image.LayerIDs()
if err != nil {
return types.ArtifactReference{}, xerrors.Errorf("unable to get layer IDs: %w", err)
}
configFile, err := a.image.ConfigFile()
if err != nil {
return types.ArtifactReference{}, xerrors.Errorf("unable to get the image's config file: %w", err)
}
// Debug
log.Logger.Debugf("Image ID: %s", imageID)
log.Logger.Debugf("Diff IDs: %v", diffIDs)
// Try to detect base layers.
baseDiffIDs := a.guessBaseLayers(diffIDs, configFile)
log.Logger.Debugf("Base Layers: %v", baseDiffIDs)
// Convert image ID and layer IDs to cache keys
imageKey, layerKeys, layerKeyMap, err := a.calcCacheKeys(imageID, diffIDs)
if err != nil {
return types.ArtifactReference{}, err
}
missingImage, missingLayers, err := a.cache.MissingBlobs(imageKey, layerKeys)
if err != nil {
return types.ArtifactReference{}, xerrors.Errorf("unable to get missing layers: %w", err)
}
missingImageKey := imageKey
if missingImage {
log.Logger.Debugf("Missing image ID in cache: %s", imageID)
} else {
missingImageKey = ""
}
//执行扫描
if err = a.inspect(ctx, missingImageKey, missingLayers, baseDiffIDs, layerKeyMap); err != nil {
return types.ArtifactReference{}, xerrors.Errorf("analyze error: %w", err)
}
return types.ArtifactReference{
Name: a.image.Name(),
Type: types.ArtifactContainerImage,
ID: imageKey,
BlobIDs: layerKeys,
ImageMetadata: types.ImageMetadata{
ID: imageID,
DiffIDs: diffIDs,
RepoTags: a.image.RepoTags(),
RepoDigests: a.image.RepoDigests(),
ConfigFile: *configFile,
},
}, nil
}
main=>run=>NewApp=>NewImageCommand=>Run=>ScanImage=>scanArtifact=>scan=>ScanArtifact=>Inspect=>inspect
func (a Artifact) inspect(ctx context.Context, missingImage string, layerKeys, baseDiffIDs []string, layerKeyMap map[string]string) error {
done := make(chan struct{})
errCh := make(chan error)
var osFound types.OS
for _, k := range layerKeys {
go func(ctx context.Context, layerKey string) {
diffID := layerKeyMap[layerKey]
// If it is a base layer, secret scanning should not be performed.
var disabledAnalyers []analyzer.Type
if slices.Contains(baseDiffIDs, diffID) {
disabledAnalyers = append(disabledAnalyers, analyzer.TypeSecret)
}
layerInfo, err := a.inspectLayer(ctx, diffID, disabledAnalyers)//扫描
if err != nil {
errCh <- xerrors.Errorf("failed to analyze layer: %s : %w", diffID, err)
return
}
if err = a.cache.PutBlob(layerKey, layerInfo); err != nil {
errCh <- xerrors.Errorf("failed to store layer: %s in cache: %w", layerKey, err)
return
}
if layerInfo.OS != nil {
osFound = *layerInfo.OS
}
done <- struct{}{}
}(ctx, k)
}
for range layerKeys {
select {
case <-done:
case err := <-errCh:
return err
case <-ctx.Done():
return xerrors.Errorf("timeout: %w", ctx.Err())
}
}
if missingImage != "" {
if err := a.inspectConfig(missingImage, osFound); err != nil {
return xerrors.Errorf("unable to analyze config: %w", err)
}
}
return nil
}
main=>run=>NewApp=>NewImageCommand=>Run=>ScanImage=>scanArtifact=>scan=>ScanArtifact=>Inspect=>inspect=>inspectLayer
func (a Artifact) inspectLayer(ctx context.Context, diffID string, disabled []analyzer.Type) (types.BlobInfo, error) {
log.Logger.Debugf("Missing diff ID in cache: %s", diffID)
layerDigest, r, err := a.uncompressedLayer(diffID)//解压镜像的层
if err != nil {
return types.BlobInfo{}, xerrors.Errorf("unable to get uncompressed layer %s: %w", diffID, err)
}
// Prepare variables
var wg sync.WaitGroup
opts := analyzer.AnalysisOptions{Offline: a.artifactOption.Offline}
result := analyzer.NewAnalysisResult()
limit := semaphore.NewWeighted(parallel)
// Walk a tar layer
opqDirs, whFiles, err := a.walker.Walk(r, func(filePath string, info os.FileInfo, opener analyzer.Opener) error {
if err = a.analyzer.AnalyzeFile(ctx, &wg, limit, result, "", filePath, info, opener, disabled, opts); err != nil {//执行扫描
return xerrors.Errorf("failed to analyze %s: %w", filePath, err)
}
return nil
})
if err != nil {
return types.BlobInfo{}, xerrors.Errorf("walk error: %w", err)
}
// Wait for all the goroutine to finish.
wg.Wait()
// Sort the analysis result for consistent results
result.Sort()
blobInfo := types.BlobInfo{
SchemaVersion: types.BlobJSONSchemaVersion,
Digest: layerDigest,
DiffID: diffID,
OS: result.OS,
Repository: result.Repository,
PackageInfos: result.PackageInfos,
Applications: result.Applications,
Secrets: result.Secrets,
Licenses: result.Licenses,
OpaqueDirs: opqDirs,
WhiteoutFiles: whFiles,
CustomResources: result.CustomResources,
// For Red Hat
BuildInfo: result.BuildInfo,
}
// Call post handlers to modify blob info
if err = a.handlerManager.PostHandle(ctx, result, &blobInfo); err != nil {
return types.BlobInfo{}, xerrors.Errorf("post handler error: %w", err)
}
return blobInfo, nil
}
main=>run=>NewApp=>NewImageCommand=>Run=>ScanImage=>scanArtifact=>scan=>ScanArtifact=>Inspect=>inspect=>inspectLayer=>Walk
//analyzeFn 执行解析os release的函数指针
func (w LayerTar) Walk(layer io.Reader, analyzeFn WalkFunc) ([]string, []string, error) {
var opqDirs, whFiles, skipDirs []string
tr := tar.NewReader(layer)//读取tar包
for {
hdr, err := tr.Next()//遍历tar中的文件或目录
if err == io.EOF {
break
} else if err != nil {
return nil, nil, xerrors.Errorf("failed to extract the archive: %w", err)
}
filePath := hdr.Name
filePath = strings.TrimLeft(filepath.Clean(filePath), "/")
fileDir, fileName := filepath.Split(filePath)
// e.g. etc/.wh..wh..opq
if opq == fileName {
opqDirs = append(opqDirs, fileDir)
continue
}
// etc/.wh.hostname
if strings.HasPrefix(fileName, wh) {
name := strings.TrimPrefix(fileName, wh)
fpath := filepath.Join(fileDir, name)
whFiles = append(whFiles, fpath)
continue
}
switch hdr.Typeflag {
case tar.TypeDir:
if w.shouldSkipDir(filePath) {
skipDirs = append(skipDirs, filePath)
continue
}
case tar.TypeSymlink, tar.TypeLink, tar.TypeReg:
if w.shouldSkipFile(filePath) {
continue
}
default:
continue
}
if underSkippedDir(filePath, skipDirs) {
continue
}
// A symbolic/hard link or regular file will reach here.
if err = w.processFile(filePath, tr, hdr.FileInfo(), analyzeFn); err != nil {//处理文件
return nil, nil, xerrors.Errorf("failed to process the file: %w", err)
}
}
return opqDirs, whFiles, nil
}
main=>run=>NewApp=>NewImageCommand=>Run=>ScanImage=>scanArtifact=>scan=>ScanArtifact=>Inspect=>inspect=>inspectLayer=>Walk=>processFile
func (w LayerTar) processFile(filePath string, tr *tar.Reader, fi fs.FileInfo, analyzeFn WalkFunc) error {
tf := newTarFile(fi.Size(), tr)
defer func() {
// nolint
_ = tf.Clean()
}()
//传入的函数指针为匿名哈数,里面调用的对应的是我们前面看到a.analyzer.AnalyzeFile,下面我们看看具体实现
if err := analyzeFn(filePath, fi, tf.Open); err != nil {
return xerrors.Errorf("failed to analyze file: %w", err)
}
return nil
}
main=>run=>NewApp=>NewImageCommand=>Run=>ScanImage=>scanArtifact=>scan=>ScanArtifact=>Inspect=>inspect=>inspectLayer=>Walk=>processFile=>AnalyzeFile
func (ag AnalyzerGroup) AnalyzeFile(ctx context.Context, wg *sync.WaitGroup, limit *semaphore.Weighted, result *AnalysisResult,
dir, filePath string, info os.FileInfo, opener Opener, disabled []Type, opts AnalysisOptions) error {
if info.IsDir() {
return nil
}
//遍历所有分析器,一个一个执行
for _, a := range ag.analyzers {
// Skip disabled analyzers
if slices.Contains(disabled, a.Type()) {//是否禁用了某些类型,是则跳过
continue
}
// filepath extracted from tar file doesn't have the prefix "/"
if !a.Required(strings.TrimLeft(filePath, "/"), info) {//调用Required检查文件名是否是对应的分析器的所指定的文件,在我们的这个例子中为etc/lsb-release,检查通过,继续
continue
}
rc, err := opener()
if errors.Is(err, fs.ErrPermission) {
log.Logger.Debugf("Permission error: %s", filePath)
break
} else if err != nil {
return xerrors.Errorf("unable to open %s: %w", filePath, err)
}
if err = limit.Acquire(ctx, 1); err != nil {//获取信号量,进行并发控制
return xerrors.Errorf("semaphore acquire: %w", err)
}
wg.Add(1)
go func(a analyzer, rc dio.ReadSeekCloserAt) {//启动 goroutine,执行分析器
defer limit.Release(1)
defer wg.Done()
defer rc.Close()
ret, err := a.Analyze(ctx, AnalysisInput{
Dir: dir,
FilePath: filePath,
Info: info,
Content: rc,
Options: opts,
})//执行分析函数,对应我们的就是ubuntuOSAnalyzer.Analyze函数
if err != nil && !xerrors.Is(err, aos.AnalyzeOSError) {
log.Logger.Debugf("Analysis error: %s", err)
return
}
if ret != nil {
result.Merge(ret)
}
}(a, rc)
}
return nil
}
至此整个代码的基本流程就讲完了。