main
py 323 lines 11.3 KB
Raw
1 from typing import Union
2
3 ## Auth Things
4 from fastapi import APIRouter
5 from fastapi import Depends
6 from fastapi import File
7 from fastapi import HTTPException
8 from fastapi import Security
9 from fastapi import UploadFile
10 from loguru import logger
11 from sqlalchemy.ext.asyncio import AsyncSession
12
13 from app.auth.utils import AuthHandler
14 from app.connectors.schema import ConnectorListResponse
15 from app.connectors.schema import ConnectorResponse
16 from app.connectors.schema import ConnectorsListResponse
17 from app.connectors.schema import UpdateConnector
18 from app.connectors.schema import VerifyConnectorResponse
19 from app.connectors.services import ConnectorServices
20 from app.db.db_session import get_db
21
22 connector_router = APIRouter()
23
24
25 @connector_router.get(
26 "",
27 response_model=ConnectorsListResponse,
28 description="Fetch all available connectors",
29 dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
30 )
31 async def get_connectors(
32 session: AsyncSession = Depends(get_db),
33 ) -> ConnectorsListResponse:
34 """
35 Fetch all available connectors from the database.
36
37 This endpoint retrieves all the connectors stored in the database and returns them
38 along with a success status and message.
39
40 Returns:
41 ConnectorListResponse: A Pydantic model containing a list of connectors and additional metadata.
42
43 Raises:
44 HTTPException: An exception with a 404 status code is raised if no connectors are found.
45 """
46 connectors = await ConnectorServices.fetch_all_connectors(session=session)
47 if connectors:
48 return {
49 "connectors": connectors,
50 "success": True,
51 "message": "Connectors fetched successfully",
52 }
53 else:
54 raise HTTPException(status_code=404, detail="No connectors found")
55
56
57 @connector_router.get(
58 "/{connector_id}",
59 response_model=ConnectorListResponse,
60 description="Fetch a specific connector",
61 # Admin-only: this response includes plaintext connector_password / connector_api_key,
62 # so it must match the admin-only list endpoint above and must not be reachable by the
63 # analyst role. See GHSA-c5pw-2h98-r798.
64 dependencies=[Security(AuthHandler().require_any_scope("admin"))],
65 )
66 async def get_connector(
67 connector_id: int,
68 session: AsyncSession = Depends(get_db),
69 ) -> Union[ConnectorResponse, HTTPException]:
70 """
71 Fetch a specific connector by its ID.
72
73 This endpoint retrieves a connector identified by `connector_id` from the database.
74
75 Args:
76 connector_id (int): The unique identifier for the connector to fetch.
77
78 Returns:
79 ConnectorResponse: A Pydantic model representing the fetched connector.
80
81 Raises:
82 HTTPException: An exception with a 404 status code is raised if the connector is not found.
83 """
84 connector = await ConnectorServices.fetch_connector_by_id(
85 connector_id,
86 session=session,
87 )
88 if connector is not None:
89 return {
90 "connector": connector,
91 "success": True,
92 "message": "Connector fetched successfully",
93 }
94 else:
95 raise HTTPException(
96 status_code=404,
97 detail=f"No connector found for ID: {connector_id}".format(
98 connector_id=connector_id,
99 ),
100 )
101
102
103 @connector_router.post(
104 "/verify/{connector_id}",
105 response_model=VerifyConnectorResponse,
106 description="Verify a connector. Makes an API call to the connector to verify it is working.",
107 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
108 )
109 async def verify_connector(
110 connector_id: int,
111 session: AsyncSession = Depends(get_db),
112 ) -> Union[VerifyConnectorResponse, HTTPException]:
113 """
114 Verify a connector by its ID.
115
116 This endpoint verifies a connector identified by `connector_id` by making an API call to the connector.
117
118 Args:
119 connector_id (int): The unique identifier for the connector to verify.
120
121 Returns:
122 ConnectorResponse: A Pydantic model representing the verified connector.
123
124 Raises:
125 HTTPException: An exception with a 404 status code is raised if the connector is not found.
126 """
127 connector = await ConnectorServices.verify_connector_by_id(
128 connector_id,
129 session=session,
130 )
131 if connector is None:
132 raise HTTPException(
133 status_code=404,
134 detail=f"No connector found for ID: {connector_id}".format(
135 connector_id=connector_id,
136 ),
137 )
138 if connector["connectionSuccessful"] is False:
139 raise HTTPException(
140 status_code=500,
141 detail=f"Failed to verify connector: {connector['message']}",
142 )
143 return connector
144
145
146 @connector_router.put(
147 "/{connector_id}",
148 response_model=ConnectorListResponse,
149 description="Update a connector",
150 dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
151 )
152 async def update_connector(
153 connector_id: int,
154 connector: UpdateConnector,
155 session: AsyncSession = Depends(get_db),
156 ) -> ConnectorListResponse:
157 """
158 Update a connector by its ID.
159
160 This endpoint updates a connector identified by `connector_id` in the database.
161
162 Args:
163 connector_id (int): The unique identifier for the connector to update.
164 connector (ConnectorListResponse): The updated connector data.
165
166 Returns:
167 ConnectorListResponse: A Pydantic model representing the updated connector.
168
169 Raises:
170 HTTPException: An exception with a 404 status code is raised if the connector is not found.
171 """
172 updated_connector = await ConnectorServices.update_connector_by_id(
173 connector_id,
174 connector,
175 session=session,
176 )
177 if updated_connector is not None:
178 await ConnectorServices.verify_connector_by_id(connector_id, session=session)
179 return {
180 "connector": updated_connector,
181 "success": True,
182 "message": "Connector updated successfully",
183 }
184 else:
185 raise HTTPException(
186 status_code=404,
187 detail=f"No connector found for ID: {connector_id}".format(
188 connector_id=connector_id,
189 ),
190 )
191
192
193 # @connector_router.post(
194 # "/upload/{connector_id}",
195 # description="Upload a YAML file for a specific connector",
196 # dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
197 # )
198 # async def upload_yaml_file(
199 # connector_id: int,
200 # file: UploadFile = File(...),
201 # session: AsyncSession = Depends(get_db),
202 # ) -> dict:
203 # """
204 # Upload a YAML file for a specific connector ID.
205
206 # This endpoint allows you to upload a `.yaml` file for a specific connector
207 # identified by `connector_id`.
208
209 # Args:
210 # connector_id (int): The unique identifier for the connector.
211 # file (UploadFile): The `.yaml` file to be uploaded.
212
213 # Returns:
214 # dict: A dictionary with a success message and other information.
215
216 # Raises:
217 # HTTPException: An exception with a 400 status code is raised if the file format is incorrect or connector ID is not 6.
218 # """
219 # if connector_id not in [5, 6]:
220 # raise HTTPException(
221 # status_code=400,
222 # detail="Only the Velociraptor or another specific connector is allowed for YAML file uploads.",
223 # )
224 # if not file.filename.endswith(".yaml"):
225 # raise HTTPException(status_code=400, detail="Only .yaml files are allowed.")
226 # try:
227 # save_file_result = await ConnectorServices.save_file(file, connector_id, session=session)
228 # if save_file_result:
229 # await ConnectorServices.verify_connector_by_id(
230 # connector_id,
231 # session=session,
232 # )
233 # return {"success": True, "message": "File uploaded successfully"}
234 # else:
235 # raise HTTPException(status_code=500, detail="Failed to upload file")
236 # except Exception as e:
237 # logger.error(f"Failed to upload file: {e}")
238 # raise HTTPException(status_code=500, detail="Failed to upload file")
239
240
241 @connector_router.post(
242 "/upload/{connector_identifier}",
243 description="Upload a YAML file for a specific connector",
244 dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
245 )
246 async def upload_yaml_file(
247 connector_identifier: str,
248 file: UploadFile = File(...),
249 session: AsyncSession = Depends(get_db),
250 ) -> dict:
251 """
252 Upload a YAML file for a specific connector identified by ID or name.
253
254 This endpoint allows you to upload a `.yaml` file for a connector
255 identified by either its ID (number) or name (string).
256
257 Args:
258 connector_identifier (str): The unique identifier (ID or name) for the connector.
259 file (UploadFile): The `.yaml` file to be uploaded.
260
261 Returns:
262 dict: A dictionary with a success message and other information.
263
264 Raises:
265 HTTPException: An exception is raised if the file format is incorrect or connector is not found/supported.
266 """
267 # Try to parse as integer for connector_id
268 connector_id = None
269 try:
270 connector_id = int(connector_identifier)
271 # Check if this is a valid connector ID for YAML uploads
272 if connector_id not in [5, 6]:
273 raise HTTPException(
274 status_code=400,
275 detail="Only the Velociraptor or another specific connector is allowed for YAML file uploads.",
276 )
277 except ValueError:
278 # If not an integer, treat as connector name
279 connector_name = connector_identifier
280 logger.info(f"Using connector name: {connector_name}")
281 try:
282 # Look up the connector ID from the name
283 connector = await ConnectorServices.fetch_connector_by_name(connector_name, session=session)
284 if not connector:
285 raise HTTPException(
286 status_code=404,
287 detail=f"No connector found with name: {connector_name}",
288 )
289
290 # Access as an attribute using dot notation, not as a dictionary
291 connector_id = connector.id
292
293 # Check if this is a valid connector for YAML uploads
294 if connector_id not in [5, 6]:
295 raise HTTPException(
296 status_code=400,
297 detail=f"Connector '{connector_name}' does not support YAML file uploads.",
298 )
299 except Exception as e:
300 logger.error(f"Error finding connector by name: {str(e)}")
301 raise HTTPException(
302 status_code=404,
303 detail=f"Could not find connector with name: {connector_name}",
304 )
305
306 # Check file format
307 if not file.filename.endswith(".yaml"):
308 raise HTTPException(status_code=400, detail="Only .yaml files are allowed.")
309
310 # Process the file upload
311 try:
312 save_file_result = await ConnectorServices.save_file(file, connector_id, session=session)
313 if save_file_result:
314 await ConnectorServices.verify_connector_by_id(
315 connector_id,
316 session=session,
317 )
318 return {"success": True, "message": "File uploaded successfully"}
319 else:
320 raise HTTPException(status_code=500, detail="Failed to upload file")
321 except Exception as e:
322 logger.error(f"Failed to upload file: {e}")
323 raise HTTPException(status_code=500, detail="Failed to upload file")