@cryptotaxi247 / CoPilot / commits / 2055302a

Copilot action (#496)

* Add Copilot Action routes and service for inventory management * precommit-fixes

taylor_socfortress committed Aug 27, 2025 at 12:23 UTC 2055302a3f60304fef495ede79705e95d58699ca
5 files changed +494
backend/app/integrations/copilot_action/routes/copilot_action.py new
+184
@@ -0,0 +1,184 @@
1 +import os
2 +from typing import Optional
3 +
4 +from fastapi import APIRouter
5 +from fastapi import HTTPException
6 +from fastapi import Query
7 +from fastapi import Security
8 +from loguru import logger
9 +
10 +from app.auth.routes.auth import AuthHandler
11 +from app.integrations.copilot_action.schema.copilot_action import ActionDetailResponse
12 +from app.integrations.copilot_action.schema.copilot_action import (
13 + InventoryMetricsResponse,
14 +)
15 +from app.integrations.copilot_action.schema.copilot_action import InventoryResponse
16 +from app.integrations.copilot_action.schema.copilot_action import Technology
17 +from app.integrations.copilot_action.services.copilot_action import CopilotActionService
18 +
19 +copilot_action_router = APIRouter()
20 +auth_handler = AuthHandler()
21 +
22 +
23 +@copilot_action_router.get(
24 + "/inventory",
25 + response_model=InventoryResponse,
26 + description="Get inventory of available active response scripts",
27 + dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
28 +)
29 +async def get_inventory(
30 + technology: Optional[Technology] = Query(None, description="Filter by technology type"),
31 + category: Optional[str] = Query(None, description="Filter by category"),
32 + tag: Optional[str] = Query(None, description="Filter by tag"),
33 + q: Optional[str] = Query(None, description="Free-text search query"),
34 + limit: int = Query(100, ge=1, le=1000, description="Maximum number of results"),
35 + offset: int = Query(0, ge=0, description="Offset for pagination"),
36 + refresh: bool = Query(False, description="Force refresh cache"),
37 + include: Optional[str] = Query(None, description="Comma-separated extra fields to include"),
38 +) -> InventoryResponse:
39 + """
40 + Retrieve inventory of available active response scripts.
41 +
42 + This endpoint fetches the catalog of active response scripts from the
43 + Copilot Action service, with optional filtering and pagination.
44 +
45 + Args:
46 + technology: Filter by technology type (e.g., Windows, Linux, Wazuh)
47 + category: Filter by category if present
48 + tag: Filter by tag contained in the tags list
49 + q: Free-text search in name/description
50 + limit: Maximum number of results (1-1000)
51 + offset: Offset for pagination
52 + refresh: Force refresh the remote cache
53 + include: Extra fields to include (e.g., 'category,tags')
54 +
55 + Returns:
56 + InventoryResponse: List of active response scripts with metadata
57 + """
58 + logger.info(f"Fetching active response inventory with filters: tech={technology}, category={category}, tag={tag}, q={q}")
59 +
60 + # Get license key from environment variable
61 + license_key = os.getenv("COPILOT_API_KEY")
62 + if not license_key:
63 + logger.error("COPILOT_API_KEY environment variable not set")
64 + raise HTTPException(status_code=500, detail="COPILOT_API_KEY environment variable not configured")
65 +
66 + # Fetch inventory from service
67 + try:
68 + response = await CopilotActionService.get_inventory(
69 + license_key=license_key,
70 + technology=technology,
71 + category=category,
72 + tag=tag,
73 + q=q,
74 + limit=limit,
75 + offset=offset,
76 + refresh=refresh,
77 + include=include,
78 + )
79 +
80 + logger.info(f"Successfully fetched inventory: {len(response.copilot_actions)} actions")
81 + return response
82 +
83 + except Exception as e:
84 + logger.error(f"Error fetching inventory: {str(e)}")
85 + raise HTTPException(status_code=500, detail=f"Error fetching inventory: {str(e)}")
86 +
87 +
88 +@copilot_action_router.get(
89 + "/inventory/{copilot_action_name}",
90 + response_model=ActionDetailResponse,
91 + description="Get details for a specific active response script",
92 + dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
93 +)
94 +async def get_action_by_name(copilot_action_name: str) -> ActionDetailResponse:
95 + """
96 + Get detailed information for a specific active response script.
97 +
98 + Args:
99 + copilot_action_name: Name of the action to retrieve
100 +
101 + Returns:
102 + ActionDetailResponse: Detailed information about the action
103 + """
104 + logger.info(f"Fetching action details for: {copilot_action_name}")
105 +
106 + # Get license key from environment variable
107 + license_key = os.getenv("COPILOT_API_KEY")
108 + if not license_key:
109 + logger.error("COPILOT_API_KEY environment variable not set")
110 + raise HTTPException(status_code=500, detail="COPILOT_API_KEY environment variable not configured")
111 +
112 + # Fetch action details from service
113 + try:
114 + response = await CopilotActionService.get_action_by_name(license_key=license_key, copilot_action_name=copilot_action_name)
115 +
116 + if not response.success:
117 + if "not found" in response.message.lower():
118 + raise HTTPException(status_code=404, detail=response.message)
119 + else:
120 + raise HTTPException(status_code=500, detail=response.message)
121 +
122 + logger.info(f"Successfully fetched action details for: {copilot_action_name}")
123 + return response
124 +
125 + except HTTPException:
126 + raise
127 + except Exception as e:
128 + logger.error(f"Error fetching action details: {str(e)}")
129 + raise HTTPException(status_code=500, detail=f"Error fetching action details: {str(e)}")
130 +
131 +
132 +@copilot_action_router.get(
133 + "/metrics",
134 + response_model=InventoryMetricsResponse,
135 + description="Get inventory metrics and status",
136 + dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
137 +)
138 +async def get_metrics() -> InventoryMetricsResponse:
139 + """
140 + Get metrics and status information for the inventory service.
141 +
142 + Returns:
143 + InventoryMetricsResponse: Service metrics and status
144 + """
145 + logger.info("Fetching inventory metrics")
146 +
147 + # Get license key from environment variable
148 + license_key = os.getenv("COPILOT_API_KEY")
149 + if not license_key:
150 + logger.error("COPILOT_API_KEY environment variable not set")
151 + raise HTTPException(status_code=500, detail="COPILOT_API_KEY environment variable not configured")
152 +
153 + # Fetch metrics from service
154 + try:
155 + response = await CopilotActionService.get_metrics(license_key=license_key)
156 +
157 + logger.info("Successfully fetched inventory metrics")
158 + return response
159 +
160 + except Exception as e:
161 + logger.error(f"Error fetching metrics: {str(e)}")
162 + raise HTTPException(status_code=500, detail=f"Error fetching metrics: {str(e)}")
163 +
164 +
165 +@copilot_action_router.get(
166 + "/technologies",
167 + description="Get available technology types",
168 + dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
169 +)
170 +async def get_technologies() -> dict:
171 + """
172 + Get list of available technology types for filtering.
173 +
174 + Returns:
175 + Dictionary containing available technology types
176 + """
177 + technologies = [tech.value for tech in Technology]
178 +
179 + return {
180 + "technologies": technologies,
181 + "total": len(technologies),
182 + "message": "Successfully retrieved available technologies",
183 + "success": True,
184 + }
backend/app/integrations/copilot_action/schema/copilot_action.py new
+109
@@ -0,0 +1,109 @@
1 +from datetime import datetime
2 +from enum import Enum
3 +from typing import Any
4 +from typing import Dict
5 +from typing import List
6 +from typing import Optional
7 +from typing import Union
8 +
9 +from pydantic import BaseModel
10 +from pydantic import Field
11 +from pydantic import HttpUrl
12 +from pydantic import validator
13 +
14 +
15 +class Technology(str, Enum):
16 + """Technology types for active response scripts"""
17 +
18 + WAZUH = "Wazuh"
19 + LINUX = "Linux"
20 + WINDOWS = "Windows"
21 + MACOS = "macOS"
22 + NETWORK = "Network"
23 + CLOUD = "Cloud"
24 + VELOCIRAPTOR = "Velociraptor"
25 +
26 +
27 +class ScriptParameter(BaseModel):
28 + """Parameters required for script execution"""
29 +
30 + name: str
31 + type: str
32 + required: bool
33 + description: Optional[str] = None
34 + default: Optional[Union[str, int, float, bool, list, dict]] = None
35 + enum: Optional[List[str]] = None
36 +
37 + @validator("type")
38 + def validate_type(cls, v):
39 + allowed = {"string", "int", "float", "bool", "path", "enum", "list", "json", "integer", "boolean"}
40 + if v not in allowed:
41 + raise ValueError(f"type must be one of {sorted(allowed)}")
42 + return v
43 +
44 +
45 +class ActiveResponseItem(BaseModel):
46 + """Individual active response script item"""
47 +
48 + copilot_action_name: str
49 + description: str
50 + technology: Technology
51 + icon: Optional[str] = None
52 + script_parameters: List[ScriptParameter] = Field(default_factory=list)
53 + repo_url: HttpUrl
54 + script_name: Optional[str] = None
55 + version: Optional[str] = None
56 + last_updated: Optional[datetime] = None
57 + category: Optional[str] = None
58 + tags: Optional[List[str]] = None
59 +
60 + @validator("icon", always=True)
61 + def set_icon_default(cls, v, values):
62 + if v is None and "technology" in values:
63 + return values["technology"].value.lower()
64 + return v
65 +
66 + @validator("repo_url")
67 + def ensure_repo_url_ends_with_main(cls, v):
68 + repo_str = str(v)
69 + if not repo_str.endswith("/main"):
70 + return HttpUrl(f"{repo_str}/main")
71 + return v
72 +
73 +
74 +class InventoryQueryRequest(BaseModel):
75 + """Request model for inventory queries"""
76 +
77 + technology: Optional[Technology] = None
78 + category: Optional[str] = None
79 + tag: Optional[str] = None
80 + q: Optional[str] = None # Free-text search
81 + limit: int = Field(default=100, ge=1, le=1000)
82 + offset: int = Field(default=0, ge=0)
83 + refresh: bool = False
84 + include: Optional[str] = None # Comma-separated extra fields
85 +
86 +
87 +class InventoryResponse(BaseModel):
88 + """Response model for inventory queries"""
89 +
90 + copilot_actions: List[ActiveResponseItem]
91 + message: str
92 + success: bool
93 +
94 +
95 +class ActionDetailResponse(BaseModel):
96 + """Response model for single action details"""
97 +
98 + active_response: ActiveResponseItem
99 + message: str
100 + success: bool
101 +
102 +
103 +class InventoryMetricsResponse(BaseModel):
104 + """Response model for inventory metrics"""
105 +
106 + status: str
107 + metrics: Dict[str, Any]
108 + message: str = "Successfully retrieved inventory metrics"
109 + success: bool = True
backend/app/integrations/copilot_action/services/copilot_action.py new
+186
@@ -0,0 +1,186 @@
1 +from typing import Optional
2 +
3 +import httpx
4 +from loguru import logger
5 +
6 +from app.integrations.copilot_action.schema.copilot_action import ActionDetailResponse
7 +from app.integrations.copilot_action.schema.copilot_action import (
8 + InventoryMetricsResponse,
9 +)
10 +from app.integrations.copilot_action.schema.copilot_action import InventoryResponse
11 +from app.integrations.copilot_action.schema.copilot_action import Technology
12 +
13 +
14 +class CopilotActionService:
15 + """Service for interacting with the Copilot Action inventory API"""
16 +
17 + BASE_URL = "https://copilot-action.socfortress.co"
18 + MODULE_VERSION = "1.0.0"
19 +
20 + @classmethod
21 + async def get_inventory(
22 + cls,
23 + license_key: str,
24 + technology: Optional[Technology] = None,
25 + category: Optional[str] = None,
26 + tag: Optional[str] = None,
27 + q: Optional[str] = None,
28 + limit: int = 100,
29 + offset: int = 0,
30 + refresh: bool = False,
31 + include: Optional[str] = None,
32 + ) -> InventoryResponse:
33 + """
34 + Fetch inventory from the Copilot Action service.
35 +
36 + Args:
37 + license_key: API key for authentication
38 + technology: Filter by technology type
39 + category: Filter by category
40 + tag: Filter by tag
41 + q: Free-text search query
42 + limit: Maximum number of results
43 + offset: Offset for pagination
44 + refresh: Force refresh cache
45 + include: Comma-separated extra fields to include
46 +
47 + Returns:
48 + InventoryResponse: The inventory data
49 + """
50 + try:
51 + url = f"{cls.BASE_URL}/inventory"
52 +
53 + headers = {"x-api-key": license_key, "module-version": cls.MODULE_VERSION, "Accept": "application/json"}
54 +
55 + params = {}
56 + if technology:
57 + params["technology"] = technology.value
58 + if category:
59 + params["category"] = category
60 + if tag:
61 + params["tag"] = tag
62 + if q:
63 + params["q"] = q
64 + if limit != 100:
65 + params["limit"] = limit
66 + if offset != 0:
67 + params["offset"] = offset
68 + if refresh:
69 + params["refresh"] = "true"
70 + if include:
71 + params["include"] = include
72 +
73 + logger.info(f"Fetching inventory from {url} with params: {params}")
74 +
75 + async with httpx.AsyncClient() as client:
76 + response = await client.get(url, headers=headers, params=params, timeout=30.0)
77 +
78 + response.raise_for_status()
79 +
80 + try:
81 + data = response.json()
82 + except ValueError:
83 + logger.error(f"Non-JSON response from inventory API: {response.text[:200]}")
84 + return InventoryResponse(copilot_actions=[], message="Invalid response format from inventory API", success=False)
85 +
86 + logger.info(f"Successfully fetched inventory: {len(data.get('copilot_actions', []))} actions")
87 + return InventoryResponse(**data)
88 +
89 + except httpx.HTTPError as e:
90 + logger.error(f"HTTP error fetching inventory: {str(e)}")
91 + return InventoryResponse(copilot_actions=[], message=f"HTTP error fetching inventory: {str(e)}", success=False)
92 + except Exception as e:
93 + logger.error(f"Unexpected error fetching inventory: {str(e)}")
94 + return InventoryResponse(copilot_actions=[], message=f"Unexpected error: {str(e)}", success=False)
95 +
96 + @classmethod
97 + async def get_action_by_name(cls, license_key: str, copilot_action_name: str) -> ActionDetailResponse:
98 + """
99 + Fetch details for a specific action by name.
100 +
101 + Args:
102 + license_key: API key for authentication
103 + copilot_action_name: Name of the action to fetch
104 +
105 + Returns:
106 + ActionDetailResponse: The action details
107 + """
108 + try:
109 + url = f"{cls.BASE_URL}/inventory/{copilot_action_name}"
110 +
111 + headers = {"x-api-key": license_key, "module-version": cls.MODULE_VERSION, "Accept": "application/json"}
112 +
113 + logger.info(f"Fetching action details for: {copilot_action_name}")
114 +
115 + async with httpx.AsyncClient() as client:
116 + response = await client.get(url, headers=headers, timeout=30.0)
117 +
118 + response.raise_for_status()
119 +
120 + try:
121 + data = response.json()
122 + except ValueError:
123 + logger.error(f"Non-JSON response from action API: {response.text[:200]}")
124 + return ActionDetailResponse(active_response=None, message="Invalid response format from action API", success=False)
125 +
126 + logger.info(f"Successfully fetched action details for: {copilot_action_name}")
127 + return ActionDetailResponse(**data)
128 +
129 + except httpx.HTTPStatusError as e:
130 + if e.response.status_code == 404:
131 + logger.warning(f"Action not found: {copilot_action_name}")
132 + return ActionDetailResponse(active_response=None, message=f"Action '{copilot_action_name}' not found", success=False)
133 + logger.error(f"HTTP error fetching action details: {str(e)}")
134 + return ActionDetailResponse(active_response=None, message=f"HTTP error fetching action details: {str(e)}", success=False)
135 + except Exception as e:
136 + logger.error(f"Unexpected error fetching action details: {str(e)}")
137 + return ActionDetailResponse(active_response=None, message=f"Unexpected error: {str(e)}", success=False)
138 +
139 + @classmethod
140 + async def get_metrics(cls, license_key: str) -> InventoryMetricsResponse:
141 + """
142 + Fetch inventory metrics.
143 +
144 + Args:
145 + license_key: API key for authentication
146 +
147 + Returns:
148 + InventoryMetricsResponse: The metrics data
149 + """
150 + try:
151 + url = f"{cls.BASE_URL}/metrics"
152 +
153 + headers = {"x-api-key": license_key, "module-version": cls.MODULE_VERSION, "Accept": "application/json"}
154 +
155 + logger.info("Fetching inventory metrics")
156 +
157 + async with httpx.AsyncClient() as client:
158 + response = await client.get(url, headers=headers, timeout=30.0)
159 +
160 + response.raise_for_status()
161 +
162 + try:
163 + data = response.json()
164 + except ValueError:
165 + logger.error(f"Non-JSON response from metrics API: {response.text[:200]}")
166 + return InventoryMetricsResponse(
167 + status="error",
168 + metrics={},
169 + message="Invalid response format from metrics API",
170 + success=False,
171 + )
172 +
173 + logger.info("Successfully fetched inventory metrics")
174 + return InventoryMetricsResponse(
175 + status=data.get("status", "unknown"),
176 + metrics=data.get("metrics", {}),
177 + message="Successfully retrieved inventory metrics",
178 + success=True,
179 + )
180 +
181 + except httpx.HTTPError as e:
182 + logger.error(f"HTTP error fetching metrics: {str(e)}")
183 + return InventoryMetricsResponse(status="error", metrics={}, message=f"HTTP error fetching metrics: {str(e)}", success=False)
184 + except Exception as e:
185 + logger.error(f"Unexpected error fetching metrics: {str(e)}")
186 + return InventoryMetricsResponse(status="error", metrics={}, message=f"Unexpected error: {str(e)}", success=False)
backend/app/routers/copilot_action.py new
+13
@@ -0,0 +1,13 @@
1 +from fastapi import APIRouter
2 +
3 +from app.integrations.copilot_action.routes.copilot_action import copilot_action_router
4 +
5 +# Instantiate the APIRouter
6 +router = APIRouter()
7 +
8 +# Include the Copilot Action related routes
9 +router.include_router(
10 + copilot_action_router,
11 + prefix="/copilot_action",
12 + tags=["Copilot Action"],
13 +)
backend/copilot.py
+2
@@ -38,6 +38,7 @@ from app.routers import bitdefender
38 from app.routers import carbonblack
39 from app.routers import cato
40 from app.routers import connectors
41 +from app.routers import copilot_action
42 from app.routers import copilot_mcp
43 from app.routers import cortex
44 from app.routers import crowdstrike
@@ -145,6 +146,7 @@ api_router.include_router(threat_intel.router)
146 api_router.include_router(alert_creation_settings.router)
147 api_router.include_router(integrations.router)
148 api_router.include_router(office365.router)
149 +api_router.include_router(copilot_action.router)
150 api_router.include_router(copilot_mcp.router)
151 api_router.include_router(mimecast.router)
152 api_router.include_router(scheduler.router)