diff --git a/.github/workflows/keyfactor-bootstrap-workflow.yml b/.github/workflows/keyfactor-bootstrap-workflow.yml index a4c7eaf..735191a 100644 --- a/.github/workflows/keyfactor-bootstrap-workflow.yml +++ b/.github/workflows/keyfactor-bootstrap-workflow.yml @@ -10,215 +10,8 @@ on: - 'release-*.*' jobs: - get-versions: - runs-on: ubuntu-latest - outputs: - PR_BASE_REF: ${{ steps.set-outputs.outputs.PR_BASE_REF }} - PR_COMMIT_SHA: ${{ steps.set-outputs.outputs.PR_COMMIT_SHA }} - GITHUB_SHA: ${{ steps.set-outputs.outputs.GITHUB_SHA }} - PR_BASE_TAG: ${{ steps.set-outputs.outputs.PR_BASE_TAG }} - IS_FULL_RELEASE: ${{ steps.set-outputs.outputs.IS_FULL_RELEASE }} - IS_PRE_RELEASE: ${{ steps.set-outputs.outputs.IS_PRE_RELEASE }} - INC_LEVEL: ${{ steps.set-outputs.outputs.INC_LEVEL }} - IS_RELEASE_BRANCH: ${{ steps.set-outputs.outputs.IS_RELEASE_BRANCH }} - IS_HOTFIX: ${{ steps.set-outputs.outputs.IS_HOTFIX }} - LATEST_TAG: ${{ steps.set-outputs.outputs.LATEST_TAG }} - NEXT_VERSION: ${{ steps.set-outputs.outputs.NEW_PKG_VERSION }} - - steps: - - name: Check out the code - uses: actions/checkout@v3 - with: - token: ${{ secrets.V2BUILDTOKEN}} - - - name: Display base.ref from Pull Request - if: github.event_name == 'pull_request' - id: display-from-pr - run: | - echo "Event: ${{ github.event_name }}" | tee -a $GITHUB_STEP_SUMMARY - echo "Event Action: ${{ github.event.action }}" | tee -a $GITHUB_STEP_SUMMARY - echo "PR_BASE_REF=${{ github.event.pull_request.base.ref }}" | tee -a "$GITHUB_ENV" | tee -a $GITHUB_STEP_SUMMARY - echo "PR_STATE=${{ github.event.pull_request.state }}" | tee -a "$GITHUB_ENV" | tee -a $GITHUB_STEP_SUMMARY - echo "PR_MERGED=${{ github.event.pull_request.merged }}" | tee -a "$GITHUB_ENV" | tee -a $GITHUB_STEP_SUMMARY - echo "PR_COMMIT_SHA=${{ github.event.pull_request.merge_commit_sha }}" | tee -a "$GITHUB_ENV" | tee -a $GITHUB_STEP_SUMMARY - echo "GITHUB_SHA=${{ github.sha }}" | tee -a "$GITHUB_ENV" | tee -a $GITHUB_STEP_SUMMARY - baseref="${{ github.event.pull_request.base.ref }}" - basetag="${baseref#release-}" - echo "PR_BASE_TAG=$basetag" | tee -a "$GITHUB_ENV" | tee -a $GITHUB_STEP_SUMMARY - - - name: Display base_ref from Push Event - if: github.event_name == 'push' - id: display-from-push - run: | - echo "Branch Ref: ${{ github.ref }}" | tee -a $GITHUB_STEP_SUMMARY - echo "Event: ${{ github.event_name }}" | tee -a $GITHUB_STEP_SUMMARY - echo "github.sha: ${{ github.sha }}" | tee -a $GITHUB_STEP_SUMMARY - - - name: Find Latest Tag - if: github.event_name == 'pull_request' - id: find-latest-tag - run: | - prbasetag="${{env.PR_BASE_TAG}}" - git fetch --tags - if [[ -n `git tag` ]]; then - echo "Setting vars" - allBranchTags=`git tag --sort=-v:refname | grep "^$prbasetag" || echo ""` - allRepoTags=`git tag --sort=-v:refname` - branchTagBase=`git tag --sort=-v:refname | grep "^$prbasetag" | grep -o '^[0-9.]*' | head -n 1 || echo ""` - latestTagBase=`git tag --sort=-v:refname | grep -o '^[0-9.]*' | head -n 1` - latestBranchTag=`git tag --sort=-v:refname | grep "^$prbasetag" | grep "^$branchTagBase" | head -n 1 || echo ""` - latestReleasedTag=`git tag --sort=-v:refname | grep "^$prbasetag" | grep "^$branchTagBase$" | head -n 1 || echo ""` - - # If the *TagBase values are not found in the list of tags, it means no final release was produced, and the latest*Tag vars will be empty - if [[ -z "$latestReleasedTag" ]]; then - latestTag="$latestBranchTag" - else - latestTag="$latestReleasedTag" - fi - echo "LATEST_TAG=${latestTag}" | tee -a "$GITHUB_ENV" - - if [[ "$latestTagBase" == *"$branchTagBase" ]]; then - hf="False" - else - hf="True" - fi - - # The intention is to use this to set the make_latest:false property when - # dispatching the create-release action, but it is not *yet* a configurable option - echo "IS_HOTFIX=$hf" | tee -a "$GITHUB_ENV" - else - echo "No tags exist in this repo" - echo "LATEST_TAG=" | tee -a "$GITHUB_ENV" - fi - - name: Set Outputs - id: set-outputs - run: | - echo "PR_BASE_REF=${{ env.PR_BASE_REF }}" | tee -a "$GITHUB_OUTPUT" - echo "PR_STATE=${{ env.PR_STATE }}" - echo "PR_MERGED=${{ env.PR_MERGED }}" - if [[ "${{ env.PR_STATE }}" == "closed" && "${{ env.PR_MERGED }}" == "true" && "${{ env.PR_COMMIT_SHA }}" == "${{ env.GITHUB_SHA }}" ]]; then - echo "IS_FULL_RELEASE=True" | tee -a "$GITHUB_OUTPUT" - echo "INC_LEVEL=patch" | tee -a "$GITHUB_OUTPUT" - fi - if [[ "${{ env.PR_STATE }}" == "open" ]]; then - echo "IS_PRE_RELEASE=True" | tee -a "$GITHUB_OUTPUT" | tee -a "$GITHUB_ENV" - echo "INC_LEVEL=prerelease" | tee -a "$GITHUB_OUTPUT" - fi - if [[ "${{ env.PR_BASE_REF }}" == "release-"* ]]; then - echo "IS_RELEASE_BRANCH=True" | tee -a "$GITHUB_OUTPUT" | tee -a "$GITHUB_ENV" - fi - echo "PR_COMMIT_SHA=${{ env.PR_COMMIT_SHA }}" | tee -a "$GITHUB_OUTPUT" - echo "GITHUB_SHA=${{ env.GITHUB_SHA }}" | tee -a "$GITHUB_OUTPUT" - echo "PR_BASE_TAG=${{ env.PR_BASE_TAG }}" | tee -a "$GITHUB_OUTPUT" - echo "IS_HOTFIX=${{ env.IS_HOTFIX }}" | tee -a "$GITHUB_OUTPUT" - echo "LATEST_TAG=${{ env.LATEST_TAG }}" | tee -a "$GITHUB_OUTPUT" - -# check-package-version: -# needs: get-versions -# if: github.event_name == 'pull_request' && needs.get-versions.outputs.IS_RELEASE_BRANCH == 'True' -# outputs: -# release_version: ${{ steps.create_release.outputs.current_tag }} -# release_url: ${{ steps.create_release.outputs.upload_url }} -# update_version: ${{ steps.check_version.outputs.update_version }} -# next_version: ${{ steps.set-semver-info.outputs.new_version }} -# runs-on: ubuntu-latest -# steps: -# - name: Check out the code -# uses: actions/checkout@v3 -# - run: | -# echo "INC_LEVEL=${{ needs.get-versions.outputs.INC_LEVEL}}" -# - name: Check if initial release -# if: needs.get-versions.outputs.LATEST_TAG == '' -# run: | -# echo "INITIAL_VERSION=${{needs.get-versions.outputs.PR_BASE_TAG}}.0-rc.0" | tee -a "$GITHUB_STEP_SUMMARY" | tee -a "$GITHUB_ENV" -# echo "MANUAL_VERSION=${{needs.get-versions.outputs.PR_BASE_TAG}}.0-rc.0" | tee -a "$GITHUB_ENV" -# - name: Set semver info -# id: set-semver-info -# if: needs.get-versions.outputs.LATEST_TAG != '' -# uses: fiddlermikey/action-bump-semver@main -# with: -# current_version: ${{ needs.get-versions.outputs.LATEST_TAG}} -# level: ${{ needs.get-versions.outputs.INC_LEVEL}} -# preID: rc -# - name: Show next sem-version -# if: needs.get-versions.outputs.LATEST_TAG != '' -# run: | -# echo "MANUAL_VERSION=${{ steps.set-semver-info.outputs.new_version }}" > "$GITHUB_ENV" -# - run: | -# echo "Next version: ${{ env.MANUAL_VERSION }}" | tee -a "$GITHUB_STEP_SUMMARY" -# -# - name: Get Package Version -# id: get-pkg-version -# run: | -# pwd -# ls -la -# echo "CURRENT_PKG_VERSION=$(cat pkg/version/version.go | grep 'const VERSION' | awk '{print $NF}' | tr -d '"')" | tee -a "$GITHUB_ENV" -# - name: Compare package version -# id: check_version -# run: | -# if [ "${{ env.CURRENT_PKG_VERSION }}" != "${{ env.MANUAL_VERSION }}" ]; then -# echo "Updating version in version.go" -# echo "update_version=true" | tee -a $GITHUB_ENV | tee -a $GITHUB_OUTPUT -# echo "update_version=true" | tee -a "$GITHUB_STEP_SUMMARY" -# else -# echo "Versions match, no update needed" -# echo "update_version=false" | tee -a $GITHUB_ENV | tee -a $GITHUB_OUTPUT -# echo "update_version=false" | tee -a $GITHUB_STEP_SUMMARY -# fi -# env: -# UPDATE_VERSION: ${{ steps.check_version.outputs.update_version }} -# -# - name: Set Outputs -# id: set-outputs -# if: needs.get-versions.outputs.LATEST_TAG != '' -# run: | -# echo "UPDATE_VERSION=${{ steps.check_version.outputs.update_version }}" | tee -a "$GITHUB_OUTPUT" -# echo "CURRENT_PKG_VERSION=${{ env.CURRENT_PKG_VERSION }}" | tee -a "$GITHUB_OUTPUT" -# echo "MANUAL_VERSION=${{ env.MANUAL_VERSION }}" | tee -a "$GITHUB_OUTPUT" -# echo "NEW_PKG_VERSION=${{ env.MANUAL_VERSION }}" | tee -a "$GITHUB_OUTPUT" -# -# update-pkg-version: -# needs: -# - check-package-version -# runs-on: ubuntu-latest -# -# steps: -# - name: Checkout repository -# uses: actions/checkout@v3 -# with: -# token: ${{ secrets.V2BUILDTOKEN}} -# - name: No Update -# if: ${{ needs.check-package-version.outputs.update_version != 'true' }} -# run: | -# echo "Versions match, no update needed" -# exit 0 -# -# - name: Commit to PR branch -# id: commit-version -# if: ${{ needs.check-package-version.outputs.update_version == 'true' }} -# env: -# AUTHOR_EMAIL: keyfactor@keyfactor.github.io -# AUTHOR_NAME: Keyfactor Robot -# GITHUB_TOKEN: ${{ secrets.V2BUILDTOKEN}} -# run: | -# git remote -v -# echo "Checking out ${{ github.head_ref }}" -# git fetch -# echo "git checkout -b ${{ github.head_ref }}" -# git checkout -b ${{ github.head_ref }} -# git reset --hard origin/${{ github.head_ref }} -# sed -i "s/const VERSION = .*/const VERSION = \"${{ needs.check-package-version.outputs.next_version }}\"/" pkg/version/version.go -# git add pkg/version/version.go -# git config --global user.email "${{ env.AUTHOR_EMAIL }}" -# git config --global user.name "${{ env.AUTHOR_NAME }}" -# git commit -m "Bump package version to ${{ needs.check-package-version.outputs.next_version }}" -# git push --set-upstream origin ${{ github.head_ref }} -# echo "Version mismatch! Please create a new pull request with the updated version." -# exit 1 - call-starter-workflow: uses: keyfactor/actions/.github/workflows/starter.yml@v2 - needs: get-versions secrets: token: ${{ secrets.V2BUILDTOKEN}} APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}} diff --git a/README.md b/README.md index dfe160a..08fc1b1 100644 --- a/README.md +++ b/README.md @@ -440,3 +440,4 @@ alternatively you can specify the parent command cobra-cli add -p 'Cmd' ``` + diff --git a/cmd/constants.go b/cmd/constants.go index b0caea3..b260479 100644 --- a/cmd/constants.go +++ b/cmd/constants.go @@ -1,4 +1,4 @@ -// Copyright 2024 Keyfactor +// Package cmd Copyright 2024 Keyfactor // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,28 +16,47 @@ package cmd import "fmt" const ( - ColorRed = "\033[31m" - ColorWhite = "\033[37m" - DefaultAPIPath = "KeyfactorAPI" - DefaultConfigFileName = "command_config.json" - FailedAuthMsg = "Login failed!" - SuccessfulAuthMsg = "Login successful!" - XKeyfactorRequestedWith = "APIClient" - XKeyfactorApiVersion = "1" - FlagGitRef = "git-ref" - FlagFromFile = "from-file" - DebugFuncEnter = "entered: %s" - DebugFuncExit = "exiting: %s" - DebugFuncCall = "calling: %s" + ColorRed = "\033[31m" + ColorWhite = "\033[37m" + DefaultAPIPath = "KeyfactorAPI" + DefaultConfigFileName = "command_config.json" + DefaultROTAuditStoresOutfilePath = "rot_audit_selected_stores.csv" + DefaultROTAuditAddCertsOutfilePath = "rot_audit_selected_certs_add.csv" + DefaultROTAuditRemoveCertsOutfilePath = "rot_audit_selected_certs_remove.csv" + FailedAuthMsg = "Login failed!" + SuccessfulAuthMsg = "Login successful!" + XKeyfactorRequestedWith = "APIClient" + XKeyfactorApiVersion = "1" + FlagGitRef = "git-ref" + FlagFromFile = "from-file" + DebugFuncEnter = "entered: %s" + DebugFuncExit = "exiting: %s" + DebugFuncCall = "calling: %s" + ErrMsgEmptyResponse = "empty response received from Keyfactor Command %s" +) + +// CLI Menu Defaults +const ( + DefaultMenuPageSizeSmall = 25 + DefaultMenuPageSizeLarge = 100 +) + +var ( + DefaultSourceTypeOptions = []string{"API", "File"} ) var ProviderTypeChoices = []string{ "azid", } var ValidAuthProviders = [2]string{"azure-id", "azid"} +var ErrKfcEmptyResponse = fmt.Errorf("empty response recieved from Keyfactor Command") // Error messages var ( - StoreTypeReadError = fmt.Errorf("error reading store type from configuration file") - InvalidInputError = fmt.Errorf("invalid input") + StoreTypeReadError = fmt.Errorf("error reading store type from configuration file") + InvalidInputError = fmt.Errorf("invalid input") + InvalidROTCertsInputErr = fmt.Errorf( + "at least one of `--add-certs` or `--remove-certs` is required to perform a" + + " root of trust audit", + ) ) diff --git a/cmd/export.go b/cmd/export.go index 07d0431..9048465 100644 --- a/cmd/export.go +++ b/cmd/export.go @@ -18,12 +18,13 @@ import ( "context" "encoding/json" "fmt" + "os" + "strconv" + "github.com/Keyfactor/keyfactor-go-client-sdk/api/keyfactor" "github.com/Keyfactor/keyfactor-go-client/v2/api" "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "os" - "strconv" ) var exportPath string @@ -371,8 +372,10 @@ func getIssuedAlerts(kfClient *keyfactor.APIClient) []keyfactor.KeyfactorApiMode func getDeniedAlerts(kfClient *keyfactor.APIClient) []keyfactor.KeyfactorApiModelsAlertsDeniedDeniedAlertCreationRequest { alerts, _, reqErr := kfClient.DeniedAlertApi.DeniedAlertGetDeniedAlerts( - context.Background()).XKeyfactorRequestedWith( - XKeyfactorRequestedWith).XKeyfactorApiVersion(XKeyfactorApiVersion).Execute() + context.Background(), + ).XKeyfactorRequestedWith( + XKeyfactorRequestedWith, + ).XKeyfactorApiVersion(XKeyfactorApiVersion).Execute() if reqErr != nil { fmt.Printf("%s Error! Unable to get denied cert alerts %s%s\n", ColorRed, reqErr, ColorWhite) } @@ -575,7 +578,13 @@ func init() { exportCmd.Flags().Lookup("collections").NoOptDefVal = "true" exportCmd.Flags().BoolVarP(&fMetadata, "metadata", "m", false, "export metadata to JSON file") exportCmd.Flags().Lookup("metadata").NoOptDefVal = "true" - exportCmd.Flags().BoolVarP(&fExpirationAlerts, "expiration-alerts", "e", false, "export expiration cert alerts to JSON file") + exportCmd.Flags().BoolVarP( + &fExpirationAlerts, + "expiration-alerts", + "e", + false, + "export expiration cert alerts to JSON file", + ) exportCmd.Flags().Lookup("expiration-alerts").NoOptDefVal = "true" exportCmd.Flags().BoolVarP(&fIssuedAlerts, "issued-alerts", "i", false, "export issued cert alerts to JSON file") exportCmd.Flags().Lookup("issued-alerts").NoOptDefVal = "true" @@ -585,7 +594,13 @@ func init() { exportCmd.Flags().Lookup("pending-alerts").NoOptDefVal = "true" exportCmd.Flags().BoolVarP(&fNetworks, "networks", "n", false, "export SSL networks to JSON file") exportCmd.Flags().Lookup("networks").NoOptDefVal = "true" - exportCmd.Flags().BoolVarP(&fWorkflowDefinitions, "workflow-definitions", "w", false, "export workflow definitions to JSON file") + exportCmd.Flags().BoolVarP( + &fWorkflowDefinitions, + "workflow-definitions", + "w", + false, + "export workflow definitions to JSON file", + ) exportCmd.Flags().Lookup("workflow-definitions").NoOptDefVal = "true" exportCmd.Flags().BoolVarP(&fReports, "reports", "r", false, "export reports to JSON file") exportCmd.Flags().Lookup("reports").NoOptDefVal = "true" diff --git a/cmd/helpers.go b/cmd/helpers.go index 0a07df8..5a5c7af 100644 --- a/cmd/helpers.go +++ b/cmd/helpers.go @@ -19,18 +19,34 @@ import ( "encoding/json" "errors" "fmt" - "github.com/google/uuid" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" "io" "net/http" "os" "path/filepath" "strconv" "time" + + "github.com/google/uuid" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" ) +func mergeErrsToString(errs *[]error, indent bool) string { + var errStr string + if errs == nil || len(*errs) == 0 { + return "" + } + for _, err := range *errs { + if indent { + errStr += fmt.Sprintf(" \t%s\r\n", err) + continue + } + errStr += fmt.Sprintf("%s\r\n", err) + } + return errStr +} + func boolToPointer(b bool) *bool { return &b } diff --git a/cmd/root.go b/cmd/root.go index 7029f29..fe1dc3c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -16,15 +16,18 @@ package cmd import ( "fmt" + "io" + stdlog "log" + "os" + "os/signal" + "syscall" + "github.com/Keyfactor/keyfactor-go-client-sdk/api/keyfactor" "github.com/Keyfactor/keyfactor-go-client/v2/api" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" "golang.org/x/crypto/bcrypt" - "io" - stdlog "log" - "os" ) var ( @@ -45,6 +48,19 @@ var ( outputFormat string ) +func setupSignalHandler() { + // Start a goroutine to listen for SIGINT signals + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT) + + go func() { + <-sigChan + // Handle SIGINT signal + fmt.Println("\nCtrl+C pressed. Exiting...") + os.Exit(1) + }() +} + func hashSecretValue(secretValue string) string { log.Debug().Msg("Enter hashSecretValue()") if logInsecure { @@ -63,7 +79,15 @@ func hashSecretValue(secretValue string) string { return string(hashedPassword) } -func initClient(flagConfigFile string, flagProfile string, flagAuthProviderType string, flagAuthProviderProfile string, noPrompt bool, authConfig *api.AuthConfig, saveConfig bool) (*api.Client, error) { +func initClient( + flagConfigFile string, + flagProfile string, + flagAuthProviderType string, + flagAuthProviderProfile string, + noPrompt bool, + authConfig *api.AuthConfig, + saveConfig bool, +) (*api.Client, error) { log.Debug().Msg("Enter initClient()") var clientAuth api.AuthConfig var commandConfig ConfigurationFile @@ -163,9 +187,18 @@ func initClient(flagConfigFile string, flagProfile string, flagAuthProviderType if !noPrompt { // Auth user interactively authConfigEntry := commandConfig.Servers[flagProfile] - commandConfig, _ = authInteractive(authConfigEntry.Hostname, authConfigEntry.Username, authConfigEntry.Password, authConfigEntry.Domain, authConfigEntry.APIPath, flagProfile, false, false, flagConfigFile) + commandConfig, _ = authInteractive( + authConfigEntry.Hostname, + authConfigEntry.Username, + authConfigEntry.Password, + authConfigEntry.Domain, + authConfigEntry.APIPath, + flagProfile, + false, + false, + flagConfigFile, + ) } else { - //log.Fatalf("[ERROR] auth config profile: %s", flagProfile) log.Error().Str("flagProfile", flagProfile).Msg("invalid auth config profile") return nil, fmt.Errorf("invalid auth config profile: %s", flagProfile) } @@ -191,14 +224,19 @@ func initClient(flagConfigFile string, flagProfile string, flagAuthProviderType if err != nil { //fmt.Printf("Error connecting to Keyfactor: %s\n", err) outputError(err, true, "text") - //log.Fatalf("[ERROR] creating Keyfactor client: %s", err) return nil, fmt.Errorf("unable to create Keyfactor Command client: %s", err) } log.Info().Msg("Keyfactor Command client created") return c, nil } -func initGenClient(flagConfig string, flagProfile string, noPrompt bool, authConfig *api.AuthConfig, saveConfig bool) (*keyfactor.APIClient, error) { +func initGenClient( + flagConfig string, + flagProfile string, + noPrompt bool, + authConfig *api.AuthConfig, + saveConfig bool, +) (*keyfactor.APIClient, error) { var commandConfig ConfigurationFile if providerType != "" { @@ -246,7 +284,17 @@ func initGenClient(flagConfig string, flagProfile string, noPrompt bool, authCon if !noPrompt { // Auth user interactively authConfigEntry := commandConfig.Servers[flagProfile] - commandConfig, _ = authInteractive(authConfigEntry.Hostname, authConfigEntry.Username, authConfigEntry.Password, authConfigEntry.Domain, authConfigEntry.APIPath, flagProfile, false, false, flagConfig) + commandConfig, _ = authInteractive( + authConfigEntry.Hostname, + authConfigEntry.Username, + authConfigEntry.Password, + authConfigEntry.Domain, + authConfigEntry.APIPath, + flagProfile, + false, + false, + flagConfig, + ) } else { //log.Fatalf("[ERROR] auth config profile: %s", flagProfile) log.Error().Str("flagProfile", flagProfile).Msg("invalid auth config profile") @@ -306,24 +354,92 @@ func init() { defaultConfigPath := fmt.Sprintf("$HOME/.keyfactor/%s", DefaultConfigFileName) - RootCmd.PersistentFlags().StringVarP(&configFile, "config", "", "", fmt.Sprintf("Full path to config file in JSON format. (default is %s)", defaultConfigPath)) - RootCmd.PersistentFlags().BoolVar(&noPrompt, "no-prompt", false, "Do not prompt for any user input and assume defaults or environmental variables are set.") - RootCmd.PersistentFlags().BoolVar(&expEnabled, "exp", false, "Enable expEnabled features. (USE AT YOUR OWN RISK, these features are not supported and may change or be removed at any time.)") + RootCmd.PersistentFlags().StringVarP( + &configFile, + "config", + "", + "", + fmt.Sprintf("Full path to config file in JSON format. (default is %s)", defaultConfigPath), + ) + RootCmd.PersistentFlags().BoolVar( + &noPrompt, + "no-prompt", + false, + "Do not prompt for any user input and assume defaults or environmental variables are set.", + ) + RootCmd.PersistentFlags().BoolVar( + &expEnabled, + "exp", + false, + "Enable expEnabled features. (USE AT YOUR OWN RISK, these features are not supported and may change or be removed at any time.)", + ) RootCmd.PersistentFlags().BoolVar(&debugFlag, "debug", false, "Enable debugFlag logging.") - RootCmd.PersistentFlags().BoolVar(&logInsecure, "log-insecure", false, "Log insecure API requests. (USE AT YOUR OWN RISK, this WILL log sensitive information to the console.)") - RootCmd.PersistentFlags().StringVarP(&profile, "profile", "", "", "Use a specific profile from your config file. If not specified the config named 'default' will be used if it exists.") - RootCmd.PersistentFlags().StringVar(&outputFormat, "format", "text", "How to format the CLI output. Currently only `text` is supported.") + RootCmd.PersistentFlags().BoolVar( + &logInsecure, + "log-insecure", + false, + "Log insecure API requests. (USE AT YOUR OWN RISK, this WILL log sensitive information to the console.)", + ) + RootCmd.PersistentFlags().StringVarP( + &profile, + "profile", + "", + "", + "Use a specific profile from your config file. If not specified the config named 'default' will be used if it exists.", + ) + RootCmd.PersistentFlags().StringVar( + &outputFormat, + "format", + "text", + "How to format the CLI output. Currently only `text` is supported.", + ) RootCmd.PersistentFlags().StringVar(&providerType, "auth-provider-type", "", "Provider type choices: (azid)") // Validating the provider-type flag against the predefined choices RootCmd.PersistentFlags().SetAnnotation("auth-provider-type", cobra.BashCompCustom, ProviderTypeChoices) - RootCmd.PersistentFlags().StringVarP(&providerProfile, "auth-provider-profile", "", "default", "The profile to use defined in the securely stored config. If not specified the config named 'default' will be used if it exists.") - - RootCmd.PersistentFlags().StringVarP(&kfcUsername, "username", "", "", "Username to use for authenticating to Keyfactor Command.") - RootCmd.PersistentFlags().StringVarP(&kfcHostName, "hostname", "", "", "Hostname to use for authenticating to Keyfactor Command.") - RootCmd.PersistentFlags().StringVarP(&kfcPassword, "password", "", "", "Password to use for authenticating to Keyfactor Command. WARNING: Remember to delete your console history if providing kfcPassword here in plain text.") - RootCmd.PersistentFlags().StringVarP(&kfcDomain, "domain", "", "", "Domain to use for authenticating to Keyfactor Command.") - RootCmd.PersistentFlags().StringVarP(&kfcAPIPath, "api-path", "", "KeyfactorAPI", "API Path to use for authenticating to Keyfactor Command. (default is KeyfactorAPI)") + RootCmd.PersistentFlags().StringVarP( + &providerProfile, + "auth-provider-profile", + "", + "default", + "The profile to use defined in the securely stored config. If not specified the config named 'default' will be used if it exists.", + ) + + RootCmd.PersistentFlags().StringVarP( + &kfcUsername, + "username", + "", + "", + "Username to use for authenticating to Keyfactor Command.", + ) + RootCmd.PersistentFlags().StringVarP( + &kfcHostName, + "hostname", + "", + "", + "Hostname to use for authenticating to Keyfactor Command.", + ) + RootCmd.PersistentFlags().StringVarP( + &kfcPassword, + "password", + "", + "", + "Password to use for authenticating to Keyfactor Command. WARNING: Remember to delete your console history if providing kfcPassword here in plain text.", + ) + RootCmd.PersistentFlags().StringVarP( + &kfcDomain, + "domain", + "", + "", + "Domain to use for authenticating to Keyfactor Command.", + ) + RootCmd.PersistentFlags().StringVarP( + &kfcAPIPath, + "api-path", + "", + "KeyfactorAPI", + "API Path to use for authenticating to Keyfactor Command. (default is KeyfactorAPI)", + ) // Cobra also supports local flags, which will only run // when this action is called directly. diff --git a/cmd/rot.go b/cmd/rot.go index 897e65f..05abc87 100644 --- a/cmd/rot.go +++ b/cmd/rot.go @@ -16,42 +16,51 @@ package cmd import ( "bufio" + "context" "encoding/csv" "encoding/json" "errors" "fmt" - "github.com/Keyfactor/keyfactor-go-client/v2/api" - "github.com/spf13/cobra" - "log" + "net/http" "os" + "path/filepath" + "sort" "strconv" "strings" + + "github.com/AlecAivazis/survey/v2" + + sdk "github.com/Keyfactor/keyfactor-go-client-sdk/api/keyfactor" + "github.com/Keyfactor/keyfactor-go-client/v2/api" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" ) type templateType string -type StoreCSVEntry struct { - ID string `json:"id"` - Type string `json:"type"` - Machine string `json:"address"` - Path string `json:"path"` - Thumbprints map[string]bool `json:"thumbprints,omitempty"` - Serials map[string]bool `json:"serials,omitempty"` - Ids map[int]bool `json:"ids,omitempty"` + +type TrustStoreCriteria struct { + MinCerts int + MaxKeys int + MaxLeaf int } -type ROTCert struct { - ID int `json:"id,omitempty"` - ThumbPrint string `json:"thumbprint,omitempty"` - CN string `json:"cn,omitempty"` - Locations []api.CertificateLocations `json:"locations,omitempty"` + +func (t *TrustStoreCriteria) String() string { + return fmt.Sprintf("MinCerts: %d, MaxKeys: %d, MaxLeaf: %d", t.MinCerts, t.MaxKeys, t.MaxLeaf) +} + +var trustCriteria = TrustStoreCriteria{ + MinCerts: 1, + MaxKeys: 0, + MaxLeaf: 1, } -type ROTAction struct { - StoreID string `json:"store_id,omitempty"` - StoreType string `json:"store_type,omitempty"` - StorePath string `json:"store_path,omitempty"` - Thumbprint string `json:"thumbprint,omitempty"` - CertID int `json:"cert_id,omitempty" mapstructure:"CertID,omitempty"` - AddCert bool `json:"add,omitempty" mapstructure:"AddCert,omitempty"` - RemoveCert bool `json:"remove,omitempty" mapstructure:"RemoveCert,omitempty"` + +type KFCStore struct { + ApiResponse api.GetCertificateStoreResponse + Inventory []api.CertStoreInventory +} + +type KFCStores struct { + Stores map[string]KFCStore } const ( @@ -59,13 +68,6 @@ const ( reconcileDefaultFileName string = "rot_audit.csv" ) -var ( - AuditHeader = []string{"Thumbprint", "CertID", "SubjectName", "Issuer", "StoreID", "StoreType", "Machine", "Path", "AddCert", "RemoveCert", "Deployed", "AuditDate"} - ReconciledAuditHeader = []string{"Thumbprint", "CertID", "SubjectName", "Issuer", "StoreID", "StoreType", "Machine", "Path", "AddCert", "RemoveCert", "Deployed", "ReconciledDate"} - StoreHeader = []string{"StoreID", "StoreType", "StoreMachine", "StorePath", "ContainerId", "ContainerName", "LastQueriedDate"} - CertHeader = []string{"Thumbprint", "SubjectName", "Issuer", "CertID", "Locations", "LastQueriedDate"} -) - // String is used both by fmt.Print and by Cobra in help text func (e *templateType) String() string { return string(*e) @@ -89,14 +91,22 @@ func (e *templateType) Type() string { func templateTypeCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{ - "certs\tGenerates template CSV for certificate input to be used w/ `--add-certs` or `--remove-certs`", - "stores\tGenerates template CSV for certificate input to be used w/ `--stores`", - "actions\tGenerates template CSV for certificate input to be used w/ `--actions`", + "certsGenerates template CSV for certificate input to be used w/ `--add-certs` or `--remove-certs`", + "storesGenerates template CSV for certificate input to be used w/ `--stores`", + "actionsGenerates template CSV for certificate input to be used w/ `--actions`", }, cobra.ShellCompDirectiveDefault } -func generateAuditReport(addCerts map[string]string, removeCerts map[string]string, stores map[string]StoreCSVEntry, outpath string, kfClient *api.Client) ([][]string, map[string][]ROTAction, error) { - log.Println("[DEBUG] generateAuditReport called") +func generateAuditReport( + addCerts map[string]string, + removeCerts map[string]string, + stores map[string]StoreCSVEntry, + outputFilePath string, + kfClient *api.Client, +) ([][]string, map[string][]ROTAction, error) { + log.Debug().Msg(fmt.Sprintf(DebugFuncEnter, "generateAuditReport")) + + log.Info().Str("output_file", outputFilePath).Msg("Generating audit report") var ( data [][]string ) @@ -104,256 +114,2386 @@ func generateAuditReport(addCerts map[string]string, removeCerts map[string]stri data = append(data, AuditHeader) var csvFile *os.File var fErr error - if outpath == "" { + log.Debug().Str("output_file", outputFilePath).Msg("Checking for output file") + if outputFilePath == "" { + log.Debug().Str("output_file", reconcileDefaultFileName).Msg("No output file specified, using default") csvFile, fErr = os.Create(reconcileDefaultFileName) - outpath = reconcileDefaultFileName + outputFilePath = reconcileDefaultFileName } else { - csvFile, fErr = os.Create(outpath) + csvFile, fErr = os.Create(outputFilePath) } if fErr != nil { fmt.Printf("%s", fErr) - log.Fatalf("[ERROR] creating audit file: %s", fErr) + log.Error().Err(fErr).Str("output_file", outputFilePath).Msg("Error creating output file") } + + log.Trace().Str("output_file", outputFilePath).Msg("Creating CSV writer") csvWriter := csv.NewWriter(csvFile) + log.Debug().Str("output_file", outputFilePath).Strs("csv_header", AuditHeader).Msg("Writing header to CSV") cErr := csvWriter.Write(AuditHeader) if cErr != nil { - fmt.Printf("%s", cErr) - log.Fatalf("[ERROR] writing audit header: %s", cErr) + log.Error().Err(cErr).Str("output_file", outputFilePath).Msg("Error writing header to CSV") + return nil, nil, cErr } + + log.Trace().Str("output_file", outputFilePath).Msg("Creating actions map") actions := make(map[string][]ROTAction) - for _, cert := range addCerts { - certLookupReq := api.GetCertificateContextArgs{ - IncludeMetadata: boolToPointer(true), - IncludeLocations: boolToPointer(true), - CollectionId: nil, - Thumbprint: cert, - Id: 0, + var errs []error + for tp, cId := range addCerts { + log.Debug().Str("thumbprint", tp). + Str("cert_id", cId). + Msg("Looking up certificate") + certLookupReq := api.GetCertificateContextArgs{} + if cId != "" { + certIdInt, cErr := strconv.Atoi(cId) + if cErr != nil { + log.Error(). + Err(cErr). + Str("thumbprint", tp). + Msg("Error converting cert ID to integer, skipping") + errs = append(errs, cErr) + continue + } + certLookupReq = api.GetCertificateContextArgs{ + IncludeMetadata: boolToPointer(true), + IncludeLocations: boolToPointer(true), + CollectionId: nil, //todo: add CollectionID support + Thumbprint: "", + Id: certIdInt, + } + } else { + certLookupReq = api.GetCertificateContextArgs{ + IncludeMetadata: boolToPointer(true), + IncludeLocations: boolToPointer(true), + CollectionId: nil, //todo: add CollectionID support + Thumbprint: tp, + Id: 0, //todo: should also allow KFC ID + } } + + log.Debug(). + Str("thumbprint", tp). + Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetCertificateContext")) certLookup, err := kfClient.GetCertificateContext(&certLookupReq) if err != nil { - fmt.Printf("[ERROR] looking up certificate %s: %s\n", cert, err) - log.Printf("[ERROR] looking up cert: %s\n%v", cert, err) + log.Error(). + Err(err). + Str("thumbprint", tp). + Msg("Error looking up certificate, skipping") + errMsg := fmt.Errorf( + "error recieved from Keyfactor Command when looking up thumbprint '%s':'%w'", + tp, + err, + ) + errs = append(errs, errMsg) continue } certID := certLookup.Id certIDStr := strconv.Itoa(certID) + log.Debug().Str("thumbprint", tp).Msg("Iterating over stores") for _, store := range stores { - if _, ok := store.Thumbprints[cert]; ok { + log.Debug().Str("thumbprint", tp).Str("store_id", store.ID).Msg("Checking if cert is deployed to store") + if _, ok := store.Thumbprints[tp]; ok { // Cert is already in the store do nothing - row := []string{cert, certIDStr, certLookup.IssuedDN, certLookup.IssuerDN, store.ID, store.Type, store.Machine, store.Path, "false", "false", "true", getCurrentTime("")} + log.Info().Str("thumbprint", tp).Str("store_id", store.ID).Msg("Cert is already deployed to store") + row := []string{ + //todo: this should be a toCSV field on whatever object this is + tp, + certIDStr, + certLookup.IssuedDN, + certLookup.IssuerDN, + store.ID, + store.Type, + store.Machine, + store.Path, + "false", + "false", + "true", + getCurrentTime(""), + } + log.Trace().Str("thumbprint", tp).Strs("row", row).Msg("Appending data row") data = append(data, row) + log.Trace().Str("thumbprint", tp).Strs("row", row).Msg("Writing data row to CSV") wErr := csvWriter.Write(row) if wErr != nil { - fmt.Printf("[ERROR] writing audit file row: %s\n", wErr) - log.Printf("[ERROR] writing audit row: %s", wErr) + log.Error(). + Err(wErr). + Str("thumbprint", tp). + Str("output_file", outputFilePath). + Strs("row", row). + Msg("Error writing row to CSV") } } else { // Cert is not deployed to this store and will need to be added - row := []string{cert, certIDStr, certLookup.IssuedDN, certLookup.IssuerDN, store.ID, store.Type, store.Machine, store.Path, "true", "false", "false", getCurrentTime("")} + log.Info(). + Str("thumbprint", tp). + Str("store_id", store.ID). + Msg("Cert is not deployed to store") + row := []string{ + //todo: this should be a toCSV + tp, + certIDStr, + certLookup.IssuedDN, + certLookup.IssuerDN, + store.ID, + store.Type, + store.Machine, + store.Path, + "true", + "false", + "false", + getCurrentTime(""), + } + log.Trace(). + Str("thumbprint", tp). + Strs("row", row). + Msg("Appending data row") data = append(data, row) + log.Debug(). + Str("thumbprint", tp). + Strs("row", row). + Msg("Writing data row to CSV") wErr := csvWriter.Write(row) if wErr != nil { - fmt.Printf("[ERROR] writing audit file row: %s\n", wErr) - log.Printf("[ERROR] writing audit row: %s", wErr) + log.Error(). + Err(wErr). + Str("thumbprint", tp). + Str("output_file", outputFilePath). + Strs("row", row). + Msg("Error writing row to CSV") } - actions[cert] = append(actions[cert], ROTAction{ - Thumbprint: cert, - CertID: certID, - StoreID: store.ID, - StoreType: store.Type, - StorePath: store.Path, - AddCert: true, - RemoveCert: false, - }) + log.Debug(). + Str("thumbprint", tp). + Msg("Adding 'add' action to actions map") + actions[tp] = append( + actions[tp], ROTAction{ + Thumbprint: tp, + CertID: certID, + StoreID: store.ID, + StoreType: store.Type, + StorePath: store.Path, + AddCert: true, + RemoveCert: false, + Deployed: false, + }, + ) } } } - for _, cert := range removeCerts { - certLookupReq := api.GetCertificateContextArgs{ - IncludeMetadata: boolToPointer(true), - IncludeLocations: boolToPointer(true), - CollectionId: nil, - Thumbprint: cert, - Id: 0, + for tp, cId := range removeCerts { + log.Debug().Str("thumbprint", tp). + Str("cert_id", cId). + Msg("Looking up certificate") + certLookupReq := api.GetCertificateContextArgs{} + if cId != "" { + certIdInt, cErr := strconv.Atoi(cId) + if cErr != nil { + log.Error(). + Err(cErr). + Str("thumbprint", tp). + Msg("Error converting cert ID to integer, skipping") + errs = append(errs, cErr) + continue + } + certLookupReq = api.GetCertificateContextArgs{ + IncludeMetadata: boolToPointer(true), + IncludeLocations: boolToPointer(true), + CollectionId: nil, //todo: add CollectionID support + Thumbprint: "", + Id: certIdInt, + } + } else { + certLookupReq = api.GetCertificateContextArgs{ + IncludeMetadata: boolToPointer(true), + IncludeLocations: boolToPointer(true), + CollectionId: nil, //todo: add CollectionID support + Thumbprint: tp, + Id: 0, //todo: should also allow KFC ID + } } + + log.Debug(). + Str("thumbprint", tp). + Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetCertificateContext")) certLookup, err := kfClient.GetCertificateContext(&certLookupReq) if err != nil { - log.Printf("[ERROR] looking up cert: %s", err) + log.Error(). + Err(err). + Str("thumbprint", tp). + Msg("Error looking up certificate, skipping") + errMsg := fmt.Errorf( + "error recieved from Keyfactor Command when looking up thumbprint '%s':'%w'", + tp, + err, + ) + errs = append(errs, errMsg) continue } certID := certLookup.Id certIDStr := strconv.Itoa(certID) + log.Debug().Str("thumbprint", tp).Msg("Iterating over stores") for _, store := range stores { - if _, ok := store.Thumbprints[cert]; ok { - // Cert is deployed to this store and will need to be removed - row := []string{cert, certIDStr, certLookup.IssuedDN, certLookup.IssuerDN, store.ID, store.Type, store.Machine, store.Path, "false", "true", "true", getCurrentTime("")} + log.Debug().Str("thumbprint", tp).Str("store_id", store.ID).Msg("Checking if cert is deployed to store") + if _, ok := store.Thumbprints[tp]; !ok { + // Cert is already in the store do nothing + log.Info().Str("thumbprint", tp).Str("store_id", store.ID).Msg("Cert is not deployed to store") + row := []string{ + //todo: this should be a toCSV field on whatever object this is + tp, + certIDStr, + certLookup.IssuedDN, + certLookup.IssuerDN, + store.ID, + store.Type, + store.Machine, + store.Path, + "false", // Add to store + "false", // Remove from store + "false", // Is Deployed + getCurrentTime(""), + } + log.Trace().Str("thumbprint", tp).Strs("row", row).Msg("Appending data row") data = append(data, row) + log.Trace().Str("thumbprint", tp).Strs("row", row).Msg("Writing data row to CSV") wErr := csvWriter.Write(row) if wErr != nil { - fmt.Printf("%s", wErr) - log.Printf("[ERROR] writing row to CSV: %s", wErr) + log.Error(). + Err(wErr). + Str("thumbprint", tp). + Str("output_file", outputFilePath). + Strs("row", row). + Msg("Error writing row to CSV") } - actions[cert] = append(actions[cert], ROTAction{ - Thumbprint: cert, - CertID: certID, - StoreID: store.ID, - StoreType: store.Type, - StorePath: store.Path, - AddCert: false, - RemoveCert: true, - }) } else { - // Cert is not deployed to this store do nothing - row := []string{cert, certIDStr, certLookup.IssuedDN, certLookup.IssuerDN, store.ID, store.Type, store.Machine, store.Path, "false", "false", "false", getCurrentTime("")} + // Cert is deployed to this store and will need to be removed + log.Info(). + Str("thumbprint", tp). + Str("store_id", store.ID). + Msg("Cert is deployed to store") + row := []string{ + //todo: this should be a toCSV + tp, + certIDStr, + certLookup.IssuedDN, + certLookup.IssuerDN, + store.ID, + store.Type, + store.Machine, + store.Path, + "false", // Add to store + "true", // Remove from store + "true", // Is Deployed + getCurrentTime(""), + } + log.Trace(). + Str("thumbprint", tp). + Strs("row", row). + Msg("Appending data row") data = append(data, row) + log.Debug(). + Str("thumbprint", tp). + Strs("row", row). + Msg("Writing data row to CSV") wErr := csvWriter.Write(row) if wErr != nil { - fmt.Printf("%s", wErr) - log.Printf("[ERROR] writing row to CSV: %s", wErr) + log.Error(). + Err(wErr). + Str("thumbprint", tp). + Str("output_file", outputFilePath). + Strs("row", row). + Msg("Error writing row to CSV") } + log.Debug(). + Str("thumbprint", tp). + Msg("Adding 'remove' action to actions map") + actions[tp] = append( + actions[tp], ROTAction{ + Thumbprint: tp, + StoreAlias: "", //TODO get this value + CertID: certID, + StoreID: store.ID, + StoreType: store.Type, + StorePath: store.Path, + AddCert: false, + RemoveCert: true, + Deployed: true, + }, + ) } } } + log.Trace(). + Str("output_file", outputFilePath). + Msg("Flushing CSV writer") csvWriter.Flush() + log.Trace(). + Str("output_file", outputFilePath). + Msg("Closing CSV file") ioErr := csvFile.Close() if ioErr != nil { - fmt.Println(ioErr) - log.Printf("[ERROR] closing audit file: %s", ioErr) + log.Error(). + Err(ioErr). + Str("output_file", outputFilePath). + Msg("Error closing CSV file") + } + log.Info(). + Str("output_file", outputFilePath). + Msg("Audit report written to disk successfully") + fmt.Printf("Audit report written to %s\n", outputFilePath) //todo: remove or propagate message to CLI + fmt.Printf( + "Please review the report and run `kfutil stores rot reconcile --import-csv --input"+ + "-file %s` apply the changes\n", outputFilePath, + ) + + if len(errs) > 0 { + errStr := mergeErrsToString(&errs, false) + log.Trace().Str("output_file", outputFilePath).Str( + "errors", + errStr, + ).Msg("The following errors occurred while generating audit report") + return data, actions, fmt.Errorf("the following errors occurred while generating audit report:\r\n%s", errStr) } - fmt.Printf("Audit report written to %s\n", outpath) + log.Debug().Msg(fmt.Sprintf(DebugFuncExit, "generateAuditReport")) return data, actions, nil } func reconcileRoots(actions map[string][]ROTAction, kfClient *api.Client, reportFile string, dryRun bool) error { - log.Printf("[DEBUG] Reconciling roots") + log.Debug().Msg(fmt.Sprintf(DebugFuncEnter, "reconcileRoots")) if len(actions) == 0 { - log.Printf("[INFO] No actions to take, roots are up-to-date.") + log.Info().Msg("No actions to reconcile detected, root of trust stores are up-to-date.") return nil } + log.Info().Msg("Reconciling root of trust stores") + rFileName := fmt.Sprintf("%s_reconciled.csv", strings.Split(reportFile, ".csv")[0]) + log.Debug(). + Str("report_file", reportFile). + Str("reconciled_file", rFileName). + Msg("Creating reconciled report file") csvFile, fErr := os.Create(rFileName) if fErr != nil { - fmt.Printf("[ERROR] creating reconciled report file: %s", fErr) + log.Error(). + Err(fErr). + Str("reconciled_file", rFileName). + Msg("Error creating reconciled report file") + return fErr } + log.Trace().Str("reconciled_file", rFileName).Msg("Creating CSV writer") csvWriter := csv.NewWriter(csvFile) + + log.Debug().Str("reconciled_file", rFileName).Strs("csv_header", ReconciledAuditHeader).Msg("Writing header to CSV") cErr := csvWriter.Write(ReconciledAuditHeader) if cErr != nil { - fmt.Printf("%s", cErr) - log.Fatalf("[ERROR] writing audit header: %s", cErr) + log.Error().Err(cErr).Str("reconciled_file", rFileName).Msg("Error writing header to CSV") + return cErr } + log.Info().Str("report_file", reportFile).Msg("Processing reconciliation actions") + var errs []error for thumbprint, action := range actions { - for _, a := range action { if a.AddCert { - log.Printf("[INFO] Adding cert %s to store %s(%s)", thumbprint, a.StoreID, a.StorePath) if !dryRun { - cStore := api.CertificateStore{ + log.Info().Str("thumbprint", thumbprint).Str("store_id", a.StoreID).Str( + "store_path", + a.StorePath, + ).Msg("Attempting to add cert to store") + log.Debug().Str("thumbprint", thumbprint).Str( + "store_id", + a.StoreID, + ).Msg("Creating orchestrator 'add' job request") + + log.Trace().Str("thumbprint", thumbprint).Str( + "store_id", + a.StoreID, + ).Msg("Creating certificate store object") + apiStore := api.CertificateStore{ CertificateStoreId: a.StoreID, Overwrite: true, } + + log.Trace().Str("thumbprint", thumbprint).Str( + "store_id", + a.StoreID, + ).Msg("Creating certificate store array") var stores []api.CertificateStore - stores = append(stores, cStore) + log.Trace().Str("thumbprint", thumbprint).Str( + "store_id", + a.StoreID, + ).Msg("Appending certificate store to array") + stores = append(stores, apiStore) + + log.Trace().Str("thumbprint", thumbprint).Str( + "store_id", + a.StoreID, + ).Msg("Creating inventory 'immediate' schedule") schedule := &api.InventorySchedule{ Immediate: boolToPointer(true), } + + log.Trace().Str("thumbprint", thumbprint).Str( + "store_id", + a.StoreID, + ).Msg("Creating add certificate request") addReq := api.AddCertificateToStore{ CertificateId: a.CertID, CertificateStores: &stores, InventorySchedule: schedule, } - log.Printf("[DEBUG] Adding cert %s to store %s", thumbprint, a.StoreID) - log.Printf("[TRACE] Add request: %+v", addReq) - addReqJSON, _ := json.Marshal(addReq) - log.Printf("[TRACE] Add request JSON: %s", addReqJSON) + + log.Trace().Str("thumbprint", thumbprint).Interface( + "add_request", + addReq, + ).Msg("Converting add request to JSON") + addReqJSON, jErr := json.Marshal(addReq) + if jErr != nil { + log.Error().Err(jErr).Str("thumbprint", thumbprint).Msg("Error converting add request to JSON") + errMsg := fmt.Errorf( + "error converting add request for '%s' in stores '%v' to JSON: %s", + thumbprint, stores, jErr, + ) + errs = append(errs, errMsg) + continue + } + log.Debug().Str("thumbprint", thumbprint).Str( + "add_request", + string(addReqJSON), + ).Msg(fmt.Sprintf(DebugFuncCall, "kfClient.AddCertificateToStores")) _, err := kfClient.AddCertificateToStores(&addReq) if err != nil { - fmt.Printf("[ERROR] adding cert %s (%d) to store %s (%s): %s\n", a.Thumbprint, a.CertID, a.StoreID, a.StorePath, err) + fmt.Printf( + "ERROR adding cert %s(%d) to store %s: %s\n", + a.Thumbprint, + a.CertID, + a.StoreID, + err, + ) + log.Error().Err(err).Str("thumbprint", thumbprint).Str( + "store_id", + a.StoreID, + ).Str("store_path", a.StorePath).Msg("unable to add cert to store") continue } } else { - log.Printf("[INFO] DRY RUN: Would have added cert %s from store %s", thumbprint, a.StoreID) + log.Info().Str("thumbprint", thumbprint).Str( + "store_id", + a.StoreID, + ).Msg("DRY RUN: Would have added cert to store") } } else if a.RemoveCert { if !dryRun { - log.Printf("[INFO] Removing cert from store %s", a.StoreID) + log.Info().Str("thumbprint", thumbprint).Str( + "store_id", + a.StoreID, + ).Msg("Attempting to remove cert from store") cStore := api.CertificateStore{ CertificateStoreId: a.StoreID, - Alias: a.Thumbprint, + Alias: a.Thumbprint, //todo: support non-thumbprint aliases } + log.Trace().Interface("store_object", cStore).Msg("Converting store to slice of single store") var stores []api.CertificateStore stores = append(stores, cStore) + + log.Trace().Str("thumbprint", thumbprint).Str( + "store_id", + a.StoreID, + ).Msg("Creating inventory 'immediate' schedule") schedule := &api.InventorySchedule{ Immediate: boolToPointer(true), } + + log.Trace().Str("thumbprint", thumbprint).Str( + "store_id", + a.StoreID, + ).Msg("Creating remove certificate request") removeReq := api.RemoveCertificateFromStore{ CertificateId: a.CertID, CertificateStores: &stores, InventorySchedule: schedule, } + log.Debug().Str("thumbprint", thumbprint).Interface( + "remove_request", + removeReq, + ).Msg(fmt.Sprintf(DebugFuncCall, "kfClient.RemoveCertificateFromStores")) _, err := kfClient.RemoveCertificateFromStores(&removeReq) if err != nil { - fmt.Printf("[ERROR] removing cert %s (ID: %d) from store %s (%s): %s\n", a.Thumbprint, a.CertID, a.StoreID, a.StorePath, err) + log.Error().Err(err).Str("thumbprint", thumbprint).Str( + "store_id", + a.StoreID, + ).Str("store_path", a.StorePath).Msg("unable to remove cert from store") + fmt.Printf( + "ERROR removing cert %s(%d) from store %s: %s\n", + a.Thumbprint, + a.CertID, + a.StoreID, + err, + ) } } else { - fmt.Printf("DRY RUN: Would have removed cert %s from store %s\n", thumbprint, a.StoreID) - log.Printf("[INFO] DRY RUN: Would have removed cert %s from store %s", thumbprint, a.StoreID) + fmt.Printf( + "DRY RUN: Would have removed cert %s from store %s\n", thumbprint, + a.StoreID, + ) //todo: propagate back to CLI + log.Info().Str("thumbprint", thumbprint).Str( + "store_id", + a.StoreID, + ).Msg("DRY RUN: Would have removed cert from store") + } + } + } + } + log.Info().Str("reconciled_file", rFileName).Msg("Reconciliation actions scheduled on Keyfactor Command") + if len(errs) > 0 { + errStr := mergeErrsToString(&errs, false) + log.Trace().Str("reconciled_file", rFileName).Str( + "errors", + errStr, + ).Msg("The following errors occurred while reconciling actions") + return fmt.Errorf("The following errors occurred while reconciling actions:\r\n%s", errStr) + } + log.Debug().Msg(fmt.Sprintf(DebugFuncExit, "reconcileRoots")) + return nil +} + +func readCertsFile(certsFilePath string, kfclient *api.Client) (map[string]string, error) { + log.Debug().Msg(fmt.Sprintf(DebugFuncEnter, "readCertsFile")) + // Read in the cert CSV + log.Info().Str("certs_file", certsFilePath).Msg("Reading in certs file") + csvFile, ioErr := os.Open(certsFilePath) + if ioErr != nil { + log.Error().Err(ioErr).Str("certs_file", certsFilePath).Msg("Error reading in certs file") + return nil, ioErr + } + + log.Trace().Str("certs_file", certsFilePath).Msg("Creating CSV reader") + reader := csv.NewReader(bufio.NewReader(csvFile)) + + certEntries, rErr := reader.ReadAll() + if rErr != nil { + log.Error().Err(rErr).Str("certs_file", certsFilePath).Msg("Error reading in certs file") + return nil, rErr + } + + //validate header + if len(certEntries) == 0 { + log.Error().Str("certs_file", certsFilePath).Msg("Empty CSV file") + return nil, errors.New("empty CSV file") + } + + log.Debug().Str("certs_file", certsFilePath).Msg("Parsing CSV data") + var certs = make(map[string]string) + log.Trace().Str("certs_file", certsFilePath).Msg("Iterating over CSV data") + headerMap := make(map[string]int) + for i, entry := range certEntries { + if i == 0 { + for j, h := range entry { + headerMap[h] = j + } + continue + } + + log.Trace().Strs("entry", entry).Msg("Processing row") + switch entry[0] { + case "CertID", "thumbprint", "id", "CertId", "Thumbprint": //todo: is there a way to do this with a var? + log.Trace().Strs("entry", entry).Msg("Skipping header row") + continue // Skip header + } + tp := entry[headerMap["Thumbprint"]] + if tp == "" { + log.Warn().Strs("entry", entry).Msg("Thumbprint is empty, skipping") + continue + } + + cId := entry[headerMap["CertID"]] + if cId == "" { + log.Warn().Strs("entry", entry).Msg("CertID is empty, skipping") + continue + } + + log.Trace().Strs("entry", entry).Msg("Adding thumbprint to map") + certs[tp] = cId + log.Trace().Interface("certs", certs).Msg("Cert map") + } + log.Info().Str("certs_file", certsFilePath).Msg("Certs file read successfully") + log.Debug().Msg(fmt.Sprintf(DebugFuncExit, "readCertsFile")) + log.Trace().Interface("certs", certs).Msg("Returning certs map") + return certs, nil +} + +func isRootStore( + st *api.GetCertificateStoreResponse, + invs *[]api.CertStoreInventory, + minCerts int, + maxKeys int, + maxLeaf int, +) bool { + log.Debug(). + Int("min_certs", minCerts). + Int("max_keys", maxKeys). + Int("max_leaf", maxLeaf). + Msg(fmt.Sprintf(DebugFuncEnter, "isRootStore")) + leafCount := 0 + keyCount := 0 + certCount := 0 + + log.Info(). + Str("store_id", st.Id). + Msg("Checking if store is a root store") + + if invs == nil || len(*invs) == 0 { + log.Warn().Str("store_id", st.Id).Msg("No certificates found in inventory for store") + //log.Info().Str("store_id", st.Id).Msg("Empty store is not a root store") + //return false + invs = &[]api.CertStoreInventory{} + } + + log.Debug().Str("store_id", st.Id).Msg("Iterating over inventory") + for _, inv := range *invs { + log.Trace().Str("store_id", st.Id).Interface("inv", inv).Msg("Processing inventory") + certCount += len(inv.Certificates) + + if len(inv.Certificates) == 0 { + log.Warn().Str("store_id", st.Id).Msg("No certificates found in inventory for store") + log.Info().Str("store_id", st.Id).Msg("Empty store is not a root store") + continue + } + + log.Debug().Str("store_id", st.Id).Msg("Iterating over certificates in inventory") + for _, cert := range inv.Certificates { + log.Debug().Str("store_id", st.Id).Str("cert_thumbprint", cert.Thumbprint).Msg("Checking if cert is a leaf") + if cert.IssuedDN != cert.IssuerDN { + log.Debug().Str("store_id", st.Id).Str("cert_thumbprint", cert.Thumbprint).Msg("Cert is a leaf") + leafCount++ + } + + log.Debug().Str("store_id", st.Id).Str( + "cert_thumbprint", + cert.Thumbprint, + ).Msg("Checking if cert has a private key") + //if inv.Parameters["PrivateKeyEntry"] == "Yes" { + // log.Debug().Str("store_id", st.Id).Str( + // "cert_thumbprint", + // cert.Thumbprint, + // ).Msg("Cert has a private key") + // keyCount++ + //} + } + } + + log.Info().Str("store_id", st.Id). + Int("cert_count", certCount). + Int("min_certs", minCerts). + Msg("Checking if store meets minimum cert count") + if certCount < minCerts && minCerts >= 0 { + log.Info().Str("store_id", st.Id). + Int("cert_count", certCount). + Int("min_certs", minCerts). + Msg("Store does not meet minimum cert count to be considered a root of trust") + log.Debug().Msg(fmt.Sprintf(DebugFuncExit, "isRootStore")) + return false + } + if leafCount > maxLeaf && maxLeaf >= 0 { + log.Info().Str("store_id", st.Id). + Int("leaf_count", leafCount). + Int("max_leaves", maxLeaf). + Msg("Store has too many leaf certs to be considered a root of trust") + log.Debug().Msg(fmt.Sprintf(DebugFuncExit, "isRootStore")) + return false + } + + if keyCount > maxKeys && maxKeys >= 0 { + log.Info().Str("store_id", st.Id). + Int("key_count", keyCount). + Int("max_keys", maxKeys). + Msg("Store has too many private keys to be considered a root of trust") + log.Debug().Msg(fmt.Sprintf(DebugFuncExit, "isRootStore")) + return false + } + + log.Info().Str("store_id", st.Id). + Int("cert_count", certCount). + Int("min_certs", minCerts). + Int("leaf_count", leafCount). + Int("max_leaves", maxLeaf). + Int("key_count", keyCount). + Int("max_keys", maxKeys). + Msg("Store meets criteria to be considered a root of trust") + log.Debug().Msg(fmt.Sprintf(DebugFuncExit, "isRootStore")) + return true +} + +func findTrustStores( + criteria *TrustStoreCriteria, + containerName string, + c *api.Client, +) (*KFCStores, error) { + log.Debug().Msg(fmt.Sprintf(DebugFuncEnter, "findTrustStores")) + trustStores := KFCStores{ + Stores: map[string]KFCStore{}, + } + + log.Info().Msg("Finding root of trust stList") + log.Debug().Msg("Iterating over stList") + + // fetch list of stList from Keyfactor + params := make(map[string]interface{}) + if containerName != "" { + //check if name is an int + _, err := strconv.Atoi(containerName) + if err == nil { + params["ContainerId"] = containerName + } else { + params["ContainerName"] = containerName + } + } + log.Debug(). + Str("container", containerName). + Interface("params", params). + Msg(fmt.Sprintf(DebugFuncCall, "c.ListCertificateStores")) + stList, stErr := c.ListCertificateStores(¶ms) + if stErr != nil { + log.Error().Err(stErr).Msg("Error fetching stList from Keyfactor Command") + return nil, stErr + } else if stList == nil { + log.Error(). + Interface("params", params). + Msg("No stList returned from Keyfactor Command") + return nil, fmt.Errorf("no stList returned from Keyfactor Command") + } + + log.Debug().Str("stList", fmt.Sprintf("%v", stList)).Msg("Stores fetched successfully") + + var stLkErrs []string + for _, st := range *stList { + log.Debug().Str("store_id", st.Id). + Str("store_id", st.Id). + Str("store_path", st.StorePath). + Str("client_machine", st.ClientMachine). + Msg(fmt.Sprintf(DebugFuncCall, "GetCertStoreInventory")) + inventory, invErr := c.GetCertStoreInventory(st.Id) + if invErr != nil { + log.Error().Err(invErr).Str("store_id", st.Id).Msg("Error getting cert store inventory") + errLine := fmt.Sprintf("%s,%s,%s,%s\n", st.Id, st.StorePath, st.ClientMachine, st.CertStoreType) + stLkErrs = append(stLkErrs, errLine) + continue + } else if inventory == nil { + log.Error().Str( + "store_id", + st.Id, + ).Msg("No inventory response returned for store from Keyfactor Command") + errLine := fmt.Sprintf("%s,%s,%s,%s\n", st.Id, st.StorePath, st.ClientMachine, st.CertStoreType) + stLkErrs = append(stLkErrs, errLine) + continue + } + + log.Debug().Str("store_id", st.Id). + Int("min_certs", criteria.MinCerts). + Int("max_keys", criteria.MaxKeys). + Int("max_leaf", criteria.MaxLeaf). + Str("store_id", st.Id). + Str("store_path", st.StorePath). + Str("client_machine", st.ClientMachine). + Msg(fmt.Sprintf(DebugFuncCall, "isRootStore")) + if isRootStore(&st, inventory, criteria.MinCerts, criteria.MaxKeys, criteria.MaxLeaf) { + log.Info(). + Str("store_id", st.Id). + Str("store_path", st.StorePath). + Str("client_machine", st.ClientMachine). + Msg("certificate store is considered a 'trust' store") + trstSt := KFCStore{ + ApiResponse: st, + Inventory: *inventory, + } + trustStores.Stores[st.Id] = trstSt + continue + } + log.Info(). + Int("min_certs", criteria.MinCerts). + Int("max_keys", criteria.MaxKeys). + Int("max_leaf", criteria.MaxLeaf). + Str("store_id", st.Id). + Str("store_path", st.StorePath). + Str("client_machine", st.ClientMachine). + Msg("certificate store is NOT considered a 'trust' store") + } + + if len(stLkErrs) > 0 { + log.Error(). + Strs("stList", stLkErrs). + Msg("Error looking up inventory for stList") + log.Debug().Msg(fmt.Sprintf(DebugFuncExit, "findTrustStores")) + return &trustStores, fmt.Errorf("error looking up inventory for stList: %s", strings.Join(stLkErrs, ",")) + } + + log.Debug().Msg(fmt.Sprintf(DebugFuncExit, "findTrustStores")) + return &trustStores, nil +} + +func validateStoresInput(storesFile *string, noPrompt *bool, kfClient *api.Client) (string, error) { + if noPrompt == nil { + noPrompt = boolToPointer(false) + } + + if storesFile == nil || *storesFile == "" { + if *noPrompt { + return "", fmt.Errorf("stores file is required, use flag `--stores` to specify 1 or more file paths") + } + apiOrFile := promptSelectRotStores("certificate stores") + switch apiOrFile { + case "Manual Select": + selectedStores := promptSelectStores(kfClient) + if len(selectedStores) == 0 { + return "", errors.New("no certificate stores selected, unable to continue") + } + //create stores file + storesFile = stringToPointer(fmt.Sprintf("%s", DefaultROTAuditStoresOutfilePath)) + // create file + f, ioErr := os.Create(*storesFile) + if ioErr != nil { + log.Error().Err(ioErr).Str("stores_file", *storesFile).Msg("Error creating stores file") + return "", ioErr + } + defer f.Close() + // create CSV writer + log.Debug().Str("stores_file", *storesFile).Msg("Creating CSV writer") + writer := csv.NewWriter(f) + defer writer.Flush() + // write header + log.Debug().Str("stores_file", *storesFile).Msg("Writing header to stores file") + wErr := writer.Write(StoreHeader) + if wErr != nil { + log.Error().Err(wErr).Str("stores_file", *storesFile).Msg("Error writing header to stores file") + return "", wErr + } + // write selected stores + for _, store := range selectedStores { + log.Debug().Str("store_id", store).Msg("Adding store to stores file") + //parse ID from selection `: ` + storeId := strings.Split(store, ":")[1] + //remove () and white spaces from storeId + storeId = strings.Trim(strings.Trim(strings.Trim(storeId, " "), "("), ")") + + storeInstance := ROTStore{ + StoreID: storeId, + StoreType: "", + StoreMachine: "", + StorePath: "", + ContainerId: "", + ContainerName: "", + LastQueried: "", + } + storeLine := storeInstance.toCSV() + + wErr = writer.Write(strings.Split(storeLine, ",")) + if wErr != nil { + log.Error().Err(wErr).Str( + "stores_file", + *storesFile, + ).Msg("Error writing store to stores file") + continue + } + } + writer.Flush() + f.Close() + return *storesFile, nil + case "File": + return promptForFilePath("Input a file path for the CSV file containing stores to audit."), nil + case "Discover": + promptForCriteria() + trusts, sErr := findTrustStores(&trustCriteria, "", kfClient) + if sErr != nil { + return "", sErr + } else if trusts == nil || trusts.Stores == nil || len(trusts.Stores) == 0 { + return "", fmt.Errorf("no trust stores found using the following criteria:\n%s", trustCriteria.String()) + } + storesFile = stringToPointer(fmt.Sprintf("%s", DefaultROTAuditStoresOutfilePath)) + // create file + f, ioErr := os.Create(*storesFile) + if ioErr != nil { + log.Error().Err(ioErr).Str("stores_file", *storesFile).Msg("Error creating stores file") + return "", ioErr + } + defer f.Close() + // create CSV writer + log.Debug().Str("stores_file", *storesFile).Msg("Creating CSV writer") + writer := csv.NewWriter(f) + defer writer.Flush() + // write header + log.Debug().Str("stores_file", *storesFile).Msg("Writing header to stores file") + wErr := writer.Write(StoreHeader) + if wErr != nil { + log.Error().Err(wErr).Str("stores_file", *storesFile).Msg("Error writing header to stores file") + return "", wErr + } + for _, store := range trusts.Stores { + storeInstance := ROTStore{ + StoreID: store.ApiResponse.Id, + StoreType: fmt.Sprintf("%d", store.ApiResponse.CertStoreType), + StoreMachine: store.ApiResponse.ClientMachine, + StorePath: store.ApiResponse.StorePath, + ContainerId: fmt.Sprintf("%d", store.ApiResponse.ContainerId), + ContainerName: store.ApiResponse.ContainerName, + LastQueried: getCurrentTime(""), + } + storeLine := storeInstance.toCSV() + + wErr = writer.Write(strings.Split(storeLine, ",")) + if wErr != nil { + log.Error().Err(wErr).Str( + "stores_file", + *storesFile, + ).Msg("Error writing store to stores file") + continue + } + } + return f.Name(), nil + default: + return "", errors.New("invalid selection") + } + } + return *storesFile, nil +} + +func validateCertsInput(addRootsFile string, removeRootsFile string, client *api.Client) ( + string, + string, + error, +) { + log.Debug().Str("add_certs_file", addRootsFile). + Str("remove_certs_file", removeRootsFile). + Bool("no_prompt", noPrompt). + Msg(fmt.Sprintf(DebugFuncEnter, "validateCertsInput")) + + if addRootsFile == "" && removeRootsFile == "" && noPrompt { + //cmd.SilenceUsage = false //todo: is this necessary? + return addRootsFile, removeRootsFile, InvalidROTCertsInputErr + } + + if addRootsFile == "" || removeRootsFile == "" { + if addRootsFile == "" && !noPrompt { + //prmpt := "Would you like to include a 'certs to add' CSV file?" + prmpt := "Provide certificates to add to and/or that should be present in selected stores?" + provideAddFile := promptYesNo(prmpt) + if provideAddFile { + addSrcType := promptSelectFromAPIorFile("certificates") + switch addSrcType { + case "API": + selectedCerts := promptSelectCerts(client) + if len(selectedCerts) == 0 { + return "", "", InvalidROTCertsInputErr + } + //create stores file + addRootsFile = fmt.Sprintf("%s", DefaultROTAuditAddCertsOutfilePath) + // create file + f, ioErr := os.Create(addRootsFile) + if ioErr != nil { + log.Error().Err(ioErr).Str( + "add_certs_file", + addRootsFile, + ).Msg("Error creating certs to add file") + return addRootsFile, removeRootsFile, ioErr + } + defer f.Close() + // create CSV writer + log.Debug().Str("add_certs_file", addRootsFile).Msg("Creating CSV writer") + writer := csv.NewWriter(f) + defer writer.Flush() + // write header + log.Debug().Str("add_certs_file", addRootsFile).Msg("Writing header to certs to add file") + wErr := writer.Write(CertHeader) + if wErr != nil { + log.Error().Err(wErr).Str( + "stores_file", + addRootsFile, + ).Msg("Error writing header to stores file") + return addRootsFile, removeRootsFile, wErr + } + // write selected stores + for _, c := range selectedCerts { + log.Debug().Str("cert_id", c).Msg("Adding cert to certs file") + + //parse certID, cn and thumbprint from selection `: () - ` + + //parse id from selection `: () ` + certId := strings.Split(c, ":")[0] + //remove () and white spaces from storeId + certId = strings.Trim(certId, " ") + certIdInt, cIdErr := strconv.Atoi(certId) + if cIdErr != nil { + log.Error().Err(cIdErr).Str("cert_id", certId).Msg("Error converting cert ID to int") + certIdInt = -1 + } + + //parse the cn from the selection `: () ` + cn := strings.Split(c, "(")[0] + cn = strings.Split(cn, ":")[1] + cn = strings.Trim(cn, " ") + + //parse thumbprint from selection `: () ` + thumbprint := strings.Split(c, "(")[1] + thumbprint = strings.Split(thumbprint, ")")[0] + thumbprint = strings.Trim(strings.Trim(thumbprint, " "), ")") + + certInstance := ROTCert{ + ID: certIdInt, + ThumbPrint: thumbprint, + CN: cn, + SANs: []string{}, + Alias: "", + Locations: []api.CertificateLocations{}, + } + certLine := certInstance.toCSV() + + wErr = writer.Write(strings.Split(certLine, ",")) + if wErr != nil { + log.Error().Err(wErr).Str( + "add_certs_file", + addRootsFile, + ).Msg("Error writing store to stores file") + continue + } + } + writer.Flush() + f.Close() + default: + addRootsFile = promptForFilePath("Input a file path for the 'certs to add' CSV.") + } + } + } + if removeRootsFile == "" && !noPrompt { + prmpt := "Provide certificates to remove from and/or that should NOT be present in selected stores?" + provideRemoveFile := promptYesNo(prmpt) + if provideRemoveFile { + //removeRootsFile = promptForFilePath("Input a file path for the 'certs to remove' CSV. ") + remSrcType := promptSelectFromAPIorFile("certificates") + switch remSrcType { + case "API": + selectedCerts := promptSelectCerts(client) + if len(selectedCerts) == 0 { + return "", "", InvalidROTCertsInputErr + } + //create stores file + removeRootsFile = fmt.Sprintf("%s", DefaultROTAuditRemoveCertsOutfilePath) + // create file + f, ioErr := os.Create(removeRootsFile) + if ioErr != nil { + log.Error().Err(ioErr).Str( + "remove_certs_file", + removeRootsFile, + ).Msg("Error creating certs to remove file") + return addRootsFile, removeRootsFile, ioErr + } + defer f.Close() + // create CSV writer + log.Debug().Str("remove_certs_file", removeRootsFile).Msg("Creating CSV writer") + writer := csv.NewWriter(f) + defer writer.Flush() + // write header + log.Debug().Str("remove_certs_file", removeRootsFile).Msg("Writing header to certs to remove file") + wErr := writer.Write(CertHeader) + if wErr != nil { + log.Error().Err(wErr).Str( + "stores_file", + removeRootsFile, + ).Msg("Error writing header to stores file") + return addRootsFile, removeRootsFile, wErr + } + // write selected stores + for _, c := range selectedCerts { + log.Debug().Str("cert_id", c).Msg("Adding cert to certs file") + + //parse certID, cn and thumbprint from selection `: () - ` + + //parse id from selection `: () ` + certId := strings.Split(c, ":")[0] + //remove () and white spaces from storeId + certId = strings.Trim(certId, " ") + certIdInt, cIdErr := strconv.Atoi(certId) + if cIdErr != nil { + log.Error().Err(cIdErr).Str("cert_id", certId).Msg("Error converting cert ID to int") + certIdInt = -1 + } + + //parse the cn from the selection `: () ` + cn := strings.Split(c, "(")[0] + cn = strings.Split(cn, ":")[1] + cn = strings.Trim(cn, " ") + + //parse thumbprint from selection `: () ` + thumbprint := strings.Split(c, "(")[1] + thumbprint = strings.Split(thumbprint, ")")[0] + thumbprint = strings.Trim(strings.Trim(thumbprint, " "), ")") + + certInstance := ROTCert{ + ID: certIdInt, + ThumbPrint: thumbprint, + CN: cn, + SANs: []string{}, + Alias: "", + Locations: []api.CertificateLocations{}, + } + certLine := certInstance.toCSV() + + wErr = writer.Write(strings.Split(certLine, ",")) + if wErr != nil { + log.Error().Err(wErr).Str( + "remove_certs_file", + removeRootsFile, + ).Msg("Error writing store to stores file") + continue + } + } + writer.Flush() + f.Close() + default: + removeRootsFile = promptForFilePath("Input a file path for the 'certs to remove' CSV.") } } } + if addRootsFile == "" && removeRootsFile == "" { + return "", "", InvalidROTCertsInputErr + } + } + return addRootsFile, removeRootsFile, nil + +} + +func processFromStoresAndCertFiles( + storesFile string, + addRootsFile string, + removeRootsFile string, + reportFile string, + outputFilePath string, + minCerts int, + maxLeaves int, + maxKeys int, + kfClient *api.Client, + dryRun bool, +) error { + // Read in the stores CSV + log.Debug().Str("stores_file", storesFile).Msg("Reading in stores file") + csvFile, _ := os.Open(storesFile) + reader := csv.NewReader(bufio.NewReader(csvFile)) + storeEntries, _ := reader.ReadAll() + var stores = make(map[string]StoreCSVEntry) + var lookupFailures []string + var errs []error + for i, row := range storeEntries { + if len(row) == 0 { + log.Warn(). + Str("stores_file", storesFile). + Int("row", i).Msg("Skipping empty row") + continue + } else if row[0] == "StoreID" || row[0] == "StoreId" || i == 0 { + log.Trace().Strs("row", row).Msg("Skipping header row") + continue // Skip header + } + + log.Debug().Strs("row", row). + Str("store_id", row[0]). + Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetCertificateStoreByID")) + apiResp, err := kfClient.GetCertificateStoreByID(row[0]) + if err != nil { + errs = append(errs, err) + log.Error().Err(err).Str("store_id", row[0]).Msg("failed to retrieve store from Keyfactor Command") + lookupFailures = append(lookupFailures, row[0]) + continue + } + + log.Debug().Str("store_id", row[0]).Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetCertStoreInventoryV1")) + inventory, invErr := kfClient.GetCertStoreInventory(row[0]) + if invErr != nil { + errs = append(errs, invErr) + log.Error().Err(invErr).Str( + "store_id", + row[0], + ).Msg("failed to retrieve inventory for certificate store from Keyfactor Command") + continue + } + + if !isRootStore(apiResp, inventory, minCerts, maxLeaves, maxKeys) { + log.Error().Str( + "store_id", + row[0], + ).Msg("Store is not considered a root of trust store and will be excluded.") + errs = append(errs, fmt.Errorf("store '%s' is not considered a root of trust store", row[0])) + continue + } + + log.Info().Str("store_id", row[0]).Msg("Store is considered a root of trust store") + log.Trace().Str("store_id", row[0]).Msg("Creating StoreCSVEntry object") + stores[row[0]] = StoreCSVEntry{ + ID: row[0], + Type: row[1], + Machine: row[2], + Path: row[3], + Thumbprints: make(map[string]bool), + Serials: make(map[string]bool), + Ids: make(map[int]bool), + } + + log.Debug().Str("store_id", row[0]).Msg( + "Iterating over inventory for thumbprints, " + + "serial numbers and cert IDs", + ) + for _, cert := range *inventory { + log.Trace().Str("store_id", row[0]).Interface("cert", cert).Msg("Processing inventory") + thumb := cert.Thumbprints + for t, v := range thumb { + log.Trace().Str("store_id", row[0]). + Str("value", v). + Int("thumbprint", t).Msg("Adding cert thumbprint to store object") + //stores[row[0]].Thumbprints[t] = v + } + for t, v := range cert.Serials { + log.Trace().Str("store_id", row[0]). + Str("value", v). + Int("serial", t).Msg("Adding cert serial to store object") + //stores[row[0]].Serials[t] = v + } + for t, v := range cert.Ids { + log.Trace().Str("store_id", row[0]). + Int("value", v). + Int("cert_id", t).Msg("Adding cert ID to store object") + //stores[row[0]].Ids[t] = v + } + } + } + if len(lookupFailures) > 0 { + errMsg := fmt.Errorf("The following stores were not found:\r\n%s", strings.Join(lookupFailures, ",\r\n")) + fmt.Printf(errMsg.Error()) + log.Error().Err(errMsg). + Strs("lookup_failures", lookupFailures). + Msg("The following stores could not be found") + if len(errs) > 0 { + apiErrs := mergeErrsToString(&errs, false) + errMsg = fmt.Errorf("%s\r\n%s", errMsg, apiErrs) + } + return errMsg + } + if len(stores) == 0 { + errMsg := fmt.Errorf("no root of trust stores found that meet the defined criteria") + log.Error(). + Err(errMsg). + Int("min_certs", minCerts). + Int("max_leaves", maxLeaves). + Int("max_keys", maxKeys).Send() + + if len(errs) > 0 { + apiErrs := mergeErrsToString(&errs, false) + errMsg = fmt.Errorf("%s\r\n%s", errMsg, apiErrs) + } + return errMsg + } + // Read in the add addCerts CSV + var certsToAdd = make(map[string]string) + var rErr error + if addRootsFile == "" { + log.Info().Msg("No add certs file specified, add operations will not be performed") + } else { + log.Info().Str("add_certs_file", addRootsFile).Msg("Reading certs to add file") + log.Debug().Str("add_certs_file", addRootsFile).Msg(fmt.Sprintf(DebugFuncCall, "readCertsFile")) + certsToAdd, rErr = readCertsFile(addRootsFile, kfClient) + if rErr != nil { + log.Error().Err(rErr).Str("add_certs_file", addRootsFile).Msg("Error reading certs to add file") + if len(errs) > 0 { + apiErrs := mergeErrsToString(&errs, false) + rErr = fmt.Errorf("%s\r\n%s", rErr, apiErrs) + } + return rErr + } + log.Debug().Str("add_certs_file", addRootsFile).Msg("finished reading certs to add file") + } + + // Read in the remove removeCerts CSV + var certsToRemove = make(map[string]string) + if removeRootsFile == "" { + log.Info().Msg("No remove certs file specified, remove operations will not be performed") + } else { + log.Info().Str("remove_certs_file", removeRootsFile).Msg("Reading certs to remove file") + log.Debug().Str("remove_certs_file", removeRootsFile).Msg(fmt.Sprintf(DebugFuncCall, "readCertsFile")) + certsToRemove, rErr = readCertsFile(removeRootsFile, kfClient) + if rErr != nil { + log.Error().Err(rErr).Str("remove_certs_file", removeRootsFile).Msg("Error reading certs to remove file") + if len(errs) > 0 { + apiErrs := mergeErrsToString(&errs, false) + rErr = fmt.Errorf("%s\r\n%s", rErr, apiErrs) + } + return rErr + } + } + + if len(certsToAdd) == 0 && len(certsToRemove) == 0 { + log.Info().Msg("No add or remove operations specified, please verify your configuration") + if len(errs) > 0 { + apiErrs := mergeErrsToString(&errs, false) + return fmt.Errorf(apiErrs) + } + fmt.Println("No add or remove operations specified, please verify your configuration") + return nil + } + + log.Trace().Interface("certs_to_add", certsToAdd). + Interface("certs_to_remove", certsToRemove). + Str("stores_file", storesFile). + Msg("Generating audit report") + + log.Debug(). + Msg(fmt.Sprintf(DebugFuncCall, "generateAuditReport")) + _, actions, err := generateAuditReport(certsToAdd, certsToRemove, stores, outputFilePath, kfClient) + if err != nil { + log.Error(). + Err(err). + Str("outputFilePath", outputFilePath). + Msg("Error generating audit report") + } + if len(actions) == 0 { + msg := "No reconciliation actions to take, the specified root of trust stores are up-to-date" + log.Info(). + Str("stores_file", storesFile). + Str("add_certs_file", addRootsFile). + Str("remove_certs_file", removeRootsFile). + Msg(msg) + fmt.Println("No reconciliation actions to take, root stores are up-to-date. Exiting.") + if len(errs) > 0 { + apiErrs := mergeErrsToString(&errs, false) + return fmt.Errorf(apiErrs) + } + return nil + } + + log.Debug().Msg(fmt.Sprintf(DebugFuncCall, "reconcileRoots")) + rErr = reconcileRoots(actions, kfClient, reportFile, dryRun) + if rErr != nil { + log.Error().Err(rErr).Msg("Error reconciling root of trust stores") + if len(errs) > 0 { + apiErrs := mergeErrsToString(&errs, false) + rErr = fmt.Errorf("%s\r\n%s", rErr, apiErrs) + } + return rErr + } + if lookupFailures != nil { + errMsg := fmt.Errorf( + "The following stores could not be found:\r\n%s", strings.Join(lookupFailures, ",\r\n"), + ) + log.Error().Err(errMsg).Strs("lookup_failures", lookupFailures).Send() + if len(errs) > 0 { + apiErrs := mergeErrsToString(&errs, false) + errMsg = fmt.Errorf("%s\r\n%s", errMsg, apiErrs) + return errMsg + } + return errMsg + } + orchsURL := fmt.Sprintf( + "https://%s/Keyfactor/Portal/AgentJobStatus/Index", + kfClient.Hostname, + ) //todo: this path might not work for everyone + + log.Info(). + Str("orchs_url", orchsURL). + Str("outputFilePath", outputFilePath). + Msg("Reconciliation completed. Check orchestrator jobs for details.") + fmt.Println(fmt.Sprintf("Reconciliation completed. Check orchestrator jobs for details. %s", orchsURL)) + + if len(lookupFailures) > 0 { + lookupErrs := fmt.Errorf( + "Reconciliation completed with failures, "+ + "the following stores could not be found:\r\n%s", strings.Join( + lookupFailures, + "\r\n", + ), + ) + log.Error().Err(lookupErrs).Strs( + "lookup_failures", + lookupFailures, + ).Msg("The following stores could not be found") + if len(errs) > 0 { + apiErrs := mergeErrsToString(&errs, false) + lookupErrs = fmt.Errorf("%s\r\n%s", lookupErrs, apiErrs) + } + return lookupErrs + } else if len(errs) > 0 { + apiErrs := mergeErrsToString(&errs, false) + log.Error().Str("api_errors", apiErrs).Msg("Reconciliation completed with failures") + return fmt.Errorf("Reconciliation completed with failures:\r\n%s", apiErrs) + } + return nil +} + +func processCSVReportFile(reportFile string, kfClient *api.Client, dryRun bool) error { + log.Debug().Str("report_file", reportFile).Bool("dry_run", dryRun). + Msg("Parsing existing audit report") + // Read in the CSV + + log.Debug().Str("report_file", reportFile).Msg("reading audit report file") + csvFile, err := os.Open(reportFile) + if err != nil { + log.Error().Err(err).Str("report_file", reportFile).Msg("Error reading audit report file") + return err + } + + validHeader := false + log.Trace().Str("report_file", reportFile).Msg("Creating CSV reader") + aCSV := csv.NewReader(csvFile) + aCSV.FieldsPerRecord = -1 + log.Debug().Str("report_file", reportFile).Msg("Reading CSV data") + inFile, cErr := aCSV.ReadAll() + if cErr != nil { + log.Error().Err(cErr).Str("report_file", reportFile).Msg("Error reading CSV file") + return cErr + } + + actions := make(map[string][]ROTAction) + fieldMap := make(map[int]string) + + log.Debug().Str("report_file", reportFile). + Strs("csv_header", AuditHeader). + Msg("Creating field map, index to header name") + for i, field := range AuditHeader { + log.Trace().Str("report_file", reportFile).Str("field", field).Int( + "index", + i, + ).Msg("Processing field") + fieldMap[i] = field + } + + log.Debug().Str("report_file", reportFile).Msg("Iterating over CSV rows") + var errs []error + for ri, row := range inFile { + log.Trace().Str("report_file", reportFile).Strs("row", row).Msg("Processing row") + if strings.EqualFold(strings.Join(row, ","), strings.Join(AuditHeader, ",")) { + log.Trace().Str("report_file", reportFile).Strs("row", row).Msg("Skipping header row") + validHeader = true + continue // Skip header + } + if !validHeader { + invalidHeaderErr := fmt.Errorf( + "invalid header in audit report file please use '%s'", strings.Join( + AuditHeader, + ",", + ), + ) + log.Error().Err(invalidHeaderErr).Str( + "report_file", + reportFile, + ).Msg("Invalid header in audit report file") + return invalidHeaderErr + } + + log.Debug().Str("report_file", reportFile).Msg("Creating action map") + action := make(map[string]interface{}) + for i, field := range row { + log.Trace().Str("report_file", reportFile).Str("field", field).Int( + "index", + i, + ).Msg("Processing field") + fieldInt, iErr := strconv.Atoi(field) + if iErr != nil { + log.Trace().Err(iErr).Str("report_file", reportFile). + Str("field", field). + Int("index", i). + Msg("Field is not an integer, replacing with index value") + action[fieldMap[i]] = field + } else { + log.Trace().Err(iErr).Str("report_file", reportFile). + Str("field", field). + Int("index", i). + Msg("Field is an integer") + action[fieldMap[i]] = fieldInt + } + } + + log.Debug().Str("report_file", reportFile).Msg("Processing add cert action") + addCertStr, aOk := action["AddCert"].(string) + if !aOk { + log.Warn().Str("report_file", reportFile).Msg( + "AddCert field not found in action, " + + "using empty string", + ) + addCertStr = "" + } + + log.Trace().Str("report_file", reportFile).Str( + "add_cert", + addCertStr, + ).Msg("Converting addCertStr to bool") + addCert, acErr := strconv.ParseBool(addCertStr) + if acErr != nil { + log.Warn().Str("report_file", reportFile).Err(acErr).Msg( + "Unable to parse bool from addCertStr, defaulting to FALSE", + ) + addCert = false + } + + log.Debug().Str("report_file", reportFile).Msg("Processing remove cert action") + removeCertStr, rOk := action["RemoveCert"].(string) + if !rOk { + log.Warn().Str("report_file", reportFile).Msg( + "RemoveCert field not found in action, " + + "using empty string", + ) + removeCertStr = "" + } + log.Trace().Str("report_file", reportFile).Str( + "remove_cert", + removeCertStr, + ).Msg("Converting removeCertStr to bool") + removeCert, rcErr := strconv.ParseBool(removeCertStr) + if rcErr != nil { + log.Warn(). + Str("report_file", reportFile). + Err(rcErr). + Msg("Unable to parse bool from removeCertStr, defaulting to FALSE") + removeCert = false + } + + log.Trace().Str("report_file", reportFile).Msg("Processing store type") + sType, sOk := action["StoreType"].(string) + if !sOk { + log.Warn().Str("report_file", reportFile).Msg( + "StoreType field not found in action, " + + "using empty string", + ) + sType = "" + } + + log.Trace().Str("report_file", reportFile).Msg("Processing store path") + sPath, pOk := action["Path"].(string) + if !pOk { + log.Warn().Str("report_file", reportFile).Msg( + "Path field not found in action, " + + "using empty string", + ) + sPath = "" + } + + log.Trace().Str("report_file", reportFile).Msg("Processing thumbprint") + tp, tpOk := action["Thumbprint"].(string) + if !tpOk { + log.Warn().Str("report_file", reportFile).Msg( + "Thumbprint field not found in action, " + + "using empty string", + ) + tp = "" + } + + log.Trace().Str("report_file", reportFile).Msg("Processing cert id") + cid, cidOk := action["CertID"].(int) + if !cidOk { + log.Warn().Str("report_file", reportFile).Msg( + "CertID field not found in action, " + + "using -1", + ) + cid = -1 + } + + if !tpOk && !cidOk { + errMsg := fmt.Errorf("row is missing Thumbprint or CertID") + log.Error().Err(errMsg). + Str("report_file", reportFile). + Int("row", ri). + Msg("Invalid row in audit report file") + errs = append(errs, errMsg) + continue + } + + sId, sIdOk := action["StoreID"].(string) + if !sIdOk { + errMsg := fmt.Errorf("row is missing StoreID") + log.Error().Err(errMsg). + Str("report_file", reportFile). + Int("row", ri). + Msg("Invalid row in audit report file") + errs = append(errs, errMsg) + continue + } + if cid == -1 && tp != "" { + log.Debug().Str("report_file", reportFile). + Int("row", ri). + Str("thumbprint", tp). + Msg("Looking up certificate by thumbprint") + certLookupReq := api.GetCertificateContextArgs{ + IncludeMetadata: boolToPointer(true), + IncludeLocations: boolToPointer(true), + CollectionId: nil, //todo: add support for collection ID + Thumbprint: tp, + Id: 0, //force to 0 as -1 will error out the API request + } + log.Debug().Str("report_file", reportFile). + Int("row", ri). + Str("thumbprint", tp). + Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetCertificateContext")) + + certLookup, err := kfClient.GetCertificateContext(&certLookupReq) + if err != nil { + log.Error().Err(err).Str("report_file", reportFile). + Int("row", ri). + Str("thumbprint", tp). + Msg("Error looking up certificate by thumbprint") + continue + } + cid = certLookup.Id + log.Debug().Str("report_file", reportFile). + Int("row", ri). + Str("thumbprint", tp). + Int("cert_id", cid). + Msg("Certificate found by thumbprint") + } + + log.Trace().Str("report_file", reportFile). + Int("row", ri). + Str("store_id", sId). + Str("store_type", sType). + Str("store_path", sPath). + Str("thumbprint", tp). + Int("cert_id", cid). + Bool("add_cert", addCert). + Bool("remove_cert", removeCert). + Msg("Creating reconciliation action") + a := ROTAction{ + StoreID: sId, + StoreType: sType, + StorePath: sPath, + Thumbprint: tp, + CertID: cid, + AddCert: addCert, + RemoveCert: removeCert, + } + + log.Trace().Str("report_file", reportFile). + Int("row", ri).Interface("action", a).Msg("Adding action to actions map") + actions[a.Thumbprint] = append(actions[a.Thumbprint], a) + } + + log.Info().Str("report_file", reportFile).Msg("Audit report parsed successfully") + if len(actions) == 0 { + rtMsg := "No reconciliation actions to take, root stores are up-to-date. Exiting." + log.Info().Str("report_file", reportFile). + Msg(rtMsg) + fmt.Println(rtMsg) + if len(errs) > 0 { + errStr := mergeErrsToString(&errs, false) + log.Error().Str("report_file", reportFile). + Str("errors", errStr). + Msg("Errors encountered while parsing audit report") + return fmt.Errorf("errors encountered while parsing audit report: %s", errStr) + } + return nil + } + + log.Debug().Str("report_file", reportFile).Msg(fmt.Sprintf(DebugFuncCall, "reconcileRoots")) + rErr := reconcileRoots(actions, kfClient, reportFile, dryRun) + if rErr != nil { + log.Error().Err(rErr).Str("report_file", reportFile).Msg("Error reconciling roots") + return rErr + } + defer csvFile.Close() + + orchsURL := fmt.Sprintf( + "https://%s/Keyfactor/Portal/AgentJobStatus/Index", + kfClient.Hostname, + ) //todo: this pathing might not work for everyone + + if len(errs) > 0 { + errStr := mergeErrsToString(&errs, false) + log.Error().Str("report_file", reportFile). + Str("errors", errStr). + Msg("Errors encountered while reconciling root of trust stores") + return fmt.Errorf("errors encountered while reconciling roots:\r\n%s", errStr) + + } + + log.Info().Str("report_file", reportFile). + Str("orchs_url", orchsURL). + Msg("Reconciliation completed. Check orchestrator jobs for details") + fmt.Println(fmt.Sprintf("Reconciliation completed. Check orchestrator jobs for details. %s", orchsURL)) + + return nil +} + +func init() { + var ( + stores string + addCerts string + removeCerts string + minCertsInStore int + maxPrivateKeys int + maxLeaves int + tType = tTypeCerts + outputFilePath string + inputFile string + storeTypes []string + containerNames []string + subjectNames []string + collections []string + ) + + storesCmd.AddCommand(rotCmd) + + // Root of trust `audit` command + rotCmd.AddCommand(rotAuditCmd) + rotAuditCmd.Flags().StringVarP(&stores, "stores", "s", "", "CSV file containing cert stores to enroll into") + rotAuditCmd.Flags().StringVarP( + &addCerts, "add-certs", "a", "", + "CSV file containing cert(s) to enroll into the defined cert stores", + ) + rotAuditCmd.Flags().StringVarP( + &removeCerts, "remove-certs", "r", "", + "CSV file containing cert(s) to remove from the defined cert stores", + ) + rotAuditCmd.Flags().IntVarP( + &minCertsInStore, + "min-certs", + "m", + -1, + "The minimum number of certs that should be in a store to be considered a 'root' store. If set to `-1` then all stores will be considered.", + ) + rotAuditCmd.Flags().IntVarP( + &maxPrivateKeys, + "max-keys", + "k", + -1, + "The max number of private keys that should be in a store to be considered a 'root' store. If set to `-1` then all stores will be considered.", + ) + rotAuditCmd.Flags().IntVarP( + &maxLeaves, + "max-leaf-certs", + "l", + -1, + "The max number of non-root-certs that should be in a store to be considered a 'root' store. If set to `-1` then all stores will be considered.", + ) + rotAuditCmd.Flags().BoolP("dry-run", "d", false, "Dry run mode") + rotAuditCmd.Flags().StringVarP( + &outputFilePath, "outputFilePath", "o", "", + "Path to write the audit report file to. If not specified, the file will be written to the current directory.", + ) + + // Root of trust `reconcile` command + rotCmd.AddCommand(rotReconcileCmd) + rotReconcileCmd.Flags().StringVarP(&stores, "stores", "s", "", "CSV file containing cert stores to enroll into") + rotReconcileCmd.Flags().StringVarP( + &addCerts, "add-certs", "a", "", + "CSV file containing cert(s) to enroll into the defined cert stores", + ) + rotReconcileCmd.Flags().StringVarP( + &removeCerts, "remove-certs", "r", "", + "CSV file containing cert(s) to remove from the defined cert stores", + ) + rotReconcileCmd.Flags().IntVarP( + &minCertsInStore, + "min-certs", + "m", + -1, + "The minimum number of certs that should be in a store to be considered a 'root' store. If set to `-1` then all stores will be considered.", + ) + rotReconcileCmd.Flags().IntVarP( + &maxPrivateKeys, + "max-keys", + "k", + -1, + "The max number of private keys that should be in a store to be considered a 'root' store. If set to `-1` then all stores will be considered.", + ) + rotReconcileCmd.Flags().IntVarP( + &maxLeaves, + "max-leaf-certs", + "l", + -1, + "The max number of non-root-certs that should be in a store to be considered a 'root' store. If set to `-1` then all stores will be considered.", + ) + rotReconcileCmd.Flags().BoolP("dry-run", "d", false, "Dry run mode") + rotReconcileCmd.Flags().BoolP("import-csv", "v", false, "Import an audit report file in CSV format.") + rotReconcileCmd.Flags().StringVarP( + &inputFile, "input-file", "i", reconcileDefaultFileName, + "Path to a file generated by 'stores rot audit' command.", + ) + rotReconcileCmd.Flags().StringVarP( + &outputFilePath, "outputFilePath", "o", "", + "Path to write the audit report file to. If not specified, the file will be written to the current directory.", + ) + //rotReconcileCmd.MarkFlagsRequiredTogether("add-certs", "stores") + //rotReconcileCmd.MarkFlagsRequiredTogether("remove-certs", "stores") + rotReconcileCmd.MarkFlagsMutuallyExclusive("add-certs", "import-csv") + rotReconcileCmd.MarkFlagsMutuallyExclusive("remove-certs", "import-csv") + rotReconcileCmd.MarkFlagsMutuallyExclusive("stores", "import-csv") + + // Root of trust `generate` command + rotCmd.AddCommand(rotGenStoreTemplateCmd) + rotGenStoreTemplateCmd.Flags().StringVarP( + &outputFilePath, "outputFilePath", "o", "", + "Path to write the template file to. If not specified, the file will be written to the current directory.", + ) + rotGenStoreTemplateCmd.Flags().StringVarP( + &outputFormat, "format", "f", "csv", + "The type of template to generate. Only `csv` is supported at this time.", + ) + rotGenStoreTemplateCmd.Flags().Var( + &tType, "type", + `The type of template to generate. Only "certs|stores|actions" are supported at this time.`, + ) + rotGenStoreTemplateCmd.Flags().StringSliceVar( + &storeTypes, + "store-type", + []string{}, + "Multi value flag. Attempt to pre-populate the stores template with the certificate stores matching specified store types. If not specified, the template will be empty.", + ) + rotGenStoreTemplateCmd.Flags().StringSliceVar( + &containerNames, + "container-name", + []string{}, + "Multi value flag. Attempt to pre-populate the stores template with the certificate stores matching specified container types. If not specified, the template will be empty.", + ) + rotGenStoreTemplateCmd.Flags().StringSliceVar( + &collections, + "collection", + []string{}, + "Certificate collection name(s) to pre-populate the stores template with. If not specified, the template will be empty.", + ) + + rotGenStoreTemplateCmd.Flags().StringSliceVar( + &subjectNames, + "cn", + []string{}, + "Subject name(s) to pre-populate the 'certs' template with. If not specified, the template will be empty. Does not work with SANs.", + ) + + rotGenStoreTemplateCmd.RegisterFlagCompletionFunc("type", templateTypeCompletion) + rotGenStoreTemplateCmd.MarkFlagRequired("type") +} + +func promptYesNo(q string) bool { + isYes := false + promptMsg := fmt.Sprintf("%s", q) + //check if prompt ends with ? and add it if not + if !strings.HasSuffix(promptMsg, "?") { + promptMsg = fmt.Sprintf("%s?", promptMsg) + } + prompt := &survey.Confirm{ + Message: promptMsg, + } + survey.AskOne(prompt, &isYes) + return isYes +} + +func promptForFilePath(msg string) string { + file := "" + if msg == "" { + msg = "input a file path" + } + prompt := &survey.Input{ + Message: msg, + Suggest: func(toComplete string) []string { + files, _ := filepath.Glob(toComplete + "*") + return files + }, + } + survey.AskOne(prompt, &file) + return file +} + +func promptSelectRotStores(resourceType string) string { + var selected string + + opts := []string{ + "Manual Select", + "Discover", + "File", + "All", + } + //sort ops + sort.Strings(opts) + + selected = promptSingleSelect( + fmt.Sprintf("Source %s from:", resourceType), + opts, + DefaultMenuPageSizeSmall, + ) + return selected + +} + +func promptSelectFromAPIorFile(resourceType string) string { + var selected string + + selected = promptSingleSelect( + fmt.Sprintf("Source %s from:", resourceType), + DefaultSourceTypeOptions, + DefaultMenuPageSizeSmall, + ) + return selected + +} + +func promptSelectCerts(client *api.Client) []string { + searchOpts := []string{ + "Certificate", + "Collection", + } + var selectedCerts []string + + selectedSearch := promptMultiSelect("Select certs to include in audit by:", searchOpts) + if len(selectedSearch) == 0 { + fmt.Println("No search options selected defaulting to 'Certificate'") + selectedSearch = []string{"Certificate"} + } + + log.Debug().Strs("selected_search", selectedSearch).Msg("Processing selected search options") + for _, s := range selectedSearch { + log.Trace().Str("search_option", s).Msg("Processing search option") + switch s { + case "Certificate": + log.Debug().Msg(fmt.Sprintf(DebugFuncCall, "menuCertificates")) + certOpts, certErr := menuCertificates(client, nil) + if certErr != nil { + log.Error().Err(certErr).Msg("Error fetching certificates from Keyfactor Command") + continue + } else if len(certOpts) == 0 { + fmt.Println("No certificates returned from Keyfactor Command") + continue + } + selectedCerts = append( + selectedCerts, + promptMultiSelect("Select certificates to audit:", certOpts)..., + ) + + case "Collection": + log.Debug().Msg(fmt.Sprintf(DebugFuncCall, "menuCollections")) + collectionOpts, colErr := menuCollections(client) + if colErr != nil { + log.Error().Err(colErr).Msg("Error fetching collections from Keyfactor Command") + // todo: prompt for collection name or ID + continue + } + if len(collectionOpts) == 0 { + fmt.Println("No collections returned from Keyfactor Command") + continue + } + var selectedCollections []string + selectedCollections = append( + selectedCollections, + promptMultiSelect( + "Select certificates associated with collection(s) to audit:", + collectionOpts, + )..., + ) + //fetch certs associated with selected collections + log.Info().Msg("Fetching certificates associated with selected collections") + for _, col := range selectedCollections { + //parse collection ID from selected collection + colVals := strings.Split(col, ":") + colID, idErr := strconv.Atoi(colVals[0]) + if idErr != nil { + log.Error(). + Err(idErr). + Str("collection", col). + Msg("Error parsing collection ID, unable to fetch certificates") + continue + } + + params := make(map[string]string) + params["CollectionID"] = fmt.Sprintf("%d", colID) + log.Debug(). + Str("collection", col). + Int("collection_id", colID). + Interface("params", params). + Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetCertificatesByCollection")) + certOpts, certErr := menuCertificates(client, ¶ms) + if certErr != nil { + log.Error().Err(certErr).Msg("Error fetching certificates from Keyfactor Command") + continue + } + if len(certOpts) == 0 { + log.Warn().Str("collection", col).Msg("No certificates found associated with selected collection") + fmt.Println(fmt.Sprintf("No certificates found associated with collection %s", col)) + continue + } + selectedCerts = append(selectedCerts, certOpts...) + } + } + } + return selectedCerts +} + +func promptSelectStores(client *api.Client) []string { + searchOpts := []string{ + "Store", + "StoreType", + "Container", + //"Collection", + } + var selectedStores []string + + selectedSearch := promptMultiSelect("Select cert stores to audit by:", searchOpts) + if len(selectedSearch) == 0 { + fmt.Println("No search options selected defaulting to 'Store'") + selectedSearch = []string{"Store"} + } + + for _, s := range selectedSearch { + switch s { + case "Container": + contOpts, contErr := menuContainers(client) + if contErr != nil { + fmt.Println("Error fetching containers from Keyfactor Command: ", contErr) + continue + } else if contOpts == nil || len(contOpts) == 0 { + fmt.Println("No containers found") + continue + } + + log.Debug().Msg("Prompting user to select containers") + selectedStores = append( + selectedStores, + promptMultiSelect("Select stores associated with container(s) to audit:", contOpts)..., + ) + // Collection based store collection not supported as stores are not associated with collections certificates + // are associated with collections + //case "Collection": + // collectionOpts, colErr := menuCollections(client) + // if colErr != nil { + // fmt.Println("Error fetching collections from Keyfactor Command: ", colErr) + // continue + // } else if collectionOpts == nil || len(collectionOpts) == 0 { + // fmt.Println("No collections found") + // continue + // } + // var selectedCollections []string + // selectedCollections = append( + // selectedCollections, + // promptMultiSelect( + // "Select stores associated with collection(s) to audit:", + // collectionOpts, + // )..., + // ) + // + // //fetch stores associated with selected collections + // log.Info().Msg("Fetching stores associated with selected collections") + // log.Debug().Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetStoresByCollection")) + // stores, sErr := client.GetSt(selectedCollections) + + case "StoreType": + storeTypeNames, stErr := menuStoreType(client) + if stErr != nil { + fmt.Println("Error fetching store types from Keyfactor Command: ", stErr) + continue + } else if len(storeTypeNames) == 0 { + fmt.Println("No store types found") + continue + } + + log.Debug().Msg("Prompting user to select store types") + var selectedStoreTypes []string + selectedStoreTypes = append( + selectedStoreTypes, + promptMultiSelect( + "Select stores associated with store type(s) to audit:", + storeTypeNames, + )..., + ) + + //lookup stores associated with selected store types + log.Info().Msg("Fetching stores associated with selected store types") + for _, st := range selectedStoreTypes { + //parse storetype ID from selected store type + stVals := strings.Split(st, ":") + stID, idErr := strconv.Atoi(stVals[0]) + if idErr != nil { + log.Error(). + Err(idErr). + Str("store_type", st). + Msg("Error parsing store type ID, unable to fetch stores of type") + continue + } + + log.Debug().Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetStoresByStoreType")) + params := make(map[string]interface{}) + params["CertStoreType"] = stID + stores, sErr := menuCertificateStores(client, ¶ms) + if sErr != nil { + fmt.Println("Error fetching stores from Keyfactor Command: ", sErr) + continue + } else if len(stores) == 0 { + log.Warn(). + Str("store_type", st). + Msg("No stores found associated with selected store type") + fmt.Println(fmt.Sprintf("No stores of type %s found", st)) //todo: propagate to top CLI + continue + } + selectedStores = append(selectedStores, stores...) + } + + default: + stNames, stErr := menuCertificateStores(client, nil) + if stErr != nil { + fmt.Println("Error fetching stores from Keyfactor Command: ", stErr) + continue + } else if stNames == nil || len(stNames) == 0 { + fmt.Println("No stores found") + continue + } + + log.Debug().Msg("Prompting user to select stores") + selectedStores = append( + selectedStores, + promptMultiSelect("Select stores to audit:", stNames)..., + ) + } + } + return selectedStores +} + +func promptSingleSelect(msg string, opts []string, menuPageSize int) string { + if menuPageSize <= 0 { + menuPageSize = DefaultMenuPageSizeSmall + } + var choice string + prompt := &survey.Select{ + Message: msg, + Options: opts, + PageSize: menuPageSize, + } + survey.AskOne(prompt, &choice, survey.WithPageSize(10)) + return choice +} + +func promptMultiSelect(msg string, opts []string) []string { + var choices []string + prompt := &survey.MultiSelect{ + Message: msg, + Options: opts, + PageSize: 10, } - return nil + survey.AskOne(prompt, &choices, survey.WithPageSize(10)) + return choices } -func readCertsFile(certsFilePath string, kfclient *api.Client) (map[string]string, error) { - // Read in the cert CSV - csvFile, _ := os.Open(certsFilePath) - reader := csv.NewReader(bufio.NewReader(csvFile)) - certEntries, _ := reader.ReadAll() - var certs = make(map[string]string) - for _, entry := range certEntries { - switch entry[0] { - case "CertID", "thumbprint", "id", "CertId", "Thumbprint": - continue // Skip header - } - certs[entry[0]] = entry[0] +func menuStoreType(client *api.Client) ([]string, error) { + //fetch store type options from keyfactor command + log.Info().Msg("Fetching store types from Keyfactor Command") + log.Debug().Msg(fmt.Sprintf(DebugFuncCall, "kfClient.ListCertificateStoreTypes")) + storeTypes, stErr := client.ListCertificateStoreTypes() + if stErr != nil { + log.Error().Err(stErr).Msg("Error fetching store types from Keyfactor Command") + return nil, stErr + } else if storeTypes == nil || len(*storeTypes) == 0 { + log.Warn().Msg("No store types returned from Keyfactor Command") + //fmt.Println("No store types found") + return nil, nil } - return certs, nil + + var storeTypeNames []string + log.Trace().Interface("store_types", storeTypes).Msg("Formatting store type choices for prompt") + for _, st := range *storeTypes { + log.Trace().Interface("store_type", st).Msg("Adding store type to options") + stName := fmt.Sprintf("%d: %s", st.StoreType, st.Name) + log.Trace().Str("store_type_name", stName).Msg("Adding store type to options") + storeTypeNames = append(storeTypeNames, stName) + log.Trace().Strs("store_type_options", storeTypeNames).Msg("Store type options") + } + return storeTypeNames, nil } -func isRootStore(st *api.GetCertificateStoreResponse, invs *[]api.CertStoreInventoryV1, minCerts int, maxKeys int, maxLeaf int) bool { - leafCount := 0 - keyCount := 0 - certCount := 0 - for _, inv := range *invs { - log.Printf("[DEBUG] inv: %v", inv) - certCount += len(inv.Certificates) +func menuContainers(client *api.Client) ([]string, error) { + //fetch container options from keyfactor command + log.Info().Msg("Fetching containers from Keyfactor Command") + log.Debug().Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetStoreContainers")) + containers, cErr := client.GetStoreContainers() + if cErr != nil { + log.Error().Err(cErr).Msg("Error fetching containers from Keyfactor Command") + return nil, cErr + } else if containers == nil || len(*containers) == 0 { + log.Warn().Msg("No containers returned from Keyfactor Command") + return nil, nil + } + var contOpts []string + log.Trace(). + Interface("containers", containers). + Msg("Formatting container choices for prompt") + for _, c := range *containers { + contName := fmt.Sprintf("%d: %s", c.Id, c.Name) + log.Trace().Str("container_name", contName).Msg("Adding container to options") + contOpts = append(contOpts, contName) + log.Trace().Strs("container_options", contOpts).Msg("Container options") + } + return contOpts, nil +} - for _, cert := range inv.Certificates { - if cert.IssuedDN != cert.IssuerDN { - leafCount++ - } - if inv.Parameters["PrivateKeyEntry"] == "Yes" { - keyCount++ +func menuCollections(client *api.Client) ([]string, error) { + //fetch collection options from keyfactor command + log.Info().Msg("Fetching collections from Keyfactor Command") + log.Debug().Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetCollections")) + + sdkClient, sdkErr := convertClient(client) + if sdkErr != nil { + log.Error().Err(sdkErr).Msg("Error converting client to v2") + return nil, sdkErr + } + //createdPamProviderType, httpResponse, rErr := sdkClient.PAMProviderApi.PAMProviderCreatePamProviderType(context.Background()). + // XKeyfactorRequestedWith(XKeyfactorRequestedWith).XKeyfactorApiVersion(XKeyfactorApiVersion). + // Type_(*pamProviderType). + // Execute() + collections, httpResponse, collErr := sdkClient.CertificateCollectionApi. + CertificateCollectionGetCollections(context.Background()). + XKeyfactorRequestedWith(XKeyfactorRequestedWith). + XKeyfactorApiVersion(XKeyfactorApiVersion). + Execute() + + defer httpResponse.Body.Close() + + switch { + case collErr != nil: + log.Error().Err(collErr).Msg("Error fetching collections from Keyfactor Command") + return nil, collErr + case collections == nil || len(collections) == 0: + log.Warn().Msg("No collections returned from Keyfactor Command") + return nil, nil + case httpResponse.StatusCode != http.StatusOK: + log.Warn().Int("status_code", httpResponse.StatusCode).Msg("No collections returned from Keyfactor Command") + return nil, fmt.Errorf("%s - no collections returned from Keyfactor Command", httpResponse.Status) + } + + var collectionOpts []string + log.Trace().Interface("collections", collections).Msg("Formatting collection choices for prompt") + for _, c := range collections { + collName := fmt.Sprintf("%d: %s", *c.Id, *c.Name) + log.Trace().Str("collection_name", collName).Msg("Adding collection to options") + collectionOpts = append(collectionOpts, collName) + log.Trace().Strs("collection_options", collectionOpts).Msg("Collection options") + } + return collectionOpts, nil +} + +func convertClient(v1Client *api.Client) (*sdk.APIClient, error) { + // todo add support to convert the v1 client to v2 but for now use inputs used to created the v1 client + config := make(map[string]string) + + if v1Client != nil { + config["host"] = v1Client.Hostname + //todo: expose these values in the client + //config["username"] = v1Client.Username + //config["password"] = v1Client.Password + //config["domain"] = v1Client.Domain + } else { + config["host"] = kfcHostName + config["username"] = kfcUsername + config["password"] = kfcPassword + config["domain"] = kfcDomain + } + + configuration := sdk.NewConfiguration(config) + sdkClient := sdk.NewAPIClient(configuration) + return sdkClient, nil +} + +func menuCertificates(client *api.Client, params *map[string]string) ([]string, error) { + //fetch certificate options from keyfactor command + log.Info().Msg("Fetching certificates from Keyfactor Command") + log.Debug().Msg(fmt.Sprintf(DebugFuncEnter, "menuCertificates")) + if params == nil { + params = &map[string]string{} + } + certs, cErr := client.ListCertificates(*params) + if cErr != nil { + log.Error().Err(cErr).Msg("Error fetching certificates from Keyfactor Command") + return nil, cErr + } else if len(certs) == 0 { + log.Warn().Msg("No certificates returned from Keyfactor Command") + return nil, nil + } + + var certOpts []string + log.Trace().Interface("certificates", certs).Msg("Formatting certificate choices for prompt") + for _, c := range certs { + certName := fmt.Sprintf("%d: %s (%s) - %s", c.Id, c.IssuedCN, c.Thumbprint, c.NotBefore) + log.Trace().Str("certificate_name", certName).Msg("Adding certificate to options") + certOpts = append(certOpts, certName) + log.Trace().Strs("certificate_options", certOpts).Msg("Certificate options") + } + log.Debug().Int("certificates", len(certOpts)).Msg(fmt.Sprintf(DebugFuncExit, "menuCertificates")) + //sort certOps + sort.Strings(certOpts) + return certOpts, nil + +} + +func menuCertificateStores(client *api.Client, params *map[string]interface{}) ([]string, error) { + // fetch all stores from keyfactor command + log.Info().Msg("Fetching stores from Keyfactor Command") + log.Debug().Msg(fmt.Sprintf(DebugFuncCall, "kfClient.ListCertificateStores")) + stores, sErr := client.ListCertificateStores(params) + if sErr != nil { + log.Error().Err(sErr).Msg("Error fetching stores from Keyfactor Command") + fmt.Println("Error fetching stores from Keyfactor Command: ", sErr) + return nil, sErr + } else if stores == nil || len(*stores) == 0 { + log.Info().Msg("No stores returned from Keyfactor Command") + fmt.Println("No stores found") + return nil, nil + } + + log.Trace().Interface("stores", stores).Msg("Formatting store choices for prompt") + var stNames []string + var storeTypesLookup = make(map[int]string) + for _, st := range *stores { + //lookup store type name + var stName = fmt.Sprintf("%d", st.CertStoreType) + if _, ok := storeTypesLookup[st.CertStoreType]; !ok { + log.Debug().Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetCertificateStoreType")) + storeType, stErr := client.GetCertificateStoreType(st.CertStoreType) + if stErr != nil { + log.Error().Err(stErr).Msg("Error fetching store type name from Keyfactor Command") + } else { + storeTypesLookup[st.CertStoreType] = storeType.Name + stName = storeType.Name } + } else { + stName = storeTypesLookup[st.CertStoreType] } + + log.Trace().Interface("store", st).Msg("Adding store to options") + stMenuName := fmt.Sprintf( + "%s/%s [%s]: (%s)", st.ClientMachine, + st.StorePath, stName, st.Id, + ) + log.Trace().Str("store_name", stMenuName).Msg("Adding store to options") + stNames = append(stNames, stMenuName) } - if certCount < minCerts && minCerts >= 0 { - log.Printf("[DEBUG] Store %s has %d certs, less than the required count of %d", st.Id, certCount, minCerts) - return false + sort.Strings(stNames) + return stNames, nil +} + +func promptForCriteria() error { + var maxKeys int + prompt := &survey.Input{ + Message: "Enter max private keys:", + Help: "Enter the maximum number of private keys allowed in a certificate store for it to be considered" + + " a trusted root store", + Default: fmt.Sprintf("%d", trustCriteria.MaxKeys), } - if leafCount > maxLeaf && maxLeaf >= 0 { - log.Printf("[DEBUG] Store %s has too many leaf certs", st.Id) - return false + survey.AskOne(prompt, &maxKeys) + + var minCerts int + prompt = &survey.Input{ + Message: "Enter min certs in store:", + Help: "Enter the minimum number of certificates allowed in a certificate store for it to be considered" + + " a trusted root store", + Default: fmt.Sprintf("%d", trustCriteria.MinCerts), } - - if keyCount > maxKeys && maxKeys >= 0 { - log.Printf("[DEBUG] Store %s has too many keys", st.Id) - return false + survey.AskOne(prompt, &minCerts) + + var maxLeaves int + prompt = &survey.Input{ + Message: "Enter max leaf certs in store:", + Help: "Enter the maximum number of non-root certificates allowed in a certificate store for it to be considered" + + " a trusted root store", + Default: fmt.Sprintf("%d", trustCriteria.MaxLeaf), } + survey.AskOne(prompt, &maxLeaves) - return true + trustCriteria.MaxKeys = maxKeys + trustCriteria.MinCerts = minCerts + trustCriteria.MaxLeaf = maxLeaves + return nil } var ( @@ -392,24 +2532,13 @@ kfutil stores rot reconcile --import-csv PersistentPreRun: nil, PersistentPreRunE: nil, PreRun: nil, - PreRunE: nil, - Run: func(cmd *cobra.Command, args []string) { - // Global flags - debugFlag, _ := cmd.Flags().GetBool("debugFlag") - configFile, _ := cmd.Flags().GetString("config") - noPrompt, _ := cmd.Flags().GetBool("no-prompt") - profile, _ := cmd.Flags().GetString("profile") - - kfcUsername, _ := cmd.Flags().GetString("kfcUsername") - kfcPassword, _ := cmd.Flags().GetString("kfcPassword") - kfcDomain, _ := cmd.Flags().GetString("kfcDomain") - - authConfig := createAuthConfigFromParams(kfcHostName, kfcUsername, kfcPassword, kfcDomain, kfcAPIPath) + PreRunE: func(cmd *cobra.Command, args []string) error { + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true - debugModeEnabled := checkDebug(debugFlag) - log.Println("Debug mode enabled: ", debugModeEnabled) - var lookupFailures []string - kfClient, _ := initClient(configFile, profile, "", "", noPrompt, authConfig, false) + // Specific Flags storesFile, _ := cmd.Flags().GetString("stores") addRootsFile, _ := cmd.Flags().GetString("add-certs") removeRootsFile, _ := cmd.Flags().GetString("remove-certs") @@ -417,47 +2546,138 @@ kfutil stores rot reconcile --import-csv maxLeaves, _ := cmd.Flags().GetInt("max-leaf-certs") maxKeys, _ := cmd.Flags().GetInt("max-keys") dryRun, _ := cmd.Flags().GetBool("dry-run") - outpath, _ := cmd.Flags().GetString("outpath") - // Read in the stores CSV - log.Printf("[DEBUG] storesFile: %s", storesFile) - log.Printf("[DEBUG] addRootsFile: %s", addRootsFile) - log.Printf("[DEBUG] removeRootsFile: %s", removeRootsFile) - log.Printf("[DEBUG] dryRun: %t", dryRun) + outputFilePath, _ := cmd.Flags().GetString("outputFilePath") + + // Debug + expEnabled checks + isExperimental := false + debugErr := warnExperimentalFeature(expEnabled, isExperimental) + if debugErr != nil { + return debugErr + } + informDebug(debugFlag) + + trustCriteria.MinCerts = minCerts + trustCriteria.MaxKeys = maxKeys + trustCriteria.MaxLeaf = maxLeaves + log.Debug().Str("trust_criteria", fmt.Sprintf("%s", trustCriteria.String())). + Str("add_file", addRootsFile). + Str("remove_file", removeRootsFile). + Int("min_certs", minCerts). + Int("max_keys", maxKeys). + Int("max_leaves", maxLeaves). + Bool("dry_run", dryRun). + Msg("Root of trust audit command") + + authConfig := createAuthConfigFromParams(kfcHostName, kfcUsername, kfcPassword, kfcDomain, kfcAPIPath) + + var lookupFailures []string + kfClient, cErr := initClient(configFile, profile, "", "", noPrompt, authConfig, false) + if cErr != nil { + log.Error().Err(cErr).Msg("Error initializing Keyfactor client") + return cErr + } + + // validate flags + var storesErr error + log.Debug().Str("stores_file", storesFile).Bool("no_prompt", noPrompt). + Msg(fmt.Sprintf(DebugFuncCall, "validateStoresInput")) + storesFile, storesErr = validateStoresInput(&storesFile, &noPrompt, kfClient) + if storesErr != nil { + return storesErr + } + + log.Debug().Str("add_file", addRootsFile).Str("remove_file", removeRootsFile).Bool("no_prompt", noPrompt). + Msg(fmt.Sprintf(DebugFuncCall, "validateCertsInput")) + var certsErr error + addRootsFile, removeRootsFile, certsErr = validateCertsInput( + addRootsFile, removeRootsFile, + kfClient, + ) + if certsErr != nil { + log.Error().Err(cErr).Msg("Invalid certs input please provide certs to add or remove.") + return cErr + } + + log.Info().Str("stores_file", storesFile). + Str("add_file", addRootsFile). + Str("remove_file", removeRootsFile). + Bool("dry_run", dryRun). + Msg("Performing root of trust audit") + // Read in the stores CSV - csvFile, _ := os.Open(storesFile) + log.Debug().Str("stores_file", storesFile).Msg("Reading in stores file") + csvFile, ioErr := os.Open(storesFile) + if ioErr != nil { + log.Error().Err(ioErr).Str("stores_file", storesFile).Msg("Error reading in stores file") + return ioErr + } + + log.Trace().Str("stores_file", storesFile).Msg("Creating CSV reader") reader := csv.NewReader(bufio.NewReader(csvFile)) - storeEntries, _ := reader.ReadAll() + + log.Debug().Str("stores_file", storesFile).Msg("Reading CSV data") + storeEntries, rErr := reader.ReadAll() + if rErr != nil { + log.Error().Err(rErr).Str("stores_file", storesFile).Msg("Error reading in stores file") + return rErr + } + + log.Debug().Str("stores_file", storesFile).Msg("Validating CSV header") var stores = make(map[string]StoreCSVEntry) validHeader := false for _, entry := range storeEntries { + log.Trace().Strs("entry", entry).Msg("Processing row") if strings.EqualFold(strings.Join(entry, ","), strings.Join(StoreHeader, ",")) { validHeader = true continue // Skip header } if !validHeader { - fmt.Printf("[ERROR] Invalid header in stores file. Expected: %s", strings.Join(StoreHeader, ",")) - log.Fatalf("[ERROR] Stores CSV file is missing a valid header") + log.Error(). + Strs("header", entry). + Strs("expected_header", StoreHeader). + Msg("Invalid header in stores file") + return fmt.Errorf("invalid header in stores file please use '%s'", strings.Join(StoreHeader, ",")) } + + log.Debug().Strs("entry", entry). + Str("store_id", entry[0]). + Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetCertificateStoreByID")) apiResp, err := kfClient.GetCertificateStoreByID(entry[0]) if err != nil { - log.Printf("[ERROR] getting cert store: %s", err) - _ = append(lookupFailures, strings.Join(entry, ",")) + log.Error().Err(err).Str("store_id", entry[0]).Msg("Error getting cert store") + lookupFailures = append(lookupFailures, strings.Join(entry, ",")) continue } - inventory, invErr := kfClient.GetCertStoreInventoryV1(entry[0]) + log.Debug().Str("store_id", entry[0]). + Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetCertStoreInventoryV1")) + inventory, invErr := kfClient.GetCertStoreInventory(entry[0]) if invErr != nil { - log.Printf("[ERROR] getting cert store inventory for: %s\n%s", entry[0], invErr) + log.Error().Err(invErr).Str("store_id", entry[0]).Msg("Error getting cert store inventory") + lookupFailures = append(lookupFailures, strings.Join(entry, ",")) + continue + } else if inventory == nil { + log.Error().Str( + "store_id", + entry[0], + ).Msg("No inventory response returned for store from Keyfactor Command") + lookupFailures = append(lookupFailures, strings.Join(entry, ",")) + continue } if !isRootStore(apiResp, inventory, minCerts, maxLeaves, maxKeys) { - fmt.Printf("Store %s is not a root store, skipping.\n", entry[0]) - log.Printf("[WARN] Store %s is not a root store", apiResp.Id) + fmt.Printf( + "Store %s is not a root store, skipping.\n", + entry[0], + ) //todo: support for output formatting + log.Warn().Str("store_id", entry[0]).Msg("Store is not considered a root of trust store") continue - } else { - log.Printf("[INFO] Store %s is a root store", apiResp.Id) } + log.Info().Str("store_id", entry[0]).Msg("Store is considered a root of trust store") + + log.Trace().Str("store_id", entry[0]).Msg("Creating store entry") + stores[entry[0]] = StoreCSVEntry{ ID: entry[0], Type: entry[1], @@ -467,59 +2687,126 @@ kfutil stores rot reconcile --import-csv Serials: make(map[string]bool), Ids: make(map[int]bool), } + + log.Debug().Str("store_id", entry[0]).Msg("Iterating over inventory") for _, cert := range *inventory { + log.Trace().Str("store_id", entry[0]).Interface("cert", cert).Msg("Processing inventory") thumb := cert.Thumbprints + trcMsg := "Adding cert to store" for t, v := range thumb { - stores[entry[0]].Thumbprints[t] = v + //log.Trace().Str("store_id", entry[0]).Str("thumbprint", t).Msg(trcMsg) + //stores[entry[0]].Thumbprints[t] = v + log.Trace().Str("store_id", entry[0]). + Int("thumbprint", t). + Str("value", v). + Msg(trcMsg) + stores[entry[0]].Thumbprints[v] = true } for t, v := range cert.Serials { - stores[entry[0]].Serials[t] = v + log.Trace().Str("store_id", entry[0]). + Int("serial", t). + Str("value", v). + Msg(trcMsg) + //stores[entry[0]].Serials[t] = v + //stores[entry[0]].Serials[v] = t + stores[entry[0]].Serials[v] = true } for t, v := range cert.Ids { - stores[entry[0]].Ids[t] = v + log.Trace().Str("store_id", entry[0]). + Int("cert_id", t). + Int("value", v). + Msg(trcMsg) + //stores[entry[0]].Ids[t] = v + stores[entry[0]].Ids[v] = true } } + log.Trace().Strs("entry", entry).Msg("Row processed") + } + if len(lookupFailures) > 0 { + log.Error().Strs("lookup_failures", lookupFailures).Msg("The following stores could not be looked up") + return fmt.Errorf( + "the following stores could not be found on Keyfactor Command:\n%s\nThese errors MUST be resolved"+ + " in order to proceed", strings.Join( + lookupFailures, ","+ + "\r\n", + ), + ) } // Read in the add addCerts CSV var certsToAdd = make(map[string]string) - if addRootsFile != "" { + + if addRootsFile == "" { + log.Debug().Msg("No addCerts file specified") + } else { + log.Info().Str("add_certs_file", addRootsFile).Msg("Reading certs to add file") var rcfErr error + log.Debug().Str("add_certs_file", addRootsFile).Msg(fmt.Sprintf(DebugFuncCall, "readCertsFile")) certsToAdd, rcfErr = readCertsFile(addRootsFile, kfClient) if rcfErr != nil { - fmt.Printf("[ERROR] reading certs file %s: %s", addRootsFile, rcfErr) - log.Fatalf("[ERROR] reading addCerts file: %s", rcfErr) + log.Error().Err(rcfErr).Str("add_certs_file", addRootsFile).Msg("Error reading certs to add file") + return rcfErr } - addCertsJSON, _ := json.Marshal(certsToAdd) - log.Printf("[DEBUG] add certs JSON: %s", string(addCertsJSON)) - log.Println("[DEBUG] AddCert ROT called") - } else { - log.Printf("[DEBUG] No addCerts file specified") - log.Printf("[DEBUG] No addCerts = %s", certsToAdd) + + log.Debug().Str("add_certs_file", addRootsFile).Msg("Creating JSON of certs to add") + addCertsJSON, jErr := json.Marshal(certsToAdd) + if jErr != nil { + log.Error().Err(jErr).Str( + "add_certs_file", + addRootsFile, + ).Msg("Error converting certs to add to JSON") + return jErr + } + log.Trace().Str("add_certs_file", addRootsFile). + Str("add_certs_json", string(addCertsJSON)). + Msg("Certs to add file read successfully") } // Read in the remove removeCerts CSV var certsToRemove = make(map[string]string) - if removeRootsFile != "" { + if removeRootsFile == "" { + log.Info().Msg("No removeCerts file specified") + } else { + log.Info().Str("remove_certs_file", removeRootsFile).Msg("Reading certs to remove file") var rcfErr error + log.Debug().Str("remove_certs_file", removeRootsFile).Msg(fmt.Sprintf(DebugFuncCall, "readCertsFile")) certsToRemove, rcfErr = readCertsFile(removeRootsFile, kfClient) if rcfErr != nil { - fmt.Printf("[ERROR] reading removeCerts file %s: %s", removeRootsFile, rcfErr) - log.Fatalf("[ERROR] reading removeCerts file: %s", rcfErr) + log.Error().Err(rcfErr).Str( + "remove_certs_file", + removeRootsFile, + ).Msg("Error reading certs to remove file") } - removeCertsJSON, _ := json.Marshal(certsToRemove) - log.Printf("[DEBUG] remove certs JSON: %s", string(removeCertsJSON)) - } else { - log.Printf("[DEBUG] No removeCerts file specified") - log.Printf("[DEBUG] No removeCerts = %s", certsToRemove) + + removeCertsJSON, jErr := json.Marshal(certsToRemove) + if jErr != nil { + log.Error().Err(jErr).Str( + "remove_certs_file", + removeRootsFile, + ).Msg("Error converting certs to remove to JSON") + return jErr + } + log.Trace().Str("remove_certs_file", removeRootsFile). + Str("remove_certs_json", string(removeCertsJSON)). + Msg("Certs to remove file read successfully") } - _, _, gErr := generateAuditReport(certsToAdd, certsToRemove, stores, outpath, kfClient) + + log.Debug().Msg(fmt.Sprintf(DebugFuncCall, "generateAuditReport")) + _, _, gErr := generateAuditReport(certsToAdd, certsToRemove, stores, outputFilePath, kfClient) if gErr != nil { - log.Fatalf("[ERROR] generating audit report: %s", gErr) + log.Error().Err(gErr).Msg("Error generating audit report") + return gErr } + + log.Info(). + Str("outputFilePath", outputFilePath). + Msg("Audit report generated successfully") + log.Debug(). + Msg(fmt.Sprintf(DebugFuncExit, "generateAuditReport")) + return nil }, - RunE: nil, + Run: nil, PostRun: nil, PostRunE: nil, PersistentPostRun: nil, @@ -558,25 +2845,10 @@ the utility will first generate an audit report and then execute the add/remove PersistentPreRunE: nil, PreRun: nil, PreRunE: nil, - Run: func(cmd *cobra.Command, args []string) { - // Global flags - debugFlag, _ := cmd.Flags().GetBool("debugFlag") - configFile, _ := cmd.Flags().GetString("config") - noPrompt, _ := cmd.Flags().GetBool("no-prompt") - profile, _ := cmd.Flags().GetString("profile") - - kfcUsername, _ := cmd.Flags().GetString("kfcUsername") - kfcPassword, _ := cmd.Flags().GetString("kfcPassword") - kfcDomain, _ := cmd.Flags().GetString("kfcDomain") - - authConfig := createAuthConfigFromParams(kfcHostName, kfcUsername, kfcPassword, kfcDomain, kfcAPIPath) - - debugModeEnabled := checkDebug(debugFlag) + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true - log.Println("Debug mode enabled: ", debugModeEnabled) - - var lookupFailures []string - kfClient, _ := initClient(configFile, profile, "", "", noPrompt, authConfig, false) + // Specific Flags storesFile, _ := cmd.Flags().GetString("stores") addRootsFile, _ := cmd.Flags().GetString("add-certs") isCSV, _ := cmd.Flags().GetBool("import-csv") @@ -586,251 +2858,74 @@ the utility will first generate an audit report and then execute the add/remove maxLeaves, _ := cmd.Flags().GetInt("max-leaf-certs") maxKeys, _ := cmd.Flags().GetInt("max-keys") dryRun, _ := cmd.Flags().GetBool("dry-run") - outpath, _ := cmd.Flags().GetString("outpath") - - log.Printf("[DEBUG] configFile: %s", configFile) - log.Printf("[DEBUG] storesFile: %s", storesFile) - log.Printf("[DEBUG] addRootsFile: %s", addRootsFile) - log.Printf("[DEBUG] removeRootsFile: %s", removeRootsFile) - log.Printf("[DEBUG] dryRun: %t", dryRun) - - // Parse existing audit report - if isCSV && reportFile != "" { - log.Printf("[DEBUG] isCSV: %t", isCSV) - log.Printf("[DEBUG] reportFile: %s", reportFile) - // Read in the CSV - csvFile, err := os.Open(reportFile) - if err != nil { - fmt.Printf("[ERROR] opening file: %s", err) - log.Fatalf("[ERROR] opening CSV file: %s", err) - } - validHeader := false - - aCSV := csv.NewReader(csvFile) - aCSV.FieldsPerRecord = -1 - inFile, cErr := aCSV.ReadAll() - if cErr != nil { - fmt.Printf("[ERROR] reading CSV file: %s", cErr) - log.Fatalf("[ERROR] reading CSV file: %s", cErr) - } - actions := make(map[string][]ROTAction) - fieldMap := make(map[int]string) - for i, field := range AuditHeader { - fieldMap[i] = field - } - for ri, row := range inFile { - if strings.EqualFold(strings.Join(row, ","), strings.Join(AuditHeader, ",")) { - validHeader = true - continue // Skip header - } - if !validHeader { - fmt.Printf("[ERROR] Invalid header in stores file. Expected: %s", strings.Join(AuditHeader, ",")) - log.Fatalf("[ERROR] Stores CSV file is missing a valid header") - } - action := make(map[string]interface{}) - - for i, field := range row { - fieldInt, iErr := strconv.Atoi(field) - if iErr != nil { - log.Printf("[DEBUG] Field %s is not an int", field) - action[fieldMap[i]] = field - } else { - action[fieldMap[i]] = fieldInt - } - - } - - addCertStr, aOk := action["AddCert"].(string) - if !aOk { - addCertStr = "" - } - addCert, acErr := strconv.ParseBool(addCertStr) - if acErr != nil { - addCert = false - } - - removeCertStr, rOk := action["RemoveCert"].(string) - if !rOk { - removeCertStr = "" - } - removeCert, rcErr := strconv.ParseBool(removeCertStr) - if rcErr != nil { - removeCert = false - } - - sType, sOk := action["StoreType"].(string) - if !sOk { - sType = "" - } + outputFilePath, _ := cmd.Flags().GetString("outputFilePath") - sPath, pOk := action["Path"].(string) - if !pOk { - sPath = "" - } + // Debug + expEnabled checks + isExperimental := false + debugErr := warnExperimentalFeature(expEnabled, isExperimental) + if debugErr != nil { + return debugErr + } + informDebug(debugFlag) - tp, tpOk := action["Thumbprint"].(string) - if !tpOk { - tp = "" - } - cid, cidOk := action["CertID"].(int) - if !cidOk { - cid = -1 - } + trustCriteria.MinCerts = minCerts + trustCriteria.MaxKeys = maxKeys + trustCriteria.MaxLeaf = maxLeaves - if !tpOk && !cidOk { - fmt.Printf("[ERROR] Missing Thumbprint or CertID for row %d in report file %s", ri, reportFile) - log.Printf("[ERROR] Invalid action: %v", action) - continue - } + authConfig := createAuthConfigFromParams(kfcHostName, kfcUsername, kfcPassword, kfcDomain, kfcAPIPath) - sId, sIdOk := action["StoreID"].(string) - if !sIdOk { - fmt.Printf("[ERROR] Missing StoreID for row %d in report file %s", ri, reportFile) - log.Printf("[ERROR] Invalid action: %v", action) - continue - } - if cid == -1 && tp != "" { - certLookupReq := api.GetCertificateContextArgs{ - IncludeMetadata: boolToPointer(true), - IncludeLocations: boolToPointer(true), - CollectionId: nil, - Thumbprint: tp, - Id: 0, - } - certLookup, err := kfClient.GetCertificateContext(&certLookupReq) - if err != nil { - fmt.Printf("[ERROR] looking up certificate %s: %s\n", tp, err) - log.Printf("[ERROR] looking up cert: %s\n%v", tp, err) - continue - } - cid = certLookup.Id - } + kfClient, clErr := initClient(configFile, profile, "", "", noPrompt, authConfig, false) + if clErr != nil { + log.Error().Err(clErr).Msg("Error initializing Keyfactor client") + return clErr + } - a := ROTAction{ - StoreID: sId, - StoreType: sType, - StorePath: sPath, - Thumbprint: tp, - CertID: cid, - AddCert: addCert, - RemoveCert: removeCert, - } + log.Info().Str("stores_file", storesFile). + Str("add_file", addRootsFile). + Str("remove_file", removeRootsFile). + Bool("dry_run", dryRun). + Msg("Performing root of trust reconciliation") - actions[a.Thumbprint] = append(actions[a.Thumbprint], a) - } - if len(actions) == 0 { - fmt.Println("No reconciliation actions to take, root stores are up-to-date. Exiting.") - return - } - rErr := reconcileRoots(actions, kfClient, reportFile, dryRun) - if rErr != nil { - fmt.Printf("[ERROR] reconciling roots: %s", rErr) - log.Fatalf("[ERROR] reconciling roots: %s", rErr) + // Parse existing audit report + if isCSV && reportFile != "" { + err := processCSVReportFile(reportFile, kfClient, dryRun) + if err != nil { + log.Error().Err(err).Msg("Error processing audit report") + return err } - defer csvFile.Close() - - orchsURL := fmt.Sprintf("https://%s/Keyfactor/Portal/AgentJobStatus/Index", kfClient.Hostname) - - fmt.Println(fmt.Sprintf("Reconciliation completed. Check orchestrator jobs for details. %s", orchsURL)) + return nil } else { - // Read in the stores CSV - csvFile, _ := os.Open(storesFile) - reader := csv.NewReader(bufio.NewReader(csvFile)) - storeEntries, _ := reader.ReadAll() - var stores = make(map[string]StoreCSVEntry) - for i, entry := range storeEntries { - if entry[0] == "StoreID" || entry[0] == "StoreId" || i == 0 { - continue // Skip header - } - apiResp, err := kfClient.GetCertificateStoreByID(entry[0]) - if err != nil { - log.Printf("[ERROR] getting cert store: %s", err) - lookupFailures = append(lookupFailures, entry[0]) - continue - } - inventory, invErr := kfClient.GetCertStoreInventoryV1(entry[0]) - if invErr != nil { - log.Fatalf("[ERROR] getting cert store inventory: %s", invErr) - } - - if !isRootStore(apiResp, inventory, minCerts, maxLeaves, maxKeys) { - log.Printf("[WARN] Store %s is not a root store", apiResp.Id) - continue - } else { - log.Printf("[INFO] Store %s is a root store", apiResp.Id) - } - - stores[entry[0]] = StoreCSVEntry{ - ID: entry[0], - Type: entry[1], - Machine: entry[2], - Path: entry[3], - Thumbprints: make(map[string]bool), - Serials: make(map[string]bool), - Ids: make(map[int]bool), - } - for _, cert := range *inventory { - thumb := cert.Thumbprints - for t, v := range thumb { - stores[entry[0]].Thumbprints[t] = v - } - for t, v := range cert.Serials { - stores[entry[0]].Serials[t] = v - } - for t, v := range cert.Ids { - stores[entry[0]].Ids[t] = v - } - } - - } - if len(lookupFailures) > 0 { - fmt.Printf("[ERROR] the following stores were not found: %s", strings.Join(lookupFailures, ",")) - log.Fatalf("[ERROR] the following stores were not found: %s", strings.Join(lookupFailures, ",")) - } - if len(stores) == 0 { - fmt.Println("[ERROR] no root stores found. Exiting.") - log.Fatalf("[ERROR] No root stores found. Exiting.") - } - // Read in the add addCerts CSV - var certsToAdd = make(map[string]string) - if addRootsFile != "" { - certsToAdd, _ = readCertsFile(addRootsFile, kfClient) - log.Printf("[DEBUG] ROT add certs called") - } else { - log.Printf("[INFO] No addCerts file specified") - } - - // Read in the remove removeCerts CSV - var certsToRemove = make(map[string]string) - if removeRootsFile != "" { - certsToRemove, _ = readCertsFile(removeRootsFile, kfClient) - log.Printf("[DEBUG] ROT remove certs called") - } else { - log.Printf("[DEBUG] No removeCerts file specified") - } - _, actions, err := generateAuditReport(certsToAdd, certsToRemove, stores, outpath, kfClient) + log.Debug(). + Str("stores_file", storesFile). + Str("add_file", addRootsFile). + Str("remove_file", removeRootsFile). + Str("report_file", reportFile). + Bool("dry_run", dryRun). + Msg(fmt.Sprintf(DebugFuncCall, "processFromStoresAndCertFiles")) + err := processFromStoresAndCertFiles( + storesFile, + addRootsFile, + removeRootsFile, + reportFile, + outputFilePath, + minCerts, + maxLeaves, + maxKeys, + kfClient, + dryRun, + ) if err != nil { - log.Fatalf("[ERROR] generating audit report: %s", err) - } - if len(actions) == 0 { - fmt.Println("No reconciliation actions to take, root stores are up-to-date. Exiting.") - return - } - rErr := reconcileRoots(actions, kfClient, reportFile, dryRun) - if rErr != nil { - fmt.Printf("[ERROR] reconciling roots: %s", rErr) - log.Fatalf("[ERROR] reconciling roots: %s", rErr) + log.Error().Err(err).Msg("Error processing from stores file") + return err } - if lookupFailures != nil { - fmt.Printf("The following stores could not be found: %s", strings.Join(lookupFailures, ",")) - } - orchsURL := fmt.Sprintf("https://%s/Keyfactor/Portal/AgentJobStatus/Index", kfClient.Hostname) - - fmt.Println(fmt.Sprintf("Reconciliation completed. Check orchestrator jobs for details. %s", orchsURL)) } + log.Debug().Str("report_file", reportFile). + Str("outputFilePath", outputFilePath).Msg("Reconciliation report generated successfully") + log.Debug().Msg(fmt.Sprintf(DebugFuncExit, "reconcileRoots")) + return nil }, - RunE: nil, + Run: nil, PostRun: nil, PostRunE: nil, PersistentPostRun: nil, @@ -866,43 +2961,56 @@ the utility will first generate an audit report and then execute the add/remove PersistentPreRunE: nil, PreRun: nil, PreRunE: nil, - Run: func(cmd *cobra.Command, args []string) { - // Global flags - debugFlag, _ := cmd.Flags().GetBool("debugFlag") - configFile, _ := cmd.Flags().GetString("config") - noPrompt, _ := cmd.Flags().GetBool("no-prompt") - profile, _ := cmd.Flags().GetString("profile") - - kfcUsername, _ := cmd.Flags().GetString("kfcUsername") - kfcPassword, _ := cmd.Flags().GetString("kfcPassword") - kfcDomain, _ := cmd.Flags().GetString("kfcDomain") - - authConfig := createAuthConfigFromParams(kfcHostName, kfcUsername, kfcPassword, kfcDomain, kfcAPIPath) - - debugModeEnabled := checkDebug(debugFlag) - log.Println("Debug mode enabled: ", debugModeEnabled) + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true + // Specific Flags templateType, _ := cmd.Flags().GetString("type") format, _ := cmd.Flags().GetString("format") - outPath, _ := cmd.Flags().GetString("outpath") + outputFilePath, _ := cmd.Flags().GetString("outputFilePath") storeType, _ := cmd.Flags().GetStringSlice("store-type") containerName, _ := cmd.Flags().GetStringSlice("container-name") collection, _ := cmd.Flags().GetStringSlice("collection") subjectName, _ := cmd.Flags().GetStringSlice("cn") + + // Debug + expEnabled checks + isExperimental := false + debugErr := warnExperimentalFeature(expEnabled, isExperimental) + if debugErr != nil { + return debugErr + } + informDebug(debugFlag) + + // Authenticate + authConfig := createAuthConfigFromParams(kfcHostName, kfcUsername, kfcPassword, kfcDomain, kfcAPIPath) + kfClient, clErr := initClient( + configFile, profile, providerType, providerProfile, noPrompt, authConfig, + false, + ) + if clErr != nil { + log.Error().Err(clErr).Msg("Error initializing Keyfactor client") + return clErr + } + stID := -1 var storeData []api.GetCertificateStoreResponse var csvStoreData [][]string var csvCertData [][]string var rowLookup = make(map[string]bool) - kfClient, cErr := initClient(configFile, profile, "", "", noPrompt, authConfig, false) + var errs []error + if len(storeType) != 0 { + log.Info().Strs("store_types", storeType).Msg("Processing store types") for _, s := range storeType { - if cErr != nil { - log.Fatalf("[ERROR] creating client: %s", cErr) - } + log.Debug().Str("store_type", s).Msg("Processing store type") var sType *api.CertificateStoreType var stErr error if s == "all" { + log.Info(). + Str("store_type", s). + Msg("Getting all store types") + + log.Trace().Msg("Creating empty store type for 'all' option") sType = &api.CertificateStoreType{ Name: "", ShortName: "", @@ -932,49 +3040,84 @@ the utility will first generate an audit report and then execute the add/remove } else { // check if s is an int sInt, err := strconv.Atoi(s) + if err == nil { + log.Debug().Str("store_type", s).Msg("Getting store type by ID") sType, stErr = kfClient.GetCertificateStoreTypeById(sInt) } else { + log.Debug().Str("store_type", s).Msg("Getting store type by name") sType, stErr = kfClient.GetCertificateStoreTypeByName(s) } if stErr != nil { - fmt.Printf("[ERROR] getting store type '%s'. %s\n", s, stErr) + //fmt.Printf("unable to get store type '%s' from Keyfactor Command: %s\n", s, stErr) + errs = append(errs, stErr) continue } stID = sType.StoreType // This is the template type ID } if stID >= 0 || s == "all" { - log.Printf("[DEBUG] Store type ID: %d\n", stID) + log.Debug().Str("store_type", s). + Int("store_type_id", stID). + Msg("Getting certificate stores") params := make(map[string]interface{}) + if stID >= 0 { + params["StoreType"] = stID + } + + log.Debug().Str("store_type", s).Msg("Getting certificate stores") stores, sErr := kfClient.ListCertificateStores(¶ms) if sErr != nil { - fmt.Printf("[ERROR] getting certificate stores of type '%s': %s\n", s, sErr) - log.Fatalf("[ERROR] getting certificate stores of type '%s': %s", s, sErr) + log.Error().Err(sErr). + Str("store_type", s). + Int("store_type_id", stID). + Interface("params", params). + Msg("Error getting certificate stores") + return sErr + } + if stores == nil { + log.Warn().Str("store_type", s).Msg("No stores found") + errs = append(errs, fmt.Errorf("no stores found for store type: %s", s)) + continue } for _, store := range *stores { + log.Trace().Str("store_type", s).Msg("Processing stores of type") if store.CertStoreType == stID || s == "all" { storeData = append(storeData, store) if !rowLookup[store.Id] { + log.Trace().Str("store_type", s). + Str("store_id", store.Id). + Msg("Constructing CSV row") lineData := []string{ //"StoreID", "StoreType", "StoreMachine", "StorePath", "ContainerId" - store.Id, fmt.Sprintf("%s", sType.ShortName), store.ClientMachine, store.StorePath, fmt.Sprintf("%d", store.ContainerId), store.ContainerName, getCurrentTime(""), + store.Id, + fmt.Sprintf("%s", sType.ShortName), + store.ClientMachine, + store.StorePath, + fmt.Sprintf("%d", store.ContainerId), + store.ContainerName, + getCurrentTime(""), } + log.Trace().Strs("line_data", lineData).Msg("Adding line data to CSV data") csvStoreData = append(csvStoreData, lineData) rowLookup[store.Id] = true } } } + } else { + errMsg := fmt.Errorf("Invalid input, must provide a store type of specify 'all'") + log.Error().Err(errMsg).Msg("Invalid input") + if len(errs) == 0 { + errs = append(errs, errMsg) + } } } - fmt.Println("Done") + log.Info().Strs("store_types", storeType).Msg("Store types processed") } + if len(containerName) != 0 { + log.Info().Strs("container_names", containerName).Msg("Processing container names") for _, c := range containerName { - - if cErr != nil { - log.Fatalf("[ERROR] creating client: %s", cErr) - } cStoresResp, scErr := kfClient.GetCertificateStoreByContainerID(c) if scErr != nil { fmt.Printf("[ERROR] getting store container: %s\n", scErr) @@ -990,7 +3133,13 @@ the utility will first generate an audit report and then execute the add/remove if !rowLookup[store.Id] { lineData := []string{ // "StoreID", "StoreType", "StoreMachine", "StorePath", "ContainerId" - store.Id, sType.ShortName, store.ClientMachine, store.StorePath, fmt.Sprintf("%d", store.ContainerId), store.ContainerName, getCurrentTime(""), + store.Id, + sType.ShortName, + store.ClientMachine, + store.StorePath, + fmt.Sprintf("%d", store.ContainerId), + store.ContainerName, + getCurrentTime(""), } csvStoreData = append(csvStoreData, lineData) rowLookup[store.Id] = true @@ -999,13 +3148,11 @@ the utility will first generate an audit report and then execute the add/remove } } + log.Info().Strs("container_names", containerName).Msg("Container names processed") } if len(collection) != 0 { + log.Info().Strs("collections", collection).Msg("Processing collections") for _, c := range collection { - if cErr != nil { - fmt.Println("[ERROR] connecting to Keyfactor. Please check your configuration and try again.") - log.Fatalf("[ERROR] creating client: %s", cErr) - } q := make(map[string]string) q["collection"] = c certsResp, scErr := kfClient.ListCertificates(q) @@ -1017,7 +3164,12 @@ the utility will first generate an audit report and then execute the add/remove if !rowLookup[cert.Thumbprint] { lineData := []string{ // "Thumbprint", "SubjectName", "Issuer", "CertID", "Locations", "LastQueriedDate" - cert.Thumbprint, cert.IssuedCN, cert.IssuerDN, fmt.Sprintf("%d", cert.Id), fmt.Sprintf("%v", cert.Locations), getCurrentTime(""), + cert.Thumbprint, + cert.IssuedCN, + cert.IssuerDN, + fmt.Sprintf("%d", cert.Id), + fmt.Sprintf("%v", cert.Locations), + getCurrentTime(""), } csvCertData = append(csvCertData, lineData) rowLookup[cert.Thumbprint] = true @@ -1026,30 +3178,61 @@ the utility will first generate an audit report and then execute the add/remove } } + log.Info().Strs("collections", collection).Msg("Collections processed") } if len(subjectName) != 0 { + log.Info().Strs("subject_names", subjectName).Msg("Processing subject names") for _, s := range subjectName { - if cErr != nil { - fmt.Println("[ERROR] connecting to Keyfactor. Please check your configuration and try again.") - log.Fatalf("[ERROR] creating client: %s", cErr) - } q := make(map[string]string) q["subject"] = s + log.Debug().Str("subject_name", s).Msg("Getting certificates by subject name") certsResp, scErr := kfClient.ListCertificates(q) if scErr != nil { - fmt.Printf("No certificates found with CN: %s\n", scErr) + log.Error().Err(scErr).Str("subject_name", s).Msg("Error listing certificates by subject name") + errs = append(errs, scErr) } + if certsResp != nil { + log.Debug().Str( + "subject_name", + s, + ).Msg("processing certificates returned from Keyfactor Command") for _, cert := range certsResp { + log.Trace().Interface("cert", cert).Msg("Processing certificate") if !rowLookup[cert.Thumbprint] { + log.Trace(). + Str("thumbprint", cert.Thumbprint). + Str("subject_name", cert.IssuedCN). + Str("not_before", cert.NotBefore). + Str("not_after", cert.NotAfter). + Msg("Adding certificate to CSV data") locationsFormatted := "" + + log.Debug().Str( + "thumbprint", + cert.Thumbprint, + ).Msg("Iterating over certificate locations") for _, loc := range cert.Locations { + log.Trace().Str("thumbprint", cert.Thumbprint).Str( + "location", + loc.StoreMachine, + ).Msg("Processing location") locationsFormatted += fmt.Sprintf("%s:%s\n", loc.StoreMachine, loc.StorePath) } + log.Trace().Str("thumbprint", cert.Thumbprint).Str( + "locations", + locationsFormatted, + ).Msg("Constructing CSV line data") lineData := []string{ // "Thumbprint", "SubjectName", "Issuer", "CertID", "Locations", "LastQueriedDate" - cert.Thumbprint, cert.IssuedCN, cert.IssuerDN, fmt.Sprintf("%d", cert.Id), locationsFormatted, getCurrentTime(""), + cert.Thumbprint, + cert.IssuedCN, + cert.IssuerDN, + fmt.Sprintf("%d", cert.Id), + locationsFormatted, + getCurrentTime(""), } + log.Trace().Strs("line_data", lineData).Msg("Adding line data to CSV data") csvCertData = append(csvCertData, lineData) rowLookup[cert.Thumbprint] = true } @@ -1061,51 +3244,75 @@ the utility will first generate an audit report and then execute the add/remove // Create CSV template file var filePath string - if outPath != "" { - filePath = outPath + if outputFilePath != "" { + filePath = outputFilePath } else { filePath = fmt.Sprintf("%s_template.%s", templateType, format) } + log.Info().Str("file_path", filePath).Msg("Creating template file") file, err := os.Create(filePath) if err != nil { - fmt.Printf("[ERROR] creating file: %s", err) - log.Fatal("Cannot create file", err) + log.Error().Err(err).Str("file_path", filePath).Msg("Error creating template file") + return err } switch format { case "csv": + log.Info().Str("file_path", filePath).Msg("Creating CSV writer") writer := csv.NewWriter(file) var data [][]string + log.Debug().Str("template_type", templateType).Msg("Processing template type") switch templateType { case "stores": data = append(data, StoreHeader) if len(csvStoreData) != 0 { data = append(data, csvStoreData...) } + log.Debug().Str("template_type", templateType). + Interface("csv_data", csvStoreData). + Msg("Writing CSV data to file") case "certs": data = append(data, CertHeader) if len(csvCertData) != 0 { data = append(data, csvCertData...) } + log.Debug().Str("template_type", templateType). + Interface("csv_data", csvCertData). + Msg("Writing CSV data to file") case "actions": data = append(data, AuditHeader) + log.Debug().Str("template_type", templateType). + Interface("csv_data", csvCertData). + Msg("Writing CSV data to file") } csvErr := writer.WriteAll(data) if csvErr != nil { - fmt.Println(csvErr) + log.Error().Err(csvErr).Str("file_path", filePath).Msg("Error writing CSV data to file") + errs = append(errs, csvErr) } defer file.Close() case "json": + log.Info().Str("file_path", filePath).Msg("Creating JSON file") + log.Trace().Str("file_path", filePath).Msg("Creating JSON encoder") writer := bufio.NewWriter(file) _, err := writer.WriteString("StoreID,StoreType,StoreMachine,StorePath") if err != nil { - log.Fatal("Cannot write to file", err) + log.Error().Err(err).Str("file_path", filePath).Msg("Error writing JSON data to file") + errs = append(errs, err) } } + if len(errs) != 0 { + log.Error().Errs("errors", errs).Msg("Errors encountered while creating template file") + errMsg := mergeErrsToString(&errs, false) + return fmt.Errorf("errors encountered while creating template file: %s", errMsg) + } fmt.Printf("Template file created at %s.\n", filePath) + log.Info().Str("file_path", filePath).Msg("Template file created") + log.Debug().Msg(fmt.Sprintf(DebugFuncExit, "generateTemplate")) + return nil }, - RunE: nil, + Run: nil, PostRun: nil, PostRunE: nil, PersistentPostRun: nil, @@ -1123,84 +3330,3 @@ the utility will first generate an audit report and then execute the add/remove SuggestionsMinimumDistance: 0, } ) - -func init() { - log.SetFlags(log.LstdFlags | log.Lshortfile) - log.SetOutput(os.Stdout) - var ( - stores string - addCerts string - removeCerts string - minCertsInStore int - maxPrivateKeys int - maxLeaves int - tType = tTypeCerts - outPath string - outputFormat string - inputFile string - storeTypes []string - containerNames []string - collections []string - subjectNames []string - ) - - storesCmd.AddCommand(rotCmd) - - // Root of trust `audit` command - rotCmd.AddCommand(rotAuditCmd) - rotAuditCmd.Flags().StringVarP(&stores, "stores", "s", "", "CSV file containing cert stores to enroll into") - rotAuditCmd.Flags().StringVarP(&addCerts, "add-certs", "a", "", - "CSV file containing cert(s) to enroll into the defined cert stores") - rotAuditCmd.Flags().StringVarP(&removeCerts, "remove-certs", "r", "", - "CSV file containing cert(s) to remove from the defined cert stores") - rotAuditCmd.Flags().IntVarP(&minCertsInStore, "min-certs", "m", -1, - "The minimum number of certs that should be in a store to be considered a 'root' store. If set to `-1` then all stores will be considered.") - rotAuditCmd.Flags().IntVarP(&maxPrivateKeys, "max-keys", "k", -1, - "The max number of private keys that should be in a store to be considered a 'root' store. If set to `-1` then all stores will be considered.") - rotAuditCmd.Flags().IntVarP(&maxLeaves, "max-leaf-certs", "l", -1, - "The max number of non-root-certs that should be in a store to be considered a 'root' store. If set to `-1` then all stores will be considered.") - rotAuditCmd.Flags().BoolP("dry-run", "d", false, "Dry run mode") - rotAuditCmd.Flags().StringVarP(&outPath, "outpath", "o", "", - "Path to write the audit report file to. If not specified, the file will be written to the current directory.") - - // Root of trust `reconcile` command - rotCmd.AddCommand(rotReconcileCmd) - rotReconcileCmd.Flags().StringVarP(&stores, "stores", "s", "", "CSV file containing cert stores to enroll into") - rotReconcileCmd.Flags().StringVarP(&addCerts, "add-certs", "a", "", - "CSV file containing cert(s) to enroll into the defined cert stores") - rotReconcileCmd.Flags().StringVarP(&removeCerts, "remove-certs", "r", "", - "CSV file containing cert(s) to remove from the defined cert stores") - rotReconcileCmd.Flags().IntVarP(&minCertsInStore, "min-certs", "m", -1, - "The minimum number of certs that should be in a store to be considered a 'root' store. If set to `-1` then all stores will be considered.") - rotReconcileCmd.Flags().IntVarP(&maxPrivateKeys, "max-keys", "k", -1, - "The max number of private keys that should be in a store to be considered a 'root' store. If set to `-1` then all stores will be considered.") - rotReconcileCmd.Flags().IntVarP(&maxLeaves, "max-leaf-certs", "l", -1, - "The max number of non-root-certs that should be in a store to be considered a 'root' store. If set to `-1` then all stores will be considered.") - rotReconcileCmd.Flags().BoolP("dry-run", "d", false, "Dry run mode") - rotReconcileCmd.Flags().BoolP("import-csv", "v", false, "Import an audit report file in CSV format.") - rotReconcileCmd.Flags().StringVarP(&inputFile, "input-file", "i", reconcileDefaultFileName, - "Path to a file generated by 'stores rot audit' command.") - rotReconcileCmd.Flags().StringVarP(&outPath, "outpath", "o", "", - "Path to write the audit report file to. If not specified, the file will be written to the current directory.") - //rotReconcileCmd.MarkFlagsRequiredTogether("add-certs", "stores") - //rotReconcileCmd.MarkFlagsRequiredTogether("remove-certs", "stores") - rotReconcileCmd.MarkFlagsMutuallyExclusive("add-certs", "import-csv") - rotReconcileCmd.MarkFlagsMutuallyExclusive("remove-certs", "import-csv") - rotReconcileCmd.MarkFlagsMutuallyExclusive("stores", "import-csv") - - // Root of trust `generate` command - rotCmd.AddCommand(rotGenStoreTemplateCmd) - rotGenStoreTemplateCmd.Flags().StringVarP(&outPath, "outpath", "o", "", - "Path to write the template file to. If not specified, the file will be written to the current directory.") - rotGenStoreTemplateCmd.Flags().StringVarP(&outputFormat, "format", "f", "csv", - "The type of template to generate. Only `csv` is supported at this time.") - rotGenStoreTemplateCmd.Flags().Var(&tType, "type", - `The type of template to generate. Only "certs|stores|actions" are supported at this time.`) - rotGenStoreTemplateCmd.Flags().StringSliceVar(&storeTypes, "store-type", []string{}, "Multi value flag. Attempt to pre-populate the stores template with the certificate stores matching specified store types. If not specified, the template will be empty.") - rotGenStoreTemplateCmd.Flags().StringSliceVar(&containerNames, "container-name", []string{}, "Multi value flag. Attempt to pre-populate the stores template with the certificate stores matching specified container types. If not specified, the template will be empty.") - rotGenStoreTemplateCmd.Flags().StringSliceVar(&subjectNames, "cn", []string{}, "Subject name(s) to pre-populate the 'certs' template with. If not specified, the template will be empty. Does not work with SANs.") - rotGenStoreTemplateCmd.Flags().StringSliceVar(&collections, "collection", []string{}, "Certificate collection name(s) to pre-populate the stores template with. If not specified, the template will be empty.") - - rotGenStoreTemplateCmd.RegisterFlagCompletionFunc("type", templateTypeCompletion) - rotGenStoreTemplateCmd.MarkFlagRequired("type") -} diff --git a/cmd/rot_models.go b/cmd/rot_models.go new file mode 100644 index 0000000..84ed203 --- /dev/null +++ b/cmd/rot_models.go @@ -0,0 +1,203 @@ +package cmd + +import ( + "fmt" + "strings" + + "github.com/Keyfactor/keyfactor-go-client/v2/api" +) + +var ( + AuditHeader = []string{ + "Thumbprint", + "CertID", + "SubjectName", + "Issuer", + "StoreID", + "StoreType", + "Machine", + "Path", + "AddCert", + "RemoveCert", + "Deployed", + "AuditDate", + } + ReconciledAuditHeader = []string{ + "Thumbprint", + "CertID", + "SubjectName", + "Issuer", + "StoreID", + "StoreType", + "Machine", + "Path", + "AddCert", + "RemoveCert", + "Deployed", + "ReconciledDate", + } + StoreHeader = []string{ + "StoreID", + "StoreType", + "StoreMachine", + "StorePath", + "ContainerId", + "ContainerName", + "LastQueriedDate", + } + CertHeader = []string{"CertID", "Thumbprint", "SubjectName", "Issuer", "Alias", "Locations", "LastQueriedDate"} +) + +type ROTStore struct { + StoreID string `json:"StoreID,omitempty"` + StoreType string `json:"StoreType,omitempty"` + StoreMachine string `json:"StoreMachine,omitempty"` + StorePath string `json:"StorePath,omitempty"` + ContainerId string `json:"ContainerId,omitempty"` + ContainerName string `json:"ContainerName,omitempty"` + LastQueried string `json:"LastQueried,omitempty"` +} + +func (r *ROTStore) toCSV() string { + return fmt.Sprintf( + "%s,%s,%s,%s,%s,%s,%s", + r.StoreID, + r.StoreType, + r.StoreMachine, + r.StorePath, + r.ContainerId, + r.ContainerName, + r.LastQueried, + ) +} + +type StoreCSVEntry struct { + ID string `json:"id"` + Type string `json:"type"` + Machine string `json:"address"` + Path string `json:"path"` + Thumbprints map[string]bool `json:"thumbprints,omitempty"` + Serials map[string]bool `json:"serials,omitempty"` + Ids map[int]bool `json:"ids,omitempty"` +} +type ROTCert struct { + ThumbPrint string `json:"thumbprint"` + ID int `json:"id"` + CN string `json:"cn"` + SANs []string `json:"sans"` + Alias string `json:"alias"` + Locations []api.CertificateLocations `json:"locations"` + Issuer string `json:"issuer"` +} + +func (r *ROTCert) toCSV() string { + subjectName := strings.Join(r.SANs, ";") + // check if CN is not in subject_name + if !strings.Contains(subjectName, r.CN) { + if subjectName != "" { + subjectName = fmt.Sprintf("%s;%s", r.CN, subjectName) + } else { + subjectName = r.CN + } + } + + return fmt.Sprintf( + "%d,%s,%s,%s,%s,%v,%s", + r.ID, + r.ThumbPrint, + subjectName, + r.Issuer, + r.Alias, + //strings.Join(r.SANs, ";"), + r.Locations, + getCurrentTime(""), // LastQueriedDate + ) +} + +type ROTAction struct { + Thumbprint string `json:"thumbprint" mapstructure:"Thumbprint"` + StoreAlias string `json:"alias" mapstructure:"Alias;omitempty"` + CertID int `json:"cert_id" mapstructure:"CertID"` + CertDN string `json:"cert_dn" mapstructure:"SubjectName"` + CertSANs string `json:"cert_sans,omitempty" mapstructure:"CertSANs,omitempty"` + Issuer string `json:"issuer" mapstructure:"Issuer"` + StoreID string `json:"store_id" mapstructure:"StoreID"` + StoreType string `json:"store_type" mapstructure:"StoreType"` + Machine string `json:"client_machine" mapstructure:"Machine"` + StorePath string `json:"store_path" mapstructure:"Path"` + Alias string `json:"alias" mapstructure:"Alias,omitempty"` + AddCert bool `json:"add" mapstructure:"AddCert"` + RemoveCert bool `json:"remove" mapstructure:"RemoveCert"` + Deployed bool `json:"deployed" mapstructure:"Deployed"` + AuditDate string `json:"audit_date" mapstructure:"AuditDate"` +} + +func (r *ROTAction) getAuditHeaderMap() map[string]int { + headerMap := make(map[string]int) + for i, h := range AuditHeader { + headerMap[h] = i + } + return headerMap +} + +func (r *ROTAction) getAuditCSVHeader() []string { + //return []string{ + // "Thumbprint", + // "CertID", + // "SubjectName", + // "Issuer", + // "StoreID", + // "StoreType", + // "Machine", + // "Path", + // "AddCert", + // "RemoveCert", + // "Deployed", + // "AuditDate", + //} + return AuditHeader +} + +func (r *ROTAction) getReconciledCSVHeader() []string { + //return []string{ + // "Thumbprint", + // "CertID", + // "SubjectName", + // "Issuer", + // "StoreID", + // "StoreType", + // "Machine", + // "Path", + // "AddCert", + // "RemoveCert", + // "Deployed", + // "AuditDate", + //} + return ReconciledAuditHeader +} + +func (r *ROTAction) toCSV(rowType string) string { + + switch rowType { + case "audit": + headerMap := r.getAuditHeaderMap() + + //create csv row with fields arranged in order of the header map + row := make([]string, len(AuditHeader)) + row[headerMap["Thumbprint"]] = r.Thumbprint + row[headerMap["CertID"]] = fmt.Sprintf("%d", r.CertID) + row[headerMap["SubjectName"]] = r.CertDN + row[headerMap["Issuer"]] = r.Issuer + row[headerMap["StoreID"]] = r.StoreID + row[headerMap["StoreType"]] = r.StoreType + row[headerMap["Machine"]] = "" + row[headerMap["Path"]] = r.StorePath + row[headerMap["AddCert"]] = fmt.Sprintf("%t", r.AddCert) + row[headerMap["RemoveCert"]] = fmt.Sprintf("%t", r.RemoveCert) + row[headerMap["Deployed"]] = "" + row[headerMap["AuditDate"]] = getCurrentTime("") + + return strings.Join(row, ",") + } + return "invalid format" +} diff --git a/cmd/storeTypes.go b/cmd/storeTypes.go index 3d8b56b..e9ef5b2 100644 --- a/cmd/storeTypes.go +++ b/cmd/storeTypes.go @@ -17,15 +17,16 @@ package cmd import ( "encoding/json" "fmt" - "github.com/AlecAivazis/survey/v2" - "github.com/Keyfactor/keyfactor-go-client/v2/api" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" "io" "net/http" "os" "sort" "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/Keyfactor/keyfactor-go-client/v2/api" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" ) var storeTypesCmd = &cobra.Command{ @@ -292,7 +293,10 @@ var storesTypeDeleteCmd = &cobra.Command{ } if dryRun { - outputResult(fmt.Sprintf("dry run delete called on certificate store type (%v) with ID: %d", st, id), outputFormat) + outputResult( + fmt.Sprintf("dry run delete called on certificate store type (%v) with ID: %d", st, id), + outputFormat, + ) } else { log.Debug().Interface("storeType", st). Int("id", id). @@ -410,7 +414,12 @@ func getStoreTypesInternet(gitRef string) (map[string]interface{}, error) { //resp, err := http.Get("https://raw.githubusercontent.com/keyfactor/kfutil/main/store_types.json") //resp, err := http.Get("https://raw.githubusercontent.com/keyfactor/kfctl/master/storetypes/storetypes.json") - resp, rErr := http.Get(fmt.Sprintf("https://raw.githubusercontent.com/Keyfactor/kfutil/%s/store_types.json", gitRef)) + resp, rErr := http.Get( + fmt.Sprintf( + "https://raw.githubusercontent.com/Keyfactor/kfutil/%s/store_types.json", + gitRef, + ), + ) if rErr != nil { return nil, rErr } @@ -434,6 +443,10 @@ func getStoreTypesInternet(gitRef string) (map[string]interface{}, error) { return result2, nil } +//func getStoreTypesFromCommand(kfClient *api.Client) (map[string]interface{}, error) { +// +//} + func getValidStoreTypes(fp string, gitRef string) []string { validStoreTypes, rErr := readStoreTypesConfig(fp, gitRef) if rErr != nil { @@ -489,7 +502,13 @@ func init() { // GET store type templates storeTypesCmd.AddCommand(fetchStoreTypesCmd) - fetchStoreTypesCmd.Flags().StringVarP(&gitRef, FlagGitRef, "b", "main", "The git branch or tag to reference when pulling store-types from the internet.") + fetchStoreTypesCmd.Flags().StringVarP( + &gitRef, + FlagGitRef, + "b", + "main", + "The git branch or tag to reference when pulling store-types from the internet.", + ) // LIST command storeTypesCmd.AddCommand(storesTypesListCmd) @@ -504,10 +523,28 @@ func init() { var storeTypeName string var storeTypeID int storeTypesCmd.AddCommand(storesTypeCreateCmd) - storesTypeCreateCmd.Flags().StringVarP(&storeTypeName, "name", "n", "", "Short name of the certificate store type to get. Valid choices are: "+validTypesString) + storesTypeCreateCmd.Flags().StringVarP( + &storeTypeName, + "name", + "n", + "", + "Short name of the certificate store type to get. Valid choices are: "+validTypesString, + ) storesTypeCreateCmd.Flags().BoolVarP(&listValidStoreTypes, "list", "l", false, "List valid store types.") - storesTypeCreateCmd.Flags().StringVarP(&filePath, "from-file", "f", "", "Path to a JSON file containing certificate store type data for a single store.") - storesTypeCreateCmd.Flags().StringVarP(&gitRef, FlagGitRef, "b", "main", "The git branch or tag to reference when pulling store-types from the internet.") + storesTypeCreateCmd.Flags().StringVarP( + &filePath, + "from-file", + "f", + "", + "Path to a JSON file containing certificate store type data for a single store.", + ) + storesTypeCreateCmd.Flags().StringVarP( + &gitRef, + FlagGitRef, + "b", + "main", + "The git branch or tag to reference when pulling store-types from the internet.", + ) storesTypeCreateCmd.Flags().BoolVarP(&createAll, "all", "a", false, "Create all store types.") // UPDATE command @@ -519,7 +556,13 @@ func init() { var dryRun bool storeTypesCmd.AddCommand(storesTypeDeleteCmd) storesTypeDeleteCmd.Flags().IntVarP(&storeTypeID, "id", "i", -1, "ID of the certificate store type to delete.") - storesTypeDeleteCmd.Flags().StringVarP(&storeTypeName, "name", "n", "", "Name of the certificate store type to delete.") + storesTypeDeleteCmd.Flags().StringVarP( + &storeTypeName, + "name", + "n", + "", + "Name of the certificate store type to delete.", + ) storesTypeDeleteCmd.Flags().BoolVarP(&dryRun, "dry-run", "t", false, "Specifies whether to perform a dry run.") storesTypeDeleteCmd.MarkFlagsMutuallyExclusive("id", "name") storesTypeDeleteCmd.Flags().BoolVarP(&deleteAll, "all", "a", false, "Delete all store types.") diff --git a/go.mod b/go.mod index eeaf731..28aca46 100644 --- a/go.mod +++ b/go.mod @@ -4,45 +4,45 @@ go 1.21 require ( github.com/AlecAivazis/survey/v2 v2.3.7 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2 - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2 github.com/Jeffail/gabs v1.4.0 github.com/Keyfactor/keyfactor-go-client-sdk v1.0.2 - github.com/Keyfactor/keyfactor-go-client/v2 v2.2.7 + github.com/Keyfactor/keyfactor-go-client/v2 v2.2.8 github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 github.com/creack/pty v1.1.21 github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 github.com/joho/godotenv v1.5.1 - github.com/rs/zerolog v1.31.0 + github.com/rs/zerolog v1.32.0 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.8.4 - golang.org/x/crypto v0.19.0 + github.com/stretchr/testify v1.9.0 + golang.org/x/crypto v0.23.0 gopkg.in/yaml.v3 v3.0.1 //github.com/google/go-cmp/cmp v0.5.9 ) require ( github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/golang-jwt/jwt/v5 v5.0.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect - github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spbsoluble/go-pkcs12 v0.3.3 // indirect go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect - golang.org/x/net v0.20.0 // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/term v0.17.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/term v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect ) diff --git a/go.sum b/go.sum index f0537dc..be3670e 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,21 @@ github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2 h1:c4k2FIYIh4xtwqrQwV0Ct1v5+ehlNXj5NI/MWVsiTkQ= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2/go.mod h1:5FDJtLEO/GxwNgUxbwrY3LP0pEoThTQJtk2oysdXHxM= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 h1:BMAjVKJM0U/CYF27gA0ZMmXGkOcvfFtD0oHVZ1TIPRI= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2 h1:FDif4R1+UUR+00q6wquyX90K7A8dN+R5E8GEadoP7sU= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2/go.mod h1:aiYBYui4BJ/BJCAIKs92XiPyQfTaBWqvHujDwKb6CBU= github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 h1:LqbJ/WzJUwBf8UiaSzgX7aMclParm9/5Vgp+TY51uBQ= github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc= -github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 h1:WpB/QDNLpMw72xHJc34BNNykqSOeEJDAWkhf0u12/Jk= -github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/Jeffail/gabs v1.4.0 h1://5fYRRTq1edjfIrQGvdkcd22pkYUrHZ5YC/H2GJVAo= github.com/Jeffail/gabs v1.4.0/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc= github.com/Keyfactor/keyfactor-go-client v1.4.3 h1:CmGvWcuIbDRFM0PfYOQH6UdtAgplvZBpU++KTU8iseg= github.com/Keyfactor/keyfactor-go-client v1.4.3/go.mod h1:3ZymLNCaSazglcuYeNfm9nrzn22wcwLjIWURrnUygBo= github.com/Keyfactor/keyfactor-go-client-sdk v1.0.2 h1:caLlzFCz2L4Dth/9wh+VlypFATmOMmCSQkCPKOKMxw8= github.com/Keyfactor/keyfactor-go-client-sdk v1.0.2/go.mod h1:Z5pSk8YFGXHbKeQ1wTzVN8A4P/fZmtAwqu3NgBHbDOs= -github.com/Keyfactor/keyfactor-go-client/v2 v2.2.7 h1:fHZF5lDEWKQEI8QOPeseG/y9Bd4h2DhOiUWkNx+rKJU= -github.com/Keyfactor/keyfactor-go-client/v2 v2.2.7/go.mod h1:3mfxdcwntB532QIATokBEkBCH0eXN2G/cdMZtu9NwNg= +github.com/Keyfactor/keyfactor-go-client/v2 v2.2.8 h1:eIcdz8XwmoPlRPnAZMhp3/qIXR+pBGSzS3MTFnApbF0= +github.com/Keyfactor/keyfactor-go-client/v2 v2.2.8/go.mod h1:YRCG/SbM3wshb00YOe6hisKTRUSaCJ6oIqRBT9y652E= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -30,8 +30,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= -github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -52,18 +52,20 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= -github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= +github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spbsoluble/go-pkcs12 v0.3.3 h1:3nh7IKn16RDpmrSMtOu1JvbB0XHYq1j+IsICdU1c7J4= @@ -74,45 +76,45 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/main.go b/main.go index 69f352b..5c6f001 100644 --- a/main.go +++ b/main.go @@ -15,16 +15,24 @@ package main import ( + "os" + "os/signal" + "syscall" + "kfutil/cmd" ) func main() { - //var docsFlag bool - //flag.BoolVar(&docsFlag, "makedocs", false, "Create markdown docs.") - //flag.Parse() - //if docsFlag { - // docs() - // os.Exit(0) - //} + // Set up a signal channel for SIGINT + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT) + + // Start a goroutine to listen for signals + go func() { + <-sigChan + // Handle SIGINT signal + os.Exit(1) + }() + cmd.Execute() } diff --git a/pkg/version/version.go b/pkg/version/version.go index 354b351..04bc4ef 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -11,7 +11,6 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - package version -const VERSION = "1.4.0" +const VERSION = "1.5.0" diff --git a/spec/v2.yml b/spec/v2.yml new file mode 100644 index 0000000..bc1a326 --- /dev/null +++ b/spec/v2.yml @@ -0,0 +1,134 @@ +cli: + name: kfutil + description: Keyfactor command line interface + commands: + - name: certificates + description: Keyfactor certificate APIs and utilities + commands: + - name: list + description: List certificates + flags: + - name: id + type: string + required: false + default: "" + - name: get + description: Get a certificate by ID + flags: + - name: id + type: string + required: true + default: "" + - name: delete + description: Delete a certificate by ID + flags: + - name: id + type: string + required: true + default: "" + - name: login + description: Login to Keyfactor + flags: + - name: username + type: string + required: true + default: "" + - name: password + type: string + required: true + default: "" + - name: logout + description: Logout from Keyfactor + - name: orchs + description: Keyfactor orchestration APIs and utilities + commands: + - name: list + description: List orchestrations + - name: get + description: Get an orchestration by ID + flags: + - name: id + type: string + required: true + default: "" + - name: delete + description: Delete an orchestration by ID + flags: + - name: id + type: string + required: true + default: "" + - name: pam + description: Keyfactor PAM APIs and utilities + commands: + - name: list + description: List PAM types + - name: create + description: Create a PAM type + flags: + - name: from-file + type: string + required: true + default: "" + - name: name + type: string + required: true + default: "" + - name: repo + type: string + required: false + default: "" + - name: branch + type: string + required: false + default: "main" + - name: stores + description: Keyfactor certificate stores APIs and utilities + commands: + - name: list + description: List certificate stores + - name: get + description: Get a certificate store by ID + flags: + - name: id + type: string + required: true + default: "" + - name: delete + description: Delete a certificate store by ID + flags: + - name: id + type: string + required: true + default: "" + - name: store-types + description: Keyfactor certificate store types APIs and utilities + commands: + - name: list + description: List certificate store types + - name: create + description: Create a new certificate store type in Keyfactor + flags: + - name: name + type: string + required: true + default: "" + - name: from-file + type: string + required: false + default: "" + - name: delete + description: Delete a specific store type by name or ID + flags: + - name: id + type: int + required: false + default: -1 + - name: name + type: string + required: false + default: "" + - name: status + description: List the status of Keyfactor services + - name: version + description: Shows version of kfutil \ No newline at end of file diff --git a/spec/v3-vn.yml b/spec/v3-vn.yml new file mode 100644 index 0000000..e2f91be --- /dev/null +++ b/spec/v3-vn.yml @@ -0,0 +1,129 @@ +cli: + name: kfutil + description: Keyfactor command line interface + commands: + - name: get + description: Get operations + commands: + - name: certificates + description: Get a certificate by ID + flags: + - name: id + type: string + required: true + default: "" + - name: orchs + description: Get an orchestration by ID + flags: + - name: id + type: string + required: true + default: "" + - name: stores + description: Get a certificate store by ID + flags: + - name: id + type: string + required: true + default: "" + - name: list + description: List operations + commands: + - name: certificates + description: List certificates + flags: + - name: id + type: string + required: false + default: "" + - name: orchs + description: List orchestrations + - name: stores + description: List certificate stores + - name: store-types + description: List certificate store types + - name: delete + description: Delete operations + commands: + - name: certificates + description: Delete a certificate by ID + flags: + - name: id + type: string + required: true + default: "" + - name: orchs + description: Delete an orchestration by ID + flags: + - name: id + type: string + required: true + default: "" + - name: stores + description: Delete a certificate store by ID + flags: + - name: id + type: string + required: true + default: "" + - name: store-types + description: Delete a specific store type by name or ID + flags: + - name: id + type: int + required: false + default: -1 + - name: name + type: string + required: false + default: "" + - name: create + description: Create operations + commands: + - name: pam + description: Create a PAM type + flags: + - name: from-file + type: string + required: true + default: "" + - name: name + type: string + required: true + default: "" + - name: repo + type: string + required: false + default: "" + - name: branch + type: string + required: false + default: "main" + - name: store-types + description: Create a new certificate store type in Keyfactor + flags: + - name: name + type: string + required: true + default: "" + - name: from-file + type: string + required: false + default: "" + - name: login + description: Login to Keyfactor + flags: + - name: username + type: string + required: true + default: "" + - name: password + type: string + required: true + default: "" + - name: logout + description: Logout from Keyfactor + - name: status + description: List the status of Keyfactor services + - name: version + description: Shows version of kfutil \ No newline at end of file