From b80c8b6ecddfa06e51ae898a4c35ebb3770b1331 Mon Sep 17 00:00:00 2001 From: j3ssie Date: Sun, 1 Feb 2026 20:21:05 +0700 Subject: [PATCH] feat: add SSH test infrastructure, improve executor temp handling, enhance CLI workflow install Amp-Thread-ID: https://ampcode.com/threads/T-019c195d-0f3b-724a-946a-a3a93dd7d09b Co-authored-by: Amp --- build/docker/ssh-test/Dockerfile | 38 ++++ build/docker/ssh-test/README.md | 33 ++++ build/docker/ssh-test/docker-compose.yaml | 14 ++ build/docker/ssh-test/id_ed25519 | 7 + build/docker/ssh-test/id_ed25519.pub | 1 + internal/executor/executor.go | 169 +++++++++++++++++- internal/terminal/printer.go | 18 +- internal/terminal/symbols.go | 7 +- pkg/cli/function.go | 8 + pkg/cli/install.go | 53 +++++- pkg/cli/run.go | 30 +++- pkg/cli/workflow.go | 19 ++ .../registry-metadata-direct-fetch.json | 74 +++++--- 13 files changed, 433 insertions(+), 38 deletions(-) create mode 100644 build/docker/ssh-test/Dockerfile create mode 100644 build/docker/ssh-test/README.md create mode 100644 build/docker/ssh-test/docker-compose.yaml create mode 100644 build/docker/ssh-test/id_ed25519 create mode 100644 build/docker/ssh-test/id_ed25519.pub diff --git a/build/docker/ssh-test/Dockerfile b/build/docker/ssh-test/Dockerfile new file mode 100644 index 0000000..95ddd50 --- /dev/null +++ b/build/docker/ssh-test/Dockerfile @@ -0,0 +1,38 @@ +FROM ubuntu:22.04 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y \ + openssh-server \ + sudo \ + curl \ + wget \ + vim \ + && rm -rf /var/lib/apt/lists/* \ + && mkdir /var/run/sshd + +# Create test user with sudo access +RUN useradd -m -s /bin/bash testuser \ + && echo "testuser:testpass" | chpasswd \ + && usermod -aG sudo testuser \ + && echo "testuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers + +# Setup SSH directory for testuser +RUN mkdir -p /home/testuser/.ssh \ + && chmod 700 /home/testuser/.ssh + +# Copy authorized keys +COPY id_ed25519.pub /home/testuser/.ssh/authorized_keys + +# Set proper permissions +RUN chmod 600 /home/testuser/.ssh/authorized_keys \ + && chown -R testuser:testuser /home/testuser/.ssh + +# Configure SSH +RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin no/' /etc/ssh/sshd_config \ + && sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config \ + && sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config + +EXPOSE 22 + +CMD ["/usr/sbin/sshd", "-D"] diff --git a/build/docker/ssh-test/README.md b/build/docker/ssh-test/README.md new file mode 100644 index 0000000..8421013 --- /dev/null +++ b/build/docker/ssh-test/README.md @@ -0,0 +1,33 @@ +# SSH Test Container + +Ubuntu 22.04 container with SSH server for testing osmedeus SSH runner. + +## Quick Start + +```bash +# Build and start +docker-compose up -d --build + +# Test SSH connection +ssh -i id_ed25519 -p 2222 testuser@localhost + +# Stop +docker-compose down +``` + +## Credentials + +- **User**: `testuser` +- **Password**: `testpass` +- **SSH Key**: `id_ed25519` (in this directory) +- **Port**: `2222` + +## SSH Command Examples + +```bash +# Using key authentication +ssh -i id_ed25519 -p 2222 -o StrictHostKeyChecking=no testuser@localhost + +# Run command +ssh -i id_ed25519 -p 2222 -o StrictHostKeyChecking=no testuser@localhost "whoami" +``` diff --git a/build/docker/ssh-test/docker-compose.yaml b/build/docker/ssh-test/docker-compose.yaml new file mode 100644 index 0000000..4edb3eb --- /dev/null +++ b/build/docker/ssh-test/docker-compose.yaml @@ -0,0 +1,14 @@ +services: + ssh-test: + build: + context: . + dockerfile: Dockerfile + container_name: osmedeus-ssh-test + ports: + - "2222:22" + restart: unless-stopped + healthcheck: + test: ["CMD", "pgrep", "sshd"] + interval: 10s + timeout: 5s + retries: 3 diff --git a/build/docker/ssh-test/id_ed25519 b/build/docker/ssh-test/id_ed25519 new file mode 100644 index 0000000..b578874 --- /dev/null +++ b/build/docker/ssh-test/id_ed25519 @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACBFkHTIis1TLr0yQE9H+I+Oj5rkuRLmlbTLGTyYFztFogAAAKAlkswGJZLM +BgAAAAtzc2gtZWQyNTUxOQAAACBFkHTIis1TLr0yQE9H+I+Oj5rkuRLmlbTLGTyYFztFog +AAAECNG9yAIRlP1a223qsQFSig797wA4lcBJzcnjNlQCaWhkWQdMiKzVMuvTJAT0f4j46P +muS5EuaVtMsZPJgXO0WiAAAAF29zbWVkZXVzLXRlc3RAbG9jYWxob3N0AQIDBAUG +-----END OPENSSH PRIVATE KEY----- diff --git a/build/docker/ssh-test/id_ed25519.pub b/build/docker/ssh-test/id_ed25519.pub new file mode 100644 index 0000000..4c794b8 --- /dev/null +++ b/build/docker/ssh-test/id_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEWQdMiKzVMuvTJAT0f4j46PmuS5EuaVtMsZPJgXO0Wi osmedeus-test@localhost diff --git a/internal/executor/executor.go b/internal/executor/executor.go index f613427..9087e22 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -94,6 +94,14 @@ func BuildBuiltinVariables(cfg *config.Config, params map[string]string) map[str runUUID := uuid.New().String() execCtx := core.NewExecutionContext("func-eval", core.KindModule, runUUID, params["target"]) exec.injectBuiltinVariables(cfg, params, execCtx) + + // Add temp directory for eval (cleanup is handled by the caller) + tempDir, err := os.MkdirTemp("", "osm-tmp-") + if err == nil { + execCtx.SetVariable("TempDir", tempDir) + execCtx.SetVariable("TempFile", filepath.Join(tempDir, "osm-tmp-file")) + } + return execCtx.GetVariables() } @@ -170,13 +178,22 @@ func (e *Executor) writeVerboseOutputToLog(output string) { if e.consoleCapture == nil || output == "" { return } + // Trim whitespace from output + output = strings.TrimSpace(output) + if output == "" { + return // Don't print [output] if nothing to show + } // Format similar to printer.VerboseOutput but write directly to file var sb strings.Builder sb.WriteString(" ") sb.WriteString(terminal.Gray("[output]")) sb.WriteString("\n") - lines := strings.Split(strings.TrimSuffix(output, "\n"), "\n") + lines := strings.Split(output, "\n") for _, line := range lines { + // Skip blank lines + if strings.TrimSpace(line) == "" { + continue + } sb.WriteString(" ") sb.WriteString(line) sb.WriteString("\n") @@ -372,6 +389,30 @@ func (e *Executor) injectBuiltinVariables(cfg *config.Config, params map[string] } } +// setupTempDirectory creates a temporary directory and file for workflow execution +// Returns a cleanup function that removes the directory when called +func (e *Executor) setupTempDirectory(execCtx *core.ExecutionContext) (cleanup func()) { + tempDir, err := os.MkdirTemp("", "osm-tmp-") + if err != nil { + e.logger.Warn("Failed to create temp directory", zap.Error(err)) + return func() {} // no-op cleanup + } + + execCtx.SetVariable("TempDir", tempDir) + + // Create temp file path within temp dir + tempFile := filepath.Join(tempDir, "osm-tmp-file") + execCtx.SetVariable("TempFile", tempFile) + + return func() { + if err := os.RemoveAll(tempDir); err != nil { + e.logger.Warn("Failed to cleanup temp directory", + zap.String("path", tempDir), + zap.Error(err)) + } + } +} + func (e *Executor) debugLogTargetVariables(execCtx *core.ExecutionContext) { if execCtx == nil || execCtx.Logger == nil { return @@ -578,6 +619,122 @@ func printDryRunHeader(workflowName, workflowKind, target, tactic string, stepCo // getStepCommand extracts the command/script from a step for display func getStepCommand(step *core.Step) string { + // Handle foreach steps specially + if step.Type == core.StepTypeForeach && step.Input != "" { + parts := []string{fmt.Sprintf("foreach [[%s]] in %s", step.Variable, step.Input)} + if step.VariablePreProcess != "" { + parts = append(parts, fmt.Sprintf("pre_process: %s", step.VariablePreProcess)) + } + // Add inner step name, type, and command + if step.Step != nil && step.Step.Name != "" { + innerCmd := getInnerStepCommand(step.Step) + if innerCmd != "" { + parts = append(parts, fmt.Sprintf("step: %s (%s) cmd: %s", step.Step.Name, step.Step.Type, innerCmd)) + } else { + parts = append(parts, fmt.Sprintf("step: %s (%s)", step.Step.Name, step.Step.Type)) + } + } + threads, _ := step.Threads.Int() + if threads <= 0 { + threads = 1 + } + parts = append(parts, fmt.Sprintf("threads: %d", threads)) + return strings.Join(parts, " | ") + } + + if step.Command != "" { + return step.Command + } + if len(step.Commands) > 0 { + return step.Commands[0] + } + if step.Function != "" { + return step.Function + } + if len(step.Functions) > 0 { + return step.Functions[0] + } + return "" +} + +// getStepCommandColored returns a colored command string for foreach steps (for console display) +func getStepCommandColored(step *core.Step) string { + // Handle foreach steps specially with colors + if step.Type == core.StepTypeForeach && step.Input != "" { + var lines []string + + // First line: foreach [[variable]] in source | threads: N + var firstLineParts []string + firstLineParts = append(firstLineParts, fmt.Sprintf("foreach %s in %s", + terminal.Magenta("[["+step.Variable+"]]"), + terminal.Cyan(step.Input))) + + if step.VariablePreProcess != "" { + firstLineParts = append(firstLineParts, fmt.Sprintf("pre_process: %s", terminal.Gray(step.VariablePreProcess))) + } + + threads, _ := step.Threads.Int() + if threads <= 0 { + threads = 1 + } + firstLineParts = append(firstLineParts, fmt.Sprintf("threads: %s", terminal.Yellow(fmt.Sprintf("%d", threads)))) + lines = append(lines, strings.Join(firstLineParts, terminal.Gray(" | "))) + + // Second line: step: name (type) + if step.Step != nil && step.Step.Name != "" { + lines = append(lines, fmt.Sprintf(" %s %s %s", + terminal.Gray("step:"), + terminal.HiBlue(step.Step.Name), + terminal.Gray("("+string(step.Step.Type)+")"))) + + // Add command/commands/function/functions on separate lines + innerLines := getInnerStepCommandLines(step.Step) + lines = append(lines, innerLines...) + } + + return strings.Join(lines, "\n") + } + + // For non-foreach steps, return uncolored (will be colored by printer) + return getStepCommand(step) +} + +// getInnerStepCommandLines returns colored lines for inner step commands/functions +func getInnerStepCommandLines(step *core.Step) []string { + var lines []string + + if step.Command != "" { + lines = append(lines, fmt.Sprintf(" %s %s", + terminal.Gray("command:"), + terminal.HiGreen(step.Command))) + } + if len(step.Commands) > 0 { + lines = append(lines, fmt.Sprintf(" %s", terminal.Gray("commands:"))) + for _, cmd := range step.Commands { + lines = append(lines, fmt.Sprintf(" %s %s", + terminal.Gray("-"), + terminal.HiGreen(cmd))) + } + } + if step.Function != "" { + lines = append(lines, fmt.Sprintf(" %s %s", + terminal.Gray("function:"), + terminal.HiCyan(step.Function))) + } + if len(step.Functions) > 0 { + lines = append(lines, fmt.Sprintf(" %s", terminal.Gray("functions:"))) + for _, fn := range step.Functions { + lines = append(lines, fmt.Sprintf(" %s %s", + terminal.Gray("-"), + terminal.HiCyan(fn))) + } + } + + return lines +} + +// getInnerStepCommand extracts the command/function from an inner step (for logs) +func getInnerStepCommand(step *core.Step) string { if step.Command != "" { return step.Command } @@ -823,6 +980,8 @@ func (e *Executor) ExecuteModule(ctx context.Context, module *core.Workflow, par zap.String("tactic", params["tactic"]), ) e.injectBuiltinVariables(cfg, params, execCtx) + tempCleanup := e.setupTempDirectory(execCtx) + defer tempCleanup() e.debugLogTargetVariables(execCtx) if !e.dryRun && database.GetDB() != nil { @@ -1438,6 +1597,8 @@ func (e *Executor) ExecuteFlow(ctx context.Context, flow *core.Workflow, params // Inject builtin variables e.logger.Debug("Injecting builtin variables for flow") e.injectBuiltinVariables(cfg, params, execCtx) + tempCleanup := e.setupTempDirectory(execCtx) + defer tempCleanup() e.debugLogTargetVariables(execCtx) if !e.dryRun && database.GetDB() != nil { @@ -2025,15 +2186,17 @@ func (e *Executor) executeStep(ctx context.Context, step *core.Step, execCtx *co stepSymbol := terminal.StepTypeSymbol(string(step.Type), string(step.StepRunner)) cmdPrefix := terminal.StepCommandPrefix(string(step.Type)) stepCommand := getStepCommand(step) + stepCommandColored := getStepCommandColored(step) if stepCommand != "" { stepCommand, _ = e.templateEngine.Render(stepCommand, execCtx.GetVariables()) + stepCommandColored, _ = e.templateEngine.Render(stepCommandColored, execCtx.GetVariables()) } // Show step start (skip when progress bar is active) if e.progressBar == nil { - e.printer.StepStartWithCommand(step.Name, stepSymbol, stepCommand, cmdPrefix) + e.printer.StepStartWithCommand(step.Name, stepSymbol, stepCommandColored, cmdPrefix) } else { - // Update progress bar with current step command + // Update progress bar with current step command (uncolored for progress bar) e.progressBar.SetCommand(stepCommand) } diff --git a/internal/terminal/printer.go b/internal/terminal/printer.go index 72b36d8..ed7750b 100644 --- a/internal/terminal/printer.go +++ b/internal/terminal/printer.go @@ -130,7 +130,12 @@ func (p *Printer) StepStartWithCommand(stepName, typeSymbol, command, cmdPrefix } _, _ = fmt.Fprintf(os.Stdout, "%s %s %s %s\n", StepStartSymbol(), typeSymbol, Gray("(starting)"), HiBlue(stepName)) if command != "" { - _, _ = fmt.Fprintf(os.Stdout, " %s\n", HiGreen(formatMultilineCommand(command, cmdPrefix))) + // Check if command already contains ANSI color codes (pre-colored) + if strings.Contains(command, "\033[") { + _, _ = fmt.Fprintf(os.Stdout, " %s %s\n", cmdPrefix, command) + } else { + _, _ = fmt.Fprintf(os.Stdout, " %s\n", HiGreen(formatMultilineCommand(command, cmdPrefix))) + } } } @@ -383,9 +388,18 @@ func (p *Printer) VerboseOutput(output string) { if output == "" { return } + // Trim whitespace from output + output = strings.TrimSpace(output) + if output == "" { + return // Don't print [output] if nothing to show + } _, _ = fmt.Fprintf(os.Stdout, " %s\n", Gray("[output]")) - lines := strings.Split(strings.TrimSuffix(output, "\n"), "\n") + lines := strings.Split(output, "\n") for _, line := range lines { + // Skip blank lines + if strings.TrimSpace(line) == "" { + continue + } _, _ = fmt.Fprintf(os.Stdout, " %s\n", line) } } diff --git a/internal/terminal/symbols.go b/internal/terminal/symbols.go index 014eb0d..784f659 100644 --- a/internal/terminal/symbols.go +++ b/internal/terminal/symbols.go @@ -18,6 +18,7 @@ const ( // Step type and runner symbols SymbolFunction = "ƒ" // Function step SymbolBash = "$" // Bash/command step + SymbolForeach = "∀" // Foreach step (universal quantifier) SymbolDocker = "🐋" // Docker runner SymbolSSH = "❄" // SSH runner @@ -146,7 +147,9 @@ func StepTypeSymbol(stepType, runnerType string) string { return Cyan(SymbolFunction) case "remote-bash": return Cyan(SymbolSSH) - case "bash", "parallel-steps", "foreach": + case "foreach": + return Cyan(SymbolForeach) + case "bash", "parallel-steps": return Green(SymbolBash) default: return Green(SymbolBash) @@ -160,6 +163,8 @@ func StepCommandPrefix(stepType string) string { return SymbolBowtie // "⋈" case "function": return SymbolFunction // "ƒ" + case "foreach": + return SymbolForeach // "∀" default: return SymbolBash // "$" } diff --git a/pkg/cli/function.go b/pkg/cli/function.go index 0df0df9..63dbc0e 100644 --- a/pkg/cli/function.go +++ b/pkg/cli/function.go @@ -303,6 +303,14 @@ func executeFunctionForTarget(printer *terminal.Printer, script, target string) cfg := config.Get() ctx := executor.BuildBuiltinVariables(cfg, params) + + // Defer temp directory cleanup + if tempDir, ok := ctx["TempDir"].(string); ok && tempDir != "" { + defer func() { + _ = os.RemoveAll(tempDir) + }() + } + if target != "" { ctx["target"] = target } diff --git a/pkg/cli/install.go b/pkg/cli/install.go index 9655691..7f6670a 100644 --- a/pkg/cli/install.go +++ b/pkg/cli/install.go @@ -13,6 +13,7 @@ import ( "github.com/j3ssie/osmedeus/v5/internal/config" "github.com/j3ssie/osmedeus/v5/internal/core" "github.com/j3ssie/osmedeus/v5/internal/installer" + "github.com/j3ssie/osmedeus/v5/internal/parser" "github.com/j3ssie/osmedeus/v5/internal/terminal" "github.com/j3ssie/osmedeus/v5/public" "github.com/spf13/cobra" @@ -61,7 +62,7 @@ var installWorkflowCmd = &cobra.Command{ } return cobra.ExactArgs(1)(cmd, args) }, - RunE: runInstallWorkflow, + RunE: RunInstallWorkflow, } // installBaseCmd installs the base folder from a source @@ -141,7 +142,8 @@ This is the primary command for health checks. 'osmedeus health' is an alias for RunE: runInstallValidate, } -func runInstallWorkflow(cmd *cobra.Command, args []string) error { +// RunInstallWorkflow installs workflows from a source (exported for use by workflow install alias) +func RunInstallWorkflow(cmd *cobra.Command, args []string) error { cfg := config.Get() if cfg == nil { return fmt.Errorf("configuration not loaded") @@ -183,11 +185,40 @@ func runInstallWorkflow(cmd *cobra.Command, args []string) error { printer.Println(" %s %s", terminal.SymbolBullet, terminal.Gray(workflowURL)) } - return inst.InstallWorkflow(workflowURL) + if err := inst.InstallWorkflow(workflowURL); err != nil { + return err + } + printWorkflowSummary(printer, cfg.WorkflowsPath) + return nil } source := args[0] - return inst.InstallWorkflow(source) + if err := inst.InstallWorkflow(source); err != nil { + return err + } + printWorkflowSummary(printer, cfg.WorkflowsPath) + return nil +} + +// printWorkflowSummary counts and prints the number of workflows loaded +func printWorkflowSummary(printer *terminal.Printer, workflowsPath string) { + loader := parser.NewLoader(workflowsPath) + + flows, flowErr := loader.ListFlows() + modules, modErr := loader.ListModules() + + if flowErr == nil && modErr == nil { + total := len(flows) + len(modules) + if total > 0 { + printer.Info("Loaded %s workflows (%s flows, %s modules)", + terminal.Green(fmt.Sprintf("%d", total)), + terminal.Cyan(fmt.Sprintf("%d", len(flows))), + terminal.Yellow(fmt.Sprintf("%d", len(modules)))) + printer.Println(" %s Run %s to see workflow details", + terminal.Gray(terminal.SymbolLightning), + terminal.Cyan("osmedeus workflow ls")) + } + } } func runInstallBase(cmd *cobra.Command, args []string) error { @@ -261,6 +292,7 @@ func runInstallBase(cmd *cobra.Command, args []string) error { // Continue - workflow installation failure shouldn't block base setup } else { printer.Success("Workflows installed from: %s", terminal.Cyan(workflowURL)) + printWorkflowSummary(printer, cfg.WorkflowsPath) } printer.Newline() @@ -306,6 +338,19 @@ func runInstallBase(cmd *cobra.Command, args []string) error { return err } + // Reload config to get updated paths after base installation + reloaded, err := config.Load(cfg.BaseFolder) + if err == nil { + config.Set(reloaded) + cfg = reloaded + if reloaded.BinariesPath != "" { + binariesFolder = reloaded.BinariesPath + } + } + + // Check if workflows folder exists under the base folder and print stats + printWorkflowSummary(printer, cfg.WorkflowsPath) + ensureBinariesPathInEnv(printer, binariesFolder, true) return nil } diff --git a/pkg/cli/run.go b/pkg/cli/run.go index f39023a..7fec7a2 100644 --- a/pkg/cli/run.go +++ b/pkg/cli/run.go @@ -170,25 +170,51 @@ func captureExplicitFlags(cmd *cobra.Command) { // Returns true if empty_target preference is set to true, false otherwise // Note: This cannot work for stdin modules (--std-module) since stdin would be consumed func getWorkflowEmptyTargetPreference(cfg *config.Config) bool { + log := logger.Get() + if flowName == "" && len(moduleNames) == 0 { + log.Debug("getWorkflowEmptyTargetPreference: no flow or module specified") return false } loader := parser.NewLoader(cfg.WorkflowsPath) var workflow *core.Workflow var err error + var workflowName string if flowName != "" { + workflowName = flowName workflow, err = loader.LoadWorkflow(flowName) } else if len(moduleNames) > 0 { + workflowName = moduleNames[0] workflow, err = loader.LoadWorkflow(moduleNames[0]) } - if err != nil || workflow == nil || workflow.Preferences == nil { + if err != nil { + log.Debug("getWorkflowEmptyTargetPreference: failed to load workflow", + zap.String("workflow", workflowName), + zap.Error(err)) return false } - return workflow.Preferences.GetEmptyTarget(false) + if workflow == nil { + log.Debug("getWorkflowEmptyTargetPreference: workflow is nil", + zap.String("workflow", workflowName)) + return false + } + + if workflow.Preferences == nil { + log.Debug("getWorkflowEmptyTargetPreference: workflow has no preferences", + zap.String("workflow", workflowName)) + return false + } + + result := workflow.Preferences.GetEmptyTarget(false) + log.Debug("getWorkflowEmptyTargetPreference: checked preference", + zap.String("workflow", workflowName), + zap.Bool("empty_target", result)) + + return result } // applyWorkflowPreferences applies workflow preferences to CLI variables diff --git a/pkg/cli/workflow.go b/pkg/cli/workflow.go index 306467e..6631b7e 100644 --- a/pkg/cli/workflow.go +++ b/pkg/cli/workflow.go @@ -1134,6 +1134,23 @@ var workflowShowCmd = &cobra.Command{ }, } +// workflowInstallCmd installs workflows from a source (alias for install workflow) +var workflowInstallCmd = &cobra.Command{ + Use: "install [source]", + Short: "Install workflows from git URL, zip URL, local zip file, or local folder", + Long: `Install workflows from various sources. This is an alias for 'osmedeus install workflow'.`, + Args: func(cmd *cobra.Command, args []string) error { + if workflowPreset { + if len(args) != 0 { + return fmt.Errorf("no source argument is allowed with --preset") + } + return nil + } + return cobra.ExactArgs(1)(cmd, args) + }, + RunE: RunInstallWorkflow, +} + // workflowValidateCmd validates a workflow var workflowValidateCmd = &cobra.Command{ Use: "validate [name|path|folder]", @@ -1211,9 +1228,11 @@ func init() { workflowValidateCmd.Flags().StringVar(&lintFormat, "format", "pretty", "output format: pretty, json, github") workflowValidateCmd.Flags().StringSliceVar(&lintDisable, "disable", []string{}, "disable specific rules (comma-separated)") workflowValidateCmd.Flags().StringVar(&lintSeverity, "severity", "info", "minimum severity level: info, warning, error") + workflowInstallCmd.Flags().BoolVar(&workflowPreset, "preset", false, "install from OSM_WORKFLOW_URL environment variable (default: DEFAULT_WORKFLOW_REPO)") workflowCmd.AddCommand(workflowListCmd) workflowCmd.AddCommand(workflowShowCmd) workflowCmd.AddCommand(workflowValidateCmd) + workflowCmd.AddCommand(workflowInstallCmd) } // hasMatchingTags checks if a workflow has any of the specified tags diff --git a/public/presets/registry-metadata-direct-fetch.json b/public/presets/registry-metadata-direct-fetch.json index 1b04391..097a3ec 100644 --- a/public/presets/registry-metadata-direct-fetch.json +++ b/public/presets/registry-metadata-direct-fetch.json @@ -1,5 +1,5 @@ { - "_last_update_at": "2026-01-21T10:08:20.876322+00:00", + "_last_update_at": "2026-02-01T03:52:26.051205+00:00", "amass": { "desc": "In-depth attack surface mapping and asset discovery", "repo_link": "https://github.com/owasp-amass/amass", @@ -42,9 +42,9 @@ } }, "dnsx": { - "desc": "fast and multi-purpose DNS toolkit allow to run multiple DNS queries of your choice with a list of user-supplied resolvers", + "desc": "dnsx is a fast and multi-purpose DNS toolkit allow to run multiple DNS queries of your choice with a list of user-supplied resolvers.", "repo_link": "https://github.com/projectdiscovery/dnsx", - "version": "1.2.2", + "version": "1.2.3", "package-manager": "github-release", "tags": [ "recon", @@ -54,12 +54,12 @@ ], "valide-command": "", "linux": { - "amd64": "https://github.com/projectdiscovery/dnsx/releases/download/v1.2.2/dnsx_1.2.2_linux_amd64.zip", - "arm64": "https://github.com/projectdiscovery/dnsx/releases/download/v1.2.2/dnsx_1.2.2_linux_arm64.zip" + "amd64": "https://github.com/projectdiscovery/dnsx/releases/download/v1.2.3/dnsx_1.2.3_linux_amd64.zip", + "arm64": "https://github.com/projectdiscovery/dnsx/releases/download/v1.2.3/dnsx_1.2.3_linux_arm64.zip" }, "darwin": { - "amd64": "https://github.com/projectdiscovery/dnsx/releases/download/v1.2.2/dnsx_1.2.2_macOS_amd64.zip", - "arm64": "https://github.com/projectdiscovery/dnsx/releases/download/v1.2.2/dnsx_1.2.2_macOS_arm64.zip" + "amd64": "https://github.com/projectdiscovery/dnsx/releases/download/v1.2.3/dnsx_1.2.3_macOS_amd64.zip", + "arm64": "https://github.com/projectdiscovery/dnsx/releases/download/v1.2.3/dnsx_1.2.3_macOS_arm64.zip" } }, "assetfinder": { @@ -84,7 +84,7 @@ "nuclei": { "desc": "Nuclei is a fast, customizable vulnerability scanner powered by the global security community and built on a simple YAML-based DSL, enabling collaboration to tackle trending vulnerabilities on the internet. It helps you find vulnerabilities in your applications, APIs, networks, DNS, and cloud configurations.", "repo_link": "https://github.com/projectdiscovery/nuclei", - "version": "3.6.2", + "version": "3.7.0", "package-manager": "github-release", "tags": [ "vulnerability-scanner", @@ -93,12 +93,12 @@ ], "valide-command": "", "linux": { - "amd64": "https://github.com/projectdiscovery/nuclei/releases/download/v3.6.2/nuclei_3.6.2_linux_amd64.zip", - "arm64": "https://github.com/projectdiscovery/nuclei/releases/download/v3.6.2/nuclei_3.6.2_linux_arm64.zip" + "amd64": "https://github.com/projectdiscovery/nuclei/releases/download/v3.7.0/nuclei_3.7.0_linux_amd64.zip", + "arm64": "https://github.com/projectdiscovery/nuclei/releases/download/v3.7.0/nuclei_3.7.0_linux_arm64.zip" }, "darwin": { - "amd64": "https://github.com/projectdiscovery/nuclei/releases/download/v3.6.2/nuclei_3.6.2_macOS_amd64.zip", - "arm64": "https://github.com/projectdiscovery/nuclei/releases/download/v3.6.2/nuclei_3.6.2_macOS_arm64.zip" + "amd64": "https://github.com/projectdiscovery/nuclei/releases/download/v3.7.0/nuclei_3.7.0_macOS_amd64.zip", + "arm64": "https://github.com/projectdiscovery/nuclei/releases/download/v3.7.0/nuclei_3.7.0_macOS_arm64.zip" } }, "nuclei-templates": { @@ -122,7 +122,7 @@ "httpx": { "desc": "httpx is a fast and multi-purpose HTTP toolkit that allows running multiple probes using the retryablehttp library.", "repo_link": "https://github.com/projectdiscovery/httpx", - "version": "1.7.4", + "version": "1.8.1", "package-manager": "github-release", "tags": [ "recon", @@ -132,12 +132,12 @@ ], "valide-command": "", "linux": { - "amd64": "https://github.com/projectdiscovery/httpx/releases/download/v1.7.4/httpx_1.7.4_linux_amd64.zip", - "arm64": "https://github.com/projectdiscovery/httpx/releases/download/v1.7.4/httpx_1.7.4_linux_arm64.zip" + "amd64": "https://github.com/projectdiscovery/httpx/releases/download/v1.8.1/httpx_1.8.1_linux_amd64.zip", + "arm64": "https://github.com/projectdiscovery/httpx/releases/download/v1.8.1/httpx_1.8.1_linux_arm64.zip" }, "darwin": { - "amd64": "https://github.com/projectdiscovery/httpx/releases/download/v1.7.4/httpx_1.7.4_macOS_amd64.zip", - "arm64": "https://github.com/projectdiscovery/httpx/releases/download/v1.7.4/httpx_1.7.4_macOS_arm64.zip" + "amd64": "https://github.com/projectdiscovery/httpx/releases/download/v1.8.1/httpx_1.8.1_macOS_amd64.zip", + "arm64": "https://github.com/projectdiscovery/httpx/releases/download/v1.8.1/httpx_1.8.1_macOS_arm64.zip" } }, "katana": { @@ -164,7 +164,7 @@ "naabu": { "desc": "A fast port scanner written in go with a focus on reliability and simplicity. Designed to be used in combination with other tools for attack surface discovery in bug bounties and pentests", "repo_link": "https://github.com/projectdiscovery/naabu", - "version": "2.3.7", + "version": "2.4.0", "package-manager": "github-release", "tags": [ "port-scanner", @@ -173,12 +173,12 @@ ], "valide-command": "", "linux": { - "amd64": "https://github.com/projectdiscovery/naabu/releases/download/v2.3.7/naabu_2.3.7_linux_amd64.zip", - "arm64": "https://github.com/projectdiscovery/naabu/releases/download/v2.3.7/naabu_2.3.7_linux_arm64.zip" + "amd64": "https://github.com/projectdiscovery/naabu/releases/download/v2.4.0/naabu_2.4.0_linux_amd64.zip", + "arm64": "https://github.com/projectdiscovery/naabu/releases/download/v2.4.0/naabu_2.4.0_linux_arm64.zip" }, "darwin": { - "amd64": "https://github.com/projectdiscovery/naabu/releases/download/v2.3.7/naabu_2.3.7_macOS_amd64.zip", - "arm64": "https://github.com/projectdiscovery/naabu/releases/download/v2.3.7/naabu_2.3.7_macOS_arm64.zip" + "amd64": "https://github.com/projectdiscovery/naabu/releases/download/v2.4.0/naabu_2.4.0_macOS_amd64.zip", + "arm64": "https://github.com/projectdiscovery/naabu/releases/download/v2.4.0/naabu_2.4.0_macOS_arm64.zip" } }, "ffuf": { @@ -223,6 +223,27 @@ "arm64": "https://github.com/trufflesecurity/trufflehog/releases/download/v3.90.3/trufflehog_3.90.3_darwin_arm64.tar.gz" } }, + "kingfisher": { + "desc": "Kingfisher is a blazingly fast and highly accurate tool for secret detection and live validation across files, Git repos, GitHub, GitLab, Azure Repos, BitBucket, Gitea, AWS S3, Docker images, Jira, Slack, and Confluence", + "repo_link": "https://github.com/mongodb/kingfisher", + "version": "1.76.0", + "package-manager": "github-release", + "tags": [ + "secret-detection", + "security", + "scanning", + "credentials" + ], + "valide-command": "", + "linux": { + "amd64": "https://github.com/mongodb/kingfisher/releases/download/v1.76.0/kingfisher-linux-x64.tgz", + "arm64": "https://github.com/mongodb/kingfisher/releases/download/v1.76.0/kingfisher-linux-arm64.tgz" + }, + "darwin": { + "amd64": "https://github.com/mongodb/kingfisher/releases/download/v1.76.0/kingfisher-darwin-x64.tgz", + "arm64": "https://github.com/mongodb/kingfisher/releases/download/v1.76.0/kingfisher-darwin-arm64.tgz" + } + }, "semgrep": { "desc": "Static analysis tool for code scanning", "repo_link": "https://github.com/returntocorp/semgrep", @@ -271,7 +292,8 @@ "tags": [ "utility", "file-transfer", - "sync" + "sync", + "optional" ], "valide-command": "", "multi-commands-linux": [ @@ -519,7 +541,7 @@ } }, "puredns": { - "desc": "Fast domain resolver and subdomain bruteforcing tool with accurate wildcard filtering", + "desc": "Puredns is a fast domain resolver and subdomain bruteforcing tool that can accurately filter out wildcard subdomains and DNS poisoned entries.", "repo_link": "https://github.com/d3mondev/puredns", "version": "2.1.1", "package-manager": "github-release", @@ -703,7 +725,7 @@ "trivy": { "desc": "Find vulnerabilities, misconfigurations, secrets, SBOM in containers, Kubernetes, code repositories, clouds and more", "repo_link": "https://github.com/aquasecurity/trivy", - "version": "0.68.2", + "version": "0.69.0", "package-manager": "github-release", "tags": [ "vulnerability-scanner", @@ -793,7 +815,7 @@ "interactsh": { "desc": "An OOB interaction gathering server and client library", "repo_link": "https://github.com/projectdiscovery/interactsh", - "version": "1.2.4", + "version": "1.3.0", "package-manager": "go-getter", "tags": [ "oob-testing",