Files
trivy/pkg/fanal/applier/docker.go
Teppei Fukuda 3eecfc6b6e refactor: unify Library and Package structs (#6633)
Signed-off-by: knqyf263 <knqyf263@gmail.com>
Co-authored-by: DmitriyLewen <91113035+DmitriyLewen@users.noreply.github.com>
Co-authored-by: DmitriyLewen <dmitriy.lewen@smartforce.io>
2024-05-07 12:25:52 +00:00

339 lines
9.8 KiB
Go

package applier
import (
"fmt"
"strings"
"time"
"github.com/knqyf263/nested"
"github.com/mitchellh/hashstructure/v2"
"github.com/package-url/packageurl-go"
"github.com/samber/lo"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/purl"
"github.com/aquasecurity/trivy/pkg/types"
)
type Config struct {
ContainerConfig containerConfig `json:"container_config"`
History []History
}
type containerConfig struct {
Env []string
}
type History struct {
Created time.Time
CreatedBy string `json:"created_by"`
}
func containsPackage(e ftypes.Package, s []ftypes.Package) bool {
for _, a := range s {
if a.Name == e.Name && a.Version == e.Version && a.Release == e.Release {
return true
}
}
return false
}
func lookupOriginLayerForPkg(pkg ftypes.Package, layers []ftypes.BlobInfo) (string, string, *ftypes.BuildInfo) {
for i, layer := range layers {
for _, info := range layer.PackageInfos {
if containsPackage(pkg, info.Packages) {
return layer.Digest, layer.DiffID, lookupBuildInfo(i, layers)
}
}
}
return "", "", nil
}
// lookupBuildInfo looks up Red Hat content sets from all layers
func lookupBuildInfo(index int, layers []ftypes.BlobInfo) *ftypes.BuildInfo {
if layers[index].BuildInfo != nil {
return layers[index].BuildInfo
}
// Base layer (layers[0]) is missing content sets
// - it needs to be shared from layers[1]
if index == 0 {
if len(layers) > 1 {
return layers[1].BuildInfo
}
return nil
}
// Customer's layers build on top of Red Hat image are also missing content sets
// - it needs to be shared from the last Red Hat's layers which contains content sets
for i := index - 1; i >= 1; i-- {
if layers[i].BuildInfo != nil {
return layers[i].BuildInfo
}
}
return nil
}
func lookupOriginLayerForLib(filePath string, lib ftypes.Package, layers []ftypes.BlobInfo) (string, string) {
for _, layer := range layers {
for _, layerApp := range layer.Applications {
if filePath != layerApp.FilePath {
continue
}
if containsPackage(lib, layerApp.Packages) {
return layer.Digest, layer.DiffID
}
}
}
return "", ""
}
// ApplyLayers returns the merged layer
// nolint: gocyclo
func ApplyLayers(layers []ftypes.BlobInfo) ftypes.ArtifactDetail {
sep := "/"
nestedMap := nested.Nested{}
secretsMap := make(map[string]ftypes.Secret)
var mergedLayer ftypes.ArtifactDetail
for _, layer := range layers {
for _, opqDir := range layer.OpaqueDirs {
opqDir = strings.TrimSuffix(opqDir, sep) // this is necessary so that an empty element is not contribute into the array of the DeleteByString function
_ = nestedMap.DeleteByString(opqDir, sep) // nolint
}
for _, whFile := range layer.WhiteoutFiles {
_ = nestedMap.DeleteByString(whFile, sep) // nolint
}
mergedLayer.OS.Merge(layer.OS)
if layer.Repository != nil {
mergedLayer.Repository = layer.Repository
}
// Apply OS packages
for _, pkgInfo := range layer.PackageInfos {
key := fmt.Sprintf("%s/type:ospkg", pkgInfo.FilePath)
nestedMap.SetByString(key, sep, pkgInfo)
}
// Apply language-specific packages
for _, app := range layer.Applications {
key := fmt.Sprintf("%s/type:%s", app.FilePath, app.Type)
nestedMap.SetByString(key, sep, app)
}
// Apply misconfigurations
for _, config := range layer.Misconfigurations {
config.Layer = ftypes.Layer{
Digest: layer.Digest,
DiffID: layer.DiffID,
}
key := fmt.Sprintf("%s/type:config", config.FilePath)
nestedMap.SetByString(key, sep, config)
}
// Apply secrets
for _, secret := range layer.Secrets {
l := ftypes.Layer{
Digest: layer.Digest,
DiffID: layer.DiffID,
CreatedBy: layer.CreatedBy,
}
secretsMap = mergeSecrets(secretsMap, secret, l)
}
// Apply license files
for _, license := range layer.Licenses {
license.Layer = ftypes.Layer{
Digest: layer.Digest,
DiffID: layer.DiffID,
}
key := fmt.Sprintf("%s/type:license,%s", license.FilePath, license.Type)
nestedMap.SetByString(key, sep, license)
}
// Apply custom resources
for _, customResource := range layer.CustomResources {
key := fmt.Sprintf("%s/custom:%s", customResource.FilePath, customResource.Type)
customResource.Layer = ftypes.Layer{
Digest: layer.Digest,
DiffID: layer.DiffID,
}
nestedMap.SetByString(key, sep, customResource)
}
}
// nolint
_ = nestedMap.Walk(func(keys []string, value interface{}) error {
switch v := value.(type) {
case ftypes.PackageInfo:
mergedLayer.Packages = append(mergedLayer.Packages, v.Packages...)
case ftypes.Application:
mergedLayer.Applications = append(mergedLayer.Applications, v)
case ftypes.Misconfiguration:
mergedLayer.Misconfigurations = append(mergedLayer.Misconfigurations, v)
case ftypes.LicenseFile:
mergedLayer.Licenses = append(mergedLayer.Licenses, v)
case ftypes.CustomResource:
mergedLayer.CustomResources = append(mergedLayer.CustomResources, v)
}
return nil
})
for _, s := range secretsMap {
mergedLayer.Secrets = append(mergedLayer.Secrets, s)
}
// Extract dpkg licenses
// The license information is not stored in the dpkg database and in a separate file,
// so we have to merge the license information into the package.
dpkgLicenses := make(map[string][]string)
mergedLayer.Licenses = lo.Reject(mergedLayer.Licenses, func(license ftypes.LicenseFile, _ int) bool {
if license.Type != ftypes.LicenseTypeDpkg {
return false
}
// e.g.
// "adduser" => {"GPL-2"}
// "openssl" => {"MIT", "BSD"}
dpkgLicenses[license.PkgName] = lo.Map(license.Findings, func(finding ftypes.LicenseFinding, _ int) string {
return finding.Name
})
// Remove this license in the merged result as it is merged into the package information.
return true
})
if len(mergedLayer.Licenses) == 0 {
mergedLayer.Licenses = nil
}
for i, pkg := range mergedLayer.Packages {
// Skip lookup for SBOM
if lo.IsEmpty(pkg.Layer) {
originLayerDigest, originLayerDiffID, buildInfo := lookupOriginLayerForPkg(pkg, layers)
mergedLayer.Packages[i].Layer = ftypes.Layer{
Digest: originLayerDigest,
DiffID: originLayerDiffID,
}
mergedLayer.Packages[i].BuildInfo = buildInfo
}
if mergedLayer.OS.Family != "" {
mergedLayer.Packages[i].Identifier.PURL = newPURL(mergedLayer.OS.Family, types.Metadata{OS: &mergedLayer.OS}, pkg)
}
mergedLayer.Packages[i].Identifier.UID = calcPkgUID("", pkg)
// Only debian packages
if licenses, ok := dpkgLicenses[pkg.Name]; ok {
mergedLayer.Packages[i].Licenses = licenses
}
}
for _, app := range mergedLayer.Applications {
for i, pkg := range app.Packages {
// Skip lookup for SBOM
if lo.IsEmpty(pkg.Layer) {
originLayerDigest, originLayerDiffID := lookupOriginLayerForLib(app.FilePath, pkg, layers)
app.Packages[i].Layer = ftypes.Layer{
Digest: originLayerDigest,
DiffID: originLayerDiffID,
}
}
if pkg.Identifier.PURL == nil {
app.Packages[i].Identifier.PURL = newPURL(app.Type, types.Metadata{}, pkg)
}
app.Packages[i].Identifier.UID = calcPkgUID(app.FilePath, pkg)
}
}
// Aggregate python/ruby/node.js packages and JAR files
aggregate(&mergedLayer)
return mergedLayer
}
func newPURL(pkgType ftypes.TargetType, metadata types.Metadata, pkg ftypes.Package) *packageurl.PackageURL {
p, err := purl.New(pkgType, metadata, pkg)
if err != nil {
log.Error("Failed to create PackageURL", log.Err(err))
return nil
}
return p.Unwrap()
}
// calcPkgUID calculates the hash of the package for the unique ID
func calcPkgUID(filePath string, pkg ftypes.Package) string {
v := map[string]any{
"filePath": filePath, // To differentiate the hash of the same package but different file path
"pkg": pkg,
}
hash, err := hashstructure.Hash(v, hashstructure.FormatV2, &hashstructure.HashOptions{
ZeroNil: true,
IgnoreZeroValue: true,
})
if err != nil {
log.Warn("Failed to calculate the package hash", log.String("pkg", pkg.Name), log.Err(err))
}
return fmt.Sprintf("%x", hash)
}
// aggregate merges all packages installed by pip/gem/npm/jar/conda into each application
func aggregate(detail *ftypes.ArtifactDetail) {
var apps []ftypes.Application
aggregatedApps := make(map[ftypes.LangType]*ftypes.Application)
for _, t := range ftypes.AggregatingTypes {
aggregatedApps[t] = &ftypes.Application{Type: t}
}
for _, app := range detail.Applications {
a, ok := aggregatedApps[app.Type]
if !ok {
apps = append(apps, app)
continue
}
a.Packages = append(a.Packages, app.Packages...)
}
for _, app := range aggregatedApps {
if len(app.Packages) > 0 {
apps = append(apps, *app)
}
}
// Overwrite Applications
detail.Applications = apps
}
// We must save secrets from all layers even though they are removed in the uppler layer.
// If the secret was changed at the top level, we need to overwrite it.
func mergeSecrets(secretsMap map[string]ftypes.Secret, newSecret ftypes.Secret, layer ftypes.Layer) map[string]ftypes.Secret {
for i := range newSecret.Findings { // add layer to the Findings from the new secret
newSecret.Findings[i].Layer = layer
}
secret, ok := secretsMap[newSecret.FilePath]
if !ok {
// Add the new finding if its file doesn't exist before
secretsMap[newSecret.FilePath] = newSecret
} else {
// If the new finding has the same `RuleID` as the finding in the previous layers - use the new finding
for _, previousFinding := range secret.Findings { // secrets from previous layers
if !secretFindingsContains(newSecret.Findings, previousFinding) {
newSecret.Findings = append(newSecret.Findings, previousFinding)
}
}
secretsMap[newSecret.FilePath] = newSecret
}
return secretsMap
}
func secretFindingsContains(findings []ftypes.SecretFinding, finding ftypes.SecretFinding) bool {
for _, f := range findings {
if f.RuleID == finding.RuleID {
return true
}
}
return false
}