mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 15:50:15 -08:00
282 lines
7.5 KiB
Go
282 lines
7.5 KiB
Go
package result
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/bmatcuk/doublestar/v4"
|
|
"github.com/package-url/packageurl-go"
|
|
"golang.org/x/xerrors"
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"github.com/aquasecurity/trivy/pkg/clock"
|
|
"github.com/aquasecurity/trivy/pkg/log"
|
|
"github.com/aquasecurity/trivy/pkg/purl"
|
|
)
|
|
|
|
// IgnoreFinding represents an item to be ignored.
|
|
type IgnoreFinding struct {
|
|
// ID is the identifier of the vulnerability, misconfiguration, secret, or license.
|
|
// e.g. CVE-2019-8331, AVD-AWS-0175, etc.
|
|
// required: true
|
|
ID string `yaml:"id"`
|
|
|
|
// Paths is the list of file paths to ignore.
|
|
// If Paths is not set, the ignore finding is applied to all files.
|
|
// required: false
|
|
Paths []string `yaml:"paths"`
|
|
|
|
// PURLs is the list of packages to ignore.
|
|
// If PURLs is not set, the ignore finding is applied to packages.
|
|
// The field is currently available only for vulnerabilities.
|
|
// required: false
|
|
PURLs []*purl.PackageURL `yaml:"-"` // Filled in UnmarshalYAML
|
|
|
|
// ExpiredAt is the expiration date of the ignore finding.
|
|
// If ExpiredAt is not set, the ignore finding is always valid.
|
|
// required: false
|
|
ExpiredAt time.Time `yaml:"expired_at"`
|
|
|
|
// Statement describes the reason for ignoring the finding.
|
|
// required: false
|
|
Statement string `yaml:"statement"`
|
|
}
|
|
|
|
// UnmarshalYAML is a custom unmarshaler for IgnoreFinding that handles
|
|
// the conversion of PURLs from strings to purl.PackageURL objects.
|
|
func (i *IgnoreFinding) UnmarshalYAML(value *yaml.Node) error {
|
|
// Define a shadow type to prevent infinite recursion
|
|
type plain IgnoreFinding
|
|
var tmp struct {
|
|
plain `yaml:",inline"`
|
|
PURLs []string `yaml:"purls"`
|
|
}
|
|
if err := value.Decode(&tmp); err != nil {
|
|
return err
|
|
}
|
|
|
|
*i = IgnoreFinding(tmp.plain)
|
|
|
|
for _, pattern := range i.Paths {
|
|
if !doublestar.ValidatePattern(pattern) {
|
|
return xerrors.Errorf("invalid path pattern in the ignore file, id: %s, path: %s", i.ID, pattern)
|
|
}
|
|
}
|
|
|
|
// Convert string PURLs to purl.PackageURL objects
|
|
for _, purlStr := range tmp.PURLs {
|
|
parsedPURL, err := purl.FromString(purlStr)
|
|
if err != nil {
|
|
return xerrors.Errorf("purl error in the ignore file: %w", err)
|
|
}
|
|
i.PURLs = append(i.PURLs, parsedPURL)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type IgnoreFindings []IgnoreFinding
|
|
|
|
func (f *IgnoreFindings) Match(id, path string, pkg *packageurl.PackageURL) *IgnoreFinding {
|
|
for _, finding := range *f {
|
|
if id != finding.ID {
|
|
continue
|
|
}
|
|
if !matchPath(path, finding.Paths) || !matchPURL(pkg, finding.PURLs) {
|
|
continue
|
|
}
|
|
|
|
log.Debug("Ignored", log.String("id", id), log.String("target", path))
|
|
return &finding
|
|
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func matchPath(path string, patterns []string) bool {
|
|
if len(patterns) == 0 {
|
|
return true
|
|
}
|
|
|
|
for _, pattern := range patterns {
|
|
// Patterns are already validated, so we ignore errors here
|
|
if matched, _ := doublestar.Match(pattern, path); matched {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func matchPURL(target *packageurl.PackageURL, purls []*purl.PackageURL) bool {
|
|
if target == nil || len(purls) == 0 {
|
|
return true
|
|
}
|
|
|
|
for _, p := range purls {
|
|
if p.Match(target) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (f *IgnoreFindings) Prune(ctx context.Context) {
|
|
var findings IgnoreFindings
|
|
for _, finding := range *f {
|
|
// Filter out expired ignore findings
|
|
if !finding.ExpiredAt.IsZero() && finding.ExpiredAt.Before(clock.Now(ctx)) {
|
|
continue
|
|
}
|
|
findings = append(findings, finding)
|
|
}
|
|
*f = findings
|
|
}
|
|
|
|
// IgnoreConfig represents the structure of .trivyignore.yaml.
|
|
type IgnoreConfig struct {
|
|
FilePath string
|
|
Vulnerabilities IgnoreFindings `yaml:"vulnerabilities"`
|
|
Misconfigurations IgnoreFindings `yaml:"misconfigurations"`
|
|
Secrets IgnoreFindings `yaml:"secrets"`
|
|
Licenses IgnoreFindings `yaml:"licenses"`
|
|
}
|
|
|
|
func (c *IgnoreConfig) MatchVulnerability(vulnID, filePath, pkgPath string, pkg *packageurl.PackageURL) *IgnoreFinding {
|
|
paths := []string{
|
|
filePath,
|
|
pkgPath,
|
|
}
|
|
for _, p := range paths {
|
|
if f := c.Vulnerabilities.Match(vulnID, p, pkg); f != nil {
|
|
return f
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *IgnoreConfig) MatchMisconfiguration(misconfID, avdID, filePath string) *IgnoreFinding {
|
|
ids := []string{
|
|
misconfID,
|
|
avdID,
|
|
}
|
|
for _, id := range ids {
|
|
if f := c.Misconfigurations.Match(id, filePath, nil); f != nil {
|
|
return f
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *IgnoreConfig) MatchSecret(secretID, filePath string) *IgnoreFinding {
|
|
return c.Secrets.Match(secretID, filePath, nil)
|
|
}
|
|
|
|
func (c *IgnoreConfig) MatchLicense(licenseID, filePath string) *IgnoreFinding {
|
|
return c.Licenses.Match(licenseID, filePath, nil)
|
|
}
|
|
|
|
func ParseIgnoreFile(ctx context.Context, ignoreFile string) (IgnoreConfig, error) {
|
|
var conf IgnoreConfig
|
|
if _, err := os.Stat(ignoreFile); errors.Is(err, fs.ErrNotExist) {
|
|
// .trivyignore doesn't necessarily exist
|
|
log.Debug("Specified ignore file does not exist", log.String("file", ignoreFile))
|
|
return IgnoreConfig{}, nil
|
|
} else if filepath.Ext(ignoreFile) == ".yml" || filepath.Ext(ignoreFile) == ".yaml" {
|
|
conf, err = parseIgnoreYAML(ignoreFile)
|
|
if err != nil {
|
|
return IgnoreConfig{}, xerrors.Errorf("%s parse error: %w", ignoreFile, err)
|
|
}
|
|
} else {
|
|
ignoredFindings, err := parseIgnore(ignoreFile)
|
|
if err != nil {
|
|
return IgnoreConfig{}, xerrors.Errorf("%s parse error: %w", ignoreFile, err)
|
|
}
|
|
|
|
// IDs in .trivyignore are treated as IDs for all scanners
|
|
// as it is unclear which type of security issue they are
|
|
conf = IgnoreConfig{
|
|
Vulnerabilities: ignoredFindings,
|
|
Misconfigurations: ignoredFindings,
|
|
Secrets: ignoredFindings,
|
|
Licenses: ignoredFindings,
|
|
}
|
|
}
|
|
|
|
conf.Vulnerabilities.Prune(ctx)
|
|
conf.Misconfigurations.Prune(ctx)
|
|
conf.Secrets.Prune(ctx)
|
|
conf.Licenses.Prune(ctx)
|
|
conf.FilePath = filepath.ToSlash(filepath.Clean(ignoreFile))
|
|
|
|
return conf, nil
|
|
}
|
|
|
|
func parseIgnoreYAML(ignoreFile string) (IgnoreConfig, error) {
|
|
// Read .trivyignore.yaml
|
|
b, err := os.ReadFile(ignoreFile)
|
|
if err != nil {
|
|
return IgnoreConfig{}, xerrors.Errorf("file open error: %w", err)
|
|
}
|
|
log.Debug("Found an ignore yaml", log.FilePath(ignoreFile))
|
|
|
|
// Parse the YAML content
|
|
// We have to use Unmarshal() due to go-yaml returning an error with Decode()
|
|
// ref: https://github.com/go-yaml/yaml/issues/805
|
|
var ignoreConfig IgnoreConfig
|
|
if err = yaml.Unmarshal(b, &ignoreConfig); err != nil {
|
|
return IgnoreConfig{}, xerrors.Errorf("yaml decode error: %w", err)
|
|
}
|
|
return ignoreConfig, nil
|
|
}
|
|
|
|
func parseIgnore(ignoreFile string) (IgnoreFindings, error) {
|
|
f, err := os.Open(ignoreFile)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("file open error: %w", err)
|
|
}
|
|
defer f.Close()
|
|
log.Debug("Found an ignore file", log.FilePath(ignoreFile))
|
|
|
|
var ignoredFindings IgnoreFindings
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
line = strings.TrimSpace(line)
|
|
if strings.HasPrefix(line, "#") || line == "" {
|
|
continue
|
|
}
|
|
// Process all fields
|
|
var exp time.Time
|
|
fields := strings.Fields(line)
|
|
if len(fields) > 1 {
|
|
exp, err = getExpirationDate(fields)
|
|
if err != nil {
|
|
log.Warn("Error while parsing expiration date in .trivyignore file", log.Err(err))
|
|
continue
|
|
}
|
|
}
|
|
ignoredFindings = append(ignoredFindings, IgnoreFinding{
|
|
ID: fields[0],
|
|
ExpiredAt: exp,
|
|
})
|
|
}
|
|
|
|
return ignoredFindings, nil
|
|
}
|
|
|
|
func getExpirationDate(fields []string) (time.Time, error) {
|
|
for _, field := range fields {
|
|
if after, ok := strings.CutPrefix(field, "exp:"); ok {
|
|
return time.Parse("2006-01-02", after)
|
|
}
|
|
}
|
|
|
|
return time.Time{}, nil
|
|
}
|