Add Dashboards (#41)

This commit is contained in:
Jonas Plum
2022-03-14 00:23:29 +01:00
committed by GitHub
parent 18a4dc54e7
commit 02c7da91da
30 changed files with 2824 additions and 279 deletions

117
database/dashboard.go Normal file
View File

@@ -0,0 +1,117 @@
package database
import (
"context"
"errors"
"fmt"
"github.com/arangodb/go-driver"
"github.com/iancoleman/strcase"
"github.com/SecurityBrewery/catalyst/caql"
"github.com/SecurityBrewery/catalyst/database/busdb"
"github.com/SecurityBrewery/catalyst/generated/model"
)
func toDashboardResponse(key string, doc *model.Dashboard) *model.DashboardResponse {
return &model.DashboardResponse{
ID: key,
Name: doc.Name,
Widgets: doc.Widgets,
}
}
func (db *Database) DashboardCreate(ctx context.Context, dashboard *model.Dashboard) (*model.DashboardResponse, error) {
if dashboard == nil {
return nil, errors.New("requires dashboard")
}
if dashboard.Name == "" {
return nil, errors.New("requires dashboard name")
}
if err := db.parseWidgets(dashboard); err != nil {
return nil, err
}
var doc model.Dashboard
newctx := driver.WithReturnNew(ctx, &doc)
meta, err := db.dashboardCollection.CreateDocument(ctx, newctx, strcase.ToKebab(dashboard.Name), dashboard)
if err != nil {
return nil, err
}
return toDashboardResponse(meta.Key, &doc), nil
}
func (db *Database) DashboardGet(ctx context.Context, id string) (*model.DashboardResponse, error) {
var doc model.Dashboard
meta, err := db.dashboardCollection.ReadDocument(ctx, id, &doc)
if err != nil {
return nil, err
}
return toDashboardResponse(meta.Key, &doc), nil
}
func (db *Database) DashboardUpdate(ctx context.Context, id string, dashboard *model.Dashboard) (*model.DashboardResponse, error) {
if err := db.parseWidgets(dashboard); err != nil {
return nil, err
}
var doc model.Dashboard
ctx = driver.WithReturnNew(ctx, &doc)
meta, err := db.dashboardCollection.ReplaceDocument(ctx, id, dashboard)
if err != nil {
return nil, err
}
return toDashboardResponse(meta.Key, &doc), nil
}
func (db *Database) DashboardDelete(ctx context.Context, id string) error {
_, err := db.dashboardCollection.RemoveDocument(ctx, id)
return err
}
func (db *Database) DashboardList(ctx context.Context) ([]*model.DashboardResponse, error) {
query := "FOR d IN @@collection RETURN d"
cursor, _, err := db.Query(ctx, query, map[string]interface{}{"@collection": DashboardCollectionName}, busdb.ReadOperation)
if err != nil {
return nil, err
}
defer cursor.Close()
var docs []*model.DashboardResponse
for {
var doc model.Dashboard
meta, err := cursor.ReadDocument(ctx, &doc)
if driver.IsNoMoreDocuments(err) {
break
} else if err != nil {
return nil, err
}
docs = append(docs, toDashboardResponse(meta.Key, &doc))
}
return docs, err
}
func (db *Database) parseWidgets(dashboard *model.Dashboard) error {
for _, widget := range dashboard.Widgets {
parser := &caql.Parser{Searcher: db.Index, Prefix: "d."}
_, err := parser.Parse(widget.Aggregation)
if err != nil {
return fmt.Errorf("invalid aggregation query (%s): syntax error\n", widget.Aggregation)
}
if widget.Filter != nil {
_, err := parser.Parse(*widget.Filter)
if err != nil {
return fmt.Errorf("invalid filter query (%s): syntax error\n", *widget.Filter)
}
}
}
return nil
}

View File

@@ -26,6 +26,7 @@ const (
TicketTypeCollectionName = "tickettypes"
JobCollectionName = "jobs"
SettingsCollectionName = "settings"
DashboardCollectionName = "dashboards"
TicketArtifactsGraphName = "Graph"
RelatedTicketsCollectionName = "related"
@@ -46,6 +47,7 @@ type Database struct {
tickettypeCollection *busdb.Collection
jobCollection *busdb.Collection
settingsCollection *busdb.Collection
dashboardCollection *busdb.Collection
relatedCollection *busdb.Collection
// containsCollection *busdb.Collection
@@ -128,6 +130,10 @@ func New(ctx context.Context, index *index.Index, bus *bus.Bus, hooks *hooks.Hoo
if err != nil {
return nil, err
}
dashboardCollection, err := arangoDB.Collection(ctx, DashboardCollectionName)
if err != nil {
return nil, err
}
hookedDB, err := busdb.NewDatabase(ctx, arangoDB, bus)
if err != nil {
@@ -149,6 +155,7 @@ func New(ctx context.Context, index *index.Index, bus *bus.Bus, hooks *hooks.Hoo
tickettypeCollection: busdb.NewCollection(tickettypeCollection, hookedDB),
jobCollection: busdb.NewCollection(jobCollection, hookedDB),
settingsCollection: busdb.NewCollection(settingsCollection, hookedDB),
dashboardCollection: busdb.NewCollection(dashboardCollection, hookedDB),
}
return db, nil
@@ -197,5 +204,6 @@ func (db *Database) Truncate(ctx context.Context) {
db.jobCollection.Truncate(ctx)
db.relatedCollection.Truncate(ctx)
db.settingsCollection.Truncate(ctx)
db.dashboardCollection.Truncate(ctx)
// db.containsCollection.Truncate(ctx)
}

View File

@@ -56,6 +56,8 @@ func generateMigrations() ([]Migration, error) {
&createDocument{ID: "create-settings-global", Collection: "settings", Document: &busdb.Keyed{Key: "global", Doc: model.Settings{ArtifactStates: []*model.Type{{Icon: "mdi-help-circle-outline", ID: "unknown", Name: "Unknown", Color: pointer.String(model.TypeColorInfo)}, {Icon: "mdi-skull", ID: "malicious", Name: "Malicious", Color: pointer.String(model.TypeColorError)}, {Icon: "mdi-check", ID: "clean", Name: "Clean", Color: pointer.String(model.TypeColorSuccess)}}, ArtifactKinds: []*model.Type{{Icon: "mdi-server", ID: "asset", Name: "Asset"}, {Icon: "mdi-bullseye", ID: "ioc", Name: "IOC"}}, Timeformat: "YYYY-MM-DDThh:mm:ss"}}},
&updateSchema{ID: "update-ticket-collection", Name: "tickets", DataType: "ticket", Schema: `{"properties":{"artifacts":{"items":{"properties":{"enrichments":{"additionalProperties":{"properties":{"created":{"format":"date-time","type":"string"},"data":{"example":{"hash":"b7a067a742c20d07a7456646de89bc2d408a1153"},"properties":{},"type":"object"},"name":{"example":"hash.sha1","type":"string"}},"required":["created","data","name"],"type":"object"},"type":"object"},"name":{"example":"2.2.2.2","type":"string"},"status":{"example":"Unknown","type":"string"},"type":{"type":"string"},"kind":{"type":"string"}},"required":["name"],"type":"object"},"type":"array"},"comments":{"items":{"properties":{"created":{"format":"date-time","type":"string"},"creator":{"type":"string"},"message":{"type":"string"}},"required":["created","creator","message"],"type":"object"},"type":"array"},"created":{"format":"date-time","type":"string"},"details":{"example":{"description":"my little incident"},"properties":{},"type":"object"},"files":{"items":{"properties":{"key":{"example":"myfile","type":"string"},"name":{"example":"notes.docx","type":"string"}},"required":["key","name"],"type":"object"},"type":"array"},"modified":{"format":"date-time","type":"string"},"name":{"example":"WannyCry","type":"string"},"owner":{"example":"bob","type":"string"},"playbooks":{"additionalProperties":{"properties":{"name":{"example":"Phishing","type":"string"},"tasks":{"additionalProperties":{"properties":{"automation":{"type":"string"},"closed":{"format":"date-time","type":"string"},"created":{"format":"date-time","type":"string"},"data":{"properties":{},"type":"object"},"done":{"type":"boolean"},"join":{"example":false,"type":"boolean"},"payload":{"additionalProperties":{"type":"string"},"type":"object"},"name":{"example":"Inform user","type":"string"},"next":{"additionalProperties":{"type":"string"},"type":"object"},"owner":{"type":"string"},"schema":{"properties":{},"type":"object"},"type":{"enum":["task","input","automation"],"example":"task","type":"string"}},"required":["created","done","name","type"],"type":"object"},"type":"object"}},"required":["name","tasks"],"type":"object"},"type":"object"},"read":{"example":["bob"],"items":{"type":"string"},"type":"array"},"references":{"items":{"properties":{"href":{"example":"https://cve.mitre.org/cgi-bin/cvename.cgi?name=cve-2017-0144","type":"string"},"name":{"example":"CVE-2017-0144","type":"string"}},"required":["href","name"],"type":"object"},"type":"array"},"schema":{"example":"{}","type":"string"},"status":{"example":"open","type":"string"},"type":{"example":"incident","type":"string"},"write":{"example":["alice"],"items":{"type":"string"},"type":"array"}},"required":["created","modified","name","schema","status","type"],"type":"object"}`},
&createCollection{ID: "create-dashboard-collection", Name: "dashboards", DataType: "dashboards", Schema: `{"type":"object","properties":{"name":{"type":"string"},"widgets":{"items":{"type":"object","properties":{"aggregation":{"type":"string"},"filter":{"type":"string"},"name":{"type":"string"},"type":{"enum":[ "bar", "line", "pie" ]},"width": { "type": "integer", "minimum": 1, "maximum": 12 }},"required":["name","aggregation", "type", "width"]},"type":"array"}},"required":["name","widgets"]}`},
}, nil
}

View File

@@ -2,7 +2,9 @@ package database
import (
"context"
"fmt"
"github.com/SecurityBrewery/catalyst/caql"
"github.com/SecurityBrewery/catalyst/database/busdb"
"github.com/SecurityBrewery/catalyst/generated/model"
)
@@ -41,3 +43,49 @@ func (db *Database) Statistics(ctx context.Context) (*model.Statistics, error) {
return &statistics, nil
}
func (db *Database) WidgetData(ctx context.Context, aggregation string, filter *string) (map[string]interface{}, error) {
parser := &caql.Parser{Searcher: db.Index, Prefix: "d."}
queryTree, err := parser.Parse(aggregation)
if err != nil {
return nil, fmt.Errorf("invalid aggregation query (%s): syntax error\n", aggregation)
}
aggregationString, err := queryTree.String()
if err != nil {
return nil, fmt.Errorf("invalid widget aggregation query (%s): %w", aggregation, err)
}
aggregation = aggregationString
filterQ := ""
if filter != nil && *filter != "" {
queryTree, err := parser.Parse(*filter)
if err != nil {
return nil, fmt.Errorf("invalid filter query (%s): syntax error\n", *filter)
}
filterString, err := queryTree.String()
if err != nil {
return nil, fmt.Errorf("invalid widget filter query (%s): %w", *filter, err)
}
filterQ = "FILTER " + filterString
}
query := `RETURN MERGE(FOR d in tickets
` + filterQ + `
COLLECT field = ` + aggregation + ` WITH COUNT INTO count
RETURN ZIP([field], [count]))`
cur, _, err := db.Query(ctx, query, nil, busdb.ReadOperation)
if err != nil {
return nil, err
}
defer cur.Close()
statistics := map[string]interface{}{}
if _, err := cur.ReadDocument(ctx, &statistics); err != nil {
return nil, err
}
return statistics, nil
}

View File

@@ -449,8 +449,6 @@ func (db *Database) TicketDelete(ctx context.Context, ticketID int64) error {
func (db *Database) TicketList(ctx context.Context, ticketType string, query string, sorts []string, desc []bool, offset, count int64) (*model.TicketList, error) {
binVars := map[string]interface{}{}
parser := &caql.Parser{Searcher: db.Index, Prefix: "d."}
var typeString = ""
if ticketType != "" {
typeString = "FILTER d.type == @type "
@@ -459,6 +457,7 @@ func (db *Database) TicketList(ctx context.Context, ticketType string, query str
var filterString = ""
if query != "" {
parser := &caql.Parser{Searcher: db.Index, Prefix: "d."}
queryTree, err := parser.Parse(query)
if err != nil {
return nil, errors.New("invalid filter query: syntax error")