refactor: remove pocketbase (#1138)

This commit is contained in:
Jonas Plum
2025-09-02 21:58:08 +02:00
committed by GitHub
parent f28c238135
commit eba2615ec0
435 changed files with 42677 additions and 4730 deletions

441
app/data/demo.go Normal file
View File

@@ -0,0 +1,441 @@
package data
import (
"context"
_ "embed"
"fmt"
"time"
"github.com/brianvoe/gofakeit/v7"
"github.com/SecurityBrewery/catalyst/app/auth"
"github.com/SecurityBrewery/catalyst/app/auth/password"
"github.com/SecurityBrewery/catalyst/app/database"
"github.com/SecurityBrewery/catalyst/app/database/sqlc"
"github.com/SecurityBrewery/catalyst/app/pointer"
)
const (
minimumUserCount = 1
minimumTicketCount = 1
)
var (
//go:embed scripts/createticket.py
createTicketPy string
//go:embed scripts/alertingest.py
alertIngestPy string
//go:embed scripts/assigntickets.py
assignTicketsPy string
)
func GenerateDemoData(ctx context.Context, queries *sqlc.Queries, userCount, ticketCount int) error {
if userCount < minimumUserCount {
userCount = minimumUserCount
}
if ticketCount < minimumTicketCount {
ticketCount = minimumTicketCount
}
types, err := database.PaginateItems(ctx, func(ctx context.Context, offset, limit int64) ([]sqlc.ListTypesRow, error) {
return queries.ListTypes(ctx, sqlc.ListTypesParams{Limit: limit, Offset: offset})
})
if err != nil {
return fmt.Errorf("failed to list types: %w", err)
}
users, err := generateDemoUsers(ctx, queries, userCount, ticketCount)
if err != nil {
return fmt.Errorf("failed to create user records: %w", err)
}
if len(types) == 0 {
return fmt.Errorf("no types found")
}
if len(users) == 0 {
return fmt.Errorf("no users found")
}
if err := generateDemoTickets(ctx, queries, users, types, ticketCount); err != nil {
return fmt.Errorf("failed to create ticket records: %w", err)
}
if err := generateDemoReactions(ctx, queries, ticketCount); err != nil {
return fmt.Errorf("failed to create reaction records: %w", err)
}
if err := generateDemoGroups(ctx, queries, users, ticketCount); err != nil {
return fmt.Errorf("failed to create group records: %w", err)
}
return nil
}
func generateDemoUsers(ctx context.Context, queries *sqlc.Queries, count, ticketCount int) ([]sqlc.User, error) {
users := make([]sqlc.User, 0, count)
// create the test user
user, err := queries.GetUser(ctx, "u_test")
if err != nil {
newUser, err := createTestUser(ctx, queries)
if err != nil {
return nil, err
}
users = append(users, newUser)
} else {
users = append(users, user)
}
for range count - 1 {
newUser, err := createDemoUser(ctx, queries, ticketCount)
if err != nil {
return nil, err
}
users = append(users, newUser)
}
return users, nil
}
func createDemoUser(ctx context.Context, queries *sqlc.Queries, ticketCount int) (sqlc.User, error) {
username := gofakeit.Username()
passwordHash, tokenKey, err := password.Hash(gofakeit.Password(true, true, true, true, false, 16))
if err != nil {
return sqlc.User{}, fmt.Errorf("failed to hash password: %w", err)
}
created, updated := dates(ticketCount)
return queries.InsertUser(ctx, sqlc.InsertUserParams{
ID: database.GenerateID("u"),
Name: pointer.Pointer(gofakeit.Name()),
Email: pointer.Pointer(username + "@catalyst-soar.com"),
Username: username,
PasswordHash: passwordHash,
TokenKey: tokenKey,
Active: gofakeit.Bool(),
Created: created,
Updated: updated,
})
}
var ticketCreated = time.Date(2025, 2, 1, 11, 29, 35, 0, time.UTC)
func generateDemoTickets(ctx context.Context, queries *sqlc.Queries, users []sqlc.User, types []sqlc.ListTypesRow, count int) error { //nolint:cyclop
for range count {
newTicket, err := createDemoTicket(ctx, queries, random(types), random(users).ID, fakeTicketTitle(), fakeTicketDescription(), count)
if err != nil {
return fmt.Errorf("failed to create ticket: %w", err)
}
for range gofakeit.IntRange(1, 5) {
_, err := createDemoComment(ctx, queries, newTicket.ID, random(users).ID, fakeTicketComment(), count)
if err != nil {
return fmt.Errorf("failed to create comment for ticket %s: %w", newTicket.ID, err)
}
}
for range gofakeit.IntRange(1, 5) {
_, err := createDemoTimeline(ctx, queries, newTicket.ID, fakeTicketTimelineMessage(), count)
if err != nil {
return fmt.Errorf("failed to create timeline for ticket %s: %w", newTicket.ID, err)
}
}
for range gofakeit.IntRange(1, 5) {
_, err := createDemoTask(ctx, queries, newTicket.ID, random(users).ID, fakeTicketTask(), count)
if err != nil {
return fmt.Errorf("failed to create task for ticket %s: %w", newTicket.ID, err)
}
}
for range gofakeit.IntRange(1, 5) {
_, err := createDemoLink(ctx, queries, newTicket.ID, random([]string{"Blog", "Forum", "Wiki", "Documentation"}), gofakeit.URL(), count)
if err != nil {
return fmt.Errorf("failed to create link for ticket %s: %w", newTicket.ID, err)
}
}
}
return nil
}
func createDemoTicket(ctx context.Context, queries *sqlc.Queries, ticketType sqlc.ListTypesRow, userID, name, description string, ticketCount int) (sqlc.Ticket, error) {
created, updated := dates(ticketCount)
ticket, err := queries.InsertTicket(
ctx,
sqlc.InsertTicketParams{
ID: database.GenerateID(ticketType.Singular),
Name: name,
Description: description,
Open: gofakeit.Bool(),
Owner: &userID,
Schema: marshal(map[string]any{"type": "object", "properties": map[string]any{"tlp": map[string]any{"title": "TLP", "type": "string"}}}),
State: marshal(map[string]any{"severity": "Medium"}),
Type: ticketType.ID,
Created: created,
Updated: updated,
},
)
if err != nil {
return sqlc.Ticket{}, fmt.Errorf("failed to create ticket for user %s: %w", userID, err)
}
return ticket, nil
}
func createDemoComment(ctx context.Context, queries *sqlc.Queries, ticketID, userID, message string, ticketCount int) (*sqlc.Comment, error) {
created, updated := dates(ticketCount)
comment, err := queries.InsertComment(ctx, sqlc.InsertCommentParams{
ID: database.GenerateID("c"),
Ticket: ticketID,
Author: userID,
Message: message,
Created: created,
Updated: updated,
})
if err != nil {
return nil, fmt.Errorf("failed to create comment for ticket %s: %w", ticketID, err)
}
return &comment, nil
}
func createDemoTimeline(ctx context.Context, queries *sqlc.Queries, ticketID, message string, ticketCount int) (*sqlc.Timeline, error) {
created, updated := dates(ticketCount)
timeline, err := queries.InsertTimeline(ctx, sqlc.InsertTimelineParams{
ID: database.GenerateID("tl"),
Ticket: ticketID,
Message: message,
Time: ticketCreated,
Created: created,
Updated: updated,
})
if err != nil {
return nil, fmt.Errorf("failed to create timeline for ticket %s: %w", ticketID, err)
}
return &timeline, nil
}
func createDemoTask(ctx context.Context, queries *sqlc.Queries, ticketID, userID, name string, ticketCount int) (*sqlc.Task, error) {
created, updated := dates(ticketCount)
task, err := queries.InsertTask(ctx, sqlc.InsertTaskParams{
ID: database.GenerateID("t"),
Ticket: ticketID,
Owner: &userID,
Name: name,
Open: gofakeit.Bool(),
Created: created,
Updated: updated,
})
if err != nil {
return nil, fmt.Errorf("failed to create task for ticket %s: %w", ticketID, err)
}
return &task, nil
}
func createDemoLink(ctx context.Context, queries *sqlc.Queries, ticketID, name, url string, ticketCount int) (*sqlc.Link, error) {
created, updated := dates(ticketCount)
link, err := queries.InsertLink(ctx, sqlc.InsertLinkParams{
ID: database.GenerateID("l"),
Ticket: ticketID,
Name: name,
Url: url,
Created: created,
Updated: updated,
})
if err != nil {
return nil, fmt.Errorf("failed to create link for ticket %s: %w", ticketID, err)
}
return &link, nil
}
func generateDemoReactions(ctx context.Context, queries *sqlc.Queries, ticketCount int) error {
created, updated := dates(ticketCount)
_, err := queries.InsertReaction(ctx, sqlc.InsertReactionParams{
ID: "r-schedule",
Name: "Create New Ticket",
Trigger: "schedule",
Triggerdata: marshal(map[string]any{"expression": "12 * * * *"}),
Action: "python",
Actiondata: marshal(map[string]any{
"requirements": "requests",
"script": createTicketPy,
}),
Created: created,
Updated: updated,
})
if err != nil {
return fmt.Errorf("failed to create reaction for schedule trigger: %w", err)
}
created, updated = dates(ticketCount)
_, err = queries.InsertReaction(ctx, sqlc.InsertReactionParams{
ID: "r-webhook",
Name: "Alert Ingest Webhook",
Trigger: "webhook",
Triggerdata: marshal(map[string]any{"token": "1234567890", "path": "webhook"}),
Action: "python",
Actiondata: marshal(map[string]any{
"requirements": "requests",
"script": alertIngestPy,
}),
Created: created,
Updated: updated,
})
if err != nil {
return fmt.Errorf("failed to create reaction for webhook trigger: %w", err)
}
created, updated = dates(ticketCount)
_, err = queries.InsertReaction(ctx, sqlc.InsertReactionParams{
ID: "r-hook",
Name: "Assign new Tickets",
Trigger: "hook",
Triggerdata: marshal(map[string]any{"collections": []any{"tickets"}, "events": []any{"create"}}),
Action: "python",
Actiondata: marshal(map[string]any{
"requirements": "requests",
"script": assignTicketsPy,
}),
Created: created,
Updated: updated,
})
if err != nil {
return fmt.Errorf("failed to create reaction for hook trigger: %w", err)
}
return nil
}
func generateDemoGroups(ctx context.Context, queries *sqlc.Queries, users []sqlc.User, ticketCount int) error { //nolint:cyclop
created, updated := dates(ticketCount)
_, err := queries.InsertGroup(ctx, sqlc.InsertGroupParams{
ID: "team-ir",
Name: "IR Team",
Permissions: auth.ToJSONArray(ctx, []string{}),
Created: created,
Updated: updated,
})
if err != nil {
return fmt.Errorf("failed to create IR team group: %w", err)
}
created, updated = dates(ticketCount)
_, err = queries.InsertGroup(ctx, sqlc.InsertGroupParams{
ID: "team-seceng",
Name: "Security Engineering Team",
Permissions: auth.ToJSONArray(ctx, []string{}),
Created: created,
Updated: updated,
})
if err != nil {
return fmt.Errorf("failed to create IR team group: %w", err)
}
created, updated = dates(ticketCount)
_, err = queries.InsertGroup(ctx, sqlc.InsertGroupParams{
ID: "team-security",
Name: "Security Team",
Permissions: auth.ToJSONArray(ctx, []string{}),
Created: created,
Updated: updated,
})
if err != nil {
return fmt.Errorf("failed to create security team group: %w", err)
}
created, updated = dates(ticketCount)
_, err = queries.InsertGroup(ctx, sqlc.InsertGroupParams{
ID: "g-engineer",
Name: "Engineer",
Permissions: auth.ToJSONArray(ctx, []string{"reaction:read", "reaction:write"}),
Created: created,
Updated: updated,
})
if err != nil {
return fmt.Errorf("failed to create analyst group: %w", err)
}
for _, user := range users {
group := gofakeit.RandomString([]string{"team-seceng", "team-ir"})
if user.ID == "u_test" {
group = "admin"
}
if err := queries.AssignGroupToUser(ctx, sqlc.AssignGroupToUserParams{
UserID: user.ID,
GroupID: group,
}); err != nil {
return fmt.Errorf("failed to assign group %s to user %s: %w", group, user.ID, err)
}
}
err = queries.AssignParentGroup(ctx, sqlc.AssignParentGroupParams{
ParentGroupID: "team-ir",
ChildGroupID: "analyst",
})
if err != nil {
return fmt.Errorf("failed to assign parent group: %w", err)
}
err = queries.AssignParentGroup(ctx, sqlc.AssignParentGroupParams{
ParentGroupID: "team-seceng",
ChildGroupID: "g-engineer",
})
if err != nil {
return fmt.Errorf("failed to assign parent group: %w", err)
}
err = queries.AssignParentGroup(ctx, sqlc.AssignParentGroupParams{
ParentGroupID: "team-ir",
ChildGroupID: "team-security",
})
if err != nil {
return fmt.Errorf("failed to assign parent group: %w", err)
}
err = queries.AssignParentGroup(ctx, sqlc.AssignParentGroupParams{
ParentGroupID: "team-seceng",
ChildGroupID: "team-security",
})
if err != nil {
return fmt.Errorf("failed to assign parent group: %w", err)
}
return nil
}
func weeksAgo(c int) time.Time {
return time.Now().UTC().AddDate(0, 0, -7*c)
}
func dates(ticketCount int) (time.Time, time.Time) {
const ticketsPerWeek = 10
weeks := ticketCount / ticketsPerWeek
created := gofakeit.DateRange(weeksAgo(1), weeksAgo(weeks+1)).UTC()
updated := gofakeit.DateRange(created, time.Now()).UTC()
return created, updated
}

26
app/data/demo_test.go Normal file
View File

@@ -0,0 +1,26 @@
package data_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/SecurityBrewery/catalyst/app/data"
catalystTesting "github.com/SecurityBrewery/catalyst/testing"
)
func TestGenerate(t *testing.T) {
t.Parallel()
app, cleanup, _ := catalystTesting.App(t)
t.Cleanup(cleanup)
_ = app.Queries.DeleteUser(t.Context(), "u_admin")
_ = app.Queries.DeleteUser(t.Context(), "u_bob_analyst")
_ = app.Queries.DeleteGroup(t.Context(), "g_admin")
_ = app.Queries.DeleteGroup(t.Context(), "g_analyst")
err := data.GenerateDemoData(t.Context(), app.Queries, 4, 4)
require.NoError(t, err, "failed to generate fake data")
}

