diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 25b63b63d0..43e83dbd1a 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -63,3 +63,7 @@ vmknics Vtpm vulnerabilityassessmentsettings wil +mgroup +muser +testutils +nullgroup diff --git a/.gitignore b/.gitignore index 2d6367c79e..b233f4d3a0 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ gha-creds-*.json providers/*/dist providers/*/resources/*.resources.json providers/*/resources/*.manifest.json +providers-sdk/*/testutils/mockprovider/resources/*.resources.json !providers/core/resources/*.resources.json diff --git a/Makefile b/Makefile index 09ef78312c..9845e9e2db 100644 --- a/Makefile +++ b/Makefile @@ -151,7 +151,8 @@ providers/lr: .PHONY: providers/build # Note we need \ to escape the target line into multiple lines -providers/build: providers/build/core \ +providers/build: providers/build/mock \ + providers/build/core \ providers/build/network \ providers/build/os \ providers/build/ipmi \ @@ -173,6 +174,9 @@ providers/build: providers/build/core \ providers/build/ms365 \ providers/build/aws +providers/build/mock: providers/lr + ./lr go providers-sdk/v1/testutils/mockprovider/resources/mockprovider.lr + providers/build/core: providers/lr @$(call buildProvider, providers/core) diff --git a/mql/mql_test.go b/mql/mql_test.go index a60f112618..93f03d53bb 100644 --- a/mql/mql_test.go +++ b/mql/mql_test.go @@ -15,6 +15,7 @@ import ( "go.mondoo.com/cnquery/llx" "go.mondoo.com/cnquery/mql" "go.mondoo.com/cnquery/providers-sdk/v1/testutils" + "go.mondoo.com/cnquery/types" ) var features cnquery.Features @@ -154,3 +155,37 @@ func TestResourceAliases(t *testing.T) { }, }) } + +func TestNullResources(t *testing.T) { + x := testutils.InitTester(testutils.LinuxMock()) + x.TestSimple(t, []testutils.SimpleTest{ + { + Code: "muser.group", + ResultIndex: 0, + Expectation: &llx.MockResource{Name: "mgroup", ID: "group one"}, + }, + { + Code: "muser.nullgroup", + ResultIndex: 0, + Expectation: nil, + }, + { + Code: "muser.groups", + ResultIndex: 0, + Expectation: []interface{}{ + &llx.MockResource{Name: "mgroup", ID: "group one"}, + nil, + }, + }, + { + Code: "muser { nullgroup }", + ResultIndex: 0, + Expectation: map[string]interface{}{ + "_": &llx.RawData{Type: types.Resource("muser"), Value: &llx.MockResource{Name: "muser"}}, + "__s": llx.NilData, + "__t": llx.BoolTrue, + "A8qiFMpyfjKsr3OzVu+L+43W0BvYXoCPiwM7zu8AFQkBYEBMvZfR73ZsdfIqswmN1n9Qs/Soc1D7qxJipXv/ZA==": llx.ResourceData(nil, "mgroup"), + }, + }, + }) +} diff --git a/providers-sdk/v1/testutils/mockprovider/mockprovider.go b/providers-sdk/v1/testutils/mockprovider/mockprovider.go new file mode 100644 index 0000000000..5fa0704796 --- /dev/null +++ b/providers-sdk/v1/testutils/mockprovider/mockprovider.go @@ -0,0 +1,130 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package mockprovider + +import ( + "errors" + "strconv" + "strings" + + "go.mondoo.com/cnquery/llx" + "go.mondoo.com/cnquery/providers-sdk/v1/plugin" + "go.mondoo.com/cnquery/providers-sdk/v1/testutils/mockprovider/resources" +) + +var Config = plugin.Provider{ + Name: "mock", + ID: "go.mondoo.com/cnquery/providers-sdk/v1/testutils/mockprovider", + Version: "0.0.0", + Connectors: []plugin.Connector{}, +} + +type Service struct { + runtimes map[uint32]*plugin.Runtime + lastConnectionID uint32 +} + +func Init() *Service { + return &Service{ + runtimes: map[uint32]*plugin.Runtime{}, + } +} + +func (s *Service) ParseCLI(req *plugin.ParseCLIReq) (*plugin.ParseCLIRes, error) { + return nil, errors.New("core doesn't offer any connectors") +} + +func (s *Service) Connect(req *plugin.ConnectReq, callback plugin.ProviderCallback) (*plugin.ConnectRes, error) { + if req == nil || req.Asset == nil { + return nil, errors.New("no connection data provided") + } + + s.lastConnectionID++ + connID := s.lastConnectionID + runtime := &plugin.Runtime{ + Callback: callback, + HasRecording: req.HasRecording, + } + s.runtimes[connID] = runtime + + return &plugin.ConnectRes{ + Id: connID, + Name: "mockprovider", + }, nil +} + +// Shutdown is automatically called when the shell closes. +// It is not necessary to implement this method. +// If you want to do some cleanup, you can do it here. +func (s *Service) Shutdown(req *plugin.ShutdownReq) (*plugin.ShutdownRes, error) { + return &plugin.ShutdownRes{}, nil +} + +func (s *Service) GetData(req *plugin.DataReq) (*plugin.DataRes, error) { + runtime, ok := s.runtimes[req.Connection] + if !ok { + return nil, errors.New("connection " + strconv.FormatUint(uint64(req.Connection), 10) + " not found") + } + + args := plugin.PrimitiveArgsToRawDataArgs(req.Args, runtime) + + if req.ResourceId == "" && req.Field == "" { + res, err := resources.CreateResource(runtime, req.Resource, args) + if err != nil { + return nil, err + } + + rd := llx.ResourceData(res, req.Resource).Result() + return &plugin.DataRes{ + Data: rd.Data, + }, nil + } + + resource, ok := runtime.Resources.Get(req.Resource + "\x00" + req.ResourceId) + if !ok { + return nil, errors.New("resource '" + req.Resource + "' (id: " + req.ResourceId + ") doesn't exist") + } + + return resources.GetData(resource, req.Field, args), nil +} + +func (s *Service) StoreData(req *plugin.StoreReq) (*plugin.StoreRes, error) { + runtime, ok := s.runtimes[req.Connection] + if !ok { + return nil, errors.New("connection " + strconv.FormatUint(uint64(req.Connection), 10) + " not found") + } + + var errs []string + for i := range req.Resources { + info := req.Resources[i] + + args, err := plugin.ProtoArgsToRawDataArgs(info.Fields) + if err != nil { + errs = append(errs, "failed to add cached "+info.Name+" (id: "+info.Id+"), failed to parse arguments") + continue + } + + resource, ok := runtime.Resources.Get(info.Name + "\x00" + info.Id) + if !ok { + resource, err = resources.CreateResource(runtime, info.Name, args) + if err != nil { + errs = append(errs, "failed to add cached "+info.Name+" (id: "+info.Id+"), creation failed: "+err.Error()) + continue + } + + runtime.Resources.Set(info.Name+"\x00"+info.Id, resource) + } + + for k, v := range args { + if err := resources.SetData(resource, k, v); err != nil { + errs = append(errs, "failed to add cached "+info.Name+" (id: "+info.Id+"), field error: "+err.Error()) + } + } + } + + if len(errs) != 0 { + return nil, errors.New(strings.Join(errs, ", ")) + } + return &plugin.StoreRes{}, nil +} diff --git a/providers-sdk/v1/testutils/mockprovider/resources/all.go b/providers-sdk/v1/testutils/mockprovider/resources/all.go new file mode 100644 index 0000000000..fbfa784649 --- /dev/null +++ b/providers-sdk/v1/testutils/mockprovider/resources/all.go @@ -0,0 +1,45 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package resources + +import ( + "go.mondoo.com/cnquery/llx" + "go.mondoo.com/cnquery/providers-sdk/v1/plugin" +) + +func (c *mqlMuser) id() (string, error) { + return c.Name.Data, nil +} + +func (c *mqlMuser) group() (*mqlMgroup, error) { + o, err := CreateResource(c.MqlRuntime, "mgroup", map[string]*llx.RawData{ + "name": llx.StringData("group one"), + }) + if err != nil { + return nil, err + } + return o.(*mqlMgroup), nil +} + +func (c *mqlMuser) nullgroup() (*mqlMgroup, error) { + c.Nullgroup.State = plugin.StateIsSet | plugin.StateIsNull + return nil, nil +} + +func (c *mqlMuser) groups() ([]interface{}, error) { + one, err := CreateResource(c.MqlRuntime, "mgroup", map[string]*llx.RawData{ + "name": llx.StringData("group one"), + }) + if err != nil { + return nil, err + } + + return []interface{}{ + one, nil, + }, nil +} + +func (c *mqlMgroup) id() (string, error) { + return c.Name.Data, nil +} diff --git a/providers-sdk/v1/testutils/mockprovider/resources/mockprovider.lr b/providers-sdk/v1/testutils/mockprovider/resources/mockprovider.lr new file mode 100644 index 0000000000..3c8a5b171e --- /dev/null +++ b/providers-sdk/v1/testutils/mockprovider/resources/mockprovider.lr @@ -0,0 +1,16 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +option provider = "go.mondoo.com/cnquery/providers-sdk/v1/testutils/mockprovider" +option go_package = "go.mondoo.com/cnquery/providers-sdk/v1/testutils/mockprovider" + +muser { + name string + group() mgroup + nullgroup() mgroup + groups() []mgroup +} + +mgroup { + name string +} diff --git a/providers-sdk/v1/testutils/mockprovider/resources/mockprovider.lr.go b/providers-sdk/v1/testutils/mockprovider/resources/mockprovider.lr.go new file mode 100644 index 0000000000..bb10296997 --- /dev/null +++ b/providers-sdk/v1/testutils/mockprovider/resources/mockprovider.lr.go @@ -0,0 +1,322 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Code generated by resources. DO NOT EDIT. + +package resources + +import ( + "errors" + + "go.mondoo.com/cnquery/llx" + "go.mondoo.com/cnquery/providers-sdk/v1/plugin" + "go.mondoo.com/cnquery/types" +) + +var resourceFactories map[string]plugin.ResourceFactory + +func init() { + resourceFactories = map[string]plugin.ResourceFactory { + "muser": { + // to override args, implement: initMuser(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) + Create: createMuser, + }, + "mgroup": { + // to override args, implement: initMgroup(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) + Create: createMgroup, + }, + } +} + +// NewResource is used by the runtime of this plugin to create new resources. +// Its arguments may be provided by users. This function is generally not +// used by initializing resources from recordings or from lists. +func NewResource(runtime *plugin.Runtime, name string, args map[string]*llx.RawData) (plugin.Resource, error) { + f, ok := resourceFactories[name] + if !ok { + return nil, errors.New("cannot find resource " + name + " in this provider") + } + + if f.Init != nil { + cargs, res, err := f.Init(runtime, args) + if err != nil { + return res, err + } + + if res != nil { + id := name+"\x00"+res.MqlID() + if x, ok := runtime.Resources.Get(id); ok { + return x, nil + } + runtime.Resources.Set(id, res) + return res, nil + } + + args = cargs + } + + res, err := f.Create(runtime, args) + if err != nil { + return nil, err + } + + id := name+"\x00"+res.MqlID() + if x, ok := runtime.Resources.Get(id); ok { + return x, nil + } + + runtime.Resources.Set(id, res) + return res, nil +} + +// CreateResource is used by the runtime of this plugin to create resources. +// Its arguments must be complete and pre-processed. This method is used +// for initializing resources from recordings or from lists. +func CreateResource(runtime *plugin.Runtime, name string, args map[string]*llx.RawData) (plugin.Resource, error) { + f, ok := resourceFactories[name] + if !ok { + return nil, errors.New("cannot find resource " + name + " in this provider") + } + + res, err := f.Create(runtime, args) + if err != nil { + return nil, err + } + + id := name+"\x00"+res.MqlID() + if x, ok := runtime.Resources.Get(id); ok { + return x, nil + } + + runtime.Resources.Set(id, res) + return res, nil +} + +var getDataFields = map[string]func(r plugin.Resource) *plugin.DataRes{ + "muser.name": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMuser).GetName()).ToDataRes(types.String) + }, + "muser.group": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMuser).GetGroup()).ToDataRes(types.Resource("mgroup")) + }, + "muser.nullgroup": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMuser).GetNullgroup()).ToDataRes(types.Resource("mgroup")) + }, + "muser.groups": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMuser).GetGroups()).ToDataRes(types.Array(types.Resource("mgroup"))) + }, + "mgroup.name": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMgroup).GetName()).ToDataRes(types.String) + }, +} + +func GetData(resource plugin.Resource, field string, args map[string]*llx.RawData) *plugin.DataRes { + f, ok := getDataFields[resource.MqlName()+"."+field] + if !ok { + return &plugin.DataRes{Error: "cannot find '" + field + "' in resource '" + resource.MqlName() + "'"} + } + + return f(resource) +} + +var setDataFields = map[string]func(r plugin.Resource, v *llx.RawData) bool { + "muser.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMuser).__id, ok = v.Value.(string) + return + }, + "muser.name": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMuser).Name, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, + "muser.group": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMuser).Group, ok = plugin.RawToTValue[*mqlMgroup](v.Value, v.Error) + return + }, + "muser.nullgroup": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMuser).Nullgroup, ok = plugin.RawToTValue[*mqlMgroup](v.Value, v.Error) + return + }, + "muser.groups": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMuser).Groups, ok = plugin.RawToTValue[[]interface{}](v.Value, v.Error) + return + }, + "mgroup.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMgroup).__id, ok = v.Value.(string) + return + }, + "mgroup.name": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMgroup).Name, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, +} + +func SetData(resource plugin.Resource, field string, val *llx.RawData) error { + f, ok := setDataFields[resource.MqlName() + "." + field] + if !ok { + return errors.New("[mockprovider] cannot set '"+field+"' in resource '"+resource.MqlName()+"', field not found") + } + + if ok := f(resource, val); !ok { + return errors.New("[mockprovider] cannot set '"+field+"' in resource '"+resource.MqlName()+"', type does not match") + } + return nil +} + +func SetAllData(resource plugin.Resource, args map[string]*llx.RawData) error { + var err error + for k, v := range args { + if err = SetData(resource, k, v); err != nil { + return err + } + } + return nil +} + +// mqlMuser for the muser resource +type mqlMuser struct { + MqlRuntime *plugin.Runtime + __id string + // optional: if you define mqlMuserInternal it will be used here + Name plugin.TValue[string] + Group plugin.TValue[*mqlMgroup] + Nullgroup plugin.TValue[*mqlMgroup] + Groups plugin.TValue[[]interface{}] +} + +// createMuser creates a new instance of this resource +func createMuser(runtime *plugin.Runtime, args map[string]*llx.RawData) (plugin.Resource, error) { + res := &mqlMuser{ + MqlRuntime: runtime, + } + + err := SetAllData(res, args) + if err != nil { + return res, err + } + + if res.__id == "" { + res.__id, err = res.id() + if err != nil { + return nil, err + } + } + + if runtime.HasRecording { + args, err = runtime.ResourceFromRecording("muser", res.__id) + if err != nil || args == nil { + return res, err + } + return res, SetAllData(res, args) + } + + return res, nil +} + +func (c *mqlMuser) MqlName() string { + return "muser" +} + +func (c *mqlMuser) MqlID() string { + return c.__id +} + +func (c *mqlMuser) GetName() *plugin.TValue[string] { + return &c.Name +} + +func (c *mqlMuser) GetGroup() *plugin.TValue[*mqlMgroup] { + return plugin.GetOrCompute[*mqlMgroup](&c.Group, func() (*mqlMgroup, error) { + if c.MqlRuntime.HasRecording { + d, err := c.MqlRuntime.FieldResourceFromRecording("muser", c.__id, "group") + if err != nil { + return nil, err + } + if d != nil { + return d.Value.(*mqlMgroup), nil + } + } + + return c.group() + }) +} + +func (c *mqlMuser) GetNullgroup() *plugin.TValue[*mqlMgroup] { + return plugin.GetOrCompute[*mqlMgroup](&c.Nullgroup, func() (*mqlMgroup, error) { + if c.MqlRuntime.HasRecording { + d, err := c.MqlRuntime.FieldResourceFromRecording("muser", c.__id, "nullgroup") + if err != nil { + return nil, err + } + if d != nil { + return d.Value.(*mqlMgroup), nil + } + } + + return c.nullgroup() + }) +} + +func (c *mqlMuser) GetGroups() *plugin.TValue[[]interface{}] { + return plugin.GetOrCompute[[]interface{}](&c.Groups, func() ([]interface{}, error) { + if c.MqlRuntime.HasRecording { + d, err := c.MqlRuntime.FieldResourceFromRecording("muser", c.__id, "groups") + if err != nil { + return nil, err + } + if d != nil { + return d.Value.([]interface{}), nil + } + } + + return c.groups() + }) +} + +// mqlMgroup for the mgroup resource +type mqlMgroup struct { + MqlRuntime *plugin.Runtime + __id string + // optional: if you define mqlMgroupInternal it will be used here + Name plugin.TValue[string] +} + +// createMgroup creates a new instance of this resource +func createMgroup(runtime *plugin.Runtime, args map[string]*llx.RawData) (plugin.Resource, error) { + res := &mqlMgroup{ + MqlRuntime: runtime, + } + + err := SetAllData(res, args) + if err != nil { + return res, err + } + + if res.__id == "" { + res.__id, err = res.id() + if err != nil { + return nil, err + } + } + + if runtime.HasRecording { + args, err = runtime.ResourceFromRecording("mgroup", res.__id) + if err != nil || args == nil { + return res, err + } + return res, SetAllData(res, args) + } + + return res, nil +} + +func (c *mqlMgroup) MqlName() string { + return "mgroup" +} + +func (c *mqlMgroup) MqlID() string { + return c.__id +} + +func (c *mqlMgroup) GetName() *plugin.TValue[string] { + return &c.Name +} diff --git a/providers-sdk/v1/testutils/testdata/arch.json b/providers-sdk/v1/testutils/testdata/arch.json index 6935f2b138..69f4c2c087 100644 --- a/providers-sdk/v1/testutils/testdata/arch.json +++ b/providers-sdk/v1/testutils/testdata/arch.json @@ -30,6 +30,12 @@ "provider": "network", "connector": "", "version": "" + }, + { + "url": "mock://", + "provider": "mockprovider", + "connector": "", + "version": "" } ], "resources": [ diff --git a/providers-sdk/v1/testutils/testutils.go b/providers-sdk/v1/testutils/testutils.go index 7665f78b26..4dbad40a1d 100644 --- a/providers-sdk/v1/testutils/testutils.go +++ b/providers-sdk/v1/testutils/testutils.go @@ -24,6 +24,7 @@ import ( "go.mondoo.com/cnquery/providers" "go.mondoo.com/cnquery/providers-sdk/v1/lr" "go.mondoo.com/cnquery/providers-sdk/v1/resources" + "go.mondoo.com/cnquery/providers-sdk/v1/testutils/mockprovider" "go.mondoo.com/cnquery/providers/mock" networkconf "go.mondoo.com/cnquery/providers/network/config" networkprovider "go.mondoo.com/cnquery/providers/network/provider" @@ -150,7 +151,13 @@ func (ctx *tester) TestMqlc(t *testing.T, bundle *llx.CodeBundle, props map[stri } func mustLoadSchema(provider string) *resources.Schema { - path := filepath.Join(TestutilsDir, "../../../providers/"+provider+"/resources/"+provider+".lr") + var path string + if provider == "mockprovider" { + path = filepath.Join(TestutilsDir, "mockprovider/resources/mockprovider.lr") + } else { + path = filepath.Join(TestutilsDir, "../../../providers/"+provider+"/resources/"+provider+".lr") + } + res, err := lr.Resolve(path, func(path string) ([]byte, error) { return os.ReadFile(path) }) if err != nil { panic(err.Error()) @@ -168,6 +175,7 @@ func Local() llx.Runtime { osSchema := mustLoadSchema("os") coreSchema := mustLoadSchema("core") networkSchema := mustLoadSchema("network") + mockSchema := mustLoadSchema("mockprovider") runtime := providers.Coordinator.NewRuntime() @@ -188,6 +196,14 @@ func Local() llx.Runtime { } runtime.AddConnectedProvider(&providers.ConnectedProvider{Instance: provider}) + provider = &providers.RunningProvider{ + Name: mockprovider.Config.Name, + ID: mockprovider.Config.ID, + Plugin: mockprovider.Init(), + Schema: mockSchema, + } + runtime.AddConnectedProvider(&providers.ConnectedProvider{Instance: provider}) + return runtime } @@ -212,6 +228,10 @@ func mockRuntimeAbs(testdata string) llx.Runtime { if err != nil { panic("failed to set recording: " + err.Error()) } + err = runtime.SetRecording(recording, mockprovider.Config.ID, true, true) + if err != nil { + panic("failed to set recording: " + err.Error()) + } return runtime }