diff --git a/cmd/api.go b/cmd/api.go index 2c73d82..75b9ef7 100644 --- a/cmd/api.go +++ b/cmd/api.go @@ -10,7 +10,7 @@ import ( "github.com/go-resty/resty/v2" ) -func ProcessAPIType(policy Policy, rgPath string) error { +func ProcessAPIType(policy Policy, rgPath string, isObserve bool) error { client := resty.New() client.SetDebug(debugOutput) @@ -46,9 +46,9 @@ func ProcessAPIType(policy Policy, rgPath string) error { // Process the response based on policy type if policy.Schema.Structure != "" { - return processWithCUE(policy, resp.Body()) + return processWithCUE(policy, resp.Body(), isObserve) } else if len(policy.Regex) > 0 { - return processWithRegex(policy, resp.Body(), rgPath) + return processWithRegex(policy, resp.Body(), rgPath, isObserve) } return handlePolicyError(policy, fmt.Errorf("no processing method specified for policy %s", policy.ID)) @@ -81,7 +81,7 @@ func applyAuth(req *resty.Request, auth map[string]string) error { return nil } -func processWithCUE(policy Policy, data []byte) error { +func processWithCUE(policy Policy, data []byte, isObserve bool) error { valid, issues := validateContentAndCUE(data, policy.Schema.Structure, "json", policy.Schema.Strict, policy.ID) // Generate SARIF report @@ -109,6 +109,25 @@ func processWithCUE(policy Policy, data []byte) error { } + if isObserve { + + // if remote cache the results + if policy.RunID != "" { + var resultMsg string + + if sarifReport.Runs[0].Invocations[0].Properties.ReportCompliant { + + resultMsg = fmt.Sprintf("🟢 %s : %s", "Compliant") + + } else { + resultMsg = fmt.Sprintf("🔴 %s : %s", "Non Compliant") + } + storeResultInCache(policy.ID, resultMsg) + + } + + } + log.Debug().Msgf("Policy %s processed. SARIF report written to: %s ", policy.ID, sarifOutputFile) if !valid { @@ -121,7 +140,7 @@ func processWithCUE(policy Policy, data []byte) error { log.Debug().Msgf("Policy %s validation passed for API response ", policy.ID) return nil } -func processWithRegex(policy Policy, data []byte, rgPath string) error { +func processWithRegex(policy Policy, data []byte, rgPath string, isObserve bool) error { // Create a temporary file with the API response tempFile, err := os.CreateTemp("", "api_response_*.json") if err != nil { @@ -173,6 +192,25 @@ func processWithRegex(policy Policy, data []byte, rgPath string) error { } + if isObserve { + + // if remote cache the results + if policy.RunID != "" { + var resultMsg string + + if sarifReport.Runs[0].Invocations[0].Properties.ReportCompliant { + + resultMsg = fmt.Sprintf("🟢 %s", "Compliant") + + } else { + resultMsg = fmt.Sprintf("🔴 %s", "Non Compliant") + } + storeResultInCache(policy.ID, resultMsg) + + } + + } + log.Debug().Msgf("Policy %s processed. SARIF report written to: %s ", policy.ID, sarifOutputFile) if matchesFound { diff --git a/cmd/audit.go b/cmd/audit.go index f24c294..8e80e35 100644 --- a/cmd/audit.go +++ b/cmd/audit.go @@ -259,7 +259,7 @@ func processPolicyByType(policy Policy, rgPath, gossPath, targetDir string, file case "runtime": err = ProcessRuntimeType(policy, gossPath, targetDir, filePaths, false) case "api": - err = ProcessAPIType(policy, rgPath) + err = ProcessAPIType(policy, rgPath, false) case "yml": if policy.Schema.Patch { err = processGenericType(policy, filePaths, "yaml") diff --git a/cmd/embed_remote_darwin_amd64.go b/cmd/embed_remote_darwin_amd64.go new file mode 100644 index 0000000..bb50458 --- /dev/null +++ b/cmd/embed_remote_darwin_amd64.go @@ -0,0 +1,52 @@ +//go:build darwin && amd64 +// +build darwin,amd64 + +package cmd + +import ( + "embed" + "fmt" + "os" + "path/filepath" +) + +//go:embed gossh/gossh-darwin-amd64 +var embeddedGossh embed.FS + +var gosshPath string + +func prepareGosshExecutable() (string, error) { + tempDir, err := os.MkdirTemp("", "temp_exec") + if err != nil { + return "", fmt.Errorf("failed to create temp dir: %w", err) + } + + gosshPath, err = extractGosshExecutable(tempDir, "gossh/gossh-darwin-amd64") + if err != nil { + return "", err + } + + return gosshPath, nil +} + +func extractGosshExecutable(tempDir, executableName string) (string, error) { + executableFolder := filepath.Dir(executableName) + err := os.MkdirAll(filepath.Join(tempDir, executableFolder), 0755) + if err != nil { + return "", fmt.Errorf("failed to create folder structure: %w", err) + } + + executablePath := filepath.Join(tempDir, executableName) + + data, err := embeddedGossh.ReadFile(executableName) + if err != nil { + return "", fmt.Errorf("failed to read embedded gossh: %w", err) + } + + err = os.WriteFile(executablePath, data, 0755) + if err != nil { + return "", fmt.Errorf("failed to write gossh to temp path: %w", err) + } + + return executablePath, nil +} diff --git a/cmd/embed_remote_darwin_arm64.go b/cmd/embed_remote_darwin_arm64.go new file mode 100644 index 0000000..37ebb2a --- /dev/null +++ b/cmd/embed_remote_darwin_arm64.go @@ -0,0 +1,52 @@ +//go:build darwin && arm64 +// +build darwin,arm64 + +package cmd + +import ( + "embed" + "fmt" + "os" + "path/filepath" +) + +//go:embed gossh/gossh-darwin-arm64 +var embeddedGossh embed.FS + +var gosshPath string + +func prepareGosshExecutable() (string, error) { + tempDir, err := os.MkdirTemp("", "temp_exec") + if err != nil { + return "", fmt.Errorf("failed to create temp dir: %w", err) + } + + gosshPath, err = extractGosshExecutable(tempDir, "gossh/gossh-darwin-arm64") + if err != nil { + return "", err + } + + return gosshPath, nil +} + +func extractGosshExecutable(tempDir, executableName string) (string, error) { + executableFolder := filepath.Dir(executableName) + err := os.MkdirAll(filepath.Join(tempDir, executableFolder), 0755) + if err != nil { + return "", fmt.Errorf("failed to create folder structure: %w", err) + } + + executablePath := filepath.Join(tempDir, executableName) + + data, err := embeddedGossh.ReadFile(executableName) + if err != nil { + return "", fmt.Errorf("failed to read embedded gossh: %w", err) + } + + err = os.WriteFile(executablePath, data, 0755) + if err != nil { + return "", fmt.Errorf("failed to write gossh to temp path: %w", err) + } + + return executablePath, nil +} diff --git a/cmd/embed_remote_linux_amd64.go b/cmd/embed_remote_linux_amd64.go new file mode 100644 index 0000000..84bf1df --- /dev/null +++ b/cmd/embed_remote_linux_amd64.go @@ -0,0 +1,52 @@ +//go:build linux && amd64 +// +build linux,amd64 + +package cmd + +import ( + "embed" + "fmt" + "os" + "path/filepath" +) + +//go:embed gossh/gossh-linux-amd64 +var embeddedGossh embed.FS + +var gosshPath string + +func prepareGosshExecutable() (string, error) { + tempDir, err := os.MkdirTemp("", "temp_exec") + if err != nil { + return "", fmt.Errorf("failed to create temp dir: %w", err) + } + + gosshPath, err = extractGosshExecutable(tempDir, "gossh/gossh-linux-amd64") + if err != nil { + return "", err + } + + return gosshPath, nil +} + +func extractGosshExecutable(tempDir, executableName string) (string, error) { + executableFolder := filepath.Dir(executableName) + err := os.MkdirAll(filepath.Join(tempDir, executableFolder), 0755) + if err != nil { + return "", fmt.Errorf("failed to create folder structure: %w", err) + } + + executablePath := filepath.Join(tempDir, executableName) + + data, err := embeddedGossh.ReadFile(executableName) + if err != nil { + return "", fmt.Errorf("failed to read embedded gossh: %w", err) + } + + err = os.WriteFile(executablePath, data, 0755) + if err != nil { + return "", fmt.Errorf("failed to write gossh to temp path: %w", err) + } + + return executablePath, nil +} diff --git a/cmd/embed_remote_linux_arm64.go b/cmd/embed_remote_linux_arm64.go new file mode 100644 index 0000000..ee6979b --- /dev/null +++ b/cmd/embed_remote_linux_arm64.go @@ -0,0 +1,52 @@ +//go:build linux && arm64 +// +build linux,arm64 + +package cmd + +import ( + "embed" + "fmt" + "os" + "path/filepath" +) + +//go:embed gossh/gossh-linux-arm64 +var embeddedGossh embed.FS + +var gosshPath string + +func prepareGosshExecutable() (string, error) { + tempDir, err := os.MkdirTemp("", "temp_exec") + if err != nil { + return "", fmt.Errorf("failed to create temp dir: %w", err) + } + + gosshPath, err = extractGosshExecutable(tempDir, "gossh/gossh-linux-arm64") + if err != nil { + return "", err + } + + return gosshPath, nil +} + +func extractGosshExecutable(tempDir, executableName string) (string, error) { + executableFolder := filepath.Dir(executableName) + err := os.MkdirAll(filepath.Join(tempDir, executableFolder), 0755) + if err != nil { + return "", fmt.Errorf("failed to create folder structure: %w", err) + } + + executablePath := filepath.Join(tempDir, executableName) + + data, err := embeddedGossh.ReadFile(executableName) + if err != nil { + return "", fmt.Errorf("failed to read embedded gossh: %w", err) + } + + err = os.WriteFile(executablePath, data, 0755) + if err != nil { + return "", fmt.Errorf("failed to write gossh to temp path: %w", err) + } + + return executablePath, nil +} diff --git a/cmd/embed_remote_unavailable.go b/cmd/embed_remote_unavailable.go new file mode 100644 index 0000000..a770cf4 --- /dev/null +++ b/cmd/embed_remote_unavailable.go @@ -0,0 +1,20 @@ +//go:build windows || (linux && arm && !arm64) +// +build windows linux,arm,!arm64 + +package cmd + +import ( + "embed" +) + +var embeddedGossh embed.FS + +var gosshPath string + +func prepareGosshExecutable() (string, error) { + return "", nil +} + +func extractGosshExecutable(_, _ string) (string, error) { + return "", nil +} diff --git a/cmd/gossh/1.15.1 b/cmd/gossh/1.15.1 new file mode 100644 index 0000000..e69de29 diff --git a/cmd/gossh/gossh-darwin-amd64 b/cmd/gossh/gossh-darwin-amd64 new file mode 100644 index 0000000..3c5bdea Binary files /dev/null and b/cmd/gossh/gossh-darwin-amd64 differ diff --git a/cmd/gossh/gossh-darwin-arm64 b/cmd/gossh/gossh-darwin-arm64 new file mode 100755 index 0000000..b5fc756 Binary files /dev/null and b/cmd/gossh/gossh-darwin-arm64 differ diff --git a/cmd/gossh/gossh-linux-amd64 b/cmd/gossh/gossh-linux-amd64 new file mode 100644 index 0000000..27ed35e Binary files /dev/null and b/cmd/gossh/gossh-linux-amd64 differ diff --git a/cmd/gossh/gossh-linux-arm64 b/cmd/gossh/gossh-linux-arm64 new file mode 100644 index 0000000..dc96040 Binary files /dev/null and b/cmd/gossh/gossh-linux-arm64 differ diff --git a/cmd/ssh.go b/cmd/ssh.go index 319aab6..b6a028a 100644 --- a/cmd/ssh.go +++ b/cmd/ssh.go @@ -4,7 +4,9 @@ import ( "errors" "fmt" "net" + "os" "path/filepath" + "runtime" "strings" "time" @@ -12,32 +14,812 @@ import ( "github.com/charmbracelet/ssh" "github.com/charmbracelet/wish" bwish "github.com/charmbracelet/wish/bubbletea" + "github.com/pkg/exec" "github.com/segmentio/ksuid" "github.com/spf13/cobra" ) +// this is for --remote const ( host = "0.0.0.0" port = "23234" ) var remote_users = map[string]string{} - var filteredPolicies []Policy -var remoteCmd = &cobra.Command{ - Use: "remote", - Short: "(not final) Load the Remote Policy Execution", - Long: `(not final) Load the Remote Policy Execution endpoint with interactive interface for policy actions`, - Run: func(cmd *cobra.Command, args []string) { - log.Fatal().Msg("Not yet implemented, use `observe --remote` to start the remote policy execution interface") - }, -} +// var remoteCmd = &cobra.Command{ +// Use: "remote", +// Short: "(not final) Load the Remote Policy Execution", +// Long: `(not final) Load the Remote Policy Execution endpoint with interactive interface for policy actions`, +// Run: func(cmd *cobra.Command, args []string) { +// log.Fatal().Msg("Not yet implemented, use `observe --remote` to start the remote policy execution interface") +// }, +// } + +var ( + remoteFlags struct { + user string + password string + askPass bool + identityFile string + port int + sudo bool + asUser string + concurrency int + destPath string + files []string + execute string + force bool + zip bool + gosshPath string + inventory string + configFile string + passFile string + passphrase string + vaultPassFile string + listHosts bool + proxyServer string + proxyPort int + proxyUser string + proxyPassword string + proxyIdentityFiles string + proxyPassphrase string + commandTimeout int + taskTimeout int + connTimeout int + lang string + commandBlacklist []string + remove bool + tmpDir string + } + + remoteCmd = &cobra.Command{ + Use: "remote", + Short: "Execute remote operations using gossh", + Long: `Execute remote operations like run, push, fetch, and script using embedded gossh`, + } + + runCmd = &cobra.Command{ + Use: "run [HOST...]", + Short: "Execute commands on target hosts", + Long: `Execute commands on target hosts. + +Examples: + Execute command 'uptime' on target hosts: + $ intercept remote run host1 host2 --r-execute "uptime" --r-auth.user zhangsan --r-auth.ask-pass + + Use sudo as root to execute command on target hosts: + $ intercept remote run host[1-2] --r-execute "uptime" --r-auth.user zhangsan --r-run.sudo + + Use sudo as other user 'mysql' to execute command on target hosts: + $ intercept remote run host[1-2] --r-execute "uptime" --r-auth.user zhangsan --r-run.sudo --r-run.as-user mysql`, + RunE: executeRun, + } + + pushCmd = &cobra.Command{ + Use: "push [HOST...]", + Short: "Copy local files and dirs to target hosts", + RunE: executePush, + } + + fetchCmd = &cobra.Command{ + Use: "fetch [HOST...]", + Short: "Copy files from target hosts to local", + RunE: executeFetch, + } + + scriptCmd = &cobra.Command{ + Use: "script [HOST...]", + Short: "Execute a local shell script on target hosts", + RunE: executeScript, + } +) func init() { rootCmd.AddCommand(remoteCmd) + remoteCmd.AddCommand(runCmd, pushCmd, fetchCmd, scriptCmd) + + // Authentication flags + remoteCmd.PersistentFlags().StringVar(&remoteFlags.user, "r-auth.user", os.Getenv("USER"), "login user") + remoteCmd.PersistentFlags().StringVar(&remoteFlags.password, "r-auth.password", "", "password of login user") + remoteCmd.PersistentFlags().BoolVar(&remoteFlags.askPass, "r-auth.ask-pass", false, "ask for password") + remoteCmd.PersistentFlags().StringVar(&remoteFlags.passFile, "r-auth.pass-file", "", "file that holds the password of login user") + remoteCmd.PersistentFlags().StringVar(&remoteFlags.identityFile, "r-auth.identity-files", "", "identity files") + remoteCmd.PersistentFlags().StringVar(&remoteFlags.passphrase, "r-auth.passphrase", "", "passphrase of the identity files") + remoteCmd.PersistentFlags().StringVar(&remoteFlags.vaultPassFile, "r-auth.vault-pass-file", "", "vault password file") + + // Host flags + remoteCmd.PersistentFlags().StringVar(&remoteFlags.inventory, "r-hosts.inventory", "", "file that holds the target hosts") + remoteCmd.PersistentFlags().IntVar(&remoteFlags.port, "r-hosts.port", 22, "port of the target hosts") + remoteCmd.PersistentFlags().BoolVar(&remoteFlags.listHosts, "r-hosts.list", false, "outputs a list of target hosts") + + // Run flags + remoteCmd.PersistentFlags().BoolVar(&remoteFlags.sudo, "r-run.sudo", false, "use sudo") + remoteCmd.PersistentFlags().StringVar(&remoteFlags.asUser, "r-run.as-user", "root", "run as user") + remoteCmd.PersistentFlags().StringVar(&remoteFlags.lang, "r-run.lang", "", "specify i18n while executing command") + remoteCmd.PersistentFlags().IntVar(&remoteFlags.concurrency, "r-run.concurrency", 10, "number of concurrent connections") + remoteCmd.PersistentFlags().StringSliceVar(&remoteFlags.commandBlacklist, "r-run.command-blacklist", []string{}, "commands that are prohibited") + + // Proxy flags + remoteCmd.PersistentFlags().StringVar(&remoteFlags.proxyServer, "r-proxy.server", "", "proxy server address") + remoteCmd.PersistentFlags().IntVar(&remoteFlags.proxyPort, "r-proxy.port", 22, "proxy server port") + remoteCmd.PersistentFlags().StringVar(&remoteFlags.proxyUser, "r-proxy.user", "", "login user for proxy") + remoteCmd.PersistentFlags().StringVar(&remoteFlags.proxyPassword, "r-proxy.password", "", "password for proxy") + remoteCmd.PersistentFlags().StringVar(&remoteFlags.proxyIdentityFiles, "r-proxy.identity-files", "", "identity files for proxy") + remoteCmd.PersistentFlags().StringVar(&remoteFlags.proxyPassphrase, "r-proxy.passphrase", "", "passphrase of the identity files for proxy") + + // Timeout flags + remoteCmd.PersistentFlags().IntVar(&remoteFlags.commandTimeout, "r-timeout.command", 0, "timeout for each command") + remoteCmd.PersistentFlags().IntVar(&remoteFlags.taskTimeout, "r-timeout.task", 0, "timeout for the entire task") + remoteCmd.PersistentFlags().IntVar(&remoteFlags.connTimeout, "r-timeout.conn", 10, "timeout for connecting each host") + + // Config flag + remoteCmd.PersistentFlags().StringVar(&remoteFlags.configFile, "r-config", "", "remote config file") + + // Add command-specific flags for runCmd + runCmd.Flags().StringVar(&remoteFlags.execute, "r-execute", "", "commands to be executed on target hosts") + runCmd.Flags().Bool("r-no-safe-check", false, "ignore dangerous commands (from '--r-run.command-blacklist') check") + // Mark required flags + runCmd.MarkFlagRequired("r-execute") + + // Add command-specific flags for scriptCmd + scriptCmd.Flags().StringVar(&remoteFlags.execute, "r-script", "", "a shell script to be executed on target hosts") + scriptCmd.Flags().StringVar(&remoteFlags.destPath, "r-dest-path", "/tmp", "path of target hosts where the script will be copied to") + scriptCmd.Flags().BoolVar(&remoteFlags.force, "r-force", false, "allow overwrite script file if it already exists on target hosts") + scriptCmd.Flags().BoolVar(&remoteFlags.remove, "r-remove", false, "remove the copied script after execution") + // Mark required flags + scriptCmd.MarkFlagRequired("r-script") + + // Add command-specific flags for pushCmd + pushCmd.Flags().StringSliceVar(&remoteFlags.files, "r-files", []string{}, "local files/dirs to be copied to target hosts") + pushCmd.Flags().StringVar(&remoteFlags.destPath, "r-dest-path", "/tmp", "path of target hosts where files/dirs will be copied to") + pushCmd.Flags().BoolVar(&remoteFlags.force, "r-force", false, "allow overwrite files/dirs if they already exist on target hosts") + pushCmd.Flags().BoolVar(&remoteFlags.zip, "r-zip", false, "enable zip files ('unzip' must be installed on target hosts)") + + // Mark required flags + pushCmd.MarkFlagRequired("r-files") + + // Add command-specific flags for fetchCmd + fetchCmd.Flags().StringSliceVar(&remoteFlags.files, "r-files", []string{}, "files/dirs on target hosts that to be copied") + fetchCmd.Flags().StringVar(&remoteFlags.destPath, "r-dest-path", "", "local directory that files/dirs from target hosts will be copied to") + fetchCmd.Flags().BoolVar(&remoteFlags.zip, "r-zip", false, "enable zip files ('zip' must be installed on target hosts)") + fetchCmd.Flags().StringVar(&remoteFlags.tmpDir, "r-tmp-dir", "$HOME", "directory for storing temporary zip file on target hosts, only useful if the -z flag is used") + + // Mark required flags + fetchCmd.MarkFlagRequired("r-files") + fetchCmd.MarkFlagRequired("r-dest-path") + + if runtime.GOOS == "windows" || (runtime.GOOS == "linux" && runtime.GOARCH == "arm") { + log.Fatal().Msg("INTERCEPT REMOTE currently not supported on your architecture") + } else { + + gosshPath, err := prepareGosshExecutable() + if err != nil { + log.Fatal().Err(err).Msg("Failed to prepare gossh binary") + } + remoteFlags.gosshPath = gosshPath + } +} + +func executeRun(cmd *cobra.Command, args []string) error { + // Add validation for gosshPath + if remoteFlags.gosshPath == "" { + return fmt.Errorf("gossh binary path not set") + } + + // Modified validation: allow either direct hosts or inventory file + if len(args) == 0 && remoteFlags.inventory == "" { + return fmt.Errorf("either hosts or --r-hosts.inventory flag is required") + } + if remoteFlags.execute == "" { + return fmt.Errorf("--r-execute flag is required") + } + + // Debug log the gossh path + log.Debug().Str("gosshPath", remoteFlags.gosshPath).Msg("Using gossh binary") + + // Get the no-safe-check flag + noSafeCheck, _ := cmd.Flags().GetBool("r-no-safe-check") + + // Prepare the gossh command arguments + gosshArgs := []string{"command"} + + // Add hosts from args if provided + if len(args) > 0 { + gosshArgs = append(gosshArgs, args...) + } + + // Add inventory file if provided + if remoteFlags.inventory != "" { + gosshArgs = append(gosshArgs, "--hosts.inventory", remoteFlags.inventory) + } + + // Add config file if provided + if remoteFlags.configFile != "" { + gosshArgs = append(gosshArgs, "--config", remoteFlags.configFile) + } + + // Add the execute command + gosshArgs = append(gosshArgs, "--execute", remoteFlags.execute) + + // Add no-safe-check if enabled + if noSafeCheck { + gosshArgs = append(gosshArgs, "--no-safe-check") + } + + // Add authentication flags + if remoteFlags.user != os.Getenv("USER") { + gosshArgs = append(gosshArgs, "--auth.user", remoteFlags.user) + } + if remoteFlags.password != "" { + gosshArgs = append(gosshArgs, "--auth.password", remoteFlags.password) + } + if remoteFlags.askPass { + gosshArgs = append(gosshArgs, "--auth.ask-pass") + } + if remoteFlags.passFile != "" { + gosshArgs = append(gosshArgs, "--auth.pass-file", remoteFlags.passFile) + } + if remoteFlags.identityFile != "" { + gosshArgs = append(gosshArgs, "--auth.identity-files", remoteFlags.identityFile) + } + if remoteFlags.passphrase != "" { + gosshArgs = append(gosshArgs, "--auth.passphrase", remoteFlags.passphrase) + } + if remoteFlags.vaultPassFile != "" { + gosshArgs = append(gosshArgs, "--auth.vault-pass-file", remoteFlags.vaultPassFile) + } + + // Add host flags + if remoteFlags.port != 22 { + gosshArgs = append(gosshArgs, "--hosts.port", fmt.Sprintf("%d", remoteFlags.port)) + } + if remoteFlags.listHosts { + gosshArgs = append(gosshArgs, "--hosts.list") + } + + // Add run flags + if remoteFlags.sudo { + gosshArgs = append(gosshArgs, "--run.sudo") + } + if remoteFlags.asUser != "root" { + gosshArgs = append(gosshArgs, "--run.as-user", remoteFlags.asUser) + } + if remoteFlags.lang != "" { + gosshArgs = append(gosshArgs, "--run.lang", remoteFlags.lang) + } + if remoteFlags.concurrency != 1 { + gosshArgs = append(gosshArgs, "--run.concurrency", fmt.Sprintf("%d", remoteFlags.concurrency)) + } + if len(remoteFlags.commandBlacklist) > 0 && !noSafeCheck { + gosshArgs = append(gosshArgs, "--run.command-blacklist", strings.Join(remoteFlags.commandBlacklist, ",")) + } + + // Add proxy flags + if remoteFlags.proxyServer != "" { + gosshArgs = append(gosshArgs, "--proxy.server", remoteFlags.proxyServer) + } + if remoteFlags.proxyPort != 22 { + gosshArgs = append(gosshArgs, "--proxy.port", fmt.Sprintf("%d", remoteFlags.proxyPort)) + } + if remoteFlags.proxyUser != "" { + gosshArgs = append(gosshArgs, "--proxy.user", remoteFlags.proxyUser) + } + if remoteFlags.proxyPassword != "" { + gosshArgs = append(gosshArgs, "--proxy.password", remoteFlags.proxyPassword) + } + if remoteFlags.proxyIdentityFiles != "" { + gosshArgs = append(gosshArgs, "--proxy.identity-files", remoteFlags.proxyIdentityFiles) + } + if remoteFlags.proxyPassphrase != "" { + gosshArgs = append(gosshArgs, "--proxy.passphrase", remoteFlags.proxyPassphrase) + } + + // Add timeout flags + if remoteFlags.commandTimeout != 0 { + gosshArgs = append(gosshArgs, "--timeout.command", fmt.Sprintf("%d", remoteFlags.commandTimeout)) + } + if remoteFlags.taskTimeout != 0 { + gosshArgs = append(gosshArgs, "--timeout.task", fmt.Sprintf("%d", remoteFlags.taskTimeout)) + } + if remoteFlags.connTimeout != 10 { + gosshArgs = append(gosshArgs, "--timeout.conn", fmt.Sprintf("%d", remoteFlags.connTimeout)) + } + + //append output type + if verbosity < 2 { + gosshArgs = append(gosshArgs, "--output.json") + } + // Execute gossh command + execCmd := exec.Command(remoteFlags.gosshPath, gosshArgs...) + + // Connect stdout and stderr + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + + // Run the command + log.Debug(). + Str("command", remoteFlags.gosshPath). + Strs("args", gosshArgs). + Msg("Executing remote command") + + err := execCmd.Run() + if err != nil { + return fmt.Errorf("failed to execute remote command: %w", err) + } + + return nil +} + +func executePush(cmd *cobra.Command, args []string) error { + // Add validation for gosshPath + if remoteFlags.gosshPath == "" { + return fmt.Errorf("gossh binary path not set") + } + + // Modified validation: allow either direct hosts or inventory file + if len(args) == 0 && remoteFlags.inventory == "" { + return fmt.Errorf("either hosts or --r-hosts.inventory flag is required") + } + if remoteFlags.execute == "" { + return fmt.Errorf("--r-execute flag is required") + } + + // Debug log the gossh path + log.Debug().Str("gosshPath", remoteFlags.gosshPath).Msg("Using gossh binary") + + // Prepare the gossh command arguments + gosshArgs := []string{"push"} + + // Add hosts from args if provided + if len(args) > 0 { + gosshArgs = append(gosshArgs, args...) + } + + // Add inventory file if provided + if remoteFlags.inventory != "" { + gosshArgs = append(gosshArgs, "--hosts.inventory", remoteFlags.inventory) + } + + // Add files to be copied + gosshArgs = append(gosshArgs, "--files", strings.Join(remoteFlags.files, ",")) + + // Add destination path + gosshArgs = append(gosshArgs, "--dest-path", remoteFlags.destPath) + + // Add force flag if enabled + if remoteFlags.force { + gosshArgs = append(gosshArgs, "--force") + } + + // Add zip flag if enabled + if remoteFlags.zip { + gosshArgs = append(gosshArgs, "--zip") + } + + // Add authentication flags + if remoteFlags.user != os.Getenv("USER") { + gosshArgs = append(gosshArgs, "--auth.user", remoteFlags.user) + } + if remoteFlags.password != "" { + gosshArgs = append(gosshArgs, "--auth.password", remoteFlags.password) + } + if remoteFlags.askPass { + gosshArgs = append(gosshArgs, "--auth.ask-pass") + } + if remoteFlags.passFile != "" { + gosshArgs = append(gosshArgs, "--auth.pass-file", remoteFlags.passFile) + } + if remoteFlags.identityFile != "" { + gosshArgs = append(gosshArgs, "--auth.identity-files", remoteFlags.identityFile) + } + if remoteFlags.passphrase != "" { + gosshArgs = append(gosshArgs, "--auth.passphrase", remoteFlags.passphrase) + } + if remoteFlags.vaultPassFile != "" { + gosshArgs = append(gosshArgs, "--auth.vault-pass-file", remoteFlags.vaultPassFile) + } + + // Add host flags + if remoteFlags.port != 22 { + gosshArgs = append(gosshArgs, "--hosts.port", fmt.Sprintf("%d", remoteFlags.port)) + } + if remoteFlags.listHosts { + gosshArgs = append(gosshArgs, "--hosts.list") + } + + // Add run flags + if remoteFlags.sudo { + gosshArgs = append(gosshArgs, "--run.sudo") + } + if remoteFlags.asUser != "root" { + gosshArgs = append(gosshArgs, "--run.as-user", remoteFlags.asUser) + } + if remoteFlags.lang != "" { + gosshArgs = append(gosshArgs, "--run.lang", remoteFlags.lang) + } + if remoteFlags.concurrency != 1 { + gosshArgs = append(gosshArgs, "--run.concurrency", fmt.Sprintf("%d", remoteFlags.concurrency)) + } + if len(remoteFlags.commandBlacklist) > 0 { + gosshArgs = append(gosshArgs, "--run.command-blacklist", strings.Join(remoteFlags.commandBlacklist, ",")) + } + + // Add proxy flags + if remoteFlags.proxyServer != "" { + gosshArgs = append(gosshArgs, "--proxy.server", remoteFlags.proxyServer) + } + if remoteFlags.proxyPort != 22 { + gosshArgs = append(gosshArgs, "--proxy.port", fmt.Sprintf("%d", remoteFlags.proxyPort)) + } + if remoteFlags.proxyUser != "" { + gosshArgs = append(gosshArgs, "--proxy.user", remoteFlags.proxyUser) + } + if remoteFlags.proxyPassword != "" { + gosshArgs = append(gosshArgs, "--proxy.password", remoteFlags.proxyPassword) + } + if remoteFlags.proxyIdentityFiles != "" { + gosshArgs = append(gosshArgs, "--proxy.identity-files", remoteFlags.proxyIdentityFiles) + } + if remoteFlags.proxyPassphrase != "" { + gosshArgs = append(gosshArgs, "--proxy.passphrase", remoteFlags.proxyPassphrase) + } + + // Add timeout flags + if remoteFlags.commandTimeout != 0 { + gosshArgs = append(gosshArgs, "--timeout.command", fmt.Sprintf("%d", remoteFlags.commandTimeout)) + } + if remoteFlags.taskTimeout != 0 { + gosshArgs = append(gosshArgs, "--timeout.task", fmt.Sprintf("%d", remoteFlags.taskTimeout)) + } + if remoteFlags.connTimeout != 10 { + gosshArgs = append(gosshArgs, "--timeout.conn", fmt.Sprintf("%d", remoteFlags.connTimeout)) + } + + //append output type + if verbosity < 2 { + gosshArgs = append(gosshArgs, "--output.json") + } + + // Execute gossh command + execCmd := exec.Command(remoteFlags.gosshPath, gosshArgs...) + + // Connect stdout and stderr + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + + // Run the command + log.Debug(). + Str("command", remoteFlags.gosshPath). + Strs("args", gosshArgs). + Msg("Executing push command") + + err := execCmd.Run() + if err != nil { + return fmt.Errorf("failed to execute push command: %w", err) + } + + return nil } +func executeFetch(cmd *cobra.Command, args []string) error { + // Add validation for gosshPath + if remoteFlags.gosshPath == "" { + return fmt.Errorf("gossh binary path not set") + } + + // Modified validation: allow either direct hosts or inventory file + if len(args) == 0 && remoteFlags.inventory == "" { + return fmt.Errorf("either hosts or --r-hosts.inventory flag is required") + } + if len(remoteFlags.files) == 0 { + return fmt.Errorf("--r-files flag is required") + } + if remoteFlags.destPath == "" { + return fmt.Errorf("--r-dest-path flag is required") + } + + // Debug log the gossh path + log.Debug().Str("gosshPath", remoteFlags.gosshPath).Msg("Using gossh binary") + + // Prepare the gossh command arguments + gosshArgs := []string{"fetch"} + + // Add hosts from args if provided + if len(args) > 0 { + gosshArgs = append(gosshArgs, args...) + } + + // Add inventory file if provided + if remoteFlags.inventory != "" { + gosshArgs = append(gosshArgs, "--hosts.inventory", remoteFlags.inventory) + } + + // Add files to be copied + gosshArgs = append(gosshArgs, "--files", strings.Join(remoteFlags.files, ",")) + + // Add destination path + gosshArgs = append(gosshArgs, "--dest-path", remoteFlags.destPath) + + // Add zip flag if enabled + if remoteFlags.zip { + gosshArgs = append(gosshArgs, "--zip") + } + + // Add temporary directory for zip files if zip is enabled + if remoteFlags.zip && remoteFlags.tmpDir != "" { + gosshArgs = append(gosshArgs, "--tmp-dir", remoteFlags.tmpDir) + } + + // Add authentication flags + if remoteFlags.user != os.Getenv("USER") { + gosshArgs = append(gosshArgs, "--auth.user", remoteFlags.user) + } + if remoteFlags.password != "" { + gosshArgs = append(gosshArgs, "--auth.password", remoteFlags.password) + } + if remoteFlags.askPass { + gosshArgs = append(gosshArgs, "--auth.ask-pass") + } + if remoteFlags.passFile != "" { + gosshArgs = append(gosshArgs, "--auth.pass-file", remoteFlags.passFile) + } + if remoteFlags.identityFile != "" { + gosshArgs = append(gosshArgs, "--auth.identity-files", remoteFlags.identityFile) + } + if remoteFlags.passphrase != "" { + gosshArgs = append(gosshArgs, "--auth.passphrase", remoteFlags.passphrase) + } + if remoteFlags.vaultPassFile != "" { + gosshArgs = append(gosshArgs, "--auth.vault-pass-file", remoteFlags.vaultPassFile) + } + + // Add host flags + if remoteFlags.port != 22 { + gosshArgs = append(gosshArgs, "--hosts.port", fmt.Sprintf("%d", remoteFlags.port)) + } + if remoteFlags.listHosts { + gosshArgs = append(gosshArgs, "--hosts.list") + } + + // Add run flags + if remoteFlags.sudo { + gosshArgs = append(gosshArgs, "--run.sudo") + } + if remoteFlags.asUser != "root" { + gosshArgs = append(gosshArgs, "--run.as-user", remoteFlags.asUser) + } + if remoteFlags.lang != "" { + gosshArgs = append(gosshArgs, "--run.lang", remoteFlags.lang) + } + if remoteFlags.concurrency != 1 { + gosshArgs = append(gosshArgs, "--run.concurrency", fmt.Sprintf("%d", remoteFlags.concurrency)) + } + if len(remoteFlags.commandBlacklist) > 0 { + gosshArgs = append(gosshArgs, "--run.command-blacklist", strings.Join(remoteFlags.commandBlacklist, ",")) + } + + // Add proxy flags + if remoteFlags.proxyServer != "" { + gosshArgs = append(gosshArgs, "--proxy.server", remoteFlags.proxyServer) + } + if remoteFlags.proxyPort != 22 { + gosshArgs = append(gosshArgs, "--proxy.port", fmt.Sprintf("%d", remoteFlags.proxyPort)) + } + if remoteFlags.proxyUser != "" { + gosshArgs = append(gosshArgs, "--proxy.user", remoteFlags.proxyUser) + } + if remoteFlags.proxyPassword != "" { + gosshArgs = append(gosshArgs, "--proxy.password", remoteFlags.proxyPassword) + } + if remoteFlags.proxyIdentityFiles != "" { + gosshArgs = append(gosshArgs, "--proxy.identity-files", remoteFlags.proxyIdentityFiles) + } + if remoteFlags.proxyPassphrase != "" { + gosshArgs = append(gosshArgs, "--proxy.passphrase", remoteFlags.proxyPassphrase) + } + + // Add timeout flags + if remoteFlags.commandTimeout != 0 { + gosshArgs = append(gosshArgs, "--timeout.command", fmt.Sprintf("%d", remoteFlags.commandTimeout)) + } + if remoteFlags.taskTimeout != 0 { + gosshArgs = append(gosshArgs, "--timeout.task", fmt.Sprintf("%d", remoteFlags.taskTimeout)) + } + if remoteFlags.connTimeout != 10 { + gosshArgs = append(gosshArgs, "--timeout.conn", fmt.Sprintf("%d", remoteFlags.connTimeout)) + } + + //append output type + if verbosity < 2 { + gosshArgs = append(gosshArgs, "--output.json") + } + + // Execute gossh command + execCmd := exec.Command(remoteFlags.gosshPath, gosshArgs...) + + // Connect stdout and stderr + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + + // Run the command + log.Debug(). + Str("command", remoteFlags.gosshPath). + Strs("args", gosshArgs). + Msg("Executing fetch command") + + err := execCmd.Run() + if err != nil { + return fmt.Errorf("failed to execute fetch command: %w", err) + } + + return nil +} + +func executeScript(cmd *cobra.Command, args []string) error { + // Add validation for gosshPath + if remoteFlags.gosshPath == "" { + return fmt.Errorf("gossh binary path not set") + } + + // Modified validation: allow either direct hosts or inventory file + if len(args) == 0 && remoteFlags.inventory == "" { + return fmt.Errorf("either hosts or --r-hosts.inventory flag is required") + } + if remoteFlags.execute == "" { + return fmt.Errorf("--r-execute flag is required") + } + + // Debug log the gossh path + log.Debug().Str("gosshPath", remoteFlags.gosshPath).Msg("Using gossh binary") + + // Get the no-safe-check flag + noSafeCheck, _ := cmd.Flags().GetBool("r-no-safe-check") + + // Prepare the gossh command arguments + gosshArgs := []string{"script"} + + // Add hosts from args if provided + if len(args) > 0 { + gosshArgs = append(gosshArgs, args...) + } + + // Add inventory file if provided + if remoteFlags.inventory != "" { + gosshArgs = append(gosshArgs, "--hosts.inventory", remoteFlags.inventory) + } + + // Add the execute command + gosshArgs = append(gosshArgs, "--execute", remoteFlags.execute) + + // Add destination path + gosshArgs = append(gosshArgs, "--dest-path", remoteFlags.destPath) + + // Add force flag if enabled + if remoteFlags.force { + gosshArgs = append(gosshArgs, "--force") + } + + // Add remove flag if enabled + if remoteFlags.remove { + gosshArgs = append(gosshArgs, "--remove") + } + // Add no-safe-check if enabled + if noSafeCheck { + gosshArgs = append(gosshArgs, "--no-safe-check") + } + + // Add authentication flags + if remoteFlags.user != os.Getenv("USER") { + gosshArgs = append(gosshArgs, "--auth.user", remoteFlags.user) + } + if remoteFlags.password != "" { + gosshArgs = append(gosshArgs, "--auth.password", remoteFlags.password) + } + if remoteFlags.askPass { + gosshArgs = append(gosshArgs, "--auth.ask-pass") + } + if remoteFlags.passFile != "" { + gosshArgs = append(gosshArgs, "--auth.pass-file", remoteFlags.passFile) + } + if remoteFlags.identityFile != "" { + gosshArgs = append(gosshArgs, "--auth.identity-files", remoteFlags.identityFile) + } + if remoteFlags.passphrase != "" { + gosshArgs = append(gosshArgs, "--auth.passphrase", remoteFlags.passphrase) + } + if remoteFlags.vaultPassFile != "" { + gosshArgs = append(gosshArgs, "--auth.vault-pass-file", remoteFlags.vaultPassFile) + } + + // Add host flags + if remoteFlags.port != 22 { + gosshArgs = append(gosshArgs, "--hosts.port", fmt.Sprintf("%d", remoteFlags.port)) + } + if remoteFlags.listHosts { + gosshArgs = append(gosshArgs, "--hosts.list") + } + + // Add run flags + if remoteFlags.sudo { + gosshArgs = append(gosshArgs, "--run.sudo") + } + if remoteFlags.asUser != "root" { + gosshArgs = append(gosshArgs, "--run.as-user", remoteFlags.asUser) + } + if remoteFlags.lang != "" { + gosshArgs = append(gosshArgs, "--run.lang", remoteFlags.lang) + } + if remoteFlags.concurrency != 1 { + gosshArgs = append(gosshArgs, "--run.concurrency", fmt.Sprintf("%d", remoteFlags.concurrency)) + } + if len(remoteFlags.commandBlacklist) > 0 && !noSafeCheck { + gosshArgs = append(gosshArgs, "--run.command-blacklist", strings.Join(remoteFlags.commandBlacklist, ",")) + } + + // Add proxy flags + if remoteFlags.proxyServer != "" { + gosshArgs = append(gosshArgs, "--proxy.server", remoteFlags.proxyServer) + } + if remoteFlags.proxyPort != 22 { + gosshArgs = append(gosshArgs, "--proxy.port", fmt.Sprintf("%d", remoteFlags.proxyPort)) + } + if remoteFlags.proxyUser != "" { + gosshArgs = append(gosshArgs, "--proxy.user", remoteFlags.proxyUser) + } + if remoteFlags.proxyPassword != "" { + gosshArgs = append(gosshArgs, "--proxy.password", remoteFlags.proxyPassword) + } + if remoteFlags.proxyIdentityFiles != "" { + gosshArgs = append(gosshArgs, "--proxy.identity-files", remoteFlags.proxyIdentityFiles) + } + if remoteFlags.proxyPassphrase != "" { + gosshArgs = append(gosshArgs, "--proxy.passphrase", remoteFlags.proxyPassphrase) + } + + // Add timeout flags + if remoteFlags.commandTimeout != 0 { + gosshArgs = append(gosshArgs, "--timeout.command", fmt.Sprintf("%d", remoteFlags.commandTimeout)) + } + if remoteFlags.taskTimeout != 0 { + gosshArgs = append(gosshArgs, "--timeout.task", fmt.Sprintf("%d", remoteFlags.taskTimeout)) + } + if remoteFlags.connTimeout != 10 { + gosshArgs = append(gosshArgs, "--timeout.conn", fmt.Sprintf("%d", remoteFlags.connTimeout)) + } + + //append output type + if verbosity < 2 { + gosshArgs = append(gosshArgs, "--output.json") + } + + // Execute gossh command + execCmd := exec.Command(remoteFlags.gosshPath, gosshArgs...) + + // Connect stdout and stderr + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + + // Run the command + log.Debug(). + Str("command", remoteFlags.gosshPath). + Strs("args", gosshArgs). + Msg("Executing script command") + + err := execCmd.Run() + if err != nil { + return fmt.Errorf("failed to execute script command: %w", err) + } + + return nil +} + +// ---------------------------------------------------------------------------------- +// Remote Policy Execution +// ---------------------------------------------------------------------------------- +// this is for --remote + func authenticatedBubbleteaMiddleware() wish.Middleware { return func(next ssh.Handler) ssh.Handler { return func(s ssh.Session) { @@ -190,7 +972,7 @@ func startSSHServer(policies []Policy, outputDir string) error { // Filter policies to include only those with Type == "runtime" var runtimePolicies []Policy for _, policy := range policies { - if policy.Type == "runtime" { + if policy.Type == "runtime" || policy.Type == "api" { runtimePolicies = append(runtimePolicies, policy) } } diff --git a/cmd/worker.go b/cmd/worker.go index 1683578..a153e4b 100644 --- a/cmd/worker.go +++ b/cmd/worker.go @@ -42,7 +42,7 @@ func processPolicyInWorker(e event.Event, policyType string) error { case "runtime": return ProcessRuntimeType(policy, gossPath, targetDir, filePaths, true) case "api": - return ProcessAPIType(policy, rgPath) + return ProcessAPIType(policy, rgPath, true) case "yml": if policy.Schema.Patch { return processGenericType(policy, filePaths, "yaml") diff --git a/go.mod b/go.mod index 07270ab..fd865df 100644 --- a/go.mod +++ b/go.mod @@ -88,6 +88,7 @@ require ( github.com/natefinch/lumberjack/v3 v3.0.0-alpha github.com/open-policy-agent/opa v0.68.0 github.com/pelletier/go-toml/v2 v2.2.3 + github.com/pkg/exec v0.0.0-20150614095509-0bd164ad2a5a github.com/spf13/pflag v1.0.5 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/sys v0.24.0 // indirect diff --git a/go.sum b/go.sum index d2afecf..d14d5e2 100644 --- a/go.sum +++ b/go.sum @@ -175,6 +175,8 @@ github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNH github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/exec v0.0.0-20150614095509-0bd164ad2a5a h1:EN123kAtAAE2pg/+TvBsUBZfHCWNNFyL2ZBPPfNWAc0= +github.com/pkg/exec v0.0.0-20150614095509-0bd164ad2a5a/go.mod h1:b95YoNrAnScjaWG+asr8lxqlrsPUcT2ZEBcjvVGshMo= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/playground/policies/test_remote.yml b/playground/policies/test_remote.yml index a27ac86..e36f98f 100644 --- a/playground/policies/test_remote.yml +++ b/playground/policies/test_remote.yml @@ -5,6 +5,7 @@ Config: remote_auth: - "UserA:ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICtFRLdSvayFQwQdIOk6NKuEpEK7KvYBQz8LUVerSo8T" - "UserB:ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJyubt40tutUSi3FQqcEzbDUu14RdLstEbURvX/M2bM/" + - "Test:ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDyE3uP86ZKN+rDOOrv9MJSZfKDKjDLL6KY+JrT6iwlk" Policies: @@ -67,4 +68,38 @@ Policies: - "enforce" score: "8" _runtime: - config: runtime/simple_https.yaml \ No newline at end of file + config: runtime/simple_https.yaml + + + - id: "API-001" + type: "api" + enforcement: + - environment: "production" + fatal: "true" + exceptions: "false" + confidence: "high" + - environment: "development" + fatal: "true" + exceptions: "false" + confidence: "high" + metadata: + name: "API Regex Compliance" + description: "Enforce schema compliance on API configuration files" + msg_solution: "Generic solution message to development issue." + msg_error: "Generic error message for development issue" + tags: + - "config" + - "ini" + - "schema" + confidence: "high" + score: "8" + _api: + endpoint: "https://httpbin.org/user-agent" + insecure: false + request: "GET" + response_type: "application/json" + auth: + type: bearer + token_env: TOKEN + _regex: + - \s*user-agent\s* \ No newline at end of file