Skip to content
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

## [Unreleased]

### Changed

- **Workflow support is temporarily deprecated** ([#23](https://github.com/willwebster5/talonctl/issues/23)).
The workflow provider was non-functional: it called FalconPy `Workflows` methods
that don't exist (`delete_definition`, `get_definitions`), and the CrowdStrike
Workflows API has no workflow-definition delete plus an `update_definition` that
500s — so the provider's delete+recreate strategy was structurally impossible and
any workflow change crashed `apply` (and `sync`/`drift` raised `AttributeError`).
Workflow templates are now discovered with a clear deprecation warning and skipped
by `validate`/`plan`/`apply`/`sync`/`drift`; `talonctl init` no longer scaffolds a
`workflows/` directory. The provider code and example are retained (with deprecation
notices) for a future, live-tenant-tested rewrite.

## v0.5.5 — lookup `source:` resolves against the template's project root

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Pip-installable CLI tool for CrowdStrike NGSIEM infrastructure as code. Terrafor

This repo is the **tool** -- a pip-installable Python package. It does not contain detection templates, knowledge bases, or project-specific content. Those live in user projects (e.g., [talonctl-demo](https://github.com/willwebster5/talonctl-demo)).

- **Seven resource types** -- detections, saved searches, dashboards, workflows, lookup files, RTR scripts, RTR put files
- **Six resource types** -- detections, saved searches, dashboards, lookup files, RTR scripts, RTR put files (workflows temporarily deprecated — see issue #23)
- **Terraform-like lifecycle** -- validate, plan, apply, import, sync, drift
- **State management** -- tracks deployed resources and their CrowdStrike API IDs
- **Scaffolding** -- `talonctl init` creates new projects with the correct directory structure
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ A pip-installable CLI tool for managing CrowdStrike NGSIEM resources as code. It

What you get:
- **Terraform-like deployment** -- plan/apply/import/drift/sync for CrowdStrike NGSIEM resources
- **Seven resource types** -- detections, saved searches, dashboards, workflows, lookup files, RTR scripts, RTR put files
- **Six resource types** -- detections, saved searches, dashboards, lookup files, RTR scripts, RTR put files
- **Workflows are temporarily deprecated** -- see [issue #23](https://github.com/willwebster5/talonctl/issues/23); the CrowdStrike Workflows API has no delete and a non-working update, so workflow templates are ignored until support is rebuilt and tested.
- **State management** -- tracks deployed resources, content hashes, and CrowdStrike API IDs
- **Dependency resolution** -- DAG-based ordering so resources deploy in the right sequence
- **Drift detection** -- catch manual console changes that diverge from your templates
Expand Down Expand Up @@ -86,7 +87,7 @@ talonctl backup restore <tag> # Restore from backup
| Detection | `resources/detections/` | Correlation rules (CQL queries with severity, MITRE mapping) |
| Saved Search | `resources/saved_searches/` | Reusable CQL functions called with `$function_name()` |
| Dashboard | `resources/dashboards/` | LogScale dashboards with sections and widgets |
| Workflow | `resources/workflows/` | Falcon Fusion automation workflows |
| Workflow | `resources/workflows/` | Falcon Fusion automation workflows _(temporarily deprecated — #23)_ |
| Lookup File | `resources/lookup_files/` | CSV lookup tables for enrichment |
| RTR Script | `resources/rtr_scripts/` | Real Time Response scripts |
| RTR Put File | `resources/rtr_put_files/` | Files pushed to endpoints via RTR |
Expand All @@ -107,7 +108,7 @@ talonctl backup restore <tag> # Restore from backup
| Saved Search | `ngsiem:read` | `ngsiem:write` |
| Dashboard | `ngsiem:read` | `ngsiem:write` |
| Lookup File | `ngsiem:read` | `ngsiem:write` |
| Workflow | `workflow:read` | `workflow:write` |
| Workflow | `workflow:read` | `workflow:write` _(deprecated — #23)_ |
| RTR Script | `real-time-response-admin:write` | `real-time-response-admin:write` |
| RTR Put File | `real-time-response-admin:write` | `real-time-response-admin:write` |

Expand Down
2 changes: 1 addition & 1 deletion examples/resources/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ One example of each resource type managed by the talonctl IaC engine.
| `saved_search_function.yaml` | Saved Search | 90-day baseline builder function (real, deployable) |
| `saved_search_hunting.yaml` | Saved Search | Certutil LOLBIN hunting query (real, deployable) |
| `lookup_file.yaml` | Lookup File | Tor exit node IP list metadata (needs CSV data) |
| `workflow.yaml` | Workflow | Notification workflow (synthetic — shows format) |
| `workflow.yaml` | Workflow | Notification workflow (synthetic — shows format) _(temporarily deprecated — see issue #23)_ |
| `rtr_script.yaml` | RTR Script | Windows service lister (needs PowerShell file) |
| `rtr_put_file.yaml` | RTR Put File | Config file push (synthetic — shows format) |

Expand Down
5 changes: 5 additions & 0 deletions examples/resources/workflow.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# Example: Falcon Fusion Workflow
#
# DEPRECATED (issue #23): talonctl workflow support is temporarily deprecated.
# This file is kept for format reference only — workflow templates are ignored
# by validate/plan/apply/sync/drift until support is rebuilt and tested.
#
# A simple notification workflow triggered by a high-severity detection.
# NOTE: No workflows existed in the source repo — this is a synthetic example
# showing the YAML template format the IaC engine expects.
Expand Down
2 changes: 1 addition & 1 deletion src/talonctl/commands/find.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
_RESOURCE_TYPES = [
"detection",
"saved_search",
"workflow",
# workflow support is temporarily deprecated (#23) — not an active filter choice
"lookup_file",
"rtr_script",
"rtr_put_file",
Expand Down
1 change: 0 additions & 1 deletion src/talonctl/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"detections",
"saved_searches",
"dashboards",
"workflows",
"lookup_files",
"rtr_scripts",
"rtr_put_files",
Expand Down
2 changes: 1 addition & 1 deletion src/talonctl/core/drift_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class DriftDetector:
"""

# Resource types that support bulk remote fetch
FETCHABLE_TYPES = {"detection", "saved_search", "rtr_script", "rtr_put_file", "workflow", "lookup_file"}
FETCHABLE_TYPES = {"detection", "saved_search", "rtr_script", "rtr_put_file", "lookup_file"}

def __init__(self, falcon_client: Any, state_manager: Any, provider_adapter: Any, template_discovery: Any):
self.falcon = falcon_client
Expand Down
3 changes: 0 additions & 3 deletions src/talonctl/core/provider_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ def __init__(
# Initialize providers (lazy import to avoid circular dependency)
from talonctl.providers import (
DetectionProvider,
WorkflowProvider,
SavedSearchProvider,
LookupFileProvider,
RTRScriptProvider,
Expand All @@ -62,7 +61,6 @@ def __init__(
provider_config = {"credentials": credentials} if credentials else {}

self.detection_provider = DetectionProvider(falcon_client, config=provider_config)
self.workflow_provider = WorkflowProvider(falcon_client)
self.saved_search_provider = SavedSearchProvider(falcon_client)
self.lookup_file_provider = LookupFileProvider(falcon_client)

Expand All @@ -74,7 +72,6 @@ def __init__(
# Provider registry
self.providers: Dict[str, BaseResourceProvider] = {
"detection": self.detection_provider,
"workflow": self.workflow_provider,
"saved_search": self.saved_search_provider,
"lookup_file": self.lookup_file_provider,
"rtr_script": self.rtr_script_provider,
Expand Down
21 changes: 20 additions & 1 deletion src/talonctl/core/template_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,20 @@ class TemplateDiscovery:
# Valid resource types
VALID_RESOURCE_TYPES = [
"detection",
"workflow",
"saved_search",
"lookup_file",
"rtr_script",
"rtr_put_file",
"dashboard",
]

# Resource types whose talonctl support is temporarily deprecated. Templates
# of these types are still scanned (so we can warn) but excluded from results
# and never reach plan/apply/sync/drift. See issue #23: the workflow provider
# is non-functional (CrowdStrike Workflows API has no delete, update_definition
# 500s) and cannot be validated without live-tenant access.
DEPRECATED_RESOURCE_TYPES = {"workflow"}

# Resource type -> on-disk directory name.
TYPE_TO_DIR = {
"detection": "detections",
Expand Down Expand Up @@ -104,6 +110,9 @@ def __init__(self, resources_dir: Optional[Path] = None, project_root: Optional[
self.resources_dir = Path(resources_dir)
self.project_root = Path(project_root)
self._template_cache: Dict[str, DiscoveredTemplate] = {}
# Deprecated resource types already warned about this discovery pass
# (keeps the warning to once-per-run instead of once-per-file).
self._warned_deprecated: Set[str] = set()

def _find_project_root(self) -> Path:
"""Find project root directory by walking up from CWD looking for .crowdstrike/."""
Expand Down Expand Up @@ -167,6 +176,7 @@ def _discover_all_templates(self) -> List[DiscoveredTemplate]:
"""Recursively discover every resource under resources/, routing each by
its type (v2 ``kind``; v1 top-level directory)."""
templates: List[DiscoveredTemplate] = []
self._warned_deprecated = set()
if not self.resources_dir.exists():
logger.debug(f"Resources directory not found: {self.resources_dir}")
return templates
Expand Down Expand Up @@ -207,6 +217,15 @@ def _load_template(self, file_path: Path, default_resource_type: Optional[str])
origin = str(file_path.resolve())

for env in envelopes:
if env.resource_type in self.DEPRECATED_RESOURCE_TYPES:
if env.resource_type not in self._warned_deprecated:
self._warned_deprecated.add(env.resource_type)
logger.warning(
f"{env.resource_type} support is temporarily deprecated "
f"(see issue #23) — {env.resource_type} template(s) will be "
f"ignored by validate/plan/apply/sync/drift."
)
continue
if env.resource_type not in self.VALID_RESOURCE_TYPES:
logger.warning(f"{file_path}: unknown resource type '{env.resource_type}' — skipping")
continue
Expand Down
2 changes: 1 addition & 1 deletion src/talonctl/providers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Providers are the bridge between YAML templates and CrowdStrike APIs. Each provi
| Provider | Resource Type | CrowdStrike API | Template Location |
|----------|---------------|-----------------|-------------------|
| **DetectionProvider** | NGSIEM detection rules | Custom IOA API | `resources/detections/` |
| **WorkflowProvider** | SOAR automation workflows | Workflows API | `resources/workflows/` |
| **WorkflowProvider** | SOAR automation workflows _(temporarily deprecated — #23)_ | Workflows API | `resources/workflows/` |
| **SavedSearchProvider** | Reusable FQL query functions | Saved Searches API | `resources/saved_searches/` |
| **LookupFileProvider** | CSV/JSON correlation data | Lookup Tables API | `resources/lookup_files/` |
| **RTRScriptProvider** | Real-Time Response scripts | RTR Admin API | `resources/rtr_scripts/` |
Expand Down
2 changes: 1 addition & 1 deletion src/talonctl/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

This package contains all resource provider implementations:
- DetectionProvider: NGSIEM detection rules
- WorkflowProvider: SOAR workflows
- WorkflowProvider: SOAR workflows (temporarily deprecated — issue #23; kept but unregistered)
- SavedSearchProvider: NGSIEM saved queries
- LookupFileProvider: NGSIEM lookup files
- RTRScriptProvider: RTR custom scripts for runscript command
Expand Down
21 changes: 19 additions & 2 deletions src/talonctl/providers/workflow_provider.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
"""
Workflow Provider - CrowdStrike SOAR Workflows

This provider implements the BaseResourceProvider interface for managing
CrowdStrike SOAR workflows as Infrastructure as Code resources.
DEPRECATION NOTICE (issue #23): talonctl workflow support is temporarily
deprecated and this provider is NOT registered (see core/provider_adapter.py)
nor discovered (see core/template_discovery.DEPRECATED_RESOURCE_TYPES). The
code is retained as the starting point for a future, tested rewrite.

Why it's shelved — verified against FalconPy ``Workflows`` (v1.6.1):
* No delete method exists on the API at all, so the provider's REPLACE
(delete + recreate) strategy is structurally impossible.
* ``update_definition`` (PUT) exists but 500s on a naive compiled-model
round-trip; the correct payload shape is unknown.
* ``import_definition`` is create-only (no id targeting / upsert).
* ``search_definitions`` already returns full definition models, so the old
``get_definitions`` batch loop (which called a non-existent method) was
never needed — normalize ``search_definitions`` resources directly.
* ``workflow_definition_action`` supports enable/disable/cancel — the likely
basis for a future "disable-as-destroy" semantics.

A working rewrite requires live-tenant testing (and possibly a CrowdStrike
support ticket on the update 500), which is out of scope here.
"""

import json
Expand Down
3 changes: 2 additions & 1 deletion tests/unit/test_init_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ def test_creates_project_structure(self, tmp_path):
assert (project / "resources" / "detections").is_dir()
assert (project / "resources" / "saved_searches").is_dir()
assert (project / "resources" / "dashboards").is_dir()
assert (project / "resources" / "workflows").is_dir()
# workflow support is temporarily deprecated (#23) — not scaffolded
assert not (project / "resources" / "workflows").exists()
assert (project / "resources" / "lookup_files").is_dir()
assert (project / "resources" / "rtr_scripts").is_dir()
assert (project / "resources" / "rtr_put_files").is_dir()
Expand Down
11 changes: 6 additions & 5 deletions tests/unit/test_provider_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@ def temp_state_file(self):
def adapter(self, mock_falcon, temp_state_file):
"""Create ProviderAdapter instance"""
with patch("talonctl.providers.detection_provider.DetectionProvider"):
with patch("talonctl.providers.workflow_provider.WorkflowProvider"):
adapter = ProviderAdapter(mock_falcon, temp_state_file)
return adapter
adapter = ProviderAdapter(mock_falcon, temp_state_file)
return adapter

def test_plan_detection_changes_create(self, adapter):
"""Test planning detection creation"""
Expand Down Expand Up @@ -142,11 +141,12 @@ def test_plan_detection_changes_delete(self, adapter):
def test_get_provider(self, adapter):
"""Test getting provider by type"""
detection_provider = adapter.get_provider("detection")
# workflow support is temporarily deprecated (#23) — not registered
workflow_provider = adapter.get_provider("workflow")
unknown_provider = adapter.get_provider("unknown")

assert detection_provider is not None
assert workflow_provider is not None
assert workflow_provider is None
assert unknown_provider is None

def test_get_provider_registry_returns_all_types(self, adapter):
Expand All @@ -155,14 +155,15 @@ def test_get_provider_registry_returns_all_types(self, adapter):
assert isinstance(registry, dict)
expected_types = {
"detection",
"workflow",
"saved_search",
"lookup_file",
"rtr_script",
"rtr_put_file",
"dashboard",
}
assert set(registry.keys()) == expected_types
# workflow support is temporarily deprecated (#23) — not registered
assert "workflow" not in registry
for provider in registry.values():
assert provider is not None

Expand Down
42 changes: 40 additions & 2 deletions tests/unit/test_template_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,8 +480,15 @@ def test_template_data_reinjects_template_path(self, tmp_path):
assert v2.template_data["_template_path"].endswith("v2_rule.yaml")

def test_valid_resource_types_matches_type_to_dir(self):
"""Drift guard: VALID_RESOURCE_TYPES and TYPE_TO_DIR must cover the same keys."""
assert set(FsTemplateDiscovery.VALID_RESOURCE_TYPES) == set(FsTemplateDiscovery.TYPE_TO_DIR)
"""Drift guard: TYPE_TO_DIR must cover every active and deprecated type.

Deprecated types (e.g. workflow) keep their TYPE_TO_DIR entry so their
directory is still scanned and the deprecation warning fires, but they
are excluded from VALID_RESOURCE_TYPES. See issue #23.
"""
assert set(FsTemplateDiscovery.TYPE_TO_DIR) == (
set(FsTemplateDiscovery.VALID_RESOURCE_TYPES) | FsTemplateDiscovery.DEPRECATED_RESOURCE_TYPES
)

def test_v2_resource_routed_by_kind_regardless_of_directory(self, tmp_path):
# A v2 resource is routed by its `kind`, not its directory — so a Detection
Expand Down Expand Up @@ -554,3 +561,34 @@ def test_v1_still_routes_by_directory(self, tmp_path):
disco = FsTemplateDiscovery(resources_dir=resources, project_root=tmp_path)
discovered = disco.discover_all()
assert "legacy_lookup" in {t.name for t in discovered["lookup_file"]}


def test_workflow_templates_are_deprecated_and_skipped(tmp_path, caplog):
"""Workflow templates must be excluded from discovery with a clear
deprecation warning (issue #23), not surface as an active resource type."""
import logging
from talonctl.core.template_discovery import TemplateDiscovery

resources = tmp_path / "resources"
(resources / "workflows").mkdir(parents=True)
(resources / "workflows" / "wf.yaml").write_text(
"type: workflow\n"
"resource_id: example_wf\n"
"name: Example Workflow\n"
"trigger:\n event: detection\n type: detection\n"
"actions:\n notify:\n type: email\n"
)

discovery = TemplateDiscovery(resources_dir=resources, project_root=tmp_path)

# workflow is no longer an active type, and no "workflow" key is emitted
assert "workflow" not in TemplateDiscovery.VALID_RESOURCE_TYPES
assert "workflow" in TemplateDiscovery.DEPRECATED_RESOURCE_TYPES

with caplog.at_level(logging.WARNING):
discovered = discovery.discover_all()

assert "workflow" not in discovered
assert any("deprecated" in r.message.lower() and "workflow" in r.message.lower() for r in caplog.records), (
"expected a workflow deprecation warning"
)
Loading
Loading