Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ MAIN_PACKAGES=$$($(GO_CMD) list ./...)

default: format test
all: generate format test
update: mod_update mod_tidy
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Declare the new command targets as phony.
If a file named update, mod_update, or mod_tidy exists, make will skip the recipes. Add them to .PHONY (and optionally existing command targets).

✅ Proposed change
+.PHONY: update mod_update mod_tidy
+
 update: mod_update mod_tidy

Also applies to: 23-27

🧰 Tools
🪛 checkmake (0.2.2)

[warning] 10-10: Target "update" should be declared PHONY.

(phonydeclared)

🤖 Prompt for AI Agents
In `@Makefile` at line 10, The Makefile introduces new targets update, mod_update,
and mod_tidy but doesn't declare them as phony; add these names to the .PHONY
declaration (alongside any existing phony targets) so make won't skip their
recipes if files with those names exist—update the .PHONY line to include
update, mod_update, and mod_tidy.


generate:
@$(GO_CMD) generate ./...
Expand All @@ -17,4 +18,10 @@ format:
test:
@for pkg in $(MAIN_PACKAGES) ; do \
$(GO_CMD) test -cover $$pkg -parallel=20 ; \
done
done

mod_update:
@$(GO_CMD) get -u ./...

mod_tidy:
@$(GO_CMD) mod tidy
9 changes: 5 additions & 4 deletions algo/reach.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import (
"github.com/gammazero/deque"
"github.com/specterops/dawgs/cardinality"
"github.com/specterops/dawgs/container"
"github.com/specterops/dawgs/database/v1compat"
"github.com/specterops/dawgs/database/v1compat/query"
"github.com/specterops/dawgs/graph"
"github.com/specterops/dawgs/query"
)

