diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index cf213c8c17..25b63b63d0 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -3,6 +3,7 @@ activitylog aks asns Auths +autoaccept autoscaler backupconfiguration bigquery @@ -27,14 +28,19 @@ hostkeys iap iex ilb +iotedge ingresstls loggingservice managedzone +mcr messagestoragepolicy mfs Mpim nodepool oci +opc +opcplc +opcua Pids postgre pushconfig @@ -43,11 +49,13 @@ resourcegroup Sas serviceprincipals Snat +sph spo sqlserver sshkeys toplevel tpu +uint vcn vcns vdcs diff --git a/Makefile b/Makefile index 810eecaa75..1a3758ee2e 100644 --- a/Makefile +++ b/Makefile @@ -126,6 +126,9 @@ lr/build: ./lr go resources/packs/oci/oci.lr ./lr docs yaml resources/packs/oci/oci.lr --docs-file resources/packs/oci/oci.lr.manifest.yaml ./lr docs json resources/packs/oci/oci.lr.manifest.yaml + ./lr go resources/packs/opcua/opcua.lr + ./lr docs yaml resources/packs/opcua/opcua.lr --docs-file resources/packs/opcua/opcua.lr.manifest.yaml + ./lr docs json resources/packs/opcua/opcua.lr.manifest.yaml lr/release: go generate . @@ -239,6 +242,11 @@ lr/docs/markdown: lr/build --description "The Oracle Cloud Infrastructure (OCI) resource pack lets you use MQL to query and assess the security of your OCI cloud services." \ --docs-file resources/packs/oci/oci.lr.manifest.yaml \ --output ../docs/docs/mql/resources/oci-pack + ./lr markdown resources/packs/opcua/opcua.lr \ + --pack-name "OPC UP" \ + --description "The OPC-UA resource pack lets you use MQL to query and assess the security of your OPC-UA servers." \ + --docs-file resources/packs/opcua/opcua.lr.manifest.yaml \ + --output ../docs/docs/mql/resources/opcua-pack .PHONY: resources resources: | lr resources/generate resources/test diff --git a/apps/cnquery/cmd/builder/builder.go b/apps/cnquery/cmd/builder/builder.go index 44206d9f98..9c56a789f4 100644 --- a/apps/cnquery/cmd/builder/builder.go +++ b/apps/cnquery/cmd/builder/builder.go @@ -179,6 +179,7 @@ func buildCmd(baseCmd *cobra.Command, commonCmdFlags common.CommonFlagsFn, preRu baseCmd.AddCommand(scanSlackCmd(commonCmdFlags, preRun, runFn, docs)) baseCmd.AddCommand(scanVcdCmd(commonCmdFlags, preRun, runFn, docs)) baseCmd.AddCommand(scanFilesystemCmd(commonCmdFlags, preRun, runFn, docs)) + baseCmd.AddCommand(scanOpcUaCmd(commonCmdFlags, preRun, runFn, docs)) } func localProviderCmd(commonCmdFlags common.CommonFlagsFn, preRun common.CommonPreRunFn, runFn runFn, docs common.CommandsDocs) *cobra.Command { @@ -547,3 +548,11 @@ func scanFilesystemCmd(commonCmdFlags common.CommonFlagsFn, preRun common.Common cmd := common.ScanFilesystemCmd(commonCmdFlags, preRun, wrapRunFn, docs) return cmd } + +func scanOpcUaCmd(commonCmdFlags common.CommonFlagsFn, preRun common.CommonPreRunFn, runFn runFn, docs common.CommandsDocs) *cobra.Command { + wrapRunFn := func(cmd *cobra.Command, args []string) { + runFn(cmd, args, providers.ProviderType_OPCUA, DefaultAssetType) + } + cmd := common.ScanOpcUACmd(commonCmdFlags, preRun, wrapRunFn, docs) + return cmd +} diff --git a/apps/cnquery/cmd/builder/common/common.go b/apps/cnquery/cmd/builder/common/common.go index 2034d137e0..c04a4f18e3 100644 --- a/apps/cnquery/cmd/builder/common/common.go +++ b/apps/cnquery/cmd/builder/common/common.go @@ -788,3 +788,20 @@ func ScanFilesystemCmd(commonCmdFlags CommonFlagsFn, preRun CommonPreRunFn, runF commonCmdFlags(cmd) return cmd } + +func ScanOpcUACmd(commonCmdFlags CommonFlagsFn, preRun CommonPreRunFn, runFn RunFn, docs CommandsDocs) *cobra.Command { + cmd := &cobra.Command{ + Use: "opcua", + Short: docs.GetShort("opcua"), + Long: docs.GetLong("opcua"), + Args: cobra.ExactArgs(0), + PreRun: func(cmd *cobra.Command, args []string) { + viper.BindPFlag("endpoint", cmd.Flags().Lookup("endpoint")) + preRun(cmd, args) + }, + Run: runFn, + } + commonCmdFlags(cmd) + cmd.Flags().String("endpoint", "", "OPC UA service endpoint") + return cmd +} diff --git a/apps/cnquery/cmd/builder/parse.go b/apps/cnquery/cmd/builder/parse.go index 366117c102..54ffaadbca 100644 --- a/apps/cnquery/cmd/builder/parse.go +++ b/apps/cnquery/cmd/builder/parse.go @@ -745,6 +745,21 @@ func ParseTargetAsset(cmd *cobra.Command, args []string, providerType providers. case providers.ProviderType_FS: connection.Backend = providerType connection.Options["path"] = filepath + case providers.ProviderType_OPCUA: + connection.Backend = providerType + + cred := &vault.Credential{ + Type: vault.CredentialType_password, + Password: password, + } + + if x, err := cmd.Flags().GetString("endpoint"); err != nil { + log.Fatal().Err(err).Msg("cannot parse --endpoint value") + } else if x != "" { + connection.Options["endpoint"] = x + } + + connection.Credentials = append(connection.Credentials, cred) } parsedAsset.Connections = []*providers.Config{connection} diff --git a/apps/cnquery/cmd/scan.go b/apps/cnquery/cmd/scan.go index 756b536118..4ece075738 100644 --- a/apps/cnquery/cmd/scan.go +++ b/apps/cnquery/cmd/scan.go @@ -250,6 +250,9 @@ This example connects to Microsoft 365 using the PEM formatted certificate: "filesystem": { Short: "Scan a mounted file system target.", }, + "opcua": { + Short: "Scan a OPC UA endpoint.", + }, }, }, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { diff --git a/apps/cnquery/cmd/shell.go b/apps/cnquery/cmd/shell.go index 36b2151c2d..685d50f6b6 100644 --- a/apps/cnquery/cmd/shell.go +++ b/apps/cnquery/cmd/shell.go @@ -80,16 +80,16 @@ Provide the recording with mock data as an argument: `, }, "vagrant": { - Short: "Scan a Vagrant host.", + Short: "Connect to a Vagrant host.", }, "terraform": { - Short: "Scan Terraform HCL (files.tf and directories), plan files (json), and state files (json).", + Short: "Connect to Terraform HCL (files.tf and directories), plan files (json), and state files (json).", }, "ssh": { - Short: "Scan an SSH target.", + Short: "Connect to an SSH target.", }, "winrm": { - Short: "Scan a WinRM target.", + Short: "Connect to a WinRM target.", }, "container": { Short: "Connect to a container, image, or registry.", @@ -242,6 +242,9 @@ This example connects to Microsoft 365 using the PKCS #12 formatted certificate: "filesystem": { Short: "Connect to a mounted file system target.", }, + "opcua": { + Short: "Connect to a OPC UA endpoint.", + }, }, }, Run: func(cmd *cobra.Command, args []string, provider providers.ProviderType, assetType builder.AssetType) { diff --git a/go.mod b/go.mod index dacbc0a2f0..ddc70d220d 100644 --- a/go.mod +++ b/go.mod @@ -118,6 +118,7 @@ require ( github.com/google/go-containerregistry v0.12.1 github.com/google/go-github/v49 v49.1.0 github.com/google/uuid v1.3.0 + github.com/gopcua/opcua v0.3.13 github.com/gosimple/slug v1.13.1 github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-hclog v1.2.0 @@ -172,7 +173,7 @@ require ( github.com/zclconf/go-cty v1.10.0 go.mondoo.com/ranger-rpc v0.0.0-20230328135530-12135c17095f go.opentelemetry.io/otel v1.14.0 - golang.org/x/crypto v0.5.0 + golang.org/x/crypto v0.7.0 golang.org/x/net v0.8.0 golang.org/x/oauth2 v0.5.0 golang.org/x/sync v0.1.0 diff --git a/go.sum b/go.sum index f0f71e29f8..681d48d4af 100644 --- a/go.sum +++ b/go.sum @@ -712,6 +712,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gopcua/opcua v0.3.13 h1:33qX6pjZraA65+j7yx7hnDk/M8WMSkbTrJHtIVghTNU= +github.com/gopcua/opcua v0.3.13/go.mod h1:DVDwHvR5lYgO9T4nTn+QxzGl6VQMywgaRgmOH1skBps= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gordonklaus/ineffassign v0.0.0-20230107090616-13ace0543b28 h1:9alfqbrhuD+9fLZ4iaAVwhlp5PEhmnBt7yvK2Oy5C1U= @@ -1416,8 +1418,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= -golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/motor/discovery/opcua/resolver.go b/motor/discovery/opcua/resolver.go new file mode 100644 index 0000000000..3a9ae8d09a --- /dev/null +++ b/motor/discovery/opcua/resolver.go @@ -0,0 +1,61 @@ +package opcua + +import ( + "context" + "errors" + + "go.mondoo.com/cnquery/motor/asset" + "go.mondoo.com/cnquery/motor/discovery/common" + "go.mondoo.com/cnquery/motor/providers" + opcua_provider "go.mondoo.com/cnquery/motor/providers/opcua" + "go.mondoo.com/cnquery/motor/providers/resolver" + "go.mondoo.com/cnquery/motor/vault" +) + +type Resolver struct{} + +func (k *Resolver) Name() string { + return "OPC-UA Resolver" +} + +func (r *Resolver) AvailableDiscoveryTargets() []string { + return []string{common.DiscoveryAuto, common.DiscoveryAll} +} + +func (r *Resolver) Resolve(ctx context.Context, root *asset.Asset, cc *providers.Config, credsResolver vault.Resolver, sfn common.QuerySecretFn, userIdDetectors ...providers.PlatformIdDetector) ([]*asset.Asset, error) { + resolved := []*asset.Asset{} + + m, err := resolver.NewMotorConnection(ctx, cc, credsResolver) + if err != nil { + return nil, err + } + defer m.Close() + + provider, ok := m.Provider.(*opcua_provider.Provider) + if !ok { + return nil, errors.New("could not create ms OPC UA provider") + } + + identifier, err := provider.Identifier() + if err != nil { + return nil, err + } + + // detect platform info for the asset + pf, err := m.Platform() + if err != nil { + return nil, err + } + + if cc.IncludesOneOfDiscoveryTarget(common.DiscoveryAuto, common.DiscoveryAll) { + resolved = append(resolved, &asset.Asset{ + PlatformIds: []string{identifier}, + Name: "OPC UA server", + Platform: pf, + Connections: []*providers.Config{cc}, // pass-in the current config + Labels: map[string]string{}, + }) + } + + return resolved, nil +} diff --git a/motor/discovery/resolve.go b/motor/discovery/resolve.go index 51b17d22b0..34aa8c8e3e 100644 --- a/motor/discovery/resolve.go +++ b/motor/discovery/resolve.go @@ -14,8 +14,6 @@ import ( "context" "strings" - "go.mondoo.com/cnquery/motor/discovery/oci" - "github.com/cockroachdb/errors" "github.com/rs/zerolog/log" "go.mondoo.com/cnquery/motor/asset" @@ -36,7 +34,9 @@ import ( "go.mondoo.com/cnquery/motor/discovery/mock" "go.mondoo.com/cnquery/motor/discovery/ms365" "go.mondoo.com/cnquery/motor/discovery/network" + "go.mondoo.com/cnquery/motor/discovery/oci" "go.mondoo.com/cnquery/motor/discovery/okta" + "go.mondoo.com/cnquery/motor/discovery/opcua" "go.mondoo.com/cnquery/motor/discovery/os" "go.mondoo.com/cnquery/motor/discovery/slack" "go.mondoo.com/cnquery/motor/discovery/tar" @@ -95,6 +95,7 @@ func init() { providers.ProviderID_SLACK: &slack.Resolver{}, providers.ProviderID_VCD: &vcd.Resolver{}, providers.ProviderID_OCI: &oci.Resolver{}, + providers.ProviderID_OPCUA: &opcua.Resolver{}, } } diff --git a/motor/platform/detector/detector.go b/motor/platform/detector/detector.go index 9b61f9b634..15319b0279 100644 --- a/motor/platform/detector/detector.go +++ b/motor/platform/detector/detector.go @@ -2,6 +2,7 @@ package detector import ( "errors" + "go.mondoo.com/cnquery/motor/providers/opcua" "runtime" "go.mondoo.com/cnquery/motor/platform" @@ -133,6 +134,8 @@ func (d *Detector) Platform() (*platform.Platform, error) { return pt.PlatformInfo() case *oci.Provider: return pt.PlatformInfo() + case *opcua.Provider: + return pt.PlatformInfo() case os.OperatingSystemProvider: var resolved bool pi, resolved = d.resolveOS(pt) diff --git a/motor/providers/id.go b/motor/providers/id.go index 7564a9be2b..ba43662d32 100644 --- a/motor/providers/id.go +++ b/motor/providers/id.go @@ -44,6 +44,7 @@ const ( ProviderID_SLACK = "slack" ProviderID_VCD = "vcd" ProviderID_OCI = "oci" + ProviderID_OPCUA = "opc-ua" // NOTE: its not mapped directly to a transport, it is transformed into ssh ProviderID_AWS_EC2_INSTANCE_CONNECT = "aws-ec2-connect" @@ -84,6 +85,7 @@ var ProviderType_id = map[ProviderType]string{ ProviderType_SLACK: ProviderID_SLACK, ProviderType_VCD: ProviderID_VCD, ProviderType_OCI: ProviderID_OCI, + ProviderType_OPCUA: ProviderID_OPCUA, } var ProviderType_idvalue = map[string]ProviderType{ @@ -121,6 +123,7 @@ var ProviderType_idvalue = map[string]ProviderType{ ProviderID_SLACK: ProviderType_SLACK, ProviderID_VCD: ProviderType_VCD, ProviderID_OCI: ProviderType_OCI, + ProviderID_OPCUA: ProviderType_OPCUA, } func (x ProviderType) Id() string { diff --git a/motor/providers/opcua/opcua.go b/motor/providers/opcua/opcua.go new file mode 100644 index 0000000000..447d789fb6 --- /dev/null +++ b/motor/providers/opcua/opcua.go @@ -0,0 +1,96 @@ +package opcua + +import ( + "context" + "errors" + "github.com/gopcua/opcua" + "github.com/gopcua/opcua/ua" + "go.mondoo.com/cnquery/motor/providers" +) + +var ( + _ providers.Instance = (*Provider)(nil) + _ providers.PlatformIdentifier = (*Provider)(nil) +) + +func New(pCfg *providers.Config) (*Provider, error) { + if pCfg.Backend != providers.ProviderType_OPCUA { + return nil, providers.ErrProviderTypeDoesNotMatch + } + + if pCfg.Options == nil || pCfg.Options["endpoint"] == "" { + return nil, errors.New("opcua provider requires an endpoint. please set option `endpoint`") + } + + endpoint := pCfg.Options["endpoint"] + + policy := "None" // None, Basic128Rsa15, Basic256, Basic256Sha256. Default: auto" + mode := "None" // None, Sign, SignAndEncrypt. Default: auto + //certFile := "created/server_cert.der" + //keyFile := "created/server_key.der" + + ctx := context.Background() + + endpoints, err := opcua.GetEndpoints(ctx, endpoint) + if err != nil { + return nil, err + } + ep := opcua.SelectEndpoint(endpoints, policy, ua.MessageSecurityModeFromString(mode)) + if ep == nil { + return nil, errors.New("failed to find suitable endpoint") + } + + opts := []opcua.Option{ + opcua.SecurityPolicy(policy), + opcua.SecurityModeString(mode), + //opcua.CertificateFile(certFile), + //opcua.PrivateKeyFile(keyFile), + opcua.AuthAnonymous(), + opcua.SecurityFromEndpoint(ep, ua.UserTokenTypeAnonymous), + } + + c := opcua.NewClient(endpoint, opts...) + if err := c.Connect(ctx); err != nil { + return nil, err + } + + p := &Provider{ + id: "", + client: c, + endpoint: endpoint, + } + + return p, nil +} + +type Provider struct { + id string + client *opcua.Client + endpoint string +} + +func (p *Provider) Close() { + p.client.CloseWithContext(context.Background()) +} + +func (p *Provider) Capabilities() providers.Capabilities { + return providers.Capabilities{} +} + +func (p *Provider) Kind() providers.Kind { + return providers.Kind_KIND_API +} + +func (p *Provider) Runtime() string { + return providers.RUNTIME_OPCUA +} + +func (p *Provider) PlatformIdDetectors() []providers.PlatformIdDetector { + return []providers.PlatformIdDetector{ + providers.TransportPlatformIdentifierDetector, + } +} + +func (p *Provider) Client() *opcua.Client { + return p.client +} diff --git a/motor/providers/opcua/platform.go b/motor/providers/opcua/platform.go new file mode 100644 index 0000000000..336c133b4e --- /dev/null +++ b/motor/providers/opcua/platform.go @@ -0,0 +1,17 @@ +package opcua + +import "go.mondoo.com/cnquery/motor/platform" + +func (p *Provider) Identifier() (string, error) { + return "//platformid.api.mondoo.app/runtime/opcua/" + p.id, nil +} + +func (p *Provider) PlatformInfo() (*platform.Platform, error) { + return &platform.Platform{ + Name: "opcua", + Title: "OPC UA", + Runtime: p.Runtime(), + Kind: p.Kind(), + Family: []string{}, + }, nil +} diff --git a/motor/providers/provider.pb.go b/motor/providers/provider.pb.go index 9de78bdcd0..706472bb71 100644 --- a/motor/providers/provider.pb.go +++ b/motor/providers/provider.pb.go @@ -57,6 +57,7 @@ const ( ProviderType_SLACK ProviderType = 31 ProviderType_VCD ProviderType = 32 ProviderType_OCI ProviderType = 33 + ProviderType_OPCUA ProviderType = 34 ) // Enum value maps for ProviderType. @@ -95,6 +96,7 @@ var ( 31: "SLACK", 32: "VCD", 33: "OCI", + 34: "OPCUA", } ProviderType_value = map[string]int32{ "LOCAL_OS": 0, @@ -130,6 +132,7 @@ var ( "SLACK": 31, "VCD": 32, "OCI": 33, + "OPCUA": 34, } ) @@ -578,7 +581,7 @@ var file_provider_proto_rawDesc = []byte{ 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x2a, 0xd7, 0x03, 0x0a, 0x0c, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x54, 0x79, + 0x01, 0x2a, 0xe2, 0x03, 0x0a, 0x0c, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0c, 0x0a, 0x08, 0x4c, 0x4f, 0x43, 0x41, 0x4c, 0x5f, 0x4f, 0x53, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x44, 0x4f, 0x43, 0x4b, 0x45, 0x52, 0x5f, 0x45, 0x4e, 0x47, 0x49, 0x4e, 0x45, 0x5f, 0x49, 0x4d, 0x41, 0x47, 0x45, 0x10, 0x01, 0x12, 0x1b, 0x0a, 0x17, 0x44, 0x4f, 0x43, @@ -607,30 +610,31 @@ var file_provider_proto_rawDesc = []byte{ 0x10, 0x1d, 0x12, 0x14, 0x0a, 0x10, 0x47, 0x4f, 0x4f, 0x47, 0x4c, 0x45, 0x5f, 0x57, 0x4f, 0x52, 0x4b, 0x53, 0x50, 0x41, 0x43, 0x45, 0x10, 0x1e, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x4c, 0x41, 0x43, 0x4b, 0x10, 0x1f, 0x12, 0x07, 0x0a, 0x03, 0x56, 0x43, 0x44, 0x10, 0x20, 0x12, 0x07, 0x0a, 0x03, - 0x4f, 0x43, 0x49, 0x10, 0x21, 0x22, 0x04, 0x08, 0x0b, 0x10, 0x0b, 0x2a, 0xbe, 0x02, 0x0a, 0x04, - 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x0c, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x55, 0x4e, 0x4b, - 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x1e, 0x0a, 0x1a, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x56, - 0x49, 0x52, 0x54, 0x55, 0x41, 0x4c, 0x5f, 0x4d, 0x41, 0x43, 0x48, 0x49, 0x4e, 0x45, 0x5f, 0x49, - 0x4d, 0x41, 0x47, 0x45, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x43, - 0x4f, 0x4e, 0x54, 0x41, 0x49, 0x4e, 0x45, 0x52, 0x5f, 0x49, 0x4d, 0x41, 0x47, 0x45, 0x10, 0x02, - 0x12, 0x0d, 0x0a, 0x09, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x10, 0x03, 0x12, - 0x10, 0x0a, 0x0c, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x50, 0x41, 0x43, 0x4b, 0x41, 0x47, 0x45, 0x10, - 0x04, 0x12, 0x18, 0x0a, 0x14, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x56, 0x49, 0x52, 0x54, 0x55, 0x41, - 0x4c, 0x5f, 0x4d, 0x41, 0x43, 0x48, 0x49, 0x4e, 0x45, 0x10, 0x05, 0x12, 0x12, 0x0a, 0x0e, 0x4b, - 0x49, 0x4e, 0x44, 0x5f, 0x43, 0x4f, 0x4e, 0x54, 0x41, 0x49, 0x4e, 0x45, 0x52, 0x10, 0x06, 0x12, - 0x10, 0x0a, 0x0c, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x50, 0x52, 0x4f, 0x43, 0x45, 0x53, 0x53, 0x10, - 0x07, 0x12, 0x0c, 0x0a, 0x08, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x41, 0x50, 0x49, 0x10, 0x08, 0x12, - 0x13, 0x0a, 0x0f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, - 0x41, 0x4c, 0x10, 0x09, 0x12, 0x10, 0x0a, 0x0c, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x4e, 0x45, 0x54, - 0x57, 0x4f, 0x52, 0x4b, 0x10, 0x0a, 0x12, 0x13, 0x0a, 0x0f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x4b, - 0x38, 0x53, 0x5f, 0x4f, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x10, 0x0b, 0x12, 0x13, 0x0a, 0x0f, 0x4b, - 0x49, 0x4e, 0x44, 0x5f, 0x41, 0x57, 0x53, 0x5f, 0x4f, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x10, 0x0c, - 0x12, 0x13, 0x0a, 0x0f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x47, 0x43, 0x50, 0x5f, 0x4f, 0x42, 0x4a, - 0x45, 0x43, 0x54, 0x10, 0x0d, 0x12, 0x15, 0x0a, 0x11, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x41, 0x5a, - 0x55, 0x52, 0x45, 0x5f, 0x4f, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x10, 0x0e, 0x42, 0x27, 0x5a, 0x25, - 0x67, 0x6f, 0x2e, 0x6d, 0x6f, 0x6e, 0x64, 0x6f, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6e, - 0x71, 0x75, 0x65, 0x72, 0x79, 0x2f, 0x6d, 0x6f, 0x74, 0x6f, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x4f, 0x43, 0x49, 0x10, 0x21, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x50, 0x43, 0x55, 0x41, 0x10, 0x22, + 0x22, 0x04, 0x08, 0x0b, 0x10, 0x0b, 0x2a, 0xbe, 0x02, 0x0a, 0x04, 0x4b, 0x69, 0x6e, 0x64, 0x12, + 0x10, 0x0a, 0x0c, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, + 0x00, 0x12, 0x1e, 0x0a, 0x1a, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x56, 0x49, 0x52, 0x54, 0x55, 0x41, + 0x4c, 0x5f, 0x4d, 0x41, 0x43, 0x48, 0x49, 0x4e, 0x45, 0x5f, 0x49, 0x4d, 0x41, 0x47, 0x45, 0x10, + 0x01, 0x12, 0x18, 0x0a, 0x14, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x43, 0x4f, 0x4e, 0x54, 0x41, 0x49, + 0x4e, 0x45, 0x52, 0x5f, 0x49, 0x4d, 0x41, 0x47, 0x45, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x4b, + 0x49, 0x4e, 0x44, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x10, 0x03, 0x12, 0x10, 0x0a, 0x0c, 0x4b, 0x49, + 0x4e, 0x44, 0x5f, 0x50, 0x41, 0x43, 0x4b, 0x41, 0x47, 0x45, 0x10, 0x04, 0x12, 0x18, 0x0a, 0x14, + 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x56, 0x49, 0x52, 0x54, 0x55, 0x41, 0x4c, 0x5f, 0x4d, 0x41, 0x43, + 0x48, 0x49, 0x4e, 0x45, 0x10, 0x05, 0x12, 0x12, 0x0a, 0x0e, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x43, + 0x4f, 0x4e, 0x54, 0x41, 0x49, 0x4e, 0x45, 0x52, 0x10, 0x06, 0x12, 0x10, 0x0a, 0x0c, 0x4b, 0x49, + 0x4e, 0x44, 0x5f, 0x50, 0x52, 0x4f, 0x43, 0x45, 0x53, 0x53, 0x10, 0x07, 0x12, 0x0c, 0x0a, 0x08, + 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x41, 0x50, 0x49, 0x10, 0x08, 0x12, 0x13, 0x0a, 0x0f, 0x4b, 0x49, + 0x4e, 0x44, 0x5f, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x10, 0x09, 0x12, + 0x10, 0x0a, 0x0c, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x10, + 0x0a, 0x12, 0x13, 0x0a, 0x0f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x4b, 0x38, 0x53, 0x5f, 0x4f, 0x42, + 0x4a, 0x45, 0x43, 0x54, 0x10, 0x0b, 0x12, 0x13, 0x0a, 0x0f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x41, + 0x57, 0x53, 0x5f, 0x4f, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x10, 0x0c, 0x12, 0x13, 0x0a, 0x0f, 0x4b, + 0x49, 0x4e, 0x44, 0x5f, 0x47, 0x43, 0x50, 0x5f, 0x4f, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x10, 0x0d, + 0x12, 0x15, 0x0a, 0x11, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x41, 0x5a, 0x55, 0x52, 0x45, 0x5f, 0x4f, + 0x42, 0x4a, 0x45, 0x43, 0x54, 0x10, 0x0e, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x6f, 0x2e, 0x6d, 0x6f, + 0x6e, 0x64, 0x6f, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6e, 0x71, 0x75, 0x65, 0x72, 0x79, + 0x2f, 0x6d, 0x6f, 0x74, 0x6f, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/motor/providers/provider.proto b/motor/providers/provider.proto index e6fa965f0d..a285fbaded 100644 --- a/motor/providers/provider.proto +++ b/motor/providers/provider.proto @@ -40,6 +40,7 @@ enum ProviderType { SLACK = 31; VCD = 32; OCI = 33; + OPCUA = 34; } enum Kind { diff --git a/motor/providers/resolver/resolver.go b/motor/providers/resolver/resolver.go index 77c1f3bd92..8c99edb9fd 100644 --- a/motor/providers/resolver/resolver.go +++ b/motor/providers/resolver/resolver.go @@ -24,6 +24,7 @@ import ( "go.mondoo.com/cnquery/motor/providers/network" "go.mondoo.com/cnquery/motor/providers/oci" "go.mondoo.com/cnquery/motor/providers/okta" + "go.mondoo.com/cnquery/motor/providers/opcua" "go.mondoo.com/cnquery/motor/providers/slack" "go.mondoo.com/cnquery/motor/providers/ssh" "go.mondoo.com/cnquery/motor/providers/tar" @@ -367,6 +368,15 @@ func NewMotorConnection(ctx context.Context, tc *providers.Config, credsResolver if err != nil { return nil, err } + case providers.ProviderType_OPCUA: + p, err := opcua.New(resolvedConfig) + if err != nil { + return nil, err + } + m, err = motor.New(p) + if err != nil { + return nil, err + } default: return nil, fmt.Errorf("connection> unsupported backend '%s'", resolvedConfig.Backend) } diff --git a/motor/providers/runtime.go b/motor/providers/runtime.go index e7af07f72b..93122f93ea 100644 --- a/motor/providers/runtime.go +++ b/motor/providers/runtime.go @@ -30,4 +30,5 @@ const ( RUNTIME_OKTA = "okta" RUNTIME_SLACK = "slack" RUNTIME_OCI = "oci" + RUNTIME_OPCUA = "opc-ua" ) diff --git a/resources/packs/all/all.go b/resources/packs/all/all.go index 620a5f533b..c6a90315c0 100644 --- a/resources/packs/all/all.go +++ b/resources/packs/all/all.go @@ -15,6 +15,7 @@ import ( "go.mondoo.com/cnquery/resources/packs/ms365" "go.mondoo.com/cnquery/resources/packs/oci" "go.mondoo.com/cnquery/resources/packs/okta" + "go.mondoo.com/cnquery/resources/packs/opcua" "go.mondoo.com/cnquery/resources/packs/os" "go.mondoo.com/cnquery/resources/packs/python" "go.mondoo.com/cnquery/resources/packs/slack" @@ -51,4 +52,5 @@ func init() { Registry.Add(ipmi.Registry) Registry.Add(python.Registry) Registry.Add(oci.Registry) + Registry.Add(opcua.Registry) } diff --git a/resources/packs/all/info/all.go b/resources/packs/all/info/all.go index d7f94992ff..f2270f515b 100644 --- a/resources/packs/all/info/all.go +++ b/resources/packs/all/info/all.go @@ -23,7 +23,9 @@ import ( ipmiInfo "go.mondoo.com/cnquery/resources/packs/ipmi/info" k8sInfo "go.mondoo.com/cnquery/resources/packs/k8s/info" ms365Info "go.mondoo.com/cnquery/resources/packs/ms365/info" + ociInfo "go.mondoo.com/cnquery/resources/packs/oci/info" oktaInfo "go.mondoo.com/cnquery/resources/packs/okta/info" + opcuaInfo "go.mondoo.com/cnquery/resources/packs/opcua/info" osInfo "go.mondoo.com/cnquery/resources/packs/os/info" slackInfo "go.mondoo.com/cnquery/resources/packs/slack/info" terraformInfo "go.mondoo.com/cnquery/resources/packs/terraform/info" @@ -50,6 +52,8 @@ func init() { Registry.Add(vcdInfo.Registry) Registry.Add(aristaInfo.Registry) Registry.Add(ipmiInfo.Registry) + Registry.Add(ociInfo.Registry) + Registry.Add(opcuaInfo.Registry) ResourceDocs = mergeDocs( coreInfo.ResourceDocs, @@ -69,6 +73,8 @@ func init() { vcdInfo.ResourceDocs, aristaInfo.ResourceDocs, ipmiInfo.ResourceDocs, + ociInfo.ResourceDocs, + opcuaInfo.ResourceDocs, ) } diff --git a/resources/packs/opcua/README.md b/resources/packs/opcua/README.md new file mode 100644 index 0000000000..e548d9325d --- /dev/null +++ b/resources/packs/opcua/README.md @@ -0,0 +1,74 @@ +# OPC UA Resource Pack + +```coffeescript +# gather all available namespaces +opcua.namespaces { * } +opcua.namespaces: [ + 0: { + id: 0 + name: "http://opcfoundation.org/UA/" + } + 1: { + id: 1 + name: "urn:open62541.server.application" + } +] + +# gather root node +cnquery> opcua.root +opcua.root: opcua.node id="i=84" name="Root" + + +# gather all nodes +cnquery> opcua.nodes { name namespace.name } + +# gather node with a specific id +cnquery> opcua.nodes.where (id == "i=2253") +opcua.nodes.where: [ + 0: opcua.node id="i=2253" name="Server" +] + +# gather details about the server +cnquery> opcua.server { * } +opcua.server: { + buildInfo: { + BuildDate: "2023-05-21T21:03:43.817369Z" + BuildNumber: "May 20 2023 15:51:32" + ManufacturerName: "open62541" + ProductName: "open62541 OPC UA Server" + ProductURI: "http://open62541.org" + SoftwareVersion: "1.3.5-994-g5d73f0cc5" + } + node: opcua.node id="i=2253" name="Server" + currentTime: 2023-05-22 08:28:30.625932 +0000 UTC + state: "ServerStateRunning" + startTime: 2023-05-21 21:03:43.834304 +0000 UTC +} +``` + +## Example Servers + +*Open62541* + +The [Open62541](https://github.com/open62541/open62541) includes may examples for building an Open62541 server. + + +*Azure IoT Edge* + +Azure has an example for [OPC PLC server](https://github.com/Azure-Samples/iot-edge-opc-plc) available that you can quickly start locally: + +```bash +# run service with no security configuration +docker run --rm -it -p 50000:50000 -p 8080:8080 --name opcplc mcr.microsoft.com/iotedge/opc-plc:latest --pn=50000 --autoaccept --sph --sn=5 --sr=10 --st=uint --fn=5 --fr=1 --ft=uint --gn=5 --ut --dca +``` + +## UI + +*Simple OPC-UA GUI client* + +This is a very simple [client](https://github.com/FreeOpcUa/opcua-client-gui) that allows you to browse the OPC UA data: + +```bash +pip3 install opcua-client +opcua-client +``` \ No newline at end of file diff --git a/resources/packs/opcua/info/info.go b/resources/packs/opcua/info/info.go new file mode 100644 index 0000000000..bcf99c7e47 --- /dev/null +++ b/resources/packs/opcua/info/info.go @@ -0,0 +1,33 @@ +package info + +// Load metadata for this resource pack + +import ( + _ "embed" + "encoding/json" + + "go.mondoo.com/cnquery/resources" + "go.mondoo.com/cnquery/resources/lr/docs" +) + +//go:embed opcua.lr.json +var info []byte + +//go:embed opcua.lr.manifest.json +var manifest []byte + +// Registry contains the resource info necessary for the compiler to work with this pack. +var Registry = resources.NewRegistry() + +// ResourceDocs contains additional resource metadata for the compiler to use. +var ResourceDocs docs.LrDocs + +func init() { + if err := Registry.LoadJson(info); err != nil { + panic(err.Error()) + } + + if err := json.Unmarshal(manifest, &ResourceDocs); err != nil { + panic(err.Error()) + } +} diff --git a/resources/packs/opcua/info/opcua.lr.json b/resources/packs/opcua/info/opcua.lr.json new file mode 100644 index 0000000000..fd8d5326c9 --- /dev/null +++ b/resources/packs/opcua/info/opcua.lr.json @@ -0,0 +1 @@ +{"resources":{"opcua":{"id":"opcua","name":"opcua","fields":{"namespaces":{"name":"namespaces","type":"\u0019\u001bopcua.namespace","title":"Namespaces"},"nodes":{"name":"nodes","type":"\u0019\u001bopcua.node","title":"List of all nodes"},"root":{"name":"root","type":"\u001bopcua.node","title":"Root node"}},"title":"OPC UA"},"opcua.namespace":{"id":"opcua.namespace","name":"opcua.namespace","fields":{"id":{"name":"id","type":"\u0005","is_mandatory":true,"title":"Namespace ID"},"name":{"name":"name","type":"\u0007","is_mandatory":true,"title":"Namespace Name"}},"title":"OPC UA Namespace"},"opcua.node":{"id":"opcua.node","name":"opcua.node","fields":{"accessLevel":{"name":"accessLevel","type":"\u0007","is_mandatory":true,"title":"Access Level"},"class":{"name":"class","type":"\u0007","is_mandatory":true,"title":"Node class"},"components":{"name":"components","type":"\u0019\u001bopcua.node","title":"Components"},"dataType":{"name":"dataType","type":"\u0007","is_mandatory":true,"title":"Data Type"},"description":{"name":"description","type":"\u0007","is_mandatory":true,"title":"Node description"},"id":{"name":"id","type":"\u0007","is_mandatory":true,"title":"Node id"},"max":{"name":"max","type":"\u0007","is_mandatory":true,"title":"Max value"},"min":{"name":"min","type":"\u0007","is_mandatory":true,"title":"Min value"},"name":{"name":"name","type":"\u0007","is_mandatory":true,"title":"Node browser name"},"namespace":{"name":"namespace","type":"\u001bopcua.namespace","title":"Namespace"},"organizes":{"name":"organizes","type":"\u0019\u001bopcua.node","title":"Organizes"},"properties":{"name":"properties","type":"\u0019\u001bopcua.node","title":"Properties"},"unit":{"name":"unit","type":"\u0007","is_mandatory":true,"title":"Node unit"},"writeable":{"name":"writeable","type":"\u0004","is_mandatory":true,"title":"Indicates if value is writable"}},"title":"OPC UA Node","defaults":"id name"},"opcua.server":{"id":"opcua.server","name":"opcua.server","fields":{"buildInfo":{"name":"buildInfo","type":"\n","is_mandatory":true,"title":"Server build info"},"currentTime":{"name":"currentTime","type":"\t","is_mandatory":true,"title":"Current time on server"},"node":{"name":"node","type":"\u001bopcua.node","is_mandatory":true,"title":"Reference to node"},"startTime":{"name":"startTime","type":"\t","is_mandatory":true,"title":"Time when the server started"},"state":{"name":"state","type":"\u0007","is_mandatory":true,"title":"Server state"}},"title":"Server Object"}}} \ No newline at end of file diff --git a/resources/packs/opcua/info/opcua.lr.manifest.json b/resources/packs/opcua/info/opcua.lr.manifest.json new file mode 100644 index 0000000000..f7946f9c6e --- /dev/null +++ b/resources/packs/opcua/info/opcua.lr.manifest.json @@ -0,0 +1 @@ +{"resources":{"opcua":{"fields":{"namespaces":{},"nodes":{},"root":{}},"min_mondoo_version":"latest"},"opcua.namespace":{"fields":{"id":{},"name":{}},"min_mondoo_version":"latest"},"opcua.node":{"fields":{"accessLevel":{},"class":{},"components":{},"dataType":{},"description":{},"id":{},"max":{},"min":{},"name":{},"namespace":{},"nodeid":{},"organizes":{},"properties":{},"unit":{},"writeable":{}},"min_mondoo_version":"latest"},"opcua.server":{"fields":{"buildInfo":{},"currentTime":{},"node":{},"startTime":{},"state":{}},"min_mondoo_version":"latest"}}} \ No newline at end of file diff --git a/resources/packs/opcua/namespace.go b/resources/packs/opcua/namespace.go new file mode 100644 index 0000000000..2da81db2c7 --- /dev/null +++ b/resources/packs/opcua/namespace.go @@ -0,0 +1,42 @@ +package opcua + +import ( + "go.mondoo.com/cnquery/resources" + "strconv" +) + +func (o *mqlOpcuaNamespace) id() (string, error) { + id, err := o.Id() + if err != nil { + return "", err + } + s := strconv.FormatInt(id, 10) + return "opcua.namespace/" + s, nil +} + +// https://reference.opcfoundation.org/DI/v102/docs/11.2 +func (o *mqlOpcua) GetNamespaces() ([]interface{}, error) { + op, err := opcuaProvider(o.MotorRuntime.Motor.Provider) + if err != nil { + return nil, err + } + client := op.Client() + + namespaces := client.Namespaces() + resList := []interface{}{} + for i := range namespaces { + res, err := newMqlOpcuaNamespaceResource(o.MotorRuntime, int64(i), namespaces[i]) + if err != nil { + return nil, err + } + resList = append(resList, res) + } + return resList, nil +} + +func newMqlOpcuaNamespaceResource(runtime *resources.Runtime, id int64, name string) (interface{}, error) { + return runtime.CreateResource("opcua.namespace", + "id", id, + "name", name, + ) +} diff --git a/resources/packs/opcua/node.go b/resources/packs/opcua/node.go new file mode 100644 index 0000000000..347bb4f66f --- /dev/null +++ b/resources/packs/opcua/node.go @@ -0,0 +1,294 @@ +package opcua + +import ( + "context" + "fmt" + "github.com/gopcua/opcua" + "github.com/gopcua/opcua/errors" + "github.com/gopcua/opcua/id" + "github.com/gopcua/opcua/ua" + "go.mondoo.com/cnquery/resources" +) + +type nodeMeta struct { + NodeID *ua.NodeID + NodeClass ua.NodeClass + BrowseName string + Description string + AccessLevel ua.AccessLevelType + Path string + DataType string + Writable bool + Unit string + Scale string + Min string + Max string + Components []*opcua.Node + Organizes []*opcua.Node + Properties []*opcua.Node +} + +func fetchNodeInfo(ctx context.Context, n *opcua.Node) (*nodeMeta, error) { + attrs, err := n.AttributesWithContext(ctx, ua.AttributeIDNodeClass, ua.AttributeIDBrowseName, ua.AttributeIDDescription, ua.AttributeIDAccessLevel, ua.AttributeIDDataType) + if err != nil { + return nil, err + } + + var def = nodeMeta{ + NodeID: n.ID, + } + + switch err := attrs[0].Status; err { + case ua.StatusOK: + def.NodeClass = ua.NodeClass(attrs[0].Value.Int()) + default: + return nil, err + } + + switch err := attrs[0].Status; err { + case ua.StatusOK: + def.NodeClass = ua.NodeClass(attrs[0].Value.Int()) + default: + return nil, err + } + + switch err := attrs[1].Status; err { + case ua.StatusOK: + def.BrowseName = attrs[1].Value.String() + default: + return nil, err + } + + switch err := attrs[2].Status; err { + case ua.StatusOK: + def.Description = attrs[2].Value.String() + case ua.StatusBadAttributeIDInvalid: + // ignore + default: + return nil, err + } + + switch err := attrs[3].Status; err { + case ua.StatusOK: + def.AccessLevel = ua.AccessLevelType(attrs[3].Value.Int()) + def.Writable = def.AccessLevel&ua.AccessLevelTypeCurrentWrite == ua.AccessLevelTypeCurrentWrite + case ua.StatusBadAttributeIDInvalid: + // ignore + default: + return nil, err + } + + switch err := attrs[4].Status; err { + case ua.StatusOK: + switch v := attrs[4].Value.NodeID().IntID(); v { + case id.DateTime: + def.DataType = "time.Time" + case id.Boolean: + def.DataType = "bool" + case id.SByte: + def.DataType = "int8" + case id.Int16: + def.DataType = "int16" + case id.Int32: + def.DataType = "int32" + case id.Byte: + def.DataType = "byte" + case id.UInt16: + def.DataType = "uint16" + case id.UInt32: + def.DataType = "uint32" + case id.UtcTime: + def.DataType = "time.Time" + case id.String: + def.DataType = "string" + case id.Float: + def.DataType = "float32" + case id.Double: + def.DataType = "float64" + default: + def.DataType = attrs[4].Value.NodeID().String() + } + case ua.StatusBadAttributeIDInvalid: + // ignore + default: + return nil, err + } + + // TODO: set path + + fetchReference := func(refType uint32) ([]*opcua.Node, error) { + return n.ReferencedNodesWithContext(ctx, refType, ua.BrowseDirectionForward, ua.NodeClassAll, true) + } + + if componentRefs, err := fetchReference(id.HasComponent); err != nil { + return nil, err + } else { + def.Components = append(def.Components, componentRefs...) + } + + if componentRefs, err := fetchReference(id.Organizes); err != nil { + return nil, err + } else { + def.Organizes = append(def.Organizes, componentRefs...) + } + + if componentRefs, err := fetchReference(id.HasProperty); err != nil { + return nil, err + } else { + def.Properties = append(def.Properties, componentRefs...) + } + + return &def, nil +} + +func newMqlOpcuaNodeResource(runtime *resources.Runtime, ndef *nodeMeta) (interface{}, error) { + res, err := runtime.CreateResource("opcua.node", + "id", ndef.NodeID.String(), + "name", ndef.BrowseName, + "class", ndef.NodeClass.String(), + "description", ndef.Description, + "writeable", ndef.Writable, + "dataType", ndef.DataType, + "min", ndef.Min, + "max", ndef.Max, + "unit", ndef.Unit, + "accessLevel", ndef.AccessLevel.String(), + ) + if err != nil { + return nil, err + } + res.MqlResource().Cache.Store("_object", &resources.CacheEntry{ + Data: ndef, + }) + return res, nil +} + +func (o *mqlOpcuaNode) id() (string, error) { + id, err := o.Id() + if err != nil { + return "", err + } + return "opcua.node/" + id, nil +} + +func (o *mqlOpcuaNode) GetNamespace() (interface{}, error) { + res, ok := o.Cache.Load("_object") + if !ok { + return nil, errors.New("could not fetch properties") + } + + if res.Error != nil { + return nil, res.Error + } + nodeDef, ok := res.Data.(*nodeMeta) + if !ok { + return nil, fmt.Errorf("\"opcua\" failed to cast field \"node\" to the right type: %#v", res) + } + + obj, err := o.MotorRuntime.CreateResource("opcua") + if err != nil { + return nil, err + } + mqlOpcua := obj.(Opcua) + + namespaces, err := mqlOpcua.Namespaces() + if err != nil { + return nil, err + } + + entry := namespaces[nodeDef.NodeID.Namespace()] + return entry, nil +} + +func (o *mqlOpcuaNode) GetProperties() ([]interface{}, error) { + res, ok := o.Cache.Load("_object") + if !ok { + return nil, errors.New("could not fetch properties") + } + if res.Error != nil { + return nil, res.Error + } + nodeDef, ok := res.Data.(*nodeMeta) + if !ok { + return nil, fmt.Errorf("\"opcua\" failed to cast field \"node\" to the right type: %#v", res) + } + + ctx := context.Background() + results := []interface{}{} + for i := range nodeDef.Properties { + def := nodeDef.Properties[i] + n, err := fetchNodeInfo(ctx, def) + if err != nil { + return nil, err + } + r, err := newMqlOpcuaNodeResource(o.MotorRuntime, n) + if err != nil { + return nil, err + } + results = append(results, r) + } + + return results, nil +} + +func (o *mqlOpcuaNode) GetComponents() ([]interface{}, error) { + res, ok := o.Cache.Load("_object") + if !ok { + return nil, errors.New("could not fetch properties") + } + if res.Error != nil { + return nil, res.Error + } + nodeDef, ok := res.Data.(*nodeMeta) + if !ok { + return nil, fmt.Errorf("\"opcua\" failed to cast field \"node\" to the right type: %#v", res) + } + + ctx := context.Background() + results := []interface{}{} + for i := range nodeDef.Components { + def := nodeDef.Components[i] + n, err := fetchNodeInfo(ctx, def) + if err != nil { + return nil, err + } + r, err := newMqlOpcuaNodeResource(o.MotorRuntime, n) + if err != nil { + return nil, err + } + results = append(results, r) + } + + return results, nil +} + +func (o *mqlOpcuaNode) GetOrganizes() ([]interface{}, error) { + res, ok := o.Cache.Load("_object") + if !ok { + return nil, errors.New("could not fetch properties") + } + if res.Error != nil { + return nil, res.Error + } + nodeDef, ok := res.Data.(*nodeMeta) + if !ok { + return nil, fmt.Errorf("\"opcua\" failed to cast field \"node\" to the right type: %#v", res) + } + + ctx := context.Background() + results := []interface{}{} + for i := range nodeDef.Organizes { + def := nodeDef.Organizes[i] + n, err := fetchNodeInfo(ctx, def) + if err != nil { + return nil, err + } + r, err := newMqlOpcuaNodeResource(o.MotorRuntime, n) + if err != nil { + return nil, err + } + results = append(results, r) + } + + return results, nil +} diff --git a/resources/packs/opcua/opcua.go b/resources/packs/opcua/opcua.go new file mode 100644 index 0000000000..7f997b7426 --- /dev/null +++ b/resources/packs/opcua/opcua.go @@ -0,0 +1,128 @@ +package opcua + +import ( + "context" + "errors" + + "github.com/gopcua/opcua/id" + "github.com/gopcua/opcua/ua" + "go.mondoo.com/cnquery/motor/providers" + opcua_provider "go.mondoo.com/cnquery/motor/providers/opcua" + "go.mondoo.com/cnquery/resources/packs/opcua/info" +) + +var Registry = info.Registry + +func init() { + Init(Registry) +} + +func opcuaProvider(p providers.Instance) (*opcua_provider.Provider, error) { + at, ok := p.(*opcua_provider.Provider) + if !ok { + return nil, errors.New("OPC UA resource is not supported on this provider") + } + return at, nil +} + +func (o *mqlOpcua) id() (string, error) { + return "opcua", nil +} + +func (o *mqlOpcua) GetRoot() (interface{}, error) { + op, err := opcuaProvider(o.MotorRuntime.Motor.Provider) + if err != nil { + return nil, err + } + client := op.Client() + + ctx := context.Background() + n := client.Node(ua.NewNumericNodeID(0, id.RootFolder)) + ndef, err := fetchNodeInfo(ctx, n) + if err != nil { + return nil, err + } + return newMqlOpcuaNodeResource(o.MotorRuntime, ndef) +} + +func resolve(ctx context.Context, meta *nodeMeta) ([]*nodeMeta, error) { + nodeList := []*nodeMeta{} + + for i := range meta.Organizes { + child := meta.Organizes[i] + nInfoChild, err := fetchNodeInfo(ctx, child) + if err != nil { + return nil, err + } + nodeList = append(nodeList, nInfoChild) + resolved, err := resolve(ctx, nInfoChild) + if err != nil { + return nil, err + } + nodeList = append(nodeList, resolved...) + } + + for i := range meta.Properties { + child := meta.Properties[i] + nInfoChild, err := fetchNodeInfo(ctx, child) + if err != nil { + return nil, err + } + nodeList = append(nodeList, nInfoChild) + resolved, err := resolve(ctx, nInfoChild) + if err != nil { + return nil, err + } + nodeList = append(nodeList, resolved...) + } + + for i := range meta.Components { + child := meta.Components[i] + nInfoChild, err := fetchNodeInfo(ctx, child) + if err != nil { + return nil, err + } + nodeList = append(nodeList, nInfoChild) + resolved, err := resolve(ctx, nInfoChild) + if err != nil { + return nil, err + } + nodeList = append(nodeList, resolved...) + } + + return nodeList, nil +} + +func (o *mqlOpcua) GetNodes() ([]interface{}, error) { + op, err := opcuaProvider(o.MotorRuntime.Motor.Provider) + if err != nil { + return nil, err + } + client := op.Client() + + ctx := context.Background() + n := client.Node(ua.NewNumericNodeID(0, id.RootFolder)) + + nodeList := []*nodeMeta{} + nInfo, err := fetchNodeInfo(ctx, n) + if err != nil { + return nil, err + } + nodeList = append(nodeList, nInfo) + resolved, err := resolve(ctx, nInfo) + if err != nil { + return nil, err + } + nodeList = append(nodeList, resolved...) + + // convert list to interface + res := []interface{}{} + for i := range nodeList { + entry, err := newMqlOpcuaNodeResource(o.MotorRuntime, nodeList[i]) + if err != nil { + return nil, err + } + res = append(res, entry) + } + return res, nil +} diff --git a/resources/packs/opcua/opcua.lr b/resources/packs/opcua/opcua.lr new file mode 100644 index 0000000000..141cf5200f --- /dev/null +++ b/resources/packs/opcua/opcua.lr @@ -0,0 +1,65 @@ +option go_package = "go.mondoo.com/cnquery/resources/packs/opcua" + +// OPC UA +opcua { + // Namespaces + namespaces() []opcua.namespace + // Root node + root() opcua.node + // List of all nodes + nodes() []opcua.node +} + +// Server Object +opcua.server { + // Reference to node + node opcua.node + // Server build info + buildInfo dict + // Current time on server + currentTime time + // Time when the server started + startTime time + // Server state + state string +} + +// OPC UA Namespace +opcua.namespace { + // Namespace ID + id int + // Namespace Name + name string +} + +// OPC UA Node +opcua.node @defaults("id name") { + // Node id + id string + // Node browser name + name string + // Namespace + namespace() opcua.namespace + // Node class + class string + // Node description + description string + // Indicates if value is writable + writeable bool + // Data Type + dataType string + // Min value + min string + // Max value + max string + // Node unit + unit string + // Access Level + accessLevel string + // Properties + properties() []opcua.node + // Components + components() []opcua.node + // Organizes + organizes() []opcua.node +} diff --git a/resources/packs/opcua/opcua.lr.go b/resources/packs/opcua/opcua.lr.go new file mode 100644 index 0000000000..8541563b1d --- /dev/null +++ b/resources/packs/opcua/opcua.lr.go @@ -0,0 +1,1248 @@ +// Code generated by resources. DO NOT EDIT. +package opcua + +import ( + "errors" + "fmt" + "time" + + "go.mondoo.com/cnquery/resources" + "github.com/rs/zerolog/log" +) + +// Init all resources into the registry +func Init(registry *resources.Registry) { + registry.AddFactory("opcua", newOpcua) + registry.AddFactory("opcua.server", newOpcuaServer) + registry.AddFactory("opcua.namespace", newOpcuaNamespace) + registry.AddFactory("opcua.node", newOpcuaNode) +} + +// Opcua resource interface +type Opcua interface { + MqlResource() (*resources.Resource) + MqlCompute(string) error + Field(string) (interface{}, error) + Register(string) error + Validate() error + Namespaces() ([]interface{}, error) + Root() (OpcuaNode, error) + Nodes() ([]interface{}, error) +} + +// mqlOpcua for the opcua resource +type mqlOpcua struct { + *resources.Resource +} + +// MqlResource to retrieve the underlying resource info +func (s *mqlOpcua) MqlResource() *resources.Resource { + return s.Resource +} + +// create a new instance of the opcua resource +func newOpcua(runtime *resources.Runtime, args *resources.Args) (interface{}, error) { + // User hooks + var err error + res := mqlOpcua{runtime.NewResource("opcua")} + // assign all named fields + var id string + + now := time.Now().Unix() + for name, val := range *args { + if val == nil { + res.Cache.Store(name, &resources.CacheEntry{Data: val, Valid: true, Timestamp: now}) + continue + } + + switch name { + case "namespaces": + if _, ok := val.([]interface{}); !ok { + return nil, errors.New("Failed to initialize \"opcua\", its \"namespaces\" argument has the wrong type (expected type \"[]interface{}\")") + } + case "root": + if _, ok := val.(OpcuaNode); !ok { + return nil, errors.New("Failed to initialize \"opcua\", its \"root\" argument has the wrong type (expected type \"OpcuaNode\")") + } + case "nodes": + if _, ok := val.([]interface{}); !ok { + return nil, errors.New("Failed to initialize \"opcua\", its \"nodes\" argument has the wrong type (expected type \"[]interface{}\")") + } + case "__id": + idVal, ok := val.(string) + if !ok { + return nil, errors.New("Failed to initialize \"opcua\", its \"__id\" argument has the wrong type (expected type \"string\")") + } + id = idVal + default: + return nil, errors.New("Initialized opcua with unknown argument " + name) + } + res.Cache.Store(name, &resources.CacheEntry{Data: val, Valid: true, Timestamp: now}) + } + + // Get the ID + if id == "" { + res.Resource.Id, err = res.id() + if err != nil { + return nil, err + } + } else { + res.Resource.Id = id + } + + return &res, nil +} + +func (s *mqlOpcua) Validate() error { + // required arguments + // no required fields found + + return nil +} + +// Register accessor autogenerated +func (s *mqlOpcua) Register(name string) error { + log.Trace().Str("field", name).Msg("[opcua].Register") + switch name { + case "namespaces": + return nil + case "root": + return nil + case "nodes": + return nil + default: + return errors.New("Cannot find field '" + name + "' in \"opcua\" resource") + } +} + +// Field accessor autogenerated +func (s *mqlOpcua) Field(name string) (interface{}, error) { + log.Trace().Str("field", name).Msg("[opcua].Field") + switch name { + case "namespaces": + return s.Namespaces() + case "root": + return s.Root() + case "nodes": + return s.Nodes() + default: + return nil, fmt.Errorf("Cannot find field '" + name + "' in \"opcua\" resource") + } +} + +// Namespaces accessor autogenerated +func (s *mqlOpcua) Namespaces() ([]interface{}, error) { + res, ok := s.Cache.Load("namespaces") + if !ok || !res.Valid { + if err := s.ComputeNamespaces(); err != nil { + return nil, err + } + res, ok = s.Cache.Load("namespaces") + if !ok { + return nil, errors.New("\"opcua\" calculated \"namespaces\" but didn't find its value in cache.") + } + s.MotorRuntime.Trigger(s, "namespaces") + } + if res.Error != nil { + return nil, res.Error + } + tres, ok := res.Data.([]interface{}) + if !ok { + return nil, fmt.Errorf("\"opcua\" failed to cast field \"namespaces\" to the right type ([]interface{}): %#v", res) + } + return tres, nil +} + +// Root accessor autogenerated +func (s *mqlOpcua) Root() (OpcuaNode, error) { + res, ok := s.Cache.Load("root") + if !ok || !res.Valid { + if err := s.ComputeRoot(); err != nil { + return nil, err + } + res, ok = s.Cache.Load("root") + if !ok { + return nil, errors.New("\"opcua\" calculated \"root\" but didn't find its value in cache.") + } + s.MotorRuntime.Trigger(s, "root") + } + if res.Error != nil { + return nil, res.Error + } + tres, ok := res.Data.(OpcuaNode) + if !ok { + return nil, fmt.Errorf("\"opcua\" failed to cast field \"root\" to the right type (OpcuaNode): %#v", res) + } + return tres, nil +} + +// Nodes accessor autogenerated +func (s *mqlOpcua) Nodes() ([]interface{}, error) { + res, ok := s.Cache.Load("nodes") + if !ok || !res.Valid { + if err := s.ComputeNodes(); err != nil { + return nil, err + } + res, ok = s.Cache.Load("nodes") + if !ok { + return nil, errors.New("\"opcua\" calculated \"nodes\" but didn't find its value in cache.") + } + s.MotorRuntime.Trigger(s, "nodes") + } + if res.Error != nil { + return nil, res.Error + } + tres, ok := res.Data.([]interface{}) + if !ok { + return nil, fmt.Errorf("\"opcua\" failed to cast field \"nodes\" to the right type ([]interface{}): %#v", res) + } + return tres, nil +} + +// Compute accessor autogenerated +func (s *mqlOpcua) MqlCompute(name string) error { + log.Trace().Str("field", name).Msg("[opcua].MqlCompute") + switch name { + case "namespaces": + return s.ComputeNamespaces() + case "root": + return s.ComputeRoot() + case "nodes": + return s.ComputeNodes() + default: + return errors.New("Cannot find field '" + name + "' in \"opcua\" resource") + } +} + +// ComputeNamespaces computer autogenerated +func (s *mqlOpcua) ComputeNamespaces() error { + var err error + if _, ok := s.Cache.Load("namespaces"); ok { + return nil + } + vres, err := s.GetNamespaces() + if _, ok := err.(resources.NotReadyError); ok { + return err + } + s.Cache.Store("namespaces", &resources.CacheEntry{Data: vres, Valid: true, Error: err, Timestamp: time.Now().Unix()}) + return nil +} + +// ComputeRoot computer autogenerated +func (s *mqlOpcua) ComputeRoot() error { + var err error + if _, ok := s.Cache.Load("root"); ok { + return nil + } + vres, err := s.GetRoot() + if _, ok := err.(resources.NotReadyError); ok { + return err + } + s.Cache.Store("root", &resources.CacheEntry{Data: vres, Valid: true, Error: err, Timestamp: time.Now().Unix()}) + return nil +} + +// ComputeNodes computer autogenerated +func (s *mqlOpcua) ComputeNodes() error { + var err error + if _, ok := s.Cache.Load("nodes"); ok { + return nil + } + vres, err := s.GetNodes() + if _, ok := err.(resources.NotReadyError); ok { + return err + } + s.Cache.Store("nodes", &resources.CacheEntry{Data: vres, Valid: true, Error: err, Timestamp: time.Now().Unix()}) + return nil +} + +// OpcuaServer resource interface +type OpcuaServer interface { + MqlResource() (*resources.Resource) + MqlCompute(string) error + Field(string) (interface{}, error) + Register(string) error + Validate() error + Node() (OpcuaNode, error) + BuildInfo() (interface{}, error) + CurrentTime() (*time.Time, error) + StartTime() (*time.Time, error) + State() (string, error) +} + +// mqlOpcuaServer for the opcua.server resource +type mqlOpcuaServer struct { + *resources.Resource +} + +// MqlResource to retrieve the underlying resource info +func (s *mqlOpcuaServer) MqlResource() *resources.Resource { + return s.Resource +} + +// create a new instance of the opcua.server resource +func newOpcuaServer(runtime *resources.Runtime, args *resources.Args) (interface{}, error) { + // User hooks + var err error + res := mqlOpcuaServer{runtime.NewResource("opcua.server")} + var existing OpcuaServer + args, existing, err = res.init(args) + if err != nil { + return nil, err + } + if existing != nil { + return existing, nil + } + + // assign all named fields + var id string + + now := time.Now().Unix() + for name, val := range *args { + if val == nil { + res.Cache.Store(name, &resources.CacheEntry{Data: val, Valid: true, Timestamp: now}) + continue + } + + switch name { + case "node": + if _, ok := val.(OpcuaNode); !ok { + return nil, errors.New("Failed to initialize \"opcua.server\", its \"node\" argument has the wrong type (expected type \"OpcuaNode\")") + } + case "buildInfo": + if _, ok := val.(interface{}); !ok { + return nil, errors.New("Failed to initialize \"opcua.server\", its \"buildInfo\" argument has the wrong type (expected type \"interface{}\")") + } + case "currentTime": + if _, ok := val.(*time.Time); !ok { + return nil, errors.New("Failed to initialize \"opcua.server\", its \"currentTime\" argument has the wrong type (expected type \"*time.Time\")") + } + case "startTime": + if _, ok := val.(*time.Time); !ok { + return nil, errors.New("Failed to initialize \"opcua.server\", its \"startTime\" argument has the wrong type (expected type \"*time.Time\")") + } + case "state": + if _, ok := val.(string); !ok { + return nil, errors.New("Failed to initialize \"opcua.server\", its \"state\" argument has the wrong type (expected type \"string\")") + } + case "__id": + idVal, ok := val.(string) + if !ok { + return nil, errors.New("Failed to initialize \"opcua.server\", its \"__id\" argument has the wrong type (expected type \"string\")") + } + id = idVal + default: + return nil, errors.New("Initialized opcua.server with unknown argument " + name) + } + res.Cache.Store(name, &resources.CacheEntry{Data: val, Valid: true, Timestamp: now}) + } + + // Get the ID + if id == "" { + res.Resource.Id, err = res.id() + if err != nil { + return nil, err + } + } else { + res.Resource.Id = id + } + + return &res, nil +} + +func (s *mqlOpcuaServer) Validate() error { + // required arguments + if _, ok := s.Cache.Load("node"); !ok { + return errors.New("Initialized \"opcua.server\" resource without a \"node\". This field is required.") + } + if _, ok := s.Cache.Load("buildInfo"); !ok { + return errors.New("Initialized \"opcua.server\" resource without a \"buildInfo\". This field is required.") + } + if _, ok := s.Cache.Load("currentTime"); !ok { + return errors.New("Initialized \"opcua.server\" resource without a \"currentTime\". This field is required.") + } + if _, ok := s.Cache.Load("startTime"); !ok { + return errors.New("Initialized \"opcua.server\" resource without a \"startTime\". This field is required.") + } + if _, ok := s.Cache.Load("state"); !ok { + return errors.New("Initialized \"opcua.server\" resource without a \"state\". This field is required.") + } + + return nil +} + +// Register accessor autogenerated +func (s *mqlOpcuaServer) Register(name string) error { + log.Trace().Str("field", name).Msg("[opcua.server].Register") + switch name { + case "node": + return nil + case "buildInfo": + return nil + case "currentTime": + return nil + case "startTime": + return nil + case "state": + return nil + default: + return errors.New("Cannot find field '" + name + "' in \"opcua.server\" resource") + } +} + +// Field accessor autogenerated +func (s *mqlOpcuaServer) Field(name string) (interface{}, error) { + log.Trace().Str("field", name).Msg("[opcua.server].Field") + switch name { + case "node": + return s.Node() + case "buildInfo": + return s.BuildInfo() + case "currentTime": + return s.CurrentTime() + case "startTime": + return s.StartTime() + case "state": + return s.State() + default: + return nil, fmt.Errorf("Cannot find field '" + name + "' in \"opcua.server\" resource") + } +} + +// Node accessor autogenerated +func (s *mqlOpcuaServer) Node() (OpcuaNode, error) { + res, ok := s.Cache.Load("node") + if !ok || !res.Valid { + return nil, errors.New("\"opcua.server\" failed: no value provided for static field \"node\"") + } + if res.Error != nil { + return nil, res.Error + } + tres, ok := res.Data.(OpcuaNode) + if !ok { + return nil, fmt.Errorf("\"opcua.server\" failed to cast field \"node\" to the right type (OpcuaNode): %#v", res) + } + return tres, nil +} + +// BuildInfo accessor autogenerated +func (s *mqlOpcuaServer) BuildInfo() (interface{}, error) { + res, ok := s.Cache.Load("buildInfo") + if !ok || !res.Valid { + return nil, errors.New("\"opcua.server\" failed: no value provided for static field \"buildInfo\"") + } + if res.Error != nil { + return nil, res.Error + } + tres, ok := res.Data.(interface{}) + if !ok { + return nil, fmt.Errorf("\"opcua.server\" failed to cast field \"buildInfo\" to the right type (interface{}): %#v", res) + } + return tres, nil +} + +// CurrentTime accessor autogenerated +func (s *mqlOpcuaServer) CurrentTime() (*time.Time, error) { + res, ok := s.Cache.Load("currentTime") + if !ok || !res.Valid { + return nil, errors.New("\"opcua.server\" failed: no value provided for static field \"currentTime\"") + } + if res.Error != nil { + return nil, res.Error + } + tres, ok := res.Data.(*time.Time) + if !ok { + return nil, fmt.Errorf("\"opcua.server\" failed to cast field \"currentTime\" to the right type (*time.Time): %#v", res) + } + return tres, nil +} + +// StartTime accessor autogenerated +func (s *mqlOpcuaServer) StartTime() (*time.Time, error) { + res, ok := s.Cache.Load("startTime") + if !ok || !res.Valid { + return nil, errors.New("\"opcua.server\" failed: no value provided for static field \"startTime\"") + } + if res.Error != nil { + return nil, res.Error + } + tres, ok := res.Data.(*time.Time) + if !ok { + return nil, fmt.Errorf("\"opcua.server\" failed to cast field \"startTime\" to the right type (*time.Time): %#v", res) + } + return tres, nil +} + +// State accessor autogenerated +func (s *mqlOpcuaServer) State() (string, error) { + res, ok := s.Cache.Load("state") + if !ok || !res.Valid { + return "", errors.New("\"opcua.server\" failed: no value provided for static field \"state\"") + } + if res.Error != nil { + return "", res.Error + } + tres, ok := res.Data.(string) + if !ok { + return "", fmt.Errorf("\"opcua.server\" failed to cast field \"state\" to the right type (string): %#v", res) + } + return tres, nil +} + +// Compute accessor autogenerated +func (s *mqlOpcuaServer) MqlCompute(name string) error { + log.Trace().Str("field", name).Msg("[opcua.server].MqlCompute") + switch name { + case "node": + return nil + case "buildInfo": + return nil + case "currentTime": + return nil + case "startTime": + return nil + case "state": + return nil + default: + return errors.New("Cannot find field '" + name + "' in \"opcua.server\" resource") + } +} + +// OpcuaNamespace resource interface +type OpcuaNamespace interface { + MqlResource() (*resources.Resource) + MqlCompute(string) error + Field(string) (interface{}, error) + Register(string) error + Validate() error + Id() (int64, error) + Name() (string, error) +} + +// mqlOpcuaNamespace for the opcua.namespace resource +type mqlOpcuaNamespace struct { + *resources.Resource +} + +// MqlResource to retrieve the underlying resource info +func (s *mqlOpcuaNamespace) MqlResource() *resources.Resource { + return s.Resource +} + +// create a new instance of the opcua.namespace resource +func newOpcuaNamespace(runtime *resources.Runtime, args *resources.Args) (interface{}, error) { + // User hooks + var err error + res := mqlOpcuaNamespace{runtime.NewResource("opcua.namespace")} + // assign all named fields + var id string + + now := time.Now().Unix() + for name, val := range *args { + if val == nil { + res.Cache.Store(name, &resources.CacheEntry{Data: val, Valid: true, Timestamp: now}) + continue + } + + switch name { + case "id": + if _, ok := val.(int64); !ok { + return nil, errors.New("Failed to initialize \"opcua.namespace\", its \"id\" argument has the wrong type (expected type \"int64\")") + } + case "name": + if _, ok := val.(string); !ok { + return nil, errors.New("Failed to initialize \"opcua.namespace\", its \"name\" argument has the wrong type (expected type \"string\")") + } + case "__id": + idVal, ok := val.(string) + if !ok { + return nil, errors.New("Failed to initialize \"opcua.namespace\", its \"__id\" argument has the wrong type (expected type \"string\")") + } + id = idVal + default: + return nil, errors.New("Initialized opcua.namespace with unknown argument " + name) + } + res.Cache.Store(name, &resources.CacheEntry{Data: val, Valid: true, Timestamp: now}) + } + + // Get the ID + if id == "" { + res.Resource.Id, err = res.id() + if err != nil { + return nil, err + } + } else { + res.Resource.Id = id + } + + return &res, nil +} + +func (s *mqlOpcuaNamespace) Validate() error { + // required arguments + if _, ok := s.Cache.Load("id"); !ok { + return errors.New("Initialized \"opcua.namespace\" resource without a \"id\". This field is required.") + } + if _, ok := s.Cache.Load("name"); !ok { + return errors.New("Initialized \"opcua.namespace\" resource without a \"name\". This field is required.") + } + + return nil +} + +// Register accessor autogenerated +func (s *mqlOpcuaNamespace) Register(name string) error { + log.Trace().Str("field", name).Msg("[opcua.namespace].Register") + switch name { + case "id": + return nil + case "name": + return nil + default: + return errors.New("Cannot find field '" + name + "' in \"opcua.namespace\" resource") + } +} + +// Field accessor autogenerated +func (s *mqlOpcuaNamespace) Field(name string) (interface{}, error) { + log.Trace().Str("field", name).Msg("[opcua.namespace].Field") + switch name { + case "id": + return s.Id() + case "name": + return s.Name() + default: + return nil, fmt.Errorf("Cannot find field '" + name + "' in \"opcua.namespace\" resource") + } +} + +// Id accessor autogenerated +func (s *mqlOpcuaNamespace) Id() (int64, error) { + res, ok := s.Cache.Load("id") + if !ok || !res.Valid { + return 0, errors.New("\"opcua.namespace\" failed: no value provided for static field \"id\"") + } + if res.Error != nil { + return 0, res.Error + } + tres, ok := res.Data.(int64) + if !ok { + return 0, fmt.Errorf("\"opcua.namespace\" failed to cast field \"id\" to the right type (int64): %#v", res) + } + return tres, nil +} + +// Name accessor autogenerated +func (s *mqlOpcuaNamespace) Name() (string, error) { + res, ok := s.Cache.Load("name") + if !ok || !res.Valid { + return "", errors.New("\"opcua.namespace\" failed: no value provided for static field \"name\"") + } + if res.Error != nil { + return "", res.Error + } + tres, ok := res.Data.(string) + if !ok { + return "", fmt.Errorf("\"opcua.namespace\" failed to cast field \"name\" to the right type (string): %#v", res) + } + return tres, nil +} + +// Compute accessor autogenerated +func (s *mqlOpcuaNamespace) MqlCompute(name string) error { + log.Trace().Str("field", name).Msg("[opcua.namespace].MqlCompute") + switch name { + case "id": + return nil + case "name": + return nil + default: + return errors.New("Cannot find field '" + name + "' in \"opcua.namespace\" resource") + } +} + +// OpcuaNode resource interface +type OpcuaNode interface { + MqlResource() (*resources.Resource) + MqlCompute(string) error + Field(string) (interface{}, error) + Register(string) error + Validate() error + Id() (string, error) + Name() (string, error) + Namespace() (OpcuaNamespace, error) + Class() (string, error) + Description() (string, error) + Writeable() (bool, error) + DataType() (string, error) + Min() (string, error) + Max() (string, error) + Unit() (string, error) + AccessLevel() (string, error) + Properties() ([]interface{}, error) + Components() ([]interface{}, error) + Organizes() ([]interface{}, error) +} + +// mqlOpcuaNode for the opcua.node resource +type mqlOpcuaNode struct { + *resources.Resource +} + +// MqlResource to retrieve the underlying resource info +func (s *mqlOpcuaNode) MqlResource() *resources.Resource { + return s.Resource +} + +// create a new instance of the opcua.node resource +func newOpcuaNode(runtime *resources.Runtime, args *resources.Args) (interface{}, error) { + // User hooks + var err error + res := mqlOpcuaNode{runtime.NewResource("opcua.node")} + // assign all named fields + var id string + + now := time.Now().Unix() + for name, val := range *args { + if val == nil { + res.Cache.Store(name, &resources.CacheEntry{Data: val, Valid: true, Timestamp: now}) + continue + } + + switch name { + case "id": + if _, ok := val.(string); !ok { + return nil, errors.New("Failed to initialize \"opcua.node\", its \"id\" argument has the wrong type (expected type \"string\")") + } + case "name": + if _, ok := val.(string); !ok { + return nil, errors.New("Failed to initialize \"opcua.node\", its \"name\" argument has the wrong type (expected type \"string\")") + } + case "namespace": + if _, ok := val.(OpcuaNamespace); !ok { + return nil, errors.New("Failed to initialize \"opcua.node\", its \"namespace\" argument has the wrong type (expected type \"OpcuaNamespace\")") + } + case "class": + if _, ok := val.(string); !ok { + return nil, errors.New("Failed to initialize \"opcua.node\", its \"class\" argument has the wrong type (expected type \"string\")") + } + case "description": + if _, ok := val.(string); !ok { + return nil, errors.New("Failed to initialize \"opcua.node\", its \"description\" argument has the wrong type (expected type \"string\")") + } + case "writeable": + if _, ok := val.(bool); !ok { + return nil, errors.New("Failed to initialize \"opcua.node\", its \"writeable\" argument has the wrong type (expected type \"bool\")") + } + case "dataType": + if _, ok := val.(string); !ok { + return nil, errors.New("Failed to initialize \"opcua.node\", its \"dataType\" argument has the wrong type (expected type \"string\")") + } + case "min": + if _, ok := val.(string); !ok { + return nil, errors.New("Failed to initialize \"opcua.node\", its \"min\" argument has the wrong type (expected type \"string\")") + } + case "max": + if _, ok := val.(string); !ok { + return nil, errors.New("Failed to initialize \"opcua.node\", its \"max\" argument has the wrong type (expected type \"string\")") + } + case "unit": + if _, ok := val.(string); !ok { + return nil, errors.New("Failed to initialize \"opcua.node\", its \"unit\" argument has the wrong type (expected type \"string\")") + } + case "accessLevel": + if _, ok := val.(string); !ok { + return nil, errors.New("Failed to initialize \"opcua.node\", its \"accessLevel\" argument has the wrong type (expected type \"string\")") + } + case "properties": + if _, ok := val.([]interface{}); !ok { + return nil, errors.New("Failed to initialize \"opcua.node\", its \"properties\" argument has the wrong type (expected type \"[]interface{}\")") + } + case "components": + if _, ok := val.([]interface{}); !ok { + return nil, errors.New("Failed to initialize \"opcua.node\", its \"components\" argument has the wrong type (expected type \"[]interface{}\")") + } + case "organizes": + if _, ok := val.([]interface{}); !ok { + return nil, errors.New("Failed to initialize \"opcua.node\", its \"organizes\" argument has the wrong type (expected type \"[]interface{}\")") + } + case "__id": + idVal, ok := val.(string) + if !ok { + return nil, errors.New("Failed to initialize \"opcua.node\", its \"__id\" argument has the wrong type (expected type \"string\")") + } + id = idVal + default: + return nil, errors.New("Initialized opcua.node with unknown argument " + name) + } + res.Cache.Store(name, &resources.CacheEntry{Data: val, Valid: true, Timestamp: now}) + } + + // Get the ID + if id == "" { + res.Resource.Id, err = res.id() + if err != nil { + return nil, err + } + } else { + res.Resource.Id = id + } + + return &res, nil +} + +func (s *mqlOpcuaNode) Validate() error { + // required arguments + if _, ok := s.Cache.Load("id"); !ok { + return errors.New("Initialized \"opcua.node\" resource without a \"id\". This field is required.") + } + if _, ok := s.Cache.Load("name"); !ok { + return errors.New("Initialized \"opcua.node\" resource without a \"name\". This field is required.") + } + if _, ok := s.Cache.Load("class"); !ok { + return errors.New("Initialized \"opcua.node\" resource without a \"class\". This field is required.") + } + if _, ok := s.Cache.Load("description"); !ok { + return errors.New("Initialized \"opcua.node\" resource without a \"description\". This field is required.") + } + if _, ok := s.Cache.Load("writeable"); !ok { + return errors.New("Initialized \"opcua.node\" resource without a \"writeable\". This field is required.") + } + if _, ok := s.Cache.Load("dataType"); !ok { + return errors.New("Initialized \"opcua.node\" resource without a \"dataType\". This field is required.") + } + if _, ok := s.Cache.Load("min"); !ok { + return errors.New("Initialized \"opcua.node\" resource without a \"min\". This field is required.") + } + if _, ok := s.Cache.Load("max"); !ok { + return errors.New("Initialized \"opcua.node\" resource without a \"max\". This field is required.") + } + if _, ok := s.Cache.Load("unit"); !ok { + return errors.New("Initialized \"opcua.node\" resource without a \"unit\". This field is required.") + } + if _, ok := s.Cache.Load("accessLevel"); !ok { + return errors.New("Initialized \"opcua.node\" resource without a \"accessLevel\". This field is required.") + } + + return nil +} + +// Register accessor autogenerated +func (s *mqlOpcuaNode) Register(name string) error { + log.Trace().Str("field", name).Msg("[opcua.node].Register") + switch name { + case "id": + return nil + case "name": + return nil + case "namespace": + return nil + case "class": + return nil + case "description": + return nil + case "writeable": + return nil + case "dataType": + return nil + case "min": + return nil + case "max": + return nil + case "unit": + return nil + case "accessLevel": + return nil + case "properties": + return nil + case "components": + return nil + case "organizes": + return nil + default: + return errors.New("Cannot find field '" + name + "' in \"opcua.node\" resource") + } +} + +// Field accessor autogenerated +func (s *mqlOpcuaNode) Field(name string) (interface{}, error) { + log.Trace().Str("field", name).Msg("[opcua.node].Field") + switch name { + case "id": + return s.Id() + case "name": + return s.Name() + case "namespace": + return s.Namespace() + case "class": + return s.Class() + case "description": + return s.Description() + case "writeable": + return s.Writeable() + case "dataType": + return s.DataType() + case "min": + return s.Min() + case "max": + return s.Max() + case "unit": + return s.Unit() + case "accessLevel": + return s.AccessLevel() + case "properties": + return s.Properties() + case "components": + return s.Components() + case "organizes": + return s.Organizes() + default: + return nil, fmt.Errorf("Cannot find field '" + name + "' in \"opcua.node\" resource") + } +} + +// Id accessor autogenerated +func (s *mqlOpcuaNode) Id() (string, error) { + res, ok := s.Cache.Load("id") + if !ok || !res.Valid { + return "", errors.New("\"opcua.node\" failed: no value provided for static field \"id\"") + } + if res.Error != nil { + return "", res.Error + } + tres, ok := res.Data.(string) + if !ok { + return "", fmt.Errorf("\"opcua.node\" failed to cast field \"id\" to the right type (string): %#v", res) + } + return tres, nil +} + +// Name accessor autogenerated +func (s *mqlOpcuaNode) Name() (string, error) { + res, ok := s.Cache.Load("name") + if !ok || !res.Valid { + return "", errors.New("\"opcua.node\" failed: no value provided for static field \"name\"") + } + if res.Error != nil { + return "", res.Error + } + tres, ok := res.Data.(string) + if !ok { + return "", fmt.Errorf("\"opcua.node\" failed to cast field \"name\" to the right type (string): %#v", res) + } + return tres, nil +} + +// Namespace accessor autogenerated +func (s *mqlOpcuaNode) Namespace() (OpcuaNamespace, error) { + res, ok := s.Cache.Load("namespace") + if !ok || !res.Valid { + if err := s.ComputeNamespace(); err != nil { + return nil, err + } + res, ok = s.Cache.Load("namespace") + if !ok { + return nil, errors.New("\"opcua.node\" calculated \"namespace\" but didn't find its value in cache.") + } + s.MotorRuntime.Trigger(s, "namespace") + } + if res.Error != nil { + return nil, res.Error + } + tres, ok := res.Data.(OpcuaNamespace) + if !ok { + return nil, fmt.Errorf("\"opcua.node\" failed to cast field \"namespace\" to the right type (OpcuaNamespace): %#v", res) + } + return tres, nil +} + +// Class accessor autogenerated +func (s *mqlOpcuaNode) Class() (string, error) { + res, ok := s.Cache.Load("class") + if !ok || !res.Valid { + return "", errors.New("\"opcua.node\" failed: no value provided for static field \"class\"") + } + if res.Error != nil { + return "", res.Error + } + tres, ok := res.Data.(string) + if !ok { + return "", fmt.Errorf("\"opcua.node\" failed to cast field \"class\" to the right type (string): %#v", res) + } + return tres, nil +} + +// Description accessor autogenerated +func (s *mqlOpcuaNode) Description() (string, error) { + res, ok := s.Cache.Load("description") + if !ok || !res.Valid { + return "", errors.New("\"opcua.node\" failed: no value provided for static field \"description\"") + } + if res.Error != nil { + return "", res.Error + } + tres, ok := res.Data.(string) + if !ok { + return "", fmt.Errorf("\"opcua.node\" failed to cast field \"description\" to the right type (string): %#v", res) + } + return tres, nil +} + +// Writeable accessor autogenerated +func (s *mqlOpcuaNode) Writeable() (bool, error) { + res, ok := s.Cache.Load("writeable") + if !ok || !res.Valid { + return false, errors.New("\"opcua.node\" failed: no value provided for static field \"writeable\"") + } + if res.Error != nil { + return false, res.Error + } + tres, ok := res.Data.(bool) + if !ok { + return false, fmt.Errorf("\"opcua.node\" failed to cast field \"writeable\" to the right type (bool): %#v", res) + } + return tres, nil +} + +// DataType accessor autogenerated +func (s *mqlOpcuaNode) DataType() (string, error) { + res, ok := s.Cache.Load("dataType") + if !ok || !res.Valid { + return "", errors.New("\"opcua.node\" failed: no value provided for static field \"dataType\"") + } + if res.Error != nil { + return "", res.Error + } + tres, ok := res.Data.(string) + if !ok { + return "", fmt.Errorf("\"opcua.node\" failed to cast field \"dataType\" to the right type (string): %#v", res) + } + return tres, nil +} + +// Min accessor autogenerated +func (s *mqlOpcuaNode) Min() (string, error) { + res, ok := s.Cache.Load("min") + if !ok || !res.Valid { + return "", errors.New("\"opcua.node\" failed: no value provided for static field \"min\"") + } + if res.Error != nil { + return "", res.Error + } + tres, ok := res.Data.(string) + if !ok { + return "", fmt.Errorf("\"opcua.node\" failed to cast field \"min\" to the right type (string): %#v", res) + } + return tres, nil +} + +// Max accessor autogenerated +func (s *mqlOpcuaNode) Max() (string, error) { + res, ok := s.Cache.Load("max") + if !ok || !res.Valid { + return "", errors.New("\"opcua.node\" failed: no value provided for static field \"max\"") + } + if res.Error != nil { + return "", res.Error + } + tres, ok := res.Data.(string) + if !ok { + return "", fmt.Errorf("\"opcua.node\" failed to cast field \"max\" to the right type (string): %#v", res) + } + return tres, nil +} + +// Unit accessor autogenerated +func (s *mqlOpcuaNode) Unit() (string, error) { + res, ok := s.Cache.Load("unit") + if !ok || !res.Valid { + return "", errors.New("\"opcua.node\" failed: no value provided for static field \"unit\"") + } + if res.Error != nil { + return "", res.Error + } + tres, ok := res.Data.(string) + if !ok { + return "", fmt.Errorf("\"opcua.node\" failed to cast field \"unit\" to the right type (string): %#v", res) + } + return tres, nil +} + +// AccessLevel accessor autogenerated +func (s *mqlOpcuaNode) AccessLevel() (string, error) { + res, ok := s.Cache.Load("accessLevel") + if !ok || !res.Valid { + return "", errors.New("\"opcua.node\" failed: no value provided for static field \"accessLevel\"") + } + if res.Error != nil { + return "", res.Error + } + tres, ok := res.Data.(string) + if !ok { + return "", fmt.Errorf("\"opcua.node\" failed to cast field \"accessLevel\" to the right type (string): %#v", res) + } + return tres, nil +} + +// Properties accessor autogenerated +func (s *mqlOpcuaNode) Properties() ([]interface{}, error) { + res, ok := s.Cache.Load("properties") + if !ok || !res.Valid { + if err := s.ComputeProperties(); err != nil { + return nil, err + } + res, ok = s.Cache.Load("properties") + if !ok { + return nil, errors.New("\"opcua.node\" calculated \"properties\" but didn't find its value in cache.") + } + s.MotorRuntime.Trigger(s, "properties") + } + if res.Error != nil { + return nil, res.Error + } + tres, ok := res.Data.([]interface{}) + if !ok { + return nil, fmt.Errorf("\"opcua.node\" failed to cast field \"properties\" to the right type ([]interface{}): %#v", res) + } + return tres, nil +} + +// Components accessor autogenerated +func (s *mqlOpcuaNode) Components() ([]interface{}, error) { + res, ok := s.Cache.Load("components") + if !ok || !res.Valid { + if err := s.ComputeComponents(); err != nil { + return nil, err + } + res, ok = s.Cache.Load("components") + if !ok { + return nil, errors.New("\"opcua.node\" calculated \"components\" but didn't find its value in cache.") + } + s.MotorRuntime.Trigger(s, "components") + } + if res.Error != nil { + return nil, res.Error + } + tres, ok := res.Data.([]interface{}) + if !ok { + return nil, fmt.Errorf("\"opcua.node\" failed to cast field \"components\" to the right type ([]interface{}): %#v", res) + } + return tres, nil +} + +// Organizes accessor autogenerated +func (s *mqlOpcuaNode) Organizes() ([]interface{}, error) { + res, ok := s.Cache.Load("organizes") + if !ok || !res.Valid { + if err := s.ComputeOrganizes(); err != nil { + return nil, err + } + res, ok = s.Cache.Load("organizes") + if !ok { + return nil, errors.New("\"opcua.node\" calculated \"organizes\" but didn't find its value in cache.") + } + s.MotorRuntime.Trigger(s, "organizes") + } + if res.Error != nil { + return nil, res.Error + } + tres, ok := res.Data.([]interface{}) + if !ok { + return nil, fmt.Errorf("\"opcua.node\" failed to cast field \"organizes\" to the right type ([]interface{}): %#v", res) + } + return tres, nil +} + +// Compute accessor autogenerated +func (s *mqlOpcuaNode) MqlCompute(name string) error { + log.Trace().Str("field", name).Msg("[opcua.node].MqlCompute") + switch name { + case "id": + return nil + case "name": + return nil + case "namespace": + return s.ComputeNamespace() + case "class": + return nil + case "description": + return nil + case "writeable": + return nil + case "dataType": + return nil + case "min": + return nil + case "max": + return nil + case "unit": + return nil + case "accessLevel": + return nil + case "properties": + return s.ComputeProperties() + case "components": + return s.ComputeComponents() + case "organizes": + return s.ComputeOrganizes() + default: + return errors.New("Cannot find field '" + name + "' in \"opcua.node\" resource") + } +} + +// ComputeNamespace computer autogenerated +func (s *mqlOpcuaNode) ComputeNamespace() error { + var err error + if _, ok := s.Cache.Load("namespace"); ok { + return nil + } + vres, err := s.GetNamespace() + if _, ok := err.(resources.NotReadyError); ok { + return err + } + s.Cache.Store("namespace", &resources.CacheEntry{Data: vres, Valid: true, Error: err, Timestamp: time.Now().Unix()}) + return nil +} + +// ComputeProperties computer autogenerated +func (s *mqlOpcuaNode) ComputeProperties() error { + var err error + if _, ok := s.Cache.Load("properties"); ok { + return nil + } + vres, err := s.GetProperties() + if _, ok := err.(resources.NotReadyError); ok { + return err + } + s.Cache.Store("properties", &resources.CacheEntry{Data: vres, Valid: true, Error: err, Timestamp: time.Now().Unix()}) + return nil +} + +// ComputeComponents computer autogenerated +func (s *mqlOpcuaNode) ComputeComponents() error { + var err error + if _, ok := s.Cache.Load("components"); ok { + return nil + } + vres, err := s.GetComponents() + if _, ok := err.(resources.NotReadyError); ok { + return err + } + s.Cache.Store("components", &resources.CacheEntry{Data: vres, Valid: true, Error: err, Timestamp: time.Now().Unix()}) + return nil +} + +// ComputeOrganizes computer autogenerated +func (s *mqlOpcuaNode) ComputeOrganizes() error { + var err error + if _, ok := s.Cache.Load("organizes"); ok { + return nil + } + vres, err := s.GetOrganizes() + if _, ok := err.(resources.NotReadyError); ok { + return err + } + s.Cache.Store("organizes", &resources.CacheEntry{Data: vres, Valid: true, Error: err, Timestamp: time.Now().Unix()}) + return nil +} + diff --git a/resources/packs/opcua/opcua.lr.manifest.yaml b/resources/packs/opcua/opcua.lr.manifest.yaml new file mode 100755 index 0000000000..219cd07a78 --- /dev/null +++ b/resources/packs/opcua/opcua.lr.manifest.yaml @@ -0,0 +1,38 @@ +resources: + opcua: + fields: + namespaces: {} + nodes: {} + root: {} + min_mondoo_version: latest + opcua.namespace: + fields: + id: {} + name: {} + min_mondoo_version: latest + opcua.node: + fields: + accessLevel: {} + class: {} + components: {} + dataType: {} + description: {} + id: {} + max: {} + min: {} + name: {} + namespace: {} + nodeid: {} + organizes: {} + properties: {} + unit: {} + writeable: {} + min_mondoo_version: latest + opcua.server: + fields: + buildInfo: {} + currentTime: {} + node: {} + startTime: {} + state: {} + min_mondoo_version: latest diff --git a/resources/packs/opcua/server.go b/resources/packs/opcua/server.go new file mode 100644 index 0000000000..8c4a9f65a2 --- /dev/null +++ b/resources/packs/opcua/server.go @@ -0,0 +1,70 @@ +package opcua + +import ( + "context" + "github.com/gopcua/opcua/id" + "github.com/gopcua/opcua/ua" + "go.mondoo.com/cnquery/resources" + "go.mondoo.com/cnquery/resources/packs/core" +) + +func (o *mqlOpcuaServer) init(args *resources.Args) (*resources.Args, OpcuaServer, error) { + op, err := opcuaProvider(o.MotorRuntime.Motor.Provider) + if err != nil { + return nil, nil, err + } + client := op.Client() + + ctx := context.Background() + + n := client.Node(ua.NewNumericNodeID(0, id.Server)) + ndef, err := fetchNodeInfo(ctx, n) + if err != nil { + return nil, nil, err + } + + // create server resource + serverNode, err := newMqlOpcuaNodeResource(o.MotorRuntime, ndef) + if err != nil { + return nil, nil, err + } + (*args)["node"] = serverNode + + // server status variable of server + v, err := client.Node(ua.NewNumericNodeID(0, id.Server_ServerStatus)).Value() + switch { + case err != nil: + return nil, nil, err + case v == nil: + (*args)["buildInfo"] = nil + (*args)["currentTime"] = nil + (*args)["startTime"] = nil + (*args)["state"] = "" + default: + res := v.Value() + extensionObject := res.(*ua.ExtensionObject) + serverStatus := extensionObject.Value.(*ua.ServerStatusDataType) + + buildInfo, _ := core.JsonToDict(serverStatus.BuildInfo) + (*args)["buildInfo"] = buildInfo + (*args)["currentTime"] = &serverStatus.CurrentTime + (*args)["startTime"] = &serverStatus.StartTime + (*args)["state"] = serverStatus.State.String() + } + + return args, nil, nil +} + +func (o *mqlOpcuaServer) id() (string, error) { + node, err := o.Node() + if err != nil { + return "", err + } + + id, err := node.Id() + if err != nil { + return "", err + } + + return "opcua.server/" + id, nil +}