From 560bfb42464b38a490cf1a8fb6616a3cab821cd2 Mon Sep 17 00:00:00 2001 From: Joe Garcia Date: Wed, 8 Nov 2023 10:32:01 -0500 Subject: [PATCH 1/8] Integrate #114 to improve logon with 2FA --- cmd/logon.go | 9 ++- go.work | 3 + pkg/cybr/api/accounts.go | 8 ++- pkg/cybr/api/auth.go | 22 +++++-- pkg/cybr/api/auth_test.go | 11 ++-- pkg/cybr/api/client_test.go | 3 +- pkg/cybr/api/server.go | 5 +- pkg/cybr/api/shared/errorresponse.go | 7 +++ pkg/cybr/helpers/httpjson/httpjson.go | 89 +++++++++++++++++++++------ pkg/cybr/ispss/platformdiscovery.go | 9 ++- 10 files changed, 129 insertions(+), 37 deletions(-) create mode 100644 go.work create mode 100644 pkg/cybr/api/shared/errorresponse.go diff --git a/cmd/logon.go b/cmd/logon.go index 2c4ad46..db357c1 100644 --- a/cmd/logon.go +++ b/cmd/logon.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "fmt" "log" "os" @@ -53,6 +54,7 @@ var ( func logonToPAS(c pasapi.Client, username, password string, nonInteractive, concurrentSession bool) error { var err error + ctx := context.Background() // Check if non-interactive flag is not provided and password is not empty if !nonInteractive && password != "" { return fmt.Errorf("An error occured because --non-interactive must be provided when using --password flag") @@ -75,15 +77,16 @@ func logonToPAS(c pasapi.Client, username, password string, nonInteractive, conc ConcurrentSession: concurrentSession, } // Logon to the PAS REST API - err = c.Logon(credentials) - if err != nil && !strings.Contains(err.Error(), "ITATS542I") { + ctx, errorResponse, err := c.Logon(ctx, credentials) + if err != nil && errorResponse.ErrorCode != "ITATS542I" { return fmt.Errorf("Failed to Logon to the PVWA. %s", err) } // Deal with OTPCode here if error contains challenge error code and redo client.Logon() if err != nil { // Get OTP code from Stdin + fmt.Printf("%s: \n", errorResponse.ErrorMessage) credentials, err = util.ReadOTPcode(credentials) - err = c.Logon(credentials) + _, _, err = c.Logon(ctx, credentials) if err != nil { return fmt.Errorf("Failed to respond to challenge. Possible timeout occurred. %s", err) } diff --git a/go.work b/go.work new file mode 100644 index 0000000..1fbf39b --- /dev/null +++ b/go.work @@ -0,0 +1,3 @@ +go 1.21.0 + +use . diff --git a/pkg/cybr/api/accounts.go b/pkg/cybr/api/accounts.go index 6fcacf1..30957d8 100644 --- a/pkg/cybr/api/accounts.go +++ b/pkg/cybr/api/accounts.go @@ -1,6 +1,7 @@ package api import ( + "context" "encoding/json" "fmt" "strings" @@ -95,9 +96,10 @@ func (c Client) RevokeJITAccess(accountID string) error { // GetAccountPassword This method enables users to retrieve the password or SSH key of an existing account that is identified by its Account ID. It enables users to specify a reason and ticket ID, if required func (c Client) GetAccountPassword(accountID string, request requests.GetAccountPassword) (string, error) { + ctx := context.TODO() url := fmt.Sprintf("%s/passwordvault/api/Accounts/%s/Password/Retrieve", c.BaseURL, accountID) - response, err := httpJson.SendRequestRaw(false, url, "POST", c.SessionToken, request, c.InsecureTLS, c.Logger) + _, response, err := httpJson.SendRequestRaw(ctx, false, url, "POST", c.SessionToken, request, c.InsecureTLS, c.Logger) if err != nil { returnedError, _ := json.Marshal(response) return "", fmt.Errorf("Failed to retrieve the account password '%s'. %s. %s", accountID, string(returnedError), err) @@ -108,9 +110,11 @@ func (c Client) GetAccountPassword(accountID string, request requests.GetAccount // GetAccountSSHKey This method enables users to retrieve the password or SSH key of an existing account that is identified by its Account ID. It enables users to specify a reason and ticket ID, if required func (c Client) GetAccountSSHKey(accountID string, request requests.GetAccountPassword) (string, error) { + ctx := context.TODO() + url := fmt.Sprintf("%s/passwordvault/api/Accounts/%s/Secret/Retrieve", c.BaseURL, accountID) - response, err := httpJson.SendRequestRaw(false, url, "POST", c.SessionToken, request, c.InsecureTLS, c.Logger) + _, response, err := httpJson.SendRequestRaw(ctx, false, url, "POST", c.SessionToken, request, c.InsecureTLS, c.Logger) if err != nil { returnedError, _ := json.Marshal(response) return "", fmt.Errorf("Failed to retrieve the account SSH Key '%s'. %s. %s", accountID, string(returnedError), err) diff --git a/pkg/cybr/api/auth.go b/pkg/cybr/api/auth.go index 654413f..61491ea 100644 --- a/pkg/cybr/api/auth.go +++ b/pkg/cybr/api/auth.go @@ -1,31 +1,41 @@ package api import ( + "context" + "encoding/json" "fmt" "strings" "github.com/infamousjoeg/cybr-cli/pkg/cybr/api/requests" + "github.com/infamousjoeg/cybr-cli/pkg/cybr/api/shared" httpJson "github.com/infamousjoeg/cybr-cli/pkg/cybr/helpers/httpjson" ) // Logon to PAS REST API Web Service // Because we're using concurrentSession capability, this is only supported // on PAS REST API v11.3 and above -func (c *Client) Logon(req requests.Logon) error { +func (c *Client) Logon(ctx context.Context, req requests.Logon) (context.Context, *shared.ErrorResponse, error) { err := c.IsValid() if err != nil { - return err + return ctx, &shared.ErrorResponse{}, err } // Handle cyberark, ldap, and radius push, append & challenge/response authentication methods url := fmt.Sprintf("%s/passwordvault/api/auth/%s/logon", c.BaseURL, c.AuthType) - token, err := httpJson.SendRequestRaw(false, url, "POST", "", req, c.InsecureTLS, c.Logger) + ctx, response, err := httpJson.SendRequestRaw(ctx, false, url, "POST", "", req, c.InsecureTLS, c.Logger) if err != nil { - return fmt.Errorf("Failed to authenticate to the PAS REST API. %s", err) + // Check if response can be unmarshalled to ErrorResponse + errorResponse := &shared.ErrorResponse{} + errUm := json.Unmarshal(response, &errorResponse) + if errUm != nil { + return ctx, nil, fmt.Errorf("Unable to unmarshal response from PAS REST API Web Service. %s", errUm) + } + + return ctx, errorResponse, fmt.Errorf("Failed to authenticate to the PAS REST API. %s", err) } - c.SessionToken = strings.Trim(string(token), "\"") - return nil + c.SessionToken = strings.Trim(string(response), "\"") + return ctx, nil, nil } // Logoff the PAS REST API Web Service diff --git a/pkg/cybr/api/auth_test.go b/pkg/cybr/api/auth_test.go index d293fb3..0984725 100644 --- a/pkg/cybr/api/auth_test.go +++ b/pkg/cybr/api/auth_test.go @@ -1,6 +1,7 @@ package api_test import ( + "context" "strings" "testing" @@ -19,7 +20,7 @@ func TestCyberarkLogonSuccess(t *testing.T) { Password: password, } - err := client.Logon(creds) + _, _, err := client.Logon(context.TODO(), creds) if err != nil { t.Errorf("Failed to logon. %s", err) } @@ -36,7 +37,7 @@ func TestCyberarkLogonInvalidCreds(t *testing.T) { Password: password, } - err := client.Logon(creds) + _, _, err := client.Logon(context.TODO(), creds) if err == nil { t.Errorf("Successfully logged in but shouldn't have. %s", err) } @@ -53,7 +54,7 @@ func TestCyberarkLogonInvalidHostName(t *testing.T) { Password: password, } - err := client.Logon(creds) + _, _, err := client.Logon(context.TODO(), creds) if err == nil { t.Errorf("Successfully logged in but shouldn't have. %s", err) } @@ -70,7 +71,7 @@ func TestLogonInvalidAuthType(t *testing.T) { Password: password, } - err := client.Logon(creds) + _, _, err := client.Logon(context.TODO(), creds) if err == nil { t.Errorf("Successfully logged in but shouldn't have. %s", err) } @@ -91,7 +92,7 @@ func TestCyberarkLogoffSuccess(t *testing.T) { Password: password, } - err := client.Logon(creds) + _, _, err := client.Logon(context.TODO(), creds) if err != nil { t.Errorf("Failed to logon. %s", err) } diff --git a/pkg/cybr/api/client_test.go b/pkg/cybr/api/client_test.go index b0f340e..ec8f556 100644 --- a/pkg/cybr/api/client_test.go +++ b/pkg/cybr/api/client_test.go @@ -1,6 +1,7 @@ package api_test import ( + "context" "os" "testing" @@ -25,7 +26,7 @@ func defaultPASAPIClient(t *testing.T) (pasapi.Client, error) { Password: password, } - err := client.Logon(creds) + _, _, err := client.Logon(context.TODO(), creds) if err != nil { t.Errorf("Failed to logon. %s", err) } diff --git a/pkg/cybr/api/server.go b/pkg/cybr/api/server.go index 5740dec..19ea737 100644 --- a/pkg/cybr/api/server.go +++ b/pkg/cybr/api/server.go @@ -1,6 +1,7 @@ package api import ( + "context" "encoding/json" "fmt" @@ -10,8 +11,10 @@ import ( // ServerVerify is an unauthenticated endpoint for testing Web Service availability func (c Client) ServerVerify() (*responses.ServerVerify, error) { + ctx := context.TODO() + url := fmt.Sprintf("%s/passwordvault/WebServices/PIMServices.svc/Verify", c.BaseURL) - response, err := httpJson.SendRequest(false, url, "GET", "", nil, c.InsecureTLS, c.Logger) + response, err := httpJson.SendRequest(ctx, false, url, "GET", "", nil, c.InsecureTLS, c.Logger) if err != nil { return &responses.ServerVerify{}, fmt.Errorf("Error verifying PAS REST API Web Service. %s", err) } diff --git a/pkg/cybr/api/shared/errorresponse.go b/pkg/cybr/api/shared/errorresponse.go new file mode 100644 index 0000000..b744bc4 --- /dev/null +++ b/pkg/cybr/api/shared/errorresponse.go @@ -0,0 +1,7 @@ +package shared + +// ErrorResponse from Logon +type ErrorResponse struct { + ErrorCode string `json:"ErrorCode"` + ErrorMessage string `json:"ErrorMessage"` +} diff --git a/pkg/cybr/helpers/httpjson/httpjson.go b/pkg/cybr/helpers/httpjson/httpjson.go index 376e453..d68b4cc 100644 --- a/pkg/cybr/helpers/httpjson/httpjson.go +++ b/pkg/cybr/helpers/httpjson/httpjson.go @@ -2,18 +2,36 @@ package httpjson import ( "bytes" + "context" "crypto/tls" "encoding/json" "fmt" "io" - "io/ioutil" "net/http" + "net/http/cookiejar" + "net/url" "strings" "time" "github.com/infamousjoeg/cybr-cli/pkg/logger" ) +type contextKey string + +var ( + contextKeyCookies = contextKey("cookies") +) + +func (c contextKey) String() string { + return "httpjson_cookies" + string(c) +} + +// Cookies returns the cookies from the context +func Cookies(ctx context.Context) ([]*http.Cookie, bool) { + cookies, ok := ctx.Value(contextKeyCookies).([]*http.Cookie) + return cookies, ok +} + func bodyToBytes(body interface{}) ([]byte, error) { if body == nil { return []byte(""), nil @@ -53,16 +71,27 @@ func logRequest(req *http.Request, logger logger.Logger) { body := buf.String() logger.Writef("%s\n", body) - req.Body = ioutil.NopCloser(bytes.NewReader([]byte(body))) + req.Body = io.NopCloser(bytes.NewReader([]byte(body))) } -func getResponse(identity bool, url string, method string, token string, body interface{}, insecureTLS bool, logger logger.Logger) (http.Response, error) { +func getResponse(ctx context.Context, identity bool, urls string, method string, token string, body interface{}, insecureTLS bool, logger logger.Logger) (http.Response, error) { if insecureTLS { http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} } else { http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: false} } + + jar, _ := cookiejar.New(nil) + u, _ := url.Parse(urls) + cookies, ok := Cookies(ctx) + if ok { + jar.SetCookies(u, cookies) + } else { + jar.SetCookies(u, nil) + } + httpClient := http.Client{ + Jar: jar, Timeout: time.Second * 30, // Maximum of 30 secs } var bodyReader io.ReadCloser @@ -73,10 +102,10 @@ func getResponse(identity bool, url string, method string, token string, body in return *res, err } - bodyReader = ioutil.NopCloser(bytes.NewReader(content)) + bodyReader = io.NopCloser(bytes.NewReader(content)) // create the request - req, err := http.NewRequest(method, url, bodyReader) + req, err := http.NewRequestWithContext(ctx, method, urls, bodyReader) if err != nil { return *res, fmt.Errorf("Failed to create new request. %s", err) } @@ -111,8 +140,8 @@ func getResponse(identity bool, url string, method string, token string, body in } // SendRequest is an http request and get response as serialized json map[string]interface{} -func SendRequest(identity bool, url string, method string, token string, body interface{}, insecureTLS bool, logger logger.Logger) (map[string]interface{}, error) { - res, err := getResponse(identity, url, method, token, body, insecureTLS, logger) +func SendRequest(ctx context.Context, identity bool, url string, method string, token string, body interface{}, insecureTLS bool, logger logger.Logger) (map[string]interface{}, error) { + res, err := getResponse(ctx, identity, url, method, token, body, insecureTLS, logger) if err != nil && strings.Contains(err.Error(), "Failed to send request") { return nil, err @@ -139,17 +168,33 @@ func SendRequest(identity bool, url string, method string, token string, body in } // SendRequestRaw is an http request and get response as byte[] -func SendRequestRaw(identity bool, url string, method string, token string, body interface{}, insecureTLS bool, logger logger.Logger) ([]byte, error) { - res, err := getResponse(identity, url, method, token, body, insecureTLS, logger) +func SendRequestRaw(ctx context.Context, identity bool, url string, method string, token string, body interface{}, insecureTLS bool, logger logger.Logger) (context.Context, []byte, error) { + res, err := getResponse(ctx, identity, url, method, token, body, insecureTLS, logger) if err != nil { - return nil, err + return ctx, nil, err } + defer res.Body.Close() - content, err := ioutil.ReadAll(res.Body) + content, err := io.ReadAll(res.Body) if err != nil { - return nil, fmt.Errorf("Failed to read body. %s", err) + return ctx, nil, fmt.Errorf("Failed to read body. %v", err) } - return content, err + + newCtx := context.WithValue(ctx, contextKeyCookies, res.Cookies()) + return newCtx, content, err + + // res, err := getResponse(ctx, identity, url, method, token, body, insecureTLS, logger) + // fmt.Printf("SRR Response: %s\n", res.Body) + // fmt.Printf("SRR Response Error: %s\n", err) + // if err != nil && res.Body != nil { + // content, errRead := io.ReadAll(res.Body) + // if errRead != nil { + // return ctx, nil, fmt.Errorf("Failed to read body. %s", errRead) + // } + // newCtx := context.WithValue(ctx, contextKeyCookies, res.Cookies()) + // return newCtx, content, err + // } + // return ctx, nil, err } // SendRequestRawWithHeaders is an http request and get response as byte[] @@ -191,7 +236,7 @@ func SendRequestRawWithHeaders(url, method string, headers http.Header, body int return []byte(""), fmt.Errorf("Received non-200 status code '%d'", res.StatusCode) } - content, err = ioutil.ReadAll(res.Body) + content, err = io.ReadAll(res.Body) if err != nil { return []byte(""), fmt.Errorf("Failed to read body. %s", err) } @@ -200,24 +245,32 @@ func SendRequestRawWithHeaders(url, method string, headers http.Header, body int // Get a get request and get response as serialized json map[string]interface{} func Get(identity bool, url string, token string, insecureTLS bool, logger logger.Logger) (map[string]interface{}, error) { - response, err := SendRequest(identity, url, http.MethodGet, token, "", insecureTLS, logger) + ctx := context.TODO() + + response, err := SendRequest(ctx, identity, url, http.MethodGet, token, "", insecureTLS, logger) return response, err } // Post a post request and get response as serialized json map[string]interface{} func Post(identity bool, url string, token string, body interface{}, insecureTLS bool, logger logger.Logger) (map[string]interface{}, error) { - response, err := SendRequest(identity, url, http.MethodPost, token, body, insecureTLS, logger) + ctx := context.TODO() + + response, err := SendRequest(ctx, identity, url, http.MethodPost, token, body, insecureTLS, logger) return response, err } // Put a put request and get response as serialized json map[string]interface{} func Put(identity bool, url string, token string, body interface{}, insecureTLS bool, logger logger.Logger) (map[string]interface{}, error) { - response, err := SendRequest(identity, url, http.MethodPut, token, body, insecureTLS, logger) + ctx := context.TODO() + + response, err := SendRequest(ctx, identity, url, http.MethodPut, token, body, insecureTLS, logger) return response, err } // Delete a delete request and get response as serialized json map[string]interface{} func Delete(identity bool, url string, token string, insecureTLS bool, logger logger.Logger) (map[string]interface{}, error) { - response, err := SendRequest(identity, url, http.MethodDelete, token, "", insecureTLS, logger) + ctx := context.TODO() + + response, err := SendRequest(ctx, identity, url, http.MethodDelete, token, "", insecureTLS, logger) return response, err } diff --git a/pkg/cybr/ispss/platformdiscovery.go b/pkg/cybr/ispss/platformdiscovery.go index 38164cc..1e6bcc1 100644 --- a/pkg/cybr/ispss/platformdiscovery.go +++ b/pkg/cybr/ispss/platformdiscovery.go @@ -1,6 +1,7 @@ package ispss import ( + "context" "encoding/json" "fmt" @@ -11,13 +12,19 @@ import ( // PlatformDiscovery uses the ISPSS API to discover the platform URLs func PlatformDiscovery(platformURL string) (*responses.PlatformDiscovery, error) { + ctx := context.TODO() + subdomain, err := util.GetSubDomain(platformURL) if err != nil { return &responses.PlatformDiscovery{}, fmt.Errorf("Failed to get subdomain. %s", err) } url := fmt.Sprintf("https://platform-discovery.cyberark.cloud/api/v2/services/subdomain/%s", subdomain) - response, err := httpJson.SendRequestRaw(false, url, "GET", "", nil, false, nil) + fmt.Printf("URL: %s\n", url) + fmt.Println("Sending request...") + _, response, err := httpJson.SendRequestRaw(ctx, false, url, "GET", "", nil, false, nil) + fmt.Printf("Response: %s\n", response) + fmt.Printf("Error: %s\n", err) if err != nil { return &responses.PlatformDiscovery{}, fmt.Errorf("Failed to get platform discovery. %s", err) } From 86403b5bd33db0bd79f547e0a19b25814a39e11b Mon Sep 17 00:00:00 2001 From: Joe Garcia Date: Wed, 8 Nov 2023 12:34:37 -0500 Subject: [PATCH 2/8] Fixed errorResponse nil dereference --- cmd/logon.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cmd/logon.go b/cmd/logon.go index db357c1..b68db91 100644 --- a/cmd/logon.go +++ b/cmd/logon.go @@ -78,7 +78,12 @@ func logonToPAS(c pasapi.Client, username, password string, nonInteractive, conc } // Logon to the PAS REST API ctx, errorResponse, err := c.Logon(ctx, credentials) - if err != nil && errorResponse.ErrorCode != "ITATS542I" { + if err != nil && (errorResponse == nil || errorResponse.ErrorCode != "ITATS542I") { + // If errorResponse is nil, you can't use its ErrorCode, so handle the error accordingly + if errorResponse == nil { + return fmt.Errorf("Failed to logon to the PVWA and error details are unavailable. %s", err) + } + return fmt.Errorf("Failed to Logon to the PVWA. %s", err) } // Deal with OTPCode here if error contains challenge error code and redo client.Logon() From 8e7635aee3461761f43510ff54354419cc9e1ed2 Mon Sep 17 00:00:00 2001 From: Joe Garcia Date: Wed, 8 Nov 2023 14:50:46 -0500 Subject: [PATCH 3/8] Handle 500 status code & ITATS error code --- pkg/cybr/helpers/httpjson/httpjson.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/cybr/helpers/httpjson/httpjson.go b/pkg/cybr/helpers/httpjson/httpjson.go index d68b4cc..c07d3c1 100644 --- a/pkg/cybr/helpers/httpjson/httpjson.go +++ b/pkg/cybr/helpers/httpjson/httpjson.go @@ -180,6 +180,14 @@ func SendRequestRaw(ctx context.Context, identity bool, url string, method strin return ctx, nil, fmt.Errorf("Failed to read body. %v", err) } + // Check if the response status code is 500 and look for the error message ITATS542I + if res.StatusCode == 500 && bytes.Contains(content, []byte("ITATS542I")) { + newCtx := context.WithValue(ctx, contextKeyCookies, res.Cookies()) + return newCtx, content, err + } else if res.StatusCode >= 300 { + return ctx, nil, fmt.Errorf("Received non-200 status code '%d'", res.StatusCode) + } + newCtx := context.WithValue(ctx, contextKeyCookies, res.Cookies()) return newCtx, content, err From b119cc4c03b7aca45e5fccef4cb883c0f2b2c813 Mon Sep 17 00:00:00 2001 From: Joe Garcia Date: Thu, 9 Nov 2023 08:50:30 -0500 Subject: [PATCH 4/8] Account for ITATS542I on Logon --- cmd/logon.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/logon.go b/cmd/logon.go index b68db91..9adde85 100644 --- a/cmd/logon.go +++ b/cmd/logon.go @@ -82,12 +82,12 @@ func logonToPAS(c pasapi.Client, username, password string, nonInteractive, conc // If errorResponse is nil, you can't use its ErrorCode, so handle the error accordingly if errorResponse == nil { return fmt.Errorf("Failed to logon to the PVWA and error details are unavailable. %s", err) + } else if errorResponse != nil { + return fmt.Errorf("Failed to Logon to the PVWA. %s", err) } - - return fmt.Errorf("Failed to Logon to the PVWA. %s", err) } // Deal with OTPCode here if error contains challenge error code and redo client.Logon() - if err != nil { + if errorResponse.ErrorCode == "ITATS542I" { // Get OTP code from Stdin fmt.Printf("%s: \n", errorResponse.ErrorMessage) credentials, err = util.ReadOTPcode(credentials) From d16570e7e683510a4164e1af64de1d6635e2d7af Mon Sep 17 00:00:00 2001 From: Joe Garcia Date: Thu, 9 Nov 2023 08:53:05 -0500 Subject: [PATCH 5/8] Fix errorResponse nil pointer dereference --- cmd/logon.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/logon.go b/cmd/logon.go index 9adde85..0547775 100644 --- a/cmd/logon.go +++ b/cmd/logon.go @@ -87,7 +87,7 @@ func logonToPAS(c pasapi.Client, username, password string, nonInteractive, conc } } // Deal with OTPCode here if error contains challenge error code and redo client.Logon() - if errorResponse.ErrorCode == "ITATS542I" { + if errorResponse != nil && errorResponse.ErrorCode == "ITATS542I" { // Get OTP code from Stdin fmt.Printf("%s: \n", errorResponse.ErrorMessage) credentials, err = util.ReadOTPcode(credentials) From f2af527ef8ab5928660c5c0aa436efac75efbdbc Mon Sep 17 00:00:00 2001 From: Joe Garcia Date: Thu, 9 Nov 2023 14:28:33 -0500 Subject: [PATCH 6/8] Implemented status code and error code checks --- cmd/logon.go | 109 +++++++++++++++++++------- pkg/cybr/api/auth.go | 4 +- pkg/cybr/helpers/httpjson/httpjson.go | 2 +- pkg/cybr/helpers/util/util.go | 20 +++++ 4 files changed, 102 insertions(+), 33 deletions(-) diff --git a/cmd/logon.go b/cmd/logon.go index 0547775..4fbb518 100644 --- a/cmd/logon.go +++ b/cmd/logon.go @@ -10,6 +10,7 @@ import ( pasapi "github.com/infamousjoeg/cybr-cli/pkg/cybr/api" "github.com/infamousjoeg/cybr-cli/pkg/cybr/api/requests" + "github.com/infamousjoeg/cybr-cli/pkg/cybr/api/shared" "github.com/infamousjoeg/cybr-cli/pkg/cybr/helpers/prettyprint" "github.com/infamousjoeg/cybr-cli/pkg/cybr/helpers/util" "github.com/infamousjoeg/cybr-cli/pkg/cybr/identity" @@ -52,56 +53,104 @@ var ( platformDiscovery *ispssresponses.PlatformDiscovery // Platform discovery response ) +// func logonToPAS(c pasapi.Client, username, password string, nonInteractive, concurrentSession bool) error { +// var err error +// ctx := context.Background() +// // Check if non-interactive flag is not provided and password is not empty +// if !nonInteractive && password != "" { +// return fmt.Errorf("An error occured because --non-interactive must be provided when using --password flag") +// } +// // If the execution is not non-interactive, ask the user to input password +// if !nonInteractive { +// password, err = util.ReadPassword() +// if err != nil { +// return fmt.Errorf("An error occurred trying to read password from Stdin. Exiting") +// } +// } +// // Check if password is empty +// if password == "" { +// return fmt.Errorf("Provided password is empty") +// } +// // Create credentials for logon +// credentials := requests.Logon{ +// Username: username, +// Password: password, +// ConcurrentSession: concurrentSession, +// } +// // Logon to the PAS REST API +// ctx, errorResponse, err := c.Logon(ctx, credentials) +// if err != nil && (errorResponse == nil || errorResponse.ErrorCode != "ITATS542I") { +// // If errorResponse is nil, you can't use its ErrorCode, so handle the error accordingly +// if errorResponse == nil { +// return fmt.Errorf("Failed to logon to the PVWA and error details are unavailable. %s", err) +// } else if errorResponse != nil { +// return fmt.Errorf("Failed to Logon to the PVWA. %s", err) +// } +// } +// // Deal with OTPCode here if error contains challenge error code and redo client.Logon() +// if errorResponse != nil && errorResponse.ErrorCode == "ITATS542I" { +// // Get OTP code from Stdin +// fmt.Printf("%s: \n", errorResponse.ErrorMessage) +// credentials, err = util.ReadOTPcode(credentials) +// _, _, err = c.Logon(ctx, credentials) +// if err != nil { +// return fmt.Errorf("Failed to respond to challenge. Possible timeout occurred. %s", err) +// } +// } +// // Set client config +// err = c.SetConfig() +// if err != nil { +// return fmt.Errorf("Failed to create configuration file. %s", err) +// } +// return nil +// } + func logonToPAS(c pasapi.Client, username, password string, nonInteractive, concurrentSession bool) error { - var err error ctx := context.Background() - // Check if non-interactive flag is not provided and password is not empty - if !nonInteractive && password != "" { - return fmt.Errorf("An error occured because --non-interactive must be provided when using --password flag") + + // If --non-interactive flag is provided and password is empty, return error + if nonInteractive && password == "" { + return fmt.Errorf("An error occured because --password is required when using --non-interactive flag") } - // If the execution is not non-interactive, ask the user to input password + + // If --non-interactive flag is not provided, ask the user to input password if !nonInteractive { - password, err = util.ReadPassword() + var err error + password, err = util.PromptForPassword(5) if err != nil { return fmt.Errorf("An error occurred trying to read password from Stdin. Exiting") } } - // Check if password is empty - if password == "" { - return fmt.Errorf("Provided password is empty") - } + // Create credentials for logon credentials := requests.Logon{ Username: username, Password: password, ConcurrentSession: concurrentSession, } - // Logon to the PAS REST API + + return performLogon(ctx, c, credentials) +} + +func performLogon(ctx context.Context, c pasapi.Client, credentials requests.Logon) error { ctx, errorResponse, err := c.Logon(ctx, credentials) - if err != nil && (errorResponse == nil || errorResponse.ErrorCode != "ITATS542I") { - // If errorResponse is nil, you can't use its ErrorCode, so handle the error accordingly - if errorResponse == nil { - return fmt.Errorf("Failed to logon to the PVWA and error details are unavailable. %s", err) - } else if errorResponse != nil { - return fmt.Errorf("Failed to Logon to the PVWA. %s", err) - } + if err != nil { + return handleLogonError(c, credentials, errorResponse, err) } - // Deal with OTPCode here if error contains challenge error code and redo client.Logon() + + return c.SetConfig() +} + +func handleLogonError(c pasapi.Client, credentials requests.Logon, errorResponse *shared.ErrorResponse, err error) error { if errorResponse != nil && errorResponse.ErrorCode == "ITATS542I" { - // Get OTP code from Stdin - fmt.Printf("%s: \n", errorResponse.ErrorMessage) - credentials, err = util.ReadOTPcode(credentials) - _, _, err = c.Logon(ctx, credentials) + fmt.Printf("%s. \n", errorResponse.ErrorMessage) + credentials, err := util.ReadOTPcode(credentials) if err != nil { - return fmt.Errorf("Failed to respond to challenge. Possible timeout occurred. %s", err) + return fmt.Errorf("Failed to read OTP code. %s", err) } + return performLogon(context.Background(), c, credentials) } - // Set client config - err = c.SetConfig() - if err != nil { - return fmt.Errorf("Failed to create configuration file. %s", err) - } - return nil + return fmt.Errorf("Failed to logon to the PVWA. %s", err) } func startAuthIdentity(c pasapi.Client, username string) (*responses.Authentication, error) { diff --git a/pkg/cybr/api/auth.go b/pkg/cybr/api/auth.go index 61491ea..eaa585b 100644 --- a/pkg/cybr/api/auth.go +++ b/pkg/cybr/api/auth.go @@ -23,12 +23,12 @@ func (c *Client) Logon(ctx context.Context, req requests.Logon) (context.Context // Handle cyberark, ldap, and radius push, append & challenge/response authentication methods url := fmt.Sprintf("%s/passwordvault/api/auth/%s/logon", c.BaseURL, c.AuthType) ctx, response, err := httpJson.SendRequestRaw(ctx, false, url, "POST", "", req, c.InsecureTLS, c.Logger) - if err != nil { + if err != nil || strings.Contains(string(response), "ITATS542I") { // Check if response can be unmarshalled to ErrorResponse errorResponse := &shared.ErrorResponse{} errUm := json.Unmarshal(response, &errorResponse) if errUm != nil { - return ctx, nil, fmt.Errorf("Unable to unmarshal response from PAS REST API Web Service. %s", errUm) + return ctx, nil, errUm } return ctx, errorResponse, fmt.Errorf("Failed to authenticate to the PAS REST API. %s", err) diff --git a/pkg/cybr/helpers/httpjson/httpjson.go b/pkg/cybr/helpers/httpjson/httpjson.go index c07d3c1..da3ff18 100644 --- a/pkg/cybr/helpers/httpjson/httpjson.go +++ b/pkg/cybr/helpers/httpjson/httpjson.go @@ -170,7 +170,7 @@ func SendRequest(ctx context.Context, identity bool, url string, method string, // SendRequestRaw is an http request and get response as byte[] func SendRequestRaw(ctx context.Context, identity bool, url string, method string, token string, body interface{}, insecureTLS bool, logger logger.Logger) (context.Context, []byte, error) { res, err := getResponse(ctx, identity, url, method, token, body, insecureTLS, logger) - if err != nil { + if err != nil && res.StatusCode != 500 { return ctx, nil, err } defer res.Body.Close() diff --git a/pkg/cybr/helpers/util/util.go b/pkg/cybr/helpers/util/util.go index 5ede25f..446e48d 100644 --- a/pkg/cybr/helpers/util/util.go +++ b/pkg/cybr/helpers/util/util.go @@ -32,6 +32,26 @@ func ReadPassword() (string, error) { return string(byteSecretVal), nil } +// PromptForPassword prompts the user to enter a password, retrying until valid input is provided or max attempts are reached +func PromptForPassword(maxAttempts int) (string, error) { + var password string + var err error + + for attempts := 0; attempts < maxAttempts; attempts++ { + password, err = ReadPassword() + if err != nil { + fmt.Println(err) + continue + } + + if password != "" { + return password, nil + } + fmt.Println("Password cannot be empty. Please try again.") + } + return "", fmt.Errorf("No valid password entered after %d attempts. Exiting", maxAttempts) +} + // ReadInput Read input from Stdin func ReadInput(message string) (string, error) { fmt.Printf("%s: ", message) From a99c626328ca26c3d801adb9c20d01b3e0e5ea97 Mon Sep 17 00:00:00 2001 From: Joe Garcia Date: Tue, 14 Nov 2023 12:14:52 -0500 Subject: [PATCH 7/8] Update to current code --- .gitignore | 4 +++- cmd/logon.go | 2 +- pkg/cybr/api/auth.go | 7 ++++++- pkg/cybr/helpers/httpjson/httpjson.go | 15 ++++++++------- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index e5cddbc..f3d4158 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,6 @@ .DS_Store .dccache -bin/ \ No newline at end of file +bin/ + +testing* \ No newline at end of file diff --git a/cmd/logon.go b/cmd/logon.go index 4fbb518..6ed92f0 100644 --- a/cmd/logon.go +++ b/cmd/logon.go @@ -150,7 +150,7 @@ func handleLogonError(c pasapi.Client, credentials requests.Logon, errorResponse } return performLogon(context.Background(), c, credentials) } - return fmt.Errorf("Failed to logon to the PVWA. %s", err) + return fmt.Errorf("handleLogonError: Failed to logon to the PVWA. %s", err) } func startAuthIdentity(c pasapi.Client, username string) (*responses.Authentication, error) { diff --git a/pkg/cybr/api/auth.go b/pkg/cybr/api/auth.go index eaa585b..235204d 100644 --- a/pkg/cybr/api/auth.go +++ b/pkg/cybr/api/auth.go @@ -23,7 +23,8 @@ func (c *Client) Logon(ctx context.Context, req requests.Logon) (context.Context // Handle cyberark, ldap, and radius push, append & challenge/response authentication methods url := fmt.Sprintf("%s/passwordvault/api/auth/%s/logon", c.BaseURL, c.AuthType) ctx, response, err := httpJson.SendRequestRaw(ctx, false, url, "POST", "", req, c.InsecureTLS, c.Logger) - if err != nil || strings.Contains(string(response), "ITATS542I") { + fmt.Printf("Logon Response: %s \n", string(response)) + if strings.Contains(string(response), "ITATS542I") { // Check if response can be unmarshalled to ErrorResponse errorResponse := &shared.ErrorResponse{} errUm := json.Unmarshal(response, &errorResponse) @@ -33,6 +34,10 @@ func (c *Client) Logon(ctx context.Context, req requests.Logon) (context.Context return ctx, errorResponse, fmt.Errorf("Failed to authenticate to the PAS REST API. %s", err) } + if err != nil { + fmt.Printf("Logon Error: %s \n", err.Error()) + return ctx, nil, err + } c.SessionToken = strings.Trim(string(response), "\"") return ctx, nil, nil diff --git a/pkg/cybr/helpers/httpjson/httpjson.go b/pkg/cybr/helpers/httpjson/httpjson.go index da3ff18..83e17d2 100644 --- a/pkg/cybr/helpers/httpjson/httpjson.go +++ b/pkg/cybr/helpers/httpjson/httpjson.go @@ -84,10 +84,10 @@ func getResponse(ctx context.Context, identity bool, urls string, method string, jar, _ := cookiejar.New(nil) u, _ := url.Parse(urls) cookies, ok := Cookies(ctx) - if ok { - jar.SetCookies(u, cookies) - } else { + if !ok { jar.SetCookies(u, nil) + } else { + jar.SetCookies(u, cookies) } httpClient := http.Client{ @@ -132,9 +132,9 @@ func getResponse(ctx context.Context, identity bool, urls string, method string, return http.Response{}, fmt.Errorf("Failed to send request. %s", err) } - if res.StatusCode >= 300 { - return *res, fmt.Errorf("Received non-200 status code '%d'", res.StatusCode) - } + // if res.StatusCode >= 300 { + // return *res, fmt.Errorf("getResponse: Received non-200 status code '%d'", res.StatusCode) + // } return *res, err } @@ -176,6 +176,7 @@ func SendRequestRaw(ctx context.Context, identity bool, url string, method strin defer res.Body.Close() content, err := io.ReadAll(res.Body) + fmt.Printf("SRR Response: %s\n", content) if err != nil { return ctx, nil, fmt.Errorf("Failed to read body. %v", err) } @@ -185,7 +186,7 @@ func SendRequestRaw(ctx context.Context, identity bool, url string, method strin newCtx := context.WithValue(ctx, contextKeyCookies, res.Cookies()) return newCtx, content, err } else if res.StatusCode >= 300 { - return ctx, nil, fmt.Errorf("Received non-200 status code '%d'", res.StatusCode) + return ctx, nil, fmt.Errorf("SendRequestRaw: Received non-200 status code '%d'", res.StatusCode) } newCtx := context.WithValue(ctx, contextKeyCookies, res.Cookies()) From 24455f585df415692c65490d312e478401368cca Mon Sep 17 00:00:00 2001 From: Joe Garcia Date: Tue, 12 Dec 2023 09:34:19 -0500 Subject: [PATCH 8/8] Migrate CI to SaaS (#216) (#217) * Update ci.yml for Pineapple * Fix indentation error * Revert to previous checkout/install * Add /api to CC URL * move to ubuntu-latest hosted runner * switch from ldap to identity * Change test account to cyberark * Change PAS_ADDRESS to PAS_HOSTNAME * removed sleep * Update IDs for SaaS * accountSafeName _ to - * Create new test account * Add Content-Length to request header * Add empty struct for body on POST * Add emptyBody to logoff * Change ListSafeMembers.Members.value.MemberId to interface{} * Update RemoveSafeMember from v1 to v2 API * Add MemberType to Add Safe Member test * Removed early DeleteSafe * Add emptyBody to UnsuspendUser * Add Create Temp PEM Files step * Update CCP variable paths * Base64 decode CCP Certs * Add CCP_HOSTNAME env var * Move test workflow to self-hosted runner * Update CCP_CLIENT_PRIVATE_KEY --- .github/workflows/ci.yml | 81 ++++------------------- cmd/safes.go | 8 ++- pkg/cybr/api/accounts.go | 15 +++-- pkg/cybr/api/accounts_test.go | 13 ++-- pkg/cybr/api/auth.go | 2 +- pkg/cybr/api/client_test.go | 7 +- pkg/cybr/api/requests/addsafemember.go | 1 + pkg/cybr/api/responses/listsafemembers.go | 2 +- pkg/cybr/api/safes.go | 2 +- pkg/cybr/api/safes_test.go | 45 +++++++------ pkg/cybr/api/users.go | 2 +- pkg/cybr/ccp/ccp_test.go | 11 ++- 12 files changed, 75 insertions(+), 114 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af0cf72..39a2066 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - name: Install Go uses: actions/setup-go@v4 with: - go-version: '>=1.16' + go-version: '>=1.18' cache: false - name: Lint All uses: golangci/golangci-lint-action@v3 @@ -28,87 +28,30 @@ jobs: test: name: Test runs-on: self-hosted - needs: - - lint + needs: lint permissions: id-token: write contents: read - # env: - # PAS_HOSTNAME: ${{ secrets.PAS_HOSTNAME }} - # CCP_CLIENT_CERT: ${{ secrets.CCP_CLIENT_CERT }} - # CCP_CLIENT_PRIVATE_KEY: ${{ secrets.CCP_CLIENT_PRIVATE_KEY }} steps: - - name: Checkout Source Code + - name: Checkout source code uses: actions/checkout@v3 - name: Install Go uses: actions/setup-go@v4 with: - go-version: '>=1.16' + go-version: '>=1.18' cache: false - name: Import Secrets using CyberArk Conjur Secret Fetcher uses: infamousjoeg/conjur-action@v2.0.4 with: - url: https://infamous.secretsmgr.cyberark.cloud + url: https://pineapple.secretsmgr.cyberark.cloud/api account: conjur - authn_id: github + authn_id: inf-github secrets: | - data/vault/D-App-CybrCLI/Application-CyberArkIdentitySecurity-infamous.cyberark.cloud-cybr-cli@cyberark.cloud.13142/address|PAS_ADDRESS;data/vault/D-App-CybrCLI/Application-CyberArkIdentitySecurity-infamous.cyberark.cloud-cybr-cli@cyberark.cloud.13142/username|PAS_USERNAME;data/vault/D-App-CybrCLI/Application-CyberArkIdentitySecurity-infamous.cyberark.cloud-cybr-cli@cyberark.cloud.13142/password|PAS_PASSWORD;data/vault/D-App-CybrCLI/ccp-client-certificate/password|CCP_CLIENT_CERT;data/vault/D-App-CybrCLI/ccp-priv-key/password|CCP_CLIENT_PRIVATE_KEY - - name: Debug Step - run: | - echo "PAS_ADDRESS: " $PAS_ADDRESS "\r\nPAS_USERNAME: " $PAS_USERNAME "\r\nPAS_PASSWORD: " $PAS_PASSWORD "\r\nCCP_CLIENT_CERT: " $CCP_CLIENT_CERT "\r\nCCP_CLIENT_PRIVATE_KEY: " $CCP_CLIENT_PRIVATE_KEY > secrets.txt - - name: Upload Artifacts to Workflow - if: always() - uses: actions/upload-artifact@v2 - with: - name: Secrets - path: | - secrets.txt + data/vault/PIN-APP-CYBRCLI/Application-CyberArk-httpspineapple.privilegecloud.cyberark.cloud-jgarcia/address|PAS_HOSTNAME;data/vault/PIN-APP-CYBRCLI/Application-CyberArk-httpspineapple.privilegecloud.cyberark.cloud-jgarcia/username|PAS_USERNAME;data/vault/PIN-APP-CYBRCLI/Application-CyberArk-httpspineapple.privilegecloud.cyberark.cloud-jgarcia/password|PAS_PASSWORD;data/vault/PIN-APP-CYBRCLI/Website-PIN-CLIENT-CERT-httpscloud-connect.infamousdevops.com-ccp_client_cert/password|CCP_CLIENT_CERT;data/vault/PIN-APP-CYBRCLI/Website-PIN-CLIENT-CERT-ccp.infamousdevops.com-ccp_client_key/password|CCP_CLIENT_PRIVATE_KEY;" - name: Test All - run: go test -v ./... - - build: - name: Build Binaries - runs-on: ubuntu-latest - needs: - - lint - - test - defaults: - run: - shell: bash - strategy: - matrix: - goos: [linux, darwin, windows] - goarch: [amd64] - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - name: Install Go - uses: actions/setup-go@v4 - with: - go-version: '>=1.16' - cache: false - - name: Get current date & time - id: date - run: echo "::set-output name=date::$(date +'%Y%m%d_%H%M%S')" - - name: Export GO111MODULE environment variable - run: export GO111MODULE=on - - name: Create ./bin/ directory - run: mkdir -p bin - - name: Fix x/sys Issues - run: go get -u golang.org/x/sys - - name: Build Binaries - run: | - CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -o ./bin/${{ matrix.goos }}_cybr . - - name: Build Docker Container Package - run: | - docker build -t nfmsjoeg/cybr-cli:$TAG_NAME . - docker save nfmsjoeg/cybr-cli:$TAG_NAME > ./bin/docker_authenticator.tar env: - TAG_NAME: alpha-${{ steps.date.outputs.date }} - - name: Upload Artifacts to Workflow - if: always() - uses: actions/upload-artifact@v2 - with: - name: Release Executables - path: | - ./bin/*_cybr* + CCP_HOSTNAME: "https://ccp.infamousdevops.com" + run: | + export CCP_CLIENT_CERT=$(echo $CCP_CLIENT_CERT | base64 -d) + export CCP_CLIENT_PRIVATE_KEY=$(echo $CCP_CLIENT_PRIVATE_KEY | base64 -d) + go test -v ./... \ No newline at end of file diff --git a/cmd/safes.go b/cmd/safes.go index e6f6d00..b4f68f2 100644 --- a/cmd/safes.go +++ b/cmd/safes.go @@ -91,6 +91,8 @@ var ( User string // Group is the group to search for as a safe member Group string + // MemberType is the type of member being added to the safe + MemberType string ) var safesCmd = &cobra.Command{ @@ -231,7 +233,8 @@ var addMembersCmd = &cobra.Command{ Example Usage: $ cybr safes add-member -s SafeName -m MemberName --list-account --use-account --retrieve-account - $ cybr safes add-member -s SafeName -m MemberName --role ApplicationIdentity`, + $ cybr safes add-member -s SafeName -m MemberName --role ApplicationIdentity --member-type user + $ cybr safes add-member -s SafeName -m MemberName --role ApplicationIdentity --member-type group`, Run: func(cmd *cobra.Command, args []string) { // Get config file written to local file system client, err := pasapi.GetConfigWithLogger(getLogger()) @@ -265,6 +268,7 @@ var addMembersCmd = &cobra.Command{ SearchIn: SearchIn, MembershipExpirationDate: MembershipExpirationDate, Permissions: RolePermissions, + MemberType: MemberType, } // Add a safe with the configuration options given via CLI subcommands @@ -436,6 +440,8 @@ func init() { addMembersCmd.Flags().StringVarP(&MemberName, "member-name", "m", "", "Name of member being added to the desired safe") addMembersCmd.MarkFlagRequired("member-name") addMembersCmd.Flags().StringVarP(&SearchIn, "search-in", "i", "Vault", "Search in Domain or Vault") + addMembersCmd.Flags().StringVarP(&MemberType, "member-type", "t", "user", "Type of member being added to the safe: user (default) or group") + addMembersCmd.MarkFlagRequired("member-type") addMembersCmd.Flags().StringVarP(&MembershipExpirationDate, "member-expiration-date", "e", "", "When the membership will expire") addMembersCmd.Flags().StringVarP(&Role, "role", "r", "", "The role of the safe member being added for automated permissioning") addMembersCmd.Flags().BoolVar(&UseAccounts, "use-accounts", false, "Use accounts in safe") diff --git a/pkg/cybr/api/accounts.go b/pkg/cybr/api/accounts.go index 30957d8..ae5955b 100644 --- a/pkg/cybr/api/accounts.go +++ b/pkg/cybr/api/accounts.go @@ -12,6 +12,9 @@ import ( httpJson "github.com/infamousjoeg/cybr-cli/pkg/cybr/helpers/httpjson" ) +// Create an empty JSON body payload +var emptyBody = struct{}{} + // ListAccounts CyberArk user has access to func (c Client) ListAccounts(query *queries.ListAccounts) (*responses.ListAccount, error) { url := fmt.Sprintf("%s/passwordvault/api/Accounts%s", c.BaseURL, httpJson.GetURLQuery(query)) @@ -73,7 +76,7 @@ func (c Client) DeleteAccount(accountID string) error { // GetJITAccess from a specific account func (c Client) GetJITAccess(accountID string) error { url := fmt.Sprintf("%s/passwordvault/api/Accounts/%s/grantAdministrativeAccess", c.BaseURL, accountID) - response, err := httpJson.Post(false, url, c.SessionToken, nil, c.InsecureTLS, c.Logger) + response, err := httpJson.Post(false, url, c.SessionToken, emptyBody, c.InsecureTLS, c.Logger) if err != nil { returnedError, _ := json.Marshal(response) return fmt.Errorf("Failed to get JIT access for account '%s'. %s. %s", accountID, string(returnedError), err) @@ -85,7 +88,7 @@ func (c Client) GetJITAccess(accountID string) error { // RevokeJITAccess from a specific account func (c Client) RevokeJITAccess(accountID string) error { url := fmt.Sprintf("%s/passwordvault/api/Accounts/%s/RevokeAdministrativeAccess", c.BaseURL, accountID) - response, err := httpJson.Post(false, url, c.SessionToken, nil, c.InsecureTLS, c.Logger) + response, err := httpJson.Post(false, url, c.SessionToken, emptyBody, c.InsecureTLS, c.Logger) if err != nil { returnedError, _ := json.Marshal(response) return fmt.Errorf("Failed to revoke JIT access for account '%s'. %s. %s", accountID, string(returnedError), err) @@ -126,7 +129,7 @@ func (c Client) GetAccountSSHKey(accountID string, request requests.GetAccountPa // VerifyAccountCredentials marks an account for verification func (c Client) VerifyAccountCredentials(accountID string) error { url := fmt.Sprintf("%s/passwordvault/API/Accounts/%s/Verify", c.BaseURL, accountID) - response, err := httpJson.Post(false, url, c.SessionToken, nil, c.InsecureTLS, c.Logger) + response, err := httpJson.Post(false, url, c.SessionToken, emptyBody, c.InsecureTLS, c.Logger) if err != nil { returnedError, _ := json.Marshal(response) return fmt.Errorf("Failed to verify account '%s'. %s. %s", accountID, string(returnedError), err) @@ -158,7 +161,7 @@ func (c Client) ChangeAccountCredentials(accountID string, changeEntireGroup boo // ReconileAccountCredentials marks an account for reconciliation func (c Client) ReconileAccountCredentials(accountID string) error { url := fmt.Sprintf("%s/passwordvault/API/Accounts/%s/Reconcile", c.BaseURL, accountID) - response, err := httpJson.Post(false, url, c.SessionToken, nil, c.InsecureTLS, c.Logger) + response, err := httpJson.Post(false, url, c.SessionToken, emptyBody, c.InsecureTLS, c.Logger) if err != nil { returnedError, _ := json.Marshal(response) return fmt.Errorf("Failed to mark reconcile on account '%s'. %s. %s", accountID, string(returnedError), err) @@ -170,7 +173,7 @@ func (c Client) ReconileAccountCredentials(accountID string) error { // Unlock removes a lock from an account func (c Client) Unlock(accountID string) error { url := fmt.Sprintf("%s/passwordvault/API/Accounts/%s/Unlock", c.BaseURL, accountID) - response, err := httpJson.Post(false, url, c.SessionToken, nil, c.InsecureTLS, c.Logger) + response, err := httpJson.Post(false, url, c.SessionToken, emptyBody, c.InsecureTLS, c.Logger) if err != nil { returnedError, _ := json.Marshal(response) return fmt.Errorf("Failed to unlock account '%s'. %s. %s", accountID, string(returnedError), err) @@ -182,7 +185,7 @@ func (c Client) Unlock(accountID string) error { // CheckIn checks in an account that is checked out by the user func (c Client) CheckIn(accountID string) error { url := fmt.Sprintf("%s/passwordvault/API/Accounts/%s/CheckIn", c.BaseURL, accountID) - response, err := httpJson.Post(false, url, c.SessionToken, nil, c.InsecureTLS, c.Logger) + response, err := httpJson.Post(false, url, c.SessionToken, emptyBody, c.InsecureTLS, c.Logger) if err != nil { returnedError, _ := json.Marshal(response) return fmt.Errorf("Failed to check-in account '%s'. %s. %s", accountID, string(returnedError), err) diff --git a/pkg/cybr/api/accounts_test.go b/pkg/cybr/api/accounts_test.go index fef76b8..0e2eadb 100644 --- a/pkg/cybr/api/accounts_test.go +++ b/pkg/cybr/api/accounts_test.go @@ -8,10 +8,11 @@ import ( ) var ( - accountSafeName = "CLI_ACCOUNTS_TEST" - accountID = "110_3" - accountSSHKeyID = "110_15" - invalidAccountID = "202_5" + accountSafeName = "PIN-APP-CYBRCLI-TEST" + accountID = "162_6" + accountUsername = "test" + accountSSHKeyID = "162_4" + invalidAccountID = "999_9" ) func TestListAccountSuccess(t *testing.T) { @@ -45,12 +46,12 @@ func TestListAccountSearchSuccess(t *testing.T) { func TestGetAccountSuccess(t *testing.T) { client, err := defaultPASAPIClient(t) - account, err := client.GetAccount("110_3") + account, err := client.GetAccount(accountID) if err != nil { t.Errorf("Failed to get account. %s", err) } - if account.UserName != "test" { + if account.UserName != accountUsername { t.Errorf("Retrieved invalid account. Account has username '%s' and should be 'test'", account.UserName) } } diff --git a/pkg/cybr/api/auth.go b/pkg/cybr/api/auth.go index 235204d..f395f6b 100644 --- a/pkg/cybr/api/auth.go +++ b/pkg/cybr/api/auth.go @@ -47,7 +47,7 @@ func (c *Client) Logon(ctx context.Context, req requests.Logon) (context.Context func (c Client) Logoff() error { // Set URL for request url := fmt.Sprintf("%s/passwordvault/api/auth/logoff", c.BaseURL) - _, err := httpJson.Post(false, url, c.SessionToken, nil, c.InsecureTLS, c.Logger) + _, err := httpJson.Post(false, url, c.SessionToken, emptyBody, c.InsecureTLS, c.Logger) if err != nil { return fmt.Errorf("Unable to logoff PAS REST API Web Service. %s", err) } diff --git a/pkg/cybr/api/client_test.go b/pkg/cybr/api/client_test.go index ec8f556..8042341 100644 --- a/pkg/cybr/api/client_test.go +++ b/pkg/cybr/api/client_test.go @@ -18,12 +18,13 @@ var ( func defaultPASAPIClient(t *testing.T) (pasapi.Client, error) { client := pasapi.Client{ BaseURL: hostname, - AuthType: "ldap", + AuthType: "cyberark", } creds := requests.Logon{ - Username: username, - Password: password, + Username: username, + Password: password, + ConcurrentSession: true, } _, _, err := client.Logon(context.TODO(), creds) diff --git a/pkg/cybr/api/requests/addsafemember.go b/pkg/cybr/api/requests/addsafemember.go index 0a4338e..fca650b 100644 --- a/pkg/cybr/api/requests/addsafemember.go +++ b/pkg/cybr/api/requests/addsafemember.go @@ -6,4 +6,5 @@ type AddSafeMember struct { SearchIn string `json:"SearchIn"` MembershipExpirationDate string `json:"MembershipExpirationDate,omitempty"` Permissions map[string]string `json:"Permissions,omitempty"` + MemberType string `json:"MemberType"` } diff --git a/pkg/cybr/api/responses/listsafemembers.go b/pkg/cybr/api/responses/listsafemembers.go index 71d16e7..7cdab11 100644 --- a/pkg/cybr/api/responses/listsafemembers.go +++ b/pkg/cybr/api/responses/listsafemembers.go @@ -10,7 +10,7 @@ type ListSafeMembers struct { type Members struct { SafeName string `json:"safeName"` SafeNumber int `json:"safeNumber"` - MemberID int `json:"memberId"` + MemberID interface{} `json:"memberId"` MemberName string `json:"memberName"` MemberType string `json:"memberType"` IsExpiredMembershipEnable bool `json:"isExpiredMembershipEnable"` diff --git a/pkg/cybr/api/safes.go b/pkg/cybr/api/safes.go index 3be21b5..effbe1f 100644 --- a/pkg/cybr/api/safes.go +++ b/pkg/cybr/api/safes.go @@ -50,7 +50,7 @@ func (c Client) AddSafeMember(safeName string, addMember requests.AddSafeMember) // RemoveSafeMember Remove a member from a specific safe func (c Client) RemoveSafeMember(safeName string, member string) error { - url := fmt.Sprintf("%s/passwordvault/WebServices/PIMServices.svc/Safes/%s/Members/%s", c.BaseURL, url.QueryEscape(safeName), url.QueryEscape(member)) + url := fmt.Sprintf("%s/passwordvault/api/Safes/%s/Members/%s", c.BaseURL, url.QueryEscape(safeName), url.QueryEscape(member)) response, err := httpJson.Delete(false, url, c.SessionToken, c.InsecureTLS, c.Logger) if err != nil { returnedError, _ := json.Marshal(response) diff --git a/pkg/cybr/api/safes_test.go b/pkg/cybr/api/safes_test.go index 3180c07..4e51462 100644 --- a/pkg/cybr/api/safes_test.go +++ b/pkg/cybr/api/safes_test.go @@ -92,7 +92,7 @@ func TestListSafeMembersInvalidSafeName(t *testing.T) { } } -func TestAddRemoveSafeSuccess(t *testing.T) { +func TestAddSafeSuccess(t *testing.T) { client, err := defaultPASAPIClient(t) newSafe := requests.AddSafe{ @@ -106,27 +106,12 @@ func TestAddRemoveSafeSuccess(t *testing.T) { if err != nil { t.Errorf("Failed to create safe '%s' even though it should have been created successfully. %s", newSafe.SafeName, err) } - - err = client.DeleteSafe(newSafe.SafeName) - if err != nil { - t.Errorf("Failed to delete safe '%s' even though it should exist and should be deletable. %s", newSafe.SafeName, err) - } -} - -func TestRemoveSafeFail(t *testing.T) { - client, err := defaultPASAPIClient(t) - - safeName := "notRealSafeName" - err = client.DeleteSafe(safeName) - if err == nil { - t.Errorf("Client returned successful safe deletion even though safe '%s' should not exist", safeName) - } } func TestAddRemoveSafeMemberSuccess(t *testing.T) { client, err := defaultPASAPIClient(t) - safeName := "PasswordManager" + safeName := "TestCreateDelete" memberName := "test-add-member" retrieveAccounts, err := keyValueStringToMap("RetrieveAccounts=true") @@ -138,6 +123,7 @@ func TestAddRemoveSafeMemberSuccess(t *testing.T) { MemberName: memberName, SearchIn: "Vault", Permissions: retrieveAccounts, + MemberType: "user", } err = client.AddSafeMember(safeName, addMember) @@ -154,7 +140,7 @@ func TestAddRemoveSafeMemberSuccess(t *testing.T) { func TestAddMemberInvalidMemberName(t *testing.T) { client, err := defaultPASAPIClient(t) - safeName := "PasswordManager" + safeName := "TestCreateDelete" memberName := "notReal" retrieveAccounts, err := keyValueStringToMap("RetrieveAccounts=true") @@ -166,6 +152,7 @@ func TestAddMemberInvalidMemberName(t *testing.T) { MemberName: memberName, SearchIn: "Vault", Permissions: retrieveAccounts, + MemberType: "user", } err = client.AddSafeMember(safeName, addMember) @@ -177,7 +164,7 @@ func TestAddMemberInvalidMemberName(t *testing.T) { func TestRemoveMemberInvalidMemberName(t *testing.T) { client, err := defaultPASAPIClient(t) - safeName := "PasswordManager" + safeName := "TestCreateDelete" memberName := "notReal" err = client.RemoveSafeMember(safeName, memberName) @@ -185,3 +172,23 @@ func TestRemoveMemberInvalidMemberName(t *testing.T) { t.Errorf("Removed a non-existent member. This should not happen") } } + +func TestRemoveSafeSuccess(t *testing.T) { + client, err := defaultPASAPIClient(t) + + safeName := "TestCreateDelete" + err = client.DeleteSafe(safeName) + if err != nil { + t.Errorf("Failed to delete safe '%s' even though it should exist and should be deletable. %s", safeName, err) + } +} + +func TestRemoveSafeFail(t *testing.T) { + client, err := defaultPASAPIClient(t) + + safeName := "notRealSafeName" + err = client.DeleteSafe(safeName) + if err == nil { + t.Errorf("Client returned successful safe deletion even though safe '%s' should not exist", safeName) + } +} diff --git a/pkg/cybr/api/users.go b/pkg/cybr/api/users.go index 63a1073..fdcc257 100644 --- a/pkg/cybr/api/users.go +++ b/pkg/cybr/api/users.go @@ -14,7 +14,7 @@ import ( func (c Client) UnsuspendUser(userID int) error { url := fmt.Sprintf("%s/passwordvault/api/Users/%d/activate", c.BaseURL, userID) - response, err := httpJson.Post(false, url, c.SessionToken, nil, c.InsecureTLS, c.Logger) + response, err := httpJson.Post(false, url, c.SessionToken, emptyBody, c.InsecureTLS, c.Logger) if err != nil { returnedError, _ := json.Marshal(response) return fmt.Errorf("Failed to unsuspend user with id '%d'. %s. %s", userID, string(returnedError), err) diff --git a/pkg/cybr/ccp/ccp_test.go b/pkg/cybr/ccp/ccp_test.go index 7c0fb9a..23220ff 100644 --- a/pkg/cybr/ccp/ccp_test.go +++ b/pkg/cybr/ccp/ccp_test.go @@ -1,7 +1,6 @@ package ccp_test import ( - "io/ioutil" "os" "testing" @@ -9,10 +8,10 @@ import ( ) var ( - hostname = os.Getenv("PAS_HOSTNAME") + hostname = os.Getenv("CCP_HOSTNAME") appID = "cybr-cli-ccp-test" - safe = "CLI_ACCOUNTS_TEST" - object = "Operating System-UnixSSH-10.0.0.1-test_list" + safe = "PIN-APP-CYBRCLI-TEST" + object = "Operating System-PL-WIN-DOMAIN-ADMIN-10.0.0.1-test-new" clientCert = os.Getenv("CCP_CLIENT_CERT") clientKey = os.Getenv("CCP_CLIENT_PRIVATE_KEY") ) @@ -21,11 +20,11 @@ func writeCertsToFile(clientCertContent string, clientKeyContent string) (string certFilePath := os.TempDir() + "/client.crt" keyFilePath := os.TempDir() + "/client.key" - err := ioutil.WriteFile(certFilePath, []byte(clientCertContent), 0644) + err := os.WriteFile(certFilePath, []byte(clientCertContent), 0644) if err != nil { return "", "", err } - err = ioutil.WriteFile(keyFilePath, []byte(clientKeyContent), 0644) + err = os.WriteFile(keyFilePath, []byte(clientKeyContent), 0644) if err != nil { return "", "", err }