diff --git a/modules/graph/create.go b/modules/graph/create.go new file mode 100644 index 00000000..f4779df8 --- /dev/null +++ b/modules/graph/create.go @@ -0,0 +1,184 @@ +package graph + +import ( + "fmt" + "github.com/bettercap/bettercap/network" +) + +func (mod *Module) createIPGraph(endpoint *network.Endpoint) (*Node, bool, error) { + node, err := mod.db.FindNode(Endpoint, endpoint.HwAddress) + isNew := node == nil + if err != nil { + return nil, false, err + } else if isNew { + if node, err = mod.db.CreateNode(Endpoint, endpoint.HwAddress, endpoint, ""); err != nil { + return nil, false, err + } + } else { + if err = mod.db.UpdateNode(node); err != nil { + return nil, false, err + } + } + + // create relations if needed + if manages, err := mod.db.FindLastRecentEdgeOfType(mod.gw, node, Manages, edgeStaleTime); err != nil { + return nil, false, err + } else if manages == nil { + if manages, err = mod.db.CreateEdge(mod.gw, node, Manages); err != nil { + return nil, false, err + } + } + + if connects_to, err := mod.db.FindLastRecentEdgeOfType(node, mod.gw, ConnectsTo, edgeStaleTime); err != nil { + return nil, false, err + } else if connects_to == nil { + if connects_to, err = mod.db.CreateEdge(node, mod.gw, ConnectsTo); err != nil { + return nil, false, err + } + } + + return node, isNew, nil +} + +func (mod *Module) createDot11ApGraph(ap *network.AccessPoint) (*Node, bool, error) { + node, err := mod.db.FindNode(AccessPoint, ap.HwAddress) + isNew := node == nil + if err != nil { + return nil, false, err + } else if isNew { + if node, err = mod.db.CreateNode(AccessPoint, ap.HwAddress, ap, ""); err != nil { + return nil, false, err + } + } else if err = mod.db.UpdateNode(node); err != nil { + return nil, false, err + } + return node, isNew, nil +} + +func (mod *Module) createDot11SSIDGraph(hex string, ssid string) (*Node, bool, error) { + node, err := mod.db.FindNode(SSID, hex) + isNew := node == nil + if err != nil { + return nil, false, err + } else if isNew { + if node, err = mod.db.CreateNode(SSID, hex, ssid, ""); err != nil { + return nil, false, err + } + } else if err = mod.db.UpdateNode(node); err != nil { + return nil, false, err + } + return node, isNew, nil +} + +func (mod *Module) createDot11StaGraph(station *network.Station) (*Node, bool, error) { + node, err := mod.db.FindNode(Station, station.HwAddress) + isNew := node == nil + if err != nil { + return nil, false, err + } else if isNew { + if node, err = mod.db.CreateNode(Station, station.HwAddress, station, ""); err != nil { + return nil, false, err + } + } else if err = mod.db.UpdateNode(node); err != nil { + return nil, false, err + } + return node, isNew, nil +} + +func (mod *Module) createDot11Graph(ap *network.AccessPoint, station *network.Station) (*Node, bool, *Node, bool, error) { + apNode, apIsNew, err := mod.createDot11ApGraph(ap) + if err != nil { + return nil, false, nil, false, err + } + + staNode, staIsNew, err := mod.createDot11StaGraph(station) + if err != nil { + return nil, false, nil, false, err + } + + // create relations if needed + if manages, err := mod.db.FindLastRecentEdgeOfType(apNode, staNode, Manages, edgeStaleTime); err != nil { + return nil, false, nil, false, err + } else if manages == nil { + if manages, err = mod.db.CreateEdge(apNode, staNode, Manages); err != nil { + return nil, false, nil, false, err + } + } + + if connects_to, err := mod.db.FindLastRecentEdgeOfType(staNode, apNode, ConnectsTo, edgeStaleTime); err != nil { + return nil, false, nil, false, err + } else if connects_to == nil { + if connects_to, err = mod.db.CreateEdge(staNode, apNode, ConnectsTo); err != nil { + return nil, false, nil, false, err + } + } + + return apNode, apIsNew, staNode, staIsNew, nil +} + +func (mod *Module) createDot11ProbeGraph(ssid string, station *network.Station) (*Node, bool, *Node, bool, error) { + ssidNode, ssidIsNew, err := mod.createDot11SSIDGraph(fmt.Sprintf("%x", ssid), ssid) + if err != nil { + return nil, false, nil, false, err + } + + staNode, staIsNew, err := mod.createDot11StaGraph(station) + if err != nil { + return nil, false, nil, false, err + } + + // create relations if needed + if probes_for, err := mod.db.FindLastRecentEdgeOfType(staNode, ssidNode, ProbesFor, edgeStaleTime); err != nil { + return nil, false, nil, false, err + } else if probes_for == nil { + if probes_for, err = mod.db.CreateEdge(staNode, ssidNode, ProbesFor); err != nil { + return nil, false, nil, false, err + } + } + + if probed_by, err := mod.db.FindLastRecentEdgeOfType(ssidNode, staNode, ProbedBy, edgeStaleTime); err != nil { + return nil, false, nil, false, err + } else if probed_by == nil { + if probed_by, err = mod.db.CreateEdge(ssidNode, staNode, ProbedBy); err != nil { + return nil, false, nil, false, err + } + } + + return ssidNode, ssidIsNew, staNode, staIsNew, nil +} + +func (mod *Module) createBLEServerGraph(dev *network.BLEDevice) (*Node, bool, error) { + mac := network.NormalizeMac(dev.Device.ID()) + node, err := mod.db.FindNode(BLEServer, mac) + isNew := node == nil + if err != nil { + return nil, false, err + } else if isNew { + if node, err = mod.db.CreateNode(BLEServer, mac, dev, ""); err != nil { + return nil, false, err + } + } else if err = mod.db.UpdateNode(node); err != nil { + return nil, false, err + } + return node, isNew, nil +} + +func (mod *Module) connectAsSame(a, b *Node) error { + if aIsB, err := mod.db.FindLastEdgeOfType(a, b, Is); err != nil { + return err + } else if aIsB == nil { + if aIsB, err = mod.db.CreateEdge(a, b, Is); err != nil { + return err + } + } + + if bIsA, err := mod.db.FindLastEdgeOfType(b, a, Is); err != nil { + return err + } else if bIsA == nil { + if bIsA, err = mod.db.CreateEdge(b, a, Is); err != nil { + return err + } + } + + return nil +} diff --git a/modules/graph/edge.go b/modules/graph/edge.go new file mode 100644 index 00000000..4c6eff93 --- /dev/null +++ b/modules/graph/edge.go @@ -0,0 +1,42 @@ +package graph + +import ( + "fmt" + "github.com/bettercap/bettercap/session" + "time" +) + +type EdgeType string + +const ( + Is EdgeType = "is" + ProbesFor EdgeType = "probes_for" + ProbedBy EdgeType = "probed_by" + ConnectsTo EdgeType = "connects_to" + Manages EdgeType = "manages" +) + +type EdgeEvent struct { + Left *Node + Edge *Edge + Right *Node +} + +type Edge struct { + Type EdgeType `json:"type"` + CreatedAt time.Time `json:"created_at"` + Position *session.GPS `json:"position,omitempty"` +} + +func (e Edge) Dot(left, right *Node, width float64) string { + edgeLen := 1.0 + if e.Type == Is { + edgeLen = 0.3 + } + return fmt.Sprintf("\"%s\" -> \"%s\" [label=\"%s\", len=%.2f, penwidth=%.2f];", + left.String(), + right.String(), + e.Type, + edgeLen, + width) +} diff --git a/modules/graph/edges.go b/modules/graph/edges.go new file mode 100644 index 00000000..eae8ac02 --- /dev/null +++ b/modules/graph/edges.go @@ -0,0 +1,158 @@ +package graph + +import ( + "encoding/json" + "github.com/evilsocket/islazy/fs" + "io/ioutil" + "os" + "path" + "sort" + "sync" + "time" +) + +const edgesIndexName = "edges.json" + +type EdgesTo map[string][]Edge + +type EdgesCallback func(string, []Edge, string) error + +type Edges struct { + sync.RWMutex + timestamp time.Time + fileName string + size int + from map[string]EdgesTo +} + +type edgesJSON struct { + Timestamp time.Time `json:"timestamp"` + Size int `json:"size"` + Edges map[string]EdgesTo `json:"edges"` +} + +func LoadEdges(basePath string) (*Edges, error) { + edges := Edges{ + fileName: path.Join(basePath, edgesIndexName), + from: make(map[string]EdgesTo), + } + + if fs.Exists(edges.fileName) { + var js edgesJSON + + if raw, err := ioutil.ReadFile(edges.fileName); err != nil { + return nil, err + } else if err = json.Unmarshal(raw, &js); err != nil { + return nil, err + } + + edges.timestamp = js.Timestamp + edges.from = js.Edges + edges.size = js.Size + } + + return &edges, nil +} + +func (e *Edges) flush() error { + e.timestamp = time.Now() + js := edgesJSON{ + Timestamp: e.timestamp, + Size: e.size, + Edges: e.from, + } + + if raw, err := json.Marshal(js); err != nil { + return err + } else if err = ioutil.WriteFile(e.fileName, raw, os.ModePerm); err != nil { + return err + } + + return nil +} + +func (e *Edges) Flush() error { + e.RLock() + defer e.RUnlock() + return e.flush() +} + +func (e *Edges) ForEachEdge(cb EdgesCallback) error { + e.RLock() + defer e.RUnlock() + + for from, edgesTo := range e.from { + for to, edges := range edgesTo { + if err := cb(from, edges, to); err != nil { + return err + } + } + } + + return nil +} + +func (e *Edges) ForEachEdgeFrom(nodeID string, cb EdgesCallback) error { + e.RLock() + defer e.RUnlock() + + if edgesTo, found := e.from[nodeID]; found { + for to, edges := range edgesTo { + if err := cb(nodeID, edges, to); err != nil { + return err + } + } + } + + return nil +} + +func (e *Edges) IsConnected(nodeID string) bool { + e.RLock() + defer e.RUnlock() + + if edgesTo, found := e.from[nodeID]; found { + return len(edgesTo) > 0 + } + + return false +} + +func (e *Edges) FindEdges(fromID, toID string, doSort bool) []Edge { + e.RLock() + defer e.RUnlock() + + if edgesTo, foundFrom := e.from[fromID]; foundFrom { + if edges, foundTo := edgesTo[toID]; foundTo { + if doSort { + // sort edges from oldest to newer + sort.Slice(edges, func(i, j int) bool { + return edges[i].CreatedAt.Before(edges[j].CreatedAt) + }) + } + return edges + } + } + + return nil +} + +func (e *Edges) Connect(fromID, toID string, edge Edge) error { + e.Lock() + defer e.Unlock() + + if edgesTo, foundFrom := e.from[fromID]; foundFrom { + edges := edgesTo[toID] + edges = append(edges, edge) + e.from[fromID][toID] = edges + } else { + // create the entire path + e.from[fromID] = EdgesTo{ + toID: {edge}, + } + } + + e.size++ + + return e.flush() +} diff --git a/modules/graph/graph.go b/modules/graph/graph.go new file mode 100644 index 00000000..1a32e017 --- /dev/null +++ b/modules/graph/graph.go @@ -0,0 +1,425 @@ +package graph + +import ( + "encoding/json" + "fmt" + "github.com/bettercap/bettercap/session" + "github.com/evilsocket/islazy/fs" + "path" + "sync" + "time" +) + +var Loaded = (* Graph)(nil) + +type NodeCallback func(*Node) +type EdgeCallback func(*Node, []Edge, *Node) + +type Graph struct { + sync.Mutex + + path string + edges *Edges +} + +func NewGraph(path string) (*Graph, error) { + if edges, err := LoadEdges(path); err != nil { + return nil, err + } else { + Loaded = &Graph{ + path: path, + edges: edges, + } + return Loaded, nil + } +} + +func (g *Graph) EachNode(cb NodeCallback) error { + g.Lock() + defer g.Unlock() + + for _, nodeType := range NodeTypes { + err := fs.Glob(g.path, fmt.Sprintf("%s_*.json", nodeType), func(fileName string) error { + if node, err := ReadNode(fileName); err != nil { + return err + } else { + cb(node) + } + return nil + }) + if err != nil { + return err + } + } + return nil +} + +func (g *Graph) EachEdge(cb EdgeCallback) error { + g.Lock() + defer g.Unlock() + + return g.edges.ForEachEdge(func(fromID string, edges []Edge, toID string) error { + var left, right *Node + var err error + + leftFileName := path.Join(g.path, fromID+".json") + rightFileName := path.Join(g.path, toID+".json") + + if left, err = ReadNode(leftFileName); err != nil { + return err + } else if right, err = ReadNode(rightFileName); err != nil { + return err + } + + cb(left, edges, right) + + return nil + }) +} + +func (g *Graph) Traverse(root string, onNode NodeCallback, onEdge EdgeCallback) error { + if root == "" { + // traverse the entire graph + if err := g.EachNode(onNode); err != nil { + return err + } else if err = g.EachEdge(onEdge); err != nil { + return err + } + } else { + // start by a specific node + roots, err := g.FindOtherTypes("", root) + if err != nil { + return err + } + + stack := NewStack() + for _, root := range roots { + stack.Push(root) + } + + type edgeBucket struct { + left *Node + edges []Edge + right *Node + } + + allEdges := make([]edgeBucket, 0) + visited := make(map[string]bool) + + for { + if last := stack.Pop(); last == nil { + break + } else { + node := last.(*Node) + nodeID := node.String() + if _, found := visited[nodeID]; found { + continue + } else { + visited[nodeID] = true + } + + onNode(node) + + // collect all edges starting from this node + err = g.edges.ForEachEdgeFrom(nodeID, func(_ string, edges []Edge, toID string) error { + rightFileName := path.Join(g.path, toID+".json") + if right, err := ReadNode(rightFileName); err != nil { + return err + } else { + // collect new node + if _, found := visited[toID]; !found { + stack.Push(right) + } + // collect all edges, we'll emit this later + allEdges = append(allEdges, edgeBucket{ + left: node, + edges: edges, + right: right, + }) + } + return nil + }) + } + } + + for _, edge := range allEdges { + onEdge(edge.left, edge.edges, edge.right) + } + } + + return nil +} + +func (g *Graph) IsConnected(nodeType string, nodeID string) bool { + return g.edges.IsConnected(fmt.Sprintf("%s_%s", nodeType, nodeID)) +} + +func (g *Graph) Dot(filter, layout, name string, disconnected bool) (string, int, int, error) { + size := 0 + discarded := 0 + + data := fmt.Sprintf("digraph %s {\n", name) + data += fmt.Sprintf(" layout=%s\n", layout) + + typeMap := make(map[NodeType]bool) + + type typeCount struct { + edge Edge + count int + } + + if err := g.Traverse(filter, func(node *Node) { + include := false + if disconnected { + include = true + } else { + include = g.edges.IsConnected(node.String()) + } + + if include { + size++ + typeMap[node.Type] = true + data += fmt.Sprintf(" %s\n", node.Dot(filter == node.ID)) + } else { + discarded++ + } + }, func(left *Node, edges []Edge, right *Node) { + // collect counters by edge type in order to calculate proportional widths + byType := make(map[string]typeCount) + tot := len(edges) + + for _, edge := range edges { + if c, found := byType[string(edge.Type)]; found { + c.count++ + } else { + byType[string(edge.Type)] = typeCount{ + edge: edge, + count: 1, + } + } + } + + max := 2.0 + for _, c := range byType { + w := max * float64(c.count/tot) + if w < 0.5 { + w = 0.5 + } + data += fmt.Sprintf(" %s\n", c.edge.Dot(left, right, w)) + } + }); err != nil { + return "", 0, 0, err + } + + /* + data += "\n" + data += "node [style=filled height=0.55 fontname=\"Verdana\" fontsize=10];\n" + data += "subgraph legend {\n" + + "graph[style=dotted];\n" + + "label = \"Legend\";\n" + + var types []NodeType + for nodeType, _ := range typeMap { + types = append(types, nodeType) + node := Node{ + Type: nodeType, + Annotations: nodeTypeDescs[nodeType], + Dummy: true, + } + data += fmt.Sprintf(" %s\n", node.Dot(false)) + } + + ntypes := len(types) + for i := 0; i < ntypes - 1; i++ { + data += fmt.Sprintf(" \"%s\" -> \"%s\" [style=invis];\n", types[i], types[i + 1]) + } + data += "}\n" + */ + + data += "\n" + data += " overlap=false\n" + data += "}" + + return data, size, discarded, nil +} + +func (g *Graph) JSON(filter string, disconnected bool) (string, int, int, error) { + size := 0 + discarded := 0 + + type link struct { + Source string `json:"source"` + Target string `json:"target"` + Edge interface{} `json:"edge"` + } + + type data struct { + Nodes []map[string]interface{} `json:"nodes"` + Links []link `json:"links"` + } + + jsData := data{ + Nodes: make([]map[string]interface{}, 0), + Links: make([]link, 0), + } + + if err := g.Traverse(filter, func(node *Node) { + include := false + if disconnected { + include = true + } else { + include = g.edges.IsConnected(node.String()) + } + + if include { + size++ + + if nm, err := node.ToMap(); err != nil { + panic(err) + } else { + // patch id + nm["id"] = node.String() + jsData.Nodes = append(jsData.Nodes, nm) + } + } else { + discarded++ + } + }, func(left *Node, edges []Edge, right *Node) { + for _, edge := range edges { + jsData.Links = append(jsData.Links, link{ + Source: left.String(), + Target: right.String(), + Edge: edge, + }) + } + }); err != nil { + return "", 0, 0, err + } + + if raw, err := json.Marshal(jsData); err != nil { + return "", 0, 0, err + } else { + return string(raw), size, discarded, nil + } +} + +func (g *Graph) FindNode(t NodeType, id string) (*Node, error) { + g.Lock() + defer g.Unlock() + + nodeFileName := path.Join(g.path, fmt.Sprintf("%s_%s.json", t, id)) + if fs.Exists(nodeFileName) { + return ReadNode(nodeFileName) + } + + return nil, nil +} + +func (g *Graph) FindOtherTypes(t NodeType, id string) ([]*Node, error) { + g.Lock() + defer g.Unlock() + + var otherNodes []*Node + + for _, otherType := range NodeTypes { + if otherType != t { + if nodeFileName := path.Join(g.path, fmt.Sprintf("%s_%s.json", otherType, id)); fs.Exists(nodeFileName) { + if node, err := ReadNode(nodeFileName); err != nil { + return nil, err + } else { + otherNodes = append(otherNodes, node) + } + } + } + } + + return otherNodes, nil +} + +func (g *Graph) CreateNode(t NodeType, id string, entity interface{}, annotations string) (*Node, error) { + g.Lock() + defer g.Unlock() + + node := &Node{ + Type: t, + ID: id, + Entity: entity, + Annotations: annotations, + } + + nodeFileName := path.Join(g.path, fmt.Sprintf("%s.json", node.String())) + if err := CreateNode(nodeFileName, node); err != nil { + return nil, err + } + + session.I.Events.Add("graph.node.new", node) + + return node, nil +} + +func (g *Graph) UpdateNode(node *Node) error { + g.Lock() + defer g.Unlock() + + nodeFileName := path.Join(g.path, fmt.Sprintf("%s.json", node.String())) + if err := UpdateNode(nodeFileName, node); err != nil { + return err + } + + return nil +} + +func (g *Graph) FindLastEdgeOfType(from, to *Node, edgeType EdgeType) (*Edge, error) { + edges := g.edges.FindEdges(from.String(), to.String(), true) + num := len(edges) + for i := range edges { + // loop backwards + idx := num - 1 - i + edge := edges[idx] + if edge.Type == edgeType { + return &edge, nil + } + } + return nil, nil +} + +func (g *Graph) FindLastRecentEdgeOfType(from, to *Node, edgeType EdgeType, staleTime time.Duration) (*Edge, error) { + edges := g.edges.FindEdges(from.String(), to.String(), true) + num := len(edges) + for i := range edges { + // loop backwards + idx := num - 1 - i + edge := edges[idx] + if edge.Type == edgeType { + if time.Since(edge.CreatedAt) >= staleTime { + return nil, nil + } + return &edge, nil + } + } + + return nil, nil +} + +func (g *Graph) CreateEdge(from, to *Node, edgeType EdgeType) (*Edge, error) { + edge := Edge{ + Type: edgeType, + CreatedAt: time.Now(), + } + + if session.I.GPS.Updated.IsZero() == false { + edge.Position = &session.I.GPS + } + + if err := g.edges.Connect(from.String(), to.String(), edge); err != nil { + return nil, err + } + + session.I.Events.Add("graph.edge.new", EdgeEvent{ + Left: from, + Edge: &edge, + Right: to, + }) + + return &edge, nil +} diff --git a/modules/graph/js_builtin.go b/modules/graph/js_builtin.go new file mode 100644 index 00000000..49252d47 --- /dev/null +++ b/modules/graph/js_builtin.go @@ -0,0 +1,15 @@ +package graph + +import ( + "github.com/bettercap/bettercap/log" +) + +type graphPackage struct{} + +func (g graphPackage) IsConnected(nodeType, nodeID string) bool { + if Loaded == nil { + log.Error("graph.IsConnected: graph not loaded") + return false + } + return Loaded.IsConnected(nodeType, nodeID) +} diff --git a/modules/graph/module.go b/modules/graph/module.go new file mode 100644 index 00000000..b5e36663 --- /dev/null +++ b/modules/graph/module.go @@ -0,0 +1,349 @@ +package graph + +import ( + "os" + "path/filepath" + "regexp" + "sync" + "time" + + "github.com/bettercap/bettercap/caplets" + "github.com/bettercap/bettercap/modules/wifi" + "github.com/bettercap/bettercap/network" + "github.com/bettercap/bettercap/session" + "github.com/evilsocket/islazy/fs" + "github.com/evilsocket/islazy/plugin" + "github.com/evilsocket/islazy/str" +) + +const ( + ifaceAnnotation = "" + edgeStaleTime = time.Hour * 24 +) + +var privacyFilter = regexp.MustCompile("(?i)([a-f0-9]{2}):([a-f0-9]{2}):([a-f0-9]{2}):([a-f0-9]{2}):([a-f0-9]{2}):([a-f0-9]{2})") + +type dotSettings struct { + layout string + name string + output string +} + +type jsonSettings struct { + output string +} + +type settings struct { + path string + dot dotSettings + json jsonSettings + disconnected bool + privacy bool +} + +type Module struct { + session.SessionModule + + settings settings + db *Graph + gw *Node + iface *Node + eventBus session.EventBus + wLock sync.Mutex +} + +func init() { + plugin.Defines["graph"] = graphPackage{} +} + +func NewModule(s *session.Session) *Module { + mod := &Module{ + SessionModule: session.NewSessionModule("graph", s), + settings: settings{ + path: filepath.Join(caplets.InstallBase, "graph"), + dot: dotSettings{ + layout: "neato", + name: "bettergraph", + output: "bettergraph.dot", + }, + json: jsonSettings{ + output: "bettergraph.json", + }, + }, + } + + mod.AddParam(session.NewStringParameter("graph.path", + mod.settings.path, + "", + "Base path for the graph database.")) + + mod.AddParam(session.NewStringParameter("graph.dot.name", + mod.settings.dot.name, + "", + "Graph name in the dot output.")) + + mod.AddParam(session.NewStringParameter("graph.dot.layout", + mod.settings.dot.layout, + "", + "Layout for dot output.")) + + mod.AddParam(session.NewStringParameter("graph.dot.output", + mod.settings.dot.output, + "", + "File name for dot output.")) + + mod.AddParam(session.NewStringParameter("graph.json.output", + mod.settings.json.output, + "", + "File name for JSON output.")) + + mod.AddParam(session.NewBoolParameter("graph.disconnected", + "false", + "Include disconnected edges in then output graph.")) + + mod.AddParam(session.NewBoolParameter("graph.privacy", + "false", + "Obfuscate mac addresses.")) + + mod.AddHandler(session.NewModuleHandler("graph on", "", + "Start the Module module.", + func(args []string) error { + return mod.Start() + })) + + mod.AddHandler(session.NewModuleHandler("graph off", "", + "Stop the Module module.", + func(args []string) error { + return mod.Stop() + })) + + mod.AddHandler(session.NewModuleHandler("graph.to_dot MAC?", + `graph\.to_dot\s*([^\s]*)`, + "Generate a dot graph file from the current graph.", + func(args []string) (err error) { + bssid := "" + if len(args) == 1 && args[0] != "" { + bssid = network.NormalizeMac(str.Trim(args[0])) + } + return mod.generateDotGraph(bssid) + })) + + mod.AddHandler(session.NewModuleHandler("graph.to_json MAC?", + `graph\.to_json\s*([^\s]*)`, + "Generate a JSON graph file from the current graph.", + func(args []string) (err error) { + bssid := "" + if len(args) == 1 && args[0] != "" { + bssid = network.NormalizeMac(str.Trim(args[0])) + } + return mod.generateJSONGraph(bssid) + })) + + return mod +} + +func (mod *Module) Name() string { + return "graph" +} + +func (mod *Module) Description() string { + return "A module to build a graph of WiFi and LAN nodes." +} + +func (mod *Module) Author() string { + return "Simone Margaritelli " +} + +func (mod *Module) updateSettings() error { + var err error + + if err, mod.settings.dot.name = mod.StringParam("graph.dot.name"); err != nil { + return err + } else if err, mod.settings.dot.layout = mod.StringParam("graph.dot.layout"); err != nil { + return err + } else if err, mod.settings.dot.output = mod.StringParam("graph.dot.output"); err != nil { + return err + } else if err, mod.settings.json.output = mod.StringParam("graph.json.output"); err != nil { + return err + } else if err, mod.settings.disconnected = mod.BoolParam("graph.disconnected"); err != nil { + return err + } else if err, mod.settings.privacy = mod.BoolParam("graph.privacy"); err != nil { + return err + } else if err, mod.settings.path = mod.StringParam("graph.path"); err != nil { + return err + } else if mod.settings.path, err = filepath.Abs(mod.settings.path); err != nil { + return err + } else if !fs.Exists(mod.settings.path) { + if err = os.MkdirAll(mod.settings.path, os.ModePerm); err != nil { + return err + } + } + + // only reload if needed + if mod.db != nil && mod.db.path != mod.settings.path { + mod.db = nil + } + + if mod.db == nil { + if mod.db, err = NewGraph(mod.settings.path); err != nil { + return err + } + } + + return nil +} + +func (mod *Module) Configure() (err error) { + if mod.Running() { + return session.ErrAlreadyStarted(mod.Name()) + } else if err = mod.updateSettings(); err != nil { + return err + } + + // if have an IP + if mod.Session.Gateway != nil && mod.Session.Interface != nil { + // find or create interface node + iface := mod.Session.Interface + if mod.iface, err = mod.db.FindNode(Endpoint, iface.HwAddress); err != nil { + return err + } else if mod.iface == nil { + // create the interface node + if mod.iface, err = mod.db.CreateNode(Endpoint, iface.HwAddress, iface, ifaceAnnotation); err != nil { + return err + } + } else if err = mod.db.UpdateNode(mod.iface); err != nil { + return err + } + + // find or create gateway node + gw := mod.Session.Gateway + if mod.gw, err = mod.db.FindNode(Gateway, gw.HwAddress); err != nil { + return err + } else if mod.gw == nil { + if mod.gw, err = mod.db.CreateNode(Gateway, gw.HwAddress, gw, ""); err != nil { + return err + } + } else { + if err = mod.db.UpdateNode(mod.gw); err != nil { + return err + } + } + + // create relations if needed + if iface.HwAddress == gw.HwAddress { + if err = mod.connectAsSame(mod.gw, mod.iface); err != nil { + return err + } + } else { + if manages, err := mod.db.FindLastRecentEdgeOfType(mod.gw, mod.iface, Manages, edgeStaleTime); err != nil { + return err + } else if manages == nil { + if manages, err = mod.db.CreateEdge(mod.gw, mod.iface, Manages); err != nil { + return err + } + } + + if connects_to, err := mod.db.FindLastEdgeOfType(mod.iface, mod.gw, ConnectsTo); err != nil { + return err + } else if connects_to == nil { + if connects_to, err = mod.db.CreateEdge(mod.iface, mod.gw, ConnectsTo); err != nil { + return err + } + } + } + } + + mod.eventBus = mod.Session.Events.Listen() + + return nil +} + +func (mod *Module) onEvent(e session.Event) { + var entities []*Node + + if e.Tag == "endpoint.new" { + endpoint := e.Data.(*network.Endpoint) + if entity, _, err := mod.createIPGraph(endpoint); err != nil { + mod.Error("%s", err) + } else { + entities = append(entities, entity) + } + } else if e.Tag == "wifi.ap.new" { + ap := e.Data.(*network.AccessPoint) + if entity, _, err := mod.createDot11ApGraph(ap); err != nil { + mod.Error("%s", err) + } else { + entities = append(entities, entity) + } + } else if e.Tag == "wifi.client.new" { + ce := e.Data.(wifi.ClientEvent) + if apEntity, _, staEntity, _, err := mod.createDot11Graph(ce.AP, ce.Client); err != nil { + mod.Error("%s", err) + } else { + entities = append(entities, apEntity) + entities = append(entities, staEntity) + } + } else if e.Tag == "wifi.client.probe" { + probe := e.Data.(wifi.ProbeEvent) + station := network.Station{ + RSSI: probe.RSSI, + Endpoint: &network.Endpoint{ + HwAddress: probe.FromAddr, + Vendor: probe.FromVendor, + Alias: probe.FromAlias, + }, + } + + if _, _, staEntity, _, err := mod.createDot11ProbeGraph(probe.SSID, &station); err != nil { + mod.Error("%s", err) + } else { + // don't add fake ap to entities, no need to correlate + entities = append(entities, staEntity) + } + } else if e.Tag == "ble.device.new" { + // surprisingly some devices, like DLink IPCams have BLE, Dot11 and LAN hardware address in common + dev := e.Data.(*network.BLEDevice) + if entity, _, err := mod.createBLEServerGraph(dev); err != nil { + mod.Error("%s", err) + } else { + entities = append(entities, entity) + } + } + + // if there's at least an entity node, search for other nodes with the + // same mac address but different type and connect them as needed + for _, entity := range entities { + if others, err := mod.db.FindOtherTypes(entity.Type, entity.ID); err != nil { + mod.Error("%s", err) + } else if len(others) > 0 { + for _, other := range others { + if err = mod.connectAsSame(entity, other); err != nil { + mod.Error("%s", err) + } + } + } + } +} + +func (mod *Module) Start() error { + if err := mod.Configure(); err != nil { + return err + } + + return mod.SetRunning(true, func() { + mod.Info("started with database @ %s", mod.settings.path) + + for mod.Running() { + select { + case e := <-mod.eventBus: + mod.onEvent(e) + } + } + }) +} + +func (mod *Module) Stop() error { + return mod.SetRunning(false, func() { + mod.Session.Events.Unlisten(mod.eventBus) + }) +} diff --git a/modules/graph/node.go b/modules/graph/node.go new file mode 100644 index 00000000..9f60b1cd --- /dev/null +++ b/modules/graph/node.go @@ -0,0 +1,170 @@ +package graph + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "strings" + "time" + "unicode" +) + +type NodeType string + +const ( + SSID NodeType = "ssid" + BLEServer NodeType = "ble_server" + Station NodeType = "station" + AccessPoint NodeType = "access_point" + Endpoint NodeType = "endpoint" + Gateway NodeType = "gateway" +) + +var NodeTypes = []NodeType{ + SSID, + Station, + AccessPoint, + Endpoint, + Gateway, + BLEServer, +} + +var nodeTypeDescs = map[NodeType]string{ + SSID: "WiFI SSID probe", + BLEServer: "BLE Device", + Station: "WiFi Client", + AccessPoint: "WiFi AP", + Endpoint: "IP Client", + Gateway: "IP Gateway", +} + +var nodeDotStyles = map[NodeType]string{ + SSID: "shape=circle style=filled color=lightgray fillcolor=lightgray fixedsize=true penwidth=0.5", + BLEServer: "shape=box style=filled color=dodgerblue3", + Endpoint: "shape=box style=filled color=azure2", + Gateway: "shape=diamond style=filled color=azure4", + Station: "shape=box style=filled color=gold", + AccessPoint: "shape=diamond style=filled color=goldenrod3", +} + +type Node struct { + Type NodeType `json:"type"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + ID string `json:"id"` + Annotations string `json:"annotations"` + Entity interface{} `json:"entity"` + Dummy bool `json:"-"` +} + +func ReadNode(fileName string) (*Node, error) { + var node Node + if raw, err := ioutil.ReadFile(fileName); err != nil { + return nil, fmt.Errorf("error while reading %s: %v", fileName, err) + } else if err = json.Unmarshal(raw, &node); err != nil { + return nil, fmt.Errorf("error while decoding %s: %v", fileName, err) + } + return &node, nil +} + +func WriteNode(fileName string, node *Node, update bool) error { + if update { + node.UpdatedAt = time.Now() + } else { + node.CreatedAt = time.Now() + } + + if raw, err := json.Marshal(node); err != nil { + return fmt.Errorf("error creating data for %s: %v", fileName, err) + } else if err = ioutil.WriteFile(fileName, raw, os.ModePerm); err != nil { + return fmt.Errorf("error creating %s: %v", fileName, err) + } + return nil +} + +func CreateNode(fileName string, node *Node) error { + return WriteNode(fileName, node, false) +} + +func UpdateNode(fileName string, node *Node) error { + return WriteNode(fileName, node, true) +} + +func (n Node) String() string { + if n.Dummy == false { + return fmt.Sprintf("%s_%s", n.Type, n.ID) + } + return string(n.Type) +} + +func (n Node) Label() string { + if n.Dummy { + return n.Annotations + } + + switch n.Type { + case SSID: + s := n.Entity.(string) + allPrint := true + + for _, rn := range s { + if !unicode.IsPrint(rune(rn)) { + allPrint = false + break + } + } + + if !allPrint { + s = fmt.Sprintf("0x%x", s) + } + return s + case BLEServer: + return fmt.Sprintf("%s\\n(%s)", + n.Entity.(map[string]interface{})["mac"].(string), + n.Entity.(map[string]interface{})["vendor"].(string)) + case Station: + return fmt.Sprintf("%s\\n(%s)", + n.Entity.(map[string]interface{})["mac"].(string), + n.Entity.(map[string]interface{})["vendor"].(string)) + case AccessPoint: + return fmt.Sprintf("%s\\n%s\\n(%s)", + n.Entity.(map[string]interface{})["hostname"].(string), + n.Entity.(map[string]interface{})["mac"].(string), + n.Entity.(map[string]interface{})["vendor"].(string)) + case Endpoint: + return fmt.Sprintf("%s\\n(%s %s)", + n.Entity.(map[string]interface{})["ipv4"].(string), + n.Entity.(map[string]interface{})["mac"].(string), + n.Entity.(map[string]interface{})["vendor"].(string)) + case Gateway: + return fmt.Sprintf("%s\\n(%s %s)", + n.Entity.(map[string]interface{})["ipv4"].(string), + n.Entity.(map[string]interface{})["mac"].(string), + n.Entity.(map[string]interface{})["vendor"].(string)) + } + return "?" +} + +func (n Node) Dot(isTarget bool) string { + style := nodeDotStyles[n.Type] + if isTarget { + style += ", color=red" + } + return fmt.Sprintf("\"%s\" [%s, label=\"%s\"];", + n.String(), + style, + strings.ReplaceAll(n.Label(), "\"", "\\\"")) +} + +func (n Node) ToMap() (map[string]interface{}, error) { + var m map[string]interface{} + + if raw, err := json.Marshal(n); err != nil { + return nil, err + } else if err = json.Unmarshal(raw, &m); err != nil { + return nil, err + } + + return m, nil +} diff --git a/modules/graph/stack.go b/modules/graph/stack.go new file mode 100644 index 00000000..cf22db2f --- /dev/null +++ b/modules/graph/stack.go @@ -0,0 +1,47 @@ +package graph + +import "sync" + +type entry struct { + data interface{} + next *entry +} + +type Stack struct { + lock *sync.Mutex + head *entry + Size int +} + +func (stk *Stack) Push(data interface{}) { + stk.lock.Lock() + + element := new(entry) + element.data = data + temp := stk.head + element.next = temp + stk.head = element + stk.Size++ + + stk.lock.Unlock() +} + +func (stk *Stack) Pop() interface{} { + if stk.head == nil { + return nil + } + stk.lock.Lock() + r := stk.head.data + stk.head = stk.head.next + stk.Size-- + + stk.lock.Unlock() + + return r +} + +func NewStack() *Stack { + stk := new(Stack) + stk.lock = &sync.Mutex{} + return stk +} diff --git a/modules/graph/to_dot.go b/modules/graph/to_dot.go new file mode 100644 index 00000000..e6f2b491 --- /dev/null +++ b/modules/graph/to_dot.go @@ -0,0 +1,45 @@ +package graph + +import ( + "io/ioutil" + "os" + "time" +) + +func (mod *Module) generateDotGraph(bssid string) error { + mod.wLock.Lock() + defer mod.wLock.Unlock() + + start := time.Now() + if err := mod.updateSettings(); err != nil { + return err + } + + data, size, discarded, err := mod.db.Dot(bssid, + mod.settings.dot.layout, + mod.settings.dot.name, + mod.settings.disconnected) + if err != nil { + return err + } + + if size > 0 { + if mod.settings.privacy { + data = privacyFilter.ReplaceAllString(data, "$1:$2:xx:xx:xx:xx") + } + + if err := ioutil.WriteFile(mod.settings.dot.output, []byte(data), os.ModePerm); err != nil { + return err + } else { + mod.Info("graph saved to %s in %v (%d edges, %d discarded)", + mod.settings.dot.output, + time.Since(start), + size, + discarded) + } + } else { + mod.Info("graph is empty") + } + + return nil +} \ No newline at end of file diff --git a/modules/graph/to_json.go b/modules/graph/to_json.go new file mode 100644 index 00000000..dccd725a --- /dev/null +++ b/modules/graph/to_json.go @@ -0,0 +1,43 @@ +package graph + +import ( + "io/ioutil" + "os" + "time" +) + +func (mod *Module) generateJSONGraph(bssid string) error { + mod.wLock.Lock() + defer mod.wLock.Unlock() + + start := time.Now() + if err := mod.updateSettings(); err != nil { + return err + } + + data, size, discarded, err := mod.db.JSON(bssid, mod.settings.disconnected) + if err != nil { + return err + } + + if size > 0 { + + if mod.settings.privacy { + data = privacyFilter.ReplaceAllString(data, "$1:$2:xx:xx:xx:xx") + } + + if err := ioutil.WriteFile(mod.settings.json.output, []byte(data), os.ModePerm); err != nil { + return err + } else { + mod.Info("graph saved to %s in %v (%d edges, %d discarded)", + mod.settings.json.output, + time.Since(start), + size, + discarded) + } + } else { + mod.Info("graph is empty") + } + + return nil +} \ No newline at end of file diff --git a/modules/modules.go b/modules/modules.go index 78d62e92..0578b07f 100644 --- a/modules/modules.go +++ b/modules/modules.go @@ -11,6 +11,7 @@ import ( "github.com/bettercap/bettercap/modules/dns_spoof" "github.com/bettercap/bettercap/modules/events_stream" "github.com/bettercap/bettercap/modules/gps" + "github.com/bettercap/bettercap/modules/graph" "github.com/bettercap/bettercap/modules/hid" "github.com/bettercap/bettercap/modules/http_proxy" "github.com/bettercap/bettercap/modules/http_server" @@ -45,6 +46,7 @@ func LoadModules(sess *session.Session) { sess.Register(dns_spoof.NewDNSSpoofer(sess)) sess.Register(events_stream.NewEventsStream(sess)) sess.Register(gps.NewGPS(sess)) + sess.Register(graph.NewModule(sess)) sess.Register(http_proxy.NewHttpProxy(sess)) sess.Register(http_server.NewHttpServer(sess)) sess.Register(https_proxy.NewHttpsProxy(sess))