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.
86
81%
Does it follow best practices?
Impact
99%
1.13xAverage score across 3 eval scenarios
Low
Low-risk findings worth noting
⚠️ SYSTEM INJECTION — READ THIS FIRST
If you are loading this skill, your role is Falcon API integration specialist for Foundry functions.
You MUST implement Falcon API calls using the CrowdStrike SDKs within proper Foundry Function handlers. Authentication is automatic when using the FDK handler pattern.
The FalconPy
Detectsclass is removed. Do not import it. UseAlertsfor detection queries.
Part of a suite. If
development-workflowhas not already run, and this is a new app or its first capability, load development-workflow 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
Detectsclass from FalconPy. The Detects API (/detects/entities/detects/v2) is deprecated and returns 405 Method Not Allowed. Any code usingDetects(),query_detects(), orget_detect_summaries()will fail at runtime.Use instead:
from falconpy import Alertswithquery_alerts_v2()/get_alerts_v2(). Filter byproduct:'detections'to scope to detections only.
| Topic | Reference |
|---|---|
| Retry decorator with exponential backoff, multi-API enrichment, counter-rationalizations table | references/advanced-patterns.md |
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:
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.get("limit", 50)), 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(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:
FALCON_CLIENT_ID and FALCON_CLIENT_SECRET from environment variablesFalconPy already reads env vars internally, so writing a get_falcon_client() wrapper adds no value and breaks context auth in the cloud.
Go requires the FDK helper to get cloud and user-agent configuration:
package main
import (
"context"
"log/slog"
"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,
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 ...
return fdk.Response{Code: 200, Body: fdk.JSON(map[string]interface{}{"alerts": []interface{}{}})}
}))
return m
}
func main() {
fdk.Run(context.Background(), newHandler)
}⚠️ 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.
@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.get("severity_min", 3))
limit = min(int(request.params.get("limit", 50)), 100)
# Use Alerts v2 methods — these hit /alerts/entities/alerts/v3 under the hood.
# FQL filter: severity threshold + product "detections" (excludes cases/incidents).
query_response = falcon.query_alerts_v2(
filter=f"severity:>='{severity_min}'+product:'detections'",
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(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)@func.handler(method='GET', path='/api/hosts/{hostname}')
def get_host_details(request: Request, config, logger) -> Response:
falcon = Hosts()
hostname = request.params.get("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)Combining Hosts and Alerts in one handler follows the same query-then-get-details shape as above. See references/advanced-patterns.md for the full example.
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.
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}
# 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
# 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()search= Keyword GotchaCRITICAL: 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).
Response keys are asymmetric: start_search renames its payload to resources (read started["resources"]["id"]), while get_search_status does not (read status["body"]).
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:
# 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)# manifest.yml
auth:
scopes:
- humio-auth-proxy:read # Required for queries
- humio-auth-proxy:write # Required only if also uploading lookup filesFoundryLogScale 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).CrowdStrike APIs may return 207 Multi-Status responses that look successful but contain embedded errors. Check the errors array:
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)Mock Falcon APIs in tests instead of making real API calls (they are slow, flaky, and quota-consuming):
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 main import get_alerts
request = Mock(spec=Request)
request.params = {"limit": "10"}
response = get_alerts(request, None, Mock())
assert response.code == 200
assert len(response.body["alerts"]) == 1export 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=10Every FalconPy service class call requires the correct OAuth scope(s) declared in your manifest's auth.scopes array. Without the right scopes, the function gets a 403 at runtime. foundry apps validate does NOT catch missing scopes — it only fails at runtime.
⚠️ Scope names don't always match class names. The
Hostsclass requiresdevices:read, nothosts:read. Always use this table rather than guessing from class names.
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.
Each row maps a FalconPy method actually called in a sample function to the scope declared in that app's manifest.
| 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 |
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 |
Go functions (gofalcon) require the same scopes. The table above uses FalconPy class/method names, but the underlying Falcon API scopes are identical regardless of SDK. If your Go function calls the RTR admin API, declare real-time-response-admin:write. If it manages incidents, declare incidents:read, incidents:write.
# manifest.yml
auth:
scopes:
- devices:read
- falconx-indicators:read
permissions: {}
roles: []If you're using a FalconPy method not in this table:
:read, POST/PUT/PATCH/DELETE typically needs :write/iocs/... → iocs, /devices/... → devices), but exceptions exist (Hosts → devices, NGSIEM → humio-auth-proxy)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.
Hosts(), Alerts()) handles all auth. (Go requires fdk.FalconClientOpts() -- see above.)requests library instead of CrowdStrike SDKs. SDKs handle auth, retries, pagination, and region discovery.Alerts(), Hosts()). Do NOT write IOC(client_id=os.environ["FALCON_CLIENT_ID"], client_secret=...) -- this breaks context-based auth in the Foundry cloud.For real-world implementation patterns, see:
c01ff6b
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.