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

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package resources
     5  
     6  import (
     7  	"errors"
     8  	"regexp"
     9  	"sync"
    10  
    11  	"github.com/rs/zerolog/log"
    12  	"go.mondoo.com/cnquery/llx"
    13  	"go.mondoo.com/cnquery/providers-sdk/v1/plugin"
    14  	"go.mondoo.com/cnquery/providers/os/connection/shared"
    15  	"go.mondoo.com/cnquery/providers/os/resources/packages"
    16  	"go.mondoo.com/cnquery/utils/multierr"
    17  )
    18  
    19  var PKG_IDENTIFIER = regexp.MustCompile(`^(.*):\/\/(.*)\/(.*)\/(.*)$`)
    20  
    21  // A system package cannot be installed twice but there are edge cases:
    22  // - the same package name could be installed for multiple archs
    23  // - linux-kernel package get extra treatment and can co-exist in multiple versions
    24  // We use identifiers similar to grafeas artifact identifier for packages
    25  // - deb://name/version/arch
    26  // - rpm://name/version/arch
    27  func (x *mqlPackage) id() (string, error) {
    28  	return x.Format.Data + "://" + x.Name.Data + "/" + x.Version.Data + "/" + x.Arch.Data, nil
    29  }
    30  
    31  func initPackage(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) {
    32  	// we only look up the package, if we have been supplied by its name and nothing else
    33  	raw, ok := args["name"]
    34  	if !ok || len(args) != 1 {
    35  		return args, nil, nil
    36  	}
    37  	name := raw.Value.(string)
    38  
    39  	pkgs, err := CreateResource(runtime, "packages", nil)
    40  	if err != nil {
    41  		return nil, nil, multierr.Wrap(err, "cannot get list of packages")
    42  	}
    43  	packages := pkgs.(*mqlPackages)
    44  
    45  	if err = packages.refreshCache(nil); err != nil {
    46  		return nil, nil, err
    47  	}
    48  
    49  	if res, ok := packages.packagesByName[name]; ok {
    50  		return nil, res, nil
    51  	}
    52  
    53  	res := &mqlPackage{}
    54  	res.Name = plugin.TValue[string]{Data: name, State: plugin.StateIsSet}
    55  	res.Installed = plugin.TValue[bool]{Data: false, State: plugin.StateIsSet}
    56  	res.Outdated = plugin.TValue[bool]{Data: false, State: plugin.StateIsSet}
    57  	res.Version.State = plugin.StateIsSet | plugin.StateIsNull
    58  	res.Epoch.State = plugin.StateIsSet | plugin.StateIsNull
    59  	res.Available.State = plugin.StateIsSet | plugin.StateIsNull
    60  	res.Description.State = plugin.StateIsSet | plugin.StateIsNull
    61  	res.Arch.State = plugin.StateIsSet | plugin.StateIsNull
    62  	res.Format.State = plugin.StateIsSet | plugin.StateIsNull
    63  	res.Origin.State = plugin.StateIsSet | plugin.StateIsNull
    64  	res.Status.State = plugin.StateIsSet | plugin.StateIsNull
    65  	return nil, res, nil
    66  }
    67  
    68  func (p *mqlPackage) status() (string, error) {
    69  	return "", nil
    70  }
    71  
    72  func (p *mqlPackage) outdated() (bool, error) {
    73  	if len(p.Available.Data) > 0 {
    74  		return true, nil
    75  	}
    76  	return false, nil
    77  }
    78  
    79  func (p *mqlPackage) origin() (string, error) {
    80  	return "", nil
    81  }
    82  
    83  type mqlPackagesInternal struct {
    84  	lock           sync.Mutex
    85  	packagesByName map[string]*mqlPackage
    86  }
    87  
    88  func (x *mqlPackages) list() ([]interface{}, error) {
    89  	x.lock.Lock()
    90  	defer x.lock.Unlock()
    91  
    92  	conn := x.MqlRuntime.Connection.(shared.Connection)
    93  	pm, err := packages.ResolveSystemPkgManager(conn)
    94  	if pm == nil || err != nil {
    95  		return nil, errors.New("could not detect suitable package manager for platform")
    96  	}
    97  
    98  	// retrieve all system packages
    99  	osPkgs, err := pm.List()
   100  	if err != nil {
   101  		return nil, multierr.Wrap(err, "could not retrieve package list for platform")
   102  	}
   103  
   104  	// TODO: do we really need to make this a blocking call, we could update available updates async
   105  	// we try to retrieve the available updates
   106  	osAvailablePkgs, err := pm.Available()
   107  	if err != nil {
   108  		log.Debug().Err(err).Msg("mql[packages]> could not retrieve available updates")
   109  		osAvailablePkgs = map[string]packages.PackageUpdate{}
   110  	}
   111  
   112  	// make available updates easily findable
   113  	// we use packagename-arch as identifier
   114  	availableMap := make(map[string]packages.PackageUpdate)
   115  	for _, a := range osAvailablePkgs {
   116  		availableMap[a.Name+"/"+a.Arch] = a
   117  	}
   118  
   119  	// create MQL package os for each package
   120  	pkgs := make([]interface{}, len(osPkgs))
   121  	for i, osPkg := range osPkgs {
   122  		// check if we found a newer version
   123  		available := ""
   124  		update, ok := availableMap[osPkg.Name+"/"+osPkg.Arch]
   125  		if ok {
   126  			available = update.Available
   127  			log.Debug().Str("package", osPkg.Name).Str("available", update.Available).Msg("mql[packages]> found newer version")
   128  		}
   129  
   130  		pkg, err := CreateResource(x.MqlRuntime, "package", map[string]*llx.RawData{
   131  			"name":        llx.StringData(osPkg.Name),
   132  			"version":     llx.StringData(osPkg.Version),
   133  			"available":   llx.StringData(available),
   134  			"arch":        llx.StringData(osPkg.Arch),
   135  			"status":      llx.StringData(osPkg.Status),
   136  			"description": llx.StringData(osPkg.Description),
   137  			"format":      llx.StringData(osPkg.Format),
   138  			"installed":   llx.BoolData(true),
   139  			"origin":      llx.StringData(osPkg.Origin),
   140  			"epoch":       llx.NilData, // TODO: support Epoch
   141  		})
   142  		if err != nil {
   143  			return nil, err
   144  		}
   145  
   146  		pkgs[i] = pkg
   147  	}
   148  
   149  	return pkgs, x.refreshCache(pkgs)
   150  }
   151  
   152  func (x *mqlPackages) refreshCache(all []interface{}) error {
   153  	if all == nil {
   154  		raw := x.GetList()
   155  		if raw.Error != nil {
   156  			return raw.Error
   157  		}
   158  		all = raw.Data
   159  	}
   160  
   161  	x.packagesByName = map[string]*mqlPackage{}
   162  
   163  	for i := range all {
   164  		u := all[i].(*mqlPackage)
   165  		x.packagesByName[u.Name.Data] = u
   166  	}
   167  
   168  	return nil
   169  }