mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-08-20 22:52:32 +02:00
- Implement cloud provider infrastructure (DigitalOcean, AWS, GCP, Linode, Azure) with Pulumi integration for distributed scanning - Add nmap and tmux utility functions for port scanning results processing and long-running background session management - Introduce webhook-triggered run execution with unique UUID and authentication key support for external integrations
444 lines
13 KiB
Go
444 lines
13 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/goccy/go-yaml/parser"
|
|
"github.com/j3ssie/osmedeus/v5/internal/cloud"
|
|
"github.com/j3ssie/osmedeus/v5/internal/config"
|
|
"github.com/j3ssie/osmedeus/v5/public"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
// Cloud command flags
|
|
cloudProvider string
|
|
cloudMode string
|
|
cloudInstances int
|
|
cloudForce bool
|
|
)
|
|
|
|
// cloudCmd represents the cloud command
|
|
var cloudCmd = &cobra.Command{
|
|
Use: "cloud",
|
|
Short: "Cloud infrastructure management commands",
|
|
Long: `Provision and manage cloud infrastructure for distributed scanning`,
|
|
}
|
|
|
|
// cloudConfigCmd manages cloud configuration
|
|
var cloudConfigCmd = &cobra.Command{
|
|
Use: "config",
|
|
Short: "Manage cloud configuration",
|
|
Long: `View and update cloud configuration settings`,
|
|
}
|
|
|
|
// cloudConfigSetCmd sets a cloud config value
|
|
var cloudConfigSetCmd = &cobra.Command{
|
|
Use: "set <key> <value>",
|
|
Short: "Set a cloud configuration value",
|
|
Long: `Set a cloud configuration value using dot notation (e.g., defaults.provider digitalocean)`,
|
|
Args: cobra.ExactArgs(2),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cfg := config.Get()
|
|
if cfg == nil {
|
|
return errConfigNotLoaded
|
|
}
|
|
|
|
configPath := cfg.Cloud.CloudSettings
|
|
if configPath == "" {
|
|
configPath = filepath.Join(cfg.BaseFolder, "cloud", "cloud-settings.yaml")
|
|
}
|
|
|
|
// Expand template variables
|
|
configPath = strings.ReplaceAll(configPath, "{{base_folder}}", cfg.BaseFolder)
|
|
|
|
// Auto-create from preset if file doesn't exist
|
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
|
if err := ensureCloudConfig(configPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Load existing config
|
|
cloudCfg, err := cloud.LoadCloudConfig(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load cloud config: %w", err)
|
|
}
|
|
|
|
// Set the value using dot notation
|
|
key := args[0]
|
|
value := args[1]
|
|
|
|
if err := setCloudConfigValue(cloudCfg, key, value); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Save config
|
|
if err := cloud.SaveCloudConfig(cloudCfg, configPath); err != nil {
|
|
return fmt.Errorf("failed to save cloud config: %w", err)
|
|
}
|
|
|
|
printer.Success("Cloud config updated: %s = %s", key, value)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var cloudConfigListShowSecrets bool
|
|
|
|
// cloudConfigListCmd lists cloud configuration as flattened key=value pairs
|
|
var cloudConfigListCmd = &cobra.Command{
|
|
Use: "list",
|
|
Aliases: []string{"ls"},
|
|
Short: "List cloud configuration values",
|
|
Long: `Display cloud configuration as flattened key=value pairs`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cfg := config.Get()
|
|
if cfg == nil {
|
|
return errConfigNotLoaded
|
|
}
|
|
|
|
configPath := cfg.Cloud.CloudSettings
|
|
if configPath == "" {
|
|
configPath = filepath.Join(cfg.BaseFolder, "cloud", "cloud-settings.yaml")
|
|
}
|
|
|
|
// Expand template variables
|
|
configPath = strings.ReplaceAll(configPath, "{{base_folder}}", cfg.BaseFolder)
|
|
|
|
// Auto-create from preset if file doesn't exist
|
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
|
if err := ensureCloudConfig(configPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Read and parse YAML via AST to flatten
|
|
content, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read cloud config: %w", err)
|
|
}
|
|
|
|
file, err := parser.ParseBytes(content, parser.ParseComments)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse cloud config: %w", err)
|
|
}
|
|
if len(file.Docs) == 0 {
|
|
return fmt.Errorf("empty cloud config file")
|
|
}
|
|
|
|
out := map[string]string{}
|
|
flattenASTScalars(file.Docs[0].Body, "", out)
|
|
|
|
keys := make([]string, 0, len(out))
|
|
for k := range out {
|
|
keys = append(keys, k)
|
|
}
|
|
sortStrings(keys)
|
|
|
|
for _, k := range keys {
|
|
v := out[k]
|
|
if !cloudConfigListShowSecrets {
|
|
v = redactValueForDisplay(k, v, false)
|
|
}
|
|
fmt.Printf("%s = %s\n", getCategoryColor(k)(k), v)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
// ensureCloudConfig creates the cloud config file from the embedded preset
|
|
func ensureCloudConfig(configPath string) error {
|
|
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
|
|
return fmt.Errorf("failed to create cloud config directory: %w", err)
|
|
}
|
|
|
|
data, err := public.GetCloudConfigExample()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read embedded cloud config preset: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(configPath, data, 0644); err != nil {
|
|
return fmt.Errorf("failed to write cloud config: %w", err)
|
|
}
|
|
|
|
printer.Success("Created cloud config from preset at %s", configPath)
|
|
return nil
|
|
}
|
|
|
|
// cloudCreateCmd provisions cloud infrastructure
|
|
var cloudCreateCmd = &cobra.Command{
|
|
Use: "create",
|
|
Short: "Create cloud infrastructure",
|
|
Long: `Provision cloud infrastructure (VMs or serverless functions)`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cfg := config.Get()
|
|
if cfg == nil {
|
|
return errConfigNotLoaded
|
|
}
|
|
|
|
if !cfg.Cloud.Enabled {
|
|
return fmt.Errorf("cloud features are disabled. Enable in osm-settings.yaml: cloud.enabled = true")
|
|
}
|
|
|
|
// Load cloud config
|
|
configPath := strings.ReplaceAll(cfg.Cloud.CloudSettings, "{{base_folder}}", cfg.BaseFolder)
|
|
cloudCfg, err := cloud.LoadCloudConfig(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load cloud config: %w", err)
|
|
}
|
|
|
|
// Override provider if specified
|
|
providerType := cloud.ProviderType(cloudCfg.Defaults.Provider)
|
|
if cloudProvider != "" {
|
|
providerType = cloud.ProviderType(cloudProvider)
|
|
}
|
|
|
|
// Override mode if specified
|
|
mode := cloud.ExecutionMode(cloudCfg.Defaults.Mode)
|
|
if cloudMode != "" {
|
|
mode = cloud.ExecutionMode(cloudMode)
|
|
}
|
|
|
|
// Override instance count if specified
|
|
instanceCount := cloudCfg.Defaults.MaxInstances
|
|
if cloudInstances > 0 {
|
|
instanceCount = cloudInstances
|
|
}
|
|
|
|
// Validate against limits
|
|
if instanceCount > cloudCfg.Limits.MaxInstances {
|
|
return fmt.Errorf("instance count (%d) exceeds limit (%d)", instanceCount, cloudCfg.Limits.MaxInstances)
|
|
}
|
|
|
|
printer.Section("Creating Cloud Infrastructure")
|
|
printer.Info("Provider: %s", providerType)
|
|
printer.Info("Mode: %s", mode)
|
|
printer.Info("Instances: %d", instanceCount)
|
|
|
|
// Create provider
|
|
provider, err := cloud.CreateProvider(cloudCfg, providerType)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create provider: %w", err)
|
|
}
|
|
|
|
// Validate provider credentials
|
|
ctx := context.Background()
|
|
if err := provider.Validate(ctx); err != nil {
|
|
return fmt.Errorf("provider validation failed: %w", err)
|
|
}
|
|
|
|
// Estimate cost
|
|
estimate, err := provider.EstimateCost(mode, instanceCount)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to estimate cost: %w", err)
|
|
}
|
|
|
|
printer.Info("Estimated cost: $%.2f/hour ($%.2f/day)", estimate.HourlyCost, estimate.DailyCost)
|
|
for note := range estimate.Notes {
|
|
printer.Info(" - %s", estimate.Notes[note])
|
|
}
|
|
|
|
// TODO: Add confirmation prompt
|
|
|
|
// Create infrastructure (placeholder - will be implemented)
|
|
printer.Warning("Infrastructure creation not yet fully implemented")
|
|
return fmt.Errorf("not yet implemented")
|
|
},
|
|
}
|
|
|
|
// cloudListCmd lists cloud infrastructure
|
|
var cloudListCmd = &cobra.Command{
|
|
Use: "list",
|
|
Short: "List cloud infrastructure",
|
|
Long: `List all active cloud infrastructure`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cfg := config.Get()
|
|
if cfg == nil {
|
|
return errConfigNotLoaded
|
|
}
|
|
|
|
statePath := strings.ReplaceAll(cfg.Cloud.CloudPath, "{{base_folder}}", cfg.BaseFolder)
|
|
infrastructures, err := cloud.ListInfrastructures(statePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list infrastructures: %w", err)
|
|
}
|
|
|
|
if len(infrastructures) == 0 {
|
|
printer.Info("No active cloud infrastructure found")
|
|
return nil
|
|
}
|
|
|
|
printer.Section("Cloud Infrastructure")
|
|
for _, infra := range infrastructures {
|
|
printer.Info("ID: %s", infra.ID)
|
|
printer.Info(" Provider: %s", infra.Provider)
|
|
printer.Info(" Mode: %s", infra.Mode)
|
|
printer.Info(" Created: %s", infra.CreatedAt.Format("2006-01-02 15:04:05"))
|
|
printer.Info(" Resources: %d", len(infra.Resources))
|
|
for _, res := range infra.Resources {
|
|
printer.Info(" - %s (%s) - %s", res.Name, res.Type, res.Status)
|
|
if res.PublicIP != "" {
|
|
printer.Info(" IP: %s", res.PublicIP)
|
|
}
|
|
if res.WorkerID != "" {
|
|
printer.Info(" Worker: %s", res.WorkerID)
|
|
}
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
// cloudDestroyCmd destroys cloud infrastructure
|
|
var cloudDestroyCmd = &cobra.Command{
|
|
Use: "destroy [infrastructure-id]",
|
|
Short: "Destroy cloud infrastructure",
|
|
Long: `Tear down cloud infrastructure and clean up resources`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cfg := config.Get()
|
|
if cfg == nil {
|
|
return errConfigNotLoaded
|
|
}
|
|
|
|
// TODO: Implement destroy logic
|
|
printer.Warning("Infrastructure destruction not yet fully implemented")
|
|
return fmt.Errorf("not yet implemented")
|
|
},
|
|
}
|
|
|
|
// cloudRunCmd runs a workflow on cloud infrastructure
|
|
var cloudRunCmd = &cobra.Command{
|
|
Use: "run",
|
|
Short: "Run workflow on cloud infrastructure",
|
|
Long: `Provision cloud infrastructure, run workflow, and collect results`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cfg := config.Get()
|
|
if cfg == nil {
|
|
return errConfigNotLoaded
|
|
}
|
|
|
|
// TODO: Implement cloud run logic
|
|
printer.Warning("Cloud run not yet fully implemented")
|
|
return fmt.Errorf("not yet implemented")
|
|
},
|
|
}
|
|
|
|
// setCloudConfigValue sets a nested config value using dot notation
|
|
func setCloudConfigValue(cfg *config.CloudConfigs, key, value string) error {
|
|
parts := strings.Split(key, ".")
|
|
if len(parts) < 2 {
|
|
return fmt.Errorf("invalid key format. Use dot notation (e.g., defaults.provider)")
|
|
}
|
|
|
|
// Simple implementation for common keys
|
|
switch parts[0] {
|
|
case "defaults":
|
|
switch parts[1] {
|
|
case "provider":
|
|
cfg.Defaults.Provider = value
|
|
case "mode":
|
|
cfg.Defaults.Mode = value
|
|
case "max_instances":
|
|
var val int
|
|
if _, err := fmt.Sscanf(value, "%d", &val); err != nil {
|
|
return fmt.Errorf("invalid integer value: %s", value)
|
|
}
|
|
cfg.Defaults.MaxInstances = val
|
|
case "cleanup_on_failure":
|
|
cfg.Defaults.CleanupOnFailure = (value == "true")
|
|
default:
|
|
return fmt.Errorf("unknown key: %s", key)
|
|
}
|
|
|
|
case "providers":
|
|
if len(parts) < 3 {
|
|
return fmt.Errorf("provider key requires 3 parts (e.g., providers.digitalocean.token)")
|
|
}
|
|
switch parts[1] {
|
|
case "digitalocean":
|
|
switch parts[2] {
|
|
case "token":
|
|
cfg.Providers.DigitalOcean.Token = value
|
|
case "region":
|
|
cfg.Providers.DigitalOcean.Region = value
|
|
case "size":
|
|
cfg.Providers.DigitalOcean.Size = value
|
|
default:
|
|
return fmt.Errorf("unknown DigitalOcean key: %s", parts[2])
|
|
}
|
|
case "aws":
|
|
switch parts[2] {
|
|
case "access_key_id":
|
|
cfg.Providers.AWS.AccessKeyID = value
|
|
case "secret_access_key":
|
|
cfg.Providers.AWS.SecretAccessKey = value
|
|
case "region":
|
|
cfg.Providers.AWS.Region = value
|
|
default:
|
|
return fmt.Errorf("unknown AWS key: %s", parts[2])
|
|
}
|
|
default:
|
|
return fmt.Errorf("unknown provider: %s", parts[1])
|
|
}
|
|
|
|
case "limits":
|
|
switch parts[1] {
|
|
case "max_hourly_spend":
|
|
var val float64
|
|
if _, err := fmt.Sscanf(value, "%f", &val); err != nil {
|
|
return fmt.Errorf("invalid float value: %s", value)
|
|
}
|
|
cfg.Limits.MaxHourlySpend = val
|
|
case "max_total_spend":
|
|
var val float64
|
|
if _, err := fmt.Sscanf(value, "%f", &val); err != nil {
|
|
return fmt.Errorf("invalid float value: %s", value)
|
|
}
|
|
cfg.Limits.MaxTotalSpend = val
|
|
case "max_instances":
|
|
var val int
|
|
if _, err := fmt.Sscanf(value, "%d", &val); err != nil {
|
|
return fmt.Errorf("invalid integer value: %s", value)
|
|
}
|
|
cfg.Limits.MaxInstances = val
|
|
default:
|
|
return fmt.Errorf("unknown limit key: %s", parts[1])
|
|
}
|
|
|
|
default:
|
|
return fmt.Errorf("unknown config section: %s", parts[0])
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func init() {
|
|
// Add subcommands
|
|
cloudCmd.AddCommand(cloudConfigCmd)
|
|
cloudCmd.AddCommand(cloudCreateCmd)
|
|
cloudCmd.AddCommand(cloudListCmd)
|
|
cloudCmd.AddCommand(cloudDestroyCmd)
|
|
cloudCmd.AddCommand(cloudRunCmd)
|
|
|
|
cloudConfigCmd.AddCommand(cloudConfigSetCmd)
|
|
cloudConfigCmd.AddCommand(cloudConfigListCmd)
|
|
cloudConfigListCmd.Flags().BoolVar(&cloudConfigListShowSecrets, "show-secrets", false, "show sensitive values")
|
|
|
|
// Flags for create command
|
|
cloudCreateCmd.Flags().StringVarP(&cloudProvider, "provider", "p", "", "Cloud provider (aws, gcp, digitalocean, linode, azure)")
|
|
cloudCreateCmd.Flags().StringVarP(&cloudMode, "mode", "m", "", "Execution mode (vm, serverless)")
|
|
cloudCreateCmd.Flags().IntVarP(&cloudInstances, "instances", "n", 0, "Number of instances to create")
|
|
cloudCreateCmd.Flags().BoolVarP(&cloudForce, "force", "f", false, "Force recreation of existing infrastructure")
|
|
|
|
// Flags for run command (inherit from run command)
|
|
cloudRunCmd.Flags().StringVarP(&cloudProvider, "provider", "p", "", "Cloud provider")
|
|
cloudRunCmd.Flags().StringVarP(&cloudMode, "mode", "m", "", "Execution mode")
|
|
cloudRunCmd.Flags().IntVarP(&cloudInstances, "instances", "n", 0, "Number of instances")
|
|
}
|