Compare commits

..

5 Commits

Author SHA1 Message Date
Jonas Plum
03a4806d45 Fix timeformat (#44) 2022-03-19 14:26:36 +01:00
Jonas Plum
e6baead486 Fix routing (#43) 2022-03-19 13:41:34 +01:00
Jonas Plum
3618f9784d Fix home screen (#42) 2022-03-16 00:27:46 +01:00
Jonas Plum
02c7da91da Add Dashboards (#41) 2022-03-14 00:23:29 +01:00
Jonas Plum
18a4dc54e7 Add global settings (#40) 2022-03-13 13:45:10 +01:00
47 changed files with 4160 additions and 542 deletions

View File

@@ -1,8 +1,6 @@
package cmd
import (
"fmt"
"github.com/alecthomas/kong"
kongyaml "github.com/alecthomas/kong-yaml"
"github.com/coreos/go-oidc/v3/oidc"
@@ -11,8 +9,6 @@ import (
"github.com/SecurityBrewery/catalyst"
"github.com/SecurityBrewery/catalyst/bus"
"github.com/SecurityBrewery/catalyst/database"
"github.com/SecurityBrewery/catalyst/generated/model"
"github.com/SecurityBrewery/catalyst/generated/pointer"
"github.com/SecurityBrewery/catalyst/role"
"github.com/SecurityBrewery/catalyst/storage"
)
@@ -47,9 +43,7 @@ type CLI struct {
EmitterIOHost string `env:"EMITTER_IO_HOST" default:"tcp://emitter:8080"`
EmitterIORKey string `env:"EMITTER_IO_KEY" required:""`
Timeformat string `env:"TIMEFORMAT" default:"yyyy-MM-dd HH:mm:ss" help:""`
ArtifactStates []map[string]string `env:"ARTIFACT_STATES"`
InitialAPIKey string `env:"INITIAL_API_KEY"`
InitialAPIKey string `env:"INITIAL_API_KEY"`
}
func ParseCatalystConfig() (*catalyst.Config, error) {
@@ -68,19 +62,6 @@ func MapConfig(cli CLI) (*catalyst.Config, error) {
roles = append(roles, role.Explodes(cli.AuthDefaultRoles)...)
roles = role.Explodes(role.Strings(roles))
artifactStates, err := toTypes(cli.ArtifactStates)
if err != nil {
return nil, err
}
if len(artifactStates) == 0 {
artifactStates = []*model.Type{
{Icon: "mdi-help-circle-outline", ID: "unknown", Name: "Unknown", Color: pointer.String(model.TypeColorInfo)},
{Icon: "mdi-skull", ID: "malicious", Name: "Malicious", Color: pointer.String(model.TypeColorError)},
{Icon: "mdi-check", ID: "clean", Name: "Clean", Color: pointer.String(model.TypeColorSuccess)},
}
}
scopes := unique(append([]string{oidc.ScopeOpenID, "profile", "email"}, cli.OIDCScopes...))
config := &catalyst.Config{
IndexPath: cli.IndexPath,
@@ -99,49 +80,12 @@ func MapConfig(cli CLI) (*catalyst.Config, error) {
AuthDefaultRoles: roles,
AuthAdminUsers: cli.AuthAdminUsers,
},
Bus: &bus.Config{Host: cli.EmitterIOHost, Key: cli.EmitterIORKey, APIUrl: cli.CatalystAddress + "/api"},
UISettings: &model.Settings{
ArtifactStates: artifactStates,
Timeformat: cli.Timeformat,
Version: catalyst.GetVersion(),
Tier: model.SettingsTierCommunity,
},
Bus: &bus.Config{Host: cli.EmitterIOHost, Key: cli.EmitterIORKey, APIUrl: cli.CatalystAddress + "/api"},
InitialAPIKey: cli.InitialAPIKey,
}
return config, nil
}
func toTypes(params []map[string]string) ([]*model.Type, error) {
var types []*model.Type
for _, param := range params {
t := &model.Type{}
icon, iconOK := param["icon"]
if iconOK {
t.Icon = icon
}
id, idOK := param["id"]
if idOK {
t.ID = id
}
name, nameOK := param["name"]
if nameOK {
t.Name = name
}
color, ok := param["color"]
if ok {
t.Color = pointer.String(color)
}
if iconOK && idOK && nameOK {
types = append(types, t)
} else {
return nil, fmt.Errorf("incomplete type: icon, id and name need to be provided (%s)", params)
}
}
return types, nil
}
func unique(l []string) []string {
keys := make(map[string]bool)
var list []string

117
database/dashboard.go Normal file
View File

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

View File

@@ -25,6 +25,8 @@ const (
UserCollectionName = "users"
TicketTypeCollectionName = "tickettypes"
JobCollectionName = "jobs"
SettingsCollectionName = "settings"
DashboardCollectionName = "dashboards"
TicketArtifactsGraphName = "Graph"
RelatedTicketsCollectionName = "related"
@@ -44,6 +46,8 @@ type Database struct {
userCollection *busdb.Collection
tickettypeCollection *busdb.Collection
jobCollection *busdb.Collection
settingsCollection *busdb.Collection
dashboardCollection *busdb.Collection
relatedCollection *busdb.Collection
// containsCollection *busdb.Collection
@@ -122,6 +126,14 @@ func New(ctx context.Context, index *index.Index, bus *bus.Bus, hooks *hooks.Hoo
if err != nil {
return nil, err
}
settingsCollection, err := arangoDB.Collection(ctx, SettingsCollectionName)
if err != nil {
return nil, err
}
dashboardCollection, err := arangoDB.Collection(ctx, DashboardCollectionName)
if err != nil {
return nil, err
}
hookedDB, err := busdb.NewDatabase(ctx, arangoDB, bus)
if err != nil {
@@ -142,6 +154,8 @@ func New(ctx context.Context, index *index.Index, bus *bus.Bus, hooks *hooks.Hoo
userCollection: busdb.NewCollection(userCollection, hookedDB),
tickettypeCollection: busdb.NewCollection(tickettypeCollection, hookedDB),
jobCollection: busdb.NewCollection(jobCollection, hookedDB),
settingsCollection: busdb.NewCollection(settingsCollection, hookedDB),
dashboardCollection: busdb.NewCollection(dashboardCollection, hookedDB),
}
return db, nil
@@ -189,5 +203,7 @@ func (db *Database) Truncate(ctx context.Context) {
db.tickettypeCollection.Truncate(ctx)
db.jobCollection.Truncate(ctx)
db.relatedCollection.Truncate(ctx)
db.settingsCollection.Truncate(ctx)
db.dashboardCollection.Truncate(ctx)
// db.containsCollection.Truncate(ctx)
}

View File

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

View File

@@ -2,84 +2,34 @@ package database
import (
"context"
"errors"
"github.com/arangodb/go-driver"
"github.com/SecurityBrewery/catalyst/database/busdb"
"github.com/SecurityBrewery/catalyst/generated/model"
)
func toUserDataResponse(key string, doc *model.UserData) *model.UserDataResponse {
return &model.UserDataResponse{
Email: doc.Email,
ID: key,
Image: doc.Image,
Name: doc.Name,
Timeformat: doc.Timeformat,
func (db *Database) Settings(ctx context.Context) (*model.Settings, error) {
settings := &model.Settings{}
if _, err := db.settingsCollection.ReadDocument(ctx, "global", settings); err != nil {
return nil, err
}
return settings, nil
}
func (db *Database) UserDataCreate(ctx context.Context, id string, userdata *model.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 context.Context, id string, newUserData *model.UserData) (*model.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) (*model.UserDataResponse, error) {
var doc model.UserData
meta, err := db.userdataCollection.ReadDocument(ctx, id, &doc)
func (db *Database) SaveSettings(ctx context.Context, settings *model.Settings) (*model.Settings, error) {
exists, err := db.settingsCollection.DocumentExists(ctx, "global")
if err != nil {
return nil, err
}
return toUserDataResponse(meta.Key, &doc), err
}
func (db *Database) UserDataList(ctx context.Context) ([]*model.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 []*model.UserDataResponse
for {
var doc model.UserData
meta, err := cursor.ReadDocument(ctx, &doc)
if driver.IsNoMoreDocuments(err) {
break
} else if err != nil {
if exists {
if _, err := db.settingsCollection.ReplaceDocument(ctx, "global", settings); err != nil {
return nil, err
}
} else {
if _, err := db.settingsCollection.CreateDocument(ctx, ctx, "global", settings); 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 *model.UserData) (*model.UserDataResponse, error) {
var doc model.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
return settings, nil
}

View File

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

View File

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

85
database/userdata.go Normal file
View File

@@ -0,0 +1,85 @@
package database
import (
"context"
"errors"
"github.com/arangodb/go-driver"
"github.com/SecurityBrewery/catalyst/database/busdb"
"github.com/SecurityBrewery/catalyst/generated/model"
)
func toUserDataResponse(key string, doc *model.UserData) *model.UserDataResponse {
return &model.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 *model.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 context.Context, id string, newUserData *model.UserData) (*model.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) (*model.UserDataResponse, error) {
var doc model.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) ([]*model.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 []*model.UserDataResponse
for {
var doc model.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 *model.UserData) (*model.UserDataResponse, error) {
var doc model.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
}

View File

@@ -10,6 +10,7 @@ definitions:
properties:
name: { type: string, example: "2.2.2.2" }
type: { type: string }
kind: { type: string }
status: { type: string, example: "Unknown" }
enrichments: { type: object, additionalProperties: { $ref: "#/definitions/Enrichment" } }

167
definition/dashboards.yaml Normal file
View File

@@ -0,0 +1,167 @@
swagger: "2.0"
info: { version: "", title: "" }
paths:
/statistics:
get:
tags: [ "statistics" ]
summary: "Get statistics"
operationId: "getStatistics"
responses:
"200":
description: "successful operation"
schema: { $ref: '#/definitions/Statistics' }
examples:
test:
unassigned: 0
open_tickets_per_user: { }
tickets_per_week: { "2021-39": 3 }
tickets_per_type: { "alert": 2, "incident": 1 }
security: [ { roles: [ "ticket:read" ] } ]
/dashboards:
get:
tags: [ "dashboards" ]
summary: "List dashboards"
operationId: "listDashboards"
responses:
"200":
description: "successful operation"
schema: { type: array, items: { $ref: "#/definitions/DashboardResponse" } }
examples:
test:
- id: simple
name: Simple
widgets:
- name: "open_tickets_per_user"
aggregation: "owner"
filter: 'status == "open"'
type: "bar"
width: 4
- name: "tickets_per_week"
aggregation: 'CONCAT(DATE_YEAR(created), "-", DATE_ISOWEEK(created) < 10 ? "0" : "", DATE_ISOWEEK(created))'
type: "line"
width: 8
security: [ { roles: [ "dashboard:read" ] } ]
post:
tags: [ "dashboards" ]
summary: "Create a new dashboard"
operationId: "createDashboard"
parameters:
- { name: "template", in: "body", description: "New template", required: true, schema: { $ref: "#/definitions/Dashboard" }, x-example: { name: "My Dashboard", widgets: [ ] } }
responses:
"200":
description: "successful operation"
schema: { $ref: "#/definitions/DashboardResponse" }
examples:
test:
id: "my-dashboard"
name: "My Dashboard"
widgets: []
security: [ { roles: [ "dashboard:write" ] } ]
/dashboards/{id}:
get:
tags: [ "dashboards" ]
summary: "Get a single dashboard"
operationId: "getDashboard"
parameters:
- { name: "id", in: "path", description: "Dashboard ID", required: true, type: string, x-example: "simple" }
responses:
"200":
description: "successful operation"
schema: { $ref: "#/definitions/DashboardResponse" }
examples:
test:
id: simple
name: Simple
widgets:
- name: "open_tickets_per_user"
aggregation: "owner"
filter: 'status == "open"'
type: "bar"
width: 4
- name: "tickets_per_week"
aggregation: 'CONCAT(DATE_YEAR(created), "-", DATE_ISOWEEK(created) < 10 ? "0" : "", DATE_ISOWEEK(created))'
type: "line"
width: 8
security: [ { roles: [ "dashboard:read" ] } ]
put:
tags: [ "dashboards" ]
summary: "Update an existing dashboard"
operationId: "updateDashboard"
parameters:
- { name: "id", in: "path", description: "Dashboard ID", required: true, type: string, x-example: "simple" }
- { name: "dashboard", in: "body", description: "Dashboard object that needs to be added", required: true, schema: { $ref: "#/definitions/Dashboard" }, x-example: { name: "Simple", widgets: [] } }
responses:
"200":
description: "successful operation"
schema: { $ref: "#/definitions/DashboardResponse" }
examples:
test:
id: simple
name: Simple
widgets: []
security: [ { roles: [ "dashboard:write" ] } ]
delete:
tags: [ "dashboards" ]
summary: "Delete a dashboard"
operationId: "deleteDashboard"
parameters:
- { name: "id", in: "path", description: "Dashboard ID", required: true, type: string, x-example: "simple" }
responses:
"204": { description: "successful operation" }
security: [ { roles: [ "dashboard:write" ] } ]
/dashboard/data:
get:
tags: [ "dashboards" ]
summary: "Get widget data"
operationId: "dashboardData"
parameters:
- { name: "aggregation", in: "query", description: "Aggregation", required: true, type: string, x-example: "type" }
- { name: "filter", in: "query", description: "Filter", type: string, x-example: 'status == "closed"' }
responses:
"200":
description: "successful operation"
schema: { type: object }
examples:
test:
alert: 2
incident: 1
security: [ { roles: [ "dashboard:read" ] } ]
definitions:
Statistics:
type: object
required: [ unassigned, open_tickets_per_user, tickets_per_week, tickets_per_type ]
properties:
unassigned: { type: integer }
open_tickets_per_user: { type: object, additionalProperties: { type: integer } }
tickets_per_week: { type: object, additionalProperties: { type: integer } }
tickets_per_type: { type: object, additionalProperties: { type: integer } }
Dashboard:
type: object
required: [ name, widgets ]
properties:
name: { type: string }
widgets: { type: array, items: { $ref: "#/definitions/Widget" } }
DashboardResponse:
type: object
required: [ id, name, widgets ]
properties:
id: { type: string }
name: { type: string }
widgets: { type: array, items: { $ref: "#/definitions/Widget" } }
Widget:
type: object
required: [ name, type, aggregation, width ]
properties:
name: { type: string }
type: { type: string, enum: [ "bar", "line", "pie" ] }
filter: { type: string }
aggregation: { type: string }
width: { type: integer, minimum: 1, maximum: 12 }

View File

@@ -10,42 +10,92 @@ paths:
responses:
"200":
description: "successful operation"
schema: { $ref: "#/definitions/Settings" }
schema: { $ref: "#/definitions/SettingsResponse" }
examples:
test:
version: "0.0.0-test"
tier: community
timeformat: "YYYY-MM-DDThh:mm:ss"
timeformat: "yyyy-MM-dd hh:mm:ss"
ticketTypes:
- { icon: "mdi-alert", id: "alert", name: "Alerts", default_template: "default", default_playbooks: [ ] }
- { icon: "mdi-radioactive", id: "incident", name: "Incidents", default_template: "default", default_playbooks: [ ] }
- { icon: "mdi-fingerprint", id: "investigation", name: "Forensic Investigations", default_template: "default", default_playbooks: [ ] }
- { icon: "mdi-target", id: "hunt", name: "Threat Hunting", default_template: "default", default_playbooks: [ ] }
artifactKinds:
- { icon: "mdi-server", id: "asset", name: "Asset" }
- { icon: "mdi-bullseye", id: "ioc", name: "IOC" }
artifactStates:
- { icon: "mdi-help-circle-outline", id: "unknown", name: "Unknown", color: "info" }
- { icon: "mdi-skull", id: "malicious", name: "Malicious", color: "error" }
- { icon: "mdi-check", id: "clean", name: "Clean", color: "success" }
roles: [
"admin:backup:read", "admin:backup:restore", "admin:group:write", "admin:job:read", "admin:job:write",
"admin:log:read", "admin:ticket:delete", "admin:user:write", "admin:userdata:read",
"admin:userdata:write", "analyst:automation:read",
"analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read",
"analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read",
"analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write",
"analyst:tickettype:read", "analyst:user:read", "engineer:automation:write",
"engineer:playbook:write", "engineer:rule:write", "engineer:template:write",
"engineer:tickettype:write" ]
"admin:backup:read", "admin:backup:restore", "admin:dashboard:write", "admin:group:write", "admin:job:read", "admin:job:write",
"admin:log:read", "admin:settings:write", "admin:ticket:delete", "admin:user:write", "admin:userdata:read",
"admin:userdata:write", "analyst:automation:read",
"analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read",
"analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read",
"analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write",
"analyst:tickettype:read", "analyst:user:read", "engineer:automation:write",
"engineer:playbook:write", "engineer:rule:write", "engineer:template:write",
"engineer:tickettype:write" ]
security: [ { roles: [ "settings:read" ] } ]
post:
tags: [ "settings" ]
summary: "Save settings"
operationId: "saveSettings"
parameters:
- { name: "settings", in: "body", description: "Save settings", required: true, schema: { $ref: "#/definitions/Settings" }, x-example: { timeformat: "yyyy-MM-dd hh:mm:ss", artifactKinds: [ { icon: "mdi-server", id: "asset", name: "Asset" }, { icon: "mdi-bullseye", id: "ioc", name: "IOC" } ], artifactStates: [ { icon: "mdi-help-circle-outline", id: "unknown", name: "Unknown", color: "info" },{ icon: "mdi-skull", id: "malicious", name: "Malicious", color: "error" },{ icon: "mdi-check", id: "clean", name: "Clean", color: "success" } ] } }
responses:
"200":
description: "successful operation"
schema: { $ref: "#/definitions/SettingsResponse" }
examples:
test:
version: "0.0.0-test"
tier: community
timeformat: "yyyy-MM-dd hh:mm:ss"
ticketTypes:
- { icon: "mdi-alert", id: "alert", name: "Alerts", default_template: "default", default_playbooks: [ ] }
- { icon: "mdi-radioactive", id: "incident", name: "Incidents", default_template: "default", default_playbooks: [ ] }
- { icon: "mdi-fingerprint", id: "investigation", name: "Forensic Investigations", default_template: "default", default_playbooks: [ ] }
- { icon: "mdi-target", id: "hunt", name: "Threat Hunting", default_template: "default", default_playbooks: [ ] }
artifactKinds:
- { icon: "mdi-server", id: "asset", name: "Asset" }
- { icon: "mdi-bullseye", id: "ioc", name: "IOC" }
artifactStates:
- { icon: "mdi-help-circle-outline", id: "unknown", name: "Unknown", color: "info" }
- { icon: "mdi-skull", id: "malicious", name: "Malicious", color: "error" }
- { icon: "mdi-check", id: "clean", name: "Clean", color: "success" }
roles: [
"admin:backup:read", "admin:backup:restore", "admin:dashboard:write", "admin:group:write", "admin:job:read", "admin:job:write",
"admin:log:read", "admin:settings:write", "admin:ticket:delete", "admin:user:write", "admin:userdata:read",
"admin:userdata:write", "analyst:automation:read",
"analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read",
"analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read",
"analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write",
"analyst:tickettype:read", "analyst:user:read", "engineer:automation:write",
"engineer:playbook:write", "engineer:rule:write", "engineer:template:write",
"engineer:tickettype:write" ]
security: [ { roles: [ "settings:write" ] } ]
definitions:
Settings:
type: object
required: [ version, tier, timeformat, ticketTypes, artifactStates ]
required: [ timeformat, artifactKinds, artifactStates ]
properties:
timeformat: { title: "Time Format", type: string }
artifactKinds: { title: "Artifact Kinds", type: array, items: { $ref: "#/definitions/Type" } }
artifactStates: { title: "Artifact States", type: array, items: { $ref: "#/definitions/Type" } }
SettingsResponse:
type: object
required: [ version, tier, timeformat, ticketTypes, artifactKinds, artifactStates ]
properties:
version: { title: "Version", type: string }
tier: { title: "Tier", type: string, enum: [ "community", "enterprise" ] }
timeformat: { title: "Time Format", type: string }
ticketTypes: { title: "Ticket Types", type: array, items: { $ref: "#/definitions/TicketTypeResponse" } }
artifactKinds: { title: "Artifact Kinds", type: array, items: { $ref: "#/definitions/Type" } }
artifactStates: { title: "Artifact States", type: array, items: { $ref: "#/definitions/Type" } }
roles: { title: "Roles", type: array, items: { type: string } }

View File

@@ -1,31 +0,0 @@
swagger: "2.0"
info: { version: "", title: "" }
paths:
/statistics:
get:
tags: [ "statistics" ]
summary: "Get statistics"
operationId: "getStatistics"
responses:
"200":
description: "successful operation"
schema: { $ref: '#/definitions/Statistics' }
examples:
test:
unassigned: 0
open_tickets_per_user: { }
tickets_per_week: { "2021-39": 3 }
tickets_per_type: { "alert": 2, "incident": 1 }
security: [ { roles: [ "ticket:read" ] } ]
definitions:
Statistics:
type: object
required: [ unassigned, open_tickets_per_user, tickets_per_week, tickets_per_type ]
properties:
unassigned: { type: integer }
open_tickets_per_user: { type: object, additionalProperties: { type: integer } }
tickets_per_week: { type: object, additionalProperties: { type: integer } }
tickets_per_type: { type: object, additionalProperties: { type: integer } }

View File

@@ -12,7 +12,7 @@ paths:
description: "successful operation"
schema: { $ref: "#/definitions/UserResponse" }
examples:
test: { id: bob, roles: [ "admin:backup:read", "admin:backup:restore", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ], blocked: false, apikey: false }
test: { id: bob, roles: [ "admin:backup:read", "admin:backup:restore", "admin:dashboard:write", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:settings:write", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read","analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ], blocked: false, apikey: false }
security: [ { roles: [ "currentuser:read" ] } ]
/users:
@@ -26,8 +26,8 @@ paths:
schema: { type: array, items: { $ref: "#/definitions/UserResponse" } }
examples:
test:
- { id: bob, blocked: false, roles: [ "admin:backup:read", "admin:backup:restore", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ], apikey: false }
- { id: script, roles: [ "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ], blocked: false, apikey: true }
- { id: bob, blocked: false, roles: [ "admin:backup:read", "admin:backup:restore", "admin:dashboard:write", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:settings:write", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ], apikey: false }
- { id: script, roles: [ "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ], blocked: false, apikey: true }
security: [ { roles: [ "user:read" ] } ]
post:
tags: [ "users" ]
@@ -40,7 +40,7 @@ paths:
description: "successful operation"
schema: { $ref: "#/definitions/NewUserResponse" }
examples:
test: { id: "syncscript", roles: [ "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read" ], secret: "v39bOuobnlEljfWzjAgoKzhmnh1xSMxH", blocked: false }
test: { id: "syncscript", roles: [ "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read" ], secret: "v39bOuobnlEljfWzjAgoKzhmnh1xSMxH", blocked: false }
security: [ { roles: [ "user:write" ] } ]
/users/{id}:
get:
@@ -54,7 +54,7 @@ paths:
description: "successful operation"
schema: { $ref: "#/definitions/UserResponse" }
examples:
test: { id: "script", roles: [ "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ], blocked: false, apikey: true }
test: { id: "script", roles: [ "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ], blocked: false, apikey: true }
security: [ { roles: [ "user:read" ] } ]
put:
tags: [ "users" ]
@@ -70,7 +70,7 @@ paths:
examples:
test:
id: bob
roles: [ "admin:backup:read", "admin:backup:restore", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
roles: [ "admin:backup:read", "admin:backup:restore", "admin:dashboard:write", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:settings:write", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
apikey: false
blocked: false
security: [ { roles: [ "user:write" ] } ]

View File

@@ -19,6 +19,12 @@ type Service interface {
CurrentUser(context.Context) (*model.UserResponse, error)
CurrentUserData(context.Context) (*model.UserDataResponse, error)
UpdateCurrentUserData(context.Context, *model.UserData) (*model.UserDataResponse, error)
DashboardData(context.Context, string, *string) (map[string]interface{}, error)
ListDashboards(context.Context) ([]*model.DashboardResponse, error)
CreateDashboard(context.Context, *model.Dashboard) (*model.DashboardResponse, error)
GetDashboard(context.Context, string) (*model.DashboardResponse, error)
UpdateDashboard(context.Context, string, *model.Dashboard) (*model.DashboardResponse, error)
DeleteDashboard(context.Context, string) error
ListJobs(context.Context) ([]*model.JobResponse, error)
RunJob(context.Context, *model.JobForm) (*model.JobResponse, error)
GetJob(context.Context, string) (*model.JobResponse, error)
@@ -29,7 +35,8 @@ type Service interface {
GetPlaybook(context.Context, string) (*model.PlaybookTemplateResponse, error)
UpdatePlaybook(context.Context, string, *model.PlaybookTemplateForm) (*model.PlaybookTemplateResponse, error)
DeletePlaybook(context.Context, string) error
GetSettings(context.Context) (*model.Settings, error)
GetSettings(context.Context) (*model.SettingsResponse, error)
SaveSettings(context.Context, *model.Settings) (*model.SettingsResponse, error)
GetStatistics(context.Context) (*model.Statistics, error)
ListTasks(context.Context) ([]*model.TaskWithContext, error)
ListTemplates(context.Context) ([]*model.TicketTemplateResponse, error)
@@ -90,6 +97,12 @@ func NewServer(service Service, roleAuth func([]string) func(http.Handler) http.
r.With(roleAuth([]string{"currentuser:read"})).Get("/currentuser", s.currentUserHandler)
r.With(roleAuth([]string{"currentuserdata:read"})).Get("/currentuserdata", s.currentUserDataHandler)
r.With(roleAuth([]string{"currentuserdata:write"})).Put("/currentuserdata", s.updateCurrentUserDataHandler)
r.With(roleAuth([]string{"dashboard:read"})).Get("/dashboard/data", s.dashboardDataHandler)
r.With(roleAuth([]string{"dashboard:read"})).Get("/dashboards", s.listDashboardsHandler)
r.With(roleAuth([]string{"dashboard:write"})).Post("/dashboards", s.createDashboardHandler)
r.With(roleAuth([]string{"dashboard:read"})).Get("/dashboards/{id}", s.getDashboardHandler)
r.With(roleAuth([]string{"dashboard:write"})).Put("/dashboards/{id}", s.updateDashboardHandler)
r.With(roleAuth([]string{"dashboard:write"})).Delete("/dashboards/{id}", s.deleteDashboardHandler)
r.With(roleAuth([]string{"job:read"})).Get("/jobs", s.listJobsHandler)
r.With(roleAuth([]string{"job:write"})).Post("/jobs", s.runJobHandler)
r.With(roleAuth([]string{"job:read"})).Get("/jobs/{id}", s.getJobHandler)
@@ -101,6 +114,7 @@ func NewServer(service Service, roleAuth func([]string) func(http.Handler) http.
r.With(roleAuth([]string{"playbook:write"})).Put("/playbooks/{id}", s.updatePlaybookHandler)
r.With(roleAuth([]string{"playbook:write"})).Delete("/playbooks/{id}", s.deletePlaybookHandler)
r.With(roleAuth([]string{"settings:read"})).Get("/settings", s.getSettingsHandler)
r.With(roleAuth([]string{"settings:write"})).Post("/settings", s.saveSettingsHandler)
r.With(roleAuth([]string{"ticket:read"})).Get("/statistics", s.getStatisticsHandler)
r.With(roleAuth([]string{"ticket:read"})).Get("/tasks", s.listTasksHandler)
r.With(roleAuth([]string{"template:read"})).Get("/templates", s.listTemplatesHandler)
@@ -245,6 +259,77 @@ func (s *server) updateCurrentUserDataHandler(w http.ResponseWriter, r *http.Req
response(w, result, err)
}
func (s *server) dashboardDataHandler(w http.ResponseWriter, r *http.Request) {
aggregationP := r.URL.Query().Get("aggregation")
filterP := r.URL.Query().Get("filter")
result, err := s.service.DashboardData(r.Context(), aggregationP, &filterP)
response(w, result, err)
}
func (s *server) listDashboardsHandler(w http.ResponseWriter, r *http.Request) {
result, err := s.service.ListDashboards(r.Context())
response(w, result, err)
}
func (s *server) createDashboardHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
JSONError(w, err)
return
}
if validateSchema(body, model.DashboardSchema, w) {
return
}
var templateP *model.Dashboard
if err := parseBody(body, &templateP); err != nil {
JSONError(w, err)
return
}
result, err := s.service.CreateDashboard(r.Context(), templateP)
response(w, result, err)
}
func (s *server) getDashboardHandler(w http.ResponseWriter, r *http.Request) {
idP := chi.URLParam(r, "id")
result, err := s.service.GetDashboard(r.Context(), idP)
response(w, result, err)
}
func (s *server) updateDashboardHandler(w http.ResponseWriter, r *http.Request) {
idP := chi.URLParam(r, "id")
body, err := io.ReadAll(r.Body)
if err != nil {
JSONError(w, err)
return
}
if validateSchema(body, model.DashboardSchema, w) {
return
}
var dashboardP *model.Dashboard
if err := parseBody(body, &dashboardP); err != nil {
JSONError(w, err)
return
}
result, err := s.service.UpdateDashboard(r.Context(), idP, dashboardP)
response(w, result, err)
}
func (s *server) deleteDashboardHandler(w http.ResponseWriter, r *http.Request) {
idP := chi.URLParam(r, "id")
response(w, nil, s.service.DeleteDashboard(r.Context(), idP))
}
func (s *server) listJobsHandler(w http.ResponseWriter, r *http.Request) {
result, err := s.service.ListJobs(r.Context())
response(w, result, err)
@@ -375,6 +460,27 @@ func (s *server) getSettingsHandler(w http.ResponseWriter, r *http.Request) {
response(w, result, err)
}
func (s *server) saveSettingsHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
JSONError(w, err)
return
}
if validateSchema(body, model.SettingsSchema, w) {
return
}
var settingsP *model.Settings
if err := parseBody(body, &settingsP); err != nil {
JSONError(w, err)
return
}
result, err := s.service.SaveSettings(r.Context(), settingsP)
response(w, result, err)
}
func (s *server) getStatisticsHandler(w http.ResponseWriter, r *http.Request) {
result, err := s.service.GetStatistics(r.Context())
response(w, result, err)

View File

@@ -5,8 +5,23 @@ import (
"net/http"
"net/http/httputil"
"net/url"
"strings"
)
func VueStatic(fsys fs.FS) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
handler := http.FileServer(http.FS(fsys))
if strings.HasPrefix(r.URL.Path, "/static/") {
handler = http.StripPrefix("/static/", handler)
} else {
r.URL.Path = "/"
}
handler.ServeHTTP(w, r)
}
}
func Static(fsys fs.FS) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
http.FileServer(http.FS(fsys)).ServeHTTP(w, r)

View File

@@ -68,7 +68,7 @@ var Tests = []struct {
Args: Args{Method: "Get", URL: "/currentuser"},
Want: Want{
Status: 200,
Body: map[string]interface{}{"apikey": false, "blocked": false, "id": "bob", "roles": []interface{}{"admin:backup:read", "admin:backup:restore", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write"}},
Body: map[string]interface{}{"apikey": false, "blocked": false, "id": "bob", "roles": []interface{}{"admin:backup:read", "admin:backup:restore", "admin:dashboard:write", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:settings:write", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write"}},
},
},
@@ -90,6 +90,60 @@ var Tests = []struct {
},
},
{
Name: "DashboardData",
Args: Args{Method: "Get", URL: "/dashboard/data?aggregation=type&filter=status+%3D%3D+%22closed%22"},
Want: Want{
Status: 200,
Body: map[string]interface{}{"alert": 2, "incident": 1},
},
},
{
Name: "ListDashboards",
Args: Args{Method: "Get", URL: "/dashboards"},
Want: Want{
Status: 200,
Body: []interface{}{map[string]interface{}{"id": "simple", "name": "Simple", "widgets": []interface{}{map[string]interface{}{"aggregation": "owner", "filter": "status == \"open\"", "name": "open_tickets_per_user", "type": "bar", "width": 4}, map[string]interface{}{"aggregation": "CONCAT(DATE_YEAR(created), \"-\", DATE_ISOWEEK(created) < 10 ? \"0\" : \"\", DATE_ISOWEEK(created))", "name": "tickets_per_week", "type": "line", "width": 8}}}},
},
},
{
Name: "CreateDashboard",
Args: Args{Method: "Post", URL: "/dashboards", Data: map[string]interface{}{"name": "My Dashboard", "widgets": []interface{}{}}},
Want: Want{
Status: 200,
Body: map[string]interface{}{"id": "my-dashboard", "name": "My Dashboard", "widgets": []interface{}{}},
},
},
{
Name: "GetDashboard",
Args: Args{Method: "Get", URL: "/dashboards/simple"},
Want: Want{
Status: 200,
Body: map[string]interface{}{"id": "simple", "name": "Simple", "widgets": []interface{}{map[string]interface{}{"aggregation": "owner", "filter": "status == \"open\"", "name": "open_tickets_per_user", "type": "bar", "width": 4}, map[string]interface{}{"aggregation": "CONCAT(DATE_YEAR(created), \"-\", DATE_ISOWEEK(created) < 10 ? \"0\" : \"\", DATE_ISOWEEK(created))", "name": "tickets_per_week", "type": "line", "width": 8}}},
},
},
{
Name: "UpdateDashboard",
Args: Args{Method: "Put", URL: "/dashboards/simple", Data: map[string]interface{}{"name": "Simple", "widgets": []interface{}{}}},
Want: Want{
Status: 200,
Body: map[string]interface{}{"id": "simple", "name": "Simple", "widgets": []interface{}{}},
},
},
{
Name: "DeleteDashboard",
Args: Args{Method: "Delete", URL: "/dashboards/simple"},
Want: Want{
Status: 204,
Body: nil,
},
},
{
Name: "ListJobs",
Args: Args{Method: "Get", URL: "/jobs"},
@@ -185,7 +239,16 @@ var Tests = []struct {
Args: Args{Method: "Get", URL: "/settings"},
Want: Want{
Status: 200,
Body: map[string]interface{}{"artifactStates": []interface{}{map[string]interface{}{"color": "info", "icon": "mdi-help-circle-outline", "id": "unknown", "name": "Unknown"}, map[string]interface{}{"color": "error", "icon": "mdi-skull", "id": "malicious", "name": "Malicious"}, map[string]interface{}{"color": "success", "icon": "mdi-check", "id": "clean", "name": "Clean"}}, "roles": []interface{}{"admin:backup:read", "admin:backup:restore", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write"}, "ticketTypes": []interface{}{map[string]interface{}{"default_playbooks": []interface{}{}, "default_template": "default", "icon": "mdi-alert", "id": "alert", "name": "Alerts"}, map[string]interface{}{"default_playbooks": []interface{}{}, "default_template": "default", "icon": "mdi-radioactive", "id": "incident", "name": "Incidents"}, map[string]interface{}{"default_playbooks": []interface{}{}, "default_template": "default", "icon": "mdi-fingerprint", "id": "investigation", "name": "Forensic Investigations"}, map[string]interface{}{"default_playbooks": []interface{}{}, "default_template": "default", "icon": "mdi-target", "id": "hunt", "name": "Threat Hunting"}}, "tier": "community", "timeformat": "YYYY-MM-DDThh:mm:ss", "version": "0.0.0-test"},
Body: map[string]interface{}{"artifactKinds": []interface{}{map[string]interface{}{"icon": "mdi-server", "id": "asset", "name": "Asset"}, map[string]interface{}{"icon": "mdi-bullseye", "id": "ioc", "name": "IOC"}}, "artifactStates": []interface{}{map[string]interface{}{"color": "info", "icon": "mdi-help-circle-outline", "id": "unknown", "name": "Unknown"}, map[string]interface{}{"color": "error", "icon": "mdi-skull", "id": "malicious", "name": "Malicious"}, map[string]interface{}{"color": "success", "icon": "mdi-check", "id": "clean", "name": "Clean"}}, "roles": []interface{}{"admin:backup:read", "admin:backup:restore", "admin:dashboard:write", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:settings:write", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write"}, "ticketTypes": []interface{}{map[string]interface{}{"default_playbooks": []interface{}{}, "default_template": "default", "icon": "mdi-alert", "id": "alert", "name": "Alerts"}, map[string]interface{}{"default_playbooks": []interface{}{}, "default_template": "default", "icon": "mdi-radioactive", "id": "incident", "name": "Incidents"}, map[string]interface{}{"default_playbooks": []interface{}{}, "default_template": "default", "icon": "mdi-fingerprint", "id": "investigation", "name": "Forensic Investigations"}, map[string]interface{}{"default_playbooks": []interface{}{}, "default_template": "default", "icon": "mdi-target", "id": "hunt", "name": "Threat Hunting"}}, "tier": "community", "timeformat": "yyyy-MM-dd hh:mm:ss", "version": "0.0.0-test"},
},
},
{
Name: "SaveSettings",
Args: Args{Method: "Post", URL: "/settings", Data: map[string]interface{}{"artifactKinds": []interface{}{map[string]interface{}{"icon": "mdi-server", "id": "asset", "name": "Asset"}, map[string]interface{}{"icon": "mdi-bullseye", "id": "ioc", "name": "IOC"}}, "artifactStates": []interface{}{map[string]interface{}{"color": "info", "icon": "mdi-help-circle-outline", "id": "unknown", "name": "Unknown"}, map[string]interface{}{"color": "error", "icon": "mdi-skull", "id": "malicious", "name": "Malicious"}, map[string]interface{}{"color": "success", "icon": "mdi-check", "id": "clean", "name": "Clean"}}, "timeformat": "yyyy-MM-dd hh:mm:ss"}},
Want: Want{
Status: 200,
Body: map[string]interface{}{"artifactKinds": []interface{}{map[string]interface{}{"icon": "mdi-server", "id": "asset", "name": "Asset"}, map[string]interface{}{"icon": "mdi-bullseye", "id": "ioc", "name": "IOC"}}, "artifactStates": []interface{}{map[string]interface{}{"color": "info", "icon": "mdi-help-circle-outline", "id": "unknown", "name": "Unknown"}, map[string]interface{}{"color": "error", "icon": "mdi-skull", "id": "malicious", "name": "Malicious"}, map[string]interface{}{"color": "success", "icon": "mdi-check", "id": "clean", "name": "Clean"}}, "roles": []interface{}{"admin:backup:read", "admin:backup:restore", "admin:dashboard:write", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:settings:write", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write"}, "ticketTypes": []interface{}{map[string]interface{}{"default_playbooks": []interface{}{}, "default_template": "default", "icon": "mdi-alert", "id": "alert", "name": "Alerts"}, map[string]interface{}{"default_playbooks": []interface{}{}, "default_template": "default", "icon": "mdi-radioactive", "id": "incident", "name": "Incidents"}, map[string]interface{}{"default_playbooks": []interface{}{}, "default_template": "default", "icon": "mdi-fingerprint", "id": "investigation", "name": "Forensic Investigations"}, map[string]interface{}{"default_playbooks": []interface{}{}, "default_template": "default", "icon": "mdi-target", "id": "hunt", "name": "Threat Hunting"}}, "tier": "community", "timeformat": "yyyy-MM-dd hh:mm:ss", "version": "0.0.0-test"},
},
},
@@ -545,7 +608,7 @@ var Tests = []struct {
Args: Args{Method: "Get", URL: "/users"},
Want: Want{
Status: 200,
Body: []interface{}{map[string]interface{}{"apikey": false, "blocked": false, "id": "bob", "roles": []interface{}{"admin:backup:read", "admin:backup:restore", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write"}}, map[string]interface{}{"apikey": true, "blocked": false, "id": "script", "roles": []interface{}{"analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write"}}},
Body: []interface{}{map[string]interface{}{"apikey": false, "blocked": false, "id": "bob", "roles": []interface{}{"admin:backup:read", "admin:backup:restore", "admin:dashboard:write", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:settings:write", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write"}}, map[string]interface{}{"apikey": true, "blocked": false, "id": "script", "roles": []interface{}{"analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write"}}},
},
},
@@ -554,7 +617,7 @@ var Tests = []struct {
Args: Args{Method: "Post", URL: "/users", Data: map[string]interface{}{"apikey": true, "blocked": false, "id": "syncscript", "roles": []interface{}{"analyst"}}},
Want: Want{
Status: 200,
Body: map[string]interface{}{"blocked": false, "id": "syncscript", "roles": []interface{}{"analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read"}, "secret": "v39bOuobnlEljfWzjAgoKzhmnh1xSMxH"},
Body: map[string]interface{}{"blocked": false, "id": "syncscript", "roles": []interface{}{"analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read"}, "secret": "v39bOuobnlEljfWzjAgoKzhmnh1xSMxH"},
},
},
@@ -563,7 +626,7 @@ var Tests = []struct {
Args: Args{Method: "Get", URL: "/users/script"},
Want: Want{
Status: 200,
Body: map[string]interface{}{"apikey": true, "blocked": false, "id": "script", "roles": []interface{}{"analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write"}},
Body: map[string]interface{}{"apikey": true, "blocked": false, "id": "script", "roles": []interface{}{"analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write"}},
},
},
@@ -572,7 +635,7 @@ var Tests = []struct {
Args: Args{Method: "Put", URL: "/users/bob", Data: map[string]interface{}{"apikey": false, "blocked": false, "id": "syncscript", "roles": []interface{}{"analyst", "admin"}}},
Want: Want{
Status: 200,
Body: map[string]interface{}{"apikey": false, "blocked": false, "id": "bob", "roles": []interface{}{"admin:backup:read", "admin:backup:restore", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write"}},
Body: map[string]interface{}{"apikey": false, "blocked": false, "id": "bob", "roles": []interface{}{"admin:backup:read", "admin:backup:restore", "admin:dashboard:write", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:settings:write", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write"}},
},
},

View File

@@ -225,7 +225,7 @@
"apikey" : false,
"blocked" : false,
"id" : "bob",
"roles" : [ "admin:backup:read", "admin:backup:restore", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
"roles" : [ "admin:backup:read", "admin:backup:restore", "admin:dashboard:write", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:settings:write", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
}
}
},
@@ -307,6 +307,257 @@
"x-codegen-request-body-name" : "userdata"
}
},
"/dashboard/data" : {
"get" : {
"operationId" : "dashboardData",
"parameters" : [ {
"description" : "Aggregation",
"example" : "type",
"in" : "query",
"name" : "aggregation",
"required" : true,
"schema" : {
"type" : "string"
}
}, {
"description" : "Filter",
"example" : "status == \"closed\"",
"in" : "query",
"name" : "filter",
"schema" : {
"type" : "string"
}
} ],
"responses" : {
"200" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object"
}
},
"test" : {
"example" : {
"alert" : 2,
"incident" : 1
}
}
},
"description" : "successful operation"
}
},
"security" : [ {
"roles" : [ "dashboard:read" ]
} ],
"summary" : "Get widget data",
"tags" : [ "dashboards" ]
}
},
"/dashboards" : {
"get" : {
"operationId" : "listDashboards",
"responses" : {
"200" : {
"content" : {
"application/json" : {
"schema" : {
"items" : {
"$ref" : "#/components/schemas/DashboardResponse"
},
"type" : "array"
}
},
"test" : {
"example" : [ {
"id" : "simple",
"name" : "Simple",
"widgets" : [ {
"aggregation" : "owner",
"filter" : "status == \"open\"",
"name" : "open_tickets_per_user",
"type" : "bar",
"width" : 4
}, {
"aggregation" : "CONCAT(DATE_YEAR(created), \"-\", DATE_ISOWEEK(created) < 10 ? \"0\" : \"\", DATE_ISOWEEK(created))",
"name" : "tickets_per_week",
"type" : "line",
"width" : 8
} ]
} ]
}
},
"description" : "successful operation"
}
},
"security" : [ {
"roles" : [ "dashboard:read" ]
} ],
"summary" : "List dashboards",
"tags" : [ "dashboards" ]
},
"post" : {
"operationId" : "createDashboard",
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Dashboard"
}
}
},
"description" : "New template",
"required" : true
},
"responses" : {
"200" : {
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/DashboardResponse"
}
},
"test" : {
"example" : {
"id" : "my-dashboard",
"name" : "My Dashboard",
"widgets" : [ ]
}
}
},
"description" : "successful operation"
}
},
"security" : [ {
"roles" : [ "dashboard:write" ]
} ],
"summary" : "Create a new dashboard",
"tags" : [ "dashboards" ],
"x-codegen-request-body-name" : "template"
}
},
"/dashboards/{id}" : {
"delete" : {
"operationId" : "deleteDashboard",
"parameters" : [ {
"description" : "Dashboard ID",
"example" : "simple",
"in" : "path",
"name" : "id",
"required" : true,
"schema" : {
"type" : "string"
}
} ],
"responses" : {
"204" : {
"content" : { },
"description" : "successful operation"
}
},
"security" : [ {
"roles" : [ "dashboard:write" ]
} ],
"summary" : "Delete a dashboard",
"tags" : [ "dashboards" ]
},
"get" : {
"operationId" : "getDashboard",
"parameters" : [ {
"description" : "Dashboard ID",
"example" : "simple",
"in" : "path",
"name" : "id",
"required" : true,
"schema" : {
"type" : "string"
}
} ],
"responses" : {
"200" : {
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/DashboardResponse"
}
},
"test" : {
"example" : {
"id" : "simple",
"name" : "Simple",
"widgets" : [ {
"aggregation" : "owner",
"filter" : "status == \"open\"",
"name" : "open_tickets_per_user",
"type" : "bar",
"width" : 4
}, {
"aggregation" : "CONCAT(DATE_YEAR(created), \"-\", DATE_ISOWEEK(created) < 10 ? \"0\" : \"\", DATE_ISOWEEK(created))",
"name" : "tickets_per_week",
"type" : "line",
"width" : 8
} ]
}
}
},
"description" : "successful operation"
}
},
"security" : [ {
"roles" : [ "dashboard:read" ]
} ],
"summary" : "Get a single dashboard",
"tags" : [ "dashboards" ]
},
"put" : {
"operationId" : "updateDashboard",
"parameters" : [ {
"description" : "Dashboard ID",
"example" : "simple",
"in" : "path",
"name" : "id",
"required" : true,
"schema" : {
"type" : "string"
}
} ],
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Dashboard"
}
}
},
"description" : "Dashboard object that needs to be added",
"required" : true
},
"responses" : {
"200" : {
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/DashboardResponse"
}
},
"test" : {
"example" : {
"id" : "simple",
"name" : "Simple",
"widgets" : [ ]
}
}
},
"description" : "successful operation"
}
},
"security" : [ {
"roles" : [ "dashboard:write" ]
} ],
"summary" : "Update an existing dashboard",
"tags" : [ "dashboards" ],
"x-codegen-request-body-name" : "dashboard"
}
},
"/graph/{col}/{id}" : {
"get" : {
"operationId" : "graph",
@@ -1143,11 +1394,20 @@
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Settings"
"$ref" : "#/components/schemas/SettingsResponse"
}
},
"test" : {
"example" : {
"artifactKinds" : [ {
"icon" : "mdi-server",
"id" : "asset",
"name" : "Asset"
}, {
"icon" : "mdi-bullseye",
"id" : "ioc",
"name" : "IOC"
} ],
"artifactStates" : [ {
"color" : "info",
"icon" : "mdi-help-circle-outline",
@@ -1164,7 +1424,7 @@
"id" : "clean",
"name" : "Clean"
} ],
"roles" : [ "admin:backup:read", "admin:backup:restore", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ],
"roles" : [ "admin:backup:read", "admin:backup:restore", "admin:dashboard:write", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:settings:write", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ],
"ticketTypes" : [ {
"default_playbooks" : [ ],
"default_template" : "default",
@@ -1191,7 +1451,7 @@
"name" : "Threat Hunting"
} ],
"tier" : "community",
"timeformat" : "YYYY-MM-DDThh:mm:ss",
"timeformat" : "yyyy-MM-dd hh:mm:ss",
"version" : "0.0.0-test"
}
}
@@ -1204,6 +1464,96 @@
} ],
"summary" : "Get settings",
"tags" : [ "settings" ]
},
"post" : {
"operationId" : "saveSettings",
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Settings"
}
}
},
"description" : "Save settings",
"required" : true
},
"responses" : {
"200" : {
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/SettingsResponse"
}
},
"test" : {
"example" : {
"artifactKinds" : [ {
"icon" : "mdi-server",
"id" : "asset",
"name" : "Asset"
}, {
"icon" : "mdi-bullseye",
"id" : "ioc",
"name" : "IOC"
} ],
"artifactStates" : [ {
"color" : "info",
"icon" : "mdi-help-circle-outline",
"id" : "unknown",
"name" : "Unknown"
}, {
"color" : "error",
"icon" : "mdi-skull",
"id" : "malicious",
"name" : "Malicious"
}, {
"color" : "success",
"icon" : "mdi-check",
"id" : "clean",
"name" : "Clean"
} ],
"roles" : [ "admin:backup:read", "admin:backup:restore", "admin:dashboard:write", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:settings:write", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ],
"ticketTypes" : [ {
"default_playbooks" : [ ],
"default_template" : "default",
"icon" : "mdi-alert",
"id" : "alert",
"name" : "Alerts"
}, {
"default_playbooks" : [ ],
"default_template" : "default",
"icon" : "mdi-radioactive",
"id" : "incident",
"name" : "Incidents"
}, {
"default_playbooks" : [ ],
"default_template" : "default",
"icon" : "mdi-fingerprint",
"id" : "investigation",
"name" : "Forensic Investigations"
}, {
"default_playbooks" : [ ],
"default_template" : "default",
"icon" : "mdi-target",
"id" : "hunt",
"name" : "Threat Hunting"
} ],
"tier" : "community",
"timeformat" : "yyyy-MM-dd hh:mm:ss",
"version" : "0.0.0-test"
}
}
},
"description" : "successful operation"
}
},
"security" : [ {
"roles" : [ "settings:write" ]
} ],
"summary" : "Save settings",
"tags" : [ "settings" ],
"x-codegen-request-body-name" : "settings"
}
},
"/statistics" : {
@@ -4993,12 +5343,12 @@
"apikey" : false,
"blocked" : false,
"id" : "bob",
"roles" : [ "admin:backup:read", "admin:backup:restore", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
"roles" : [ "admin:backup:read", "admin:backup:restore", "admin:dashboard:write", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:settings:write", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
}, {
"apikey" : true,
"blocked" : false,
"id" : "script",
"roles" : [ "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
"roles" : [ "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
} ]
}
},
@@ -5036,7 +5386,7 @@
"example" : {
"blocked" : false,
"id" : "syncscript",
"roles" : [ "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read" ],
"roles" : [ "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read" ],
"secret" : "v39bOuobnlEljfWzjAgoKzhmnh1xSMxH"
}
}
@@ -5102,7 +5452,7 @@
"apikey" : true,
"blocked" : false,
"id" : "script",
"roles" : [ "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
"roles" : [ "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
}
}
},
@@ -5151,7 +5501,7 @@
"apikey" : false,
"blocked" : false,
"id" : "bob",
"roles" : [ "admin:backup:read", "admin:backup:restore", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
"roles" : [ "admin:backup:read", "admin:backup:restore", "admin:dashboard:write", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:settings:write", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
}
}
},
@@ -5177,6 +5527,9 @@
},
"type" : "object"
},
"kind" : {
"type" : "string"
},
"name" : {
"example" : "2.2.2.2",
"type" : "string"
@@ -5329,6 +5682,39 @@
},
"type" : "object"
},
"Dashboard" : {
"properties" : {
"name" : {
"type" : "string"
},
"widgets" : {
"items" : {
"$ref" : "#/components/schemas/Widget"
},
"type" : "array"
}
},
"required" : [ "name", "widgets" ],
"type" : "object"
},
"DashboardResponse" : {
"properties" : {
"id" : {
"type" : "string"
},
"name" : {
"type" : "string"
},
"widgets" : {
"items" : {
"$ref" : "#/components/schemas/Widget"
},
"type" : "array"
}
},
"required" : [ "id", "name", "widgets" ],
"type" : "object"
},
"Enrichment" : {
"properties" : {
"created" : {
@@ -5793,6 +6179,37 @@
},
"Settings" : {
"properties" : {
"artifactKinds" : {
"items" : {
"$ref" : "#/components/schemas/Type"
},
"title" : "Artifact Kinds",
"type" : "array"
},
"artifactStates" : {
"items" : {
"$ref" : "#/components/schemas/Type"
},
"title" : "Artifact States",
"type" : "array"
},
"timeformat" : {
"title" : "Time Format",
"type" : "string"
}
},
"required" : [ "artifactKinds", "artifactStates", "timeformat" ],
"type" : "object"
},
"SettingsResponse" : {
"properties" : {
"artifactKinds" : {
"items" : {
"$ref" : "#/components/schemas/Type"
},
"title" : "Artifact Kinds",
"type" : "array"
},
"artifactStates" : {
"items" : {
"$ref" : "#/components/schemas/Type"
@@ -5828,7 +6245,7 @@
"type" : "string"
}
},
"required" : [ "artifactStates", "ticketTypes", "tier", "timeformat", "version" ],
"required" : [ "artifactKinds", "artifactStates", "ticketTypes", "tier", "timeformat", "version" ],
"type" : "object"
},
"Statistics" : {
@@ -6762,6 +7179,30 @@
},
"required" : [ "apikey", "blocked", "id", "roles" ],
"type" : "object"
},
"Widget" : {
"properties" : {
"aggregation" : {
"type" : "string"
},
"filter" : {
"type" : "string"
},
"name" : {
"type" : "string"
},
"type" : {
"enum" : [ "bar", "line", "pie" ],
"type" : "string"
},
"width" : {
"maximum" : 12,
"minimum" : 1,
"type" : "integer"
}
},
"required" : [ "aggregation", "name", "type", "width" ],
"type" : "object"
}
}
},

View File

@@ -8,6 +8,8 @@ definitions:
additionalProperties:
$ref: '#/definitions/Enrichment'
type: object
kind:
type: string
name:
example: 2.2.2.2
type: string
@@ -139,6 +141,33 @@ definitions:
ticket:
$ref: '#/definitions/TicketResponse'
type: object
Dashboard:
properties:
name:
type: string
widgets:
items:
$ref: '#/definitions/Widget'
type: array
required:
- name
- widgets
type: object
DashboardResponse:
properties:
id:
type: string
name:
type: string
widgets:
items:
$ref: '#/definitions/Widget'
type: array
required:
- id
- name
- widgets
type: object
Enrichment:
properties:
created:
@@ -501,6 +530,31 @@ definitions:
type: object
Settings:
properties:
artifactKinds:
items:
$ref: '#/definitions/Type'
title: Artifact Kinds
type: array
artifactStates:
items:
$ref: '#/definitions/Type'
title: Artifact States
type: array
timeformat:
title: Time Format
type: string
required:
- timeformat
- artifactKinds
- artifactStates
type: object
SettingsResponse:
properties:
artifactKinds:
items:
$ref: '#/definitions/Type'
title: Artifact Kinds
type: array
artifactStates:
items:
$ref: '#/definitions/Type'
@@ -533,6 +587,7 @@ definitions:
- tier
- timeformat
- ticketTypes
- artifactKinds
- artifactStates
type: object
Statistics:
@@ -1307,6 +1362,30 @@ definitions:
- roles
- apikey
type: object
Widget:
properties:
aggregation:
type: string
filter:
type: string
name:
type: string
type:
enum:
- bar
- line
- pie
type: string
width:
maximum: 12
minimum: 1
type: integer
required:
- name
- type
- aggregation
- width
type: object
host: .
info:
description: API for the catalyst incident response platform.
@@ -1548,10 +1627,12 @@ paths:
roles:
- admin:backup:read
- admin:backup:restore
- admin:dashboard:write
- admin:group:write
- admin:job:read
- admin:job:write
- admin:log:read
- admin:settings:write
- admin:ticket:delete
- admin:user:write
- admin:userdata:read
@@ -1560,6 +1641,7 @@ paths:
- analyst:currentsettings:write
- analyst:currentuser:read
- analyst:currentuserdata:read
- analyst:dashboard:read
- analyst:file
- analyst:group:read
- analyst:playbook:read
@@ -1630,6 +1712,183 @@ paths:
summary: Update current user data
tags:
- userdata
/dashboard/data:
get:
operationId: dashboardData
parameters:
- description: Aggregation
in: query
name: aggregation
required: true
type: string
x-example: type
- description: Filter
in: query
name: filter
type: string
x-example: status == "closed"
responses:
"200":
description: successful operation
examples:
test:
alert: 2
incident: 1
schema:
type: object
security:
- roles:
- dashboard:read
summary: Get widget data
tags:
- dashboards
/dashboards:
get:
operationId: listDashboards
responses:
"200":
description: successful operation
examples:
test:
- id: simple
name: Simple
widgets:
- aggregation: owner
filter: status == "open"
name: open_tickets_per_user
type: bar
width: 4
- aggregation: 'CONCAT(DATE_YEAR(created), "-", DATE_ISOWEEK(created)
< 10 ? "0" : "", DATE_ISOWEEK(created))'
name: tickets_per_week
type: line
width: 8
schema:
items:
$ref: '#/definitions/DashboardResponse'
type: array
security:
- roles:
- dashboard:read
summary: List dashboards
tags:
- dashboards
post:
operationId: createDashboard
parameters:
- description: New template
in: body
name: template
required: true
schema:
$ref: '#/definitions/Dashboard'
x-example:
name: My Dashboard
widgets: []
responses:
"200":
description: successful operation
examples:
test:
id: my-dashboard
name: My Dashboard
widgets: []
schema:
$ref: '#/definitions/DashboardResponse'
security:
- roles:
- dashboard:write
summary: Create a new dashboard
tags:
- dashboards
/dashboards/{id}:
delete:
operationId: deleteDashboard
parameters:
- description: Dashboard ID
in: path
name: id
required: true
type: string
x-example: simple
responses:
"204":
description: successful operation
security:
- roles:
- dashboard:write
summary: Delete a dashboard
tags:
- dashboards
get:
operationId: getDashboard
parameters:
- description: Dashboard ID
in: path
name: id
required: true
type: string
x-example: simple
responses:
"200":
description: successful operation
examples:
test:
id: simple
name: Simple
widgets:
- aggregation: owner
filter: status == "open"
name: open_tickets_per_user
type: bar
width: 4
- aggregation: 'CONCAT(DATE_YEAR(created), "-", DATE_ISOWEEK(created)
< 10 ? "0" : "", DATE_ISOWEEK(created))'
name: tickets_per_week
type: line
width: 8
schema:
$ref: '#/definitions/DashboardResponse'
security:
- roles:
- dashboard:read
summary: Get a single dashboard
tags:
- dashboards
put:
operationId: updateDashboard
parameters:
- description: Dashboard ID
in: path
name: id
required: true
type: string
x-example: simple
- description: Dashboard object that needs to be added
in: body
name: dashboard
required: true
schema:
$ref: '#/definitions/Dashboard'
x-example:
name: Simple
widgets: []
responses:
"200":
description: successful operation
examples:
test:
id: simple
name: Simple
widgets: []
schema:
$ref: '#/definitions/DashboardResponse'
security:
- roles:
- dashboard:write
summary: Update an existing dashboard
tags:
- dashboards
/graph/{col}/{id}:
get:
operationId: graph
@@ -2549,6 +2808,13 @@ paths:
description: successful operation
examples:
test:
artifactKinds:
- icon: mdi-server
id: asset
name: Asset
- icon: mdi-bullseye
id: ioc
name: IOC
artifactStates:
- color: info
icon: mdi-help-circle-outline
@@ -2565,10 +2831,12 @@ paths:
roles:
- admin:backup:read
- admin:backup:restore
- admin:dashboard:write
- admin:group:write
- admin:job:read
- admin:job:write
- admin:log:read
- admin:settings:write
- admin:ticket:delete
- admin:user:write
- admin:userdata:read
@@ -2577,6 +2845,7 @@ paths:
- analyst:currentsettings:write
- analyst:currentuser:read
- analyst:currentuserdata:read
- analyst:dashboard:read
- analyst:file
- analyst:group:read
- analyst:playbook:read
@@ -2614,16 +2883,137 @@ paths:
id: hunt
name: Threat Hunting
tier: community
timeformat: YYYY-MM-DDThh:mm:ss
timeformat: yyyy-MM-dd hh:mm:ss
version: 0.0.0-test
schema:
$ref: '#/definitions/Settings'
$ref: '#/definitions/SettingsResponse'
security:
- roles:
- settings:read
summary: Get settings
tags:
- settings
post:
operationId: saveSettings
parameters:
- description: Save settings
in: body
name: settings
required: true
schema:
$ref: '#/definitions/Settings'
x-example:
artifactKinds:
- icon: mdi-server
id: asset
name: Asset
- icon: mdi-bullseye
id: ioc
name: IOC
artifactStates:
- color: info
icon: mdi-help-circle-outline
id: unknown
name: Unknown
- color: error
icon: mdi-skull
id: malicious
name: Malicious
- color: success
icon: mdi-check
id: clean
name: Clean
timeformat: yyyy-MM-dd hh:mm:ss
responses:
"200":
description: successful operation
examples:
test:
artifactKinds:
- icon: mdi-server
id: asset
name: Asset
- icon: mdi-bullseye
id: ioc
name: IOC
artifactStates:
- color: info
icon: mdi-help-circle-outline
id: unknown
name: Unknown
- color: error
icon: mdi-skull
id: malicious
name: Malicious
- color: success
icon: mdi-check
id: clean
name: Clean
roles:
- admin:backup:read
- admin:backup:restore
- admin:dashboard:write
- admin:group:write
- admin:job:read
- admin:job:write
- admin:log:read
- admin:settings:write
- admin:ticket:delete
- admin:user:write
- admin:userdata:read
- admin:userdata:write
- analyst:automation:read
- analyst:currentsettings:write
- analyst:currentuser:read
- analyst:currentuserdata:read
- analyst:dashboard:read
- analyst:file
- analyst:group:read
- analyst:playbook:read
- analyst:rule:read
- analyst:settings:read
- analyst:template:read
- analyst:ticket:read
- analyst:ticket:write
- analyst:tickettype:read
- analyst:user:read
- engineer:automation:write
- engineer:playbook:write
- engineer:rule:write
- engineer:template:write
- engineer:tickettype:write
ticketTypes:
- default_playbooks: []
default_template: default
icon: mdi-alert
id: alert
name: Alerts
- default_playbooks: []
default_template: default
icon: mdi-radioactive
id: incident
name: Incidents
- default_playbooks: []
default_template: default
icon: mdi-fingerprint
id: investigation
name: Forensic Investigations
- default_playbooks: []
default_template: default
icon: mdi-target
id: hunt
name: Threat Hunting
tier: community
timeformat: yyyy-MM-dd hh:mm:ss
version: 0.0.0-test
schema:
$ref: '#/definitions/SettingsResponse'
security:
- roles:
- settings:write
summary: Save settings
tags:
- settings
/statistics:
get:
operationId: getStatistics
@@ -6966,10 +7356,12 @@ paths:
roles:
- admin:backup:read
- admin:backup:restore
- admin:dashboard:write
- admin:group:write
- admin:job:read
- admin:job:write
- admin:log:read
- admin:settings:write
- admin:ticket:delete
- admin:user:write
- admin:userdata:read
@@ -6978,6 +7370,7 @@ paths:
- analyst:currentsettings:write
- analyst:currentuser:read
- analyst:currentuserdata:read
- analyst:dashboard:read
- analyst:file
- analyst:group:read
- analyst:playbook:read
@@ -7001,6 +7394,7 @@ paths:
- analyst:currentsettings:write
- analyst:currentuser:read
- analyst:currentuserdata:read
- analyst:dashboard:read
- analyst:file
- analyst:group:read
- analyst:playbook:read
@@ -7053,6 +7447,7 @@ paths:
- analyst:currentsettings:write
- analyst:currentuser:read
- analyst:currentuserdata:read
- analyst:dashboard:read
- analyst:file
- analyst:group:read
- analyst:playbook:read
@@ -7113,6 +7508,7 @@ paths:
- analyst:currentsettings:write
- analyst:currentuser:read
- analyst:currentuserdata:read
- analyst:dashboard:read
- analyst:file
- analyst:group:read
- analyst:playbook:read
@@ -7169,10 +7565,12 @@ paths:
roles:
- admin:backup:read
- admin:backup:restore
- admin:dashboard:write
- admin:group:write
- admin:job:read
- admin:job:write
- admin:log:read
- admin:settings:write
- admin:ticket:delete
- admin:user:write
- admin:userdata:read
@@ -7181,6 +7579,7 @@ paths:
- analyst:currentsettings:write
- analyst:currentuser:read
- analyst:currentuserdata:read
- analyst:dashboard:read
- analyst:file
- analyst:group:read
- analyst:playbook:read

View File

@@ -225,7 +225,7 @@
"apikey" : false,
"blocked" : false,
"id" : "bob",
"roles" : [ "admin:backup:read", "admin:backup:restore", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
"roles" : [ "admin:backup:read", "admin:backup:restore", "admin:dashboard:write", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:settings:write", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
}
}
},
@@ -307,6 +307,257 @@
"x-codegen-request-body-name" : "userdata"
}
},
"/dashboard/data" : {
"get" : {
"operationId" : "dashboardData",
"parameters" : [ {
"description" : "Aggregation",
"example" : "type",
"in" : "query",
"name" : "aggregation",
"required" : true,
"schema" : {
"type" : "string"
}
}, {
"description" : "Filter",
"example" : "status == \"closed\"",
"in" : "query",
"name" : "filter",
"schema" : {
"type" : "string"
}
} ],
"responses" : {
"200" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object"
}
},
"test" : {
"example" : {
"alert" : 2,
"incident" : 1
}
}
},
"description" : "successful operation"
}
},
"security" : [ {
"roles" : [ "dashboard:read" ]
} ],
"summary" : "Get widget data",
"tags" : [ "dashboards" ]
}
},
"/dashboards" : {
"get" : {
"operationId" : "listDashboards",
"responses" : {
"200" : {
"content" : {
"application/json" : {
"schema" : {
"items" : {
"$ref" : "#/components/schemas/DashboardResponse"
},
"type" : "array"
}
},
"test" : {
"example" : [ {
"id" : "simple",
"name" : "Simple",
"widgets" : [ {
"aggregation" : "owner",
"filter" : "status == \"open\"",
"name" : "open_tickets_per_user",
"type" : "bar",
"width" : 4
}, {
"aggregation" : "CONCAT(DATE_YEAR(created), \"-\", DATE_ISOWEEK(created) < 10 ? \"0\" : \"\", DATE_ISOWEEK(created))",
"name" : "tickets_per_week",
"type" : "line",
"width" : 8
} ]
} ]
}
},
"description" : "successful operation"
}
},
"security" : [ {
"roles" : [ "dashboard:read" ]
} ],
"summary" : "List dashboards",
"tags" : [ "dashboards" ]
},
"post" : {
"operationId" : "createDashboard",
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Dashboard"
}
}
},
"description" : "New template",
"required" : true
},
"responses" : {
"200" : {
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/DashboardResponse"
}
},
"test" : {
"example" : {
"id" : "my-dashboard",
"name" : "My Dashboard",
"widgets" : [ ]
}
}
},
"description" : "successful operation"
}
},
"security" : [ {
"roles" : [ "dashboard:write" ]
} ],
"summary" : "Create a new dashboard",
"tags" : [ "dashboards" ],
"x-codegen-request-body-name" : "template"
}
},
"/dashboards/{id}" : {
"delete" : {
"operationId" : "deleteDashboard",
"parameters" : [ {
"description" : "Dashboard ID",
"example" : "simple",
"in" : "path",
"name" : "id",
"required" : true,
"schema" : {
"type" : "string"
}
} ],
"responses" : {
"204" : {
"content" : { },
"description" : "successful operation"
}
},
"security" : [ {
"roles" : [ "dashboard:write" ]
} ],
"summary" : "Delete a dashboard",
"tags" : [ "dashboards" ]
},
"get" : {
"operationId" : "getDashboard",
"parameters" : [ {
"description" : "Dashboard ID",
"example" : "simple",
"in" : "path",
"name" : "id",
"required" : true,
"schema" : {
"type" : "string"
}
} ],
"responses" : {
"200" : {
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/DashboardResponse"
}
},
"test" : {
"example" : {
"id" : "simple",
"name" : "Simple",
"widgets" : [ {
"aggregation" : "owner",
"filter" : "status == \"open\"",
"name" : "open_tickets_per_user",
"type" : "bar",
"width" : 4
}, {
"aggregation" : "CONCAT(DATE_YEAR(created), \"-\", DATE_ISOWEEK(created) < 10 ? \"0\" : \"\", DATE_ISOWEEK(created))",
"name" : "tickets_per_week",
"type" : "line",
"width" : 8
} ]
}
}
},
"description" : "successful operation"
}
},
"security" : [ {
"roles" : [ "dashboard:read" ]
} ],
"summary" : "Get a single dashboard",
"tags" : [ "dashboards" ]
},
"put" : {
"operationId" : "updateDashboard",
"parameters" : [ {
"description" : "Dashboard ID",
"example" : "simple",
"in" : "path",
"name" : "id",
"required" : true,
"schema" : {
"type" : "string"
}
} ],
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Dashboard"
}
}
},
"description" : "Dashboard object that needs to be added",
"required" : true
},
"responses" : {
"200" : {
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/DashboardResponse"
}
},
"test" : {
"example" : {
"id" : "simple",
"name" : "Simple",
"widgets" : [ ]
}
}
},
"description" : "successful operation"
}
},
"security" : [ {
"roles" : [ "dashboard:write" ]
} ],
"summary" : "Update an existing dashboard",
"tags" : [ "dashboards" ],
"x-codegen-request-body-name" : "dashboard"
}
},
"/jobs" : {
"get" : {
"operationId" : "listJobs",
@@ -713,11 +964,20 @@
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Settings"
"$ref" : "#/components/schemas/SettingsResponse"
}
},
"test" : {
"example" : {
"artifactKinds" : [ {
"icon" : "mdi-server",
"id" : "asset",
"name" : "Asset"
}, {
"icon" : "mdi-bullseye",
"id" : "ioc",
"name" : "IOC"
} ],
"artifactStates" : [ {
"color" : "info",
"icon" : "mdi-help-circle-outline",
@@ -734,7 +994,7 @@
"id" : "clean",
"name" : "Clean"
} ],
"roles" : [ "admin:backup:read", "admin:backup:restore", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ],
"roles" : [ "admin:backup:read", "admin:backup:restore", "admin:dashboard:write", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:settings:write", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ],
"ticketTypes" : [ {
"default_playbooks" : [ ],
"default_template" : "default",
@@ -761,7 +1021,7 @@
"name" : "Threat Hunting"
} ],
"tier" : "community",
"timeformat" : "YYYY-MM-DDThh:mm:ss",
"timeformat" : "yyyy-MM-dd hh:mm:ss",
"version" : "0.0.0-test"
}
}
@@ -774,6 +1034,96 @@
} ],
"summary" : "Get settings",
"tags" : [ "settings" ]
},
"post" : {
"operationId" : "saveSettings",
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Settings"
}
}
},
"description" : "Save settings",
"required" : true
},
"responses" : {
"200" : {
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/SettingsResponse"
}
},
"test" : {
"example" : {
"artifactKinds" : [ {
"icon" : "mdi-server",
"id" : "asset",
"name" : "Asset"
}, {
"icon" : "mdi-bullseye",
"id" : "ioc",
"name" : "IOC"
} ],
"artifactStates" : [ {
"color" : "info",
"icon" : "mdi-help-circle-outline",
"id" : "unknown",
"name" : "Unknown"
}, {
"color" : "error",
"icon" : "mdi-skull",
"id" : "malicious",
"name" : "Malicious"
}, {
"color" : "success",
"icon" : "mdi-check",
"id" : "clean",
"name" : "Clean"
} ],
"roles" : [ "admin:backup:read", "admin:backup:restore", "admin:dashboard:write", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:settings:write", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ],
"ticketTypes" : [ {
"default_playbooks" : [ ],
"default_template" : "default",
"icon" : "mdi-alert",
"id" : "alert",
"name" : "Alerts"
}, {
"default_playbooks" : [ ],
"default_template" : "default",
"icon" : "mdi-radioactive",
"id" : "incident",
"name" : "Incidents"
}, {
"default_playbooks" : [ ],
"default_template" : "default",
"icon" : "mdi-fingerprint",
"id" : "investigation",
"name" : "Forensic Investigations"
}, {
"default_playbooks" : [ ],
"default_template" : "default",
"icon" : "mdi-target",
"id" : "hunt",
"name" : "Threat Hunting"
} ],
"tier" : "community",
"timeformat" : "yyyy-MM-dd hh:mm:ss",
"version" : "0.0.0-test"
}
}
},
"description" : "successful operation"
}
},
"security" : [ {
"roles" : [ "settings:write" ]
} ],
"summary" : "Save settings",
"tags" : [ "settings" ],
"x-codegen-request-body-name" : "settings"
}
},
"/statistics" : {
@@ -4563,12 +4913,12 @@
"apikey" : false,
"blocked" : false,
"id" : "bob",
"roles" : [ "admin:backup:read", "admin:backup:restore", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
"roles" : [ "admin:backup:read", "admin:backup:restore", "admin:dashboard:write", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:settings:write", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
}, {
"apikey" : true,
"blocked" : false,
"id" : "script",
"roles" : [ "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
"roles" : [ "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
} ]
}
},
@@ -4606,7 +4956,7 @@
"example" : {
"blocked" : false,
"id" : "syncscript",
"roles" : [ "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read" ],
"roles" : [ "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read" ],
"secret" : "v39bOuobnlEljfWzjAgoKzhmnh1xSMxH"
}
}
@@ -4672,7 +5022,7 @@
"apikey" : true,
"blocked" : false,
"id" : "script",
"roles" : [ "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
"roles" : [ "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
}
}
},
@@ -4721,7 +5071,7 @@
"apikey" : false,
"blocked" : false,
"id" : "bob",
"roles" : [ "admin:backup:read", "admin:backup:restore", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
"roles" : [ "admin:backup:read", "admin:backup:restore", "admin:dashboard:write", "admin:group:write", "admin:job:read", "admin:job:write", "admin:log:read", "admin:settings:write", "admin:ticket:delete", "admin:user:write", "admin:userdata:read", "admin:userdata:write", "analyst:automation:read", "analyst:currentsettings:write", "analyst:currentuser:read", "analyst:currentuserdata:read", "analyst:dashboard:read", "analyst:file", "analyst:group:read", "analyst:playbook:read", "analyst:rule:read", "analyst:settings:read", "analyst:template:read", "analyst:ticket:read", "analyst:ticket:write", "analyst:tickettype:read", "analyst:user:read", "engineer:automation:write", "engineer:playbook:write", "engineer:rule:write", "engineer:template:write", "engineer:tickettype:write" ]
}
}
},
@@ -4747,6 +5097,9 @@
},
"type" : "object"
},
"kind" : {
"type" : "string"
},
"name" : {
"example" : "2.2.2.2",
"type" : "string"
@@ -4899,6 +5252,39 @@
},
"type" : "object"
},
"Dashboard" : {
"properties" : {
"name" : {
"type" : "string"
},
"widgets" : {
"items" : {
"$ref" : "#/components/schemas/Widget"
},
"type" : "array"
}
},
"required" : [ "name", "widgets" ],
"type" : "object"
},
"DashboardResponse" : {
"properties" : {
"id" : {
"type" : "string"
},
"name" : {
"type" : "string"
},
"widgets" : {
"items" : {
"$ref" : "#/components/schemas/Widget"
},
"type" : "array"
}
},
"required" : [ "id", "name", "widgets" ],
"type" : "object"
},
"Enrichment" : {
"properties" : {
"created" : {
@@ -5214,6 +5600,37 @@
},
"Settings" : {
"properties" : {
"artifactKinds" : {
"items" : {
"$ref" : "#/components/schemas/Type"
},
"title" : "Artifact Kinds",
"type" : "array"
},
"artifactStates" : {
"items" : {
"$ref" : "#/components/schemas/Type"
},
"title" : "Artifact States",
"type" : "array"
},
"timeformat" : {
"title" : "Time Format",
"type" : "string"
}
},
"required" : [ "artifactKinds", "artifactStates", "timeformat" ],
"type" : "object"
},
"SettingsResponse" : {
"properties" : {
"artifactKinds" : {
"items" : {
"$ref" : "#/components/schemas/Type"
},
"title" : "Artifact Kinds",
"type" : "array"
},
"artifactStates" : {
"items" : {
"$ref" : "#/components/schemas/Type"
@@ -5249,7 +5666,7 @@
"type" : "string"
}
},
"required" : [ "artifactStates", "ticketTypes", "tier", "timeformat", "version" ],
"required" : [ "artifactKinds", "artifactStates", "ticketTypes", "tier", "timeformat", "version" ],
"type" : "object"
},
"Statistics" : {
@@ -6183,6 +6600,30 @@
},
"required" : [ "apikey", "blocked", "id", "roles" ],
"type" : "object"
},
"Widget" : {
"properties" : {
"aggregation" : {
"type" : "string"
},
"filter" : {
"type" : "string"
},
"name" : {
"type" : "string"
},
"type" : {
"enum" : [ "bar", "line", "pie" ],
"type" : "string"
},
"width" : {
"maximum" : 12,
"minimum" : 1,
"type" : "integer"
}
},
"required" : [ "aggregation", "name", "type", "width" ],
"type" : "object"
}
}
},