View File

@@ -0,0 +1,20 @@
import sys
import json
import random
import os
import requests
# Parse the event from the webhook payload
event = json.loads(sys.argv[1])
body = json.loads(event["body"])
url = os.environ["CATALYST_APP_URL"]
header = {"Authorization": "Bearer " + os.environ["CATALYST_TOKEN"]}
# Create a new ticket
requests.post(url + "/api/tickets", headers=header, json={
"name": body["name"],
"type": "alert",
"open": True,
})

View File

@@ -0,0 +1,21 @@
import sys
import json
import random
import os
import requests
# Parse the ticket from the input
ticket = json.loads(sys.argv[1])
url = os.environ["CATALYST_APP_URL"]
header = {"Authorization": "Bearer " + os.environ["CATALYST_TOKEN"]}
# Get a random user
users = requests.get(url + "/api/users", headers=header).json()
random_user = random.choice(users)
# Assign the ticket to the random user
requests.patch(url + "/api/tickets/" + ticket["record"]["id"], headers=header, json={
"owner": random_user["id"]
})

View File

@@ -0,0 +1,20 @@
import sys
import json
import random
import os
import requests
url = os.environ["CATALYST_APP_URL"]
header = {"Authorization": "Bearer " + os.environ["CATALYST_TOKEN"]}
newtickets = requests.get(url + "/api/tickets?limit=3", headers=header).json()
for ticket in newtickets:
requests.delete(url + "/api/tickets/" + ticket["id"], headers=header)
# Create a new ticket
requests.post(url + "/api/tickets", headers=header, json={
"name": "New Ticket",
"type": "alert",
"open": True,
})

