Skip to content

Commit da96c5d

Browse files
committed
feat: BED-5468 - added a graph fixture loader for graph json files
1 parent 5425d6b commit da96c5d

File tree

1 file changed

+110
-0
lines changed

1 file changed

+110
-0
lines changed

packages/go/lab/loader.go

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package lab
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io/fs"
8+
9+
"github.com/specterops/bloodhound/dawgs/graph"
10+
)
11+
12+
// GraphFixture is the JSON representation of the graph we are importing.
13+
type GraphFixture struct {
14+
Nodes []Node `json:"nodes"`
15+
Relationships []Edge `json:"relationships"`
16+
}
17+
18+
// Node is the JSON representation of a graph node.
19+
type Node struct {
20+
// ID is the local identifier for the Node within the file.
21+
// This ID is not preserved when imported into the database.
22+
ID string `json:"id"`
23+
24+
// Labels are the node types. This is equivalent to what we call
25+
// Kinds in BloodHound.
26+
Labels []string `json:"labels"`
27+
28+
// Properties is the key:value map used for storing extra information
29+
// on the Node. We currently do not validate that Nodes have an
30+
// `object_id` property, but it is best practice to include one as
31+
// `object_id` is the main identifier we use in BloodHound.
32+
Properties map[string]any `json:"properties"`
33+
}
34+
35+
// Edge is the JSON representation of a graph edge.
36+
type Edge struct {
37+
// FromID is the local Node identifier for the start of an Edge.
38+
FromID string `json:"fromId"`
39+
40+
// ToID is the local Node identifier for the end of an Edge.
41+
ToID string `json:"toId"`
42+
43+
// Type is the 'label' we apply to the Edge. This is synonymous to
44+
// the edge Kind in BloodHound.
45+
Type string `json:"type"`
46+
47+
// Properties is the key:value map used for storing extra information
48+
// on the Edge.
49+
Properties map[string]any `json:"properties"`
50+
}
51+
52+
// ParseGraphFixtureJsonFile takes in a fs.File interface and parses its contents into a
53+
// GraphFixture struct.
54+
func ParseGraphFixtureJsonFile(fh fs.File) (GraphFixture, error) {
55+
var graphFixture GraphFixture
56+
if err := json.NewDecoder(fh).Decode(&graphFixture); err != nil {
57+
return graphFixture, fmt.Errorf("could not unmarshal graph data: %w", err)
58+
} else {
59+
return graphFixture, nil
60+
}
61+
}
62+
63+
// LoadGraphFixture requires a graph.Database interface and a GraphFixture struct.
64+
// It will import the nodes and edges from the GraphFixture and inserts them into
65+
// the graph database. It uses the `id` property of the nodes as the local
66+
// identifier and then maps them to database IDs, meaning that the `id` given in
67+
// the file will not be preserved.
68+
func LoadGraphFixture(db graph.Database, g *GraphFixture) error {
69+
var nodeMap = make(map[string]graph.ID)
70+
if err := db.WriteTransaction(context.Background(), func(tx graph.Transaction) error {
71+
for _, node := range g.Nodes {
72+
if dbNode, err := tx.CreateNode(graph.AsProperties(node.Properties), graph.StringsToKinds(node.Labels)...); err != nil {
73+
return fmt.Errorf("could not create node `%s`: %w", node.ID, err)
74+
} else {
75+
nodeMap[node.ID] = dbNode.ID
76+
}
77+
}
78+
for _, edge := range g.Relationships {
79+
if startId, ok := nodeMap[edge.FromID]; !ok {
80+
return fmt.Errorf("could not find start node %s", edge.FromID)
81+
} else if endId, ok := nodeMap[edge.ToID]; !ok {
82+
return fmt.Errorf("could not find end node %s", edge.ToID)
83+
} else if _, err := tx.CreateRelationshipByIDs(startId, endId, graph.StringKind(edge.Type), graph.AsProperties(edge.Properties)); err != nil {
84+
return fmt.Errorf("could not create relationship `%s` from `%s` to `%s`: %w", edge.Type, edge.FromID, edge.ToID, err)
85+
}
86+
}
87+
return nil
88+
}); err != nil {
89+
return fmt.Errorf("error writing graph data: %w", err)
90+
}
91+
return nil
92+
}
93+
94+
// LoadGraphFixtureFile takes a graph.Database interface, a fs.FS interface,
95+
// and a path string. It will attempt to read the given path from the FS and
96+
// parse then file into a GraphFixture, and finally import the GraphFixture
97+
// Nodes and Edges into the graph.Database.
98+
func LoadGraphFixtureFile(db graph.Database, fSys fs.FS, path string) error {
99+
if fh, err := fSys.Open(path); err != nil {
100+
return fmt.Errorf("could not open graph data file: %w", err)
101+
} else {
102+
defer fh.Close()
103+
if graphFixture, err := ParseGraphFixtureJsonFile(fh); err != nil {
104+
return fmt.Errorf("could not parse graph data file: %w", err)
105+
} else if err := LoadGraphFixture(db, &graphFixture); err != nil {
106+
return fmt.Errorf("could not load graph data: %w", err)
107+
}
108+
}
109+
return nil
110+
}

0 commit comments

Comments
 (0)