View File

@@ -8,6 +8,8 @@ definitions:
additionalProperties:
$ref: '#/definitions/Enrichment'
type: object
kind:
type: string
name:
example: 2.2.2.2
type: string
@@ -139,6 +141,33 @@ definitions:
ticket:
$ref: '#/definitions/TicketResponse'
type: object
Dashboard:
properties:
name:
type: string
widgets:
items:
$ref: '#/definitions/Widget'
type: array
required:
- name
- widgets
type: object
DashboardResponse:
properties:
id:
type: string
name:
type: string
widgets:
items:
$ref: '#/definitions/Widget'
type: array
required:
- id
- name
- widgets
type: object
Enrichment:
properties:
created:
@@ -382,6 +411,31 @@ definitions:
type: array
Settings:
properties:
artifactKinds:
items:
$ref: '#/definitions/Type'
title: Artifact Kinds
type: array
artifactStates:
items:
$ref: '#/definitions/Type'
title: Artifact States
type: array
timeformat:
title: Time Format
type: string
required:
- timeformat
- artifactKinds
- artifactStates
type: object
SettingsResponse:
properties:
artifactKinds:
items:
$ref: '#/definitions/Type'
title: Artifact Kinds
type: array
artifactStates:
items:
$ref: '#/definitions/Type'
@@ -414,6 +468,7 @@ definitions:
- tier
- timeformat
- ticketTypes
- artifactKinds
- artifactStates
type: object
Statistics:
@@ -1188,6 +1243,30 @@ definitions:
- roles
- apikey
type: object
Widget:
properties:
aggregation:
type: string
filter:
type: string
name:
type: string
type:
enum:
- bar
- line
- pie
type: string
width:
maximum: 12
minimum: 1
type: integer
required:
- name
- type
- aggregation
- width
type: object
host: .
info:
description: API for the catalyst incident response platform.
@@ -1429,10 +1508,12 @@ paths:
roles:
- admin:backup:read
- admin:backup:restore
- admin:dashboard:write
- admin:group:write
- admin:job:read
- admin:job:write
- admin:log:read
- admin:settings:write
- admin:ticket:delete
- admin:user:write
- admin:userdata:read
@@ -1441,6 +1522,7 @@ paths:
- analyst:currentsettings:write
- analyst:currentuser:read
- analyst:currentuserdata:read
- analyst:dashboard:read
- analyst:file
- analyst:group:read
- analyst:playbook:read
@@ -1511,6 +1593,183 @@ paths:
summary: Update current user data
tags:
- userdata
/dashboard/data:
get:
operationId: dashboardData
parameters:
- description: Aggregation
in: query
name: aggregation
required: true
type: string
x-example: type
- description: Filter
in: query
name: filter
type: string
x-example: status == "closed"
responses:
"200":
description: successful operation
examples:
test:
alert: 2
incident: 1
schema:
type: object
security:
- roles:
- dashboard:read
summary: Get widget data
tags:
- dashboards
/dashboards:
get:
operationId: listDashboards
responses:
"200":
description: successful operation
examples:
test:
- id: simple
name: Simple
widgets:
- aggregation: owner
filter: status == "open"
name: open_tickets_per_user
type: bar
width: 4
- aggregation: 'CONCAT(DATE_YEAR(created), "-", DATE_ISOWEEK(created)
< 10 ? "0" : "", DATE_ISOWEEK(created))'
name: tickets_per_week
type: line
width: 8
schema:
items:
$ref: '#/definitions/DashboardResponse'
type: array
security:
- roles:
- dashboard:read
summary: List dashboards
tags:
- dashboards
post:
operationId: createDashboard
parameters:
- description: New template
in: body
name: template
required: true
schema:
$ref: '#/definitions/Dashboard'
x-example:
name: My Dashboard
widgets: []
responses:
"200":
description: successful operation
examples:
test:
id: my-dashboard
name: My Dashboard
widgets: []
schema:
$ref: '#/definitions/DashboardResponse'
security:
- roles:
- dashboard:write
summary: Create a new dashboard
tags:
- dashboards
/dashboards/{id}:
delete:
operationId: deleteDashboard
parameters:
- description: Dashboard ID
in: path
name: id
required: true
type: string
x-example: simple
responses:
"204":
description: successful operation
security:
- roles:
- dashboard:write
summary: Delete a dashboard
tags:
- dashboards
get:
operationId: getDashboard
parameters:
- description: Dashboard ID
in: path
name: id
required: true
type: string
x-example: simple
responses:
"200":
description: successful operation
examples:
test:
id: simple
name: Simple
widgets:
- aggregation: owner
filter: status == "open"
name: open_tickets_per_user
type: bar
width: 4
- aggregation: 'CONCAT(DATE_YEAR(created), "-", DATE_ISOWEEK(created)
< 10 ? "0" : "", DATE_ISOWEEK(created))'
name: tickets_per_week
type: line
width: 8
schema:
$ref: '#/definitions/DashboardResponse'
security:
- roles:
- dashboard:read
summary: Get a single dashboard
tags:
- dashboards
put:
operationId: updateDashboard
parameters:
- description: Dashboard ID
in: path
name: id
required: true
type: string
x-example: simple
- description: Dashboard object that needs to be added
in: body
name: dashboard
required: true
schema:
$ref: '#/definitions/Dashboard'
x-example:
name: Simple
widgets: []
responses:
"200":
description: successful operation
examples:
test:
id: simple
name: Simple
widgets: []
schema:
$ref: '#/definitions/DashboardResponse'
security:
- roles:
- dashboard:write
summary: Update an existing dashboard
tags:
- dashboards
/jobs:
get:
operationId: listJobs
@@ -2137,6 +2396,13 @@ paths:
description: successful operation
examples:
test:
artifactKinds:
- icon: mdi-server
id: asset
name: Asset
- icon: mdi-bullseye
id: ioc
name: IOC
artifactStates:
- color: info
icon: mdi-help-circle-outline
@@ -2153,10 +2419,12 @@ paths:
roles:
- admin:backup:read
- admin:backup:restore
- admin:dashboard:write
- admin:group:write
- admin:job:read
- admin:job:write
- admin:log:read
- admin:settings:write
- admin:ticket:delete
- admin:user:write
- admin:userdata:read
@@ -2165,6 +2433,7 @@ paths:
- analyst:currentsettings:write
- analyst:currentuser:read
- analyst:currentuserdata:read
- analyst:dashboard:read
- analyst:file
- analyst:group:read
- analyst:playbook:read
@@ -2202,16 +2471,137 @@ paths:
id: hunt
name: Threat Hunting
tier: community
timeformat: YYYY-MM-DDThh:mm:ss
timeformat: yyyy-MM-dd hh:mm:ss
version: 0.0.0-test
schema:
$ref: '#/definitions/Settings'
$ref: '#/definitions/SettingsResponse'
security:
- roles:
- settings:read
summary: Get settings
tags:
- settings
post:
operationId: saveSettings
parameters:
- description: Save settings
in: body
name: settings
required: true
schema:
$ref: '#/definitions/Settings'
x-example:
artifactKinds:
- icon: mdi-server
id: asset
name: Asset
- icon: mdi-bullseye
id: ioc
name: IOC
artifactStates:
- color: info
icon: mdi-help-circle-outline
id: unknown
name: Unknown
- color: error
icon: mdi-skull
id: malicious
name: Malicious
- color: success
icon: mdi-check
id: clean
name: Clean
timeformat: yyyy-MM-dd hh:mm:ss
responses:
"200":
description: successful operation
examples:
test:
artifactKinds:
- icon: mdi-server
id: asset
name: Asset
- icon: mdi-bullseye
id: ioc
name: IOC
artifactStates:
- color: info
icon: mdi-help-circle-outline
id: unknown
name: Unknown
- color: error
icon: mdi-skull
id: malicious
name: Malicious
- color: success
icon: mdi-check
id: clean
name: Clean
roles:
- admin:backup:read
- admin:backup:restore
- admin:dashboard:write
- admin:group:write
- admin:job:read
- admin:job:write
- admin:log:read
- admin:settings:write
- admin:ticket:delete
- admin:user:write
- admin:userdata:read
- admin:userdata:write
- analyst:automation:read
- analyst:currentsettings:write
- analyst:currentuser:read
- analyst:currentuserdata:read
- analyst:dashboard:read
- analyst:file
- analyst:group:read
- analyst:playbook:read
- analyst:rule:read
- analyst:settings:read
- analyst:template:read
- analyst:ticket:read
- analyst:ticket:write
- analyst:tickettype:read
- analyst:user:read
- engineer:automation:write
- engineer:playbook:write
- engineer:rule:write
- engineer:template:write
- engineer:tickettype:write
ticketTypes:
- default_playbooks: []
default_template: default
icon: mdi-alert
id: alert
name: Alerts
- default_playbooks: []
default_template: default
icon: mdi-radioactive
id: incident
name: Incidents
- default_playbooks: []
default_template: default
icon: mdi-fingerprint
id: investigation
name: Forensic Investigations
- default_playbooks: []
default_template: default
icon: mdi-target
id: hunt
name: Threat Hunting
tier: community
timeformat: yyyy-MM-dd hh:mm:ss
version: 0.0.0-test
schema:
$ref: '#/definitions/SettingsResponse'
security:
- roles:
- settings:write
summary: Save settings
tags:
- settings
/statistics:
get:
operationId: getStatistics
@@ -6554,10 +6944,12 @@ paths:
roles:
- admin:backup:read
- admin:backup:restore
- admin:dashboard:write
- admin:group:write
- admin:job:read
- admin:job:write
- admin:log:read
- admin:settings:write
- admin:ticket:delete
- admin:user:write
- admin:userdata:read
@@ -6566,6 +6958,7 @@ paths:
- analyst:currentsettings:write
- analyst:currentuser:read
- analyst:currentuserdata:read
- analyst:dashboard:read
- analyst:file
- analyst:group:read
- analyst:playbook:read
@@ -6589,6 +6982,7 @@ paths:
- analyst:currentsettings:write
- analyst:currentuser:read
- analyst:currentuserdata:read
- analyst:dashboard:read
- analyst:file
- analyst:group:read
- analyst:playbook:read
@@ -6641,6 +7035,7 @@ paths:
- analyst:currentsettings:write
- analyst:currentuser:read
- analyst:currentuserdata:read
- analyst:dashboard:read
- analyst:file
- analyst:group:read
- analyst:playbook:read
@@ -6701,6 +7096,7 @@ paths:
- analyst:currentsettings:write
- analyst:currentuser:read
- analyst:currentuserdata:read
- analyst:dashboard:read
- analyst:file
- analyst:group:read
- analyst:playbook:read
@@ -6757,10 +7153,12 @@ paths:
roles:
- admin:backup:read
- admin:backup:restore
- admin:dashboard:write
- admin:group:write
- admin:job:read
- admin:job:write
- admin:log:read
- admin:settings:write
- admin:ticket:delete
- admin:user:write
- admin:userdata:read
@@ -6769,6 +7167,7 @@ paths:
- analyst:currentsettings:write
- analyst:currentuser:read
- analyst:currentuserdata:read
- analyst:dashboard:read
- analyst:file
- analyst:group:read
- analyst:playbook:read