View File

@@ -0,0 +1,21 @@
import sys
import json
import random
import os
from pocketbase import PocketBase
# Connect to the PocketBase server
client = PocketBase(os.environ["CATALYST_APP_URL"])
client.auth_store.save(token=os.environ["CATALYST_TOKEN"])
newtickets = client.collection("tickets").get_list(1, 200, {"filter": 'name = "New Ticket"'})
for ticket in newtickets.items:
client.collection("tickets").delete(ticket.id)
# Create a new ticket
client.collection("tickets").create({
"name": "New Ticket",
"type": "alert",
"open": True,
})

227
app/data/testdata.go Normal file
View File

@@ -0,0 +1,227 @@
package data
import (
"context"
"encoding/json"
"log/slog"
"os"
"path"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/SecurityBrewery/catalyst/app/database"
"github.com/SecurityBrewery/catalyst/app/database/sqlc"
"github.com/SecurityBrewery/catalyst/app/pointer"
)
const (
AdminEmail = "admin@catalyst-soar.com"
AnalystEmail = "analyst@catalyst-soar.com"
)
func DefaultTestData(t *testing.T, dir string, queries *sqlc.Queries) {
t.Helper()
parseTime := func(s string) time.Time {
t, _ := time.Parse(time.RFC3339Nano, s)
return t
}
ctx := t.Context()
// Insert users
_, err := queries.InsertUser(ctx, sqlc.InsertUserParams{
Created: parseTime("2025-06-21T22:21:26.271Z"),
Updated: parseTime("2025-06-21T22:21:26.271Z"),
Email: pointer.Pointer("analyst@catalyst-soar.com"),
Username: "u_bob_analyst",
Name: pointer.Pointer("Bob Analyst"),
PasswordHash: "$2a$10$ZEHNh9ZKJ81N717wovDnMuLwZOLa6.g22IRzRr4goG6zGN.57UzJG",
TokenKey: "z3Jj8bbzcq_cSZs07XKoGlB0UtvmQiphHgwNkE4akoY=",
Active: true,
ID: "u_bob_analyst",
})
require.NoError(t, err, "failed to insert analyst user")
_, err = queries.InsertUser(ctx, sqlc.InsertUserParams{
Created: parseTime("2025-06-21T22:21:26.271Z"),
Updated: parseTime("2025-06-21T22:21:26.271Z"),
Email: pointer.Pointer("admin@catalyst-soar.com"),
Username: "u_admin",
Name: pointer.Pointer("Admin User"),
PasswordHash: "$2a$10$Z3/0HHWau6oi1t1aRPiI0uiVOWI.IosTAYEL0DJ2XJaalP9kesgBa",
TokenKey: "5BWDKLIAn3SQkpQlBUGrS_XEbFf91DsDpuh_Xmt4Nwg=",
Active: true,
ID: "u_admin",
})
require.NoError(t, err, "failed to insert admin user")
// Insert webhooks
_, err = queries.InsertWebhook(ctx, sqlc.InsertWebhookParams{
ID: "w_test_webhook",
Name: "Test Webhook",
Collection: "tickets",
Destination: "https://example.com",
Created: parseTime("2025-06-21T22:21:26.271Z"),
Updated: parseTime("2025-06-21T22:21:26.271Z"),
})
require.NoError(t, err, "failed to insert webhook")
// Insert types
_, err = queries.InsertType(ctx, sqlc.InsertTypeParams{
ID: "test-type",
Singular: "Test",
Plural: "Tests",
Schema: []byte(`{}`),
Created: parseTime("2025-06-21T22:21:26.271Z"),
Updated: parseTime("2025-06-21T22:21:26.271Z"),
})
require.NoError(t, err, "failed to insert type")
// Insert tickets
_, err = queries.InsertTicket(ctx, sqlc.InsertTicketParams{
Created: parseTime("2025-06-21T22:21:26.271Z"),
Description: "This is a test ticket.",
ID: "test-ticket",
Name: "Test Ticket",
Open: true,
Owner: pointer.Pointer("u_bob_analyst"),
Schema: json.RawMessage(`{"type":"object","properties":{"tlp":{"title":"TLP","type":"string"}}}`),
State: json.RawMessage(`{"tlp":"AMBER"}`),
Type: "incident",
Updated: parseTime("2025-06-21T22:21:26.271Z"),
})
require.NoError(t, err, "failed to insert ticket")
// Insert tasks
_, err = queries.InsertTask(ctx, sqlc.InsertTaskParams{
Created: parseTime("2025-06-21T22:21:26.271Z"),
ID: "k_test_task",
Name: "Test Task",
Open: true,
Owner: pointer.Pointer("u_bob_analyst"),
Ticket: "test-ticket",
Updated: parseTime("2025-06-21T22:21:26.271Z"),
})
require.NoError(t, err, "failed to insert task")
// Insert comments
_, err = queries.InsertComment(ctx, sqlc.InsertCommentParams{
Author: "u_bob_analyst",
Created: parseTime("2025-06-21T22:21:26.271Z"),
ID: "c_test_comment",
Message: "Initial comment on the test ticket.",
Ticket: "test-ticket",
Updated: parseTime("2025-06-21T22:21:26.271Z"),
})
require.NoError(t, err, "failed to insert comment")
// Insert timeline
_, err = queries.InsertTimeline(ctx, sqlc.InsertTimelineParams{
Created: parseTime("2025-06-21T22:21:26.271Z"),
ID: "h_test_timeline",
Message: "Initial timeline entry.",
Ticket: "test-ticket",
Time: parseTime("2023-01-01T00:00:00Z"),
Updated: parseTime("2025-06-21T22:21:26.271Z"),
})
require.NoError(t, err, "failed to insert timeline entry")
// Insert links
_, err = queries.InsertLink(ctx, sqlc.InsertLinkParams{
Created: parseTime("2025-06-21T22:21:26.271Z"),
ID: "l_test_link",
Name: "Catalyst",
Ticket: "test-ticket",
Updated: parseTime("2025-06-21T22:21:26.271Z"),
Url: "https://example.com",
})
require.NoError(t, err, "failed to insert link")
// Insert files
_, err = queries.InsertFile(ctx, sqlc.InsertFileParams{
Created: parseTime("2025-06-21T22:21:26.271Z"),
ID: "b_test_file",
Name: "hello.txt",
Size: 5,
Ticket: "test-ticket",
Updated: parseTime("2025-06-21T22:21:26.271Z"),
Blob: "hello_a20DUE9c77rj.txt",
})
require.NoError(t, err, "failed to insert file")
// Insert features
_, err = queries.CreateFeature(ctx, "dev")
require.NoError(t, err, "failed to insert feature 'dev'")
// Insert reactions
_, err = queries.InsertReaction(ctx, sqlc.InsertReactionParams{
ID: "r-test-webhook",
Name: "Reaction",
Action: "python",
Actiondata: []byte(`{"requirements":"requests","script":"print('Hello, World!')"}`),
Trigger: "webhook",
Triggerdata: []byte(`{"token":"1234567890","path":"test"}`),
Created: parseTime("2025-06-21T22:21:26.271Z"),
Updated: parseTime("2025-06-21T22:21:26.271Z"),
})
require.NoError(t, err, "failed to insert reaction")
_, err = queries.InsertReaction(ctx, sqlc.InsertReactionParams{
ID: "r-test-proxy",
Action: "webhook",
Name: "Reaction",
Actiondata: []byte(`{"headers":{"Content-Type":"application/json"},"url":"http://127.0.0.1:12345/webhook"}`),
Trigger: "webhook",
Triggerdata: []byte(`{"path":"test2"}`),
Created: parseTime("2025-06-21T22:21:26.271Z"),
Updated: parseTime("2025-06-21T22:21:26.271Z"),
})
require.NoError(t, err, "failed to insert reaction")
_, err = queries.InsertReaction(ctx, sqlc.InsertReactionParams{
ID: "r-test-hook",
Name: "Hook",
Action: "python",
Actiondata: []byte(`{"requirements":"requests","script":"import requests\nrequests.post('http://127.0.0.1:12346/test', json={'test':True})"}`),
Trigger: "hook",
Triggerdata: json.RawMessage(`{"collections":["tickets"],"events":["create"]}`),
Created: parseTime("2025-06-21T22:21:26.271Z"),
Updated: parseTime("2025-06-21T22:21:26.271Z"),
})
require.NoError(t, err, "failed to insert reaction")
// Insert user_groups
err = queries.AssignGroupToUser(ctx, sqlc.AssignGroupToUserParams{
UserID: "u_bob_analyst",
GroupID: "analyst",
})
require.NoError(t, err, "failed to assign analyst group to user")
err = queries.AssignGroupToUser(ctx, sqlc.AssignGroupToUserParams{
UserID: "u_admin",
GroupID: "admin",
})
require.NoError(t, err, "failed to assign admin group to user")
files, err := database.PaginateItems(ctx, func(ctx context.Context, offset, limit int64) ([]sqlc.ListFilesRow, error) {
return queries.ListFiles(ctx, sqlc.ListFilesParams{Limit: limit, Offset: offset})
})
require.NoError(t, err, "failed to list files")
for _, file := range files {
_ = os.MkdirAll(path.Join(dir, "uploads", file.ID), 0o755)
infoFilePath := path.Join(dir, "uploads", file.ID+".info")
slog.InfoContext(t.Context(), "Creating file info", "path", infoFilePath)
err = os.WriteFile(infoFilePath, []byte(`{"MetaData":{"filetype":"text/plain"}}`), 0o600)
require.NoError(t, err, "failed to write file info")
err = os.WriteFile(path.Join(dir, "uploads", file.ID, file.Blob), []byte("hello"), 0o600)
require.NoError(t, err, "failed to write file blob")
}
}

