diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9038b529..3eb949ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,8 @@ name: Build and Release on: push: + branches: + - main tags: - 'v*.*.*' diff --git a/README.md b/README.md index 8a8ba75b..45060eae 100644 --- a/README.md +++ b/README.md @@ -5,22 +5,77 @@ AlertFlow is a monitoring automation platform designed to streamline workflows, ## Table of Contents - [Features](#features) -- [Installation](#installation) -- [Usage](#usage) +- [Preview](#preview) +- [Self Hosting](#self-hosting) +- [Runners](#runners) - [Project Structure](#project-structure) +- [Local Development](#local-development) - [Contributing](#contributing) - [License](#license) ## Features -- **Project Management**: Create, edit, and delete projects. -- **Flows**: Create flows to design workflows per alarm pattern. -- **Payloads**: Receive incoming alarms from your monitoring infrastructure via payloads & flows. -- **Runners**: Manage self-hosted runners for executing tasks. +- **Project Management**: Projects combine a number of Flows and add the option to invite members and control their access. +- **Flows**: Create flows to design workflows for your incoming alarms. +- **Payloads**: Receive incoming alarms from your monitoring infrastructure via payloads and automate them via Flows. +- **Runners**: Runners execute your workflows and can also be self-hosted. +- **Shared Runners**: Create runners which can be used for all projects. +- **Scalable to your Needs**: Runners can be scaled with the Auto Join option - **Team Collaboration**: Invite team members, assign roles, and manage permissions. -- **Audit Logs**: Track changes and activities within projects. +- **Audit Logs**: Track changes and activities within projects and flows. -## Installation +## Preview +If you want to checkout AlertFlow before hosting it on your own, have a look at +https://alertflow.org + +Login Details: +``` +Username: Demo +Password: demo123 +``` + +## Self Hosting +To run your own version of AlertFlow we provide various docker images available at +[Docker Hub](https://hub.docker.com/repository/docker/justnz/alertflow/general). +- **justnz/alertflow:latest** - Full version including frontend and backend +- **justnz/alertflow:vx.x.x** - Versioned release. Also available for the single frontend and backend images +- **justnz/alertflow:frontend-latest** - Only frontend +- **justnz/alertflow:backend-latest** - Only backend + +### Full Version + +Config example: [config.yaml](https://github.com/AlertFlow/alertflow/blob/main/services/backend/config/config.yaml) + +```sh +docker run -p 80:3000 -v /your/config/path/config.yaml:/etc/alertflow/backend_config.yaml justnz/alertflow:latest +``` + +### Frontend Only +If you want to run only the frontend of AlertFlow, please provide the backend endpoint via the below env flag. +```sh +docker run -p 80:3000 -e NEXT_PUBLIC_API_URL=https://api-url.com justnz/alertflow:frontend-latest +``` + +### Backend Only +```sh +docker run -p 8080:8080 -v /your/config/path/config.yaml:/etc/alertflow/backend_config.yaml justnz/alertflow:backend-latest +``` + +## Runners +Runners are the key part of AlertFlow and provide the functionality of execution so called Workflows. + +To create / run your own runners you require to have a fully set up AlertFlow instance. + +Please see the repo [Runner](https://github.com/AlertFlow/runner) for more informations. + +## Project Structure + +The project structure is organized as follows: + +- **backend**: Contains the backend code for handling API requests, database interactions, and business logic. +- **frontend**: Contains the frontend code for the user interface, including components, pages, and styles. + +## Local Development To get started with the AlertFlow project, follow these steps: @@ -37,7 +92,7 @@ To get started with the AlertFlow project, follow these steps: cd services/backend && go mod download ``` -3. Create a [config.yaml](https://github.com/AlertFlow/alertflow/blob/main/services/backend/config/config.yaml) file in the `config` directory and add the necessary configuration: +3. Create a [config.yaml](https://github.com/AlertFlow/alertflow/blob/main/services/backend/config/config.yaml) file and add the necessary configuration: ```yaml LogLevel: info @@ -48,14 +103,18 @@ To get started with the AlertFlow project, follow these steps: User: postgres Password: postgres + Encryption: + Enabled: true + Key: your-encryption-key + JWT: Secret: your-jwt-secret ``` 4. Build and run the backend server: ```sh - go build -o alertflow-backend - ./alertflow-backend --config config/config.yaml + $ go build -o alertflow-backend + $ ./alertflow-backend --config config/config.yaml ``` ### Frontend @@ -80,17 +139,6 @@ To get started with the AlertFlow project, follow these steps: npm run dev ``` -## Usage - -Once the development server is running, you can access the application at `http://localhost:3000`. From there, you can create and manage projects, invite team members, and monitor project activities. - -## Project Structure - -The project structure is organized as follows: - -- **backend**: Contains the backend code for handling API requests, database interactions, and business logic. -- **frontend**: Contains the frontend code for the user interface, including components, pages, and styles. - ## Contributing We welcome contributions to the AlertFlow project! To contribute, follow these steps: @@ -112,4 +160,4 @@ We welcome contributions to the AlertFlow project! To contribute, follow these s ## License -This project is licensed under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3. See the [LICENSE](https://github.com/AlertFlow/alertflow/blob/main/LICENSE) file for details. \ No newline at end of file +This project is licensed under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3. See the [LICENSE](https://github.com/AlertFlow/alertflow/blob/main/LICENSE) file for details. diff --git a/services/backend/database/migrations/3_encrypted_flows.go b/services/backend/database/migrations/3_encrypted_flows.go new file mode 100644 index 00000000..94cd8b79 --- /dev/null +++ b/services/backend/database/migrations/3_encrypted_flows.go @@ -0,0 +1,93 @@ +package migrations + +import ( + "context" + "fmt" + + log "github.com/sirupsen/logrus" + "github.com/uptrace/bun" +) + +func init() { + Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error { + return addEncryptPayloadsToFlows(ctx, db) + }, func(ctx context.Context, db *bun.DB) error { + return removeEncryptPayloadsFromFlows(ctx, db) + }) +} + +func addEncryptPayloadsToFlows(ctx context.Context, db *bun.DB) error { + exists, err := columnExists(ctx, db, "flows", "encrypt_payloads") + if err != nil { + return fmt.Errorf("failed to check if encrypt_payloads column exists: %v", err) + } + if !exists { + _, err := db.NewAddColumn(). + Table("flows"). + ColumnExpr("encrypt_payloads BOOL DEFAULT true"). + Exec(ctx) + + if err != nil { + return fmt.Errorf("failed to add encrypt_payloads column to flows table: %v", err) + } + } else { + log.Debug("encrypt_payloads column already exists in flows table") + } + + exists, err = columnExists(ctx, db, "flows", "encrypt_executions") + if err != nil { + return fmt.Errorf("failed to check if encrypt_executions column exists: %v", err) + } + if !exists { + _, err := db.NewAddColumn(). + Table("flows"). + ColumnExpr("encrypt_executions BOOL DEFAULT true"). + Exec(ctx) + + if err != nil { + return fmt.Errorf("failed to add encrypt_executions column to flows table: %v", err) + } + } else { + log.Debug("encrypt_executions column already exists in flows table") + } + + return nil +} + +func removeEncryptPayloadsFromFlows(ctx context.Context, db *bun.DB) error { + exists, err := columnExists(ctx, db, "flows", "encrypt_payloads") + if err != nil { + return fmt.Errorf("failed to check if encrypt_payloads column exists: %v", err) + } + if exists { + _, err := db.NewDropColumn(). + Table("flows"). + Column("encrypt_payloads"). + Exec(ctx) + + if err != nil { + return fmt.Errorf("failed to remove encrypt_payloads column from flows table: %v", err) + } + } else { + log.Debug("encrypt_payloads column already removed from flows table") + } + + exists, err = columnExists(ctx, db, "flows", "encrypt_executions") + if err != nil { + return fmt.Errorf("failed to check if encrypt_executions column exists: %v", err) + } + if exists { + _, err := db.NewDropColumn(). + Table("flows"). + Column("encrypt_executions"). + Exec(ctx) + + if err != nil { + return fmt.Errorf("failed to remove encrypt_executions column from flows table: %v", err) + } + } else { + log.Debug("encrypt_executions column already removed from flows table") + } + + return nil +} diff --git a/services/backend/database/migrations/4_encrypted_flow_components.go b/services/backend/database/migrations/4_encrypted_flow_components.go new file mode 100644 index 00000000..73c319bf --- /dev/null +++ b/services/backend/database/migrations/4_encrypted_flow_components.go @@ -0,0 +1,93 @@ +package migrations + +import ( + "context" + "fmt" + + log "github.com/sirupsen/logrus" + "github.com/uptrace/bun" +) + +func init() { + Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error { + return addEncryptedToFlowComponents(ctx, db) + }, func(ctx context.Context, db *bun.DB) error { + return removeEncryptedFromFlowComponents(ctx, db) + }) +} + +func addEncryptedToFlowComponents(ctx context.Context, db *bun.DB) error { + exists, err := columnExists(ctx, db, "payloads", "encrypted") + if err != nil { + return fmt.Errorf("failed to check if encrypted column exists: %v", err) + } + if !exists { + _, err := db.NewAddColumn(). + Table("payloads"). + ColumnExpr("encrypted BOOL DEFAULT false"). + Exec(ctx) + + if err != nil { + return fmt.Errorf("failed to add encrypted column to payloads table: %v", err) + } + } else { + log.Debug("encrypted column already exists in payloads table") + } + + exists, err = columnExists(ctx, db, "execution_steps", "encrypted") + if err != nil { + return fmt.Errorf("failed to check if encrypted column exists: %v", err) + } + if !exists { + _, err := db.NewAddColumn(). + Table("execution_steps"). + ColumnExpr("encrypted BOOL DEFAULT false"). + Exec(ctx) + + if err != nil { + return fmt.Errorf("failed to add encrypted column to execution_steps table: %v", err) + } + } else { + log.Debug("encrypted column already exists in execution_steps table") + } + + return nil +} + +func removeEncryptedFromFlowComponents(ctx context.Context, db *bun.DB) error { + exists, err := columnExists(ctx, db, "payloads", "encrypted") + if err != nil { + return fmt.Errorf("failed to check if encrypted column exists: %v", err) + } + if exists { + _, err := db.NewDropColumn(). + Table("payloads"). + Column("encrypted"). + Exec(ctx) + + if err != nil { + return fmt.Errorf("failed to remove encrypted column from payloads table: %v", err) + } + } else { + log.Debug("encrypted column already removed from payloads table") + } + + exists, err = columnExists(ctx, db, "execution_steps", "encrypted") + if err != nil { + return fmt.Errorf("failed to check if encrypted column exists: %v", err) + } + if exists { + _, err := db.NewDropColumn(). + Table("execution_steps"). + Column("encrypted"). + Exec(ctx) + + if err != nil { + return fmt.Errorf("failed to remove encrypted column from execution_steps table: %v", err) + } + } else { + log.Debug("encrypted column already removed from execution_steps table") + } + + return nil +} diff --git a/services/backend/functions/encryption/execution_step_action_message.go b/services/backend/functions/encryption/execution_step_action_message.go new file mode 100644 index 00000000..00fc1de0 --- /dev/null +++ b/services/backend/functions/encryption/execution_step_action_message.go @@ -0,0 +1,61 @@ +package encryption + +import ( + "alertflow-backend/config" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "io" +) + +func EncryptExecutionStepActionMessage(messages []string) ([]string, error) { + block, err := aes.NewCipher([]byte(config.Config.Encryption.Key)) + if err != nil { + return nil, err + } + + for i := range messages { + plaintext := []byte(messages[i]) + ciphertext := make([]byte, aes.BlockSize+len(plaintext)) + iv := ciphertext[:aes.BlockSize] + + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return nil, err + } + + stream := cipher.NewCFBEncrypter(block, iv) + stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext) + + // Encode the ciphertext as base64 to ensure it can be stored as JSON + encodedCiphertext := base64.StdEncoding.EncodeToString(ciphertext) + messages[i] = encodedCiphertext + } + + return messages, nil +} + +func DecryptExecutionStepActionMessage(encryptedMessage []string) ([]string, error) { + block, err := aes.NewCipher([]byte(config.Config.Encryption.Key)) + if err != nil { + return nil, err + } + + for i := range encryptedMessage { + encodedCiphertext := encryptedMessage[i] + ciphertext, err := base64.StdEncoding.DecodeString(encodedCiphertext) + if err != nil { + return nil, err + } + + iv := ciphertext[:aes.BlockSize] + ciphertext = ciphertext[aes.BlockSize:] + + stream := cipher.NewCFBDecrypter(block, iv) + stream.XORKeyStream(ciphertext, ciphertext) + + encryptedMessage[i] = string(ciphertext) + } + + return encryptedMessage, nil +} diff --git a/services/backend/functions/encryption/payload.go b/services/backend/functions/encryption/payload.go new file mode 100644 index 00000000..7d43b380 --- /dev/null +++ b/services/backend/functions/encryption/payload.go @@ -0,0 +1,63 @@ +package encryption + +import ( + "alertflow-backend/config" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/json" + "io" +) + +func EncryptPayload(payload json.RawMessage) (json.RawMessage, error) { + block, err := aes.NewCipher([]byte(config.Config.Encryption.Key)) + if err != nil { + return nil, err + } + + plaintext := []byte(payload) + ciphertext := make([]byte, aes.BlockSize+len(plaintext)) + iv := ciphertext[:aes.BlockSize] + + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return nil, err + } + + stream := cipher.NewCFBEncrypter(block, iv) + stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext) + + // Encode the ciphertext as base64 to ensure it can be stored as JSON + encodedCiphertext := base64.StdEncoding.EncodeToString(ciphertext) + encryptedPayload, err := json.Marshal(encodedCiphertext) + if err != nil { + return nil, err + } + + return json.RawMessage(encryptedPayload), nil +} + +func DecryptPayload(payload json.RawMessage) (json.RawMessage, error) { + block, err := aes.NewCipher([]byte(config.Config.Encryption.Key)) + if err != nil { + return nil, err + } + + var encodedCiphertext string + if err := json.Unmarshal(payload, &encodedCiphertext); err != nil { + return nil, err + } + + ciphertext, err := base64.StdEncoding.DecodeString(encodedCiphertext) + if err != nil { + return nil, err + } + + iv := ciphertext[:aes.BlockSize] + ciphertext = ciphertext[aes.BlockSize:] + + stream := cipher.NewCFBDecrypter(block, iv) + stream.XORKeyStream(ciphertext, ciphertext) + + return json.RawMessage(ciphertext), nil +} diff --git a/services/backend/handlers/executions/create_step.go b/services/backend/handlers/executions/create_step.go index 8306a187..3377e970 100644 --- a/services/backend/handlers/executions/create_step.go +++ b/services/backend/handlers/executions/create_step.go @@ -1,6 +1,7 @@ package executions import ( + "alertflow-backend/functions/encryption" "alertflow-backend/functions/httperror" "alertflow-backend/models" "net/http" @@ -17,8 +18,34 @@ func CreateStep(context *gin.Context, db *bun.DB) { return } + // get parent execution data + var execution models.Executions + err := db.NewSelect().Model(&execution).Column("flow_id").Where("id = ?", step.ExecutionID).Scan(context) + if err != nil { + httperror.InternalServerError(context, "Error fetching parent execution data", err) + return + } + // get flow data + var flow models.Flows + err = db.NewSelect().Model(&flow).Where("id = ?", execution.FlowID).Scan(context) + if err != nil { + httperror.InternalServerError(context, "Error fetching flow data", err) + return + } + + // check for ecryption + if flow.EncryptExecutions && step.ActionMessages != nil && len(step.ActionMessages) > 0 { + step.ActionMessages, err = encryption.EncryptExecutionStepActionMessage(step.ActionMessages) + if err != nil { + httperror.InternalServerError(context, "Error encrypting execution step action messages", err) + return + } + + step.Encrypted = true + } + step.ID = uuid.New() - _, err := db.NewInsert().Model(&step).Exec(context) + _, err = db.NewInsert().Model(&step).Exec(context) if err != nil { httperror.InternalServerError(context, "Error creating execution step on db", err) return diff --git a/services/backend/handlers/executions/get_step.go b/services/backend/handlers/executions/get_step.go index f79929b3..319d3e1e 100644 --- a/services/backend/handlers/executions/get_step.go +++ b/services/backend/handlers/executions/get_step.go @@ -1,6 +1,7 @@ package executions import ( + "alertflow-backend/functions/encryption" "alertflow-backend/functions/httperror" "alertflow-backend/models" "net/http" @@ -20,5 +21,13 @@ func GetStep(context *gin.Context, db *bun.DB) { return } + if step.Encrypted { + step.ActionMessages, err = encryption.DecryptExecutionStepActionMessage(step.ActionMessages) + if err != nil { + httperror.InternalServerError(context, "Error decrypting execution step action messages", err) + return + } + } + context.JSON(http.StatusOK, gin.H{"step": step}) } diff --git a/services/backend/handlers/executions/get_steps.go b/services/backend/handlers/executions/get_steps.go index a23297cb..3ad6a934 100644 --- a/services/backend/handlers/executions/get_steps.go +++ b/services/backend/handlers/executions/get_steps.go @@ -1,6 +1,7 @@ package executions import ( + "alertflow-backend/functions/encryption" "alertflow-backend/functions/httperror" "alertflow-backend/models" "net/http" @@ -19,5 +20,15 @@ func GetSteps(context *gin.Context, db *bun.DB) { return } + for i := range steps { + if steps[i].Encrypted { + steps[i].ActionMessages, err = encryption.DecryptExecutionStepActionMessage(steps[i].ActionMessages) + if err != nil { + httperror.InternalServerError(context, "Error decrypting execution step action messages", err) + return + } + } + } + context.JSON(http.StatusOK, gin.H{"steps": steps}) } diff --git a/services/backend/handlers/executions/update_step.go b/services/backend/handlers/executions/update_step.go index b386c4a7..75cc6248 100644 --- a/services/backend/handlers/executions/update_step.go +++ b/services/backend/handlers/executions/update_step.go @@ -1,6 +1,7 @@ package executions import ( + "alertflow-backend/functions/encryption" "alertflow-backend/functions/httperror" "alertflow-backend/models" "net/http" @@ -26,6 +27,30 @@ func UpdateStep(context *gin.Context, db *bun.DB) { return } + // get parent execution data + var execution models.Executions + err = db.NewSelect().Model(&execution).Column("flow_id").Where("id = ?", dbStep.ExecutionID).Scan(context) + if err != nil { + httperror.InternalServerError(context, "Error fetching parent execution data", err) + return + } + // get flow data + var flow models.Flows + err = db.NewSelect().Model(&flow).Where("id = ?", execution.FlowID).Scan(context) + if err != nil { + httperror.InternalServerError(context, "Error fetching flow data", err) + return + } + + // check for ecryption and decrypt if needed + if flow.EncryptExecutions && dbStep.ActionMessages != nil && len(dbStep.ActionMessages) > 0 { + dbStep.ActionMessages, err = encryption.DecryptExecutionStepActionMessage(dbStep.ActionMessages) + if err != nil { + httperror.InternalServerError(context, "Error decrypting execution step action messages", err) + return + } + } + // append new message to existing step.ActionMessages = append(dbStep.ActionMessages, step.ActionMessages...) @@ -41,6 +66,17 @@ func UpdateStep(context *gin.Context, db *bun.DB) { step.RunnerID = dbStep.RunnerID } + // check for ecryption and encrypt if needed + if flow.EncryptExecutions && step.ActionMessages != nil && len(step.ActionMessages) > 0 { + step.ActionMessages, err = encryption.EncryptExecutionStepActionMessage(step.ActionMessages) + if err != nil { + httperror.InternalServerError(context, "Error encrypting execution step action messages", err) + return + } + + step.Encrypted = true + } + _, err = db.NewUpdate().Model(&step).Column( "action_messages", "runner_id", @@ -62,6 +98,7 @@ func UpdateStep(context *gin.Context, db *bun.DB) { "finished", "started_at", "finished_at", + "encrypted", ).Where("id = ?", stepID).Exec(context) if err != nil { httperror.InternalServerError(context, "Error updating step on db", err) diff --git a/services/backend/handlers/flows/create_payload.go b/services/backend/handlers/flows/create_payload.go index c1198917..46d00ba0 100644 --- a/services/backend/handlers/flows/create_payload.go +++ b/services/backend/handlers/flows/create_payload.go @@ -1,6 +1,8 @@ package flows import ( + "alertflow-backend/config" + "alertflow-backend/functions/encryption" functions "alertflow-backend/functions/flow" "alertflow-backend/functions/httperror" "alertflow-backend/models" @@ -22,7 +24,7 @@ func CreatePayload(context *gin.Context, db *bun.DB) { } var flow models.Flows - flowCount, err := db.NewSelect().Model(&flow).Where("id = ?", payload.FlowID).Count(context) + flowCount, err := db.NewSelect().Model(&flow).Where("id = ?", payload.FlowID).ScanAndCount(context) if err != nil { httperror.InternalServerError(context, "Error collecting flow data from db", err) return @@ -33,7 +35,18 @@ func CreatePayload(context *gin.Context, db *bun.DB) { } payload.ID = uuid.New() - res, err := db.NewInsert().Model(&payload).Column("id", "payload", "flow_id", "runner_id", "endpoint").Exec(context) + + // encrypt payload if enabled + if flow.EncryptPayloads && config.Config.Encryption.Enabled { + payload.Payload, err = encryption.EncryptPayload(payload.Payload) + if err != nil { + httperror.InternalServerError(context, "Error encrypting payload", err) + return + } + payload.Encrypted = true + } + + res, err := db.NewInsert().Model(&payload).Column("id", "payload", "flow_id", "runner_id", "endpoint", "encrypted").Exec(context) if err != nil { httperror.InternalServerError(context, "Error creating payload on db", err) return diff --git a/services/backend/handlers/flows/get_payloads.go b/services/backend/handlers/flows/get_payloads.go index 3b1250e6..ba47d7df 100644 --- a/services/backend/handlers/flows/get_payloads.go +++ b/services/backend/handlers/flows/get_payloads.go @@ -1,6 +1,7 @@ package flows import ( + "alertflow-backend/functions/encryption" "alertflow-backend/functions/gatekeeper" "alertflow-backend/functions/httperror" "alertflow-backend/models" @@ -41,5 +42,15 @@ func GetFlowPayloads(context *gin.Context, db *bun.DB) { return } + for i := range payloads { + if payloads[i].Encrypted { + payloads[i].Payload, err = encryption.DecryptPayload(payloads[i].Payload) + if err != nil { + httperror.InternalServerError(context, "Error decrypting payload", err) + return + } + } + } + context.JSON(http.StatusOK, gin.H{"payloads": payloads}) } diff --git a/services/backend/handlers/flows/update.go b/services/backend/handlers/flows/update.go index cb98ba6f..cd316c26 100644 --- a/services/backend/handlers/flows/update.go +++ b/services/backend/handlers/flows/update.go @@ -47,7 +47,7 @@ func UpdateFlow(context *gin.Context, db *bun.DB) { } flow.UpdatedAt = time.Now() - _, err = db.NewUpdate().Model(&flow).Column("name", "description", "project_id", "runner_id", "updated_at").Where("id = ?", flowID).Exec(context) + _, err = db.NewUpdate().Model(&flow).Column("name", "description", "project_id", "runner_id", "encrypt_action_params", "encrypt_payloads", "encrypt_executions", "updated_at").Where("id = ?", flowID).Exec(context) if err != nil { httperror.InternalServerError(context, "Error updating flow on db", err) return diff --git a/services/backend/handlers/payloads/get_payload.go b/services/backend/handlers/payloads/get_payload.go index 1dc9aeb4..53884267 100644 --- a/services/backend/handlers/payloads/get_payload.go +++ b/services/backend/handlers/payloads/get_payload.go @@ -1,6 +1,7 @@ package payloads import ( + "alertflow-backend/functions/encryption" "alertflow-backend/functions/gatekeeper" "alertflow-backend/functions/httperror" "alertflow-backend/models" @@ -40,5 +41,13 @@ func GetSingle(context *gin.Context, db *bun.DB) { return } + if payload.Encrypted { + payload.Payload, err = encryption.DecryptPayload(payload.Payload) + if err != nil { + httperror.InternalServerError(context, "Error decrypting payload", err) + return + } + } + context.JSON(http.StatusOK, gin.H{"payload": payload}) } diff --git a/services/backend/handlers/payloads/get_payloads.go b/services/backend/handlers/payloads/get_payloads.go index 00164b8f..1c808acb 100644 --- a/services/backend/handlers/payloads/get_payloads.go +++ b/services/backend/handlers/payloads/get_payloads.go @@ -1,6 +1,7 @@ package payloads import ( + "alertflow-backend/functions/encryption" "alertflow-backend/functions/httperror" "alertflow-backend/models" "net/http" @@ -18,5 +19,15 @@ func GetMultiple(context *gin.Context, db *bun.DB) { return } + for i := range payloads { + if payloads[i].Encrypted { + payloads[i].Payload, err = encryption.DecryptPayload(payloads[i].Payload) + if err != nil { + httperror.InternalServerError(context, "Error decrypting payload", err) + return + } + } + } + context.JSON(http.StatusOK, gin.H{"payloads": payloads}) } diff --git a/services/backend/main.go b/services/backend/main.go index 53c0871e..fb7f76a7 100644 --- a/services/backend/main.go +++ b/services/backend/main.go @@ -11,7 +11,7 @@ import ( log "github.com/sirupsen/logrus" ) -const version string = "1.0.0-beta13" +const version string = "1.0.0-beta14" var ( configFile = kingpin.Flag("config", "Config file").Short('c').Default("config.yaml").String() diff --git a/services/backend/models/execution_steps.go b/services/backend/models/execution_steps.go index 03ce6dc9..2497366c 100644 --- a/services/backend/models/execution_steps.go +++ b/services/backend/models/execution_steps.go @@ -39,4 +39,5 @@ type ExecutionSteps struct { CreatedAt time.Time `bun:"created_at,type:timestamptz,default:now()" json:"created_at"` StartedAt time.Time `bun:"started_at,type:timestamptz" json:"started_at"` FinishedAt time.Time `bun:"finished_at,type:timestamptz" json:"finished_at"` + Encrypted bool `bun:"encrypted,type:bool,default:false" json:"encrypted"` } diff --git a/services/backend/models/flows.go b/services/backend/models/flows.go index 369779a8..0c2009e5 100644 --- a/services/backend/models/flows.go +++ b/services/backend/models/flows.go @@ -25,6 +25,8 @@ type Flows struct { CreatedAt time.Time `bun:"created_at,type:timestamptz,default:now()" json:"created_at"` UpdatedAt time.Time `bun:"updated_at,type:timestamptz" json:"updated_at"` EncryptActionParams bool `bun:"encrypt_action_params,type:bool,default:true" json:"encrypt_action_params"` + EncryptPayloads bool `bun:"encrypt_payloads,type:bool,default:true" json:"encrypt_payloads"` + EncryptExecutions bool `bun:"encrypt_executions,type:bool,default:true" json:"encrypt_executions"` } type Actions struct { diff --git a/services/backend/models/payloads.go b/services/backend/models/payloads.go index 374d8000..1caf69e9 100644 --- a/services/backend/models/payloads.go +++ b/services/backend/models/payloads.go @@ -18,4 +18,5 @@ type Payloads struct { RunnerID string `bun:"runner_id,type:text,default:''" json:"runner_id"` Endpoint string `bun:"endpoint,type:text,default:''" json:"endpoint"` CreatedAt time.Time `bun:"created_at,type:timestamptz,default:now()" json:"created_at"` + Encrypted bool `bun:"encrypted,type:bool,default:false" json:"encrypted"` } diff --git a/services/frontend/components/dashboard/flows/flow/encryption.tsx b/services/frontend/components/dashboard/flows/flow/encryption.tsx new file mode 100644 index 00000000..251bd2fc --- /dev/null +++ b/services/frontend/components/dashboard/flows/flow/encryption.tsx @@ -0,0 +1,133 @@ +import { Card, CardBody, Spacer, Switch } from "@nextui-org/react"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; + +import UpdateFlow from "@/lib/fetch/flow/PUT/UpdateFlow"; +import ErrorCard from "@/components/error/ErrorCard"; + +export default function FlowEncryption({ flow }: { flow: any }) { + const router = useRouter(); + + const [encryptActionParams, setEncryptActionParams] = useState( + flow.encrypt_action_params, + ); + const [encryptPayloads, setEncryptPayloads] = useState(flow.encrypt_payloads); + const [encryptExecutions, setEncryptExecutions] = useState( + flow.encrypt_executions, + ); + + const [error, setError] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + + useEffect(() => { + setEncryptActionParams(flow.encrypt_action_params); + setEncryptPayloads(flow.encrypt_payloads); + setEncryptExecutions(flow.encrypt_executions); + }, [flow]); + + useEffect(() => { + if ( + encryptActionParams === flow.encrypt_action_params && + encryptPayloads === flow.encrypt_payloads && + encryptExecutions === flow.encrypt_executions + ) { + return; + } + updateFlow(); + }, [encryptActionParams, encryptPayloads, encryptExecutions]); + + async function updateFlow() { + const response = (await UpdateFlow( + flow.id, + flow.name, + flow.description, + flow.project_id, + flow.runner_id, + encryptActionParams, + encryptPayloads, + encryptExecutions, + )) as any; + + if (!response) { + setError(true); + setErrorMessage("An error occurred while updating the flow"); + + return; + } + + if (response.success) { + router.refresh(); + toast.success("Flow updated successfully"); + } else { + setError(true); + setErrorMessage(response.message); + toast.error("Failed to update flow"); + } + } + + return ( + <> + {error && } +
+ + +
+

