mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-21 23:00:42 -08:00
* feat: adding helm support - adding tests for helm analyzer - add test for non helm tarball - adding in-memory filesystem for helm - handle multiple charts at a time - check the size is smaller than arbitrary size of 200MB if a tarball
345 lines
8.6 KiB
Go
345 lines
8.6 KiB
Go
package analyzer
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io/fs"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"golang.org/x/exp/slices"
|
|
"golang.org/x/sync/semaphore"
|
|
"golang.org/x/xerrors"
|
|
|
|
aos "github.com/aquasecurity/fanal/analyzer/os"
|
|
"github.com/aquasecurity/fanal/log"
|
|
"github.com/aquasecurity/fanal/types"
|
|
dio "github.com/aquasecurity/go-dep-parser/pkg/io"
|
|
)
|
|
|
|
var (
|
|
analyzers = map[Type]analyzer{}
|
|
configAnalyzers = map[Type]configAnalyzer{}
|
|
|
|
// ErrUnknownOS occurs when unknown OS is analyzed.
|
|
ErrUnknownOS = xerrors.New("unknown OS")
|
|
// ErrPkgAnalysis occurs when the analysis of packages is failed.
|
|
ErrPkgAnalysis = xerrors.New("failed to analyze packages")
|
|
// ErrNoPkgsDetected occurs when the required files for an OS package manager are not detected
|
|
ErrNoPkgsDetected = xerrors.New("no packages detected")
|
|
)
|
|
|
|
type AnalysisInput struct {
|
|
Dir string
|
|
FilePath string
|
|
Info os.FileInfo
|
|
Content dio.ReadSeekerAt
|
|
|
|
Options AnalysisOptions
|
|
}
|
|
|
|
type AnalysisOptions struct {
|
|
Offline bool
|
|
}
|
|
|
|
type analyzer interface {
|
|
Type() Type
|
|
Version() int
|
|
Analyze(ctx context.Context, input AnalysisInput) (*AnalysisResult, error)
|
|
Required(filePath string, info os.FileInfo) bool
|
|
}
|
|
|
|
type configAnalyzer interface {
|
|
Type() Type
|
|
Version() int
|
|
Analyze(targetOS types.OS, content []byte) ([]types.Package, error)
|
|
Required(osFound types.OS) bool
|
|
}
|
|
|
|
type Group string
|
|
|
|
const GroupBuiltin Group = "builtin"
|
|
|
|
func RegisterAnalyzer(analyzer analyzer) {
|
|
analyzers[analyzer.Type()] = analyzer
|
|
}
|
|
|
|
func RegisterConfigAnalyzer(analyzer configAnalyzer) {
|
|
configAnalyzers[analyzer.Type()] = analyzer
|
|
}
|
|
|
|
// CustomGroup returns a group name for custom analyzers
|
|
// This is mainly intended to be used in Aqua products.
|
|
type CustomGroup interface {
|
|
Group() Group
|
|
}
|
|
|
|
type Opener func() (dio.ReadSeekCloserAt, error)
|
|
|
|
type AnalyzerGroup struct {
|
|
analyzers []analyzer
|
|
configAnalyzers []configAnalyzer
|
|
}
|
|
|
|
type AnalysisResult struct {
|
|
m sync.Mutex
|
|
OS *types.OS
|
|
Repository *types.Repository
|
|
PackageInfos []types.PackageInfo
|
|
Applications []types.Application
|
|
Secrets []types.Secret
|
|
SystemInstalledFiles []string // A list of files installed by OS package manager
|
|
|
|
Files map[types.HandlerType][]types.File
|
|
|
|
// For Red Hat
|
|
BuildInfo *types.BuildInfo
|
|
|
|
// CustomResources hold analysis results from custom analyzers.
|
|
// It is for extensibility and not used in OSS.
|
|
CustomResources []types.CustomResource
|
|
}
|
|
|
|
func NewAnalysisResult() *AnalysisResult {
|
|
result := new(AnalysisResult)
|
|
result.Files = map[types.HandlerType][]types.File{}
|
|
return result
|
|
}
|
|
|
|
func (r *AnalysisResult) isEmpty() bool {
|
|
return r.OS == nil && r.Repository == nil && len(r.PackageInfos) == 0 && len(r.Applications) == 0 &&
|
|
len(r.Secrets) == 0 && len(r.SystemInstalledFiles) == 0 && r.BuildInfo == nil && len(r.Files) == 0 && len(r.CustomResources) == 0
|
|
}
|
|
|
|
func (r *AnalysisResult) Sort() {
|
|
sort.Slice(r.PackageInfos, func(i, j int) bool {
|
|
return r.PackageInfos[i].FilePath < r.PackageInfos[j].FilePath
|
|
})
|
|
|
|
for _, pi := range r.PackageInfos {
|
|
sort.Slice(pi.Packages, func(i, j int) bool {
|
|
return pi.Packages[i].Name < pi.Packages[j].Name
|
|
})
|
|
}
|
|
|
|
sort.Slice(r.Applications, func(i, j int) bool {
|
|
return r.Applications[i].FilePath < r.Applications[j].FilePath
|
|
})
|
|
|
|
for _, app := range r.Applications {
|
|
sort.Slice(app.Libraries, func(i, j int) bool {
|
|
if app.Libraries[i].Name != app.Libraries[j].Name {
|
|
return app.Libraries[i].Name < app.Libraries[j].Name
|
|
}
|
|
return app.Libraries[i].Version < app.Libraries[j].Version
|
|
})
|
|
}
|
|
|
|
for _, files := range r.Files {
|
|
sort.Slice(files, func(i, j int) bool {
|
|
return files[i].Path < files[j].Path
|
|
})
|
|
}
|
|
|
|
// Secrets
|
|
sort.Slice(r.Secrets, func(i, j int) bool {
|
|
return r.Secrets[i].FilePath < r.Secrets[j].FilePath
|
|
})
|
|
for _, sec := range r.Secrets {
|
|
sort.Slice(sec.Findings, func(i, j int) bool {
|
|
if sec.Findings[i].RuleID != sec.Findings[j].RuleID {
|
|
return sec.Findings[i].RuleID < sec.Findings[j].RuleID
|
|
}
|
|
return sec.Findings[i].StartLine < sec.Findings[j].StartLine
|
|
})
|
|
}
|
|
}
|
|
|
|
func (r *AnalysisResult) Merge(new *AnalysisResult) {
|
|
if new == nil || new.isEmpty() {
|
|
return
|
|
}
|
|
|
|
// this struct is accessed by multiple goroutines
|
|
r.m.Lock()
|
|
defer r.m.Unlock()
|
|
|
|
if new.OS != nil {
|
|
// OLE also has /etc/redhat-release and it detects OLE as RHEL by mistake.
|
|
// In that case, OS must be overwritten with the content of /etc/oracle-release.
|
|
// There is the same problem between Debian and Ubuntu.
|
|
if r.OS == nil || r.OS.Family == aos.RedHat || r.OS.Family == aos.Debian {
|
|
r.OS = new.OS
|
|
}
|
|
}
|
|
|
|
if new.Repository != nil {
|
|
r.Repository = new.Repository
|
|
}
|
|
|
|
if len(new.PackageInfos) > 0 {
|
|
r.PackageInfos = append(r.PackageInfos, new.PackageInfos...)
|
|
}
|
|
|
|
if len(new.Applications) > 0 {
|
|
r.Applications = append(r.Applications, new.Applications...)
|
|
}
|
|
|
|
for t, files := range new.Files {
|
|
if v, ok := r.Files[t]; ok {
|
|
r.Files[t] = append(v, files...)
|
|
} else {
|
|
r.Files[t] = files
|
|
}
|
|
}
|
|
|
|
r.Secrets = append(r.Secrets, new.Secrets...)
|
|
r.SystemInstalledFiles = append(r.SystemInstalledFiles, new.SystemInstalledFiles...)
|
|
|
|
if new.BuildInfo != nil {
|
|
if r.BuildInfo == nil {
|
|
r.BuildInfo = new.BuildInfo
|
|
} else {
|
|
// We don't need to merge build info here
|
|
// because there is theoretically only one file about build info in each layer.
|
|
if new.BuildInfo.Nvr != "" || new.BuildInfo.Arch != "" {
|
|
r.BuildInfo.Nvr = new.BuildInfo.Nvr
|
|
r.BuildInfo.Arch = new.BuildInfo.Arch
|
|
}
|
|
if len(new.BuildInfo.ContentSets) > 0 {
|
|
r.BuildInfo.ContentSets = new.BuildInfo.ContentSets
|
|
}
|
|
}
|
|
}
|
|
|
|
r.CustomResources = append(r.CustomResources, new.CustomResources...)
|
|
}
|
|
|
|
func belongToGroup(groupName Group, analyzerType Type, disabledAnalyzers []Type, analyzer any) bool {
|
|
if slices.Contains(disabledAnalyzers, analyzerType) {
|
|
return false
|
|
}
|
|
|
|
analyzerGroupName := GroupBuiltin
|
|
if cg, ok := analyzer.(CustomGroup); ok {
|
|
analyzerGroupName = cg.Group()
|
|
}
|
|
if analyzerGroupName != groupName {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func NewAnalyzerGroup(groupName Group, disabledAnalyzers []Type) AnalyzerGroup {
|
|
if groupName == "" {
|
|
groupName = GroupBuiltin
|
|
}
|
|
|
|
var group AnalyzerGroup
|
|
for analyzerType, a := range analyzers {
|
|
if !belongToGroup(groupName, analyzerType, disabledAnalyzers, a) {
|
|
continue
|
|
}
|
|
group.analyzers = append(group.analyzers, a)
|
|
}
|
|
|
|
for analyzerType, a := range configAnalyzers {
|
|
if slices.Contains(disabledAnalyzers, analyzerType) {
|
|
continue
|
|
}
|
|
group.configAnalyzers = append(group.configAnalyzers, a)
|
|
}
|
|
|
|
return group
|
|
}
|
|
|
|
// AnalyzerVersions returns analyzer version identifier used for cache keys.
|
|
func (ag AnalyzerGroup) AnalyzerVersions() map[string]int {
|
|
versions := map[string]int{}
|
|
for _, a := range ag.analyzers {
|
|
versions[string(a.Type())] = a.Version()
|
|
}
|
|
return versions
|
|
}
|
|
|
|
// ImageConfigAnalyzerVersions returns analyzer version identifier used for cache keys.
|
|
func (ag AnalyzerGroup) ImageConfigAnalyzerVersions() map[string]int {
|
|
versions := map[string]int{}
|
|
for _, ca := range ag.configAnalyzers {
|
|
versions[string(ca.Type())] = ca.Version()
|
|
}
|
|
return versions
|
|
}
|
|
|
|
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) {
|
|
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) {
|
|
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,
|
|
})
|
|
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
|
|
}
|
|
|
|
func (ag AnalyzerGroup) AnalyzeImageConfig(targetOS types.OS, configBlob []byte) []types.Package {
|
|
for _, d := range ag.configAnalyzers {
|
|
if !d.Required(targetOS) {
|
|
continue
|
|
}
|
|
|
|
pkgs, err := d.Analyze(targetOS, configBlob)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
return pkgs
|
|
}
|
|
return nil
|
|
}
|