diff --git a/internal/core/clone.go b/internal/core/clone.go index 08941a7..55e5486 100644 --- a/internal/core/clone.go +++ b/internal/core/clone.go @@ -219,9 +219,11 @@ func (m *ModuleRef) Clone() *ModuleRef { } cloned := &ModuleRef{ - Name: m.Name, - Path: m.Path, - Condition: m.Condition, + Name: m.Name, + Path: m.Path, + Condition: m.Condition, + Runner: m.Runner, + Description: m.Description, } if len(m.Params) > 0 { @@ -252,6 +254,16 @@ func (m *ModuleRef) Clone() *ModuleRef { cloned.Decision = m.Decision.Clone() + // Clone inline module fields + if len(m.Steps) > 0 { + cloned.Steps = make([]Step, len(m.Steps)) + for i, s := range m.Steps { + cloned.Steps[i] = *s.Clone() + } + } + + cloned.RunnerConfig = m.RunnerConfig.Clone() + return cloned } diff --git a/internal/core/workflow.go b/internal/core/workflow.go index 58f5894..d792dcc 100644 --- a/internal/core/workflow.go +++ b/internal/core/workflow.go @@ -78,16 +78,42 @@ type RunnerConfig struct { WorkDir string `yaml:"workdir,omitempty"` // Working directory on remote/container } -// ModuleRef references a module in a flow +// ModuleRef references a module in a flow or defines an inline module type ModuleRef struct { Name string `yaml:"name"` - Path string `yaml:"path"` + Path string `yaml:"path,omitempty"` // Path to external module file (omit for inline) Params map[string]string `yaml:"params"` DependsOn []string `yaml:"depends_on"` Condition string `yaml:"condition"` OnSuccess []Action `yaml:"on_success"` OnError []Action `yaml:"on_error"` Decision *DecisionConfig `yaml:"decision"` + + // Inline module fields (used when Path is empty) + Steps []Step `yaml:"steps,omitempty"` // Inline steps (makes this an inline module) + Runner RunnerType `yaml:"runner,omitempty"` // Runner type for inline module + RunnerConfig *RunnerConfig `yaml:"runner_config,omitempty"` // Runner configuration for inline module + Description string `yaml:"description,omitempty"` // Description for inline module +} + +// IsInline returns true if this is an inline module (has steps defined directly) +func (m *ModuleRef) IsInline() bool { + return len(m.Steps) > 0 +} + +// ToWorkflow converts an inline ModuleRef to a Workflow for execution +func (m *ModuleRef) ToWorkflow() *Workflow { + if !m.IsInline() { + return nil + } + return &Workflow{ + Kind: KindModule, + Name: m.Name, + Description: m.Description, + Steps: m.Steps, + Runner: m.Runner, + RunnerConfig: m.RunnerConfig, + } } // IsModule returns true if the workflow is a module diff --git a/internal/executor/executor.go b/internal/executor/executor.go index 02a4809..a4bcc82 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -1338,6 +1338,15 @@ func (e *Executor) preloadModules(ctx context.Context, modules []core.ModuleRef) for i := range modules { modRef := &modules[i] + + // Skip inline modules - they don't need loading from disk + if modRef.IsInline() { + mu.Lock() + result[modRef.Name] = modRef.ToWorkflow() + mu.Unlock() + continue + } + wg.Add(1) go func(ref *core.ModuleRef) { defer wg.Done() @@ -1652,17 +1661,27 @@ func (e *Executor) ExecuteFlow(ctx context.Context, flow *core.Workflow, params } // Execute module - execCtx.Logger.Info("Executing module", - zap.String("module", modRef.Name), - zap.String("path", modRef.Path), - ) + if modRef.IsInline() { + execCtx.Logger.Info("Executing inline module", + zap.String("module", modRef.Name), + ) + } else { + execCtx.Logger.Info("Executing module", + zap.String("module", modRef.Name), + zap.String("path", modRef.Path), + ) + } - // Use preloaded module if available, else load on-demand + // Use preloaded module if available, else load on-demand or use inline var module *core.Workflow var err error if preloadedMod, ok := preloaded[modRef.Name]; ok { module = preloadedMod execCtx.Logger.Debug("Using preloaded module", zap.String("module", modRef.Name)) + } else if modRef.IsInline() { + // Use inline module definition + module = modRef.ToWorkflow() + execCtx.Logger.Debug("Using inline module", zap.String("module", modRef.Name)) } else { // Load the module workflow on-demand (fallback for failed preloads) module, err = e.loader.LoadWorkflowByPath(modRef.Path) diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 9feab76..1afd091 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -125,10 +125,18 @@ func (p *Parser) validateFlow(w *core.Workflow) error { Message: "module reference name is required", } } - if mod.Path == "" { + // Path is required only for external modules (not inline modules) + if mod.Path == "" && !mod.IsInline() { return &ValidationError{ Field: fmt.Sprintf("modules[%d].path", i), - Message: "module reference path is required", + Message: "module reference path is required (or define inline steps)", + } + } + // Inline modules must have at least one step + if mod.IsInline() && len(mod.Steps) == 0 { + return &ValidationError{ + Field: fmt.Sprintf("modules[%d].steps", i), + Message: "inline module must have at least one step", } } } diff --git a/pkg/cli/root.go b/pkg/cli/root.go index f346f46..eabcd9a 100644 --- a/pkg/cli/root.go +++ b/pkg/cli/root.go @@ -1,6 +1,7 @@ package cli import ( + "encoding/json" "errors" "fmt" "os" @@ -452,12 +453,41 @@ var versionCmd = &cobra.Command{ Use: "version", Short: "Print version information", Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("%s - %s\n", core.BINARY, core.DESC) - fmt.Printf("Version: %s\n", core.VERSION) - fmt.Printf("Build: %s\n", buildTime) - fmt.Printf("Commit: %s\n", commitHash) - fmt.Printf("Author: %s\n", core.AUTHOR) - fmt.Printf("Docs: %s\n", core.DOCS) + if globalJSON { + // JSON output + versionInfo := map[string]string{ + "name": core.BINARY, + "description": core.DESC, + "version": core.VERSION, + "build": buildTime, + "commit": commitHash, + "author": core.AUTHOR, + "docs": core.DOCS, + } + jsonOut, _ := json.MarshalIndent(versionInfo, "", " ") + fmt.Println(string(jsonOut)) + return + } + + // Colored output + fmt.Printf("%s - %s\n", + terminal.BoldCyan(core.BINARY), + terminal.HiBlue(core.DESC)) + fmt.Printf("%s %s\n", + terminal.Bold("Version:"), + terminal.Green(core.VERSION)) + fmt.Printf("%s %s\n", + terminal.Bold("Build:"), + terminal.Yellow(buildTime)) + fmt.Printf("%s %s\n", + terminal.Bold("Commit:"), + terminal.Cyan(commitHash)) + fmt.Printf("%s %s\n", + terminal.Bold("Author:"), + terminal.Magenta(core.AUTHOR)) + fmt.Printf("%s %s\n", + terminal.Bold("Docs:"), + terminal.Blue(core.DOCS)) }, } diff --git a/test/testdata/complex-workflows/vulnscan.yaml b/test/testdata/complex-workflows/vulnscan.yaml deleted file mode 100644 index 77d8ffd..0000000 --- a/test/testdata/complex-workflows/vulnscan.yaml +++ /dev/null @@ -1,327 +0,0 @@ -name: vulnscan -kind: module -description: Run vulnerability scan on all HTTP hosts using Jaeles and Nuclei scanners - -params: - - name: target - required: true - - name: httpFile - default: "{{Output}}/probing/http-{{TargetSpace}}.txt" - - name: output_dir - default: "{{Output}}/vuln" - - name: sign - default: "~/.jaeles/base-signatures/cves/.*" - - name: sign2 - default: "~/.jaeles/base-signatures/common/.*" - - name: sign3 - default: "~/.jaeles/base-signatures/sensitive/.*" - - name: splitLines - default: "500" - - name: limit - default: "25000" - - name: extra - default: " " - - name: enableNuclei - default: "true" - - name: threads - default: "10" - - name: nucleiThreads - default: "{{threads * 10}}" - - name: jaelesThreads - default: "{{threads * 5}}" - - name: nucleiTimeout - default: "8h" - - name: jaelesTimeout - default: "3h" - - name: nucleiSeverity - default: "critical,high,medium,low,info" - - name: defaultUA - default: "User-Agent: Mozilla/5.0 (compatible; Osmedeus/v4; +https://github.com/j3ssie/osmedeus)" - -steps: - # ============================================================ - # Phase 1: Validate Dependencies - # ============================================================ - - name: validate-dependencies - type: function - function: | - file_exists("{{Binaries}}/jaeles") && - file_exists("{{Binaries}}/nuclei") - exports: - deps_valid: "output" - on_error: - - action: log - message: "Required binaries (jaeles, nuclei) not found" - - action: abort - - # ============================================================ - # Phase 2: Setup Output Directories - # ============================================================ - - name: setup-directories - type: bash - commands: - - mkdir -p {{output_dir}} - - mkdir -p {{output_dir}}/raw - - mkdir -p {{output_dir}}/active - - mkdir -p {{output_dir}}/sensitive - - mkdir -p {{output_dir}}/nuclei - - # ============================================================ - # Phase 3: Validate Input File - # ============================================================ - - name: check-input-exists - type: function - function: file_exists("{{httpFile}}") - exports: - input_exists: "output" - on_error: - - action: log - message: "Input file {{httpFile}} not found" - - action: abort - - - name: count-input-lines - type: function - function: file_length("{{httpFile}}") - exports: - input_count: "output" - - # Decision: Abort if input file exceeds limit - - name: check-input-limit - type: function - function: | - var count = parse_int("{{input_count}}"); - var limit = parse_int("{{limit}}"); - if (count > limit) { - return "exceeds_limit"; - } - return "valid"; - exports: - input_valid: "{{Result}}" - decision: - switch: "{{input_valid}}" - cases: - "exceeds_limit": - goto: abort-large-input - default: - goto: split-input-file - - - name: abort-large-input - type: function - function: printf("ERROR: Input file has {{input_count}} lines, exceeds limit of {{limit}}") - on_error: - - action: abort - - # ============================================================ - # Phase 4: Split Input for Parallel Processing - # ============================================================ - - name: split-input-file - type: function - function: SplitFile("{{httpFile}}", "{{TargetSpace}}-index", {{splitLines}}, "{{output_dir}}/raw") - exports: - split_dir: "{{output_dir}}/raw" - - - name: list-split-files - type: bash - command: "ls {{output_dir}}/raw/{{TargetSpace}}-index* 2>/dev/null | head -100 > {{output_dir}}/raw/split-files.txt || touch {{output_dir}}/raw/split-files.txt" - exports: - split_files: "{{output_dir}}/raw/split-files.txt" - - - name: count-split-files - type: function - function: file_length("{{output_dir}}/raw/split-files.txt") - exports: - split_count: "output" - - # ============================================================ - # Phase 5: Jaeles Vulnerability Scanning - # ============================================================ - - name: jaeles-active-scan - type: foreach - pre_condition: 'parse_int("{{split_count}}") > 0' - input: "{{output_dir}}/raw/split-files.txt" - variable: splitfile - threads: 1 - step: - name: run-jaeles-active - type: bash - command: | - echo "Running Jaeles active scan on [[splitfile]]..." - timeout -k 1m {{jaelesTimeout}} {{Binaries}}/jaeles scan -c {{jaelesThreads}} -s '{{sign}}' -s '{{sign2}}' -U [[splitfile]] -o {{output_dir}}/active/ {{extra}} 2>/dev/null || true - timeout: 14400 - - - name: jaeles-sensitive-scan - type: foreach - pre_condition: 'parse_int("{{split_count}}") > 0' - input: "{{output_dir}}/raw/split-files.txt" - variable: splitfile - threads: 1 - step: - name: run-jaeles-sensitive - type: bash - command: | - echo "Running Jaeles sensitive scan on [[splitfile]]..." - timeout -k 1m {{jaelesTimeout}} {{Binaries}}/jaeles scan --fi -c {{jaelesThreads}} -s '{{sign3}}' -L 2 -U [[splitfile]] -o {{output_dir}}/sensitive/ {{extra}} 2>/dev/null || true - timeout: 14400 - - # ============================================================ - # Phase 6: Generate Jaeles Reports - # ============================================================ - - name: generate-jaeles-reports - type: parallel-steps - parallel_steps: - - name: generate-active-report - type: bash - command: "{{Binaries}}/jaeles report -o {{output_dir}}/active/ -R {{output_dir}}/active/{{TargetSpace}}-report.html 2>/dev/null || true" - on_error: - - action: continue - - - name: generate-sensitive-report - type: bash - command: "{{Binaries}}/jaeles report -o {{output_dir}}/sensitive/ -R {{output_dir}}/sensitive/{{TargetSpace}}-sensitive.html 2>/dev/null || true" - on_error: - - action: continue - - # ============================================================ - # Phase 7: Process Jaeles Results - # ============================================================ - - name: copy-active-summary - type: bash - pre_condition: 'file_exists("{{output_dir}}/active/jaeles-summary.txt")' - command: "cp {{output_dir}}/active/jaeles-summary.txt {{output_dir}}/active/activescan-{{TargetSpace}}-{{TS}}.txt" - exports: - active_summary: "{{output_dir}}/active/activescan-{{TargetSpace}}-{{TS}}.txt" - - - name: notify-active-results - type: function - pre_condition: 'file_exists("{{output_dir}}/active/activescan-{{TargetSpace}}-{{TS}}.txt")' - parallel_functions: - - TeleMessByFile("#report", "{{output_dir}}/active/activescan-{{TargetSpace}}-{{TS}}.txt") - - Cat("{{output_dir}}/active/activescan-{{TargetSpace}}-{{TS}}.txt") - - TotalVulnerability("{{output_dir}}/active/activescan-{{TargetSpace}}-{{TS}}.txt") - on_error: - - action: log - message: "Failed to notify active scan results" - - action: continue - - - name: copy-sensitive-summary - type: bash - pre_condition: 'file_exists("{{output_dir}}/sensitive/jaeles-summary.txt")' - command: "cp {{output_dir}}/sensitive/jaeles-summary.txt {{output_dir}}/sensitive/sensitivescan-{{TargetSpace}}-{{TS}}.txt" - exports: - sensitive_summary: "{{output_dir}}/sensitive/sensitivescan-{{TargetSpace}}-{{TS}}.txt" - - - name: notify-sensitive-results - type: function - pre_condition: 'file_exists("{{output_dir}}/sensitive/sensitivescan-{{TargetSpace}}-{{TS}}.txt")' - parallel_functions: - - TeleMessByFile("#sensitive", "{{output_dir}}/sensitive/sensitivescan-{{TargetSpace}}-{{TS}}.txt") - - Cat("{{output_dir}}/sensitive/sensitivescan-{{TargetSpace}}-{{TS}}.txt") - - TotalVulnerability("{{output_dir}}/sensitive/sensitivescan-{{TargetSpace}}-{{TS}}.txt") - on_error: - - action: log - message: "Failed to notify sensitive scan results" - - action: continue - - # ============================================================ - # Phase 8: Nuclei Vulnerability Scanning - # ============================================================ - - name: nuclei-scan - type: bash - pre_condition: '"{{enableNuclei}}" == "true" && file_exists("{{httpFile}}")' - command: | - timeout -k 1m {{nucleiTimeout}} {{Binaries}}/nuclei \ - -H '{{defaultUA}}' \ - -silent \ - -c {{nucleiThreads}} \ - -jsonl \ - -severity '{{nucleiSeverity}}' \ - -t ~/nuclei-templates/ \ - -l {{httpFile}} \ - -irr \ - -o {{output_dir}}/nuclei/{{TargetSpace}}-nuclei-json.txt - timeout: 28800 - exports: - nuclei_json: "{{output_dir}}/nuclei/{{TargetSpace}}-nuclei-json.txt" - on_error: - - action: log - message: "Nuclei scan failed or timed out" - - action: continue - - - name: count-nuclei-results - type: function - pre_condition: 'file_exists("{{output_dir}}/nuclei/{{TargetSpace}}-nuclei-json.txt")' - function: file_length("{{output_dir}}/nuclei/{{TargetSpace}}-nuclei-json.txt") - exports: - nuclei_count: "output" - - # ============================================================ - # Phase 9: Process Nuclei Results - # ============================================================ - - name: generate-nuclei-report - type: function - pre_condition: 'parse_int("{{nuclei_count}}") > 0' - function: GenNucleiReport("{{output_dir}}/nuclei/{{TargetSpace}}-nuclei-json.txt", "{{output_dir}}/nuclei/{{TargetSpace}}-nuclei.html") - on_error: - - action: log - message: "Failed to generate Nuclei HTML report" - - action: continue - - - name: parse-nuclei-json - type: bash - pre_condition: 'parse_int("{{nuclei_count}}") > 0' - command: | - cat {{output_dir}}/nuclei/{{TargetSpace}}-nuclei-json.txt | \ - jq -r '[.info.severity,.\"template-id\",.\"matched-at\",.\"matched-name\"] | join(\" - \")' \ - > {{output_dir}}/nuclei/{{TargetSpace}}-nuclei-scan.txt 2>/dev/null || true - exports: - nuclei_parsed: "{{output_dir}}/nuclei/{{TargetSpace}}-nuclei-scan.txt" - - - name: sort-nuclei-results - type: function - pre_condition: 'file_exists("{{output_dir}}/nuclei/{{TargetSpace}}-nuclei-scan.txt")' - function: SortU("{{output_dir}}/nuclei/{{TargetSpace}}-nuclei-scan.txt") - - - name: notify-nuclei-results - type: function - pre_condition: 'parse_int("{{nuclei_count}}") > 0' - parallel_functions: - - TeleMessByFile("#sensitive", "{{output_dir}}/nuclei/{{TargetSpace}}-nuclei-scan.txt") - - Cat("{{output_dir}}/nuclei/{{TargetSpace}}-nuclei-scan.txt") - on_error: - - action: log - message: "Failed to notify Nuclei results" - - action: continue - - # ============================================================ - # Phase 10: Generate Final Report - # ============================================================ - - name: generate-final-report - type: function - pre_condition: 'file_exists("{{Data}}/markdown/general-template.md")' - function: GenMarkdownReport("{{Data}}/markdown/general-template.md", "{{Output}}/summary.html") - on_error: - - action: log - message: "Final report generation skipped - template not found" - - action: continue - - - name: generate-vuln-summary - type: bash - commands: - - | - echo "=== Vulnerability Scan Report ===" > {{output_dir}}/final-report-{{TargetSpace}}.txt - echo "Target: {{Target}}" >> {{output_dir}}/final-report-{{TargetSpace}}.txt - echo "Workspace: {{TargetSpace}}" >> {{output_dir}}/final-report-{{TargetSpace}}.txt - echo "Date: $(date)" >> {{output_dir}}/final-report-{{TargetSpace}}.txt - echo "" >> {{output_dir}}/final-report-{{TargetSpace}}.txt - echo "=== Statistics ===" >> {{output_dir}}/final-report-{{TargetSpace}}.txt - echo "Input Hosts: {{input_count}}" >> {{output_dir}}/final-report-{{TargetSpace}}.txt - echo "Nuclei Findings: {{nuclei_count}}" >> {{output_dir}}/final-report-{{TargetSpace}}.txt - echo "" >> {{output_dir}}/final-report-{{TargetSpace}}.txt - echo "=== Reports Generated ===" >> {{output_dir}}/final-report-{{TargetSpace}}.txt - echo "- Active Scan: {{output_dir}}/active/{{TargetSpace}}-report.html" >> {{output_dir}}/final-report-{{TargetSpace}}.txt - echo "- Sensitive Scan: {{output_dir}}/sensitive/{{TargetSpace}}-sensitive.html" >> {{output_dir}}/final-report-{{TargetSpace}}.txt - echo "- Nuclei Scan: {{output_dir}}/nuclei/{{TargetSpace}}-nuclei.html" >> {{output_dir}}/final-report-{{TargetSpace}}.txt - - - name: notify-completion - type: function - function: printf("Vulnerability scan complete: {{input_count}} hosts scanned, {{nuclei_count}} nuclei findings") diff --git a/test/testdata/workflows/test-inline-modules.yaml b/test/testdata/workflows/test-inline-modules.yaml new file mode 100644 index 0000000..a9d013c --- /dev/null +++ b/test/testdata/workflows/test-inline-modules.yaml @@ -0,0 +1,40 @@ +name: test-inline-modules +kind: flow +description: Simple test flow with inline modules (no path references) + +params: + - name: target + required: true + +modules: + - name: greet-module + description: Simple greeting module + steps: + - name: greet + type: bash + command: echo "Hello, {{target}}!" + exports: + greeting: "{{StepOutput}}" + + - name: info-module + description: Displays system info + depends_on: + - greet-module + steps: + - name: show-info + type: bash + command: echo "Target is {{target}}, greeting was {{greeting}}" + - name: date-step + type: bash + command: date +%Y-%m-%d + exports: + current_date: "{{StepOutput}}" + + - name: final-module + description: Final summary module + depends_on: + - info-module + steps: + - name: summary + type: bash + command: echo "Flow completed on {{current_date}} for {{target}}"