Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions internal/campaign/campaign.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@ func ParseYAML(data []byte) (*Campaign, error) {
if s.DependsOn == s.ID {
return nil, fmt.Errorf("campaign %q step %q: self-dependency", c.Name, s.ID)
}
// A when clause is evaluated against the predecessor's output, so it is
// only meaningful with a depends_on. Without one, the Runner never
// reaches the when check and the step runs UNCONDITIONALLY — an operator
// gating a destructive tool with `when:` would get a silently-inert
// guard (fail-open). Reject at load so the misconfiguration surfaces
// loudly instead of running the tool.
if s.When != "" && s.DependsOn == "" {
return nil, fmt.Errorf("campaign %q step %q: when clause requires depends_on (a when is evaluated against the predecessor's output; without one the step would run unconditionally)", c.Name, s.ID)
}
}

// Third pass: declaration-order check. The Runner iterates steps in
Expand Down
20 changes: 20 additions & 0 deletions internal/campaign/campaign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -513,3 +513,23 @@ func (r *recordingExecutor) Run(ctx context.Context, tool string, _ map[string]i
}
return "ok", nil
}

// TestParseYAML_RejectsWhenWithoutDependsOn pins the fail-closed fix for the
// inert-gate bug: a `when` clause is evaluated against the predecessor's output,
// so without a depends_on the Runner never reaches the when check and the step
// runs UNCONDITIONALLY. An operator gating a destructive tool with `when:` would
// otherwise get a silently-inert guard. Such a campaign must be rejected at load.
func TestParseYAML_RejectsWhenWithoutDependsOn(t *testing.T) {
yamlDoc := `campaign: ungated-when
steps:
- id: scan
tool: wifi_scan_ap
- id: gated
tool: wifi_sniff_pmkid
when: contains "AUTHORIZED"
`
_, err := ParseYAML([]byte(yamlDoc))
if err == nil || !strings.Contains(err.Error(), "when clause requires depends_on") {
t.Errorf("a when without depends_on must be rejected (else the gate runs unconditionally): %v", err)
}
}
Loading