This is page 3 of 5. Use http://codebase.md/crowdstrike/falcon-mcp?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .dockerignore
├── .env.dev.example
├── .env.example
├── .github
│ ├── dependabot.yml
│ ├── ISSUE_TEMPLATE
│ │ ├── bug.yaml
│ │ ├── config.yml
│ │ ├── feature-request.yaml
│ │ └── question.yaml
│ └── workflows
│ ├── docker-build-push.yml
│ ├── docker-build-test.yml
│ ├── markdown-lint.yml
│ ├── python-lint.yml
│ ├── python-test-e2e.yml
│ ├── python-test.yml
│ └── release.yml
├── .gitignore
├── .markdownlint.json
├── CHANGELOG.md
├── Dockerfile
├── docs
│ ├── CODE_OF_CONDUCT.md
│ ├── CONTRIBUTING.md
│ ├── deployment
│ │ ├── amazon_bedrock_agentcore.md
│ │ └── google_cloud.md
│ ├── e2e_testing.md
│ ├── module_development.md
│ ├── resource_development.md
│ └── SECURITY.md
├── examples
│ ├── adk
│ │ ├── adk_agent_operations.sh
│ │ ├── falcon_agent
│ │ │ ├── __init__.py
│ │ │ ├── agent.py
│ │ │ ├── env.properties
│ │ │ └── requirements.txt
│ │ └── README.md
│ ├── basic_usage.py
│ ├── mcp_config.json
│ ├── sse_usage.py
│ └── streamable_http_usage.py
├── falcon_mcp
│ ├── __init__.py
│ ├── client.py
│ ├── common
│ │ ├── __init__.py
│ │ ├── api_scopes.py
│ │ ├── errors.py
│ │ ├── logging.py
│ │ └── utils.py
│ ├── modules
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── cloud.py
│ │ ├── detections.py
│ │ ├── discover.py
│ │ ├── hosts.py
│ │ ├── idp.py
│ │ ├── incidents.py
│ │ ├── intel.py
│ │ ├── sensor_usage.py
│ │ ├── serverless.py
│ │ └── spotlight.py
│ ├── registry.py
│ ├── resources
│ │ ├── __init__.py
│ │ ├── cloud.py
│ │ ├── detections.py
│ │ ├── discover.py
│ │ ├── hosts.py
│ │ ├── incidents.py
│ │ ├── intel.py
│ │ ├── sensor_usage.py
│ │ ├── serverless.py
│ │ └── spotlight.py
│ └── server.py
├── LICENSE
├── pyproject.toml
├── README.md
├── scripts
│ ├── generate_e2e_report.py
│ └── test_results_viewer.html
├── SUPPORT.md
├── tests
│ ├── __init__.py
│ ├── common
│ │ ├── __init__.py
│ │ ├── test_api_scopes.py
│ │ ├── test_errors.py
│ │ ├── test_logging.py
│ │ └── test_utils.py
│ ├── conftest.py
│ ├── e2e
│ │ ├── __init__.py
│ │ ├── modules
│ │ │ ├── __init__.py
│ │ │ ├── test_cloud.py
│ │ │ ├── test_detections.py
│ │ │ ├── test_discover.py
│ │ │ ├── test_hosts.py
│ │ │ ├── test_idp.py
│ │ │ ├── test_incidents.py
│ │ │ ├── test_intel.py
│ │ │ ├── test_sensor_usage.py
│ │ │ ├── test_serverless.py
│ │ │ └── test_spotlight.py
│ │ └── utils
│ │ ├── __init__.py
│ │ └── base_e2e_test.py
│ ├── modules
│ │ ├── __init__.py
│ │ ├── test_base.py
│ │ ├── test_cloud.py
│ │ ├── test_detections.py
│ │ ├── test_discover.py
│ │ ├── test_hosts.py
│ │ ├── test_idp.py
│ │ ├── test_incidents.py
│ │ ├── test_intel.py
│ │ ├── test_sensor_usage.py
│ │ ├── test_serverless.py
│ │ ├── test_spotlight.py
│ │ └── utils
│ │ └── test_modules.py
│ ├── test_client.py
│ ├── test_registry.py
│ ├── test_server.py
│ └── test_streamable_http_transport.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/tests/e2e/modules/test_idp.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | E2E tests for the Identity Protection (IDP) module.
3 | """
4 |
5 | import unittest
6 |
7 | import pytest
8 |
9 | from tests.e2e.utils.base_e2e_test import BaseE2ETest
10 |
11 |
12 | @pytest.mark.e2e
13 | class TestIdpModuleE2E(BaseE2ETest):
14 | """
15 | End-to-end test suite for the Falcon MCP Server Identity Protection Module.
16 | """
17 |
18 | def test_investigate_entity_comprehensive(self):
19 | """Test comprehensive entity investigation - What can you tell me about the user 'Wallace Muniz'?"""
20 |
21 | async def test_logic():
22 | fixtures = [
23 | # First call: Entity name resolution (search for Wallace Muniz)
24 | {
25 | "operation": "api_preempt_proxy_post_graphql",
26 | "validator": lambda kwargs: (
27 | "primaryDisplayNames" in kwargs.get("body", {}).get("query", "")
28 | and "Wallace Muniz" in kwargs.get("body", {}).get("query", "")
29 | ),
30 | "response": {
31 | "status_code": 200,
32 | "body": {
33 | "data": {
34 | "entities": {
35 | "nodes": [
36 | {
37 | "entityId": "wallace-muniz-001",
38 | "primaryDisplayName": "Wallace Muniz",
39 | }
40 | ]
41 | }
42 | }
43 | },
44 | },
45 | },
46 | # Second call: Comprehensive entity details (default investigation includes entity_details)
47 | {
48 | "operation": "api_preempt_proxy_post_graphql",
49 | "validator": lambda kwargs: (
50 | "entityIds" in kwargs.get("body", {}).get("query", "")
51 | and "wallace-muniz-001"
52 | in kwargs.get("body", {}).get("query", "")
53 | and "riskFactors" in kwargs.get("body", {}).get("query", "")
54 | ),
55 | "response": {
56 | "status_code": 200,
57 | "body": {
58 | "data": {
59 | "entities": {
60 | "nodes": [
61 | {
62 | "entityId": "wallace-muniz-001",
63 | "primaryDisplayName": "Wallace Muniz",
64 | "secondaryDisplayName": "[email protected]",
65 | "type": "USER",
66 | "riskScore": 85.5,
67 | "riskScoreSeverity": "HIGH",
68 | "riskFactors": [
69 | {
70 | "type": "EXCESSIVE_PRIVILEGES",
71 | "severity": "HIGH",
72 | },
73 | {
74 | "type": "SUSPICIOUS_ACTIVITY",
75 | "severity": "MEDIUM",
76 | },
77 | ],
78 | "associations": [
79 | {
80 | "bindingType": "MEMBER_OF",
81 | "entity": {
82 | "entityId": "admin-group-001",
83 | "primaryDisplayName": "Domain Admins",
84 | "secondaryDisplayName": "CORP.LOCAL\\Domain Admins",
85 | "type": "ENTITY_CONTAINER",
86 | },
87 | }
88 | ],
89 | "accounts": [
90 | {
91 | "domain": "CORP.LOCAL",
92 | "samAccountName": "wmuniz",
93 | "passwordAttributes": {
94 | "lastChange": "2024-01-10T08:30:00Z",
95 | "strength": "STRONG",
96 | },
97 | }
98 | ],
99 | "openIncidents": {
100 | "nodes": [
101 | {
102 | "type": "SUSPICIOUS_LOGIN",
103 | "startTime": "2024-01-15T10:30:00Z",
104 | "compromisedEntities": [
105 | {
106 | "entityId": "wallace-muniz-001",
107 | "primaryDisplayName": "Wallace Muniz",
108 | }
109 | ],
110 | }
111 | ]
112 | },
113 | }
114 | ]
115 | }
116 | }
117 | },
118 | },
119 | },
120 | ]
121 |
122 | self._mock_api_instance.command.side_effect = (
123 | self._create_mock_api_side_effect(fixtures)
124 | )
125 |
126 | # Comprehensive question that should trigger entity investigation
127 | prompt = "What can you tell me about the user Wallace Muniz?"
128 | return await self._run_agent_stream(prompt)
129 |
130 | def assertions(tools, result):
131 | # Basic checks - tool was called and we got a result
132 | self.assertGreaterEqual(len(tools), 1, "Expected at least 1 tool call")
133 |
134 | # Check that the IDP investigate entity tool was used
135 | used_tool = tools[-1] # Get the last tool used
136 | tool_name = used_tool["input"]["tool_name"]
137 | self.assertEqual(
138 | tool_name,
139 | "falcon_idp_investigate_entity",
140 | f"Expected idp_investigate_entity tool, got: {tool_name}",
141 | )
142 |
143 | # Check that the tool was called with Wallace Muniz in entity_names
144 | tool_input = used_tool["input"]["tool_input"]
145 | self.assertIn(
146 | "entity_names",
147 | tool_input,
148 | "Tool should be called with entity_names parameter",
149 | )
150 |
151 | entity_names = tool_input.get("entity_names", [])
152 | self.assertTrue(
153 | any("Wallace Muniz" in name for name in entity_names),
154 | f"Tool should be called with Wallace Muniz in entity_names: {entity_names}",
155 | )
156 |
157 | # Check that we got comprehensive result mentioning the entity details
158 | result_lower = result.lower()
159 | self.assertIn("wallace", result_lower, "Result should mention Wallace")
160 |
161 | # Should mention some key details from the comprehensive response
162 | self.assertTrue(
163 | any(
164 | keyword in result_lower
165 | for keyword in ["risk", "high", "privileges", "admin"]
166 | ),
167 | "Result should mention risk-related information",
168 | )
169 |
170 | # Check that the mock API was called at least twice (entity resolution + details)
171 | self.assertGreaterEqual(
172 | self._mock_api_instance.command.call_count,
173 | 2,
174 | "API should be called at least twice for comprehensive investigation",
175 | )
176 |
177 | # Verify the API calls were made with expected GraphQL queries
178 | api_calls = self._mock_api_instance.command.call_args_list
179 |
180 | # First call should be entity name search
181 | first_call_query = api_calls[0][1].get("body", {}).get("query", "")
182 | self.assertIn(
183 | "primaryDisplayNames",
184 | first_call_query,
185 | "First call should search by primaryDisplayNames",
186 | )
187 | self.assertIn(
188 | "Wallace Muniz",
189 | first_call_query,
190 | "First call should search for Wallace Muniz",
191 | )
192 |
193 | # Second call should be detailed entity lookup
194 | second_call_query = api_calls[1][1].get("body", {}).get("query", "")
195 | self.assertIn(
196 | "entityIds", second_call_query, "Second call should lookup by entityIds"
197 | )
198 | self.assertIn(
199 | "riskFactors",
200 | second_call_query,
201 | "Second call should include risk factors",
202 | )
203 |
204 | self.run_test_with_retries(
205 | "test_investigate_entity_comprehensive", test_logic, assertions
206 | )
207 |
208 |
209 | if __name__ == "__main__":
210 | unittest.main()
211 |
```
--------------------------------------------------------------------------------
/falcon_mcp/modules/cloud.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Cloud module for Falcon MCP Server
3 |
4 | This module provides tools for accessing and analyzing CrowdStrike Falcon cloud resources like
5 | Kubernetes & Containers Inventory, Images Vulnerabilities, Cloud Assets.
6 | """
7 |
8 | from textwrap import dedent
9 | from typing import Any, Dict, List
10 |
11 | from mcp.server import FastMCP
12 | from mcp.server.fastmcp.resources import TextResource
13 | from pydantic import AnyUrl, Field
14 |
15 | from falcon_mcp.common.errors import handle_api_response
16 | from falcon_mcp.common.logging import get_logger
17 | from falcon_mcp.common.utils import prepare_api_parameters
18 | from falcon_mcp.modules.base import BaseModule
19 | from falcon_mcp.resources.cloud import (
20 | IMAGES_VULNERABILITIES_FQL_DOCUMENTATION,
21 | KUBERNETES_CONTAINERS_FQL_DOCUMENTATION,
22 | )
23 |
24 | logger = get_logger(__name__)
25 |
26 |
27 | class CloudModule(BaseModule):
28 | """Module for accessing and analyzing CrowdStrike Falcon cloud resources."""
29 |
30 | def register_tools(self, server: FastMCP) -> None:
31 | """Register tools with the MCP server.
32 |
33 | Args:
34 | server: MCP server instance
35 | """
36 | # Register tools
37 | self._add_tool(
38 | server=server,
39 | method=self.search_kubernetes_containers,
40 | name="search_kubernetes_containers",
41 | )
42 |
43 | # fmt: off
44 | self._add_tool(
45 | server=server,
46 | method=self.count_kubernetes_containers,
47 | name="count_kubernetes_containers",
48 | )
49 |
50 | self._add_tool(
51 | server=server,
52 | method=self.search_images_vulnerabilities,
53 | name="search_images_vulnerabilities",
54 | )
55 |
56 | def register_resources(self, server: FastMCP) -> None:
57 | """Register resources with the MCP server.
58 | Args:
59 | server: MCP server instance
60 | """
61 | kubernetes_containers_fql_resource = TextResource(
62 | uri=AnyUrl("falcon://cloud/kubernetes-containers/fql-guide"),
63 | name="falcon_kubernetes_containers_fql_filter_guide",
64 | description="Contains the guide for the `filter` param of the `falcon_search_kubernetes_containers` and `falcon_count_kubernetes_containers` tools.",
65 | text=KUBERNETES_CONTAINERS_FQL_DOCUMENTATION,
66 | )
67 |
68 | images_vulnerabilities_fql_resource = TextResource(
69 | uri=AnyUrl("falcon://cloud/images-vulnerabilities/fql-guide"),
70 | name="falcon_images_vulnerabilities_fql_filter_guide",
71 | description="Contains the guide for the `filter` param of the `falcon_search_images_vulnerabilities` tool.",
72 | text=IMAGES_VULNERABILITIES_FQL_DOCUMENTATION,
73 | )
74 |
75 | self._add_resource(
76 | server,
77 | kubernetes_containers_fql_resource,
78 | )
79 | self._add_resource(
80 | server,
81 | images_vulnerabilities_fql_resource,
82 | )
83 |
84 | def search_kubernetes_containers(
85 | self,
86 | filter: str | None = Field(
87 | default=None,
88 | description="FQL Syntax formatted string used to limit the results. IMPORTANT: use the `falcon://cloud/kubernetes-containers/fql-guide` resource when building this filter parameter.",
89 | examples={"cloud:'AWS'", "cluster_name:'prod'"},
90 | ),
91 | limit: int = Field(
92 | default=10,
93 | ge=1,
94 | le=9999,
95 | description="The maximum number of containers to return in this response (default: 10; max: 9999). Use with the offset parameter to manage pagination of results.",
96 | ),
97 | offset: int | None = Field(
98 | default=None,
99 | description="Starting index of overall result set from which to return containers.",
100 | ),
101 | sort: str | None = Field(
102 | default=None,
103 | description=dedent(
104 | """
105 | Sort kubernetes containers using these options:
106 |
107 | cloud_name: Cloud provider name
108 | cloud_region: Cloud region name
109 | cluster_name: Kubernetes cluster name
110 | container_name: Kubernetes container name
111 | namespace: Kubernetes namespace name
112 | last_seen: Timestamp when the container was last seen
113 | first_seen: Timestamp when the container was first seen
114 | running_status: Container running status which is either true or false
115 |
116 | Sort either asc (ascending) or desc (descending).
117 | Both formats are supported: 'container_name.desc' or 'container_name|desc'
118 |
119 | When searching containers running vulnerable images, use 'image_vulnerability_count.desc' to get container with most images vulnerabilities.
120 |
121 | Examples: 'container_name.desc', 'last_seen.desc'
122 | """
123 | ).strip(),
124 | examples={"container_name.desc", "last_seen.desc"},
125 | ),
126 | ) -> List[Dict[str, Any]]:
127 | """Search for kubernetes containers in your CrowdStrike Kubernetes & Containers Inventory
128 |
129 | IMPORTANT: You must use the `falcon://cloud/kubernetes-containers/fql-guide` resource when you need to use the `filter` parameter.
130 | This resource contains the guide on how to build the FQL `filter` parameter for `falcon_search_kubernetes_containers` tool.
131 | """
132 |
133 | # Prepare parameters
134 | params = prepare_api_parameters(
135 | {
136 | "filter": filter,
137 | "limit": limit,
138 | "offset": offset,
139 | "sort": sort,
140 | }
141 | )
142 |
143 | # Define the operation name
144 | operation = "ReadContainerCombined"
145 |
146 | # Make the API request
147 | response = self.client.command(operation, parameters=params)
148 |
149 | # Handle the response
150 | return handle_api_response(
151 | response,
152 | operation=operation,
153 | error_message="Failed to perform operation",
154 | default_result=[],
155 | )
156 |
157 | def count_kubernetes_containers(
158 | self,
159 | filter: str | None = Field(
160 | default=None,
161 | description="FQL Syntax formatted string used to limit the results. IMPORTANT: use the `falcon://cloud/kubernetes-containers/fql-guide` resource when building this filter parameter.",
162 | examples={"cloud:'Azure'", "container_name:'service'"},
163 | ),
164 | ) -> int:
165 | """Count kubernetes containers in your CrowdStrike Kubernetes & Containers Inventory
166 |
167 | IMPORTANT: You must use the `falcon://cloud/kubernetes-containers/fql-guide` resource when you need to use the `filter` parameter.
168 | This resource contains the guide on how to build the FQL `filter` parameter for `falcon_count_kubernetes_containers` tool.
169 | """
170 |
171 | # Prepare parameters
172 | params = prepare_api_parameters(
173 | {
174 | "filter": filter,
175 | }
176 | )
177 |
178 | # Define the operation name
179 | operation = "ReadContainerCount"
180 |
181 | # Make the API request
182 | response = self.client.command(operation, parameters=params)
183 |
184 | # Handle the response
185 | return handle_api_response(
186 | response,
187 | operation=operation,
188 | error_message="Failed to perform operation",
189 | default_result=[],
190 | )
191 |
192 | def search_images_vulnerabilities(
193 | self,
194 | filter: str | None = Field(
195 | default=None,
196 | description="FQL Syntax formatted string used to limit the results. IMPORTANT: use the `falcon://cloud/images-vulnerabilities/fql-guide` resource when building this filter parameter.",
197 | examples={"cve_id:*'*2025*'", "cvss_score:>5"},
198 | ),
199 | limit: int = Field(
200 | default=10,
201 | ge=1,
202 | le=9999,
203 | description="The maximum number of containers to return in this response (default: 10; max: 9999). Use with the offset parameter to manage pagination of results.",
204 | ),
205 | offset: int | None = Field(
206 | default=None,
207 | description="Starting index of overall result set from which to return containers.",
208 | ),
209 | sort: str | None = Field(
210 | default=None,
211 | description=dedent(
212 | """
213 | Sort images vulnerabilities using these options:
214 |
215 | cps_current_rating: CSP rating of the image vulnerability
216 | cve_id: CVE ID of the image vulnerability
217 | cvss_score: CVSS score of the image vulnerability
218 | images_impacted: Number of images impacted by the vulnerability
219 |
220 | Sort either asc (ascending) or desc (descending).
221 | Both formats are supported: 'container_name.desc' or 'container_name|desc'
222 |
223 | Examples: 'cvss_score.desc', 'cps_current_rating.asc'
224 | """
225 | ).strip(),
226 | examples={"cvss_score.desc", "cps_current_rating.asc"},
227 | ),
228 | ) -> List[Dict[str, Any]]:
229 | """Search for images vulnerabilities in your CrowdStrike Image Assessments
230 |
231 | IMPORTANT: You must use the `falcon://cloud/images-vulnerabilities/fql-guide` resource when you need to use the `filter` parameter.
232 | This resource contains the guide on how to build the FQL `filter` parameter for `falcon_search_images_vulnerabilities` tool.
233 | """
234 |
235 | # Prepare parameters
236 | params = prepare_api_parameters(
237 | {
238 | "filter": filter,
239 | "limit": limit,
240 | "offset": offset,
241 | "sort": sort,
242 | }
243 | )
244 |
245 | # Define the operation name
246 | operation = "ReadCombinedVulnerabilities"
247 |
248 | # Make the API request
249 | response = self.client.command(operation, parameters=params)
250 |
251 | # Handle the response
252 | return handle_api_response(
253 | response,
254 | operation=operation,
255 | error_message="Failed to perform operation",
256 | default_result=[],
257 | )
258 |
```
--------------------------------------------------------------------------------
/falcon_mcp/resources/incidents.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Contains Incidents resources.
3 | """
4 |
5 | from falcon_mcp.common.utils import generate_md_table
6 |
7 | # List of tuples containing filter options data: (name, type, operators, description)
8 | CROWD_SCORE_FQL_FILTERS = [
9 | (
10 | "Name",
11 | "Type",
12 | "Operators",
13 | "Description"
14 | ),
15 | (
16 | "id",
17 | "String",
18 | "Yes",
19 | """
20 | Unique identifier for the CrowdScore entity.
21 | """
22 | ),
23 | (
24 | "cid",
25 | "String",
26 | "No",
27 | """
28 | Customer ID.
29 | """
30 | ),
31 | (
32 | "timestamp",
33 | "Timestamp",
34 | "Yes",
35 | """
36 | Time when the CrowdScore was recorded (UTC).
37 |
38 | Ex: timestamp:>'2023-01-01T00:00:00Z'
39 | """
40 | ),
41 | (
42 | "score",
43 | "Number",
44 | "Yes",
45 | """
46 | The CrowdScore value.
47 |
48 | Ex: score:>50
49 | """
50 | ),
51 | (
52 | "adjusted_score",
53 | "Number",
54 | "Yes",
55 | """
56 | The adjusted CrowdScore value.
57 | """
58 | ),
59 | (
60 | "modified_timestamp",
61 | "Timestamp",
62 | "Yes",
63 | """
64 | Time when the CrowdScore was last modified (UTC).
65 |
66 | Ex: modified_timestamp:>'2023-01-01T00:00:00Z'
67 | """
68 | ),
69 | ]
70 |
71 | CROWD_SCORE_FQL_DOCUMENTATION = """Falcon Query Language (FQL) - CrowdScore Guide
72 |
73 | === BASIC SYNTAX ===
74 | property_name:[operator]'value'
75 |
76 | === AVAILABLE OPERATORS ===
77 | • No operator = equals (default)
78 | • ! = not equal to
79 | • > = greater than
80 | • >= = greater than or equal
81 | • < = less than
82 | • <= = less than or equal
83 | • ~ = text match (ignores case, spaces, punctuation)
84 | • !~ = does not text match
85 | • * = wildcard matching (one or more characters)
86 |
87 | === DATA TYPES & SYNTAX ===
88 | • Strings: 'value' or ['exact_value'] for exact match
89 | • Dates: 'YYYY-MM-DDTHH:MM:SSZ' (UTC format)
90 | • Booleans: true or false (no quotes)
91 | • Numbers: 123 (no quotes)
92 | • Wildcards: 'partial*' or '*partial' or '*partial*'
93 |
94 | === COMBINING CONDITIONS ===
95 | • + = AND condition
96 | • , = OR condition
97 | • ( ) = Group expressions
98 |
99 | === falcon_show_crowd_score FQL filter options ===
100 |
101 | """ + generate_md_table(CROWD_SCORE_FQL_FILTERS) + """
102 |
103 | === EXAMPLE USAGE ===
104 |
105 | • score:>50
106 | • timestamp:>'2023-01-01T00:00:00Z'
107 | • modified_timestamp:>'2023-01-01T00:00:00Z'+score:>70
108 |
109 | === IMPORTANT NOTES ===
110 | • Use single quotes around string values: 'value'
111 | • Use square brackets for exact matches: ['exact_value']
112 | • Date format must be UTC: 'YYYY-MM-DDTHH:MM:SSZ'
113 | """
114 |
115 | # List of tuples containing filter options data: (name, type, operators, description)
116 | SEARCH_INCIDENTS_FQL_FILTERS = [
117 | (
118 | "Name",
119 | "Type",
120 | "Operators",
121 | "Description"
122 | ),
123 | (
124 | "host_ids",
125 | "String",
126 | "No",
127 | """
128 | The device IDs of all the hosts on which the incident occurred.
129 |
130 | Ex: 9a07d39f8c9f430eb3e474d1a0c16ce9
131 | """
132 | ),
133 | (
134 | "lm_host_ids",
135 | "String",
136 | "No",
137 | """
138 | If lateral movement has occurred, this field shows the remote
139 | device IDs of the hosts on which the lateral movement occurred.
140 |
141 | Ex: c4e9e4643999495da6958ea9f21ee597
142 | """
143 | ),
144 | (
145 | "lm_hosts_capped",
146 | "Boolean",
147 | "No",
148 | """
149 | Indicates that the list of lateral movement hosts has been
150 | truncated. The limit is 15 hosts.
151 |
152 | Ex: True
153 | """
154 | ),
155 | (
156 | "name",
157 | "String",
158 | "Yes",
159 | """
160 | The name of the incident. Initially the name is assigned by
161 | CrowdScore, but it can be updated through the API.
162 |
163 | Ex: Incident on DESKTOP-27LTE3R at 2019-12-20T19:56:16Z
164 | """
165 | ),
166 | (
167 | "description",
168 | "String",
169 | "Yes",
170 | """
171 | The description of the incident. Initially the description is
172 | assigned by CrowdScore, but it can be updated through the API.
173 |
174 | Ex: Objectives in this incident: Keep Access.
175 | Techniques: Masquerading.
176 | Involved hosts and end users: DESKTOP-27LTE3R.
177 | """
178 | ),
179 | (
180 | "users",
181 | "String",
182 | "Yes",
183 | """
184 | The usernames of the accounts associated with the incident.
185 |
186 | Ex: someuser
187 | """
188 | ),
189 | (
190 | "tags",
191 | "String",
192 | "Yes",
193 | """
194 | Tags associated with the incident. CrowdScore will assign an
195 | initial set of tags, but tags can be added or removed through
196 | the API.
197 |
198 | Ex: Objective/Keep Access
199 | """
200 | ),
201 | (
202 | "final_score",
203 | "Number",
204 | "Yes",
205 | """
206 | The incident score. Divide the integer by 10 to match the
207 | displayed score for the incident.
208 |
209 | Ex: 56
210 | """
211 | ),
212 | (
213 | "start",
214 | "Timestamp",
215 | "Yes",
216 | """
217 | The recorded time of the earliest behavior.
218 |
219 | Ex: 2017-01-31T22:36:11Z
220 | """
221 | ),
222 | (
223 | "end",
224 | "Timestamp",
225 | "Yes",
226 | """
227 | The recorded time of the latest behavior.
228 |
229 | Ex: 2017-01-31T22:36:11Z
230 | """
231 | ),
232 | (
233 | "assigned_to_name",
234 | "String",
235 | "Yes",
236 | """
237 | The name of the user the incident is assigned to.
238 | """
239 | ),
240 | (
241 | "state",
242 | "String",
243 | "No",
244 | """
245 | The incident state: "open" or "closed"
246 |
247 | Ex: open
248 | """
249 | ),
250 | (
251 | "status",
252 | "Number",
253 | "No",
254 | """
255 | The incident status as a number:
256 | - 20: New
257 | - 25: Reopened
258 | - 30: In Progress
259 | - 40: Closed
260 |
261 | Ex: 20
262 | """
263 | ),
264 | (
265 | "modified_timestamp",
266 | "Timestamp",
267 | "Yes",
268 | """
269 | The most recent time a user has updated the incident.
270 |
271 | Ex: 2021-02-04T05:57:04Z
272 | """
273 | ),
274 | ]
275 |
276 | SEARCH_INCIDENTS_FQL_DOCUMENTATION = """Falcon Query Language (FQL) - Search Incidents Guide
277 |
278 | === BASIC SYNTAX ===
279 | property_name:[operator]'value'
280 |
281 | === AVAILABLE OPERATORS ===
282 | • No operator = equals (default)
283 | • ! = not equal to
284 | • > = greater than
285 | • >= = greater than or equal
286 | • < = less than
287 | • <= = less than or equal
288 | • ~ = text match (ignores case, spaces, punctuation)
289 | • !~ = does not text match
290 | • * = wildcard matching (one or more characters)
291 |
292 | === DATA TYPES & SYNTAX ===
293 | • Strings: 'value' or ['exact_value'] for exact match
294 | • Dates: 'YYYY-MM-DDTHH:MM:SSZ' (UTC format)
295 | • Booleans: true or false (no quotes)
296 | • Numbers: 123 (no quotes)
297 | • Wildcards: 'partial*' or '*partial' or '*partial*'
298 |
299 | === COMBINING CONDITIONS ===
300 | • + = AND condition
301 | • , = OR condition
302 | • ( ) = Group expressions
303 |
304 | === falcon_search_incidents FQL filter options ===
305 |
306 | """ + generate_md_table(SEARCH_INCIDENTS_FQL_FILTERS) + """
307 |
308 | === EXAMPLE USAGE ===
309 |
310 | • state:'open'
311 | • status:'20'
312 | • final_score:>50
313 | • tags:'Objective/Keep Access'
314 | • modified_timestamp:>'2023-01-01T00:00:00Z'
315 | • state:'open'+final_score:>50
316 |
317 | === IMPORTANT NOTES ===
318 | • Use single quotes around string values: 'value'
319 | • Use square brackets for exact matches: ['exact_value']
320 | • Date format must be UTC: 'YYYY-MM-DDTHH:MM:SSZ'
321 | • Status values: 20: New, 25: Reopened, 30: In Progress, 40: Closed
322 | """
323 |
324 | # List of tuples containing filter options data: (name, type, operators, description)
325 | SEARCH_BEHAVIORS_FQL_FILTERS = [
326 | (
327 | "Name",
328 | "Type",
329 | "Operators",
330 | "Description"
331 | ),
332 | (
333 | "aid",
334 | "String",
335 | "No",
336 | """
337 | Agent ID of the host where the behavior was detected.
338 |
339 | Ex: 9a07d39f8c9f430eb3e474d1a0c16ce9
340 | """
341 | ),
342 | (
343 | "behavior_id",
344 | "String",
345 | "No",
346 | """
347 | Unique identifier for the behavior.
348 | """
349 | ),
350 | (
351 | "incident_id",
352 | "String",
353 | "No",
354 | """
355 | Incident ID that this behavior is associated with.
356 | """
357 | ),
358 | (
359 | "tactic",
360 | "String",
361 | "Yes",
362 | """
363 | MITRE ATT&CK tactic associated with the behavior.
364 |
365 | Ex: Defense Evasion
366 | """
367 | ),
368 | (
369 | "technique",
370 | "String",
371 | "Yes",
372 | """
373 | MITRE ATT&CK technique associated with the behavior.
374 |
375 | Ex: Masquerading
376 | """
377 | ),
378 | (
379 | "objective",
380 | "String",
381 | "Yes",
382 | """
383 | Attack objective associated with the behavior.
384 |
385 | Ex: Keep Access
386 | """
387 | ),
388 | (
389 | "timestamp",
390 | "Timestamp",
391 | "Yes",
392 | """
393 | When the behavior occurred (UTC).
394 |
395 | Ex: 2023-01-01T00:00:00Z
396 | """
397 | ),
398 | (
399 | "confidence",
400 | "Number",
401 | "Yes",
402 | """
403 | Confidence level of the behavior detection.
404 |
405 | Ex: 80
406 | """
407 | ),
408 | ]
409 |
410 | SEARCH_BEHAVIORS_FQL_DOCUMENTATION = """Falcon Query Language (FQL) - Search Behaviors Guide
411 |
412 | === BASIC SYNTAX ===
413 | property_name:[operator]'value'
414 |
415 | === AVAILABLE OPERATORS ===
416 | • No operator = equals (default)
417 | • ! = not equal to
418 | • > = greater than
419 | • >= = greater than or equal
420 | • < = less than
421 | • <= = less than or equal
422 | • ~ = text match (ignores case, spaces, punctuation)
423 | • !~ = does not text match
424 | • * = wildcard matching (one or more characters)
425 |
426 | === DATA TYPES & SYNTAX ===
427 | • Strings: 'value' or ['exact_value'] for exact match
428 | • Dates: 'YYYY-MM-DDTHH:MM:SSZ' (UTC format)
429 | • Booleans: true or false (no quotes)
430 | • Numbers: 123 (no quotes)
431 | • Wildcards: 'partial*' or '*partial' or '*partial*'
432 |
433 | === COMBINING CONDITIONS ===
434 | • + = AND condition
435 | • , = OR condition
436 | • ( ) = Group expressions
437 |
438 | === falcon_search_behaviors FQL filter options ===
439 |
440 | """ + generate_md_table(SEARCH_BEHAVIORS_FQL_FILTERS) + """
441 |
442 | === EXAMPLE USAGE ===
443 |
444 | • tactic:'Defense Evasion'
445 | • technique:'Masquerading'
446 | • timestamp:>'2023-01-01T00:00:00Z'
447 | • tactic:'Persistence'+confidence:>80
448 | • objective:'Keep Access'
449 |
450 | === IMPORTANT NOTES ===
451 | • Use single quotes around string values: 'value'
452 | • Use square brackets for exact matches: ['exact_value']
453 | • Date format must be UTC: 'YYYY-MM-DDTHH:MM:SSZ'
454 | """
455 |
```
--------------------------------------------------------------------------------
/scripts/test_results_viewer.html:
--------------------------------------------------------------------------------
```html
1 | <!DOCTYPE html>
2 | <html lang="en">
3 | <head>
4 | <meta charset="UTF-8">
5 | <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 | <title>Test Results</title>
7 | <style>
8 | body {
9 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
10 | background-color: #f4f7f9;
11 | color: #333;
12 | margin: 0;
13 | padding: 20px;
14 | }
15 | h1, h2, h3 {
16 | color: #2c3e50;
17 | }
18 | h1 {
19 | text-align: center;
20 | margin-bottom: 30px;
21 | }
22 | #summary {
23 | background-color: #fff;
24 | padding: 20px;
25 | border-radius: 8px;
26 | box-shadow: 0 2px 4px rgba(0,0,0,0.1);
27 | margin-bottom: 30px;
28 | }
29 | #results-container {
30 | display: flex;
31 | flex-direction: column;
32 | gap: 20px;
33 | }
34 | .test-group {
35 | background-color: #fff;
36 | padding: 20px;
37 | border-radius: 8px;
38 | box-shadow: 0 2px 4px rgba(0,0,0,0.1);
39 | }
40 | .module-group {
41 | background-color: #fff;
42 | padding: 20px;
43 | border-radius: 8px;
44 | box-shadow: 0 2px 4px rgba(0,0,0,0.1);
45 | margin-bottom: 20px;
46 | }
47 | .module-group > h2 {
48 | color: #1976d2;
49 | border-bottom: 2px solid #e3f2fd;
50 | padding-bottom: 10px;
51 | margin-bottom: 20px;
52 | }
53 | .test-group {
54 | background-color: #f8f9fa;
55 | padding: 15px;
56 | border-radius: 6px;
57 | margin-bottom: 15px;
58 | border-left: 4px solid #2196f3;
59 | }
60 | .test-group > h3 {
61 | color: #2c3e50;
62 | margin-top: 0;
63 | margin-bottom: 15px;
64 | }
65 | .model-group {
66 | margin-top: 15px;
67 | padding-left: 20px;
68 | border-left: 3px solid #e0e0e0;
69 | }
70 | .model-group > h4 {
71 | color: #555;
72 | margin-bottom: 10px;
73 | }
74 | .run-grid {
75 | display: grid;
76 | grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
77 | gap: 15px;
78 | margin-top: 10px;
79 | }
80 | .test-run {
81 | padding: 15px;
82 | border-radius: 6px;
83 | border: 1px solid;
84 | }
85 | .test-run.success {
86 | background-color: #e8f5e9;
87 | border-color: #4caf50;
88 | }
89 | .test-run.failure {
90 | background-color: #ffebee;
91 | border-color: #f44336;
92 | }
93 | .test-run h4 {
94 | margin-top: 0;
95 | }
96 | .test-run h5 {
97 | margin-top: 0;
98 | margin-bottom: 10px;
99 | color: #333;
100 | font-size: 14px;
101 | }
102 | .failure-reason {
103 | color: #c62828;
104 | font-family: monospace;
105 | white-space: pre-wrap;
106 | background: #fff0f0;
107 | padding: 5px;
108 | border-radius: 4px;
109 | }
110 | details {
111 | margin-top: 10px;
112 | }
113 | summary {
114 | cursor: pointer;
115 | font-weight: bold;
116 | }
117 | .tools-content, .agent-result {
118 | margin-top: 10px;
119 | background: #fdfdfd;
120 | padding: 10px;
121 | border-radius: 4px;
122 | border: 1px solid #eee;
123 | }
124 | pre {
125 | white-space: pre-wrap;
126 | word-wrap: break-word;
127 | font-size: 13px;
128 | }
129 | code {
130 | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
131 | }
132 | </style>
133 | </head>
134 | <body>
135 | <h1>MCP E2E Test Results</h1>
136 | <div id="summary">
137 | <h2>Summary</h2>
138 | <p>Total Tests Run: <span id="total-tests"></span></p>
139 | <p>Success Rate: <span id="success-rate"></span>%</p>
140 | </div>
141 | <div id="results-container"></div>
142 |
143 | <script>
144 | document.addEventListener('DOMContentLoaded', () => {
145 | fetch('test_results.json')
146 | .then(response => {
147 | if (!response.ok) {
148 | throw new Error('Network response was not ok');
149 | }
150 | return response.json();
151 | })
152 | .then(data => {
153 | renderResults(data);
154 | })
155 | .catch(error => {
156 | console.error('Error fetching or parsing test_results.json:', error);
157 | document.getElementById('results-container').innerHTML = '<p style="color: red;">Could not load test results. Please ensure test_results.json is in the same directory and is valid JSON.</p>';
158 | });
159 | });
160 |
161 | function renderResults(data) {
162 | const container = document.getElementById('results-container');
163 | if (!data || data.length === 0) {
164 | container.innerHTML = '<p>No test results found.</p>';
165 | return;
166 | }
167 |
168 | // Calculate summary
169 | const totalRuns = data.length;
170 | const successfulRuns = data.filter(run => run.status === 'success').length;
171 | const successRate = totalRuns > 0 ? (successfulRuns / totalRuns * 100).toFixed(2) : 0;
172 | document.getElementById('total-tests').textContent = totalRuns;
173 | document.getElementById('success-rate').textContent = successRate;
174 |
175 | // Group first by module, then by test name
176 | const groupedByModule = data.reduce((acc, run) => {
177 | const moduleName = run.module_name || 'Unknown Module';
178 | const testName = run.test_name;
179 |
180 | if (!acc[moduleName]) {
181 | acc[moduleName] = {};
182 | }
183 | if (!acc[moduleName][testName]) {
184 | acc[moduleName][testName] = [];
185 | }
186 | acc[moduleName][testName].push(run);
187 | return acc;
188 | }, {});
189 |
190 | // Render modules
191 | for (const moduleName in groupedByModule) {
192 | const moduleGroupEl = document.createElement('div');
193 | moduleGroupEl.className = 'module-group';
194 | moduleGroupEl.innerHTML = `<h2>${moduleName}</h2>`;
195 |
196 | const tests = groupedByModule[moduleName];
197 |
198 | for (const testName in tests) {
199 | const testGroupEl = document.createElement('div');
200 | testGroupEl.className = 'test-group';
201 | testGroupEl.innerHTML = `<h3>${testName}</h3>`;
202 |
203 | const groupedByModel = tests[testName].reduce((acc, run) => {
204 | const modelName = run.model_name;
205 | if (!acc[modelName]) {
206 | acc[modelName] = [];
207 | }
208 | acc[modelName].push(run);
209 | return acc;
210 | }, {});
211 |
212 | for (const modelName in groupedByModel) {
213 | const modelGroupEl = document.createElement('div');
214 | modelGroupEl.className = 'model-group';
215 | modelGroupEl.innerHTML = `<h4>${modelName}</h4>`;
216 |
217 | const runGridEl = document.createElement('div');
218 | runGridEl.className = 'run-grid';
219 |
220 | groupedByModel[modelName].forEach(run => {
221 | const runEl = document.createElement('div');
222 | runEl.className = `test-run ${run.status}`;
223 | let runContent = `<h5>Run ${run.run_number} - ${run.status.toUpperCase()}</h5>`;
224 |
225 | if (run.status === 'failure' && run.failure_reason) {
226 | runContent += `<p><strong>Failure Reason:</strong></p><pre class="failure-reason"><code>${escapeHtml(run.failure_reason)}</code></pre>`;
227 | }
228 |
229 | runContent += `
230 | <details>
231 | <summary>Agent Result</summary>
232 | <div class="agent-result"><pre><code>${escapeHtml(run.agent_result || 'No result')}</code></pre></div>
233 | </details>
234 | `;
235 |
236 | if (run.tools_used && run.tools_used.length > 0) {
237 | runContent += `
238 | <details>
239 | <summary>Tools Used (${run.tools_used.length})</summary>
240 | <div class="tools-content">
241 | <pre><code>${escapeHtml(JSON.stringify(run.tools_used, null, 2))}</code></pre>
242 | </div>
243 | </details>
244 | `;
245 | } else {
246 | runContent += `<p>No tools were used.</p>`;
247 | }
248 |
249 | runEl.innerHTML = runContent;
250 | runGridEl.appendChild(runEl);
251 | });
252 |
253 | modelGroupEl.appendChild(runGridEl);
254 | testGroupEl.appendChild(modelGroupEl);
255 | }
256 |
257 | moduleGroupEl.appendChild(testGroupEl);
258 | }
259 |
260 | container.appendChild(moduleGroupEl);
261 | }
262 | }
263 |
264 | function escapeHtml(unsafe) {
265 | if (unsafe === null || typeof unsafe === 'undefined') {
266 | return '';
267 | }
268 | return unsafe
269 | .toString()
270 | .replace(/&/g, "&")
271 | .replace(/</g, "<")
272 | .replace(/>/g, ">")
273 | .replace(/"/g, """)
274 | .replace(/'/g, "'");
275 | }
276 |
277 | </script>
278 | </body>
279 | </html>
```
--------------------------------------------------------------------------------
/falcon_mcp/resources/hosts.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Contains Hosts resources.
3 | """
4 |
5 | from falcon_mcp.common.utils import generate_md_table
6 |
7 | # List of tuples containing filter options data: (name, type, operators, description)
8 | SEARCH_HOSTS_FQL_FILTERS = [
9 | (
10 | "Name",
11 | "Type",
12 | "Operators",
13 | "Description"
14 | ),
15 | (
16 | "device_id",
17 | "String",
18 | "No",
19 | """
20 | The ID of the device.
21 |
22 | Ex: 061a51ec742c44624a176f079d742052
23 | """
24 | ),
25 | (
26 | "agent_load_flags",
27 | "String",
28 | "No",
29 | """
30 | Agent configuration field
31 | """
32 | ),
33 | (
34 | "agent_version",
35 | "String",
36 | "No",
37 | """
38 | Agent version.
39 |
40 | Ex: 7.26.17905.0
41 | """
42 | ),
43 | (
44 | "bios_manufacturer",
45 | "String",
46 | "No",
47 | """
48 | BIOS manufacturer.
49 |
50 | Ex: Phoenix Technologies LTD
51 | """
52 | ),
53 | (
54 | "bios_version",
55 | "String",
56 | "No",
57 | """
58 | BIOS version.
59 |
60 | Ex: 6.00
61 | """
62 | ),
63 | (
64 | "config_id_base",
65 | "String",
66 | "No",
67 | """
68 | Agent configuration field
69 | """
70 | ),
71 | (
72 | "config_id_build",
73 | "String",
74 | "No",
75 | """
76 | Agent configuration field
77 | """
78 | ),
79 | (
80 | "config_id_platform",
81 | "String",
82 | "No",
83 | """
84 | Agent configuration field
85 | """
86 | ),
87 | (
88 | "cpu_signature",
89 | "String",
90 | "Yes",
91 | """
92 | CPU signature.
93 |
94 | Ex: GenuineIntel
95 | """
96 | ),
97 | (
98 | "cid",
99 | "String",
100 | "No",
101 | """
102 | Customer ID
103 | """
104 | ),
105 | (
106 | "deployment_type",
107 | "String",
108 | "Yes",
109 | """
110 | Linux deployment type: Standard, DaemonSet
111 | """
112 | ),
113 | (
114 | "external_ip",
115 | "IP Address",
116 | "Yes",
117 | """
118 | External IP address.
119 |
120 | Ex: 192.0.2.100
121 | """
122 | ),
123 | (
124 | "first_seen",
125 | "Timestamp",
126 | "Yes",
127 | """
128 | First connection timestamp (UTC).
129 |
130 | Ex: first_seen:>'2016-07-19T11:14:15Z'
131 | """
132 | ),
133 | (
134 | "groups",
135 | "String",
136 | "No",
137 | """
138 | Host group ID.
139 |
140 | Ex: groups:'0bd018b7bd8b47cc8834228a294eabf2'
141 | """
142 | ),
143 | (
144 | "hostname",
145 | "String",
146 | "No",
147 | """
148 | The name of the machine. ⚠️ LIMITED wildcard support:
149 | - hostname:'PC*' (prefix) - ✅ WORKS
150 | - hostname:'*-01' (suffix) - ✅ WORKS
151 | - hostname:'*server*' (contains) - ❌ FAILS
152 |
153 | Ex: hostname:'WinPC9251' or hostname:'PC*'
154 | """
155 | ),
156 | (
157 | "instance_id",
158 | "String",
159 | "No",
160 | """
161 | Cloud resource information (EC2 instance ID, Azure VM ID,
162 | GCP instance ID, etc.).
163 |
164 | Ex: instance_id:'i-0dc41d0939384cd15'
165 | Ex: instance_id:'f9d3cef9-0123-4567-8901-123456789def'
166 | """
167 | ),
168 | (
169 | "kernel_version",
170 | "String",
171 | "No",
172 | """
173 | Kernel version of the host OS.
174 |
175 | Ex: kernel_version:'6.1.7601.18741'
176 | """
177 | ),
178 | (
179 | "last_login_timestamp",
180 | "Timestamp",
181 | "Yes",
182 | """
183 | User logon event timestamp, once a week.
184 | """
185 | ),
186 | (
187 | "last_seen",
188 | "Timestamp",
189 | "Yes",
190 | """
191 | Last connection timestamp (UTC).
192 |
193 | Ex: last_seen:<'2016-07-19T11:14:15Z'
194 | """
195 | ),
196 | (
197 | "linux_sensor_mode",
198 | "String",
199 | "Yes",
200 | """
201 | Linux sensor mode: Kernel Mode, User Mode
202 | """
203 | ),
204 | (
205 | "local_ip",
206 | "IP Address",
207 | "No",
208 | """
209 | Local IP address.
210 |
211 | Ex: 192.0.2.1
212 | """
213 | ),
214 | (
215 | "local_ip.raw",
216 | "IP Address with wildcards",
217 | "No",
218 | """
219 | Local IP with wildcard support. Use * prefix:
220 |
221 | Ex: local_ip.raw:*'192.0.2.*'
222 | Ex: local_ip.raw:*'*.0.2.100'
223 | """
224 | ),
225 | (
226 | "mac_address",
227 | "String",
228 | "No",
229 | """
230 | The MAC address of the device
231 |
232 | Ex: 2001:db8:ffff:ffff:ffff:ffff:ffff:ffff
233 | """
234 | ),
235 | (
236 | "machine_domain",
237 | "String",
238 | "No",
239 | """
240 | Active Directory domain name.
241 | """
242 | ),
243 | (
244 | "major_version",
245 | "String",
246 | "No",
247 | """
248 | Major version of the Operating System
249 | """
250 | ),
251 | (
252 | "minor_version",
253 | "String",
254 | "No",
255 | """
256 | Minor version of the Operating System
257 | """
258 | ),
259 | (
260 | "modified_timestamp",
261 | "Timestamp",
262 | "Yes",
263 | """
264 | Last record update timestamp (UTC)
265 | """
266 | ),
267 | (
268 | "os_version",
269 | "String",
270 | "No",
271 | """
272 | Operating system version.
273 |
274 | Ex: Windows 7
275 | """
276 | ),
277 | (
278 | "ou",
279 | "String",
280 | "No",
281 | """
282 | Active Directory organizational unit name
283 | """
284 | ),
285 | (
286 | "platform_id",
287 | "String",
288 | "No",
289 | """
290 | Agent configuration field
291 | """
292 | ),
293 | (
294 | "platform_name",
295 | "String",
296 | "No",
297 | """
298 | Operating system platform:
299 | Windows, Mac, Linux
300 | """
301 | ),
302 | (
303 | "product_type_desc",
304 | "String",
305 | "No",
306 | """
307 | Product type: Server, Workstation
308 | """
309 | ),
310 | (
311 | "reduced_functionality_mode",
312 | "String",
313 | "Yes",
314 | """
315 | Reduced functionality mode status: yes, no, or ""
316 |
317 | Ex: reduced_functionality_mode:'no'
318 | """
319 | ),
320 | (
321 | "release_group",
322 | "String",
323 | "No",
324 | """
325 | Deployment group name
326 | """
327 | ),
328 | (
329 | "serial_number",
330 | "String",
331 | "Yes",
332 | """
333 | Serial number of the device.
334 |
335 | Ex: C42AFKEBM563
336 | """
337 | ),
338 | (
339 | "service_provider",
340 | "String",
341 | "No",
342 | """
343 | The cloud service provider.
344 |
345 | Available options:
346 | - AWS_EC2_V2
347 | - AZURE
348 | - GCP
349 |
350 | Ex: service_provider:'AZURE'
351 | """
352 | ),
353 | (
354 | "service_provider_account_id",
355 | "String",
356 | "No",
357 | """
358 | The cloud account ID (AWS Account ID, Azure Subscription ID,
359 | GCP Project ID, etc.).
360 |
361 | Ex: service_provider_account_id:'99841e6a-b123-4567-8901-123456789abc'
362 | """
363 | ),
364 | (
365 | "site_name",
366 | "String",
367 | "No",
368 | """
369 | Active Directory site name.
370 | """
371 | ),
372 | (
373 | "status",
374 | "String",
375 | "No",
376 | """
377 | Containment Status of the machine. "Normal" denotes good
378 | operations; other values might mean reduced functionality
379 | or support.
380 |
381 | Possible values:
382 | - normal
383 | - containment_pending
384 | - contained
385 | - lift_containment_pending
386 | """
387 | ),
388 | (
389 | "system_manufacturer",
390 | "String",
391 | "No",
392 | """
393 | Name of system manufacturer
394 |
395 | Ex: VMware, Inc.
396 | """
397 | ),
398 | (
399 | "system_product_name",
400 | "String",
401 | "No",
402 | """
403 | Name of system product
404 |
405 | Ex: VMware Virtual Platform
406 | """
407 | ),
408 | (
409 | "tags",
410 | "String",
411 | "No",
412 | """
413 | Falcon grouping tags
414 | """
415 | ),
416 | ]
417 |
418 | SEARCH_HOSTS_FQL_DOCUMENTATION = """Falcon Query Language (FQL) - Search Hosts Guide
419 |
420 | === BASIC SYNTAX ===
421 | property_name:[operator]'value'
422 |
423 | === AVAILABLE OPERATORS ===
424 |
425 | ✅ **WORKING OPERATORS:**
426 | • No operator = equals (default) - ALL FIELDS
427 | • ! = not equal to - ALL FIELDS
428 | • > = greater than - TIMESTAMP FIELDS ONLY
429 | • >= = greater than or equal - TIMESTAMP FIELDS ONLY
430 | • < = less than - TIMESTAMP FIELDS ONLY
431 | • <= = less than or equal - TIMESTAMP FIELDS ONLY
432 | • ~ = text match (case insensitive) - TEXT FIELDS ONLY
433 | • * = wildcard matching - LIMITED SUPPORT (see examples below)
434 |
435 | ❌ **NON-WORKING OPERATORS:**
436 | • !~ = does not text match - NOT SUPPORTED
437 | • Simple wildcards (field:*) - NOT SUPPORTED
438 |
439 | === DATA TYPES & SYNTAX ===
440 | • Strings: 'value' or ['exact_value'] for exact match
441 | • Dates: 'YYYY-MM-DDTHH:MM:SSZ' (UTC format)
442 | • Booleans: true or false (no quotes)
443 | • Numbers: 123 (no quotes)
444 | • Wildcards: 'partial*' or '*partial' or '*partial*'
445 |
446 | === COMBINING CONDITIONS ===
447 | • + = AND condition
448 | • , = OR condition
449 | • ( ) = Group expressions
450 |
451 | === falcon_search_hosts FQL filter options ===
452 |
453 | """ + generate_md_table(SEARCH_HOSTS_FQL_FILTERS) + """
454 |
455 | === ✅ WORKING PATTERNS ===
456 |
457 | **Basic Equality:**
458 | • platform_name:'Windows', platform_name:'Linux', platform_name:'Mac'
459 | • product_type_desc:'Server', product_type_desc:'Workstation'
460 | • status:'normal', reduced_functionality_mode:'no'
461 | • service_provider:'AZURE', service_provider:'AWS_EC2_V2', service_provider:'GCP'
462 |
463 | **Combined Conditions:**
464 | • service_provider:'AZURE'+platform_name:'Linux'
465 | • platform_name:'Linux'+product_type_desc:'Server'
466 | • (service_provider:'AZURE',service_provider:'AWS_EC2_V2')+platform_name:'Linux'
467 |
468 | **Timestamp Comparisons:**
469 | • first_seen:>'2020-01-01T00:00:00Z'
470 | • first_seen:>='2020-01-01T00:00:00Z'
471 | • last_seen:<='2024-12-31T23:59:59Z'
472 |
473 | **Inequality Filters:**
474 | • platform_name:!'Windows' (non-Windows hosts)
475 | • service_provider_account_id:!'' (not empty)
476 | • instance_id:!'' (not empty)
477 |
478 | **Hostname Wildcards (Limited):**
479 | • hostname:'PC*' (prefix) ✅
480 | • hostname:'*-01' (suffix) ✅
481 | • hostname:'*server*' (contains) ❌ Does NOT work
482 |
483 | **IP Address Wildcards:**
484 | • local_ip.raw:*'192.168.*'
485 | • local_ip.raw:*'10.*'
486 |
487 | **Text Match:**
488 | • hostname:~'server'
489 | • os_version:~'windows'
490 |
491 | === ❌ PATTERNS TO AVOID ===
492 | • Simple wildcards: service_provider_account_id:*, hostname:*, etc.
493 | • Contains wildcards: hostname:'*server*'
494 | • Wrong IP syntax: local_ip:*
495 |
496 | === 💡 SYNTAX RULES ===
497 | • Use single quotes around string values: 'value'
498 | • Date format must be UTC: 'YYYY-MM-DDTHH:MM:SSZ'
499 | • Combine conditions with + (AND) or , (OR)
500 | • Use parentheses for grouping: (condition1,condition2)+condition3
501 | """
502 |
```
--------------------------------------------------------------------------------
/tests/modules/test_intel.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Tests for the Intel module.
3 | """
4 |
5 | import unittest
6 |
7 | from falcon_mcp.modules.intel import IntelModule
8 | from tests.modules.utils.test_modules import TestModules
9 |
10 |
11 | class TestIntelModule(TestModules):
12 | """Test cases for the Intel module."""
13 |
14 | def setUp(self):
15 | """Set up test fixtures."""
16 | self.setup_module(IntelModule)
17 |
18 | def test_register_tools(self):
19 | """Test registering tools with the server."""
20 | expected_tools = [
21 | "falcon_search_actors",
22 | "falcon_search_indicators",
23 | "falcon_search_reports",
24 | ]
25 | self.assert_tools_registered(expected_tools)
26 |
27 | def test_register_resources(self):
28 | """Test registering resources with the server."""
29 | expected_resources = [
30 | "falcon_search_actors_fql_guide",
31 | "falcon_search_indicators_fql_guide",
32 | "falcon_search_reports_fql_guide",
33 | ]
34 | self.assert_resources_registered(expected_resources)
35 |
36 | def test_search_actors_success(self):
37 | """Test searching actors with successful response."""
38 | # Setup mock response with sample actors
39 | mock_response = {
40 | "status_code": 200,
41 | "body": {
42 | "resources": [
43 | {"id": "actor1", "name": "Actor 1", "description": "Description 1"},
44 | {"id": "actor2", "name": "Actor 2", "description": "Description 2"},
45 | ]
46 | },
47 | }
48 | self.mock_client.command.return_value = mock_response
49 |
50 | # Call search_actors with test parameters
51 | result = self.module.query_actor_entities(
52 | filter="name:'Actor*'",
53 | limit=100,
54 | offset=0,
55 | sort="name.asc",
56 | q="test",
57 | )
58 |
59 | # Verify client command was called correctly
60 | self.mock_client.command.assert_called_once_with(
61 | "QueryIntelActorEntities",
62 | parameters={
63 | "filter": "name:'Actor*'",
64 | "limit": 100,
65 | "offset": 0,
66 | "sort": "name.asc",
67 | "q": "test",
68 | },
69 | )
70 |
71 | # Verify result contains expected values
72 | self.assertEqual(len(result), 2)
73 | self.assertEqual(result[0]["id"], "actor1")
74 | self.assertEqual(result[1]["id"], "actor2")
75 |
76 | def test_search_actors_empty_response(self):
77 | """Test searching actors with empty response."""
78 | # Setup mock response with empty resources
79 | mock_response = {"status_code": 200, "body": {"resources": []}}
80 | self.mock_client.command.return_value = mock_response
81 |
82 | # Call search_actors
83 | result = self.module.query_actor_entities()
84 |
85 | # Verify client command was called with the correct operation
86 | self.assertEqual(self.mock_client.command.call_count, 1)
87 | call_args = self.mock_client.command.call_args
88 | self.assertEqual(call_args[0][0], "QueryIntelActorEntities")
89 |
90 | # Verify result is an empty list
91 | self.assertEqual(result, [])
92 |
93 | def test_search_actors_error(self):
94 | """Test searching actors with API error."""
95 | # Setup mock response with error
96 | mock_response = {
97 | "status_code": 400,
98 | "body": {"errors": [{"message": "Invalid query"}]},
99 | }
100 | self.mock_client.command.return_value = mock_response
101 |
102 | # Call search_actors
103 | results = self.module.query_actor_entities(filter="invalid query")
104 | result = results[0]
105 |
106 | # Verify result contains error
107 | self.assertIn("error", result)
108 | self.assertIn("details", result)
109 | # Check that the error message starts with the expected prefix
110 | self.assertTrue(result["error"].startswith("Failed to search actors"))
111 |
112 | def test_query_indicator_entities_success(self):
113 | """Test querying indicator entities with successful response."""
114 | # Setup mock response with sample indicators
115 | mock_response = {
116 | "status_code": 200,
117 | "body": {
118 | "resources": [
119 | {
120 | "id": "indicator1",
121 | "indicator": "malicious.com",
122 | "type": "domain",
123 | },
124 | {
125 | "id": "indicator2",
126 | "indicator": "192.168.1.1",
127 | "type": "ip_address",
128 | },
129 | ]
130 | },
131 | }
132 | self.mock_client.command.return_value = mock_response
133 |
134 | # Call query_indicator_entities with test parameters
135 | result = self.module.query_indicator_entities(
136 | filter="type:'domain'",
137 | limit=100,
138 | offset=0,
139 | sort="published_date.desc",
140 | q="malicious",
141 | include_deleted=True,
142 | include_relations=True,
143 | )
144 |
145 | # Verify client command was called correctly
146 | self.mock_client.command.assert_called_once_with(
147 | "QueryIntelIndicatorEntities",
148 | parameters={
149 | "filter": "type:'domain'",
150 | "limit": 100,
151 | "offset": 0,
152 | "sort": "published_date.desc",
153 | "q": "malicious",
154 | "include_deleted": True,
155 | "include_relations": True,
156 | },
157 | )
158 |
159 | # Verify result contains expected values
160 | self.assertEqual(len(result), 2)
161 | self.assertEqual(result[0]["id"], "indicator1")
162 | self.assertEqual(result[1]["id"], "indicator2")
163 |
164 | def test_query_indicator_entities_empty_response(self):
165 | """Test querying indicator entities with empty response."""
166 | # Setup mock response with empty resources
167 | mock_response = {"status_code": 200, "body": {"resources": []}}
168 | self.mock_client.command.return_value = mock_response
169 |
170 | # Call query_indicator_entities
171 | result = self.module.query_indicator_entities()
172 |
173 | # Verify client command was called with the correct operation
174 | self.assertEqual(self.mock_client.command.call_count, 1)
175 | call_args = self.mock_client.command.call_args
176 | self.assertEqual(call_args[0][0], "QueryIntelIndicatorEntities")
177 |
178 | # Verify result is an empty list
179 | self.assertEqual(result, [])
180 |
181 | def test_query_indicator_entities_error(self):
182 | """Test querying indicator entities with API error."""
183 | # Setup mock response with error
184 | mock_response = {
185 | "status_code": 400,
186 | "body": {"errors": [{"message": "Invalid query"}]},
187 | }
188 | self.mock_client.command.return_value = mock_response
189 |
190 | # Call query_indicator_entities
191 | result = self.module.query_indicator_entities(filter="invalid query")
192 |
193 | # Verify result contains error
194 | self.assertEqual(len(result), 1)
195 | self.assertIn("error", result[0])
196 | self.assertIn("details", result[0])
197 | # Check that the error message starts with the expected prefix
198 | self.assertTrue(result[0]["error"].startswith("Failed to search indicators"))
199 |
200 | def test_query_report_entities_success(self):
201 | """Test querying report entities with successful response."""
202 | # Setup mock response with sample reports
203 | mock_response = {
204 | "status_code": 200,
205 | "body": {
206 | "resources": [
207 | {
208 | "id": "report1",
209 | "name": "Report 1",
210 | "description": "Description 1",
211 | },
212 | {
213 | "id": "report2",
214 | "name": "Report 2",
215 | "description": "Description 2",
216 | },
217 | ]
218 | },
219 | }
220 | self.mock_client.command.return_value = mock_response
221 |
222 | # Call query_report_entities with test parameters
223 | result = self.module.query_report_entities(
224 | filter="name:'Report*'",
225 | limit=100,
226 | offset=0,
227 | sort="created_date.desc",
228 | q="test",
229 | )
230 |
231 | # Verify client command was called correctly
232 | self.mock_client.command.assert_called_once_with(
233 | "QueryIntelReportEntities",
234 | parameters={
235 | "filter": "name:'Report*'",
236 | "limit": 100,
237 | "offset": 0,
238 | "sort": "created_date.desc",
239 | "q": "test",
240 | },
241 | )
242 |
243 | # Verify result contains expected values
244 | self.assertEqual(len(result), 2)
245 | self.assertEqual(result[0]["id"], "report1")
246 | self.assertEqual(result[1]["id"], "report2")
247 |
248 | def test_query_report_entities_empty_response(self):
249 | """Test querying report entities with empty response."""
250 | # Setup mock response with empty resources
251 | mock_response = {"status_code": 200, "body": {"resources": []}}
252 | self.mock_client.command.return_value = mock_response
253 |
254 | # Call query_report_entities
255 | result = self.module.query_report_entities()
256 |
257 | # Verify client command was called with the correct operation
258 | self.assertEqual(self.mock_client.command.call_count, 1)
259 | call_args = self.mock_client.command.call_args
260 | self.assertEqual(call_args[0][0], "QueryIntelReportEntities")
261 |
262 | # Verify result is an empty list
263 | self.assertEqual(result, [])
264 |
265 | def test_query_report_entities_error(self):
266 | """Test querying report entities with API error."""
267 | # Setup mock response with error
268 | mock_response = {
269 | "status_code": 400,
270 | "body": {"errors": [{"message": "Invalid query"}]},
271 | }
272 | self.mock_client.command.return_value = mock_response
273 |
274 | # Call query_report_entities
275 | result = self.module.query_report_entities(filter="invalid query")
276 |
277 | # Verify result contains error
278 | self.assertEqual(len(result), 1)
279 | self.assertIn("error", result[0])
280 | self.assertIn("details", result[0])
281 | # Check that the error message starts with the expected prefix
282 | self.assertTrue(result[0]["error"].startswith("Failed to search reports"))
283 |
284 |
285 | if __name__ == "__main__":
286 | unittest.main()
287 |
```
--------------------------------------------------------------------------------
/tests/test_client.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Tests for the Falcon API client.
3 | """
4 |
5 | import platform
6 | import sys
7 | import unittest
8 | from unittest.mock import MagicMock, patch
9 |
10 | from falcon_mcp.client import FalconClient
11 |
12 |
13 | class TestFalconClient(unittest.TestCase):
14 | """Test cases for the Falcon API client."""
15 |
16 | @patch("falcon_mcp.client.os.environ.get")
17 | @patch("falcon_mcp.client.APIHarnessV2")
18 | def test_client_initialization(self, mock_apiharness, mock_environ_get):
19 | """Test client initialization with base URL."""
20 | # Setup mock environment variables
21 | mock_environ_get.side_effect = lambda key, default=None: {
22 | "FALCON_CLIENT_ID": "test-client-id",
23 | "FALCON_CLIENT_SECRET": "test-client-secret",
24 | }.get(key, default)
25 |
26 | # Create client with base URL
27 | _client = FalconClient(base_url="https://api.test.crowdstrike.com", debug=True)
28 |
29 | # Verify APIHarnessV2 was initialized correctly with config values
30 | mock_apiharness.assert_called_once()
31 | call_args = mock_apiharness.call_args[1]
32 | self.assertEqual(call_args["client_id"], "test-client-id")
33 | self.assertEqual(call_args["client_secret"], "test-client-secret")
34 | self.assertEqual(call_args["base_url"], "https://api.test.crowdstrike.com")
35 | self.assertTrue(call_args["debug"])
36 |
37 | @patch("falcon_mcp.client.os.environ.get")
38 | @patch("falcon_mcp.client.APIHarnessV2")
39 | def test_client_initialization_with_env_vars(
40 | self, mock_apiharness, mock_environ_get
41 | ):
42 | """Test client initialization with environment variables."""
43 | # Setup mock environment variables
44 | mock_environ_get.side_effect = lambda key, default=None: {
45 | "FALCON_CLIENT_ID": "env-client-id",
46 | "FALCON_CLIENT_SECRET": "env-client-secret",
47 | "FALCON_BASE_URL": "https://api.env.crowdstrike.com",
48 | }.get(key, default)
49 |
50 | # Create client with environment variables
51 | _client = FalconClient()
52 |
53 | # Verify APIHarnessV2 was initialized correctly
54 | mock_apiharness.assert_called_once()
55 | call_args = mock_apiharness.call_args[1]
56 | self.assertEqual(call_args["client_id"], "env-client-id")
57 | self.assertEqual(call_args["client_secret"], "env-client-secret")
58 | self.assertEqual(call_args["base_url"], "https://api.env.crowdstrike.com")
59 | self.assertFalse(call_args["debug"])
60 |
61 | @patch("falcon_mcp.client.os.environ.get")
62 | def test_client_initialization_missing_credentials(self, mock_environ_get):
63 | """Test client initialization with missing credentials."""
64 | # Setup mock environment variables (missing credentials)
65 | mock_environ_get.return_value = None
66 |
67 | # Verify ValueError is raised when credentials are missing
68 | with self.assertRaises(ValueError):
69 | FalconClient()
70 |
71 | @patch("falcon_mcp.client.os.environ.get")
72 | @patch("falcon_mcp.client.APIHarnessV2")
73 | def test_authenticate(self, mock_apiharness, mock_environ_get):
74 | """Test authenticate method."""
75 | # Setup mock environment variables
76 | mock_environ_get.side_effect = lambda key, default=None: {
77 | "FALCON_CLIENT_ID": "test-client-id",
78 | "FALCON_CLIENT_SECRET": "test-client-secret",
79 | }.get(key, default)
80 |
81 | # Setup mock
82 | mock_instance = MagicMock()
83 | mock_instance.login.return_value = True
84 | mock_apiharness.return_value = mock_instance
85 |
86 | # Create client and authenticate
87 | client = FalconClient()
88 | result = client.authenticate()
89 |
90 | # Verify login was called and result is correct
91 | mock_instance.login.assert_called_once()
92 | self.assertTrue(result)
93 |
94 | @patch("falcon_mcp.client.os.environ.get")
95 | @patch("falcon_mcp.client.APIHarnessV2")
96 | def test_is_authenticated(self, mock_apiharness, mock_environ_get):
97 | """Test is_authenticated method."""
98 | # Setup mock environment variables
99 | mock_environ_get.side_effect = lambda key, default=None: {
100 | "FALCON_CLIENT_ID": "test-client-id",
101 | "FALCON_CLIENT_SECRET": "test-client-secret",
102 | }.get(key, default)
103 |
104 | # Setup mock
105 | mock_instance = MagicMock()
106 | mock_instance.token_valid = True
107 | mock_apiharness.return_value = mock_instance
108 |
109 | # Create client and check authentication status
110 | client = FalconClient()
111 | result = client.is_authenticated()
112 |
113 | # Verify result is correct
114 | self.assertTrue(result)
115 |
116 | @patch("falcon_mcp.client.os.environ.get")
117 | @patch("falcon_mcp.client.APIHarnessV2")
118 | def test_get_headers(self, mock_apiharness, mock_environ_get):
119 | """Test get_headers method."""
120 | # Setup mock environment variables
121 | mock_environ_get.side_effect = lambda key, default=None: {
122 | "FALCON_CLIENT_ID": "test-client-id",
123 | "FALCON_CLIENT_SECRET": "test-client-secret",
124 | }.get(key, default)
125 |
126 | # Setup mock
127 | mock_instance = MagicMock()
128 | mock_instance.auth_headers = {"Authorization": "Bearer test-token"}
129 | mock_apiharness.return_value = mock_instance
130 |
131 | # Create client and get headers
132 | client = FalconClient()
133 | headers = client.get_headers()
134 |
135 | # Verify headers are correct
136 | self.assertEqual(headers, {"Authorization": "Bearer test-token"})
137 |
138 | @patch("falcon_mcp.client.os.environ.get")
139 | @patch("falcon_mcp.client.APIHarnessV2")
140 | def test_command(self, mock_apiharness, mock_environ_get):
141 | """Test command method."""
142 | # Setup mock environment variables
143 | mock_environ_get.side_effect = lambda key, default=None: {
144 | "FALCON_CLIENT_ID": "test-client-id",
145 | "FALCON_CLIENT_SECRET": "test-client-secret",
146 | }.get(key, default)
147 |
148 | # Setup mock
149 | mock_instance = MagicMock()
150 | mock_instance.command.return_value = {
151 | "status_code": 200,
152 | "body": {"resources": [{"id": "test"}]},
153 | }
154 | mock_apiharness.return_value = mock_instance
155 |
156 | # Create client and execute command
157 | client = FalconClient()
158 | response = client.command("TestOperation", parameters={"filter": "test"})
159 |
160 | # Verify command was called with correct arguments
161 | mock_instance.command.assert_called_once_with(
162 | "TestOperation", parameters={"filter": "test"}
163 | )
164 |
165 | # Verify response is correct
166 | self.assertEqual(response["status_code"], 200)
167 | self.assertEqual(response["body"]["resources"][0]["id"], "test")
168 |
169 | @patch("falcon_mcp.client.version")
170 | @patch("falcon_mcp.client.os.environ.get")
171 | @patch("falcon_mcp.client.APIHarnessV2")
172 | def test_get_user_agent_best_case_scenario(
173 | self, mock_apiharness, mock_environ_get, mock_version
174 | ):
175 | """Test get_user_agent method in the best case scenario with all packages installed."""
176 | # Setup mock environment variables
177 | mock_environ_get.side_effect = lambda key, default=None: {
178 | "FALCON_CLIENT_ID": "test-client-id",
179 | "FALCON_CLIENT_SECRET": "test-client-secret",
180 | }.get(key, default)
181 |
182 | # Setup mock version calls for best case scenario
183 | def version_side_effect(package_name):
184 | if package_name == "falcon-mcp":
185 | return "1.2.3"
186 | if package_name == "crowdstrike-falconpy":
187 | return "1.3.4"
188 | raise ValueError(f"Unexpected package: {package_name}")
189 |
190 | mock_version.side_effect = version_side_effect
191 | mock_apiharness.return_value = MagicMock()
192 |
193 | # Create client and get user agent
194 | client = FalconClient()
195 | user_agent = client.get_user_agent()
196 |
197 | # Verify user agent format and content
198 | python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
199 | platform_info = f"{platform.system()}/{platform.release()}"
200 | expected = f"falcon-mcp/1.2.3 (falconpy/1.3.4; Python/{python_version}; {platform_info})"
201 |
202 | self.assertEqual(user_agent, expected)
203 |
204 | # Verify user agent is properly used in APIHarnessV2 initialization
205 | mock_apiharness.assert_called_once()
206 | call_args = mock_apiharness.call_args[1]
207 | self.assertEqual(call_args["user_agent"], expected)
208 |
209 | # Verify format components
210 | self.assertTrue(user_agent.startswith("falcon-mcp/1.2.3"))
211 | self.assertIn(f"Python/{python_version}", user_agent)
212 | self.assertIn(platform_info, user_agent)
213 | self.assertIn("falconpy/1.3.4", user_agent)
214 |
215 | @patch("falcon_mcp.client.version")
216 | @patch("falcon_mcp.client.os.environ.get")
217 | @patch("falcon_mcp.client.APIHarnessV2")
218 | def test_get_user_agent_with_user_agent_comment(
219 | self, mock_apiharness, mock_environ_get, mock_version
220 | ):
221 | """Test get_user_agent method with a user agent comment."""
222 | # Setup mock environment variables
223 | mock_environ_get.side_effect = lambda key, default=None: {
224 | "FALCON_CLIENT_ID": "test-client-id",
225 | "FALCON_CLIENT_SECRET": "test-client-secret",
226 | }.get(key, default)
227 |
228 | # Setup mock version calls
229 | def version_side_effect(package_name):
230 | if package_name == "falcon-mcp":
231 | return "1.2.3"
232 | if package_name == "crowdstrike-falconpy":
233 | return "1.3.4"
234 | raise ValueError(f"Unexpected package: {package_name}")
235 |
236 | mock_version.side_effect = version_side_effect
237 | mock_apiharness.return_value = MagicMock()
238 |
239 | # Create client with user agent comment
240 | client = FalconClient(user_agent_comment="CustomApp/1.0")
241 | user_agent = client.get_user_agent()
242 |
243 | # Verify user agent format and content (RFC-compliant format)
244 | python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
245 | platform_info = f"{platform.system()}/{platform.release()}"
246 | expected = f"falcon-mcp/1.2.3 (CustomApp/1.0; falconpy/1.3.4; Python/{python_version}; {platform_info})"
247 |
248 | self.assertEqual(user_agent, expected)
249 |
250 | # Verify user agent is properly used in APIHarnessV2 initialization
251 | mock_apiharness.assert_called_once()
252 | call_args = mock_apiharness.call_args[1]
253 | self.assertEqual(call_args["user_agent"], expected)
254 |
255 |
256 | if __name__ == "__main__":
257 | unittest.main()
258 |
```
--------------------------------------------------------------------------------
/falcon_mcp/modules/intel.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Intel module for Falcon MCP Server
3 |
4 | This module provides tools for accessing and analyzing CrowdStrike Falcon intelligence data.
5 | """
6 |
7 | from typing import Any, Dict, List
8 |
9 | from mcp.server import FastMCP
10 | from mcp.server.fastmcp.resources import TextResource
11 | from pydantic import AnyUrl, Field
12 |
13 | from falcon_mcp.common.errors import handle_api_response
14 | from falcon_mcp.common.logging import get_logger
15 | from falcon_mcp.common.utils import prepare_api_parameters
16 | from falcon_mcp.modules.base import BaseModule
17 | from falcon_mcp.resources.intel import (
18 | QUERY_ACTOR_ENTITIES_FQL_DOCUMENTATION,
19 | QUERY_INDICATOR_ENTITIES_FQL_DOCUMENTATION,
20 | QUERY_REPORT_ENTITIES_FQL_DOCUMENTATION,
21 | )
22 |
23 | logger = get_logger(__name__)
24 |
25 |
26 | class IntelModule(BaseModule):
27 | """Module for accessing and analyzing CrowdStrike Falcon intelligence data."""
28 |
29 | def register_tools(self, server: FastMCP) -> None:
30 | """Register tools with the MCP server.
31 |
32 | Args:
33 | server: MCP server instance
34 | """
35 | # Register tools
36 | self._add_tool(
37 | server=server,
38 | method=self.query_actor_entities,
39 | name="search_actors",
40 | )
41 |
42 | self._add_tool(
43 | server=server,
44 | method=self.query_indicator_entities,
45 | name="search_indicators",
46 | )
47 |
48 | self._add_tool(
49 | server=server,
50 | method=self.query_report_entities,
51 | name="search_reports",
52 | )
53 |
54 | def register_resources(self, server: FastMCP) -> None:
55 | """Register resources with the MCP server.
56 |
57 | Args:
58 | server: MCP server instance
59 | """
60 | search_actors_fql_resource = TextResource(
61 | uri=AnyUrl("falcon://intel/actors/fql-guide"),
62 | name="falcon_search_actors_fql_guide",
63 | description="Contains the guide for the `filter` param of the `falcon_search_actors` tool.",
64 | text=QUERY_ACTOR_ENTITIES_FQL_DOCUMENTATION,
65 | )
66 |
67 | search_indicators_fql_resource = TextResource(
68 | uri=AnyUrl("falcon://intel/indicators/fql-guide"),
69 | name="falcon_search_indicators_fql_guide",
70 | description="Contains the guide for the `filter` param of the `falcon_search_indicators` tool.",
71 | text=QUERY_INDICATOR_ENTITIES_FQL_DOCUMENTATION,
72 | )
73 |
74 | search_reports_fql_resource = TextResource(
75 | uri=AnyUrl("falcon://intel/reports/fql-guide"),
76 | name="falcon_search_reports_fql_guide",
77 | description="Contains the guide for the `filter` param of the `falcon_search_reports` tool.",
78 | text=QUERY_REPORT_ENTITIES_FQL_DOCUMENTATION,
79 | )
80 |
81 | self._add_resource(
82 | server,
83 | search_actors_fql_resource,
84 | )
85 | self._add_resource(
86 | server,
87 | search_indicators_fql_resource,
88 | )
89 | self._add_resource(
90 | server,
91 | search_reports_fql_resource,
92 | )
93 |
94 | def query_actor_entities(
95 | self,
96 | filter: str | None = Field(
97 | default=None,
98 | description="FQL query expression that should be used to limit the results. IMPORTANT: use the `falcon://intel/actors/fql-guide` resource when building this filter parameter.",
99 | ),
100 | limit: int = Field(
101 | default=10,
102 | ge=1,
103 | le=5000,
104 | description="Maximum number of records to return. Max 5000",
105 | examples={10, 20, 100},
106 | ),
107 | offset: int | None = Field(
108 | default=None,
109 | description="Starting index of overall result set from which to return ids.",
110 | examples=[0, 10],
111 | ),
112 | sort: str | None = Field(
113 | default=None,
114 | description="The field and direction to sort results on. The format is {field}|{asc/desc}. Valid values include: name, target_countries, target_industries, type, created_date, last_activity_date and last_modified_date. Ex: created_date|desc",
115 | examples={"created_date|desc"},
116 | ),
117 | q: str | None = Field(
118 | default=None,
119 | description="Free text search across all indexed fields.",
120 | examples={"BEAR"},
121 | ),
122 | ) -> List[Dict[str, Any]]:
123 | """Research threat actors and adversary groups tracked by CrowdStrike intelligence.
124 |
125 | IMPORTANT: You must use the `falcon://intel/actors/fql-guide` resource when you need to use the `filter` parameter.
126 | This resource contains the guide on how to build the FQL `filter` parameter for the `falcon_search_actors` tool.
127 | """
128 | # Prepare parameters
129 | params = prepare_api_parameters(
130 | {
131 | "filter": filter,
132 | "limit": limit,
133 | "offset": offset,
134 | "sort": sort,
135 | "q": q,
136 | }
137 | )
138 |
139 | # Define the operation name
140 | operation = "QueryIntelActorEntities"
141 |
142 | logger.debug("Searching actors with params: %s", params)
143 |
144 | # Make the API request
145 | command_response = self.client.command(operation, parameters=params)
146 |
147 | # Handle the response
148 | api_response = handle_api_response(
149 | command_response,
150 | operation=operation,
151 | error_message="Failed to search actors",
152 | default_result=[],
153 | )
154 |
155 | if self._is_error(api_response):
156 | return [api_response]
157 |
158 | return api_response
159 |
160 | def query_indicator_entities(
161 | self,
162 | filter: str | None = Field(
163 | default=None,
164 | description="FQL query expression that should be used to limit the results. IMPORTANT: use the `falcon://intel/indicators/fql-guide` resource when building this filter parameter.",
165 | ),
166 | limit: int = Field(
167 | default=10,
168 | ge=1,
169 | le=5000,
170 | description="Maximum number of records to return. (Max: 5000)",
171 | ),
172 | offset: int | None = Field(
173 | default=None,
174 | description="Starting index of overall result set from which to return ids.",
175 | ),
176 | sort: str | None = Field(
177 | default=None,
178 | description="The field and direction to sort results on. The format is {field}|{asc/desc}. Valid values are: id, indicator, type, published_date, last_updated, and _marker. Ex: published_date|desc",
179 | examples={"published_date|desc"},
180 | ),
181 | q: str | None = Field(
182 | default=None,
183 | description="Free text search across all indexed fields.",
184 | ),
185 | include_deleted: bool = Field(
186 | default=False,
187 | description="Flag indicating if both published and deleted indicators should be returned.",
188 | ),
189 | include_relations: bool = Field(
190 | default=False,
191 | description="Flag indicating if related indicators should be returned.",
192 | ),
193 | ) -> List[Dict[str, Any]]:
194 | """Search for threat indicators and indicators of compromise (IOCs) from CrowdStrike intelligence.
195 |
196 | IMPORTANT: You must use the `falcon://intel/indicators/fql-guide` resource when you need to use the `filter` parameter.
197 | This resource contains the guide on how to build the FQL `filter` parameter for the `falcon_search_indicators` tool.
198 | """
199 | # Prepare parameters
200 | params = prepare_api_parameters(
201 | {
202 | "filter": filter,
203 | "limit": limit,
204 | "offset": offset,
205 | "sort": sort,
206 | "q": q,
207 | "include_deleted": include_deleted,
208 | "include_relations": include_relations,
209 | }
210 | )
211 |
212 | # Define the operation name
213 | operation = "QueryIntelIndicatorEntities"
214 |
215 | logger.debug("Searching indicators with params: %s", params)
216 |
217 | # Make the API request
218 | command_response = self.client.command(operation, parameters=params)
219 |
220 | # Handle the response
221 | api_response = handle_api_response(
222 | command_response,
223 | operation=operation,
224 | error_message="Failed to search indicators",
225 | default_result=[],
226 | )
227 |
228 | if self._is_error(api_response):
229 | return [api_response]
230 |
231 | return api_response
232 |
233 | def query_report_entities(
234 | self,
235 | filter: str | None = Field(
236 | default=None,
237 | description="FQL query expression that should be used to limit the results. IMPORTANT: use the `falcon://intel/reports/fql-guide` resource when building this filter parameter.",
238 | ),
239 | limit: int = Field(
240 | default=10,
241 | ge=1,
242 | le=5000,
243 | description="Maximum number of records to return. (Max: 5000)",
244 | ),
245 | offset: int | None = Field(
246 | default=None,
247 | description="Starting index of overall result set from which to return ids.",
248 | ),
249 | sort: str | None = Field(
250 | default=None,
251 | description="The field and direction to sort results on in the format of: {field}.{asc}or {field}.{desc}. Valid values include: name, target_countries, target_industries, type, created_date, last_modified_date. Ex: created_date|desc",
252 | examples={"created_date|desc"},
253 | ),
254 | q: str | None = Field(
255 | default=None,
256 | description="Free text search across all indexed fields.",
257 | ),
258 | ) -> List[Dict[str, Any]]:
259 | """Access CrowdStrike intelligence publications and threat reports.
260 |
261 | This tool returns comprehensive intelligence report details based on your search criteria.
262 | Use this when you need to find CrowdStrike intelligence publications matching specific conditions.
263 | For guidance on building FQL filters, use the `falcon://intel/reports/fql-guide` resource.
264 |
265 | IMPORTANT: You must use the `falcon://intel/reports/fql-guide` resource when you need to use the `filter` parameter.
266 | This resource contains the guide on how to build the FQL `filter` parameter for the `falcon_search_reports` tool.
267 | """
268 | # Prepare parameters
269 | params = prepare_api_parameters(
270 | {
271 | "filter": filter,
272 | "limit": limit,
273 | "offset": offset,
274 | "sort": sort,
275 | "q": q,
276 | }
277 | )
278 |
279 | # Define the operation name
280 | operation = "QueryIntelReportEntities"
281 |
282 | logger.debug("Searching reports with params: %s", params)
283 |
284 | # Make the API request
285 | command_response = self.client.command(operation, parameters=params)
286 |
287 | # Handle the response
288 | api_response = handle_api_response(
289 | command_response,
290 | operation=operation,
291 | error_message="Failed to search reports",
292 | default_result=[],
293 | )
294 |
295 | # If handle_api_response returns an error dict instead of a list,
296 | # it means there was an error, so we return it wrapped in a list
297 | if self._is_error(api_response):
298 | return [api_response]
299 |
300 | return api_response
301 |
```
--------------------------------------------------------------------------------
/falcon_mcp/server.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Falcon MCP Server - Main entry point
3 |
4 | This module provides the main server class for the Falcon MCP server
5 | and serves as the entry point for the application.
6 | """
7 |
8 | import argparse
9 | import os
10 | import sys
11 | from typing import Dict, List, Optional, Set
12 |
13 | import uvicorn
14 | from dotenv import load_dotenv
15 | from mcp.server.fastmcp import FastMCP
16 |
17 | from falcon_mcp import registry
18 | from falcon_mcp.client import FalconClient
19 | from falcon_mcp.common.logging import configure_logging, get_logger
20 |
21 | logger = get_logger(__name__)
22 |
23 |
24 | class FalconMCPServer:
25 | """Main server class for the Falcon MCP server."""
26 |
27 | def __init__(
28 | self,
29 | base_url: Optional[str] = None,
30 | debug: bool = False,
31 | enabled_modules: Optional[Set[str]] = None,
32 | user_agent_comment: Optional[str] = None,
33 | ):
34 | """Initialize the Falcon MCP server.
35 |
36 | Args:
37 | base_url: Falcon API base URL
38 | debug: Enable debug logging
39 | enabled_modules: Set of module names to enable (defaults to all modules)
40 | user_agent_comment: Additional information to include in the User-Agent comment section
41 | """
42 | # Store configuration
43 | self.base_url = base_url
44 | self.debug = debug
45 | self.user_agent_comment = user_agent_comment
46 |
47 | self.enabled_modules = enabled_modules or set(registry.get_module_names())
48 |
49 | # Configure logging
50 | configure_logging(debug=self.debug)
51 | logger.info("Initializing Falcon MCP Server")
52 |
53 | # Initialize the Falcon client
54 | self.falcon_client = FalconClient(
55 | base_url=self.base_url,
56 | debug=self.debug,
57 | user_agent_comment=self.user_agent_comment,
58 | )
59 |
60 | # Authenticate with the Falcon API
61 | if not self.falcon_client.authenticate():
62 | logger.error("Failed to authenticate with the Falcon API")
63 | raise RuntimeError("Failed to authenticate with the Falcon API")
64 |
65 | # Initialize the MCP server
66 | self.server = FastMCP(
67 | name="Falcon MCP Server",
68 | instructions="This server provides access to CrowdStrike Falcon capabilities.",
69 | debug=self.debug,
70 | log_level="DEBUG" if self.debug else "INFO",
71 | )
72 |
73 | # Initialize and register modules
74 | self.modules = {}
75 | available_modules = registry.get_available_modules()
76 | for module_name in self.enabled_modules:
77 | if module_name in available_modules:
78 | module_class = available_modules[module_name]
79 | self.modules[module_name] = module_class(self.falcon_client)
80 | logger.debug("Initialized module: %s", module_name)
81 |
82 | # Register tools and resources from modules
83 | tool_count = self._register_tools()
84 | tool_word = "tool" if tool_count == 1 else "tools"
85 |
86 | resource_count = self._register_resources()
87 | resource_word = "resource" if resource_count == 1 else "resources"
88 |
89 | # Count modules and tools with proper grammar
90 | module_count = len(self.modules)
91 | module_word = "module" if module_count == 1 else "modules"
92 |
93 | logger.info(
94 | "Initialized %d %s with %d %s and %d %s",
95 | module_count,
96 | module_word,
97 | tool_count,
98 | tool_word,
99 | resource_count,
100 | resource_word,
101 | )
102 |
103 | def _register_tools(self) -> int:
104 | """Register tools from all modules.
105 |
106 | Returns:
107 | int: Number of tools registered
108 | """
109 | # Register core tools directly
110 | self.server.add_tool(
111 | self.falcon_check_connectivity,
112 | name="falcon_check_connectivity",
113 | )
114 |
115 | self.server.add_tool(
116 | self.list_enabled_modules,
117 | name="falcon_list_enabled_modules",
118 | )
119 |
120 | self.server.add_tool(
121 | self.list_modules,
122 | name="falcon_list_modules",
123 | )
124 |
125 | tool_count = 3 # the tools added above
126 |
127 | # Register tools from modules
128 | for module in self.modules.values():
129 | module.register_tools(self.server)
130 |
131 | tool_count += sum(len(getattr(m, "tools", [])) for m in self.modules.values())
132 |
133 | return tool_count
134 |
135 | def _register_resources(self) -> int:
136 | """Register resources from all modules.
137 |
138 | Returns:
139 | int: Number of resources registered
140 | """
141 | # Register resources from modules
142 | for module in self.modules.values():
143 | # Check if the module has a register_resources method
144 | if hasattr(module, "register_resources") and callable(module.register_resources):
145 | module.register_resources(self.server)
146 |
147 | return sum(len(getattr(m, "resources", [])) for m in self.modules.values())
148 |
149 | def falcon_check_connectivity(self) -> Dict[str, bool]:
150 | """Check connectivity to the Falcon API."""
151 | return {"connected": self.falcon_client.is_authenticated()}
152 |
153 | def list_enabled_modules(self) -> Dict[str, List[str]]:
154 | """Lists enabled modules in the falcon-mcp server.
155 |
156 | These modules are determined by the --modules flag when starting the server.
157 | If no modules are specified, all available modules are enabled.
158 | """
159 | return {"modules": list(self.modules.keys())}
160 |
161 | def list_modules(self) -> Dict[str, List[str]]:
162 | """Lists all available modules in the falcon-mcp server."""
163 | return {"modules": registry.get_module_names()}
164 |
165 | def run(self, transport: str = "stdio", host: str = "127.0.0.1", port: int = 8000):
166 | """Run the MCP server.
167 |
168 | Args:
169 | transport: Transport protocol to use ("stdio", "sse", or "streamable-http")
170 | host: Host to bind to for HTTP transports (default: 127.0.0.1)
171 | port: Port to listen on for HTTP transports (default: 8000)
172 | """
173 | if transport == "streamable-http":
174 | # For streamable-http, use uvicorn directly for custom host/port
175 | logger.info("Starting streamable-http server on %s:%d", host, port)
176 |
177 | # Get the ASGI app from FastMCP (handles /mcp path automatically)
178 | app = self.server.streamable_http_app()
179 |
180 | # Run with uvicorn for custom host/port configuration
181 | uvicorn.run(
182 | app,
183 | host=host,
184 | port=port,
185 | log_level="info" if not self.debug else "debug",
186 | )
187 | elif transport == "sse":
188 | # For sse, use uvicorn directly for custom host/port (same pattern as streamable-http)
189 | logger.info("Starting sse server on %s:%d", host, port)
190 |
191 | # Get the ASGI app from FastMCP
192 | app = self.server.sse_app()
193 |
194 | # Run with uvicorn for custom host/port configuration
195 | uvicorn.run(
196 | app,
197 | host=host,
198 | port=port,
199 | log_level="info" if not self.debug else "debug",
200 | )
201 | else:
202 | # For stdio, use the default FastMCP run method (no host/port needed)
203 | self.server.run(transport)
204 |
205 |
206 | def parse_modules_list(modules_string):
207 | """Parse and validate comma-separated module list.
208 |
209 | Args:
210 | modules_string: Comma-separated string of module names
211 |
212 | Returns:
213 | List of validated module names (returns all available modules if empty string)
214 |
215 | Raises:
216 | argparse.ArgumentTypeError: If any module names are invalid
217 | """
218 | # Get available modules
219 | available_modules = registry.get_module_names()
220 |
221 | # If empty string, return all available modules (default behavior)
222 | if not modules_string:
223 | return available_modules
224 |
225 | # Split by comma and clean up whitespace
226 | modules = [m.strip() for m in modules_string.split(",") if m.strip()]
227 |
228 | # Validate against available modules
229 | invalid_modules = [m for m in modules if m not in available_modules]
230 | if invalid_modules:
231 | raise argparse.ArgumentTypeError(
232 | f"Invalid modules: {', '.join(invalid_modules)}. "
233 | f"Available modules: {', '.join(available_modules)}"
234 | )
235 |
236 | return modules
237 |
238 |
239 | def parse_args():
240 | """Parse command line arguments."""
241 | parser = argparse.ArgumentParser(description="Falcon MCP Server")
242 |
243 | # Transport options
244 | parser.add_argument(
245 | "--transport",
246 | "-t",
247 | choices=["stdio", "sse", "streamable-http"],
248 | default=os.environ.get("FALCON_MCP_TRANSPORT", "stdio"),
249 | help="Transport protocol to use (default: stdio, env: FALCON_MCP_TRANSPORT)",
250 | )
251 |
252 | # Module selection
253 | available_modules = registry.get_module_names()
254 |
255 | parser.add_argument(
256 | "--modules",
257 | "-m",
258 | type=parse_modules_list,
259 | default=parse_modules_list(os.environ.get("FALCON_MCP_MODULES", "")),
260 | metavar="MODULE1,MODULE2,...",
261 | help=f"Comma-separated list of modules to enable. Available: [{', '.join(available_modules)}] "
262 | f"(default: all modules, env: FALCON_MCP_MODULES)",
263 | )
264 |
265 | # Debug mode
266 | parser.add_argument(
267 | "--debug",
268 | "-d",
269 | action="store_true",
270 | default=os.environ.get("FALCON_MCP_DEBUG", "").lower() == "true",
271 | help="Enable debug logging (env: FALCON_MCP_DEBUG)",
272 | )
273 |
274 | # API base URL
275 | parser.add_argument(
276 | "--base-url",
277 | default=os.environ.get("FALCON_BASE_URL"),
278 | help="Falcon API base URL (env: FALCON_BASE_URL)",
279 | )
280 |
281 | # HTTP transport configuration
282 | parser.add_argument(
283 | "--host",
284 | default=os.environ.get("FALCON_MCP_HOST", "127.0.0.1"),
285 | help="Host to bind to for HTTP transports (default: 127.0.0.1, env: FALCON_MCP_HOST)",
286 | )
287 |
288 | parser.add_argument(
289 | "--port",
290 | "-p",
291 | type=int,
292 | default=int(os.environ.get("FALCON_MCP_PORT", "8000")),
293 | help="Port to listen on for HTTP transports (default: 8000, env: FALCON_MCP_PORT)",
294 | )
295 |
296 | parser.add_argument(
297 | "--user-agent-comment",
298 | default=os.environ.get("FALCON_MCP_USER_AGENT_COMMENT"),
299 | help="Additional information to include in the User-Agent comment section (env: FALCON_MCP_USER_AGENT_COMMENT)",
300 | )
301 |
302 | return parser.parse_args()
303 |
304 |
305 | def main():
306 | """Main entry point for the Falcon MCP server."""
307 | # Load environment variables
308 | load_dotenv()
309 |
310 | # Parse command line arguments (includes environment variable defaults)
311 | args = parse_args()
312 |
313 | try:
314 | # Create and run the server
315 | server = FalconMCPServer(
316 | base_url=args.base_url,
317 | debug=args.debug,
318 | enabled_modules=set(args.modules),
319 | user_agent_comment=args.user_agent_comment,
320 | )
321 | logger.info("Starting server with %s transport", args.transport)
322 | server.run(args.transport, host=args.host, port=args.port)
323 | except RuntimeError as e:
324 | logger.error("Runtime error: %s", e)
325 | sys.exit(1)
326 | except ValueError as e:
327 | logger.error("Configuration error: %s", e)
328 | sys.exit(1)
329 | except KeyboardInterrupt:
330 | logger.info("Server stopped by user")
331 | sys.exit(0)
332 | except Exception as e:
333 | # Catch any other exceptions to ensure graceful shutdown
334 | logger.error("Unexpected error running server: %s", e)
335 | sys.exit(1)
336 |
337 |
338 | if __name__ == "__main__":
339 | main()
340 |
```
--------------------------------------------------------------------------------
/falcon_mcp/modules/incidents.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Incidents module for Falcon MCP Server
3 |
4 | This module provides tools for accessing and analyzing CrowdStrike Falcon incidents.
5 | """
6 |
7 | from typing import Any, Dict, List
8 |
9 | from mcp.server import FastMCP
10 | from mcp.server.fastmcp.resources import TextResource
11 | from pydantic import AnyUrl, Field
12 |
13 | from falcon_mcp.common.errors import handle_api_response
14 | from falcon_mcp.common.utils import prepare_api_parameters
15 | from falcon_mcp.modules.base import BaseModule
16 | from falcon_mcp.resources.incidents import (
17 | CROWD_SCORE_FQL_DOCUMENTATION,
18 | SEARCH_BEHAVIORS_FQL_DOCUMENTATION,
19 | SEARCH_INCIDENTS_FQL_DOCUMENTATION,
20 | )
21 |
22 |
23 | class IncidentsModule(BaseModule):
24 | """Module for accessing and analyzing CrowdStrike Falcon incidents."""
25 |
26 | def register_tools(self, server: FastMCP) -> None:
27 | """Register tools with the MCP server.
28 |
29 | Args:
30 | server: MCP server instance
31 | """
32 | # Register tools
33 | self._add_tool(
34 | server=server,
35 | method=self.show_crowd_score,
36 | name="show_crowd_score",
37 | )
38 |
39 | self._add_tool(
40 | server=server,
41 | method=self.search_incidents,
42 | name="search_incidents",
43 | )
44 |
45 | self._add_tool(
46 | server=server,
47 | method=self.get_incident_details,
48 | name="get_incident_details",
49 | )
50 |
51 | self._add_tool(
52 | server=server,
53 | method=self.search_behaviors,
54 | name="search_behaviors",
55 | )
56 |
57 | self._add_tool(
58 | server=server,
59 | method=self.get_behavior_details,
60 | name="get_behavior_details",
61 | )
62 |
63 | def register_resources(self, server: FastMCP) -> None:
64 | """Register resources with the MCP server.
65 |
66 | Args:
67 | server: MCP server instance
68 | """
69 | crowd_score_fql_resource = TextResource(
70 | uri=AnyUrl("falcon://incidents/crowd-score/fql-guide"),
71 | name="falcon_show_crowd_score_fql_guide",
72 | description="Contains the guide for the `filter` param of the `falcon_show_crowd_score` tool.",
73 | text=CROWD_SCORE_FQL_DOCUMENTATION,
74 | )
75 |
76 | search_incidents_fql_resource = TextResource(
77 | uri=AnyUrl("falcon://incidents/search/fql-guide"),
78 | name="falcon_search_incidents_fql_guide",
79 | description="Contains the guide for the `filter` param of the `falcon_search_incidents` tool.",
80 | text=SEARCH_INCIDENTS_FQL_DOCUMENTATION,
81 | )
82 |
83 | search_behaviors_fql_resource = TextResource(
84 | uri=AnyUrl("falcon://incidents/behaviors/fql-guide"),
85 | name="falcon_search_behaviors_fql_guide",
86 | description="Contains the guide for the `filter` param of the `falcon_search_behaviors` tool.",
87 | text=SEARCH_BEHAVIORS_FQL_DOCUMENTATION,
88 | )
89 |
90 | self._add_resource(
91 | server,
92 | crowd_score_fql_resource,
93 | )
94 | self._add_resource(
95 | server,
96 | search_incidents_fql_resource,
97 | )
98 | self._add_resource(
99 | server,
100 | search_behaviors_fql_resource,
101 | )
102 |
103 | def show_crowd_score(
104 | self,
105 | filter: str | None = Field(
106 | default=None,
107 | description="FQL Syntax formatted string used to limit the results. IMPORTANT: use the `falcon://incidents/crowd-score/fql-guide` resource when building this filter parameter.",
108 | ),
109 | limit: int = Field(
110 | default=10,
111 | ge=1,
112 | le=2500,
113 | description="Maximum number of records to return. (Max: 2500)",
114 | ),
115 | offset: int | None = Field(
116 | default=None,
117 | description="Starting index of overall result set from which to return ids.",
118 | ),
119 | sort: str | None = Field(
120 | default=None,
121 | description="The property to sort by. (Ex: modified_timestamp.desc)",
122 | examples={"modified_timestamp.desc"},
123 | ),
124 | ) -> Dict[str, Any]:
125 | """View calculated CrowdScores and security posture metrics for your environment.
126 |
127 | IMPORTANT: You must use the `falcon://incidents/crowd-score/fql-guide` resource when you need to use the `filter` parameter.
128 | This resource contains the guide on how to build the FQL `filter` parameter for the `falcon_show_crowd_score` tool.
129 | """
130 | # Prepare parameters
131 | params = prepare_api_parameters(
132 | {
133 | "filter": filter,
134 | "limit": limit,
135 | "offset": offset,
136 | "sort": sort,
137 | }
138 | )
139 |
140 | # Define the operation name (used for error handling)
141 | operation = "CrowdScore"
142 |
143 | # Make the API request
144 | response = self.client.command(operation, parameters=params)
145 |
146 | # Handle the response
147 | api_response = handle_api_response(
148 | response,
149 | operation=operation,
150 | error_message="Failed to perform operation",
151 | default_result=[],
152 | )
153 |
154 | # Check if we received an error response
155 | if self._is_error(api_response):
156 | # Return the error response as is
157 | return api_response
158 |
159 | # Initialize result with all scores
160 | result = {
161 | "average_score": 0,
162 | "average_adjusted_score": 0,
163 | "scores": api_response, # Include all the scores in the result
164 | }
165 |
166 | if api_response: # If we have scores (list of score objects)
167 | score_sum = 0
168 | adjusted_score_sum = 0
169 | count = len(api_response)
170 |
171 | for item in api_response:
172 | score_sum += item.get("score", 0)
173 | adjusted_score_sum += item.get("adjusted_score", 0)
174 |
175 | if count > 0:
176 | # Round to ensure integer output
177 | result["average_score"] = round(score_sum / count)
178 | result["average_adjusted_score"] = round(adjusted_score_sum / count)
179 |
180 | return result
181 |
182 | def search_incidents(
183 | self,
184 | filter: str | None = Field(
185 | default=None,
186 | description="FQL Syntax formatted string used to limit the results. IMPORTANT: use the `falcon://incidents/search/fql-guide` resource when building this filter parameter.",
187 | ),
188 | limit: int = Field(
189 | default=10,
190 | ge=1,
191 | le=500,
192 | description="Maximum number of records to return. (Max: 500)",
193 | ),
194 | offset: int | None = Field(
195 | default=None,
196 | description="Starting index of overall result set from which to return ids.",
197 | ),
198 | sort: str | None = Field(
199 | default=None,
200 | description="The property to sort by. FQL syntax. Ex: state.asc, name.desc",
201 | ),
202 | ) -> List[Dict[str, Any]]:
203 | """Find and analyze security incidents to understand coordinated activity in your environment.
204 |
205 | IMPORTANT: You must use the `falcon://incidents/search/fql-guide` resource when you need to use the `filter` parameter.
206 | This resource contains the guide on how to build the FQL `filter` parameter for the `falcon_search_incidents` tool.
207 | """
208 | incident_ids = self._base_query(
209 | operation="QueryIncidents",
210 | filter=filter,
211 | limit=limit,
212 | offset=offset,
213 | sort=sort,
214 | )
215 |
216 | if self._is_error(incident_ids):
217 | return [incident_ids]
218 |
219 | # If we have incident IDs, get the details for each one
220 | if incident_ids:
221 | return self.get_incident_details(incident_ids)
222 |
223 | return []
224 |
225 | def get_incident_details(
226 | self,
227 | ids: List[str] = Field(description="Incident ID(s) to retrieve."),
228 | ) -> List[Dict[str, Any]]:
229 | """Get comprehensive incident details to understand attack patterns and coordinated activities.
230 |
231 | This tool returns comprehensive incident details for one or more incident IDs.
232 | Use this when you already have specific incident IDs and need their full details.
233 | For searching/discovering incidents, use the `falcon_search_incidents` tool instead.
234 | """
235 | incidents = self._base_get_by_ids(
236 | operation="GetIncidents",
237 | ids=ids,
238 | )
239 |
240 | if self._is_error(incidents):
241 | return [incidents]
242 |
243 | return incidents
244 |
245 | def search_behaviors(
246 | self,
247 | filter: str | None = Field(
248 | default=None,
249 | description="FQL Syntax formatted string used to limit the results. IMPORTANT: use the `falcon://incidents/behaviors/fql-guide` resource when building this filter parameter.",
250 | ),
251 | limit: int = Field(
252 | default=10,
253 | ge=1,
254 | le=500,
255 | description="Maximum number of records to return. (Max: 500)",
256 | ),
257 | offset: int | None = Field(
258 | default=None,
259 | description="Starting index of overall result set from which to return ids.",
260 | ),
261 | sort: str | None = Field(
262 | default=None,
263 | description="The property to sort by. (Ex: modified_timestamp.desc)",
264 | ),
265 | ) -> List[Dict[str, Any]]:
266 | """Find and analyze behaviors to understand suspicious activity in your environment.
267 |
268 | Use this when you need to find behaviors matching certain criteria rather than retrieving specific behaviors by ID.
269 | For retrieving details of known behavior IDs, use falcon_get_behavior_details instead.
270 |
271 | IMPORTANT: You must use the `falcon://incidents/behaviors/fql-guide` resource when you need to use the `filter` parameter.
272 | This resource contains the guide on how to build the FQL `filter` parameter for the `falcon_search_behaviors` tool.
273 | """
274 | behavior_ids = self._base_query(
275 | operation="QueryBehaviors",
276 | filter=filter,
277 | limit=limit,
278 | offset=offset,
279 | sort=sort,
280 | )
281 |
282 | if self._is_error(behavior_ids):
283 | return [behavior_ids]
284 |
285 | # If we have behavior IDs, get the details for each one
286 | if behavior_ids:
287 | return self.get_behavior_details(behavior_ids)
288 |
289 | return []
290 |
291 | def get_behavior_details(
292 | self,
293 | ids: List[str] = Field(description="Behavior ID(s) to retrieve."),
294 | ) -> List[Dict[str, Any]]:
295 | """Get detailed behavior information to understand attack techniques and tactics.
296 |
297 | Use this when you already know the specific behavior ID(s) and need to retrieve their details.
298 | For searching behaviors based on criteria, use the `falcon_search_behaviors` tool instead.
299 | """
300 | behaviors = self._base_get_by_ids(
301 | operation="GetBehaviors",
302 | ids=ids,
303 | )
304 |
305 | if self._is_error(behaviors):
306 | return [behaviors]
307 |
308 | return behaviors
309 |
310 | def _base_query(
311 | self,
312 | operation: str,
313 | filter: str | None = None,
314 | limit: int = 100,
315 | offset: int | None = None,
316 | sort: str | None = None,
317 | ) -> List[str] | Dict[str, Any]:
318 | # Prepare parameters
319 | params = prepare_api_parameters(
320 | {
321 | "filter": filter,
322 | "limit": limit,
323 | "offset": offset,
324 | "sort": sort,
325 | }
326 | )
327 |
328 | # Make the API request
329 | response = self.client.command(operation, parameters=params)
330 |
331 | # Handle the response
332 | return handle_api_response(
333 | response,
334 | operation=operation,
335 | error_message="Failed to perform operation",
336 | default_result=[],
337 | )
338 |
```
--------------------------------------------------------------------------------
/examples/adk/adk_agent_operations.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/bash
2 |
3 | # This script runs or deploys an AI agent based on the provided operation mode.
4 | # It loads environment variables from ./falcon_agent/.env and validates required variables
5 | # for the chosen mode.
6 |
7 | # IMPORTANT: If you want the exported environment variables to persist in your
8 | # calling shell (e.g., your terminal session) after this script finishes,
9 | # you MUST 'source' this script, like:
10 | # source ./adk_agent_operations.sh local_run
11 |
12 | # --- Configuration ---
13 | ENV_DIR="./falcon_agent"
14 | ENV_FILE="${ENV_DIR}/.env"
15 | ENV_PROPERTIES_TEMPLATE="${ENV_DIR}/env.properties"
16 | INVALID_VALUE="NOT_SET" # The literal string considered an invalid value
17 | ENV_BACKUP_FILE="${ENV_FILE}.bak"
18 |
19 | # Define required variables for each operation mode
20 | # These are comma-separated lists of variable names
21 | vars_for_local_run="GOOGLE_GENAI_USE_VERTEXAI,GOOGLE_API_KEY,GOOGLE_MODEL,FALCON_CLIENT_ID,FALCON_CLIENT_SECRET,FALCON_BASE_URL,FALCON_AGENT_PROMPT"
22 | vars_for_cloudrun_deploy="GOOGLE_GENAI_USE_VERTEXAI,GOOGLE_MODEL,FALCON_CLIENT_ID,FALCON_CLIENT_SECRET,FALCON_BASE_URL,FALCON_AGENT_PROMPT,PROJECT_ID,REGION"
23 | vars_for_agent_engine_deploy="GOOGLE_GENAI_USE_VERTEXAI,GOOGLE_MODEL,FALCON_CLIENT_ID,FALCON_CLIENT_SECRET,FALCON_BASE_URL,FALCON_AGENT_PROMPT,PROJECT_ID,REGION,AGENT_ENGINE_STAGING_BUCKET"
24 | vars_for_agentspace_register="GOOGLE_GENAI_USE_VERTEXAI,GOOGLE_MODEL,FALCON_CLIENT_ID,FALCON_CLIENT_SECRET,FALCON_BASE_URL,FALCON_AGENT_PROMPT,PROJECT_ID,REGION,PROJECT_NUMBER,AGENT_LOCATION,REASONING_ENGINE_NUMBER,AGENT_SPACE_APP_NAME" # Added AGENT_SPACE_APP_NAME
25 |
26 | # --- Functions ---
27 |
28 | # Function to display script usage
29 | usage() {
30 | echo "Usage: $0 <operation_mode>"
31 | echo ""
32 | echo "Operation Modes:"
33 | echo " local_run"
34 | echo " cloudrun_deploy"
35 | echo " agent_engine_deploy"
36 | echo " agentspace_register"
37 | echo ""
38 | echo "Example: source $0 local_run"
39 | exit 1 # Exit with error code
40 | }
41 |
42 | # Function to validate required environment variables
43 | # Arguments: $1 = comma-separated string of required variable names
44 | validate_required_vars() {
45 | local required_vars_string="$1"
46 | IFS=',' read -r -a required_vars_array <<< "$required_vars_string" # Split string into array
47 |
48 | local all_vars_valid=true
49 |
50 | echo "--- Validating required environment variables for '$OPERATION_MODE' mode ---"
51 | for var_name in "${required_vars_array[@]}"; do
52 | # Check if variable is set and not empty
53 | if [[ -z "${!var_name}" ]]; then
54 | echo "ERROR: Required environment variable '$var_name' is missing or empty."
55 | all_vars_valid=false
56 | # Check if variable's value is the literal INVALID_VALUE string
57 | elif [[ "${!var_name}" == "$INVALID_VALUE" ]]; then
58 | echo "ERROR: Required environment variable '$var_name' has an invalid value: '$INVALID_VALUE'."
59 | all_vars_valid=false
60 | else
61 | echo "INFO: Variable '$var_name' is set and valid."
62 | fi
63 | done
64 |
65 | if ! $all_vars_valid; then
66 | echo "--- Validation FAILED. Please check your '$ENV_FILE' file. ---"
67 | return 1 # Indicate validation failure within the function
68 | fi
69 |
70 | echo "--- All required environment variables are VALID. ---"
71 | return 0 # Indicate validation success within the function
72 | }
73 |
74 | # Function to backup, modify, and restore .env file
75 | # This function is intended to be called with 'trap' for cleanup.
76 | cleanup_env_on_exit() {
77 | if [[ -f "$ENV_BACKUP_FILE" ]]; then
78 | echo "INFO: Restoring .env file from backup: '$ENV_BACKUP_FILE'."
79 | mv "$ENV_BACKUP_FILE" "$ENV_FILE" || echo "WARNING: Failed to restore .env file from backup. Manual intervention may be required."
80 | fi
81 | }
82 |
83 | # --- Main Script Logic ---
84 |
85 | # Handle case: no arguments provided
86 | if [ "$#" -eq 0 ]; then
87 | if [ ! -f "$ENV_FILE" ]; then
88 | echo "INFO: No operation mode provided and '$ENV_FILE' is not found."
89 | echo "INFO: Attempting to copy template '$ENV_PROPERTIES_TEMPLATE' to '$ENV_FILE'."
90 |
91 | # Ensure the directory exists
92 | mkdir -p "$ENV_DIR" || { echo "ERROR: Failed to create directory '$ENV_DIR'."; exit 1; }
93 |
94 | if [ -f "$ENV_PROPERTIES_TEMPLATE" ]; then
95 | cp "$ENV_PROPERTIES_TEMPLATE" "$ENV_FILE" || { echo "ERROR: Failed to copy '$ENV_PROPERTIES_TEMPLATE' to '$ENV_FILE'."; exit 1; }
96 | echo "SUCCESS: '$ENV_PROPERTIES_TEMPLATE' copied to '$ENV_FILE'."
97 | echo "ACTION REQUIRED: Please update the variables in '$ENV_FILE' before running this script with an operation mode."
98 | exit 0 # Exit after setup, user needs to edit the file
99 | else
100 | echo "ERROR: Template file '$ENV_PROPERTIES_TEMPLATE' not found. Cannot create '$ENV_FILE'."
101 | exit 1
102 | fi
103 | else
104 | # .env file exists but no arguments provided, so show usage
105 | echo "ERROR: No operation mode argument provided."
106 | usage # usage function calls exit 1
107 | fi
108 | fi
109 |
110 |
111 | OPERATION_MODE="$1"
112 |
113 | # Validate the provided operation mode
114 | case "$OPERATION_MODE" in
115 | local_run|cloudrun_deploy|agent_engine_deploy|agentspace_register)
116 | echo "INFO: Operation mode selected: '$OPERATION_MODE'."
117 | ;;
118 | *)
119 | echo "ERROR: Invalid operation mode: '$OPERATION_MODE'."
120 | usage # usage function calls exit 1
121 | ;;
122 | esac
123 |
124 | # Check if the .env file exists (after potential creation in no-arg case)
125 | if [ ! -f "$ENV_FILE" ]; then
126 | echo "ERROR: Environment file '$ENV_FILE' not found after initial checks. This should not happen."
127 | exit 1 # Terminate the script
128 | fi
129 |
130 | echo "--- Loading environment variables from '$ENV_FILE' ---"
131 | # Load all valid variables from the .env file into the current shell environment.
132 | eval "$(grep -v '^[[:space:]]*#' "$ENV_FILE" | grep -E '^[[:alnum:]_]+=.*$' | sed -E 's/^([[:alnum:]_]+)=(.*)$/export \1="\2"/' )"
133 | echo "--- Environment variables loaded. ---"
134 |
135 | # Perform validation and execute mode-specific logic
136 | case "$OPERATION_MODE" in
137 | local_run)
138 | validate_required_vars "$vars_for_local_run" || exit 1 # Exit if validation fails
139 | echo "INFO: Running ADK Agent for local development..."
140 | # Execute the local run command
141 | adk web
142 | local_run_status=$? # Capture exit status of the command
143 | if [ $local_run_status -eq 0 ]; then
144 | echo "SUCCESS: 'adk web' command completed successfully."
145 | else
146 | echo "ERROR: 'adk web' command failed with exit status $local_run_status."
147 | exit $local_run_status
148 | fi
149 | ;;
150 |
151 | cloudrun_deploy)
152 | validate_required_vars "$vars_for_cloudrun_deploy" || exit 1 # Exit if validation fails
153 | echo "INFO: Preparing for Cloud Run deployment..."
154 |
155 | # Backup .env file
156 | echo "INFO: Backing up '$ENV_FILE' to '$ENV_BACKUP_FILE'."
157 | cp "$ENV_FILE" "$ENV_BACKUP_FILE" || { echo "ERROR: Failed to backup .env file."; exit 1; }
158 | # Set trap to restore .env file on script exit (success or failure)
159 | trap cleanup_env_on_exit EXIT ERR
160 |
161 | # Modify .env file variables
162 | echo "INFO: Modifying '$ENV_FILE': Deleting GOOGLE_API_KEY and setting GOOGLE_GENAI_USE_VERTEXAI=True."
163 | # Use sed -i for in-place editing.
164 | # Delete line containing GOOGLE_API_KEY
165 | sed -i '/^GOOGLE_API_KEY=/d' "$ENV_FILE" || { echo "WARNING: Failed to delete GOOGLE_API_KEY from .env."; }
166 | # Replace GOOGLE_GENAI_USE_VERTEXAI value
167 | sed -i 's/^GOOGLE_GENAI_USE_VERTEXAI=.*/GOOGLE_GENAI_USE_VERTEXAI=True/' "$ENV_FILE" || { echo "WARNING: Failed to set GOOGLE_GENAI_USE_VERTEXAI=True in .env."; }
168 |
169 | # Re-load modified variables into current shell environment
170 | echo "INFO: Re-loading modified environment variables."
171 | eval "$(grep -v '^[[:space:]]*#' "$ENV_FILE" | grep -E '^[[:alnum:]_]+=.*$' | sed -E 's/^([[:alnum:]_]+)=(.*)$/export \1="\2"/' )"
172 |
173 | echo "INFO: Deploying ADK Agent to Cloud Run..."
174 | # Execute the Cloud Run deployment command
175 | adk deploy cloud_run --project="$PROJECT_ID" --region="$REGION" --service_name="falcon-agent-service" --with_ui ./falcon_agent
176 | deploy_status=$? # Capture exit status of the command
177 |
178 | # Trap will handle restoration on exit
179 | if [ $deploy_status -eq 0 ]; then
180 | echo "SUCCESS: Cloud Run deployment completed successfully."
181 | else
182 | echo "ERROR: Cloud Run deployment failed with exit status $deploy_status."
183 | exit $deploy_status
184 | fi
185 | ;;
186 |
187 | agent_engine_deploy)
188 | validate_required_vars "$vars_for_agent_engine_deploy" || exit 1 # Exit if validation fails
189 | echo "INFO: Preparing for Agent Engine deployment..."
190 |
191 | # Backup .env file
192 | echo "INFO: Backing up '$ENV_FILE' to '$ENV_BACKUP_FILE'."
193 | cp "$ENV_FILE" "$ENV_BACKUP_FILE" || { echo "ERROR: Failed to backup .env file."; exit 1; }
194 | # Set trap to restore .env file on script exit (success or failure)
195 | trap cleanup_env_on_exit EXIT ERR
196 |
197 | # Modify .env file variables (same as cloudrun_deploy for now)
198 | echo "INFO: Modifying '$ENV_FILE': Deleting GOOGLE_API_KEY and setting GOOGLE_GENAI_USE_VERTEXAI=True."
199 | sed -i '/^GOOGLE_API_KEY=/d' "$ENV_FILE" || { echo "WARNING: Failed to delete GOOGLE_API_KEY from .env."; }
200 | sed -i 's/^GOOGLE_GENAI_USE_VERTEXAI=.*/GOOGLE_GENAI_USE_VERTEXAI=True/' "$ENV_FILE" || { echo "WARNING: Failed to set GOOGLE_GENAI_USE_VERTEXAI=True in .env."; }
201 |
202 | # Re-load modified variables into current shell environment
203 | echo "INFO: Re-loading modified environment variables."
204 | eval "$(grep -v '^[[:space:]]*#' "$ENV_FILE" | grep -E '^[[:alnum:]_]+=.*$' | sed -E 's/^([[:alnum:]_]+)=(.*)$/export \1="\2"/' )"
205 |
206 | echo "INFO: Deploying ADK Agent to Agent Engine..."
207 | # Execute the Agent Engine deployment command
208 | adk deploy agent_engine --project="$PROJECT_ID" --region="$REGION" --staging_bucket="$AGENT_ENGINE_STAGING_BUCKET" --display_name=falcon_agent ./falcon_agent
209 | deploy_status=$? # Capture exit status of the command
210 |
211 | # Trap will handle restoration on exit
212 | if [ $deploy_status -eq 0 ]; then
213 | echo "SUCCESS: Agent Engine deployment completed successfully."
214 | else
215 | echo "ERROR: Agent Engine deployment failed with exit status $deploy_status."
216 | exit $deploy_status
217 | fi
218 | ;;
219 |
220 | agentspace_register)
221 | validate_required_vars "$vars_for_agentspace_register" || exit 1 # Exit if validation fails
222 | echo "INFO: Registering ADK Agent with AgentSpace..."
223 |
224 | TARGET_URL="https://discoveryengine.googleapis.com/v1alpha/projects/$PROJECT_ID/locations/$AGENT_LOCATION/collections/default_collection/engines/$AGENT_SPACE_APP_NAME/assistants/default_assistant/agents"
225 |
226 | # Construct JSON data using a here-document
227 | JSON_DATA=$(cat <<EOF
228 | {
229 | "displayName": "CrowdStrike Falcon Agent",
230 | "description": "Allows users interact with CrowdStrike Falcon backend",
231 | "adk_agent_definition":
232 | {
233 | "tool_settings": {
234 | "tool_description": "CrowdStrike Falcon tools"
235 | },
236 | "provisioned_reasoning_engine": {
237 | "reasoning_engine":"projects/$PROJECT_NUMBER/locations/$REGION/reasoningEngines/$REASONING_ENGINE_NUMBER"
238 | }
239 | }
240 | }
241 | EOF
242 | )
243 |
244 | echo "INFO: Sending POST request to: $TARGET_URL"
245 | echo "DEBUG: Request Body :"
246 | echo "$JSON_DATA"
247 | echo "..."
248 |
249 | # Perform the POST request using curl
250 | # Note: X-Goog-User-Project header should use the variable value
251 | curl -X POST \
252 | -H "Content-Type: application/json" \
253 | -H "Authorization: Bearer $(gcloud auth print-access-token)" \
254 | -H "X-Goog-User-Project: $PROJECT_ID" \
255 | -d "$JSON_DATA" \
256 | "$TARGET_URL"
257 | curl_status=$? # Capture exit status of curl
258 |
259 | echo "" # Add a newline after curl output for better readability
260 | if [ $curl_status -eq 0 ]; then
261 | echo "SUCCESS: cURL command completed successfully for AgentSpace registration."
262 | else
263 | echo "ERROR: cURL command failed with exit status $curl_status during AgentSpace registration."
264 | exit $curl_status
265 | fi
266 | ;;
267 | esac
268 |
269 | echo "--- Operation '$OPERATION_MODE' complete. ---"
270 |
```
--------------------------------------------------------------------------------
/tests/common/test_utils.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Tests for the utility functions.
3 | """
4 |
5 | import unittest
6 | from unittest.mock import patch
7 |
8 | from falcon_mcp.common.utils import (
9 | extract_first_resource,
10 | extract_resources,
11 | filter_none_values,
12 | generate_md_table,
13 | prepare_api_parameters,
14 | )
15 |
16 |
17 | class TestUtilFunctions(unittest.TestCase):
18 | """Test cases for the utility functions."""
19 |
20 | def test_filter_none_values(self):
21 | """Test filter_none_values function."""
22 | # Dictionary with None values
23 | data = {
24 | "key1": "value1",
25 | "key2": None,
26 | "key3": 0,
27 | "key4": False,
28 | "key5": "",
29 | "key6": None,
30 | }
31 |
32 | filtered = filter_none_values(data)
33 |
34 | # Verify None values were removed
35 | self.assertEqual(
36 | filtered,
37 | {
38 | "key1": "value1",
39 | "key3": 0,
40 | "key4": False,
41 | "key5": "",
42 | },
43 | )
44 |
45 | # Empty dictionary
46 | self.assertEqual(filter_none_values({}), {})
47 |
48 | # Dictionary without None values
49 | data = {"key1": "value1", "key2": 2}
50 | self.assertEqual(filter_none_values(data), data)
51 |
52 | def test_prepare_api_parameters(self):
53 | """Test prepare_api_parameters function."""
54 | # Parameters with None values
55 | params = {
56 | "filter": "name:test",
57 | "limit": 100,
58 | "offset": None,
59 | "sort": None,
60 | }
61 |
62 | prepared = prepare_api_parameters(params)
63 |
64 | # Verify None values were removed
65 | self.assertEqual(prepared, {"filter": "name:test", "limit": 100})
66 |
67 | # Empty parameters
68 | self.assertEqual(prepare_api_parameters({}), {})
69 |
70 | # Parameters without None values
71 | params = {"filter": "name:test", "limit": 100}
72 | self.assertEqual(prepare_api_parameters(params), params)
73 |
74 | def test_extract_resources(self):
75 | """Test extract_resources function."""
76 | # Success response with resources
77 | response = {
78 | "status_code": 200,
79 | "body": {
80 | "resources": [
81 | {"id": "resource1", "name": "Resource 1"},
82 | {"id": "resource2", "name": "Resource 2"},
83 | ]
84 | },
85 | }
86 |
87 | resources = extract_resources(response)
88 |
89 | # Verify resources were extracted
90 | self.assertEqual(
91 | resources,
92 | [
93 | {"id": "resource1", "name": "Resource 1"},
94 | {"id": "resource2", "name": "Resource 2"},
95 | ],
96 | )
97 |
98 | # Success response with empty resources
99 | response = {"status_code": 200, "body": {"resources": []}}
100 |
101 | resources = extract_resources(response)
102 |
103 | # Verify empty list was returned
104 | self.assertEqual(resources, [])
105 |
106 | # Success response with empty resources and default
107 | default = [{"id": "default", "name": "Default Resource"}]
108 | resources = extract_resources(response, default=default)
109 |
110 | # Verify default was returned
111 | self.assertEqual(resources, default)
112 |
113 | # Error response
114 | response = {
115 | "status_code": 400,
116 | "body": {"errors": [{"message": "Bad request"}]},
117 | }
118 |
119 | resources = extract_resources(response)
120 |
121 | # Verify empty list was returned
122 | self.assertEqual(resources, [])
123 |
124 | # Error response with default
125 | resources = extract_resources(response, default=default)
126 |
127 | # Verify default was returned
128 | self.assertEqual(resources, default)
129 |
130 | @patch("falcon_mcp.common.utils._format_error_response")
131 | def test_extract_first_resource(self, mock_format_error):
132 | """Test extract_first_resource function."""
133 | # Mock format_error_response
134 | mock_format_error.return_value = {"error": "Resource not found"}
135 |
136 | # Success response with resources
137 | response = {
138 | "status_code": 200,
139 | "body": {
140 | "resources": [
141 | {"id": "resource1", "name": "Resource 1"},
142 | {"id": "resource2", "name": "Resource 2"},
143 | ]
144 | },
145 | }
146 |
147 | resource = extract_first_resource(response, "TestOperation")
148 |
149 | # Verify first resource was returned
150 | self.assertEqual(resource, {"id": "resource1", "name": "Resource 1"})
151 |
152 | # Success response with empty resources
153 | response = {"status_code": 200, "body": {"resources": []}}
154 |
155 | resource = extract_first_resource(
156 | response, "TestOperation", not_found_error="Custom error"
157 | )
158 |
159 | # Verify error response was returned
160 | mock_format_error.assert_called_with("Custom error", operation="TestOperation")
161 | self.assertEqual(resource, {"error": "Resource not found"})
162 |
163 | # Error response
164 | response = {
165 | "status_code": 400,
166 | "body": {"errors": [{"message": "Bad request"}]},
167 | }
168 |
169 | resource = extract_first_resource(response, "TestOperation")
170 |
171 | # Verify error response was returned
172 | mock_format_error.assert_called_with(
173 | "Resource not found", operation="TestOperation"
174 | )
175 | self.assertEqual(resource, {"error": "Resource not found"})
176 |
177 | def test_generate_md_table(self):
178 | """Test generate_md_table function."""
179 | # Test data with headers as the first row
180 | data = [
181 | # Header row
182 | (" Name", " Type", "Operators ", "Description ", "Extra"),
183 | # Data rows
184 | (
185 | "test_string",
186 | "String",
187 | "Yes",
188 | """
189 | This is a test description.
190 |
191 | It has multiple lines.
192 | For testing purposes.
193 | """
194 | ),
195 | (
196 | "test_bool",
197 | "\nBoolean",
198 | "\nYes",
199 | "This is a test description.\nIt has multiple lines.\nFor testing purposes.",
200 | True,
201 | ),
202 | (
203 | "test_none",
204 | " None",
205 | " No",
206 | """
207 | Multi line description.
208 | Hello
209 | """,
210 | None,
211 | ),
212 | (
213 | "test_number",
214 | "Number ",
215 | "No ",
216 | "Single line description.",
217 | 42,
218 | )
219 | ]
220 |
221 | # Generate table
222 | table = generate_md_table(data)
223 |
224 | # Expected table format (with exact spacing and formatting)
225 | expected_table = """|Name|Type|Operators|Description|Extra|
226 | |-|-|-|-|-|
227 | |test_string|String|Yes|This is a test description. It has multiple lines. For testing purposes.||
228 | |test_bool|Boolean|Yes|This is a test description. It has multiple lines. For testing purposes.|true|
229 | |test_none|None|No|Multi line description. Hello||
230 | |test_number|Number|No|Single line description.|42|"""
231 |
232 | # Compare the generated table with the expected table
233 | self.assertEqual(table, expected_table)
234 |
235 | # Split into lines for easier assertion
236 | lines = table.split('\n')
237 |
238 | # Check basic structure
239 | self.assertEqual(len(lines), 6) # header + separator + 4 data rows
240 |
241 | # Check header row exists and contains all headers (stripped of spaces)
242 | header_row = lines[0]
243 | for header in data[0]:
244 | self.assertIn(header.strip(), header_row)
245 |
246 | # Check for multi-line handling - descriptions should be combined with spaces
247 | self.assertIn("This is a test description. It has multiple lines. For testing purposes.", lines[2])
248 |
249 | # Check for proper pipe character usage
250 | for i in range(6): # Check all lines
251 | self.assertTrue(lines[i].startswith('|'))
252 | self.assertTrue(lines[i].endswith('|'))
253 | # Should have exactly 6 | characters (start, end, and 4 column separators)
254 | self.assertEqual(lines[i].count('|'), 6)
255 |
256 | def test_generate_table_with_non_string_headers(self):
257 | """Test generate_table function with non-string headers."""
258 | # Test data with non-string headers
259 | data = [
260 | # Header row with a non-string value
261 | ("Name", 123, "Operators", "Description", "Extra"),
262 | # Data rows
263 | (
264 | "test_string",
265 | "String",
266 | "Yes",
267 | "This is a test description.",
268 | None,
269 | ),
270 | ]
271 |
272 | # Verify that TypeError is raised
273 | with self.assertRaises(TypeError) as context:
274 | generate_md_table(data)
275 |
276 | # Check the error message
277 | self.assertIn("Header values must be strings", str(context.exception))
278 | self.assertIn("got int", str(context.exception))
279 |
280 | def test_generate_table_with_single_column(self):
281 | """Test generate_table function with a single column."""
282 | # Test data with a single column
283 | data = [
284 | # Header row with a single value
285 | ("Name",),
286 | # Data rows with a single value
287 | ("test_string",),
288 | ("test_bool",),
289 | ("test_none",),
290 | ]
291 |
292 | # Generate table
293 | table = generate_md_table(data)
294 |
295 | # Expected table format (with exact spacing and formatting)
296 | expected_table = """|Name|
297 | |-|
298 | |test_string|
299 | |test_bool|
300 | |test_none|"""
301 |
302 | # Compare the generated table with the expected table
303 | self.assertEqual(table, expected_table)
304 |
305 | # Split into lines for easier assertion
306 | lines = table.split('\n')
307 |
308 | # Check basic structure
309 | self.assertEqual(len(lines), 5) # header + separator + 3 data rows
310 |
311 | # Check header row exists and contains the header
312 | header_row = lines[0]
313 | self.assertEqual(header_row, "|Name|")
314 |
315 | # Check separator row
316 | self.assertEqual(lines[1], "|-|")
317 |
318 | # Check data rows exist with correct content
319 | self.assertEqual(lines[2], "|test_string|")
320 | self.assertEqual(lines[3], "|test_bool|")
321 | self.assertEqual(lines[4], "|test_none|")
322 |
323 | # Check for proper pipe character usage
324 | for i in range(5): # Check all lines
325 | self.assertTrue(lines[i].startswith('|'))
326 | self.assertTrue(lines[i].endswith('|'))
327 | # Should have exactly 2 | characters (start and end)
328 | self.assertEqual(lines[i].count('|'), 2)
329 |
330 | def test_generate_table_with_empty_header_row(self):
331 | """Test generate_table function with an empty header row."""
332 | # Test data with an empty header row
333 | data = [
334 | # Empty header row
335 | (),
336 | # Data rows
337 | ("test_string",),
338 | ]
339 |
340 | # Verify that ValueError is raised
341 | with self.assertRaises(ValueError) as context:
342 | generate_md_table(data)
343 |
344 | # Check the error message
345 | self.assertIn("Header row cannot be empty", str(context.exception))
346 |
347 | def test_generate_table_with_insufficient_data(self):
348 | """Test generate_table function with insufficient data."""
349 | # Test data with only a header row and no data rows
350 | data = [
351 | # Header row
352 | ("Name", "Type"),
353 | ]
354 |
355 | # Verify that TypeError is raised
356 | with self.assertRaises(TypeError) as context:
357 | generate_md_table(data)
358 |
359 | # Check the error message
360 | self.assertIn("Need at least 2 items", str(context.exception))
361 |
362 | # Test with empty data
363 | with self.assertRaises(TypeError) as context:
364 | generate_md_table([])
365 |
366 | # Check the error message
367 | self.assertIn("Need at least 2 items", str(context.exception))
368 |
369 |
370 | if __name__ == "__main__":
371 | unittest.main()
372 |
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
1 | # Changelog
2 |
3 | ## [0.3.0](https://github.com/CrowdStrike/falcon-mcp/compare/v0.2.0...v0.3.0) (2025-09-08)
4 |
5 |
6 | ### Features
7 |
8 | * **module/discover:** Add unmanaged assets search tool to Discover module ([#132](https://github.com/CrowdStrike/falcon-mcp/issues/132)) ([1c7a798](https://github.com/CrowdStrike/falcon-mcp/commit/1c7a7985637fe81c789ac7b0912f748d135238a3))
9 | * **modules/discover:** add new discover module ([#131](https://github.com/CrowdStrike/falcon-mcp/issues/131)) ([2862361](https://github.com/CrowdStrike/falcon-mcp/commit/2862361b8d0402ab7db4458794eb2b9bf62ef829))
10 | * **modules/idp:** Add geolocation info to entities and timeline in i… ([#124](https://github.com/CrowdStrike/falcon-mcp/issues/124)) ([31bb268](https://github.com/CrowdStrike/falcon-mcp/commit/31bb268070a55cd9a0dc52cc3eab566a65dd5ac3))
11 | * **modules/idp:** Add geolocation info to entities and timeline in idp module ([#121](https://github.com/CrowdStrike/falcon-mcp/issues/121)) ([31bb268](https://github.com/CrowdStrike/falcon-mcp/commit/31bb268070a55cd9a0dc52cc3eab566a65dd5ac3))
12 | * **modules/serverless:** add serverless module ([#127](https://github.com/CrowdStrike/falcon-mcp/issues/127)) ([0d7b7b3](https://github.com/CrowdStrike/falcon-mcp/commit/0d7b7b3e33b05541a9507278861d37621d32dfaa))
13 |
14 |
15 | ### Bug Fixes
16 |
17 | * fix incorrect module registration assumptions ([#153](https://github.com/CrowdStrike/falcon-mcp/issues/153)) ([bd3aa95](https://github.com/CrowdStrike/falcon-mcp/commit/bd3aa95706a2a35004d6c3c95dbbddd9e8fcffcf))
18 | * **modules/identity:** add missing scope for Identity Protection module ([#148](https://github.com/CrowdStrike/falcon-mcp/issues/148)) ([791a262](https://github.com/CrowdStrike/falcon-mcp/commit/791a2621ed97d20553c0b0d98c6e0690a165208a))
19 |
20 | ## [0.2.0](https://github.com/CrowdStrike/falcon-mcp/compare/v0.1.0...v0.2.0) (2025-08-07)
21 |
22 |
23 | ### Features
24 |
25 | * add origins to intel fql guide ([#89](https://github.com/CrowdStrike/falcon-mcp/issues/89)) ([c9a147e](https://github.com/CrowdStrike/falcon-mcp/commit/c9a147eef3f1c991eebc5c2e63781f8ab0eda311))
26 | * disable telemetry ([#102](https://github.com/CrowdStrike/falcon-mcp/issues/102)) ([feb4507](https://github.com/CrowdStrike/falcon-mcp/commit/feb450797b981f9b9dd768e54cb7419f42cdfc90))
27 | * **modules/sensorusage:** add new sensor usage module ([#101](https://github.com/CrowdStrike/falcon-mcp/issues/101)) ([ad97eb8](https://github.com/CrowdStrike/falcon-mcp/commit/ad97eb853f45b3d37af1a9b447531eb859201a0d))
28 | * **resources/spotlight:** FQL filter as tuples ([#91](https://github.com/CrowdStrike/falcon-mcp/issues/91)) ([d9664a6](https://github.com/CrowdStrike/falcon-mcp/commit/d9664a6e37bafa102e1fea1ff109843c4ba9437d))
29 | * **server:** add distinct tools for active vs available modules ([#103](https://github.com/CrowdStrike/falcon-mcp/issues/103)) ([f5f941a](https://github.com/CrowdStrike/falcon-mcp/commit/f5f941a28e9f2e6765d9de0fd060580274d7baab))
30 |
31 |
32 | ### Bug Fixes
33 |
34 | * **resources/detections:** added severity_name over severity level and cleaned up example filters ([#93](https://github.com/CrowdStrike/falcon-mcp/issues/93)) ([5f4b775](https://github.com/CrowdStrike/falcon-mcp/commit/5f4b7750ad87475a3ec59f2b493db82193b7358d))
35 |
36 |
37 | ### Refactoring
38 |
39 | * remove all return statements from tool docstrings ([#117](https://github.com/CrowdStrike/falcon-mcp/issues/117)) ([80250bb](https://github.com/CrowdStrike/falcon-mcp/commit/80250bb23da4029f0c8bb812cc6334aa7b36673d))
40 | * remove mention to Host from FQL guide ([cf82392](https://github.com/CrowdStrike/falcon-mcp/commit/cf82392cc9f299334ae5cf7a07bd42a81b01f607))
41 | * **resources/cloud:** remove mention to Host from FQL guide ([#76](https://github.com/CrowdStrike/falcon-mcp/issues/76)) ([81ec4de](https://github.com/CrowdStrike/falcon-mcp/commit/81ec4de3c121d407290dde6965942da26478f652))
42 | * **resources/cloud:** use new tuple methodology to create filters ([#95](https://github.com/CrowdStrike/falcon-mcp/issues/95)) ([fd5cce7](https://github.com/CrowdStrike/falcon-mcp/commit/fd5cce7ed458b99f6aa89c4f9cfed0823e51290f))
43 | * **resources/detections:** update guide to be more accurate ([#83](https://github.com/CrowdStrike/falcon-mcp/issues/83)) ([4ff2144](https://github.com/CrowdStrike/falcon-mcp/commit/4ff2144bbf2af3c2db3d2d8e5351c075cee7f610))
44 | * **resources/detections:** use new tuple method for fql detections table ([#97](https://github.com/CrowdStrike/falcon-mcp/issues/97)) ([f328b79](https://github.com/CrowdStrike/falcon-mcp/commit/f328b79cbdcac9e5a1e29cbf11fc517c19e24606))
45 | * **resources/hosts:** tested and updated fql filters and operator support for hosts module ([#63](https://github.com/CrowdStrike/falcon-mcp/issues/63)) ([e0b971c](https://github.com/CrowdStrike/falcon-mcp/commit/e0b971c6b4e4dcda693ea7f8407a21a3e847a1dc))
46 | * **resources/hosts:** use new tuple methodology to create filters ([#96](https://github.com/CrowdStrike/falcon-mcp/issues/96)) ([da38d69](https://github.com/CrowdStrike/falcon-mcp/commit/da38d6904d25ccf8fcdfc8aef62a762acc89507d))
47 | * **resources/incidents:** use new tuple methodology to create filters ([#98](https://github.com/CrowdStrike/falcon-mcp/issues/98)) ([a9ba2f7](https://github.com/CrowdStrike/falcon-mcp/commit/a9ba2f7ba94fe1b7b6108d5e89e4c767afad5657))
48 | * **resources/intel:** use new tuple methodology to create filters ([#99](https://github.com/CrowdStrike/falcon-mcp/issues/99)) ([cf0c19e](https://github.com/CrowdStrike/falcon-mcp/commit/cf0c19ea77b21b8e1590c5642a6aa3de6dbd1a14))
49 | * standardize parameter consistency across all modules ([#106](https://github.com/CrowdStrike/falcon-mcp/issues/106)) ([3c9c299](https://github.com/CrowdStrike/falcon-mcp/commit/3c9c29946942941b50d1fbcf9d640329ea8bc84a))
50 |
51 | ## 0.1.0 (2025-07-16)
52 |
53 |
54 | ### Features
55 |
56 | * add Docker support ([#19](https://github.com/crowdstrike/falcon-mcp/issues/19)) ([f60adc1](https://github.com/crowdstrike/falcon-mcp/commit/f60adc1c1e7e0a441a57d671fa44bb430b66280d))
57 | * add E2E testing ([#16](https://github.com/crowdstrike/falcon-mcp/issues/16)) ([c8a1d18](https://github.com/crowdstrike/falcon-mcp/commit/c8a1d18400fc5d89ef26c7cbe01fe4d46628fdff))
58 | * add filter guide for all tools which have filter param ([#46](https://github.com/crowdstrike/falcon-mcp/issues/46)) ([61ffde9](https://github.com/crowdstrike/falcon-mcp/commit/61ffde90062644bb6014bb89c8b50ec904c728d5))
59 | * add hosts module ([#42](https://github.com/crowdstrike/falcon-mcp/issues/42)) ([9375f4b](https://github.com/crowdstrike/falcon-mcp/commit/9375f4b2399b3ed793d548a498dc132e69ef6081))
60 | * add intel module ([#22](https://github.com/crowdstrike/falcon-mcp/issues/22)) ([6da3359](https://github.com/crowdstrike/falcon-mcp/commit/6da3359e3890d6ee218b105f4342a1ae13690e79))
61 | * add resources infrastructure ([#39](https://github.com/crowdstrike/falcon-mcp/issues/39)) ([2629eae](https://github.com/crowdstrike/falcon-mcp/commit/2629eaef671f75d244f355d43c3e18cad47ee488))
62 | * add spotlight module ([#58](https://github.com/crowdstrike/falcon-mcp/issues/58)) ([713b551](https://github.com/crowdstrike/falcon-mcp/commit/713b55193141fc5d71f3bdc273d960c20e99bff8))
63 | * add streamable-http transport with Docker support and testing ([#24](https://github.com/crowdstrike/falcon-mcp/issues/24)) ([5e44e97](https://github.com/crowdstrike/falcon-mcp/commit/5e44e9708bcccd2580444ffcaf27b03fb6716c9d))
64 | * add user agent ([#68](https://github.com/crowdstrike/falcon-mcp/issues/68)) ([824a69f](https://github.com/crowdstrike/falcon-mcp/commit/824a69f23211cb1e0699332fa07b453bbf0401b4))
65 | * average CrowdScore ([#20](https://github.com/crowdstrike/falcon-mcp/issues/20)) ([6580663](https://github.com/crowdstrike/falcon-mcp/commit/65806634d49248c6b59ef509eadbf4d2b64145f1))
66 | * cloud module ([#56](https://github.com/crowdstrike/falcon-mcp/issues/56)) ([7f563c2](https://github.com/crowdstrike/falcon-mcp/commit/7f563c2e0b5afa35af3d9dbfb778f07b014812ab))
67 | * convert fql guides to resources ([#62](https://github.com/crowdstrike/falcon-mcp/issues/62)) ([63bff7d](https://github.com/crowdstrike/falcon-mcp/commit/63bff7d3a87ea6c07b290f0c610e95e3a4c8423d))
68 | * create _is_error method ([ee7bd01](https://github.com/crowdstrike/falcon-mcp/commit/ee7bd01d691a2cd6a74c2a9c50f406f3bd6e09de))
69 | * flexible tool input parsing ([#41](https://github.com/crowdstrike/falcon-mcp/issues/41)) ([06287fe](https://github.com/crowdstrike/falcon-mcp/commit/06287feaccf41f4c41d587c9ab2f0a874382455b))
70 | * idp support domain lookup and input sanitization ([#73](https://github.com/crowdstrike/falcon-mcp/issues/73)) ([9d6858c](https://github.com/crowdstrike/falcon-mcp/commit/9d6858cd7d0f97a1fbcca3858cafccf688e73da6))
71 | * implement lazy module discovery ([#37](https://github.com/crowdstrike/falcon-mcp/issues/37)) ([a38c949](https://github.com/crowdstrike/falcon-mcp/commit/a38c94973aae3ebdc5b5f51f0980b0266c287680))
72 | * implement lazy module discovery approach ([a38c949](https://github.com/crowdstrike/falcon-mcp/commit/a38c94973aae3ebdc5b5f51f0980b0266c287680))
73 | * initial implementation for the falcon-mcp server ([#4](https://github.com/crowdstrike/falcon-mcp/issues/4)) ([773ecb5](https://github.com/crowdstrike/falcon-mcp/commit/773ecb54f5c7ef7760933a5c12b473df953ca85c))
74 | * refactor to use falcon_mcp name and absolute imports ([#52](https://github.com/crowdstrike/falcon-mcp/issues/52)) ([8fe3f2d](https://github.com/crowdstrike/falcon-mcp/commit/8fe3f2d28573258a620c50270cd23c56aaf4d5fb))
75 |
76 |
77 | ### Bug Fixes
78 |
79 | * conversational incidents ([#21](https://github.com/crowdstrike/falcon-mcp/issues/21)) ([ee7bd01](https://github.com/crowdstrike/falcon-mcp/commit/ee7bd01d691a2cd6a74c2a9c50f406f3bd6e09de))
80 | * count number of tools correctly ([#72](https://github.com/crowdstrike/falcon-mcp/issues/72)) ([6c2284e](https://github.com/crowdstrike/falcon-mcp/commit/6c2284e2bac220bfc55b9aea1b416300dbceffb6))
81 | * discover modules in examples ([#31](https://github.com/crowdstrike/falcon-mcp/issues/31)) ([e443fc8](https://github.com/crowdstrike/falcon-mcp/commit/e443fc8348b8aa8c79c17733833b0cb3509d7451))
82 | * ensures proper lists are passed to module arg + ENV VAR support for args ([#54](https://github.com/crowdstrike/falcon-mcp/issues/54)) ([9820310](https://github.com/crowdstrike/falcon-mcp/commit/982031012184b4fe5d5054ace41a4abcac0ff86b))
83 | * freshen up e2e tests ([#40](https://github.com/crowdstrike/falcon-mcp/issues/40)) ([7ba3d86](https://github.com/crowdstrike/falcon-mcp/commit/7ba3d86faed06b4033074bbed0eb5410d87f117f))
84 | * improve error handling and fix lint issue ([#69](https://github.com/crowdstrike/falcon-mcp/issues/69)) ([31672ad](https://github.com/crowdstrike/falcon-mcp/commit/31672ad20a7a78f9edb5e7d5f7e5d610bf8aafb6))
85 | * lock version for mcp-use to 1.3.1 ([#47](https://github.com/crowdstrike/falcon-mcp/issues/47)) ([475fe0a](https://github.com/crowdstrike/falcon-mcp/commit/475fe0a59879a5c53198ebd5e9b548d2fdfd9538))
86 | * make api scope names the UI name to prevent confusion ([#67](https://github.com/crowdstrike/falcon-mcp/issues/67)) ([0089fec](https://github.com/crowdstrike/falcon-mcp/commit/0089fec425c5d1a58e15ebb3d6262cfa21b61931))
87 | * return types for incidents ([ee7bd01](https://github.com/crowdstrike/falcon-mcp/commit/ee7bd01d691a2cd6a74c2a9c50f406f3bd6e09de))
88 |
89 |
90 | ### Documentation
91 |
92 | * major refinements to README ([#55](https://github.com/crowdstrike/falcon-mcp/issues/55)) ([c98dde4](https://github.com/crowdstrike/falcon-mcp/commit/c98dde4a35491806a27bc1ef3ec53e184810b7b9))
93 | * minor readme updates ([7ad3285](https://github.com/crowdstrike/falcon-mcp/commit/7ad3285a942917502cebd8bf1bf067db12a0d6c6))
94 | * provide better clarity around using .env ([#71](https://github.com/crowdstrike/falcon-mcp/issues/71)) ([2e5ec0c](https://github.com/crowdstrike/falcon-mcp/commit/2e5ec0cfd5ba918625481b0c4ea75bf161a3a606))
95 | * update descriptions for better clarity ([#49](https://github.com/crowdstrike/falcon-mcp/issues/49)) ([1fceee1](https://github.com/crowdstrike/falcon-mcp/commit/1fceee1070d04da20fea8e1c19c0c4e286e67828))
96 | * update readme ([#64](https://github.com/crowdstrike/falcon-mcp/issues/64)) ([7b21c1b](https://github.com/crowdstrike/falcon-mcp/commit/7b21c1b8f42a33c3704e116a56e13af6108609aa))
97 |
```
--------------------------------------------------------------------------------
/falcon_mcp/resources/cloud.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Contains Cloud resources.
3 | """
4 |
5 | from falcon_mcp.common.utils import generate_md_table
6 |
7 | FQL_DOCUMENTATION = """Falcon Query Language (FQL)
8 |
9 | === BASIC SYNTAX ===
10 | property_name:[operator]'value'
11 |
12 | === AVAILABLE OPERATORS ===
13 | • No operator = equals (default)
14 | • ! = not equal
15 | • > = greater than
16 | • >= = greater than or equal
17 | • < = less than
18 | • <= = less than or equal
19 | • ~ = text match (ignores case, spaces, punctuation)
20 | • !~ = not text match
21 | • * = wildcard (one or more characters)
22 | • !* = not wildcard (one or more characters)
23 |
24 | === COMBINING CONDITIONS ===
25 | • + = AND condition
26 | • , = OR condition
27 | • ( ) = Group expressions
28 |
29 | === DATA TYPES & SUPPORTED OPERATORS ===
30 | • String: equal, not equal, wildcard.
31 | • Date, Timestamp: equal, not equal, less than, less than or equal, greater than, greater than or equal.
32 | • Boolean: equal, not equal.
33 | • Number: equal, not equal, less than, less than or equal, greater than, greater than or equal.
34 |
35 | === DATA TYPES & SYNTAX ===
36 | • String: 'value' or ['value1', 'value2'] for a list of values. Wildcards: 'partial*' or '*partial' or '*partial*'.
37 | • Date, Timestamp: 'YYYY-MM-DDTHH:MM:SSZ' (UTC format).
38 | • Boolean: true or false (no quotes).
39 | • Number: 123 (no quotes).
40 |
41 | === IMPORTANT NOTES ===
42 | • Use single quotes around string values: 'value'
43 | • Use square brackets for list of string values: ['value 1', 'value 2']
44 | • Use wildcard operator to determine if a property contains or not a substring. Ex: `property:*'*sub*'`, `property:!*'*sub*'`
45 | • Dates and timestamps format must be UTC: 'YYYY-MM-DDTHH:MM:SSZ'
46 | """
47 |
48 | # List of tuples containing filter options data: (name, type, description)
49 | KUBERNETES_CONTAINERS_FQL_FILTERS = [
50 | (
51 | "Name",
52 | "Type",
53 | "Description"
54 | ),
55 | (
56 | "agent_id",
57 | "String",
58 | """
59 | The sensor agent ID running in the container.
60 |
61 | Ex: agent_id:'3c1ca4a114504ca89af51fd126991efd'
62 | """
63 | ),
64 | (
65 | "agent_type",
66 | "String",
67 | """
68 | The sensor agent type running in the container.
69 |
70 | Ex: agent_type:'Falcon sensor for linux'
71 | """
72 | ),
73 | (
74 | "ai_related",
75 | "Boolean",
76 | """
77 | Determines if the container hosts AI related packages.
78 |
79 | Ex: ai_related:true
80 | """
81 | ),
82 | (
83 | "cloud_account_id",
84 | "String",
85 | """
86 | The cloud provider account ID.
87 |
88 | Ex: cloud_account_id:'171998889118'
89 | """
90 | ),
91 | (
92 | "cloud_name",
93 | "String",
94 | """
95 | The cloud provider name.
96 |
97 | Ex: cloud_name:'AWS'
98 | """
99 | ),
100 | (
101 | "cloud_region",
102 | "String",
103 | """
104 | The cloud region.
105 |
106 | Ex: cloud_region:'us-1'
107 | """
108 | ),
109 | (
110 | "cluster_id",
111 | "String",
112 | """
113 | The kubernetes cluster ID of the container.
114 |
115 | Ex: cluster_id:'6055bde7-acfe-48ae-9ee0-0ac1a60d8eac'
116 | """
117 | ),
118 | (
119 | "cluster_name",
120 | "String",
121 | """
122 | The kubernetes cluster that manages the container.
123 |
124 | Ex: cluster_name:'prod-cluster'
125 | """
126 | ),
127 | (
128 | "container_id",
129 | "String",
130 | """
131 | The kubernetes container ID.
132 |
133 | Ex: container_id:'c30c45f9-4702-4663-bce8-cca9f2237d1d'
134 | """
135 | ),
136 | (
137 | "container_name",
138 | "String",
139 | """
140 | The kubernetes container name.
141 |
142 | Ex: container_name:'prod-cluster'
143 | """
144 | ),
145 | (
146 | "cve_id",
147 | "String",
148 | """
149 | The CVE ID found in the container image.
150 |
151 | Ex: cve_id:'CVE-2025-1234'
152 | """
153 | ),
154 | (
155 | "detection_name",
156 | "String",
157 | """
158 | The name of the detection found in the container image.
159 |
160 | Ex: detection_name:'RunningAsRootContainer'
161 | """
162 | ),
163 | (
164 | "first_seen",
165 | "Timestamp",
166 | """
167 | Timestamp when the kubernetes container was first seen in UTC date format ("YYYY-MM-DDTHH:MM:SSZ").
168 |
169 | Ex: first_seen:'2025-01-19T11:14:15Z'
170 | """
171 | ),
172 | (
173 | "image_detection_count",
174 | "Number",
175 | """
176 | Number of images detections found in the container image.
177 |
178 | Ex: image_detection_count:5
179 | """
180 | ),
181 | (
182 | "image_digest",
183 | "String",
184 | """
185 | The digest of the container image.
186 |
187 | Ex: image_digest:'sha256:a08d3ee8ee68ebd8a78525a710c6479270692259e'
188 | """
189 | ),
190 | (
191 | "image_has_been_assessed",
192 | "Boolean",
193 | """
194 | Tells whether the container image has been assessed.
195 |
196 | Ex: image_has_been_assessed:true
197 | """
198 | ),
199 | (
200 | "image_id",
201 | "String",
202 | """
203 | The ID of the container image.
204 |
205 | Ex: image_id:'a90f484d134848af858cd409801e213e'
206 | """
207 | ),
208 | (
209 | "image_registry",
210 | "String",
211 | """
212 | The registry of the container image.
213 | """
214 | ),
215 | (
216 | "image_repository",
217 | "String",
218 | """
219 | The repository of the container image.
220 |
221 | Ex: image_repository:'my-app'
222 | """
223 | ),
224 | (
225 | "image_tag",
226 | "String",
227 | """
228 | The tag of the container image.
229 |
230 | Ex: image_tag:'v1.0.0'
231 | """
232 | ),
233 | (
234 | "image_vulnerability_count",
235 | "Number",
236 | """
237 | Number of image vulnerabilities found in the container image.
238 |
239 | Ex: image_vulnerability_count:1
240 | """
241 | ),
242 | (
243 | "insecure_mount_source",
244 | "String",
245 | """
246 | File path of the insecure mount in the container.
247 |
248 | Ex: insecure_mount_source:'/var/data'
249 | """
250 | ),
251 | (
252 | "insecure_mount_type",
253 | "String",
254 | """
255 | Type of the insecure mount in the container.
256 |
257 | Ex: insecure_mount_type:'hostPath'
258 | """
259 | ),
260 | (
261 | "insecure_propagation_mode",
262 | "Boolean",
263 | """
264 | Tells whether the container has an insecure mount propagation mode.
265 |
266 | Ex: insecure_propagation_mode:false
267 | """
268 | ),
269 | (
270 | "interactive_mode",
271 | "Boolean",
272 | """
273 | Tells whether the container is running in interactive mode.
274 |
275 | Ex: interactive_mode:true
276 | """
277 | ),
278 | (
279 | "ipv4",
280 | "String",
281 | """
282 | The IPv4 of the container.
283 |
284 | Ex: ipv4:'10.10.1.5'
285 | """
286 | ),
287 | (
288 | "ipv6",
289 | "String",
290 | """
291 | The IPv6 of the container.
292 |
293 | Ex: ipv6:'2001:db8::ff00:42:8329'
294 | """
295 | ),
296 | (
297 | "last_seen",
298 | "Timestamp",
299 | """
300 | Timestamp when the kubernetes container was last seen in UTC date format ("YYYY-MM-DDTHH:MM:SSZ").
301 |
302 | Ex: last_seen:'2025-01-19T11:14:15Z'
303 | """
304 | ),
305 | (
306 | "namespace",
307 | "String",
308 | """
309 | The kubernetes namespace name.
310 |
311 | Ex: namespace:'default'
312 | """
313 | ),
314 | (
315 | "node_name",
316 | "String",
317 | """
318 | The name of the kubernetes node.
319 |
320 | Ex: node_name:'k8s-pool'
321 | """
322 | ),
323 | (
324 | "node_uid",
325 | "String",
326 | """
327 | The kubernetes node UID of the container.
328 |
329 | Ex: node_uid:'79f1741e7db542bdaaecac11a7f7b7ae'
330 | """
331 | ),
332 | (
333 | "pod_id",
334 | "String",
335 | """
336 | The kubernetes pod ID of the container.
337 |
338 | Ex: pod_id:'6ab0fffa-2662-440b-8e95-2be93e11da3c'
339 | """
340 | ),
341 | (
342 | "pod_name",
343 | "String",
344 | """
345 | The kubernetes pod name of the container.
346 | """
347 | ),
348 | (
349 | "port",
350 | "String",
351 | """
352 | The port that the container exposes.
353 | """
354 | ),
355 | (
356 | "privileged",
357 | "Boolean",
358 | """
359 | Tells whether the container is running with elevated privileges.
360 |
361 | Ex: privileged:false
362 | """
363 | ),
364 | (
365 | "root_write_access",
366 | "Boolean",
367 | """
368 | Tells whether the container has root write access.
369 |
370 | Ex: root_write_access:false
371 | """
372 | ),
373 | (
374 | "run_as_root_group",
375 | "Boolean",
376 | """
377 | Tells whether the container is running as root group.
378 | """
379 | ),
380 | (
381 | "run_as_root_user",
382 | "Boolean",
383 | """
384 | Tells whether the container is running as root user.
385 | """
386 | ),
387 | (
388 | "running_status",
389 | "Boolean",
390 | """
391 | Tells whether the container is running.
392 |
393 | Ex: running_status:true
394 | """
395 | ),
396 | ]
397 |
398 | KUBERNETES_CONTAINERS_FQL_DOCUMENTATION = (
399 | FQL_DOCUMENTATION
400 | + """
401 | === falcon_search_kubernetes_containers FQL filter available fields ===
402 |
403 | """ + generate_md_table(KUBERNETES_CONTAINERS_FQL_FILTERS) + """
404 |
405 | === falcon_search_kubernetes_containers FQL filter examples ===
406 |
407 | # Find kubernetes containers that are running and have 1 or more image vulnerabilities
408 | image_vulnerability_count:>0+running_status:true
409 |
410 | # Find kubernetes containers seen in the last 7 days and by the CVE ID found in their container images
411 | cve_id:'CVE-2025-1234'+last_seen:>'2025-03-15T00:00:00Z'
412 |
413 | # Find kubernetes containers whose cloud_name is in a list
414 | cloud_name:['AWS', 'Azure']
415 |
416 | # Find kubernetes containers whose names starts with "app-"
417 | container_name:*'app-*'
418 |
419 | # Find kubernetes containers whose cluster or namespace name is "prod"
420 | cluster_name:'prod',namespace:'prod'
421 |
422 | === falcon_count_kubernetes_containers FQL filter examples ===
423 |
424 | # Count kubernetes containers by cluster name
425 | cluster_name:'staging'
426 |
427 | # Count kubernetes containers by agent type
428 | agent_type:'Kubernetes'
429 | """
430 | )
431 |
432 | # List of tuples containing filter options data: (name, type, description)
433 | IMAGES_VULNERABILITIES_FQL_FILTERS = [
434 | (
435 | "Name",
436 | "Type",
437 | "Description"
438 | ),
439 | (
440 | "ai_related",
441 | "Boolean",
442 | """
443 | Tells whether the image has AI related packages.
444 |
445 | Ex: ai_related:true
446 | """
447 | ),
448 | (
449 | "base_os",
450 | "String",
451 | """
452 | The base operating system of the image.
453 |
454 | Ex: base_os:'ubuntu'
455 | """
456 | ),
457 | (
458 | "container_id",
459 | "String",
460 | """
461 | The kubernetes container id in which the image vulnerability was detected.
462 |
463 | Ex: container_id:'515f976c43eaa3edf51590e7217ac8191a7e50c59'
464 | """
465 | ),
466 | (
467 | "container_running_status",
468 | "Boolean",
469 | """
470 | The running status of the kubernetes container in which the image vulnerability was detected.
471 |
472 | Ex: container_running_status:true
473 | """
474 | ),
475 | (
476 | "cps_rating",
477 | "String",
478 | """
479 | The CSP rating of the image vulnerability.
480 | Possible values: Low, Medium, High, Critical
481 |
482 | Ex: cps_rating:'Critical'
483 | """
484 | ),
485 | (
486 | "cve_id",
487 | "String",
488 | """
489 | The CVE ID of the image vulnerability.
490 |
491 | Ex: cve_id:'CVE-2025-1234'
492 | """
493 | ),
494 | (
495 | "cvss_score",
496 | "Number",
497 | """
498 | The CVSS Score of the image vulnerability. The value must be between 0 and 10.
499 |
500 | Ex: cvss_score:8
501 | """
502 | ),
503 | (
504 | "image_digest",
505 | "String",
506 | """
507 | The digest of the image.
508 |
509 | Ex: image_digest:'sha256:a08d3ee8ee68ebd8a78525a710c6479270692259e'
510 | """
511 | ),
512 | (
513 | "image_id",
514 | "String",
515 | """
516 | The ID of the image.
517 |
518 | Ex: image_id:'a90f484d134848af858cd409801e213e'
519 | """
520 | ),
521 | (
522 | "registry",
523 | "String",
524 | """
525 | The image registry of the image in which the vulnerability was detected.
526 |
527 | Ex: registry:'docker.io'
528 | """
529 | ),
530 | (
531 | "repository",
532 | "String",
533 | """
534 | The image repository of the image in which the vulnerability was detected.
535 |
536 | Ex: repository:'my-app'
537 | """
538 | ),
539 | (
540 | "severity",
541 | "String",
542 | """
543 | The severity of the vulnerability.
544 | Available values: Low, Medium, High, Critical.
545 |
546 | Ex: severity:'High'
547 | """
548 | ),
549 | (
550 | "tag",
551 | "String",
552 | """
553 | The image tag of the image in which the vulnerability was detected.
554 |
555 | Ex: tag:'v1.0.0'
556 | """
557 | ),
558 | ]
559 |
560 | IMAGES_VULNERABILITIES_FQL_DOCUMENTATION = (
561 | FQL_DOCUMENTATION
562 | + """
563 | === falcon_search_images_vulnerabilities FQL filter options ===
564 |
565 | """ + generate_md_table(IMAGES_VULNERABILITIES_FQL_FILTERS) + """
566 |
567 | === falcon_search_images_vulnerabilities FQL filter examples ===
568 |
569 | # Find images vulnerabilities by container ID
570 | container_id:'12341223'
571 |
572 | # Find images vulnerabilities by a list of container IDs
573 | container_id:['12341223', '199929292', '1000101']
574 |
575 | # Find images vulnerabilities by CVSS score and container with running status true
576 | cvss_score:>5+container_running_status:true
577 |
578 | # Find images vulnerabilities by image registry using wildcard
579 | registry:*'*docker*'
580 | """
581 | )
582 |
```