github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/fanal/applier/docker.go (about)

     1  package applier
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  	"time"
     7  
     8  	"github.com/knqyf263/nested"
     9  	"github.com/samber/lo"
    10  
    11  	"github.com/devseccon/trivy/pkg/fanal/types"
    12  )
    13  
    14  type Config struct {
    15  	ContainerConfig containerConfig `json:"container_config"`
    16  	History         []History
    17  }
    18  
    19  type containerConfig struct {
    20  	Env []string
    21  }
    22  
    23  type History struct {
    24  	Created   time.Time
    25  	CreatedBy string `json:"created_by"`
    26  }
    27  
    28  func containsPackage(e types.Package, s []types.Package) bool {
    29  	for _, a := range s {
    30  		if a.Name == e.Name && a.Version == e.Version && a.Release == e.Release {
    31  			return true
    32  		}
    33  	}
    34  	return false
    35  }
    36  
    37  func lookupOriginLayerForPkg(pkg types.Package, layers []types.BlobInfo) (string, string, *types.BuildInfo) {
    38  	for i, layer := range layers {
    39  		for _, info := range layer.PackageInfos {
    40  			if containsPackage(pkg, info.Packages) {
    41  				return layer.Digest, layer.DiffID, lookupBuildInfo(i, layers)
    42  			}
    43  		}
    44  	}
    45  	return "", "", nil
    46  }
    47  
    48  // lookupBuildInfo looks up Red Hat content sets from all layers
    49  func lookupBuildInfo(index int, layers []types.BlobInfo) *types.BuildInfo {
    50  	if layers[index].BuildInfo != nil {
    51  		return layers[index].BuildInfo
    52  	}
    53  
    54  	// Base layer (layers[0]) is missing content sets
    55  	//   - it needs to be shared from layers[1]
    56  	if index == 0 {
    57  		if len(layers) > 1 {
    58  			return layers[1].BuildInfo
    59  		}
    60  		return nil
    61  	}
    62  
    63  	// Customer's layers build on top of Red Hat image are also missing content sets
    64  	//   - it needs to be shared from the last Red Hat's layers which contains content sets
    65  	for i := index - 1; i >= 1; i-- {
    66  		if layers[i].BuildInfo != nil {
    67  			return layers[i].BuildInfo
    68  		}
    69  	}
    70  	return nil
    71  }
    72  
    73  func lookupOriginLayerForLib(filePath string, lib types.Package, layers []types.BlobInfo) (string, string) {
    74  	for _, layer := range layers {
    75  		for _, layerApp := range layer.Applications {
    76  			if filePath != layerApp.FilePath {
    77  				continue
    78  			}
    79  			if containsPackage(lib, layerApp.Libraries) {
    80  				return layer.Digest, layer.DiffID
    81  			}
    82  		}
    83  	}
    84  	return "", ""
    85  }
    86  
    87  // ApplyLayers returns the merged layer
    88  // nolint: gocyclo
    89  func ApplyLayers(layers []types.BlobInfo) types.ArtifactDetail {
    90  	sep := "/"
    91  	nestedMap := nested.Nested{}
    92  	secretsMap := make(map[string]types.Secret)
    93  	var mergedLayer types.ArtifactDetail
    94  
    95  	for _, layer := range layers {
    96  		for _, opqDir := range layer.OpaqueDirs {
    97  			opqDir = strings.TrimSuffix(opqDir, sep)  // this is necessary so that an empty element is not contribute into the array of the DeleteByString function
    98  			_ = nestedMap.DeleteByString(opqDir, sep) // nolint
    99  		}
   100  		for _, whFile := range layer.WhiteoutFiles {
   101  			_ = nestedMap.DeleteByString(whFile, sep) // nolint
   102  		}
   103  
   104  		mergedLayer.OS.Merge(layer.OS)
   105  
   106  		if layer.Repository != nil {
   107  			mergedLayer.Repository = layer.Repository
   108  		}
   109  
   110  		// Apply OS packages
   111  		for _, pkgInfo := range layer.PackageInfos {
   112  			key := fmt.Sprintf("%s/type:ospkg", pkgInfo.FilePath)
   113  			nestedMap.SetByString(key, sep, pkgInfo)
   114  		}
   115  
   116  		// Apply language-specific packages
   117  		for _, app := range layer.Applications {
   118  			key := fmt.Sprintf("%s/type:%s", app.FilePath, app.Type)
   119  			nestedMap.SetByString(key, sep, app)
   120  		}
   121  
   122  		// Apply misconfigurations
   123  		for _, config := range layer.Misconfigurations {
   124  			config.Layer = types.Layer{
   125  				Digest: layer.Digest,
   126  				DiffID: layer.DiffID,
   127  			}
   128  			key := fmt.Sprintf("%s/type:config", config.FilePath)
   129  			nestedMap.SetByString(key, sep, config)
   130  		}
   131  
   132  		// Apply secrets
   133  		for _, secret := range layer.Secrets {
   134  			l := types.Layer{
   135  				Digest:    layer.Digest,
   136  				DiffID:    layer.DiffID,
   137  				CreatedBy: layer.CreatedBy,
   138  			}
   139  			secretsMap = mergeSecrets(secretsMap, secret, l)
   140  		}
   141  
   142  		// Apply license files
   143  		for _, license := range layer.Licenses {
   144  			license.Layer = types.Layer{
   145  				Digest: layer.Digest,
   146  				DiffID: layer.DiffID,
   147  			}
   148  			key := fmt.Sprintf("%s/type:license,%s", license.FilePath, license.Type)
   149  			nestedMap.SetByString(key, sep, license)
   150  		}
   151  
   152  		// Apply custom resources
   153  		for _, customResource := range layer.CustomResources {
   154  			key := fmt.Sprintf("%s/custom:%s", customResource.FilePath, customResource.Type)
   155  			customResource.Layer = types.Layer{
   156  				Digest: layer.Digest,
   157  				DiffID: layer.DiffID,
   158  			}
   159  			nestedMap.SetByString(key, sep, customResource)
   160  		}
   161  	}
   162  
   163  	// nolint
   164  	_ = nestedMap.Walk(func(keys []string, value interface{}) error {
   165  		switch v := value.(type) {
   166  		case types.PackageInfo:
   167  			mergedLayer.Packages = append(mergedLayer.Packages, v.Packages...)
   168  		case types.Application:
   169  			mergedLayer.Applications = append(mergedLayer.Applications, v)
   170  		case types.Misconfiguration:
   171  			mergedLayer.Misconfigurations = append(mergedLayer.Misconfigurations, v)
   172  		case types.LicenseFile:
   173  			mergedLayer.Licenses = append(mergedLayer.Licenses, v)
   174  		case types.CustomResource:
   175  			mergedLayer.CustomResources = append(mergedLayer.CustomResources, v)
   176  		}
   177  		return nil
   178  	})
   179  
   180  	for _, s := range secretsMap {
   181  		mergedLayer.Secrets = append(mergedLayer.Secrets, s)
   182  	}
   183  
   184  	// Extract dpkg licenses
   185  	// The license information is not stored in the dpkg database and in a separate file,
   186  	// so we have to merge the license information into the package.
   187  	dpkgLicenses := make(map[string][]string)
   188  	mergedLayer.Licenses = lo.Reject(mergedLayer.Licenses, func(license types.LicenseFile, _ int) bool {
   189  		if license.Type != types.LicenseTypeDpkg {
   190  			return false
   191  		}
   192  		// e.g.
   193  		//	"adduser" => {"GPL-2"}
   194  		//  "openssl" => {"MIT", "BSD"}
   195  		dpkgLicenses[license.PkgName] = lo.Map(license.Findings, func(finding types.LicenseFinding, _ int) string {
   196  			return finding.Name
   197  		})
   198  		// Remove this license in the merged result as it is merged into the package information.
   199  		return true
   200  	})
   201  	if len(mergedLayer.Licenses) == 0 {
   202  		mergedLayer.Licenses = nil
   203  	}
   204  
   205  	for i, pkg := range mergedLayer.Packages {
   206  		// Skip lookup for SBOM
   207  		if !lo.IsEmpty(pkg.Layer) {
   208  			continue
   209  		}
   210  		originLayerDigest, originLayerDiffID, buildInfo := lookupOriginLayerForPkg(pkg, layers)
   211  		mergedLayer.Packages[i].Layer = types.Layer{
   212  			Digest: originLayerDigest,
   213  			DiffID: originLayerDiffID,
   214  		}
   215  		mergedLayer.Packages[i].BuildInfo = buildInfo
   216  
   217  		// Only debian packages
   218  		if licenses, ok := dpkgLicenses[pkg.Name]; ok {
   219  			mergedLayer.Packages[i].Licenses = licenses
   220  		}
   221  	}
   222  
   223  	for _, app := range mergedLayer.Applications {
   224  		for i, lib := range app.Libraries {
   225  			// Skip lookup for SBOM
   226  			if !lo.IsEmpty(lib.Layer) {
   227  				continue
   228  			}
   229  			originLayerDigest, originLayerDiffID := lookupOriginLayerForLib(app.FilePath, lib, layers)
   230  			app.Libraries[i].Layer = types.Layer{
   231  				Digest: originLayerDigest,
   232  				DiffID: originLayerDiffID,
   233  			}
   234  		}
   235  	}
   236  
   237  	// Aggregate python/ruby/node.js packages and JAR files
   238  	aggregate(&mergedLayer)
   239  
   240  	return mergedLayer
   241  }
   242  
   243  // aggregate merges all packages installed by pip/gem/npm/jar/conda into each application
   244  func aggregate(detail *types.ArtifactDetail) {
   245  	var apps []types.Application
   246  
   247  	aggregatedApps := map[types.LangType]*types.Application{
   248  		types.PythonPkg: {Type: types.PythonPkg},
   249  		types.CondaPkg:  {Type: types.CondaPkg},
   250  		types.GemSpec:   {Type: types.GemSpec},
   251  		types.NodePkg:   {Type: types.NodePkg},
   252  		types.Jar:       {Type: types.Jar},
   253  	}
   254  
   255  	for _, app := range detail.Applications {
   256  		a, ok := aggregatedApps[app.Type]
   257  		if !ok {
   258  			apps = append(apps, app)
   259  			continue
   260  		}
   261  		a.Libraries = append(a.Libraries, app.Libraries...)
   262  	}
   263  
   264  	for _, app := range aggregatedApps {
   265  		if len(app.Libraries) > 0 {
   266  			apps = append(apps, *app)
   267  		}
   268  	}
   269  
   270  	// Overwrite Applications
   271  	detail.Applications = apps
   272  }
   273  
   274  // We must save secrets from all layers even though they are removed in the uppler layer.
   275  // If the secret was changed at the top level, we need to overwrite it.
   276  func mergeSecrets(secretsMap map[string]types.Secret, newSecret types.Secret, layer types.Layer) map[string]types.Secret {
   277  	for i := range newSecret.Findings { // add layer to the Findings from the new secret
   278  		newSecret.Findings[i].Layer = layer
   279  	}
   280  
   281  	secret, ok := secretsMap[newSecret.FilePath]
   282  	if !ok {
   283  		// Add the new finding if its file doesn't exist before
   284  		secretsMap[newSecret.FilePath] = newSecret
   285  	} else {
   286  		// If the new finding has the same `RuleID` as the finding in the previous layers - use the new finding
   287  		for _, previousFinding := range secret.Findings { // secrets from previous layers
   288  			if !secretFindingsContains(newSecret.Findings, previousFinding) {
   289  				newSecret.Findings = append(newSecret.Findings, previousFinding)
   290  			}
   291  		}
   292  		secretsMap[newSecret.FilePath] = newSecret
   293  	}
   294  	return secretsMap
   295  }
   296  
   297  func secretFindingsContains(findings []types.SecretFinding, finding types.SecretFinding) bool {
   298  	for _, f := range findings {
   299  		if f.RuleID == finding.RuleID {
   300  			return true
   301  		}
   302  	}
   303  	return false
   304  }