go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers/os/connection/tar.go (about)

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package connection
     5  
     6  import (
     7  	"archive/tar"
     8  	"bytes"
     9  	"errors"
    10  	"io"
    11  	"os"
    12  
    13  	v1 "github.com/google/go-containerregistry/pkg/v1"
    14  	"github.com/google/go-containerregistry/pkg/v1/mutate"
    15  	"github.com/google/go-containerregistry/pkg/v1/tarball"
    16  	"github.com/rs/zerolog/log"
    17  	"github.com/spf13/afero"
    18  	"go.mondoo.com/cnquery/providers-sdk/v1/inventory"
    19  	"go.mondoo.com/cnquery/providers/os/connection/container/cache"
    20  	"go.mondoo.com/cnquery/providers/os/connection/shared"
    21  	provider_tar "go.mondoo.com/cnquery/providers/os/connection/tar"
    22  	"go.mondoo.com/cnquery/providers/os/fsutil"
    23  	"go.mondoo.com/cnquery/providers/os/id/containerid"
    24  )
    25  
    26  const (
    27  	Tar              shared.ConnectionType = "tar"
    28  	OPTION_FILE                            = "path"
    29  	FLATTENED_IMAGE                        = "flattened_path"
    30  	COMPRESSED_IMAGE                       = "compressed_path"
    31  )
    32  
    33  type TarConnection struct {
    34  	id    uint32
    35  	asset *inventory.Asset
    36  	conf  *inventory.Config
    37  
    38  	Fs      *provider_tar.FS
    39  	CloseFN func()
    40  	// fields are exposed since the tar backend is re-used for the docker backend
    41  	PlatformKind         string
    42  	PlatformRuntime      string
    43  	PlatformIdentifier   string
    44  	PlatformArchitecture string
    45  	// optional metadata to store additional information
    46  	Metadata struct {
    47  		Name   string
    48  		Labels map[string]string
    49  	}
    50  }
    51  
    52  func (p *TarConnection) ID() uint32 {
    53  	return p.id
    54  }
    55  
    56  func (p *TarConnection) Name() string {
    57  	return string(Tar)
    58  }
    59  
    60  func (p *TarConnection) Type() shared.ConnectionType {
    61  	return Tar
    62  }
    63  
    64  func (p *TarConnection) Asset() *inventory.Asset {
    65  	return p.asset
    66  }
    67  
    68  func (p *TarConnection) Conf() *inventory.Config {
    69  	return p.conf
    70  }
    71  
    72  func (c *TarConnection) Identifier() (string, error) {
    73  	return c.PlatformIdentifier, nil
    74  }
    75  
    76  func (c *TarConnection) Capabilities() shared.Capabilities {
    77  	return shared.Capability_File | shared.Capability_FileSearch
    78  }
    79  
    80  func (p *TarConnection) RunCommand(command string) (*shared.Command, error) {
    81  	res := shared.Command{Command: command, Stdout: &bytes.Buffer{}, Stderr: &bytes.Buffer{}, ExitStatus: -1}
    82  	return &res, nil
    83  }
    84  
    85  func (p *TarConnection) FileSystem() afero.Fs {
    86  	return p.Fs
    87  }
    88  
    89  func (c *TarConnection) FileInfo(path string) (shared.FileInfoDetails, error) {
    90  	fs := c.FileSystem()
    91  	afs := &afero.Afero{Fs: fs}
    92  	stat, err := afs.Stat(path)
    93  	if err != nil {
    94  		return shared.FileInfoDetails{}, err
    95  	}
    96  
    97  	uid := int64(-1)
    98  	gid := int64(-1)
    99  	if stat, ok := stat.Sys().(*tar.Header); ok {
   100  		uid = int64(stat.Uid)
   101  		gid = int64(stat.Gid)
   102  	}
   103  	mode := stat.Mode()
   104  
   105  	return shared.FileInfoDetails{
   106  		Mode: shared.FileModeDetails{mode},
   107  		Size: stat.Size(),
   108  		Uid:  uid,
   109  		Gid:  gid,
   110  	}, nil
   111  }
   112  
   113  func (c *TarConnection) Close() {
   114  	if c.CloseFN != nil {
   115  		c.CloseFN()
   116  	}
   117  }
   118  
   119  func (c *TarConnection) Load(stream io.Reader) error {
   120  	tr := tar.NewReader(stream)
   121  	for {
   122  		h, err := tr.Next()
   123  		if err == io.EOF {
   124  			break
   125  		}
   126  		if err != nil {
   127  			log.Error().Err(err).Msg("tar> error reading tar stream")
   128  			return err
   129  		}
   130  
   131  		path := provider_tar.Abs(h.Name)
   132  		c.Fs.FileMap[path] = h
   133  	}
   134  	log.Debug().Int("files", len(c.Fs.FileMap)).Msg("tar> successfully loaded")
   135  	return nil
   136  }
   137  
   138  func (c *TarConnection) LoadFile(path string) error {
   139  	log.Debug().Str("path", path).Msg("tar> load tar file into backend")
   140  
   141  	f, err := os.Open(path)
   142  	if err != nil {
   143  		return err
   144  	}
   145  	defer f.Close()
   146  	return c.Load(f)
   147  }
   148  
   149  func (c *TarConnection) Kind() string {
   150  	return c.PlatformKind
   151  }
   152  
   153  func (c *TarConnection) Runtime() string {
   154  	return c.PlatformRuntime
   155  }
   156  
   157  func NewTarConnection(id uint32, conf *inventory.Config, asset *inventory.Asset) (*TarConnection, error) {
   158  	return NewWithClose(id, conf, asset, nil)
   159  }
   160  
   161  // NewWithReader provides a tar provider from a container image stream
   162  func NewWithReader(id uint32, conf *inventory.Config, asset *inventory.Asset, rc io.ReadCloser) (*TarConnection, error) {
   163  	filename := ""
   164  	if x, ok := rc.(*os.File); ok {
   165  		filename = x.Name()
   166  	} else {
   167  		// cache file locally
   168  		f, err := cache.RandomFile()
   169  		if err != nil {
   170  			return nil, err
   171  		}
   172  
   173  		// we return a pure tar image
   174  		filename = f.Name()
   175  
   176  		err = cache.StreamToTmpFile(rc, f)
   177  		if err != nil {
   178  			os.Remove(filename)
   179  			return nil, err
   180  		}
   181  	}
   182  
   183  	return NewWithClose(id, &inventory.Config{
   184  		Type:    "tar",
   185  		Runtime: "docker-image",
   186  		Options: map[string]string{
   187  			OPTION_FILE: filename,
   188  		},
   189  	}, asset, func() {
   190  		log.Debug().Str("tar", filename).Msg("tar> remove temporary tar file on connection close")
   191  		os.Remove(filename)
   192  	})
   193  }
   194  
   195  func NewWithClose(id uint32, conf *inventory.Config, asset *inventory.Asset, closeFn func()) (*TarConnection, error) {
   196  	if conf == nil || len(conf.Options[OPTION_FILE]) == 0 {
   197  		return nil, errors.New("tar provider requires a valid tar file")
   198  	}
   199  
   200  	filename := conf.Options[OPTION_FILE]
   201  	var identifier string
   202  
   203  	// try to determine if the tar is a container image
   204  	img, iErr := tarball.ImageFromPath(filename, nil)
   205  	if iErr == nil {
   206  		hash, err := img.Digest()
   207  		if err != nil {
   208  			return nil, err
   209  		}
   210  		identifier = containerid.MondooContainerImageID(hash.String())
   211  
   212  		// we cache the flattened image locally
   213  		c, err := newWithFlattenedImage(id, conf, asset, &img, closeFn)
   214  		if err != nil {
   215  			return nil, err
   216  		}
   217  
   218  		c.PlatformIdentifier = identifier
   219  		return c, nil
   220  	} else {
   221  		hash, err := fsutil.LocalFileSha256(filename)
   222  		if err != nil {
   223  			return nil, err
   224  		}
   225  		identifier = "//platformid.api.mondoo.app/runtime/tar/hash/" + hash
   226  
   227  		c := &TarConnection{
   228  			id:              id,
   229  			asset:           asset,
   230  			Fs:              provider_tar.NewFs(filename),
   231  			CloseFN:         closeFn,
   232  			PlatformKind:    conf.Type,
   233  			PlatformRuntime: conf.Runtime,
   234  		}
   235  
   236  		err = c.LoadFile(filename)
   237  		if err != nil {
   238  			log.Error().Err(err).Str("tar", filename).Msg("tar> could not load tar file")
   239  			return nil, err
   240  		}
   241  
   242  		c.PlatformIdentifier = identifier
   243  		return c, nil
   244  	}
   245  }
   246  
   247  func newWithFlattenedImage(id uint32, conf *inventory.Config, asset *inventory.Asset, img *v1.Image, closeFn func()) (*TarConnection, error) {
   248  	imageFilename := ""
   249  	useCached := false
   250  	if asset != nil && len(asset.Connections) > 0 {
   251  		if x, ok := asset.Connections[0].Options[FLATTENED_IMAGE]; ok && x != "" {
   252  			log.Debug().Str("tar", asset.Connections[0].Options[FLATTENED_IMAGE]).Msg("tar> use cached tar file")
   253  			imageFilename = asset.Connections[0].Options[FLATTENED_IMAGE]
   254  			useCached = true
   255  		}
   256  	}
   257  	if !useCached {
   258  		f, err := cache.RandomFile()
   259  		if err != nil {
   260  			return nil, err
   261  		}
   262  		imageFilename = f.Name()
   263  		err = cache.StreamToTmpFile(mutate.Extract(*img), f)
   264  		if err != nil {
   265  			os.Remove(imageFilename)
   266  			return nil, err
   267  		}
   268  	}
   269  
   270  	c := &TarConnection{
   271  		id:    id,
   272  		asset: asset,
   273  		Fs:    provider_tar.NewFs(imageFilename),
   274  		CloseFN: func() {
   275  			if closeFn != nil {
   276  				closeFn()
   277  			}
   278  			// remove temporary file on stream close
   279  			log.Debug().Str("tar", imageFilename).Msg("tar> remove temporary flattened image file on connection close")
   280  			os.Remove(imageFilename)
   281  		},
   282  		PlatformKind:    conf.Type,
   283  		PlatformRuntime: conf.Runtime,
   284  		conf: &inventory.Config{
   285  			Options: map[string]string{
   286  				OPTION_FILE: imageFilename,
   287  			},
   288  		},
   289  	}
   290  	if asset != nil && len(asset.Connections) > 0 {
   291  		if asset.Connections[0].Options == nil {
   292  			asset.Connections[0].Options = map[string]string{}
   293  		}
   294  		asset.Connections[0].Options[FLATTENED_IMAGE] = imageFilename
   295  	}
   296  
   297  	err := c.LoadFile(imageFilename)
   298  	if err != nil {
   299  		log.Error().Err(err).Str("tar", imageFilename).Msg("tar> could not load tar file")
   300  		os.Remove(imageFilename)
   301  		return nil, err
   302  	}
   303  
   304  	return c, nil
   305  }