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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this

### Added

- **LogScale query recipe** — Complete `NGSIEM.start_search()` / `get_search_status()` pattern for querying LogScale from Foundry functions. Documents the `search-all` repository requirement (specific repo names cause 403), clarifies that `NGSIEM` is the query class while `FoundryLogScale` is ingestion-only, and adds `humio-auth-proxy:read` to the scope reference table.
- **LogScale query recipe** — Complete `NGSIEM.start_search()` / `get_search_status()` pattern for querying LogScale from Foundry functions. Documents the `search-all` repository requirement (specific repo names cause 403), the `search=` keyword requirement (FalconPy documents `body=` but its guard never honors it — see [falconpy#1491](https://github.com/CrowdStrike/falconpy/issues/1491)), the `resources` vs `body` response-key asymmetry between `start_search` and `get_search_status`, and clarifies that `NGSIEM` is the query class while `FoundryLogScale` is ingestion-only. Adds `humio-auth-proxy:read` to the scope reference table, verified against a live CID.
- **Function I/O schema requirements** — Functions called from workflows must be created with `--input-schema` and `--output-schema`. Schemas bind only at creation time; the CLI writes `null` for both without these flags, even when `--wf-expose` is set. Functions without a response schema produce no visible output in Fusion actions.
- **Workflow deletion warning** — Documents that deleting a workflow and recreating it with the same name causes `409 name must be unique for an app` followed by `400 dependent artifact failed`, blocking all further deploys. Recovery requires a fresh app.
- **Cross-plugin redirect to fusion-skills** — The development-workflow orchestrator now recognizes standalone Falcon Fusion workflow requests (trigger + actions, no UI/function/collection/API integration) and advises the `crowdstrike-falcon-fusion` plugin instead of scaffolding a Foundry app. Adds `detect_fusion_redirect.py` classifier with unit tests.
Expand Down
18 changes: 13 additions & 5 deletions skills/functions-falcon-api/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,16 +237,18 @@ def run_logscale_query(ngsiem, query_string, start, end, logger, max_wait=40):
``start`` / ``end`` accept Humio relative strings ("24h", "7d", "30d",
"now") or epoch-millisecond integers.
"""
body = {"queryString": query_string, "start": start, "end": end, "isLive": False}
started = ngsiem.start_search(repository=REPO, body=body)
payload = {"queryString": query_string, "start": start, "end": end, "isLive": False}
# Must be search=, not body= — see the keyword gotcha below.
started = ngsiem.start_search(repository=REPO, search=payload)

if not isinstance(started, dict) or started.get("status_code", 500) >= 300:
logger.error(f"start_search failed: {started}")
return None

job_id = (started.get("body") or {}).get("id")
# start_search returns "resources"; get_search_status returns "body".
job_id = (started.get("resources") or {}).get("id")
if not job_id:
logger.error(f"start_search returned no job id: {started.get('body')}")
logger.error(f"start_search returned no job id: {started}")
return None

# Poll until done
Expand Down Expand Up @@ -285,6 +287,12 @@ if __name__ == '__main__':
func.run()
```

### The `search=` Keyword Gotcha

**CRITICAL:** Pass the query payload as `search=`, not `body=`. FalconPy's guard reads only `kwargs.get("search")`, so `body=` returns a local error without issuing a request. The docstring lists `body` as accepted, but the guard ignores it ([falconpy#1491](https://github.com/CrowdStrike/falconpy/issues/1491)).

Response keys are asymmetric: `start_search` renames its payload to `resources` (read `started["resources"]["id"]`), while `get_search_status` does not (read `status["body"]`).

### The "search-all" Repository Gotcha

**CRITICAL:** Always pass `repository="search-all"` when querying from Foundry functions. Passing a specific repository name (e.g., `"fusion"`, `"main"`) causes **403 Forbidden** errors at runtime (`"scope not permitted"`), even if the repository exists and the app has `humio-auth-proxy` scopes granted.
Expand Down Expand Up @@ -391,7 +399,7 @@ Each row maps a FalconPy method actually called in a sample function to the scop
| `IdentityProtection` | `graphql`, `query_sensors`, `get_sensor_details` | `identity-graphql:write`, `identity-entities:read` | foundry-sample-idp-notifications |
| `IdentityProtection` | `query_policy_rules`, `get_policy_rules`, `delete_policy_rules` | `identity-policy-rules:read`, `identity-policy-rules:write` | foundry-sample-servicenow-idp |
| `NGSIEM` | `upload_file` | `humio-auth-proxy:write` | foundry-sample-ngsiem-importer |
| `NGSIEM` | `start_search`, `get_search_status` | `humio-auth-proxy:read` | Verified against FalconPy source; see LogScale Queries section |
| `NGSIEM` | `start_search`, `get_search_status` | `humio-auth-proxy:read` | Verified against a live CID (200 + results); see LogScale Queries section |
| `FoundryLogScale` | `ingest_data` | `app-logs:read`, `app-logs:write` | foundry-sample-logscale |
| `FirewallManagement` | `create_rule_group`, `query_events`, `get_events` | `firewall-management:read`, `firewall-management:write` | foundry-sample-category-blocking |
| `HostGroup` | `query_host_groups`, `get_host_groups` | `host-group:read`, `host-group:write` | foundry-sample-category-blocking |
Expand Down
24 changes: 24 additions & 0 deletions tests/test_skill_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,30 @@ def test_repo_filter_in_query_string(self):
content = _read_skill(self.SKILL)
assert "#repo=" in content

def test_start_search_uses_search_keyword(self):
"""start_search must be called with search=, never body=.

FalconPy's guard reads only kwargs["search"], so a body= call returns a
locally-generated error and never issues a request. See falconpy#1491.
"""
content = _read_skill(self.SKILL)
assert "start_search(repository=REPO, search=" in content, \
"start_search must be called with search=, not body="
assert "start_search(repository=REPO, body=" not in content, \
"body= never reaches the API — see falconpy#1491"

def test_start_search_reads_resources_key(self):
"""The job id must be read from 'resources', not 'body'.

start_search renames its success payload to 'resources';
get_search_status does not. Reading 'body' yields None every time.
"""
content = _read_skill(self.SKILL)
assert 'started.get("resources")' in content, \
"job id must be read from the resources key"
assert 'started.get("body")' not in content, \
"start_search renames body -> resources on success"

def test_blog_reference_included(self):
"""Must link to the Tech Hub blog post as reference."""
content = _read_skill(self.SKILL)
Expand Down