slim/pkg/app/master/commands/xray/handler.go
2021-11-07 18:45:28 -08:00

1273 lines
35 KiB
Go

package xray
import (
"fmt"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/docker-slim/docker-slim/pkg/app"
"github.com/docker-slim/docker-slim/pkg/app/master/commands"
"github.com/docker-slim/docker-slim/pkg/app/master/docker/dockerclient"
"github.com/docker-slim/docker-slim/pkg/app/master/inspectors/image"
"github.com/docker-slim/docker-slim/pkg/app/master/version"
"github.com/docker-slim/docker-slim/pkg/command"
"github.com/docker-slim/docker-slim/pkg/docker/buildpackinfo"
"github.com/docker-slim/docker-slim/pkg/docker/dockerimage"
"github.com/docker-slim/docker-slim/pkg/docker/dockerutil"
"github.com/docker-slim/docker-slim/pkg/report"
"github.com/docker-slim/docker-slim/pkg/util/errutil"
"github.com/docker-slim/docker-slim/pkg/util/fsutil"
v "github.com/docker-slim/docker-slim/pkg/version"
//"github.com/bmatcuk/doublestar/v3"
"github.com/dustin/go-humanize"
log "github.com/sirupsen/logrus"
)
const appName = commands.AppName
type ovars = app.OutVars
// Xray command exit codes
const (
ecxOther = iota + 1
ecxImageNotFound
)
const (
fatDockerfileName = "Dockerfile.fat"
)
// OnCommand implements the 'xray' docker-slim command
func OnCommand(
xc *app.ExecutionContext,
gparams *commands.GenericParams,
targetRef string,
doPull bool,
dockerConfigPath string,
registryAccount string,
registrySecret string,
doShowPullLogs bool,
changes map[string]struct{},
changesOutputs map[string]struct{},
layers map[string]struct{},
layerChangesMax int,
allChangesMax int,
addChangesMax int,
modifyChangesMax int,
deleteChangesMax int,
topChangesMax int,
changePathMatchers []*dockerimage.ChangePathMatcher,
changeDataMatcherList []*dockerimage.ChangeDataMatcher,
changeDataHashMatcherList []*dockerimage.ChangeDataHashMatcher,
doHashData bool,
doDetectDuplicates bool,
doShowDuplicates bool,
doShowSpecialPerms bool,
changeMatchLayersOnly bool,
doAddImageManifest bool,
doAddImageConfig bool,
doReuseSavedImage bool,
doRmFileArtifacts bool,
utf8Detector *dockerimage.UTF8Detector,
doDetectAllCertFiles bool,
doDetectAllCertPKFiles bool,
xdArtifactsPath string,
) {
const cmdName = Name
logger := log.WithFields(log.Fields{"app": appName, "command": cmdName})
prefix := fmt.Sprintf("cmd=%s", cmdName)
changeDataMatchers := map[string]*dockerimage.ChangeDataMatcher{}
for _, cdm := range changeDataMatcherList {
matcher, err := regexp.Compile(cdm.DataPattern)
errutil.FailOn(err)
cdm.Matcher = matcher
changeDataMatchers[cdm.DataPattern] = cdm
}
changeDataHashMatchers := map[string]*dockerimage.ChangeDataHashMatcher{}
for _, cdhm := range changeDataHashMatcherList {
changeDataHashMatchers[cdhm.Hash] = cdhm
}
viChan := version.CheckAsync(gparams.CheckVersion, gparams.InContainer, gparams.IsDSImage)
cmdReport := report.NewXrayCommand(gparams.ReportLocation, gparams.InContainer)
cmdReport.State = command.StateStarted
cmdReport.TargetReference = targetRef
xc.Out.State("started")
xc.Out.Info("params",
ovars{
"target": targetRef,
"add-image-manifest": doAddImageManifest,
"add-image-config": doAddImageConfig,
"rm-file-artifacts": doRmFileArtifacts,
})
client, err := dockerclient.New(gparams.ClientConfig)
if err == dockerclient.ErrNoDockerInfo {
exitMsg := "missing Docker connection info"
if gparams.InContainer && gparams.IsDSImage {
exitMsg = "make sure to pass the Docker connect parameters to the docker-slim container"
}
xc.Out.Error("docker.connect.error", exitMsg)
exitCode := commands.ECTCommon | commands.ECNoDockerConnectInfo
xc.Out.State("exited",
ovars{
"exit.code": exitCode,
"version": v.Current(),
"location": fsutil.ExeDir(),
})
xc.Exit(exitCode)
}
errutil.FailOn(err)
if gparams.Debug {
version.Print(prefix, logger, client, false, gparams.InContainer, gparams.IsDSImage)
}
imageInspector, err := image.NewInspector(client, targetRef)
errutil.FailOn(err)
if imageInspector.NoImage() {
if doPull {
xc.Out.Info("target.image",
ovars{
"status": "not.found",
"image": targetRef,
"message": "trying to pull target image",
})
err := imageInspector.Pull(doShowPullLogs, dockerConfigPath, registryAccount, registrySecret)
errutil.FailOn(err)
} else {
xc.Out.Error("image.not.found", "make sure the target image already exists locally (use --pull flag to auto-download it from registry)")
exitCode := commands.ECTBuild | ecxImageNotFound
xc.Out.State("exited",
ovars{
"exit.code": exitCode,
})
xc.Exit(exitCode)
}
}
//refresh the target refs
targetRef = imageInspector.ImageRef
cmdReport.TargetReference = imageInspector.ImageRef
xc.Out.State("image.api.inspection.start")
logger.Info("inspecting 'fat' image metadata...")
err = imageInspector.Inspect()
errutil.FailOn(err)
localVolumePath, artifactLocation, statePath, stateKey := fsutil.PrepareImageStateDirs(gparams.StatePath, imageInspector.ImageInfo.ID)
imageInspector.ArtifactLocation = artifactLocation
logger.Debugf("localVolumePath=%v, artifactLocation=%v, statePath=%v, stateKey=%v", localVolumePath, artifactLocation, statePath, stateKey)
xc.Out.Info("image",
ovars{
"id": imageInspector.ImageInfo.ID,
"size.bytes": imageInspector.ImageInfo.VirtualSize,
"size.human": humanize.Bytes(uint64(imageInspector.ImageInfo.VirtualSize)),
})
logger.Info("processing 'fat' image info...")
err = imageInspector.ProcessCollectedData()
errutil.FailOn(err)
if imageInspector.DockerfileInfo != nil {
if imageInspector.DockerfileInfo.ExeUser != "" {
xc.Out.Info("image.users",
ovars{
"exe": imageInspector.DockerfileInfo.ExeUser,
"all": strings.Join(imageInspector.DockerfileInfo.AllUsers, ","),
})
}
if len(imageInspector.DockerfileInfo.ImageStack) > 0 {
cmdReport.ImageStack = imageInspector.DockerfileInfo.ImageStack
for idx, imageInfo := range imageInspector.DockerfileInfo.ImageStack {
xc.Out.Info("image.stack",
ovars{
"index": idx,
"name": imageInfo.FullName,
"id": imageInfo.ID,
"instructions": len(imageInfo.Instructions),
"message": "see report file for details",
})
}
}
if len(imageInspector.DockerfileInfo.ExposedPorts) > 0 {
xc.Out.Info("image.exposed_ports",
ovars{
"list": strings.Join(imageInspector.DockerfileInfo.ExposedPorts, ","),
})
}
}
imgIdentity := dockerutil.ImageToIdentity(imageInspector.ImageInfo)
cmdReport.SourceImage = report.ImageMetadata{
Identity: report.ImageIdentity{
ID: imgIdentity.ID,
Tags: imgIdentity.ShortTags,
Names: imgIdentity.RepoTags,
Digests: imgIdentity.ShortDigests,
FullDigests: imgIdentity.RepoDigests,
},
Size: imageInspector.ImageInfo.VirtualSize,
SizeHuman: humanize.Bytes(uint64(imageInspector.ImageInfo.VirtualSize)),
CreateTime: imageInspector.ImageInfo.Created.UTC().Format(time.RFC3339),
Author: imageInspector.ImageInfo.Author,
DockerVersion: imageInspector.ImageInfo.DockerVersion,
Architecture: imageInspector.ImageInfo.Architecture,
User: imageInspector.ImageInfo.Config.User,
OS: imageInspector.ImageInfo.OS,
WorkDir: imageInspector.ImageInfo.Config.WorkingDir,
ContainerEntry: report.ContainerEntryInfo{
Entrypoint: imageInspector.ImageInfo.Config.Entrypoint,
Cmd: imageInspector.ImageInfo.Config.Cmd,
},
InheritedInstructions: imageInspector.ImageInfo.Config.OnBuild,
}
cmdReport.SourceImage.EnvVars = imageInspector.ImageInfo.Config.Env
for k := range imageInspector.ImageInfo.Config.ExposedPorts {
cmdReport.SourceImage.ExposedPorts = append(cmdReport.SourceImage.ExposedPorts, string(k))
}
for k := range imageInspector.ImageInfo.Config.Volumes {
cmdReport.SourceImage.Volumes = append(cmdReport.SourceImage.Volumes, k)
}
cmdReport.SourceImage.Labels = imageInspector.ImageInfo.Config.Labels
maintainers := map[string]struct{}{}
if buildpackinfo.HasBuildbackLabels(imageInspector.ImageInfo.Config.Labels) {
for k, v := range imageInspector.ImageInfo.Config.Labels {
if k == "io.buildpacks.stack.maintainer" {
buildpackMaintainer := v + " (buildpack)"
maintainers[buildpackMaintainer] = struct{}{}
}
}
bpStack := imageInspector.ImageInfo.Config.Labels[buildpackinfo.LabelKeyStackID]
cmdReport.SourceImage.Buildpack = &report.BuildpackInfo{
Stack: bpStack,
}
}
for _, m := range imageInspector.DockerfileInfo.Maintainers {
maintainers[m] = struct{}{}
}
for k, v := range cmdReport.SourceImage.Labels {
if strings.ToLower(k) == "maintainer" || strings.ToLower(k) == "author" || strings.ToLower(k) == "authors" || k == "org.opencontainers.image.authors" {
maintainers[v] = struct{}{}
}
}
for m := range maintainers {
cmdReport.SourceImage.Maintainers = append(cmdReport.SourceImage.Maintainers, m)
}
cmdReport.ArtifactLocation = imageInspector.ArtifactLocation
xc.Out.State("image.api.inspection.done")
xc.Out.State("image.data.inspection.start")
imageID := dockerutil.CleanImageID(imageInspector.ImageInfo.ID)
iaName := fmt.Sprintf("%s.tar", imageID)
iaPath := filepath.Join(localVolumePath, "image", iaName)
iaPathReady := fmt.Sprintf("%s.ready", iaPath)
var doSave bool
if fsutil.IsRegularFile(iaPath) {
if !doReuseSavedImage {
doSave = true
}
if !fsutil.Exists(iaPathReady) {
doSave = true
}
} else {
doSave = true
}
if doSave {
if fsutil.Exists(iaPathReady) {
fsutil.Remove(iaPathReady)
}
xc.Out.Info("image.data.inspection.save.image.start")
err = dockerutil.SaveImage(client, imageID, iaPath, false, false)
errutil.FailOn(err)
err = fsutil.Touch(iaPathReady)
errutil.WarnOn(err)
xc.Out.Info("image.data.inspection.save.image.end")
} else {
logger.Debugf("exported image already exists - %s", iaPath)
}
xc.Out.Info("image.data.inspection.process.image.start")
imagePkg, err := dockerimage.LoadPackage(
iaPath,
imageID,
false,
topChangesMax,
doHashData,
doDetectDuplicates,
changeDataHashMatchers,
changePathMatchers,
changeDataMatchers,
utf8Detector,
doDetectAllCertFiles,
doDetectAllCertPKFiles)
errutil.FailOn(err)
xc.Out.Info("image.data.inspection.process.image.end")
if utf8Detector != nil {
errutil.FailOn(utf8Detector.Close())
}
xc.Out.State("image.data.inspection.done")
if len(imageInspector.DockerfileInfo.AllInstructions) == len(imagePkg.Config.History) {
for instIdx, instInfo := range imageInspector.DockerfileInfo.AllInstructions {
instInfo.Author = imagePkg.Config.History[instIdx].Author
instInfo.EmptyLayer = imagePkg.Config.History[instIdx].EmptyLayer
instInfo.LayerID = imagePkg.Config.History[instIdx].LayerID
instInfo.LayerIndex = imagePkg.Config.History[instIdx].LayerIndex
instInfo.LayerFSDiffID = imagePkg.Config.History[instIdx].LayerFSDiffID
}
} else {
logger.Debugf("history instruction set size mismatch - %v/%v ",
len(imageInspector.DockerfileInfo.AllInstructions),
len(imagePkg.Config.History))
}
allEntryParams := append(cmdReport.SourceImage.ContainerEntry.Entrypoint,
cmdReport.SourceImage.ContainerEntry.Cmd...)
if len(allEntryParams) > 0 {
cmdReport.SourceImage.ContainerEntry.ExePath = allEntryParams[0]
cmdReport.SourceImage.ContainerEntry.ExeArgs = allEntryParams[1:]
//fix up exe path if relative
if !strings.HasPrefix(cmdReport.SourceImage.ContainerEntry.ExePath, "/") {
var envPaths []string
for _, envInfo := range cmdReport.SourceImage.EnvVars {
if strings.HasPrefix(envInfo, "PATH=") {
envInfo = strings.TrimPrefix(envInfo, "PATH=")
envPaths = strings.Split(envInfo, ":")
break
}
}
for _, envPath := range envPaths {
fullExePath := fmt.Sprintf("%s/%s", envPath, cmdReport.SourceImage.ContainerEntry.ExePath)
object := findChange(imagePkg, fullExePath)
if object != nil {
cmdReport.SourceImage.ContainerEntry.FullExePath =
&report.ContainerFileInfo{
Name: fullExePath,
Layer: object.LayerIndex,
}
break
}
}
} else {
object := findChange(imagePkg, cmdReport.SourceImage.ContainerEntry.ExePath)
if object != nil {
cmdReport.SourceImage.ContainerEntry.FullExePath =
&report.ContainerFileInfo{
Name: cmdReport.SourceImage.ContainerEntry.ExePath,
Layer: object.LayerIndex,
}
}
}
//find files in exe args
for _, exeArg := range cmdReport.SourceImage.ContainerEntry.ExeArgs {
//if starts with / assume a full path and lookup/find in the layer references
//otherwise try to use workdir and lookup/find in the layer references
if strings.HasPrefix(exeArg, "-") {
//skip flag names (might have false positives)
continue
}
var filePath string
if strings.HasPrefix(exeArg, "/") {
filePath = exeArg
} else {
//not a perfect way to find potential files
//but better than nothing
filePath = fmt.Sprintf("%s/%s", cmdReport.SourceImage.WorkDir, exeArg)
}
object := findChange(imagePkg, filePath)
if object != nil {
cmdReport.SourceImage.ContainerEntry.ArgFiles =
append(cmdReport.SourceImage.ContainerEntry.ArgFiles,
&report.ContainerFileInfo{
Name: filePath,
Layer: object.LayerIndex,
})
break
}
}
}
printImagePackage(
xc,
imagePkg,
appName,
cmdName,
changes,
changesOutputs,
layers,
layerChangesMax,
allChangesMax,
addChangesMax,
modifyChangesMax,
deleteChangesMax,
doHashData,
doDetectDuplicates,
doShowDuplicates,
doShowSpecialPerms,
changeMatchLayersOnly,
changeDataHashMatchers,
changePathMatchers,
changeDataMatchers,
cmdReport)
if doAddImageManifest {
cmdReport.RawImageManifest = imagePkg.Manifest
}
if doAddImageConfig {
cmdReport.RawImageConfig = imagePkg.Config
}
xc.Out.State("completed")
cmdReport.State = command.StateCompleted
if doRmFileArtifacts {
logger.Info("removing temporary artifacts...")
err = fsutil.Remove(iaPath)
errutil.WarnOn(err)
} else {
cmdReport.ImageArchiveLocation = iaPath
}
xc.Out.State("done")
xc.Out.Info("results",
ovars{
"artifacts.location": cmdReport.ArtifactLocation,
})
xc.Out.Info("results",
ovars{
"artifacts.dockerfile.original": "Dockerfile.fat",
})
vinfo := <-viChan
version.PrintCheckVersion(xc, "", vinfo)
cmdReport.State = command.StateDone
if cmdReport.Save() {
xc.Out.Info("report",
ovars{
"file": cmdReport.ReportLocation(),
})
}
if xdArtifactsPath != "" {
var filesToExport []string
filesToExport = append(filesToExport, cmdReport.ReportLocation())
filesToExport = append(filesToExport, filepath.Join(cmdReport.ArtifactLocation, fatDockerfileName))
if utf8Detector.DumpArchive != "" {
filesToExport = append(filesToExport, utf8Detector.DumpArchive)
}
if xdArtifactsPath == "." {
xdArtifactsPath = "data-artifacts.tar"
}
if err := fsutil.ArchiveFiles(xdArtifactsPath, filesToExport, true, ""); err == nil {
xc.Out.Info("exported-data-artifacts",
ovars{
"file": xdArtifactsPath,
})
} else {
logger.Errorf("error exporting data artifacts (%s) - %v", xdArtifactsPath, err)
}
}
}
func findChange(pkg *dockerimage.Package, filepath string) *dockerimage.ObjectMetadata {
for _, layer := range pkg.Layers {
if object, found := layer.References[filepath]; found {
return object
}
}
return nil
}
func printImagePackage(
xc *app.ExecutionContext,
pkg *dockerimage.Package,
appName string,
cmdName command.Type,
changes map[string]struct{},
changesOutputs map[string]struct{},
layers map[string]struct{},
layerChangesMax int,
allChangesMax int,
addChangesMax int,
modifyChangesMax int,
deleteChangesMax int,
doHashData bool,
doDetectDuplicates bool,
doShowDuplicates bool,
doShowSpecialPerms bool,
changeMatchLayersOnly bool,
changeDataHashMatchers map[string]*dockerimage.ChangeDataHashMatcher,
changePathMatchers []*dockerimage.ChangePathMatcher,
changeDataMatchers map[string]*dockerimage.ChangeDataMatcher,
cmdReport *report.XrayCommand) {
var allChangesCount int
var addChangesCount int
var modifyChangesCount int
var deleteChangesCount int
xc.Out.Info("image.package.details")
xc.Out.Info("layers.count",
ovars{
"value": len(pkg.Layers),
})
cmdReport.ImageReport = &dockerimage.ImageReport{
Stats: pkg.Stats,
}
for k := range pkg.Certs.Bundles {
cmdReport.ImageReport.Certs.Bundles =
append(cmdReport.ImageReport.Certs.Bundles, k)
}
for k := range pkg.Certs.Files {
cmdReport.ImageReport.Certs.Files =
append(cmdReport.ImageReport.Certs.Files, k)
}
cmdReport.ImageReport.Certs.Links = pkg.Certs.Links
cmdReport.ImageReport.Certs.Hashes = pkg.Certs.Hashes
for k := range pkg.Certs.PrivateKeys {
cmdReport.ImageReport.Certs.PrivateKeys =
append(cmdReport.ImageReport.Certs.PrivateKeys, k)
}
cmdReport.ImageReport.Certs.PrivateKeyLinks = pkg.Certs.PrivateKeyLinks
for k := range pkg.CACerts.Bundles {
cmdReport.ImageReport.CACerts.Bundles =
append(cmdReport.ImageReport.CACerts.Bundles, k)
}
for k := range pkg.CACerts.Files {
cmdReport.ImageReport.CACerts.Files =
append(cmdReport.ImageReport.CACerts.Files, k)
}
cmdReport.ImageReport.CACerts.Links = pkg.CACerts.Links
cmdReport.ImageReport.CACerts.Hashes = pkg.CACerts.Hashes
for k := range pkg.CACerts.PrivateKeys {
cmdReport.ImageReport.CACerts.PrivateKeys =
append(cmdReport.ImageReport.CACerts.PrivateKeys, k)
}
cmdReport.ImageReport.CACerts.PrivateKeyLinks = pkg.CACerts.PrivateKeyLinks
if doDetectDuplicates && pkg.Stats.DuplicateFileCount > 0 {
xc.Out.Info("image.stats.duplicates",
ovars{
"file_count": pkg.Stats.DuplicateFileCount,
"file_total_count": pkg.Stats.DuplicateFileTotalCount,
"file_size.bytes": pkg.Stats.DuplicateFileSize,
"file_size.human": humanize.Bytes(pkg.Stats.DuplicateFileSize),
"file_total_size.bytes": pkg.Stats.DuplicateFileTotalSize,
"file_total_size.human": humanize.Bytes(pkg.Stats.DuplicateFileTotalSize),
"wasted.bytes": pkg.Stats.DuplicateFileWastedSize,
"wasted.human": humanize.Bytes(pkg.Stats.DuplicateFileWastedSize),
})
}
doShow := func(changeMatchLayersOnly bool, layer *dockerimage.Layer) bool {
if !changeMatchLayersOnly || (changeMatchLayersOnly && layer.HasMatches()) {
return true
}
return false
}
for _, layer := range pkg.Layers {
layerInfo := ovars{
"index": layer.Index,
"id": layer.ID,
"path": layer.Path,
}
if layer.MetadataChangesOnly {
layerInfo["metadata_change_only"] = true
}
if layer.LayerDataSource != "" {
layerInfo["layer_data_source"] = layer.LayerDataSource
}
if doShow(changeMatchLayersOnly, layer) {
xc.Out.Info("layer.start")
xc.Out.Info("layer", layerInfo)
}
var layerChangesCount int
if layer.Distro != nil {
distro := &report.DistroInfo{
Name: layer.Distro.Name,
Version: layer.Distro.Version,
DisplayName: layer.Distro.DisplayName,
}
xc.Out.Info("distro",
ovars{
"name": distro.Name,
"version": distro.Version,
"display": distro.DisplayName,
})
cmdReport.SourceImage.Distro = distro
}
topList := layer.Top.List()
layerReport := dockerimage.LayerReport{
ID: layer.ID,
Index: layer.Index,
Path: layer.Path,
LayerDataSource: layer.LayerDataSource,
MetadataChangesOnly: layer.MetadataChangesOnly,
FSDiffID: layer.FSDiffID,
Stats: layer.Stats,
}
layerReport.Changes.Deleted = uint64(len(layer.Changes.Deleted))
layerReport.Changes.Modified = uint64(len(layer.Changes.Modified))
layerReport.Changes.Added = uint64(len(layer.Changes.Added))
layerReport.Top = topList
for imgIdx, imgInfo := range cmdReport.ImageStack {
for instIdx, instInfo := range imgInfo.Instructions {
if layerReport.ID == instInfo.LayerID {
if !instInfo.EmptyLayer {
if layerReport.ChangeInstruction != nil {
log.Debugf("overwriting existing layerReport.ChangeInstruction = %#v", layerReport.ChangeInstruction)
}
layerReport.ChangeInstruction = &dockerimage.InstructionSummary{
Index: instIdx,
ImageIndex: imgIdx,
Type: instInfo.Type,
All: instInfo.CommandAll,
Snippet: instInfo.CommandSnippet,
}
} else {
extraInst := &dockerimage.InstructionSummary{
Index: instIdx,
ImageIndex: imgIdx,
Type: instInfo.Type,
All: instInfo.CommandAll,
Snippet: instInfo.CommandSnippet,
}
layerReport.OtherInstructions = append(layerReport.OtherInstructions, extraInst)
}
}
}
}
cmdReport.ImageLayers = append(cmdReport.ImageLayers, &layerReport)
if layerReport.ChangeInstruction != nil {
if doShow(changeMatchLayersOnly, layer) {
xc.Out.Info("change.instruction",
ovars{
"index": fmt.Sprintf("%d:%d", layerReport.ChangeInstruction.ImageIndex, layerReport.ChangeInstruction.Index),
"type": layerReport.ChangeInstruction.Type,
"snippet": layerReport.ChangeInstruction.Snippet,
"all": layerReport.ChangeInstruction.All,
})
}
}
if doShow(changeMatchLayersOnly, layer) {
if layerReport.OtherInstructions != nil {
xc.Out.Info("other.instructions",
ovars{
"count": len(layerReport.OtherInstructions),
})
for idx, info := range layerReport.OtherInstructions {
xc.Out.Info("other.instruction",
ovars{
"pos": idx,
"index": fmt.Sprintf("%d:%d", info.ImageIndex, info.Index),
"type": info.Type,
"snippet": info.Snippet,
"all": info.All,
})
}
}
if layer.Stats.AllSize != 0 {
xc.Out.Info("layer.stats",
ovars{
"all_size.human": humanize.Bytes(uint64(layer.Stats.AllSize)),
"all_size.bytes": layer.Stats.AllSize,
})
}
if layer.Stats.ObjectCount != 0 {
xc.Out.Info("layer.stats",
ovars{
"object_count": layer.Stats.ObjectCount,
})
}
if layer.Stats.DirCount != 0 {
xc.Out.Info("layer.stats",
ovars{
"dir_count": layer.Stats.DirCount,
})
}
if layer.Stats.FileCount != 0 {
xc.Out.Info("layer.stats",
ovars{
"file_count": layer.Stats.FileCount,
})
}
if layer.Stats.LinkCount != 0 {
xc.Out.Info("layer.stats",
ovars{
"link_count": layer.Stats.LinkCount,
})
}
if layer.Stats.MaxFileSize != 0 {
xc.Out.Info("layer.stats",
ovars{
"max_file_size.human": humanize.Bytes(uint64(layer.Stats.MaxFileSize)),
"max_file_size.bytes": layer.Stats.MaxFileSize,
})
}
if layer.Stats.DeletedCount != 0 {
xc.Out.Info("layer.stats",
ovars{
"deleted_count": layer.Stats.DeletedCount,
})
}
if layer.Stats.DeletedDirCount != 0 {
xc.Out.Info("layer.stats",
ovars{
"deleted_dir_count": layer.Stats.DeletedDirCount,
})
}
if layer.Stats.DeletedFileCount != 0 {
xc.Out.Info("layer.stats",
ovars{
"deleted_file_count": layer.Stats.DeletedFileCount,
})
}
if layer.Stats.DeletedLinkCount != 0 {
xc.Out.Info("layer.stats",
ovars{
"deleted_link_count": layer.Stats.DeletedLinkCount,
})
}
if layer.Stats.DeletedSize != 0 {
xc.Out.Info("layer.stats",
ovars{
"deleted_size": layer.Stats.DeletedSize,
})
}
if layer.Stats.AddedSize != 0 {
xc.Out.Info("layer.stats",
ovars{
"added_size.human": humanize.Bytes(uint64(layer.Stats.AddedSize)),
"added_size.bytes": layer.Stats.AddedSize,
})
}
if layer.Stats.ModifiedSize != 0 {
xc.Out.Info("layer.stats",
ovars{
"modified_size.human": humanize.Bytes(uint64(layer.Stats.ModifiedSize)),
"modified_size.bytes": layer.Stats.ModifiedSize,
})
}
}
changeCount := len(layer.Changes.Deleted) + len(layer.Changes.Modified) + len(layer.Changes.Added)
if doShow(changeMatchLayersOnly, layer) {
xc.Out.Info("layer.change.summary",
ovars{
"deleted": len(layer.Changes.Deleted),
"modified": len(layer.Changes.Modified),
"added": len(layer.Changes.Added),
"all": changeCount,
})
xc.Out.Info("layer.objects.count",
ovars{
"value": len(layer.Objects),
})
if len(topList) > 0 {
xc.Out.Info("layer.objects.top.start")
for _, topObject := range topList {
match := topObject.PathMatch
if !match && len(changePathMatchers) > 0 {
log.Tracef("Change path patterns, no match. skipping 'top' change ['%s']", topObject.Name)
continue
} else {
if len(changeDataMatchers) > 0 {
matchedPatterns, found := layer.DataMatches[topObject.Name]
if !found {
log.Tracef("Change data patterns, no match. skipping 'top' change ['%s']", topObject.Name)
continue
}
log.Tracef("'%s' ('top' change) matched data patterns - %d", topObject.Name, len(matchedPatterns))
for _, cdm := range matchedPatterns {
log.Tracef("matched => PP='%s' DP='%s'", cdm.PathPattern, cdm.DataPattern)
}
} else {
if len(changeDataHashMatchers) > 0 {
matched, found := layer.DataHashMatches[topObject.Name]
if !found {
log.Trace("Change data hash patterns, no match. skipping 'top' change...")
continue
}
log.Tracef("'%s' ('top' change) matched data hash pattern - %s", topObject.Name, matched.Hash)
}
}
}
printObject(xc, topObject)
}
xc.Out.Info("layer.objects.top.end")
}
}
showLayer := true
if len(layers) > 0 {
showLayer = false
_, hasID := layers[layer.ID]
layerIdx := fmt.Sprintf("%v", layer.Index)
_, hasIndex := layers[layerIdx]
if hasID || hasIndex {
showLayer = true
}
}
if doShow(changeMatchLayersOnly, layer) && showLayer {
if _, ok := changes["delete"]; ok && len(layer.Changes.Deleted) > 0 {
xc.Out.Info("layer.objects.deleted.start")
for _, objectIdx := range layer.Changes.Deleted {
allChangesCount++
deleteChangesCount++
layerChangesCount++
objectInfo := layer.Objects[objectIdx]
//TODO: add a flag to select change type to apply path patterns
match := objectInfo.PathMatch
if !match && len(changePathMatchers) > 0 {
log.Tracef("Change path patterns, no match. skipping 'delete' change ['%s']", objectInfo.Name)
continue
}
//NOTE: not checking change data pattern matches for deletes
if allChangesMax > -1 && allChangesCount > allChangesMax {
break
}
if deleteChangesMax > -1 && deleteChangesCount > deleteChangesMax {
break
}
if layerChangesMax > -1 && layerChangesCount > layerChangesMax {
break
}
if _, ok := changesOutputs["report"]; ok {
layerReport.Deleted = append(layerReport.Deleted, objectInfo)
}
if _, ok := changesOutputs["console"]; ok {
printObject(xc, objectInfo)
}
}
xc.Out.Info("layer.objects.deleted.end")
}
if _, ok := changes["modify"]; ok && len(layer.Changes.Modified) > 0 {
xc.Out.Info("layer.objects.modified.start")
for _, objectIdx := range layer.Changes.Modified {
allChangesCount++
modifyChangesCount++
layerChangesCount++
objectInfo := layer.Objects[objectIdx]
//TODO: add a flag to select change type to apply path patterns
match := objectInfo.PathMatch
if !match && len(changePathMatchers) > 0 {
log.Tracef("Change path patterns, no match. skipping 'modify' change ['%s']", objectInfo.Name)
continue
} else {
if len(changeDataMatchers) > 0 {
matchedPatterns, found := layer.DataMatches[objectInfo.Name]
if !found {
log.Tracef("Change data patterns, no match. skipping change ['%s']", objectInfo.Name)
continue
}
log.Tracef("'%s' ('modify' change) matched data patterns - %d", objectInfo.Name, len(matchedPatterns))
for _, cdm := range matchedPatterns {
log.Tracef("matched => PP='%s' DP='%s'", cdm.PathPattern, cdm.DataPattern)
}
} else {
if len(changeDataHashMatchers) > 0 {
matched, found := layer.DataHashMatches[objectInfo.Name]
if !found {
log.Trace("Change data hash patterns, no match. skipping 'modify' change...")
continue
}
log.Tracef("'%s' ('modify' change) matched data hash pattern - %s", objectInfo.Name, matched.Hash)
}
}
}
if allChangesMax > -1 && allChangesCount > allChangesMax {
break
}
if modifyChangesMax > -1 && modifyChangesCount > modifyChangesMax {
break
}
if layerChangesMax > -1 && layerChangesCount > layerChangesMax {
break
}
if _, ok := changesOutputs["report"]; ok {
layerReport.Modified = append(layerReport.Modified, objectInfo)
}
if _, ok := changesOutputs["console"]; ok {
printObject(xc, objectInfo)
}
}
xc.Out.Info("layer.objects.modified.end")
}
if _, ok := changes["add"]; ok && len(layer.Changes.Added) > 0 {
xc.Out.Info("layer.objects.added.start")
for _, objectIdx := range layer.Changes.Added {
allChangesCount++
addChangesCount++
layerChangesCount++
objectInfo := layer.Objects[objectIdx]
//TODO: add a flag to select change type to apply path patterns
match := objectInfo.PathMatch
if !match && len(changePathMatchers) > 0 {
log.Tracef("Change path patterns, no match. skipping 'add' change ['%s']", objectInfo.Name)
continue
} else {
if len(changeDataMatchers) > 0 {
matchedPatterns, found := layer.DataMatches[objectInfo.Name]
if !found {
log.Tracef("change data patterns, no match. skipping change ['%s']", objectInfo.Name)
continue
}
log.Tracef("'%s' ('add' change) matched data patterns - %d", objectInfo.Name, len(matchedPatterns))
for _, cdm := range matchedPatterns {
log.Tracef("matched => PP='%s' DP='%s'", cdm.PathPattern, cdm.DataPattern)
}
} else {
if len(changeDataHashMatchers) > 0 {
matched, found := layer.DataHashMatches[objectInfo.Name]
if !found {
log.Trace("Change data hash patterns, no match. skipping 'add' change...")
continue
}
log.Tracef("'%s' ('add' change) matched data hash pattern - %s", objectInfo.Name, matched.Hash)
}
}
}
if allChangesMax > -1 && allChangesCount > allChangesMax {
break
}
if addChangesMax > -1 && addChangesCount > addChangesMax {
break
}
if layerChangesMax > -1 && layerChangesCount > layerChangesMax {
break
}
if _, ok := changesOutputs["report"]; ok {
layerReport.Added = append(layerReport.Added, layer.Objects[objectIdx])
}
if _, ok := changesOutputs["console"]; ok {
printObject(xc, layer.Objects[objectIdx])
}
}
xc.Out.Info("layer.objects.added.end")
}
}
if doShow(changeMatchLayersOnly, layer) {
xc.Out.Info("layer.end")
}
}
for _, info := range pkg.OSShells {
xc.Out.Info("image.shells",
ovars{
"full_name": info.FullName,
"short_name": info.ShortName,
"exe_path": info.ExePath,
"link_path": info.LinkPath,
"reference": info.Reference,
"verified": info.Verified,
})
cmdReport.ImageReport.OSShells = append(cmdReport.ImageReport.OSShells, info)
}
xc.Out.Info("image.entry",
ovars{
"exe_path": cmdReport.SourceImage.ContainerEntry.ExePath,
"exe_args": strings.Join(cmdReport.SourceImage.ContainerEntry.ExeArgs, ","),
})
if cmdReport.SourceImage.ContainerEntry.FullExePath != nil {
xc.Out.Info("image.entry.full_exe_path",
ovars{
"name": cmdReport.SourceImage.ContainerEntry.FullExePath.Name,
"layer": cmdReport.SourceImage.ContainerEntry.FullExePath.Layer,
})
}
if len(cmdReport.SourceImage.ContainerEntry.ArgFiles) > 0 {
for _, argFile := range cmdReport.SourceImage.ContainerEntry.ArgFiles {
xc.Out.Info("image.entry.arg_file",
ovars{
"name": argFile.Name,
"layer": argFile.Layer,
})
}
}
if doShowSpecialPerms &&
(len(pkg.SpecialPermRefs.Setuid) > 0 ||
len(pkg.SpecialPermRefs.Setgid) > 0 ||
len(pkg.SpecialPermRefs.Sticky) > 0) {
cmdReport.ImageReport.SpecialPerms = &dockerimage.SpecialPermsInfo{}
for name := range pkg.SpecialPermRefs.Setuid {
xc.Out.Info("image.special_perms.setuid",
ovars{
"name": name,
})
cmdReport.ImageReport.SpecialPerms.Setuid =
append(cmdReport.ImageReport.SpecialPerms.Setuid, name)
}
for name := range pkg.SpecialPermRefs.Setgid {
xc.Out.Info("image.special_perms.setgid",
ovars{
"name": name,
})
cmdReport.ImageReport.SpecialPerms.Setgid =
append(cmdReport.ImageReport.SpecialPerms.Setgid, name)
}
for name := range pkg.SpecialPermRefs.Sticky {
xc.Out.Info("image.special_perms.sticky",
ovars{
"name": name,
})
cmdReport.ImageReport.SpecialPerms.Sticky =
append(cmdReport.ImageReport.SpecialPerms.Sticky, name)
}
}
if doDetectDuplicates && len(pkg.HashReferences) > 0 {
cmdReport.ImageReport.Duplicates = map[string]*dockerimage.DuplicateFilesReport{}
//TODO: show duplicates by duplicate total size (biggest waste first)
for hash, hobjects := range pkg.HashReferences {
var dfr *dockerimage.DuplicateFilesReport
showStart := true
for fpath, info := range hobjects {
if showStart {
dfr = &dockerimage.DuplicateFilesReport{
Files: map[string]int{},
FileCount: uint64(len(hobjects)),
FileSize: uint64(info.Size),
AllFileSize: uint64(info.Size * int64(len(hobjects))),
}
dfr.WastedSize = dfr.AllFileSize - dfr.FileSize
cmdReport.ImageReport.Duplicates[hash] = dfr
if doShowDuplicates {
xc.Out.Info("image.duplicates.set.start",
ovars{
"hash": hash,
"count": dfr.FileCount,
"size.bytes": dfr.FileSize,
"size.human": humanize.Bytes(uint64(dfr.FileSize)),
"all_size.bytes": dfr.AllFileSize,
"size_total.human": humanize.Bytes(dfr.AllFileSize),
"wasted.bytes": dfr.WastedSize,
"wasted.human": humanize.Bytes(dfr.WastedSize),
})
}
showStart = false
}
dfr.Files[fpath] = info.LayerIndex
if doShowDuplicates {
xc.Out.Info("image.duplicates.object",
ovars{
"name": fpath,
"layer": info.LayerIndex,
})
}
}
if doShowDuplicates {
xc.Out.Info("image.duplicates.set.end")
}
}
}
}
func objectHistoryString(history *dockerimage.ObjectHistory) string {
if history == nil {
return "H=[]"
}
var builder strings.Builder
builder.WriteString("H=[")
if history.Add != nil {
builder.WriteString(fmt.Sprintf("A:%d", history.Add.Layer))
}
if history.Add != nil {
var idxList []string
for _, mod := range history.Modifies {
idxList = append(idxList, fmt.Sprintf("%d", mod.Layer))
}
if len(idxList) > 0 {
builder.WriteString(fmt.Sprintf("/M:%s", strings.Join(idxList, ",")))
}
}
if history.Delete != nil {
builder.WriteString(fmt.Sprintf("/D:%d", history.Delete.Layer))
}
builder.WriteString("]")
return builder.String()
}
func printObject(xc *app.ExecutionContext, object *dockerimage.ObjectMetadata) {
var hashInfo string
if object.Hash != "" {
hashInfo = fmt.Sprintf(" hash=%s", object.Hash)
}
fmt.Printf("%s: mode=%s size.human='%v' size.bytes=%d uid=%d gid=%d mtime='%s' %s%s '%s'",
object.Change,
object.Mode,
humanize.Bytes(uint64(object.Size)),
object.Size,
object.UID,
object.GID,
object.ModTime.UTC().Format(time.RFC3339),
objectHistoryString(object.History),
hashInfo,
object.Name)
if object.LinkTarget != "" {
fmt.Printf(" -> '%s'\n", object.LinkTarget)
} else {
fmt.Printf("\n")
}
}