From 4cfd48da63445135c1c8a6ac06a4f2a3d317372f Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Thu, 3 Jul 2025 12:45:02 -0700 Subject: [PATCH 1/8] Add support for scanning PDU components by probing JAWS as well as redfish Signed-off-by: Ben McDonald --- cmd/scan.go | 4 +++- pkg/scan.go | 63 ++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/cmd/scan.go b/cmd/scan.go index 7decba6..4a1deb9 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -66,7 +66,7 @@ var ScanCmd = &cobra.Command{ "specified. The `--scheme` flag works similarly and the default value is 'https' in the host URL or with the\n" + "'--protocol' flag.\n\n" + "If the '--disable-probe` flag is used, the tool will not send another request to probe for available.\n" + - "Redfish services. This is not recommended, since the extra request makes the scan a bit more reliable\n" + + "Redfish and JAWS services. This is not recommended, since the extra request makes the scan a bit more reliable\n" + "for determining which hosts to collect inventory data.\n\n", Run: func(cmd *cobra.Command, args []string) { // add default ports for hosts if none are specified with flag @@ -138,6 +138,8 @@ var ScanCmd = &cobra.Command{ DisableProbing: disableProbing, Verbose: verbose, Debug: debug, + Username: username, + Password: password, }) if len(foundAssets) > 0 && debug { diff --git a/pkg/scan.go b/pkg/scan.go index 58785ca..2ef87fd 100644 --- a/pkg/scan.go +++ b/pkg/scan.go @@ -16,11 +16,12 @@ import ( ) type RemoteAsset struct { - Host string `json:"host"` - Port int `json:"port"` - Protocol string `json:"protocol"` - State bool `json:"state"` - Timestamp time.Time `json:"timestamp"` + Host string `json:"host"` + Port int `json:"port"` + Protocol string `json:"protocol"` + State bool `json:"state"` + Timestamp time.Time `json:"timestamp"` + ServiceType string `json:"service_type,omitempty"` } // ScanParams is a collection of commom parameters passed to the CLI @@ -33,6 +34,8 @@ type ScanParams struct { DisableProbing bool Verbose bool Debug bool + Username string + Password string } // ScanForAssets() performs a net scan on a network to find available services @@ -45,7 +48,7 @@ type ScanParams struct { // to be made concurrently. // // If the "disableProbing" flag is set, then the function will skip the extra -// HTTP request made to check if the response was from a Redfish service. +// HTTP request made to check if the response was from a Redfish or JAWS service. // Otherwise, not receiving a 200 OK response code from the HTTP request will // remove the service from being stored in the list of scanned results. // @@ -61,6 +64,17 @@ func ScanForAssets(params *ScanParams) []RemoteAsset { log.Info().Any("args", params).Msg("starting scan...") } + probesToRun := []struct { + Type, Path string + }{ + {Type: "Redfish", Path: "/redfish/v1/"}, + {Type: "JAWS", Path: "/jaws/monitor/outlets"}, + } + + probeClient := &http.Client{ + Timeout: time.Duration(params.Timeout) * time.Second, + } + var wg sync.WaitGroup wg.Add(params.Concurrency) for i := 0; i < params.Concurrency; i++ { @@ -78,26 +92,35 @@ func ScanForAssets(params *ScanParams) []RemoteAsset { if params.Verbose { log.Debug().Err(err).Msgf("failed to connect to host") } - wg.Done() - return + // NOTE: This was wg.Done() and return in the original, but that stops the whole worker. + // Continuing allows the worker to process other hosts in its queue. + continue } if !params.DisableProbing { assetsToAdd := []RemoteAsset{} for _, foundAsset := range foundAssets { - url := fmt.Sprintf("%s:%d/redfish/v1/", foundAsset.Host, foundAsset.Port) - res, _, err := client.MakeRequest(nil, url, http.MethodGet, nil, nil) - if err != nil || res == nil { - if params.Verbose { - log.Printf("failed to make request: %v\n", err) + for _, probe := range probesToRun { + probeURL := fmt.Sprintf("%s:%d%s", foundAsset.Host, foundAsset.Port, probe.Path) + req, err := http.NewRequest("GET", probeURL, nil) + if err != nil { + continue } - continue - } else if res.StatusCode != http.StatusOK { - if params.Verbose { - log.Printf("request returned code: %v\n", res.StatusCode) + + // Add authentication for JAWS endpoints if credentials are provided + if probe.Type == "JAWS" && params.Username != "" && params.Password != "" { + req.SetBasicAuth(params.Username, params.Password) + } + + res, err := probeClient.Do(req) + if err == nil && res != nil && res.StatusCode == http.StatusOK { + res.Body.Close() + foundAsset.ServiceType = probe.Type + assetsToAdd = append(assetsToAdd, foundAsset) + break // Found a valid service, no need to probe other types + } + if res != nil { + res.Body.Close() } - continue - } else { - assetsToAdd = append(assetsToAdd, foundAsset) } } results = append(results, assetsToAdd...) From 488ad241f6393458e28ba48199e750c61a5179e5 Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Thu, 3 Jul 2025 12:52:18 -0700 Subject: [PATCH 2/8] ADd secure, remove username and password Signed-off-by: Ben McDonald --- cmd/scan.go | 5 +++-- pkg/scan.go | 17 +++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/cmd/scan.go b/cmd/scan.go index 4a1deb9..2f1e932 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -24,6 +24,7 @@ var ( targetHosts [][]string disableProbing bool disableCache bool + insecure bool ) // The `scan` command is usually the first step to using the CLI tool. @@ -138,8 +139,7 @@ var ScanCmd = &cobra.Command{ DisableProbing: disableProbing, Verbose: verbose, Debug: debug, - Username: username, - Password: password, + Insecure: insecure, }) if len(foundAssets) > 0 && debug { @@ -179,6 +179,7 @@ func init() { 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().BoolVar(&insecure, "insecure", false, "Skip TLS certificate verification during probe") checkBindFlagError(viper.BindPFlag("scan.ports", ScanCmd.Flags().Lookup("port"))) checkBindFlagError(viper.BindPFlag("scan.scheme", ScanCmd.Flags().Lookup("scheme"))) diff --git a/pkg/scan.go b/pkg/scan.go index 2ef87fd..af4907f 100644 --- a/pkg/scan.go +++ b/pkg/scan.go @@ -1,6 +1,7 @@ package magellan import ( + "crypto/tls" "fmt" "math" "net" @@ -34,8 +35,7 @@ type ScanParams struct { DisableProbing bool Verbose bool Debug bool - Username string - Password string + Insecure bool } // ScanForAssets() performs a net scan on a network to find available services @@ -71,8 +71,12 @@ func ScanForAssets(params *ScanParams) []RemoteAsset { {Type: "JAWS", Path: "/jaws/monitor/outlets"}, } + transport := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: params.Insecure}, + } probeClient := &http.Client{ - Timeout: time.Duration(params.Timeout) * time.Second, + Timeout: time.Duration(params.Timeout) * time.Second, + Transport: transport, } var wg sync.WaitGroup @@ -92,8 +96,6 @@ func ScanForAssets(params *ScanParams) []RemoteAsset { if params.Verbose { log.Debug().Err(err).Msgf("failed to connect to host") } - // NOTE: This was wg.Done() and return in the original, but that stops the whole worker. - // Continuing allows the worker to process other hosts in its queue. continue } if !params.DisableProbing { @@ -106,11 +108,6 @@ func ScanForAssets(params *ScanParams) []RemoteAsset { continue } - // Add authentication for JAWS endpoints if credentials are provided - if probe.Type == "JAWS" && params.Username != "" && params.Password != "" { - req.SetBasicAuth(params.Username, params.Password) - } - res, err := probeClient.Do(req) if err == nil && res != nil && res.StatusCode == http.StatusOK { res.Body.Close() From 7988ed6b9fcae896fe2e2393017c94a333dfef7b Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Thu, 3 Jul 2025 13:04:19 -0700 Subject: [PATCH 3/8] Remove unnecessary insecure flag Signed-off-by: Ben McDonald --- cmd/scan.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/scan.go b/cmd/scan.go index 2f1e932..d4c9788 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -24,7 +24,6 @@ var ( targetHosts [][]string disableProbing bool disableCache bool - insecure bool ) // The `scan` command is usually the first step to using the CLI tool. From c413dd5649d53e36b1633f54b087713a6a05bcdb Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Thu, 3 Jul 2025 14:47:24 -0700 Subject: [PATCH 4/8] Add printing to stdout or a file instead of only DB for scan Signed-off-by: Ben McDonald --- cmd/scan.go | 72 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/cmd/scan.go b/cmd/scan.go index d4c9788..d5f3def 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -10,6 +10,7 @@ import ( "github.com/OpenCHAMI/magellan/internal/cache/sqlite" magellan "github.com/OpenCHAMI/magellan/pkg" "github.com/rs/zerolog/log" + "gopkg.in/yaml.v3" urlx "github.com/OpenCHAMI/magellan/internal/url" "github.com/cznic/mathutil" @@ -24,6 +25,7 @@ var ( targetHosts [][]string disableProbing bool disableCache bool + format string ) // The `scan` command is usually the first step to using the CLI tool. @@ -141,30 +143,62 @@ var ScanCmd = &cobra.Command{ Insecure: insecure, }) - if len(foundAssets) > 0 && debug { - log.Info().Any("assets", foundAssets).Msgf("found assets from scan") - } - - if !disableCache && cachePath != "" { - // make the cache directory path if needed - err := os.MkdirAll(path.Dir(cachePath), 0755) - if err != nil { - log.Printf("failed to make cache directory: %v", err) + switch format { + case "json", "yaml": + if len(foundAssets) == 0 { + log.Info().Msg("Scan complete. No responsive assets were found.") + return } - // TODO: change this to use an extensible plugin system for storage solutions - // (i.e. something like cache.InsertScannedAssets(path, assets) which implements a Cache interface) - if len(foundAssets) > 0 { - err = sqlite.InsertScannedAssets(cachePath, foundAssets...) + var output []byte + var err error + + if format == "json" { + output, err = json.MarshalIndent(foundAssets, "", " ") + } else { + output, err = yaml.Marshal(foundAssets) + } + + if err != nil { + log.Error().Err(err).Msgf("Failed to marshal output to %s", format) + return + } + + // if -o flag was used, write to file, if not print to console. + if outputPath != "" { + err := os.WriteFile(outputPath, output, 0644) if err != nil { - log.Error().Err(err).Msg("failed to write scanned assets to cache") - } - if verbose { - log.Info().Msgf("saved assets to cache: %s", cachePath) + log.Error().Err(err).Msgf("Failed to write to file: %s", outputPath) + } else { + log.Info().Msgf("Scan results successfully written to %s", outputPath) } } else { - log.Warn().Msg("no assets found to save") + fmt.Println(string(output)) } + + case "db": + if len(foundAssets) > 0 && debug { + log.Info().Any("assets", foundAssets).Msgf("found assets from scan") + } + + if !disableCache && cachePath != "" { + err := os.MkdirAll(path.Dir(cachePath), 0755) + if err != nil { + log.Printf("failed to make cache directory: %v", err) + } + if len(foundAssets) > 0 { + err := sqlite.InsertScannedAssets(cachePath, foundAssets...) + if err != nil { + log.Error().Err(err).Msg("failed to write scanned assets to cache") + } else if verbose { + log.Info().Msgf("Saved assets to cache: %s", cachePath) + } + } else { + log.Warn().Msg("no assets found to save") + } + } + default: + log.Error().Msgf("unknown format specified: %s. Please use 'db', 'json', or 'yaml'.", format) } }, @@ -179,6 +213,8 @@ func init() { 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().BoolVar(&insecure, "insecure", false, "Skip TLS certificate verification during probe") + ScanCmd.Flags().StringVarP(&format, "format", "F", "db", "Output format (db, json, yaml)") + ScanCmd.Flags().StringVarP(&outputPath, "output", "o", "", "Output file path (for json/yaml formats)") checkBindFlagError(viper.BindPFlag("scan.ports", ScanCmd.Flags().Lookup("port"))) checkBindFlagError(viper.BindPFlag("scan.scheme", ScanCmd.Flags().Lookup("scheme"))) From 96931cd8a3d1c10e6802ef4f1ffe94f3261093bc Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Thu, 3 Jul 2025 14:57:29 -0700 Subject: [PATCH 5/8] Add include flag Signed-off-by: Ben McDonald --- cmd/scan.go | 3 +++ pkg/scan.go | 12 +++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/cmd/scan.go b/cmd/scan.go index d5f3def..3dfdcc2 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -26,6 +26,7 @@ var ( disableProbing bool disableCache bool format string + include []string ) // The `scan` command is usually the first step to using the CLI tool. @@ -141,6 +142,7 @@ var ScanCmd = &cobra.Command{ Verbose: verbose, Debug: debug, Insecure: insecure, + Include: include, }) switch format { @@ -215,6 +217,7 @@ func init() { ScanCmd.Flags().BoolVar(&insecure, "insecure", false, "Skip TLS certificate verification during probe") ScanCmd.Flags().StringVarP(&format, "format", "F", "db", "Output format (db, json, yaml)") ScanCmd.Flags().StringVarP(&outputPath, "output", "o", "", "Output file path (for json/yaml formats)") + ScanCmd.Flags().StringSliceVar(&include, "include", []string{"bmcs"}, "Asset types to scan for (bmcs, pdus)") checkBindFlagError(viper.BindPFlag("scan.ports", ScanCmd.Flags().Lookup("port"))) checkBindFlagError(viper.BindPFlag("scan.scheme", ScanCmd.Flags().Lookup("scheme"))) diff --git a/pkg/scan.go b/pkg/scan.go index af4907f..27c0411 100644 --- a/pkg/scan.go +++ b/pkg/scan.go @@ -36,6 +36,7 @@ type ScanParams struct { Verbose bool Debug bool Insecure bool + Include []string } // ScanForAssets() performs a net scan on a network to find available services @@ -66,9 +67,14 @@ func ScanForAssets(params *ScanParams) []RemoteAsset { probesToRun := []struct { Type, Path string - }{ - {Type: "Redfish", Path: "/redfish/v1/"}, - {Type: "JAWS", Path: "/jaws/monitor/outlets"}, + }{} + for _, item := range params.Include { + if item == "bmcs" { + probesToRun = append(probesToRun, struct{ Type, Path string }{Type: "Redfish", Path: "/redfish/v1/"}) + } + if item == "pdus" { + probesToRun = append(probesToRun, struct{ Type, Path string }{Type: "JAWS", Path: "/jaws/monitor/outlets"}) + } } transport := &http.Transport{ From 117783d264a3df96fa1e02cfdc488c50db2f5476 Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Tue, 8 Jul 2025 11:11:54 -0700 Subject: [PATCH 6/8] Pull length of assets check out, per David's feedback Signed-off-by: Ben McDonald --- cmd/scan.go | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/cmd/scan.go b/cmd/scan.go index 3dfdcc2..bcaad9f 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -145,13 +145,17 @@ var ScanCmd = &cobra.Command{ Include: include, }) + if len(foundAssets) > 0 && debug { + log.Info().Any("assets", foundAssets).Msgf("found assets from scan") + } + + if len(foundAssets) == 0 { + log.Info().Msg("Scan complete. No responsive assets were found.") + return + } + switch format { case "json", "yaml": - if len(foundAssets) == 0 { - log.Info().Msg("Scan complete. No responsive assets were found.") - return - } - var output []byte var err error @@ -166,7 +170,6 @@ var ScanCmd = &cobra.Command{ return } - // if -o flag was used, write to file, if not print to console. if outputPath != "" { err := os.WriteFile(outputPath, output, 0644) if err != nil { @@ -179,24 +182,16 @@ var ScanCmd = &cobra.Command{ } case "db": - if len(foundAssets) > 0 && debug { - log.Info().Any("assets", foundAssets).Msgf("found assets from scan") - } - if !disableCache && cachePath != "" { err := os.MkdirAll(path.Dir(cachePath), 0755) if err != nil { log.Printf("failed to make cache directory: %v", err) } - if len(foundAssets) > 0 { - err := sqlite.InsertScannedAssets(cachePath, foundAssets...) - if err != nil { - log.Error().Err(err).Msg("failed to write scanned assets to cache") - } else if verbose { - log.Info().Msgf("Saved assets to cache: %s", cachePath) - } - } else { - log.Warn().Msg("no assets found to save") + err = sqlite.InsertScannedAssets(cachePath, foundAssets...) + if err != nil { + log.Error().Err(err).Msg("failed to write scanned assets to cache") + } else if verbose { + log.Info().Msgf("Saved assets to cache: %s", cachePath) } } default: From 3a6ed5a24e31ace35f231e95558464f887aece9d Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Tue, 8 Jul 2025 15:09:03 -0700 Subject: [PATCH 7/8] Switch to a warn Signed-off-by: Ben McDonald --- cmd/scan.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/scan.go b/cmd/scan.go index bcaad9f..f7fa258 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -150,7 +150,7 @@ var ScanCmd = &cobra.Command{ } if len(foundAssets) == 0 { - log.Info().Msg("Scan complete. No responsive assets were found.") + log.Warn().Msg("Scan complete. No responsive assets were found.") return } From 833fb80c10aa83c6f30eb3c5f12295114b706960 Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Tue, 8 Jul 2025 15:16:26 -0700 Subject: [PATCH 8/8] Switch insecure default to true Signed-off-by: Ben McDonald --- cmd/scan.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/scan.go b/cmd/scan.go index f7fa258..47846d5 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -209,7 +209,7 @@ func init() { 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().BoolVar(&insecure, "insecure", false, "Skip TLS certificate verification during probe") + ScanCmd.Flags().BoolVar(&insecure, "insecure", true, "Skip TLS certificate verification during probe") ScanCmd.Flags().StringVarP(&format, "format", "F", "db", "Output format (db, json, yaml)") ScanCmd.Flags().StringVarP(&outputPath, "output", "o", "", "Output file path (for json/yaml formats)") ScanCmd.Flags().StringSliceVar(&include, "include", []string{"bmcs"}, "Asset types to scan for (bmcs, pdus)")