16
app/data/testdata.sql Normal file
View File

@@ -0,0 +1,16 @@
INSERT INTO users VALUES('2025-06-21 22:21:26.271Z','2025-06-21 22:21:26.271Z','analyst@catalyst-soar.com','u_bob_analyst','','','Bob Analyst','$2a$10$ZEHNh9ZKJ81N717wovDnMuLwZOLa6.g22IRzRr4goG6zGN.57UzJG','z3Jj8bbzcq_cSZs07XKoGlB0UtvmQiphHgwNkE4akoY=','2025-06-21 22:21:26.271Z','u_bob_analyst',1);
INSERT INTO users VALUES('2025-06-21 22:21:26.271Z','2025-06-21 22:21:26.271Z','admin@catalyst-soar.com','u_admin','','','Admin User','$2a$10$Z3/0HHWau6oi1t1aRPiI0uiVOWI.IosTAYEL0DJ2XJaalP9kesgBa','5BWDKLIAn3SQkpQlBUGrS_XEbFf91DsDpuh_Xmt4Nwg=','2025-06-21 22:21:26.271Z','u_admin',1);
INSERT INTO webhooks VALUES('tickets','2025-06-21 22:21:26.271Z','https://example.com','w_test_webhook','Test Webhook','2025-06-21 22:21:26.271Z');
INSERT INTO types VALUES('2025-06-21 22:21:26.271Z','Bug','test-type','Tests','{}','Test','2025-06-21 22:21:26.271Z');
INSERT INTO tickets VALUES('2025-06-21 22:21:26.271Z','This is a test ticket.','test-ticket','Test Ticket',1,'u_bob_analyst','','{"type":"object","properties":{"tlp":{"title":"TLP","type":"string"}}}','{"tlp":"AMBER"}','incident','2025-06-21 22:21:26.271Z');
INSERT INTO tasks VALUES('2025-06-21 22:21:26.271Z','k_test_task','Test Task',1,'u_bob_analyst','test-ticket','2025-06-21 22:21:26.271Z');
INSERT INTO comments VALUES('u_bob_analyst','2025-06-21 22:21:26.271Z','c_test_comment','Initial comment on the test ticket.','test-ticket','2025-06-21 22:21:26.271Z');
INSERT INTO timeline VALUES('2025-06-21 22:21:26.271Z','h_test_timeline','Initial timeline entry.','test-ticket','2023-01-01T00:00:00Z','2025-06-21 22:21:26.271Z');
INSERT INTO links VALUES('2025-06-21 22:21:26.271Z','l_test_link','Catalyst','test-ticket','2025-06-21 22:21:26.271Z','https://example.com');
INSERT INTO files VALUES('hello_a20DUE9c77rj.txt','2025-06-21 22:21:26.271Z','b_test_file','hello.txt',5,'test-ticket','2025-06-21 22:21:26.271Z');
INSERT INTO features VALUES('2025-06-21 22:21:26.271Z','rce91818107f46a','dev','2025-06-21 22:21:26.271Z');
INSERT INTO reactions VALUES('python','{"requirements":"requests","script":"print(''Hello, World!'')"}','','r-test-webhook','Reaction','webhook','{"token":"1234567890","path":"test"}','2025-06-21 22:21:26.271Z');
INSERT INTO reactions VALUES('webhook','{"headers":{"Content-Type":"application/json"},"url":"http://127.0.0.1:12345/webhook"}','','r-test-proxy','Reaction','webhook','{"path":"test2"}','2025-06-21 22:21:26.271Z');
INSERT INTO reactions VALUES('python','{"requirements":"requests","script":"import requests\nrequests.post(''http://127.0.0.1:12346/test'', json={''test'':True})"}','','r-test-hook','Hook','hook','{"collections":["tickets"],"events":["create"]}','2025-06-21 22:21:26.271Z');
INSERT INTO user_groups VALUES('u_bob_analyst','analyst');
INSERT INTO user_groups VALUES('u_admin','admin');

