mirror of
https://github.com/bettercap/bettercap
synced 2025-07-07 05:22:04 -07:00
new: pushed new graph experimental module
This commit is contained in:
parent
ca2e257fbb
commit
826f13e47a
11 changed files with 1480 additions and 0 deletions
184
modules/graph/create.go
Normal file
184
modules/graph/create.go
Normal file
|
@ -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
|
||||||
|
}
|
42
modules/graph/edge.go
Normal file
42
modules/graph/edge.go
Normal file
|
@ -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)
|
||||||
|
}
|
158
modules/graph/edges.go
Normal file
158
modules/graph/edges.go
Normal file
|
@ -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()
|
||||||
|
}
|
425
modules/graph/graph.go
Normal file
425
modules/graph/graph.go
Normal file
|
@ -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
|
||||||
|
}
|
15
modules/graph/js_builtin.go
Normal file
15
modules/graph/js_builtin.go
Normal file
|
@ -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)
|
||||||
|
}
|
349
modules/graph/module.go
Normal file
349
modules/graph/module.go
Normal file
|
@ -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 = "<interface>"
|
||||||
|
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 <evilsocket@gmail.com>"
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
170
modules/graph/node.go
Normal file
170
modules/graph/node.go
Normal file
|
@ -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
|
||||||
|
}
|
47
modules/graph/stack.go
Normal file
47
modules/graph/stack.go
Normal file
|
@ -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
|
||||||
|
}
|
45
modules/graph/to_dot.go
Normal file
45
modules/graph/to_dot.go
Normal file
|
@ -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
|
||||||
|
}
|
43
modules/graph/to_json.go
Normal file
43
modules/graph/to_json.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/bettercap/bettercap/modules/dns_spoof"
|
"github.com/bettercap/bettercap/modules/dns_spoof"
|
||||||
"github.com/bettercap/bettercap/modules/events_stream"
|
"github.com/bettercap/bettercap/modules/events_stream"
|
||||||
"github.com/bettercap/bettercap/modules/gps"
|
"github.com/bettercap/bettercap/modules/gps"
|
||||||
|
"github.com/bettercap/bettercap/modules/graph"
|
||||||
"github.com/bettercap/bettercap/modules/hid"
|
"github.com/bettercap/bettercap/modules/hid"
|
||||||
"github.com/bettercap/bettercap/modules/http_proxy"
|
"github.com/bettercap/bettercap/modules/http_proxy"
|
||||||
"github.com/bettercap/bettercap/modules/http_server"
|
"github.com/bettercap/bettercap/modules/http_server"
|
||||||
|
@ -45,6 +46,7 @@ func LoadModules(sess *session.Session) {
|
||||||
sess.Register(dns_spoof.NewDNSSpoofer(sess))
|
sess.Register(dns_spoof.NewDNSSpoofer(sess))
|
||||||
sess.Register(events_stream.NewEventsStream(sess))
|
sess.Register(events_stream.NewEventsStream(sess))
|
||||||
sess.Register(gps.NewGPS(sess))
|
sess.Register(gps.NewGPS(sess))
|
||||||
|
sess.Register(graph.NewModule(sess))
|
||||||
sess.Register(http_proxy.NewHttpProxy(sess))
|
sess.Register(http_proxy.NewHttpProxy(sess))
|
||||||
sess.Register(http_server.NewHttpServer(sess))
|
sess.Register(http_server.NewHttpServer(sess))
|
||||||
sess.Register(https_proxy.NewHttpsProxy(sess))
|
sess.Register(https_proxy.NewHttpsProxy(sess))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue