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 }