84
app/data/testdata_test.go Normal file
View File

@@ -0,0 +1,84 @@
package data
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/SecurityBrewery/catalyst/app/database/sqlc"
"github.com/SecurityBrewery/catalyst/app/pointer"
)
func TestDBInitialization(t *testing.T) {
t.Parallel()
queries := NewTestDB(t, t.TempDir())
user, err := queries.SystemUser(t.Context())
require.NoError(t, err)
assert.Equal(t, "system", user.ID)
types, err := queries.ListTypes(t.Context(), sqlc.ListTypesParams{Offset: 0, Limit: 10})
require.NoError(t, err)
assert.GreaterOrEqual(t, len(types), 1)
}
func TestNewTestDBDefaultData(t *testing.T) {
t.Parallel()
queries := NewTestDB(t, t.TempDir())
user, err := queries.UserByEmail(t.Context(), pointer.Pointer(AdminEmail))
require.NoError(t, err)
assert.Equal(t, AdminEmail, *user.Email)
ticket, err := queries.Ticket(t.Context(), "test-ticket")
require.NoError(t, err)
assert.Equal(t, "test-ticket", ticket.ID)
comment, err := queries.GetComment(t.Context(), "c_test_comment")
require.NoError(t, err)
assert.Equal(t, "c_test_comment", comment.ID)
timeline, err := queries.GetTimeline(t.Context(), "h_test_timeline")
require.NoError(t, err)
assert.Equal(t, "h_test_timeline", timeline.ID)
}
func TestReadWrite(t *testing.T) {
t.Parallel()
queries := NewTestDB(t, t.TempDir())
for range 3 {
y, err := queries.CreateType(t.Context(), sqlc.CreateTypeParams{
Singular: "Foo",
Plural: "Foos",
Icon: pointer.Pointer("Bug"),
Schema: json.RawMessage("{}"),
})
require.NoError(t, err)
_, err = queries.GetType(t.Context(), y.ID)
require.NoError(t, err)
err = queries.DeleteType(t.Context(), y.ID)
require.NoError(t, err)
}
}
func TestRead(t *testing.T) {
t.Parallel()
queries := NewTestDB(t, t.TempDir())
// read from a table
_, err := queries.GetUser(t.Context(), "u_bob_analyst")
require.NoError(t, err)
// read from a view
_, err = queries.GetSidebar(t.Context())
require.NoError(t, err)
}

