diff --git a/resources/packs/opcua/README.md b/resources/packs/opcua/README.md new file mode 100644 index 0000000000..b090b53c4a --- /dev/null +++ b/resources/packs/opcua/README.md @@ -0,0 +1,76 @@ +# OPC UA Resource Pack + +Query all nodes: + +```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/opcua.lr.json b/resources/packs/opcua/info/opcua.lr.json index b0d6f471c6..a1722eb96c 100644 --- a/resources/packs/opcua/info/opcua.lr.json +++ b/resources/packs/opcua/info/opcua.lr.json @@ -1 +1 @@ -{"resources":{"opcua":{"id":"opcua","name":"opcua","fields":{"namespaces":{"name":"namespaces","type":"\u0019\u0007"},"root":{"name":"root","type":"\u001bopcua.node"}},"title":"OPC UA"},"opcua.namespace":{"id":"opcua.namespace","name":"opcua.namespace","fields":{"id":{"name":"id","type":"\u0005","is_mandatory":true},"name":{"name":"name","type":"\u0007","is_mandatory":true}},"title":"OPC UA Namespace"},"opcua.node":{"id":"opcua.node","name":"opcua.node","fields":{"accessLevel":{"name":"accessLevel","type":"\u0007","is_mandatory":true},"class":{"name":"class","type":"\u0007","is_mandatory":true},"components":{"name":"components","type":"\u0019\u001bopcua.node"},"dataType":{"name":"dataType","type":"\u0007","is_mandatory":true},"description":{"name":"description","type":"\u0007","is_mandatory":true},"id":{"name":"id","type":"\u0007","is_mandatory":true},"max":{"name":"max","type":"\u0007","is_mandatory":true},"min":{"name":"min","type":"\u0007","is_mandatory":true},"name":{"name":"name","type":"\u0007","is_mandatory":true},"organizes":{"name":"organizes","type":"\u0019\u001bopcua.node"},"properties":{"name":"properties","type":"\u0019\u001bopcua.node"},"unit":{"name":"unit","type":"\u0007","is_mandatory":true},"writeable":{"name":"writeable","type":"\u0004","is_mandatory":true}},"title":"OPC UA Node","defaults":"id name"}}} \ No newline at end of file +{"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},"class":{"name":"class","type":"\u0007","is_mandatory":true},"components":{"name":"components","type":"\u0019\u001bopcua.node"},"dataType":{"name":"dataType","type":"\u0007","is_mandatory":true},"description":{"name":"description","type":"\u0007","is_mandatory":true},"id":{"name":"id","type":"\u0007","is_mandatory":true},"max":{"name":"max","type":"\u0007","is_mandatory":true},"min":{"name":"min","type":"\u0007","is_mandatory":true},"name":{"name":"name","type":"\u0007","is_mandatory":true},"namespace":{"name":"namespace","type":"\u001bopcua.namespace"},"organizes":{"name":"organizes","type":"\u0019\u001bopcua.node"},"properties":{"name":"properties","type":"\u0019\u001bopcua.node"},"unit":{"name":"unit","type":"\u0007","is_mandatory":true},"writeable":{"name":"writeable","type":"\u0004","is_mandatory":true}},"title":"OPC UA Node","defaults":"id name"},"opcua.server":{"id":"opcua.server","name":"opcua.server","fields":{"buildInfo":{"name":"buildInfo","type":"\n","is_mandatory":true},"currentTime":{"name":"currentTime","type":"\t","is_mandatory":true},"node":{"name":"node","type":"\u001bopcua.node","is_mandatory":true},"startTime":{"name":"startTime","type":"\t","is_mandatory":true},"state":{"name":"state","type":"\u0007","is_mandatory":true}},"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 index 2d1e35c441..f7946f9c6e 100644 --- a/resources/packs/opcua/info/opcua.lr.manifest.json +++ b/resources/packs/opcua/info/opcua.lr.manifest.json @@ -1 +1 @@ -{"resources":{"opcua":{"fields":{"namespaces":{},"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"}}} \ No newline at end of file +{"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 index f5e182c7dd..347bb4f66f 100644 --- a/resources/packs/opcua/node.go +++ b/resources/packs/opcua/node.go @@ -2,12 +2,15 @@ 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 NodeDef struct { +type nodeMeta struct { NodeID *ua.NodeID NodeClass ua.NodeClass BrowseName string @@ -25,13 +28,13 @@ type NodeDef struct { Properties []*opcua.Node } -func fetchNodeInfo(ctx context.Context, n *opcua.Node) (*NodeDef, error) { +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 = NodeDef{ + var def = nodeMeta{ NodeID: n.ID, } @@ -137,3 +140,155 @@ func fetchNodeInfo(ctx context.Context, n *opcua.Node) (*NodeDef, error) { 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 index ae69f68bd8..b0d7352eed 100644 --- a/resources/packs/opcua/opcua.go +++ b/resources/packs/opcua/opcua.go @@ -3,13 +3,10 @@ package opcua import ( "context" "errors" - "fmt" "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" - "go.mondoo.com/cnquery/resources/packs/core" "go.mondoo.com/cnquery/resources/packs/opcua/info" ) @@ -31,40 +28,6 @@ func (o *mqlOpcua) id() (string, error) { return "opcua", 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() - return core.StrSliceToInterface(namespaces), nil -} - -func newMqlOpcuaNodeResource(runtime *resources.Runtime, ndef *NodeDef) (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 *mqlOpcua) GetRoot() (interface{}, error) { op, err := opcuaProvider(o.MotorRuntime.Motor.Provider) if err != nil { @@ -72,121 +35,93 @@ func (o *mqlOpcua) GetRoot() (interface{}, error) { } client := op.Client() - n := client.Node(ua.NewNumericNodeID(0, id.RootFolder)) 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 (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) { - return nil, 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.(*NodeDef) - if !ok { - return nil, fmt.Errorf("\"opcua\" failed to cast field \"node\" to the right type: %#v", res) - } +func resolve(ctx context.Context, meta *nodeMeta) ([]*nodeMeta, error) { + nodeList := []*nodeMeta{} - ctx := context.Background() - results := []interface{}{} - for i := range nodeDef.Properties { - def := nodeDef.Properties[i] - n, err := fetchNodeInfo(ctx, def) + for i := range meta.Organizes { + child := meta.Organizes[i] + nInfoChild, err := fetchNodeInfo(ctx, child) if err != nil { return nil, err } - r, err := newMqlOpcuaNodeResource(o.MotorRuntime, n) + nodeList = append(nodeList, nInfoChild) + resolved, err := resolve(ctx, nInfoChild) if err != nil { return nil, err } - results = append(results, r) + nodeList = append(nodeList, resolved...) } - 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.(*NodeDef) - if !ok { - return nil, fmt.Errorf("\"opcua\" failed to cast field \"node\" to the right type: %#v", res) + 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...) } - ctx := context.Background() - results := []interface{}{} - for i := range nodeDef.Components { - def := nodeDef.Components[i] - n, err := fetchNodeInfo(ctx, def) + for i := range meta.Components { + child := meta.Components[i] + nInfoChild, err := fetchNodeInfo(ctx, child) if err != nil { return nil, err } - r, err := newMqlOpcuaNodeResource(o.MotorRuntime, n) + nodeList = append(nodeList, nInfoChild) + resolved, err := resolve(ctx, nInfoChild) if err != nil { return nil, err } - results = append(results, r) + nodeList = append(nodeList, resolved...) } - return results, nil + return nodeList, nil } -func (o *mqlOpcuaNode) GetOrganizes() ([]interface{}, error) { - res, ok := o.Cache.Load("_object") - if !ok { - return nil, errors.New("could not fetch properties") +func (o *mqlOpcua) GetNodes() ([]interface{}, error) { + op, err := opcuaProvider(o.MotorRuntime.Motor.Provider) + if err != nil { + return nil, err } - if res.Error != nil { - return nil, res.Error + 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 } - nodeDef, ok := res.Data.(*NodeDef) - if !ok { - return nil, fmt.Errorf("\"opcua\" failed to cast field \"node\" to the right type: %#v", res) + nodeList = append(nodeList, nInfo) + resolved, err := resolve(ctx, nInfo) + if err != nil { + return nil, err } + nodeList = append(nodeList, resolved...) - 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) + // convert list to interface + res := []interface{}{} + for i := range nodeList { + entry, err := newMqlOpcuaNodeResource(o.MotorRuntime, nodeList[i]) if err != nil { return nil, err } - results = append(results, r) + res = append(res, entry) } - - return results, nil -} - -func (o *mqlOpcuaNamespace) id() (string, error) { - return "opcua.namespace", nil + return res, nil } diff --git a/resources/packs/opcua/opcua.lr b/resources/packs/opcua/opcua.lr index 627acffac8..3014aaceef 100644 --- a/resources/packs/opcua/opcua.lr +++ b/resources/packs/opcua/opcua.lr @@ -2,29 +2,64 @@ option go_package = "go.mondoo.com/cnquery/resources/packs/opcua" // OPC UA opcua { - namespaces() []string + // Namespaces + namespaces() []opcua.namespace + // Root node root() opcua.node + // List of all nodes + nodes() []opcua.node +} + +// Server Object +opcua.server { + // + node opcua.node + // + buildInfo dict + // + currentTime time + // + startTime time + // + state string } // OPC UA Namespace opcua.namespace { + // Namespace ID id int + // Namespace Name name string } // OPC UA Node opcua.node @defaults("id name") { + // id string + // name string + // + namespace() opcua.namespace + // class string + // description string + // writeable bool + // dataType string + // min string + // max string + // unit string + // accessLevel string + // properties() []opcua.node + // components() []opcua.node + // organizes() []opcua.node } diff --git a/resources/packs/opcua/opcua.lr.go b/resources/packs/opcua/opcua.lr.go index 76698dc55c..8541563b1d 100644 --- a/resources/packs/opcua/opcua.lr.go +++ b/resources/packs/opcua/opcua.lr.go @@ -13,6 +13,7 @@ import ( // 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) } @@ -26,6 +27,7 @@ type Opcua interface { Validate() error Namespaces() ([]interface{}, error) Root() (OpcuaNode, error) + Nodes() ([]interface{}, error) } // mqlOpcua for the opcua resource @@ -62,6 +64,10 @@ func newOpcua(runtime *resources.Runtime, args *resources.Args) (interface{}, er 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 { @@ -102,6 +108,8 @@ func (s *mqlOpcua) Register(name string) error { return nil case "root": return nil + case "nodes": + return nil default: return errors.New("Cannot find field '" + name + "' in \"opcua\" resource") } @@ -115,6 +123,8 @@ func (s *mqlOpcua) Field(name string) (interface{}, error) { 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") } @@ -166,6 +176,29 @@ func (s *mqlOpcua) Root() (OpcuaNode, error) { 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") @@ -174,6 +207,8 @@ func (s *mqlOpcua) MqlCompute(name string) error { return s.ComputeNamespaces() case "root": return s.ComputeRoot() + case "nodes": + return s.ComputeNodes() default: return errors.New("Cannot find field '" + name + "' in \"opcua\" resource") } @@ -207,6 +242,272 @@ func (s *mqlOpcua) ComputeRoot() error { 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) @@ -369,6 +670,7 @@ type OpcuaNode interface { Validate() error Id() (string, error) Name() (string, error) + Namespace() (OpcuaNamespace, error) Class() (string, error) Description() (string, error) Writeable() (bool, error) @@ -416,6 +718,10 @@ func newOpcuaNode(runtime *resources.Runtime, args *resources.Args) (interface{} 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\")") @@ -529,6 +835,8 @@ func (s *mqlOpcuaNode) Register(name string) error { return nil case "name": return nil + case "namespace": + return nil case "class": return nil case "description": @@ -564,6 +872,8 @@ func (s *mqlOpcuaNode) Field(name string) (interface{}, error) { return s.Id() case "name": return s.Name() + case "namespace": + return s.Namespace() case "class": return s.Class() case "description": @@ -623,6 +933,29 @@ func (s *mqlOpcuaNode) Name() (string, error) { 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") @@ -828,6 +1161,8 @@ func (s *mqlOpcuaNode) MqlCompute(name string) error { return nil case "name": return nil + case "namespace": + return s.ComputeNamespace() case "class": return nil case "description": @@ -855,6 +1190,20 @@ func (s *mqlOpcuaNode) MqlCompute(name string) error { } } +// 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 diff --git a/resources/packs/opcua/opcua.lr.manifest.yaml b/resources/packs/opcua/opcua.lr.manifest.yaml index 91388c0363..219cd07a78 100755 --- a/resources/packs/opcua/opcua.lr.manifest.yaml +++ b/resources/packs/opcua/opcua.lr.manifest.yaml @@ -2,6 +2,7 @@ resources: opcua: fields: namespaces: {} + nodes: {} root: {} min_mondoo_version: latest opcua.namespace: @@ -27,3 +28,11 @@ resources: 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 +}