Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 49 additions & 13 deletions cmd/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -24,6 +25,8 @@ var (
targetHosts [][]string
disableProbing bool
disableCache bool
format string
include []string
)

// The `scan` command is usually the first step to using the CLI tool.
Expand Down Expand Up @@ -66,7 +69,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
Expand Down Expand Up @@ -138,32 +141,61 @@ var ScanCmd = &cobra.Command{
DisableProbing: disableProbing,
Verbose: verbose,
Debug: debug,
Insecure: insecure,
Include: include,
})

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 len(foundAssets) == 0 {
log.Warn().Msg("Scan complete. No responsive assets were found.")
return
}

switch format {
case "json", "yaml":
var output []byte
var err error

if format == "json" {
output, err = json.MarshalIndent(foundAssets, "", " ")
} else {
output, err = yaml.Marshal(foundAssets)
}
Comment thread
davidallendj marked this conversation as resolved.

if err != nil {
log.Printf("failed to make cache directory: %v", err)
log.Error().Err(err).Msgf("Failed to marshal output to %s", format)
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 {
if outputPath != "" {
err := os.WriteFile(outputPath, output, 0644)
if err != nil {
log.Error().Err(err).Msgf("Failed to write to file: %s", outputPath)
} else {
log.Info().Msgf("Scan results successfully written to %s", outputPath)
}
} else {
fmt.Println(string(output))
}

case "db":
if !disableCache && cachePath != "" {
err := os.MkdirAll(path.Dir(cachePath), 0755)
if err != nil {
log.Printf("failed to make cache directory: %v", err)
}
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)
}
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)
}

},
Expand All @@ -177,6 +209,10 @@ 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", 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)")

checkBindFlagError(viper.BindPFlag("scan.ports", ScanCmd.Flags().Lookup("port")))
checkBindFlagError(viper.BindPFlag("scan.scheme", ScanCmd.Flags().Lookup("scheme")))
Expand Down
66 changes: 46 additions & 20 deletions pkg/scan.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package magellan

import (
"crypto/tls"
"fmt"
"math"
"net"
Expand All @@ -16,11 +17,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
Expand All @@ -33,6 +35,8 @@ type ScanParams struct {
DisableProbing bool
Verbose bool
Debug bool
Insecure bool
Include []string
}

// ScanForAssets() performs a net scan on a network to find available services
Expand All @@ -45,7 +49,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.
//
Expand All @@ -61,6 +65,26 @@ func ScanForAssets(params *ScanParams) []RemoteAsset {
log.Info().Any("args", params).Msg("starting scan...")
}

probesToRun := []struct {
Type, Path string
}{}
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{
TLSClientConfig: &tls.Config{InsecureSkipVerify: params.Insecure},
}
probeClient := &http.Client{
Timeout: time.Duration(params.Timeout) * time.Second,
Transport: transport,
}

var wg sync.WaitGroup
wg.Add(params.Concurrency)
for i := 0; i < params.Concurrency; i++ {
Expand All @@ -78,26 +102,28 @@ func ScanForAssets(params *ScanParams) []RemoteAsset {
if params.Verbose {
log.Debug().Err(err).Msgf("failed to connect to host")
}
wg.Done()
return
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
}

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
}
continue
} else if res.StatusCode != http.StatusOK {
if params.Verbose {
log.Printf("request returned code: %v\n", res.StatusCode)
if res != nil {
res.Body.Close()
}
continue
} else {
assetsToAdd = append(assetsToAdd, foundAsset)
}
}
results = append(results, assetsToAdd...)
Expand Down
Loading