Skip to content

Commit 5d7e00e

Browse files
committed
Public release v1.0.0
1 parent 5a28fb0 commit 5d7e00e

File tree

17 files changed

+1817
-0
lines changed

17 files changed

+1817
-0
lines changed

.github/FUNDING.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# These are supported funding model platforms
2+
3+
github: p0dalirius
4+
patreon: Podalirius

.github/banner.png

544 KB
Loading

.github/workflows/unit_tests.yaml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Run Unit Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- '*' # Run tests on every branch for every commit
7+
8+
jobs:
9+
test:
10+
name: Run Unit Tests
11+
runs-on: ${{ matrix.os == 'windows' && 'windows-latest' || 'ubuntu-latest' }}
12+
13+
strategy:
14+
matrix:
15+
os: [linux, windows]
16+
arch: [amd64, arm64, 386]
17+
exclude:
18+
- os: windows
19+
arch: arm64
20+
21+
env:
22+
GO111MODULE: 'on'
23+
CGO_ENABLED: '0'
24+
25+
steps:
26+
- name: Checkout Repository
27+
uses: actions/checkout@v3
28+
29+
- name: Set up QEMU for ARM64 emulation
30+
if: matrix.arch == 'arm64' && matrix.os != 'windows'
31+
uses: docker/setup-qemu-action@v2
32+
with:
33+
platforms: arm64
34+
35+
- name: Set up Go
36+
uses: actions/setup-go@v4
37+
with:
38+
go-version: '1.22.1'
39+
40+
- name: Run Unit Tests
41+
env:
42+
GOOS: ${{ matrix.os }}
43+
GOARCH: ${{ matrix.arch }}
44+
run: |
45+
echo "$GOOS, $GOARCH"
46+
go test -v $(go list ./...) -v

Makefile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.PHONY: all build test clean deps
2+
3+
GOCMD=go
4+
GOTEST=$(GOCMD) test
5+
6+
test:
7+
@ $(GOTEST) -count=1 ./...
8+
9+
clean:
10+
@ $(GOCMD) clean
11+

