talonctl validatenow acceptsCaseNotificationGroup,CaseSla, andCaseTemplateenvelopes. The JSON Schema enum inschemas/envelope.schema.jsonwas never updated when the three case kinds were added toKIND_TO_TYPE, causingvalidate_authored_envelope()to reject all case management templates with an "is not one of" error. The Python code (provider registry, template discovery,VALID_KINDS) was correct; only the JSON Schema enum was stale.- Drift-guard test added (
test_schema_kind_enum_matches_kind_to_type) that asserts the schema's kind enum exactly matchesKIND_TO_TYPE.keys(), so the two sources of truth cannot diverge again.
- Three new resource types for CrowdStrike Case Management, managed as v2 IaC
through the standard lifecycle (validate / plan / apply / import / sync /
destroy):
case_notification_group,case_sla, andcase_template. They form a dependency chain — notification groups → SLAs → templates — and deploy/destroy in that order automatically. - Cross-resource references by stable
resource_id. A template names its SLA viasla_ref, and an SLA's escalation steps name a notification group vianotification_group_ref. A new sharedRefResolver(core/ref_resolver.py) substitutes the live API id (sla_id,notification_group_id) at apply time, andextract_dependenciesderives the ordering graph from those refs. Content hashing uses the stable ref rather than the resolved API id, so redeploying a referenced resource never spuriously marks its dependents as drifted. import/syncsupport for all three types via paginated bulk-fetch, with API ids reverse-mapped back to*_ref(the raw id is preserved and a warning is logged when a sibling cannot be resolved).- Reference templates for the three types in
examples/resources/, plus scaffolding of their directories intalonctl initand registration infind --type.
driftdoes not yet cover the case types, consistent with the existingdashboardtype.
- Lookup-file
source:CSV/JSON paths no longer resolve relative to the current working directory. Relative sources are authored project-root-relative (e.g.resources/lookup_files/x.csv) and the data file lives next to the templates, so resolving against CWD brokevalidate,plan, andapplywhenever talonctl ran from anywhere but the project root — most visibly under--path DIRinvoked from an unrelated directory, which reported every relative-source lookup as "source file not found". Resolution now anchors to the project root walked up from each template's own location (_template_path), via a singleLookupFileProvider._resolve_source_pathhelper shared byvalidate_template,compute_content_hash,apply_create, andapply_update. Absolute sources are unchanged; when a template's origin is unknown (hand-built dicts), it falls back to the previous CWD-relative behavior. Closes the follow-up noted in v0.5.4. Content hashes for existing lookups are unaffected (same bytes hashed), so no deploy churn.
-
Global
--path DIRoption on thetalonctlgroup: discover resources from an arbitrary directory instead of the hardwired<project-root>/resources. Available to every command (validate,plan,apply,find,show,drift,sync,import,migrate,health, …) via the shared context, sotalonctl --path /opt/detections planjust works. Default is unchanged, so existing repos behave identically. State location is independent — set it with--state-file(or the.crowdstrike/project root);--pathonly governs where templates are discovered.Note: lookup-file
source:CSV paths are still resolved relative to the current working directory, so running with--pathfrom an unrelated CWD can report "source file not found" for lookups with relative sources — use absolutesourcepaths or run from the project for now. Anchoring lookup sources to the resources directory is a follow-up.
-
Template discovery is now kind-routed. It makes a single recursive pass over
resources/and routes each resource by its own type — v2 bykind, v1 by its top-level directory (v1 carries no kind). Previously discovery scanned per type-directory and skipped any resource whosekinddidn't match its directory, which meant a mixed-kind multi-resource file (e.g. a lookup grouped with the detection that uses it) silently dropped its off-type resources on plan/apply — even though the loader andvalidateaccepted it. Now a file may declare resources of any kind regardless of where it lives, and they all deploy. v1 directory routing is unchanged (fully backward compatible); standard one-type- per-directory layouts produce identical results.Removes the
kind/dir mismatch … skippingwarning (a misplaced v2 resource is now routed by kind instead of dropped).find_template_by_idresolves across all ofresources/too.
validatenow flags block-unsafe whitespace in v2 templates. Any multiline string value (descriptions, CQL filters, queries) with trailing whitespace on a line, or an embedded tab, fails validation with the offending field path. These force PyYAML to emit an unreadable quoted"...\n..."scalar instead of a clean|literal block, so the check keeps authored v2 files canonical and catches the problem at PR time. Implemented ascheck_whitespace_hygieneincore/envelope_validation.py, wired intotalonctl validate.
Bug-fix release hardening talonctl migrate's v1→v2 rewrap so it is lossless and
produces readable v2 templates.
- List-form
labelsare no longer dropped.v1_compatonly recognized mapping-style labels; saved searches carrylabelsas a list of strings (LogScale's native model), which were silently discarded on rewrap. Because the same normalization also feeds the engine's loader, the loss madeplan/applysee those labels as absent and try to strip them from deployed resources. Both shapes (list and mapping) now round-trip, and the v2 envelope schema accepts either. - Multiline strings serialize as
|literal blocks instead of double-quoted scalars with\nescapes. The serializer hints block style for any string containing newlines. It stays strictly lossless: strings PyYAML cannot block-represent (trailing whitespace or embedded tabs) fall back to a quoted scalar rather than being mutated — so content hashes and deployments do not churn.
The foundation for talonctl v2: a unified Kubernetes-style resource envelope,
resource_id-keyed state, and an idempotent migration command. Backward
compatible — existing v1 templates and v3 state continue to load unchanged; v2
adoption is opt-in via talonctl migrate.
- Canonical v2 envelope (
apiVersion: talon/v2withkind/metadata/spec/status) as the in-memory model every resource normalizes to. A dual-read loader transparently reads both legacy v1 flat templates and v2 envelopes, so no template changes are required to upgrade. - Multi-resource files — a single YAML file may declare one or many
independent resources (multi-doc
---or a top-level list), each keyed by its ownresource_id. (There is nokind: Module; a file is 1..N independent resources.) - In-package JSON Schema + schema-driven validator for the v2 envelope;
talonctl validateunderstands both v1 and v2 files. - State v4 — state is keyed by stable
resource_id; v3 (name-keyed) state auto-migrates in memory on load, non-destructively. A read-onlystatusprojection (server_id,rule_id,deployed_at,content_hash) is derived from existing state fields; authors cannot writestatus. talonctl migrate— idempotent, dry-run-by-default command that rewrites v1 templates to v2 in place and reconciles v3 state to v4:--writeapplies changes (dry-run writes nothing — neither templates nor state);--templates-only/--state-onlyscope each half;--format json -o FILEemits a machine-readable report.- Reports orphans (state with no template), unmanaged templates (template with no state), and conflicts — report-only; never deletes or creates resources.
- Content hashes are preserved byte-for-byte through a rewrap, so already-deployed
resources do not show as changed on the next
plan.
- All seven providers now consume the canonical
Envelope;plan,apply,validate, anddriftoperate on v1 and v2 templates end-to-end through a single parse path. - API-consumed fields (e.g.
_search_domain) are classified intospec; the v1 top-levelmetadata:block routes to envelopemetadata.
- New modules:
core/envelope.py,core/envelope_loader.py,core/v1_compat.py,core/envelope_validation.py,core/envelope_serializer.py,core/status_projection.py,core/migrate.py;schemas/envelope.schema.json.TemplateDiscoverydelegates to the shared envelope loader (one parse path). - Merge-blocking content-hash stability tests guarantee the v1→v2 transform and the serializer never alter a deployed resource's content hash.
Run talonctl migrate (dry-run) to preview, then talonctl migrate --write to
convert templates to v2 and re-key state to v4 in place. Git is the rollback. v1
templates and v3 state continue to work unmigrated; a future release will
announce the v1-reader removal deadline.
talonctl find QUERY— offline identifier resolution (rule_id UUID, resource_id,ngsiem:/fcs:/thirdparty:/cwpp:composite IDs, display-name substring, glob). Closes #10.talonctl validate --queries(-Q) — after schema validation passes, CQL-parses every query across detections, saved searches, and dashboards against NGSIEM in parallel. Output anchors each failure at its exact location (search.filter,widgets.<id>.queryString,parameters.<id>.query, orqueryString). Requires credentials; exits 1 with a documented message when missing. Closes #11.
NGSIEMClienterror messages no longer emit misleading"Unknown error"fallbacks. Rejections now readLogScale rejected query (status=<N>, no detail returned by API)when the upstream API returns nothing structured, or pass the payload through verbatim when present.- Detection query-field precedence (
search.filterwins oversearch.query) is now consistent acrossvalidate,validate-query, and the plan-path query validator. QueryValidationResultnow carries an optionallocationfield used by the formatter to point at the exact widget or field path.
- New
talonctl.core.query_collectionmodule (QueryRef+collect_queries_from_templates) centralises per-resource-type query-field knowledge. - New
DeploymentOrchestrator.validate_queries()method. Plan-path detection validation (_validate_detection_queries) is unchanged; broadening it to saved searches and dashboards is tracked as a follow-up.
- Top-level
ads:block is removed. Move it undermetadata.ads:. - Top-level flat
metadata:with maturity fields at root is removed. Nest fields undermetadata.maturity:. - All seven resource types now share a reserved top-level
metadata:namespace that is stripped from API payloads and content hashes. Third-party frameworks can add sub-namespaces (e.g.metadata.<team_name>:) with a guarantee they will never leak to the CrowdStrike/Humio API.
Before:
resource_id: my_detection
name: "My Detection"
ads:
goal: "Detect something"
mitre_attack: ["TA0011:T1090.003"]
metadata:
created: "2026-04-14"
last_tuned: "2026-04-16"
tune_count: 2
confidence: high
severity: 50
search: { ... }After:
resource_id: my_detection
name: "My Detection"
metadata:
maturity:
created: "2026-04-14"
last_tuned: "2026-04-16"
tune_count: 2
confidence: high
ads:
goal: "Detect something"
mitre_attack: ["TA0011:T1090.003"]
severity: 50
search: { ... }Migration rules (mechanical):
- Top-level
ads:→metadata.ads:. - Top-level flat
metadata:withcreated/last_tuned/tune_count/confidenceat root →metadata.maturity:with the same fields inside. - If both were present, merge into a single
metadata:withmaturity:andads:sub-namespaces.
No talonctl migrate command ships — the corpus is small enough to hand-edit
or rewrite with an editor or assistant.
- Dashboard apply no longer leaks
_template_pathinto the Humio schema validation payload (closes issue #7). The stripping fix is shared across all resource types viacore/template_sanitizer(shipped in v0.2.1) and is now exercised by every provider's payload/hash path. - Reference templates
examples/resources/lookup_file.yaml,rtr_put_file.yaml, andworkflow.yamlhad pre-existing schema bugs (missing required fields, wrong field names, broken file paths). These are fixed and now exercised by a new parity test.
metadata.maturity:validation now runs on every resource type, not just detections. Schema:created(YYYY-MM-DD),last_tuned(YYYY-MM-DD or null),tune_count(non-negative int),confidence(low/medium/high/ validated). All four fields optional.metadata.ads:remains detection-only. Non-detection providers reject it with a clear error pointing at the resource type.- Third-party frameworks may add arbitrary sub-namespaces under
metadata.(e.g.metadata.acme_corp:). These are stripped from API payloads and ignored by talonctl; users supply their own validators if needed. tests/unit/test_examples.pyparity test ensures reference YAMLs underexamples/resources/do not drift out of spec.tests/unit/test_old_shape_rejection.pylocks every provider's migration pointer error string so a silent refactor can never drop it.
- Dashboard apply no longer leaks
_template_pathinto the Humio schema validation payload (closes #7).DashboardProvider._prepare_yaml_payloadand_normalize_for_hashnow route through a newcore/template_sanitizerhelper that strips_-prefixed tool-internal keys and the universally-IaC field set{resource_id, type, dependencies, metadata}. Dashboard-specific transforms (tags → labels,descriptionstrip) are preserved unchanged.
- Added
.pre-commit-config.yamlwith ruff hooks mirroring the CI lint gate. Install withpre-commit installafterpip install -e .[dev].
- v0.2.0 was never released to PyPI. Its git tag was pushed malformed
(
v.0.2.0with an extra dot) and the release job's VCS-versioning could not parse it. The malformed tag remains on the remote — a GitHub repository rule blocks tag deletion — but it is orphaned: hatchling's VCS versioning cannot parse it, so no release will ever be cut from it. v0.2.1 is the first working 0.2.x release.