diff --git a/automation/automation.go b/automation/automation.go deleted file mode 100644 index 1cd2053..0000000 --- a/automation/automation.go +++ /dev/null @@ -1,26 +0,0 @@ -package automation - -import ( - "context" - "log" - - "github.com/SecurityBrewery/catalyst/bus" - "github.com/SecurityBrewery/catalyst/database" - "github.com/SecurityBrewery/catalyst/database/busdb" - "github.com/SecurityBrewery/catalyst/generated/models" - "github.com/SecurityBrewery/catalyst/role" -) - -func New(apiurl, apikey string, bus *bus.Bus, db *database.Database) error { - if err := jobAutomation(jobContext(), apiurl, apikey, bus, db); err != nil { - log.Fatal(err) - } - - return resultAutomation(bus, db) -} - -func jobContext() context.Context { - // TODO: change roles? - bot := &models.UserResponse{ID: "bot", Roles: []string{role.Admin}} - return busdb.UserContext(context.Background(), bot) -} diff --git a/automation/job.go b/automation/job.go deleted file mode 100644 index fe98daa..0000000 --- a/automation/job.go +++ /dev/null @@ -1,116 +0,0 @@ -package automation - -import ( - "encoding/json" - "fmt" - "log" - - "golang.org/x/net/context" - - "github.com/SecurityBrewery/catalyst/bus" - "github.com/SecurityBrewery/catalyst/database" - "github.com/SecurityBrewery/catalyst/generated/models" -) - -func jobAutomation(ctx context.Context, apiurl, apikey string, catalystBus *bus.Bus, db *database.Database) error { - return catalystBus.SubscribeJob(func(automationMsg *bus.JobMsg) { - job, err := db.JobCreate(ctx, automationMsg.ID, &models.JobForm{ - Automation: automationMsg.Automation, - Payload: automationMsg.Message.Payload, - Origin: automationMsg.Origin, - }) - if err != nil { - log.Println(err) - return - } - - automation, err := db.AutomationGet(ctx, automationMsg.Automation) - if err != nil { - log.Println(err) - return - } - - if automation.Script == "" { - log.Println("automation is empty") - return - } - - if automationMsg.Message.Secrets == nil { - automationMsg.Message.Secrets = map[string]string{} - } - automationMsg.Message.Secrets["catalyst_apikey"] = apikey - automationMsg.Message.Secrets["catalyst_apiurl"] = apiurl - - scriptMessage, _ := json.Marshal(automationMsg.Message) - - containerID, logs, err := createContainer(ctx, automation.Image, automation.Script, string(scriptMessage)) - if err != nil { - log.Println(err) - return - } - - if _, err := db.JobUpdate(ctx, automationMsg.ID, &models.Job{ - Automation: job.Automation, - Container: &containerID, - Origin: job.Origin, - Output: job.Output, - Log: &logs, - Payload: job.Payload, - Status: job.Status, - }); err != nil { - log.Println(err) - return - } - - var result map[string]interface{} - - stdout, _, err := runDocker(ctx, automationMsg.ID, containerID, db) - if err != nil { - result = map[string]interface{}{"error": fmt.Sprintf("error running script %s %s", err, string(stdout))} - } else { - var data map[string]interface{} - if err := json.Unmarshal(stdout, &data); err != nil { - result = map[string]interface{}{"error": string(stdout)} - } else { - result = data - } - } - - if err := catalystBus.PublishResult(automationMsg.Automation, result, automationMsg.Origin); err != nil { - log.Println(err) - } - - if err := db.JobComplete(ctx, automationMsg.ID, result); err != nil { - log.Println(err) - return - } - }) -} - -/* -func getAutomation(automationID string, config *Config) (*models.AutomationResponse, error) { - req, err := http.NewRequest(http.MethodGet, config.CatalystAPIUrl+"/automations/"+automationID, nil) - if err != nil { - return nil, err - } - - req.Header.Set("PRIVATE-TOKEN", config.CatalystAPIKey) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - b, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - var automation models.AutomationResponse - if err := json.Unmarshal(b, &automation); err != nil { - return nil, err - } - return &automation, nil -} -*/ diff --git a/automation/result.go b/automation/result.go deleted file mode 100644 index a7e5ff6..0000000 --- a/automation/result.go +++ /dev/null @@ -1,38 +0,0 @@ -package automation - -import ( - "log" - - "github.com/SecurityBrewery/catalyst/bus" - "github.com/SecurityBrewery/catalyst/database" - "github.com/SecurityBrewery/catalyst/generated/models" -) - -func resultAutomation(catalystBus *bus.Bus, db *database.Database) error { - return catalystBus.SubscribeResult(func(resultMsg *bus.ResultMsg) { - if resultMsg.Target != nil { - ctx := jobContext() - switch { - case resultMsg.Target.TaskOrigin != nil: - if _, err := db.TaskComplete( - ctx, - resultMsg.Target.TaskOrigin.TicketId, - resultMsg.Target.TaskOrigin.PlaybookId, - resultMsg.Target.TaskOrigin.TaskId, - resultMsg.Data, - ); err != nil { - log.Println(err) - } - case resultMsg.Target.ArtifactOrigin != nil: - enrichment := &models.EnrichmentForm{ - Data: resultMsg.Data, - Name: resultMsg.Automation, - } - _, err := db.EnrichArtifact(ctx, resultMsg.Target.ArtifactOrigin.TicketId, resultMsg.Target.ArtifactOrigin.Artifact, enrichment) - if err != nil { - log.Println(err) - } - } - } - }) -} diff --git a/bus/bus.go b/bus/bus.go index 6492552..53d97a9 100644 --- a/bus/bus.go +++ b/bus/bus.go @@ -4,16 +4,7 @@ import ( "encoding/json" "log" - "github.com/arangodb/go-driver" emitter "github.com/emitter-io/go/v2" - - "github.com/SecurityBrewery/catalyst/generated/models" -) - -const ( - channelUpdate = "data" - channelJob = "job" - channelResult = "result" ) type Bus struct { @@ -22,25 +13,13 @@ type Bus struct { } type Config struct { - Host string - Key string - resultBusKey string - jobBusKey string - dataBusKey string - APIUrl string -} - -type JobMsg struct { - ID string `json:"id"` - Automation string `json:"automation"` - Origin *models.Origin `json:"origin"` - Message *models.Message `json:"message"` -} - -type ResultMsg struct { - Automation string `json:"automation"` - Data map[string]interface{} `json:"data,omitempty"` - Target *models.Origin `json:"target"` + Host string + Key string + databaseUpdateBusKey string + jobBusKey string + resultBusKey string + requestKey string + APIUrl string } func New(c *Config) (*Bus, error) { @@ -51,7 +30,7 @@ func New(c *Config) (*Bus, error) { return nil, err } - c.dataBusKey, err = client.GenerateKey(c.Key, channelUpdate+"/", "rwls", 0) + c.databaseUpdateBusKey, err = client.GenerateKey(c.Key, channelDatabaseUpdate+"/", "rwls", 0) if err != nil { return nil, err } @@ -63,30 +42,14 @@ func New(c *Config) (*Bus, error) { if err != nil { return nil, err } + c.requestKey, err = client.GenerateKey(c.Key, ChannelRequest+"/", "rwls", 0) + if err != nil { + return nil, err + } return &Bus{config: c, client: client}, err } -func (b *Bus) PublishUpdate(ids []driver.DocumentID) error { - return b.jsonPublish(ids, channelUpdate, b.config.dataBusKey) -} - -func (b *Bus) PublishJob(id, automation string, payload interface{}, context *models.Context, origin *models.Origin) error { - return b.jsonPublish(&JobMsg{ - ID: id, - Automation: automation, - Origin: origin, - Message: &models.Message{ - Context: context, - Payload: payload, - }, - }, channelJob, b.config.jobBusKey) -} - -func (b *Bus) PublishResult(automation string, data map[string]interface{}, target *models.Origin) error { - return b.jsonPublish(&ResultMsg{Automation: automation, Data: data, Target: target}, channelResult, b.config.resultBusKey) -} - func (b *Bus) jsonPublish(msg interface{}, channel, key string) error { payload, err := json.Marshal(msg) if err != nil { @@ -96,39 +59,6 @@ func (b *Bus) jsonPublish(msg interface{}, channel, key string) error { return b.client.Publish(key, channel, payload) } -func (b *Bus) SubscribeUpdate(f func(ids []driver.DocumentID)) error { - return b.safeSubscribe(b.config.dataBusKey, channelUpdate, func(c *emitter.Client, m emitter.Message) { - var msg []driver.DocumentID - if err := json.Unmarshal(m.Payload(), &msg); err != nil { - log.Println(err) - return - } - go f(msg) - }) -} - -func (b *Bus) SubscribeJob(f func(msg *JobMsg)) error { - return b.safeSubscribe(b.config.jobBusKey, channelJob, func(c *emitter.Client, m emitter.Message) { - var msg JobMsg - if err := json.Unmarshal(m.Payload(), &msg); err != nil { - log.Println(err) - return - } - go f(&msg) - }) -} - -func (b *Bus) SubscribeResult(f func(msg *ResultMsg)) error { - return b.safeSubscribe(b.config.resultBusKey, channelResult, func(c *emitter.Client, m emitter.Message) { - var msg ResultMsg - if err := json.Unmarshal(m.Payload(), &msg); err != nil { - log.Println(err) - return - } - go f(&msg) - }) -} - func (b *Bus) safeSubscribe(key, channel string, handler func(c *emitter.Client, m emitter.Message)) error { defer func() { if r := recover(); r != nil { diff --git a/bus/databaseupdate.go b/bus/databaseupdate.go new file mode 100644 index 0000000..e22e459 --- /dev/null +++ b/bus/databaseupdate.go @@ -0,0 +1,42 @@ +package bus + +import ( + "encoding/json" + "log" + + "github.com/arangodb/go-driver" + emitter "github.com/emitter-io/go/v2" +) + +const channelDatabaseUpdate = "databaseupdate" + +type DatabaseUpdateType string + +const ( + DatabaseEntryRead DatabaseUpdateType = "read" + DatabaseEntryCreated DatabaseUpdateType = "created" + DatabaseEntryUpdated DatabaseUpdateType = "updated" +) + +type DatabaseUpdateMsg struct { + IDs []driver.DocumentID `json:"ids"` + Type DatabaseUpdateType `json:"type"` +} + +func (b *Bus) PublishDatabaseUpdate(ids []driver.DocumentID, databaseUpdateType DatabaseUpdateType) error { + return b.jsonPublish(&DatabaseUpdateMsg{ + IDs: ids, + Type: databaseUpdateType, + }, channelDatabaseUpdate, b.config.databaseUpdateBusKey) +} + +func (b *Bus) SubscribeDatabaseUpdate(f func(msg *DatabaseUpdateMsg)) error { + return b.safeSubscribe(b.config.databaseUpdateBusKey, channelDatabaseUpdate, func(c *emitter.Client, m emitter.Message) { + var msg DatabaseUpdateMsg + if err := json.Unmarshal(m.Payload(), &msg); err != nil { + log.Println(err) + return + } + go f(&msg) + }) +} diff --git a/bus/job.go b/bus/job.go new file mode 100644 index 0000000..169fee4 --- /dev/null +++ b/bus/job.go @@ -0,0 +1,42 @@ +package bus + +import ( + "encoding/json" + "log" + + emitter "github.com/emitter-io/go/v2" + + "github.com/SecurityBrewery/catalyst/generated/models" +) + +const channelJob = "job" + +type JobMsg struct { + ID string `json:"id"` + Automation string `json:"automation"` + Origin *models.Origin `json:"origin"` + Message *models.Message `json:"message"` +} + +func (b *Bus) PublishJob(id, automation string, payload interface{}, context *models.Context, origin *models.Origin) error { + return b.jsonPublish(&JobMsg{ + ID: id, + Automation: automation, + Origin: origin, + Message: &models.Message{ + Context: context, + Payload: payload, + }, + }, channelJob, b.config.jobBusKey) +} + +func (b *Bus) SubscribeJob(f func(msg *JobMsg)) error { + return b.safeSubscribe(b.config.jobBusKey, channelJob, func(c *emitter.Client, m emitter.Message) { + var msg JobMsg + if err := json.Unmarshal(m.Payload(), &msg); err != nil { + log.Println(err) + return + } + go f(&msg) + }) +} diff --git a/bus/request.go b/bus/request.go new file mode 100644 index 0000000..9fbccde --- /dev/null +++ b/bus/request.go @@ -0,0 +1,36 @@ +package bus + +import ( + "encoding/json" + "log" + + "github.com/arangodb/go-driver" + emitter "github.com/emitter-io/go/v2" +) + +const ChannelRequest = "request" + +type RequestMsg struct { + IDs []driver.DocumentID `json:"ids"` + Function string `json:"function"` + User string `json:"user"` +} + +func (b *Bus) PublishRequest(user, f string, ids []driver.DocumentID) error { + return b.jsonPublish(&RequestMsg{ + User: user, + Function: f, + IDs: ids, + }, ChannelRequest, b.config.requestKey) +} + +func (b *Bus) SubscribeRequest(f func(msg *RequestMsg)) error { + return b.safeSubscribe(b.config.requestKey, ChannelRequest, func(c *emitter.Client, m emitter.Message) { + var msg RequestMsg + if err := json.Unmarshal(m.Payload(), &msg); err != nil { + log.Println(err) + return + } + go f(&msg) + }) +} diff --git a/bus/result.go b/bus/result.go new file mode 100644 index 0000000..c564881 --- /dev/null +++ b/bus/result.go @@ -0,0 +1,33 @@ +package bus + +import ( + "encoding/json" + "log" + + emitter "github.com/emitter-io/go/v2" + + "github.com/SecurityBrewery/catalyst/generated/models" +) + +const channelResult = "result" + +type ResultMsg struct { + Automation string `json:"automation"` + Data map[string]interface{} `json:"data,omitempty"` + Target *models.Origin `json:"target"` +} + +func (b *Bus) PublishResult(automation string, data map[string]interface{}, target *models.Origin) error { + return b.jsonPublish(&ResultMsg{Automation: automation, Data: data, Target: target}, channelResult, b.config.resultBusKey) +} + +func (b *Bus) SubscribeResult(f func(msg *ResultMsg)) error { + return b.safeSubscribe(b.config.resultBusKey, channelResult, func(c *emitter.Client, m emitter.Message) { + var msg ResultMsg + if err := json.Unmarshal(m.Payload(), &msg); err != nil { + log.Println(err) + return + } + go f(&msg) + }) +} diff --git a/busservice/busservice.go b/busservice/busservice.go new file mode 100644 index 0000000..4ff1d34 --- /dev/null +++ b/busservice/busservice.go @@ -0,0 +1,60 @@ +package busservice + +import ( + "context" + "log" + + "github.com/SecurityBrewery/catalyst/bus" + "github.com/SecurityBrewery/catalyst/database" + "github.com/SecurityBrewery/catalyst/database/busdb" + "github.com/SecurityBrewery/catalyst/generated/models" + "github.com/SecurityBrewery/catalyst/role" + "github.com/SecurityBrewery/catalyst/time" +) + +type busService struct { + db *database.Database + apiURL string + apiKey string + catalystBus *bus.Bus +} + +func New(apiurl, apikey string, catalystBus *bus.Bus, db *database.Database) error { + + h := &busService{db: db, apiURL: apiurl, apiKey: apikey, catalystBus: catalystBus} + + if err := catalystBus.SubscribeRequest(h.logRequest); err != nil { + return err + } + if err := catalystBus.SubscribeResult(h.handleResult); err != nil { + return err + } + if err := catalystBus.SubscribeJob(h.handleJob); err != nil { + return err + } + + return nil +} + +func busContext() context.Context { + // TODO: change roles? + bot := &models.UserResponse{ID: "bot", Roles: []string{role.Admin}} + return busdb.UserContext(context.Background(), bot) +} + +func (h *busService) logRequest(msg *bus.RequestMsg) { + var logEntries []*models.LogEntry + for _, i := range msg.IDs { + logEntries = append(logEntries, &models.LogEntry{ + Type: bus.ChannelRequest, + Reference: i.String(), + Creator: msg.User, + Message: msg.Function, + Created: time.Now().UTC(), + }) + } + + if err := h.db.LogBatchCreate(busContext(), logEntries); err != nil { + log.Println(err) + } +} diff --git a/automation/docker.go b/busservice/docker.go similarity index 99% rename from automation/docker.go rename to busservice/docker.go index c92b223..e4ac35f 100644 --- a/automation/docker.go +++ b/busservice/docker.go @@ -1,4 +1,4 @@ -package automation +package busservice import ( "archive/tar" diff --git a/busservice/job.go b/busservice/job.go new file mode 100644 index 0000000..c520053 --- /dev/null +++ b/busservice/job.go @@ -0,0 +1,113 @@ +package busservice + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/SecurityBrewery/catalyst/bus" + "github.com/SecurityBrewery/catalyst/generated/models" +) + +func (h *busService) handleJob(automationMsg *bus.JobMsg) { + ctx := busContext() + + job, err := h.db.JobCreate(ctx, automationMsg.ID, &models.JobForm{ + Automation: automationMsg.Automation, + Payload: automationMsg.Message.Payload, + Origin: automationMsg.Origin, + }) + if err != nil { + log.Println(err) + return + } + + automation, err := h.db.AutomationGet(ctx, automationMsg.Automation) + if err != nil { + log.Println(err) + return + } + + if automation.Script == "" { + log.Println("automation is empty") + return + } + + if automationMsg.Message.Secrets == nil { + automationMsg.Message.Secrets = map[string]string{} + } + automationMsg.Message.Secrets["catalyst_apikey"] = h.apiKey + automationMsg.Message.Secrets["catalyst_apiurl"] = h.apiURL + + scriptMessage, _ := json.Marshal(automationMsg.Message) + + containerID, logs, err := createContainer(ctx, automation.Image, automation.Script, string(scriptMessage)) + if err != nil { + log.Println(err) + return + } + + if _, err := h.db.JobUpdate(ctx, automationMsg.ID, &models.Job{ + Automation: job.Automation, + Container: &containerID, + Origin: job.Origin, + Output: job.Output, + Log: &logs, + Payload: job.Payload, + Status: job.Status, + }); err != nil { + log.Println(err) + return + } + + var result map[string]interface{} + + stdout, _, err := runDocker(ctx, automationMsg.ID, containerID, h.db) + if err != nil { + result = map[string]interface{}{"error": fmt.Sprintf("error running script %s %s", err, string(stdout))} + } else { + var data map[string]interface{} + if err := json.Unmarshal(stdout, &data); err != nil { + result = map[string]interface{}{"error": string(stdout)} + } else { + result = data + } + } + + if err := h.catalystBus.PublishResult(automationMsg.Automation, result, automationMsg.Origin); err != nil { + log.Println(err) + } + + if err := h.db.JobComplete(ctx, automationMsg.ID, result); err != nil { + log.Println(err) + return + } +} + +/* +func getAutomation(automationID string, config *Config) (*models.AutomationResponse, error) { + req, err := http.NewRequest(http.MethodGet, config.CatalystAPIUrl+"/automations/"+automationID, nil) + if err != nil { + return nil, err + } + + req.Header.Set("PRIVATE-TOKEN", config.CatalystAPIKey) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var automation models.AutomationResponse + if err := json.Unmarshal(b, &automation); err != nil { + return nil, err + } + return &automation, nil +} +*/ diff --git a/busservice/result.go b/busservice/result.go new file mode 100644 index 0000000..322f8cb --- /dev/null +++ b/busservice/result.go @@ -0,0 +1,35 @@ +package busservice + +import ( + "log" + + "github.com/SecurityBrewery/catalyst/bus" + "github.com/SecurityBrewery/catalyst/generated/models" +) + +func (h *busService) handleResult(resultMsg *bus.ResultMsg) { + if resultMsg.Target != nil { + ctx := busContext() + switch { + case resultMsg.Target.TaskOrigin != nil: + if _, err := h.db.TaskComplete( + ctx, + resultMsg.Target.TaskOrigin.TicketId, + resultMsg.Target.TaskOrigin.PlaybookId, + resultMsg.Target.TaskOrigin.TaskId, + resultMsg.Data, + ); err != nil { + log.Println(err) + } + case resultMsg.Target.ArtifactOrigin != nil: + enrichment := &models.EnrichmentForm{ + Data: resultMsg.Data, + Name: resultMsg.Automation, + } + _, err := h.db.EnrichArtifact(ctx, resultMsg.Target.ArtifactOrigin.TicketId, resultMsg.Target.ArtifactOrigin.Artifact, enrichment) + if err != nil { + log.Println(err) + } + } + } +} diff --git a/database/artifact.go b/database/artifact.go index 6a860df..c34b634 100644 --- a/database/artifact.go +++ b/database/artifact.go @@ -6,6 +6,7 @@ import ( "github.com/arangodb/go-driver" + "github.com/SecurityBrewery/catalyst/bus" "github.com/SecurityBrewery/catalyst/database/busdb" "github.com/SecurityBrewery/catalyst/generated/models" "github.com/SecurityBrewery/catalyst/time" @@ -60,11 +61,10 @@ func (db *Database) ArtifactUpdate(ctx context.Context, id int64, name string, a "name": name, "artifact": artifact, }, ticketFilterVars), &busdb.Operation{ - OperationType: busdb.Update, + Type: bus.DatabaseEntryUpdated, Ids: []driver.DocumentID{ driver.DocumentID(fmt.Sprintf("%s/%d", TicketCollectionName, id)), }, - Msg: fmt.Sprintf("Update artifact %s", name), }) } @@ -92,10 +92,9 @@ func (db *Database) EnrichArtifact(ctx context.Context, id int64, name string, e "enrichmentname": enrichment.Name, "enrichment": enrichment, }, ticketFilterVars), &busdb.Operation{ - OperationType: busdb.Update, + Type: bus.DatabaseEntryUpdated, Ids: []driver.DocumentID{ driver.DocumentID(fmt.Sprintf("%s/%d", TicketCollectionName, id)), }, - Msg: fmt.Sprintf("Run %s on artifact", enrichment.Name), }) } diff --git a/database/busdb/busdb.go b/database/busdb/busdb.go index 6366147..8d1074d 100644 --- a/database/busdb/busdb.go +++ b/database/busdb/busdb.go @@ -33,22 +33,13 @@ func NewDatabase(ctx context.Context, internal driver.Database, b *bus.Bus) (*Bu }, nil } -type OperationType int - -const ( - Create OperationType = iota - Read = iota - Update = iota -) - type Operation struct { - OperationType OperationType - Ids []driver.DocumentID - Msg string + Type bus.DatabaseUpdateType + Ids []driver.DocumentID } -var CreateOperation = &Operation{OperationType: Create} -var ReadOperation = &Operation{OperationType: Read} +var CreateOperation = &Operation{Type: bus.DatabaseEntryCreated} +var ReadOperation = &Operation{Type: bus.DatabaseEntryRead} 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) @@ -59,8 +50,8 @@ func (db BusDatabase) Query(ctx context.Context, query string, vars map[string]i var logs *models.LogEntry switch { - case operation.OperationType == Update: - if err := db.LogAndNotify(ctx, operation.Ids, operation.Msg); err != nil { + case operation.Type == bus.DatabaseEntryCreated, operation.Type == bus.DatabaseEntryUpdated: + if err := db.bus.PublishDatabaseUpdate(operation.Ids, operation.Type); err != nil { return nil, nil, err } } @@ -68,19 +59,6 @@ func (db BusDatabase) Query(ctx context.Context, query string, vars map[string]i 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) } @@ -104,7 +82,7 @@ func (c Collection) CreateDocument(ctx, newctx context.Context, key string, docu return meta, err } - err = c.db.LogAndNotify(ctx, []driver.DocumentID{meta.ID}, "Document created") + err = c.db.bus.PublishDatabaseUpdate([]driver.DocumentID{meta.ID}, bus.DatabaseEntryCreated) if err != nil { return meta, err } @@ -117,7 +95,7 @@ func (c Collection) CreateEdge(ctx, newctx context.Context, edge *driver.EdgeDoc return meta, err } - err = c.db.LogAndNotify(ctx, []driver.DocumentID{meta.ID}, "Document created") + err = c.db.bus.PublishDatabaseUpdate([]driver.DocumentID{meta.ID}, bus.DatabaseEntryCreated) if err != nil { return meta, err } @@ -138,7 +116,7 @@ func (c Collection) CreateEdges(ctx context.Context, edges []*driver.EdgeDocumen ids = append(ids, meta.ID) } - err = c.db.LogAndNotify(ctx, ids, "Document created") + err = c.db.bus.PublishDatabaseUpdate(ids, bus.DatabaseEntryCreated) if err != nil { return metas, err } @@ -160,7 +138,7 @@ func (c Collection) UpdateDocument(ctx context.Context, key string, update inter return meta, err } - return meta, c.db.bus.PublishUpdate([]driver.DocumentID{meta.ID}) + return meta, c.db.bus.PublishDatabaseUpdate([]driver.DocumentID{meta.ID}, bus.DatabaseEntryUpdated) } func (c Collection) ReplaceDocument(ctx context.Context, key string, document interface{}) (driver.DocumentMeta, error) { @@ -169,7 +147,7 @@ func (c Collection) ReplaceDocument(ctx context.Context, key string, document in return meta, err } - return meta, c.db.bus.PublishUpdate([]driver.DocumentID{meta.ID}) + return meta, c.db.bus.PublishDatabaseUpdate([]driver.DocumentID{meta.ID}, bus.DatabaseEntryUpdated) } func (c Collection) RemoveDocument(ctx context.Context, formatInt string) (driver.DocumentMeta, error) { diff --git a/database/busdb/log.go b/database/busdb/log.go index ef198f2..c16de36 100644 --- a/database/busdb/log.go +++ b/database/busdb/log.go @@ -3,6 +3,8 @@ package busdb import ( "context" "errors" + "github.com/SecurityBrewery/catalyst/bus" + "strings" "github.com/arangodb/go-driver" @@ -12,15 +14,16 @@ import ( const LogCollectionName = "logs" -func (db *BusDatabase) LogCreate(ctx context.Context, reference, message string) (*models.LogEntry, error) { +func (db *BusDatabase) LogCreate(ctx context.Context, logType, reference, message string) (*models.LogEntry, error) { user, ok := UserFromContext(ctx) if !ok { return nil, errors.New("no user in context") } logentry := &models.LogEntry{ + Type: logType, Reference: reference, - Created: time.Now(), + Created: time.Now().UTC(), Creator: user.ID, Message: message, } @@ -31,27 +34,18 @@ func (db *BusDatabase) LogCreate(ctx context.Context, reference, message string) return nil, err } - return &doc, db.bus.PublishUpdate([]driver.DocumentID{driver.DocumentID(logentry.Reference)}) + return &doc, nil } -func (db *BusDatabase) LogBatchCreate(ctx context.Context, logEntryForms []*models.LogEntry) error { - user, ok := UserFromContext(ctx) - if !ok { - return errors.New("no user in context") - } - +func (db *BusDatabase) LogBatchCreate(ctx context.Context, logentries []*models.LogEntry) error { 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, + for _, entry := range logentries { + if strings.HasPrefix(entry.Reference, "tickets/") { + ids = append(ids, driver.DocumentID(entry.Reference)) } - - logentries = append(logentries, logentry) - ids = append(ids, driver.DocumentID(logentry.Reference)) + } + if ids != nil { + go db.bus.PublishDatabaseUpdate(ids, bus.DatabaseEntryCreated) } _, errs, err := db.logCollection.CreateDocuments(ctx, logentries) @@ -63,7 +57,7 @@ func (db *BusDatabase) LogBatchCreate(ctx context.Context, logEntryForms []*mode return err } - return db.bus.PublishUpdate(ids) + return nil } func (db *BusDatabase) LogList(ctx context.Context, reference string) ([]*models.LogEntry, error) { diff --git a/database/job.go b/database/job.go index 5143ea3..f3cfbce 100644 --- a/database/job.go +++ b/database/job.go @@ -11,6 +11,7 @@ import ( "github.com/docker/docker/client" "github.com/xeipuuv/gojsonschema" + "github.com/SecurityBrewery/catalyst/bus" "github.com/SecurityBrewery/catalyst/caql" "github.com/SecurityBrewery/catalyst/database/busdb" "github.com/SecurityBrewery/catalyst/generated/models" @@ -144,11 +145,10 @@ func (db *Database) JobLogAppend(ctx context.Context, id string, logLine string) "ID": id, "logline": logLine, }, &busdb.Operation{ - OperationType: busdb.Update, + Type: bus.DatabaseEntryUpdated, Ids: []driver.DocumentID{ driver.DocumentID(fmt.Sprintf("%s/%s", JobCollectionName, id)), }, - Msg: fmt.Sprintf("Append logline"), }) if err != nil { return err @@ -166,11 +166,10 @@ func (db *Database) JobComplete(ctx context.Context, id string, out interface{}) "ID": id, "out": out, }, &busdb.Operation{ - OperationType: busdb.Update, + Type: bus.DatabaseEntryUpdated, Ids: []driver.DocumentID{ driver.DocumentID(fmt.Sprintf("%s/%s", JobCollectionName, id)), }, - Msg: fmt.Sprintf("Set output"), }) if err != nil { return err diff --git a/database/relationships.go b/database/relationships.go index 95ff059..2bc1e8d 100644 --- a/database/relationships.go +++ b/database/relationships.go @@ -7,6 +7,7 @@ import ( "github.com/arangodb/go-driver" + "github.com/SecurityBrewery/catalyst/bus" "github.com/SecurityBrewery/catalyst/database/busdb" ) @@ -37,12 +38,11 @@ func (db *Database) RelatedRemove(ctx context.Context, id, id2 int64) error { "id": driver.DocumentID(TicketCollectionName + "/" + strconv.Itoa(int(id))), "id2": driver.DocumentID(TicketCollectionName + "/" + strconv.Itoa(int(id2))), }, &busdb.Operation{ - OperationType: busdb.Update, + Type: bus.DatabaseEntryUpdated, Ids: []driver.DocumentID{ driver.DocumentID(TicketCollectionName + "/" + strconv.Itoa(int(id))), driver.DocumentID(TicketCollectionName + "/" + strconv.Itoa(int(id2))), }, - Msg: "Removed ticket/artifact relation", }) return err } diff --git a/database/ticket.go b/database/ticket.go index ffcc604..7aac399 100644 --- a/database/ticket.go +++ b/database/ticket.go @@ -13,6 +13,7 @@ import ( "github.com/arangodb/go-driver" "github.com/xeipuuv/gojsonschema" + "github.com/SecurityBrewery/catalyst/bus" "github.com/SecurityBrewery/catalyst/caql" "github.com/SecurityBrewery/catalyst/database/busdb" "github.com/SecurityBrewery/catalyst/generated/models" @@ -133,7 +134,7 @@ func toTicketSimpleResponse(key string, ticket *models.Ticket) (*models.TicketSi }, nil } -func toTicketWithTickets(ticketResponse *models.TicketResponse, tickets []*models.TicketSimpleResponse) *models.TicketWithTickets { +func toTicketWithTickets(ticketResponse *models.TicketResponse, tickets []*models.TicketSimpleResponse, logs []*models.LogEntry) *models.TicketWithTickets { return &models.TicketWithTickets{ Artifacts: ticketResponse.Artifacts, Comments: ticketResponse.Comments, @@ -152,6 +153,7 @@ func toTicketWithTickets(ticketResponse *models.TicketResponse, tickets []*model Type: ticketResponse.Type, Write: ticketResponse.Write, + Logs: logs, Tickets: tickets, } } @@ -244,9 +246,8 @@ func (db *Database) TicketBatchCreate(ctx context.Context, ticketForms []*models 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 - } + + go db.bus.PublishDatabaseUpdate(ids, bus.DatabaseEntryUpdated) ticketResponses, err := toTicketResponses(apiTickets) if err != nil { @@ -405,7 +406,12 @@ func (db *Database) ticketGetQuery(ctx context.Context, ticketID int64, query st return nil, err } - return toTicketWithTickets(ticketResponse, tickets), nil + logs, err := db.LogList(ctx, fmt.Sprintf("%s/%d", TicketCollectionName, ticketID)) + if err != nil { + return nil, err + } + + return toTicketWithTickets(ticketResponse, tickets, logs), nil } func (db *Database) TicketUpdate(ctx context.Context, ticketID int64, ticket *models.Ticket) (*models.TicketWithTickets, error) { @@ -420,10 +426,9 @@ func (db *Database) TicketUpdate(ctx context.Context, ticketID int64, ticket *mo 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{ + Type: bus.DatabaseEntryUpdated, Ids: []driver.DocumentID{ driver.NewDocumentID(TicketCollectionName, strconv.FormatInt(ticketID, 10)), }, - Msg: "Ticket updated", }) } diff --git a/database/ticket_field.go b/database/ticket_field.go index 31dd890..e7944ba 100644 --- a/database/ticket_field.go +++ b/database/ticket_field.go @@ -9,6 +9,7 @@ import ( "github.com/iancoleman/strcase" "github.com/mingrammer/commonregex" + "github.com/SecurityBrewery/catalyst/bus" "github.com/SecurityBrewery/catalyst/database/busdb" "github.com/SecurityBrewery/catalyst/generated/models" "github.com/SecurityBrewery/catalyst/pointer" @@ -34,11 +35,10 @@ func (db *Database) AddArtifact(ctx context.Context, id int64, artifact *models. UPDATE d WITH { "modified": @now, "artifacts": PUSH(NOT_NULL(d.artifacts, []), @artifact) } IN @@collection RETURN NEW` return db.ticketGetQuery(ctx, id, query, mergeMaps(map[string]interface{}{"artifact": artifact, "now": time.Now().UTC()}, ticketFilterVars), &busdb.Operation{ - OperationType: busdb.Update, + Type: bus.DatabaseEntryUpdated, Ids: []driver.DocumentID{ driver.DocumentID(fmt.Sprintf("%s/%d", TicketCollectionName, id)), }, - Msg: "Add artifact", }) } @@ -74,11 +74,10 @@ func (db *Database) RemoveArtifact(ctx context.Context, id int64, name string) ( UPDATE d WITH { "modified": @now, "artifacts": newartifacts } IN @@collection RETURN NEW` return db.ticketGetQuery(ctx, id, query, mergeMaps(map[string]interface{}{"name": name, "now": time.Now().UTC()}, ticketFilterVars), &busdb.Operation{ - OperationType: busdb.Update, + Type: bus.DatabaseEntryUpdated, Ids: []driver.DocumentID{ driver.DocumentID(fmt.Sprintf("%s/%d", TicketCollectionName, id)), }, - Msg: "Remove artifact", }) } @@ -93,11 +92,10 @@ func (db *Database) SetTemplate(ctx context.Context, id int64, schema string) (* 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, + Type: bus.DatabaseEntryUpdated, Ids: []driver.DocumentID{ driver.DocumentID(fmt.Sprintf("%s/%d", TicketCollectionName, id)), }, - Msg: "Set Template", }) } @@ -125,11 +123,10 @@ func (db *Database) AddComment(ctx context.Context, id int64, comment *models.Co UPDATE d WITH { "modified": @now, "comments": PUSH(NOT_NULL(d.comments, []), @comment) } IN @@collection RETURN NEW` return db.ticketGetQuery(ctx, id, query, mergeMaps(map[string]interface{}{"comment": comment, "now": time.Now().UTC()}, ticketFilterVars), &busdb.Operation{ - OperationType: busdb.Update, + Type: bus.DatabaseEntryUpdated, Ids: []driver.DocumentID{ driver.DocumentID(fmt.Sprintf("%s/%d", TicketCollectionName, id)), }, - Msg: "Add comment", }) } @@ -144,11 +141,10 @@ func (db *Database) RemoveComment(ctx context.Context, id int64, commentID int64 UPDATE d WITH { "modified": @now, "comments": REMOVE_NTH(d.comments, @commentID) } IN @@collection RETURN NEW` return db.ticketGetQuery(ctx, id, query, mergeMaps(map[string]interface{}{"commentID": commentID, "now": time.Now().UTC()}, ticketFilterVars), &busdb.Operation{ - OperationType: busdb.Update, + Type: bus.DatabaseEntryUpdated, Ids: []driver.DocumentID{ driver.DocumentID(fmt.Sprintf("%s/%d", TicketCollectionName, id)), }, - Msg: "Remove comment", }) } @@ -163,11 +159,10 @@ func (db *Database) SetReferences(ctx context.Context, id int64, references []*m UPDATE d WITH { "modified": @now, "references": @references } IN @@collection RETURN NEW` return db.ticketGetQuery(ctx, id, query, mergeMaps(map[string]interface{}{"references": references, "now": time.Now().UTC()}, ticketFilterVars), &busdb.Operation{ - OperationType: busdb.Update, + Type: bus.DatabaseEntryUpdated, Ids: []driver.DocumentID{ driver.DocumentID(fmt.Sprintf("%s/%d", TicketCollectionName, id)), }, - Msg: "Changed references", }) } @@ -182,11 +177,10 @@ func (db *Database) LinkFiles(ctx context.Context, id int64, files []*models.Fil UPDATE d WITH { "modified": @now, "files": @files } IN @@collection RETURN NEW` return db.ticketGetQuery(ctx, id, query, mergeMaps(map[string]interface{}{"files": files, "now": time.Now().UTC()}, ticketFilterVars), &busdb.Operation{ - OperationType: busdb.Update, + Type: bus.DatabaseEntryUpdated, Ids: []driver.DocumentID{ driver.DocumentID(fmt.Sprintf("%s/%d", TicketCollectionName, id)), }, - Msg: "Linked files", }) } @@ -224,11 +218,10 @@ func (db *Database) AddTicketPlaybook(ctx context.Context, id int64, playbookTem "playbookID": findName(parentTicket.Playbooks, playbookID), "now": time.Now().UTC(), }, ticketFilterVars), &busdb.Operation{ - OperationType: busdb.Update, + Type: bus.DatabaseEntryUpdated, Ids: []driver.DocumentID{ driver.NewDocumentID(TicketCollectionName, fmt.Sprintf("%d", id)), }, - Msg: "Added playbook", }) if err != nil { return nil, err @@ -284,10 +277,9 @@ func (db *Database) RemoveTicketPlaybook(ctx context.Context, id int64, playbook "playbookID": playbookID, "now": time.Now().UTC(), }, ticketFilterVars), &busdb.Operation{ - OperationType: busdb.Update, + Type: bus.DatabaseEntryUpdated, Ids: []driver.DocumentID{ driver.NewDocumentID(TicketCollectionName, fmt.Sprintf("%d", id)), }, - Msg: fmt.Sprintf("Removed playbook %s", playbookID), }) } diff --git a/database/ticket_task.go b/database/ticket_task.go index 510a883..af5cfe0 100644 --- a/database/ticket_task.go +++ b/database/ticket_task.go @@ -9,6 +9,7 @@ import ( "github.com/arangodb/go-driver" "github.com/google/uuid" + "github.com/SecurityBrewery/catalyst/bus" "github.com/SecurityBrewery/catalyst/database/busdb" "github.com/SecurityBrewery/catalyst/generated/models" "github.com/SecurityBrewery/catalyst/time" @@ -74,11 +75,10 @@ func (db *Database) TaskComplete(ctx context.Context, id int64, playbookID strin "closed": time.Now().UTC(), "now": time.Now().UTC(), }, ticketFilterVars), &busdb.Operation{ - OperationType: busdb.Update, + Type: bus.DatabaseEntryUpdated, 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 @@ -136,11 +136,10 @@ func (db *Database) TaskUpdate(ctx context.Context, id int64, playbookID string, "task": task, "now": time.Now().UTC(), }, ticketFilterVars), &busdb.Operation{ - OperationType: busdb.Update, + Type: bus.DatabaseEntryUpdated, 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 diff --git a/definition/logs.yaml b/definition/logs.yaml index d06c73b..2337dc4 100644 --- a/definition/logs.yaml +++ b/definition/logs.yaml @@ -16,14 +16,15 @@ paths: schema: { type: array, items: { $ref: "#/definitions/LogEntry" } } examples: test: - - { "created": "2021-12-12T12:12:12.000000012Z","creator": "bob","reference": "tickets/294511","message": "Fail run account resist lend solve incident centre priority temperature. Cause change distribution examine location technique shape partner milk customer. Rail tea plate soil report cook railway interpretation breath action. Exercise dream accept park conclusion addition shoot assistance may answer. Gold writer link stop combine hear power name commitment operation. Determine lifespan support grow degree henry exclude detail set religion. Direct library policy convention chain retain discover ride walk student. Gather proposal select march aspect play noise avoid encourage employ. Assessment preserve transport combine wish influence income guess run stand. Charge limit crime ignore statement foundation study issue stop claim." } + - { type: "manual", "created": "2021-12-12T12:12:12.000000012Z","creator": "bob","reference": "tickets/294511","message": "Fail run account resist lend solve incident centre priority temperature. Cause change distribution examine location technique shape partner milk customer. Rail tea plate soil report cook railway interpretation breath action. Exercise dream accept park conclusion addition shoot assistance may answer. Gold writer link stop combine hear power name commitment operation. Determine lifespan support grow degree henry exclude detail set religion. Direct library policy convention chain retain discover ride walk student. Gather proposal select march aspect play noise avoid encourage employ. Assessment preserve transport combine wish influence income guess run stand. Charge limit crime ignore statement foundation study issue stop claim." } security: [ { roles: [ "log:read" ] } ] definitions: LogEntry: type: object - required: [ reference, creator, created, message ] + required: [ type, reference, creator, created, message ] properties: + type: { type: string } reference: { type: string } creator: { type: string } created: { type: string, format: "date-time" } diff --git a/definition/tickets.yaml b/definition/tickets.yaml index 83714c1..6ae6c32 100644 --- a/definition/tickets.yaml +++ b/definition/tickets.yaml @@ -1037,6 +1037,8 @@ definitions: comments: { type: array, items: { $ref: '#/definitions/Comment' } } artifacts: { type: array, items: { $ref: "#/definitions/Artifact" } } + logs: { type: array, items: { $ref: '#/definitions/LogEntry' } } + created: { type: string, format: "date-time", example: "1985-04-12T23:20:50.52Z" } modified: { type: string, format: "date-time", example: "1985-04-12T23:20:50.52Z" } diff --git a/generated/catalyst.json b/generated/catalyst.json index 3dfec04..7699c7e 100644 --- a/generated/catalyst.json +++ b/generated/catalyst.json @@ -723,7 +723,8 @@ "created" : "2021-12-12T12:12:12.000+0000", "creator" : "bob", "message" : "Fail run account resist lend solve incident centre priority temperature. Cause change distribution examine location technique shape partner milk customer. Rail tea plate soil report cook railway interpretation breath action. Exercise dream accept park conclusion addition shoot assistance may answer. Gold writer link stop combine hear power name commitment operation. Determine lifespan support grow degree henry exclude detail set religion. Direct library policy convention chain retain discover ride walk student. Gather proposal select march aspect play noise avoid encourage employ. Assessment preserve transport combine wish influence income guess run stand. Charge limit crime ignore statement foundation study issue stop claim.", - "reference" : "tickets/294511" + "reference" : "tickets/294511", + "type" : "manual" } ] } }, @@ -5418,9 +5419,12 @@ }, "reference" : { "type" : "string" + }, + "type" : { + "type" : "string" } }, - "required" : [ "created", "creator", "message", "reference" ], + "required" : [ "created", "creator", "message", "reference", "type" ], "type" : "object" }, "Message" : { @@ -6445,6 +6449,12 @@ "format" : "int64", "type" : "integer" }, + "logs" : { + "items" : { + "$ref" : "#/components/schemas/LogEntry" + }, + "type" : "array" + }, "modified" : { "format" : "date-time", "type" : "string" diff --git a/generated/catalyst.yml b/generated/catalyst.yml index 18d0b2b..4d65698 100644 --- a/generated/catalyst.yml +++ b/generated/catalyst.yml @@ -312,7 +312,10 @@ definitions: type: string reference: type: string + type: + type: string required: + - type - reference - creator - created @@ -1152,6 +1155,10 @@ definitions: example: 123 format: int64 type: integer + logs: + items: + $ref: '#/definitions/LogEntry' + type: array modified: example: 1985-04-12T23:20:50.52Z format: date-time @@ -1945,6 +1952,7 @@ paths: combine wish influence income guess run stand. Charge limit crime ignore statement foundation study issue stop claim. reference: tickets/294511 + type: manual schema: items: $ref: '#/definitions/LogEntry' diff --git a/generated/community.json b/generated/community.json index d7e76d2..3ff0129 100644 --- a/generated/community.json +++ b/generated/community.json @@ -491,7 +491,8 @@ "created" : "2021-12-12T12:12:12.000+0000", "creator" : "bob", "message" : "Fail run account resist lend solve incident centre priority temperature. Cause change distribution examine location technique shape partner milk customer. Rail tea plate soil report cook railway interpretation breath action. Exercise dream accept park conclusion addition shoot assistance may answer. Gold writer link stop combine hear power name commitment operation. Determine lifespan support grow degree henry exclude detail set religion. Direct library policy convention chain retain discover ride walk student. Gather proposal select march aspect play noise avoid encourage employ. Assessment preserve transport combine wish influence income guess run stand. Charge limit crime ignore statement foundation study issue stop claim.", - "reference" : "tickets/294511" + "reference" : "tickets/294511", + "type" : "manual" } ] } }, @@ -4905,9 +4906,12 @@ }, "reference" : { "type" : "string" + }, + "type" : { + "type" : "string" } }, - "required" : [ "created", "creator", "message", "reference" ], + "required" : [ "created", "creator", "message", "reference", "type" ], "type" : "object" }, "Message" : { @@ -5866,6 +5870,12 @@ "format" : "int64", "type" : "integer" }, + "logs" : { + "items" : { + "$ref" : "#/components/schemas/LogEntry" + }, + "type" : "array" + }, "modified" : { "format" : "date-time", "type" : "string" diff --git a/generated/community.yml b/generated/community.yml index 9727056..091956b 100644 --- a/generated/community.yml +++ b/generated/community.yml @@ -247,7 +247,10 @@ definitions: type: string reference: type: string + type: + type: string required: + - type - reference - creator - created @@ -1033,6 +1036,10 @@ definitions: example: 123 format: int64 type: integer + logs: + items: + $ref: '#/definitions/LogEntry' + type: array modified: example: 1985-04-12T23:20:50.52Z format: date-time @@ -1674,6 +1681,7 @@ paths: combine wish influence income guess run stand. Charge limit crime ignore statement foundation study issue stop claim. reference: tickets/294511 + type: manual schema: items: $ref: '#/definitions/LogEntry' diff --git a/generated/models/models.go b/generated/models/models.go index 3cb7667..a24788f 100755 --- a/generated/models/models.go +++ b/generated/models/models.go @@ -77,7 +77,7 @@ func init() { gojsonschema.NewStringLoader(`{"type":"object","required":["automation","running","status"],"x-embed":"","properties":{"automation":{"type":"string"},"container":{"type":"string"},"log":{"type":"string"},"origin":{"$ref":"#/definitions/Origin"},"output":{"type":"object"},"payload":{},"running":{"type":"boolean"},"status":{"type":"string"}},"$id":"#/definitions/Job"}`), gojsonschema.NewStringLoader(`{"type":"object","required":["automation"],"x-embed":"","properties":{"automation":{"type":"string"},"origin":{"$ref":"#/definitions/Origin"},"payload":{}},"$id":"#/definitions/JobForm"}`), gojsonschema.NewStringLoader(`{"type":"object","required":["id","automation","status"],"x-embed":"","properties":{"automation":{"type":"string"},"container":{"type":"string"},"id":{"type":"string"},"log":{"type":"string"},"origin":{"$ref":"#/definitions/Origin"},"output":{"type":"object"},"payload":{},"status":{"type":"string"}},"$id":"#/definitions/JobResponse"}`), - gojsonschema.NewStringLoader(`{"type":"object","required":["reference","creator","created","message"],"x-embed":"","properties":{"created":{"format":"date-time","type":"string"},"creator":{"type":"string"},"message":{"type":"string"},"reference":{"type":"string"}},"$id":"#/definitions/LogEntry"}`), + gojsonschema.NewStringLoader(`{"type":"object","required":["type","reference","creator","created","message"],"x-embed":"","properties":{"created":{"format":"date-time","type":"string"},"creator":{"type":"string"},"message":{"type":"string"},"reference":{"type":"string"},"type":{"type":"string"}},"$id":"#/definitions/LogEntry"}`), gojsonschema.NewStringLoader(`{"type":"object","x-embed":"","properties":{"context":{"$ref":"#/definitions/Context"},"payload":{"type":"object"},"secrets":{"type":"object","additionalProperties":{"type":"string"}}},"$id":"#/definitions/Message"}`), gojsonschema.NewStringLoader(`{"type":"object","required":["id","blocked","roles"],"x-embed":"","properties":{"blocked":{"type":"boolean"},"id":{"type":"string"},"roles":{"items":{"type":"string"},"type":"array"},"secret":{"type":"string"}},"$id":"#/definitions/NewUserResponse"}`), gojsonschema.NewStringLoader(`{"type":"object","x-embed":"","properties":{"artifact_origin":{"$ref":"#/definitions/ArtifactOrigin"},"task_origin":{"$ref":"#/definitions/TaskOrigin"}},"$id":"#/definitions/Origin"}`), @@ -105,7 +105,7 @@ func init() { gojsonschema.NewStringLoader(`{"type":"object","required":["name","icon","default_template","default_playbooks"],"x-embed":"","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"}},"$id":"#/definitions/TicketType"}`), gojsonschema.NewStringLoader(`{"type":"object","required":["name","icon","default_template","default_playbooks"],"x-embed":"","properties":{"default_groups":{"items":{"type":"string"},"type":"array"},"default_playbooks":{"items":{"type":"string"},"type":"array"},"default_template":{"type":"string"},"icon":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"}},"$id":"#/definitions/TicketTypeForm"}`), gojsonschema.NewStringLoader(`{"type":"object","required":["id","name","icon","default_template","default_playbooks"],"x-embed":"","properties":{"default_groups":{"items":{"type":"string"},"type":"array"},"default_playbooks":{"items":{"type":"string"},"type":"array"},"default_template":{"type":"string"},"icon":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"}},"$id":"#/definitions/TicketTypeResponse"}`), - gojsonschema.NewStringLoader(`{"type":"object","required":["id","name","type","status","created","modified","schema"],"x-embed":"","properties":{"artifacts":{"items":{"$ref":"#/definitions/Artifact"},"type":"array"},"comments":{"items":{"$ref":"#/definitions/Comment"},"type":"array"},"created":{"format":"date-time","type":"string"},"details":{"type":"object"},"files":{"items":{"$ref":"#/definitions/File"},"type":"array"},"id":{"format":"int64","type":"integer"},"modified":{"format":"date-time","type":"string"},"name":{"type":"string"},"owner":{"type":"string"},"playbooks":{"type":"object","additionalProperties":{"$ref":"#/definitions/PlaybookResponse"}},"read":{"items":{"type":"string"},"type":"array"},"references":{"items":{"$ref":"#/definitions/Reference"},"type":"array"},"schema":{"type":"string"},"status":{"type":"string"},"tickets":{"items":{"$ref":"#/definitions/TicketSimpleResponse"},"type":"array"},"type":{"type":"string"},"write":{"items":{"type":"string"},"type":"array"}},"$id":"#/definitions/TicketWithTickets"}`), + gojsonschema.NewStringLoader(`{"type":"object","required":["id","name","type","status","created","modified","schema"],"x-embed":"","properties":{"artifacts":{"items":{"$ref":"#/definitions/Artifact"},"type":"array"},"comments":{"items":{"$ref":"#/definitions/Comment"},"type":"array"},"created":{"format":"date-time","type":"string"},"details":{"type":"object"},"files":{"items":{"$ref":"#/definitions/File"},"type":"array"},"id":{"format":"int64","type":"integer"},"logs":{"items":{"$ref":"#/definitions/LogEntry"},"type":"array"},"modified":{"format":"date-time","type":"string"},"name":{"type":"string"},"owner":{"type":"string"},"playbooks":{"type":"object","additionalProperties":{"$ref":"#/definitions/PlaybookResponse"}},"read":{"items":{"type":"string"},"type":"array"},"references":{"items":{"$ref":"#/definitions/Reference"},"type":"array"},"schema":{"type":"string"},"status":{"type":"string"},"tickets":{"items":{"$ref":"#/definitions/TicketSimpleResponse"},"type":"array"},"type":{"type":"string"},"write":{"items":{"type":"string"},"type":"array"}},"$id":"#/definitions/TicketWithTickets"}`), gojsonschema.NewStringLoader(`{"type":"object","required":["id","name","icon"],"x-embed":"","properties":{"color":{"title":"Color","type":"string","enum":["error","info","success","warning"]},"icon":{"title":"Icon (https://materialdesignicons.com)","type":"string"},"id":{"title":"ID","type":"string"},"name":{"title":"Name","type":"string"}},"$id":"#/definitions/Type"}`), gojsonschema.NewStringLoader(`{"type":"object","required":["blocked","apikey","roles"],"x-embed":"","properties":{"apikey":{"type":"boolean"},"blocked":{"type":"boolean"},"roles":{"items":{"type":"string"},"type":"array"},"sha256":{"type":"string"}},"$id":"#/definitions/User"}`), gojsonschema.NewStringLoader(`{"type":"object","x-embed":"","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"}},"$id":"#/definitions/UserData"}`), @@ -271,6 +271,7 @@ type LogEntry struct { Creator string `json:"creator"` Message string `json:"message"` Reference string `json:"reference"` + Type string `json:"type"` } type Message struct { @@ -530,6 +531,7 @@ type TicketWithTickets struct { Details interface{} `json:"details,omitempty"` Files []*File `json:"files,omitempty"` ID int64 `json:"id"` + Logs []*LogEntry `json:"logs,omitempty"` Modified time.Time `json:"modified"` Name string `json:"name"` Owner *string `json:"owner,omitempty"` diff --git a/generated/restapi/embedded_spec.go b/generated/restapi/embedded_spec.go index 94c4828..be92fc3 100644 --- a/generated/restapi/embedded_spec.go +++ b/generated/restapi/embedded_spec.go @@ -640,7 +640,8 @@ func init() { "created": "2021-12-12T12:12:12.000000012Z", "creator": "bob", "message": "Fail run account resist lend solve incident centre priority temperature. Cause change distribution examine location technique shape partner milk customer. Rail tea plate soil report cook railway interpretation breath action. Exercise dream accept park conclusion addition shoot assistance may answer. Gold writer link stop combine hear power name commitment operation. Determine lifespan support grow degree henry exclude detail set religion. Direct library policy convention chain retain discover ride walk student. Gather proposal select march aspect play noise avoid encourage employ. Assessment preserve transport combine wish influence income guess run stand. Charge limit crime ignore statement foundation study issue stop claim.", - "reference": "tickets/294511" + "reference": "tickets/294511", + "type": "manual" } ] } @@ -5680,6 +5681,7 @@ func init() { "LogEntry": { "type": "object", "required": [ + "type", "reference", "creator", "created", @@ -5698,6 +5700,9 @@ func init() { }, "reference": { "type": "string" + }, + "type": { + "type": "string" } } }, @@ -6816,6 +6821,12 @@ func init() { "format": "int64", "example": 123 }, + "logs": { + "type": "array", + "items": { + "$ref": "#/definitions/LogEntry" + } + }, "modified": { "type": "string", "format": "date-time", @@ -7668,7 +7679,8 @@ func init() { "created": "2021-12-12T12:12:12.000000012Z", "creator": "bob", "message": "Fail run account resist lend solve incident centre priority temperature. Cause change distribution examine location technique shape partner milk customer. Rail tea plate soil report cook railway interpretation breath action. Exercise dream accept park conclusion addition shoot assistance may answer. Gold writer link stop combine hear power name commitment operation. Determine lifespan support grow degree henry exclude detail set religion. Direct library policy convention chain retain discover ride walk student. Gather proposal select march aspect play noise avoid encourage employ. Assessment preserve transport combine wish influence income guess run stand. Charge limit crime ignore statement foundation study issue stop claim.", - "reference": "tickets/294511" + "reference": "tickets/294511", + "type": "manual" } ] } @@ -12708,6 +12720,7 @@ func init() { "LogEntry": { "type": "object", "required": [ + "type", "reference", "creator", "created", @@ -12726,6 +12739,9 @@ func init() { }, "reference": { "type": "string" + }, + "type": { + "type": "string" } } }, @@ -13844,6 +13860,12 @@ func init() { "format": "int64", "example": 123 }, + "logs": { + "type": "array", + "items": { + "$ref": "#/definitions/LogEntry" + } + }, "modified": { "type": "string", "format": "date-time", diff --git a/generated/test/api_server_test.go b/generated/test/api_server_test.go index c9b4d24..670b349 100644 --- a/generated/test/api_server_test.go +++ b/generated/test/api_server_test.go @@ -233,7 +233,7 @@ func TestService(t *testing.T) { args: args{method: "GET", url: "/api/logs/tickets%252F294511"}, want: want{ status: 200, - body: []interface{}{map[string]interface{}{"created": "2021-12-12T12:12:12.000000012Z", "creator": "bob", "message": "Fail run account resist lend solve incident centre priority temperature. Cause change distribution examine location technique shape partner milk customer. Rail tea plate soil report cook railway interpretation breath action. Exercise dream accept park conclusion addition shoot assistance may answer. Gold writer link stop combine hear power name commitment operation. Determine lifespan support grow degree henry exclude detail set religion. Direct library policy convention chain retain discover ride walk student. Gather proposal select march aspect play noise avoid encourage employ. Assessment preserve transport combine wish influence income guess run stand. Charge limit crime ignore statement foundation study issue stop claim.", "reference": "tickets/294511"}}, + body: []interface{}{map[string]interface{}{"created": "2021-12-12T12:12:12.000000012Z", "creator": "bob", "message": "Fail run account resist lend solve incident centre priority temperature. Cause change distribution examine location technique shape partner milk customer. Rail tea plate soil report cook railway interpretation breath action. Exercise dream accept park conclusion addition shoot assistance may answer. Gold writer link stop combine hear power name commitment operation. Determine lifespan support grow degree henry exclude detail set religion. Direct library policy convention chain retain discover ride walk student. Gather proposal select march aspect play noise avoid encourage employ. Assessment preserve transport combine wish influence income guess run stand. Charge limit crime ignore statement foundation study issue stop claim.", "reference": "tickets/294511", "type": "manual"}}, }, }, { diff --git a/go.mod b/go.mod index e9cfd8a..bebcb14 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( go.etcd.io/bbolt v1.3.6 // indirect go.mongodb.org/mongo-driver v1.7.4 // indirect golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect - golang.org/x/net v0.0.0-20211105192438-b53810dc28af + golang.org/x/net v0.0.0-20211105192438-b53810dc28af // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect diff --git a/server.go b/server.go index 3643123..c26eb26 100644 --- a/server.go +++ b/server.go @@ -8,8 +8,8 @@ import ( "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" - "github.com/SecurityBrewery/catalyst/automation" "github.com/SecurityBrewery/catalyst/bus" + "github.com/SecurityBrewery/catalyst/busservice" "github.com/SecurityBrewery/catalyst/database" "github.com/SecurityBrewery/catalyst/database/busdb" "github.com/SecurityBrewery/catalyst/generated/models" @@ -72,7 +72,7 @@ func New(hooks *hooks.Hooks, config *Config) (*Server, error) { return nil, err } - err = automation.New(config.Bus.APIUrl, config.InitialAPIKey, catalystBus, catalystDatabase) + err = busservice.New(config.Bus.APIUrl, config.InitialAPIKey, catalystBus, catalystDatabase) if err != nil { return nil, err } diff --git a/service/artifact.go b/service/artifact.go index a5a960a..3b4b25f 100644 --- a/service/artifact.go +++ b/service/artifact.go @@ -10,25 +10,29 @@ import ( "github.com/SecurityBrewery/catalyst/generated/restapi/operations/tickets" ) -func (s *Service) RunArtifact(ctx context.Context, params *tickets.RunArtifactParams) *api.Response { +func (s *Service) RunArtifact(ctx context.Context, params *tickets.RunArtifactParams) (r *api.Response) { artifact, err := s.database.ArtifactGet(ctx, params.ID, params.Name) if err != nil { - return response(nil, err) + return s.response(ctx, "RunArtifact", ticketID(params.ID), nil, err) } jobID := uuid.NewString() origin := &models.Origin{ArtifactOrigin: &models.ArtifactOrigin{TicketId: params.ID, Artifact: params.Name}} - return response(nil, s.bus.PublishJob(jobID, params.Automation, params.Name, &models.Context{Artifact: artifact}, origin)) + err = s.bus.PublishJob(jobID, params.Automation, params.Name, &models.Context{Artifact: artifact}, origin) + return s.response(ctx, "RunArtifact", ticketID(params.ID), nil, err) } func (s *Service) EnrichArtifact(ctx context.Context, params *tickets.EnrichArtifactParams) *api.Response { - return response(s.database.EnrichArtifact(ctx, params.ID, params.Name, params.Data)) + i, err := s.database.EnrichArtifact(ctx, params.ID, params.Name, params.Data) + return s.response(ctx, "EnrichArtifact", ticketID(params.ID), i, err) } func (s *Service) SetArtifact(ctx context.Context, params *tickets.SetArtifactParams) *api.Response { - return response(s.database.ArtifactUpdate(ctx, params.ID, params.Name, params.Artifact)) + i, err := s.database.ArtifactUpdate(ctx, params.ID, params.Name, params.Artifact) + return s.response(ctx, "SetArtifact", ticketID(params.ID), i, err) } func (s *Service) GetArtifact(ctx context.Context, params *tickets.GetArtifactParams) *api.Response { - return response(s.database.ArtifactGet(ctx, params.ID, params.Name)) + i, err := s.database.ArtifactGet(ctx, params.ID, params.Name) + return s.response(ctx, "GetArtifact", nil, i, err) } diff --git a/service/automation.go b/service/automation.go index 1461619..6fcd0bc 100644 --- a/service/automation.go +++ b/service/automation.go @@ -2,27 +2,40 @@ package service import ( "context" + "fmt" + "github.com/arangodb/go-driver" + + "github.com/SecurityBrewery/catalyst/database" "github.com/SecurityBrewery/catalyst/generated/restapi/api" "github.com/SecurityBrewery/catalyst/generated/restapi/operations/automations" ) +func automationID(id string) []driver.DocumentID { + return []driver.DocumentID{driver.DocumentID(fmt.Sprintf("%s/%s", database.AutomationCollectionName, id))} +} + func (s *Service) CreateAutomation(ctx context.Context, params *automations.CreateAutomationParams) *api.Response { - return response(s.database.AutomationCreate(ctx, params.Automation)) + i, err := s.database.AutomationCreate(ctx, params.Automation) + return s.response(ctx, "CreateAutomation", automationID(i.ID), i, err) } func (s *Service) GetAutomation(ctx context.Context, params *automations.GetAutomationParams) *api.Response { - return response(s.database.AutomationGet(ctx, params.ID)) + i, err := s.database.AutomationGet(ctx, params.ID) + return s.response(ctx, "GetAutomation", nil, i, err) } func (s *Service) UpdateAutomation(ctx context.Context, params *automations.UpdateAutomationParams) *api.Response { - return response(s.database.AutomationUpdate(ctx, params.ID, params.Automation)) + i, err := s.database.AutomationUpdate(ctx, params.ID, params.Automation) + return s.response(ctx, "UpdateAutomation", automationID(i.ID), i, err) } func (s *Service) DeleteAutomation(ctx context.Context, params *automations.DeleteAutomationParams) *api.Response { - return response(nil, s.database.AutomationDelete(ctx, params.ID)) + err := s.database.AutomationDelete(ctx, params.ID) + return s.response(ctx, "DeleteAutomation", automationID(params.ID), nil, err) } func (s *Service) ListAutomations(ctx context.Context) *api.Response { - return response(s.database.AutomationList(ctx)) + i, err := s.database.AutomationList(ctx) + return s.response(ctx, "ListAutomations", nil, i, err) } diff --git a/service/job.go b/service/job.go index cc5bf03..3c8e0be 100644 --- a/service/job.go +++ b/service/job.go @@ -2,28 +2,38 @@ package service import ( "context" + "fmt" + "github.com/arangodb/go-driver" "github.com/google/uuid" + "github.com/SecurityBrewery/catalyst/database" "github.com/SecurityBrewery/catalyst/generated/models" "github.com/SecurityBrewery/catalyst/generated/restapi/api" "github.com/SecurityBrewery/catalyst/generated/restapi/operations/jobs" ) -func (s *Service) RunJob(_ context.Context, params *jobs.RunJobParams) *api.Response { +func jobID(id string) []driver.DocumentID { + return []driver.DocumentID{driver.DocumentID(fmt.Sprintf("%s/%s", database.JobCollectionName, id))} +} + +func (s *Service) RunJob(ctx context.Context, params *jobs.RunJobParams) *api.Response { msgContext := &models.Context{} - jobID := uuid.NewString() - return response(nil, s.bus.PublishJob(jobID, params.Job.Automation, params.Job.Payload, msgContext, params.Job.Origin)) + newJobID := uuid.NewString() + return s.response(ctx, "RunJob", jobID(newJobID), nil, s.bus.PublishJob(newJobID, params.Job.Automation, params.Job.Payload, msgContext, params.Job.Origin)) } func (s *Service) GetJob(ctx context.Context, params *jobs.GetJobParams) *api.Response { - return response(s.database.JobGet(ctx, params.ID)) + i, err := s.database.JobGet(ctx, params.ID) + return s.response(ctx, "GetJob", nil, i, err) } func (s *Service) ListJobs(ctx context.Context) *api.Response { - return response(s.database.JobList(ctx)) + i, err := s.database.JobList(ctx) + return s.response(ctx, "ListJobs", nil, i, err) } func (s *Service) UpdateJob(ctx context.Context, params *jobs.UpdateJobParams) *api.Response { - return response(s.database.JobUpdate(ctx, params.ID, params.Job)) + i, err := s.database.JobUpdate(ctx, params.ID, params.Job) + return s.response(ctx, "UpdateJob", jobID(i.ID), i, err) } diff --git a/service/log.go b/service/log.go index 4c8892f..d8d34cd 100644 --- a/service/log.go +++ b/service/log.go @@ -10,5 +10,6 @@ import ( func (s *Service) GetLogs(ctx context.Context, params *logs.GetLogsParams) *api.Response { id, _ := url.QueryUnescape(params.Reference) - return response(s.database.LogList(ctx, id)) + i, err := s.database.LogList(ctx, id) + return s.response(ctx, "GetLogs", nil, i, err) } diff --git a/service/playbook.go b/service/playbook.go index c36430f..550f5d0 100644 --- a/service/playbook.go +++ b/service/playbook.go @@ -5,35 +5,46 @@ import ( "fmt" "strings" + "github.com/arangodb/go-driver" "github.com/xeipuuv/gojsonschema" + "github.com/SecurityBrewery/catalyst/database" "github.com/SecurityBrewery/catalyst/generated/models" "github.com/SecurityBrewery/catalyst/generated/restapi/api" "github.com/SecurityBrewery/catalyst/generated/restapi/operations/playbooks" ) +func playbookID(id string) []driver.DocumentID { + return []driver.DocumentID{driver.DocumentID(fmt.Sprintf("%s/%s", database.PlaybookCollectionName, id))} +} + func (s *Service) CreatePlaybook(ctx context.Context, params *playbooks.CreatePlaybookParams) *api.Response { - return response(s.database.PlaybookCreate(ctx, params.Playbook)) + i, err := s.database.PlaybookCreate(ctx, params.Playbook) + return s.response(ctx, "CreatePlaybook", playbookID(i.ID), i, err) } func (s *Service) GetPlaybook(ctx context.Context, params *playbooks.GetPlaybookParams) *api.Response { - return response(s.database.PlaybookGet(ctx, params.ID)) + i, err := s.database.PlaybookGet(ctx, params.ID) + return s.response(ctx, "GetPlaybook", nil, i, err) } func (s *Service) UpdatePlaybook(ctx context.Context, params *playbooks.UpdatePlaybookParams) *api.Response { if err := validate(params.Playbook, models.PlaybookTemplateFormSchema); err != nil { - return response(nil, err) + return s.response(ctx, "UpdatePlaybook", nil, nil, err) } - return response(s.database.PlaybookUpdate(ctx, params.ID, params.Playbook)) + i, err := s.database.PlaybookUpdate(ctx, params.ID, params.Playbook) + return s.response(ctx, "UpdatePlaybook", playbookID(i.ID), i, err) } func (s *Service) DeletePlaybook(ctx context.Context, params *playbooks.DeletePlaybookParams) *api.Response { - return response(nil, s.database.PlaybookDelete(ctx, params.ID)) + err := s.database.PlaybookDelete(ctx, params.ID) + return s.response(ctx, "DeletePlaybook", playbookID(params.ID), nil, err) } func (s *Service) ListPlaybooks(ctx context.Context) *api.Response { - return response(s.database.PlaybookList(ctx)) + i, err := s.database.PlaybookList(ctx) + return s.response(ctx, "ListPlaybooks", nil, i, err) } func validate(e interface{}, schema *gojsonschema.Schema) error { diff --git a/service/service.go b/service/service.go index 9e23fa7..9f45da2 100644 --- a/service/service.go +++ b/service/service.go @@ -1,6 +1,7 @@ package service import ( + "context" "errors" "log" "net/http" @@ -10,6 +11,7 @@ import ( "github.com/SecurityBrewery/catalyst/bus" "github.com/SecurityBrewery/catalyst/database" + "github.com/SecurityBrewery/catalyst/database/busdb" "github.com/SecurityBrewery/catalyst/generated/models" "github.com/SecurityBrewery/catalyst/generated/restapi/api" "github.com/SecurityBrewery/catalyst/storage" @@ -30,11 +32,22 @@ func (s *Service) Healthy() bool { return true } -func response(v interface{}, err error) *api.Response { +func (s *Service) response(ctx context.Context, function string, ids []driver.DocumentID, v interface{}, err error) *api.Response { if err != nil { log.Println(err) return &api.Response{Code: httpStatus(err), Body: gin.H{"error": err.Error()}} } + + if ids != nil { + userID := "unknown" + user, ok := busdb.UserFromContext(ctx) + if ok { + userID = user.ID + } + + go s.bus.PublishRequest(userID, function, ids) + } + if v == nil { return &api.Response{Code: http.StatusNoContent, Body: v} } diff --git a/service/statistics.go b/service/statistics.go index f0ea6f2..248e233 100644 --- a/service/statistics.go +++ b/service/statistics.go @@ -7,5 +7,6 @@ import ( ) func (s *Service) GetStatistics(ctx context.Context) *api.Response { - return response(s.database.Statistics(ctx)) + i, err := s.database.Statistics(ctx) + return s.response(ctx, "GetStatistics", nil, i, err) } diff --git a/service/task.go b/service/task.go index 5343f9e..52a2155 100644 --- a/service/task.go +++ b/service/task.go @@ -7,5 +7,6 @@ import ( ) func (s *Service) ListTasks(ctx context.Context) *api.Response { - return response(s.database.TaskList(ctx)) + i, err := s.database.TaskList(ctx) + return s.response(ctx, "ListTasks", nil, i, err) } diff --git a/service/template.go b/service/template.go index 3321fe2..7e2fff6 100644 --- a/service/template.go +++ b/service/template.go @@ -2,27 +2,40 @@ package service import ( "context" + "fmt" + "github.com/arangodb/go-driver" + + "github.com/SecurityBrewery/catalyst/database" "github.com/SecurityBrewery/catalyst/generated/restapi/api" "github.com/SecurityBrewery/catalyst/generated/restapi/operations/templates" ) +func templateID(s string) []driver.DocumentID { + return []driver.DocumentID{driver.DocumentID(fmt.Sprintf("%s/%s", database.TemplateCollectionName, s))} +} + func (s *Service) CreateTemplate(ctx context.Context, params *templates.CreateTemplateParams) *api.Response { - return response(s.database.TemplateCreate(ctx, params.Template)) + i, err := s.database.TemplateCreate(ctx, params.Template) + return s.response(ctx, "CreateTemplate", templateID(i.ID), i, err) } func (s *Service) GetTemplate(ctx context.Context, params *templates.GetTemplateParams) *api.Response { - return response(s.database.TemplateGet(ctx, params.ID)) + i, err := s.database.TemplateGet(ctx, params.ID) + return s.response(ctx, "GetTemplate", nil, i, err) } func (s *Service) UpdateTemplate(ctx context.Context, params *templates.UpdateTemplateParams) *api.Response { - return response(s.database.TemplateUpdate(ctx, params.ID, params.Template)) + i, err := s.database.TemplateUpdate(ctx, params.ID, params.Template) + return s.response(ctx, "UpdateTemplate", templateID(i.ID), i, err) } func (s *Service) DeleteTemplate(ctx context.Context, params *templates.DeleteTemplateParams) *api.Response { - return response(nil, s.database.TemplateDelete(ctx, params.ID)) + err := s.database.TemplateDelete(ctx, params.ID) + return s.response(ctx, "DeleteTemplate", templateID(params.ID), nil, err) } func (s *Service) ListTemplates(ctx context.Context) *api.Response { - return response(s.database.TemplateList(ctx)) + i, err := s.database.TemplateList(ctx) + return s.response(ctx, "ListTemplates", nil, i, err) } diff --git a/service/ticket.go b/service/ticket.go index bf4a71d..9a8623a 100644 --- a/service/ticket.go +++ b/service/ticket.go @@ -4,39 +4,56 @@ import ( "context" "fmt" + "github.com/arangodb/go-driver" + + "github.com/SecurityBrewery/catalyst/database" "github.com/SecurityBrewery/catalyst/generated/models" "github.com/SecurityBrewery/catalyst/generated/restapi/api" "github.com/SecurityBrewery/catalyst/generated/restapi/operations/tickets" ) +func ticketID(id int64) []driver.DocumentID { + return []driver.DocumentID{driver.DocumentID(fmt.Sprintf("%s/%d", database.TicketCollectionName, id))} +} + +func ticketIDs(ticketResponses []*models.TicketResponse) []driver.DocumentID { + var ids []driver.DocumentID + for _, ticketResponse := range ticketResponses { + ids = append(ids, ticketID(ticketResponse.ID)...) + } + return ids +} + func (s *Service) CreateTicket(ctx context.Context, params *tickets.CreateTicketParams) *api.Response { createdTickets, err := s.database.TicketBatchCreate(ctx, []*models.TicketForm{params.Ticket}) if len(createdTickets) > 0 { - return response(createdTickets[0], err) + return s.response(ctx, "CreateTicket", ticketIDs(createdTickets), createdTickets[0], err) } - return response(nil, err) + return s.response(ctx, "CreateTicket", ticketIDs(createdTickets), nil, err) } func (s *Service) CreateTicketBatch(ctx context.Context, params *tickets.CreateTicketBatchParams) *api.Response { - _, err := s.database.TicketBatchCreate(ctx, params.Ticket) - return response(nil, err) + ticketBatch, err := s.database.TicketBatchCreate(ctx, params.Ticket) + return s.response(ctx, "CreateTicketBatch", ticketIDs(ticketBatch), nil, err) } func (s *Service) GetTicket(ctx context.Context, params *tickets.GetTicketParams) *api.Response { - return response(s.database.TicketGet(ctx, params.ID)) + ticket, err := s.database.TicketGet(ctx, params.ID) + return s.response(ctx, "GetTicket", nil, ticket, err) } func (s *Service) UpdateTicket(ctx context.Context, params *tickets.UpdateTicketParams) *api.Response { - return response(s.database.TicketUpdate(ctx, params.ID, params.Ticket)) + ticket, err := s.database.TicketUpdate(ctx, params.ID, params.Ticket) + return s.response(ctx, "UpdateTicket", ticketID(ticket.ID), ticket, err) } func (s *Service) DeleteTicket(ctx context.Context, params *tickets.DeleteTicketParams) *api.Response { if err := s.database.TicketDelete(ctx, params.ID); err != nil { - return response(nil, err) + return s.response(ctx, "DeleteTicket", ticketID(params.ID), nil, err) } _ = s.storage.DeleteBucket(fmt.Sprint(params.ID)) - return response(nil, nil) + return s.response(ctx, "DeleteTicket", ticketID(params.ID), nil, nil) } func (s *Service) ListTickets(ctx context.Context, params *tickets.ListTicketsParams) *api.Response { @@ -48,5 +65,7 @@ func (s *Service) ListTickets(ctx context.Context, params *tickets.ListTicketsPa if params.Type != nil && *params.Type != "" { t = *params.Type } - return response(s.database.TicketList(ctx, t, q, params.Sort, params.Desc, *params.Offset, *params.Count)) + + ticketList, err := s.database.TicketList(ctx, t, q, params.Sort, params.Desc, *params.Offset, *params.Count) + return s.response(ctx, "ListTickets", nil, ticketList, err) } diff --git a/service/ticket_field.go b/service/ticket_field.go index d01adaf..aa6cd4b 100644 --- a/service/ticket_field.go +++ b/service/ticket_field.go @@ -8,67 +8,81 @@ import ( ) func (s *Service) AddArtifact(ctx context.Context, params *tickets.AddArtifactParams) *api.Response { - return response(s.database.AddArtifact(ctx, params.ID, params.Artifact)) + i, err := s.database.AddArtifact(ctx, params.ID, params.Artifact) + return s.response(ctx, "AddArtifact", ticketID(params.ID), i, err) } func (s *Service) RemoveArtifact(ctx context.Context, params *tickets.RemoveArtifactParams) *api.Response { - return response(s.database.RemoveArtifact(ctx, params.ID, params.Name)) + i, err := s.database.RemoveArtifact(ctx, params.ID, params.Name) + return s.response(ctx, "RemoveArtifact", ticketID(params.ID), i, err) } func (s *Service) SetSchema(ctx context.Context, params *tickets.SetSchemaParams) *api.Response { - return response(s.database.SetTemplate(ctx, params.ID, params.Schema)) + i, err := s.database.SetTemplate(ctx, params.ID, params.Schema) + return s.response(ctx, "SetSchema", ticketID(params.ID), i, err) } func (s *Service) AddComment(ctx context.Context, params *tickets.AddCommentParams) *api.Response { - return response(s.database.AddComment(ctx, params.ID, params.Comment)) + i, err := s.database.AddComment(ctx, params.ID, params.Comment) + return s.response(ctx, "AddComment", ticketID(params.ID), i, err) } func (s *Service) RemoveComment(ctx context.Context, params *tickets.RemoveCommentParams) *api.Response { - return response(s.database.RemoveComment(ctx, params.ID, params.CommentID)) + i, err := s.database.RemoveComment(ctx, params.ID, params.CommentID) + return s.response(ctx, "RemoveComment", ticketID(params.ID), i, err) } func (s *Service) LinkTicket(ctx context.Context, params *tickets.LinkTicketParams) *api.Response { err := s.database.RelatedCreate(ctx, params.ID, params.LinkedID) if err != nil { - return response(nil, err) + return s.response(ctx, "LinkTicket", ticketID(params.ID), nil, err) } - return s.GetTicket(ctx, &tickets.GetTicketParams{ID: params.ID}) + i, err := s.database.TicketGet(ctx, params.ID) + return s.response(ctx, "LinkTicket", ticketID(params.ID), i, err) } func (s *Service) UnlinkTicket(ctx context.Context, params *tickets.UnlinkTicketParams) *api.Response { err := s.database.RelatedRemove(ctx, params.ID, params.LinkedID) if err != nil { - return response(nil, err) + return s.response(ctx, "UnlinkTicket", ticketID(params.ID), nil, err) } - return s.GetTicket(ctx, &tickets.GetTicketParams{ID: params.ID}) + i, err := s.database.TicketGet(ctx, params.ID) + return s.response(ctx, "UnlinkTicket", ticketID(params.ID), i, err) } func (s Service) SetReferences(ctx context.Context, params *tickets.SetReferencesParams) *api.Response { - return response(s.database.SetReferences(ctx, params.ID, params.References)) + i, err := s.database.SetReferences(ctx, params.ID, params.References) + return s.response(ctx, "SetReferences", ticketID(params.ID), i, err) } func (s Service) LinkFiles(ctx context.Context, params *tickets.LinkFilesParams) *api.Response { - return response(s.database.LinkFiles(ctx, params.ID, params.Files)) + i, err := s.database.LinkFiles(ctx, params.ID, params.Files) + return s.response(ctx, "LinkFiles", ticketID(params.ID), i, err) } func (s Service) AddTicketPlaybook(ctx context.Context, params *tickets.AddTicketPlaybookParams) *api.Response { - return response(s.database.AddTicketPlaybook(ctx, params.ID, params.Playbook)) + i, err := s.database.AddTicketPlaybook(ctx, params.ID, params.Playbook) + return s.response(ctx, "AddTicketPlaybook", ticketID(params.ID), i, err) } func (s Service) RemoveTicketPlaybook(ctx context.Context, params *tickets.RemoveTicketPlaybookParams) *api.Response { - return response(s.database.RemoveTicketPlaybook(ctx, params.ID, params.PlaybookID)) + i, err := s.database.RemoveTicketPlaybook(ctx, params.ID, params.PlaybookID) + return s.response(ctx, "RemoveTicketPlaybook", ticketID(params.ID), i, err) } func (s Service) CompleteTask(ctx context.Context, params *tickets.CompleteTaskParams) *api.Response { - return response(s.database.TaskComplete(ctx, params.ID, params.PlaybookID, params.TaskID, params.Data)) + i, err := s.database.TaskComplete(ctx, params.ID, params.PlaybookID, params.TaskID, params.Data) + return s.response(ctx, "CompleteTask", ticketID(params.ID), i, err) } func (s Service) SetTask(ctx context.Context, params *tickets.SetTaskParams) *api.Response { - return response(s.database.TaskUpdate(ctx, params.ID, params.PlaybookID, params.TaskID, params.Task)) + i, err := s.database.TaskUpdate(ctx, params.ID, params.PlaybookID, params.TaskID, params.Task) + return s.response(ctx, "SetTask", ticketID(params.ID), i, err) } func (s *Service) RunTask(ctx context.Context, params *tickets.RunTaskParams) *api.Response { - return response(nil, s.database.TaskRun(ctx, params.ID, params.PlaybookID, params.TaskID)) + err := s.database.TaskRun(ctx, params.ID, params.PlaybookID, params.TaskID) + return s.response(ctx, "RunTask", ticketID(params.ID), nil, err) } diff --git a/service/tickettype.go b/service/tickettype.go index e42346a..84a34f5 100644 --- a/service/tickettype.go +++ b/service/tickettype.go @@ -2,27 +2,40 @@ package service import ( "context" + "fmt" + "github.com/arangodb/go-driver" + + "github.com/SecurityBrewery/catalyst/database" "github.com/SecurityBrewery/catalyst/generated/restapi/api" "github.com/SecurityBrewery/catalyst/generated/restapi/operations/tickettypes" ) +func ticketTypeID(id string) []driver.DocumentID { + return []driver.DocumentID{driver.DocumentID(fmt.Sprintf("%s/%s", database.TicketTypeCollectionName, id))} +} + func (s *Service) CreateTicketType(ctx context.Context, params *tickettypes.CreateTicketTypeParams) *api.Response { - return response(s.database.TicketTypeCreate(ctx, params.Tickettype)) + ticketType, err := s.database.TicketTypeCreate(ctx, params.Tickettype) + return s.response(ctx, "CreateTicketType", ticketTypeID(ticketType.ID), ticketType, err) } func (s *Service) GetTicketType(ctx context.Context, params *tickettypes.GetTicketTypeParams) *api.Response { - return response(s.database.TicketTypeGet(ctx, params.ID)) + ticketType, err := s.database.TicketTypeGet(ctx, params.ID) + return s.response(ctx, "GetTicketType", nil, ticketType, err) } func (s *Service) UpdateTicketType(ctx context.Context, params *tickettypes.UpdateTicketTypeParams) *api.Response { - return response(s.database.TicketTypeUpdate(ctx, params.ID, params.Tickettype)) + ticketType, err := s.database.TicketTypeUpdate(ctx, params.ID, params.Tickettype) + return s.response(ctx, "UpdateTicketType", ticketTypeID(ticketType.ID), ticketType, err) } func (s *Service) DeleteTicketType(ctx context.Context, params *tickettypes.DeleteTicketTypeParams) *api.Response { - return response(nil, s.database.TicketTypeDelete(ctx, params.ID)) + err := s.database.TicketTypeDelete(ctx, params.ID) + return s.response(ctx, "DeleteTicketType", ticketTypeID(params.ID), nil, err) } func (s *Service) ListTicketTypes(ctx context.Context) *api.Response { - return response(s.database.TicketTypeList(ctx)) + ticketTypes, err := s.database.TicketTypeList(ctx) + return s.response(ctx, "ListTicketTypes", nil, ticketTypes, err) } diff --git a/service/uisettings.go b/service/uisettings.go index 56c48d8..b7b5b92 100644 --- a/service/uisettings.go +++ b/service/uisettings.go @@ -14,24 +14,24 @@ import ( func (s *Service) GetSettings(ctx context.Context) *api.Response { user, ok := busdb.UserFromContext(ctx) if !ok { - return response(nil, errors.New("no user in context")) + return s.response(ctx, "GetSettings", nil, nil, errors.New("no user in context")) } setting, err := s.database.UserDataGet(ctx, user.ID) if err != nil { - return response(nil, err) + return s.response(ctx, "GetSettings", nil, nil, err) } settings := mergeSettings(s.settings, setting) ticketTypeList, err := s.database.TicketTypeList(ctx) if err != nil { - return response(nil, err) + return s.response(ctx, "GetSettings", nil, nil, err) } settings.TicketTypes = ticketTypeList - return response(settings, nil) + return s.response(ctx, "GetSettings", nil, settings, nil) } func mergeSettings(globalSettings *models.Settings, user *models.UserDataResponse) *models.Settings { diff --git a/service/user.go b/service/user.go index 56c7fea..c99fda7 100644 --- a/service/user.go +++ b/service/user.go @@ -3,36 +3,49 @@ package service import ( "context" "errors" + "fmt" + "github.com/arangodb/go-driver" + + "github.com/SecurityBrewery/catalyst/database" "github.com/SecurityBrewery/catalyst/database/busdb" "github.com/SecurityBrewery/catalyst/generated/restapi/api" "github.com/SecurityBrewery/catalyst/generated/restapi/operations/users" ) +func userID(id string) []driver.DocumentID { + return []driver.DocumentID{driver.DocumentID(fmt.Sprintf("%s/%s", database.UserCollectionName, id))} +} + func (s *Service) GetUser(ctx context.Context, params *users.GetUserParams) *api.Response { - return response(s.database.UserGet(ctx, params.ID)) + i, err := s.database.UserGet(ctx, params.ID) + return s.response(ctx, "GetUser", nil, i, err) } func (s *Service) ListUsers(ctx context.Context) *api.Response { - return response(s.database.UserList(ctx)) + i, err := s.database.UserList(ctx) + return s.response(ctx, "ListUsers", nil, i, err) } func (s *Service) CreateUser(ctx context.Context, params *users.CreateUserParams) *api.Response { - return response(s.database.UserCreate(ctx, params.User)) + i, err := s.database.UserCreate(ctx, params.User) + return s.response(ctx, "CreateUser", userID(i.ID), i, err) } func (s *Service) DeleteUser(ctx context.Context, params *users.DeleteUserParams) *api.Response { - return response(nil, s.database.UserDelete(ctx, params.ID)) + err := s.database.UserDelete(ctx, params.ID) + return s.response(ctx, "DeleteUser", userID(params.ID), nil, err) } func (s *Service) CurrentUser(ctx context.Context) *api.Response { user, ok := busdb.UserFromContext(ctx) if !ok { - return response(nil, errors.New("no user in context")) + return s.response(ctx, "CurrentUser", nil, nil, errors.New("no user in context")) } - return response(user, nil) + return s.response(ctx, "CurrentUser", nil, user, nil) } func (s *Service) UpdateUser(ctx context.Context, params *users.UpdateUserParams) *api.Response { - return response(s.database.UserUpdate(ctx, params.ID, params.User)) + i, err := s.database.UserUpdate(ctx, params.ID, params.User) + return s.response(ctx, "UpdateUser", userID(i.ID), i, err) } diff --git a/service/userdata.go b/service/userdata.go index 7e8e10e..5fae0f2 100644 --- a/service/userdata.go +++ b/service/userdata.go @@ -3,37 +3,50 @@ package service import ( "context" "errors" + "fmt" + "github.com/arangodb/go-driver" + + "github.com/SecurityBrewery/catalyst/database" "github.com/SecurityBrewery/catalyst/database/busdb" "github.com/SecurityBrewery/catalyst/generated/restapi/api" "github.com/SecurityBrewery/catalyst/generated/restapi/operations/userdata" ) +func userdataID(id string) []driver.DocumentID { + return []driver.DocumentID{driver.DocumentID(fmt.Sprintf("%s/%s", database.UserDataCollectionName, id))} +} + func (s *Service) GetUserData(ctx context.Context, params *userdata.GetUserDataParams) *api.Response { - return response(s.database.UserDataGet(ctx, params.ID)) + userData, err := s.database.UserDataGet(ctx, params.ID) + return s.response(ctx, "GetUserData", nil, userData, err) } func (s *Service) ListUserData(ctx context.Context) *api.Response { - return response(s.database.UserDataList(ctx)) + userData, err := s.database.UserDataList(ctx) + return s.response(ctx, "ListUserData", nil, userData, err) } func (s *Service) UpdateUserData(ctx context.Context, params *userdata.UpdateUserDataParams) *api.Response { - return response(s.database.UserDataUpdate(ctx, params.ID, params.Userdata)) + userData, err := s.database.UserDataUpdate(ctx, params.ID, params.Userdata) + return s.response(ctx, "UpdateUserData", userdataID(userData.ID), userData, err) } func (s *Service) CurrentUserData(ctx context.Context) *api.Response { user, ok := busdb.UserFromContext(ctx) if !ok { - return response(nil, errors.New("no user in context")) + return s.response(ctx, "CurrentUserData", userdataID(user.ID), nil, errors.New("no user in context")) } - return s.GetUserData(ctx, &userdata.GetUserDataParams{ID: user.ID}) + userData, err := s.database.UserDataGet(ctx, user.ID) + return s.response(ctx, "GetUserData", nil, userData, err) } func (s *Service) UpdateCurrentUserData(ctx context.Context, params *userdata.UpdateCurrentUserDataParams) *api.Response { user, ok := busdb.UserFromContext(ctx) if !ok { - return response(nil, errors.New("no user in context")) + return s.response(ctx, "UpdateCurrentUserData", userdataID(user.ID), nil, errors.New("no user in context")) } - return response(s.database.UserDataUpdate(ctx, user.ID, params.Userdata)) + userData, err := s.database.UserDataUpdate(ctx, user.ID, params.Userdata) + return s.response(ctx, "UpdateCurrentUserData", userdataID(user.ID), userData, err) } diff --git a/test/data.go b/test/data.go index 1d39c17..826976d 100644 --- a/test/data.go +++ b/test/data.go @@ -77,7 +77,7 @@ func SetupTestData(ctx context.Context, db *database.Database) error { return err } - if _, err := db.LogCreate(ctx, "tickets/294511", "Fail run account resist lend solve incident centre priority temperature. Cause change distribution examine location technique shape partner milk customer. Rail tea plate soil report cook railway interpretation breath action. Exercise dream accept park conclusion addition shoot assistance may answer. Gold writer link stop combine hear power name commitment operation. Determine lifespan support grow degree henry exclude detail set religion. Direct library policy convention chain retain discover ride walk student. Gather proposal select march aspect play noise avoid encourage employ. Assessment preserve transport combine wish influence income guess run stand. Charge limit crime ignore statement foundation study issue stop claim."); err != nil { + if _, err := db.LogCreate(ctx, "manual", "tickets/294511", "Fail run account resist lend solve incident centre priority temperature. Cause change distribution examine location technique shape partner milk customer. Rail tea plate soil report cook railway interpretation breath action. Exercise dream accept park conclusion addition shoot assistance may answer. Gold writer link stop combine hear power name commitment operation. Determine lifespan support grow degree henry exclude detail set religion. Direct library policy convention chain retain discover ride walk student. Gather proposal select march aspect play noise avoid encourage employ. Assessment preserve transport combine wish influence income guess run stand. Charge limit crime ignore statement foundation study issue stop claim."); err != nil { return err } diff --git a/ui/src/client/api.ts b/ui/src/client/api.ts index 4f7f2fd..ee81ae9 100644 --- a/ui/src/client/api.ts +++ b/ui/src/client/api.ts @@ -612,6 +612,12 @@ export interface LogEntry { * @memberof LogEntry */ reference: string; + /** + * + * @type {string} + * @memberof LogEntry + */ + type: string; } /** * @@ -2026,6 +2032,12 @@ export interface TicketWithTickets { * @memberof TicketWithTickets */ id: number; + /** + * + * @type {Array} + * @memberof TicketWithTickets + */ + logs?: Array; /** * * @type {string} diff --git a/ui/src/views/Ticket.vue b/ui/src/views/Ticket.vue index 0560df3..e870e89 100644 --- a/ui/src/views/Ticket.vue +++ b/ui/src/views/Ticket.vue @@ -200,8 +200,15 @@ -
- +
+
+ + {{ comment.message }} · + {{ comment.creator }} · + {{ relDate(comment.created) }} + +
+ {{ comment.creator }} @@ -791,7 +798,7 @@ import { Task, Type, TypeColorEnum, - TaskResponse, PlaybookResponse, UserResponse, TaskTypeEnum + TaskResponse, PlaybookResponse, UserResponse, TaskTypeEnum, TicketWithTickets, } from "../client"; import {API} from "@/services/api"; @@ -1520,6 +1527,9 @@ export default Vue.extend({ }); } return relDate; + }, + logs: function(ticket: TicketWithTickets) { + return this.lodash.reverse(this.lodash.sortBy(this.lodash.union(ticket.comments, ticket.logs), ['created'])) } }, mounted() { diff --git a/websocket.go b/websocket.go index afd421a..387ca56 100644 --- a/websocket.go +++ b/websocket.go @@ -6,7 +6,6 @@ import ( "net/http" "sync" - "github.com/arangodb/go-driver" "github.com/gin-gonic/gin" "github.com/gobwas/ws" "github.com/gobwas/ws/wsutil" @@ -48,10 +47,10 @@ func handleWebSocket(catalystBus *bus.Bus) func(ctx *gin.Context) { broker := websocketBroker{clients: map[string]chan []byte{}} // send all messages from bus to websocket - err := catalystBus.SubscribeUpdate(func(ids []driver.DocumentID) { + err := catalystBus.SubscribeDatabaseUpdate(func(msg *bus.DatabaseUpdateMsg) { b, err := json.Marshal(map[string]interface{}{ "action": "update", - "ids": ids, + "ids": msg.IDs, }) if err != nil { return