From 659c63b43bd502aa138f6191261e2dd82188aba3 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Thu, 30 Jan 2025 08:43:42 -0700 Subject: [PATCH 01/10] bugfix: fixed URL param not being set for UpdateFirmwareRemote --- cmd/update.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/update.go b/cmd/update.go index d5f9a50..0800a70 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -64,7 +64,7 @@ var updateCmd = &cobra.Command{ Component: component, TransferProtocol: strings.ToUpper(transferProtocol), CollectParams: magellan.CollectParams{ - URI: host, + URI: arg, Username: username, Password: password, Timeout: timeout, From 90c394e245004fc6b22b848f18c85cb2fa7d18f3 Mon Sep 17 00:00:00 2001 From: David Allen Date: Tue, 4 Feb 2025 13:51:57 -0700 Subject: [PATCH 02/10] cmd: exported commands for external use --- cmd/collect.go | 40 ++++++++++++++++++++-------------------- cmd/crawl.go | 16 ++++++++-------- cmd/list.go | 8 ++++---- cmd/scan.go | 36 ++++++++++++++++++------------------ 4 files changed, 50 insertions(+), 50 deletions(-) diff --git a/cmd/collect.go b/cmd/collect.go index 1f3288a..a095b13 100644 --- a/cmd/collect.go +++ b/cmd/collect.go @@ -17,7 +17,7 @@ import ( // The `collect` command fetches data from a collection of BMC nodes. // This command should be ran after the `scan` to find available hosts // on a subnet. -var collectCmd = &cobra.Command{ +var CollectCmd = &cobra.Command{ Use: "collect", Short: "Collect system information by interrogating BMC node", Long: "Send request(s) to a collection of hosts running Redfish services found stored from the 'scan' in cache.\n" + @@ -75,28 +75,28 @@ var collectCmd = &cobra.Command{ func init() { currentUser, _ = user.Current() - collectCmd.PersistentFlags().StringVar(&host, "host", "", "Set the URI to the SMD root endpoint") - collectCmd.PersistentFlags().StringVar(&username, "username", "", "Set the BMC user") - collectCmd.PersistentFlags().StringVar(&password, "password", "", "Set the BMC password") - collectCmd.PersistentFlags().StringVar(&scheme, "scheme", "https", "Set the scheme used to query") - collectCmd.PersistentFlags().StringVar(&protocol, "protocol", "tcp", "Set the protocol used to query") - collectCmd.PersistentFlags().StringVarP(&outputPath, "output", "o", fmt.Sprintf("/tmp/%smagellan/inventory/", currentUser.Username+"/"), "Set the path to store collection data") - collectCmd.PersistentFlags().BoolVar(&forceUpdate, "force-update", false, "Set flag to force update data sent to SMD") - collectCmd.PersistentFlags().StringVar(&cacertPath, "cacert", "", "Path to CA cert. (defaults to system CAs)") + CollectCmd.PersistentFlags().StringVar(&host, "host", "", "Set the URI to the SMD root endpoint") + CollectCmd.PersistentFlags().StringVar(&username, "username", "", "Set the BMC user") + CollectCmd.PersistentFlags().StringVar(&password, "password", "", "Set the BMC password") + CollectCmd.PersistentFlags().StringVar(&scheme, "scheme", "https", "Set the scheme used to query") + CollectCmd.PersistentFlags().StringVar(&protocol, "protocol", "tcp", "Set the protocol used to query") + CollectCmd.PersistentFlags().StringVarP(&outputPath, "output", "o", fmt.Sprintf("/tmp/%smagellan/inventory/", currentUser.Username+"/"), "Set the path to store collection data") + CollectCmd.PersistentFlags().BoolVar(&forceUpdate, "force-update", false, "Set flag to force update data sent to SMD") + CollectCmd.PersistentFlags().StringVar(&cacertPath, "cacert", "", "Path to CA cert. (defaults to system CAs)") // set flags to only be used together - collectCmd.MarkFlagsRequiredTogether("username", "password") + CollectCmd.MarkFlagsRequiredTogether("username", "password") // bind flags to config properties - checkBindFlagError(viper.BindPFlag("collect.host", collectCmd.Flags().Lookup("host"))) - checkBindFlagError(viper.BindPFlag("collect.username", collectCmd.Flags().Lookup("username"))) - checkBindFlagError(viper.BindPFlag("collect.password", collectCmd.Flags().Lookup("password"))) - checkBindFlagError(viper.BindPFlag("collect.scheme", collectCmd.Flags().Lookup("scheme"))) - checkBindFlagError(viper.BindPFlag("collect.protocol", collectCmd.Flags().Lookup("protocol"))) - checkBindFlagError(viper.BindPFlag("collect.output", collectCmd.Flags().Lookup("output"))) - checkBindFlagError(viper.BindPFlag("collect.force-update", collectCmd.Flags().Lookup("force-update"))) - checkBindFlagError(viper.BindPFlag("collect.cacert", collectCmd.Flags().Lookup("cacert"))) - checkBindFlagError(viper.BindPFlags(collectCmd.Flags())) + checkBindFlagError(viper.BindPFlag("collect.host", CollectCmd.Flags().Lookup("host"))) + checkBindFlagError(viper.BindPFlag("collect.username", CollectCmd.Flags().Lookup("username"))) + checkBindFlagError(viper.BindPFlag("collect.password", CollectCmd.Flags().Lookup("password"))) + checkBindFlagError(viper.BindPFlag("collect.scheme", CollectCmd.Flags().Lookup("scheme"))) + checkBindFlagError(viper.BindPFlag("collect.protocol", CollectCmd.Flags().Lookup("protocol"))) + checkBindFlagError(viper.BindPFlag("collect.output", CollectCmd.Flags().Lookup("output"))) + checkBindFlagError(viper.BindPFlag("collect.force-update", CollectCmd.Flags().Lookup("force-update"))) + checkBindFlagError(viper.BindPFlag("collect.cacert", CollectCmd.Flags().Lookup("cacert"))) + checkBindFlagError(viper.BindPFlags(CollectCmd.Flags())) - rootCmd.AddCommand(collectCmd) + rootCmd.AddCommand(CollectCmd) } diff --git a/cmd/crawl.go b/cmd/crawl.go index 2611ed3..ae61135 100644 --- a/cmd/crawl.go +++ b/cmd/crawl.go @@ -14,7 +14,7 @@ import ( // The `crawl` command walks a collection of Redfish endpoints to collect // specfic inventory detail. This command only expects host names and does // not require a scan to be performed beforehand. -var crawlCmd = &cobra.Command{ +var CrawlCmd = &cobra.Command{ Use: "crawl [uri]", Short: "Crawl a single BMC for inventory information", Long: "Crawl a single BMC for inventory information. This command does NOT store information\n" + @@ -57,13 +57,13 @@ var crawlCmd = &cobra.Command{ } func init() { - crawlCmd.Flags().StringP("username", "u", "", "Set the username for the BMC") - crawlCmd.Flags().StringP("password", "p", "", "Set the password for the BMC") - crawlCmd.Flags().BoolP("insecure", "i", false, "Ignore SSL errors") + CrawlCmd.Flags().StringP("username", "u", "", "Set the username for the BMC") + CrawlCmd.Flags().StringP("password", "p", "", "Set the password for the BMC") + CrawlCmd.Flags().BoolP("insecure", "i", false, "Ignore SSL errors") - checkBindFlagError(viper.BindPFlag("crawl.username", crawlCmd.Flags().Lookup("username"))) - checkBindFlagError(viper.BindPFlag("crawl.password", crawlCmd.Flags().Lookup("password"))) - checkBindFlagError(viper.BindPFlag("crawl.insecure", crawlCmd.Flags().Lookup("insecure"))) + checkBindFlagError(viper.BindPFlag("crawl.username", CrawlCmd.Flags().Lookup("username"))) + checkBindFlagError(viper.BindPFlag("crawl.password", CrawlCmd.Flags().Lookup("password"))) + checkBindFlagError(viper.BindPFlag("crawl.insecure", CrawlCmd.Flags().Lookup("insecure"))) - rootCmd.AddCommand(crawlCmd) + rootCmd.AddCommand(CrawlCmd) } diff --git a/cmd/list.go b/cmd/list.go index e09299f..d760501 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -19,7 +19,7 @@ var ( // The `list` command provides an easy way to show what was found // and stored in a cache database from a scan. The data that's stored // is what is consumed by the `collect` command with the --cache flag. -var listCmd = &cobra.Command{ +var ListCmd = &cobra.Command{ Use: "list", Short: "List information stored in cache from a scan", Long: "Prints all of the host and associated data found from performing a scan.\n" + @@ -55,7 +55,7 @@ var listCmd = &cobra.Command{ } func init() { - listCmd.Flags().StringVar(&format, "format", "", "Set the output format (json|default)") - listCmd.Flags().BoolVar(&showCache, "cache-info", false, "Show cache information and exit") - rootCmd.AddCommand(listCmd) + ListCmd.Flags().StringVar(&format, "format", "", "Set the output format (json|default)") + ListCmd.Flags().BoolVar(&showCache, "cache-info", false, "Show cache information and exit") + rootCmd.AddCommand(ListCmd) } diff --git a/cmd/scan.go b/cmd/scan.go index fedc691..864449f 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -32,7 +32,7 @@ var ( // // See the `ScanForAssets()` function in 'internal/scan.go' for details // related to the implementation. -var scanCmd = &cobra.Command{ +var ScanCmd = &cobra.Command{ Use: "scan urls...", Short: "Scan to discover BMC nodes on a network", Long: "Perform a net scan by attempting to connect to each host and port specified and getting a response.\n" + @@ -175,23 +175,23 @@ var scanCmd = &cobra.Command{ func init() { // scanCmd.Flags().StringSliceVar(&hosts, "host", []string{}, "set additional hosts to scan") - scanCmd.Flags().StringSliceVar(&hosts, "host", nil, "Add individual hosts to scan. (example: https://my.bmc.com:5000; same as using positional args)") - scanCmd.Flags().IntSliceVar(&ports, "port", nil, "Adds additional ports to scan for each host with unspecified ports.") - scanCmd.Flags().StringVar(&scheme, "scheme", "https", "Set the default scheme to use if not specified in host URI. (default is 'https')") - scanCmd.Flags().StringVar(&protocol, "protocol", "tcp", "Set the default protocol to use in scan. (default is 'tcp')") - scanCmd.Flags().StringSliceVar(&subnets, "subnet", nil, "Add additional hosts from specified subnets to scan.") - scanCmd.Flags().IPMaskVar(&subnetMask, "subnet-mask", net.IPv4Mask(255, 255, 255, 0), "Set the default subnet mask to use for with all subnets not using CIDR notation.") - scanCmd.Flags().BoolVar(&disableProbing, "disable-probing", false, "Disable probing found assets for Redfish service(s) running on BMC nodes") - scanCmd.Flags().BoolVar(&disableCache, "disable-cache", false, "Disable saving found assets to a cache database specified with 'cache' flag") + ScanCmd.Flags().StringSliceVar(&hosts, "host", nil, "Add individual hosts to scan. (example: https://my.bmc.com:5000; same as using positional args)") + ScanCmd.Flags().IntSliceVar(&ports, "port", nil, "Adds additional ports to scan for each host with unspecified ports.") + ScanCmd.Flags().StringVar(&scheme, "scheme", "https", "Set the default scheme to use if not specified in host URI. (default is 'https')") + ScanCmd.Flags().StringVar(&protocol, "protocol", "tcp", "Set the default protocol to use in scan. (default is 'tcp')") + ScanCmd.Flags().StringSliceVar(&subnets, "subnet", nil, "Add additional hosts from specified subnets to scan.") + ScanCmd.Flags().IPMaskVar(&subnetMask, "subnet-mask", net.IPv4Mask(255, 255, 255, 0), "Set the default subnet mask to use for with all subnets not using CIDR notation.") + ScanCmd.Flags().BoolVar(&disableProbing, "disable-probing", false, "Disable probing found assets for Redfish service(s) running on BMC nodes") + ScanCmd.Flags().BoolVar(&disableCache, "disable-cache", false, "Disable saving found assets to a cache database specified with 'cache' flag") - checkBindFlagError(viper.BindPFlag("scan.hosts", scanCmd.Flags().Lookup("host"))) - checkBindFlagError(viper.BindPFlag("scan.ports", scanCmd.Flags().Lookup("port"))) - checkBindFlagError(viper.BindPFlag("scan.scheme", scanCmd.Flags().Lookup("scheme"))) - checkBindFlagError(viper.BindPFlag("scan.protocol", scanCmd.Flags().Lookup("protocol"))) - checkBindFlagError(viper.BindPFlag("scan.subnets", scanCmd.Flags().Lookup("subnet"))) - checkBindFlagError(viper.BindPFlag("scan.subnet-masks", scanCmd.Flags().Lookup("subnet-mask"))) - checkBindFlagError(viper.BindPFlag("scan.disable-probing", scanCmd.Flags().Lookup("disable-probing"))) - checkBindFlagError(viper.BindPFlag("scan.disable-cache", scanCmd.Flags().Lookup("disable-cache"))) + checkBindFlagError(viper.BindPFlag("scan.hosts", ScanCmd.Flags().Lookup("host"))) + checkBindFlagError(viper.BindPFlag("scan.ports", ScanCmd.Flags().Lookup("port"))) + checkBindFlagError(viper.BindPFlag("scan.scheme", ScanCmd.Flags().Lookup("scheme"))) + checkBindFlagError(viper.BindPFlag("scan.protocol", ScanCmd.Flags().Lookup("protocol"))) + checkBindFlagError(viper.BindPFlag("scan.subnets", ScanCmd.Flags().Lookup("subnet"))) + checkBindFlagError(viper.BindPFlag("scan.subnet-masks", ScanCmd.Flags().Lookup("subnet-mask"))) + checkBindFlagError(viper.BindPFlag("scan.disable-probing", ScanCmd.Flags().Lookup("disable-probing"))) + checkBindFlagError(viper.BindPFlag("scan.disable-cache", ScanCmd.Flags().Lookup("disable-cache"))) - rootCmd.AddCommand(scanCmd) + rootCmd.AddCommand(ScanCmd) } From ffe60f4a8ce024bedf7d2d1ec1fcffcaebd382f5 Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Wed, 5 Feb 2025 12:01:10 -0500 Subject: [PATCH 03/10] chore: update build workflow and add container build script (#70) * chore: update build workflow and add container build script * Add build dependencies to workflow * fix: remove unnecessary magellan installation path from goreleaser config --- .github/workflows/main.yml | 11 +++++-- .goreleaser.yaml | 56 +++++++++++++++++------------------ build-in-container.sh | 60 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 31 deletions(-) create mode 100644 build-in-container.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1136387..d5109a2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,6 +21,13 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v3 + + - name: Install dependencies + run: | + sudo apt update && sudo apt install -y curl git gcc g++ make \ + gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu \ + libc6-dev-arm64-cross software-properties-common + - name: Checkout uses: actions/checkout@v4 @@ -53,6 +60,6 @@ jobs: args: release --clean id: goreleaser - name: Attest Binaries - uses: actions/attest-build-provenance@v1 + uses: actions/attest-build-provenance@v2 with: - subject-path: '${{ github.workspace }}/dist/magellan_linux_amd64_v3/magellan' + subject-checksums: dist/checksums.txt diff --git a/.goreleaser.yaml b/.goreleaser.yaml index d8552b6..df7b3c8 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -27,18 +27,20 @@ builds: - version goos: - linux - - darwin - - windows goarch: - amd64 - arm64 goamd64: - v3 + goarm: + - 7 env: - - CGO_ENABLED=0 + - CGO_ENABLED=1 + - CC={{ if eq .Arch "arm64" }}aarch64-linux-gnu-gcc{{ else }}gcc{{ end }} + - CXX={{ if eq .Arch "arm64" }}aarch64-linux-gnu-g++{{ else }}g++{{ end }} archives: - - format: tar.gz + - formats: [ 'tar.gz' ] # this name template makes the OS and Arch compatible with the results of uname. name_template: >- {{ .ProjectName }}_ @@ -54,23 +56,22 @@ archives: - magellan.1 nfpms: - - id: magellan - formats: - - deb - - rpm - - apk - - archlinux - maintainer: "David J. Allen " - description: "Magellan is a discovery tool for BMCs." - homepage: "https://www.openchami.org" - license: MIT - section: utils - priority: optional - contents: - - src: dist/magellan_{{ .Os }}_{{ if eq .Arch "amd64" }}{{ .Arch }}_{{ .Amd64 }}{{ else }}{{ .Arch }}{{ end }}/magellan - dst: /usr/local/bin/magellan - - src: magellan.1 - dst: /usr/share/man/man1/ + - id: magellan + formats: + - deb + - rpm + - apk + - archlinux + maintainer: "David J. Allen " + description: "Magellan is a discovery tool for BMCs." + homepage: "https://www.openchami.org" + license: MIT + section: utils + priority: optional + contents: + - src: magellan.1 + dst: /usr/share/man/man1/ + dockers: @@ -93,7 +94,7 @@ dockers: - CHANGELOG.md - README.md - image_templates: - - &arm64v8_linux_image ghcr.io/openchami/{{.ProjectName}}:{{ .Tag }}-arm64 + - &arm64v7_linux_image ghcr.io/openchami/{{.ProjectName}}:{{ .Tag }}-arm64 - ghcr.io/openchami/{{.ProjectName}}:{{ .Major }}-arm64 - ghcr.io/openchami/{{.ProjectName}}:{{ .Major }}.{{ .Minor }}-arm64 use: buildx @@ -114,25 +115,22 @@ docker_manifests: - name_template: "ghcr.io/openchami/{{.ProjectName}}:latest" image_templates: - *amd64_linux_image - - *arm64v8_linux_image + - *arm64v7_linux_image - name_template: "ghcr.io/openchami/{{.ProjectName}}:{{ .Tag }}" image_templates: - *amd64_linux_image - - *arm64v8_linux_image + - *arm64v7_linux_image - name_template: "ghcr.io/openchami/{{.ProjectName}}:{{ .Major }}" image_templates: - *amd64_linux_image - - *arm64v8_linux_image + - *arm64v7_linux_image - name_template: "ghcr.io/openchami/{{.ProjectName}}:{{ .Major }}.{{ .Minor }}" image_templates: - *amd64_linux_image - - *arm64v8_linux_image - - - + - *arm64v7_linux_image checksum: name_template: 'checksums.txt' diff --git a/build-in-container.sh b/build-in-container.sh new file mode 100644 index 0000000..ba07c82 --- /dev/null +++ b/build-in-container.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# This script uses the latest Ubuntu 24.04 container to build the project with GoReleaser. It emulates the GitHub Actions environment as closely as possible. +# Before submitting a PR for release/build. please run this script to ensure your PR will pass the build. + +# Name of the container +CONTAINER_NAME="goreleaser-build" + +# Directory where built binaries will be available +OUTPUT_DIR="$(pwd)/dist" + +export GIT_STATE=$(if git diff-index --quiet HEAD --; then echo 'clean'; else echo 'dirty'; fi) +export BUILD_HOST=$(hostname) +export GO_VERSION=$(go version | awk '{print $3}') +export BUILD_USER=$(whoami) + +# Start a new disposable Ubuntu 24.04 container with the current directory mounted +${CONTAINER_CMD:-docker} run --rm -it \ + --name "$CONTAINER_NAME" \ + -v "$(pwd)":/workspace \ + -w /workspace \ + ubuntu:24.04 bash -c " + + # Suppress timezone prompts + export DEBIAN_FRONTEND=noninteractive + export TZ=UTC + + + # Update package lists and install dependencies + apt update && apt install -y curl git gcc g++ make \ + gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu \ + libc6-dev-arm64-cross software-properties-common + + # Install Go (match GitHub runner version) + curl -fsSL https://golang.org/dl/go1.21.5.linux-amd64.tar.gz | tar -C /usr/local -xz + export PATH=\$PATH:/usr/local/go/bin + go version # Verify Go installation + + # Set GOPATH and update PATH to include Go binaries + export GOPATH=\$(go env GOPATH) + export PATH=\$PATH:\$GOPATH/bin + echo \"GOPATH: \$GOPATH\" && echo \"PATH: \$PATH\"`` + + # Install Goreleaser + curl -sL https://github.com/goreleaser/goreleaser/releases/latest/download/goreleaser_Linux_x86_64.tar.gz | tar -xz -C /usr/local/bin + goreleaser --version # Verify Goreleaser installation + + # Set Build Environment Variables + export GIT_STATE="$GIT_STATE" + export BUILD_HOST="$BUILD_HOST" + export BUILD_USER="$BUILD_USER" + export GO_VERSION=$(go version | awk '{print $3}') + + # Run Goreleaser + goreleaser build --snapshot --clean +" + +# Notify user of success +echo "✅ Build complete! Check the output in: $OUTPUT_DIR" + From 241c45f584fd88bae9cabd49a03134213c7c9520 Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Wed, 5 Feb 2025 12:34:13 -0500 Subject: [PATCH 04/10] Update prbuild.yml Signed-off-by: Alex Lovell-Troy --- .github/workflows/prbuild.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/prbuild.yml b/.github/workflows/prbuild.yml index 476ba60..6621214 100644 --- a/.github/workflows/prbuild.yml +++ b/.github/workflows/prbuild.yml @@ -20,6 +20,13 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v3 + + - name: Install dependencies + run: | + sudo apt update && sudo apt install -y curl git gcc g++ make \ + gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu \ + libc6-dev-arm64-cross software-properties-common + - name: Checkout uses: actions/checkout@v4 with: @@ -41,4 +48,4 @@ jobs: with: version: '~> v2' args: release --snapshot - id: goreleaser \ No newline at end of file + id: goreleaser From 588b1b9798dd9952bcf30d3fecbfee58cef6c592 Mon Sep 17 00:00:00 2001 From: David Allen Date: Thu, 6 Feb 2025 12:59:24 -0700 Subject: [PATCH 05/10] fix: change db.MustExec to db.Exec and handle error --- internal/cache/sqlite/sqlite.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/cache/sqlite/sqlite.go b/internal/cache/sqlite/sqlite.go index 594fd92..a3fc0dc 100644 --- a/internal/cache/sqlite/sqlite.go +++ b/internal/cache/sqlite/sqlite.go @@ -27,7 +27,10 @@ func CreateScannedAssetIfNotExists(path string) (*sqlx.DB, error) { if err != nil { return nil, fmt.Errorf("failed to open database: %v", err) } - db.MustExec(schema) + _, err = db.Exec(schema) + if err != nil { + return nil, fmt.Errorf("failed to create scanned assets cache: %v", err) + } return db, nil } From 3b85dd30497e0e53cac3a0994b91e012a932cefe Mon Sep 17 00:00:00 2001 From: David Allen Date: Thu, 6 Feb 2025 13:06:37 -0700 Subject: [PATCH 06/10] chore: fix critical dependabot issues by updating crypto --- go.mod | 6 +++--- go.sum | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e153625..8d40e30 100644 --- a/go.mod +++ b/go.mod @@ -49,9 +49,9 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.22.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.21.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index befbed6..4129ac5 100644 --- a/go.sum +++ b/go.sum @@ -125,6 +125,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc= golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -153,6 +155,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -166,6 +170,7 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= From 81116ec6167b4be8c94848987541c59639d25aeb Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Thu, 20 Feb 2025 17:09:21 -0700 Subject: [PATCH 07/10] refactor: moved internal functions to pkg and updated refs --- cmd/collect.go | 2 +- cmd/scan.go | 2 +- cmd/update.go | 2 +- internal/cache/sqlite/sqlite.go | 2 +- {internal => pkg}/collect.go | 0 {internal => pkg}/scan.go | 0 {internal => pkg}/update.go | 0 tests/api_test.go | 2 +- 8 files changed, 5 insertions(+), 5 deletions(-) rename {internal => pkg}/collect.go (100%) rename {internal => pkg}/scan.go (100%) rename {internal => pkg}/update.go (100%) diff --git a/cmd/collect.go b/cmd/collect.go index a095b13..3e43499 100644 --- a/cmd/collect.go +++ b/cmd/collect.go @@ -4,9 +4,9 @@ import ( "fmt" "os/user" - magellan "github.com/OpenCHAMI/magellan/internal" "github.com/OpenCHAMI/magellan/internal/cache/sqlite" urlx "github.com/OpenCHAMI/magellan/internal/url" + magellan "github.com/OpenCHAMI/magellan/pkg" "github.com/OpenCHAMI/magellan/pkg/auth" "github.com/cznic/mathutil" "github.com/rs/zerolog/log" diff --git a/cmd/scan.go b/cmd/scan.go index 864449f..76955c0 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -7,8 +7,8 @@ import ( "os" "path" - magellan "github.com/OpenCHAMI/magellan/internal" "github.com/OpenCHAMI/magellan/internal/cache/sqlite" + magellan "github.com/OpenCHAMI/magellan/pkg" "github.com/rs/zerolog/log" urlx "github.com/OpenCHAMI/magellan/internal/url" diff --git a/cmd/update.go b/cmd/update.go index 0800a70..1102ffa 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -4,7 +4,7 @@ import ( "os" "strings" - magellan "github.com/OpenCHAMI/magellan/internal" + magellan "github.com/OpenCHAMI/magellan/pkg" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/internal/cache/sqlite/sqlite.go b/internal/cache/sqlite/sqlite.go index a3fc0dc..e653aa0 100644 --- a/internal/cache/sqlite/sqlite.go +++ b/internal/cache/sqlite/sqlite.go @@ -3,8 +3,8 @@ package sqlite import ( "fmt" - magellan "github.com/OpenCHAMI/magellan/internal" "github.com/OpenCHAMI/magellan/internal/util" + magellan "github.com/OpenCHAMI/magellan/pkg" "github.com/jmoiron/sqlx" ) diff --git a/internal/collect.go b/pkg/collect.go similarity index 100% rename from internal/collect.go rename to pkg/collect.go diff --git a/internal/scan.go b/pkg/scan.go similarity index 100% rename from internal/scan.go rename to pkg/scan.go diff --git a/internal/update.go b/pkg/update.go similarity index 100% rename from internal/update.go rename to pkg/update.go diff --git a/tests/api_test.go b/tests/api_test.go index e823bfa..999f142 100644 --- a/tests/api_test.go +++ b/tests/api_test.go @@ -22,8 +22,8 @@ import ( "flag" - magellan "github.com/OpenCHAMI/magellan/internal" "github.com/OpenCHAMI/magellan/internal/util" + magellan "github.com/OpenCHAMI/magellan/pkg" "github.com/OpenCHAMI/magellan/pkg/client" "github.com/rs/zerolog/log" ) From dcd80a0bc7e3b438153882699c9aa5878db2e8c1 Mon Sep 17 00:00:00 2001 From: Pat Riehecky Date: Mon, 24 Feb 2025 13:07:38 -0600 Subject: [PATCH 08/10] Fix README link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6cb34be..763c732 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # OpenCHAMI Magellan -The `magellan` CLI tool is a Redfish-based, board management controller (BMC) discovery tool designed to scan networks and is written in Go. The tool collects information from BMC nodes using the provided Redfish RESTful API with [`gofish`](https://github.com/stmcginnis/gofish) and loads the queried data into an [SMD](https://github.com/OpenCHAMI/smd/tree/master) instance. The tool strives to be more flexible by implementing multiple methods of discovery to work for a wider range of systems (WIP) and is capable of using independently of other tools or services. +The `magellan` CLI tool is a Redfish-based, board management controller (BMC) discovery tool designed to scan networks and is written in Go. The tool collects information from BMC nodes using the provided Redfish RESTful API with [`gofish`](https://github.com/stmcginnis/gofish) and loads the queried data into an [SMD](https://github.com/OpenCHAMI/smd/) instance. The tool strives to be more flexible by implementing multiple methods of discovery to work for a wider range of systems (WIP) and is capable of using independently of other tools or services. **Note: `magellan` v0.1.0 is incompatible with SMD v2.15.3 and earlier.** From 97a569dd7a6d96a4825b5fac2bb681a3a4a1cca9 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Wed, 26 Feb 2025 16:18:42 -0700 Subject: [PATCH 09/10] collect: return collection output from CollectInventory() --- cmd/collect.go | 2 +- go.sum | 6 +----- pkg/collect.go | 16 ++++++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cmd/collect.go b/cmd/collect.go index 3e43499..242fd0d 100644 --- a/cmd/collect.go +++ b/cmd/collect.go @@ -55,7 +55,7 @@ var CollectCmd = &cobra.Command{ if concurrency <= 0 { concurrency = mathutil.Clamp(len(scannedResults), 1, 10000) } - err = magellan.CollectInventory(&scannedResults, &magellan.CollectParams{ + _, err = magellan.CollectInventory(&scannedResults, &magellan.CollectParams{ URI: host, Username: username, Password: password, diff --git a/go.sum b/go.sum index 4129ac5..be886fb 100644 --- a/go.sum +++ b/go.sum @@ -123,8 +123,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc= @@ -153,8 +151,6 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -168,8 +164,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= diff --git a/pkg/collect.go b/pkg/collect.go index e52bc60..1e47ca7 100644 --- a/pkg/collect.go +++ b/pkg/collect.go @@ -10,8 +10,8 @@ import ( "net/http" "os" "path" - "strings" "path/filepath" + "strings" "sync" "time" @@ -48,19 +48,20 @@ type CollectParams struct { // // Requests can be made to several of the nodes using a goroutine by setting the q.Concurrency // property value between 1 and 10000. -func CollectInventory(assets *[]RemoteAsset, params *CollectParams) error { +func CollectInventory(assets *[]RemoteAsset, params *CollectParams) ([]map[string]any, error) { // check for available remote assets found from scan if assets == nil { - return fmt.Errorf("no assets found") + return nil, fmt.Errorf("no assets found") } if len(*assets) <= 0 { - return fmt.Errorf("no assets found") + return nil, fmt.Errorf("no assets found") } // collect bmc information asynchronously var ( offset = 0 wg sync.WaitGroup + collection = make([]map[string]any, 0) found = make([]string, 0, len(*assets)) done = make(chan struct{}, params.Concurrency+1) chanAssets = make(chan RemoteAsset, params.Concurrency+1) @@ -73,7 +74,7 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) error { if params.CaCertPath != "" { cacert, err := os.ReadFile(params.CaCertPath) if err != nil { - return fmt.Errorf("failed to read CA cert path: %w", err) + return nil, fmt.Errorf("failed to read CA cert path: %w", err) } certPool := x509.NewCertPool() certPool.AppendCertsFromPEM(cacert) @@ -169,6 +170,9 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) error { fmt.Printf("%v\n", string(body)) } + // add data output to collections + collection = append(collection, data) + // write JSON data to file if output path is set using hive partitioning strategy if outputPath != "" { var ( @@ -241,7 +245,7 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) error { wg.Wait() close(done) - return nil + return collection, nil } // FindMACAddressWithIP() returns the MAC address of an ethernet interface with From f9d1cba4700acb054a6b36fdf2bdc00ecb4af561 Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Fri, 7 Mar 2025 17:10:31 -0500 Subject: [PATCH 10/10] feat(secrets): implement SecretStore interface and StaticStore/LocalStore for credential management --- cmd/collect.go | 7 +- cmd/crawl.go | 12 ++- go.mod | 6 +- pkg/collect.go | 19 +++-- pkg/crawler/main.go | 63 ++++++++++++-- pkg/scan.go | 2 +- pkg/secrets/encryption.go | 75 ++++++++++++++++ pkg/secrets/encryption_test.go | 41 +++++++++ pkg/secrets/localstore.go | 129 ++++++++++++++++++++++++++++ pkg/secrets/localstore_test.go | 151 +++++++++++++++++++++++++++++++++ pkg/secrets/main.go | 7 ++ pkg/secrets/staticstore.go | 28 ++++++ tests/compatibility_test.go | 14 ++- 13 files changed, 525 insertions(+), 29 deletions(-) create mode 100644 pkg/secrets/encryption.go create mode 100644 pkg/secrets/encryption_test.go create mode 100644 pkg/secrets/localstore.go create mode 100644 pkg/secrets/localstore_test.go create mode 100644 pkg/secrets/main.go create mode 100644 pkg/secrets/staticstore.go diff --git a/cmd/collect.go b/cmd/collect.go index 242fd0d..c304beb 100644 --- a/cmd/collect.go +++ b/cmd/collect.go @@ -8,6 +8,7 @@ import ( urlx "github.com/OpenCHAMI/magellan/internal/url" magellan "github.com/OpenCHAMI/magellan/pkg" "github.com/OpenCHAMI/magellan/pkg/auth" + "github.com/OpenCHAMI/magellan/pkg/secrets" "github.com/cznic/mathutil" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -55,10 +56,10 @@ var CollectCmd = &cobra.Command{ if concurrency <= 0 { concurrency = mathutil.Clamp(len(scannedResults), 1, 10000) } + // Create a StaticSecretStore to hold the username and password + secrets := secrets.NewStaticStore(username, password) _, err = magellan.CollectInventory(&scannedResults, &magellan.CollectParams{ URI: host, - Username: username, - Password: password, Timeout: timeout, Concurrency: concurrency, Verbose: verbose, @@ -66,7 +67,7 @@ var CollectCmd = &cobra.Command{ OutputPath: outputPath, ForceUpdate: forceUpdate, AccessToken: accessToken, - }) + }, secrets) if err != nil { log.Error().Err(err).Msgf("failed to collect data") } diff --git a/cmd/crawl.go b/cmd/crawl.go index ae61135..e9e91bd 100644 --- a/cmd/crawl.go +++ b/cmd/crawl.go @@ -7,6 +7,7 @@ import ( urlx "github.com/OpenCHAMI/magellan/internal/url" "github.com/OpenCHAMI/magellan/pkg/crawler" + "github.com/OpenCHAMI/magellan/pkg/secrets" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -35,11 +36,14 @@ var CrawlCmd = &cobra.Command{ return nil }, Run: func(cmd *cobra.Command, args []string) { + staticStore := &secrets.StaticStore{ + Username: viper.GetString("crawl.username"), + Password: viper.GetString("crawl.password"), + } systems, err := crawler.CrawlBMCForSystems(crawler.CrawlerConfig{ - URI: args[0], - Username: cmd.Flag("username").Value.String(), - Password: cmd.Flag("password").Value.String(), - Insecure: cmd.Flag("insecure").Value.String() == "true", + URI: args[0], + CredentialStore: staticStore, + Insecure: cmd.Flag("insecure").Value.String() == "true", }) if err != nil { log.Fatalf("Error crawling BMC: %v", err) diff --git a/go.mod b/go.mod index 8d40e30..01bcf2b 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,10 @@ require ( golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 ) -require github.com/rs/zerolog v1.33.0 +require ( + github.com/rs/zerolog v1.33.0 + golang.org/x/crypto v0.32.0 +) require ( github.com/google/go-cmp v0.6.0 // indirect @@ -49,7 +52,6 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.32.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/pkg/collect.go b/pkg/collect.go index 1e47ca7..ccb1a67 100644 --- a/pkg/collect.go +++ b/pkg/collect.go @@ -17,6 +17,7 @@ import ( "github.com/OpenCHAMI/magellan/pkg/client" "github.com/OpenCHAMI/magellan/pkg/crawler" + "github.com/OpenCHAMI/magellan/pkg/secrets" "github.com/rs/zerolog/log" @@ -48,7 +49,7 @@ type CollectParams struct { // // Requests can be made to several of the nodes using a goroutine by setting the q.Concurrency // property value between 1 and 10000. -func CollectInventory(assets *[]RemoteAsset, params *CollectParams) ([]map[string]any, error) { +func CollectInventory(assets *[]RemoteAsset, params *CollectParams, store secrets.SecretStore) ([]map[string]any, error) { // check for available remote assets found from scan if assets == nil { return nil, fmt.Errorf("no assets found") @@ -117,10 +118,9 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) ([]map[strin systems []crawler.InventoryDetail managers []crawler.Manager config = crawler.CrawlerConfig{ - URI: fmt.Sprintf("%s:%d", sr.Host, sr.Port), - Username: params.Username, - Password: params.Password, - Insecure: true, + URI: fmt.Sprintf("%s:%d", sr.Host, sr.Port), + CredentialStore: store, + Insecure: true, } ) systems, err := crawler.CrawlBMCForSystems(config) @@ -260,10 +260,15 @@ func FindMACAddressWithIP(config crawler.CrawlerConfig, targetIP net.IP) (string // gofish (at least for now). If there's a need for grabbing more // manager information in the future, we can move the logic into // the crawler. + bmc_creds, err := config.GetUserPass() + if err != nil { + return "", fmt.Errorf("failed to get credentials for URI: %s", config.URI) + } + client, err := gofish.Connect(gofish.ClientConfig{ Endpoint: config.URI, - Username: config.Username, - Password: config.Password, + Username: bmc_creds.Username, + Password: bmc_creds.Password, Insecure: config.Insecure, BasicAuth: true, }) diff --git a/pkg/crawler/main.go b/pkg/crawler/main.go index 771efb9..e4a17d0 100644 --- a/pkg/crawler/main.go +++ b/pkg/crawler/main.go @@ -1,19 +1,29 @@ package crawler import ( + "encoding/json" "fmt" "strings" + "github.com/OpenCHAMI/magellan/pkg/secrets" "github.com/rs/zerolog/log" "github.com/stmcginnis/gofish" "github.com/stmcginnis/gofish/redfish" ) type CrawlerConfig struct { - URI string // URI of the BMC - Username string // Username for the BMC - Password string // Password for the BMC - Insecure bool // Whether to ignore SSL errors + URI string // URI of the BMC + Insecure bool // Whether to ignore SSL errors + CredentialStore secrets.SecretStore +} + +func (cc *CrawlerConfig) GetUserPass() (BMCUsernamePassword, error) { + return loadBMCCreds(*cc) +} + +type BMCUsernamePassword struct { + Username string `json:"username"` + Password string `json:"password"` } type EthernetInterface struct { @@ -82,11 +92,20 @@ func CrawlBMCForSystems(config CrawlerConfig) ([]InventoryDetail, error) { systems []InventoryDetail rf_systems []*redfish.ComputerSystem ) + // get username and password from secret store + bmc_creds, err := loadBMCCreds(config) + if err != nil { + event := log.Error() + event.Err(err) + event.Msg("failed to load BMC credentials") + return nil, err + } + // initialize gofish client client, err := gofish.Connect(gofish.ClientConfig{ Endpoint: config.URI, - Username: config.Username, - Password: config.Password, + Username: bmc_creds.Username, + Password: bmc_creds.Password, Insecure: config.Insecure, BasicAuth: true, }) @@ -131,12 +150,21 @@ func CrawlBMCForSystems(config CrawlerConfig) ([]InventoryDetail, error) { // CrawlBMCForSystems pulls BMC manager information. func CrawlBMCForManagers(config CrawlerConfig) ([]Manager, error) { + + // get username and password from secret store + bmc_creds, err := loadBMCCreds(config) + if err != nil { + event := log.Error() + event.Err(err) + event.Msg("failed to load BMC credentials") + return nil, err + } // initialize gofish client var managers []Manager client, err := gofish.Connect(gofish.ClientConfig{ Endpoint: config.URI, - Username: config.Username, - Password: config.Password, + Username: bmc_creds.Username, + Password: bmc_creds.Password, Insecure: config.Insecure, BasicAuth: true, }) @@ -288,3 +316,22 @@ func walkManagers(rf_managers []*redfish.Manager, baseURI string) ([]Manager, er } return managers, nil } + +func loadBMCCreds(config CrawlerConfig) (BMCUsernamePassword, error) { + creds, err := config.CredentialStore.GetSecretByID(config.URI) + if err != nil { + event := log.Error() + event.Err(err) + event.Msg("failed to get credentials from secret store") + return BMCUsernamePassword{}, err + } + var bmc_creds BMCUsernamePassword + err = json.Unmarshal([]byte(creds), &bmc_creds) + if err != nil { + event := log.Error() + event.Err(err) + event.Msg("failed to unmarshal credentials") + return BMCUsernamePassword{}, err + } + return bmc_creds, nil +} diff --git a/pkg/scan.go b/pkg/scan.go index a88116d..58785ca 100644 --- a/pkg/scan.go +++ b/pkg/scan.go @@ -203,7 +203,7 @@ func rawConnect(address string, protocol string, timeoutSeconds int, keepOpenOnl ) // try to conntect to host (expects host in format [10.0.0.0]:443) - target := fmt.Sprintf("%s:%s", uri.Hostname(), uri.Port()) + target := net.JoinHostPort(uri.Hostname(), uri.Port()) conn, err := net.DialTimeout(protocol, target, timeoutDuration) if err != nil { asset.State = false diff --git a/pkg/secrets/encryption.go b/pkg/secrets/encryption.go new file mode 100644 index 0000000..6faa737 --- /dev/null +++ b/pkg/secrets/encryption.go @@ -0,0 +1,75 @@ +package secrets + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + + "golang.org/x/crypto/hkdf" +) + +// Derive a unique AES key per SecretID using HKDF +func deriveAESKey(masterKey []byte, secretID string) []byte { + salt := []byte(secretID) + hkdf := hkdf.New(sha256.New, masterKey, salt, nil) + derivedKey := make([]byte, 32) // AES-256 key + io.ReadFull(hkdf, derivedKey) + return derivedKey +} + +// Encrypt data using AES-GCM +func encryptAESGCM(key, plaintext []byte) (string, error) { + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + nonce := make([]byte, aesGCM.NonceSize()) + _, err = io.ReadFull(rand.Reader, nonce) + if err != nil { + return "", err + } + + ciphertext := aesGCM.Seal(nonce, nonce, plaintext, nil) + return hex.EncodeToString(ciphertext), nil +} + +// Decrypt data using AES-GCM +func decryptAESGCM(key []byte, encryptedData string) (string, error) { + data, err := hex.DecodeString(encryptedData) + if err != nil { + return "", err + } + + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + nonceSize := aesGCM.NonceSize() + if len(data) < nonceSize { + return "", fmt.Errorf("ciphertext too short") + } + + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil) + if err != nil { + return "", err + } + + return string(plaintext), nil +} diff --git a/pkg/secrets/encryption_test.go b/pkg/secrets/encryption_test.go new file mode 100644 index 0000000..bc1919b --- /dev/null +++ b/pkg/secrets/encryption_test.go @@ -0,0 +1,41 @@ +package secrets + +import ( + "testing" +) + +func TestDeriveAESKey(t *testing.T) { + masterKey := []byte("testmasterkey") + secretID := "mySecretID" + key1 := deriveAESKey(masterKey, secretID) + key2 := deriveAESKey(masterKey, secretID) + + if len(key1) != 32 { + t.Errorf("derived key should be 32 bytes, got %d", len(key1)) + } + if string(key1) != string(key2) { + t.Errorf("keys derived from same secretID should match") + } +} + +func TestEncryptDecryptAESGCM(t *testing.T) { + masterKey := []byte("anotherTestMasterKey") + secretID := "testSecret" + plaintext := "Hello, secrets!" + + key := deriveAESKey(masterKey, secretID) + + encrypted, err := encryptAESGCM(key, []byte(plaintext)) + if err != nil { + t.Fatalf("encryption failed: %v", err) + } + + decrypted, err := decryptAESGCM(key, encrypted) + if err != nil { + t.Fatalf("decryption failed: %v", err) + } + + if decrypted != plaintext { + t.Errorf("expected %q, got %q", plaintext, decrypted) + } +} diff --git a/pkg/secrets/localstore.go b/pkg/secrets/localstore.go new file mode 100644 index 0000000..76fd136 --- /dev/null +++ b/pkg/secrets/localstore.go @@ -0,0 +1,129 @@ +package secrets + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "sync" +) + +// Structure to store encrypted secrets in a JSON file +type LocalSecretStore struct { + mu sync.RWMutex + masterKey []byte + filename string + Secrets map[string]string `json:"secrets"` +} + +func NewLocalSecretStore(masterKeyHex, filename string, create bool) (*LocalSecretStore, error) { + var secrets map[string]string + + masterKey, err := hex.DecodeString(masterKeyHex) + if err != nil { + return nil, fmt.Errorf("unable to generate masterkey from hex representation: %v", err) + } + + if _, err := os.Stat(filename); os.IsNotExist(err) { + if !create { + return nil, fmt.Errorf("file %s does not exist", filename) + } + file, err := os.Create(filename) + if err != nil { + return nil, fmt.Errorf("unable to create file %s: %v", filename, err) + } + file.Close() + secrets = make(map[string]string) + } + + if secrets == nil { + secrets, err = loadSecrets(filename) + if err != nil { + return nil, fmt.Errorf("unable to load secrets from file: %v", err) + } + } + + return &LocalSecretStore{ + masterKey: masterKey, + filename: filename, + Secrets: secrets, + }, nil +} + +// GenerateMasterKey creates a 32-byte random key and returns it as a hex string. +func GenerateMasterKey() (string, error) { + key := make([]byte, 32) // 32 bytes for AES-256 + _, err := rand.Read(key) + if err != nil { + return "", err + } + return hex.EncodeToString(key), nil +} + +// GetSecretByID decrypts the secret using the master key and returns it +func (l *LocalSecretStore) GetSecretByID(secretID string) (string, error) { + l.mu.RLock() + encrypted, exists := l.Secrets[secretID] + l.mu.RUnlock() + if !exists { + return "", fmt.Errorf("no secret found for %s", secretID) + } + + derivedKey := deriveAESKey(l.masterKey, secretID) + return decryptAESGCM(derivedKey, encrypted) +} + +// StoreSecretByID encrypts the secret using the master key and stores it in the JSON file +func (l *LocalSecretStore) StoreSecretByID(secretID, secret string) error { + derivedKey := deriveAESKey(l.masterKey, secretID) + encryptedSecret, err := encryptAESGCM(derivedKey, []byte(secret)) + if err != nil { + return err + } + + l.mu.Lock() + l.Secrets[secretID] = encryptedSecret + err = saveSecrets(l.filename, l.Secrets) + l.mu.Unlock() + return err +} + +// ListSecrets returns a copy of secret IDs to secrets stored in memory +func (l *LocalSecretStore) ListSecrets() (map[string]string, error) { + l.mu.RLock() + defer l.mu.RUnlock() + + secretsCopy := make(map[string]string) + for key, value := range l.Secrets { + secretsCopy[key] = value + } + return secretsCopy, nil +} + +// Saves secrets back to the JSON file +func saveSecrets(jsonFile string, store map[string]string) error { + file, err := os.OpenFile(jsonFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return err + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + return encoder.Encode(store) +} + +// Loads the secrets JSON file +func loadSecrets(jsonFile string) (map[string]string, error) { + file, err := os.Open(jsonFile) + if err != nil { + return nil, fmt.Errorf("unable to open secret file %s:%v", jsonFile, err) + } + defer file.Close() + + store := make(map[string]string) + decoder := json.NewDecoder(file) + err = decoder.Decode(&store) + return store, err +} diff --git a/pkg/secrets/localstore_test.go b/pkg/secrets/localstore_test.go new file mode 100644 index 0000000..4009946 --- /dev/null +++ b/pkg/secrets/localstore_test.go @@ -0,0 +1,151 @@ +package secrets + +import ( + "encoding/hex" + "os" + "testing" +) + +func TestNewLocalSecretStore(t *testing.T) { + masterKey, err := GenerateMasterKey() + if err != nil { + t.Fatalf("Failed to generate master key: %v", err) + } + + filename := "test_secrets.json" + defer os.Remove(filename) + + store, err := NewLocalSecretStore(masterKey, filename, true) + if err != nil { + t.Fatalf("Failed to create LocalSecretStore: %v", err) + } + + if store.filename != filename { + t.Errorf("Expected filename %s, got %s", filename, store.filename) + } + + if hex.EncodeToString(store.masterKey) != masterKey { + t.Errorf("Expected master key %s, got %s", masterKey, hex.EncodeToString(store.masterKey)) + } +} + +func TestGenerateMasterKey(t *testing.T) { + key, err := GenerateMasterKey() + if err != nil { + t.Fatalf("Failed to generate master key: %v", err) + } + + if len(key) != 64 { // 32 bytes in hex representation + t.Errorf("Expected key length 64, got %d", len(key)) + } +} + +func TestStoreAndGetSecretByID(t *testing.T) { + masterKey, err := GenerateMasterKey() + if err != nil { + t.Fatalf("Failed to generate master key: %v", err) + } + + filename := "test_secrets.json" + defer os.Remove(filename) + + store, err := NewLocalSecretStore(masterKey, filename, true) + if err != nil { + t.Fatalf("Failed to create LocalSecretStore: %v", err) + } + + secretID := "test_secret" + secretValue := "my_secret_value" + + err = store.StoreSecretByID(secretID, secretValue) + if err != nil { + t.Fatalf("Failed to store secret: %v", err) + } + + retrievedSecret, err := store.GetSecretByID(secretID) + if err != nil { + t.Fatalf("Failed to get secret: %v", err) + } + + if retrievedSecret != secretValue { + t.Errorf("Expected secret value %s, got %s", secretValue, retrievedSecret) + } +} + +func TestStoreAndGetSecretJSON(t *testing.T) { + masterKey, err := GenerateMasterKey() + if err != nil { + t.Fatalf("Failed to generate master key: %v", err) + } + + filename := "test_secrets.json" + defer os.Remove(filename) + + store, err := NewLocalSecretStore(masterKey, filename, true) + if err != nil { + t.Fatalf("Failed to create LocalSecretStore: %v", err) + } + + secretID := "json_creds" + jsonSecret := `{"username":"testUser","password":"testPass"}` + + if err := store.StoreSecretByID(secretID, jsonSecret); err != nil { + t.Fatalf("Failed to store JSON secret: %v", err) + } + + retrieved, err := store.GetSecretByID(secretID) + if err != nil { + t.Fatalf("Failed to get JSON secret by ID: %v", err) + } + + if retrieved != jsonSecret { + t.Errorf("Expected %s, got %s", jsonSecret, retrieved) + } +} + +func TestListSecrets(t *testing.T) { + masterKey, err := GenerateMasterKey() + if err != nil { + t.Fatalf("Failed to generate master key: %v", err) + } + + filename := "test_secrets.json" + defer os.Remove(filename) + + store, err := NewLocalSecretStore(masterKey, filename, true) + if err != nil { + t.Fatalf("Failed to create LocalSecretStore: %v", err) + } + + secretID1 := "test_secret_1" + secretValue1 := "my_secret_value_1" + secretID2 := "test_secret_2" + secretValue2 := "my_secret_value_2" + + err = store.StoreSecretByID(secretID1, secretValue1) + if err != nil { + t.Fatalf("Failed to store secret: %v", err) + } + + err = store.StoreSecretByID(secretID2, secretValue2) + if err != nil { + t.Fatalf("Failed to store secret: %v", err) + } + + secrets, err := store.ListSecrets() + if err != nil { + t.Fatalf("Failed to list secrets: %v", err) + } + + if len(secrets) != 2 { + t.Errorf("Expected 2 secrets, got %d", len(secrets)) + } + + if secrets[secretID1] != store.Secrets[secretID1] { + t.Errorf("Expected secret value %s, got %s", store.Secrets[secretID1], secrets[secretID1]) + } + + if secrets[secretID2] != store.Secrets[secretID2] { + t.Errorf("Expected secret value %s, got %s", store.Secrets[secretID2], secrets[secretID2]) + } +} diff --git a/pkg/secrets/main.go b/pkg/secrets/main.go new file mode 100644 index 0000000..5925d53 --- /dev/null +++ b/pkg/secrets/main.go @@ -0,0 +1,7 @@ +package secrets + +type SecretStore interface { + GetSecretByID(secretID string) (string, error) + StoreSecretByID(secretID, secret string) error + ListSecrets() (map[string]string, error) +} diff --git a/pkg/secrets/staticstore.go b/pkg/secrets/staticstore.go new file mode 100644 index 0000000..3e77870 --- /dev/null +++ b/pkg/secrets/staticstore.go @@ -0,0 +1,28 @@ +package secrets + +import "fmt" + +type StaticStore struct { + Username string + Password string +} + +// NewStaticStore creates a new StaticStore with the given username and password. +func NewStaticStore(username, password string) *StaticStore { + return &StaticStore{ + Username: username, + Password: password, + } +} + +func (s *StaticStore) GetSecretByID(secretID string) (string, error) { + return fmt.Sprintf(`{"username":"%s","password":"%s"}`, s.Username, s.Password), nil +} +func (s *StaticStore) StoreSecretByID(secretID, secret string) error { + return nil +} +func (s *StaticStore) ListSecrets() (map[string]string, error) { + return map[string]string{ + "static_creds": fmt.Sprintf(`{"username":"%s","password":"%s"}`, s.Username, s.Password), + }, nil +} diff --git a/tests/compatibility_test.go b/tests/compatibility_test.go index ce2e876..dfcc5e5 100644 --- a/tests/compatibility_test.go +++ b/tests/compatibility_test.go @@ -16,6 +16,7 @@ import ( "github.com/OpenCHAMI/magellan/pkg/client" "github.com/OpenCHAMI/magellan/pkg/crawler" + "github.com/OpenCHAMI/magellan/pkg/secrets" ) var ( @@ -126,12 +127,17 @@ func TestExpectedOutput(t *testing.T) { t.Fatalf("failed while waiting for emulator: %v", err) } + // initialize a credential store + staticStore := &secrets.StaticStore{ + Username: *username, + Password: *password, + } + systems, err := crawler.CrawlBMCForSystems( crawler.CrawlerConfig{ - URI: *host, - Username: *username, - Password: *password, - Insecure: true, + URI: *host, + CredentialStore: staticStore, + Insecure: true, }, )