diff --git a/providers/nmap/README.md b/providers/nmap/README.md index c17b1dd39e..e869f46189 100644 --- a/providers/nmap/README.md +++ b/providers/nmap/README.md @@ -1,4 +1,4 @@ -# Nmap provider +# Nmap Provider Nmap, short for Network Mapper, is a powerful and versatile open-source tool used for network discovery and security auditing. This tool is widely utilized by network administrators, security professionals, and penetration testers to map out network structures, discover hosts, identify services, and detect vulnerabilities. diff --git a/providers/nmap/config/config.go b/providers/nmap/config/config.go index 9d5169e85c..db4be379fb 100644 --- a/providers/nmap/config/config.go +++ b/providers/nmap/config/config.go @@ -4,7 +4,9 @@ package config import ( + "go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory" "go.mondoo.com/cnquery/v11/providers-sdk/v1/plugin" + "go.mondoo.com/cnquery/v11/providers/nmap/connection" "go.mondoo.com/cnquery/v11/providers/nmap/provider" ) @@ -15,11 +17,35 @@ var Config = plugin.Provider{ ConnectionTypes: []string{provider.DefaultConnectionType}, Connectors: []plugin.Connector{ { - Name: "nmap", - Use: "nmap", - Short: "a Nmap network scanner", - Discovery: []string{}, - Flags: []plugin.Flag{}, + Name: "nmap", + Use: "nmap", + Short: "a Nmap network scanner", + MinArgs: 0, + MaxArgs: 2, + Discovery: []string{ + connection.DiscoveryAll, + connection.DiscoveryAuto, + connection.DiscoveryHosts, + }, + Flags: []plugin.Flag{ + { + Long: "networks", + Type: plugin.FlagType_List, + Default: "", + Desc: "Only include repositories with matching names", + }, + }, + }, + }, + AssetUrlTrees: []*inventory.AssetUrlBranch{ + { + PathSegments: []string{"technology=network", "category=nmap"}, + Key: "kind", + Title: "Kind", + Values: map[string]*inventory.AssetUrlBranch{ + "host": nil, + "domain": nil, + }, }, }, } diff --git a/providers/nmap/connection/connection.go b/providers/nmap/connection/connection.go index 54f1515e6b..a9dfbb3504 100644 --- a/providers/nmap/connection/connection.go +++ b/providers/nmap/connection/connection.go @@ -4,22 +4,30 @@ package connection import ( + "strings" + "go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory" "go.mondoo.com/cnquery/v11/providers-sdk/v1/plugin" ) +const ( + DiscoveryAll = "all" + DiscoveryAuto = "auto" + DiscoveryHosts = "hosts" +) + type NmapConnection struct { plugin.Connection - Conf *inventory.Config - asset *inventory.Asset + Conf *inventory.Config + asset *inventory.Asset // Add custom connection fields here } func NewNmapConnection(id uint32, asset *inventory.Asset, conf *inventory.Config) (*NmapConnection, error) { conn := &NmapConnection{ Connection: plugin.NewConnection(id, asset), - Conf: conf, - asset: asset, + Conf: conf, + asset: asset, } // initialize your connection here @@ -35,3 +43,67 @@ func (c *NmapConnection) Asset() *inventory.Asset { return c.asset } +func nmapHostPlatform() *inventory.Platform { + return &inventory.Platform{ + Name: "nmap-host", + Title: "Nmap Host", + Family: []string{"nmap"}, + Kind: "api", + Runtime: "nmap", + TechnologyUrlSegments: []string{"network", "nmap", "host"}, + } +} + +func nmapDomainPlatform() *inventory.Platform { + return &inventory.Platform{ + Name: "nmap-domain", + Title: "Nmap Domain", + Family: []string{"nmap"}, + Kind: "api", + Runtime: "nmap", + TechnologyUrlSegments: []string{"network", "nmap", "domain"}, + } +} + +func nmapPlatform() *inventory.Platform { + return &inventory.Platform{ + Name: "nmap-org", + Title: "Nmap", + Family: []string{"nmap"}, + Kind: "api", + Runtime: "nmap", + TechnologyUrlSegments: []string{"network", "nmap", "org"}, + } +} + +func (c *NmapConnection) PlatformInfo() (*inventory.Platform, error) { + conf := c.asset.Connections[0] + + if conf.Options != nil && conf.Options["search"] != "" { + search := conf.Options["search"] + switch search { + case "host": + return nmapHostPlatform(), nil + case "domain": + return nmapDomainPlatform(), nil + } + } + return nmapPlatform(), nil +} + +func (c *NmapConnection) Identifier() string { + baseId := "//platformid.api.mondoo.app/runtime/nmap" + + conf := c.asset.Connections[0] + if conf.Options != nil && conf.Options["search"] != "" { + search := conf.Options["search"] + switch search { + case "host": + return baseId + "/host/" + strings.ToLower(conf.Host) + case "domain": + return baseId + "/domain/" + strings.ToLower(conf.Host) + } + } + + return baseId +} diff --git a/providers/nmap/provider/provider.go b/providers/nmap/provider/provider.go index 9f635ca951..f42a1b562f 100644 --- a/providers/nmap/provider/provider.go +++ b/providers/nmap/provider/provider.go @@ -6,6 +6,7 @@ package provider import ( "context" "errors" + "strings" "go.mondoo.com/cnquery/v11/llx" "go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory" @@ -40,9 +41,48 @@ func (s *Service) ParseCLI(req *plugin.ParseCLIReq) (*plugin.ParseCLIRes, error) Options: map[string]string{}, } - // Do custom flag parsing here + // discovery flags + discoverTargets := []string{} + if x, ok := flags["discover"]; ok && len(x.Array) != 0 { + for i := range x.Array { + entry := string(x.Array[i].Value) + discoverTargets = append(discoverTargets, entry) + } + } else { + discoverTargets = []string{"auto"} + } + conf.Discover = &inventory.Discovery{Targets: discoverTargets} + + // nmap also supports the following sub-commands, those are optional + name := "" + if len(req.Args) > 0 { + switch req.Args[0] { + case "host": + conf.Host = req.Args[1] + conf.Options["search"] = "host" + case "domain": + conf.Host = req.Args[1] + conf.Options["search"] = "domain" + default: + return nil, errors.New("invalid nmap sub-command, supported are: host or domain") + } + } else { + name = "Nmap" + } + + if networks, ok := flags["networks"]; ok { + if networks.Array != nil { + networksValues := []string{} + for _, network := range networks.Array { + networksValues = append(networksValues, string(network.Value)) + } + + conf.Options["networks"] = strings.Join(networksValues, ",") + } + } asset := inventory.Asset{ + Name: name, Connections: []*inventory.Config{conf}, } @@ -66,11 +106,16 @@ func (s *Service) Connect(req *plugin.ConnectReq, callback plugin.ProviderCallba } } + inv, err := s.discover(conn) + if err != nil { + return nil, err + } + return &plugin.ConnectRes{ Id: conn.ID(), Name: conn.Name(), Asset: req.Asset, - Inventory: nil, + Inventory: inv, }, nil } @@ -120,22 +165,33 @@ func (s *Service) connect(req *plugin.ConnectReq, callback plugin.ProviderCallba } func (s *Service) detect(asset *inventory.Asset, conn *connection.NmapConnection) error { - // TODO: adjust asset detection asset.Id = conn.Conf.Type asset.Name = conn.Conf.Host - asset.Platform = &inventory.Platform{ - Name: "nmap", - Family: []string{"nmap"}, - Kind: "api", - Title: "nmap", + platform, err := conn.PlatformInfo() + if err != nil { + return err } - // TODO: Add platform IDs - asset.PlatformIds = []string{"//platformid.api.mondoo.app/runtime/nmap"} + asset.Platform = platform + asset.PlatformIds = []string{conn.Identifier()} return nil } +func (s *Service) discover(conn *connection.NmapConnection) (*inventory.Inventory, error) { + conf := conn.Asset().Connections[0] + if conf.Discover == nil { + return nil, nil + } + + runtime, err := s.GetRuntime(conn.ID()) + if err != nil { + return nil, err + } + + return resources.Discover(runtime, conf.Options) +} + func (s *Service) MockConnect(req *plugin.ConnectReq, callback plugin.ProviderCallback) (*plugin.ConnectRes, error) { return nil, errors.New("mock connect not yet implemented") } diff --git a/providers/nmap/resources/discovery.go b/providers/nmap/resources/discovery.go new file mode 100644 index 0000000000..7da4dc1e27 --- /dev/null +++ b/providers/nmap/resources/discovery.go @@ -0,0 +1,78 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package resources + +import ( + "strings" + + "go.mondoo.com/cnquery/v11/llx" + "go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory" + "go.mondoo.com/cnquery/v11/providers-sdk/v1/plugin" + "go.mondoo.com/cnquery/v11/providers/nmap/connection" + "go.mondoo.com/cnquery/v11/utils/stringx" +) + +func Discover(runtime *plugin.Runtime, opts map[string]string) (*inventory.Inventory, error) { + conn := runtime.Connection.(*connection.NmapConnection) + if conn == nil || conn.Asset() == nil || len(conn.Asset().Connections) == 0 { + return nil, nil + } + + conf := conn.Asset().Connections[0] + targets := handleTargets(conf.Discover.Targets) + if !stringx.ContainsAnyOf(targets, connection.DiscoveryHosts, connection.DiscoveryAll, connection.DiscoveryAuto) { + return nil, nil + } + + // we only need to discover when networks are specified + networkValue, ok := conf.Options["networks"] + if !ok || networkValue == "" { + return nil, nil + } + networks := strings.Split(networkValue, ",") + assetList := []*inventory.Asset{} + + for i := range networks { + network := networks[i] + + targetResource, err := runtime.CreateResource(runtime, "nmap.target ", map[string]*llx.RawData{ + "target": llx.StringData(network), + }) + if err != nil { + return nil, err + } + hosts := targetResource.(*mqlNmapTarget).GetHosts().Data + for i := range hosts { + entry := hosts[i] + host := entry.(*mqlNmapHost) + + a := &inventory.Asset{ + Name: host.GetName().Data, + Connections: []*inventory.Config{ + { + Type: "nmap", + Host: host.GetName().Data, + Credentials: conf.Credentials, + }, + }, + } + + assetList = append(assetList, a) + } + } + + in := &inventory.Inventory{Spec: &inventory.InventorySpec{ + Assets: assetList, + }} + return in, nil +} + +func handleTargets(targets []string) []string { + if stringx.Contains(targets, connection.DiscoveryAll) { + return []string{ + connection.DiscoveryHosts, + } + } + return targets +} diff --git a/providers/nmap/resources/host.go b/providers/nmap/resources/host.go new file mode 100644 index 0000000000..9517f6729b --- /dev/null +++ b/providers/nmap/resources/host.go @@ -0,0 +1,94 @@ +package resources + +import ( + "errors" + "strconv" + "strings" + "time" + + "github.com/Ullaakut/nmap/v3" + "github.com/google/uuid" + "go.mondoo.com/cnquery/v11/llx" + "go.mondoo.com/cnquery/v11/providers-sdk/v1/plugin" + "go.mondoo.com/cnquery/v11/providers-sdk/v1/util/convert" + "go.mondoo.com/cnquery/v11/providers/nmap/connection" + "go.mondoo.com/cnquery/v11/types" +) + +func initNmapHost(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) { + if _, ok := args["target"]; !ok { + // try to get the ip from the connection + conn := runtime.Connection.(*connection.NmapConnection) + if conn.Conf.Options != nil && conn.Conf.Options["search"] == "host" { + args["target"] = llx.StringData(conn.Conf.Host) + } + } + + if _, ok := args["target"]; !ok { + return nil, nil, errors.New("missing required argument 'host'") + } + + return args, nil, nil +} + +func newMqlNmapHost(runtime *plugin.Runtime, host nmap.Host) (*mqlNmapHost, error) { + distance, _ := convert.JsonToDict(host.Distance) + os, _ := convert.JsonToDict(host.OS) + trace, _ := convert.JsonToDict(host.Trace) + addresses, _ := convert.JsonToDictSlice(host.Addresses) + hostnames, _ := convert.JsonToDictSlice(host.Hostnames) + + // TODO: consider using the host IP from nmap since it is more reliable + id := uuid.New().String() + + ports := make([]interface{}, 0) + for _, port := range host.Ports { + r, err := newMqlNmapPort(runtime, id, port) + if err != nil { + return nil, err + } + ports = append(ports, r) + } + + name := "" + if len(host.Addresses) == 1 { + name = host.Addresses[0].Addr + } else { + entries := []string{} + for _, addr := range host.Addresses { + if addr.Addr != "" { + entries = append(entries, addr.Addr) + } + } + name = strings.Join(entries, ", ") + } + + mqlNmapHostResource, err := CreateResource(runtime, "nmap.host", map[string]*llx.RawData{ + "__id": llx.StringData("nmap.host/" + id), + "name": llx.StringData(name), + "distance": llx.DictData(distance), + "os": llx.DictData(os), + "endTime": llx.TimeData(time.Time(host.EndTime)), + "comment": llx.StringData(host.Comment), + "trace": llx.DictData(trace), + "addresses": llx.ArrayData(addresses, types.Dict), + "hostnames": llx.ArrayData(hostnames, types.Dict), + "ports": llx.ArrayData(ports, types.Resource("nmap.port")), + "state": llx.StringData(host.Status.State), + }) + return mqlNmapHostResource.(*mqlNmapHost), err +} + +func newMqlNmapPort(runtime *plugin.Runtime, id string, port nmap.Port) (*mqlNmapPort, error) { + mqlPort, err := CreateResource(runtime, "nmap.port", map[string]*llx.RawData{ + "__id": llx.StringData("nmap.port/" + id + "/" + strconv.Itoa(int(port.ID))), + "port": llx.IntData(int64(port.ID)), + "service": llx.StringData(port.Service.Name), + "method": llx.StringData(port.Service.Method), + "protocol": llx.StringData(port.Protocol), + "product": llx.StringData(port.Service.Product), + "version": llx.StringData(port.Service.Version), + "state": llx.StringData(port.State.State), + }) + return mqlPort.(*mqlNmapPort), err +} diff --git a/providers/nmap/resources/nmap.go b/providers/nmap/resources/nmap.go index 568e465c5a..3b7c77e7c9 100644 --- a/providers/nmap/resources/nmap.go +++ b/providers/nmap/resources/nmap.go @@ -5,17 +5,13 @@ package resources import ( "context" - "strconv" - "strings" "time" "github.com/Ullaakut/nmap/v3" "github.com/cockroachdb/errors" - "github.com/google/uuid" "go.mondoo.com/cnquery/v11/llx" "go.mondoo.com/cnquery/v11/providers-sdk/v1/plugin" "go.mondoo.com/cnquery/v11/providers-sdk/v1/util/convert" - "go.mondoo.com/cnquery/v11/types" ) // standard nmap scan @@ -82,68 +78,6 @@ func (r *mqlNmapTarget) scan() error { return nil } -func newMqlNmapHost(runtime *plugin.Runtime, host nmap.Host) (*mqlNmapHost, error) { - distance, _ := convert.JsonToDict(host.Distance) - os, _ := convert.JsonToDict(host.OS) - trace, _ := convert.JsonToDict(host.Trace) - addresses, _ := convert.JsonToDictSlice(host.Addresses) - hostnames, _ := convert.JsonToDictSlice(host.Hostnames) - - id := uuid.New().String() - - ports := make([]interface{}, 0) - for _, port := range host.Ports { - r, err := newMqlNmapPort(runtime, id, port) - if err != nil { - return nil, err - } - ports = append(ports, r) - } - - name := "" - if len(host.Addresses) == 1 { - name = host.Addresses[0].Addr - } else { - entries := []string{} - for _, addr := range host.Addresses { - if addr.Addr != "" { - entries = append(entries, addr.Addr) - } - } - name = strings.Join(entries, ", ") - } - - mqlNmapHostResource, err := CreateResource(runtime, "nmap.host", map[string]*llx.RawData{ - "__id": llx.StringData("nmap.host/" + id), - "name": llx.StringData(name), - "distance": llx.DictData(distance), - "os": llx.DictData(os), - "endTime": llx.TimeData(time.Time(host.EndTime)), - "comment": llx.StringData(host.Comment), - "trace": llx.DictData(trace), - "addresses": llx.ArrayData(addresses, types.Dict), - "hostnames": llx.ArrayData(hostnames, types.Dict), - "ports": llx.ArrayData(ports, types.Resource("nmap.port")), - "state": llx.StringData(host.Status.State), - }) - return mqlNmapHostResource.(*mqlNmapHost), err -} - -func newMqlNmapPort(runtime *plugin.Runtime, id string, port nmap.Port) (*mqlNmapPort, error) { - - mqlPort, err := CreateResource(runtime, "nmap.port", map[string]*llx.RawData{ - "__id": llx.StringData("nmap.port/" + id + "/" + strconv.Itoa(int(port.ID))), - "port": llx.IntData(int64(port.ID)), - "service": llx.StringData(port.Service.Name), - "method": llx.StringData(port.Service.Method), - "protocol": llx.StringData(port.Protocol), - "product": llx.StringData(port.Service.Product), - "version": llx.StringData(port.Service.Version), - "state": llx.StringData(port.State.State), - }) - return mqlPort.(*mqlNmapPort), err -} - func (r *mqlNmapTarget) hosts() ([]interface{}, error) { return nil, r.scan() } diff --git a/providers/nmap/resources/nmap.lr.go b/providers/nmap/resources/nmap.lr.go index 5c690bac3c..780801f0fc 100644 --- a/providers/nmap/resources/nmap.lr.go +++ b/providers/nmap/resources/nmap.lr.go @@ -27,7 +27,7 @@ func init() { Create: createNmapTarget, }, "nmap.host": { - // to override args, implement: initNmapHost(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) + Init: initNmapHost, Create: createNmapHost, }, "nmap.port": {