27
app/data/testing.go Normal file
View File

@@ -0,0 +1,27 @@
package data
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/SecurityBrewery/catalyst/app/database"
"github.com/SecurityBrewery/catalyst/app/database/sqlc"
"github.com/SecurityBrewery/catalyst/app/migration"
"github.com/SecurityBrewery/catalyst/app/upload"
)
func NewTestDB(t *testing.T, dir string) *sqlc.Queries {
t.Helper()
queries := database.TestDB(t, dir)
uploader, err := upload.New(dir)
require.NoError(t, err)
err = migration.Apply(t.Context(), queries, dir, uploader)
require.NoError(t, err)
DefaultTestData(t, dir, queries)
return queries
}

40
app/data/testing_test.go Normal file
View File

@@ -0,0 +1,40 @@
package data
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/SecurityBrewery/catalyst/app/database/sqlc"
"github.com/SecurityBrewery/catalyst/app/pointer"
)
func TestNewTestDB(t *testing.T) {
t.Parallel()
dir := t.TempDir()
queries := NewTestDB(t, dir)
user, err := queries.GetUser(t.Context(), "u_bob_analyst")
require.NoError(t, err)
assert.Equal(t, "u_bob_analyst", user.ID)
assert.Equal(t, "Bob Analyst", *user.Name)
assert.Equal(t, time.Date(2025, time.June, 21, 22, 21, 26, 271000000, time.UTC), user.Created)
alice, err := queries.InsertUser(t.Context(), sqlc.InsertUserParams{
ID: "u_alice_admin",
Name: pointer.Pointer("Alice Admin"),
Username: "alice_admin",
PasswordHash: "",
TokenKey: "",
Created: time.Date(2025, time.June, 21, 22, 21, 26, 0, time.UTC),
Updated: time.Date(2025, time.June, 21, 22, 21, 26, 0, time.UTC),
})
require.NoError(t, err)
assert.Equal(t, time.Date(2025, time.June, 21, 22, 21, 26, 0, time.UTC), alice.Created)
}

127
app/data/text.go Normal file
View File

