mirror of
https://github.com/SecurityBrewery/catalyst.git
synced 2026-01-24 15:03:27 +01:00
Release catalyst
This commit is contained in:
101
database/artifact.go
Normal file
101
database/artifact.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/arangodb/go-driver"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/database/busdb"
|
||||
"github.com/SecurityBrewery/catalyst/generated/models"
|
||||
)
|
||||
|
||||
func (db *Database) ArtifactGet(ctx context.Context, id int64, name string) (*models.Artifact, error) {
|
||||
ticketFilterQuery, ticketFilterVars, err := db.Hooks.TicketWriteFilter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := `LET d = DOCUMENT(@@collection, @ID)
|
||||
` + ticketFilterQuery + `
|
||||
FOR a in NOT_NULL(d.artifacts, [])
|
||||
FILTER a.name == @name
|
||||
RETURN a`
|
||||
cursor, _, err := db.Query(ctx, query, mergeMaps(ticketFilterVars, map[string]interface{}{
|
||||
"@collection": TicketCollectionName,
|
||||
"ID": fmt.Sprint(id),
|
||||
"name": name,
|
||||
}), busdb.ReadOperation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close()
|
||||
|
||||
var doc models.Artifact
|
||||
_, err = cursor.ReadDocument(ctx, &doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &doc, nil
|
||||
}
|
||||
|
||||
func (db *Database) ArtifactUpdate(ctx context.Context, id int64, name string, artifact *models.Artifact) (*models.TicketWithTickets, error) {
|
||||
ticketFilterQuery, ticketFilterVars, err := db.Hooks.TicketWriteFilter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := `LET d = DOCUMENT(@@collection, @ID)
|
||||
` + ticketFilterQuery + `
|
||||
FOR a IN NOT_NULL(d.artifacts, [])
|
||||
FILTER a.name == @name
|
||||
LET newartifacts = APPEND(REMOVE_VALUE(d.artifacts, a), @artifact)
|
||||
UPDATE d WITH { "artifacts": newartifacts } IN @@collection
|
||||
RETURN NEW`
|
||||
return db.ticketGetQuery(ctx, id, query, mergeMaps(map[string]interface{}{
|
||||
"@collection": TicketCollectionName,
|
||||
"ID": id,
|
||||
"name": name,
|
||||
"artifact": artifact,
|
||||
}, ticketFilterVars), &busdb.Operation{
|
||||
OperationType: busdb.Update,
|
||||
Ids: []driver.DocumentID{
|
||||
driver.DocumentID(fmt.Sprintf("%s/%d", TicketCollectionName, id)),
|
||||
},
|
||||
Msg: fmt.Sprintf("Update artifact %s", name),
|
||||
})
|
||||
}
|
||||
|
||||
func (db *Database) EnrichArtifact(ctx context.Context, id int64, name string, enrichmentForm *models.EnrichmentForm) (*models.TicketWithTickets, error) {
|
||||
enrichment := models.Enrichment{time.Now().UTC(), enrichmentForm.Data, enrichmentForm.Name}
|
||||
|
||||
ticketFilterQuery, ticketFilterVars, err := db.Hooks.TicketWriteFilter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := `LET d = DOCUMENT(@@collection, @ID)
|
||||
` + ticketFilterQuery + `
|
||||
FOR a IN d.artifacts
|
||||
FILTER a.name == @name
|
||||
LET enrichments = NOT_NULL(a.enrichments, {})
|
||||
LET newenrichments = MERGE(enrichments, ZIP( [@enrichmentname], [@enrichment]) )
|
||||
LET newartifacts = APPEND(REMOVE_VALUE(d.artifacts, a), MERGE(a, { "enrichments": newenrichments }))
|
||||
UPDATE d WITH { "artifacts": newartifacts } IN @@collection
|
||||
RETURN NEW`
|
||||
return db.ticketGetQuery(ctx, id, query, mergeMaps(map[string]interface{}{
|
||||
"@collection": TicketCollectionName,
|
||||
"ID": id,
|
||||
"name": name,
|
||||
"enrichmentname": enrichment.Name,
|
||||
"enrichment": enrichment,
|
||||
}, ticketFilterVars), &busdb.Operation{
|
||||
OperationType: busdb.Update,
|
||||
Ids: []driver.DocumentID{
|
||||
driver.DocumentID(fmt.Sprintf("%s/%d", TicketCollectionName, id)),
|
||||
},
|
||||
Msg: fmt.Sprintf("Run %s on artifact", enrichment.Name),
|
||||
})
|
||||
}
|
||||
99
database/automation.go
Normal file
99
database/automation.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/arangodb/go-driver"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/database/busdb"
|
||||
"github.com/SecurityBrewery/catalyst/generated/models"
|
||||
)
|
||||
|
||||
func toAutomation(doc *models.AutomationForm) interface{} {
|
||||
return &models.Automation{
|
||||
Image: doc.Image,
|
||||
Script: doc.Script,
|
||||
Schema: doc.Schema,
|
||||
Type: doc.Type,
|
||||
}
|
||||
}
|
||||
|
||||
func toAutomationResponse(id string, doc models.Automation) *models.AutomationResponse {
|
||||
return &models.AutomationResponse{
|
||||
ID: id,
|
||||
Image: doc.Image,
|
||||
Script: doc.Script,
|
||||
Schema: doc.Schema,
|
||||
Type: doc.Type,
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Database) AutomationCreate(ctx context.Context, automation *models.AutomationForm) (*models.AutomationResponse, error) {
|
||||
if automation == nil {
|
||||
return nil, errors.New("requires automation")
|
||||
}
|
||||
if automation.ID == "" {
|
||||
return nil, errors.New("requires automation ID")
|
||||
}
|
||||
|
||||
var doc models.Automation
|
||||
newctx := driver.WithReturnNew(ctx, &doc)
|
||||
|
||||
meta, err := db.automationCollection.CreateDocument(ctx, newctx, automation.ID, toAutomation(automation))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toAutomationResponse(meta.Key, doc), nil
|
||||
}
|
||||
|
||||
func (db *Database) AutomationGet(ctx context.Context, id string) (*models.AutomationResponse, error) {
|
||||
var doc models.Automation
|
||||
meta, err := db.automationCollection.ReadDocument(ctx, id, &doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toAutomationResponse(meta.Key, doc), nil
|
||||
}
|
||||
|
||||
func (db *Database) AutomationUpdate(ctx context.Context, id string, automation *models.AutomationForm) (*models.AutomationResponse, error) {
|
||||
var doc models.Automation
|
||||
ctx = driver.WithReturnNew(ctx, &doc)
|
||||
|
||||
meta, err := db.automationCollection.ReplaceDocument(ctx, id, toAutomation(automation))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toAutomationResponse(meta.Key, doc), nil
|
||||
}
|
||||
|
||||
func (db *Database) AutomationDelete(ctx context.Context, id string) error {
|
||||
_, err := db.automationCollection.RemoveDocument(ctx, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Database) AutomationList(ctx context.Context) ([]*models.AutomationResponse, error) {
|
||||
query := "FOR d IN @@collection SORT d._key ASC RETURN UNSET(d, 'script')"
|
||||
cursor, _, err := db.Query(ctx, query, map[string]interface{}{"@collection": AutomationCollectionName}, busdb.ReadOperation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close()
|
||||
var docs []*models.AutomationResponse
|
||||
for {
|
||||
var doc models.Automation
|
||||
meta, err := cursor.ReadDocument(ctx, &doc)
|
||||
if driver.IsNoMoreDocuments(err) {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
docs = append(docs, toAutomationResponse(meta.Key, doc))
|
||||
}
|
||||
|
||||
return docs, err
|
||||
}
|
||||
182
database/busdb/busdb.go
Normal file
182
database/busdb/busdb.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package busdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/arangodb/go-driver"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/bus"
|
||||
"github.com/SecurityBrewery/catalyst/generated/models"
|
||||
)
|
||||
|
||||
type Hook interface {
|
||||
PublishAction(action string, context, msg map[string]interface{}) error
|
||||
PublishUpdate(col, id string) error
|
||||
}
|
||||
|
||||
// BusDatabase
|
||||
// 1. Save entry to log
|
||||
// 2. Send update ticket to bus
|
||||
// 3. Add document to index
|
||||
type BusDatabase struct {
|
||||
internal driver.Database
|
||||
logCollection driver.Collection
|
||||
bus *bus.Bus
|
||||
// index *index.Index
|
||||
}
|
||||
|
||||
func NewDatabase(ctx context.Context, internal driver.Database, b *bus.Bus) (*BusDatabase, error) {
|
||||
logCollection, err := internal.Collection(ctx, LogCollectionName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &BusDatabase{
|
||||
internal: internal,
|
||||
logCollection: logCollection,
|
||||
bus: b,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type OperationType int
|
||||
|
||||
const (
|
||||
Create OperationType = iota
|
||||
Read = iota
|
||||
Update = iota
|
||||
)
|
||||
|
||||
type Operation struct {
|
||||
OperationType OperationType
|
||||
Ids []driver.DocumentID
|
||||
Msg string
|
||||
}
|
||||
|
||||
var CreateOperation = &Operation{OperationType: Create}
|
||||
var ReadOperation = &Operation{OperationType: Read}
|
||||
|
||||
func (db BusDatabase) Query(ctx context.Context, query string, vars map[string]interface{}, operation *Operation) (driver.Cursor, *models.LogEntry, error) {
|
||||
cur, err := db.internal.Query(ctx, query, vars)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var logs *models.LogEntry
|
||||
|
||||
switch {
|
||||
case operation.OperationType == Update:
|
||||
if err := db.LogAndNotify(ctx, operation.Ids, operation.Msg); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return cur, logs, err
|
||||
}
|
||||
|
||||
func (db BusDatabase) LogAndNotify(ctx context.Context, ids []driver.DocumentID, msg string) error {
|
||||
var logEntries []*models.LogEntry
|
||||
for _, i := range ids {
|
||||
logEntries = append(logEntries, &models.LogEntry{Reference: i.String(), Message: msg})
|
||||
}
|
||||
|
||||
if err := db.LogBatchCreate(ctx, logEntries); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.bus.PublishUpdate(ids)
|
||||
}
|
||||
|
||||
func (db BusDatabase) Remove(ctx context.Context) error {
|
||||
return db.internal.Remove(ctx)
|
||||
}
|
||||
|
||||
func (db BusDatabase) Collection(ctx context.Context, name string) (driver.Collection, error) {
|
||||
return db.internal.Collection(ctx, name)
|
||||
}
|
||||
|
||||
type Collection struct {
|
||||
internal driver.Collection
|
||||
db *BusDatabase
|
||||
}
|
||||
|
||||
func NewCollection(internal driver.Collection, db *BusDatabase) *Collection {
|
||||
return &Collection{internal: internal, db: db}
|
||||
}
|
||||
|
||||
func (c Collection) CreateDocument(ctx, newctx context.Context, key string, document interface{}) (driver.DocumentMeta, error) {
|
||||
meta, err := c.internal.CreateDocument(newctx, &Keyed{Key: key, Doc: document})
|
||||
if err != nil {
|
||||
return meta, err
|
||||
}
|
||||
|
||||
err = c.db.LogAndNotify(ctx, []driver.DocumentID{meta.ID}, "Document created")
|
||||
if err != nil {
|
||||
return meta, err
|
||||
}
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
func (c Collection) CreateEdge(ctx, newctx context.Context, edge *driver.EdgeDocument) (driver.DocumentMeta, error) {
|
||||
meta, err := c.internal.CreateDocument(newctx, edge)
|
||||
if err != nil {
|
||||
return meta, err
|
||||
}
|
||||
|
||||
err = c.db.LogAndNotify(ctx, []driver.DocumentID{meta.ID}, "Document created")
|
||||
if err != nil {
|
||||
return meta, err
|
||||
}
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
func (c Collection) CreateEdges(ctx context.Context, edges []*driver.EdgeDocument) (driver.DocumentMetaSlice, error) {
|
||||
metas, errs, err := c.internal.CreateDocuments(ctx, edges)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if errs.FirstNonNil() != nil {
|
||||
return nil, errs.FirstNonNil()
|
||||
}
|
||||
|
||||
var ids []driver.DocumentID
|
||||
for _, meta := range metas {
|
||||
ids = append(ids, meta.ID)
|
||||
}
|
||||
|
||||
err = c.db.LogAndNotify(ctx, ids, "Document created")
|
||||
if err != nil {
|
||||
return metas, err
|
||||
}
|
||||
|
||||
return metas, nil
|
||||
}
|
||||
|
||||
func (c Collection) DocumentExists(ctx context.Context, id string) (bool, error) {
|
||||
return c.internal.DocumentExists(ctx, id)
|
||||
}
|
||||
|
||||
func (c Collection) ReadDocument(ctx context.Context, key string, result interface{}) (driver.DocumentMeta, error) {
|
||||
return c.internal.ReadDocument(ctx, key, result)
|
||||
}
|
||||
|
||||
func (c Collection) UpdateDocument(ctx context.Context, key string, update interface{}) (driver.DocumentMeta, error) {
|
||||
meta, err := c.internal.UpdateDocument(ctx, key, update)
|
||||
if err != nil {
|
||||
return meta, err
|
||||
}
|
||||
|
||||
return meta, c.db.bus.PublishUpdate([]driver.DocumentID{meta.ID})
|
||||
}
|
||||
|
||||
func (c Collection) ReplaceDocument(ctx context.Context, key string, document interface{}) (driver.DocumentMeta, error) {
|
||||
meta, err := c.internal.ReplaceDocument(ctx, key, document)
|
||||
if err != nil {
|
||||
return meta, err
|
||||
}
|
||||
|
||||
return meta, c.db.bus.PublishUpdate([]driver.DocumentID{meta.ID})
|
||||
}
|
||||
|
||||
func (c Collection) RemoveDocument(ctx context.Context, formatInt string) (driver.DocumentMeta, error) {
|
||||
return c.internal.RemoveDocument(ctx, formatInt)
|
||||
}
|
||||
34
database/busdb/context.go
Normal file
34
database/busdb/context.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package busdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/generated/models"
|
||||
"github.com/SecurityBrewery/catalyst/role"
|
||||
)
|
||||
|
||||
const (
|
||||
userContextKey = "user"
|
||||
groupContextKey = "groups"
|
||||
)
|
||||
|
||||
func SetContext(ctx *gin.Context, user *models.UserResponse) {
|
||||
user.Roles = role.Strings(role.Explodes(user.Roles))
|
||||
ctx.Set(userContextKey, user)
|
||||
}
|
||||
|
||||
func SetGroupContext(ctx *gin.Context, groups []string) {
|
||||
ctx.Set(groupContextKey, groups)
|
||||
}
|
||||
|
||||
func UserContext(ctx context.Context, user *models.UserResponse) context.Context {
|
||||
user.Roles = role.Strings(role.Explodes(user.Roles))
|
||||
return context.WithValue(ctx, userContextKey, user)
|
||||
}
|
||||
|
||||
func UserFromContext(ctx context.Context) (*models.UserResponse, bool) {
|
||||
u, ok := ctx.Value(userContextKey).(*models.UserResponse)
|
||||
return u, ok
|
||||
}
|
||||
25
database/busdb/keyed.go
Normal file
25
database/busdb/keyed.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package busdb
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Keyed struct {
|
||||
Key string
|
||||
Doc interface{}
|
||||
}
|
||||
|
||||
func (p Keyed) MarshalJSON() ([]byte, error) {
|
||||
b, err := json.Marshal(p.Doc)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var m map[string]interface{}
|
||||
err = json.Unmarshal(b, &m)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
m["_key"] = p.Key
|
||||
|
||||
return json.Marshal(m)
|
||||
}
|
||||
92
database/busdb/log.go
Normal file
92
database/busdb/log.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package busdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/arangodb/go-driver"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/generated/models"
|
||||
)
|
||||
|
||||
const LogCollectionName = "logs"
|
||||
|
||||
func (db *BusDatabase) LogCreate(ctx context.Context, id, message string) (*models.LogEntry, error) {
|
||||
user, ok := UserFromContext(ctx)
|
||||
if !ok {
|
||||
return nil, errors.New("no user in context")
|
||||
}
|
||||
|
||||
logentry := &models.LogEntry{
|
||||
Reference: id,
|
||||
Created: time.Now(),
|
||||
Creator: user.ID,
|
||||
Message: message,
|
||||
}
|
||||
|
||||
doc := models.LogEntry{}
|
||||
_, err := db.logCollection.CreateDocument(driver.WithReturnNew(ctx, &doc), logentry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &doc, db.bus.PublishUpdate([]driver.DocumentID{driver.DocumentID(logentry.Reference)})
|
||||
}
|
||||
|
||||
func (db *BusDatabase) LogBatchCreate(ctx context.Context, logEntryForms []*models.LogEntry) error {
|
||||
user, ok := UserFromContext(ctx)
|
||||
if !ok {
|
||||
return errors.New("no user in context")
|
||||
}
|
||||
|
||||
var ids []driver.DocumentID
|
||||
var logentries []*models.LogEntry
|
||||
for _, logEntryForm := range logEntryForms {
|
||||
logentry := &models.LogEntry{
|
||||
Reference: logEntryForm.Reference,
|
||||
Created: time.Now(),
|
||||
Creator: user.ID,
|
||||
Message: logEntryForm.Message,
|
||||
}
|
||||
|
||||
logentries = append(logentries, logentry)
|
||||
ids = append(ids, driver.DocumentID(logentry.Reference))
|
||||
}
|
||||
|
||||
_, errs, err := db.logCollection.CreateDocuments(ctx, logentries)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = errs.FirstNonNil()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.bus.PublishUpdate(ids)
|
||||
}
|
||||
|
||||
func (db *BusDatabase) LogList(ctx context.Context, reference string) ([]*models.LogEntry, error) {
|
||||
query := "FOR d IN @@collection FILTER d.reference == @reference SORT d.created DESC RETURN d"
|
||||
cursor, err := db.internal.Query(ctx, query, map[string]interface{}{
|
||||
"@collection": LogCollectionName,
|
||||
"reference": reference,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close()
|
||||
var docs []*models.LogEntry
|
||||
for {
|
||||
var doc models.LogEntry
|
||||
_, err := cursor.ReadDocument(ctx, &doc)
|
||||
if driver.IsNoMoreDocuments(err) {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
docs = append(docs, &doc)
|
||||
}
|
||||
|
||||
return docs, err
|
||||
}
|
||||
177
database/db.go
Normal file
177
database/db.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/arangodb/go-driver"
|
||||
"github.com/arangodb/go-driver/http"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/bus"
|
||||
"github.com/SecurityBrewery/catalyst/database/busdb"
|
||||
"github.com/SecurityBrewery/catalyst/database/migrations"
|
||||
"github.com/SecurityBrewery/catalyst/hooks"
|
||||
"github.com/SecurityBrewery/catalyst/index"
|
||||
)
|
||||
|
||||
const (
|
||||
Name = "catalyst"
|
||||
TicketCollectionName = "tickets"
|
||||
TemplateCollectionName = "templates"
|
||||
PlaybookCollectionName = "playbooks"
|
||||
AutomationCollectionName = "automations"
|
||||
UserDataCollectionName = "userdata"
|
||||
UserCollectionName = "users"
|
||||
TicketTypeCollectionName = "tickettypes"
|
||||
JobCollectionName = "jobs"
|
||||
|
||||
TicketArtifactsGraphName = "Graph"
|
||||
RelatedTicketsCollectionName = "related"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
*busdb.BusDatabase
|
||||
Index *index.Index
|
||||
bus *bus.Bus
|
||||
Hooks *hooks.Hooks
|
||||
|
||||
templateCollection *busdb.Collection
|
||||
ticketCollection *busdb.Collection
|
||||
playbookCollection *busdb.Collection
|
||||
automationCollection *busdb.Collection
|
||||
userdataCollection *busdb.Collection
|
||||
userCollection *busdb.Collection
|
||||
tickettypeCollection *busdb.Collection
|
||||
jobCollection *busdb.Collection
|
||||
|
||||
relatedCollection *busdb.Collection
|
||||
containsCollection *busdb.Collection
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Host string
|
||||
User string
|
||||
Password string
|
||||
Name string
|
||||
}
|
||||
|
||||
func New(ctx context.Context, index *index.Index, bus *bus.Bus, hooks *hooks.Hooks, config *Config) (*Database, error) {
|
||||
name := config.Name
|
||||
if config.Name == "" {
|
||||
name = Name
|
||||
}
|
||||
|
||||
conn, err := http.NewConnection(http.ConnectionConfig{Endpoints: []string{config.Host}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err := driver.NewClient(driver.ClientConfig{
|
||||
Connection: conn,
|
||||
Authentication: driver.BasicAuthentication(config.User, config.Password),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hooks.DatabaseAfterConnect(ctx, client, name)
|
||||
|
||||
db, err := setupDB(ctx, client, name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("DB setup failed: %w", err)
|
||||
}
|
||||
|
||||
if err = migrations.PerformMigrations(ctx, db); err != nil {
|
||||
return nil, fmt.Errorf("migrations failed: %w", err)
|
||||
}
|
||||
|
||||
ticketCollection, err := db.Collection(ctx, TicketCollectionName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
templateCollection, err := db.Collection(ctx, TemplateCollectionName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
playbookCollection, err := db.Collection(ctx, PlaybookCollectionName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
relatedCollection, err := db.Collection(ctx, RelatedTicketsCollectionName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
automationCollection, err := db.Collection(ctx, AutomationCollectionName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userdataCollection, err := db.Collection(ctx, UserDataCollectionName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userCollection, err := db.Collection(ctx, UserCollectionName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tickettypeCollection, err := db.Collection(ctx, TicketTypeCollectionName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jobCollection, err := db.Collection(ctx, JobCollectionName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hookedDB, err := busdb.NewDatabase(ctx, db, bus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Database{
|
||||
BusDatabase: hookedDB,
|
||||
bus: bus,
|
||||
Index: index,
|
||||
Hooks: hooks,
|
||||
templateCollection: busdb.NewCollection(templateCollection, hookedDB),
|
||||
ticketCollection: busdb.NewCollection(ticketCollection, hookedDB),
|
||||
playbookCollection: busdb.NewCollection(playbookCollection, hookedDB),
|
||||
automationCollection: busdb.NewCollection(automationCollection, hookedDB),
|
||||
relatedCollection: busdb.NewCollection(relatedCollection, hookedDB),
|
||||
userdataCollection: busdb.NewCollection(userdataCollection, hookedDB),
|
||||
userCollection: busdb.NewCollection(userCollection, hookedDB),
|
||||
tickettypeCollection: busdb.NewCollection(tickettypeCollection, hookedDB),
|
||||
jobCollection: busdb.NewCollection(jobCollection, hookedDB),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func setupDB(ctx context.Context, client driver.Client, dbName string) (driver.Database, error) {
|
||||
databaseExists, err := client.DatabaseExists(ctx, dbName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var db driver.Database
|
||||
if !databaseExists {
|
||||
db, err = client.CreateDatabase(ctx, dbName, nil)
|
||||
} else {
|
||||
db, err = client.Database(ctx, dbName)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
collectionExists, err := db.CollectionExists(ctx, migrations.MigrationCollection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !collectionExists {
|
||||
if _, err := db.CreateCollection(ctx, migrations.MigrationCollection, &driver.CreateCollectionOptions{
|
||||
KeyOptions: &driver.CollectionKeyOptions{AllowUserKeys: true},
|
||||
}); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
256
database/job.go
Normal file
256
database/job.go
Normal file
@@ -0,0 +1,256 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/arangodb/go-driver"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/xeipuuv/gojsonschema"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/caql"
|
||||
"github.com/SecurityBrewery/catalyst/database/busdb"
|
||||
"github.com/SecurityBrewery/catalyst/generated/models"
|
||||
)
|
||||
|
||||
func toJob(doc *models.JobForm) *models.Job {
|
||||
return &models.Job{
|
||||
Automation: doc.Automation,
|
||||
Payload: doc.Payload,
|
||||
Origin: doc.Origin,
|
||||
Running: true,
|
||||
Status: "created",
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Database) toJobResponse(ctx context.Context, key string, doc *models.Job, update bool) (*models.JobResponse, error) {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
status := doc.Status
|
||||
|
||||
if doc.Running {
|
||||
inspect, err := cli.ContainerInspect(ctx, key)
|
||||
if err != nil || inspect.State == nil {
|
||||
doc.Running = false
|
||||
if update {
|
||||
db.JobUpdate(ctx, key, doc)
|
||||
}
|
||||
} else if doc.Status != inspect.State.Status {
|
||||
status = inspect.State.Status
|
||||
doc.Status = inspect.State.Status
|
||||
if update {
|
||||
db.JobUpdate(ctx, key, doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &models.JobResponse{
|
||||
Automation: doc.Automation,
|
||||
ID: key,
|
||||
Log: doc.Log,
|
||||
Payload: doc.Payload,
|
||||
Origin: doc.Origin,
|
||||
Output: doc.Output,
|
||||
Status: status,
|
||||
Container: doc.Container,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (db *Database) JobCreate(ctx context.Context, id string, job *models.JobForm) (*models.JobResponse, error) {
|
||||
if job == nil {
|
||||
return nil, errors.New("requires job")
|
||||
}
|
||||
|
||||
var doc models.Job
|
||||
newctx := driver.WithReturnNew(ctx, &doc)
|
||||
|
||||
/* Start validation */
|
||||
j := toJob(job)
|
||||
b, _ := json.Marshal(j)
|
||||
|
||||
r, err := models.JobSchema.Validate(gojsonschema.NewBytesLoader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !r.Valid() {
|
||||
var errs []string
|
||||
for _, e := range r.Errors() {
|
||||
errs = append(errs, e.String())
|
||||
}
|
||||
return nil, errors.New(strings.Join(errs, ", "))
|
||||
}
|
||||
/* End validation */
|
||||
|
||||
meta, err := db.jobCollection.CreateDocument(ctx, newctx, id, j)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db.toJobResponse(ctx, meta.Key, &doc, true)
|
||||
}
|
||||
|
||||
func (db *Database) JobGet(ctx context.Context, id string) (*models.JobResponse, error) {
|
||||
var doc models.Job
|
||||
meta, err := db.jobCollection.ReadDocument(ctx, id, &doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db.toJobResponse(ctx, meta.Key, &doc, true)
|
||||
}
|
||||
|
||||
func (db *Database) JobUpdate(ctx context.Context, id string, job *models.Job) (*models.JobResponse, error) {
|
||||
var doc models.Job
|
||||
ctx = driver.WithReturnNew(ctx, &doc)
|
||||
|
||||
/* Start validation */
|
||||
b, _ := json.Marshal(job)
|
||||
|
||||
r, err := models.JobSchema.Validate(gojsonschema.NewBytesLoader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !r.Valid() {
|
||||
var errs []string
|
||||
for _, e := range r.Errors() {
|
||||
errs = append(errs, e.String())
|
||||
}
|
||||
return nil, errors.New(strings.Join(errs, ", "))
|
||||
}
|
||||
/* End validation */
|
||||
|
||||
meta, err := db.jobCollection.ReplaceDocument(ctx, id, job)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db.toJobResponse(ctx, meta.Key, &doc, true)
|
||||
}
|
||||
|
||||
func (db *Database) JobLogAppend(ctx context.Context, id string, logLine string) error {
|
||||
query := `LET d = DOCUMENT(@@collection, @ID)
|
||||
UPDATE d WITH { "log": CONCAT(NOT_NULL(d.log, ""), @logline) } IN @@collection`
|
||||
cur, _, err := db.Query(ctx, query, map[string]interface{}{
|
||||
"@collection": JobCollectionName,
|
||||
"ID": id,
|
||||
"logline": logLine,
|
||||
}, &busdb.Operation{
|
||||
OperationType: busdb.Update,
|
||||
Ids: []driver.DocumentID{
|
||||
driver.DocumentID(fmt.Sprintf("%s/%s", JobCollectionName, id)),
|
||||
},
|
||||
Msg: fmt.Sprintf("Append logline"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cur.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) JobComplete(ctx context.Context, id string, out interface{}) error {
|
||||
query := `LET d = DOCUMENT(@@collection, @ID)
|
||||
UPDATE d WITH { "output": @out, "status": "completed", "running": false } IN @@collection`
|
||||
cur, _, err := db.Query(ctx, query, map[string]interface{}{
|
||||
"@collection": JobCollectionName,
|
||||
"ID": id,
|
||||
"out": out,
|
||||
}, &busdb.Operation{
|
||||
OperationType: busdb.Update,
|
||||
Ids: []driver.DocumentID{
|
||||
driver.DocumentID(fmt.Sprintf("%s/%s", JobCollectionName, id)),
|
||||
},
|
||||
Msg: fmt.Sprintf("Set output"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cur.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) JobDelete(ctx context.Context, id string) error {
|
||||
_, err := db.jobCollection.RemoveDocument(ctx, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Database) JobList(ctx context.Context) ([]*models.JobResponse, error) {
|
||||
query := "FOR d IN @@collection RETURN d"
|
||||
cursor, _, err := db.Query(ctx, query, map[string]interface{}{"@collection": JobCollectionName}, busdb.ReadOperation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close()
|
||||
var docs []*models.JobResponse
|
||||
for {
|
||||
var doc models.Job
|
||||
meta, err := cursor.ReadDocument(ctx, &doc)
|
||||
if driver.IsNoMoreDocuments(err) {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
job, err := db.toJobResponse(ctx, meta.Key, &doc, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
docs = append(docs, job)
|
||||
}
|
||||
|
||||
return docs, err
|
||||
}
|
||||
|
||||
func publishJobMapping(id, automation string, contextStructs *models.Context, origin *models.Origin, payloadMapping map[string]string, db *Database) error {
|
||||
msg, err := generatePayload(payloadMapping, contextStructs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("message generation failed: %w", err)
|
||||
}
|
||||
|
||||
return publishJob(id, automation, contextStructs, origin, msg, db)
|
||||
}
|
||||
|
||||
func publishJob(id, automation string, contextStructs *models.Context, origin *models.Origin, payload map[string]interface{}, db *Database) error {
|
||||
return db.bus.PublishJob(id, automation, payload, contextStructs, origin)
|
||||
}
|
||||
|
||||
func generatePayload(msgMapping map[string]string, contextStructs *models.Context) (map[string]interface{}, error) {
|
||||
contextJson, err := json.Marshal(contextStructs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
automationContext := map[string]interface{}{}
|
||||
err = json.Unmarshal(contextJson, &automationContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parser := caql.Parser{}
|
||||
msg := map[string]interface{}{}
|
||||
for arg, expr := range msgMapping {
|
||||
tree, err := parser.Parse(expr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, err := tree.Eval(automationContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msg[arg] = v
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
25
database/migrations/automations/comment.py
Normal file
25
database/migrations/automations/comment.py
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
subprocess.call(
|
||||
[sys.executable, "-m", "pip", "install", "requests"],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
import json
|
||||
import requests
|
||||
|
||||
|
||||
def run(msg):
|
||||
if "ticket" in msg["context"]:
|
||||
headers = {"PRIVATE-TOKEN": msg["secrets"]["catalyst_apikey"]}
|
||||
url = "%s/tickets/%d/comments" % (msg["secrets"]["catalyst_apiurl"], msg["context"]["ticket"]["id"])
|
||||
data = {'message': msg["payload"]["default"], 'creator': 'automation'}
|
||||
requests.post(url, json=data, headers=headers).json()
|
||||
|
||||
return {"done": True}
|
||||
|
||||
|
||||
print(json.dumps(run(json.loads(sys.argv[1]))))
|
||||
13
database/migrations/automations/hash.sha1.py
Executable file
13
database/migrations/automations/hash.sha1.py
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
import json
|
||||
import hashlib
|
||||
|
||||
|
||||
def run(msg):
|
||||
sha1 = hashlib.sha1(msg['payload']['default'].encode('utf-8'))
|
||||
return {"hash": sha1.hexdigest()}
|
||||
|
||||
|
||||
print(json.dumps(run(json.loads(sys.argv[1]))))
|
||||
630
database/migrations/automations/thehive.py
Normal file
630
database/migrations/automations/thehive.py
Normal file
@@ -0,0 +1,630 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import json
|
||||
from datetime import datetime
|
||||
import io
|
||||
|
||||
subprocess.check_call(
|
||||
[sys.executable, "-m", "pip", "install", "thehive4py", "requests", "minio"],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
defaultschema = {
|
||||
"definitions": {},
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://example.com/object1618746510.json",
|
||||
"title": "Default",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"severity",
|
||||
"description",
|
||||
"summary",
|
||||
"tlp",
|
||||
"pap"
|
||||
],
|
||||
"properties": {
|
||||
"severity": {
|
||||
"$id": "#root/severity",
|
||||
"title": "Severity",
|
||||
"type": "string",
|
||||
"default": "Medium",
|
||||
"x-cols": 6,
|
||||
"x-class": "pr-2",
|
||||
"x-display": "icon",
|
||||
"x-itemIcon": "icon",
|
||||
"oneOf": [
|
||||
{
|
||||
"const": "Unknown",
|
||||
"title": "Unknown",
|
||||
"icon": "mdi-help"
|
||||
},
|
||||
{
|
||||
"const": "Low",
|
||||
"title": "Low",
|
||||
"icon": "mdi-chevron-up"
|
||||
},
|
||||
{
|
||||
"const": "Medium",
|
||||
"title": "Medium",
|
||||
"icon": "mdi-chevron-double-up"
|
||||
},
|
||||
{
|
||||
"const": "High",
|
||||
"title": "High",
|
||||
"icon": "mdi-chevron-triple-up"
|
||||
},
|
||||
{
|
||||
"const": "Very High",
|
||||
"title": "Very High",
|
||||
"icon": "mdi-exclamation"
|
||||
}
|
||||
]
|
||||
},
|
||||
"flag": {
|
||||
"title": "Flag",
|
||||
"type": "boolean",
|
||||
"x-cols": 6,
|
||||
},
|
||||
"tlp": {
|
||||
"$id": "#root/tlp",
|
||||
"title": "TLP",
|
||||
"type": "string",
|
||||
"x-cols": 6,
|
||||
"x-class": "pr-2",
|
||||
"x-display": "icon",
|
||||
"x-itemIcon": "icon",
|
||||
"oneOf": [
|
||||
{
|
||||
"const": "White",
|
||||
"title": "White",
|
||||
"icon": "mdi-alpha-w"
|
||||
},
|
||||
{
|
||||
"const": "Green",
|
||||
"title": "Green",
|
||||
"icon": "mdi-alpha-g"
|
||||
},
|
||||
{
|
||||
"const": "Amber",
|
||||
"title": "Amber",
|
||||
"icon": "mdi-alpha-a"
|
||||
},
|
||||
{
|
||||
"const": "Red",
|
||||
"title": "Red",
|
||||
"icon": "mdi-alpha-r"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pap": {
|
||||
"$id": "#root/pap",
|
||||
"title": "PAP",
|
||||
"type": "string",
|
||||
"x-cols": 6,
|
||||
"x-class": "pr-2",
|
||||
"x-display": "icon",
|
||||
"x-itemIcon": "icon",
|
||||
"oneOf": [
|
||||
{
|
||||
"const": "White",
|
||||
"title": "White",
|
||||
"icon": "mdi-alpha-w"
|
||||
},
|
||||
{
|
||||
"const": "Green",
|
||||
"title": "Green",
|
||||
"icon": "mdi-alpha-g"
|
||||
},
|
||||
{
|
||||
"const": "Amber",
|
||||
"title": "Amber",
|
||||
"icon": "mdi-alpha-a"
|
||||
},
|
||||
{
|
||||
"const": "Red",
|
||||
"title": "Red",
|
||||
"icon": "mdi-alpha-r"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tags": {
|
||||
"$id": "#root/tags",
|
||||
"title": "Tags",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"$id": "#root/description",
|
||||
"title": "Description",
|
||||
"type": "string",
|
||||
"x-display": "textarea",
|
||||
"x-class": "pr-2"
|
||||
},
|
||||
"resolutionStatus": {
|
||||
"$id": "#root/resolutionStatus",
|
||||
"title": "Resolution Status",
|
||||
"type": "string",
|
||||
"x-cols": 6,
|
||||
"x-class": "pr-2",
|
||||
},
|
||||
"endDate": {
|
||||
"$id": "#root/endDate",
|
||||
"title": "End Data",
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"x-cols": 6,
|
||||
"x-class": "pr-2",
|
||||
},
|
||||
"summary": {
|
||||
"$id": "#root/summary",
|
||||
"title": "Summary",
|
||||
"type": "string",
|
||||
"x-display": "textarea",
|
||||
"x-class": "pr-2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaultalertschema = {
|
||||
"definitions": {},
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://example.com/object1618746510.json",
|
||||
"title": "Default",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"severity",
|
||||
"description",
|
||||
"summary",
|
||||
"tlp",
|
||||
"pap"
|
||||
],
|
||||
"properties": {
|
||||
"severity": {
|
||||
"$id": "#root/severity",
|
||||
"title": "Severity",
|
||||
"type": "string",
|
||||
"default": "Medium",
|
||||
"x-cols": 6,
|
||||
"x-class": "pr-2",
|
||||
"x-display": "icon",
|
||||
"x-itemIcon": "icon",
|
||||
"oneOf": [
|
||||
{
|
||||
"const": "Unknown",
|
||||
"title": "Unknown",
|
||||
"icon": "mdi-help"
|
||||
},
|
||||
{
|
||||
"const": "Low",
|
||||
"title": "Low",
|
||||
"icon": "mdi-chevron-up"
|
||||
},
|
||||
{
|
||||
"const": "Medium",
|
||||
"title": "Medium",
|
||||
"icon": "mdi-chevron-double-up"
|
||||
},
|
||||
{
|
||||
"const": "High",
|
||||
"title": "High",
|
||||
"icon": "mdi-chevron-triple-up"
|
||||
},
|
||||
{
|
||||
"const": "Very High",
|
||||
"title": "Very High",
|
||||
"icon": "mdi-exclamation"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tlp": {
|
||||
"$id": "#root/tlp",
|
||||
"title": "TLP",
|
||||
"type": "string",
|
||||
"x-cols": 6,
|
||||
"x-class": "pr-2",
|
||||
"x-display": "icon",
|
||||
"x-itemIcon": "icon",
|
||||
"oneOf": [
|
||||
{
|
||||
"const": "White",
|
||||
"title": "White",
|
||||
"icon": "mdi-alpha-w"
|
||||
},
|
||||
{
|
||||
"const": "Green",
|
||||
"title": "Green",
|
||||
"icon": "mdi-alpha-g"
|
||||
},
|
||||
{
|
||||
"const": "Amber",
|
||||
"title": "Amber",
|
||||
"icon": "mdi-alpha-a"
|
||||
},
|
||||
{
|
||||
"const": "Red",
|
||||
"title": "Red",
|
||||
"icon": "mdi-alpha-r"
|
||||
}
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"$id": "#root/source",
|
||||
"title": "Source",
|
||||
"type": "string",
|
||||
"x-cols": 4,
|
||||
"x-class": "pr-2",
|
||||
},
|
||||
"sourceRef": {
|
||||
"$id": "#root/sourceRef",
|
||||
"title": "Source Ref",
|
||||
"type": "string",
|
||||
"x-cols": 4,
|
||||
"x-class": "pr-2",
|
||||
},
|
||||
"type": {
|
||||
"$id": "#root/type",
|
||||
"title": "Type",
|
||||
"type": "string",
|
||||
"x-cols": 4,
|
||||
"x-class": "pr-2",
|
||||
},
|
||||
"description": {
|
||||
"$id": "#root/description",
|
||||
"title": "Description",
|
||||
"type": "string",
|
||||
"x-display": "textarea",
|
||||
"x-class": "pr-2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class schema:
|
||||
def __init__(self):
|
||||
self.schema = defaultschema
|
||||
|
||||
def add_string(self, title):
|
||||
self.schema["properties"][title] = { "type": "string", "x-cols": 6, "x-class": "pr-2" }
|
||||
|
||||
def add_boolean(self, title):
|
||||
self.schema["properties"][title] = { "type": "boolean", "x-cols": 6, "x-class": "pr-2" }
|
||||
|
||||
def add_date(self, title):
|
||||
self.schema["properties"][title] = { "type": "string", "format": "date-time", "x-cols": 6, "x-class": "pr-2" }
|
||||
|
||||
def add_integer(self, title):
|
||||
self.schema["properties"][title] = { "type": "integer", "x-cols": 6, "x-class": "pr-2" }
|
||||
|
||||
def add_float(self, title):
|
||||
self.schema["properties"][title] = { "type": "number", "x-cols": 6, "x-class": "pr-2" }
|
||||
|
||||
|
||||
class alertschema:
|
||||
def __init__(self):
|
||||
self.schema = defaultalertschema
|
||||
|
||||
|
||||
def maptime(hivetime):
|
||||
if hivetime is None:
|
||||
return None
|
||||
return datetime.fromtimestamp(hivetime/1000).isoformat() + "Z"
|
||||
|
||||
|
||||
def mapstatus(hivestatus):
|
||||
if hivestatus == "Open" or hivestatus == "New":
|
||||
return "open"
|
||||
return "closed"
|
||||
|
||||
|
||||
def maptlp(hivetlp):
|
||||
if hivetlp == 0:
|
||||
return "White"
|
||||
if hivetlp == 1:
|
||||
return "Green"
|
||||
if hivetlp == 2:
|
||||
return "Amber"
|
||||
if hivetlp == 3:
|
||||
return "Red"
|
||||
return "White"
|
||||
|
||||
|
||||
def mapseverity(hiveseverity):
|
||||
if hiveseverity == 1:
|
||||
return "Low"
|
||||
if hiveseverity == 2:
|
||||
return "Medium"
|
||||
if hiveseverity == 3:
|
||||
return "High"
|
||||
if hiveseverity == 4:
|
||||
return "Very High"
|
||||
return "Unknown"
|
||||
|
||||
# {
|
||||
# "_id": "~16416",
|
||||
# "id": "~16416",
|
||||
# "createdBy": "jonas@thehive.local",
|
||||
# "updatedBy": "jonas@thehive.local",
|
||||
# "createdAt": 1638704013583,
|
||||
# "updatedAt": 1638704061151,
|
||||
# "_type": "case",
|
||||
# "caseId": 1,
|
||||
# "title": "My Test 1",
|
||||
# "description": "My Testcase",
|
||||
# "severity": 2,
|
||||
# "startDate": 1638703980000,
|
||||
# "endDate": null,
|
||||
# "impactStatus": null,
|
||||
# "resolutionStatus": null,
|
||||
# "tags": [],
|
||||
# "flag": false,
|
||||
# "tlp": 2,
|
||||
# "pap": 2,
|
||||
# "status": "Open",
|
||||
# "summary": null,
|
||||
# "owner": "jonas@thehive.local",
|
||||
# "customFields": {},
|
||||
# "stats": {},
|
||||
# "permissions": [ "manageShare", "manageAnalyse", "manageTask", "manageCaseTemplate", "manageCase", "manageUser", "manageProcedure", "managePage", "manageObservable", "manageTag", "manageConfig", "manageAlert", "accessTheHiveFS", "manageAction" ]
|
||||
# }
|
||||
def mapcase(hivecase, url, keep_ids):
|
||||
|
||||
s = schema()
|
||||
details = {}
|
||||
for name, data in hivecase["customFields"].items():
|
||||
if "string" in data and data["string"] is not None:
|
||||
s.add_string(name)
|
||||
details[name] = data["string"]
|
||||
if "boolean" in data and data["boolean"] is not None:
|
||||
s.add_boolean(name)
|
||||
details[name] = data["boolean"]
|
||||
if "date" in data and data["date"] is not None:
|
||||
s.add_date(name)
|
||||
details[name] = maptime(data["date"])
|
||||
if "integer" in data and data["integer"] is not None:
|
||||
s.add_integer(name)
|
||||
details[name] = data["integer"]
|
||||
if "float" in data and data["float"] is not None:
|
||||
s.add_float(name)
|
||||
details[name] = data["float"]
|
||||
|
||||
case = {}
|
||||
if keep_ids:
|
||||
case["id"] = hivecase["caseId"]
|
||||
|
||||
return {
|
||||
"name": hivecase["title"],
|
||||
"type": "incident",
|
||||
"status": mapstatus(hivecase["status"]),
|
||||
|
||||
"owner": hivecase["owner"],
|
||||
# "write": hivecase["write"],
|
||||
# "read": hivecase["read"],
|
||||
|
||||
"schema": json.dumps(s.schema),
|
||||
"details": {
|
||||
"tlp": maptlp(hivecase["tlp"]),
|
||||
"pap": maptlp(hivecase["pap"]),
|
||||
"severity": mapseverity(hivecase["severity"]),
|
||||
"description": hivecase["description"],
|
||||
"summary": hivecase["summary"],
|
||||
"tags": hivecase["tags"],
|
||||
"endDate": maptime(hivecase["endDate"]),
|
||||
"resolutionStatus": hivecase["resolutionStatus"],
|
||||
"flag": hivecase["flag"],
|
||||
} | details,
|
||||
"references": [
|
||||
{ "name": "TheHive #%d" % hivecase["caseId"], "href": "%s/index.html#!/case/~%s/details" % (url, hivecase["id"]) }
|
||||
],
|
||||
#
|
||||
# "playbooks": hivecase["playbooks"],
|
||||
#
|
||||
"files": [],
|
||||
"comments": [],
|
||||
# creator, created, message
|
||||
#
|
||||
"artifacts": [],
|
||||
# name, type, status, enrichment
|
||||
# name, data
|
||||
|
||||
"created": maptime(hivecase["createdAt"]),
|
||||
"modified": maptime(hivecase["updatedAt"]),
|
||||
} | case
|
||||
|
||||
# {
|
||||
# "_id": "ce2c00f17132359cb3c50dfbb1901810",
|
||||
# "_type": "alert",
|
||||
# "artifacts": [],
|
||||
# "createdAt": 1495012062014,
|
||||
# "createdBy": "myuser",
|
||||
# "date": 1495012062016,
|
||||
# "description": "N/A",
|
||||
# "follow": true,
|
||||
# "id": "ce2c00f17132359cb3c50dfbb1901810",
|
||||
# "lastSyncDate": 1495012062016,
|
||||
# "severity": 2,
|
||||
# "source": "instance1",
|
||||
# "sourceRef": "alert-ref",
|
||||
# "status": "New",
|
||||
# "title": "New Alert",
|
||||
# "tlp": 2,
|
||||
# "type": "external",
|
||||
# "user": "myuser"
|
||||
# }
|
||||
def mapalert(hivealert, url):
|
||||
s = alertschema()
|
||||
details = {}
|
||||
|
||||
return {
|
||||
"name": hivealert["title"],
|
||||
"type": "alert",
|
||||
"status": mapstatus(hivealert["status"]),
|
||||
"owner": hivealert["user"],
|
||||
"schema": json.dumps(s.schema),
|
||||
"details": {
|
||||
"tlp": maptlp(hivealert["tlp"]),
|
||||
"severity": mapseverity(hivealert["severity"]),
|
||||
"description": hivealert["description"],
|
||||
"source": hivealert["source"],
|
||||
"sourceRef": hivealert["sourceRef"],
|
||||
"type": hivealert["type"],
|
||||
} | details,
|
||||
"references": [
|
||||
{ "name": "TheHive Alerts", "href": "%s/index.html#!/alert/list" % url }
|
||||
],
|
||||
"files": [],
|
||||
"comments": [],
|
||||
"artifacts": [],
|
||||
"created": maptime(hivealert["createdAt"]),
|
||||
"modified": maptime(hivealert["lastSyncDate"]),
|
||||
}
|
||||
|
||||
# {
|
||||
# "_id": "~41152",
|
||||
# "id": "~41152",
|
||||
# "createdBy": "jonas@thehive.local",
|
||||
# "createdAt": 1638723814523,
|
||||
# "_type": "case_artifact",
|
||||
# "dataType": "ip",
|
||||
# "data": "2.2.2.2",
|
||||
# "startDate": 1638723814523,
|
||||
# "tlp": 2,
|
||||
# "tags": [],
|
||||
# "ioc": false,
|
||||
# "sighted": false,
|
||||
# "message": ".",
|
||||
# "reports": {},
|
||||
# "stats": {},
|
||||
# "ignoreSimilarity": false
|
||||
# }
|
||||
def mapobservable(hiveobservable):
|
||||
status = "unknown"
|
||||
if hiveobservable["ioc"]:
|
||||
status = "malicious"
|
||||
return {
|
||||
"name": hiveobservable["data"],
|
||||
"type": hiveobservable["dataType"],
|
||||
"status": status,
|
||||
}
|
||||
|
||||
# {
|
||||
# "id": "~12296",
|
||||
# "_id": "~12296",
|
||||
# "createdBy": "jonas@thehive.local",
|
||||
# "createdAt": 1638704029800,
|
||||
# "_type": "case_task",
|
||||
# "title": "Start",
|
||||
# "group": "MyTaskGroup1",
|
||||
# "owner": "jonas@thehive.local",
|
||||
# "status": "InProgress",
|
||||
# "flag": false,
|
||||
# "startDate": 1638704115667,
|
||||
# "order": 0
|
||||
# }
|
||||
# {
|
||||
# "_id": "~24656",
|
||||
# "id": "~24656",
|
||||
# "createdBy": "jonas@thehive.local",
|
||||
# "createdAt": 1638729992590,
|
||||
# "_type": "case_task_log",
|
||||
# "message": "asd",
|
||||
# "startDate": 1638729992590,
|
||||
# "attachment": {
|
||||
# "name": "Chemistry Vector.eps",
|
||||
# "hashes": [
|
||||
# "adf2d4cd72f4141fe7f8eb4af035596415a29c048d3039be6449008f291258e9",
|
||||
# "180f66a6d22b1f09ed198afd814f701e42440e7c",
|
||||
# "b28ae347371df003b76cbb8c6199c97e"
|
||||
# ],
|
||||
# "size": 3421842,
|
||||
# "contentType": "application/postscript",
|
||||
# "id": "adf2d4cd72f4141fe7f8eb4af035596415a29c048d3039be6449008f291258e9"
|
||||
# },
|
||||
# "status": "Ok",
|
||||
# "owner": "jonas@thehive.local"
|
||||
# }
|
||||
def maptasklog(hivetask, hivetasklog):
|
||||
message = "**" + hivetask["group"] + ": " + hivetask["title"] + "** (" + hivetask["status"] + ")\n\n"
|
||||
message += hivetasklog["message"]
|
||||
if 'attachment' in hivetasklog:
|
||||
message += "\n\n*Attachment*: " + hivetasklog['attachment']["name"]
|
||||
return {
|
||||
"creator": hivetasklog["createdBy"],
|
||||
"created": maptime(hivetasklog["createdAt"]),
|
||||
"message": message,
|
||||
}
|
||||
|
||||
|
||||
def run(msg):
|
||||
skip_files = msg["payload"]["skip_files"]
|
||||
keep_ids = msg["payload"]["keep_ids"]
|
||||
|
||||
from thehive4py.api import TheHiveApi
|
||||
import requests
|
||||
from minio import Minio
|
||||
|
||||
headers = {"PRIVATE-TOKEN": msg["secrets"]["catalyst_apikey"]}
|
||||
# minioclient = Minio("try.catalyst-soar.com:9000", access_key="minio", secret_key="password")
|
||||
if not skip_files:
|
||||
minioclient = Minio(
|
||||
msg["secrets"]["minio_host"],
|
||||
access_key=msg["secrets"]["minio_access_key"],
|
||||
secret_key=msg["secrets"]["minio_secret_key"])
|
||||
|
||||
# url = "http://localhost:9000"
|
||||
url = msg["payload"]["thehiveurl"]
|
||||
# api = TheHiveApi(url, "dtUCnzY4h291GIFHJKW/Z2I2SgjTRQqo")
|
||||
api = TheHiveApi(url, msg["payload"]["thehivekey"])
|
||||
|
||||
print("find alerts", file=sys.stderr)
|
||||
alerts = []
|
||||
resp = api.find_alerts(query={}, sort=['-createdAt'], range='all')
|
||||
resp.raise_for_status()
|
||||
for alert in resp.json():
|
||||
alerts.append(mapalert(alert, url))
|
||||
|
||||
if alerts:
|
||||
print("create %s alerts" % len(alerts), file=sys.stderr)
|
||||
response = requests.post(msg["secrets"]["catalyst_apiurl"] + "/tickets/batch", json=alerts, headers=headers)
|
||||
response.raise_for_status()
|
||||
|
||||
print("find incidents", file=sys.stderr)
|
||||
incidents = []
|
||||
resp = api.find_cases(query={}, sort=['-createdAt'], range='all')
|
||||
resp.raise_for_status()
|
||||
for case in resp.json():
|
||||
incident = mapcase(case, url, keep_ids)
|
||||
for observable in api.get_case_observables(case["id"]).json():
|
||||
incident["artifacts"].append(mapobservable(observable))
|
||||
for task in api.get_case_tasks(case["id"]).json():
|
||||
for log in api.get_task_logs(task["id"]).json():
|
||||
incident["comments"].append(maptasklog(task, log))
|
||||
if 'attachment' in log and not skip_files:
|
||||
incident["files"].append({ "key": log['attachment']["id"], "name": log['attachment']["name"] })
|
||||
|
||||
bucket_name = "catalyst-%d" % incident["id"]
|
||||
if not minioclient.bucket_exists(bucket_name):
|
||||
minioclient.make_bucket(bucket_name)
|
||||
|
||||
response = api.download_attachment(log["attachment"]["id"])
|
||||
data = io.BytesIO(response.content)
|
||||
|
||||
minioclient.put_object(bucket_name, log["attachment"]["id"], data, length=-1, part_size=10*1024*1024)
|
||||
incidents.append(incident)
|
||||
|
||||
if incidents:
|
||||
if keep_ids:
|
||||
print("delete incidents", file=sys.stderr)
|
||||
for incident in incidents:
|
||||
requests.delete(msg["secrets"]["catalyst_apiurl"] + "/tickets/%d" % incident["id"], headers=headers)
|
||||
print("create %d incidents" % len(incidents), file=sys.stderr)
|
||||
response = requests.post(msg["secrets"]["catalyst_apiurl"] + "/tickets/batch", json=incidents, headers=headers)
|
||||
response.raise_for_status()
|
||||
|
||||
return {"done": True}
|
||||
|
||||
|
||||
print(json.dumps(run(json.loads(sys.argv[1]))))
|
||||
22
database/migrations/automations/vt.hash.py
Normal file
22
database/migrations/automations/vt.hash.py
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
subprocess.call(
|
||||
[sys.executable, "-m", "pip", "install", "requests"],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
import json
|
||||
import requests
|
||||
|
||||
|
||||
def run(msg):
|
||||
api_key = msg['secrets']['vt_api_key'].encode('utf-8')
|
||||
resource = msg['payload']['default'].encode('utf-8')
|
||||
params = {'apikey': api_key, 'resource': resource}
|
||||
return requests.get("https://www.virustotal.com/vtapi/v2/file/report", params=params).json()
|
||||
|
||||
|
||||
print(json.dumps(run(json.loads(sys.argv[1]))))
|
||||
27
database/migrations/content.go
Normal file
27
database/migrations/content.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package migrations
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed templates/default.json
|
||||
var DefaultTemplateSchema string
|
||||
|
||||
//go:embed automations/hash.sha1.py
|
||||
var SHA1HashAutomation string
|
||||
|
||||
//go:embed automations/vt.hash.py
|
||||
var VTHashAutomation string
|
||||
|
||||
//go:embed automations/thehive.py
|
||||
var TheHiveAutomation string
|
||||
|
||||
//go:embed automations/comment.py
|
||||
var CommentAutomation string
|
||||
|
||||
//go:embed playbooks/malware.yml
|
||||
var MalwarePlaybook string
|
||||
|
||||
//go:embed playbooks/phishing.yml
|
||||
var PhishingPlaybook string
|
||||
|
||||
//go:embed playbooks/simple.yaml
|
||||
var SimplePlaybook string
|
||||
217
database/migrations/migrations.go
Normal file
217
database/migrations/migrations.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/arangodb/go-driver"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/database/busdb"
|
||||
"github.com/SecurityBrewery/catalyst/generated/models"
|
||||
"github.com/SecurityBrewery/catalyst/pointer"
|
||||
)
|
||||
|
||||
const MigrationCollection string = "migrations"
|
||||
|
||||
type Migration interface {
|
||||
MID() string
|
||||
Migrate(ctx context.Context, driver driver.Database) error
|
||||
}
|
||||
|
||||
func generateMigrations() ([]Migration, error) {
|
||||
// content here should never change
|
||||
return []Migration{
|
||||
&createCollection{ID: "create-log-collection", Name: "logs", DataType: "log", Schema: `{"properties":{"created":{"format":"date-time","type":"string"},"creator":{"type":"string"},"message":{"type":"string"},"reference":{"type":"string"}},"required":["created","creator","message","reference"],"type":"object"}`},
|
||||
&createCollection{ID: "create-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"}},"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-template-collection", Name: "templates", DataType: "template", Schema: `{"properties":{"name":{"type":"string"},"schema":{"type":"string"}},"required":["name","schema"],"type":"object"}`},
|
||||
&createCollection{ID: "create-playbook-collection", Name: "playbooks", DataType: "playbook", Schema: `{"properties":{"name":{"type":"string"},"yaml":{"type":"string"}},"required":["name","yaml"],"type":"object"}`},
|
||||
&createCollection{ID: "create-automation-collection", Name: "automations", DataType: "automation", Schema: `{"properties":{"image":{"type":"string"},"script":{"type":"string"}},"required":["image","script"],"type":"object"}`},
|
||||
&createCollection{ID: "create-userdata-collection", Name: "userdata", DataType: "userdata", Schema: `{"properties":{"email":{"type":"string"},"image":{"type":"string"},"name":{"type":"string"},"timeformat":{"title":"Time Format (https://moment.github.io/luxon/docs/manual/formatting.html#table-of-tokens)","type":"string"}},"type":"object"}`},
|
||||
&createCollection{ID: "create-tickettype-collection", Name: "tickettypes", DataType: "tickettype", Schema: `{"properties":{"default_groups":{"items":{"type":"string"},"type":"array"},"default_playbooks":{"items":{"type":"string"},"type":"array"},"default_template":{"type":"string"},"icon":{"type":"string"},"name":{"type":"string"}},"required":["default_playbooks","default_template","icon","name"],"type":"object"}`},
|
||||
&createCollection{ID: "create-user-collection", Name: "users", DataType: "user", Schema: `{"properties":{"apikey":{"type":"boolean"},"blocked":{"type":"boolean"},"roles":{"items":{"type":"string"},"type":"array"},"sha256":{"type":"string"}},"required":["apikey","blocked","roles"],"type":"object"}`},
|
||||
|
||||
&createGraph{ID: "create-ticket-graph", Name: "Graph", EdgeDefinitions: []driver.EdgeDefinition{{Collection: "related", From: []string{"tickets"}, To: []string{"tickets"}}}},
|
||||
|
||||
&createDocument{ID: "create-template-default", Collection: "templates", Document: &busdb.Keyed{Key: "default", Doc: models.TicketTemplate{Schema: DefaultTemplateSchema, Name: "Default"}}},
|
||||
&createDocument{ID: "create-automation-vt.hash", Collection: "automations", Document: &busdb.Keyed{Key: "vt.hash", Doc: models.Automation{Image: "docker.io/python:3", Script: VTHashAutomation}}},
|
||||
&createDocument{ID: "create-automation-comment", Collection: "automations", Document: &busdb.Keyed{Key: "comment", Doc: models.Automation{Image: "docker.io/python:3", Script: CommentAutomation}}},
|
||||
&createDocument{ID: "create-automation-thehive", Collection: "automations", Document: &busdb.Keyed{Key: "thehive", Doc: models.Automation{Image: "docker.io/python:3", Script: TheHiveAutomation}}},
|
||||
&createDocument{ID: "create-automation-hash.sha1", Collection: "automations", Document: &busdb.Keyed{Key: "hash.sha1", Doc: models.Automation{Image: "docker.io/python:3", Script: SHA1HashAutomation}}},
|
||||
&createDocument{ID: "create-playbook-malware", Collection: "playbooks", Document: &busdb.Keyed{Key: "malware", Doc: models.PlaybookTemplate{Name: "Malware", Yaml: MalwarePlaybook}}},
|
||||
&createDocument{ID: "create-playbook-phishing", Collection: "playbooks", Document: &busdb.Keyed{Key: "phishing", Doc: models.PlaybookTemplate{Name: "Phishing", Yaml: PhishingPlaybook}}},
|
||||
&createDocument{ID: "create-tickettype-alert", Collection: "tickettypes", Document: &busdb.Keyed{Key: "alert", Doc: models.TicketType{Name: "Alerts", Icon: "mdi-alert", DefaultTemplate: "default", DefaultPlaybooks: []string{}, DefaultGroups: nil}}},
|
||||
&createDocument{ID: "create-tickettype-incident", Collection: "tickettypes", Document: &busdb.Keyed{Key: "incident", Doc: models.TicketType{Name: "Incidents", Icon: "mdi-radioactive", DefaultTemplate: "default", DefaultPlaybooks: []string{}, DefaultGroups: nil}}},
|
||||
&createDocument{ID: "create-tickettype-investigation", Collection: "tickettypes", Document: &busdb.Keyed{Key: "investigation", Doc: models.TicketType{Name: "Forensic Investigations", Icon: "mdi-fingerprint", DefaultTemplate: "default", DefaultPlaybooks: []string{}, DefaultGroups: nil}}},
|
||||
&createDocument{ID: "create-tickettype-hunt", Collection: "tickettypes", Document: &busdb.Keyed{Key: "hunt", Doc: models.TicketType{Name: "Threat Hunting", Icon: "mdi-target", DefaultTemplate: "default", DefaultPlaybooks: []string{}, DefaultGroups: nil}}},
|
||||
|
||||
&updateSchema{ID: "update-automation-collection-1", Name: "automations", DataType: "automation", Schema: `{"properties":{"image":{"type":"string"},"script":{"type":"string"}},"required":["image","script"],"type":"object"}`},
|
||||
&updateDocument{ID: "update-automation-vt.hash-1", Collection: "automations", Key: "vt.hash", Document: models.Automation{Image: "docker.io/python:3", Script: VTHashAutomation, Schema: pointer.String(`{"title":"Input","type":"object","properties":{"default":{"type":"string","title":"Value"}},"required":["default"]}`), Type: []string{"global", "artifact", "playbook"}}},
|
||||
&updateDocument{ID: "update-automation-comment-1", Collection: "automations", Key: "comment", Document: models.Automation{Image: "docker.io/python:3", Script: CommentAutomation, Type: []string{"playbook"}}},
|
||||
&updateDocument{ID: "update-automation-thehive-1", Collection: "automations", Key: "thehive", Document: models.Automation{Image: "docker.io/python:3", Script: TheHiveAutomation, Schema: pointer.String(`{"title":"TheHive credentials","type":"object","properties":{"thehiveurl":{"type":"string","title":"TheHive URL (e.g. 'https://thehive.example.org')"},"thehivekey":{"type":"string","title":"TheHive API Key"},"skip_files":{"type":"boolean", "default": true, "title":"Skip Files (much faster)"},"keep_ids":{"type":"boolean", "default": true, "title":"Keep IDs and overwrite existing IDs"}},"required":["thehiveurl", "thehivekey", "skip_files", "keep_ids"]}`), Type: []string{"global"}}},
|
||||
&updateDocument{ID: "update-automation-hash.sha1-1", Collection: "automations", Key: "hash.sha1", Document: models.Automation{Image: "docker.io/python:3", Script: SHA1HashAutomation, Schema: pointer.String(`{"title":"Input","type":"object","properties":{"default":{"type":"string","title":"Value"}},"required":["default"]}`), Type: []string{"global", "artifact", "playbook"}}},
|
||||
|
||||
&createCollection{ID: "create-job-collection", Name: "jobs", DataType: "job", Schema: `{"properties":{"automation":{"type":"string"},"log":{"type":"string"},"payload":{},"origin":{"properties":{"artifact_origin":{"properties":{"artifact":{"type":"string"},"ticket_id":{"format":"int64","type":"integer"}},"required":["artifact","ticket_id"],"type":"object"},"task_origin":{"properties":{"playbook_id":{"type":"string"},"task_id":{"type":"string"},"ticket_id":{"format":"int64","type":"integer"}},"required":["playbook_id","task_id","ticket_id"],"type":"object"}},"type":"object"},"output":{"properties":{},"type":"object"},"running":{"type":"boolean"},"status":{"type":"string"}},"required":["automation","running","status"],"type":"object"}`},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func loadSchema(dataType, jsonschema string) (*driver.CollectionSchemaOptions, error) {
|
||||
ticketCollectionSchema := &driver.CollectionSchemaOptions{Level: driver.CollectionSchemaLevelStrict, Message: fmt.Sprintf("Validation of %s failed", dataType)}
|
||||
|
||||
err := ticketCollectionSchema.LoadRule([]byte(jsonschema))
|
||||
return ticketCollectionSchema, err
|
||||
}
|
||||
|
||||
type migration struct {
|
||||
Key string `json:"_key"`
|
||||
}
|
||||
|
||||
func PerformMigrations(ctx context.Context, db driver.Database) error {
|
||||
collection, err := db.Collection(ctx, MigrationCollection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migrations, err := generateMigrations()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not generate migrations: %w", err)
|
||||
}
|
||||
|
||||
for _, m := range migrations {
|
||||
migrationRan, err := collection.DocumentExists(ctx, m.MID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !migrationRan {
|
||||
if err := m.Migrate(ctx, db); err != nil {
|
||||
return fmt.Errorf("migration %s failed: %w", m.MID(), err)
|
||||
}
|
||||
|
||||
if _, err := collection.CreateDocument(ctx, &migration{Key: m.MID()}); err != nil {
|
||||
return fmt.Errorf("could not save %s migration document: %w", m.MID(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type createCollection struct {
|
||||
ID string
|
||||
Name string
|
||||
DataType string
|
||||
Schema string
|
||||
}
|
||||
|
||||
func (m *createCollection) MID() string {
|
||||
return m.ID
|
||||
}
|
||||
|
||||
func (m *createCollection) Migrate(ctx context.Context, db driver.Database) error {
|
||||
schema, err := loadSchema(m.DataType, m.Schema)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.CreateCollection(ctx, m.Name, &driver.CreateCollectionOptions{
|
||||
Schema: schema,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type updateSchema struct {
|
||||
ID string
|
||||
Name string
|
||||
DataType string
|
||||
Schema string
|
||||
}
|
||||
|
||||
func (m *updateSchema) MID() string {
|
||||
return m.ID
|
||||
}
|
||||
|
||||
func (m *updateSchema) Migrate(ctx context.Context, db driver.Database) error {
|
||||
schema, err := loadSchema(m.DataType, m.Schema)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
col, err := db.Collection(ctx, m.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = col.SetProperties(ctx, driver.SetCollectionPropertiesOptions{
|
||||
Schema: schema,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type createGraph struct {
|
||||
ID string
|
||||
Name string
|
||||
EdgeDefinitions []driver.EdgeDefinition
|
||||
}
|
||||
|
||||
func (m *createGraph) MID() string {
|
||||
return m.ID
|
||||
}
|
||||
|
||||
func (m *createGraph) Migrate(ctx context.Context, db driver.Database) error {
|
||||
_, err := db.CreateGraph(ctx, m.Name, &driver.CreateGraphOptions{
|
||||
EdgeDefinitions: m.EdgeDefinitions,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
type createDocument struct {
|
||||
ID string
|
||||
Collection string
|
||||
Document interface{}
|
||||
}
|
||||
|
||||
func (m *createDocument) MID() string {
|
||||
return m.ID
|
||||
}
|
||||
|
||||
func (m *createDocument) Migrate(ctx context.Context, driver driver.Database) error {
|
||||
collection, err := driver.Collection(ctx, m.Collection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = collection.CreateDocument(ctx, m.Document)
|
||||
return err
|
||||
}
|
||||
|
||||
type updateDocument struct {
|
||||
ID string
|
||||
Collection string
|
||||
Key string
|
||||
Document interface{}
|
||||
}
|
||||
|
||||
func (m *updateDocument) MID() string {
|
||||
return m.ID
|
||||
}
|
||||
|
||||
func (m *updateDocument) Migrate(ctx context.Context, driver driver.Database) error {
|
||||
collection, err := driver.Collection(ctx, m.Collection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exists, err := collection.DocumentExists(ctx, m.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
_, err = collection.CreateDocument(ctx, m.Document)
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = collection.ReplaceDocument(ctx, m.Key, m.Document)
|
||||
return err
|
||||
}
|
||||
63
database/migrations/playbooks/malware.yml
Normal file
63
database/migrations/playbooks/malware.yml
Normal file
@@ -0,0 +1,63 @@
|
||||
name: Malware
|
||||
tasks:
|
||||
file-or-hash:
|
||||
name: Do you have the file or the hash?
|
||||
type: input
|
||||
schema:
|
||||
title: Malware
|
||||
type: object
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
title: "I have the"
|
||||
enum: [ "File", "Hash" ]
|
||||
next:
|
||||
enter-hash: "file == 'Hash'"
|
||||
upload: "file == 'File'"
|
||||
|
||||
enter-hash:
|
||||
name: Please enter the hash
|
||||
type: input
|
||||
schema:
|
||||
title: Malware
|
||||
type: object
|
||||
properties:
|
||||
hash:
|
||||
type: string
|
||||
title: Please enter the hash value
|
||||
minlength: 32
|
||||
next:
|
||||
virustotal: "hash != ''"
|
||||
|
||||
upload:
|
||||
name: Upload the malware
|
||||
type: input
|
||||
schema:
|
||||
title: Malware
|
||||
type: object
|
||||
properties:
|
||||
malware:
|
||||
type: object
|
||||
x-display: file
|
||||
title: Please upload the malware
|
||||
next:
|
||||
hash: "malware"
|
||||
|
||||
hash:
|
||||
name: Hash the malware
|
||||
type: automation
|
||||
automation: hash.sha1
|
||||
payload:
|
||||
default: "playbook.tasks['upload'].data['malware']"
|
||||
next:
|
||||
virustotal:
|
||||
|
||||
virustotal:
|
||||
name: Send hash to VirusTotal
|
||||
type: automation
|
||||
automation: vt.hash
|
||||
args:
|
||||
hash: "playbook.tasks['enter-hash'].data['hash'] || playbook.tasks['hash'].data['hash']"
|
||||
# next:
|
||||
# known-malware: "score > 5"
|
||||
# sandbox: "score < 6" # unknown-malware
|
||||
85
database/migrations/playbooks/phishing.yml
Normal file
85
database/migrations/playbooks/phishing.yml
Normal file
@@ -0,0 +1,85 @@
|
||||
name: Phishing
|
||||
tasks:
|
||||
board:
|
||||
name: Board Involvement?
|
||||
description: Is a board member involved?
|
||||
type: input
|
||||
schema:
|
||||
properties:
|
||||
boardInvolved:
|
||||
default: false
|
||||
title: A board member is involved.
|
||||
type: boolean
|
||||
required:
|
||||
- boardInvolved
|
||||
title: Board Involvement?
|
||||
type: object
|
||||
next:
|
||||
escalate: "boardInvolved == true"
|
||||
mail-available: "boardInvolved == false"
|
||||
|
||||
escalate:
|
||||
name: Escalate to CISO
|
||||
description: Please escalate the task to the CISO
|
||||
type: task
|
||||
|
||||
mail-available:
|
||||
name: Mail available
|
||||
type: input
|
||||
schema:
|
||||
oneOf:
|
||||
- properties:
|
||||
mail:
|
||||
title: Mail
|
||||
type: string
|
||||
x-display: textarea
|
||||
schemaKey:
|
||||
const: 'yes'
|
||||
type: string
|
||||
required:
|
||||
- mail
|
||||
title: 'Yes'
|
||||
- properties:
|
||||
schemaKey:
|
||||
const: 'no'
|
||||
type: string
|
||||
title: 'No'
|
||||
title: Mail available
|
||||
type: object
|
||||
next:
|
||||
block-sender: "schemaKey == 'yes'"
|
||||
extract-iocs: "schemaKey == 'yes'"
|
||||
search-email-gateway: "schemaKey == 'no'"
|
||||
|
||||
search-email-gateway:
|
||||
name: Search email gateway
|
||||
description: Please search email-gateway for the phishing mail.
|
||||
type: task
|
||||
next:
|
||||
extract-iocs:
|
||||
|
||||
block-sender:
|
||||
name: Block sender
|
||||
type: task
|
||||
next:
|
||||
extract-iocs:
|
||||
|
||||
extract-iocs:
|
||||
name: Extract IOCs
|
||||
description: Please insert the IOCs
|
||||
type: input
|
||||
schema:
|
||||
properties:
|
||||
iocs:
|
||||
items:
|
||||
type: string
|
||||
title: IOCs
|
||||
type: array
|
||||
title: Extract IOCs
|
||||
type: object
|
||||
next:
|
||||
block-iocs:
|
||||
|
||||
block-iocs:
|
||||
name: Block IOCs
|
||||
type: task
|
||||
37
database/migrations/playbooks/simple.yaml
Normal file
37
database/migrations/playbooks/simple.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Simple
|
||||
tasks:
|
||||
input:
|
||||
name: Enter something to hash
|
||||
type: input
|
||||
schema:
|
||||
title: Something
|
||||
type: object
|
||||
properties:
|
||||
something:
|
||||
type: string
|
||||
title: Something
|
||||
default: ""
|
||||
next:
|
||||
hash: "something != ''"
|
||||
|
||||
hash:
|
||||
name: Hash the something
|
||||
type: automation
|
||||
automation: hash.sha1
|
||||
payload:
|
||||
default: "playbook.tasks['input'].data['something']"
|
||||
next:
|
||||
comment: "hash != ''"
|
||||
|
||||
comment:
|
||||
name: Comment the hash
|
||||
type: automation
|
||||
automation: comment
|
||||
payload:
|
||||
default: "playbook.tasks['hash'].data['hash']"
|
||||
next:
|
||||
done: "done"
|
||||
|
||||
done:
|
||||
name: You can close this case now
|
||||
type: task
|
||||
208
database/migrations/templates/advanced.json
Normal file
208
database/migrations/templates/advanced.json
Normal file
@@ -0,0 +1,208 @@
|
||||
{
|
||||
"definitions": {},
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://example.com/object1618746510.json",
|
||||
"title": "Advanced",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"severity": {
|
||||
"$id": "#root/severity",
|
||||
"title": "Severity",
|
||||
"type": "string",
|
||||
"default": "Medium",
|
||||
"x-cols": 6,
|
||||
"x-class": "pr-2",
|
||||
"x-display": "icon",
|
||||
"x-itemIcon": "icon",
|
||||
"oneOf": [
|
||||
{
|
||||
"const": "Low",
|
||||
"title": "Low",
|
||||
"icon": "mdi-chevron-up"
|
||||
},
|
||||
{
|
||||
"const": "Medium",
|
||||
"title": "Medium",
|
||||
"icon": "mdi-chevron-double-up"
|
||||
},
|
||||
{
|
||||
"const": "High",
|
||||
"title": "High",
|
||||
"icon": "mdi-chevron-triple-up"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tlp": {
|
||||
"$id": "#root/tlp",
|
||||
"title": "TLP",
|
||||
"type": "string",
|
||||
"nx-enum": [
|
||||
"White",
|
||||
"Green",
|
||||
"Amber",
|
||||
"Red"
|
||||
],
|
||||
"x-cols": 6,
|
||||
"x-class": "pr-2",
|
||||
"x-display": "icon",
|
||||
"x-itemIcon": "icon",
|
||||
"oneOf": [
|
||||
{
|
||||
"const": "White",
|
||||
"title": "White",
|
||||
"icon": "mdi-alpha-w"
|
||||
},
|
||||
{
|
||||
"const": "Green",
|
||||
"title": "Green",
|
||||
"icon": "mdi-alpha-g"
|
||||
},
|
||||
{
|
||||
"const": "Amber",
|
||||
"title": "Amber",
|
||||
"icon": "mdi-alpha-a"
|
||||
},
|
||||
{
|
||||
"const": "Red",
|
||||
"title": "Red",
|
||||
"icon": "mdi-alpha-r"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"$id": "#root/description",
|
||||
"title": "Description",
|
||||
"type": "string",
|
||||
"x-display": "textarea",
|
||||
"x-class": "pr-2"
|
||||
},
|
||||
|
||||
"type": {
|
||||
"type": "object",
|
||||
"title": "Select an incident type",
|
||||
"oneOf": [
|
||||
{
|
||||
"title": "Malware",
|
||||
"properties": {
|
||||
"schemaKey": {
|
||||
"type": "string",
|
||||
"const": "malware"
|
||||
},
|
||||
"malware_type": {
|
||||
"type": "string",
|
||||
"title": "Malware Type",
|
||||
"enum": ["Ransomware", "Worm", "Virus"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Phishing",
|
||||
"properties": {
|
||||
"schemaKey": {
|
||||
"type": "string",
|
||||
"const": "phishing"
|
||||
},
|
||||
"phishing_type": {
|
||||
"type": "string",
|
||||
"title": "Phishing Type",
|
||||
"enum": ["Normal", "Spear", "Whale"]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"x-cols": 12
|
||||
},
|
||||
"apt": {
|
||||
"type": "boolean",
|
||||
"x-display": "switch",
|
||||
"title": "APT involved?",
|
||||
"x-cols": 6
|
||||
},
|
||||
"apt-group": {
|
||||
"type": "string",
|
||||
"title": "Select APT",
|
||||
"enum": ["Lazarus Group", "Equation Group", "Fancy Bear (APT 28)", "OceanLotus (APT 32)", "Other"],
|
||||
"x-if": "apt",
|
||||
"x-cols": 6
|
||||
},
|
||||
"tactics": {
|
||||
"type": "array",
|
||||
"title": "MITRE Att&ck",
|
||||
"description": "This description is used as a help message.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"oneOf": [
|
||||
{
|
||||
"title": "Reconnaissance",
|
||||
"properties": {
|
||||
"tactic": {
|
||||
"type": "string",
|
||||
"const": "reconnaissance",
|
||||
"title": "Tactic",
|
||||
"description": "The adversary is trying to gather information they can use to plan future operations."
|
||||
},
|
||||
"techniques": {
|
||||
"type": "array",
|
||||
"title": "Techniques",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"const": "T1595",
|
||||
"title": "Active Scanning",
|
||||
"description": "Adversaries may execute active reconnaissance scans to gather information that can be used during targeting. Active scans are those where the adversary probes victim infrastructure via network traffic, as opposed to other forms of reconnaissance that do not involve direct interaction."
|
||||
},
|
||||
{
|
||||
"const": "T1592",
|
||||
"title": "Gather Victim Host Information"
|
||||
}
|
||||
]
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Persistence",
|
||||
"properties": {
|
||||
"tactic": {
|
||||
"type": "string",
|
||||
"const": "persistence"
|
||||
},
|
||||
"techniques": {
|
||||
"type": "string",
|
||||
"title": "Techniques",
|
||||
"oneOf": [
|
||||
{
|
||||
"const": "T1098",
|
||||
"title": "Account Manipulation"
|
||||
},
|
||||
{
|
||||
"const": "T1197",
|
||||
"title": "BITS Jobs"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"title": "Tags",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"misp",
|
||||
"external report",
|
||||
"internal report"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["severity", "description", "tactics", "type"]
|
||||
}
|
||||
|
||||
79
database/migrations/templates/default.json
Normal file
79
database/migrations/templates/default.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"definitions": {},
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://example.com/object1618746510.json",
|
||||
"title": "Default",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"severity",
|
||||
"description",
|
||||
"tlp"
|
||||
],
|
||||
"properties": {
|
||||
"severity": {
|
||||
"$id": "#root/severity",
|
||||
"title": "Severity",
|
||||
"type": "string",
|
||||
"default": "Medium",
|
||||
"x-cols": 6,
|
||||
"x-class": "pr-2",
|
||||
"x-display": "icon",
|
||||
"x-itemIcon": "icon",
|
||||
"oneOf": [
|
||||
{
|
||||
"const": "Low",
|
||||
"title": "Low",
|
||||
"icon": "mdi-chevron-up"
|
||||
},
|
||||
{
|
||||
"const": "Medium",
|
||||
"title": "Medium",
|
||||
"icon": "mdi-chevron-double-up"
|
||||
},
|
||||
{
|
||||
"const": "High",
|
||||
"title": "High",
|
||||
"icon": "mdi-chevron-triple-up"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tlp": {
|
||||
"$id": "#root/tlp",
|
||||
"title": "TLP",
|
||||
"type": "string",
|
||||
"x-cols": 6,
|
||||
"x-class": "pr-2",
|
||||
"x-display": "icon",
|
||||
"x-itemIcon": "icon",
|
||||
"oneOf": [
|
||||
{
|
||||
"const": "White",
|
||||
"title": "White",
|
||||
"icon": "mdi-alpha-w"
|
||||
},
|
||||
{
|
||||
"const": "Green",
|
||||
"title": "Green",
|
||||
"icon": "mdi-alpha-g"
|
||||
},
|
||||
{
|
||||
"const": "Amber",
|
||||
"title": "Amber",
|
||||
"icon": "mdi-alpha-a"
|
||||
},
|
||||
{
|
||||
"const": "Red",
|
||||
"title": "Red",
|
||||
"icon": "mdi-alpha-r"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"$id": "#root/description",
|
||||
"title": "Description",
|
||||
"type": "string",
|
||||
"x-display": "textarea",
|
||||
"x-class": "pr-2"
|
||||
}
|
||||
}
|
||||
}
|
||||
3644
database/migrations/templates/veris.json
Normal file
3644
database/migrations/templates/veris.json
Normal file
File diff suppressed because it is too large
Load Diff
152
database/playbook.go
Normal file
152
database/playbook.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/arangodb/go-driver"
|
||||
"github.com/iancoleman/strcase"
|
||||
"github.com/icza/dyno"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/database/busdb"
|
||||
"github.com/SecurityBrewery/catalyst/generated/models"
|
||||
)
|
||||
|
||||
type PlaybookYAML struct {
|
||||
Name string `yaml:"name"`
|
||||
Tasks map[string]TaskYAML `yaml:"tasks"`
|
||||
}
|
||||
|
||||
type TaskYAML struct {
|
||||
Name string `yaml:"name"`
|
||||
Type string `yaml:"type"`
|
||||
Schema interface{} `yaml:"schema"`
|
||||
Automation string `yaml:"automation"`
|
||||
Payload map[string]string `yaml:"payload"`
|
||||
Next map[string]string `yaml:"next"`
|
||||
Join bool `yaml:"join"`
|
||||
}
|
||||
|
||||
func toPlaybooks(docs []*models.PlaybookTemplateForm) (map[string]*models.Playbook, error) {
|
||||
playbooks := map[string]*models.Playbook{}
|
||||
for _, doc := range docs {
|
||||
playbook, err := toPlaybook(doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if doc.ID != nil {
|
||||
playbooks[*doc.ID] = playbook
|
||||
} else {
|
||||
playbooks[strcase.ToKebab(playbook.Name)] = playbook
|
||||
}
|
||||
}
|
||||
return playbooks, nil
|
||||
}
|
||||
|
||||
func toPlaybook(doc *models.PlaybookTemplateForm) (*models.Playbook, error) {
|
||||
ticketPlaybook := &models.Playbook{}
|
||||
err := yaml.Unmarshal([]byte(doc.Yaml), ticketPlaybook)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for idx, task := range ticketPlaybook.Tasks {
|
||||
if task.Schema != nil {
|
||||
task.Schema = dyno.ConvertMapI2MapS(task.Schema.(map[string]interface{}))
|
||||
}
|
||||
task.Created = time.Now().UTC()
|
||||
ticketPlaybook.Tasks[idx] = task
|
||||
}
|
||||
return ticketPlaybook, nil
|
||||
}
|
||||
|
||||
func toPlaybookTemplateResponse(key string, doc *models.PlaybookTemplate) *models.PlaybookTemplateResponse {
|
||||
return &models.PlaybookTemplateResponse{ID: key, Name: doc.Name, Yaml: doc.Yaml}
|
||||
}
|
||||
|
||||
func (db *Database) PlaybookCreate(ctx context.Context, playbook *models.PlaybookTemplateForm) (*models.PlaybookTemplateResponse, error) {
|
||||
if playbook == nil {
|
||||
return nil, errors.New("requires playbook")
|
||||
}
|
||||
|
||||
var playbookYAML PlaybookYAML
|
||||
err := yaml.Unmarshal([]byte(playbook.Yaml), &playbookYAML)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if playbookYAML.Name == "" {
|
||||
return nil, errors.New("requires template name")
|
||||
}
|
||||
p := models.PlaybookTemplate{Name: playbookYAML.Name, Yaml: playbook.Yaml}
|
||||
|
||||
var doc models.PlaybookTemplate
|
||||
newctx := driver.WithReturnNew(ctx, &doc)
|
||||
|
||||
meta, err := db.playbookCollection.CreateDocument(ctx, newctx, strcase.ToKebab(playbookYAML.Name), p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toPlaybookTemplateResponse(meta.Key, &doc), nil
|
||||
}
|
||||
|
||||
func (db *Database) PlaybookGet(ctx context.Context, id string) (*models.PlaybookTemplateResponse, error) {
|
||||
doc := models.PlaybookTemplate{}
|
||||
meta, err := db.playbookCollection.ReadDocument(ctx, id, &doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toPlaybookTemplateResponse(meta.Key, &doc), nil
|
||||
}
|
||||
|
||||
func (db *Database) PlaybookDelete(ctx context.Context, id string) error {
|
||||
_, err := db.playbookCollection.RemoveDocument(ctx, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Database) PlaybookUpdate(ctx context.Context, id string, playbook *models.PlaybookTemplateForm) (*models.PlaybookTemplateResponse, error) {
|
||||
var pb PlaybookYAML
|
||||
err := yaml.Unmarshal([]byte(playbook.Yaml), &pb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pb.Name == "" {
|
||||
return nil, errors.New("requires template name")
|
||||
}
|
||||
|
||||
var doc models.PlaybookTemplate
|
||||
ctx = driver.WithReturnNew(ctx, &doc)
|
||||
|
||||
meta, err := db.playbookCollection.ReplaceDocument(ctx, id, models.PlaybookTemplate{Name: pb.Name, Yaml: playbook.Yaml})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toPlaybookTemplateResponse(meta.Key, &doc), nil
|
||||
}
|
||||
|
||||
func (db *Database) PlaybookList(ctx context.Context) ([]*models.PlaybookTemplateResponse, error) {
|
||||
query := "FOR d IN @@collection RETURN d"
|
||||
cursor, _, err := db.Query(ctx, query, map[string]interface{}{"@collection": PlaybookCollectionName}, busdb.ReadOperation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close()
|
||||
var docs []*models.PlaybookTemplateResponse
|
||||
for {
|
||||
var doc models.PlaybookTemplate
|
||||
meta, err := cursor.ReadDocument(ctx, &doc)
|
||||
if driver.IsNoMoreDocuments(err) {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
docs = append(docs, toPlaybookTemplateResponse(meta.Key, &doc))
|
||||
}
|
||||
|
||||
return docs, err
|
||||
}
|
||||
182
database/playbookutils.go
Normal file
182
database/playbookutils.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/caql"
|
||||
"github.com/SecurityBrewery/catalyst/dag"
|
||||
"github.com/SecurityBrewery/catalyst/generated/models"
|
||||
)
|
||||
|
||||
func playbookGraph(playbook *models.Playbook) (*dag.Graph, error) {
|
||||
d := dag.NewGraph()
|
||||
|
||||
var taskIDs []string
|
||||
for taskID := range playbook.Tasks {
|
||||
taskIDs = append(taskIDs, taskID)
|
||||
}
|
||||
sort.Strings(taskIDs)
|
||||
|
||||
for _, taskID := range taskIDs {
|
||||
if err := d.AddNode(taskID); err != nil {
|
||||
return nil, errors.New("could not add node")
|
||||
}
|
||||
}
|
||||
for _, taskID := range taskIDs {
|
||||
task := playbook.Tasks[taskID]
|
||||
for next := range task.Next {
|
||||
if err := d.AddEdge(taskID, next); err != nil {
|
||||
return nil, errors.New("could not add edge")
|
||||
}
|
||||
}
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func toTaskResponse(playbook *models.Playbook, taskID string, order int, graph *dag.Graph) (*models.TaskResponse, error) {
|
||||
task, ok := playbook.Tasks[taskID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("task %s not found", taskID)
|
||||
}
|
||||
|
||||
tr := &models.TaskResponse{
|
||||
Automation: task.Automation,
|
||||
Closed: task.Closed,
|
||||
Created: task.Created,
|
||||
Data: task.Data,
|
||||
Done: task.Done,
|
||||
Join: task.Join,
|
||||
Payload: task.Payload,
|
||||
Name: task.Name,
|
||||
Next: task.Next,
|
||||
Owner: task.Owner,
|
||||
Schema: task.Schema,
|
||||
Type: task.Type,
|
||||
// Active: active,
|
||||
// Order: v.Order,
|
||||
}
|
||||
|
||||
tr.Order = int64(order)
|
||||
|
||||
taskActive, _ := active(playbook, taskID, graph, task)
|
||||
tr.Active = taskActive
|
||||
|
||||
return tr, nil
|
||||
}
|
||||
|
||||
func activePlaybook(playbook *models.Playbook, taskID string) (bool, error) {
|
||||
task, ok := playbook.Tasks[taskID]
|
||||
if !ok {
|
||||
return false, fmt.Errorf("playbook does not contain tasks %s", taskID)
|
||||
}
|
||||
|
||||
d, err := playbookGraph(playbook)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return active(playbook, taskID, d, task)
|
||||
}
|
||||
|
||||
func active(playbook *models.Playbook, taskID string, d *dag.Graph, task *models.Task) (bool, error) {
|
||||
if task.Done {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
parents := d.GetParents(taskID)
|
||||
|
||||
if len(parents) == 0 {
|
||||
return true, nil // valid(&task)
|
||||
}
|
||||
|
||||
if task.Join != nil && *task.Join {
|
||||
for _, parent := range parents {
|
||||
parentTask := playbook.Tasks[parent]
|
||||
if !parentTask.Done {
|
||||
return false, nil
|
||||
}
|
||||
requirement := parentTask.Next[taskID]
|
||||
|
||||
b, err := evalRequirement(requirement, parentTask.Data)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !b {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
for _, parent := range parents {
|
||||
parentTask := playbook.Tasks[parent]
|
||||
if !parentTask.Done {
|
||||
// return false, nil
|
||||
continue
|
||||
}
|
||||
requirement := parentTask.Next[taskID]
|
||||
|
||||
b, err := evalRequirement(requirement, parentTask.Data)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if b {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func evalRequirement(aql string, data interface{}) (bool, error) {
|
||||
if aql == "" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
parser := caql.Parser{}
|
||||
tree, err := parser.Parse(aql)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var dataMap map[string]interface{}
|
||||
if data != nil {
|
||||
if dataMapX, ok := data.(map[string]interface{}); ok {
|
||||
dataMap = dataMapX
|
||||
} else {
|
||||
log.Println("wrong data type for task data")
|
||||
}
|
||||
}
|
||||
|
||||
v, err := tree.Eval(dataMap)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if b, ok := v.(bool); ok {
|
||||
return b, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
/*
|
||||
// "github.com/qri-io/jsonschema"
|
||||
func valid(task *models.Task) (bool, error) {
|
||||
schema, err := json.Marshal(task.Schema)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
rs := &jsonschema.Schema{}
|
||||
if err := json.Unmarshal(schema, rs); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
state := rs.Validate(context.Background(), task.Data)
|
||||
return len(*state.Errs) > 0, nil
|
||||
}
|
||||
*/
|
||||
135
database/playbookutils_test.go
Normal file
135
database/playbookutils_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/generated/models"
|
||||
)
|
||||
|
||||
var playbook2 = &models.Playbook{
|
||||
Name: "Phishing",
|
||||
Tasks: map[string]*models.Task{
|
||||
"board": {Next: map[string]string{
|
||||
"escalate": "boardInvolved == true",
|
||||
"aquire-mail": "boardInvolved == false",
|
||||
}},
|
||||
"escalate": {},
|
||||
"aquire-mail": {Next: map[string]string{
|
||||
"extract-iocs": "schemaKey == 'yes'",
|
||||
"block-sender": "schemaKey == 'yes'",
|
||||
"search-email-gateway": "schemaKey == 'no'",
|
||||
}},
|
||||
"extract-iocs": {Next: map[string]string{"fetch-iocs": ""}},
|
||||
"fetch-iocs": {Next: map[string]string{"block-iocs": ""}},
|
||||
"search-email-gateway": {Next: map[string]string{"block-iocs": ""}},
|
||||
"block-sender": {Next: map[string]string{"block-iocs": ""}},
|
||||
"block-iocs": {Next: map[string]string{"block-ioc": ""}},
|
||||
"block-ioc": {},
|
||||
},
|
||||
}
|
||||
|
||||
var playbook3 = &models.Playbook{
|
||||
Name: "Phishing",
|
||||
Tasks: map[string]*models.Task{
|
||||
"board": {Next: map[string]string{
|
||||
"escalate": "boardInvolved == true",
|
||||
"aquire-mail": "boardInvolved == false",
|
||||
}, Data: map[string]interface{}{"boardInvolved": true}, Done: true},
|
||||
"escalate": {},
|
||||
"aquire-mail": {Next: map[string]string{
|
||||
"extract-iocs": "schemaKey == 'yes'",
|
||||
"block-sender": "schemaKey == 'yes'",
|
||||
"search-email-gateway": "schemaKey == 'no'",
|
||||
}},
|
||||
"extract-iocs": {Next: map[string]string{"fetch-iocs": ""}},
|
||||
"fetch-iocs": {Next: map[string]string{"block-iocs": ""}},
|
||||
"search-email-gateway": {Next: map[string]string{"block-iocs": ""}},
|
||||
"block-sender": {Next: map[string]string{"block-iocs": ""}},
|
||||
"block-iocs": {Next: map[string]string{"block-ioc": ""}},
|
||||
"block-ioc": {},
|
||||
},
|
||||
}
|
||||
|
||||
var playbook4 = &models.Playbook{
|
||||
Name: "Malware",
|
||||
Tasks: map[string]*models.Task{
|
||||
"file-or-hash": {Next: map[string]string{
|
||||
"enter-hash": "file == 'Hash'",
|
||||
"upload": "file == 'File'",
|
||||
}},
|
||||
"enter-hash": {Next: map[string]string{
|
||||
"virustotal": "hash != ''",
|
||||
}},
|
||||
"upload": {Next: map[string]string{
|
||||
"hash": "malware",
|
||||
}},
|
||||
"hash": {Next: map[string]string{"virustotal": ""}},
|
||||
"virustotal": {},
|
||||
},
|
||||
}
|
||||
|
||||
func Test_canBeCompleted(t *testing.T) {
|
||||
type args struct {
|
||||
playbook *models.Playbook
|
||||
taskID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
{"playbook2 board", args{playbook: playbook2, taskID: "board"}, true, false},
|
||||
{"playbook2 escalate", args{playbook: playbook2, taskID: "escalate"}, false, false},
|
||||
{"playbook2 aquire-mail", args{playbook: playbook2, taskID: "aquire-mail"}, false, false},
|
||||
{"playbook2 block-ioc", args{playbook: playbook2, taskID: "block-ioc"}, false, false},
|
||||
{"playbook3 board", args{playbook: playbook3, taskID: "board"}, false, false},
|
||||
{"playbook3 escalate", args{playbook: playbook3, taskID: "escalate"}, true, false},
|
||||
{"playbook3 aquire-mail", args{playbook: playbook3, taskID: "aquire-mail"}, false, false},
|
||||
{"playbook3 block-ioc", args{playbook: playbook3, taskID: "block-ioc"}, false, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := activePlaybook(tt.args.playbook, tt.args.taskID)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("activePlaybook() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("activePlaybook() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_playbookOrder(t *testing.T) {
|
||||
type args struct {
|
||||
playbook *models.Playbook
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []string
|
||||
wantErr bool
|
||||
}{
|
||||
{"playbook4", args{playbook: playbook4}, []string{"file-or-hash", "enter-hash", "upload", "hash", "virustotal"}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := toPlaybookResponse(tt.args.playbook)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("activePlaybook() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
names := make([]string, len(got.Tasks))
|
||||
for name, task := range got.Tasks {
|
||||
names[task.Order] = name
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.want, names)
|
||||
})
|
||||
}
|
||||
}
|
||||
48
database/relationships.go
Normal file
48
database/relationships.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/arangodb/go-driver"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/database/busdb"
|
||||
)
|
||||
|
||||
func (db *Database) RelatedCreate(ctx context.Context, id, id2 int64) error {
|
||||
if id == id2 {
|
||||
return errors.New("tickets cannot relate to themself")
|
||||
}
|
||||
|
||||
_, err := db.relatedCollection.CreateEdge(ctx, ctx, &driver.EdgeDocument{
|
||||
From: driver.DocumentID(TicketCollectionName + "/" + strconv.Itoa(int(id))),
|
||||
To: driver.DocumentID(TicketCollectionName + "/" + strconv.Itoa(int(id2))),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Database) RelatedBatchCreate(ctx context.Context, edges []*driver.EdgeDocument) error {
|
||||
_, err := db.relatedCollection.CreateEdges(ctx, edges)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Database) RelatedRemove(ctx context.Context, id, id2 int64) error {
|
||||
q := `
|
||||
FOR d in @@collection
|
||||
FILTER (d._from == @id && d._to == @id2) || (d._to == @id && d._from == @id2)
|
||||
REMOVE d in @@collection`
|
||||
_, _, err := db.Query(ctx, q, map[string]interface{}{
|
||||
"@collection": RelatedTicketsCollectionName,
|
||||
"id": driver.DocumentID(TicketCollectionName + "/" + strconv.Itoa(int(id))),
|
||||
"id2": driver.DocumentID(TicketCollectionName + "/" + strconv.Itoa(int(id2))),
|
||||
}, &busdb.Operation{
|
||||
OperationType: busdb.Update,
|
||||
Ids: []driver.DocumentID{
|
||||
driver.DocumentID(TicketCollectionName + "/" + strconv.Itoa(int(id))),
|
||||
driver.DocumentID(TicketCollectionName + "/" + strconv.Itoa(int(id2))),
|
||||
},
|
||||
Msg: "Removed ticket/artifact relation",
|
||||
})
|
||||
return err
|
||||
}
|
||||
86
database/settings.go
Normal file
86
database/settings.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/arangodb/go-driver"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/database/busdb"
|
||||
"github.com/SecurityBrewery/catalyst/generated/models"
|
||||
)
|
||||
|
||||
func toUserDataResponse(key string, doc *models.UserData) *models.UserDataResponse {
|
||||
return &models.UserDataResponse{
|
||||
Email: doc.Email,
|
||||
ID: key,
|
||||
Image: doc.Image,
|
||||
Name: doc.Name,
|
||||
Timeformat: doc.Timeformat,
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Database) UserDataCreate(ctx context.Context, id string, userdata *models.UserData) error {
|
||||
if userdata == nil {
|
||||
return errors.New("requires setting")
|
||||
}
|
||||
if id == "" {
|
||||
return errors.New("requires username")
|
||||
}
|
||||
|
||||
_, err := db.userdataCollection.CreateDocument(ctx, ctx, id, userdata)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Database) UserDataGetOrCreate(ctx *gin.Context, id string, newUserData *models.UserData) (*models.UserDataResponse, error) {
|
||||
setting, err := db.UserDataGet(ctx, id)
|
||||
if err != nil {
|
||||
return toUserDataResponse(id, newUserData), db.UserDataCreate(ctx, id, newUserData)
|
||||
}
|
||||
return setting, nil
|
||||
}
|
||||
|
||||
func (db *Database) UserDataGet(ctx context.Context, id string) (*models.UserDataResponse, error) {
|
||||
var doc models.UserData
|
||||
meta, err := db.userdataCollection.ReadDocument(ctx, id, &doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toUserDataResponse(meta.Key, &doc), err
|
||||
}
|
||||
|
||||
func (db *Database) UserDataList(ctx context.Context) ([]*models.UserDataResponse, error) {
|
||||
query := "FOR d IN @@collection SORT d.username ASC RETURN d"
|
||||
cursor, _, err := db.Query(ctx, query, map[string]interface{}{"@collection": UserDataCollectionName}, busdb.ReadOperation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close()
|
||||
var docs []*models.UserDataResponse
|
||||
for {
|
||||
var doc models.UserData
|
||||
meta, err := cursor.ReadDocument(ctx, &doc)
|
||||
if driver.IsNoMoreDocuments(err) {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
docs = append(docs, toUserDataResponse(meta.Key, &doc))
|
||||
}
|
||||
|
||||
return docs, err
|
||||
}
|
||||
|
||||
func (db *Database) UserDataUpdate(ctx context.Context, id string, userdata *models.UserData) (*models.UserDataResponse, error) {
|
||||
var doc models.UserData
|
||||
ctx = driver.WithReturnNew(ctx, &doc)
|
||||
|
||||
meta, err := db.userdataCollection.ReplaceDocument(ctx, id, userdata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toUserDataResponse(meta.Key, &doc), nil
|
||||
}
|
||||
159
database/settings_test.go
Normal file
159
database/settings_test.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package database_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/generated/models"
|
||||
"github.com/SecurityBrewery/catalyst/pointer"
|
||||
"github.com/SecurityBrewery/catalyst/test"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gin.SetMode(gin.TestMode)
|
||||
}
|
||||
|
||||
var bob = &models.UserData{
|
||||
Email: pointer.String("bob@example.org"),
|
||||
Name: pointer.String("Bob"),
|
||||
}
|
||||
|
||||
var bobResponse = &models.UserDataResponse{
|
||||
ID: "bob",
|
||||
Email: pointer.String("bob@example.org"),
|
||||
Name: pointer.String("Bob"),
|
||||
}
|
||||
|
||||
func TestDatabase_UserDataCreate(t *testing.T) {
|
||||
type args struct {
|
||||
id string
|
||||
setting *models.UserData
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "Normal setting", args: args{id: "bob", setting: bob}, wantErr: false},
|
||||
{name: "Nil setting", args: args{id: "bob"}, wantErr: true},
|
||||
{name: "UserData without settingname", args: args{id: ""}, wantErr: true},
|
||||
{name: "Only settingname", args: args{id: "bob"}, wantErr: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, _, _, _, _, db, cleanup, err := test.DB(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
if err := db.UserDataCreate(test.Context(), tt.args.id, tt.args.setting); (err != nil) != tt.wantErr {
|
||||
t.Errorf("settingCreate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabase_UserDataGet(t *testing.T) {
|
||||
type args struct {
|
||||
id string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *models.UserDataResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "Normal get", args: args{id: "bob"}, want: bobResponse},
|
||||
{name: "Not existing", args: args{id: "foo"}, wantErr: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, _, _, _, _, db, cleanup, err := test.DB(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
if err := db.UserDataCreate(test.Context(), "bob", bob); err != nil {
|
||||
t.Errorf("settingCreate() error = %v", err)
|
||||
}
|
||||
|
||||
got, err := db.UserDataGet(test.Context(), tt.args.id)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("UserDataGet() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabase_UserDataList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want []*models.UserDataResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "Normal list", want: []*models.UserDataResponse{bobResponse}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, _, _, _, _, db, cleanup, err := test.DB(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
if err := db.UserDataCreate(test.Context(), "bob", bob); err != nil {
|
||||
t.Errorf("settingCreate() error = %v", err)
|
||||
}
|
||||
|
||||
got, err := db.UserDataList(test.Context())
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("UserDataList() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabase_UserDataUpdate(t *testing.T) {
|
||||
type args struct {
|
||||
id string
|
||||
setting *models.UserData
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "Normal", args: args{id: "bob", setting: bob}},
|
||||
{name: "Not existing", args: args{id: "foo"}, wantErr: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, _, _, _, _, db, cleanup, err := test.DB(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
if err := db.UserDataCreate(test.Context(), "bob", bob); err != nil {
|
||||
t.Errorf("settingCreate() error = %v", err)
|
||||
}
|
||||
|
||||
if _, err := db.UserDataUpdate(test.Context(), tt.args.id, tt.args.setting); (err != nil) != tt.wantErr {
|
||||
t.Errorf("UserDataUpdate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
43
database/statistics.go
Normal file
43
database/statistics.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/database/busdb"
|
||||
"github.com/SecurityBrewery/catalyst/generated/models"
|
||||
)
|
||||
|
||||
func (db *Database) Statistics(ctx context.Context) (*models.Statistics, error) {
|
||||
query := `RETURN {
|
||||
tickets_per_type: MERGE(FOR d in tickets
|
||||
COLLECT type = d.type WITH COUNT INTO typecount
|
||||
RETURN ZIP([type], [typecount])),
|
||||
|
||||
unassigned: FIRST(FOR d in tickets
|
||||
FILTER d.status == "open" AND !d.owner
|
||||
COLLECT WITH COUNT INTO length
|
||||
RETURN length),
|
||||
|
||||
open_tickets_per_user: MERGE(FOR d in tickets
|
||||
FILTER d.status == "open"
|
||||
COLLECT user = d.owner WITH COUNT INTO usercount
|
||||
RETURN ZIP([user], [usercount])),
|
||||
|
||||
tickets_per_week: MERGE(FOR d in tickets
|
||||
COLLECT week = CONCAT(DATE_YEAR(d.created), "-", DATE_ISOWEEK(d.created) < 10 ? "0" : "", DATE_ISOWEEK(d.created)) WITH COUNT INTO weekcount
|
||||
RETURN ZIP([week], [weekcount])),
|
||||
}`
|
||||
|
||||
cur, _, err := db.Query(ctx, query, nil, busdb.ReadOperation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cur.Close()
|
||||
|
||||
statistics := models.Statistics{}
|
||||
if _, err := cur.ReadDocument(ctx, &statistics); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &statistics, nil
|
||||
}
|
||||
68
database/task.go
Normal file
68
database/task.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/arangodb/go-driver"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/database/busdb"
|
||||
"github.com/SecurityBrewery/catalyst/generated/models"
|
||||
)
|
||||
|
||||
type playbookResponse struct {
|
||||
PlaybookId string `json:"playbook_id"`
|
||||
PlaybookName string `json:"playbook_name"`
|
||||
Playbook models.Playbook `json:"playbook"`
|
||||
TicketId int64 `json:"ticket_id"`
|
||||
TicketName string `json:"ticket_name"`
|
||||
}
|
||||
|
||||
func (db *Database) TaskList(ctx context.Context) ([]*models.TaskWithContext, error) {
|
||||
ticketFilterQuery, ticketFilterVars, err := db.Hooks.TicketWriteFilter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := `FOR d IN @@collection
|
||||
` + ticketFilterQuery + `
|
||||
FILTER d.status == 'open'
|
||||
FOR playbook IN NOT_NULL(VALUES(d.playbooks), [])
|
||||
RETURN { ticket_id: TO_NUMBER(d._key), ticket_name: d.name, playbook_id: POSITION(d.playbooks, playbook, true), playbook_name: playbook.name, playbook: playbook }`
|
||||
cursor, _, err := db.Query(ctx, query, mergeMaps(ticketFilterVars, map[string]interface{}{
|
||||
"@collection": TicketCollectionName,
|
||||
}), busdb.ReadOperation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close()
|
||||
docs := []*models.TaskWithContext{}
|
||||
for {
|
||||
var doc playbookResponse
|
||||
_, err := cursor.ReadDocument(ctx, &doc)
|
||||
if driver.IsNoMoreDocuments(err) {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
playbook, err := toPlaybookResponse(&doc.Playbook)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, task := range playbook.Tasks {
|
||||
if task.Active {
|
||||
docs = append(docs, &models.TaskWithContext{
|
||||
PlaybookId: doc.PlaybookId,
|
||||
PlaybookName: doc.PlaybookName,
|
||||
Task: *task,
|
||||
TicketId: doc.TicketId,
|
||||
TicketName: doc.TicketName,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return docs, err
|
||||
}
|
||||
88
database/template.go
Normal file
88
database/template.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/arangodb/go-driver"
|
||||
"github.com/iancoleman/strcase"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/database/busdb"
|
||||
"github.com/SecurityBrewery/catalyst/generated/models"
|
||||
)
|
||||
|
||||
func toTicketTemplate(doc *models.TicketTemplateForm) *models.TicketTemplate {
|
||||
return &models.TicketTemplate{Name: doc.Name, Schema: doc.Schema}
|
||||
}
|
||||
|
||||
func toTicketTemplateResponse(key string, doc *models.TicketTemplate) *models.TicketTemplateResponse {
|
||||
return &models.TicketTemplateResponse{ID: key, Name: doc.Name, Schema: doc.Schema}
|
||||
}
|
||||
|
||||
func (db *Database) TemplateCreate(ctx context.Context, template *models.TicketTemplateForm) (*models.TicketTemplateResponse, error) {
|
||||
if template == nil {
|
||||
return nil, errors.New("requires template")
|
||||
}
|
||||
if template.Name == "" {
|
||||
return nil, errors.New("requires template name")
|
||||
}
|
||||
|
||||
var doc models.TicketTemplate
|
||||
newctx := driver.WithReturnNew(ctx, &doc)
|
||||
|
||||
meta, err := db.templateCollection.CreateDocument(ctx, newctx, strcase.ToKebab(template.Name), toTicketTemplate(template))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toTicketTemplateResponse(meta.Key, &doc), nil
|
||||
}
|
||||
|
||||
func (db *Database) TemplateGet(ctx context.Context, id string) (*models.TicketTemplateResponse, error) {
|
||||
var doc models.TicketTemplate
|
||||
meta, err := db.templateCollection.ReadDocument(ctx, id, &doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toTicketTemplateResponse(meta.Key, &doc), nil
|
||||
}
|
||||
|
||||
func (db *Database) TemplateUpdate(ctx context.Context, id string, template *models.TicketTemplateForm) (*models.TicketTemplateResponse, error) {
|
||||
var doc models.TicketTemplate
|
||||
ctx = driver.WithReturnNew(ctx, &doc)
|
||||
|
||||
meta, err := db.templateCollection.ReplaceDocument(ctx, id, toTicketTemplate(template))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toTicketTemplateResponse(meta.Key, &doc), nil
|
||||
}
|
||||
|
||||
func (db *Database) TemplateDelete(ctx context.Context, id string) error {
|
||||
_, err := db.templateCollection.RemoveDocument(ctx, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Database) TemplateList(ctx context.Context) ([]*models.TicketTemplateResponse, error) {
|
||||
query := "FOR d IN @@collection RETURN d"
|
||||
cursor, _, err := db.Query(ctx, query, map[string]interface{}{"@collection": TemplateCollectionName}, busdb.ReadOperation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close()
|
||||
var docs []*models.TicketTemplateResponse
|
||||
for {
|
||||
var doc models.TicketTemplate
|
||||
meta, err := cursor.ReadDocument(ctx, &doc)
|
||||
if driver.IsNoMoreDocuments(err) {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
docs = append(docs, toTicketTemplateResponse(meta.Key, &doc))
|
||||
}
|
||||
|
||||
return docs, err
|
||||
}
|
||||
182
database/template_test.go
Normal file
182
database/template_test.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package database_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/database/migrations"
|
||||
"github.com/SecurityBrewery/catalyst/generated/models"
|
||||
"github.com/SecurityBrewery/catalyst/test"
|
||||
)
|
||||
|
||||
var template1 = &models.TicketTemplateForm{
|
||||
Schema: migrations.DefaultTemplateSchema,
|
||||
Name: "Template 1",
|
||||
}
|
||||
var default1 = &models.TicketTemplateForm{
|
||||
Schema: migrations.DefaultTemplateSchema,
|
||||
Name: "Default",
|
||||
}
|
||||
|
||||
func TestDatabase_TemplateCreate(t *testing.T) {
|
||||
type args struct {
|
||||
template *models.TicketTemplateForm
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "Normal", args: args{template: template1}},
|
||||
{name: "Duplicate", args: args{template: default1}, wantErr: true},
|
||||
{name: "Nil template", args: args{}, wantErr: true},
|
||||
{name: "Template without fields", args: args{template: &models.TicketTemplateForm{}}, wantErr: true},
|
||||
{name: "Only name", args: args{template: &models.TicketTemplateForm{Name: "name"}}, wantErr: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, _, _, _, _, db, cleanup, err := test.DB(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
if _, err := db.TemplateCreate(test.Context(), tt.args.template); (err != nil) != tt.wantErr {
|
||||
t.Errorf("TemplateCreate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabase_TemplateDelete(t *testing.T) {
|
||||
type args struct {
|
||||
id string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "Normal", args: args{"default"}},
|
||||
{name: "Not existing", args: args{"foobar"}, wantErr: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, _, _, _, _, db, cleanup, err := test.DB(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
if _, err := db.TemplateCreate(test.Context(), template1); err != nil {
|
||||
t.Errorf("TemplateCreate() error = %v", err)
|
||||
}
|
||||
|
||||
if err := db.TemplateDelete(test.Context(), tt.args.id); (err != nil) != tt.wantErr {
|
||||
t.Errorf("TemplateDelete() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabase_TemplateGet(t *testing.T) {
|
||||
type args struct {
|
||||
id string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *models.TicketTemplateResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "Normal", args: args{id: "default"}, want: &models.TicketTemplateResponse{ID: "default", Name: "Default", Schema: migrations.DefaultTemplateSchema}},
|
||||
{name: "Not existing", args: args{id: "foobar"}, wantErr: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, _, _, _, _, db, cleanup, err := test.DB(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
if _, err := db.TemplateCreate(test.Context(), template1); err != nil {
|
||||
t.Errorf("TemplateCreate() error = %v", err)
|
||||
}
|
||||
|
||||
got, err := db.TemplateGet(test.Context(), tt.args.id)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("TemplateGet() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, got, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabase_TemplateList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want []*models.TicketTemplateResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "Normal", want: []*models.TicketTemplateResponse{{ID: "default", Name: "Default", Schema: migrations.DefaultTemplateSchema}, {ID: "template-1", Name: template1.Name, Schema: template1.Schema}}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, _, _, _, _, db, cleanup, err := test.DB(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
if _, err := db.TemplateCreate(test.Context(), template1); err != nil {
|
||||
t.Errorf("TemplateCreate() error = %v", err)
|
||||
}
|
||||
|
||||
got, err := db.TemplateList(test.Context())
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("TemplateList() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
assert.Equal(t, got, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabase_TemplateUpdate(t *testing.T) {
|
||||
type args struct {
|
||||
id string
|
||||
template *models.TicketTemplateForm
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "Normal", args: args{"default", template1}},
|
||||
{name: "Not existing", args: args{"foobar", template1}, wantErr: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, _, _, _, _, db, cleanup, err := test.DB(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
if _, err := db.TemplateCreate(test.Context(), template1); err != nil {
|
||||
t.Errorf("TemplateCreate() error = %v", err)
|
||||
}
|
||||
|
||||
if _, err := db.TemplateUpdate(test.Context(), tt.args.id, tt.args.template); (err != nil) != tt.wantErr {
|
||||
t.Errorf("TemplateUpdate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
604
database/ticket.go
Normal file
604
database/ticket.go
Normal file
@@ -0,0 +1,604 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/arangodb/go-driver"
|
||||
"github.com/xeipuuv/gojsonschema"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/caql"
|
||||
"github.com/SecurityBrewery/catalyst/database/busdb"
|
||||
"github.com/SecurityBrewery/catalyst/generated/models"
|
||||
"github.com/SecurityBrewery/catalyst/index"
|
||||
)
|
||||
|
||||
func toTicket(ticketForm *models.TicketForm) (interface{}, error) {
|
||||
playbooks, err := toPlaybooks(ticketForm.Playbooks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ticket := &models.Ticket{
|
||||
Artifacts: ticketForm.Artifacts,
|
||||
Comments: ticketForm.Comments,
|
||||
Details: ticketForm.Details,
|
||||
Files: ticketForm.Files,
|
||||
Name: ticketForm.Name,
|
||||
Owner: ticketForm.Owner,
|
||||
Playbooks: playbooks,
|
||||
Read: ticketForm.Read,
|
||||
References: ticketForm.References,
|
||||
Status: ticketForm.Status,
|
||||
Type: ticketForm.Type,
|
||||
Write: ticketForm.Write,
|
||||
// ID: ticketForm.ID,
|
||||
// Created: ticketForm.Created,
|
||||
// Modified: ticketForm.Modified,
|
||||
// Schema: ticketForm.Schema,
|
||||
}
|
||||
|
||||
if ticketForm.Created != nil {
|
||||
ticket.Created = *ticketForm.Created
|
||||
} else {
|
||||
ticket.Created = time.Now().UTC()
|
||||
}
|
||||
if ticketForm.Modified != nil {
|
||||
ticket.Modified = *ticketForm.Modified
|
||||
} else {
|
||||
ticket.Modified = time.Now().UTC()
|
||||
}
|
||||
if ticketForm.Schema != nil {
|
||||
ticket.Schema = *ticketForm.Schema
|
||||
} else {
|
||||
ticket.Schema = "{}"
|
||||
}
|
||||
if ticketForm.Status == "" {
|
||||
ticket.Status = "open"
|
||||
}
|
||||
if ticketForm.ID != nil {
|
||||
return &busdb.Keyed{Key: strconv.FormatInt(*ticketForm.ID, 10), Doc: ticket}, nil
|
||||
}
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
func toTicketResponses(tickets []*models.TicketSimpleResponse) ([]*models.TicketResponse, error) {
|
||||
var extendedTickets []*models.TicketResponse
|
||||
for _, simple := range tickets {
|
||||
tr, err := toTicketResponse(simple)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extendedTickets = append(extendedTickets, tr)
|
||||
}
|
||||
return extendedTickets, nil
|
||||
}
|
||||
|
||||
func toTicketResponse(ticket *models.TicketSimpleResponse) (*models.TicketResponse, error) {
|
||||
playbooks, err := toPlaybookResponses(ticket.Playbooks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.TicketResponse{
|
||||
ID: ticket.ID,
|
||||
Artifacts: ticket.Artifacts,
|
||||
Comments: ticket.Comments,
|
||||
Created: ticket.Created,
|
||||
Details: ticket.Details,
|
||||
Files: ticket.Files,
|
||||
Modified: ticket.Modified,
|
||||
Name: ticket.Name,
|
||||
Owner: ticket.Owner,
|
||||
Playbooks: playbooks,
|
||||
Read: ticket.Read,
|
||||
References: ticket.References,
|
||||
Schema: ticket.Schema,
|
||||
Status: ticket.Status,
|
||||
Type: ticket.Type,
|
||||
Write: ticket.Write,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toTicketSimpleResponse(key string, ticket *models.Ticket) (*models.TicketSimpleResponse, error) {
|
||||
id, err := strconv.ParseInt(key, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.TicketSimpleResponse{
|
||||
Artifacts: ticket.Artifacts,
|
||||
Comments: ticket.Comments,
|
||||
Created: ticket.Created,
|
||||
Details: ticket.Details,
|
||||
Files: ticket.Files,
|
||||
ID: id,
|
||||
Modified: ticket.Modified,
|
||||
Name: ticket.Name,
|
||||
Owner: ticket.Owner,
|
||||
Playbooks: ticket.Playbooks,
|
||||
Read: ticket.Read,
|
||||
References: ticket.References,
|
||||
Schema: ticket.Schema,
|
||||
Status: ticket.Status,
|
||||
Type: ticket.Type,
|
||||
Write: ticket.Write,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toTicketWithTickets(ticketResponse *models.TicketResponse, tickets []*models.TicketSimpleResponse) *models.TicketWithTickets {
|
||||
return &models.TicketWithTickets{
|
||||
Artifacts: ticketResponse.Artifacts,
|
||||
Comments: ticketResponse.Comments,
|
||||
Created: ticketResponse.Created,
|
||||
Details: ticketResponse.Details,
|
||||
Files: ticketResponse.Files,
|
||||
ID: ticketResponse.ID,
|
||||
Modified: ticketResponse.Modified,
|
||||
Name: ticketResponse.Name,
|
||||
Owner: ticketResponse.Owner,
|
||||
Playbooks: ticketResponse.Playbooks,
|
||||
Read: ticketResponse.Read,
|
||||
References: ticketResponse.References,
|
||||
Schema: ticketResponse.Schema,
|
||||
Status: ticketResponse.Status,
|
||||
Type: ticketResponse.Type,
|
||||
Write: ticketResponse.Write,
|
||||
|
||||
Tickets: tickets,
|
||||
}
|
||||
}
|
||||
|
||||
func toPlaybookResponses(playbooks map[string]*models.Playbook) (map[string]*models.PlaybookResponse, error) {
|
||||
pr := map[string]*models.PlaybookResponse{}
|
||||
var err error
|
||||
for k, v := range playbooks {
|
||||
pr[k], err = toPlaybookResponse(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
func toPlaybookResponse(playbook *models.Playbook) (*models.PlaybookResponse, error) {
|
||||
graph, err := playbookGraph(playbook)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
re := &models.PlaybookResponse{
|
||||
Name: playbook.Name,
|
||||
Tasks: map[string]*models.TaskResponse{},
|
||||
}
|
||||
|
||||
results, err := graph.Toposort()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
i := 0
|
||||
for _, taskID := range results {
|
||||
rootTask, err := toTaskResponse(playbook, taskID, i, graph)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
re.Tasks[taskID] = rootTask
|
||||
i++
|
||||
}
|
||||
return re, nil
|
||||
}
|
||||
|
||||
func (db *Database) TicketBatchCreate(ctx context.Context, ticketForms []*models.TicketForm) ([]*models.TicketResponse, error) {
|
||||
update, err := db.Hooks.IngestionFilter(ctx, db.Index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dbTickets []interface{}
|
||||
for _, ticketForm := range ticketForms {
|
||||
ticket, err := toTicket(ticketForm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := validate(ticket, models.TicketSchema); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dbTickets = append(dbTickets, ticket)
|
||||
}
|
||||
|
||||
ticketFilterQuery, ticketFilterVars, err := db.Hooks.TicketWriteFilter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := `FOR d IN @tickets
|
||||
` + ticketFilterQuery + `
|
||||
LET updates = ` + update + `
|
||||
LET newdoc = LENGTH(updates) != 0 ? APPLY("MERGE_RECURSIVE", APPEND([d], updates)) : d
|
||||
LET keyeddoc = HAS(newdoc, "id") ? MERGE(newdoc, {"_key": TO_STRING(newdoc.id)}) : newdoc
|
||||
LET noiddoc = UNSET(keyeddoc, "id")
|
||||
INSERT noiddoc INTO @@collection
|
||||
RETURN NEW`
|
||||
apiTickets, _, err := db.ticketListQuery(ctx, query, mergeMaps(map[string]interface{}{
|
||||
"tickets": dbTickets,
|
||||
}, ticketFilterVars), busdb.CreateOperation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = batchIndex(db.Index, apiTickets); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ids []driver.DocumentID
|
||||
for _, apiTicket := range apiTickets {
|
||||
ids = append(ids, driver.NewDocumentID(TicketCollectionName, fmt.Sprint(apiTicket.ID)))
|
||||
}
|
||||
if err := db.BusDatabase.LogAndNotify(ctx, ids, "Ticket created"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ticketResponses, err := toTicketResponses(apiTickets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, ticketResponse := range ticketResponses {
|
||||
for playbookID := range ticketResponse.Playbooks {
|
||||
if err := runRootTask(ticketResponse, playbookID, db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ticketResponses, nil
|
||||
}
|
||||
|
||||
func (db *Database) IndexRebuild(ctx context.Context) error {
|
||||
if err := db.Index.Truncate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tickets, _, err := db.ticketListQuery(ctx, "FOR d IN @@collection RETURN d", nil, busdb.ReadOperation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return batchIndex(db.Index, tickets)
|
||||
}
|
||||
|
||||
func batchIndex(index *index.Index, tickets []*models.TicketSimpleResponse) error {
|
||||
var wg sync.WaitGroup
|
||||
var batch []*models.TicketSimpleResponse
|
||||
for _, ticket := range tickets {
|
||||
batch = append(batch, ticket)
|
||||
|
||||
if len(batch) > 100 {
|
||||
wg.Add(1)
|
||||
go func(docs []*models.TicketSimpleResponse) {
|
||||
index.Index(docs)
|
||||
wg.Done()
|
||||
}(batch)
|
||||
batch = []*models.TicketSimpleResponse{}
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) TicketGet(ctx context.Context, ticketID int64) (*models.TicketWithTickets, error) {
|
||||
ticketFilterQuery, ticketFilterVars, err := db.Hooks.TicketReadFilter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db.ticketGetQuery(ctx, ticketID, `LET d = DOCUMENT(@@collection, @ID) `+ticketFilterQuery+` RETURN d`, ticketFilterVars, busdb.ReadOperation)
|
||||
}
|
||||
|
||||
func (db *Database) ticketGetQuery(ctx context.Context, ticketID int64, query string, bindVars map[string]interface{}, operation *busdb.Operation) (*models.TicketWithTickets, error) {
|
||||
if bindVars == nil {
|
||||
bindVars = map[string]interface{}{}
|
||||
}
|
||||
bindVars["@collection"] = TicketCollectionName
|
||||
if ticketID != 0 {
|
||||
bindVars["ID"] = fmt.Sprint(ticketID)
|
||||
}
|
||||
|
||||
cur, _, err := db.Query(ctx, query, bindVars, operation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cur.Close()
|
||||
|
||||
ticket := models.Ticket{}
|
||||
meta, err := cur.ReadDocument(ctx, &ticket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ticketSimpleResponse, err := toTicketSimpleResponse(meta.Key, &ticket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// index
|
||||
go db.Index.Index([]*models.TicketSimpleResponse{ticketSimpleResponse})
|
||||
|
||||
ticketFilterQuery, ticketFilterVars, err := db.Hooks.TicketReadFilter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// tickets
|
||||
ticketsQuery := `FOR vertex, edge IN OUTBOUND
|
||||
DOCUMENT(@@tickets, @ID)
|
||||
GRAPH @graph
|
||||
FILTER IS_SAME_COLLECTION(@@collection, vertex)
|
||||
FILTER vertex != null
|
||||
LET d = DOCUMENT(@@collection, edge["_to"])
|
||||
` + ticketFilterQuery + `
|
||||
RETURN d`
|
||||
|
||||
outTickets, _, err := db.ticketListQuery(ctx, ticketsQuery, mergeMaps(map[string]interface{}{
|
||||
"ID": fmt.Sprint(ticketID),
|
||||
"graph": TicketArtifactsGraphName,
|
||||
"@tickets": TicketCollectionName,
|
||||
}, ticketFilterVars), busdb.ReadOperation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ticketsQuery = `FOR vertex, edge IN INBOUND
|
||||
DOCUMENT(@@tickets, @ID)
|
||||
GRAPH @graph
|
||||
FILTER IS_SAME_COLLECTION(@@collection, vertex)
|
||||
FILTER vertex != null
|
||||
LET d = DOCUMENT(@@collection, edge["_from"])
|
||||
` + ticketFilterQuery + `
|
||||
RETURN d`
|
||||
|
||||
inTickets, _, err := db.ticketListQuery(ctx, ticketsQuery, mergeMaps(map[string]interface{}{
|
||||
"ID": fmt.Sprint(ticketID),
|
||||
"graph": TicketArtifactsGraphName,
|
||||
"@tickets": TicketCollectionName,
|
||||
}, ticketFilterVars), busdb.ReadOperation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var artifactNames []string
|
||||
for _, artifact := range ticketSimpleResponse.Artifacts {
|
||||
artifactNames = append(artifactNames, artifact.Name)
|
||||
}
|
||||
ticketsQuery = `FOR d IN @@collection
|
||||
FILTER d._key != @ID
|
||||
` + ticketFilterQuery + `
|
||||
FOR a IN NOT_NULL(d.artifacts, [])
|
||||
FILTER POSITION(@artifacts, a.name)
|
||||
RETURN d`
|
||||
sameArtifactTickets, _, err := db.ticketListQuery(ctx, ticketsQuery, mergeMaps(map[string]interface{}{
|
||||
"ID": fmt.Sprint(ticketID),
|
||||
"artifacts": artifactNames,
|
||||
}, ticketFilterVars), busdb.ReadOperation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tickets := append(outTickets, inTickets...)
|
||||
tickets = append(tickets, sameArtifactTickets...)
|
||||
sort.Slice(tickets, func(i, j int) bool {
|
||||
return tickets[i].ID < tickets[j].ID
|
||||
})
|
||||
|
||||
ticketResponse, err := toTicketResponse(ticketSimpleResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toTicketWithTickets(ticketResponse, tickets), nil
|
||||
}
|
||||
|
||||
func (db *Database) TicketUpdate(ctx context.Context, ticketID int64, ticket *models.Ticket) (*models.TicketWithTickets, error) {
|
||||
ticketFilterQuery, ticketFilterVars, err := db.Hooks.TicketWriteFilter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := `LET d = DOCUMENT(@@collection, @ID)
|
||||
` + ticketFilterQuery + `
|
||||
REPLACE d WITH @ticket IN @@collection
|
||||
RETURN NEW`
|
||||
ticket.Modified = time.Now().UTC() // TODO make setable?
|
||||
return db.ticketGetQuery(ctx, ticketID, query, mergeMaps(map[string]interface{}{"ticket": ticket}, ticketFilterVars), &busdb.Operation{
|
||||
OperationType: busdb.Update, Ids: []driver.DocumentID{
|
||||
driver.NewDocumentID(TicketCollectionName, strconv.FormatInt(ticketID, 10)),
|
||||
},
|
||||
Msg: "Ticket updated",
|
||||
})
|
||||
}
|
||||
|
||||
func (db *Database) TicketDelete(ctx context.Context, ticketID int64) error {
|
||||
_, err := db.TicketGet(ctx, ticketID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.ticketCollection.RemoveDocument(ctx, strconv.FormatInt(ticketID, 10))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) TicketList(ctx context.Context, ticketType string, query string, sorts []string, desc []bool, offset, count int64) (*models.TicketList, error) {
|
||||
binVars := map[string]interface{}{}
|
||||
|
||||
parser := &caql.Parser{Searcher: db.Index, Prefix: "d."}
|
||||
|
||||
var typeString = ""
|
||||
if ticketType != "" {
|
||||
typeString = "FILTER d.type == @type "
|
||||
binVars["type"] = ticketType
|
||||
}
|
||||
|
||||
var filterString = ""
|
||||
if query != "" {
|
||||
queryTree, err := parser.Parse(query)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid filter query: syntax error")
|
||||
}
|
||||
filterString, err = queryTree.String()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid filter query: %w", err)
|
||||
}
|
||||
filterString = "FILTER " + filterString
|
||||
}
|
||||
|
||||
documentCount, err := db.TicketCount(ctx, typeString, filterString, binVars)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sortQ := sortQuery(sorts, desc, binVars)
|
||||
binVars["offset"] = offset
|
||||
binVars["count"] = count
|
||||
|
||||
ticketFilterQuery, ticketFilterVars, err := db.Hooks.TicketReadFilter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q := `FOR d IN @@collection
|
||||
` + ticketFilterQuery + `
|
||||
` + sortQ + `
|
||||
` + typeString + `
|
||||
` + filterString + `
|
||||
LIMIT @offset, @count
|
||||
SORT d._key ASC
|
||||
RETURN d`
|
||||
// RETURN KEEP(d, "_key", "id", "name", "type", "created")`
|
||||
ticketList, _, err := db.ticketListQuery(ctx, q, mergeMaps(binVars, ticketFilterVars), busdb.ReadOperation)
|
||||
return &models.TicketList{
|
||||
Count: documentCount,
|
||||
Tickets: ticketList,
|
||||
}, err
|
||||
// return map[string]interface{}{"tickets": ticketList, "count": documentCount}, err
|
||||
}
|
||||
|
||||
func (db *Database) ticketListQuery(ctx context.Context, query string, bindVars map[string]interface{}, operation *busdb.Operation) ([]*models.TicketSimpleResponse, *models.LogEntry, error) {
|
||||
if bindVars == nil {
|
||||
bindVars = map[string]interface{}{}
|
||||
}
|
||||
bindVars["@collection"] = TicketCollectionName
|
||||
|
||||
cursor, logEntry, err := db.Query(ctx, query, bindVars, operation)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer cursor.Close()
|
||||
|
||||
var docs []*models.TicketSimpleResponse
|
||||
for {
|
||||
doc := models.Ticket{}
|
||||
meta, err := cursor.ReadDocument(ctx, &doc)
|
||||
if driver.IsNoMoreDocuments(err) {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
resp, err := toTicketSimpleResponse(meta.Key, &doc)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
docs = append(docs, resp)
|
||||
}
|
||||
|
||||
return docs, logEntry, nil
|
||||
}
|
||||
|
||||
func (db *Database) TicketCount(ctx context.Context, typequery, filterquery string, bindVars map[string]interface{}) (int, error) {
|
||||
if bindVars == nil {
|
||||
bindVars = map[string]interface{}{}
|
||||
}
|
||||
bindVars["@collection"] = TicketCollectionName
|
||||
|
||||
ticketFilterQuery, ticketFilterVars, err := db.Hooks.TicketReadFilter(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
countQuery := `RETURN LENGTH(FOR d IN @@collection ` + ticketFilterQuery + " " + typequery + " " + filterquery + ` RETURN 1)`
|
||||
cursor, _, err := db.Query(ctx, countQuery, mergeMaps(bindVars, ticketFilterVars), busdb.ReadOperation)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
documentCount := 0
|
||||
_, err = cursor.ReadDocument(ctx, &documentCount)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
cursor.Close()
|
||||
return documentCount, nil
|
||||
}
|
||||
|
||||
func sortQuery(paramsSort []string, paramsDesc []bool, bindVars map[string]interface{}) string {
|
||||
sort := ""
|
||||
if len(paramsSort) > 0 {
|
||||
var sorts []string
|
||||
for i, column := range paramsSort {
|
||||
colsort := fmt.Sprintf("d.@column%d", i)
|
||||
if len(paramsDesc) > i && paramsDesc[i] {
|
||||
colsort += " DESC"
|
||||
}
|
||||
sorts = append(sorts, colsort)
|
||||
bindVars[fmt.Sprintf("column%d", i)] = column
|
||||
}
|
||||
sort = "SORT " + strings.Join(sorts, ", ")
|
||||
}
|
||||
return sort
|
||||
}
|
||||
|
||||
func mergeMaps(a map[string]interface{}, b map[string]interface{}) map[string]interface{} {
|
||||
merged := map[string]interface{}{}
|
||||
for k, v := range a {
|
||||
merged[k] = v
|
||||
}
|
||||
for k, v := range b {
|
||||
merged[k] = v
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
func validate(e interface{}, schema *gojsonschema.Schema) error {
|
||||
b, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := schema.Validate(gojsonschema.NewStringLoader(string(b)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(res.Errors()) > 0 {
|
||||
var l []string
|
||||
for _, e := range res.Errors() {
|
||||
l = append(l, e.String())
|
||||
}
|
||||
return fmt.Errorf("validation failed: %v", strings.Join(l, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
291
database/ticket_field.go
Normal file
291
database/ticket_field.go
Normal file
@@ -0,0 +1,291 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/arangodb/go-driver"
|
||||
"github.com/iancoleman/strcase"
|
||||
"github.com/mingrammer/commonregex"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/database/busdb"
|
||||
"github.com/SecurityBrewery/catalyst/generated/models"
|
||||
"github.com/SecurityBrewery/catalyst/pointer"
|
||||
)
|
||||
|
||||
func (db *Database) AddArtifact(ctx context.Context, id int64, artifact *models.Artifact) (*models.TicketWithTickets, error) {
|
||||
ticketFilterQuery, ticketFilterVars, err := db.Hooks.TicketWriteFilter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if artifact.Status == nil {
|
||||
artifact.Status = pointer.String("unknown")
|
||||
}
|
||||
|
||||
if artifact.Type == nil {
|
||||
artifact.Type = pointer.String(inferType(artifact.Name))
|
||||
}
|
||||
|
||||
query := `LET d = DOCUMENT(@@collection, @ID)
|
||||
` + ticketFilterQuery + `
|
||||
UPDATE d WITH { "modified": DATE_ISO8601(DATE_NOW()), "artifacts": PUSH(NOT_NULL(d.artifacts, []), @artifact) } IN @@collection
|
||||
RETURN NEW`
|
||||
return db.ticketGetQuery(ctx, id, query, mergeMaps(map[string]interface{}{"artifact": artifact}, ticketFilterVars), &busdb.Operation{
|
||||
OperationType: busdb.Update,
|
||||
Ids: []driver.DocumentID{
|
||||
driver.DocumentID(fmt.Sprintf("%s/%d", TicketCollectionName, id)),
|
||||
},
|
||||
Msg: "Add artifact",
|
||||
})
|
||||
}
|
||||
|
||||
func inferType(name string) string {
|
||||
switch {
|
||||
case commonregex.IPRegex.MatchString(name):
|
||||
return "ip"
|
||||
case commonregex.LinkRegex.MatchString(name):
|
||||
return "url"
|
||||
case commonregex.EmailRegex.MatchString(name):
|
||||
return "email"
|
||||
case commonregex.MD5HexRegex.MatchString(name):
|
||||
return "md5"
|
||||
case commonregex.SHA1HexRegex.MatchString(name):
|
||||
return "sha1"
|
||||
case commonregex.SHA256HexRegex.MatchString(name):
|
||||
return "sha256"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func (db *Database) RemoveArtifact(ctx context.Context, id int64, name string) (*models.TicketWithTickets, error) {
|
||||
ticketFilterQuery, ticketFilterVars, err := db.Hooks.TicketWriteFilter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := `LET d = DOCUMENT(@@collection, @ID)
|
||||
` + ticketFilterQuery + `
|
||||
FOR a IN NOT_NULL(d.artifacts, [])
|
||||
FILTER a.name == @name
|
||||
LET newartifacts = REMOVE_VALUE(d.artifacts, a)
|
||||
UPDATE d WITH { "modified": DATE_ISO8601(DATE_NOW()), "artifacts": newartifacts } IN @@collection
|
||||
RETURN NEW`
|
||||
return db.ticketGetQuery(ctx, id, query, mergeMaps(map[string]interface{}{"name": name}, ticketFilterVars), &busdb.Operation{
|
||||
OperationType: busdb.Update,
|
||||
Ids: []driver.DocumentID{
|
||||
driver.DocumentID(fmt.Sprintf("%s/%d", TicketCollectionName, id)),
|
||||
},
|
||||
Msg: "Remove artifact",
|
||||
})
|
||||
}
|
||||
|
||||
func (db *Database) SetTemplate(ctx context.Context, id int64, schema string) (*models.TicketWithTickets, error) {
|
||||
ticketFilterQuery, ticketFilterVars, err := db.Hooks.TicketWriteFilter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := `LET d = DOCUMENT(@@collection, @ID)
|
||||
` + ticketFilterQuery + `
|
||||
UPDATE d WITH { "schema": @schema } IN @@collection
|
||||
RETURN NEW`
|
||||
return db.ticketGetQuery(ctx, id, query, mergeMaps(map[string]interface{}{"schema": schema}, ticketFilterVars), &busdb.Operation{
|
||||
OperationType: busdb.Update,
|
||||
Ids: []driver.DocumentID{
|
||||
driver.DocumentID(fmt.Sprintf("%s/%d", TicketCollectionName, id)),
|
||||
},
|
||||
Msg: "Set Template",
|
||||
})
|
||||
}
|
||||
|
||||
func (db *Database) AddComment(ctx context.Context, id int64, comment *models.CommentForm) (*models.TicketWithTickets, error) {
|
||||
ticketFilterQuery, ticketFilterVars, err := db.Hooks.TicketWriteFilter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if comment.Creator == nil || *comment.Creator == "" {
|
||||
user, exists := busdb.UserFromContext(ctx)
|
||||
if !exists {
|
||||
return nil, errors.New("no user in context")
|
||||
}
|
||||
|
||||
comment.Creator = pointer.String(user.ID)
|
||||
}
|
||||
|
||||
if comment.Created == nil {
|
||||
comment.Created = pointer.Time(time.Now().UTC())
|
||||
}
|
||||
|
||||
query := `LET d = DOCUMENT(@@collection, @ID)
|
||||
` + ticketFilterQuery + `
|
||||
UPDATE d WITH { "modified": DATE_ISO8601(DATE_NOW()), "comments": PUSH(NOT_NULL(d.comments, []), @comment) } IN @@collection
|
||||
RETURN NEW`
|
||||
return db.ticketGetQuery(ctx, id, query, mergeMaps(map[string]interface{}{"comment": comment}, ticketFilterVars), &busdb.Operation{
|
||||
OperationType: busdb.Update,
|
||||
Ids: []driver.DocumentID{
|
||||
driver.DocumentID(fmt.Sprintf("%s/%d", TicketCollectionName, id)),
|
||||
},
|
||||
Msg: "Add comment",
|
||||
})
|
||||
}
|
||||
|
||||
func (db *Database) RemoveComment(ctx context.Context, id int64, commentID int64) (*models.TicketWithTickets, error) {
|
||||
ticketFilterQuery, ticketFilterVars, err := db.Hooks.TicketWriteFilter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := `LET d = DOCUMENT(@@collection, @ID)
|
||||
` + ticketFilterQuery + `
|
||||
UPDATE d WITH { "modified": DATE_ISO8601(DATE_NOW()), "comments": REMOVE_NTH(d.comments, @commentID) } IN @@collection
|
||||
RETURN NEW`
|
||||
return db.ticketGetQuery(ctx, id, query, mergeMaps(map[string]interface{}{"commentID": commentID}, ticketFilterVars), &busdb.Operation{
|
||||
OperationType: busdb.Update,
|
||||
Ids: []driver.DocumentID{
|
||||
driver.DocumentID(fmt.Sprintf("%s/%d", TicketCollectionName, id)),
|
||||
},
|
||||
Msg: "Remove comment",
|
||||
})
|
||||
}
|
||||
|
||||
func (db *Database) SetReferences(ctx context.Context, id int64, references []*models.Reference) (*models.TicketWithTickets, error) {
|
||||
ticketFilterQuery, ticketFilterVars, err := db.Hooks.TicketWriteFilter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := `LET d = DOCUMENT(@@collection, @ID)
|
||||
` + ticketFilterQuery + `
|
||||
UPDATE d WITH { "modified": DATE_ISO8601(DATE_NOW()), "references": @references } IN @@collection
|
||||
RETURN NEW`
|
||||
return db.ticketGetQuery(ctx, id, query, mergeMaps(map[string]interface{}{"references": references}, ticketFilterVars), &busdb.Operation{
|
||||
OperationType: busdb.Update,
|
||||
Ids: []driver.DocumentID{
|
||||
driver.DocumentID(fmt.Sprintf("%s/%d", TicketCollectionName, id)),
|
||||
},
|
||||
Msg: "Changed references",
|
||||
})
|
||||
}
|
||||
|
||||
func (db *Database) LinkFiles(ctx context.Context, id int64, files []*models.File) (*models.TicketWithTickets, error) {
|
||||
ticketFilterQuery, ticketFilterVars, err := db.Hooks.TicketWriteFilter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := `LET d = DOCUMENT(@@collection, @ID)
|
||||
` + ticketFilterQuery + `
|
||||
UPDATE d WITH { "modified": DATE_ISO8601(DATE_NOW()), "files": @files } IN @@collection
|
||||
RETURN NEW`
|
||||
return db.ticketGetQuery(ctx, id, query, mergeMaps(map[string]interface{}{"files": files}, ticketFilterVars), &busdb.Operation{
|
||||
OperationType: busdb.Update,
|
||||
Ids: []driver.DocumentID{
|
||||
driver.DocumentID(fmt.Sprintf("%s/%d", TicketCollectionName, id)),
|
||||
},
|
||||
Msg: "Linked files",
|
||||
})
|
||||
}
|
||||
|
||||
func (db *Database) AddTicketPlaybook(ctx context.Context, id int64, playbookTemplate *models.PlaybookTemplateForm) (*models.TicketWithTickets, error) {
|
||||
pb, err := toPlaybook(playbookTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ticketFilterQuery, ticketFilterVars, err := db.Hooks.TicketWriteFilter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
playbookID := strcase.ToKebab(pb.Name)
|
||||
if playbookTemplate.ID != nil {
|
||||
playbookID = *playbookTemplate.ID
|
||||
}
|
||||
|
||||
parentTicket, err := db.TicketGet(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := `FOR d IN @@collection
|
||||
` + ticketFilterQuery + `
|
||||
FILTER d._key == @ID
|
||||
LET newplaybook = ZIP( [@playbookID], [@playbook] )
|
||||
LET newplaybooks = MERGE(NOT_NULL(d.playbooks, {}), newplaybook)
|
||||
LET newticket = MERGE(d, { "modified": DATE_ISO8601(DATE_NOW()), "playbooks": newplaybooks })
|
||||
REPLACE d WITH newticket IN @@collection
|
||||
RETURN NEW`
|
||||
ticket, err := db.ticketGetQuery(ctx, id, query, mergeMaps(map[string]interface{}{
|
||||
"playbook": pb,
|
||||
"playbookID": findName(parentTicket.Playbooks, playbookID),
|
||||
}, ticketFilterVars), &busdb.Operation{
|
||||
OperationType: busdb.Update,
|
||||
Ids: []driver.DocumentID{
|
||||
driver.NewDocumentID(TicketCollectionName, fmt.Sprintf("%d", id)),
|
||||
},
|
||||
Msg: "Added playbook",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := runRootTask(extractTicketResponse(ticket), playbookID, db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
func findName(playbooks map[string]*models.PlaybookResponse, name string) string {
|
||||
if _, ok := playbooks[name]; !ok {
|
||||
return name
|
||||
}
|
||||
|
||||
for i := 0; ; i++ {
|
||||
try := fmt.Sprintf("%s%d", name, i)
|
||||
if _, ok := playbooks[try]; !ok {
|
||||
return try
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runRootTask(ticket *models.TicketResponse, playbookID string, db *Database) error {
|
||||
playbook := ticket.Playbooks[playbookID]
|
||||
|
||||
var root *models.TaskResponse
|
||||
for _, task := range playbook.Tasks {
|
||||
if task.Order == 0 {
|
||||
root = task
|
||||
}
|
||||
}
|
||||
|
||||
runNextTasks(ticket.ID, playbookID, root.Next, root.Data, ticket, db)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) RemoveTicketPlaybook(ctx context.Context, id int64, playbookID string) (*models.TicketWithTickets, error) {
|
||||
ticketFilterQuery, ticketFilterVars, err := db.Hooks.TicketWriteFilter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := `FOR d IN @@collection
|
||||
` + ticketFilterQuery + `
|
||||
FILTER d._key == @ID
|
||||
LET newplaybooks = UNSET(d.playbooks, @playbookID)
|
||||
REPLACE d WITH MERGE(d, { "modified": DATE_ISO8601(DATE_NOW()), "playbooks": newplaybooks }) IN @@collection
|
||||
RETURN NEW`
|
||||
return db.ticketGetQuery(ctx, id, query, mergeMaps(map[string]interface{}{
|
||||
"playbookID": playbookID,
|
||||
}, ticketFilterVars), &busdb.Operation{
|
||||
OperationType: busdb.Update,
|
||||
Ids: []driver.DocumentID{
|
||||
driver.NewDocumentID(TicketCollectionName, fmt.Sprintf("%d", id)),
|
||||
},
|
||||
Msg: fmt.Sprintf("Removed playbook %s", playbookID),
|
||||
})
|
||||
}
|
||||
186
database/ticket_task.go
Normal file
186
database/ticket_task.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/arangodb/go-driver"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/database/busdb"
|
||||
"github.com/SecurityBrewery/catalyst/generated/models"
|
||||
)
|
||||
|
||||
func (db *Database) TaskGet(ctx context.Context, id int64, playbookID string, taskID string) (*models.TicketWithTickets, *models.PlaybookResponse, *models.TaskWithContext, error) {
|
||||
inc, err := db.TicketGet(ctx, id)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
playbook, ok := inc.Playbooks[playbookID]
|
||||
if !ok {
|
||||
return nil, nil, nil, errors.New("playbook does not exist")
|
||||
}
|
||||
|
||||
task, ok := playbook.Tasks[taskID]
|
||||
if !ok {
|
||||
return nil, nil, nil, errors.New("task does not exist")
|
||||
}
|
||||
|
||||
return inc, playbook, &models.TaskWithContext{
|
||||
PlaybookId: playbookID,
|
||||
PlaybookName: playbook.Name,
|
||||
TaskId: taskID,
|
||||
Task: *task,
|
||||
TicketId: id,
|
||||
TicketName: inc.Name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (db *Database) TaskComplete(ctx context.Context, id int64, playbookID string, taskID string, data interface{}) (*models.TicketWithTickets, error) {
|
||||
inc, err := db.TicketGet(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
completable := inc.Playbooks[playbookID].Tasks[taskID].Active
|
||||
if !completable {
|
||||
return nil, errors.New("cannot be completed")
|
||||
}
|
||||
|
||||
ticketFilterQuery, ticketFilterVars, err := db.Hooks.TicketWriteFilter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := `LET d = DOCUMENT(@@collection, @ID)
|
||||
` + ticketFilterQuery + `
|
||||
LET playbook = d.playbooks[@playbookID]
|
||||
LET task = playbook.tasks[@taskID]
|
||||
LET newtask = MERGE(task, {"data": NOT_NULL(@data, {}), "done": true, closed: @closed })
|
||||
LET newtasks = MERGE(playbook.tasks, { @taskID: newtask } )
|
||||
LET newplaybook = MERGE(playbook, {"tasks": newtasks})
|
||||
LET newplaybooks = MERGE(d.playbooks, { @playbookID: newplaybook } )
|
||||
|
||||
UPDATE d WITH { "modified": DATE_ISO8601(DATE_NOW()), "playbooks": newplaybooks } IN @@collection
|
||||
RETURN NEW`
|
||||
ticket, err := db.ticketGetQuery(ctx, id, query, mergeMaps(map[string]interface{}{
|
||||
"playbookID": playbookID,
|
||||
"taskID": taskID,
|
||||
"data": data,
|
||||
"closed": time.Now().UTC(),
|
||||
}, ticketFilterVars), &busdb.Operation{
|
||||
OperationType: busdb.Update,
|
||||
Ids: []driver.DocumentID{
|
||||
driver.NewDocumentID(TicketCollectionName, fmt.Sprintf("%d", id)),
|
||||
},
|
||||
Msg: fmt.Sprintf("Completed task %s in playbook %s", taskID, playbookID),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
playbook := ticket.Playbooks[playbookID]
|
||||
task := playbook.Tasks[taskID]
|
||||
|
||||
runNextTasks(id, playbookID, task.Next, task.Data, extractTicketResponse(ticket), db)
|
||||
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
func extractTicketResponse(ticket *models.TicketWithTickets) *models.TicketResponse {
|
||||
return &models.TicketResponse{
|
||||
Artifacts: ticket.Artifacts,
|
||||
Comments: ticket.Comments,
|
||||
Created: ticket.Created,
|
||||
Details: ticket.Details,
|
||||
Files: ticket.Files,
|
||||
ID: ticket.ID,
|
||||
Modified: ticket.Modified,
|
||||
Name: ticket.Name,
|
||||
Owner: ticket.Owner,
|
||||
Playbooks: ticket.Playbooks,
|
||||
Read: ticket.Read,
|
||||
References: ticket.References,
|
||||
Schema: ticket.Schema,
|
||||
Status: ticket.Status,
|
||||
Type: ticket.Type,
|
||||
Write: ticket.Write,
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Database) TaskUpdate(ctx context.Context, id int64, playbookID string, taskID string, task *models.Task) (*models.TicketWithTickets, error) {
|
||||
ticketFilterQuery, ticketFilterVars, err := db.Hooks.TicketWriteFilter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := `LET d = DOCUMENT(@@collection, @ID)
|
||||
` + ticketFilterQuery + `
|
||||
LET playbook = d.playbooks[@playbookID]
|
||||
LET newtasks = MERGE(playbook.tasks, { @taskID: @task } )
|
||||
LET newplaybook = MERGE(playbook, {"tasks": newtasks})
|
||||
LET newplaybooks = MERGE(d.playbooks, { @playbookID: newplaybook } )
|
||||
|
||||
UPDATE d WITH { "modified": DATE_ISO8601(DATE_NOW()), "playbooks": newplaybooks } IN @@collection
|
||||
RETURN NEW`
|
||||
ticket, err := db.ticketGetQuery(ctx, id, query, mergeMaps(map[string]interface{}{
|
||||
"playbookID": playbookID,
|
||||
"taskID": taskID,
|
||||
"task": task,
|
||||
}, ticketFilterVars), &busdb.Operation{
|
||||
OperationType: busdb.Update,
|
||||
Ids: []driver.DocumentID{
|
||||
driver.NewDocumentID(TicketCollectionName, fmt.Sprintf("%d", id)),
|
||||
},
|
||||
Msg: fmt.Sprintf("Saved task %s in playbook %s", taskID, playbookID),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
func (db *Database) TaskRun(ctx context.Context, id int64, playbookID string, taskID string) error {
|
||||
ticket, _, task, err := db.TaskGet(ctx, id, playbookID, taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if task.Task.Type == models.TaskTypeAutomation {
|
||||
if err := runTask(id, playbookID, taskID, &task.Task, extractTicketResponse(ticket), db); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runNextTasks(id int64, playbookID string, next map[string]string, data interface{}, ticket *models.TicketResponse, db *Database) {
|
||||
for nextTaskID, requirement := range next {
|
||||
nextTask := ticket.Playbooks[playbookID].Tasks[nextTaskID]
|
||||
if nextTask.Type == models.TaskTypeAutomation {
|
||||
b, err := evalRequirement(requirement, data)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if b {
|
||||
if err := runTask(id, playbookID, nextTaskID, nextTask, ticket, db); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runTask(ticketID int64, playbookID string, taskID string, task *models.TaskResponse, ticket *models.TicketResponse, db *Database) error {
|
||||
playbook := ticket.Playbooks[playbookID]
|
||||
msgContext := &models.Context{Playbook: playbook, Task: task, Ticket: ticket}
|
||||
origin := &models.Origin{TaskOrigin: &models.TaskOrigin{TaskId: taskID, PlaybookId: playbookID, TicketId: ticketID}}
|
||||
jobID := uuid.NewString()
|
||||
return publishJobMapping(jobID, *task.Automation, msgContext, origin, task.Payload, db)
|
||||
}
|
||||
101
database/tickettype.go
Normal file
101
database/tickettype.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/arangodb/go-driver"
|
||||
"github.com/iancoleman/strcase"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/database/busdb"
|
||||
"github.com/SecurityBrewery/catalyst/generated/models"
|
||||
)
|
||||
|
||||
func toTicketType(doc *models.TicketTypeForm) *models.TicketType {
|
||||
return &models.TicketType{
|
||||
Name: doc.Name,
|
||||
Icon: doc.Icon,
|
||||
DefaultPlaybooks: doc.DefaultPlaybooks,
|
||||
DefaultTemplate: doc.DefaultTemplate,
|
||||
DefaultGroups: doc.DefaultGroups,
|
||||
}
|
||||
}
|
||||
|
||||
func toTicketTypeResponse(key string, doc *models.TicketType) *models.TicketTypeResponse {
|
||||
return &models.TicketTypeResponse{
|
||||
ID: key,
|
||||
Name: doc.Name,
|
||||
Icon: doc.Icon,
|
||||
DefaultPlaybooks: doc.DefaultPlaybooks,
|
||||
DefaultTemplate: doc.DefaultTemplate,
|
||||
DefaultGroups: doc.DefaultGroups,
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Database) TicketTypeCreate(ctx context.Context, tickettype *models.TicketTypeForm) (*models.TicketTypeResponse, error) {
|
||||
if tickettype == nil {
|
||||
return nil, errors.New("requires ticket type")
|
||||
}
|
||||
if tickettype.Name == "" {
|
||||
return nil, errors.New("requires ticket type name")
|
||||
}
|
||||
|
||||
var doc models.TicketType
|
||||
newctx := driver.WithReturnNew(ctx, &doc)
|
||||
|
||||
meta, err := db.tickettypeCollection.CreateDocument(ctx, newctx, strcase.ToKebab(tickettype.Name), toTicketType(tickettype))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toTicketTypeResponse(meta.Key, &doc), nil
|
||||
}
|
||||
|
||||
func (db *Database) TicketTypeGet(ctx context.Context, id string) (*models.TicketTypeResponse, error) {
|
||||
var doc models.TicketType
|
||||
meta, err := db.tickettypeCollection.ReadDocument(ctx, id, &doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toTicketTypeResponse(meta.Key, &doc), nil
|
||||
}
|
||||
|
||||
func (db *Database) TicketTypeUpdate(ctx context.Context, id string, tickettype *models.TicketTypeForm) (*models.TicketTypeResponse, error) {
|
||||
var doc models.TicketType
|
||||
ctx = driver.WithReturnNew(ctx, &doc)
|
||||
|
||||
meta, err := db.tickettypeCollection.ReplaceDocument(ctx, id, toTicketType(tickettype))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toTicketTypeResponse(meta.Key, &doc), nil
|
||||
}
|
||||
|
||||
func (db *Database) TicketTypeDelete(ctx context.Context, id string) error {
|
||||
_, err := db.tickettypeCollection.RemoveDocument(ctx, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Database) TicketTypeList(ctx context.Context) ([]*models.TicketTypeResponse, error) {
|
||||
query := "FOR d IN @@collection RETURN d"
|
||||
cursor, _, err := db.Query(ctx, query, map[string]interface{}{"@collection": TicketTypeCollectionName}, busdb.ReadOperation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close()
|
||||
var docs []*models.TicketTypeResponse
|
||||
for {
|
||||
var doc models.TicketType
|
||||
meta, err := cursor.ReadDocument(ctx, &doc)
|
||||
if driver.IsNoMoreDocuments(err) {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
docs = append(docs, toTicketTypeResponse(meta.Key, &doc))
|
||||
}
|
||||
|
||||
return docs, err
|
||||
}
|
||||
201
database/user.go
Normal file
201
database/user.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/arangodb/go-driver"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/iancoleman/strcase"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/database/busdb"
|
||||
"github.com/SecurityBrewery/catalyst/generated/models"
|
||||
"github.com/SecurityBrewery/catalyst/pointer"
|
||||
"github.com/SecurityBrewery/catalyst/role"
|
||||
)
|
||||
|
||||
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_")
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func generateKey() string {
|
||||
b := make([]rune, 32)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func toUser(user *models.UserForm, sha256 *string) *models.User {
|
||||
roles := []string{}
|
||||
roles = append(roles, role.Strings(role.Explodes(user.Roles))...)
|
||||
u := &models.User{
|
||||
Blocked: user.Blocked,
|
||||
Roles: roles,
|
||||
Sha256: sha256,
|
||||
Apikey: user.Apikey,
|
||||
}
|
||||
|
||||
// log.Println(u)
|
||||
// b, _ := json.Marshal(u)
|
||||
// loader := gojsonschema.NewBytesLoader(b)
|
||||
// res, err := models.UserSchema.Validate(loader)
|
||||
// if err != nil {
|
||||
// log.Println(err)
|
||||
// }
|
||||
// log.Println(res.Errors())
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
func toUserResponse(key string, user *models.User) *models.UserResponse {
|
||||
return &models.UserResponse{
|
||||
ID: key,
|
||||
Roles: user.Roles,
|
||||
Blocked: user.Blocked,
|
||||
Apikey: user.Apikey,
|
||||
}
|
||||
}
|
||||
|
||||
func toNewUserResponse(key string, user *models.User, secret *string) *models.NewUserResponse {
|
||||
return &models.NewUserResponse{
|
||||
ID: key,
|
||||
Roles: user.Roles,
|
||||
Secret: secret,
|
||||
Blocked: user.Blocked,
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Database) UserGetOrCreate(ctx *gin.Context, newUser *models.UserForm) (*models.UserResponse, error) {
|
||||
user, err := db.UserGet(ctx, newUser.ID)
|
||||
if err != nil {
|
||||
newUser, err := db.UserCreate(ctx, newUser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &models.UserResponse{ID: newUser.ID, Roles: newUser.Roles, Blocked: newUser.Blocked}, nil
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (db *Database) UserCreate(ctx context.Context, newUser *models.UserForm) (*models.NewUserResponse, error) {
|
||||
var key string
|
||||
var hash *string
|
||||
if newUser.Apikey {
|
||||
key = generateKey()
|
||||
hash = pointer.String(fmt.Sprintf("%x", sha256.Sum256([]byte(key))))
|
||||
}
|
||||
|
||||
var doc models.User
|
||||
newctx := driver.WithReturnNew(ctx, &doc)
|
||||
meta, err := db.userCollection.CreateDocument(ctx, newctx, strcase.ToKebab(newUser.ID), toUser(newUser, hash))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toNewUserResponse(meta.Key, &doc, pointer.String(key)), nil
|
||||
}
|
||||
|
||||
func (db *Database) UserCreateSetupAPIKey(ctx context.Context, key string) (*models.UserResponse, error) {
|
||||
newUser := &models.UserForm{
|
||||
ID: "setup",
|
||||
Roles: []string{role.Admin},
|
||||
Apikey: true,
|
||||
Blocked: false,
|
||||
}
|
||||
hash := pointer.String(fmt.Sprintf("%x", sha256.Sum256([]byte(key))))
|
||||
|
||||
var doc models.User
|
||||
newctx := driver.WithReturnNew(ctx, &doc)
|
||||
meta, err := db.userCollection.CreateDocument(ctx, newctx, strcase.ToKebab(newUser.ID), toUser(newUser, hash))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toUserResponse(meta.Key, &doc), nil
|
||||
}
|
||||
|
||||
func (db *Database) UserGet(ctx context.Context, id string) (*models.UserResponse, error) {
|
||||
var doc models.User
|
||||
meta, err := db.userCollection.ReadDocument(ctx, id, &doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toUserResponse(meta.Key, &doc), nil
|
||||
}
|
||||
|
||||
func (db *Database) UserDelete(ctx context.Context, id string) error {
|
||||
_, err := db.userCollection.RemoveDocument(ctx, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Database) UserList(ctx context.Context) ([]*models.UserResponse, error) {
|
||||
query := "FOR d IN @@collection RETURN d"
|
||||
cursor, _, err := db.Query(ctx, query, map[string]interface{}{"@collection": UserCollectionName}, busdb.ReadOperation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close()
|
||||
var docs []*models.UserResponse
|
||||
for {
|
||||
var doc models.User
|
||||
meta, err := cursor.ReadDocument(ctx, &doc)
|
||||
if driver.IsNoMoreDocuments(err) {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc.Sha256 = nil
|
||||
docs = append(docs, toUserResponse(meta.Key, &doc))
|
||||
}
|
||||
|
||||
return docs, err
|
||||
}
|
||||
|
||||
func (db *Database) UserByHash(ctx context.Context, sha256 string) (*models.UserResponse, error) {
|
||||
query := `FOR d in @@collection
|
||||
FILTER d.sha256 == @sha256
|
||||
RETURN d`
|
||||
|
||||
cursor, _, err := db.Query(ctx, query, map[string]interface{}{"@collection": UserCollectionName, "sha256": sha256}, busdb.ReadOperation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close()
|
||||
|
||||
var doc models.User
|
||||
meta, err := cursor.ReadDocument(ctx, &doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toUserResponse(meta.Key, &doc), err
|
||||
}
|
||||
|
||||
func (db *Database) UserUpdate(ctx context.Context, id string, user *models.UserForm) (*models.UserResponse, error) {
|
||||
var doc models.User
|
||||
_, err := db.userCollection.ReadDocument(ctx, id, &doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if doc.Sha256 != nil {
|
||||
return nil, errors.New("cannot update an API key")
|
||||
}
|
||||
|
||||
ctx = driver.WithReturnNew(ctx, &doc)
|
||||
|
||||
meta, err := db.userCollection.ReplaceDocument(ctx, id, toUser(user, nil))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toUserResponse(meta.Key, &doc), nil
|
||||
}
|
||||
Reference in New Issue
Block a user