mirror of
https://github.com/SecurityBrewery/catalyst.git
synced 2026-02-22 13:05:26 +01:00
Release catalyst
This commit is contained in:
164
dag/dag.go
Normal file
164
dag/dag.go
Normal file
@@ -0,0 +1,164 @@
|
||||
// Adapted from https://github.com/philopon/go-toposort under the MIT License
|
||||
// Original License:
|
||||
//
|
||||
// Copyright (c) 2017 Hirotomo Moriwaki
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package dag
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type Graph struct {
|
||||
nodes []string
|
||||
|
||||
outputs map[string]map[string]struct{}
|
||||
|
||||
// node: number of parents
|
||||
inputs map[string]int
|
||||
}
|
||||
|
||||
func NewGraph() *Graph {
|
||||
return &Graph{
|
||||
nodes: []string{},
|
||||
inputs: make(map[string]int),
|
||||
outputs: make(map[string]map[string]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Graph) AddNode(name string) error {
|
||||
g.nodes = append(g.nodes, name)
|
||||
|
||||
if _, ok := g.outputs[name]; ok {
|
||||
return errors.New("duplicate detected")
|
||||
}
|
||||
g.outputs[name] = make(map[string]struct{})
|
||||
g.inputs[name] = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Graph) AddNodes(names ...string) error {
|
||||
for _, name := range names {
|
||||
if err := g.AddNode(name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Graph) AddEdge(from, to string) error {
|
||||
m, ok := g.outputs[from]
|
||||
if !ok {
|
||||
return errors.New("node does not exist")
|
||||
}
|
||||
|
||||
m[to] = struct{}{}
|
||||
g.inputs[to]++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Graph) Toposort() ([]string, error) {
|
||||
outputs := map[string]map[string]struct{}{}
|
||||
for key, value := range g.outputs {
|
||||
outputs[key] = map[string]struct{}{}
|
||||
for k, v := range value {
|
||||
outputs[key][k] = v
|
||||
}
|
||||
}
|
||||
|
||||
L := make([]string, 0, len(g.nodes))
|
||||
S := make([]string, 0, len(g.nodes))
|
||||
|
||||
sort.Strings(g.nodes)
|
||||
for _, n := range g.nodes {
|
||||
if g.inputs[n] == 0 {
|
||||
S = append(S, n)
|
||||
}
|
||||
}
|
||||
|
||||
for len(S) > 0 {
|
||||
var n string
|
||||
n, S = S[0], S[1:]
|
||||
L = append(L, n)
|
||||
|
||||
ms := make([]string, len(outputs[n]))
|
||||
for _, k := range keys(outputs[n]) {
|
||||
m := k
|
||||
// i := outputs[n][m]
|
||||
// ms[i-1] = m
|
||||
ms = append(ms, m)
|
||||
}
|
||||
|
||||
for _, m := range ms {
|
||||
delete(outputs[n], m)
|
||||
g.inputs[m]--
|
||||
|
||||
if g.inputs[m] == 0 {
|
||||
S = append(S, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
N := 0
|
||||
for _, v := range g.inputs {
|
||||
N += v
|
||||
}
|
||||
|
||||
if N > 0 {
|
||||
return L, errors.New("cycle detected")
|
||||
}
|
||||
|
||||
return L, nil
|
||||
}
|
||||
|
||||
func keys(m map[string]struct{}) []string {
|
||||
var keys []string
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func (g *Graph) GetParents(id string) []string {
|
||||
var parents []string
|
||||
for node, targets := range g.outputs {
|
||||
if _, ok := targets[id]; ok {
|
||||
parents = append(parents, node)
|
||||
}
|
||||
}
|
||||
sort.Strings(parents)
|
||||
return parents
|
||||
}
|
||||
|
||||
func (g *Graph) GetRoot() (string, error) {
|
||||
var roots []string
|
||||
for n, parents := range g.inputs {
|
||||
if parents == 0 {
|
||||
roots = append(roots, n)
|
||||
}
|
||||
}
|
||||
if len(roots) != 1 {
|
||||
return "", errors.New("more than one root")
|
||||
}
|
||||
return roots[0], nil
|
||||
}
|
||||
238
dag/dag_test.go
Normal file
238
dag/dag_test.go
Normal file
@@ -0,0 +1,238 @@
|
||||
// Adapted from https://github.com/philopon/go-toposort under the MIT License
|
||||
// Original License:
|
||||
//
|
||||
// Copyright (c) 2017 Hirotomo Moriwaki
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package dag
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func index(s []string, v string) int {
|
||||
for i, s := range s {
|
||||
if s == v {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
type Edge struct {
|
||||
From string
|
||||
To string
|
||||
}
|
||||
|
||||
func TestDuplicatedNode(t *testing.T) {
|
||||
graph := NewGraph()
|
||||
assert.NoError(t, graph.AddNode("a"))
|
||||
assert.Error(t, graph.AddNode("a"))
|
||||
}
|
||||
|
||||
func TestWikipedia(t *testing.T) {
|
||||
graph := NewGraph()
|
||||
assert.NoError(t, graph.AddNodes("2", "3", "5", "7", "8", "9", "10", "11"))
|
||||
|
||||
edges := []Edge{
|
||||
{"7", "8"},
|
||||
{"7", "11"},
|
||||
|
||||
{"5", "11"},
|
||||
|
||||
{"3", "8"},
|
||||
{"3", "10"},
|
||||
|
||||
{"11", "2"},
|
||||
{"11", "9"},
|
||||
{"11", "10"},
|
||||
|
||||
{"8", "9"},
|
||||
}
|
||||
|
||||
for _, e := range edges {
|
||||
assert.NoError(t, graph.AddEdge(e.From, e.To))
|
||||
}
|
||||
|
||||
result, err := graph.Toposort()
|
||||
if err != nil {
|
||||
t.Errorf("closed path detected in no closed pathed graph")
|
||||
}
|
||||
|
||||
for _, e := range edges {
|
||||
if i, j := index(result, e.From), index(result, e.To); i > j {
|
||||
t.Errorf("dependency failed: not satisfy %v(%v) > %v(%v)", e.From, i, e.To, j)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCycle(t *testing.T) {
|
||||
graph := NewGraph()
|
||||
assert.NoError(t, graph.AddNodes("1", "2", "3"))
|
||||
|
||||
assert.NoError(t, graph.AddEdge("1", "2"))
|
||||
assert.NoError(t, graph.AddEdge("2", "3"))
|
||||
assert.NoError(t, graph.AddEdge("3", "1"))
|
||||
|
||||
_, err := graph.Toposort()
|
||||
if err == nil {
|
||||
t.Errorf("closed path not detected in closed pathed graph")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGraph_GetParents(t *testing.T) {
|
||||
type fields struct {
|
||||
nodes []string
|
||||
edges map[string]string
|
||||
}
|
||||
type args struct {
|
||||
id string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want []string
|
||||
}{
|
||||
{"parents 2", fields{nodes: []string{"1", "2", "3"}, edges: map[string]string{"1": "2", "2": "3"}}, args{id: "2"}, []string{"1"}},
|
||||
{"parents 3", fields{nodes: []string{"1", "2", "3"}, edges: map[string]string{"1": "3", "2": "3"}}, args{id: "3"}, []string{"1", "2"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewGraph()
|
||||
for _, node := range tt.fields.nodes {
|
||||
assert.NoError(t, g.AddNode(node))
|
||||
}
|
||||
for from, to := range tt.fields.edges {
|
||||
assert.NoError(t, g.AddEdge(from, to))
|
||||
}
|
||||
|
||||
if got := g.GetParents(tt.args.id); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GetParents() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDAG_AddNode(t *testing.T) {
|
||||
dag := NewGraph()
|
||||
|
||||
v := "1"
|
||||
assert.NoError(t, dag.AddNode(v))
|
||||
|
||||
assert.Error(t, dag.AddNode(v))
|
||||
}
|
||||
|
||||
func TestDAG_AddEdge(t *testing.T) {
|
||||
dag := NewGraph()
|
||||
assert.NoError(t, dag.AddNode("0"))
|
||||
assert.NoError(t, dag.AddNode("1"))
|
||||
assert.NoError(t, dag.AddNode("2"))
|
||||
assert.NoError(t, dag.AddNode("3"))
|
||||
|
||||
// add a single edge and inspect the graph
|
||||
assert.NoError(t, dag.AddEdge("1", "2"))
|
||||
|
||||
if parents := dag.GetParents("2"); len(parents) != 1 {
|
||||
t.Errorf("GetParents(v2) = %d, want 1", len(parents))
|
||||
}
|
||||
|
||||
assert.NoError(t, dag.AddEdge("2", "3"))
|
||||
|
||||
_ = dag.AddEdge("0", "1")
|
||||
}
|
||||
|
||||
func TestDAG_GetParents(t *testing.T) {
|
||||
dag := NewGraph()
|
||||
assert.NoError(t, dag.AddNode("1"))
|
||||
assert.NoError(t, dag.AddNode("2"))
|
||||
assert.NoError(t, dag.AddNode("3"))
|
||||
_ = dag.AddEdge("1", "3")
|
||||
_ = dag.AddEdge("2", "3")
|
||||
|
||||
parents := dag.GetParents("3")
|
||||
if length := len(parents); length != 2 {
|
||||
t.Errorf("GetParents(v3) = %d, want 2", length)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDAG_GetDescendants(t *testing.T) {
|
||||
dag := NewGraph()
|
||||
assert.NoError(t, dag.AddNode("1"))
|
||||
assert.NoError(t, dag.AddNode("2"))
|
||||
assert.NoError(t, dag.AddNode("3"))
|
||||
assert.NoError(t, dag.AddNode("4"))
|
||||
|
||||
assert.NoError(t, dag.AddEdge("1", "2"))
|
||||
assert.NoError(t, dag.AddEdge("2", "3"))
|
||||
assert.NoError(t, dag.AddEdge("2", "4"))
|
||||
}
|
||||
|
||||
func TestDAG_Topsort(t *testing.T) {
|
||||
dag := NewGraph()
|
||||
assert.NoError(t, dag.AddNode("1"))
|
||||
assert.NoError(t, dag.AddNode("2"))
|
||||
assert.NoError(t, dag.AddNode("3"))
|
||||
assert.NoError(t, dag.AddNode("4"))
|
||||
|
||||
assert.NoError(t, dag.AddEdge("1", "2"))
|
||||
assert.NoError(t, dag.AddEdge("2", "3"))
|
||||
assert.NoError(t, dag.AddEdge("2", "4"))
|
||||
|
||||
desc, _ := dag.Toposort()
|
||||
assert.Equal(t, desc, []string{"1", "2", "3", "4"})
|
||||
}
|
||||
|
||||
func TestDAG_TopsortStable(t *testing.T) {
|
||||
dag := NewGraph()
|
||||
assert.NoError(t, dag.AddNode("1"))
|
||||
assert.NoError(t, dag.AddNode("2"))
|
||||
assert.NoError(t, dag.AddNode("3"))
|
||||
|
||||
assert.NoError(t, dag.AddEdge("1", "2"))
|
||||
assert.NoError(t, dag.AddEdge("1", "3"))
|
||||
|
||||
desc, _ := dag.Toposort()
|
||||
assert.Equal(t, desc, []string{"1", "2", "3"})
|
||||
}
|
||||
|
||||
func TestDAG_TopsortStable2(t *testing.T) {
|
||||
dag := NewGraph()
|
||||
|
||||
assert.NoError(t, dag.AddNodes("block-ioc", "block-iocs", "block-sender", "board", "fetch-iocs", "escalate", "extract-iocs", "mail-available", "search-email-gateway"))
|
||||
assert.NoError(t, dag.AddEdge("block-iocs", "block-ioc"))
|
||||
assert.NoError(t, dag.AddEdge("block-sender", "extract-iocs"))
|
||||
assert.NoError(t, dag.AddEdge("board", "escalate"))
|
||||
assert.NoError(t, dag.AddEdge("board", "mail-available"))
|
||||
assert.NoError(t, dag.AddEdge("fetch-iocs", "block-iocs"))
|
||||
assert.NoError(t, dag.AddEdge("extract-iocs", "fetch-iocs"))
|
||||
assert.NoError(t, dag.AddEdge("mail-available", "block-sender"))
|
||||
assert.NoError(t, dag.AddEdge("mail-available", "extract-iocs"))
|
||||
assert.NoError(t, dag.AddEdge("mail-available", "search-email-gateway"))
|
||||
assert.NoError(t, dag.AddEdge("search-email-gateway", "extract-iocs"))
|
||||
|
||||
sorted, err := dag.Toposort()
|
||||
assert.NoError(t, err)
|
||||
|
||||
want := []string{"board", "escalate", "mail-available", "block-sender", "search-email-gateway", "extract-iocs", "fetch-iocs", "block-iocs", "block-ioc"}
|
||||
assert.Equal(t, want, sorted)
|
||||
}
|
||||
Reference in New Issue
Block a user