View File

@@ -16,6 +16,8 @@ var (
CommentSchema = new(gojsonschema.Schema)
CommentFormSchema = new(gojsonschema.Schema)
ContextSchema = new(gojsonschema.Schema)
DashboardSchema = new(gojsonschema.Schema)
DashboardResponseSchema = new(gojsonschema.Schema)
EnrichmentSchema = new(gojsonschema.Schema)
EnrichmentFormSchema = new(gojsonschema.Schema)
FileSchema = new(gojsonschema.Schema)
@@ -35,6 +37,7 @@ var (
ReferenceSchema = new(gojsonschema.Schema)
ReferenceArraySchema = new(gojsonschema.Schema)
SettingsSchema = new(gojsonschema.Schema)
SettingsResponseSchema = new(gojsonschema.Schema)
StatisticsSchema = new(gojsonschema.Schema)
TaskSchema = new(gojsonschema.Schema)
TaskOriginSchema = new(gojsonschema.Schema)
@@ -59,11 +62,12 @@ var (
UserDataResponseSchema = new(gojsonschema.Schema)
UserFormSchema = new(gojsonschema.Schema)
UserResponseSchema = new(gojsonschema.Schema)
WidgetSchema = new(gojsonschema.Schema)
)
func init() {
err := schemaLoader.AddSchemas(
gojsonschema.NewStringLoader(`{"type":"object","properties":{"enrichments":{"type":"object","additionalProperties":{"$ref":"#/definitions/Enrichment"}},"name":{"type":"string"},"status":{"type":"string"},"type":{"type":"string"}},"required":["name"],"$id":"#/definitions/Artifact"}`),
gojsonschema.NewStringLoader(`{"type":"object","properties":{"enrichments":{"type":"object","additionalProperties":{"$ref":"#/definitions/Enrichment"}},"kind":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"type":{"type":"string"}},"required":["name"],"$id":"#/definitions/Artifact"}`),
gojsonschema.NewStringLoader(`{"type":"object","properties":{"artifact":{"type":"string"},"ticket_id":{"format":"int64","type":"integer"}},"required":["ticket_id","artifact"],"$id":"#/definitions/ArtifactOrigin"}`),
gojsonschema.NewStringLoader(`{"type":"object","properties":{"image":{"type":"string"},"schema":{"type":"string"},"script":{"type":"string"},"type":{"items":{"type":"string","enum":["artifact","playbook","global"]},"type":"array"}},"required":["image","script","type"],"$id":"#/definitions/Automation"}`),
gojsonschema.NewStringLoader(`{"type":"object","properties":{"id":{"type":"string"},"image":{"type":"string"},"schema":{"type":"string"},"script":{"type":"string"},"type":{"items":{"type":"string","enum":["artifact","playbook","global"]},"type":"array"}},"required":["id","image","script","type"],"$id":"#/definitions/AutomationForm"}`),
@@ -71,6 +75,8 @@ func init() {
gojsonschema.NewStringLoader(`{"type":"object","properties":{"created":{"format":"date-time","type":"string"},"creator":{"type":"string"},"message":{"type":"string"}},"required":["creator","created","message"],"$id":"#/definitions/Comment"}`),
gojsonschema.NewStringLoader(`{"type":"object","properties":{"created":{"format":"date-time","type":"string"},"creator":{"type":"string"},"message":{"type":"string"}},"required":["message"],"$id":"#/definitions/CommentForm"}`),
gojsonschema.NewStringLoader(`{"type":"object","properties":{"artifact":{"$ref":"#/definitions/Artifact"},"playbook":{"$ref":"#/definitions/PlaybookResponse"},"task":{"$ref":"#/definitions/TaskResponse"},"ticket":{"$ref":"#/definitions/TicketResponse"}},"$id":"#/definitions/Context"}`),
gojsonschema.NewStringLoader(`{"type":"object","properties":{"name":{"type":"string"},"widgets":{"items":{"$ref":"#/definitions/Widget"},"type":"array"}},"required":["name","widgets"],"$id":"#/definitions/Dashboard"}`),
gojsonschema.NewStringLoader(`{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"widgets":{"items":{"$ref":"#/definitions/Widget"},"type":"array"}},"required":["id","name","widgets"],"$id":"#/definitions/DashboardResponse"}`),
gojsonschema.NewStringLoader(`{"type":"object","properties":{"created":{"format":"date-time","type":"string"},"data":{"type":"object"},"name":{"type":"string"}},"required":["name","data","created"],"$id":"#/definitions/Enrichment"}`),
gojsonschema.NewStringLoader(`{"type":"object","properties":{"data":{"type":"object"},"name":{"type":"string"}},"required":["name","data"],"$id":"#/definitions/EnrichmentForm"}`),
gojsonschema.NewStringLoader(`{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"}},"required":["key","name"],"$id":"#/definitions/File"}`),
@@ -89,7 +95,8 @@ func init() {
gojsonschema.NewStringLoader(`{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"yaml":{"type":"string"}},"required":["id","name","yaml"],"$id":"#/definitions/PlaybookTemplateResponse"}`),
gojsonschema.NewStringLoader(`{"type":"object","properties":{"href":{"type":"string"},"name":{"type":"string"}},"required":["name","href"],"$id":"#/definitions/Reference"}`),
gojsonschema.NewStringLoader(`{"items":{"$ref":"#/definitions/Reference"},"type":"array","$id":"#/definitions/ReferenceArray"}`),
gojsonschema.NewStringLoader(`{"type":"object","properties":{"artifactStates":{"title":"Artifact States","items":{"$ref":"#/definitions/Type"},"type":"array"},"roles":{"title":"Roles","items":{"type":"string"},"type":"array"},"ticketTypes":{"title":"Ticket Types","items":{"$ref":"#/definitions/TicketTypeResponse"},"type":"array"},"tier":{"title":"Tier","type":"string","enum":["community","enterprise"]},"timeformat":{"title":"Time Format","type":"string"},"version":{"title":"Version","type":"string"}},"required":["version","tier","timeformat","ticketTypes","artifactStates"],"$id":"#/definitions/Settings"}`),
gojsonschema.NewStringLoader(`{"type":"object","properties":{"artifactKinds":{"title":"Artifact Kinds","items":{"$ref":"#/definitions/Type"},"type":"array"},"artifactStates":{"title":"Artifact States","items":{"$ref":"#/definitions/Type"},"type":"array"},"timeformat":{"title":"Time Format","type":"string"}},"required":["timeformat","artifactKinds","artifactStates"],"$id":"#/definitions/Settings"}`),
gojsonschema.NewStringLoader(`{"type":"object","properties":{"artifactKinds":{"title":"Artifact Kinds","items":{"$ref":"#/definitions/Type"},"type":"array"},"artifactStates":{"title":"Artifact States","items":{"$ref":"#/definitions/Type"},"type":"array"},"roles":{"title":"Roles","items":{"type":"string"},"type":"array"},"ticketTypes":{"title":"Ticket Types","items":{"$ref":"#/definitions/TicketTypeResponse"},"type":"array"},"tier":{"title":"Tier","type":"string","enum":["community","enterprise"]},"timeformat":{"title":"Time Format","type":"string"},"version":{"title":"Version","type":"string"}},"required":["version","tier","timeformat","ticketTypes","artifactKinds","artifactStates"],"$id":"#/definitions/SettingsResponse"}`),
gojsonschema.NewStringLoader(`{"type":"object","properties":{"open_tickets_per_user":{"type":"object","additionalProperties":{"type":"integer"}},"tickets_per_type":{"type":"object","additionalProperties":{"type":"integer"}},"tickets_per_week":{"type":"object","additionalProperties":{"type":"integer"}},"unassigned":{"type":"integer"}},"required":["unassigned","open_tickets_per_user","tickets_per_week","tickets_per_type"],"$id":"#/definitions/Statistics"}`),
gojsonschema.NewStringLoader(`{"type":"object","properties":{"automation":{"type":"string"},"closed":{"format":"date-time","type":"string"},"created":{"format":"date-time","type":"string"},"data":{"type":"object"},"done":{"type":"boolean"},"join":{"type":"boolean"},"name":{"type":"string"},"next":{"type":"object","additionalProperties":{"type":"string"}},"owner":{"type":"string"},"payload":{"type":"object","additionalProperties":{"type":"string"}},"schema":{"type":"object"},"type":{"type":"string","enum":["task","input","automation"]}},"required":["name","type","done","created"],"$id":"#/definitions/Task"}`),
gojsonschema.NewStringLoader(`{"type":"object","properties":{"playbook_id":{"type":"string"},"task_id":{"type":"string"},"ticket_id":{"format":"int64","type":"integer"}},"required":["ticket_id","playbook_id","task_id"],"$id":"#/definitions/TaskOrigin"}`),
@@ -114,6 +121,7 @@ func init() {
gojsonschema.NewStringLoader(`{"type":"object","properties":{"email":{"type":"string"},"id":{"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"}},"required":["id"],"$id":"#/definitions/UserDataResponse"}`),
gojsonschema.NewStringLoader(`{"type":"object","properties":{"apikey":{"type":"boolean"},"blocked":{"type":"boolean"},"id":{"type":"string"},"roles":{"items":{"type":"string"},"type":"array"}},"required":["id","blocked","roles","apikey"],"$id":"#/definitions/UserForm"}`),
gojsonschema.NewStringLoader(`{"type":"object","properties":{"apikey":{"type":"boolean"},"blocked":{"type":"boolean"},"id":{"type":"string"},"roles":{"items":{"type":"string"},"type":"array"}},"required":["id","blocked","roles","apikey"],"$id":"#/definitions/UserResponse"}`),
gojsonschema.NewStringLoader(`{"type":"object","properties":{"aggregation":{"type":"string"},"filter":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["bar","line","pie"]},"width":{"maximum":12,"type":"integer"}},"required":["name","type","aggregation","width"],"$id":"#/definitions/Widget"}`),
)
if err != nil {
panic(err)
@@ -127,6 +135,8 @@ func init() {
CommentSchema = mustCompile(`#/definitions/Comment`)
CommentFormSchema = mustCompile(`#/definitions/CommentForm`)
ContextSchema = mustCompile(`#/definitions/Context`)
DashboardSchema = mustCompile(`#/definitions/Dashboard`)
DashboardResponseSchema = mustCompile(`#/definitions/DashboardResponse`)
EnrichmentSchema = mustCompile(`#/definitions/Enrichment`)
EnrichmentFormSchema = mustCompile(`#/definitions/EnrichmentForm`)
FileSchema = mustCompile(`#/definitions/File`)
@@ -146,6 +156,7 @@ func init() {
ReferenceSchema = mustCompile(`#/definitions/Reference`)
ReferenceArraySchema = mustCompile(`#/definitions/ReferenceArray`)
SettingsSchema = mustCompile(`#/definitions/Settings`)
SettingsResponseSchema = mustCompile(`#/definitions/SettingsResponse`)
StatisticsSchema = mustCompile(`#/definitions/Statistics`)
TaskSchema = mustCompile(`#/definitions/Task`)
TaskOriginSchema = mustCompile(`#/definitions/TaskOrigin`)
@@ -170,10 +181,12 @@ func init() {
UserDataResponseSchema = mustCompile(`#/definitions/UserDataResponse`)
UserFormSchema = mustCompile(`#/definitions/UserForm`)
UserResponseSchema = mustCompile(`#/definitions/UserResponse`)
WidgetSchema = mustCompile(`#/definitions/Widget`)
}
type Artifact struct {
Enrichments map[string]*Enrichment `json:"enrichments,omitempty"`
Kind *string `json:"kind,omitempty"`
Name string `json:"name"`
Status *string `json:"status,omitempty"`
Type *string `json:"type,omitempty"`
@@ -226,6 +239,17 @@ type Context struct {
Ticket *TicketResponse `json:"ticket,omitempty"`
}
type Dashboard struct {
Name string `json:"name"`
Widgets []*Widget `json:"widgets"`
}
type DashboardResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Widgets []*Widget `json:"widgets"`
}
type Enrichment struct {
Created time.Time `json:"created"`
Data map[string]interface{} `json:"data"`
@@ -338,6 +362,13 @@ type Reference struct {
type ReferenceArray []*Reference
type Settings struct {
ArtifactKinds []*Type `json:"artifactKinds"`
ArtifactStates []*Type `json:"artifactStates"`
Timeformat string `json:"timeformat"`
}
type SettingsResponse struct {
ArtifactKinds []*Type `json:"artifactKinds"`
ArtifactStates []*Type `json:"artifactStates"`
Roles []string `json:"roles,omitempty"`
TicketTypes []*TicketTypeResponse `json:"ticketTypes"`
@@ -589,6 +620,14 @@ type UserResponse struct {
Roles []string `json:"roles"`
}
type Widget struct {
Aggregation string `json:"aggregation"`
Filter *string `json:"filter,omitempty"`
Name string `json:"name"`
Type string `json:"type"`
Width int `json:"width"`
}
func mustCompile(uri string) *gojsonschema.Schema {
s, err := schemaLoader.Compile(gojsonschema.NewReferenceLoader(uri))
if err != nil {
@@ -598,9 +637,9 @@ func mustCompile(uri string) *gojsonschema.Schema {
}
const (
SettingsTierCommunity = "community"
SettingsResponseTierCommunity = "community"
SettingsTierEnterprise = "enterprise"
SettingsResponseTierEnterprise = "enterprise"
TaskTypeTask = "task"
@@ -621,4 +660,10 @@ const (
TypeColorSuccess = "success"
TypeColorWarning = "warning"
WidgetTypeBar = "bar"
WidgetTypeLine = "line"
WidgetTypePie = "pie"
)

View File

@@ -78,3 +78,7 @@ func (i *Index) Truncate() error {
i.internal = index
return nil
}
func (i *Index) Close() error {
return i.internal.Close()
}

View File

@@ -16,9 +16,10 @@ const (
Admin string = "admin"
AutomationRead Role = "analyst:automation:read"
CurrentuserRead Role = "analyst:currentuser:read"
CurrentuserdataRead Role = "analyst:currentuserdata:read"
CurrentuserdataWrite Role = "analyst:currentsettings:write"
CurrentuserRead Role = "analyst:currentuser:read"
DashboardRead Role = "analyst:dashboard:read"
FileReadWrite Role = "analyst:file"
GroupRead Role = "analyst:group:read"
PlaybookRead Role = "analyst:playbook:read"
@@ -26,8 +27,8 @@ const (
SettingsRead Role = "analyst:settings:read"
TemplateRead Role = "analyst:template:read"
TicketRead Role = "analyst:ticket:read"
TickettypeRead Role = "analyst:tickettype:read"
TicketWrite Role = "analyst:ticket:write"
TickettypeRead Role = "analyst:tickettype:read"
UserRead Role = "analyst:user:read"
AutomationWrite Role = "engineer:automation:write"
@@ -36,16 +37,18 @@ const (
TemplateWrite Role = "engineer:template:write"
TickettypeWrite Role = "engineer:tickettype:write"
BackupRead Role = "admin:backup:read"
BackupRestore Role = "admin:backup:restore"
GroupWrite Role = "admin:group:write"
JobWrite Role = "admin:job:write"
JobRead Role = "admin:job:read"
LogRead Role = "admin:log:read"
UserdataRead Role = "admin:userdata:read"
UserdataWrite Role = "admin:userdata:write"
TicketDelete Role = "admin:ticket:delete"
UserWrite Role = "admin:user:write"
BackupRead Role = "admin:backup:read"
BackupRestore Role = "admin:backup:restore"
DashboardWrite Role = "admin:dashboard:write"
GroupWrite Role = "admin:group:write"
JobRead Role = "admin:job:read"
JobWrite Role = "admin:job:write"
LogRead Role = "admin:log:read"
SettingsWrite Role = "admin:settings:write"
TicketDelete Role = "admin:ticket:delete"
UserWrite Role = "admin:user:write"
UserdataRead Role = "admin:userdata:read"
UserdataWrite Role = "admin:userdata:write"
)
func (p Role) String() string {
@@ -145,6 +148,7 @@ func List() []Role {
TicketWrite, UserRead, AutomationWrite, PlaybookWrite, RuleWrite,
TemplateWrite, TickettypeWrite, BackupRead, BackupRestore, GroupWrite,
LogRead, UserdataWrite, TicketDelete, UserWrite, JobRead, JobWrite,
SettingsWrite, DashboardRead, DashboardWrite,
}
}

View File

@@ -3,7 +3,6 @@ package catalyst
import (
"context"
"io/fs"
"log"
"net/http"
"time"
@@ -31,7 +30,6 @@ type Config struct {
Storage *storage.Config
Bus *bus.Config
UISettings *model.Settings
Secret []byte
Auth *AuthConfig
ExternalAddress string
@@ -82,7 +80,7 @@ func New(hooks *hooks.Hooks, config *Config) (*Server, error) {
return nil, err
}
catalystService, err := service.New(catalystBus, catalystDatabase, catalystStorage, config.UISettings)
catalystService, err := service.New(catalystBus, catalystDatabase, catalystStorage, GetVersion())
if err != nil {
return nil, err
}
@@ -117,35 +115,33 @@ func New(hooks *hooks.Hooks, config *Config) (*Server, error) {
}
func setupAPI(catalystService *service.Service, catalystStorage *storage.Storage, catalystDatabase *database.Database, dbConfig *database.Config, bus *bus.Bus, config *Config) (chi.Router, error) {
// create server
allowAll := cors.AllowAll().Handler
apiServer := api.NewServer(
catalystService,
AuthorizeRole,
allowAll, Authenticate(catalystDatabase, config.Auth), AuthorizeBlockedUser(),
)
middlewares := []func(next http.Handler) http.Handler{Authenticate(catalystDatabase, config.Auth), AuthorizeBlockedUser()}
apiServer.With(AuthorizeRole([]string{role.FileReadWrite.String()})).Head("/files/{ticketID}/tusd/{id}", tusdUpload(catalystDatabase, bus, catalystStorage.S3(), config.ExternalAddress))
apiServer.With(AuthorizeRole([]string{role.FileReadWrite.String()})).Patch("/files/{ticketID}/tusd/{id}", tusdUpload(catalystDatabase, bus, catalystStorage.S3(), config.ExternalAddress))
apiServer.With(AuthorizeRole([]string{role.FileReadWrite.String()})).Post("/files/{ticketID}/tusd", tusdUpload(catalystDatabase, bus, catalystStorage.S3(), config.ExternalAddress))
apiServer.With(AuthorizeRole([]string{role.FileReadWrite.String()})).Post("/files/{ticketID}/upload", upload(catalystDatabase, catalystStorage.S3(), catalystStorage.Uploader()))
apiServer.With(AuthorizeRole([]string{role.FileReadWrite.String()})).Get("/files/{ticketID}/download/{key}", download(catalystStorage.Downloader()))
// create server
apiServerMiddleware := []func(next http.Handler) http.Handler{cors.AllowAll().Handler}
apiServerMiddleware = append(apiServerMiddleware, middlewares...)
apiServer := api.NewServer(catalystService, AuthorizeRole, apiServerMiddleware...)
fileReadWrite := AuthorizeRole([]string{role.FileReadWrite.String()})
tudHandler := tusdUpload(catalystDatabase, bus, catalystStorage.S3(), config.ExternalAddress)
apiServer.With(fileReadWrite).Head("/files/{ticketID}/tusd/{id}", tudHandler)
apiServer.With(fileReadWrite).Patch("/files/{ticketID}/tusd/{id}", tudHandler)
apiServer.With(fileReadWrite).Post("/files/{ticketID}/tusd", tudHandler)
apiServer.With(fileReadWrite).Post("/files/{ticketID}/upload", upload(catalystDatabase, catalystStorage.S3(), catalystStorage.Uploader()))
apiServer.With(fileReadWrite).Get("/files/{ticketID}/download/{key}", download(catalystStorage.Downloader()))
apiServer.With(AuthorizeRole([]string{role.BackupRead.String()})).Get("/backup/create", backupHandler(catalystStorage, dbConfig))
apiServer.With(AuthorizeRole([]string{role.BackupRestore.String()})).Post("/backup/restore", restoreHandler(catalystStorage, catalystDatabase, dbConfig))
server := chi.NewRouter()
server.Use(middleware.RequestID, middleware.RealIP, middleware.Logger, middleware.Recoverer, allowAll)
server.Use(middleware.RequestID, middleware.RealIP, middleware.Logger, middleware.Recoverer, cors.AllowAll().Handler)
server.Mount("/api", apiServer)
server.Get("/callback", callback(config.Auth))
server.With(Authenticate(catalystDatabase, config.Auth), AuthorizeBlockedUser()).Handle("/wss", handleWebSocket(bus))
server.With(middlewares...).Handle("/wss", handleWebSocket(bus))
fsys, _ := fs.Sub(ui.UI, "dist")
server.NotFound(func(w http.ResponseWriter, r *http.Request) {
log.Println("not found", r.URL.RawPath)
Authenticate(catalystDatabase, config.Auth)(AuthorizeBlockedUser()(http.HandlerFunc(api.Static(fsys)))).ServeHTTP(w, r)
})
server.With(middlewares...).NotFound(api.VueStatic(fsys))
return server, nil
}

49
service/dashboard.go Normal file
View File

@@ -0,0 +1,49 @@
package service
import (
"context"
"fmt"
"github.com/arangodb/go-driver"
"github.com/SecurityBrewery/catalyst/database"
"github.com/SecurityBrewery/catalyst/generated/model"
)
func dashboardResponseID(doc *model.DashboardResponse) []driver.DocumentID {
if doc == nil {
return nil
}
return templateID(doc.ID)
}
func dashboardID(id string) []driver.DocumentID {
return []driver.DocumentID{driver.DocumentID(fmt.Sprintf("%s/%s", database.DashboardCollectionName, id))}
}
func (s *Service) ListDashboards(ctx context.Context) ([]*model.DashboardResponse, error) {
return s.database.DashboardList(ctx)
}
func (s *Service) CreateDashboard(ctx context.Context, dashboard *model.Dashboard) (doc *model.DashboardResponse, err error) {
defer s.publishRequest(ctx, err, "CreateDashboard", dashboardResponseID(doc))
return s.database.DashboardCreate(ctx, dashboard)
}
func (s *Service) GetDashboard(ctx context.Context, id string) (*model.DashboardResponse, error) {
return s.database.DashboardGet(ctx, id)
}
func (s *Service) UpdateDashboard(ctx context.Context, id string, form *model.Dashboard) (doc *model.DashboardResponse, err error) {
defer s.publishRequest(ctx, err, "UpdateDashboard", dashboardResponseID(doc))
return s.database.DashboardUpdate(ctx, id, form)
}
func (s *Service) DeleteDashboard(ctx context.Context, id string) (err error) {
defer s.publishRequest(ctx, err, "DeleteDashboard", dashboardID(id))
return s.database.DashboardDelete(ctx, id)
}
func (s *Service) DashboardData(ctx context.Context, aggregation string, filter *string) (map[string]interface{}, error) {
return s.database.WidgetData(ctx, aggregation, filter)
}

View File

@@ -8,19 +8,18 @@ import (
"github.com/SecurityBrewery/catalyst/bus"
"github.com/SecurityBrewery/catalyst/database"
"github.com/SecurityBrewery/catalyst/database/busdb"
"github.com/SecurityBrewery/catalyst/generated/model"
"github.com/SecurityBrewery/catalyst/storage"
)
type Service struct {
bus *bus.Bus
database *database.Database
settings *model.Settings
storage *storage.Storage
version string
}
func New(bus *bus.Bus, database *database.Database, storage *storage.Storage, settings *model.Settings) (*Service, error) {
return &Service{database: database, bus: bus, settings: settings, storage: storage}, nil
func New(bus *bus.Bus, database *database.Database, storage *storage.Storage, version string) (*Service, error) {
return &Service{database: database, bus: bus, storage: storage, version: version}, nil
}
func (s *Service) publishRequest(ctx context.Context, err error, function string, ids []driver.DocumentID) {

62
service/settings.go Normal file
View File

@@ -0,0 +1,62 @@
package service
import (
"context"
"errors"
"sort"
"github.com/SecurityBrewery/catalyst/database/busdb"
"github.com/SecurityBrewery/catalyst/generated/model"
"github.com/SecurityBrewery/catalyst/role"
)
func (s *Service) GetSettings(ctx context.Context) (*model.SettingsResponse, error) {
globalSettings, err := s.database.Settings(ctx)
if err != nil {
return nil, err
}
return s.settings(ctx, globalSettings)
}
func (s *Service) SaveSettings(ctx context.Context, settings *model.Settings) (*model.SettingsResponse, error) {
globalSettings, err := s.database.SaveSettings(ctx, settings)
if err != nil {
return nil, err
}
return s.settings(ctx, globalSettings)
}
func (s *Service) settings(ctx context.Context, globalSettings *model.Settings) (*model.SettingsResponse, error) {
user, ok := busdb.UserFromContext(ctx)
if !ok {
return nil, errors.New("no user in context")
}
userData, err := s.database.UserDataGet(ctx, user.ID)
if err != nil {
return nil, err
}
ticketTypeList, err := s.database.TicketTypeList(ctx)
if err != nil {
return nil, err
}
if userData.Timeformat != nil {
globalSettings.Timeformat = *userData.Timeformat
}
roles := role.Strings(role.List())
sort.Strings(roles)
return &model.SettingsResponse{
Tier: model.SettingsResponseTierCommunity,
Version: s.version,
Roles: roles,
TicketTypes: ticketTypeList,
ArtifactStates: globalSettings.ArtifactStates,
ArtifactKinds: globalSettings.ArtifactKinds,
Timeformat: globalSettings.Timeformat,
}, nil
}

View File

@@ -1,45 +0,0 @@
package service
import (
"context"
"errors"
"sort"
"github.com/SecurityBrewery/catalyst/database/busdb"
"github.com/SecurityBrewery/catalyst/generated/model"
"github.com/SecurityBrewery/catalyst/role"
)
func (s *Service) GetSettings(ctx context.Context) (*model.Settings, error) {
user, ok := busdb.UserFromContext(ctx)
if !ok {
return nil, errors.New("no user in context")
}
setting, err := s.database.UserDataGet(ctx, user.ID)
if err != nil {
return nil, err
}
settings := mergeSettings(s.settings, setting)
ticketTypeList, err := s.database.TicketTypeList(ctx)
if err != nil {
return nil, err
}
settings.TicketTypes = ticketTypeList
return settings, nil
}
func mergeSettings(globalSettings *model.Settings, user *model.UserDataResponse) *model.Settings {
if user.Timeformat != nil {
globalSettings.Timeformat = *user.Timeformat
}
roles := role.Strings(role.List())
sort.Strings(roles)
globalSettings.Roles = roles
return globalSettings
}

View File

@@ -78,6 +78,27 @@ func SetupTestData(ctx context.Context, db *database.Database) error {
return err
}
if _, err := db.DashboardCreate(ctx, &model.Dashboard{
Name: "Simple",
Widgets: []*model.Widget{
{
Name: "open_tickets_per_user",
Type: model.WidgetTypeBar,
Aggregation: "owner",
Filter: pointer.String(`status == "open"`),
Width: 4,
},
{
Name: "tickets_per_week",
Type: model.WidgetTypeLine,
Aggregation: `CONCAT(DATE_YEAR(created), "-", DATE_ISOWEEK(created) < 10 ? "0" : "", DATE_ISOWEEK(created))`,
Width: 8,
},
},
}); err != nil {
return err
}
return nil
}

View File

@@ -20,7 +20,6 @@ import (
"github.com/SecurityBrewery/catalyst/database/busdb"
"github.com/SecurityBrewery/catalyst/generated/api"
"github.com/SecurityBrewery/catalyst/generated/model"
"github.com/SecurityBrewery/catalyst/generated/pointer"
"github.com/SecurityBrewery/catalyst/hooks"
"github.com/SecurityBrewery/catalyst/index"
"github.com/SecurityBrewery/catalyst/service"
@@ -51,22 +50,6 @@ func Config(ctx context.Context) (*catalyst.Config, error) {
Key: "A9RysEsPJni8RaHeg_K0FKXQNfBrUyw-",
APIUrl: "http://localhost:8002/api",
},
UISettings: &model.Settings{
ArtifactStates: []*model.Type{
{Icon: "mdi-help-circle-outline", ID: "unknown", Name: "Unknown", Color: pointer.String(model.TypeColorInfo)},
{Icon: "mdi-skull", ID: "malicious", Name: "Malicious", Color: pointer.String(model.TypeColorError)},
{Icon: "mdi-check", ID: "clean", Name: "Clean", Color: pointer.String(model.TypeColorSuccess)},
},
TicketTypes: []*model.TicketTypeResponse{
{ID: "alert", Icon: "mdi-alert", Name: "Alerts"},
{ID: "incident", Icon: "mdi-radioactive", Name: "Incidents"},
{ID: "investigation", Icon: "mdi-fingerprint", Name: "Forensic Investigations"},
{ID: "hunt", Icon: "mdi-target", Name: "Threat Hunting"},
},
Version: "0.0.0-test",
Tier: model.SettingsTierCommunity,
Timeformat: "YYYY-MM-DDThh:mm:ss",
},
Secret: []byte("4ef5b29539b70233dd40c02a1799d25079595565e05a193b09da2c3e60ada1cd"),
Auth: &catalyst.AuthConfig{
OIDCIssuer: "http://localhost:9002/auth/realms/catalyst",
@@ -101,7 +84,7 @@ func Index(t *testing.T) (*index.Index, func(), error) {
if err != nil {
return nil, nil, err
}
return catalystIndex, func() { os.RemoveAll(dir) }, nil
return catalystIndex, func() { catalystIndex.Close(); os.RemoveAll(dir) }, nil
}
func Bus(t *testing.T) (context.Context, *catalyst.Config, *bus.Bus, error) {
@@ -168,7 +151,7 @@ func Service(t *testing.T) (context.Context, *catalyst.Config, *bus.Bus, *index.
t.Fatal(err)
}
catalystService, err := service.New(rbus, db, catalystStorage, config.UISettings)
catalystService, err := service.New(rbus, db, catalystStorage, "0.0.0-test")
if err != nil {
t.Fatal(err)
}

View File

@@ -4,7 +4,6 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="/flask.png?v=1">
<title>Catalyst</title>
</head>
<body>
@@ -12,8 +11,5 @@
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script>let global = globalThis;</script>
<script type="module" src="./src/main.ts"></script>
<!-- built files will be auto injected -->
</body>
</html>

View File

@@ -2,9 +2,9 @@
<v-app class="background">
<v-navigation-drawer dark permanent :mini-variant="mini" :expand-on-hover="mini" app color="statusbar">
<v-list>
<v-list-item class="px-2" :to="{ name: 'Dashboard' }">
<v-list-item class="px-2" :to="{ name: 'Home' }">
<v-list-item-avatar rounded="0">
<v-img src="/flask_white.svg" :width="40"></v-img>
<v-img src="/static/flask_white.svg" :width="40"></v-img>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title class="title">
@@ -132,9 +132,7 @@
</v-btn>
</v-app-bar>
<div>
<router-view></router-view>
</div>
<router-view></router-view>
<v-snackbar v-model="snackbar" :color="$store.state.alert.type" :timeout="$store.state.alert.type === 'error' ? -1 : 5000" outlined>
<b style="display: block">{{ $store.state.alert.name | capitalize }}</b>
{{ $store.state.alert.detail }}
@@ -164,6 +162,7 @@ export default Vue.extend({
{ icon: "mdi-account-group", name: "Groups", to: "GroupList", role: "admin:group:write", tier: "enterprise" },
{ icon: "mdi-cogs", name: "User Data", to: "UserDataList", role: "admin:userdata:write" },
{ icon: "mdi-format-list-checks", name: "Jobs", to: "JobList", role: "admin:job:write" },
{ icon: "mdi-cog", name: "Settings", to: "Settings", role: "admin:settings:write" },
],
mini: true,
goto: "",
@@ -181,6 +180,7 @@ export default Vue.extend({
},
internal: function (): Array<any> {
return [
{ icon: "mdi-view-dashboard", name: "Dashboards", to: "DashboardList", role: "analyst:dashboard:read" },
{ icon: "mdi-check-bold", name: "Open Tasks", to: "TaskList", count: this.$store.state.task_count },
]
},
@@ -188,6 +188,8 @@ export default Vue.extend({
return this.$store.state.showAlert
},
crumbs: function() {
this.$route.name
let pathArray = this.$route.path.split("/")
pathArray.shift()

View File

@@ -33,6 +33,12 @@ export interface Artifact {
* @memberof Artifact
*/
'enrichments'?: { [key: string]: Enrichment; };
/**
*
* @type {string}
* @memberof Artifact
*/
'kind'?: string;
/**
*
* @type {string}
@@ -284,6 +290,50 @@ export interface Context {
*/
'ticket'?: TicketResponse;
}
/**
*
* @export
* @interface Dashboard
*/
export interface Dashboard {
/**
*
* @type {string}
* @memberof Dashboard
*/
'name': string;
/**
*
* @type {Array<Widget>}
* @memberof Dashboard
*/
'widgets': Array<Widget>;
}
/**
*
* @export
* @interface DashboardResponse
*/
export interface DashboardResponse {
/**
*
* @type {string}
* @memberof DashboardResponse
*/
'id': string;
/**
*
* @type {string}
* @memberof DashboardResponse
*/
'name': string;
/**
*
* @type {Array<Widget>}
* @memberof DashboardResponse
*/
'widgets': Array<Widget>;
}
/**
*
* @export
@@ -976,50 +1026,81 @@ export interface RuleResponse {
* @interface Settings
*/
export interface Settings {
/**
*
* @type {Array<Type>}
* @memberof Settings
*/
'artifactKinds': Array<Type>;
/**
*
* @type {Array<Type>}
* @memberof Settings
*/
'artifactStates': Array<Type>;
/**
*
* @type {Array<string>}
* @memberof Settings
*/
'roles'?: Array<string>;
/**
*
* @type {Array<TicketTypeResponse>}
* @memberof Settings
*/
'ticketTypes': Array<TicketTypeResponse>;
/**
*
* @type {string}
* @memberof Settings
*/
'tier': SettingsTierEnum;
/**
*
* @type {string}
* @memberof Settings
*/
'timeformat': string;
}
/**
*
* @export
* @interface SettingsResponse
*/
export interface SettingsResponse {
/**
*
* @type {Array<Type>}
* @memberof SettingsResponse
*/
'artifactKinds': Array<Type>;
/**
*
* @type {Array<Type>}
* @memberof SettingsResponse
*/
'artifactStates': Array<Type>;
/**
*
* @type {Array<string>}
* @memberof SettingsResponse
*/
'roles'?: Array<string>;
/**
*
* @type {Array<TicketTypeResponse>}
* @memberof SettingsResponse
*/
'ticketTypes': Array<TicketTypeResponse>;
/**
*
* @type {string}
* @memberof Settings
* @memberof SettingsResponse
*/
'tier': SettingsResponseTierEnum;
/**
*
* @type {string}
* @memberof SettingsResponse
*/
'timeformat': string;
/**
*
* @type {string}
* @memberof SettingsResponse
*/
'version': string;
}
export const SettingsTierEnum = {
export const SettingsResponseTierEnum = {
Community: 'community',
Enterprise: 'enterprise'
} as const;
export type SettingsTierEnum = typeof SettingsTierEnum[keyof typeof SettingsTierEnum];
export type SettingsResponseTierEnum = typeof SettingsResponseTierEnum[keyof typeof SettingsResponseTierEnum];
/**
*
@@ -2242,6 +2323,52 @@ export interface UserResponse {
*/
'roles': Array<string>;
}
/**
*
* @export
* @interface Widget
*/
export interface Widget {
/**
*
* @type {string}
* @memberof Widget
*/
'aggregation': string;
/**
*
* @type {string}
* @memberof Widget
*/
'filter'?: string;
/**
*
* @type {string}
* @memberof Widget
*/
'name': string;
/**
*
* @type {string}
* @memberof Widget
*/
'type': WidgetTypeEnum;
/**
*
* @type {number}
* @memberof Widget
*/
'width': number;
}
export const WidgetTypeEnum = {
Bar: 'bar',
Line: 'line',
Pie: 'pie'
} as const;
export type WidgetTypeEnum = typeof WidgetTypeEnum[keyof typeof WidgetTypeEnum];
/**
* AutomationsApi - axios parameter creator
@@ -2620,6 +2747,461 @@ export class AutomationsApi extends BaseAPI {
}
/**
* DashboardsApi - axios parameter creator
* @export
*/
export const DashboardsApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @summary Create a new dashboard
* @param {Dashboard} template New template
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createDashboard: async (template: Dashboard, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'template' is not null or undefined
assertParamExists('createDashboard', 'template', template)
const localVarPath = `/dashboards`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(template, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary Get widget data
* @param {string} aggregation Aggregation
* @param {string} [filter] Filter
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
dashboardData: async (aggregation: string, filter?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'aggregation' is not null or undefined
assertParamExists('dashboardData', 'aggregation', aggregation)
const localVarPath = `/dashboard/data`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
if (aggregation !== undefined) {
localVarQueryParameter['aggregation'] = aggregation;
}
if (filter !== undefined) {
localVarQueryParameter['filter'] = filter;
}
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary Delete a dashboard
* @param {string} id Dashboard ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteDashboard: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('deleteDashboard', 'id', id)
const localVarPath = `/dashboards/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary Get a single dashboard
* @param {string} id Dashboard ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getDashboard: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('getDashboard', 'id', id)
const localVarPath = `/dashboards/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary List dashboards
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listDashboards: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/dashboards`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary Update an existing dashboard
* @param {string} id Dashboard ID
* @param {Dashboard} dashboard Dashboard object that needs to be added
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateDashboard: async (id: string, dashboard: Dashboard, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('updateDashboard', 'id', id)
// verify required parameter 'dashboard' is not null or undefined
assertParamExists('updateDashboard', 'dashboard', dashboard)
const localVarPath = `/dashboards/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(dashboard, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
}
};
/**
* DashboardsApi - functional programming interface
* @export
*/
export const DashboardsApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = DashboardsApiAxiosParamCreator(configuration)
return {
/**
*
* @summary Create a new dashboard
* @param {Dashboard} template New template
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async createDashboard(template: Dashboard, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DashboardResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createDashboard(template, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary Get widget data
* @param {string} aggregation Aggregation
* @param {string} [filter] Filter
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async dashboardData(aggregation: string, filter?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.dashboardData(aggregation, filter, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary Delete a dashboard
* @param {string} id Dashboard ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async deleteDashboard(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteDashboard(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary Get a single dashboard
* @param {string} id Dashboard ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getDashboard(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DashboardResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getDashboard(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary List dashboards
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listDashboards(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<DashboardResponse>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listDashboards(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary Update an existing dashboard
* @param {string} id Dashboard ID
* @param {Dashboard} dashboard Dashboard object that needs to be added
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async updateDashboard(id: string, dashboard: Dashboard, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DashboardResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateDashboard(id, dashboard, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};
/**
* DashboardsApi - factory interface
* @export
*/
export const DashboardsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = DashboardsApiFp(configuration)
return {
/**
*
* @summary Create a new dashboard
* @param {Dashboard} template New template
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createDashboard(template: Dashboard, options?: any): AxiosPromise<DashboardResponse> {
return localVarFp.createDashboard(template, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Get widget data
* @param {string} aggregation Aggregation
* @param {string} [filter] Filter
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
dashboardData(aggregation: string, filter?: string, options?: any): AxiosPromise<object> {
return localVarFp.dashboardData(aggregation, filter, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Delete a dashboard
* @param {string} id Dashboard ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteDashboard(id: string, options?: any): AxiosPromise<void> {
return localVarFp.deleteDashboard(id, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Get a single dashboard
* @param {string} id Dashboard ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getDashboard(id: string, options?: any): AxiosPromise<DashboardResponse> {
return localVarFp.getDashboard(id, options).then((request) => request(axios, basePath));
},
/**
*
* @summary List dashboards
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listDashboards(options?: any): AxiosPromise<Array<DashboardResponse>> {
return localVarFp.listDashboards(options).then((request) => request(axios, basePath));
},
/**
*
* @summary Update an existing dashboard
* @param {string} id Dashboard ID
* @param {Dashboard} dashboard Dashboard object that needs to be added
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateDashboard(id: string, dashboard: Dashboard, options?: any): AxiosPromise<DashboardResponse> {
return localVarFp.updateDashboard(id, dashboard, options).then((request) => request(axios, basePath));
},
};
};
/**
* DashboardsApi - object-oriented interface
* @export
* @class DashboardsApi
* @extends {BaseAPI}
*/
export class DashboardsApi extends BaseAPI {
/**
*
* @summary Create a new dashboard
* @param {Dashboard} template New template
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DashboardsApi
*/
public createDashboard(template: Dashboard, options?: AxiosRequestConfig) {
return DashboardsApiFp(this.configuration).createDashboard(template, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Get widget data
* @param {string} aggregation Aggregation
* @param {string} [filter] Filter
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DashboardsApi
*/
public dashboardData(aggregation: string, filter?: string, options?: AxiosRequestConfig) {
return DashboardsApiFp(this.configuration).dashboardData(aggregation, filter, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Delete a dashboard
* @param {string} id Dashboard ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DashboardsApi
*/
public deleteDashboard(id: string, options?: AxiosRequestConfig) {
return DashboardsApiFp(this.configuration).deleteDashboard(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Get a single dashboard
* @param {string} id Dashboard ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DashboardsApi
*/
public getDashboard(id: string, options?: AxiosRequestConfig) {
return DashboardsApiFp(this.configuration).getDashboard(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary List dashboards
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DashboardsApi
*/
public listDashboards(options?: AxiosRequestConfig) {
return DashboardsApiFp(this.configuration).listDashboards(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Update an existing dashboard
* @param {string} id Dashboard ID
* @param {Dashboard} dashboard Dashboard object that needs to be added
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DashboardsApi
*/
public updateDashboard(id: string, dashboard: Dashboard, options?: AxiosRequestConfig) {
return DashboardsApiFp(this.configuration).updateDashboard(id, dashboard, options).then((request) => request(this.axios, this.basePath));
}
}
/**
* GraphApi - axios parameter creator
* @export
@@ -4319,6 +4901,42 @@ export const SettingsApiAxiosParamCreator = function (configuration?: Configurat
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary Save settings
* @param {Settings} settings Save settings
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
saveSettings: async (settings: Settings, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'settings' is not null or undefined
assertParamExists('saveSettings', 'settings', settings)
const localVarPath = `/settings`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(settings, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
@@ -4340,10 +4958,21 @@ export const SettingsApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getSettings(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Settings>> {
async getSettings(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SettingsResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getSettings(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary Save settings
* @param {Settings} settings Save settings
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async saveSettings(settings: Settings, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SettingsResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.saveSettings(settings, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};
@@ -4360,9 +4989,19 @@ export const SettingsApiFactory = function (configuration?: Configuration, baseP
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getSettings(options?: any): AxiosPromise<Settings> {
getSettings(options?: any): AxiosPromise<SettingsResponse> {
return localVarFp.getSettings(options).then((request) => request(axios, basePath));
},
/**
*
* @summary Save settings
* @param {Settings} settings Save settings
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
saveSettings(settings: Settings, options?: any): AxiosPromise<SettingsResponse> {
return localVarFp.saveSettings(settings, options).then((request) => request(axios, basePath));
},
};
};
@@ -4383,6 +5022,18 @@ export class SettingsApi extends BaseAPI {
public getSettings(options?: AxiosRequestConfig) {
return SettingsApiFp(this.configuration).getSettings(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Save settings
* @param {Settings} settings Save settings
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SettingsApi
*/
public saveSettings(settings: Settings, options?: AxiosRequestConfig) {
return SettingsApiFp(this.configuration).saveSettings(settings, options).then((request) => request(this.axios, this.basePath));
}
}

View File

@@ -9,6 +9,9 @@
<v-icon small class="mr-1" :color="statusColor">{{ statusIcon }}</v-icon>
<span :class="statusColor + '--text'">{{ artifact.status | capitalize }}</span>
<v-icon small class="mx-1" :color="kindColor">{{ kindIcon }}</v-icon>
<span :class="kindColor + '--text'">{{ artifact.kind | capitalize }}</span>
<v-spacer></v-spacer>
<v-icon small class="mr-1">mdi-information</v-icon>
<span class="mr-1">{{ artifact.enrichments ? lodash.size(artifact.enrichments) : 0 }}</span>
@@ -47,6 +50,24 @@ export default Vue.extend({
}
})
return color;
},
kindIcon: function () {
let icon = "mdi-help";
this.lodash.forEach(this.$store.state.settings.artifactKinds, (state: Type) => {
if (this.artifact.kind === state.id) {
icon = state.icon;
}
})
return icon;
},
kindColor: function () {
let color = TypeColorEnum.Info as TypeColorEnum;
this.lodash.forEach(this.$store.state.settings.artifactKinds, (state: Type) => {
if (this.artifact.kind === state.id && state.color) {
color = state.color;
}
})
return color;
}
},
methods: {

View File

@@ -68,19 +68,26 @@ const v = new Vue({
}).$mount("#app");
axios.interceptors.response.use(
response => response,
// response => response,
response => {
lodash.unset(response.data, 'notoast');
return Promise.resolve(response);
},
error => {
console.log(error)
if (error.response.data && 'title' in error.response.data && 'detail' in error.response.data) {
const problem = error.response.data as Problem;
v.$store.dispatch("alertError", { name: problem.title, detail: problem.detail });
return Promise.reject(error);
if (!lodash.has(error.response.data, 'notoast')) {
if (error.response.data && 'title' in error.response.data && 'detail' in error.response.data) {
const problem = error.response.data as Problem;
v.$store.dispatch("alertError", { name: problem.title, detail: problem.detail });
return Promise.reject(error);
}
if (error.response.data && 'error' in error.response.data) {
v.$store.dispatch("alertError", { name: "Error", detail: error.response.data.error });
return Promise.reject(error);
}
v.$store.dispatch("alertError", { name: "Error", detail: JSON.stringify(error.response.data) });
}
if (error.response.data && 'error' in error.response.data) {
v.$store.dispatch("alertError", { name: "Error", detail: error.response.data.error });
return Promise.reject(error);
}
v.$store.dispatch("alertError", { name: "Error", detail: JSON.stringify(error.response.data) });
return Promise.reject(error);
}
);

View File

@@ -15,17 +15,21 @@ import Rule from "../views/Rule.vue";
import RuleList from "../views/RuleList.vue";
import Template from "../views/Template.vue";
import TemplateList from "../views/TemplateList.vue";
import Dashboard from "../views/Dashboard.vue";
import DashboardList from "../views/DashboardList.vue";
import API from "../views/API.vue";
import User from '../views/User.vue';
import UserList from "@/views/UserList.vue";
import Job from '../views/Job.vue';
import JobList from "@/views/JobList.vue";
import GroupList from "@/views/GroupList.vue";
import Dashboard from "@/views/Dashboard.vue";
import Home from "@/views/Home.vue";
import Group from "@/views/Group.vue";
import TicketType from '../views/TicketType.vue';
import TicketTypeList from "@/views/TicketTypeList.vue";
import TaskList from "@/views/TaskList.vue";
import Settings from "@/views/Settings.vue";
import NotFound from "@/views/NotFound.vue";
Vue.use(VueRouter);
@@ -54,14 +58,14 @@ const routes: Array<RouteConfig> = [
{
path: "/",
name: "Catalyst",
redirect: { name: "Dashboard" },
redirect: { name: "Home" },
},
{
path: "/dashboard",
name: "Dashboard",
component: Dashboard,
meta: { title: "Dashboard" },
path: "/home",
name: "Home",
component: Home,
meta: { title: "Home" },
},
{
@@ -226,6 +230,27 @@ const routes: Array<RouteConfig> = [
]
},
{
path: "/dashboards",
name: "DashboardList",
component: DashboardList,
meta: { title: "Dashboards" },
children: [
{
path: ":id",
name: "Dashboard",
component: Dashboard,
},
]
},
{
path: "/settings",
name: "Settings",
component: Settings,
meta: { title: "Settings" },
},
{
path: "/apidocs",
name: "API",
@@ -239,6 +264,13 @@ const routes: Array<RouteConfig> = [
component: Graph,
meta: { title: "Graph" },
},
{
path: '*',
name: "Not Found",
component: NotFound,
meta: { title: "Not Found" },
}
];
const router = new VueRouter({

View File

@@ -29,7 +29,7 @@ import {
SettingsApi,
SettingsApiFactory,
JobsApi,
JobsApiFactory,
JobsApiFactory, DashboardsApiFactory, DashboardsApi,
} from "@/client";
const config = new Configuration({
@@ -56,7 +56,8 @@ export const API: TicketsApi &
SettingsApi &
TickettypesApi &
JobsApi &
TasksApi = Object.assign(
TasksApi &
DashboardsApi = Object.assign(
{},
TicketsApiFactory(config),
PlaybooksApiFactory(config),
@@ -74,5 +75,6 @@ export const API: TicketsApi &
TickettypesApiFactory(config),
TasksApiFactory(config),
SettingsApiFactory(config),
JobsApiFactory(config)
JobsApiFactory(config),
DashboardsApiFactory(config)
);

View File

@@ -1,7 +1,7 @@
import Vue from "vue";
import Vuex, {ActionContext} from "vuex";
import {API} from "@/services/api";
import {UserData, TicketList, Settings, UserResponse} from "@/client";
import {UserData, TicketList, UserResponse, SettingsResponse} from "@/client";
import {AxiosResponse} from "axios";
import {Alert} from "@/types/types";
import {templateStore} from "./modules/templates";
@@ -19,7 +19,7 @@ export default new Vuex.Store({
counts: {} as Record<string, number>,
task_count: 0 as number,
settings: {} as Settings,
settings: {} as SettingsResponse,
userdata: {} as UserData,
alert: {} as Alert,
@@ -46,7 +46,7 @@ export default new Vuex.Store({
setUserData (state, msg: UserData) {
state.userdata = msg
},
setSettings (state, msg: Settings) {
setSettings (state, msg: SettingsResponse) {
state.settings = msg
},
setAlert (state, msg: Alert) {
@@ -68,7 +68,7 @@ export default new Vuex.Store({
})
},
getSettings (context: ActionContext<any, any>) {
API.getSettings().then((response: AxiosResponse<Settings>) => {
API.getSettings().then((response: AxiosResponse<SettingsResponse>) => {
context.commit("setSettings", response.data);
context.dispatch("fetchCount");
})

View File

@@ -22,6 +22,23 @@
</v-list>
</v-menu>
&middot;
Kind:
<v-menu offset-y class="mr-2">
<template v-slot:activator="{ on, attrs }">
<span v-bind="attrs" v-on="on">
<v-icon small class="mr-1" :color="kindColor(artifact.kind)">{{ kindIcon(artifact.kind) }}</v-icon>
<span :class="kindColor(artifact.kind) + '--text'">{{ artifact.kind | capitalize }}</span>
</span>
</template>
<v-list>
<v-list-item dense link v-for="state in otherKinds" :key="state.id" @click="setKind(state.id)">
<v-list-item-title>
Set kind to <v-icon small>{{ kindIcon(state.id) }}</v-icon> {{ state.name }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
&middot;
Type:
<v-menu
:close-on-content-click="false"
@@ -148,6 +165,14 @@ export default Vue.extend({
return state.id !== this.artifact.status;
})
},
otherKinds: function (): Array<Type> {
return this.lodash.filter(this.$store.state.settings.artifactKinds, (state: Type) => {
if (!this.artifact || !this.artifact.status) {
return true;
}
return state.id !== this.artifact.status;
})
},
},
methods: {
setArtifactType() {
@@ -229,6 +254,42 @@ export default Vue.extend({
}
});
},
kindIcon: function (kind: string): string {
let icon = "mdi-help";
this.lodash.forEach(this.$store.state.settings.artifactKinds, (state: Type) => {
if (kind === state.id) {
icon = state.icon;
}
})
return icon;
},
kindColor: function (kind: string) {
let color = TypeColorEnum.Info as TypeColorEnum;
this.lodash.forEach(this.$store.state.settings.artifactKinds, (state: Type) => {
if (kind === state.id && state.color) {
color = state.color
}
})
return color;
},
setKind(kind: string) {
if (!this.artifact || !this.artifact.name || this.ticketID === undefined) {
return;
}
let artifact = this.artifact
artifact.kind = kind
API.setArtifact(this.ticketID, this.artifact.name, artifact).then((response) => {
this.$store.dispatch("alertSuccess", { name: "Artifact kind changed", type: "success" })
if (response.data.artifacts) {
this.lodash.forEach(response.data.artifacts, (artifact) => {
if (artifact.name == this.name) {
this.artifact = artifact;
}
})
}
});
},
load() {
this.loadArtifact(this.ticketID, this.name);
}

View File

@@ -1,110 +1,123 @@
<template>
<v-main>
<div v-if="dashboard">
<h2 class="d-flex">
<span v-if="!editmode">{{ dashboard.name }}</span>
<v-text-field v-else v-model="dashboard.name" outlined dense class="mb-0" hide-details></v-text-field>
<v-spacer></v-spacer>
<v-btn v-if="editmode" small outlined @click="addWidget" class="mr-1">
<v-icon>mdi-plus</v-icon>
Add Widget
</v-btn>
<v-btn v-if="editmode" small outlined @click="save" class="mr-1">
<v-icon>mdi-content-save</v-icon>
Save
</v-btn>
<v-btn v-if="editmode && $route.params.id !== 'new'" small outlined @click="cancel">
<v-icon>mdi-cancel</v-icon>
Cancel
</v-btn>
<v-btn v-if="!editmode" small outlined @click="edit">
<v-icon>mdi-pencil</v-icon>
Edit
</v-btn>
</h2>
<v-row>
<v-col v-if="statistics" cols="12" lg="7">
<v-row>
<v-col cols="4">
<v-subheader>Unassigned tickets</v-subheader>
<span style="font-size: 60pt; text-align: center; display: block">
<router-link :to="{
name: 'TicketList',
params: { query: 'status == \'open\' AND !owner' }
}">
{{ statistics.unassigned }}
</router-link>
</span>
<v-subheader>Your tickets</v-subheader>
<span style="font-size: 60pt; text-align: center; display: block">
<router-link :to="{
name: 'TicketList',
params: { query: 'status == \'open\' AND owner == \'' + $store.state.user.id + '\'' }
}">
{{ $store.state.user.id in statistics.open_tickets_per_user ? statistics.open_tickets_per_user[$store.state.user.id] : 0 }}
</router-link>
</span>
</v-col>
<v-col cols="8">
<v-subheader>Open tickets per owner</v-subheader>
<bar-chart
v-if="open_tickets_per_user"
:chart-data="open_tickets_per_user"
:styles="{
width: '100%',
'max-height': '400px',
position: 'relative'
}"
:chart-options="{
responsive: true,
maintainAspectRatio: false,
legend: undefined,
scales: { xAxes: [ { ticks: { beginAtZero: true, precision: 0 } } ] },
onClick: clickUser,
hover: {
onHover: function(e) {
var point = this.getElementAtEvent(e);
if (point.length) e.target.style.cursor = 'pointer';
else e.target.style.cursor = 'default';
}
}
}"
></bar-chart>
</v-col>
</v-row>
<v-row>
<v-col cols="7">
<v-subheader>Tickets created per week</v-subheader>
<v-col v-for="(widget, index) in dashboard.widgets" :key="index" :cols="widget.width">
<v-card class="mb-2">
<v-card-title>
<span v-if="!editmode">{{ widget.name }}</span>
<v-text-field v-else outlined dense hide-details v-model="widget.name" class="mr-1"></v-text-field>
<v-btn v-if="editmode" outlined @click="removeWidget(index)">
<v-icon>mdi-close</v-icon>
Remove
</v-btn>
</v-card-title>
<v-card-text v-if="editmode">
<v-row>
<v-col cols="8">
<v-select label="Type" v-model="widget.type" :items="['line', 'bar', 'pie']"></v-select>
</v-col>
<v-col cols="4">
<v-text-field label="Width" type="number" v-model="widget.width"></v-text-field>
</v-col>
</v-row>
<v-text-field label="Aggregation" v-model="widget.aggregation"></v-text-field>
<v-text-field label="Filter" v-model="widget.filter" clearable></v-text-field>
</v-card-text>
<v-card-text v-if="data[index] === null">
{{ widgetErrors[index] }}
</v-card-text>
<div v-else>
<line-chart
v-if="tickets_per_week"
:chart-data="tickets_per_week"
:styles="{ width: '100%', position: 'relative' }"
:chart-options="{
responsive: true,
maintainAspectRatio: false,
legend: undefined,
scales: { yAxes: [ { ticks: { beginAtZero: true, precision: 0 } } ] }
}"
v-if="widget.type === 'line' && data[index]"
:chart-data="data[index]"
:styles="{ width: '100%', position: 'relative' }"
:chart-options="{
responsive: true,
maintainAspectRatio: false,
legend: false,
scales: { yAxes: [ { ticks: { beginAtZero: true, precision: 0 } } ] }
}"
>
</line-chart>
</v-col>
<v-col cols="5">
<v-subheader>Ticket Types</v-subheader>
<pie-chart
v-if="tickets_per_type"
:chart-data="tickets_per_type"
:styles="{ width: '100%', position: 'relative' }"
:chart-options="{
onClick: clickPie,
hover: {
onHover: function(e) {
var point = this.getElementAtEvent(e);
if (point.length) e.target.style.cursor = 'pointer';
else e.target.style.cursor = 'default';
}
}
}"
v-if="widget.type === 'pie' && data[index]"
:chart-data="data[index]"
:styles="{ width: '100%', position: 'relative' }"
:chart-options="{
responsive: true,
maintainAspectRatio: false,
}"
>
</pie-chart>
</v-col>
</v-row>
</v-col>
<v-col cols="12" lg="5">
<TicketList :type="this.$route.params.type" @click="open"></TicketList>
<bar-chart
v-if="widget.type === 'bar' && data[index]"
:chart-data="data[index]"
:styles="{
width: '100%',
'max-height': '400px',
position: 'relative'
}"
:chart-options="{
responsive: true,
maintainAspectRatio: false,
legend: false,
scales: { xAxes: [ { ticks: { beginAtZero: true, precision: 0 } } ] },
}"
></bar-chart>
</div>
</v-card>
</v-col>
</v-row>
</v-main>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import {DashboardResponse, Widget} from "@/client";
import { API } from "@/services/api";
import {createHash} from "crypto";
import {colors} from "@/plugins/vuetify";
import LineChart from "../components/charts/Line";
import BarChart from "../components/charts/Bar";
import PieChart from "../components/charts/Doughnut";
import { API } from "@/services/api";
import {Statistics, TicketResponse} from "@/client";
import {DateTime} from "luxon";
import { colors } from "@/plugins/vuetify";
import TicketList from "@/components/TicketList.vue";
import { createHash } from "crypto";
import {ChartData} from "chart.js";
import {AxiosError, AxiosTransformer} from "axios";
interface State {
dashboard?: DashboardResponse;
undodashboard?: DashboardResponse;
data: Record<string, any>;
editmode: boolean;
widgetErrors: Record<number, string>;
}
export default Vue.extend({
name: "Dashboard",
@@ -112,108 +125,130 @@ export default Vue.extend({
LineChart,
BarChart,
PieChart,
TicketList
},
data() {
return {
statistics: (undefined as unknown) as Statistics
};
},
computed: {
tickets_per_type: function () {
let data = { labels: [] as Array<string>, datasets: [{ backgroundColor: [] as Array<string>, data: [] as Array<number> }] }
this.lodash.forEach(this.statistics.tickets_per_type, (count, type) => {
data.labels.push(type);
data.datasets[0].data.push(count);
data.datasets[0].backgroundColor.push(this.color(type));
})
return data
data: (): State => ({
dashboard: undefined,
undodashboard: undefined,
data: {},
editmode: false,
widgetErrors: {},
}),
watch: {
$route: function () {
this.loadDashboard();
},
open_tickets_per_user: function () {
let data = { labels: [] as Array<string>, datasets: [{ backgroundColor: [] as Array<string>, data: [] as Array<number> }] }
this.lodash.forEach(this.statistics.open_tickets_per_user, (count, user) => {
if (!user) {
data.labels.push("unassigned");
} else {
data.labels.push(user);
}
data.datasets[0].data.push(count);
data.datasets[0].backgroundColor.push(this.color(user));
})
return data
},
tickets_per_week: function () {
let data = {labels: [] as Array<string>, datasets: [{backgroundColor: [] as Array<string>, data: [] as Array<number> }]}
this.lodash.forEach(this.weeks(), (week) => {
data.labels.push(week);
if (week in this.statistics.tickets_per_week) {
data.datasets[0].data.push(this.statistics.tickets_per_week[week]);
} else {
data.datasets[0].data.push(0);
}
data.datasets[0].backgroundColor.push("#607d8b");
})
return data
}
},
methods: {
open: function (ticket: TicketResponse) {
if (ticket.id === undefined) {
return;
edit: function () {
this.undodashboard = this.lodash.cloneDeep(this.dashboard);
this.editmode = true;
},
save: function () {
if (!this.dashboard) {
return
}
this.$router.push({
name: "Ticket",
params: {type: '-', id: ticket.id.toString()}
});
},
clickUser: function (evt, elem) {
let owner = this.open_tickets_per_user.labels[elem[0]._index];
let query = 'status == \'open\' AND owner == \'' + owner + '\'';
let widgets = [] as Array<Widget>;
this.lodash.forEach(this.dashboard.widgets, (widget) => {
widget.width = this.lodash.toInteger(widget.width);
if (!widget.filter) {
this.lodash.unset(widget, "filter")
}
widgets.push(widget);
})
this.dashboard.widgets = widgets;
if (owner == 'unassigned') {
query = 'status == \'open\' AND !owner';
if (this.$route.params.id === 'new') {
API.createDashboard(this.dashboard).then((response) => {
this.loadWidgetData(response.data.widgets);
this.dashboard = response.data;
this.editmode = false;
this.$router.push({ name: "Dashboard", params: { id: response.data.id }})
})
} else {
API.updateDashboard(this.dashboard.id, this.dashboard).then((response) => {
this.loadWidgetData(response.data.widgets);
this.dashboard = response.data;
this.editmode = false;
})
}
},
cancel: function () {
this.dashboard = this.lodash.cloneDeep(this.undodashboard);
this.editmode = false;
},
addWidget: function () {
if (!this.dashboard) {
return
}
this.$router.push({
name: "TicketList",
params: {query: query}
});
this.dashboard.widgets.push({name: "new widget", width: 6, aggregation: "", type: "line"})
},
clickPie: function (evt, elem) {
this.$router.push({
name: "TicketList",
params: {type: this.tickets_per_type.labels[elem[0]._index]}
});
removeWidget: function (id: number) {
if (!this.dashboard) {
return
}
console.log(id);
let widgets = this.lodash.cloneDeep(this.dashboard.widgets);
this.lodash.pullAt(widgets, [id]);
Vue.set(this.dashboard, "widgets", widgets);
},
loadDashboard: function () {
if (this.$route.params.id === 'new') {
this.dashboard = {
name: "New dashboard",
widgets: [{name: "new widget", width: 6, aggregation: "", type: "line"}],
} as DashboardResponse
this.editmode = true;
} else {
API.getDashboard(this.$route.params.id).then((response) => {
this.loadWidgetData(response.data.widgets);
this.dashboard = response.data;
});
}
},
loadWidgetData: function (widgets: Array<Widget>) {
this.lodash.forEach(widgets, (widget: Widget, index: number) => {
let widgetErrors = {};
let defaultTransformers = this.axios.defaults.transformResponse as AxiosTransformer[]
let transformResponse = defaultTransformers.concat((data) => {
data.notoast = true;
return data
});
API.dashboardData(widget.aggregation, widget.filter, {transformResponse: transformResponse}).then((response) => {
let d = { labels: [], datasets: [{data: [], backgroundColor: []}] } as ChartData;
this.lodash.forEach(response.data, (v: any, k: string) => {
// @ts-expect-error T2532
d.labels.push(k)
// @ts-expect-error T2532
d.datasets[0].data.push(v)
if (widget.type !== 'line') {
// @ts-expect-error T2532
d.datasets[0].backgroundColor.push(this.color(this.lodash.toString(v)));
}
})
Vue.set(this.data, index, d);
}).catch((err: AxiosError) => {
widgetErrors[index] = this.lodash.toString(err.response?.data.error);
Vue.set(this.data, index, null);
})
Vue.set(this, 'widgetErrors', widgetErrors);
})
},
color: function (s: string): string {
let pos = createHash('md5').update(s).digest().readUInt32BE(0) % colors.length;
return colors[pos];
},
fillData() {
API.getStatistics().then(response => {
this.statistics = response.data;
});
},
weeks: function () {
let w = [] as Array<string>;
for (let i = 0; i < 53; i++) {
w.push(DateTime.utc().minus({ weeks: i }).toFormat("kkkk-WW"))
}
this.lodash.reverse(w);
return w
}
},
mounted() {
this.fillData();
this.loadDashboard();
}
});
</script>
<style>
canvas {
position: relative !important;
margin: 0 auto;
}
</style>

View File

@@ -0,0 +1,64 @@
<template>
<v-main style="min-height: 100vh;">
<List
:items="dashboards"
routername="Dashboard"
itemid="id"
itemname="name"
singular="Dashboard"
plural="Dashboards"
writepermission="admin:dashboard:write"
@delete="deleteDashboard"
></List>
</v-main>
</template>
<script lang="ts">
import Vue from "vue";
import {Dashboard} from "@/client";
import {API} from "@/services/api";
import List from "../components/List.vue";
interface State {
dashboards: Array<Dashboard>;
}
export default Vue.extend({
name: "DashboardList",
components: {List},
data: (): State => ({
dashboards: [],
}),
methods: {
loadDashboards() {
API.listDashboards().then((response) => {
this.dashboards = response.data;
});
},
deleteDashboard(id: string) {
API.deleteDashboard(id).then(() => {
this.loadDashboards();
})
}
},
mounted() {
this.loadDashboards();
this.$store.subscribeAction((action, state) => {
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
return
}
let reload = false;
Vue.lodash.forEach(action.payload["ids"], (id) => {
if (this.lodash.startsWith(id, "dashboard/")) {
reload = true;
}
});
if (reload) {
this.loadDashboards()
}
})
},
});
</script>

219
ui/src/views/Home.vue Normal file
View File

@@ -0,0 +1,219 @@
<template>
<v-main>
<v-row>
<v-col v-if="statistics" cols="12" lg="7">
<v-row>
<v-col cols="4">
<v-subheader>Unassigned tickets</v-subheader>
<span style="font-size: 60pt; text-align: center; display: block">
<router-link :to="{
name: 'TicketList',
params: { query: 'status == \'open\' AND !owner' }
}">
{{ statistics.unassigned }}
</router-link>
</span>
<v-subheader>Your tickets</v-subheader>
<span style="font-size: 60pt; text-align: center; display: block">
<router-link :to="{
name: 'TicketList',
params: { query: 'status == \'open\' AND owner == \'' + $store.state.user.id + '\'' }
}">
{{ $store.state.user.id in statistics.open_tickets_per_user ? statistics.open_tickets_per_user[$store.state.user.id] : 0 }}
</router-link>
</span>
</v-col>
<v-col cols="8">
<v-subheader>Open tickets per owner</v-subheader>
<bar-chart
v-if="open_tickets_per_user"
:chart-data="open_tickets_per_user"
:styles="{
width: '100%',
'max-height': '400px',
position: 'relative'
}"
:chart-options="{
responsive: true,
maintainAspectRatio: false,
legend: undefined,
scales: { xAxes: [ { ticks: { beginAtZero: true, precision: 0 } } ] },
onClick: clickUser,
hover: {
onHover: function(e) {
var point = this.getElementAtEvent(e);
if (point.length) e.target.style.cursor = 'pointer';
else e.target.style.cursor = 'default';
}
}
}"
></bar-chart>
</v-col>
</v-row>
<v-row>
<v-col cols="7">
<v-subheader>Tickets created per week</v-subheader>
<line-chart
v-if="tickets_per_week"
:chart-data="tickets_per_week"
:styles="{ width: '100%', position: 'relative' }"
:chart-options="{
responsive: true,
maintainAspectRatio: false,
legend: undefined,
scales: { yAxes: [ { ticks: { beginAtZero: true, precision: 0 } } ] }
}"
>
</line-chart>
</v-col>
<v-col cols="5">
<v-subheader>Ticket Types</v-subheader>
<pie-chart
v-if="tickets_per_type"
:chart-data="tickets_per_type"
:styles="{ width: '100%', position: 'relative' }"
:chart-options="{
onClick: clickPie,
hover: {
onHover: function(e) {
var point = this.getElementAtEvent(e);
if (point.length) e.target.style.cursor = 'pointer';
else e.target.style.cursor = 'default';
}
}
}"
>
</pie-chart>
</v-col>
</v-row>
</v-col>
<v-col cols="12" lg="5">
<TicketList :type="this.$route.params.type" @click="open"></TicketList>
</v-col>
</v-row>
</v-main>
</template>
<script lang="ts">
import Vue from "vue";
import LineChart from "../components/charts/Line";
import BarChart from "../components/charts/Bar";
import PieChart from "../components/charts/Doughnut";
import { API } from "@/services/api";
import {Statistics, TicketResponse} from "@/client";
import {DateTime} from "luxon";
import { colors } from "@/plugins/vuetify";
import TicketList from "@/components/TicketList.vue";
import { createHash } from "crypto";
export default Vue.extend({
name: "Home",
components: {
LineChart,
BarChart,
PieChart,
TicketList
},
data() {
return {
statistics: (undefined as unknown) as Statistics
};
},
computed: {
tickets_per_type: function () {
let data = { labels: [] as Array<string>, datasets: [{ backgroundColor: [] as Array<string>, data: [] as Array<number> }] }
this.lodash.forEach(this.statistics.tickets_per_type, (count, type) => {
data.labels.push(type);
data.datasets[0].data.push(count);
data.datasets[0].backgroundColor.push(this.color(type));
})
return data
},
open_tickets_per_user: function () {
let data = { labels: [] as Array<string>, datasets: [{ backgroundColor: [] as Array<string>, data: [] as Array<number> }] }
this.lodash.forEach(this.statistics.open_tickets_per_user, (count, user) => {
if (!user) {
data.labels.push("unassigned");
} else {
data.labels.push(user);
}
data.datasets[0].data.push(count);
data.datasets[0].backgroundColor.push(this.color(user));
})
return data
},
tickets_per_week: function () {
let data = {labels: [] as Array<string>, datasets: [{backgroundColor: [] as Array<string>, data: [] as Array<number> }]}
this.lodash.forEach(this.weeks(), (week) => {
data.labels.push(week);
if (week in this.statistics.tickets_per_week) {
data.datasets[0].data.push(this.statistics.tickets_per_week[week]);
} else {
data.datasets[0].data.push(0);
}
data.datasets[0].backgroundColor.push("#607d8b");
})
return data
}
},
methods: {
open: function (ticket: TicketResponse) {
if (ticket.id === undefined) {
return;
}
this.$router.push({
name: "Ticket",
params: {type: '-', id: ticket.id.toString()}
});
},
clickUser: function (evt, elem) {
let owner = this.open_tickets_per_user.labels[elem[0]._index];
let query = 'status == \'open\' AND owner == \'' + owner + '\'';
if (owner == 'unassigned') {
query = 'status == \'open\' AND !owner';
}
this.$router.push({
name: "TicketList",
params: {query: query}
});
},
clickPie: function (evt, elem) {
this.$router.push({
name: "TicketList",
params: {type: this.tickets_per_type.labels[elem[0]._index]}
});
},
color: function (s: string): string {
let pos = createHash('md5').update(s).digest().readUInt32BE(0) % colors.length;
return colors[pos];
},
fillData() {
API.getStatistics().then(response => {
this.statistics = response.data;
});
},
weeks: function () {
let w = [] as Array<string>;
for (let i = 0; i < 53; i++) {
w.push(DateTime.utc().minus({ weeks: i }).toFormat("kkkk-WW"))
}
this.lodash.reverse(w);
return w
}
},
mounted() {
this.fillData();
}
});
</script>
<style>
canvas {
position: relative !important;
margin: 0 auto;
}
</style>

13
ui/src/views/NotFound.vue Normal file
View File

@@ -0,0 +1,13 @@
<template>
<v-main>
<div class="fill-height d-flex flex-row align-center justify-center">
<h1>Page not found :(</h1>
</div>
</v-main>
</template>
<script>
export default {
name: "NotFound"
}
</script>

177
ui/src/views/Settings.vue Normal file
View File

@@ -0,0 +1,177 @@
<template>
<v-main class="ma-4">
<div v-if="settings !== undefined">
<v-text-field label="Time Format" v-model="settings.timeformat"></v-text-field>
<v-subheader class="mx-0 px-0">Artifact States</v-subheader>
<v-card v-for="state in settings.artifactStates" :key="state.id" class="d-flex mb-2">
<v-row class="px-4 pt-2" dense>
<v-col>
<v-text-field label="ID" v-model="state.id"></v-text-field>
</v-col>
<v-col>
<v-text-field label="Name" v-model="state.name"></v-text-field>
</v-col>
<v-col>
<v-text-field label="Icon" v-model="state.icon"></v-text-field>
</v-col>
<v-col>
<v-select label="Color" v-model="state.color" :items="['info', 'error', 'success', 'warning']" clearable></v-select>
</v-col>
</v-row>
<v-btn icon class="mt-6 mr-4" @click="removeState(state.id)"><v-icon>mdi-close</v-icon></v-btn>
</v-card>
<v-card class="d-flex mb-2">
<v-row class="px-4 pt-2" dense>
<v-col>
<v-text-field label="ID" v-model="newState.id"></v-text-field>
</v-col>
<v-col>
<v-text-field label="Name" v-model="newState.name"></v-text-field>
</v-col>
<v-col>
<v-text-field label="Icon" v-model="newState.icon"></v-text-field>
</v-col>
<v-col>
<v-select label="Color" v-model="newState.color" :items="['info', 'error', 'success', 'warning']" clearable></v-select>
</v-col>
</v-row>
<v-btn icon class="mt-6 mr-4" @click="addState"><v-icon>mdi-plus</v-icon></v-btn>
</v-card>
<v-subheader class="mx-0 px-0">Artifact Types</v-subheader>
<v-card v-for="state in settings.artifactKinds" :key="state.id" class="d-flex mb-2">
<v-row class="px-4 pt-2" dense>
<v-col>
<v-text-field label="ID" v-model="state.id"></v-text-field>
</v-col>
<v-col>
<v-text-field label="Name" v-model="state.name"></v-text-field>
</v-col>
<v-col>
<v-text-field label="Icon" v-model="state.icon"></v-text-field>
</v-col>
<v-col>
<v-select label="Color" v-model="state.color" :items="['info', 'error', 'success', 'warning']" clearable></v-select>
</v-col>
</v-row>
<v-btn icon class="mt-6 mr-4" @click="removeKind(state.id)"><v-icon>mdi-close</v-icon></v-btn>
</v-card>
<v-card class="d-flex mb-2">
<v-row class="px-4 pt-2" dense>
<v-col>
<v-text-field label="ID" v-model="newKind.id"></v-text-field>
</v-col>
<v-col>
<v-text-field label="Name" v-model="newKind.name"></v-text-field>
</v-col>
<v-col>
<v-text-field label="Icon" v-model="newKind.icon"></v-text-field>
</v-col>
<v-col>
<v-select label="Color" v-model="newKind.color" :items="['info', 'error', 'success', 'warning']" clearable></v-select>
</v-col>
</v-row>
<v-btn icon class="mt-6 mr-4" @click="addKind"><v-icon>mdi-plus</v-icon></v-btn>
</v-card>
<v-btn color="success" @click="save" outlined class="mt-2">
<v-icon>mdi-content-save</v-icon>
Save
</v-btn>
</div>
</v-main>
</template>
<script lang="ts">
import {DateTime} from "luxon";
import Vue from "vue";
import {Settings, SettingsResponse, Type} from "@/client";
import {API} from "@/services/api";
import {AxiosResponse} from "axios";
interface State {
valid: boolean;
settings?: Settings;
newState: Type;
newKind: Type;
}
export default Vue.extend({
name: "Settings",
data: (): State => ({
valid: true,
settings: undefined,
newState: {} as Type,
newKind: {} as Type,
}),
methods: {
save: function () {
if (this.settings === undefined) {
return
}
API.saveSettings(this.settings).then((response) => {
this.settings = response.data;
this.$store.dispatch("getSettings");
})
},
addState: function () {
if (this.settings === undefined) {
return
}
this.settings.artifactStates.push(this.newState);
this.newState = {} as Type;
},
removeState: function (id: string) {
if (this.settings === undefined) {
return
}
this.settings.artifactStates = this.lodash.filter(this.settings.artifactStates, function (t) { return t.id !== id });
},
addKind: function () {
if (this.settings === undefined) {
return
}
this.settings.artifactKinds.push(this.newKind);
this.newKind = {} as Type;
},
removeKind: function (id: string) {
if (this.settings === undefined) {
return
}
this.settings.artifactKinds = this.lodash.filter(this.settings.artifactKinds, function (t) { return t.id !== id });
},
timeformat: function (s: string) {
let format = this.$store.state.settings.timeformat;
if (!format) {
return DateTime.fromISO(s).toLocaleString(DateTime.DATETIME_SHORT);
}
return DateTime.fromISO(s).toFormat(format);
},
dateformat: function (s: string) {
let format = this.$store.state.settings.timeformat;
if (!format) {
return DateTime.fromISO(s).toLocaleString(DateTime.DATETIME_SHORT);
}
return DateTime.fromISO(s).toFormat(format);
},
datetimeformat: function (s: string) {
let format = this.$store.state.settings.timeformat;
if (!format) {
return DateTime.fromISO(s).toLocaleString(DateTime.DATETIME_SHORT);
}
return DateTime.fromISO(s).toFormat(format);
},
},
mounted() {
API.getSettings().then((response: AxiosResponse<SettingsResponse>) => {
this.settings = response.data;
})
}
});
</script>

View File

@@ -181,7 +181,7 @@
<v-jsf
v-model="ticket.details"
:schema="schema"
:options="{ initialValidation: 'all', formats: { time: timeformat, date: dateformat, 'date-time': datetimeformat } }"
:options="{ initialValidation: 'all', formats: { time: timeformat, date: dateformat, 'date-time': datetimeformat }, editMode: 'inline' }"
/>
</v-form>
<v-btn small class="float-right mb-2" color="card" @click="saveTicket" outlined>

View File

@@ -1,4 +1,5 @@
module.exports = {
publicPath: "/static/",
transpileDependencies: ["vuetify", "@koumoul/vjsf"],
pwa: {
name: "Catalyst",