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 }