#
tokens: 46965/50000 14/111 files (page 3/5)
lines: on (toggle) GitHub
raw markdown copy reset
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, "&amp;")
271 |                 .replace(/</g, "&lt;")
272 |                 .replace(/>/g, "&gt;")
273 |                 .replace(/"/g, "&quot;")
274 |                 .replace(/'/g, "&#039;");
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 | 
```
Page 3/5FirstPrevNextLast