From f90c0ff11803b8da0b41dfcd8804d9c799d398fb Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 3 Oct 2022 05:11:29 -0700 Subject: [PATCH 01/14] chore: Bump API client version feat(util): Base implementation of `root of trust` --- .github/workflows/release.yml | 3 +- .goreleaser.yml | 37 +++- GNUmakefile | 6 +- cmd/rot.go | 346 ++++++++++++++++++++++++++++------ go.mod | 2 +- go.sum | 4 +- integration-manifest.json | 11 ++ 7 files changed, 344 insertions(+), 65 deletions(-) create mode 100644 integration-manifest.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 15e1dbf..96ceb10 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,4 +38,5 @@ jobs: env: # GitHub sets the GITHUB_TOKEN secret automatically. GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} \ No newline at end of file + GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} + GPG_TTY: $(tty) \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml index e9073ea..d184672 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -33,7 +33,9 @@ archives: - format: zip name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' checksum: - extra_files: [] + extra_files: + - glob: 'integration-manifest.json' + name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' algorithm: sha256 signs: @@ -49,8 +51,35 @@ signs: - "--detach-sign" - "${artifact}" release: - extra_files: [] + extra_files: + - glob: 'integration-manifest.json' + name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' # If you want to manually examine the release before its live, uncomment this line: - draft: true + # draft: true changelog: - skip: true \ No newline at end of file + sort: asc + use: github + filters: + exclude: + - '^test:' + - '^chore' + - 'merge conflict' + - Merge pull request + - Merge remote-tracking branch + - Merge branch + - go mod tidy + groups: + - title: Dependency updates + regexp: "^.*(feat|fix)\\(deps\\)*:+.*$" + order: 300 + - title: 'New Features' + regexp: "^.*feat[(\\w)]*:+.*$" + order: 100 + - title: 'Bug fixes' + regexp: "^.*fix[(\\w)]*:+.*$" + order: 200 + - title: 'Documentation updates' + regexp: "^.*docs[(\\w)]*:+.*$" + order: 400 + - title: Other work + order: 9999 diff --git a/GNUmakefile b/GNUmakefile index 42e8a9b..ace2ad1 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -6,7 +6,7 @@ NAMESPACE=keyfactor WEBSITE_REPO=https://github.com/Keyfactor/kfutil NAME=kfutil BINARY=${NAME} -VERSION=0.0.2 +VERSION=v0.0.3 OS_ARCH := $(shell go env GOOS)_$(shell go env GOARCH) BASEDIR := ${HOME}/go/bin INSTALLDIR := ${BASEDIR} @@ -46,8 +46,8 @@ fmt: gofmt -w $(GOFMT_FILES) prerelease: - git tag -d $(VERSION) - git push origin :$(VERSION) + git tag -d $(VERSION) || true + git push origin :$(VERSION) || true git tag $(VERSION) git push origin $(VERSION) diff --git a/cmd/rot.go b/cmd/rot.go index 90d7288..e647de0 100644 --- a/cmd/rot.go +++ b/cmd/rot.go @@ -11,6 +11,7 @@ import ( "fmt" "log" "os" + "strconv" "strings" "github.com/Keyfactor/keyfactor-go-client/api" @@ -18,22 +19,36 @@ import ( ) type StoreCSVEntry struct { - Id string `json:"id"` - Type string `json:"type"` - Machine string `json:"address"` - Path string `json:"path"` + 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 string `json:"id"` - ThumbPrint string `json:"thumbprint"` + Id int `json:"id;omitempty"` + ThumbPrint string `json:"thumbprint;omitempty"` + CN string `json:"cn;omitempty"` + Locations []api.CertificateLocations `json:"locations;omitempty"` } -// rotCmd represents the rot command -var rotCmd = &cobra.Command{ - Use: "rot", - Short: "Root Of Trust", - Long: `Root Of Trust: Will parse a CSV and attempt to enroll a cert or set of certs into a list of cert stores.`, +type RotAction struct { + StoreId string + StoreType string + Thumbprint string + CertId int + Add bool + Remove bool +} + +// rotAuditCmd represents the rot command +var rotAuditCmd = &cobra.Command{ + Use: "rot-audit", + Short: "Root Of Trust Audit", + 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.`, Run: func(cmd *cobra.Command, args []string) { var lookupFailures []string kfClient, _ := initClient() @@ -62,78 +77,291 @@ var rotCmd = &cobra.Command{ lookupFailures = append(lookupFailures, strings.Join(entry, ",")) continue } + + log.Printf("[DEBUG] Store: %s", apiResp) + inventory, invErr := kfClient.GetCertStoreInventory(entry[0]) + if invErr != nil { + log.Fatal("[ERROR] Error getting cert store inventory: %s", invErr) + } stores[entry[0]] = StoreCSVEntry{ - Id: entry[0], - Type: entry[1], - Machine: entry[2], - Path: entry[3], + Id: entry[0], + Type: entry[1], + Machine: entry[2], + Path: entry[3], + Thumbprints: inventory.Thumbprints, + Serials: inventory.Serials, + Ids: inventory.Ids, } - log.Printf("[DEBUG] Store: %s", apiResp) + } storesJson, _ := json.Marshal(stores) fmt.Println(string(storesJson)) // Read in the add addCerts CSV - var addCerts = make(map[string]RotCert) + var certsToAdd = make(map[string]string) if addRootsFile != "" { - addCerts, err := readCertsFile(addRootsFile) - if err != nil { - log.Fatalf("Error reading addCerts file: %s", err) - } - addCertsJson, _ := json.Marshal(addCerts) + certsToAdd, _ = readCertsFile(addRootsFile, kfClient) + //if err != nil { + // log.Fatalf("Error reading addCerts file: %s", err) + //} + addCertsJson, _ := json.Marshal(certsToAdd) fmt.Printf("[DEBUG] add certs JSON: %s", string(addCertsJson)) fmt.Println("add rot called") } else { log.Printf("[DEBUG] No addCerts file specified") - log.Printf("[DEBUG] No addCerts = %s", addCerts) + log.Printf("[DEBUG] No addCerts = %s", certsToAdd) } // Read in the remove removeCerts CSV - var removeCerts = make(map[string]RotCert) + var certsToRemove = make(map[string]string) if removeRootsFile != "" { - removeCerts, err := readCertsFile(removeRootsFile) + certsToRemove, _ = readCertsFile(removeRootsFile, kfClient) + //if err != nil { + // log.Fatalf("Error reading removeCerts file: %s", err) + //} + removeCertsJson, _ := json.Marshal(certsToRemove) + fmt.Println(string(removeCertsJson)) + fmt.Println("remove rot called") + } else { + log.Printf("[DEBUG] No removeCerts file specified") + log.Printf("[DEBUG] No removeCerts = %s", certsToRemove) + } + generateAuditReport(certsToAdd, certsToRemove, stores, kfClient) + }, +} + +var rotReconcileCmd = &cobra.Command{ + Use: "rot-reconcile", + Short: "Root Of Trust", + Long: `Root Of Trust: Will parse a CSV and attempt to enroll a cert or set of certs into a list of cert stores.`, + Run: func(cmd *cobra.Command, args []string) { + var lookupFailures []string + kfClient, _ := initClient() + storesFile, _ := cmd.Flags().GetString("stores") + addRootsFile, _ := cmd.Flags().GetString("add-certs") + removeRootsFile, _ := cmd.Flags().GetString("remove-certs") + dryRun, _ := cmd.Flags().GetBool("dry-run") + log.Printf("[DEBUG] storesFile: %s", storesFile) + log.Printf("[DEBUG] addRootsFile: %s", addRootsFile) + log.Printf("[DEBUG] removeRootsFile: %s", removeRootsFile) + log.Printf("[DEBUG] dryRun: %t", dryRun) + + // 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 _, entry := range storeEntries { + if entry[0] == "StoreId" { + continue // Skip header + } + apiResp, err := kfClient.GetCertificateStoreByID(entry[0]) if err != nil { - log.Fatalf("Error reading removeCerts file: %s", err) + //log.Fatalf("Error getting cert store: %s", err) + log.Printf("[ERROR] Error getting cert store: %s", err) + lookupFailures = append(lookupFailures, strings.Join(entry, ",")) + continue + } + + log.Printf("[DEBUG] Store: %s", apiResp) + inventory, invErr := kfClient.GetCertStoreInventory(entry[0]) + if invErr != nil { + log.Fatal("[ERROR] Error getting cert store inventory: %s", invErr) } - removeCertsJson, _ := json.Marshal(removeCerts) + stores[entry[0]] = StoreCSVEntry{ + Id: entry[0], + Type: entry[1], + Machine: entry[2], + Path: entry[3], + Thumbprints: inventory.Thumbprints, + Serials: inventory.Serials, + Ids: inventory.Ids, + } + + } + storesJson, _ := json.Marshal(stores) + fmt.Println(string(storesJson)) + + // Read in the add addCerts CSV + var certsToAdd = make(map[string]string) + if addRootsFile != "" { + certsToAdd, _ = readCertsFile(addRootsFile, kfClient) + //if err != nil { + // log.Fatalf("Error reading addCerts file: %s", err) + //} + addCertsJson, _ := json.Marshal(certsToAdd) + fmt.Printf("[DEBUG] add certs JSON: %s", string(addCertsJson)) + fmt.Println("add rot called") + } else { + log.Printf("[DEBUG] No addCerts file specified") + log.Printf("[DEBUG] No addCerts = %s", certsToAdd) + } + + // Read in the remove removeCerts CSV + var certsToRemove = make(map[string]string) + if removeRootsFile != "" { + certsToRemove, _ = readCertsFile(removeRootsFile, kfClient) + //if err != nil { + // log.Fatalf("Error reading removeCerts file: %s", err) + //} + removeCertsJson, _ := json.Marshal(certsToRemove) fmt.Println(string(removeCertsJson)) fmt.Println("remove rot called") } else { log.Printf("[DEBUG] No removeCerts file specified") - log.Printf("[DEBUG] No removeCerts = %s", removeCerts) + log.Printf("[DEBUG] No removeCerts = %s", certsToRemove) } + _, actions, err := generateAuditReport(certsToAdd, certsToRemove, stores, kfClient) + if err != nil { + log.Fatalf("[ERROR] Error generating audit report: %s", err) + } + reconcileRoots(actions, kfClient, dryRun) }, } -func readCertsFile(certsFilePath string) (map[string]RotCert, error) { +func reconcileRoots(actions map[string]RotAction, kfClient *api.Client, dryRun bool) { + for thumbprint, action := range actions { + if action.Add { + log.Printf("[INFO] Adding cert %s to store %s", thumbprint, action.StoreId) + if !dryRun { + cStore := api.CertificateStore{ + CertificateStoreId: action.StoreId, + } + var stores []api.CertificateStore + stores = append(stores, cStore) + schedule := &api.InventorySchedule{ + Immediate: boolToPointer(true), + } + addReq := api.AddCertificateToStore{ + CertificateId: action.CertId, + CertificateStores: &stores, + InventorySchedule: schedule, + } + _, err := kfClient.AddCertificateToStores(&addReq) + if err != nil { + log.Fatalf("[ERROR] Error adding cert to store: %s", err) + } + } + } else if action.Remove { + log.Printf("[INFO] Removing cert from store %s", action.StoreId) + if !dryRun { + cStore := api.CertificateStore{ + CertificateStoreId: action.StoreId, + } + var stores []api.CertificateStore + stores = append(stores, cStore) + schedule := &api.InventorySchedule{ + Immediate: boolToPointer(true), + } + removeReq := api.RemoveCertificateFromStore{ + CertificateId: action.CertId, + Alias: fmt.Sprintf("KeyfactorAdd%d", action.CertId), + CertificateStores: &stores, + InventorySchedule: schedule, + } + _, err := kfClient.RemoveCertificateFromStores(&removeReq) + if err != nil { + log.Fatalf("[ERROR] Error removing cert from store: %s", err) + } + } + } + } +} + +func readCertsFile(certsFilePath string, kfclient *api.Client) (map[string]string, error) { // Read in the cert CSV csvFile, _ := os.Open(certsFilePath) reader := csv.NewReader(bufio.NewReader(csvFile)) certEntries, _ := reader.ReadAll() - var certs = make(map[string]RotCert) + var certs = make(map[string]string) for _, entry := range certEntries { switch entry[0] { case "CertId", "thumbprint", "id", "certId", "Thumbprint": continue // Skip header } + certs[entry[0]] = entry[0] + } + return certs, nil +} - certs[entry[0]] = RotCert{ - ThumbPrint: entry[0], +func generateAuditReport(addCerts map[string]string, removeCerts map[string]string, stores map[string]StoreCSVEntry, kfClient *api.Client) ([][]string, map[string]RotAction, error) { + log.Println("[DEBUG] generateAuditReport called") + var data [][]string + header := []string{"Thumbprint", "StoreId", "StoreType", "Machine", "Path", "AddCert", "RemoveCert", "Deployed"} + data = append(data, header) + csvFile, _ := os.Create("rot_audit.csv") + csvWriter := csv.NewWriter(csvFile) + csvWriter.Write(header) + actions := make(map[string]RotAction) + + for _, cert := range addCerts { + certLookupReq := api.GetCertificateContextArgs{ + IncludeMetadata: boolToPointer(true), + IncludeLocations: boolToPointer(true), + CollectionId: nil, + Thumbprint: cert, + Id: 0, + } + certLookup, err := kfClient.GetCertificateContext(&certLookupReq) + if err != nil { + log.Fatalf("[ERROR] Error looking up cert: %s", err) + continue + } + certId := certLookup.Id + certIdStr := strconv.Itoa(certId) + for _, store := range stores { + if _, ok := store.Thumbprints[cert]; ok { + // Cert is already in the store do nothing + row := []string{cert, certIdStr, certIdStr, store.Id, store.Type, store.Machine, store.Path, "false", "false", "true"} + data = append(data, row) + csvWriter.Write(row) + } else { + // Cert is not deployed to this store and will need to be added + row := []string{cert, certIdStr, store.Id, store.Type, store.Machine, store.Path, "true", "false", "false"} + data = append(data, row) + csvWriter.Write(row) + actions[cert] = RotAction{ + Thumbprint: cert, + CertId: certId, + StoreId: store.Id, + StoreType: store.Type, + Add: true, + Remove: false, + } + } } - // Get certificate context - //args := &api.GetCertificateContextArgs{ - // IncludeMetadata: boolToPointer(true), - // IncludeLocations: boolToPointer(true), - // CollectionId: nil, - // Id: certificateIdInt, - //} - //cResp, err := r.p.client.GetCertificateContext(args) } - return certs, nil + for _, cert := range removeCerts { + for _, store := range stores { + if _, ok := store.Thumbprints[cert]; ok { + // Cert is deployed to this store and will need to be removed + row := []string{cert, store.Id, store.Type, store.Machine, store.Path, "false", "true", "true"} + data = append(data, row) + csvWriter.Write(row) + actions[cert] = RotAction{ + Thumbprint: cert, + StoreId: store.Id, + StoreType: store.Type, + Add: false, + Remove: true, + } + } else { + // Cert is not deployed to this store do nothing + row := []string{cert, store.Id, store.Type, store.Machine, store.Path, "false", "false", "false"} + data = append(data, row) + csvWriter.Write(row) + } + } + } + csvWriter.Flush() + csvFile.Close() + + log.Printf("[DEBUG] data: %s", data) + return data, actions, nil } var rotGenStoreTemplateCmd = &cobra.Command{ - Use: "generate-template-rot", + Use: "rot-generate-template", Short: "For generating Root Of Trust template(s)", Long: `Root Of Trust: Will parse a CSV and attempt to enroll a cert or set of certs into a list of cert stores.`, Run: func(cmd *cobra.Command, args []string) { @@ -210,18 +438,28 @@ func initClient() (*api.Client, error) { } func init() { - storesCmd.AddCommand(rotCmd) var stores string - var certs string - rotCmd.Flags().StringVarP(&stores, "stores", "s", "", "CSV file containing cert stores to enroll into") - rotCmd.MarkFlagRequired("stores") - rotCmd.Flags().StringVarP(&certs, "add-certs", "a", "", "CSV file containing cert(s) to enroll into the defined cert stores") - rotCmd.Flags().StringVarP(&certs, "remove-certs", "r", "", "CSV file containing cert(s) to remove from the defined cert stores") - - rotCmd.Flags().BoolP("dry-run", "d", false, "Dry run mode") - rotCmd.MarkFlagRequired("certs") + var addCerts string + var removeCerts string + + storesCmd.AddCommand(rotAuditCmd) + rotAuditCmd.Flags().StringVarP(&stores, "stores", "s", "", "CSV file containing cert stores to enroll into") + rotAuditCmd.MarkFlagRequired("stores") + 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().BoolP("dry-run", "d", false, "Dry run mode") + rotAuditCmd.MarkFlagRequired("certs") + + storesCmd.AddCommand(rotReconcileCmd) + rotReconcileCmd.Flags().StringVarP(&stores, "stores", "s", "", "CSV file containing cert stores to enroll into") + rotReconcileCmd.MarkFlagRequired("stores") + 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().BoolP("dry-run", "d", false, "Dry run mode") + rotReconcileCmd.MarkFlagRequired("certs") + storesCmd.AddCommand(rotGenStoreTemplateCmd) - rotGenStoreTemplateCmd.Flags().String("outpath", "template.csv", "Output file to write the template to") + rotGenStoreTemplateCmd.Flags().String("outpath", "", "Output file to write the template to") rotGenStoreTemplateCmd.Flags().String("format", "csv", "The type of template to generate. Only `csv` is supported at this time.") rotGenStoreTemplateCmd.Flags().String("type", "stores", "The type of template to generate. Only `certs|stores` are supported at this time.") //rotGenStoreTemplateCmd.MarkFlagRequired("type") @@ -230,9 +468,9 @@ func init() { // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: - // rotCmd.PersistentFlags().String("foo", "", "A help for foo") + // rotAuditCmd.PersistentFlags().String("foo", "", "A help for foo") // Cobra supports local flags which will only run when this command // is called directly, e.g.: - // rotCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + // rotAuditCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } diff --git a/go.mod b/go.mod index 456fb35..e0a220a 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module kfutil go 1.18 require ( - github.com/Keyfactor/keyfactor-go-client v1.0.2 + github.com/Keyfactor/keyfactor-go-client v1.0.3 github.com/spf13/cobra v1.5.0 ) diff --git a/go.sum b/go.sum index 0cff442..ecc46bf 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/Keyfactor/keyfactor-go-client v1.0.2 h1:rBZevcPWPsq7bMtc8p/IIMZRE9k7I05uhHBu79Er6vQ= -github.com/Keyfactor/keyfactor-go-client v1.0.2/go.mod h1:u1M1AjcwiO/Tbvc7EsNl9YTy757hO5wmey1/W/7Qkbs= +github.com/Keyfactor/keyfactor-go-client v1.0.3 h1:0jZOy7tTlM3V4BXl1ag/Y9i7lpTs+ZXGcnbr1+3j5vM= +github.com/Keyfactor/keyfactor-go-client v1.0.3/go.mod h1:u1M1AjcwiO/Tbvc7EsNl9YTy757hO5wmey1/W/7Qkbs= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= diff --git a/integration-manifest.json b/integration-manifest.json new file mode 100644 index 0000000..4eb30a0 --- /dev/null +++ b/integration-manifest.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://keyfactor.github.io/integration-manifest-schema.json", + "integration_type": "api-client", + "name": "kfutil", + "status": "pilot", + "description": "", + "support_level": "kf-community", + "link_github": false, + "update_catalog": false +} + From 478d85f2d4bbc4b7479ffb38e0332a6a93b5a49f Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 3 Oct 2022 12:40:13 -0700 Subject: [PATCH 02/14] fix(rot): Added logic and input params for determining if a store is a `root store`. --- cmd/rot.go | 47 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/cmd/rot.go b/cmd/rot.go index e647de0..bf771ee 100644 --- a/cmd/rot.go +++ b/cmd/rot.go @@ -140,6 +140,9 @@ var rotReconcileCmd = &cobra.Command{ 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") log.Printf("[DEBUG] storesFile: %s", storesFile) log.Printf("[DEBUG] addRootsFile: %s", addRootsFile) @@ -168,6 +171,14 @@ var rotReconcileCmd = &cobra.Command{ if invErr != nil { log.Fatal("[ERROR] 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], @@ -413,8 +424,31 @@ var rotGenStoreTemplateCmd = &cobra.Command{ }} -func isRootStore(client *api.Client) bool { - //client.GetCertInventory() +func isRootStore(st *api.GetStoreByIDResp, inv *api.CertStoreInventory, minCerts int, maxKeys int, maxLeaf int) bool { + certCount := len(inv.Certificates) + if certCount < minCerts { + log.Printf("[DEBUG] Store %s has %d certs, less than the required count of %d", st.Id, certCount, minCerts) + return false + } + leafCount := 0 + keyCount := 0 + for _, cert := range inv.Certificates { + if cert.IssuedDN != cert.IssuerDN { + leafCount++ + if leafCount > maxLeaf { + log.Printf("[DEBUG] Store %s has too many leaf certs", st.Id) + return false + } + } + if inv.Parameters["PrivateKeyEntry"] == "Yes" { + keyCount++ + if keyCount > maxKeys { + log.Printf("[DEBUG] Store %s has too many keys", st.Id) + return false + } + } + } + return true } @@ -441,12 +475,18 @@ func init() { var stores string var addCerts string var removeCerts string + var minCertsInStore int + var maxPrivateKeys int + var maxLeaves int storesCmd.AddCommand(rotAuditCmd) rotAuditCmd.Flags().StringVarP(&stores, "stores", "s", "", "CSV file containing cert stores to enroll into") rotAuditCmd.MarkFlagRequired("stores") 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") + rotAuditCmd.Flags().IntVarP(&maxPrivateKeys, "max-keys", "x", 5, "The max number of private keys that should be in a store to be considered a 'root' store") + rotAuditCmd.Flags().IntVarP(&maxLeaves, "max-leaf-certs", "n", 5, "The max number of non-root-certs that should be in a store to be considered a 'root' store") rotAuditCmd.Flags().BoolP("dry-run", "d", false, "Dry run mode") rotAuditCmd.MarkFlagRequired("certs") @@ -455,6 +495,9 @@ func init() { rotReconcileCmd.MarkFlagRequired("stores") 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") + rotReconcileCmd.Flags().IntVarP(&maxPrivateKeys, "max-keys", "x", 5, "The max number of private keys that should be in a store to be considered a 'root' store") + rotReconcileCmd.Flags().IntVarP(&maxLeaves, "max-leaf-certs", "n", 5, "The max number of non-root-certs that should be in a store to be considered a 'root' store") rotReconcileCmd.Flags().BoolP("dry-run", "d", false, "Dry run mode") rotReconcileCmd.MarkFlagRequired("certs") From 8f9bb986985cb78e3d2f01e0ceabb47c1fbc6a6f Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 6 Oct 2022 10:46:39 -0700 Subject: [PATCH 03/14] feat(stores): Added `stores list` and `stores get --id $store_id` functions that output JSON to stdout. fix(cli): Refactored CLI to put `rot` in its own command sub tree --- cmd/root.go | 30 +++++++++++++---- cmd/rot.go | 93 +++++++++++++++++++++++---------------------------- cmd/stores.go | 56 ++++++++++++++++++++++++++----- 3 files changed, 113 insertions(+), 66 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 7547ab2..8ae07d3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,19 +7,35 @@ package cmd import ( "os" + "github.com/Keyfactor/keyfactor-go-client/api" "github.com/spf13/cobra" + "log" ) +func initClient() (*api.Client, error) { + var clientAuth api.AuthConfig + clientAuth.Username = os.Getenv("KEYFACTOR_USERNAME") + log.Printf("[DEBUG] Username: %s", clientAuth.Username) + clientAuth.Password = os.Getenv("KEYFACTOR_PASSWORD") + log.Printf("[DEBUG] Password: %s", clientAuth.Password) + clientAuth.Domain = os.Getenv("KEYFACTOR_DOMAIN") + log.Printf("[DEBUG] Domain: %s", clientAuth.Domain) + clientAuth.Hostname = os.Getenv("KEYFACTOR_HOSTNAME") + log.Printf("[DEBUG] Hostname: %s", clientAuth.Hostname) + + c, err := api.NewKeyfactorClient(&clientAuth) + + if err != nil { + log.Fatalf("Error creating Keyfactor client: %s", err) + } + return c, err +} + // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "kfutil", - Short: "A brief description of your application", - Long: `A longer description that spans multiple lines and likely contains -examples and usage of using your application. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, + Short: "Keyfactor CLI utilities", + Long: `A CLI wrapper around the Keyfactor Platform API.`, // Uncomment the following line if your bare application // has an action associated with it: // Run: func(cmd *cobra.Command, args []string) { }, diff --git a/cmd/rot.go b/cmd/rot.go index bf771ee..34679c6 100644 --- a/cmd/rot.go +++ b/cmd/rot.go @@ -38,15 +38,25 @@ type RotCert struct { type RotAction struct { StoreId string StoreType string + StorePath string Thumbprint string CertId int Add bool Remove bool } +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.`, + //Run: func(cmd *cobra.Command, args []string) { + // fmt.Println("stores called") + //}, +} + // rotAuditCmd represents the rot command var rotAuditCmd = &cobra.Command{ - Use: "rot-audit", + Use: "audit", Short: "Root Of Trust Audit", 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.`, Run: func(cmd *cobra.Command, args []string) { @@ -70,7 +80,7 @@ var rotAuditCmd = &cobra.Command{ if entry[0] == "StoreId" { continue // Skip header } - apiResp, err := kfClient.GetCertificateStoreByID(entry[0]) + _, err := kfClient.GetCertificateStoreByID(entry[0]) if err != nil { //log.Fatalf("Error getting cert store: %s", err) log.Printf("[ERROR] Error getting cert store: %s", err) @@ -78,7 +88,7 @@ var rotAuditCmd = &cobra.Command{ continue } - log.Printf("[DEBUG] Store: %s", apiResp) + //log.Printf("[DEBUG] Store: %s", apiResp) inventory, invErr := kfClient.GetCertStoreInventory(entry[0]) if invErr != nil { log.Fatal("[ERROR] Error getting cert store inventory: %s", invErr) @@ -94,8 +104,7 @@ var rotAuditCmd = &cobra.Command{ } } - storesJson, _ := json.Marshal(stores) - fmt.Println(string(storesJson)) + //storesJson, _ := json.Marshal(stores) // Read in the add addCerts CSV var certsToAdd = make(map[string]string) @@ -105,8 +114,8 @@ var rotAuditCmd = &cobra.Command{ // log.Fatalf("Error reading addCerts file: %s", err) //} addCertsJson, _ := json.Marshal(certsToAdd) - fmt.Printf("[DEBUG] add certs JSON: %s", string(addCertsJson)) - fmt.Println("add rot called") + log.Printf("[DEBUG] add certs JSON: %s", string(addCertsJson)) + log.Println("[DEBUG] Add ROT called") } else { log.Printf("[DEBUG] No addCerts file specified") log.Printf("[DEBUG] No addCerts = %s", certsToAdd) @@ -131,7 +140,7 @@ var rotAuditCmd = &cobra.Command{ } var rotReconcileCmd = &cobra.Command{ - Use: "rot-reconcile", + Use: "reconcile", Short: "Root Of Trust", Long: `Root Of Trust: Will parse a CSV and attempt to enroll a cert or set of certs into a list of cert stores.`, Run: func(cmd *cobra.Command, args []string) { @@ -166,7 +175,7 @@ var rotReconcileCmd = &cobra.Command{ continue } - log.Printf("[DEBUG] Store: %s", apiResp) + //log.Printf("[DEBUG] Store: %s", apiResp) inventory, invErr := kfClient.GetCertStoreInventory(entry[0]) if invErr != nil { log.Fatal("[ERROR] Error getting cert store inventory: %s", invErr) @@ -190,37 +199,23 @@ var rotReconcileCmd = &cobra.Command{ } } - storesJson, _ := json.Marshal(stores) - fmt.Println(string(storesJson)) // Read in the add addCerts CSV var certsToAdd = make(map[string]string) if addRootsFile != "" { certsToAdd, _ = readCertsFile(addRootsFile, kfClient) - //if err != nil { - // log.Fatalf("Error reading addCerts file: %s", err) - //} - addCertsJson, _ := json.Marshal(certsToAdd) - fmt.Printf("[DEBUG] add certs JSON: %s", string(addCertsJson)) - fmt.Println("add rot called") + log.Printf("[DEBUG] ROT add certs called") } else { log.Printf("[DEBUG] No addCerts file specified") - log.Printf("[DEBUG] No addCerts = %s", certsToAdd) } // Read in the remove removeCerts CSV var certsToRemove = make(map[string]string) if removeRootsFile != "" { certsToRemove, _ = readCertsFile(removeRootsFile, kfClient) - //if err != nil { - // log.Fatalf("Error reading removeCerts file: %s", err) - //} - removeCertsJson, _ := json.Marshal(certsToRemove) - fmt.Println(string(removeCertsJson)) - fmt.Println("remove rot called") + log.Printf("[DEBUG] ROT remove certs called") } else { log.Printf("[DEBUG] No removeCerts file specified") - log.Printf("[DEBUG] No removeCerts = %s", certsToRemove) } _, actions, err := generateAuditReport(certsToAdd, certsToRemove, stores, kfClient) if err != nil { @@ -231,12 +226,18 @@ var rotReconcileCmd = &cobra.Command{ } func reconcileRoots(actions map[string]RotAction, kfClient *api.Client, dryRun bool) { + log.Printf("[DEBUG] Reconciling roots") + if len(actions) == 0 { + log.Printf("[INFO] No actions to take, roots are up-to-date.") + return + } for thumbprint, action := range actions { if action.Add { - log.Printf("[INFO] Adding cert %s to store %s", thumbprint, action.StoreId) + log.Printf("[INFO] Adding cert %s to store %s(%s)", thumbprint, action.StoreId, action.StorePath) if !dryRun { cStore := api.CertificateStore{ CertificateStoreId: action.StoreId, + Overwrite: true, } var stores []api.CertificateStore stores = append(stores, cStore) @@ -252,10 +253,12 @@ func reconcileRoots(actions map[string]RotAction, kfClient *api.Client, dryRun b if err != nil { log.Fatalf("[ERROR] Error adding cert to store: %s", err) } + } else { + log.Printf("[INFO] DRY RUN: Would have added cert %s from store %s", thumbprint, action.StoreId) } } else if action.Remove { - log.Printf("[INFO] Removing cert from store %s", action.StoreId) if !dryRun { + log.Printf("[INFO] Removing cert from store %s", action.StoreId) cStore := api.CertificateStore{ CertificateStoreId: action.StoreId, } @@ -274,7 +277,10 @@ func reconcileRoots(actions map[string]RotAction, kfClient *api.Client, dryRun b if err != nil { log.Fatalf("[ERROR] Error removing cert from store: %s", err) } + } else { + log.Printf("[INFO] DRY RUN: Would have removed cert %s from store %s", thumbprint, action.StoreId) } + } } } @@ -336,6 +342,7 @@ func generateAuditReport(addCerts map[string]string, removeCerts map[string]stri CertId: certId, StoreId: store.Id, StoreType: store.Type, + StorePath: store.Path, Add: true, Remove: false, } @@ -367,12 +374,12 @@ func generateAuditReport(addCerts map[string]string, removeCerts map[string]stri csvWriter.Flush() csvFile.Close() - log.Printf("[DEBUG] data: %s", data) + //log.Printf("[DEBUG] data: %s", data) return data, actions, nil } var rotGenStoreTemplateCmd = &cobra.Command{ - Use: "rot-generate-template", + Use: "generate-template", Short: "For generating Root Of Trust template(s)", Long: `Root Of Trust: Will parse a CSV and attempt to enroll a cert or set of certs into a list of cert stores.`, Run: func(cmd *cobra.Command, args []string) { @@ -452,26 +459,9 @@ func isRootStore(st *api.GetStoreByIDResp, inv *api.CertStoreInventory, minCerts return true } -func initClient() (*api.Client, error) { - var clientAuth api.AuthConfig - clientAuth.Username = os.Getenv("KEYFACTOR_USERNAME") - log.Printf("[DEBUG] Username: %s", clientAuth.Username) - clientAuth.Password = os.Getenv("KEYFACTOR_PASSWORD") - log.Printf("[DEBUG] Password: %s", clientAuth.Password) - clientAuth.Domain = os.Getenv("KEYFACTOR_DOMAIN") - log.Printf("[DEBUG] Domain: %s", clientAuth.Domain) - clientAuth.Hostname = os.Getenv("KEYFACTOR_HOSTNAME") - log.Printf("[DEBUG] Hostname: %s", clientAuth.Hostname) - - c, err := api.NewKeyfactorClient(&clientAuth) - - if err != nil { - log.Fatalf("Error creating Keyfactor client: %s", err) - } - return c, err -} - func init() { + log.SetFlags(log.LstdFlags | log.Lshortfile) + log.SetOutput(os.Stdout) var stores string var addCerts string var removeCerts string @@ -479,7 +469,8 @@ func init() { var maxPrivateKeys int var maxLeaves int - storesCmd.AddCommand(rotAuditCmd) + storesCmd.AddCommand(rotCmd) + rotCmd.AddCommand(rotAuditCmd) rotAuditCmd.Flags().StringVarP(&stores, "stores", "s", "", "CSV file containing cert stores to enroll into") rotAuditCmd.MarkFlagRequired("stores") rotAuditCmd.Flags().StringVarP(&addCerts, "add-certs", "a", "", "CSV file containing cert(s) to enroll into the defined cert stores") @@ -490,7 +481,7 @@ func init() { rotAuditCmd.Flags().BoolP("dry-run", "d", false, "Dry run mode") rotAuditCmd.MarkFlagRequired("certs") - storesCmd.AddCommand(rotReconcileCmd) + rotCmd.AddCommand(rotReconcileCmd) rotReconcileCmd.Flags().StringVarP(&stores, "stores", "s", "", "CSV file containing cert stores to enroll into") rotReconcileCmd.MarkFlagRequired("stores") rotReconcileCmd.Flags().StringVarP(&addCerts, "add-certs", "a", "", "CSV file containing cert(s) to enroll into the defined cert stores") @@ -501,7 +492,7 @@ func init() { rotReconcileCmd.Flags().BoolP("dry-run", "d", false, "Dry run mode") rotReconcileCmd.MarkFlagRequired("certs") - storesCmd.AddCommand(rotGenStoreTemplateCmd) + rotCmd.AddCommand(rotGenStoreTemplateCmd) rotGenStoreTemplateCmd.Flags().String("outpath", "", "Output file to write the template to") rotGenStoreTemplateCmd.Flags().String("format", "csv", "The type of template to generate. Only `csv` is supported at this time.") rotGenStoreTemplateCmd.Flags().String("type", "stores", "The type of template to generate. Only `certs|stores` are supported at this time.") diff --git a/cmd/stores.go b/cmd/stores.go index c54e735..5753fd9 100644 --- a/cmd/stores.go +++ b/cmd/stores.go @@ -5,28 +5,68 @@ Copyright © 2022 NAME HERE package cmd import ( + "encoding/json" "fmt" - "github.com/spf13/cobra" + "io/ioutil" + "log" ) // storesCmd represents the stores command var storesCmd = &cobra.Command{ Use: "stores", - Short: "A brief description of your command", - Long: `A longer description that spans multiple lines and likely contains examples -and usage of using your command. For example: + Short: "Keyfactor certificate stores APIs and utilities.", + Long: `A collections of APIs and utilities for interacting with Keyfactor certificate stores.`, + //Run: func(cmd *cobra.Command, args []string) { + // fmt.Println("stores called") + //}, +} + +var storesListCmd = &cobra.Command{ + Use: "list", + Short: "List certificate stores.", + Long: `List certificate stores.`, + Run: func(cmd *cobra.Command, args []string) { + log.SetOutput(ioutil.Discard) + kfClient, _ := initClient() + stores, err := kfClient.ListCertificateStores() + if err != nil { + log.Printf("Error: %s", err) + } + output, jErr := json.Marshal(stores) + if jErr != nil { + log.Printf("Error: %s", jErr) + } + fmt.Printf("%s", output) + }, +} -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, +var storesGetCmd = &cobra.Command{ + Use: "get", + Short: "Get a certificate store by ID.", + Long: `Get a certificate store by ID.`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println("stores called") + log.SetOutput(ioutil.Discard) //todo: remove this and set it global + storeId, _ := cmd.Flags().GetString("id") + kfClient, _ := initClient() + stores, err := kfClient.GetCertificateStoreByID(storeId) + if err != nil { + log.Printf("Error: %s", err) + } + output, jErr := json.Marshal(stores) + if jErr != nil { + log.Printf("Error: %s", jErr) + } + fmt.Printf("%s", output) }, } func init() { + var storeId string rootCmd.AddCommand(storesCmd) + storesCmd.AddCommand(storesListCmd) + storesCmd.AddCommand(storesGetCmd) + storesGetCmd.Flags().StringVarP(&storeId, "id", "i", "", "Id of the certificate store to get.") // Here you will define your flags and configuration settings. From b45d874bcc7728c474eee79847365884ae220e6a Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 6 Oct 2022 12:23:30 -0700 Subject: [PATCH 04/14] chore(mod): Bump Keyfactor API client version --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e0a220a..2b6d7f3 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module kfutil go 1.18 require ( - github.com/Keyfactor/keyfactor-go-client v1.0.3 + github.com/Keyfactor/keyfactor-go-client v1.1.0-rc1 github.com/spf13/cobra v1.5.0 ) diff --git a/go.sum b/go.sum index ecc46bf..cae68e3 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/Keyfactor/keyfactor-go-client v1.0.3 h1:0jZOy7tTlM3V4BXl1ag/Y9i7lpTs+ZXGcnbr1+3j5vM= -github.com/Keyfactor/keyfactor-go-client v1.0.3/go.mod h1:u1M1AjcwiO/Tbvc7EsNl9YTy757hO5wmey1/W/7Qkbs= +github.com/Keyfactor/keyfactor-go-client v1.1.0-rc1 h1:OBMIRIYeK9R0GmW/KGIuWty4axmODsjLglFu5Z1HnBo= +github.com/Keyfactor/keyfactor-go-client v1.1.0-rc1/go.mod h1:u1M1AjcwiO/Tbvc7EsNl9YTy757hO5wmey1/W/7Qkbs= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= From 94d25242bfe5060f2e5a8616719d7d61db965c4f Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 10 Oct 2022 07:35:27 -0700 Subject: [PATCH 05/14] chore(lint): Minor linting fix(cli-rot): Added some flag validation around template generation. --- GNUmakefile | 5 +- cmd/rot.go | 932 +++++++++++++++++++++++++++++++------------------- cmd/stores.go | 2 +- 3 files changed, 582 insertions(+), 357 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index ace2ad1..3a24104 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -6,7 +6,7 @@ NAMESPACE=keyfactor WEBSITE_REPO=https://github.com/Keyfactor/kfutil NAME=kfutil BINARY=${NAME} -VERSION=v0.0.3 +VERSION=v0.0.4 OS_ARCH := $(shell go env GOOS)_$(shell go env GOARCH) BASEDIR := ${HOME}/go/bin INSTALLDIR := ${BASEDIR} @@ -35,7 +35,8 @@ install: rm -rf ${INSTALLDIR}/${BINARY} mkdir -p ${INSTALLDIR} chmod oug+x ${BINARY} - mv ${BINARY} ${INSTALLDIR} + cp ${BINARY} ${INSTALLDIR} + mv ${BINARY} /usr/local/bin/${BINARY} test: diff --git a/cmd/rot.go b/cmd/rot.go index 34679c6..d7a3f38 100644 --- a/cmd/rot.go +++ b/cmd/rot.go @@ -1,14 +1,13 @@ -/* -Copyright © 2022 NAME HERE - -*/ +// Package cmd /* package cmd import ( "bufio" "encoding/csv" "encoding/json" + "errors" "fmt" + "io/ioutil" "log" "os" "strconv" @@ -18,225 +17,206 @@ import ( "github.com/spf13/cobra" ) +type templateType string type StoreCSVEntry struct { - Id string `json:"id"` + 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"` + 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 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 +type ROTAction struct { + StoreID string StoreType string StorePath string Thumbprint string - CertId int + CertID int Add bool Remove bool } -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.`, - //Run: func(cmd *cobra.Command, args []string) { - // fmt.Println("stores called") - //}, -} +const ( + tTypeCerts templateType = "certs" + tTypeStores templateType = "stores" + tTypeActions templateType = "actions" +) -// rotAuditCmd represents the rot command -var rotAuditCmd = &cobra.Command{ - Use: "audit", - Short: "Root Of Trust Audit", - 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.`, - Run: func(cmd *cobra.Command, args []string) { - var lookupFailures []string - kfClient, _ := initClient() - storesFile, _ := cmd.Flags().GetString("stores") - addRootsFile, _ := cmd.Flags().GetString("add-certs") - removeRootsFile, _ := cmd.Flags().GetString("remove-certs") - dryRun, _ := cmd.Flags().GetBool("dry-run") - log.Printf("[DEBUG] storesFile: %s", storesFile) - log.Printf("[DEBUG] addRootsFile: %s", addRootsFile) - log.Printf("[DEBUG] removeRootsFile: %s", removeRootsFile) - log.Printf("[DEBUG] dryRun: %t", dryRun) - - // 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 _, entry := range storeEntries { - if entry[0] == "StoreId" { - continue // Skip header - } - _, err := kfClient.GetCertificateStoreByID(entry[0]) - if err != nil { - //log.Fatalf("Error getting cert store: %s", err) - log.Printf("[ERROR] Error getting cert store: %s", err) - lookupFailures = append(lookupFailures, strings.Join(entry, ",")) - continue - } +var ( + AuditHeader = []string{"Thumbprint", "CertID", "StoreID", "StoreType", "Machine", "Path", "AddCert", "RemoveCert", "Deployed"} + StoreHeader = []string{"StoreID", "StoreType", "StoreMachine", "StorePath"} + CertHeader = []string{"Thumbprint"} +) - //log.Printf("[DEBUG] Store: %s", apiResp) - inventory, invErr := kfClient.GetCertStoreInventory(entry[0]) - if invErr != nil { - log.Fatal("[ERROR] Error getting cert store inventory: %s", invErr) - } - stores[entry[0]] = StoreCSVEntry{ - Id: entry[0], - Type: entry[1], - Machine: entry[2], - Path: entry[3], - Thumbprints: inventory.Thumbprints, - Serials: inventory.Serials, - Ids: inventory.Ids, - } +// String is used both by fmt.Print and by Cobra in help text +func (e *templateType) String() string { + return string(*e) +} - } - //storesJson, _ := json.Marshal(stores) - - // Read in the add addCerts CSV - var certsToAdd = make(map[string]string) - if addRootsFile != "" { - certsToAdd, _ = readCertsFile(addRootsFile, kfClient) - //if err != nil { - // log.Fatalf("Error reading addCerts file: %s", err) - //} - addCertsJson, _ := json.Marshal(certsToAdd) - log.Printf("[DEBUG] add certs JSON: %s", string(addCertsJson)) - log.Println("[DEBUG] Add ROT called") - } else { - log.Printf("[DEBUG] No addCerts file specified") - log.Printf("[DEBUG] No addCerts = %s", certsToAdd) - } +// Set must have pointer receiver, so it doesn't change the value of a copy +func (e *templateType) Set(v string) error { + switch v { + case "certs", "stores", "actions": + *e = templateType(v) + return nil + default: + return errors.New(`must be one of "certs", "stores", or "actions"`) + } +} - // Read in the remove removeCerts CSV - var certsToRemove = make(map[string]string) - if removeRootsFile != "" { - certsToRemove, _ = readCertsFile(removeRootsFile, kfClient) - //if err != nil { - // log.Fatalf("Error reading removeCerts file: %s", err) - //} - removeCertsJson, _ := json.Marshal(certsToRemove) - fmt.Println(string(removeCertsJson)) - fmt.Println("remove rot called") - } else { - log.Printf("[DEBUG] No removeCerts file specified") - log.Printf("[DEBUG] No removeCerts = %s", certsToRemove) - } - generateAuditReport(certsToAdd, certsToRemove, stores, kfClient) - }, +// Type is only used in help text +func (e *templateType) Type() string { + return "string" } -var rotReconcileCmd = &cobra.Command{ - Use: "reconcile", - Short: "Root Of Trust", - Long: `Root Of Trust: Will parse a CSV and attempt to enroll a cert or set of certs into a list of cert stores.`, - Run: func(cmd *cobra.Command, args []string) { - var lookupFailures []string - kfClient, _ := initClient() - 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") - log.Printf("[DEBUG] storesFile: %s", storesFile) - log.Printf("[DEBUG] addRootsFile: %s", addRootsFile) - log.Printf("[DEBUG] removeRootsFile: %s", removeRootsFile) - log.Printf("[DEBUG] dryRun: %t", dryRun) - - // 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 _, entry := range storeEntries { - if entry[0] == "StoreId" { - continue // Skip header - } - apiResp, err := kfClient.GetCertificateStoreByID(entry[0]) - if err != nil { - //log.Fatalf("Error getting cert store: %s", err) - log.Printf("[ERROR] Error getting cert store: %s", err) - lookupFailures = append(lookupFailures, strings.Join(entry, ",")) - continue - } +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`", + }, cobra.ShellCompDirectiveDefault +} - //log.Printf("[DEBUG] Store: %s", apiResp) - inventory, invErr := kfClient.GetCertStoreInventory(entry[0]) - if invErr != nil { - log.Fatal("[ERROR] Error getting cert store inventory: %s", invErr) - } +func generateAuditReport(addCerts map[string]string, removeCerts map[string]string, stores map[string]StoreCSVEntry, kfClient *api.Client) ([][]string, map[string]ROTAction, error) { + log.Println("[DEBUG] generateAuditReport called") + var ( + data [][]string + ) + + data = append(data, AuditHeader) + csvFile, fErr := os.Create("rot_audit.csv") + if fErr != nil { + fmt.Printf("%s", fErr) + log.Fatalf("[ERROR] Error creating audit file: %s", fErr) + } + csvWriter := csv.NewWriter(csvFile) + cErr := csvWriter.Write(AuditHeader) + if cErr != nil { + fmt.Printf("%s", cErr) + log.Fatalf("[ERROR] Error writing audit header: %s", cErr) + } + actions := make(map[string]ROTAction) - if !isRootStore(apiResp, inventory, minCerts, maxLeaves, maxKeys) { - log.Printf("[WARN] Store %s is not a root store", apiResp.Id) - continue + for _, cert := range addCerts { + certLookupReq := api.GetCertificateContextArgs{ + IncludeMetadata: boolToPointer(true), + IncludeLocations: boolToPointer(true), + CollectionId: nil, + Thumbprint: cert, + Id: 0, + } + certLookup, err := kfClient.GetCertificateContext(&certLookupReq) + if err != nil { + log.Printf("[ERROR] Error looking up cert: %s\n%v", cert, err) + continue + } + certID := certLookup.Id + certIDStr := strconv.Itoa(certID) + for _, store := range stores { + if _, ok := store.Thumbprints[cert]; ok { + // Cert is already in the store do nothing + row := []string{cert, certIDStr, store.ID, store.Type, store.Machine, store.Path, "false", "false", "true"} + data = append(data, row) + wErr := csvWriter.Write(row) + if wErr != nil { + fmt.Printf("%s", wErr) + log.Printf("[ERROR] Error writing audit row: %s", wErr) + } } 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: inventory.Thumbprints, - Serials: inventory.Serials, - Ids: inventory.Ids, + // Cert is not deployed to this store and will need to be added + row := []string{cert, certIDStr, store.ID, store.Type, store.Machine, store.Path, "true", "false", "false"} + data = append(data, row) + wErr := csvWriter.Write(row) + if wErr != nil { + fmt.Printf("%s", wErr) + log.Printf("[ERROR] Error writing audit row: %s", wErr) + } + actions[cert] = ROTAction{ + Thumbprint: cert, + CertID: certID, + StoreID: store.ID, + StoreType: store.Type, + StorePath: store.Path, + Add: true, + Remove: false, + } } - } - - // 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("[DEBUG] 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") + } + for _, cert := range removeCerts { + certLookupReq := api.GetCertificateContextArgs{ + IncludeMetadata: boolToPointer(true), + IncludeLocations: boolToPointer(true), + CollectionId: nil, + Thumbprint: cert, + Id: 0, } - _, actions, err := generateAuditReport(certsToAdd, certsToRemove, stores, kfClient) + certLookup, err := kfClient.GetCertificateContext(&certLookupReq) if err != nil { - log.Fatalf("[ERROR] Error generating audit report: %s", err) + log.Printf("[ERROR] Error looking up cert: %s", err) + continue + } + certID := certLookup.Id + certIDStr := strconv.Itoa(certID) + for _, store := range stores { + if _, ok := store.Thumbprints[cert]; ok { + // Cert is deployed to this store and will need to be removed + row := []string{cert, certIDStr, store.ID, store.Type, store.Machine, store.Path, "false", "true", "true"} + data = append(data, row) + wErr := csvWriter.Write(row) + if wErr != nil { + fmt.Printf("%s", wErr) + log.Printf("[ERROR] Error writing row to CSV: %s", wErr) + } + actions[cert] = ROTAction{ + Thumbprint: cert, + StoreID: store.ID, + StoreType: store.Type, + Add: false, + Remove: true, + } + } else { + // Cert is not deployed to this store do nothing + row := []string{cert, certIDStr, store.ID, store.Type, store.Machine, store.Path, "false", "false", "false"} + data = append(data, row) + wErr := csvWriter.Write(row) + if wErr != nil { + fmt.Printf("%s", wErr) + log.Printf("[ERROR] Error writing row to CSV: %s", wErr) + } + } } - reconcileRoots(actions, kfClient, dryRun) - }, + } + csvWriter.Flush() + ioErr := csvFile.Close() + if ioErr != nil { + fmt.Println(ioErr) + log.Printf("[ERROR] Error closing audit file: %s", ioErr) + } + fmt.Println("Audit report written to rot_audit.csv") + //log.Printf("[DEBUG] data: %s", data) + return data, actions, nil } -func reconcileRoots(actions map[string]RotAction, kfClient *api.Client, dryRun bool) { +func reconcileRoots(actions map[string]ROTAction, kfClient *api.Client, dryRun bool) error { log.Printf("[DEBUG] Reconciling roots") if len(actions) == 0 { log.Printf("[INFO] No actions to take, roots are up-to-date.") - return + return nil } for thumbprint, action := range actions { if action.Add { - log.Printf("[INFO] Adding cert %s to store %s(%s)", thumbprint, action.StoreId, action.StorePath) + log.Printf("[INFO] Adding cert %s to store %s(%s)", thumbprint, action.StoreID, action.StorePath) if !dryRun { cStore := api.CertificateStore{ - CertificateStoreId: action.StoreId, + CertificateStoreId: action.StoreID, Overwrite: true, } var stores []api.CertificateStore @@ -245,7 +225,7 @@ func reconcileRoots(actions map[string]RotAction, kfClient *api.Client, dryRun b Immediate: boolToPointer(true), } addReq := api.AddCertificateToStore{ - CertificateId: action.CertId, + CertificateId: action.CertID, CertificateStores: &stores, InventorySchedule: schedule, } @@ -254,13 +234,13 @@ func reconcileRoots(actions map[string]RotAction, kfClient *api.Client, dryRun b log.Fatalf("[ERROR] Error adding cert to store: %s", err) } } else { - log.Printf("[INFO] DRY RUN: Would have added cert %s from store %s", thumbprint, action.StoreId) + log.Printf("[INFO] DRY RUN: Would have added cert %s from store %s", thumbprint, action.StoreID) } } else if action.Remove { if !dryRun { - log.Printf("[INFO] Removing cert from store %s", action.StoreId) + log.Printf("[INFO] Removing cert from store %s", action.StoreID) cStore := api.CertificateStore{ - CertificateStoreId: action.StoreId, + CertificateStoreId: action.StoreID, } var stores []api.CertificateStore stores = append(stores, cStore) @@ -268,8 +248,8 @@ func reconcileRoots(actions map[string]RotAction, kfClient *api.Client, dryRun b Immediate: boolToPointer(true), } removeReq := api.RemoveCertificateFromStore{ - CertificateId: action.CertId, - Alias: fmt.Sprintf("KeyfactorAdd%d", action.CertId), + CertificateId: action.CertID, + Alias: fmt.Sprintf("KeyfactorAdd%d", action.CertID), CertificateStores: &stores, InventorySchedule: schedule, } @@ -278,11 +258,12 @@ func reconcileRoots(actions map[string]RotAction, kfClient *api.Client, dryRun b log.Fatalf("[ERROR] Error removing cert from store: %s", err) } } else { - log.Printf("[INFO] DRY RUN: Would have removed cert %s from store %s", thumbprint, action.StoreId) + log.Printf("[INFO] DRY RUN: Would have removed cert %s from store %s", thumbprint, action.StoreID) } } } + return nil } func readCertsFile(certsFilePath string, kfclient *api.Client) (map[string]string, error) { @@ -293,7 +274,7 @@ func readCertsFile(certsFilePath string, kfclient *api.Client) (map[string]strin var certs = make(map[string]string) for _, entry := range certEntries { switch entry[0] { - case "CertId", "thumbprint", "id", "certId", "Thumbprint": + case "CertID", "thumbprint", "id", "CertId", "Thumbprint": continue // Skip header } certs[entry[0]] = entry[0] @@ -301,203 +282,446 @@ func readCertsFile(certsFilePath string, kfclient *api.Client) (map[string]strin return certs, nil } -func generateAuditReport(addCerts map[string]string, removeCerts map[string]string, stores map[string]StoreCSVEntry, kfClient *api.Client) ([][]string, map[string]RotAction, error) { - log.Println("[DEBUG] generateAuditReport called") - var data [][]string - header := []string{"Thumbprint", "StoreId", "StoreType", "Machine", "Path", "AddCert", "RemoveCert", "Deployed"} - data = append(data, header) - csvFile, _ := os.Create("rot_audit.csv") - csvWriter := csv.NewWriter(csvFile) - csvWriter.Write(header) - actions := make(map[string]RotAction) - - for _, cert := range addCerts { - certLookupReq := api.GetCertificateContextArgs{ - IncludeMetadata: boolToPointer(true), - IncludeLocations: boolToPointer(true), - CollectionId: nil, - Thumbprint: cert, - Id: 0, - } - certLookup, err := kfClient.GetCertificateContext(&certLookupReq) - if err != nil { - log.Fatalf("[ERROR] Error looking up cert: %s", err) - continue - } - certId := certLookup.Id - certIdStr := strconv.Itoa(certId) - for _, store := range stores { - if _, ok := store.Thumbprints[cert]; ok { - // Cert is already in the store do nothing - row := []string{cert, certIdStr, certIdStr, store.Id, store.Type, store.Machine, store.Path, "false", "false", "true"} - data = append(data, row) - csvWriter.Write(row) - } else { - // Cert is not deployed to this store and will need to be added - row := []string{cert, certIdStr, store.Id, store.Type, store.Machine, store.Path, "true", "false", "false"} - data = append(data, row) - csvWriter.Write(row) - actions[cert] = RotAction{ - Thumbprint: cert, - CertId: certId, - StoreId: store.Id, - StoreType: store.Type, - StorePath: store.Path, - Add: true, - Remove: false, - } +func isRootStore(st *api.GetStoreByIDResp, invs *[]api.CertStoreInventory, minCerts int, maxKeys int, maxLeaf int) bool { + leafCount := 0 + keyCount := 0 + certCount := 0 + for _, inv := range *invs { + log.Printf("[DEBUG] inv: %v", inv) + certCount += len(inv.Certificates) + + for _, cert := range inv.Certificates { + if cert.IssuedDN != cert.IssuerDN { + leafCount++ } - } - } - for _, cert := range removeCerts { - for _, store := range stores { - if _, ok := store.Thumbprints[cert]; ok { - // Cert is deployed to this store and will need to be removed - row := []string{cert, store.Id, store.Type, store.Machine, store.Path, "false", "true", "true"} - data = append(data, row) - csvWriter.Write(row) - actions[cert] = RotAction{ - Thumbprint: cert, - StoreId: store.Id, - StoreType: store.Type, - Add: false, - Remove: true, - } - } else { - // Cert is not deployed to this store do nothing - row := []string{cert, store.Id, store.Type, store.Machine, store.Path, "false", "false", "false"} - data = append(data, row) - csvWriter.Write(row) + if inv.Parameters["PrivateKeyEntry"] == "Yes" { + keyCount++ } } } - csvWriter.Flush() - csvFile.Close() + if certCount < minCerts { + log.Printf("[DEBUG] Store %s has %d certs, less than the required count of %d", st.Id, certCount, minCerts) + return false + } + if leafCount > maxLeaf { + log.Printf("[DEBUG] Store %s has too many leaf certs", st.Id) + return false + } - //log.Printf("[DEBUG] data: %s", data) - return data, actions, nil -} + if keyCount > maxKeys { + log.Printf("[DEBUG] Store %s has too many keys", st.Id) + return false + } -var rotGenStoreTemplateCmd = &cobra.Command{ - Use: "generate-template", - Short: "For generating Root Of Trust template(s)", - Long: `Root Of Trust: Will parse a CSV and attempt to enroll a cert or set of certs into a list of cert stores.`, - Run: func(cmd *cobra.Command, args []string) { + return true +} - templateType, _ := cmd.Flags().GetString("type") - format, _ := cmd.Flags().GetString("format") - outpath, _ := cmd.Flags().GetString("outpath") +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.`, + //Run: func(cmd *cobra.Command, args []string) { + // fmt.Println("stores called") + //}, + } + rotAuditCmd = &cobra.Command{ + Use: "audit", + Aliases: nil, + SuggestFor: nil, + Short: "Root Of Trust Audit", + 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: nil, + Run: func(cmd *cobra.Command, args []string) { + var lookupFailures []string + kfClient, _ := initClient() + 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") + outPath, _ := cmd.Flags().GetString("outpath") + log.Printf("[DEBUG] storesFile: %s", storesFile) + log.Printf("[DEBUG] addRootsFile: %s", addRootsFile) + log.Printf("[DEBUG] removeRootsFile: %s", removeRootsFile) + log.Printf("[DEBUG] dryRun: %t", dryRun) + log.Printf("[DEBUG] outPath: %s", outPath) + + // 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 _, entry := range storeEntries { + if entry[0] == "StoreID" { + continue // Skip header + } + apiResp, err := kfClient.GetCertificateStoreByID(entry[0]) + if err != nil { + log.Printf("[ERROR] Error getting cert store: %s", err) + lookupFailures = append(lookupFailures, strings.Join(entry, ",")) + continue + } - // Create CSV template file + inventory, invErr := kfClient.GetCertStoreInventory(entry[0]) + if invErr != nil { + log.Printf("[ERROR] Error getting cert store inventory for: %s\n%s", entry[0], invErr) + } - var filePath string - if outpath != "" { - filePath = outpath - } else { - filePath = fmt.Sprintf("%s_template.%s", templateType, format) - } - file, err := os.Create(filePath) - if err != nil { - log.Fatal("Cannot create file", err) - } + 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) + } - switch format { - case "csv": - writer := csv.NewWriter(file) - var data = [][]string{} - switch templateType { - case "stores": - data = [][]string{ - {"StoreId", "StoreType", "StoreMachine", "StorePath"}, + 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), } - case "certs": - data = [][]string{ - {"Thumbprint"}, + 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 + } } + } - csvErr := writer.WriteAll(data) - if csvErr != nil { - fmt.Println(csvErr) + + // Read in the add addCerts CSV + var certsToAdd = make(map[string]string) + if addRootsFile != "" { + certsToAdd, _ = readCertsFile(addRootsFile, kfClient) + //if err != nil { + // log.Fatalf("Error reading addCerts file: %s", err) + //} + addCertsJson, _ := json.Marshal(certsToAdd) + log.Printf("[DEBUG] add certs JSON: %s", string(addCertsJson)) + log.Println("[DEBUG] Add ROT called") + } else { + log.Printf("[DEBUG] No addCerts file specified") + log.Printf("[DEBUG] No addCerts = %s", certsToAdd) } - defer file.Close() - case "json": - writer := bufio.NewWriter(file) - _, err := writer.WriteString("StoreId,StoreType,StoreMachine,StorePath") - if err != nil { - log.Fatal("Cannot write to file", err) + // Read in the remove removeCerts CSV + var certsToRemove = make(map[string]string) + if removeRootsFile != "" { + certsToRemove, rErr := readCertsFile(removeRootsFile, kfClient) + if rErr != nil { + fmt.Printf("Error reading removeCerts file: %s", rErr) + log.Fatalf("Error reading removeCerts file: %s", rErr) + } + removeCertsJSON, _ := json.Marshal(certsToRemove) + fmt.Println(string(removeCertsJSON)) + fmt.Println("remove rot called") + } else { + log.Printf("[DEBUG] No removeCerts file specified") + log.Printf("[DEBUG] No removeCerts = %s", certsToRemove) } - } + _, _, gErr := generateAuditReport(certsToAdd, certsToRemove, stores, kfClient) + if gErr != nil { + log.Fatalf("Error generating audit report: %s", gErr) + } + }, + RunE: 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, + } + rotReconcileCmd = &cobra.Command{ + Use: "reconcile", + Short: "Root Of Trust", + Long: `Root Of Trust: Will parse a CSV and attempt to enroll a cert or set of certs into a list of cert stores.`, + Run: func(cmd *cobra.Command, args []string) { + var lookupFailures []string + kfClient, _ := initClient() + 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") + log.Printf("[DEBUG] storesFile: %s", storesFile) + log.Printf("[DEBUG] addRootsFile: %s", addRootsFile) + log.Printf("[DEBUG] removeRootsFile: %s", removeRootsFile) + log.Printf("[DEBUG] dryRun: %t", dryRun) + + // 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 _, entry := range storeEntries { + if entry[0] == "StoreID" { + continue // Skip header + } + apiResp, err := kfClient.GetCertificateStoreByID(entry[0]) + if err != nil { + //log.Fatalf("Error getting cert store: %s", err) + log.Printf("[ERROR] Error getting cert store: %s", err) + lookupFailures = append(lookupFailures, strings.Join(entry, ",")) + continue + } - }} + //log.Printf("[DEBUG] Store: %s", apiResp) + inventory, invErr := kfClient.GetCertStoreInventory(entry[0]) + if invErr != nil { + log.Fatalf("[ERROR] 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 + } + } -func isRootStore(st *api.GetStoreByIDResp, inv *api.CertStoreInventory, minCerts int, maxKeys int, maxLeaf int) bool { - certCount := len(inv.Certificates) - if certCount < minCerts { - log.Printf("[DEBUG] Store %s has %d certs, less than the required count of %d", st.Id, certCount, minCerts) - return false - } - leafCount := 0 - keyCount := 0 - for _, cert := range inv.Certificates { - if cert.IssuedDN != cert.IssuerDN { - leafCount++ - if leafCount > maxLeaf { - log.Printf("[DEBUG] Store %s has too many leaf certs", st.Id) - return false } - } - if inv.Parameters["PrivateKeyEntry"] == "Yes" { - keyCount++ - if keyCount > maxKeys { - log.Printf("[DEBUG] Store %s has too many keys", st.Id) - return false + + // 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, kfClient) + if err != nil { + log.Fatalf("[ERROR] 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, dryRun) + if rErr != nil { + fmt.Printf("Error reconciling roots: %s", rErr) + log.Fatalf("[ERROR] Error reconciling roots: %s", rErr) + } + }, } + 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 enroll 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, + Run: func(cmd *cobra.Command, args []string) { + + templateType, _ := cmd.Flags().GetString("type") + format, _ := cmd.Flags().GetString("format") + outPath, _ := cmd.Flags().GetString("outPath") + + // Create CSV template file + + var filePath string + if outPath != "" { + filePath = outPath + } else { + filePath = fmt.Sprintf("%s_template.%s", templateType, format) + } + file, err := os.Create(filePath) + if err != nil { + log.Fatal("Cannot create file", err) + } - return true -} + switch format { + case "csv": + writer := csv.NewWriter(file) + var data []string + switch templateType { + case "stores": + data = StoreHeader + case "certs": + data = CertHeader + case "actions": + data = AuditHeader + } + csvErr := writer.WriteAll([][]string{data}) + if csvErr != nil { + fmt.Println(csvErr) + } + defer file.Close() + + case "json": + writer := bufio.NewWriter(file) + _, err := writer.WriteString("StoreID,StoreType,StoreMachine,StorePath") + if err != nil { + log.Fatal("Cannot write to file", err) + } + } + + }, + RunE: 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 init() { log.SetFlags(log.LstdFlags | log.Lshortfile) log.SetOutput(os.Stdout) - var stores string - var addCerts string - var removeCerts string - var minCertsInStore int - var maxPrivateKeys int - var maxLeaves int + log.SetOutput(ioutil.Discard) //todo: remove this and set it global + var ( + stores string + addCerts string + removeCerts string + minCertsInStore int + maxPrivateKeys int + maxLeaves int + tType = tTypeCerts + outPath string + outputFormat string + actions 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.MarkFlagRequired("stores") - 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") - rotAuditCmd.Flags().IntVarP(&maxPrivateKeys, "max-keys", "x", 5, "The max number of private keys that should be in a store to be considered a 'root' store") - rotAuditCmd.Flags().IntVarP(&maxLeaves, "max-leaf-certs", "n", 5, "The max number of non-root-certs that should be in a store to be considered a 'root' store") + 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", 3, + "The minimum number of certs that should be in a store to be considered a 'root' store") + rotAuditCmd.Flags().IntVarP(&maxPrivateKeys, "max-keys", "x", 1, + "The max number of private keys that should be in a store to be considered a 'root' store") + rotAuditCmd.Flags().IntVarP(&maxLeaves, "max-leaf-certs", "n", 0, + "The max number of non-root-certs that should be in a store to be considered a 'root' store") rotAuditCmd.Flags().BoolP("dry-run", "d", false, "Dry run mode") - rotAuditCmd.MarkFlagRequired("certs") + rotAuditCmd.Flags().StringVarP(&outPath, "outpath", "o", "", + "Path to write the audit report file to. If not specified, the file will be written to the current directory.") + // Root of trust `reconcile` command rotCmd.AddCommand(rotReconcileCmd) rotReconcileCmd.Flags().StringVarP(&stores, "stores", "s", "", "CSV file containing cert stores to enroll into") - rotReconcileCmd.MarkFlagRequired("stores") - 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") - rotReconcileCmd.Flags().IntVarP(&maxPrivateKeys, "max-keys", "x", 5, "The max number of private keys that should be in a store to be considered a 'root' store") - rotReconcileCmd.Flags().IntVarP(&maxLeaves, "max-leaf-certs", "n", 5, "The max number of non-root-certs that should be in a store to be considered a 'root' store") + 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().StringVarP(&actions, "actions", "z", "", + "CSV file containing reconciliation actions to perform. If this is specified, the other flags are ignored.") + rotReconcileCmd.Flags().IntVarP(&minCertsInStore, "min-certs", "m", 3, + "The minimum number of certs that should be in a store to be considered a 'root' store") + rotReconcileCmd.Flags().IntVarP(&maxPrivateKeys, "max-keys", "x", 1, + "The max number of private keys that should be in a store to be considered a 'root' store") + rotReconcileCmd.Flags().IntVarP(&maxLeaves, "max-leaf-certs", "n", 0, + "The max number of non-root-certs that should be in a store to be considered a 'root' store") rotReconcileCmd.Flags().BoolP("dry-run", "d", false, "Dry run mode") - rotReconcileCmd.MarkFlagRequired("certs") + //rotReconcileCmd.MarkFlagsRequiredTogether("add-certs", "stores") + //rotReconcileCmd.MarkFlagsRequiredTogether("remove-certs", "stores") + rotReconcileCmd.MarkFlagsMutuallyExclusive("add-certs", "actions") + rotReconcileCmd.MarkFlagsMutuallyExclusive("remove-certs", "actions") + rotReconcileCmd.MarkFlagsMutuallyExclusive("stores", "actions") + // Root of trust `generate` command rotCmd.AddCommand(rotGenStoreTemplateCmd) - rotGenStoreTemplateCmd.Flags().String("outpath", "", "Output file to write the template to") - rotGenStoreTemplateCmd.Flags().String("format", "csv", "The type of template to generate. Only `csv` is supported at this time.") - rotGenStoreTemplateCmd.Flags().String("type", "stores", "The type of template to generate. Only `certs|stores` are supported at this time.") - //rotGenStoreTemplateCmd.MarkFlagRequired("type") - //rotGenStoreTemplateCmd.MarkFlagRequired("format") + 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.RegisterFlagCompletionFunc("type", templateTypeCompletion) + rotGenStoreTemplateCmd.MarkFlagRequired("type") // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command diff --git a/cmd/stores.go b/cmd/stores.go index 5753fd9..5ed3f40 100644 --- a/cmd/stores.go +++ b/cmd/stores.go @@ -66,7 +66,7 @@ func init() { rootCmd.AddCommand(storesCmd) storesCmd.AddCommand(storesListCmd) storesCmd.AddCommand(storesGetCmd) - storesGetCmd.Flags().StringVarP(&storeId, "id", "i", "", "Id of the certificate store to get.") + storesGetCmd.Flags().StringVarP(&storeId, "id", "i", "", "ID of the certificate store to get.") // Here you will define your flags and configuration settings. From 3d4a2f7e8a726e0eeff90fd134e517e134c048d7 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 13 Oct 2022 11:42:50 -0700 Subject: [PATCH 06/14] feat(cli): Adding some basic `store-type` functionality --- cmd/rot.go | 107 ++++++++++++++-------- cmd/storeTypes.go | 221 ++++++++++++++++++++++++++++++++++++++++++++++ store_types.json | 38 ++++++++ 3 files changed, 327 insertions(+), 39 deletions(-) create mode 100644 cmd/storeTypes.go create mode 100644 store_types.json diff --git a/cmd/rot.go b/cmd/rot.go index d7a3f38..4e5fb2a 100644 --- a/cmd/rot.go +++ b/cmd/rot.go @@ -28,19 +28,19 @@ type StoreCSVEntry struct { Ids map[int]bool `json:"ids,omitempty"` } type ROTCert struct { - Id int `json:"id,omitempty"` + 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 - StoreType string - StorePath string - Thumbprint string - CertID int - Add bool - Remove bool + 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"` + Add bool `json:"add,omitempty"` + Remove bool `json:"remove,omitempty"` } const ( @@ -201,7 +201,6 @@ func generateAuditReport(addCerts map[string]string, removeCerts map[string]stri log.Printf("[ERROR] Error closing audit file: %s", ioErr) } fmt.Println("Audit report written to rot_audit.csv") - //log.Printf("[DEBUG] data: %s", data) return data, actions, nil } @@ -260,7 +259,6 @@ func reconcileRoots(actions map[string]ROTAction, kfClient *api.Client, dryRun b } else { log.Printf("[INFO] DRY RUN: Would have removed cert %s from store %s", thumbprint, action.StoreID) } - } } return nil @@ -299,16 +297,16 @@ func isRootStore(st *api.GetStoreByIDResp, invs *[]api.CertStoreInventory, minCe } } } - if certCount < minCerts { + if certCount < minCerts && minCerts >= 0 { log.Printf("[DEBUG] Store %s has %d certs, less than the required count of %d", st.Id, certCount, minCerts) return false } - if leafCount > maxLeaf { + if leafCount > maxLeaf && maxLeaf >= 0 { log.Printf("[DEBUG] Store %s has too many leaf certs", st.Id) return false } - if keyCount > maxKeys { + if keyCount > maxKeys && maxKeys >= 0 { log.Printf("[DEBUG] Store %s has too many keys", st.Id) return false } @@ -321,9 +319,6 @@ var ( Use: "rot", Short: "Root of trust utility", Long: `Root of trust allows you to manage your trusted roots using Keyfactor certificate stores.`, - //Run: func(cmd *cobra.Command, args []string) { - // fmt.Println("stores called") - //}, } rotAuditCmd = &cobra.Command{ Use: "audit", @@ -354,26 +349,30 @@ var ( 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) - log.Printf("[DEBUG] outPath: %s", outPath) - // Read in the stores CSV csvFile, _ := os.Open(storesFile) reader := csv.NewReader(bufio.NewReader(csvFile)) storeEntries, _ := reader.ReadAll() var stores = make(map[string]StoreCSVEntry) + validHeader := false for _, entry := range storeEntries { - if entry[0] == "StoreID" { + 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") + } apiResp, err := kfClient.GetCertificateStoreByID(entry[0]) if err != nil { log.Printf("[ERROR] Error getting cert store: %s", err) - lookupFailures = append(lookupFailures, strings.Join(entry, ",")) + _ = append(lookupFailures, strings.Join(entry, ",")) continue } @@ -420,8 +419,8 @@ var ( //if err != nil { // log.Fatalf("Error reading addCerts file: %s", err) //} - addCertsJson, _ := json.Marshal(certsToAdd) - log.Printf("[DEBUG] add certs JSON: %s", string(addCertsJson)) + addCertsJSON, _ := json.Marshal(certsToAdd) + log.Printf("[DEBUG] add certs JSON: %s", string(addCertsJSON)) log.Println("[DEBUG] Add ROT called") } else { log.Printf("[DEBUG] No addCerts file specified") @@ -466,9 +465,24 @@ var ( SuggestionsMinimumDistance: 0, } rotReconcileCmd = &cobra.Command{ - Use: "reconcile", - Short: "Root Of Trust", - Long: `Root Of Trust: Will parse a CSV and attempt to enroll a cert or set of certs into a list of cert stores.`, + Use: "reconcile", + Aliases: nil, + SuggestFor: nil, + Short: "Root Of Trust", + Long: `Root Of Trust: Will parse a CSV and attempt to enroll 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, Run: func(cmd *cobra.Command, args []string) { var lookupFailures []string kfClient, _ := initClient() @@ -495,7 +509,6 @@ var ( } apiResp, err := kfClient.GetCertificateStoreByID(entry[0]) if err != nil { - //log.Fatalf("Error getting cert store: %s", err) log.Printf("[ERROR] Error getting cert store: %s", err) lookupFailures = append(lookupFailures, strings.Join(entry, ",")) continue @@ -569,6 +582,22 @@ var ( log.Fatalf("[ERROR] Error reconciling roots: %s", rErr) } }, + RunE: 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", @@ -680,12 +709,12 @@ func init() { "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", 3, - "The minimum number of certs that should be in a store to be considered a 'root' store") - rotAuditCmd.Flags().IntVarP(&maxPrivateKeys, "max-keys", "x", 1, - "The max number of private keys that should be in a store to be considered a 'root' store") - rotAuditCmd.Flags().IntVarP(&maxLeaves, "max-leaf-certs", "n", 0, - "The max number of non-root-certs that should be in a store to be considered a 'root' store") + 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", "x", -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", "n", -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.") @@ -699,12 +728,12 @@ func init() { "CSV file containing cert(s) to remove from the defined cert stores") rotReconcileCmd.Flags().StringVarP(&actions, "actions", "z", "", "CSV file containing reconciliation actions to perform. If this is specified, the other flags are ignored.") - rotReconcileCmd.Flags().IntVarP(&minCertsInStore, "min-certs", "m", 3, - "The minimum number of certs that should be in a store to be considered a 'root' store") - rotReconcileCmd.Flags().IntVarP(&maxPrivateKeys, "max-keys", "x", 1, - "The max number of private keys that should be in a store to be considered a 'root' store") - rotReconcileCmd.Flags().IntVarP(&maxLeaves, "max-leaf-certs", "n", 0, - "The max number of non-root-certs that should be in a store to be considered a 'root' store") + 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", "x", -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", "n", -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.MarkFlagsRequiredTogether("add-certs", "stores") //rotReconcileCmd.MarkFlagsRequiredTogether("remove-certs", "stores") diff --git a/cmd/storeTypes.go b/cmd/storeTypes.go new file mode 100644 index 0000000..caf18b8 --- /dev/null +++ b/cmd/storeTypes.go @@ -0,0 +1,221 @@ +/* +Copyright © 2022 NAME HERE + +*/ +package cmd + +import ( + "encoding/json" + "fmt" + "github.com/Keyfactor/keyfactor-go-client/api" + "io/ioutil" + "log" + + "github.com/spf13/cobra" +) + +// storeTypesCmd represents the storeTypes command +var storeTypesCmd = &cobra.Command{ + Use: "store-types", + Short: "Keyfactor certificate store types APIs and utilities.", + Long: `A collections of APIs and utilities for interacting with Keyfactor certificate store types.`, +} + +// storesTypesListCmd represents the list command +var storesTypesListCmd = &cobra.Command{ + Use: "list", + Short: "List certificate store types.", + Long: `List certificate store types.`, + Run: func(cmd *cobra.Command, args []string) { + log.SetOutput(ioutil.Discard) + kfClient, _ := initClient() + storeTypes, err := kfClient.ListCertificateStoreTypes() + if err != nil { + log.Printf("Error: %s", err) + fmt.Printf("Error: %s\n", err) + return + } + output, jErr := json.Marshal(storeTypes) + if jErr != nil { + log.Printf("Error: %s", jErr) + } + fmt.Printf("%s", output) + }, +} + +var storesTypeGetCmd = &cobra.Command{ + Use: "get", + Short: "Get a specific store type by either name or ID.", + Long: `Get a specific store type by either name or ID.`, + Run: func(cmd *cobra.Command, args []string) { + log.SetOutput(ioutil.Discard) + id, _ := cmd.Flags().GetInt("id") + name, _ := cmd.Flags().GetString("name") + kfClient, _ := initClient() + var st interface{} + // Check inputs + if id < 0 && name == "" { + log.Printf("Error: ID must be a positive integer.") + fmt.Printf("Error: ID must be a positive integer.\n") + return + } else if id >= 0 && name != "" { + log.Printf("Error: ID and Name are mutually exclusive.") + fmt.Printf("Error: ID and Name are mutually exclusive.\n") + return + } else if id >= 0 { + st = id + } else if name != "" { + st = name + } else { + log.Printf("Error: Invalid input.") + fmt.Printf("Error: Invalid input.\n") + return + } + + storeTypes, err := kfClient.GetCertificateStoreType(st) + if err != nil { + log.Printf("Error: %s", err) + fmt.Printf("Error: %s\n", err) + return + } + output, jErr := json.Marshal(storeTypes) + if jErr != nil { + log.Printf("Error: %s", jErr) + } + fmt.Printf("%s", output) + }, +} + +var storesTypeCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new certificate store type in Keyfactor.", + Long: `Create a new certificate store type in Keyfactor.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("create called") + }, +} + +var storesTypeUpdateCmd = &cobra.Command{ + Use: "update", + Short: "Update a certificate store type in Keyfactor.", + Long: `Update a certificate store type in Keyfactor.`, + Run: func(cmd *cobra.Command, args []string) { + log.SetOutput(ioutil.Discard) + fmt.Println("update called") + }, +} + +var storesTypeDeleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete a specific store type by ID.", + Long: `Delete a specific store type by ID.`, + Run: func(cmd *cobra.Command, args []string) { + log.SetOutput(ioutil.Discard) + id, _ := cmd.Flags().GetInt("id") + dryRun, _ := cmd.Flags().GetBool("dry-run") + kfClient, _ := initClient() + _, err := kfClient.GetCertificateStoreType(id) + if err != nil { + log.Printf("Error: %s", err) + fmt.Printf("Error: %s\n", err) + return + } + if dryRun { + fmt.Printf("dry run delete called on certificate store type with ID: %d", id) + } else { + d, err := kfClient.DeleteCertificateStoreType(id) + if err != nil { + log.Printf("Error: %s", err) + fmt.Printf("Error: %s\n", err) + return + } + fmt.Printf("Certificate store type with ID: %d deleted", d.ID) + } + }, +} + +var fetchStoreTypes = &cobra.Command{ + Use: "templates-fetch", + Short: "Fetches store type templates from Keyfactor's Github.", + Long: `Fetches store type templates from Keyfactor's Github.`, + Run: func(cmd *cobra.Command, args []string) { + templates, err := readStoreTypesConfig() + if err != nil { + log.Printf("Error: %s", err) + fmt.Printf("Error: %s\n", err) + return + } + output, jErr := json.Marshal(templates) + if jErr != nil { + log.Printf("Error: %s", jErr) + } + fmt.Println(string(output)) + }, +} + +var generateStoreTypeTemplate = &cobra.Command{ + Use: "templates-generate", + Short: "Generates either a JSON or CSV template file for certificate store type bulk operations.", + Long: `Generates either a JSON or CSV template file for certificate store type bulk operations.`, + Run: func(cmd *cobra.Command, args []string) { + templates, err := readStoreTypesConfig() + if err != nil { + log.Printf("Error: %s", err) + fmt.Printf("Error: %s\n", err) + return + } + output, jErr := json.Marshal(templates) + if jErr != nil { + log.Printf("Error: %s", jErr) + } + fmt.Println(string(output)) + }, +} + +func readStoreTypesConfig() (map[string]api.CertificateStoreType, error) { + content, err := ioutil.ReadFile("./store_types.json") + if err != nil { + log.Fatal("Error when opening file: ", err) + } + + // Now let's unmarshall the data into `payload` + var payload map[string]api.CertificateStoreType + err = json.Unmarshal(content, &payload) + if err != nil { + log.Printf("Error during Unmarshal(): %s", err) + return nil, err + } + return payload, nil +} + +func init() { + rootCmd.AddCommand(storeTypesCmd) + + // GET store type templates + storeTypesCmd.AddCommand(fetchStoreTypes) + + // LIST command + storeTypesCmd.AddCommand(storesTypesListCmd) + + // GET commands + storeTypesCmd.AddCommand(storesTypeGetCmd) + var storeTypeID int + var storeTypeName string + var dryRun bool + storesTypeGetCmd.Flags().IntVarP(&storeTypeID, "id", "i", -1, "ID of the certificate store type to get.") + storesTypeGetCmd.Flags().StringVarP(&storeTypeName, "name", "n", "", "Name of the certificate store type to get.") + storesTypeGetCmd.MarkFlagsMutuallyExclusive("id", "name") + + // CREATE command + storeTypesCmd.AddCommand(storesTypeCreateCmd) + + // UPDATE command + storeTypesCmd.AddCommand(storesTypeUpdateCmd) + + // DELETE command + storeTypesCmd.AddCommand(storesTypeDeleteCmd) + storesTypeDeleteCmd.Flags().IntVarP(&storeTypeID, "id", "i", -1, "ID of the certificate store type to get.") + storesTypeDeleteCmd.Flags().BoolVarP(&dryRun, "dry-run", "t", false, "Specifies whether to perform a dry run.") + storesTypeDeleteCmd.MarkFlagRequired("id") + +} diff --git a/store_types.json b/store_types.json new file mode 100644 index 0000000..78b3e66 --- /dev/null +++ b/store_types.json @@ -0,0 +1,38 @@ +{ + "SAMPLETYPE": { + "Name": "Sample Store Type", + "ShortName": "SAMPLETYPE", + "Capability": "SAMPLETYPE", + "StoreType": 104, + "ImportType": 104, + "LocalStore": false, + "SupportedOperations": { + "Add": false, + "Create": false, + "Discovery": false, + "Enrollment": false, + "Remove": false + }, + "Properties": [], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "", + "PrivateKeyAllowed": "Forbidden", + "JobProperties": [], + "ServerRequired": false, + "PowerShell": false, + "BlueprintAllowed": false, + "CustomAliasAllowed": "Forbidden", + "ServerRegistration": 0, + "InventoryEndpoint": "/AnyInventory/Update", + "InventoryJobType": "0b10c7eb-2cd6-4a3c-9e67-8ee6cc88750d", + "ManagementJobType": "", + "DiscoveryJobType": "", + "EnrollmentJobType": "" + } +} \ No newline at end of file From 7a7be14b5736b3cc8726576fe234d8f4010dc553 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 13 Oct 2022 13:24:30 -0700 Subject: [PATCH 07/14] feat(cli): Added scaffolding for store `containers`, `orchs`(orchestrators/agents), and `status` --- cmd/containers.go | 83 ++++++++++++++++++++++++++++++++++ cmd/orchs.go | 112 ++++++++++++++++++++++++++++++++++++++++++++++ cmd/rot.go | 1 + cmd/status.go | 46 +++++++++++++++++++ 4 files changed, 242 insertions(+) create mode 100644 cmd/containers.go create mode 100644 cmd/orchs.go create mode 100644 cmd/status.go diff --git a/cmd/containers.go b/cmd/containers.go new file mode 100644 index 0000000..72d8afd --- /dev/null +++ b/cmd/containers.go @@ -0,0 +1,83 @@ +/* +Copyright © 2022 NAME HERE + +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// containersCmd represents the containers command +var containersCmd = &cobra.Command{ + Use: "containers", + Short: "Keyfactor CertificateStoreContainer API and utilities.", + Long: `A collections of APIs and utilities for interacting with Keyfactor certificate store containers.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("containers called") + }, +} + +var containersCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create certificate store container.", + Long: `Create certificate store container.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("containers create called") + }, +} + +var containersGetCmd = &cobra.Command{ + Use: "get", + Short: "Get certificate store container by ID or name.", + Long: `Get certificate store container by ID or name.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("containers get called") + }, +} + +var containersUpdateCmd = &cobra.Command{ + Use: "update", + Short: "Update certificate store container by ID or name.", + Long: `Update certificate store container by ID or name.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("containers update called") + }, +} + +var containersDeleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete certificate store container by ID or name.", + Long: `Delete certificate store container by ID or name.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("containers delete called") + }, +} + +var containersListCmd = &cobra.Command{ + Use: "list", + Short: "List certificate store containers.", + Long: `List certificate store containers.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("containers list called") + }, +} + +func init() { + rootCmd.AddCommand(containersCmd) + + // LIST containers command + containersCmd.AddCommand(containersListCmd) + // GET containers command + containersCmd.AddCommand(containersGetCmd) + // CREATE containers command + containersCmd.AddCommand(containersCreateCmd) + // UPDATE containers command + containersCmd.AddCommand(containersUpdateCmd) + // DELETE containers command + containersCmd.AddCommand(containersDeleteCmd) + + // Utility functions +} diff --git a/cmd/orchs.go b/cmd/orchs.go new file mode 100644 index 0000000..ce2b16c --- /dev/null +++ b/cmd/orchs.go @@ -0,0 +1,112 @@ +/* +Copyright © 2022 NAME HERE + +*/ +package cmd + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + + "github.com/spf13/cobra" +) + +// orchsCmd represents the orchs command +var orchsCmd = &cobra.Command{ + Use: "orchs", + Short: "Keyfactor agents APIs and utilities.", + Long: `A collections of APIs and utilities for interacting with Keyfactor orchestrators.`, +} + +var getOrchestratorCmd = &cobra.Command{ + Use: "get", + Short: "Get orchestrator by ID or machine/host name.", + Long: `Get orchestrator by ID or machine/host name.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("orchestrator get called") + }, +} + +var approveOrchestratorCmd = &cobra.Command{ + Use: "approve", + Short: "Approve orchestrator by ID or machine/host name.", + Long: `Approve orchestrator by ID or machine/host name.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("orchestrator approve called") + }, +} + +var disapproveOrchestratorCmd = &cobra.Command{ + Use: "disapprove", + Short: "Disapprove orchestrator by ID or machine/host name.", + Long: `Disapprove orchestrator by ID or machine/host name.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("orchestrator disapprove called") + }, +} + +var resetOrchestratorCmd = &cobra.Command{ + Use: "reset", + Short: "Reset orchestrator by ID or machine/host name.", + Long: `Reset orchestrator by ID or machine/host name.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("orchestrator reset called") + }, +} + +var getLogsOrchestratorCmd = &cobra.Command{ + Use: "logs", + Short: "Get orchestrator logs by ID or machine/host name.", + Long: `Get orchestrator logs by ID or machine/host name.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("orchestrator logs called") + }, +} + +var listOrchestratorsCmd = &cobra.Command{ + Use: "list", + Short: "List orchestrators.", + Long: `Returns a JSON list of Keyfactor orchestrators.`, + Run: func(cmd *cobra.Command, args []string) { + log.SetOutput(ioutil.Discard) + kfClient, _ := initClient() + agents, err := kfClient.GetAgentList() + if err != nil { + log.Printf("Error: %s", err) + } + output, jErr := json.Marshal(agents) + if jErr != nil { + log.Printf("Error: %s", jErr) + } + fmt.Printf("%s", output) + }, +} + +func init() { + rootCmd.AddCommand(orchsCmd) + + // LIST orchestrators command + orchsCmd.AddCommand(listOrchestratorsCmd) + // GET orchestrator command + orchsCmd.AddCommand(getOrchestratorCmd) + // CREATE orchestrator command TODO: API NOT SUPPORTED + //orchsCmd.AddCommand(createOrchestratorCmd) + // UPDATE orchestrator command TODO: API NOT SUPPORTED + //orchsCmd.AddCommand(updateOrchestratorCmd) + // DELETE orchestrator command TODO: API NOT SUPPORTED + //orchsCmd.AddCommand(deleteOrchestratorCmd) + // APPROVE orchestrator command + orchsCmd.AddCommand(approveOrchestratorCmd) + // DISAPPROVE orchestrator command + orchsCmd.AddCommand(disapproveOrchestratorCmd) + // RESET orchestrator command + orchsCmd.AddCommand(resetOrchestratorCmd) + // GET orchestrator logs command + orchsCmd.AddCommand(getLogsOrchestratorCmd) + // SET orchestrator auth certificate reenrollment command TODO: Not implemented + //orchsCmd.AddCommand(setOrchestratorAuthCertReenrollCmd) + // Utility commands + //orchsCmd.AddCommand(downloadOrchestrator) TODO: Not implemented +} diff --git a/cmd/rot.go b/cmd/rot.go index 4e5fb2a..66bd793 100644 --- a/cmd/rot.go +++ b/cmd/rot.go @@ -735,6 +735,7 @@ func init() { rotReconcileCmd.Flags().IntVarP(&maxLeaves, "max-leaf-certs", "n", -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, "Dry run mode") //rotReconcileCmd.MarkFlagsRequiredTogether("add-certs", "stores") //rotReconcileCmd.MarkFlagsRequiredTogether("remove-certs", "stores") rotReconcileCmd.MarkFlagsMutuallyExclusive("add-certs", "actions") diff --git a/cmd/status.go b/cmd/status.go new file mode 100644 index 0000000..2d85d84 --- /dev/null +++ b/cmd/status.go @@ -0,0 +1,46 @@ +/* +Copyright © 2022 NAME HERE + +*/ +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" +) + +// statusCmd represents the status command +var statusCmd = &cobra.Command{ + Use: "status", + Short: "List the status of Keyfactor services.", + Long: `Returns a list of all API endpoints.`, + Run: func(cmd *cobra.Command, args []string) { + //log.SetOutput(ioutil.Discard) + //kfClient, _ := initClient() + //status, err := kfClient.GetStatus() + //if err != nil { + // log.Printf("Error: %s", err) + //} + //output, jErr := json.Marshal(status) + //if jErr != nil { + // log.Printf("Error: %s", jErr) + //} + //fmt.Printf("%s", output) + // + fmt.Println("status called") + }, +} + +func init() { + rootCmd.AddCommand(statusCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // statusCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // statusCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} From 14782f99071710e31da1538b3b8e789f2b87715d Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 13 Oct 2022 13:37:06 -0700 Subject: [PATCH 08/14] chore: Bump API client and cobra versions. --- go.mod | 4 ++-- go.sum | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 2b6d7f3..40e294d 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module kfutil go 1.18 require ( - github.com/Keyfactor/keyfactor-go-client v1.1.0-rc1 - github.com/spf13/cobra v1.5.0 + github.com/Keyfactor/keyfactor-go-client v1.1.0-rc2 + github.com/spf13/cobra v1.6.0 ) require ( diff --git a/go.sum b/go.sum index cae68e3..17ba9a8 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,13 @@ github.com/Keyfactor/keyfactor-go-client v1.1.0-rc1 h1:OBMIRIYeK9R0GmW/KGIuWty4axmODsjLglFu5Z1HnBo= github.com/Keyfactor/keyfactor-go-client v1.1.0-rc1/go.mod h1:u1M1AjcwiO/Tbvc7EsNl9YTy757hO5wmey1/W/7Qkbs= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spbsoluble/go-pkcs12 v0.3.1 h1:3DWrjdP3HOeYW6aTUSO9pqqAgRL8VKZLqvD5PGkLVMo= github.com/spbsoluble/go-pkcs12 v0.3.1/go.mod h1:MX7DY37hx8xHKEMuJ16EMaVT8sT+4KPqK4gTTLFGcH0= -github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= -github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI= +github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= @@ -23,4 +22,4 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 3c53edee1b32550aecb57d7765c6f642a9d50f76 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 13 Oct 2022 13:38:54 -0700 Subject: [PATCH 09/14] chore: Bump API client --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 40e294d..deba2a8 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module kfutil go 1.18 require ( - github.com/Keyfactor/keyfactor-go-client v1.1.0-rc2 + github.com/Keyfactor/keyfactor-go-client v1.1.0-rc1 github.com/spf13/cobra v1.6.0 ) From 5af9fe7be1b5a83012f8069bca2e2619eb1d81d6 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 13 Oct 2022 14:08:42 -0700 Subject: [PATCH 10/14] chore: Bump API client --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index deba2a8..a9e5c64 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module kfutil go 1.18 require ( - github.com/Keyfactor/keyfactor-go-client v1.1.0-rc1 + github.com/Keyfactor/keyfactor-go-client v1.1.0-rc5 github.com/spf13/cobra v1.6.0 ) diff --git a/go.sum b/go.sum index 17ba9a8..ee05e5c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/Keyfactor/keyfactor-go-client v1.1.0-rc1 h1:OBMIRIYeK9R0GmW/KGIuWty4axmODsjLglFu5Z1HnBo= -github.com/Keyfactor/keyfactor-go-client v1.1.0-rc1/go.mod h1:u1M1AjcwiO/Tbvc7EsNl9YTy757hO5wmey1/W/7Qkbs= +github.com/Keyfactor/keyfactor-go-client v1.1.0-rc5 h1:4nQ/T6YGk9wfgHhyLpUIbK/Ae3CIIbznoJSdLSdZAT8= +github.com/Keyfactor/keyfactor-go-client v1.1.0-rc5/go.mod h1:u1M1AjcwiO/Tbvc7EsNl9YTy757hO5wmey1/W/7Qkbs= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= From 49be900a85983eadd8bdc27132f4b9f4737f7901 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Sun, 16 Oct 2022 21:12:59 -0700 Subject: [PATCH 11/14] chore: Bump go to version `1.19` --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index a9e5c64..57e46bc 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module kfutil -go 1.18 +go 1.19 require ( github.com/Keyfactor/keyfactor-go-client v1.1.0-rc5 From fb1de18677cd21d9c7d97e243ddda1b7714fefad Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 17 Oct 2022 05:01:14 -0700 Subject: [PATCH 12/14] feat(rot): Added ability for `stores rot reconcile` to take a modified audit report as an input. feat(store-types): Added ability to create store types based on templated JSON. --- cmd/containers.go | 3 - cmd/rot.go | 298 ++++++++---- cmd/storeTypes.go | 131 ++++- createTypes.sh | 29 ++ go.mod | 1 + go.sum | 2 + store_types.json | 1186 ++++++++++++++++++++++++++++++++++++++++++++- 7 files changed, 1525 insertions(+), 125 deletions(-) create mode 100644 createTypes.sh diff --git a/cmd/containers.go b/cmd/containers.go index 72d8afd..49b53ef 100644 --- a/cmd/containers.go +++ b/cmd/containers.go @@ -1,6 +1,5 @@ /* Copyright © 2022 NAME HERE - */ package cmd @@ -67,7 +66,6 @@ var containersListCmd = &cobra.Command{ func init() { rootCmd.AddCommand(containersCmd) - // LIST containers command containersCmd.AddCommand(containersListCmd) // GET containers command @@ -78,6 +76,5 @@ func init() { containersCmd.AddCommand(containersUpdateCmd) // DELETE containers command containersCmd.AddCommand(containersDeleteCmd) - // Utility functions } diff --git a/cmd/rot.go b/cmd/rot.go index 66bd793..94c14f0 100644 --- a/cmd/rot.go +++ b/cmd/rot.go @@ -38,15 +38,16 @@ type ROTAction struct { StoreType string `json:"store_type,omitempty"` StorePath string `json:"store_path,omitempty"` Thumbprint string `json:"thumbprint,omitempty"` - CertID int `json:"cert_id,omitempty"` - Add bool `json:"add,omitempty"` - Remove bool `json:"remove,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" - tTypeStores templateType = "stores" - tTypeActions templateType = "actions" + tTypeCerts templateType = "certs" + tTypeStores templateType = "stores" + tTypeActions templateType = "actions" + reconcileDefaultFileName = "rot_audit.csv" ) var ( @@ -91,7 +92,7 @@ func generateAuditReport(addCerts map[string]string, removeCerts map[string]stri ) data = append(data, AuditHeader) - csvFile, fErr := os.Create("rot_audit.csv") + csvFile, fErr := os.Create(reconcileDefaultFileName) if fErr != nil { fmt.Printf("%s", fErr) log.Fatalf("[ERROR] Error creating audit file: %s", fErr) @@ -144,8 +145,8 @@ func generateAuditReport(addCerts map[string]string, removeCerts map[string]stri StoreID: store.ID, StoreType: store.Type, StorePath: store.Path, - Add: true, - Remove: false, + AddCert: true, + RemoveCert: false, } } } @@ -177,10 +178,12 @@ func generateAuditReport(addCerts map[string]string, removeCerts map[string]stri } actions[cert] = ROTAction{ Thumbprint: cert, + CertID: certID, StoreID: store.ID, StoreType: store.Type, - Add: false, - Remove: true, + StorePath: store.Path, + AddCert: false, + RemoveCert: true, } } else { // Cert is not deployed to this store do nothing @@ -200,7 +203,7 @@ func generateAuditReport(addCerts map[string]string, removeCerts map[string]stri fmt.Println(ioErr) log.Printf("[ERROR] Error closing audit file: %s", ioErr) } - fmt.Println("Audit report written to rot_audit.csv") + fmt.Printf("Audit report written to %s", reconcileDefaultFileName) return data, actions, nil } @@ -211,7 +214,7 @@ func reconcileRoots(actions map[string]ROTAction, kfClient *api.Client, dryRun b return nil } for thumbprint, action := range actions { - if action.Add { + if action.AddCert { log.Printf("[INFO] Adding cert %s to store %s(%s)", thumbprint, action.StoreID, action.StorePath) if !dryRun { cStore := api.CertificateStore{ @@ -230,16 +233,18 @@ func reconcileRoots(actions map[string]ROTAction, kfClient *api.Client, dryRun b } _, err := kfClient.AddCertificateToStores(&addReq) if err != nil { + fmt.Println("Error adding cert to store: ", err) log.Fatalf("[ERROR] Error adding cert to store: %s", err) } } else { log.Printf("[INFO] DRY RUN: Would have added cert %s from store %s", thumbprint, action.StoreID) } - } else if action.Remove { + } else if action.RemoveCert { if !dryRun { log.Printf("[INFO] Removing cert from store %s", action.StoreID) cStore := api.CertificateStore{ CertificateStoreId: action.StoreID, + Alias: action.Thumbprint, } var stores []api.CertificateStore stores = append(stores, cStore) @@ -248,12 +253,12 @@ func reconcileRoots(actions map[string]ROTAction, kfClient *api.Client, dryRun b } removeReq := api.RemoveCertificateFromStore{ CertificateId: action.CertID, - Alias: fmt.Sprintf("KeyfactorAdd%d", action.CertID), CertificateStores: &stores, InventorySchedule: schedule, } _, err := kfClient.RemoveCertificateFromStores(&removeReq) if err != nil { + fmt.Printf("[ERROR] Error removing cert from store: %s", err) log.Fatalf("[ERROR] Error removing cert from store: %s", err) } } else { @@ -421,7 +426,7 @@ var ( //} addCertsJSON, _ := json.Marshal(certsToAdd) log.Printf("[DEBUG] add certs JSON: %s", string(addCertsJSON)) - log.Println("[DEBUG] Add ROT called") + log.Println("[DEBUG] AddCert ROT called") } else { log.Printf("[DEBUG] No addCerts file specified") log.Printf("[DEBUG] No addCerts = %s", certsToAdd) @@ -488,6 +493,8 @@ var ( kfClient, _ := initClient() 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") @@ -498,89 +505,177 @@ var ( log.Printf("[DEBUG] removeRootsFile: %s", removeRootsFile) log.Printf("[DEBUG] dryRun: %t", dryRun) - // 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 _, entry := range storeEntries { - if entry[0] == "StoreID" { - continue // Skip header - } - apiResp, err := kfClient.GetCertificateStoreByID(entry[0]) + // 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 { - log.Printf("[ERROR] Error getting cert store: %s", err) - lookupFailures = append(lookupFailures, strings.Join(entry, ",")) - continue + fmt.Printf("Error opening file: %s", err) + log.Fatalf("Error opening CSV file: %s", err) } - - //log.Printf("[DEBUG] Store: %s", apiResp) - inventory, invErr := kfClient.GetCertStoreInventory(entry[0]) - if invErr != nil { - log.Fatalf("[ERROR] Error getting cert store inventory: %s", invErr) + validHeader := false + inFile, cErr := csv.NewReader(csvFile).ReadAll() + if cErr != nil { + log.Fatalf("Error reading CSV file: %s", cErr) } - - 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) + actions := make(map[string]ROTAction) + fieldMap := make(map[int]string) + for i, field := range AuditHeader { + fieldMap[i] = field } + for _, 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{}) - 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 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 + } + + } + addCert, _ := strconv.ParseBool(action["AddCert"].(string)) + removeCert, _ := strconv.ParseBool(action["RemoveCert"].(string)) + + a := ROTAction{ + StoreID: action["StoreID"].(string), + StoreType: action["StoreType"].(string), + StorePath: action["Path"].(string), + Thumbprint: action["Thumbprint"].(string), + CertID: action["CertID"].(int), + AddCert: addCert, + RemoveCert: removeCert, + } + + actions[a.Thumbprint] = a + + //actions[cert] = ROTAction{ + // Thumbprint: cert, + // CertID: certID, + // StoreID: store.ID, + // StoreType: store.Type, + // StorePath: store.Path, + // AddCert: true, + // RemoveCert: false, + //} } - for _, cert := range *inventory { - thumb := cert.Thumbprints - for t, v := range thumb { - stores[entry[0]].Thumbprints[t] = v + if len(actions) == 0 { + fmt.Println("No reconciliation actions to take, root stores are up-to-date. Exiting.") + return + } + rErr := reconcileRoots(actions, kfClient, dryRun) + if rErr != nil { + fmt.Printf("Error reconciling roots: %s", rErr) + log.Fatalf("[ERROR] Error reconciling roots: %s", rErr) + } + defer csvFile.Close() + fmt.Println("Reconciliation completed. Check orchestrator jobs for details.") + } 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 } - for t, v := range cert.Serials { - stores[entry[0]].Serials[t] = v + apiResp, err := kfClient.GetCertificateStoreByID(entry[0]) + if err != nil { + log.Printf("[ERROR] Error getting cert store: %s", err) + lookupFailures = append(lookupFailures, entry[0]) + continue } - for t, v := range cert.Ids { - stores[entry[0]].Ids[t] = v + inventory, invErr := kfClient.GetCertStoreInventory(entry[0]) + if invErr != nil { + log.Fatalf("[ERROR] 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) + } - // 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") - } + 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 + } + } - // 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, kfClient) - if err != nil { - log.Fatalf("[ERROR] 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, dryRun) - if rErr != nil { - fmt.Printf("Error reconciling roots: %s", rErr) - log.Fatalf("[ERROR] Error reconciling roots: %s", rErr) + } + if len(lookupFailures) > 0 { + fmt.Printf("Error the following stores were not found: %s", strings.Join(lookupFailures, ",")) + log.Fatalf("[ERROR] 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, kfClient) + if err != nil { + log.Fatalf("[ERROR] 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, dryRun) + if rErr != nil { + fmt.Printf("Error reconciling roots: %s", rErr) + log.Fatalf("[ERROR] Error reconciling roots: %s", rErr) + } + if lookupFailures != nil { + fmt.Printf("The following stores could not be found: %s", strings.Join(lookupFailures, ",")) + } } + }, RunE: nil, PostRun: nil, @@ -697,7 +792,7 @@ func init() { tType = tTypeCerts outPath string outputFormat string - actions string + inputFile string ) storesCmd.AddCommand(rotCmd) @@ -711,9 +806,9 @@ func init() { "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", "x", -1, + 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", "n", -1, + 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", "", @@ -726,21 +821,21 @@ func init() { "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().StringVarP(&actions, "actions", "z", "", - "CSV file containing reconciliation actions to perform. If this is specified, the other flags are ignored.") 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", "x", -1, + 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", "n", -1, + 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, "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.MarkFlagsRequiredTogether("add-certs", "stores") //rotReconcileCmd.MarkFlagsRequiredTogether("remove-certs", "stores") - rotReconcileCmd.MarkFlagsMutuallyExclusive("add-certs", "actions") - rotReconcileCmd.MarkFlagsMutuallyExclusive("remove-certs", "actions") - rotReconcileCmd.MarkFlagsMutuallyExclusive("stores", "actions") + 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) @@ -752,13 +847,4 @@ func init() { `The type of template to generate. Only "certs|stores|actions" are supported at this time.`) rotGenStoreTemplateCmd.RegisterFlagCompletionFunc("type", templateTypeCompletion) rotGenStoreTemplateCmd.MarkFlagRequired("type") - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // rotAuditCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // rotAuditCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } diff --git a/cmd/storeTypes.go b/cmd/storeTypes.go index caf18b8..b4ba286 100644 --- a/cmd/storeTypes.go +++ b/cmd/storeTypes.go @@ -1,6 +1,5 @@ /* Copyright © 2022 NAME HERE - */ package cmd @@ -10,10 +9,38 @@ import ( "github.com/Keyfactor/keyfactor-go-client/api" "io/ioutil" "log" + "sort" + "strings" "github.com/spf13/cobra" ) +// Flag enums + +// End enums + +// Helpers +func buildStoreTypePropertiesInterface(properties []interface{}) ([]api.StoreTypePropertyDefinition, error) { + var output []api.StoreTypePropertyDefinition + + for _, prop := range properties { + log.Printf("Prop: %v", prop) + p := prop.(map[string]interface{}) + output = append(output, api.StoreTypePropertyDefinition{ + Name: p["Name"].(string), + DisplayName: p["DisplayName"].(string), + Type: p["Type"].(string), + DependsOn: p["DependsOn"].(string), + DefaultValue: p["DefaultValue"], + Required: p["Required"].(bool), + }) + } + + return output, nil +} + +// End helpers + // storeTypesCmd represents the storeTypes command var storeTypesCmd = &cobra.Command{ Use: "store-types", @@ -92,6 +119,81 @@ var storesTypeCreateCmd = &cobra.Command{ Long: `Create a new certificate store type in Keyfactor.`, Run: func(cmd *cobra.Command, args []string) { fmt.Println("create called") + //Check if store type is valid + validStoreTypes := getValidStoreTypes() + storeType, _ := cmd.Flags().GetString("name") + storeTypeIsValid := false + for _, v := range validStoreTypes { + if strings.EqualFold(v, strings.ToUpper(storeType)) { + log.Printf("[DEBUG] Valid store type: %s", storeType) + storeTypeIsValid = true + break + } + } + if !storeTypeIsValid { + fmt.Printf("Error: Invalid store type: %s\nValid types are: %s", storeType, validStoreTypes) + log.Fatalf("Error: Invalid store type: %s", storeType) + } else { + kfClient, _ := initClient() + storeTypeConfig, _ := readStoreTypesConfig() + log.Printf("[DEBUG] Store type config: %v", storeTypeConfig[storeType]) + sConfig := storeTypeConfig[storeType].(map[string]interface{}) + props, pErr := buildStoreTypePropertiesInterface(sConfig["Properties"].([]interface{})) + if pErr != nil { + fmt.Printf("Error: %s", pErr) + log.Printf("Error: %s", pErr) + } + createReq := api.CertificateStoreType{ + Name: storeTypeConfig[storeType].(map[string]interface{})["Name"].(string), + ShortName: storeTypeConfig[storeType].(map[string]interface{})["ShortName"].(string), + Capability: storeTypeConfig[storeType].(map[string]interface{})["Capability"].(string), + SupportedOperations: struct { + Add bool `json:"Add"` + Create bool `json:"Create"` + Discovery bool `json:"Discovery"` + Enrollment bool `json:"Enrollment"` + Remove bool `json:"Remove"` + }{ + Add: storeTypeConfig[storeType].(map[string]interface{})["SupportedOperations"].(map[string]interface{})["Add"].(bool), + Create: storeTypeConfig[storeType].(map[string]interface{})["SupportedOperations"].(map[string]interface{})["Create"].(bool), + Discovery: storeTypeConfig[storeType].(map[string]interface{})["SupportedOperations"].(map[string]interface{})["Discovery"].(bool), + Enrollment: storeTypeConfig[storeType].(map[string]interface{})["SupportedOperations"].(map[string]interface{})["Enrollment"].(bool), + Remove: storeTypeConfig[storeType].(map[string]interface{})["SupportedOperations"].(map[string]interface{})["Remove"].(bool), + }, + Properties: props, + EntryParameters: []api.EntryParameter{}, + PasswordOptions: struct { + EntrySupported bool `json:"EntrySupported"` + StoreRequired bool `json:"StoreRequired"` + Style string `json:"Style"` + }{ + EntrySupported: storeTypeConfig[storeType].(map[string]interface{})["PasswordOptions"].(map[string]interface{})["EntrySupported"].(bool), + StoreRequired: storeTypeConfig[storeType].(map[string]interface{})["PasswordOptions"].(map[string]interface{})["StoreRequired"].(bool), + Style: storeTypeConfig[storeType].(map[string]interface{})["PasswordOptions"].(map[string]interface{})["Style"].(string), + }, + //StorePathType: "", + //StorePathValue: "", + PrivateKeyAllowed: storeTypeConfig[storeType].(map[string]interface{})["PrivateKeyAllowed"].(string), + JobProperties: nil, + ServerRequired: storeTypeConfig[storeType].(map[string]interface{})["ServerRequired"].(bool), + PowerShell: storeTypeConfig[storeType].(map[string]interface{})["PowerShell"].(bool), + BlueprintAllowed: storeTypeConfig[storeType].(map[string]interface{})["BlueprintAllowed"].(bool), + CustomAliasAllowed: storeTypeConfig[storeType].(map[string]interface{})["CustomAliasAllowed"].(string), + //ServerRegistration: 0, + //InventoryEndpoint: "", + //InventoryJobType: "", + //ManagementJobType: "", + //DiscoveryJobType: "", + //EnrollmentJobType: "", + } + log.Printf("[DEBUG] Create request: %v", createReq) + createResp, err := kfClient.CreateStoreType(&createReq) + if err != nil { + fmt.Printf("Error creating store type: %s", err) + log.Printf("[ERROR] creating store type : %s", err) + } + log.Printf("[DEBUG] Create response: %v", createResp) + } }, } @@ -172,23 +274,37 @@ var generateStoreTypeTemplate = &cobra.Command{ }, } -func readStoreTypesConfig() (map[string]api.CertificateStoreType, error) { - content, err := ioutil.ReadFile("./store_types.json") +func readStoreTypesConfig() (map[string]interface{}, error) { + content, err := ioutil.ReadFile("./store_types.json") //todo: make this read from github if err != nil { log.Fatal("Error when opening file: ", err) } // Now let's unmarshall the data into `payload` - var payload map[string]api.CertificateStoreType - err = json.Unmarshal(content, &payload) + //var payload map[string]api.CertificateStoreType + var datas map[string]interface{} + err = json.Unmarshal(content, &datas) if err != nil { log.Printf("Error during Unmarshal(): %s", err) return nil, err } - return payload, nil + return datas, nil +} + +func getValidStoreTypes() []string { + validStoreTypes, _ := readStoreTypesConfig() + validStoreTypesList := make([]string, 0, len(validStoreTypes)) + for k := range validStoreTypes { + validStoreTypesList = append(validStoreTypesList, k) + } + sort.Strings(validStoreTypesList) + return validStoreTypesList + } func init() { + + validTypesString := strings.Join(getValidStoreTypes(), ", ") rootCmd.AddCommand(storeTypesCmd) // GET store type templates @@ -208,9 +324,12 @@ func init() { // CREATE command storeTypesCmd.AddCommand(storesTypeCreateCmd) + storesTypeCreateCmd.Flags().StringVarP(&storeTypeName, "name", "n", "", "Name of the certificate store type to get. Valid choices are: "+validTypesString) + storesTypeCreateCmd.MarkFlagRequired("name") // UPDATE command storeTypesCmd.AddCommand(storesTypeUpdateCmd) + storesTypeUpdateCmd.Flags().StringVarP(&storeTypeName, "name", "n", "", "Name of the certificate store type to get.") // DELETE command storeTypesCmd.AddCommand(storesTypeDeleteCmd) diff --git a/createTypes.sh b/createTypes.sh new file mode 100644 index 0000000..f78ad79 --- /dev/null +++ b/createTypes.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +kfutil store-types create --name A10 +kfutil store-types create --name AZUREKEYVAULT +kfutil store-types create --name FORTANIX +kfutil store-types create --name GCPAPIGEE +kfutil store-types create --name HASHIVAULT +kfutil store-types create --name IISWBINDINGS +kfutil store-types create --name JKSSSH +kfutil store-types create --name PALOALTOFW +kfutil store-types create --name RFJKS +kfutil store-types create --name RFPEM +kfutil store-types create --name RFPKCS12 +kfutil store-types create --name SAMPLETYPE +kfutil store-types create --name WINCERMGMT + +# Precreated store types +kfutil store-types create --name AWS +kfutil store-types create --name F5CABUNDLES +kfutil store-types create --name F5PROFILES +kfutil store-types create --name F5PROFILESREST +kfutil store-types create --name F5WEB +kfutil store-types create --name F5WEBREST +kfutil store-types create --name FTP +kfutil store-types create --name IISPERSONAL +kfutil store-types create --name IISREVOKED +kfutil store-types create --name IISROOTS +kfutil store-types create --name JKS +kfutil store-types create --name NETSCALER +kfutil store-types create --name PEM diff --git a/go.mod b/go.mod index 57e46bc..efa7751 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( github.com/Keyfactor/keyfactor-go-client v1.1.0-rc5 + github.com/mitchellh/mapstructure v1.5.0 github.com/spf13/cobra v1.6.0 ) diff --git a/go.sum b/go.sum index ee05e5c..211076e 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/Keyfactor/keyfactor-go-client v1.1.0-rc5/go.mod h1:u1M1AjcwiO/Tbvc7Es github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spbsoluble/go-pkcs12 v0.3.1 h1:3DWrjdP3HOeYW6aTUSO9pqqAgRL8VKZLqvD5PGkLVMo= github.com/spbsoluble/go-pkcs12 v0.3.1/go.mod h1:MX7DY37hx8xHKEMuJ16EMaVT8sT+4KPqK4gTTLFGcH0= diff --git a/store_types.json b/store_types.json index 78b3e66..d149fa2 100644 --- a/store_types.json +++ b/store_types.json @@ -1,11 +1,1146 @@ { + "A10": { + "Name": "A10 vThunder", + "ShortName": "vThunderU", + "Capability": "vThunderU", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": false, + "Enrollment": false, + "Remove": true + }, + "Properties": [ + { + "StoreTypeId;omitempty": 0, + "Name": "protocol", + "DisplayName": "Protocol", + "Type": "String", + "DependsOn": "", + "DefaultValue": null, + "Required": true + }, + { + "StoreTypeId;omitempty": 0, + "Name": "allowInvalidCert", + "DisplayName": "Allow Invalid Cert", + "Type": "Bool", + "DependsOn": "", + "DefaultValue": "false", + "Required": true + } + ], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "", + "PrivateKeyAllowed": "Optional", + "JobProperties": [], + "ServerRequired": true, + "PowerShell": false, + "BlueprintAllowed": true, + "CustomAliasAllowed": "Required" + }, + "AZUREKEYVAULT": { + "Name": "Azure KeyVault", + "ShortName": "AKV", + "Capability": "AKV", + "SupportedOperations": { + "Add": true, + "Create": true, + "Discovery": true, + "Enrollment": false, + "Remove": true + }, + "Properties": [ + { + "StoreTypeId;omitempty": 0, + "Name": "VaultName", + "DisplayName": "Vault Name", + "Type": "String", + "DependsOn": "", + "DefaultValue": null, + "Required": true + }, + { + "StoreTypeId;omitempty": 0, + "Name": "ResourceGroupName", + "DisplayName": "Resource Group Name", + "Type": "String", + "DependsOn": "", + "DefaultValue": null, + "Required": true + } + ], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "", + "PrivateKeyAllowed": "Required", + "JobProperties": [], + "ServerRequired": true, + "PowerShell": false, + "BlueprintAllowed": false, + "CustomAliasAllowed": "Required" + }, + "AWS": { + "Name": "Amazon Web Services", + "ShortName": "AWS", + "Capability": "AWS", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": false, + "Enrollment": false, + "Remove": true + }, + "Properties": [ + { + "StoreTypeId;omitempty": 0, + "Name": "AccessKey", + "DisplayName": "Access Key", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": null, + "Required": true + }, + { + "StoreTypeId;omitempty": 0, + "Name": "SecretKey", + "DisplayName": "Secret Key", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": null, + "Required": true + } + ], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "[\"AP Northeast 1\", \"AP Northeast 2\", \"AP South 1\", \"AP Southeast 1\", \"AP Southeast 2\", \"CA Central 1\", \"EU Central 1\", \"EU West 1\", \"EU West 2\", \"EU West 3\", \"SA East 1\", \"US East 1\", \"US East 2\", \"US West 1\", \"US West 2\"]", + "PrivateKeyAllowed": "Required", + "JobProperties": [], + "ServerRequired": false, + "PowerShell": false, + "BlueprintAllowed": false, + "CustomAliasAllowed": "Forbidden" + }, + "F5PROFILES": { + "Name": "F5 SSL Profiles", + "ShortName": "F5", + "Capability": "F5", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": false, + "Enrollment": false, + "Remove": true + }, + "Properties": [], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "", + "PrivateKeyAllowed": "Optional", + "JobProperties": [], + "ServerRequired": true, + "PowerShell": false, + "BlueprintAllowed": false, + "CustomAliasAllowed": "Required" + }, + "F5WEB": { + "Name": "F5 Web Server", + "ShortName": "F5", + "Capability": "F5", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": false, + "Enrollment": false, + "Remove": false + }, + "Properties": [], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "F5 Web Server", + "PrivateKeyAllowed": "Required", + "JobProperties": [], + "ServerRequired": true, + "PowerShell": false, + "BlueprintAllowed": false, + "CustomAliasAllowed": "Forbidden" + }, + "F5CABUNDLES": { + "Name": "F5 CA Bundles REST", + "ShortName": "F5-CA-REST", + "Capability": "F5-CA-REST", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": true, + "Enrollment": false, + "Remove": true + }, + "Properties": [ + { + "StoreTypeId;omitempty": 0, + "Name": "PrimaryNode", + "DisplayName": "Primary Node", + "Type": "String", + "DependsOn": "", + "DefaultValue": "", + "Required": true + }, + { + "StoreTypeId;omitempty": 0, + "Name": "PrimaryNodeCheckRetryMax", + "DisplayName": "Primary Node Check Retry Maximum", + "Type": "String", + "DependsOn": "", + "DefaultValue": "3", + "Required": true + }, + { + "StoreTypeId;omitempty": 0, + "Name": "PrimaryNodeCheckRetryWaitSecs", + "DisplayName": "Primary Node Check Retry Wait Seconds", + "Type": "String", + "DependsOn": "", + "DefaultValue": "120", + "Required": true + }, + { + "StoreTypeId;omitempty": 0, + "Name": "F5Version", + "DisplayName": "Version of F5", + "Type": "MultipleChoice", + "DependsOn": "", + "DefaultValue": "v12,v13,v14,v15", + "Required": true + } + ], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "", + "PrivateKeyAllowed": "Forbidden", + "JobProperties": [], + "ServerRequired": true, + "PowerShell": false, + "BlueprintAllowed": true, + "CustomAliasAllowed": "Required" + }, + "F5PROFILESREST": { + "Name": "F5 SSL Profiles REST", + "ShortName": "F5-SL-REST", + "Capability": "F5-SL-REST", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": true, + "Enrollment": false, + "Remove": true + }, + "Properties": [ + { + "StoreTypeId;omitempty": 0, + "Name": "PrimaryNode", + "DisplayName": "Primary Node", + "Type": "String", + "DependsOn": "", + "DefaultValue": "", + "Required": true + }, + { + "StoreTypeId;omitempty": 0, + "Name": "PrimaryNodeCheckRetryWaitSecs", + "DisplayName": "Primary Node Check Retry Wait Seconds", + "Type": "String", + "DependsOn": "", + "DefaultValue": "120", + "Required": true + }, + { + "StoreTypeId;omitempty": 0, + "Name": "PrimaryNodeCheckRetryMax", + "DisplayName": "Primary Node Check Retry Maximum", + "Type": "String", + "DependsOn": "", + "DefaultValue": "3", + "Required": true + }, + { + "StoreTypeId;omitempty": 0, + "Name": "F5Version", + "DisplayName": "Version of F5", + "Type": "MultipleChoice", + "DependsOn": "", + "DefaultValue": "v12,v13,v14,v15", + "Required": true + } + ], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "", + "PrivateKeyAllowed": "Optional", + "JobProperties": [], + "ServerRequired": true, + "PowerShell": false, + "BlueprintAllowed": true, + "CustomAliasAllowed": "Required" + }, + "F5WEBREST": { + "Name": "F5 Web Server REST", + "ShortName": "F5-WS-REST", + "Capability": "F5-WS-REST", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": false, + "Enrollment": false, + "Remove": false + }, + "Properties": [ + { + "StoreTypeId;omitempty": 0, + "Name": "PrimaryNode", + "DisplayName": "Primary Node", + "Type": "String", + "DependsOn": "", + "DefaultValue": "", + "Required": true + }, + { + "StoreTypeId;omitempty": 0, + "Name": "PrimaryNodeCheckRetryWaitSecs", + "DisplayName": "Primary Node Check Retry Wait Seconds", + "Type": "String", + "DependsOn": "", + "DefaultValue": "120", + "Required": true + }, + { + "StoreTypeId;omitempty": 0, + "Name": "PrimaryNodeCheckRetryMax", + "DisplayName": "Primary Node Check Retry Maximum", + "Type": "String", + "DependsOn": "", + "DefaultValue": "3", + "Required": true + }, + { + "StoreTypeId;omitempty": 0, + "Name": "F5Version", + "DisplayName": "Version of F5", + "Type": "MultipleChoice", + "DependsOn": "", + "DefaultValue": "v12,v13,v14,v15", + "Required": true + } + ], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "WebServer", + "PrivateKeyAllowed": "Required", + "JobProperties": [], + "ServerRequired": true, + "PowerShell": false, + "BlueprintAllowed": false, + "CustomAliasAllowed": "Forbidden" + }, + "FORTANIX": { + "Name": "Fortanix", + "ShortName": "Fortanix", + "Capability": "Fortanix", + "SupportedOperations": { + "Add": false, + "Create": false, + "Discovery": false, + "Enrollment": false, + "Remove": false + }, + "Properties": [], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": true, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "", + "PrivateKeyAllowed": "Optional", + "JobProperties": [], + "ServerRequired": false, + "PowerShell": false, + "BlueprintAllowed": false, + "CustomAliasAllowed": "Required", + "ServerRegistration": 0 + }, + "FTP": { + "Name": "File Transfer Protocol", + "ShortName": "FTP", + "Capability": "FTP", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": false, + "Enrollment": false, + "Remove": true + }, + "Properties": [], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "", + "PrivateKeyAllowed": "Optional", + "JobProperties": [], + "ServerRequired": true, + "PowerShell": false, + "BlueprintAllowed": false, + "CustomAliasAllowed": "Forbidden" + }, + "GCPAPIGEE" : { + "Name": "Google Cloud Provider Apigee", + "ShortName": "GcpApigee", + "Capability": "GcpApigee", + "SupportedOperations": { + "Add": true, + "Create": true, + "Discovery": false, + "Enrollment": false, + "Remove": true + }, + "Properties": [ + { + "Name": "isTrustStore", + "DisplayName": "Is Trust Store?", + "Type": "Bool", + "DependsOn": "", + "DefaultValue": "false", + "Required": true + }, + { + "Name": "jsonKey", + "DisplayName": "Google Json Key File", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": true + } + ], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "", + "PrivateKeyAllowed": "Optional", + "JobProperties": [], + "ServerRequired": false, + "PowerShell": false, + "BlueprintAllowed": true, + "CustomAliasAllowed": "Required", + "ServerRegistration": 0 + }, + "HASHIVAULT": { + "Name": "Hashicorp Vault", + "ShortName": "HCV", + "Capability": "HCV", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": true, + "Enrollment": false, + "Remove": true + }, + "Properties": [ + { + "StoreTypeId;omitempty": 0, + "Name": "MountPoint", + "DisplayName": "Mount Point", + "Type": "String", + "DependsOn": "", + "DefaultValue": null, + "Required": false + }, + { + "StoreTypeId;omitempty": 0, + "Name": "VaultServerUrl", + "DisplayName": "VaultServerURL", + "Type": "String", + "DependsOn": "", + "DefaultValue": null, + "Required": true + }, + { + "StoreTypeId;omitempty": 0, + "Name": "VaultToken", + "DisplayName": "Vault Token", + "Type": "String", + "DependsOn": "", + "DefaultValue": null, + "Required": true + } + ], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "", + "PrivateKeyAllowed": "Forbidden", + "JobProperties": [], + "ServerRequired": false, + "PowerShell": false, + "BlueprintAllowed": false, + "CustomAliasAllowed": "Forbidden" + }, + "IISROOTS": { + "Name": "IIS Roots", + "ShortName": "IIS", + "Capability": "IIS", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": false, + "Enrollment": false, + "Remove": true + }, + "Properties": [ + { + "StoreTypeId;omitempty": 0, + "Name": "UseSSL", + "DisplayName": "UseSSL", + "Type": "Bool", + "DependsOn": "", + "DefaultValue": "false", + "Required": true + } + ], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "IIS Trusted Roots", + "PrivateKeyAllowed": "Forbidden", + "JobProperties": [], + "ServerRequired": false, + "PowerShell": false, + "BlueprintAllowed": false, + "CustomAliasAllowed": "Forbidden", + "ServerRegistration": 0 + }, + "IISPERSONAL": { + "Name": "IIS Personal", + "ShortName": "IIS", + "Capability": "IIS", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": false, + "Enrollment": false, + "Remove": true + }, + "Properties": [ + { + "StoreTypeId;omitempty": 0, + "Name": "UseSSL", + "DisplayName": "UseSSL", + "Type": "Bool", + "DependsOn": "", + "DefaultValue": "false", + "Required": true + } + ], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "IIS Personal", + "PrivateKeyAllowed": "Required", + "JobProperties": [], + "ServerRequired": false, + "PowerShell": false, + "BlueprintAllowed": false, + "CustomAliasAllowed": "Optional", + "ServerRegistration": 0 + }, + "IISREVOKED": { + "Name": "IIS Revoked", + "ShortName": "IIS", + "Capability": "IIS", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": false, + "Enrollment": false, + "Remove": true + }, + "Properties": [ + { + "StoreTypeId;omitempty": 0, + "Name": "UseSSL", + "DisplayName": "UseSSL", + "Type": "Bool", + "DependsOn": "", + "DefaultValue": "false", + "Required": true + } + ], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "IIS Revoked", + "PrivateKeyAllowed": "Optional", + "JobProperties": [], + "ServerRequired": false, + "PowerShell": false, + "BlueprintAllowed": false, + "CustomAliasAllowed": "Forbidden" + }, + "IISWBINDINGS": { + "Name": "IISWithBindings", + "ShortName": "IISWBin", + "Capability": "IISBindings", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": false, + "Enrollment": false, + "Remove": true + }, + "Properties": [ + { + "StoreTypeId;omitempty": 0, + "Name": "spnwithport", + "DisplayName": "SPN With Port?", + "Type": "Bool", + "DependsOn": "", + "DefaultValue": "false", + "Required": false + }, + { + "StoreTypeId;omitempty": 0, + "Name": "WinRm Protocol", + "DisplayName": "WinRm Protocol", + "Type": "MultipleChoice", + "DependsOn": "", + "DefaultValue": "http", + "Required": true + }, + { + "StoreTypeId;omitempty": 0, + "Name": "WinRm Port", + "DisplayName": "WinRm Port", + "Type": "String", + "DependsOn": "", + "DefaultValue": "5985", + "Required": true + } + ], + "EntryParameters": [ + { + "StoreTypeId": 103, + "Name": "Site Name", + "DisplayName": "Site Name", + "Type": "String", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": true, + "OnRemove": true, + "OnReenrollment": false + }, + "DependsOn": "", + "DefaultValue": "Default Web Site", + "Options": "" + }, + { + "StoreTypeId": 103, + "Name": "Port", + "DisplayName": "Port", + "Type": "String", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": true, + "OnRemove": true, + "OnReenrollment": false + }, + "DependsOn": "", + "DefaultValue": "443", + "Options": "" + }, + { + "StoreTypeId": 103, + "Name": "IP Address", + "DisplayName": "IP Address", + "Type": "String", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": true, + "OnRemove": true, + "OnReenrollment": false + }, + "DependsOn": "", + "DefaultValue": "*", + "Options": "" + }, + { + "StoreTypeId": 103, + "Name": "Host Name", + "DisplayName": "Host Name", + "Type": "String", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": false, + "OnRemove": false, + "OnReenrollment": false + }, + "DependsOn": "", + "DefaultValue": "", + "Options": "" + }, + { + "StoreTypeId": 103, + "Name": "Sni Flag", + "DisplayName": "Sni Flag", + "Type": "String", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": false, + "OnRemove": false, + "OnReenrollment": false + }, + "DependsOn": "", + "DefaultValue": "0 - No SNI", + "Options": "" + }, + { + "StoreTypeId": 103, + "Name": "Protocol", + "DisplayName": "Protocol", + "Type": "String", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": true, + "OnRemove": true, + "OnReenrollment": false + }, + "DependsOn": "", + "DefaultValue": "https", + "Options": "" + } + ], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "[\"My\",\"WebHosting\"]", + "PrivateKeyAllowed": "Required", + "JobProperties": [ + "Site Name", + "Port", + "IP Address", + "Host Name", + "Sni Flag", + "Protocol" + ], + "ServerRequired": true, + "PowerShell": false, + "BlueprintAllowed": false, + "CustomAliasAllowed": "Forbidden" + }, + "JKS": { + "Name": "Java Keystore", + "ShortName": "JKS", + "Capability": "JKS", + "SupportedOperations": { + "Add": true, + "Create": true, + "Discovery": true, + "Enrollment": true, + "Remove": true + }, + "Properties": [ + { + "StoreTypeId;omitempty": 0, + "Name": "ProviderType", + "DisplayName": "Type", + "Type": "MultipleChoice", + "DependsOn": "", + "DefaultValue": "JKS,PKCS12,Windows-MY", + "Required": true + } + ], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": true, + "StoreRequired": true, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "", + "PrivateKeyAllowed": "Optional", + "JobProperties": [], + "ServerRequired": false, + "PowerShell": false, + "BlueprintAllowed": true, + "CustomAliasAllowed": "Required" + }, + "JKSSSH": { + "Name": "JKS-SSH", + "ShortName": "JKS-SSH", + "Capability": "JKS-SSH", + "SupportedOperations": { + "Add": true, + "Create": true, + "Discovery": true, + "Enrollment": false, + "Remove": true + }, + "Properties": [ + { + "StoreTypeId;omitempty": 0, + "Name": "linuxFilePermissionsOnStoreCreation", + "DisplayName": "Linux Create File Permissions", + "Type": "String", + "DependsOn": "", + "DefaultValue": null, + "Required": false + } + ], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": true, + "StoreRequired": true, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "", + "PrivateKeyAllowed": "Optional", + "JobProperties": [], + "ServerRequired": true, + "PowerShell": false, + "BlueprintAllowed": false, + "CustomAliasAllowed": "Required" + }, + "NETSCALER": { + "Name": "NetScaler", + "ShortName": "NS", + "Capability": "NS", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": false, + "Enrollment": false, + "Remove": true + }, + "Properties": [], + "EntryParameters": [ + { + "StoreTypeId": 5, + "Name": "NetscalerVserver", + "DisplayName": "NetscalerVserver", + "Type": "String", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": false, + "OnRemove": false, + "OnReenrollment": false + }, + "DependsOn": "", + "DefaultValue": "", + "Options": "" + } + ], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "", + "PrivateKeyAllowed": "Optional", + "JobProperties": [ + "NetscalerVserver" + ], + "ServerRequired": true, + "PowerShell": false, + "BlueprintAllowed": false, + "CustomAliasAllowed": "Required" + }, + "PALOALTOFW": { + "Name": "PaloAlto", + "ShortName": "PaloAlto", + "Capability": "PaloAlto", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": false, + "Enrollment": false, + "Remove": true + }, + "Properties": [], + "EntryParameters": [ + { + "StoreTypeId": 112, + "Name": "Trusted Root", + "DisplayName": "Trusted Root", + "Type": "Bool", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": true, + "OnRemove": false, + "OnReenrollment": false + }, + "DependsOn": "", + "DefaultValue": "false", + "Options": "" + } + ], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "", + "PrivateKeyAllowed": "Optional", + "JobProperties": [ + "Trusted Root" + ], + "ServerRequired": true, + "PowerShell": false, + "BlueprintAllowed": false, + "CustomAliasAllowed": "Required" + }, + "PEM": { + "Name": "PEM File", + "ShortName": "PEM", + "Capability": "PEM", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": true, + "Enrollment": true, + "Remove": true + }, + "Properties": [ + { + "StoreTypeId;omitempty": 0, + "Name": "separatePrivateKey", + "DisplayName": "Separate Private Key", + "Type": "Bool", + "DependsOn": "", + "DefaultValue": "false", + "Required": false + }, + { + "StoreTypeId;omitempty": 0, + "Name": "privateKeyPath", + "DisplayName": "Path To Private Key File", + "Type": "String", + "DependsOn": "separatePrivateKey", + "DefaultValue": null, + "Required": true + } + ], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Custom" + }, + "StorePathType": "", + "StorePathValue": "", + "PrivateKeyAllowed": "Optional", + "JobProperties": [], + "ServerRequired": false, + "PowerShell": false, + "BlueprintAllowed": true, + "CustomAliasAllowed": "Forbidden" + }, + "RFJKS" : { + "Name": "RFJKS", + "ShortName": "RFJKS", + "Capability": "RFJKS", + "ServerRequired": true, + "BlueprintAllowed": false, + "CustomAliasAllowed": "Required", + "PowerShell": false, + "PrivateKeyAllowed": "Optional", + "SupportedOperations": { + "Add": true, + "Create": true, + "Discovery": true, + "Enrollment": false, + "Remove": true + }, + "PasswordOptions": { + "Style": "Default", + "EntrySupported": false, + "StoreRequired": true + }, + "Properties": [ + { + "Name": "LinuxFilePermissionsOnStoreCreation", + "DisplayName": "Linux File Permissions on Store Creation", + "Required": false, + "DependsOn": "", + "Type": "String", + "DefaultValue": "" + } + ], + "EntryParameters": [] + }, + "RFPEM" : { + "Name": "RFPEM", + "ShortName": "RFPEM", + "Capability": "RFPEM", + "ServerRequired": true, + "BlueprintAllowed": false, + "CustomAliasAllowed": "Forbidden", + "PowerShell": false, + "PrivateKeyAllowed": "Optional", + "SupportedOperations": { + "Add": true, + "Create": true, + "Discovery": true, + "Enrollment": false, + "Remove": true + }, + "PasswordOptions": { + "Style": "Default", + "EntrySupported": false, + "StoreRequired": true + }, + "Properties": [ + { + "Name": "LinuxFilePermissionsOnStoreCreation", + "DisplayName": "Linux File Permissions on Store Creation", + "Required": false, + "DependsOn": "", + "Type": "String", + "DefaultValue": "" + }, + { + "Name": "IsTrustStore", + "DisplayName": "Trust Store", + "Required": false, + "DependsOn": "", + "Type": "Bool", + "DefaultValue": false + }, + { + "Name": "IncludesChain", + "DisplayName": "Store Includes Chain", + "Required": false, + "DependsOn": "", + "Type": "Bool", + "DefaultValue": false + }, + { + "Name": "SeparatePrivateKeyFilePath", + "DisplayName": "Separate Private Key File Location", + "Required": false, + "DependsOn": "", + "Type": "String", + "DefaultValue": "" + } + ], + "EntryParameters": [] + }, + "RFPKCS12" : { + "Name": "RFPkcs12", + "ShortName": "RFPkcs12", + "Capability": "RFPkcs12", + "ServerRequired": true, + "BlueprintAllowed": false, + "CustomAliasAllowed": "Required", + "PowerShell": false, + "PrivateKeyAllowed": "Optional", + "SupportedOperations": { + "Add": true, + "Create": true, + "Discovery": true, + "Enrollment": false, + "Remove": true + }, + "PasswordOptions": { + "Style": "Default", + "EntrySupported": false, + "StoreRequired": true + }, + "Properties": [ + { + "Name": "LinuxFilePermissionsOnStoreCreation", + "DisplayName": "Linux File Permissions on Store Creation", + "Required": false, + "DependsOn": "", + "Type": "String", + "DefaultValue": "" + } + ], + "EntryParameters": [] + }, "SAMPLETYPE": { "Name": "Sample Store Type", "ShortName": "SAMPLETYPE", "Capability": "SAMPLETYPE", - "StoreType": 104, - "ImportType": 104, - "LocalStore": false, "SupportedOperations": { "Add": false, "Create": false, @@ -27,12 +1162,43 @@ "ServerRequired": false, "PowerShell": false, "BlueprintAllowed": false, - "CustomAliasAllowed": "Forbidden", - "ServerRegistration": 0, - "InventoryEndpoint": "/AnyInventory/Update", - "InventoryJobType": "0b10c7eb-2cd6-4a3c-9e67-8ee6cc88750d", - "ManagementJobType": "", - "DiscoveryJobType": "", - "EnrollmentJobType": "" + "CustomAliasAllowed": "Forbidden" + }, + "WINCERMGMT": { + "Name": "Windows Certificate Store Manager", + "ShortName": "WinCerMgmt", + "Capability": "WinCerMgmt", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": false, + "Enrollment": false, + "Remove": true + }, + "Properties": [ + { + "StoreTypeId;omitempty": 0, + "Name": "spnwithport", + "DisplayName": "spnwithport", + "Type": "Bool", + "DependsOn": "", + "DefaultValue": "false", + "Required": false + } + ], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "StorePathType": "", + "StorePathValue": "", + "PrivateKeyAllowed": "Optional", + "JobProperties": [], + "ServerRequired": true, + "PowerShell": false, + "BlueprintAllowed": false, + "CustomAliasAllowed": "Forbidden" } } \ No newline at end of file From 8d0ec81aeee2aa5debcad23c6827ca66931214d8 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 17 Oct 2022 10:23:45 -0700 Subject: [PATCH 13/14] fix(rot): Allow a cert to be deployed to multiple stores rather than just one --- cmd/rot.go | 122 ++++++++++++++++++++++++++-------------------------- rot_demo.sh | 50 +++++++++++++++++++++ 2 files changed, 112 insertions(+), 60 deletions(-) create mode 100644 rot_demo.sh diff --git a/cmd/rot.go b/cmd/rot.go index 94c14f0..7029957 100644 --- a/cmd/rot.go +++ b/cmd/rot.go @@ -85,7 +85,7 @@ 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, kfClient *api.Client) ([][]string, map[string]ROTAction, error) { +func generateAuditReport(addCerts map[string]string, removeCerts map[string]string, stores map[string]StoreCSVEntry, kfClient *api.Client) ([][]string, map[string][]ROTAction, error) { log.Println("[DEBUG] generateAuditReport called") var ( data [][]string @@ -103,7 +103,7 @@ func generateAuditReport(addCerts map[string]string, removeCerts map[string]stri fmt.Printf("%s", cErr) log.Fatalf("[ERROR] Error writing audit header: %s", cErr) } - actions := make(map[string]ROTAction) + actions := make(map[string][]ROTAction) for _, cert := range addCerts { certLookupReq := api.GetCertificateContextArgs{ @@ -139,7 +139,7 @@ func generateAuditReport(addCerts map[string]string, removeCerts map[string]stri fmt.Printf("%s", wErr) log.Printf("[ERROR] Error writing audit row: %s", wErr) } - actions[cert] = ROTAction{ + actions[cert] = append(actions[cert], ROTAction{ Thumbprint: cert, CertID: certID, StoreID: store.ID, @@ -147,7 +147,7 @@ func generateAuditReport(addCerts map[string]string, removeCerts map[string]stri StorePath: store.Path, AddCert: true, RemoveCert: false, - } + }) } } } @@ -176,7 +176,7 @@ func generateAuditReport(addCerts map[string]string, removeCerts map[string]stri fmt.Printf("%s", wErr) log.Printf("[ERROR] Error writing row to CSV: %s", wErr) } - actions[cert] = ROTAction{ + actions[cert] = append(actions[cert], ROTAction{ Thumbprint: cert, CertID: certID, StoreID: store.ID, @@ -184,7 +184,7 @@ func generateAuditReport(addCerts map[string]string, removeCerts map[string]stri StorePath: store.Path, AddCert: false, RemoveCert: true, - } + }) } else { // Cert is not deployed to this store do nothing row := []string{cert, certIDStr, store.ID, store.Type, store.Machine, store.Path, "false", "false", "false"} @@ -203,66 +203,68 @@ func generateAuditReport(addCerts map[string]string, removeCerts map[string]stri fmt.Println(ioErr) log.Printf("[ERROR] Error closing audit file: %s", ioErr) } - fmt.Printf("Audit report written to %s", reconcileDefaultFileName) + fmt.Printf("Audit report written to %s\n", reconcileDefaultFileName) return data, actions, nil } -func reconcileRoots(actions map[string]ROTAction, kfClient *api.Client, dryRun bool) error { +func reconcileRoots(actions map[string][]ROTAction, kfClient *api.Client, dryRun bool) error { log.Printf("[DEBUG] Reconciling roots") if len(actions) == 0 { log.Printf("[INFO] No actions to take, roots are up-to-date.") return nil } for thumbprint, action := range actions { - if action.AddCert { - log.Printf("[INFO] Adding cert %s to store %s(%s)", thumbprint, action.StoreID, action.StorePath) - if !dryRun { - cStore := api.CertificateStore{ - CertificateStoreId: action.StoreID, - Overwrite: true, - } - var stores []api.CertificateStore - stores = append(stores, cStore) - schedule := &api.InventorySchedule{ - Immediate: boolToPointer(true), - } - addReq := api.AddCertificateToStore{ - CertificateId: action.CertID, - CertificateStores: &stores, - InventorySchedule: schedule, - } - _, err := kfClient.AddCertificateToStores(&addReq) - if err != nil { - fmt.Println("Error adding cert to store: ", err) - log.Fatalf("[ERROR] Error adding cert to store: %s", err) - } - } else { - log.Printf("[INFO] DRY RUN: Would have added cert %s from store %s", thumbprint, action.StoreID) - } - } else if action.RemoveCert { - if !dryRun { - log.Printf("[INFO] Removing cert from store %s", action.StoreID) - cStore := api.CertificateStore{ - CertificateStoreId: action.StoreID, - Alias: action.Thumbprint, - } - var stores []api.CertificateStore - stores = append(stores, cStore) - schedule := &api.InventorySchedule{ - Immediate: boolToPointer(true), - } - removeReq := api.RemoveCertificateFromStore{ - CertificateId: action.CertID, - CertificateStores: &stores, - InventorySchedule: schedule, - } - _, err := kfClient.RemoveCertificateFromStores(&removeReq) - if err != nil { - fmt.Printf("[ERROR] Error removing cert from store: %s", err) - log.Fatalf("[ERROR] Error removing cert from store: %s", err) + 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{ + CertificateStoreId: a.StoreID, + Overwrite: true, + } + var stores []api.CertificateStore + stores = append(stores, cStore) + schedule := &api.InventorySchedule{ + Immediate: boolToPointer(true), + } + addReq := api.AddCertificateToStore{ + CertificateId: a.CertID, + CertificateStores: &stores, + InventorySchedule: schedule, + } + _, 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) + continue + } + } else { + log.Printf("[INFO] DRY RUN: Would have added cert %s from store %s", thumbprint, a.StoreID) + } + } else if a.RemoveCert { + if !dryRun { + log.Printf("[INFO] Removing cert from store %s", a.StoreID) + cStore := api.CertificateStore{ + CertificateStoreId: a.StoreID, + Alias: a.Thumbprint, + } + var stores []api.CertificateStore + stores = append(stores, cStore) + schedule := &api.InventorySchedule{ + Immediate: boolToPointer(true), + } + removeReq := api.RemoveCertificateFromStore{ + CertificateId: a.CertID, + CertificateStores: &stores, + InventorySchedule: schedule, + } + _, err := kfClient.RemoveCertificateFromStores(&removeReq) + if err != nil { + fmt.Printf("Error removing cert %s (%d) from store %s (%s): %s", a.Thumbprint, a.CertID, a.StoreID, a.StorePath, err) + log.Fatalf("[ERROR] Error removing cert from store: %s", err) + } + } else { + log.Printf("[INFO] DRY RUN: Would have removed cert %s from store %s", thumbprint, a.StoreID) } - } else { - log.Printf("[INFO] DRY RUN: Would have removed cert %s from store %s", thumbprint, action.StoreID) } } } @@ -441,8 +443,7 @@ var ( log.Fatalf("Error reading removeCerts file: %s", rErr) } removeCertsJSON, _ := json.Marshal(certsToRemove) - fmt.Println(string(removeCertsJSON)) - fmt.Println("remove rot called") + log.Printf("[DEBUG] remove certs JSON: %s", string(removeCertsJSON)) } else { log.Printf("[DEBUG] No removeCerts file specified") log.Printf("[DEBUG] No removeCerts = %s", certsToRemove) @@ -520,7 +521,7 @@ var ( if cErr != nil { log.Fatalf("Error reading CSV file: %s", cErr) } - actions := make(map[string]ROTAction) + actions := make(map[string][]ROTAction) fieldMap := make(map[int]string) for i, field := range AuditHeader { fieldMap[i] = field @@ -559,7 +560,7 @@ var ( RemoveCert: removeCert, } - actions[a.Thumbprint] = a + actions[a.Thumbprint] = append(actions[a.Thumbprint], a) //actions[cert] = ROTAction{ // Thumbprint: cert, @@ -674,6 +675,7 @@ var ( if lookupFailures != nil { fmt.Printf("The following stores could not be found: %s", strings.Join(lookupFailures, ",")) } + fmt.Println("Reconciliation completed. Check orchestrator jobs for details.") } }, diff --git a/rot_demo.sh b/rot_demo.sh new file mode 100644 index 0000000..d075fa0 --- /dev/null +++ b/rot_demo.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# ROT Help +kfutil stores rot --help + +# Create a report of actions the utility will take based on inputs from addCerts.csv and removeCerts.csv. Actions added +# to the report will also fit the criteria of a max of 1 private key, a minimum of 3 certs in the inventory and a +# maximum of 1 leaf cert. +kfutil stores reconcile audit \ + --add-certs addCerts.csv \ + --remove-certs removeCerts.csv \ + --max-keys 1 \ + --min-certs 3 \ + --max-leaf-certs 1 \ + --stores stores.csv + +# Add certs listed in addCerts.csv to stores listed if they meet the criteria of a max of 1 private key, a minimum of 3 +# certs in the inventory and a maximum of 1 leaf cert. Then remove the certs listed in removeCerts.csv from the stores. +kfutil stores rot reconcile \ + --add-certs addCerts.csv \ + --remove-certs removeCerts.csv \ + --max-keys 1 \ + --min-certs 3 \ + --max-leaf-certs 1 \ + --stores stores.csv + +# Add certs listed in addCerts.csv to stores listed with no criteria. Then remove the certs listed in removeCerts.csv \ +# from the stores. +kfutil stores rot reconcile \ + --add-certs addCerts.csv \ + --remove-certs removeCerts.csv \ + --stores stores.csv + +# Remove all added certs from the demo +kfutil stores rot reconcile \ + --remove-certs removeCerts.csv \ + --stores stores.csv +kfutil stores rot reconcile \ + --remove-certs removeCerts2.csv \ + --stores stores.csv +kfutil stores rot reconcile \ + --remove-certs addCerts.csv \ + --stores stores.csv +kfutil stores rot reconcile \ + --remove-certs addCerts2.csv \ + --stores stores.csv + +# List stores and convert to CSV into stores.csv format +echo '"StoreId","StoreType","StoreMachine","StorePath"' > meow.csv +kfutil stores list | jq -r '.[] | [.Id, .cert_store_type, .ClientMachine, .Storepath] | @csv' >> meow.csv + From 3ce630e014e1e73ce611e79c8a376b322ee13050 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 19 Oct 2022 08:28:31 -0700 Subject: [PATCH 14/14] chore(dep): Bump api client version to `v1.1.0` --- go.mod | 3 +-- go.sum | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index efa7751..598fd6a 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,7 @@ module kfutil go 1.19 require ( - github.com/Keyfactor/keyfactor-go-client v1.1.0-rc5 - github.com/mitchellh/mapstructure v1.5.0 + github.com/Keyfactor/keyfactor-go-client v1.1.0 github.com/spf13/cobra v1.6.0 ) diff --git a/go.sum b/go.sum index 211076e..0e4960d 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,8 @@ -github.com/Keyfactor/keyfactor-go-client v1.1.0-rc5 h1:4nQ/T6YGk9wfgHhyLpUIbK/Ae3CIIbznoJSdLSdZAT8= -github.com/Keyfactor/keyfactor-go-client v1.1.0-rc5/go.mod h1:u1M1AjcwiO/Tbvc7EsNl9YTy757hO5wmey1/W/7Qkbs= +github.com/Keyfactor/keyfactor-go-client v1.1.0 h1:Q+Aa9yb86N/VmIJDoeYamjWipWoUqo0V56NRHN4tDG4= +github.com/Keyfactor/keyfactor-go-client v1.1.0/go.mod h1:u1M1AjcwiO/Tbvc7EsNl9YTy757hO5wmey1/W/7Qkbs= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spbsoluble/go-pkcs12 v0.3.1 h1:3DWrjdP3HOeYW6aTUSO9pqqAgRL8VKZLqvD5PGkLVMo= github.com/spbsoluble/go-pkcs12 v0.3.1/go.mod h1:MX7DY37hx8xHKEMuJ16EMaVT8sT+4KPqK4gTTLFGcH0=