@@ -0,0 +1,127 @@
package data
import "github.com/brianvoe/gofakeit/v7"
func fakeTicketTitle() string {
return random([]string{
"Unauthorized Access Attempt",
"Multiple Failed Login Attempts",
"Suspicious File Download",
"User Account Locked",
"Unusual Network Activity",
"Phishing Email Reported",
"Sensitive Data Transfer Detected",
"Malware Infection Found",
"Unauthorized Device Connected",
"Brute-Force Attack Attempt",
"Security Patch Required",
"External IP Address Probing Network",
"Suspicious Behavior Detected",
"Unauthorized Software Installation",
"Access Control System Malfunction",
"DDoS Attack Detected",
})
}
func fakeTicketDescription() string {
return random([]string{
"Unauthorized access attempt detected in the main server room.",
"Multiple failed login attempts from an unknown IP address.",
"Suspicious file download flagged by antivirus software.",
"User account locked due to repeated incorrect password entries.",
"Unusual network activity observed on the internal firewall.",
"Phishing email reported by several employees.",
"Sensitive data transfer detected outside the approved hours.",
"Malware infection found on a workstation in the finance department.",
"Unauthorized device connected to the company network.",
"Brute-force attack attempt on the admin account detected.",
"Security patch required for vulnerability in outdated software.",
"External IP address attempting to probe network ports.",
"Suspicious behavior detected by user in HR department.",
"Unauthorized software installation on company laptop.",
"Access control system malfunction at the main entrance.",
"DDoS attack detected on company web server.",
"Unusual outbound traffic to a known malicious domain.",
"Potential insider threat flagged by behavior analysis tool.",
"Compromised credentials detected on dark web.",
"Encryption key rotation required for compliance with security policy.",
})
}
func fakeTicketComment() string {
return random([]string{
"Ticket opened by user.",
"Initial investigation started.",
"Further analysis required.",
"Escalated to security team.",
"Action taken to mitigate risk.",
"Resolution in progress.",
"User notified of incident.",
"Security incident confirmed.",
"Containment measures implemented.",
"Root cause analysis underway.",
"Forensic investigation initiated.",
"Data breach confirmed.",
"Incident response team activated.",
"Legal counsel consulted.",
"Public relations notified.",
"Regulatory authorities informed.",
"Compensation plan developed.",
"Press release drafted.",
"Media monitoring in progress.",
"Post-incident review scheduled.",
})
}
func fakeTicketTimelineMessage() string {
return random([]string{
"Initial investigation started.",
"Further analysis required.",
"Escalated to security team.",
"Action taken to mitigate risk.",
"Resolution in progress.",
"User notified of incident.",
"Security incident confirmed.",
"Containment measures implemented.",
"Root cause analysis underway.",
"Forensic investigation initiated.",
"Data breach confirmed.",
"Incident response team activated.",
"Legal counsel consulted.",
"Public relations notified.",
"Regulatory authorities informed.",
"Compensation plan developed.",
"Press release drafted.",
"Media monitoring in progress.",
"Post-incident review scheduled.",
})
}
func fakeTicketTask() string {
return random([]string{
"Interview witnesses.",
"Review security camera footage.",
"Analyze network traffic logs.",
"Scan for malware on affected systems.",
"Check for unauthorized software installations.",
"Conduct vulnerability assessment.",
"Implement security patch.",
"Change firewall rules.",
"Reset compromised credentials.",
"Isolate infected systems.",
"Monitor for further suspicious activity.",
"Coordinate with law enforcement.",
"Notify affected customers.",
"Prepare incident report.",
"Update security policies.",
"Train employees on security best practices.",
"Conduct post-incident review.",
"Implement lessons learned.",
"Improve incident response procedures.",
"Enhance security awareness program.",
})
}
func random[T any](e []T) T {
return e[gofakeit.IntN(len(e))]
}

60
app/data/text_test.go Normal file
View File

@@ -0,0 +1,60 @@
package data
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_fakeTicketComment(t *testing.T) {
t.Parallel()
assert.NotEmpty(t, fakeTicketComment())
}
func Test_fakeTicketDescription(t *testing.T) {
t.Parallel()
assert.NotEmpty(t, fakeTicketDescription())
}
func Test_fakeTicketTask(t *testing.T) {
t.Parallel()
assert.NotEmpty(t, fakeTicketTask())
}
func Test_fakeTicketTimelineMessage(t *testing.T) {
t.Parallel()
assert.NotEmpty(t, fakeTicketTimelineMessage())
}
func Test_random(t *testing.T) {
t.Parallel()
type args[T any] struct {
e []T
}
type testCase[T any] struct {
name string
args args[T]
}
tests := []testCase[int]{
{
name: "Test random",
args: args[int]{e: []int{1, 2, 3, 4, 5}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := random(tt.args.e)
assert.Contains(t, tt.args.e, got)
})
}
}

216
app/data/upgradetest.go Normal file
View File