Encrypted Action Parameters

+

+ All action parameters will be encrypted. +

+
+ + { + setEncryptActionParams(value); + }} + /> +
+
+ + + +
+

Encrypted Payloads

+

+ Incoming payloads will be encrypted stored on the db when they + reached the backend +

+
+ + { + setEncryptPayloads(value); + }} + /> +
+
+ + + +
+

Encypted Executions

+

+ All execution action messages will be stored encrypted on the db +

+
+ + { + setEncryptExecutions(value); + }} + /> +
+
+
+ + ); +} diff --git a/services/frontend/components/dashboard/flows/flow/execution/execution.tsx b/services/frontend/components/dashboard/flows/flow/execution/execution.tsx index 0a92d554..dfb33335 100644 --- a/services/frontend/components/dashboard/flows/flow/execution/execution.tsx +++ b/services/frontend/components/dashboard/flows/flow/execution/execution.tsx @@ -63,6 +63,8 @@ export function Execution({ flow, execution, runners, userDetails }: any) { }); }, [execution]); + console.log(steps); + function status(step: any) { if (step.pending) { return "Pending"; @@ -445,27 +447,45 @@ export function Execution({ flow, execution, runners, userDetails }: any) {

); - case "data": + case "message": return ( <> {step.pending ? (

Step not started yet

) : (
- - {step.action_messages.map((data: any, index: any) => ( -

- - {data} -

- ))} -
+
+ + {step.action_messages.map((data: any, index: any) => ( +

+ + {data} +

+ ))} +
+ {step.encrypted && ( + + + + + + )} +
{step.interactive && !step.interacted && (