mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-09-29 04:54:57 +02:00
feat: add LLM step executor with vision and tool support, event workflow system, and inheritance
- Add LLM executor supporting OpenAI vision, tool calling, embeddings, and structured outputs - Introduce event emitter/receiver workflows with deduplication and filtering (generate_event functions) - Add workflow extends/override system enabling inheritance chains and step merge modes - Update function naming to snake_case across all testdata (fileExists→file_exists, etc.) - Add comprehensive test fixtures for linter, events, CDN, step dependencies, and extends workflows
This commit is contained in:
+99
@@ -0,0 +1,99 @@
|
||||
# Event Workflow Examples
|
||||
|
||||
This folder contains sample workflows demonstrating the event trigger and generation system.
|
||||
|
||||
## Workflows
|
||||
|
||||
### Emitters (Event Generators)
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `simple-emitter.yaml` | Basic event emission with `generate_event` and `generate_event_from_file` |
|
||||
| `vuln-emitter.yaml` | Simulates a vulnerability scanner emitting structured finding events |
|
||||
|
||||
### Receivers (Event Triggers)
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `simple-receiver.yaml` | Basic event trigger that listens for discovery events |
|
||||
| `filtered-receiver.yaml` | Advanced filtering with severity checks and jq extraction |
|
||||
| `dedupe-receiver.yaml` | Event deduplication to prevent duplicate processing |
|
||||
|
||||
## Event Functions
|
||||
|
||||
### generate_event(workspace, topic, source, data_type, data)
|
||||
Emit a single event with optional structured data. The workspace parameter identifies the target space for the event.
|
||||
|
||||
```yaml
|
||||
- type: function
|
||||
function: |
|
||||
generate_event("{{Workspace}}", "discovery.asset", "my-scanner", "subdomain", "api.example.com")
|
||||
```
|
||||
|
||||
### generate_event_from_file(workspace, topic, source, data_type, file_path)
|
||||
Emit one event per line from a file. Returns the count of events emitted.
|
||||
|
||||
```yaml
|
||||
- type: function
|
||||
functions:
|
||||
- 'generate_event_from_file("{{Workspace}}", "discovery.asset", "my-scanner", "subdomain", "{{Output}}/subdomains.txt")'
|
||||
```
|
||||
|
||||
## Trigger Configuration
|
||||
|
||||
### Basic Event Trigger
|
||||
```yaml
|
||||
trigger:
|
||||
- name: on-new-asset
|
||||
on: event
|
||||
event:
|
||||
topic: "discovery.asset"
|
||||
input:
|
||||
type: event_data
|
||||
field: "value"
|
||||
name: target
|
||||
enabled: true
|
||||
```
|
||||
|
||||
### Filtered Event Trigger
|
||||
```yaml
|
||||
trigger:
|
||||
- name: on-high-severity
|
||||
on: event
|
||||
event:
|
||||
topic: "scan.finding"
|
||||
filters:
|
||||
- "event.data.severity == 'high'"
|
||||
- "event.data.confirmed == true"
|
||||
input:
|
||||
type: function
|
||||
function: 'jq("{{event.data}}", ".url")'
|
||||
name: target
|
||||
enabled: true
|
||||
```
|
||||
|
||||
### Deduplicated Event Trigger
|
||||
```yaml
|
||||
trigger:
|
||||
- name: on-url-dedupe
|
||||
on: event
|
||||
event:
|
||||
topic: "crawler.url"
|
||||
dedupe_key: "{{event.data.url}}"
|
||||
dedupe_window: "5m"
|
||||
input:
|
||||
type: event_data
|
||||
field: "value"
|
||||
name: target
|
||||
enabled: true
|
||||
```
|
||||
|
||||
## Running Examples
|
||||
|
||||
```bash
|
||||
# Run the emitter to generate events
|
||||
osmedeus run -m simple-emitter -t example.com
|
||||
|
||||
# The receiver will automatically process events if registered with the scheduler
|
||||
osmedeus run -m simple-receiver -t example.com
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
# Deduplicated Event Receiver Workflow
|
||||
# This workflow demonstrates event deduplication to prevent processing duplicates
|
||||
name: dedupe-receiver
|
||||
kind: module
|
||||
description: Workflow with event deduplication to avoid processing duplicates
|
||||
tags: event,receiver,dedupe,example
|
||||
|
||||
trigger:
|
||||
# Deduplicate events by URL within a 5 minute window
|
||||
- name: on-new-url-dedupe
|
||||
on: event
|
||||
event:
|
||||
topic: "crawler.url"
|
||||
filters:
|
||||
- "event.data_type == 'url'"
|
||||
dedupe_key: "{{event.data.url}}"
|
||||
dedupe_window: "5m"
|
||||
input:
|
||||
type: event_data
|
||||
field: "value"
|
||||
name: target
|
||||
enabled: true
|
||||
|
||||
# Deduplicate by composite key (source + target)
|
||||
- name: on-asset-composite-dedupe
|
||||
on: event
|
||||
event:
|
||||
topic: "discovery.asset"
|
||||
dedupe_key: "{{event.source}}-{{event.data.value}}"
|
||||
dedupe_window: "1h"
|
||||
input:
|
||||
type: event_data
|
||||
field: "value"
|
||||
name: target
|
||||
enabled: true
|
||||
|
||||
params:
|
||||
- name: target
|
||||
required: true
|
||||
|
||||
steps:
|
||||
- name: log-unique
|
||||
type: function
|
||||
functions:
|
||||
- 'print_blue("Processing unique event for: {{target}}")'
|
||||
|
||||
- name: process-unique
|
||||
type: bash
|
||||
command: |
|
||||
echo "Unique target: {{target}}"
|
||||
echo "{{target}}" >> {{Output}}/unique-targets.txt
|
||||
|
||||
- name: log-done
|
||||
type: function
|
||||
functions:
|
||||
- 'print_green("Processed unique: {{target}}")'
|
||||
@@ -0,0 +1,55 @@
|
||||
# Filtered Event Receiver Workflow
|
||||
# This workflow demonstrates advanced event filtering and input extraction
|
||||
name: filtered-receiver
|
||||
kind: module
|
||||
description: Workflow with filtered event triggers and complex input extraction
|
||||
tags: event,receiver,filter,example
|
||||
|
||||
trigger:
|
||||
# Only trigger on high/critical severity findings
|
||||
- name: on-high-severity-finding
|
||||
on: event
|
||||
event:
|
||||
topic: "scan.finding"
|
||||
filters:
|
||||
- "event.data.severity == 'high' || event.data.severity == 'critical'"
|
||||
- "event.data.confirmed == true"
|
||||
input:
|
||||
type: function
|
||||
function: 'jq("{{event.data}}", ".url")'
|
||||
name: target
|
||||
enabled: true
|
||||
|
||||
# Trigger on completed scans with results
|
||||
- name: on-scan-complete
|
||||
on: event
|
||||
event:
|
||||
topic: "scan.complete"
|
||||
filters:
|
||||
- "event.data.finding_count > 0"
|
||||
input:
|
||||
type: event_data
|
||||
field: "data.target"
|
||||
name: target
|
||||
enabled: true
|
||||
|
||||
params:
|
||||
- name: target
|
||||
required: true
|
||||
|
||||
steps:
|
||||
- name: log-alert
|
||||
type: function
|
||||
functions:
|
||||
- 'print_red("ALERT: High severity finding for {{target}}")'
|
||||
|
||||
- name: notify
|
||||
type: function
|
||||
functions:
|
||||
- 'print_yellow("Sending notification for {{target}}")'
|
||||
- 'notify_webhook("High severity finding detected: {{target}}")'
|
||||
|
||||
- name: log-processed
|
||||
type: function
|
||||
functions:
|
||||
- 'print_green("Alert processed for {{target}}")'
|
||||
@@ -0,0 +1,52 @@
|
||||
# Simple Event Emitter Workflow
|
||||
# This workflow demonstrates how to emit events that can trigger other workflows
|
||||
name: simple-emitter
|
||||
kind: module
|
||||
description: Simple workflow that emits events for discovered assets
|
||||
tags: event,emitter,example
|
||||
|
||||
params:
|
||||
- name: target
|
||||
required: true
|
||||
|
||||
steps:
|
||||
- name: log-start
|
||||
type: function
|
||||
functions:
|
||||
- 'print_green("Starting discovery for {{target}}")'
|
||||
|
||||
- name: discover-assets
|
||||
type: bash
|
||||
command: |
|
||||
echo "api.{{target}}" > {{Output}}/assets.txt
|
||||
echo "www.{{target}}" >> {{Output}}/assets.txt
|
||||
echo "mail.{{target}}" >> {{Output}}/assets.txt
|
||||
|
||||
- name: emit-single-event
|
||||
type: function
|
||||
description: Emit a single event for a specific asset
|
||||
functions:
|
||||
- 'generate_event("{{Workspace}}", "discovery.asset", "simple-emitter", "subdomain", "new.{{target}}")'
|
||||
- 'print_blue("Emitted single event for new.{{target}}")'
|
||||
|
||||
- name: emit-events-from-file
|
||||
type: function
|
||||
description: Emit events for each line in a file
|
||||
functions:
|
||||
- 'set_var("count", generate_event_from_file("{{Workspace}}", "discovery.asset", "simple-emitter", "subdomain", "{{Output}}/assets.txt"))'
|
||||
- 'print_green("Emitted " + get_var("count") + " events from file")'
|
||||
|
||||
- name: emit-structured-event
|
||||
type: function
|
||||
description: Emit an event with structured JSON data
|
||||
function: |
|
||||
generate_event("{{Workspace}}", "discovery.complete", "simple-emitter", "summary", {
|
||||
target: "{{target}}",
|
||||
asset_count: 3,
|
||||
status: "completed"
|
||||
})
|
||||
|
||||
- name: log-complete
|
||||
type: function
|
||||
functions:
|
||||
- 'print_green("Discovery complete for {{target}}")'
|
||||
@@ -0,0 +1,45 @@
|
||||
# Simple Event Receiver Workflow
|
||||
# This workflow demonstrates how to receive events via triggers
|
||||
name: simple-receiver
|
||||
kind: module
|
||||
description: Simple workflow that receives and processes events
|
||||
tags: event,receiver,trigger,example
|
||||
|
||||
trigger:
|
||||
# Trigger on new asset discovery events
|
||||
- name: on-new-asset
|
||||
on: event
|
||||
event:
|
||||
topic: "discovery.asset"
|
||||
filters:
|
||||
- "event.source == 'simple-emitter'"
|
||||
- "event.data_type == 'subdomain'"
|
||||
input:
|
||||
type: event_data
|
||||
field: "value"
|
||||
name: target
|
||||
enabled: true
|
||||
- name: manual-trigger
|
||||
on: manual
|
||||
enabled: false
|
||||
|
||||
params:
|
||||
- name: target
|
||||
required: true
|
||||
|
||||
steps:
|
||||
- name: log-received
|
||||
type: function
|
||||
functions:
|
||||
- 'print_blue("Received event for target: {{target}}")'
|
||||
|
||||
- name: process-asset
|
||||
type: bash
|
||||
command: |
|
||||
echo "Processing asset: {{target}}"
|
||||
echo "{{target}}" >> {{Output}}/processed-assets.txt
|
||||
|
||||
- name: log-complete
|
||||
type: function
|
||||
functions:
|
||||
- 'print_green("Processed: {{target}}")'
|
||||
@@ -0,0 +1,83 @@
|
||||
# Vulnerability Event Emitter Workflow
|
||||
# This workflow simulates a scanner that emits vulnerability findings as events
|
||||
name: vuln-emitter
|
||||
kind: module
|
||||
description: Simulates a vulnerability scanner emitting finding events
|
||||
tags: event,emitter,vulnerability,example
|
||||
|
||||
params:
|
||||
- name: target
|
||||
required: true
|
||||
|
||||
steps:
|
||||
- name: log-start
|
||||
type: function
|
||||
functions:
|
||||
- 'print_blue("Starting vulnerability scan for {{target}}")'
|
||||
|
||||
- name: simulate-scan
|
||||
type: bash
|
||||
command: |
|
||||
echo "Scanning {{target}} for vulnerabilities..."
|
||||
sleep 1
|
||||
|
||||
- name: emit-critical-finding
|
||||
type: function
|
||||
description: Emit a critical severity finding
|
||||
function: |
|
||||
generate_event("{{Workspace}}", "scan.finding", "vuln-scanner", "vulnerability", {
|
||||
url: "https://{{target}}/admin",
|
||||
severity: "critical",
|
||||
template_id: "exposed-admin-panel",
|
||||
confirmed: true,
|
||||
matched_at: "/admin",
|
||||
description: "Admin panel exposed without authentication"
|
||||
})
|
||||
post_run:
|
||||
- 'print_red("CRITICAL: Exposed admin panel found")'
|
||||
|
||||
- name: emit-high-finding
|
||||
type: function
|
||||
description: Emit a high severity finding
|
||||
function: |
|
||||
generate_event("{{Workspace}}", "scan.finding", "vuln-scanner", "vulnerability", {
|
||||
url: "https://{{target}}/api/v1/users",
|
||||
severity: "high",
|
||||
template_id: "api-info-disclosure",
|
||||
confirmed: true,
|
||||
matched_at: "/api/v1/users",
|
||||
description: "API endpoint leaking user information"
|
||||
})
|
||||
post_run:
|
||||
- 'print_yellow("HIGH: API info disclosure found")'
|
||||
|
||||
- name: emit-low-finding
|
||||
type: function
|
||||
description: Emit a low severity finding (should not trigger filtered-receiver)
|
||||
function: |
|
||||
generate_event("{{Workspace}}", "scan.finding", "vuln-scanner", "vulnerability", {
|
||||
url: "https://{{target}}/robots.txt",
|
||||
severity: "low",
|
||||
template_id: "robots-txt-exposed",
|
||||
confirmed: true,
|
||||
matched_at: "/robots.txt",
|
||||
description: "Robots.txt file accessible"
|
||||
})
|
||||
|
||||
- name: emit-scan-complete
|
||||
type: function
|
||||
description: Emit scan completion event with summary
|
||||
function: |
|
||||
generate_event("{{Workspace}}", "scan.complete", "vuln-scanner", "summary", {
|
||||
target: "{{target}}",
|
||||
finding_count: 3,
|
||||
critical_count: 1,
|
||||
high_count: 1,
|
||||
low_count: 1,
|
||||
duration_seconds: 5
|
||||
})
|
||||
|
||||
- name: log-complete
|
||||
type: function
|
||||
functions:
|
||||
- 'print_green("Vulnerability scan complete for {{target}}")'
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
kind: flow
|
||||
name: base-flow
|
||||
description: Base flow for testing inheritance
|
||||
|
||||
params:
|
||||
- name: target
|
||||
type: string
|
||||
required: true
|
||||
|
||||
modules:
|
||||
- name: module-a
|
||||
path: base-module
|
||||
|
||||
- name: module-b
|
||||
path: base-module
|
||||
depends_on:
|
||||
- module-a
|
||||
@@ -0,0 +1,33 @@
|
||||
kind: module
|
||||
name: base-module
|
||||
description: Base module for testing inheritance
|
||||
|
||||
params:
|
||||
- name: target
|
||||
type: string
|
||||
required: true
|
||||
- name: threads
|
||||
type: string
|
||||
default: "10"
|
||||
- name: timeout
|
||||
type: string
|
||||
default: "3600"
|
||||
|
||||
dependencies:
|
||||
commands:
|
||||
- echo
|
||||
files:
|
||||
- /bin/bash
|
||||
|
||||
steps:
|
||||
- name: step-one
|
||||
type: bash
|
||||
command: echo "Step one for {{target}}"
|
||||
|
||||
- name: step-two
|
||||
type: bash
|
||||
command: echo "Step two with threads={{threads}}"
|
||||
|
||||
- name: step-three
|
||||
type: bash
|
||||
command: echo "Step three"
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
kind: module
|
||||
name: chain-a
|
||||
description: Chain test - root
|
||||
|
||||
params:
|
||||
- name: param-a
|
||||
default: "from-a"
|
||||
|
||||
steps:
|
||||
- name: step-from-a
|
||||
type: bash
|
||||
command: echo "from chain-a"
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
kind: module
|
||||
name: chain-b
|
||||
description: Chain test - middle
|
||||
extends: chain-a
|
||||
|
||||
override:
|
||||
params:
|
||||
param-a:
|
||||
default: "overridden-by-b"
|
||||
steps:
|
||||
mode: append
|
||||
steps:
|
||||
- name: step-from-b
|
||||
type: bash
|
||||
command: echo "from chain-b"
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
kind: module
|
||||
name: chain-c
|
||||
description: Chain test - leaf
|
||||
extends: chain-b
|
||||
|
||||
override:
|
||||
params:
|
||||
param-a:
|
||||
default: "final-from-c"
|
||||
steps:
|
||||
mode: append
|
||||
steps:
|
||||
- name: step-from-c
|
||||
type: bash
|
||||
command: echo "from chain-c"
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
kind: flow
|
||||
name: child-flow
|
||||
description: Child flow that extends base flow
|
||||
extends: base-flow
|
||||
|
||||
override:
|
||||
modules:
|
||||
mode: append
|
||||
modules:
|
||||
- name: module-c
|
||||
path: base-module
|
||||
depends_on:
|
||||
- module-b
|
||||
@@ -0,0 +1,18 @@
|
||||
kind: module
|
||||
name: child-merge-steps
|
||||
description: Child that merges steps (replace specific, remove some, add new)
|
||||
extends: base-module
|
||||
|
||||
override:
|
||||
steps:
|
||||
mode: merge
|
||||
steps:
|
||||
- name: step-new
|
||||
type: bash
|
||||
command: echo "New step added"
|
||||
remove:
|
||||
- step-three
|
||||
replace:
|
||||
- name: step-two
|
||||
type: bash
|
||||
command: echo "Step two replaced with new command"
|
||||
@@ -0,0 +1,15 @@
|
||||
kind: module
|
||||
name: child-override-steps
|
||||
description: Child that appends steps
|
||||
extends: base-module
|
||||
|
||||
override:
|
||||
params:
|
||||
threads:
|
||||
default: "20"
|
||||
steps:
|
||||
mode: append
|
||||
steps:
|
||||
- name: step-four
|
||||
type: bash
|
||||
command: echo "Step four added by child"
|
||||
@@ -0,0 +1,12 @@
|
||||
kind: module
|
||||
name: child-prepend-steps
|
||||
description: Child that prepends steps
|
||||
extends: base-module
|
||||
|
||||
override:
|
||||
steps:
|
||||
mode: prepend
|
||||
steps:
|
||||
- name: step-zero
|
||||
type: bash
|
||||
command: echo "Step zero prepended by child"
|
||||
@@ -0,0 +1,16 @@
|
||||
kind: module
|
||||
name: child-replace-steps
|
||||
description: Child that replaces all steps
|
||||
extends: base-module
|
||||
|
||||
override:
|
||||
steps:
|
||||
mode: replace
|
||||
steps:
|
||||
- name: new-step-one
|
||||
type: bash
|
||||
command: echo "Completely new step one"
|
||||
|
||||
- name: new-step-two
|
||||
type: bash
|
||||
command: echo "Completely new step two"
|
||||
@@ -0,0 +1,11 @@
|
||||
kind: module
|
||||
name: child-simple
|
||||
description: Simple child that only overrides params
|
||||
extends: base-module
|
||||
|
||||
override:
|
||||
params:
|
||||
threads:
|
||||
default: "5"
|
||||
timeout:
|
||||
default: "1800"
|
||||
@@ -0,0 +1,9 @@
|
||||
kind: module
|
||||
name: circular-a
|
||||
description: Circular dependency test A
|
||||
extends: circular-b
|
||||
|
||||
steps:
|
||||
- name: step-a
|
||||
type: bash
|
||||
command: echo "A"
|
||||
@@ -0,0 +1,9 @@
|
||||
kind: module
|
||||
name: circular-b
|
||||
description: Circular dependency test B
|
||||
extends: circular-a
|
||||
|
||||
steps:
|
||||
- name: step-b
|
||||
type: bash
|
||||
command: echo "B"
|
||||
@@ -0,0 +1,8 @@
|
||||
kind: flow
|
||||
name: kind-mismatch
|
||||
description: This flow tries to extend a module (should fail)
|
||||
extends: base-module
|
||||
|
||||
modules:
|
||||
- name: module-x
|
||||
path: base-module
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
name: all-errors
|
||||
kind: module
|
||||
description: A workflow with all types of lint errors for testing
|
||||
tags: test,linter,errors
|
||||
|
||||
params:
|
||||
- name: target
|
||||
required: true
|
||||
|
||||
steps:
|
||||
# Error: undefined-variable - Taget is misspelled
|
||||
- name: undefined-var-step
|
||||
type: bash
|
||||
command: echo "{{Taget}}"
|
||||
|
||||
# Error: duplicate-step-name
|
||||
- name: duplicate-step
|
||||
type: bash
|
||||
command: echo "first"
|
||||
|
||||
- name: duplicate-step
|
||||
type: bash
|
||||
command: echo "second duplicate"
|
||||
|
||||
# Error: empty-step - bash step with no command
|
||||
- name: empty-bash-step
|
||||
type: bash
|
||||
|
||||
# Error: invalid-goto - references non-existent step
|
||||
- name: decision-step
|
||||
type: bash
|
||||
command: echo "decision"
|
||||
decision:
|
||||
switch: "{{target}}"
|
||||
cases:
|
||||
"skip":
|
||||
goto: nonexistent-step
|
||||
default:
|
||||
goto: _end
|
||||
|
||||
# Error: invalid-depends-on - references non-existent step
|
||||
- name: invalid-dep-step
|
||||
type: bash
|
||||
command: echo "depends"
|
||||
depends_on:
|
||||
- step-that-does-not-exist
|
||||
|
||||
# Error: circular-dependency - creates a cycle
|
||||
- name: cycle-step-a
|
||||
type: bash
|
||||
command: echo "a"
|
||||
depends_on:
|
||||
- cycle-step-b
|
||||
|
||||
- name: cycle-step-b
|
||||
type: bash
|
||||
command: echo "b"
|
||||
depends_on:
|
||||
- cycle-step-a
|
||||
@@ -0,0 +1,33 @@
|
||||
name: valid-workflow
|
||||
kind: module
|
||||
description: A valid workflow with no lint issues
|
||||
tags: test,linter
|
||||
|
||||
params:
|
||||
- name: target
|
||||
required: true
|
||||
- name: threads
|
||||
default: 10
|
||||
|
||||
steps:
|
||||
- name: step-one
|
||||
type: bash
|
||||
command: echo "Target is {{target}}"
|
||||
timeout: 5m
|
||||
exports:
|
||||
output_one: "output"
|
||||
|
||||
- name: step-two
|
||||
type: bash
|
||||
command: echo "Output is {{output_one}}"
|
||||
depends_on:
|
||||
- step-one
|
||||
timeout: 5m
|
||||
exports:
|
||||
final_output: "output"
|
||||
|
||||
- name: step-three
|
||||
type: function
|
||||
function: 'log_info("Final: {{final_output}}")'
|
||||
depends_on:
|
||||
- step-two
|
||||
@@ -0,0 +1,22 @@
|
||||
name: warnings-only
|
||||
kind: module
|
||||
description: A workflow with only warning-level issues
|
||||
tags: test,linter,warnings
|
||||
|
||||
params:
|
||||
- name: target
|
||||
required: true
|
||||
|
||||
steps:
|
||||
# Warning: unused-variable - exported but never used
|
||||
- name: step-with-unused-export
|
||||
type: bash
|
||||
command: echo "Hello {{target}}"
|
||||
exports:
|
||||
unused_output: "output"
|
||||
another_unused: "also_unused"
|
||||
|
||||
- name: final-step
|
||||
type: bash
|
||||
command: echo "done"
|
||||
timeout: 5m
|
||||
+1
-1
@@ -24,7 +24,7 @@ steps:
|
||||
|
||||
- name: verify-exports-and-params
|
||||
type: bash
|
||||
pre_condition: 'fileExists("{{anotherParam3as1}}")'
|
||||
pre_condition: 'file_exists("{{anotherParam3as1}}")'
|
||||
command: |
|
||||
echo "Module 2: paramFromFlowFile={{paramFromFlowFile}}"
|
||||
echo "Module 2: module1_completed={{module1_completed}}"
|
||||
|
||||
@@ -21,7 +21,7 @@ steps:
|
||||
# Foreach with templated input path
|
||||
- name: foreach-with-templates
|
||||
type: foreach
|
||||
pre_condition: 'fileExists("{{inputFile}}")'
|
||||
pre_condition: 'file_exists("{{inputFile}}")'
|
||||
input: "{{inputFile}}"
|
||||
variable: item
|
||||
threads: 2
|
||||
@@ -34,7 +34,7 @@ steps:
|
||||
# Verify results
|
||||
- name: verify-foreach
|
||||
type: bash
|
||||
pre_condition: 'fileExists("{{outputDir}}/item1.txt")'
|
||||
pre_condition: 'file_exists("{{outputDir}}/item1.txt")'
|
||||
command: |
|
||||
echo "=== Foreach Verification ==="
|
||||
echo "Foreach completed for {{Target}}"
|
||||
|
||||
@@ -36,7 +36,7 @@ steps:
|
||||
|
||||
- name: verify-parallel
|
||||
type: bash
|
||||
pre_condition: 'fileExists("{{parallelOutput}}/bash1.txt")'
|
||||
pre_condition: 'file_exists("{{parallelOutput}}/bash1.txt")'
|
||||
command: |
|
||||
echo "=== Parallel Steps Verification ==="
|
||||
echo "parallel_dir={{parallel_dir}}"
|
||||
|
||||
@@ -26,7 +26,7 @@ steps:
|
||||
# 2. Function step with templated function calls
|
||||
- name: function-with-templates
|
||||
type: function
|
||||
pre_condition: 'fileExists("{{bash_output_path}}")'
|
||||
pre_condition: 'file_exists("{{bash_output_path}}")'
|
||||
function: 'log_info("Processing {{Target}} with path {{customPath}}")'
|
||||
exports:
|
||||
function_result: "processed-{{Target}}"
|
||||
@@ -50,7 +50,7 @@ steps:
|
||||
# 5. Final verification step
|
||||
- name: verify-all-templates
|
||||
type: bash
|
||||
pre_condition: 'fileExists("{{customPath}}")'
|
||||
pre_condition: 'file_exists("{{customPath}}")'
|
||||
command: |
|
||||
echo "=== Template Rendering Verification ==="
|
||||
echo "bash_output_path={{bash_output_path}}"
|
||||
|
||||
+3
-3
@@ -52,7 +52,7 @@ steps:
|
||||
## String Functions Demo
|
||||
|
||||
```osm-func
|
||||
"Uppercase target: " + toUpperCase("{{Target}}")
|
||||
"Uppercase target: " + to_upper_case("{{Target}}")
|
||||
```
|
||||
|
||||
```osm-func
|
||||
@@ -73,7 +73,7 @@ steps:
|
||||
uuid()
|
||||
``` |
|
||||
| Random String | ```osm-func
|
||||
randomString(8)
|
||||
random_string(8)
|
||||
``` |
|
||||
| Contains 'example' | ```osm-func
|
||||
contains("{{Target}}", "example")
|
||||
@@ -100,7 +100,7 @@ steps:
|
||||
# Step 4: Verify the report was created
|
||||
- name: verify-report
|
||||
type: function
|
||||
function: 'fileExists("{{Output}}/security-report.md")'
|
||||
function: 'file_exists("{{Output}}/security-report.md")'
|
||||
|
||||
# Step 5: Display report path
|
||||
- name: show-report-path
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
name: test-cdn-functions
|
||||
kind: module
|
||||
description: Test CDN/Storage utility functions
|
||||
tags: test,cdn,storage,functions
|
||||
|
||||
params:
|
||||
- name: target
|
||||
required: true
|
||||
|
||||
steps:
|
||||
# Test cdn_list with empty prefix (should return empty array when no storage configured)
|
||||
- name: test-cdn-list-empty
|
||||
type: function
|
||||
function: cdn_list("")
|
||||
exports:
|
||||
list_result: "output"
|
||||
|
||||
# Test cdn_list with prefix
|
||||
- name: test-cdn-list-prefix
|
||||
type: function
|
||||
function: cdn_list("scans/")
|
||||
exports:
|
||||
list_prefix_result: "output"
|
||||
|
||||
# Test cdn_stat with non-existent file (should return null)
|
||||
- name: test-cdn-stat-missing
|
||||
type: function
|
||||
function: cdn_stat("nonexistent/file.txt")
|
||||
exports:
|
||||
stat_result: "output"
|
||||
|
||||
# Test cdn_exists with non-existent file
|
||||
- name: test-cdn-exists-missing
|
||||
type: function
|
||||
function: cdn_exists("nonexistent/file.txt")
|
||||
exports:
|
||||
exists_result: "output"
|
||||
|
||||
# Test cdn_get_presigned_url with empty path (should return empty string)
|
||||
- name: test-cdn-presigned-empty
|
||||
type: function
|
||||
function: cdn_get_presigned_url("")
|
||||
exports:
|
||||
presigned_empty: "output"
|
||||
|
||||
# Test cdn_get_presigned_url with path and expiry
|
||||
- name: test-cdn-presigned-with-expiry
|
||||
type: function
|
||||
function: cdn_get_presigned_url("test/file.txt", 60)
|
||||
exports:
|
||||
presigned_result: "output"
|
||||
|
||||
# Test cdn_sync_upload with empty directory (should handle gracefully)
|
||||
- name: test-cdn-sync-upload-empty
|
||||
type: function
|
||||
function: cdn_sync_upload("", "remote/prefix/")
|
||||
exports:
|
||||
sync_upload_empty: "output"
|
||||
|
||||
# Test cdn_sync_download with empty local directory (should handle gracefully)
|
||||
- name: test-cdn-sync-download-empty
|
||||
type: function
|
||||
function: cdn_sync_download("remote/prefix/", "")
|
||||
exports:
|
||||
sync_download_empty: "output"
|
||||
|
||||
# Test cdn_upload with empty paths
|
||||
- name: test-cdn-upload-empty
|
||||
type: function
|
||||
function: cdn_upload("", "")
|
||||
exports:
|
||||
upload_empty: "output"
|
||||
|
||||
# Test cdn_download with empty paths
|
||||
- name: test-cdn-download-empty
|
||||
type: function
|
||||
function: cdn_download("", "")
|
||||
exports:
|
||||
download_empty: "output"
|
||||
|
||||
# Test cdn_delete with empty path
|
||||
- name: test-cdn-delete-empty
|
||||
type: function
|
||||
function: cdn_delete("")
|
||||
exports:
|
||||
delete_empty: "output"
|
||||
|
||||
# Create local test directory for sync testing
|
||||
- name: create-test-dir
|
||||
type: bash
|
||||
command: |
|
||||
mkdir -p /tmp/cdn-test-{{target}}
|
||||
echo "file1 content" > /tmp/cdn-test-{{target}}/file1.txt
|
||||
echo "file2 content" > /tmp/cdn-test-{{target}}/file2.txt
|
||||
mkdir -p /tmp/cdn-test-{{target}}/subdir
|
||||
echo "subfile content" > /tmp/cdn-test-{{target}}/subdir/subfile.txt
|
||||
|
||||
# Test cdn_sync_upload with actual directory (will fail if no storage, but should not crash)
|
||||
- name: test-cdn-sync-upload-dir
|
||||
type: function
|
||||
function: cdn_sync_upload("/tmp/cdn-test-{{target}}", "test-sync/{{target}}/")
|
||||
exports:
|
||||
sync_upload_result: "output"
|
||||
|
||||
# Test cdn_sync_download to actual directory
|
||||
- name: test-cdn-sync-download-dir
|
||||
type: function
|
||||
function: cdn_sync_download("test-sync/{{target}}/", "/tmp/cdn-download-{{target}}")
|
||||
exports:
|
||||
sync_download_result: "output"
|
||||
|
||||
# Cleanup
|
||||
- name: cleanup
|
||||
type: bash
|
||||
command: |
|
||||
rm -rf /tmp/cdn-test-{{target}}
|
||||
rm -rf /tmp/cdn-download-{{target}}
|
||||
|
||||
# Log results summary
|
||||
- name: log-results
|
||||
type: function
|
||||
function: |
|
||||
log_info("CDN Function Test Results:");
|
||||
log_info(" cdn_list empty: " + JSON.stringify({{list_result}}));
|
||||
log_info(" cdn_stat missing: " + ({{stat_result}} === null ? "null" : JSON.stringify({{stat_result}})));
|
||||
log_info(" cdn_exists missing: " + {{exists_result}});
|
||||
log_info(" cdn_sync_upload result: " + JSON.stringify({{sync_upload_result}}));
|
||||
log_info(" cdn_sync_download result: " + JSON.stringify({{sync_download_result}}));
|
||||
true
|
||||
@@ -87,7 +87,7 @@ steps:
|
||||
- name: verify-ports-file
|
||||
type: function
|
||||
log: "Verifying ports file exists"
|
||||
function: fileExists("{{ports_file}}")
|
||||
function: file_exists("{{ports_file}}")
|
||||
exports:
|
||||
ports_verified: "output"
|
||||
|
||||
@@ -150,7 +150,7 @@ steps:
|
||||
- name: read-subdomain-results
|
||||
type: function
|
||||
log: "Reading subdomain scan results"
|
||||
function: readFile("{{output_dir}}/subdomain-results.txt")
|
||||
function: read_file("{{output_dir}}/subdomain-results.txt")
|
||||
exports:
|
||||
scan_results: "output"
|
||||
|
||||
@@ -200,8 +200,8 @@ steps:
|
||||
type: function
|
||||
log: "Getting file statistics"
|
||||
parallel_functions:
|
||||
- fileLength("{{output_dir}}/report.txt")
|
||||
- fileExists("{{output_dir}}/all-ports.txt")
|
||||
- file_length("{{output_dir}}/report.txt")
|
||||
- file_exists("{{output_dir}}/all-ports.txt")
|
||||
- trim(" {{target}} ")
|
||||
exports:
|
||||
file_stats: "output"
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
name: test-debounce-dedupe
|
||||
kind: module
|
||||
desc: Test workflow for debounce and deduplication features
|
||||
|
||||
params:
|
||||
- name: output
|
||||
default: "/tmp/debounce-dedupe-test"
|
||||
|
||||
triggers:
|
||||
# Watch trigger with debounce - waits 500ms after last change before triggering
|
||||
- name: on-asset-change
|
||||
on: watch
|
||||
path: "{{output}}/assets.txt"
|
||||
debounce: 500ms
|
||||
enabled: true
|
||||
input:
|
||||
type: file
|
||||
path: "{{output}}/assets.txt"
|
||||
|
||||
# Event trigger with deduplication - ignores duplicate events within 10s window
|
||||
- name: on-new-asset
|
||||
on: event
|
||||
event:
|
||||
topic: "assets.new"
|
||||
dedupe_key: "{{event.source}}-{{event.data.url}}"
|
||||
dedupe_window: 10s
|
||||
enabled: true
|
||||
input:
|
||||
type: event_data
|
||||
field: url
|
||||
name: target
|
||||
|
||||
# Event trigger with filters and deduplication
|
||||
- name: on-critical-vuln
|
||||
on: event
|
||||
event:
|
||||
topic: "vulnerabilities.new"
|
||||
filters:
|
||||
- "event.data.severity == 'critical'"
|
||||
dedupe_key: "{{event.data.template}}-{{event.data.host}}"
|
||||
dedupe_window: 5m
|
||||
enabled: true
|
||||
input:
|
||||
type: event_data
|
||||
field: host
|
||||
name: target
|
||||
|
||||
steps:
|
||||
- name: log-event
|
||||
type: function
|
||||
call: log_info("Trigger fired: {{_step_name}}")
|
||||
|
||||
- name: process-target
|
||||
type: bash
|
||||
run: echo "Processing target: {{target}}"
|
||||
+1
-1
@@ -169,7 +169,7 @@ steps:
|
||||
- name: check-alive-hosts
|
||||
type: function
|
||||
log: "Checking alive hosts count"
|
||||
function: fileLength("{{Output}}/enumeration/httpx.json")
|
||||
function: file_length("{{Output}}/enumeration/httpx.json")
|
||||
exports:
|
||||
alive_count: "output"
|
||||
decision:
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
name: test-event-chain-stage1
|
||||
kind: module
|
||||
description: Stage 1 of event chain - discovery
|
||||
tags: test,event,chain
|
||||
|
||||
params:
|
||||
- name: target
|
||||
required: true
|
||||
|
||||
steps:
|
||||
- name: discover
|
||||
type: bash
|
||||
command: |
|
||||
echo "Discovering subdomains for {{target}}..."
|
||||
echo "api.{{target}}" > {{Output}}/discovered.txt
|
||||
echo "www.{{target}}" >> {{Output}}/discovered.txt
|
||||
|
||||
- name: emit-discoveries
|
||||
type: function
|
||||
function: |
|
||||
generate_event_from_file("stage1.complete", "chain-stage1", "subdomain", "{{Output}}/discovered.txt")
|
||||
log_info("Stage 1 complete - emitted discovery events")
|
||||
@@ -0,0 +1,34 @@
|
||||
name: test-event-chain-stage2
|
||||
kind: module
|
||||
description: Stage 2 of event chain - probing (triggered by stage1)
|
||||
tags: test,event,chain
|
||||
|
||||
trigger:
|
||||
- name: on-stage1-complete
|
||||
on: event
|
||||
event:
|
||||
topic: "stage1.complete"
|
||||
filters:
|
||||
- "event.source == 'chain-stage1'"
|
||||
input:
|
||||
type: event_data
|
||||
field: "value"
|
||||
name: target
|
||||
enabled: true
|
||||
|
||||
params:
|
||||
- name: target
|
||||
required: true
|
||||
|
||||
steps:
|
||||
- name: probe
|
||||
type: bash
|
||||
command: |
|
||||
echo "Probing {{target}}..."
|
||||
echo "{{target}} is live" > {{Output}}/probed.txt
|
||||
|
||||
- name: emit-probe-complete
|
||||
type: function
|
||||
function: |
|
||||
generate_event("{{Workspace}}", "stage2.complete", "chain-stage2", "probed-host", "{{target}}")
|
||||
log_info("Stage 2 complete - probed {{target}}")
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
name: test-event-emitter
|
||||
kind: module
|
||||
description: Test workflow that emits events
|
||||
tags: test,event,emitter
|
||||
|
||||
params:
|
||||
- name: target
|
||||
required: true
|
||||
|
||||
steps:
|
||||
- name: generate-subdomains
|
||||
type: bash
|
||||
command: |
|
||||
echo "api.{{target}}" > {{Output}}/subdomains.txt
|
||||
echo "www.{{target}}" >> {{Output}}/subdomains.txt
|
||||
echo "admin.{{target}}" >> {{Output}}/subdomains.txt
|
||||
|
||||
- name: emit-single-event
|
||||
type: function
|
||||
function: |
|
||||
generate_event("{{Workspace}}", "assets.new", "test-emitter", "subdomain", "single.{{target}}")
|
||||
|
||||
- name: emit-events-from-file
|
||||
type: function
|
||||
function: |
|
||||
generate_event_from_file("{{Workspace}}", "assets.new", "test-emitter", "subdomain", "{{Output}}/subdomains.txt")
|
||||
exports:
|
||||
emitted_count: "{{result}}"
|
||||
|
||||
- name: emit-complex-event
|
||||
type: function
|
||||
function: |
|
||||
generate_event("{{Workspace}}", "vulnerabilities.new", "test-scanner", "finding", {
|
||||
url: "https://{{target}}/admin",
|
||||
severity: "high",
|
||||
template: "exposed-admin-panel",
|
||||
matched: "/admin"
|
||||
})
|
||||
|
||||
- name: log-result
|
||||
type: function
|
||||
function: |
|
||||
log_info("Emitted events for {{target}}, file event count: " + "{{emitted_count}}")
|
||||
@@ -0,0 +1,50 @@
|
||||
name: test-event-receiver
|
||||
kind: module
|
||||
description: Test workflow that receives events via trigger
|
||||
tags: test,event,receiver,trigger
|
||||
|
||||
trigger:
|
||||
- name: on-new-subdomain
|
||||
on: event
|
||||
event:
|
||||
topic: "assets.new"
|
||||
filters:
|
||||
- "event.source == 'test-emitter'"
|
||||
- "event.data_type == 'subdomain'"
|
||||
input:
|
||||
type: event_data
|
||||
field: "value"
|
||||
name: target
|
||||
enabled: true
|
||||
|
||||
- name: on-vulnerability
|
||||
on: event
|
||||
event:
|
||||
topic: "vulnerabilities.new"
|
||||
filters:
|
||||
- "event.source == 'test-scanner'"
|
||||
- "event.data.severity == 'high' || event.data.severity == 'critical'"
|
||||
input:
|
||||
type: function
|
||||
function: 'jq("{{event.data}}", ".url")'
|
||||
name: target
|
||||
enabled: true
|
||||
|
||||
params:
|
||||
- name: target
|
||||
required: true
|
||||
|
||||
steps:
|
||||
- name: log-received
|
||||
type: function
|
||||
function: |
|
||||
log_info("Event received for target: {{target}}")
|
||||
|
||||
- name: process-target
|
||||
type: bash
|
||||
command: echo "Processing event target: {{target}}" > {{Output}}/processed.txt
|
||||
|
||||
- name: mark-complete
|
||||
type: function
|
||||
function: |
|
||||
appendFile("{{Output}}/events.log", "Processed: {{target}}\n")
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
name: test-exports-functions
|
||||
kind: module
|
||||
description: Test exports with utility functions like fileLength, contains, fileExists, replace
|
||||
description: Test exports with utility functions like file_length, contains, file_exists, replace
|
||||
tags: test,exports,functions,utility
|
||||
|
||||
params:
|
||||
@@ -29,13 +29,13 @@ steps:
|
||||
type: bash
|
||||
command: echo "Checking file length"
|
||||
exports:
|
||||
line_count: "fileLength('{{outputFile}}')"
|
||||
line_count: "file_length('{{outputFile}}')"
|
||||
|
||||
- name: test-trim
|
||||
type: bash
|
||||
command: echo "Checking trim"
|
||||
exports:
|
||||
trimmed_output: "trim(readFile('{{trimTestFile}}'))"
|
||||
trimmed_output: "trim(read_file('{{trimTestFile}}'))"
|
||||
|
||||
- name: test-contains-success
|
||||
type: bash
|
||||
@@ -53,19 +53,19 @@ steps:
|
||||
type: bash
|
||||
command: echo "Checking existence"
|
||||
exports:
|
||||
file_exists: "fileExists('{{outputFile}}')"
|
||||
file_exists: "file_exists('{{outputFile}}')"
|
||||
|
||||
- name: test-fileexists-false
|
||||
type: bash
|
||||
command: echo "Checking nonexistent"
|
||||
exports:
|
||||
missing_file: "fileExists('{{Output}}/nonexistent.txt')"
|
||||
missing_file: "file_exists('{{Output}}/nonexistent.txt')"
|
||||
|
||||
- name: test-replace
|
||||
type: bash
|
||||
command: echo "Checking replace"
|
||||
exports:
|
||||
replaced_output: "replace(readFile('{{replaceTestFile}}'), ',', '-')"
|
||||
replaced_output: "replace(read_file('{{replaceTestFile}}'), ',', '-')"
|
||||
|
||||
- name: final-summary
|
||||
type: bash
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
name: test-extends-aggressive
|
||||
kind: module
|
||||
description: Aggressive variant - extends base with higher resource usage
|
||||
tags: test,extends,aggressive
|
||||
extends: test-extends-base
|
||||
|
||||
override:
|
||||
params:
|
||||
threads:
|
||||
default: "50"
|
||||
rate_limit:
|
||||
default: "500"
|
||||
verbose:
|
||||
default: "true"
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
name: test-extends-base
|
||||
kind: module
|
||||
description: Base workflow for testing extends functionality
|
||||
tags: test,extends,base
|
||||
|
||||
params:
|
||||
- name: target
|
||||
required: true
|
||||
- name: threads
|
||||
type: string
|
||||
default: "10"
|
||||
- name: rate_limit
|
||||
type: string
|
||||
default: "100"
|
||||
- name: verbose
|
||||
type: string
|
||||
default: "false"
|
||||
|
||||
steps:
|
||||
- name: show-config
|
||||
type: bash
|
||||
command: echo "Target={{target}} Threads={{threads}} RateLimit={{rate_limit}} Verbose={{verbose}}"
|
||||
|
||||
- name: scan-step
|
||||
type: bash
|
||||
command: echo "Running scan with {{threads}} threads at rate {{rate_limit}}"
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
name: test-extends-fast
|
||||
kind: module
|
||||
description: Fast variant - extends base with lower resource usage
|
||||
tags: test,extends,fast
|
||||
extends: test-extends-base
|
||||
|
||||
override:
|
||||
params:
|
||||
threads:
|
||||
default: "5"
|
||||
rate_limit:
|
||||
default: "50"
|
||||
+2
-2
@@ -17,13 +17,13 @@ steps:
|
||||
|
||||
- name: check-file
|
||||
type: function
|
||||
function: fileExists("/tmp/test-{{target}}.txt")
|
||||
function: file_exists("/tmp/test-{{target}}.txt")
|
||||
exports:
|
||||
exists: "output"
|
||||
|
||||
- name: read-file
|
||||
type: function
|
||||
function: readFile("/tmp/test-{{target}}.txt")
|
||||
function: read_file("/tmp/test-{{target}}.txt")
|
||||
exports:
|
||||
content: "output"
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
name: test-multi-type-variable
|
||||
kind: module
|
||||
description: Test comma-separated types in dependencies.variables
|
||||
tags: test,dependencies,multi-type
|
||||
|
||||
params:
|
||||
- name: target
|
||||
required: true
|
||||
|
||||
dependencies:
|
||||
variables:
|
||||
- name: Target
|
||||
type: domain,url
|
||||
required: true
|
||||
|
||||
steps:
|
||||
- name: echo-ok
|
||||
type: bash
|
||||
command: echo "OK for {{target}}"
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
name: test-notification
|
||||
kind: module
|
||||
description: Test notification triggers
|
||||
tags: test,notification
|
||||
|
||||
params:
|
||||
- name: target
|
||||
required: true
|
||||
|
||||
steps:
|
||||
- name: trigger-step-started
|
||||
type: function
|
||||
function: |
|
||||
send_webhook_event("step_started", {step: "trigger-step-started", target: "{{target}}"})
|
||||
|
||||
- name: echo-test
|
||||
type: bash
|
||||
command: echo "Testing notifications for {{target}}"
|
||||
|
||||
- name: trigger-step-complete
|
||||
type: function
|
||||
function: |
|
||||
send_webhook_event("step_complete", {step: "echo-test", target: "{{target}}"})
|
||||
notify_webhook("Workflow completed for {{target}}")
|
||||
+1
-1
@@ -17,7 +17,7 @@ steps:
|
||||
parallel_functions:
|
||||
- trim(" hello ")
|
||||
- contains("hello world", "world")
|
||||
- fileExists("/tmp/parallel-func-test.txt")
|
||||
- file_exists("/tmp/parallel-func-test.txt")
|
||||
exports:
|
||||
func_results: "output"
|
||||
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ steps:
|
||||
|
||||
- name: verify-files
|
||||
type: function
|
||||
function: fileExists("/tmp/parallel-steps-test/sub1.txt")
|
||||
function: file_exists("/tmp/parallel-steps-test/sub1.txt")
|
||||
exports:
|
||||
file_exists: "output"
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
name: test-step-dependencies
|
||||
kind: module
|
||||
description: Test DAG-style step dependencies
|
||||
|
||||
params:
|
||||
- name: target
|
||||
required: true
|
||||
|
||||
steps:
|
||||
- name: step-a
|
||||
type: bash
|
||||
command: echo "A completed"
|
||||
|
||||
- name: step-b
|
||||
type: bash
|
||||
command: echo "B completed"
|
||||
depends_on: [step-a]
|
||||
|
||||
- name: step-c
|
||||
type: bash
|
||||
command: echo "C completed"
|
||||
depends_on: [step-a]
|
||||
|
||||
- name: step-d
|
||||
type: bash
|
||||
command: echo "D completed"
|
||||
depends_on: [step-b, step-c]
|
||||
Reference in New Issue
Block a user