From f9b8a2685f6ac532579ca1348880f798c8cbb9d5 Mon Sep 17 00:00:00 2001 From: "Remi GASCOU (Podalirius)" <79218792+p0dalirius@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:29:47 +0100 Subject: [PATCH] Import an exisiting Opengraph JSON file to append to it, fixes #5 --- OpenGraph.go | 218 ++++++++++++++++++++++++++++++++++++++++++++++ OpenGraph_test.go | 66 ++++++++++++++ 2 files changed, 284 insertions(+) diff --git a/OpenGraph.go b/OpenGraph.go index ff34180..157e9b5 100644 --- a/OpenGraph.go +++ b/OpenGraph.go @@ -7,6 +7,7 @@ import ( "github.com/TheManticoreProject/gopengraph/edge" "github.com/TheManticoreProject/gopengraph/node" + "github.com/TheManticoreProject/gopengraph/properties" ) // OpenGraph struct for managing a graph structure compatible with BloodHound OpenGraph. @@ -165,6 +166,29 @@ func (g *OpenGraph) RemoveNodeByID(id string) bool { return true } +// HasNode checks if a node exists in the graph after performing validation checks. +// +// It verifies that the node exists in the graph, +// and that the node has a valid ID. If any validation fails, +// the node is not returned. +// +// Arguments: +// +// n *node.Node: The node to check for existence in the graph. +// +// Returns: +// +// bool: True if the node exists in the graph, false if validation failed +// (e.g., node does not exist or has an invalid ID). +func (g *OpenGraph) HasNode(n *node.Node) bool { + for _, node := range g.nodes { + if node.Equal(n) { + return true + } + } + return false +} + // GetNode returns a node by ID after performing validation checks. // // It verifies that the node exists in the graph, @@ -207,6 +231,29 @@ func (g *OpenGraph) GetNodesByKind(kind string) []*node.Node { return nodes } +// HasEdge checks if an edge exists in the graph after performing validation checks. +// +// It verifies that the edge exists in the graph, +// and that the edge has a valid ID. If any validation fails, +// the edge is not returned. +// +// Arguments: +// +// e *edge.Edge: The edge to check for existence in the graph. +// +// Returns: +// +// bool: True if the edge exists in the graph, false if validation failed +// (e.g., edge does not exist or has an invalid ID). +func (g *OpenGraph) HasEdge(e *edge.Edge) bool { + for _, edge := range g.edges { + if edge.Equal(e) { + return true + } + } + return false +} + // GetEdgesByKind returns all edges of a specific kind after performing validation checks. // // It verifies that the kind is valid, @@ -279,6 +326,40 @@ func (g *OpenGraph) GetEdgesToNode(id string) []*edge.Edge { return edges } +// Metadata operations + +// GetSourceKind returns the source kind of the graph after performing validation checks. +// +// It verifies that the source kind exists in the graph, +// and that the source kind has a valid ID. If any validation fails, +// the source kind is not returned. +// +// Returns: +// +// string: The source kind if it exists, nil if validation failed +// (e.g., source kind does not exist or has an invalid ID). +func (g *OpenGraph) GetSourceKind() string { + return g.sourceKind +} + +// SetSourceKind sets the source kind of the graph after performing validation checks. +// +// It verifies that the source kind exists in the graph, +// and that the source kind has a valid ID. If any validation fails, +// the source kind is not set. +// +// Arguments: +// +// sourceKind string: The source kind to set for the graph. +// +// Returns: +// +// string: The source kind if it exists, nil if validation failed +// (e.g., source kind does not exist or has an invalid ID). +func (g *OpenGraph) SetSourceKind(sourceKind string) { + g.sourceKind = sourceKind +} + // Graph operations // FindPaths finds all paths between two nodes using BFS after performing validation checks. @@ -515,6 +596,108 @@ func (g *OpenGraph) ExportToFile(filename string) error { return os.WriteFile(filename, []byte(jsonData), 0644) } +// Graph imports + +// FromJSON imports graph data from a JSON string and appends it to the current graph. +// +// It expects JSON following the BloodHound OpenGraph schema. Nodes are added first, +// followed by edges. Existing nodes are left unchanged and duplicate edges are skipped. +// +// If metadata.source_kind is present and the current graph has no source kind set, +// it will be adopted. +func (g *OpenGraph) FromJSON(jsonData string) error { + // Temporary structures to unmarshal incoming JSON + type ogNode struct { + ID string `json:"id"` + Kinds []string `json:"kinds"` + Properties map[string]interface{} `json:"properties"` + } + type endpoint struct { + Value string `json:"value"` + MatchBy string `json:"match_by"` + } + type ogEdge struct { + Kind string `json:"kind"` + Start endpoint `json:"start"` + End endpoint `json:"end"` + Properties map[string]interface{} `json:"properties"` + } + type ogGraph struct { + Nodes []ogNode `json:"nodes"` + Edges []ogEdge `json:"edges"` + } + var openGraphDocument struct { + Graph ogGraph `json:"graph"` + Metadata struct { + SourceKind string `json:"source_kind"` + } `json:"metadata"` + } + + if err := json.Unmarshal([]byte(jsonData), &openGraphDocument); err != nil { + return fmt.Errorf("failed to parse JSON: %w", err) + } + + // Adopt source_kind if not already set + if g.sourceKind == "" && openGraphDocument.Metadata.SourceKind != "" { + g.sourceKind = openGraphDocument.Metadata.SourceKind + } + + // Import nodes first + for _, n := range openGraphDocument.Graph.Nodes { + var props *properties.Properties + if n.Properties != nil { + props = properties.NewPropertiesFromMap(n.Properties) + } else { + props = properties.NewProperties() + } + + newNode, err := node.NewNode(n.ID, append([]string{}, n.Kinds...), props) + if err != nil { + return fmt.Errorf("invalid node '%s': %w", n.ID, err) + } + // Append semantics: skip duplicates silently + _ = g.AddNode(newNode) + } + + // Then import edges + for _, e := range openGraphDocument.Graph.Edges { + // Only 'id' is supported for match_by + if e.Start.MatchBy != "" && e.Start.MatchBy != "id" { + return fmt.Errorf("unsupported start.match_by '%s' for edge kind '%s'", e.Start.MatchBy, e.Kind) + } + if e.End.MatchBy != "" && e.End.MatchBy != "id" { + return fmt.Errorf("unsupported end.match_by '%s' for edge kind '%s'", e.End.MatchBy, e.Kind) + } + + var props *properties.Properties + if e.Properties != nil { + props = properties.NewPropertiesFromMap(e.Properties) + } else { + props = properties.NewProperties() + } + + newEdge, err := edge.NewEdge(e.Start.Value, e.End.Value, e.Kind, props) + if err != nil { + return fmt.Errorf("invalid edge (%s -> %s, kind '%s'): %w", e.Start.Value, e.End.Value, e.Kind, err) + } + // Append semantics: skip duplicates and require nodes to exist + _ = g.AddEdge(newEdge) + } + + return nil +} + +// FromJSONFile imports graph data from a JSON file and appends it to the current graph. +// +// It reads the file content and delegates to FromJSON. +func (g *OpenGraph) FromJSONFile(filename string) error { + data, err := os.ReadFile(filename) + if err != nil { + return fmt.Errorf("failed to read file '%s': %w", filename, err) + } + return g.FromJSON(string(data)) +} + // Graph infos // GetNodeCount returns the total number of nodes after performing validation checks. @@ -598,3 +781,38 @@ func (g *OpenGraph) String() string { return fmt.Sprintf("OpenGraph(nodes=%d, edges=%d, source_kind='%s')", len(g.nodes), len(g.edges), g.sourceKind) } + +// Equal checks if two graphs are equal after performing validation checks. +// +// It verifies that the nodes and edges exist in the graph, +// and that the nodes and edges have valid IDs. If any validation fails, +// the graphs are not equal. +// +// Arguments: +// +// Returns: +// +// bool: True if the graphs are equal, false if validation failed +// (e.g., nodes or edges do not exist or have an invalid ID). +func (g *OpenGraph) Equal(other *OpenGraph) bool { + if g.GetNodeCount() != other.GetNodeCount() { + return false + } + if g.GetEdgeCount() != other.GetEdgeCount() { + return false + } + if g.GetSourceKind() != other.GetSourceKind() { + return false + } + for _, node := range g.nodes { + if !other.HasNode(node) { + return false + } + } + for _, edge := range g.edges { + if !other.HasEdge(edge) { + return false + } + } + return true +} diff --git a/OpenGraph_test.go b/OpenGraph_test.go index 30f89de..3afb8ad 100644 --- a/OpenGraph_test.go +++ b/OpenGraph_test.go @@ -214,3 +214,69 @@ func TestGetConnectedComponents(t *testing.T) { } } } + +func TestJSONioInvolution(t *testing.T) { + // Create an OpenGraph instance + graph := gopengraph.NewOpenGraph("Base") + + // Create nodes + bobProps := properties.NewProperties() + bobProps.SetProperty("displayname", "bob") + bobProps.SetProperty("property", "a") + bobProps.SetProperty("objectid", "123") + bobProps.SetProperty("name", "BOB") + + bobNode, _ := node.NewNode("123", []string{"Person", "Base"}, bobProps) + + aliceProps := properties.NewProperties() + aliceProps.SetProperty("displayname", "alice") + aliceProps.SetProperty("property", "b") + aliceProps.SetProperty("objectid", "234") + aliceProps.SetProperty("name", "ALICE") + + aliceNode, _ := node.NewNode("234", []string{"Person", "Base"}, aliceProps) + + // Add nodes to graph + graph.AddNode(bobNode) + graph.AddNode(aliceNode) + + // Create edge: Bob knows Alice + knowsEdge, _ := edge.NewEdge( + bobNode.GetID(), // Bob is the start + aliceNode.GetID(), // Alice is the end + "Knows", + nil, + ) + + // Add edge to graph + graph.AddEdge(knowsEdge) + + // ============================ + + // Export to JSON + jsonData, err := graph.ExportJSON(true) + if err != nil { + t.Fatalf("ExportJSON failed: %v", err) + } + + importedGraph := gopengraph.NewOpenGraph("") + // Import from JSON + err = importedGraph.FromJSON(jsonData) + if err != nil { + t.Fatalf("FromJSON failed: %v", err) + } + + // Check that the graph is the same + if importedGraph.GetNodeCount() != graph.GetNodeCount() { + t.Errorf("Expected %d nodes, got %d", graph.GetNodeCount(), importedGraph.GetNodeCount()) + } + if importedGraph.GetEdgeCount() != graph.GetEdgeCount() { + t.Errorf("Expected %d edges, got %d", graph.GetEdgeCount(), importedGraph.GetEdgeCount()) + } + if importedGraph.GetSourceKind() != graph.GetSourceKind() { + t.Errorf("Expected source kind '%s', got %s", graph.GetSourceKind(), importedGraph.GetSourceKind()) + } + if !importedGraph.Equal(graph) { + t.Errorf("Expected graphs to be equal") + } +}