From 5facdd67a6a4cf19401f3ffa94b1d9aceb40c4cb Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 20 Mar 2024 08:22:10 -0700 Subject: [PATCH 1/9] chore(ci): Remove `get-versions` step chore(deps): Update deps Signed-off-by: sbailey <1661003+spbsoluble@users.noreply.github.com> --- .../keyfactor-bootstrap-workflow.yml | 207 ------------------ go.mod | 28 +-- go.sum | 56 ++--- 3 files changed, 43 insertions(+), 248 deletions(-) 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/go.mod b/go.mod index eeaf731..74cf5a6 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.10.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 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.21.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/net v0.22.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index f0537dc..b4e5734 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.10.0 h1:n1DH8TPV4qqPTje2RcUBYwtrTWlabVp4n46+74X2pn4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.10.0/go.mod h1:HDcZnuGbiyppErN6lB+idp4CKhjbc8gwjto6OPpyggM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo= 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,39 +76,39 @@ 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.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 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.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.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.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 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= From c55d95d2bac5a8e079ac4f9258538634b86d5941 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 20 Mar 2024 15:51:41 -0700 Subject: [PATCH 2/9] feat(cli): `rot` enhanced logging and refactor. Signed-off-by: sbailey <1661003+spbsoluble@users.noreply.github.com> --- cmd/constants.go | 2 + cmd/helpers.go | 20 +- cmd/rot.go | 1976 +++++++++++++++++++++++++++++++++++----------- 3 files changed, 1523 insertions(+), 475 deletions(-) diff --git a/cmd/constants.go b/cmd/constants.go index b0caea3..bff54b6 100644 --- a/cmd/constants.go +++ b/cmd/constants.go @@ -29,12 +29,14 @@ const ( DebugFuncEnter = "entered: %s" DebugFuncExit = "exiting: %s" DebugFuncCall = "calling: %s" + ErrMsgEmptyResponse = "empty response received from Keyfactor Command %s" ) var ProviderTypeChoices = []string{ "azid", } var ValidAuthProviders = [2]string{"azure-id", "azid"} +var ErrKfcEmptyResponse = fmt.Errorf("empty response recieved from Keyfactor Command") // Error messages var ( diff --git a/cmd/helpers.go b/cmd/helpers.go index 0a07df8..63b21f7 100644 --- a/cmd/helpers.go +++ b/cmd/helpers.go @@ -19,18 +19,30 @@ 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) string { + var errStr string + if errs == nil || len(*errs) == 0 { + return "" + } + for _, err := range *errs { + errStr += fmt.Sprintf("%s\n", err) + } + return errStr +} + func boolToPointer(b bool) *bool { return &b } diff --git a/cmd/rot.go b/cmd/rot.go index 897e65f..8cb5ae0 100644 --- a/cmd/rot.go +++ b/cmd/rot.go @@ -20,12 +20,13 @@ import ( "encoding/json" "errors" "fmt" - "github.com/Keyfactor/keyfactor-go-client/v2/api" - "github.com/spf13/cobra" - "log" "os" "strconv" "strings" + + "github.com/Keyfactor/keyfactor-go-client/v2/api" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" ) type templateType string @@ -60,10 +61,44 @@ const ( ) 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"} + 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 @@ -95,8 +130,16 @@ func templateTypeCompletion(cmd *cobra.Command, args []string, toComplete string }, 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,73 +147,145 @@ 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) + var errs []error for _, cert := range addCerts { + log.Debug().Str("thumbprint", cert).Msg("Looking up certificate") certLookupReq := api.GetCertificateContextArgs{ IncludeMetadata: boolToPointer(true), IncludeLocations: boolToPointer(true), - CollectionId: nil, + CollectionId: nil, //todo: add CollectionID support Thumbprint: cert, - Id: 0, + Id: 0, //todo: should also allow KFC ID } + log.Debug(). + Str("thumbprint", cert). + 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", cert). + Msg("Error looking up certificate, skipping") + errs = append(errs, err) continue } certID := certLookup.Id certIDStr := strconv.Itoa(certID) + log.Debug().Str("thumbprint", cert).Msg("Iterating over stores") for _, store := range stores { + log.Debug().Str("thumbprint", cert).Str("store_id", store.ID).Msg("Checking if cert is deployed to store") if _, ok := store.Thumbprints[cert]; 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", cert).Str("store_id", store.ID).Msg("Cert is already deployed to store") + row := []string{ + cert, + certIDStr, + certLookup.IssuedDN, + certLookup.IssuerDN, + store.ID, + store.Type, + store.Machine, + store.Path, + "false", + "false", + "true", + getCurrentTime(""), + } + log.Trace().Str("thumbprint", cert).Strs("row", row).Msg("Appending data row") data = append(data, row) + log.Debug().Str("thumbprint", cert).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", cert). + 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", cert). + Str("store_id", store.ID). + Msg("Cert is not deployed to store") + row := []string{ + cert, + certIDStr, + certLookup.IssuedDN, + certLookup.IssuerDN, + store.ID, + store.Type, + store.Machine, + store.Path, + "true", + "false", + "false", + getCurrentTime(""), + } + log.Trace(). + Str("thumbprint", cert). + Strs("row", row). + Msg("Appending data row") data = append(data, row) + log.Debug(). + Str("thumbprint", cert). + 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", cert). + 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", cert). + Msg("Adding 'add' action to actions map") + actions[cert] = append( + actions[cert], ROTAction{ + Thumbprint: cert, + CertID: certID, + StoreID: store.ID, + StoreType: store.Type, + StorePath: store.Path, + AddCert: true, + RemoveCert: false, + }, + ) } } } for _, cert := range removeCerts { + log.Debug().Str("thumbprint", cert).Msg("Looking up certificate to remove") certLookupReq := api.GetCertificateContextArgs{ IncludeMetadata: boolToPointer(true), IncludeLocations: boolToPointer(true), @@ -178,181 +293,476 @@ func generateAuditReport(addCerts map[string]string, removeCerts map[string]stri Thumbprint: cert, Id: 0, } + log.Debug().Str("thumbprint", cert).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", cert). + Msg("Error looking up certificate, unable to remove from store") + errs = append(errs, err) + continue + } else if certLookup == nil { + log.Error(). + Err(ErrKfcEmptyResponse). + Str("thumbprint", cert). + Msg(fmt.Sprintf("%s when looking up certificate", ErrMsgEmptyResponse)) + errs = append(errs, ErrKfcEmptyResponse) continue } + certID := certLookup.Id + log.Trace(). + Str("thumbprint", cert). + Int("cert_id", certID). + Msg("Converting cert ID to string") certIDStr := strconv.Itoa(certID) for _, store := range stores { + storeIdentifier := fmt.Sprintf("%s/%s", store.Machine, store.Path) + log.Debug().Str("thumbprint", cert). + Str("store_id", store.ID). + Str("store_name", storeIdentifier). + Msg("Checking if cert is deployed to store") 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.Info(). + Str("thumbprint", cert). + Str("store_id", store.ID). + Str("store_name", storeIdentifier). + Msg("Cert is deployed to store") + row := []string{ + cert, + certIDStr, + certLookup.IssuedDN, + certLookup.IssuerDN, + store.ID, + store.Type, + store.Machine, + store.Path, + "false", + "true", + "true", + getCurrentTime(""), + } + log.Trace(). + Str("thumbprint", cert). + Strs("row", row). + Msg("Appending data row") data = append(data, row) + log.Debug(). + Str("thumbprint", cert). + 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", cert). + Str("output_file", outputFilePath). + Strs("row", row). + Msg("Error writing row to CSV") + errs = append(errs, wErr) + //todo: continue? } - actions[cert] = append(actions[cert], ROTAction{ - Thumbprint: cert, - CertID: certID, - StoreID: store.ID, - StoreType: store.Type, - StorePath: store.Path, - AddCert: false, - RemoveCert: true, - }) + log.Debug().Str("thumbprint", cert).Msg("Adding remove action to actions map") + 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("")} + log.Info().Str("thumbprint", cert).Str( + "store_id", + store.ID, + ).Msg("Cert is not deployed to store, skipping") + row := []string{ + cert, + certIDStr, + certLookup.IssuedDN, + certLookup.IssuerDN, + store.ID, + store.Type, + store.Machine, + store.Path, + "false", + "false", + "false", + getCurrentTime(""), + } + log.Trace().Str("thumbprint", cert).Strs("row", row).Msg("Appending data row") data = append(data, row) + log.Debug().Str("thumbprint", cert).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", cert).Str("output_file", outputFilePath).Strs( + "row", + row, + ).Msg("Error writing row to CSV") + errs = append(errs, wErr) } } } } + 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") } - fmt.Printf("Audit report written to %s\n", outpath) + 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 + + if len(errs) > 0 { + //combine all errors into single string + errStr := mergeErrsToString(&errs) + 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) + } + 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): %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 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, } + 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") } } 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) + 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 - csvFile, _ := os.Open(certsFilePath) + 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, _ := reader.ReadAll() + + certEntries, rErr := reader.ReadAll() + if rErr != nil { + log.Error().Err(rErr).Str("certs_file", certsFilePath).Msg("Error reading in certs file") + return nil, rErr + } + + 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") for _, entry := range certEntries { + log.Trace().Strs("entry", entry).Msg("Processing row") switch entry[0] { - case "CertID", "thumbprint", "id", "CertId", "Thumbprint": + 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 } + log.Trace().Strs("entry", entry).Msg("Adding thumbprint to map") certs[entry[0]] = entry[0] + 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.CertStoreInventoryV1, minCerts int, maxKeys int, maxLeaf int) bool { +func isRootStore( + st *api.GetCertificateStoreResponse, + invs *[]api.CertStoreInventoryV1, + minCerts int, + maxKeys int, + maxLeaf int, +) bool { + log.Debug().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 + } + + log.Debug().Str("store_id", st.Id).Msg("Iterating over inventory") for _, inv := range *invs { - log.Printf("[DEBUG] inv: %v", inv) + 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.Printf("[DEBUG] Store %s has %d certs, less than the required count of %d", st.Id, certCount, minCerts) + 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.Printf("[DEBUG] Store %s has too many leaf certs", st.Id) + 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.Printf("[DEBUG] Store %s has too many keys", st.Id) + 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("leaf_count", leafCount). + Int("key_count", keyCount). + Msg("Store meets criteria to be considered a root of trust") + log.Debug().Msg(fmt.Sprintf(DebugFuncExit, "isRootStore")) return true } @@ -393,23 +803,10 @@ kfutil stores rot reconcile --import-csv 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") + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true - authConfig := createAuthConfigFromParams(kfcHostName, kfcUsername, kfcPassword, kfcDomain, kfcAPIPath) - - 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 +814,104 @@ 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) + + 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 + } + + 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, ",")) + lookupFailures = append(lookupFailures, strings.Join(entry, ",")) continue } + log.Debug().Str("store_id", entry[0]). + Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetCertStoreInventoryV1")) inventory, invErr := kfClient.GetCertStoreInventoryV1(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 +921,102 @@ 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 { + log.Trace().Str("store_id", entry[0]).Str("thumbprint", t).Msg(trcMsg) stores[entry[0]].Thumbprints[t] = v } for t, v := range cert.Serials { + log.Trace().Str("store_id", entry[0]).Str("serial", t).Msg(trcMsg) stores[entry[0]].Serials[t] = v } for t, v := range cert.Ids { + log.Trace().Str("store_id", entry[0]).Int("cert_id", t).Msg(trcMsg) stores[entry[0]].Ids[t] = v } } - + log.Trace().Strs("entry", entry).Msg("Row processed") } // 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 + } + + 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 } - 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.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 +1055,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) + 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") isCSV, _ := cmd.Flags().GetBool("import-csv") @@ -586,251 +1068,70 @@ 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 = "" - } - - tp, tpOk := action["Thumbprint"].(string) - if !tpOk { - tp = "" - } - cid, cidOk := action["CertID"].(int) - if !cidOk { - cid = -1 - } + // Debug + expEnabled checks + isExperimental := false + debugErr := warnExperimentalFeature(expEnabled, isExperimental) + if debugErr != nil { + return debugErr + } + informDebug(debugFlag) - 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 +1167,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 +1246,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 +1339,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 +1354,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 +1370,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 +1384,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 +1450,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) + 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, @@ -1124,9 +1537,560 @@ the utility will first generate an audit report and then execute the add/remove } ) +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.GetCertStoreInventoryV1(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]). + Bool("value", v). + Str("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]). + Bool("value", v). + Str("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]). + Bool("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\t%s", strings.Join(lookupFailures, ",\r\n\t")) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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\t%s", strings.Join(lookupFailures, ",\r\n\t"), + ) + log.Error().Err(errMsg).Strs("lookup_failures", lookupFailures).Send() + if len(errs) > 0 { + apiErrs := mergeErrsToString(&errs) + 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\t%s", strings.Join( + lookupFailures, + "\r\n\t", + ), + ) + log.Error().Err(lookupErrs).Strs( + "lookup_failures", + lookupFailures, + ).Msg("The following stores could not be found") + if len(errs) > 0 { + apiErrs := mergeErrsToString(&errs) + lookupErrs = fmt.Errorf("%s\r\n%s", lookupErrs, apiErrs) + } + return lookupErrs + } else if len(errs) > 0 { + apiErrs := mergeErrsToString(&errs) + log.Error().Str("api_errors", apiErrs).Msg("Reconciliation completed with failures") + return fmt.Errorf("Reconciliation completed with failures:\r\n\t%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) + 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) + 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\t%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() { - log.SetFlags(log.LstdFlags | log.Lshortfile) - log.SetOutput(os.Stdout) var ( stores string addCerts string @@ -1135,7 +2099,7 @@ func init() { maxPrivateKeys int maxLeaves int tType = tTypeCerts - outPath string + outputFilePath string outputFormat string inputFile string storeTypes []string @@ -1149,39 +2113,83 @@ func init() { // 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().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.") + 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().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.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") @@ -1190,16 +2198,42 @@ func init() { // 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.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( + &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") From f6ae1382a6390d3a926279ea0191a8116e0df4ad Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 29 Mar 2024 12:36:16 -0700 Subject: [PATCH 3/9] chore(docs): Add CLI YAML def. Signed-off-by: sbailey <1661003+spbsoluble@users.noreply.github.com> --- cmd/constants.go | 49 +- cmd/export.go | 27 +- cmd/helpers.go | 8 +- cmd/root.go | 160 +++++- cmd/rot.go | 1058 ++++++++++++++++++++++++++++++++++------ cmd/rot_models.go | 202 ++++++++ cmd/storeTypes.go | 65 ++- go.mod | 2 +- go.sum | 4 +- main.go | 22 +- pkg/version/version.go | 3 +- spec/v2.yml | 134 +++++ spec/v3-vn.yml | 129 +++++ 13 files changed, 1635 insertions(+), 228 deletions(-) create mode 100644 cmd/rot_models.go create mode 100644 spec/v2.yml create mode 100644 spec/v3-vn.yml diff --git a/cmd/constants.go b/cmd/constants.go index bff54b6..96d063c 100644 --- a/cmd/constants.go +++ b/cmd/constants.go @@ -16,20 +16,33 @@ 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" - ErrMsgEmptyResponse = "empty response received from Keyfactor Command %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{ @@ -40,6 +53,10 @@ var ErrKfcEmptyResponse = fmt.Errorf("empty response recieved from Keyfactor Com // 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 63b21f7..5a5c7af 100644 --- a/cmd/helpers.go +++ b/cmd/helpers.go @@ -32,13 +32,17 @@ import ( "github.com/spf13/cobra" ) -func mergeErrsToString(errs *[]error) string { +func mergeErrsToString(errs *[]error, indent bool) string { var errStr string if errs == nil || len(*errs) == 0 { return "" } for _, err := range *errs { - errStr += fmt.Sprintf("%s\n", err) + if indent { + errStr += fmt.Sprintf(" \t%s\r\n", err) + continue + } + errStr += fmt.Sprintf("%s\r\n", err) } return errStr } 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 8cb5ae0..8492b59 100644 --- a/cmd/rot.go +++ b/cmd/rot.go @@ -16,91 +16,33 @@ package cmd import ( "bufio" + "context" "encoding/csv" "encoding/json" "errors" "fmt" + "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 ROTCert struct { - ID int `json:"id,omitempty"` - ThumbPrint string `json:"thumbprint,omitempty"` - CN string `json:"cn,omitempty"` - Locations []api.CertificateLocations `json:"locations,omitempty"` -} -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"` -} const ( tTypeCerts templateType = "certs" 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) @@ -124,9 +66,9 @@ 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 } @@ -174,37 +116,66 @@ func generateAuditReport( actions := make(map[string][]ROTAction) var errs []error - for _, cert := range addCerts { - log.Debug().Str("thumbprint", cert).Msg("Looking up certificate") - certLookupReq := api.GetCertificateContextArgs{ - IncludeMetadata: boolToPointer(true), - IncludeLocations: boolToPointer(true), - CollectionId: nil, //todo: add CollectionID support - Thumbprint: cert, - Id: 0, //todo: should also allow KFC ID + 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", cert). + Str("thumbprint", tp). Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetCertificateContext")) certLookup, err := kfClient.GetCertificateContext(&certLookupReq) if err != nil { log.Error(). Err(err). - Str("thumbprint", cert). + Str("thumbprint", tp). Msg("Error looking up certificate, skipping") - errs = append(errs, err) + 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", cert).Msg("Iterating over stores") + log.Debug().Str("thumbprint", tp).Msg("Iterating over stores") for _, store := range stores { - log.Debug().Str("thumbprint", cert).Str("store_id", store.ID).Msg("Checking if cert is deployed to store") - 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 - log.Info().Str("thumbprint", cert).Str("store_id", store.ID).Msg("Cert is already deployed to store") + log.Info().Str("thumbprint", tp).Str("store_id", store.ID).Msg("Cert is already deployed to store") row := []string{ - cert, + //todo: this should be a toCSV field on whatever object this is + tp, certIDStr, certLookup.IssuedDN, certLookup.IssuerDN, @@ -217,14 +188,14 @@ func generateAuditReport( "true", getCurrentTime(""), } - log.Trace().Str("thumbprint", cert).Strs("row", row).Msg("Appending data row") + log.Trace().Str("thumbprint", tp).Strs("row", row).Msg("Appending data row") data = append(data, row) - log.Debug().Str("thumbprint", cert).Strs("row", row).Msg("Writing data row to CSV") + log.Trace().Str("thumbprint", tp).Strs("row", row).Msg("Writing data row to CSV") wErr := csvWriter.Write(row) if wErr != nil { log.Error(). Err(wErr). - Str("thumbprint", cert). + Str("thumbprint", tp). Str("output_file", outputFilePath). Strs("row", row). Msg("Error writing row to CSV") @@ -232,11 +203,12 @@ func generateAuditReport( } else { // Cert is not deployed to this store and will need to be added log.Info(). - Str("thumbprint", cert). + Str("thumbprint", tp). Str("store_id", store.ID). Msg("Cert is not deployed to store") row := []string{ - cert, + //todo: this should be a toCSV + tp, certIDStr, certLookup.IssuedDN, certLookup.IssuerDN, @@ -250,29 +222,29 @@ func generateAuditReport( getCurrentTime(""), } log.Trace(). - Str("thumbprint", cert). + Str("thumbprint", tp). Strs("row", row). Msg("Appending data row") data = append(data, row) log.Debug(). - Str("thumbprint", cert). + Str("thumbprint", tp). Strs("row", row). Msg("Writing data row to CSV") wErr := csvWriter.Write(row) if wErr != nil { log.Error(). Err(wErr). - Str("thumbprint", cert). + Str("thumbprint", tp). Str("output_file", outputFilePath). Strs("row", row). Msg("Error writing row to CSV") } log.Debug(). - Str("thumbprint", cert). + Str("thumbprint", tp). Msg("Adding 'add' action to actions map") - actions[cert] = append( - actions[cert], ROTAction{ - Thumbprint: cert, + actions[tp] = append( + actions[tp], ROTAction{ + Thumbprint: tp, CertID: certID, StoreID: store.ID, StoreType: store.Type, @@ -289,7 +261,7 @@ func generateAuditReport( certLookupReq := api.GetCertificateContextArgs{ IncludeMetadata: boolToPointer(true), IncludeLocations: boolToPointer(true), - CollectionId: nil, + CollectionId: nil, //todo: add support for collection ID Thumbprint: cert, Id: 0, } @@ -428,15 +400,18 @@ func generateAuditReport( 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 { - //combine all errors into single string - errStr := mergeErrsToString(&errs) + 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) + return data, actions, fmt.Errorf("the following errors occurred while generating audit report:\r\n%s", errStr) } log.Debug().Msg(fmt.Sprintf(DebugFuncExit, "generateAuditReport")) return data, actions, nil @@ -622,7 +597,7 @@ func reconcileRoots(actions map[string][]ROTAction, kfClient *api.Client, report } log.Info().Str("reconciled_file", rFileName).Msg("Reconciliation actions scheduled on Keyfactor Command") if len(errs) > 0 { - errStr := mergeErrsToString(&errs) + errStr := mergeErrsToString(&errs, false) log.Trace().Str("reconciled_file", rFileName).Str( "errors", errStr, @@ -652,18 +627,44 @@ func readCertsFile(certsFilePath string, kfclient *api.Client) (map[string]strin 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") - for _, entry := range certEntries { + 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[entry[0]] = entry[0] + certs[tp] = cId log.Trace().Interface("certs", certs).Msg("Cert map") } log.Info().Str("certs_file", certsFilePath).Msg("Certs file read successfully") @@ -674,7 +675,7 @@ func readCertsFile(certsFilePath string, kfclient *api.Client) (map[string]strin func isRootStore( st *api.GetCertificateStoreResponse, - invs *[]api.CertStoreInventoryV1, + invs *[]api.CertStoreInventory, minCerts int, maxKeys int, maxLeaf int, @@ -690,8 +691,9 @@ func isRootStore( 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 + //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") @@ -717,13 +719,13 @@ func isRootStore( "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++ - } + //if inv.Parameters["PrivateKeyEntry"] == "Yes" { + // log.Debug().Str("store_id", st.Id).Str( + // "cert_thumbprint", + // cert.Thumbprint, + // ).Msg("Cert has a private key") + // keyCount++ + //} } } @@ -766,6 +768,104 @@ func isRootStore( return true } +func findTrustStores( + minCerts int, + maxKeys int, + maxLeaf int, +) []api.GetCertificateStoreResponse { + log.Debug().Msg(fmt.Sprintf(DebugFuncEnter, "findTrustStores")) + var trustStores []api.GetCertificateStoreResponse + log.Info().Msg("Finding root of trust stores") + log.Debug().Msg("Iterating over stores") + + // fetch list of stores from Keyfactor + stores, err := kfClient.GetCertificateStores() + + for _, st := range *stores { + log.Trace().Str("store_id", st.Id).Msg("Processing store") + if isRootStore(&st, invs, minCerts, maxKeys, maxLeaf) { + log.Debug().Str("store_id", st.Id).Msg("Store is a root of trust") + trustStores = append(trustStores, st) + } + } + log.Debug().Msg(fmt.Sprintf(DebugFuncExit, "findTrustStores")) + return trustStores +} + +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 := promptSelectFromAPIorFile("certificate stores") + switch apiOrFile { + case "API": + 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 + } + } + return *storesFile, nil +} + var ( rotCmd = &cobra.Command{ Use: "rot", @@ -802,7 +902,9 @@ kfutil stores rot reconcile --import-csv PersistentPreRun: nil, PersistentPreRunE: nil, PreRun: nil, - PreRunE: nil, + PreRunE: func(cmd *cobra.Command, args []string) error { + return nil + }, RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true @@ -833,6 +935,27 @@ kfutil stores rot reconcile --import-csv 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). @@ -879,14 +1002,14 @@ kfutil stores rot reconcile --import-csv Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetCertificateStoreByID")) apiResp, err := kfClient.GetCertificateStoreByID(entry[0]) if err != nil { - log.Printf("[ERROR] getting cert store: %s", err) + log.Error().Err(err).Str("store_id", entry[0]).Msg("Error getting cert store") lookupFailures = append(lookupFailures, strings.Join(entry, ",")) continue } log.Debug().Str("store_id", entry[0]). Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetCertStoreInventoryV1")) - inventory, invErr := kfClient.GetCertStoreInventoryV1(entry[0]) + inventory, invErr := kfClient.GetCertStoreInventory(entry[0]) if invErr != nil { log.Error().Err(invErr).Str("store_id", entry[0]).Msg("Error getting cert store inventory") lookupFailures = append(lookupFailures, strings.Join(entry, ",")) @@ -912,6 +1035,7 @@ kfutil stores rot reconcile --import-csv 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], @@ -928,21 +1052,46 @@ kfutil stores rot reconcile --import-csv thumb := cert.Thumbprints trcMsg := "Adding cert to store" for t, v := range thumb { - 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]).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 { - log.Trace().Str("store_id", entry[0]).Str("serial", t).Msg(trcMsg) - 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 { - log.Trace().Str("store_id", entry[0]).Int("cert_id", t).Msg(trcMsg) - 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) @@ -967,7 +1116,6 @@ kfutil stores rot reconcile --import-csv ).Msg("Error converting certs to add to JSON") return jErr } - log.Printf("[DEBUG] add certs JSON: %s", string(addCertsJSON)) log.Trace().Str("add_certs_file", addRootsFile). Str("add_certs_json", string(addCertsJSON)). Msg("Certs to add file read successfully") @@ -1510,7 +1658,7 @@ the utility will first generate an audit report and then execute the add/remove } if len(errs) != 0 { log.Error().Errs("errors", errs).Msg("Errors encountered while creating template file") - errMsg := mergeErrsToString(&errs) + errMsg := mergeErrsToString(&errs, false) return fmt.Errorf("errors encountered while creating template file: %s", errMsg) } fmt.Printf("Template file created at %s.\n", filePath) @@ -1537,6 +1685,126 @@ the utility will first generate an audit report and then execute the add/remove } ) +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 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 { + provideRemoveFile := promptYesNo("Would you like to include a 'certs to remove' CSV file?") + if provideRemoveFile { + 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, @@ -1580,7 +1848,7 @@ func processFromStoresAndCertFiles( } log.Debug().Str("store_id", row[0]).Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetCertStoreInventoryV1")) - inventory, invErr := kfClient.GetCertStoreInventoryV1(row[0]) + inventory, invErr := kfClient.GetCertStoreInventory(row[0]) if invErr != nil { errs = append(errs, invErr) log.Error().Err(invErr).Str( @@ -1620,32 +1888,32 @@ func processFromStoresAndCertFiles( thumb := cert.Thumbprints for t, v := range thumb { log.Trace().Str("store_id", row[0]). - Bool("value", v). - Str("thumbprint", t).Msg("Adding cert thumbprint to store object") - stores[row[0]].Thumbprints[t] = v + 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]). - Bool("value", v). - Str("serial", t).Msg("Adding cert serial to store object") - stores[row[0]].Serials[t] = v + 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]). - Bool("value", v). + Int("value", v). Int("cert_id", t).Msg("Adding cert ID to store object") - stores[row[0]].Ids[t] = v + //stores[row[0]].Ids[t] = v } } } if len(lookupFailures) > 0 { - errMsg := fmt.Errorf("The following stores were not found:\r\n\t%s", strings.Join(lookupFailures, ",\r\n\t")) + 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) + apiErrs := mergeErrsToString(&errs, false) errMsg = fmt.Errorf("%s\r\n%s", errMsg, apiErrs) } return errMsg @@ -1659,7 +1927,7 @@ func processFromStoresAndCertFiles( Int("max_keys", maxKeys).Send() if len(errs) > 0 { - apiErrs := mergeErrsToString(&errs) + apiErrs := mergeErrsToString(&errs, false) errMsg = fmt.Errorf("%s\r\n%s", errMsg, apiErrs) } return errMsg @@ -1676,7 +1944,7 @@ func processFromStoresAndCertFiles( 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) + apiErrs := mergeErrsToString(&errs, false) rErr = fmt.Errorf("%s\r\n%s", rErr, apiErrs) } return rErr @@ -1695,7 +1963,7 @@ func processFromStoresAndCertFiles( 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) + apiErrs := mergeErrsToString(&errs, false) rErr = fmt.Errorf("%s\r\n%s", rErr, apiErrs) } return rErr @@ -1705,7 +1973,7 @@ func processFromStoresAndCertFiles( 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) + apiErrs := mergeErrsToString(&errs, false) return fmt.Errorf(apiErrs) } fmt.Println("No add or remove operations specified, please verify your configuration") @@ -1735,7 +2003,7 @@ func processFromStoresAndCertFiles( Msg(msg) fmt.Println("No reconciliation actions to take, root stores are up-to-date. Exiting.") if len(errs) > 0 { - apiErrs := mergeErrsToString(&errs) + apiErrs := mergeErrsToString(&errs, false) return fmt.Errorf(apiErrs) } return nil @@ -1746,18 +2014,18 @@ func processFromStoresAndCertFiles( if rErr != nil { log.Error().Err(rErr).Msg("Error reconciling root of trust stores") if len(errs) > 0 { - apiErrs := mergeErrsToString(&errs) + 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\t%s", strings.Join(lookupFailures, ",\r\n\t"), + "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) + apiErrs := mergeErrsToString(&errs, false) errMsg = fmt.Errorf("%s\r\n%s", errMsg, apiErrs) return errMsg } @@ -1777,9 +2045,9 @@ func processFromStoresAndCertFiles( if len(lookupFailures) > 0 { lookupErrs := fmt.Errorf( "Reconciliation completed with failures, "+ - "the following stores could not be found:\r\n\t%s", strings.Join( + "the following stores could not be found:\r\n%s", strings.Join( lookupFailures, - "\r\n\t", + "\r\n", ), ) log.Error().Err(lookupErrs).Strs( @@ -1787,14 +2055,14 @@ func processFromStoresAndCertFiles( lookupFailures, ).Msg("The following stores could not be found") if len(errs) > 0 { - apiErrs := mergeErrsToString(&errs) + apiErrs := mergeErrsToString(&errs, false) lookupErrs = fmt.Errorf("%s\r\n%s", lookupErrs, apiErrs) } return lookupErrs } else if len(errs) > 0 { - apiErrs := mergeErrsToString(&errs) + apiErrs := mergeErrsToString(&errs, false) log.Error().Str("api_errors", apiErrs).Msg("Reconciliation completed with failures") - return fmt.Errorf("Reconciliation completed with failures:\r\n\t%s", apiErrs) + return fmt.Errorf("Reconciliation completed with failures:\r\n%s", apiErrs) } return nil } @@ -2051,7 +2319,7 @@ func processCSVReportFile(reportFile string, kfClient *api.Client, dryRun bool) Msg(rtMsg) fmt.Println(rtMsg) if len(errs) > 0 { - errStr := mergeErrsToString(&errs) + errStr := mergeErrsToString(&errs, false) log.Error().Str("report_file", reportFile). Str("errors", errStr). Msg("Errors encountered while parsing audit report") @@ -2074,11 +2342,11 @@ func processCSVReportFile(reportFile string, kfClient *api.Client, dryRun bool) ) //todo: this pathing might not work for everyone if len(errs) > 0 { - errStr := mergeErrsToString(&errs) + 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\t%s", errStr) + return fmt.Errorf("errors encountered while reconciling roots:\r\n%s", errStr) } @@ -2100,11 +2368,9 @@ func init() { maxLeaves int tType = tTypeCerts outputFilePath string - outputFormat string inputFile string storeTypes []string containerNames []string - collections []string subjectNames []string ) @@ -2228,13 +2494,487 @@ func init() { []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") } + +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 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, + } + survey.AskOne(prompt, &choices, survey.WithPageSize(10)) + return choices +} + +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 + } + + 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 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 +} + +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) + } + sort.Strings(stNames) + return stNames, nil +} diff --git a/cmd/rot_models.go b/cmd/rot_models.go new file mode 100644 index 0000000..9d1e9af --- /dev/null +++ b/cmd/rot_models.go @@ -0,0 +1,202 @@ +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"` + 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 74cf5a6..d1cdb43 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 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.8 + github.com/Keyfactor/keyfactor-go-client/v2 v2.2.7 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 diff --git a/go.sum b/go.sum index b4e5734..4c9a8e3 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/Keyfactor/keyfactor-go-client v1.4.3 h1:CmGvWcuIbDRFM0PfYOQH6UdtAgplv 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.8 h1:eIcdz8XwmoPlRPnAZMhp3/qIXR+pBGSzS3MTFnApbF0= -github.com/Keyfactor/keyfactor-go-client/v2 v2.2.8/go.mod h1:YRCG/SbM3wshb00YOe6hisKTRUSaCJ6oIqRBT9y652E= +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/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= 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 From 178a6bb1d1a94c8b4589265220c5675b285370aa Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 3 Apr 2024 15:27:50 -0700 Subject: [PATCH 4/9] feat(rot): Add option to "discover" roots of trust based on criteria. Signed-off-by: sbailey <1661003+spbsoluble@users.noreply.github.com> --- cmd/constants.go | 2 +- cmd/rot.go | 4147 ++++++++++++++++++++++++---------------------- 2 files changed, 2193 insertions(+), 1956 deletions(-) diff --git a/cmd/constants.go b/cmd/constants.go index 96d063c..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. diff --git a/cmd/rot.go b/cmd/rot.go index 8492b59..3bce448 100644 --- a/cmd/rot.go +++ b/cmd/rot.go @@ -38,6 +38,31 @@ import ( type templateType string +type TrustStoreCriteria struct { + MinCerts int + MaxKeys int + MaxLeaf int +} + +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 KFCStore struct { + ApiResponse api.GetCertificateStoreResponse + Inventory []api.CertStoreInventory +} + +type KFCStores struct { + Stores map[string]KFCStore +} + const ( tTypeCerts templateType = "certs" reconcileDefaultFileName string = "rot_audit.csv" @@ -680,7 +705,11 @@ func isRootStore( maxKeys int, maxLeaf int, ) bool { - log.Debug().Msg(fmt.Sprintf(DebugFuncEnter, "isRootStore")) + 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 @@ -761,35 +790,121 @@ func isRootStore( 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( - minCerts int, - maxKeys int, - maxLeaf int, -) []api.GetCertificateStoreResponse { + criteria *TrustStoreCriteria, + containerName string, + c *api.Client, +) (*KFCStores, error) { log.Debug().Msg(fmt.Sprintf(DebugFuncEnter, "findTrustStores")) - var trustStores []api.GetCertificateStoreResponse - log.Info().Msg("Finding root of trust stores") - log.Debug().Msg("Iterating over stores") + trustStores := KFCStores{ + Stores: map[string]KFCStore{}, + } - // fetch list of stores from Keyfactor - stores, err := kfClient.GetCertificateStores() + log.Info().Msg("Finding root of trust stList") + log.Debug().Msg("Iterating over stList") - for _, st := range *stores { - log.Trace().Str("store_id", st.Id).Msg("Processing store") - if isRootStore(&st, invs, minCerts, maxKeys, maxLeaf) { - log.Debug().Str("store_id", st.Id).Msg("Store is a root of trust") - trustStores = append(trustStores, st) + // 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 + return &trustStores, nil } func validateStoresInput(storesFile *string, noPrompt *bool, kfClient *api.Client) (string, error) { @@ -801,9 +916,9 @@ func validateStoresInput(storesFile *string, noPrompt *bool, kfClient *api.Clien if *noPrompt { return "", fmt.Errorf("stores file is required, use flag `--stores` to specify 1 or more file paths") } - apiOrFile := promptSelectFromAPIorFile("certificate stores") + apiOrFile := promptSelectRotStores("certificate stores") switch apiOrFile { - case "API": + case "Manual Select": selectedStores := promptSelectStores(kfClient) if len(selectedStores) == 0 { return "", errors.New("no certificate stores selected, unable to continue") @@ -861,2120 +976,2242 @@ func validateStoresInput(storesFile *string, noPrompt *bool, kfClient *api.Clien 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 } -var ( - rotCmd = &cobra.Command{ - Use: "rot", - Short: "Root of trust utility", - Long: `Root of trust allows you to manage your trusted roots using Keyfactor certificate stores. -For example if you wish to add a list of "root" certs to a list of certificate stores you would simply generate and fill -out the template CSV file. These template files can be generated with the following commands: -kfutil stores rot generate-template --type certs -kfutil stores rot generate-template --type stores -Once those files are filled out you can use the following command to add the certs to the stores: -kfutil stores rot audit --certs-file --stores-file -Will generate a CSV report file 'rot_audit.csv' of what actions will be taken. If those actions are correct you can run -the following command to actually perform the actions: -kfutil stores rot reconcile --certs-file --stores-file -OR if you want to use the audit report file generated you can run this command: -kfutil stores rot reconcile --import-csv -`, - } - rotAuditCmd = &cobra.Command{ - Use: "audit", - Aliases: nil, - SuggestFor: nil, - Short: "Audit generates a CSV report of what actions will be taken based on input CSV files.", - Long: `Root of Trust Audit: Will read and parse inputs to generate a report of certs that need to be added or removed from the "root of trust" stores.`, - Example: "", - ValidArgs: nil, - ValidArgsFunction: nil, - Args: nil, - ArgAliases: nil, - BashCompletionFunction: "", - Deprecated: "", - Annotations: nil, - Version: "", - PersistentPreRun: nil, - PersistentPreRunE: nil, - PreRun: nil, - PreRunE: func(cmd *cobra.Command, args []string) error { - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - cmd.SilenceUsage = true - - // Specific Flags - storesFile, _ := cmd.Flags().GetString("stores") - addRootsFile, _ := cmd.Flags().GetString("add-certs") - removeRootsFile, _ := cmd.Flags().GetString("remove-certs") - minCerts, _ := cmd.Flags().GetInt("min-certs") - maxLeaves, _ := cmd.Flags().GetInt("max-leaf-certs") - maxKeys, _ := cmd.Flags().GetInt("max-keys") - dryRun, _ := cmd.Flags().GetBool("dry-run") - outputFilePath, _ := cmd.Flags().GetString("outputFilePath") +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")) - // Debug + expEnabled checks - isExperimental := false - debugErr := warnExperimentalFeature(expEnabled, isExperimental) - if debugErr != nil { - return debugErr - } - informDebug(debugFlag) + if addRootsFile == "" && removeRootsFile == "" && noPrompt { + //cmd.SilenceUsage = false //todo: is this necessary? + return addRootsFile, removeRootsFile, InvalidROTCertsInputErr + } - authConfig := createAuthConfigFromParams(kfcHostName, kfcUsername, kfcPassword, kfcDomain, kfcAPIPath) + if addRootsFile == "" || removeRootsFile == "" { + if addRootsFile == "" && !noPrompt { + //prmpt := "Would you like to include a 'certs to add' CSV file?" + prmpt := "Provide certificates to add 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") - 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 - } + //parse certID, cn and thumbprint from selection `: () - ` - // 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 - } + //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 + } - 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 - } + //parse the cn from the selection `: () ` + cn := strings.Split(c, "(")[0] + cn = strings.Split(cn, ":")[1] + cn = strings.Trim(cn, " ") - log.Info().Str("stores_file", storesFile). - Str("add_file", addRootsFile). - Str("remove_file", removeRootsFile). - Bool("dry_run", dryRun). - Msg("Performing root of trust audit") + //parse thumbprint from selection `: () ` + thumbprint := strings.Split(c, "(")[1] + thumbprint = strings.Split(thumbprint, ")")[0] + thumbprint = strings.Trim(strings.Trim(thumbprint, " "), ")") - // Read in the stores CSV - 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 - } + certInstance := ROTCert{ + ID: certIdInt, + ThumbPrint: thumbprint, + CN: cn, + SANs: []string{}, + Alias: "", + Locations: []api.CertificateLocations{}, + } + certLine := certInstance.toCSV() - log.Trace().Str("stores_file", storesFile).Msg("Creating CSV reader") - reader := csv.NewReader(bufio.NewReader(csvFile)) - - 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 { - 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, ",")) + 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 { + provideRemoveFile := promptYesNo("Would you like to include a 'certs to remove' CSV file?") + if provideRemoveFile { + removeRootsFile = promptForFilePath("Input a file path for the 'certs to remove' CSV. ") + } + } + if addRootsFile == "" && removeRootsFile == "" { + return "", "", InvalidROTCertsInputErr + } + } + return addRootsFile, removeRootsFile, nil - 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.Error().Err(err).Str("store_id", entry[0]).Msg("Error getting cert store") - lookupFailures = append(lookupFailures, strings.Join(entry, ",")) - continue - } +} - log.Debug().Str("store_id", entry[0]). - Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetCertStoreInventoryV1")) - inventory, invErr := kfClient.GetCertStoreInventory(entry[0]) - if invErr != nil { - 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 - } +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 + } - if !isRootStore(apiResp, inventory, minCerts, maxLeaves, maxKeys) { - 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 - } + 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.Info().Str("store_id", entry[0]).Msg("Store is considered a root of trust store") + 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 + } - log.Trace().Str("store_id", entry[0]).Msg("Creating store entry") + 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 + } - 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), - } + 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", 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 { - //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 { - 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 { - 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") + 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 } - - 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", - ), - ) + 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 } - - // Read in the add addCerts CSV - var certsToAdd = make(map[string]string) - - 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 { - log.Error().Err(rcfErr).Str("add_certs_file", addRootsFile).Msg("Error reading certs to add file") - return rcfErr - } - - 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") + 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() - // Read in the remove removeCerts CSV - var certsToRemove = make(map[string]string) - 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 { - log.Error().Err(rcfErr).Str( - "remove_certs_file", - removeRootsFile, - ).Msg("Error reading certs to remove file") - } - - 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") + 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") + } - log.Debug().Msg(fmt.Sprintf(DebugFuncCall, "generateAuditReport")) - _, _, gErr := generateAuditReport(certsToAdd, certsToRemove, stores, outputFilePath, kfClient) - if gErr != nil { - log.Error().Err(gErr).Msg("Error generating audit report") - return gErr + // 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) } - - log.Info(). - Str("outputFilePath", outputFilePath). - Msg("Audit report generated successfully") - log.Debug(). - Msg(fmt.Sprintf(DebugFuncExit, "generateAuditReport")) - return nil - }, - Run: nil, - PostRun: nil, - PostRunE: nil, - PersistentPostRun: nil, - PersistentPostRunE: nil, - FParseErrWhitelist: cobra.FParseErrWhitelist{}, - CompletionOptions: cobra.CompletionOptions{}, - TraverseChildren: false, - Hidden: false, - SilenceErrors: false, - SilenceUsage: false, - DisableFlagParsing: false, - DisableAutoGenTag: false, - DisableFlagsInUseLine: false, - DisableSuggestions: false, - SuggestionsMinimumDistance: 0, + return rErr + } } - rotReconcileCmd = &cobra.Command{ - Use: "reconcile", - Aliases: nil, - SuggestFor: nil, - Short: "Reconcile either takes in or will generate an audit report and then add/remove certs as needed.", - Long: `Root of Trust (rot): Will parse either a combination of CSV files that define certs to -add and/or certs to remove with a CSV of certificate stores or an audit CSV file. If an audit CSV file is provided, the -add and remove actions defined in the audit file will be immediately executed. If a combination of CSV files are provided, -the utility will first generate an audit report and then execute the add/remove actions defined in the audit report.`, - Example: "", - ValidArgs: nil, - ValidArgsFunction: nil, - Args: nil, - ArgAliases: nil, - BashCompletionFunction: "", - Deprecated: "", - Annotations: nil, - Version: "", - PersistentPreRun: nil, - PersistentPreRunE: nil, - PreRun: nil, - PreRunE: nil, - RunE: func(cmd *cobra.Command, args []string) error { - cmd.SilenceUsage = true - // Specific Flags - storesFile, _ := cmd.Flags().GetString("stores") - addRootsFile, _ := cmd.Flags().GetString("add-certs") - isCSV, _ := cmd.Flags().GetBool("import-csv") - reportFile, _ := cmd.Flags().GetString("input-file") - removeRootsFile, _ := cmd.Flags().GetString("remove-certs") - minCerts, _ := cmd.Flags().GetInt("min-certs") - maxLeaves, _ := cmd.Flags().GetInt("max-leaf-certs") - maxKeys, _ := cmd.Flags().GetInt("max-keys") - dryRun, _ := cmd.Flags().GetBool("dry-run") - outputFilePath, _ := cmd.Flags().GetString("outputFilePath") + 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 + } - // Debug + expEnabled checks - isExperimental := false - debugErr := warnExperimentalFeature(expEnabled, isExperimental) - if debugErr != nil { - return debugErr - } - informDebug(debugFlag) + log.Trace().Interface("certs_to_add", certsToAdd). + Interface("certs_to_remove", certsToRemove). + Str("stores_file", storesFile). + Msg("Generating audit report") - authConfig := createAuthConfigFromParams(kfcHostName, kfcUsername, kfcPassword, kfcDomain, kfcAPIPath) + 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 + } - kfClient, clErr := initClient(configFile, profile, "", "", noPrompt, authConfig, false) - if clErr != nil { - log.Error().Err(clErr).Msg("Error initializing Keyfactor client") - return clErr - } + 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("stores_file", storesFile). - Str("add_file", addRootsFile). - Str("remove_file", removeRootsFile). - Bool("dry_run", dryRun). - Msg("Performing root of trust reconciliation") + 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)) - // 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 - } - return nil - } else { - 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.Error().Err(err).Msg("Error processing from stores file") - return err - } - } + 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 +} - log.Debug().Str("report_file", reportFile). - Str("outputFilePath", outputFilePath).Msg("Reconciliation report generated successfully") - log.Debug().Msg(fmt.Sprintf(DebugFuncExit, "reconcileRoots")) - return nil - }, - Run: nil, - PostRun: nil, - PostRunE: nil, - PersistentPostRun: nil, - PersistentPostRunE: nil, - FParseErrWhitelist: cobra.FParseErrWhitelist{}, - CompletionOptions: cobra.CompletionOptions{}, - TraverseChildren: false, - Hidden: false, - SilenceErrors: false, - SilenceUsage: false, - DisableFlagParsing: false, - DisableAutoGenTag: false, - DisableFlagsInUseLine: false, - DisableSuggestions: false, - SuggestionsMinimumDistance: 0, +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 } - rotGenStoreTemplateCmd = &cobra.Command{ - Use: "generate-template", - Aliases: nil, - SuggestFor: nil, - Short: "For generating Root Of Trust template(s)", - Long: `Root Of Trust: Will parse a CSV and attempt to deploy a cert or set of certs into a list of cert stores.`, - Example: "", - ValidArgs: nil, - ValidArgsFunction: nil, - Args: nil, - ArgAliases: nil, - BashCompletionFunction: "", - Deprecated: "", - Annotations: nil, - Version: "", - PersistentPreRun: nil, - PersistentPreRunE: nil, - PreRun: nil, - PreRunE: nil, - RunE: func(cmd *cobra.Command, args []string) error { - cmd.SilenceUsage = true - // Specific Flags - templateType, _ := cmd.Flags().GetString("type") - format, _ := cmd.Flags().GetString("format") - 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") + 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 + } - // Debug + expEnabled checks - isExperimental := false - debugErr := warnExperimentalFeature(expEnabled, isExperimental) - if debugErr != nil { - return debugErr - } - informDebug(debugFlag) + actions := make(map[string][]ROTAction) + fieldMap := make(map[int]string) - // Authenticate - authConfig := createAuthConfigFromParams(kfcHostName, kfcUsername, kfcPassword, kfcDomain, kfcAPIPath) - kfClient, clErr := initClient( - configFile, profile, providerType, providerProfile, noPrompt, authConfig, - false, + 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, + ",", + ), ) - if clErr != nil { - log.Error().Err(clErr).Msg("Error initializing Keyfactor client") - return clErr - } + log.Error().Err(invalidHeaderErr).Str( + "report_file", + reportFile, + ).Msg("Invalid header in audit report file") + return invalidHeaderErr + } - stID := -1 - var storeData []api.GetCertificateStoreResponse - var csvStoreData [][]string - var csvCertData [][]string - var rowLookup = make(map[string]bool) - var errs []error + 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 + } + } - if len(storeType) != 0 { - log.Info().Strs("store_types", storeType).Msg("Processing store types") - for _, s := range storeType { - 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.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().Msg("Creating empty store type for 'all' option") - sType = &api.CertificateStoreType{ - Name: "", - ShortName: "", - Capability: "", - StoreType: 0, - ImportType: 0, - LocalStore: false, - SupportedOperations: nil, - Properties: nil, - EntryParameters: nil, - PasswordOptions: nil, - StorePathType: "", - StorePathValue: "", - PrivateKeyAllowed: "", - JobProperties: nil, - ServerRequired: false, - PowerShell: false, - BlueprintAllowed: false, - CustomAliasAllowed: "", - ServerRegistration: 0, - InventoryEndpoint: "", - InventoryJobType: "", - ManagementJobType: "", - DiscoveryJobType: "", - EnrollmentJobType: "", - } - } else { - // check if s is an int - sInt, err := strconv.Atoi(s) + 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 + } - 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("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.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 { - 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(""), - } - 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) - } - } - } - log.Info().Strs("store_types", storeType).Msg("Store types processed") - } + 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 + } - if len(containerName) != 0 { - log.Info().Strs("container_names", containerName).Msg("Processing container names") - for _, c := range containerName { - cStoresResp, scErr := kfClient.GetCertificateStoreByContainerID(c) - if scErr != nil { - fmt.Printf("[ERROR] getting store container: %s\n", scErr) - } - if cStoresResp != nil { - for _, store := range *cStoresResp { - sType, stErr := kfClient.GetCertificateStoreType(store.CertStoreType) - if stErr != nil { - fmt.Printf("[ERROR] getting store type: %s\n", stErr) - continue - } - storeData = append(storeData, store) - 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(""), - } - csvStoreData = append(csvStoreData, lineData) - rowLookup[store.Id] = true - } - } + 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.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 { - q := make(map[string]string) - q["collection"] = c - certsResp, scErr := kfClient.ListCertificates(q) - if scErr != nil { - fmt.Printf("No certificates found in collection: %s\n", scErr) - } - if certsResp != nil { - for _, cert := range certsResp { - 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(""), - } - csvCertData = append(csvCertData, lineData) - rowLookup[cert.Thumbprint] = true - } - } + 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.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 { - 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 { - log.Error().Err(scErr).Str("subject_name", s).Msg("Error listing certificates by subject name") - errs = append(errs, scErr) - } + 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 = "" + } - 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.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 + } - 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(""), - } - log.Trace().Strs("line_data", lineData).Msg("Adding line data to CSV data") - csvCertData = append(csvCertData, lineData) - rowLookup[cert.Thumbprint] = true - } - } + 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 } - // Create CSV template file + log.Debug().Str("report_file", reportFile). + Int("row", ri). + Str("thumbprint", tp). + Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetCertificateContext")) - var filePath string - 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) + certLookup, err := kfClient.GetCertificateContext(&certLookupReq) if err != nil { - log.Error().Err(err).Str("file_path", filePath).Msg("Error creating template file") - return err + 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") + } - 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 { - 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.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 - }, - Run: nil, - PostRun: nil, - PostRunE: nil, - PersistentPostRun: nil, - PersistentPostRunE: nil, - FParseErrWhitelist: cobra.FParseErrWhitelist{}, - CompletionOptions: cobra.CompletionOptions{}, - TraverseChildren: false, - Hidden: false, - SilenceErrors: false, - SilenceUsage: false, - DisableFlagParsing: false, - DisableAutoGenTag: false, - DisableFlagsInUseLine: false, - DisableSuggestions: false, - SuggestionsMinimumDistance: 0, + 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) } -) -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")) + 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 + } - if addRootsFile == "" && removeRootsFile == "" && noPrompt { - //cmd.SilenceUsage = false //todo: is this necessary? - return addRootsFile, removeRootsFile, InvalidROTCertsInputErr + 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() - if addRootsFile == "" || removeRootsFile == "" { - if addRootsFile == "" && !noPrompt { - //prmpt := "Would you like to include a 'certs to add' CSV file?" - prmpt := "Provide certificates to add 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") + orchsURL := fmt.Sprintf( + "https://%s/Keyfactor/Portal/AgentJobStatus/Index", + kfClient.Hostname, + ) //todo: this pathing might not work for everyone - //parse certID, cn and thumbprint from selection `: () - ` + 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) - //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, " ") + 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 + ) + + 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( + &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 +} - //parse thumbprint from selection `: () ` - thumbprint := strings.Split(c, "(")[1] - thumbprint = strings.Split(thumbprint, ")")[0] - thumbprint = strings.Trim(strings.Trim(thumbprint, " "), ")") +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 +} - certInstance := ROTCert{ - ID: certIdInt, - ThumbPrint: thumbprint, - CN: cn, - SANs: []string{}, - Alias: "", - Locations: []api.CertificateLocations{}, - } - certLine := certInstance.toCSV() +func promptSelectRotStores(resourceType string) string { + var selected string - 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 { - provideRemoveFile := promptYesNo("Would you like to include a 'certs to remove' CSV file?") - if provideRemoveFile { - removeRootsFile = promptForFilePath("Input a file path for the 'certs to remove' CSV. ") - } - } - if addRootsFile == "" && removeRootsFile == "" { - return "", "", InvalidROTCertsInputErr - } + opts := []string{ + "Manual Select", + "Discover", + "File", + "All", } - return addRootsFile, removeRootsFile, nil + //sort ops + sort.Strings(opts) + + selected = promptSingleSelect( + fmt.Sprintf("Source %s from:", resourceType), + opts, + DefaultMenuPageSizeSmall, + ) + return selected } -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 - } +func promptSelectFromAPIorFile(resourceType string) string { + var selected string - 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 - } + selected = promptSingleSelect( + fmt.Sprintf("Source %s from:", resourceType), + DefaultSourceTypeOptions, + DefaultMenuPageSizeSmall, + ) + return selected - 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 - } +func promptSelectCerts(client *api.Client) []string { + searchOpts := []string{ + "Certificate", + "Collection", + } + var selectedCerts []string - 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), - } + 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().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 + 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 } - 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 + 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 } - 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(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...) } } } - 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() + return selectedCerts +} - if len(errs) > 0 { - apiErrs := mergeErrsToString(&errs, false) - errMsg = fmt.Errorf("%s\r\n%s", errMsg, apiErrs) - } - return errMsg +func promptSelectStores(client *api.Client) []string { + searchOpts := []string{ + "Store", + "StoreType", + "Container", + //"Collection", } - // 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") + 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"} } - // 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) + 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 } - 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.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) - log.Trace().Interface("certs_to_add", certsToAdd). - Interface("certs_to_remove", certsToRemove). - Str("stores_file", storesFile). - Msg("Generating audit report") + 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(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("Prompting user to select store types") + var selectedStoreTypes []string + selectedStoreTypes = append( + selectedStoreTypes, + promptMultiSelect( + "Select stores associated with store type(s) to audit:", + storeTypeNames, + )..., + ) - 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 + //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.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)) + 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...) + } - 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) + 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 nil + return selectedStores } -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 +func promptSingleSelect(msg string, opts []string, menuPageSize int) string { + if menuPageSize <= 0 { + menuPageSize = DefaultMenuPageSizeSmall } - - 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 + var choice string + prompt := &survey.Select{ + Message: msg, + Options: opts, + PageSize: menuPageSize, } + survey.AskOne(prompt, &choice, survey.WithPageSize(10)) + return choice +} - 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 +func promptMultiSelect(msg string, opts []string) []string { + var choices []string + prompt := &survey.MultiSelect{ + Message: msg, + Options: opts, + PageSize: 10, } + survey.AskOne(prompt, &choices, survey.WithPageSize(10)) + return choices +} - 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 - } +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 + } - 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 - } - } + 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 +} - 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 = "" - } +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 +} - 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 - } +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")) - 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 - } + 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() - 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 = "" - } + defer httpResponse.Body.Close() - 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 = "" - } + 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) + } - 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 = "" - } + 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 +} - 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 - } +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 !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 - } + 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 + } - 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")) + configuration := sdk.NewConfiguration(config) + sdkClient := sdk.NewAPIClient(configuration) + return sdkClient, nil +} - 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") - } +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 + } - 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, - } + 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 - 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) +} + +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.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) + 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] } - return nil + + 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) } + sort.Strings(stNames) + return stNames, 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 +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), + } + 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), + } + 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) + + trustCriteria.MaxKeys = maxKeys + trustCriteria.MinCerts = minCerts + trustCriteria.MaxLeaf = maxLeaves + return nil +} + +var ( + rotCmd = &cobra.Command{ + Use: "rot", + Short: "Root of trust utility", + Long: `Root of trust allows you to manage your trusted roots using Keyfactor certificate stores. +For example if you wish to add a list of "root" certs to a list of certificate stores you would simply generate and fill +out the template CSV file. These template files can be generated with the following commands: +kfutil stores rot generate-template --type certs +kfutil stores rot generate-template --type stores +Once those files are filled out you can use the following command to add the certs to the stores: +kfutil stores rot audit --certs-file --stores-file +Will generate a CSV report file 'rot_audit.csv' of what actions will be taken. If those actions are correct you can run +the following command to actually perform the actions: +kfutil stores rot reconcile --certs-file --stores-file +OR if you want to use the audit report file generated you can run this command: +kfutil stores rot reconcile --import-csv +`, } - defer csvFile.Close() + rotAuditCmd = &cobra.Command{ + Use: "audit", + Aliases: nil, + SuggestFor: nil, + Short: "Audit generates a CSV report of what actions will be taken based on input CSV files.", + Long: `Root of Trust Audit: Will read and parse inputs to generate a report of certs that need to be added or removed from the "root of trust" stores.`, + Example: "", + ValidArgs: nil, + ValidArgsFunction: nil, + Args: nil, + ArgAliases: nil, + BashCompletionFunction: "", + Deprecated: "", + Annotations: nil, + Version: "", + PersistentPreRun: nil, + PersistentPreRunE: nil, + PreRun: nil, + PreRunE: func(cmd *cobra.Command, args []string) error { + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true - orchsURL := fmt.Sprintf( - "https://%s/Keyfactor/Portal/AgentJobStatus/Index", - kfClient.Hostname, - ) //todo: this pathing might not work for everyone + // Specific Flags + storesFile, _ := cmd.Flags().GetString("stores") + addRootsFile, _ := cmd.Flags().GetString("add-certs") + removeRootsFile, _ := cmd.Flags().GetString("remove-certs") + minCerts, _ := cmd.Flags().GetInt("min-certs") + maxLeaves, _ := cmd.Flags().GetInt("max-leaf-certs") + maxKeys, _ := cmd.Flags().GetInt("max-keys") + dryRun, _ := cmd.Flags().GetBool("dry-run") + outputFilePath, _ := cmd.Flags().GetString("outputFilePath") - 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) + // 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") - 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)) + authConfig := createAuthConfigFromParams(kfcHostName, kfcUsername, kfcPassword, kfcDomain, kfcAPIPath) - return nil -} + 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 + } -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 - ) + // 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 + } - storesCmd.AddCommand(rotCmd) + 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 + } - // 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.", - ) + log.Info().Str("stores_file", storesFile). + Str("add_file", addRootsFile). + Str("remove_file", removeRootsFile). + Bool("dry_run", dryRun). + Msg("Performing root of trust audit") - // 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") + // Read in the stores CSV + 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 + } - // 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( - &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.", - ) + log.Trace().Str("stores_file", storesFile).Msg("Creating CSV reader") + reader := csv.NewReader(bufio.NewReader(csvFile)) - rotGenStoreTemplateCmd.RegisterFlagCompletionFunc("type", templateTypeCompletion) - rotGenStoreTemplateCmd.MarkFlagRequired("type") -} + 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 + } -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 -} + 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 { + 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, ",")) + } -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 -} + 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.Error().Err(err).Str("store_id", entry[0]).Msg("Error getting cert store") + lookupFailures = append(lookupFailures, strings.Join(entry, ",")) + continue + } -func promptSelectFromAPIorFile(resourceType string) string { - var selected string + log.Debug().Str("store_id", entry[0]). + Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetCertStoreInventoryV1")) + inventory, invErr := kfClient.GetCertStoreInventory(entry[0]) + if invErr != nil { + 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 + } - selected = promptSingleSelect( - fmt.Sprintf("Source %s from:", resourceType), - DefaultSourceTypeOptions, - DefaultMenuPageSizeSmall, - ) - return selected + if !isRootStore(apiResp, inventory, minCerts, maxLeaves, maxKeys) { + 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 + } -} + log.Info().Str("store_id", entry[0]).Msg("Store is considered a root of trust store") -func promptSelectCerts(client *api.Client) []string { - searchOpts := []string{ - "Certificate", - "Collection", - } - var selectedCerts []string + log.Trace().Str("store_id", entry[0]).Msg("Creating store entry") - 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"} - } + 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), + } - 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 + 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 { + //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 { + 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 { + 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") } - 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 + 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", + ), + ) } - 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 + + // Read in the add addCerts CSV + var certsToAdd = make(map[string]string) + + 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 { + log.Error().Err(rcfErr).Str("add_certs_file", addRootsFile).Msg("Error reading certs to add file") + return rcfErr } - 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 + 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 } - 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 + 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 == "" { + 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 { + log.Error().Err(rcfErr).Str( + "remove_certs_file", + removeRootsFile, + ).Msg("Error reading certs to remove file") } - selectedCerts = append(selectedCerts, certOpts...) + + 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") } - } - } - return selectedCerts -} -func promptSelectStores(client *api.Client) []string { - searchOpts := []string{ - "Store", - "StoreType", - "Container", - //"Collection", + log.Debug().Msg(fmt.Sprintf(DebugFuncCall, "generateAuditReport")) + _, _, gErr := generateAuditReport(certsToAdd, certsToRemove, stores, outputFilePath, kfClient) + if gErr != nil { + 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 + }, + Run: nil, + PostRun: nil, + PostRunE: nil, + PersistentPostRun: nil, + PersistentPostRunE: nil, + FParseErrWhitelist: cobra.FParseErrWhitelist{}, + CompletionOptions: cobra.CompletionOptions{}, + TraverseChildren: false, + Hidden: false, + SilenceErrors: false, + SilenceUsage: false, + DisableFlagParsing: false, + DisableAutoGenTag: false, + DisableFlagsInUseLine: false, + DisableSuggestions: false, + SuggestionsMinimumDistance: 0, } - var selectedStores []string + rotReconcileCmd = &cobra.Command{ + Use: "reconcile", + Aliases: nil, + SuggestFor: nil, + Short: "Reconcile either takes in or will generate an audit report and then add/remove certs as needed.", + Long: `Root of Trust (rot): Will parse either a combination of CSV files that define certs to +add and/or certs to remove with a CSV of certificate stores or an audit CSV file. If an audit CSV file is provided, the +add and remove actions defined in the audit file will be immediately executed. If a combination of CSV files are provided, +the utility will first generate an audit report and then execute the add/remove actions defined in the audit report.`, + Example: "", + ValidArgs: nil, + ValidArgsFunction: nil, + Args: nil, + ArgAliases: nil, + BashCompletionFunction: "", + Deprecated: "", + Annotations: nil, + Version: "", + PersistentPreRun: nil, + PersistentPreRunE: nil, + PreRun: nil, + PreRunE: nil, + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true - 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"} - } + // Specific Flags + storesFile, _ := cmd.Flags().GetString("stores") + addRootsFile, _ := cmd.Flags().GetString("add-certs") + isCSV, _ := cmd.Flags().GetBool("import-csv") + reportFile, _ := cmd.Flags().GetString("input-file") + removeRootsFile, _ := cmd.Flags().GetString("remove-certs") + minCerts, _ := cmd.Flags().GetInt("min-certs") + maxLeaves, _ := cmd.Flags().GetInt("max-leaf-certs") + maxKeys, _ := cmd.Flags().GetInt("max-keys") + dryRun, _ := cmd.Flags().GetBool("dry-run") + outputFilePath, _ := cmd.Flags().GetString("outputFilePath") - 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 + // Debug + expEnabled checks + isExperimental := false + debugErr := warnExperimentalFeature(expEnabled, isExperimental) + if debugErr != nil { + return debugErr } + informDebug(debugFlag) - 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) + trustCriteria.MinCerts = minCerts + trustCriteria.MaxKeys = maxKeys + trustCriteria.MaxLeaf = maxLeaves - 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 + authConfig := createAuthConfigFromParams(kfcHostName, kfcUsername, kfcPassword, kfcDomain, kfcAPIPath) + + kfClient, clErr := initClient(configFile, profile, "", "", noPrompt, authConfig, false) + if clErr != nil { + log.Error().Err(clErr).Msg("Error initializing Keyfactor client") + return clErr } - 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, - )..., - ) + log.Info().Str("stores_file", storesFile). + Str("add_file", addRootsFile). + Str("remove_file", removeRootsFile). + Bool("dry_run", dryRun). + Msg("Performing root of trust reconciliation") - //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 + // 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 } - - 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 + return nil + } else { + 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.Error().Err(err).Msg("Error processing from stores file") + return err } - 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, - } - survey.AskOne(prompt, &choices, survey.WithPageSize(10)) - return choices -} - -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 + log.Debug().Str("report_file", reportFile). + Str("outputFilePath", outputFilePath).Msg("Reconciliation report generated successfully") + log.Debug().Msg(fmt.Sprintf(DebugFuncExit, "reconcileRoots")) + return nil + }, + Run: nil, + PostRun: nil, + PostRunE: nil, + PersistentPostRun: nil, + PersistentPostRunE: nil, + FParseErrWhitelist: cobra.FParseErrWhitelist{}, + CompletionOptions: cobra.CompletionOptions{}, + TraverseChildren: false, + Hidden: false, + SilenceErrors: false, + SilenceUsage: false, + DisableFlagParsing: false, + DisableAutoGenTag: false, + DisableFlagsInUseLine: false, + DisableSuggestions: false, + SuggestionsMinimumDistance: 0, } + rotGenStoreTemplateCmd = &cobra.Command{ + Use: "generate-template", + Aliases: nil, + SuggestFor: nil, + Short: "For generating Root Of Trust template(s)", + Long: `Root Of Trust: Will parse a CSV and attempt to deploy a cert or set of certs into a list of cert stores.`, + Example: "", + ValidArgs: nil, + ValidArgsFunction: nil, + Args: nil, + ArgAliases: nil, + BashCompletionFunction: "", + Deprecated: "", + Annotations: nil, + Version: "", + PersistentPreRun: nil, + PersistentPreRunE: nil, + PreRun: nil, + PreRunE: nil, + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true - 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 -} + // Specific Flags + templateType, _ := cmd.Flags().GetString("type") + format, _ := cmd.Flags().GetString("format") + 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") -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 -} + // Debug + expEnabled checks + isExperimental := false + debugErr := warnExperimentalFeature(expEnabled, isExperimental) + if debugErr != nil { + return debugErr + } + informDebug(debugFlag) -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")) + // 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 + } - 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() + stID := -1 + var storeData []api.GetCertificateStoreResponse + var csvStoreData [][]string + var csvCertData [][]string + var rowLookup = make(map[string]bool) + var errs []error - defer httpResponse.Body.Close() + if len(storeType) != 0 { + log.Info().Strs("store_types", storeType).Msg("Processing store types") + for _, s := range storeType { + 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") - 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) - } + log.Trace().Msg("Creating empty store type for 'all' option") + sType = &api.CertificateStoreType{ + Name: "", + ShortName: "", + Capability: "", + StoreType: 0, + ImportType: 0, + LocalStore: false, + SupportedOperations: nil, + Properties: nil, + EntryParameters: nil, + PasswordOptions: nil, + StorePathType: "", + StorePathValue: "", + PrivateKeyAllowed: "", + JobProperties: nil, + ServerRequired: false, + PowerShell: false, + BlueprintAllowed: false, + CustomAliasAllowed: "", + ServerRegistration: 0, + InventoryEndpoint: "", + InventoryJobType: "", + ManagementJobType: "", + DiscoveryJobType: "", + EnrollmentJobType: "", + } + } else { + // check if s is an int + sInt, err := strconv.Atoi(s) - 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 -} + 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("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 + } -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 stID >= 0 || s == "all" { + 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 + } - 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 - } + log.Debug().Str("store_type", s).Msg("Getting certificate stores") + stores, sErr := kfClient.ListCertificateStores(¶ms) + if sErr != nil { + 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(""), + } + 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) + } + } + } + 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 { + cStoresResp, scErr := kfClient.GetCertificateStoreByContainerID(c) + if scErr != nil { + fmt.Printf("[ERROR] getting store container: %s\n", scErr) + } + if cStoresResp != nil { + for _, store := range *cStoresResp { + sType, stErr := kfClient.GetCertificateStoreType(store.CertStoreType) + if stErr != nil { + fmt.Printf("[ERROR] getting store type: %s\n", stErr) + continue + } + storeData = append(storeData, store) + 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(""), + } + csvStoreData = append(csvStoreData, lineData) + rowLookup[store.Id] = true + } + } - configuration := sdk.NewConfiguration(config) - sdkClient := sdk.NewAPIClient(configuration) - return sdkClient, nil -} + } + } + 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 { + q := make(map[string]string) + q["collection"] = c + certsResp, scErr := kfClient.ListCertificates(q) + if scErr != nil { + fmt.Printf("No certificates found in collection: %s\n", scErr) + } + if certsResp != nil { + for _, cert := range certsResp { + 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(""), + } + csvCertData = append(csvCertData, lineData) + rowLookup[cert.Thumbprint] = true + } + } -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 - } + } + } + 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 { + 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 { + log.Error().Err(scErr).Str("subject_name", s).Msg("Error listing certificates by subject name") + errs = append(errs, scErr) + } - 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 + 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(""), + } + log.Trace().Strs("line_data", lineData).Msg("Adding line data to CSV data") + csvCertData = append(csvCertData, lineData) + rowLookup[cert.Thumbprint] = true + } + } -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 - } + } + } + } + // Create CSV template file - 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") + var filePath string + if outputFilePath != "" { + filePath = outputFilePath } else { - storeTypesLookup[st.CertStoreType] = storeType.Name - stName = storeType.Name + 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 { + log.Error().Err(err).Str("file_path", filePath).Msg("Error creating template file") + return err } - } 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) + 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 { + 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.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 + }, + Run: nil, + PostRun: nil, + PostRunE: nil, + PersistentPostRun: nil, + PersistentPostRunE: nil, + FParseErrWhitelist: cobra.FParseErrWhitelist{}, + CompletionOptions: cobra.CompletionOptions{}, + TraverseChildren: false, + Hidden: false, + SilenceErrors: false, + SilenceUsage: false, + DisableFlagParsing: false, + DisableAutoGenTag: false, + DisableFlagsInUseLine: false, + DisableSuggestions: false, + SuggestionsMinimumDistance: 0, } - sort.Strings(stNames) - return stNames, nil -} +) From fa0b36d9748b62083219148ab9a7559c23d21679 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 9 May 2024 12:44:58 -0700 Subject: [PATCH 5/9] feat(rot): Add user interactive option to specifying certs to remove. Signed-off-by: sbailey <1661003+spbsoluble@users.noreply.github.com> --- cmd/rot.go | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 3 deletions(-) diff --git a/cmd/rot.go b/cmd/rot.go index 3bce448..271246a 100644 --- a/cmd/rot.go +++ b/cmd/rot.go @@ -1050,7 +1050,7 @@ func validateCertsInput(addRootsFile string, removeRootsFile string, client *api if addRootsFile == "" || removeRootsFile == "" { if addRootsFile == "" && !noPrompt { //prmpt := "Would you like to include a 'certs to add' CSV file?" - prmpt := "Provide certificates to add and/or that should be present in selected stores?" + prmpt := "Provide certificates to add to and/or that should be present in selected stores?" provideAddFile := promptYesNo(prmpt) if provideAddFile { addSrcType := promptSelectFromAPIorFile("certificates") @@ -1139,9 +1139,93 @@ func validateCertsInput(addRootsFile string, removeRootsFile string, client *api } } if removeRootsFile == "" && !noPrompt { - provideRemoveFile := promptYesNo("Would you like to include a 'certs to remove' CSV file?") + 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. ") + //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 == "" { From ad3e2e57660a3388c10a05556f75a2ff876d9cd4 Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Thu, 9 May 2024 19:45:30 +0000 Subject: [PATCH 6/9] Update generated README --- README.md | 1 + 1 file changed, 1 insertion(+) 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' ``` + From 1cb716c4831fc134a0456c3e42ea3f2a147d98c2 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 9 May 2024 13:51:37 -0700 Subject: [PATCH 7/9] feat(rot): Remove certs lookup passing ID as thumbprint. chore(deps): Bump `keyfactor-go-client` version to `v2.2.8` Signed-off-by: sbailey <1661003+spbsoluble@users.noreply.github.com> --- cmd/rot.go | 166 +++++++++++++++++++++++++++++------------------------ go.mod | 2 +- go.sum | 4 +- 3 files changed, 93 insertions(+), 79 deletions(-) diff --git a/cmd/rot.go b/cmd/rot.go index 271246a..eaf0ad8 100644 --- a/cmd/rot.go +++ b/cmd/rot.go @@ -281,54 +281,99 @@ func generateAuditReport( } } } - for _, cert := range removeCerts { - log.Debug().Str("thumbprint", cert).Msg("Looking up certificate to remove") - certLookupReq := api.GetCertificateContextArgs{ - IncludeMetadata: boolToPointer(true), - IncludeLocations: boolToPointer(true), - CollectionId: nil, //todo: add support for collection ID - 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", cert).Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetCertificateContext")) + + log.Debug(). + Str("thumbprint", tp). + Msg(fmt.Sprintf(DebugFuncCall, "kfClient.GetCertificateContext")) certLookup, err := kfClient.GetCertificateContext(&certLookupReq) if err != nil { log.Error(). Err(err). - Str("thumbprint", cert). - Msg("Error looking up certificate, unable to remove from store") - errs = append(errs, err) - continue - } else if certLookup == nil { - log.Error(). - Err(ErrKfcEmptyResponse). - Str("thumbprint", cert). - Msg(fmt.Sprintf("%s when looking up certificate", ErrMsgEmptyResponse)) - errs = append(errs, ErrKfcEmptyResponse) + 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 - log.Trace(). - Str("thumbprint", cert). - Int("cert_id", certID). - Msg("Converting cert ID to string") certIDStr := strconv.Itoa(certID) + log.Debug().Str("thumbprint", tp).Msg("Iterating over stores") for _, store := range stores { - storeIdentifier := fmt.Sprintf("%s/%s", store.Machine, store.Path) - log.Debug().Str("thumbprint", cert). - Str("store_id", store.ID). - Str("store_name", storeIdentifier). - Msg("Checking if cert is deployed to store") - 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 + 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 { + log.Error(). + Err(wErr). + Str("thumbprint", tp). + Str("output_file", outputFilePath). + Strs("row", row). + Msg("Error writing row to CSV") + } + } else { // Cert is deployed to this store and will need to be removed log.Info(). - Str("thumbprint", cert). + Str("thumbprint", tp). Str("store_id", store.ID). - Str("store_name", storeIdentifier). Msg("Cert is deployed to store") row := []string{ - cert, + //todo: this should be a toCSV + tp, certIDStr, certLookup.IssuedDN, certLookup.IssuerDN, @@ -336,35 +381,35 @@ func generateAuditReport( store.Type, store.Machine, store.Path, - "false", - "true", - "true", + "false", // Add to store + "true", // Remove from store + "false", // Is Deployed getCurrentTime(""), } log.Trace(). - Str("thumbprint", cert). + Str("thumbprint", tp). Strs("row", row). Msg("Appending data row") data = append(data, row) log.Debug(). - Str("thumbprint", cert). + Str("thumbprint", tp). Strs("row", row). Msg("Writing data row to CSV") wErr := csvWriter.Write(row) if wErr != nil { log.Error(). Err(wErr). - Str("thumbprint", cert). + Str("thumbprint", tp). Str("output_file", outputFilePath). Strs("row", row). Msg("Error writing row to CSV") - errs = append(errs, wErr) - //todo: continue? } - log.Debug().Str("thumbprint", cert).Msg("Adding remove action to actions map") - actions[cert] = append( - actions[cert], ROTAction{ - Thumbprint: cert, + 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, @@ -373,37 +418,6 @@ func generateAuditReport( RemoveCert: true, }, ) - } else { - // Cert is not deployed to this store do nothing - log.Info().Str("thumbprint", cert).Str( - "store_id", - store.ID, - ).Msg("Cert is not deployed to store, skipping") - row := []string{ - cert, - certIDStr, - certLookup.IssuedDN, - certLookup.IssuerDN, - store.ID, - store.Type, - store.Machine, - store.Path, - "false", - "false", - "false", - getCurrentTime(""), - } - log.Trace().Str("thumbprint", cert).Strs("row", row).Msg("Appending data row") - data = append(data, row) - log.Debug().Str("thumbprint", cert).Strs("row", row).Msg("Writing data row to CSV") - wErr := csvWriter.Write(row) - if wErr != nil { - log.Error().Err(wErr).Str("thumbprint", cert).Str("output_file", outputFilePath).Strs( - "row", - row, - ).Msg("Error writing row to CSV") - errs = append(errs, wErr) - } } } } diff --git a/go.mod b/go.mod index d1cdb43..74cf5a6 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 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 diff --git a/go.sum b/go.sum index 4c9a8e3..b4e5734 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/Keyfactor/keyfactor-go-client v1.4.3 h1:CmGvWcuIbDRFM0PfYOQH6UdtAgplv 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= From ad76356bd16931df6df15a9379cb4b542d1ab7cb Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 9 May 2024 14:43:20 -0700 Subject: [PATCH 8/9] fix(rot): remove cert errors sent to stdout Signed-off-by: sbailey <1661003+spbsoluble@users.noreply.github.com> --- cmd/rot.go | 18 +++++++++++++----- go.mod | 14 +++++++------- go.sum | 28 ++++++++++++++-------------- 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/cmd/rot.go b/cmd/rot.go index eaf0ad8..7e66206 100644 --- a/cmd/rot.go +++ b/cmd/rot.go @@ -276,6 +276,7 @@ func generateAuditReport( StorePath: store.Path, AddCert: true, RemoveCert: false, + Deployed: false, }, ) } @@ -383,7 +384,7 @@ func generateAuditReport( store.Path, "false", // Add to store "true", // Remove from store - "false", // Is Deployed + "true", // Is Deployed getCurrentTime(""), } log.Trace(). @@ -406,7 +407,7 @@ func generateAuditReport( } log.Debug(). Str("thumbprint", tp). - Msg("Adding 'add' action to actions map") + Msg("Adding 'remove' action to actions map") actions[tp] = append( actions[tp], ROTAction{ Thumbprint: tp, @@ -416,6 +417,7 @@ func generateAuditReport( StorePath: store.Path, AddCert: false, RemoveCert: true, + Deployed: true, }, ) } @@ -560,11 +562,10 @@ func reconcileRoots(actions map[string][]ROTAction, kfClient *api.Client, report _, err := kfClient.AddCertificateToStores(&addReq) if err != nil { fmt.Printf( - "[ERROR] adding cert %s (%d) to store %s (%s): %s\n", + "ERROR adding cert %s(%d) to store %s: %s\n", a.Thumbprint, a.CertID, a.StoreID, - a.StorePath, err, ) log.Error().Err(err).Str("thumbprint", thumbprint).Str( @@ -587,7 +588,7 @@ func reconcileRoots(actions map[string][]ROTAction, kfClient *api.Client, report ).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 @@ -620,6 +621,13 @@ func reconcileRoots(actions map[string][]ROTAction, kfClient *api.Client, report "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( diff --git a/go.mod b/go.mod index 74cf5a6..28aca46 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,8 @@ go 1.21 require ( github.com/AlecAivazis/survey/v2 v2.3.7 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.10.0 - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 + 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.8 @@ -19,7 +19,7 @@ require ( github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 - golang.org/x/crypto v0.21.0 + golang.org/x/crypto v0.23.0 gopkg.in/yaml.v3 v3.0.1 //github.com/google/go-cmp/cmp v0.5.9 ) @@ -41,8 +41,8 @@ require ( 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.22.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/term v0.18.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 b4e5734..be3670e 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ 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.10.0 h1:n1DH8TPV4qqPTje2RcUBYwtrTWlabVp4n46+74X2pn4= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.10.0/go.mod h1:HDcZnuGbiyppErN6lB+idp4CKhjbc8gwjto6OPpyggM= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo= +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.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= @@ -83,14 +83,14 @@ go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdH 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.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +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.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +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= @@ -103,18 +103,18 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc 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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.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.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +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= From 12b394659122a875a6e7101850f5a01c14e58d8a Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 13 May 2024 07:30:34 -0700 Subject: [PATCH 9/9] fix(rot): Add `--collection` flag back for cert template gen Signed-off-by: sbailey <1661003+spbsoluble@users.noreply.github.com> --- cmd/rot.go | 9 +++++++++ cmd/rot_models.go | 1 + 2 files changed, 10 insertions(+) diff --git a/cmd/rot.go b/cmd/rot.go index 7e66206..05abc87 100644 --- a/cmd/rot.go +++ b/cmd/rot.go @@ -411,6 +411,7 @@ func generateAuditReport( actions[tp] = append( actions[tp], ROTAction{ Thumbprint: tp, + StoreAlias: "", //TODO get this value CertID: certID, StoreID: store.ID, StoreType: store.Type, @@ -1825,6 +1826,7 @@ func init() { storeTypes []string containerNames []string subjectNames []string + collections []string ) storesCmd.AddCommand(rotCmd) @@ -1941,6 +1943,13 @@ func init() { []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", diff --git a/cmd/rot_models.go b/cmd/rot_models.go index 9d1e9af..84ed203 100644 --- a/cmd/rot_models.go +++ b/cmd/rot_models.go @@ -116,6 +116,7 @@ func (r *ROTCert) toCSV() string { 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"`