v1.6.0 to v1.6.0

31 added, 35 removed. Audit A to A.

---
name: functions-falcon-api
description: Call CrowdStrike Falcon platform APIs (detections, alerts, hosts, RTR) from within Foundry function handlers. TRIGGER when user asks to "call Falcon APIs from a function", "use FalconPy in a function", "use gofalcon in a function", or needs to integrate Falcon platform APIs within serverless function code. DO NOT TRIGGER when user wants to expose external third-party APIs to Foundry โ€” use api-integrations instead.
version: 1.6.0
updated: 2026-10-01
tags: [foundry, functions, falcon-api, falconpy, gofalcon]
author: CrowdStrike
license: MIT
compatibility: Claude Code >=1.0
metadata:
category: backend
---
# Falcon API Integration in Functions
> Call Falcon APIs through the CrowdStrike SDKs inside Foundry function handlers. Authentication is automatic with the FDK handler pattern.
>
- > The FalconPy `Detects` class is **removed**. Do not import it. Use `Alerts` for detection queries.
+ > The FalconPy `Detects` class is **deprecated**: it still ships, but its API returns 405. Do not import it. Use `Alerts` for detection queries.
> **Part of a suite.** If `development-workflow` has not already run, and this is a new app or its first capability, load the `development-workflow` skill first โ€” it owns the CLI prerequisite check, scaffolding order, and manifest coordination.
This skill covers calling CrowdStrike Falcon APIs from within Foundry functions (serverless Go or Python code). Authentication is completely automatic when code runs inside Foundry function handlers โ€” the platform handles all OAuth flows, token management, and credential injection.
For exposing external APIs to Foundry via OpenAPI specs, see **api-integrations** instead.
> **๐Ÿšซ DEPRECATED API โ€” NEVER USE:**
>
> **Do NOT import or use the `Detects` class from FalconPy.** The Detects API (`/detects/entities/detects/v2`) is deprecated and returns **405 Method Not Allowed**. Any code using `Detects()`, `query_detects()`, or `get_detect_summaries()` will fail at runtime.
>
- > **Use instead:** `from falconpy import Alerts` with `query_alerts_v2()` / `get_alerts_v2()`. Filter by `product:'detections'` to scope to detections only.
+ > **Use instead:** `from falconpy import Alerts` with `query_alerts_v2()` / `get_alerts_v2()`. Filter by `product:'epp'` to scope to endpoint detections.
## Reference Files
| Topic | Reference |
|-------|-----------|
| Retry, worker threads, shared-CID listing, Workflows API as the app, enrichment | [references/advanced-patterns.md](references/advanced-patterns.md) |
## Python: Zero-Argument Authentication
FalconPy Service Classes require zero arguments when called inside Foundry Function handlers. The `crowdstrike.foundry.function` FDK provides the handler decorator that enables automatic authentication:
```python
from logging import Logger
from typing import Any, Dict, Union
from crowdstrike.foundry.function import Function, Request, Response
from falconpy import Alerts, Hosts
func = Function.instance()
@func.handler(method='GET', path='/api/alerts')
def get_alerts(request: Request, config: Union[Dict[str, Any], None], logger: Logger) -> Response:
falcon = Alerts() # Zero-arg constructor โ€” auth is automatic
limit = min(int(request.params.query.get("limit", ["50"])[0]), 100)
# FQL filter: high-severity alerts from the last 24 hours.
# Combine conditions with '+' (AND); relative times like 'now-24h' are supported.
response = falcon.query_alerts_v2(
filter="severity_name:'High'+created_timestamp:>'now-24h'",
limit=limit,
sort="created_timestamp|desc",
)
if response["status_code"] != 200:
logger.error(f"Failed to query alerts: {response.get('errors')}")
return Response(body={"error": "Failed to fetch alerts"}, code=500)
alert_ids = response.get("body", {}).get("resources", [])
if not alert_ids:
return Response(body={"alerts": []}, code=200)
details_response = falcon.get_alerts_v2(composite_ids=alert_ids)
if details_response["status_code"] != 200:
return Response(body={"error": "Failed to fetch alert details"}, code=500)
alerts = details_response.get("body", {}).get("resources", [])
return Response(body={"alerts": alerts}, code=200)
if __name__ == '__main__':
func.run()
```
**How it works:**
- **In Foundry cloud**: Uses context-based authentication injected by the platform
- **Locally**: Reads `FALCON_CLIENT_ID` and `FALCON_CLIENT_SECRET` from environment variables
FalconPy already reads env vars internally, so writing a `get_falcon_client()` wrapper adds no value and breaks context auth in the cloud.
## Go: FDK Helper Authentication
Go requires the FDK helper to get cloud and user-agent configuration:
```go
package main
import (
"context"
"log/slog"
+
+ fdk "github.com/CrowdStrike/foundry-fn-go"
"github.com/crowdstrike/gofalcon/falcon"
- "github.com/crowdstrike/gofalcon/falcon/client"
- fdk "github.com/crowdstrike/foundry-fn-go"
)
func newHandler(_ context.Context, _ *slog.Logger, _ fdk.SkipCfg) fdk.Handler {
m := fdk.NewMux()
m.Get("/api/alerts", fdk.HandleFnOf(func(ctx context.Context, r fdk.RequestOf[struct{}]) fdk.Response {
- accessToken := r.Header.Get("X-CS-ACCESSTOKEN")
-
opts := fdk.FalconClientOpts()
falconClient, err := falcon.NewClient(&falcon.ApiConfig{
- AccessToken: accessToken,
+ AccessToken: r.AccessToken,
Cloud: falcon.Cloud(opts.Cloud),
Context: ctx,
UserAgentOverride: opts.UserAgent,
})
if err != nil {
return fdk.Response{Code: 500, Body: fdk.JSON(map[string]string{"error": "Failed to authenticate"})}
}
- // ... API calls with falconClient ...
+ _ = falconClient // call Falcon APIs here, e.g. falconClient.Alerts
return fdk.Response{Code: 200, Body: fdk.JSON(map[string]interface{}{"alerts": []interface{}{}})}
}))
return m
}
func main() {
fdk.Run(context.Background(), newHandler)
}
```
## Common API Patterns
### Detection Queries (via Alerts API)
- > **โš ๏ธ The legacy Detects API (`/detects/entities/detects/v2`) is deprecated and returns 405 Method Not Allowed.** Use the Alerts API (`/alerts/entities/alerts/v3`) for all detection queries โ€” it covers both detections and cases.
+ > **โš ๏ธ The legacy Detects API (`/detects/entities/detects/v2`) is deprecated and returns 405 Method Not Allowed.** Use the Alerts API for all detection queries.
```python
@func.handler(method='GET', path='/api/detections')
def get_detections(request: Request, config, logger) -> Response:
falcon = Alerts() # Zero-arg โ€” auth is automatic
- severity_min = int(request.params.query.get("severity_min", ["3"])[0])
+ severity_min = int(request.params.query.get("severity_min", ["70"])[0]) # 1-100 scale
limit = min(int(request.params.query.get("limit", ["50"])[0]), 100)
- # Use Alerts v2 methods โ€” these hit /alerts/entities/alerts/v3 under the hood.
- # FQL filter: severity threshold + product "detections" (excludes cases/incidents).
+ # query_alerts_v2 calls GET /alerts/queries/alerts/v2; get_alerts_v2 calls POST /alerts/entities/alerts/v2.
+ # FQL filter: severity threshold + endpoint detections only.
query_response = falcon.query_alerts_v2(
- filter=f"severity:>='{severity_min}'+product:'detections'",
+ filter=f"severity:>={severity_min}+product:'epp'",
limit=limit,
sort="created_timestamp|desc",
)
if query_response["status_code"] != 200:
return Response(body={"error": "Failed to query detections"}, code=500)
alert_ids = query_response.get("body", {}).get("resources", [])
if not alert_ids:
return Response(body={"detections": []}, code=200)
details = falcon.get_alerts_v2(composite_ids=alert_ids)
if details["status_code"] != 200:
return Response(body={"error": "Failed to get details"}, code=500)
return Response(body={"detections": details["body"]["resources"]}, code=200)
```
### Host Lookups
```python
@func.handler(method='GET', path='/api/hosts')
def get_host_details(request: Request, config, logger) -> Response:
falcon = Hosts()
hostname = request.params.query.get("hostname", [""])[0] # ?hostname=...
if not hostname:
return Response(body={"error": "Hostname required"}, code=400)
query = falcon.query_devices_by_filter(filter=f"hostname:'{hostname}'")
if query["status_code"] != 200:
return Response(body={"error": "Failed to query devices"}, code=500)
host_ids = query.get("body", {}).get("resources", [])
if not host_ids:
return Response(body={"error": f"Host not found: {hostname}"}, code=404)
details = falcon.get_device_details(ids=host_ids)
host = details.get("body", {}).get("resources", [{}])[0]
return Response(body={"host": host}, code=200)
```
### Multi-API Enrichment
Combining `Hosts` and `Alerts` in one handler follows the same query-then-get-details shape as above. See [references/advanced-patterns.md](references/advanced-patterns.md) for the full example.
## LogScale / NG-SIEM Queries from Functions
When you need to **query** LogScale data (e.g., workflow execution stats from the "fusion" repo, detection telemetry, custom ingested events), use the `NGSIEM` class. This is distinct from **ingestion** (covered in functions-development).
> **โš ๏ธ Class Disambiguation:**
> - `NGSIEM` โ€” Use for **querying** (async search jobs) and file uploads (lookup files, CSV imports). This is the query interface.
> - `FoundryLogScale` โ€” Use only for **ingestion** (`ingest_data`). Despite the name suggesting broad LogScale functionality, it does NOT have the search methods.
### Query Pattern (Python)
```python
import time
from logging import Logger
from typing import Any, Dict, Union
from crowdstrike.foundry.function import Function, Request, Response
from falconpy import NGSIEM
func = Function.instance()
# Always "search-all" โ€” see the gotcha below. Scope to a repo in the query, not here.
REPO = "search-all"
def run_logscale_query(ngsiem, query_string, start, end, logger, max_wait=40):
"""Execute a CQL query as an async search job and return result rows.
``start`` / ``end`` accept Humio relative strings ("24h", "7d", "30d",
"now") or epoch-millisecond integers.
"""
payload = {"queryString": query_string, "start": start, "end": end, "isLive": False}
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
# 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}")
return None
# Poll until done
waited = 0.0
poll_interval = 1.5
while waited < max_wait:
time.sleep(poll_interval)
waited += poll_interval
status = ngsiem.get_search_status(repository=REPO, id=job_id)
if not isinstance(status, dict) or status.get("status_code", 500) >= 300:
logger.error(f"get_search_status failed: {status}")
return None
sbody = status.get("body") or {}
if sbody.get("done"):
return sbody.get("events", []) or []
logger.warning(f"query timed out after {max_wait}s: {query_string}")
return None
@func.handler(method='POST', path='/api/query')
def handle_query(request: Request, config: Union[Dict[str, Any], None], logger: Logger) -> Response:
ngsiem = NGSIEM() # Zero-arg auth โ€” automatic in Foundry
query = request.body.get("query", "")
start = request.body.get("start", "24h")
end = request.body.get("end", "now")
events = run_logscale_query(ngsiem, query, start, end, logger)
if events is None:
return Response(body={"error": "LogScale query failed"}, code=500)
return Response(body={"results": events, "count": len(events)}, code=200)
if __name__ == '__main__':
func.run()
```
### `start_search` Arguments and Responses
Pass the query to `start_search` as `search=`, as in the example above. 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.
The NG-SIEM queryjobs API is addressed by **searchable view**, not by raw repo name. Use `search-all` as the repository and add `#repo=fusion` (or whichever repo you need) as a filter prefix in your query string:
```python
# Filter to a specific repo within the query itself
query = "#repo=fusion | execution_log_type=summary | execution_log_subtype=end | groupby([status], function=count(execution_id, distinct=true))"
events = run_logscale_query(ngsiem, query, "24h", "now", logger)
```
### Required OAuth Scopes
```yaml
# manifest.yml
auth:
scopes:
- humio-auth-proxy:read # Required for queries
- humio-auth-proxy:write # Required only if also uploading lookup files
```
### Reference
- [Exporting Falcon Next-Gen SIEM Query Results to CSV with Falcon Foundry](https://www.crowdstrike.com/tech-hub/ng-siem/exporting-falcon-next-gen-siem-query-results-to-csv-with-falcon-foundry/) โ€” background on async LogScale querying from Foundry, plus CSV export. Note that this post reaches for `FoundryLogScale` with `mode="async"`; the `NGSIEM` pattern above is what has been verified end-to-end against the queryjobs API in a deployed app. Use the pattern above, and treat the post as context for the surrounding workflow (time ranges, result handling, export).
## Falcon Next-Gen SIEM Content APIs
Dashboards, saved queries, lookup files, and persisted aggregations take different search domain names, and a wrong one answers `404` or `400`, not `403`. See [ngsiem-content-apis](references/ngsiem-content-apis.md).
## Charlotte AI AgentWorks (`/agentic-studio/*`)
Use the `Agents`, `Spans`, `AgentInvocation`, `AgentVersions`, and `Models` classes with the `charlotte-ai-agent-definition` scopes (`read`; `write` to invoke and poll). Filter spans on `attributes.aw_agent.id` (not `aw_agent.agent_id`, which silently matches nothing). Route-to-method map: [advanced-patterns](references/advanced-patterns.md).
## The 207 Multi-Status Gotcha
CrowdStrike APIs may return `207 Multi-Status` responses that look successful but contain embedded errors. Check the errors array:
```python
response = falcon.perform_action(action_name="contain", ids=host_ids)
if response["status_code"] == 207:
errors = response.get("body", {}).get("errors", [])
rate_limited = [e for e in errors if e.get("code") == 429]
if rate_limited:
return Response(body={"error": "Rate limited", "failed_ids": [e.get("id") for e in rate_limited]}, code=429)
```
## Testing
Mock Falcon APIs in tests instead of making real API calls (they are slow, flaky, and quota-consuming):
```python
+ import importlib
+ from unittest.mock import Mock, patch
+
+ from crowdstrike.foundry.function import Request, RequestParams
+
+ import main
+
def test_get_alerts_success():
mock_falcon = Mock()
mock_falcon.query_alerts_v2.return_value = {
"status_code": 200,
"body": {"resources": ["alert-001", "alert-002"]}
}
mock_falcon.get_alerts_v2.return_value = {
"status_code": 200,
"body": {"resources": [{"id": "alert-001", "severity": 80}]}
}
- with patch('falconpy.Alerts', return_value=mock_falcon):
- from crowdstrike.foundry.function import RequestParams
- from main import get_alerts
- request = Mock(spec=Request)
- request.params = RequestParams(query={"limit": ["10"]})
- response = get_alerts(request, None, Mock())
+ # @func.handler registers the function and returns None, so swap in an
+ # identity decorator and reload main (as foundry-sample-functions-python's tests do)
+ with patch("crowdstrike.foundry.function.Function.handler", new=lambda *a, **k: (lambda f: f)):
+ importlib.reload(main)
+ with patch("main.Alerts", return_value=mock_falcon):
+ request = Request(params=RequestParams(query={"limit": ["10"]}))
+ response = main.get_alerts(request, None, Mock())
assert response.code == 200
assert len(response.body["alerts"]) == 1
# get_alerts_v2 reads only composite_ids; ids= fails with a 400 before any request is sent
mock_falcon.get_alerts_v2.assert_called_once_with(composite_ids=["alert-001", "alert-002"])
```
## Local Testing
```bash
export FALCON_CLIENT_ID="your-client-id"
export FALCON_CLIENT_SECRET="your-client-secret"
cd functions/my-function && python3 main.py
- curl -X GET http://localhost:8081/api/alerts?limit=10
+ # The FDK reads a JSON envelope, not the URL: POST it to the root with method, url, and params
+ curl -X POST http://localhost:8081 -H 'Content-Type: application/json' \
+ -d '{"method": "GET", "url": "/api/alerts", "params": {"query": {"limit": ["10"]}}}'
```
## OAuth Scopes for manifest.yml
Every FalconPy service class call requires the correct OAuth scope(s) declared in your manifest's `auth.scopes` array. Without them the function gets a 403 at runtime; `foundry apps validate` doesn't catch it.
> **โš ๏ธ Scope names don't always match class names.** `Hosts` needs `devices:read`, not `hosts:read`. Use this table, don't guess.
- > **Built-in capabilities don't need scopes.** API integrations, collections, workflows, and LogScale ingestion work without declaring their scopes when used through Foundry's built-in SDK patterns (`falcon.apiIntegration()`, `CustomStorage()` for app collections, etc.). Only declare scopes when calling Falcon platform APIs directly via FalconPy service classes.
+ > **Built-in capabilities don't need scopes.** API integrations, collections, and workflows work without declaring their scopes when used through Foundry's built-in SDK patterns (`falcon.apiIntegration()`, `CustomStorage()` for app collections, etc.). Only declare scopes when calling Falcon platform APIs directly via FalconPy service classes.
### Scope Reference
Each row is verified against a sample app's manifest, a live CID, or the API swagger.
| FalconPy Class | Methods | Required Scope(s) | Verified In |
|---|---|---|---|
| `Hosts` | `get_device_details` | `devices:read` | foundry-sample-functions-python |
| `Intel` | `query_indicator_ids` | `falconx-indicators:read` | foundry-sample-zscaler-internet-access |
| `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 a live CID (200 + results); see LogScale Queries section |
| `NGSIEM` | `/ngsiem-content/` calls | See [ngsiem-content-apis](references/ngsiem-content-apis.md) | Live CID |
| `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 |
| `Agents`, `Spans`, `AgentInvocation`, `AgentVersions`, `Models` | AgentWorks | `charlotte-ai-agent-definition:read`, `:write` | Endpoints verified on EU-1 |
- | `CaseManagement` | `query_case_ids`, `get_cases` / `update_case` | `cases:read` / `cases:write` | API swagger |
+ | `CaseManagement` | `query_case_ids`, `get_cases` / `update_case_fields` | `cases:read` / `cases:write` | API swagger |
| `RealTimeResponse` | `batch_init_sessions`, `batch_command` / `batch_active_responder_command` (incl. `runscript`) | `real-time-response:read` / `:write` | API swagger |
| `RealTimeResponseAdmin` | `batch_admin_command` (`put`, `run`) | `real-time-response-admin:write` | API swagger |
**Go functions (gofalcon) need the same scopes.** Incidents use `incidents:read` / `:write`.
### How to declare scopes
```yaml
# manifest.yml
auth:
scopes:
- devices:read
- falconx-indicators:read
permissions: {}
roles: []
```
### When unsure about the correct scope
If you're using a FalconPy method not in this table:
1. Check the method's HTTP verb and API path in FalconPy source โ€” GET typically needs `:read`, POST/PUT/PATCH/DELETE typically needs `:write`
2. The scope prefix is usually the **API path prefix** (e.g., `/iocs/...` โ†’ `iocs`, `/devices/...` โ†’ `devices`), but exceptions exist (`Hosts` โ†’ `devices`, `/humio/` โ†’ `humio-auth-proxy`, `/ngsiem-content/` โ†’ one per type)
3. When ambiguous, **ask the user** which scopes to include rather than guessing
## Falcon Severity Values
- CrowdStrike APIs return severity as **integers** (1-5) or display names. When integrating with external systems (Jira, ServiceNow, email), map them explicitly:
-
- | Falcon Severity | Display Name | Typical External Mapping |
- |----------------|--------------|--------------------------|
- | 1 | Informational | Low / Lowest |
- | 2 | Low | Low |
- | 3 | Medium | Medium |
- | 4 | High | High |
- | 5 | Critical | Highest / Critical |
-
- Use `max_severity_displayname` for FQL filters (string comparison) or `max_severity` for numeric comparison. When passing severity to external ticketing systems, always map to their expected format rather than passing the raw value through.
+ Alerts carry `severity` as an integer from 1 to 100 (100 is most severe) and `severity_name` as its label, for example `30` / `"Low"` and `70` / `"High"`. Filter on `severity_name:'High'` or a numeric threshold such as `severity:>=70`; a low threshold like `severity:>=3` matches almost every alert. The `max_severity` / `max_severity_displayname` fields belong to the deprecated Detects API. When passing severity to an external system (Jira, ServiceNow, email), map `severity_name` to its expected values instead of passing the raw number through.
## Common Pitfalls
- **Writing OAuth code or credential management.** Auth is automatic inside FDK handlers. The zero-arg pattern (`Hosts()`, `Alerts()`) handles all auth. (Go requires `fdk.FalconClientOpts()` -- see above.)
- **Using `requests` library instead of CrowdStrike SDKs.** SDKs handle auth, retries, pagination, and region discovery.
- **Passing credentials explicitly to constructors.** Use zero-arg constructors (`Alerts()`, `Hosts()`). Do NOT write `IOC(client_id=os.environ["FALCON_CLIENT_ID"], client_secret=...)` -- this breaks context-based auth in the Foundry cloud.
- **Writing Falcon API calls outside of FDK handler functions.** The handler pattern is required for automatic auth injection.
- **Module-scope client, or `request.params.get()` / `request.query`.** `401` on every call, or an attribute error.
- **Not handling 207 Multi-Status.** These responses look successful but may contain embedded errors.
## Use Cases
For real-world implementation patterns, see:
- `use-cases/python-functions.md` โ€” Python handler patterns, SDK usage, testing
## Reference Implementations
- **[foundry-sample-functions-python](https://github.com/CrowdStrike/foundry-sample-functions-python)**: Reference Python patterns. See also [Dive into Falcon Foundry Functions with Python](https://www.crowdstrike.com/tech-hub/ng-siem/dive-into-falcon-foundry-functions-with-python/).
- **[foundry-sample-anomali-threatstream](https://github.com/CrowdStrike/foundry-sample-anomali-threatstream)**: Side-by-side Go and Python auth patterns.
- - **[foundry-sample-detection-translation](https://github.com/CrowdStrike/foundry-sample-detection-translation)**: CrowdStrike alerts API from functions.
- - **[foundry-sample-threat-intel](https://github.com/CrowdStrike/foundry-sample-threat-intel)**: CrowdStrike Intelligence APIs from functions.
- **[foundry-sample-idp-notifications](https://github.com/CrowdStrike/foundry-sample-idp-notifications)**: Falcon IdP domain and connector monitoring.