From cd276dfb30eb056a9a6fd713ebee08966e6d453d Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Mon, 11 Aug 2025 15:07:33 +0200 Subject: [PATCH 01/39] Add Key Management Store to the SSH capability This is done via a .env flag and directory indication. We use the kms property of the user_auth field in CACAO for the reference to the key. The key identifier is just the name of the file in the directory as given in .env. This is convenient for persistence and interaction with other tasks. The keys are cached. There is room for an api interacting with the system. There is also some extra information in the ssh logging, which helps with identifying errors in the ssh layer. --- .env.example | 5 + internal/controller/controller.go | 9 ++ pkg/core/capability/ssh/keymanagement.go | 158 +++++++++++++++++++++++ pkg/core/capability/ssh/ssh.go | 41 ++++-- 4 files changed, 202 insertions(+), 11 deletions(-) create mode 100644 pkg/core/capability/ssh/keymanagement.go diff --git a/.env.example b/.env.example index 83a5bc2f..3af2fe22 100644 --- a/.env.example +++ b/.env.example @@ -35,3 +35,8 @@ AUTH_ENABLED: false OIDC_PROVIDER: "https://authentikuri:9443/application/o/soarca/" OIDC_CLIENT_ID: "some client ID" OIDC_SKIP_TLS_VERIFY: false + +# SSH Key Management +ENABLE_SSH_KMS: false +SSH_KMS_DIR: "/ssh-kms/" + diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 69ebe33e..2da9cf97 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -271,6 +271,15 @@ func initializeCore(app *gin.Engine) error { return err } + if utils.GetEnv("ENABLE_SSH_KMS", "false") == "true" { + kms_dir := utils.GetEnv("SSH_KMS_DIR", ".ssh/") + err = ssh.InitKeyManagement(kms_dir) + if err != nil { + log.Error(err) + return err + } + } + // Manual capability native routes routes.Manual(app, mainInteraction) diff --git a/pkg/core/capability/ssh/keymanagement.go b/pkg/core/capability/ssh/keymanagement.go new file mode 100644 index 00000000..9e238c66 --- /dev/null +++ b/pkg/core/capability/ssh/keymanagement.go @@ -0,0 +1,158 @@ +package ssh + +import ( + "fmt" + "os" + "path" + "slices" + "soarca/pkg/utils" + "strings" + + "golang.org/x/crypto/ssh" +) + +type KeyPair struct { + Public ssh.PublicKey + Private ssh.Signer +} +type KeyManagement struct { + underlying_dir string + cached_keys map[string]KeyPair +} + +var keyManagement *KeyManagement + +func InitKeyManagement(underlying_dir string) error { + keyManagement = &KeyManagement{ + underlying_dir: underlying_dir, + } + return keyManagement.Refresh() +} + +func (management *KeyManagement) GetKeyPair(name string) (*KeyPair, error) { + keypair, ok := management.cached_keys[name] + if !ok { + return nil, fmt.Errorf("Could not find keypair for %s", name) + } + return &keypair, nil +} +func (management *KeyManagement) GetPrivate(name string) (ssh.Signer, error) { + keypair, err := management.GetKeyPair(name) + if err != nil { + return nil, err + } + return keypair.Private, nil +} +func parsePrivateKey(filename string) (ssh.Signer, error) { + log.Tracef("Opening private key from path %s", filename) + file, err := os.Open(filename) + if err != nil { + return nil, err + } + passphrase := utils.GetEnv("SSH_KMS_PASSPHRASE", "") + file_buffer := make([]byte, 2048) + if _, err := file.Read(file_buffer); err != nil { + return nil, err + } + if passphrase == "" { + return ssh.ParsePrivateKey(file_buffer) + } else { + return ssh.ParsePrivateKeyWithPassphrase(file_buffer, []byte(passphrase)) + } +} +func parsePublicKey(filename string) (ssh.PublicKey, error) { + log.Tracef("Opening public key from path %s", filename) + file, err := os.Open(filename) + if err != nil { + return nil, err + } + file_buffer := make([]byte, 2048) + if _, err := file.Read(file_buffer); err != nil { + return nil, err + } + return ssh.ParsePublicKey(file_buffer) + +} +func (management *KeyManagement) Refresh() error { + management.cached_keys = make(map[string]KeyPair) + dir, err := os.Open(management.underlying_dir) + if err != nil { + return err + } + filenames, err := dir.Readdirnames(0) + if err != nil { + return err + } + for _, filename := range filenames { + if strings.HasSuffix(filename, ".pub") { + private_filename := strings.TrimSuffix(filename, ".pub") + if !slices.Contains(filenames, private_filename) { + return fmt.Errorf("found public key %s without corresponding private key (%s)", filename, private_filename) + } + private, err := parsePrivateKey(private_filename) + if err != nil { + return err + } + public, err := parsePublicKey(filename) + if err != nil { + return err + } + management.cached_keys[private_filename] = KeyPair{Public: public, Private: private} + } + } + return nil +} + +func (management *KeyManagement) Insert(public string, private string, name string) error { + for key, _ := range management.cached_keys { + if key == name { + return fmt.Errorf("Inserting key with name %s: already present!", name) + } + } + public_filename := path.Clean(name) + if strings.HasPrefix(public_filename, "..") { + return fmt.Errorf("Cannot insert key in parent of Key Management directory") + } + public_filename = path.Join(management.underlying_dir, public_filename) + private_filename := public_filename + ".pub" + public_file, err := os.Open(public_filename) + if err != nil { + return err + } + private_file, err := os.Open(private_filename) + if err != nil { + return err + } + n_read, err := public_file.WriteString(public) + if err != nil { + return err + } + if n_read < len(public) { + return fmt.Errorf("Write error: did not write whole public key file") + } + if _, err := private_file.WriteString(private); err != nil { + return err + } + if n_read < len(private) { + return fmt.Errorf("Write error: did not write whole private key file") + } + public_file.Close() + private_file.Close() + return management.insertInternal(public, private, name) +} + +func (management *KeyManagement) insertInternal(public string, private string, name string) error { + public_key, err := ssh.ParsePublicKey([]byte(public)) + if err != nil { + return err + } + passphrase := utils.GetEnv("SSH_KMS_PASSPHRASE", "") + var private_key ssh.Signer + if passphrase == "" { + private_key, err = ssh.ParsePrivateKey([]byte(private)) + } else { + private_key, err = ssh.ParsePrivateKeyWithPassphrase([]byte(private), []byte(passphrase)) + } + management.cached_keys[name] = KeyPair{Public: public_key, Private: private_key} + return nil +} diff --git a/pkg/core/capability/ssh/ssh.go b/pkg/core/capability/ssh/ssh.go index 25f3498a..fea9e20a 100644 --- a/pkg/core/capability/ssh/ssh.go +++ b/pkg/core/capability/ssh/ssh.go @@ -2,6 +2,7 @@ package ssh import ( "errors" + "fmt" "reflect" "soarca/pkg/core/capability" "soarca/pkg/models/cacao" @@ -69,7 +70,7 @@ func executeCommand(session *ssh.Session, response, err := session.Output(StripSshPrepend(command.Command)) if err != nil { - log.Error(err) + log.Errorf("Output: %s", err) return cacao.NewVariables(), err } results := cacao.NewVariables(cacao.Variable{Type: cacao.VariableTypeString, @@ -77,10 +78,11 @@ func executeCommand(session *ssh.Session, Value: string(response)}) log.Trace("Finished ssh execution will return the variables: ", results) sessionErr := session.Close() - if sessionErr != nil { - log.Error(sessionErr) + if sessionErr != nil && sessionErr.Error() != "EOF" { + // The ssh api is subtle, and it can happen that we get EOF as an error. + // This is likely not an error, as the session can also be closed by the host. + log.Errorf("Close: %s", sessionErr) } - return results, err } @@ -91,8 +93,22 @@ func getConfig(authentication cacao.AuthenticationInformation) (ssh.ClientConfig switch authentication.Type { case "user-auth": - config.Auth = []ssh.AuthMethod{ - ssh.Password(authentication.Password)} + if authentication.Password != "" { + config.Auth = []ssh.AuthMethod{ + ssh.Password(authentication.Password)} + } + if authentication.Kms { + if authentication.KmsKeyIdentifier == "" { + return config, fmt.Errorf("KMS indicated, but no kms_key_identifier given") + } + private_key, err := keyManagement.GetPrivate(authentication.KmsKeyIdentifier) + if err != nil { + return config, err + } + config.Auth = []ssh.AuthMethod{ + ssh.PublicKeys(private_key), + } + } return config, nil case "private-key": @@ -115,13 +131,13 @@ func getSession(config ssh.ClientConfig, target cacao.AgentTarget) (*ssh.Session host := CombinePortAndAddress(target.Address, target.Port) client, err := ssh.Dial("tcp", host, &config) if err != nil { - log.Error(err) + log.Errorf("Dialing: %s", err) return nil, nil, err } session, err := client.NewSession() if err != nil { - log.Error(err) + log.Errorf("Session: %s", err) close(client) return nil, nil, err } @@ -156,8 +172,11 @@ func CheckSshAuthenticationInfo(authentication cacao.AuthenticationInformation) switch authentication.Type { case "user-auth": - if strings.TrimSpace(authentication.Password) == "" { - return errors.New("password is empty") + if strings.TrimSpace(authentication.Password) == "" && !authentication.Kms { + return errors.New("password is empty and KMS is not indicated") + } + if authentication.Kms && strings.TrimSpace(authentication.KmsKeyIdentifier) == "" { + return errors.New("KMS is indicated but no identifier is given") } case "private-key": if strings.TrimSpace(authentication.PrivateKey) == "" { @@ -173,7 +192,7 @@ func close(client *ssh.Client) { if client != nil { err := client.Close() if err != nil { - log.Error(err) + log.Errorf("Closing: %s", err) } } } From 710414aa33f3b0db33a61d4f572de1338af49311 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Mon, 11 Aug 2025 15:31:54 +0200 Subject: [PATCH 02/39] Added example for the ssh KMS There is now an example playbook using the KMS system. There is also a testing docker config. This does not use the same setup as the existing ssh example unfortunately. The example requires some work to set up the KMS folder. You should first set up an example keypair in the deployment example, and then set up a folder for the KMS system containing those keys (either copying or using the deployment location). --- .../testing/ssh-kms-test/docker-compose.yml | 25 ++++ examples/ssh-kms-playbook.json | 117 ++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 deployments/docker/testing/ssh-kms-test/docker-compose.yml create mode 100644 examples/ssh-kms-playbook.json diff --git a/deployments/docker/testing/ssh-kms-test/docker-compose.yml b/deployments/docker/testing/ssh-kms-test/docker-compose.yml new file mode 100644 index 00000000..e818e462 --- /dev/null +++ b/deployments/docker/testing/ssh-kms-test/docker-compose.yml @@ -0,0 +1,25 @@ +services: + openssh-server: + image: lscr.io/linuxserver/openssh-server:latest + container_name: openssh-server + hostname: openssh-server #optional + environment: + - PUID=1000 + - PGID=1000 + - TZ=Etc/UTC + - PUBLIC_KEY=yourpublickey #optional + - PUBLIC_KEY_FILE=/path/to/file #optional + - PUBLIC_KEY_DIR=/path/to/directory/containing/_only_/pubkeys #optional + - PUBLIC_KEY_URL=https://github.com/username.keys #optional + - SUDO_ACCESS=false #optional + - PASSWORD_ACCESS=false #optional + - USER_PASSWORD=password #optional + - USER_PASSWORD_FILE=/path/to/file #optional + - USER_NAME=linuxserver.io #optional + - LOG_STDOUT= ssh.log#optional + volumes: + - /home/thijs/Code/SOARCA/kms/deployments/docker/ssh/config:/config + ports: + - 2222:2222 + restart: unless-stopped + diff --git a/examples/ssh-kms-playbook.json b/examples/ssh-kms-playbook.json new file mode 100644 index 00000000..b4b7af4d --- /dev/null +++ b/examples/ssh-kms-playbook.json @@ -0,0 +1,117 @@ +{ + "type": "playbook", + "spec_version": "cacao-2.0", + "id": "playbook--300270f9-0e64-42c8-93cc-0927edbe3ae7", + "name": "Example ssh", + "description": "This playbook demonstrates ssh functionality", + "playbook_types": [ + "notification" + ], + "created_by": "identity--96abab60-238a-44ff-8962-5806aa60cbce", + "created": "2023-11-20T15:56:00.123456Z", + "modified": "2023-11-20T15:56:00.123456Z", + "valid_from": "2023-11-20T15:56:00.123456Z", + "valid_until": "2123-11-20T15:56:00.123456Z", + "priority": 1, + "severity": 1, + "impact": 1, + "labels": [ + "soarca", + "ssh", + "example" + ], + "authentication_info_definitions": { + "user-auth--b7ddc2ea-9f6a-4e82-8eaa-be202e942090": { + "type": "user-auth", + "kms":true, + "kms_key_identifier": "test", + "username": "linuxserver.io" + } + }, + "agent_definitions": { + "soarca--00010001-1000-1000-a000-000100010001": { + "type": "soarca", + "name": "soarca-ssh" + } + }, + "target_definitions": { + "ssh--1c3900b4-f86b-430d-b415-12312b9e31f4": { + "type": "ssh", + "name": "system 1", + "address": { + "ipv4": [ + "127.0.0.1" + ] + }, + "port": "2222", + "authentication_info": "user-auth--b7ddc2ea-9f6a-4e82-8eaa-be202e942090" + } + }, + "external_references": [ + { + "name": "TNO COSSAS", + "description": "TNO COSSAS", + "source": "TNO COSSAS", + "url": "https://cossas-project.org" + } + ], + "workflow_start": "start--9e7d62b2-88ac-4656-94e1-dbd4413ba008", + "workflow_exception": "end--a6f0b81e-affb-4bca-b4f6-a2d5af908958", + "workflow": { + "start--9e7d62b2-88ac-4656-94e1-dbd4413ba008": { + "type": "start", + "name": "Start ssh example", + "on_completion": "action--eb9372d4-d524-49fc-bf24-be26ea084779" + }, + "action--eb9372d4-d524-49fc-bf24-be26ea084779": { + "type": "action", + "name": "Execute command", + "description": "Execute command specified in variable", + "on_completion": "action--88f4c4df-fa96-44e6-b310-1c06d193ea55", + "commands": [ + { + "type": "ssh", + "command": "__command__:value" + } + ], + "targets": [ + "ssh--1c3900b4-f86b-430d-b415-12312b9e31f4" + ], + "agent": "soarca--00010001-1000-1000-a000-000100010001", + "step_variables": { + "__command__": { + "type": "string", + "value": "ls -la", + "constant": true + } + } + }, + "action--88f4c4df-fa96-44e6-b310-1c06d193ea55": { + "type": "action", + "name": "Touch file", + "description": "Touch file at path specified by variable", + "on_completion": "end--a6f0b81e-affb-4bca-b4f6-a2d5af908958", + "commands": [ + { + "type": "ssh", + "command": "touch __path__:value" + } + ], + "targets": [ + "ssh--1c3900b4-f86b-430d-b415-12312b9e31f4" + ], + "agent": "soarca--00010001-1000-1000-a000-000100010001", + "step_variables": { + "__path__": { + "type": "string", + "value": "touchy", + "constant": true + } + } + }, + "end--a6f0b81e-affb-4bca-b4f6-a2d5af908958": { + "type": "end", + "name": "End Flow" + } + } +} From 1d4f04db491c889f06dca8910312122e87a77182 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Tue, 12 Aug 2025 17:09:37 +0200 Subject: [PATCH 03/39] Added REST API for SSH key management This makes it possible to get the list of keys, add keys manually, and revoke keys. Revoking does not delete keys but just moves and renames the underlying files to prevent irrevertible mishaps. This should serve as a starting point for consumers such as soarca-gui. --- internal/controller/controller.go | 6 +- pkg/api/api.go | 17 +++++ pkg/api/keymanagement/keymanagement_api.go | 88 ++++++++++++++++++++++ pkg/core/capability/ssh/keymanagement.go | 48 +++++++++--- pkg/core/capability/ssh/ssh.go | 2 +- pkg/models/api/keymanagement.go | 9 +++ 6 files changed, 156 insertions(+), 14 deletions(-) create mode 100644 pkg/api/keymanagement/keymanagement_api.go create mode 100644 pkg/models/api/keymanagement.go diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 2da9cf97..55238404 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -273,11 +273,13 @@ func initializeCore(app *gin.Engine) error { if utils.GetEnv("ENABLE_SSH_KMS", "false") == "true" { kms_dir := utils.GetEnv("SSH_KMS_DIR", ".ssh/") - err = ssh.InitKeyManagement(kms_dir) + log.Trace("Setting up key management in directory ", kms_dir) + key_manager, err := ssh.InitKeyManagement(kms_dir) if err != nil { - log.Error(err) + log.Error("Failed to set up key management: ", err) return err } + routes.KeyManagement(app, key_manager) } // Manual capability native routes diff --git a/pkg/api/api.go b/pkg/api/api.go index f1dc9698..aabd9552 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -7,10 +7,12 @@ import ( "soarca/internal/controller/decomposer_controller" "soarca/internal/controller/informer" "soarca/internal/logger" + "soarca/pkg/api/keymanagement" playbook_handler "soarca/pkg/api/playbook" reporter_handler "soarca/pkg/api/reporter" status_handler "soarca/pkg/api/status" "soarca/pkg/core/capability/manual/interaction" + "soarca/pkg/core/capability/ssh" manual_handler "soarca/pkg/api/manual" @@ -53,6 +55,11 @@ func Manual(app *gin.Engine, interaction interaction.IInteractionStorage) { manualHandler := manual_handler.NewManualHandler(interaction) ManualRoutes(app, manualHandler) } +func KeyManagement(app *gin.Engine, key_manager *ssh.KeyManagement) { + log.Trace("Setting up key management routes") + keyManagement := keymanagement.NewKeyManagementHandler(key_manager) + KeyManagementRoutes(app, keyManagement) +} func Api(app *gin.Engine, controller decomposer_controller.IController, @@ -144,3 +151,13 @@ func ManualRoutes(route *gin.Engine, manualHandler *manual_handler.ManualHandler manualRoutes.POST("/continue", manualHandler.PostContinue) } } + +func KeyManagementRoutes(route *gin.Engine, keyManagementHandler *keymanagement.KeyManagementHandler) { + keyManagementRoutes := route.Group("/keymanagement") + { + keyManagementRoutes.GET("/", keyManagementHandler.GetKeys) + keyManagementRoutes.PUT("/:keyname", keyManagementHandler.AddKey) + keyManagementRoutes.POST("/refresh/", keyManagementHandler.Refresh) + keyManagementRoutes.DELETE("/:keyname", keyManagementHandler.RevokeKey) + } +} diff --git a/pkg/api/keymanagement/keymanagement_api.go b/pkg/api/keymanagement/keymanagement_api.go new file mode 100644 index 00000000..393f4911 --- /dev/null +++ b/pkg/api/keymanagement/keymanagement_api.go @@ -0,0 +1,88 @@ +package keymanagement + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "reflect" + "soarca/internal/logger" + "soarca/pkg/core/capability/ssh" + "soarca/pkg/models/api" + "strconv" + + "github.com/gin-gonic/gin" +) + +var log *logger.Log + +type Empty struct{} + +func init() { + log = logger.Logger(reflect.TypeOf(Empty{}).PkgPath(), logger.Info, "", logger.Json) +} + +type KeyManagementHandler struct { + Manager *ssh.KeyManagement +} + +func NewKeyManagementHandler(manager *ssh.KeyManagement) *KeyManagementHandler { + return &KeyManagementHandler{ + Manager: manager, + } +} + +func (handler *KeyManagementHandler) GetKeys(context *gin.Context) { + keyInfo := handler.Manager.ListAllNames() + context.JSON(http.StatusOK, keyInfo) +} + +func (handler *KeyManagementHandler) Refresh(context *gin.Context) { + err := handler.Manager.Refresh() + if err != nil { + log.Trace("Refreshing keys has failed: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to refresh keys", "POST /keymanagement/refresh") + return + } + context.JSON(http.StatusOK, Empty{}) +} + +func (handler *KeyManagementHandler) AddKey(context *gin.Context) { + keyname := context.Param("keyname") + jsonData, err := io.ReadAll(context.Request.Body) + if err != nil { + log.Trace("Submit key has failed: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to read json on server side", "POST /keymanagement") + return + } + var key api.KeyManagementKey + if err := json.Unmarshal(jsonData, &key); err != nil { + log.Trace("Submit key failed to unmarshal: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to marshall json on server side", "POST /keymanagement") + return + } + handler.Manager.Insert(key.Public, key.Private, keyname) + context.JSON(http.StatusOK, Empty{}) +} + +func (handler *KeyManagementHandler) RevokeKey(context *gin.Context) { + keyname := context.Param("keyname") + if err := handler.Manager.Revoke(keyname); err != nil { + log.Trace("Revoke key failed: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to revoke key", "DELETE /keymanagement") + return + } + context.JSON(http.StatusOK, gin.H{ + "status": 200, + "message": fmt.Sprintf("Successfuly removed key %s from SOARCA listing. To permanently remove key, delete the revoked key files in key management directory", keyname), + }) +} + +func SendErrorResponse(context *gin.Context, status int, message string, orginal_call string) { + msg := gin.H{ + "status": strconv.Itoa(status), + "message": message, + "original-call": orginal_call, + } + context.JSON(status, msg) +} diff --git a/pkg/core/capability/ssh/keymanagement.go b/pkg/core/capability/ssh/keymanagement.go index 9e238c66..be8189b5 100644 --- a/pkg/core/capability/ssh/keymanagement.go +++ b/pkg/core/capability/ssh/keymanagement.go @@ -7,6 +7,7 @@ import ( "slices" "soarca/pkg/utils" "strings" + "time" "golang.org/x/crypto/ssh" ) @@ -20,13 +21,14 @@ type KeyManagement struct { cached_keys map[string]KeyPair } -var keyManagement *KeyManagement +var globalKeyManagement *KeyManagement -func InitKeyManagement(underlying_dir string) error { - keyManagement = &KeyManagement{ +func InitKeyManagement(underlying_dir string) (*KeyManagement, error) { + globalKeyManagement = &KeyManagement{ underlying_dir: underlying_dir, } - return keyManagement.Refresh() + err := globalKeyManagement.Refresh() + return globalKeyManagement, err } func (management *KeyManagement) GetKeyPair(name string) (*KeyPair, error) { @@ -67,10 +69,11 @@ func parsePublicKey(filename string) (ssh.PublicKey, error) { return nil, err } file_buffer := make([]byte, 2048) - if _, err := file.Read(file_buffer); err != nil { + if _, err = file.Read(file_buffer); err != nil { return nil, err } - return ssh.ParsePublicKey(file_buffer) + key, _, _, _, err := ssh.ParseAuthorizedKey(file_buffer) + return key, err } func (management *KeyManagement) Refresh() error { @@ -89,13 +92,14 @@ func (management *KeyManagement) Refresh() error { if !slices.Contains(filenames, private_filename) { return fmt.Errorf("found public key %s without corresponding private key (%s)", filename, private_filename) } - private, err := parsePrivateKey(private_filename) + log.Trace("Found public key named ", private_filename) + private, err := parsePrivateKey(path.Join(management.underlying_dir, private_filename)) if err != nil { - return err + return fmt.Errorf("Private key parsing error: %s", err) } - public, err := parsePublicKey(filename) + public, err := parsePublicKey(path.Join(management.underlying_dir, filename)) if err != nil { - return err + return fmt.Errorf("Public key parsing error: %s", err) } management.cached_keys[private_filename] = KeyPair{Public: public, Private: private} } @@ -142,7 +146,7 @@ func (management *KeyManagement) Insert(public string, private string, name stri } func (management *KeyManagement) insertInternal(public string, private string, name string) error { - public_key, err := ssh.ParsePublicKey([]byte(public)) + public_key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(public)) if err != nil { return err } @@ -156,3 +160,25 @@ func (management *KeyManagement) insertInternal(public string, private string, n management.cached_keys[name] = KeyPair{Public: public_key, Private: private_key} return nil } +func (management *KeyManagement) ListAllNames() []string { + keys := make([]string, len(management.cached_keys)) + index := 0 + for key := range management.cached_keys { + keys[index] = key + index++ + } + return keys +} +func (management *KeyManagement) Revoke(keyname string) error { + if _, ok := management.cached_keys[keyname]; !ok { + return fmt.Errorf("Unknown key %s", keyname) + } + public_filename := path.Join(management.underlying_dir, keyname+".pub") + private_filename := path.Join(management.underlying_dir, keyname) + now := time.Now() + suffix := fmt.Sprintf(".revoked_%d-%d_%d-%s-%d", now.Second(), now.Hour(), now.Day(), now.Month().String(), now.Year()) + os.Rename(public_filename, public_filename+suffix) + os.Rename(private_filename, private_filename+suffix) + delete(management.cached_keys, keyname) + return nil +} diff --git a/pkg/core/capability/ssh/ssh.go b/pkg/core/capability/ssh/ssh.go index fea9e20a..9f2068eb 100644 --- a/pkg/core/capability/ssh/ssh.go +++ b/pkg/core/capability/ssh/ssh.go @@ -101,7 +101,7 @@ func getConfig(authentication cacao.AuthenticationInformation) (ssh.ClientConfig if authentication.KmsKeyIdentifier == "" { return config, fmt.Errorf("KMS indicated, but no kms_key_identifier given") } - private_key, err := keyManagement.GetPrivate(authentication.KmsKeyIdentifier) + private_key, err := globalKeyManagement.GetPrivate(authentication.KmsKeyIdentifier) if err != nil { return config, err } diff --git a/pkg/models/api/keymanagement.go b/pkg/models/api/keymanagement.go new file mode 100644 index 00000000..6ce88ccc --- /dev/null +++ b/pkg/models/api/keymanagement.go @@ -0,0 +1,9 @@ +package api + +type KeyManagementKeyList struct { + Keys []string `json:"keys"` +} +type KeyManagementKey struct { + Private string `json:"private"` + Public string `json:"public"` +} From 17c73722d32260da9e9f9660eb0051c5280b0e0c Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Wed, 13 Aug 2025 11:42:07 +0200 Subject: [PATCH 04/39] Added key management unit tests This also removes some bugs from the existing code --- .gitignore | 1 + pkg/core/capability/ssh/keymanagement.go | 71 ++++++++++++++-- pkg/core/capability/ssh/keymanagement_test.go | 82 +++++++++++++++++++ pkg/core/capability/ssh/ssh_test.go | 4 +- test/unittest/mocks/mock_utils/test-key | 7 ++ test/unittest/mocks/mock_utils/test-key.pub | 1 + 6 files changed, 157 insertions(+), 9 deletions(-) create mode 100644 pkg/core/capability/ssh/keymanagement_test.go create mode 100644 test/unittest/mocks/mock_utils/test-key create mode 100644 test/unittest/mocks/mock_utils/test-key.pub diff --git a/.gitignore b/.gitignore index 387a3367..4be741be 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ bin/* api/* **.env test/cacao/flatfile-db-example.json +pkg/core/capability/ssh/.ssh_test/ docs/public diff --git a/pkg/core/capability/ssh/keymanagement.go b/pkg/core/capability/ssh/keymanagement.go index be8189b5..84f37fd6 100644 --- a/pkg/core/capability/ssh/keymanagement.go +++ b/pkg/core/capability/ssh/keymanagement.go @@ -2,6 +2,7 @@ package ssh import ( "fmt" + "io" "os" "path" "slices" @@ -108,7 +109,7 @@ func (management *KeyManagement) Refresh() error { } func (management *KeyManagement) Insert(public string, private string, name string) error { - for key, _ := range management.cached_keys { + for key := range management.cached_keys { if key == name { return fmt.Errorf("Inserting key with name %s: already present!", name) } @@ -121,11 +122,25 @@ func (management *KeyManagement) Insert(public string, private string, name stri private_filename := public_filename + ".pub" public_file, err := os.Open(public_filename) if err != nil { - return err + if os.IsNotExist(err) { + public_file, err = os.Create(public_filename) + if err != nil { + return err + } + } else { + return err + } } private_file, err := os.Open(private_filename) if err != nil { - return err + if os.IsNotExist(err) { + private_file, err = os.Create(private_filename) + if err != nil { + return err + } + } else { + return err + } } n_read, err := public_file.WriteString(public) if err != nil { @@ -169,16 +184,58 @@ func (management *KeyManagement) ListAllNames() []string { } return keys } + +const revoked_directory string = ".revoked" + +func moveFile(source string, dest string) error { + source_file, err := os.Open(source) + if err != nil { + return err + } + defer source_file.Close() + dest_file, err := os.OpenFile(dest, os.O_WRONLY, 0) + if err != nil { + if os.IsNotExist(err) { + dest_file, err = os.Create(dest) + if err != nil { + return err + } + } else { + return err + } + } + defer dest_file.Close() + if _, err := io.Copy(dest_file, source_file); err != nil { + return err + } + return os.Remove(source) +} func (management *KeyManagement) Revoke(keyname string) error { if _, ok := management.cached_keys[keyname]; !ok { return fmt.Errorf("Unknown key %s", keyname) } - public_filename := path.Join(management.underlying_dir, keyname+".pub") - private_filename := path.Join(management.underlying_dir, keyname) + if _, err := os.ReadDir(path.Join(management.underlying_dir, revoked_directory)); err != nil { + if err := os.Mkdir(path.Join(management.underlying_dir, revoked_directory), 0777); err != nil { + return fmt.Errorf("Error while creating directory for revoked keys: %s", err) + } + } now := time.Now() suffix := fmt.Sprintf(".revoked_%d-%d_%d-%s-%d", now.Second(), now.Hour(), now.Day(), now.Month().String(), now.Year()) - os.Rename(public_filename, public_filename+suffix) - os.Rename(private_filename, private_filename+suffix) + + public_filename := path.Join(management.underlying_dir, keyname+".pub") + public_target := path.Join(management.underlying_dir, revoked_directory, keyname+".pub"+suffix) + log.Infof("Moving %s to %s", public_filename, public_target) + if err := moveFile(public_filename, public_target); err != nil { + return fmt.Errorf("Error while moving key: %s", err) + } + + private_filename := path.Join(management.underlying_dir, keyname) + private_target := path.Join(management.underlying_dir, revoked_directory, keyname+suffix) + log.Infof("Moving %s to %s", private_filename, private_target) + if err := moveFile(private_filename, private_target); err != nil { + return fmt.Errorf("Error while moving key: %s", err) + } + delete(management.cached_keys, keyname) return nil } diff --git a/pkg/core/capability/ssh/keymanagement_test.go b/pkg/core/capability/ssh/keymanagement_test.go new file mode 100644 index 00000000..6f931445 --- /dev/null +++ b/pkg/core/capability/ssh/keymanagement_test.go @@ -0,0 +1,82 @@ +package ssh + +import ( + "os" + "path" + "slices" + "testing" + + "github.com/stretchr/testify/assert" +) + +const testdir string = ".ssh_test" +const testkey string = "test-key" + +func testkey_dir() string { + return path.Join("..", "..", "..", "..", "test", "unittest", "mocks", "mock_utils") +} + +func init() { + err := os.Mkdir(testdir, 0777) + if err != nil { + if err.(*os.PathError).Err.Error() == "file exists" { + if err := os.RemoveAll(testdir); err != nil { + panic(err) + } + if err := os.Mkdir(testdir, 0777); err != nil { + panic(err) + } + } else { + panic(err) + } + } + if _, err = InitKeyManagement(testdir); err != nil { + panic(err) + } +} + +func TestRevoke(t *testing.T) { + addKey(t, testkey) + assert.True(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey)) + assert.Nil(t, globalKeyManagement.Revoke(testkey)) +} +func addKey(t *testing.T, keyname string) { + pubkey_path := path.Join(testkey_dir(), "test-key.pub") + privkey_path := path.Join(testkey_dir(), "test-key") + pubkey_file, err := os.Open(pubkey_path) + assert.Nil(t, err) + defer pubkey_file.Close() + + privkey_file, err := os.Open(privkey_path) + assert.Nil(t, err) + defer privkey_file.Close() + pubkey_buf := make([]byte, 2048) + privkey_buf := make([]byte, 2048) + _, err = pubkey_file.Read(pubkey_buf) + assert.Nil(t, err) + _, err = privkey_file.Read(privkey_buf) + assert.Nil(t, err) + assert.Nil(t, globalKeyManagement.Insert(string(pubkey_buf), string(privkey_buf), keyname)) +} + +func TestAddKey(t *testing.T) { + addKey(t, testkey) + assert.True(t, slices.Equal(globalKeyManagement.ListAllNames(), []string{testkey})) +} + +func copyFile(src string, dst string) error { + data, err := os.ReadFile(src) + if err != nil { + return err + } + return os.WriteFile(dst, data, 0666) +} +func TestRefresh(t *testing.T) { + assert.False(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey+"1")) + pubkey_path := path.Join(testkey_dir(), "test-key.pub") + privkey_path := path.Join(testkey_dir(), "test-key") + copyFile(pubkey_path, path.Join(testdir, testkey+"1.pub")) + copyFile(privkey_path, path.Join(testdir, testkey+"1")) + globalKeyManagement.Refresh() + assert.True(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey+"1")) +} diff --git a/pkg/core/capability/ssh/ssh_test.go b/pkg/core/capability/ssh/ssh_test.go index f5aeb5aa..2ba58aed 100644 --- a/pkg/core/capability/ssh/ssh_test.go +++ b/pkg/core/capability/ssh/ssh_test.go @@ -29,14 +29,14 @@ func TestAuthenticationValidationUserAuth(t *testing.T) { func TestAuthenticationValidationUserAuthMissingPassword(t *testing.T) { auth := cacao.AuthenticationInformation{Type: "user-auth", Username: "root"} result := CheckSshAuthenticationInfo(auth) - err := errors.New("password is empty") + err := errors.New("password is empty and KMS is not indicated") assert.Equal(t, result, err) } func TestAuthenticationValidationUserAuthSpacesAsPassword(t *testing.T) { auth := cacao.AuthenticationInformation{Type: "user-auth", Username: "root", Password: " "} result := CheckSshAuthenticationInfo(auth) - err := errors.New("password is empty") + err := errors.New("password is empty and KMS is not indicated") assert.Equal(t, result, err) } diff --git a/test/unittest/mocks/mock_utils/test-key b/test/unittest/mocks/mock_utils/test-key new file mode 100644 index 00000000..71809225 --- /dev/null +++ b/test/unittest/mocks/mock_utils/test-key @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACBs4o06933hic/ArsSo0fs9cUTk0AHc2vON1ZqS68Vf7gAAAJhCJkxRQiZM +UQAAAAtzc2gtZWQyNTUxOQAAACBs4o06933hic/ArsSo0fs9cUTk0AHc2vON1ZqS68Vf7g +AAAEBI7PvrlDTFnWuMJHGaLuUkulSpH/Ni378Y2vLZcpldxmzijTr3feGJz8CuxKjR+z1x +ROTQAdza843VmpLrxV/uAAAADnRoaWpzQFBDLTQ0MzE4AQIDBAUGBw== +-----END OPENSSH PRIVATE KEY----- diff --git a/test/unittest/mocks/mock_utils/test-key.pub b/test/unittest/mocks/mock_utils/test-key.pub new file mode 100644 index 00000000..d6e7b2c9 --- /dev/null +++ b/test/unittest/mocks/mock_utils/test-key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGzijTr3feGJz8CuxKjR+z1xROTQAdza843VmpLrxV/u thijs@PC-44318 From 2fcaeaebd00de13fc2cbe40b978be343a1bee9a1 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Wed, 13 Aug 2025 12:05:07 +0200 Subject: [PATCH 05/39] Improve example deployment --- .../testing/ssh-kms-test/docker-compose.yml | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/deployments/docker/testing/ssh-kms-test/docker-compose.yml b/deployments/docker/testing/ssh-kms-test/docker-compose.yml index e818e462..ee32b830 100644 --- a/deployments/docker/testing/ssh-kms-test/docker-compose.yml +++ b/deployments/docker/testing/ssh-kms-test/docker-compose.yml @@ -2,23 +2,15 @@ services: openssh-server: image: lscr.io/linuxserver/openssh-server:latest container_name: openssh-server - hostname: openssh-server #optional + hostname: openssh-server environment: - PUID=1000 - PGID=1000 - TZ=Etc/UTC - - PUBLIC_KEY=yourpublickey #optional - - PUBLIC_KEY_FILE=/path/to/file #optional - - PUBLIC_KEY_DIR=/path/to/directory/containing/_only_/pubkeys #optional - - PUBLIC_KEY_URL=https://github.com/username.keys #optional - - SUDO_ACCESS=false #optional - - PASSWORD_ACCESS=false #optional - - USER_PASSWORD=password #optional - - USER_PASSWORD_FILE=/path/to/file #optional - - USER_NAME=linuxserver.io #optional - - LOG_STDOUT= ssh.log#optional + - USER_NAME=linuxserver.io + - LOG_STDOUT= ssh.log volumes: - - /home/thijs/Code/SOARCA/kms/deployments/docker/ssh/config:/config + - ./config:/config ports: - 2222:2222 restart: unless-stopped From b769c8a1381f13d5f55c088940482a57ba7df579 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Wed, 13 Aug 2025 13:58:20 +0200 Subject: [PATCH 06/39] Added helper script for KMS example setup --- .env.example | 2 +- deployments/docker/testing/ssh-kms-test/prepare_keys.sh | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100755 deployments/docker/testing/ssh-kms-test/prepare_keys.sh diff --git a/.env.example b/.env.example index 3af2fe22..c69ca0fe 100644 --- a/.env.example +++ b/.env.example @@ -38,5 +38,5 @@ OIDC_SKIP_TLS_VERIFY: false # SSH Key Management ENABLE_SSH_KMS: false -SSH_KMS_DIR: "/ssh-kms/" +SSH_KMS_DIR: "deployments/docker/testing/ssh-kms-test/ssh-keystore/" diff --git a/deployments/docker/testing/ssh-kms-test/prepare_keys.sh b/deployments/docker/testing/ssh-kms-test/prepare_keys.sh new file mode 100755 index 00000000..21edf4c9 --- /dev/null +++ b/deployments/docker/testing/ssh-kms-test/prepare_keys.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -u +mkdir -p ssh-keystore/ +ssh-keygen -q -N "" -f ssh-keystore/test +mkdir -p config/.ssh/ +cat ssh-keystore/test.pub > config/.ssh/authorized_keys From a5c2a7d1f9c6524f45e068e17ed8b59dbd1a8a2a Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Wed, 13 Aug 2025 14:32:21 +0200 Subject: [PATCH 07/39] Updated linting errors --- pkg/api/keymanagement/keymanagement_api.go | 7 +++- pkg/core/capability/ssh/keymanagement.go | 41 ++++++++++++------- pkg/core/capability/ssh/keymanagement_test.go | 10 ++--- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/pkg/api/keymanagement/keymanagement_api.go b/pkg/api/keymanagement/keymanagement_api.go index 393f4911..0e0b04a9 100644 --- a/pkg/api/keymanagement/keymanagement_api.go +++ b/pkg/api/keymanagement/keymanagement_api.go @@ -61,7 +61,12 @@ func (handler *KeyManagementHandler) AddKey(context *gin.Context) { SendErrorResponse(context, http.StatusBadRequest, "Failed to marshall json on server side", "POST /keymanagement") return } - handler.Manager.Insert(key.Public, key.Private, keyname) + if err := handler.Manager.Insert(key.Public, key.Private, keyname); err != nil { + log.Trace("Submit key failed to insert: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to insert key on server side", "POST /keymanagement") + return + + } context.JSON(http.StatusOK, Empty{}) } diff --git a/pkg/core/capability/ssh/keymanagement.go b/pkg/core/capability/ssh/keymanagement.go index 84f37fd6..31e8d5e8 100644 --- a/pkg/core/capability/ssh/keymanagement.go +++ b/pkg/core/capability/ssh/keymanagement.go @@ -35,7 +35,7 @@ func InitKeyManagement(underlying_dir string) (*KeyManagement, error) { func (management *KeyManagement) GetKeyPair(name string) (*KeyPair, error) { keypair, ok := management.cached_keys[name] if !ok { - return nil, fmt.Errorf("Could not find keypair for %s", name) + return nil, fmt.Errorf("could not find keypair for %s", name) } return &keypair, nil } @@ -96,11 +96,11 @@ func (management *KeyManagement) Refresh() error { log.Trace("Found public key named ", private_filename) private, err := parsePrivateKey(path.Join(management.underlying_dir, private_filename)) if err != nil { - return fmt.Errorf("Private key parsing error: %s", err) + return fmt.Errorf("private key parsing error: %s", err) } public, err := parsePublicKey(path.Join(management.underlying_dir, filename)) if err != nil { - return fmt.Errorf("Public key parsing error: %s", err) + return fmt.Errorf("public key parsing error: %s", err) } management.cached_keys[private_filename] = KeyPair{Public: public, Private: private} } @@ -111,12 +111,12 @@ func (management *KeyManagement) Refresh() error { func (management *KeyManagement) Insert(public string, private string, name string) error { for key := range management.cached_keys { if key == name { - return fmt.Errorf("Inserting key with name %s: already present!", name) + return fmt.Errorf("inserting key with name %s: already present!", name) } } public_filename := path.Clean(name) if strings.HasPrefix(public_filename, "..") { - return fmt.Errorf("Cannot insert key in parent of Key Management directory") + return fmt.Errorf("cannot insert key in parent of Key Management directory") } public_filename = path.Join(management.underlying_dir, public_filename) private_filename := public_filename + ".pub" @@ -147,16 +147,20 @@ func (management *KeyManagement) Insert(public string, private string, name stri return err } if n_read < len(public) { - return fmt.Errorf("Write error: did not write whole public key file") + return fmt.Errorf("write error: did not write whole public key file") } if _, err := private_file.WriteString(private); err != nil { return err } if n_read < len(private) { - return fmt.Errorf("Write error: did not write whole private key file") + return fmt.Errorf("write error: did not write whole private key file") + } + if err := public_file.Close(); err != nil { + return err + } + if err := private_file.Close(); err != nil { + return err } - public_file.Close() - private_file.Close() return management.insertInternal(public, private, name) } @@ -172,6 +176,9 @@ func (management *KeyManagement) insertInternal(public string, private string, n } else { private_key, err = ssh.ParsePrivateKeyWithPassphrase([]byte(private), []byte(passphrase)) } + if err != nil { + return err + } management.cached_keys[name] = KeyPair{Public: public_key, Private: private_key} return nil } @@ -192,7 +199,6 @@ func moveFile(source string, dest string) error { if err != nil { return err } - defer source_file.Close() dest_file, err := os.OpenFile(dest, os.O_WRONLY, 0) if err != nil { if os.IsNotExist(err) { @@ -204,19 +210,24 @@ func moveFile(source string, dest string) error { return err } } - defer dest_file.Close() if _, err := io.Copy(dest_file, source_file); err != nil { return err } + if err := source_file.Close(); err != nil { + return err + } + if err := dest_file.Close(); err != nil { + return err + } return os.Remove(source) } func (management *KeyManagement) Revoke(keyname string) error { if _, ok := management.cached_keys[keyname]; !ok { - return fmt.Errorf("Unknown key %s", keyname) + return fmt.Errorf("unknown key %s", keyname) } if _, err := os.ReadDir(path.Join(management.underlying_dir, revoked_directory)); err != nil { if err := os.Mkdir(path.Join(management.underlying_dir, revoked_directory), 0777); err != nil { - return fmt.Errorf("Error while creating directory for revoked keys: %s", err) + return fmt.Errorf("error while creating directory for revoked keys: %s", err) } } now := time.Now() @@ -226,14 +237,14 @@ func (management *KeyManagement) Revoke(keyname string) error { public_target := path.Join(management.underlying_dir, revoked_directory, keyname+".pub"+suffix) log.Infof("Moving %s to %s", public_filename, public_target) if err := moveFile(public_filename, public_target); err != nil { - return fmt.Errorf("Error while moving key: %s", err) + return fmt.Errorf("error while moving key: %s", err) } private_filename := path.Join(management.underlying_dir, keyname) private_target := path.Join(management.underlying_dir, revoked_directory, keyname+suffix) log.Infof("Moving %s to %s", private_filename, private_target) if err := moveFile(private_filename, private_target); err != nil { - return fmt.Errorf("Error while moving key: %s", err) + return fmt.Errorf("error while moving key: %s", err) } delete(management.cached_keys, keyname) diff --git a/pkg/core/capability/ssh/keymanagement_test.go b/pkg/core/capability/ssh/keymanagement_test.go index 6f931445..89e86758 100644 --- a/pkg/core/capability/ssh/keymanagement_test.go +++ b/pkg/core/capability/ssh/keymanagement_test.go @@ -45,11 +45,9 @@ func addKey(t *testing.T, keyname string) { privkey_path := path.Join(testkey_dir(), "test-key") pubkey_file, err := os.Open(pubkey_path) assert.Nil(t, err) - defer pubkey_file.Close() privkey_file, err := os.Open(privkey_path) assert.Nil(t, err) - defer privkey_file.Close() pubkey_buf := make([]byte, 2048) privkey_buf := make([]byte, 2048) _, err = pubkey_file.Read(pubkey_buf) @@ -57,6 +55,8 @@ func addKey(t *testing.T, keyname string) { _, err = privkey_file.Read(privkey_buf) assert.Nil(t, err) assert.Nil(t, globalKeyManagement.Insert(string(pubkey_buf), string(privkey_buf), keyname)) + assert.Nil(t, privkey_file.Close()) + assert.Nil(t, pubkey_file.Close()) } func TestAddKey(t *testing.T) { @@ -75,8 +75,8 @@ func TestRefresh(t *testing.T) { assert.False(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey+"1")) pubkey_path := path.Join(testkey_dir(), "test-key.pub") privkey_path := path.Join(testkey_dir(), "test-key") - copyFile(pubkey_path, path.Join(testdir, testkey+"1.pub")) - copyFile(privkey_path, path.Join(testdir, testkey+"1")) - globalKeyManagement.Refresh() + assert.Nil(t, copyFile(pubkey_path, path.Join(testdir, testkey+"1.pub"))) + assert.Nil(t, copyFile(privkey_path, path.Join(testdir, testkey+"1"))) + assert.Nil(t, globalKeyManagement.Refresh()) assert.True(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey+"1")) } From 23ab2813739469b9afb66b730ea8c3f737d1dcdf Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Wed, 13 Aug 2025 14:48:44 +0200 Subject: [PATCH 08/39] Removed a public-private mixup --- pkg/core/capability/ssh/keymanagement.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/core/capability/ssh/keymanagement.go b/pkg/core/capability/ssh/keymanagement.go index 31e8d5e8..de6821ba 100644 --- a/pkg/core/capability/ssh/keymanagement.go +++ b/pkg/core/capability/ssh/keymanagement.go @@ -96,11 +96,11 @@ func (management *KeyManagement) Refresh() error { log.Trace("Found public key named ", private_filename) private, err := parsePrivateKey(path.Join(management.underlying_dir, private_filename)) if err != nil { - return fmt.Errorf("private key parsing error: %s", err) + return fmt.Errorf("private key (%s) parsing error: %s", private_filename, err) } public, err := parsePublicKey(path.Join(management.underlying_dir, filename)) if err != nil { - return fmt.Errorf("public key parsing error: %s", err) + return fmt.Errorf("public key (%s) parsing error: %s", filename, err) } management.cached_keys[private_filename] = KeyPair{Public: public, Private: private} } @@ -114,12 +114,12 @@ func (management *KeyManagement) Insert(public string, private string, name stri return fmt.Errorf("inserting key with name %s: already present!", name) } } - public_filename := path.Clean(name) - if strings.HasPrefix(public_filename, "..") { + private_filename := path.Clean(name) + if strings.HasPrefix(private_filename, "..") { return fmt.Errorf("cannot insert key in parent of Key Management directory") } - public_filename = path.Join(management.underlying_dir, public_filename) - private_filename := public_filename + ".pub" + private_filename = path.Join(management.underlying_dir, private_filename) + public_filename := private_filename + ".pub" public_file, err := os.Open(public_filename) if err != nil { if os.IsNotExist(err) { From dc1e841bef4e29134b1a5d5e9459b6182950743d Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Wed, 13 Aug 2025 14:53:10 +0200 Subject: [PATCH 09/39] Removed lint error --- pkg/core/capability/ssh/keymanagement.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/core/capability/ssh/keymanagement.go b/pkg/core/capability/ssh/keymanagement.go index de6821ba..ffb73e34 100644 --- a/pkg/core/capability/ssh/keymanagement.go +++ b/pkg/core/capability/ssh/keymanagement.go @@ -111,7 +111,7 @@ func (management *KeyManagement) Refresh() error { func (management *KeyManagement) Insert(public string, private string, name string) error { for key := range management.cached_keys { if key == name { - return fmt.Errorf("inserting key with name %s: already present!", name) + return fmt.Errorf("inserting key with name %s: already present", name) } } private_filename := path.Clean(name) From 3feb02f69cfea18f688afc8f515e7974134719bd Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Mon, 18 Aug 2025 12:08:25 +0200 Subject: [PATCH 10/39] Fixed the logger level configuration The Severity.fromString method did not do anything with the base it was called from. This meant that all logs took the global level instead of the per-package option. --- internal/logger/logger.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 4b4544b9..87963cc2 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -31,7 +31,7 @@ const ( Trace ) -func (severity Severity) fromString(name string) Severity { +func severityFromString(name string) Severity { nameToLower := strings.ToLower(name) switch nameToLower { case "panic": @@ -106,7 +106,12 @@ func Logger(name string, severity Severity, fileName FileName, format Format) *L setFormat(instance, globalLogFormat) - instance.SetLevel(logrus.Level(severity.fromString(globalLogSeverity))) + globalSeverityLevel := severityFromString(globalLogSeverity) + if globalSeverityLevel > severity { + instance.SetLevel(logrus.Level(globalSeverityLevel)) + } else { + instance.SetLevel(logrus.Level(severity)) + } if globalOperationMode == development { if fileName != "" { From 64e631a8c80766335bec11ab9af8e9e655fd4dfe Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Mon, 18 Aug 2025 16:59:19 +0200 Subject: [PATCH 11/39] Changed KMS to use database backend --- internal/controller/controller.go | 38 ++- .../database/keymanagement/keymanagement.go | 69 +++++ internal/database/memory/keymanagement.go | 41 +++ .../memory/{memory.go => playbook.go} | 18 +- .../{memory_test.go => playbook_test.go} | 12 +- internal/database/mongodb/mongo.go | 11 +- pkg/api/api.go | 12 +- pkg/api/keymanagement/keymanagement_api.go | 101 +++++-- pkg/core/capability/ssh/keymanagement.go | 252 ------------------ pkg/core/capability/ssh/keymanagement_test.go | 82 ------ pkg/core/capability/ssh/ssh.go | 30 +-- pkg/keymanagement/keymanagement.go | 122 +++++++++ pkg/keymanagement/keymanagement_test.go | 53 ++++ pkg/models/api/keymanagement.go | 5 +- pkg/models/keymanagement/keymanagent.go | 8 + 15 files changed, 441 insertions(+), 413 deletions(-) create mode 100644 internal/database/keymanagement/keymanagement.go create mode 100644 internal/database/memory/keymanagement.go rename internal/database/memory/{memory.go => playbook.go} (70%) rename internal/database/memory/{memory_test.go => playbook_test.go} (96%) delete mode 100644 pkg/core/capability/ssh/keymanagement.go delete mode 100644 pkg/core/capability/ssh/keymanagement_test.go create mode 100644 pkg/keymanagement/keymanagement.go create mode 100644 pkg/keymanagement/keymanagement_test.go create mode 100644 pkg/models/keymanagement/keymanagent.go diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 55238404..870faac8 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "reflect" + keymanagementrepository "soarca/internal/database/keymanagement" "soarca/internal/database/memory" "soarca/internal/logger" @@ -21,6 +22,7 @@ import ( "soarca/pkg/core/executors/condition" "soarca/pkg/core/executors/playbook_action" "soarca/pkg/extensions/soarca/assignment" + "soarca/pkg/keymanagement" "soarca/pkg/reporting/cases" "soarca/pkg/reporting/reporter" "soarca/pkg/utils" @@ -61,8 +63,10 @@ func init() { } type Controller struct { - finController finChannelController.IFinController - playbookRepo playbookrepository.IPlaybookRepository + finController finChannelController.IFinController + playbookRepo playbookrepository.IPlaybookRepository + keyManagementRepo keymanagementrepository.IKeyManagementRepository + keyManagement *keymanagement.KeyManagement } var mainController = Controller{} @@ -75,7 +79,7 @@ const defaultCacheSize int = 10 var mainInteraction = interaction.New(registerManualIntegration()) func (controller *Controller) NewDecomposer() decomposer.IDecomposer { - ssh := new(ssh.SshCapability) + ssh := &ssh.SshCapability{Keys: controller.keyManagement} capabilities := map[string]capability.ICapability{ssh.GetType(): ssh} skip, _ := strconv.ParseBool(utils.GetEnv("HTTP_SKIP_CERT_VALIDATION", "false")) @@ -143,6 +147,7 @@ func (controller *Controller) setupDatabase() error { initMongoDatabase, _ := strconv.ParseBool(utils.GetEnv("DATABASE", "false")) if initMongoDatabase { + log.Info("Setting up mongo database") mongo.LoadComponent() @@ -160,14 +165,22 @@ func (controller *Controller) setupDatabase() error { return err } controller.playbookRepo = playbookrepository.SetupPlaybookRepository(mongo.GetCacaoRepo(), mongo.DefaultLimitOpts()) + controller.keyManagementRepo = keymanagementrepository.SetupKeyManagementRepository(mongo.GetKeyManagementRepo(), mongo.DefaultLimitOpts()) } else { // Use in memory database - controller.playbookRepo = memory.New() + log.Info("Setting up in-memory database") + controller.playbookRepo = memory.NewPlaybookDatabase() + controller.keyManagementRepo = memory.NewKeyManagementDatabase() } return nil } +func (controller *Controller) setupKeyManagement() error { + controller.keyManagement = keymanagement.InitKeyManagement(controller.keyManagementRepo) + return nil +} + func (controller *Controller) GetDatabaseInstance() playbookrepository.IPlaybookRepository { return controller.playbookRepo } @@ -251,6 +264,12 @@ func initializeCore(app *gin.Engine) error { return err } + err = mainController.setupKeyManagement() + if err != nil { + log.Error("Failed to setup key management:", err) + return err + } + err = routes.Api(app, &mainController, &mainController) if err != nil { log.Error(err) @@ -271,16 +290,7 @@ func initializeCore(app *gin.Engine) error { return err } - if utils.GetEnv("ENABLE_SSH_KMS", "false") == "true" { - kms_dir := utils.GetEnv("SSH_KMS_DIR", ".ssh/") - log.Trace("Setting up key management in directory ", kms_dir) - key_manager, err := ssh.InitKeyManagement(kms_dir) - if err != nil { - log.Error("Failed to set up key management: ", err) - return err - } - routes.KeyManagement(app, key_manager) - } + routes.KeyManagement(app, mainController.keyManagement) // Manual capability native routes routes.Manual(app, mainInteraction) diff --git a/internal/database/keymanagement/keymanagement.go b/internal/database/keymanagement/keymanagement.go new file mode 100644 index 00000000..a3f0be88 --- /dev/null +++ b/internal/database/keymanagement/keymanagement.go @@ -0,0 +1,69 @@ +package keymanagementrepository + +import ( + "errors" + "soarca/internal/database" + "soarca/pkg/models/keymanagement" +) + +type IKeyManagementRepository interface { + GetKeyNames() ([]string, error) + Create(name string, keypair keymanagement.KeyPair) error + Read(id string) (keymanagement.KeyPair, error) + Update(id string, keypair keymanagement.KeyPair) error + Delete(id string) error +} + +type KeyManagementRepository struct { + db database.Database + options database.FindOptions +} + +type KeyPairEntry struct { + name string + keypair keymanagement.KeyPair +} + +func SetupKeyManagementRepository(db database.Database, options database.FindOptions) *KeyManagementRepository { + return &KeyManagementRepository{db: db, options: options} +} + +func (keymanagementRepo *KeyManagementRepository) GetKeyNames() ([]string, error) { + keys, err := keymanagementRepo.db.Find(nil) + if err != nil { + return nil, err + } + ret := []string{} + for _, key := range keys { + ret = append(ret, key.(KeyPairEntry).name) + } + return ret, nil +} + +func (keymanagementRepo *KeyManagementRepository) Create(name string, keypair keymanagement.KeyPair) error { + return keymanagementRepo.db.Create(KeyPairEntry{name, keypair}) +} + +func (keymanagementRepo *KeyManagementRepository) Read(id string) (keymanagement.KeyPair, error) { + returnedObject, err := keymanagementRepo.db.Read(id) + if err != nil { + return keymanagement.KeyPair{}, err + } + + keypair, ok := returnedObject.(keymanagement.KeyPair) + + if !ok { + err = errors.New("could not cast lookup object to keypair type") + return keymanagement.KeyPair{}, err + } + + return keypair, nil +} + +func (keymanagementRepo *KeyManagementRepository) Update(id string, keypair keymanagement.KeyPair) error { + return keymanagementRepo.db.Update(id, keypair) +} + +func (keymanagementRepo *KeyManagementRepository) Delete(id string) error { + return keymanagementRepo.db.Delete(id) +} diff --git a/internal/database/memory/keymanagement.go b/internal/database/memory/keymanagement.go new file mode 100644 index 00000000..fb89cc45 --- /dev/null +++ b/internal/database/memory/keymanagement.go @@ -0,0 +1,41 @@ +package memory + +import ( + "fmt" + "soarca/pkg/models/keymanagement" +) + +type InMemoryKeyManagementDatabase struct { + keys map[string]keymanagement.KeyPair +} + +func NewKeyManagementDatabase() *InMemoryKeyManagementDatabase { + return &InMemoryKeyManagementDatabase{keys: make(map[string]keymanagement.KeyPair)} +} + +func (database *InMemoryKeyManagementDatabase) GetKeyNames() ([]string, error) { + ret := []string{} + for key := range database.keys { + ret = append(ret, key) + } + return ret, nil +} +func (database *InMemoryKeyManagementDatabase) Create(name string, keypair keymanagement.KeyPair) error { + database.keys[name] = keypair + return nil +} +func (database *InMemoryKeyManagementDatabase) Read(id string) (keymanagement.KeyPair, error) { + keypair, ok := database.keys[id] + if !ok { + return keymanagement.KeyPair{}, fmt.Errorf("Could not find key named %s", id) + } + return keypair, nil +} +func (database *InMemoryKeyManagementDatabase) Update(id string, keypair keymanagement.KeyPair) error { + database.keys[id] = keypair + return nil +} +func (database *InMemoryKeyManagementDatabase) Delete(id string) error { + delete(database.keys, id) + return nil +} diff --git a/internal/database/memory/memory.go b/internal/database/memory/playbook.go similarity index 70% rename from internal/database/memory/memory.go rename to internal/database/memory/playbook.go index ef0bddc8..ad94591f 100644 --- a/internal/database/memory/memory.go +++ b/internal/database/memory/playbook.go @@ -7,15 +7,15 @@ import ( "soarca/pkg/models/decoder" ) -type InMemoryDatabase struct { +type InMemoryPlaybookDatabase struct { playbooks map[string]cacao.Playbook } -func New() *InMemoryDatabase { - return &InMemoryDatabase{playbooks: make(map[string]cacao.Playbook)} +func NewPlaybookDatabase() *InMemoryPlaybookDatabase { + return &InMemoryPlaybookDatabase{playbooks: make(map[string]cacao.Playbook)} } -func (memory *InMemoryDatabase) GetPlaybooks() ([]cacao.Playbook, error) { +func (memory *InMemoryPlaybookDatabase) GetPlaybooks() ([]cacao.Playbook, error) { size := len(memory.playbooks) playbookList := make([]cacao.Playbook, 0, size) for _, playbook := range memory.playbooks { @@ -25,7 +25,7 @@ func (memory *InMemoryDatabase) GetPlaybooks() ([]cacao.Playbook, error) { return playbookList, nil } -func (memory *InMemoryDatabase) GetPlaybookMetas() ([]api.PlaybookMeta, error) { +func (memory *InMemoryPlaybookDatabase) GetPlaybookMetas() ([]api.PlaybookMeta, error) { size := len(memory.playbooks) playbookList := make([]api.PlaybookMeta, 0, size) for _, playbook := range memory.playbooks { @@ -40,7 +40,7 @@ func (memory *InMemoryDatabase) GetPlaybookMetas() ([]api.PlaybookMeta, error) { return playbookList, nil } -func (memory *InMemoryDatabase) Create(json *[]byte) (cacao.Playbook, error) { +func (memory *InMemoryPlaybookDatabase) Create(json *[]byte) (cacao.Playbook, error) { if json == nil { return cacao.Playbook{}, errors.New("empty input") @@ -57,7 +57,7 @@ func (memory *InMemoryDatabase) Create(json *[]byte) (cacao.Playbook, error) { return memory.playbooks[result.ID], nil } -func (memory *InMemoryDatabase) Read(id string) (cacao.Playbook, error) { +func (memory *InMemoryPlaybookDatabase) Read(id string) (cacao.Playbook, error) { playbook, ok := memory.playbooks[id] if !ok { return cacao.Playbook{}, errors.New("playbook is not in repository") @@ -65,7 +65,7 @@ func (memory *InMemoryDatabase) Read(id string) (cacao.Playbook, error) { return playbook, nil } -func (memory *InMemoryDatabase) Update(id string, json *[]byte) (cacao.Playbook, error) { +func (memory *InMemoryPlaybookDatabase) Update(id string, json *[]byte) (cacao.Playbook, error) { playbook, err := memory.Read(id) if err != nil { return playbook, err @@ -78,7 +78,7 @@ func (memory *InMemoryDatabase) Update(id string, json *[]byte) (cacao.Playbook, return *updatePlaybook, nil } -func (memory *InMemoryDatabase) Delete(id string) error { +func (memory *InMemoryPlaybookDatabase) Delete(id string) error { delete(memory.playbooks, id) return nil } diff --git a/internal/database/memory/memory_test.go b/internal/database/memory/playbook_test.go similarity index 96% rename from internal/database/memory/memory_test.go rename to internal/database/memory/playbook_test.go index d206aae5..a7a5408b 100644 --- a/internal/database/memory/memory_test.go +++ b/internal/database/memory/playbook_test.go @@ -32,7 +32,7 @@ func TestCreate(t *testing.T) { } var workflow = decoder.DecodeValidate(byteValue) - mem := New() + mem := NewPlaybookDatabase() playbook, err := mem.Create(&byteValue) assert.Equal(t, err, nil) assert.Equal(t, playbook, workflow) @@ -56,7 +56,7 @@ func TestRead(t *testing.T) { var workflow = decoder.DecodeValidate(byteValue) - mem := New() + mem := NewPlaybookDatabase() empty, err := mem.Read(workflow.ID) assert.Equal(t, err, errors.New("playbook is not in repository")) assert.Equal(t, empty, cacao.Playbook{}) @@ -87,7 +87,7 @@ func TestUpdate(t *testing.T) { var workflow = decoder.DecodeValidate(byteValue) - mem := New() + mem := NewPlaybookDatabase() empty, err := mem.Update(workflow.ID, nil) assert.Equal(t, err, errors.New("playbook is not in repository")) assert.Equal(t, empty, cacao.Playbook{}) @@ -129,7 +129,7 @@ func TestDelete(t *testing.T) { var workflow = decoder.DecodeValidate(byteValue) - mem := New() + mem := NewPlaybookDatabase() err = mem.Delete(workflow.ID) assert.Equal(t, err, nil) @@ -166,7 +166,7 @@ func TestGetAllPlaybooks(t *testing.T) { var workflow = decoder.DecodeValidate(byteValue) - mem := New() + mem := NewPlaybookDatabase() list := []string{ "playbook--f47d4081-21ed-4f21-9d05-6b368d73da30", @@ -214,7 +214,7 @@ func TestGetAllPlaybookMetas(t *testing.T) { var workflow = decoder.DecodeValidate(byteValue) - mem := New() + mem := NewPlaybookDatabase() list := []string{ "playbook--f47d4081-21ed-4f21-9d05-6b368d73da30", diff --git a/internal/database/mongodb/mongo.go b/internal/database/mongodb/mongo.go index 15964997..5a307f43 100644 --- a/internal/database/mongodb/mongo.go +++ b/internal/database/mongodb/mongo.go @@ -6,6 +6,7 @@ import ( "reflect" "time" + keymanagementrepository "soarca/internal/database/keymanagement" "soarca/internal/database/projections" cacao "soarca/pkg/models/cacao" @@ -18,11 +19,12 @@ const writeErrorDuplicationCode = 11000 var ( cacaoPlayBookRepo *mongoCollection[cacao.Playbook] + keyManagementRepo *mongoCollection[keymanagementrepository.KeyPairEntry] mongoclient *mongo.Client ) type dbtypes interface { - cacao.Playbook // | for other supported types + cacao.Playbook | keymanagementrepository.KeyPairEntry // | for other supported types } type mongoCollection[T dbtypes] struct { @@ -57,6 +59,9 @@ func (mongoOpts mongoFindOptions) GetProjectionByType(interface{}) interface{} { func GetCacaoRepo() *mongoCollection[cacao.Playbook] { return cacaoPlayBookRepo } +func GetKeyManagementRepo() *mongoCollection[keymanagementrepository.KeyPairEntry] { + return keyManagementRepo +} // func GetMongoClient() *mongodbClient { // return mongoclient @@ -77,6 +82,7 @@ func SetupMongodb(uri string, username string, password string) error { } cacaoPlayBookRepo, err = NewMongoCollection[cacao.Playbook](mongoclient, "soarca", "cacoa_playbook_collection") + keyManagementRepo, err = NewMongoCollection[keymanagementrepository.KeyPairEntry](mongoclient, "keymanagement", "keymanagement_collection") return err } @@ -220,7 +226,7 @@ func NewMongoCollection[T dbtypes](mongo *mongo.Client, dbName string, colName s } func InitMongoClient(mongo_uri string, username string, password string) error { - log.Trace("Trying to setup new MongoClient") + log.Trace("Trying to setup new MongoClient at uri ", mongo_uri) var err error if mongo_uri == "" { log.Error("mongo uri not valid, because empty") @@ -232,6 +238,7 @@ func InitMongoClient(mongo_uri string, username string, password string) error { return errors.New("username or password not correctly set") } + log.Trace("Logging in to mongo with username ", username, " and password ", password) clientOpts := options.Client().ApplyURI(mongo_uri).SetAuth(options.Credential{ Username: username, Password: password, diff --git a/pkg/api/api.go b/pkg/api/api.go index aabd9552..042346f3 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -7,12 +7,12 @@ import ( "soarca/internal/controller/decomposer_controller" "soarca/internal/controller/informer" "soarca/internal/logger" - "soarca/pkg/api/keymanagement" + keymanagement_handler "soarca/pkg/api/keymanagement" playbook_handler "soarca/pkg/api/playbook" reporter_handler "soarca/pkg/api/reporter" status_handler "soarca/pkg/api/status" "soarca/pkg/core/capability/manual/interaction" - "soarca/pkg/core/capability/ssh" + "soarca/pkg/keymanagement" manual_handler "soarca/pkg/api/manual" @@ -55,9 +55,9 @@ func Manual(app *gin.Engine, interaction interaction.IInteractionStorage) { manualHandler := manual_handler.NewManualHandler(interaction) ManualRoutes(app, manualHandler) } -func KeyManagement(app *gin.Engine, key_manager *ssh.KeyManagement) { +func KeyManagement(app *gin.Engine, key_manager *keymanagement.KeyManagement) { log.Trace("Setting up key management routes") - keyManagement := keymanagement.NewKeyManagementHandler(key_manager) + keyManagement := keymanagement_handler.NewKeyManagementHandler(key_manager) KeyManagementRoutes(app, keyManagement) } @@ -152,12 +152,12 @@ func ManualRoutes(route *gin.Engine, manualHandler *manual_handler.ManualHandler } } -func KeyManagementRoutes(route *gin.Engine, keyManagementHandler *keymanagement.KeyManagementHandler) { +func KeyManagementRoutes(route *gin.Engine, keyManagementHandler *keymanagement_handler.KeyManagementHandler) { keyManagementRoutes := route.Group("/keymanagement") { keyManagementRoutes.GET("/", keyManagementHandler.GetKeys) keyManagementRoutes.PUT("/:keyname", keyManagementHandler.AddKey) - keyManagementRoutes.POST("/refresh/", keyManagementHandler.Refresh) + keyManagementRoutes.PATCH("/:keyname", keyManagementHandler.UpdateKey) keyManagementRoutes.DELETE("/:keyname", keyManagementHandler.RevokeKey) } } diff --git a/pkg/api/keymanagement/keymanagement_api.go b/pkg/api/keymanagement/keymanagement_api.go index 0e0b04a9..16341bb9 100644 --- a/pkg/api/keymanagement/keymanagement_api.go +++ b/pkg/api/keymanagement/keymanagement_api.go @@ -1,4 +1,4 @@ -package keymanagement +package keymanagement_api import ( "encoding/json" @@ -7,7 +7,7 @@ import ( "net/http" "reflect" "soarca/internal/logger" - "soarca/pkg/core/capability/ssh" + "soarca/pkg/keymanagement" "soarca/pkg/models/api" "strconv" @@ -19,68 +19,123 @@ var log *logger.Log type Empty struct{} func init() { - log = logger.Logger(reflect.TypeOf(Empty{}).PkgPath(), logger.Info, "", logger.Json) + log = logger.Logger(reflect.TypeOf(Empty{}).PkgPath(), logger.Trace, "", logger.Json) } type KeyManagementHandler struct { - Manager *ssh.KeyManagement + Manager *keymanagement.KeyManagement } -func NewKeyManagementHandler(manager *ssh.KeyManagement) *KeyManagementHandler { +func NewKeyManagementHandler(manager *keymanagement.KeyManagement) *KeyManagementHandler { return &KeyManagementHandler{ Manager: manager, } } +// GetKeys GET handler for obtaining all keys in the key managements system +// +// @Summary gets all keys from the KMS +// @Schemes +// @Description return all keys in the KMS +// @Tags keymanagement +// @Produce json +// @success 200 {array} string +// @failure 400 {object} api.Error +// @Router /keymanagement/ [GET] func (handler *KeyManagementHandler) GetKeys(context *gin.Context) { keyInfo := handler.Manager.ListAllNames() + log.Trace("Listing all key names") context.JSON(http.StatusOK, keyInfo) } -func (handler *KeyManagementHandler) Refresh(context *gin.Context) { - err := handler.Manager.Refresh() +// AddKey PUT handler to add key to KMS +// +// @Summary add key to KMS +// @Schemes +// @Description adds a key to the KMS; load key into cache and write to database +// @Tags keymanagement +// @Param data body api.KeyManagementKey true "key" +// @Produce json +// @success 200 {json} Empty +// @failure 400 {object} api.Error +// @Router /keymanagement/:keyname/ [PUT] +func (handler *KeyManagementHandler) AddKey(context *gin.Context) { + keyname := context.Param("keyname") + jsonData, err := io.ReadAll(context.Request.Body) if err != nil { - log.Trace("Refreshing keys has failed: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to refresh keys", "POST /keymanagement/refresh") + log.Trace("Submit key has failed: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to read json on server side", "PUT /keymanagement/:keyname") + return + } + var key api.KeyManagementKey + if err := json.Unmarshal(jsonData, &key); err != nil { + log.Trace("Submit key failed to unmarshal: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to marshall json on server side", "PUT /keymanagement/:keyname") return } + if err := handler.Manager.Insert(key.Public, key.Private, key.Passphrase, keyname); err != nil { + log.Trace("Submit key failed to insert: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to insert key on server side", "PUT /keymanagement/:keyname") + return + + } + log.Trace("Inserted key ", keyname) context.JSON(http.StatusOK, Empty{}) } -func (handler *KeyManagementHandler) AddKey(context *gin.Context) { +// UpdateKey PATCH handler to update key in KMS +// +// @Summary update key in KMS +// @Schemes +// @Description update a key in the KMS; load key into cache and write to database +// @Tags keymanagement +// @Param data body api.KeyManagementKey true "key" +// @Produce json +// @success 200 {json} Empty +// @failure 400 {object} api.Error +// @Router /keymanagement/:keyname/ [PATCH] +func (handler *KeyManagementHandler) UpdateKey(context *gin.Context) { keyname := context.Param("keyname") jsonData, err := io.ReadAll(context.Request.Body) if err != nil { - log.Trace("Submit key has failed: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to read json on server side", "POST /keymanagement") + log.Trace("Update key has failed: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to read json on server side", "PATCH /keymanagement/:keyname") return } var key api.KeyManagementKey if err := json.Unmarshal(jsonData, &key); err != nil { - log.Trace("Submit key failed to unmarshal: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to marshall json on server side", "POST /keymanagement") + log.Trace("Update key failed to unmarshal: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to marshall json on server side", "PATCH /keymanagement/:keyname") return } - if err := handler.Manager.Insert(key.Public, key.Private, keyname); err != nil { - log.Trace("Submit key failed to insert: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to insert key on server side", "POST /keymanagement") + if err := handler.Manager.Update(key.Public, key.Private, key.Passphrase, keyname); err != nil { + log.Trace("Update key failed to insert: ", err.Error()) + SendErrorResponse(context, http.StatusBadRequest, "Failed to update key on server side", "PATCH /keymanagement/:keyname") return } + log.Trace("Updated key ", keyname) context.JSON(http.StatusOK, Empty{}) } +// RevokeKey DELETE handler to remove key from KMS +// +// @Summary remove key from KMS +// @Schemes +// @Description revokes the key by moving it to .revoked and renaming it +// @Tags keymanagement +// @Produce json +// @success 200 {json} Empty +// @failure 400 {object} api.Error +// @Router /keymanagement/:keyname/ [DELETE] func (handler *KeyManagementHandler) RevokeKey(context *gin.Context) { keyname := context.Param("keyname") - if err := handler.Manager.Revoke(keyname); err != nil { - log.Trace("Revoke key failed: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to revoke key", "DELETE /keymanagement") - return - } + handler.Manager.Revoke(keyname) context.JSON(http.StatusOK, gin.H{ "status": 200, - "message": fmt.Sprintf("Successfuly removed key %s from SOARCA listing. To permanently remove key, delete the revoked key files in key management directory", keyname), + "message": fmt.Sprintf("Removed key %s from SOARCA listing", keyname), }) + log.Trace("Removed key ", keyname) } func SendErrorResponse(context *gin.Context, status int, message string, orginal_call string) { diff --git a/pkg/core/capability/ssh/keymanagement.go b/pkg/core/capability/ssh/keymanagement.go deleted file mode 100644 index ffb73e34..00000000 --- a/pkg/core/capability/ssh/keymanagement.go +++ /dev/null @@ -1,252 +0,0 @@ -package ssh - -import ( - "fmt" - "io" - "os" - "path" - "slices" - "soarca/pkg/utils" - "strings" - "time" - - "golang.org/x/crypto/ssh" -) - -type KeyPair struct { - Public ssh.PublicKey - Private ssh.Signer -} -type KeyManagement struct { - underlying_dir string - cached_keys map[string]KeyPair -} - -var globalKeyManagement *KeyManagement - -func InitKeyManagement(underlying_dir string) (*KeyManagement, error) { - globalKeyManagement = &KeyManagement{ - underlying_dir: underlying_dir, - } - err := globalKeyManagement.Refresh() - return globalKeyManagement, err -} - -func (management *KeyManagement) GetKeyPair(name string) (*KeyPair, error) { - keypair, ok := management.cached_keys[name] - if !ok { - return nil, fmt.Errorf("could not find keypair for %s", name) - } - return &keypair, nil -} -func (management *KeyManagement) GetPrivate(name string) (ssh.Signer, error) { - keypair, err := management.GetKeyPair(name) - if err != nil { - return nil, err - } - return keypair.Private, nil -} -func parsePrivateKey(filename string) (ssh.Signer, error) { - log.Tracef("Opening private key from path %s", filename) - file, err := os.Open(filename) - if err != nil { - return nil, err - } - passphrase := utils.GetEnv("SSH_KMS_PASSPHRASE", "") - file_buffer := make([]byte, 2048) - if _, err := file.Read(file_buffer); err != nil { - return nil, err - } - if passphrase == "" { - return ssh.ParsePrivateKey(file_buffer) - } else { - return ssh.ParsePrivateKeyWithPassphrase(file_buffer, []byte(passphrase)) - } -} -func parsePublicKey(filename string) (ssh.PublicKey, error) { - log.Tracef("Opening public key from path %s", filename) - file, err := os.Open(filename) - if err != nil { - return nil, err - } - file_buffer := make([]byte, 2048) - if _, err = file.Read(file_buffer); err != nil { - return nil, err - } - key, _, _, _, err := ssh.ParseAuthorizedKey(file_buffer) - return key, err - -} -func (management *KeyManagement) Refresh() error { - management.cached_keys = make(map[string]KeyPair) - dir, err := os.Open(management.underlying_dir) - if err != nil { - return err - } - filenames, err := dir.Readdirnames(0) - if err != nil { - return err - } - for _, filename := range filenames { - if strings.HasSuffix(filename, ".pub") { - private_filename := strings.TrimSuffix(filename, ".pub") - if !slices.Contains(filenames, private_filename) { - return fmt.Errorf("found public key %s without corresponding private key (%s)", filename, private_filename) - } - log.Trace("Found public key named ", private_filename) - private, err := parsePrivateKey(path.Join(management.underlying_dir, private_filename)) - if err != nil { - return fmt.Errorf("private key (%s) parsing error: %s", private_filename, err) - } - public, err := parsePublicKey(path.Join(management.underlying_dir, filename)) - if err != nil { - return fmt.Errorf("public key (%s) parsing error: %s", filename, err) - } - management.cached_keys[private_filename] = KeyPair{Public: public, Private: private} - } - } - return nil -} - -func (management *KeyManagement) Insert(public string, private string, name string) error { - for key := range management.cached_keys { - if key == name { - return fmt.Errorf("inserting key with name %s: already present", name) - } - } - private_filename := path.Clean(name) - if strings.HasPrefix(private_filename, "..") { - return fmt.Errorf("cannot insert key in parent of Key Management directory") - } - private_filename = path.Join(management.underlying_dir, private_filename) - public_filename := private_filename + ".pub" - public_file, err := os.Open(public_filename) - if err != nil { - if os.IsNotExist(err) { - public_file, err = os.Create(public_filename) - if err != nil { - return err - } - } else { - return err - } - } - private_file, err := os.Open(private_filename) - if err != nil { - if os.IsNotExist(err) { - private_file, err = os.Create(private_filename) - if err != nil { - return err - } - } else { - return err - } - } - n_read, err := public_file.WriteString(public) - if err != nil { - return err - } - if n_read < len(public) { - return fmt.Errorf("write error: did not write whole public key file") - } - if _, err := private_file.WriteString(private); err != nil { - return err - } - if n_read < len(private) { - return fmt.Errorf("write error: did not write whole private key file") - } - if err := public_file.Close(); err != nil { - return err - } - if err := private_file.Close(); err != nil { - return err - } - return management.insertInternal(public, private, name) -} - -func (management *KeyManagement) insertInternal(public string, private string, name string) error { - public_key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(public)) - if err != nil { - return err - } - passphrase := utils.GetEnv("SSH_KMS_PASSPHRASE", "") - var private_key ssh.Signer - if passphrase == "" { - private_key, err = ssh.ParsePrivateKey([]byte(private)) - } else { - private_key, err = ssh.ParsePrivateKeyWithPassphrase([]byte(private), []byte(passphrase)) - } - if err != nil { - return err - } - management.cached_keys[name] = KeyPair{Public: public_key, Private: private_key} - return nil -} -func (management *KeyManagement) ListAllNames() []string { - keys := make([]string, len(management.cached_keys)) - index := 0 - for key := range management.cached_keys { - keys[index] = key - index++ - } - return keys -} - -const revoked_directory string = ".revoked" - -func moveFile(source string, dest string) error { - source_file, err := os.Open(source) - if err != nil { - return err - } - dest_file, err := os.OpenFile(dest, os.O_WRONLY, 0) - if err != nil { - if os.IsNotExist(err) { - dest_file, err = os.Create(dest) - if err != nil { - return err - } - } else { - return err - } - } - if _, err := io.Copy(dest_file, source_file); err != nil { - return err - } - if err := source_file.Close(); err != nil { - return err - } - if err := dest_file.Close(); err != nil { - return err - } - return os.Remove(source) -} -func (management *KeyManagement) Revoke(keyname string) error { - if _, ok := management.cached_keys[keyname]; !ok { - return fmt.Errorf("unknown key %s", keyname) - } - if _, err := os.ReadDir(path.Join(management.underlying_dir, revoked_directory)); err != nil { - if err := os.Mkdir(path.Join(management.underlying_dir, revoked_directory), 0777); err != nil { - return fmt.Errorf("error while creating directory for revoked keys: %s", err) - } - } - now := time.Now() - suffix := fmt.Sprintf(".revoked_%d-%d_%d-%s-%d", now.Second(), now.Hour(), now.Day(), now.Month().String(), now.Year()) - - public_filename := path.Join(management.underlying_dir, keyname+".pub") - public_target := path.Join(management.underlying_dir, revoked_directory, keyname+".pub"+suffix) - log.Infof("Moving %s to %s", public_filename, public_target) - if err := moveFile(public_filename, public_target); err != nil { - return fmt.Errorf("error while moving key: %s", err) - } - - private_filename := path.Join(management.underlying_dir, keyname) - private_target := path.Join(management.underlying_dir, revoked_directory, keyname+suffix) - log.Infof("Moving %s to %s", private_filename, private_target) - if err := moveFile(private_filename, private_target); err != nil { - return fmt.Errorf("error while moving key: %s", err) - } - - delete(management.cached_keys, keyname) - return nil -} diff --git a/pkg/core/capability/ssh/keymanagement_test.go b/pkg/core/capability/ssh/keymanagement_test.go deleted file mode 100644 index 89e86758..00000000 --- a/pkg/core/capability/ssh/keymanagement_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package ssh - -import ( - "os" - "path" - "slices" - "testing" - - "github.com/stretchr/testify/assert" -) - -const testdir string = ".ssh_test" -const testkey string = "test-key" - -func testkey_dir() string { - return path.Join("..", "..", "..", "..", "test", "unittest", "mocks", "mock_utils") -} - -func init() { - err := os.Mkdir(testdir, 0777) - if err != nil { - if err.(*os.PathError).Err.Error() == "file exists" { - if err := os.RemoveAll(testdir); err != nil { - panic(err) - } - if err := os.Mkdir(testdir, 0777); err != nil { - panic(err) - } - } else { - panic(err) - } - } - if _, err = InitKeyManagement(testdir); err != nil { - panic(err) - } -} - -func TestRevoke(t *testing.T) { - addKey(t, testkey) - assert.True(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey)) - assert.Nil(t, globalKeyManagement.Revoke(testkey)) -} -func addKey(t *testing.T, keyname string) { - pubkey_path := path.Join(testkey_dir(), "test-key.pub") - privkey_path := path.Join(testkey_dir(), "test-key") - pubkey_file, err := os.Open(pubkey_path) - assert.Nil(t, err) - - privkey_file, err := os.Open(privkey_path) - assert.Nil(t, err) - pubkey_buf := make([]byte, 2048) - privkey_buf := make([]byte, 2048) - _, err = pubkey_file.Read(pubkey_buf) - assert.Nil(t, err) - _, err = privkey_file.Read(privkey_buf) - assert.Nil(t, err) - assert.Nil(t, globalKeyManagement.Insert(string(pubkey_buf), string(privkey_buf), keyname)) - assert.Nil(t, privkey_file.Close()) - assert.Nil(t, pubkey_file.Close()) -} - -func TestAddKey(t *testing.T) { - addKey(t, testkey) - assert.True(t, slices.Equal(globalKeyManagement.ListAllNames(), []string{testkey})) -} - -func copyFile(src string, dst string) error { - data, err := os.ReadFile(src) - if err != nil { - return err - } - return os.WriteFile(dst, data, 0666) -} -func TestRefresh(t *testing.T) { - assert.False(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey+"1")) - pubkey_path := path.Join(testkey_dir(), "test-key.pub") - privkey_path := path.Join(testkey_dir(), "test-key") - assert.Nil(t, copyFile(pubkey_path, path.Join(testdir, testkey+"1.pub"))) - assert.Nil(t, copyFile(privkey_path, path.Join(testdir, testkey+"1"))) - assert.Nil(t, globalKeyManagement.Refresh()) - assert.True(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey+"1")) -} diff --git a/pkg/core/capability/ssh/ssh.go b/pkg/core/capability/ssh/ssh.go index 9f2068eb..a64ae285 100644 --- a/pkg/core/capability/ssh/ssh.go +++ b/pkg/core/capability/ssh/ssh.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" "soarca/pkg/core/capability" + "soarca/pkg/keymanagement" "soarca/pkg/models/cacao" "soarca/pkg/models/execution" "strings" @@ -21,6 +22,7 @@ const ( ) type SshCapability struct { + Keys *keymanagement.KeyManagement } var component = reflect.TypeOf(SshCapability{}).PkgPath() @@ -38,30 +40,24 @@ func (sshCapability *SshCapability) Execute(metadata execution.Metadata, context capability.Context) (cacao.Variables, error) { log.Trace(metadata.ExecutionId) - return execute(context.Command, context.Authentication, context.Target) -} - -func execute(command cacao.Command, - authentication cacao.AuthenticationInformation, - target cacao.AgentTarget) (cacao.Variables, error) { - err := CheckSshAuthenticationInfo(authentication) + err := CheckSshAuthenticationInfo(context.Authentication) if err != nil { log.Error(err) return cacao.NewVariables(), err } - config, err := getConfig(authentication) + config, err := sshCapability.getConfig(context.Authentication) if err != nil { return cacao.NewVariables(), err } - session, client, err := getSession(config, target) + session, client, err := getSession(config, context.Target) if err != nil { return cacao.NewVariables(), err } defer close(client) - return executeCommand(session, command) + return executeCommand(session, context.Command) } func executeCommand(session *ssh.Session, @@ -70,7 +66,7 @@ func executeCommand(session *ssh.Session, response, err := session.Output(StripSshPrepend(command.Command)) if err != nil { - log.Errorf("Output: %s", err) + log.Error("Output: ", err) return cacao.NewVariables(), err } results := cacao.NewVariables(cacao.Variable{Type: cacao.VariableTypeString, @@ -81,12 +77,12 @@ func executeCommand(session *ssh.Session, if sessionErr != nil && sessionErr.Error() != "EOF" { // The ssh api is subtle, and it can happen that we get EOF as an error. // This is likely not an error, as the session can also be closed by the host. - log.Errorf("Close: %s", sessionErr) + log.Error("Close: ", sessionErr) } return results, err } -func getConfig(authentication cacao.AuthenticationInformation) (ssh.ClientConfig, error) { +func (sshCapability *SshCapability) getConfig(authentication cacao.AuthenticationInformation) (ssh.ClientConfig, error) { config := ssh.ClientConfig{User: authentication.Username, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: time.Duration(time.Second * 20)} @@ -101,7 +97,7 @@ func getConfig(authentication cacao.AuthenticationInformation) (ssh.ClientConfig if authentication.KmsKeyIdentifier == "" { return config, fmt.Errorf("KMS indicated, but no kms_key_identifier given") } - private_key, err := globalKeyManagement.GetPrivate(authentication.KmsKeyIdentifier) + private_key, err := sshCapability.Keys.GetPrivate(authentication.KmsKeyIdentifier) if err != nil { return config, err } @@ -131,13 +127,13 @@ func getSession(config ssh.ClientConfig, target cacao.AgentTarget) (*ssh.Session host := CombinePortAndAddress(target.Address, target.Port) client, err := ssh.Dial("tcp", host, &config) if err != nil { - log.Errorf("Dialing: %s", err) + log.Error(err) return nil, nil, err } session, err := client.NewSession() if err != nil { - log.Errorf("Session: %s", err) + log.Error(err) close(client) return nil, nil, err } @@ -192,7 +188,7 @@ func close(client *ssh.Client) { if client != nil { err := client.Close() if err != nil { - log.Errorf("Closing: %s", err) + log.Error(err) } } } diff --git a/pkg/keymanagement/keymanagement.go b/pkg/keymanagement/keymanagement.go new file mode 100644 index 00000000..d92b580f --- /dev/null +++ b/pkg/keymanagement/keymanagement.go @@ -0,0 +1,122 @@ +package keymanagement + +import ( + "fmt" + "os" + "reflect" + keymanagementrepository "soarca/internal/database/keymanagement" + "soarca/internal/logger" + keys "soarca/pkg/models/keymanagement" + + "golang.org/x/crypto/ssh" +) + +var component = reflect.TypeOf(KeyManagement{}).PkgPath() +var log *logger.Log + +func init() { + log = logger.Logger(component, logger.Info, "", logger.Json) +} + +type KeyManagement struct { + database keymanagementrepository.IKeyManagementRepository + cached_keys map[string]keys.KeyPair +} + +func InitKeyManagement(database keymanagementrepository.IKeyManagementRepository) *KeyManagement { + return &KeyManagement{database: database, cached_keys: make(map[string]keys.KeyPair)} +} + +func (management *KeyManagement) GetKeyPair(name string) (*keys.KeyPair, error) { + keypair, ok := management.cached_keys[name] + if !ok { + keypair, err := management.database.Read(name) + if err != nil { + return nil, err + } + return &keypair, nil + } + return &keypair, nil +} +func (management *KeyManagement) GetPrivate(name string) (ssh.Signer, error) { + keypair, err := management.GetKeyPair(name) + if err != nil { + return nil, err + } + return keypair.Private, nil +} +func parsePrivateKey(filename string, passphrase string) (ssh.Signer, error) { + log.Tracef("Opening private key from path %s", filename) + file_buffer, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + if passphrase == "" { + return ssh.ParsePrivateKey(file_buffer) + } else { + return ssh.ParsePrivateKeyWithPassphrase(file_buffer, []byte(passphrase)) + } +} +func parsePublicKey(filename string) (ssh.PublicKey, error) { + log.Tracef("Opening public key from path %s", filename) + file, err := os.Open(filename) + if err != nil { + return nil, err + } + file_buffer := make([]byte, 2048) + if _, err = file.Read(file_buffer); err != nil { + return nil, err + } + key, _, _, _, err := ssh.ParseAuthorizedKey(file_buffer) + return key, err + +} + +func (management *KeyManagement) Insert(public string, private string, passphrase string, name string) error { + if _, err := management.GetKeyPair(name); err == nil { + return fmt.Errorf("Key with name already exists: %s (error: %s)", name, err) + } + return management.insertInternal(public, private, passphrase, name) +} + +func (management *KeyManagement) Update(public string, private string, passphrase string, name string) error { + if _, err := management.GetKeyPair(name); err != nil { + return fmt.Errorf("No such key exists: %s (error: %s)", name, err) + } + return management.insertInternal(public, private, passphrase, name) +} + +func (management *KeyManagement) insertInternal(public string, private string, passphrase string, name string) error { + public_key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(public)) + if err != nil { + return fmt.Errorf("parsing public key: %s", err) + } + var private_key ssh.Signer + if passphrase == "" { + private_key, err = ssh.ParsePrivateKey([]byte(private)) + } else { + private_key, err = ssh.ParsePrivateKeyWithPassphrase([]byte(private), []byte(passphrase)) + } + if err != nil { + return fmt.Errorf("parsing private key: %s", err) + } + keypair := keys.KeyPair{Public: public_key, Private: private_key} + management.cached_keys[name] = keypair + management.database.Create(name, keypair) + return nil +} + +func (management *KeyManagement) ListAllNames() []string { + keys := make([]string, len(management.cached_keys)) + index := 0 + for key := range management.cached_keys { + keys[index] = key + index++ + } + return keys +} + +func (management *KeyManagement) Revoke(keyname string) { + management.database.Delete(keyname) + delete(management.cached_keys, keyname) +} diff --git a/pkg/keymanagement/keymanagement_test.go b/pkg/keymanagement/keymanagement_test.go new file mode 100644 index 00000000..7e4506d4 --- /dev/null +++ b/pkg/keymanagement/keymanagement_test.go @@ -0,0 +1,53 @@ +package keymanagement + +import ( + "os" + "path" + "slices" + "soarca/internal/database/memory" + "testing" + + "github.com/stretchr/testify/assert" +) + +const testkey string = "test-key" + +func testkey_dir() string { + return path.Join("..", "..", "test", "unittest", "mocks", "mock_utils") +} + +var globalKeyManagement *KeyManagement + +func init() { + globalKeyManagement = InitKeyManagement(memory.NewKeyManagementDatabase()) +} + +func TestRevoke(t *testing.T) { + addKey(t, testkey) + assert.True(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey)) + globalKeyManagement.Revoke(testkey) + assert.False(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey)) +} +func addKey(t *testing.T, keyname string) { + pubkey_path := path.Join(testkey_dir(), "test-key.pub") + privkey_path := path.Join(testkey_dir(), "test-key") + pubkey_file, err := os.Open(pubkey_path) + assert.Nil(t, err) + + privkey_file, err := os.Open(privkey_path) + assert.Nil(t, err) + pubkey_buf := make([]byte, 2048) + privkey_buf := make([]byte, 2048) + _, err = pubkey_file.Read(pubkey_buf) + assert.Nil(t, err) + _, err = privkey_file.Read(privkey_buf) + assert.Nil(t, err) + assert.Nil(t, globalKeyManagement.Insert(string(pubkey_buf), string(privkey_buf), "", keyname)) + assert.Nil(t, privkey_file.Close()) + assert.Nil(t, pubkey_file.Close()) +} + +func TestAddKey(t *testing.T) { + addKey(t, testkey) + assert.True(t, slices.Equal(globalKeyManagement.ListAllNames(), []string{testkey})) +} diff --git a/pkg/models/api/keymanagement.go b/pkg/models/api/keymanagement.go index 6ce88ccc..41808ed5 100644 --- a/pkg/models/api/keymanagement.go +++ b/pkg/models/api/keymanagement.go @@ -4,6 +4,7 @@ type KeyManagementKeyList struct { Keys []string `json:"keys"` } type KeyManagementKey struct { - Private string `json:"private"` - Public string `json:"public"` + Private string `json:"private"` + Public string `json:"public"` + Passphrase string `json:"passphrase"` } diff --git a/pkg/models/keymanagement/keymanagent.go b/pkg/models/keymanagement/keymanagent.go new file mode 100644 index 00000000..a9619961 --- /dev/null +++ b/pkg/models/keymanagement/keymanagent.go @@ -0,0 +1,8 @@ +package keymanagement + +import "golang.org/x/crypto/ssh" + +type KeyPair struct { + Public ssh.PublicKey + Private ssh.Signer +} From c052902d2c4c97fe251e55e5a377f3861e559ff9 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Mon, 18 Aug 2025 17:36:56 +0200 Subject: [PATCH 12/39] Updated ssh-kms deployment and added ci --- .github/workflows/ci.yml | 7 +- .../docker/testing/ssh-kms-test/Dockerfile | 19 ++++ .../testing/ssh-kms-test/docker-compose.yml | 22 ++--- .../testing/ssh-kms-test/prepare_keys.sh | 6 -- examples/ssh-kms-playbook.json | 2 +- test/integration/kms/kms_test.go | 89 +++++++++++++++++++ 6 files changed, 120 insertions(+), 25 deletions(-) create mode 100644 deployments/docker/testing/ssh-kms-test/Dockerfile delete mode 100755 deployments/docker/testing/ssh-kms-test/prepare_keys.sh create mode 100644 test/integration/kms/kms_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d29f5e6..a467d693 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,10 +59,13 @@ jobs: - name: Install swaggo run: go install github.com/swaggo/swag/cmd/swag@latest timeout-minutes: 12 - - name: Start docker containers for test + - name: Start docker container for http test run: docker compose -f "deployments/docker/testing/httpbin-test/docker-compose.yml" up -d --build + - name: Start docker container for ssh test + run: docker compose -f "deployments/docker/testing/ssh-test/docker-compose.yml" up -d --build + - name: Start docker container for kms test + run: docker compose -f "deployments/docker/testing/ssh-kms-test/docker-compose.yml" up -d --build - name: Run tests run: | - docker compose -f "deployments/docker/testing/ssh-test/docker-compose.yml" up -d --build make ci-test diff --git a/deployments/docker/testing/ssh-kms-test/Dockerfile b/deployments/docker/testing/ssh-kms-test/Dockerfile new file mode 100644 index 00000000..59d20de0 --- /dev/null +++ b/deployments/docker/testing/ssh-kms-test/Dockerfile @@ -0,0 +1,19 @@ +FROM ubuntu:latest + +RUN apt update && apt install openssh-server sudo -y + +RUN sudo useradd sshtest + + +RUN mkdir -p /home/sshtest/ +RUN ssh-keygen -q -N "" -f /home/sshtest/test +RUN mkdir -p /home/sshtest/.ssh/ +RUN cat /home/sshtest/test.pub > /home/sshtest/.ssh/authorized_keys +RUN echo "PasswordAuthentication no" >> /etc/ssh/sshd_config +RUN echo "PubkeyAuthentication yes" >> /etc/ssh/sshd_config + +RUN service ssh start + +EXPOSE 22 + +CMD ["/usr/sbin/sshd","-D"] diff --git a/deployments/docker/testing/ssh-kms-test/docker-compose.yml b/deployments/docker/testing/ssh-kms-test/docker-compose.yml index ee32b830..be883465 100644 --- a/deployments/docker/testing/ssh-kms-test/docker-compose.yml +++ b/deployments/docker/testing/ssh-kms-test/docker-compose.yml @@ -1,17 +1,7 @@ -services: - openssh-server: - image: lscr.io/linuxserver/openssh-server:latest - container_name: openssh-server - hostname: openssh-server - environment: - - PUID=1000 - - PGID=1000 - - TZ=Etc/UTC - - USER_NAME=linuxserver.io - - LOG_STDOUT= ssh.log - volumes: - - ./config:/config + services: + ssh_server: + container_name: ssh_server + build: + dockerfile: Dockerfile ports: - - 2222:2222 - restart: unless-stopped - + - 2223:22 diff --git a/deployments/docker/testing/ssh-kms-test/prepare_keys.sh b/deployments/docker/testing/ssh-kms-test/prepare_keys.sh deleted file mode 100755 index 21edf4c9..00000000 --- a/deployments/docker/testing/ssh-kms-test/prepare_keys.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -set -u -mkdir -p ssh-keystore/ -ssh-keygen -q -N "" -f ssh-keystore/test -mkdir -p config/.ssh/ -cat ssh-keystore/test.pub > config/.ssh/authorized_keys diff --git a/examples/ssh-kms-playbook.json b/examples/ssh-kms-playbook.json index b4b7af4d..8a2a5b4b 100644 --- a/examples/ssh-kms-playbook.json +++ b/examples/ssh-kms-playbook.json @@ -43,7 +43,7 @@ "127.0.0.1" ] }, - "port": "2222", + "port": "2223", "authentication_info": "user-auth--b7ddc2ea-9f6a-4e82-8eaa-be202e942090" } }, diff --git a/test/integration/kms/kms_test.go b/test/integration/kms/kms_test.go new file mode 100644 index 00000000..32a80323 --- /dev/null +++ b/test/integration/kms/kms_test.go @@ -0,0 +1,89 @@ +package kms + +import ( + "fmt" + "os" + "path" + "soarca/internal/database/memory" + "soarca/pkg/core/capability" + "soarca/pkg/core/capability/ssh" + "soarca/pkg/keymanagement" + "soarca/pkg/models/cacao" + "soarca/pkg/models/execution" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +var globalKeyManagement *keymanagement.KeyManagement + +func init() { + globalKeyManagement = keymanagement.InitKeyManagement(memory.NewKeyManagementDatabase()) +} + +const testkey string = "test" + +func testkey_dir() string { + return path.Join("..", "..", "..", "deployments", "docker", "testing", "ssh-kms-test") +} +func addTestKey(t *testing.T) { + pubkey_path := path.Join(testkey_dir(), testkey+".pub") + privkey_path := path.Join(testkey_dir(), testkey) + pubkey, err := os.ReadFile(pubkey_path) + assert.Nil(t, err) + privkey, err := os.ReadFile(privkey_path) + assert.Nil(t, err) + assert.Nil(t, globalKeyManagement.Insert(string(pubkey), string(privkey), "", testkey)) +} + +func TestSshConnection(t *testing.T) { + sshCapability := ssh.SshCapability{Keys: globalKeyManagement} + addTestKey(t) + + expectedCommand := cacao.Command{ + Type: "ssh", + Command: "ls -la", + } + + expectedAuthenticationInformation := cacao.AuthenticationInformation{ + ID: "some-authid-1", + Type: "user-auth", + Username: "sshtest", + Kms: true, + KmsKeyIdentifier: testkey, + } + + expectedTarget := cacao.AgentTarget{ + Type: "ssh", + Address: map[cacao.NetAddressType][]string{"ipv4": {"localhost"}}, + Port: "2223", + AuthInfoIdentifier: "some-authid-1", + } + + expectedVariables := cacao.Variable{ + Type: "string", + Name: "var1", + Value: "testing", + } + + var executionId, _ = uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") + var playbookId = "playbook--d09351a2-a075-40c8-8054-0b7c423db83f" + var stepId = "step--81eff59f-d084-4324-9e0a-59e353dbd28f" + var metadata = execution.Metadata{ExecutionId: executionId, PlaybookId: playbookId, StepId: stepId} + data := capability.Context{ + Command: expectedCommand, + Target: expectedTarget, + Authentication: expectedAuthenticationInformation, + Variables: cacao.NewVariables(expectedVariables), + } + results, err := sshCapability.Execute(metadata, + data) + if err != nil { + fmt.Println(err) + t.Fail() + } + + fmt.Println(results) + +} From a05aac0cf0e77fe5e4fca473001961b40a3431eb Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Tue, 19 Aug 2025 10:29:56 +0200 Subject: [PATCH 13/39] Updated ci build to properly fetch keys --- .github/workflows/ci.yml | 5 ++++- .../docker/testing/ssh-kms-test/Dockerfile | 16 +++++++++------- .../testing/ssh-kms-test/docker-compose.yml | 1 + 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a467d693..31736f11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,10 @@ jobs: - name: Start docker container for ssh test run: docker compose -f "deployments/docker/testing/ssh-test/docker-compose.yml" up -d --build - name: Start docker container for kms test - run: docker compose -f "deployments/docker/testing/ssh-kms-test/docker-compose.yml" up -d --build + run: | + docker compose -f "deployments/docker/testing/ssh-kms-test/docker-compose.yml" up -d --build + docker compose -f "deployments/docker/testing/ssh-kms-test/docker-compose.yml" cp ssh_server:/test deployments/docker/testing/ssh-kms-test + docker compose -f "deployments/docker/testing/ssh-kms-test/docker-compose.yml" cp ssh_server:/test.pub deployments/docker/testing/ssh-kms-test - name: Run tests run: | make ci-test diff --git a/deployments/docker/testing/ssh-kms-test/Dockerfile b/deployments/docker/testing/ssh-kms-test/Dockerfile index 59d20de0..ea3e6b6c 100644 --- a/deployments/docker/testing/ssh-kms-test/Dockerfile +++ b/deployments/docker/testing/ssh-kms-test/Dockerfile @@ -1,19 +1,21 @@ -FROM ubuntu:latest +FROM ubuntu:latest RUN apt update && apt install openssh-server sudo -y -RUN sudo useradd sshtest +RUN sudo useradd -m sshtest +RUN ssh-keygen -q -N "" -f /test -RUN mkdir -p /home/sshtest/ -RUN ssh-keygen -q -N "" -f /home/sshtest/test RUN mkdir -p /home/sshtest/.ssh/ -RUN cat /home/sshtest/test.pub > /home/sshtest/.ssh/authorized_keys + +RUN cat /test.pub > /home/sshtest/.ssh/authorized_keys + RUN echo "PasswordAuthentication no" >> /etc/ssh/sshd_config + RUN echo "PubkeyAuthentication yes" >> /etc/ssh/sshd_config -RUN service ssh start +RUN sudo service ssh start EXPOSE 22 -CMD ["/usr/sbin/sshd","-D"] +CMD ["sudo","/usr/sbin/sshd","-D"] diff --git a/deployments/docker/testing/ssh-kms-test/docker-compose.yml b/deployments/docker/testing/ssh-kms-test/docker-compose.yml index be883465..d84e7844 100644 --- a/deployments/docker/testing/ssh-kms-test/docker-compose.yml +++ b/deployments/docker/testing/ssh-kms-test/docker-compose.yml @@ -5,3 +5,4 @@ dockerfile: Dockerfile ports: - 2223:22 + From 8683e42da691bfd2aee6221a6e4f154b1d1c7c14 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Tue, 19 Aug 2025 10:43:05 +0200 Subject: [PATCH 14/39] Smaller fixes from review --- pkg/api/keymanagement/keymanagement_api.go | 41 +++++++-------------- pkg/core/capability/ssh/ssh.go | 21 +++++++---- test/unittest/mocks/mock_utils/test-key.pub | 2 +- 3 files changed, 28 insertions(+), 36 deletions(-) diff --git a/pkg/api/keymanagement/keymanagement_api.go b/pkg/api/keymanagement/keymanagement_api.go index 16341bb9..5f4402c8 100644 --- a/pkg/api/keymanagement/keymanagement_api.go +++ b/pkg/api/keymanagement/keymanagement_api.go @@ -2,14 +2,13 @@ package keymanagement_api import ( "encoding/json" - "fmt" "io" "net/http" "reflect" "soarca/internal/logger" + apiError "soarca/pkg/api/error" "soarca/pkg/keymanagement" "soarca/pkg/models/api" - "strconv" "github.com/gin-gonic/gin" ) @@ -63,19 +62,19 @@ func (handler *KeyManagementHandler) AddKey(context *gin.Context) { keyname := context.Param("keyname") jsonData, err := io.ReadAll(context.Request.Body) if err != nil { - log.Trace("Submit key has failed: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to read json on server side", "PUT /keymanagement/:keyname") + log.Error("Submit key has failed: ", err.Error()) + apiError.SendErrorResponse(context, http.StatusBadRequest, "Failed to read json on server side", "PUT /keymanagement/:keyname", "") return } var key api.KeyManagementKey if err := json.Unmarshal(jsonData, &key); err != nil { - log.Trace("Submit key failed to unmarshal: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to marshall json on server side", "PUT /keymanagement/:keyname") + log.Error("Submit key failed to unmarshal: ", err.Error()) + apiError.SendErrorResponse(context, http.StatusBadRequest, "Failed to marshall json on server side", "PUT /keymanagement/:keyname", "") return } if err := handler.Manager.Insert(key.Public, key.Private, key.Passphrase, keyname); err != nil { - log.Trace("Submit key failed to insert: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to insert key on server side", "PUT /keymanagement/:keyname") + log.Error("Submit key failed to insert: ", err.Error()) + apiError.SendErrorResponse(context, http.StatusBadRequest, "Failed to insert key on server side", "PUT /keymanagement/:keyname", "") return } @@ -98,19 +97,19 @@ func (handler *KeyManagementHandler) UpdateKey(context *gin.Context) { keyname := context.Param("keyname") jsonData, err := io.ReadAll(context.Request.Body) if err != nil { - log.Trace("Update key has failed: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to read json on server side", "PATCH /keymanagement/:keyname") + log.Error("Update key has failed: ", err.Error()) + apiError.SendErrorResponse(context, http.StatusBadRequest, "Failed to read json on server side", "PATCH /keymanagement/:keyname", "") return } var key api.KeyManagementKey if err := json.Unmarshal(jsonData, &key); err != nil { - log.Trace("Update key failed to unmarshal: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to marshall json on server side", "PATCH /keymanagement/:keyname") + log.Error("Update key failed to unmarshal: ", err.Error()) + apiError.SendErrorResponse(context, http.StatusBadRequest, "Failed to marshall json on server side", "PATCH /keymanagement/:keyname", "") return } if err := handler.Manager.Update(key.Public, key.Private, key.Passphrase, keyname); err != nil { - log.Trace("Update key failed to insert: ", err.Error()) - SendErrorResponse(context, http.StatusBadRequest, "Failed to update key on server side", "PATCH /keymanagement/:keyname") + log.Error("Update key failed to insert: ", err.Error()) + apiError.SendErrorResponse(context, http.StatusBadRequest, "Failed to update key on server side", "PATCH /keymanagement/:keyname", "") return } @@ -131,18 +130,6 @@ func (handler *KeyManagementHandler) UpdateKey(context *gin.Context) { func (handler *KeyManagementHandler) RevokeKey(context *gin.Context) { keyname := context.Param("keyname") handler.Manager.Revoke(keyname) - context.JSON(http.StatusOK, gin.H{ - "status": 200, - "message": fmt.Sprintf("Removed key %s from SOARCA listing", keyname), - }) + context.JSON(http.StatusOK, Empty{}) log.Trace("Removed key ", keyname) } - -func SendErrorResponse(context *gin.Context, status int, message string, orginal_call string) { - msg := gin.H{ - "status": strconv.Itoa(status), - "message": message, - "original-call": orginal_call, - } - context.JSON(status, msg) -} diff --git a/pkg/core/capability/ssh/ssh.go b/pkg/core/capability/ssh/ssh.go index a64ae285..d5f627e7 100644 --- a/pkg/core/capability/ssh/ssh.go +++ b/pkg/core/capability/ssh/ssh.go @@ -74,10 +74,14 @@ func executeCommand(session *ssh.Session, Value: string(response)}) log.Trace("Finished ssh execution will return the variables: ", results) sessionErr := session.Close() - if sessionErr != nil && sessionErr.Error() != "EOF" { - // The ssh api is subtle, and it can happen that we get EOF as an error. - // This is likely not an error, as the session can also be closed by the host. - log.Error("Close: ", sessionErr) + if sessionErr != nil { + if sessionErr.Error() != "EOF" { + // The ssh api is subtle, and it can happen that we get EOF as an error. + // This is likely not an error, as the session can also be closed by the host. + log.Debug("SSH session close got EOF") + } else { + log.Error("Close: ", sessionErr) + } } return results, err } @@ -89,10 +93,6 @@ func (sshCapability *SshCapability) getConfig(authentication cacao.Authenticatio switch authentication.Type { case "user-auth": - if authentication.Password != "" { - config.Auth = []ssh.AuthMethod{ - ssh.Password(authentication.Password)} - } if authentication.Kms { if authentication.KmsKeyIdentifier == "" { return config, fmt.Errorf("KMS indicated, but no kms_key_identifier given") @@ -104,6 +104,11 @@ func (sshCapability *SshCapability) getConfig(authentication cacao.Authenticatio config.Auth = []ssh.AuthMethod{ ssh.PublicKeys(private_key), } + } else if authentication.Password != "" { + config.Auth = []ssh.AuthMethod{ + ssh.Password(authentication.Password)} + } else { + return config, fmt.Errorf("No authentication method given in user-auth") } return config, nil diff --git a/test/unittest/mocks/mock_utils/test-key.pub b/test/unittest/mocks/mock_utils/test-key.pub index d6e7b2c9..e860bf5d 100644 --- a/test/unittest/mocks/mock_utils/test-key.pub +++ b/test/unittest/mocks/mock_utils/test-key.pub @@ -1 +1 @@ -ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGzijTr3feGJz8CuxKjR+z1xROTQAdza843VmpLrxV/u thijs@PC-44318 +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGzijTr3feGJz8CuxKjR+z1xROTQAdza843VmpLrxV/u user@PC From ef7f7126d67b8fc60e7467b359d6ca1e1ad7662c Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Tue, 19 Aug 2025 10:45:50 +0200 Subject: [PATCH 15/39] Fixed name clash --- .github/workflows/ci.yml | 4 ++-- deployments/docker/testing/ssh-kms-test/docker-compose.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31736f11..7169bf41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,8 +66,8 @@ jobs: - name: Start docker container for kms test run: | docker compose -f "deployments/docker/testing/ssh-kms-test/docker-compose.yml" up -d --build - docker compose -f "deployments/docker/testing/ssh-kms-test/docker-compose.yml" cp ssh_server:/test deployments/docker/testing/ssh-kms-test - docker compose -f "deployments/docker/testing/ssh-kms-test/docker-compose.yml" cp ssh_server:/test.pub deployments/docker/testing/ssh-kms-test + docker compose -f "deployments/docker/testing/ssh-kms-test/docker-compose.yml" cp ssh_kms_server:/test deployments/docker/testing/ssh-kms-test + docker compose -f "deployments/docker/testing/ssh-kms-test/docker-compose.yml" cp ssh_kms_server:/test.pub deployments/docker/testing/ssh-kms-test - name: Run tests run: | make ci-test diff --git a/deployments/docker/testing/ssh-kms-test/docker-compose.yml b/deployments/docker/testing/ssh-kms-test/docker-compose.yml index d84e7844..43ff5674 100644 --- a/deployments/docker/testing/ssh-kms-test/docker-compose.yml +++ b/deployments/docker/testing/ssh-kms-test/docker-compose.yml @@ -1,6 +1,6 @@ services: - ssh_server: - container_name: ssh_server + ssh_kms_server: + container_name: ssh_kms_server build: dockerfile: Dockerfile ports: From b0a72b13976c3c55de78fd94b944f3aa2c472ed3 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Tue, 19 Aug 2025 10:49:47 +0200 Subject: [PATCH 16/39] More capitalization errors --- internal/database/memory/keymanagement.go | 2 +- pkg/core/capability/ssh/ssh.go | 2 +- pkg/keymanagement/keymanagement.go | 30 ++--------------------- 3 files changed, 4 insertions(+), 30 deletions(-) diff --git a/internal/database/memory/keymanagement.go b/internal/database/memory/keymanagement.go index fb89cc45..bdfeb098 100644 --- a/internal/database/memory/keymanagement.go +++ b/internal/database/memory/keymanagement.go @@ -27,7 +27,7 @@ func (database *InMemoryKeyManagementDatabase) Create(name string, keypair keyma func (database *InMemoryKeyManagementDatabase) Read(id string) (keymanagement.KeyPair, error) { keypair, ok := database.keys[id] if !ok { - return keymanagement.KeyPair{}, fmt.Errorf("Could not find key named %s", id) + return keymanagement.KeyPair{}, fmt.Errorf("could not find key named %s", id) } return keypair, nil } diff --git a/pkg/core/capability/ssh/ssh.go b/pkg/core/capability/ssh/ssh.go index d5f627e7..525a235b 100644 --- a/pkg/core/capability/ssh/ssh.go +++ b/pkg/core/capability/ssh/ssh.go @@ -108,7 +108,7 @@ func (sshCapability *SshCapability) getConfig(authentication cacao.Authenticatio config.Auth = []ssh.AuthMethod{ ssh.Password(authentication.Password)} } else { - return config, fmt.Errorf("No authentication method given in user-auth") + return config, fmt.Errorf("no authentication method given in user-auth") } return config, nil diff --git a/pkg/keymanagement/keymanagement.go b/pkg/keymanagement/keymanagement.go index d92b580f..fb66bbe6 100644 --- a/pkg/keymanagement/keymanagement.go +++ b/pkg/keymanagement/keymanagement.go @@ -45,43 +45,17 @@ func (management *KeyManagement) GetPrivate(name string) (ssh.Signer, error) { } return keypair.Private, nil } -func parsePrivateKey(filename string, passphrase string) (ssh.Signer, error) { - log.Tracef("Opening private key from path %s", filename) - file_buffer, err := os.ReadFile(filename) - if err != nil { - return nil, err - } - if passphrase == "" { - return ssh.ParsePrivateKey(file_buffer) - } else { - return ssh.ParsePrivateKeyWithPassphrase(file_buffer, []byte(passphrase)) - } -} -func parsePublicKey(filename string) (ssh.PublicKey, error) { - log.Tracef("Opening public key from path %s", filename) - file, err := os.Open(filename) - if err != nil { - return nil, err - } - file_buffer := make([]byte, 2048) - if _, err = file.Read(file_buffer); err != nil { - return nil, err - } - key, _, _, _, err := ssh.ParseAuthorizedKey(file_buffer) - return key, err - -} func (management *KeyManagement) Insert(public string, private string, passphrase string, name string) error { if _, err := management.GetKeyPair(name); err == nil { - return fmt.Errorf("Key with name already exists: %s (error: %s)", name, err) + return fmt.Errorf("key with name already exists: %s (error: %s)", name, err) } return management.insertInternal(public, private, passphrase, name) } func (management *KeyManagement) Update(public string, private string, passphrase string, name string) error { if _, err := management.GetKeyPair(name); err != nil { - return fmt.Errorf("No such key exists: %s (error: %s)", name, err) + return fmt.Errorf("no such key exists: %s (error: %s)", name, err) } return management.insertInternal(public, private, passphrase, name) } From 62aa23e5270d10feaa9ccb1f7ac3375576fe0417 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Tue, 19 Aug 2025 10:57:48 +0200 Subject: [PATCH 17/39] Fixing lint errors --- internal/database/mongodb/mongo.go | 10 +++++++++- pkg/api/keymanagement/keymanagement_api.go | 7 +++++-- pkg/keymanagement/keymanagement.go | 13 ++++++++----- pkg/keymanagement/keymanagement_test.go | 2 +- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/internal/database/mongodb/mongo.go b/internal/database/mongodb/mongo.go index 5a307f43..06764243 100644 --- a/internal/database/mongodb/mongo.go +++ b/internal/database/mongodb/mongo.go @@ -82,8 +82,16 @@ func SetupMongodb(uri string, username string, password string) error { } cacaoPlayBookRepo, err = NewMongoCollection[cacao.Playbook](mongoclient, "soarca", "cacoa_playbook_collection") + if err != nil { + log.Error("failed to setup playbook MongoCollection, error msg: ", err.Error()) + return err + } keyManagementRepo, err = NewMongoCollection[keymanagementrepository.KeyPairEntry](mongoclient, "keymanagement", "keymanagement_collection") - return err + if err != nil { + log.Error("failed to setup kms MongoCollection, error msg: ", err.Error()) + return err + } + return nil } // helper function to poperly obtain whether object is already in the database store diff --git a/pkg/api/keymanagement/keymanagement_api.go b/pkg/api/keymanagement/keymanagement_api.go index 5f4402c8..e88c29e6 100644 --- a/pkg/api/keymanagement/keymanagement_api.go +++ b/pkg/api/keymanagement/keymanagement_api.go @@ -111,7 +111,6 @@ func (handler *KeyManagementHandler) UpdateKey(context *gin.Context) { log.Error("Update key failed to insert: ", err.Error()) apiError.SendErrorResponse(context, http.StatusBadRequest, "Failed to update key on server side", "PATCH /keymanagement/:keyname", "") return - } log.Trace("Updated key ", keyname) context.JSON(http.StatusOK, Empty{}) @@ -129,7 +128,11 @@ func (handler *KeyManagementHandler) UpdateKey(context *gin.Context) { // @Router /keymanagement/:keyname/ [DELETE] func (handler *KeyManagementHandler) RevokeKey(context *gin.Context) { keyname := context.Param("keyname") - handler.Manager.Revoke(keyname) + if err := handler.Manager.Revoke(keyname); err != nil { + log.Error("Failed to remove key:", err.Error()) + apiError.SendErrorResponse(context, http.StatusBadRequest, "Failed to delete key on server side", "DELETE /keymanagement/:keyname", "") + return + } context.JSON(http.StatusOK, Empty{}) log.Trace("Removed key ", keyname) } diff --git a/pkg/keymanagement/keymanagement.go b/pkg/keymanagement/keymanagement.go index fb66bbe6..688903ae 100644 --- a/pkg/keymanagement/keymanagement.go +++ b/pkg/keymanagement/keymanagement.go @@ -2,7 +2,6 @@ package keymanagement import ( "fmt" - "os" "reflect" keymanagementrepository "soarca/internal/database/keymanagement" "soarca/internal/logger" @@ -28,6 +27,7 @@ func InitKeyManagement(database keymanagementrepository.IKeyManagementRepository } func (management *KeyManagement) GetKeyPair(name string) (*keys.KeyPair, error) { + log.Trace("Getting keypair named", name) keypair, ok := management.cached_keys[name] if !ok { keypair, err := management.database.Read(name) @@ -61,6 +61,7 @@ func (management *KeyManagement) Update(public string, private string, passphras } func (management *KeyManagement) insertInternal(public string, private string, passphrase string, name string) error { + log.Trace("Inserting keypair named", name) public_key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(public)) if err != nil { return fmt.Errorf("parsing public key: %s", err) @@ -76,11 +77,11 @@ func (management *KeyManagement) insertInternal(public string, private string, p } keypair := keys.KeyPair{Public: public_key, Private: private_key} management.cached_keys[name] = keypair - management.database.Create(name, keypair) - return nil + return management.database.Create(name, keypair) } func (management *KeyManagement) ListAllNames() []string { + log.Trace("Listing all keys") keys := make([]string, len(management.cached_keys)) index := 0 for key := range management.cached_keys { @@ -90,7 +91,9 @@ func (management *KeyManagement) ListAllNames() []string { return keys } -func (management *KeyManagement) Revoke(keyname string) { - management.database.Delete(keyname) +func (management *KeyManagement) Revoke(keyname string) error { + log.Trace("Deleting keypair named", keyname) + err := management.database.Delete(keyname) delete(management.cached_keys, keyname) + return err } diff --git a/pkg/keymanagement/keymanagement_test.go b/pkg/keymanagement/keymanagement_test.go index 7e4506d4..eb0b4e81 100644 --- a/pkg/keymanagement/keymanagement_test.go +++ b/pkg/keymanagement/keymanagement_test.go @@ -25,7 +25,7 @@ func init() { func TestRevoke(t *testing.T) { addKey(t, testkey) assert.True(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey)) - globalKeyManagement.Revoke(testkey) + assert.Nil(t, globalKeyManagement.Revoke(testkey)) assert.False(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey)) } func addKey(t *testing.T, keyname string) { From 92008b076e515b0089b7f8518315090d0ca0fce4 Mon Sep 17 00:00:00 2001 From: Thijs Heijligenberg Date: Tue, 19 Aug 2025 11:10:35 +0200 Subject: [PATCH 18/39] Removed cached keys --- pkg/api/keymanagement/keymanagement_api.go | 7 ++++- pkg/keymanagement/keymanagement.go | 30 ++++++---------------- pkg/keymanagement/keymanagement_test.go | 12 ++++++--- 3 files changed, 23 insertions(+), 26 deletions(-) diff --git a/pkg/api/keymanagement/keymanagement_api.go b/pkg/api/keymanagement/keymanagement_api.go index e88c29e6..cdcd133e 100644 --- a/pkg/api/keymanagement/keymanagement_api.go +++ b/pkg/api/keymanagement/keymanagement_api.go @@ -42,7 +42,12 @@ func NewKeyManagementHandler(manager *keymanagement.KeyManagement) *KeyManagemen // @failure 400 {object} api.Error // @Router /keymanagement/ [GET] func (handler *KeyManagementHandler) GetKeys(context *gin.Context) { - keyInfo := handler.Manager.ListAllNames() + keyInfo, err := handler.Manager.ListAllNames() + if err != nil { + log.Error("Listing keys has failed: ", err.Error()) + apiError.SendErrorResponse(context, http.StatusBadRequest, "Failed to read keys on server side", "GET /keymanagement/", "") + return + } log.Trace("Listing all key names") context.JSON(http.StatusOK, keyInfo) } diff --git a/pkg/keymanagement/keymanagement.go b/pkg/keymanagement/keymanagement.go index 688903ae..3af0caf1 100644 --- a/pkg/keymanagement/keymanagement.go +++ b/pkg/keymanagement/keymanagement.go @@ -18,23 +18,18 @@ func init() { } type KeyManagement struct { - database keymanagementrepository.IKeyManagementRepository - cached_keys map[string]keys.KeyPair + database keymanagementrepository.IKeyManagementRepository } func InitKeyManagement(database keymanagementrepository.IKeyManagementRepository) *KeyManagement { - return &KeyManagement{database: database, cached_keys: make(map[string]keys.KeyPair)} + return &KeyManagement{database: database} } func (management *KeyManagement) GetKeyPair(name string) (*keys.KeyPair, error) { log.Trace("Getting keypair named", name) - keypair, ok := management.cached_keys[name] - if !ok { - keypair, err := management.database.Read(name) - if err != nil { - return nil, err - } - return &keypair, nil + keypair, err := management.database.Read(name) + if err != nil { + return nil, err } return &keypair, nil } @@ -76,24 +71,15 @@ func (management *KeyManagement) insertInternal(public string, private string, p return fmt.Errorf("parsing private key: %s", err) } keypair := keys.KeyPair{Public: public_key, Private: private_key} - management.cached_keys[name] = keypair return management.database.Create(name, keypair) } -func (management *KeyManagement) ListAllNames() []string { +func (management *KeyManagement) ListAllNames() ([]string, error) { log.Trace("Listing all keys") - keys := make([]string, len(management.cached_keys)) - index := 0 - for key := range management.cached_keys { - keys[index] = key - index++ - } - return keys + return management.database.GetKeyNames() } func (management *KeyManagement) Revoke(keyname string) error { log.Trace("Deleting keypair named", keyname) - err := management.database.Delete(keyname) - delete(management.cached_keys, keyname) - return err + return management.database.Delete(keyname) } diff --git a/pkg/keymanagement/keymanagement_test.go b/pkg/keymanagement/keymanagement_test.go index eb0b4e81..f1f07ffd 100644 --- a/pkg/keymanagement/keymanagement_test.go +++ b/pkg/keymanagement/keymanagement_test.go @@ -24,9 +24,9 @@ func init() { func TestRevoke(t *testing.T) { addKey(t, testkey) - assert.True(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey)) + assert.True(t, slices.Contains(allNames(t), testkey)) assert.Nil(t, globalKeyManagement.Revoke(testkey)) - assert.False(t, slices.Contains(globalKeyManagement.ListAllNames(), testkey)) + assert.False(t, slices.Contains(allNames(t), testkey)) } func addKey(t *testing.T, keyname string) { pubkey_path := path.Join(testkey_dir(), "test-key.pub") @@ -47,7 +47,13 @@ func addKey(t *testing.T, keyname string) { assert.Nil(t, pubkey_file.Close()) } +func allNames(t *testing.T) []string { + allnames, err := globalKeyManagement.ListAllNames() + assert.Nil(t, err) + return allnames +} + func TestAddKey(t *testing.T) { addKey(t, testkey) - assert.True(t, slices.Equal(globalKeyManagement.ListAllNames(), []string{testkey})) + assert.True(t, slices.Equal(allNames(t), []string{testkey})) } From 6494b0275d6c3fecde68da6a38ce21c930dbff59 Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Tue, 5 May 2026 14:23:49 +0200 Subject: [PATCH 19/39] Revert "Fixed the logger level configuration" This reverts commit 23315e1a7bb6ae7bd80c2f34c6acf09022a58ed7. --- internal/logger/logger.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 87963cc2..4b4544b9 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -31,7 +31,7 @@ const ( Trace ) -func severityFromString(name string) Severity { +func (severity Severity) fromString(name string) Severity { nameToLower := strings.ToLower(name) switch nameToLower { case "panic": @@ -106,12 +106,7 @@ func Logger(name string, severity Severity, fileName FileName, format Format) *L setFormat(instance, globalLogFormat) - globalSeverityLevel := severityFromString(globalLogSeverity) - if globalSeverityLevel > severity { - instance.SetLevel(logrus.Level(globalSeverityLevel)) - } else { - instance.SetLevel(logrus.Level(severity)) - } + instance.SetLevel(logrus.Level(severity.fromString(globalLogSeverity))) if globalOperationMode == development { if fileName != "" { From 07c3e8a2bdfdbd8a3746ffd4e3be1af8ed85014f Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:52:38 +0200 Subject: [PATCH 20/39] Reverted: In memory database changes --- .../database/memory/{playbook.go => memory.go} | 18 +++++++++--------- .../{playbook_test.go => memory_test.go} | 12 ++++++------ 2 files changed, 15 insertions(+), 15 deletions(-) rename internal/database/memory/{playbook.go => memory.go} (70%) rename internal/database/memory/{playbook_test.go => memory_test.go} (96%) diff --git a/internal/database/memory/playbook.go b/internal/database/memory/memory.go similarity index 70% rename from internal/database/memory/playbook.go rename to internal/database/memory/memory.go index ad94591f..ef0bddc8 100644 --- a/internal/database/memory/playbook.go +++ b/internal/database/memory/memory.go @@ -7,15 +7,15 @@ import ( "soarca/pkg/models/decoder" ) -type InMemoryPlaybookDatabase struct { +type InMemoryDatabase struct { playbooks map[string]cacao.Playbook } -func NewPlaybookDatabase() *InMemoryPlaybookDatabase { - return &InMemoryPlaybookDatabase{playbooks: make(map[string]cacao.Playbook)} +func New() *InMemoryDatabase { + return &InMemoryDatabase{playbooks: make(map[string]cacao.Playbook)} } -func (memory *InMemoryPlaybookDatabase) GetPlaybooks() ([]cacao.Playbook, error) { +func (memory *InMemoryDatabase) GetPlaybooks() ([]cacao.Playbook, error) { size := len(memory.playbooks) playbookList := make([]cacao.Playbook, 0, size) for _, playbook := range memory.playbooks { @@ -25,7 +25,7 @@ func (memory *InMemoryPlaybookDatabase) GetPlaybooks() ([]cacao.Playbook, error) return playbookList, nil } -func (memory *InMemoryPlaybookDatabase) GetPlaybookMetas() ([]api.PlaybookMeta, error) { +func (memory *InMemoryDatabase) GetPlaybookMetas() ([]api.PlaybookMeta, error) { size := len(memory.playbooks) playbookList := make([]api.PlaybookMeta, 0, size) for _, playbook := range memory.playbooks { @@ -40,7 +40,7 @@ func (memory *InMemoryPlaybookDatabase) GetPlaybookMetas() ([]api.PlaybookMeta, return playbookList, nil } -func (memory *InMemoryPlaybookDatabase) Create(json *[]byte) (cacao.Playbook, error) { +func (memory *InMemoryDatabase) Create(json *[]byte) (cacao.Playbook, error) { if json == nil { return cacao.Playbook{}, errors.New("empty input") @@ -57,7 +57,7 @@ func (memory *InMemoryPlaybookDatabase) Create(json *[]byte) (cacao.Playbook, er return memory.playbooks[result.ID], nil } -func (memory *InMemoryPlaybookDatabase) Read(id string) (cacao.Playbook, error) { +func (memory *InMemoryDatabase) Read(id string) (cacao.Playbook, error) { playbook, ok := memory.playbooks[id] if !ok { return cacao.Playbook{}, errors.New("playbook is not in repository") @@ -65,7 +65,7 @@ func (memory *InMemoryPlaybookDatabase) Read(id string) (cacao.Playbook, error) return playbook, nil } -func (memory *InMemoryPlaybookDatabase) Update(id string, json *[]byte) (cacao.Playbook, error) { +func (memory *InMemoryDatabase) Update(id string, json *[]byte) (cacao.Playbook, error) { playbook, err := memory.Read(id) if err != nil { return playbook, err @@ -78,7 +78,7 @@ func (memory *InMemoryPlaybookDatabase) Update(id string, json *[]byte) (cacao.P return *updatePlaybook, nil } -func (memory *InMemoryPlaybookDatabase) Delete(id string) error { +func (memory *InMemoryDatabase) Delete(id string) error { delete(memory.playbooks, id) return nil } diff --git a/internal/database/memory/playbook_test.go b/internal/database/memory/memory_test.go similarity index 96% rename from internal/database/memory/playbook_test.go rename to internal/database/memory/memory_test.go index a7a5408b..d206aae5 100644 --- a/internal/database/memory/playbook_test.go +++ b/internal/database/memory/memory_test.go @@ -32,7 +32,7 @@ func TestCreate(t *testing.T) { } var workflow = decoder.DecodeValidate(byteValue) - mem := NewPlaybookDatabase() + mem := New() playbook, err := mem.Create(&byteValue) assert.Equal(t, err, nil) assert.Equal(t, playbook, workflow) @@ -56,7 +56,7 @@ func TestRead(t *testing.T) { var workflow = decoder.DecodeValidate(byteValue) - mem := NewPlaybookDatabase() + mem := New() empty, err := mem.Read(workflow.ID) assert.Equal(t, err, errors.New("playbook is not in repository")) assert.Equal(t, empty, cacao.Playbook{}) @@ -87,7 +87,7 @@ func TestUpdate(t *testing.T) { var workflow = decoder.DecodeValidate(byteValue) - mem := NewPlaybookDatabase() + mem := New() empty, err := mem.Update(workflow.ID, nil) assert.Equal(t, err, errors.New("playbook is not in repository")) assert.Equal(t, empty, cacao.Playbook{}) @@ -129,7 +129,7 @@ func TestDelete(t *testing.T) { var workflow = decoder.DecodeValidate(byteValue) - mem := NewPlaybookDatabase() + mem := New() err = mem.Delete(workflow.ID) assert.Equal(t, err, nil) @@ -166,7 +166,7 @@ func TestGetAllPlaybooks(t *testing.T) { var workflow = decoder.DecodeValidate(byteValue) - mem := NewPlaybookDatabase() + mem := New() list := []string{ "playbook--f47d4081-21ed-4f21-9d05-6b368d73da30", @@ -214,7 +214,7 @@ func TestGetAllPlaybookMetas(t *testing.T) { var workflow = decoder.DecodeValidate(byteValue) - mem := NewPlaybookDatabase() + mem := New() list := []string{ "playbook--f47d4081-21ed-4f21-9d05-6b368d73da30", From cb99818203c7c6e9c3ab65a7721e76c1a9fa8e0e Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:52:49 +0200 Subject: [PATCH 21/39] Moved to temporary package --- internal/database/memory/{ => keymanagement}/keymanagement.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename internal/database/memory/{ => keymanagement}/keymanagement.go (93%) diff --git a/internal/database/memory/keymanagement.go b/internal/database/memory/keymanagement/keymanagement.go similarity index 93% rename from internal/database/memory/keymanagement.go rename to internal/database/memory/keymanagement/keymanagement.go index bdfeb098..3eee406f 100644 --- a/internal/database/memory/keymanagement.go +++ b/internal/database/memory/keymanagement/keymanagement.go @@ -1,4 +1,4 @@ -package memory +package memorykms import ( "fmt" @@ -9,7 +9,7 @@ type InMemoryKeyManagementDatabase struct { keys map[string]keymanagement.KeyPair } -func NewKeyManagementDatabase() *InMemoryKeyManagementDatabase { +func New() *InMemoryKeyManagementDatabase { return &InMemoryKeyManagementDatabase{keys: make(map[string]keymanagement.KeyPair)} } From db8895cebf0046c6842bbd6cc5281253266c4b54 Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:52:58 +0200 Subject: [PATCH 22/39] Reverted Mongo changes --- internal/database/mongodb/mongo.go | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/internal/database/mongodb/mongo.go b/internal/database/mongodb/mongo.go index 06764243..15964997 100644 --- a/internal/database/mongodb/mongo.go +++ b/internal/database/mongodb/mongo.go @@ -6,7 +6,6 @@ import ( "reflect" "time" - keymanagementrepository "soarca/internal/database/keymanagement" "soarca/internal/database/projections" cacao "soarca/pkg/models/cacao" @@ -19,12 +18,11 @@ const writeErrorDuplicationCode = 11000 var ( cacaoPlayBookRepo *mongoCollection[cacao.Playbook] - keyManagementRepo *mongoCollection[keymanagementrepository.KeyPairEntry] mongoclient *mongo.Client ) type dbtypes interface { - cacao.Playbook | keymanagementrepository.KeyPairEntry // | for other supported types + cacao.Playbook // | for other supported types } type mongoCollection[T dbtypes] struct { @@ -59,9 +57,6 @@ func (mongoOpts mongoFindOptions) GetProjectionByType(interface{}) interface{} { func GetCacaoRepo() *mongoCollection[cacao.Playbook] { return cacaoPlayBookRepo } -func GetKeyManagementRepo() *mongoCollection[keymanagementrepository.KeyPairEntry] { - return keyManagementRepo -} // func GetMongoClient() *mongodbClient { // return mongoclient @@ -82,16 +77,7 @@ func SetupMongodb(uri string, username string, password string) error { } cacaoPlayBookRepo, err = NewMongoCollection[cacao.Playbook](mongoclient, "soarca", "cacoa_playbook_collection") - if err != nil { - log.Error("failed to setup playbook MongoCollection, error msg: ", err.Error()) - return err - } - keyManagementRepo, err = NewMongoCollection[keymanagementrepository.KeyPairEntry](mongoclient, "keymanagement", "keymanagement_collection") - if err != nil { - log.Error("failed to setup kms MongoCollection, error msg: ", err.Error()) - return err - } - return nil + return err } // helper function to poperly obtain whether object is already in the database store @@ -234,7 +220,7 @@ func NewMongoCollection[T dbtypes](mongo *mongo.Client, dbName string, colName s } func InitMongoClient(mongo_uri string, username string, password string) error { - log.Trace("Trying to setup new MongoClient at uri ", mongo_uri) + log.Trace("Trying to setup new MongoClient") var err error if mongo_uri == "" { log.Error("mongo uri not valid, because empty") @@ -246,7 +232,6 @@ func InitMongoClient(mongo_uri string, username string, password string) error { return errors.New("username or password not correctly set") } - log.Trace("Logging in to mongo with username ", username, " and password ", password) clientOpts := options.Client().ApplyURI(mongo_uri).SetAuth(options.Credential{ Username: username, Password: password, From 7bb61cebf59feb4b3edab4c1b0722673d2882e7d Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Mon, 13 Jul 2026 17:23:16 +0200 Subject: [PATCH 23/39] renamed key management --- internal/controller/controller.go | 7 ++++--- internal/database/memory/keymanagement/keymanagement.go | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 870faac8..5d3da1e7 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -7,6 +7,7 @@ import ( "reflect" keymanagementrepository "soarca/internal/database/keymanagement" "soarca/internal/database/memory" + "soarca/internal/database/memory/memorykms" "soarca/internal/logger" "soarca/pkg/core/capability" @@ -165,12 +166,12 @@ func (controller *Controller) setupDatabase() error { return err } controller.playbookRepo = playbookrepository.SetupPlaybookRepository(mongo.GetCacaoRepo(), mongo.DefaultLimitOpts()) - controller.keyManagementRepo = keymanagementrepository.SetupKeyManagementRepository(mongo.GetKeyManagementRepo(), mongo.DefaultLimitOpts()) + controller.keyManagementRepo = memorykms.New() } else { // Use in memory database log.Info("Setting up in-memory database") - controller.playbookRepo = memory.NewPlaybookDatabase() - controller.keyManagementRepo = memory.NewKeyManagementDatabase() + controller.playbookRepo = memory.New() + controller.keyManagementRepo = memorykms.New() } return nil diff --git a/internal/database/memory/keymanagement/keymanagement.go b/internal/database/memory/keymanagement/keymanagement.go index 3eee406f..c78f7361 100644 --- a/internal/database/memory/keymanagement/keymanagement.go +++ b/internal/database/memory/keymanagement/keymanagement.go @@ -1,4 +1,4 @@ -package memorykms +package keymanagement import ( "fmt" From 1cdf219af119a49b5106fb99b0b7c26d44d71693 Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Tue, 14 Jul 2026 09:37:29 +0200 Subject: [PATCH 24/39] Allowed any creation for collection in mongo anticipating refactor --- internal/database/mongodb/mongo.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/database/mongodb/mongo.go b/internal/database/mongodb/mongo.go index 15964997..0051f37a 100644 --- a/internal/database/mongodb/mongo.go +++ b/internal/database/mongodb/mongo.go @@ -25,7 +25,7 @@ type dbtypes interface { cacao.Playbook // | for other supported types } -type mongoCollection[T dbtypes] struct { +type mongoCollection[T any] struct { Collection *mongo.Collection collectionname string } @@ -197,7 +197,7 @@ func (mongocollection *mongoCollection[T]) Delete(id string) error { return err } -func NewMongoCollection[T dbtypes](mongo *mongo.Client, dbName string, colName string) (*mongoCollection[T], error) { +func NewMongoCollection[T any](mongo *mongo.Client, dbName string, colName string) (*mongoCollection[T], error) { log.Trace("Setting a new Mongo Collection name: ", colName) if mongo == nil { From 108c9f75f5ee1d1a94f65e1e105724787d83d671 Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Tue, 14 Jul 2026 09:37:39 +0200 Subject: [PATCH 25/39] Ad-Hoc collection creation --- internal/controller/controller.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 5d3da1e7..48e3e990 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -7,7 +7,7 @@ import ( "reflect" keymanagementrepository "soarca/internal/database/keymanagement" "soarca/internal/database/memory" - "soarca/internal/database/memory/memorykms" + memorykms "soarca/internal/database/memory/keymanagement" "soarca/internal/logger" "soarca/pkg/core/capability" @@ -167,6 +167,11 @@ func (controller *Controller) setupDatabase() error { } controller.playbookRepo = playbookrepository.SetupPlaybookRepository(mongo.GetCacaoRepo(), mongo.DefaultLimitOpts()) controller.keyManagementRepo = memorykms.New() + + // Ad-hoc creation of mongo client TODO: refactor mongo client. + kms, err := mongo.NewMongoCollection[keymanagementrepository.KeyPairEntry](mongo.GetCacaoRepo().Collection.Database().Client(), "database-kms", "kms-collection") + controller.keyManagementRepo = keymanagementrepository.SetupKeyManagementRepository(kms, mongo.DefaultLimitOpts()) + } else { // Use in memory database log.Info("Setting up in-memory database") From 75acb4918f974936ed39b747cef7b3582ef1d711 Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Tue, 14 Jul 2026 09:37:52 +0200 Subject: [PATCH 26/39] Updated stray go.mod --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 2e86916f..e16fe1db 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/stretchr/testify v1.10.0 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.1 go.mongodb.org/mongo-driver v1.12.1 golang.org/x/crypto v0.52.0 ) @@ -76,7 +77,6 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/swaggo/swag v1.16.1 // indirect github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect From 80fd9bd160eaea28c0682c032c89e925e9a568a9 Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:52:31 +0200 Subject: [PATCH 27/39] Refactored key management test --- pkg/keymanagement/keymanagement.go | 2 +- pkg/keymanagement/keymanagement_test.go | 61 ++++++++++++++----------- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/pkg/keymanagement/keymanagement.go b/pkg/keymanagement/keymanagement.go index 3af0caf1..b2d6c061 100644 --- a/pkg/keymanagement/keymanagement.go +++ b/pkg/keymanagement/keymanagement.go @@ -21,7 +21,7 @@ type KeyManagement struct { database keymanagementrepository.IKeyManagementRepository } -func InitKeyManagement(database keymanagementrepository.IKeyManagementRepository) *KeyManagement { +func New(database keymanagementrepository.IKeyManagementRepository) *KeyManagement { return &KeyManagement{database: database} } diff --git a/pkg/keymanagement/keymanagement_test.go b/pkg/keymanagement/keymanagement_test.go index f1f07ffd..09383519 100644 --- a/pkg/keymanagement/keymanagement_test.go +++ b/pkg/keymanagement/keymanagement_test.go @@ -4,33 +4,20 @@ import ( "os" "path" "slices" - "soarca/internal/database/memory" + memory "soarca/internal/database/memory/keymanagement" "testing" "github.com/stretchr/testify/assert" ) const testkey string = "test-key" - -func testkey_dir() string { - return path.Join("..", "..", "test", "unittest", "mocks", "mock_utils") -} - -var globalKeyManagement *KeyManagement - -func init() { - globalKeyManagement = InitKeyManagement(memory.NewKeyManagementDatabase()) -} +const testPath = "../../test/unittest/mocks/mock_utils" func TestRevoke(t *testing.T) { - addKey(t, testkey) - assert.True(t, slices.Contains(allNames(t), testkey)) - assert.Nil(t, globalKeyManagement.Revoke(testkey)) - assert.False(t, slices.Contains(allNames(t), testkey)) -} -func addKey(t *testing.T, keyname string) { - pubkey_path := path.Join(testkey_dir(), "test-key.pub") - privkey_path := path.Join(testkey_dir(), "test-key") + keyManagement := New(memory.New()) + + pubkey_path := path.Join(testPath, "test-key.pub") + privkey_path := path.Join(testPath, "test-key") pubkey_file, err := os.Open(pubkey_path) assert.Nil(t, err) @@ -42,18 +29,40 @@ func addKey(t *testing.T, keyname string) { assert.Nil(t, err) _, err = privkey_file.Read(privkey_buf) assert.Nil(t, err) - assert.Nil(t, globalKeyManagement.Insert(string(pubkey_buf), string(privkey_buf), "", keyname)) + assert.Nil(t, keyManagement.Insert(string(pubkey_buf), string(privkey_buf), "", testkey)) assert.Nil(t, privkey_file.Close()) assert.Nil(t, pubkey_file.Close()) -} -func allNames(t *testing.T) []string { - allnames, err := globalKeyManagement.ListAllNames() + allNames, err := keyManagement.ListAllNames() + assert.Nil(t, err) + assert.True(t, slices.Contains(allNames, testkey)) + assert.Nil(t, keyManagement.Revoke(testkey)) + allNames, err = keyManagement.ListAllNames() assert.Nil(t, err) - return allnames + assert.Equal(t, false, slices.Contains(allNames, testkey)) } func TestAddKey(t *testing.T) { - addKey(t, testkey) - assert.True(t, slices.Equal(allNames(t), []string{testkey})) + keyManagement := New(memory.New()) + + pubkey_path := path.Join(testPath, "test-key.pub") + privkey_path := path.Join(testPath, "test-key") + pubkey_file, err := os.Open(pubkey_path) + assert.Nil(t, err) + + privkey_file, err := os.Open(privkey_path) + assert.Nil(t, err) + pubkey_buf := make([]byte, 2048) + privkey_buf := make([]byte, 2048) + _, err = pubkey_file.Read(pubkey_buf) + assert.Nil(t, err) + _, err = privkey_file.Read(privkey_buf) + assert.Nil(t, err) + assert.Nil(t, keyManagement.Insert(string(pubkey_buf), string(privkey_buf), "", testkey)) + assert.Nil(t, privkey_file.Close()) + assert.Nil(t, pubkey_file.Close()) + + allNames, err := keyManagement.ListAllNames() + assert.Nil(t, err) + assert.True(t, slices.Equal(allNames, []string{testkey})) } From b1d4a11e419e50789695c5983b1afcdff9e906a1 Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:52:46 +0200 Subject: [PATCH 28/39] Refactored KMS test --- test/integration/kms/kms_test.go | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/test/integration/kms/kms_test.go b/test/integration/kms/kms_test.go index 32a80323..a930f427 100644 --- a/test/integration/kms/kms_test.go +++ b/test/integration/kms/kms_test.go @@ -4,7 +4,7 @@ import ( "fmt" "os" "path" - "soarca/internal/database/memory" + memory "soarca/internal/database/memory/keymanagement" "soarca/pkg/core/capability" "soarca/pkg/core/capability/ssh" "soarca/pkg/keymanagement" @@ -18,28 +18,20 @@ import ( var globalKeyManagement *keymanagement.KeyManagement -func init() { - globalKeyManagement = keymanagement.InitKeyManagement(memory.NewKeyManagementDatabase()) -} - const testkey string = "test" +const testPath = "../../../deployments/docker/testing/ssh-kms-testing" -func testkey_dir() string { - return path.Join("..", "..", "..", "deployments", "docker", "testing", "ssh-kms-test") -} -func addTestKey(t *testing.T) { - pubkey_path := path.Join(testkey_dir(), testkey+".pub") - privkey_path := path.Join(testkey_dir(), testkey) +func TestSshConnection(t *testing.T) { + globalKeyManagement = keymanagement.New(memory.New()) + sshCapability := ssh.SshCapability{Keys: globalKeyManagement} + + pubkey_path := path.Join(testPath, testkey+".pub") + privkey_path := path.Join(testPath, testkey) pubkey, err := os.ReadFile(pubkey_path) assert.Nil(t, err) privkey, err := os.ReadFile(privkey_path) assert.Nil(t, err) assert.Nil(t, globalKeyManagement.Insert(string(pubkey), string(privkey), "", testkey)) -} - -func TestSshConnection(t *testing.T) { - sshCapability := ssh.SshCapability{Keys: globalKeyManagement} - addTestKey(t) expectedCommand := cacao.Command{ Type: "ssh", From 2bb39242faede75a1f203fe34c8da539c125a510 Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:56:19 +0200 Subject: [PATCH 29/39] Moved test-keys keymanagement to test/kms --- pkg/keymanagement/keymanagement_test.go | 2 +- test/{unittest/mocks/mock_utils => kms}/test-key | 0 test/{unittest/mocks/mock_utils => kms}/test-key.pub | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename test/{unittest/mocks/mock_utils => kms}/test-key (100%) rename test/{unittest/mocks/mock_utils => kms}/test-key.pub (100%) diff --git a/pkg/keymanagement/keymanagement_test.go b/pkg/keymanagement/keymanagement_test.go index 09383519..72afce4e 100644 --- a/pkg/keymanagement/keymanagement_test.go +++ b/pkg/keymanagement/keymanagement_test.go @@ -11,7 +11,7 @@ import ( ) const testkey string = "test-key" -const testPath = "../../test/unittest/mocks/mock_utils" +const testPath = "../../test/kms" func TestRevoke(t *testing.T) { keyManagement := New(memory.New()) diff --git a/test/unittest/mocks/mock_utils/test-key b/test/kms/test-key similarity index 100% rename from test/unittest/mocks/mock_utils/test-key rename to test/kms/test-key diff --git a/test/unittest/mocks/mock_utils/test-key.pub b/test/kms/test-key.pub similarity index 100% rename from test/unittest/mocks/mock_utils/test-key.pub rename to test/kms/test-key.pub From aede9362946d7f6fd1e33a00800b9aa5866980f0 Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:56:44 +0200 Subject: [PATCH 30/39] Initialisation cleanups --- internal/controller/controller.go | 5 ++++- internal/database/mongodb/mongo.go | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 48e3e990..cd1ac479 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -170,6 +170,9 @@ func (controller *Controller) setupDatabase() error { // Ad-hoc creation of mongo client TODO: refactor mongo client. kms, err := mongo.NewMongoCollection[keymanagementrepository.KeyPairEntry](mongo.GetCacaoRepo().Collection.Database().Client(), "database-kms", "kms-collection") + if err != nil { + return errors.New("could not create database collection for kms") + } controller.keyManagementRepo = keymanagementrepository.SetupKeyManagementRepository(kms, mongo.DefaultLimitOpts()) } else { @@ -183,7 +186,7 @@ func (controller *Controller) setupDatabase() error { } func (controller *Controller) setupKeyManagement() error { - controller.keyManagement = keymanagement.InitKeyManagement(controller.keyManagementRepo) + controller.keyManagement = keymanagement.New(controller.keyManagementRepo) return nil } diff --git a/internal/database/mongodb/mongo.go b/internal/database/mongodb/mongo.go index 0051f37a..02c9f9bd 100644 --- a/internal/database/mongodb/mongo.go +++ b/internal/database/mongodb/mongo.go @@ -21,9 +21,9 @@ var ( mongoclient *mongo.Client ) -type dbtypes interface { - cacao.Playbook // | for other supported types -} +// type dbtypes interface { +// cacao.Playbook // | for other supported types +// } type mongoCollection[T any] struct { Collection *mongo.Collection From 0ab247941bedafc8260951d18a2f1269a770f38e Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Tue, 14 Jul 2026 12:01:24 +0200 Subject: [PATCH 31/39] Updated docs api spec --- docs/static/openapi/swagger.json | 26 +++++++++++++++++++------- docs/static/openapi/swagger.yaml | 23 +++++++++++++++-------- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/docs/static/openapi/swagger.json b/docs/static/openapi/swagger.json index f14e4f60..ae9e74e2 100644 --- a/docs/static/openapi/swagger.json +++ b/docs/static/openapi/swagger.json @@ -37,7 +37,7 @@ }, "/keymanagement/:keyname/": { "put": { - "description": "adds a key to the KMS; load key into cache and write file", + "description": "adds a key to the KMS; load key into cache and write to database", "produces": [ "application/json" ], @@ -94,18 +94,27 @@ } } } - } - }, - "/keymanagement/refresh/": { - "post": { - "description": "refresh the KMS and re-parse the underlying directory", + }, + "patch": { + "description": "update a key in the KMS; load key into cache and write to database", "produces": [ "application/json" ], "tags": [ "keymanagement" ], - "summary": "refresh the KMS system, which allows the user to include manually added files in the KMS", + "summary": "update key in KMS", + "parameters": [ + { + "description": "key", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.KeyManagementKey" + } + } + ], "responses": { "200": { "description": "OK", @@ -773,6 +782,9 @@ "api.KeyManagementKey": { "type": "object", "properties": { + "passphrase": { + "type": "string" + }, "private": { "type": "string" }, diff --git a/docs/static/openapi/swagger.yaml b/docs/static/openapi/swagger.yaml index b590e453..944be4f4 100644 --- a/docs/static/openapi/swagger.yaml +++ b/docs/static/openapi/swagger.yaml @@ -75,6 +75,8 @@ definitions: type: object api.KeyManagementKey: properties: + passphrase: + type: string private: type: string public: @@ -773,8 +775,8 @@ paths: summary: remove key from KMS tags: - keymanagement - put: - description: adds a key to the KMS; load key into cache and write file + patch: + description: update a key in the KMS; load key into cache and write to database parameters: - description: key in: body @@ -793,12 +795,18 @@ paths: description: Bad Request schema: $ref: '#/definitions/api.Error' - summary: add key to KMS + summary: update key in KMS tags: - keymanagement - /keymanagement/refresh/: - post: - description: refresh the KMS and re-parse the underlying directory + put: + description: adds a key to the KMS; load key into cache and write to database + parameters: + - description: key + in: body + name: data + required: true + schema: + $ref: '#/definitions/api.KeyManagementKey' produces: - application/json responses: @@ -810,8 +818,7 @@ paths: description: Bad Request schema: $ref: '#/definitions/api.Error' - summary: refresh the KMS system, which allows the user to include manually added - files in the KMS + summary: add key to KMS tags: - keymanagement /manual/: From f8f09e7c5c822c3f78bce55add57817d4a5ad4ed Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Tue, 14 Jul 2026 13:29:42 +0200 Subject: [PATCH 32/39] Fixed a typo in the path --- test/integration/kms/kms_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/kms/kms_test.go b/test/integration/kms/kms_test.go index a930f427..5b5cf8f5 100644 --- a/test/integration/kms/kms_test.go +++ b/test/integration/kms/kms_test.go @@ -19,7 +19,7 @@ import ( var globalKeyManagement *keymanagement.KeyManagement const testkey string = "test" -const testPath = "../../../deployments/docker/testing/ssh-kms-testing" +const testPath = "../../../deployments/docker/testing/ssh-kms-test" func TestSshConnection(t *testing.T) { globalKeyManagement = keymanagement.New(memory.New()) From 85aa98a28172e9c133f102b5ad333c0a46b6c985 Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:26:11 +0200 Subject: [PATCH 33/39] Fixed error if KMS is unset that SOARCA would crash --- pkg/core/capability/ssh/ssh.go | 4 ++++ pkg/core/capability/ssh/ssh_test.go | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/pkg/core/capability/ssh/ssh.go b/pkg/core/capability/ssh/ssh.go index 525a235b..900ccc12 100644 --- a/pkg/core/capability/ssh/ssh.go +++ b/pkg/core/capability/ssh/ssh.go @@ -94,6 +94,10 @@ func (sshCapability *SshCapability) getConfig(authentication cacao.Authenticatio switch authentication.Type { case "user-auth": if authentication.Kms { + if sshCapability.Keys == nil { + return config, errors.New("no KMS was set so it cannot be used by the playbook") + } + if authentication.KmsKeyIdentifier == "" { return config, fmt.Errorf("KMS indicated, but no kms_key_identifier given") } diff --git a/pkg/core/capability/ssh/ssh_test.go b/pkg/core/capability/ssh/ssh_test.go index 2ba58aed..7a2e70d4 100644 --- a/pkg/core/capability/ssh/ssh_test.go +++ b/pkg/core/capability/ssh/ssh_test.go @@ -103,3 +103,13 @@ func TestAddressAndPortCombinationNoIpv4Address(t *testing.T) { result := CombinePortAndAddress(ipv4, port) assert.Equal(t, result, expectedFqdn) } + +// This test needs to be here to see if the sshCapability won't panic when KMS is not set +func TestKmsIsNill(t *testing.T) { + ssh := &SshCapability{Keys: nil} + + auth := cacao.AuthenticationInformation{ID: "1", Type: "user-auth", Kms: true, KmsKeyIdentifier: "id"} + + _, err := ssh.getConfig(auth) + assert.Equal(t, err, errors.New("no KMS was set so it cannot be used by the playbook")) +} From d7aeaa5a8ec29e3bd0f6e04c9662af8dbd0010ff Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:26:27 +0200 Subject: [PATCH 34/39] Added the ENABLE_SSH_KMS flag into the logic --- internal/controller/controller.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/controller/controller.go b/internal/controller/controller.go index cd1ac479..41544b30 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -186,7 +186,12 @@ func (controller *Controller) setupDatabase() error { } func (controller *Controller) setupKeyManagement() error { - controller.keyManagement = keymanagement.New(controller.keyManagementRepo) + if utils.GetEnv("ENABLE_SSH_KMS", "false") == "true" { + controller.keyManagement = keymanagement.New(controller.keyManagementRepo) + log.Info("KMS is enabled") + } else { + log.Trace("KMS is not enabled") + } return nil } From 815f1de4c11c330764c2db82f09f0b5a98e3ce59 Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Fri, 17 Jul 2026 21:06:24 +0200 Subject: [PATCH 35/39] Introduced KMS interfaces --- pkg/keymanagement/keymanagement.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/pkg/keymanagement/keymanagement.go b/pkg/keymanagement/keymanagement.go index b2d6c061..9d25025d 100644 --- a/pkg/keymanagement/keymanagement.go +++ b/pkg/keymanagement/keymanagement.go @@ -17,6 +17,17 @@ func init() { log = logger.Logger(component, logger.Info, "", logger.Json) } +type IKms interface { + GetPrivate(name string) (ssh.Signer, error) +} + +type IKeyManagementApi interface { + Insert(public string, private string, passphrase string, name string) error + Update(public string, private string, passphrase string, name string) error + ListAllNames() ([]string, error) + Revoke(keyname string) error +} + type KeyManagement struct { database keymanagementrepository.IKeyManagementRepository } @@ -25,7 +36,7 @@ func New(database keymanagementrepository.IKeyManagementRepository) *KeyManageme return &KeyManagement{database: database} } -func (management *KeyManagement) GetKeyPair(name string) (*keys.KeyPair, error) { +func (management *KeyManagement) getKeyPair(name string) (*keys.KeyPair, error) { log.Trace("Getting keypair named", name) keypair, err := management.database.Read(name) if err != nil { @@ -34,7 +45,7 @@ func (management *KeyManagement) GetKeyPair(name string) (*keys.KeyPair, error) return &keypair, nil } func (management *KeyManagement) GetPrivate(name string) (ssh.Signer, error) { - keypair, err := management.GetKeyPair(name) + keypair, err := management.getKeyPair(name) if err != nil { return nil, err } @@ -42,14 +53,14 @@ func (management *KeyManagement) GetPrivate(name string) (ssh.Signer, error) { } func (management *KeyManagement) Insert(public string, private string, passphrase string, name string) error { - if _, err := management.GetKeyPair(name); err == nil { + if _, err := management.getKeyPair(name); err == nil { return fmt.Errorf("key with name already exists: %s (error: %s)", name, err) } return management.insertInternal(public, private, passphrase, name) } func (management *KeyManagement) Update(public string, private string, passphrase string, name string) error { - if _, err := management.GetKeyPair(name); err != nil { + if _, err := management.getKeyPair(name); err != nil { return fmt.Errorf("no such key exists: %s (error: %s)", name, err) } return management.insertInternal(public, private, passphrase, name) From 1e304725697158f99484d9f412c8de9205812d99 Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Fri, 17 Jul 2026 21:07:04 +0200 Subject: [PATCH 36/39] Changed ssh capability dependency to IKms --- pkg/core/capability/ssh/ssh.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/core/capability/ssh/ssh.go b/pkg/core/capability/ssh/ssh.go index 900ccc12..a0b5061c 100644 --- a/pkg/core/capability/ssh/ssh.go +++ b/pkg/core/capability/ssh/ssh.go @@ -22,7 +22,7 @@ const ( ) type SshCapability struct { - Keys *keymanagement.KeyManagement + Keys keymanagement.IKms } var component = reflect.TypeOf(SshCapability{}).PkgPath() From a915709b33de61731d3a47ff50d740d03631be86 Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Fri, 17 Jul 2026 21:07:27 +0200 Subject: [PATCH 37/39] Changed keymanagement api to IKeyManagementApi --- pkg/api/keymanagement/keymanagement_api.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/keymanagement/keymanagement_api.go b/pkg/api/keymanagement/keymanagement_api.go index cdcd133e..6618d065 100644 --- a/pkg/api/keymanagement/keymanagement_api.go +++ b/pkg/api/keymanagement/keymanagement_api.go @@ -1,4 +1,4 @@ -package keymanagement_api +package keymanagement import ( "encoding/json" @@ -22,7 +22,7 @@ func init() { } type KeyManagementHandler struct { - Manager *keymanagement.KeyManagement + Manager keymanagement.IKeyManagementApi } func NewKeyManagementHandler(manager *keymanagement.KeyManagement) *KeyManagementHandler { From 2a869a59e509573fba22a8ef606af755fb57d788 Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:35:20 +0200 Subject: [PATCH 38/39] Updated api description --- pkg/api/keymanagement/keymanagement_api.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/api/keymanagement/keymanagement_api.go b/pkg/api/keymanagement/keymanagement_api.go index 6618d065..18d24212 100644 --- a/pkg/api/keymanagement/keymanagement_api.go +++ b/pkg/api/keymanagement/keymanagement_api.go @@ -58,11 +58,12 @@ func (handler *KeyManagementHandler) GetKeys(context *gin.Context) { // @Schemes // @Description adds a key to the KMS; load key into cache and write to database // @Tags keymanagement +// @Param keyname path string true "keyname" // @Param data body api.KeyManagementKey true "key" // @Produce json // @success 200 {json} Empty // @failure 400 {object} api.Error -// @Router /keymanagement/:keyname/ [PUT] +// @Router /keymanagement/{keyname} [PUT] func (handler *KeyManagementHandler) AddKey(context *gin.Context) { keyname := context.Param("keyname") jsonData, err := io.ReadAll(context.Request.Body) @@ -93,11 +94,12 @@ func (handler *KeyManagementHandler) AddKey(context *gin.Context) { // @Schemes // @Description update a key in the KMS; load key into cache and write to database // @Tags keymanagement +// @Param keyname path string true "keyname" // @Param data body api.KeyManagementKey true "key" // @Produce json // @success 200 {json} Empty // @failure 400 {object} api.Error -// @Router /keymanagement/:keyname/ [PATCH] +// @Router /keymanagement/{keyname} [PATCH] func (handler *KeyManagementHandler) UpdateKey(context *gin.Context) { keyname := context.Param("keyname") jsonData, err := io.ReadAll(context.Request.Body) @@ -127,10 +129,11 @@ func (handler *KeyManagementHandler) UpdateKey(context *gin.Context) { // @Schemes // @Description revokes the key by moving it to .revoked and renaming it // @Tags keymanagement +// @Param keyname path string true "keyname" // @Produce json // @success 200 {json} Empty // @failure 400 {object} api.Error -// @Router /keymanagement/:keyname/ [DELETE] +// @Router /keymanagement/{keyname} [DELETE] func (handler *KeyManagementHandler) RevokeKey(context *gin.Context) { keyname := context.Param("keyname") if err := handler.Manager.Revoke(keyname); err != nil { From 85d62e450a658c9220f11f4ae9d22ba868737c24 Mon Sep 17 00:00:00 2001 From: Maarten de Kruijf <16100232+MaartendeKruijf@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:35:31 +0200 Subject: [PATCH 39/39] Added info log to keymanagement --- pkg/keymanagement/keymanagement.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/keymanagement/keymanagement.go b/pkg/keymanagement/keymanagement.go index 9d25025d..2e0922b9 100644 --- a/pkg/keymanagement/keymanagement.go +++ b/pkg/keymanagement/keymanagement.go @@ -82,6 +82,7 @@ func (management *KeyManagement) insertInternal(public string, private string, p return fmt.Errorf("parsing private key: %s", err) } keypair := keys.KeyPair{Public: public_key, Private: private_key} + log.Info("Inserted key with id ", name) return management.database.Create(name, keypair) }