type reachCursor struct {
Expand Down Expand Up @@ -262,11 +263,11 @@ func (s *ReachabilityCache) XorReach(node uint64, direction graph.Direction, dup
duplex.Xor(reachBitmap)
}

func edgesFilteredByKinds(kinds ...graph.Kind) graph.Criteria {
func edgesFilteredByKinds(kinds ...graph.Kind) v1compat.Criteria {
return query.KindIn(query.Relationship(), kinds...)
}

func FetchReachabilityCache(ctx context.Context, db graph.Database, criteria graph.Criteria) (*ReachabilityCache, error) {
func FetchReachabilityCache(ctx context.Context, db v1compat.Database, criteria v1compat.Criteria) (*ReachabilityCache, error) {
if digraph, err := container.FetchDirectedGraph(ctx, db, criteria); err != nil {
return nil, err
} else {
Expand All @@ -275,6 +276,6 @@ func FetchReachabilityCache(ctx context.Context, db graph.Database, criteria gra
}
}

func FetchFilteredReachabilityCache(ctx context.Context, db graph.Database, traversalKinds ...graph.Kind) (*ReachabilityCache, error) {
func FetchFilteredReachabilityCache(ctx context.Context, db v1compat.Database, traversalKinds ...graph.Kind) (*ReachabilityCache, error) {
return FetchReachabilityCache(ctx, db, edgesFilteredByKinds(traversalKinds...))
}
2 changes: 1 addition & 1 deletion algo/scc.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func StronglyConnectedComponents(ctx context.Context, digraph container.Directed
}
}

return true
return util.IsContextLive(ctx)
})

return components, nodeToComp
Expand Down
40 changes: 22 additions & 18 deletions cardinality/lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import (

type threadSafeDuplex[T uint32 | uint64] struct {
provider Duplex[T]
lock *sync.Mutex
lock *sync.RWMutex
}

func ThreadSafeDuplex[T uint32 | uint64](provider Duplex[T]) Duplex[T] {
return threadSafeDuplex[T]{
provider: provider,
lock: &sync.Mutex{},
lock: &sync.RWMutex{},
}
}

Expand Down Expand Up @@ -66,56 +66,60 @@ func (s threadSafeDuplex[T]) Or(other Provider[T]) {
}

func (s threadSafeDuplex[T]) Cardinality() uint64 {
s.lock.Lock()
defer s.lock.Unlock()
s.lock.RLock()
defer s.lock.RUnlock()

return s.provider.Cardinality()
}

func (s threadSafeDuplex[T]) Slice() []T {
s.lock.Lock()
defer s.lock.Unlock()
s.lock.RLock()
defer s.lock.RUnlock()

return s.provider.Slice()
}

func (s threadSafeDuplex[T]) Contains(value T) bool {
s.lock.Lock()
defer s.lock.Unlock()
s.lock.RLock()
defer s.lock.RUnlock()

return s.provider.Contains(value)
}

func (s threadSafeDuplex[T]) Each(delegate func(value T) bool) {
s.lock.Lock()
defer s.lock.Unlock()
s.lock.RLock()
defer s.lock.RUnlock()

s.provider.Each(delegate)
}

func (s threadSafeDuplex[T]) CheckedAdd(value T) bool {
if s.Contains(value) {
return false
}

s.lock.Lock()
defer s.lock.Unlock()

return s.provider.CheckedAdd(value)
}
Comment on lines 96 to 105
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: TOCTOU race condition in CheckedAdd.

The pre-check at lines 97-99 creates a time-of-check to time-of-use race condition. Between releasing RLock (after Contains) and acquiring the write Lock, another goroutine can add the same value, violating atomicity.

Scenario:

  1. Thread A calls CheckedAdd(5), Contains(5) returns false (acquires/releases RLock)
  2. Thread B calls CheckedAdd(5), Contains(5) returns false (acquires/releases RLock)
  3. Both threads proceed to acquire write lock and call provider.CheckedAdd(5)
  4. Both may believe they successfully added the value first

Apply this diff to fix using the double-check pattern:

 func (s threadSafeDuplex[T]) CheckedAdd(value T) bool {
-	if s.Contains(value) {
+	s.lock.RLock()
+	alreadyExists := s.provider.Contains(value)
+	s.lock.RUnlock()
+	
+	if alreadyExists {
 		return false
 	}
 
 	s.lock.Lock()
 	defer s.lock.Unlock()
-
+	
+	// Double-check after acquiring write lock
+	if s.provider.Contains(value) {
+		return false
+	}
+	
 	return s.provider.CheckedAdd(value)
 }

Alternatively, if provider.CheckedAdd already implements the check-and-add atomically, simply use:

 func (s threadSafeDuplex[T]) CheckedAdd(value T) bool {
-	if s.Contains(value) {
-		return false
-	}
-
 	s.lock.Lock()
 	defer s.lock.Unlock()
 
 	return s.provider.CheckedAdd(value)
 }
🤖 Prompt for AI Agents
In cardinality/lock.go around lines 96 to 105, the current CheckedAdd does a
separate Contains check before acquiring the write lock causing a TOCTOU race;
change it to acquire the write lock first and then either (a) perform the
Contains check again (double-check pattern) before calling provider.CheckedAdd,
or (b) if provider.CheckedAdd is already atomic, drop the preliminary Contains
entirely and call provider.CheckedAdd under the write lock (or simply call
provider.CheckedAdd without any pre-check/extra locking). Ensure the write lock
is held across the check-and-add so the operation is atomic.


func (s threadSafeDuplex[T]) Clone() Duplex[T] {
s.lock.Lock()
defer s.lock.Unlock()
s.lock.RLock()
defer s.lock.RUnlock()

return ThreadSafeDuplex(s.provider.Clone())
}

type threadSafeSimplex[T uint32 | uint64] struct {
provider Simplex[T]
lock *sync.Mutex
lock *sync.RWMutex
}

func ThreadSafeSimplex[T uint32 | uint64](provider Simplex[T]) Simplex[T] {
return threadSafeSimplex[T]{
provider: provider,
lock: &sync.Mutex{},
lock: &sync.RWMutex{},
}
}

Expand All @@ -141,15 +145,15 @@ func (s threadSafeSimplex[T]) Or(other Provider[T]) {
}

func (s threadSafeSimplex[T]) Cardinality() uint64 {
s.lock.Lock()
defer s.lock.Unlock()
s.lock.RLock()
defer s.lock.RUnlock()

return s.provider.Cardinality()
}

func (s threadSafeSimplex[T]) Clone() Simplex[T] {
s.lock.Lock()
defer s.lock.Unlock()
s.lock.RLock()
defer s.lock.RUnlock()

return ThreadSafeSimplex(s.provider.Clone())
}
120 changes: 120 additions & 0 deletions cmd/viz/graph.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package main

import (
"io"
"os"
"strconv"

"github.com/go-echarts/go-echarts/v2/charts"
"github.com/go-echarts/go-echarts/v2/components"
"github.com/go-echarts/go-echarts/v2/opts"
"github.com/specterops/dawgs/container"
"github.com/specterops/dawgs/graph"
)

var graphNodes = []opts.GraphNode{
{Name: "Node1"},
{Name: "Node2"},
{Name: "Node3"},
{Name: "Node4"},
{Name: "Node5"},
{Name: "Node6"},
{Name: "Node7"},
{Name: "Node8"},
}

func genLinks() []opts.GraphLink {
links := make([]opts.GraphLink, 0)
for i := 0; i < len(graphNodes); i++ {
for j := 0; j < len(graphNodes); j++ {
links = append(links, opts.GraphLink{Source: graphNodes[i].Name, Target: graphNodes[j].Name})
}
}
return links
}

func graphBase() *charts.Graph {
graph := charts.NewGraph()
graph.SetGlobalOptions(
charts.WithTitleOpts(opts.Title{Title: "basic graph example"}),
)
graph.AddSeries("graph", graphNodes, genLinks(),
charts.WithGraphChartOpts(
opts.GraphChart{Force: &opts.GraphForce{Repulsion: 8000}},
),
)
return graph
}

func graphCircle() *charts.Graph {
graph := charts.NewGraph()
graph.SetGlobalOptions(
charts.WithTitleOpts(opts.Title{Title: "Circular layout"}),
)

graph.AddSeries("graph", graphNodes, genLinks()).
SetSeriesOptions(
charts.WithGraphChartOpts(
opts.GraphChart{
Force: &opts.GraphForce{Repulsion: 8000},
Layout: "circular",
}),
charts.WithLabelOpts(opts.Label{Show: opts.Bool(true), Position: "right"}),
)
return graph
}
Comment on lines +36 to +65
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if graphBase or graphCircle are used elsewhere in the codebase
rg -n '\b(graphBase|graphCircle)\b' --type=go

Repository: SpecterOps/DAWGS

Length of output: 167


Remove unused functions graphBase and graphCircle.

These functions are defined but never called anywhere in the codebase. If they're needed for future work, they should be re-added with clear context rather than kept as dead code.

🤖 Prompt for AI Agents
In cmd/viz/graph.go around lines 36 to 65, the functions graphBase and
graphCircle are defined but never used; remove both function definitions
(including their bodies) from the file to eliminate dead code and update any
imports or references if they become unused as a result; if you intend to keep
them for future use, instead add a clear TODO comment and unit tests or a
feature flag to justify keeping them, but the immediate fix is to delete these
unused functions.


func graphDigraph(digraph container.DirectedGraph, direction graph.Direction) *charts.Graph {
graph := charts.NewGraph()
graph.SetGlobalOptions(
charts.WithTitleOpts(opts.Title{
Title: "demo",
}))

var (
nodes []opts.GraphNode
links []opts.GraphLink
)

digraph.EachNode(func(node uint64) bool {
sourceNode := strconv.FormatUint(node, 10)

nodes = append(nodes, opts.GraphNode{
Name: sourceNode,
})

digraph.EachAdjacentNode(node, direction, func(adjacent uint64) bool {
links = append(links, opts.GraphLink{
Source: sourceNode,
Target: strconv.FormatUint(adjacent, 10),
})

return true
})

return true
})

graph.AddSeries("graph", nodes, links).
SetSeriesOptions(
charts.WithGraphChartOpts(opts.GraphChart{
Force: &opts.GraphForce{Repulsion: 8000},
}),
)
return graph
}

func doTheGraph(digraph container.DirectedGraph, direction graph.Direction) {
page := components.NewPage()
page.AddCharts(
graphDigraph(digraph, direction),
)

f, err := os.Create("graph.html")
if err != nil {
panic(err)

}
page.Render(io.MultiWriter(f))
f.Close()
Comment on lines +107 to +119
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Resource leak and missing error handling in doTheGraph.

  1. page.Render returns an error that is ignored.
  2. f.Close() should be deferred immediately after successful creation to ensure cleanup even if Render panics or fails.
  3. Consider returning an error instead of panicking for better composability.
 func doTheGraph(digraph container.DirectedGraph, direction graph.Direction) {
 	page := components.NewPage()
 	page.AddCharts(
 		graphDigraph(digraph, direction),
 	)

 	f, err := os.Create("graph.html")
 	if err != nil {
 		panic(err)
-
 	}
-	page.Render(io.MultiWriter(f))
-	f.Close()
+	defer f.Close()
+
+	if err := page.Render(io.MultiWriter(f)); err != nil {
+		panic(err)
+	}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func doTheGraph(digraph container.DirectedGraph, direction graph.Direction) {
page := components.NewPage()
page.AddCharts(
graphDigraph(digraph, direction),
)
f, err := os.Create("graph.html")
if err != nil {
panic(err)
}
page.Render(io.MultiWriter(f))
f.Close()
func doTheGraph(digraph container.DirectedGraph, direction graph.Direction) {
page := components.NewPage()
page.AddCharts(
graphDigraph(digraph, direction),
)
f, err := os.Create("graph.html")
if err != nil {
panic(err)
}
defer f.Close()
if err := page.Render(io.MultiWriter(f)); err != nil {
panic(err)
}
}
🤖 Prompt for AI Agents
In cmd/viz/graph.go around lines 107 to 119, doTheGraph currently ignores the
error returned by page.Render, defers file closing too late, and panics on
os.Create failure; change the function to return an error, call os.Create and
immediately defer f.Close() after a successful create, check and propagate the
error from page.Render (return it), and replace the panic with returned error
handling so callers can handle failures instead of the process exiting.

}
43 changes: 43 additions & 0 deletions cmd/viz/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package main

import (
"context"
"fmt"

"github.com/specterops/dawgs"
"github.com/specterops/dawgs/container"
"github.com/specterops/dawgs/database/pg"
"github.com/specterops/dawgs/graph"
"github.com/specterops/dawgs/query"
)

func main() {
ctx, done := context.WithCancel(context.Background())
defer done()

dbInst, err := dawgs.Open(ctx, pg.DriverName, dawgs.Config{
ConnectionString: "user=postgres dbname=bhe password=bhe4eva host=localhost",
})
Comment on lines +18 to +20
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Remove hardcoded database credentials.

The connection string contains hardcoded credentials (password=bhe4eva), which is a security risk. Use environment variables or a configuration file instead.

Apply this diff to use environment variables:

+	import "os"
+
-	dbInst, err := dawgs.Open(ctx, pg.DriverName, dawgs.Config{
-		ConnectionString: "user=postgres dbname=bhe password=bhe4eva host=localhost",
-	})
+	connStr := os.Getenv("DATABASE_URL")
+	if connStr == "" {
+		connStr = "user=postgres dbname=bhe host=localhost" // fallback without password
+	}
+	
+	dbInst, err := dawgs.Open(ctx, pg.DriverName, dawgs.Config{
+		ConnectionString: connStr,
+	})

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In cmd/viz/main.go around lines 18-20, the call to dawgs.Open uses a hardcoded
connection string including a plaintext password; replace this by building the
connection string from environment variables (e.g. DB_USER, DB_NAME,
DB_PASSWORD, DB_HOST, DB_PORT) or loading from a config source, validating
required vars and returning an error if missing, and avoid printing the password
to logs; construct the connection string programmatically from those env vars
(with sensible defaults for host/port if desired), then pass that to dawgs.Open.


if err != nil {
panic(err)
}

metaKinds := graph.Kinds{
graph.StringKind("Meta"),
graph.StringKind("MetaDetail"),
}

kindFilter := query.And(
query.Not(query.Start().Kinds().HasOneOf(metaKinds)),
query.Not(query.End().Kinds().HasOneOf(metaKinds)),
)

if digraph, err := container.FetchTSDB(ctx, dbInst, kindFilter); err != nil {
panic(err)
} else {
fmt.Printf("Loaded %d nodes\n", digraph.Triplestore.NumNodes())

// algo.CalculateKatzCentrality(digraph, 0.01, 1, 0.01, 1000)
}
}
1 change: 1 addition & 0 deletions container/adjacencymap.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func BuildAdjacencyMapGraph(adj map[uint64][]uint64) MutableDirectedGraph {

return digraph
}

func (s *adjacencyMapDigraph) AddNode(node uint64) {
s.nodes.Add(node)
}
Expand Down
26 changes: 13 additions & 13 deletions container/digraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,6 @@ func (s KindMap) FindAll(id uint64) graph.Kinds {
return matchedKinds
}

type DirectedGraph interface {
NumNodes() uint64
EachNode(delegate func(node uint64) bool)
EachAdjacentNode(node uint64, direction graph.Direction, delegate func(adjacent uint64) bool)
}

type MutableDirectedGraph interface {
DirectedGraph

AddNode(node uint64)
AddEdge(start, end uint64)
}

func BuildGraph(constructor func() MutableDirectedGraph, adj map[uint64][]uint64) MutableDirectedGraph {
digraph := constructor()

Expand Down Expand Up @@ -110,6 +97,19 @@ func AdjacentNodes(digraph DirectedGraph, node uint64, direction graph.Direction
return nodes
}

type DirectedGraph interface {
NumNodes() uint64
EachNode(delegate func(node uint64) bool)
EachAdjacentNode(node uint64, direction graph.Direction, delegate func(adjacent uint64) bool)
}

type MutableDirectedGraph interface {
DirectedGraph

AddNode(node uint64)
AddEdge(start, end uint64)
}

func Dimensions(digraph DirectedGraph, direction graph.Direction) (uint64, uint64) {
var largestRow uint64 = 0

Expand Down
Loading