OpenGraph.go

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
package gopengraph
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
8+
"github.com/TheManticoreProject/gopengraph/edge"
9+
"github.com/TheManticoreProject/gopengraph/node"
10+
)
11+
12+
// OpenGraph struct for managing a graph structure compatible with BloodHound OpenGraph.
13+
//
14+
// Follows BloodHound OpenGraph schema requirements and best practices.
15+
//
16+
// Sources:
17+
// - https://bloodhound.specterops.io/opengraph/schema#opengraph
18+
// - https://bloodhound.specterops.io/opengraph/schema#minimal-working-json
19+
// - https://bloodhound.specterops.io/opengraph/best-practices
20+
type OpenGraph struct {
21+
nodes map[string]*node.Node
22+
edges []*edge.Edge
23+
sourceKind string
24+
}
25+
26+
// NewOpenGraph creates a new OpenGraph instance
27+
func NewOpenGraph(sourceKind string) *OpenGraph {
28+
return &OpenGraph{
29+
nodes: make(map[string]*node.Node),
30+
edges: make([]*edge.Edge, 0),
31+
sourceKind: sourceKind,
32+
}
33+
}
34+
35+
// AddNode adds a node to the graph
36+
func (g *OpenGraph) AddNode(node *node.Node) bool {
37+
if _, exists := g.nodes[node.GetID()]; exists {
38+
return false
39+
}
40+
41+
// Add source kind if specified and not already present
42+
if g.sourceKind != "" && !node.HasKind(g.sourceKind) {
43+
node.AddKind(g.sourceKind)
44+
}
45+
46+
g.nodes[node.GetID()] = node
47+
return true
48+
}
49+
50+
// AddEdge adds an edge to the graph
51+
func (g *OpenGraph) AddEdge(edge *edge.Edge) bool {
52+
// Verify both nodes exist
53+
if _, exists := g.nodes[edge.GetStartNodeID()]; !exists {
54+
return false
55+
}
56+
if _, exists := g.nodes[edge.GetEndNodeID()]; !exists {
57+
return false
58+
}
59+
60+
// Check for duplicate edge
61+
for _, e := range g.edges {
62+
if e.Equal(edge) {
63+
return false
64+
}
65+
}
66+
67+
g.edges = append(g.edges, edge)
68+
return true
69+
}
70+
71+
// RemoveNodeByID removes a node and its associated edges
72+
func (g *OpenGraph) RemoveNodeByID(id string) bool {
73+
if _, exists := g.nodes[id]; !exists {
74+
return false
75+
}
76+
77+
delete(g.nodes, id)
78+
79+
// Remove associated edges
80+
newEdges := make([]*edge.Edge, 0)
81+
for _, e := range g.edges {
82+
if e.GetStartNodeID() != id && e.GetEndNodeID() != id {
83+
newEdges = append(newEdges, e)
84+
}
85+
}
86+
g.edges = newEdges
87+
88+
return true
89+
}
90+
91+
// GetNode returns a node by ID
92+
func (g *OpenGraph) GetNode(id string) *node.Node {
93+
return g.nodes[id]
94+
}
95+
96+
// GetNodesByKind returns all nodes of a specific kind
97+
func (g *OpenGraph) GetNodesByKind(kind string) []*node.Node {
98+
var nodes []*node.Node
99+
for _, n := range g.nodes {
100+
if n.HasKind(kind) {
101+
nodes = append(nodes, n)
102+
}
103+
}
104+
return nodes
105+
}
106+
107+
// GetEdgesByKind returns all edges of a specific kind
108+
func (g *OpenGraph) GetEdgesByKind(kind string) []*edge.Edge {
109+
var edges []*edge.Edge
110+
for _, e := range g.edges {
111+
if e.GetKind() == kind {
112+
edges = append(edges, e)
113+
}
114+
}
115+
return edges
116+
}
117+
118+
// GetEdgesFromNode returns all edges starting from a node
119+
func (g *OpenGraph) GetEdgesFromNode(id string) []*edge.Edge {
120+
var edges []*edge.Edge
121+
for _, e := range g.edges {
122+
if e.GetStartNodeID() == id {
123+
edges = append(edges, e)
124+
}
125+
}
126+
return edges
127+
}
128+
129+
// GetEdgesToNode returns all edges ending at a node
130+
func (g *OpenGraph) GetEdgesToNode(id string) []*edge.Edge {
131+
var edges []*edge.Edge
132+
for _, e := range g.edges {
133+
if e.GetEndNodeID() == id {
134+
edges = append(edges, e)
135+
}
136+
}
137+
return edges
138+
}
139+
140+
// FindPaths finds all paths between two nodes using BFS
141+
func (g *OpenGraph) FindPaths(startID, endID string, maxDepth int) [][]string {
142+
if _, exists := g.nodes[startID]; !exists {
143+
return nil
144+
}
145+
if _, exists := g.nodes[endID]; !exists {
146+
return nil
147+
}
148+
149+
if startID == endID {
150+
return [][]string{{startID}}
151+
}
152+
153+
var paths [][]string
154+
visited := make(map[string]bool)
155+
queue := []struct {
156+
id string
157+
path []string
158+
}{{startID, []string{startID}}}
159+
visited[startID] = true
160+
161+
for len(queue) > 0 && len(queue[0].path) <= maxDepth {
162+
current := queue[0]
163+
queue = queue[1:]
164+
165+
for _, edge := range g.GetEdgesFromNode(current.id) {
166+
nextID := edge.GetEndNodeID()
167+
if !visited[nextID] {
168+
newPath := append([]string{}, current.path...)
169+
newPath = append(newPath, nextID)
170+
171+
if nextID == endID {
172+
paths = append(paths, newPath)
173+
} else {
174+
visited[nextID] = true
175+
queue = append(queue, struct {
176+
id string
177+
path []string
178+
}{nextID, newPath})
179+
}
180+
}
181+
}
182+
}
183+
184+
return paths
185+
}
186+
187+
// GetConnectedComponents finds all connected components
188+
func (g *OpenGraph) GetConnectedComponents() []map[string]bool {
189+
visited := make(map[string]bool)
190+
var components []map[string]bool
191+
192+
for nodeID := range g.nodes {
193+
if !visited[nodeID] {
194+
component := make(map[string]bool)
195+
stack := []string{nodeID}
196+
197+
for len(stack) > 0 {
198+
current := stack[len(stack)-1]
199+
stack = stack[:len(stack)-1]
200+
201+
if !visited[current] {
202+
visited[current] = true
203+
component[current] = true
204+
205+
// Add adjacent nodes
206+
for _, edge := range g.GetEdgesFromNode(current) {
207+
if !visited[edge.GetEndNodeID()] {
208+
stack = append(stack, edge.GetEndNodeID())
209+
}
210+
}
211+
for _, edge := range g.GetEdgesToNode(current) {
212+
if !visited[edge.GetStartNodeID()] {
213+
stack = append(stack, edge.GetStartNodeID())
214+
}
215+
}
216+
}
217+
}
218+
components = append(components, component)
219+
}
220+
}
221+
222+
return components
223+
}
224+
225+
// ValidateGraph checks for common graph issues
226+
func (g *OpenGraph) ValidateGraph() []string {
227+
var errors []string
228+
229+
// Check for orphaned edges
230+
for _, edge := range g.edges {
231+
if _, exists := g.nodes[edge.GetStartNodeID()]; !exists {
232+
errors = append(errors, fmt.Sprintf("Edge %s references non-existent start node: %s",
233+
edge.GetKind(), edge.GetStartNodeID()))
234+
}
235+
if _, exists := g.nodes[edge.GetEndNodeID()]; !exists {
236+
errors = append(errors, fmt.Sprintf("Edge %s references non-existent end node: %s",
237+
edge.GetKind(), edge.GetEndNodeID()))
238+
}
239+
}
240+
241+
// Check for isolated nodes
242+
var isolatedNodes []string
243+
for id := range g.nodes {
244+
if len(g.GetEdgesFromNode(id)) == 0 && len(g.GetEdgesToNode(id)) == 0 {
245+
isolatedNodes = append(isolatedNodes, id)
246+
}
247+
}
248+
249+
if len(isolatedNodes) > 0 {
250+
errors = append(errors, fmt.Sprintf("Found %d isolated nodes: %v",
251+
len(isolatedNodes), isolatedNodes))
252+
}
253+
254+
return errors
255+
}
256+
257+
// ExportJSON exports the graph to JSON format
258+
func (g *OpenGraph) ExportJSON(includeMetadata bool) (string, error) {
259+
graphData := make(map[string]interface{})
260+
graphContent := make(map[string]interface{})
261+
262+
// Convert nodes to dict format
263+
var nodesData []map[string]interface{}
264+
for _, n := range g.nodes {
265+
nodesData = append(nodesData, n.ToDict())
266+
}
267+
graphContent["nodes"] = nodesData
268+
269+
// Convert edges to dict format
270+
var edgesData []map[string]interface{}
271+
for _, e := range g.edges {
272+
edgesData = append(edgesData, e.ToDict())
273+
}
274+
graphContent["edges"] = edgesData
275+
276+
graphData["graph"] = graphContent
277+
278+
if includeMetadata && g.sourceKind != "" {
279+
graphData["metadata"] = map[string]interface{}{
280+
"source_kind": g.sourceKind,
281+
}
282+
}
283+
284+
jsonData, err := json.MarshalIndent(graphData, "", " ")
285+
if err != nil {
286+
return "", err
287+
}
288+
289+
return string(jsonData), nil
290+
}
291+
292+
// ExportToFile exports the graph to a JSON file
293+
func (g *OpenGraph) ExportToFile(filename string) error {
294+
jsonData, err := g.ExportJSON(true)
295+
if err != nil {
296+
return err
297+
}
298+
299+
return os.WriteFile(filename, []byte(jsonData), 0644)
300+
}
301+
302+
// GetNodeCount returns the total number of nodes
303+
func (g *OpenGraph) GetNodeCount() int {
304+
return len(g.nodes)
305+
}
306+
307+
// GetEdgeCount returns the total number of edges
308+
func (g *OpenGraph) GetEdgeCount() int {
309+
return len(g.edges)
310+
}
311+
312+
// Clear removes all nodes and edges
313+
func (g *OpenGraph) Clear() {
314+
g.nodes = make(map[string]*node.Node)
315+
g.edges = make([]*edge.Edge, 0)
316+
}
317+
318+
// Len returns the total number of nodes and edges
319+
func (g *OpenGraph) Len() int {
320+
return len(g.nodes) + len(g.edges)
321+
}
322+
323+
func (g *OpenGraph) String() string {
324+
return fmt.Sprintf("OpenGraph(nodes=%d, edges=%d, source_kind='%s')",
325+
len(g.nodes), len(g.edges), g.sourceKind)
326+
}

0 commit comments

Comments
 (0)