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/.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 2c4ad46..6ed92f0 100644 --- a/cmd/logon.go +++ b/cmd/logon.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "fmt" "log" "os" @@ -9,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" @@ -51,49 +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 - // 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") + ctx := context.Background() + + // 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 - err = c.Logon(credentials) - if err != nil && !strings.Contains(err.Error(), "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() + + 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 { - // Get OTP code from Stdin - credentials, err = util.ReadOTPcode(credentials) - err = c.Logon(credentials) + return handleLogonError(c, credentials, errorResponse, err) + } + + return c.SetConfig() +} + +func handleLogonError(c pasapi.Client, credentials requests.Logon, errorResponse *shared.ErrorResponse, err error) error { + if errorResponse != nil && errorResponse.ErrorCode == "ITATS542I" { + 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("handleLogonError: Failed to logon to the PVWA. %s", err) } func startAuthIdentity(c pasapi.Client, username string) (*responses.Authentication, error) { 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/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..ae5955b 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" @@ -11,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)) @@ -72,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) @@ -84,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) @@ -95,9 +99,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 +113,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) @@ -122,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) @@ -154,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) @@ -166,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) @@ -178,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 654413f..f395f6b 100644 --- a/pkg/cybr/api/auth.go +++ b/pkg/cybr/api/auth.go @@ -1,38 +1,53 @@ 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) + 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) + if errUm != nil { + return ctx, nil, errUm + } + + return ctx, errorResponse, fmt.Errorf("Failed to authenticate to the PAS REST API. %s", err) + } if err != nil { - return fmt.Errorf("Failed to authenticate to the PAS REST API. %s", err) + fmt.Printf("Logon Error: %s \n", err.Error()) + return ctx, nil, 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 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/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..8042341 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" @@ -17,15 +18,16 @@ 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(creds) + _, _, err := client.Logon(context.TODO(), creds) if err != nil { t.Errorf("Failed to logon. %s", err) } 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/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/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 } diff --git a/pkg/cybr/helpers/httpjson/httpjson.go b/pkg/cybr/helpers/httpjson/httpjson.go index 376e453..83e17d2 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, nil) + } else { + jar.SetCookies(u, cookies) + } + 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) } @@ -103,16 +132,16 @@ func getResponse(identity bool, url string, method string, token string, body in 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 } // 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,42 @@ 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) - if err != nil { - return nil, err +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 && res.StatusCode != 500 { + return ctx, nil, err } + defer res.Body.Close() - content, err := ioutil.ReadAll(res.Body) + content, err := io.ReadAll(res.Body) + fmt.Printf("SRR Response: %s\n", content) 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 + + // 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("SendRequestRaw: Received non-200 status code '%d'", res.StatusCode) + } + + 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 +245,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 +254,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/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) 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) }