@@ -0,0 +1,216 @@
package data
import (
"context"
_ "embed"
"encoding/json"
"fmt"
"time"
"github.com/SecurityBrewery/catalyst/app/database/sqlc"
"github.com/SecurityBrewery/catalyst/app/pointer"
)
//go:embed scripts/upgradetest.py
var Script string
func GenerateUpgradeTestData(ctx context.Context, queries *sqlc.Queries) error { //nolint:cyclop
if _, err := createTestUser(ctx, queries); err != nil {
return err
}
for _, ticket := range CreateUpgradeTestDataTickets() {
_, err := queries.InsertTicket(ctx, sqlc.InsertTicketParams{
ID: ticket.ID,
Name: ticket.Name,
Type: ticket.Type,
Description: ticket.Description,
Open: ticket.Open,
Schema: ticket.Schema,
State: ticket.State,
Owner: ticket.Owner,
Resolution: ticket.Resolution,
Created: ticket.Created,
Updated: ticket.Updated,
})
if err != nil {
return fmt.Errorf("failed to create ticket: %w", err)
}
}
for _, comment := range CreateUpgradeTestDataComments() {
_, err := queries.InsertComment(ctx, sqlc.InsertCommentParams{
ID: comment.ID,
Ticket: comment.Ticket,
Author: comment.Author,
Message: comment.Message,
Created: comment.Created,
Updated: comment.Updated,
})
if err != nil {
return fmt.Errorf("failed to create comment: %w", err)
}
}
for _, timeline := range CreateUpgradeTestDataTimeline() {
_, err := queries.InsertTimeline(ctx, sqlc.InsertTimelineParams{
ID: timeline.ID,
Ticket: timeline.Ticket,
Time: timeline.Time,
Message: timeline.Message,
Created: timeline.Created,
Updated: timeline.Updated,
})
if err != nil {
return fmt.Errorf("failed to create timeline: %w", err)
}
}
for _, task := range CreateUpgradeTestDataTasks() {
_, err := queries.InsertTask(ctx, sqlc.InsertTaskParams{
ID: task.ID,
Ticket: task.Ticket,
Name: task.Name,
Open: task.Open,
Owner: task.Owner,
Created: task.Created,
Updated: task.Updated,
})
if err != nil {
return fmt.Errorf("failed to create task: %w", err)
}
}
for _, link := range CreateUpgradeTestDataLinks() {
_, err := queries.InsertLink(ctx, sqlc.InsertLinkParams{
ID: link.ID,
Ticket: link.Ticket,
Url: link.Url,
Name: link.Name,
Created: link.Created,
Updated: link.Updated,
})
if err != nil {
return fmt.Errorf("failed to create link: %w", err)
}
}
for _, reaction := range CreateUpgradeTestDataReaction() {
_, err := queries.InsertReaction(ctx, sqlc.InsertReactionParams{ //nolint: staticcheck
ID: reaction.ID,
Name: reaction.Name,
Trigger: reaction.Trigger,
Triggerdata: reaction.Triggerdata,
Action: reaction.Action,
Actiondata: reaction.Actiondata,
Created: reaction.Created,
Updated: reaction.Updated,
})
if err != nil {
return fmt.Errorf("failed to create reaction: %w", err)
}
}
return nil
}
func CreateUpgradeTestDataTickets() map[string]sqlc.Ticket {
return map[string]sqlc.Ticket{
"t_0": {
ID: "t_0",
Created: ticketCreated,
Updated: ticketCreated.Add(time.Minute * 5),
Name: "phishing-123",
Type: "alert",
Description: "Phishing email reported by several employees.",
Open: true,
Schema: json.RawMessage(`{"type":"object","properties":{"tlp":{"title":"TLP","type":"string"}}}`),
State: json.RawMessage(`{"severity":"Medium"}`),
Owner: pointer.Pointer("u_test"),
},
}
}
func CreateUpgradeTestDataComments() map[string]sqlc.Comment {
return map[string]sqlc.Comment{
"c_0": {
ID: "c_0",
Created: ticketCreated.Add(time.Minute * 10),
Updated: ticketCreated.Add(time.Minute * 15),
Ticket: "t_0",
Author: "u_test",
Message: "This is a test comment.",
},
}
}
func CreateUpgradeTestDataTimeline() map[string]sqlc.Timeline {
return map[string]sqlc.Timeline{
"tl_0": {
ID: "tl_0",
Created: ticketCreated.Add(time.Minute * 15),
Updated: ticketCreated.Add(time.Minute * 20),
Ticket: "t_0",
Time: ticketCreated.Add(time.Minute * 15),
Message: "This is a test timeline message.",
},
}
}
func CreateUpgradeTestDataTasks() map[string]sqlc.Task {
return map[string]sqlc.Task{
"ts_0": {
ID: "ts_0",
Created: ticketCreated.Add(time.Minute * 20),
Updated: ticketCreated.Add(time.Minute * 25),
Ticket: "t_0",
Name: "This is a test task.",
Open: true,
Owner: pointer.Pointer("u_test"),
},
}
}
func CreateUpgradeTestDataLinks() map[string]sqlc.Link {
return map[string]sqlc.Link{
"l_0": {
ID: "l_0",
Created: ticketCreated.Add(time.Minute * 25),
Updated: ticketCreated.Add(time.Minute * 30),
Ticket: "t_0",
Url: "https://www.example.com",
Name: "This is a test link.",
},
}
}
func CreateUpgradeTestDataReaction() map[string]sqlc.Reaction {
var (
reactionCreated = time.Date(2025, 2, 1, 11, 30, 0, 0, time.UTC)
reactionUpdated = reactionCreated.Add(time.Minute * 5)
)
createTicketActionData := marshal(map[string]any{
"requirements": "pocketbase",
"script": Script,
})
return map[string]sqlc.Reaction{
"w_0": {
ID: "w_0",
Created: reactionCreated,
Updated: reactionUpdated,
Name: "Create New Ticket",
Trigger: "schedule",
Triggerdata: json.RawMessage(`{"expression":"12 * * * *"}`),
Action: "python",
Actiondata: createTicketActionData,
},
}
}
func marshal(m map[string]any) json.RawMessage {
b, _ := json.Marshal(m) //nolint:errchkjson
return b
}

30
app/data/user.go Normal file
View File

@@ -0,0 +1,30 @@
package data
import (
"context"
"fmt"
"time"
"github.com/SecurityBrewery/catalyst/app/auth/password"
"github.com/SecurityBrewery/catalyst/app/database/sqlc"
"github.com/SecurityBrewery/catalyst/app/pointer"
)
func createTestUser(ctx context.Context, queries *sqlc.Queries) (sqlc.User, error) {
passwordHash, tokenKey, err := password.Hash("1234567890")
if err != nil {
return sqlc.User{}, fmt.Errorf("failed to hash password: %w", err)
}
return queries.InsertUser(ctx, sqlc.InsertUserParams{
ID: "u_test",
Username: "u_test",
Name: pointer.Pointer("Test User"),
Email: pointer.Pointer("user@catalyst-soar.com"),
Active: true,
PasswordHash: passwordHash,
TokenKey: tokenKey,
Created: time.Now(),
Updated: time.Now(),
})
}