@cryptotaxi247 / CoPilot / commits / b3f39683

Refactor (#136)

* Split Dockerfile into two containers * split project (be/fe) * updated docker * Update logo path in README.md * Move image file to frontend directory * updated nginx config * updated docker file * updated nginx * Add APIRouter for modularizing routes * updated nginx * updated ignore file * Update Dockerfile and docker-compose.yml * added env file * new tmp dockerfile for fe * docker changes * Remove port mapping for copilot-backend container and update nginx.conf for API endpoint but still not working as expected * updated fe package.json * fix httpclient headers * updated dependencies * Fix authentication bug and improve error handling * updated docker * Update Office365 integration documentation and Docker configuration * updated images * Delete unused files and update Docker configuration * Update Dockerfile to expose port 2000 * added ssl * updated ssl generator * precommit fixes * ignore ssl files * remove nginx certs * updated ssl script * add default ss keys * Update agent SOC cases and bookmarked alerts * precommit fixes * Add Docker installation check and update frontend URL * Update image reference for copilot-frontend container * Add SERVER_IP configuration for remote machine connection * Add build-dockers.sh script to build copilot-frontend image * Remove unnecessary SSL configuration * Update API URL in .env.example file * Refactor nginx.conf for improved logging and HTTPS redirection * Update Copilot port and protocol --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Feb 9, 2024 at 09:53 UTC b3f39683269d57d9d24b84cf9e7a577e49623ac9
583 files changed +7923 -16749
.dockerignore deleted
-2
@@ -1,2 +0,0 @@
1 -backend/data/copilot.db
2 -backend/copilot.db
.env.example
+2 -12
@@ -1,15 +1,5 @@
1 -# base url
2 -SERVER_IP=YOUR_SERVER_IP
3 -VITE_API_URL=http://YOUR_SERVER_IP:5000
4 -
5 -# value in seconds
6 -VITE_TOKEN_DEBOUNCE_TIME=10
7 -
8 -# alert if value is over
9 -VITE_UNCOMMITTED_JOURNAL_ENTRIES_THRESHOLD=50000
10 -
11 -# value in seconds
12 -VITE_HEALTHCHECKS_INTERVAL=120
1 +# Leave this as is if connecting from a remote machine
2 +SERVER_IP=0.0.0.0
3
4 # Connector Credentials
5 # ! SETTING UP YOUR CONNECTORS DEMOs https://www.youtube.com/@taylorwalton_socfortress/videos! #
.gitignore
+7 -3
@@ -1,6 +1,10 @@
1 +#server.key
2 +server.csr
3 +#server.crt
4 +
5 # Logs
6 logs
3 -!src/components/logs
7 +!frontend/src/components/logs
8 *.log
9 npm-debug.log*
10 yarn-debug.log*
@@ -15,8 +19,8 @@ dist-ssr
19 coverage
20 *.local
21
18 -/cypress/videos/
19 -/cypress/screenshots/
22 +cypress/videos/
23 +cypress/screenshots/
24
25 # Editor directories and files
26 .vscode/*
.nvmrc deleted
-1
@@ -1 +0,0 @@
1 -18.17.1
README.md
+6 -3
@@ -1,6 +1,6 @@
1 <h1 align="center">
2
3 -<a href="https://www.socfortress.co"><img src="src/assets/images/socfortress_logo.svg" width="300" height="200"></a>
3 +<a href="https://www.socfortress.co"><img src="frontend/src/assets/images/socfortress_logo.svg" width="300" height="200"></a>
4
5 SOCFortress CoPilot
6
@@ -13,7 +13,7 @@ SOCFortress CoPilot
13
14 [SOCFortress CoPilot](https://www.socfortress.co) focuses on providing a single pane of glass for all your security operations needs. Simplify your open source security stack with a single platform focused on making open source security tools easier to use and more accessible.
15
16 -![demo_timeline](src/assets/images/copilot_gif.gif)
16 +![demo_timeline](frontend/src/assets/images/copilot_gif.gif)
17
18 ## Table of contents
19
@@ -71,11 +71,14 @@ cp .env.example .env
71
72 # Make your changes to the .env file
73
74 +# Build the copilot-frontend image
75 +bash build-dockers.sh
76 +
77 # Run Copilot
78 docker compose up -d
79 ```
80
78 -Copilot shall be available on the host interface, port 5173, protocol HTTP - `http://<your_instance_ip>:5173`.
81 +Copilot shall be available on the host interface, port 443, protocol HTTPS - `https://<your_instance_ip>`.
82 By default, an `admin` account is created. The password is printed in stdout the very first time Copilot is started. It won't be printed anymore after that.
83 `Admin user password` can be searched in the logs of the `copilot` docker to find the password. You will use the `plain` password to login to the web interface.
84
backend/.dockerignore new
+2
@@ -0,0 +1,2 @@
1 +data/copilot.db
2 +copilot.db
backend/Dockerfile renamed
+5 -16
@@ -1,6 +1,7 @@
1 # build with `docker build -t python-backend -f Dockerfile.deb .`
2 # run with `docker run -p 5000:5000 -d python-backend`
3 # Start with the base Debian 11 image
4 +# looking to split into 2 containers, one for the backend and one for the frontend
5 FROM debian:11
6
7 # Set environment variables
@@ -37,27 +38,15 @@ RUN /opt/venv/bin/pip install setuptools
38
39 # Install your application's dependencies
40 WORKDIR /opt/copilot/backend
40 -COPY backend/requirements.txt ./
41 +COPY requirements.txt ./
42 RUN /opt/venv/bin/pip install --no-cache-dir -r requirements.txt
43
44 # Copy your application into the Docker image
44 -WORKDIR /opt/copilot
45 +WORKDIR /opt/copilot/backend
46 COPY . .
47
47 -# Install Node.js and npm
48 -RUN curl -sL https://deb.nodesource.com/setup_18.x | bash -
49 -RUN apt-get install -y nodejs
50 -
51 -# Install concurrently
52 -RUN npm install -g concurrently
53 -
54 -# Install your Vue.js application's dependencies
55 -WORKDIR /opt/copilot
56 -RUN npm install
57 -
48 # Expose ports
59 -EXPOSE 5000 5173
49 +EXPOSE 5000
50
51 # Run your application
62 -#CMD ["sh", "-c", "cd backend && python copilot.py & cd /opt/copilot && npm run dev"]
63 -CMD ["sh", "-c", "cd /opt/copilot && npm run start"]
52 +CMD ["sh", "-c", "ls -la && /opt/venv/bin/python copilot.py"]
backend/app/agents/dfir_iris/services/cases.py
+12 -5
@@ -1,13 +1,16 @@
1 from typing import List
2
3 -from loguru import logger
4 -
3 from app.agents.dfir_iris.schema.cases import AssetCaseIDResponse
4 from app.connectors.dfir_iris.services.assets import get_case_assets
5 from app.connectors.dfir_iris.services.cases import get_all_cases
6 +from loguru import logger
7 +from sqlalchemy.ext.asyncio import AsyncSession
8
9
10 -async def collect_agent_soc_cases(agent_id: int) -> AssetCaseIDResponse:
10 +async def collect_agent_soc_cases(
11 + agent_id: int,
12 + session: AsyncSession,
13 +) -> AssetCaseIDResponse:
14 """
15 Get all cases for the given agent ID.
16
@@ -18,11 +21,15 @@ async def collect_agent_soc_cases(agent_id: int) -> AssetCaseIDResponse:
21 AssetCaseIDResponse: An instance of AssetCaseIDResponse containing the cases for the given agent ID.
22 """
23 logger.info(f"Getting cases for agent: {agent_id}")
21 - all_cases = await get_all_cases()
24 + all_cases = await get_all_cases(session=session)
25 case_ids = await filter_cases_by_agent_id(all_cases, agent_id)
26
27 logger.info(f"Found cases: {case_ids}")
25 - return AssetCaseIDResponse(case_ids=case_ids, success=True, message="Successfully retrieved cases for agent")
28 + return AssetCaseIDResponse(
29 + case_ids=case_ids,
30 + success=True,
31 + message="Successfully retrieved cases for agent",
32 + )
33
34
35 async def filter_cases_by_agent_id(cases, agent_id: int) -> List[int]:
backend/app/agents/routes/agents.py
+127 -47
@@ -1,21 +1,15 @@
1 -from fastapi import APIRouter
2 -from fastapi import BackgroundTasks
3 -from fastapi import Depends
4 -from fastapi import HTTPException
5 -from fastapi import Security
6 -from loguru import logger
7 -from sqlalchemy import delete
8 -from sqlalchemy.ext.asyncio import AsyncSession
9 -from sqlalchemy.future import select
10 -
1 from app.agents.dfir_iris.services.cases import collect_agent_soc_cases
12 -from app.agents.schema.agents import AgentModifyResponse
13 -from app.agents.schema.agents import AgentsResponse
14 -from app.agents.schema.agents import OutdatedVelociraptorAgentsResponse
15 -from app.agents.schema.agents import OutdatedWazuhAgentsResponse
16 -from app.agents.schema.agents import SyncedAgentsResponse
17 -from app.agents.services.status import get_outdated_agents_velociraptor
18 -from app.agents.services.status import get_outdated_agents_wazuh
2 +from app.agents.schema.agents import (
3 + AgentModifyResponse,
4 + AgentsResponse,
5 + OutdatedVelociraptorAgentsResponse,
6 + OutdatedWazuhAgentsResponse,
7 + SyncedAgentsResponse,
8 +)
9 +from app.agents.services.status import (
10 + get_outdated_agents_velociraptor,
11 + get_outdated_agents_wazuh,
12 +)
13 from app.agents.services.sync import sync_agents
14 from app.agents.velociraptor.services.agents import delete_agent_velociraptor
15 from app.agents.wazuh.schema.agents import WazuhAgentVulnerabilitiesResponse
@@ -29,6 +23,11 @@ from app.db.db_session import get_db
23 # App specific imports
24 # from app.db.db_session import session
25 from app.db.universal_models import Agents
26 +from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Security
27 +from loguru import logger
28 +from sqlalchemy import delete
29 +from sqlalchemy.ext.asyncio import AsyncSession
30 +from sqlalchemy.future import select
31
32 agents_router = APIRouter()
33
@@ -57,7 +56,10 @@ async def fetch_velociraptor_id(db: AsyncSession, agent_id: str) -> str:
56 raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
57 except Exception as e:
58 logger.error(f"Failed to fetch agent {agent_id} from database: {e}")
60 - raise HTTPException(status_code=500, detail=f"Failed to fetch agent {agent_id} from database: {e}")
59 + raise HTTPException(
60 + status_code=500,
61 + detail=f"Failed to fetch agent {agent_id} from database: {e}",
62 + )
63
64
65 async def delete_agent_from_database(db: AsyncSession, agent_id: str):
@@ -78,7 +80,10 @@ async def delete_agent_from_database(db: AsyncSession, agent_id: str):
80 except Exception as e:
81 logger.error(f"Failed to delete agent {agent_id} from database: {e}")
82 await db.rollback()
81 - raise HTTPException(status_code=500, detail=f"Failed to delete agent {agent_id} from database: {e}")
83 + raise HTTPException(
84 + status_code=500,
85 + detail=f"Failed to delete agent {agent_id} from database: {e}",
86 + )
87
88
89 @agents_router.get(
@@ -101,7 +106,11 @@ async def get_agents(db: AsyncSession = Depends(get_db)) -> AgentsResponse:
106 try:
107 result = await db.execute(select(Agents))
108 agents = result.scalars().all()
104 - return AgentsResponse(agents=agents, success=True, message="Agents fetched successfully")
109 + return AgentsResponse(
110 + agents=agents,
111 + success=True,
112 + message="Agents fetched successfully",
113 + )
114 except Exception as e:
115 logger.error(f"Failed to fetch agents: {e}")
116 raise HTTPException(status_code=500, detail=f"Failed to fetch agents: {e}")
@@ -113,7 +122,10 @@ async def get_agents(db: AsyncSession = Depends(get_db)) -> AgentsResponse:
122 description="Get agent by agent_id",
123 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
124 )
116 -async def get_agent(agent_id: str, db: AsyncSession = Depends(get_db)) -> AgentsResponse:
125 +async def get_agent(
126 + agent_id: str,
127 + db: AsyncSession = Depends(get_db),
128 +) -> AgentsResponse:
129 """
130 Retrieve an agent by agent_id.
131
@@ -132,12 +144,24 @@ async def get_agent(agent_id: str, db: AsyncSession = Depends(get_db)) -> Agents
144 result = await db.execute(select(Agents).filter(Agents.agent_id == agent_id))
145 agent = result.scalars().first()
146 if agent:
135 - return AgentsResponse(agents=[agent], success=True, message="Agent fetched successfully")
147 + return AgentsResponse(
148 + agents=[agent],
149 + success=True,
150 + message="Agent fetched successfully",
151 + )
152 else:
137 - raise HTTPException(status_code=404, detail=f"Agent with agent_id {agent_id} not found")
153 + raise HTTPException(
154 + status_code=404,
155 + detail=f"Agent with agent_id {agent_id} not found",
156 + )
157 except Exception as e:
139 - logger.error(f"Failed to fetch agent: {agent_id} with error {e}. Does it exist?")
140 - raise HTTPException(status_code=500, detail=f"Failed to fetch agent: {agent_id}. Does it exist?")
158 + logger.error(
159 + f"Failed to fetch agent: {agent_id} with error {e}. Does it exist?",
160 + )
161 + raise HTTPException(
162 + status_code=500,
163 + detail=f"Failed to fetch agent: {agent_id}. Does it exist?",
164 + )
165
166
167 @agents_router.get(
@@ -146,7 +170,10 @@ async def get_agent(agent_id: str, db: AsyncSession = Depends(get_db)) -> Agents
170 description="Get agent by hostname",
171 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
172 )
149 -async def get_agent_by_hostname(hostname: str, db: AsyncSession = Depends(get_db)) -> AgentsResponse:
173 +async def get_agent_by_hostname(
174 + hostname: str,
175 + db: AsyncSession = Depends(get_db),
176 +) -> AgentsResponse:
177 """
178 Retrieve an agent by its hostname.
179
@@ -165,9 +192,16 @@ async def get_agent_by_hostname(hostname: str, db: AsyncSession = Depends(get_db
192 result = await db.execute(select(Agents).filter(Agents.hostname == hostname))
193 agent = result.scalars().first()
194 if agent:
168 - return AgentsResponse(agents=[agent], success=True, message="Agent fetched successfully")
195 + return AgentsResponse(
196 + agents=[agent],
197 + success=True,
198 + message="Agent fetched successfully",
199 + )
200 else:
170 - raise HTTPException(status_code=404, detail=f"Agent with hostname {hostname} not found")
201 + raise HTTPException(
202 + status_code=404,
203 + detail=f"Agent with hostname {hostname} not found",
204 + )
205 except Exception as e:
206 logger.error(f"Failed to fetch agent: {e}")
207 # The exception message should not be exposed directly, especially in production
@@ -178,9 +212,14 @@ async def get_agent_by_hostname(hostname: str, db: AsyncSession = Depends(get_db
212 "/sync",
213 response_model=SyncedAgentsResponse,
214 description="Sync agents from Wazuh Manager",
181 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst", "scheduler"))],
215 + dependencies=[
216 + Security(AuthHandler().require_any_scope("admin", "analyst", "scheduler")),
217 + ],
218 )
183 -async def sync_all_agents(backgroud_tasks: BackgroundTasks, session: AsyncSession = Depends(get_db)) -> SyncedAgentsResponse:
219 +async def sync_all_agents(
220 + backgroud_tasks: BackgroundTasks,
221 + session: AsyncSession = Depends(get_db),
222 +) -> SyncedAgentsResponse:
223 """
224 Sync all agents from Wazuh Manager.
225
@@ -197,7 +236,10 @@ async def sync_all_agents(backgroud_tasks: BackgroundTasks, session: AsyncSessio
236 """
237 logger.info("Syncing agents from Wazuh Manager")
238 backgroud_tasks.add_task(sync_agents, session)
200 - return SyncedAgentsResponse(success=True, message="Agents synced started successfully")
239 + return SyncedAgentsResponse(
240 + success=True,
241 + message="Agents synced started successfully",
242 + )
243
244
245 @agents_router.post(
@@ -206,7 +248,10 @@ async def sync_all_agents(backgroud_tasks: BackgroundTasks, session: AsyncSessio
248 description="Mark agent as critical",
249 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
250 )
209 -async def mark_agent_as_critical(agent_id: str, session: AsyncSession = Depends(get_db)) -> AgentModifyResponse:
251 +async def mark_agent_as_critical(
252 + agent_id: str,
253 + session: AsyncSession = Depends(get_db),
254 +) -> AgentModifyResponse:
255 """
256 Marks the specified agent as critical.
257
@@ -220,19 +265,30 @@ async def mark_agent_as_critical(agent_id: str, session: AsyncSession = Depends(
265 logger.info(f"Marking agent {agent_id} as critical")
266 try:
267 # Asynchronously fetch the agent by id
223 - result = await session.execute(select(Agents).filter(Agents.agent_id == agent_id))
268 + result = await session.execute(
269 + select(Agents).filter(Agents.agent_id == agent_id),
270 + )
271 agent = result.scalars().first()
272
273 if not agent:
227 - raise HTTPException(status_code=404, detail=f"Agent with agent_id {agent_id} not found")
274 + raise HTTPException(
275 + status_code=404,
276 + detail=f"Agent with agent_id {agent_id} not found",
277 + )
278
279 agent.critical_asset = True
280 await session.commit()
281
232 - return AgentModifyResponse(success=True, message=f"Agent {agent_id} marked as critical: {True}")
282 + return AgentModifyResponse(
283 + success=True,
284 + message=f"Agent {agent_id} marked as critical: {True}",
285 + )
286 except Exception as e:
287 session.rollback() # Roll back the session in case of error
235 - raise HTTPException(status_code=500, detail=f"Failed to mark agent as critical: {str(e)}")
288 + raise HTTPException(
289 + status_code=500,
290 + detail=f"Failed to mark agent as critical: {str(e)}",
291 + )
292
293
294 @agents_router.post(
@@ -241,7 +297,10 @@ async def mark_agent_as_critical(agent_id: str, session: AsyncSession = Depends(
297 description="Mark agent as not critical",
298 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
299 )
244 -async def mark_agent_as_not_critical(agent_id: str, session: AsyncSession = Depends(get_db)) -> AgentModifyResponse:
300 +async def mark_agent_as_not_critical(
301 + agent_id: str,
302 + session: AsyncSession = Depends(get_db),
303 +) -> AgentModifyResponse:
304 """
305 Marks the specified agent as not critical.
306
@@ -257,19 +316,30 @@ async def mark_agent_as_not_critical(agent_id: str, session: AsyncSession = Depe
316 """
317 logger.info(f"Marking agent {agent_id} as not critical")
318 try:
260 - result = await session.execute(select(Agents).filter(Agents.agent_id == agent_id))
319 + result = await session.execute(
320 + select(Agents).filter(Agents.agent_id == agent_id),
321 + )
322 agent = result.scalars().first()
323
324 if not agent:
264 - raise HTTPException(status_code=404, detail=f"Agent with agent_id {agent_id} not found")
325 + raise HTTPException(
326 + status_code=404,
327 + detail=f"Agent with agent_id {agent_id} not found",
328 + )
329
330 agent.critical_asset = False
331 await session.commit()
332
269 - return AgentModifyResponse(success=True, message=f"Agent {agent_id} marked as not critical")
333 + return AgentModifyResponse(
334 + success=True,
335 + message=f"Agent {agent_id} marked as not critical",
336 + )
337 except Exception as e:
338 await session.rollback() # Roll back the session in case of error
272 - raise HTTPException(status_code=500, detail=f"Failed to mark agent as not critical: {str(e)}")
339 + raise HTTPException(
340 + status_code=500,
341 + detail=f"Failed to mark agent as not critical: {str(e)}",
342 + )
343
344
345 @agents_router.get(
@@ -298,7 +368,7 @@ async def get_agent_vulnerabilities(agent_id: str) -> WazuhAgentVulnerabilitiesR
368 description="Get SOC cases for agent",
369 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
370 )
301 -async def get_agent_soc_cases(agent_id: str):
371 +async def get_agent_soc_cases(agent_id: str, session: AsyncSession = Depends(get_db)):
372 """
373 Fetches the SOC cases of a specific agent.
374
@@ -309,7 +379,7 @@ async def get_agent_soc_cases(agent_id: str):
379 SocCasesResponse: The response containing the agent SOC cases.
380 """
381 logger.info(f"Fetching agent {agent_id} SOC cases")
312 - return await collect_agent_soc_cases(agent_id)
382 + return await collect_agent_soc_cases(agent_id, session)
383
384
385 @agents_router.get(
@@ -318,7 +388,9 @@ async def get_agent_soc_cases(agent_id: str):
388 description="Get all outdated Wazuh agents",
389 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
390 )
321 -async def get_outdated_wazuh_agents(session: AsyncSession = Depends(get_db)) -> OutdatedWazuhAgentsResponse:
391 +async def get_outdated_wazuh_agents(
392 + session: AsyncSession = Depends(get_db),
393 +) -> OutdatedWazuhAgentsResponse:
394 """
395 Retrieve all outdated Wazuh agents.
396
@@ -337,7 +409,9 @@ async def get_outdated_wazuh_agents(session: AsyncSession = Depends(get_db)) ->
409 description="Get all outdated Velociraptor agents",
410 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
411 )
340 -async def get_outdated_velociraptor_agents(session: AsyncSession = Depends(get_db)) -> OutdatedVelociraptorAgentsResponse:
412 +async def get_outdated_velociraptor_agents(
413 + session: AsyncSession = Depends(get_db),
414 +) -> OutdatedVelociraptorAgentsResponse:
415 """
416 Fetches all outdated Velociraptor agents.
417
@@ -358,7 +432,10 @@ async def get_outdated_velociraptor_agents(session: AsyncSession = Depends(get_d
432 description="Delete agent",
433 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
434 )
361 -async def delete_agent(agent_id: str, session: AsyncSession = Depends(get_db)) -> AgentModifyResponse:
435 +async def delete_agent(
436 + agent_id: str,
437 + session: AsyncSession = Depends(get_db),
438 +) -> AgentModifyResponse:
439 """
440 Delete an agent.
441
@@ -376,7 +453,10 @@ async def delete_agent(agent_id: str, session: AsyncSession = Depends(get_db)) -
453 if client_id != "n/a":
454 await delete_agent_velociraptor(client_id)
455 await delete_agent_from_database(db=session, agent_id=agent_id)
379 - return AgentModifyResponse(success=True, message=f"Agent {agent_id} deleted successfully")
456 + return AgentModifyResponse(
457 + success=True,
458 + message=f"Agent {agent_id} deleted successfully",
459 + )
460
461
462 # ! TODO: CURRENTLY UPDATES IN THE DB BUT NEED TO UPDATE IN WAZUH # !
backend/app/agents/schema/agents.py
+1 -3
@@ -1,11 +1,9 @@
1 from typing import List
2
3 -from pydantic import BaseModel
4 -from pydantic import Field
5 -
3 from app.agents.velociraptor.schema.agents import VelociraptorAgent
4 from app.agents.wazuh.schema.agents import WazuhAgent
5 from app.db.universal_models import Agents
6 +from pydantic import BaseModel, Field
7
8
9 class AgentsResponse(BaseModel):
backend/app/agents/services/modify.py
+9 -4
@@ -1,8 +1,7 @@
1 -from fastapi import HTTPException
2 -
1 import app.agents.wazuh.services.agents as wazuh_services
2 from app.db.db_session import session
3 from app.db.universal_models import Agents
4 +from fastapi import HTTPException
5
6
7 def delete_agent_db(agent_id: str):
@@ -17,7 +16,10 @@ def delete_agent_db(agent_id: str):
16 """
17 agent = session.query(Agents).filter(Agents.agent_id == agent_id).first()
18 if not agent:
20 - raise HTTPException(status_code=404, detail=f"Agent with agent_id {agent_id} not found")
19 + raise HTTPException(
20 + status_code=404,
21 + detail=f"Agent with agent_id {agent_id} not found",
22 + )
23 session.delete(agent)
24 session.commit()
25 return {"success": True, "message": f"Agent {agent_id} deleted from database"}
@@ -40,4 +42,7 @@ def delete_agent_wazuh(agent_id: str):
42 wazuh_services.delete_agent(agent_id)
43 return {"success": True, "message": f"Agent {agent_id} deleted from Wazuh"}
44 except Exception as e:
43 - raise HTTPException(status_code=500, detail=f"Failed to delete agent {agent_id} from Wazuh: {e}")
45 + raise HTTPException(
46 + status_code=500,
47 + detail=f"Failed to delete agent {agent_id} from Wazuh: {e}",
48 + )
backend/app/agents/services/status.py
+44 -16
@@ -1,16 +1,17 @@
1 from typing import List
2
3 +from app.agents.schema.agents import (
4 + OutdatedVelociraptorAgentsResponse,
5 + OutdatedWazuhAgentsResponse,
6 +)
7 +from app.connectors.velociraptor.utils.universal import UniversalService
8 +from app.db.db_session import session
9 +from app.db.universal_models import Agents
10 from fastapi import HTTPException
11 from loguru import logger
12 from sqlalchemy.ext.asyncio import AsyncSession
13 from sqlalchemy.future import select
14
8 -from app.agents.schema.agents import OutdatedVelociraptorAgentsResponse
9 -from app.agents.schema.agents import OutdatedWazuhAgentsResponse
10 -from app.connectors.velociraptor.utils.universal import UniversalService
11 -from app.db.db_session import session
12 -from app.db.universal_models import Agents
13 -
15
16 def get_agent(agent_id: str) -> List[Agents]:
17 """
@@ -26,10 +27,15 @@ def get_agent(agent_id: str) -> List[Agents]:
27 return session.query(Agents).filter(Agents.agent_id == agent_id).first()
28 except Exception as e:
29 logger.error(f"Failed to fetch agent with agent_id {agent_id}: {e}")
29 - raise HTTPException(status_code=500, detail=f"Failed to fetch agent with agent_id {agent_id}: {e}")
30 + raise HTTPException(
31 + status_code=500,
32 + detail=f"Failed to fetch agent with agent_id {agent_id}: {e}",
33 + )
34
35
32 -async def get_outdated_agents_wazuh(session: AsyncSession) -> OutdatedWazuhAgentsResponse:
36 +async def get_outdated_agents_wazuh(
37 + session: AsyncSession,
38 +) -> OutdatedWazuhAgentsResponse:
39 """
40 Retrieves all agents with outdated Wazuh agent versions from the database asynchronously.
41
@@ -40,15 +46,23 @@ async def get_outdated_agents_wazuh(session: AsyncSession) -> OutdatedWazuhAgent
46 OutdatedWazuhAgentsResponse: Response object containing the outdated agents.
47 """
48 try:
43 - wazuh_manager_result = await session.execute(select(Agents).filter(Agents.agent_id == "000"))
49 + wazuh_manager_result = await session.execute(
50 + select(Agents).filter(Agents.agent_id == "000"),
51 + )
52 wazuh_manager = wazuh_manager_result.scalars().first()
53
54 if wazuh_manager is None:
55 logger.error("Wazuh Manager with agent_id '000' not found.")
48 - raise HTTPException(status_code=404, detail="Wazuh Manager with agent_id '000' not found.")
56 + raise HTTPException(
57 + status_code=404,
58 + detail="Wazuh Manager with agent_id '000' not found.",
59 + )
60
61 outdated_agents_result = await session.execute(
51 - select(Agents).filter(Agents.agent_id != "000", Agents.wazuh_agent_version != wazuh_manager.wazuh_agent_version),
62 + select(Agents).filter(
63 + Agents.agent_id != "000",
64 + Agents.wazuh_agent_version != wazuh_manager.wazuh_agent_version,
65 + ),
66 )
67 outdated_wazuh_agents = outdated_agents_result.scalars().all()
68
@@ -58,10 +72,15 @@ async def get_outdated_agents_wazuh(session: AsyncSession) -> OutdatedWazuhAgent
72 outdated_wazuh_agents=outdated_wazuh_agents,
73 )
74 except Exception as e:
61 - raise HTTPException(status_code=500, detail=f"Failed to fetch outdated Wazuh agents: {e}")
75 + raise HTTPException(
76 + status_code=500,
77 + detail=f"Failed to fetch outdated Wazuh agents: {e}",
78 + )
79
80
64 -async def get_outdated_agents_velociraptor(session: AsyncSession) -> OutdatedVelociraptorAgentsResponse:
81 +async def get_outdated_agents_velociraptor(
82 + session: AsyncSession,
83 +) -> OutdatedVelociraptorAgentsResponse:
84 """
85 Retrieves all agents with outdated Velociraptor client versions from the database asynchronously.
86
@@ -76,10 +95,16 @@ async def get_outdated_agents_velociraptor(session: AsyncSession) -> OutdatedVel
95
96 try:
97 # Assuming _get_server_version is an async function
79 - server_version = await velociraptor_service._get_server_version(vql_server_version)
98 + server_version = await velociraptor_service._get_server_version(
99 + vql_server_version,
100 + )
101 agents_result = await session.execute(select(Agents))
102 agents = agents_result.scalars().all()
82 - outdated_velociraptor_agents = [agent for agent in agents if agent.velociraptor_agent_version != server_version]
103 + outdated_velociraptor_agents = [
104 + agent
105 + for agent in agents
106 + if agent.velociraptor_agent_version != server_version
107 + ]
108
109 return OutdatedVelociraptorAgentsResponse(
110 message="Outdated Velociraptor agents fetched successfully.",
@@ -87,4 +112,7 @@ async def get_outdated_agents_velociraptor(session: AsyncSession) -> OutdatedVel
112 outdated_velociraptor_agents=outdated_velociraptor_agents,
113 )
114 except Exception as e:
90 - raise HTTPException(status_code=500, detail=f"Failed to fetch outdated Velociraptor agents: {e}")
115 + raise HTTPException(
116 + status_code=500,
117 + detail=f"Failed to fetch outdated Velociraptor agents: {e}",
118 + )
backend/app/agents/services/sync.py
+38 -15
@@ -1,18 +1,15 @@
1 from typing import List
2
3 -from loguru import logger
4 -from sqlalchemy.ext.asyncio import AsyncSession
5 -from sqlalchemy.future import select
6 -
3 import app.agents.velociraptor.services.agents as velociraptor_services
4 import app.agents.wazuh.services.agents as wazuh_services
9 -from app.agents.schema.agents import SyncedAgent
10 -from app.agents.schema.agents import SyncedAgentsResponse
5 +from app.agents.schema.agents import SyncedAgent, SyncedAgentsResponse
6 from app.agents.velociraptor.schema.agents import VelociraptorAgent
12 -from app.agents.wazuh.schema.agents import WazuhAgent
13 -from app.agents.wazuh.schema.agents import WazuhAgentsList
7 +from app.agents.wazuh.schema.agents import WazuhAgent, WazuhAgentsList
8 from app.connectors.models import Connectors
9 from app.db.universal_models import Agents
10 +from loguru import logger
11 +from sqlalchemy.ext.asyncio import AsyncSession
12 +from sqlalchemy.future import select
13
14
15 async def fetch_wazuh_agents() -> WazuhAgentsList:
@@ -47,7 +44,12 @@ async def fetch_velociraptor_agent(agent_name: str) -> VelociraptorAgent:
44 return await velociraptor_services.collect_velociraptor_agent(agent_name)
45
46
50 -async def add_agent_to_db(session: AsyncSession, agent: WazuhAgent, client: VelociraptorAgent, customer_code: str):
47 +async def add_agent_to_db(
48 + session: AsyncSession,
49 + agent: WazuhAgent,
50 + client: VelociraptorAgent,
51 + customer_code: str,
52 +):
53 """Add new agent to database.
54
55 Args:
@@ -114,7 +116,9 @@ async def get_velociraptor_connector(session):
116 Returns:
117 The first result of the query as a scalar value.
118 """
117 - connector_query = select(Connectors).filter(Connectors.connector_name == "Velociraptor")
119 + connector_query = select(Connectors).filter(
120 + Connectors.connector_name == "Velociraptor",
121 + )
122 result = await session.execute(connector_query)
123 return result.scalars().first()
124
@@ -187,24 +191,43 @@ async def sync_agents(session: AsyncSession) -> SyncedAgentsResponse:
191 try:
192 velociraptor_agent = await process_velociraptor_agent(session, wazuh_agent)
193 except Exception as e:
190 - logger.error(f"Failed to collect Velociraptor Agent for {wazuh_agent.agent_name}: {e}")
194 + logger.error(
195 + f"Failed to collect Velociraptor Agent for {wazuh_agent.agent_name}: {e}",
196 + )
197 continue
198
199 customer_code = extract_customer_code(wazuh_agent.agent_label)
200
201 # Asynchronously fetch the existing agent
196 - existing_agent_query = select(Agents).filter(Agents.hostname == wazuh_agent.agent_name)
202 + existing_agent_query = select(Agents).filter(
203 + Agents.hostname == wazuh_agent.agent_name,
204 + )
205 result = await session.execute(existing_agent_query)
206 existing_agent = result.scalars().first()
207
208 if existing_agent:
201 - await update_agent_in_db(session, existing_agent, wazuh_agent, velociraptor_agent, customer_code)
209 + await update_agent_in_db(
210 + session,
211 + existing_agent,
212 + wazuh_agent,
213 + velociraptor_agent,
214 + customer_code,
215 + )
216 else:
203 - await add_agent_to_db(session, wazuh_agent, velociraptor_agent, customer_code)
217 + await add_agent_to_db(
218 + session,
219 + wazuh_agent,
220 + velociraptor_agent,
221 + customer_code,
222 + )
223
224 # Combine the wazuh agent and velociraptor agent into one object
225 synced_agent = SyncedAgent(**wazuh_agent.dict(), **velociraptor_agent.dict())
226 agents_added_list.append(synced_agent)
227
228 logger.info(f"Agents Added List: {agents_added_list}")
210 - return SyncedAgentsResponse(success=True, message="Agents synced successfully", agents_added=agents_added_list)
229 + return SyncedAgentsResponse(
230 + success=True,
231 + message="Agents synced successfully",
232 + agents_added=agents_added_list,
233 + )
backend/app/agents/velociraptor/schema/agents.py
+1 -2
@@ -1,8 +1,7 @@
1 from datetime import datetime
2 from typing import Optional
3
4 -from pydantic import BaseModel
5 -from pydantic import Field
4 +from pydantic import BaseModel, Field
5
6
7 class VelociraptorAgent(BaseModel):
backend/app/agents/velociraptor/services/agents.py
+51 -16
@@ -1,11 +1,10 @@
1 from datetime import datetime
2
3 -from fastapi import HTTPException
4 -from loguru import logger
5 -
3 from app.agents.schema.agents import AgentModifyResponse
4 from app.agents.velociraptor.schema.agents import VelociraptorAgent
5 from app.connectors.velociraptor.utils.universal import UniversalService
6 +from fastapi import HTTPException
7 +from loguru import logger
8
9
10 def create_query(query: str) -> str:
@@ -39,29 +38,45 @@ async def collect_velociraptor_agent(agent_name: str) -> VelociraptorAgent:
38 client_id = client_id["results"][0]["client_id"]
39 except (KeyError, IndexError, TypeError) as e:
40 logger.error(f"Failed to get client ID for {agent_name}. Error: {e}")
42 - return VelociraptorAgent(client_id="Unknown", client_last_seen="Unknown", client_version="Unknown")
41 + return VelociraptorAgent(
42 + client_id="Unknown",
43 + client_last_seen="Unknown",
44 + client_version="Unknown",
45 + )
46
47 try:
45 - vql_last_seen_at = f"select last_seen_at from clients(search='host:{agent_name}')"
46 - last_seen_at = await velociraptor_service._get_last_seen_timestamp(vql_last_seen_at)
48 + vql_last_seen_at = (
49 + f"select last_seen_at from clients(search='host:{agent_name}')"
50 + )
51 + last_seen_at = await velociraptor_service._get_last_seen_timestamp(
52 + vql_last_seen_at,
53 + )
54 client_last_seen = datetime.fromtimestamp(
55 int(last_seen_at) / 1000000,
56 ).strftime(
57 "%Y-%m-%dT%H:%M:%S+00:00",
58 ) # Converting to string format
59 except Exception as e:
53 - logger.error(f"Failed to get or convert last seen at for {agent_name}. Error: {e}")
60 + logger.error(
61 + f"Failed to get or convert last seen at for {agent_name}. Error: {e}",
62 + )
63 client_last_seen = "1970-01-01T00:00:00+00:00"
64
65 try:
66 vql_client_version = f"select * from clients(search='host:{agent_name}')"
67 # client_version = UniversalService()._get_client_version(vql_client_version)
59 - client_version = await velociraptor_service._get_client_version(vql_client_version)
68 + client_version = await velociraptor_service._get_client_version(
69 + vql_client_version,
70 + )
71 except Exception as e:
72 logger.error(f"Failed to get client version for {agent_name}. Error: {e}")
73 client_version = "Unknown"
74
64 - return VelociraptorAgent(client_id=client_id, client_last_seen=client_last_seen, client_version=client_version)
75 + return VelociraptorAgent(
76 + client_id=client_id,
77 + client_last_seen=client_last_seen,
78 + client_version=client_version,
79 + )
80
81
82 def execute_query(universal_service, query: str) -> dict:
@@ -96,10 +111,16 @@ def check_flow_success(flow: dict, client_id: str) -> dict:
111 """
112 if flow["success"]:
113 logger.info(f"Successfully deleted velociraptor client {client_id}")
99 - return {"message": f"Successfully deleted velociraptor client {client_id}", "success": True}
114 + return {
115 + "message": f"Successfully deleted velociraptor client {client_id}",
116 + "success": True,
117 + }
118 else:
119 logger.error(f"Failed to delete velociraptor client {client_id}")
102 - return handle_exception(e="Failed to delete velociraptor client", client_id=client_id)
120 + return handle_exception(
121 + e="Failed to delete velociraptor client",
122 + client_id=client_id,
123 + )
124
125
126 def check_client_in_results(results: dict, client_id: str) -> dict:
@@ -115,12 +136,18 @@ def check_client_in_results(results: dict, client_id: str) -> dict:
136 """
137 if results["results"] == []:
138 logger.info(f"Successfully deleted velociraptor client {client_id}")
118 - return {"message": f"Successfully deleted velociraptor client {client_id}", "success": True}
139 + return {
140 + "message": f"Successfully deleted velociraptor client {client_id}",
141 + "success": True,
142 + }
143
144 for result in results["results"]:
145 if result["client_id"] == client_id:
146 logger.error(f"Failed to delete velociraptor client {client_id}")
123 - return handle_exception(e="Failed to delete velociraptor client", client_id=client_id)
147 + return handle_exception(
148 + e="Failed to delete velociraptor client",
149 + client_id=client_id,
150 + )
151
152
153 def handle_exception(e: Exception, client_id: str) -> dict:
@@ -195,15 +222,23 @@ async def ensure_client_deleted(client_id: str) -> dict:
222 """
223 universal_service = UniversalService()
224 try:
198 - query = create_query("SELECT collect_client(client_id='server', artifacts=['Server.Information.Clients'], env=dict()) FROM scope()")
225 + query = create_query(
226 + "SELECT collect_client(client_id='server', artifacts=['Server.Information.Clients'], env=dict()) FROM scope()",
227 + )
228 flow = execute_query(universal_service, query)
229 flow_id = (
230 flow.get("results")[0]
202 - .get("collect_client(client_id='server', artifacts=['Server.Information.Clients'], env=dict())")
231 + .get(
232 + "collect_client(client_id='server', artifacts=['Server.Information.Clients'], env=dict())",
233 + )
234 .get("flow_id")
235 )
236
206 - results = universal_service.read_collection_results(client_id=client_id, flow_id=flow_id, artifact="Server.Information.Clients")
237 + results = universal_service.read_collection_results(
238 + client_id=client_id,
239 + flow_id=flow_id,
240 + artifact="Server.Information.Clients",
241 + )
242 return check_client_in_results(results, client_id)
243 except Exception as e:
244 return handle_exception(e, client_id)
backend/app/agents/velociraptor/utils/universal.py
+3 -1
@@ -16,5 +16,7 @@ def parse_date(date_string: str) -> datetime:
16 try:
17 return datetime.strptime(date_string, "%Y-%m-%dT%H:%M:%S+00:00")
18 except ValueError:
19 - logger.info(f"Invalid format for date: {date_string}. Using the epoch time as default.")
19 + logger.info(
20 + f"Invalid format for date: {date_string}. Using the epoch time as default.",
21 + )
22 return datetime.strptime("1970-01-01T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S+00:00")
backend/app/agents/wazuh/schema/agents.py
+2 -4
@@ -1,9 +1,7 @@
1 from datetime import datetime
2 -from typing import List
3 -from typing import Optional
2 +from typing import List, Optional
3
5 -from pydantic import BaseModel
6 -from pydantic import Field
4 +from pydantic import BaseModel, Field
5
6
7 class WazuhAgent(BaseModel):
backend/app/agents/wazuh/services/agents.py
+24 -10
@@ -1,12 +1,12 @@
1 +from app.agents.schema.agents import AgentModifyResponse
2 +from app.agents.wazuh.schema.agents import WazuhAgent, WazuhAgentsList
3 +from app.connectors.wazuh_manager.utils.universal import (
4 + send_delete_request,
5 + send_get_request,
6 +)
7 from fastapi import HTTPException
8 from loguru import logger
9
4 -from app.agents.schema.agents import AgentModifyResponse
5 -from app.agents.wazuh.schema.agents import WazuhAgent
6 -from app.agents.wazuh.schema.agents import WazuhAgentsList
7 -from app.connectors.wazuh_manager.utils.universal import send_delete_request
8 -from app.connectors.wazuh_manager.utils.universal import send_get_request
9 -
10
11 async def collect_wazuh_agents() -> WazuhAgentsList:
12 """
@@ -16,7 +16,10 @@ async def collect_wazuh_agents() -> WazuhAgentsList:
16 WazuhAgentsList: A list of WazuhAgent objects representing the collected agents.
17 """
18 logger.info("Collecting all agents from Wazuh Manager")
19 - agents_collected = await send_get_request(endpoint="/agents", params={"limit": 1000})
19 + agents_collected = await send_get_request(
20 + endpoint="/agents",
21 + params={"limit": 1000},
22 + )
23
24 if agents_collected.get("success") is False:
25 raise HTTPException(
@@ -26,7 +29,11 @@ async def collect_wazuh_agents() -> WazuhAgentsList:
29 try:
30 if agents_collected.get("success"):
31 wazuh_agents_list = []
29 - for agent in agents_collected.get("data", {}).get("data", {}).get("affected_items", []):
32 + for agent in (
33 + agents_collected.get("data", {})
34 + .get("data", {})
35 + .get("affected_items", [])
36 + ):
37 os_name = agent.get("os", {}).get("name", "Unknown")
38 last_keep_alive = agent.get("lastKeepAlive", "Unknown")
39 agent_group_list = agent.get("group", [])
@@ -43,7 +50,11 @@ async def collect_wazuh_agents() -> WazuhAgentsList:
50 )
51 wazuh_agents_list.append(wazuh_agent)
52
46 - return WazuhAgentsList(agents=wazuh_agents_list, success=True, message="Agents collected successfully")
53 + return WazuhAgentsList(
54 + agents=wazuh_agents_list,
55 + success=True,
56 + message="Agents collected successfully",
57 + )
58
59 except (KeyError, IndexError, HTTPException) as e:
60 # Handle or log the error as needed
@@ -116,4 +127,7 @@ async def delete_agent_wazuh(agent_id: str) -> AgentModifyResponse:
127
128 except Exception as e:
129 # * Catch-all for other exceptions
119 - raise HTTPException(status_code=500, detail=f"Failed to delete agent {agent_id} from Wazuh Manager: {e}")
130 + raise HTTPException(
131 + status_code=500,
132 + detail=f"Failed to delete agent {agent_id} from Wazuh Manager: {e}",
133 + )
backend/app/agents/wazuh/services/vulnerabilities.py
+22 -9
@@ -1,12 +1,13 @@
1 from typing import List
2
3 +from app.agents.wazuh.schema.agents import (
4 + WazuhAgentVulnerabilities,
5 + WazuhAgentVulnerabilitiesResponse,
6 +)
7 +from app.connectors.wazuh_manager.utils.universal import send_get_request
8 from fastapi import HTTPException
9 from loguru import logger
10
6 -from app.agents.wazuh.schema.agents import WazuhAgentVulnerabilities
7 -from app.agents.wazuh.schema.agents import WazuhAgentVulnerabilitiesResponse
8 -from app.connectors.wazuh_manager.utils.universal import send_get_request
9 -
11
12 async def collect_agent_vulnerabilities(agent_id: str):
13 """
@@ -22,11 +23,15 @@ async def collect_agent_vulnerabilities(agent_id: str):
23 HTTPException: If there is an error collecting the vulnerabilities.
24 """
25 logger.info(f"Collecting agent {agent_id} vulnerabilities from Wazuh Manager")
25 - agent_vulnerabilities = await send_get_request(endpoint=f"/vulnerability/{agent_id}")
26 + agent_vulnerabilities = await send_get_request(
27 + endpoint=f"/vulnerability/{agent_id}",
28 + )
29 if agent_vulnerabilities["success"] is False:
30 raise HTTPException(status_code=500, detail=agent_vulnerabilities["message"])
31
29 - processed_vulnerabilities = process_agent_vulnerabilities(agent_vulnerabilities["data"])
32 + processed_vulnerabilities = process_agent_vulnerabilities(
33 + agent_vulnerabilities["data"],
34 + )
35 return WazuhAgentVulnerabilitiesResponse(
36 vulnerabilities=processed_vulnerabilities,
37 success=True,
@@ -34,7 +39,9 @@ async def collect_agent_vulnerabilities(agent_id: str):
39 )
40
41
37 -def process_agent_vulnerabilities(agent_vulnerabilities: dict) -> List[WazuhAgentVulnerabilities]:
42 +def process_agent_vulnerabilities(
43 + agent_vulnerabilities: dict,
44 +) -> List[WazuhAgentVulnerabilities]:
45 """
46 Process agent vulnerabilities and return a list of WazuhAgentVulnerabilities objects.
47
@@ -48,7 +55,13 @@ def process_agent_vulnerabilities(agent_vulnerabilities: dict) -> List[WazuhAgen
55 HTTPException: If there is an error processing the agent vulnerabilities.
56 """
57 try:
51 - vulnerabilities = agent_vulnerabilities.get("data", {}).get("affected_items", [])
58 + vulnerabilities = agent_vulnerabilities.get("data", {}).get(
59 + "affected_items",
60 + [],
61 + )
62 return [WazuhAgentVulnerabilities(**vuln) for vuln in vulnerabilities]
63 except Exception as e:
54 - raise HTTPException(status_code=500, detail=f"Failed to process agent vulnerabilities: {e}")
64 + raise HTTPException(
65 + status_code=500,
66 + detail=f"Failed to process agent vulnerabilities: {e}",
67 + )
backend/app/auth/models/users.py
+28 -11
@@ -6,12 +6,8 @@ from enum import Enum
6 from typing import Optional
7
8 import bcrypt
9 -from pydantic import BaseModel
10 -from pydantic import EmailStr
11 -from pydantic import validator
12 -from sqlmodel import Field
13 -from sqlmodel import Relationship
14 -from sqlmodel import SQLModel
9 +from pydantic import BaseModel, EmailStr, validator
10 +from sqlmodel import Field, Relationship, SQLModel
11
12
13 class Role(SQLModel, table=True):
@@ -50,7 +46,11 @@ class UserInput(SQLModel):
46 description="Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one special character",
47 )
48 email: EmailStr
53 - role_id: RoleEnum = Field(RoleEnum.analyst, description="Role ID 1: admin, 2: analyst", foreign_key="role.id")
49 + role_id: RoleEnum = Field(
50 + RoleEnum.analyst,
51 + description="Role ID 1: admin, 2: analyst",
52 + foreign_key="role.id",
53 + )
54
55
56 class UserLogin(SQLModel):
@@ -84,7 +84,12 @@ class SMTPInput(SQLModel):
84
85
86 class Password(BaseModel):
87 - length: int = Field(default=12, ge=8, le=128, description="The length of the password")
87 + length: int = Field(
88 + default=12,
89 + ge=8,
90 + le=128,
91 + description="The length of the password",
92 + )
93 hashed: str # Holds the hashed password
94 plain: str # Holds the plain password
95
@@ -106,11 +111,19 @@ class Password(BaseModel):
111 punctuation = string.punctuation
112
113 # Ensure the password has at least one lowercase, one uppercase, one digit, and one symbol
109 - password_chars = [random.choice(lowercase), random.choice(uppercase), random.choice(digits), random.choice(punctuation)]
114 + password_chars = [
115 + random.choice(lowercase),
116 + random.choice(uppercase),
117 + random.choice(digits),
118 + random.choice(punctuation),
119 + ]
120
121 # Fill the rest of the password length with a random mix of characters
122 if length > 4:
113 - password_chars += random.choices(lowercase + uppercase + digits + punctuation, k=length - 4)
123 + password_chars += random.choices(
124 + lowercase + uppercase + digits + punctuation,
125 + k=length - 4,
126 + )
127
128 # Shuffle the resulting password list to avoid predictable patterns
129 random.shuffle(password_chars)
@@ -122,7 +135,11 @@ class Password(BaseModel):
135 hashed_password = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
136
137 # Return the Password object with both the plain and hashed password
125 - return cls(length=length, hashed=hashed_password.decode("utf-8"), plain=password)
138 + return cls(
139 + length=length,
140 + hashed=hashed_password.decode("utf-8"),
141 + plain=password,
142 + )
143
144
145 # ! PASSWORD RESET TOKEN GENERATION NOT USING FOR NOW! #
backend/app/auth/routes/auth.py
+61 -28
@@ -1,27 +1,21 @@
1 from datetime import timedelta
2
3 -from fastapi import APIRouter
4 -from fastapi import Depends
5 -from fastapi import HTTPException
6 -from fastapi import Security
7 -from fastapi import status
8 -from fastapi.security import OAuth2PasswordRequestForm
9 -from loguru import logger
10 -from sqlalchemy.ext.asyncio import AsyncSession
11 -
12 -from app.auth.models.users import PasswordReset
13 -from app.auth.models.users import PasswordResetToken
14 -from app.auth.models.users import User
15 -from app.auth.models.users import UserInput
16 -from app.auth.models.users import UserLogin
17 -from app.auth.schema.auth import Token
18 -from app.auth.schema.auth import UserLoginResponse
19 -from app.auth.schema.auth import UserResponse
3 +from app.auth.models.users import (
4 + PasswordReset,
5 + PasswordResetToken,
6 + User,
7 + UserInput,
8 + UserLogin,
9 +)
10 +from app.auth.schema.auth import Token, UserLoginResponse, UserResponse
11 from app.auth.schema.user import UserBaseResponse
21 -from app.auth.services.universal import find_user
22 -from app.auth.services.universal import select_all_users
12 +from app.auth.services.universal import find_user, select_all_users
13 from app.auth.utils import AuthHandler
14 from app.db.db_session import get_db
15 +from fastapi import APIRouter, Depends, HTTPException, Security, status
16 +from fastapi.security import OAuth2PasswordRequestForm
17 +from loguru import logger
18 +from sqlalchemy.ext.asyncio import AsyncSession
19
20 ACCESS_TOKEN_EXPIRE_MINUTES = 1440
21
@@ -41,6 +35,7 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(
35 dict: A dictionary containing the access token and token type.
36 """
37 # user = auth_handler.authenticate_user(form_data.username, form_data.password)
38 +
39 user = await auth_handler.authenticate_user(form_data.username, form_data.password)
40 if not user:
41 raise HTTPException(
@@ -50,6 +45,7 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(
45 )
46 access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
47 access_token = await auth_handler.encode_token(user.username, access_token_expires)
48 + logger.info(f"Access token: {access_token}")
49 return {"access_token": access_token, "token_type": "bearer"}
50
51
@@ -65,11 +61,19 @@ async def refresh_token(current_user: User = Depends(auth_handler.get_current_us
61 - dict: A dictionary containing the refreshed access token and token type.
62 """
63 access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
68 - access_token = await auth_handler.encode_token(current_user.username, access_token_expires)
64 + access_token = await auth_handler.encode_token(
65 + current_user.username,
66 + access_token_expires,
67 + )
68 return {"access_token": access_token, "token_type": "bearer"}
69
70
72 -@auth_router.post("/register", response_model=UserResponse, status_code=201, description="Register new user")
71 +@auth_router.post(
72 + "/register",
73 + response_model=UserResponse,
74 + status_code=201,
75 + description="Register new user",
76 +)
77 async def register(user: UserInput, session: AsyncSession = Depends(get_db)):
78 """
79 Register a new user.
@@ -85,14 +89,24 @@ async def register(user: UserInput, session: AsyncSession = Depends(get_db)):
89 if any(x.username == user.username for x in users):
90 raise HTTPException(status_code=400, detail="Username is taken")
91 hashed_pwd = auth_handler.get_password_hash(user.password)
88 - u = User(username=user.username, password=hashed_pwd, email=user.email, role_id=user.role_id)
92 + u = User(
93 + username=user.username,
94 + password=hashed_pwd,
95 + email=user.email,
96 + role_id=user.role_id,
97 + )
98 logger.info(f"User: {u}")
99 session.add(u)
100 await session.commit()
101 return {"message": "User created successfully", "success": True}
102
103
95 -@auth_router.post("/login", response_model=UserLoginResponse, description="Login user", deprecated=True)
104 +@auth_router.post(
105 + "/login",
106 + response_model=UserLoginResponse,
107 + description="Login user",
108 + deprecated=True,
109 +)
110 async def login(user: UserLogin):
111 """
112 Logs in a user.
@@ -136,12 +150,24 @@ async def get_users(session: AsyncSession = Depends(get_db)):
150
151 """
152 users = await select_all_users()
139 - return UserBaseResponse(users=users, message="Users retrieved successfully", success=True)
153 + return UserBaseResponse(
154 + users=users,
155 + message="Users retrieved successfully",
156 + success=True,
157 + )
158
159
160 # ! TODO: HAVE LOGIC TO HANDLE PASSWORD RESET VIA A TOKEN BUT NOT IMPLEMENTED YET ! #
143 -@auth_router.post("/reset-token", status_code=200, description="Request password reset", include_in_schema=False)
144 -async def request_password_reset(password_reset_request: PasswordResetToken, session: AsyncSession = Depends(get_db)):
161 +@auth_router.post(
162 + "/reset-token",
163 + status_code=200,
164 + description="Request password reset",
165 + include_in_schema=False,
166 +)
167 +async def request_password_reset(
168 + password_reset_request: PasswordResetToken,
169 + session: AsyncSession = Depends(get_db),
170 +):
171 """
172 Request a password reset.
173
@@ -189,7 +215,10 @@ async def request_password_reset(password_reset_request: PasswordResetToken, ses
215 description="Reset user's password via username",
216 dependencies=[Security(AuthHandler().require_any_scope("admin"))],
217 )
192 -async def reset_password_via_username(request: PasswordReset, session: AsyncSession = Depends(get_db)):
218 +async def reset_password_via_username(
219 + request: PasswordReset,
220 + session: AsyncSession = Depends(get_db),
221 +):
222 """
223 Reset a user's password via the username. Must be an admin.
224
@@ -217,7 +246,11 @@ async def reset_password_via_username(request: PasswordReset, session: AsyncSess
246 description="Reset user's password",
247 dependencies=[Security(AuthHandler().require_any_scope("analyst", "admin"))],
248 )
220 -async def reset_password_me(request: PasswordReset, token: str = Depends(AuthHandler().security), session: AsyncSession = Depends(get_db)):
249 +async def reset_password_me(
250 + request: PasswordReset,
251 + token: str = Depends(AuthHandler().security),
252 + session: AsyncSession = Depends(get_db),
253 +):
254 """
255 Reset a user's password.
256
backend/app/auth/schema/user.py
+1 -2
@@ -1,7 +1,6 @@
1 from typing import List
2
3 -from pydantic import BaseModel
4 -from pydantic import EmailStr
3 +from pydantic import BaseModel, EmailStr
4
5
6 class UserBase(BaseModel):
backend/app/auth/services/universal.py
+16 -11
@@ -1,14 +1,11 @@
1 +from app.auth.models.users import Password, Role, User
2 +from app.db.db_session import async_engine
3 from loguru import logger
4
5 # ! New with Async
6 from sqlalchemy.ext.asyncio import AsyncSession
7 from sqlmodel import select
8
7 -from app.auth.models.users import Password
8 -from app.auth.models.users import Role
9 -from app.auth.models.users import User
10 -from app.db.db_session import async_engine
11 -
9 passwords_in_memory = {}
10
11
@@ -35,10 +32,14 @@ async def find_user(name: str):
32 Returns:
33 User: The user object if found, None otherwise.
34 """
38 - async with AsyncSession(async_engine) as session:
39 - statement = select(User).where(User.username == name)
40 - result = await session.execute(statement)
41 - return result.scalars().first()
35 + try:
36 + async with AsyncSession(async_engine) as session:
37 + statement = select(User).where(User.username == name)
38 + result = await session.execute(statement)
39 + return result.scalars().first()
40 + except Exception as e:
41 + logger.error(f"Error: {e}")
42 + return None
43
44
45 async def get_role(name: str):
@@ -103,7 +104,9 @@ async def create_admin_user(session: AsyncSession):
104 Returns:
105 - None
106 """
106 - if not await check_admin_user_exists(session): # The check function needs to be passed the session as well
107 + if not await check_admin_user_exists(
108 + session,
109 + ): # The check function needs to be passed the session as well
110 # Create the admin user
111 password_model = Password.generate(length=12)
112 admin_user = User(
@@ -133,7 +136,9 @@ async def create_scheduler_user(session: AsyncSession):
136 Returns:
137 - None
138 """
136 - if not await check_scheduler_user_exists(session): # The check function needs to be passed the session as well
139 + if not await check_scheduler_user_exists(
140 + session,
141 + ): # The check function needs to be passed the session as well
142 # Create the scheduler user
143 password_model = Password.generate(length=12)
144 scheduler_user = User(
backend/app/auth/utils.py
+36 -18
@@ -1,22 +1,21 @@
1 -from datetime import datetime
2 -from datetime import timedelta
1 +from datetime import datetime, timedelta
2
3 import jwt
5 -from fastapi import Depends
6 -from fastapi import HTTPException
7 -from fastapi.security import OAuth2PasswordBearer
8 -from fastapi.security import SecurityScopes
4 +from app.auth.services.universal import find_user, get_role
5 +from fastapi import Depends, HTTPException
6 +from fastapi.security import OAuth2PasswordBearer, SecurityScopes
7 from loguru import logger
8 from passlib.context import CryptContext
9
12 -from app.auth.services.universal import find_user
13 -from app.auth.services.universal import get_role
14 -
10
11 class AuthHandler:
12 security = OAuth2PasswordBearer(
18 - tokenUrl="auth/token",
19 - scopes={"admin": "Admin users", "analyst": "SOC Analysts", "scheduler": "Scheduler for automated tasks"},
13 + tokenUrl="api/auth/token",
14 + scopes={
15 + "admin": "Admin users",
16 + "analyst": "SOC Analysts",
17 + "scheduler": "Scheduler for automated tasks",
18 + },
19 )
20 pwd_context = CryptContext(schemes=["bcrypt"])
21 secret = "bL4unrkoxtFs1MT6A7Ns2yMLkduyuqrkTxDV9CjlbNc="
@@ -28,7 +27,11 @@ class AuthHandler:
27 return self.pwd_context.verify(plain_password, hashed_password)
28
29 # ! TODO: HAVE LOGIC TO HANDLE PASSWORD RESET VIA A TOKEN BUT NOT IMPLEMENTED YET ! #
31 - def generate_reset_token(self, username: str, expires_delta: timedelta = timedelta(minutes=30)):
30 + def generate_reset_token(
31 + self,
32 + username: str,
33 + expires_delta: timedelta = timedelta(minutes=30),
34 + ):
35 """
36 Generates a password reset token.
37
@@ -78,7 +81,10 @@ class AuthHandler:
81 if payload["sub"] == user.username:
82 return payload["sub"]
83 else:
81 - raise HTTPException(status_code=401, detail="Invalid token. Username does not match.")
84 + raise HTTPException(
85 + status_code=401,
86 + detail="Invalid token. Username does not match.",
87 + )
88 except jwt.ExpiredSignatureError:
89 raise HTTPException(status_code=401, detail="Token expired")
90 except jwt.InvalidTokenError:
@@ -98,13 +104,21 @@ class AuthHandler:
104 otherwise False.
105 """
106 user = await find_user(username)
101 - if not user or not self.verify_password(password, user.password):
102 - logger.info("Password is not verified")
107 + try:
108 + if not user or not self.verify_password(password, user.password):
109 + logger.info("Password is not verified")
110 + return False
111 + return user
112 + except Exception as e:
113 + logger.error(f"Error: {e}")
114 return False
104 - return user
115
116 # ! New with Async
107 - async def encode_token(self, username: str, access_token_expires: timedelta = timedelta(hours=24)):
117 + async def encode_token(
118 + self,
119 + username: str,
120 + access_token_expires: timedelta = timedelta(hours=24),
121 + ):
122 """
123 Encodes a JWT token with the given username and expiration time.
124
@@ -147,7 +161,11 @@ class AuthHandler:
161 except jwt.InvalidTokenError:
162 return "Invalid token", []
163
150 - async def get_current_user(self, security_scopes: SecurityScopes, token: str = Depends(security)):
164 + async def get_current_user(
165 + self,
166 + security_scopes: SecurityScopes,
167 + token: str = Depends(security),
168 + ):
169 """
170 Retrieves the current user based on the provided security scopes and token.
171
backend/app/connectors/cortex/routes/analyzers.py
+21 -15
@@ -1,17 +1,14 @@
1 from typing import List
2
3 -from fastapi import APIRouter
4 -from fastapi import Depends
5 -from fastapi import HTTPException
6 -from fastapi import Security
7 -from loguru import logger
8 -
3 from app.auth.utils import AuthHandler
10 -from app.connectors.cortex.schema.analyzers import AnalyzersResponse
11 -from app.connectors.cortex.schema.analyzers import RunAnalyzerBody
12 -from app.connectors.cortex.schema.analyzers import RunAnalyzerResponse
13 -from app.connectors.cortex.services.analyzers import get_analyzers
14 -from app.connectors.cortex.services.analyzers import run_analyzer
4 +from app.connectors.cortex.schema.analyzers import (
5 + AnalyzersResponse,
6 + RunAnalyzerBody,
7 + RunAnalyzerResponse,
8 +)
9 +from app.connectors.cortex.services.analyzers import get_analyzers, run_analyzer
10 +from fastapi import APIRouter, Depends, HTTPException, Security
11 +from loguru import logger
12
13 # App specific imports
14
@@ -45,7 +42,10 @@ async def verify_analyzer_exists(run_analyzer_body: RunAnalyzerBody) -> RunAnaly
42 """
43 available_analyzers = await get_available_analyzers()
44 if run_analyzer_body.analyzer_name not in available_analyzers:
48 - raise HTTPException(status_code=400, detail=f"Analyzer {run_analyzer_body.analyzer_name} does not exist.")
45 + raise HTTPException(
46 + status_code=400,
47 + detail=f"Analyzer {run_analyzer_body.analyzer_name} does not exist.",
48 + )
49 return run_analyzer_body
50
51
@@ -72,7 +72,9 @@ async def get_all_analyzers() -> AnalyzersResponse:
72 description="Run an analyzer",
73 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
74 )
75 -async def run_analyzer_route(run_analyzer_body: RunAnalyzerBody = Depends(verify_analyzer_exists)) -> RunAnalyzerResponse:
75 +async def run_analyzer_route(
76 + run_analyzer_body: RunAnalyzerBody = Depends(verify_analyzer_exists),
77 +) -> RunAnalyzerResponse:
78 """
79 Run an analyzer.
80
@@ -82,9 +84,13 @@ async def run_analyzer_route(run_analyzer_body: RunAnalyzerBody = Depends(verify
84 Returns:
85 RunAnalyzerResponse: The response containing the result of running the analyzer.
86 """
85 - is_valid, data_type = RunAnalyzerBody.is_valid_datatype(run_analyzer_body.analyzer_data)
87 + is_valid, data_type = RunAnalyzerBody.is_valid_datatype(
88 + run_analyzer_body.analyzer_data,
89 + )
90 if not is_valid:
91 raise HTTPException(status_code=400, detail=f"Invalid data type: {data_type}")
92
89 - logger.info(f"Running analyzer {run_analyzer_body.analyzer_name} with data {run_analyzer_body.analyzer_data} of type {data_type}")
93 + logger.info(
94 + f"Running analyzer {run_analyzer_body.analyzer_name} with data {run_analyzer_body.analyzer_data} of type {data_type}",
95 + )
96 return await run_analyzer(run_analyzer_body, data_type)
backend/app/connectors/cortex/schema/analyzers.py
+25 -14
@@ -1,16 +1,12 @@
1 import ipaddress
2 import re
3 -from typing import Any
4 -from typing import Dict
5 -from typing import List
6 -from typing import Optional
7 -from typing import Tuple
3 +from typing import Any, Dict, List, Optional, Tuple
4
9 -from pydantic import BaseModel
10 -from pydantic import Field
11 -from pydantic import validator
5 +from pydantic import BaseModel, Field, validator
6
13 -HASH_REGEX = re.compile(r"[a-fA-F\d]{32}|[a-fA-F\d]{64}") # Update this regex to match your specific hash format
7 +HASH_REGEX = re.compile(
8 + r"[a-fA-F\d]{32}|[a-fA-F\d]{64}",
9 +) # Update this regex to match your specific hash format
10 DOMAIN_REGEX = re.compile(
11 r"^(?:[a-z0-9](?:[a-z0-9\-]{0,61}[a-z0-9])?\.)+[a-z]{2,6}$",
12 ) # Update this regex to match your specific domain format
@@ -24,8 +20,14 @@ class AnalyzersResponse(BaseModel):
20
21 class RunAnalyzerBody(BaseModel):
22 analyzer_name: str = Field(..., description="Name of the analyzer to be run.")
27 - analyzer_data: str = Field(..., description="The Indicator of Compromise (IoC) to be analyzed.")
28 - data_type: Optional[str] = Field(default=None, description="Data type determined after validation")
23 + analyzer_data: str = Field(
24 + ...,
25 + description="The Indicator of Compromise (IoC) to be analyzed.",
26 + )
27 + data_type: Optional[str] = Field(
28 + default=None,
29 + description="Data type determined after validation",
30 + )
31
32 @validator("analyzer_data", pre=True, always=True)
33 def validate_and_set_data_type(cls, value: str, values: dict) -> str:
@@ -70,7 +72,16 @@ class RunAnalyzerResponse(BaseModel):
72
73
74 class AnalyzerJobData(BaseModel):
73 - data: str = Field(..., description="The Indicator of Compromise (IoC) to be analyzed.")
74 - dataType: str = Field(..., description="The type of the IoC (e.g., 'IP', 'hash', 'domain').")
75 + data: str = Field(
76 + ...,
77 + description="The Indicator of Compromise (IoC) to be analyzed.",
78 + )
79 + dataType: str = Field(
80 + ...,
81 + description="The type of the IoC (e.g., 'IP', 'hash', 'domain').",
82 + )
83 tlp: int = Field(1, description="Traffic Light Protocol (TLP) level.")
76 - message: str = Field("custom message sent to analyzer", description="Custom message.")
84 + message: str = Field(
85 + "custom message sent to analyzer",
86 + description="Custom message.",
87 + )
backend/app/connectors/cortex/services/analyzers.py
+37 -21
@@ -1,22 +1,19 @@
1 -from typing import Dict
2 -from typing import List
3 -from typing import Union
1 +from typing import Dict, List, Union
2
3 +from app.connectors.cortex.schema.analyzers import (
4 + AnalyzerJobData,
5 + AnalyzersResponse,
6 + RunAnalyzerBody,
7 + RunAnalyzerResponse,
8 +)
9 +from app.connectors.cortex.utils.universal import ( # Importing create_cortex_client; Importing from universal.py
10 + create_cortex_client,
11 + run_and_wait_for_analyzer,
12 +)
13 from cortex4py.api import Api
14 from fastapi import HTTPException
15 from loguru import logger
16
9 -from app.connectors.cortex.schema.analyzers import AnalyzerJobData
10 -from app.connectors.cortex.schema.analyzers import AnalyzersResponse
11 -from app.connectors.cortex.schema.analyzers import RunAnalyzerBody
12 -from app.connectors.cortex.schema.analyzers import RunAnalyzerResponse
13 -from app.connectors.cortex.utils.universal import (
14 - create_cortex_client, # Importing create_cortex_client
15 -)
16 -from app.connectors.cortex.utils.universal import (
17 - run_and_wait_for_analyzer, # Importing from universal.py
18 -)
19 -
17
18 async def fetch_analyzers(api: Api) -> List[Dict]:
19 """
@@ -97,10 +94,17 @@ async def get_analyzers() -> AnalyzersResponse:
94 analyzers = await fetch_analyzers(api)
95 analyzer_names = extract_analyzer_names(analyzers)
96
100 - return AnalyzersResponse(success=True, message="Successfully fetched analyzers", analyzers=analyzer_names)
97 + return AnalyzersResponse(
98 + success=True,
99 + message="Successfully fetched analyzers",
100 + analyzers=analyzer_names,
101 + )
102
103
103 -async def run_analyzer(run_analyzer_body: RunAnalyzerBody, data_type: str) -> RunAnalyzerResponse:
104 +async def run_analyzer(
105 + run_analyzer_body: RunAnalyzerBody,
106 + data_type: str,
107 +) -> RunAnalyzerResponse:
108 """
109 Runs an analyzer with the given analyzer name, analyzer data, and data type.
110
@@ -116,13 +120,25 @@ async def run_analyzer(run_analyzer_body: RunAnalyzerBody, data_type: str) -> Ru
120
121 analyzer_name = run_analyzer_body.analyzer_name
122 analyzer_data = run_analyzer_body.analyzer_data
119 - logger.info(f"Running analyzer {analyzer_name} with data {analyzer_data} of type {data_type}")
123 + logger.info(
124 + f"Running analyzer {analyzer_name} with data {analyzer_data} of type {data_type}",
125 + )
126 job_data = AnalyzerJobData(data=analyzer_data, dataType=data_type)
127
122 - result = await run_and_wait_for_analyzer(analyzer_name=analyzer_name, job_data=job_data)
128 + result = await run_and_wait_for_analyzer(
129 + analyzer_name=analyzer_name,
130 + job_data=job_data,
131 + )
132
133 if result is None:
134 logger.error(f"Failed to run analyzer {analyzer_name}")
126 - raise HTTPException(status_code=500, detail=f"Failed to run analyzer {analyzer_name}")
127 -
128 - return RunAnalyzerResponse(success=True, message="Successfully ran analyzer", report=result)
135 + raise HTTPException(
136 + status_code=500,
137 + detail=f"Failed to run analyzer {analyzer_name}",
138 + )
139 +
140 + return RunAnalyzerResponse(
141 + success=True,
142 + message="Successfully ran analyzer",
143 + report=result,
144 + )
backend/app/connectors/cortex/utils/universal.py
+45 -16
@@ -1,14 +1,12 @@
1 import time
2 -from typing import Any
3 -from typing import Dict
4 -
5 -from cortex4py.api import Api
6 -from fastapi import HTTPException
7 -from loguru import logger
2 +from typing import Any, Dict
3
4 from app.connectors.cortex.schema.analyzers import AnalyzerJobData
5 from app.connectors.utils import get_connector_info_from_db
6 from app.db.db_session import get_db_session
7 +from cortex4py.api import Api
8 +from fastapi import HTTPException
9 +from loguru import logger
10
11
12 async def verify_cortex_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
@@ -21,18 +19,35 @@ async def verify_cortex_credentials(attributes: Dict[str, Any]) -> Dict[str, Any
19 logger.info(f"Verifying the Cortex connection to {attributes['connector_url']}")
20
21 try:
24 - api = Api(attributes["connector_url"], attributes["connector_api_key"], verify_cert=False)
22 + api = Api(
23 + attributes["connector_url"],
24 + attributes["connector_api_key"],
25 + verify_cert=False,
26 + )
27 # Get Cortex Status
28 status = api.status
29 if status:
30 logger.debug("Cortex connection successful")
29 - return {"connectionSuccessful": True, "message": "Cortex connection successful"}
31 + return {
32 + "connectionSuccessful": True,
33 + "message": "Cortex connection successful",
34 + }
35 else:
31 - logger.error(f"Connection to {attributes['connector_url']} failed with error.")
32 - return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error."}
36 + logger.error(
37 + f"Connection to {attributes['connector_url']} failed with error.",
38 + )
39 + return {
40 + "connectionSuccessful": False,
41 + "message": f"Connection to {attributes['connector_url']} failed with error.",
42 + }
43 except Exception as e:
34 - logger.error(f"Connection to {attributes['connector_url']} failed with error: {e}")
35 - return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error: {e}"}
44 + logger.error(
45 + f"Connection to {attributes['connector_url']} failed with error: {e}",
46 + )
47 + return {
48 + "connectionSuccessful": False,
49 + "message": f"Connection to {attributes['connector_url']} failed with error: {e}",
50 + }
51
52
53 async def verify_cortex_connection(connector_name: str) -> str:
@@ -62,10 +77,17 @@ async def create_cortex_client(connector_name: str) -> Api:
77 if attributes is None:
78 logger.error("No Wazuh Indexer connector found in the database")
79 return None
65 - return Api(attributes["connector_url"], attributes["connector_api_key"], verify_cert=False)
80 + return Api(
81 + attributes["connector_url"],
82 + attributes["connector_api_key"],
83 + verify_cert=False,
84 + )
85
86
68 -async def run_and_wait_for_analyzer(analyzer_name: str, job_data: AnalyzerJobData) -> Dict[str, Any]:
87 +async def run_and_wait_for_analyzer(
88 + analyzer_name: str,
89 + job_data: AnalyzerJobData,
90 +) -> Dict[str, Any]:
91 """
92 Runs an analyzer by name and waits for the job to complete.
93
@@ -83,7 +105,10 @@ async def run_and_wait_for_analyzer(analyzer_name: str, job_data: AnalyzerJobDat
105 job = api.analyzers.run_by_name(analyzer_name, job_data.dict(), force=1)
106 return await monitor_analyzer_job(api, job)
107 except Exception as e:
86 - raise HTTPException(status_code=500, detail=f"Error running analyzer {analyzer_name}: {e}")
108 + raise HTTPException(
109 + status_code=500,
110 + detail=f"Error running analyzer {analyzer_name}: {e}",
111 + )
112
113
114 async def monitor_analyzer_job(api: Api, job: Any) -> Dict[str, Any]:
@@ -138,4 +163,8 @@ async def retrieve_final_report(api: Api, job_id: str) -> Dict[str, Any]:
163 """
164 report = api.jobs.get_report(job_id).report
165 final_report = report["full"]
141 - return {"success": True, "message": "Analyzer ran successfully", "report": final_report}
166 + return {
167 + "success": True,
168 + "message": "Analyzer ran successfully",
169 + "report": final_report,
170 + }
backend/app/connectors/dfir_iris/routes/alerts.py
+57 -31
@@ -1,26 +1,26 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from fastapi import HTTPException
4 -from fastapi import Security
5 -from loguru import logger
6 -from sqlalchemy.ext.asyncio import AsyncSession
7 -
1 from app.auth.utils import AuthHandler
9 -from app.connectors.dfir_iris.schema.alerts import AlertResponse
10 -from app.connectors.dfir_iris.schema.alerts import AlertsResponse
11 -from app.connectors.dfir_iris.schema.alerts import BookmarkedAlertsResponse
12 -from app.connectors.dfir_iris.schema.alerts import CaseCreationResponse
13 -from app.connectors.dfir_iris.schema.alerts import DeleteAlertResponse
14 -from app.connectors.dfir_iris.schema.alerts import DeleteMultipleAlertsRequest
15 -from app.connectors.dfir_iris.schema.alerts import FilterAlertsRequest
16 -from app.connectors.dfir_iris.services.alerts import bookmark_alert
17 -from app.connectors.dfir_iris.services.alerts import create_case
18 -from app.connectors.dfir_iris.services.alerts import delete_alert
19 -from app.connectors.dfir_iris.services.alerts import get_alert
20 -from app.connectors.dfir_iris.services.alerts import get_alerts
21 -from app.connectors.dfir_iris.services.alerts import get_bookmarked_alerts
2 +from app.connectors.dfir_iris.schema.alerts import (
3 + AlertResponse,
4 + AlertsResponse,
5 + BookmarkedAlertsResponse,
6 + CaseCreationResponse,
7 + DeleteAlertResponse,
8 + DeleteMultipleAlertsRequest,
9 + FilterAlertsRequest,
10 +)
11 +from app.connectors.dfir_iris.services.alerts import (
12 + bookmark_alert,
13 + create_case,
14 + delete_alert,
15 + get_alert,
16 + get_alerts,
17 + get_bookmarked_alerts,
18 +)
19 from app.connectors.dfir_iris.utils.universal import check_alert_exists
20 from app.db.db_session import get_db
21 +from fastapi import APIRouter, Depends, HTTPException, Security
22 +from loguru import logger
23 +from sqlalchemy.ext.asyncio import AsyncSession
24
25 # App specific imports
26
@@ -53,7 +53,9 @@ dfir_iris_alerts_router = APIRouter()
53 description="Get all bookmarked alerts",
54 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
55 )
56 -async def get_all_bookmarked_alerts() -> BookmarkedAlertsResponse:
56 +async def get_all_bookmarked_alerts(
57 + session: AsyncSession = Depends(get_db),
58 +) -> BookmarkedAlertsResponse:
59 """
60 Fetches all bookmarked alerts.
61
@@ -61,7 +63,7 @@ async def get_all_bookmarked_alerts() -> BookmarkedAlertsResponse:
63 BookmarkedAlertsResponse: The response containing the bookmarked alerts.
64 """
65 logger.info("Fetching all bookmarked alerts")
64 - return await get_bookmarked_alerts()
66 + return await get_bookmarked_alerts(session=session)
67
68
69 @dfir_iris_alerts_router.post(
@@ -70,7 +72,10 @@ async def get_all_bookmarked_alerts() -> BookmarkedAlertsResponse:
72 description="Get alerts from IRIS based on the provided filters",
73 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
74 )
73 -async def get_alerts_filtered(request: FilterAlertsRequest, session: AsyncSession = Depends(get_db)) -> AlertsResponse:
75 +async def get_alerts_filtered(
76 + request: FilterAlertsRequest,
77 + session: AsyncSession = Depends(get_db),
78 +) -> AlertsResponse:
79 """
80 Retrieve alerts from DFIR-IRIS based on the provided filters.
81
@@ -88,7 +93,10 @@ async def get_alerts_filtered(request: FilterAlertsRequest, session: AsyncSessio
93 description="Get an alert by ID",
94 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
95 )
91 -async def get_alert_by_id(alert_id: str = Depends(verify_alert_exists), session: AsyncSession = Depends(get_db)) -> AlertResponse:
96 +async def get_alert_by_id(
97 + alert_id: str = Depends(verify_alert_exists),
98 + session: AsyncSession = Depends(get_db),
99 +) -> AlertResponse:
100 """
101 Retrieve an alert by its ID.
102
@@ -119,13 +127,21 @@ async def get_all_alerts_assigned_to_user(user_id: int) -> AlertsResponse:
127 AlertsResponse: The response containing the fetched alerts assigned to the user.
128 """
129 logger.info(f"Fetching all alerts assigned to user {user_id}")
122 - alerts = (await get_alerts(request=FilterAlertsRequest(alert_owner_id=user_id, per_page=1000))).alerts
130 + alerts = (
131 + await get_alerts(
132 + request=FilterAlertsRequest(alert_owner_id=user_id, per_page=1000),
133 + )
134 + ).alerts
135 alerts_assigned_to_user = []
136 for alert in alerts:
137 if alert["alert_owner_id"] == user_id:
138 alerts_assigned_to_user.append(alert)
139
128 - return AlertsResponse(success=True, message="Successfully fetched alerts assigned to user", alerts=alerts_assigned_to_user)
140 + return AlertsResponse(
141 + success=True,
142 + message="Successfully fetched alerts assigned to user",
143 + alerts=alerts_assigned_to_user,
144 + )
145
146
147 @dfir_iris_alerts_router.post(
@@ -134,7 +150,9 @@ async def get_all_alerts_assigned_to_user(user_id: int) -> AlertsResponse:
150 description="Assign an alert to a user",
151 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
152 )
137 -async def create_case_from_alert(alert_id: str = Depends(verify_alert_exists)) -> CaseCreationResponse:
153 +async def create_case_from_alert(
154 + alert_id: str = Depends(verify_alert_exists),
155 +) -> CaseCreationResponse:
156 """
157 Create a case from an alert.
158
@@ -154,7 +172,9 @@ async def create_case_from_alert(alert_id: str = Depends(verify_alert_exists)) -
172 description="Bookmark an alert",
173 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
174 )
157 -async def bookmark_alert_route(alert_id: str = Depends(verify_alert_exists)) -> AlertResponse:
175 +async def bookmark_alert_route(
176 + alert_id: str = Depends(verify_alert_exists),
177 +) -> AlertResponse:
178 """
179 Bookmark an alert.
180
@@ -174,7 +194,9 @@ async def bookmark_alert_route(alert_id: str = Depends(verify_alert_exists)) ->
194 description="Unbookmark an alert",
195 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
196 )
177 -async def unbookmark_alert_route(alert_id: str = Depends(verify_alert_exists)) -> AlertResponse:
197 +async def unbookmark_alert_route(
198 + alert_id: str = Depends(verify_alert_exists),
199 +) -> AlertResponse:
200 """
201 Unbookmark an alert.
202
@@ -194,7 +216,9 @@ async def unbookmark_alert_route(alert_id: str = Depends(verify_alert_exists)) -
216 description="Delete multiple alerts",
217 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
218 )
197 -async def delete_multiple_alerts_route(request: DeleteMultipleAlertsRequest) -> DeleteAlertResponse:
219 +async def delete_multiple_alerts_route(
220 + request: DeleteMultipleAlertsRequest,
221 +) -> DeleteAlertResponse:
222 """
223 Delete multiple alerts.
224
@@ -237,7 +261,9 @@ async def purge_alerts_route() -> DeleteAlertResponse:
261 description="Delete an alert",
262 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
263 )
240 -async def delete_alert_route(alert_id: str = Depends(verify_alert_exists)) -> DeleteAlertResponse:
264 +async def delete_alert_route(
265 + alert_id: str = Depends(verify_alert_exists),
266 +) -> DeleteAlertResponse:
267 """
268 Delete an alert.
269
backend/app/connectors/dfir_iris/routes/assets.py
+5 -7
@@ -1,13 +1,9 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from fastapi import HTTPException
4 -from fastapi import Security
5 -from loguru import logger
6 -
1 from app.auth.utils import AuthHandler
2 from app.connectors.dfir_iris.schema.assets import AssetResponse
3 from app.connectors.dfir_iris.services.assets import get_case_assets
4 from app.connectors.dfir_iris.utils.universal import check_case_exists
5 +from fastapi import APIRouter, Depends, HTTPException, Security
6 +from loguru import logger
7
8 # App specific imports
9
@@ -39,7 +35,9 @@ dfir_iris_assets_router = APIRouter()
35 description="Get all assets for a case",
36 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
37 )
42 -async def get_case_assets_route(case_id: int = Depends(verify_case_exists)) -> AssetResponse:
38 +async def get_case_assets_route(
39 + case_id: int = Depends(verify_case_exists),
40 +) -> AssetResponse:
41 """
42 Retrieve all assets for a given case.
43
backend/app/connectors/dfir_iris/routes/cases.py
+42 -29
@@ -1,31 +1,31 @@
1 from datetime import timedelta
2
3 -from fastapi import APIRouter
4 -from fastapi import Depends
5 -from fastapi import HTTPException
6 -from fastapi import Security
7 -from loguru import logger
8 -from sqlalchemy.ext.asyncio import AsyncSession
9 -
3 from app.auth.utils import AuthHandler
11 -from app.connectors.dfir_iris.schema.cases import CaseOlderThanBody
12 -from app.connectors.dfir_iris.schema.cases import CaseResponse
13 -from app.connectors.dfir_iris.schema.cases import CasesBreachedResponse
14 -from app.connectors.dfir_iris.schema.cases import ClosedCaseResponse
15 -from app.connectors.dfir_iris.schema.cases import PurgeCaseResponse
16 -from app.connectors.dfir_iris.schema.cases import ReopenedCaseResponse
17 -from app.connectors.dfir_iris.schema.cases import SingleCaseBody
18 -from app.connectors.dfir_iris.schema.cases import SingleCaseResponse
19 -from app.connectors.dfir_iris.schema.cases import TimeUnit
20 -from app.connectors.dfir_iris.services.cases import close_case
21 -from app.connectors.dfir_iris.services.cases import delete_single_case
22 -from app.connectors.dfir_iris.services.cases import get_all_cases
23 -from app.connectors.dfir_iris.services.cases import get_cases_older_than
24 -from app.connectors.dfir_iris.services.cases import get_single_case
25 -from app.connectors.dfir_iris.services.cases import purge_cases
26 -from app.connectors.dfir_iris.services.cases import reopen_case
4 +from app.connectors.dfir_iris.schema.cases import (
5 + CaseOlderThanBody,
6 + CaseResponse,
7 + CasesBreachedResponse,
8 + ClosedCaseResponse,
9 + PurgeCaseResponse,
10 + ReopenedCaseResponse,
11 + SingleCaseBody,
12 + SingleCaseResponse,
13 + TimeUnit,
14 +)
15 +from app.connectors.dfir_iris.services.cases import (
16 + close_case,
17 + delete_single_case,
18 + get_all_cases,
19 + get_cases_older_than,
20 + get_single_case,
21 + purge_cases,
22 + reopen_case,
23 +)
24 from app.connectors.dfir_iris.utils.universal import check_case_exists
25 from app.db.db_session import get_db
26 +from fastapi import APIRouter, Depends, HTTPException, Security
27 +from loguru import logger
28 +from sqlalchemy.ext.asyncio import AsyncSession
29
30
31 async def verify_case_exists(case_id: int) -> int:
@@ -94,7 +94,9 @@ async def get_cases_route(session: AsyncSession = Depends(get_db)) -> CaseRespon
94 description="Get all cases older than a specified date",
95 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
96 )
97 -async def get_cases_older_than_route(case_older_than_body: CaseOlderThanBody = Depends(get_timedelta)) -> CaseResponse:
97 +async def get_cases_older_than_route(
98 + case_older_than_body: CaseOlderThanBody = Depends(get_timedelta),
99 +) -> CaseResponse:
100 """
101 Fetches all cases older than a specified date.
102
@@ -104,7 +106,9 @@ async def get_cases_older_than_route(case_older_than_body: CaseOlderThanBody = D
106 Returns:
107 CaseResponse: The response containing the cases older than the specified date.
108 """
107 - logger.info(f"Fetching all cases older than {case_older_than_body.older_than} ({case_older_than_body.time_unit.value})")
109 + logger.info(
110 + f"Fetching all cases older than {case_older_than_body.older_than} ({case_older_than_body.time_unit.value})",
111 + )
112 return await get_cases_older_than(case_older_than_body)
113
114
@@ -131,7 +135,9 @@ async def purge_cases_route() -> PurgeCaseResponse:
135 description="Purge a single case",
136 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
137 )
134 -async def purge_single_case_route(case_id: int = Depends(verify_case_exists)) -> PurgeCaseResponse:
138 +async def purge_single_case_route(
139 + case_id: int = Depends(verify_case_exists),
140 +) -> PurgeCaseResponse:
141 """
142 Purge a single case by its ID.
143
@@ -152,7 +158,10 @@ async def purge_single_case_route(case_id: int = Depends(verify_case_exists)) ->
158 description="Get a single case",
159 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
160 )
155 -async def get_single_case_route(case_id: int = Depends(verify_case_exists), session: AsyncSession = Depends(get_db)) -> SingleCaseResponse:
161 +async def get_single_case_route(
162 + case_id: int = Depends(verify_case_exists),
163 + session: AsyncSession = Depends(get_db),
164 +) -> SingleCaseResponse:
165 """
166 Retrieve a single case by its ID.
167
@@ -173,7 +182,9 @@ async def get_single_case_route(case_id: int = Depends(verify_case_exists), sess
182 description="Close a single case",
183 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
184 )
176 -async def close_single_case_route(case_id: int = Depends(verify_case_exists)) -> ClosedCaseResponse:
185 +async def close_single_case_route(
186 + case_id: int = Depends(verify_case_exists),
187 +) -> ClosedCaseResponse:
188 """
189 Close a single case by its ID.
190
@@ -194,7 +205,9 @@ async def close_single_case_route(case_id: int = Depends(verify_case_exists)) ->
205 description="Open a single case",
206 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
207 )
197 -async def reopen_single_case_route(case_id: int = Depends(verify_case_exists)) -> ReopenedCaseResponse:
208 +async def reopen_single_case_route(
209 + case_id: int = Depends(verify_case_exists),
210 +) -> ReopenedCaseResponse:
211 """
212 Open a single case by its ID.
213
backend/app/connectors/dfir_iris/routes/notes.py
+16 -13
@@ -1,18 +1,15 @@
1 from typing import Optional
2
3 -from fastapi import APIRouter
4 -from fastapi import Depends
5 -from fastapi import HTTPException
6 -from fastapi import Security
7 -from loguru import logger
8 -
3 from app.auth.utils import AuthHandler
10 -from app.connectors.dfir_iris.schema.notes import NoteCreationBody
11 -from app.connectors.dfir_iris.schema.notes import NoteCreationResponse
12 -from app.connectors.dfir_iris.schema.notes import NotesResponse
13 -from app.connectors.dfir_iris.services.notes import create_case_note
14 -from app.connectors.dfir_iris.services.notes import get_case_notes
4 +from app.connectors.dfir_iris.schema.notes import (
5 + NoteCreationBody,
6 + NoteCreationResponse,
7 + NotesResponse,
8 +)
9 +from app.connectors.dfir_iris.services.notes import create_case_note, get_case_notes
10 from app.connectors.dfir_iris.utils.universal import check_case_exists
11 +from fastapi import APIRouter, Depends, HTTPException, Security
12 +from loguru import logger
13
14
15 async def verify_case_exists(case_id: int) -> int:
@@ -42,7 +39,10 @@ dfir_iris_notes_router = APIRouter()
39 description="Get all notes for a case",
40 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
41 )
45 -async def get_case_notes_route(case_id: int = Depends(verify_case_exists), search_term: Optional[str] = "%") -> NotesResponse:
42 +async def get_case_notes_route(
43 + case_id: int = Depends(verify_case_exists),
44 + search_term: Optional[str] = "%",
45 +) -> NotesResponse:
46 """
47 Retrieve all notes for a specific case.
48
@@ -63,7 +63,10 @@ async def get_case_notes_route(case_id: int = Depends(verify_case_exists), searc
63 description="Create a note for a case",
64 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
65 )
66 -async def create_case_note_route(case_id: int, note_creation_body: NoteCreationBody) -> NoteCreationResponse:
66 +async def create_case_note_route(
67 + case_id: int,
68 + note_creation_body: NoteCreationBody,
69 +) -> NoteCreationResponse:
70 """
71 Create a note for a case.
72
backend/app/connectors/dfir_iris/routes/users.py
+20 -15
@@ -1,18 +1,17 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from fastapi import HTTPException
4 -from fastapi import Security
5 -from loguru import logger
6 -
1 from app.auth.utils import AuthHandler
2 from app.connectors.dfir_iris.schema.alerts import AlertResponse
9 -from app.connectors.dfir_iris.schema.users import User
10 -from app.connectors.dfir_iris.schema.users import UsersResponse
11 -from app.connectors.dfir_iris.services.users import assign_user_to_alert
12 -from app.connectors.dfir_iris.services.users import delete_user_from_alert
13 -from app.connectors.dfir_iris.services.users import get_users
14 -from app.connectors.dfir_iris.utils.universal import check_alert_exists
15 -from app.connectors.dfir_iris.utils.universal import check_user_exists
3 +from app.connectors.dfir_iris.schema.users import User, UsersResponse
4 +from app.connectors.dfir_iris.services.users import (
5 + assign_user_to_alert,
6 + delete_user_from_alert,
7 + get_users,
8 +)
9 +from app.connectors.dfir_iris.utils.universal import (
10 + check_alert_exists,
11 + check_user_exists,
12 +)
13 +from fastapi import APIRouter, Depends, HTTPException, Security
14 +from loguru import logger
15
16
17 def verify_user_exists(user_id: int) -> int:
@@ -77,7 +76,10 @@ async def get_all_users() -> UsersResponse:
76 description="Assign a user to an alert",
77 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
78 )
80 -async def assign_user_to_alert_route(alert_id: str = Depends(verify_alert_exists), user_id: int = Depends(verify_user_exists)) -> User:
79 +async def assign_user_to_alert_route(
80 + alert_id: str = Depends(verify_alert_exists),
81 + user_id: int = Depends(verify_user_exists),
82 +) -> User:
83 """
84 Assigns a user to an alert.
85
@@ -101,7 +103,10 @@ async def assign_user_to_alert_route(alert_id: str = Depends(verify_alert_exists
103 description="Delete a user from an alert",
104 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
105 )
104 -async def delete_user_from_alert_route(alert_id: str = Depends(verify_alert_exists), user_id: int = Depends(verify_user_exists)) -> User:
106 +async def delete_user_from_alert_route(
107 + alert_id: str = Depends(verify_alert_exists),
108 + user_id: int = Depends(verify_user_exists),
109 +) -> User:
110 """
111 Delete a user from an alert.
112
backend/app/connectors/dfir_iris/schema/admin.py
+2 -5
@@ -1,11 +1,8 @@
1 import uuid
2 from datetime import datetime
3 -from typing import Dict
4 -from typing import List
5 -from typing import Optional
3 +from typing import Dict, List, Optional
4
7 -from pydantic import UUID4
8 -from pydantic import BaseModel
5 +from pydantic import UUID4, BaseModel
6
7
8 class CreateCustomerData(BaseModel):
backend/app/connectors/dfir_iris/schema/alerts.py
+22 -11
@@ -1,27 +1,32 @@
1 from enum import Enum
2 -from typing import Any
3 -from typing import Dict
4 -from typing import List
5 -from typing import Optional
2 +from typing import Any, Dict, List, Optional
3
7 -from pydantic import BaseModel
8 -from pydantic import Field
4 +from pydantic import BaseModel, Field
5
6
7 class AlertsResponse(BaseModel):
12 - alerts: Optional[List[Dict[str, Any]]] = Field([], description="The alerts returned from the search.")
8 + alerts: Optional[List[Dict[str, Any]]] = Field(
9 + [],
10 + description="The alerts returned from the search.",
11 + )
12 message: str
13 success: bool
14
15
16 class AlertResponse(BaseModel):
18 - alert: Optional[Dict[str, Any]] = Field({}, description="The alert returned from the search.")
17 + alert: Optional[Dict[str, Any]] = Field(
18 + {},
19 + description="The alert returned from the search.",
20 + )
21 message: str
22 success: bool
23
24
25 class BookmarkedAlertsResponse(BaseModel):
24 - bookmarked_alerts: Optional[List[Dict[str, Any]]] = Field([], description="The alerts returned from the search.")
26 + bookmarked_alerts: Optional[List[Dict[str, Any]]] = Field(
27 + [],
28 + description="The alerts returned from the search.",
29 + )
30 message: str
31 success: bool
32
@@ -43,9 +48,15 @@ class SortOrder(Enum):
48 class FilterAlertsRequest(BaseModel):
49 per_page: int = Field(1000, description="The number of alerts to return per page.")
50 page: int = Field(1, description="The page number to return.")
46 - sort: SortOrder = Field(SortOrder.desc, description="The sort order for the alerts.")
51 + sort: SortOrder = Field(
52 + SortOrder.desc,
53 + description="The sort order for the alerts.",
54 + )
55 alert_title: Optional[str] = Field(None, description="The title of the alert.")
48 - alert_owner_id: Optional[int] = Field(None, description="The ID of the alert owner.")
56 + alert_owner_id: Optional[int] = Field(
57 + None,
58 + description="The ID of the alert owner.",
59 + )
60
61
62 class CaseModificationHistory(BaseModel):
backend/app/connectors/dfir_iris/schema/assets.py
+6 -5
@@ -1,8 +1,6 @@
1 -from typing import List
2 -from typing import Optional
1 +from typing import List, Optional
2
4 -from pydantic import BaseModel
5 -from pydantic import Field
3 +from pydantic import BaseModel, Field
4
5
6 class AssetState(BaseModel):
@@ -20,7 +18,10 @@ class Asset(BaseModel):
18 analysis_status: str
19 analysis_status_id: int
20 asset_compromise_status_id: Optional[int]
23 - asset_description: Optional[str] = Field(None, description="The description of the asset.")
21 + asset_description: Optional[str] = Field(
22 + None,
23 + description="The description of the asset.",
24 + )
25 asset_domain: Optional[str]
26 asset_icon_compromised: str
27 asset_icon_not_compromised: str
backend/app/connectors/dfir_iris/schema/cases.py
+3 -8
@@ -1,13 +1,8 @@
1 -from datetime import date
2 -from datetime import timedelta
1 +from datetime import date, timedelta
2 from enum import Enum
4 -from typing import Dict
5 -from typing import List
6 -from typing import Optional
7 -from typing import Union
3 +from typing import Dict, List, Optional, Union
4
9 -from pydantic import BaseModel
10 -from pydantic import Field
5 +from pydantic import BaseModel, Field
6
7
8 class CaseModel(BaseModel):
backend/app/connectors/dfir_iris/schema/notes.py
+6 -6
@@ -1,9 +1,6 @@
1 -from typing import Dict
2 -from typing import List
3 -from typing import Optional
1 +from typing import Dict, List, Optional
2
5 -from pydantic import BaseModel
6 -from pydantic import Field
3 +from pydantic import BaseModel, Field
4
5
6 class CustomAttributes(BaseModel):
@@ -44,7 +41,10 @@ class NotesResponse(BaseModel):
41
42 class NotesQueryParams(BaseModel):
43 case_id: int
47 - search_term: Optional[str] = Field("%", description="Search term to filter notes by. Defaults to wildcard search (%).")
44 + search_term: Optional[str] = Field(
45 + "%",
46 + description="Search term to filter notes by. Defaults to wildcard search (%).",
47 + )
48
49
50 class NoteCreationBody(BaseModel):
backend/app/connectors/dfir_iris/services/alerts.py
+101 -34
@@ -1,20 +1,23 @@
1 +from app.connectors.dfir_iris.schema.alerts import (
2 + AlertResponse,
3 + AlertsResponse,
4 + BookmarkedAlertsResponse,
5 + CaseCreationResponse,
6 + DeleteAlertResponse,
7 + FilterAlertsRequest,
8 +)
9 +from app.connectors.dfir_iris.utils.universal import (
10 + fetch_and_validate_data,
11 + initialize_client_and_alert,
12 +)
13 +from app.integrations.alert_creation_settings.models.alert_creation_settings import (
14 + AlertCreationSettings,
15 +)
16 from fastapi import HTTPException
17 from loguru import logger
18 from sqlalchemy.ext.asyncio import AsyncSession
19 from sqlalchemy.future import select
20
6 -from app.connectors.dfir_iris.schema.alerts import AlertResponse
7 -from app.connectors.dfir_iris.schema.alerts import AlertsResponse
8 -from app.connectors.dfir_iris.schema.alerts import BookmarkedAlertsResponse
9 -from app.connectors.dfir_iris.schema.alerts import CaseCreationResponse
10 -from app.connectors.dfir_iris.schema.alerts import DeleteAlertResponse
11 -from app.connectors.dfir_iris.schema.alerts import FilterAlertsRequest
12 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
13 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
14 -from app.integrations.alert_creation_settings.models.alert_creation_settings import (
15 - AlertCreationSettings,
16 -)
17 -
21
22 async def get_customer_code(session: AsyncSession, customer_id: int) -> str:
23 """
@@ -30,18 +33,25 @@ async def get_customer_code(session: AsyncSession, customer_id: int) -> str:
33 logger.info(f"Retrieving customer code for customer ID {customer_id}")
34 try:
35 alert_creation_settings = await session.execute(
33 - select(AlertCreationSettings).filter(AlertCreationSettings.iris_customer_id == customer_id),
36 + select(AlertCreationSettings).filter(
37 + AlertCreationSettings.iris_customer_id == customer_id,
38 + ),
39 )
40 alert_creation_settings = alert_creation_settings.scalars().first()
41 if alert_creation_settings is None:
42 return "Customer Not Found"
43 return alert_creation_settings.customer_code
44 except Exception as e:
40 - logger.error(f"Error retrieving customer code for customer ID {customer_id}: {e}")
45 + logger.error(
46 + f"Error retrieving customer code for customer ID {customer_id}: {e}",
47 + )
48 return "Customer Not Found"
49
50
44 -async def get_alerts(request: FilterAlertsRequest, session: AsyncSession) -> AlertsResponse:
51 +async def get_alerts(
52 + request: FilterAlertsRequest,
53 + session: AsyncSession,
54 +) -> AlertsResponse:
55 """
56 Retrieves alerts from the DFIR-IRIS service.
57
@@ -54,13 +64,25 @@ async def get_alerts(request: FilterAlertsRequest, session: AsyncSession) -> Ale
64 try:
65 client, alert = await initialize_client_and_alert("DFIR-IRIS")
66 params = construct_params(request)
57 - result = await fetch_and_validate_data(client, lambda: alert.filter_alerts(**params))
58 - logger.info(f"Successfully fetched length {len(result['data']['alerts'])} alerts")
67 + result = await fetch_and_validate_data(
68 + client,
69 + lambda: alert.filter_alerts(**params),
70 + )
71 + logger.info(
72 + f"Successfully fetched length {len(result['data']['alerts'])} alerts",
73 + )
74 # Add the customer code to each alert
75 for alert in result["data"]["alerts"]:
61 - customer_code = await get_customer_code(session, alert["customer"]["customer_id"])
76 + customer_code = await get_customer_code(
77 + session,
78 + alert["customer"]["customer_id"],
79 + )
80 alert["customer"]["customer_code"] = customer_code
63 - return AlertsResponse(success=True, message="Successfully fetched alerts", alerts=result["data"]["alerts"])
81 + return AlertsResponse(
82 + success=True,
83 + message="Successfully fetched alerts",
84 + alerts=result["data"]["alerts"],
85 + )
86 except Exception as e:
87 logger.error(f"Error fetching alerts: {e}")
88 raise HTTPException(status_code=500, detail=f"Error fetching alerts: {e}")
@@ -104,9 +126,16 @@ async def get_alert(alert_id: str, session: AsyncSession) -> AlertResponse:
126 client, alert = await initialize_client_and_alert("DFIR-IRIS")
127 result = await fetch_and_validate_data(client, alert.get_alert, alert_id)
128 # Add the customer code to the alert
107 - customer_code = await get_customer_code(session, result["data"]["customer"]["customer_id"])
129 + customer_code = await get_customer_code(
130 + session,
131 + result["data"]["customer"]["customer_id"],
132 + )
133 result["data"]["customer"]["customer_code"] = customer_code
109 - return AlertResponse(success=True, message="Successfully fetched alert", alert=result["data"])
134 + return AlertResponse(
135 + success=True,
136 + message="Successfully fetched alert",
137 + alert=result["data"],
138 + )
139
140
141 async def create_case(alert_id: str) -> CaseCreationResponse:
@@ -124,9 +153,16 @@ async def create_case(alert_id: str) -> CaseCreationResponse:
153 alert_details = await fetch_and_validate_data(client, alert.get_alert, alert_id)
154 params = construct_case_creation_params(alert_details["data"])
155 logger.info(f"Creating case with params {params}")
127 - result = await fetch_and_validate_data(client, lambda: alert.escalate_alert(int(alert_id), **params))
156 + result = await fetch_and_validate_data(
157 + client,
158 + lambda: alert.escalate_alert(int(alert_id), **params),
159 + )
160 logger.info(f"Successfully created case for alert: {result}")
129 - return CaseCreationResponse(success=True, message="Successfully created case for alert", case=result["data"])
161 + return CaseCreationResponse(
162 + success=True,
163 + message="Successfully created case for alert",
164 + case=result["data"],
165 + )
166
167
168 def construct_case_creation_params(alert_details: dict) -> dict:
@@ -144,7 +180,9 @@ def construct_case_creation_params(alert_details: dict) -> dict:
180 "case_tags": alert_details["alert_tags"],
181 "escalation_note": "Case created from CoPilot",
182 "iocs_import_list": [ioc["ioc_uuid"] for ioc in alert_details["iocs"]],
147 - "assets_import_list": [asset["asset_uuid"] for asset in alert_details["assets"]],
183 + "assets_import_list": [
184 + asset["asset_uuid"] for asset in alert_details["assets"]
185 + ],
186 }
187
188 # Replace None values with the string "None"
@@ -164,27 +202,52 @@ async def bookmark_alert(alert_id: str, bookmarked: bool) -> AlertResponse:
202 """
203 client, alert = await initialize_client_and_alert("DFIR-IRIS")
204 if bookmarked:
167 - result = await fetch_and_validate_data(client, alert.update_alert, alert_id, {"alert_tags": "bookmarked"})
168 - return AlertResponse(success=True, message="Successfully bookmarked alert", alert=result["data"])
169 - result = await fetch_and_validate_data(client, alert.update_alert, alert_id, {"alert_tags": ""})
170 - return AlertResponse(success=True, message="Successfully removed bookmark from alert", alert=result["data"])
171 -
172 -
173 -async def get_bookmarked_alerts() -> BookmarkedAlertsResponse:
205 + result = await fetch_and_validate_data(
206 + client,
207 + alert.update_alert,
208 + alert_id,
209 + {"alert_tags": "bookmarked"},
210 + )
211 + return AlertResponse(
212 + success=True,
213 + message="Successfully bookmarked alert",
214 + alert=result["data"],
215 + )
216 + result = await fetch_and_validate_data(
217 + client,
218 + alert.update_alert,
219 + alert_id,
220 + {"alert_tags": ""},
221 + )
222 + return AlertResponse(
223 + success=True,
224 + message="Successfully removed bookmark from alert",
225 + alert=result["data"],
226 + )
227 +
228 +
229 +async def get_bookmarked_alerts(session: AsyncSession) -> BookmarkedAlertsResponse:
230 """
231 Retrieves the bookmarked alerts from the system.
232
233 Returns:
234 BookmarkedAlertsResponse: The response object containing the bookmarked alerts.
235 """
180 - alerts = await get_alerts(request=FilterAlertsRequest(per_page=10000))
236 + alerts = await get_alerts(
237 + request=FilterAlertsRequest(per_page=10000),
238 + session=session,
239 + )
240 alerts = alerts.alerts
241 bookmarked_alerts = []
242 for alert in alerts:
243 if alert["alert_tags"] is not None and "bookmarked" in alert["alert_tags"]:
244 bookmarked_alerts.append(alert)
245
187 - return BookmarkedAlertsResponse(success=True, message="Successfully fetched bookmarked alerts", bookmarked_alerts=bookmarked_alerts)
246 + return BookmarkedAlertsResponse(
247 + success=True,
248 + message="Successfully fetched bookmarked alerts",
249 + bookmarked_alerts=bookmarked_alerts,
250 + )
251
252
253 async def delete_alert(alert_id: int) -> DeleteAlertResponse:
@@ -199,4 +262,8 @@ async def delete_alert(alert_id: int) -> DeleteAlertResponse:
262 """
263 client, alert = await initialize_client_and_alert("DFIR-IRIS")
264 result = await fetch_and_validate_data(client, alert.delete_alert, alert_id)
202 - return DeleteAlertResponse(success=True, message="Successfully deleted alert", alert=result["data"])
265 + return DeleteAlertResponse(
266 + success=True,
267 + message="Successfully deleted alert",
268 + alert=result["data"],
269 + )
backend/app/connectors/dfir_iris/services/assets.py
+5 -6
@@ -1,11 +1,10 @@
1 +from app.connectors.dfir_iris.schema.assets import Asset, AssetResponse, AssetState
2 +from app.connectors.dfir_iris.utils.universal import (
3 + fetch_and_validate_data,
4 + initialize_client_and_case,
5 +)
6 from fastapi import HTTPException
7
3 -from app.connectors.dfir_iris.schema.assets import Asset
4 -from app.connectors.dfir_iris.schema.assets import AssetResponse
5 -from app.connectors.dfir_iris.schema.assets import AssetState
6 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
7 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
8 -
8
9 async def get_case_assets(case_id: int) -> AssetResponse:
10 """
backend/app/connectors/dfir_iris/services/cases.py
+79 -32
@@ -1,27 +1,29 @@
1 from datetime import datetime
2 -from typing import Dict
3 -from typing import List
4 -
2 +from typing import Dict, List
3 +
4 +from app.connectors.dfir_iris.schema.cases import (
5 + CaseOlderThanBody,
6 + CaseResponse,
7 + CasesBreachedResponse,
8 + ClosedCaseResponse,
9 + PurgeCaseResponse,
10 + ReopenedCaseResponse,
11 + SingleCaseBody,
12 + SingleCaseResponse,
13 +)
14 +from app.connectors.dfir_iris.utils.universal import (
15 + create_dfir_iris_client,
16 + fetch_and_parse_data,
17 +)
18 +from app.integrations.alert_creation_settings.models.alert_creation_settings import (
19 + AlertCreationSettings,
20 +)
21 from dfir_iris_client.case import Case
22 from fastapi import HTTPException
23 from loguru import logger
24 from sqlalchemy.ext.asyncio import AsyncSession
25 from sqlalchemy.future import select
26
11 -from app.connectors.dfir_iris.schema.cases import CaseOlderThanBody
12 -from app.connectors.dfir_iris.schema.cases import CaseResponse
13 -from app.connectors.dfir_iris.schema.cases import CasesBreachedResponse
14 -from app.connectors.dfir_iris.schema.cases import ClosedCaseResponse
15 -from app.connectors.dfir_iris.schema.cases import PurgeCaseResponse
16 -from app.connectors.dfir_iris.schema.cases import ReopenedCaseResponse
17 -from app.connectors.dfir_iris.schema.cases import SingleCaseBody
18 -from app.connectors.dfir_iris.schema.cases import SingleCaseResponse
19 -from app.connectors.dfir_iris.utils.universal import create_dfir_iris_client
20 -from app.connectors.dfir_iris.utils.universal import fetch_and_parse_data
21 -from app.integrations.alert_creation_settings.models.alert_creation_settings import (
22 - AlertCreationSettings,
23 -)
24 -
27
28 async def get_client_and_cases() -> Dict:
29 """
@@ -50,14 +52,18 @@ async def get_customer_code(session: AsyncSession, client_name: str) -> str:
52 """
53 try:
54 alert_creation_settings = await session.execute(
53 - select(AlertCreationSettings).filter(AlertCreationSettings.iris_customer_name == client_name),
55 + select(AlertCreationSettings).filter(
56 + AlertCreationSettings.iris_customer_name == client_name,
57 + ),
58 )
59 alert_creation_settings = alert_creation_settings.scalars().first()
60 if alert_creation_settings is None:
61 return "Customer Not Found"
62 return alert_creation_settings.customer_code
63 except Exception as e:
60 - logger.error(f"Error retrieving customer code for customer ID {client_name}: {e}")
64 + logger.error(
65 + f"Error retrieving customer code for customer ID {client_name}: {e}",
66 + )
67 return "Customer Not Found"
68
69
@@ -94,7 +100,9 @@ def filter_cases_older_than(cases: List[Dict], older_than: datetime) -> List[Dic
100 else case["case_open_date"]
101 )
102 if case_open_date < current_time - older_than:
97 - case["case_open_date"] = case_open_date.strftime("%m/%d/%Y") # Convert back to string to match the model
103 + case["case_open_date"] = case_open_date.strftime(
104 + "%m/%d/%Y",
105 + ) # Convert back to string to match the model
106 filtered_cases.append(case)
107 return filtered_cases
108
@@ -113,16 +121,28 @@ async def get_all_cases(session: AsyncSession) -> CaseResponse:
121 try:
122 if not result["success"]:
123 logger.error(f"Failed to get all cases: {result['message']}")
116 - raise HTTPException(status_code=500, detail=f"Failed to get all cases: {result['message']}")
124 + raise HTTPException(
125 + status_code=500,
126 + detail=f"Failed to get all cases: {result['message']}",
127 + )
128 for case in result["data"]:
118 - case["customer_code"] = await get_customer_code(session, case["client_name"])
119 - return CaseResponse(success=True, message="Successfully fetched all cases", cases=result["data"])
129 + case["customer_code"] = await get_customer_code(
130 + session,
131 + case["client_name"],
132 + )
133 + return CaseResponse(
134 + success=True,
135 + message="Successfully fetched all cases",
136 + cases=result["data"],
137 + )
138 except Exception as err:
139 logger.error(f"Failed to get all cases: {err}")
140 raise HTTPException(status_code=500, detail=f"Failed to get all cases: {err}")
141
142
125 -async def get_cases_older_than(case_older_than_body: CaseOlderThanBody) -> CasesBreachedResponse:
143 +async def get_cases_older_than(
144 + case_older_than_body: CaseOlderThanBody,
145 +) -> CasesBreachedResponse:
146 """
147 Retrieves cases that are older than a specified duration.
148
@@ -135,10 +155,16 @@ async def get_cases_older_than(case_older_than_body: CaseOlderThanBody) -> Cases
155 result = await get_client_and_cases()
156 if not result["success"]:
157 logger.error(f"Failed to get all cases: {result['message']}")
138 - return HTTPException(status_code=500, detail=f"Failed to get all cases: {result['message']}")
158 + return HTTPException(
159 + status_code=500,
160 + detail=f"Failed to get all cases: {result['message']}",
161 + )
162
163 open_cases = filter_open_cases(result["data"])
141 - breached_cases = filter_cases_older_than(open_cases, case_older_than_body.older_than)
164 + breached_cases = filter_cases_older_than(
165 + open_cases,
166 + case_older_than_body.older_than,
167 + )
168 return CasesBreachedResponse(
169 success=True,
170 message=f"Successfully fetched all cases older than {case_older_than_body.older_than}",
@@ -146,7 +172,10 @@ async def get_cases_older_than(case_older_than_body: CaseOlderThanBody) -> Cases
172 )
173
174
149 -async def get_single_case(case_id: SingleCaseBody, session: AsyncSession) -> SingleCaseResponse:
175 +async def get_single_case(
176 + case_id: SingleCaseBody,
177 + session: AsyncSession,
178 +) -> SingleCaseResponse:
179 """
180 Fetches a single case from DFIR-IRIS based on the provided case ID.
181
@@ -162,8 +191,15 @@ async def get_single_case(case_id: SingleCaseBody, session: AsyncSession) -> Sin
191 dfir_iris_client = await create_dfir_iris_client("DFIR-IRIS")
192 case = Case(session=dfir_iris_client)
193 result = await fetch_and_parse_data(dfir_iris_client, case.get_case, case_id)
165 - result["data"]["customer_code"] = await get_customer_code(session, result["data"]["customer_name"])
166 - return SingleCaseResponse(success=True, message="Successfully fetched single case", case=result["data"])
194 + result["data"]["customer_code"] = await get_customer_code(
195 + session,
196 + result["data"]["customer_name"],
197 + )
198 + return SingleCaseResponse(
199 + success=True,
200 + message="Successfully fetched single case",
201 + case=result["data"],
202 + )
203
204
205 async def close_case(case_id: SingleCaseBody) -> ClosedCaseResponse:
@@ -183,7 +219,11 @@ async def close_case(case_id: SingleCaseBody) -> ClosedCaseResponse:
219 dfir_iris_client = await create_dfir_iris_client("DFIR-IRIS")
220 case = Case(session=dfir_iris_client)
221 result = await fetch_and_parse_data(dfir_iris_client, case.close_case, case_id)
186 - return ClosedCaseResponse(success=True, case=result["data"], message="Successfully closed case")
222 + return ClosedCaseResponse(
223 + success=True,
224 + case=result["data"],
225 + message="Successfully closed case",
226 + )
227
228
229 async def reopen_case(case_id: SingleCaseBody) -> ReopenedCaseResponse:
@@ -204,7 +244,11 @@ async def reopen_case(case_id: SingleCaseBody) -> ReopenedCaseResponse:
244 case = Case(session=dfir_iris_client)
245 result = await fetch_and_parse_data(dfir_iris_client, case.reopen_case, case_id)
246 logger.info(f"Successfully opened case: {result}")
207 - return ReopenedCaseResponse(success=True, case=result["data"], message="Successfully opened case")
247 + return ReopenedCaseResponse(
248 + success=True,
249 + case=result["data"],
250 + message="Successfully opened case",
251 + )
252
253
254 ############# ! DELETE ACTIONS ! #############
@@ -268,7 +312,10 @@ async def purge_case(client, case, case_id) -> PurgeCaseResponse:
312 try:
313 logger.info(f"Purging case: {case_id}")
314 await fetch_and_parse_data(client, case.delete_case, case_id)
271 - return PurgeCaseResponse(success=True, message=f"Successfully purged case {case_id}")
315 + return PurgeCaseResponse(
316 + success=True,
317 + message=f"Successfully purged case {case_id}",
318 + )
319 except Exception as err:
320 error_message = f"Failed to purge case {case_id}: {err}"
321 logger.error(error_message)
backend/app/connectors/dfir_iris/services/notes.py
+50 -19
@@ -1,18 +1,19 @@
1 -from typing import Any
2 -from typing import Dict
3 -from typing import List
4 -
1 +from typing import Any, Dict, List
2 +
3 +from app.connectors.dfir_iris.schema.notes import (
4 + NoteCreationBody,
5 + NoteCreationResponse,
6 + NoteDetails,
7 + NoteDetailsResponse,
8 + NotesResponse,
9 +)
10 +from app.connectors.dfir_iris.utils.universal import (
11 + fetch_and_validate_data,
12 + initialize_client_and_case,
13 +)
14 from dfir_iris_client.case import Case
15 from loguru import logger
16
8 -from app.connectors.dfir_iris.schema.notes import NoteCreationBody
9 -from app.connectors.dfir_iris.schema.notes import NoteCreationResponse
10 -from app.connectors.dfir_iris.schema.notes import NoteDetails
11 -from app.connectors.dfir_iris.schema.notes import NoteDetailsResponse
12 -from app.connectors.dfir_iris.schema.notes import NotesResponse
13 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
14 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
15 -
17
18 async def process_notes(notes: List[Dict], case_id: int) -> List[Dict]:
19 """
@@ -46,9 +47,18 @@ async def get_case_notes(case_id: int, search_term: str) -> NotesResponse:
47 NotesResponse: An object containing the success status, message, and retrieved notes.
48 """
49 client, case = await initialize_client_and_case("DFIR-IRIS")
49 - result = await fetch_and_validate_data(client, case.search_notes, search_term, case_id)
50 + result = await fetch_and_validate_data(
51 + client,
52 + case.search_notes,
53 + search_term,
54 + case_id,
55 + )
56 processed_notes = await process_notes(result["data"], case_id)
51 - return NotesResponse(success=True, message="Successfully fetched notes for case", notes=processed_notes)
57 + return NotesResponse(
58 + success=True,
59 + message="Successfully fetched notes for case",
60 + notes=processed_notes,
61 + )
62
63
64 async def get_case_note_details(note_id: int, case_id: int) -> NoteDetailsResponse:
@@ -68,10 +78,19 @@ async def get_case_note_details(note_id: int, case_id: int) -> NoteDetailsRespon
78 client, case = await initialize_client_and_case("DFIR-IRIS")
79 result = await fetch_and_validate_data(client, case.get_note, note_id, case_id)
80 note_details = NoteDetails(**result["data"])
71 - return NoteDetailsResponse(success=True, message="Successfully fetched note details", note_details=note_details)
81 + return NoteDetailsResponse(
82 + success=True,
83 + message="Successfully fetched note details",
84 + note_details=note_details,
85 + )
86
87
74 -async def perform_note_creation(client: Any, case: Case, note_creation_body: NoteCreationBody, case_id: int) -> Dict:
88 +async def perform_note_creation(
89 + client: Any,
90 + case: Case,
91 + note_creation_body: NoteCreationBody,
92 + case_id: int,
93 +) -> Dict:
94 """
95 Performs the creation of a note in a case.
96
@@ -84,7 +103,12 @@ async def perform_note_creation(client: Any, case: Case, note_creation_body: Not
103 Returns:
104 Dict: The response data containing the created note information.
105 """
87 - result = await fetch_and_validate_data(client, case.add_notes_group, note_creation_body.note_title, case_id)
106 + result = await fetch_and_validate_data(
107 + client,
108 + case.add_notes_group,
109 + note_creation_body.note_title,
110 + case_id,
111 + )
112 note_id = result["data"]["group_id"]
113 custom_attributes = {}
114 return await fetch_and_validate_data(
@@ -98,7 +122,10 @@ async def perform_note_creation(client: Any, case: Case, note_creation_body: Not
122 )
123
124
101 -async def create_case_note(case_id: int, note_creation_body: NoteCreationBody) -> NoteCreationResponse:
125 +async def create_case_note(
126 + case_id: int,
127 + note_creation_body: NoteCreationBody,
128 +) -> NoteCreationResponse:
129 """
130 Creates a note for a specific case.
131
@@ -111,4 +138,8 @@ async def create_case_note(case_id: int, note_creation_body: NoteCreationBody) -
138 """
139 client, case = await initialize_client_and_case("DFIR-IRIS")
140 result = await perform_note_creation(client, case, note_creation_body, case_id)
114 - return NoteCreationResponse(success=True, message="Successfully created note", note=result["data"])
141 + return NoteCreationResponse(
142 + success=True,
143 + message="Successfully created note",
144 + note=result["data"],
145 + )
backend/app/connectors/dfir_iris/services/users.py
+32 -8
@@ -1,8 +1,10 @@
1 from app.connectors.dfir_iris.schema.alerts import AlertResponse
2 from app.connectors.dfir_iris.schema.users import UsersResponse
3 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
4 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
5 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_user
3 +from app.connectors.dfir_iris.utils.universal import (
4 + fetch_and_validate_data,
5 + initialize_client_and_alert,
6 + initialize_client_and_user,
7 +)
8
9
10 async def get_users() -> UsersResponse:
@@ -14,7 +16,11 @@ async def get_users() -> UsersResponse:
16 """
17 client, user = await initialize_client_and_user("DFIR-IRIS")
18 result = await fetch_and_validate_data(client, user.list_users)
17 - return UsersResponse(success=True, message="Successfully fetched users", users=result["data"])
19 + return UsersResponse(
20 + success=True,
21 + message="Successfully fetched users",
22 + users=result["data"],
23 + )
24
25
26 async def assign_user_to_alert(alert_id: str, user_id: int) -> AlertResponse:
@@ -29,8 +35,17 @@ async def assign_user_to_alert(alert_id: str, user_id: int) -> AlertResponse:
35 AlertResponse: The response containing the updated alert information.
36 """
37 client, alert = await initialize_client_and_alert("DFIR-IRIS")
32 - result = await fetch_and_validate_data(client, alert.update_alert, alert_id, {"alert_owner_id": user_id})
33 - return AlertResponse(success=True, message="Successfully assigned user to alert", alert=result["data"])
38 + result = await fetch_and_validate_data(
39 + client,
40 + alert.update_alert,
41 + alert_id,
42 + {"alert_owner_id": user_id},
43 + )
44 + return AlertResponse(
45 + success=True,
46 + message="Successfully assigned user to alert",
47 + alert=result["data"],
48 + )
49
50
51 async def delete_user_from_alert(alert_id: str, user_id: int) -> AlertResponse:
@@ -45,5 +60,14 @@ async def delete_user_from_alert(alert_id: str, user_id: int) -> AlertResponse:
60 AlertResponse: The response containing the result of the operation.
61 """
62 client, alert = await initialize_client_and_alert("DFIR-IRIS")
48 - result = await fetch_and_validate_data(client, alert.update_alert, alert_id, {"alert_owner_id": None})
49 - return AlertResponse(success=True, message="Successfully deleted user from alert", alert=result["data"])
63 + result = await fetch_and_validate_data(
64 + client,
65 + alert.update_alert,
66 + alert_id,
67 + {"alert_owner_id": None},
68 + )
69 + return AlertResponse(
70 + success=True,
71 + message="Successfully deleted user from alert",
72 + alert=result["data"],
73 + )
backend/app/connectors/dfir_iris/utils/universal.py
+28 -17
@@ -1,25 +1,18 @@
1 -from typing import Any
2 -from typing import Callable
3 -from typing import Dict
4 -from typing import Optional
5 -from typing import Tuple
6 -from typing import Union
1 +from typing import Any, Callable, Dict, Optional, Tuple, Union
2
3 import requests
4 +from app.connectors.utils import get_connector_info_from_db
5 +from app.db.db_session import get_db_session
6 from dfir_iris_client.admin import AdminHelper
7 from dfir_iris_client.alert import Alert
8 from dfir_iris_client.case import Case
9 from dfir_iris_client.customer import Customer
13 -from dfir_iris_client.helper.utils import assert_api_resp
14 -from dfir_iris_client.helper.utils import get_data_from_resp
10 +from dfir_iris_client.helper.utils import assert_api_resp, get_data_from_resp
11 from dfir_iris_client.session import ClientSession
12 from dfir_iris_client.users import User
13 from fastapi import HTTPException
14 from loguru import logger
15
20 -from app.connectors.utils import get_connector_info_from_db
21 -from app.db.db_session import get_db_session
22 -
16
17 async def verify_dfir_iris_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
18 """
@@ -45,10 +38,18 @@ async def verify_dfir_iris_credentials(attributes: Dict[str, Any]) -> Dict[str,
38 f"Connection to {attributes['connector_url']} successful",
39 )
40 logger.debug("DFIR-IRIS connection successful")
48 - return {"connectionSuccessful": True, "message": "DFIR-IRIS connection successful"}
41 + return {
42 + "connectionSuccessful": True,
43 + "message": "DFIR-IRIS connection successful",
44 + }
45 except Exception as e:
50 - logger.error(f"Connection to {attributes['connector_url']} failed with error: {e}")
51 - return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error: {e}"}
46 + logger.error(
47 + f"Connection to {attributes['connector_url']} failed with error: {e}",
48 + )
49 + return {
50 + "connectionSuccessful": False,
51 + "message": f"Connection to {attributes['connector_url']} failed with error: {e}",
52 + }
53
54
55 async def verify_dfir_iris_connection(connector_name: str) -> str:
@@ -91,10 +92,17 @@ async def create_dfir_iris_client(connector_name: str) -> ClientSession:
92 )
93 except Exception as e:
94 logger.error(f"Error creating session with DFIR-IRIS: {e}")
94 - raise HTTPException(status_code=500, detail=f"Error creating session with DFIR-IRIS: {e}")
95 + raise HTTPException(
96 + status_code=500,
97 + detail=f"Error creating session with DFIR-IRIS: {e}",
98 + )
99
100
97 -async def fetch_and_parse_data(session: ClientSession, action: Callable, *args) -> Dict[str, Union[bool, Optional[Dict]]]:
101 +async def fetch_and_parse_data(
102 + session: ClientSession,
103 + action: Callable,
104 + *args,
105 +) -> Dict[str, Union[bool, Optional[Dict]]]:
106 """
107 Fetches and parses data from DFIR-IRIS using a specified action.
108
@@ -115,7 +123,10 @@ async def fetch_and_parse_data(session: ClientSession, action: Callable, *args)
123 return {"success": True, "data": data}
124 except Exception as err:
125 logger.error(f"Failed to execute {action.__name__}: {err}")
118 - raise HTTPException(status_code=500, detail=f"Failed to execute {action.__name__}: {err}")
126 + raise HTTPException(
127 + status_code=500,
128 + detail=f"Failed to execute {action.__name__}: {err}",
129 + )
130
131
132 async def initialize_client_and_case(service_name: str) -> Tuple[Any, Case]:
backend/app/connectors/event_shipper/utils/universal.py
+4 -2
@@ -1,7 +1,6 @@
1 from typing import Optional
2
3 import asyncgelf
4 -
4 from app.connectors.utils import get_connector_info_from_db
5 from app.db.db_session import get_db_session
6
@@ -29,4 +28,7 @@ class GelfLogger:
28 async def create_gelf_logger():
29 async with get_db_session() as session:
30 connector_info = await get_connector_info_from_db("Event Shipper", session)
32 - return GelfLogger(host=connector_info["connector_url"], port=str(connector_info["connector_extra_data"]))
31 + return GelfLogger(
32 + host=connector_info["connector_url"],
33 + port=str(connector_info["connector_extra_data"]),
34 + )
backend/app/connectors/grafana/routes/dashboards.py
+6 -7
@@ -1,12 +1,11 @@
1 -from fastapi import APIRouter
2 -from fastapi import Body
3 -from fastapi import Security
4 -from loguru import logger
5 -
1 from app.auth.utils import AuthHandler
7 -from app.connectors.grafana.schema.dashboards import DashboardProvisionRequest
8 -from app.connectors.grafana.schema.dashboards import GrafanaDashboardResponse
2 +from app.connectors.grafana.schema.dashboards import (
3 + DashboardProvisionRequest,
4 + GrafanaDashboardResponse,
5 +)
6 from app.connectors.grafana.services.dashboards import provision_dashboards
7 +from fastapi import APIRouter, Body, Security
8 +from loguru import logger
9
10 # App specific imports
11
backend/app/connectors/grafana/schema/dashboards.py
+16 -7
@@ -1,9 +1,7 @@
1 from enum import Enum
2 from typing import List
3
4 -from pydantic import BaseModel
5 -from pydantic import Field
6 -from pydantic import validator
4 +from pydantic import BaseModel, Field, validator
5
6
7 class GrafanaDashboard(BaseModel):
@@ -67,14 +65,25 @@ class MimecastDashboard(Enum):
65
66
67 class DashboardProvisionRequest(BaseModel):
70 - dashboards: List[str] = Field(..., description="List of dashboard identifiers to provision")
71 - organizationId: int = Field(0, description="Organization ID to provision dashboards to")
68 + dashboards: List[str] = Field(
69 + ...,
70 + description="List of dashboard identifiers to provision",
71 + )
72 + organizationId: int = Field(
73 + 0,
74 + description="Organization ID to provision dashboards to",
75 + )
76 folderId: int = Field(0, description="Folder ID to provision dashboards to")
73 - datasourceUid: str = Field("uid-to-be-replaced", description="Datasource UID to use for dashboards")
77 + datasourceUid: str = Field(
78 + "uid-to-be-replaced",
79 + description="Datasource UID to use for dashboards",
80 + )
81
82 @validator("dashboards", each_item=True)
83 def check_dashboard_exists(cls, e):
77 - valid_dashboards = {item.name: item for item in list(WazuhDashboard) + list(Office365Dashboard)}
84 + valid_dashboards = {
85 + item.name: item for item in list(WazuhDashboard) + list(Office365Dashboard)
86 + }
87 if e not in valid_dashboards:
88 raise ValueError(f'Dashboard identifier "{e}" is not recognized.')
89 return e
backend/app/connectors/grafana/services/dashboards.py
+61 -20
@@ -1,17 +1,18 @@
1 import json
2 from pathlib import Path
3
4 +from app.connectors.grafana.schema.dashboards import (
5 + DashboardProvisionRequest,
6 + GrafanaDashboard,
7 + GrafanaDashboardResponse,
8 + MimecastDashboard,
9 + Office365Dashboard,
10 + WazuhDashboard,
11 +)
12 +from app.connectors.grafana.utils.universal import create_grafana_client
13 from fastapi import HTTPException
14 from loguru import logger
15
7 -from app.connectors.grafana.schema.dashboards import DashboardProvisionRequest
8 -from app.connectors.grafana.schema.dashboards import GrafanaDashboard
9 -from app.connectors.grafana.schema.dashboards import GrafanaDashboardResponse
10 -from app.connectors.grafana.schema.dashboards import MimecastDashboard
11 -from app.connectors.grafana.schema.dashboards import Office365Dashboard
12 -from app.connectors.grafana.schema.dashboards import WazuhDashboard
13 -from app.connectors.grafana.utils.universal import create_grafana_client
14 -
16
17 def get_dashboard_path(dashboard_info: tuple) -> Path:
18 """
@@ -25,7 +26,9 @@ def get_dashboard_path(dashboard_info: tuple) -> Path:
26 """
27 folder_name, file_name = dashboard_info
28 current_file = Path(__file__) # Path to the current file
28 - base_dir = current_file.parent.parent # Move up two levels to the 'grafana' directory
29 + base_dir = (
30 + current_file.parent.parent
31 + ) # Move up two levels to the 'grafana' directory
32 return base_dir / "dashboards" / folder_name / file_name
33
34
@@ -62,7 +65,12 @@ def load_dashboard_json(dashboard_info: tuple, datasource_uid: str) -> dict:
65 raise HTTPException(status_code=500, detail="Error decoding JSON from file")
66
67
65 -def replace_uid_value(obj, new_value, key_to_replace="uid", old_value="replace_datasource_uid"):
68 +def replace_uid_value(
69 + obj,
70 + new_value,
71 + key_to_replace="uid",
72 + old_value="replace_datasource_uid",
73 +):
74 """
75 Recursively replaces the value of a specified key in a nested dictionary or list.
76
@@ -83,7 +91,11 @@ def replace_uid_value(obj, new_value, key_to_replace="uid", old_value="replace_d
91 replace_uid_value(item, new_value, key_to_replace, old_value)
92
93
86 -async def update_dashboard(dashboard_json: dict, organization_id: int, folder_id: int) -> dict:
94 +async def update_dashboard(
95 + dashboard_json: dict,
96 + organization_id: int,
97 + folder_id: int,
98 +) -> dict:
99 """
100 Update a dashboard in Grafana.
101
@@ -98,20 +110,30 @@ async def update_dashboard(dashboard_json: dict, organization_id: int, folder_id
110 Raises:
111 HTTPException: If there is an error updating the dashboard.
112 """
101 - logger.info(f"Updating dashboards for organization {organization_id} and folder {folder_id}")
113 + logger.info(
114 + f"Updating dashboards for organization {organization_id} and folder {folder_id}",
115 + )
116 try:
117 grafana_client = await create_grafana_client("Grafana")
118 # Switch to the newly created organization
119 grafana_client.user.switch_actual_user_organisation(organization_id)
106 - logger.info(f"Updating dashboards for organization {organization_id} and folder {folder_id}")
107 - dashboard_update_payload = {"dashboard": dashboard_json, "folderId": folder_id, "overwrite": True}
120 + logger.info(
121 + f"Updating dashboards for organization {organization_id} and folder {folder_id}",
122 + )
123 + dashboard_update_payload = {
124 + "dashboard": dashboard_json,
125 + "folderId": folder_id,
126 + "overwrite": True,
127 + }
128 return grafana_client.dashboard.update_dashboard(dashboard_update_payload)
129 except Exception as e:
130 logger.error(f"Error updating dashboard: {e}")
131 raise HTTPException(status_code=500, detail=f"Error updating dashboard: {e}")
132
133
114 -async def provision_dashboards(dashboard_request: DashboardProvisionRequest) -> GrafanaDashboardResponse:
134 +async def provision_dashboards(
135 + dashboard_request: DashboardProvisionRequest,
136 +) -> GrafanaDashboardResponse:
137 """
138 Provisions dashboards in Grafana.
139
@@ -125,12 +147,20 @@ async def provision_dashboards(dashboard_request: DashboardProvisionRequest) ->
147 provisioned_dashboards = []
148 errors = []
149
128 - valid_dashboards = {item.name: item for item in list(WazuhDashboard) + list(Office365Dashboard) + list(MimecastDashboard)}
150 + valid_dashboards = {
151 + item.name: item
152 + for item in list(WazuhDashboard)
153 + + list(Office365Dashboard)
154 + + list(MimecastDashboard)
155 + }
156
157 for dashboard_name in dashboard_request.dashboards:
158 dashboard_enum = valid_dashboards[dashboard_name]
159 try:
133 - dashboard_json = load_dashboard_json(dashboard_enum.value, datasource_uid=dashboard_request.datasourceUid)
160 + dashboard_json = load_dashboard_json(
161 + dashboard_enum.value,
162 + datasource_uid=dashboard_request.datasourceUid,
163 + )
164 updated_dashboard = await update_dashboard(
165 dashboard_json=dashboard_json,
166 organization_id=dashboard_request.organizationId,
@@ -139,8 +169,19 @@ async def provision_dashboards(dashboard_request: DashboardProvisionRequest) ->
169 provisioned_dashboards.append(GrafanaDashboard(**updated_dashboard))
170 except HTTPException as e:
171 errors.append(f"Failed to update dashboard {dashboard_name}: {e.detail}")
142 - raise HTTPException(status_code=500, detail=f"Error updating dashboard: {e}")
172 + raise HTTPException(
173 + status_code=500,
174 + detail=f"Error updating dashboard: {e}",
175 + )
176
177 success = len(errors) == 0
145 - message = "All dashboards provisioned successfully" if success else "Some dashboards failed to provision"
146 - return GrafanaDashboardResponse(provisioned_dashboards=provisioned_dashboards, success=success, message=message)
178 + message = (
179 + "All dashboards provisioned successfully"
180 + if success
181 + else "Some dashboards failed to provision"
182 + )
183 + return GrafanaDashboardResponse(
184 + provisioned_dashboards=provisioned_dashboards,
185 + success=success,
186 + message=message,
187 + )
backend/app/connectors/grafana/utils/universal.py
+28 -12
@@ -1,16 +1,18 @@
1 -from typing import Any
2 -from typing import Dict
3 -
4 -from fastapi import HTTPException
5 -from grafana_client import GrafanaApi
6 -from loguru import logger
1 +from typing import Any, Dict
2
3 from app.connectors.grafana.schema.organization import GrafanaCreateOrganizationResponse
4 from app.connectors.utils import get_connector_info_from_db
5 from app.db.db_session import get_db_session
6 +from fastapi import HTTPException
7 +from grafana_client import GrafanaApi
8 +from loguru import logger
9
10
13 -async def construct_grafana_url(connector_url: str, username: str, password: str) -> str:
11 +async def construct_grafana_url(
12 + connector_url: str,
13 + username: str,
14 + password: str,
15 +) -> str:
16 """
17 Constructs a Grafana URL with embedded credentials.
18
@@ -55,14 +57,22 @@ async def verify_grafana_credentials(attributes: Dict[str, Any]) -> Dict[str, An
57
58 create_org = GrafanaCreateOrganizationResponse(**create_org)
59
58 - remove_org = grafana_client.organizations.delete_organization(organization_id=create_org.orgId)
60 + remove_org = grafana_client.organizations.delete_organization(
61 + organization_id=create_org.orgId,
62 + )
63 logger.info(f"Remove organization: {remove_org}")
64
65 logger.info(f"Connection to {grafana_url} successful")
62 - return {"connectionSuccessful": True, "message": "Grafana connection successful"}
66 + return {
67 + "connectionSuccessful": True,
68 + "message": "Grafana connection successful",
69 + }
70 except Exception as e:
71 logger.error(f"Connection to {grafana_url} failed with error: {e}")
65 - return {"connectionSuccessful": False, "message": f"Connection to {grafana_url} failed with error: {e}"}
72 + return {
73 + "connectionSuccessful": False,
74 + "message": f"Connection to {grafana_url} failed with error: {e}",
75 + }
76
77
78 async def verify_grafana_connection(connector_name: str) -> str:
@@ -98,7 +108,10 @@ async def create_grafana_client(connector_name: str) -> GrafanaApi:
108 async with get_db_session() as session: # This will correctly enter the context manager
109 attributes = await get_connector_info_from_db(connector_name, session)
110 if attributes is None:
101 - raise HTTPException(status_code=500, detail=f"No {connector_name} connector found in the database")
111 + raise HTTPException(
112 + status_code=500,
113 + detail=f"No {connector_name} connector found in the database",
114 + )
115 try:
116 grafana_url = await construct_grafana_url(
117 attributes["connector_url"],
@@ -107,4 +120,7 @@ async def create_grafana_client(connector_name: str) -> GrafanaApi:
120 )
121 return GrafanaApi.from_url(grafana_url)
122 except Exception as e:
110 - raise HTTPException(status_code=500, detail=f"Failed to create Grafana client: {e}")
123 + raise HTTPException(
124 + status_code=500,
125 + detail=f"Failed to create Grafana client: {e}",
126 + )
backend/app/connectors/graylog/routes/collector.py
+14 -12
@@ -1,16 +1,18 @@
1 -from fastapi import APIRouter
2 -from fastapi import Security
3 -from loguru import logger
4 -
1 from app.auth.utils import AuthHandler
6 -from app.connectors.graylog.schema.collector import ConfiguredInputsResponse
7 -from app.connectors.graylog.schema.collector import GraylogIndicesResponse
8 -from app.connectors.graylog.schema.collector import GraylogInputsResponse
9 -from app.connectors.graylog.schema.collector import RunningInputsResponse
10 -from app.connectors.graylog.services.collector import get_indices_full
11 -from app.connectors.graylog.services.collector import get_inputs
12 -from app.connectors.graylog.services.collector import get_inputs_configured
13 -from app.connectors.graylog.services.collector import get_inputs_running
2 +from app.connectors.graylog.schema.collector import (
3 + ConfiguredInputsResponse,
4 + GraylogIndicesResponse,
5 + GraylogInputsResponse,
6 + RunningInputsResponse,
7 +)
8 +from app.connectors.graylog.services.collector import (
9 + get_indices_full,
10 + get_inputs,
11 + get_inputs_configured,
12 + get_inputs_running,
13 +)
14 +from fastapi import APIRouter, Security
15 +from loguru import logger
16
17 # App specific imports
18
backend/app/connectors/graylog/routes/events.py
+8 -9
@@ -1,13 +1,12 @@
1 -from fastapi import APIRouter
2 -from fastapi import Security
3 -from loguru import logger
4 -
1 from app.auth.utils import AuthHandler
6 -from app.connectors.graylog.schema.events import AlertQuery
7 -from app.connectors.graylog.schema.events import GraylogAlertsResponse
8 -from app.connectors.graylog.schema.events import GraylogEventDefinitionsResponse
9 -from app.connectors.graylog.services.events import get_alerts
10 -from app.connectors.graylog.services.events import get_event_definitions
2 +from app.connectors.graylog.schema.events import (
3 + AlertQuery,
4 + GraylogAlertsResponse,
5 + GraylogEventDefinitionsResponse,
6 +)
7 +from app.connectors.graylog.services.events import get_alerts, get_event_definitions
8 +from fastapi import APIRouter, Security
9 +from loguru import logger
10
11 # App specific imports
12
backend/app/connectors/graylog/routes/management.py
+46 -31
@@ -1,32 +1,34 @@
1 from typing import List
2
3 -from fastapi import APIRouter
4 -from fastapi import Depends
5 -from fastapi import HTTPException
6 -from fastapi import Security
7 -from loguru import logger
8 -
3 from app.auth.utils import AuthHandler
10 -from app.connectors.graylog.schema.management import DeletedIndexBody
11 -from app.connectors.graylog.schema.management import DeletedIndexResponse
12 -from app.connectors.graylog.schema.management import StartInputBody
13 -from app.connectors.graylog.schema.management import StartInputResponse
14 -from app.connectors.graylog.schema.management import StartStreamBody
15 -from app.connectors.graylog.schema.management import StartStreamResponse
16 -from app.connectors.graylog.schema.management import StopInputBody
17 -from app.connectors.graylog.schema.management import StopInputResponse
18 -from app.connectors.graylog.schema.management import StopStreamBody
19 -from app.connectors.graylog.schema.management import StopStreamResponse
20 -from app.connectors.graylog.schema.management import UrlWhitelistEntryResponse
21 -from app.connectors.graylog.services.collector import get_index_names
22 -from app.connectors.graylog.services.collector import get_input_ids
23 -from app.connectors.graylog.services.collector import get_url_whitelist_entries
24 -from app.connectors.graylog.services.management import delete_index
25 -from app.connectors.graylog.services.management import start_input
26 -from app.connectors.graylog.services.management import start_stream
27 -from app.connectors.graylog.services.management import stop_input
28 -from app.connectors.graylog.services.management import stop_stream
4 +from app.connectors.graylog.schema.management import (
5 + DeletedIndexBody,
6 + DeletedIndexResponse,
7 + StartInputBody,
8 + StartInputResponse,
9 + StartStreamBody,
10 + StartStreamResponse,
11 + StopInputBody,
12 + StopInputResponse,
13 + StopStreamBody,
14 + StopStreamResponse,
15 + UrlWhitelistEntryResponse,
16 +)
17 +from app.connectors.graylog.services.collector import (
18 + get_index_names,
19 + get_input_ids,
20 + get_url_whitelist_entries,
21 +)
22 +from app.connectors.graylog.services.management import (
23 + delete_index,
24 + start_input,
25 + start_stream,
26 + stop_input,
27 + stop_stream,
28 +)
29 from app.connectors.graylog.services.streams import get_stream_ids
30 +from fastapi import APIRouter, Depends, HTTPException, Security
31 +from loguru import logger
32
33 graylog_management_router = APIRouter()
34
@@ -104,7 +106,10 @@ async def verify_input_id(stop_input_body: StopInputBody) -> StopInputBody:
106
107 managed_input_ids = await get_managed_input_ids()
108 if stop_input_body.input_id not in managed_input_ids:
107 - raise HTTPException(status_code=400, detail=f"Input ID '{stop_input_body.input_id}' is not managed by Graylog or no longer exists.")
109 + raise HTTPException(
110 + status_code=400,
111 + detail=f"Input ID '{stop_input_body.input_id}' is not managed by Graylog or no longer exists.",
112 + )
113 return stop_input_body
114
115
@@ -157,7 +162,9 @@ async def get_url_whitelist() -> UrlWhitelistEntryResponse:
162 description="Delete index",
163 dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
164 )
160 -async def delete_index_route(deleted_index_body: DeletedIndexBody = Depends(verify_index_name)) -> DeletedIndexResponse:
165 +async def delete_index_route(
166 + deleted_index_body: DeletedIndexBody = Depends(verify_index_name),
167 +) -> DeletedIndexResponse:
168 """
169 Delete index route.
170
@@ -181,7 +188,9 @@ async def delete_index_route(deleted_index_body: DeletedIndexBody = Depends(veri
188 description="Stop input",
189 dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
190 )
184 -async def stop_input_route(stop_input_body: StopInputBody = Depends(verify_input_id)) -> StopInputResponse:
191 +async def stop_input_route(
192 + stop_input_body: StopInputBody = Depends(verify_input_id),
193 +) -> StopInputResponse:
194 """
195 Stop input route.
196
@@ -204,7 +213,9 @@ async def stop_input_route(stop_input_body: StopInputBody = Depends(verify_input
213 description="Start input",
214 dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
215 )
207 -async def start_input_route(start_input_body: StartInputBody = Depends(verify_input_id)) -> StartInputResponse:
216 +async def start_input_route(
217 + start_input_body: StartInputBody = Depends(verify_input_id),
218 +) -> StartInputResponse:
219 """
220 Start the input with the given input ID.
221
@@ -225,7 +236,9 @@ async def start_input_route(start_input_body: StartInputBody = Depends(verify_in
236 description="Stop stream",
237 dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
238 )
228 -async def stop_stream_route(stop_stream_body: StopStreamBody = Depends(verify_stream_id)) -> StopStreamResponse:
239 +async def stop_stream_route(
240 + stop_stream_body: StopStreamBody = Depends(verify_stream_id),
241 +) -> StopStreamResponse:
242 """
243 Stop stream route.
244
@@ -248,7 +261,9 @@ async def stop_stream_route(stop_stream_body: StopStreamBody = Depends(verify_st
261 description="Start stream",
262 dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
263 )
251 -async def start_stream_route(start_stream_body: StartStreamBody = Depends(verify_stream_id)) -> StartStreamResponse:
264 +async def start_stream_route(
265 + start_stream_body: StartStreamBody = Depends(verify_stream_id),
266 +) -> StartStreamResponse:
267 """
268 Start stream route.
269
backend/app/connectors/graylog/routes/monitoring.py
+12 -10
@@ -1,14 +1,16 @@
1 -from fastapi import APIRouter
2 -from fastapi import Security
3 -from loguru import logger
4 -
1 from app.auth.utils import AuthHandler
6 -from app.connectors.graylog.schema.monitoring import GraylogEventNotificationsResponse
7 -from app.connectors.graylog.schema.monitoring import GraylogMessagesResponse
8 -from app.connectors.graylog.schema.monitoring import GraylogMetricsResponse
9 -from app.connectors.graylog.services.monitoring import get_event_notifications
10 -from app.connectors.graylog.services.monitoring import get_messages
11 -from app.connectors.graylog.services.monitoring import get_metrics
2 +from app.connectors.graylog.schema.monitoring import (
3 + GraylogEventNotificationsResponse,
4 + GraylogMessagesResponse,
5 + GraylogMetricsResponse,
6 +)
7 +from app.connectors.graylog.services.monitoring import (
8 + get_event_notifications,
9 + get_messages,
10 + get_metrics,
11 +)
12 +from fastapi import APIRouter, Security
13 +from loguru import logger
14
15 # App specific imports
16
backend/app/connectors/graylog/routes/pipelines.py
+36 -22
@@ -1,22 +1,23 @@
1 -from typing import Dict
2 -from typing import List
3 -
4 -from fastapi import APIRouter
5 -from fastapi import Security
6 -from loguru import logger
1 +from typing import Dict, List
2
3 from app.auth.utils import AuthHandler
9 -from app.connectors.graylog.schema.pipelines import GraylogPipelinesResponse
10 -from app.connectors.graylog.schema.pipelines import GraylogPipelinesResponseWithRuleID
11 -from app.connectors.graylog.schema.pipelines import Pipeline
12 -from app.connectors.graylog.schema.pipelines import PipelineRule
13 -from app.connectors.graylog.schema.pipelines import PipelineRulesResponse
14 -from app.connectors.graylog.schema.pipelines import PipelineWithRuleID
15 -from app.connectors.graylog.schema.pipelines import Stage
16 -from app.connectors.graylog.schema.pipelines import StageWithRuleID
17 -from app.connectors.graylog.services.pipelines import get_pipeline_rule_by_id
18 -from app.connectors.graylog.services.pipelines import get_pipeline_rules
19 -from app.connectors.graylog.services.pipelines import get_pipelines
4 +from app.connectors.graylog.schema.pipelines import (
5 + GraylogPipelinesResponse,
6 + GraylogPipelinesResponseWithRuleID,
7 + Pipeline,
8 + PipelineRule,
9 + PipelineRulesResponse,
10 + PipelineWithRuleID,
11 + Stage,
12 + StageWithRuleID,
13 +)
14 +from app.connectors.graylog.services.pipelines import (
15 + get_pipeline_rule_by_id,
16 + get_pipeline_rules,
17 + get_pipelines,
18 +)
19 +from fastapi import APIRouter, Security
20 +from loguru import logger
21
22 # App specific imports
23
@@ -40,7 +41,10 @@ def create_rule_title_to_id_dict(pipeline_rules: List[PipelineRule]) -> Dict[str
41 return rule_title_to_id
42
43
43 -def transform_stages_with_rule_ids(stages: List[Stage], rule_title_to_id: Dict[str, str]) -> List[StageWithRuleID]:
44 +def transform_stages_with_rule_ids(
45 + stages: List[Stage],
46 + rule_title_to_id: Dict[str, str],
47 +) -> List[StageWithRuleID]:
48 """
49 Transforms a list of stages by adding corresponding rule IDs based on a dictionary mapping rule titles to IDs.
50
@@ -53,13 +57,18 @@ def transform_stages_with_rule_ids(stages: List[Stage], rule_title_to_id: Dict[s
57 """
58 new_stages = []
59 for stage in stages:
56 - rule_ids = [rule_title_to_id.get(rule_title, None) for rule_title in stage.rules]
60 + rule_ids = [
61 + rule_title_to_id.get(rule_title, None) for rule_title in stage.rules
62 + ]
63 new_stage = StageWithRuleID(**stage.dict(), rule_ids=rule_ids)
64 new_stages.append(new_stage)
65 return new_stages
66
67
62 -def transform_pipeline_with_rule_ids(pipeline: Pipeline, rule_title_to_id: Dict[str, str]) -> PipelineWithRuleID:
68 +def transform_pipeline_with_rule_ids(
69 + pipeline: Pipeline,
70 + rule_title_to_id: Dict[str, str],
71 +) -> PipelineWithRuleID:
72 """
73 Transforms a pipeline by replacing rule titles with rule IDs.
74
@@ -110,9 +119,14 @@ async def get_all_pipelines_with_rule_ids() -> GraylogPipelinesResponseWithRuleI
119 pipelines_response = await get_pipelines()
120 pipeline_rules_response = await get_pipeline_rules()
121
113 - rule_title_to_id = create_rule_title_to_id_dict(pipeline_rules_response.pipeline_rules)
122 + rule_title_to_id = create_rule_title_to_id_dict(
123 + pipeline_rules_response.pipeline_rules,
124 + )
125
115 - new_pipelines = [transform_pipeline_with_rule_ids(pipeline, rule_title_to_id) for pipeline in pipelines_response.pipelines]
126 + new_pipelines = [
127 + transform_pipeline_with_rule_ids(pipeline, rule_title_to_id)
128 + for pipeline in pipelines_response.pipelines
129 + ]
130
131 return GraylogPipelinesResponseWithRuleID(
132 pipelines=new_pipelines,
backend/app/connectors/graylog/routes/streams.py
+2 -4
@@ -1,10 +1,8 @@
1 -from fastapi import APIRouter
2 -from fastapi import Security
3 -from loguru import logger
4 -
1 from app.auth.utils import AuthHandler
2 from app.connectors.graylog.schema.streams import GraylogStreamsResponse
3 from app.connectors.graylog.services.streams import get_streams
4 +from fastapi import APIRouter, Security
5 +from loguru import logger
6
7 # App specific imports
8
backend/app/connectors/graylog/schema/collector.py
+2 -5
@@ -1,9 +1,6 @@
1 -from typing import Dict
2 -from typing import List
3 -from typing import Optional
1 +from typing import Dict, List, Optional
2
5 -from pydantic import BaseModel
6 -from pydantic import Field
3 +from pydantic import BaseModel, Field
4
5
6 class Document(BaseModel):
backend/app/connectors/graylog/schema/events.py
+9 -6
@@ -1,7 +1,4 @@
1 -from typing import Dict
2 -from typing import List
3 -from typing import Optional
4 -from typing import Union
1 +from typing import Dict, List, Optional, Union
2
3 from pydantic import BaseModel
4
@@ -68,8 +65,14 @@ class AlertQuery(BaseModel):
65 query: Optional[str] = ""
66 page: int = 1
67 per_page: int = 100
71 - filter: Optional[Dict[str, Union[str, List[str]]]] = {"alerts": "only", "event_definitions": []}
72 - timerange: Optional[Dict[str, Union[int, str]]] = {"range": 86400, "type": "relative"}
68 + filter: Optional[Dict[str, Union[str, List[str]]]] = {
69 + "alerts": "only",
70 + "event_definitions": [],
71 + }
72 + timerange: Optional[Dict[str, Union[int, str]]] = {
73 + "range": 86400,
74 + "type": "relative",
75 + }
76
77
78 class SimplifiedEventDefinition(BaseModel):
backend/app/connectors/graylog/schema/monitoring.py
+23 -11
@@ -1,8 +1,6 @@
1 -from typing import List
2 -from typing import Optional
1 +from typing import List, Optional
2
4 -from pydantic import BaseModel
5 -from pydantic import Field
3 +from pydantic import BaseModel, Field
4
5
6 class GraylogMessages(BaseModel):
@@ -29,13 +27,27 @@ class GraylogThroughputMetrics(BaseModel):
27
28
29 class GraylogThroughputMetricsCollection(BaseModel):
32 - graylog2_buffers_input_usage: Optional[str] = Field(alias="org.graylog2.buffers.input.usage")
33 - graylog2_buffers_output_usage: Optional[str] = Field(alias="org.graylog2.buffers.output.usage")
34 - graylog2_buffers_process_usage: Optional[str] = Field(alias="org.graylog2.buffers.process.usage")
35 - graylog2_throughput_input_1_sec_rate: Optional[str] = Field(alias="org.graylog2.throughput.input.1-sec-rate")
36 - graylog2_throughput_output_1_sec_rate: Optional[str] = Field(alias="org.graylog2.throughput.output.1-sec-rate")
37 - graylog2_throughput_output: Optional[str] = Field(alias="org.graylog2.throughput.output")
38 - graylog2_throughput_input: Optional[str] = Field(alias="org.graylog2.throughput.input")
30 + graylog2_buffers_input_usage: Optional[str] = Field(
31 + alias="org.graylog2.buffers.input.usage",
32 + )
33 + graylog2_buffers_output_usage: Optional[str] = Field(
34 + alias="org.graylog2.buffers.output.usage",
35 + )
36 + graylog2_buffers_process_usage: Optional[str] = Field(
37 + alias="org.graylog2.buffers.process.usage",
38 + )
39 + graylog2_throughput_input_1_sec_rate: Optional[str] = Field(
40 + alias="org.graylog2.throughput.input.1-sec-rate",
41 + )
42 + graylog2_throughput_output_1_sec_rate: Optional[str] = Field(
43 + alias="org.graylog2.throughput.output.1-sec-rate",
44 + )
45 + graylog2_throughput_output: Optional[str] = Field(
46 + alias="org.graylog2.throughput.output",
47 + )
48 + graylog2_throughput_input: Optional[str] = Field(
49 + alias="org.graylog2.throughput.input",
50 + )
51
52
53 class GraylogThroughputMetricsList(BaseModel):
backend/app/connectors/graylog/schema/pipelines.py
+1 -2
@@ -1,5 +1,4 @@
1 -from typing import List
2 -from typing import Optional
1 +from typing import List, Optional
2
3 from pydantic import BaseModel
4
backend/app/connectors/graylog/schema/streams.py
+1 -2
@@ -1,5 +1,4 @@
1 -from typing import List
2 -from typing import Optional
1 +from typing import List, Optional
2
3 from pydantic import BaseModel
4
backend/app/connectors/graylog/services/collector.py
+58 -23
@@ -1,18 +1,18 @@
1 -from typing import List
2 -from typing import Tuple
3 -
4 -from fastapi import HTTPException
5 -from loguru import logger
6 -
7 -from app.connectors.graylog.schema.collector import ConfiguredInput
8 -from app.connectors.graylog.schema.collector import ConfiguredInputsResponse
9 -from app.connectors.graylog.schema.collector import GraylogIndexItem
10 -from app.connectors.graylog.schema.collector import GraylogIndicesResponse
11 -from app.connectors.graylog.schema.collector import GraylogInputsResponse
12 -from app.connectors.graylog.schema.collector import RunningInput
13 -from app.connectors.graylog.schema.collector import RunningInputsResponse
1 +from typing import List, Tuple
2 +
3 +from app.connectors.graylog.schema.collector import (
4 + ConfiguredInput,
5 + ConfiguredInputsResponse,
6 + GraylogIndexItem,
7 + GraylogIndicesResponse,
8 + GraylogInputsResponse,
9 + RunningInput,
10 + RunningInputsResponse,
11 +)
12 from app.connectors.graylog.schema.management import UrlWhitelistEntryResponse
13 from app.connectors.graylog.utils.universal import send_get_request
14 +from fastapi import HTTPException
15 +from loguru import logger
16
17
18 async def get_indices_full() -> GraylogIndicesResponse:
@@ -33,11 +33,22 @@ async def get_indices_full() -> GraylogIndicesResponse:
33 raise HTTPException(status_code=500, detail="Failed to collect indices key")
34
35 # Convert the dictionary to a list of GraylogIndexItem
36 - indices_list = [GraylogIndexItem(index_name=name, index_info=info) for name, info in indices_data.items()]
36 + indices_list = [
37 + GraylogIndexItem(index_name=name, index_info=info)
38 + for name, info in indices_data.items()
39 + ]
40
38 - return GraylogIndicesResponse(indices=indices_list, success=True, message="Indices collected successfully")
41 + return GraylogIndicesResponse(
42 + indices=indices_list,
43 + success=True,
44 + message="Indices collected successfully",
45 + )
46 else:
40 - return GraylogIndicesResponse(indices=[], success=False, message="Failed to collect indices")
47 + return GraylogIndicesResponse(
48 + indices=[],
49 + success=False,
50 + message="Failed to collect indices",
51 + )
52
53
54 async def fetch_configured_inputs() -> Tuple[bool, List[ConfiguredInput]]:
@@ -51,7 +62,10 @@ async def fetch_configured_inputs() -> Tuple[bool, List[ConfiguredInput]]:
62 success = configured_inputs_collected.get("success", False)
63
64 if success:
54 - return True, [ConfiguredInput(**input_data) for input_data in configured_inputs_collected["data"]["inputs"]]
65 + return True, [
66 + ConfiguredInput(**input_data)
67 + for input_data in configured_inputs_collected["data"]["inputs"]
68 + ]
69 else:
70 logger.error("Failed to fetch configured inputs")
71 return False, []
@@ -64,11 +78,16 @@ async def fetch_running_inputs() -> Tuple[bool, List[RunningInput]]:
78 Returns:
79 A tuple containing a boolean indicating the success of the request and a list of RunningInput objects.
80 """
67 - running_inputs_collected = await send_get_request(endpoint="/api/system/inputstates")
81 + running_inputs_collected = await send_get_request(
82 + endpoint="/api/system/inputstates",
83 + )
84 success = running_inputs_collected.get("success", False)
85
86 if success:
71 - return True, [RunningInput(**input_data) for input_data in running_inputs_collected["data"]["states"]]
87 + return True, [
88 + RunningInput(**input_data)
89 + for input_data in running_inputs_collected["data"]["states"]
90 + ]
91 else:
92 logger.error("Failed to fetch running inputs")
93 return False, []
@@ -104,7 +123,12 @@ async def get_inputs() -> GraylogInputsResponse:
123 )
124 else:
125 logger.error("Failed to fetch one or both types of inputs")
107 - return GraylogInputsResponse(configured_inputs=[], running_inputs=[], success=False, message="Failed to collect inputs")
126 + return GraylogInputsResponse(
127 + configured_inputs=[],
128 + running_inputs=[],
129 + success=False,
130 + message="Failed to collect inputs",
131 + )
132
133
134 async def get_inputs_running() -> RunningInputsResponse:
@@ -116,7 +140,11 @@ async def get_inputs_running() -> RunningInputsResponse:
140 logger.info("Getting running inputs from Graylog")
141 run_success, running_inputs_list = await fetch_running_inputs()
142 if run_success:
119 - return RunningInputsResponse(running_inputs=running_inputs_list, success=True, message="Successfully retrieved running inputs")
143 + return RunningInputsResponse(
144 + running_inputs=running_inputs_list,
145 + success=True,
146 + message="Successfully retrieved running inputs",
147 + )
148
149
150 async def get_inputs_configured() -> ConfiguredInputsResponse:
@@ -185,11 +213,18 @@ async def get_url_whitelist_entries() -> UrlWhitelistEntryResponse:
213 try:
214 url_whitelist_entries = response["data"]
215 except KeyError:
188 - raise HTTPException(status_code=500, detail="Failed to collect URL whitelist entries")
216 + raise HTTPException(
217 + status_code=500,
218 + detail="Failed to collect URL whitelist entries",
219 + )
220 return UrlWhitelistEntryResponse(
221 url_whitelist_entries=url_whitelist_entries,
222 success=True,
223 message="URL whitelist entries collected successfully",
224 )
225 else:
195 - return UrlWhitelistEntryResponse(url_whitelist_entries=[], success=False, message="Failed to collect URL whitelist entries")
226 + return UrlWhitelistEntryResponse(
227 + url_whitelist_entries=[],
228 + success=False,
229 + message="Failed to collect URL whitelist entries",
230 + )
backend/app/connectors/graylog/services/events.py
+47 -20
@@ -1,17 +1,17 @@
1 +from app.connectors.graylog.schema.events import (
2 + AlertEvent,
3 + AlertQuery,
4 + Alerts,
5 + Context,
6 + EventDefinition,
7 + GraylogAlertsResponse,
8 + GraylogEventDefinitionsResponse,
9 + Parameters,
10 +)
11 +from app.connectors.graylog.utils.universal import send_get_request, send_post_request
12 from fastapi import HTTPException
13 from loguru import logger
14
4 -from app.connectors.graylog.schema.events import AlertEvent
5 -from app.connectors.graylog.schema.events import AlertQuery
6 -from app.connectors.graylog.schema.events import Alerts
7 -from app.connectors.graylog.schema.events import Context
8 -from app.connectors.graylog.schema.events import EventDefinition
9 -from app.connectors.graylog.schema.events import GraylogAlertsResponse
10 -from app.connectors.graylog.schema.events import GraylogEventDefinitionsResponse
11 -from app.connectors.graylog.schema.events import Parameters
12 -from app.connectors.graylog.utils.universal import send_get_request
13 -from app.connectors.graylog.utils.universal import send_post_request
14 -
15
16 async def get_event_definitions() -> GraylogEventDefinitionsResponse:
17 """Get event definitions from Graylog.
@@ -20,15 +20,25 @@ async def get_event_definitions() -> GraylogEventDefinitionsResponse:
20 GraylogEventDefinitionsResponse: The response containing the event definitions.
21 """
22 logger.info("Getting event definitions from Graylog")
23 - event_definitions_collected = await send_get_request(endpoint="/api/events/definitions")
23 + event_definitions_collected = await send_get_request(
24 + endpoint="/api/events/definitions",
25 + )
26 if event_definitions_collected["success"]:
27 try:
26 - event_definitions_data = event_definitions_collected["data"]["event_definitions"]
28 + event_definitions_data = event_definitions_collected["data"][
29 + "event_definitions"
30 + ]
31 except KeyError:
28 - raise HTTPException(status_code=500, detail="Failed to collect event definitions key")
32 + raise HTTPException(
33 + status_code=500,
34 + detail="Failed to collect event definitions key",
35 + )
36
37 # Convert the dictionary to a list of GraylogIndexItem
31 - event_definitions_list = [EventDefinition(**event_definition_data) for event_definition_data in event_definitions_data]
38 + event_definitions_list = [
39 + EventDefinition(**event_definition_data)
40 + for event_definition_data in event_definitions_data
41 + ]
42
43 return GraylogEventDefinitionsResponse(
44 event_definitions=event_definitions_list,
@@ -36,7 +46,11 @@ async def get_event_definitions() -> GraylogEventDefinitionsResponse:
46 message="Event definitions collected successfully",
47 )
48 else:
39 - return GraylogEventDefinitionsResponse(event_definitions=[], success=False, message="Failed to collect event definitions")
49 + return GraylogEventDefinitionsResponse(
50 + event_definitions=[],
51 + success=False,
52 + message="Failed to collect event definitions",
53 + )
54
55
56 async def get_alerts(alert_query: AlertQuery) -> GraylogAlertsResponse:
@@ -53,7 +67,10 @@ async def get_alerts(alert_query: AlertQuery) -> GraylogAlertsResponse:
67 HTTPException: If there is an error collecting the alerts.
68 """
69 logger.info("Getting alerts from Graylog")
56 - response = await send_post_request(endpoint="/api/events/search", data=alert_query.dict())
70 + response = await send_post_request(
71 + endpoint="/api/events/search",
72 + data=alert_query.dict(),
73 + )
74
75 if response["success"]:
76 try:
@@ -61,7 +78,9 @@ async def get_alerts(alert_query: AlertQuery) -> GraylogAlertsResponse:
78 except KeyError:
79 raise HTTPException(status_code=500, detail="Failed to collect data key")
80 # Convert raw event data to Event objects
64 - event_objects = [AlertEvent(**event_data) for event_data in raw_alerts_data["events"]]
81 + event_objects = [
82 + AlertEvent(**event_data) for event_data in raw_alerts_data["events"]
83 + ]
84
85 # Build the Alerts object
86 alerts = Alerts(
@@ -74,9 +93,17 @@ async def get_alerts(alert_query: AlertQuery) -> GraylogAlertsResponse:
93 )
94
95 # Build the final GraylogAlertsResponse
77 - final_response = GraylogAlertsResponse(alerts=alerts, message="Successfully collected alerts", success=True)
96 + final_response = GraylogAlertsResponse(
97 + alerts=alerts,
98 + message="Successfully collected alerts",
99 + success=True,
100 + )
101
102 logger.info(f"Events collected: {event_objects}")
103 return final_response
104 else:
82 - return GraylogAlertsResponse(alerts=Alerts(events=[]), success=False, message="Failed to collect alerts")
105 + return GraylogAlertsResponse(
106 + alerts=Alerts(events=[]),
107 + success=False,
108 + message="Failed to collect alerts",
109 + )
backend/app/connectors/graylog/services/management.py
+54 -24
@@ -1,19 +1,22 @@
1 -from loguru import logger
2 -
3 -from app.connectors.graylog.schema.management import DeletedIndexBody
4 -from app.connectors.graylog.schema.management import DeletedIndexResponse
5 -from app.connectors.graylog.schema.management import StartInputBody
6 -from app.connectors.graylog.schema.management import StartInputResponse
7 -from app.connectors.graylog.schema.management import StartStreamBody
8 -from app.connectors.graylog.schema.management import StartStreamResponse
9 -from app.connectors.graylog.schema.management import StopInputBody
10 -from app.connectors.graylog.schema.management import StopInputResponse
11 -from app.connectors.graylog.schema.management import StopStreamBody
12 -from app.connectors.graylog.schema.management import StopStreamResponse
1 +from app.connectors.graylog.schema.management import (
2 + DeletedIndexBody,
3 + DeletedIndexResponse,
4 + StartInputBody,
5 + StartInputResponse,
6 + StartStreamBody,
7 + StartStreamResponse,
8 + StopInputBody,
9 + StopInputResponse,
10 + StopStreamBody,
11 + StopStreamResponse,
12 +)
13 from app.connectors.graylog.services.collector import get_index_names
14 -from app.connectors.graylog.utils.universal import send_delete_request
15 -from app.connectors.graylog.utils.universal import send_post_request
16 -from app.connectors.graylog.utils.universal import send_put_request
14 +from app.connectors.graylog.utils.universal import (
15 + send_delete_request,
16 + send_post_request,
17 + send_put_request,
18 +)
19 +from loguru import logger
20
21
22 async def delete_index(index_name: DeletedIndexBody) -> DeletedIndexResponse:
@@ -36,7 +39,10 @@ async def delete_index(index_name: DeletedIndexBody) -> DeletedIndexResponse:
39 message=f"Failed to delete index {index_name}. If the index is still in use, it cannot be deleted.",
40 )
41 else:
39 - return DeletedIndexResponse(success=True, message=f"Successfully deleted index {index_name}")
42 + return DeletedIndexResponse(
43 + success=True,
44 + message=f"Successfully deleted index {index_name}",
45 + )
46
47
48 async def stop_input(input_id: StopInputBody) -> StopInputResponse:
@@ -51,9 +57,15 @@ async def stop_input(input_id: StopInputBody) -> StopInputResponse:
57 logger.info(f"Stopping input {input_id} in Graylog")
58 response = await send_delete_request(endpoint=f"/api/system/inputstates/{input_id}")
59 if response["success"]:
54 - return StopInputResponse(success=True, message=f"Successfully stopped input {input_id}")
60 + return StopInputResponse(
61 + success=True,
62 + message=f"Successfully stopped input {input_id}",
63 + )
64 else:
56 - return StopInputResponse(success=False, message=f"Failed to stop input {input_id}")
65 + return StopInputResponse(
66 + success=False,
67 + message=f"Failed to stop input {input_id}",
68 + )
69
70
71 async def start_input(input_id: StartInputBody) -> StartInputResponse:
@@ -68,9 +80,15 @@ async def start_input(input_id: StartInputBody) -> StartInputResponse:
80 logger.info(f"Starting input {input_id} in Graylog")
81 response = await send_put_request(endpoint=f"/api/system/inputstates/{input_id}")
82 if response["success"]:
71 - return StartInputResponse(success=True, message=f"Successfully started input {input_id}")
83 + return StartInputResponse(
84 + success=True,
85 + message=f"Successfully started input {input_id}",
86 + )
87 else:
73 - return StartInputResponse(success=False, message=f"Failed to start input {input_id}")
88 + return StartInputResponse(
89 + success=False,
90 + message=f"Failed to start input {input_id}",
91 + )
92
93
94 async def stop_stream(stream_id: StopStreamBody) -> StopStreamResponse:
@@ -86,9 +104,15 @@ async def stop_stream(stream_id: StopStreamBody) -> StopStreamResponse:
104 response = await send_post_request(endpoint=f"/api/streams/{stream_id}/pause")
105 logger.info(f"Response: {response}")
106 if response["success"]:
89 - return StopStreamResponse(success=True, message=f"Successfully stopped stream {stream_id}")
107 + return StopStreamResponse(
108 + success=True,
109 + message=f"Successfully stopped stream {stream_id}",
110 + )
111 else:
91 - return StopStreamResponse(success=False, message=f"Failed to stop stream {stream_id}")
112 + return StopStreamResponse(
113 + success=False,
114 + message=f"Failed to stop stream {stream_id}",
115 + )
116
117
118 async def start_stream(stream_id: StartStreamBody) -> StartStreamResponse:
@@ -103,6 +127,12 @@ async def start_stream(stream_id: StartStreamBody) -> StartStreamResponse:
127 logger.info(f"Starting stream {stream_id} in Graylog")
128 response = await send_post_request(endpoint=f"/api/streams/{stream_id}/resume")
129 if response["success"]:
106 - return StartStreamResponse(success=True, message=f"Successfully started stream {stream_id}")
130 + return StartStreamResponse(
131 + success=True,
132 + message=f"Successfully started stream {stream_id}",
133 + )
134 else:
108 - return StartStreamResponse(success=False, message=f"Failed to start stream {stream_id}")
135 + return StartStreamResponse(
136 + success=False,
137 + message=f"Failed to start stream {stream_id}",
138 + )
backend/app/connectors/graylog/services/monitoring.py
+57 -21
@@ -1,15 +1,16 @@
1 +from app.connectors.graylog.schema.monitoring import (
2 + GraylogEventNotificationsResponse,
3 + GraylogMessages,
4 + GraylogMessagesResponse,
5 + GraylogMetricsResponse,
6 + GraylogThroughputMetrics,
7 + GraylogThroughputMetricsCollection,
8 + GraylogUncommittedJournalEntries,
9 +)
10 +from app.connectors.graylog.utils.universal import send_get_request
11 from fastapi import HTTPException
12 from loguru import logger
13
4 -from app.connectors.graylog.schema.monitoring import GraylogEventNotificationsResponse
5 -from app.connectors.graylog.schema.monitoring import GraylogMessages
6 -from app.connectors.graylog.schema.monitoring import GraylogMessagesResponse
7 -from app.connectors.graylog.schema.monitoring import GraylogMetricsResponse
8 -from app.connectors.graylog.schema.monitoring import GraylogThroughputMetrics
9 -from app.connectors.graylog.schema.monitoring import GraylogThroughputMetricsCollection
10 -from app.connectors.graylog.schema.monitoring import GraylogUncommittedJournalEntries
11 -from app.connectors.graylog.utils.universal import send_get_request
12 -
14
15 async def get_messages(page_number: int) -> GraylogMessagesResponse:
16 """Get messages from Graylog.
@@ -25,7 +26,10 @@ async def get_messages(page_number: int) -> GraylogMessagesResponse:
26 """
27 logger.info("Getting messages from Graylog")
28 params = {"page": page_number}
28 - messages_collected = await send_get_request(endpoint="/api/system/messages", params=params)
29 + messages_collected = await send_get_request(
30 + endpoint="/api/system/messages",
31 + params=params,
32 + )
33 try:
34 if messages_collected["success"]:
35 graylog_messages_list = []
@@ -46,11 +50,18 @@ async def get_messages(page_number: int) -> GraylogMessagesResponse:
50
51 except KeyError as e:
52 logger.error(f"Failed to collect messages key: {e}")
49 - raise HTTPException(status_code=500, detail=f"Failed to collect messages key: {e}")
53 + raise HTTPException(
54 + status_code=500,
55 + detail=f"Failed to collect messages key: {e}",
56 + )
57 except Exception as e:
58 logger.error(f"Failed to collect messages: {e}")
59 raise HTTPException(status_code=500, detail=f"Failed to collect messages: {e}")
53 - return GraylogMessagesResponse(graylog_messages=[], success=False, message="Failed to collect messages")
60 + return GraylogMessagesResponse(
61 + graylog_messages=[],
62 + success=False,
63 + message="Failed to collect messages",
64 + )
65
66
67 async def fetch_metrics_from_graylog() -> dict:
@@ -98,7 +109,10 @@ def filter_and_create_throughput_metrics(merged_metrics: dict) -> list:
109 Returns:
110 list: A list of GraylogThroughputMetrics objects.
111 """
101 - model_fields = [field_info.alias for field_info in GraylogThroughputMetricsCollection.__fields__.values()]
112 + model_fields = [
113 + field_info.alias
114 + for field_info in GraylogThroughputMetricsCollection.__fields__.values()
115 + ]
116 throughput_metrics_list = [
117 GraylogThroughputMetrics(metric=metric_name, value=metric_data.get("value", 0))
118 for metric_name, metric_data in merged_metrics.items()
@@ -118,12 +132,19 @@ async def get_metrics() -> GraylogMetricsResponse:
132 throughput_metrics_collected = await fetch_metrics_from_graylog()
133 uncommitted_journal_entries_collected = await fetch_uncommitted_journal_entries()
134 try:
121 - if throughput_metrics_collected["success"] and uncommitted_journal_entries_collected["success"]:
135 + if (
136 + throughput_metrics_collected["success"]
137 + and uncommitted_journal_entries_collected["success"]
138 + ):
139 merged_metrics = merge_metrics_data(throughput_metrics_collected)
123 - throughput_metrics_list = filter_and_create_throughput_metrics(merged_metrics)
140 + throughput_metrics_list = filter_and_create_throughput_metrics(
141 + merged_metrics,
142 + )
143
144 uncommitted_journal_entries = GraylogUncommittedJournalEntries(
126 - uncommitted_journal_entries=uncommitted_journal_entries_collected["data"]["uncommitted_journal_entries"],
145 + uncommitted_journal_entries=uncommitted_journal_entries_collected[
146 + "data"
147 + ]["uncommitted_journal_entries"],
148 )
149
150 return GraylogMetricsResponse(
@@ -133,7 +154,10 @@ async def get_metrics() -> GraylogMetricsResponse:
154 message="Metrics collected successfully",
155 )
156 except KeyError as e:
136 - raise HTTPException(status_code=500, detail=f"Failed to collect metrics key: {e}")
157 + raise HTTPException(
158 + status_code=500,
159 + detail=f"Failed to collect metrics key: {e}",
160 + )
161 except Exception as e:
162 raise HTTPException(status_code=500, detail=f"Failed to collect metrics: {e}")
163
@@ -153,7 +177,9 @@ async def get_event_notifications() -> GraylogEventNotificationsResponse:
177 GraylogEventNotificationsResponse: The response object containing the collected event notifications.
178 """
179 logger.info("Getting event notifications from Graylog")
156 - event_notifications_collected = await send_get_request(endpoint="/api/events/notifications")
180 + event_notifications_collected = await send_get_request(
181 + endpoint="/api/events/notifications",
182 + )
183 try:
184 if event_notifications_collected["success"]:
185 return GraylogEventNotificationsResponse(
@@ -162,8 +188,18 @@ async def get_event_notifications() -> GraylogEventNotificationsResponse:
188 message="Event notifications collected successfully",
189 )
190 except KeyError as e:
165 - raise HTTPException(status_code=500, detail=f"Failed to collect event notifications key: {e}")
191 + raise HTTPException(
192 + status_code=500,
193 + detail=f"Failed to collect event notifications key: {e}",
194 + )
195 except Exception as e:
167 - raise HTTPException(status_code=500, detail=f"Failed to collect event notifications: {e}")
196 + raise HTTPException(
197 + status_code=500,
198 + detail=f"Failed to collect event notifications: {e}",
199 + )
200
169 - return GraylogEventNotificationsResponse(event_notifications=[], success=False, message="Failed to collect event notifications")
201 + return GraylogEventNotificationsResponse(
202 + event_notifications=[],
203 + success=False,
204 + message="Failed to collect event notifications",
205 + )
backend/app/connectors/graylog/services/pipelines.py
+83 -29
@@ -1,17 +1,19 @@
1 +from app.connectors.graylog.schema.pipelines import (
2 + CreatePipeline,
3 + CreatePipelineRule,
4 + GraylogPipelinesResponse,
5 + Pipeline,
6 + PipelineRule,
7 + PipelineRulesResponse,
8 +)
9 +from app.connectors.graylog.utils.universal import send_get_request, send_post_request
10 +from app.customer_provisioning.schema.graylog import (
11 + StreamConnectionToPipelineRequest,
12 + StreamConnectionToPipelineResponse,
13 +)
14 from fastapi import HTTPException
15 from loguru import logger
16
4 -from app.connectors.graylog.schema.pipelines import CreatePipeline
5 -from app.connectors.graylog.schema.pipelines import CreatePipelineRule
6 -from app.connectors.graylog.schema.pipelines import GraylogPipelinesResponse
7 -from app.connectors.graylog.schema.pipelines import Pipeline
8 -from app.connectors.graylog.schema.pipelines import PipelineRule
9 -from app.connectors.graylog.schema.pipelines import PipelineRulesResponse
10 -from app.connectors.graylog.utils.universal import send_get_request
11 -from app.connectors.graylog.utils.universal import send_post_request
12 -from app.customer_provisioning.schema.graylog import StreamConnectionToPipelineRequest
13 -from app.customer_provisioning.schema.graylog import StreamConnectionToPipelineResponse
14 -
17
18 async def get_pipelines() -> GraylogPipelinesResponse:
19 """Get pipelines from Graylog.
@@ -23,14 +25,26 @@ async def get_pipelines() -> GraylogPipelinesResponse:
25 HTTPException: If there is an error collecting the pipelines.
26 """
27 logger.info("Getting pipelines from Graylog")
26 - pipelines_collected = await send_get_request(endpoint="/api/system/pipelines/pipeline")
28 + pipelines_collected = await send_get_request(
29 + endpoint="/api/system/pipelines/pipeline",
30 + )
31 try:
32 if pipelines_collected["success"]:
29 - pipelines_list = [Pipeline(**pipeline_data) for pipeline_data in pipelines_collected["data"]]
30 - return GraylogPipelinesResponse(pipelines=pipelines_list, success=True, message="Pipelines collected successfully")
33 + pipelines_list = [
34 + Pipeline(**pipeline_data)
35 + for pipeline_data in pipelines_collected["data"]
36 + ]
37 + return GraylogPipelinesResponse(
38 + pipelines=pipelines_list,
39 + success=True,
40 + message="Pipelines collected successfully",
41 + )
42 except KeyError as e:
43 logger.error(f"Failed to collect pipelines key: {e}")
33 - raise HTTPException(status_code=500, detail=f"Failed to collect pipelines key: {e}")
44 + raise HTTPException(
45 + status_code=500,
46 + detail=f"Failed to collect pipelines key: {e}",
47 + )
48 except Exception as e:
49 logger.error(f"Failed to collect pipelines: {e}")
50 raise HTTPException(status_code=500, detail=f"Failed to collect pipelines: {e}")
@@ -44,17 +58,32 @@ async def get_pipeline_rules() -> PipelineRulesResponse:
58 PipelineRulesResponse: The response object containing the pipeline rules.
59 """
60 logger.info("Getting pipeline rules from Graylog")
47 - pipeline_rules_collected = await send_get_request(endpoint="/api/system/pipelines/rule")
61 + pipeline_rules_collected = await send_get_request(
62 + endpoint="/api/system/pipelines/rule",
63 + )
64 try:
65 if pipeline_rules_collected["success"]:
50 - pipeline_rules_list = [PipelineRule(**pipeline_rule_data) for pipeline_rule_data in pipeline_rules_collected["data"]]
51 - return PipelineRulesResponse(pipeline_rules=pipeline_rules_list, success=True, message="Pipeline rules collected successfully")
66 + pipeline_rules_list = [
67 + PipelineRule(**pipeline_rule_data)
68 + for pipeline_rule_data in pipeline_rules_collected["data"]
69 + ]
70 + return PipelineRulesResponse(
71 + pipeline_rules=pipeline_rules_list,
72 + success=True,
73 + message="Pipeline rules collected successfully",
74 + )
75 except KeyError as e:
76 logger.error(f"Failed to collect pipeline rules key: {e}")
54 - raise HTTPException(status_code=500, detail=f"Failed to collect pipeline rules key: {e}")
77 + raise HTTPException(
78 + status_code=500,
79 + detail=f"Failed to collect pipeline rules key: {e}",
80 + )
81 except Exception as e:
82 logger.error(f"Failed to collect pipeline rules: {e}")
57 - raise HTTPException(status_code=500, detail=f"Failed to collect pipeline rules: {e}")
83 + raise HTTPException(
84 + status_code=500,
85 + detail=f"Failed to collect pipeline rules: {e}",
86 + )
87
88
89 async def get_pipeline_rule_by_id(rule_id) -> PipelineRulesResponse:
@@ -71,18 +100,30 @@ async def get_pipeline_rule_by_id(rule_id) -> PipelineRulesResponse:
100 HTTPException: If there is an error collecting the pipeline rules.
101 """
102 logger.info(f"Getting pipeline rules from Graylog for pipeline {rule_id}")
74 - pipeline_rules_collected = await send_get_request(endpoint=f"/api/system/pipelines/rule/{rule_id}")
103 + pipeline_rules_collected = await send_get_request(
104 + endpoint=f"/api/system/pipelines/rule/{rule_id}",
105 + )
106 logger.info(pipeline_rules_collected)
107 try:
108 if pipeline_rules_collected["success"]:
109 pipeline_rule = PipelineRule(**pipeline_rules_collected["data"])
79 - return PipelineRulesResponse(pipeline_rules=[pipeline_rule], success=True, message="Pipeline rules collected successfully")
110 + return PipelineRulesResponse(
111 + pipeline_rules=[pipeline_rule],
112 + success=True,
113 + message="Pipeline rules collected successfully",
114 + )
115 except KeyError as e:
116 logger.error(f"Failed to collect pipeline rules key: {e}")
82 - raise HTTPException(status_code=500, detail=f"Failed to collect pipeline rules key: {e}")
117 + raise HTTPException(
118 + status_code=500,
119 + detail=f"Failed to collect pipeline rules key: {e}",
120 + )
121 except Exception as e:
122 logger.error(f"Failed to collect pipeline rules: {e}")
85 - raise HTTPException(status_code=500, detail=f"Failed to collect pipeline rules: {e}")
123 + raise HTTPException(
124 + status_code=500,
125 + detail=f"Failed to collect pipeline rules: {e}",
126 + )
127
128
129 async def create_pipeline_rule(rule: CreatePipelineRule) -> None:
@@ -131,13 +172,21 @@ async def get_pipeline_id(subscription: str) -> str:
172 if subscription.lower() in pipeline.description.lower():
173 return [pipeline.id]
174 logger.error(f"Failed to get pipeline ID for subscription {subscription}")
134 - raise HTTPException(status_code=500, detail=f"Failed to get pipeline ID for subscription {subscription}")
175 + raise HTTPException(
176 + status_code=500,
177 + detail=f"Failed to get pipeline ID for subscription {subscription}",
178 + )
179 else:
180 logger.error(f"Failed to get pipelines: {pipelines_response.message}")
137 - raise HTTPException(status_code=500, detail=f"Failed to get pipelines: {pipelines_response.message}")
181 + raise HTTPException(
182 + status_code=500,
183 + detail=f"Failed to get pipelines: {pipelines_response.message}",
184 + )
185
186
140 -async def connect_stream_to_pipeline(stream_and_pipeline: StreamConnectionToPipelineRequest):
187 +async def connect_stream_to_pipeline(
188 + stream_and_pipeline: StreamConnectionToPipelineRequest,
189 +):
190 """
191 Connects a stream to a pipeline.
192
@@ -147,7 +196,12 @@ async def connect_stream_to_pipeline(stream_and_pipeline: StreamConnectionToPipe
196 Returns:
197 StreamConnectionToPipelineResponse: The response object containing the connection details.
198 """
150 - logger.info(f"Connecting stream {stream_and_pipeline.stream_id} to pipeline {stream_and_pipeline.pipeline_ids}")
151 - response_json = await send_post_request(endpoint="/api/system/pipelines/connections/to_stream", data=stream_and_pipeline.dict())
199 + logger.info(
200 + f"Connecting stream {stream_and_pipeline.stream_id} to pipeline {stream_and_pipeline.pipeline_ids}",
201 + )
202 + response_json = await send_post_request(
203 + endpoint="/api/system/pipelines/connections/to_stream",
204 + data=stream_and_pipeline.dict(),
205 + )
206 logger.info(f"Response: {response_json}")
207 return StreamConnectionToPipelineResponse(**response_json)
backend/app/connectors/graylog/services/streams.py
+24 -9
@@ -1,12 +1,10 @@
1 from typing import List
2
3 +from app.connectors.graylog.schema.streams import GraylogStreamsResponse, Stream
4 +from app.connectors.graylog.utils.universal import send_get_request
5 from fastapi import HTTPException
6 from loguru import logger
7
6 -from app.connectors.graylog.schema.streams import GraylogStreamsResponse
7 -from app.connectors.graylog.schema.streams import Stream
8 -from app.connectors.graylog.utils.universal import send_get_request
9 -
8
9 async def get_streams() -> GraylogStreamsResponse:
10 """Get streams from Graylog.
@@ -21,7 +19,10 @@ async def get_streams() -> GraylogStreamsResponse:
19 streams_collected = await send_get_request(endpoint="/api/streams")
20 try:
21 if streams_collected["success"]:
24 - streams_list = [Stream(**stream_data) for stream_data in streams_collected["data"]["streams"]]
22 + streams_list = [
23 + Stream(**stream_data)
24 + for stream_data in streams_collected["data"]["streams"]
25 + ]
26 return GraylogStreamsResponse(
27 streams=streams_list,
28 success=True,
@@ -29,10 +30,18 @@ async def get_streams() -> GraylogStreamsResponse:
30 total=streams_collected["data"]["total"],
31 )
32 else:
32 - return GraylogStreamsResponse(streams=[], success=False, message="Failed to collect streams", total=0)
33 + return GraylogStreamsResponse(
34 + streams=[],
35 + success=False,
36 + message="Failed to collect streams",
37 + total=0,
38 + )
39 except KeyError as e:
40 logger.error(f"Failed to collect streams key: {e}")
35 - raise HTTPException(status_code=500, detail=f"Failed to collect streams key: {e}")
41 + raise HTTPException(
42 + status_code=500,
43 + detail=f"Failed to collect streams key: {e}",
44 + )
45 except Exception as e:
46 logger.error(f"Failed to collect streams: {e}")
47 raise HTTPException(status_code=500, detail=f"Failed to collect streams: {e}")
@@ -51,12 +60,18 @@ async def get_stream_ids() -> List[str]:
60 streams_collected = await send_get_request(endpoint="/api/streams")
61 try:
62 if streams_collected["success"]:
54 - return [stream_data["id"] for stream_data in streams_collected["data"]["streams"]]
63 + return [
64 + stream_data["id"]
65 + for stream_data in streams_collected["data"]["streams"]
66 + ]
67 else:
68 return []
69 except KeyError as e:
70 logger.error(f"Failed to collect streams key: {e}")
59 - raise HTTPException(status_code=500, detail=f"Failed to collect streams key: {e}")
71 + raise HTTPException(
72 + status_code=500,
73 + detail=f"Failed to collect streams key: {e}",
74 + )
75 except Exception as e:
76 logger.error(f"Failed to collect streams: {e}")
77 raise HTTPException(status_code=500, detail=f"Failed to collect streams: {e}")
backend/app/connectors/graylog/utils/universal.py
+89 -25
@@ -1,13 +1,10 @@
1 -from typing import Any
2 -from typing import Dict
3 -from typing import Optional
1 +from typing import Any, Dict, Optional
2
3 import requests
6 -from fastapi import HTTPException
7 -from loguru import logger
8 -
4 from app.connectors.utils import get_connector_info_from_db
5 from app.db.db_session import get_db_session
6 +from fastapi import HTTPException
7 +from loguru import logger
8
9 HEADERS = {"X-Requested-By": "CoPilot"}
10
@@ -35,7 +32,10 @@ async def verify_graylog_credentials(attributes: Dict[str, Any]) -> Dict[str, An
32 logger.info(
33 f"Connection to {attributes['connector_url']} successful",
34 )
38 - return {"connectionSuccessful": True, "message": "Graylog connection successful"}
35 + return {
36 + "connectionSuccessful": True,
37 + "message": "Graylog connection successful",
38 + }
39 else:
40 logger.error(
41 f"Connection to {attributes['connector_url']} failed with error: {graylog_roles.text}",
@@ -48,7 +48,10 @@ async def verify_graylog_credentials(attributes: Dict[str, Any]) -> Dict[str, An
48 logger.error(
49 f"Connection to {attributes['connector_url']} failed with error: {e}",
50 )
51 - return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error: {e}"}
51 + return {
52 + "connectionSuccessful": False,
53 + "message": f"Connection to {attributes['connector_url']} failed with error: {e}",
54 + }
55
56
57 async def verify_graylog_connection(connector_name: str) -> str:
@@ -64,7 +67,11 @@ async def verify_graylog_connection(connector_name: str) -> str:
67 return await verify_graylog_credentials(attributes)
68
69
67 -async def send_get_request(endpoint: str, params: Optional[Dict[str, Any]] = None, connector_name: str = "Graylog") -> Dict[str, Any]:
70 +async def send_get_request(
71 + endpoint: str,
72 + params: Optional[Dict[str, Any]] = None,
73 + connector_name: str = "Graylog",
74 +) -> Dict[str, Any]:
75 """
76 Sends a GET request to the Graylog service.
77
@@ -98,14 +105,25 @@ async def send_get_request(endpoint: str, params: Optional[Dict[str, Any]] = Non
105 status_code=404,
106 detail=f"Failed to send GET request to {endpoint} with error: {response.json()['message']}",
107 )
101 - return {"data": response.json(), "success": True, "message": "Successfully retrieved data"}
108 + return {
109 + "data": response.json(),
110 + "success": True,
111 + "message": "Successfully retrieved data",
112 + }
113 except HTTPException as e:
114 raise e
115 except Exception as e:
105 - raise HTTPException(status_code=500, detail=f"Failed to send GET request to {endpoint} with error: {e}")
116 + raise HTTPException(
117 + status_code=500,
118 + detail=f"Failed to send GET request to {endpoint} with error: {e}",
119 + )
120
121
108 -async def send_post_request(endpoint: str, data: Dict[str, Any] = None, connector_name: str = "Graylog") -> Dict[str, Any]:
122 +async def send_post_request(
123 + endpoint: str,
124 + data: Dict[str, Any] = None,
125 + connector_name: str = "Graylog",
126 +) -> Dict[str, Any]:
127 """
128 Sends a POST request to the Graylog service.
129
@@ -122,7 +140,10 @@ async def send_post_request(endpoint: str, data: Dict[str, Any] = None, connecto
140 attributes = await get_connector_info_from_db(connector_name, session)
141 if attributes is None:
142 logger.error("No Graylog connector found in the database")
125 - return {"success": False, "message": "No Graylog connector found in the database"}
143 + return {
144 + "success": False,
145 + "message": "No Graylog connector found in the database",
146 + }
147
148 try:
149 response = requests.post(
@@ -137,11 +158,23 @@ async def send_post_request(endpoint: str, data: Dict[str, Any] = None, connecto
158 )
159
160 if response.status_code == 200:
140 - return {"data": response.json(), "success": True, "message": "Successfully completed request"}
161 + return {
162 + "data": response.json(),
163 + "success": True,
164 + "message": "Successfully completed request",
165 + }
166 elif response.status_code == 204:
142 - return {"data": None, "success": True, "message": "Successfully completed request with no content"}
167 + return {
168 + "data": None,
169 + "success": True,
170 + "message": "Successfully completed request with no content",
171 + }
172 elif response.status_code == 201:
144 - return {"data": response.json(), "success": True, "message": "Successfully created data"}
173 + return {
174 + "data": response.json(),
175 + "success": True,
176 + "message": "Successfully created data",
177 + }
178 else:
179 raise HTTPException(
180 status_code=500,
@@ -150,10 +183,17 @@ async def send_post_request(endpoint: str, data: Dict[str, Any] = None, connecto
183 except HTTPException as e:
184 raise e
185 except Exception as e:
153 - raise HTTPException(status_code=500, detail=f"Failed to send POST request to {endpoint} with error: {e}")
186 + raise HTTPException(
187 + status_code=500,
188 + detail=f"Failed to send POST request to {endpoint} with error: {e}",
189 + )
190
191
156 -async def send_delete_request(endpoint: str, params: Optional[Dict[str, Any]] = None, connector_name: str = "Graylog") -> Dict[str, Any]:
192 +async def send_delete_request(
193 + endpoint: str,
194 + params: Optional[Dict[str, Any]] = None,
195 + connector_name: str = "Graylog",
196 +) -> Dict[str, Any]:
197 """
198 Sends a DELETE request to the Graylog service.
199
@@ -187,15 +227,26 @@ async def send_delete_request(endpoint: str, params: Optional[Dict[str, Any]] =
227 status_code=404,
228 detail=f"Failed to send DELETE request to {endpoint} with error: {response.json()['message']}",
229 )
190 - return {"data": "No content returned", "success": True, "message": "Successfully deleted data"}
230 + return {
231 + "data": "No content returned",
232 + "success": True,
233 + "message": "Successfully deleted data",
234 + }
235 except HTTPException as e:
236 raise e
237 except Exception as e:
238 logger.error(f"Failed to send DELETE request to {endpoint} with error: {e}")
195 - return {"success": False, "message": f"Failed to send DELETE request to {endpoint} with error: {e}"}
239 + return {
240 + "success": False,
241 + "message": f"Failed to send DELETE request to {endpoint} with error: {e}",
242 + }
243
244
198 -async def send_put_request(endpoint: str, data: Optional[Dict[str, Any]] = None, connector_name: str = "Graylog") -> Dict[str, Any]:
245 +async def send_put_request(
246 + endpoint: str,
247 + data: Optional[Dict[str, Any]] = None,
248 + connector_name: str = "Graylog",
249 +) -> Dict[str, Any]:
250 """
251 Sends a PUT request to the Graylog service.
252
@@ -224,17 +275,30 @@ async def send_put_request(endpoint: str, data: Optional[Dict[str, Any]] = None,
275 json=data,
276 verify=False,
277 )
227 - logger.info(f"Response from PUT request: {response.status_code} {response.text}")
278 + logger.info(
279 + f"Response from PUT request: {response.status_code} {response.text}",
280 + )
281 if response.status_code not in [200, 204]:
282 raise HTTPException(
283 status_code=404,
284 detail=f"Failed to send PUT request to {endpoint} with error: {response.json().get('message', '')}",
285 )
286 if response.status_code == 204:
234 - return {"data": None, "success": True, "message": "Successfully sent PUT request, no content returned"}
235 - return {"data": response.json(), "success": True, "message": "Successfully retrieved data"}
287 + return {
288 + "data": None,
289 + "success": True,
290 + "message": "Successfully sent PUT request, no content returned",
291 + }
292 + return {
293 + "data": response.json(),
294 + "success": True,
295 + "message": "Successfully retrieved data",
296 + }
297 except HTTPException as e:
298 raise e
299 except Exception as e:
300 logger.error(f"Failed to send PUT request to {endpoint} with error: {e}")
240 - return {"success": False, "message": f"Failed to send PUT request to {endpoint} with error: {e}"}
301 + return {
302 + "success": False,
303 + "message": f"Failed to send PUT request to {endpoint} with error: {e}",
304 + }
backend/app/connectors/influxdb/routes/alerts.py
+2 -4
@@ -1,10 +1,8 @@
1 -from fastapi import APIRouter
2 -from fastapi import Security
3 -from loguru import logger
4 -
1 from app.auth.utils import AuthHandler
2 from app.connectors.influxdb.schema.alerts import InfluxDBAlertsResponse
3 from app.connectors.influxdb.services.alerts import get_alerts
4 +from fastapi import APIRouter, Security
5 +from loguru import logger
6
7 # App specific imports
8
backend/app/connectors/influxdb/services/alerts.py
+12 -7
@@ -1,13 +1,13 @@
1 from typing import List
2
3 +from app.connectors.influxdb.schema.alerts import InfluxDBAlert, InfluxDBAlertsResponse
4 +from app.connectors.influxdb.utils.universal import (
5 + create_influxdb_client,
6 + get_influxdb_organization,
7 +)
8 from fastapi import HTTPException
9 from loguru import logger
10
6 -from app.connectors.influxdb.schema.alerts import InfluxDBAlert
7 -from app.connectors.influxdb.schema.alerts import InfluxDBAlertsResponse
8 -from app.connectors.influxdb.utils.universal import create_influxdb_client
9 -from app.connectors.influxdb.utils.universal import get_influxdb_organization
10 -
11 # Constants
12 BUCKET_NAME = "_monitoring"
13
@@ -46,7 +46,9 @@ async def process_alert_records(result) -> List[InfluxDBAlert]:
46 for table in result:
47 for record in table.records:
48 alert = InfluxDBAlert(
49 - time=record.values.get("time").isoformat() if record.values.get("time") else None,
49 + time=record.values.get("time").isoformat()
50 + if record.values.get("time")
51 + else None,
52 message=record.values.get("message"),
53 checkID=record.values.get("checkID"),
54 checkName=record.values.get("checkName"),
@@ -69,7 +71,10 @@ async def get_alerts() -> InfluxDBAlertsResponse:
71 try:
72 query = construct_query()
73 query_api = client.query_api()
72 - result = await query_api.query(org=await get_influxdb_organization(), query=query)
74 + result = await query_api.query(
75 + org=await get_influxdb_organization(),
76 + query=query,
77 + )
78
79 alerts = await process_alert_records(result)
80
backend/app/connectors/influxdb/utils/universal.py
+30 -12
@@ -1,13 +1,11 @@
1 -from typing import Any
2 -from typing import Dict
1 +from typing import Any, Dict
2
3 +from app.connectors.utils import get_connector_info_from_db
4 +from app.db.db_session import get_db_session
5 from fastapi import HTTPException
6 from influxdb_client.client.influxdb_client_async import InfluxDBClientAsync
7 from loguru import logger
8
8 -from app.connectors.utils import get_connector_info_from_db
9 -from app.db.db_session import get_db_session
10 -
9
10 async def verify_influxdb_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
11 """
@@ -27,13 +25,24 @@ async def verify_influxdb_credentials(attributes: Dict[str, Any]) -> Dict[str, A
25 logger.info(f"Response from InfluxDB: {ping}")
26 if ping:
27 logger.info(f"Connection to {attributes['connector_url']} successful")
30 - return {"connectionSuccessful": True, "message": "InfluxDB connection successful"}
28 + return {
29 + "connectionSuccessful": True,
30 + "message": "InfluxDB connection successful",
31 + }
32 else:
33 logger.error(f"Connection to {attributes['connector_url']} failed")
33 - return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed"}
34 + return {
35 + "connectionSuccessful": False,
36 + "message": f"Connection to {attributes['connector_url']} failed",
37 + }
38 except Exception as e:
35 - logger.error(f"Connection to {attributes['connector_url']} failed with error: {e}")
36 - return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error: {e}"}
39 + logger.error(
40 + f"Connection to {attributes['connector_url']} failed with error: {e}",
41 + )
42 + return {
43 + "connectionSuccessful": False,
44 + "message": f"Connection to {attributes['connector_url']} failed with error: {e}",
45 + }
46 finally:
47 # Make sure to close the client session
48 await influxdb_client.close()
@@ -66,7 +75,10 @@ async def create_influxdb_client(connector_name: str) -> InfluxDBClientAsync:
75 async with get_db_session() as session: # This will correctly enter the context manager
76 attributes = await get_connector_info_from_db(connector_name, session)
77 if attributes is None:
69 - raise HTTPException(status_code=500, detail=f"No {connector_name} connector found in the database")
78 + raise HTTPException(
79 + status_code=500,
80 + detail=f"No {connector_name} connector found in the database",
81 + )
82 try:
83 return InfluxDBClientAsync(
84 url=attributes["connector_url"],
@@ -74,7 +86,10 @@ async def create_influxdb_client(connector_name: str) -> InfluxDBClientAsync:
86 org=await get_influxdb_organization(),
87 )
88 except Exception as e:
77 - raise HTTPException(status_code=500, detail=f"Failed to create Elasticsearch client: {e}")
89 + raise HTTPException(
90 + status_code=500,
91 + detail=f"Failed to create Elasticsearch client: {e}",
92 + )
93
94
95 async def get_influxdb_organization() -> str:
@@ -85,5 +100,8 @@ async def get_influxdb_organization() -> str:
100 async with get_db_session() as session: # This will correctly enter the context manager
101 attributes = await get_connector_info_from_db("InfluxDB", session)
102 if attributes is None:
88 - raise HTTPException(status_code=500, detail="No InfluxDB connector found in the database")
103 + raise HTTPException(
104 + status_code=500,
105 + detail="No InfluxDB connector found in the database",
106 + )
107 return attributes["connector_extra_data"].split(",")[0]
backend/app/connectors/models.py
+6 -6
@@ -1,10 +1,7 @@
1 from datetime import datetime
2 -from typing import List
3 -from typing import Optional
2 +from typing import List, Optional
3
5 -from sqlmodel import Field
6 -from sqlmodel import Relationship
7 -from sqlmodel import SQLModel
4 +from sqlmodel import Field, Relationship, SQLModel
5
6
7 class ConnectorHistory(SQLModel, table=True):
@@ -71,7 +68,10 @@ class Connectors(SQLModel, table=True):
68 connector_extra_data: Optional[str] = Field(default=None)
69
70 # Relationship
74 - history_logs: List[ConnectorHistory] = Relationship(back_populates="connector", sa_relationship_kwargs={"lazy": "selectin"})
71 + history_logs: List[ConnectorHistory] = Relationship(
72 + back_populates="connector",
73 + sa_relationship_kwargs={"lazy": "selectin"},
74 + )
75
76
77 # Example usage
backend/app/connectors/routes.py
+82 -30
@@ -1,24 +1,21 @@
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 -
3 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
4 +from app.connectors.schema import (
5 + ConnectorListResponse,
6 + ConnectorResponse,
7 + ConnectorsListResponse,
8 + UpdateConnector,
9 + VerifyConnectorResponse,
10 +)
11 from app.connectors.services import ConnectorServices
12 from app.db.db_session import get_db
13
14 +## Auth Things
15 +from fastapi import APIRouter, Depends, File, HTTPException, Security, UploadFile
16 +from loguru import logger
17 +from sqlalchemy.ext.asyncio import AsyncSession
18 +
19 connector_router = APIRouter()
20
21
@@ -28,7 +25,9 @@ connector_router = APIRouter()
25 description="Fetch all available connectors",
26 dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
27 )
31 -async def get_connectors(session: AsyncSession = Depends(get_db)) -> ConnectorsListResponse:
28 +async def get_connectors(
29 + session: AsyncSession = Depends(get_db),
30 +) -> ConnectorsListResponse:
31 """
32 Fetch all available connectors from the database.
33
@@ -43,7 +42,11 @@ async def get_connectors(session: AsyncSession = Depends(get_db)) -> ConnectorsL
42 """
43 connectors = await ConnectorServices.fetch_all_connectors(session=session)
44 if connectors:
46 - return {"connectors": connectors, "success": True, "message": "Connectors fetched successfully"}
45 + return {
46 + "connectors": connectors,
47 + "success": True,
48 + "message": "Connectors fetched successfully",
49 + }
50 else:
51 raise HTTPException(status_code=404, detail="No connectors found")
52
@@ -54,7 +57,10 @@ async def get_connectors(session: AsyncSession = Depends(get_db)) -> ConnectorsL
57 description="Fetch a specific connector",
58 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
59 )
57 -async def get_connector(connector_id: int, session: AsyncSession = Depends(get_db)) -> Union[ConnectorResponse, HTTPException]:
60 +async def get_connector(
61 + connector_id: int,
62 + session: AsyncSession = Depends(get_db),
63 +) -> Union[ConnectorResponse, HTTPException]:
64 """
65 Fetch a specific connector by its ID.
66
@@ -69,11 +75,23 @@ async def get_connector(connector_id: int, session: AsyncSession = Depends(get_d
75 Raises:
76 HTTPException: An exception with a 404 status code is raised if the connector is not found.
77 """
72 - connector = await ConnectorServices.fetch_connector_by_id(connector_id, session=session)
78 + connector = await ConnectorServices.fetch_connector_by_id(
79 + connector_id,
80 + session=session,
81 + )
82 if connector is not None:
74 - return {"connector": connector, "success": True, "message": "Connector fetched successfully"}
83 + return {
84 + "connector": connector,
85 + "success": True,
86 + "message": "Connector fetched successfully",
87 + }
88 else:
76 - raise HTTPException(status_code=404, detail=f"No connector found for ID: {connector_id}".format(connector_id=connector_id))
89 + raise HTTPException(
90 + status_code=404,
91 + detail=f"No connector found for ID: {connector_id}".format(
92 + connector_id=connector_id,
93 + ),
94 + )
95
96
97 @connector_router.post(
@@ -100,11 +118,22 @@ async def verify_connector(
118 Raises:
119 HTTPException: An exception with a 404 status code is raised if the connector is not found.
120 """
103 - connector = await ConnectorServices.verify_connector_by_id(connector_id, session=session)
121 + connector = await ConnectorServices.verify_connector_by_id(
122 + connector_id,
123 + session=session,
124 + )
125 if connector is None:
105 - raise HTTPException(status_code=404, detail=f"No connector found for ID: {connector_id}".format(connector_id=connector_id))
126 + raise HTTPException(
127 + status_code=404,
128 + detail=f"No connector found for ID: {connector_id}".format(
129 + connector_id=connector_id,
130 + ),
131 + )
132 if connector["connectionSuccessful"] is False:
107 - raise HTTPException(status_code=500, detail=f"Failed to verify connector: {connector['message']}")
133 + raise HTTPException(
134 + status_code=500,
135 + detail=f"Failed to verify connector: {connector['message']}",
136 + )
137 return connector
138
139
@@ -134,12 +163,25 @@ async def update_connector(
163 Raises:
164 HTTPException: An exception with a 404 status code is raised if the connector is not found.
165 """
137 - updated_connector = await ConnectorServices.update_connector_by_id(connector_id, connector, session=session)
166 + updated_connector = await ConnectorServices.update_connector_by_id(
167 + connector_id,
168 + connector,
169 + session=session,
170 + )
171 if updated_connector is not None:
172 await ConnectorServices.verify_connector_by_id(connector_id, session=session)
140 - return {"connector": updated_connector, "success": True, "message": "Connector updated successfully"}
173 + return {
174 + "connector": updated_connector,
175 + "success": True,
176 + "message": "Connector updated successfully",
177 + }
178 else:
142 - raise HTTPException(status_code=404, detail=f"No connector found for ID: {connector_id}".format(connector_id=connector_id))
179 + raise HTTPException(
180 + status_code=404,
181 + detail=f"No connector found for ID: {connector_id}".format(
182 + connector_id=connector_id,
183 + ),
184 + )
185
186
187 @connector_router.post(
@@ -147,7 +189,11 @@ async def update_connector(
189 description="Upload a YAML file for a specific connector",
190 dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
191 )
150 -async def upload_yaml_file(connector_id: int, file: UploadFile = File(...), session: AsyncSession = Depends(get_db)) -> dict:
192 +async def upload_yaml_file(
193 + connector_id: int,
194 + file: UploadFile = File(...),
195 + session: AsyncSession = Depends(get_db),
196 +) -> dict:
197 """
198 Upload a YAML file for a specific connector ID.
199
@@ -165,13 +211,19 @@ async def upload_yaml_file(connector_id: int, file: UploadFile = File(...), sess
211 HTTPException: An exception with a 400 status code is raised if the file format is incorrect or connector ID is not 6.
212 """
213 if connector_id != 6:
168 - raise HTTPException(status_code=400, detail="Only the Velociraptor connector is allowed for YAML file uploads.")
214 + raise HTTPException(
215 + status_code=400,
216 + detail="Only the Velociraptor connector is allowed for YAML file uploads.",
217 + )
218 if not file.filename.endswith(".yaml"):
219 raise HTTPException(status_code=400, detail="Only .yaml files are allowed.")
220 try:
221 save_file_result = await ConnectorServices.save_file(file, session=session)
222 if save_file_result:
174 - await ConnectorServices.verify_connector_by_id(connector_id, session=session)
223 + await ConnectorServices.verify_connector_by_id(
224 + connector_id,
225 + session=session,
226 + )
227 return {"success": True, "message": "File uploaded successfully"}
228 else:
229 raise HTTPException(status_code=500, detail="Failed to upload file")
backend/app/connectors/schema.py
+1 -2
@@ -1,6 +1,5 @@
1 from datetime import datetime
2 -from typing import List
3 -from typing import Optional
2 +from typing import List, Optional
3
4 from pydantic import BaseModel
5
backend/app/connectors/services.py
+122 -42
@@ -1,18 +1,8 @@
1 import os
2 from datetime import datetime
3 -from typing import List
4 -from typing import Optional
5 -from typing import Type
6 -from typing import Union
3 +from typing import List, Optional, Type, Union
4
5 import aiofiles
9 -from fastapi import UploadFile
10 -from loguru import logger
11 -from pydantic import BaseModel
12 -from sqlalchemy.ext.asyncio import AsyncSession
13 -from sqlalchemy.future import select
14 -from werkzeug.utils import secure_filename
15 -
6 from app.connectors.cortex.utils.universal import verify_cortex_connection
7 from app.connectors.dfir_iris.utils.universal import verify_dfir_iris_connection
8 from app.connectors.grafana.utils.universal import verify_grafana_connection
@@ -32,108 +22,173 @@ from app.integrations.utils.event_shipper import verify_event_shipper_connection
22 from app.threat_intel.services.socfortress import (
23 verifiy_socfortress_threat_intel_connector,
24 )
35 -from app.utils import verify_alert_creation_provisioning_connection
36 -from app.utils import verify_wazuh_worker_provisioning_connection
25 +from app.utils import (
26 + verify_alert_creation_provisioning_connection,
27 + verify_wazuh_worker_provisioning_connection,
28 +)
29 +from fastapi import UploadFile
30 +from loguru import logger
31 +from pydantic import BaseModel
32 +from sqlalchemy.ext.asyncio import AsyncSession
33 +from sqlalchemy.future import select
34 +from werkzeug.utils import secure_filename
35
36 UPLOAD_FOLDER = "file-store"
39 -UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), UPLOAD_FOLDER)
37 +UPLOAD_FOLDER = os.path.join(
38 + os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
39 + UPLOAD_FOLDER,
40 +)
41 ALLOWED_EXTENSIONS = set(["yaml"]) # replace with your allowed file extensions
42
43
44 # Create an interface for connector services
45 class ConnectorServiceInterface(BaseModel):
45 - async def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
46 + async def verify_authentication(
47 + self,
48 + connector: ConnectorResponse,
49 + ) -> Optional[ConnectorResponse]:
50 raise NotImplementedError
51
52
53 # Wazuh Manager Service
54 class WazuhManagerService(ConnectorServiceInterface):
51 - async def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
55 + async def verify_authentication(
56 + self,
57 + connector: ConnectorResponse,
58 + ) -> Optional[ConnectorResponse]:
59 return await verify_wazuh_manager_connection(connector.connector_name)
60
61
62 # Wazuh Indexer Service
63 class WazuhIndexerService(ConnectorServiceInterface):
57 - async def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
64 + async def verify_authentication(
65 + self,
66 + connector: ConnectorResponse,
67 + ) -> Optional[ConnectorResponse]:
68 return await verify_wazuh_indexer_connection(connector.connector_name)
69
70
71 # Velociraptor Service
72 class VelociraptorService(ConnectorServiceInterface):
63 - async def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
73 + async def verify_authentication(
74 + self,
75 + connector: ConnectorResponse,
76 + ) -> Optional[ConnectorResponse]:
77 return await verify_velociraptor_connection(connector.connector_name)
78
79
80 # Graylog Service
81 class GraylogService(ConnectorServiceInterface):
69 - async def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
82 + async def verify_authentication(
83 + self,
84 + connector: ConnectorResponse,
85 + ) -> Optional[ConnectorResponse]:
86 return await verify_graylog_connection(connector.connector_name)
87
88
89 # DFIR-IRIS Service
90 class DfirIrisService(ConnectorServiceInterface):
75 - async def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
91 + async def verify_authentication(
92 + self,
93 + connector: ConnectorResponse,
94 + ) -> Optional[ConnectorResponse]:
95 return await verify_dfir_iris_connection(connector.connector_name)
96
97
98 # Cortex Service
99 class CortexService(ConnectorServiceInterface):
81 - async def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
100 + async def verify_authentication(
101 + self,
102 + connector: ConnectorResponse,
103 + ) -> Optional[ConnectorResponse]:
104 return await verify_cortex_connection(connector.connector_name)
105
106
107 # Shuffle Service
108 class ShuffleService(ConnectorServiceInterface):
87 - async def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
109 + async def verify_authentication(
110 + self,
111 + connector: ConnectorResponse,
112 + ) -> Optional[ConnectorResponse]:
113 return await verify_shuffle_connection(connector.connector_name)
114
115
116 # Sublime Service
117 class SublimeService(ConnectorServiceInterface):
93 - async def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
118 + async def verify_authentication(
119 + self,
120 + connector: ConnectorResponse,
121 + ) -> Optional[ConnectorResponse]:
122 return await verify_sublime_connection(connector.connector_name)
123
124
125 # InfluxDB Service
126 class InfluxDBService(ConnectorServiceInterface):
99 - async def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
127 + async def verify_authentication(
128 + self,
129 + connector: ConnectorResponse,
130 + ) -> Optional[ConnectorResponse]:
131 return await verify_influxdb_connection(connector.connector_name)
132
133
134 # Grafana Service
135 class GrafanaService(ConnectorServiceInterface):
105 - async def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
136 + async def verify_authentication(
137 + self,
138 + connector: ConnectorResponse,
139 + ) -> Optional[ConnectorResponse]:
140 return await verify_grafana_connection(connector.connector_name)
141
142
143 # Wazuh Worker Provisioning Service
144 class WazuhWorkerProvisioningService(ConnectorServiceInterface):
111 - async def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
112 - return await verify_wazuh_worker_provisioning_connection(connector.connector_name)
145 + async def verify_authentication(
146 + self,
147 + connector: ConnectorResponse,
148 + ) -> Optional[ConnectorResponse]:
149 + return await verify_wazuh_worker_provisioning_connection(
150 + connector.connector_name,
151 + )
152
153
154 # SOCFortress Threat Intel Service
155 class SocfortressThreatIntelService(ConnectorServiceInterface):
117 - async def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
118 - return await verifiy_socfortress_threat_intel_connector(connector.connector_name)
156 + async def verify_authentication(
157 + self,
158 + connector: ConnectorResponse,
159 + ) -> Optional[ConnectorResponse]:
160 + return await verifiy_socfortress_threat_intel_connector(
161 + connector.connector_name,
162 + )
163
164
165 # ASK SOCFortress Service
166 class AskSocfortressService(ConnectorServiceInterface):
123 - async def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
167 + async def verify_authentication(
168 + self,
169 + connector: ConnectorResponse,
170 + ) -> Optional[ConnectorResponse]:
171 return await verify_ask_socfortress_connector(connector.connector_name)
172
173
174 # Event Shipper Service
175 class EventShipperService(ConnectorServiceInterface):
129 - async def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
176 + async def verify_authentication(
177 + self,
178 + connector: ConnectorResponse,
179 + ) -> Optional[ConnectorResponse]:
180 return await verify_event_shipper_connection(connector.connector_name)
181
182
183 # Alert Creation Service
184 class AlertCreationService(ConnectorServiceInterface):
135 - async def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
136 - return await verify_alert_creation_provisioning_connection(connector.connector_name)
185 + async def verify_authentication(
186 + self,
187 + connector: ConnectorResponse,
188 + ) -> Optional[ConnectorResponse]:
189 + return await verify_alert_creation_provisioning_connection(
190 + connector.connector_name,
191 + )
192
193
194 # Factory function to create a service instance based on connector name
@@ -173,7 +228,10 @@ class ConnectorServices:
228 """
229
230 @classmethod
176 - async def fetch_all_connectors(cls, session: AsyncSession) -> List[ConnectorResponse]:
231 + async def fetch_all_connectors(
232 + cls,
233 + session: AsyncSession,
234 + ) -> List[ConnectorResponse]:
235 """
236 Fetches all connectors from the database.
237
@@ -192,7 +250,11 @@ class ConnectorServices:
250 return [ConnectorResponse.from_orm(connector) for connector in connectors]
251
252 @classmethod
195 - async def fetch_connector_by_id(cls, connector_id: int, session: AsyncSession) -> Optional[ConnectorResponse]:
253 + async def fetch_connector_by_id(
254 + cls,
255 + connector_id: int,
256 + session: AsyncSession,
257 + ) -> Optional[ConnectorResponse]:
258 """
259 Fetches a connector by its ID from the database.
260
@@ -203,14 +265,20 @@ class ConnectorServices:
265 Returns:
266 Optional[ConnectorResponse]: The fetched connector, or None if not found.
267 """
206 - result = await session.execute(select(Connectors).where(Connectors.id == connector_id))
268 + result = await session.execute(
269 + select(Connectors).where(Connectors.id == connector_id),
270 + )
271 connector = result.scalar_one_or_none()
272 if connector:
273 return ConnectorResponse.from_orm(connector)
274 return None
275
276 @classmethod
213 - async def verify_connector_by_id(cls, connector_id: int, session: AsyncSession) -> Optional[ConnectorResponse]:
277 + async def verify_connector_by_id(
278 + cls,
279 + connector_id: int,
280 + session: AsyncSession,
281 + ) -> Optional[ConnectorResponse]:
282 """
283 Verify a connector by making an API call to it asynchronously.
284
@@ -241,7 +309,9 @@ class ConnectorServices:
309 if ServiceClass is not None:
310 service_instance = ServiceClass()
311 # If verify_authentication is an async function, you will need to await it
244 - connector_response = await service_instance.verify_authentication(connector_response)
312 + connector_response = await service_instance.verify_authentication(
313 + connector_response,
314 + )
315 # If the connector is verified, update the connector record in the database
316 if connector_response["connectionSuccessful"]:
317 connector.connector_verified = True
@@ -256,7 +326,9 @@ class ConnectorServices:
326 await session.commit()
327
328 else:
259 - logger.error(f"Connector type {connector_response.connector_name} is not supported")
329 + logger.error(
330 + f"Connector type {connector_response.connector_name} is not supported",
331 + )
332 return None
333
334 return connector_response
@@ -324,10 +396,16 @@ class ConnectorServices:
396 Returns:
397 bool: True if the file is allowed, False otherwise.
398 """
327 - return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
399 + return (
400 + "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
401 + )
402
403 @classmethod
330 - async def save_file(cls, file: UploadFile, session: AsyncSession) -> Union[ConnectorResponse, bool]:
404 + async def save_file(
405 + cls,
406 + file: UploadFile,
407 + session: AsyncSession,
408 + ) -> Union[ConnectorResponse, bool]:
409 """
410 Saves the uploaded file to a specified location and updates the connector record in the database.
411
@@ -345,7 +423,9 @@ class ConnectorServices:
423
424 # Save the file asynchronously
425 async with aiofiles.open(file_path, "wb") as buffer:
348 - await buffer.write(await file.read()) # Assuming file doesn't need to be read in chunks
426 + await buffer.write(
427 + await file.read(),
428 + ) # Assuming file doesn't need to be read in chunks
429
430 # Update connector using async session and ORM
431 query = select(Connectors).where(Connectors.id == 6)
backend/app/connectors/shuffle/routes/workflows.py
+19 -12
@@ -1,14 +1,15 @@
1 -from fastapi import APIRouter
2 -from fastapi import HTTPException
3 -from fastapi import Security
4 -from loguru import logger
5 -
1 from app.auth.utils import AuthHandler
7 -from app.connectors.shuffle.schema.workflows import WorkflowExecutionBodyModel
8 -from app.connectors.shuffle.schema.workflows import WorkflowExecutionResponseModel
9 -from app.connectors.shuffle.schema.workflows import WorkflowsResponse
10 -from app.connectors.shuffle.services.workflows import get_workflow_executions
11 -from app.connectors.shuffle.services.workflows import get_workflows
2 +from app.connectors.shuffle.schema.workflows import (
3 + WorkflowExecutionBodyModel,
4 + WorkflowExecutionResponseModel,
5 + WorkflowsResponse,
6 +)
7 +from app.connectors.shuffle.services.workflows import (
8 + get_workflow_executions,
9 + get_workflows,
10 +)
11 +from fastapi import APIRouter, HTTPException, Security
12 +from loguru import logger
13
14 shuffle_workflows_router = APIRouter()
15
@@ -64,9 +65,15 @@ async def get_all_workflow_executions() -> WorkflowExecutionResponseModel:
65 {
66 "workflow_id": workflow["id"],
67 "workflow_name": workflow["name"],
67 - "status": await get_workflow_executions(WorkflowExecutionBodyModel(workflow_id=workflow["id"])),
68 + "status": await get_workflow_executions(
69 + WorkflowExecutionBodyModel(workflow_id=workflow["id"]),
70 + ),
71 },
72 )
70 - return WorkflowExecutionResponseModel(success=True, message="Successfully fetched workflow executions", workflows=workflow_details)
73 + return WorkflowExecutionResponseModel(
74 + success=True,
75 + message="Successfully fetched workflow executions",
76 + workflows=workflow_details,
77 + )
78 else:
79 raise HTTPException(status_code=404, detail="No workflows found")
backend/app/connectors/shuffle/schema/workflows.py
+14 -9
@@ -1,16 +1,15 @@
1 -from typing import Any
2 -from typing import Dict
3 -from typing import List
4 -from typing import Optional
1 +from typing import Any, Dict, List, Optional
2
6 -from pydantic import BaseModel
7 -from pydantic import Field
3 +from pydantic import BaseModel, Field
4
5
6 class WorkflowsResponse(BaseModel):
7 message: str
8 success: bool
13 - workflows: Optional[List[Dict[str, Any]]] = Field([], description="The alerts returned from the search.")
9 + workflows: Optional[List[Dict[str, Any]]] = Field(
10 + [],
11 + description="The alerts returned from the search.",
12 + )
13
14
15 class WorkflowStatusExecutionModel(BaseModel):
@@ -28,7 +27,10 @@ class WorkflowExecutionStatusResponseModel(BaseModel):
27
28
29 class WorkflowExecutionModel(BaseModel):
31 - status: WorkflowExecutionStatusResponseModel = Field(..., description="Status object")
30 + status: WorkflowExecutionStatusResponseModel = Field(
31 + ...,
32 + description="Status object",
33 + )
34 workflow_id: str = Field(..., description="Unique identifier for the workflow")
35 workflow_name: str = Field(..., description="Name of the workflow")
36
@@ -36,4 +38,7 @@ class WorkflowExecutionModel(BaseModel):
38 class WorkflowExecutionResponseModel(BaseModel):
39 message: str = Field(..., description="Response message")
40 success: bool = Field(..., description="Success status")
39 - workflows: List[WorkflowExecutionModel] = Field(..., description="List of workflow objects")
41 + workflows: List[WorkflowExecutionModel] = Field(
42 + ...,
43 + description="List of workflow objects",
44 + )
backend/app/connectors/shuffle/services/workflows.py
+34 -12
@@ -1,13 +1,14 @@
1 from typing import List
2
3 +from app.connectors.shuffle.schema.workflows import (
4 + WorkflowExecutionBodyModel,
5 + WorkflowExecutionStatusResponseModel,
6 + WorkflowsResponse,
7 +)
8 +from app.connectors.shuffle.utils.universal import send_get_request
9 from fastapi import HTTPException
10 from loguru import logger
11
6 -from app.connectors.shuffle.schema.workflows import WorkflowExecutionBodyModel
7 -from app.connectors.shuffle.schema.workflows import WorkflowExecutionStatusResponseModel
8 -from app.connectors.shuffle.schema.workflows import WorkflowsResponse
9 -from app.connectors.shuffle.utils.universal import send_get_request
10 -
12
13 def remove_large_images_from_actions(workflows: List) -> List:
14 """
@@ -22,7 +23,10 @@ def remove_large_images_from_actions(workflows: List) -> List:
23 for workflow in workflows:
24 if "actions" in workflow:
25 for action in workflow["actions"]:
25 - action.pop("large_image", None) # Use pop to avoid KeyError if 'large_image' does not exist
26 + action.pop(
27 + "large_image",
28 + None,
29 + ) # Use pop to avoid KeyError if 'large_image' does not exist
30 return workflows
31
32
@@ -38,19 +42,32 @@ async def get_workflows() -> WorkflowsResponse:
42 try:
43 response = await send_get_request("/api/v1/workflows")
44 if response is None:
41 - return WorkflowsResponse(success=False, message="Failed to get workflows", workflows=[])
45 + return WorkflowsResponse(
46 + success=False,
47 + message="Failed to get workflows",
48 + workflows=[],
49 + )
50
51 workflows = response.get("data")
52 workflows_without_large_images = remove_large_images_from_actions(workflows)
53
46 - return WorkflowsResponse(success=True, message="Successfully fetched workflows", workflows=workflows_without_large_images)
54 + return WorkflowsResponse(
55 + success=True,
56 + message="Successfully fetched workflows",
57 + workflows=workflows_without_large_images,
58 + )
59
60 except Exception as e:
61 logger.error(f"Failed to get workflows with error: {e}")
50 - raise HTTPException(status_code=500, detail=f"Failed to get workflows with error: {e}")
62 + raise HTTPException(
63 + status_code=500,
64 + detail=f"Failed to get workflows with error: {e}",
65 + )
66
67
53 -async def get_workflow_executions(exection_body: WorkflowExecutionBodyModel) -> WorkflowExecutionStatusResponseModel:
68 +async def get_workflow_executions(
69 + exection_body: WorkflowExecutionBodyModel,
70 +) -> WorkflowExecutionStatusResponseModel:
71 """
72 Returns a list of workflow executions.
73
@@ -64,7 +81,9 @@ async def get_workflow_executions(exection_body: WorkflowExecutionBodyModel) ->
81 - HTTPException: If there is an error while getting the workflow executions.
82 """
83 logger.info("Getting workflow executions")
67 - response = await send_get_request(f"/api/v1/workflows/{exection_body.workflow_id}/executions")
84 + response = await send_get_request(
85 + f"/api/v1/workflows/{exection_body.workflow_id}/executions",
86 + )
87 try:
88 executions = response["data"]
89 if executions:
@@ -76,4 +95,7 @@ async def get_workflow_executions(exection_body: WorkflowExecutionBodyModel) ->
95 return WorkflowExecutionStatusResponseModel(last_run=status)
96 except Exception as e:
97 logger.error(f"Failed to get workflow executions with error: {e}")
79 - raise HTTPException(status_code=500, detail=f"Failed to get workflow executions with error: {e}")
98 + raise HTTPException(
99 + status_code=500,
100 + detail=f"Failed to get workflow executions with error: {e}",
101 + )
backend/app/connectors/shuffle/utils/universal.py
+86 -25
@@ -1,13 +1,10 @@
1 -from typing import Any
2 -from typing import Dict
3 -from typing import Optional
1 +from typing import Any, Dict, Optional
2
3 import requests
6 -from fastapi import HTTPException
7 -from loguru import logger
8 -
4 from app.connectors.utils import get_connector_info_from_db
5 from app.db.db_session import get_db_session
6 +from fastapi import HTTPException
7 +from loguru import logger
8
9
10 async def verify_shuffle_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
@@ -33,7 +30,10 @@ async def verify_shuffle_credentials(attributes: Dict[str, Any]) -> Dict[str, An
30 logger.info(
31 f"Connection to {attributes['connector_url']} successful",
32 )
36 - return {"connectionSuccessful": True, "message": "Shuffle connection successful"}
33 + return {
34 + "connectionSuccessful": True,
35 + "message": "Shuffle connection successful",
36 + }
37 else:
38 logger.error(
39 f"Connection to {attributes['connector_url']} failed with error: {shuffle_apps.text}",
@@ -46,7 +46,10 @@ async def verify_shuffle_credentials(attributes: Dict[str, Any]) -> Dict[str, An
46 logger.error(
47 f"Connection to {attributes['connector_url']} failed with error: {e}",
48 )
49 - return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error: {e}"}
49 + return {
50 + "connectionSuccessful": False,
51 + "message": f"Connection to {attributes['connector_url']} failed with error: {e}",
52 + }
53
54
55 async def verify_shuffle_connection(connector_name: str) -> str:
@@ -62,7 +65,11 @@ async def verify_shuffle_connection(connector_name: str) -> str:
65 return await verify_shuffle_credentials(attributes)
66
67
65 -async def send_get_request(endpoint: str, params: Optional[Dict[str, Any]] = None, connector_name: str = "Shuffle") -> Dict[str, Any]:
68 +async def send_get_request(
69 + endpoint: str,
70 + params: Optional[Dict[str, Any]] = None,
71 + connector_name: str = "Shuffle",
72 +) -> Dict[str, Any]:
73 """
74 Sends a GET request to the Shuffle service.
75
@@ -90,14 +97,28 @@ async def send_get_request(endpoint: str, params: Optional[Dict[str, Any]] = Non
97 params=params,
98 verify=False,
99 )
93 - return {"data": response.json(), "success": True, "message": "Successfully retrieved data"}
100 + return {
101 + "data": response.json(),
102 + "success": True,
103 + "message": "Successfully retrieved data",
104 + }
105 except Exception as e:
106 logger.error(f"Failed to send GET request to {endpoint} with error: {e}")
96 - raise HTTPException(status_code=500, detail=f"Failed to send GET request to {endpoint} with error: {e}")
97 - return {"success": False, "message": f"Failed to send GET request to {endpoint} with error: {e}"}
107 + raise HTTPException(
108 + status_code=500,
109 + detail=f"Failed to send GET request to {endpoint} with error: {e}",
110 + )
111 + return {
112 + "success": False,
113 + "message": f"Failed to send GET request to {endpoint} with error: {e}",
114 + }
115
116
100 -def send_post_request(endpoint: str, data: Dict[str, Any] = None, connector_name: str = "Shuffle") -> Dict[str, Any]:
117 +def send_post_request(
118 + endpoint: str,
119 + data: Dict[str, Any] = None,
120 + connector_name: str = "Shuffle",
121 +) -> Dict[str, Any]:
122 """
123 Sends a POST request to the Shuffle service.
124
@@ -113,7 +134,10 @@ def send_post_request(endpoint: str, data: Dict[str, Any] = None, connector_name
134 attributes = get_connector_info_from_db(connector_name)
135 if attributes is None:
136 logger.error("No Shuffle connector found in the database")
116 - return {"success": False, "message": "No Shuffle connector found in the database"}
137 + return {
138 + "success": False,
139 + "message": "No Shuffle connector found in the database",
140 + }
141
142 try:
143 HEADERS = {
@@ -131,21 +155,37 @@ def send_post_request(endpoint: str, data: Dict[str, Any] = None, connector_name
155 )
156
157 if response.status_code == 204:
134 - return {"data": None, "success": True, "message": "Successfully completed request with no content"}
158 + return {
159 + "data": None,
160 + "success": True,
161 + "message": "Successfully completed request with no content",
162 + }
163 else:
164 return {
165 "data": response.json(),
166 "success": False if response.status_code >= 400 else True,
139 - "message": "Successfully retrieved data" if response.status_code < 400 else "Failed to retrieve data",
167 + "message": "Successfully retrieved data"
168 + if response.status_code < 400
169 + else "Failed to retrieve data",
170 }
171 except Exception as e:
172 logger.debug(f"Response: {response}")
173 logger.error(f"Failed to send POST request to {endpoint} with error: {e}")
144 - raise HTTPException(status_code=500, detail=f"Failed to send POST request to {endpoint} with error: {e}")
145 - return {"success": False, "message": f"Failed to send POST request to {endpoint} with error: {e}"}
174 + raise HTTPException(
175 + status_code=500,
176 + detail=f"Failed to send POST request to {endpoint} with error: {e}",
177 + )
178 + return {
179 + "success": False,
180 + "message": f"Failed to send POST request to {endpoint} with error: {e}",
181 + }
182
183
148 -def send_delete_request(endpoint: str, params: Optional[Dict[str, Any]] = None, connector_name: str = "Shuffle") -> Dict[str, Any]:
184 +def send_delete_request(
185 + endpoint: str,
186 + params: Optional[Dict[str, Any]] = None,
187 + connector_name: str = "Shuffle",
188 +) -> Dict[str, Any]:
189 """
190 Sends a DELETE request to the Shuffle service.
191
@@ -176,14 +216,28 @@ def send_delete_request(endpoint: str, params: Optional[Dict[str, Any]] = None,
216 params=params,
217 verify=False,
218 )
179 - return {"data": response.json(), "success": True, "message": "Successfully retrieved data"}
219 + return {
220 + "data": response.json(),
221 + "success": True,
222 + "message": "Successfully retrieved data",
223 + }
224 except Exception as e:
225 logger.error(f"Failed to send DELETE request to {endpoint} with error: {e}")
182 - raise HTTPException(status_code=500, detail=f"Failed to send DELETE request to {endpoint} with error: {e}")
183 - return {"success": False, "message": f"Failed to send DELETE request to {endpoint} with error: {e}"}
226 + raise HTTPException(
227 + status_code=500,
228 + detail=f"Failed to send DELETE request to {endpoint} with error: {e}",
229 + )
230 + return {
231 + "success": False,
232 + "message": f"Failed to send DELETE request to {endpoint} with error: {e}",
233 + }
234
235
186 -def send_put_request(endpoint: str, data: Optional[Dict[str, Any]] = None, connector_name: str = "Shuffle") -> Dict[str, Any]:
236 +def send_put_request(
237 + endpoint: str,
238 + data: Optional[Dict[str, Any]] = None,
239 + connector_name: str = "Shuffle",
240 +) -> Dict[str, Any]:
241 """
242 Sends a PUT request to the Shuffle service.
243
@@ -214,7 +268,14 @@ def send_put_request(endpoint: str, data: Optional[Dict[str, Any]] = None, conne
268 json=data,
269 verify=False,
270 )
217 - return {"data": response.json(), "success": True, "message": "Successfully retrieved data"}
271 + return {
272 + "data": response.json(),
273 + "success": True,
274 + "message": "Successfully retrieved data",
275 + }
276 except Exception as e:
277 logger.error(f"Failed to send PUT request to {endpoint} with error: {e}")
220 - raise HTTPException(status_code=500, detail=f"Failed to send PUT request to {endpoint} with error: {e}")
278 + raise HTTPException(
279 + status_code=500,
280 + detail=f"Failed to send PUT request to {endpoint} with error: {e}",
281 + )
backend/app/connectors/sublime/models/alerts.py
+13 -8
@@ -1,17 +1,17 @@
1 import datetime
2 -from typing import List
3 -from typing import Optional
2 +from typing import List, Optional
3
5 -from sqlmodel import Field
6 -from sqlmodel import Relationship
7 -from sqlmodel import SQLModel
4 +from sqlmodel import Field, Relationship, SQLModel
5
6
7 class FlaggedRule(SQLModel, table=True):
8 id: Optional[int] = Field(default=None, primary_key=True)
9 rule_id: str
10 name: str
14 - severity: Optional[str] = Field(None, description="Severity level of the flagged rule")
11 + severity: Optional[str] = Field(
12 + None,
13 + description="Severity level of the flagged rule",
14 + )
15 tags: str
16 sublime_alert_id: int = Field(foreign_key="sublimealerts.id")
17
@@ -21,7 +21,10 @@ class FlaggedRule(SQLModel, table=True):
21
22 class Mailbox(SQLModel, table=True):
23 id: Optional[int] = Field(default=None, primary_key=True)
24 - external_id: Optional[str] = Field(None, description="External identifier for the mailbox")
24 + external_id: Optional[str] = Field(
25 + None,
26 + description="External identifier for the mailbox",
27 + )
28 mailbox_id: str
29 sublime_alert_id: int = Field(foreign_key="sublimealerts.id")
30
@@ -74,6 +77,8 @@ class SublimeAlerts(SQLModel, table=True):
77
78 flagged_rules: List[FlaggedRule] = Relationship(back_populates="sublime_alert")
79 mailbox: List[Mailbox] = Relationship(back_populates="sublime_alert")
77 - triggered_actions: List[TriggeredAction] = Relationship(back_populates="sublime_alert")
80 + triggered_actions: List[TriggeredAction] = Relationship(
81 + back_populates="sublime_alert",
82 + )
83 sender: List[Sender] = Relationship(back_populates="sublime_alert")
84 recipients: List[Recipient] = Relationship(back_populates="sublime_alert")
backend/app/connectors/sublime/routes/alerts.py
+20 -14
@@ -1,22 +1,26 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from fastapi import Security
4 -from loguru import logger
5 -from sqlalchemy.ext.asyncio import AsyncSession
6 -
1 from app.auth.utils import AuthHandler
8 -from app.connectors.sublime.schema.alerts import AlertRequestBody
9 -from app.connectors.sublime.schema.alerts import AlertResponseBody
10 -from app.connectors.sublime.schema.alerts import SublimeAlertsResponse
11 -from app.connectors.sublime.services.alerts import collect_alerts
12 -from app.connectors.sublime.services.alerts import store_sublime_alert
2 +from app.connectors.sublime.schema.alerts import (
3 + AlertRequestBody,
4 + AlertResponseBody,
5 + SublimeAlertsResponse,
6 +)
7 +from app.connectors.sublime.services.alerts import collect_alerts, store_sublime_alert
8 from app.db.db_session import get_db
9 +from fastapi import APIRouter, Depends, Security
10 +from loguru import logger
11 +from sqlalchemy.ext.asyncio import AsyncSession
12
13 sublime_alerts_router = APIRouter()
14
15
18 -@sublime_alerts_router.post("/alert", description="Receive alert from Sublime and store it in the database")
19 -async def receive_sublime_alert(alert_request_body: AlertRequestBody, session: AsyncSession = Depends(get_db)) -> AlertResponseBody:
16 +@sublime_alerts_router.post(
17 + "/alert",
18 + description="Receive alert from Sublime and store it in the database",
19 +)
20 +async def receive_sublime_alert(
21 + alert_request_body: AlertRequestBody,
22 + session: AsyncSession = Depends(get_db),
23 +) -> AlertResponseBody:
24 """
25 Endpoint to store alert in the `sublimealerts` table.
26 Invoked by the Sublime alert webhook which is configured in the Sublime UI.
@@ -34,7 +38,9 @@ async def receive_sublime_alert(alert_request_body: AlertRequestBody, session: A
38 description="Get all alerts",
39 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
40 )
37 -async def get_sublime_alerts(session: AsyncSession = Depends(get_db)) -> SublimeAlertsResponse:
41 +async def get_sublime_alerts(
42 + session: AsyncSession = Depends(get_db),
43 +) -> SublimeAlertsResponse:
44 """
45 Endpoint to retrieve alerts from the `sublimealerts` table.
46
backend/app/connectors/sublime/schema/alerts.py
+39 -13
@@ -1,26 +1,36 @@
1 import datetime
2 -from typing import List
3 -from typing import Optional
2 +from typing import List, Optional
3
5 -from pydantic import BaseModel
6 -from pydantic import Field
4 +from pydantic import BaseModel, Field
5
6
7 class FlaggedRule(BaseModel):
8 id: str = Field(..., description="Unique identifier for the flagged rule")
9 name: str = Field(..., description="Name of the flagged rule")
12 - severity: Optional[str] = Field(None, description="Severity level of the flagged rule")
13 - tags: List[str] = Field(..., description="List of tags associated with the flagged rule")
10 + severity: Optional[str] = Field(
11 + None,
12 + description="Severity level of the flagged rule",
13 + )
14 + tags: List[str] = Field(
15 + ...,
16 + description="List of tags associated with the flagged rule",
17 + )
18
19
20 class Mailbox(BaseModel):
17 - external_id: Optional[str] = Field(None, description="External identifier for the mailbox")
21 + external_id: Optional[str] = Field(
22 + None,
23 + description="External identifier for the mailbox",
24 + )
25 id: str = Field(..., description="Unique identifier for the mailbox")
26
27
28 class Message(BaseModel):
29 canonical_id: str = Field(..., description="Canonical identifier for the message")
23 - external_id: Optional[str] = Field(None, description="External identifier for the mailbox")
30 + external_id: Optional[str] = Field(
31 + None,
32 + description="External identifier for the mailbox",
33 + )
34 id: str = Field(..., description="Unique identifier for the message")
35 mailbox: Mailbox = Field(..., description="Mailbox details")
36 message_source_id: str = Field(..., description="Source identifier for the message")
@@ -35,12 +45,19 @@ class TriggeredAction(BaseModel):
45 class Data(BaseModel):
46 flagged_rules: List[FlaggedRule] = Field(..., description="List of flagged rules")
47 message: Message = Field(..., description="Message details")
38 - triggered_actions: List[TriggeredAction] = Field(..., description="List of triggered actions")
48 + triggered_actions: List[TriggeredAction] = Field(
49 + ...,
50 + description="List of triggered actions",
51 + )
52
53
54 class AlertRequestBody(BaseModel):
55 api_version: str = Field(..., description="API version", alias="api_version")
43 - created_at: str = Field(..., description="Creation timestamp in ISO 8601 format", alias="created_at")
56 + created_at: str = Field(
57 + ...,
58 + description="Creation timestamp in ISO 8601 format",
59 + alias="created_at",
60 + )
61 data: Data = Field(..., description="Nested data object")
62 id: str = Field(..., description="Unique identifier for the request body")
63 type: str = Field(..., description="Type of event, e.g., message.flagged")
@@ -48,14 +65,20 @@ class AlertRequestBody(BaseModel):
65
66 class AlertResponseBody(BaseModel):
67 success: bool = Field(..., description="Success status of the request")
51 - message: str = Field(..., description="Message describing the result of the request")
68 + message: str = Field(
69 + ...,
70 + description="Message describing the result of the request",
71 + )
72
73
74 ### SQLModel Schema
75 class FlaggedRuleSchema(BaseModel):
76 rule_id: str
77 name: str
58 - severity: Optional[str] = Field(None, description="Severity level of the flagged rule")
78 + severity: Optional[str] = Field(
79 + None,
80 + description="Severity level of the flagged rule",
81 + )
82 tags: str
83
84 class Config:
@@ -63,7 +86,10 @@ class FlaggedRuleSchema(BaseModel):
86
87
88 class MailboxSchema(BaseModel):
66 - external_id: Optional[str] = Field(None, description="External identifier for the mailbox")
89 + external_id: Optional[str] = Field(
90 + None,
91 + description="External identifier for the mailbox",
92 + )
93 mailbox_id: str
94
95 class Config:
backend/app/connectors/sublime/services/alerts.py
+87 -29
@@ -1,24 +1,27 @@
1 import json
2 from typing import List
3
4 +from app.connectors.sublime.models.alerts import (
5 + FlaggedRule,
6 + Mailbox,
7 + Recipient,
8 + Sender,
9 + SublimeAlerts,
10 + TriggeredAction,
11 +)
12 +from app.connectors.sublime.schema.alerts import (
13 + AlertRequestBody,
14 + AlertResponseBody,
15 + SublimeAlertsResponse,
16 + SublimeAlertsSchema,
17 +)
18 +from app.connectors.sublime.utils.universal import send_get_request
19 from fastapi import HTTPException
20 from loguru import logger
21 from sqlalchemy.ext.asyncio import AsyncSession
22 from sqlalchemy.future import select
23 from sqlalchemy.orm import selectinload
24
10 -from app.connectors.sublime.models.alerts import FlaggedRule
11 -from app.connectors.sublime.models.alerts import Mailbox
12 -from app.connectors.sublime.models.alerts import Recipient
13 -from app.connectors.sublime.models.alerts import Sender
14 -from app.connectors.sublime.models.alerts import SublimeAlerts
15 -from app.connectors.sublime.models.alerts import TriggeredAction
16 -from app.connectors.sublime.schema.alerts import AlertRequestBody
17 -from app.connectors.sublime.schema.alerts import AlertResponseBody
18 -from app.connectors.sublime.schema.alerts import SublimeAlertsResponse
19 -from app.connectors.sublime.schema.alerts import SublimeAlertsSchema
20 -from app.connectors.sublime.utils.universal import send_get_request
21 -
25
26 def create_sublime_alert(alert_request_body: AlertRequestBody) -> SublimeAlerts:
27 """
@@ -42,7 +45,10 @@ def create_sublime_alert(alert_request_body: AlertRequestBody) -> SublimeAlerts:
45 )
46
47
45 -def create_flagged_rules(alert_request_body: AlertRequestBody, sublime_alert_id: int) -> List[FlaggedRule]:
48 +def create_flagged_rules(
49 + alert_request_body: AlertRequestBody,
50 + sublime_alert_id: int,
51 +) -> List[FlaggedRule]:
52 """
53 Create a list of flagged rules based on the given alert request body and sublime alert ID.
54
@@ -58,12 +64,21 @@ def create_flagged_rules(alert_request_body: AlertRequestBody, sublime_alert_id:
64 for rule in alert_request_body.data.flagged_rules:
65 tags_str = json.dumps(rule.tags)
66 flagged_rules.append(
61 - FlaggedRule(rule_id=rule.id, name=rule.name, severity=rule.severity, tags=tags_str, sublime_alert_id=sublime_alert_id),
67 + FlaggedRule(
68 + rule_id=rule.id,
69 + name=rule.name,
70 + severity=rule.severity,
71 + tags=tags_str,
72 + sublime_alert_id=sublime_alert_id,
73 + ),
74 )
75 return flagged_rules
76
77
66 -def create_mailbox(alert_request_body: AlertRequestBody, sublime_alert_id: int) -> Mailbox:
78 +def create_mailbox(
79 + alert_request_body: AlertRequestBody,
80 + sublime_alert_id: int,
81 +) -> Mailbox:
82 """
83 Create a mailbox object based on the provided alert request body and sublime alert ID.
84
@@ -81,7 +96,10 @@ def create_mailbox(alert_request_body: AlertRequestBody, sublime_alert_id: int)
96 )
97
98
84 -def create_triggered_actions(alert_request_body: AlertRequestBody, sublime_alert_id: int) -> List[TriggeredAction]:
99 +def create_triggered_actions(
100 + alert_request_body: AlertRequestBody,
101 + sublime_alert_id: int,
102 +) -> List[TriggeredAction]:
103 """
104 Create a list of TriggeredAction objects based on the provided alert request body and sublime alert ID.
105
@@ -95,12 +113,20 @@ def create_triggered_actions(alert_request_body: AlertRequestBody, sublime_alert
113 triggered_actions = []
114 for action in alert_request_body.data.triggered_actions:
115 triggered_actions.append(
98 - TriggeredAction(action_id=action.id, name=action.name, type=action.type, sublime_alert_id=sublime_alert_id),
116 + TriggeredAction(
117 + action_id=action.id,
118 + name=action.name,
119 + type=action.type,
120 + sublime_alert_id=sublime_alert_id,
121 + ),
122 )
123 return triggered_actions
124
125
103 -async def store_sublime_alert(session: AsyncSession, alert_request_body: AlertRequestBody) -> AlertResponseBody:
126 +async def store_sublime_alert(
127 + session: AsyncSession,
128 + alert_request_body: AlertRequestBody,
129 +) -> AlertResponseBody:
130 """
131 Stores a Sublime alert in the database.
132
@@ -118,7 +144,10 @@ async def store_sublime_alert(session: AsyncSession, alert_request_body: AlertRe
144
145 flagged_rules = create_flagged_rules(alert_request_body, sublime_alert.id)
146 mailbox = create_mailbox(alert_request_body, sublime_alert.id)
121 - triggered_actions = create_triggered_actions(alert_request_body, sublime_alert.id)
147 + triggered_actions = create_triggered_actions(
148 + alert_request_body,
149 + sublime_alert.id,
150 + )
151 sender = await create_sender(alert_request_body, sublime_alert.id)
152 recipient = await create_recipient(alert_request_body, sublime_alert.id)
153
@@ -132,15 +161,26 @@ async def store_sublime_alert(session: AsyncSession, alert_request_body: AlertRe
161 await session.commit() # Commit the changes asynchronously
162 logger.info(f"Alert {alert_request_body.id} stored in the database")
163
135 - return AlertResponseBody(success=True, message=f"Alert {alert_request_body.id} stored in the database")
164 + return AlertResponseBody(
165 + success=True,
166 + message=f"Alert {alert_request_body.id} stored in the database",
167 + )
168 except Exception as e:
169 # Rollback in case of error
170 await session.rollback()
139 - logger.error(f"Failed to store alert {alert_request_body.id} in the database: {e}")
140 - raise HTTPException(status_code=500, detail=f"Failed to store alert {alert_request_body.id} in the database: {e}")
171 + logger.error(
172 + f"Failed to store alert {alert_request_body.id} in the database: {e}",
173 + )
174 + raise HTTPException(
175 + status_code=500,
176 + detail=f"Failed to store alert {alert_request_body.id} in the database: {e}",
177 + )
178
179
143 -async def create_sender(alert_request_body: AlertRequestBody, sublime_alert_id: int) -> Sender:
180 +async def create_sender(
181 + alert_request_body: AlertRequestBody,
182 + sublime_alert_id: int,
183 +) -> Sender:
184 """
185 Create a Sender object based on the given alert request body and sublime alert ID.
186
@@ -151,10 +191,17 @@ async def create_sender(alert_request_body: AlertRequestBody, sublime_alert_id:
191 Returns:
192 Sender: The created Sender object.
193 """
154 - return Sender(email=await collect_sender(alert_request_body.data.message.id), name="n/a", sublime_alert_id=sublime_alert_id)
194 + return Sender(
195 + email=await collect_sender(alert_request_body.data.message.id),
196 + name="n/a",
197 + sublime_alert_id=sublime_alert_id,
198 + )
199
200
157 -async def create_recipient(alert_request_body: AlertRequestBody, sublime_alert_id: int) -> Recipient:
201 +async def create_recipient(
202 + alert_request_body: AlertRequestBody,
203 + sublime_alert_id: int,
204 +) -> Recipient:
205 """
206 Create a recipient for the given alert request body and sublime alert ID.
207
@@ -165,7 +212,11 @@ async def create_recipient(alert_request_body: AlertRequestBody, sublime_alert_i
212 Returns:
213 Recipient: The created recipient.
214 """
168 - return Recipient(email=await collect_recipient(alert_request_body.data.message.id), name="n/a", sublime_alert_id=sublime_alert_id)
215 + return Recipient(
216 + email=await collect_recipient(alert_request_body.data.message.id),
217 + name="n/a",
218 + sublime_alert_id=sublime_alert_id,
219 + )
220
221
222 async def collect_sender(message_id: str) -> Sender:
@@ -184,7 +235,9 @@ async def collect_sender(message_id: str) -> Sender:
235 logger.info(f"Getting Sublime Alert with message_id {message_id}")
236 message_details = await send_get_request(f"/v0/messages/{message_id}")
237 if not message_details["success"]:
187 - logger.error(f"Failed to get Sublime Alert with message_id {message_id}: {message_details['message']}")
238 + logger.error(
239 + f"Failed to get Sublime Alert with message_id {message_id}: {message_details['message']}",
240 + )
241 raise HTTPException(
242 status_code=500,
243 detail=f"Failed to get Sublime Alert with message_id {message_id}: {message_details['message']}",
@@ -209,7 +262,9 @@ async def collect_recipient(message_id: str) -> Recipient:
262 logger.info(f"Getting Sublime Alert with message_id {message_id}")
263 message_details = await send_get_request(f"/v0/messages/{message_id}")
264 if not message_details["success"]:
212 - logger.error(f"Failed to get Sublime Alert with message_id {message_id}: {message_details['message']}")
265 + logger.error(
266 + f"Failed to get Sublime Alert with message_id {message_id}: {message_details['message']}",
267 + )
268 raise HTTPException(
269 status_code=500,
270 detail=f"Failed to get Sublime Alert with message_id {message_id}: {message_details['message']}",
@@ -249,4 +304,7 @@ async def collect_alerts(session: AsyncSession) -> List[SublimeAlertsResponse]:
304 )
305 except Exception as e:
306 logger.error(f"Failed to get all Sublime Alerts with error: {e}")
252 - raise HTTPException(status_code=500, detail=f"Failed to get all Sublime Alerts with error: {e}")
307 + raise HTTPException(
308 + status_code=500,
309 + detail=f"Failed to get all Sublime Alerts with error: {e}",
310 + )
backend/app/connectors/sublime/utils/universal.py
+24 -10
@@ -1,12 +1,9 @@
1 -from typing import Any
2 -from typing import Dict
3 -from typing import Optional
1 +from typing import Any, Dict, Optional
2
3 import requests
6 -from loguru import logger
7 -
4 from app.connectors.utils import get_connector_info_from_db
5 from app.db.db_session import get_db_session
6 +from loguru import logger
7
8
9 async def verify_sublime_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
@@ -37,7 +34,10 @@ async def verify_sublime_credentials(attributes: Dict[str, Any]) -> Dict[str, An
34 logger.info(
35 f"Connection to {attributes['connector_url']} successful",
36 )
40 - return {"connectionSuccessful": True, "message": "Sublime connection successful"}
37 + return {
38 + "connectionSuccessful": True,
39 + "message": "Sublime connection successful",
40 + }
41 else:
42 logger.error(
43 f"Connection to {attributes['connector_url']} failed with error: {sublime.text}",
@@ -50,7 +50,10 @@ async def verify_sublime_credentials(attributes: Dict[str, Any]) -> Dict[str, An
50 logger.error(
51 f"Connection to {attributes['connector_url']} failed with error: {e}",
52 )
53 - return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error: {e}"}
53 + return {
54 + "connectionSuccessful": False,
55 + "message": f"Connection to {attributes['connector_url']} failed with error: {e}",
56 + }
57
58
59 async def verify_sublime_connection(connector_name: str) -> str:
@@ -66,7 +69,11 @@ async def verify_sublime_connection(connector_name: str) -> str:
69 return await verify_sublime_credentials(attributes)
70
71
69 -async def send_get_request(endpoint: str, params: Optional[Dict[str, Any]] = None, connector_name: str = "Sublime") -> Dict[str, Any]:
72 +async def send_get_request(
73 + endpoint: str,
74 + params: Optional[Dict[str, Any]] = None,
75 + connector_name: str = "Sublime",
76 +) -> Dict[str, Any]:
77 """
78 Sends a GET request to the Sublime service.
79
@@ -95,7 +102,14 @@ async def send_get_request(endpoint: str, params: Optional[Dict[str, Any]] = Non
102 params=params,
103 verify=False,
104 )
98 - return {"data": response.json(), "success": True, "message": "Successfully retrieved data"}
105 + return {
106 + "data": response.json(),
107 + "success": True,
108 + "message": "Successfully retrieved data",
109 + }
110 except Exception as e:
111 logger.error(f"Failed to send GET request to {endpoint} with error: {e}")
101 - return {"success": False, "message": f"Failed to send GET request to {endpoint} with error: {e}"}
112 + return {
113 + "success": False,
114 + "message": f"Failed to send GET request to {endpoint} with error: {e}",
115 + }
backend/app/connectors/utils.py
+7 -7
@@ -1,17 +1,17 @@
1 -from typing import Any
2 -from typing import Dict
3 -from typing import Optional
1 +from typing import Any, Dict, Optional
2
3 +from app.connectors.models import Connectors
4 +from app.connectors.schema import ConnectorResponse
5 from loguru import logger
6 from sqlalchemy.ext.asyncio import AsyncSession
7 from sqlalchemy.future import select
8
9 -from app.connectors.models import Connectors
10 -from app.connectors.schema import ConnectorResponse
11 -
9
10 # ! New with Async
14 -async def get_connector_info_from_db(connector_name: str, db: AsyncSession) -> Optional[Dict[str, Any]]:
11 +async def get_connector_info_from_db(
12 + connector_name: str,
13 + db: AsyncSession,
14 +) -> Optional[Dict[str, Any]]:
15 """
16 Fetches connector information from the database based on the given connector name.
17
backend/app/connectors/velociraptor/routes/artifacts.py
+112 -45
@@ -1,29 +1,29 @@
1 from typing import List
2
3 -from fastapi import APIRouter
4 -from fastapi import Depends
5 -from fastapi import HTTPException
6 -from fastapi import Security
7 -from loguru import logger
8 -from sqlalchemy.ext.asyncio import AsyncSession
9 -from sqlalchemy.future import select
10 -
3 from app.auth.utils import AuthHandler
12 -from app.connectors.velociraptor.schema.artifacts import ArtifactsResponse
13 -from app.connectors.velociraptor.schema.artifacts import CollectArtifactBody
14 -from app.connectors.velociraptor.schema.artifacts import CollectArtifactResponse
15 -from app.connectors.velociraptor.schema.artifacts import OSPrefixEnum
16 -from app.connectors.velociraptor.schema.artifacts import OSPrefixModel
17 -from app.connectors.velociraptor.schema.artifacts import QuarantineBody
18 -from app.connectors.velociraptor.schema.artifacts import QuarantineResponse
19 -from app.connectors.velociraptor.schema.artifacts import RunCommandBody
20 -from app.connectors.velociraptor.schema.artifacts import RunCommandResponse
21 -from app.connectors.velociraptor.services.artifacts import get_artifacts
22 -from app.connectors.velociraptor.services.artifacts import quarantine_host
23 -from app.connectors.velociraptor.services.artifacts import run_artifact_collection
24 -from app.connectors.velociraptor.services.artifacts import run_remote_command
4 +from app.connectors.velociraptor.schema.artifacts import (
5 + ArtifactsResponse,
6 + CollectArtifactBody,
7 + CollectArtifactResponse,
8 + OSPrefixEnum,
9 + OSPrefixModel,
10 + QuarantineBody,
11 + QuarantineResponse,
12 + RunCommandBody,
13 + RunCommandResponse,
14 +)
15 +from app.connectors.velociraptor.services.artifacts import (
16 + get_artifacts,
17 + quarantine_host,
18 + run_artifact_collection,
19 + run_remote_command,
20 +)
21 from app.db.db_session import get_db
22 from app.db.universal_models import Agents
23 +from fastapi import APIRouter, Depends, HTTPException, Security
24 +from loguru import logger
25 +from sqlalchemy.ext.asyncio import AsyncSession
26 +from sqlalchemy.future import select
27
28 # App specific imports
29
@@ -63,9 +63,14 @@ def verify_os_prefix_exists(os_prefix: str) -> str:
63 valid_os_prefixes = get_valid_os_prefixes()
64
65 if os_prefix_lower not in valid_os_prefixes:
66 - raise HTTPException(status_code=400, detail=f"OS prefix {os_prefix} does not exist.")
66 + raise HTTPException(
67 + status_code=400,
68 + detail=f"OS prefix {os_prefix} does not exist.",
69 + )
70
68 - return OSPrefixEnum[os_prefix_upper].value # Use the uppercase version for Enum matching
71 + return OSPrefixEnum[
72 + os_prefix_upper
73 + ].value # Use the uppercase version for Enum matching
74
75
76 def get_os_prefix_from_os_name(os_name: str) -> str:
@@ -105,16 +110,26 @@ async def get_velociraptor_id(session: AsyncSession, hostname: str) -> str:
110 agent = result.scalars().first()
111
112 if not agent:
108 - raise HTTPException(status_code=404, detail=f"Agent with hostname {hostname} not found")
113 + raise HTTPException(
114 + status_code=404,
115 + detail=f"Agent with hostname {hostname} not found",
116 + )
117
118 if agent.velociraptor_id == "n/a":
111 - raise HTTPException(status_code=404, detail=f"Velociraptor ID for hostname {hostname} is not available")
119 + raise HTTPException(
120 + status_code=404,
121 + detail=f"Velociraptor ID for hostname {hostname} is not available",
122 + )
123
124 logger.info(f"velociraptor_id for hostname {hostname} is {agent.velociraptor_id}")
125 return agent.velociraptor_id
126
127
117 -async def update_agent_quarantine_status(session: AsyncSession, quarantine_body: QuarantineBody, quarantine_response: QuarantineResponse):
128 +async def update_agent_quarantine_status(
129 + session: AsyncSession,
130 + quarantine_body: QuarantineBody,
131 + quarantine_response: QuarantineResponse,
132 +):
133 """
134 Updates the quarantine status of an agent.
135
@@ -129,27 +144,42 @@ async def update_agent_quarantine_status(session: AsyncSession, quarantine_body:
144 Returns:
145 None
146 """
132 - logger.info(f"Updating agent quarantine status for hostname {quarantine_body.hostname}")
133 - result = await session.execute(select(Agents).filter(Agents.hostname == quarantine_body.hostname))
147 + logger.info(
148 + f"Updating agent quarantine status for hostname {quarantine_body.hostname}",
149 + )
150 + result = await session.execute(
151 + select(Agents).filter(Agents.hostname == quarantine_body.hostname),
152 + )
153 agent = result.scalars().first()
154
155 if not agent:
137 - raise HTTPException(status_code=404, detail=f"Agent with hostname {quarantine_body.hostname} not found")
156 + raise HTTPException(
157 + status_code=404,
158 + detail=f"Agent with hostname {quarantine_body.hostname} not found",
159 + )
160
161 if quarantine_body.action == "quarantine":
162 if quarantine_response.success:
163 agent.quarantined = True
164 else:
143 - raise HTTPException(status_code=500, detail=f"Failed to quarantine hostname {quarantine_body.hostname}")
165 + raise HTTPException(
166 + status_code=500,
167 + detail=f"Failed to quarantine hostname {quarantine_body.hostname}",
168 + )
169 elif quarantine_body.action == "remove_quarantine":
170 if quarantine_response.success:
171 agent.quarantined = False
172 else:
148 - raise HTTPException(status_code=500, detail=f"Failed to remove quarantine for hostname {quarantine_body.hostname}")
173 + raise HTTPException(
174 + status_code=500,
175 + detail=f"Failed to remove quarantine for hostname {quarantine_body.hostname}",
176 + )
177
178 await session.commit()
179
152 - logger.info(f"Agent quarantine status for hostname {quarantine_body.hostname} updated to {agent.quarantined}")
180 + logger.info(
181 + f"Agent quarantine status for hostname {quarantine_body.hostname} updated to {agent.quarantined}",
182 + )
183
184 return None
185
@@ -177,7 +207,9 @@ async def get_all_artifacts() -> ArtifactsResponse:
207 description="Get all artifacts for a specific OS prefix",
208 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
209 )
180 -async def get_all_artifacts_for_os_prefix(os_prefix: str = Depends(verify_os_prefix_exists)) -> ArtifactsResponse:
210 +async def get_all_artifacts_for_os_prefix(
211 + os_prefix: str = Depends(verify_os_prefix_exists),
212 +) -> ArtifactsResponse:
213 """
214 Fetches all artifacts for a specific OS prefix.
215
@@ -191,8 +223,14 @@ async def get_all_artifacts_for_os_prefix(os_prefix: str = Depends(verify_os_pre
223 # Get all the artifacts names that begin with the OS prefix
224 artifacts = await get_artifacts()
225 artifacts = artifacts.artifacts
194 - artifacts_for_os_prefix = [artifact for artifact in artifacts if artifact.name.startswith(os_prefix)]
195 - return ArtifactsResponse(success=True, message=f"All artifacts for OS prefix {os_prefix} retrieved", artifacts=artifacts_for_os_prefix)
226 + artifacts_for_os_prefix = [
227 + artifact for artifact in artifacts if artifact.name.startswith(os_prefix)
228 + ]
229 + return ArtifactsResponse(
230 + success=True,
231 + message=f"All artifacts for OS prefix {os_prefix} retrieved",
232 + artifacts=artifacts_for_os_prefix,
233 + )
234
235
236 @velociraptor_artifacts_router.get(
@@ -201,7 +239,10 @@ async def get_all_artifacts_for_os_prefix(os_prefix: str = Depends(verify_os_pre
239 description="Get all artifacts for a specific host's OS prefix",
240 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
241 )
204 -async def get_all_artifacts_for_hostname(hostname: str, session: AsyncSession = Depends(get_db)) -> ArtifactsResponse:
242 +async def get_all_artifacts_for_hostname(
243 + hostname: str,
244 + session: AsyncSession = Depends(get_db),
245 +) -> ArtifactsResponse:
246 """
247 Retrieve all artifacts for a specific host's OS prefix.
248
@@ -215,15 +256,23 @@ async def get_all_artifacts_for_hostname(hostname: str, session: AsyncSession =
256 logger.info(f"Fetching all artifacts for hostname {hostname}")
257
258 # Asynchronous query to find the agent
218 - agent_result = await session.execute(select(Agents).filter(Agents.hostname == hostname))
259 + agent_result = await session.execute(
260 + select(Agents).filter(Agents.hostname == hostname),
261 + )
262 agent = agent_result.scalars().first()
263
264 if not agent:
222 - raise HTTPException(status_code=404, detail=f"Agent with hostname {hostname} not found")
265 + raise HTTPException(
266 + status_code=404,
267 + detail=f"Agent with hostname {hostname} not found",
268 + )
269
270 os_prefix = get_os_prefix_from_os_name(os_name=agent.os.lower())
271 if not os_prefix:
226 - raise HTTPException(status_code=404, detail=f"OS prefix of {agent.os.lower()} for hostname {hostname} not found")
272 + raise HTTPException(
273 + status_code=404,
274 + detail=f"OS prefix of {agent.os.lower()} for hostname {hostname} not found",
275 + )
276
277 # Assuming get_all_artifacts_for_os_prefix is an async function
278 result = await get_all_artifacts_for_os_prefix(os_prefix)
@@ -256,7 +305,10 @@ async def collect_artifact(
305 CollectArtifactResponse: The response containing the collected artifact.
306 """
307 logger.info(f"Received request to collect artifact {collect_artifact_body}")
259 - result = await get_all_artifacts_for_hostname(collect_artifact_body.hostname, session)
308 + result = await get_all_artifacts_for_hostname(
309 + collect_artifact_body.hostname,
310 + session,
311 + )
312 artifact_names = [artifact.name for artifact in result.artifacts]
313
314 if collect_artifact_body.artifact_name not in artifact_names:
@@ -265,7 +317,10 @@ async def collect_artifact(
317 detail=f"Artifact name {collect_artifact_body.artifact_name} does not apply for hostname {collect_artifact_body.hostname} or does not exist",
318 )
319
268 - collect_artifact_body.velociraptor_id = await get_velociraptor_id(session, collect_artifact_body.hostname)
320 + collect_artifact_body.velociraptor_id = await get_velociraptor_id(
321 + session,
322 + collect_artifact_body.hostname,
323 + )
324
325 # Assuming run_artifact_collection is an async function and takes a session as a parameter
326 return await run_artifact_collection(collect_artifact_body)
@@ -277,7 +332,10 @@ async def collect_artifact(
332 description="Run a remote command",
333 dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
334 )
280 -async def run_command(run_command_body: RunCommandBody, session: AsyncSession = Depends(get_db)) -> RunCommandResponse:
335 +async def run_command(
336 + run_command_body: RunCommandBody,
337 + session: AsyncSession = Depends(get_db),
338 +) -> RunCommandResponse:
339 """
340 Run a remote command.
341
@@ -297,7 +355,10 @@ async def run_command(run_command_body: RunCommandBody, session: AsyncSession =
355 detail=f"Artifact name {run_command_body.artifact_name.value} does not apply for hostname {run_command_body.hostname} or does not exist",
356 )
357 # Add the velociraptor_id to the run_command_body object
300 - run_command_body.velociraptor_id = await get_velociraptor_id(session, run_command_body.hostname)
358 + run_command_body.velociraptor_id = await get_velociraptor_id(
359 + session,
360 + run_command_body.hostname,
361 + )
362 # Run the command
363 return await run_remote_command(run_command_body)
364
@@ -308,7 +369,10 @@ async def run_command(run_command_body: RunCommandBody, session: AsyncSession =
369 description="Quarantine a host",
370 dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
371 )
311 -async def quarantine(quarantine_body: QuarantineBody, session: AsyncSession = Depends(get_db)) -> QuarantineResponse:
372 +async def quarantine(
373 + quarantine_body: QuarantineBody,
374 + session: AsyncSession = Depends(get_db),
375 +) -> QuarantineResponse:
376 """
377 Quarantine a host.
378
@@ -329,7 +393,10 @@ async def quarantine(quarantine_body: QuarantineBody, session: AsyncSession = De
393 )
394 # Add the velociraptor_id to the run_command_body object
395 # Add the velociraptor_id to the quarantine_body object
332 - quarantine_body.velociraptor_id = await get_velociraptor_id(session, quarantine_body.hostname)
396 + quarantine_body.velociraptor_id = await get_velociraptor_id(
397 + session,
398 + quarantine_body.hostname,
399 + )
400 # Quarantine the host
401 quarantine_response = await quarantine_host(quarantine_body)
402
backend/app/connectors/velociraptor/routes/flows.py
+21 -16
@@ -1,19 +1,13 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from fastapi import HTTPException
4 -from fastapi import Security
5 -from loguru import logger
6 -from sqlalchemy.ext.asyncio import AsyncSession
7 -from sqlalchemy.future import select
8 -
1 from app.auth.utils import AuthHandler
2 from app.connectors.velociraptor.schema.artifacts import CollectArtifactResponse
11 -from app.connectors.velociraptor.schema.flows import FlowResponse
12 -from app.connectors.velociraptor.schema.flows import RetrieveFlowRequest
13 -from app.connectors.velociraptor.services.flows import get_flow
14 -from app.connectors.velociraptor.services.flows import get_flows
3 +from app.connectors.velociraptor.schema.flows import FlowResponse, RetrieveFlowRequest
4 +from app.connectors.velociraptor.services.flows import get_flow, get_flows
5 from app.db.db_session import get_db
6 from app.db.universal_models import Agents
7 +from fastapi import APIRouter, Depends, HTTPException, Security
8 +from loguru import logger
9 +from sqlalchemy.ext.asyncio import AsyncSession
10 +from sqlalchemy.future import select
11
12 velociraptor_flows_router = APIRouter()
13
@@ -41,10 +35,16 @@ async def get_velociraptor_id(session: AsyncSession, hostname: str) -> str:
35 agent = result.scalars().first()
36
37 if not agent:
44 - raise HTTPException(status_code=404, detail=f"Agent with hostname {hostname} not found")
38 + raise HTTPException(
39 + status_code=404,
40 + detail=f"Agent with hostname {hostname} not found",
41 + )
42
43 if agent.velociraptor_id == "n/a":
47 - raise HTTPException(status_code=404, detail=f"Velociraptor ID for hostname {hostname} is not available")
44 + raise HTTPException(
45 + status_code=404,
46 + detail=f"Velociraptor ID for hostname {hostname} is not available",
47 + )
48
49 logger.info(f"velociraptor_id for hostname {hostname} is {agent.velociraptor_id}")
50 return agent.velociraptor_id
@@ -56,7 +56,10 @@ async def get_velociraptor_id(session: AsyncSession, hostname: str) -> str:
56 description="Get all artifacts for a specific host's OS prefix",
57 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
58 )
59 -async def get_all_flows_for_hostname(hostname: str, session: AsyncSession = Depends(get_db)) -> FlowResponse:
59 +async def get_all_flows_for_hostname(
60 + hostname: str,
61 + session: AsyncSession = Depends(get_db),
62 +) -> FlowResponse:
63 """
64 Retrieve ran flows for a specific host.
65
@@ -80,7 +83,9 @@ async def get_all_flows_for_hostname(hostname: str, session: AsyncSession = Depe
83 description="Retrieve a flow based on the flow_id",
84 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
85 )
83 -async def retrieve_flow(retrieve_flow_request: RetrieveFlowRequest) -> CollectArtifactResponse:
86 +async def retrieve_flow(
87 + retrieve_flow_request: RetrieveFlowRequest,
88 +) -> CollectArtifactResponse:
89 """
90 Retrieve ran flows for a specific host.
91
backend/app/connectors/velociraptor/schema/artifacts.py
+18 -10
@@ -1,11 +1,7 @@
1 from enum import Enum
2 -from typing import Any
3 -from typing import Dict
4 -from typing import List
5 -from typing import Optional
2 +from typing import Any, Dict, List, Optional
3
7 -from pydantic import BaseModel
8 -from pydantic import Field
4 +from pydantic import BaseModel, Field
5
6
7 class Artifacts(BaseModel):
@@ -76,23 +72,35 @@ class BaseBody(BaseModel):
72
73
74 class CollectArtifactBody(BaseBody):
79 - artifact_name: Optional[str] = Field(None, description="Name of the artifact for collection or command running")
75 + artifact_name: Optional[str] = Field(
76 + None,
77 + description="Name of the artifact for collection or command running",
78 + )
79
80
81 class RunCommandBody(BaseBody):
82 command: Optional[str] = Field(None, description="Command to run")
84 - artifact_name: CommandArtifactsEnum = Field(None, description="Name of the artifact for command running")
83 + artifact_name: CommandArtifactsEnum = Field(
84 + None,
85 + description="Name of the artifact for command running",
86 + )
87
88
89 class QuarantineBody(BaseBody):
90 action: ActionEnum = Field(..., description="Action to perform")
89 - artifact_name: QuarantineArtifactsEnum = Field(None, description="Name of the artifact for quarantine or removal of quarantine")
91 + artifact_name: QuarantineArtifactsEnum = Field(
92 + None,
93 + description="Name of the artifact for quarantine or removal of quarantine",
94 + )
95
96
97 class BaseResponse(BaseModel):
98 message: str = Field(...)
99 success: bool = Field(...) # Changed from str to bool based on your sample data
95 - results: Optional[List[Dict[str, Any]]] = Field(None, description="Results of the operation")
100 + results: Optional[List[Dict[str, Any]]] = Field(
101 + None,
102 + description="Results of the operation",
103 + )
104
105
106 class CollectArtifactResponse(BaseResponse):
backend/app/connectors/velociraptor/schema/flows.py
+14 -8
@@ -1,11 +1,8 @@
1 -from typing import List
2 -from typing import Optional
1 +from typing import List, Optional
2
3 from fastapi import HTTPException
4 from loguru import logger
6 -from pydantic import BaseModel
7 -from pydantic import Field
8 -from pydantic import root_validator
5 +from pydantic import BaseModel, Field, root_validator
6
7
8 class FlowSpecParameter(BaseModel):
@@ -16,7 +13,10 @@ class FlowSpecParameter(BaseModel):
13
14 class FlowSpec(BaseModel):
15 artifact: str
19 - parameters: Optional[List[FlowSpecParameter]] = Field(None, description="The parameters of the artifact.")
16 + parameters: Optional[List[FlowSpecParameter]] = Field(
17 + None,
18 + description="The parameters of the artifact.",
19 + )
20
21
22 class FlowRequest(BaseModel):
@@ -26,7 +26,10 @@ class FlowRequest(BaseModel):
26 flow_id: str
27 urgent: bool
28 artifacts: List[str]
29 - specs: Optional[List[FlowSpec]] = Field(None, description="The specs of the artifacts.")
29 + specs: Optional[List[FlowSpec]] = Field(
30 + None,
31 + description="The specs of the artifacts.",
32 + )
33 cpu_limit: int
34 iops_limit: int
35 progress_timeout: int
@@ -113,5 +116,8 @@ class RetrieveFlowRequest(BaseModel):
116 @root_validator(pre=True)
117 def validate_session_id(cls, values):
118 if "session_id" in values and values["session_id"] == "":
116 - raise HTTPException(status_code=400, detail="The session_id cannot be an empty string")
119 + raise HTTPException(
120 + status_code=400,
121 + detail="The session_id cannot be an empty string",
122 + )
123 return values
backend/app/connectors/velociraptor/services/artifacts.py
+67 -24
@@ -1,16 +1,17 @@
1 +from app.connectors.velociraptor.schema.artifacts import (
2 + Artifacts,
3 + ArtifactsResponse,
4 + CollectArtifactBody,
5 + CollectArtifactResponse,
6 + QuarantineBody,
7 + QuarantineResponse,
8 + RunCommandBody,
9 + RunCommandResponse,
10 +)
11 +from app.connectors.velociraptor.utils.universal import UniversalService
12 from fastapi import HTTPException
13 from loguru import logger
14
4 -from app.connectors.velociraptor.schema.artifacts import Artifacts
5 -from app.connectors.velociraptor.schema.artifacts import ArtifactsResponse
6 -from app.connectors.velociraptor.schema.artifacts import CollectArtifactBody
7 -from app.connectors.velociraptor.schema.artifacts import CollectArtifactResponse
8 -from app.connectors.velociraptor.schema.artifacts import QuarantineBody
9 -from app.connectors.velociraptor.schema.artifacts import QuarantineResponse
10 -from app.connectors.velociraptor.schema.artifacts import RunCommandBody
11 -from app.connectors.velociraptor.schema.artifacts import RunCommandResponse
12 -from app.connectors.velociraptor.utils.universal import UniversalService
13 -
15
16 def create_query(query: str) -> str:
17 """
@@ -60,7 +61,10 @@ def get_artifact_key(analyzer_body: CollectArtifactBody) -> str:
61 f"env=dict(Command='{analyzer_body.command}'))"
62 )
63 else:
63 - return f"collect_client(client_id='{analyzer_body.velociraptor_id}', " f"artifacts=['{analyzer_body.artifact_name}'])"
64 + return (
65 + f"collect_client(client_id='{analyzer_body.velociraptor_id}', "
66 + f"artifacts=['{analyzer_body.artifact_name}'])"
67 + )
68
69
70 async def get_artifacts() -> ArtifactsResponse:
@@ -77,15 +81,27 @@ async def get_artifacts() -> ArtifactsResponse:
81 try:
82 if all_artifacts["success"]:
83 artifacts = [Artifacts(**artifact) for artifact in all_artifacts["results"]]
80 - return ArtifactsResponse(success=True, message="All artifacts retrieved", artifacts=artifacts)
84 + return ArtifactsResponse(
85 + success=True,
86 + message="All artifacts retrieved",
87 + artifacts=artifacts,
88 + )
89 else:
82 - raise HTTPException(status_code=500, detail=f"Failed to get all artifacts: {all_artifacts['message']}")
90 + raise HTTPException(
91 + status_code=500,
92 + detail=f"Failed to get all artifacts: {all_artifacts['message']}",
93 + )
94 except Exception as err:
95 logger.error(f"Failed to get all artifacts: {err}")
85 - raise HTTPException(status_code=500, detail=f"Failed to get all artifacts: {err}")
96 + raise HTTPException(
97 + status_code=500,
98 + detail=f"Failed to get all artifacts: {err}",
99 + )
100
101
88 -async def run_artifact_collection(collect_artifact_body: CollectArtifactBody) -> CollectArtifactResponse:
102 +async def run_artifact_collection(
103 + collect_artifact_body: CollectArtifactBody,
104 +) -> CollectArtifactResponse:
105 """
106 Run an artifact collection on a client.
107
@@ -119,13 +135,26 @@ async def run_artifact_collection(collect_artifact_body: CollectArtifactBody) ->
135
136 logger.info(f"Successfully read collection results on {results}")
137
122 - return CollectArtifactResponse(success=results["success"], message=results["message"], results=results["results"])
123 - except HTTPException as he: # Catch HTTPException separately to propagate the original message
124 - logger.error(f"HTTPException while running artifact collection on {collect_artifact_body}: {he.detail}")
138 + return CollectArtifactResponse(
139 + success=results["success"],
140 + message=results["message"],
141 + results=results["results"],
142 + )
143 + except (
144 + HTTPException
145 + ) as he: # Catch HTTPException separately to propagate the original message
146 + logger.error(
147 + f"HTTPException while running artifact collection on {collect_artifact_body}: {he.detail}",
148 + )
149 raise he
150 except Exception as err:
127 - logger.error(f"Failed to run artifact collection on {collect_artifact_body}: {err}")
128 - raise HTTPException(status_code=500, detail=f"Failed to run artifact collection on {collect_artifact_body}: {err}")
151 + logger.error(
152 + f"Failed to run artifact collection on {collect_artifact_body}: {err}",
153 + )
154 + raise HTTPException(
155 + status_code=500,
156 + detail=f"Failed to run artifact collection on {collect_artifact_body}: {err}",
157 + )
158
159
160 async def run_remote_command(run_command_body: RunCommandBody) -> RunCommandResponse:
@@ -169,10 +198,17 @@ async def run_remote_command(run_command_body: RunCommandBody) -> RunCommandResp
198
199 logger.info(f"Successfully read collection results on {results}")
200
172 - return RunCommandResponse(success=results["success"], message=results["message"], results=results["results"])
201 + return RunCommandResponse(
202 + success=results["success"],
203 + message=results["message"],
204 + results=results["results"],
205 + )
206 except Exception as err:
207 logger.error(f"Failed to run artifact collection on {run_command_body}: {err}")
175 - raise HTTPException(status_code=500, detail=f"Failed to run artifact collection on {run_command_body}: {err}")
208 + raise HTTPException(
209 + status_code=500,
210 + detail=f"Failed to run artifact collection on {run_command_body}: {err}",
211 + )
212
213
214 async def quarantine_host(quarantine_body: QuarantineBody) -> QuarantineResponse:
@@ -226,7 +262,14 @@ async def quarantine_host(quarantine_body: QuarantineBody) -> QuarantineResponse
262
263 logger.info(f"Successfully read collection results on {results}")
264
229 - return QuarantineResponse(success=results["success"], message=results["message"], results=results["results"])
265 + return QuarantineResponse(
266 + success=results["success"],
267 + message=results["message"],
268 + results=results["results"],
269 + )
270 except Exception as err:
271 logger.error(f"Failed to run artifact collection on {quarantine_body}: {err}")
232 - raise HTTPException(status_code=500, detail=f"Failed to run artifact collection on {quarantine_body}: {err}")
272 + raise HTTPException(
273 + status_code=500,
274 + detail=f"Failed to run artifact collection on {quarantine_body}: {err}",
275 + )
backend/app/connectors/velociraptor/services/flows.py
+28 -11
@@ -1,11 +1,12 @@
1 -from fastapi import HTTPException
2 -from loguru import logger
3 -
1 from app.connectors.velociraptor.schema.artifacts import CollectArtifactResponse
5 -from app.connectors.velociraptor.schema.flows import FlowClientSession
6 -from app.connectors.velociraptor.schema.flows import FlowResponse
7 -from app.connectors.velociraptor.schema.flows import RetrieveFlowRequest
2 +from app.connectors.velociraptor.schema.flows import (
3 + FlowClientSession,
4 + FlowResponse,
5 + RetrieveFlowRequest,
6 +)
7 from app.connectors.velociraptor.utils.universal import UniversalService
8 +from fastapi import HTTPException
9 +from loguru import logger
10
11
12 def create_query(query: str) -> str:
@@ -41,12 +42,22 @@ async def get_flows(velociraptor_id: str) -> FlowResponse:
42 if all_flows["success"]:
43 flows = [FlowClientSession(**flow) for flow in all_flows["results"]]
44 logger.info(f"flows: {flows}")
44 - return FlowResponse(success=True, message="All flows retrieved.", results=flows)
45 + return FlowResponse(
46 + success=True,
47 + message="All flows retrieved.",
48 + results=flows,
49 + )
50 else:
46 - raise HTTPException(status_code=500, detail=f"Failed to retrieve flows from Velociraptor: {all_flows['message']}")
51 + raise HTTPException(
52 + status_code=500,
53 + detail=f"Failed to retrieve flows from Velociraptor: {all_flows['message']}",
54 + )
55 except Exception as e:
56 logger.error(f"Failed to retrieve flows from Velociraptor: {e}")
49 - raise HTTPException(status_code=500, detail=f"Failed to retrieve flows from Velociraptor: {e}")
57 + raise HTTPException(
58 + status_code=500,
59 + detail=f"Failed to retrieve flows from Velociraptor: {e}",
60 + )
61
62
63 async def get_flow(retrieve_flow_request: RetrieveFlowRequest):
@@ -71,7 +82,13 @@ async def get_flow(retrieve_flow_request: RetrieveFlowRequest):
82 results=flow_results["results"],
83 )
84 else:
74 - raise HTTPException(status_code=500, detail=f"Failed to retrieve flow results from Velociraptor: {flow_results['message']}")
85 + raise HTTPException(
86 + status_code=500,
87 + detail=f"Failed to retrieve flow results from Velociraptor: {flow_results['message']}",
88 + )
89 except Exception as e:
90 logger.error(f"Failed to retrieve flow results from Velociraptor: {e}")
77 - raise HTTPException(status_code=500, detail=f"Failed to retrieve flow results from Velociraptor: {e}")
91 + raise HTTPException(
92 + status_code=500,
93 + detail=f"Failed to retrieve flow results from Velociraptor: {e}",
94 + )
backend/app/connectors/velociraptor/utils/universal.py
+36 -17
@@ -1,18 +1,14 @@
1 import json
2 from datetime import datetime
3 -from typing import Any
4 -from typing import Dict
3 +from typing import Any, Dict
4
5 import grpc
6 import pyvelociraptor
7 +from app.connectors.utils import get_connector_info_from_db
8 +from app.db.db_session import AsyncSessionLocal, get_db_session
9 from fastapi import HTTPException
10 from loguru import logger
10 -from pyvelociraptor import api_pb2
11 -from pyvelociraptor import api_pb2_grpc
12 -
13 -from app.connectors.utils import get_connector_info_from_db
14 -from app.db.db_session import AsyncSessionLocal
15 -from app.db.db_session import get_db_session
11 +from pyvelociraptor import api_pb2, api_pb2_grpc
12
13
14 async def verify_velociraptor_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
@@ -60,20 +56,31 @@ async def verify_velociraptor_credentials(attributes: Dict[str, Any]) -> Dict[st
56 for response in stub.Query(client_request):
57 if response.Response:
58 r = r + json.loads(response.Response)
63 - return {"connectionSuccessful": True, "message": "Connection to Velociraptor successful"}
59 + return {
60 + "connectionSuccessful": True,
61 + "message": "Connection to Velociraptor successful",
62 + }
63 except Exception as e:
64 logger.error(f"Failed to verify connection to Velociraptor: {e}")
66 - return {"connectionSuccessful": False, "message": f"Failed to verify connection to Velociraptor: {e}"}
65 + return {
66 + "connectionSuccessful": False,
67 + "message": f"Failed to verify connection to Velociraptor: {e}",
68 + }
69 except Exception as e:
70 logger.error(f"Failed to get connector_api_key from the database: {e}")
69 - return {"connectionSuccessful": False, "message": f"Failed to get connector_api_key from the database: {e}"}
71 + return {
72 + "connectionSuccessful": False,
73 + "message": f"Failed to get connector_api_key from the database: {e}",
74 + }
75
76
77 async def verify_velociraptor_connection(connector_name: str) -> str:
78 """
79 Verifies the connection to Velociraptor service.
80 """
76 - logger.info(f"Verifying the Velociraptor connection for connector: {connector_name}")
81 + logger.info(
82 + f"Verifying the Velociraptor connection for connector: {connector_name}",
83 + )
84 async with get_db_session() as session: # This will correctly enter the context manager
85 attributes = await get_connector_info_from_db(connector_name, session)
86 if attributes is None:
@@ -183,7 +190,10 @@ class UniversalService:
190 )
191 else:
192 logger.error(f"Failed to execute query: {e}")
186 - raise HTTPException(status_code=500, detail=f"Failed to execute query: {e.details()}")
193 + raise HTTPException(
194 + status_code=500,
195 + detail=f"Failed to execute query: {e.details()}",
196 + )
197 except Exception as e:
198 logger.error(f"Failed to execute query: {e}")
199 raise HTTPException(status_code=500, detail=f"Failed to execute query: {e}")
@@ -234,8 +244,12 @@ class UniversalService:
244 """
245 # Formulate queries
246 try:
237 - vql_client_id = f"select client_id,os_info from clients(search='host:{client_name}')"
238 - vql_last_seen_at = f"select last_seen_at from clients(search='host:{client_name}')"
247 + vql_client_id = (
248 + f"select client_id,os_info from clients(search='host:{client_name}')"
249 + )
250 + vql_last_seen_at = (
251 + f"select last_seen_at from clients(search='host:{client_name}')"
252 + )
253
254 # Get the last seen timestamp
255 logger.info(f"Getting last seen at timestamp for {client_name}")
@@ -293,7 +307,10 @@ class UniversalService:
307 try:
308 return self.execute_query(vql)["results"][0]["version"]["version"]
309 except IndexError as e:
296 - raise HTTPException(status_code=500, detail=f"Failed to get server version: {e}")
310 + raise HTTPException(
311 + status_code=500,
312 + detail=f"Failed to get server version: {e}",
313 + )
314
315 async def _is_offline(self, last_seen_at: float):
316 """
@@ -305,4 +322,6 @@ class UniversalService:
322 Returns:
323 bool: True if the client is offline, False otherwise.
324 """
308 - return (datetime.now() - datetime.fromtimestamp(last_seen_at / 1000000)).total_seconds() > 30
325 + return (
326 + datetime.now() - datetime.fromtimestamp(last_seen_at / 1000000)
327 + ).total_seconds() > 30
backend/app/connectors/wazuh_indexer/routes/alerts.py
+36 -26
@@ -1,28 +1,28 @@
1 from typing import List
2
3 -from fastapi import APIRouter
4 -from fastapi import Depends
5 -from fastapi import HTTPException
6 -from fastapi import Security
7 -from loguru import logger
8 -
3 from app.auth.utils import AuthHandler
10 -from app.connectors.wazuh_indexer.schema.alerts import AlertsByHostResponse
11 -from app.connectors.wazuh_indexer.schema.alerts import AlertsByRulePerHostResponse
12 -from app.connectors.wazuh_indexer.schema.alerts import AlertsByRuleResponse
13 -from app.connectors.wazuh_indexer.schema.alerts import AlertsSearchBody
14 -from app.connectors.wazuh_indexer.schema.alerts import AlertsSearchResponse
15 -from app.connectors.wazuh_indexer.schema.alerts import HostAlertsSearchBody
16 -from app.connectors.wazuh_indexer.schema.alerts import HostAlertsSearchResponse
17 -from app.connectors.wazuh_indexer.schema.alerts import IndexAlertsSearchBody
18 -from app.connectors.wazuh_indexer.schema.alerts import IndexAlertsSearchResponse
19 -from app.connectors.wazuh_indexer.services.alerts import get_alerts
20 -from app.connectors.wazuh_indexer.services.alerts import get_alerts_by_host
21 -from app.connectors.wazuh_indexer.services.alerts import get_alerts_by_rule
22 -from app.connectors.wazuh_indexer.services.alerts import get_alerts_by_rule_per_host
23 -from app.connectors.wazuh_indexer.services.alerts import get_host_alerts
24 -from app.connectors.wazuh_indexer.services.alerts import get_index_alerts
4 +from app.connectors.wazuh_indexer.schema.alerts import (
5 + AlertsByHostResponse,
6 + AlertsByRulePerHostResponse,
7 + AlertsByRuleResponse,
8 + AlertsSearchBody,
9 + AlertsSearchResponse,
10 + HostAlertsSearchBody,
11 + HostAlertsSearchResponse,
12 + IndexAlertsSearchBody,
13 + IndexAlertsSearchResponse,
14 +)
15 +from app.connectors.wazuh_indexer.services.alerts import (
16 + get_alerts,
17 + get_alerts_by_host,
18 + get_alerts_by_rule,
19 + get_alerts_by_rule_per_host,
20 + get_host_alerts,
21 + get_index_alerts,
22 +)
23 from app.connectors.wazuh_indexer.utils.universal import collect_indices
24 +from fastapi import APIRouter, Depends, HTTPException, Security
25 +from loguru import logger
26
27 # App specific imports
28
@@ -41,7 +41,9 @@ async def get_index_names() -> List[str]:
41 return indices.indices_list
42
43
44 -async def verify_index_name(index_alerts_search_body: IndexAlertsSearchBody) -> IndexAlertsSearchBody:
44 +async def verify_index_name(
45 + index_alerts_search_body: IndexAlertsSearchBody,
46 +) -> IndexAlertsSearchBody:
47 """
48 Verifies if the given index name is managed by Wazuh Indexer or still exists.
49
@@ -92,7 +94,9 @@ async def get_all_alerts(alerts_search_body: AlertsSearchBody) -> AlertsSearchRe
94 description="Get all alerts for a host",
95 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
96 )
95 -async def get_all_alerts_for_host(host_alerts_search_body: HostAlertsSearchBody) -> HostAlertsSearchResponse:
97 +async def get_all_alerts_for_host(
98 + host_alerts_search_body: HostAlertsSearchBody,
99 +) -> HostAlertsSearchResponse:
100 """
101 Get all alerts for a specific host.
102
@@ -134,7 +138,9 @@ async def get_all_alerts_for_index(
138 description="Get number of all alerts for all hosts",
139 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
140 )
137 -async def get_all_alerts_by_host(alerts_search_body: AlertsSearchBody) -> AlertsByHostResponse:
141 +async def get_all_alerts_by_host(
142 + alerts_search_body: AlertsSearchBody,
143 +) -> AlertsByHostResponse:
144 """
145 Fetches the number of all alerts for all hosts.
146
@@ -154,7 +160,9 @@ async def get_all_alerts_by_host(alerts_search_body: AlertsSearchBody) -> Alerts
160 description="Get number of all alerts for all rules",
161 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
162 )
157 -async def get_all_alerts_by_rule(alerts_search_body: AlertsSearchBody) -> AlertsByRuleResponse:
163 +async def get_all_alerts_by_rule(
164 + alerts_search_body: AlertsSearchBody,
165 +) -> AlertsByRuleResponse:
166 """
167 Fetches the number of all alerts for all rules.
168
@@ -174,7 +182,9 @@ async def get_all_alerts_by_rule(alerts_search_body: AlertsSearchBody) -> Alerts
182 description="Get number of all alerts for all rules per host",
183 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
184 )
177 -async def get_all_alerts_by_rule_per_host(alerts_search_body: AlertsSearchBody) -> AlertsByRulePerHostResponse:
185 +async def get_all_alerts_by_rule_per_host(
186 + alerts_search_body: AlertsSearchBody,
187 +) -> AlertsByRulePerHostResponse:
188 """
189 Get number of all alerts for all rules per host
190
backend/app/connectors/wazuh_indexer/routes/monitoring.py
+17 -13
@@ -1,20 +1,21 @@
1 from typing import Union
2
3 -from fastapi import APIRouter
4 -from fastapi import HTTPException
5 -from fastapi import Security
6 -
3 from app.auth.utils import AuthHandler
8 -from app.connectors.wazuh_indexer.schema.monitoring import ClusterHealthResponse
9 -from app.connectors.wazuh_indexer.schema.monitoring import IndicesStatsResponse
10 -from app.connectors.wazuh_indexer.schema.monitoring import NodeAllocationResponse
11 -from app.connectors.wazuh_indexer.schema.monitoring import ShardsResponse
4 +from app.connectors.wazuh_indexer.schema.monitoring import (
5 + ClusterHealthResponse,
6 + IndicesStatsResponse,
7 + NodeAllocationResponse,
8 + ShardsResponse,
9 +)
10
11 # from app.connectors.wazuh_indexer.schema import WazuhIndexerResponse, WazuhIndexerListResponse
14 -from app.connectors.wazuh_indexer.services.monitoring import cluster_healthcheck
15 -from app.connectors.wazuh_indexer.services.monitoring import indices_stats
16 -from app.connectors.wazuh_indexer.services.monitoring import node_allocation
17 -from app.connectors.wazuh_indexer.services.monitoring import shards
12 +from app.connectors.wazuh_indexer.services.monitoring import (
13 + cluster_healthcheck,
14 + indices_stats,
15 + node_allocation,
16 + shards,
17 +)
18 +from fastapi import APIRouter, HTTPException, Security
19
20 wazuh_indexer_router = APIRouter()
21
@@ -66,7 +67,10 @@ async def get_node_allocation() -> Union[NodeAllocationResponse, HTTPException]:
67 if node_allocation_response is not None:
68 return node_allocation_response
69 else:
69 - raise HTTPException(status_code=500, detail="Failed to retrieve node allocation.")
70 + raise HTTPException(
71 + status_code=500,
72 + detail="Failed to retrieve node allocation.",
73 + )
74
75
76 @wazuh_indexer_router.get(
backend/app/connectors/wazuh_indexer/schema/alerts.py
+31 -15
@@ -1,35 +1,43 @@
1 from enum import Enum
2 -from typing import Any
3 -from typing import Dict
4 -from typing import List
5 -from typing import Optional
2 +from typing import Any, Dict, List, Optional
3
7 -from pydantic import BaseModel
8 -from pydantic import Field
9 -from pydantic import validator
4 +from pydantic import BaseModel, Field, validator
5
6
7 class Alert(BaseModel):
8 index_name: str
9 total_alerts: int
15 - alerts: Optional[List[Dict[str, Any]]] = Field([], description="The alerts returned from the search.")
10 + alerts: Optional[List[Dict[str, Any]]] = Field(
11 + [],
12 + description="The alerts returned from the search.",
13 + )
14
15
16 class AlertsSearchBody(BaseModel):
17 size: int = Field(10, description="The number of alerts to return.")
18 timerange: str = Field("24h", description="The time range to search alerts in.")
21 - alert_field: str = Field("syslog_level", description="The field to search alerts in.")
19 + alert_field: str = Field(
20 + "syslog_level",
21 + description="The field to search alerts in.",
22 + )
23 alert_value: str = Field("ALERT", description="The value to search alerts for.")
23 - timestamp_field: str = Field("timestamp_utc", description="The timestamp field to search alerts in.")
24 + timestamp_field: str = Field(
25 + "timestamp_utc",
26 + description="The timestamp field to search alerts in.",
27 + )
28
29 @validator("timerange")
30 def validate_timerange(cls, value):
31 if value[-1] not in ("h", "d", "w"):
28 - raise ValueError("Invalid timerange format. The string should end with either 'h', 'd', 'w'.")
32 + raise ValueError(
33 + "Invalid timerange format. The string should end with either 'h', 'd', 'w'.",
34 + )
35
36 # Optionally, you can check that the prefix is a number
37 if not value[:-1].isdigit():
32 - raise ValueError("Invalid timerange format. The string should start with a number.")
38 + raise ValueError(
39 + "Invalid timerange format. The string should start with a number.",
40 + )
41
42 return value
43
@@ -47,7 +55,10 @@ class CollectAlertsResponse(BaseModel):
55
56
57 class HostAlertsSearchBody(AlertsSearchBody):
50 - agent_name: str = Field(..., description="The name of the agent to search alerts for.")
58 + agent_name: str = Field(
59 + ...,
60 + description="The name of the agent to search alerts for.",
61 + )
62
63
64 class HostAlertsSearchResponse(BaseModel):
@@ -57,7 +68,10 @@ class HostAlertsSearchResponse(BaseModel):
68
69
70 class IndexAlertsSearchBody(AlertsSearchBody):
60 - index_name: str = Field(..., description="The name of the index to search alerts for.")
71 + index_name: str = Field(
72 + ...,
73 + description="The name of the index to search alerts for.",
74 + )
75
76
77 class IndexAlertsSearchResponse(BaseModel):
@@ -102,6 +116,8 @@ class AlertsByRulePerHostResponse(BaseModel):
116
117 ############# ! PASSABLE MESSAGES FROM ES CLIENT ! #############
118 class SkippableWazuhIndexerClientErrors(Enum):
105 - NO_MAPPING_FOR_TIMESTAMP = "No mapping found for [timestamp_utc] in order to sort on"
119 + NO_MAPPING_FOR_TIMESTAMP = (
120 + "No mapping found for [timestamp_utc] in order to sort on"
121 + )
122 # Add other error messages here, for example:
123 # ANOTHER_ERROR = "Another specific error message"
backend/app/connectors/wazuh_indexer/schema/indices.py
+1 -2
@@ -1,7 +1,6 @@
1 from typing import Dict
2
3 -from pydantic import BaseModel
4 -from pydantic import Field
3 +from pydantic import BaseModel, Field
4
5
6 class Indices(BaseModel):
backend/app/connectors/wazuh_indexer/schema/monitoring.py
+2 -5
@@ -1,9 +1,6 @@
1 -from typing import List
2 -from typing import Optional
3 -from typing import Union
1 +from typing import List, Optional, Union
2
5 -from pydantic import BaseModel
6 -from pydantic import Field
3 +from pydantic import BaseModel, Field
4
5
6 class ClusterHealth(BaseModel):
backend/app/connectors/wazuh_indexer/services/alerts.py
+124 -51
@@ -1,31 +1,34 @@
1 -from typing import Dict
2 -from typing import List
3 -from typing import Optional
4 -from typing import Type
5 -
1 +from typing import Dict, List, Optional, Type
2 +
3 +from app.connectors.wazuh_indexer.schema.alerts import (
4 + AlertsByHost,
5 + AlertsByHostResponse,
6 + AlertsByRule,
7 + AlertsByRulePerHost,
8 + AlertsByRulePerHostResponse,
9 + AlertsByRuleResponse,
10 + AlertsSearchBody,
11 + AlertsSearchResponse,
12 + CollectAlertsResponse,
13 + HostAlertsSearchBody,
14 + HostAlertsSearchResponse,
15 + IndexAlertsSearchBody,
16 + IndexAlertsSearchResponse,
17 + SkippableWazuhIndexerClientErrors,
18 +)
19 +from app.connectors.wazuh_indexer.utils.universal import (
20 + AlertsQueryBuilder,
21 + collect_indices,
22 + create_wazuh_indexer_client,
23 +)
24 from fastapi import HTTPException
25 from loguru import logger
26
9 -from app.connectors.wazuh_indexer.schema.alerts import AlertsByHost
10 -from app.connectors.wazuh_indexer.schema.alerts import AlertsByHostResponse
11 -from app.connectors.wazuh_indexer.schema.alerts import AlertsByRule
12 -from app.connectors.wazuh_indexer.schema.alerts import AlertsByRulePerHost
13 -from app.connectors.wazuh_indexer.schema.alerts import AlertsByRulePerHostResponse
14 -from app.connectors.wazuh_indexer.schema.alerts import AlertsByRuleResponse
15 -from app.connectors.wazuh_indexer.schema.alerts import AlertsSearchBody
16 -from app.connectors.wazuh_indexer.schema.alerts import AlertsSearchResponse
17 -from app.connectors.wazuh_indexer.schema.alerts import CollectAlertsResponse
18 -from app.connectors.wazuh_indexer.schema.alerts import HostAlertsSearchBody
19 -from app.connectors.wazuh_indexer.schema.alerts import HostAlertsSearchResponse
20 -from app.connectors.wazuh_indexer.schema.alerts import IndexAlertsSearchBody
21 -from app.connectors.wazuh_indexer.schema.alerts import IndexAlertsSearchResponse
22 -from app.connectors.wazuh_indexer.schema.alerts import SkippableWazuhIndexerClientErrors
23 -from app.connectors.wazuh_indexer.utils.universal import AlertsQueryBuilder
24 -from app.connectors.wazuh_indexer.utils.universal import collect_indices
25 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
26 -
27 -
28 -async def collect_and_aggregate_alerts(field_names: List[str], search_body: AlertsSearchBody) -> Dict[str, int]:
27 +
28 +async def collect_and_aggregate_alerts(
29 + field_names: List[str],
30 + search_body: AlertsSearchBody,
31 +) -> Dict[str, int]:
32 """
33 Collects and aggregates alerts based on the specified field names and search body.
34
@@ -45,21 +48,38 @@ async def collect_and_aggregate_alerts(field_names: List[str], search_body: Aler
48 alerts_response = await collect_alerts_generic(index_name, body=search_body)
49 if alerts_response.success:
50 for alert in alerts_response.alerts:
48 - composite_key = tuple(alert["_source"][field] for field in field_names)
49 - aggregated_alerts_dict[composite_key] = aggregated_alerts_dict.get(composite_key, 0) + 1
51 + composite_key = tuple(
52 + alert["_source"][field] for field in field_names
53 + )
54 + aggregated_alerts_dict[composite_key] = (
55 + aggregated_alerts_dict.get(composite_key, 0) + 1
56 + )
57 except HTTPException as e:
58 detail_str = str(e.detail) # Convert to string to make sure it's comparable
52 - if any(err.value in detail_str for err in SkippableWazuhIndexerClientErrors):
53 - logger.warning(f"Skipping index {index_name} due to specific error: {e.detail}")
59 + if any(
60 + err.value in detail_str for err in SkippableWazuhIndexerClientErrors
61 + ):
62 + logger.warning(
63 + f"Skipping index {index_name} due to specific error: {e.detail}",
64 + )
65 continue # Skip this index and continue with the next one
66 else:
56 - logger.warning(f"An error occurred while processing index {index_name}: {e.detail}")
57 - raise HTTPException(status_code=500, detail=f"An error occurred while processing index {index_name}: {e.detail}")
67 + logger.warning(
68 + f"An error occurred while processing index {index_name}: {e.detail}",
69 + )
70 + raise HTTPException(
71 + status_code=500,
72 + detail=f"An error occurred while processing index {index_name}: {e.detail}",
73 + )
74
75 return aggregated_alerts_dict
76
77
62 -async def collect_alerts_generic(index_name: str, body: AlertsSearchBody, is_host_specific: bool = False) -> CollectAlertsResponse:
78 +async def collect_alerts_generic(
79 + index_name: str,
80 + body: AlertsSearchBody,
81 + is_host_specific: bool = False,
82 +) -> CollectAlertsResponse:
83 """
84 Collects alerts from the specified index based on the provided search criteria.
85
@@ -78,7 +98,10 @@ async def collect_alerts_generic(index_name: str, body: AlertsSearchBody, is_hos
98 """
99 es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
100 query_builder = AlertsQueryBuilder()
81 - query_builder.add_time_range(timerange=body.timerange, timestamp_field=body.timestamp_field)
101 + query_builder.add_time_range(
102 + timerange=body.timerange,
103 + timestamp_field=body.timestamp_field,
104 + )
105 query_builder.add_matches(matches=[(body.alert_field, body.alert_value)])
106 query_builder.add_sort(body.timestamp_field)
107
@@ -92,13 +115,24 @@ async def collect_alerts_generic(index_name: str, body: AlertsSearchBody, is_hos
115 logger.info(f"Alerts collected: {alerts}")
116 alerts_list = [alert for alert in alerts["hits"]["hits"]]
117 logger.info(f"Alerts collected: {alerts_list}")
95 - return CollectAlertsResponse(alerts=alerts_list, success=True, message="Alerts collected successfully")
118 + return CollectAlertsResponse(
119 + alerts=alerts_list,
120 + success=True,
121 + message="Alerts collected successfully",
122 + )
123 except Exception as e:
124 logger.warning(f"An error occurred while collecting alerts: {e}")
98 - raise HTTPException(status_code=500, detail=f"An error occurred while collecting alerts: {e}")
125 + raise HTTPException(
126 + status_code=500,
127 + detail=f"An error occurred while collecting alerts: {e}",
128 + )
129
130
101 -async def get_alerts_generic(search_body: Type[AlertsSearchBody], is_host_specific: bool = False, index_name: Optional[str] = None):
131 +async def get_alerts_generic(
132 + search_body: Type[AlertsSearchBody],
133 + is_host_specific: bool = False,
134 + index_name: Optional[str] = None,
135 +):
136 """
137 Retrieves alerts from the Wazuh Indexer based on the provided search criteria.
138
@@ -110,14 +144,22 @@ async def get_alerts_generic(search_body: Type[AlertsSearchBody], is_host_specif
144 Returns:
145 dict: A dictionary containing the alerts summary, success status, and a message.
146 """
113 - logger.info(f"Collecting Wazuh Indexer alerts for host {search_body.agent_name if is_host_specific else ''}")
147 + logger.info(
148 + f"Collecting Wazuh Indexer alerts for host {search_body.agent_name if is_host_specific else ''}",
149 + )
150 alerts_summary = []
151 indices = await collect_indices()
116 - index_list = [index_name] if index_name else indices.indices_list # Use the provided index_name or get all indices
152 + index_list = (
153 + [index_name] if index_name else indices.indices_list
154 + ) # Use the provided index_name or get all indices
155
156 for index_name in index_list:
157 try:
120 - alerts = await collect_alerts_generic(index_name, body=search_body, is_host_specific=is_host_specific)
158 + alerts = await collect_alerts_generic(
159 + index_name,
160 + body=search_body,
161 + is_host_specific=is_host_specific,
162 + )
163 if alerts.success and len(alerts.alerts) > 0:
164 alerts_summary.append(
165 {
@@ -128,19 +170,32 @@ async def get_alerts_generic(search_body: Type[AlertsSearchBody], is_host_specif
170 )
171 except HTTPException as e:
172 detail_str = str(e.detail) # Convert to string to make sure it's comparable
131 - if any(err.value in detail_str for err in SkippableWazuhIndexerClientErrors):
132 - logger.warning(f"Skipping index {index_name} due to specific error: {e.detail}")
173 + if any(
174 + err.value in detail_str for err in SkippableWazuhIndexerClientErrors
175 + ):
176 + logger.warning(
177 + f"Skipping index {index_name} due to specific error: {e.detail}",
178 + )
179 continue # Skip this index and continue with the next one
180 else:
135 - logger.warning(f"An error occurred while processing index {index_name}: {e.detail}")
136 - raise HTTPException(status_code=500, detail=f"An error occurred while processing index {index_name}: {e.detail}")
181 + logger.warning(
182 + f"An error occurred while processing index {index_name}: {e.detail}",
183 + )
184 + raise HTTPException(
185 + status_code=500,
186 + detail=f"An error occurred while processing index {index_name}: {e.detail}",
187 + )
188
189 if len(alerts_summary) == 0:
190 message = "No alerts found"
191 else:
192 message = f"Succesfully collected top {search_body.size} alerts for each index"
193
143 - return {"alerts_summary": alerts_summary, "success": len(alerts_summary) > 0, "message": message}
194 + return {
195 + "alerts_summary": alerts_summary,
196 + "success": len(alerts_summary) > 0,
197 + "message": message,
198 + }
199
200
201 async def get_alerts(search_body: AlertsSearchBody) -> AlertsSearchResponse:
@@ -157,7 +212,9 @@ async def get_alerts(search_body: AlertsSearchBody) -> AlertsSearchResponse:
212 return AlertsSearchResponse(**result)
213
214
160 -async def get_host_alerts(search_body: HostAlertsSearchBody) -> HostAlertsSearchResponse:
215 +async def get_host_alerts(
216 + search_body: HostAlertsSearchBody,
217 +) -> HostAlertsSearchResponse:
218 """
219 Retrieves alerts specific to a host.
220
@@ -171,7 +228,9 @@ async def get_host_alerts(search_body: HostAlertsSearchBody) -> HostAlertsSearch
228 return HostAlertsSearchResponse(**result)
229
230
174 -async def get_index_alerts(search_body: IndexAlertsSearchBody) -> IndexAlertsSearchResponse:
231 +async def get_index_alerts(
232 + search_body: IndexAlertsSearchBody,
233 +) -> IndexAlertsSearchResponse:
234 """
235 Retrieves alerts from the specified index based on the search criteria.
236
@@ -198,7 +257,10 @@ async def get_alerts_by_host(search_body: AlertsSearchBody) -> AlertsByHostRespo
257 """
258 aggregated_by_host = await collect_and_aggregate_alerts(["agent_name"], search_body)
259 alerts_by_host_list: List[AlertsByHost] = [
201 - AlertsByHost(agent_name=host[0], number_of_alerts=count) # host[0] because host is now a tuple
260 + AlertsByHost(
261 + agent_name=host[0],
262 + number_of_alerts=count,
263 + ) # host[0] because host is now a tuple
264 for host, count in aggregated_by_host.items()
265 ]
266 return AlertsByHostResponse(
@@ -219,9 +281,15 @@ async def get_alerts_by_rule(search_body: AlertsSearchBody) -> AlertsByRuleRespo
281 AlertsByRuleResponse: The response containing the alerts grouped by rule.
282
283 """
222 - aggregated_by_rule = await collect_and_aggregate_alerts(["rule_description"], search_body)
284 + aggregated_by_rule = await collect_and_aggregate_alerts(
285 + ["rule_description"],
286 + search_body,
287 + )
288 alerts_by_rule_list: List[AlertsByRule] = [
224 - AlertsByRule(rule=rule[0], number_of_alerts=count) # rule[0] because rule is now a tuple
289 + AlertsByRule(
290 + rule=rule[0],
291 + number_of_alerts=count,
292 + ) # rule[0] because rule is now a tuple
293 for rule, count in aggregated_by_rule.items()
294 ]
295 return AlertsByRuleResponse(
@@ -231,7 +299,9 @@ async def get_alerts_by_rule(search_body: AlertsSearchBody) -> AlertsByRuleRespo
299 )
300
301
234 -async def get_alerts_by_rule_per_host(search_body: AlertsSearchBody) -> AlertsByRulePerHostResponse:
302 +async def get_alerts_by_rule_per_host(
303 + search_body: AlertsSearchBody,
304 +) -> AlertsByRulePerHostResponse:
305 """
306 Retrieves alerts grouped by rule per host based on the provided search criteria.
307
@@ -242,7 +312,10 @@ async def get_alerts_by_rule_per_host(search_body: AlertsSearchBody) -> AlertsBy
312 AlertsByRulePerHostResponse: The response containing the alerts grouped by rule per host.
313
314 """
245 - aggregated_by_rule_per_host = await collect_and_aggregate_alerts(["agent_name", "rule_description"], search_body)
315 + aggregated_by_rule_per_host = await collect_and_aggregate_alerts(
316 + ["agent_name", "rule_description"],
317 + search_body,
318 + )
319 alerts_by_rule_per_host_list: List[AlertsByRulePerHost] = [
320 AlertsByRulePerHost(agent_name=agent_name, rule=rule, number_of_alerts=count)
321 for (agent_name, rule), count in aggregated_by_rule_per_host.items()
backend/app/connectors/wazuh_indexer/services/monitoring.py
+35 -21
@@ -1,21 +1,23 @@
1 -from typing import Dict
2 -from typing import Union
3 -
1 +from typing import Dict, Union
2 +
3 +from app.connectors.wazuh_indexer.schema.monitoring import (
4 + ClusterHealth,
5 + ClusterHealthResponse,
6 + IndicesStats,
7 + IndicesStatsResponse,
8 + NodeAllocation,
9 + NodeAllocationResponse,
10 + Shards,
11 + ShardsResponse,
12 +)
13 +from app.connectors.wazuh_indexer.utils.universal import (
14 + create_wazuh_indexer_client,
15 + format_indices_stats,
16 + format_node_allocation,
17 + format_shards,
18 +)
19 from loguru import logger
20
6 -from app.connectors.wazuh_indexer.schema.monitoring import ClusterHealth
7 -from app.connectors.wazuh_indexer.schema.monitoring import ClusterHealthResponse
8 -from app.connectors.wazuh_indexer.schema.monitoring import IndicesStats
9 -from app.connectors.wazuh_indexer.schema.monitoring import IndicesStatsResponse
10 -from app.connectors.wazuh_indexer.schema.monitoring import NodeAllocation
11 -from app.connectors.wazuh_indexer.schema.monitoring import NodeAllocationResponse
12 -from app.connectors.wazuh_indexer.schema.monitoring import Shards
13 -from app.connectors.wazuh_indexer.schema.monitoring import ShardsResponse
14 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
15 -from app.connectors.wazuh_indexer.utils.universal import format_indices_stats
16 -from app.connectors.wazuh_indexer.utils.universal import format_node_allocation
17 -from app.connectors.wazuh_indexer.utils.universal import format_shards
18 -
21
22 async def cluster_healthcheck() -> Union[ClusterHealthResponse, Dict[str, str]]:
23 """
@@ -58,9 +60,13 @@ async def node_allocation() -> Union[NodeAllocationResponse, Dict[str, bool]]:
60 raw_node_allocation_data = es_client.cat.allocation(format="json")
61 logger.info(raw_node_allocation_data)
62
61 - formatted_node_allocation_data = await format_node_allocation(raw_node_allocation_data)
63 + formatted_node_allocation_data = await format_node_allocation(
64 + raw_node_allocation_data,
65 + )
66
63 - node_allocation_models = [NodeAllocation(**node) for node in formatted_node_allocation_data]
67 + node_allocation_models = [
68 + NodeAllocation(**node) for node in formatted_node_allocation_data
69 + ]
70
71 return NodeAllocationResponse(
72 node_allocation=node_allocation_models,
@@ -87,9 +93,13 @@ async def indices_stats() -> Union[IndicesStatsResponse, Dict[str, str]]:
93 try:
94 raw_indices_stats_data = es_client.cat.indices(format="json")
95
90 - formatted_indices_stats_data = await format_indices_stats(raw_indices_stats_data)
96 + formatted_indices_stats_data = await format_indices_stats(
97 + raw_indices_stats_data,
98 + )
99
92 - indices_stats_models = [IndicesStats(**index) for index in formatted_indices_stats_data]
100 + indices_stats_models = [
101 + IndicesStats(**index) for index in formatted_indices_stats_data
102 + ]
103
104 return IndicesStatsResponse(
105 indices_stats=indices_stats_models,
@@ -120,7 +130,11 @@ async def shards() -> Union[ShardsResponse, Dict[str, str]]:
130
131 shard_models = [Shards(**shard) for shard in formatted_shards_data]
132
123 - return ShardsResponse(shards=shard_models, success=True, message="Successfully collected Wazuh Indexer shards")
133 + return ShardsResponse(
134 + shards=shard_models,
135 + success=True,
136 + message="Successfully collected Wazuh Indexer shards",
137 + )
138 except Exception as e:
139 logger.error(f"Shards check failed with error: {e}")
140 e = f"Shards check failed with error: {e}"
backend/app/connectors/wazuh_indexer/utils/universal.py
+71 -30
@@ -1,32 +1,33 @@
1 -from datetime import datetime
2 -from datetime import timedelta
3 -from typing import Any
4 -from typing import Dict
5 -from typing import Iterable
6 -from typing import Tuple
1 +from datetime import datetime, timedelta
2 +from typing import Any, Dict, Iterable, Tuple
3
4 +from app.connectors.utils import get_connector_info_from_db
5 +from app.connectors.wazuh_indexer.schema.indices import IndexConfigModel, Indices
6 +from app.db.db_session import get_db_session
7 from elasticsearch7 import Elasticsearch
8 from fastapi import HTTPException
9 from loguru import logger
10
12 -from app.connectors.utils import get_connector_info_from_db
13 -from app.connectors.wazuh_indexer.schema.indices import IndexConfigModel
14 -from app.connectors.wazuh_indexer.schema.indices import Indices
15 -from app.db.db_session import get_db_session
11
17 -
18 -async def verify_wazuh_indexer_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
12 +async def verify_wazuh_indexer_credentials(
13 + attributes: Dict[str, Any],
14 +) -> Dict[str, Any]:
15 """
16 Verifies the connection to Wazuh Indexer service.
17
18 Returns:
19 dict: A dictionary containing 'connectionSuccessful' status and 'authToken' if the connection is successful.
20 """
25 - logger.info(f"Verifying the wazuh-indexer connection to {attributes['connector_url']}")
21 + logger.info(
22 + f"Verifying the wazuh-indexer connection to {attributes['connector_url']}",
23 + )
24 try:
25 es = Elasticsearch(
26 [attributes["connector_url"]],
29 - http_auth=(attributes["connector_username"], attributes["connector_password"]),
27 + http_auth=(
28 + attributes["connector_username"],
29 + attributes["connector_password"],
30 + ),
31 verify_certs=False,
32 timeout=15,
33 max_retries=10,
@@ -34,10 +35,18 @@ async def verify_wazuh_indexer_credentials(attributes: Dict[str, Any]) -> Dict[s
35 )
36 es.cluster.health()
37 logger.debug("Wazuh Indexer connection successful")
37 - return {"connectionSuccessful": True, "message": "Wazuh Indexer connection successful"}
38 + return {
39 + "connectionSuccessful": True,
40 + "message": "Wazuh Indexer connection successful",
41 + }
42 except Exception as e:
39 - logger.error(f"Connection to {attributes['connector_url']} failed with error: {e}")
40 - return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error: {e}"}
43 + logger.error(
44 + f"Connection to {attributes['connector_url']} failed with error: {e}",
45 + )
46 + return {
47 + "connectionSuccessful": False,
48 + "message": f"Connection to {attributes['connector_url']} failed with error: {e}",
49 + }
50
51
52 async def verify_wazuh_indexer_connection(connector_name: str) -> str:
@@ -49,7 +58,9 @@ async def verify_wazuh_indexer_connection(connector_name: str) -> str:
58 """
59 async with get_db_session() as session: # This will correctly enter the context manager
60 attributes = await get_connector_info_from_db(connector_name, session)
52 - logger.info(f"Verifying the wazuh-indexer connection to {attributes['connector_url']}")
61 + logger.info(
62 + f"Verifying the wazuh-indexer connection to {attributes['connector_url']}",
63 + )
64 if attributes is None:
65 logger.error("No Wazuh Indexer connector found in the database")
66 return None
@@ -67,20 +78,32 @@ async def create_wazuh_indexer_client(connector_name: str) -> Elasticsearch:
78 async with get_db_session() as session: # This will correctly enter the context manager
79 attributes = await get_connector_info_from_db(connector_name, session)
80 if attributes is None:
70 - raise HTTPException(status_code=500, detail=f"No {connector_name} connector found in the database")
81 + raise HTTPException(
82 + status_code=500,
83 + detail=f"No {connector_name} connector found in the database",
84 + )
85 if attributes["connector_url"] == "https://1.1.1.1:9200":
72 - raise HTTPException(status_code=500, detail=f"Please update the {connector_name} connector URL")
86 + raise HTTPException(
87 + status_code=500,
88 + detail=f"Please update the {connector_name} connector URL",
89 + )
90 try:
91 return Elasticsearch(
92 [attributes["connector_url"]],
76 - http_auth=(attributes["connector_username"], attributes["connector_password"]),
93 + http_auth=(
94 + attributes["connector_username"],
95 + attributes["connector_password"],
96 + ),
97 verify_certs=False,
98 timeout=15,
99 max_retries=10,
100 retry_on_timeout=False,
101 )
102 except Exception as e:
83 - raise HTTPException(status_code=500, detail=f"Failed to create Elasticsearch client: {e}")
103 + raise HTTPException(
104 + status_code=500,
105 + detail=f"Failed to create Elasticsearch client: {e}",
106 + )
107
108
109 async def format_node_allocation(node_allocation):
@@ -166,8 +189,14 @@ async def collect_indices() -> Indices:
189 indices_list = list(indices_dict.keys())
190 # Check if the index is valid
191 index_config = IndexConfigModel()
169 - indices_list = [index for index in indices_list if index_config.is_valid_index(index)]
170 - return Indices(indices_list=indices_list, success=True, message="Indices collected successfully")
192 + indices_list = [
193 + index for index in indices_list if index_config.is_valid_index(index)
194 + ]
195 + return Indices(
196 + indices_list=indices_list,
197 + success=True,
198 + message="Indices collected successfully",
199 + )
200 except Exception as e:
201 logger.error(f"Failed to collect indices: {e}")
202 raise HTTPException(status_code=500, detail=f"Failed to collect indices: {e}")
@@ -192,10 +221,14 @@ class AlertsQueryBuilder:
221 elif timerange.endswith("w"):
222 delta = timedelta(weeks=int(timerange[:-1]))
223 else:
195 - raise ValueError("Invalid timerange format. Expected a string like '24h', '1d', '1w', etc.")
224 + raise ValueError(
225 + "Invalid timerange format. Expected a string like '24h', '1d', '1w', etc.",
226 + )
227
228 start = datetime.utcnow() - delta
198 - return start.isoformat() + "Z" # Elasticsearch expects the time in ISO format with a Z at the end
229 + return (
230 + start.isoformat() + "Z"
231 + ) # Elasticsearch expects the time in ISO format with a Z at the end
232
233 def __init__(self):
234 self.query = {
@@ -219,7 +252,9 @@ class AlertsQueryBuilder:
252 self: The updated instance of the class.
253 """
254 start = self._get_time_range_start(timerange)
222 - self.query["query"]["bool"]["must"].append({"range": {timestamp_field: {"gte": start, "lte": "now"}}})
255 + self.query["query"]["bool"]["must"].append(
256 + {"range": {timestamp_field: {"gte": start, "lte": "now"}}},
257 + )
258 return self
259
260 def add_matches(self, matches: Iterable[Tuple[str, str]]):
@@ -310,10 +345,14 @@ class LogsQueryBuilder:
345 elif timerange.endswith("w"):
346 delta = timedelta(weeks=int(timerange[:-1]))
347 else:
313 - raise ValueError("Invalid timerange format. Expected a string like '24h', '1d', '1w', '1m', etc.")
348 + raise ValueError(
349 + "Invalid timerange format. Expected a string like '24h', '1d', '1w', '1m', etc.",
350 + )
351
352 start = datetime.utcnow() - delta
316 - return start.isoformat() + "Z" # Elasticsearch expects the time in ISO format with a Z at the end
353 + return (
354 + start.isoformat() + "Z"
355 + ) # Elasticsearch expects the time in ISO format with a Z at the end
356
357 def __init__(self):
358 self.query = {
@@ -337,7 +376,9 @@ class LogsQueryBuilder:
376 self: The updated instance of the class.
377 """
378 start = self._get_time_range_start(timerange)
340 - self.query["query"]["bool"]["must"].append({"range": {timestamp_field: {"gte": start, "lte": "now"}}})
379 + self.query["query"]["bool"]["must"].append(
380 + {"range": {timestamp_field: {"gte": start, "lte": "now"}}},
381 + )
382 return self
383
384 def add_matches(self, matches: Iterable[Tuple[str, str]]):
backend/app/connectors/wazuh_manager/models/rules.py
+1 -2
@@ -1,8 +1,7 @@
1 import datetime
2 from typing import Optional
3
4 -from sqlmodel import Field
5 -from sqlmodel import SQLModel
4 +from sqlmodel import Field, SQLModel
5
6
7 class DisabledRule(SQLModel, table=True):
backend/app/connectors/wazuh_manager/routes/rules.py
+30 -20
@@ -1,27 +1,24 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from fastapi import HTTPException
4 -from fastapi import Security
5 -from loguru import logger
6 -from sqlalchemy.ext.asyncio import AsyncSession
7 -from sqlalchemy.future import select
8 -
1 # App specific imports
2 from app.auth.routes.auth import AuthHandler
3 from app.connectors.wazuh_manager.models.rules import DisabledRule
12 -from app.connectors.wazuh_manager.schema.rules import AllDisabledRuleResponse
13 -from app.connectors.wazuh_manager.schema.rules import RuleDisable
14 -from app.connectors.wazuh_manager.schema.rules import RuleDisableResponse
15 -from app.connectors.wazuh_manager.schema.rules import RuleEnable
16 -from app.connectors.wazuh_manager.schema.rules import RuleEnableResponse
4 +from app.connectors.wazuh_manager.schema.rules import (
5 + AllDisabledRuleResponse,
6 + RuleDisable,
7 + RuleDisableResponse,
8 + RuleEnable,
9 + RuleEnableResponse,
10 +)
11
12 # from app.connectors.wazuh_manager.schema.rules import RuleExclude
13 # from app.connectors.wazuh_manager.schema.rules import RuleExcludeResponse
20 -from app.connectors.wazuh_manager.services.rules import disable_rule
21 -from app.connectors.wazuh_manager.services.rules import enable_rule
14 +from app.connectors.wazuh_manager.services.rules import disable_rule, enable_rule
15
16 # from app.connectors.wazuh_manager.services.rules import exclude_rule
17 from app.db.db_session import get_db
18 +from fastapi import APIRouter, Depends, HTTPException, Security
19 +from loguru import logger
20 +from sqlalchemy.ext.asyncio import AsyncSession
21 +from sqlalchemy.future import select
22
23 # from app.connectors.wazuh_manager.schema.rules import RuleExclude
24 # from app.connectors.wazuh_manager.schema.rules import RuleExcludeResponse
@@ -40,7 +37,9 @@ auth_handler = AuthHandler()
37 description="Get all disabled rules",
38 dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
39 )
43 -async def get_disabled_rules(session: AsyncSession = Depends(get_db)) -> AllDisabledRuleResponse:
40 +async def get_disabled_rules(
41 + session: AsyncSession = Depends(get_db),
42 +) -> AllDisabledRuleResponse:
43 """
44 Retrieve all disabled rules from the database.
45
@@ -52,7 +51,11 @@ async def get_disabled_rules(session: AsyncSession = Depends(get_db)) -> AllDisa
51 """
52 result = await session.execute(select(DisabledRule))
53 disabled_rules = result.scalars().all()
55 - return AllDisabledRuleResponse(disabled_rules=disabled_rules, success=True, message="Successfully fetched all disabled rules")
54 + return AllDisabledRuleResponse(
55 + disabled_rules=disabled_rules,
56 + success=True,
57 + message="Successfully fetched all disabled rules",
58 + )
59
60
61 @wazuh_manager_rules_router.post(
@@ -81,7 +84,9 @@ async def disable_wazuh_rule(
84 HTTPException: If the rule is already disabled or if the rule cannot be disabled.
85 """
86 # Asynchronously check if the rule is already disabled
84 - result = await session.execute(select(DisabledRule).where(DisabledRule.rule_id == rule.rule_id))
87 + result = await session.execute(
88 + select(DisabledRule).where(DisabledRule.rule_id == rule.rule_id),
89 + )
90 if result.scalars().first():
91 raise HTTPException(status_code=500, detail="Rule is already disabled")
92
@@ -109,7 +114,10 @@ async def disable_wazuh_rule(
114 description="Enable a Wazuh Rule",
115 dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
116 )
112 -async def enable_wazuh_rule(rule: RuleEnable, session: AsyncSession = Depends(get_db)) -> RuleEnableResponse:
117 +async def enable_wazuh_rule(
118 + rule: RuleEnable,
119 + session: AsyncSession = Depends(get_db),
120 +) -> RuleEnableResponse:
121 """
122 Enable a Wazuh rule.
123
@@ -125,7 +133,9 @@ async def enable_wazuh_rule(rule: RuleEnable, session: AsyncSession = Depends(ge
133 """
134 # Asynchronously fetch the disabled rule
135 logger.info(f"rule: {rule}")
128 - result = await session.execute(select(DisabledRule).where(DisabledRule.rule_id == rule.rule_id))
136 + result = await session.execute(
137 + select(DisabledRule).where(DisabledRule.rule_id == rule.rule_id),
138 + )
139 disabled_rule = result.scalars().first()
140
141 if not disabled_rule:
backend/app/connectors/wazuh_manager/schema/rules.py
+2 -4
@@ -1,8 +1,6 @@
1 -from typing import List
2 -from typing import Optional
1 +from typing import List, Optional
2
4 -from pydantic import BaseModel
5 -from pydantic import Field
3 +from pydantic import BaseModel, Field
4
5
6 class RuleDisable(BaseModel):
backend/app/connectors/wazuh_manager/services/rules.py
+38 -21
@@ -1,26 +1,25 @@
1 import re
2 from enum import Enum
3 -from typing import Any
4 -from typing import Dict
5 -from typing import List
6 -from typing import Tuple
7 -from typing import Union
3 +from typing import Any, Dict, List, Tuple, Union
4
5 # import pcre2
6 import xmltodict
7 +from app.connectors.wazuh_manager.schema.rules import (
8 + RuleDisable,
9 + RuleDisableResponse,
10 + RuleEnable,
11 + RuleEnableResponse,
12 + RuleExclude,
13 + RuleExcludeResponse,
14 +)
15 +from app.connectors.wazuh_manager.utils.universal import (
16 + restart_service,
17 + send_get_request,
18 + send_put_request,
19 +)
20 from fastapi import HTTPException
21 from loguru import logger
22
14 -from app.connectors.wazuh_manager.schema.rules import RuleDisable
15 -from app.connectors.wazuh_manager.schema.rules import RuleDisableResponse
16 -from app.connectors.wazuh_manager.schema.rules import RuleEnable
17 -from app.connectors.wazuh_manager.schema.rules import RuleEnableResponse
18 -from app.connectors.wazuh_manager.schema.rules import RuleExclude
19 -from app.connectors.wazuh_manager.schema.rules import RuleExcludeResponse
20 -from app.connectors.wazuh_manager.utils.universal import restart_service
21 -from app.connectors.wazuh_manager.utils.universal import send_get_request
22 -from app.connectors.wazuh_manager.utils.universal import send_put_request
23 -
23
24 async def fetch_filename(rule_id: str) -> str:
25 """
@@ -39,7 +38,10 @@ async def fetch_filename(rule_id: str) -> str:
38 params = {"rule_ids": rule_id}
39 filename_data = await send_get_request(endpoint=endpoint, params=params)
40 if filename_data["data"]["data"]["total_affected_items"] == 0:
42 - raise HTTPException(status_code=404, detail=f"Rule {rule_id} not found. Make sure the rule ID is correct within the Wazuh Manager.")
41 + raise HTTPException(
42 + status_code=404,
43 + detail=f"Rule {rule_id} not found. Make sure the rule ID is correct within the Wazuh Manager.",
44 + )
45 return filename_data["data"]["data"]["affected_items"][0]["filename"]
46
47
@@ -66,7 +68,11 @@ async def fetch_file_content(filename: str) -> str:
68 return file_content_data["data"]["data"]["affected_items"][0]["group"]
69
70
69 -async def set_rule_level(file_content: Any, rule_id: str, new_level: str) -> Tuple[str, Any]:
71 +async def set_rule_level(
72 + file_content: Any,
73 + rule_id: str,
74 + new_level: str,
75 +) -> Tuple[str, Any]:
76 """
77 Sets the level of a rule identified by its ID in the given file content.
78
@@ -97,7 +103,9 @@ async def set_rule_level(file_content: Any, rule_id: str, new_level: str) -> Tup
103 return previous_level, file_content
104
105
100 -async def convert_to_xml(updated_file_content: Union[Dict[str, str], List[Dict[str, str]]]) -> str:
106 +async def convert_to_xml(
107 + updated_file_content: Union[Dict[str, str], List[Dict[str, str]]],
108 +) -> str:
109 """
110 Converts the updated file content to XML format.
111
@@ -115,7 +123,10 @@ async def convert_to_xml(updated_file_content: Union[Dict[str, str], List[Dict[s
123 for group in updated_file_content:
124 xml_dict = {"group": group}
125 xml_content = xmltodict.unparse(xml_dict, pretty=True)
118 - xml_content = xml_content.replace('<?xml version="1.0" encoding="utf-8"?>', "")
126 + xml_content = xml_content.replace(
127 + '<?xml version="1.0" encoding="utf-8"?>',
128 + "",
129 + )
130 xml_content_list.append(xml_content)
131 except Exception as e:
132 raise HTTPException(status_code=500, detail=f"Failed to convert to XML: {e}")
@@ -145,7 +156,10 @@ async def upload_updated_rule(filename: str, xml_content: str):
156 )
157 logger.info(response)
158 if response["data"]["data"]["total_affected_items"] == 0:
148 - raise HTTPException(status_code=500, detail="Failed to upload updated rule to Wazuh Manager.")
159 + raise HTTPException(
160 + status_code=500,
161 + detail="Failed to upload updated rule to Wazuh Manager.",
162 + )
163 return response
164
165
@@ -163,7 +177,10 @@ async def process_rule(rule, rule_action_func, ResponseModel):
177 An instance of ResponseModel with the previous level, success status, and a message.
178 """
179 filename, file_content = await fetch_filename_and_content(rule.rule_id)
166 - previous_level, updated_file_content = await rule_action_func(file_content, rule.rule_id)
180 + previous_level, updated_file_content = await rule_action_func(
181 + file_content,
182 + rule.rule_id,
183 + )
184 xml_content = await convert_to_xml(updated_file_content)
185 await upload_updated_rule(filename, xml_content)
186 await restart_service()
backend/app/connectors/wazuh_manager/utils/universal.py
+96 -30
@@ -1,23 +1,23 @@
1 -from typing import Any
2 -from typing import Dict
3 -from typing import Optional
1 +from typing import Any, Dict, Optional
2
3 import requests
6 -from loguru import logger
7 -
4 from app.connectors.utils import get_connector_info_from_db
9 -from app.db.db_session import AsyncSessionLocal
10 -from app.db.db_session import get_db_session
5 +from app.db.db_session import AsyncSessionLocal, get_db_session
6 +from loguru import logger
7
8
13 -async def verify_wazuh_manager_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
9 +async def verify_wazuh_manager_credentials(
10 + attributes: Dict[str, Any],
11 +) -> Dict[str, Any]:
12 """
13 Verifies the connection to Wazuh manager service.
14
15 Returns:
16 dict: A dictionary containing 'connectionSuccessful' status and 'authToken' if the connection is successful.
17 """
20 - logger.info(f"Verifying the wazuh-manager connection to {attributes['connector_url']}")
18 + logger.info(
19 + f"Verifying the wazuh-manager connection to {attributes['connector_url']}",
20 + )
21
22 try:
23 wazuh_auth_token = requests.get(
@@ -31,15 +31,28 @@ async def verify_wazuh_manager_credentials(attributes: Dict[str, Any]) -> Dict[s
31
32 if wazuh_auth_token.status_code == 200:
33 logger.debug("Wazuh Authentication Token successful")
34 - return {"connectionSuccessful": True, "message": "Wazuh Manager authentication successful"}
34 + return {
35 + "connectionSuccessful": True,
36 + "message": "Wazuh Manager authentication successful",
37 + }
38 else:
36 - logger.error(f"Connection to {attributes['connector_url']} failed with error: {wazuh_auth_token.text}")
39 + logger.error(
40 + f"Connection to {attributes['connector_url']} failed with error: {wazuh_auth_token.text}",
41 + )
42
38 - return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed"}
43 + return {
44 + "connectionSuccessful": False,
45 + "message": f"Connection to {attributes['connector_url']} failed",
46 + }
47 except Exception as e:
40 - logger.error(f"Connection to {attributes['connector_url']} failed with error: {e}")
48 + logger.error(
49 + f"Connection to {attributes['connector_url']} failed with error: {e}",
50 + )
51
42 - return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error."}
52 + return {
53 + "connectionSuccessful": False,
54 + "message": f"Connection to {attributes['connector_url']} failed with error.",
55 + }
56
57
58 async def verify_wazuh_manager_connection(connector_name: str) -> str:
@@ -72,7 +85,9 @@ async def create_wazuh_manager_client(connector_name: str) -> str:
85 if attributes is None:
86 logger.error("No Wazuh Manager connector found in the database")
87 return None
75 - logger.info(f"Verifying the wazuh-manager connection to {attributes['connector_url']}")
88 + logger.info(
89 + f"Verifying the wazuh-manager connection to {attributes['connector_url']}",
90 + )
91 try:
92 wazuh_auth_token = requests.get(
93 f"{attributes['connector_url']}/security/user/authenticate",
@@ -90,16 +105,24 @@ async def create_wazuh_manager_client(connector_name: str) -> str:
105
106 return {"Authorization": f"Bearer {wazuh_auth_token}"}
107 else:
93 - logger.error(f"Connection to {attributes['connector_url']} failed with error: {wazuh_auth_token.text}")
108 + logger.error(
109 + f"Connection to {attributes['connector_url']} failed with error: {wazuh_auth_token.text}",
110 + )
111
112 return None
113 except Exception as e:
97 - logger.error(f"Connection to {attributes['connector_url']} failed with error: {e}")
114 + logger.error(
115 + f"Connection to {attributes['connector_url']} failed with error: {e}",
116 + )
117
118 return None
119
120
102 -async def send_get_request(endpoint: str, params: Optional[Dict[str, Any]] = None, connector_name: str = "Wazuh-Manager") -> Dict[str, Any]:
121 +async def send_get_request(
122 + endpoint: str,
123 + params: Optional[Dict[str, Any]] = None,
124 + connector_name: str = "Wazuh-Manager",
125 +) -> Dict[str, Any]:
126 """
127 Sends a GET request to the Wazuh Manager service.
128
@@ -130,7 +153,11 @@ async def send_get_request(endpoint: str, params: Optional[Dict[str, Any]] = Non
153 verify=False,
154 )
155 response.raise_for_status()
133 - return {"data": response.text, "success": True, "message": "Successfully retrieved data"}
156 + return {
157 + "data": response.text,
158 + "success": True,
159 + "message": "Successfully retrieved data",
160 + }
161 response = requests.get(
162 f"{attributes['connector_url']}/{endpoint}",
163 headers=wazuh_manager_client,
@@ -138,13 +165,24 @@ async def send_get_request(endpoint: str, params: Optional[Dict[str, Any]] = Non
165 verify=False,
166 )
167 response.raise_for_status()
141 - return {"data": response.json(), "success": True, "message": "Successfully retrieved data"}
168 + return {
169 + "data": response.json(),
170 + "success": True,
171 + "message": "Successfully retrieved data",
172 + }
173 except Exception as e:
174 logger.error(f"Failed to send GET request to {endpoint} with error: {e}")
144 - return {"success": False, "message": f"Failed to send GET request to {endpoint} with error: {e}"}
175 + return {
176 + "success": False,
177 + "message": f"Failed to send GET request to {endpoint} with error: {e}",
178 + }
179
180
147 -async def send_post_request(endpoint: str, data: Dict[str, Any], connector_name: str = "Wazuh-Manager") -> Dict[str, Any]:
181 +async def send_post_request(
182 + endpoint: str,
183 + data: Dict[str, Any],
184 + connector_name: str = "Wazuh-Manager",
185 +) -> Dict[str, Any]:
186 """
187 Sends a POST request to the Wazuh Manager service.
188
@@ -171,10 +209,17 @@ async def send_post_request(endpoint: str, data: Dict[str, Any], connector_name:
209 verify=False,
210 )
211 response.raise_for_status()
174 - return {"data": response.json(), "success": True, "message": "Successfully retrieved data"}
212 + return {
213 + "data": response.json(),
214 + "success": True,
215 + "message": "Successfully retrieved data",
216 + }
217 except Exception as e:
218 logger.error(f"Failed to send POST request to {endpoint} with error: {e}")
177 - return {"success": False, "message": f"Failed to send POST request to {endpoint} with error: {e}"}
219 + return {
220 + "success": False,
221 + "message": f"Failed to send POST request to {endpoint} with error: {e}",
222 + }
223
224
225 async def send_put_request(
@@ -220,10 +265,17 @@ async def send_put_request(
265 verify=False,
266 )
267 response.raise_for_status()
223 - return {"data": response.json(), "success": True, "message": "Successfully retrieved data"}
268 + return {
269 + "data": response.json(),
270 + "success": True,
271 + "message": "Successfully retrieved data",
272 + }
273 except Exception as e:
274 logger.error(f"Failed to send PUT request to {endpoint} with error: {e}")
226 - return {"success": False, "message": f"Failed to send PUT request to {endpoint} with error: {e}"}
275 + return {
276 + "success": False,
277 + "message": f"Failed to send PUT request to {endpoint} with error: {e}",
278 + }
279
280
281 async def send_delete_request(
@@ -257,10 +309,17 @@ async def send_delete_request(
309 verify=False,
310 )
311 response.raise_for_status()
260 - return {"data": response.json(), "success": True, "message": "Successfully deleted data"}
312 + return {
313 + "data": response.json(),
314 + "success": True,
315 + "message": "Successfully deleted data",
316 + }
317 except Exception as e:
318 logger.error(f"Failed to send DELETE request to {endpoint} with error: {e}")
263 - return {"success": False, "message": f"Failed to send DELETE request to {endpoint} with error: {e}"}
319 + return {
320 + "success": False,
321 + "message": f"Failed to send DELETE request to {endpoint} with error: {e}",
322 + }
323
324
325 async def restart_service(connector_name: str = "Wazuh-Manager") -> Dict[str, Any]:
@@ -284,7 +343,14 @@ async def restart_service(connector_name: str = "Wazuh-Manager") -> Dict[str, An
343 verify=False,
344 )
345 response.raise_for_status()
287 - return {"data": response.json(), "success": True, "message": "Successfully restarted service"}
346 + return {
347 + "data": response.json(),
348 + "success": True,
349 + "message": "Successfully restarted service",
350 + }
351 except Exception as e:
352 logger.error(f"Failed to restart Wazuh Manager service with error: {e}")
290 - return {"success": False, "message": f"Failed to restart Wazuh Manager service with error: {e}"}
353 + return {
354 + "success": False,
355 + "message": f"Failed to restart Wazuh Manager service with error: {e}",
356 + }
backend/app/customer_provisioning/routes/decommission.py
+15 -11
@@ -1,16 +1,12 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from fastapi import HTTPException
4 -from fastapi import Security
5 -from loguru import logger
6 -from sqlalchemy.ext.asyncio import AsyncSession
7 -from sqlalchemy.future import select
8 -
1 from app.auth.utils import AuthHandler
2 from app.customer_provisioning.schema.decommission import DecommissionCustomerResponse
3 from app.customer_provisioning.services.decommission import decomission_wazuh_customer
4 from app.db.db_session import get_db
5 from app.db.universal_models import CustomersMeta
6 +from fastapi import APIRouter, Depends, HTTPException, Security
7 +from loguru import logger
8 +from sqlalchemy.ext.asyncio import AsyncSession
9 +from sqlalchemy.future import select
10
11 # App specific imports
12
@@ -18,7 +14,10 @@ from app.db.universal_models import CustomersMeta
14 customer_decommissioning_router = APIRouter()
15
16
21 -async def check_customermeta_exists(customer_code: str, session: AsyncSession = Depends(get_db)) -> CustomersMeta:
17 +async def check_customermeta_exists(
18 + customer_code: str,
19 + session: AsyncSession = Depends(get_db),
20 +) -> CustomersMeta:
21 """
22 Check if a customer exists in the database.
23
@@ -33,11 +32,16 @@ async def check_customermeta_exists(customer_code: str, session: AsyncSession =
32 HTTPException: If the customer is not found.
33 """
34 logger.info(f"Checking if customer {customer_code} exists")
36 - result = await session.execute(select(CustomersMeta).filter(CustomersMeta.customer_code == customer_code))
35 + result = await session.execute(
36 + select(CustomersMeta).filter(CustomersMeta.customer_code == customer_code),
37 + )
38 customer_meta = result.scalars().first()
39
40 if not customer_meta:
40 - raise HTTPException(status_code=404, detail=f"Customer: {customer_code} not found. Please create the customer first.")
41 + raise HTTPException(
42 + status_code=404,
43 + detail=f"Customer: {customer_code} not found. Please create the customer first.",
44 + )
45
46 return customer_meta
47
backend/app/customer_provisioning/routes/provision.py
+55 -29
@@ -1,25 +1,20 @@
1 -from fastapi import APIRouter
2 -from fastapi import Body
3 -from fastapi import Depends
4 -from fastapi import HTTPException
5 -from fastapi import Security
6 -from loguru import logger
7 -from sqlalchemy.ext.asyncio import AsyncSession
8 -from sqlalchemy.future import select
9 -
1 from app.auth.utils import AuthHandler
11 -from app.connectors.grafana.schema.dashboards import Office365Dashboard
12 -from app.connectors.grafana.schema.dashboards import WazuhDashboard
13 -from app.customer_provisioning.schema.provision import CustomerProvisionResponse
14 -from app.customer_provisioning.schema.provision import CustomersMetaResponse
15 -from app.customer_provisioning.schema.provision import CustomerSubsctipion
16 -from app.customer_provisioning.schema.provision import GetDashboardsResponse
17 -from app.customer_provisioning.schema.provision import GetSubscriptionsResponse
18 -from app.customer_provisioning.schema.provision import ProvisionNewCustomer
2 +from app.connectors.grafana.schema.dashboards import Office365Dashboard, WazuhDashboard
3 +from app.customer_provisioning.schema.provision import (
4 + CustomerProvisionResponse,
5 + CustomersMetaResponse,
6 + CustomerSubsctipion,
7 + GetDashboardsResponse,
8 + GetSubscriptionsResponse,
9 + ProvisionNewCustomer,
10 +)
11 from app.customer_provisioning.services.provision import provision_wazuh_customer
12 from app.db.db_session import get_db
21 -from app.db.universal_models import Customers
22 -from app.db.universal_models import CustomersMeta
13 +from app.db.universal_models import Customers, CustomersMeta
14 +from fastapi import APIRouter, Body, Depends, HTTPException, Security
15 +from loguru import logger
16 +from sqlalchemy.ext.asyncio import AsyncSession
17 +from sqlalchemy.future import select
18
19 customer_provisioning_router = APIRouter()
20
@@ -39,7 +34,10 @@ def get_available_dashboards():
34 office365_dashboards = [dashboard.name for dashboard in Office365Dashboard]
35 return wazuh_dashboards + office365_dashboards
36 except Exception as e:
42 - raise HTTPException(status_code=500, detail=f"Error getting available dashboards: {e}")
37 + raise HTTPException(
38 + status_code=500,
39 + detail=f"Error getting available dashboards: {e}",
40 + )
41
42
43 def get_available_subscriptions():
@@ -55,10 +53,16 @@ def get_available_subscriptions():
53 try:
54 return [subscription.value for subscription in CustomerSubsctipion]
55 except Exception as e:
58 - raise HTTPException(status_code=500, detail=f"Error getting available subscriptions: {e}")
56 + raise HTTPException(
57 + status_code=500,
58 + detail=f"Error getting available subscriptions: {e}",
59 + )
60
61
61 -async def check_customer_exists(customer_code: str, session: AsyncSession = Depends(get_db)) -> Customers:
62 +async def check_customer_exists(
63 + customer_code: str,
64 + session: AsyncSession = Depends(get_db),
65 +) -> Customers:
66 """
67 Check if a customer exists in the database.
68
@@ -73,11 +77,16 @@ async def check_customer_exists(customer_code: str, session: AsyncSession = Depe
77 HTTPException: If the customer is not found in the database.
78 """
79 logger.info(f"Checking if customer {customer_code} exists")
76 - result = await session.execute(select(Customers).filter(Customers.customer_code == customer_code))
80 + result = await session.execute(
81 + select(Customers).filter(Customers.customer_code == customer_code),
82 + )
83 customer = result.scalars().first()
84
85 if not customer:
80 - raise HTTPException(status_code=404, detail=f"Customer: {customer_code} not found. Please create the customer first.")
86 + raise HTTPException(
87 + status_code=404,
88 + detail=f"Customer: {customer_code} not found. Please create the customer first.",
89 + )
90
91 return customer
92
@@ -93,7 +102,11 @@ async def check_unique_ports(request: ProvisionNewCustomer, session: AsyncSessio
102 Raises:
103 HTTPException: If the ports are not unique.
104 """
96 - ports = {"registration": request.wazuh_registration_port, "logs": request.wazuh_logs_port, "api": request.wazuh_api_port}
105 + ports = {
106 + "registration": request.wazuh_registration_port,
107 + "logs": request.wazuh_logs_port,
108 + "api": request.wazuh_api_port,
109 + }
110
111 for port_type, port_value in ports.items():
112 customer_meta = await get_customer_meta_by_port(port_value, session)
@@ -168,7 +181,11 @@ async def get_dashboards_route():
181 """
182 logger.info("Getting list of dashboards")
183 available_dashboards = get_available_dashboards()
171 - return GetDashboardsResponse(available_dashboards=available_dashboards, success=True, message="Dashboards retrieved successfully")
184 + return GetDashboardsResponse(
185 + available_dashboards=available_dashboards,
186 + success=True,
187 + message="Dashboards retrieved successfully",
188 + )
189
190
191 @customer_provisioning_router.get(
@@ -200,7 +217,10 @@ async def get_subscriptions_route():
217 description="Get Customer Meta",
218 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
219 )
203 -async def get_customer_meta(customer_code: str, session: AsyncSession = Depends(get_db)):
220 +async def get_customer_meta(
221 + customer_code: str,
222 + session: AsyncSession = Depends(get_db),
223 +):
224 """
225 Retrieve customer meta data for a given customer code.
226
@@ -215,7 +235,9 @@ async def get_customer_meta(customer_code: str, session: AsyncSession = Depends(
235 CustomersMetaResponse: The response containing the customer meta data.
236 """
237 logger.info(f"Getting customer meta for customer {customer_code}")
218 - result = await session.execute(select(CustomersMeta).filter(CustomersMeta.customer_code == customer_code))
238 + result = await session.execute(
239 + select(CustomersMeta).filter(CustomersMeta.customer_code == customer_code),
240 + )
241 customer_meta = result.scalars().first()
242
243 if not customer_meta:
@@ -224,4 +246,8 @@ async def get_customer_meta(customer_code: str, session: AsyncSession = Depends(
246 detail=f"Customer meta not found for customer: {customer_code}. Please provision the customer first.",
247 )
248
227 - return CustomersMetaResponse(message="Customer meta retrieved successfully", success=True, customer_meta=customer_meta)
249 + return CustomersMetaResponse(
250 + message="Customer meta retrieved successfully",
251 + success=True,
252 + customer_meta=customer_meta,
253 + )
backend/app/customer_provisioning/schema/decommission.py
+20 -6
@@ -1,12 +1,19 @@
1 from typing import List
2
3 -from pydantic import BaseModel
4 -from pydantic import Field
3 +from pydantic import BaseModel, Field
4
5
6 class DecommissionedData(BaseModel):
8 - agents_deleted: List[str] = Field(..., example=["agent1", "agent2"], description="List of agents deleted")
9 - groups_deleted: List[str] = Field(..., example=["group1", "group2"], description="List of groups deleted")
7 + agents_deleted: List[str] = Field(
8 + ...,
9 + example=["agent1", "agent2"],
10 + description="List of agents deleted",
11 + )
12 + groups_deleted: List[str] = Field(
13 + ...,
14 + example=["group1", "group2"],
15 + description="List of groups deleted",
16 + )
17 stream_deleted: str = Field(..., example="stream1", description="Stream deleted")
18 index_deleted: str = Field(..., example="index1", description="Index deleted")
19
@@ -17,5 +24,12 @@ class DecommissionCustomerResponse(BaseModel):
24 example="Customer decommissioned successfully",
25 description="Message indicating the customer was decommissioned successfully",
26 )
20 - success: bool = Field(..., example=True, description="Whether the customer was decommissioned successfully or not")
21 - decomissioned_data: DecommissionedData = Field(..., description="Data from the decomissioning process")
27 + success: bool = Field(
28 + ...,
29 + example=True,
30 + description="Whether the customer was decommissioned successfully or not",
31 + )
32 + decomissioned_data: DecommissionedData = Field(
33 + ...,
34 + description="Data from the decomissioning process",
35 + )
backend/app/customer_provisioning/schema/grafana.py
+9 -4
@@ -1,13 +1,15 @@
1 from datetime import datetime
2 from typing import Dict
3
4 -from pydantic import BaseModel
5 -from pydantic import Field
4 +from pydantic import BaseModel, Field
5
6
7 # ! Organization ! #
8 class GrafanaOrganizationCreation(BaseModel):
10 - message: str = Field(..., description="Message detailing the outcome of the request")
9 + message: str = Field(
10 + ...,
11 + description="Message detailing the outcome of the request",
12 + )
13 orgId: int = Field(..., description="ID of the created organization")
14
15
@@ -76,7 +78,10 @@ class DataSourceCreationDatasource(BaseModel):
78 withCredentials: bool = Field(..., alias="withCredentials")
79 isDefault: bool = Field(..., alias="isDefault")
80 jsonData: DataSourceCreationJsonData
79 - secureJsonFields: DataSourceCreationSecureJsonFields = Field(..., alias="secureJsonFields")
81 + secureJsonFields: DataSourceCreationSecureJsonFields = Field(
82 + ...,
83 + alias="secureJsonFields",
84 + )
85 version: int
86 readOnly: bool = Field(..., alias="readOnly")
87
backend/app/customer_provisioning/schema/graylog.py
+58 -16
@@ -1,8 +1,6 @@
1 -from typing import List
2 -from typing import Optional
1 +from typing import List, Optional
2
4 -from pydantic import BaseModel
5 -from pydantic import Field
3 +from pydantic import BaseModel, Field
4
5
6 # ! INDEX SETS ! #
@@ -87,7 +85,10 @@ class GraylogIndexSetData(BaseModel):
85 retention_strategy: RetentionStrategyConfig
86 creation_date: str = Field(..., alias="creation_date")
87 index_analyzer: str = Field(..., alias="index_analyzer")
90 - index_optimization_max_num_segments: int = Field(..., alias="index_optimization_max_num_segments")
88 + index_optimization_max_num_segments: int = Field(
89 + ...,
90 + alias="index_optimization_max_num_segments",
91 + )
92 index_optimization_disabled: bool = Field(..., alias="index_optimization_disabled")
93 field_type_refresh_interval: int = Field(..., alias="field_type_refresh_interval")
94 index_template_type: Optional[str] = Field(None, alias="index_template_type")
@@ -115,8 +116,14 @@ class WazuhEventStream(BaseModel):
116 index_set_id: str = Field(..., description="ID of the associated index set")
117 rules: List[StreamRule] = Field(..., description="List of rules for the stream")
118 matching_type: str = Field(..., description="Matching type for the rules")
118 - remove_matches_from_default_stream: bool = Field(..., description="Whether to remove matches from the default stream")
119 - content_pack: Optional[str] = Field(None, description="Associated content pack, if any")
119 + remove_matches_from_default_stream: bool = Field(
120 + ...,
121 + description="Whether to remove matches from the default stream",
122 + )
123 + content_pack: Optional[str] = Field(
124 + None,
125 + description="Associated content pack, if any",
126 + )
127
128 class Config:
129 schema_extra = {
@@ -124,7 +131,14 @@ class WazuhEventStream(BaseModel):
131 "title": "WAZUH EVENTS CUSTOMERS - Example Company",
132 "description": "WAZUH EVENTS CUSTOMERS - Example Company",
133 "index_set_id": "12345",
127 - "rules": [{"field": "agent_labels_customer", "type": 1, "inverted": False, "value": "ExampleCode"}],
134 + "rules": [
135 + {
136 + "field": "agent_labels_customer",
137 + "type": 1,
138 + "inverted": False,
139 + "value": "ExampleCode",
140 + },
141 + ],
142 "matching_type": "AND",
143 "remove_matches_from_default_stream": True,
144 "content_pack": None,
@@ -138,8 +152,14 @@ class Office365EventStream(BaseModel):
152 index_set_id: str = Field(..., description="ID of the associated index set")
153 rules: List[StreamRule] = Field(..., description="List of rules for the stream")
154 matching_type: str = Field(..., description="Matching type for the rules")
141 - remove_matches_from_default_stream: bool = Field(..., description="Whether to remove matches from the default stream")
142 - content_pack: Optional[str] = Field(None, description="Associated content pack, if any")
155 + remove_matches_from_default_stream: bool = Field(
156 + ...,
157 + description="Whether to remove matches from the default stream",
158 + )
159 + content_pack: Optional[str] = Field(
160 + None,
161 + description="Associated content pack, if any",
162 + )
163
164 class Config:
165 schema_extra = {
@@ -148,8 +168,18 @@ class Office365EventStream(BaseModel):
168 "description": "Office365 EVENTS - Example Company",
169 "index_set_id": "12345",
170 "rules": [
151 - {"field": "agent_labels_customer", "type": 1, "inverted": False, "value": "ExampleCode"},
152 - {"field": "agent_labels_integration", "type": 1, "inverted": False, "value": "Office365"},
171 + {
172 + "field": "agent_labels_customer",
173 + "type": 1,
174 + "inverted": False,
175 + "value": "ExampleCode",
176 + },
177 + {
178 + "field": "agent_labels_integration",
179 + "type": 1,
180 + "inverted": False,
181 + "value": "Office365",
182 + },
183 ],
184 "matching_type": "AND",
185 "remove_matches_from_default_stream": True,
@@ -165,20 +195,32 @@ class StreamData(BaseModel):
195 class StreamCreationResponse(BaseModel):
196 data: StreamData
197 success: bool = Field(..., description="Indicates if the request was successful")
168 - message: str = Field(..., description="A message detailing the outcome of the request")
198 + message: str = Field(
199 + ...,
200 + description="A message detailing the outcome of the request",
201 + )
202
203
204 class StreamAndPipelineData(BaseModel):
205 stream_id: str = Field(..., description="ID of the stream")
173 - pipeline_ids: List[str] = Field(..., description="List of pipeline IDs connected to the stream")
206 + pipeline_ids: List[str] = Field(
207 + ...,
208 + description="List of pipeline IDs connected to the stream",
209 + )
210
211
212 class StreamConnectionToPipelineRequest(BaseModel):
213 stream_id: str = Field(..., description="ID of the stream to connect")
178 - pipeline_ids: List[str] = Field(..., description="List of pipeline IDs to connect to the stream")
214 + pipeline_ids: List[str] = Field(
215 + ...,
216 + description="List of pipeline IDs to connect to the stream",
217 + )
218
219
220 class StreamConnectionToPipelineResponse(BaseModel):
221 data: StreamAndPipelineData
222 success: bool = Field(..., description="Indicates if the request was successful")
184 - message: str = Field(..., description="A message detailing the outcome of the request")
223 + message: str = Field(
224 + ...,
225 + description="A message detailing the outcome of the request",
226 + )
backend/app/customer_provisioning/schema/provision.py
+76 -23
@@ -1,14 +1,10 @@
1 import re
2 from enum import Enum
3 -from typing import List
4 -from typing import Optional
5 -
6 -from pydantic import BaseModel
7 -from pydantic import Field
8 -from pydantic import validator
3 +from typing import List, Optional
4
5 from app.connectors.grafana.schema.dashboards import DashboardProvisionRequest
6 from app.db.universal_models import CustomersMeta
7 +from pydantic import BaseModel, Field, validator
8
9
10 class CustomerSubsctipion(Enum):
@@ -17,17 +13,41 @@ class CustomerSubsctipion(Enum):
13
14
15 class ProvisionNewCustomer(BaseModel):
20 - customer_name: str = Field(..., example="SOC Fortress", description="Name of the customer")
16 + customer_name: str = Field(
17 + ...,
18 + example="SOC Fortress",
19 + description="Name of the customer",
20 + )
21 customer_code: str = Field(
22 ...,
23 example="SOCF",
24 description="Code of the customer. Referenced in Wazuh Agent Label, Graylog Stream, etc.",
25 )
26 - customer_index_name: str = Field(..., example="socf", description="Index prefix for the customer's Graylog instance")
27 - customer_grafana_org_name: str = Field(..., example="SOCFortress", description="Name of the customer's Grafana organization")
28 - hot_data_retention: int = Field(..., example=30, description="Number of days to retain hot data")
29 - index_replicas: int = Field(..., example=1, description="Number of replicas for the customer's Graylog instance")
30 - index_shards: int = Field(..., example=1, description="Number of shards for the customer's Graylog instance")
26 + customer_index_name: str = Field(
27 + ...,
28 + example="socf",
29 + description="Index prefix for the customer's Graylog instance",
30 + )
31 + customer_grafana_org_name: str = Field(
32 + ...,
33 + example="SOCFortress",
34 + description="Name of the customer's Grafana organization",
35 + )
36 + hot_data_retention: int = Field(
37 + ...,
38 + example=30,
39 + description="Number of days to retain hot data",
40 + )
41 + index_replicas: int = Field(
42 + ...,
43 + example=1,
44 + description="Number of replicas for the customer's Graylog instance",
45 + )
46 + index_shards: int = Field(
47 + ...,
48 + example=1,
49 + description="Number of shards for the customer's Graylog instance",
50 + )
51 customer_subscription: List[CustomerSubsctipion] = Field(
52 ...,
53 example=["Wazuh", "Office365"],
@@ -38,7 +58,10 @@ class ProvisionNewCustomer(BaseModel):
58 description="Dashboards to include in the customer's Grafana instance",
59 )
60 wazuh_auth_password: str = Field(..., description="Password for the Wazuh API user")
41 - wazuh_registration_port: str = Field(..., description="Port for the Wazuh registration service")
61 + wazuh_registration_port: str = Field(
62 + ...,
63 + description="Port for the Wazuh registration service",
64 + )
65 wazuh_logs_port: str = Field(..., description="Port for the Wazuh logs service")
66 wazuh_api_port: str = Field(..., description="Port for the Wazuh API service")
67 wazuh_cluster_name: str = Field(..., description="Name of the Wazuh cluster")
@@ -67,25 +90,55 @@ class CustomerProvisionMeta(BaseModel):
90
91
92 class CustomerProvisionResponse(BaseModel):
70 - message: str = Field(..., description="Message indicating the status of the customer provisioning process")
71 - success: bool = Field(..., description="Whether the customer provisioning process was successful or not")
72 - customer_meta: CustomersMeta = Field(..., description="Customer meta data for the newly provisioned customer")
73 - wazuh_worker_provisioned: Optional[bool] = Field(None, description="Whether the Wazuh worker was provisioned successfully")
93 + message: str = Field(
94 + ...,
95 + description="Message indicating the status of the customer provisioning process",
96 + )
97 + success: bool = Field(
98 + ...,
99 + description="Whether the customer provisioning process was successful or not",
100 + )
101 + customer_meta: CustomersMeta = Field(
102 + ...,
103 + description="Customer meta data for the newly provisioned customer",
104 + )
105 + wazuh_worker_provisioned: Optional[bool] = Field(
106 + None,
107 + description="Whether the Wazuh worker was provisioned successfully",
108 + )
109
110
111 class GetDashboardsResponse(BaseModel):
77 - available_dashboards: List[str] = Field(..., description="List of dashboards available for provisioning")
78 - message: str = Field(..., description="Message indicating the status of the request")
112 + available_dashboards: List[str] = Field(
113 + ...,
114 + description="List of dashboards available for provisioning",
115 + )
116 + message: str = Field(
117 + ...,
118 + description="Message indicating the status of the request",
119 + )
120 success: bool = Field(..., description="Whether the request was successful or not")
121
122
123 class GetSubscriptionsResponse(BaseModel):
83 - available_subscriptions: List[str] = Field(..., description="List of subscriptions available for provisioning")
84 - message: str = Field(..., description="Message indicating the status of the request")
124 + available_subscriptions: List[str] = Field(
125 + ...,
126 + description="List of subscriptions available for provisioning",
127 + )
128 + message: str = Field(
129 + ...,
130 + description="Message indicating the status of the request",
131 + )
132 success: bool = Field(..., description="Whether the request was successful or not")
133
134
135 class CustomersMetaResponse(BaseModel):
89 - message: str = Field(..., description="Message indicating the status of the request")
136 + message: str = Field(
137 + ...,
138 + description="Message indicating the status of the request",
139 + )
140 success: bool = Field(..., description="Whether the request was successful or not")
91 - customer_meta: CustomersMeta = Field(..., description="Customer meta data for the newly provisioned customer")
141 + customer_meta: CustomersMeta = Field(
142 + ...,
143 + description="Customer meta data for the newly provisioned customer",
144 + )
backend/app/customer_provisioning/schema/wazuh_worker.py
+1 -2
@@ -1,5 +1,4 @@
1 -from pydantic import BaseModel
2 -from pydantic import Field
1 +from pydantic import BaseModel, Field
2
3
4 class ProvisionWorkerRequest(BaseModel):
backend/app/customer_provisioning/services/decommission.py
+43 -18
@@ -1,22 +1,27 @@
1 import requests
2 -from loguru import logger
3 -from sqlalchemy.ext.asyncio import AsyncSession
4 -
2 from app.customer_provisioning.schema.decommission import DecommissionCustomerResponse
6 -from app.customer_provisioning.schema.wazuh_worker import DecommissionWorkerRequest
7 -from app.customer_provisioning.schema.wazuh_worker import DecommissionWorkerResponse
3 +from app.customer_provisioning.schema.wazuh_worker import (
4 + DecommissionWorkerRequest,
5 + DecommissionWorkerResponse,
6 +)
7 from app.customer_provisioning.services.dfir_iris import delete_customer
8 from app.customer_provisioning.services.grafana import delete_grafana_organization
10 -from app.customer_provisioning.services.graylog import delete_index_set
11 -from app.customer_provisioning.services.graylog import delete_stream
12 -from app.customer_provisioning.services.wazuh_manager import delete_wazuh_agents
13 -from app.customer_provisioning.services.wazuh_manager import delete_wazuh_groups
14 -from app.customer_provisioning.services.wazuh_manager import gather_wazuh_agents
9 +from app.customer_provisioning.services.graylog import delete_index_set, delete_stream
10 +from app.customer_provisioning.services.wazuh_manager import (
11 + delete_wazuh_agents,
12 + delete_wazuh_groups,
13 + gather_wazuh_agents,
14 +)
15 from app.db.universal_models import CustomersMeta
16 from app.utils import get_connector_attribute
17 +from loguru import logger
18 +from sqlalchemy.ext.asyncio import AsyncSession
19
20
19 -async def decomission_wazuh_customer(customer_meta: CustomersMeta, session: AsyncSession) -> DecommissionCustomerResponse:
21 +async def decomission_wazuh_customer(
22 + customer_meta: CustomersMeta,
23 + session: AsyncSession,
24 +) -> DecommissionCustomerResponse:
25 """
26 Decommissions a Wazuh customer by performing the following steps:
27 1. Deletes the Wazuh Agents associated with the customer.
@@ -40,7 +45,9 @@ async def decomission_wazuh_customer(customer_meta: CustomersMeta, session: Asyn
45 # Delete the Wazuh Agents
46 agents = await gather_wazuh_agents(customer_meta.customer_code)
47 agents_deleted = await delete_wazuh_agents(agents)
43 - logger.info(f"Deleted {agents_deleted} agents for customer {customer_meta.customer_name}")
48 + logger.info(
49 + f"Deleted {agents_deleted} agents for customer {customer_meta.customer_name}",
50 + )
51
52 # Delete Wazuh Group
53 groups_deleted = await delete_wazuh_groups(customer_meta.customer_code)
@@ -52,13 +59,18 @@ async def decomission_wazuh_customer(customer_meta: CustomersMeta, session: Asyn
59 await delete_index_set(customer_meta.customer_meta_graylog_index)
60
61 # Delete Grafana Organization
55 - await delete_grafana_organization(organization_id=int(customer_meta.customer_meta_grafana_org_id))
62 + await delete_grafana_organization(
63 + organization_id=int(customer_meta.customer_meta_grafana_org_id),
64 + )
65
66 # Delete DFIR-IRIS Customer
67 await delete_customer(customer_id=customer_meta.customer_meta_iris_customer_id)
68
69 # Decommission Wazuh Worker
61 - await decommission_wazuh_worker(request=DecommissionWorkerRequest(customer_name=customer_meta.customer_name), session=session)
70 + await decommission_wazuh_worker(
71 + request=DecommissionWorkerRequest(customer_name=customer_meta.customer_name),
72 + session=session,
73 + )
74
75 # Delete Customer Meta
76 await session.delete(customer_meta)
@@ -77,7 +89,10 @@ async def decomission_wazuh_customer(customer_meta: CustomersMeta, session: Asyn
89
90
91 ######### ! Decommission Wazuh Worker ! ############
80 -async def decommission_wazuh_worker(request: DecommissionWorkerRequest, session: AsyncSession) -> DecommissionWorkerResponse:
92 +async def decommission_wazuh_worker(
93 + request: DecommissionWorkerRequest,
94 + session: AsyncSession,
95 +) -> DecommissionWorkerResponse:
96 """
97 Decomissions a Wazuh worker. https://github.com/socfortress/Customer-Provisioning-Worker
98
@@ -89,7 +104,11 @@ async def decommission_wazuh_worker(request: DecommissionWorkerRequest, session:
104 ProvisionWorkerResponse: The response object indicating the success or failure of the provisioning operation.
105 """
106 logger.info(f"Decommissioning Wazuh worker {request}")
92 - api_endpoint = await get_connector_attribute(connector_id=13, column_name="connector_url", session=session)
107 + api_endpoint = await get_connector_attribute(
108 + connector_id=13,
109 + column_name="connector_url",
110 + session=session,
111 + )
112 # Send the POST request to the Wazuh worker
113 response = requests.post(
114 url=f"{api_endpoint}/provision_worker/decommission",
@@ -97,6 +116,12 @@ async def decommission_wazuh_worker(request: DecommissionWorkerRequest, session:
116 )
117 # Check the response status code
118 if response.status_code != 200:
100 - return DecommissionWorkerResponse(success=False, message=f"Failed to provision Wazuh worker: {response.text}")
119 + return DecommissionWorkerResponse(
120 + success=False,
121 + message=f"Failed to provision Wazuh worker: {response.text}",
122 + )
123 # Return the response
102 - return DecommissionWorkerResponse(success=True, message="Wazuh worker provisioned successfully")
124 + return DecommissionWorkerResponse(
125 + success=True,
126 + message="Wazuh worker provisioned successfully",
127 + )
backend/app/customer_provisioning/services/dfir_iris.py
+10 -7
@@ -1,12 +1,12 @@
1 +from app.connectors.dfir_iris.schema.admin import CreateCustomerResponse, ListCustomers
2 +from app.connectors.dfir_iris.utils.universal import (
3 + fetch_and_validate_data,
4 + initialize_client_and_admin,
5 + initialize_client_and_customer,
6 +)
7 from fastapi import HTTPException
8 from loguru import logger
9
4 -from app.connectors.dfir_iris.schema.admin import CreateCustomerResponse
5 -from app.connectors.dfir_iris.schema.admin import ListCustomers
6 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
7 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_admin
8 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_customer
9 -
10
11 async def check_customer_exists(customer_name: str) -> bool:
12 """
@@ -39,7 +39,10 @@ async def create_customer(customer_name: str) -> CreateCustomerResponse:
39 # check if the customer exists
40 exists = await check_customer_exists(customer_name)
41 if exists:
42 - raise HTTPException(status_code=400, detail=f"Customer {customer_name} already exists")
42 + raise HTTPException(
43 + status_code=400,
44 + detail=f"Customer {customer_name} already exists",
45 + )
46 client, admin = await initialize_client_and_admin("DFIR-IRIS")
47 result = await fetch_and_validate_data(client, admin.add_customer, customer_name)
48 return CreateCustomerResponse(success=result["success"], data=result["data"])
backend/app/customer_provisioning/services/grafana.py
+49 -19
@@ -1,20 +1,23 @@
1 -from fastapi import HTTPException
2 -from loguru import logger
3 -from sqlalchemy.ext.asyncio import AsyncSession
4 -
1 from app.connectors.grafana.utils.universal import create_grafana_client
2 from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
7 -from app.customer_provisioning.schema.grafana import GrafanaDatasource
8 -from app.customer_provisioning.schema.grafana import GrafanaDataSourceCreationResponse
9 -from app.customer_provisioning.schema.grafana import GrafanaFolderCreationResponse
10 -from app.customer_provisioning.schema.grafana import GrafanaOrganizationCreation
11 -from app.customer_provisioning.schema.grafana import NodesVersionResponse
3 +from app.customer_provisioning.schema.grafana import (
4 + GrafanaDatasource,
5 + GrafanaDataSourceCreationResponse,
6 + GrafanaFolderCreationResponse,
7 + GrafanaOrganizationCreation,
8 + NodesVersionResponse,
9 +)
10 from app.customer_provisioning.schema.provision import ProvisionNewCustomer
11 from app.utils import get_connector_attribute
12 +from fastapi import HTTPException
13 +from loguru import logger
14 +from sqlalchemy.ext.asyncio import AsyncSession
15
16
17 ################# ! GRAFANA PROVISIONING ! #################
17 -async def create_grafana_organization(request: ProvisionNewCustomer) -> GrafanaOrganizationCreation:
18 +async def create_grafana_organization(
19 + request: ProvisionNewCustomer,
20 +) -> GrafanaOrganizationCreation:
21 """
22 Creates a Grafana organization for a customer.
23
@@ -60,12 +63,24 @@ async def create_grafana_datasource(
63 type="grafana-opensearch-datasource",
64 typeName="OpenSearch",
65 access="proxy",
63 - url=await get_connector_attribute(connector_id=1, column_name="connector_url", session=session),
66 + url=await get_connector_attribute(
67 + connector_id=1,
68 + column_name="connector_url",
69 + session=session,
70 + ),
71 database=f"{request.customer_index_name}*",
72 basicAuth=True,
66 - basicAuthUser=await get_connector_attribute(connector_id=1, column_name="connector_username", session=session),
73 + basicAuthUser=await get_connector_attribute(
74 + connector_id=1,
75 + column_name="connector_username",
76 + session=session,
77 + ),
78 secureJsonData={
68 - "basicAuthPassword": await get_connector_attribute(connector_id=1, column_name="connector_password", session=session),
79 + "basicAuthPassword": await get_connector_attribute(
80 + connector_id=1,
81 + column_name="connector_password",
82 + session=session,
83 + ),
84 },
85 isDefault=False,
86 jsonData={
@@ -88,7 +103,10 @@ async def create_grafana_datasource(
103 return GrafanaDataSourceCreationResponse(**results)
104
105
91 -async def create_grafana_folder(organization_id: int, folder_title: str) -> GrafanaFolderCreationResponse:
106 +async def create_grafana_folder(
107 + organization_id: int,
108 + folder_title: str,
109 +) -> GrafanaFolderCreationResponse:
110 """
111 Creates a Grafana folder in the specified organization.
112
@@ -124,7 +142,10 @@ async def get_opensearch_version() -> str:
142 opensearch_client = await create_wazuh_indexer_client("Wazuh-Indexer")
143
144 # Retrieve version information
127 - version_response = opensearch_client.nodes.info(node_id="_local", filter_path=["nodes.*.version"])
145 + version_response = opensearch_client.nodes.info(
146 + node_id="_local",
147 + filter_path=["nodes.*.version"],
148 + )
149
150 # Parse the response to get the first version found
151 nodes_version_response = NodesVersionResponse(**version_response)
@@ -132,7 +153,10 @@ async def get_opensearch_version() -> str:
153 return node_info.version
154
155 # If no version is found, raise an exception
135 - raise HTTPException(status_code=500, detail="Failed to retrieve OpenSearch version.")
156 + raise HTTPException(
157 + status_code=500,
158 + detail="Failed to retrieve OpenSearch version.",
159 + )
160
161
162 ################# ! GRAFANA DECOMISSIONING ! #################
@@ -146,13 +170,19 @@ async def delete_grafana_organization(organization_id: int):
170 logger.info("Deleting Grafana organization")
171 grafana_client = await create_grafana_client("Grafana")
172 try:
149 - organization_deleted = grafana_client.organizations.delete_organization(organization_id=organization_id)
173 + organization_deleted = grafana_client.organizations.delete_organization(
174 + organization_id=organization_id,
175 + )
176 logger.info(f"Organization deleted: {organization_deleted}")
177 except Exception as e:
178 # Switch the organization to the default and try again
153 - logger.info(f"Failed to delete organization: {e}. Switching to default organization and trying again.")
179 + logger.info(
180 + f"Failed to delete organization: {e}. Switching to default organization and trying again.",
181 + )
182 grafana_client.user.switch_actual_user_organisation(1)
155 - organization_deleted = grafana_client.organizations.delete_organization(organization_id=organization_id)
183 + organization_deleted = grafana_client.organizations.delete_organization(
184 + organization_id=organization_id,
185 + )
186 logger.info(f"Organization deleted: {organization_deleted}")
187 return organization_deleted
188 return organization_deleted
backend/app/customer_provisioning/services/graylog.py
+56 -23
@@ -1,19 +1,22 @@
1 import json
2 from datetime import datetime
3
4 -from fastapi import HTTPException
5 -from loguru import logger
6 -
4 from app.connectors.graylog.services.pipelines import get_pipelines
8 -from app.connectors.graylog.utils.universal import send_delete_request
9 -from app.connectors.graylog.utils.universal import send_post_request
10 -from app.customer_provisioning.schema.graylog import GraylogIndexSetCreationResponse
11 -from app.customer_provisioning.schema.graylog import StreamConnectionToPipelineRequest
12 -from app.customer_provisioning.schema.graylog import StreamConnectionToPipelineResponse
13 -from app.customer_provisioning.schema.graylog import StreamCreationResponse
14 -from app.customer_provisioning.schema.graylog import TimeBasedIndexSet
15 -from app.customer_provisioning.schema.graylog import WazuhEventStream
5 +from app.connectors.graylog.utils.universal import (
6 + send_delete_request,
7 + send_post_request,
8 +)
9 +from app.customer_provisioning.schema.graylog import (
10 + GraylogIndexSetCreationResponse,
11 + StreamConnectionToPipelineRequest,
12 + StreamConnectionToPipelineResponse,
13 + StreamCreationResponse,
14 + TimeBasedIndexSet,
15 + WazuhEventStream,
16 +)
17 from app.customer_provisioning.schema.provision import ProvisionNewCustomer
18 +from fastapi import HTTPException
19 +from loguru import logger
20
21
22 ######### ! GRAYLOG PROVISIONING ! ############
@@ -56,7 +59,9 @@ def build_index_set_config(request: ProvisionNewCustomer) -> TimeBasedIndexSet:
59
60
61 # Function to send the POST request and handle the response
59 -async def send_index_set_creation_request(index_set: TimeBasedIndexSet) -> GraylogIndexSetCreationResponse:
62 +async def send_index_set_creation_request(
63 + index_set: TimeBasedIndexSet,
64 +) -> GraylogIndexSetCreationResponse:
65 """
66 Sends a request to create an index set in Graylog.
67
@@ -68,12 +73,17 @@ async def send_index_set_creation_request(index_set: TimeBasedIndexSet) -> Grayl
73 """
74 json_index_set = json.dumps(index_set.dict())
75 logger.info(f"json_index_set set: {json_index_set}")
71 - response_json = await send_post_request(endpoint="/api/system/indices/index_sets", data=index_set.dict())
76 + response_json = await send_post_request(
77 + endpoint="/api/system/indices/index_sets",
78 + data=index_set.dict(),
79 + )
80 return GraylogIndexSetCreationResponse(**response_json)
81
82
83 # Refactored create_index_set function
76 -async def create_index_set(request: ProvisionNewCustomer) -> GraylogIndexSetCreationResponse:
84 +async def create_index_set(
85 + request: ProvisionNewCustomer,
86 +) -> GraylogIndexSetCreationResponse:
87 """
88 Creates an index set for a new customer.
89
@@ -104,7 +114,10 @@ def extract_index_set_id(response: GraylogIndexSetCreationResponse) -> str:
114
115 # ! Event STREAMS ! #
116 # Function to create event stream configuration
107 -def build_event_stream_config(request: ProvisionNewCustomer, index_set_id: str) -> WazuhEventStream:
117 +def build_event_stream_config(
118 + request: ProvisionNewCustomer,
119 + index_set_id: str,
120 +) -> WazuhEventStream:
121 """
122 Build the configuration for a Wazuh event stream.
123
@@ -133,7 +146,9 @@ def build_event_stream_config(request: ProvisionNewCustomer, index_set_id: str)
146 )
147
148
136 -async def send_event_stream_creation_request(event_stream: WazuhEventStream) -> StreamCreationResponse:
149 +async def send_event_stream_creation_request(
150 + event_stream: WazuhEventStream,
151 +) -> StreamCreationResponse:
152 """
153 Sends a request to create an event stream.
154
@@ -145,7 +160,10 @@ async def send_event_stream_creation_request(event_stream: WazuhEventStream) ->
160 """
161 json_event_stream = json.dumps(event_stream.dict())
162 logger.info(f"json_event_stream set: {json_event_stream}")
148 - response_json = await send_post_request(endpoint="/api/streams", data=event_stream.dict())
163 + response_json = await send_post_request(
164 + endpoint="/api/streams",
165 + data=event_stream.dict(),
166 + )
167 return StreamCreationResponse(**response_json)
168
169
@@ -187,13 +205,21 @@ async def get_pipeline_id(subscription: str) -> str:
205 if subscription.lower() in pipeline.description.lower():
206 return [pipeline.id]
207 logger.error(f"Failed to get pipeline ID for subscription {subscription}")
190 - raise HTTPException(status_code=500, detail=f"Failed to get pipeline ID for subscription {subscription}")
208 + raise HTTPException(
209 + status_code=500,
210 + detail=f"Failed to get pipeline ID for subscription {subscription}",
211 + )
212 else:
213 logger.error(f"Failed to get pipelines: {pipelines_response.message}")
193 - raise HTTPException(status_code=500, detail=f"Failed to get pipelines: {pipelines_response.message}")
214 + raise HTTPException(
215 + status_code=500,
216 + detail=f"Failed to get pipelines: {pipelines_response.message}",
217 + )
218
219
196 -async def connect_stream_to_pipeline(stream_and_pipeline: StreamConnectionToPipelineRequest):
220 +async def connect_stream_to_pipeline(
221 + stream_and_pipeline: StreamConnectionToPipelineRequest,
222 +):
223 """
224 Connects a stream to a pipeline.
225
@@ -203,8 +229,13 @@ async def connect_stream_to_pipeline(stream_and_pipeline: StreamConnectionToPipe
229 Returns:
230 StreamConnectionToPipelineResponse: The response object containing the connection details.
231 """
206 - logger.info(f"Connecting stream {stream_and_pipeline.stream_id} to pipeline {stream_and_pipeline.pipeline_ids}")
207 - response_json = await send_post_request(endpoint="/api/system/pipelines/connections/to_stream", data=stream_and_pipeline.dict())
232 + logger.info(
233 + f"Connecting stream {stream_and_pipeline.stream_id} to pipeline {stream_and_pipeline.pipeline_ids}",
234 + )
235 + response_json = await send_post_request(
236 + endpoint="/api/system/pipelines/connections/to_stream",
237 + data=stream_and_pipeline.dict(),
238 + )
239 logger.info(f"Response: {response_json}")
240 return StreamConnectionToPipelineResponse(**response_json)
241
@@ -236,5 +267,7 @@ async def delete_index_set(index_set_id: str):
267 The result of the index set deletion request.
268 """
269 logger.info(f"Deleting index set {index_set_id}")
239 - response = await send_delete_request(endpoint=f"/api/system/indices/index_sets/{index_set_id}")
270 + response = await send_delete_request(
271 + endpoint=f"/api/system/indices/index_sets/{index_set_id}",
272 + )
273 return response
backend/app/customer_provisioning/services/provision.py
+93 -34
@@ -1,36 +1,48 @@
1 import requests
2 -from fastapi import HTTPException
3 -from loguru import logger
4 -from sqlalchemy.ext.asyncio import AsyncSession
5 -
2 from app.connectors.grafana.schema.dashboards import DashboardProvisionRequest
3 from app.connectors.grafana.services.dashboards import provision_dashboards
4 from app.connectors.graylog.services.management import start_stream
5 from app.customer_provisioning.schema.graylog import StreamConnectionToPipelineRequest
10 -from app.customer_provisioning.schema.provision import CustomerProvisionMeta
11 -from app.customer_provisioning.schema.provision import CustomerProvisionResponse
12 -from app.customer_provisioning.schema.provision import ProvisionNewCustomer
13 -from app.customer_provisioning.schema.wazuh_worker import ProvisionWorkerRequest
14 -from app.customer_provisioning.schema.wazuh_worker import ProvisionWorkerResponse
6 +from app.customer_provisioning.schema.provision import (
7 + CustomerProvisionMeta,
8 + CustomerProvisionResponse,
9 + ProvisionNewCustomer,
10 +)
11 +from app.customer_provisioning.schema.wazuh_worker import (
12 + ProvisionWorkerRequest,
13 + ProvisionWorkerResponse,
14 +)
15 from app.customer_provisioning.services.dfir_iris import create_customer
16 -from app.customer_provisioning.services.grafana import create_grafana_datasource
17 -from app.customer_provisioning.services.grafana import create_grafana_folder
18 -from app.customer_provisioning.services.grafana import create_grafana_organization
19 -from app.customer_provisioning.services.graylog import connect_stream_to_pipeline
20 -from app.customer_provisioning.services.graylog import create_event_stream
21 -from app.customer_provisioning.services.graylog import create_index_set
22 -from app.customer_provisioning.services.graylog import get_pipeline_id
23 -from app.customer_provisioning.services.wazuh_manager import apply_group_configurations
24 -from app.customer_provisioning.services.wazuh_manager import create_wazuh_groups
16 +from app.customer_provisioning.services.grafana import (
17 + create_grafana_datasource,
18 + create_grafana_folder,
19 + create_grafana_organization,
20 +)
21 +from app.customer_provisioning.services.graylog import (
22 + connect_stream_to_pipeline,
23 + create_event_stream,
24 + create_index_set,
25 + get_pipeline_id,
26 +)
27 +from app.customer_provisioning.services.wazuh_manager import (
28 + apply_group_configurations,
29 + create_wazuh_groups,
30 +)
31 from app.db.universal_models import CustomersMeta
32 from app.integrations.alert_creation_settings.models.alert_creation_settings import (
33 AlertCreationSettings,
34 )
35 from app.utils import get_connector_attribute
36 +from fastapi import HTTPException
37 +from loguru import logger
38 +from sqlalchemy.ext.asyncio import AsyncSession
39
40
41 # ! MAIN FUNCTION ! #
33 -async def provision_wazuh_customer(request: ProvisionNewCustomer, session: AsyncSession) -> CustomerProvisionResponse:
42 +async def provision_wazuh_customer(
43 + request: ProvisionNewCustomer,
44 + session: AsyncSession,
45 +) -> CustomerProvisionResponse:
46 """
47 This function is the main function for provisioning a new customer for their Wazuh instance.
48 It will call all the other functions to provision the customer.
@@ -49,7 +61,9 @@ async def provision_wazuh_customer(request: ProvisionNewCustomer, session: Async
61 # Initialize an empty dictionary to store the meta data
62 provision_meta_data = {}
63 provision_meta_data["index_set_id"] = (await create_index_set(request)).data.id
52 - provision_meta_data["stream_id"] = (await create_event_stream(request, provision_meta_data["index_set_id"])).data.stream_id
64 + provision_meta_data["stream_id"] = (
65 + await create_event_stream(request, provision_meta_data["index_set_id"])
66 + ).data.stream_id
67 provision_meta_data["pipeline_ids"] = await get_pipeline_id(subscription="Wazuh")
68 stream_and_pipeline = StreamConnectionToPipelineRequest(
69 stream_id=provision_meta_data["stream_id"],
@@ -57,15 +71,27 @@ async def provision_wazuh_customer(request: ProvisionNewCustomer, session: Async
71 )
72 await connect_stream_to_pipeline(stream_and_pipeline)
73 if await start_stream(stream_id=provision_meta_data["stream_id"]) is False:
60 - raise HTTPException(status_code=500, detail=f"Failed to start stream {provision_meta_data['stream_id']}")
74 + raise HTTPException(
75 + status_code=500,
76 + detail=f"Failed to start stream {provision_meta_data['stream_id']}",
77 + )
78 await create_wazuh_groups(request)
79 await apply_group_configurations(request)
63 - provision_meta_data["grafana_organization_id"] = (await create_grafana_organization(request)).orgId
80 + provision_meta_data["grafana_organization_id"] = (
81 + await create_grafana_organization(request)
82 + ).orgId
83 provision_meta_data["wazuh_datasource_uid"] = (
65 - await create_grafana_datasource(request=request, organization_id=provision_meta_data["grafana_organization_id"], session=session)
84 + await create_grafana_datasource(
85 + request=request,
86 + organization_id=provision_meta_data["grafana_organization_id"],
87 + session=session,
88 + )
89 ).datasource.uid
90 provision_meta_data["grafana_edr_folder_id"] = (
68 - await create_grafana_folder(organization_id=provision_meta_data["grafana_organization_id"], folder_title="EDR")
91 + await create_grafana_folder(
92 + organization_id=provision_meta_data["grafana_organization_id"],
93 + folder_title="EDR",
94 + )
95 ).id
96 await provision_dashboards(
97 DashboardProvisionRequest(
@@ -76,11 +102,21 @@ async def provision_wazuh_customer(request: ProvisionNewCustomer, session: Async
102 ),
103 )
104
79 - provision_meta_data["iris_customer_id"] = (await create_customer(request.customer_name)).data.customer_id
105 + provision_meta_data["iris_customer_id"] = (
106 + await create_customer(request.customer_name)
107 + ).data.customer_id
108
109 customer_provision_meta = CustomerProvisionMeta(**provision_meta_data)
82 - customer_meta = await update_customer_meta_table(request, customer_provision_meta, session)
83 - await update_customer_alert_settings_table(request, customer_provision_meta, session)
110 + customer_meta = await update_customer_meta_table(
111 + request,
112 + customer_provision_meta,
113 + session,
114 + )
115 + await update_customer_alert_settings_table(
116 + request,
117 + customer_provision_meta,
118 + session,
119 + )
120
121 provision_worker = await provision_wazuh_worker(
122 ProvisionWorkerRequest(
@@ -113,7 +149,11 @@ async def provision_wazuh_customer(request: ProvisionNewCustomer, session: Async
149
150
151 ######### ! Update CustomerMeta Table ! ############
116 -async def update_customer_meta_table(request: ProvisionNewCustomer, customer_meta: CustomerProvisionMeta, session: AsyncSession):
152 +async def update_customer_meta_table(
153 + request: ProvisionNewCustomer,
154 + customer_meta: CustomerProvisionMeta,
155 + session: AsyncSession,
156 +):
157 """
158 Update the customer meta table with the provided information.
159
@@ -146,7 +186,11 @@ async def update_customer_meta_table(request: ProvisionNewCustomer, customer_met
186
187
188 ######### ! Update Customer Alert Settings Table ! ############
149 -async def update_customer_alert_settings_table(request: ProvisionNewCustomer, customer_meta: CustomerProvisionMeta, session: AsyncSession):
189 +async def update_customer_alert_settings_table(
190 + request: ProvisionNewCustomer,
191 + customer_meta: CustomerProvisionMeta,
192 + session: AsyncSession,
193 +):
194 """
195 Update the customer alert settings table with the provided information.
196
@@ -158,7 +202,9 @@ async def update_customer_alert_settings_table(request: ProvisionNewCustomer, cu
202 Returns:
203 AlertCreationSettings: The updated customer meta object.
204 """
161 - logger.info(f"Updating customer alert settings table for customer {request.customer_name}")
205 + logger.info(
206 + f"Updating customer alert settings table for customer {request.customer_name}",
207 + )
208 customer_alert_settings = AlertCreationSettings(
209 customer_code=request.customer_code,
210 customer_name=request.customer_name,
@@ -176,7 +222,10 @@ async def update_customer_alert_settings_table(request: ProvisionNewCustomer, cu
222
223
224 ######### ! Provision Wazuh Worker ! ############
179 -async def provision_wazuh_worker(request: ProvisionWorkerRequest, session: AsyncSession) -> ProvisionWorkerResponse:
225 +async def provision_wazuh_worker(
226 + request: ProvisionWorkerRequest,
227 + session: AsyncSession,
228 +) -> ProvisionWorkerResponse:
229 """
230 Provisions a Wazuh worker. https://github.com/socfortress/Customer-Provisioning-Worker
231
@@ -188,7 +237,11 @@ async def provision_wazuh_worker(request: ProvisionWorkerRequest, session: Async
237 ProvisionWorkerResponse: The response object indicating the success or failure of the provisioning operation.
238 """
239 logger.info(f"Provisioning Wazuh worker {request}")
191 - api_endpoint = await get_connector_attribute(connector_id=13, column_name="connector_url", session=session)
240 + api_endpoint = await get_connector_attribute(
241 + connector_id=13,
242 + column_name="connector_url",
243 + session=session,
244 + )
245 # Send the POST request to the Wazuh worker
246 response = requests.post(
247 url=f"{api_endpoint}/provision_worker",
@@ -196,6 +249,12 @@ async def provision_wazuh_worker(request: ProvisionWorkerRequest, session: Async
249 )
250 # Check the response status code
251 if response.status_code != 200:
199 - return ProvisionWorkerResponse(success=False, message=f"Failed to provision Wazuh worker: {response.text}")
252 + return ProvisionWorkerResponse(
253 + success=False,
254 + message=f"Failed to provision Wazuh worker: {response.text}",
255 + )
256 # Return the response
201 - return ProvisionWorkerResponse(success=True, message="Wazuh worker provisioned successfully")
257 + return ProvisionWorkerResponse(
258 + success=True,
259 + message="Wazuh worker provisioned successfully",
260 + )
backend/app/customer_provisioning/services/wazuh_manager.py
+21 -7
@@ -1,8 +1,6 @@
1 from pathlib import Path
2 from typing import List
3
4 -from loguru import logger
5 -
4 from app.connectors.wazuh_manager.utils.universal import (
5 send_delete_request as send_wazuh_delete_request,
6 )
@@ -17,6 +15,7 @@ from app.connectors.wazuh_manager.utils.universal import (
15 )
16 from app.customer_provisioning.schema.provision import ProvisionNewCustomer
17 from app.customer_provisioning.schema.wazuh_manager import WazuhAgentsTemplatePaths
18 +from loguru import logger
19
20
21 ######### ! WAZUH MANAGER PROVISIONING ! ############
@@ -63,9 +62,15 @@ async def create_wazuh_groups(request: ProvisionNewCustomer):
62 Returns:
63 None
64 """
66 - logger.info(f"Creating Wazuh groups for customer {request.customer_name} with code {request.customer_code}")
65 + logger.info(
66 + f"Creating Wazuh groups for customer {request.customer_name} with code {request.customer_code}",
67 + )
68
68 - wazuh_groups = ["Linux", "Windows", "Mac"] # This list can be moved to a config file or a global variable
69 + wazuh_groups = [
70 + "Linux",
71 + "Windows",
72 + "Mac",
73 + ] # This list can be moved to a config file or a global variable
74
75 for group in wazuh_groups:
76 group_code = generate_group_code(group, request.customer_code)
@@ -116,7 +121,11 @@ async def configure_wazuh_group(group_code, template_path):
121 group_config = config_template.replace("REPLACE", group_code.split("_")[-1])
122
123 # Make the API request to update the group configuration
119 - return await send_wazuh_put_request(endpoint=f"groups/{group_code}/configuration", data=group_config, xml_data=True)
124 + return await send_wazuh_put_request(
125 + endpoint=f"groups/{group_code}/configuration",
126 + data=group_config,
127 + xml_data=True,
128 + )
129
130
131 # Function to apply configurations for all groups
@@ -134,7 +143,9 @@ async def apply_group_configurations(request: ProvisionNewCustomer):
143 Exception: If there is an error configuring a group.
144
145 """
137 - logger.info(f"Applying configurations for Wazuh groups for customer {request.customer_name} with code {request.customer_code}")
146 + logger.info(
147 + f"Applying configurations for Wazuh groups for customer {request.customer_name} with code {request.customer_code}",
148 + )
149
150 group_templates = {
151 "Linux": WazuhAgentsTemplatePaths.LINUX_AGENT,
@@ -166,7 +177,10 @@ async def get_agent_ids(group_code: str) -> List[str]:
177
178 """
179 try:
169 - response = await send_wazuh_get_request(endpoint="agents", params={"group": group_code})
180 + response = await send_wazuh_get_request(
181 + endpoint="agents",
182 + params={"group": group_code},
183 + )
184 logger.info(f"Response for {group_code}: {response}")
185
186 # Extracting agents from the nested response
backend/app/customers/routes/customers.py
+201 -72
@@ -1,32 +1,31 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from fastapi import HTTPException
4 -from fastapi import Query
5 -from fastapi import Security
6 -from loguru import logger
7 -from sqlalchemy.ext.asyncio import AsyncSession
8 -from sqlalchemy.future import select
9 -from starlette.status import HTTP_401_UNAUTHORIZED
10 -
1 from app.auth.utils import AuthHandler
2
3 # App specific imports
14 -from app.customers.schema.customers import AgentModel
15 -from app.customers.schema.customers import AgentsResponse
16 -from app.customers.schema.customers import CustomerFullResponse
17 -from app.customers.schema.customers import CustomerMetaRequestBody
18 -from app.customers.schema.customers import CustomerMetaResponse
19 -from app.customers.schema.customers import CustomerRequestBody
20 -from app.customers.schema.customers import CustomerResponse
21 -from app.customers.schema.customers import CustomersResponse
4 +from app.customers.schema.customers import (
5 + AgentModel,
6 + AgentsResponse,
7 + CustomerFullResponse,
8 + CustomerMetaRequestBody,
9 + CustomerMetaResponse,
10 + CustomerRequestBody,
11 + CustomerResponse,
12 + CustomersResponse,
13 +)
14 from app.db.db_session import get_db
23 -from app.db.universal_models import Agents
24 -from app.db.universal_models import Customers
25 -from app.db.universal_models import CustomersMeta
26 -from app.healthchecks.agents.schema.agents import AgentHealthCheckResponse
27 -from app.healthchecks.agents.schema.agents import TimeCriteriaModel
28 -from app.healthchecks.agents.services.agents import velociraptor_agents_healthcheck
29 -from app.healthchecks.agents.services.agents import wazuh_agents_healthcheck
15 +from app.db.universal_models import Agents, Customers, CustomersMeta
16 +from app.healthchecks.agents.schema.agents import (
17 + AgentHealthCheckResponse,
18 + TimeCriteriaModel,
19 +)
20 +from app.healthchecks.agents.services.agents import (
21 + velociraptor_agents_healthcheck,
22 + wazuh_agents_healthcheck,
23 +)
24 +from fastapi import APIRouter, Depends, HTTPException, Query, Security
25 +from loguru import logger
26 +from sqlalchemy.ext.asyncio import AsyncSession
27 +from sqlalchemy.future import select
28 +from starlette.status import HTTP_401_UNAUTHORIZED
29
30 customers_router = APIRouter()
31
@@ -48,7 +47,10 @@ def verify_admin(user):
47 raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Unauthorized")
48
49
51 -async def verify_unique_customer_code(session: AsyncSession, customer: CustomerRequestBody):
50 +async def verify_unique_customer_code(
51 + session: AsyncSession,
52 + customer: CustomerRequestBody,
53 +):
54 """
55 Verifies if the given customer code is unique in the database.
56
@@ -63,7 +65,10 @@ async def verify_unique_customer_code(session: AsyncSession, customer: CustomerR
65 result = await session.execute(stmt)
66 existing_customer = result.scalars().first()
67 if existing_customer:
66 - raise HTTPException(status_code=400, detail="Customer with this customer_code already exists")
68 + raise HTTPException(
69 + status_code=400,
70 + detail="Customer with this customer_code already exists",
71 + )
72
73
74 @customers_router.post(
@@ -72,7 +77,10 @@ async def verify_unique_customer_code(session: AsyncSession, customer: CustomerR
77 description="Create a new customer",
78 dependencies=[Security(AuthHandler().require_any_scope("admin"))],
79 )
75 -async def create_customer(customer: CustomerRequestBody, session: AsyncSession = Depends(get_db)) -> CustomerResponse:
80 +async def create_customer(
81 + customer: CustomerRequestBody,
82 + session: AsyncSession = Depends(get_db),
83 +) -> CustomerResponse:
84 """
85 Create a new customer.
86
@@ -91,7 +99,11 @@ async def create_customer(customer: CustomerRequestBody, session: AsyncSession =
99 new_customer = Customers(**customer.dict())
100 session.add(new_customer)
101 await session.commit() # Use await to perform the commit operation asynchronously
94 - return CustomerResponse(customer=customer, success=True, message="Customer created successfully")
102 + return CustomerResponse(
103 + customer=customer,
104 + success=True,
105 + message="Customer created successfully",
106 + )
107
108
109 @customers_router.get(
@@ -118,7 +130,11 @@ async def get_customers(session: AsyncSession = Depends(get_db)) -> CustomersRes
130
131 # Parse the customer ORM objects into schema objects
132 customers_list = [CustomerRequestBody.from_orm(customer) for customer in customers]
121 - return CustomersResponse(customers=customers_list, success=True, message="Customers fetched successfully")
133 + return CustomersResponse(
134 + customers=customers_list,
135 + success=True,
136 + message="Customers fetched successfully",
137 + )
138
139
140 @customers_router.get(
@@ -127,7 +143,10 @@ async def get_customers(session: AsyncSession = Depends(get_db)) -> CustomersRes
143 description="Get customer by customer_code",
144 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
145 )
130 -async def get_customer(customer_code: str, session: AsyncSession = Depends(get_db)) -> CustomerResponse:
146 +async def get_customer(
147 + customer_code: str,
148 + session: AsyncSession = Depends(get_db),
149 +) -> CustomerResponse:
150 """
151 Get customer by customer_code.
152
@@ -144,15 +163,24 @@ async def get_customer(customer_code: str, session: AsyncSession = Depends(get_d
163 logger.info(f"Fetching customer with customer_code: {customer_code}")
164
165 # Asynchronous query to fetch customer
147 - result = await session.execute(select(Customers).filter(Customers.customer_code == customer_code))
166 + result = await session.execute(
167 + select(Customers).filter(Customers.customer_code == customer_code),
168 + )
169 customer = result.scalars().first()
170
171 if not customer:
151 - raise HTTPException(status_code=404, detail=f"Customer with customer_code {customer_code} not found")
172 + raise HTTPException(
173 + status_code=404,
174 + detail=f"Customer with customer_code {customer_code} not found",
175 + )
176
177 # Convert ORM object to Pydantic model
178 customer_data = CustomerRequestBody.from_orm(customer)
155 - return CustomerResponse(customer=customer_data, success=True, message="Customer fetched successfully")
179 + return CustomerResponse(
180 + customer=customer_data,
181 + success=True,
182 + message="Customer fetched successfully",
183 + )
184
185
186 @customers_router.put(
@@ -183,11 +211,16 @@ async def update_customer(
211 logger.info(f"Updating customer with customer_code: {customer_code}")
212
213 # Asynchronous query to find the existing customer
186 - result = await session.execute(select(Customers).filter(Customers.customer_code == customer_code))
214 + result = await session.execute(
215 + select(Customers).filter(Customers.customer_code == customer_code),
216 + )
217 existing_customer = result.scalars().first()
218
219 if not existing_customer:
190 - raise HTTPException(status_code=404, detail=f"Customer with customer_code {customer_code} not found")
220 + raise HTTPException(
221 + status_code=404,
222 + detail=f"Customer with customer_code {customer_code} not found",
223 + )
224
225 # Update model instance with input data
226 for key, value in customer.dict().items():
@@ -209,7 +242,10 @@ async def update_customer(
242 description="Delete customer by customer_code",
243 dependencies=[Security(AuthHandler().require_any_scope("admin"))],
244 )
212 -async def delete_customer(customer_code: str, session: AsyncSession = Depends(get_db)) -> CustomerResponse:
245 +async def delete_customer(
246 + customer_code: str,
247 + session: AsyncSession = Depends(get_db),
248 +) -> CustomerResponse:
249 """
250 Delete a customer by customer_code.
251
@@ -225,11 +261,16 @@ async def delete_customer(customer_code: str, session: AsyncSession = Depends(ge
261 """
262 logger.info(f"Deleting customer with customer_code: {customer_code}")
263
228 - result = await session.execute(select(Customers).filter(Customers.customer_code == customer_code))
264 + result = await session.execute(
265 + select(Customers).filter(Customers.customer_code == customer_code),
266 + )
267 existing_customer = result.scalars().first()
268
269 if not existing_customer:
232 - raise HTTPException(status_code=404, detail=f"Customer with customer_code {customer_code} not found")
270 + raise HTTPException(
271 + status_code=404,
272 + detail=f"Customer with customer_code {customer_code} not found",
273 + )
274
275 # Capture the customer data before deleting
276 customer_data = CustomerRequestBody.from_orm(existing_customer)
@@ -276,11 +317,16 @@ async def add_customer_meta(
317 """
318 logger.info(f"Adding new customer meta: {customer_meta}")
319
279 - result = await session.execute(select(Customers).filter(Customers.customer_code == customer_code))
320 + result = await session.execute(
321 + select(Customers).filter(Customers.customer_code == customer_code),
322 + )
323 existing_customer = result.scalars().first()
324
325 if not existing_customer:
283 - raise HTTPException(status_code=404, detail=f"Customer with customer_code {customer_code} not found")
326 + raise HTTPException(
327 + status_code=404,
328 + detail=f"Customer with customer_code {customer_code} not found",
329 + )
330
331 logger.info(f"Got existing customer: {existing_customer}")
332 new_customer_meta = CustomersMeta(**customer_meta.dict())
@@ -290,7 +336,11 @@ async def add_customer_meta(
336 session.add(new_customer_meta)
337 await session.commit() # Use await to perform the commit operation asynchronously
338
293 - return CustomerMetaResponse(customer_meta=customer_meta, success=True, message="Customer meta added successfully")
339 + return CustomerMetaResponse(
340 + customer_meta=customer_meta,
341 + success=True,
342 + message="Customer meta added successfully",
343 + )
344
345
346 @customers_router.get(
@@ -300,7 +350,10 @@ async def add_customer_meta(
350 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
351 deprecated=True,
352 )
303 -async def get_customer_meta(customer_code: str, session: AsyncSession = Depends(get_db)) -> CustomerMetaResponse:
353 +async def get_customer_meta(
354 + customer_code: str,
355 + session: AsyncSession = Depends(get_db),
356 +) -> CustomerMetaResponse:
357 """
358 Retrieve customer meta data by customer_code.
359
@@ -316,11 +369,16 @@ async def get_customer_meta(customer_code: str, session: AsyncSession = Depends(
369 """
370 logger.info(f"Fetching customer meta with customer_code: {customer_code}")
371
319 - result = await session.execute(select(CustomersMeta).filter(CustomersMeta.customer_code == customer_code))
372 + result = await session.execute(
373 + select(CustomersMeta).filter(CustomersMeta.customer_code == customer_code),
374 + )
375 customer_meta = result.scalars().first()
376
377 if not customer_meta:
323 - raise HTTPException(status_code=404, detail=f"Customer meta with customer_code {customer_code} not found")
378 + raise HTTPException(
379 + status_code=404,
380 + detail=f"Customer meta with customer_code {customer_code} not found",
381 + )
382
383 # Assuming CustomerMetaRequestBody can be created from the ORM model directly
384 customer_meta_data = CustomerMetaRequestBody.from_orm(customer_meta)
@@ -359,11 +417,16 @@ async def update_customer_meta(
417 """
418 logger.info(f"Updating customer meta with customer_code: {customer_code}")
419
362 - result = await session.execute(select(CustomersMeta).filter(CustomersMeta.customer_code == customer_code))
420 + result = await session.execute(
421 + select(CustomersMeta).filter(CustomersMeta.customer_code == customer_code),
422 + )
423 existing_customer_meta = result.scalars().first()
424
425 if not existing_customer_meta:
366 - raise HTTPException(status_code=404, detail=f"Customer meta with customer_code {customer_code} not found")
426 + raise HTTPException(
427 + status_code=404,
428 + detail=f"Customer meta with customer_code {customer_code} not found",
429 + )
430
431 # Update the existing record with new values
432 for key, value in customer_meta.dict(exclude_unset=True).items():
@@ -386,7 +449,10 @@ async def update_customer_meta(
449 dependencies=[Security(AuthHandler().require_any_scope("admin"))],
450 deprecated=True,
451 )
389 -async def delete_customer_meta(customer_code: str, session: AsyncSession = Depends(get_db)) -> CustomerMetaResponse:
452 +async def delete_customer_meta(
453 + customer_code: str,
454 + session: AsyncSession = Depends(get_db),
455 +) -> CustomerMetaResponse:
456 """
457 Delete customer meta by customer_code.
458
@@ -402,11 +468,16 @@ async def delete_customer_meta(customer_code: str, session: AsyncSession = Depen
468 """
469 logger.info(f"Deleting customer meta with customer_code: {customer_code}")
470
405 - result = await session.execute(select(CustomersMeta).filter(CustomersMeta.customer_code == customer_code))
471 + result = await session.execute(
472 + select(CustomersMeta).filter(CustomersMeta.customer_code == customer_code),
473 + )
474 existing_customer_meta = result.scalars().first()
475
476 if not existing_customer_meta:
409 - raise HTTPException(status_code=404, detail=f"Customer meta with customer_code {customer_code} not found")
477 + raise HTTPException(
478 + status_code=404,
479 + detail=f"Customer meta with customer_code {customer_code} not found",
480 + )
481
482 # Store customer meta data for response before deleting
483 customer_meta_data = CustomerMetaRequestBody.from_orm(existing_customer_meta)
@@ -430,7 +501,10 @@ async def delete_customer_meta(customer_code: str, session: AsyncSession = Depen
501 description="Get customer and customer meta by customer_code",
502 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
503 )
433 -async def get_customer_full(customer_code: str, session: AsyncSession = Depends(get_db)) -> CustomerFullResponse:
504 +async def get_customer_full(
505 + customer_code: str,
506 + session: AsyncSession = Depends(get_db),
507 +) -> CustomerFullResponse:
508 """
509 Retrieve the customer and customer meta information based on the customer code.
510
@@ -445,14 +519,23 @@ async def get_customer_full(customer_code: str, session: AsyncSession = Depends(
519 HTTPException: If the customer with the specified code is not found.
520
521 """
448 - logger.info(f"Fetching customer and customer meta with customer_code: {customer_code}")
522 + logger.info(
523 + f"Fetching customer and customer meta with customer_code: {customer_code}",
524 + )
525
450 - customer_result = await session.execute(select(Customers).filter(Customers.customer_code == customer_code))
526 + customer_result = await session.execute(
527 + select(Customers).filter(Customers.customer_code == customer_code),
528 + )
529 customer = customer_result.scalars().first()
530 if not customer:
453 - raise HTTPException(status_code=404, detail=f"Customer with customer_code {customer_code} not found")
531 + raise HTTPException(
532 + status_code=404,
533 + detail=f"Customer with customer_code {customer_code} not found",
534 + )
535
455 - customer_meta_result = await session.execute(select(CustomersMeta).filter(CustomersMeta.customer_code == customer_code))
536 + customer_meta_result = await session.execute(
537 + select(CustomersMeta).filter(CustomersMeta.customer_code == customer_code),
538 + )
539 customer_meta = customer_meta_result.scalars().first()
540 if not customer_meta:
541 return CustomerFullResponse(
@@ -475,7 +558,10 @@ async def get_customer_full(customer_code: str, session: AsyncSession = Depends(
558 description="Get agents for the given customer_code",
559 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
560 )
478 -async def get_agents(customer_code: str, session: AsyncSession = Depends(get_db)) -> AgentsResponse:
561 +async def get_agents(
562 + customer_code: str,
563 + session: AsyncSession = Depends(get_db),
564 +) -> AgentsResponse:
565 """
566 Fetches agents for the given customer_code.
567
@@ -489,18 +575,29 @@ async def get_agents(customer_code: str, session: AsyncSession = Depends(get_db)
575 logger.info(f"Fetching agents for customer_code: {customer_code}")
576
577 # Check if the customer exists
492 - customer_result = await session.execute(select(Customers).filter(Customers.customer_code == customer_code))
578 + customer_result = await session.execute(
579 + select(Customers).filter(Customers.customer_code == customer_code),
580 + )
581 customer = customer_result.scalars().first()
582 if not customer:
495 - raise HTTPException(status_code=404, detail=f"Customer with customer_code {customer_code} not found")
583 + raise HTTPException(
584 + status_code=404,
585 + detail=f"Customer with customer_code {customer_code} not found",
586 + )
587
588 # Asynchronously fetch all agents for the customer
498 - agents_result = await session.execute(select(Agents).filter(Agents.customer_code == customer_code))
589 + agents_result = await session.execute(
590 + select(Agents).filter(Agents.customer_code == customer_code),
591 + )
592 agents = agents_result.scalars().all()
593
594 # Convert ORM objects to Pydantic models
595 agents_list = [AgentModel.from_orm(agent) for agent in agents]
503 - return AgentsResponse(agents=agents_list, success=True, message="Agents fetched successfully")
596 + return AgentsResponse(
597 + agents=agents_list,
598 + success=True,
599 + message="Agents fetched successfully",
600 + )
601
602
603 @customers_router.get(
@@ -512,9 +609,18 @@ async def get_agents(customer_code: str, session: AsyncSession = Depends(get_db)
609 async def get_wazuh_agents_healthcheck(
610 customer_code: str,
611 session: AsyncSession = Depends(get_db),
515 - minutes: int = Query(60, description="Number of minutes within which the agent should have been last seen to be considered healthy."),
516 - hours: int = Query(0, description="Number of hours within which the agent should have been last seen to be considered healthy."),
517 - days: int = Query(0, description="Number of days within which the agent should have been last seen to be considered healthy."),
612 + minutes: int = Query(
613 + 60,
614 + description="Number of minutes within which the agent should have been last seen to be considered healthy.",
615 + ),
616 + hours: int = Query(
617 + 0,
618 + description="Number of hours within which the agent should have been last seen to be considered healthy.",
619 + ),
620 + days: int = Query(
621 + 0,
622 + description="Number of days within which the agent should have been last seen to be considered healthy.",
623 + ),
624 ) -> AgentHealthCheckResponse:
625 """
626 Get agents healthcheck for the given customer_code.
@@ -528,12 +634,19 @@ async def get_wazuh_agents_healthcheck(
634 logger.info(f"Fetching agents for customer_code: {customer_code}")
635
636 # Asynchronously fetch customer and agents
531 - customer_result = await session.execute(select(Customers).filter(Customers.customer_code == customer_code))
637 + customer_result = await session.execute(
638 + select(Customers).filter(Customers.customer_code == customer_code),
639 + )
640 customer = customer_result.scalars().first()
641 if not customer:
534 - raise HTTPException(status_code=404, detail=f"Customer with customer_code {customer_code} not found")
642 + raise HTTPException(
643 + status_code=404,
644 + detail=f"Customer with customer_code {customer_code} not found",
645 + )
646
536 - agents_result = await session.execute(select(Agents).filter(Agents.customer_code == customer_code))
647 + agents_result = await session.execute(
648 + select(Agents).filter(Agents.customer_code == customer_code),
649 + )
650 agents = agents_result.scalars().all()
651
652 # Convert ORM objects to Pydantic models
@@ -553,9 +666,18 @@ async def get_wazuh_agents_healthcheck(
666 async def get_velociraptor_agents_healthcheck(
667 customer_code: str,
668 session: AsyncSession = Depends(get_db),
556 - minutes: int = Query(60, description="Number of minutes within which the agent should have been last seen to be considered healthy."),
557 - hours: int = Query(0, description="Number of hours within which the agent should have been last seen to be considered healthy."),
558 - days: int = Query(0, description="Number of days within which the agent should have been last seen to be considered healthy."),
669 + minutes: int = Query(
670 + 60,
671 + description="Number of minutes within which the agent should have been last seen to be considered healthy.",
672 + ),
673 + hours: int = Query(
674 + 0,
675 + description="Number of hours within which the agent should have been last seen to be considered healthy.",
676 + ),
677 + days: int = Query(
678 + 0,
679 + description="Number of days within which the agent should have been last seen to be considered healthy.",
680 + ),
681 ) -> AgentHealthCheckResponse:
682 """
683 Fetches the healthcheck of agents for the given customer_code.
@@ -573,13 +695,20 @@ async def get_velociraptor_agents_healthcheck(
695 logger.info(f"Fetching agents for customer_code: {customer_code}")
696
697 # Asynchronously fetch customer
576 - customer_result = await session.execute(select(Customers).filter(Customers.customer_code == customer_code))
698 + customer_result = await session.execute(
699 + select(Customers).filter(Customers.customer_code == customer_code),
700 + )
701 customer = customer_result.scalars().first()
702 if not customer:
579 - raise HTTPException(status_code=404, detail=f"Customer with customer_code {customer_code} not found")
703 + raise HTTPException(
704 + status_code=404,
705 + detail=f"Customer with customer_code {customer_code} not found",
706 + )
707
708 # Asynchronously fetch all agents for the customer
582 - agents_result = await session.execute(select(Agents).filter(Agents.customer_code == customer_code))
709 + agents_result = await session.execute(
710 + select(Agents).filter(Agents.customer_code == customer_code),
711 + )
712 agents = agents_result.scalars().all()
713 agents = [AgentModel.parse_obj(agent.__dict__) for agent in agents]
714 time_criteria = TimeCriteriaModel(minutes=minutes, hours=hours, days=days)
backend/app/customers/schema/customers.py
+38 -13
@@ -1,9 +1,7 @@
1 from datetime import datetime
2 -from typing import List
3 -from typing import Optional
2 +from typing import List, Optional
3
5 -from pydantic import BaseModel
6 -from pydantic import Field
4 +from pydantic import BaseModel, Field
5
6
7 class CustomerRequestBody(BaseModel):
@@ -12,7 +10,10 @@ class CustomerRequestBody(BaseModel):
10 contact_last_name: str = Field(..., description="Last name of the contact person")
11 contact_first_name: str = Field(..., description="First name of the contact person")
12
15 - parent_customer_code: Optional[str] = Field(None, description="Code for the parent customer")
13 + parent_customer_code: Optional[str] = Field(
14 + None,
15 + description="Code for the parent customer",
16 + )
17 phone: Optional[str] = Field(None, description="Phone number")
18 address_line1: Optional[str] = Field(None, description="First line of the address")
19 address_line2: Optional[str] = Field(None, description="Second line of the address")
@@ -58,14 +59,38 @@ class CustomersResponse(BaseModel):
59
60 ############# Customer Meta
61 class CustomerMetaRequestBody(BaseModel):
61 - customer_meta_graylog_index: str = Field(..., description="Graylog index for the customer")
62 - customer_meta_graylog_stream: str = Field(..., description="Graylog stream for the customer")
63 - customer_meta_grafana_org_id: str = Field(..., description="Grafana organization for the customer")
64 - customer_meta_wazuh_group: str = Field(..., description="Wazuh group for the customer")
65 - customer_meta_index_retention: str = Field(..., description="Index retention for the customer")
66 - customer_meta_wazuh_registration_port: str = Field(..., description="Wazuh registration port for the customer")
67 - customer_meta_wazuh_log_ingestion_port: str = Field(..., description="Wazuh log ingestion port for the customer")
68 - customer_meta_wazuh_auth_password: str = Field(..., description="Wazuh auth password for the customer")
62 + customer_meta_graylog_index: str = Field(
63 + ...,
64 + description="Graylog index for the customer",
65 + )
66 + customer_meta_graylog_stream: str = Field(
67 + ...,
68 + description="Graylog stream for the customer",
69 + )
70 + customer_meta_grafana_org_id: str = Field(
71 + ...,
72 + description="Grafana organization for the customer",
73 + )
74 + customer_meta_wazuh_group: str = Field(
75 + ...,
76 + description="Wazuh group for the customer",
77 + )
78 + customer_meta_index_retention: str = Field(
79 + ...,
80 + description="Index retention for the customer",
81 + )
82 + customer_meta_wazuh_registration_port: str = Field(
83 + ...,
84 + description="Wazuh registration port for the customer",
85 + )
86 + customer_meta_wazuh_log_ingestion_port: str = Field(
87 + ...,
88 + description="Wazuh log ingestion port for the customer",
89 + )
90 + customer_meta_wazuh_auth_password: str = Field(
91 + ...,
92 + description="Wazuh auth password for the customer",
93 + )
94
95 class Config:
96 orm_mode = True
backend/app/db/db_populate.py
+113 -32
@@ -1,22 +1,27 @@
1 import os
2
3 -from dotenv import load_dotenv
4 -from loguru import logger
5 -from sqlalchemy import and_
6 -from sqlalchemy.ext.asyncio import AsyncSession
7 -from sqlalchemy.future import select
8 -
3 from app.auth.models.users import Role
4 from app.connectors.models import Connectors
11 -from app.integrations.models.customer_integration_settings import AvailableIntegrations
5 from app.integrations.models.customer_integration_settings import (
6 + AvailableIntegrations,
7 AvailableIntegrationsAuthKeys,
8 )
9 +from dotenv import load_dotenv
10 +from loguru import logger
11 +from sqlalchemy import and_
12 +from sqlalchemy.ext.asyncio import AsyncSession
13 +from sqlalchemy.future import select
14
15 load_dotenv()
16
17
19 -def load_connector_data(connector_name, connector_type, accepts_key, description, extra_data_key=None):
18 +def load_connector_data(
19 + connector_name,
20 + connector_type,
21 + accepts_key,
22 + description,
23 + extra_data_key=None,
24 +):
25 """
26 Load connector data from environment variables.
27
@@ -32,7 +37,9 @@ def load_connector_data(connector_name, connector_type, accepts_key, description
37 """
38 env_prefix = connector_name.upper().replace("-", "_").replace(" ", "_")
39 url = os.getenv(f"{env_prefix}_URL")
35 - logger.info(f"Loading connector data for {connector_name} from environment variables with URL: {url}")
40 + logger.info(
41 + f"Loading connector data for {connector_name} from environment variables with URL: {url}",
42 + )
43 return {
44 "connector_name": connector_name,
45 "connector_type": connector_type,
@@ -62,16 +69,47 @@ def get_connectors_list():
69 """
70 connectors = [
71 ("Wazuh-Indexer", "4.4.1", "username_password", "Connection to Wazuh-Indexer."),
65 - ("Wazuh-Manager", "4.4.1", "username_password", "Connection to Wazuh-Manager. Default is wazuh-wui:wazuh-wui"),
72 + (
73 + "Wazuh-Manager",
74 + "4.4.1",
75 + "username_password",
76 + "Connection to Wazuh-Manager. Default is wazuh-wui:wazuh-wui",
77 + ),
78 ("Graylog", "5.0.7", "username_password", "Connection to Graylog."),
79 ("Shuffle", "1.1.0", "api_key", "Connection to Shuffle."),
80 ("DFIR-IRIS", "2.0", "api_key", "Connection to DFIR-IRIS."),
69 - ("Velociraptor", "0.6.8", "file", "Connection to Velociraptor. Make sure you have generated the api file first."),
81 + (
82 + "Velociraptor",
83 + "0.6.8",
84 + "file",
85 + "Connection to Velociraptor. Make sure you have generated the api file first.",
86 + ),
87 ("Sublime", "3", "api_key", "Connection to Sublime."),
71 - ("InfluxDB", "3", "api_key", "Connection to InfluxDB.", "INFLUXDB_ORG_AND_BUCKET"),
72 - ("AskSocfortress", "3", "api_key", "Connection to AskSocfortress. Make sure you have requested an API key."),
73 - ("SocfortressThreatIntel", "3", "api_key", "Connection to Socfortress Threat Intel. Make sure you have requested an API key."),
74 - ("Cortex", "3", "api_key", "Connection to Cortex. Make sure you have created an API key."),
88 + (
89 + "InfluxDB",
90 + "3",
91 + "api_key",
92 + "Connection to InfluxDB.",
93 + "INFLUXDB_ORG_AND_BUCKET",
94 + ),
95 + (
96 + "AskSocfortress",
97 + "3",
98 + "api_key",
99 + "Connection to AskSocfortress. Make sure you have requested an API key.",
100 + ),
101 + (
102 + "SocfortressThreatIntel",
103 + "3",
104 + "api_key",
105 + "Connection to Socfortress Threat Intel. Make sure you have requested an API key.",
106 + ),
107 + (
108 + "Cortex",
109 + "3",
110 + "api_key",
111 + "Connection to Cortex. Make sure you have created an API key.",
112 + ),
113 ("Grafana", "3", "username_password", "Connection to Grafana."),
114 (
115 "Wazuh Worker Provisioning",
@@ -119,7 +157,9 @@ async def add_connectors_if_not_exist(session: AsyncSession):
157 connector_list = get_connectors_list()
158
159 for connector_data in connector_list:
122 - query = select(Connectors).where(Connectors.connector_name == connector_data["connector_name"])
160 + query = select(Connectors).where(
161 + Connectors.connector_name == connector_data["connector_name"],
162 + )
163 result = await session.execute(query)
164 existing_connector = result.scalars().first()
165
@@ -163,7 +203,11 @@ async def add_roles_if_not_exist(session: AsyncSession) -> None:
203 logger.info("Role check and addition completed.")
204
205
166 -def load_available_integrations_data(integration_name: str, description: str, integration_details: str):
206 +def load_available_integrations_data(
207 + integration_name: str,
208 + description: str,
209 + integration_details: str,
210 +):
211 """
212 Load available integrations data from environment variables.
213
@@ -193,7 +237,12 @@ def load_markdown_for_integration(integration_name: str) -> str:
237 str: The content of the markdown file.
238 """
239 # file_path = os.path.join("integrations_markdown", f"{integration_name.lower()}.md")
196 - file_path = os.path.join("app", "integrations", "markdown", f"{integration_name.lower()}.md")
240 + file_path = os.path.join(
241 + "app",
242 + "integrations",
243 + "markdown",
244 + f"{integration_name.lower()}.md",
245 + )
246 try:
247 with open(file_path, "r") as file:
248 return file.read()
@@ -215,7 +264,11 @@ def get_available_integrations_list():
264 ]
265
266 return [
218 - load_available_integrations_data(integration_name, description, load_markdown_for_integration(integration_name))
267 + load_available_integrations_data(
268 + integration_name,
269 + description,
270 + load_markdown_for_integration(integration_name),
271 + )
272 for integration_name, description in available_integrations
273 ]
274
@@ -234,18 +287,27 @@ async def add_available_integrations_if_not_exist(session: AsyncSession):
287
288 for available_integration_data in available_integrations_list:
289 query = select(AvailableIntegrations).where(
237 - AvailableIntegrations.integration_name == available_integration_data["integration_name"],
290 + AvailableIntegrations.integration_name
291 + == available_integration_data["integration_name"],
292 )
293 result = await session.execute(query)
294 existing_available_integration = result.scalars().first()
295
296 if existing_available_integration is None:
243 - new_available_integration = AvailableIntegrations(**available_integration_data)
297 + new_available_integration = AvailableIntegrations(
298 + **available_integration_data,
299 + )
300 session.add(new_available_integration)
245 - logger.info(f"Added new available integration: {available_integration_data['integration_name']}")
301 + logger.info(
302 + f"Added new available integration: {available_integration_data['integration_name']}",
303 + )
304
305
248 -def load_available_integrations_auth_keys(integration_id: int, integration_name: str, auth_key_name: str):
306 +def load_available_integrations_auth_keys(
307 + integration_id: int,
308 + integration_name: str,
309 + auth_key_name: str,
310 +):
311 """
312 Load available integrations auth keys from environment variables.
313
@@ -257,7 +319,9 @@ def load_available_integrations_auth_keys(integration_id: int, integration_name:
319 Returns:
320 dict: A dictionary containing the auth key data.
321 """
260 - logger.info(f"Loading available integrations auth keys data for {integration_name}.")
322 + logger.info(
323 + f"Loading available integrations auth keys data for {integration_name}.",
324 + )
325 return {
326 "integration_id": integration_id,
327 "integration_name": integration_name,
@@ -290,12 +354,20 @@ async def get_available_integrations_auth_keys_list(session: AsyncSession):
354 ]
355
356 for integration_name, auth_key_name in available_integrations:
293 - query = select(AvailableIntegrations.id).where(AvailableIntegrations.integration_name == integration_name)
357 + query = select(AvailableIntegrations.id).where(
358 + AvailableIntegrations.integration_name == integration_name,
359 + )
360 result = await session.execute(query)
361 integration_id = result.scalars().first()
362
363 if integration_id:
298 - available_integrations_auth_keys.append(load_available_integrations_auth_keys(integration_id, integration_name, auth_key_name))
364 + available_integrations_auth_keys.append(
365 + load_available_integrations_auth_keys(
366 + integration_id,
367 + integration_name,
368 + auth_key_name,
369 + ),
370 + )
371
372 return available_integrations_auth_keys
373
@@ -310,28 +382,37 @@ async def add_available_integrations_auth_keys_if_not_exist(session: AsyncSessio
382 Returns:
383 None
384 """
313 - available_integrations_auth_keys_list = await get_available_integrations_auth_keys_list(session=session)
385 + available_integrations_auth_keys_list = (
386 + await get_available_integrations_auth_keys_list(session=session)
387 + )
388
389 for available_integration_auth_keys_data in available_integrations_auth_keys_list:
390 query = select(AvailableIntegrations).where(
317 - AvailableIntegrations.integration_name == available_integration_auth_keys_data["integration_name"],
391 + AvailableIntegrations.integration_name
392 + == available_integration_auth_keys_data["integration_name"],
393 )
394 result = await session.execute(query)
395 existing_integration = result.scalars().first()
396
397 if existing_integration:
323 - available_integration_auth_keys_data["integration_id"] = existing_integration.id
398 + available_integration_auth_keys_data[
399 + "integration_id"
400 + ] = existing_integration.id
401 auth_key_query = select(AvailableIntegrationsAuthKeys).where(
402 and_(
326 - AvailableIntegrationsAuthKeys.integration_id == existing_integration.id,
327 - AvailableIntegrationsAuthKeys.auth_key_name == available_integration_auth_keys_data["auth_key_name"],
403 + AvailableIntegrationsAuthKeys.integration_id
404 + == existing_integration.id,
405 + AvailableIntegrationsAuthKeys.auth_key_name
406 + == available_integration_auth_keys_data["auth_key_name"],
407 ),
408 )
409 auth_key_result = await session.execute(auth_key_query)
410 existing_auth_key = auth_key_result.scalars().first()
411
412 if existing_auth_key is None:
334 - new_auth_key = AvailableIntegrationsAuthKeys(**available_integration_auth_keys_data)
413 + new_auth_key = AvailableIntegrationsAuthKeys(
414 + **available_integration_auth_keys_data,
415 + )
416 session.add(new_auth_key)
417 logger.info(
418 f"Added new available integration auth keys: "
backend/app/db/db_session.py
+22 -13
@@ -1,30 +1,39 @@
1 # ! Old Testing without Async
2 -from sqlmodel import Session
3 -from sqlmodel import create_engine
4 -
2 from settings import SQLALCHEMY_DATABASE_URI
3 +from sqlmodel import Session, create_engine
4
7 -engine = create_engine(SQLALCHEMY_DATABASE_URI, connect_args={"check_same_thread": False})
5 +engine = create_engine(
6 + SQLALCHEMY_DATABASE_URI,
7 + connect_args={"check_same_thread": False},
8 +)
9 session = "placeholder"
10
10 -from contextlib import asynccontextmanager
11 -from contextlib import contextmanager
11 +from contextlib import asynccontextmanager, contextmanager
12
13 from loguru import logger
14 +from settings import SQLALCHEMY_DATABASE_URI
15 from sqlalchemy import create_engine
15 -from sqlalchemy.ext.asyncio import AsyncSession
16 -from sqlalchemy.ext.asyncio import create_async_engine
16 +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
17 from sqlalchemy.orm import sessionmaker
18
19 -from settings import SQLALCHEMY_DATABASE_URI
20 -
19 # create async engine for SQLite using aiosqlite
20 async_engine = create_async_engine(SQLALCHEMY_DATABASE_URI, echo=False)
23 -sync_engine = create_engine(SQLALCHEMY_DATABASE_URI.replace("+aiosqlite", ""), echo=False)
21 +sync_engine = create_engine(
22 + SQLALCHEMY_DATABASE_URI.replace("+aiosqlite", ""),
23 + echo=False,
24 +)
25
26 # create a configured "AsyncSession" class
26 -AsyncSessionLocal = sessionmaker(bind=async_engine, class_=AsyncSession, expire_on_commit=False)
27 -SyncSessionLocal = sessionmaker(bind=sync_engine, class_=Session, expire_on_commit=False)
27 +AsyncSessionLocal = sessionmaker(
28 + bind=async_engine,
29 + class_=AsyncSession,
30 + expire_on_commit=False,
31 +)
32 +SyncSessionLocal = sessionmaker(
33 + bind=sync_engine,
34 + class_=Session,
35 + expire_on_commit=False,
36 +)
37
38
39 @asynccontextmanager
backend/app/db/db_setup.py
+17 -10
@@ -1,17 +1,20 @@
1 +from app.auth.services.universal import (
2 + create_admin_user,
3 + create_scheduler_user,
4 + remove_scheduler_user,
5 +)
6 +from app.db.db_populate import (
7 + add_available_integrations_auth_keys_if_not_exist,
8 + add_available_integrations_if_not_exist,
9 + add_connectors_if_not_exist,
10 + add_roles_if_not_exist,
11 +)
12 from loguru import logger
13 from sqlalchemy.ext.asyncio import AsyncSession
14
15 # ! New with Async
16 from sqlmodel import SQLModel
17
7 -from app.auth.services.universal import create_admin_user
8 -from app.auth.services.universal import create_scheduler_user
9 -from app.auth.services.universal import remove_scheduler_user
10 -from app.db.db_populate import add_available_integrations_auth_keys_if_not_exist
11 -from app.db.db_populate import add_available_integrations_if_not_exist
12 -from app.db.db_populate import add_connectors_if_not_exist
13 -from app.db.db_populate import add_roles_if_not_exist
14 -
18
19 async def create_tables(async_engine):
20 """
@@ -44,7 +47,9 @@ async def create_roles(async_engine):
47 None
48 """
49 logger.info("Creating roles")
47 - async with AsyncSession(async_engine) as session: # Create an AsyncSession, not just a connection
50 + async with AsyncSession(
51 + async_engine,
52 + ) as session: # Create an AsyncSession, not just a connection
53 async with session.begin(): # Start a transaction
54 await add_roles_if_not_exist(session)
55
@@ -60,7 +65,9 @@ async def create_available_integrations(async_engine):
65 None
66 """
67 logger.info("Creating available integrations")
63 - async with AsyncSession(async_engine) as session: # Create an AsyncSession, not just a connection
68 + async with AsyncSession(
69 + async_engine,
70 + ) as session: # Create an AsyncSession, not just a connection
71 async with session.begin(): # Start a transaction
72 await add_available_integrations_if_not_exist(session)
73 await add_available_integrations_auth_keys_if_not_exist(session)
backend/app/db/universal_models.py
+33 -13
@@ -1,9 +1,7 @@
1 from datetime import datetime
2 from typing import Optional
3
4 -from sqlmodel import Field
5 -from sqlmodel import Relationship
6 -from sqlmodel import SQLModel
4 +from sqlmodel import Field, Relationship, SQLModel
5
6
7 class Customers(SQLModel, table=True):
@@ -73,12 +71,22 @@ class CustomersMeta(SQLModel, table=True):
71 self.customer_meta_grafana_org_id = customer_meta.customer_meta_grafana_org_id
72 self.customer_meta_wazuh_group = customer_meta.customer_meta_wazuh_group
73 self.customer_meta_index_retention = customer_meta.customer_meta_index_retention
76 - self.customer_meta_wazuh_registration_port = customer_meta.customer_meta_wazuh_registration_port
77 - self.customer_meta_wazuh_log_ingestion_port = customer_meta.customer_meta_wazuh_log_ingestion_port
74 + self.customer_meta_wazuh_registration_port = (
75 + customer_meta.customer_meta_wazuh_registration_port
76 + )
77 + self.customer_meta_wazuh_log_ingestion_port = (
78 + customer_meta.customer_meta_wazuh_log_ingestion_port
79 + )
80 self.customer_meta_wazuh_api_port = customer_meta.customer_meta_wazuh_api_port
79 - self.customer_meta_wazuh_auth_password = customer_meta.customer_meta_wazuh_auth_password
80 - self.customer_meta_iris_customer_id = customer_meta.customer_meta_iris_customer_id
81 - self.customer_meta_office365_organization_id = customer_meta.customer_meta_office365_organization_id
81 + self.customer_meta_wazuh_auth_password = (
82 + customer_meta.customer_meta_wazuh_auth_password
83 + )
84 + self.customer_meta_iris_customer_id = (
85 + customer_meta.customer_meta_iris_customer_id
86 + )
87 + self.customer_meta_office365_organization_id = (
88 + customer_meta.customer_meta_office365_organization_id
89 + )
90
91
92 class Agents(SQLModel, table=True):
@@ -103,7 +111,9 @@ class Agents(SQLModel, table=True):
111 def create_from_model(cls, wazuh_agent, velociraptor_agent, customer_code):
112 # Check if agent_last_seen is 'Unknown' and set wazuh_last_seen accordingly
113 if wazuh_agent.agent_last_seen == "Unknown":
106 - wazuh_last_seen_value = "1970-01-01T00:00:00+00:00" # default datetime value
114 + wazuh_last_seen_value = (
115 + "1970-01-01T00:00:00+00:00" # default datetime value
116 + )
117 else:
118 wazuh_last_seen_value = wazuh_agent.agent_last_seen_as_datetime
119
@@ -115,15 +125,23 @@ class Agents(SQLModel, table=True):
125 label=wazuh_agent.agent_label,
126 wazuh_last_seen=wazuh_last_seen_value,
127 wazuh_agent_version=wazuh_agent.wazuh_agent_version,
118 - velociraptor_id=velociraptor_agent.client_id if velociraptor_agent.client_id else "n/a",
128 + velociraptor_id=velociraptor_agent.client_id
129 + if velociraptor_agent.client_id
130 + else "n/a",
131 velociraptor_last_seen=velociraptor_agent.client_last_seen_as_datetime,
132 velociraptor_agent_version=velociraptor_agent.client_version,
133 customer_code=customer_code,
134 )
135
136 def update_from_model(self, wazuh_agent, velociraptor_agent, customer_code):
125 - if wazuh_agent.agent_last_seen == "Unknown" or wazuh_agent.agent_last_seen == "1970-01-01T00:00:00+00:00":
126 - wazuh_last_seen_value = datetime.strptime("1970-01-01T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S%z") # default datetime value
137 + if (
138 + wazuh_agent.agent_last_seen == "Unknown"
139 + or wazuh_agent.agent_last_seen == "1970-01-01T00:00:00+00:00"
140 + ):
141 + wazuh_last_seen_value = datetime.strptime(
142 + "1970-01-01T00:00:00+00:00",
143 + "%Y-%m-%dT%H:%M:%S%z",
144 + ) # default datetime value
145 else:
146 wazuh_last_seen_value = wazuh_agent.agent_last_seen_as_datetime
147
@@ -134,7 +152,9 @@ class Agents(SQLModel, table=True):
152 self.label = wazuh_agent.agent_label
153 self.wazuh_last_seen = wazuh_last_seen_value
154 self.wazuh_agent_version = wazuh_agent.wazuh_agent_version
137 - self.velociraptor_id = velociraptor_agent.client_id if velociraptor_agent.client_id else "n/a"
155 + self.velociraptor_id = (
156 + velociraptor_agent.client_id if velociraptor_agent.client_id else "n/a"
157 + )
158 self.velociraptor_last_seen = velociraptor_agent.client_last_seen_as_datetime
159 self.velociraptor_agent_version = velociraptor_agent.client_version
160 self.customer_code = customer_code
backend/app/healthchecks/agents/routes/agents.py
+87 -36
@@ -1,24 +1,23 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from fastapi import HTTPException
4 -from fastapi import Query
5 -from fastapi import Security
6 -from loguru import logger
7 -from sqlalchemy.ext.asyncio import AsyncSession
8 -from sqlalchemy.future import select
9 -
1 from app.auth.utils import AuthHandler
2 from app.db.db_session import get_db
3 from app.db.universal_models import Agents
13 -from app.healthchecks.agents.schema.agents import AgentHealthCheckResponse
14 -from app.healthchecks.agents.schema.agents import HostLogsSearchBody
15 -from app.healthchecks.agents.schema.agents import HostLogsSearchResponse
16 -from app.healthchecks.agents.schema.agents import TimeCriteriaModel
17 -from app.healthchecks.agents.services.agents import host_logs
18 -from app.healthchecks.agents.services.agents import velociraptor_agent_healthcheck
19 -from app.healthchecks.agents.services.agents import velociraptor_agents_healthcheck
20 -from app.healthchecks.agents.services.agents import wazuh_agent_healthcheck
21 -from app.healthchecks.agents.services.agents import wazuh_agents_healthcheck
4 +from app.healthchecks.agents.schema.agents import (
5 + AgentHealthCheckResponse,
6 + HostLogsSearchBody,
7 + HostLogsSearchResponse,
8 + TimeCriteriaModel,
9 +)
10 +from app.healthchecks.agents.services.agents import (
11 + host_logs,
12 + velociraptor_agent_healthcheck,
13 + velociraptor_agents_healthcheck,
14 + wazuh_agent_healthcheck,
15 + wazuh_agents_healthcheck,
16 +)
17 +from fastapi import APIRouter, Depends, HTTPException, Query, Security
18 +from loguru import logger
19 +from sqlalchemy.ext.asyncio import AsyncSession
20 +from sqlalchemy.future import select
21
22 healtcheck_agents_router = APIRouter()
23
@@ -31,9 +30,18 @@ healtcheck_agents_router = APIRouter()
30 )
31 async def get_wazuh_agent_healthcheck(
32 session: AsyncSession = Depends(get_db),
34 - minutes: int = Query(60, description="Number of minutes within which the agent should have been last seen to be considered healthy."),
35 - hours: int = Query(0, description="Number of hours within which the agent should have been last seen to be considered healthy."),
36 - days: int = Query(0, description="Number of days within which the agent should have been last seen to be considered healthy."),
33 + minutes: int = Query(
34 + 60,
35 + description="Number of minutes within which the agent should have been last seen to be considered healthy.",
36 + ),
37 + hours: int = Query(
38 + 0,
39 + description="Number of hours within which the agent should have been last seen to be considered healthy.",
40 + ),
41 + days: int = Query(
42 + 0,
43 + description="Number of days within which the agent should have been last seen to be considered healthy.",
44 + ),
45 ) -> AgentHealthCheckResponse:
46 """
47 Get the healthcheck of Wazuh agents based on the specified time criteria.
@@ -64,9 +72,18 @@ async def get_wazuh_agent_healthcheck(
72 async def get_wazuh_agent_healthcheck_by_agent_id(
73 agent_id: str,
74 session: AsyncSession = Depends(get_db),
67 - minutes: int = Query(60, description="Number of minutes within which the agent should have been last seen to be considered healthy."),
68 - hours: int = Query(0, description="Number of hours within which the agent should have been last seen to be considered healthy."),
69 - days: int = Query(0, description="Number of days within which the agent should have been last seen to be considered healthy."),
75 + minutes: int = Query(
76 + 60,
77 + description="Number of minutes within which the agent should have been last seen to be considered healthy.",
78 + ),
79 + hours: int = Query(
80 + 0,
81 + description="Number of hours within which the agent should have been last seen to be considered healthy.",
82 + ),
83 + days: int = Query(
84 + 0,
85 + description="Number of days within which the agent should have been last seen to be considered healthy.",
86 + ),
87 ) -> AgentHealthCheckResponse:
88 """
89 Get the healthcheck of a Wazuh agent by agent_id.
@@ -91,7 +108,10 @@ async def get_wazuh_agent_healthcheck_by_agent_id(
108 agent = result.scalars().first()
109
110 if not agent:
94 - raise HTTPException(status_code=404, detail=f"Agent with agent_id {agent_id} not found")
111 + raise HTTPException(
112 + status_code=404,
113 + detail=f"Agent with agent_id {agent_id} not found",
114 + )
115 return await wazuh_agent_healthcheck(agent, time_criteria)
116
117
@@ -103,9 +123,18 @@ async def get_wazuh_agent_healthcheck_by_agent_id(
123 )
124 async def get_velociraptor_agent_healthcheck(
125 session: AsyncSession = Depends(get_db),
106 - minutes: int = Query(60, description="Number of minutes within which the agent should have been last seen to be considered healthy."),
107 - hours: int = Query(0, description="Number of hours within which the agent should have been last seen to be considered healthy."),
108 - days: int = Query(0, description="Number of days within which the agent should have been last seen to be considered healthy."),
126 + minutes: int = Query(
127 + 60,
128 + description="Number of minutes within which the agent should have been last seen to be considered healthy.",
129 + ),
130 + hours: int = Query(
131 + 0,
132 + description="Number of hours within which the agent should have been last seen to be considered healthy.",
133 + ),
134 + days: int = Query(
135 + 0,
136 + description="Number of days within which the agent should have been last seen to be considered healthy.",
137 + ),
138 ) -> AgentHealthCheckResponse:
139 """
140 Get Velociraptor agents healthcheck.
@@ -136,9 +165,18 @@ async def get_velociraptor_agent_healthcheck(
165 async def get_velociraptor_agent_healthcheck_by_agent_id(
166 agent_id: str,
167 session: AsyncSession = Depends(get_db),
139 - minutes: int = Query(60, description="Number of minutes within which the agent should have been last seen to be considered healthy."),
140 - hours: int = Query(0, description="Number of hours within which the agent should have been last seen to be considered healthy."),
141 - days: int = Query(0, description="Number of days within which the agent should have been last seen to be considered healthy."),
168 + minutes: int = Query(
169 + 60,
170 + description="Number of minutes within which the agent should have been last seen to be considered healthy.",
171 + ),
172 + hours: int = Query(
173 + 0,
174 + description="Number of hours within which the agent should have been last seen to be considered healthy.",
175 + ),
176 + days: int = Query(
177 + 0,
178 + description="Number of days within which the agent should have been last seen to be considered healthy.",
179 + ),
180 ) -> AgentHealthCheckResponse:
181 """
182 Get Velociraptor agent healthcheck by agent_id.
@@ -159,7 +197,10 @@ async def get_velociraptor_agent_healthcheck_by_agent_id(
197 result = await session.execute(select(Agents).filter(Agents.agent_id == agent_id))
198 agent = result.scalars().first()
199 if not agent:
162 - raise HTTPException(status_code=404, detail=f"Agent with agent_id {agent_id} not found")
200 + raise HTTPException(
201 + status_code=404,
202 + detail=f"Agent with agent_id {agent_id} not found",
203 + )
204 return await velociraptor_agent_healthcheck(agent, time_criteria)
205
206
@@ -167,9 +208,14 @@ async def get_velociraptor_agent_healthcheck_by_agent_id(
208 "/logs",
209 response_model=HostLogsSearchResponse,
210 description="Get host logs",
170 - dependencies=[Security(AuthHandler().get_current_user, scopes=["admin", "analyst"])],
211 + dependencies=[
212 + Security(AuthHandler().get_current_user, scopes=["admin", "analyst"]),
213 + ],
214 )
172 -async def get_host_logs(body: HostLogsSearchBody, session: AsyncSession = Depends(get_db)) -> HostLogsSearchResponse:
215 +async def get_host_logs(
216 + body: HostLogsSearchBody,
217 + session: AsyncSession = Depends(get_db),
218 +) -> HostLogsSearchResponse:
219 """
220 Get host logs for a specific agent.
221
@@ -186,9 +232,14 @@ async def get_host_logs(body: HostLogsSearchBody, session: AsyncSession = Depend
232 logger.info(f"Received request to get host logs for {body.agent_name}")
233
234 # Asynchronously verify the agent exists
189 - result = await session.execute(select(Agents).filter(Agents.hostname == body.agent_name))
235 + result = await session.execute(
236 + select(Agents).filter(Agents.hostname == body.agent_name),
237 + )
238 agent = result.scalars().first()
239
240 if not agent:
193 - raise HTTPException(status_code=404, detail=f"Agent with hostname {body.agent_name} not found")
241 + raise HTTPException(
242 + status_code=404,
243 + detail=f"Agent with hostname {body.agent_name} not found",
244 + )
245 return await host_logs(body)
backend/app/healthchecks/agents/schema/agents.py
+48 -19
@@ -1,12 +1,7 @@
1 from datetime import datetime
2 -from typing import Any
3 -from typing import Dict
4 -from typing import List
5 -from typing import Optional
2 +from typing import Any, Dict, List, Optional
3
7 -from pydantic import BaseModel
8 -from pydantic import Field
9 -from pydantic import validator
4 +from pydantic import BaseModel, Field, validator
5
6
7 class AgentModel(BaseModel):
@@ -29,9 +24,18 @@ class AgentModel(BaseModel):
24
25
26 class ExtendedAgentModel(AgentModel):
32 - unhealthy_wazuh_agent: Optional[bool] = Field(None, description="Whether the agent is unhealthy in Wazuh")
33 - unhealthy_velociraptor_agent: Optional[bool] = Field(None, description="Whether the agent is unhealthy in Velociraptor")
34 - unhealthy_recent_logs_collected: Optional[bool] = Field(None, description="Whether the agent has not collected logs recently")
27 + unhealthy_wazuh_agent: Optional[bool] = Field(
28 + None,
29 + description="Whether the agent is unhealthy in Wazuh",
30 + )
31 + unhealthy_velociraptor_agent: Optional[bool] = Field(
32 + None,
33 + description="Whether the agent is unhealthy in Velociraptor",
34 + )
35 + unhealthy_recent_logs_collected: Optional[bool] = Field(
36 + None,
37 + description="Whether the agent has not collected logs recently",
38 + )
39
40
41 class AgentHealthCheckResponse(BaseModel):
@@ -46,9 +50,18 @@ class AgentHealthCheckResponse(BaseModel):
50
51
52 class TimeCriteriaModel(BaseModel):
49 - minutes: int = Field(60, description="Number of minutes within which the agent should have been last seen to be considered healthy.")
50 - hours: int = Field(0, description="Number of hours within which the agent should have been last seen to be considered healthy.")
51 - days: int = Field(0, description="Number of days within which the agent should have been last seen to be considered healthy.")
53 + minutes: int = Field(
54 + 60,
55 + description="Number of minutes within which the agent should have been last seen to be considered healthy.",
56 + )
57 + hours: int = Field(
58 + 0,
59 + description="Number of hours within which the agent should have been last seen to be considered healthy.",
60 + )
61 + days: int = Field(
62 + 0,
63 + description="Number of days within which the agent should have been last seen to be considered healthy.",
64 + )
65
66
67 ########## Logs Schemas ##########
@@ -57,7 +70,10 @@ class TimeCriteriaModel(BaseModel):
70 class Log(BaseModel):
71 index_name: str
72 total_logs: int
60 - logs: Optional[List[Dict[str, Any]]] = Field([], description="The logs returned from the search.")
73 + logs: Optional[List[Dict[str, Any]]] = Field(
74 + [],
75 + description="The logs returned from the search.",
76 + )
77
78
79 class LogsSearchBody(BaseModel):
@@ -65,16 +81,23 @@ class LogsSearchBody(BaseModel):
81 timerange: str = Field("24h", description="The time range to search logs in.")
82 log_field: str = Field("syslog_level", description="The field to search logs in.")
83 log_value: str = Field("INFO", description="The value to search logs for.")
68 - timestamp_field: str = Field("timestamp_utc", description="The timestamp field to search logs in.")
84 + timestamp_field: str = Field(
85 + "timestamp_utc",
86 + description="The timestamp field to search logs in.",
87 + )
88
89 @validator("timerange")
90 def validate_timerange(cls, value):
91 if value[-1] not in ("h", "d", "w", "m"):
73 - raise ValueError("Invalid timerange format. The string should end with either 'h', 'd', 'w', or 'm'.")
92 + raise ValueError(
93 + "Invalid timerange format. The string should end with either 'h', 'd', 'w', or 'm'.",
94 + )
95
96 # Optionally, you can check that the prefix is a number
97 if not value[:-1].isdigit():
77 - raise ValueError("Invalid timerange format. The string should start with a number.")
98 + raise ValueError(
99 + "Invalid timerange format. The string should start with a number.",
100 + )
101
102 return value
103
@@ -92,11 +115,17 @@ class CollectLogsResponse(BaseModel):
115
116
117 class HostLogsSearchBody(LogsSearchBody):
95 - agent_name: str = Field(..., description="The name of the agent to search logs for.")
118 + agent_name: str = Field(
119 + ...,
120 + description="The name of the agent to search logs for.",
121 + )
122
123
124 class HostLogsSearchResponse(BaseModel):
99 - logs_summary: Optional[List[Log]] = Field([], description="The logs summary returned from the search.")
125 + logs_summary: Optional[List[Log]] = Field(
126 + [],
127 + description="The logs summary returned from the search.",
128 + )
129 healthy: bool = Field(False, description="Whether the host is healthy or not.")
130 success: bool
131 message: str
backend/app/healthchecks/agents/services/agents.py
+97 -37
@@ -1,25 +1,29 @@
1 -from datetime import datetime
2 -from datetime import timedelta
3 -from typing import Optional
4 -from typing import Type
5 -
1 +from datetime import datetime, timedelta
2 +from typing import Optional, Type
3 +
4 +from app.connectors.wazuh_indexer.utils.universal import (
5 + LogsQueryBuilder,
6 + collect_indices,
7 + create_wazuh_indexer_client,
8 +)
9 +from app.healthchecks.agents.schema.agents import (
10 + AgentHealthCheckResponse,
11 + AgentModel,
12 + CollectLogsResponse,
13 + ExtendedAgentModel,
14 + HostLogsSearchBody,
15 + HostLogsSearchResponse,
16 + LogsSearchBody,
17 + TimeCriteriaModel,
18 +)
19 from fastapi import HTTPException
20 from loguru import logger
21
9 -from app.connectors.wazuh_indexer.utils.universal import LogsQueryBuilder
10 -from app.connectors.wazuh_indexer.utils.universal import collect_indices
11 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
12 -from app.healthchecks.agents.schema.agents import AgentHealthCheckResponse
13 -from app.healthchecks.agents.schema.agents import AgentModel
14 -from app.healthchecks.agents.schema.agents import CollectLogsResponse
15 -from app.healthchecks.agents.schema.agents import ExtendedAgentModel
16 -from app.healthchecks.agents.schema.agents import HostLogsSearchBody
17 -from app.healthchecks.agents.schema.agents import HostLogsSearchResponse
18 -from app.healthchecks.agents.schema.agents import LogsSearchBody
19 -from app.healthchecks.agents.schema.agents import TimeCriteriaModel
20 -
22
22 -def is_wazuh_agent_unhealthy(agent: AgentModel, time_criteria: TimeCriteriaModel) -> ExtendedAgentModel:
23 +def is_wazuh_agent_unhealthy(
24 + agent: AgentModel,
25 + time_criteria: TimeCriteriaModel,
26 +) -> ExtendedAgentModel:
27 """
28 Checks if a Wazuh agent is unhealthy based on the last seen time and time criteria.
29
@@ -34,18 +38,25 @@ def is_wazuh_agent_unhealthy(agent: AgentModel, time_criteria: TimeCriteriaModel
38 wazuh_last_seen = agent.wazuh_last_seen
39
40 if wazuh_last_seen > current_time:
37 - logger.info(f"Agent {agent} has a wazuh_last_seen time in the future: {wazuh_last_seen}")
41 + logger.info(
42 + f"Agent {agent} has a wazuh_last_seen time in the future: {wazuh_last_seen}",
43 + )
44 return ExtendedAgentModel(**agent.dict(), unhealthy_wazuh_agent=True)
45
46 # Calculate the total time delta based on the criteria
41 - total_minutes = time_criteria.minutes + time_criteria.hours * 60 + time_criteria.days * 24 * 60
47 + total_minutes = (
48 + time_criteria.minutes + time_criteria.hours * 60 + time_criteria.days * 24 * 60
49 + )
50 time_delta = timedelta(minutes=total_minutes)
51
52 is_unhealthy = (current_time - wazuh_last_seen) > time_delta
53 return ExtendedAgentModel(**agent.dict(), unhealthy_wazuh_agent=is_unhealthy)
54
55
48 -def is_velociraptor_agent_unhealthy(agent: AgentModel, time_criteria: TimeCriteriaModel) -> ExtendedAgentModel:
56 +def is_velociraptor_agent_unhealthy(
57 + agent: AgentModel,
58 + time_criteria: TimeCriteriaModel,
59 +) -> ExtendedAgentModel:
60 """
61 Checks if a velociraptor agent is unhealthy based on the last seen time and time criteria.
62
@@ -60,18 +71,25 @@ def is_velociraptor_agent_unhealthy(agent: AgentModel, time_criteria: TimeCriter
71 velociraptor_last_seen = agent.velociraptor_last_seen
72
73 if velociraptor_last_seen > current_time:
63 - logger.info(f"Agent {agent} has a velociraptor_last_seen time in the future: {velociraptor_last_seen}")
74 + logger.info(
75 + f"Agent {agent} has a velociraptor_last_seen time in the future: {velociraptor_last_seen}",
76 + )
77 return ExtendedAgentModel(**agent.dict(), unhealthy_velociraptor_agent=True)
78
79 # Calculate the total time delta based on the criteria
67 - total_minutes = time_criteria.minutes + time_criteria.hours * 60 + time_criteria.days * 24 * 60
80 + total_minutes = (
81 + time_criteria.minutes + time_criteria.hours * 60 + time_criteria.days * 24 * 60
82 + )
83 time_delta = timedelta(minutes=total_minutes)
84
85 is_unhealthy = (current_time - velociraptor_last_seen) > time_delta
86 return ExtendedAgentModel(**agent.dict(), unhealthy_velociraptor_agent=is_unhealthy)
87
88
74 -async def wazuh_agents_healthcheck(agents: list, time_criteria: TimeCriteriaModel) -> AgentHealthCheckResponse:
89 +async def wazuh_agents_healthcheck(
90 + agents: list,
91 + time_criteria: TimeCriteriaModel,
92 +) -> AgentHealthCheckResponse:
93 """
94 Perform a health check on Wazuh agents.
95
@@ -104,7 +122,10 @@ async def wazuh_agents_healthcheck(agents: list, time_criteria: TimeCriteriaMode
122 )
123
124
107 -async def wazuh_agent_healthcheck(agent: AgentModel, time_criteria: TimeCriteriaModel) -> AgentHealthCheckResponse:
125 +async def wazuh_agent_healthcheck(
126 + agent: AgentModel,
127 + time_criteria: TimeCriteriaModel,
128 +) -> AgentHealthCheckResponse:
129 """
130 Performs a health check on a Wazuh agent.
131
@@ -132,7 +153,10 @@ async def wazuh_agent_healthcheck(agent: AgentModel, time_criteria: TimeCriteria
153 )
154
155
135 -async def velociraptor_agents_healthcheck(agents: list, time_criteria: TimeCriteriaModel) -> AgentHealthCheckResponse:
156 +async def velociraptor_agents_healthcheck(
157 + agents: list,
158 + time_criteria: TimeCriteriaModel,
159 +) -> AgentHealthCheckResponse:
160 """
161 Perform health check on Velociraptor agents.
162
@@ -166,7 +190,10 @@ async def velociraptor_agents_healthcheck(agents: list, time_criteria: TimeCrite
190 )
191
192
169 -async def velociraptor_agent_healthcheck(agent: AgentModel, time_criteria: TimeCriteriaModel) -> AgentHealthCheckResponse:
193 +async def velociraptor_agent_healthcheck(
194 + agent: AgentModel,
195 + time_criteria: TimeCriteriaModel,
196 +) -> AgentHealthCheckResponse:
197 """
198 Perform a health check on a Velociraptor agent.
199
@@ -229,7 +256,11 @@ async def host_logs(search_body: HostLogsSearchBody) -> HostLogsSearchResponse:
256 )
257
258
232 -async def get_logs_generic(search_body: Type[LogsSearchBody], is_host_specific: bool = False, index_name: Optional[str] = None):
259 +async def get_logs_generic(
260 + search_body: Type[LogsSearchBody],
261 + is_host_specific: bool = False,
262 + index_name: Optional[str] = None,
263 +):
264 """
265 Retrieves logs based on the provided search criteria.
266
@@ -241,14 +272,22 @@ async def get_logs_generic(search_body: Type[LogsSearchBody], is_host_specific:
272 Returns:
273 dict: A dictionary containing the logs summary, success status, and message.
274 """
244 - logger.info(f"Collecting Wazuh Indexer alerts for host {search_body.agent_name if is_host_specific else ''}")
275 + logger.info(
276 + f"Collecting Wazuh Indexer alerts for host {search_body.agent_name if is_host_specific else ''}",
277 + )
278 logs_summary = []
279 indices = await collect_indices()
247 - index_list = [index_name] if index_name else indices.indices_list # Use the provided index_name or get all indices
280 + index_list = (
281 + [index_name] if index_name else indices.indices_list
282 + ) # Use the provided index_name or get all indices
283
284 for index_name in index_list:
285 try:
251 - logs = await collect_logs_generic(index_name, body=search_body, is_host_specific=is_host_specific)
286 + logs = await collect_logs_generic(
287 + index_name,
288 + body=search_body,
289 + is_host_specific=is_host_specific,
290 + )
291 if logs.success and len(logs.logs) > 0:
292 logs_summary.append(
293 {
@@ -259,17 +298,27 @@ async def get_logs_generic(search_body: Type[LogsSearchBody], is_host_specific:
298 )
299 break # Only collect logs from the first index that has logs
300 except HTTPException as e:
262 - logger.warning(f"An error occurred while processing index {index_name}: {e.detail}")
301 + logger.warning(
302 + f"An error occurred while processing index {index_name}: {e.detail}",
303 + )
304
305 if len(logs_summary) == 0:
306 message = "No logs found"
307 else:
308 message = f"Succesfully collected top {search_body.size} logs for each index"
309
269 - return {"logs_summary": logs_summary, "success": len(logs_summary) > 0, "message": message}
310 + return {
311 + "logs_summary": logs_summary,
312 + "success": len(logs_summary) > 0,
313 + "message": message,
314 + }
315
316
272 -async def collect_logs_generic(index_name: str, body: LogsSearchBody, is_host_specific: bool = False) -> CollectLogsResponse:
317 +async def collect_logs_generic(
318 + index_name: str,
319 + body: LogsSearchBody,
320 + is_host_specific: bool = False,
321 +) -> CollectLogsResponse:
322 """
323 Collects logs from Elasticsearch based on the specified parameters.
324
@@ -283,7 +332,10 @@ async def collect_logs_generic(index_name: str, body: LogsSearchBody, is_host_sp
332 """
333 es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
334 query_builder = LogsQueryBuilder()
286 - query_builder.add_time_range(timerange=body.timerange, timestamp_field=body.timestamp_field)
335 + query_builder.add_time_range(
336 + timerange=body.timerange,
337 + timestamp_field=body.timestamp_field,
338 + )
339 query_builder.add_matches(matches=[(body.log_field, body.log_value)])
340 query_builder.add_sort(body.timestamp_field)
341
@@ -297,7 +349,15 @@ async def collect_logs_generic(index_name: str, body: LogsSearchBody, is_host_sp
349 logger.info(f"logs collected: {logs}")
350 logs_list = [log for log in logs["hits"]["hits"]]
351 logger.info(f"logs collected: {logs_list}")
300 - return CollectLogsResponse(logs=logs_list, success=True, message="logs collected successfully")
352 + return CollectLogsResponse(
353 + logs=logs_list,
354 + success=True,
355 + message="logs collected successfully",
356 + )
357 except Exception as e:
358 logger.debug(f"Failed to collect logs: {e}")
303 - return CollectLogsResponse(logs=[], success=False, message=f"Failed to collect logs: {e}")
359 + return CollectLogsResponse(
360 + logs=[],
361 + success=False,
362 + message=f"Failed to collect logs: {e}",
363 + )
backend/app/integrations/alert_creation/general/routes/alert.py
+36 -17
@@ -1,22 +1,24 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from fastapi import HTTPException
4 -from loguru import logger
5 -from sqlalchemy.ext.asyncio import AsyncSession
6 -from sqlalchemy.future import select
7 -
1 from app.db.db_session import get_db
9 -from app.integrations.alert_creation.general.schema.alert import CreateAlertRequest
10 -from app.integrations.alert_creation.general.schema.alert import CreateAlertResponse
2 +from app.integrations.alert_creation.general.schema.alert import (
3 + CreateAlertRequest,
4 + CreateAlertResponse,
5 +)
6 from app.integrations.alert_creation.general.services.alert import create_alert
7 from app.integrations.alert_creation_settings.models.alert_creation_settings import (
8 AlertCreationSettings,
9 )
10 +from fastapi import APIRouter, Depends, HTTPException
11 +from loguru import logger
12 +from sqlalchemy.ext.asyncio import AsyncSession
13 +from sqlalchemy.future import select
14
15 general_alerts_router = APIRouter()
16
17
19 -async def is_rule_id_valid(create_alert_request: CreateAlertRequest, session: AsyncSession) -> bool:
18 +async def is_rule_id_valid(
19 + create_alert_request: CreateAlertRequest,
20 + session: AsyncSession,
21 +) -> bool:
22 """
23 Checks if the given rule ID is valid for the specified customer.
24
@@ -27,20 +29,30 @@ async def is_rule_id_valid(create_alert_request: CreateAlertRequest, session: As
29 Returns:
30 bool: True if the rule ID is valid for the customer, False otherwise.
31 """
30 - logger.info(f"Checking if rule_id: {create_alert_request.rule_id} is valid for customer: {create_alert_request.agent_labels_customer}")
32 + logger.info(
33 + f"Checking if rule_id: {create_alert_request.rule_id} is valid for customer: {create_alert_request.agent_labels_customer}",
34 + )
35
36 result = await session.execute(
33 - select(AlertCreationSettings).where(AlertCreationSettings.customer_code == create_alert_request.agent_labels_customer),
37 + select(AlertCreationSettings).where(
38 + AlertCreationSettings.customer_code
39 + == create_alert_request.agent_labels_customer,
40 + ),
41 )
42 settings = result.scalars().first()
43
37 - if settings and str(create_alert_request.rule_id) in (settings.excluded_wazuh_rules or "").split(","):
44 + if settings and str(create_alert_request.rule_id) in (
45 + settings.excluded_wazuh_rules or ""
46 + ).split(","):
47 return False
48
49 return True
50
51
43 -async def is_customer_code_valid(create_alert_request: CreateAlertRequest, session: AsyncSession) -> bool:
52 +async def is_customer_code_valid(
53 + create_alert_request: CreateAlertRequest,
54 + session: AsyncSession,
55 +) -> bool:
56 """
57 Checks if the customer code provided in the create_alert_request is valid.
58
@@ -51,10 +63,15 @@ async def is_customer_code_valid(create_alert_request: CreateAlertRequest, sessi
63 Returns:
64 bool: True if the customer code is valid, False otherwise.
65 """
54 - logger.info(f"Checking if customer_code: {create_alert_request.agent_labels_customer} is valid.")
66 + logger.info(
67 + f"Checking if customer_code: {create_alert_request.agent_labels_customer} is valid.",
68 + )
69
70 result = await session.execute(
57 - select(AlertCreationSettings).where(AlertCreationSettings.customer_code == create_alert_request.agent_labels_customer),
71 + select(AlertCreationSettings).where(
72 + AlertCreationSettings.customer_code
73 + == create_alert_request.agent_labels_customer,
74 + ),
75 )
76 settings = result.scalars().first()
77
@@ -89,7 +106,9 @@ async def create_general_alert(
106 logger.info(f"create_alert_request: {create_alert_request.dict()}")
107
108 if await is_customer_code_valid(create_alert_request, session) is False:
92 - logger.info(f"Invalid customer_code: {create_alert_request.agent_labels_customer}")
109 + logger.info(
110 + f"Invalid customer_code: {create_alert_request.agent_labels_customer}",
111 + )
112 raise HTTPException(status_code=200, detail="Invalid customer_code.")
113
114 if await is_rule_id_valid(create_alert_request, session) is False:
backend/app/integrations/alert_creation/general/schema/alert.py
+3 -8
@@ -1,12 +1,7 @@
1 from enum import Enum
2 -from typing import Any
3 -from typing import Dict
4 -from typing import List
5 -from typing import Optional
6 -
7 -from pydantic import BaseModel
8 -from pydantic import Extra
9 -from pydantic import Field
2 +from typing import Any, Dict, List, Optional
3 +
4 +from pydantic import BaseModel, Extra, Field
5
6
7 class ValidIocFields(Enum):
backend/app/integrations/alert_creation/general/services/alert.py
+96 -34
@@ -1,29 +1,33 @@
1 -from typing import Optional
2 -from typing import Set
3 -
4 -from fastapi import HTTPException
5 -from loguru import logger
6 -from sqlalchemy.ext.asyncio import AsyncSession
1 +from typing import Optional, Set
2
3 from app.agents.routes.agents import get_agent
4 from app.agents.schema.agents import AgentsResponse
10 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
11 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
12 -from app.integrations.alert_creation.general.schema.alert import CreateAlertRequest
13 -from app.integrations.alert_creation.general.schema.alert import CreateAlertResponse
14 -from app.integrations.alert_creation.general.schema.alert import IrisAlertContext
15 -from app.integrations.alert_creation.general.schema.alert import IrisAlertPayload
16 -from app.integrations.alert_creation.general.schema.alert import IrisAsset
17 -from app.integrations.alert_creation.general.schema.alert import IrisIoc
18 -from app.integrations.alert_creation.general.schema.alert import ValidIocFields
5 +from app.connectors.dfir_iris.utils.universal import (
6 + fetch_and_validate_data,
7 + initialize_client_and_alert,
8 +)
9 +from app.integrations.alert_creation.general.schema.alert import (
10 + CreateAlertRequest,
11 + CreateAlertResponse,
12 + IrisAlertContext,
13 + IrisAlertPayload,
14 + IrisAsset,
15 + IrisIoc,
16 + ValidIocFields,
17 +)
18 from app.integrations.alert_creation.general.services.alert_multi_exclude import (
19 AlertDetailsService,
20 )
22 -from app.integrations.utils.alerts import get_asset_type_id
23 -from app.integrations.utils.alerts import send_to_shuffle
24 -from app.integrations.utils.alerts import validate_ioc_type
21 +from app.integrations.utils.alerts import (
22 + get_asset_type_id,
23 + send_to_shuffle,
24 + validate_ioc_type,
25 +)
26 from app.integrations.utils.schema import ShufflePayload
27 from app.utils import get_customer_alert_settings
28 +from fastapi import HTTPException
29 +from loguru import logger
30 +from sqlalchemy.ext.asyncio import AsyncSession
31
32
33 def valid_ioc_fields() -> Set[str]:
@@ -37,7 +41,10 @@ def valid_ioc_fields() -> Set[str]:
41 return {field.value for field in ValidIocFields}
42
43
40 -async def construct_alert_source_link(alert_details: CreateAlertRequest, session: AsyncSession) -> str:
44 +async def construct_alert_source_link(
45 + alert_details: CreateAlertRequest,
46 + session: AsyncSession,
47 +) -> str:
48 """
49 Construct the alert source link for the alert details.
50 Parameters
@@ -50,12 +57,22 @@ async def construct_alert_source_link(alert_details: CreateAlertRequest, session
57 The alert source link.
58 """
59 # Check if the alert has a process id and that it is not "No process ID found"
53 - if hasattr(alert_details, "process_id") and alert_details.process_id != "No process ID found":
54 - query_string = f"%22query%22:%22process_id:%5C%22{alert_details.process_id}%5C%22%20AND%20"
60 + if (
61 + hasattr(alert_details, "process_id")
62 + and alert_details.process_id != "No process ID found"
63 + ):
64 + query_string = (
65 + f"%22query%22:%22process_id:%5C%22{alert_details.process_id}%5C%22%20AND%20"
66 + )
67 else:
68 query_string = f"%22query%22:%22_id:%5C%22{alert_details.id}%5C%22%20AND%20"
69
58 - grafana_url = (await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)).grafana_url
70 + grafana_url = (
71 + await get_customer_alert_settings(
72 + customer_code=alert_details.agent_labels_customer,
73 + session=session,
74 + )
75 + ).grafana_url
76
77 return (
78 f"{grafana_url}/explore?left=%5B%22now-6h%22,%22now%22,%22WAZUH%22,%7B%22refId%22:%22A%22,"
@@ -130,11 +147,22 @@ async def build_alert_context_payload(
147 """
148 return IrisAlertContext(
149 customer_iris_id=(
133 - await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)
150 + await get_customer_alert_settings(
151 + customer_code=alert_details.agent_labels_customer,
152 + session=session,
153 + )
154 ).iris_customer_id,
135 - customer_name=(await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)).customer_name,
155 + customer_name=(
156 + await get_customer_alert_settings(
157 + customer_code=alert_details.agent_labels_customer,
158 + session=session,
159 + )
160 + ).customer_name,
161 customer_cases_index=(
137 - await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)
162 + await get_customer_alert_settings(
163 + customer_code=alert_details.agent_labels_customer,
164 + session=session,
165 + )
166 ).iris_index,
167 alert_id=alert_details.id,
168 alert_name=alert_details.rule_description,
@@ -177,8 +205,17 @@ async def build_alert_payload(
205 IrisAlertPayload: The built alert payload.
206 """
207 asset_payload = await build_asset_payload(agent_data, alert_details)
180 - context_payload = await build_alert_context_payload(alert_details=alert_details, agent_data=agent_data, session=session)
181 - timefield = (await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)).timefield
208 + context_payload = await build_alert_context_payload(
209 + alert_details=alert_details,
210 + agent_data=agent_data,
211 + session=session,
212 + )
213 + timefield = (
214 + await get_customer_alert_settings(
215 + customer_code=alert_details.agent_labels_customer,
216 + session=session,
217 + )
218 + ).timefield
219 # Get the timefield value from the alert_details
220 if hasattr(alert_details, timefield):
221 alert_details.time_field = getattr(alert_details, timefield)
@@ -187,14 +224,20 @@ async def build_alert_payload(
224 logger.info(f"Alert has IoC: {ioc_payload}")
225 return IrisAlertPayload(
226 alert_title=alert_details.rule_description,
190 - alert_source_link=await construct_alert_source_link(alert_details, session=session),
227 + alert_source_link=await construct_alert_source_link(
228 + alert_details,
229 + session=session,
230 + ),
231 alert_description=alert_details.rule_description,
232 alert_source="CoPilot",
233 assets=[asset_payload],
234 alert_status_id=3,
235 alert_severity_id=5,
236 alert_customer_id=(
197 - await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)
237 + await get_customer_alert_settings(
238 + customer_code=alert_details.agent_labels_customer,
239 + session=session,
240 + )
241 ).iris_customer_id,
242 alert_source_content=alert_details.to_dict(),
243 alert_context=context_payload,
@@ -205,14 +248,20 @@ async def build_alert_payload(
248 logger.info("Alert does not have IoC")
249 return IrisAlertPayload(
250 alert_title=alert_details.rule_description,
208 - alert_source_link=await construct_alert_source_link(alert_details, session=session),
251 + alert_source_link=await construct_alert_source_link(
252 + alert_details,
253 + session=session,
254 + ),
255 alert_description=alert_details.rule_description,
256 alert_source="SOCFORTRESS RULE",
257 assets=[asset_payload],
258 alert_status_id=3,
259 alert_severity_id=5,
260 alert_customer_id=(
215 - await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)
261 + await get_customer_alert_settings(
262 + customer_code=alert_details.agent_labels_customer,
263 + session=session,
264 + )
265 ).iris_customer_id,
266 alert_source_content=alert_details.to_dict(),
267 alert_context=context_payload,
@@ -220,7 +269,10 @@ async def build_alert_payload(
269 )
270
271
223 -async def create_alert(alert: CreateAlertRequest, session: AsyncSession) -> CreateAlertResponse:
272 +async def create_alert(
273 + alert: CreateAlertRequest,
274 + session: AsyncSession,
275 +) -> CreateAlertResponse:
276 """
277 Creates an alert in IRIS.
278
@@ -276,7 +328,12 @@ async def create_alert(alert: CreateAlertRequest, session: AsyncSession) -> Crea
328 alert_id,
329 {"iocs": [dict(IrisIoc(**iris_alert_payload.alert_iocs[0].to_dict()))]},
330 )
279 - customer_name = (await get_customer_alert_settings(customer_code=alert.agent_labels_customer, session=session)).customer_name
331 + customer_name = (
332 + await get_customer_alert_settings(
333 + customer_code=alert.agent_labels_customer,
334 + session=session,
335 + )
336 + ).customer_name
337 await send_to_shuffle(
338 ShufflePayload(
339 alert_id=alert_id,
@@ -290,7 +347,12 @@ async def create_alert(alert: CreateAlertRequest, session: AsyncSession) -> Crea
347 )
348 return CreateAlertResponse(
349 alert_id=alert_id,
293 - customer=(await get_customer_alert_settings(customer_code=alert.agent_labels_customer, session=session)).customer_name,
350 + customer=(
351 + await get_customer_alert_settings(
352 + customer_code=alert.agent_labels_customer,
353 + session=session,
354 + )
355 + ).customer_name,
356 alert_source_link=await construct_alert_source_link(alert, session=session),
357 success=True,
358 message=f"Successfully created alert {alert_id} in IRIS.",
backend/app/integrations/alert_creation/general/services/alert_multi_exclude.py
+23 -13
@@ -1,17 +1,13 @@
1 -from typing import Any
2 -from typing import Dict
3 -from typing import List
4 -from typing import Tuple
5 -
6 -from elasticsearch7 import NotFoundError
7 -from loguru import logger
8 -from sqlalchemy.ext.asyncio import AsyncSession
1 +from typing import Any, Dict, List, Tuple
2
3 from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
4 from app.integrations.alert_creation_settings.models.alert_creation_settings import (
5 AlertCreationEventConfig,
6 )
7 from app.utils import get_customer_alert_event_configs
8 +from elasticsearch7 import NotFoundError
9 +from loguru import logger
10 +from sqlalchemy.ext.asyncio import AsyncSession
11
12
13 class AlertDetailsService:
@@ -69,7 +65,9 @@ class AlertDetailsService:
65 result = self.es.search(index="_all", body=query)
66
67 # Extract (index, id) pairs from the result
72 - index_id_pairs = [(hit["_index"], hit["_id"]) for hit in result["hits"]["hits"]]
68 + index_id_pairs = [
69 + (hit["_index"], hit["_id"]) for hit in result["hits"]["hits"]
70 + ]
71 logger.info(
72 f"Found {len(index_id_pairs)} alerts with syslog_level of 'ALERT' within the last 1 hour.",
73 )
@@ -157,7 +155,11 @@ class AlertDetailsService:
155 "query": {"bool": {"must": must_terms}},
156 }
157
160 - async def process_events(self, events: list, event_configs: List[AlertCreationEventConfig]):
158 + async def process_events(
159 + self,
160 + events: list,
161 + event_configs: List[AlertCreationEventConfig],
162 + ):
163 """
164 Process the events and check for exclusions.
165 For every event in the `config.ini` file there is a field and value to check for.
@@ -179,7 +181,10 @@ class AlertDetailsService:
181 for index, event in enumerate(events):
182 event_id = event_order[0 if not first_match_found else 1]
183 logger.info(f"Checking for event_id: {event_id}")
182 - event_config = next((config for config in event_configs if config.event_id == event_id), None)
184 + event_config = next(
185 + (config for config in event_configs if config.event_id == event_id),
186 + None,
187 + )
188 if event_config is None:
189 continue
190
@@ -232,14 +237,19 @@ class AlertDetailsService:
237 logger.info(f"Total alert timeline hits: {total_hits}")
238
239 # Build and sort the list of events
235 - events = [event["_source"] for event in alert_timeline_events["hits"]["hits"]]
240 + events = [
241 + event["_source"] for event in alert_timeline_events["hits"]["hits"]
242 + ]
243 events.sort(key=lambda x: x["timestamp_utc"])
244
245 # return self.process_events(events)
246 logger.info(f"Events: {events}")
247
248 # Get all order keys from the 'Order' section in config.ini
242 - order_keys = await get_customer_alert_event_configs(customer_code=events[0]["agent_labels_customer"], session=session)
249 + order_keys = await get_customer_alert_event_configs(
250 + customer_code=events[0]["agent_labels_customer"],
251 + session=session,
252 + )
253 logger.info(f"Order keys: {order_keys}")
254
255 # Process events for each order key
backend/app/integrations/alert_creation/office365/routes/alert.py
+20 -20
@@ -1,28 +1,13 @@
1 # from app.alerts.office365.services.threat_intel import create_threat_intel_alert
2 -from fastapi import APIRouter
3 -from fastapi import Depends
4 -from fastapi import HTTPException
5 -from loguru import logger
6 -from sqlalchemy import select
7 -from sqlalchemy.ext.asyncio import AsyncSession
8 -
2 from app.db.db_session import get_db
3 from app.integrations.alert_creation.office365.schema.exchange import (
4 Office365ExchangeAlertBase,
12 -)
13 -from app.integrations.alert_creation.office365.schema.exchange import (
5 Office365ExchangeAlertRequest,
15 -)
16 -from app.integrations.alert_creation.office365.schema.exchange import (
6 Office365ExchangeAlertResponse,
18 -)
19 -from app.integrations.alert_creation.office365.schema.exchange import (
7 ValidOffice365Workloads,
8 )
9 from app.integrations.alert_creation.office365.schema.threat_intel import (
10 Office365ThreatIntelAlertRequest,
24 -)
25 -from app.integrations.alert_creation.office365.schema.threat_intel import (
11 Office365ThreatIntelAlertResponse,
12 )
13 from app.integrations.alert_creation.office365.services.exchange import (
@@ -34,11 +19,18 @@ from app.integrations.alert_creation.office365.services.threat_intel import (
19 from app.integrations.alert_creation_settings.models.alert_creation_settings import (
20 AlertCreationSettings,
21 )
22 +from fastapi import APIRouter, Depends, HTTPException
23 +from loguru import logger
24 +from sqlalchemy import select
25 +from sqlalchemy.ext.asyncio import AsyncSession
26
27 office365_alerts_router = APIRouter()
28
29
41 -async def is_office365_organization_id_valid(create_alert_request: Office365ExchangeAlertBase, session: AsyncSession) -> bool:
30 +async def is_office365_organization_id_valid(
31 + create_alert_request: Office365ExchangeAlertBase,
32 + session: AsyncSession,
33 +) -> bool:
34 """
35 Checks if the given organization ID is valid for the specified customer.
36
@@ -55,12 +47,16 @@ async def is_office365_organization_id_valid(create_alert_request: Office365Exch
47
48 result = await session.execute(
49 select(AlertCreationSettings).where(
58 - AlertCreationSettings.office365_organization_id == create_alert_request.data_office365_OrganizationId,
50 + AlertCreationSettings.office365_organization_id
51 + == create_alert_request.data_office365_OrganizationId,
52 ),
53 )
54 settings = result.scalars().first()
55 if settings is None:
63 - raise HTTPException(status_code=400, detail="Office365 organization ID is not valid, make sure to provision the customer.")
56 + raise HTTPException(
57 + status_code=400,
58 + detail="Office365 organization ID is not valid, make sure to provision the customer.",
59 + )
60
61 return True
62
@@ -75,7 +71,9 @@ async def create_office365_exchange_alert(
71 session: AsyncSession = Depends(get_db),
72 ):
73 logger.info(f"create_alert_request: {create_alert_request}")
78 - if create_alert_request.data_office365_Workload not in [workload.value for workload in ValidOffice365Workloads]:
74 + if create_alert_request.data_office365_Workload not in [
75 + workload.value for workload in ValidOffice365Workloads
76 + ]:
77 logger.info(f"Invalid workload: {create_alert_request.data_office365_Workload}")
78 raise HTTPException(status_code=400, detail="Invalid workload")
79 logger.info(f"Workload is valid: {create_alert_request.data_office365_Workload}")
@@ -93,7 +91,9 @@ async def create_office365_threat_intel_alert(
91 session: AsyncSession = Depends(get_db),
92 ):
93 logger.info(f"create_alert_request: {create_alert_request}")
96 - if create_alert_request.data_office365_Workload not in [workload.value for workload in ValidOffice365Workloads]:
94 + if create_alert_request.data_office365_Workload not in [
95 + workload.value for workload in ValidOffice365Workloads
96 + ]:
97 logger.info(f"Invalid workload: {create_alert_request.data_office365_Workload}")
98 raise HTTPException(status_code=400, detail="Invalid workload")
99 logger.info(f"Workload is valid: {create_alert_request.data_office365_Workload}")
backend/app/integrations/alert_creation/office365/schema/exchange.py
+3 -8
@@ -1,12 +1,7 @@
1 from enum import Enum
2 -from typing import Any
3 -from typing import Dict
4 -from typing import List
5 -from typing import Optional
6 -
7 -from pydantic import BaseModel
8 -from pydantic import Extra
9 -from pydantic import Field
2 +from typing import Any, Dict, List, Optional
3 +
4 +from pydantic import BaseModel, Extra, Field
5
6
7 class ValidOffice365Workloads(Enum):
backend/app/integrations/alert_creation/office365/schema/threat_intel.py
+2 -7
@@ -1,12 +1,7 @@
1 from enum import Enum
2 -from typing import Any
3 -from typing import Dict
4 -from typing import List
5 -from typing import Optional
2 +from typing import Any, Dict, List, Optional
3
7 -from pydantic import BaseModel
8 -from pydantic import Extra
9 -from pydantic import Field
4 +from pydantic import BaseModel, Extra, Field
5
6
7 class ValidOffice365Workloads(Enum):
backend/app/integrations/alert_creation/office365/services/exchange.py
+54 -26
@@ -1,26 +1,23 @@
1 -from typing import Optional
2 -from typing import Set
1 +from typing import Optional, Set
2
4 -from loguru import logger
5 -from sqlalchemy.ext.asyncio import AsyncSession
6 -
7 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
8 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
3 +from app.connectors.dfir_iris.utils.universal import (
4 + fetch_and_validate_data,
5 + initialize_client_and_alert,
6 +)
7 from app.integrations.alert_creation.general.schema.alert import ValidIocFields
10 -from app.integrations.alert_creation.office365.schema.exchange import IrisAlertContext
11 -from app.integrations.alert_creation.office365.schema.exchange import IrisAlertPayload
12 -from app.integrations.alert_creation.office365.schema.exchange import IrisAsset
13 -from app.integrations.alert_creation.office365.schema.exchange import IrisIoc
8 from app.integrations.alert_creation.office365.schema.exchange import (
9 + IrisAlertContext,
10 + IrisAlertPayload,
11 + IrisAsset,
12 + IrisIoc,
13 Office365ExchangeAlertRequest,
16 -)
17 -from app.integrations.alert_creation.office365.schema.exchange import (
14 Office365ExchangeAlertResponse,
15 )
20 -from app.integrations.utils.alerts import send_to_shuffle
21 -from app.integrations.utils.alerts import validate_ioc_type
16 +from app.integrations.utils.alerts import send_to_shuffle, validate_ioc_type
17 from app.integrations.utils.schema import ShufflePayload
18 from app.utils import get_customer_alert_settings_office365
19 +from loguru import logger
20 +from sqlalchemy.ext.asyncio import AsyncSession
21
22
23 def valid_ioc_fields() -> Set[str]:
@@ -34,7 +31,10 @@ def valid_ioc_fields() -> Set[str]:
31 return {field.value for field in ValidIocFields}
32
33
37 -async def construct_alert_source_link(alert_details: Office365ExchangeAlertRequest, session: AsyncSession) -> str:
34 +async def construct_alert_source_link(
35 + alert_details: Office365ExchangeAlertRequest,
36 + session: AsyncSession,
37 +) -> str:
38 """
39 Construct the alert source link for the alert details.
40 Parameters
@@ -47,7 +47,10 @@ async def construct_alert_source_link(alert_details: Office365ExchangeAlertReque
47 The alert source link.
48 """
49 grafana_url = (
50 - await get_customer_alert_settings_office365(office365_organization_id=alert_details.data_office365_OrganizationId, session=session)
50 + await get_customer_alert_settings_office365(
51 + office365_organization_id=alert_details.data_office365_OrganizationId,
52 + session=session,
53 + )
54 ).grafana_url
55
56 return (
@@ -59,7 +62,9 @@ async def construct_alert_source_link(alert_details: Office365ExchangeAlertReque
62 )
63
64
62 -async def build_ioc_payload(alert_details: Office365ExchangeAlertRequest) -> Optional[IrisIoc]:
65 +async def build_ioc_payload(
66 + alert_details: Office365ExchangeAlertRequest,
67 +) -> Optional[IrisIoc]:
68 """
69 Builds an IoC payload based on the provided alert details.
70
@@ -82,7 +87,9 @@ async def build_ioc_payload(alert_details: Office365ExchangeAlertRequest) -> Opt
87 return None
88
89
85 -async def build_asset_payload(alert_details: Office365ExchangeAlertRequest) -> IrisAsset:
90 +async def build_asset_payload(
91 + alert_details: Office365ExchangeAlertRequest,
92 +) -> IrisAsset:
93 if alert_details.data_office365_UserId:
94 return IrisAsset(
95 asset_name=alert_details.data_office365_UserId,
@@ -160,7 +167,10 @@ async def build_alert_payload(
167 IrisAlertPayload: The built alert payload.
168 """
169 asset_payload = await build_asset_payload(alert_details)
163 - context_payload = await build_alert_context_payload(alert_details=alert_details, session=session)
170 + context_payload = await build_alert_context_payload(
171 + alert_details=alert_details,
172 + session=session,
173 + )
174 timefield = "timestamp_utc"
175 # Get the timefield value from the alert_details
176 if hasattr(alert_details, timefield):
@@ -170,7 +180,10 @@ async def build_alert_payload(
180 logger.info(f"Alert has IoC: {ioc_payload}")
181 return IrisAlertPayload(
182 alert_title=alert_details.data_office365_Operation,
173 - alert_source_link=await construct_alert_source_link(alert_details, session=session),
183 + alert_source_link=await construct_alert_source_link(
184 + alert_details,
185 + session=session,
186 + ),
187 alert_description=alert_details.rule_description,
188 alert_source="Office365 Exchange Rule",
189 assets=[asset_payload],
@@ -191,7 +204,10 @@ async def build_alert_payload(
204 logger.info("Alert does not have IoC")
205 return IrisAlertPayload(
206 alert_title=alert_details.data_office365_Operation,
194 - alert_source_link=await construct_alert_source_link(alert_details, session=session),
207 + alert_source_link=await construct_alert_source_link(
208 + alert_details,
209 + session=session,
210 + ),
211 alert_description=alert_details.rule_description,
212 alert_source="Office365 Exchange Rule",
213 assets=[asset_payload],
@@ -209,7 +225,10 @@ async def build_alert_payload(
225 )
226
227
212 -async def create_exchange_alert(alert: Office365ExchangeAlertRequest, session: AsyncSession) -> Office365ExchangeAlertResponse:
228 +async def create_exchange_alert(
229 + alert: Office365ExchangeAlertRequest,
230 + session: AsyncSession,
231 +) -> Office365ExchangeAlertResponse:
232 """
233 Creates an Office365 Exchange alert in IRIS.
234
@@ -255,10 +274,16 @@ async def create_exchange_alert(alert: Office365ExchangeAlertRequest, session: A
274 ShufflePayload(
275 alert_id=alert_id,
276 customer=(
258 - await get_customer_alert_settings_office365(office365_organization_id=alert.data_office365_OrganizationId, session=session)
277 + await get_customer_alert_settings_office365(
278 + office365_organization_id=alert.data_office365_OrganizationId,
279 + session=session,
280 + )
281 ).customer_name,
282 customer_code=(
261 - await get_customer_alert_settings_office365(office365_organization_id=alert.data_office365_OrganizationId, session=session)
283 + await get_customer_alert_settings_office365(
284 + office365_organization_id=alert.data_office365_OrganizationId,
285 + session=session,
286 + )
287 ).customer_code,
288 alert_source_link=await construct_alert_source_link(alert, session=session),
289 rule_description=alert.rule_description,
@@ -269,7 +294,10 @@ async def create_exchange_alert(alert: Office365ExchangeAlertRequest, session: A
294 return Office365ExchangeAlertResponse(
295 alert_id=alert_id,
296 customer=(
272 - await get_customer_alert_settings_office365(office365_organization_id=alert.data_office365_OrganizationId, session=session)
297 + await get_customer_alert_settings_office365(
298 + office365_organization_id=alert.data_office365_OrganizationId,
299 + session=session,
300 + )
301 ).customer_name,
302 alert_source_link=await construct_alert_source_link(alert, session=session),
303 success=True,
backend/app/integrations/alert_creation/office365/services/threat_intel.py
+52 -28
@@ -1,30 +1,23 @@
1 -from typing import Optional
2 -from typing import Set
1 +from typing import Optional, Set
2
4 -from loguru import logger
5 -from sqlalchemy.ext.asyncio import AsyncSession
6 -
7 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
8 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
3 +from app.connectors.dfir_iris.utils.universal import (
4 + fetch_and_validate_data,
5 + initialize_client_and_alert,
6 +)
7 from app.integrations.alert_creation.general.schema.alert import ValidIocFields
8 from app.integrations.alert_creation.office365.schema.threat_intel import (
9 IrisAlertContext,
12 -)
13 -from app.integrations.alert_creation.office365.schema.threat_intel import (
10 IrisAlertPayload,
15 -)
16 -from app.integrations.alert_creation.office365.schema.threat_intel import IrisAsset
17 -from app.integrations.alert_creation.office365.schema.threat_intel import IrisIoc
18 -from app.integrations.alert_creation.office365.schema.threat_intel import (
11 + IrisAsset,
12 + IrisIoc,
13 Office365ThreatIntelAlertRequest,
20 -)
21 -from app.integrations.alert_creation.office365.schema.threat_intel import (
14 Office365ThreatIntelAlertResponse,
15 )
24 -from app.integrations.utils.alerts import send_to_shuffle
25 -from app.integrations.utils.alerts import validate_ioc_type
16 +from app.integrations.utils.alerts import send_to_shuffle, validate_ioc_type
17 from app.integrations.utils.schema import ShufflePayload
18 from app.utils import get_customer_alert_settings_office365
19 +from loguru import logger
20 +from sqlalchemy.ext.asyncio import AsyncSession
21
22
23 def valid_ioc_fields() -> Set[str]:
@@ -38,7 +31,10 @@ def valid_ioc_fields() -> Set[str]:
31 return {field.value for field in ValidIocFields}
32
33
41 -async def construct_alert_source_link(alert_details: Office365ThreatIntelAlertRequest, session: AsyncSession) -> str:
34 +async def construct_alert_source_link(
35 + alert_details: Office365ThreatIntelAlertRequest,
36 + session: AsyncSession,
37 +) -> str:
38 """
39 Construct the alert source link for the alert details.
40 Parameters
@@ -51,7 +47,10 @@ async def construct_alert_source_link(alert_details: Office365ThreatIntelAlertRe
47 The alert source link.
48 """
49 grafana_url = (
54 - await get_customer_alert_settings_office365(office365_organization_id=alert_details.data_office365_OrganizationId, session=session)
50 + await get_customer_alert_settings_office365(
51 + office365_organization_id=alert_details.data_office365_OrganizationId,
52 + session=session,
53 + )
54 ).grafana_url
55
56 return (
@@ -63,7 +62,9 @@ async def construct_alert_source_link(alert_details: Office365ThreatIntelAlertRe
62 )
63
64
66 -async def build_ioc_payload(alert_details: Office365ThreatIntelAlertRequest) -> Optional[IrisIoc]:
65 +async def build_ioc_payload(
66 + alert_details: Office365ThreatIntelAlertRequest,
67 +) -> Optional[IrisIoc]:
68 """
69 Builds an IoC payload based on the provided alert details.
70
@@ -86,7 +87,9 @@ async def build_ioc_payload(alert_details: Office365ThreatIntelAlertRequest) ->
87 return None
88
89
89 -async def build_asset_payload(alert_details: Office365ThreatIntelAlertRequest) -> IrisAsset:
90 +async def build_asset_payload(
91 + alert_details: Office365ThreatIntelAlertRequest,
92 +) -> IrisAsset:
93 if alert_details.data_office365_UserId:
94 return IrisAsset(
95 asset_name=alert_details.data_office365_UserId,
@@ -164,7 +167,10 @@ async def build_alert_payload(
167 IrisAlertPayload: The built alert payload.
168 """
169 asset_payload = await build_asset_payload(alert_details)
167 - context_payload = await build_alert_context_payload(alert_details=alert_details, session=session)
170 + context_payload = await build_alert_context_payload(
171 + alert_details=alert_details,
172 + session=session,
173 + )
174 timefield = "timestamp_utc"
175 # Get the timefield value from the alert_details
176 if hasattr(alert_details, timefield):
@@ -174,7 +180,10 @@ async def build_alert_payload(
180 logger.info(f"Alert has IoC: {ioc_payload}")
181 return IrisAlertPayload(
182 alert_title=alert_details.data_office365_Operation,
177 - alert_source_link=await construct_alert_source_link(alert_details, session=session),
183 + alert_source_link=await construct_alert_source_link(
184 + alert_details,
185 + session=session,
186 + ),
187 alert_description=alert_details.rule_description,
188 alert_source="Office365 Threat Intel Rule",
189 assets=[asset_payload],
@@ -195,7 +204,10 @@ async def build_alert_payload(
204 logger.info("Alert does not have IoC")
205 return IrisAlertPayload(
206 alert_title=alert_details.data_office365_Operation,
198 - alert_source_link=await construct_alert_source_link(alert_details, session=session),
207 + alert_source_link=await construct_alert_source_link(
208 + alert_details,
209 + session=session,
210 + ),
211 alert_description=alert_details.rule_description,
212 alert_source="Office365 Threat Intel Rule",
213 assets=[asset_payload],
@@ -213,7 +225,10 @@ async def build_alert_payload(
225 )
226
227
216 -async def create_threat_intel_alert(alert: Office365ThreatIntelAlertRequest, session: AsyncSession) -> Office365ThreatIntelAlertResponse:
228 +async def create_threat_intel_alert(
229 + alert: Office365ThreatIntelAlertRequest,
230 + session: AsyncSession,
231 +) -> Office365ThreatIntelAlertResponse:
232 """
233 Creates an Office365 Threat Intel alert in IRIS.
234
@@ -259,10 +274,16 @@ async def create_threat_intel_alert(alert: Office365ThreatIntelAlertRequest, ses
274 ShufflePayload(
275 alert_id=alert_id,
276 customer=(
262 - await get_customer_alert_settings_office365(office365_organization_id=alert.data_office365_OrganizationId, session=session)
277 + await get_customer_alert_settings_office365(
278 + office365_organization_id=alert.data_office365_OrganizationId,
279 + session=session,
280 + )
281 ).customer_name,
282 customer_code=(
265 - await get_customer_alert_settings_office365(office365_organization_id=alert.data_office365_OrganizationId, session=session)
283 + await get_customer_alert_settings_office365(
284 + office365_organization_id=alert.data_office365_OrganizationId,
285 + session=session,
286 + )
287 ).customer_code,
288 alert_source_link=await construct_alert_source_link(alert, session=session),
289 rule_description=alert.rule_description,
@@ -273,7 +294,10 @@ async def create_threat_intel_alert(alert: Office365ThreatIntelAlertRequest, ses
294 return Office365ThreatIntelAlertResponse(
295 alert_id=alert_id,
296 customer=(
276 - await get_customer_alert_settings_office365(office365_organization_id=alert.data_office365_OrganizationId, session=session)
297 + await get_customer_alert_settings_office365(
298 + office365_organization_id=alert.data_office365_OrganizationId,
299 + session=session,
300 + )
301 ).customer_name,
302 alert_source_link=await construct_alert_source_link(alert, session=session),
303 success=True,
backend/app/integrations/alert_creation_settings/models/alert_creation_settings.py
+27 -12
@@ -1,15 +1,15 @@
1 -from typing import List
2 -from typing import Optional
1 +from typing import List, Optional
2
4 -from sqlmodel import Field
5 -from sqlmodel import Relationship
6 -from sqlmodel import SQLModel
3 +from sqlmodel import Field, Relationship, SQLModel
4
5
6 class Condition(SQLModel, table=True):
7 __tablename__ = "custom_alert_creation_condition"
8 id: int = Field(default=None, primary_key=True)
12 - event_order_id: int = Field(default=None, foreign_key="custom_alert_creation_event_order.id")
9 + event_order_id: int = Field(
10 + default=None,
11 + foreign_key="custom_alert_creation_event_order.id",
12 + )
13 field_name: str = Field(max_length=1024)
14 field_value: str = Field(max_length=1024)
15 event_order: "EventOrder" = Relationship(back_populates="conditions")
@@ -18,11 +18,18 @@ class Condition(SQLModel, table=True):
18 class EventOrder(SQLModel, table=True):
19 __tablename__ = "custom_alert_creation_event_order"
20 id: int = Field(default=None, primary_key=True)
21 - alert_creation_settings_id: int = Field(default=None, foreign_key="custom_alert_creation_settings.id")
21 + alert_creation_settings_id: int = Field(
22 + default=None,
23 + foreign_key="custom_alert_creation_settings.id",
24 + )
25 order_label: str = Field(max_length=255)
26 conditions: List["Condition"] = Relationship(back_populates="event_order")
24 - alert_creation_settings: "AlertCreationSettings" = Relationship(back_populates="event_orders")
25 - event_configs: List["AlertCreationEventConfig"] = Relationship(back_populates="event_order")
27 + alert_creation_settings: "AlertCreationSettings" = Relationship(
28 + back_populates="event_orders",
29 + )
30 + event_configs: List["AlertCreationEventConfig"] = Relationship(
31 + back_populates="event_order",
32 + )
33
34
35 class AlertCreationSettings(SQLModel, table=True):
@@ -42,14 +49,22 @@ class AlertCreationSettings(SQLModel, table=True):
49 opencti_url: Optional[str] = Field(max_length=1024)
50 custom_message: Optional[str] = Field(max_length=1024)
51 shuffle_endpoint: Optional[str] = Field(max_length=1024)
45 - nvd_url: Optional[str] = Field(default="https://services.nvd.nist.gov/rest/json/cves/2.0?cveId", max_length=1024)
46 - event_orders: List[EventOrder] = Relationship(back_populates="alert_creation_settings")
52 + nvd_url: Optional[str] = Field(
53 + default="https://services.nvd.nist.gov/rest/json/cves/2.0?cveId",
54 + max_length=1024,
55 + )
56 + event_orders: List[EventOrder] = Relationship(
57 + back_populates="alert_creation_settings",
58 + )
59
60
61 class AlertCreationEventConfig(SQLModel, table=True):
62 __tablename__ = "custom_alert_creation_event_config"
63 id: Optional[int] = Field(default=None, primary_key=True)
52 - event_order_id: Optional[int] = Field(default=None, foreign_key="custom_alert_creation_event_order.id")
64 + event_order_id: Optional[int] = Field(
65 + default=None,
66 + foreign_key="custom_alert_creation_event_order.id",
67 + )
68 event_id: str = Field(max_length=255)
69 field: str = Field(max_length=1024)
70 value: str = Field(max_length=1024)
backend/app/integrations/alert_creation_settings/routes/alert_creation_settings.py
+107 -45
@@ -1,39 +1,24 @@
1 from typing import List
2
3 -from fastapi import APIRouter
4 -from fastapi import Depends
5 -from fastapi import HTTPException
6 -from loguru import logger
7 -from sqlalchemy.ext.asyncio import AsyncSession
8 -from sqlalchemy.future import select
9 -from sqlalchemy.orm import joinedload
10 -
3 from app.db.db_session import get_db
4 from app.integrations.alert_creation_settings.models.alert_creation_settings import (
5 AlertCreationEventConfig,
14 -)
15 -from app.integrations.alert_creation_settings.models.alert_creation_settings import (
6 AlertCreationSettings,
17 -)
18 -from app.integrations.alert_creation_settings.models.alert_creation_settings import (
7 EventOrder,
8 )
9 from app.integrations.alert_creation_settings.schema.alert_creation_settings import (
10 AlertCreationEventConfigResponse,
23 -)
24 -from app.integrations.alert_creation_settings.schema.alert_creation_settings import (
11 AlertCreationSettingsCreate,
26 -)
27 -from app.integrations.alert_creation_settings.schema.alert_creation_settings import (
12 AlertCreationSettingsResponse,
29 -)
30 -from app.integrations.alert_creation_settings.schema.alert_creation_settings import (
13 EventOrderCreate,
32 -)
33 -from app.integrations.alert_creation_settings.schema.alert_creation_settings import (
14 EventOrderResponse,
15 )
16 from app.utils import get_customer_alert_event_configs
17 +from fastapi import APIRouter, Depends, HTTPException
18 +from loguru import logger
19 +from sqlalchemy.ext.asyncio import AsyncSession
20 +from sqlalchemy.future import select
21 +from sqlalchemy.orm import joinedload
22
23 alert_creation_settings_router = APIRouter()
24
@@ -63,7 +48,10 @@ async def get_customer_event_configs(
48 event_configs = await get_customer_alert_event_configs(customer_code, session)
49
50 if not event_configs:
66 - raise HTTPException(status_code=404, detail="No event configs found for this customer.")
51 + raise HTTPException(
52 + status_code=404,
53 + detail="No event configs found for this customer.",
54 + )
55
56 return event_configs
57
@@ -90,22 +78,38 @@ async def create_alert_creation_settings(
78 logger.info(f"alert_creation_settings: {alert_creation_settings.dict()}")
79
80 result = await session.execute(
93 - select(AlertCreationSettings).where(AlertCreationSettings.customer_code == alert_creation_settings.customer_code),
81 + select(AlertCreationSettings).where(
82 + AlertCreationSettings.customer_code
83 + == alert_creation_settings.customer_code,
84 + ),
85 )
86 settings = result.scalars().first()
87
88 if settings:
98 - logger.info(f"Alert creation settings already exist for customer_code: {alert_creation_settings.customer_code}")
99 - raise HTTPException(status_code=200, detail="Alert creation settings already exist.")
100 -
101 - alert_creation_settings_db = AlertCreationSettings(**alert_creation_settings.dict(exclude={"event_orders"}))
89 + logger.info(
90 + f"Alert creation settings already exist for customer_code: {alert_creation_settings.customer_code}",
91 + )
92 + raise HTTPException(
93 + status_code=200,
94 + detail="Alert creation settings already exist.",
95 + )
96 +
97 + alert_creation_settings_db = AlertCreationSettings(
98 + **alert_creation_settings.dict(exclude={"event_orders"}),
99 + )
100
101 if alert_creation_settings.event_orders is not None:
102 for event_order in alert_creation_settings.event_orders:
105 - event_order_db = EventOrder(order_label=event_order.order_label, alert_creation_settings=alert_creation_settings_db)
103 + event_order_db = EventOrder(
104 + order_label=event_order.order_label,
105 + alert_creation_settings=alert_creation_settings_db,
106 + )
107 session.add(event_order_db)
108 for event_config in event_order.event_configs:
108 - event_config_db = AlertCreationEventConfig(**event_config.dict(), event_order=event_order_db)
109 + event_config_db = AlertCreationEventConfig(
110 + **event_config.dict(),
111 + event_order=event_order_db,
112 + )
113 session.add(event_config_db)
114
115 session.add(alert_creation_settings_db)
@@ -138,13 +142,20 @@ async def get_alert_creation_settings(
142 """
143 result = await session.execute(
144 select(AlertCreationSettings)
141 - .options(joinedload(AlertCreationSettings.event_orders).joinedload(EventOrder.event_configs))
145 + .options(
146 + joinedload(AlertCreationSettings.event_orders).joinedload(
147 + EventOrder.event_configs,
148 + ),
149 + )
150 .where(AlertCreationSettings.customer_name == customer_name),
151 )
152 settings = result.scalars().first()
153
154 if not settings:
147 - raise HTTPException(status_code=404, detail="Alert creation settings not found.")
155 + raise HTTPException(
156 + status_code=404,
157 + detail="Alert creation settings not found.",
158 + )
159
160 return settings
161
@@ -172,26 +183,41 @@ async def add_event_order(
183 """
184 result = await session.execute(
185 select(AlertCreationSettings)
175 - .options(joinedload(AlertCreationSettings.event_orders).joinedload(EventOrder.event_configs))
186 + .options(
187 + joinedload(AlertCreationSettings.event_orders).joinedload(
188 + EventOrder.event_configs,
189 + ),
190 + )
191 .where(AlertCreationSettings.customer_name == customer_name),
192 )
193 settings = result.scalars().first()
194
195 if not settings:
181 - raise HTTPException(status_code=404, detail="Alert creation settings not found.")
196 + raise HTTPException(
197 + status_code=404,
198 + detail="Alert creation settings not found.",
199 + )
200
201 # Create new event order and configs
184 - event_order_db = EventOrder(order_label=event_order.order_label, alert_creation_settings=settings)
202 + event_order_db = EventOrder(
203 + order_label=event_order.order_label,
204 + alert_creation_settings=settings,
205 + )
206 session.add(event_order_db)
207 for event_config in event_order.event_configs:
187 - event_config_db = AlertCreationEventConfig(**event_config.dict(), event_order=event_order_db)
208 + event_config_db = AlertCreationEventConfig(
209 + **event_config.dict(),
210 + event_order=event_order_db,
211 + )
212 session.add(event_config_db)
213
214 await session.commit()
215
216 # Query the EventOrder instance again to ensure event_configs are loaded
217 result = await session.execute(
194 - select(EventOrder).options(joinedload(EventOrder.event_configs)).where(EventOrder.id == event_order_db.id),
218 + select(EventOrder)
219 + .options(joinedload(EventOrder.event_configs))
220 + .where(EventOrder.id == event_order_db.id),
221 )
222 event_order_db = result.scalars().first()
223
@@ -221,27 +247,47 @@ async def update_event_orders(
247 """
248 result = await session.execute(
249 select(AlertCreationSettings)
224 - .options(joinedload(AlertCreationSettings.event_orders).joinedload(EventOrder.event_configs))
250 + .options(
251 + joinedload(AlertCreationSettings.event_orders).joinedload(
252 + EventOrder.event_configs,
253 + ),
254 + )
255 .where(AlertCreationSettings.customer_name == customer_name),
256 )
257 settings = result.scalars().first()
258
259 if not settings:
230 - raise HTTPException(status_code=404, detail="Alert creation settings not found.")
260 + raise HTTPException(
261 + status_code=404,
262 + detail="Alert creation settings not found.",
263 + )
264
265 # Create new event orders and configs or add to existing ones
266 for event_order in event_orders:
267 # Check if an EventOrder with the given order_label already exists
235 - existing_order = next((order for order in settings.event_orders if order.order_label == event_order.order_label), None)
268 + existing_order = next(
269 + (
270 + order
271 + for order in settings.event_orders
272 + if order.order_label == event_order.order_label
273 + ),
274 + None,
275 + )
276
277 if existing_order:
278 # If it does, add the new EventConfig instances to it
279 for event_config in event_order.event_configs:
240 - event_config_db = AlertCreationEventConfig(**event_config.dict(), event_order=existing_order)
280 + event_config_db = AlertCreationEventConfig(
281 + **event_config.dict(),
282 + event_order=existing_order,
283 + )
284 session.add(event_config_db)
285 else:
286 # If it doesn't, return a 404
244 - raise HTTPException(status_code=404, detail=f"Event order with order_label: {event_order.order_label} not found.")
287 + raise HTTPException(
288 + status_code=404,
289 + detail=f"Event order with order_label: {event_order.order_label} not found.",
290 + )
291
292 await session.commit()
293 await session.refresh(settings)
@@ -271,16 +317,26 @@ async def delete_event_order(
317 """
318 result = await session.execute(
319 select(AlertCreationSettings)
274 - .options(joinedload(AlertCreationSettings.event_orders).joinedload(EventOrder.event_configs))
320 + .options(
321 + joinedload(AlertCreationSettings.event_orders).joinedload(
322 + EventOrder.event_configs,
323 + ),
324 + )
325 .where(AlertCreationSettings.customer_name == customer_name),
326 )
327 settings = result.scalars().first()
328
329 if not settings:
280 - raise HTTPException(status_code=404, detail="Alert creation settings not found.")
330 + raise HTTPException(
331 + status_code=404,
332 + detail="Alert creation settings not found.",
333 + )
334
335 # Check if an EventOrder with the given order_label exists
283 - existing_order = next((order for order in settings.event_orders if order.order_label == order_label), None)
336 + existing_order = next(
337 + (order for order in settings.event_orders if order.order_label == order_label),
338 + None,
339 + )
340
341 if existing_order:
342 # If it does, delete its AlertCreationEventConfig instances
@@ -291,8 +347,14 @@ async def delete_event_order(
347 await session.delete(existing_order)
348 else:
349 # If it doesn't, return a 404
294 - raise HTTPException(status_code=404, detail=f"Event order with order_label: {order_label} not found.")
350 + raise HTTPException(
351 + status_code=404,
352 + detail=f"Event order with order_label: {order_label} not found.",
353 + )
354
355 await session.commit()
356
298 - return {"message": f"Event order with order_label: {order_label} and related alert creation event configs deleted.", "success": True}
357 + return {
358 + "message": f"Event order with order_label: {order_label} and related alert creation event configs deleted.",
359 + "success": True,
360 + }
backend/app/integrations/alert_creation_settings/schema/alert_creation_settings.py
+1 -2
@@ -1,5 +1,4 @@
1 -from typing import List
2 -from typing import Optional
1 +from typing import List, Optional
2
3 from pydantic import BaseModel
4
backend/app/integrations/alert_escalation/routes/general_alert.py
+11 -9
@@ -1,14 +1,13 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from fastapi import Security
4 -from loguru import logger
5 -from sqlalchemy.ext.asyncio import AsyncSession
6 -
1 from app.auth.utils import AuthHandler
2 from app.db.db_session import get_db
9 -from app.integrations.alert_escalation.schema.general_alert import CreateAlertRequest
10 -from app.integrations.alert_escalation.schema.general_alert import CreateAlertResponse
3 +from app.integrations.alert_escalation.schema.general_alert import (
4 + CreateAlertRequest,
5 + CreateAlertResponse,
6 +)
7 from app.integrations.alert_escalation.services.general_alert import create_alert
8 +from fastapi import APIRouter, Depends, Security
9 +from loguru import logger
10 +from sqlalchemy.ext.asyncio import AsyncSession
11
12 integration_general_alerts_router = APIRouter()
13
@@ -19,7 +18,10 @@ integration_general_alerts_router = APIRouter()
18 description="Manually create an alert in IRIS from Copilot WebUI",
19 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
20 )
22 -async def create_alert_route(create_alert_request: CreateAlertRequest, session: AsyncSession = Depends(get_db)) -> CreateAlertResponse:
21 +async def create_alert_route(
22 + create_alert_request: CreateAlertRequest,
23 + session: AsyncSession = Depends(get_db),
24 +) -> CreateAlertResponse:
25 """
26 Create an alert in IRIS. Manually create an alert in IRIS from Copilot WebUI.
27
backend/app/integrations/alert_escalation/schema/general_alert.py
+100 -28
@@ -1,12 +1,7 @@
1 from enum import Enum
2 -from typing import Any
3 -from typing import Dict
4 -from typing import List
5 -from typing import Optional
2 +from typing import Any, Dict, List, Optional
3
7 -from pydantic import BaseModel
8 -from pydantic import Extra
9 -from pydantic import Field
4 +from pydantic import BaseModel, Extra, Field
5
6
7 class ValidIocFields(Enum):
@@ -16,7 +11,10 @@ class ValidIocFields(Enum):
11
12
13 class CreateAlertRequest(BaseModel):
19 - index_name: str = Field(..., description="The name of the index to search alerts for.")
14 + index_name: str = Field(
15 + ...,
16 + description="The name of the index to search alerts for.",
17 + )
18 alert_id: str = Field(..., description="The alert id.")
19
20
@@ -35,7 +33,10 @@ class GenericSourceModel(BaseModel):
33 rule_level: int = Field(..., description="The level of the rule.")
34 rule_description: str = Field(..., description="The description of the rule.")
35 timestamp: str = Field(..., description="The timestamp of the alert.")
38 - timestamp_utc: Optional[str] = Field(..., description="The UTC timestamp of the alert.")
36 + timestamp_utc: Optional[str] = Field(
37 + ...,
38 + description="The UTC timestamp of the alert.",
39 + )
40 process_id: Optional[str] = Field(None, description="The process id of the alert.")
41
42 class Config:
@@ -51,8 +52,14 @@ class GenericAlertModel(BaseModel):
52 None,
53 description="The asset type id of the alert which is needed for when we add the asset to IRIS.",
54 )
54 - ioc_value: Optional[str] = Field(None, description="The IoC value of the alert which is needed for when we add the IoC to IRIS.")
55 - ioc_type: Optional[str] = Field(None, description="The IoC type of the alert which is needed for when we add the IoC to IRIS.")
55 + ioc_value: Optional[str] = Field(
56 + None,
57 + description="The IoC value of the alert which is needed for when we add the IoC to IRIS.",
58 + )
59 + ioc_type: Optional[str] = Field(
60 + None,
61 + description="The IoC type of the alert which is needed for when we add the IoC to IRIS.",
62 + )
63 time_field: Optional[str] = Field(
64 "timestamp",
65 description="The timefield of the alert to be used when creating the IRIS alert.",
@@ -79,8 +86,16 @@ sample_data = {
86 ########### Create Alerts Schemas ###########
87 class IrisAsset(BaseModel):
88 asset_name: str = Field(..., description="Name of the asset", example="Server01")
82 - asset_ip: str = Field(..., description="IP address of the asset", example="192.168.1.1")
83 - asset_description: str = Field(..., description="Description of the asset", example="Windows Server")
89 + asset_ip: str = Field(
90 + ...,
91 + description="IP address of the asset",
92 + example="192.168.1.1",
93 + )
94 + asset_description: str = Field(
95 + ...,
96 + description="Description of the asset",
97 + example="Windows Server",
98 + )
99 asset_type_id: int = Field(..., description="Type ID of the asset", example=1)
100 asset_tags: Optional[str] = Field(
101 "Agent ID not found. Ensure the agent has been registered with Wazuh Manager and synced to the Agents table.",
@@ -112,29 +127,86 @@ class IrisIoc(BaseModel):
127
128 class IrisAlertContext(BaseModel):
129 alert_id: str = Field(..., description="ID of the alert", example="123")
115 - alert_name: str = Field(..., description="Name of the alert", example="Intrusion Detected")
130 + alert_name: str = Field(
131 + ...,
132 + description="Name of the alert",
133 + example="Intrusion Detected",
134 + )
135 alert_level: int = Field(..., description="Severity level of the alert", example=3)
117 - rule_id: str = Field(..., description="ID of the rule that triggered the alert", example="2001")
118 - asset_name: str = Field(..., description="Name of the affected asset", example="Server01")
119 - asset_ip: str = Field(..., description="IP address of the affected asset", example="192.168.1.1")
136 + rule_id: str = Field(
137 + ...,
138 + description="ID of the rule that triggered the alert",
139 + example="2001",
140 + )
141 + asset_name: str = Field(
142 + ...,
143 + description="Name of the affected asset",
144 + example="Server01",
145 + )
146 + asset_ip: str = Field(
147 + ...,
148 + description="IP address of the affected asset",
149 + example="192.168.1.1",
150 + )
151 asset_type: int = Field(..., description="Type ID of the affected asset", example=1)
121 - process_id: Optional[str] = Field("No process ID found", description="Process ID involved in the alert", example="4567")
122 - rule_mitre_id: Optional[str] = Field("n/a", description="MITRE ATT&CK ID of the rule", example="T1234")
123 - rule_mitre_tactic: Optional[str] = Field("n/a", description="MITRE ATT&CK Tactic", example="Execution")
124 - rule_mitre_technique: Optional[str] = Field("n/a", description="MITRE ATT&CK Technique", example="Scripting")
152 + process_id: Optional[str] = Field(
153 + "No process ID found",
154 + description="Process ID involved in the alert",
155 + example="4567",
156 + )
157 + rule_mitre_id: Optional[str] = Field(
158 + "n/a",
159 + description="MITRE ATT&CK ID of the rule",
160 + example="T1234",
161 + )
162 + rule_mitre_tactic: Optional[str] = Field(
163 + "n/a",
164 + description="MITRE ATT&CK Tactic",
165 + example="Execution",
166 + )
167 + rule_mitre_technique: Optional[str] = Field(
168 + "n/a",
169 + description="MITRE ATT&CK Technique",
170 + example="Scripting",
171 + )
172
173
174 class IrisAlertPayload(BaseModel):
128 - alert_title: str = Field(..., description="Title of the alert", example="Intrusion Detected")
129 - alert_description: str = Field(..., description="Description of the alert", example="Intrusion Detected by Firewall")
175 + alert_title: str = Field(
176 + ...,
177 + description="Title of the alert",
178 + example="Intrusion Detected",
179 + )
180 + alert_description: str = Field(
181 + ...,
182 + description="Description of the alert",
183 + example="Intrusion Detected by Firewall",
184 + )
185 alert_source: str = Field(..., description="Source of the alert", example="Wazuh")
186 assets: List[IrisAsset] = Field(..., description="List of affected assets")
187 alert_status_id: int = Field(..., description="Status ID of the alert", example=3)
133 - alert_severity_id: int = Field(..., description="Severity ID of the alert", example=5)
134 - alert_customer_id: int = Field(..., description="Customer ID related to the alert", example=1)
135 - alert_source_content: Dict[str, Any] = Field(..., description="Original content from the alert source")
136 - alert_context: IrisAlertContext = Field(..., description="Contextual information about the alert")
137 - alert_iocs: Optional[List[IrisIoc]] = Field(None, description="List of IoCs related to the alert")
188 + alert_severity_id: int = Field(
189 + ...,
190 + description="Severity ID of the alert",
191 + example=5,
192 + )
193 + alert_customer_id: int = Field(
194 + ...,
195 + description="Customer ID related to the alert",
196 + example=1,
197 + )
198 + alert_source_content: Dict[str, Any] = Field(
199 + ...,
200 + description="Original content from the alert source",
201 + )
202 + alert_context: IrisAlertContext = Field(
203 + ...,
204 + description="Contextual information about the alert",
205 + )
206 + alert_iocs: Optional[List[IrisIoc]] = Field(
207 + None,
208 + description="List of IoCs related to the alert",
209 + )
210
211 def to_dict(self):
212 return self.dict(exclude_none=True)
backend/app/integrations/alert_escalation/services/general_alert.py
+195 -60
@@ -1,33 +1,34 @@
1 -from typing import Optional
2 -from typing import Set
3 -
4 -from fastapi import HTTPException
5 -from loguru import logger
6 -from sqlalchemy.ext.asyncio import AsyncSession
7 -from sqlalchemy.future import select
1 +from typing import Optional, Set
2
3 # from app.integrations.alert_escalation.utils.universal import get_agent_data
4 from app.agents.routes.agents import get_agent
5 from app.agents.schema.agents import AgentsResponse
12 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
13 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
6 +from app.connectors.dfir_iris.utils.universal import (
7 + fetch_and_validate_data,
8 + initialize_client_and_alert,
9 +)
10 from app.connectors.utils import get_connector_info_from_db
11 from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
12 from app.integrations.alert_creation_settings.models.alert_creation_settings import (
13 AlertCreationSettings,
14 )
19 -from app.integrations.alert_escalation.schema.general_alert import CreateAlertRequest
20 -from app.integrations.alert_escalation.schema.general_alert import CreateAlertResponse
21 -from app.integrations.alert_escalation.schema.general_alert import GenericAlertModel
22 -from app.integrations.alert_escalation.schema.general_alert import GenericSourceModel
23 -from app.integrations.alert_escalation.schema.general_alert import IrisAlertContext
24 -from app.integrations.alert_escalation.schema.general_alert import IrisAlertPayload
25 -from app.integrations.alert_escalation.schema.general_alert import IrisAsset
26 -from app.integrations.alert_escalation.schema.general_alert import IrisIoc
27 -from app.integrations.alert_escalation.schema.general_alert import ValidIocFields
28 -from app.integrations.utils.alerts import get_asset_type_id
29 -from app.integrations.utils.alerts import validate_ioc_type
15 +from app.integrations.alert_escalation.schema.general_alert import (
16 + CreateAlertRequest,
17 + CreateAlertResponse,
18 + GenericAlertModel,
19 + GenericSourceModel,
20 + IrisAlertContext,
21 + IrisAlertPayload,
22 + IrisAsset,
23 + IrisIoc,
24 + ValidIocFields,
25 +)
26 +from app.integrations.utils.alerts import get_asset_type_id, validate_ioc_type
27 from app.utils import get_customer_alert_settings
28 +from fastapi import HTTPException
29 +from loguru import logger
30 +from sqlalchemy.ext.asyncio import AsyncSession
31 +from sqlalchemy.future import select
32
33
34 async def is_customer_code_valid(customer_code: str, session: AsyncSession) -> bool:
@@ -44,7 +45,9 @@ async def is_customer_code_valid(customer_code: str, session: AsyncSession) -> b
45 logger.info(f"Checking if customer_code: {customer_code} is valid.")
46
47 result = await session.execute(
47 - select(AlertCreationSettings).where(AlertCreationSettings.customer_code == customer_code),
48 + select(AlertCreationSettings).where(
49 + AlertCreationSettings.customer_code == customer_code,
50 + ),
51 )
52 settings = result.scalars().first()
53 logger.info(f"Settings: {settings}")
@@ -66,7 +69,10 @@ def valid_ioc_fields() -> Set[str]:
69 return {field.value for field in ValidIocFields}
70
71
69 -async def construct_alert_source_link(alert_details: GenericAlertModel, session: AsyncSession) -> str:
72 +async def construct_alert_source_link(
73 + alert_details: GenericAlertModel,
74 + session: AsyncSession,
75 +) -> str:
76 """
77 Construct the alert source link for the alert details.
78 Parameters
@@ -79,13 +85,19 @@ async def construct_alert_source_link(alert_details: GenericAlertModel, session:
85 The alert source link.
86 """
87 # Check if the alert has a process id and that it is not "No process ID found"
82 - if hasattr(alert_details, "process_id") and alert_details._source.process_id != "No process ID found":
88 + if (
89 + hasattr(alert_details, "process_id")
90 + and alert_details._source.process_id != "No process ID found"
91 + ):
92 query_string = f"%22query%22:%22process_id:%5C%22{alert_details._source.process_id}%5C%22%20AND%20"
93 else:
94 query_string = f"%22query%22:%22_id:%5C%22{alert_details._id}%5C%22%20AND%20"
95
96 grafana_url = (
88 - await get_customer_alert_settings(customer_code=alert_details._source.agent_labels_customer, session=session)
97 + await get_customer_alert_settings(
98 + customer_code=alert_details._source.agent_labels_customer,
99 + session=session,
100 + )
101 ).grafana_url
102
103 return (
@@ -97,7 +109,9 @@ async def construct_alert_source_link(alert_details: GenericAlertModel, session:
109 )
110
111
100 -async def get_single_alert_details(alert_details: CreateAlertRequest) -> GenericAlertModel:
112 +async def get_single_alert_details(
113 + alert_details: CreateAlertRequest,
114 +) -> GenericAlertModel:
115 """
116 Fetches the details of a single alert.
117
@@ -110,15 +124,25 @@ async def get_single_alert_details(alert_details: CreateAlertRequest) -> Generic
124 Raises:
125 HTTPException: If there is an error while fetching the alert details.
126 """
113 - logger.info(f"Fetching alert details for alert {alert_details.alert_id} in index {alert_details.index_name}")
127 + logger.info(
128 + f"Fetching alert details for alert {alert_details.alert_id} in index {alert_details.index_name}",
129 + )
130 es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
131 try:
132 alert = es_client.get(index=alert_details.index_name, id=alert_details.alert_id)
133 source_model = GenericSourceModel(**alert["_source"])
118 - return GenericAlertModel(_source=source_model, _id=alert["_id"], _index=alert["_index"], _version=alert["_version"])
134 + return GenericAlertModel(
135 + _source=source_model,
136 + _id=alert["_id"],
137 + _index=alert["_index"],
138 + _version=alert["_version"],
139 + )
140 except Exception as e:
141 logger.debug(f"Failed to collect alert details: {e}")
121 - raise HTTPException(status_code=400, detail=f"Failed to collect alert details: {e}")
142 + raise HTTPException(
143 + status_code=400,
144 + detail=f"Failed to collect alert details: {e}",
145 + )
146
147
148 async def build_ioc_payload(alert_details: GenericAlertModel) -> Optional[IrisIoc]:
@@ -135,7 +159,12 @@ async def build_ioc_payload(alert_details: GenericAlertModel) -> Optional[IrisIo
159 if hasattr(alert_details._source, field):
160 ioc_value = getattr(alert_details._source, field)
161 ioc_type = await validate_ioc_type(ioc_value=ioc_value)
138 - return IrisIoc(ioc_value=ioc_value, ioc_description="IoC found in alert", ioc_tlp_id=1, ioc_type_id=ioc_type)
162 + return IrisIoc(
163 + ioc_value=ioc_value,
164 + ioc_description="IoC found in alert",
165 + ioc_tlp_id=1,
166 + ioc_type_id=ioc_type,
167 + )
168 return None
169
170
@@ -179,13 +208,22 @@ async def build_alert_context_payload(
208 """
209 return IrisAlertContext(
210 customer_iris_id=(
182 - await get_customer_alert_settings(customer_code=alert_details._source.agent_labels_customer, session=session)
211 + await get_customer_alert_settings(
212 + customer_code=alert_details._source.agent_labels_customer,
213 + session=session,
214 + )
215 ).iris_customer_id,
216 customer_name=(
185 - await get_customer_alert_settings(customer_code=alert_details._source.agent_labels_customer, session=session)
217 + await get_customer_alert_settings(
218 + customer_code=alert_details._source.agent_labels_customer,
219 + session=session,
220 + )
221 ).customer_name,
222 customer_cases_index=(
188 - await get_customer_alert_settings(customer_code=alert_details._source.agent_labels_customer, session=session)
223 + await get_customer_alert_settings(
224 + customer_code=alert_details._source.agent_labels_customer,
225 + session=session,
226 + )
227 ).iris_index,
228 alert_id=alert_details._id,
229 alert_name=alert_details._source.rule_description,
@@ -195,9 +233,21 @@ async def build_alert_context_payload(
233 asset_ip=agent_data.agents[0].ip_address,
234 asset_type=await get_asset_type_id(agent_data.agents[0].os),
235 process_id=getattr(alert_details._source, "process_id", "No process id found"),
198 - rule_mitre_id=getattr(alert_details._source, "rule_mitre_id", "No rule mitre id found"),
199 - rule_mitre_tactic=getattr(alert_details._source, "rule_mitre_tactic", "No rule mitre tactic found"),
200 - rule_mitre_technique=getattr(alert_details._source, "rule_mitre_technique", "No rule mitre technique found"),
236 + rule_mitre_id=getattr(
237 + alert_details._source,
238 + "rule_mitre_id",
239 + "No rule mitre id found",
240 + ),
241 + rule_mitre_tactic=getattr(
242 + alert_details._source,
243 + "rule_mitre_tactic",
244 + "No rule mitre tactic found",
245 + ),
246 + rule_mitre_technique=getattr(
247 + alert_details._source,
248 + "rule_mitre_technique",
249 + "No rule mitre technique found",
250 + ),
251 )
252
253
@@ -223,8 +273,17 @@ async def build_alert_payload(
273 HTTPException: If there is an error while building the alert payload.
274 """
275 asset_payload = await build_asset_payload(agent_data, alert_details)
226 - context_payload = await build_alert_context_payload(alert_details=alert_details, agent_data=agent_data, session=session)
227 - timefield = (await get_customer_alert_settings(customer_code=alert_details._source.agent_labels_customer, session=session)).timefield
276 + context_payload = await build_alert_context_payload(
277 + alert_details=alert_details,
278 + agent_data=agent_data,
279 + session=session,
280 + )
281 + timefield = (
282 + await get_customer_alert_settings(
283 + customer_code=alert_details._source.agent_labels_customer,
284 + session=session,
285 + )
286 + ).timefield
287 # Get the timefield value from the alert_details
288 if hasattr(alert_details, timefield):
289 alert_details.time_field = getattr(alert_details, timefield)
@@ -234,14 +293,20 @@ async def build_alert_payload(
293 logger.info(f"Alert has IoC: {ioc_payload}")
294 return IrisAlertPayload(
295 alert_title=alert_details._source.rule_description,
237 - alert_source_link=await construct_alert_source_link(alert_details, session=session),
296 + alert_source_link=await construct_alert_source_link(
297 + alert_details,
298 + session=session,
299 + ),
300 alert_description=alert_details._source.rule_description,
301 alert_source="CoPilot",
302 assets=[asset_payload],
303 alert_status_id=3,
304 alert_severity_id=5,
305 alert_customer_id=(
244 - await get_customer_alert_settings(customer_code=alert_details._source.agent_labels_customer, session=session)
306 + await get_customer_alert_settings(
307 + customer_code=alert_details._source.agent_labels_customer,
308 + session=session,
309 + )
310 ).iris_customer_id,
311 alert_source_content=alert_details._source,
312 alert_context=context_payload,
@@ -252,14 +317,20 @@ async def build_alert_payload(
317 logger.info("Alert does not have IoC")
318 return IrisAlertPayload(
319 alert_title=alert_details._source.rule_description,
255 - alert_source_link=await construct_alert_source_link(alert_details, session=session),
320 + alert_source_link=await construct_alert_source_link(
321 + alert_details,
322 + session=session,
323 + ),
324 alert_description=alert_details._source.rule_description,
325 alert_source="CoPilot",
326 assets=[asset_payload],
327 alert_status_id=3,
328 alert_severity_id=5,
329 alert_customer_id=(
262 - await get_customer_alert_settings(customer_code=alert_details._source.agent_labels_customer, session=session)
330 + await get_customer_alert_settings(
331 + customer_code=alert_details._source.agent_labels_customer,
332 + session=session,
333 + )
334 ).iris_customer_id,
335 alert_source_content=alert_details._source,
336 alert_context=context_payload,
@@ -267,7 +338,10 @@ async def build_alert_payload(
338 )
339 except Exception as e:
340 logger.error(f"Failed to build alert payload: {e}")
270 - raise HTTPException(status_code=500, detail=f"Failed to build alert payload: {e}")
341 + raise HTTPException(
342 + status_code=500,
343 + detail=f"Failed to build alert payload: {e}",
344 + )
345
346
347 async def construct_soc_alert_url(root_url: str, soc_alert_id: int) -> str:
@@ -285,7 +359,12 @@ async def construct_soc_alert_url(root_url: str, soc_alert_id: int) -> str:
359 return f"{root_url}{url_path}"
360
361
288 -async def add_alert_to_document(es_client, alert: CreateAlertRequest, soc_alert_id: int, session: AsyncSession) -> Optional[str]:
362 +async def add_alert_to_document(
363 + es_client,
364 + alert: CreateAlertRequest,
365 + soc_alert_id: int,
366 + session: AsyncSession,
367 +) -> Optional[str]:
368 """
369 Update the alert document in Elasticsearch with the provided SOC alert ID URL.
370
@@ -300,32 +379,60 @@ async def add_alert_to_document(es_client, alert: CreateAlertRequest, soc_alert_
379 """
380 try:
381 connector_info = await get_connector_info_from_db("DFIR-IRIS", session)
303 - full_url = await construct_soc_alert_url(connector_info["connector_url"], soc_alert_id)
304 - es_client.update(index=alert.index_name, id=alert.alert_id, body={"doc": {"alert_url": full_url}})
305 - logger.info(f"Added alert ID {soc_alert_id} to alert {alert.alert_id} in index {alert.index_name}")
382 + full_url = await construct_soc_alert_url(
383 + connector_info["connector_url"],
384 + soc_alert_id,
385 + )
386 + es_client.update(
387 + index=alert.index_name,
388 + id=alert.alert_id,
389 + body={"doc": {"alert_url": full_url}},
390 + )
391 + logger.info(
392 + f"Added alert ID {soc_alert_id} to alert {alert.alert_id} in index {alert.index_name}",
393 + )
394 return full_url
395 except Exception as e:
308 - logger.error(f"Failed to add alert ID {soc_alert_id} to alert {alert.alert_id} in index {alert.index_name}: {e}")
396 + logger.error(
397 + f"Failed to add alert ID {soc_alert_id} to alert {alert.alert_id} in index {alert.index_name}: {e}",
398 + )
399 # Attempt to remove read-only block
400 try:
311 - es_client.indices.put_settings(index=alert.index_name, body={"index.blocks.write": None})
312 - logger.info(f"Removed read-only block from index {alert.index_name}. Retrying update.")
401 + es_client.indices.put_settings(
402 + index=alert.index_name,
403 + body={"index.blocks.write": None},
404 + )
405 + logger.info(
406 + f"Removed read-only block from index {alert.index_name}. Retrying update.",
407 + )
408
409 # Retry the update operation
315 - es_client.update(index=alert.index_name, id=alert.alert_id, body={"doc": {"alert_url": full_url}})
410 + es_client.update(
411 + index=alert.index_name,
412 + id=alert.alert_id,
413 + body={"doc": {"alert_url": full_url}},
414 + )
415 logger.info(
416 f"Added alert ID {soc_alert_id} to alert {alert.alert_id} in index {alert.index_name} after removing read-only block",
417 )
418
419 # Reenable the write block
321 - es_client.indices.put_settings(index=alert.index_name, body={"index.blocks.write": True})
420 + es_client.indices.put_settings(
421 + index=alert.index_name,
422 + body={"index.blocks.write": True},
423 + )
424 return full_url
425 except Exception as e2:
324 - logger.error(f"Failed to remove read-only block from index {alert.index_name}: {e2}")
426 + logger.error(
427 + f"Failed to remove read-only block from index {alert.index_name}: {e2}",
428 + )
429 return False
430
431
328 -async def create_alert(alert: CreateAlertRequest, session: AsyncSession) -> CreateAlertResponse:
432 +async def create_alert(
433 + alert: CreateAlertRequest,
434 + session: AsyncSession,
435 +) -> CreateAlertResponse:
436 """
437 Creates an alert in IRIS.
438
@@ -342,9 +449,20 @@ async def create_alert(alert: CreateAlertRequest, session: AsyncSession) -> Crea
449 logger.info(f"Creating alert {alert.alert_id} in IRIS")
450 alert_details = await get_single_alert_details(alert_details=alert)
451 logger.info(f"Alert details: {alert_details}")
345 - if await is_customer_code_valid(customer_code=alert_details._source.agent_labels_customer, session=session) is False:
346 - logger.info(f"Invalid customer_code: {alert_details._source.agent_labels_customer}")
347 - raise HTTPException(status_code=200, detail="Invalid customer_code, or the customer is not configured for alert creation.")
452 + if (
453 + await is_customer_code_valid(
454 + customer_code=alert_details._source.agent_labels_customer,
455 + session=session,
456 + )
457 + is False
458 + ):
459 + logger.info(
460 + f"Invalid customer_code: {alert_details._source.agent_labels_customer}",
461 + )
462 + raise HTTPException(
463 + status_code=200,
464 + detail="Invalid customer_code, or the customer is not configured for alert creation.",
465 + )
466 agent_data = await get_agent(agent_id=alert_details._source.agent_id, db=session)
467 ioc_payload = await build_ioc_payload(alert_details=alert_details)
468 iris_alert_payload = await build_alert_payload(
@@ -354,7 +472,11 @@ async def create_alert(alert: CreateAlertRequest, session: AsyncSession) -> Crea
472 session=session,
473 )
474 client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
357 - result = await fetch_and_validate_data(client, alert_client.add_alert, iris_alert_payload.to_dict())
475 + result = await fetch_and_validate_data(
476 + client,
477 + alert_client.add_alert,
478 + iris_alert_payload.to_dict(),
479 + )
480 alert_id = result["data"]["alert_id"]
481 logger.info(f"Successfully created alert {alert_id} in IRIS.")
482 # Update the alert with the asset payload
@@ -373,10 +495,23 @@ async def create_alert(alert: CreateAlertRequest, session: AsyncSession) -> Crea
495 {"iocs": [dict(IrisIoc(**iris_alert_payload.alert_iocs[0].to_dict()))]},
496 )
497 es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
376 - iris_url = await add_alert_to_document(es_client, alert, result["data"]["alert_id"], session=session)
498 + iris_url = await add_alert_to_document(
499 + es_client,
500 + alert,
501 + result["data"]["alert_id"],
502 + session=session,
503 + )
504 try:
505 alert_id = result["data"]["alert_id"]
379 - return CreateAlertResponse(alert_id=alert_id, success=True, message=f"Alert {alert_id} created successfully", alert_url=iris_url)
506 + return CreateAlertResponse(
507 + alert_id=alert_id,
508 + success=True,
509 + message=f"Alert {alert_id} created successfully",
510 + alert_url=iris_url,
511 + )
512 except Exception as e:
513 logger.error(f"Failed to create alert {alert.alert_id}: {e}")
382 - raise HTTPException(status_code=500, detail=f"Failed to create alert for ID {alert.alert_id}: {e}")
514 + raise HTTPException(
515 + status_code=500,
516 + detail=f"Failed to create alert for ID {alert.alert_id}: {e}",
517 + )
backend/app/integrations/ask_socfortress/routes/ask_socfortress.py
+12 -11
@@ -1,22 +1,16 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from fastapi import HTTPException
4 -from fastapi import Security
5 -from loguru import logger
6 -from sqlalchemy.ext.asyncio import AsyncSession
7 -
1 from app.auth.utils import AuthHandler
2 from app.db.db_session import get_db
3 from app.integrations.ask_socfortress.schema.ask_socfortress import (
4 AskSocfortressRequest,
12 -)
13 -from app.integrations.ask_socfortress.schema.ask_socfortress import (
5 AskSocfortressSigmaResponse,
6 )
7 from app.integrations.ask_socfortress.services.ask_socfortress import (
8 ask_socfortress_lookup,
9 )
10 from app.utils import get_connector_attribute
11 +from fastapi import APIRouter, Depends, HTTPException, Security
12 +from loguru import logger
13 +from sqlalchemy.ext.asyncio import AsyncSession
14
15 # App specific imports
16
@@ -36,11 +30,18 @@ async def ensure_api_key_exists(session: AsyncSession = Depends(get_db)) -> bool
30 Returns:
31 bool: True if the API key exists, otherwise raises HTTPException.
32 """
39 - api_key = await get_connector_attribute(connector_id=10, column_name="connector_api_key", session=session)
33 + api_key = await get_connector_attribute(
34 + connector_id=10,
35 + column_name="connector_api_key",
36 + session=session,
37 + )
38 # Close the session
39 await session.close()
40 if not api_key:
43 - raise HTTPException(status_code=500, detail="Ask SocFortress API key not found in the database.")
41 + raise HTTPException(
42 + status_code=500,
43 + detail="Ask SocFortress API key not found in the database.",
44 + )
45 return True
46
47
backend/app/integrations/ask_socfortress/schema/ask_socfortress.py
+5 -3
@@ -1,9 +1,11 @@
1 -from pydantic import BaseModel
2 -from pydantic import Field
1 +from pydantic import BaseModel, Field
2
3
4 class AskSocfortressRequest(BaseModel):
6 - index_name: str = Field(..., description="The name of the index to search alerts for.")
5 + index_name: str = Field(
6 + ...,
7 + description="The name of the index to search alerts for.",
8 + )
9 alert_id: str = Field(..., description="The alert id.")
10
11
backend/app/integrations/ask_socfortress/services/ask_socfortress.py
+122 -42
@@ -1,31 +1,28 @@
1 -from typing import Any
2 -from typing import Dict
3 -from typing import Optional
1 +from typing import Any, Dict, Optional
2
3 import httpx
6 -from fastapi import HTTPException
7 -from loguru import logger
8 -from sqlalchemy.ext.asyncio import AsyncSession
9 -
4 from app.connectors.utils import get_connector_info_from_db
5 from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
6 from app.db.db_session import get_db_session
13 -from app.integrations.alert_escalation.schema.general_alert import CreateAlertRequest
14 -from app.integrations.alert_escalation.schema.general_alert import GenericAlertModel
15 -from app.integrations.alert_escalation.schema.general_alert import GenericSourceModel
16 -from app.integrations.ask_socfortress.schema.ask_socfortress import (
17 - AskSocfortressRequest,
7 +from app.integrations.alert_escalation.schema.general_alert import (
8 + CreateAlertRequest,
9 + GenericAlertModel,
10 + GenericSourceModel,
11 )
12 from app.integrations.ask_socfortress.schema.ask_socfortress import (
13 + AskSocfortressRequest,
14 AskSocfortressSigmaRequest,
21 -)
22 -from app.integrations.ask_socfortress.schema.ask_socfortress import (
15 AskSocfortressSigmaResponse,
16 )
17 from app.utils import get_connector_attribute
18 +from fastapi import HTTPException
19 +from loguru import logger
20 +from sqlalchemy.ext.asyncio import AsyncSession
21
22
28 -async def get_single_alert_details(alert_details: CreateAlertRequest) -> GenericAlertModel:
23 +async def get_single_alert_details(
24 + alert_details: CreateAlertRequest,
25 +) -> GenericAlertModel:
26 """
27 Fetches the details of a single alert.
28
@@ -38,18 +35,31 @@ async def get_single_alert_details(alert_details: CreateAlertRequest) -> Generic
35 Raises:
36 HTTPException: If there is an error while fetching the alert details.
37 """
41 - logger.info(f"Fetching alert details for alert {alert_details.alert_id} in index {alert_details.index_name}")
38 + logger.info(
39 + f"Fetching alert details for alert {alert_details.alert_id} in index {alert_details.index_name}",
40 + )
41 es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
42 try:
43 alert = es_client.get(index=alert_details.index_name, id=alert_details.alert_id)
44 source_model = GenericSourceModel(**alert["_source"])
46 - return GenericAlertModel(_source=source_model, _id=alert["_id"], _index=alert["_index"], _version=alert["_version"])
45 + return GenericAlertModel(
46 + _source=source_model,
47 + _id=alert["_id"],
48 + _index=alert["_index"],
49 + _version=alert["_version"],
50 + )
51 except Exception as e:
52 logger.debug(f"Failed to collect alert details: {e}")
49 - raise HTTPException(status_code=400, detail=f"Failed to collect alert details: {e}")
53 + raise HTTPException(
54 + status_code=400,
55 + detail=f"Failed to collect alert details: {e}",
56 + )
57
58
52 -async def get_ask_socfortress_attributes(column_name: str, session: AsyncSession) -> str:
59 +async def get_ask_socfortress_attributes(
60 + column_name: str,
61 + session: AsyncSession,
62 +) -> str:
63 """
64 Gets the Ask SocFortress attribute from the database.
65
@@ -64,15 +74,24 @@ async def get_ask_socfortress_attributes(column_name: str, session: AsyncSession
74 str: The Ask SocFortress Attribute.
75
76 """
67 - attribute_value = await get_connector_attribute(connector_id=9, column_name=column_name, session=session)
77 + attribute_value = await get_connector_attribute(
78 + connector_id=9,
79 + column_name=column_name,
80 + session=session,
81 + )
82 # Close the session
83 await session.close()
84 if not attribute_value:
71 - raise HTTPException(status_code=500, detail="Ask Socfortress attributes not found in the database.")
85 + raise HTTPException(
86 + status_code=500,
87 + detail="Ask Socfortress attributes not found in the database.",
88 + )
89 return attribute_value
90
91
75 -async def verify_ask_socfortress_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
92 +async def verify_ask_socfortress_credentials(
93 + attributes: Dict[str, Any],
94 +) -> Dict[str, Any]:
95 """
96 Verifies the Ask SocFortress credentials.
97
@@ -89,7 +108,10 @@ async def verify_ask_socfortress_credentials(attributes: Dict[str, Any]) -> Dict
108 url = attributes.get("connector_url", None)
109 if api_key is None or url is None:
110 logger.error("No Ask Socfortress credentials found in the database")
92 - raise HTTPException(status_code=500, detail="Ask Socfortress credentials not found in the database")
111 + raise HTTPException(
112 + status_code=500,
113 + detail="Ask Socfortress credentials not found in the database",
114 + )
115 return attributes
116
117
@@ -112,17 +134,33 @@ async def verify_ask_socfortress_connector(connector_name: str) -> str:
134 if attributes is None:
135 logger.error("No Ask Socfortress connector found in the database")
136 return None
115 - request = AskSocfortressSigmaRequest(sigma_rule_name="Process Explorer Driver Creation By Non-Sysinternals Binary")
116 - response = await invoke_ask_socfortress_api(attributes["connector_api_key"], attributes["connector_url"], request)
137 + request = AskSocfortressSigmaRequest(
138 + sigma_rule_name="Process Explorer Driver Creation By Non-Sysinternals Binary",
139 + )
140 + response = await invoke_ask_socfortress_api(
141 + attributes["connector_api_key"],
142 + attributes["connector_url"],
143 + request,
144 + )
145 if response["message"] != "Forbidden":
146 logger.info("Ask Socfortress connector verified successfully")
119 - return {"connectionSuccessful": True, "message": "Successfully verified ASK SOCFortress connector"}
147 + return {
148 + "connectionSuccessful": True,
149 + "message": "Successfully verified ASK SOCFortress connector",
150 + }
151 else:
152 logger.error("Failed to verify Ask Socfortress connector")
122 - return {"connectionSuccessful": False, "message": "Failed to verify ASK SOCFortress connector"}
153 + return {
154 + "connectionSuccessful": False,
155 + "message": "Failed to verify ASK SOCFortress connector",
156 + }
157
158
125 -async def invoke_ask_socfortress_api(api_key: str, url: str, request: AskSocfortressSigmaRequest) -> dict:
159 +async def invoke_ask_socfortress_api(
160 + api_key: str,
161 + url: str,
162 + request: AskSocfortressSigmaRequest,
163 +) -> dict:
164 """
165 Invokes the Socfortress Threat Intel API with the provided API key, URL, and request parameters.
166
@@ -137,14 +175,21 @@ async def invoke_ask_socfortress_api(api_key: str, url: str, request: AskSocfort
175 Raises:
176 httpx.HTTPStatusError: If the API request fails with a non-successful status code.
177 """
140 - headers = {"module-version": "your_module_version", "x-api-key": api_key, "Content-Type": "application/json"}
178 + headers = {
179 + "module-version": "your_module_version",
180 + "x-api-key": api_key,
181 + "Content-Type": "application/json",
182 + }
183 data = {"sigma_rule_name": request.sigma_rule_name}
184 async with httpx.AsyncClient(timeout=60) as client:
185 response = await client.post(url=f"{url}/v1/sigma", headers=headers, json=data)
186 return response.json()
187
188
147 -async def get_ask_socfortress_response(request: AskSocfortressSigmaRequest, session: AsyncSession) -> AskSocfortressSigmaResponse:
189 +async def get_ask_socfortress_response(
190 + request: AskSocfortressSigmaRequest,
191 + session: AsyncSession,
192 +) -> AskSocfortressSigmaResponse:
193 """
194 Retrieves IoC response from Socfortress Threat Intel API.
195
@@ -166,7 +211,12 @@ async def get_ask_socfortress_response(request: AskSocfortressSigmaRequest, sess
211 return AskSocfortressSigmaResponse(success=success, message=message)
212
213
169 -async def add_alert_to_document(es_client, alert: CreateAlertRequest, result: str, session: AsyncSession) -> Optional[str]:
214 +async def add_alert_to_document(
215 + es_client,
216 + alert: CreateAlertRequest,
217 + result: str,
218 + session: AsyncSession,
219 +) -> Optional[str]:
220 """
221 Update the alert document in Elasticsearch with the provided SOC alert ID URL.
222
@@ -180,32 +230,57 @@ async def add_alert_to_document(es_client, alert: CreateAlertRequest, result: st
230 - True if the update is successful, False otherwise.
231 """
232 try:
183 - es_client.update(index=alert.index_name, id=alert.alert_id, body={"doc": {"ask_socfortress_message": result}})
184 - logger.info(f"Added Ask SOCFortress Message to alert {alert.alert_id} in index {alert.index_name}")
233 + es_client.update(
234 + index=alert.index_name,
235 + id=alert.alert_id,
236 + body={"doc": {"ask_socfortress_message": result}},
237 + )
238 + logger.info(
239 + f"Added Ask SOCFortress Message to alert {alert.alert_id} in index {alert.index_name}",
240 + )
241 return None
242 except Exception as e:
187 - logger.error(f"Failed to add Ask SOCFortress Message to alert {alert.alert_id} in index {alert.index_name}: {e}")
243 + logger.error(
244 + f"Failed to add Ask SOCFortress Message to alert {alert.alert_id} in index {alert.index_name}: {e}",
245 + )
246
247 # Attempt to remove read-only block
248 try:
191 - es_client.indices.put_settings(index=alert.index_name, body={"index.blocks.write": None})
192 - logger.info(f"Removed read-only block from index {alert.index_name}. Retrying update.")
249 + es_client.indices.put_settings(
250 + index=alert.index_name,
251 + body={"index.blocks.write": None},
252 + )
253 + logger.info(
254 + f"Removed read-only block from index {alert.index_name}. Retrying update.",
255 + )
256
257 # Retry the update operation
195 - es_client.update(index=alert.index_name, id=alert.alert_id, body={"doc": {"ask_socfortress": result}})
258 + es_client.update(
259 + index=alert.index_name,
260 + id=alert.alert_id,
261 + body={"doc": {"ask_socfortress": result}},
262 + )
263 logger.info(
264 f"Added Ask SOCFortress Message to alert {alert.alert_id} in index {alert.index_name} after removing read-only block",
265 )
266
267 # Reenable the write block
201 - es_client.indices.put_settings(index=alert.index_name, body={"index.blocks.write": True})
268 + es_client.indices.put_settings(
269 + index=alert.index_name,
270 + body={"index.blocks.write": True},
271 + )
272 return True
273 except Exception as e2:
204 - logger.error(f"Failed to remove read-only block from index {alert.index_name}: {e2}")
274 + logger.error(
275 + f"Failed to remove read-only block from index {alert.index_name}: {e2}",
276 + )
277 return False
278
279
208 -async def ask_socfortress_lookup(alert: AskSocfortressRequest, session: AsyncSession) -> AskSocfortressSigmaResponse:
280 +async def ask_socfortress_lookup(
281 + alert: AskSocfortressRequest,
282 + session: AsyncSession,
283 +) -> AskSocfortressSigmaResponse:
284 """
285 Performs a Ask SOCFortress lookup using the Socfortress service.
286
@@ -220,8 +295,13 @@ async def ask_socfortress_lookup(alert: AskSocfortressRequest, session: AsyncSes
295 logger.info(f"Alert details: {alert_details}")
296 if alert_details._source.rule_group3 != "sigma":
297 raise HTTPException(status_code=400, detail="Alert is not a Sigma alert.")
223 - sigma_rule_name = AskSocfortressSigmaRequest(sigma_rule_name=alert_details._source.data_name)
224 - ask_socfortress_response = await get_ask_socfortress_response(sigma_rule_name, session)
298 + sigma_rule_name = AskSocfortressSigmaRequest(
299 + sigma_rule_name=alert_details._source.data_name,
300 + )
301 + ask_socfortress_response = await get_ask_socfortress_response(
302 + sigma_rule_name,
303 + session,
304 + )
305 result = ask_socfortress_response.message
306 es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
307 await add_alert_to_document(es_client, alert, result, session=session)
backend/app/integrations/dnstwist/routes/analyze.py
+12 -8
@@ -1,12 +1,11 @@
1 import regex
2 -from fastapi import APIRouter
3 -from fastapi import Depends
4 -from fastapi import HTTPException
5 -from loguru import logger
6 -
7 -from app.integrations.dnstwist.schema.analyze import DomainAnalysisResponse
8 -from app.integrations.dnstwist.schema.analyze import DomainRequestBody
2 +from app.integrations.dnstwist.schema.analyze import (
3 + DomainAnalysisResponse,
4 + DomainRequestBody,
5 +)
6 from app.integrations.dnstwist.services.analyze import analyze_domain
7 +from fastapi import APIRouter, Depends, HTTPException
8 +from loguru import logger
9
10 dnstwist_router = APIRouter()
11
@@ -30,7 +29,12 @@ def is_domain(domain: str) -> DomainRequestBody:
29 return DomainRequestBody(domain=domain)
30
31
33 -@dnstwist_router.post("/analyze", response_model=DomainAnalysisResponse, status_code=200, description="Analyze domain with DNS Twist")
32 +@dnstwist_router.post(
33 + "/analyze",
34 + response_model=DomainAnalysisResponse,
35 + status_code=200,
36 + description="Analyze domain with DNS Twist",
37 +)
38 async def analyze(body: DomainRequestBody = Depends(is_domain)):
39 """
40 Analyzes a domain using DNS Twist.
backend/app/integrations/dnstwist/schema/analyze.py
+2 -4
@@ -1,8 +1,6 @@
1 -from typing import List
2 -from typing import Optional
1 +from typing import List, Optional
2
4 -from pydantic import BaseModel
5 -from pydantic import Field
3 +from pydantic import BaseModel, Field
4
5
6 class DomainData(BaseModel):
backend/app/integrations/dnstwist/services/analyze.py
+14 -5
@@ -1,9 +1,10 @@
1 import dnstwist
2 +from app.integrations.dnstwist.schema.analyze import (
3 + DomainAnalysisResponse,
4 + DomainRequestBody,
5 +)
6 from loguru import logger
7
4 -from app.integrations.dnstwist.schema.analyze import DomainAnalysisResponse
5 -from app.integrations.dnstwist.schema.analyze import DomainRequestBody
6 -
8
9 def analyze_domain(domain: DomainRequestBody) -> DomainAnalysisResponse:
10 """
@@ -18,7 +19,11 @@ def analyze_domain(domain: DomainRequestBody) -> DomainAnalysisResponse:
19 logger.info(f"Analyzing domain {domain} with DNS Twist.")
20 logger.info("Analyzing domain for registered domains.")
21 data = dnstwist.run(domain=domain, registered=True, format="json")
21 - return DomainAnalysisResponse(data=data, message="Domain analysis completed.", success=True)
22 + return DomainAnalysisResponse(
23 + data=data,
24 + message="Domain analysis completed.",
25 + success=True,
26 + )
27
28
29 def analyze_domain_phishing(domain: DomainRequestBody) -> DomainAnalysisResponse:
@@ -39,4 +44,8 @@ def analyze_domain_phishing(domain: DomainRequestBody) -> DomainAnalysisResponse
44 format="json",
45 lsh=True,
46 )
42 - return DomainAnalysisResponse(data=data, message="Domain analysis completed.", success=True)
47 + return DomainAnalysisResponse(
48 + data=data,
49 + message="Domain analysis completed.",
50 + success=True,
51 + )
backend/app/integrations/markdown/office365.md
+11 -11
@@ -29,26 +29,26 @@ For **Wazuh** to successfully connect to the **Office365 API**, an authenticatio
29
30 To authenticate with the Microsoft identity platform endpoint, you need to register an app in your [Microsoft Azure portal app registrations](https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade) section. Once there click on **New registration**:
31
32 -![Register your app](/src/assets/images/office365/0-azure-app-new-registration.png)
32 +![Register your app](/images/office365/0-azure-app-new-registration.png)
33
34 Fill in the name of your app, choose the desired account type and click on the **Register** button:
35
36 -![Register your app](/src/assets/images/office365/1-azure-wazuh-app-register-application.png)
36 +![Register your app](/images/office365/1-azure-wazuh-app-register-application.png)
37
38 The app is now registered, and you can see information about it in its **Overview** section, at this point we can get the `client` and `tenant` IDs:
39
40 -![Register your app](/src/assets/images/office365/2-azure-wazuh-app-overview.png)
40 +![Register your app](/images/office365/2-azure-wazuh-app-overview.png)
41
42 # Certificates & secrets
43
44 You can generate a password to use during the authentication process. Go to **Certificates & secrets** and click on **New client secret**,
45 then the name and the expiration date of the **New client secret** are requested:
46
47 -![Certificates & secrets](/src/assets/images/office365/3-azure-wazuh-app-create-password.png)
47 +![Certificates & secrets](/images/office365/3-azure-wazuh-app-create-password.png)
48
49 Copy and save the value section.
50
51 -![Certificates & secrets](/src/assets/images/office365/3-azure-wazuh-app-create-password-copy-value.png)
51 +![Certificates & secrets](/images/office365/3-azure-wazuh-app-create-password-copy-value.png)
52
53 Make sure you write it down because the UI won’t let you copy it afterward.
54
@@ -64,23 +64,23 @@ You need to add the following permissions under the **ActivityFeed** group:
64
65 - `ActivityFeed.ReadDlp`. Read DLP policy events including detected sensitive data.
66
67 -![API permissions](/src/assets/images/office365/4-azure-wazuh-app-configure-permissions.png)
67 +![API permissions](/images/office365/4-azure-wazuh-app-configure-permissions.png)
68
69 Admin consent is required for API permission changes.
70
71 -![API permissions](/src/assets/images/office365/4-azure-wazuh-app-configure-permissions-admin-consent.png)
71 +![API permissions](/images/office365/4-azure-wazuh-app-configure-permissions-admin-consent.png)
72
73 ### CoPilot configuration
74
75 Next, we will see how to deploy this module in CoPilot. To do so, we will need to navigate to the `Customers` section and select the customer we want to deploy the module to. Once there, we will click on the `Integrations` tab and then on the `Add integration` button. We will select the `Office365` module and fill in the required fields.
76
77 -![Copilot Configuration](/src/assets/images/office365/copilot_config_customer_details.PNG)
77 +![Copilot Configuration](/images/office365/copilot_config_customer_details.PNG)
78
79 -![Copilot Configuration](/src/assets/images/office365/copilot_config_customer_integration.PNG)
79 +![Copilot Configuration](/images/office365/copilot_config_customer_integration.PNG)
80
81 -![Copilot Configuration](/src/assets/images/office365/copilot_config_customer_integration_config.PNG)
81 +![Copilot Configuration](/images/office365/copilot_config_customer_integration_config.PNG)
82
83 -![Copilot Configuration](/src/assets/images/office365/copilot_config_customer_integration_auth.PNG)
83 +![Copilot Configuration](/images/office365/copilot_config_customer_integration_auth.PNG)
84
85 Once deployed, Copilot will automatically add the required configuration to the `Wazuh manager`, deploy the required Index, Stream, and Pipeline to `Graylog` and create the required Dashboards within `Grafana`. `Praeco` will also be configured to send `Exchange` and `Threat Intel` Office365 alerts to `DFIR-IRIS`.
86
backend/app/integrations/mimecast/routes/mimecast.py
+30 -18
@@ -1,20 +1,20 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from fastapi import Security
4 -from loguru import logger
5 -from sqlalchemy.ext.asyncio import AsyncSession
6 -
1 from app.auth.utils import AuthHandler
2 from app.db.db_session import get_db
9 -from app.integrations.mimecast.schema.mimecast import MimecastAuthKeys
10 -from app.integrations.mimecast.schema.mimecast import MimecastRequest
11 -from app.integrations.mimecast.schema.mimecast import MimecastResponse
12 -from app.integrations.mimecast.schema.mimecast import MimecastTTPURLSRequest
13 -from app.integrations.mimecast.services.mimecast import get_ttp_urls
14 -from app.integrations.mimecast.services.mimecast import invoke_mimecast
3 +from app.integrations.mimecast.schema.mimecast import (
4 + MimecastAuthKeys,
5 + MimecastRequest,
6 + MimecastResponse,
7 + MimecastTTPURLSRequest,
8 +)
9 +from app.integrations.mimecast.services.mimecast import get_ttp_urls, invoke_mimecast
10 from app.integrations.routes import find_customer_integration
16 -from app.integrations.utils.utils import extract_mimecast_auth_keys
17 -from app.integrations.utils.utils import get_customer_integration_response
11 +from app.integrations.utils.utils import (
12 + extract_mimecast_auth_keys,
13 + get_customer_integration_response,
14 +)
15 +from fastapi import APIRouter, Depends, Security
16 +from loguru import logger
17 +from sqlalchemy.ext.asyncio import AsyncSession
18
19 integration_mimecast_router = APIRouter()
20
@@ -25,7 +25,10 @@ integration_mimecast_router = APIRouter()
25 description="Invoke a mimecast integration.",
26 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
27 )
28 -async def invoke_mimecast_route(mimecast_request: MimecastRequest, session: AsyncSession = Depends(get_db)) -> MimecastResponse:
28 +async def invoke_mimecast_route(
29 + mimecast_request: MimecastRequest,
30 + session: AsyncSession = Depends(get_db),
31 +) -> MimecastResponse:
32 """
33 Invoke a mimecast integration.
34
@@ -43,7 +46,10 @@ async def invoke_mimecast_route(mimecast_request: MimecastRequest, session: Asyn
46 Returns:
47 - MimecastResponse: The response model containing the result of the mimecast integration invocation.
48 """
46 - customer_integration_response = await get_customer_integration_response(mimecast_request.customer_code, session)
49 + customer_integration_response = await get_customer_integration_response(
50 + mimecast_request.customer_code,
51 + session,
52 + )
53
54 customer_integration = await find_customer_integration(
55 mimecast_request.customer_code,
@@ -64,10 +70,16 @@ async def invoke_mimecast_route(mimecast_request: MimecastRequest, session: Asyn
70 description="Pull down Mimecast TTP URLs for a given time range. "
71 "Link to docs: https://integrations.mimecast.com/documentation/endpoint-reference/logs-and-statistics/get-ttp-url-logs/ ",
72 )
67 -async def mimecast_ttp_url_route(mimecast_request: MimecastRequest, session: AsyncSession = Depends(get_db)):
73 +async def mimecast_ttp_url_route(
74 + mimecast_request: MimecastRequest,
75 + session: AsyncSession = Depends(get_db),
76 +):
77 logger.info("Mimecast TTP URL request received")
78 customer_code = mimecast_request.customer_code
70 - customer_integration_response = await get_customer_integration_response(mimecast_request.customer_code, session)
79 + customer_integration_response = await get_customer_integration_response(
80 + mimecast_request.customer_code,
81 + session,
82 + )
83
84 customer_integration = await find_customer_integration(
85 mimecast_request.customer_code,
backend/app/integrations/mimecast/routes/provision.py
+28 -12
@@ -1,15 +1,15 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from sqlalchemy.ext.asyncio import AsyncSession
4 -
1 from app.db.db_session import get_db
2 from app.integrations.mimecast.schema.mimecast import MimecastScheduledResponse
7 -from app.integrations.mimecast.schema.provision import ProvisionMimecastRequest
8 -from app.integrations.mimecast.schema.provision import ProvisionMimecastResponse
3 +from app.integrations.mimecast.schema.provision import (
4 + ProvisionMimecastRequest,
5 + ProvisionMimecastResponse,
6 +)
7 from app.integrations.mimecast.services.provision import provision_mimecast
8 from app.integrations.utils.utils import get_customer_integration_response
9 from app.schedulers.models.scheduler import CreateSchedulerRequest
10 from app.schedulers.scheduler import add_scheduler_jobs
11 +from fastapi import APIRouter, Depends
12 +from sqlalchemy.ext.asyncio import AsyncSession
13
14 integration_mimecast_scheduler_router = APIRouter()
15
@@ -34,7 +34,10 @@ async def provision_mimecast_route(
34 ProvisionMimecastResponse: The response object indicating the success or failure of the provisioning process.
35 """
36 # Check if the customer integration settings are available and can be provisioned
37 - await get_customer_integration_response(provision_mimecast_request.customer_code, session)
37 + await get_customer_integration_response(
38 + provision_mimecast_request.customer_code,
39 + session,
40 + )
41 await provision_mimecast(provision_mimecast_request, session)
42 await add_scheduler_jobs(
43 CreateSchedulerRequest(
@@ -50,14 +53,19 @@ async def provision_mimecast_route(
53 job_id="invoke_mimecast_integration",
54 ),
55 )
53 - return ProvisionMimecastResponse(success=True, message="Mimecast integration provisioned.")
56 + return ProvisionMimecastResponse(
57 + success=True,
58 + message="Mimecast integration provisioned.",
59 + )
60
61
62 @integration_mimecast_scheduler_router.post(
63 "/invoke/scheduler/siem",
64 description="Invoke a mimecast integration.",
65 )
60 -async def invoke_mimecast_siem_schedule_create(time_interval: int) -> MimecastScheduledResponse:
66 +async def invoke_mimecast_siem_schedule_create(
67 + time_interval: int,
68 +) -> MimecastScheduledResponse:
69 """
70 Invoke a mimecast integration and schedule it based on the specified time interval.
71
@@ -74,14 +82,19 @@ async def invoke_mimecast_siem_schedule_create(time_interval: int) -> MimecastSc
82 job_id="invoke_mimecast_integration",
83 ),
84 )
77 - return MimecastScheduledResponse(success=True, message="Mimecast integration scheduled.")
85 + return MimecastScheduledResponse(
86 + success=True,
87 + message="Mimecast integration scheduled.",
88 + )
89
90
91 @integration_mimecast_scheduler_router.post(
92 "/invoke/scheduler/ttp",
93 description="Invoke a mimecast integration.",
94 )
84 -async def invoke_mimecast_ttp_schedule_create(time_interval: int) -> MimecastScheduledResponse:
95 +async def invoke_mimecast_ttp_schedule_create(
96 + time_interval: int,
97 +) -> MimecastScheduledResponse:
98 """
99 Invoke a Mimecast integration with a specified time interval.
100
@@ -98,4 +111,7 @@ async def invoke_mimecast_ttp_schedule_create(time_interval: int) -> MimecastSch
111 job_id="invoke_mimecast_integration",
112 ),
113 )
101 - return MimecastScheduledResponse(success=True, message="Mimecast integration scheduled.")
114 + return MimecastScheduledResponse(
115 + success=True,
116 + message="Mimecast integration scheduled.",
117 + )
backend/app/integrations/mimecast/schema/mimecast.py
+19 -14
@@ -2,17 +2,11 @@ import base64
2 import hashlib
3 import hmac
4 import uuid
5 -from datetime import datetime
6 -from datetime import timedelta
5 +from datetime import datetime, timedelta
6 from enum import Enum
8 -from typing import Dict
9 -from typing import List
10 -from typing import Optional
7 +from typing import Dict, List, Optional
8
12 -from pydantic import BaseModel
13 -from pydantic import Field
14 -from pydantic import HttpUrl
15 -from pydantic import root_validator
9 +from pydantic import BaseModel, Field, HttpUrl, root_validator
10
11
12 class PipelineRuleTitles(Enum):
@@ -147,7 +141,9 @@ class MimecastHeaders(BaseModel):
141 )
142
143 class Config:
150 - allow_population_by_field_name = True # This allows field population by both alias and field name
144 + allow_population_by_field_name = (
145 + True # This allows field population by both alias and field name
146 + )
147
148
149 class MimecastTTPURLSRequest(BaseModel):
@@ -162,7 +158,10 @@ class MimecastTTPURLSRequest(BaseModel):
158 ...,
159 description="The email address of the Mimecast administrator.",
160 )
165 - BaseURL: Optional[str] = Field(None, description="The base URL for the Mimecast API.")
161 + BaseURL: Optional[str] = Field(
162 + None,
163 + description="The base URL for the Mimecast API.",
164 + )
165 time_range: Optional[str] = Field(
166 "15m",
167 pattern="^[1-9][0-9]*[mhdw]$",
@@ -200,7 +199,9 @@ class MimecastTTPURLSRequest(BaseModel):
199 elif unit == "w":
200 lower_bound = now - timedelta(weeks=amount)
201
203 - values["lower_bound"] = lower_bound.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
202 + values["lower_bound"] = (
203 + lower_bound.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
204 + )
205 values["upper_bound"] = now.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
206 return values
207
@@ -227,7 +228,9 @@ class MimecastTTPURLSRequest(BaseModel):
228 "Content-Type": "application/json",
229 }
230
230 - self.headers = MimecastHeaders(**headers_dict) # Create a new instance of MimecastHeaders and assign
231 + self.headers = MimecastHeaders(
232 + **headers_dict,
233 + ) # Create a new instance of MimecastHeaders and assign
234
235 return headers_dict
236
@@ -245,7 +248,9 @@ class DataItem(BaseModel):
248 scanResult: str = Field(..., description="Scan result.")
249
250 class Config:
248 - allow_population_by_field_name = True # This allows field population by both alias and field name
251 + allow_population_by_field_name = (
252 + True # This allows field population by both alias and field name
253 + )
254
255
256 class RequestBody(BaseModel):
backend/app/integrations/mimecast/schema/provision.py
+22 -11
@@ -1,11 +1,6 @@
1 -from typing import Any
2 -from typing import Dict
3 -from typing import List
4 -from typing import Optional
1 +from typing import Any, Dict, List, Optional
2
6 -from pydantic import BaseModel
7 -from pydantic import Field
8 -from pydantic import root_validator
3 +from pydantic import BaseModel, Field, root_validator
4
5
6 class ProvisionMimecastRequest(BaseModel):
@@ -46,8 +41,14 @@ class MimecastEventStream(BaseModel):
41 index_set_id: str = Field(..., description="ID of the associated index set")
42 rules: List[StreamRule] = Field(..., description="List of rules for the stream")
43 matching_type: str = Field(..., description="Matching type for the rules")
49 - remove_matches_from_default_stream: bool = Field(..., description="Whether to remove matches from the default stream")
50 - content_pack: Optional[str] = Field(None, description="Associated content pack, if any")
44 + remove_matches_from_default_stream: bool = Field(
45 + ...,
46 + description="Whether to remove matches from the default stream",
47 + )
48 + content_pack: Optional[str] = Field(
49 + None,
50 + description="Associated content pack, if any",
51 + )
52
53 class Config:
54 schema_extra = {
@@ -56,8 +57,18 @@ class MimecastEventStream(BaseModel):
57 "description": "Mimecast EVENTS - Example Company",
58 "index_set_id": "12345",
59 "rules": [
59 - {"field": "agent_labels_customer", "type": 1, "inverted": False, "value": "ExampleCode"},
60 - {"field": "agent_labels_integration", "type": 1, "inverted": False, "value": "Office365"},
60 + {
61 + "field": "agent_labels_customer",
62 + "type": 1,
63 + "inverted": False,
64 + "value": "ExampleCode",
65 + },
66 + {
67 + "field": "agent_labels_integration",
68 + "type": 1,
69 + "inverted": False,
70 + "value": "Office365",
71 + },
72 ],
73 "matching_type": "AND",
74 "remove_matches_from_default_stream": True,
backend/app/integrations/mimecast/services/mimecast.py
+92 -31
@@ -12,20 +12,21 @@ from zipfile import ZipFile
12
13 import aiofiles
14 import requests
15 -from fastapi import HTTPException
16 -from loguru import logger
17 -
18 -from app.integrations.mimecast.schema.mimecast import DataItem
19 -from app.integrations.mimecast.schema.mimecast import MimecastAPIEndpointResponse
20 -from app.integrations.mimecast.schema.mimecast import MimecastAuthKeys
21 -from app.integrations.mimecast.schema.mimecast import MimecastRequest
22 -from app.integrations.mimecast.schema.mimecast import MimecastResponse
23 -from app.integrations.mimecast.schema.mimecast import MimecastTTPURLSRequest
24 -from app.integrations.mimecast.schema.mimecast import RequestBody
25 -from app.integrations.mimecast.schema.mimecast import TtpURLResponseBody
15 +from app.integrations.mimecast.schema.mimecast import (
16 + DataItem,
17 + MimecastAPIEndpointResponse,
18 + MimecastAuthKeys,
19 + MimecastRequest,
20 + MimecastResponse,
21 + MimecastTTPURLSRequest,
22 + RequestBody,
23 + TtpURLResponseBody,
24 +)
25 from app.integrations.utils.collection import send_post_request
26 from app.integrations.utils.event_shipper import event_shipper
27 from app.integrations.utils.schema import EventShipperPayload
28 +from fastapi import HTTPException
29 +from loguru import logger
30
31
32 async def get_checkpoint_filename(customer_code: str):
@@ -35,7 +36,10 @@ async def get_checkpoint_filename(customer_code: str):
36 """
37 # Relative path from the current script to the checkpoint directory
38 checkpoint_directory = os.path.join(os.path.dirname(__file__), "..", "checkpoint")
38 - checkpoint_filename = os.path.join(checkpoint_directory, f"mimecast_{customer_code}.checkpoint")
39 + checkpoint_filename = os.path.join(
40 + checkpoint_directory,
41 + f"mimecast_{customer_code}.checkpoint",
42 + )
43
44 # Normalize the path to remove relative path components
45 checkpoint_filename = os.path.normpath(checkpoint_filename)
@@ -88,7 +92,9 @@ async def get_hdr_date():
92 return datetime.datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S UTC")
93
94
91 -async def get_base_url(mimecast_auth_keys: MimecastAuthKeys) -> MimecastAPIEndpointResponse:
95 +async def get_base_url(
96 + mimecast_auth_keys: MimecastAuthKeys,
97 +) -> MimecastAPIEndpointResponse:
98 """
99 Retrieves the base URL for the Mimecast integration.
100 """
@@ -111,17 +117,33 @@ async def get_base_url(mimecast_auth_keys: MimecastAuthKeys) -> MimecastAPIEndpo
117 data=post_body,
118 )
119 if response["success"] is True:
114 - logger.info(f"Successfully retrieved base URL for Mimecast integration. Response: {response}")
120 + logger.info(
121 + f"Successfully retrieved base URL for Mimecast integration. Response: {response}",
122 + )
123 return MimecastAPIEndpointResponse(**response)
124 else:
117 - logger.error(f"Unable to retrieve base URL for Mimecast integration. Response: {response}")
118 - raise HTTPException(status_code=400, detail="Unable to retrieve base URL for Mimecast integration.")
125 + logger.error(
126 + f"Unable to retrieve base URL for Mimecast integration. Response: {response}",
127 + )
128 + raise HTTPException(
129 + status_code=400,
130 + detail="Unable to retrieve base URL for Mimecast integration.",
131 + )
132 except Exception as e:
120 - logger.error(f"Unable to retrieve base URL for Mimecast integration. Exception: {e}")
121 - raise HTTPException(status_code=400, detail="Unable to retrieve base URL for Mimecast integration.")
133 + logger.error(
134 + f"Unable to retrieve base URL for Mimecast integration. Exception: {e}",
135 + )
136 + raise HTTPException(
137 + status_code=400,
138 + detail="Unable to retrieve base URL for Mimecast integration.",
139 + )
140
141
124 -async def get_mta_siem_logs(checkpoint_filename: str, base_url: str, auth_keys: MimecastAuthKeys):
142 +async def get_mta_siem_logs(
143 + checkpoint_filename: str,
144 + base_url: str,
145 + auth_keys: MimecastAuthKeys,
146 +):
147 """
148 Retrieves the MTA SIEM logs from the Mimecast integration.
149 """
@@ -163,8 +185,13 @@ async def get_mta_siem_logs(checkpoint_filename: str, base_url: str, auth_keys:
185 )
186 return response.content, response.headers
187 except Exception as e:
166 - logger.error(f"Unable to retrieve MTA SIEM logs from Mimecast integration. Exception: {e}")
167 - raise HTTPException(status_code=400, detail="Unable to retrieve MTA SIEM logs from Mimecast integration.")
188 + logger.error(
189 + f"Unable to retrieve MTA SIEM logs from Mimecast integration. Exception: {e}",
190 + )
191 + raise HTTPException(
192 + status_code=400,
193 + detail="Unable to retrieve MTA SIEM logs from Mimecast integration.",
194 + )
195
196
197 async def process_response(response, checkpoint_filename: str, log_file_path: str):
@@ -187,7 +214,10 @@ async def process_response(response, checkpoint_filename: str, log_file_path: st
214 file_name = file_name[1][:-1]
215
216 # Save mc-siem-token page token to check point directory
190 - await write_checkpoint_file(checkpoint_filename, resp_headers["mc-siem-token"])
217 + await write_checkpoint_file(
218 + checkpoint_filename,
219 + resp_headers["mc-siem-token"],
220 + )
221 log_filename = os.path.join(log_file_path, file_name)
222 await write_log_file(log_filename, resp_body)
223 return None
@@ -218,7 +248,12 @@ async def write_log_file(filename: str, resp_body):
248 await f.write(resp_body)
249
250
221 -async def process_log_file(filename: str, filename2: str, log_file_path: str, customer_code: str):
251 +async def process_log_file(
252 + filename: str,
253 + filename2: str,
254 + log_file_path: str,
255 + customer_code: str,
256 +):
257 """
258 Process a log file by reading its contents and shipping events.
259 """
@@ -291,10 +326,16 @@ async def delete_log_directory(log_file_path: str):
326 shutil.rmtree(log_file_path)
327 logger.info(f"Successfully deleted the directory: {log_file_path}")
328 except OSError as e:
294 - raise HTTPException(status_code=400, detail=f"Error: {e.strerror}. Directory: {log_file_path}")
329 + raise HTTPException(
330 + status_code=400,
331 + detail=f"Error: {e.strerror}. Directory: {log_file_path}",
332 + )
333
334
297 -async def invoke_mimecast(mimecast_request: MimecastRequest, auth_keys: MimecastAuthKeys) -> MimecastResponse:
335 +async def invoke_mimecast(
336 + mimecast_request: MimecastRequest,
337 + auth_keys: MimecastAuthKeys,
338 +) -> MimecastResponse:
339 """
340 Invokes the Mimecast integration.
341 """
@@ -302,23 +343,40 @@ async def invoke_mimecast(mimecast_request: MimecastRequest, auth_keys: Mimecast
343 try:
344 logger.info(f"mimecast_base_url: {mimecast_base_url.data.data[0].region.api}")
345 except Exception as e:
305 - logger.error(f"Unable to retrieve base URL for Mimecast integration. Exception: {e}")
306 - raise HTTPException(status_code=400, detail="Unable to retrieve base URL for Mimecast integration.")
346 + logger.error(
347 + f"Unable to retrieve base URL for Mimecast integration. Exception: {e}",
348 + )
349 + raise HTTPException(
350 + status_code=400,
351 + detail="Unable to retrieve base URL for Mimecast integration.",
352 + )
353 checkpoint_filename = await get_checkpoint_filename(mimecast_request.customer_code)
354 log_file_path = await get_log_file_path(mimecast_request.customer_code)
309 - response = await get_mta_siem_logs(checkpoint_filename, mimecast_base_url.data.data[0].region.api, auth_keys)
355 + response = await get_mta_siem_logs(
356 + checkpoint_filename,
357 + mimecast_base_url.data.data[0].region.api,
358 + auth_keys,
359 + )
360
361 await process_response(response, checkpoint_filename, log_file_path)
362 for filename in os.listdir(log_file_path):
363 if os.path.isdir(os.path.join(log_file_path, filename)):
364 for filename2 in os.listdir(os.path.join(log_file_path, filename)):
315 - await process_log_file(filename, filename2, log_file_path, customer_code=mimecast_request.customer_code)
365 + await process_log_file(
366 + filename,
367 + filename2,
368 + log_file_path,
369 + customer_code=mimecast_request.customer_code,
370 + )
371 logger.info(f"Log file path: {log_file_path}")
372 else:
373 await process_log_file(filename, filename2, log_file_path)
374
375 await delete_log_directory(log_file_path)
321 - return MimecastResponse(success=True, message="Successfully invoked Mimecast integration.")
376 + return MimecastResponse(
377 + success=True,
378 + message="Successfully invoked Mimecast integration.",
379 + )
380
381
382 # ! TTP URLS ! #
@@ -367,7 +425,10 @@ async def invoke_mimecast_api_ttp_urls(
425 return TtpURLResponseBody(**response.json())
426
427
370 -async def get_ttp_urls(mimecast_request: MimecastTTPURLSRequest, customer_code: str) -> MimecastResponse:
428 +async def get_ttp_urls(
429 + mimecast_request: MimecastTTPURLSRequest,
430 + customer_code: str,
431 +) -> MimecastResponse:
432 logger.info("Mimecast TTP URL request received")
433 # Get the BaseURL for the Mimecast integration
434 mimecast_base_url = await get_base_url(
backend/app/integrations/mimecast/services/provision.py
+122 -42
@@ -1,37 +1,47 @@
1 import json
2 from datetime import datetime
3
4 -from loguru import logger
5 -from sqlalchemy import and_
6 -from sqlalchemy import update
7 -from sqlalchemy.ext.asyncio import AsyncSession
8 -
9 -from app.connectors.grafana.schema.dashboards import DashboardProvisionRequest
10 -from app.connectors.grafana.schema.dashboards import MimecastDashboard
4 +from app.connectors.grafana.schema.dashboards import (
5 + DashboardProvisionRequest,
6 + MimecastDashboard,
7 +)
8 from app.connectors.grafana.services.dashboards import provision_dashboards
9 from app.connectors.grafana.utils.universal import create_grafana_client
10 from app.connectors.graylog.services.management import start_stream
11 from app.connectors.graylog.utils.universal import send_post_request
15 -from app.customer_provisioning.schema.grafana import GrafanaDatasource
16 -from app.customer_provisioning.schema.grafana import GrafanaDataSourceCreationResponse
17 -from app.customer_provisioning.schema.graylog import GraylogIndexSetCreationResponse
18 -from app.customer_provisioning.schema.graylog import StreamCreationResponse
19 -from app.customer_provisioning.schema.graylog import TimeBasedIndexSet
20 -from app.customer_provisioning.services.grafana import create_grafana_folder
21 -from app.customer_provisioning.services.grafana import get_opensearch_version
22 -from app.customers.routes.customers import get_customer
23 -from app.customers.routes.customers import get_customer_meta
24 -from app.integrations.mimecast.schema.provision import MimecastEventStream
25 -from app.integrations.mimecast.schema.provision import ProvisionMimecastRequest
26 -from app.integrations.mimecast.schema.provision import ProvisionMimecastResponse
12 +from app.customer_provisioning.schema.grafana import (
13 + GrafanaDatasource,
14 + GrafanaDataSourceCreationResponse,
15 +)
16 +from app.customer_provisioning.schema.graylog import (
17 + GraylogIndexSetCreationResponse,
18 + StreamCreationResponse,
19 + TimeBasedIndexSet,
20 +)
21 +from app.customer_provisioning.services.grafana import (
22 + create_grafana_folder,
23 + get_opensearch_version,
24 +)
25 +from app.customers.routes.customers import get_customer, get_customer_meta
26 +from app.integrations.mimecast.schema.provision import (
27 + MimecastEventStream,
28 + ProvisionMimecastRequest,
29 + ProvisionMimecastResponse,
30 +)
31 from app.integrations.models.customer_integration_settings import CustomerIntegrations
32 from app.integrations.routes import create_integration_meta
33 from app.integrations.schema import CustomerIntegrationsMetaSchema
34 from app.utils import get_connector_attribute
35 +from loguru import logger
36 +from sqlalchemy import and_, update
37 +from sqlalchemy.ext.asyncio import AsyncSession
38
39
40 ################## ! GRAYLOG ! ##################
34 -async def build_index_set_config(customer_code: str, session: AsyncSession) -> TimeBasedIndexSet:
41 +async def build_index_set_config(
42 + customer_code: str,
43 + session: AsyncSession,
44 +) -> TimeBasedIndexSet:
45 """
46 Build the configuration for a time-based index set.
47
@@ -69,7 +79,9 @@ async def build_index_set_config(customer_code: str, session: AsyncSession) -> T
79
80
81 # Function to send the POST request and handle the response
72 -async def send_index_set_creation_request(index_set: TimeBasedIndexSet) -> GraylogIndexSetCreationResponse:
82 +async def send_index_set_creation_request(
83 + index_set: TimeBasedIndexSet,
84 +) -> GraylogIndexSetCreationResponse:
85 """
86 Sends a request to create an index set in Graylog.
87
@@ -81,11 +93,17 @@ async def send_index_set_creation_request(index_set: TimeBasedIndexSet) -> Grayl
93 """
94 json_index_set = json.dumps(index_set.dict())
95 logger.info(f"json_index_set set: {json_index_set}")
84 - response_json = await send_post_request(endpoint="/api/system/indices/index_sets", data=index_set.dict())
96 + response_json = await send_post_request(
97 + endpoint="/api/system/indices/index_sets",
98 + data=index_set.dict(),
99 + )
100 return GraylogIndexSetCreationResponse(**response_json)
101
102
88 -async def create_index_set(customer_code: str, session: AsyncSession) -> GraylogIndexSetCreationResponse:
103 +async def create_index_set(
104 + customer_code: str,
105 + session: AsyncSession,
106 +) -> GraylogIndexSetCreationResponse:
107 """
108 Creates an index set for a new customer.
109
@@ -142,7 +160,9 @@ async def build_event_stream_config(
160 )
161
162
145 -async def send_event_stream_creation_request(event_stream: MimecastEventStream) -> StreamCreationResponse:
163 +async def send_event_stream_creation_request(
164 + event_stream: MimecastEventStream,
165 +) -> StreamCreationResponse:
166 """
167 Sends a request to create an event stream.
168
@@ -154,7 +174,10 @@ async def send_event_stream_creation_request(event_stream: MimecastEventStream)
174 """
175 json_event_stream = json.dumps(event_stream.dict())
176 logger.info(f"json_event_stream set: {json_event_stream}")
157 - response_json = await send_post_request(endpoint="/api/streams", data=event_stream.dict())
177 + response_json = await send_post_request(
178 + endpoint="/api/streams",
179 + data=event_stream.dict(),
180 + )
181 return StreamCreationResponse(**response_json)
182
183
@@ -173,7 +196,11 @@ async def create_event_stream(
196 Returns:
197 The result of the event stream creation request.
198 """
176 - event_stream_config = await build_event_stream_config(customer_code, index_set_id, session)
199 + event_stream_config = await build_event_stream_config(
200 + customer_code,
201 + index_set_id,
202 + session,
203 + )
204 return await send_event_stream_creation_request(event_stream_config)
205
206
@@ -196,19 +223,33 @@ async def create_grafana_datasource(
223 grafana_client = await create_grafana_client("Grafana")
224 # Switch to the newly created organization
225 grafana_client.user.switch_actual_user_organisation(
199 - (await get_customer_meta(customer_code, session)).customer_meta.customer_meta_grafana_org_id,
226 + (
227 + await get_customer_meta(customer_code, session)
228 + ).customer_meta.customer_meta_grafana_org_id,
229 )
230 datasource_payload = GrafanaDatasource(
231 name="MIMECAST",
232 type="grafana-opensearch-datasource",
233 typeName="OpenSearch",
234 access="proxy",
206 - url=await get_connector_attribute(connector_id=1, column_name="connector_url", session=session),
235 + url=await get_connector_attribute(
236 + connector_id=1,
237 + column_name="connector_url",
238 + session=session,
239 + ),
240 database=f"mimecast_{customer_code}*",
241 basicAuth=True,
209 - basicAuthUser=await get_connector_attribute(connector_id=1, column_name="connector_username", session=session),
242 + basicAuthUser=await get_connector_attribute(
243 + connector_id=1,
244 + column_name="connector_username",
245 + session=session,
246 + ),
247 secureJsonData={
211 - "basicAuthPassword": await get_connector_attribute(connector_id=1, column_name="connector_password", session=session),
248 + "basicAuthPassword": await get_connector_attribute(
249 + connector_id=1,
250 + column_name="connector_password",
251 + session=session,
252 + ),
253 },
254 isDefault=False,
255 jsonData={
@@ -231,7 +272,10 @@ async def create_grafana_datasource(
272 return GrafanaDataSourceCreationResponse(**results)
273
274
234 -async def provision_mimecast(provision_mimecast_request: ProvisionMimecastRequest, session: AsyncSession) -> ProvisionMimecastResponse:
275 +async def provision_mimecast(
276 + provision_mimecast_request: ProvisionMimecastRequest,
277 + session: AsyncSession,
278 +) -> ProvisionMimecastResponse:
279 """
280 Provisions Mimecast integration for a customer.
281
@@ -242,24 +286,43 @@ async def provision_mimecast(provision_mimecast_request: ProvisionMimecastReques
286 Returns:
287 ProvisionMimecastResponse: The response object containing the result of the provisioning.
288 """
245 - logger.info(f"Provisioning Mimecast integration for customer {provision_mimecast_request.customer_code}.")
289 + logger.info(
290 + f"Provisioning Mimecast integration for customer {provision_mimecast_request.customer_code}.",
291 + )
292
293 # Create Index Set
248 - index_set_id = (await create_index_set(customer_code=provision_mimecast_request.customer_code, session=session)).data.id
294 + index_set_id = (
295 + await create_index_set(
296 + customer_code=provision_mimecast_request.customer_code,
297 + session=session,
298 + )
299 + ).data.id
300 logger.info(f"Index set: {index_set_id}")
301 # Create event stream
251 - stream_id = (await create_event_stream(provision_mimecast_request.customer_code, index_set_id, session)).data.stream_id
302 + stream_id = (
303 + await create_event_stream(
304 + provision_mimecast_request.customer_code,
305 + index_set_id,
306 + session,
307 + )
308 + ).data.stream_id
309 # Start stream
310 await start_stream(stream_id=stream_id)
311
312 # Grafana Deployment
313 mimecast_datasource_uid = (
257 - await create_grafana_datasource(customer_code=provision_mimecast_request.customer_code, session=session)
314 + await create_grafana_datasource(
315 + customer_code=provision_mimecast_request.customer_code,
316 + session=session,
317 + )
318 ).datasource.uid
319 grafana_mimecast_folder_id = (
320 await create_grafana_folder(
321 organization_id=(
262 - await get_customer_meta(provision_mimecast_request.customer_code, session)
322 + await get_customer_meta(
323 + provision_mimecast_request.customer_code,
324 + session,
325 + )
326 ).customer_meta.customer_meta_grafana_org_id,
327 folder_title="MIMECAST",
328 )
@@ -268,7 +331,10 @@ async def provision_mimecast(provision_mimecast_request: ProvisionMimecastReques
331 DashboardProvisionRequest(
332 dashboards=[dashboard.name for dashboard in MimecastDashboard],
333 organizationId=(
271 - await get_customer_meta(provision_mimecast_request.customer_code, session)
334 + await get_customer_meta(
335 + provision_mimecast_request.customer_code,
336 + session,
337 + )
338 ).customer_meta.customer_meta_grafana_org_id,
339 folderId=grafana_mimecast_folder_id,
340 datasourceUid=mimecast_datasource_uid,
@@ -282,15 +348,24 @@ async def provision_mimecast(provision_mimecast_request: ProvisionMimecastReques
348 graylog_index_id=index_set_id,
349 graylog_stream_id=stream_id,
350 grafana_org_id=(
285 - await get_customer_meta(provision_mimecast_request.customer_code, session)
351 + await get_customer_meta(
352 + provision_mimecast_request.customer_code,
353 + session,
354 + )
355 ).customer_meta.customer_meta_grafana_org_id,
356 grafana_dashboard_folder_id=grafana_mimecast_folder_id,
357 ),
358 session,
359 )
291 - await update_customer_integration_table(provision_mimecast_request.customer_code, session)
360 + await update_customer_integration_table(
361 + provision_mimecast_request.customer_code,
362 + session,
363 + )
364
293 - return ProvisionMimecastResponse(success=True, message="Mimecast integration provisioned.")
365 + return ProvisionMimecastResponse(
366 + success=True,
367 + message="Mimecast integration provisioned.",
368 + )
369
370
371 ############## ! WRITE TO DB ! ##############
@@ -306,10 +381,15 @@ async def create_integration_meta_entry(
381 session (AsyncSession): The async session object for database operations.
382 """
383 await create_integration_meta(customer_integration_meta, session)
309 - logger.info(f"Integration meta entry created for customer {customer_integration_meta.customer_code}.")
384 + logger.info(
385 + f"Integration meta entry created for customer {customer_integration_meta.customer_code}.",
386 + )
387
388
312 -async def update_customer_integration_table(customer_code: str, session: AsyncSession) -> None:
389 +async def update_customer_integration_table(
390 + customer_code: str,
391 + session: AsyncSession,
392 +) -> None:
393 """
394 Updates the `customer_integrations` table to set the `deployed` column to True where the `customer_code`
395 matches the given customer code and the `integration_service_name` is "Mimecast".
backend/app/integrations/models/customer_integration_settings.py
+38 -16
@@ -1,10 +1,7 @@
1 -from typing import List
2 -from typing import Optional
1 +from typing import List, Optional
2
3 from sqlalchemy import Text
5 -from sqlmodel import Field
6 -from sqlmodel import Relationship
7 -from sqlmodel import SQLModel
4 +from sqlmodel import Field, Relationship, SQLModel
5
6
7 class AvailableIntegrations(SQLModel, table=True):
@@ -14,7 +11,9 @@ class AvailableIntegrations(SQLModel, table=True):
11 description: str = Field(max_length=1024)
12 integration_details: str = Field(sa_column=Text)
13 # Relationships
17 - auth_keys: List["AvailableIntegrationsAuthKeys"] = Relationship(back_populates="integration")
14 + auth_keys: List["AvailableIntegrationsAuthKeys"] = Relationship(
15 + back_populates="integration",
16 + )
17
18
19 class AvailableIntegrationsAuthKeys(SQLModel, table=True):
@@ -36,7 +35,9 @@ class CustomerIntegrations(SQLModel, table=True):
35 integration_service_name: str = Field(max_length=255, nullable=False)
36 deployed: bool = Field(default=False)
37 # Relationships
39 - integration_subscriptions: List["IntegrationSubscription"] = Relationship(back_populates="customer_integrations")
38 + integration_subscriptions: List["IntegrationSubscription"] = Relationship(
39 + back_populates="customer_integrations",
40 + )
41
42
43 class IntegrationService(SQLModel, table=True):
@@ -45,25 +46,41 @@ class IntegrationService(SQLModel, table=True):
46 service_name: str = Field(max_length=255, nullable=False)
47 auth_type: str = Field(max_length=50) # e.g., OAuth, API Key, etc.
48 # Relationships
48 - integration_subscriptions: List["IntegrationSubscription"] = Relationship(back_populates="integration_service")
49 - configs: List["IntegrationConfig"] = Relationship(back_populates="integration_service")
49 + integration_subscriptions: List["IntegrationSubscription"] = Relationship(
50 + back_populates="integration_service",
51 + )
52 + configs: List["IntegrationConfig"] = Relationship(
53 + back_populates="integration_service",
54 + )
55
56
57 class IntegrationSubscription(SQLModel, table=True):
58 __tablename__ = "integration_subscriptions"
59 id: Optional[int] = Field(default=None, primary_key=True)
60 customer_id: int = Field(default=None, foreign_key="customer_integrations.id")
56 - integration_service_id: int = Field(default=None, foreign_key="integration_services.id")
61 + integration_service_id: int = Field(
62 + default=None,
63 + foreign_key="integration_services.id",
64 + )
65 # Relationships
58 - customer_integrations: "CustomerIntegrations" = Relationship(back_populates="integration_subscriptions")
59 - integration_service: "IntegrationService" = Relationship(back_populates="integration_subscriptions")
60 - integration_auth_keys: List["IntegrationAuthKeys"] = Relationship(back_populates="integration_subscription") # Moved here
66 + customer_integrations: "CustomerIntegrations" = Relationship(
67 + back_populates="integration_subscriptions",
68 + )
69 + integration_service: "IntegrationService" = Relationship(
70 + back_populates="integration_subscriptions",
71 + )
72 + integration_auth_keys: List["IntegrationAuthKeys"] = Relationship(
73 + back_populates="integration_subscription",
74 + ) # Moved here
75
76
77 class IntegrationConfig(SQLModel, table=True):
78 __tablename__ = "integration_configs"
79 id: Optional[int] = Field(default=None, primary_key=True)
66 - integration_service_id: int = Field(default=None, foreign_key="integration_services.id")
80 + integration_service_id: int = Field(
81 + default=None,
82 + foreign_key="integration_services.id",
83 + )
84 config_key: str = Field(max_length=255) # e.g., 'endpoint', 'port', etc.
85 config_value: str = Field(max_length=1024) # e.g., 'https://api.service.com/v1'
86 # Relationships
@@ -73,11 +90,16 @@ class IntegrationConfig(SQLModel, table=True):
90 class IntegrationAuthKeys(SQLModel, table=True):
91 __tablename__ = "integration_auth_keys"
92 id: Optional[int] = Field(default=None, primary_key=True)
76 - subscription_id: int = Field(default=None, foreign_key="integration_subscriptions.id")
93 + subscription_id: int = Field(
94 + default=None,
95 + foreign_key="integration_subscriptions.id",
96 + )
97 auth_key_name: str = Field(max_length=255) # e.g., 'credentials', 'rate_limit'
98 auth_value: str = Field(max_length=1024) # e.g., JSON/encrypted credentials
99 # Relationships
80 - integration_subscription: "IntegrationSubscription" = Relationship(back_populates="integration_auth_keys") # Adjusted relationship
100 + integration_subscription: "IntegrationSubscription" = Relationship(
101 + back_populates="integration_auth_keys",
102 + ) # Adjusted relationship
103
104
105 class CustomerIntegrationsMeta(SQLModel, table=True):
backend/app/integrations/monitoring_alert/models/monitoring_alert.py
+1 -2
@@ -1,7 +1,6 @@
1 from typing import Optional
2
3 -from sqlmodel import Field
4 -from sqlmodel import SQLModel
3 +from sqlmodel import Field, SQLModel
4
5
6 class MonitoringAlerts(SQLModel, table=True):
backend/app/integrations/monitoring_alert/routes/monitoring_alert.py
+35 -24
@@ -1,32 +1,22 @@
1 from typing import List
2
3 -from fastapi import APIRouter
4 -from fastapi import Depends
5 -from fastapi import HTTPException
6 -from fastapi import Security
7 -from loguru import logger
8 -from sqlalchemy.ext.asyncio import AsyncSession
9 -from sqlalchemy.future import select
10 -
3 from app.auth.utils import AuthHandler
4 from app.db.db_session import get_db
5 from app.db.universal_models import CustomersMeta
6 from app.integrations.monitoring_alert.models.monitoring_alert import MonitoringAlerts
15 -from app.integrations.monitoring_alert.schema.monitoring_alert import GraylogPostRequest
7 from app.integrations.monitoring_alert.schema.monitoring_alert import (
8 + GraylogPostRequest,
9 GraylogPostResponse,
18 -)
19 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
10 MonitoringAlertsRequestModel,
21 -)
22 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
11 MonitoringWazuhAlertsRequestModel,
24 -)
25 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
12 WazuhAnalysisResponse,
13 )
14 from app.integrations.monitoring_alert.services.suricata import analyze_suricata_alerts
15 from app.integrations.monitoring_alert.services.wazuh import analyze_wazuh_alerts
16 +from fastapi import APIRouter, Depends, HTTPException, Security
17 +from loguru import logger
18 +from sqlalchemy.ext.asyncio import AsyncSession
19 +from sqlalchemy.future import select
20
21 monitoring_alerts_router = APIRouter()
22
@@ -44,7 +34,9 @@ async def get_customer_meta(customer_code: str, session: AsyncSession) -> Custom
34 """
35 logger.info(f"Getting customer meta for customer_code: {customer_code}")
36
47 - customer_meta = await session.execute(select(CustomersMeta).where(CustomersMeta.customer_code == customer_code))
37 + customer_meta = await session.execute(
38 + select(CustomersMeta).where(CustomersMeta.customer_code == customer_code),
39 + )
40 customer_meta = customer_meta.scalars().first()
41
42 if not customer_meta:
@@ -97,7 +89,9 @@ async def create_monitoring_alert(
89 logger.info(f"Found index name {monitoring_alert.event.alert_index}")
90
91 customer_meta = await session.execute(
100 - select(CustomersMeta).where(CustomersMeta.customer_code == monitoring_alert.event.fields.CUSTOMER_CODE),
92 + select(CustomersMeta).where(
93 + CustomersMeta.customer_code == monitoring_alert.event.fields.CUSTOMER_CODE,
94 + ),
95 )
96 customer_meta = customer_meta.scalars().first()
97
@@ -118,10 +112,16 @@ async def create_monitoring_alert(
112 logger.error(f"Error creating monitoring alert: {e}")
113 raise HTTPException(status_code=500, detail="Error creating monitoring alert")
114
121 - return GraylogPostResponse(success=True, message="Monitoring alert created successfully")
115 + return GraylogPostResponse(
116 + success=True,
117 + message="Monitoring alert created successfully",
118 + )
119
120
124 -@monitoring_alerts_router.post("/run_analysis/wazuh", response_model=WazuhAnalysisResponse)
121 +@monitoring_alerts_router.post(
122 + "/run_analysis/wazuh",
123 + response_model=WazuhAnalysisResponse,
124 +)
125 async def run_wazuh_analysis(
126 request: MonitoringWazuhAlertsRequestModel,
127 session: AsyncSession = Depends(get_db),
@@ -147,7 +147,8 @@ async def run_wazuh_analysis(
147
148 monitoring_alerts = await session.execute(
149 select(MonitoringAlerts).where(
150 - (MonitoringAlerts.customer_code == request.customer_code) & (MonitoringAlerts.alert_source == "WAZUH"),
150 + (MonitoringAlerts.customer_code == request.customer_code)
151 + & (MonitoringAlerts.alert_source == "WAZUH"),
152 ),
153 )
154 monitoring_alerts = monitoring_alerts.scalars().all()
@@ -160,10 +161,16 @@ async def run_wazuh_analysis(
161 # Call the analyze_wazuh_alerts function to analyze the alerts
162 await analyze_wazuh_alerts(monitoring_alerts, customer_meta, session)
163
163 - return WazuhAnalysisResponse(success=True, message="Analysis completed successfully")
164 + return WazuhAnalysisResponse(
165 + success=True,
166 + message="Analysis completed successfully",
167 + )
168
169
166 -@monitoring_alerts_router.post("/run_analysis/suricata", response_model=WazuhAnalysisResponse)
170 +@monitoring_alerts_router.post(
171 + "/run_analysis/suricata",
172 + response_model=WazuhAnalysisResponse,
173 +)
174 async def run_suricata_analysis(
175 request: MonitoringWazuhAlertsRequestModel,
176 session: AsyncSession = Depends(get_db),
@@ -189,7 +196,8 @@ async def run_suricata_analysis(
196
197 monitoring_alerts = await session.execute(
198 select(MonitoringAlerts).where(
192 - (MonitoringAlerts.customer_code == request.customer_code) & (MonitoringAlerts.alert_source == "SURICATA"),
199 + (MonitoringAlerts.customer_code == request.customer_code)
200 + & (MonitoringAlerts.alert_source == "SURICATA"),
201 ),
202 )
203 monitoring_alerts = monitoring_alerts.scalars().all()
@@ -202,4 +210,7 @@ async def run_suricata_analysis(
210 # Call the analyze_wazuh_alerts function to analyze the alerts
211 await analyze_suricata_alerts(monitoring_alerts, customer_meta, session)
212
205 - return WazuhAnalysisResponse(success=True, message="Analysis completed successfully")
213 + return WazuhAnalysisResponse(
214 + success=True,
215 + message="Analysis completed successfully",
216 + )
backend/app/integrations/monitoring_alert/routes/provision.py
+48 -23
@@ -1,35 +1,29 @@
1 -from fastapi import APIRouter
2 -from fastapi import HTTPException
3 -from loguru import logger
4 -
1 from app.connectors.graylog.routes.events import get_all_event_definitions
2 from app.connectors.graylog.schema.events import GraylogEventDefinitionsResponse
7 -from app.integrations.monitoring_alert.schema.provision import AvailableMonitoringAlerts
3 from app.integrations.monitoring_alert.schema.provision import (
4 + AvailableMonitoringAlerts,
5 AvailableMonitoringAlertsResponse,
10 -)
11 -from app.integrations.monitoring_alert.schema.provision import (
6 ProvisionMonitoringAlertRequest,
13 -)
14 -from app.integrations.monitoring_alert.schema.provision import (
7 ProvisionWazuhMonitoringAlertResponse,
8 )
9 from app.integrations.monitoring_alert.services.provision import (
10 provision_suricata_monitoring_alert,
19 -)
20 -from app.integrations.monitoring_alert.services.provision import (
11 provision_wazuh_monitoring_alert,
12 )
13 from app.integrations.utils.event_shipper import event_shipper
14 from app.integrations.utils.schema import EventShipperPayload
15 from app.schedulers.models.scheduler import CreateSchedulerRequest
16 from app.schedulers.scheduler import add_scheduler_jobs
17 +from fastapi import APIRouter, HTTPException
18 +from loguru import logger
19
20 monitoring_alerts_provision_router = APIRouter()
21
22
23 # Define your provision functions
32 -async def invoke_provision_wazuh_monitoring_alert(request: ProvisionMonitoringAlertRequest):
24 +async def invoke_provision_wazuh_monitoring_alert(
25 + request: ProvisionMonitoringAlertRequest,
26 +):
27 # Provision the Wazuh monitoring alert
28 await provision_wazuh_monitoring_alert(request)
29 await add_scheduler_jobs(
@@ -41,7 +35,9 @@ async def invoke_provision_wazuh_monitoring_alert(request: ProvisionMonitoringAl
35 )
36
37
44 -async def invoke_provision_suricata_monitoring_alert(request: ProvisionMonitoringAlertRequest):
38 +async def invoke_provision_suricata_monitoring_alert(
39 + request: ProvisionMonitoringAlertRequest,
40 +):
41 # Provision the Suricata monitoring alert
42 await provision_suricata_monitoring_alert(request)
43 await add_scheduler_jobs(
@@ -73,11 +69,24 @@ async def check_if_event_definition_exists(event_definition: str) -> bool:
69 """
70 event_definitions_response = await get_all_event_definitions()
71 if not event_definitions_response.success:
76 - raise HTTPException(status_code=500, detail="Failed to collect event definitions")
77 - event_definitions_response = GraylogEventDefinitionsResponse(**event_definitions_response.dict())
78 - logger.info(f"Event definitions collected: {event_definitions_response.event_definitions}")
79 - if event_definition in [event_definition.title for event_definition in event_definitions_response.event_definitions]:
80 - raise HTTPException(status_code=400, detail=f"Event definition {event_definition} already exists")
72 + raise HTTPException(
73 + status_code=500,
74 + detail="Failed to collect event definitions",
75 + )
76 + event_definitions_response = GraylogEventDefinitionsResponse(
77 + **event_definitions_response.dict(),
78 + )
79 + logger.info(
80 + f"Event definitions collected: {event_definitions_response.event_definitions}",
81 + )
82 + if event_definition in [
83 + event_definition.title
84 + for event_definition in event_definitions_response.event_definitions
85 + ]:
86 + raise HTTPException(
87 + status_code=400,
88 + detail=f"Event definition {event_definition} already exists",
89 + )
90 return False
91
92
@@ -90,8 +99,15 @@ async def get_available_monitoring_alerts_route() -> AvailableMonitoringAlertsRe
99 """
100 Get the available monitoring alerts.
101 """
93 - alerts = [{"name": alert.name.replace("_", " "), "value": alert.value} for alert in AvailableMonitoringAlerts]
94 - return AvailableMonitoringAlertsResponse(success=True, message="Alerts retrieved successfully", available_monitoring_alerts=alerts)
102 + alerts = [
103 + {"name": alert.name.replace("_", " "), "value": alert.value}
104 + for alert in AvailableMonitoringAlerts
105 + ]
106 + return AvailableMonitoringAlertsResponse(
107 + success=True,
108 + message="Alerts retrieved successfully",
109 + available_monitoring_alerts=alerts,
110 + )
111
112
113 @monitoring_alerts_provision_router.post(
@@ -108,12 +124,18 @@ async def provision_monitoring_alert_route(
124 provision_function = PROVISION_FUNCTIONS.get(request.alert_name)
125
126 if provision_function is None:
111 - raise HTTPException(status_code=400, detail=f"No provision function found for alert name {request.alert_name}")
127 + raise HTTPException(
128 + status_code=400,
129 + detail=f"No provision function found for alert name {request.alert_name}",
130 + )
131
132 # Invoke the provision function
133 await provision_function(request)
134
116 - return ProvisionWazuhMonitoringAlertResponse(success=True, message="Wazuh monitoring alerts provisioned.")
135 + return ProvisionWazuhMonitoringAlertResponse(
136 + success=True,
137 + message="Wazuh monitoring alerts provisioned.",
138 + )
139
140
141 @monitoring_alerts_provision_router.post(
@@ -134,4 +156,7 @@ async def provision_monitoring_alert_testing_route(
156 **request,
157 )
158 await event_shipper(message)
137 - return ProvisionWazuhMonitoringAlertResponse(success=True, message="Event sent to log shipper successfully.")
159 + return ProvisionWazuhMonitoringAlertResponse(
160 + success=True,
161 + message="Event sent to log shipper successfully.",
162 + )
backend/app/integrations/monitoring_alert/schema/monitoring_alert.py
+142 -38
@@ -1,15 +1,8 @@
1 from enum import Enum
2 -from typing import Any
3 -from typing import Dict
4 -from typing import List
5 -from typing import Optional
2 +from typing import Any, Dict, List, Optional
3
7 -from pydantic import BaseModel
8 -from pydantic import Extra
9 -from pydantic import Field
10 -
11 -from app.integrations.alert_creation.general.schema.alert import IrisAsset
12 -from app.integrations.alert_creation.general.schema.alert import IrisIoc
4 +from app.integrations.alert_creation.general.schema.alert import IrisAsset, IrisIoc
5 +from pydantic import BaseModel, Extra, Field
6
7
8 class MonitoringAlertsRequestModel(BaseModel):
@@ -28,34 +21,94 @@ class MonitoringWazuhAlertsRequestModel(BaseModel):
21
22
23 class GraylogEventFields(BaseModel):
31 - ALERT_ID: str = Field(..., description="Unique identifier for the alert", example="65f6a260-c1f3-11ee-93bc-86000046278a")
24 + ALERT_ID: str = Field(
25 + ...,
26 + description="Unique identifier for the alert",
27 + example="65f6a260-c1f3-11ee-93bc-86000046278a",
28 + )
29 ALERT_SOURCE: str = Field(..., description="Source of the alert", example="WAZUH")
33 - CUSTOMER_CODE: str = Field(..., description="Customer code associated with the alert", example="00002")
30 + CUSTOMER_CODE: str = Field(
31 + ...,
32 + description="Customer code associated with the alert",
33 + example="00002",
34 + )
35
36
37 class GraylogEvent(BaseModel):
37 - id: str = Field(..., description="Unique identifier for the event", example="01HNNF2YCM5SSV3KDQJSRK0EV0")
38 - event_definition_type: str = Field(..., description="Type of event definition", example="aggregation-v1")
39 - event_definition_id: str = Field(..., description="Identifier for the event definition", example="65bd28505e9a2d550cf521e7")
38 + id: str = Field(
39 + ...,
40 + description="Unique identifier for the event",
41 + example="01HNNF2YCM5SSV3KDQJSRK0EV0",
42 + )
43 + event_definition_type: str = Field(
44 + ...,
45 + description="Type of event definition",
46 + example="aggregation-v1",
47 + )
48 + event_definition_id: str = Field(
49 + ...,
50 + description="Identifier for the event definition",
51 + example="65bd28505e9a2d550cf521e7",
52 + )
53 origin_context: str = Field(
54 ...,
55 description="Context from which the event originated",
56 example="urn:graylog:message:es:wazuh_00002_290:65f6a260-c1f3-11ee-93bc-86000046278a",
57 )
45 - timestamp: str = Field(..., description="Timestamp when the event occurred", example="2024-02-02T17:49:22.694Z")
46 - timestamp_processing: str = Field(..., description="Timestamp when the event was processed", example="2024-02-02T17:50:26.708Z")
47 - timerange_start: Optional[str] = Field(None, description="Start of the timerange for the event", example=None)
48 - timerange_end: Optional[str] = Field(None, description="End of the timerange for the event", example=None)
49 - streams: List[str] = Field(..., description="List of streams associated with the event", example=[])
50 - source_streams: List[str] = Field(..., description="List of source streams for the event", example=["645a3a6123e5cc30bbc0e5dc"])
51 - message: str = Field(..., description="Message associated with the event", example="COPILOT TESTING WAZUH")
58 + timestamp: str = Field(
59 + ...,
60 + description="Timestamp when the event occurred",
61 + example="2024-02-02T17:49:22.694Z",
62 + )
63 + timestamp_processing: str = Field(
64 + ...,
65 + description="Timestamp when the event was processed",
66 + example="2024-02-02T17:50:26.708Z",
67 + )
68 + timerange_start: Optional[str] = Field(
69 + None,
70 + description="Start of the timerange for the event",
71 + example=None,
72 + )
73 + timerange_end: Optional[str] = Field(
74 + None,
75 + description="End of the timerange for the event",
76 + example=None,
77 + )
78 + streams: List[str] = Field(
79 + ...,
80 + description="List of streams associated with the event",
81 + example=[],
82 + )
83 + source_streams: List[str] = Field(
84 + ...,
85 + description="List of source streams for the event",
86 + example=["645a3a6123e5cc30bbc0e5dc"],
87 + )
88 + message: str = Field(
89 + ...,
90 + description="Message associated with the event",
91 + example="COPILOT TESTING WAZUH",
92 + )
93 source: str = Field(..., description="Source of the event", example="ASHGRL02")
53 - key_tuple: List[str] = Field(..., description="Tuple keys associated with the event", example=[])
94 + key_tuple: List[str] = Field(
95 + ...,
96 + description="Tuple keys associated with the event",
97 + example=[],
98 + )
99 key: str = Field(..., description="Key associated with the event", example="")
100 priority: int = Field(..., description="Priority of the event", example=2)
56 - alert: bool = Field(..., description="Indicates if the event is an alert", example=True)
101 + alert: bool = Field(
102 + ...,
103 + description="Indicates if the event is an alert",
104 + example=True,
105 + )
106 fields: GraylogEventFields = Field(..., description="Custom fields for the event")
58 - group_by_fields: Dict[str, Any] = Field(..., description="Fields used to group events", example={})
107 + group_by_fields: Dict[str, Any] = Field(
108 + ...,
109 + description="Fields used to group events",
110 + example={},
111 + )
112
113 @property
114 def alert_index(self) -> str:
@@ -63,24 +116,68 @@ class GraylogEvent(BaseModel):
116
117
118 class GraylogPostRequest(BaseModel):
66 - event_definition_id: str = Field(..., description="Identifier for the event definition", example="65bd28505e9a2d550cf521e7")
67 - event_definition_type: str = Field(..., description="Type of the event definition", example="aggregation-v1")
68 - event_definition_title: str = Field(..., description="Title of the event definition", example="COPILOT TESTING WAZUH")
69 - event_definition_description: Optional[str] = Field(None, description="Description of the event definition", example="")
70 - job_definition_id: str = Field(..., description="Identifier for the job definition", example="65bd284b5e9a2d550cf521dc")
71 - job_trigger_id: str = Field(..., description="Identifier for the job trigger", example="65bd2b625e9a2d550cf528e4")
119 + event_definition_id: str = Field(
120 + ...,
121 + description="Identifier for the event definition",
122 + example="65bd28505e9a2d550cf521e7",
123 + )
124 + event_definition_type: str = Field(
125 + ...,
126 + description="Type of the event definition",
127 + example="aggregation-v1",
128 + )
129 + event_definition_title: str = Field(
130 + ...,
131 + description="Title of the event definition",
132 + example="COPILOT TESTING WAZUH",
133 + )
134 + event_definition_description: Optional[str] = Field(
135 + None,
136 + description="Description of the event definition",
137 + example="",
138 + )
139 + job_definition_id: str = Field(
140 + ...,
141 + description="Identifier for the job definition",
142 + example="65bd284b5e9a2d550cf521dc",
143 + )
144 + job_trigger_id: str = Field(
145 + ...,
146 + description="Identifier for the job trigger",
147 + example="65bd2b625e9a2d550cf528e4",
148 + )
149 event: GraylogEvent = Field(..., description="Event details")
73 - backlog: List[str] = Field(..., description="List of backlog items associated with the event", example=[])
150 + backlog: List[str] = Field(
151 + ...,
152 + description="List of backlog items associated with the event",
153 + example=[],
154 + )
155
156
157 class GraylogPostResponse(BaseModel):
77 - success: bool = Field(..., description="Indicates if the request was successful", example=True)
78 - message: str = Field(..., description="Message associated with the response", example="Event processed successfully")
158 + success: bool = Field(
159 + ...,
160 + description="Indicates if the request was successful",
161 + example=True,
162 + )
163 + message: str = Field(
164 + ...,
165 + description="Message associated with the response",
166 + example="Event processed successfully",
167 + )
168
169
170 class WazuhAnalysisResponse(BaseModel):
82 - success: bool = Field(..., description="Indicates if the request was successful", example=True)
83 - message: str = Field(..., description="Message associated with the response", example="Analysis completed successfully")
171 + success: bool = Field(
172 + ...,
173 + description="Indicates if the request was successful",
174 + example=True,
175 + )
176 + message: str = Field(
177 + ...,
178 + description="Message associated with the response",
179 + example="Analysis completed successfully",
180 + )
181
182
183 # ! Wazuh Indexer Schema ! #
@@ -135,9 +232,16 @@ class SortOrder(Enum):
232 class FilterAlertsRequest(BaseModel):
233 per_page: int = Field(1000, description="The number of alerts to return per page.")
234 page: int = Field(1, description="The page number to return.")
138 - sort: SortOrder = Field(SortOrder.desc, description="The sort order for the alerts.")
235 + sort: SortOrder = Field(
236 + SortOrder.desc,
237 + description="The sort order for the alerts.",
238 + )
239 alert_tags: str = Field(..., description="The tags of the alert.")
140 - alert_status_id: int = Field(3, description="The status of the alert. Default to assigned.", example=3)
240 + alert_status_id: int = Field(
241 + 3,
242 + description="The status of the alert. Default to assigned.",
243 + example=3,
244 + )
245
246
247 class WazuhIrisAlertContext(BaseModel):
backend/app/integrations/monitoring_alert/schema/provision.py
+6 -7
@@ -1,12 +1,8 @@
1 from enum import Enum
2 -from typing import Dict
3 -from typing import List
4 -from typing import Optional
2 +from typing import Dict, List, Optional
3
4 from fastapi import HTTPException
7 -from pydantic import BaseModel
8 -from pydantic import Field
9 -from pydantic import validator
5 +from pydantic import BaseModel, Field, validator
6
7
8 class AvailableMonitoringAlerts(str, Enum):
@@ -62,7 +58,10 @@ class ProvisionMonitoringAlertRequest(BaseModel):
58 @validator("search_within_last", "execute_every")
59 def validate_non_zero(cls, v):
60 if v == 0:
65 - raise HTTPException(status_code=400, detail=f"Invalid value: {v}. Must be greater than 0.")
61 + raise HTTPException(
62 + status_code=400,
63 + detail=f"Invalid value: {v}. Must be greater than 0.",
64 + )
65 return v
66
67
backend/app/integrations/monitoring_alert/services/provision.py
+120 -61
@@ -1,49 +1,27 @@
1 import os
2 from typing import Optional
3
4 -from dotenv import load_dotenv
5 -from fastapi import HTTPException
6 -from loguru import logger
7 -
4 from app.connectors.graylog.routes.monitoring import get_all_event_notifications
5 from app.connectors.graylog.schema.management import UrlWhitelistEntryResponse
6 from app.connectors.graylog.schema.monitoring import GraylogEventNotificationsResponse
7 from app.connectors.graylog.services.collector import get_url_whitelist_entries
12 -from app.connectors.graylog.utils.universal import send_post_request
13 -from app.connectors.graylog.utils.universal import send_put_request
8 +from app.connectors.graylog.utils.universal import send_post_request, send_put_request
9 from app.integrations.monitoring_alert.schema.provision import (
10 GraylogAlertProvisionConfig,
16 -)
17 -from app.integrations.monitoring_alert.schema.provision import (
11 GraylogAlertProvisionFieldSpecItem,
19 -)
20 -from app.integrations.monitoring_alert.schema.provision import (
12 GraylogAlertProvisionModel,
22 -)
23 -from app.integrations.monitoring_alert.schema.provision import (
13 GraylogAlertProvisionNotification,
25 -)
26 -from app.integrations.monitoring_alert.schema.provision import (
14 GraylogAlertProvisionNotificationSettings,
28 -)
29 -from app.integrations.monitoring_alert.schema.provision import (
15 GraylogAlertProvisionProvider,
31 -)
32 -from app.integrations.monitoring_alert.schema.provision import (
16 GraylogAlertWebhookNotificationModel,
34 -)
35 -from app.integrations.monitoring_alert.schema.provision import (
17 GraylogUrlWhitelistEntries,
37 -)
38 -from app.integrations.monitoring_alert.schema.provision import (
18 GraylogUrlWhitelistEntryConfig,
40 -)
41 -from app.integrations.monitoring_alert.schema.provision import (
19 ProvisionMonitoringAlertRequest,
43 -)
44 -from app.integrations.monitoring_alert.schema.provision import (
20 ProvisionWazuhMonitoringAlertResponse,
21 )
22 +from dotenv import load_dotenv
23 +from fastapi import HTTPException
24 +from loguru import logger
25
26 load_dotenv()
27 import uuid
@@ -84,10 +62,20 @@ async def check_if_url_whitelist_entry_exists(url: str) -> bool:
62 """
63 url_whitelist_entries_response = await get_url_whitelist_entries()
64 if not url_whitelist_entries_response.success:
87 - raise HTTPException(status_code=500, detail="Failed to collect url whitelist entries")
88 - url_whitelist_entries_response = UrlWhitelistEntryResponse(**url_whitelist_entries_response.dict())
89 - logger.info(f"Url whitelist entries collected: {url_whitelist_entries_response.url_whitelist_entries}")
90 - if url in [url_whitelist_entry.value for url_whitelist_entry in url_whitelist_entries_response.url_whitelist_entries.entries]:
65 + raise HTTPException(
66 + status_code=500,
67 + detail="Failed to collect url whitelist entries",
68 + )
69 + url_whitelist_entries_response = UrlWhitelistEntryResponse(
70 + **url_whitelist_entries_response.dict(),
71 + )
72 + logger.info(
73 + f"Url whitelist entries collected: {url_whitelist_entries_response.url_whitelist_entries}",
74 + )
75 + if url in [
76 + url_whitelist_entry.value
77 + for url_whitelist_entry in url_whitelist_entries_response.url_whitelist_entries.entries
78 + ]:
79 logger.info(f"Url whitelist entry {url} already exists")
80 return True
81 return False
@@ -105,16 +93,27 @@ async def get_notification_id(notification_title: str) -> Optional[str]:
93 """
94 event_notifications_response = await get_all_event_notifications()
95 if not event_notifications_response.success:
108 - raise HTTPException(status_code=500, detail="Failed to collect event notifications")
109 - event_notifications_response = GraylogEventNotificationsResponse(**event_notifications_response.dict())
110 - logger.info(f"Event notifications collected: {event_notifications_response.event_notifications}")
111 - for event_notification in event_notifications_response.event_notifications.notifications:
96 + raise HTTPException(
97 + status_code=500,
98 + detail="Failed to collect event notifications",
99 + )
100 + event_notifications_response = GraylogEventNotificationsResponse(
101 + **event_notifications_response.dict(),
102 + )
103 + logger.info(
104 + f"Event notifications collected: {event_notifications_response.event_notifications}",
105 + )
106 + for (
107 + event_notification
108 + ) in event_notifications_response.event_notifications.notifications:
109 if event_notification.title == notification_title:
110 return event_notification.id
111 return None
112
113
117 -async def build_url_whitelisted_entries(whitelist_url_model: GraylogUrlWhitelistEntryConfig) -> GraylogUrlWhitelistEntries:
114 +async def build_url_whitelisted_entries(
115 + whitelist_url_model: GraylogUrlWhitelistEntryConfig,
116 +) -> GraylogUrlWhitelistEntries:
117 """
118 Builds the URL Whitelisted Entries model.
119
@@ -123,8 +122,13 @@ async def build_url_whitelisted_entries(whitelist_url_model: GraylogUrlWhitelist
122 """
123 url_whitelist_entries_response = await get_url_whitelist_entries()
124 if not url_whitelist_entries_response.success:
126 - raise HTTPException(status_code=500, detail="Failed to collect url whitelist entries")
127 - url_whitelist_entries_response = UrlWhitelistEntryResponse(**url_whitelist_entries_response.dict())
125 + raise HTTPException(
126 + status_code=500,
127 + detail="Failed to collect url whitelist entries",
128 + )
129 + url_whitelist_entries_response = UrlWhitelistEntryResponse(
130 + **url_whitelist_entries_response.dict(),
131 + )
132 logger.info(f"Url whitelist entries collected: {url_whitelist_entries_response}")
133 url_whitelist_entries = url_whitelist_entries_response.url_whitelist_entries.entries
134 url_whitelist_entries.append(whitelist_url_model)
@@ -134,7 +138,9 @@ async def build_url_whitelisted_entries(whitelist_url_model: GraylogUrlWhitelist
138 )
139
140
137 -async def provision_webhook_url_whitelist(whitelist_url_model: GraylogUrlWhitelistEntries) -> bool:
141 +async def provision_webhook_url_whitelist(
142 + whitelist_url_model: GraylogUrlWhitelistEntries,
143 +) -> bool:
144 """
145 Provisions a webhook URL for Graylog.
146
@@ -145,7 +151,10 @@ async def provision_webhook_url_whitelist(whitelist_url_model: GraylogUrlWhiteli
151 bool: True if the webhook URL was provisioned successfully, False otherwise.
152 """
153 logger.info(f"Provisioning URL Whitelist: {whitelist_url_model.dict()}")
148 - response = await send_put_request(endpoint="/api/system/urlwhitelist", data=whitelist_url_model.dict())
154 + response = await send_put_request(
155 + endpoint="/api/system/urlwhitelist",
156 + data=whitelist_url_model.dict(),
157 + )
158 logger.info(f"URL Whitelist provisioned: {response}")
159 if response["success"]:
160 return True
@@ -164,17 +173,27 @@ async def check_if_event_notification_exists(event_notification: str) -> bool:
173 """
174 event_notifications_response = await get_all_event_notifications()
175 if not event_notifications_response.success:
167 - raise HTTPException(status_code=500, detail="Failed to collect event notifications")
168 - event_notifications_response = GraylogEventNotificationsResponse(**event_notifications_response.dict())
169 - logger.info(f"Event notifications collected: {event_notifications_response.event_notifications}")
176 + raise HTTPException(
177 + status_code=500,
178 + detail="Failed to collect event notifications",
179 + )
180 + event_notifications_response = GraylogEventNotificationsResponse(
181 + **event_notifications_response.dict(),
182 + )
183 + logger.info(
184 + f"Event notifications collected: {event_notifications_response.event_notifications}",
185 + )
186 if event_notification in [
171 - event_notification.title for event_notification in event_notifications_response.event_notifications.notifications
187 + event_notification.title
188 + for event_notification in event_notifications_response.event_notifications.notifications
189 ]:
190 return True
191 return False
192
193
177 -async def provision_webhook(webhook_model: GraylogAlertWebhookNotificationModel) -> Optional[str]:
194 +async def provision_webhook(
195 + webhook_model: GraylogAlertWebhookNotificationModel,
196 +) -> Optional[str]:
197 """
198 Provisions a webhook for Graylog alerts.
199
@@ -184,14 +203,19 @@ async def provision_webhook(webhook_model: GraylogAlertWebhookNotificationModel)
203 Returns:
204 bool: True if the webhook was provisioned successfully, False otherwise.
205 """
187 - response = await send_post_request(endpoint="/api/events/notifications", data=webhook_model.dict())
206 + response = await send_post_request(
207 + endpoint="/api/events/notifications",
208 + data=webhook_model.dict(),
209 + )
210 if response["success"]:
211 logger.info(f"response: {response}")
212 return response["data"]["id"]
213 raise HTTPException(status_code=500, detail="Failed to provision webhook")
214
215
194 -async def provision_alert_definition(alert_definition_model: GraylogAlertProvisionModel) -> bool:
216 +async def provision_alert_definition(
217 + alert_definition_model: GraylogAlertProvisionModel,
218 +) -> bool:
219 """
220 Provisions an alert definition for Graylog.
221
@@ -201,13 +225,18 @@ async def provision_alert_definition(alert_definition_model: GraylogAlertProvisi
225 Returns:
226 bool: True if the alert definition was provisioned successfully, False otherwise.
227 """
204 - response = await send_post_request(endpoint="/api/events/definitions", data=alert_definition_model.dict())
228 + response = await send_post_request(
229 + endpoint="/api/events/definitions",
230 + data=alert_definition_model.dict(),
231 + )
232 if response["success"]:
233 return True
234 raise HTTPException(status_code=500, detail="Failed to provision alert definition")
235
236
210 -async def provision_wazuh_monitoring_alert(request: ProvisionMonitoringAlertRequest) -> ProvisionWazuhMonitoringAlertResponse:
237 +async def provision_wazuh_monitoring_alert(
238 + request: ProvisionMonitoringAlertRequest,
239 +) -> ProvisionWazuhMonitoringAlertResponse:
240 """
241 Provisions Wazuh monitoring alerts.
242
@@ -215,10 +244,14 @@ async def provision_wazuh_monitoring_alert(request: ProvisionMonitoringAlertRequ
244 ProvisionWazuhMonitoringAlertResponse: The response indicating the success of provisioning the monitoring alerts.
245 """
246 #
218 - logger.info(f"Invoking provision_wazuh_monitoring_alert with request: {request.dict()}")
247 + logger.info(
248 + f"Invoking provision_wazuh_monitoring_alert with request: {request.dict()}",
249 + )
250 notification_exists = await check_if_event_notification_exists("SEND TO COPILOT")
251 if not notification_exists:
221 - url_whitelisted = await check_if_url_whitelist_entry_exists(f"http://{os.getenv('SERVER_IP')}:5000/monitoring_alert/create")
252 + url_whitelisted = await check_if_url_whitelist_entry_exists(
253 + f"http://{os.getenv('SERVER_IP')}:5000/monitoring_alert/create",
254 + )
255 if not url_whitelisted:
256 logger.info("Provisioning URL Whitelist")
257 whitelisted_urls = await build_url_whitelisted_entries(
@@ -236,7 +269,10 @@ async def provision_wazuh_monitoring_alert(request: ProvisionMonitoringAlertRequ
269 GraylogAlertWebhookNotificationModel(
270 title="SEND TO COPILOT",
271 description="Send alert to Copilot",
239 - config={"url": f"http://{os.getenv('SERVER_IP')}:5000/monitoring_alert/create", "type": "http-notification-v1"},
272 + config={
273 + "url": f"http://{os.getenv('SERVER_IP')}:5000/monitoring_alert/create",
274 + "type": "http-notification-v1",
275 + },
276 ),
277 )
278 logger.info(f"SEND TO COPILOT Webhook provisioned with id: {notification_id}")
@@ -255,8 +291,12 @@ async def provision_wazuh_monitoring_alert(request: ProvisionMonitoringAlertRequ
291 conditions={
292 "expression": None,
293 },
258 - search_within_ms=await convert_seconds_to_milliseconds(request.search_within_last),
259 - execute_every_ms=await convert_seconds_to_milliseconds(request.execute_every),
294 + search_within_ms=await convert_seconds_to_milliseconds(
295 + request.search_within_last,
296 + ),
297 + execute_every_ms=await convert_seconds_to_milliseconds(
298 + request.execute_every,
299 + ),
300 ),
301 field_spec={
302 "ALERT_ID": GraylogAlertProvisionFieldSpecItem(
@@ -304,10 +344,15 @@ async def provision_wazuh_monitoring_alert(request: ProvisionMonitoringAlertRequ
344 ),
345 )
346
307 - return ProvisionWazuhMonitoringAlertResponse(success=True, message="Wazuh monitoring alerts provisioned successfully")
347 + return ProvisionWazuhMonitoringAlertResponse(
348 + success=True,
349 + message="Wazuh monitoring alerts provisioned successfully",
350 + )
351
352
310 -async def provision_suricata_monitoring_alert(request: ProvisionMonitoringAlertRequest) -> ProvisionWazuhMonitoringAlertResponse:
353 +async def provision_suricata_monitoring_alert(
354 + request: ProvisionMonitoringAlertRequest,
355 +) -> ProvisionWazuhMonitoringAlertResponse:
356 """
357 Provisions Suricata monitoring alerts.
358
@@ -315,10 +360,14 @@ async def provision_suricata_monitoring_alert(request: ProvisionMonitoringAlertR
360 ProvisionWazuhMonitoringAlertResponse: The response indicating the success of provisioning the monitoring alerts.
361 """
362 #
318 - logger.info(f"Invoking provision_suricata_monitoring_alert with request: {request.dict()}")
363 + logger.info(
364 + f"Invoking provision_suricata_monitoring_alert with request: {request.dict()}",
365 + )
366 notification_exists = await check_if_event_notification_exists("SEND TO COPILOT")
367 if not notification_exists:
321 - url_whitelisted = await check_if_url_whitelist_entry_exists(f"http://{os.getenv('SERVER_IP')}:5000/monitoring_alert/create")
368 + url_whitelisted = await check_if_url_whitelist_entry_exists(
369 + f"http://{os.getenv('SERVER_IP')}:5000/monitoring_alert/create",
370 + )
371 if not url_whitelisted:
372 logger.info("Provisioning URL Whitelist")
373 whitelisted_urls = await build_url_whitelisted_entries(
@@ -336,7 +385,10 @@ async def provision_suricata_monitoring_alert(request: ProvisionMonitoringAlertR
385 GraylogAlertWebhookNotificationModel(
386 title="SEND TO COPILOT",
387 description="Send alert to Copilot",
339 - config={"url": f"http://{os.getenv('SERVER_IP')}:5000/monitoring_alert/create", "type": "http-notification-v1"},
388 + config={
389 + "url": f"http://{os.getenv('SERVER_IP')}:5000/monitoring_alert/create",
390 + "type": "http-notification-v1",
391 + },
392 ),
393 )
394 logger.info(f"SEND TO COPILOT Webhook provisioned with id: {notification_id}")
@@ -356,8 +408,12 @@ async def provision_suricata_monitoring_alert(request: ProvisionMonitoringAlertR
408 conditions={
409 "expression": None,
410 },
359 - search_within_ms=await convert_seconds_to_milliseconds(request.search_within_last),
360 - execute_every_ms=await convert_seconds_to_milliseconds(request.execute_every),
411 + search_within_ms=await convert_seconds_to_milliseconds(
412 + request.search_within_last,
413 + ),
414 + execute_every_ms=await convert_seconds_to_milliseconds(
415 + request.execute_every,
416 + ),
417 ),
418 field_spec={
419 "ALERT_ID": GraylogAlertProvisionFieldSpecItem(
@@ -405,4 +461,7 @@ async def provision_suricata_monitoring_alert(request: ProvisionMonitoringAlertR
461 ),
462 )
463
408 - return ProvisionWazuhMonitoringAlertResponse(success=True, message="Suricata monitoring alerts provisioned successfully")
464 + return ProvisionWazuhMonitoringAlertResponse(
465 + success=True,
466 + message="Suricata monitoring alerts provisioned successfully",
467 + )
backend/app/integrations/monitoring_alert/services/suricata.py
+151 -55
@@ -1,21 +1,20 @@
1 import json
2 -from typing import Optional
3 -from typing import Set
4 -
5 -from fastapi import HTTPException
6 -from loguru import logger
7 -from sqlalchemy.ext.asyncio import AsyncSession
2 +from typing import Optional, Set
3
4 from app.agents.routes.agents import get_agent
5 from app.agents.schema.agents import AgentsResponse
11 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
12 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
6 +from app.connectors.dfir_iris.utils.universal import (
7 + fetch_and_validate_data,
8 + initialize_client_and_alert,
9 +)
10 from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
11 from app.db.universal_models import CustomersMeta
15 -from app.integrations.alert_creation.general.schema.alert import CreateAlertRequest
16 -from app.integrations.alert_creation.general.schema.alert import IrisAsset
17 -from app.integrations.alert_creation.general.schema.alert import IrisIoc
18 -from app.integrations.alert_creation.general.schema.alert import ValidIocFields
12 +from app.integrations.alert_creation.general.schema.alert import (
13 + CreateAlertRequest,
14 + IrisAsset,
15 + IrisIoc,
16 + ValidIocFields,
17 +)
18 from app.integrations.alert_creation.general.services.alert_multi_exclude import (
19 AlertDetailsService,
20 )
@@ -28,25 +27,19 @@ from app.integrations.alert_escalation.services.general_alert import (
27 from app.integrations.monitoring_alert.models.monitoring_alert import MonitoringAlerts
28 from app.integrations.monitoring_alert.schema.monitoring_alert import (
29 FilterAlertsRequest,
31 -)
32 -from app.integrations.monitoring_alert.schema.monitoring_alert import SuricataAlertModel
33 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
30 + SuricataAlertModel,
31 SuricataIrisAlertContext,
35 -)
36 -from app.integrations.monitoring_alert.schema.monitoring_alert import SuricataIrisAsset
37 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
32 + SuricataIrisAsset,
33 WazuhAnalysisResponse,
39 -)
40 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
34 WazuhIrisAlertContext,
42 -)
43 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
35 WazuhIrisAlertPayload,
36 )
37 from app.integrations.monitoring_alert.utils.db_operations import remove_alert_id
47 -from app.integrations.utils.alerts import get_asset_type_id
48 -from app.integrations.utils.alerts import validate_ioc_type
38 +from app.integrations.utils.alerts import get_asset_type_id, validate_ioc_type
39 from app.utils import get_customer_alert_settings
40 +from fastapi import HTTPException
41 +from loguru import logger
42 +from sqlalchemy.ext.asyncio import AsyncSession
43
44
45 def valid_ioc_fields() -> Set[str]:
@@ -60,7 +53,10 @@ def valid_ioc_fields() -> Set[str]:
53 return {field.value for field in ValidIocFields}
54
55
63 -async def construct_alert_source_link(alert_details: CreateAlertRequest, session: AsyncSession) -> str:
56 +async def construct_alert_source_link(
57 + alert_details: CreateAlertRequest,
58 + session: AsyncSession,
59 +) -> str:
60 """
61 Construct the alert source link for the alert details.
62 Parameters
@@ -73,12 +69,22 @@ async def construct_alert_source_link(alert_details: CreateAlertRequest, session
69 The alert source link.
70 """
71 # Check if the alert has a process id and that it is not "No process ID found"
76 - if hasattr(alert_details, "process_id") and alert_details.process_id != "No process ID found":
77 - query_string = f"%22query%22:%22process_id:%5C%22{alert_details.process_id}%5C%22%20AND%20"
72 + if (
73 + hasattr(alert_details, "process_id")
74 + and alert_details.process_id != "No process ID found"
75 + ):
76 + query_string = (
77 + f"%22query%22:%22process_id:%5C%22{alert_details.process_id}%5C%22%20AND%20"
78 + )
79 else:
80 query_string = f"%22query%22:%22_id:%5C%22{alert_details.id}%5C%22%20AND%20"
81
81 - grafana_url = (await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)).grafana_url
82 + grafana_url = (
83 + await get_customer_alert_settings(
84 + customer_code=alert_details.agent_labels_customer,
85 + session=session,
86 + )
87 + ).grafana_url
88
89 return (
90 f"{grafana_url}/explore?left=%5B%22now-6h%22,%22now%22,%22WAZUH%22,%7B%22refId%22:%22A%22,"
@@ -112,7 +118,11 @@ async def build_ioc_payload(alert_details: CreateAlertRequest) -> Optional[IrisI
118 return None
119
120
115 -async def build_asset_payload(agent_data: AgentsResponse, alert_details: CreateAlertRequest, session: AsyncSession) -> IrisAsset:
121 +async def build_asset_payload(
122 + agent_data: AgentsResponse,
123 + alert_details: CreateAlertRequest,
124 + session: AsyncSession,
125 +) -> IrisAsset:
126 """
127 Build the payload for an IrisAsset object based on the agent data and alert details.
128
@@ -128,7 +138,10 @@ async def build_asset_payload(agent_data: AgentsResponse, alert_details: CreateA
138 return IrisAsset(
139 asset_name=agent_data.agents[0].hostname,
140 asset_ip=agent_data.agents[0].ip_address,
131 - asset_description=await construct_alert_source_link(alert_details, session=session),
141 + asset_description=await construct_alert_source_link(
142 + alert_details,
143 + session=session,
144 + ),
145 asset_type_id=await get_asset_type_id(agent_data.agents[0].os),
146 asset_tags=f"agent_id:{agent_data.agents[0].agent_id}",
147 )
@@ -146,7 +159,9 @@ async def fetch_wazuh_indexer_details(alert_id: str, index: str) -> SuricataAler
159 Returns:
160 CollectAlertsResponse: The response from the Wazuh-Indexer.
161 """
149 - logger.info(f"Fetching Wazuh alert details for alert_id: {alert_id} and index: {index}")
162 + logger.info(
163 + f"Fetching Wazuh alert details for alert_id: {alert_id} and index: {index}",
164 + )
165
166 es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
167 response = es_client.get(index=index, id=alert_id)
@@ -161,7 +176,11 @@ async def fetch_alert_details(alert: MonitoringAlerts) -> SuricataAlertModel:
176 return alert_details
177
178
164 -async def check_event_exclusion(alert_details: SuricataAlertModel, alert_detail_service: AlertDetailsService, session: AsyncSession):
179 +async def check_event_exclusion(
180 + alert_details: SuricataAlertModel,
181 + alert_detail_service: AlertDetailsService,
182 + session: AsyncSession,
183 +):
184 logger.info("Checking if alert is excluded due to multi exclusion.")
185 logger.info(f"Alert details: {alert_details}")
186 event_exclude_result = await alert_detail_service.collect_alert_timeline_process_id(
@@ -190,11 +209,20 @@ async def check_if_open_alert_exists_in_iris(alert_details: SuricataAlertModel)
209 bool: True if the alert exists in IRIS, False otherwise.
210 """
211 client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
193 - request = FilterAlertsRequest(alert_tags=alert_details._source["alert_signature_id"])
212 + request = FilterAlertsRequest(
213 + alert_tags=alert_details._source["alert_signature_id"],
214 + )
215 params = construct_params(request)
195 - alert_exists = await fetch_and_validate_data(client, lambda: alert_client.filter_alerts(**params))
216 + alert_exists = await fetch_and_validate_data(
217 + client,
218 + lambda: alert_client.filter_alerts(**params),
219 + )
220 logger.info(f"Alert exists: {alert_exists['data']['alerts']}")
197 - return alert_exists["data"]["alerts"][0]["alert_id"] if alert_exists["data"]["alerts"] else []
221 + return (
222 + alert_exists["data"]["alerts"][0]["alert_id"]
223 + if alert_exists["data"]["alerts"]
224 + else []
225 + )
226
227
228 def construct_params(request: FilterAlertsRequest) -> dict:
@@ -238,11 +266,22 @@ async def build_alert_context_payload(
266 """
267 return WazuhIrisAlertContext(
268 customer_iris_id=(
241 - await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)
269 + await get_customer_alert_settings(
270 + customer_code=alert_details.agent_labels_customer,
271 + session=session,
272 + )
273 ).iris_customer_id,
243 - customer_name=(await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)).customer_name,
274 + customer_name=(
275 + await get_customer_alert_settings(
276 + customer_code=alert_details.agent_labels_customer,
277 + session=session,
278 + )
279 + ).customer_name,
280 customer_cases_index=(
245 - await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)
281 + await get_customer_alert_settings(
282 + customer_code=alert_details.agent_labels_customer,
283 + session=session,
284 + )
285 ).iris_index,
286 alert_name=alert_details.rule_description,
287 alert_level=alert_details.rule_level,
@@ -279,9 +318,22 @@ async def build_alert_payload(
318 Returns:
319 WazuhIrisAlertPayload: The built alert payload.
320 """
282 - asset_payload = await build_asset_payload(agent_data, alert_details=alert_details, session=session)
283 - context_payload = await build_alert_context_payload(alert_details=alert_details, agent_data=agent_data, session=session)
284 - timefield = (await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)).timefield
321 + asset_payload = await build_asset_payload(
322 + agent_data,
323 + alert_details=alert_details,
324 + session=session,
325 + )
326 + context_payload = await build_alert_context_payload(
327 + alert_details=alert_details,
328 + agent_data=agent_data,
329 + session=session,
330 + )
331 + timefield = (
332 + await get_customer_alert_settings(
333 + customer_code=alert_details.agent_labels_customer,
334 + session=session,
335 + )
336 + ).timefield
337 # Get the timefield value from the alert_details
338 if hasattr(alert_details, timefield):
339 alert_details.time_field = getattr(alert_details, timefield)
@@ -296,7 +348,10 @@ async def build_alert_payload(
348 alert_status_id=3,
349 alert_severity_id=5,
350 alert_customer_id=(
299 - await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)
351 + await get_customer_alert_settings(
352 + customer_code=alert_details.agent_labels_customer,
353 + session=session,
354 + )
355 ).iris_customer_id,
356 alert_source_content=alert_details.to_dict(),
357 alert_context=context_payload,
@@ -313,7 +368,10 @@ async def build_alert_payload(
368 alert_status_id=3,
369 alert_severity_id=5,
370 alert_customer_id=(
316 - await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)
371 + await get_customer_alert_settings(
372 + customer_code=alert_details.agent_labels_customer,
373 + session=session,
374 + )
375 ).iris_customer_id,
376 alert_source_content=alert_details.to_dict(),
377 alert_context=context_payload,
@@ -321,7 +379,9 @@ async def build_alert_payload(
379 )
380
381
324 -async def create_alert_details(alert_details: SuricataAlertModel) -> SuricataIrisAlertContext:
382 +async def create_alert_details(
383 + alert_details: SuricataAlertModel,
384 +) -> SuricataIrisAlertContext:
385 """
386 Create an alert details object from the Wazuh alert details.
387
@@ -341,11 +401,17 @@ async def create_alert_details(alert_details: SuricataAlertModel) -> SuricataIri
401 rule_id=alert_details._source["alert_signature_id"],
402 src_ip=alert_details._source["src_ip"],
403 dest_ip=alert_details._source["dest_ip"],
344 - app_proto=alert_details._source.get("app_proto", "No application protocol found"),
404 + app_proto=alert_details._source.get(
405 + "app_proto",
406 + "No application protocol found",
407 + ),
408 )
409
410
348 -async def create_and_update_alert_in_iris(alert_details: SuricataAlertModel, session: AsyncSession) -> int:
411 +async def create_and_update_alert_in_iris(
412 + alert_details: SuricataAlertModel,
413 + session: AsyncSession,
414 +) -> int:
415 """
416 Creates the alert, then updates the alert with the asset and IoC if available.
417
@@ -405,7 +471,11 @@ async def create_and_update_alert_in_iris(alert_details: SuricataAlertModel, ses
471
472
473 async def get_current_assets(client, alert_client, iris_alert_id):
408 - result = await fetch_and_validate_data(client, alert_client.get_alert, iris_alert_id)
474 + result = await fetch_and_validate_data(
475 + client,
476 + alert_client.get_alert,
477 + iris_alert_id,
478 + )
479 return result["data"]["assets"]
480
481
@@ -460,34 +530,60 @@ async def analyze_suricata_alerts(
530 alert_details = await fetch_alert_details(alert)
531 iris_alert_id = await check_if_open_alert_exists_in_iris(alert_details)
532 if iris_alert_id == []:
463 - logger.info(f"Alert {alert_details._id} does not exist in IRIS. Creating alert.")
464 - iris_alert_id = await create_and_update_alert_in_iris(alert_details, session)
533 + logger.info(
534 + f"Alert {alert_details._id} does not exist in IRIS. Creating alert.",
535 + )
536 + iris_alert_id = await create_and_update_alert_in_iris(
537 + alert_details,
538 + session,
539 + )
540 return None
541 await remove_alert_id(alert.alert_id, session)
542 es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
543 await add_alert_to_document(
544 es_client=es_client,
470 - alert=AddAlertRequest(alert_id=alert_details._id, index_name=alert_details._index),
545 + alert=AddAlertRequest(
546 + alert_id=alert_details._id,
547 + index_name=alert_details._index,
548 + ),
549 soc_alert_id=iris_alert_id,
550 session=session,
551 )
552
553 else:
476 - logger.info(f"Alert {iris_alert_id} exists in IRIS. Updating alert with the asset.")
554 + logger.info(
555 + f"Alert {iris_alert_id} exists in IRIS. Updating alert with the asset.",
556 + )
557 # Fetch the current list of assets from the alert to avoid overwriting them
558 client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
479 - current_assets = await get_current_assets(client, alert_client, iris_alert_id)
559 + current_assets = await get_current_assets(
560 + client,
561 + alert_client,
562 + iris_alert_id,
563 + )
564 alert_details = await create_alert_details(alert_details)
565 agent_details = await get_agent(alert_details.agent_id, session)
482 - asset_payload = await build_asset_payload(agent_data=agent_details, alert_details=alert_details, session=session)
566 + asset_payload = await build_asset_payload(
567 + agent_data=agent_details,
568 + alert_details=alert_details,
569 + session=session,
570 + )
571 current_assets.append(dict(IrisAsset(**asset_payload.to_dict())))
572 current_assets = await remove_duplicate_assets(current_assets)
485 - await update_alert_with_assets(client, alert_client, iris_alert_id, current_assets)
573 + await update_alert_with_assets(
574 + client,
575 + alert_client,
576 + iris_alert_id,
577 + current_assets,
578 + )
579 await remove_alert_id(alert.alert_id, session)
580 es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
581 await add_alert_to_document(
582 es_client=es_client,
490 - alert=AddAlertRequest(alert_id=alert_details.id, index_name=alert_details.index),
583 + alert=AddAlertRequest(
584 + alert_id=alert_details.id,
585 + index_name=alert_details.index,
586 + ),
587 soc_alert_id=iris_alert_id,
588 session=session,
589 )
backend/app/integrations/monitoring_alert/services/wazuh.py
+140 -49
@@ -1,21 +1,20 @@
1 import json
2 -from typing import Optional
3 -from typing import Set
4 -
5 -from fastapi import HTTPException
6 -from loguru import logger
7 -from sqlalchemy.ext.asyncio import AsyncSession
2 +from typing import Optional, Set
3
4 from app.agents.routes.agents import get_agent
5 from app.agents.schema.agents import AgentsResponse
11 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
12 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
6 +from app.connectors.dfir_iris.utils.universal import (
7 + fetch_and_validate_data,
8 + initialize_client_and_alert,
9 +)
10 from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
11 from app.db.universal_models import CustomersMeta
15 -from app.integrations.alert_creation.general.schema.alert import CreateAlertRequest
16 -from app.integrations.alert_creation.general.schema.alert import IrisAsset
17 -from app.integrations.alert_creation.general.schema.alert import IrisIoc
18 -from app.integrations.alert_creation.general.schema.alert import ValidIocFields
12 +from app.integrations.alert_creation.general.schema.alert import (
13 + CreateAlertRequest,
14 + IrisAsset,
15 + IrisIoc,
16 + ValidIocFields,
17 +)
18 from app.integrations.alert_creation.general.services.alert_multi_exclude import (
19 AlertDetailsService,
20 )
@@ -28,21 +27,17 @@ from app.integrations.alert_escalation.services.general_alert import (
27 from app.integrations.monitoring_alert.models.monitoring_alert import MonitoringAlerts
28 from app.integrations.monitoring_alert.schema.monitoring_alert import (
29 FilterAlertsRequest,
31 -)
32 -from app.integrations.monitoring_alert.schema.monitoring_alert import WazuhAlertModel
33 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
30 + WazuhAlertModel,
31 WazuhAnalysisResponse,
35 -)
36 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
32 WazuhIrisAlertContext,
38 -)
39 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
33 WazuhIrisAlertPayload,
34 )
35 from app.integrations.monitoring_alert.utils.db_operations import remove_alert_id
43 -from app.integrations.utils.alerts import get_asset_type_id
44 -from app.integrations.utils.alerts import validate_ioc_type
36 +from app.integrations.utils.alerts import get_asset_type_id, validate_ioc_type
37 from app.utils import get_customer_alert_settings
38 +from fastapi import HTTPException
39 +from loguru import logger
40 +from sqlalchemy.ext.asyncio import AsyncSession
41
42
43 def valid_ioc_fields() -> Set[str]:
@@ -56,7 +51,10 @@ def valid_ioc_fields() -> Set[str]:
51 return {field.value for field in ValidIocFields}
52
53
59 -async def construct_alert_source_link(alert_details: CreateAlertRequest, session: AsyncSession) -> str:
54 +async def construct_alert_source_link(
55 + alert_details: CreateAlertRequest,
56 + session: AsyncSession,
57 +) -> str:
58 """
59 Construct the alert source link for the alert details.
60 Parameters
@@ -69,12 +67,22 @@ async def construct_alert_source_link(alert_details: CreateAlertRequest, session
67 The alert source link.
68 """
69 # Check if the alert has a process id and that it is not "No process ID found"
72 - if hasattr(alert_details, "process_id") and alert_details.process_id != "No process ID found":
73 - query_string = f"%22query%22:%22process_id:%5C%22{alert_details.process_id}%5C%22%20AND%20"
70 + if (
71 + hasattr(alert_details, "process_id")
72 + and alert_details.process_id != "No process ID found"
73 + ):
74 + query_string = (
75 + f"%22query%22:%22process_id:%5C%22{alert_details.process_id}%5C%22%20AND%20"
76 + )
77 else:
78 query_string = f"%22query%22:%22_id:%5C%22{alert_details.id}%5C%22%20AND%20"
79
77 - grafana_url = (await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)).grafana_url
80 + grafana_url = (
81 + await get_customer_alert_settings(
82 + customer_code=alert_details.agent_labels_customer,
83 + session=session,
84 + )
85 + ).grafana_url
86
87 return (
88 f"{grafana_url}/explore?left=%5B%22now-6h%22,%22now%22,%22WAZUH%22,%7B%22refId%22:%22A%22,"
@@ -108,7 +116,11 @@ async def build_ioc_payload(alert_details: CreateAlertRequest) -> Optional[IrisI
116 return None
117
118
111 -async def build_asset_payload(agent_data: AgentsResponse, alert_details: CreateAlertRequest, session: AsyncSession) -> IrisAsset:
119 +async def build_asset_payload(
120 + agent_data: AgentsResponse,
121 + alert_details: CreateAlertRequest,
122 + session: AsyncSession,
123 +) -> IrisAsset:
124 """
125 Build the payload for an IrisAsset object based on the agent data and alert details.
126
@@ -124,7 +136,10 @@ async def build_asset_payload(agent_data: AgentsResponse, alert_details: CreateA
136 return IrisAsset(
137 asset_name=agent_data.agents[0].hostname,
138 asset_ip=agent_data.agents[0].ip_address,
127 - asset_description=await construct_alert_source_link(alert_details, session=session),
139 + asset_description=await construct_alert_source_link(
140 + alert_details,
141 + session=session,
142 + ),
143 asset_type_id=await get_asset_type_id(agent_data.agents[0].os),
144 asset_tags=f"agent_id:{agent_data.agents[0].agent_id}",
145 )
@@ -142,7 +157,9 @@ async def fetch_wazuh_indexer_details(alert_id: str, index: str) -> WazuhAlertMo
157 Returns:
158 CollectAlertsResponse: The response from the Wazuh-Indexer.
159 """
145 - logger.info(f"Fetching Wazuh alert details for alert_id: {alert_id} and index: {index}")
160 + logger.info(
161 + f"Fetching Wazuh alert details for alert_id: {alert_id} and index: {index}",
162 + )
163
164 es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
165 response = es_client.get(index=index, id=alert_id)
@@ -157,7 +174,11 @@ async def fetch_alert_details(alert: MonitoringAlerts) -> WazuhAlertModel:
174 return alert_details
175
176
160 -async def check_event_exclusion(alert_details: WazuhAlertModel, alert_detail_service: AlertDetailsService, session: AsyncSession):
177 +async def check_event_exclusion(
178 + alert_details: WazuhAlertModel,
179 + alert_detail_service: AlertDetailsService,
180 + session: AsyncSession,
181 +):
182 logger.info("Checking if alert is excluded due to multi exclusion.")
183 logger.info(f"Alert details: {alert_details}")
184 event_exclude_result = await alert_detail_service.collect_alert_timeline_process_id(
@@ -188,9 +209,16 @@ async def check_if_open_alert_exists_in_iris(alert_details: WazuhAlertModel) ->
209 client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
210 request = FilterAlertsRequest(alert_tags=alert_details._source["rule_id"])
211 params = construct_params(request)
191 - alert_exists = await fetch_and_validate_data(client, lambda: alert_client.filter_alerts(**params))
212 + alert_exists = await fetch_and_validate_data(
213 + client,
214 + lambda: alert_client.filter_alerts(**params),
215 + )
216 logger.info(f"Alert exists: {alert_exists['data']['alerts']}")
193 - return alert_exists["data"]["alerts"][0]["alert_id"] if alert_exists["data"]["alerts"] else []
217 + return (
218 + alert_exists["data"]["alerts"][0]["alert_id"]
219 + if alert_exists["data"]["alerts"]
220 + else []
221 + )
222
223
224 def construct_params(request: FilterAlertsRequest) -> dict:
@@ -234,11 +262,22 @@ async def build_alert_context_payload(
262 """
263 return WazuhIrisAlertContext(
264 customer_iris_id=(
237 - await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)
265 + await get_customer_alert_settings(
266 + customer_code=alert_details.agent_labels_customer,
267 + session=session,
268 + )
269 ).iris_customer_id,
239 - customer_name=(await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)).customer_name,
270 + customer_name=(
271 + await get_customer_alert_settings(
272 + customer_code=alert_details.agent_labels_customer,
273 + session=session,
274 + )
275 + ).customer_name,
276 customer_cases_index=(
241 - await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)
277 + await get_customer_alert_settings(
278 + customer_code=alert_details.agent_labels_customer,
279 + session=session,
280 + )
281 ).iris_index,
282 alert_name=alert_details.rule_description,
283 alert_level=alert_details.rule_level,
@@ -275,9 +314,22 @@ async def build_alert_payload(
314 Returns:
315 WazuhIrisAlertPayload: The built alert payload.
316 """
278 - asset_payload = await build_asset_payload(agent_data, alert_details=alert_details, session=session)
279 - context_payload = await build_alert_context_payload(alert_details=alert_details, agent_data=agent_data, session=session)
280 - timefield = (await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)).timefield
317 + asset_payload = await build_asset_payload(
318 + agent_data,
319 + alert_details=alert_details,
320 + session=session,
321 + )
322 + context_payload = await build_alert_context_payload(
323 + alert_details=alert_details,
324 + agent_data=agent_data,
325 + session=session,
326 + )
327 + timefield = (
328 + await get_customer_alert_settings(
329 + customer_code=alert_details.agent_labels_customer,
330 + session=session,
331 + )
332 + ).timefield
333 # Get the timefield value from the alert_details
334 if hasattr(alert_details, timefield):
335 alert_details.time_field = getattr(alert_details, timefield)
@@ -292,7 +344,10 @@ async def build_alert_payload(
344 alert_status_id=3,
345 alert_severity_id=5,
346 alert_customer_id=(
295 - await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)
347 + await get_customer_alert_settings(
348 + customer_code=alert_details.agent_labels_customer,
349 + session=session,
350 + )
351 ).iris_customer_id,
352 alert_source_content=alert_details.to_dict(),
353 alert_context=context_payload,
@@ -309,7 +364,10 @@ async def build_alert_payload(
364 alert_status_id=3,
365 alert_severity_id=5,
366 alert_customer_id=(
312 - await get_customer_alert_settings(customer_code=alert_details.agent_labels_customer, session=session)
367 + await get_customer_alert_settings(
368 + customer_code=alert_details.agent_labels_customer,
369 + session=session,
370 + )
371 ).iris_customer_id,
372 alert_source_content=alert_details.to_dict(),
373 alert_context=context_payload,
@@ -343,7 +401,10 @@ async def create_alert_details(alert_details: WazuhAlertModel) -> CreateAlertReq
401 )
402
403
346 -async def create_and_update_alert_in_iris(alert_details: WazuhAlertModel, session: AsyncSession) -> int:
404 +async def create_and_update_alert_in_iris(
405 + alert_details: WazuhAlertModel,
406 + session: AsyncSession,
407 +) -> int:
408 """
409 Creates the alert, then updates the alert with the asset and IoC if available.
410
@@ -396,7 +457,11 @@ async def create_and_update_alert_in_iris(alert_details: WazuhAlertModel, sessio
457
458
459 async def get_current_assets(client, alert_client, iris_alert_id):
399 - result = await fetch_and_validate_data(client, alert_client.get_alert, iris_alert_id)
460 + result = await fetch_and_validate_data(
461 + client,
462 + alert_client.get_alert,
463 + iris_alert_id,
464 + )
465 return result["data"]["assets"]
466
467
@@ -453,33 +518,59 @@ async def analyze_wazuh_alerts(
518 await check_event_exclusion(alert_details, alert_detail_service, session)
519 iris_alert_id = await check_if_open_alert_exists_in_iris(alert_details)
520 if iris_alert_id == []:
456 - logger.info(f"Alert {alert_details._id} does not exist in IRIS. Creating alert.")
457 - iris_alert_id = await create_and_update_alert_in_iris(alert_details, session)
521 + logger.info(
522 + f"Alert {alert_details._id} does not exist in IRIS. Creating alert.",
523 + )
524 + iris_alert_id = await create_and_update_alert_in_iris(
525 + alert_details,
526 + session,
527 + )
528 await remove_alert_id(alert.alert_id, session)
529 es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
530 await add_alert_to_document(
531 es_client=es_client,
462 - alert=AddAlertRequest(alert_id=alert_details._id, index_name=alert_details._index),
532 + alert=AddAlertRequest(
533 + alert_id=alert_details._id,
534 + index_name=alert_details._index,
535 + ),
536 soc_alert_id=iris_alert_id,
537 session=session,
538 )
539
540 else:
468 - logger.info(f"Alert {iris_alert_id} exists in IRIS. Updating alert with the asset.")
541 + logger.info(
542 + f"Alert {iris_alert_id} exists in IRIS. Updating alert with the asset.",
543 + )
544 # Fetch the current list of assets from the alert to avoid overwriting them
545 client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
471 - current_assets = await get_current_assets(client, alert_client, iris_alert_id)
546 + current_assets = await get_current_assets(
547 + client,
548 + alert_client,
549 + iris_alert_id,
550 + )
551 alert_details = await create_alert_details(alert_details)
552 agent_details = await get_agent(alert_details.agent_id, session)
474 - asset_payload = await build_asset_payload(agent_data=agent_details, alert_details=alert_details, session=session)
553 + asset_payload = await build_asset_payload(
554 + agent_data=agent_details,
555 + alert_details=alert_details,
556 + session=session,
557 + )
558 current_assets.append(dict(IrisAsset(**asset_payload.to_dict())))
559 current_assets = await remove_duplicate_assets(current_assets)
477 - await update_alert_with_assets(client, alert_client, iris_alert_id, current_assets)
560 + await update_alert_with_assets(
561 + client,
562 + alert_client,
563 + iris_alert_id,
564 + current_assets,
565 + )
566 await remove_alert_id(alert.alert_id, session)
567 es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
568 await add_alert_to_document(
569 es_client=es_client,
482 - alert=AddAlertRequest(alert_id=alert_details.id, index_name=alert_details.index),
570 + alert=AddAlertRequest(
571 + alert_id=alert_details.id,
572 + index_name=alert_details.index,
573 + ),
574 soc_alert_id=iris_alert_id,
575 session=session,
576 )
backend/app/integrations/monitoring_alert/utils/db_operations.py
+4 -3
@@ -1,9 +1,8 @@
1 +from app.integrations.monitoring_alert.models.monitoring_alert import MonitoringAlerts
2 from loguru import logger
3 from sqlalchemy.ext.asyncio import AsyncSession
4 from sqlalchemy.future import select
5
5 -from app.integrations.monitoring_alert.models.monitoring_alert import MonitoringAlerts
6 -
6
7 async def remove_alert_id(alert_id: str, session: AsyncSession) -> None:
8 """
@@ -15,7 +14,9 @@ async def remove_alert_id(alert_id: str, session: AsyncSession) -> None:
14 """
15 logger.info(f"Removing alert with alert_id: {alert_id}")
16
18 - alert = await session.execute(select(MonitoringAlerts).where(MonitoringAlerts.alert_id == alert_id))
17 + alert = await session.execute(
18 + select(MonitoringAlerts).where(MonitoringAlerts.alert_id == alert_id),
19 + )
20 alert = alert.scalars().first()
21
22 if not alert:
backend/app/integrations/office365/routes/provision.py
+36 -19
@@ -1,26 +1,28 @@
1 from typing import Dict
2
3 -from fastapi import APIRouter
4 -from fastapi import Depends
5 -from fastapi import HTTPException
6 -from fastapi import Security
7 -from sqlalchemy.ext.asyncio import AsyncSession
8 -
3 from app.auth.utils import AuthHandler
4 from app.db.db_session import get_db
11 -from app.integrations.office365.schema.provision import ProvisionOffice365AuthKeys
12 -from app.integrations.office365.schema.provision import ProvisionOffice365Request
13 -from app.integrations.office365.schema.provision import ProvisionOffice365Response
5 +from app.integrations.office365.schema.provision import (
6 + ProvisionOffice365AuthKeys,
7 + ProvisionOffice365Request,
8 + ProvisionOffice365Response,
9 +)
10 from app.integrations.office365.services.provision import provision_office365
15 -from app.integrations.routes import find_customer_integration
16 -from app.integrations.routes import get_customer_integrations_by_customer_code
17 -from app.integrations.schema import CustomerIntegrations
18 -from app.integrations.schema import CustomerIntegrationsResponse
11 +from app.integrations.routes import (
12 + find_customer_integration,
13 + get_customer_integrations_by_customer_code,
14 +)
15 +from app.integrations.schema import CustomerIntegrations, CustomerIntegrationsResponse
16 +from fastapi import APIRouter, Depends, HTTPException, Security
17 +from sqlalchemy.ext.asyncio import AsyncSession
18
19 integration_office365_router = APIRouter()
20
21
23 -async def get_customer_integration_response(customer_code: str, session: AsyncSession) -> CustomerIntegrationsResponse:
22 +async def get_customer_integration_response(
23 + customer_code: str,
24 + session: AsyncSession,
25 +) -> CustomerIntegrationsResponse:
26 """
27 Retrieves the integration response for a customer.
28
@@ -34,13 +36,21 @@ async def get_customer_integration_response(customer_code: str, session: AsyncSe
36 Raises:
37 HTTPException: If the customer integration settings are not found.
38 """
37 - customer_integration_response = await get_customer_integrations_by_customer_code(customer_code, session)
39 + customer_integration_response = await get_customer_integrations_by_customer_code(
40 + customer_code,
41 + session,
42 + )
43 if customer_integration_response.available_integrations == []:
39 - raise HTTPException(status_code=404, detail="Customer integration settings not found.")
44 + raise HTTPException(
45 + status_code=404,
46 + detail="Customer integration settings not found.",
47 + )
48 return customer_integration_response
49
50
43 -def extract_office365_auth_keys(customer_integration: CustomerIntegrations) -> Dict[str, str]:
51 +def extract_office365_auth_keys(
52 + customer_integration: CustomerIntegrations,
53 +) -> Dict[str, str]:
54 """
55 Extracts the authentication keys for Office365 integration from the given customer integration.
56
@@ -86,7 +96,10 @@ async def provision_office365_route(
96 Returns:
97 ProvisionOffice365Response: The response object containing the result of the provisioning.
98 """
89 - customer_integration_response = await get_customer_integration_response(provision_office365_request.customer_code, session)
99 + customer_integration_response = await get_customer_integration_response(
100 + provision_office365_request.customer_code,
101 + session,
102 + )
103
104 customer_integration = await find_customer_integration(
105 provision_office365_request.customer_code,
@@ -98,4 +111,8 @@ async def provision_office365_route(
111
112 auth_keys = ProvisionOffice365AuthKeys(**office365_auth_keys)
113
101 - return await provision_office365(provision_office365_request.customer_code, auth_keys, session)
114 + return await provision_office365(
115 + provision_office365_request.customer_code,
116 + auth_keys,
117 + session,
118 + )
backend/app/integrations/office365/schema/provision.py
+2 -5
@@ -1,10 +1,7 @@
1 from enum import Enum
2 -from typing import Any
3 -from typing import Dict
2 +from typing import Any, Dict
3
5 -from pydantic import BaseModel
6 -from pydantic import Field
7 -from pydantic import root_validator
4 +from pydantic import BaseModel, Field, root_validator
5
6
7 class PipelineRuleTitles(Enum):
backend/app/integrations/office365/services/provision.py
+278 -87
@@ -4,50 +4,65 @@ from datetime import datetime
4 from typing import List
5
6 import requests
7 -from dotenv import load_dotenv
8 -from fastapi import HTTPException
9 -from loguru import logger
10 -from sqlalchemy import and_
11 -from sqlalchemy import update
12 -from sqlalchemy.ext.asyncio import AsyncSession
13 -
14 -from app.connectors.grafana.schema.dashboards import DashboardProvisionRequest
15 -from app.connectors.grafana.schema.dashboards import Office365Dashboard
7 +from app.connectors.grafana.schema.dashboards import (
8 + DashboardProvisionRequest,
9 + Office365Dashboard,
10 +)
11 from app.connectors.grafana.services.dashboards import provision_dashboards
12 from app.connectors.grafana.utils.universal import create_grafana_client
18 -from app.connectors.graylog.schema.pipelines import CreatePipeline
19 -from app.connectors.graylog.schema.pipelines import CreatePipelineRule
20 -from app.connectors.graylog.schema.pipelines import GraylogPipelinesResponse
21 -from app.connectors.graylog.schema.pipelines import PipelineRulesResponse
13 +from app.connectors.graylog.schema.pipelines import (
14 + CreatePipeline,
15 + CreatePipelineRule,
16 + GraylogPipelinesResponse,
17 + PipelineRulesResponse,
18 +)
19 from app.connectors.graylog.services.management import start_stream
23 -from app.connectors.graylog.services.pipelines import connect_stream_to_pipeline
24 -from app.connectors.graylog.services.pipelines import create_pipeline_graylog
25 -from app.connectors.graylog.services.pipelines import create_pipeline_rule
26 -from app.connectors.graylog.services.pipelines import get_pipeline_id
27 -from app.connectors.graylog.services.pipelines import get_pipeline_rules
28 -from app.connectors.graylog.services.pipelines import get_pipelines
20 +from app.connectors.graylog.services.pipelines import (
21 + connect_stream_to_pipeline,
22 + create_pipeline_graylog,
23 + create_pipeline_rule,
24 + get_pipeline_id,
25 + get_pipeline_rules,
26 + get_pipelines,
27 +)
28 from app.connectors.graylog.utils.universal import send_post_request
30 -from app.connectors.wazuh_manager.utils.universal import send_get_request
31 -from app.connectors.wazuh_manager.utils.universal import send_put_request
32 -from app.customer_provisioning.schema.grafana import GrafanaDatasource
33 -from app.customer_provisioning.schema.grafana import GrafanaDataSourceCreationResponse
34 -from app.customer_provisioning.schema.graylog import GraylogIndexSetCreationResponse
35 -from app.customer_provisioning.schema.graylog import Office365EventStream
36 -from app.customer_provisioning.schema.graylog import StreamConnectionToPipelineRequest
37 -from app.customer_provisioning.schema.graylog import StreamCreationResponse
38 -from app.customer_provisioning.schema.graylog import TimeBasedIndexSet
39 -from app.customer_provisioning.services.grafana import create_grafana_folder
40 -from app.customer_provisioning.services.grafana import get_opensearch_version
41 -from app.customers.routes.customers import get_customer
42 -from app.customers.routes.customers import get_customer_meta
29 +from app.connectors.wazuh_manager.utils.universal import (
30 + send_get_request,
31 + send_put_request,
32 +)
33 +from app.customer_provisioning.schema.grafana import (
34 + GrafanaDatasource,
35 + GrafanaDataSourceCreationResponse,
36 +)
37 +from app.customer_provisioning.schema.graylog import (
38 + GraylogIndexSetCreationResponse,
39 + Office365EventStream,
40 + StreamConnectionToPipelineRequest,
41 + StreamCreationResponse,
42 + TimeBasedIndexSet,
43 +)
44 +from app.customer_provisioning.services.grafana import (
45 + create_grafana_folder,
46 + get_opensearch_version,
47 +)
48 +from app.customers.routes.customers import get_customer, get_customer_meta
49 from app.integrations.models.customer_integration_settings import CustomerIntegrations
44 -from app.integrations.office365.schema.provision import PipelineRuleTitles
45 -from app.integrations.office365.schema.provision import PipelineTitles
46 -from app.integrations.office365.schema.provision import ProvisionOffice365AuthKeys
47 -from app.integrations.office365.schema.provision import ProvisionOffice365Response
48 -from app.integrations.utils.schema import PraecoAlertConfig
49 -from app.integrations.utils.schema import PraecoProvisionAlertResponse
50 +from app.integrations.office365.schema.provision import (
51 + PipelineRuleTitles,
52 + PipelineTitles,
53 + ProvisionOffice365AuthKeys,
54 + ProvisionOffice365Response,
55 +)
56 +from app.integrations.utils.schema import (
57 + PraecoAlertConfig,
58 + PraecoProvisionAlertResponse,
59 +)
60 from app.utils import get_connector_attribute
61 +from dotenv import load_dotenv
62 +from fastapi import HTTPException
63 +from loguru import logger
64 +from sqlalchemy import and_, update
65 +from sqlalchemy.ext.asyncio import AsyncSession
66
67 load_dotenv()
68
@@ -66,7 +81,10 @@ async def get_wazuh_configuration() -> str:
81 return response["data"]
82
83
69 -async def office365_template_with_api_type(customer_code: str, provision_office365_auth_keys: ProvisionOffice365AuthKeys) -> str:
84 +async def office365_template_with_api_type(
85 + customer_code: str,
86 + provision_office365_auth_keys: ProvisionOffice365AuthKeys,
87 +) -> str:
88 """
89 Returns a configured Office365 template for Wazuh.
90
@@ -109,7 +127,10 @@ async def office365_template_with_api_type(customer_code: str, provision_office3
127 return template
128
129
112 -async def office365_template(customer_code: str, provision_office365_auth_keys: ProvisionOffice365AuthKeys) -> str:
130 +async def office365_template(
131 + customer_code: str,
132 + provision_office365_auth_keys: ProvisionOffice365AuthKeys,
133 +) -> str:
134 """
135 Returns a configured Office365 template for Wazuh.
136
@@ -166,7 +187,10 @@ async def append_office365_template(wazuh_config: str, office365_template: str)
187 return wazuh_config + office365_template
188
189
169 -async def update_wazuh_configuration(wazuh_config: str, provision_office365_auth_keys: ProvisionOffice365AuthKeys) -> None:
190 +async def update_wazuh_configuration(
191 + wazuh_config: str,
192 + provision_office365_auth_keys: ProvisionOffice365AuthKeys,
193 +) -> None:
194 """
195 Updates the Wazuh configuration. If it fails, remove the <api_type> tag and retry.
196
@@ -179,12 +203,18 @@ async def update_wazuh_configuration(wazuh_config: str, provision_office365_auth
203
204 try:
205 # First attempt to update configuration
182 - response = await send_put_request(endpoint=endpoint, data=data, binary_data=True)
206 + response = await send_put_request(
207 + endpoint=endpoint,
208 + data=data,
209 + binary_data=True,
210 + )
211 if response.get("success") and response["data"].get("error") == 0:
212 logger.info("Wazuh configuration updated successfully.")
213 return
214 else:
187 - logger.error("Failed to update Wazuh configuration. Error: {}".format(response))
215 + logger.error(
216 + "Failed to update Wazuh configuration. Error: {}".format(response),
217 + )
218
219 except Exception as e:
220 logger.error(f"Exception occurred during Wazuh configuration update: {e}")
@@ -195,18 +225,36 @@ async def update_wazuh_configuration(wazuh_config: str, provision_office365_auth
225 data = modified_wazuh_config.encode("utf-8")
226
227 try:
198 - response = await send_put_request(endpoint=endpoint, data=data, binary_data=True)
228 + response = await send_put_request(
229 + endpoint=endpoint,
230 + data=data,
231 + binary_data=True,
232 + )
233 if response.get("success") and response["data"].get("error") == 0:
200 - logger.info("Wazuh configuration updated successfully after removing <api_type> tag.")
234 + logger.info(
235 + "Wazuh configuration updated successfully after removing <api_type> tag.",
236 + )
237 else:
202 - logger.error("Failed to update Wazuh configuration after removing <api_type> tag. Error: {}".format(response))
238 + logger.error(
239 + "Failed to update Wazuh configuration after removing <api_type> tag. Error: {}".format(
240 + response,
241 + ),
242 + )
243
244 except Exception as e:
205 - logger.error(f"Exception occurred during retry of Wazuh configuration update: {e}")
206 - raise HTTPException(status_code=500, detail="Failed to update Wazuh configuration.")
245 + logger.error(
246 + f"Exception occurred during retry of Wazuh configuration update: {e}",
247 + )
248 + raise HTTPException(
249 + status_code=500,
250 + detail="Failed to update Wazuh configuration.",
251 + )
252
253
209 -async def check_if_office365_is_already_provisioned(customer_code: str, wazuh_config: str) -> bool:
254 +async def check_if_office365_is_already_provisioned(
255 + customer_code: str,
256 + wazuh_config: str,
257 +) -> bool:
258 """
259 If the string "Office365 Integration For {customer_code}" is found in the Wazuh configuration, return True.
260
@@ -218,7 +266,10 @@ async def check_if_office365_is_already_provisioned(customer_code: str, wazuh_co
266 bool: True if the Office365 integration is already provisioned, False otherwise.
267 """
268 if f"Office365 Integration For {customer_code}" in wazuh_config:
221 - raise HTTPException(status_code=400, detail=f"Office365 integration already provisioned for customer {customer_code}.")
269 + raise HTTPException(
270 + status_code=400,
271 + detail=f"Office365 integration already provisioned for customer {customer_code}.",
272 + )
273
274
275 async def restart_wazuh_manager() -> None:
@@ -232,7 +283,10 @@ async def restart_wazuh_manager() -> None:
283 ################## ! GRAYLOG ! ##################
284
285
235 -async def build_index_set_config(customer_code: str, session: AsyncSession) -> TimeBasedIndexSet:
286 +async def build_index_set_config(
287 + customer_code: str,
288 + session: AsyncSession,
289 +) -> TimeBasedIndexSet:
290 """
291 Build the configuration for a time-based index set.
292
@@ -270,7 +324,9 @@ async def build_index_set_config(customer_code: str, session: AsyncSession) -> T
324
325
326 # Function to send the POST request and handle the response
273 -async def send_index_set_creation_request(index_set: TimeBasedIndexSet) -> GraylogIndexSetCreationResponse:
327 +async def send_index_set_creation_request(
328 + index_set: TimeBasedIndexSet,
329 +) -> GraylogIndexSetCreationResponse:
330 """
331 Sends a request to create an index set in Graylog.
332
@@ -282,12 +338,18 @@ async def send_index_set_creation_request(index_set: TimeBasedIndexSet) -> Grayl
338 """
339 json_index_set = json.dumps(index_set.dict())
340 logger.info(f"json_index_set set: {json_index_set}")
285 - response_json = await send_post_request(endpoint="/api/system/indices/index_sets", data=index_set.dict())
341 + response_json = await send_post_request(
342 + endpoint="/api/system/indices/index_sets",
343 + data=index_set.dict(),
344 + )
345 return GraylogIndexSetCreationResponse(**response_json)
346
347
348 # Refactored create_index_set function
290 -async def create_index_set(customer_code: str, session: AsyncSession) -> GraylogIndexSetCreationResponse:
349 +async def create_index_set(
350 + customer_code: str,
351 + session: AsyncSession,
352 +) -> GraylogIndexSetCreationResponse:
353 """
354 Creates an index set for a new customer.
355
@@ -358,7 +420,9 @@ async def build_event_stream_config(
420 )
421
422
361 -async def send_event_stream_creation_request(event_stream: Office365EventStream) -> StreamCreationResponse:
423 +async def send_event_stream_creation_request(
424 + event_stream: Office365EventStream,
425 +) -> StreamCreationResponse:
426 """
427 Sends a request to create an event stream.
428
@@ -370,7 +434,10 @@ async def send_event_stream_creation_request(event_stream: Office365EventStream)
434 """
435 json_event_stream = json.dumps(event_stream.dict())
436 logger.info(f"json_event_stream set: {json_event_stream}")
373 - response_json = await send_post_request(endpoint="/api/streams", data=event_stream.dict())
437 + response_json = await send_post_request(
438 + endpoint="/api/streams",
439 + data=event_stream.dict(),
440 + )
441 return StreamCreationResponse(**response_json)
442
443
@@ -390,7 +457,12 @@ async def create_event_stream(
457 Returns:
458 The result of the event stream creation request.
459 """
393 - event_stream_config = await build_event_stream_config(customer_code, provision_office365_auth_keys, index_set_id, session)
460 + event_stream_config = await build_event_stream_config(
461 + customer_code,
462 + provision_office365_auth_keys,
463 + index_set_id,
464 + session,
465 + )
466 return await send_event_stream_creation_request(event_stream_config)
467
468
@@ -416,7 +488,9 @@ async def pipeline_rules_exists(pipeline_rules: PipelineRulesResponse) -> List[s
488 return [
489 rule_title.value
490 for rule_title in PipelineRuleTitles
419 - if not any(rule.title == rule_title.value for rule in pipeline_rules.pipeline_rules)
491 + if not any(
492 + rule.title == rule_title.value for rule in pipeline_rules.pipeline_rules
493 + )
494 ]
495
496
@@ -450,7 +524,13 @@ async def create_office365_utc_rule(rule_title: str) -> None:
524 ' set_field("timestamp_utc", creation_time);\n'
525 "end"
526 )
453 - await create_pipeline_rule(CreatePipelineRule(title=rule_title, description=rule_title, source=rule_source))
527 + await create_pipeline_rule(
528 + CreatePipelineRule(
529 + title=rule_title,
530 + description=rule_title,
531 + source=rule_source,
532 + ),
533 + )
534
535
536 async def create_wazuh_info_rule(rule_title: str) -> None:
@@ -465,7 +545,13 @@ async def create_wazuh_info_rule(rule_title: str) -> None:
545 ' set_field("syslog_level", "INFO");\n'
546 "end"
547 )
468 - await create_pipeline_rule(CreatePipelineRule(title=rule_title, description=rule_title, source=rule_source))
548 + await create_pipeline_rule(
549 + CreatePipelineRule(
550 + title=rule_title,
551 + description=rule_title,
552 + source=rule_source,
553 + ),
554 + )
555
556
557 async def create_wazuh_warning_rule(rule_title: str) -> None:
@@ -480,7 +566,13 @@ async def create_wazuh_warning_rule(rule_title: str) -> None:
566 ' set_field("syslog_level", "WARNING");\n'
567 "end"
568 )
483 - await create_pipeline_rule(CreatePipelineRule(title=rule_title, description=rule_title, source=rule_source))
569 + await create_pipeline_rule(
570 + CreatePipelineRule(
571 + title=rule_title,
572 + description=rule_title,
573 + source=rule_source,
574 + ),
575 + )
576
577
578 async def create_wazuh_notice_rule(rule_title: str) -> None:
@@ -495,7 +587,13 @@ async def create_wazuh_notice_rule(rule_title: str) -> None:
587 ' set_field("syslog_level", "NOTICE");\n'
588 "end"
589 )
498 - await create_pipeline_rule(CreatePipelineRule(title=rule_title, description=rule_title, source=rule_source))
590 + await create_pipeline_rule(
591 + CreatePipelineRule(
592 + title=rule_title,
593 + description=rule_title,
594 + source=rule_source,
595 + ),
596 + )
597
598
599 async def create_wazuh_alert_rule(rule_title: str) -> None:
@@ -503,9 +601,20 @@ async def create_wazuh_alert_rule(rule_title: str) -> None:
601 Creates the 'WAZUH CREATE FIELD SYSLOG LEVEL - ALERT' pipeline rule.
602 """
603 rule_source = (
506 - f'rule "{rule_title}"\n' "when\n" " to_long($message.rule_level) > 11\n" "then\n" ' set_field("syslog_level", "ALERT");\n' "end"
604 + f'rule "{rule_title}"\n'
605 + "when\n"
606 + " to_long($message.rule_level) > 11\n"
607 + "then\n"
608 + ' set_field("syslog_level", "ALERT");\n'
609 + "end"
610 + )
611 + await create_pipeline_rule(
612 + CreatePipelineRule(
613 + title=rule_title,
614 + description=rule_title,
615 + source=rule_source,
616 + ),
617 )
508 - await create_pipeline_rule(CreatePipelineRule(title=rule_title, description=rule_title, source=rule_source))
618
619
620 # ! PIPELINE ! #
@@ -527,7 +636,9 @@ async def pipeline_exists(pipelines: GraylogPipelinesResponse) -> List[str]:
636 return [
637 pipeline_title.value
638 for pipeline_title in PipelineTitles
530 - if not any(pipeline.title == pipeline_title.value for pipeline in pipelines.pipelines)
639 + if not any(
640 + pipeline.title == pipeline_title.value for pipeline in pipelines.pipelines
641 + )
642 ]
643
644
@@ -559,7 +670,13 @@ async def create_office365_pipeline(pipeline_title: str) -> None:
670 'rule "Office365 Timestamp - UTC"\n'
671 "end"
672 )
562 - await create_pipeline_graylog(CreatePipeline(title=pipeline_title, description=pipeline_description, source=pipeline_source))
673 + await create_pipeline_graylog(
674 + CreatePipeline(
675 + title=pipeline_title,
676 + description=pipeline_description,
677 + source=pipeline_source,
678 + ),
679 + )
680
681
682 #### ! GRAFANA ! ####
@@ -582,19 +699,33 @@ async def create_grafana_datasource(
699 grafana_client = await create_grafana_client("Grafana")
700 # Switch to the newly created organization
701 grafana_client.user.switch_actual_user_organisation(
585 - (await get_customer_meta(customer_code, session)).customer_meta.customer_meta_grafana_org_id,
702 + (
703 + await get_customer_meta(customer_code, session)
704 + ).customer_meta.customer_meta_grafana_org_id,
705 )
706 datasource_payload = GrafanaDatasource(
707 name="O365",
708 type="grafana-opensearch-datasource",
709 typeName="OpenSearch",
710 access="proxy",
592 - url=await get_connector_attribute(connector_id=1, column_name="connector_url", session=session),
711 + url=await get_connector_attribute(
712 + connector_id=1,
713 + column_name="connector_url",
714 + session=session,
715 + ),
716 database=f"office365_{customer_code}*",
717 basicAuth=True,
595 - basicAuthUser=await get_connector_attribute(connector_id=1, column_name="connector_username", session=session),
718 + basicAuthUser=await get_connector_attribute(
719 + connector_id=1,
720 + column_name="connector_username",
721 + session=session,
722 + ),
723 secureJsonData={
597 - "basicAuthPassword": await get_connector_attribute(connector_id=1, column_name="connector_password", session=session),
724 + "basicAuthPassword": await get_connector_attribute(
725 + connector_id=1,
726 + column_name="connector_password",
727 + session=session,
728 + ),
729 },
730 isDefault=False,
731 jsonData={
@@ -634,7 +765,10 @@ async def provision_office365(
765 await check_if_office365_is_already_provisioned(customer_code, wazuh_config)
766
767 # Create Office365 template
637 - office365_templated = await office365_template_with_api_type(customer_code, provision_office365_auth_keys)
768 + office365_templated = await office365_template_with_api_type(
769 + customer_code,
770 + provision_office365_auth_keys,
771 + )
772
773 # Append Office365 template to Wazuh configuration
774 wazuh_config = await append_office365_template(wazuh_config, office365_templated)
@@ -650,13 +784,25 @@ async def provision_office365(
784 await check_pipeline()
785
786 # Create Index Set
653 - index_set_id = (await create_index_set(customer_code=customer_code, session=session)).data.id
787 + index_set_id = (
788 + await create_index_set(customer_code=customer_code, session=session)
789 + ).data.id
790 logger.info(f"Index set: {index_set_id}")
791 # Create event stream
656 - stream_id = (await create_event_stream(customer_code, provision_office365_auth_keys, index_set_id, session)).data.stream_id
792 + stream_id = (
793 + await create_event_stream(
794 + customer_code,
795 + provision_office365_auth_keys,
796 + index_set_id,
797 + session,
798 + )
799 + ).data.stream_id
800 pipeline_id = await get_pipeline_id(subscription="OFFICE365")
801 # Combine stream and pipeline IDs
659 - stream_and_pipeline = StreamConnectionToPipelineRequest(stream_id=stream_id, pipeline_ids=pipeline_id)
802 + stream_and_pipeline = StreamConnectionToPipelineRequest(
803 + stream_id=stream_id,
804 + pipeline_ids=pipeline_id,
805 + )
806 # Connect stream to pipeline
807 logger.info(f"Stream and pipeline: {stream_and_pipeline}")
808 await connect_stream_to_pipeline(stream_and_pipeline)
@@ -664,17 +810,23 @@ async def provision_office365(
810 await start_stream(stream_id=stream_id)
811
812 # Grafana Deployment
667 - office365_datasource_uid = (await create_grafana_datasource(customer_code=customer_code, session=session)).datasource.uid
813 + office365_datasource_uid = (
814 + await create_grafana_datasource(customer_code=customer_code, session=session)
815 + ).datasource.uid
816 grafana_o365_folder_id = (
817 await create_grafana_folder(
670 - organization_id=(await get_customer_meta(customer_code, session)).customer_meta.customer_meta_grafana_org_id,
818 + organization_id=(
819 + await get_customer_meta(customer_code, session)
820 + ).customer_meta.customer_meta_grafana_org_id,
821 folder_title="OFFICE 365",
822 )
823 ).id
824 await provision_dashboards(
825 DashboardProvisionRequest(
826 dashboards=[dashboard.name for dashboard in Office365Dashboard],
677 - organizationId=(await get_customer_meta(customer_code, session)).customer_meta.customer_meta_grafana_org_id,
827 + organizationId=(
828 + await get_customer_meta(customer_code, session)
829 + ).customer_meta.customer_meta_grafana_org_id,
830 folderId=grafana_o365_folder_id,
831 datasourceUid=office365_datasource_uid,
832 ),
@@ -684,11 +836,21 @@ async def provision_office365(
836 await provision_alert_in_praeco(
837 PraecoAlertConfig(
838 alert=["post"],
687 - filter=[{"query": {"query_string": {"query": "syslog_level:ALERT AND data_office365_Subscription:Audit.Exchange"}}}],
839 + filter=[
840 + {
841 + "query": {
842 + "query_string": {
843 + "query": "syslog_level:ALERT AND data_office365_Subscription:Audit.Exchange",
844 + },
845 + },
846 + },
847 + ],
848 generate_kibana_discover_url=False,
849 http_post_ignore_ssl_errors=False,
850 http_post_timeout=60,
691 - http_post_url=[f"http://{os.getenv('SERVER_IP')}:5000/api/v1/alerts/office365/exchange"],
851 + http_post_url=[
852 + f"http://{os.getenv('SERVER_IP')}:5000/api/v1/alerts/office365/exchange",
853 + ],
854 import_config="BaseRule.config",
855 index="office365_*",
856 is_enabled=True,
@@ -708,11 +870,21 @@ async def provision_office365(
870 await provision_alert_in_praeco(
871 PraecoAlertConfig(
872 alert=["post"],
711 - filter=[{"query": {"query_string": {"query": "syslog_level:ALERT AND data_office365_UserId:ThreatIntel"}}}],
873 + filter=[
874 + {
875 + "query": {
876 + "query_string": {
877 + "query": "syslog_level:ALERT AND data_office365_UserId:ThreatIntel",
878 + },
879 + },
880 + },
881 + ],
882 generate_kibana_discover_url=False,
883 http_post_ignore_ssl_errors=False,
884 http_post_timeout=60,
715 - http_post_url=[f"http://{os.getenv('SERVER_IP')}:5000/api/v1/alerts/office365/threat_intel"],
885 + http_post_url=[
886 + f"http://{os.getenv('SERVER_IP')}:5000/api/v1/alerts/office365/threat_intel",
887 + ],
888 import_config="BaseRule.config",
889 index="office365_*",
890 is_enabled=True,
@@ -731,11 +903,17 @@ async def provision_office365(
903
904 await update_customer_integration_table(customer_code, session)
905
734 - return ProvisionOffice365Response(success=True, message=f"Successfully provisioned Office365 integration for customer {customer_code}.")
906 + return ProvisionOffice365Response(
907 + success=True,
908 + message=f"Successfully provisioned Office365 integration for customer {customer_code}.",
909 + )
910
911
912 ######### ! Provision in Praeco ! ############
738 -async def provision_alert_in_praeco(request: PraecoAlertConfig, session: AsyncSession) -> PraecoProvisionAlertResponse:
913 +async def provision_alert_in_praeco(
914 + request: PraecoAlertConfig,
915 + session: AsyncSession,
916 +) -> PraecoProvisionAlertResponse:
917 """
918 Provisions the given alert in Praeco. https://github.com/socfortress/Customer-Provisioning-Alert
919
@@ -747,7 +925,11 @@ async def provision_alert_in_praeco(request: PraecoAlertConfig, session: AsyncSe
925 PraecoProvisionAlertResponse: The response object indicating the success or failure of the provisioning operation.
926 """
927 logger.info(f"Provisioning to alert creation - Praeco {request}")
750 - api_endpoint = await get_connector_attribute(connector_id=15, column_name="connector_url", session=session)
928 + api_endpoint = await get_connector_attribute(
929 + connector_id=15,
930 + column_name="connector_url",
931 + session=session,
932 + )
933 # Send the POST request to Praeco
934 response = requests.post(
935 url=f"{api_endpoint}/provision_alert",
@@ -756,13 +938,22 @@ async def provision_alert_in_praeco(request: PraecoAlertConfig, session: AsyncSe
938 logger.info(f"Response: {response.json()}")
939 # Check the response status code
940 if response.status_code != 200:
759 - return PraecoProvisionAlertResponse(success=False, message=f"Failed to provision to Alert Creation App: {response.text}")
941 + return PraecoProvisionAlertResponse(
942 + success=False,
943 + message=f"Failed to provision to Alert Creation App: {response.text}",
944 + )
945 # Return the response
761 - return PraecoProvisionAlertResponse(success=True, message="Successfully provisioned to Alert Creation App.")
946 + return PraecoProvisionAlertResponse(
947 + success=True,
948 + message="Successfully provisioned to Alert Creation App.",
949 + )
950
951
952 ######### ! Update Database ! ############
765 -async def update_customer_integration_table(customer_code: str, session: AsyncSession) -> None:
953 +async def update_customer_integration_table(
954 + customer_code: str,
955 + session: AsyncSession,
956 +) -> None:
957 """
958 Updates the `customer_integrations` table to set the `deployed` column to True where the `customer_code`
959 matches the given customer code and the `integration_service_name` is "Office365".
backend/app/integrations/routes.py
+264 -95
@@ -1,49 +1,42 @@
1 -from typing import List
2 -from typing import Optional
3 -
4 -from fastapi import APIRouter
5 -from fastapi import Depends
6 -from fastapi import HTTPException
7 -from fastapi import Security
8 -from loguru import logger
9 -from sqlalchemy import delete
10 -from sqlalchemy import update
11 -from sqlalchemy.exc import NoResultFound
12 -from sqlalchemy.ext.asyncio import AsyncSession
13 -from sqlalchemy.future import select
14 -from sqlalchemy.orm import joinedload
1 +from typing import List, Optional
2
3 from app.auth.utils import AuthHandler
4 from app.db.db_session import get_db
18 -from app.db.universal_models import Customers
19 -from app.db.universal_models import CustomersMeta
5 +from app.db.universal_models import Customers, CustomersMeta
6 from app.integrations.alert_creation_settings.models.alert_creation_settings import (
7 AlertCreationSettings,
8 )
23 -from app.integrations.models.customer_integration_settings import AvailableIntegrations
24 -from app.integrations.models.customer_integration_settings import CustomerIntegrations
9 from app.integrations.models.customer_integration_settings import (
10 + AvailableIntegrations,
11 + CustomerIntegrations,
12 CustomerIntegrationsMeta,
27 -)
28 -from app.integrations.models.customer_integration_settings import IntegrationAuthKeys
29 -from app.integrations.models.customer_integration_settings import IntegrationConfig
30 -from app.integrations.models.customer_integration_settings import IntegrationService
31 -from app.integrations.models.customer_integration_settings import (
13 + IntegrationAuthKeys,
14 + IntegrationConfig,
15 + IntegrationService,
16 IntegrationSubscription,
17 )
34 -from app.integrations.schema import AuthKey
35 -from app.integrations.schema import AvailableIntegrationsResponse
36 -from app.integrations.schema import CreateIntegrationAuthKeys
37 -from app.integrations.schema import CreateIntegrationService
38 -from app.integrations.schema import CustomerIntegrationCreate
39 -from app.integrations.schema import CustomerIntegrationCreateResponse
40 -from app.integrations.schema import CustomerIntegrationDeleteResponse
41 -from app.integrations.schema import CustomerIntegrationsMetaResponse
42 -from app.integrations.schema import CustomerIntegrationsMetaSchema
43 -from app.integrations.schema import CustomerIntegrationsResponse
44 -from app.integrations.schema import DeleteCustomerIntegration
45 -from app.integrations.schema import IntegrationWithAuthKeys
46 -from app.integrations.schema import UpdateCustomerIntegration
18 +from app.integrations.schema import (
19 + AuthKey,
20 + AvailableIntegrationsResponse,
21 + CreateIntegrationAuthKeys,
22 + CreateIntegrationService,
23 + CustomerIntegrationCreate,
24 + CustomerIntegrationCreateResponse,
25 + CustomerIntegrationDeleteResponse,
26 + CustomerIntegrationsMetaResponse,
27 + CustomerIntegrationsMetaSchema,
28 + CustomerIntegrationsResponse,
29 + DeleteCustomerIntegration,
30 + IntegrationWithAuthKeys,
31 + UpdateCustomerIntegration,
32 +)
33 +from fastapi import APIRouter, Depends, HTTPException, Security
34 +from loguru import logger
35 +from sqlalchemy import delete, update
36 +from sqlalchemy.exc import NoResultFound
37 +from sqlalchemy.ext.asyncio import AsyncSession
38 +from sqlalchemy.future import select
39 +from sqlalchemy.orm import joinedload
40
41 integration_settings_router = APIRouter()
42
@@ -58,7 +51,9 @@ async def fetch_available_integrations(session: AsyncSession):
51 Returns:
52 List[IntegrationWithAuthKeys]: A list of available integrations with their auth keys.
53 """
61 - stmt = select(AvailableIntegrations).options(joinedload(AvailableIntegrations.auth_keys))
54 + stmt = select(AvailableIntegrations).options(
55 + joinedload(AvailableIntegrations.auth_keys),
56 + )
57 result = await session.execute(stmt)
58
59 # Use unique() to avoid duplicates caused by joined eager loading
@@ -66,7 +61,9 @@ async def fetch_available_integrations(session: AsyncSession):
61
62 integrations_with_auth_keys = []
63 for integration in unique_integrations:
69 - auth_keys = [AuthKey(auth_key_name=key.auth_key_name) for key in integration.auth_keys]
64 + auth_keys = [
65 + AuthKey(auth_key_name=key.auth_key_name) for key in integration.auth_keys
66 + ]
67 integration_data = IntegrationWithAuthKeys(
68 id=integration.id,
69 integration_name=integration.integration_name,
@@ -85,33 +82,54 @@ async def validate_integration_name(integration_name: str, session: AsyncSession
82 """
83 available_integrations = await fetch_available_integrations(session)
84 if integration_name not in [ai.integration_name for ai in available_integrations]:
88 - raise HTTPException(status_code=400, detail=f"Integration {integration_name} is not a valid integration.")
85 + raise HTTPException(
86 + status_code=400,
87 + detail=f"Integration {integration_name} is not a valid integration.",
88 + )
89
90
91 -async def validate_integration_auth_keys(integration_name: str, integration_auth_keys: List[AuthKey], session: AsyncSession):
91 +async def validate_integration_auth_keys(
92 + integration_name: str,
93 + integration_auth_keys: List[AuthKey],
94 + session: AsyncSession,
95 +):
96 """
97 Validate if the integration auth keys are valid.
98 """
99 available_integrations = await fetch_available_integrations(session)
96 - integration = [ai for ai in available_integrations if ai.integration_name == integration_name][0]
100 + integration = [
101 + ai for ai in available_integrations if ai.integration_name == integration_name
102 + ][0]
103 available_auth_keys = [ak.auth_key_name for ak in integration.auth_keys]
104 # loop through the `available_auth_keys` and check if the `integration_auth_keys` contains the `auth_key_name`
105 for auth_key in available_auth_keys:
106 if auth_key not in [iak.auth_key_name for iak in integration_auth_keys]:
101 - raise HTTPException(status_code=400, detail=f"Integration auth key {auth_key} does not exist.")
107 + raise HTTPException(
108 + status_code=400,
109 + detail=f"Integration auth key {auth_key} does not exist.",
110 + )
111
112
104 -async def validate_integration_auth_key_update(integration_name: str, integration_auth_key: List[AuthKey], session: AsyncSession):
113 +async def validate_integration_auth_key_update(
114 + integration_name: str,
115 + integration_auth_key: List[AuthKey],
116 + session: AsyncSession,
117 +):
118 """
119 Validate if the integration auth key is valid.
120 """
121 logger.info(f"integration_auth_key: {integration_auth_key}")
122 available_integrations = await fetch_available_integrations(session)
110 - integration = [ai for ai in available_integrations if ai.integration_name == integration_name][0]
123 + integration = [
124 + ai for ai in available_integrations if ai.integration_name == integration_name
125 + ][0]
126 available_auth_keys = [ak.auth_key_name for ak in integration.auth_keys]
127 for auth_key in integration_auth_key:
128 if auth_key.auth_key_name not in available_auth_keys:
114 - raise HTTPException(status_code=400, detail=f"Integration auth key {auth_key.auth_key_name} does not exist.")
129 + raise HTTPException(
130 + status_code=400,
131 + detail=f"Integration auth key {auth_key.auth_key_name} does not exist.",
132 + )
133
134
135 async def validate_customer_code(customer_code: str, session: AsyncSession):
@@ -121,7 +139,10 @@ async def validate_customer_code(customer_code: str, session: AsyncSession):
139 stmt = select(Customers).where(Customers.customer_code == customer_code)
140 result = await session.execute(stmt)
141 if result.scalars().first() is None:
124 - raise HTTPException(status_code=400, detail=f"Customer {customer_code} does not exist.")
142 + raise HTTPException(
143 + status_code=400,
144 + detail=f"Customer {customer_code} does not exist.",
145 + )
146
147
148 async def validate_customer_meta(customer_code: str, session: AsyncSession):
@@ -137,7 +158,11 @@ async def validate_customer_meta(customer_code: str, session: AsyncSession):
158 )
159
160
140 -async def check_existing_customer_integration(customer_code: str, integration_name: str, session: AsyncSession):
161 +async def check_existing_customer_integration(
162 + customer_code: str,
163 + integration_name: str,
164 + session: AsyncSession,
165 +):
166 """
167 Check if the customer integration already exists.
168 """
@@ -146,14 +171,24 @@ async def check_existing_customer_integration(customer_code: str, integration_na
171 select(CustomerIntegrations)
172 .join(CustomerIntegrations.integration_subscriptions)
173 .join(IntegrationSubscription.integration_service)
149 - .where(CustomerIntegrations.customer_code == customer_code, IntegrationService.service_name == integration_name)
174 + .where(
175 + CustomerIntegrations.customer_code == customer_code,
176 + IntegrationService.service_name == integration_name,
177 + )
178 )
179 result = await session.execute(stmt)
180 if result.scalars().first() is not None:
153 - raise HTTPException(status_code=400, detail=f"Customer integration {customer_code} {integration_name} already exists.")
181 + raise HTTPException(
182 + status_code=400,
183 + detail=f"Customer integration {customer_code} {integration_name} already exists.",
184 + )
185
186
156 -async def check_existing_customer_integration_meta(customer_code: str, integration_name: str, session: AsyncSession):
187 +async def check_existing_customer_integration_meta(
188 + customer_code: str,
189 + integration_name: str,
190 + session: AsyncSession,
191 +):
192 """
193 Check if the customer integration meta already exists for the customer code and integration name.
194 """
@@ -180,7 +215,12 @@ async def create_integration_service(
215 integration_service = IntegrationService(
216 service_name=integration_name,
217 auth_type=settings.auth_type,
183 - configs=[IntegrationConfig(config_key=settings.config_key, config_value=settings.config_value)],
218 + configs=[
219 + IntegrationConfig(
220 + config_key=settings.config_key,
221 + config_value=settings.config_value,
222 + ),
223 + ],
224 )
225 session.add(integration_service)
226 await session.flush()
@@ -223,7 +263,12 @@ async def create_integration_subscription(
263 new_integration_subscription = IntegrationSubscription(
264 customer_integrations=customer_integrations,
265 integration_service=integration_service,
226 - integration_auth_keys=[IntegrationAuthKeys(auth_key_name=auth_key.auth_key_name, auth_value=auth_key.auth_value)],
266 + integration_auth_keys=[
267 + IntegrationAuthKeys(
268 + auth_key_name=auth_key.auth_key_name,
269 + auth_value=auth_key.auth_value,
270 + ),
271 + ],
272 )
273 session.add(new_integration_subscription)
274 await session.commit()
@@ -233,9 +278,18 @@ async def get_customer_and_service_ids(session, customer_code, integration_name)
278 try:
279 result = await session.execute(
280 select(CustomerIntegrations.id, IntegrationService.id)
236 - .join(IntegrationSubscription, CustomerIntegrations.id == IntegrationSubscription.customer_id)
237 - .join(IntegrationService, IntegrationSubscription.integration_service_id == IntegrationService.id)
238 - .where(CustomerIntegrations.customer_code == customer_code, IntegrationService.service_name == integration_name),
281 + .join(
282 + IntegrationSubscription,
283 + CustomerIntegrations.id == IntegrationSubscription.customer_id,
284 + )
285 + .join(
286 + IntegrationService,
287 + IntegrationSubscription.integration_service_id == IntegrationService.id,
288 + )
289 + .where(
290 + CustomerIntegrations.customer_code == customer_code,
291 + IntegrationService.service_name == integration_name,
292 + ),
293 )
294 return result.all()
295 except NoResultFound:
@@ -265,23 +319,41 @@ async def get_subscription_ids(session, customer_id, integration_service_id):
319
320
321 async def delete_metadata(session, subscription_ids):
268 - await session.execute(delete(IntegrationAuthKeys).where(IntegrationAuthKeys.subscription_id.in_(subscription_ids)))
322 + await session.execute(
323 + delete(IntegrationAuthKeys).where(
324 + IntegrationAuthKeys.subscription_id.in_(subscription_ids),
325 + ),
326 + )
327
328
329 async def delete_subscriptions(session, subscription_ids):
272 - await session.execute(delete(IntegrationSubscription).where(IntegrationSubscription.id.in_(subscription_ids)))
330 + await session.execute(
331 + delete(IntegrationSubscription).where(
332 + IntegrationSubscription.id.in_(subscription_ids),
333 + ),
334 + )
335
336
337 async def delete_configs(session, integration_service_id):
276 - await session.execute(delete(IntegrationConfig).where(IntegrationConfig.integration_service_id == integration_service_id))
338 + await session.execute(
339 + delete(IntegrationConfig).where(
340 + IntegrationConfig.integration_service_id == integration_service_id,
341 + ),
342 + )
343
344
345 async def delete_integration_service(session, integration_service_id):
280 - await session.execute(delete(IntegrationService).where(IntegrationService.id == integration_service_id))
346 + await session.execute(
347 + delete(IntegrationService).where(
348 + IntegrationService.id == integration_service_id,
349 + ),
350 + )
351
352
353 async def delete_customer_integration_record(session, customer_id):
284 - await session.execute(delete(CustomerIntegrations).where(CustomerIntegrations.id == customer_id))
354 + await session.execute(
355 + delete(CustomerIntegrations).where(CustomerIntegrations.id == customer_id),
356 + )
357
358
359 async def find_customer_integration(
@@ -296,7 +368,11 @@ async def find_customer_integration(
368 return None
369
370
299 -def get_subscription_id(customer_integration, integration_name: str, auth_key_name: str) -> Optional[int]:
371 +def get_subscription_id(
372 + customer_integration,
373 + integration_name: str,
374 + auth_key_name: str,
375 +) -> Optional[int]:
376 for subscription in customer_integration.integration_subscriptions:
377 if subscription.integration_service.service_name == integration_name:
378 for auth_key in subscription.integration_auth_keys:
@@ -305,16 +381,28 @@ def get_subscription_id(customer_integration, integration_name: str, auth_key_na
381 return None
382
383
308 -async def get_tenant_id(customer_integration: CustomerIntegrationCreate, session: AsyncSession) -> str:
384 +async def get_tenant_id(
385 + customer_integration: CustomerIntegrationCreate,
386 + session: AsyncSession,
387 +) -> str:
388 """
389 Retrieves the Tenant ID for a given customer integration. This is the Office365 organization ID and
390 is used to create alerts for the customer in DFIR-IRIS.
391 """
392 stmt = (
393 select(IntegrationAuthKeys)
315 - .join(IntegrationSubscription, IntegrationAuthKeys.subscription_id == IntegrationSubscription.id)
316 - .join(CustomerIntegrations, IntegrationSubscription.customer_id == CustomerIntegrations.id)
317 - .join(IntegrationService, IntegrationSubscription.integration_service_id == IntegrationService.id)
394 + .join(
395 + IntegrationSubscription,
396 + IntegrationAuthKeys.subscription_id == IntegrationSubscription.id,
397 + )
398 + .join(
399 + CustomerIntegrations,
400 + IntegrationSubscription.customer_id == CustomerIntegrations.id,
401 + )
402 + .join(
403 + IntegrationService,
404 + IntegrationSubscription.integration_service_id == IntegrationService.id,
405 + )
406 .where(
407 CustomerIntegrations.customer_code == customer_integration.customer_code,
408 IntegrationService.service_name == customer_integration.integration_name,
@@ -325,12 +413,19 @@ async def get_tenant_id(customer_integration: CustomerIntegrationCreate, session
413 result = await session.execute(stmt)
414 tenant_id = result.scalars().first()
415 if tenant_id is None:
328 - raise HTTPException(status_code=404, detail=f"Tenant ID for customer {customer_integration.customer_code} not found.")
416 + raise HTTPException(
417 + status_code=404,
418 + detail=f"Tenant ID for customer {customer_integration.customer_code} not found.",
419 + )
420 logger.info(f"tenant_id: {tenant_id.auth_value}")
421 return tenant_id.auth_value
422
423
333 -async def update_office365_organization_id(customer_code: str, tenant_id: str, session: AsyncSession):
424 +async def update_office365_organization_id(
425 + customer_code: str,
426 + tenant_id: str,
427 + session: AsyncSession,
428 +):
429 """
430 Updates the Office365 organization ID in the alert_creation_settings table.
431 """
@@ -343,27 +438,43 @@ async def update_office365_organization_id(customer_code: str, tenant_id: str, s
438 await session.commit()
439
440
346 -async def get_integration_service_id(integration_name: str, session: AsyncSession) -> int:
441 +async def get_integration_service_id(
442 + integration_name: str,
443 + session: AsyncSession,
444 +) -> int:
445 """
446 Retrieves the AvailableIntegrations ID for a given integration name.
447 """
350 - stmt = select(AvailableIntegrations).where(AvailableIntegrations.integration_name == integration_name)
448 + stmt = select(AvailableIntegrations).where(
449 + AvailableIntegrations.integration_name == integration_name,
450 + )
451 result = await session.execute(stmt)
452 integration_service = result.scalars().first()
453 if integration_service is None:
354 - raise HTTPException(status_code=404, detail=f"Integration service {integration_name} not found.")
454 + raise HTTPException(
455 + status_code=404,
456 + detail=f"Integration service {integration_name} not found.",
457 + )
458 return integration_service.id
459
460
358 -async def get_integration_service_name(integration_name: str, session: AsyncSession) -> str:
461 +async def get_integration_service_name(
462 + integration_name: str,
463 + session: AsyncSession,
464 +) -> str:
465 """
466 Retrieves the AvailableIntegrations ID for a given integration name.
467 """
362 - stmt = select(AvailableIntegrations).where(AvailableIntegrations.integration_name == integration_name)
468 + stmt = select(AvailableIntegrations).where(
469 + AvailableIntegrations.integration_name == integration_name,
470 + )
471 result = await session.execute(stmt)
472 integration_service = result.scalars().first()
473 if integration_service is None:
366 - raise HTTPException(status_code=404, detail=f"Integration service {integration_name} not found.")
474 + raise HTTPException(
475 + status_code=404,
476 + detail=f"Integration service {integration_name} not found.",
477 + )
478 return integration_service.integration_name
479
480
@@ -372,8 +483,12 @@ async def fetch_customer_integrations_data(session: AsyncSession):
483 Fetches customer integrations data from the database.
484 """
485 stmt = select(CustomerIntegrations).options(
375 - joinedload(CustomerIntegrations.integration_subscriptions).joinedload(IntegrationSubscription.integration_service),
376 - joinedload(CustomerIntegrations.integration_subscriptions).subqueryload(IntegrationSubscription.integration_auth_keys),
486 + joinedload(CustomerIntegrations.integration_subscriptions).joinedload(
487 + IntegrationSubscription.integration_service,
488 + ),
489 + joinedload(CustomerIntegrations.integration_subscriptions).subqueryload(
490 + IntegrationSubscription.integration_auth_keys,
491 + ),
492 )
493 result = await session.execute(stmt)
494 return result.scalars().unique().all()
@@ -385,14 +500,20 @@ def process_customer_integrations(customer_integrations_data):
500 """
501 processed_customer_integrations = []
502 for ci in customer_integrations_data:
388 - first_service_id = ci.integration_subscriptions[0].integration_service_id if ci.integration_subscriptions else None
503 + first_service_id = (
504 + ci.integration_subscriptions[0].integration_service_id
505 + if ci.integration_subscriptions
506 + else None
507 + )
508 customer_integration_obj = CustomerIntegrations(
509 id=ci.id,
510 customer_code=ci.customer_code,
511 customer_name=ci.customer_name,
512 integration_subscriptions=ci.integration_subscriptions,
513 integration_service_id=first_service_id,
395 - integration_service_name=ci.integration_subscriptions[0].integration_service.service_name
514 + integration_service_name=ci.integration_subscriptions[
515 + 0
516 + ].integration_service.service_name
517 if ci.integration_subscriptions
518 else None,
519 deployed=ci.deployed,
@@ -432,7 +553,9 @@ async def get_customer_integrations(session: AsyncSession = Depends(get_db)):
553 Endpoint to get a list of customer integrations.
554 """
555 customer_integrations_data = await fetch_customer_integrations_data(session)
435 - processed_customer_integrations = process_customer_integrations(customer_integrations_data)
556 + processed_customer_integrations = process_customer_integrations(
557 + customer_integrations_data,
558 + )
559
560 logger.info(f"Processed customer_integrations: {processed_customer_integrations}")
561 return CustomerIntegrationsResponse(
@@ -484,7 +607,9 @@ async def get_customer_integrations_by_customer_code(
607 stmt = (
608 select(CustomerIntegrations)
609 .options(
487 - joinedload(CustomerIntegrations.integration_subscriptions).joinedload(IntegrationSubscription.integration_service),
610 + joinedload(CustomerIntegrations.integration_subscriptions).joinedload(
611 + IntegrationSubscription.integration_service,
612 + ),
613 joinedload(CustomerIntegrations.integration_subscriptions).subqueryload(
614 IntegrationSubscription.integration_auth_keys,
615 ), # Load IntegrationAuthKeys
@@ -514,7 +639,9 @@ async def get_customer_integrations_meta_by_customer_code(
639 """
640 Endpoint to get a list of customer integrations metadata for a specific customer.
641 """
517 - stmt = select(CustomerIntegrationsMeta).where(CustomerIntegrationsMeta.customer_code == customer_code)
642 + stmt = select(CustomerIntegrationsMeta).where(
643 + CustomerIntegrationsMeta.customer_code == customer_code,
644 + )
645 result = await session.execute(stmt)
646 customer_integrations_meta = result.scalars().all()
647 logger.info(f"customer_integrations_meta: {customer_integrations_meta}")
@@ -538,7 +665,10 @@ async def create_integration(
665 """
666 Endpoint to create a new customer integration.
667 """
541 - await validate_integration_name(customer_integration_create.integration_name, session)
668 + await validate_integration_name(
669 + customer_integration_create.integration_name,
670 + session,
671 + )
672 await validate_integration_auth_keys(
673 customer_integration_create.integration_name,
674 customer_integration_create.integration_auth_keys,
@@ -551,8 +681,14 @@ async def create_integration(
681 customer_integration_create.integration_name,
682 session,
683 )
554 - integration_service_id = await get_integration_service_id(customer_integration_create.integration_name, session)
555 - integration_service_name = await get_integration_service_name(customer_integration_create.integration_name, session)
684 + integration_service_id = await get_integration_service_id(
685 + customer_integration_create.integration_name,
686 + session,
687 + )
688 + integration_service_name = await get_integration_service_name(
689 + customer_integration_create.integration_name,
690 + session,
691 + )
692
693 integration_service = await create_integration_service(
694 customer_integration_create.integration_name,
@@ -576,7 +712,11 @@ async def create_integration(
712 # Office365 specific integration handling
713 if customer_integration_create.integration_name == "Office365":
714 tenant_id = await get_tenant_id(customer_integration_create, session)
579 - await update_office365_organization_id(customer_integration_create.customer_code, tenant_id, session)
715 + await update_office365_organization_id(
716 + customer_integration_create.customer_code,
717 + tenant_id,
718 + session,
719 + )
720
721 return CustomerIntegrationCreateResponse(
722 message=f"Customer integration {customer_integration_create.customer_code} {customer_integration_create.integration_name} successfully created.",
@@ -634,8 +774,14 @@ async def update_integration(
774 customer_integration_update: UpdateCustomerIntegration,
775 session: AsyncSession = Depends(get_db),
776 ):
637 - await validate_integration_name(customer_integration_update.integration_name, session)
638 - customer_integration_response = await get_customer_integrations_by_customer_code(customer_code, session)
777 + await validate_integration_name(
778 + customer_integration_update.integration_name,
779 + session,
780 + )
781 + customer_integration_response = await get_customer_integrations_by_customer_code(
782 + customer_code,
783 + session,
784 + )
785
786 if not customer_integration_response:
787 raise HTTPException(status_code=404, detail="Customer integrations not found")
@@ -647,7 +793,10 @@ async def update_integration(
793 )
794
795 if not customer_integration:
650 - raise HTTPException(status_code=404, detail="Customer integration with specified service name not found.")
796 + raise HTTPException(
797 + status_code=404,
798 + detail="Customer integration with specified service name not found.",
799 + )
800
801 await validate_integration_auth_key_update(
802 customer_integration_update.integration_name,
@@ -670,7 +819,9 @@ async def update_integration(
819 await session.execute(
820 update(IntegrationAuthKeys)
821 .where(IntegrationAuthKeys.subscription_id == subscription_id)
673 - .values(auth_value=customer_integration_update.integration_auth_keys[0].auth_value),
822 + .values(
823 + auth_value=customer_integration_update.integration_auth_keys[0].auth_value,
824 + ),
825 )
826
827 await session.commit()
@@ -695,12 +846,17 @@ async def update_available_integrations(
846 Endpoint to update an available integration.
847 """
848 for integration in available_integrations:
698 - stmt = select(AvailableIntegrations).where(AvailableIntegrations.integration_name == integration.integration_name)
849 + stmt = select(AvailableIntegrations).where(
850 + AvailableIntegrations.integration_name == integration.integration_name,
851 + )
852 result = await session.execute(stmt)
853 existing_integration = result.scalars().first()
854
855 if existing_integration is None:
703 - raise HTTPException(status_code=404, detail=f"Integration {integration.integration_name} not found.")
856 + raise HTTPException(
857 + status_code=404,
858 + detail=f"Integration {integration.integration_name} not found.",
859 + )
860
861 existing_integration.description = integration.description
862 existing_integration.integration_details = integration.integration_details
@@ -727,7 +883,11 @@ async def delete_integration(
883 customer_code = delete_customer_integration.customer_code
884 integration_name = delete_customer_integration.integration_name
885
730 - results = await get_customer_and_service_ids(session, customer_code, integration_name)
886 + results = await get_customer_and_service_ids(
887 + session,
888 + customer_code,
889 + integration_name,
890 + )
891 # Check if results is not empty
892 if results:
893 # Unpack the first tuple in results
@@ -736,9 +896,16 @@ async def delete_integration(
896 # Handle the case where results is empty
897 raise HTTPException(status_code=404, detail="Customer integration not found")
898
739 - subscription_ids = await get_subscription_ids(session, customer_id, integration_service_id)
899 + subscription_ids = await get_subscription_ids(
900 + session,
901 + customer_id,
902 + integration_service_id,
903 + )
904 if not subscription_ids:
741 - raise HTTPException(status_code=404, detail="No subscriptions found for customer integration")
905 + raise HTTPException(
906 + status_code=404,
907 + detail="No subscriptions found for customer integration",
908 + )
909
910 await delete_metadata(session, subscription_ids)
911 await delete_subscriptions(session, subscription_ids)
@@ -769,8 +936,10 @@ async def delete_integration_meta(
936 """
937 try:
938 stmt = delete(CustomerIntegrationsMeta).where(
772 - CustomerIntegrationsMeta.customer_code == customer_integration_meta.customer_code,
773 - CustomerIntegrationsMeta.integration_name == customer_integration_meta.integration_name,
939 + CustomerIntegrationsMeta.customer_code
940 + == customer_integration_meta.customer_code,
941 + CustomerIntegrationsMeta.integration_name
942 + == customer_integration_meta.integration_name,
943 )
944 await session.execute(stmt)
945 await session.commit()
backend/app/integrations/schema.py
+2 -4
@@ -1,8 +1,6 @@
1 -from typing import List
2 -from typing import Optional
1 +from typing import List, Optional
2
4 -from pydantic import BaseModel
5 -from pydantic import Field
3 +from pydantic import BaseModel, Field
4
5
6 class AuthKey(BaseModel):
backend/app/integrations/utils/alerts.py
+9 -7
@@ -1,19 +1,16 @@
1 import ipaddress
2 import re
3 from abc import ABC
4 -from typing import Dict
5 -from typing import Optional
6 -from typing import Union
4 +from typing import Dict, Optional, Union
5
6 import httpx
7 import regex
8 +from app.integrations.utils.schema import ShufflePayload
9 +from app.utils import get_customer_alert_settings
10 from fastapi import HTTPException
11 from loguru import logger
12 from sqlalchemy.ext.asyncio import AsyncSession
13
14 -from app.integrations.utils.schema import ShufflePayload
15 -from app.utils import get_customer_alert_settings
16 -
14
15 #################### ! DFIR IRIS ASSET VALIDATOR ! ####################
16 class AssetValidator(ABC):
@@ -334,7 +331,12 @@ async def send_to_shuffle(payload: ShufflePayload, session: AsyncSession) -> boo
331 try:
332 async with httpx.AsyncClient(verify=False) as client:
333 response = await client.post(
337 - (await get_customer_alert_settings(customer_code=payload.customer_code, session=session)).shuffle_endpoint,
334 + (
335 + await get_customer_alert_settings(
336 + customer_code=payload.customer_code,
337 + session=session,
338 + )
339 + ).shuffle_endpoint,
340 json=payload.to_dict(),
341 )
342
backend/app/integrations/utils/collection.py
+33 -11
@@ -1,7 +1,5 @@
1 import asyncio
2 -from typing import Any
3 -from typing import Dict
4 -from typing import Optional
2 +from typing import Any, Dict, Optional
3
4 import httpx
5 from loguru import logger
@@ -25,23 +23,35 @@ async def send_get_request(
23 try:
24 response = await client.get(endpoint, params=params, headers=headers)
25 response.raise_for_status()
28 - return {"data": response.json(), "success": True, "message": "Successfully retrieved data"}
26 + return {
27 + "data": response.json(),
28 + "success": True,
29 + "message": "Successfully retrieved data",
30 + }
31 except httpx.HTTPError as e:
32 return {"success": False, "message": f"Failed to retrieve data: {e}"}
33
34
33 -async def send_post_request(endpoint: str, data: Dict[str, Any], headers: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
35 +async def send_post_request(
36 + endpoint: str,
37 + data: Dict[str, Any],
38 + headers: Optional[Dict[str, Any]] = None,
39 +) -> Dict[str, Any]:
40 """
41 Send a POST request to the given endpoint.
42 """
43 async with httpx.AsyncClient() as client:
44 try:
39 - logger.info(f"Sending POST request to {endpoint} with data: {data} and headers: {headers}")
45 + logger.info(
46 + f"Sending POST request to {endpoint} with data: {data} and headers: {headers}",
47 + )
48 response = await client.post(endpoint, json=data, headers=headers)
49
50 if response.status_code == 429:
51 retry_after = int(response.headers.get("X-RateLimit-Reset", 1))
44 - logger.warning(f"Rate limit exceeded. Retrying after {retry_after} seconds.")
52 + logger.warning(
53 + f"Rate limit exceeded. Retrying after {retry_after} seconds.",
54 + )
55 await asyncio.sleep(retry_after)
56 response = await client.post(endpoint, json=data, headers=headers)
57
@@ -54,11 +64,23 @@ async def send_post_request(endpoint: str, data: Dict[str, Any], headers: Option
64 logger.info(f"Content-Type: {content_type}")
65
66 if "application/json" in content_type:
57 - logger.info(f"Successfully retrieved data from {endpoint} with data: {data} and headers: {headers}")
58 - return {"data": response.json(), "success": True, "message": "Successfully retrieved data"}
67 + logger.info(
68 + f"Successfully retrieved data from {endpoint} with data: {data} and headers: {headers}",
69 + )
70 + return {
71 + "data": response.json(),
72 + "success": True,
73 + "message": "Successfully retrieved data",
74 + }
75 else:
60 - logger.info(f"Successfully retrieved data from {endpoint} with data: {data} and headers: {headers}")
61 - return {"data": response.content, "success": True, "message": "Successfully retrieved data"}
76 + logger.info(
77 + f"Successfully retrieved data from {endpoint} with data: {data} and headers: {headers}",
78 + )
79 + return {
80 + "data": response.content,
81 + "success": True,
82 + "message": "Successfully retrieved data",
83 + }
84
85 except Exception as e:
86 error_message = f"Failed to send POST request: {str(e)}"
backend/app/integrations/utils/event_shipper.py
+37 -15
@@ -1,15 +1,15 @@
1 import asyncio
2 -from typing import Any
3 -from typing import Dict
4 -
5 -from fastapi import HTTPException
6 -from loguru import logger
2 +from typing import Any, Dict
3
4 from app.connectors.event_shipper.utils.universal import create_gelf_logger
5 from app.connectors.utils import get_connector_info_from_db
6 from app.db.db_session import get_db_session
11 -from app.integrations.utils.schema import EventShipperPayload
12 -from app.integrations.utils.schema import EventShipperPayloadResponse
7 +from app.integrations.utils.schema import (
8 + EventShipperPayload,
9 + EventShipperPayloadResponse,
10 +)
11 +from fastapi import HTTPException
12 +from loguru import logger
13
14
15 async def get_gelf_logger():
@@ -18,7 +18,10 @@ async def get_gelf_logger():
18 return gelf_logger
19 except Exception as e:
20 logger.error(f"Failed to initialize GelfLogger: {e}")
21 - raise HTTPException(status_code=500, detail=f"Failed to initialize GelfLogger: {e}")
21 + raise HTTPException(
22 + status_code=500,
23 + detail=f"Failed to initialize GelfLogger: {e}",
24 + )
25
26
27 async def event_shipper(message: EventShipperPayload) -> EventShipperPayloadResponse:
@@ -31,9 +34,15 @@ async def event_shipper(message: EventShipperPayload) -> EventShipperPayloadResp
34 await gelf_logger.tcp_handler(message=message)
35 except Exception as e:
36 logger.error(f"Failed to send test message to log shipper: {e}")
34 - raise HTTPException(status_code=500, detail=f"Failed to send test message to log shipper: {e}")
37 + raise HTTPException(
38 + status_code=500,
39 + detail=f"Failed to send test message to log shipper: {e}",
40 + )
41
36 - return EventShipperPayloadResponse(success=True, message="Successfully sent test message to log shipper.")
42 + return EventShipperPayloadResponse(
43 + success=True,
44 + message="Successfully sent test message to log shipper.",
45 + )
46
47
48 async def verify_event_shipper_healtcheck(attributes: Dict[str, Any]) -> Dict[str, Any]:
@@ -43,17 +52,30 @@ async def verify_event_shipper_healtcheck(attributes: Dict[str, Any]) -> Dict[st
52 Returns:
53 dict: A dictionary containing 'connectionSuccessful' status.
54 """
46 - logger.info(f"Verifying the event shipper connection to {attributes['connector_url']}")
55 + logger.info(
56 + f"Verifying the event shipper connection to {attributes['connector_url']}",
57 + )
58
59 # MAke a TCP connection to the Graylog Input
60 try:
50 - reader, writer = await asyncio.open_connection(attributes["connector_url"], attributes["connector_extra_data"])
61 + reader, writer = await asyncio.open_connection(
62 + attributes["connector_url"],
63 + attributes["connector_extra_data"],
64 + )
65 writer.close()
66 await writer.wait_closed()
53 - return {"connectionSuccessful": True, "message": "Event shipper healthcheck successful"}
67 + return {
68 + "connectionSuccessful": True,
69 + "message": "Event shipper healthcheck successful",
70 + }
71 except Exception as e:
55 - logger.error(f"Connection to {attributes['connector_url']} failed with error: {e}")
56 - return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed"}
72 + logger.error(
73 + f"Connection to {attributes['connector_url']} failed with error: {e}",
74 + )
75 + return {
76 + "connectionSuccessful": False,
77 + "message": f"Connection to {attributes['connector_url']} failed",
78 + }
79
80
81 async def verify_event_shipper_connection(connector_name: str) -> str:
backend/app/integrations/utils/schema.py
+2 -5
@@ -1,9 +1,6 @@
1 -from typing import List
2 -from typing import Optional
1 +from typing import List, Optional
2
4 -from pydantic import BaseModel
5 -from pydantic import Extra
6 -from pydantic import Field
3 +from pydantic import BaseModel, Extra, Field
4
5
6 class WazuhOSInfo(BaseModel):
backend/app/integrations/utils/utils.py
+17 -8
@@ -1,15 +1,16 @@
1 from typing import Dict
2
3 +from app.integrations.routes import get_customer_integrations_by_customer_code
4 +from app.integrations.schema import CustomerIntegrations, CustomerIntegrationsResponse
5 from fastapi import HTTPException
6 from loguru import logger
7 from sqlalchemy.ext.asyncio import AsyncSession
8
7 -from app.integrations.routes import get_customer_integrations_by_customer_code
8 -from app.integrations.schema import CustomerIntegrations
9 -from app.integrations.schema import CustomerIntegrationsResponse
10 -
9
12 -async def get_customer_integration_response(customer_code: str, session: AsyncSession) -> CustomerIntegrationsResponse:
10 +async def get_customer_integration_response(
11 + customer_code: str,
12 + session: AsyncSession,
13 +) -> CustomerIntegrationsResponse:
14 """
15 Retrieves the integration response for a customer.
16
@@ -23,13 +24,21 @@ async def get_customer_integration_response(customer_code: str, session: AsyncSe
24 Raises:
25 HTTPException: If the customer integration settings are not found.
26 """
26 - customer_integration_response = await get_customer_integrations_by_customer_code(customer_code, session)
27 + customer_integration_response = await get_customer_integrations_by_customer_code(
28 + customer_code,
29 + session,
30 + )
31 if customer_integration_response.available_integrations == []:
28 - raise HTTPException(status_code=404, detail="Customer integration settings not found.")
32 + raise HTTPException(
33 + status_code=404,
34 + detail="Customer integration settings not found.",
35 + )
36 return customer_integration_response
37
38
32 -def extract_mimecast_auth_keys(customer_integration: CustomerIntegrations) -> Dict[str, str]:
39 +def extract_mimecast_auth_keys(
40 + customer_integration: CustomerIntegrations,
41 +) -> Dict[str, str]:
42 """
43 Extracts the authentication keys for Office365 integration from the given customer integration.
44
backend/app/middleware/exception_handlers.py
+4 -9
@@ -1,16 +1,11 @@
1 -from fastapi import HTTPException
2 -from fastapi import Request
1 +from app.auth.utils import AuthHandler
2 +from app.db.db_session import async_engine # Make sure to import the async engine
3 +from app.utils import ErrorType, Logger, ValidationErrorItem, ValidationErrorResponse
4 +from fastapi import HTTPException, Request
5 from fastapi.exceptions import RequestValidationError
6 from fastapi.responses import JSONResponse
7 from sqlalchemy.ext.asyncio import AsyncSession
8
7 -from app.auth.utils import AuthHandler
8 -from app.db.db_session import async_engine # Make sure to import the async engine
9 -from app.utils import ErrorType
10 -from app.utils import Logger
11 -from app.utils import ValidationErrorItem
12 -from app.utils import ValidationErrorResponse
13 -
9
10 # Utility function to get user_id from request
11 async def get_user_id_from_request(request: Request, logger_instance):
backend/app/middleware/logger.py
+25 -10
@@ -1,11 +1,9 @@
1 -from fastapi import HTTPException
2 -from fastapi import Request
3 -from fastapi.responses import JSONResponse
4 -from sqlalchemy.ext.asyncio import AsyncSession
5 -
1 from app.auth.utils import AuthHandler
2 from app.db.db_session import async_engine
3 from app.utils import Logger
4 +from fastapi import HTTPException, Request
5 +from fastapi.responses import JSONResponse
6 +from sqlalchemy.ext.asyncio import AsyncSession
7
8 EXCLUDED_PATHS = ["/auth/token", "/auth/register"]
9 INTERNAL_SERVER_ERROR = 500
@@ -56,12 +54,24 @@ async def handle_exception(e, user_id, request, logger_instance):
54 JSONResponse: The response containing the error message.
55 """
56 try:
59 - user_id = await logger_instance.get_user_id_from_request(request) if user_id is None else user_id
57 + user_id = (
58 + await logger_instance.get_user_id_from_request(request)
59 + if user_id is None
60 + else user_id
61 + )
62 except HTTPException as http_exc:
61 - return JSONResponse(status_code=http_exc.status_code, content={"message": str(http_exc), "success": False})
63 + return JSONResponse(
64 + status_code=http_exc.status_code,
65 + content={"message": str(http_exc), "success": False},
66 + )
67 await logger_instance.log_error(user_id, request, e)
63 - status_code = e.status_code if isinstance(e, HTTPException) else INTERNAL_SERVER_ERROR
64 - return JSONResponse(status_code=status_code, content={"message": str(e), "success": False})
68 + status_code = (
69 + e.status_code if isinstance(e, HTTPException) else INTERNAL_SERVER_ERROR
70 + )
71 + return JSONResponse(
72 + status_code=status_code,
73 + content={"message": str(e), "success": False},
74 + )
75
76
77 async def log_requests(request: Request, call_next):
@@ -84,7 +94,12 @@ async def log_requests(request: Request, call_next):
94
95 try:
96 if not is_excluded_path(request.url.path):
87 - response, user_id = await process_request(request, call_next, session, logger_instance)
97 + response, user_id = await process_request(
98 + request,
99 + call_next,
100 + session,
101 + logger_instance,
102 + )
103 else:
104 response = await call_next(request)
105 except Exception as e:
backend/app/routers/agents.py
+1 -2
@@ -1,6 +1,5 @@
1 -from fastapi import APIRouter
2 -
1 from app.agents.routes.agents import agents_router
2 +from fastapi import APIRouter
3
4 # Instantiate the APIRouter
5 router = APIRouter()
backend/app/routers/alert_creation.py
+11 -4
@@ -1,13 +1,20 @@
1 -from fastapi import APIRouter
2 -
1 from app.integrations.alert_creation.general.routes.alert import general_alerts_router
2 from app.integrations.alert_creation.office365.routes.alert import (
3 office365_alerts_router,
4 )
5 +from fastapi import APIRouter
6
7 # Instantiate the APIRouter
8 router = APIRouter()
9
10 # Include the Ask SocFortress related routes
12 -router.include_router(general_alerts_router, prefix="/api/v1/alerts/general", tags=["Alert Creation"])
13 -router.include_router(office365_alerts_router, prefix="/api/v1/alerts/office365", tags=["Alert Creation"])
11 +router.include_router(
12 + general_alerts_router,
13 + prefix="/api/v1/alerts/general",
14 + tags=["Alert Creation"],
15 +)
16 +router.include_router(
17 + office365_alerts_router,
18 + prefix="/api/v1/alerts/office365",
19 + tags=["Alert Creation"],
20 +)
backend/app/routers/alert_creation_settings.py
+6 -3
@@ -1,11 +1,14 @@
1 -from fastapi import APIRouter
2 -
1 from app.integrations.alert_creation_settings.routes.alert_creation_settings import (
2 alert_creation_settings_router,
3 )
4 +from fastapi import APIRouter
5
6 # Instantiate the APIRouter
7 router = APIRouter()
8
9 # Include the Ask SocFortress related routes
11 -router.include_router(alert_creation_settings_router, prefix="/api/v1/alert_settings", tags=["Alert Creation Settings"])
10 +router.include_router(
11 + alert_creation_settings_router,
12 + prefix="/api/v1/alert_settings",
13 + tags=["Alert Creation Settings"],
14 +)
backend/app/routers/ask_socfortress.py
+6 -3
@@ -1,11 +1,14 @@
1 -from fastapi import APIRouter
2 -
1 from app.integrations.ask_socfortress.routes.ask_socfortress import (
2 ask_socfortress_router,
3 )
4 +from fastapi import APIRouter
5
6 # Instantiate the APIRouter
7 router = APIRouter()
8
9 # Include the Ask SocFortress related routes
11 -router.include_router(ask_socfortress_router, prefix="/ask_socfortress", tags=["Ask SocFortress Integration"])
10 +router.include_router(
11 + ask_socfortress_router,
12 + prefix="/ask_socfortress",
13 + tags=["Ask SocFortress Integration"],
14 +)
backend/app/routers/auth.py
+1 -2
@@ -1,6 +1,5 @@
1 -from fastapi import APIRouter
2 -
1 from app.auth.routes.auth import auth_router
2 +from fastapi import APIRouter
3
4 # Instantiate the APIRouter
5 router = APIRouter()
backend/app/routers/connectors.py
+1 -2
@@ -1,6 +1,5 @@
1 -from fastapi import APIRouter
2 -
1 from app.connectors.routes import connector_router
2 +from fastapi import APIRouter
3
4 router = APIRouter()
5
backend/app/routers/cortex.py
+6 -3
@@ -1,9 +1,12 @@
1 -from fastapi import APIRouter
2 -
1 from app.connectors.cortex.routes.analyzers import cortex_analyzer_router
2 +from fastapi import APIRouter
3
4 # Instantiate the APIRouter
5 router = APIRouter()
6
7 # Include the Cortex related routes
9 -router.include_router(cortex_analyzer_router, prefix="/analyzers", tags=["cortex-analyzers"])
8 +router.include_router(
9 + cortex_analyzer_router,
10 + prefix="/analyzers",
11 + tags=["cortex-analyzers"],
12 +)
backend/app/routers/customer_provisioning.py
+11 -4
@@ -1,13 +1,20 @@
1 -from fastapi import APIRouter
2 -
1 from app.customer_provisioning.routes.decommission import (
2 customer_decommissioning_router,
3 )
4 from app.customer_provisioning.routes.provision import customer_provisioning_router
5 +from fastapi import APIRouter
6
7 # Instantiate the APIRouter
8 router = APIRouter()
9
10 # Include the Shuffle related routes
12 -router.include_router(customer_provisioning_router, prefix="/customer_provisioning", tags=["Customer Provisioning"])
13 -router.include_router(customer_decommissioning_router, prefix="/customer_provisioning", tags=["Customer Provisioning"])
11 +router.include_router(
12 + customer_provisioning_router,
13 + prefix="/customer_provisioning",
14 + tags=["Customer Provisioning"],
15 +)
16 +router.include_router(
17 + customer_decommissioning_router,
18 + prefix="/customer_provisioning",
19 + tags=["Customer Provisioning"],
20 +)
backend/app/routers/customers.py
+1 -2
@@ -1,6 +1,5 @@
1 -from fastapi import APIRouter
2 -
1 from app.customers.routes.customers import customers_router
2 +from fastapi import APIRouter
3
4 # Instantiate the APIRouter
5 router = APIRouter()
backend/app/routers/dfir_iris.py
+16 -5
@@ -1,5 +1,3 @@
1 -from fastapi import APIRouter
2 -
1 from app.connectors.dfir_iris.routes.alerts import dfir_iris_alerts_router
2 from app.connectors.dfir_iris.routes.assets import dfir_iris_assets_router
3 from app.connectors.dfir_iris.routes.cases import dfir_iris_cases_router
@@ -8,14 +6,27 @@ from app.connectors.dfir_iris.routes.users import dfir_iris_users_router
6 from app.integrations.alert_escalation.routes.general_alert import (
7 integration_general_alerts_router,
8 )
9 +from fastapi import APIRouter
10
11 # Instantiate the APIRouter
12 router = APIRouter()
13
14 # Include the DFIR Iris related routes
16 -router.include_router(dfir_iris_alerts_router, prefix="/soc/alerts", tags=["soc-alerts"])
17 -router.include_router(dfir_iris_assets_router, prefix="/soc/assets", tags=["soc-assets"])
15 +router.include_router(
16 + dfir_iris_alerts_router,
17 + prefix="/soc/alerts",
18 + tags=["soc-alerts"],
19 +)
20 +router.include_router(
21 + dfir_iris_assets_router,
22 + prefix="/soc/assets",
23 + tags=["soc-assets"],
24 +)
25 router.include_router(dfir_iris_cases_router, prefix="/soc/cases", tags=["soc-cases"])
26 router.include_router(dfir_iris_notes_router, prefix="/soc/notes", tags=["soc-notes"])
27 router.include_router(dfir_iris_users_router, prefix="/soc/users", tags=["soc-users"])
21 -router.include_router(integration_general_alerts_router, prefix="/soc/general_alert", tags=["soc-general-alerts"])
28 +router.include_router(
29 + integration_general_alerts_router,
30 + prefix="/soc/general_alert",
31 + tags=["soc-general-alerts"],
32 +)
backend/app/routers/dnstwist.py
+1 -2
@@ -1,6 +1,5 @@
1 -from fastapi import APIRouter
2 -
1 from app.integrations.dnstwist.routes.analyze import dnstwist_router
2 +from fastapi import APIRouter
3
4 # Instantiate the APIRouter
5 router = APIRouter()
backend/app/routers/grafana.py
+1 -2
@@ -1,6 +1,5 @@
1 -from fastapi import APIRouter
2 -
1 from app.connectors.grafana.routes.dashboards import grafana_dashboards_router
2 +from fastapi import APIRouter
3
4 # Instantiate the APIRouter
5 router = APIRouter()
backend/app/routers/graylog.py
+1 -2
@@ -1,11 +1,10 @@
1 -from fastapi import APIRouter
2 -
1 from app.connectors.graylog.routes.collector import graylog_collector_router
2 from app.connectors.graylog.routes.events import graylog_events_router
3 from app.connectors.graylog.routes.management import graylog_management_router
4 from app.connectors.graylog.routes.monitoring import graylog_monitoring_router
5 from app.connectors.graylog.routes.pipelines import graylog_pipelines_router
6 from app.connectors.graylog.routes.streams import graylog_streams_router
7 +from fastapi import APIRouter
8
9 router = APIRouter()
10
backend/app/routers/healthcheck.py
+6 -3
@@ -1,9 +1,12 @@
1 -from fastapi import APIRouter
2 -
1 from app.healthchecks.agents.routes.agents import healtcheck_agents_router
2 +from fastapi import APIRouter
3
4 # Instantiate the APIRouter
5 router = APIRouter()
6
7 # Include the Healthcheck related routes
9 -router.include_router(healtcheck_agents_router, prefix="/healthcheck", tags=["healthcheck agents"])
8 +router.include_router(
9 + healtcheck_agents_router,
10 + prefix="/healthcheck",
11 + tags=["healthcheck agents"],
12 +)
backend/app/routers/influxdb.py
+1 -2
@@ -1,6 +1,5 @@
1 -from fastapi import APIRouter
2 -
1 from app.connectors.influxdb.routes.alerts import influxdb_alerts_router
2 +from fastapi import APIRouter
3
4 # Instantiate the APIRouter
5 router = APIRouter()
backend/app/routers/integrations.py
+6 -3
@@ -1,9 +1,12 @@
1 -from fastapi import APIRouter
2 -
1 from app.integrations.routes import integration_settings_router
2 +from fastapi import APIRouter
3
4 # Instantiate the APIRouter
5 router = APIRouter()
6
7 # Include the Inntegration Settings related routes
9 -router.include_router(integration_settings_router, prefix="/integrations", tags=["Integration Settings"])
8 +router.include_router(
9 + integration_settings_router,
10 + prefix="/integrations",
11 + tags=["Integration Settings"],
12 +)
backend/app/routers/logs.py
+1 -2
@@ -1,6 +1,5 @@
1 -from fastapi import APIRouter
2 -
1 from app.utils import logs_router
2 +from fastapi import APIRouter
3
4 # Instantiate the APIRouter
5 router = APIRouter()
backend/app/routers/mimecast.py
+11 -4
@@ -1,13 +1,20 @@
1 -from fastapi import APIRouter
2 -
1 from app.integrations.mimecast.routes.mimecast import integration_mimecast_router
2 from app.integrations.mimecast.routes.provision import (
3 integration_mimecast_scheduler_router,
4 )
5 +from fastapi import APIRouter
6
7 # Instantiate the APIRouter
8 router = APIRouter()
9
10 # Include the Mimecast related routes
12 -router.include_router(integration_mimecast_router, prefix="/mimecast", tags=["mimecast"])
13 -router.include_router(integration_mimecast_scheduler_router, prefix="/mimecast", tags=["mimecast"])
11 +router.include_router(
12 + integration_mimecast_router,
13 + prefix="/mimecast",
14 + tags=["mimecast"],
15 +)
16 +router.include_router(
17 + integration_mimecast_scheduler_router,
18 + prefix="/mimecast",
19 + tags=["mimecast"],
20 +)
backend/app/routers/monitoring_alert.py
+11 -4
@@ -1,15 +1,22 @@
1 -from fastapi import APIRouter
2 -
1 from app.integrations.monitoring_alert.routes.monitoring_alert import (
2 monitoring_alerts_router,
3 )
4 from app.integrations.monitoring_alert.routes.provision import (
5 monitoring_alerts_provision_router,
6 )
7 +from fastapi import APIRouter
8
9 # Instantiate the APIRouter
10 router = APIRouter()
11
12 # Include the Monitoring Alert related routes
14 -router.include_router(monitoring_alerts_router, prefix="/monitoring_alert", tags=["monitoring_alert"])
15 -router.include_router(monitoring_alerts_provision_router, prefix="/monitoring_alert", tags=["provision_monitoring_alert"])
13 +router.include_router(
14 + monitoring_alerts_router,
15 + prefix="/monitoring_alert",
16 + tags=["monitoring_alert"],
17 +)
18 +router.include_router(
19 + monitoring_alerts_provision_router,
20 + prefix="/monitoring_alert",
21 + tags=["provision_monitoring_alert"],
22 +)
backend/app/routers/office365.py
+6 -3
@@ -1,9 +1,12 @@
1 -from fastapi import APIRouter
2 -
1 from app.integrations.office365.routes.provision import integration_office365_router
2 +from fastapi import APIRouter
3
4 # Instantiate the APIRouter
5 router = APIRouter()
6
7 # Include the Office365 related routes
9 -router.include_router(integration_office365_router, prefix="/office365", tags=["Office365"])
8 +router.include_router(
9 + integration_office365_router,
10 + prefix="/office365",
11 + tags=["Office365"],
12 +)
backend/app/routers/scheduler.py
+1 -2
@@ -1,6 +1,5 @@
1 -from fastapi import APIRouter
2 -
1 from app.schedulers.routes.scheduler import scheduler_router
2 +from fastapi import APIRouter
3
4 # Instantiate the APIRouter
5 router = APIRouter()
backend/app/routers/shuffle.py
+6 -3
@@ -1,9 +1,12 @@
1 -from fastapi import APIRouter
2 -
1 from app.connectors.shuffle.routes.workflows import shuffle_workflows_router
2 +from fastapi import APIRouter
3
4 # Instantiate the APIRouter
5 router = APIRouter()
6
7 # Include the Shuffle related routes
9 -router.include_router(shuffle_workflows_router, prefix="/workflows", tags=["shuffle-workflows"])
8 +router.include_router(
9 + shuffle_workflows_router,
10 + prefix="/workflows",
11 + tags=["shuffle-workflows"],
12 +)
backend/app/routers/smtp.py
+1 -2
@@ -1,7 +1,6 @@
1 -from fastapi import APIRouter
2 -
1 from app.smtp.routes.configure import smtp_configure_router
2 from app.smtp.routes.reports import smtp_reports_router
3 +from fastapi import APIRouter
4
5 # Instantiate the APIRouter
6 router = APIRouter()
backend/app/routers/sublime.py
+1 -2
@@ -1,6 +1,5 @@
1 -from fastapi import APIRouter
2 -
1 from app.connectors.sublime.routes.alerts import sublime_alerts_router
2 +from fastapi import APIRouter
3
4 # Instantiate the APIRouter
5 router = APIRouter()
backend/app/routers/threat_intel.py
+6 -3
@@ -1,9 +1,12 @@
1 -from fastapi import APIRouter
2 -
1 from app.threat_intel.routes.socfortress import threat_intel_socfortress_router
2 +from fastapi import APIRouter
3
4 # Instantiate the APIRouter
5 router = APIRouter()
6
7 # Include the Threat Intel related routes
9 -router.include_router(threat_intel_socfortress_router, prefix="/threat_intel", tags=["Threat Intel"])
8 +router.include_router(
9 + threat_intel_socfortress_router,
10 + prefix="/threat_intel",
11 + tags=["Threat Intel"],
12 +)
backend/app/routers/velociraptor.py
+11 -4
@@ -1,11 +1,18 @@
1 -from fastapi import APIRouter
2 -
1 from app.connectors.velociraptor.routes.artifacts import velociraptor_artifacts_router
2 from app.connectors.velociraptor.routes.flows import velociraptor_flows_router
3 +from fastapi import APIRouter
4
5 # Instantiate the APIRouter
6 router = APIRouter()
7
8 # Include the Velociraptor related routes
10 -router.include_router(velociraptor_artifacts_router, prefix="/artifacts", tags=["velociraptor-artifacts"])
11 -router.include_router(velociraptor_flows_router, prefix="/flows", tags=["velociraptor-flows"])
9 +router.include_router(
10 + velociraptor_artifacts_router,
11 + prefix="/artifacts",
12 + tags=["velociraptor-artifacts"],
13 +)
14 +router.include_router(
15 + velociraptor_flows_router,
16 + prefix="/flows",
17 + tags=["velociraptor-flows"],
18 +)
backend/app/routers/wazuh_indexer.py
+11 -4
@@ -1,11 +1,18 @@
1 -from fastapi import APIRouter
2 -
1 from app.connectors.wazuh_indexer.routes.alerts import wazuh_indexer_alerts_router
2 from app.connectors.wazuh_indexer.routes.monitoring import wazuh_indexer_router
3 +from fastapi import APIRouter
4
5 # Instantiate the APIRouter
6 router = APIRouter()
7
8 # Include the Wazuh Indexer related routes
10 -router.include_router(wazuh_indexer_alerts_router, prefix="/alerts", tags=["wazuh-indexer-alerts"])
11 -router.include_router(wazuh_indexer_router, prefix="/wazuh_indexer", tags=["wazuh-indexer-monitoring"])
9 +router.include_router(
10 + wazuh_indexer_alerts_router,
11 + prefix="/alerts",
12 + tags=["wazuh-indexer-alerts"],
13 +)
14 +router.include_router(
15 + wazuh_indexer_router,
16 + prefix="/wazuh_indexer",
17 + tags=["wazuh-indexer-monitoring"],
18 +)
backend/app/routers/wazuh_manager.py
+6 -3
@@ -1,9 +1,12 @@
1 -from fastapi import APIRouter
2 -
1 from app.connectors.wazuh_manager.routes.rules import wazuh_manager_rules_router
2 +from fastapi import APIRouter
3
4 # Instantiate the APIRouter
5 router = APIRouter()
6
7 # Include the Wazuh Manager related routes
9 -router.include_router(wazuh_manager_rules_router, prefix="/wazuh_manager", tags=["wazuh-manager"])
8 +router.include_router(
9 + wazuh_manager_rules_router,
10 + prefix="/wazuh_manager",
11 + tags=["wazuh-manager"],
12 +)
backend/app/schedulers/models/scheduler.py
+1 -2
@@ -2,8 +2,7 @@ from datetime import datetime
2 from typing import Optional
3
4 from pydantic import BaseModel
5 -from sqlmodel import Field
6 -from sqlmodel import SQLModel
5 +from sqlmodel import Field, SQLModel
6
7
8 class JobMetadata(SQLModel, table=True):
backend/app/schedulers/routes/scheduler.py
+29 -11
@@ -1,13 +1,11 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from loguru import logger
4 -from sqlalchemy.ext.asyncio import AsyncSession
5 -from sqlalchemy.future import select
6 -
1 from app.db.db_session import get_db
2 from app.schedulers.models.scheduler import JobMetadata
3 from app.schedulers.scheduler import init_scheduler
4 from app.schedulers.schema.scheduler import JobsResponse
5 +from fastapi import APIRouter, Depends
6 +from loguru import logger
7 +from sqlalchemy.ext.asyncio import AsyncSession
8 +from sqlalchemy.future import select
9
10 scheduler_router = APIRouter()
11
@@ -77,13 +75,24 @@ async def get_all_jobs(session: AsyncSession = Depends(get_db)) -> JobsResponse:
75 jobs = scheduler.get_jobs()
76 apscheduler_jobs = []
77 for job in jobs:
80 - job_metadata = await session.execute(select(JobMetadata).filter_by(job_id=job.id))
78 + job_metadata = await session.execute(
79 + select(JobMetadata).filter_by(job_id=job.id),
80 + )
81 job_metadata = job_metadata.scalars().first()
82 apscheduler_jobs.append(
83 - {"id": job.id, "name": job.name, "time_interval": job_metadata.time_interval, "enabled": job_metadata.enabled},
83 + {
84 + "id": job.id,
85 + "name": job.name,
86 + "time_interval": job_metadata.time_interval,
87 + "enabled": job_metadata.enabled,
88 + },
89 )
90 logger.info(f"apscheduler_jobs: {apscheduler_jobs}")
86 - return JobsResponse(jobs=apscheduler_jobs, success=True, message="Jobs successfully retrieved.")
91 + return JobsResponse(
92 + jobs=apscheduler_jobs,
93 + success=True,
94 + message="Jobs successfully retrieved.",
95 + )
96
97
98 @scheduler_router.post("/start/{job_id}", description="Start a job")
@@ -133,7 +142,11 @@ async def pause_job(job_id: str):
142
143
144 @scheduler_router.put("/update/{job_id}", description="Update a job")
136 -async def update_job(job_id: str, time_interval: int, session: AsyncSession = Depends(get_db)):
145 +async def update_job(
146 + job_id: str,
147 + time_interval: int,
148 + session: AsyncSession = Depends(get_db),
149 +):
150 """
151 Update a job with the specified job_id and time_interval.
152
@@ -155,7 +168,12 @@ async def update_job(job_id: str, time_interval: int, session: AsyncSession = De
168 job = await find_job_by_id(scheduler, job_id)
169 if job:
170 job.reschedule(trigger="interval", minutes=time_interval)
158 - await manage_job_metadata(session, job_id, "update", time_interval=time_interval)
171 + await manage_job_metadata(
172 + session,
173 + job_id,
174 + "update",
175 + time_interval=time_interval,
176 + )
177 logger.info(f"Job {job_id} updated successfully")
178 return {"success": True, "message": "Job updated successfully"}
179 logger.error(f"Job {job_id} not found for updating")
backend/app/schedulers/scheduler.py
+29 -14
@@ -1,17 +1,18 @@
1 +from app.db.db_session import SyncSessionLocal, sync_engine
2 +from app.schedulers.models.scheduler import CreateSchedulerRequest, JobMetadata
3 +from app.schedulers.services.agent_sync import agent_sync
4 +from app.schedulers.services.invoke_mimecast import (
5 + invoke_mimecast_integration,
6 + invoke_mimecast_integration_ttp,
7 +)
8 +from app.schedulers.services.monitoring_alert import (
9 + invoke_suricata_monitoring_alert,
10 + invoke_wazuh_monitoring_alert,
11 +)
12 from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
13 from apscheduler.schedulers.asyncio import AsyncIOScheduler
14 from loguru import logger
15
5 -from app.db.db_session import SyncSessionLocal
6 -from app.db.db_session import sync_engine
7 -from app.schedulers.models.scheduler import CreateSchedulerRequest
8 -from app.schedulers.models.scheduler import JobMetadata
9 -from app.schedulers.services.agent_sync import agent_sync
10 -from app.schedulers.services.invoke_mimecast import invoke_mimecast_integration
11 -from app.schedulers.services.invoke_mimecast import invoke_mimecast_integration_ttp
12 -from app.schedulers.services.monitoring_alert import invoke_suricata_monitoring_alert
13 -from app.schedulers.services.monitoring_alert import invoke_wazuh_monitoring_alert
14 -
16
17 def init_scheduler():
18 """
@@ -42,9 +43,16 @@ def initialize_job_metadata():
43 # {"job_id": "invoke_mimecast_integration", "time_interval": 5, "function": invoke_mimecast_integration}
44 ]
45 for job in known_jobs:
45 - job_metadata = session.query(JobMetadata).filter_by(job_id=job["job_id"]).one_or_none()
46 + job_metadata = (
47 + session.query(JobMetadata).filter_by(job_id=job["job_id"]).one_or_none()
48 + )
49 if not job_metadata:
47 - job_metadata = JobMetadata(job_id=job["job_id"], last_success=None, time_interval=job["time_interval"], enabled=True)
50 + job_metadata = JobMetadata(
51 + job_id=job["job_id"],
52 + last_success=None,
53 + time_interval=job["time_interval"],
54 + enabled=True,
55 + )
56 session.add(job_metadata)
57 else:
58 job_metadata.time_interval = job["time_interval"]
@@ -84,7 +92,10 @@ def get_function_by_name(function_name: str):
92 "invoke_suricata_monitoring_alert": invoke_suricata_monitoring_alert,
93 # Add other function mappings here
94 }
87 - return function_map.get(function_name, lambda: ValueError(f"Function {function_name} not found"))
95 + return function_map.get(
96 + function_name,
97 + lambda: ValueError(f"Function {function_name} not found"),
98 + )
99
100
101 async def add_scheduler_jobs(create_scheduler_request: CreateSchedulerRequest):
@@ -121,7 +132,11 @@ async def add_job_metadata(create_scheduler_request: CreateSchedulerRequest):
132 create_scheduler_request (CreateSchedulerRequest): The request object containing the job details.
133 """
134 with SyncSessionLocal() as session:
124 - job_metadata = session.query(JobMetadata).filter_by(job_id=create_scheduler_request.job_id).one_or_none()
135 + job_metadata = (
136 + session.query(JobMetadata)
137 + .filter_by(job_id=create_scheduler_request.job_id)
138 + .one_or_none()
139 + )
140 if not job_metadata:
141 job_metadata = JobMetadata(
142 job_id=create_scheduler_request.job_id,
backend/app/schedulers/services/agent_sync.py
+8 -4
@@ -2,11 +2,10 @@ import os
2 from datetime import datetime
3
4 import requests
5 -from dotenv import load_dotenv
6 -
5 from app.db.db_session import get_sync_db_session
6 from app.schedulers.models.scheduler import JobMetadata
7 from app.schedulers.utils.universal import scheduler_login
8 +from dotenv import load_dotenv
9
10 load_dotenv()
11
@@ -27,7 +26,10 @@ def agent_sync():
26 # Check if the token was successfully retrieved
27 if headers:
28 # Your actual task
30 - response = requests.post(f"http://{os.getenv('SERVER_IP')}:5000/agents/sync", headers=headers)
29 + response = requests.post(
30 + f"http://{os.getenv('SERVER_IP')}:5000/agents/sync",
31 + headers=headers,
32 + )
33
34 # Process the response here if needed
35 print(response.json())
@@ -37,7 +39,9 @@ def agent_sync():
39 # Use get_sync_db_session to create and manage a synchronous session
40 with get_sync_db_session() as session:
41 # Synchronous ORM operations
40 - job_metadata = session.query(JobMetadata).filter_by(job_id="agent_sync").one_or_none()
42 + job_metadata = (
43 + session.query(JobMetadata).filter_by(job_id="agent_sync").one_or_none()
44 + )
45 if job_metadata:
46 job_metadata.last_success = datetime.utcnow()
47 session.add(job_metadata)
backend/app/schedulers/services/invoke_mimecast.py
+32 -15
@@ -1,18 +1,17 @@
1 from datetime import datetime
2
3 +from app.db.db_session import get_db_session, get_sync_db_session
4 +from app.integrations.mimecast.routes.mimecast import (
5 + invoke_mimecast_route,
6 + mimecast_ttp_url_route,
7 +)
8 +from app.integrations.mimecast.schema.mimecast import MimecastRequest, MimecastResponse
9 +from app.integrations.models.customer_integration_settings import CustomerIntegrations
10 +from app.schedulers.models.scheduler import JobMetadata
11 from dotenv import load_dotenv
12 from loguru import logger
13 from sqlalchemy import select
14
7 -from app.db.db_session import get_db_session
8 -from app.db.db_session import get_sync_db_session
9 -from app.integrations.mimecast.routes.mimecast import invoke_mimecast_route
10 -from app.integrations.mimecast.routes.mimecast import mimecast_ttp_url_route
11 -from app.integrations.mimecast.schema.mimecast import MimecastRequest
12 -from app.integrations.mimecast.schema.mimecast import MimecastResponse
13 -from app.integrations.models.customer_integration_settings import CustomerIntegrations
14 -from app.schedulers.models.scheduler import JobMetadata
15 -
15 load_dotenv()
16
17
@@ -23,20 +22,29 @@ async def invoke_mimecast_integration() -> MimecastResponse:
22 logger.info("Invoking Mimecast integration scheduled job.")
23 customer_codes = []
24 async with get_db_session() as session:
26 - stmt = select(CustomerIntegrations).where(CustomerIntegrations.integration_service_name == "Mimecast")
25 + stmt = select(CustomerIntegrations).where(
26 + CustomerIntegrations.integration_service_name == "Mimecast",
27 + )
28 result = await session.execute(stmt)
29 customer_codes = [row.customer_code for row in result.scalars()]
30 logger.info(f"customer_codes: {customer_codes}")
31 for customer_code in customer_codes:
32 await invoke_mimecast_route(
32 - MimecastRequest(customer_code=customer_code, integration_name="Mimecast"),
33 + MimecastRequest(
34 + customer_code=customer_code,
35 + integration_name="Mimecast",
36 + ),
37 session,
38 )
39 # Close the session
40 await session.close()
41 with get_sync_db_session() as session:
42 # Synchronous ORM operations
39 - job_metadata = session.query(JobMetadata).filter_by(job_id="invoke_mimecast_integration").one_or_none()
43 + job_metadata = (
44 + session.query(JobMetadata)
45 + .filter_by(job_id="invoke_mimecast_integration")
46 + .one_or_none()
47 + )
48 if job_metadata:
49 job_metadata.last_success = datetime.utcnow()
50 session.add(job_metadata)
@@ -54,20 +62,29 @@ async def invoke_mimecast_integration_ttp() -> MimecastResponse:
62 """
63 customer_codes = []
64 async with get_db_session() as session:
57 - stmt = select(CustomerIntegrations).where(CustomerIntegrations.integration_service_name == "Mimecast")
65 + stmt = select(CustomerIntegrations).where(
66 + CustomerIntegrations.integration_service_name == "Mimecast",
67 + )
68 result = await session.execute(stmt)
69 customer_codes = [row.customer_code for row in result.scalars()]
70 logger.info(f"customer_codes: {customer_codes}")
71 for customer_code in customer_codes:
72 await mimecast_ttp_url_route(
63 - MimecastRequest(customer_code=customer_code, integration_name="Mimecast"),
73 + MimecastRequest(
74 + customer_code=customer_code,
75 + integration_name="Mimecast",
76 + ),
77 session,
78 )
79 # Close the session
80 await session.close()
81 with get_sync_db_session() as session:
82 # Synchronous ORM operations
70 - job_metadata = session.query(JobMetadata).filter_by(job_id="invoke_mimecast_integration_ttp").one_or_none()
83 + job_metadata = (
84 + session.query(JobMetadata)
85 + .filter_by(job_id="invoke_mimecast_integration_ttp")
86 + .one_or_none()
87 + )
88 if job_metadata:
89 job_metadata.last_success = datetime.utcnow()
90 session.add(job_metadata)
backend/app/schedulers/services/monitoring_alert.py
+26 -14
@@ -1,23 +1,19 @@
1 from datetime import datetime
2
3 -from dotenv import load_dotenv
4 -from loguru import logger
5 -from sqlalchemy import select
6 -
7 -from app.db.db_session import get_db_session
8 -from app.db.db_session import get_sync_db_session
3 +from app.db.db_session import get_db_session, get_sync_db_session
4 from app.db.universal_models import CustomersMeta
5 from app.integrations.monitoring_alert.routes.monitoring_alert import (
6 run_suricata_analysis,
7 + run_wazuh_analysis,
8 )
13 -from app.integrations.monitoring_alert.routes.monitoring_alert import run_wazuh_analysis
9 from app.integrations.monitoring_alert.schema.monitoring_alert import (
10 MonitoringWazuhAlertsRequestModel,
16 -)
17 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
11 WazuhAnalysisResponse,
12 )
13 from app.schedulers.models.scheduler import JobMetadata
14 +from dotenv import load_dotenv
15 +from loguru import logger
16 +from sqlalchemy import select
17
18 load_dotenv()
19
@@ -45,7 +41,11 @@ async def invoke_wazuh_monitoring_alert() -> WazuhAnalysisResponse:
41 await session.close()
42 with get_sync_db_session() as session:
43 # Synchronous ORM operations
48 - job_metadata = session.query(JobMetadata).filter_by(job_id="invoke_wazuh_monitoring_alert").one_or_none()
44 + job_metadata = (
45 + session.query(JobMetadata)
46 + .filter_by(job_id="invoke_wazuh_monitoring_alert")
47 + .one_or_none()
48 + )
49 if job_metadata:
50 job_metadata.last_success = datetime.utcnow()
51 session.add(job_metadata)
@@ -54,7 +54,10 @@ async def invoke_wazuh_monitoring_alert() -> WazuhAnalysisResponse:
54 # Handle the case where job_metadata does not exist
55 logger.error("JobMetadata for 'invoke_wazuh_monitoring_alert' not found.")
56
57 - return WazuhAnalysisResponse(success=True, message="Wazuh monitoring alerts invoked.")
57 + return WazuhAnalysisResponse(
58 + success=True,
59 + message="Wazuh monitoring alerts invoked.",
60 + )
61
62
63 async def invoke_suricata_monitoring_alert() -> WazuhAnalysisResponse:
@@ -80,13 +83,22 @@ async def invoke_suricata_monitoring_alert() -> WazuhAnalysisResponse:
83 await session.close()
84 with get_sync_db_session() as session:
85 # Synchronous ORM operations
83 - job_metadata = session.query(JobMetadata).filter_by(job_id="invoke_suricata_monitoring_alert").one_or_none()
86 + job_metadata = (
87 + session.query(JobMetadata)
88 + .filter_by(job_id="invoke_suricata_monitoring_alert")
89 + .one_or_none()
90 + )
91 if job_metadata:
92 job_metadata.last_success = datetime.utcnow()
93 session.add(job_metadata)
94 session.commit()
95 else:
96 # Handle the case where job_metadata does not exist
90 - logger.error("JobMetadata for 'invoke_suricata_monitoring_alert' not found.")
97 + logger.error(
98 + "JobMetadata for 'invoke_suricata_monitoring_alert' not found.",
99 + )
100
92 - return WazuhAnalysisResponse(success=True, message="Suricata monitoring alerts invoked.")
101 + return WazuhAnalysisResponse(
102 + success=True,
103 + message="Suricata monitoring alerts invoked.",
104 + )
backend/app/schedulers/utils/universal.py
+11 -4
@@ -1,9 +1,8 @@
1 import os
2
3 import requests
4 -from dotenv import load_dotenv
5 -
4 from app.auth.services.universal import get_scheduler_password
5 +from dotenv import load_dotenv
6
7 load_dotenv()
8
@@ -22,8 +21,16 @@ def scheduler_login():
21 # Get an auth token
22 token_response = requests.post(
23 f"http://{os.getenv('SERVER_IP')}:5000/auth/token",
25 - headers={"accept": "application/json", "Content-Type": "application/x-www-form-urlencoded"},
26 - data={"grant_type": "", "username": "scheduler", "password": password, "scope": ""},
24 + headers={
25 + "accept": "application/json",
26 + "Content-Type": "application/x-www-form-urlencoded",
27 + },
28 + data={
29 + "grant_type": "",
30 + "username": "scheduler",
31 + "password": password,
32 + "scope": "",
33 + },
34 )
35
36 # Check if the token was successfully retrieved
backend/app/smtp/routes/configure.py
+34 -11
@@ -1,19 +1,21 @@
1 -from fastapi import APIRouter
2 -from fastapi import HTTPException
3 -from loguru import logger
4 -
5 -from app.auth.models.users import SMTP
6 -from app.auth.models.users import SMTPInput
1 +from app.auth.models.users import SMTP, SMTPInput
2 from app.auth.services.universal import select_all_users
3 from app.auth.utils import AuthHandler
4 from app.db.db_session import session
5 from app.smtp.schema.configure import SMTPResponse
6 +from fastapi import APIRouter, HTTPException
7 +from loguru import logger
8
9 smtp_configure_router = APIRouter()
10 auth_handler = AuthHandler()
11
12
16 -@smtp_configure_router.post("/{user_id}/register", response_model=SMTPResponse, status_code=200, description="Register new SMTP for user")
13 +@smtp_configure_router.post(
14 + "/{user_id}/register",
15 + response_model=SMTPResponse,
16 + status_code=200,
17 + description="Register new SMTP for user",
18 +)
19 async def register(user_id: int, smtp: SMTPInput):
20 """
21 Register a new SMTP configuration for a user.
@@ -34,13 +36,24 @@ async def register(user_id: int, smtp: SMTPInput):
36 if smtp_found:
37 raise HTTPException(status_code=400, detail="SMTP already exists for user")
38 hashed_pwd = auth_handler.get_password_hash(smtp.smtp_password)
37 - u = SMTP(email=smtp.email, smtp_password=hashed_pwd, smtp_server=smtp.smtp_server, smtp_port=smtp.smtp_port, user_id=user_id)
39 + u = SMTP(
40 + email=smtp.email,
41 + smtp_password=hashed_pwd,
42 + smtp_server=smtp.smtp_server,
43 + smtp_port=smtp.smtp_port,
44 + user_id=user_id,
45 + )
46 session.add(u)
47 session.commit()
48 return {"message": "SMTP created successfully", "success": True}
49
50
43 -@smtp_configure_router.get("/{user_id}", response_model=SMTP, status_code=200, description="Get SMTP for user")
51 +@smtp_configure_router.get(
52 + "/{user_id}",
53 + response_model=SMTP,
54 + status_code=200,
55 + description="Get SMTP for user",
56 +)
57 async def get_smtp(user_id: int):
58 """
59 Get SMTP configuration for a specific user.
@@ -63,7 +76,12 @@ async def get_smtp(user_id: int):
76 return smtp_found
77
78
66 -@smtp_configure_router.put("/{user_id}", response_model=SMTPResponse, status_code=200, description="Update SMTP for user")
79 +@smtp_configure_router.put(
80 + "/{user_id}",
81 + response_model=SMTPResponse,
82 + status_code=200,
83 + description="Update SMTP for user",
84 +)
85 async def update_smtp(user_id: int, smtp: SMTPInput):
86 """
87 Update SMTP settings for a user.
@@ -92,7 +110,12 @@ async def update_smtp(user_id: int, smtp: SMTPInput):
110 return {"message": "SMTP updated successfully", "success": True}
111
112
95 -@smtp_configure_router.delete("/{user_id}", response_model=SMTPResponse, status_code=200, description="Delete SMTP for user")
113 +@smtp_configure_router.delete(
114 + "/{user_id}",
115 + response_model=SMTPResponse,
116 + status_code=200,
117 + description="Delete SMTP for user",
118 +)
119 async def delete_smtp(user_id: int):
120 """
121 Delete SMTP configuration for a user.
backend/app/smtp/routes/reports.py
+16 -8
@@ -1,20 +1,22 @@
1 -from fastapi import APIRouter
2 -from fastapi import HTTPException
3 -from loguru import logger
4 -
5 -from app.auth.models.users import SMTP
6 -from app.auth.models.users import SMTPInput
1 +from app.auth.models.users import SMTP, SMTPInput
2 from app.auth.services.universal import select_all_users
3 from app.auth.utils import AuthHandler
4 from app.db.db_session import session
5 from app.smtp.schema.configure import SMTPResponse
6 +from fastapi import APIRouter, HTTPException
7 +from loguru import logger
8
9 smtp_reports_router = APIRouter()
10 auth_handler = AuthHandler()
11
12
13 # ! TODO: Add SMTP reporting all things. Example is in the services/reports.py and services/create_report.py file
17 -@smtp_reports_router.post("/{user_id}/register", response_model=SMTPResponse, status_code=200, description="Register new SMTP for user")
14 +@smtp_reports_router.post(
15 + "/{user_id}/register",
16 + response_model=SMTPResponse,
17 + status_code=200,
18 + description="Register new SMTP for user",
19 +)
20 async def register(user_id: int, smtp: SMTPInput):
21 users = select_all_users()
22 logger.info(users)
@@ -25,7 +27,13 @@ async def register(user_id: int, smtp: SMTPInput):
27 if smtp_found:
28 raise HTTPException(status_code=400, detail="SMTP already exists for user")
29 hashed_pwd = auth_handler.get_password_hash(smtp.smtp_password)
28 - u = SMTP(email=smtp.email, smtp_password=hashed_pwd, smtp_server=smtp.smtp_server, smtp_port=smtp.smtp_port, user_id=user_id)
30 + u = SMTP(
31 + email=smtp.email,
32 + smtp_password=hashed_pwd,
33 + smtp_server=smtp.smtp_server,
34 + smtp_port=smtp.smtp_port,
35 + user_id=user_id,
36 + )
37 session.add(u)
38 session.commit()
39 return {"message": "SMTP created successfully", "success": True}
backend/app/smtp/services/create_report.py
+11 -7
@@ -8,10 +8,7 @@ from reportlab.lib.styles import getSampleStyleSheet
8 from reportlab.lib.units import inch
9
10 # from reportlab.pdfgen import canvas
11 -from reportlab.platypus import Image
12 -from reportlab.platypus import Paragraph
13 -from reportlab.platypus import SimpleDocTemplate
14 -from reportlab.platypus import Spacer
11 +from reportlab.platypus import Image, Paragraph, SimpleDocTemplate, Spacer
12
13 matplotlib.use(
14 "Agg",
@@ -19,7 +16,6 @@ matplotlib.use(
16 # for scripts and web servers. This should resolve the main thread is not
17 # in main loop issue as it bypasses the need for tkinter.
18 import matplotlib.pyplot as plt
22 -
19 from app.services.wazuh_indexer.alerts import AlertsService
20
21 # ! TODO: Just a template
@@ -82,7 +78,11 @@ def create_pie_chart(alerts: dict, title: str, output_filename: str) -> None:
78
79 plt.figure(figsize=(10, 6))
80 plt.pie(num_alerts, labels=entities, autopct="%1.1f%%")
85 - plt.legend(entities, loc="lower right", bbox_to_anchor=(1.0, 1.0)) # Add this line to include a legend
81 + plt.legend(
82 + entities,
83 + loc="lower right",
84 + bbox_to_anchor=(1.0, 1.0),
85 + ) # Add this line to include a legend
86 plt.title(title)
87 plt.tight_layout()
88 plt.savefig(output_filename)
@@ -140,4 +140,8 @@ def create_alerts_report_pdf() -> None:
140 alerts_by_rules = fetch_alert_data(service, service.collect_alerts_by_rule)
141 create_pie_chart(alerts_by_rules, "Number of Alerts by Rule", "alerts_by_rule.png")
142
143 - create_pdf("Test", ["alerts_by_host.png", "alerts_by_rule.png"], "alerts_report.pdf")
143 + create_pdf(
144 + "Test",
145 + ["alerts_by_host.png", "alerts_by_rule.png"],
146 + "alerts_report.pdf",
147 + )
backend/app/smtp/services/reports.py
+9 -4
@@ -6,8 +6,7 @@ from email.mime.text import MIMEText
6 from typing import List
7
8 from app.services.smtp.create_report import create_alerts_report_pdf
9 -from app.services.smtp.universal import EmailTemplate
10 -from app.services.smtp.universal import UniversalEmailCredentials
9 +from app.services.smtp.universal import EmailTemplate, UniversalEmailCredentials
10
11 # ! SEND REPORT
12
@@ -77,7 +76,10 @@ class EmailReportSender:
76 part = MIMEBase("application", "octet-stream")
77 part.set_payload(attachment_file.read())
78 encoders.encode_base64(part)
80 - part.add_header("Content-Disposition", f"attachment; filename= {filename}")
79 + part.add_header(
80 + "Content-Disposition",
81 + f"attachment; filename= {filename}",
82 + )
83 msg.attach(part)
84 return msg
85
@@ -107,7 +109,10 @@ class EmailReportSender:
109 return {"message": credentials["error"], "success": False}
110
111 # Send the email
110 - with smtplib.SMTP(credentials["smtp_server"], credentials["smtp_port"]) as server:
112 + with smtplib.SMTP(
113 + credentials["smtp_server"],
114 + credentials["smtp_port"],
115 + ) as server:
116 server.starttls()
117 server.login(credentials["email"], credentials["password"])
118 text = msg.as_string()
backend/app/threat_intel/routes/socfortress.py
+16 -11
@@ -1,16 +1,14 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from fastapi import HTTPException
4 -from fastapi import Security
5 -from loguru import logger
6 -from sqlalchemy.ext.asyncio import AsyncSession
7 -
1 from app.auth.utils import AuthHandler
2 from app.db.db_session import get_db
10 -from app.threat_intel.schema.socfortress import IoCResponse
11 -from app.threat_intel.schema.socfortress import SocfortressThreatIntelRequest
3 +from app.threat_intel.schema.socfortress import (
4 + IoCResponse,
5 + SocfortressThreatIntelRequest,
6 +)
7 from app.threat_intel.services.socfortress import socfortress_threat_intel_lookup
8 from app.utils import get_connector_attribute
9 +from fastapi import APIRouter, Depends, HTTPException, Security
10 +from loguru import logger
11 +from sqlalchemy.ext.asyncio import AsyncSession
12
13 # App specific imports
14
@@ -30,11 +28,18 @@ async def ensure_api_key_exists(session: AsyncSession = Depends(get_db)) -> bool
28 Returns:
29 bool: True if the API key exists, otherwise raises HTTPException.
30 """
33 - api_key = await get_connector_attribute(connector_id=10, column_name="connector_api_key", session=session)
31 + api_key = await get_connector_attribute(
32 + connector_id=10,
33 + column_name="connector_api_key",
34 + session=session,
35 + )
36 # Close the session
37 await session.close()
38 if not api_key:
37 - raise HTTPException(status_code=500, detail="SocFortress API key not found in the database.")
39 + raise HTTPException(
40 + status_code=500,
41 + detail="SocFortress API key not found in the database.",
42 + )
43 return True
44
45
backend/app/threat_intel/schema/socfortress.py
+9 -4
@@ -1,17 +1,22 @@
1 from typing import Optional
2
3 -from pydantic import BaseModel
4 -from pydantic import Field
3 +from pydantic import BaseModel, Field
4
5
6 class SocfortressThreatIntelRequest(BaseModel):
7 ioc_value: str
9 - customer_code: Optional[str] = Field("socfortress_copilot", description="The customer code for the customer")
8 + customer_code: Optional[str] = Field(
9 + "socfortress_copilot",
10 + description="The customer code for the customer",
11 + )
12
13
14 class IoCMapping(BaseModel):
15 comment: Optional[str] = Field(None, description="Comment about the IOCs")
14 - ioc_source: str = Field("SOCFortress Threat Intel", description="Identifier for the source of the IOC")
16 + ioc_source: str = Field(
17 + "SOCFortress Threat Intel",
18 + description="Identifier for the source of the IOC",
19 + )
20 report_url: Optional[str] = Field(None, description="URL for the related report")
21 score: Optional[int] = Field(
22 None,
backend/app/threat_intel/services/socfortress.py
+63 -22
@@ -1,20 +1,23 @@
1 -from typing import Any
2 -from typing import Dict
1 +from typing import Any, Dict
2
3 import httpx
5 -from fastapi import HTTPException
6 -from loguru import logger
7 -from sqlalchemy.ext.asyncio import AsyncSession
8 -
4 from app.connectors.utils import get_connector_info_from_db
5 from app.db.db_session import get_db_session
11 -from app.threat_intel.schema.socfortress import IoCMapping
12 -from app.threat_intel.schema.socfortress import IoCResponse
13 -from app.threat_intel.schema.socfortress import SocfortressThreatIntelRequest
6 +from app.threat_intel.schema.socfortress import (
7 + IoCMapping,
8 + IoCResponse,
9 + SocfortressThreatIntelRequest,
10 +)
11 from app.utils import get_connector_attribute
12 +from fastapi import HTTPException
13 +from loguru import logger
14 +from sqlalchemy.ext.asyncio import AsyncSession
15
16
17 -async def get_socfortress_threat_intel_attributes(column_name: str, session: AsyncSession) -> str:
17 +async def get_socfortress_threat_intel_attributes(
18 + column_name: str,
19 + session: AsyncSession,
20 +) -> str:
21 """
22 Gets the SocFortress Threat Intel attribute from the database.
23
@@ -29,15 +32,24 @@ async def get_socfortress_threat_intel_attributes(column_name: str, session: Asy
32 str: The SocFortress Threat Intel Attribute.
33
34 """
32 - attribute_value = await get_connector_attribute(connector_id=10, column_name=column_name, session=session)
35 + attribute_value = await get_connector_attribute(
36 + connector_id=10,
37 + column_name=column_name,
38 + session=session,
39 + )
40 # Close the session
41 await session.close()
42 if not attribute_value:
36 - raise HTTPException(status_code=500, detail="SocFortress Threat Intel attributes not found in the database.")
43 + raise HTTPException(
44 + status_code=500,
45 + detail="SocFortress Threat Intel attributes not found in the database.",
46 + )
47 return attribute_value
48
49
40 -async def verify_socfortress_threat_intel_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
50 +async def verify_socfortress_threat_intel_credentials(
51 + attributes: Dict[str, Any],
52 +) -> Dict[str, Any]:
53 """
54 Verifies the SOCFortress Threat Intel credentials.
55
@@ -54,7 +66,10 @@ async def verify_socfortress_threat_intel_credentials(attributes: Dict[str, Any]
66 url = attributes.get("connector_url", None)
67 if api_key is None or url is None:
68 logger.error("No SOCFortress Threat Intel credentials found in the database")
57 - raise HTTPException(status_code=500, detail="SOCFortress Threat Intel credentials not found in the database")
69 + raise HTTPException(
70 + status_code=500,
71 + detail="SOCFortress Threat Intel credentials not found in the database",
72 + )
73 return attributes
74
75
@@ -77,17 +92,34 @@ async def verifiy_socfortress_threat_intel_connector(connector_name: str) -> str
92 if attributes is None:
93 logger.error("No SOCFortress Threat Intel connector found in the database")
94 return None
80 - request = SocfortressThreatIntelRequest(ioc_value="evil.socfortress.co", customer_code="00001")
81 - response = await invoke_socfortress_threat_intel_api(attributes["connector_api_key"], attributes["connector_url"], request)
95 + request = SocfortressThreatIntelRequest(
96 + ioc_value="evil.socfortress.co",
97 + customer_code="00001",
98 + )
99 + response = await invoke_socfortress_threat_intel_api(
100 + attributes["connector_api_key"],
101 + attributes["connector_url"],
102 + request,
103 + )
104 if "data" in response and response["data"].get("comment") == "This is a test IoC":
105 logger.info("Verified SOCFortress Threat Intel connector")
84 - return {"connectionSuccessful": True, "message": "Successfully verified SOCFortress Threat Intel connector"}
106 + return {
107 + "connectionSuccessful": True,
108 + "message": "Successfully verified SOCFortress Threat Intel connector",
109 + }
110 else:
111 logger.error("Failed to verify SOCFortress Threat Intel connector")
87 - return {"connectionSuccessful": False, "message": "Failed to verify SOCFortress Threat Intel connector"}
112 + return {
113 + "connectionSuccessful": False,
114 + "message": "Failed to verify SOCFortress Threat Intel connector",
115 + }
116
117
90 -async def invoke_socfortress_threat_intel_api(api_key: str, url: str, request: SocfortressThreatIntelRequest) -> dict:
118 +async def invoke_socfortress_threat_intel_api(
119 + api_key: str,
120 + url: str,
121 + request: SocfortressThreatIntelRequest,
122 +) -> dict:
123 """
124 Invokes the Socfortress Threat Intel API with the provided API key, URL, and request parameters.
125
@@ -110,7 +142,10 @@ async def invoke_socfortress_threat_intel_api(api_key: str, url: str, request: S
142 return response.json()
143
144
113 -async def get_ioc_response(request: SocfortressThreatIntelRequest, session: AsyncSession) -> IoCResponse:
145 +async def get_ioc_response(
146 + request: SocfortressThreatIntelRequest,
147 + session: AsyncSession,
148 +) -> IoCResponse:
149 """
150 Retrieves IoC response from Socfortress Threat Intel API.
151
@@ -121,7 +156,10 @@ async def get_ioc_response(request: SocfortressThreatIntelRequest, session: Asyn
156 Returns:
157 IoCResponse: The response object containing the IoC data and success status.
158 """
124 - api_key = await get_socfortress_threat_intel_attributes("connector_api_key", session)
159 + api_key = await get_socfortress_threat_intel_attributes(
160 + "connector_api_key",
161 + session,
162 + )
163 url = await get_socfortress_threat_intel_attributes("connector_url", session)
164 response_data = await invoke_socfortress_threat_intel_api(api_key, url, request)
165
@@ -133,7 +171,10 @@ async def get_ioc_response(request: SocfortressThreatIntelRequest, session: Asyn
171 return IoCResponse(data=IoCMapping(**data), success=success, message=message)
172
173
136 -async def socfortress_threat_intel_lookup(request: SocfortressThreatIntelRequest, session: AsyncSession) -> IoCResponse:
174 +async def socfortress_threat_intel_lookup(
175 + request: SocfortressThreatIntelRequest,
176 + session: AsyncSession,
177 +) -> IoCResponse:
178 """
179 Performs a threat intelligence lookup using the Socfortress service.
180
backend/app/utils.py
+196 -72
@@ -1,44 +1,26 @@
1 -from datetime import datetime
2 -from datetime import timedelta
1 +from datetime import datetime, timedelta
2 from enum import Enum
4 -from typing import Any
5 -from typing import Dict
6 -from typing import List
7 -from typing import Optional
8 -from typing import Union
3 +from typing import Any, Dict, List, Optional, Union
4
5 import requests
11 -from fastapi import APIRouter
12 -from fastapi import Depends
13 -from fastapi import HTTPException
14 -from fastapi import Request
15 -from fastapi import Security
16 -from fastapi.exceptions import RequestValidationError
17 -from loguru import logger
18 -from pydantic import BaseModel
19 -from pydantic import Field
20 -from pydantic import validator
21 -from sqlalchemy.ext.asyncio import AsyncSession
22 -from sqlalchemy.future import select
23 -from sqlalchemy.orm import joinedload
24 -
6 from app.auth.services.universal import find_user
7 from app.auth.utils import AuthHandler
8 from app.connectors.utils import get_connector_info_from_db
9 from app.db.all_models import Connectors
29 -from app.db.db_session import get_db
30 -from app.db.db_session import get_db_session
31 -from app.db.db_session import get_session
10 +from app.db.db_session import get_db, get_db_session, get_session
11 from app.db.universal_models import LogEntry
12 from app.integrations.alert_creation_settings.models.alert_creation_settings import (
13 AlertCreationEventConfig,
35 -)
36 -from app.integrations.alert_creation_settings.models.alert_creation_settings import (
14 AlertCreationSettings,
38 -)
39 -from app.integrations.alert_creation_settings.models.alert_creation_settings import (
15 EventOrder,
16 )
17 +from fastapi import APIRouter, Depends, HTTPException, Request, Security
18 +from fastapi.exceptions import RequestValidationError
19 +from loguru import logger
20 +from pydantic import BaseModel, Field, validator
21 +from sqlalchemy.ext.asyncio import AsyncSession
22 +from sqlalchemy.future import select
23 +from sqlalchemy.orm import joinedload
24
25
26 ################## ! 422 VALIDATION ERROR TYPES FOR PYDANTIC VALUE ERROR RESPONSE ! ##################
@@ -109,7 +91,11 @@ class LogEntryModel(BaseModel):
91 method: str = Field(..., example="GET", description="Method")
92 status_code: int = Field(..., example=200, description="Status code")
93 message: str = Field(..., example="Route accessed", description="Message")
112 - additional_info: Optional[str] = Field(None, example="Additional details here", description="Additional info")
94 + additional_info: Optional[str] = Field(
95 + None,
96 + example="Additional details here",
97 + description="Additional info",
98 + )
99
100
101 class LogRetrieveModel(LogEntryModel):
@@ -129,7 +115,10 @@ class EventType(str, Enum):
115
116
117 class TimeRangeModel(BaseModel):
132 - time_range: Union[str, int] = Field("1d", description="Time range to fetch logs for, e.g., 1, 1h, 1d, 1w")
118 + time_range: Union[str, int] = Field(
119 + "1d",
120 + description="Time range to fetch logs for, e.g., 1, 1h, 1d, 1w",
121 + )
122
123 @validator("time_range")
124 def validate_time_range(cls, value):
@@ -150,7 +139,13 @@ class TimeRangeModel(BaseModel):
139 if isinstance(value, int):
140 if value < 1 or value > 7:
141 raise RequestValidationError(
153 - [{"loc": ("time_range",), "msg": "The integer part should be between 1 and 7.", "type": "value_error.time_range"}],
142 + [
143 + {
144 + "loc": ("time_range",),
145 + "msg": "The integer part should be between 1 and 7.",
146 + "type": "value_error.time_range",
147 + },
148 + ],
149 )
150 return f"{value}d" # convert integer to day representation
151
@@ -171,12 +166,24 @@ class TimeRangeModel(BaseModel):
166
167 if int_part <= 0:
168 raise RequestValidationError(
174 - [{"loc": ("time_range",), "msg": "The integer part should be greater than 0.", "type": "value_error.time_range"}],
169 + [
170 + {
171 + "loc": ("time_range",),
172 + "msg": "The integer part should be greater than 0.",
173 + "type": "value_error.time_range",
174 + },
175 + ],
176 )
177
178 if unit == "w" and int_part > 1:
179 raise RequestValidationError(
179 - [{"loc": ("time_range",), "msg": "The maximum allowed time range is 1 week.", "type": "value_error.time_range"}],
180 + [
181 + {
182 + "loc": ("time_range",),
183 + "msg": "The maximum allowed time range is 1 week.",
184 + "type": "value_error.time_range",
185 + },
186 + ],
187 )
188 return value
189
@@ -222,7 +229,9 @@ class Logger:
229 auth_header = request.headers.get("Authorization")
230 if auth_header:
231 try:
225 - token = auth_header.split(" ")[1] # Better split by space and take the second part
232 + token = auth_header.split(" ")[
233 + 1
234 + ] # Better split by space and take the second part
235 except IndexError:
236 raise HTTPException(status_code=401, detail="Invalid token")
237 username, _ = self.auth_handler.decode_token(token)
@@ -267,7 +276,13 @@ class Logger:
276 )
277 await self.insert_log_entry(log_entry_model)
278
270 - async def log_error(self, user_id, request: Request, exception: Exception, additional_info: Optional[str] = None):
279 + async def log_error(
280 + self,
281 + user_id,
282 + request: Request,
283 + exception: Exception,
284 + additional_info: Optional[str] = None,
285 + ):
286 """
287 Logs an error event with the provided information.
288
@@ -288,7 +303,12 @@ class Logger:
303 )
304 await self.insert_log_entry(log_entry_model)
305
291 - async def log_and_raise_http_error(self, user_id, request: Request, exception: Exception):
306 + async def log_and_raise_http_error(
307 + self,
308 + user_id,
309 + request: Request,
310 + exception: Exception,
311 + ):
312 """
313 Logs the error, including the user ID, request details, and the exception,
314 and raises an HTTPException with a status code of 500 (Internal Server Error).
@@ -342,9 +362,15 @@ async def get_logs(session: AsyncSession = Depends(get_db)) -> LogsResponse:
362 auth_handler_instance = AuthHandler() # Initialize your AuthHandler
363 logger_instance = Logger(session, auth_handler_instance)
364
345 - logs = await logger_instance.fetch_all_logs() # Assuming fetch_all_logs is an async function
365 + logs = (
366 + await logger_instance.fetch_all_logs()
367 + ) # Assuming fetch_all_logs is an async function
368 if logs:
347 - return LogsResponse(logs=logs, success=True, message="Logs fetched successfully")
369 + return LogsResponse(
370 + logs=logs,
371 + success=True,
372 + message="Logs fetched successfully",
373 + )
374 else:
375 raise HTTPException(status_code=404, detail="No logs found")
376
@@ -355,7 +381,10 @@ async def get_logs(session: AsyncSession = Depends(get_db)) -> LogsResponse:
381 description="Fetch logs by user ID",
382 dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
383 )
358 -async def get_logs_by_user_id(user_id: int, session: AsyncSession = Depends(get_db)) -> LogsResponse:
384 +async def get_logs_by_user_id(
385 + user_id: int,
386 + session: AsyncSession = Depends(get_db),
387 +) -> LogsResponse:
388 """
389 Fetch all logs from the database where the user_id matches the provided user_id.
390
@@ -375,7 +404,10 @@ async def get_logs_by_user_id(user_id: int, session: AsyncSession = Depends(get_
404 logs = result.scalars().all()
405
406 if not logs:
378 - raise HTTPException(status_code=404, detail=f"No logs found for user ID: {user_id}")
407 + raise HTTPException(
408 + status_code=404,
409 + detail=f"No logs found for user ID: {user_id}",
410 + )
411
412 return LogsResponse(logs=logs, success=True, message="Logs fetched successfully")
413
@@ -386,7 +418,10 @@ async def get_logs_by_user_id(user_id: int, session: AsyncSession = Depends(get_
418 description="Fetch logs by time range",
419 dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
420 )
389 -async def get_logs_by_time_range(time_range: TimeRangeModel, session: AsyncSession = Depends(get_db)) -> LogsResponse:
421 +async def get_logs_by_time_range(
422 + time_range: TimeRangeModel,
423 + session: AsyncSession = Depends(get_db),
424 +) -> LogsResponse:
425 """
426 Fetch all logs from the database where the timestamp is within the provided time range.
427
@@ -406,13 +441,24 @@ async def get_logs_by_time_range(time_range: TimeRangeModel, session: AsyncSessi
441 logs = result.scalars().all()
442
443 if logs:
409 - logs = [log for log in logs if log.timestamp >= datetime.now() - timedelta(days=int(time_range.time_range[:-1]))]
444 + logs = [
445 + log
446 + for log in logs
447 + if log.timestamp
448 + >= datetime.now() - timedelta(days=int(time_range.time_range[:-1]))
449 + ]
450 if logs != []:
411 - return LogsResponse(logs=logs, success=True, message="Logs fetched successfully")
451 + return LogsResponse(
452 + logs=logs,
453 + success=True,
454 + message="Logs fetched successfully",
455 + )
456 else:
457 raise HTTPException(
458 status_code=404,
415 - detail=f"No logs found for time range: {time_range.time_range}".format(time_range=time_range.time_range),
459 + detail=f"No logs found for time range: {time_range.time_range}".format(
460 + time_range=time_range.time_range,
461 + ),
462 )
463 else:
464 raise HTTPException(status_code=404, detail="No logs found")
@@ -443,11 +489,16 @@ async def get_logs_by_event_type(
489 Raises:
490 HTTPException: An exception with a 404 status code is raised if no logs are found.
491 """
446 - result = await session.execute(select(LogEntry).filter(LogEntry.event_type == event_type))
492 + result = await session.execute(
493 + select(LogEntry).filter(LogEntry.event_type == event_type),
494 + )
495 logs = result.scalars().all()
496
497 if not logs:
450 - raise HTTPException(status_code=404, detail=f"No logs found for event type: {event_type}")
498 + raise HTTPException(
499 + status_code=404,
500 + detail=f"No logs found for event type: {event_type}",
501 + )
502
503 return LogsResponse(logs=logs, success=True, message="Logs fetched successfully")
504
@@ -458,7 +509,9 @@ async def get_logs_by_event_type(
509 description="Purge all logs",
510 dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
511 )
461 -async def purge_logs(session: AsyncSession = Depends(get_db)) -> LogsResponse: # Update this line to use the new model
512 +async def purge_logs(
513 + session: AsyncSession = Depends(get_db),
514 +) -> LogsResponse: # Update this line to use the new model
515 """
516 Purge all logs from the database.
517
@@ -488,7 +541,10 @@ async def purge_logs(session: AsyncSession = Depends(get_db)) -> LogsResponse:
541 description="Purge logs by time range",
542 dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
543 )
491 -async def purge_logs_by_time_range(time_range: TimeRangeModel, session: AsyncSession = Depends(get_db)) -> LogsResponse:
544 +async def purge_logs_by_time_range(
545 + time_range: TimeRangeModel,
546 + session: AsyncSession = Depends(get_db),
547 +) -> LogsResponse:
548 """
549 Purge all logs from the database where the timestamp is within the provided time range.
550
@@ -508,16 +564,27 @@ async def purge_logs_by_time_range(time_range: TimeRangeModel, session: AsyncSes
564 logs = result.scalars().all()
565
566 if logs:
511 - logs = [log for log in logs if log.timestamp >= datetime.now() - timedelta(days=int(time_range.time_range[:-1]))]
567 + logs = [
568 + log
569 + for log in logs
570 + if log.timestamp
571 + >= datetime.now() - timedelta(days=int(time_range.time_range[:-1]))
572 + ]
573 if logs != []:
574 for log in logs:
575 await session.delete(log)
576 await session.commit()
516 - return LogsResponse(logs=[], success=True, message="Logs purged successfully")
577 + return LogsResponse(
578 + logs=[],
579 + success=True,
580 + message="Logs purged successfully",
581 + )
582 else:
583 raise HTTPException(
584 status_code=404,
520 - detail=f"No logs found for time range: {time_range.time_range}".format(time_range=time_range.time_range),
585 + detail=f"No logs found for time range: {time_range.time_range}".format(
586 + time_range=time_range.time_range,
587 + ),
588 )
589 else:
590 raise HTTPException(status_code=404, detail="No logs found")
@@ -539,7 +606,11 @@ def allowed_file(filename):
606
607
608 ################## ! DATABASE UTILS ! ##################
542 -async def get_connector_attribute(connector_id: int, column_name: str, session: AsyncSession = Depends(get_session)) -> Optional[Any]:
609 +async def get_connector_attribute(
610 + connector_id: int,
611 + column_name: str,
612 + session: AsyncSession = Depends(get_session),
613 +) -> Optional[Any]:
614 """
615 Retrieve the value of a specific column from a connector.
616
@@ -551,7 +622,9 @@ async def get_connector_attribute(connector_id: int, column_name: str, session:
622 Returns:
623 Optional[Any]: The value of the column, or None if the connector or column does not exist.
624 """
554 - result = await session.execute(select(Connectors).filter(Connectors.id == connector_id))
625 + result = await session.execute(
626 + select(Connectors).filter(Connectors.id == connector_id),
627 + )
628 connector = result.scalars().first()
629
630 if connector:
@@ -559,7 +632,10 @@ async def get_connector_attribute(connector_id: int, column_name: str, session:
632 return None
633
634
562 -async def get_customer_alert_settings(customer_code: str, session: AsyncSession) -> Optional[AlertCreationSettings]:
635 +async def get_customer_alert_settings(
636 + customer_code: str,
637 + session: AsyncSession,
638 +) -> Optional[AlertCreationSettings]:
639 """
640 Retrieve the alert creation settings for a specific customer.
641
@@ -570,7 +646,11 @@ async def get_customer_alert_settings(customer_code: str, session: AsyncSession)
646 Returns:
647 Optional[AlertCreationSettings]: The alert creation settings for the customer, or None if not found.
648 """
573 - result = await session.execute(select(AlertCreationSettings).filter(AlertCreationSettings.customer_code == customer_code))
649 + result = await session.execute(
650 + select(AlertCreationSettings).filter(
651 + AlertCreationSettings.customer_code == customer_code,
652 + ),
653 + )
654 settings = result.scalars().first()
655
656 if settings:
@@ -578,7 +658,10 @@ async def get_customer_alert_settings(customer_code: str, session: AsyncSession)
658 return None
659
660
581 -async def get_customer_alert_settings_office365(office365_organization_id: str, session: AsyncSession) -> Optional[AlertCreationSettings]:
661 +async def get_customer_alert_settings_office365(
662 + office365_organization_id: str,
663 + session: AsyncSession,
664 +) -> Optional[AlertCreationSettings]:
665 """
666 Retrieve the alert creation settings for a specific customer.
667
@@ -590,7 +673,10 @@ async def get_customer_alert_settings_office365(office365_organization_id: str,
673 Optional[AlertCreationSettings]: The alert creation settings for the customer, or None if not found.
674 """
675 result = await session.execute(
593 - select(AlertCreationSettings).filter(AlertCreationSettings.office365_organization_id == office365_organization_id),
676 + select(AlertCreationSettings).filter(
677 + AlertCreationSettings.office365_organization_id
678 + == office365_organization_id,
679 + ),
680 )
681 settings = result.scalars().first()
682
@@ -615,7 +701,11 @@ async def get_customer_alert_event_configs(
701 """
702 result = await session.execute(
703 select(AlertCreationSettings)
618 - .options(joinedload(AlertCreationSettings.event_orders).joinedload(EventOrder.event_configs))
704 + .options(
705 + joinedload(AlertCreationSettings.event_orders).joinedload(
706 + EventOrder.event_configs,
707 + ),
708 + )
709 .where(AlertCreationSettings.customer_code == customer_code),
710 )
711 settings = result.scalars().first()
@@ -627,14 +717,18 @@ async def get_customer_alert_event_configs(
717
718 ################## ! Wazuh Worker Provisioning App ! ##################
719 ################## ! https://github.com/socfortress/Customer-Provisioning-Worker ! ##################
630 -async def verify_wazuh_worker_provisioning_healtcheck(attributes: Dict[str, Any]) -> Dict[str, Any]:
720 +async def verify_wazuh_worker_provisioning_healtcheck(
721 + attributes: Dict[str, Any],
722 +) -> Dict[str, Any]:
723 """
724 Verifies the connection to Wazuh Worker Provisioning service.
725
726 Returns:
727 dict: A dictionary containing 'connectionSuccessful' status.
728 """
637 - logger.info(f"Verifying the wazuh-worker provisioning connection to {attributes['connector_url']}")
729 + logger.info(
730 + f"Verifying the wazuh-worker provisioning connection to {attributes['connector_url']}",
731 + )
732
733 try:
734 wazuh_worker_provisioning_healthcheck = requests.get(
@@ -643,15 +737,28 @@ async def verify_wazuh_worker_provisioning_healtcheck(attributes: Dict[str, Any]
737 )
738
739 if wazuh_worker_provisioning_healthcheck.status_code == 200:
646 - return {"connectionSuccessful": True, "message": "Wazuh Worker Provisioning healthcheck successful"}
740 + return {
741 + "connectionSuccessful": True,
742 + "message": "Wazuh Worker Provisioning healthcheck successful",
743 + }
744 else:
648 - logger.error(f"Connection to {attributes['connector_url']} failed with error: {wazuh_worker_provisioning_healthcheck.text}")
745 + logger.error(
746 + f"Connection to {attributes['connector_url']} failed with error: {wazuh_worker_provisioning_healthcheck.text}",
747 + )
748
650 - return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed"}
749 + return {
750 + "connectionSuccessful": False,
751 + "message": f"Connection to {attributes['connector_url']} failed",
752 + }
753 except Exception as e:
652 - logger.error(f"Connection to {attributes['connector_url']} failed with error: {e}")
754 + logger.error(
755 + f"Connection to {attributes['connector_url']} failed with error: {e}",
756 + )
757
654 - return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error."}
758 + return {
759 + "connectionSuccessful": False,
760 + "message": f"Connection to {attributes['connector_url']} failed with error.",
761 + }
762
763
764 async def verify_wazuh_worker_provisioning_connection(connector_name: str) -> str:
@@ -668,14 +775,18 @@ async def verify_wazuh_worker_provisioning_connection(connector_name: str) -> st
775
776 ################## ! Alert Creation Provisioning App ! ##################
777 ################## ! https://github.com/socfortress/Customer-Provisioning-Alert ! ##################
671 -async def verify_alert_creation_provisioning_healtcheck(attributes: Dict[str, Any]) -> Dict[str, Any]:
778 +async def verify_alert_creation_provisioning_healtcheck(
779 + attributes: Dict[str, Any],
780 +) -> Dict[str, Any]:
781 """
782 Verifies the connection to Alert Creation Provisioning service.
783
784 Returns:
785 dict: A dictionary containing 'connectionSuccessful' status.
786 """
678 - logger.info(f"Verifying the Alert Creation provisioning connection to {attributes['connector_url']}")
787 + logger.info(
788 + f"Verifying the Alert Creation provisioning connection to {attributes['connector_url']}",
789 + )
790
791 try:
792 wazuh_worker_provisioning_healthcheck = requests.get(
@@ -684,15 +795,28 @@ async def verify_alert_creation_provisioning_healtcheck(attributes: Dict[str, An
795 )
796
797 if wazuh_worker_provisioning_healthcheck.status_code == 200:
687 - return {"connectionSuccessful": True, "message": "Alert Creation Provisioning healthcheck successful"}
798 + return {
799 + "connectionSuccessful": True,
800 + "message": "Alert Creation Provisioning healthcheck successful",
801 + }
802 else:
689 - logger.error(f"Connection to {attributes['connector_url']} failed with error: {wazuh_worker_provisioning_healthcheck.text}")
803 + logger.error(
804 + f"Connection to {attributes['connector_url']} failed with error: {wazuh_worker_provisioning_healthcheck.text}",
805 + )
806
691 - return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed"}
807 + return {
808 + "connectionSuccessful": False,
809 + "message": f"Connection to {attributes['connector_url']} failed",
810 + }
811 except Exception as e:
693 - logger.error(f"Connection to {attributes['connector_url']} failed with error: {e}")
812 + logger.error(
813 + f"Connection to {attributes['connector_url']} failed with error: {e}",
814 + )
815
695 - return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error."}
816 + return {
817 + "connectionSuccessful": False,
818 + "message": f"Connection to {attributes['connector_url']} failed with error.",
819 + }
820
821
822 async def verify_alert_creation_provisioning_connection(connector_name: str) -> str:
backend/copilot.py
+86 -72
@@ -1,62 +1,72 @@
1 import os
2
3 import uvicorn
4 -from dotenv import load_dotenv
5 -from fastapi import FastAPI
6 -from fastapi import HTTPException
7 -from fastapi.exceptions import RequestValidationError
8 -from fastapi.middleware.cors import CORSMiddleware
9 -from loguru import logger
10 -
4 from app.auth.utils import AuthHandler
5 from app.db.db_session import async_engine
13 -from app.db.db_setup import create_available_integrations
14 -from app.db.db_setup import create_roles
15 -from app.db.db_setup import create_tables
16 -from app.db.db_setup import ensure_admin_user
17 -from app.db.db_setup import ensure_scheduler_user
18 -from app.db.db_setup import ensure_scheduler_user_removed
19 -from app.middleware.exception_handlers import custom_http_exception_handler
20 -from app.middleware.exception_handlers import validation_exception_handler
21 -from app.middleware.exception_handlers import value_error_handler
6 +from app.db.db_setup import (
7 + create_available_integrations,
8 + create_roles,
9 + create_tables,
10 + ensure_admin_user,
11 + ensure_scheduler_user,
12 + ensure_scheduler_user_removed,
13 +)
14 +from app.middleware.exception_handlers import (
15 + custom_http_exception_handler,
16 + validation_exception_handler,
17 + value_error_handler,
18 +)
19 from app.middleware.logger import log_requests
23 -from app.routers import agents
24 -from app.routers import alert_creation
25 -from app.routers import alert_creation_settings
26 -from app.routers import ask_socfortress
27 -from app.routers import auth
28 -from app.routers import connectors
29 -from app.routers import cortex
30 -from app.routers import customer_provisioning
31 -from app.routers import customers
32 -from app.routers import dfir_iris
33 -from app.routers import dnstwist
34 -from app.routers import grafana
35 -from app.routers import graylog
36 -from app.routers import healthcheck
37 -from app.routers import influxdb
38 -from app.routers import integrations
39 -from app.routers import logs
40 -from app.routers import mimecast
41 -from app.routers import monitoring_alert
42 -from app.routers import office365
43 -from app.routers import scheduler
44 -from app.routers import shuffle
45 -from app.routers import smtp
46 -from app.routers import sublime
47 -from app.routers import threat_intel
48 -from app.routers import velociraptor
49 -from app.routers import wazuh_indexer
50 -from app.routers import wazuh_manager
20 +from app.routers import (
21 + agents,
22 + alert_creation,
23 + alert_creation_settings,
24 + ask_socfortress,
25 + auth,
26 + connectors,
27 + cortex,
28 + customer_provisioning,
29 + customers,
30 + dfir_iris,
31 + dnstwist,
32 + grafana,
33 + graylog,
34 + healthcheck,
35 + influxdb,
36 + integrations,
37 + logs,
38 + mimecast,
39 + monitoring_alert,
40 + office365,
41 + scheduler,
42 + shuffle,
43 + smtp,
44 + sublime,
45 + threat_intel,
46 + velociraptor,
47 + wazuh_indexer,
48 + wazuh_manager,
49 +)
50 from app.schedulers.scheduler import init_scheduler
51 +from dotenv import load_dotenv
52 +from fastapi import APIRouter, FastAPI, HTTPException
53 +from fastapi.exceptions import RequestValidationError
54 +from fastapi.middleware.cors import CORSMiddleware
55 +from loguru import logger
56
57 auth_handler = AuthHandler()
58 # Get the `SERVER_IP` from the `.env` file
59 load_dotenv()
60 server_ip = os.getenv("SERVER_IP", "localhost")
61 +# Not needed for now
62 +#ssl_keyfile = os.path.join(os.path.dirname(__file__), "../nginx/server.key")
63 +#ssl_certfile = os.path.join(os.path.dirname(__file__), "../nginx/server.crt")
64
65 app = FastAPI(description="CoPilot API", version="0.1.0", title="CoPilot API")
66
67 +# Create an APIRouter with a prefix of `/api`
68 +api_router = APIRouter(prefix="/api")
69 +
70
71 # Allow all origins, methods and headers
72 app.add_middleware(
@@ -79,34 +89,37 @@ app.add_exception_handler(ValueError, value_error_handler)
89
90
91 ################## ! INCLUDE ROUTES ! ##################
82 -app.include_router(connectors.router)
83 -app.include_router(wazuh_indexer.router)
84 -app.include_router(auth.router)
85 -app.include_router(wazuh_manager.router)
86 -app.include_router(agents.router)
87 -app.include_router(graylog.router)
88 -app.include_router(dfir_iris.router)
89 -app.include_router(cortex.router)
90 -app.include_router(velociraptor.router)
91 -app.include_router(shuffle.router)
92 -app.include_router(sublime.router)
93 -app.include_router(customers.router)
94 -app.include_router(healthcheck.router)
95 -app.include_router(smtp.router)
96 -app.include_router(dnstwist.router)
97 -app.include_router(logs.router)
98 -app.include_router(influxdb.router)
99 -app.include_router(grafana.router)
100 -app.include_router(customer_provisioning.router)
101 -app.include_router(threat_intel.router)
102 -app.include_router(ask_socfortress.router)
103 -app.include_router(alert_creation.router)
104 -app.include_router(alert_creation_settings.router)
105 -app.include_router(integrations.router)
106 -app.include_router(office365.router)
107 -app.include_router(mimecast.router)
108 -app.include_router(scheduler.router)
109 -app.include_router(monitoring_alert.router)
92 +api_router.include_router(connectors.router)
93 +api_router.include_router(wazuh_indexer.router)
94 +api_router.include_router(auth.router)
95 +api_router.include_router(wazuh_manager.router)
96 +api_router.include_router(agents.router)
97 +api_router.include_router(graylog.router)
98 +api_router.include_router(dfir_iris.router)
99 +api_router.include_router(cortex.router)
100 +api_router.include_router(velociraptor.router)
101 +api_router.include_router(shuffle.router)
102 +api_router.include_router(sublime.router)
103 +api_router.include_router(customers.router)
104 +api_router.include_router(healthcheck.router)
105 +api_router.include_router(smtp.router)
106 +api_router.include_router(dnstwist.router)
107 +api_router.include_router(logs.router)
108 +api_router.include_router(influxdb.router)
109 +api_router.include_router(grafana.router)
110 +api_router.include_router(customer_provisioning.router)
111 +api_router.include_router(threat_intel.router)
112 +api_router.include_router(ask_socfortress.router)
113 +api_router.include_router(alert_creation.router)
114 +api_router.include_router(alert_creation_settings.router)
115 +api_router.include_router(integrations.router)
116 +api_router.include_router(office365.router)
117 +api_router.include_router(mimecast.router)
118 +api_router.include_router(scheduler.router)
119 +api_router.include_router(monitoring_alert.router)
120 +
121 +# Include the APIRouter in the FastAPI app
122 +app.include_router(api_router)
123
124
125 @app.on_event("startup")
@@ -126,6 +139,7 @@ async def init_db():
139 scheduler.start()
140
141
142 +
143 @app.get("/")
144 def hello():
145 return {"message": "CoPilot - We Made It!"}
backend/settings.py
+6 -1
@@ -12,7 +12,9 @@ from loguru import logger
12
13 env = Env()
14 env.read_env(Path(__file__).parent.parent / ".env")
15 +# env.read_env(Path(__file__).parent.parent.parent / "docker-env" / ".env")
16 logger.info(f"Loading environment from {Path(__file__).parent.parent / '.env'}")
17 +# logger.info(f"Loading environment from {Path(__file__).parent.parent.parent / 'docker-env' / '.env'}")
18
19
20 basedir = Path().absolute()
@@ -22,7 +24,10 @@ ENV = env.str("SECRET_KEY", default="production")
24 DEBUG = env.bool("FLASK_DEBUG", default=False)
25 SECRET_KEY = env.str("SECRET_KEY", "not-a-secret")
26 # SQLALCHEMY_DATABASE_URI = env.str("SQLALCHEMY_DATABASE_URI", f"sqlite:///{db_path}")
25 -SQLALCHEMY_DATABASE_URI = env.str("SQLALCHEMY_DATABASE_URI", f"sqlite+aiosqlite:///{db_path}")
27 +SQLALCHEMY_DATABASE_URI = env.str(
28 + "SQLALCHEMY_DATABASE_URI",
29 + f"sqlite+aiosqlite:///{db_path}",
30 +)
31 SQLALCHEMY_TRACK_MODIFICATIONS = env.bool(
32 "SQLALCHEMY_TRACK_MODIFICATIONS",
33 default=False,
build-dockers.sh new
+46
@@ -0,0 +1,46 @@
1 +#!/bin/bash
2 +
3 +currDir=$(pwd)
4 +version="latest"
5 +if [[ ! -z "$1" ]]; then
6 + version="$1"
7 +fi
8 +
9 +# Function to check if Docker is installed
10 +function check_docker_installed() {
11 + if ! command -v docker &> /dev/null; then
12 + echo "Docker could not be found. Please install Docker and try again."
13 + exit 1
14 + fi
15 +}
16 +
17 +function build_backend() {
18 + echo ""
19 + echo "Build backend (version=${version})"
20 + cd $currDir/backend
21 + docker build . --no-cache -t socfortress/copilot-backend:${version}
22 +}
23 +
24 +function build_frontend() {
25 + echo ""
26 + echo "Build frontend (version=${version})"
27 + cd $currDir/frontend
28 + # Copy the .env.example to .env
29 + cp .env.example .env
30 + # Ask for the new Domain or IP address for the frontend URL
31 + echo "Please enter the new Domain or IP address for the frontend URL (e.g., yourfrontenddomain.com):"
32 + read frontendIp
33 + # Replace only the IP address part in the URL
34 + sed -i "s|0.0.0.0|${frontendIp}|g" .env
35 + docker build . --no-cache -t socfortress/copilot-frontend:${version}
36 +}
37 +
38 +echo "Copilot Docker"
39 +echo "Version: ${version}"
40 +
41 +# First, ensure Docker is installed
42 +check_docker_installed
43 +
44 +# Build processes
45 +#build_backend # Function not needed as SOCFortress will provide the backend but leaving in case you want to build your own
46 +build_frontend
docker-compose.yml
+32 -10
@@ -1,16 +1,18 @@
1 -version: "3.8"
1 +version: "2"
2 +
3 services:
3 - app:
4 - image: ghcr.io/socfortress/copilot:latest
4 + copilot-backend:
5 + container_name: copilot-backend
6 + image: ghcr.io/socfortress/copilot-backend:latest
7 + ports:
8 + - 5000:5000
9 volumes:
10 + - ./docker-env/copilot-backend-data/logs:/opt/logs
11 + # Mount the copilot.db file to persist the database
12 - ./backend/data:/opt/copilot/backend/data
7 - network_mode: "host"
13 + env_file: .env
14 environment:
15 SERVER_IP: ${SERVER_IP}
10 - VITE_API_URL: ${VITE_API_URL}
11 - VITE_TOKEN_DEBOUNCE_TIME: ${VITE_TOKEN_DEBOUNCE_TIME}
12 - VITE_UNCOMMITTED_JOURNAL_ENTRIES_THRESHOLD: ${VITE_UNCOMMITTED_JOURNAL_ENTRIES_THRESHOLD}
13 - VITE_HEALTHCHECKS_INTERVAL: ${VITE_HEALTHCHECKS_INTERVAL}
16 WAZUH_INDEXER_URL: ${WAZUH_INDEXER_URL}
17 WAZUH_INDEXER_USERNAME: ${WAZUH_INDEXER_USERNAME}
18 WAZUH_INDEXER_PASSWORD: ${WAZUH_INDEXER_PASSWORD}
@@ -41,6 +43,26 @@ services:
43 GRAFANA_USERNAME: ${GRAFANA_USERNAME}
44 GRAFANA_PASSWORD: ${GRAFANA_PASSWORD}
45 WAZUH_WORKER_PROVISIONING_URL: ${WAZUH_WORKER_PROVISIONING_URL}
46 +
47 + copilot-frontend:
48 + container_name: copilot-frontend
49 + image: socfortress/copilot-frontend:latest
50 +
51 + copilot-nginx:
52 + image: nginx
53 + container_name: copilot-nginx
54 ports:
45 - - "5000:5000"
46 - - "5173:5173"
55 + - 80:80
56 + - 443:443
57 + volumes:
58 + - ./nginx/nginx.conf:/etc/nginx/nginx.conf
59 + - ./nginx/server.key:/etc/nginx/certs.d/server.key
60 + - ./nginx/server.crt:/etc/nginx/certs.d/server.crt
61 + restart: always
62 + depends_on:
63 + - copilot-backend
64 + - copilot-frontend
65 +
66 +networks:
67 + default:
68 + driver: bridge
docker-env/.gitignore new
+1
@@ -0,0 +1 @@
1 +copilot-backend-data
frontend/.dockerignore new
+1
@@ -0,0 +1 @@
1 +node_modules
frontend/.env.example new
+11
@@ -0,0 +1,11 @@
1 +# base url
2 +VITE_API_URL=https://0.0.0.0/api
3 +
4 +# value in seconds
5 +VITE_TOKEN_DEBOUNCE_TIME=10
6 +
7 +# alert if value is over
8 +VITE_UNCOMMITTED_JOURNAL_ENTRIES_THRESHOLD=50000
9 +
10 +# value in seconds
11 +VITE_HEALTHCHECKS_INTERVAL=120
frontend/.eslintrc.cjs renamed
frontend/.npmrc renamed
frontend/.nvmrc new
+1
@@ -0,0 +1 @@
1 +20.11.0
frontend/Dockerfile new
+26
@@ -0,0 +1,26 @@
1 +# Use a node.js base image
2 +FROM node:21 as builder
3 +
4 +# Set the working directory
5 +WORKDIR /
6 +
7 +# Copy project files into the working directory
8 +COPY . .
9 +
10 +# Install project dependencies
11 +RUN npm install
12 +
13 +# Run the Vue.js project build
14 +RUN npm run build-only
15 +
16 +FROM nginx:1.24.0-alpine
17 +
18 +# Copy custom Nginx configuration
19 +COPY nginx.conf /etc/nginx/nginx.conf
20 +
21 +# Copy built static files from the builder stage
22 +COPY --from=builder dist /usr/share/nginx/html
23 +
24 +EXPOSE 2000
25 +
26 +CMD ["nginx", "-g", "daemon off;"]
frontend/cypress.config.ts renamed
frontend/cypress/e2e/example.cy.ts renamed
frontend/cypress/e2e/tsconfig.json renamed
frontend/cypress/fixtures/example.json renamed
frontend/cypress/support/commands.ts renamed
frontend/cypress/support/e2e.ts renamed
frontend/figma-tokens.json renamed
frontend/index.html renamed
frontend/nginx.conf new
+23
@@ -0,0 +1,23 @@
1 +worker_processes 1;
2 +
3 +events {
4 + worker_connections 1024;
5 +}
6 +
7 +http {
8 + include mime.types;
9 + default_type application/octet-stream;
10 + sendfile on;
11 + keepalive_timeout 65;
12 + client_max_body_size 256m;
13 +
14 + server {
15 + listen 2000;
16 + server_name _;
17 +
18 + location / {
19 + root /usr/share/nginx/html;
20 + try_files $uri $uri/ /index.html;
21 + }
22 + }
23 +}
frontend/package.json renamed
+5 -5
@@ -22,8 +22,8 @@
22 "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
23 "tailwind-config-viewer": "tailwind-config-viewer -o",
24 "design-tokens": "node scripts/tokens-tool.js",
25 - "start-server-old": "cd backend && uvicorn copilot:app --reload --port=5000",
26 - "start-server": "cd backend && /opt/venv/bin/python copilot.py",
25 + "start-server-old": "cd ../backend && uvicorn copilot:app --reload --port=5000",
26 + "start-server": "cd ../backend && /opt/venv/bin/python copilot.py",
27 "start-vue": "vite --host 0.0.0.0",
28 "start": "concurrently \"npm run start-server\" \"npm run start-vue\"",
29 "libs-check": "taze",
@@ -58,7 +58,7 @@
58 "pinia-plugin-persistedstate": "^3.2.1",
59 "secure-ls": "^1.2.6",
60 "validator": "^13.11.0",
61 - "vue": "^3.4.15",
61 + "vue": "^3.4.16",
62 "vue-advanced-cropper": "^2.8.8",
63 "vue-highlight-words": "^3.0.1",
64 "vue-i18n": "^9.9.1",
@@ -98,7 +98,7 @@
98 "json5": "^2.2.3",
99 "npm-run-all": "^4.1.5",
100 "picocolors": "^1.0.0",
101 - "postcss": "^8.4.34",
101 + "postcss": "^8.4.35",
102 "prettier": "^3.2.5",
103 "sass": "^1.70.0",
104 "start-server-and-test": "^2.0.3",
@@ -108,7 +108,7 @@
108 "ts-node": "^10.9.2",
109 "typescript": "~5.3.3",
110 "unplugin-vue-components": "^0.26.0",
111 - "vite": "^5.0.12",
111 + "vite": "^5.1.0",
112 "vite-bundle-analyzer": "^0.7.0",
113 "vite-bundle-visualizer": "^1.0.1",
114 "vite-svg-loader": "^5.1.0",
frontend/postcss.config.js renamed
frontend/public/.htaccess renamed
frontend/public/favicon.ico renamed
frontend/public/images/connectors/alert creation provisioning.svg renamed
frontend/public/images/connectors/asksocfortress.svg renamed
frontend/public/images/connectors/cortex.svg renamed
frontend/public/images/connectors/dfir-iris.svg renamed
frontend/public/images/connectors/event shipper.svg renamed
frontend/public/images/connectors/grafana.svg renamed
frontend/public/images/connectors/graylog.svg renamed
frontend/public/images/connectors/influxdb.svg renamed
frontend/public/images/connectors/rabbitmq.svg renamed
frontend/public/images/connectors/shuffle.svg renamed
frontend/public/images/connectors/socfortressthreatintel.svg renamed
frontend/public/images/connectors/sublime.svg renamed
frontend/public/images/connectors/velociraptor.svg renamed
frontend/public/images/connectors/wazuh worker provisioning.svg renamed
frontend/public/images/connectors/wazuh-indexer.svg renamed
frontend/public/images/connectors/wazuh-manager.svg renamed
frontend/public/images/img-not-found.svg renamed
frontend/public/images/login/cover.webp renamed
frontend/public/images/login/video.mp4 renamed
frontend/public/images/office365/0-azure-app-new-registration.png renamed
frontend/public/images/office365/1-azure-wazuh-app-register-application.png renamed
frontend/public/images/office365/2-azure-wazuh-app-overview.png renamed
frontend/public/images/office365/3-azure-wazuh-app-create-password-copy-value.png renamed
frontend/public/images/office365/3-azure-wazuh-app-create-password.png renamed
frontend/public/images/office365/4-azure-wazuh-app-configure-permissions-admin-consent.png renamed
frontend/public/images/office365/4-azure-wazuh-app-configure-permissions.png renamed
frontend/public/images/office365/copilot_config_customer_details.PNG renamed
frontend/public/images/office365/copilot_config_customer_integration.PNG renamed
frontend/public/images/office365/copilot_config_customer_integration_auth.PNG renamed
frontend/public/images/office365/copilot_config_customer_integration_config.PNG renamed
frontend/public/logo.svg renamed
frontend/scripts/tokens-tool.js renamed
frontend/src/App.vue renamed
frontend/src/api/agents.ts renamed
frontend/src/api/alerts.ts renamed
frontend/src/api/artifacts.ts renamed
frontend/src/api/askSocfortress.ts renamed
frontend/src/api/auth.ts renamed
frontend/src/api/connectors.ts renamed
frontend/src/api/customers.ts renamed
frontend/src/api/flow.ts renamed
frontend/src/api/graylog.ts renamed
frontend/src/api/healthchecks.ts renamed
frontend/src/api/httpClient.ts renamed
+3 -1
@@ -17,7 +17,9 @@ HttpClient.interceptors.request.use(
17 const store = useAuthStore()
18
19 if (!config.headers) config.headers = {} as AxiosRequestHeaders
20 - config.headers.Authorization = `Bearer ${store.userToken}`
20 + if (store.userToken) {
21 + config.headers.Authorization = `Bearer ${store.userToken}`
22 + }
23
24 if (isJwtExpiring(store.userToken, 60 * 60) && !__TOKEN_REFRESHING && isDebounceTimeOver(__TOKEN_LAST_CHECK)) {
25 __TOKEN_REFRESHING = true
frontend/src/api/index.ts renamed
frontend/src/api/indices.ts renamed
frontend/src/api/integrations.ts renamed
frontend/src/api/logs.ts renamed
frontend/src/api/monitoringAlerts.ts renamed
frontend/src/api/soc.ts renamed
frontend/src/api/threatIntel.ts renamed
frontend/src/assets/icons/brain-icon.svg renamed
frontend/src/assets/images/Ripple-2s-100px.svg renamed
frontend/src/assets/images/copilot_gif.gif renamed
frontend/src/assets/images/copilot_logo.PNG renamed
frontend/src/assets/images/img-not-found.svg renamed
frontend/src/assets/images/pattern-onboard.png renamed
frontend/src/assets/images/placeholder.png renamed
frontend/src/assets/images/socfortress_logo.svg renamed
frontend/src/assets/scss/_variables.scss renamed
frontend/src/assets/scss/apexchart-override.scss renamed
frontend/src/assets/scss/common.scss renamed
frontend/src/assets/scss/fonts.scss renamed
frontend/src/assets/scss/helpers.scss renamed
frontend/src/assets/scss/hljs.scss renamed
frontend/src/assets/scss/index.scss renamed
frontend/src/assets/scss/mixin.scss renamed
frontend/src/assets/scss/naive-override.scss renamed
frontend/src/assets/scss/router-animations.scss renamed
frontend/src/assets/scss/vuesjv-override.scss renamed
frontend/src/components/AuthForm/ForgotPassword.vue renamed
frontend/src/components/AuthForm/SignIn.vue renamed
frontend/src/components/AuthForm/SignUp.vue renamed
frontend/src/components/AuthForm/index.vue renamed
frontend/src/components/__tests__/SampleComponent.spec.ts renamed
frontend/src/components/agents/AgentCard.vue renamed
frontend/src/components/agents/AgentCases.vue renamed
frontend/src/components/agents/AgentToolbar.vue renamed
frontend/src/components/agents/OverviewSection.vue renamed
frontend/src/components/agents/VulnerabilitiesSection.vue renamed
frontend/src/components/agents/VulnerabilityCard.vue renamed
frontend/src/components/agents/agentFlow/AgentFlowCollectList.vue renamed
frontend/src/components/agents/agentFlow/AgentFlowItem.vue renamed
frontend/src/components/agents/agentFlow/AgentFlowList.vue renamed
frontend/src/components/agents/agentFlow/AgentFlowQueryStat.vue renamed
frontend/src/components/agents/agentFlow/AgentFlowTimeline.vue renamed
frontend/src/components/agents/utils.ts renamed
frontend/src/components/alerts/Alert.vue renamed
frontend/src/components/alerts/AlertActions.vue renamed
frontend/src/components/alerts/AlertsFilters.vue renamed
frontend/src/components/alerts/AlertsList.vue renamed
frontend/src/components/alerts/AlertsStats.vue renamed
frontend/src/components/alerts/AlertsStatsItem.vue renamed
frontend/src/components/alerts/AlertsSummary.vue renamed
frontend/src/components/alerts/ThreatIntelButton.vue renamed
frontend/src/components/alerts/ThreatIntelForm.vue renamed
frontend/src/components/alerts/mock.ts renamed
frontend/src/components/artifacts/ArtifactItem.vue renamed
frontend/src/components/artifacts/ArtifactsCollect.vue renamed
frontend/src/components/artifacts/ArtifactsCommand.vue renamed
frontend/src/components/artifacts/ArtifactsList.vue renamed
frontend/src/components/artifacts/ArtifactsQuarantine.vue renamed
frontend/src/components/artifacts/CollectItem.vue renamed
frontend/src/components/artifacts/CommandItem.vue renamed
frontend/src/components/artifacts/QuarantineItem.vue renamed
frontend/src/components/artifacts/mock.ts renamed
frontend/src/components/cards/CardActions.vue renamed
frontend/src/components/cards/CardWrapper.vue renamed
frontend/src/components/common/Badge.vue renamed
frontend/src/components/common/CardStats.vue renamed
frontend/src/components/common/CardStatsDouble.vue renamed
frontend/src/components/common/CardStatsIcon.vue renamed
frontend/src/components/common/FileDrop.vue renamed
frontend/src/components/common/Icon.vue renamed
frontend/src/components/common/ImageCropper.vue renamed
frontend/src/components/common/ImageLoader.vue renamed
frontend/src/components/common/KVCard.vue renamed
frontend/src/components/common/LayoutSettings.vue renamed
frontend/src/components/common/LocaleSelect.vue renamed
frontend/src/components/common/Markdown.vue renamed
frontend/src/components/common/Notifications/List.vue renamed
frontend/src/components/common/Notifications/Toolbar.vue renamed
frontend/src/components/common/PageSplitted.vue renamed
frontend/src/components/common/PaginationIndeterminate.vue renamed
frontend/src/components/common/Percentage.vue renamed
frontend/src/components/common/SearchDialog.vue renamed
frontend/src/components/common/TestScope.vue renamed
frontend/src/components/connectors/ConfigForm/ConfigForm.vue renamed
+1 -1
@@ -7,7 +7,7 @@
7 object-fit="contain"
8 round
9 :size="60"
10 - :src="`/src/assets/images/${
10 + :src="`/images/connectors/${
11 connector ? connector.connector_name.toLowerCase() + '.svg' : 'default-logo.svg'
12 }`"
13 :alt="`${connector.connector_name} Logo`"
frontend/src/components/connectors/ConfigForm/FormTypes/CredentialsType.vue renamed
frontend/src/components/connectors/ConfigForm/FormTypes/FileType.vue renamed
frontend/src/components/connectors/ConfigForm/FormTypes/HostType.vue renamed
frontend/src/components/connectors/ConfigForm/FormTypes/TokenType.vue renamed
frontend/src/components/connectors/ConfigForm/index.ts renamed
frontend/src/components/connectors/ConnectorItem.vue renamed
+1 -1
@@ -10,7 +10,7 @@
10 object-fit="contain"
11 round
12 :size="40"
13 - :src="`/src/assets/images/${
13 + :src="`/images/connectors/${
14 connector ? connector.connector_name.toLowerCase() + '.svg' : 'default-logo.svg'
15 }`"
16 :alt="`${connector.connector_name} Logo`"
frontend/src/components/connectors/ConnectorsList.vue renamed
frontend/src/components/customers/CustomerAgents.vue renamed
frontend/src/components/customers/CustomerCreationButton.vue renamed
frontend/src/components/customers/CustomerForm.vue renamed
frontend/src/components/customers/CustomerInfo.vue renamed
frontend/src/components/customers/CustomerItem.vue renamed
frontend/src/components/customers/CustomerMetaForm.vue renamed
frontend/src/components/customers/CustomersList.vue renamed
frontend/src/components/customers/healthcheck/CustomerHealthcheckItem.vue renamed
frontend/src/components/customers/healthcheck/CustomerHealthcheckList.vue renamed
frontend/src/components/customers/integrations/CustomerIntegrationActions.vue renamed
frontend/src/components/customers/integrations/CustomerIntegrationForm.vue renamed
frontend/src/components/customers/integrations/CustomerIntegrationItem.vue renamed
frontend/src/components/customers/integrations/CustomerIntegrations.vue renamed
frontend/src/components/customers/provision/CustomerProvision.vue renamed
frontend/src/components/customers/provision/CustomerProvisionWizard.vue renamed
frontend/src/components/graylog/Alerts/Item.vue renamed
frontend/src/components/graylog/Alerts/List.vue renamed
frontend/src/components/graylog/Events/Item.vue renamed
frontend/src/components/graylog/Events/List.vue renamed
frontend/src/components/graylog/Inputs/Item.vue renamed
frontend/src/components/graylog/Inputs/List.vue renamed
frontend/src/components/graylog/Messages/Item.vue renamed
frontend/src/components/graylog/Messages/List.vue renamed
frontend/src/components/graylog/Metrics/List.vue renamed
frontend/src/components/graylog/Metrics/UncommittedEntries.vue renamed
frontend/src/components/graylog/MonitoringAlerts/Item.vue renamed
frontend/src/components/graylog/MonitoringAlerts/List.vue renamed
frontend/src/components/graylog/Pipelines/PipeDetails.vue renamed
frontend/src/components/graylog/Pipelines/PipeInfo.vue renamed
frontend/src/components/graylog/Pipelines/PipeList.vue renamed
frontend/src/components/graylog/Pipelines/PipeTitle.vue renamed
frontend/src/components/graylog/Pipelines/Rule.vue renamed
frontend/src/components/graylog/Pipelines/RulesList.vue renamed
frontend/src/components/graylog/Pipelines/RulesSmallList.vue renamed
frontend/src/components/graylog/Streams/Item.vue renamed
frontend/src/components/graylog/Streams/List.vue renamed
frontend/src/components/healthcheck/HealthcheckItem.vue renamed
frontend/src/components/healthcheck/HealthcheckList.vue renamed
frontend/src/components/indices/ClusterHealth.vue renamed
frontend/src/components/indices/Details.vue renamed
frontend/src/components/indices/IndexCard.vue renamed
frontend/src/components/indices/IndexIcon.vue renamed
frontend/src/components/indices/Marquee.vue renamed
frontend/src/components/indices/NodeAllocation.vue renamed
frontend/src/components/indices/TopIndices.vue renamed
frontend/src/components/indices/UnhealthyIndices.vue renamed
frontend/src/components/integrations/IntegrationItem.vue renamed
frontend/src/components/integrations/IntegrationsList.vue renamed
frontend/src/components/logs/LogItem.vue renamed
frontend/src/components/logs/LogsFilters.vue renamed
frontend/src/components/logs/LogsList.vue renamed
frontend/src/components/overview/AgentsCard.vue renamed
frontend/src/components/overview/CustomersCard.vue renamed
frontend/src/components/overview/HealthcheckCard.vue renamed
frontend/src/components/overview/SocAlertsCard.vue renamed
frontend/src/components/profile/ProfileSettings.vue renamed
frontend/src/components/soc/SocAlerts/SocAlertItem.vue renamed
frontend/src/components/soc/SocAlerts/SocAlertItemActions.vue renamed
frontend/src/components/soc/SocAlerts/SocAlertTimeline.vue renamed
frontend/src/components/soc/SocAlerts/SocAlertsBookmarks.vue renamed
frontend/src/components/soc/SocAlerts/SocAlertsFullList.vue renamed
frontend/src/components/soc/SocAlerts/SocAlertsList.vue renamed
frontend/src/components/soc/SocAlerts/SocAssignUser.vue renamed
frontend/src/components/soc/SocAlerts/mock.ts renamed
frontend/src/components/soc/SocCases/SocCaseAssetLink.vue renamed
frontend/src/components/soc/SocCases/SocCaseAssetsItem.vue renamed
frontend/src/components/soc/SocCases/SocCaseAssetsList.vue renamed
frontend/src/components/soc/SocCases/SocCaseItem.vue renamed
frontend/src/components/soc/SocCases/SocCaseItemActions.vue renamed
frontend/src/components/soc/SocCases/SocCaseNote.vue renamed
frontend/src/components/soc/SocCases/SocCaseNoteForm.vue renamed
frontend/src/components/soc/SocCases/SocCaseNoteTimeline.vue renamed
frontend/src/components/soc/SocCases/SocCaseNotesList.vue renamed
frontend/src/components/soc/SocCases/SocCaseTimeline.vue renamed
frontend/src/components/soc/SocCases/SocCasesList.vue renamed
frontend/src/components/soc/SocUsers/SocUserAlerts.vue renamed
frontend/src/components/soc/SocUsers/SocUsersList.vue renamed
frontend/src/components/users/ChangePassword.vue renamed
frontend/src/components/users/UsersList.vue renamed
frontend/src/composables/useFullscreenSwitch.ts renamed
frontend/src/composables/useGlobalActions.ts renamed
frontend/src/composables/useHealthchecksNotify.ts renamed
frontend/src/composables/useHideLayoutFooter.ts renamed
frontend/src/composables/useNotifications.ts renamed
frontend/src/composables/useSearchDialog.ts renamed
frontend/src/composables/useStoreI18n.ts renamed
frontend/src/composables/useThemeSwitch.ts renamed
frontend/src/design-tokens.json renamed
frontend/src/emitter.ts renamed
frontend/src/lang/config.ts renamed
frontend/src/lang/de.ts renamed
frontend/src/lang/en.ts renamed
frontend/src/lang/es.ts renamed
frontend/src/lang/fr.ts renamed
frontend/src/lang/index.ts renamed
frontend/src/lang/it.ts renamed
frontend/src/lang/jp.ts renamed
frontend/src/layouts/Blank/MainContainer.vue renamed
frontend/src/layouts/Blank/index.vue renamed
frontend/src/layouts/HorizontalNav/HeaderBar.vue renamed
frontend/src/layouts/HorizontalNav/MainContainer.vue renamed
frontend/src/layouts/HorizontalNav/Sidebar.vue renamed
frontend/src/layouts/HorizontalNav/SidebarFooter.vue renamed
frontend/src/layouts/HorizontalNav/SidebarHeader.vue renamed
frontend/src/layouts/HorizontalNav/_variables.scss renamed
frontend/src/layouts/HorizontalNav/index.vue renamed
frontend/src/layouts/HorizontalNav/main.scss renamed
frontend/src/layouts/VerticalNav/MainContainer.vue renamed
frontend/src/layouts/VerticalNav/Sidebar.vue renamed
frontend/src/layouts/VerticalNav/SidebarFooter.vue renamed
frontend/src/layouts/VerticalNav/SidebarHeader.vue renamed
frontend/src/layouts/VerticalNav/_variables.scss renamed
frontend/src/layouts/VerticalNav/index.vue renamed
frontend/src/layouts/common/FooterEL.vue renamed
frontend/src/layouts/common/GlobalListener.vue renamed
frontend/src/layouts/common/Logo.vue renamed
frontend/src/layouts/common/Navbar/index.vue renamed
frontend/src/layouts/common/Navbar/items.tsx renamed
frontend/src/layouts/common/Provider.vue renamed
frontend/src/layouts/common/SplashScreen.vue renamed
frontend/src/layouts/common/Toolbar/Avatar.vue renamed
frontend/src/layouts/common/Toolbar/Breadcrumb.vue renamed
frontend/src/layouts/common/Toolbar/FullscreenSwitch.vue renamed
frontend/src/layouts/common/Toolbar/LocaleSwitch.vue renamed
frontend/src/layouts/common/Toolbar/Notifications.vue renamed
frontend/src/layouts/common/Toolbar/PinnedPages.vue renamed
frontend/src/layouts/common/Toolbar/Search.vue renamed
frontend/src/layouts/common/Toolbar/ThemeSwitch.vue renamed
frontend/src/layouts/common/Toolbar/index.vue renamed
frontend/src/main.ts renamed
frontend/src/router-env.d.ts renamed
frontend/src/router/index.ts renamed
frontend/src/stores/auth.ts renamed
frontend/src/stores/healthcheck.ts renamed
frontend/src/stores/i18n.ts renamed
frontend/src/stores/main.ts renamed
frontend/src/stores/settings.ts renamed
frontend/src/stores/theme.ts renamed
frontend/src/types/agents.d.ts renamed
frontend/src/types/alerts.d.ts renamed
frontend/src/types/artifacts.d.ts renamed
frontend/src/types/auth.d.ts renamed
frontend/src/types/connectors.d.ts renamed
frontend/src/types/customers.d.ts renamed
frontend/src/types/flask.d.ts renamed
frontend/src/types/flow.d.ts renamed
frontend/src/types/graylog/alerts.d.ts renamed
frontend/src/types/graylog/event-definition.d.ts renamed
frontend/src/types/graylog/index.d.ts renamed
frontend/src/types/graylog/inputs.d.ts renamed
frontend/src/types/graylog/pipelines.d.ts renamed
frontend/src/types/graylog/stream.d.ts renamed
frontend/src/types/healthchecks.d.ts renamed
frontend/src/types/indices.d.ts renamed
frontend/src/types/integrations.d.ts renamed
frontend/src/types/logs.d.ts renamed
frontend/src/types/monitoringAlerts.d.ts renamed
frontend/src/types/soc/alert.d.ts renamed
frontend/src/types/soc/asset.d.ts renamed
frontend/src/types/soc/case.d.ts renamed
frontend/src/types/soc/note.d.ts renamed
frontend/src/types/soc/user.d.ts renamed
frontend/src/types/theme.d.ts renamed
frontend/src/types/threatIntel.d.ts renamed
frontend/src/utils/auth.ts renamed
frontend/src/utils/dayjs.ts renamed
frontend/src/utils/index.ts renamed
frontend/src/utils/theme.ts renamed
frontend/src/views/AgentOverview.vue renamed
frontend/src/views/Agents.vue renamed
frontend/src/views/Alerts.vue renamed
frontend/src/views/Artifacts.vue renamed
frontend/src/views/Auth/Login.vue renamed
frontend/src/views/Connectors.vue renamed
frontend/src/views/Customers.vue renamed
frontend/src/views/Healthcheck.vue renamed
frontend/src/views/Indices.vue renamed
frontend/src/views/Integrations.vue renamed
frontend/src/views/Logs.vue renamed
frontend/src/views/NotFound.vue renamed
frontend/src/views/Overview.vue renamed
frontend/src/views/Profile.vue renamed
frontend/src/views/Users.vue renamed
frontend/src/views/graylog/Management.vue renamed
frontend/src/views/graylog/Metrics.vue renamed
frontend/src/views/graylog/Pipelines.vue renamed
frontend/src/views/soc/Alerts.vue renamed
frontend/src/views/soc/Cases.vue renamed
frontend/src/views/soc/Users.vue renamed
frontend/src/vite-env.d.ts renamed
frontend/tailwind.config.js renamed
frontend/tsconfig.app.json renamed
frontend/tsconfig.json renamed
frontend/tsconfig.node.json renamed
frontend/tsconfig.vitest.json renamed
frontend/vite.config.mts renamed
-10
@@ -4,7 +4,6 @@ import vue from "@vitejs/plugin-vue"
4 import vueJsx from "@vitejs/plugin-vue-jsx"
5 import svgLoader from "vite-svg-loader"
6 import Components from "unplugin-vue-components/vite"
7 -const hash = Math.floor(Math.random() * 90000) + 10000
7 // import { analyzer } from "vite-bundle-analyzer"
8
9 // https://vitejs.dev/config/
@@ -30,15 +29,6 @@ export default defineConfig({
29 "@": fileURLToPath(new URL("./src", import.meta.url))
30 }
31 },
33 - build: {
34 - rollupOptions: {
35 - output: {
36 - entryFileNames: `[name]` + hash + `.js`,
37 - chunkFileNames: `[name]` + hash + `.js`,
38 - assetFileNames: `[name]` + hash + `.[ext]`
39 - }
40 - }
41 - },
32 optimizeDeps: {
33 include: ["fast-deep-equal"]
34 }
frontend/vitest.config.ts renamed
git_tasks/Vagrantfile renamed
git_tasks/pyproject.toml renamed
nginx/create-certs.sh new
+7
@@ -0,0 +1,7 @@
1 +#!/usr/bin/env bash
2 +
3 +openssl genpkey -algorithm RSA -out server.key -aes256
4 +openssl req -new -key server.key -out server.csr
5 +cp server.key no-passphrase.key
6 +openssl rsa -in no-passphrase.key -out server.key
7 +openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
nginx/nginx.conf new
+50
@@ -0,0 +1,50 @@
1 +# User directive is commented out; you can specify nginx user if needed
2 +# user nobody;
3 +worker_processes 1;
4 +
5 +events {
6 + worker_connections 1024;
7 +}
8 +
9 +http {
10 + include mime.types;
11 + default_type application/octet-stream;
12 + sendfile on;
13 + keepalive_timeout 65;
14 +
15 + # Custom Logging Format
16 + log_format upstreamlog '[$time_local] $remote_addr - $remote_user - $server_name to: $upstream_addr: $request upstream_response_time $upstream_response_time msec $msec request_time $request_time';
17 + access_log /var/log/nginx/access.log upstreamlog;
18 +
19 + # HTTP to HTTPS redirection
20 + server {
21 + listen 80;
22 + return 301 https://$host$request_uri;
23 + }
24 +
25 + # HTTPS Server
26 + server {
27 + listen 443 ssl;
28 +
29 + ssl_certificate /etc/nginx/certs.d/server.crt; # Ensure the path to your SSL certificate is correct
30 + ssl_certificate_key /etc/nginx/certs.d/server.key; # Ensure the path to your SSL key is correct
31 +
32 + # Resolver
33 + resolver 127.0.0.11 ipv6=off;
34 +
35 + # Proxy /api requests to the FastAPI backend
36 + location /api {
37 + proxy_set_header Host $host;
38 + proxy_set_header X-Real-IP $remote_addr;
39 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
40 + proxy_set_header X-Forwarded-Proto $scheme;
41 + proxy_pass http://copilot-backend:5000;
42 + }
43 +
44 + # Proxy other requests to the Vue frontend
45 + location / {
46 + client_max_body_size 0;
47 + proxy_pass http://copilot-frontend:2000;
48 + }
49 + }
50 +}
nginx/server.crt new
+22
@@ -0,0 +1,22 @@
1 +-----BEGIN CERTIFICATE-----
2 +MIIDqTCCApECFG6uDa2UWfKBKQDSTDcPmZGxiLQiMA0GCSqGSIb3DQEBCwUAMIGQ
3 +MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1c3RpbjEU
4 +MBIGA1UECgwLU09DRm9ydHJlc3MxFDASBgNVBAsMC1NPQ0ZvcnRyZXNzMRAwDgYD
5 +VQQDDAdjb3BpbG90MSIwIAYJKoZIhvcNAQkBFhNpbmZvQHNvY2ZvcnRyZXNzLmNv
6 +MB4XDTI0MDIwODE4NDcxOFoXDTI1MDIwNzE4NDcxOFowgZAxCzAJBgNVBAYTAlVT
7 +MQ4wDAYDVQQIDAVUZXhhczEPMA0GA1UEBwwGQXVzdGluMRQwEgYDVQQKDAtTT0NG
8 +b3J0cmVzczEUMBIGA1UECwwLU09DRm9ydHJlc3MxEDAOBgNVBAMMB2NvcGlsb3Qx
9 +IjAgBgkqhkiG9w0BCQEWE2luZm9Ac29jZm9ydHJlc3MuY28wggEiMA0GCSqGSIb3
10 +DQEBAQUAA4IBDwAwggEKAoIBAQDWPucTtfgCAVYjPh2G/va0zUxh+J852RRlXTvl
11 ++2ydG75ZuzQYHN2aKQ4pY2Vu5MzzabMP9bqGnR2FGsSNwUJVmWLi6cAGyUSsyHA6
12 +cs4yEn+X4AJ9PzTPWRD2j7ABLmTC565Tc2kR6srNl+dwCGGPsVxfevfR2bdl+q+L
13 +QUUOaPTuJfCmr6ummcM5Gm2uLqOctdf+2VGd/h1dcGqJEUnn/+BpP3YF3qnf5Vf/
14 +o5eRBvDB1Il4ub92J3eCdR8K5x604T2VHbrLqHxKMa+4Dzvmve8DSdQ7PDfvo3Ty
15 +dZfF9F4IdayICynPv/1mCi3WK601dkUAVNWk4nmmNB8Q/JqvAgMBAAEwDQYJKoZI
16 +hvcNAQELBQADggEBAJGTbGZSQ0SFMtRwp7gZ6PysaAdrRGVlnojYO8IXnZ28Agly
17 +3yPVtwOQLZ6l4O3dPBamjELp4g08US844UVYvrlvfj9idwQvYBS2XI4GEm5WpEiT
18 +yY7CIHppmY82Y7vZx0R5L3AlAEDkKq/aDWxJmAwIn2R/z0el9TLqxjJ0DXRO4hHm
19 ++v7KANKc/jnpWkugjzx68DPzw7Ck5oTbXMUw3vfpx0YjK74526U6UjC/Wa9iBvUb
20 +p4mFRGRX3MkdaOnIVdFCf1rOkHZbvJdrA6XZDyNxVXlDNVcljuabnwOEjFJgda3L
21 +FFJLmhehHIUN3TotdIYDSixjACKTWT/DiXbyRaE=
22 +-----END CERTIFICATE-----
nginx/server.key new
+27
@@ -0,0 +1,27 @@
1 +-----BEGIN RSA PRIVATE KEY-----
2 +MIIEpAIBAAKCAQEA1j7nE7X4AgFWIz4dhv72tM1MYfifOdkUZV075ftsnRu+Wbs0
3 +GBzdmikOKWNlbuTM82mzD/W6hp0dhRrEjcFCVZli4unABslErMhwOnLOMhJ/l+AC
4 +fT80z1kQ9o+wAS5kwueuU3NpEerKzZfncAhhj7FcX3r30dm3Zfqvi0FFDmj07iXw
5 +pq+rppnDORptri6jnLXX/tlRnf4dXXBqiRFJ5//gaT92Bd6p3+VX/6OXkQbwwdSJ
6 +eLm/did3gnUfCucetOE9lR26y6h8SjGvuA875r3vA0nUOzw376N08nWXxfReCHWs
7 +iAspz7/9Zgot1iutNXZFAFTVpOJ5pjQfEPyarwIDAQABAoIBADK7nH6HZZ4b8OjR
8 +ia5w67yurLq6ZAIPzIugQ1HXcsXVTwLymIhpNXSO6kKUve0/kuTFD9jkqHG9/MKL
9 +LyYeMHYSp17yWT2CJZgAssq0au1iggJV0mEUOY4sGh84sUXmVYft0tMCDkxm2/VJ
10 +Vk2qPmgHaMdXWB3CP3KPpUgFgbPCad/pKVGRBUX4QXHI9SkqkLHx32sW/36t250L
11 +YmB+le9iip+/2d3pn0j+IeU1nt95vQbMlFJ4m/PsBHFNScXpfE4d1GpHWMiZ6bJw
12 +BObaiIzNYRHTdv7gX6QBIgi2wq1U9gapcwxJCqq3yt4BLrV2ziSEyvn1AZYKjboG
13 +ZBCyWEkCgYEA8O0x5K1vZJlEoEmTSEdYZWZlw7KEdndiP79/TRb/q+vBUglh2ssN
14 +CU0iuIyi8SFd9T3sg8AFaSB+sxFNsYSiJ6qxcgscYtofqINoTIvqF8Zq1L8LhNKh
15 +Syww6e+++9N1UoYve7nts1HTZxXqYkNhGZqvLLq1VZIsCl1wgtzc3i0CgYEA46Zf
16 +qYwK5kEWhxTmqIaQIz/axVlwWFf1czyIlwn2uMI4a2X6UMmh4oOHExA7p5bcW+RZ
17 +ERpUD2S4cu4yi3pOO2wBVXWnObDb6Ydhjnnuv1jj5+J61iyvmimHIRfIL/aowdgp
18 +GL3ylz2ABJdrL60o6BuYZzm60zYxamAal3OLQcsCgYEAjvD70CRCWdpBiCP++Twz
19 +Kv4k1IX+51aPeSkBu1gw2uymJykf4LLmCiW0uu/d4NieKVnTEJwF2ciIw285vF/n
20 +4Ub5vqLu9Ytaix1j7T555h831P+AMw7Q2YvffXxbwIdGLW8lju/SIrkihx/OjvxR
21 +COLomc4wthNig8fKcCnf9QUCgYEAgm43jx/iusv+CUUrZtE6Ukfqo3m8CZHT2sOL
22 +Cua1imQVCUuzYIg2DV8IjB6X30xrUucBNPBdyzgNCiR5pzpbCma2WO2A82fmh+V+
23 +OrCX3uAiLCCx+nY94lj6yDYlB34OcInV+gwSqx6cc4UB754JuvrclgAYj50UMPuB
24 +INcdqYMCgYBh4UCEZ4BJ6bQAJRnmtZ+iXgDrPt1ddHSjS0w5tDJcA470Qsvm4B92
25 +lpiHSyQvVegR6hqbf/I7JnVrmHoeLZu3XgIxNb5a5vpvekCtb4V+ZsAcFQ9ZKxBx
26 +CVAg4Be1wG47d1sU3zDcswJdCksvH4YMmZR6IiIoTK9mvTeqBsZuQw==
27 +-----END RSA PRIVATE KEY-----
package-lock.json deleted
-13354
@@ -1,13354 +0,0 @@
1 -{
2 - "name": "copilot",
3 - "version": "1.0.0",
4 - "lockfileVersion": 3,
5 - "requires": true,
6 - "packages": {
7 - "": {
8 - "name": "copilot",
9 - "version": "1.0.0",
10 - "dependencies": {
11 - "@ajoelp/json-to-formdata": "^1.5.0",
12 - "@f3ve/vue-markdown-it": "^0.2.0",
13 - "@fontsource/jetbrains-mono": "^5.0.18",
14 - "@fontsource/lexend": "^5.0.18",
15 - "@fontsource/public-sans": "^5.0.16",
16 - "@popperjs/core": "^2.11.8",
17 - "@vueuse/components": "^10.7.2",
18 - "@vueuse/core": "^10.7.2",
19 - "apexcharts": "^3.45.2",
20 - "bytes": "^3.1.2",
21 - "colord": "^2.9.3",
22 - "crypto-js": "^4.2.0",
23 - "dayjs": "^1.11.10",
24 - "detect-touch-device": "^1.1.6",
25 - "echarts": "^5.4.3",
26 - "jose": "^5.2.1",
27 - "js-md5": "^0.8.3",
28 - "lodash": "^4.17.21",
29 - "markdown-it-highlightjs": "^4.0.1",
30 - "mitt": "^3.0.1",
31 - "naive-ui": "^2.37.3",
32 - "password-validator": "^5.3.0",
33 - "pinia": "^2.1.7",
34 - "pinia-plugin-persistedstate": "^3.2.1",
35 - "secure-ls": "^1.2.6",
36 - "validator": "^13.11.0",
37 - "vue": "^3.4.15",
38 - "vue-advanced-cropper": "^2.8.8",
39 - "vue-highlight-words": "^3.0.1",
40 - "vue-i18n": "^9.9.1",
41 - "vue-router": "^4.2.5",
42 - "vue-sjv": "^0.0.6",
43 - "vue3-apexcharts": "^1.4.4",
44 - "vue3-marquee": "^4.1.0"
45 - },
46 - "devDependencies": {
47 - "@clack/prompts": "^0.7.0",
48 - "@iconify/vue": "^4.1.1",
49 - "@rushstack/eslint-patch": "^1.7.2",
50 - "@tsconfig/node18": "^18.2.2",
51 - "@types/bytes": "^3.1.4",
52 - "@types/fs-extra": "^11.0.4",
53 - "@types/highlight.js": "^10.1.0",
54 - "@types/inquirer": "^9.0.7",
55 - "@types/jsdom": "^21.1.6",
56 - "@types/lodash": "^4.14.202",
57 - "@types/markdown-it": "^13.0.7",
58 - "@types/markdown-it-highlightjs": "^3.3.4",
59 - "@types/node": "^20.11.16",
60 - "@types/validator": "^13.11.9",
61 - "@vitejs/plugin-vue": "^5.0.3",
62 - "@vitejs/plugin-vue-jsx": "^3.1.0",
63 - "@vue/eslint-config-prettier": "^9.0.0",
64 - "@vue/eslint-config-typescript": "^12.0.0",
65 - "@vue/test-utils": "^2.4.4",
66 - "@vue/tsconfig": "^0.5.1",
67 - "autoprefixer": "^10.4.17",
68 - "cypress": "^13.6.4",
69 - "eslint": "^8.56.0",
70 - "eslint-plugin-cypress": "^2.15.1",
71 - "eslint-plugin-vue": "^9.21.1",
72 - "fs-extra": "^11.2.0",
73 - "jsdom": "^24.0.0",
74 - "json5": "^2.2.3",
75 - "npm-run-all": "^4.1.5",
76 - "picocolors": "^1.0.0",
77 - "postcss": "^8.4.34",
78 - "prettier": "^3.2.5",
79 - "sass": "^1.70.0",
80 - "start-server-and-test": "^2.0.3",
81 - "tailwind-config-viewer": "^1.7.3",
82 - "tailwindcss": "^3.4.1",
83 - "taze": "^0.13.3",
84 - "ts-node": "^10.9.2",
85 - "typescript": "~5.3.3",
86 - "unplugin-vue-components": "^0.26.0",
87 - "vite": "^5.0.12",
88 - "vite-bundle-analyzer": "^0.7.0",
89 - "vite-bundle-visualizer": "^1.0.1",
90 - "vite-svg-loader": "^5.1.0",
91 - "vitest": "^1.2.2",
92 - "vue-tsc": "^1.8.27"
93 - },
94 - "engines": {
95 - "node": ">=18.0.0"
96 - }
97 - },
98 - "node_modules/@aashutoshrathi/word-wrap": {
99 - "version": "1.2.6",
100 - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
101 - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
102 - "engines": {
103 - "node": ">=0.10.0"
104 - }
105 - },
106 - "node_modules/@ajoelp/json-to-formdata": {
107 - "version": "1.5.0",
108 - "resolved": "https://registry.npmjs.org/@ajoelp/json-to-formdata/-/json-to-formdata-1.5.0.tgz",
109 - "integrity": "sha512-nrlfeTSL0X0dtx5r2KpzPiqLSIQquiiJjUKsQAKzWaCmO2QoYZCyb5ENZwF3YoffKronOCJr25mxaD8JRJmK8w==",
110 - "dependencies": {
111 - "lodash": "4.17.21"
112 - }
113 - },
114 - "node_modules/@alloc/quick-lru": {
115 - "version": "5.2.0",
116 - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
117 - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
118 - "dev": true,
119 - "engines": {
120 - "node": ">=10"
121 - },
122 - "funding": {
123 - "url": "https://github.com/sponsors/sindresorhus"
124 - }
125 - },
126 - "node_modules/@ampproject/remapping": {
127 - "version": "2.2.1",
128 - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
129 - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==",
130 - "dev": true,
131 - "dependencies": {
132 - "@jridgewell/gen-mapping": "^0.3.0",
133 - "@jridgewell/trace-mapping": "^0.3.9"
134 - },
135 - "engines": {
136 - "node": ">=6.0.0"
137 - }
138 - },
139 - "node_modules/@antfu/ni": {
140 - "version": "0.21.12",
141 - "resolved": "https://registry.npmjs.org/@antfu/ni/-/ni-0.21.12.tgz",
142 - "integrity": "sha512-2aDL3WUv8hMJb2L3r/PIQWsTLyq7RQr3v9xD16fiz6O8ys1xEyLhhTOv8gxtZvJiTzjTF5pHoArvRdesGL1DMQ==",
143 - "dev": true,
144 - "bin": {
145 - "na": "bin/na.mjs",
146 - "nci": "bin/nci.mjs",
147 - "ni": "bin/ni.mjs",
148 - "nlx": "bin/nlx.mjs",
149 - "nr": "bin/nr.mjs",
150 - "nu": "bin/nu.mjs",
151 - "nun": "bin/nun.mjs"
152 - }
153 - },
154 - "node_modules/@antfu/utils": {
155 - "version": "0.7.7",
156 - "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.7.tgz",
157 - "integrity": "sha512-gFPqTG7otEJ8uP6wrhDv6mqwGWYZKNvAcCq6u9hOj0c+IKCEsY4L1oC9trPq2SaWIzAfHvqfBDxF591JkMf+kg==",
158 - "funding": {
159 - "url": "https://github.com/sponsors/antfu"
160 - }
161 - },
162 - "node_modules/@babel/code-frame": {
163 - "version": "7.23.5",
164 - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
165 - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
166 - "dev": true,
167 - "dependencies": {
168 - "@babel/highlight": "^7.23.4",
169 - "chalk": "^2.4.2"
170 - },
171 - "engines": {
172 - "node": ">=6.9.0"
173 - }
174 - },
175 - "node_modules/@babel/compat-data": {
176 - "version": "7.23.5",
177 - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz",
178 - "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==",
179 - "dev": true,
180 - "engines": {
181 - "node": ">=6.9.0"
182 - }
183 - },
184 - "node_modules/@babel/core": {
185 - "version": "7.23.9",
186 - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz",
187 - "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==",
188 - "dev": true,
189 - "dependencies": {
190 - "@ampproject/remapping": "^2.2.0",
191 - "@babel/code-frame": "^7.23.5",
192 - "@babel/generator": "^7.23.6",
193 - "@babel/helper-compilation-targets": "^7.23.6",
194 - "@babel/helper-module-transforms": "^7.23.3",
195 - "@babel/helpers": "^7.23.9",
196 - "@babel/parser": "^7.23.9",
197 - "@babel/template": "^7.23.9",
198 - "@babel/traverse": "^7.23.9",
199 - "@babel/types": "^7.23.9",
200 - "convert-source-map": "^2.0.0",
201 - "debug": "^4.1.0",
202 - "gensync": "^1.0.0-beta.2",
203 - "json5": "^2.2.3",
204 - "semver": "^6.3.1"
205 - },
206 - "engines": {
207 - "node": ">=6.9.0"
208 - },
209 - "funding": {
210 - "type": "opencollective",
211 - "url": "https://opencollective.com/babel"
212 - }
213 - },
214 - "node_modules/@babel/core/node_modules/semver": {
215 - "version": "6.3.1",
216 - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
217 - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
218 - "dev": true,
219 - "bin": {
220 - "semver": "bin/semver.js"
221 - }
222 - },
223 - "node_modules/@babel/generator": {
224 - "version": "7.23.6",
225 - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
226 - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
227 - "dev": true,
228 - "dependencies": {
229 - "@babel/types": "^7.23.6",
230 - "@jridgewell/gen-mapping": "^0.3.2",
231 - "@jridgewell/trace-mapping": "^0.3.17",
232 - "jsesc": "^2.5.1"
233 - },
234 - "engines": {
235 - "node": ">=6.9.0"
236 - }
237 - },
238 - "node_modules/@babel/helper-annotate-as-pure": {
239 - "version": "7.22.5",
240 - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz",
241 - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==",
242 - "dev": true,
243 - "dependencies": {
244 - "@babel/types": "^7.22.5"
245 - },
246 - "engines": {
247 - "node": ">=6.9.0"
248 - }
249 - },
250 - "node_modules/@babel/helper-compilation-targets": {
251 - "version": "7.23.6",
252 - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz",
253 - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==",
254 - "dev": true,
255 - "dependencies": {
256 - "@babel/compat-data": "^7.23.5",
257 - "@babel/helper-validator-option": "^7.23.5",
258 - "browserslist": "^4.22.2",
259 - "lru-cache": "^5.1.1",
260 - "semver": "^6.3.1"
261 - },
262 - "engines": {
263 - "node": ">=6.9.0"
264 - }
265 - },
266 - "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
267 - "version": "6.3.1",
268 - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
269 - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
270 - "dev": true,
271 - "bin": {
272 - "semver": "bin/semver.js"
273 - }
274 - },
275 - "node_modules/@babel/helper-create-class-features-plugin": {
276 - "version": "7.23.9",
277 - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.9.tgz",
278 - "integrity": "sha512-B2L9neXTIyPQoXDm+NtovPvG6VOLWnaXu3BIeVDWwdKFgG30oNa6CqVGiJPDWQwIAK49t9gnQI9c6K6RzabiKw==",
279 - "dev": true,
280 - "dependencies": {
281 - "@babel/helper-annotate-as-pure": "^7.22.5",
282 - "@babel/helper-environment-visitor": "^7.22.20",
283 - "@babel/helper-function-name": "^7.23.0",
284 - "@babel/helper-member-expression-to-functions": "^7.23.0",
285 - "@babel/helper-optimise-call-expression": "^7.22.5",
286 - "@babel/helper-replace-supers": "^7.22.20",
287 - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
288 - "@babel/helper-split-export-declaration": "^7.22.6",
289 - "semver": "^6.3.1"
290 - },
291 - "engines": {
292 - "node": ">=6.9.0"
293 - },
294 - "peerDependencies": {
295 - "@babel/core": "^7.0.0"
296 - }
297 - },
298 - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
299 - "version": "6.3.1",
300 - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
301 - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
302 - "dev": true,
303 - "bin": {
304 - "semver": "bin/semver.js"
305 - }
306 - },
307 - "node_modules/@babel/helper-environment-visitor": {
308 - "version": "7.22.20",
309 - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
310 - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
311 - "dev": true,
312 - "engines": {
313 - "node": ">=6.9.0"
314 - }
315 - },
316 - "node_modules/@babel/helper-function-name": {
317 - "version": "7.23.0",
318 - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
319 - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
320 - "dev": true,
321 - "dependencies": {
322 - "@babel/template": "^7.22.15",
323 - "@babel/types": "^7.23.0"
324 - },
325 - "engines": {
326 - "node": ">=6.9.0"
327 - }
328 - },
329 - "node_modules/@babel/helper-hoist-variables": {
330 - "version": "7.22.5",
331 - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
332 - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
333 - "dev": true,
334 - "dependencies": {
335 - "@babel/types": "^7.22.5"
336 - },
337 - "engines": {
338 - "node": ">=6.9.0"
339 - }
340 - },
341 - "node_modules/@babel/helper-member-expression-to-functions": {
342 - "version": "7.23.0",
343 - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz",
344 - "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==",
345 - "dev": true,
346 - "dependencies": {
347 - "@babel/types": "^7.23.0"
348 - },
349 - "engines": {
350 - "node": ">=6.9.0"
351 - }
352 - },
353 - "node_modules/@babel/helper-module-imports": {
354 - "version": "7.22.15",
355 - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
356 - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==",
357 - "dev": true,
358 - "dependencies": {
359 - "@babel/types": "^7.22.15"
360 - },
361 - "engines": {
362 - "node": ">=6.9.0"
363 - }
364 - },
365 - "node_modules/@babel/helper-module-transforms": {
366 - "version": "7.23.3",
367 - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz",
368 - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==",
369 - "dev": true,
370 - "dependencies": {
371 - "@babel/helper-environment-visitor": "^7.22.20",
372 - "@babel/helper-module-imports": "^7.22.15",
373 - "@babel/helper-simple-access": "^7.22.5",
374 - "@babel/helper-split-export-declaration": "^7.22.6",
375 - "@babel/helper-validator-identifier": "^7.22.20"
376 - },
377 - "engines": {
378 - "node": ">=6.9.0"
379 - },
380 - "peerDependencies": {
381 - "@babel/core": "^7.0.0"
382 - }
383 - },
384 - "node_modules/@babel/helper-optimise-call-expression": {
385 - "version": "7.22.5",
386 - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz",
387 - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==",
388 - "dev": true,
389 - "dependencies": {
390 - "@babel/types": "^7.22.5"
391 - },
392 - "engines": {
393 - "node": ">=6.9.0"
394 - }
395 - },
396 - "node_modules/@babel/helper-plugin-utils": {
397 - "version": "7.22.5",
398 - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz",
399 - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==",
400 - "dev": true,
401 - "engines": {
402 - "node": ">=6.9.0"
403 - }
404 - },
405 - "node_modules/@babel/helper-replace-supers": {
406 - "version": "7.22.20",
407 - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz",
408 - "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==",
409 - "dev": true,
410 - "dependencies": {
411 - "@babel/helper-environment-visitor": "^7.22.20",
412 - "@babel/helper-member-expression-to-functions": "^7.22.15",
413 - "@babel/helper-optimise-call-expression": "^7.22.5"
414 - },
415 - "engines": {
416 - "node": ">=6.9.0"
417 - },
418 - "peerDependencies": {
419 - "@babel/core": "^7.0.0"
420 - }
421 - },
422 - "node_modules/@babel/helper-simple-access": {
423 - "version": "7.22.5",
424 - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz",
425 - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==",
426 - "dev": true,
427 - "dependencies": {
428 - "@babel/types": "^7.22.5"
429 - },
430 - "engines": {
431 - "node": ">=6.9.0"
432 - }
433 - },
434 - "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
435 - "version": "7.22.5",
436 - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz",
437 - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==",
438 - "dev": true,
439 - "dependencies": {
440 - "@babel/types": "^7.22.5"
441 - },
442 - "engines": {
443 - "node": ">=6.9.0"
444 - }
445 - },
446 - "node_modules/@babel/helper-split-export-declaration": {
447 - "version": "7.22.6",
448 - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
449 - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
450 - "dev": true,
451 - "dependencies": {
452 - "@babel/types": "^7.22.5"
453 - },
454 - "engines": {
455 - "node": ">=6.9.0"
456 - }
457 - },
458 - "node_modules/@babel/helper-string-parser": {
459 - "version": "7.23.4",
460 - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
461 - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
462 - "dev": true,
463 - "engines": {
464 - "node": ">=6.9.0"
465 - }
466 - },
467 - "node_modules/@babel/helper-validator-identifier": {
468 - "version": "7.22.20",
469 - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
470 - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
471 - "dev": true,
472 - "engines": {
473 - "node": ">=6.9.0"
474 - }
475 - },
476 - "node_modules/@babel/helper-validator-option": {
477 - "version": "7.23.5",
478 - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz",
479 - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==",
480 - "dev": true,
481 - "engines": {
482 - "node": ">=6.9.0"
483 - }
484 - },
485 - "node_modules/@babel/helpers": {
486 - "version": "7.23.9",
487 - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz",
488 - "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==",
489 - "dev": true,
490 - "dependencies": {
491 - "@babel/template": "^7.23.9",
492 - "@babel/traverse": "^7.23.9",
493 - "@babel/types": "^7.23.9"
494 - },
495 - "engines": {
496 - "node": ">=6.9.0"
497 - }
498 - },
499 - "node_modules/@babel/highlight": {
500 - "version": "7.23.4",
501 - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
502 - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
503 - "dev": true,
504 - "dependencies": {
505 - "@babel/helper-validator-identifier": "^7.22.20",
506 - "chalk": "^2.4.2",
507 - "js-tokens": "^4.0.0"
508 - },
509 - "engines": {
510 - "node": ">=6.9.0"
511 - }
512 - },
513 - "node_modules/@babel/parser": {
514 - "version": "7.23.9",
515 - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz",
516 - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==",
517 - "bin": {
518 - "parser": "bin/babel-parser.js"
519 - },
520 - "engines": {
521 - "node": ">=6.0.0"
522 - }
523 - },
524 - "node_modules/@babel/plugin-syntax-jsx": {
525 - "version": "7.23.3",
526 - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz",
527 - "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==",
528 - "dev": true,
529 - "dependencies": {
530 - "@babel/helper-plugin-utils": "^7.22.5"
531 - },
532 - "engines": {
533 - "node": ">=6.9.0"
534 - },
535 - "peerDependencies": {
536 - "@babel/core": "^7.0.0-0"
537 - }
538 - },
539 - "node_modules/@babel/plugin-syntax-typescript": {
540 - "version": "7.23.3",
541 - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz",
542 - "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==",
543 - "dev": true,
544 - "dependencies": {
545 - "@babel/helper-plugin-utils": "^7.22.5"
546 - },
547 - "engines": {
548 - "node": ">=6.9.0"
549 - },
550 - "peerDependencies": {
551 - "@babel/core": "^7.0.0-0"
552 - }
553 - },
554 - "node_modules/@babel/plugin-transform-typescript": {
555 - "version": "7.23.6",
556 - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.6.tgz",
557 - "integrity": "sha512-6cBG5mBvUu4VUD04OHKnYzbuHNP8huDsD3EDqqpIpsswTDoqHCjLoHb6+QgsV1WsT2nipRqCPgxD3LXnEO7XfA==",
558 - "dev": true,
559 - "dependencies": {
560 - "@babel/helper-annotate-as-pure": "^7.22.5",
561 - "@babel/helper-create-class-features-plugin": "^7.23.6",
562 - "@babel/helper-plugin-utils": "^7.22.5",
563 - "@babel/plugin-syntax-typescript": "^7.23.3"
564 - },
565 - "engines": {
566 - "node": ">=6.9.0"
567 - },
568 - "peerDependencies": {
569 - "@babel/core": "^7.0.0-0"
570 - }
571 - },
572 - "node_modules/@babel/runtime": {
573 - "version": "7.23.9",
574 - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz",
575 - "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==",
576 - "dependencies": {
577 - "regenerator-runtime": "^0.14.0"
578 - },
579 - "engines": {
580 - "node": ">=6.9.0"
581 - }
582 - },
583 - "node_modules/@babel/template": {
584 - "version": "7.23.9",
585 - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz",
586 - "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==",
587 - "dev": true,
588 - "dependencies": {
589 - "@babel/code-frame": "^7.23.5",
590 - "@babel/parser": "^7.23.9",
591 - "@babel/types": "^7.23.9"
592 - },
593 - "engines": {
594 - "node": ">=6.9.0"
595 - }
596 - },
597 - "node_modules/@babel/traverse": {
598 - "version": "7.23.9",
599 - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz",
600 - "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==",
601 - "dev": true,
602 - "dependencies": {
603 - "@babel/code-frame": "^7.23.5",
604 - "@babel/generator": "^7.23.6",
605 - "@babel/helper-environment-visitor": "^7.22.20",
606 - "@babel/helper-function-name": "^7.23.0",
607 - "@babel/helper-hoist-variables": "^7.22.5",
608 - "@babel/helper-split-export-declaration": "^7.22.6",
609 - "@babel/parser": "^7.23.9",
610 - "@babel/types": "^7.23.9",
611 - "debug": "^4.3.1",
612 - "globals": "^11.1.0"
613 - },
614 - "engines": {
615 - "node": ">=6.9.0"
616 - }
617 - },
618 - "node_modules/@babel/traverse/node_modules/globals": {
619 - "version": "11.12.0",
620 - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
621 - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
622 - "dev": true,
623 - "engines": {
624 - "node": ">=4"
625 - }
626 - },
627 - "node_modules/@babel/types": {
628 - "version": "7.23.9",
629 - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz",
630 - "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==",
631 - "dev": true,
632 - "dependencies": {
633 - "@babel/helper-string-parser": "^7.23.4",
634 - "@babel/helper-validator-identifier": "^7.22.20",
635 - "to-fast-properties": "^2.0.0"
636 - },
637 - "engines": {
638 - "node": ">=6.9.0"
639 - }
640 - },
641 - "node_modules/@clack/core": {
642 - "version": "0.3.3",
643 - "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.3.3.tgz",
644 - "integrity": "sha512-5ZGyb75BUBjlll6eOa1m/IZBxwk91dooBWhPSL67sWcLS0zt9SnswRL0l26TVdBhb0wnWORRxUn//uH6n4z7+A==",
645 - "dev": true,
646 - "dependencies": {
647 - "picocolors": "^1.0.0",
648 - "sisteransi": "^1.0.5"
649 - }
650 - },
651 - "node_modules/@clack/prompts": {
652 - "version": "0.7.0",
653 - "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.7.0.tgz",
654 - "integrity": "sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA==",
655 - "bundleDependencies": [
656 - "is-unicode-supported"
657 - ],
658 - "dev": true,
659 - "dependencies": {
660 - "@clack/core": "^0.3.3",
661 - "is-unicode-supported": "*",
662 - "picocolors": "^1.0.0",
663 - "sisteransi": "^1.0.5"
664 - }
665 - },
666 - "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
667 - "version": "1.3.0",
668 - "inBundle": true,
669 - "license": "MIT",
670 - "engines": {
671 - "node": ">=12"
672 - },
673 - "funding": {
674 - "url": "https://github.com/sponsors/sindresorhus"
675 - }
676 - },
677 - "node_modules/@colors/colors": {
678 - "version": "1.5.0",
679 - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
680 - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
681 - "dev": true,
682 - "optional": true,
683 - "engines": {
684 - "node": ">=0.1.90"
685 - }
686 - },
687 - "node_modules/@cspotcode/source-map-support": {
688 - "version": "0.8.1",
689 - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
690 - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
691 - "dev": true,
692 - "dependencies": {
693 - "@jridgewell/trace-mapping": "0.3.9"
694 - },
695 - "engines": {
696 - "node": ">=12"
697 - }
698 - },
699 - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
700 - "version": "0.3.9",
701 - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
702 - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
703 - "dev": true,
704 - "dependencies": {
705 - "@jridgewell/resolve-uri": "^3.0.3",
706 - "@jridgewell/sourcemap-codec": "^1.4.10"
707 - }
708 - },
709 - "node_modules/@css-render/plugin-bem": {
710 - "version": "0.15.12",
711 - "resolved": "https://registry.npmjs.org/@css-render/plugin-bem/-/plugin-bem-0.15.12.tgz",
712 - "integrity": "sha512-Lq2jSOZn+wYQtsyaFj6QRz2EzAnd3iW5fZeHO1WSXQdVYwvwGX0ZiH3X2JQgtgYLT1yeGtrwrqJdNdMEUD2xTw==",
713 - "peerDependencies": {
714 - "css-render": "~0.15.12"
715 - }
716 - },
717 - "node_modules/@css-render/vue3-ssr": {
718 - "version": "0.15.12",
719 - "resolved": "https://registry.npmjs.org/@css-render/vue3-ssr/-/vue3-ssr-0.15.12.tgz",
720 - "integrity": "sha512-AQLGhhaE0F+rwybRCkKUdzBdTEM/5PZBYy+fSYe1T9z9+yxMuV/k7ZRqa4M69X+EI1W8pa4kc9Iq2VjQkZx4rg==",
721 - "peerDependencies": {
722 - "vue": "^3.0.11"
723 - }
724 - },
725 - "node_modules/@cypress/request": {
726 - "version": "3.0.1",
727 - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz",
728 - "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==",
729 - "dev": true,
730 - "dependencies": {
731 - "aws-sign2": "~0.7.0",
732 - "aws4": "^1.8.0",
733 - "caseless": "~0.12.0",
734 - "combined-stream": "~1.0.6",
735 - "extend": "~3.0.2",
736 - "forever-agent": "~0.6.1",
737 - "form-data": "~2.3.2",
738 - "http-signature": "~1.3.6",
739 - "is-typedarray": "~1.0.0",
740 - "isstream": "~0.1.2",
741 - "json-stringify-safe": "~5.0.1",
742 - "mime-types": "~2.1.19",
743 - "performance-now": "^2.1.0",
744 - "qs": "6.10.4",
745 - "safe-buffer": "^5.1.2",
746 - "tough-cookie": "^4.1.3",
747 - "tunnel-agent": "^0.6.0",
748 - "uuid": "^8.3.2"
749 - },
750 - "engines": {
751 - "node": ">= 6"
752 - }
753 - },
754 - "node_modules/@cypress/xvfb": {
755 - "version": "1.2.4",
756 - "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz",
757 - "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==",
758 - "dev": true,
759 - "dependencies": {
760 - "debug": "^3.1.0",
761 - "lodash.once": "^4.1.1"
762 - }
763 - },
764 - "node_modules/@cypress/xvfb/node_modules/debug": {
765 - "version": "3.2.7",
766 - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
767 - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
768 - "dev": true,
769 - "dependencies": {
770 - "ms": "^2.1.1"
771 - }
772 - },
773 - "node_modules/@emotion/hash": {
774 - "version": "0.8.0",
775 - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
776 - "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="
777 - },
778 - "node_modules/@esbuild/aix-ppc64": {
779 - "version": "0.19.12",
780 - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
781 - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
782 - "cpu": [
783 - "ppc64"
784 - ],
785 - "dev": true,
786 - "optional": true,
787 - "os": [
788 - "aix"
789 - ],
790 - "engines": {
791 - "node": ">=12"
792 - }
793 - },
794 - "node_modules/@esbuild/android-arm": {
795 - "version": "0.19.12",
796 - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
797 - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
798 - "cpu": [
799 - "arm"
800 - ],
801 - "dev": true,
802 - "optional": true,
803 - "os": [
804 - "android"
805 - ],
806 - "engines": {
807 - "node": ">=12"
808 - }
809 - },
810 - "node_modules/@esbuild/android-arm64": {
811 - "version": "0.19.12",
812 - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
813 - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
814 - "cpu": [
815 - "arm64"
816 - ],
817 - "dev": true,
818 - "optional": true,
819 - "os": [
820 - "android"
821 - ],
822 - "engines": {
823 - "node": ">=12"
824 - }
825 - },
826 - "node_modules/@esbuild/android-x64": {
827 - "version": "0.19.12",
828 - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
829 - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
830 - "cpu": [
831 - "x64"
832 - ],
833 - "dev": true,
834 - "optional": true,
835 - "os": [
836 - "android"
837 - ],
838 - "engines": {
839 - "node": ">=12"
840 - }
841 - },
842 - "node_modules/@esbuild/darwin-arm64": {
843 - "version": "0.19.12",
844 - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
845 - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
846 - "cpu": [
847 - "arm64"
848 - ],
849 - "dev": true,
850 - "optional": true,
851 - "os": [
852 - "darwin"
853 - ],
854 - "engines": {
855 - "node": ">=12"
856 - }
857 - },
858 - "node_modules/@esbuild/darwin-x64": {
859 - "version": "0.19.12",
860 - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
861 - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
862 - "cpu": [
863 - "x64"
864 - ],
865 - "dev": true,
866 - "optional": true,
867 - "os": [
868 - "darwin"
869 - ],
870 - "engines": {
871 - "node": ">=12"
872 - }
873 - },
874 - "node_modules/@esbuild/freebsd-arm64": {
875 - "version": "0.19.12",
876 - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
877 - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
878 - "cpu": [
879 - "arm64"
880 - ],
881 - "dev": true,
882 - "optional": true,
883 - "os": [
884 - "freebsd"
885 - ],
886 - "engines": {
887 - "node": ">=12"
888 - }
889 - },
890 - "node_modules/@esbuild/freebsd-x64": {
891 - "version": "0.19.12",
892 - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
893 - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
894 - "cpu": [
895 - "x64"
896 - ],
897 - "dev": true,
898 - "optional": true,
899 - "os": [
900 - "freebsd"
901 - ],
902 - "engines": {
903 - "node": ">=12"
904 - }
905 - },
906 - "node_modules/@esbuild/linux-arm": {
907 - "version": "0.19.12",
908 - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
909 - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
910 - "cpu": [
911 - "arm"
912 - ],
913 - "dev": true,
914 - "optional": true,
915 - "os": [
916 - "linux"
917 - ],
918 - "engines": {
919 - "node": ">=12"
920 - }
921 - },
922 - "node_modules/@esbuild/linux-arm64": {
923 - "version": "0.19.12",
924 - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
925 - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
926 - "cpu": [
927 - "arm64"
928 - ],
929 - "dev": true,
930 - "optional": true,
931 - "os": [
932 - "linux"
933 - ],
934 - "engines": {
935 - "node": ">=12"
936 - }
937 - },
938 - "node_modules/@esbuild/linux-ia32": {
939 - "version": "0.19.12",
940 - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
941 - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
942 - "cpu": [
943 - "ia32"
944 - ],
945 - "dev": true,
946 - "optional": true,
947 - "os": [
948 - "linux"
949 - ],
950 - "engines": {
951 - "node": ">=12"
952 - }
953 - },
954 - "node_modules/@esbuild/linux-loong64": {
955 - "version": "0.19.12",
956 - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
957 - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
958 - "cpu": [
959 - "loong64"
960 - ],
961 - "dev": true,
962 - "optional": true,
963 - "os": [
964 - "linux"
965 - ],
966 - "engines": {
967 - "node": ">=12"
968 - }
969 - },
970 - "node_modules/@esbuild/linux-mips64el": {
971 - "version": "0.19.12",
972 - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
973 - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
974 - "cpu": [
975 - "mips64el"
976 - ],
977 - "dev": true,
978 - "optional": true,
979 - "os": [
980 - "linux"
981 - ],
982 - "engines": {
983 - "node": ">=12"
984 - }
985 - },
986 - "node_modules/@esbuild/linux-ppc64": {
987 - "version": "0.19.12",
988 - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
989 - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
990 - "cpu": [
991 - "ppc64"
992 - ],
993 - "dev": true,
994 - "optional": true,
995 - "os": [
996 - "linux"
997 - ],
998 - "engines": {
999 - "node": ">=12"
1000 - }
1001 - },
1002 - "node_modules/@esbuild/linux-riscv64": {
1003 - "version": "0.19.12",
1004 - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
1005 - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
1006 - "cpu": [
1007 - "riscv64"
1008 - ],
1009 - "dev": true,
1010 - "optional": true,
1011 - "os": [
1012 - "linux"
1013 - ],
1014 - "engines": {
1015 - "node": ">=12"
1016 - }
1017 - },
1018 - "node_modules/@esbuild/linux-s390x": {
1019 - "version": "0.19.12",
1020 - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
1021 - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
1022 - "cpu": [
1023 - "s390x"
1024 - ],
1025 - "dev": true,
1026 - "optional": true,
1027 - "os": [
1028 - "linux"
1029 - ],
1030 - "engines": {
1031 - "node": ">=12"
1032 - }
1033 - },
1034 - "node_modules/@esbuild/linux-x64": {
1035 - "version": "0.19.12",
1036 - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
1037 - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
1038 - "cpu": [
1039 - "x64"
1040 - ],
1041 - "dev": true,
1042 - "optional": true,
1043 - "os": [
1044 - "linux"
1045 - ],
1046 - "engines": {
1047 - "node": ">=12"
1048 - }
1049 - },
1050 - "node_modules/@esbuild/netbsd-x64": {
1051 - "version": "0.19.12",
1052 - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
1053 - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
1054 - "cpu": [
1055 - "x64"
1056 - ],
1057 - "dev": true,
1058 - "optional": true,
1059 - "os": [
1060 - "netbsd"
1061 - ],
1062 - "engines": {
1063 - "node": ">=12"
1064 - }
1065 - },
1066 - "node_modules/@esbuild/openbsd-x64": {
1067 - "version": "0.19.12",
1068 - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
1069 - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
1070 - "cpu": [
1071 - "x64"
1072 - ],
1073 - "dev": true,
1074 - "optional": true,
1075 - "os": [
1076 - "openbsd"
1077 - ],
1078 - "engines": {
1079 - "node": ">=12"
1080 - }
1081 - },
1082 - "node_modules/@esbuild/sunos-x64": {
1083 - "version": "0.19.12",
1084 - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
1085 - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
1086 - "cpu": [
1087 - "x64"
1088 - ],
1089 - "dev": true,
1090 - "optional": true,
1091 - "os": [
1092 - "sunos"
1093 - ],
1094 - "engines": {
1095 - "node": ">=12"
1096 - }
1097 - },
1098 - "node_modules/@esbuild/win32-arm64": {
1099 - "version": "0.19.12",
1100 - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
1101 - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
1102 - "cpu": [
1103 - "arm64"
1104 - ],
1105 - "dev": true,
1106 - "optional": true,
1107 - "os": [
1108 - "win32"
1109 - ],
1110 - "engines": {
1111 - "node": ">=12"
1112 - }
1113 - },
1114 - "node_modules/@esbuild/win32-ia32": {
1115 - "version": "0.19.12",
1116 - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
1117 - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
1118 - "cpu": [
1119 - "ia32"
1120 - ],
1121 - "dev": true,
1122 - "optional": true,
1123 - "os": [
1124 - "win32"
1125 - ],
1126 - "engines": {
1127 - "node": ">=12"
1128 - }
1129 - },
1130 - "node_modules/@esbuild/win32-x64": {
1131 - "version": "0.19.12",
1132 - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
1133 - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
1134 - "cpu": [
1135 - "x64"
1136 - ],
1137 - "dev": true,
1138 - "optional": true,
1139 - "os": [
1140 - "win32"
1141 - ],
1142 - "engines": {
1143 - "node": ">=12"
1144 - }
1145 - },
1146 - "node_modules/@eslint-community/eslint-utils": {
1147 - "version": "4.4.0",
1148 - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
1149 - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
1150 - "dependencies": {
1151 - "eslint-visitor-keys": "^3.3.0"
1152 - },
1153 - "engines": {
1154 - "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
1155 - },
1156 - "peerDependencies": {
1157 - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
1158 - }
1159 - },
1160 - "node_modules/@eslint-community/regexpp": {
1161 - "version": "4.10.0",
1162 - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
1163 - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
1164 - "engines": {
1165 - "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
1166 - }
1167 - },
1168 - "node_modules/@eslint/eslintrc": {
1169 - "version": "2.1.4",
1170 - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
1171 - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
1172 - "dependencies": {
1173 - "ajv": "^6.12.4",
1174 - "debug": "^4.3.2",
1175 - "espree": "^9.6.0",
1176 - "globals": "^13.19.0",
1177 - "ignore": "^5.2.0",
1178 - "import-fresh": "^3.2.1",
1179 - "js-yaml": "^4.1.0",
1180 - "minimatch": "^3.1.2",
1181 - "strip-json-comments": "^3.1.1"
1182 - },
1183 - "engines": {
1184 - "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
1185 - },
1186 - "funding": {
1187 - "url": "https://opencollective.com/eslint"
1188 - }
1189 - },
1190 - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
1191 - "version": "1.1.11",
1192 - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
1193 - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
1194 - "dependencies": {
1195 - "balanced-match": "^1.0.0",
1196 - "concat-map": "0.0.1"
1197 - }
1198 - },
1199 - "node_modules/@eslint/eslintrc/node_modules/minimatch": {
1200 - "version": "3.1.2",
1201 - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
1202 - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
1203 - "dependencies": {
1204 - "brace-expansion": "^1.1.7"
1205 - },
1206 - "engines": {
1207 - "node": "*"
1208 - }
1209 - },
1210 - "node_modules/@eslint/js": {
1211 - "version": "8.56.0",
1212 - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
1213 - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
1214 - "engines": {
1215 - "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
1216 - }
1217 - },
1218 - "node_modules/@f3ve/eslint-config": {
1219 - "version": "1.0.6",
1220 - "resolved": "https://registry.npmjs.org/@f3ve/eslint-config/-/eslint-config-1.0.6.tgz",
1221 - "integrity": "sha512-DVLPAVZskaOUY6Gu/1VFaL0gGyY3VhjZBVExZVde2siwwgdwt+8XDSupLll0arKqVv4bqO2fRK21QomN3jy2yg==",
1222 - "funding": [
1223 - {
1224 - "type": "GitHub Sponsors",
1225 - "url": "https://github.com/sponsors/f3ve"
1226 - },
1227 - {
1228 - "type": "Buy me a Coffee",
1229 - "url": "https://www.buymeacoffee.com/f3ve"
1230 - }
1231 - ],
1232 - "dependencies": {
1233 - "@eslint/js": "^8.50.0",
1234 - "@typescript-eslint/eslint-plugin": "^6.7.3",
1235 - "@typescript-eslint/parser": "^6.7.3",
1236 - "@unocss/eslint-config": "^0.56.4",
1237 - "eslint-config-prettier": "^9.0.0",
1238 - "eslint-define-config": "^1.23.0",
1239 - "eslint-plugin-prettier": "^5.0.0",
1240 - "eslint-plugin-vue": "^9.17.0",
1241 - "globals": "^13.22.0",
1242 - "vue-eslint-parser": "^9.3.1"
1243 - },
1244 - "peerDependencies": {
1245 - "eslint": ">=8.0.0"
1246 - }
1247 - },
1248 - "node_modules/@f3ve/vue-markdown-it": {
1249 - "version": "0.2.0",
1250 - "resolved": "https://registry.npmjs.org/@f3ve/vue-markdown-it/-/vue-markdown-it-0.2.0.tgz",
1251 - "integrity": "sha512-ILDL0vfhl+Z9FxRm+iqZBrgpMpRc0SVfdE5/YEqCVX3VAz7fWIwJc6zmM2sUGtV6CaLY2fSJzX94v1znd7O24Q==",
1252 - "dependencies": {
1253 - "@f3ve/eslint-config": "^1.0.3",
1254 - "markdown-it": "^13.0.2"
1255 - },
1256 - "peerDependencies": {
1257 - "vue": "^3.3.4"
1258 - }
1259 - },
1260 - "node_modules/@fontsource/jetbrains-mono": {
1261 - "version": "5.0.18",
1262 - "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.0.18.tgz",
1263 - "integrity": "sha512-0+YDAaAnCdXjirHFO3NfHLUW8xtKCT5rlm23Q0qG3TpZp3QBrZh5r9ikUxh3ufHc2+fVnk4Y6GWNEnVIfB7u/g=="
1264 - },
1265 - "node_modules/@fontsource/lexend": {
1266 - "version": "5.0.18",
1267 - "resolved": "https://registry.npmjs.org/@fontsource/lexend/-/lexend-5.0.18.tgz",
1268 - "integrity": "sha512-RcNekPIeQGX5ZvwRtX7UHDoDrGTg8IV2Yae13qjtxW6FO4kFaUKSlITKnrvaK8r8ly/fQ6x2mXva9jmMZPZ4Ug=="
1269 - },
1270 - "node_modules/@fontsource/public-sans": {
1271 - "version": "5.0.16",
1272 - "resolved": "https://registry.npmjs.org/@fontsource/public-sans/-/public-sans-5.0.16.tgz",
1273 - "integrity": "sha512-bThZip6sLRsnfzi/oBr1/9+aWmvHkb59QlLh/OtoAIA0Mi2+Z1cKOxtR1B2ITauljlLJHPRvjpTZDUZitUN8pA=="
1274 - },
1275 - "node_modules/@hapi/hoek": {
1276 - "version": "9.3.0",
1277 - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
1278 - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
1279 - "dev": true
1280 - },
1281 - "node_modules/@hapi/topo": {
1282 - "version": "5.1.0",
1283 - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
1284 - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
1285 - "dev": true,
1286 - "dependencies": {
1287 - "@hapi/hoek": "^9.0.0"
1288 - }
1289 - },
1290 - "node_modules/@humanwhocodes/config-array": {
1291 - "version": "0.11.14",
1292 - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
1293 - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
1294 - "dependencies": {
1295 - "@humanwhocodes/object-schema": "^2.0.2",
1296 - "debug": "^4.3.1",
1297 - "minimatch": "^3.0.5"
1298 - },
1299 - "engines": {
1300 - "node": ">=10.10.0"
1301 - }
1302 - },
1303 - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
1304 - "version": "1.1.11",
1305 - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
1306 - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
1307 - "dependencies": {
1308 - "balanced-match": "^1.0.0",
1309 - "concat-map": "0.0.1"
1310 - }
1311 - },
1312 - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
1313 - "version": "3.1.2",
1314 - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
1315 - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
1316 - "dependencies": {
1317 - "brace-expansion": "^1.1.7"
1318 - },
1319 - "engines": {
1320 - "node": "*"
1321 - }
1322 - },
1323 - "node_modules/@humanwhocodes/module-importer": {
1324 - "version": "1.0.1",
1325 - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
1326 - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
1327 - "engines": {
1328 - "node": ">=12.22"
1329 - },
1330 - "funding": {
1331 - "type": "github",
1332 - "url": "https://github.com/sponsors/nzakas"
1333 - }
1334 - },
1335 - "node_modules/@humanwhocodes/object-schema": {
1336 - "version": "2.0.2",
1337 - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
1338 - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw=="
1339 - },
1340 - "node_modules/@iconify/types": {
1341 - "version": "2.0.0",
1342 - "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
1343 - "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
1344 - "dev": true
1345 - },
1346 - "node_modules/@iconify/vue": {
1347 - "version": "4.1.1",
1348 - "resolved": "https://registry.npmjs.org/@iconify/vue/-/vue-4.1.1.tgz",
1349 - "integrity": "sha512-RL85Bm/DAe8y6rT6pux7D2FJSiUEM/TPfyK7GrbAOfTSwrhvwJW+S5yijdGcmtXouA8MtuH9C7l4hiSE4mLMjg==",
1350 - "dev": true,
1351 - "dependencies": {
1352 - "@iconify/types": "^2.0.0"
1353 - },
1354 - "funding": {
1355 - "url": "https://github.com/sponsors/cyberalien"
1356 - },
1357 - "peerDependencies": {
1358 - "vue": ">=3"
1359 - }
1360 - },
1361 - "node_modules/@intlify/core-base": {
1362 - "version": "9.9.1",
1363 - "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.9.1.tgz",
1364 - "integrity": "sha512-qsV15dg7jNX2faBRyKMgZS8UcFJViWEUPLdzZ9UR0kQZpFVeIpc0AG7ZOfeP7pX2T9SQ5jSiorq/tii9nkkafA==",
1365 - "dependencies": {
1366 - "@intlify/message-compiler": "9.9.1",
1367 - "@intlify/shared": "9.9.1"
1368 - },
1369 - "engines": {
1370 - "node": ">= 16"
1371 - },
1372 - "funding": {
1373 - "url": "https://github.com/sponsors/kazupon"
1374 - }
1375 - },
1376 - "node_modules/@intlify/message-compiler": {
1377 - "version": "9.9.1",
1378 - "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.9.1.tgz",
1379 - "integrity": "sha512-zTvP6X6HeumHOXuAE1CMMsV6tTX+opKMOxO1OHTCg5N5Sm/F7d8o2jdT6W6L5oHUsJ/vvkGefHIs7Q3hfowmsA==",
1380 - "dependencies": {
1381 - "@intlify/shared": "9.9.1",
1382 - "source-map-js": "^1.0.2"
1383 - },
1384 - "engines": {
1385 - "node": ">= 16"
1386 - },
1387 - "funding": {
1388 - "url": "https://github.com/sponsors/kazupon"
1389 - }
1390 - },
1391 - "node_modules/@intlify/shared": {
1392 - "version": "9.9.1",
1393 - "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.9.1.tgz",
1394 - "integrity": "sha512-b3Pta1nwkz5rGq434v0psHwEwHGy1pYCttfcM22IE//K9owbpkEvFptx9VcuRAxjQdrO2If249cmDDjBu5wMDA==",
1395 - "engines": {
1396 - "node": ">= 16"
1397 - },
1398 - "funding": {
1399 - "url": "https://github.com/sponsors/kazupon"
1400 - }
1401 - },
1402 - "node_modules/@isaacs/cliui": {
1403 - "version": "8.0.2",
1404 - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
1405 - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
1406 - "dev": true,
1407 - "dependencies": {
1408 - "string-width": "^5.1.2",
1409 - "string-width-cjs": "npm:string-width@^4.2.0",
1410 - "strip-ansi": "^7.0.1",
1411 - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
1412 - "wrap-ansi": "^8.1.0",
1413 - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
1414 - },
1415 - "engines": {
1416 - "node": ">=12"
1417 - }
1418 - },
1419 - "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
1420 - "version": "6.0.1",
1421 - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
1422 - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
1423 - "dev": true,
1424 - "engines": {
1425 - "node": ">=12"
1426 - },
1427 - "funding": {
1428 - "url": "https://github.com/chalk/ansi-regex?sponsor=1"
1429 - }
1430 - },
1431 - "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
1432 - "version": "9.2.2",
1433 - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
1434 - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
1435 - "dev": true
1436 - },
1437 - "node_modules/@isaacs/cliui/node_modules/string-width": {
1438 - "version": "5.1.2",
1439 - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
1440 - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
1441 - "dev": true,
1442 - "dependencies": {
1443 - "eastasianwidth": "^0.2.0",
1444 - "emoji-regex": "^9.2.2",
1445 - "strip-ansi": "^7.0.1"
1446 - },
1447 - "engines": {
1448 - "node": ">=12"
1449 - },
1450 - "funding": {
1451 - "url": "https://github.com/sponsors/sindresorhus"
1452 - }
1453 - },
1454 - "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
1455 - "version": "7.1.0",
1456 - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
1457 - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
1458 - "dev": true,
1459 - "dependencies": {
1460 - "ansi-regex": "^6.0.1"
1461 - },
1462 - "engines": {
1463 - "node": ">=12"
1464 - },
1465 - "funding": {
1466 - "url": "https://github.com/chalk/strip-ansi?sponsor=1"
1467 - }
1468 - },
1469 - "node_modules/@jest/schemas": {
1470 - "version": "29.6.3",
1471 - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
1472 - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
1473 - "dev": true,
1474 - "dependencies": {
1475 - "@sinclair/typebox": "^0.27.8"
1476 - },
1477 - "engines": {
1478 - "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
1479 - }
1480 - },
1481 - "node_modules/@jridgewell/gen-mapping": {
1482 - "version": "0.3.3",
1483 - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
1484 - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
1485 - "dev": true,
1486 - "dependencies": {
1487 - "@jridgewell/set-array": "^1.0.1",
1488 - "@jridgewell/sourcemap-codec": "^1.4.10",
1489 - "@jridgewell/trace-mapping": "^0.3.9"
1490 - },
1491 - "engines": {
1492 - "node": ">=6.0.0"
1493 - }
1494 - },
1495 - "node_modules/@jridgewell/resolve-uri": {
1496 - "version": "3.1.1",
1497 - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
1498 - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
1499 - "dev": true,
1500 - "engines": {
1501 - "node": ">=6.0.0"
1502 - }
1503 - },
1504 - "node_modules/@jridgewell/set-array": {
1505 - "version": "1.1.2",
1506 - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
1507 - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
1508 - "dev": true,
1509 - "engines": {
1510 - "node": ">=6.0.0"
1511 - }
1512 - },
1513 - "node_modules/@jridgewell/sourcemap-codec": {
1514 - "version": "1.4.15",
1515 - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
1516 - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
1517 - },
1518 - "node_modules/@jridgewell/trace-mapping": {
1519 - "version": "0.3.22",
1520 - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz",
1521 - "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==",
1522 - "dev": true,
1523 - "dependencies": {
1524 - "@jridgewell/resolve-uri": "^3.1.0",
1525 - "@jridgewell/sourcemap-codec": "^1.4.14"
1526 - }
1527 - },
1528 - "node_modules/@juggle/resize-observer": {
1529 - "version": "3.4.0",
1530 - "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
1531 - "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
1532 - },
1533 - "node_modules/@koa/router": {
1534 - "version": "12.0.1",
1535 - "resolved": "https://registry.npmjs.org/@koa/router/-/router-12.0.1.tgz",
1536 - "integrity": "sha512-ribfPYfHb+Uw3b27Eiw6NPqjhIhTpVFzEWLwyc/1Xp+DCdwRRyIlAUODX+9bPARF6aQtUu1+/PHzdNvRzcs/+Q==",
1537 - "dev": true,
1538 - "dependencies": {
1539 - "debug": "^4.3.4",
1540 - "http-errors": "^2.0.0",
1541 - "koa-compose": "^4.1.0",
1542 - "methods": "^1.1.2",
1543 - "path-to-regexp": "^6.2.1"
1544 - },
1545 - "engines": {
1546 - "node": ">= 12"
1547 - }
1548 - },
1549 - "node_modules/@nodelib/fs.scandir": {
1550 - "version": "2.1.5",
1551 - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
1552 - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
1553 - "dependencies": {
1554 - "@nodelib/fs.stat": "2.0.5",
1555 - "run-parallel": "^1.1.9"
1556 - },
1557 - "engines": {
1558 - "node": ">= 8"
1559 - }
1560 - },
1561 - "node_modules/@nodelib/fs.stat": {
1562 - "version": "2.0.5",
1563 - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
1564 - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
1565 - "engines": {
1566 - "node": ">= 8"
1567 - }
1568 - },
1569 - "node_modules/@nodelib/fs.walk": {
1570 - "version": "1.2.8",
1571 - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
1572 - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
1573 - "dependencies": {
1574 - "@nodelib/fs.scandir": "2.1.5",
1575 - "fastq": "^1.6.0"
1576 - },
1577 - "engines": {
1578 - "node": ">= 8"
1579 - }
1580 - },
1581 - "node_modules/@npmcli/agent": {
1582 - "version": "2.2.0",
1583 - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.0.tgz",
1584 - "integrity": "sha512-2yThA1Es98orMkpSLVqlDZAMPK3jHJhifP2gnNUdk1754uZ8yI5c+ulCoVG+WlntQA6MzhrURMXjSd9Z7dJ2/Q==",
1585 - "dev": true,
1586 - "dependencies": {
1587 - "agent-base": "^7.1.0",
1588 - "http-proxy-agent": "^7.0.0",
1589 - "https-proxy-agent": "^7.0.1",
1590 - "lru-cache": "^10.0.1",
1591 - "socks-proxy-agent": "^8.0.1"
1592 - },
1593 - "engines": {
1594 - "node": "^16.14.0 || >=18.0.0"
1595 - }
1596 - },
1597 - "node_modules/@npmcli/agent/node_modules/lru-cache": {
1598 - "version": "10.2.0",
1599 - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
1600 - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==",
1601 - "dev": true,
1602 - "engines": {
1603 - "node": "14 || >=16.14"
1604 - }
1605 - },
1606 - "node_modules/@npmcli/config": {
1607 - "version": "8.1.0",
1608 - "resolved": "https://registry.npmjs.org/@npmcli/config/-/config-8.1.0.tgz",
1609 - "integrity": "sha512-61LNEybTFaa9Z/f8y6X9s2Blc75aijZK67LxqC5xicBcfkw8M/88nYrRXGXxAUKm6GRlxTZ216dp1UK2+TbaYw==",
1610 - "dev": true,
1611 - "dependencies": {
1612 - "@npmcli/map-workspaces": "^3.0.2",
1613 - "ci-info": "^4.0.0",
1614 - "ini": "^4.1.0",
1615 - "nopt": "^7.0.0",
1616 - "proc-log": "^3.0.0",
1617 - "read-package-json-fast": "^3.0.2",
1618 - "semver": "^7.3.5",
1619 - "walk-up-path": "^3.0.1"
1620 - },
1621 - "engines": {
1622 - "node": "^16.14.0 || >=18.0.0"
1623 - }
1624 - },
1625 - "node_modules/@npmcli/config/node_modules/ci-info": {
1626 - "version": "4.0.0",
1627 - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz",
1628 - "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==",
1629 - "dev": true,
1630 - "funding": [
1631 - {
1632 - "type": "github",
1633 - "url": "https://github.com/sponsors/sibiraj-s"
1634 - }
1635 - ],
1636 - "engines": {
1637 - "node": ">=8"
1638 - }
1639 - },
1640 - "node_modules/@npmcli/config/node_modules/ini": {
1641 - "version": "4.1.1",
1642 - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz",
1643 - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==",
1644 - "dev": true,
1645 - "engines": {
1646 - "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
1647 - }
1648 - },
1649 - "node_modules/@npmcli/fs": {
1650 - "version": "3.1.0",
1651 - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz",
1652 - "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==",
1653 - "dev": true,
1654 - "dependencies": {
1655 - "semver": "^7.3.5"
1656 - },
1657 - "engines": {
1658 - "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
1659 - }
1660 - },
1661 - "node_modules/@npmcli/git": {
1662 - "version": "5.0.4",
1663 - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.4.tgz",
1664 - "integrity": "sha512-nr6/WezNzuYUppzXRaYu/W4aT5rLxdXqEFupbh6e/ovlYFQ8hpu1UUPV3Ir/YTl+74iXl2ZOMlGzudh9ZPUchQ==",
1665 - "dev": true,
1666 - "dependencies": {
1667 - "@npmcli/promise-spawn": "^7.0.0",
1668 - "lru-cache": "^10.0.1",
1669 - "npm-pick-manifest": "^9.0.0",
1670 - "proc-log": "^3.0.0",
1671 - "promise-inflight": "^1.0.1",
1672 - "promise-retry": "^2.0.1",
1673 - "semver": "^7.3.5",
1674 - "which": "^4.0.0"
1675 - },
1676 - "engines": {
1677 - "node": "^16.14.0 || >=18.0.0"
1678 - }
1679 - },
1680 - "node_modules/@npmcli/git/node_modules/isexe": {
1681 - "version": "3.1.1",
1682 - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
1683 - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
1684 - "dev": true,
1685 - "engines": {
1686 - "node": ">=16"
1687 - }
1688 - },
1689 - "node_modules/@npmcli/git/node_modules/lru-cache": {
1690 - "version": "10.2.0",
1691 - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
1692 - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==",
1693 - "dev": true,
1694 - "engines": {
1695 - "node": "14 || >=16.14"
1696 - }
1697 - },
1698 - "node_modules/@npmcli/git/node_modules/which": {
1699 - "version": "4.0.0",
1700 - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
1701 - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
1702 - "dev": true,
1703 - "dependencies": {
1704 - "isexe": "^3.1.1"
1705 - },
1706 - "bin": {
1707 - "node-which": "bin/which.js"
1708 - },
1709 - "engines": {
1710 - "node": "^16.13.0 || >=18.0.0"
1711 - }
1712 - },
1713 - "node_modules/@npmcli/installed-package-contents": {
1714 - "version": "2.0.2",
1715 - "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.0.2.tgz",
1716 - "integrity": "sha512-xACzLPhnfD51GKvTOOuNX2/V4G4mz9/1I2MfDoye9kBM3RYe5g2YbscsaGoTlaWqkxeiapBWyseULVKpSVHtKQ==",
1717 - "dev": true,
1718 - "dependencies": {
1719 - "npm-bundled": "^3.0.0",
1720 - "npm-normalize-package-bin": "^3.0.0"
1721 - },
1722 - "bin": {
1723 - "installed-package-contents": "lib/index.js"
1724 - },
1725 - "engines": {
1726 - "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
1727 - }
1728 - },
1729 - "node_modules/@npmcli/map-workspaces": {
1730 - "version": "3.0.4",
1731 - "resolved": "https://registry.npmjs.org/@npmcli/map-workspaces/-/map-workspaces-3.0.4.tgz",
1732 - "integrity": "sha512-Z0TbvXkRbacjFFLpVpV0e2mheCh+WzQpcqL+4xp49uNJOxOnIAPZyXtUxZ5Qn3QBTGKA11Exjd9a5411rBrhDg==",
1733 - "dev": true,
1734 - "dependencies": {
1735 - "@npmcli/name-from-folder": "^2.0.0",
1736 - "glob": "^10.2.2",
1737 - "minimatch": "^9.0.0",
1738 - "read-package-json-fast": "^3.0.0"
1739 - },
1740 - "engines": {
1741 - "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
1742 - }
1743 - },
1744 - "node_modules/@npmcli/name-from-folder": {
1745 - "version": "2.0.0",
1746 - "resolved": "https://registry.npmjs.org/@npmcli/name-from-folder/-/name-from-folder-2.0.0.tgz",
1747 - "integrity": "sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg==",
1748 - "dev": true,
1749 - "engines": {
1750 - "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
1751 - }
1752 - },
1753 - "node_modules/@npmcli/node-gyp": {
1754 - "version": "3.0.0",
1755 - "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz",
1756 - "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==",
1757 - "dev": true,
1758 - "engines": {
1759 - "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
1760 - }
1761 - },
1762 - "node_modules/@npmcli/package-json": {
1763 - "version": "5.0.0",
1764 - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.0.0.tgz",
1765 - "integrity": "sha512-OI2zdYBLhQ7kpNPaJxiflofYIpkNLi+lnGdzqUOfRmCF3r2l1nadcjtCYMJKv/Utm/ZtlffaUuTiAktPHbc17g==",
1766 - "dev": true,
1767 - "dependencies": {
1768 - "@npmcli/git": "^5.0.0",
1769 - "glob": "^10.2.2",
1770 - "hosted-git-info": "^7.0.0",
1771 - "json-parse-even-better-errors": "^3.0.0",
1772 - "normalize-package-data": "^6.0.0",
1773 - "proc-log": "^3.0.0",
1774 - "semver": "^7.5.3"
1775 - },
1776 - "engines": {
1777 - "node": "^16.14.0 || >=18.0.0"
1778 - }
1779 - },
1780 - "node_modules/@npmcli/package-json/node_modules/hosted-git-info": {
1781 - "version": "7.0.1",
1782 - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz",
1783 - "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==",
1784 - "dev": true,
1785 - "dependencies": {
1786 - "lru-cache": "^10.0.1"
1787 - },
1788 - "engines": {
1789 - "node": "^16.14.0 || >=18.0.0"
1790 - }
1791 - },
1792 - "node_modules/@npmcli/package-json/node_modules/lru-cache": {
1793 - "version": "10.2.0",
1794 - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
1795 - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==",
1796 - "dev": true,
1797 - "engines": {
1798 - "node": "14 || >=16.14"
1799 - }
1800 - },
1801 - "node_modules/@npmcli/package-json/node_modules/normalize-package-data": {
1802 - "version": "6.0.0",
1803 - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.0.tgz",
1804 - "integrity": "sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==",
1805 - "dev": true,
1806 - "dependencies": {
1807 - "hosted-git-info": "^7.0.0",
1808 - "is-core-module": "^2.8.1",
1809 - "semver": "^7.3.5",
1810 - "validate-npm-package-license": "^3.0.4"
1811 - },
1812 - "engines": {
1813 - "node": "^16.14.0 || >=18.0.0"
1814 - }
1815 - },
1816 - "node_modules/@npmcli/promise-spawn": {
1817 - "version": "7.0.1",
1818 - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.1.tgz",
1819 - "integrity": "sha512-P4KkF9jX3y+7yFUxgcUdDtLy+t4OlDGuEBLNs57AZsfSfg+uV6MLndqGpnl4831ggaEdXwR50XFoZP4VFtHolg==",
1820 - "dev": true,
1821 - "dependencies": {
1822 - "which": "^4.0.0"
1823 - },
1824 - "engines": {
1825 - "node": "^16.14.0 || >=18.0.0"
1826 - }
1827 - },
1828 - "node_modules/@npmcli/promise-spawn/node_modules/isexe": {
1829 - "version": "3.1.1",
1830 - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
1831 - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
1832 - "dev": true,
1833 - "engines": {
1834 - "node": ">=16"
1835 - }
1836 - },
1837 - "node_modules/@npmcli/promise-spawn/node_modules/which": {
1838 - "version": "4.0.0",
1839 - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
1840 - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
1841 - "dev": true,
1842 - "dependencies": {
1843 - "isexe": "^3.1.1"
1844 - },
1845 - "bin": {
1846 - "node-which": "bin/which.js"
1847 - },
1848 - "engines": {
1849 - "node": "^16.13.0 || >=18.0.0"
1850 - }
1851 - },
1852 - "node_modules/@npmcli/run-script": {
1853 - "version": "7.0.4",
1854 - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.4.tgz",
1855 - "integrity": "sha512-9ApYM/3+rBt9V80aYg6tZfzj3UWdiYyCt7gJUD1VJKvWF5nwKDSICXbYIQbspFTq6TOpbsEtIC0LArB8d9PFmg==",
1856 - "dev": true,
1857 - "dependencies": {
1858 - "@npmcli/node-gyp": "^3.0.0",
1859 - "@npmcli/package-json": "^5.0.0",
1860 - "@npmcli/promise-spawn": "^7.0.0",
1861 - "node-gyp": "^10.0.0",
1862 - "which": "^4.0.0"
1863 - },
1864 - "engines": {
1865 - "node": "^16.14.0 || >=18.0.0"
1866 - }
1867 - },
1868 - "node_modules/@npmcli/run-script/node_modules/isexe": {
1869 - "version": "3.1.1",
1870 - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
1871 - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
1872 - "dev": true,
1873 - "engines": {
1874 - "node": ">=16"
1875 - }
1876 - },
1877 - "node_modules/@npmcli/run-script/node_modules/which": {
1878 - "version": "4.0.0",
1879 - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
1880 - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
1881 - "dev": true,
1882 - "dependencies": {
1883 - "isexe": "^3.1.1"
1884 - },
1885 - "bin": {
1886 - "node-which": "bin/which.js"
1887 - },
1888 - "engines": {
1889 - "node": "^16.13.0 || >=18.0.0"
1890 - }
1891 - },
1892 - "node_modules/@one-ini/wasm": {
1893 - "version": "0.1.1",
1894 - "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
1895 - "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
1896 - "dev": true
1897 - },
1898 - "node_modules/@pkgjs/parseargs": {
1899 - "version": "0.11.0",
1900 - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
1901 - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
1902 - "dev": true,
1903 - "optional": true,
1904 - "engines": {
1905 - "node": ">=14"
1906 - }
1907 - },
1908 - "node_modules/@pkgr/core": {
1909 - "version": "0.1.1",
1910 - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz",
1911 - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==",
1912 - "engines": {
1913 - "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
1914 - },
1915 - "funding": {
1916 - "url": "https://opencollective.com/unts"
1917 - }
1918 - },
1919 - "node_modules/@polka/url": {
1920 - "version": "1.0.0-next.24",
1921 - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz",
1922 - "integrity": "sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==",
1923 - "dev": true
1924 - },
1925 - "node_modules/@popperjs/core": {
1926 - "version": "2.11.8",
1927 - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
1928 - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
1929 - "funding": {
1930 - "type": "opencollective",
1931 - "url": "https://opencollective.com/popperjs"
1932 - }
1933 - },
1934 - "node_modules/@rollup/pluginutils": {
1935 - "version": "5.1.0",
1936 - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
1937 - "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==",
1938 - "dev": true,
1939 - "dependencies": {
1940 - "@types/estree": "^1.0.0",
1941 - "estree-walker": "^2.0.2",
1942 - "picomatch": "^2.3.1"
1943 - },
1944 - "engines": {
1945 - "node": ">=14.0.0"
1946 - },
1947 - "peerDependencies": {
1948 - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
1949 - },
1950 - "peerDependenciesMeta": {
1951 - "rollup": {
1952 - "optional": true
1953 - }
1954 - }
1955 - },
1956 - "node_modules/@rollup/rollup-android-arm-eabi": {
1957 - "version": "4.9.6",
1958 - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz",
1959 - "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==",
1960 - "cpu": [
1961 - "arm"
1962 - ],
1963 - "dev": true,
1964 - "optional": true,
1965 - "os": [
1966 - "android"
1967 - ]
1968 - },
1969 - "node_modules/@rollup/rollup-android-arm64": {
1970 - "version": "4.9.6",
1971 - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz",
1972 - "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==",
1973 - "cpu": [
1974 - "arm64"
1975 - ],
1976 - "dev": true,
1977 - "optional": true,
1978 - "os": [
1979 - "android"
1980 - ]
1981 - },
1982 - "node_modules/@rollup/rollup-darwin-arm64": {
1983 - "version": "4.9.6",
1984 - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz",
1985 - "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==",
1986 - "cpu": [
1987 - "arm64"
1988 - ],
1989 - "dev": true,
1990 - "optional": true,
1991 - "os": [
1992 - "darwin"
1993 - ]
1994 - },
1995 - "node_modules/@rollup/rollup-darwin-x64": {
1996 - "version": "4.9.6",
1997 - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz",
1998 - "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==",
1999 - "cpu": [
2000 - "x64"
2001 - ],
2002 - "dev": true,
2003 - "optional": true,
2004 - "os": [
2005 - "darwin"
2006 - ]
2007 - },
2008 - "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
2009 - "version": "4.9.6",
2010 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz",
2011 - "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==",
2012 - "cpu": [
2013 - "arm"
2014 - ],
2015 - "dev": true,
2016 - "optional": true,
2017 - "os": [
2018 - "linux"
2019 - ]
2020 - },
2021 - "node_modules/@rollup/rollup-linux-arm64-gnu": {
2022 - "version": "4.9.6",
2023 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz",
2024 - "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==",
2025 - "cpu": [
2026 - "arm64"
2027 - ],
2028 - "dev": true,
2029 - "optional": true,
2030 - "os": [
2031 - "linux"
2032 - ]
2033 - },
2034 - "node_modules/@rollup/rollup-linux-arm64-musl": {
2035 - "version": "4.9.6",
2036 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz",
2037 - "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==",
2038 - "cpu": [
2039 - "arm64"
2040 - ],
2041 - "dev": true,
2042 - "optional": true,
2043 - "os": [
2044 - "linux"
2045 - ]
2046 - },
2047 - "node_modules/@rollup/rollup-linux-riscv64-gnu": {
2048 - "version": "4.9.6",
2049 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz",
2050 - "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==",
2051 - "cpu": [
2052 - "riscv64"
2053 - ],
2054 - "dev": true,
2055 - "optional": true,
2056 - "os": [
2057 - "linux"
2058 - ]
2059 - },
2060 - "node_modules/@rollup/rollup-linux-x64-gnu": {
2061 - "version": "4.9.6",
2062 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz",
2063 - "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==",
2064 - "cpu": [
2065 - "x64"
2066 - ],
2067 - "dev": true,
2068 - "optional": true,
2069 - "os": [
2070 - "linux"
2071 - ]
2072 - },
2073 - "node_modules/@rollup/rollup-linux-x64-musl": {
2074 - "version": "4.9.6",
2075 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz",
2076 - "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==",
2077 - "cpu": [
2078 - "x64"
2079 - ],
2080 - "dev": true,
2081 - "optional": true,
2082 - "os": [
2083 - "linux"
2084 - ]
2085 - },
2086 - "node_modules/@rollup/rollup-win32-arm64-msvc": {
2087 - "version": "4.9.6",
2088 - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz",
2089 - "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==",
2090 - "cpu": [
2091 - "arm64"
2092 - ],
2093 - "dev": true,
2094 - "optional": true,
2095 - "os": [
2096 - "win32"
2097 - ]
2098 - },
2099 - "node_modules/@rollup/rollup-win32-ia32-msvc": {
2100 - "version": "4.9.6",
2101 - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz",
2102 - "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==",
2103 - "cpu": [
2104 - "ia32"
2105 - ],
2106 - "dev": true,
2107 - "optional": true,
2108 - "os": [
2109 - "win32"
2110 - ]
2111 - },
2112 - "node_modules/@rollup/rollup-win32-x64-msvc": {
2113 - "version": "4.9.6",
2114 - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz",
2115 - "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==",
2116 - "cpu": [
2117 - "x64"
2118 - ],
2119 - "dev": true,
2120 - "optional": true,
2121 - "os": [
2122 - "win32"
2123 - ]
2124 - },
2125 - "node_modules/@rushstack/eslint-patch": {
2126 - "version": "1.7.2",
2127 - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz",
2128 - "integrity": "sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA==",
2129 - "dev": true
2130 - },
2131 - "node_modules/@sideway/address": {
2132 - "version": "4.1.4",
2133 - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz",
2134 - "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==",
2135 - "dev": true,
2136 - "dependencies": {
2137 - "@hapi/hoek": "^9.0.0"
2138 - }
2139 - },
2140 - "node_modules/@sideway/formula": {
2141 - "version": "3.0.1",
2142 - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
2143 - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
2144 - "dev": true
2145 - },
2146 - "node_modules/@sideway/pinpoint": {
2147 - "version": "2.0.0",
2148 - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
2149 - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
2150 - "dev": true
2151 - },
2152 - "node_modules/@sigstore/bundle": {
2153 - "version": "2.1.1",
2154 - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.1.1.tgz",
2155 - "integrity": "sha512-v3/iS+1nufZdKQ5iAlQKcCsoh0jffQyABvYIxKsZQFWc4ubuGjwZklFHpDgV6O6T7vvV78SW5NHI91HFKEcxKg==",
2156 - "dev": true,
2157 - "dependencies": {
2158 - "@sigstore/protobuf-specs": "^0.2.1"
2159 - },
2160 - "engines": {
2161 - "node": "^16.14.0 || >=18.0.0"
2162 - }
2163 - },
2164 - "node_modules/@sigstore/core": {
2165 - "version": "0.2.0",
2166 - "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-0.2.0.tgz",
2167 - "integrity": "sha512-THobAPPZR9pDH2CAvDLpkrYedt7BlZnsyxDe+Isq4ZmGfPy5juOFZq487vCU2EgKD7aHSiTfE/i7sN7aEdzQnA==",
2168 - "dev": true,
2169 - "engines": {
2170 - "node": "^16.14.0 || >=18.0.0"
2171 - }
2172 - },
2173 - "node_modules/@sigstore/protobuf-specs": {
2174 - "version": "0.2.1",
2175 - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.2.1.tgz",
2176 - "integrity": "sha512-XTWVxnWJu+c1oCshMLwnKvz8ZQJJDVOlciMfgpJBQbThVjKTCG8dwyhgLngBD2KN0ap9F/gOV8rFDEx8uh7R2A==",
2177 - "dev": true,
2178 - "engines": {
2179 - "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
2180 - }
2181 - },
2182 - "node_modules/@sigstore/sign": {
2183 - "version": "2.2.1",
2184 - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.2.1.tgz",
2185 - "integrity": "sha512-U5sKQEj+faE1MsnLou1f4DQQHeFZay+V9s9768lw48J4pKykPj34rWyI1lsMOGJ3Mae47Ye6q3HAJvgXO21rkQ==",
2186 - "dev": true,
2187 - "dependencies": {
2188 - "@sigstore/bundle": "^2.1.1",
2189 - "@sigstore/core": "^0.2.0",
2190 - "@sigstore/protobuf-specs": "^0.2.1",
2191 - "make-fetch-happen": "^13.0.0"
2192 - },
2193 - "engines": {
2194 - "node": "^16.14.0 || >=18.0.0"
2195 - }
2196 - },
2197 - "node_modules/@sigstore/tuf": {
2198 - "version": "2.3.0",
2199 - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.0.tgz",
2200 - "integrity": "sha512-S98jo9cpJwO1mtQ+2zY7bOdcYyfVYCUaofCG6wWRzk3pxKHVAkSfshkfecto2+LKsx7Ovtqbgb2LS8zTRhxJ9Q==",
2201 - "dev": true,
2202 - "dependencies": {
2203 - "@sigstore/protobuf-specs": "^0.2.1",
2204 - "tuf-js": "^2.2.0"
2205 - },
2206 - "engines": {
2207 - "node": "^16.14.0 || >=18.0.0"
2208 - }
2209 - },
2210 - "node_modules/@sigstore/verify": {
2211 - "version": "0.1.0",
2212 - "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-0.1.0.tgz",
2213 - "integrity": "sha512-2UzMNYAa/uaz11NhvgRnIQf4gpLTJ59bhb8ESXaoSS5sxedfS+eLak8bsdMc+qpNQfITUTFoSKFx5h8umlRRiA==",
2214 - "dev": true,
2215 - "dependencies": {
2216 - "@sigstore/bundle": "^2.1.1",
2217 - "@sigstore/core": "^0.2.0",
2218 - "@sigstore/protobuf-specs": "^0.2.1"
2219 - },
2220 - "engines": {
2221 - "node": "^16.14.0 || >=18.0.0"
2222 - }
2223 - },
2224 - "node_modules/@sinclair/typebox": {
2225 - "version": "0.27.8",
2226 - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
2227 - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
2228 - "dev": true
2229 - },
2230 - "node_modules/@trysound/sax": {
2231 - "version": "0.2.0",
2232 - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
2233 - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
2234 - "dev": true,
2235 - "engines": {
2236 - "node": ">=10.13.0"
2237 - }
2238 - },
2239 - "node_modules/@tsconfig/node10": {
2240 - "version": "1.0.9",
2241 - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
2242 - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
2243 - "dev": true
2244 - },
2245 - "node_modules/@tsconfig/node12": {
2246 - "version": "1.0.11",
2247 - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
2248 - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
2249 - "dev": true
2250 - },
2251 - "node_modules/@tsconfig/node14": {
2252 - "version": "1.0.3",
2253 - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
2254 - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
2255 - "dev": true
2256 - },
2257 - "node_modules/@tsconfig/node16": {
2258 - "version": "1.0.4",
2259 - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
2260 - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
2261 - "dev": true
2262 - },
2263 - "node_modules/@tsconfig/node18": {
2264 - "version": "18.2.2",
2265 - "resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.2.tgz",
2266 - "integrity": "sha512-d6McJeGsuoRlwWZmVIeE8CUA27lu6jLjvv1JzqmpsytOYYbVi1tHZEnwCNVOXnj4pyLvneZlFlpXUK+X9wBWyw==",
2267 - "dev": true
2268 - },
2269 - "node_modules/@tufjs/canonical-json": {
2270 - "version": "2.0.0",
2271 - "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
2272 - "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==",
2273 - "dev": true,
2274 - "engines": {
2275 - "node": "^16.14.0 || >=18.0.0"
2276 - }
2277 - },
2278 - "node_modules/@tufjs/models": {
2279 - "version": "2.0.0",
2280 - "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.0.tgz",
2281 - "integrity": "sha512-c8nj8BaOExmZKO2DXhDfegyhSGcG9E/mPN3U13L+/PsoWm1uaGiHHjxqSHQiasDBQwDA3aHuw9+9spYAP1qvvg==",
2282 - "dev": true,
2283 - "dependencies": {
2284 - "@tufjs/canonical-json": "2.0.0",
2285 - "minimatch": "^9.0.3"
2286 - },
2287 - "engines": {
2288 - "node": "^16.14.0 || >=18.0.0"
2289 - }
2290 - },
2291 - "node_modules/@types/bytes": {
2292 - "version": "3.1.4",
2293 - "resolved": "https://registry.npmjs.org/@types/bytes/-/bytes-3.1.4.tgz",
2294 - "integrity": "sha512-A0uYgOj3zNc4hNjHc5lYUfJQ/HVyBXiUMKdXd7ysclaE6k9oJdavQzODHuwjpUu2/boCP8afjQYi8z/GtvNCWA==",
2295 - "dev": true
2296 - },
2297 - "node_modules/@types/estree": {
2298 - "version": "1.0.5",
2299 - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
2300 - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
2301 - "dev": true
2302 - },
2303 - "node_modules/@types/fs-extra": {
2304 - "version": "11.0.4",
2305 - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz",
2306 - "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==",
2307 - "dev": true,
2308 - "dependencies": {
2309 - "@types/jsonfile": "*",
2310 - "@types/node": "*"
2311 - }
2312 - },
2313 - "node_modules/@types/highlight.js": {
2314 - "version": "10.1.0",
2315 - "resolved": "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-10.1.0.tgz",
2316 - "integrity": "sha512-77hF2dGBsOgnvZll1vymYiNUtqJ8cJfXPD6GG/2M0aLRc29PkvB7Au6sIDjIEFcSICBhCh2+Pyq6WSRS7LUm6A==",
2317 - "deprecated": "This is a stub types definition. highlight.js provides its own type definitions, so you do not need this installed.",
2318 - "dev": true,
2319 - "dependencies": {
2320 - "highlight.js": "*"
2321 - }
2322 - },
2323 - "node_modules/@types/inquirer": {
2324 - "version": "9.0.7",
2325 - "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.7.tgz",
2326 - "integrity": "sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g==",
2327 - "dev": true,
2328 - "dependencies": {
2329 - "@types/through": "*",
2330 - "rxjs": "^7.2.0"
2331 - }
2332 - },
2333 - "node_modules/@types/jsdom": {
2334 - "version": "21.1.6",
2335 - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.6.tgz",
2336 - "integrity": "sha512-/7kkMsC+/kMs7gAYmmBR9P0vGTnOoLhQhyhQJSlXGI5bzTHp6xdo0TtKWQAsz6pmSAeVqKSbqeyP6hytqr9FDw==",
2337 - "dev": true,
2338 - "dependencies": {
2339 - "@types/node": "*",
2340 - "@types/tough-cookie": "*",
2341 - "parse5": "^7.0.0"
2342 - }
2343 - },
2344 - "node_modules/@types/json-schema": {
2345 - "version": "7.0.15",
2346 - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
2347 - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
2348 - },
2349 - "node_modules/@types/jsonfile": {
2350 - "version": "6.1.4",
2351 - "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz",
2352 - "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==",
2353 - "dev": true,
2354 - "dependencies": {
2355 - "@types/node": "*"
2356 - }
2357 - },
2358 - "node_modules/@types/katex": {
2359 - "version": "0.16.7",
2360 - "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz",
2361 - "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="
2362 - },
2363 - "node_modules/@types/linkify-it": {
2364 - "version": "3.0.5",
2365 - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz",
2366 - "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==",
2367 - "dev": true
2368 - },
2369 - "node_modules/@types/lodash": {
2370 - "version": "4.14.202",
2371 - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz",
2372 - "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ=="
2373 - },
2374 - "node_modules/@types/lodash-es": {
2375 - "version": "4.17.12",
2376 - "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
2377 - "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
2378 - "dependencies": {
2379 - "@types/lodash": "*"
2380 - }
2381 - },
2382 - "node_modules/@types/markdown-it": {
2383 - "version": "13.0.7",
2384 - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.7.tgz",
2385 - "integrity": "sha512-U/CBi2YUUcTHBt5tjO2r5QV/x0Po6nsYwQU4Y04fBS6vfoImaiZ6f8bi3CjTCxBPQSO1LMyUqkByzi8AidyxfA==",
2386 - "dev": true,
2387 - "dependencies": {
2388 - "@types/linkify-it": "*",
2389 - "@types/mdurl": "*"
2390 - }
2391 - },
2392 - "node_modules/@types/markdown-it-highlightjs": {
2393 - "version": "3.3.4",
2394 - "resolved": "https://registry.npmjs.org/@types/markdown-it-highlightjs/-/markdown-it-highlightjs-3.3.4.tgz",
2395 - "integrity": "sha512-hERRPIvWifT0006DIjg1IvuoBzlksk97kPmWjynejzTW9AISS92b/mpu3PFkAWlXYNFC52RqdhNKjdJZ6GC4wg==",
2396 - "dev": true,
2397 - "dependencies": {
2398 - "@types/markdown-it": "*",
2399 - "highlight.js": "^10.1.0"
2400 - }
2401 - },
2402 - "node_modules/@types/mdurl": {
2403 - "version": "1.0.5",
2404 - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz",
2405 - "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==",
2406 - "dev": true
2407 - },
2408 - "node_modules/@types/node": {
2409 - "version": "20.11.16",
2410 - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz",
2411 - "integrity": "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==",
2412 - "dev": true,
2413 - "dependencies": {
2414 - "undici-types": "~5.26.4"
2415 - }
2416 - },
2417 - "node_modules/@types/semver": {
2418 - "version": "7.5.6",
2419 - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
2420 - "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A=="
2421 - },
2422 - "node_modules/@types/sinonjs__fake-timers": {
2423 - "version": "8.1.1",
2424 - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz",
2425 - "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==",
2426 - "dev": true
2427 - },
2428 - "node_modules/@types/sizzle": {
2429 - "version": "2.3.8",
2430 - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz",
2431 - "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==",
2432 - "dev": true
2433 - },
2434 - "node_modules/@types/through": {
2435 - "version": "0.0.33",
2436 - "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz",
2437 - "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==",
2438 - "dev": true,
2439 - "dependencies": {
2440 - "@types/node": "*"
2441 - }
2442 - },
2443 - "node_modules/@types/tough-cookie": {
2444 - "version": "4.0.5",
2445 - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
2446 - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
2447 - "dev": true
2448 - },
2449 - "node_modules/@types/validator": {
2450 - "version": "13.11.9",
2451 - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz",
2452 - "integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==",
2453 - "dev": true
2454 - },
2455 - "node_modules/@types/web-bluetooth": {
2456 - "version": "0.0.20",
2457 - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
2458 - "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="
2459 - },
2460 - "node_modules/@types/yauzl": {
2461 - "version": "2.10.3",
2462 - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
2463 - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
2464 - "dev": true,
2465 - "optional": true,
2466 - "dependencies": {
2467 - "@types/node": "*"
2468 - }
2469 - },
2470 - "node_modules/@typescript-eslint/eslint-plugin": {
2471 - "version": "6.19.1",
2472 - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.19.1.tgz",
2473 - "integrity": "sha512-roQScUGFruWod9CEyoV5KlCYrubC/fvG8/1zXuT0WTcxX87GnMMmnksMwSg99lo1xiKrBzw2icsJPMAw1OtKxg==",
2474 - "dependencies": {
2475 - "@eslint-community/regexpp": "^4.5.1",
2476 - "@typescript-eslint/scope-manager": "6.19.1",
2477 - "@typescript-eslint/type-utils": "6.19.1",
2478 - "@typescript-eslint/utils": "6.19.1",
2479 - "@typescript-eslint/visitor-keys": "6.19.1",
2480 - "debug": "^4.3.4",
2481 - "graphemer": "^1.4.0",
2482 - "ignore": "^5.2.4",
2483 - "natural-compare": "^1.4.0",
2484 - "semver": "^7.5.4",
2485 - "ts-api-utils": "^1.0.1"
2486 - },
2487 - "engines": {
2488 - "node": "^16.0.0 || >=18.0.0"
2489 - },
2490 - "funding": {
2491 - "type": "opencollective",
2492 - "url": "https://opencollective.com/typescript-eslint"
2493 - },
2494 - "peerDependencies": {
2495 - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
2496 - "eslint": "^7.0.0 || ^8.0.0"
2497 - },
2498 - "peerDependenciesMeta": {
2499 - "typescript": {
2500 - "optional": true
2501 - }
2502 - }
2503 - },
2504 - "node_modules/@typescript-eslint/parser": {
2505 - "version": "6.19.1",
2506 - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.19.1.tgz",
2507 - "integrity": "sha512-WEfX22ziAh6pRE9jnbkkLGp/4RhTpffr2ZK5bJ18M8mIfA8A+k97U9ZyaXCEJRlmMHh7R9MJZWXp/r73DzINVQ==",
2508 - "dependencies": {
2509 - "@typescript-eslint/scope-manager": "6.19.1",
2510 - "@typescript-eslint/types": "6.19.1",
2511 - "@typescript-eslint/typescript-estree": "6.19.1",
2512 - "@typescript-eslint/visitor-keys": "6.19.1",
2513 - "debug": "^4.3.4"
2514 - },
2515 - "engines": {
2516 - "node": "^16.0.0 || >=18.0.0"
2517 - },
2518 - "funding": {
2519 - "type": "opencollective",
2520 - "url": "https://opencollective.com/typescript-eslint"
2521 - },
2522 - "peerDependencies": {
2523 - "eslint": "^7.0.0 || ^8.0.0"
2524 - },
2525 - "peerDependenciesMeta": {
2526 - "typescript": {
2527 - "optional": true
2528 - }
2529 - }
2530 - },
2531 - "node_modules/@typescript-eslint/scope-manager": {
2532 - "version": "6.19.1",
2533 - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.1.tgz",
2534 - "integrity": "sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w==",
2535 - "dependencies": {
2536 - "@typescript-eslint/types": "6.19.1",
2537 - "@typescript-eslint/visitor-keys": "6.19.1"
2538 - },
2539 - "engines": {
2540 - "node": "^16.0.0 || >=18.0.0"
2541 - },
2542 - "funding": {
2543 - "type": "opencollective",
2544 - "url": "https://opencollective.com/typescript-eslint"
2545 - }
2546 - },
2547 - "node_modules/@typescript-eslint/type-utils": {
2548 - "version": "6.19.1",
2549 - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.19.1.tgz",
2550 - "integrity": "sha512-0vdyld3ecfxJuddDjACUvlAeYNrHP/pDeQk2pWBR2ESeEzQhg52DF53AbI9QCBkYE23lgkhLCZNkHn2hEXXYIg==",
2551 - "dependencies": {
2552 - "@typescript-eslint/typescript-estree": "6.19.1",
2553 - "@typescript-eslint/utils": "6.19.1",
2554 - "debug": "^4.3.4",
2555 - "ts-api-utils": "^1.0.1"
2556 - },
2557 - "engines": {
2558 - "node": "^16.0.0 || >=18.0.0"
2559 - },
2560 - "funding": {
2561 - "type": "opencollective",
2562 - "url": "https://opencollective.com/typescript-eslint"
2563 - },
2564 - "peerDependencies": {
2565 - "eslint": "^7.0.0 || ^8.0.0"
2566 - },
2567 - "peerDependenciesMeta": {
2568 - "typescript": {
2569 - "optional": true
2570 - }
2571 - }
2572 - },
2573 - "node_modules/@typescript-eslint/types": {
2574 - "version": "6.19.1",
2575 - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.1.tgz",
2576 - "integrity": "sha512-6+bk6FEtBhvfYvpHsDgAL3uo4BfvnTnoge5LrrCj2eJN8g3IJdLTD4B/jK3Q6vo4Ql/Hoip9I8aB6fF+6RfDqg==",
2577 - "engines": {
2578 - "node": "^16.0.0 || >=18.0.0"
2579 - },
2580 - "funding": {
2581 - "type": "opencollective",
2582 - "url": "https://opencollective.com/typescript-eslint"
2583 - }
2584 - },
2585 - "node_modules/@typescript-eslint/typescript-estree": {
2586 - "version": "6.19.1",
2587 - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.1.tgz",
2588 - "integrity": "sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA==",
2589 - "dependencies": {
2590 - "@typescript-eslint/types": "6.19.1",
2591 - "@typescript-eslint/visitor-keys": "6.19.1",
2592 - "debug": "^4.3.4",
2593 - "globby": "^11.1.0",
2594 - "is-glob": "^4.0.3",
2595 - "minimatch": "9.0.3",
2596 - "semver": "^7.5.4",
2597 - "ts-api-utils": "^1.0.1"
2598 - },
2599 - "engines": {
2600 - "node": "^16.0.0 || >=18.0.0"
2601 - },
2602 - "funding": {
2603 - "type": "opencollective",
2604 - "url": "https://opencollective.com/typescript-eslint"
2605 - },
2606 - "peerDependenciesMeta": {
2607 - "typescript": {
2608 - "optional": true
2609 - }
2610 - }
2611 - },
2612 - "node_modules/@typescript-eslint/utils": {
2613 - "version": "6.19.1",
2614 - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.19.1.tgz",
2615 - "integrity": "sha512-JvjfEZuP5WoMqwh9SPAPDSHSg9FBHHGhjPugSRxu5jMfjvBpq5/sGTD+9M9aQ5sh6iJ8AY/Kk/oUYVEMAPwi7w==",
2616 - "dependencies": {
2617 - "@eslint-community/eslint-utils": "^4.4.0",
2618 - "@types/json-schema": "^7.0.12",
2619 - "@types/semver": "^7.5.0",
2620 - "@typescript-eslint/scope-manager": "6.19.1",
2621 - "@typescript-eslint/types": "6.19.1",
2622 - "@typescript-eslint/typescript-estree": "6.19.1",
2623 - "semver": "^7.5.4"
2624 - },
2625 - "engines": {
2626 - "node": "^16.0.0 || >=18.0.0"
2627 - },
2628 - "funding": {
2629 - "type": "opencollective",
2630 - "url": "https://opencollective.com/typescript-eslint"
2631 - },
2632 - "peerDependencies": {
2633 - "eslint": "^7.0.0 || ^8.0.0"
2634 - }
2635 - },
2636 - "node_modules/@typescript-eslint/visitor-keys": {
2637 - "version": "6.19.1",
2638 - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.1.tgz",
2639 - "integrity": "sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ==",
2640 - "dependencies": {
2641 - "@typescript-eslint/types": "6.19.1",
2642 - "eslint-visitor-keys": "^3.4.1"
2643 - },
2644 - "engines": {
2645 - "node": "^16.0.0 || >=18.0.0"
2646 - },
2647 - "funding": {
2648 - "type": "opencollective",
2649 - "url": "https://opencollective.com/typescript-eslint"
2650 - }
2651 - },
2652 - "node_modules/@ungap/structured-clone": {
2653 - "version": "1.2.0",
2654 - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
2655 - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="
2656 - },
2657 - "node_modules/@unocss/config": {
2658 - "version": "0.56.5",
2659 - "resolved": "https://registry.npmjs.org/@unocss/config/-/config-0.56.5.tgz",
2660 - "integrity": "sha512-rscnFIYgUlN/0hXHdhANyjFcDjDutt3JO0ZRITdNLzoglh7GVNiDTURBJwUZejF/vGJ7IkMd3qOdNhPFuRY1Bg==",
2661 - "dependencies": {
2662 - "@unocss/core": "0.56.5",
2663 - "unconfig": "^0.3.10"
2664 - },
2665 - "engines": {
2666 - "node": ">=14"
2667 - },
2668 - "funding": {
2669 - "url": "https://github.com/sponsors/antfu"
2670 - }
2671 - },
2672 - "node_modules/@unocss/core": {
2673 - "version": "0.56.5",
2674 - "resolved": "https://registry.npmjs.org/@unocss/core/-/core-0.56.5.tgz",
2675 - "integrity": "sha512-fx5VhOjSHn0HdV2D34pEwFMAHJcJQRTCp1xEE4GzxY1irXzaa+m2aYf5PZjmDxehiOC16IH7TO9FOWANXk1E0w==",
2676 - "funding": {
2677 - "url": "https://github.com/sponsors/antfu"
2678 - }
2679 - },
2680 - "node_modules/@unocss/eslint-config": {
2681 - "version": "0.56.5",
2682 - "resolved": "https://registry.npmjs.org/@unocss/eslint-config/-/eslint-config-0.56.5.tgz",
2683 - "integrity": "sha512-UP7EBTl4ORRO0yqptW7oUWxM0qNh1Nk+z4rufC0C2kI8vKc8yab+sWvlBSC8DeX3kzR7uJxvtUsHGcBcoqZujg==",
2684 - "dependencies": {
2685 - "@unocss/eslint-plugin": "0.56.5"
2686 - },
2687 - "engines": {
2688 - "node": ">=14"
2689 - },
2690 - "funding": {
2691 - "url": "https://github.com/sponsors/antfu"
2692 - }
2693 - },
2694 - "node_modules/@unocss/eslint-plugin": {
2695 - "version": "0.56.5",
2696 - "resolved": "https://registry.npmjs.org/@unocss/eslint-plugin/-/eslint-plugin-0.56.5.tgz",
2697 - "integrity": "sha512-nMVw/kc0sYU5i8UBU1rEahzKhZRPTUjCztpPzo2KUKFKLBPAbTv0gJAOmU/n2kV3YiGk5Tl6jmShxu1MHjk5rA==",
2698 - "dependencies": {
2699 - "@typescript-eslint/utils": "^6.7.3",
2700 - "@unocss/config": "0.56.5",
2701 - "@unocss/core": "0.56.5",
2702 - "magic-string": "^0.30.3",
2703 - "synckit": "^0.8.5"
2704 - },
2705 - "engines": {
2706 - "node": ">=14"
2707 - },
2708 - "funding": {
2709 - "url": "https://github.com/sponsors/antfu"
2710 - }
2711 - },
2712 - "node_modules/@vitejs/plugin-vue": {
2713 - "version": "5.0.3",
2714 - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.3.tgz",
2715 - "integrity": "sha512-b8S5dVS40rgHdDrw+DQi/xOM9ed+kSRZzfm1T74bMmBDCd8XO87NKlFYInzCtwvtWwXZvo1QxE2OSspTATWrbA==",
2716 - "dev": true,
2717 - "engines": {
2718 - "node": "^18.0.0 || >=20.0.0"
2719 - },
2720 - "peerDependencies": {
2721 - "vite": "^5.0.0",
2722 - "vue": "^3.2.25"
2723 - }
2724 - },
2725 - "node_modules/@vitejs/plugin-vue-jsx": {
2726 - "version": "3.1.0",
2727 - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-3.1.0.tgz",
2728 - "integrity": "sha512-w9M6F3LSEU5kszVb9An2/MmXNxocAnUb3WhRr8bHlimhDrXNt6n6D2nJQR3UXpGlZHh/EsgouOHCsM8V3Ln+WA==",
2729 - "dev": true,
2730 - "dependencies": {
2731 - "@babel/core": "^7.23.3",
2732 - "@babel/plugin-transform-typescript": "^7.23.3",
2733 - "@vue/babel-plugin-jsx": "^1.1.5"
2734 - },
2735 - "engines": {
2736 - "node": "^14.18.0 || >=16.0.0"
2737 - },
2738 - "peerDependencies": {
2739 - "vite": "^4.0.0 || ^5.0.0",
2740 - "vue": "^3.0.0"
2741 - }
2742 - },
2743 - "node_modules/@vitest/expect": {
2744 - "version": "1.2.2",
2745 - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.2.tgz",
2746 - "integrity": "sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==",
2747 - "dev": true,
2748 - "dependencies": {
2749 - "@vitest/spy": "1.2.2",
2750 - "@vitest/utils": "1.2.2",
2751 - "chai": "^4.3.10"
2752 - },
2753 - "funding": {
2754 - "url": "https://opencollective.com/vitest"
2755 - }
2756 - },
2757 - "node_modules/@vitest/runner": {
2758 - "version": "1.2.2",
2759 - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.2.tgz",
2760 - "integrity": "sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==",
2761 - "dev": true,
2762 - "dependencies": {
2763 - "@vitest/utils": "1.2.2",
2764 - "p-limit": "^5.0.0",
2765 - "pathe": "^1.1.1"
2766 - },
2767 - "funding": {
2768 - "url": "https://opencollective.com/vitest"
2769 - }
2770 - },
2771 - "node_modules/@vitest/runner/node_modules/p-limit": {
2772 - "version": "5.0.0",
2773 - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz",
2774 - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==",
2775 - "dev": true,
2776 - "dependencies": {
2777 - "yocto-queue": "^1.0.0"
2778 - },
2779 - "engines": {
2780 - "node": ">=18"
2781 - },
2782 - "funding": {
2783 - "url": "https://github.com/sponsors/sindresorhus"
2784 - }
2785 - },
2786 - "node_modules/@vitest/runner/node_modules/yocto-queue": {
2787 - "version": "1.0.0",
2788 - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
2789 - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==",
2790 - "dev": true,
2791 - "engines": {
2792 - "node": ">=12.20"
2793 - },
2794 - "funding": {
2795 - "url": "https://github.com/sponsors/sindresorhus"
2796 - }
2797 - },
2798 - "node_modules/@vitest/snapshot": {
2799 - "version": "1.2.2",
2800 - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.2.tgz",
2801 - "integrity": "sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==",
2802 - "dev": true,
2803 - "dependencies": {
2804 - "magic-string": "^0.30.5",
2805 - "pathe": "^1.1.1",
2806 - "pretty-format": "^29.7.0"
2807 - },
2808 - "funding": {
2809 - "url": "https://opencollective.com/vitest"
2810 - }
2811 - },
2812 - "node_modules/@vitest/spy": {
2813 - "version": "1.2.2",
2814 - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.2.tgz",
2815 - "integrity": "sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==",
2816 - "dev": true,
2817 - "dependencies": {
2818 - "tinyspy": "^2.2.0"
2819 - },
2820 - "funding": {
2821 - "url": "https://opencollective.com/vitest"
2822 - }
2823 - },
2824 - "node_modules/@vitest/utils": {
2825 - "version": "1.2.2",
2826 - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.2.tgz",
2827 - "integrity": "sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==",
2828 - "dev": true,
2829 - "dependencies": {
2830 - "diff-sequences": "^29.6.3",
2831 - "estree-walker": "^3.0.3",
2832 - "loupe": "^2.3.7",
2833 - "pretty-format": "^29.7.0"
2834 - },
2835 - "funding": {
2836 - "url": "https://opencollective.com/vitest"
2837 - }
2838 - },
2839 - "node_modules/@vitest/utils/node_modules/estree-walker": {
2840 - "version": "3.0.3",
2841 - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
2842 - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
2843 - "dev": true,
2844 - "dependencies": {
2845 - "@types/estree": "^1.0.0"
2846 - }
2847 - },
2848 - "node_modules/@volar/language-core": {
2849 - "version": "1.11.1",
2850 - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz",
2851 - "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==",
2852 - "dev": true,
2853 - "dependencies": {
2854 - "@volar/source-map": "1.11.1"
2855 - }
2856 - },
2857 - "node_modules/@volar/source-map": {
2858 - "version": "1.11.1",
2859 - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz",
2860 - "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==",
2861 - "dev": true,
2862 - "dependencies": {
2863 - "muggle-string": "^0.3.1"
2864 - }
2865 - },
2866 - "node_modules/@volar/typescript": {
2867 - "version": "1.11.1",
2868 - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz",
2869 - "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==",
2870 - "dev": true,
2871 - "dependencies": {
2872 - "@volar/language-core": "1.11.1",
2873 - "path-browserify": "^1.0.1"
2874 - }
2875 - },
2876 - "node_modules/@vue/babel-helper-vue-transform-on": {
2877 - "version": "1.2.1",
2878 - "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.2.1.tgz",
2879 - "integrity": "sha512-jtEXim+pfyHWwvheYwUwSXm43KwQo8nhOBDyjrUITV6X2tB7lJm6n/+4sqR8137UVZZul5hBzWHdZ2uStYpyRQ==",
2880 - "dev": true
2881 - },
2882 - "node_modules/@vue/babel-plugin-jsx": {
2883 - "version": "1.2.1",
2884 - "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.2.1.tgz",
2885 - "integrity": "sha512-Yy9qGktktXhB39QE99So/BO2Uwm/ZG+gpL9vMg51ijRRbINvgbuhyJEi4WYmGRMx/MSTfK0xjgZ3/MyY+iLCEg==",
2886 - "dev": true,
2887 - "dependencies": {
2888 - "@babel/helper-module-imports": "^7.22.15",
2889 - "@babel/helper-plugin-utils": "^7.22.5",
2890 - "@babel/plugin-syntax-jsx": "^7.23.3",
2891 - "@babel/template": "^7.22.15",
2892 - "@babel/traverse": "^7.23.7",
2893 - "@babel/types": "^7.23.6",
2894 - "@vue/babel-helper-vue-transform-on": "1.2.1",
2895 - "@vue/babel-plugin-resolve-type": "1.2.1",
2896 - "camelcase": "^6.3.0",
2897 - "html-tags": "^3.3.1",
2898 - "svg-tags": "^1.0.0"
2899 - },
2900 - "peerDependencies": {
2901 - "@babel/core": "^7.0.0-0"
2902 - },
2903 - "peerDependenciesMeta": {
2904 - "@babel/core": {
2905 - "optional": true
2906 - }
2907 - }
2908 - },
2909 - "node_modules/@vue/babel-plugin-resolve-type": {
2910 - "version": "1.2.1",
2911 - "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.2.1.tgz",
2912 - "integrity": "sha512-IOtnI7pHunUzHS/y+EG/yPABIAp0VN8QhQ0UCS09jeMVxgAnI9qdOzO85RXdQGxq+aWCdv8/+k3W0aYO6j/8fQ==",
2913 - "dev": true,
2914 - "dependencies": {
2915 - "@babel/code-frame": "^7.23.5",
2916 - "@babel/helper-module-imports": "^7.22.15",
2917 - "@babel/helper-plugin-utils": "^7.22.5",
2918 - "@babel/parser": "^7.23.6",
2919 - "@vue/compiler-sfc": "^3.4.15"
2920 - },
2921 - "peerDependencies": {
2922 - "@babel/core": "^7.0.0-0"
2923 - }
2924 - },
2925 - "node_modules/@vue/compiler-core": {
2926 - "version": "3.4.15",
2927 - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.15.tgz",
2928 - "integrity": "sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==",
2929 - "dependencies": {
2930 - "@babel/parser": "^7.23.6",
2931 - "@vue/shared": "3.4.15",
2932 - "entities": "^4.5.0",
2933 - "estree-walker": "^2.0.2",
2934 - "source-map-js": "^1.0.2"
2935 - }
2936 - },
2937 - "node_modules/@vue/compiler-dom": {
2938 - "version": "3.4.15",
2939 - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.15.tgz",
2940 - "integrity": "sha512-wox0aasVV74zoXyblarOM3AZQz/Z+OunYcIHe1OsGclCHt8RsRm04DObjefaI82u6XDzv+qGWZ24tIsRAIi5MQ==",
2941 - "dependencies": {
2942 - "@vue/compiler-core": "3.4.15",
2943 - "@vue/shared": "3.4.15"
2944 - }
2945 - },
2946 - "node_modules/@vue/compiler-sfc": {
2947 - "version": "3.4.15",
2948 - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.15.tgz",
2949 - "integrity": "sha512-LCn5M6QpkpFsh3GQvs2mJUOAlBQcCco8D60Bcqmf3O3w5a+KWS5GvYbrrJBkgvL1BDnTp+e8q0lXCLgHhKguBA==",
2950 - "dependencies": {
2951 - "@babel/parser": "^7.23.6",
2952 - "@vue/compiler-core": "3.4.15",
2953 - "@vue/compiler-dom": "3.4.15",
2954 - "@vue/compiler-ssr": "3.4.15",
2955 - "@vue/shared": "3.4.15",
2956 - "estree-walker": "^2.0.2",
2957 - "magic-string": "^0.30.5",
2958 - "postcss": "^8.4.33",
2959 - "source-map-js": "^1.0.2"
2960 - }
2961 - },
2962 - "node_modules/@vue/compiler-ssr": {
2963 - "version": "3.4.15",
2964 - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.15.tgz",
2965 - "integrity": "sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw==",
2966 - "dependencies": {
2967 - "@vue/compiler-dom": "3.4.15",
2968 - "@vue/shared": "3.4.15"
2969 - }
2970 - },
2971 - "node_modules/@vue/devtools-api": {
2972 - "version": "6.5.1",
2973 - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.1.tgz",
2974 - "integrity": "sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA=="
2975 - },
2976 - "node_modules/@vue/eslint-config-prettier": {
2977 - "version": "9.0.0",
2978 - "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz",
2979 - "integrity": "sha512-z1ZIAAUS9pKzo/ANEfd2sO+v2IUalz7cM/cTLOZ7vRFOPk5/xuRKQteOu1DErFLAh/lYGXMVZ0IfYKlyInuDVg==",
2980 - "dev": true,
2981 - "dependencies": {
2982 - "eslint-config-prettier": "^9.0.0",
2983 - "eslint-plugin-prettier": "^5.0.0"
2984 - },
2985 - "peerDependencies": {
2986 - "eslint": ">= 8.0.0",
2987 - "prettier": ">= 3.0.0"
2988 - }
2989 - },
2990 - "node_modules/@vue/eslint-config-typescript": {
2991 - "version": "12.0.0",
2992 - "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-12.0.0.tgz",
2993 - "integrity": "sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg==",
2994 - "dev": true,
2995 - "dependencies": {
2996 - "@typescript-eslint/eslint-plugin": "^6.7.0",
2997 - "@typescript-eslint/parser": "^6.7.0",
2998 - "vue-eslint-parser": "^9.3.1"
2999 - },
3000 - "engines": {
3001 - "node": "^14.17.0 || >=16.0.0"
3002 - },
3003 - "peerDependencies": {
3004 - "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0",
3005 - "eslint-plugin-vue": "^9.0.0",
3006 - "typescript": "*"
3007 - },
3008 - "peerDependenciesMeta": {
3009 - "typescript": {
3010 - "optional": true
3011 - }
3012 - }
3013 - },
3014 - "node_modules/@vue/language-core": {
3015 - "version": "1.8.27",
3016 - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.27.tgz",
3017 - "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==",
3018 - "dev": true,
3019 - "dependencies": {
3020 - "@volar/language-core": "~1.11.1",
3021 - "@volar/source-map": "~1.11.1",
3022 - "@vue/compiler-dom": "^3.3.0",
3023 - "@vue/shared": "^3.3.0",
3024 - "computeds": "^0.0.1",
3025 - "minimatch": "^9.0.3",
3026 - "muggle-string": "^0.3.1",
3027 - "path-browserify": "^1.0.1",
3028 - "vue-template-compiler": "^2.7.14"
3029 - },
3030 - "peerDependencies": {
3031 - "typescript": "*"
3032 - },
3033 - "peerDependenciesMeta": {
3034 - "typescript": {
3035 - "optional": true
3036 - }
3037 - }
3038 - },
3039 - "node_modules/@vue/reactivity": {
3040 - "version": "3.4.15",
3041 - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.15.tgz",
3042 - "integrity": "sha512-55yJh2bsff20K5O84MxSvXKPHHt17I2EomHznvFiJCAZpJTNW8IuLj1xZWMLELRhBK3kkFV/1ErZGHJfah7i7w==",
3043 - "dependencies": {
3044 - "@vue/shared": "3.4.15"
3045 - }
3046 - },
3047 - "node_modules/@vue/runtime-core": {
3048 - "version": "3.4.15",
3049 - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.15.tgz",
3050 - "integrity": "sha512-6E3by5m6v1AkW0McCeAyhHTw+3y17YCOKG0U0HDKDscV4Hs0kgNT5G+GCHak16jKgcCDHpI9xe5NKb8sdLCLdw==",
3051 - "dependencies": {
3052 - "@vue/reactivity": "3.4.15",
3053 - "@vue/shared": "3.4.15"
3054 - }
3055 - },
3056 - "node_modules/@vue/runtime-dom": {
3057 - "version": "3.4.15",
3058 - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.15.tgz",
3059 - "integrity": "sha512-EVW8D6vfFVq3V/yDKNPBFkZKGMFSvZrUQmx196o/v2tHKdwWdiZjYUBS+0Ez3+ohRyF8Njwy/6FH5gYJ75liUw==",
3060 - "dependencies": {
3061 - "@vue/runtime-core": "3.4.15",
3062 - "@vue/shared": "3.4.15",
3063 - "csstype": "^3.1.3"
3064 - }
3065 - },
3066 - "node_modules/@vue/server-renderer": {
3067 - "version": "3.4.15",
3068 - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.15.tgz",
3069 - "integrity": "sha512-3HYzaidu9cHjrT+qGUuDhFYvF/j643bHC6uUN9BgM11DVy+pM6ATsG6uPBLnkwOgs7BpJABReLmpL3ZPAsUaqw==",
3070 - "dependencies": {
3071 - "@vue/compiler-ssr": "3.4.15",
3072 - "@vue/shared": "3.4.15"
3073 - },
3074 - "peerDependencies": {
3075 - "vue": "3.4.15"
3076 - }
3077 - },
3078 - "node_modules/@vue/shared": {
3079 - "version": "3.4.15",
3080 - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
3081 - "integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g=="
3082 - },
3083 - "node_modules/@vue/test-utils": {
3084 - "version": "2.4.4",
3085 - "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.4.tgz",
3086 - "integrity": "sha512-8jkRxz8pNhClAf4Co4ZrpAoFISdvT3nuSkUlY6Ys6rmTpw3DMWG/X3mw3gQ7QJzgCZO9f+zuE2kW57fi09MW7Q==",
3087 - "dev": true,
3088 - "dependencies": {
3089 - "js-beautify": "^1.14.9",
3090 - "vue-component-type-helpers": "^1.8.21"
3091 - },
3092 - "peerDependencies": {
3093 - "@vue/server-renderer": "^3.0.1",
3094 - "vue": "^3.0.1"
3095 - },
3096 - "peerDependenciesMeta": {
3097 - "@vue/server-renderer": {
3098 - "optional": true
3099 - }
3100 - }
3101 - },
3102 - "node_modules/@vue/tsconfig": {
3103 - "version": "0.5.1",
3104 - "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.5.1.tgz",
3105 - "integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==",
3106 - "dev": true
3107 - },
3108 - "node_modules/@vueuse/components": {
3109 - "version": "10.7.2",
3110 - "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-10.7.2.tgz",
3111 - "integrity": "sha512-r39DLLtRo1hEKI/SQzVQjCts7yelwFyUrTxDFi821NdyU3EfQ9GCNNBcMirXcn3IQApFBRKrvTTtQ9cJGrb/+A==",
3112 - "dependencies": {
3113 - "@vueuse/core": "10.7.2",
3114 - "@vueuse/shared": "10.7.2",
3115 - "vue-demi": ">=0.14.6"
3116 - }
3117 - },
3118 - "node_modules/@vueuse/components/node_modules/vue-demi": {
3119 - "version": "0.14.6",
3120 - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
3121 - "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
3122 - "hasInstallScript": true,
3123 - "bin": {
3124 - "vue-demi-fix": "bin/vue-demi-fix.js",
3125 - "vue-demi-switch": "bin/vue-demi-switch.js"
3126 - },
3127 - "engines": {
3128 - "node": ">=12"
3129 - },
3130 - "funding": {
3131 - "url": "https://github.com/sponsors/antfu"
3132 - },
3133 - "peerDependencies": {
3134 - "@vue/composition-api": "^1.0.0-rc.1",
3135 - "vue": "^3.0.0-0 || ^2.6.0"
3136 - },
3137 - "peerDependenciesMeta": {
3138 - "@vue/composition-api": {
3139 - "optional": true
3140 - }
3141 - }
3142 - },
3143 - "node_modules/@vueuse/core": {
3144 - "version": "10.7.2",
3145 - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.7.2.tgz",
3146 - "integrity": "sha512-AOyAL2rK0By62Hm+iqQn6Rbu8bfmbgaIMXcE3TSr7BdQ42wnSFlwIdPjInO62onYsEMK/yDMU8C6oGfDAtZ2qQ==",
3147 - "dependencies": {
3148 - "@types/web-bluetooth": "^0.0.20",
3149 - "@vueuse/metadata": "10.7.2",
3150 - "@vueuse/shared": "10.7.2",
3151 - "vue-demi": ">=0.14.6"
3152 - },
3153 - "funding": {
3154 - "url": "https://github.com/sponsors/antfu"
3155 - }
3156 - },
3157 - "node_modules/@vueuse/core/node_modules/vue-demi": {
3158 - "version": "0.14.6",
3159 - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
3160 - "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
3161 - "hasInstallScript": true,
3162 - "bin": {
3163 - "vue-demi-fix": "bin/vue-demi-fix.js",
3164 - "vue-demi-switch": "bin/vue-demi-switch.js"
3165 - },
3166 - "engines": {
3167 - "node": ">=12"
3168 - },
3169 - "funding": {
3170 - "url": "https://github.com/sponsors/antfu"
3171 - },
3172 - "peerDependencies": {
3173 - "@vue/composition-api": "^1.0.0-rc.1",
3174 - "vue": "^3.0.0-0 || ^2.6.0"
3175 - },
3176 - "peerDependenciesMeta": {
3177 - "@vue/composition-api": {
3178 - "optional": true
3179 - }
3180 - }
3181 - },
3182 - "node_modules/@vueuse/metadata": {
3183 - "version": "10.7.2",
3184 - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.7.2.tgz",
3185 - "integrity": "sha512-kCWPb4J2KGrwLtn1eJwaJD742u1k5h6v/St5wFe8Quih90+k2a0JP8BS4Zp34XUuJqS2AxFYMb1wjUL8HfhWsQ==",
3186 - "funding": {
3187 - "url": "https://github.com/sponsors/antfu"
3188 - }
3189 - },
3190 - "node_modules/@vueuse/shared": {
3191 - "version": "10.7.2",
3192 - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.7.2.tgz",
3193 - "integrity": "sha512-qFbXoxS44pi2FkgFjPvF4h7c9oMDutpyBdcJdMYIMg9XyXli2meFMuaKn+UMgsClo//Th6+beeCgqweT/79BVA==",
3194 - "dependencies": {
3195 - "vue-demi": ">=0.14.6"
3196 - },
3197 - "funding": {
3198 - "url": "https://github.com/sponsors/antfu"
3199 - }
3200 - },
3201 - "node_modules/@vueuse/shared/node_modules/vue-demi": {
3202 - "version": "0.14.6",
3203 - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
3204 - "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
3205 - "hasInstallScript": true,
3206 - "bin": {
3207 - "vue-demi-fix": "bin/vue-demi-fix.js",
3208 - "vue-demi-switch": "bin/vue-demi-switch.js"
3209 - },
3210 - "engines": {
3211 - "node": ">=12"
3212 - },
3213 - "funding": {
3214 - "url": "https://github.com/sponsors/antfu"
3215 - },
3216 - "peerDependencies": {
3217 - "@vue/composition-api": "^1.0.0-rc.1",
3218 - "vue": "^3.0.0-0 || ^2.6.0"
3219 - },
3220 - "peerDependenciesMeta": {
3221 - "@vue/composition-api": {
3222 - "optional": true
3223 - }
3224 - }
3225 - },
3226 - "node_modules/@yr/monotone-cubic-spline": {
3227 - "version": "1.0.3",
3228 - "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
3229 - "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA=="
3230 - },
3231 - "node_modules/abbrev": {
3232 - "version": "2.0.0",
3233 - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
3234 - "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
3235 - "dev": true,
3236 - "engines": {
3237 - "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
3238 - }
3239 - },
3240 - "node_modules/accepts": {
3241 - "version": "1.3.8",
3242 - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
3243 - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
3244 - "dev": true,
3245 - "dependencies": {
3246 - "mime-types": "~2.1.34",
3247 - "negotiator": "0.6.3"
3248 - },
3249 - "engines": {
3250 - "node": ">= 0.6"
3251 - }
3252 - },
3253 - "node_modules/acorn": {
3254 - "version": "8.11.3",
3255 - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
3256 - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
3257 - "bin": {
3258 - "acorn": "bin/acorn"
3259 - },
3260 - "engines": {
3261 - "node": ">=0.4.0"
3262 - }
3263 - },
3264 - "node_modules/acorn-jsx": {
3265 - "version": "5.3.2",
3266 - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
3267 - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
3268 - "peerDependencies": {
3269 - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
3270 - }
3271 - },
3272 - "node_modules/acorn-walk": {
3273 - "version": "8.3.2",
3274 - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
3275 - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
3276 - "dev": true,
3277 - "engines": {
3278 - "node": ">=0.4.0"
3279 - }
3280 - },
3281 - "node_modules/agent-base": {
3282 - "version": "7.1.0",
3283 - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
3284 - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==",
3285 - "dev": true,
3286 - "dependencies": {
3287 - "debug": "^4.3.4"
3288 - },
3289 - "engines": {
3290 - "node": ">= 14"
3291 - }
3292 - },
3293 - "node_modules/aggregate-error": {
3294 - "version": "3.1.0",
3295 - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
3296 - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
3297 - "dev": true,
3298 - "dependencies": {
3299 - "clean-stack": "^2.0.0",
3300 - "indent-string": "^4.0.0"
3301 - },
3302 - "engines": {
3303 - "node": ">=8"
3304 - }
3305 - },
3306 - "node_modules/ajv": {
3307 - "version": "6.12.6",
3308 - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
3309 - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
3310 - "dependencies": {
3311 - "fast-deep-equal": "^3.1.1",
3312 - "fast-json-stable-stringify": "^2.0.0",
3313 - "json-schema-traverse": "^0.4.1",
3314 - "uri-js": "^4.2.2"
3315 - },
3316 - "funding": {
3317 - "type": "github",
3318 - "url": "https://github.com/sponsors/epoberezkin"
3319 - }
3320 - },
3321 - "node_modules/ansi-colors": {
3322 - "version": "4.1.3",
3323 - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
3324 - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
3325 - "dev": true,
3326 - "engines": {
3327 - "node": ">=6"
3328 - }
3329 - },
3330 - "node_modules/ansi-escapes": {
3331 - "version": "4.3.2",
3332 - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
3333 - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
3334 - "dev": true,
3335 - "dependencies": {
3336 - "type-fest": "^0.21.3"
3337 - },
3338 - "engines": {
3339 - "node": ">=8"
3340 - },
3341 - "funding": {
3342 - "url": "https://github.com/sponsors/sindresorhus"
3343 - }
3344 - },
3345 - "node_modules/ansi-escapes/node_modules/type-fest": {
3346 - "version": "0.21.3",
3347 - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
3348 - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
3349 - "dev": true,
3350 - "engines": {
3351 - "node": ">=10"
3352 - },
3353 - "funding": {
3354 - "url": "https://github.com/sponsors/sindresorhus"
3355 - }
3356 - },
3357 - "node_modules/ansi-regex": {
3358 - "version": "5.0.1",
3359 - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
3360 - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
3361 - "engines": {
3362 - "node": ">=8"
3363 - }
3364 - },
3365 - "node_modules/ansi-styles": {
3366 - "version": "3.2.1",
3367 - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
3368 - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
3369 - "dev": true,
3370 - "dependencies": {
3371 - "color-convert": "^1.9.0"
3372 - },
3373 - "engines": {
3374 - "node": ">=4"
3375 - }
3376 - },
3377 - "node_modules/any-promise": {
3378 - "version": "1.3.0",
3379 - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
3380 - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
3381 - "dev": true
3382 - },
3383 - "node_modules/anymatch": {
3384 - "version": "3.1.3",
3385 - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
3386 - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
3387 - "dev": true,
3388 - "dependencies": {
3389 - "normalize-path": "^3.0.0",
3390 - "picomatch": "^2.0.4"
3391 - },
3392 - "engines": {
3393 - "node": ">= 8"
3394 - }
3395 - },
3396 - "node_modules/apexcharts": {
3397 - "version": "3.45.2",
3398 - "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.45.2.tgz",
3399 - "integrity": "sha512-PpuM4sJWy70sUh5U1IFn1m1p45MdHSChLUNnqEoUUUHSU2IHZugFrsVNhov1S8Q0cvfdrCRCvdBtHGSs6PSAWQ==",
3400 - "dependencies": {
3401 - "@yr/monotone-cubic-spline": "^1.0.3",
3402 - "svg.draggable.js": "^2.2.2",
3403 - "svg.easing.js": "^2.0.0",
3404 - "svg.filter.js": "^2.0.2",
3405 - "svg.pathmorphing.js": "^0.1.3",
3406 - "svg.resize.js": "^1.4.3",
3407 - "svg.select.js": "^3.0.1"
3408 - }
3409 - },
3410 - "node_modules/arch": {
3411 - "version": "2.2.0",
3412 - "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz",
3413 - "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==",
3414 - "dev": true,
3415 - "funding": [
3416 - {
3417 - "type": "github",
3418 - "url": "https://github.com/sponsors/feross"
3419 - },
3420 - {
3421 - "type": "patreon",
3422 - "url": "https://www.patreon.com/feross"
3423 - },
3424 - {
3425 - "type": "consulting",
3426 - "url": "https://feross.org/support"
3427 - }
3428 - ]
3429 - },
3430 - "node_modules/arg": {
3431 - "version": "5.0.2",
3432 - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
3433 - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
3434 - "dev": true
3435 - },
3436 - "node_modules/argparse": {
3437 - "version": "2.0.1",
3438 - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
3439 - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
3440 - },
3441 - "node_modules/array-buffer-byte-length": {
3442 - "version": "1.0.0",
3443 - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz",
3444 - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==",
3445 - "dev": true,
3446 - "dependencies": {
3447 - "call-bind": "^1.0.2",
3448 - "is-array-buffer": "^3.0.1"
3449 - },
3450 - "funding": {
3451 - "url": "https://github.com/sponsors/ljharb"
3452 - }
3453 - },
3454 - "node_modules/array-union": {
3455 - "version": "2.1.0",
3456 - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
3457 - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
3458 - "engines": {
3459 - "node": ">=8"
3460 - }
3461 - },
3462 - "node_modules/arraybuffer.prototype.slice": {
3463 - "version": "1.0.2",
3464 - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz",
3465 - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==",
3466 - "dev": true,
3467 - "dependencies": {
3468 - "array-buffer-byte-length": "^1.0.0",
3469 - "call-bind": "^1.0.2",
3470 - "define-properties": "^1.2.0",
3471 - "es-abstract": "^1.22.1",
3472 - "get-intrinsic": "^1.2.1",
3473 - "is-array-buffer": "^3.0.2",
3474 - "is-shared-array-buffer": "^1.0.2"
3475 - },
3476 - "engines": {
3477 - "node": ">= 0.4"
3478 - },
3479 - "funding": {
3480 - "url": "https://github.com/sponsors/ljharb"
3481 - }
3482 - },
3483 - "node_modules/asn1": {
3484 - "version": "0.2.6",
3485 - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
3486 - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
3487 - "dev": true,
3488 - "dependencies": {
3489 - "safer-buffer": "~2.1.0"
3490 - }
3491 - },
3492 - "node_modules/assert-plus": {
3493 - "version": "1.0.0",
3494 - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
3495 - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
3496 - "dev": true,
3497 - "engines": {
3498 - "node": ">=0.8"
3499 - }
3500 - },
3501 - "node_modules/assertion-error": {
3502 - "version": "1.1.0",
3503 - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
3504 - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
3505 - "dev": true,
3506 - "engines": {
3507 - "node": "*"
3508 - }
3509 - },
3510 - "node_modules/astral-regex": {
3511 - "version": "2.0.0",
3512 - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
3513 - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
3514 - "dev": true,
3515 - "engines": {
3516 - "node": ">=8"
3517 - }
3518 - },
3519 - "node_modules/async": {
3520 - "version": "3.2.5",
3521 - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
3522 - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==",
3523 - "dev": true
3524 - },
3525 - "node_modules/async-validator": {
3526 - "version": "4.2.5",
3527 - "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
3528 - "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg=="
3529 - },
3530 - "node_modules/asynckit": {
3531 - "version": "0.4.0",
3532 - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
3533 - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
3534 - "dev": true
3535 - },
3536 - "node_modules/at-least-node": {
3537 - "version": "1.0.0",
3538 - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
3539 - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
3540 - "dev": true,
3541 - "engines": {
3542 - "node": ">= 4.0.0"
3543 - }
3544 - },
3545 - "node_modules/autoprefixer": {
3546 - "version": "10.4.17",
3547 - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz",
3548 - "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==",
3549 - "dev": true,
3550 - "funding": [
3551 - {
3552 - "type": "opencollective",
3553 - "url": "https://opencollective.com/postcss/"
3554 - },
3555 - {
3556 - "type": "tidelift",
3557 - "url": "https://tidelift.com/funding/github/npm/autoprefixer"
3558 - },
3559 - {
3560 - "type": "github",
3561 - "url": "https://github.com/sponsors/ai"
3562 - }
3563 - ],
3564 - "dependencies": {
3565 - "browserslist": "^4.22.2",
3566 - "caniuse-lite": "^1.0.30001578",
3567 - "fraction.js": "^4.3.7",
3568 - "normalize-range": "^0.1.2",
3569 - "picocolors": "^1.0.0",
3570 - "postcss-value-parser": "^4.2.0"
3571 - },
3572 - "bin": {
3573 - "autoprefixer": "bin/autoprefixer"
3574 - },
3575 - "engines": {
3576 - "node": "^10 || ^12 || >=14"
3577 - },
3578 - "peerDependencies": {
3579 - "postcss": "^8.1.0"
3580 - }
3581 - },
3582 - "node_modules/available-typed-arrays": {
3583 - "version": "1.0.5",
3584 - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
3585 - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
3586 - "dev": true,
3587 - "engines": {
3588 - "node": ">= 0.4"
3589 - },
3590 - "funding": {
3591 - "url": "https://github.com/sponsors/ljharb"
3592 - }
3593 - },
3594 - "node_modules/aws-sign2": {
3595 - "version": "0.7.0",
3596 - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
3597 - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
3598 - "dev": true,
3599 - "engines": {
3600 - "node": "*"
3601 - }
3602 - },
3603 - "node_modules/aws4": {
3604 - "version": "1.12.0",
3605 - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz",
3606 - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==",
3607 - "dev": true
3608 - },
3609 - "node_modules/axios": {
3610 - "version": "1.6.7",
3611 - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
3612 - "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
3613 - "dev": true,
3614 - "dependencies": {
3615 - "follow-redirects": "^1.15.4",
3616 - "form-data": "^4.0.0",
3617 - "proxy-from-env": "^1.1.0"
3618 - }
3619 - },
3620 - "node_modules/axios/node_modules/form-data": {
3621 - "version": "4.0.0",
3622 - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
3623 - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
3624 - "dev": true,
3625 - "dependencies": {
3626 - "asynckit": "^0.4.0",
3627 - "combined-stream": "^1.0.8",
3628 - "mime-types": "^2.1.12"
3629 - },
3630 - "engines": {
3631 - "node": ">= 6"
3632 - }
3633 - },
3634 - "node_modules/axios/node_modules/proxy-from-env": {
3635 - "version": "1.1.0",
3636 - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
3637 - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
3638 - "dev": true
3639 - },
3640 - "node_modules/balanced-match": {
3641 - "version": "1.0.2",
3642 - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
3643 - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
3644 - },
3645 - "node_modules/base64-js": {
3646 - "version": "1.5.1",
3647 - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
3648 - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
3649 - "dev": true,
3650 - "funding": [
3651 - {
3652 - "type": "github",
3653 - "url": "https://github.com/sponsors/feross"
3654 - },
3655 - {
3656 - "type": "patreon",
3657 - "url": "https://www.patreon.com/feross"
3658 - },
3659 - {
3660 - "type": "consulting",
3661 - "url": "https://feross.org/support"
3662 - }
3663 - ]
3664 - },
3665 - "node_modules/bcrypt-pbkdf": {
3666 - "version": "1.0.2",
3667 - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
3668 - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
3669 - "dev": true,
3670 - "dependencies": {
3671 - "tweetnacl": "^0.14.3"
3672 - }
3673 - },
3674 - "node_modules/big-integer": {
3675 - "version": "1.6.52",
3676 - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
3677 - "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
3678 - "dev": true,
3679 - "engines": {
3680 - "node": ">=0.6"
3681 - }
3682 - },
3683 - "node_modules/binary-extensions": {
3684 - "version": "2.2.0",
3685 - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
3686 - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
3687 - "dev": true,
3688 - "engines": {
3689 - "node": ">=8"
3690 - }
3691 - },
3692 - "node_modules/blob-util": {
3693 - "version": "2.0.2",
3694 - "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz",
3695 - "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==",
3696 - "dev": true
3697 - },
3698 - "node_modules/bluebird": {
3699 - "version": "3.7.2",
3700 - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
3701 - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
3702 - "dev": true
3703 - },
3704 - "node_modules/boolbase": {
3705 - "version": "1.0.0",
3706 - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
3707 - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
3708 - },
3709 - "node_modules/bplist-parser": {
3710 - "version": "0.2.0",
3711 - "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz",
3712 - "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==",
3713 - "dev": true,
3714 - "dependencies": {
3715 - "big-integer": "^1.6.44"
3716 - },
3717 - "engines": {
3718 - "node": ">= 5.10.0"
3719 - }
3720 - },
3721 - "node_modules/brace-expansion": {
3722 - "version": "2.0.1",
3723 - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
3724 - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
3725 - "dependencies": {
3726 - "balanced-match": "^1.0.0"
3727 - }
3728 - },
3729 - "node_modules/braces": {
3730 - "version": "3.0.2",
3731 - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
3732 - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
3733 - "dependencies": {
3734 - "fill-range": "^7.0.1"
3735 - },
3736 - "engines": {
3737 - "node": ">=8"
3738 - }
3739 - },
3740 - "node_modules/browserslist": {
3741 - "version": "4.22.3",
3742 - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz",
3743 - "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==",
3744 - "dev": true,
3745 - "funding": [
3746 - {
3747 - "type": "opencollective",
3748 - "url": "https://opencollective.com/browserslist"
3749 - },
3750 - {
3751 - "type": "tidelift",
3752 - "url": "https://tidelift.com/funding/github/npm/browserslist"
3753 - },
3754 - {
3755 - "type": "github",
3756 - "url": "https://github.com/sponsors/ai"
3757 - }
3758 - ],
3759 - "dependencies": {
3760 - "caniuse-lite": "^1.0.30001580",
3761 - "electron-to-chromium": "^1.4.648",
3762 - "node-releases": "^2.0.14",
3763 - "update-browserslist-db": "^1.0.13"
3764 - },
3765 - "bin": {
3766 - "browserslist": "cli.js"
3767 - },
3768 - "engines": {
3769 - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
3770 - }
3771 - },
3772 - "node_modules/buffer": {
3773 - "version": "5.7.1",
3774 - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
3775 - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
3776 - "dev": true,
3777 - "funding": [
3778 - {
3779 - "type": "github",
3780 - "url": "https://github.com/sponsors/feross"
3781 - },
3782 - {
3783 - "type": "patreon",
3784 - "url": "https://www.patreon.com/feross"
3785 - },
3786 - {
3787 - "type": "consulting",
3788 - "url": "https://feross.org/support"
3789 - }
3790 - ],
3791 - "dependencies": {
3792 - "base64-js": "^1.3.1",
3793 - "ieee754": "^1.1.13"
3794 - }
3795 - },
3796 - "node_modules/buffer-crc32": {
3797 - "version": "0.2.13",
3798 - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
3799 - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
3800 - "dev": true,
3801 - "engines": {
3802 - "node": "*"
3803 - }
3804 - },
3805 - "node_modules/builtins": {
3806 - "version": "5.0.1",
3807 - "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz",
3808 - "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==",
3809 - "dev": true,
3810 - "dependencies": {
3811 - "semver": "^7.0.0"
3812 - }
3813 - },
3814 - "node_modules/bundle-name": {
3815 - "version": "3.0.0",
3816 - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz",
3817 - "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==",
3818 - "dev": true,
3819 - "dependencies": {
3820 - "run-applescript": "^5.0.0"
3821 - },
3822 - "engines": {
3823 - "node": ">=12"
3824 - },
3825 - "funding": {
3826 - "url": "https://github.com/sponsors/sindresorhus"
3827 - }
3828 - },
3829 - "node_modules/bytes": {
3830 - "version": "3.1.2",
3831 - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
3832 - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
3833 - "engines": {
3834 - "node": ">= 0.8"
3835 - }
3836 - },
3837 - "node_modules/cac": {
3838 - "version": "6.7.14",
3839 - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
3840 - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
3841 - "dev": true,
3842 - "engines": {
3843 - "node": ">=8"
3844 - }
3845 - },
3846 - "node_modules/cacache": {
3847 - "version": "18.0.2",
3848 - "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.2.tgz",
3849 - "integrity": "sha512-r3NU8h/P+4lVUHfeRw1dtgQYar3DZMm4/cm2bZgOvrFC/su7budSOeqh52VJIC4U4iG1WWwV6vRW0znqBvxNuw==",
3850 - "dev": true,
3851 - "dependencies": {
3852 - "@npmcli/fs": "^3.1.0",
3853 - "fs-minipass": "^3.0.0",
3854 - "glob": "^10.2.2",
3855 - "lru-cache": "^10.0.1",
3856 - "minipass": "^7.0.3",
3857 - "minipass-collect": "^2.0.1",
3858 - "minipass-flush": "^1.0.5",
3859 - "minipass-pipeline": "^1.2.4",
3860 - "p-map": "^4.0.0",
3861 - "ssri": "^10.0.0",
3862 - "tar": "^6.1.11",
3863 - "unique-filename": "^3.0.0"
3864 - },
3865 - "engines": {
3866 - "node": "^16.14.0 || >=18.0.0"
3867 - }
3868 - },
3869 - "node_modules/cacache/node_modules/lru-cache": {
3870 - "version": "10.2.0",
3871 - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
3872 - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==",
3873 - "dev": true,
3874 - "engines": {
3875 - "node": "14 || >=16.14"
3876 - }
3877 - },
3878 - "node_modules/cache-content-type": {
3879 - "version": "1.0.1",
3880 - "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz",
3881 - "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==",
3882 - "dev": true,
3883 - "dependencies": {
3884 - "mime-types": "^2.1.18",
3885 - "ylru": "^1.2.0"
3886 - },
3887 - "engines": {
3888 - "node": ">= 6.0.0"
3889 - }
3890 - },
3891 - "node_modules/cachedir": {
3892 - "version": "2.4.0",
3893 - "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz",
3894 - "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==",
3895 - "dev": true,
3896 - "engines": {
3897 - "node": ">=6"
3898 - }
3899 - },
3900 - "node_modules/call-bind": {
3901 - "version": "1.0.5",
3902 - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
3903 - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
3904 - "dev": true,
3905 - "dependencies": {
3906 - "function-bind": "^1.1.2",
3907 - "get-intrinsic": "^1.2.1",
3908 - "set-function-length": "^1.1.1"
3909 - },
3910 - "funding": {
3911 - "url": "https://github.com/sponsors/ljharb"
3912 - }
3913 - },
3914 - "node_modules/callsites": {
3915 - "version": "3.1.0",
3916 - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
3917 - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
3918 - "engines": {
3919 - "node": ">=6"
3920 - }
3921 - },
3922 - "node_modules/camelcase": {
3923 - "version": "6.3.0",
3924 - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
3925 - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
3926 - "dev": true,
3927 - "engines": {
3928 - "node": ">=10"
3929 - },
3930 - "funding": {
3931 - "url": "https://github.com/sponsors/sindresorhus"
3932 - }
3933 - },
3934 - "node_modules/camelcase-css": {
3935 - "version": "2.0.1",
3936 - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
3937 - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
3938 - "dev": true,
3939 - "engines": {
3940 - "node": ">= 6"
3941 - }
3942 - },
3943 - "node_modules/caniuse-lite": {
3944 - "version": "1.0.30001580",
3945 - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001580.tgz",
3946 - "integrity": "sha512-mtj5ur2FFPZcCEpXFy8ADXbDACuNFXg6mxVDqp7tqooX6l3zwm+d8EPoeOSIFRDvHs8qu7/SLFOGniULkcH2iA==",
3947 - "dev": true,
3948 - "funding": [
3949 - {
3950 - "type": "opencollective",
3951 - "url": "https://opencollective.com/browserslist"
3952 - },
3953 - {
3954 - "type": "tidelift",
3955 - "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
3956 - },
3957 - {
3958 - "type": "github",
3959 - "url": "https://github.com/sponsors/ai"
3960 - }
3961 - ]
3962 - },
3963 - "node_modules/caseless": {
3964 - "version": "0.12.0",
3965 - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
3966 - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
3967 - "dev": true
3968 - },
3969 - "node_modules/chai": {
3970 - "version": "4.4.1",
3971 - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz",
3972 - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==",
3973 - "dev": true,
3974 - "dependencies": {
3975 - "assertion-error": "^1.1.0",
3976 - "check-error": "^1.0.3",
3977 - "deep-eql": "^4.1.3",
3978 - "get-func-name": "^2.0.2",
3979 - "loupe": "^2.3.6",
3980 - "pathval": "^1.1.1",
3981 - "type-detect": "^4.0.8"
3982 - },
3983 - "engines": {
3984 - "node": ">=4"
3985 - }
3986 - },
3987 - "node_modules/chalk": {
3988 - "version": "2.4.2",
3989 - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
3990 - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
3991 - "dev": true,
3992 - "dependencies": {
3993 - "ansi-styles": "^3.2.1",
3994 - "escape-string-regexp": "^1.0.5",
3995 - "supports-color": "^5.3.0"
3996 - },
3997 - "engines": {
3998 - "node": ">=4"
3999 - }
4000 - },
4001 - "node_modules/check-error": {
4002 - "version": "1.0.3",
4003 - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
4004 - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==",
4005 - "dev": true,
4006 - "dependencies": {
4007 - "get-func-name": "^2.0.2"
4008 - },
4009 - "engines": {
4010 - "node": "*"
4011 - }
4012 - },
4013 - "node_modules/check-more-types": {
4014 - "version": "2.24.0",
4015 - "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",
4016 - "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==",
4017 - "dev": true,
4018 - "engines": {
4019 - "node": ">= 0.8.0"
4020 - }
4021 - },
4022 - "node_modules/chokidar": {
4023 - "version": "3.5.3",
4024 - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
4025 - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
4026 - "dev": true,
4027 - "funding": [
4028 - {
4029 - "type": "individual",
4030 - "url": "https://paulmillr.com/funding/"
4031 - }
4032 - ],
4033 - "dependencies": {
4034 - "anymatch": "~3.1.2",
4035 - "braces": "~3.0.2",
4036 - "glob-parent": "~5.1.2",
4037 - "is-binary-path": "~2.1.0",
4038 - "is-glob": "~4.0.1",
4039 - "normalize-path": "~3.0.0",
4040 - "readdirp": "~3.6.0"
4041 - },
4042 - "engines": {
4043 - "node": ">= 8.10.0"
4044 - },
4045 - "optionalDependencies": {
4046 - "fsevents": "~2.3.2"
4047 - }
4048 - },
4049 - "node_modules/chokidar/node_modules/glob-parent": {
4050 - "version": "5.1.2",
4051 - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
4052 - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
4053 - "dev": true,
4054 - "dependencies": {
4055 - "is-glob": "^4.0.1"
4056 - },
4057 - "engines": {
4058 - "node": ">= 6"
4059 - }
4060 - },
4061 - "node_modules/chownr": {
4062 - "version": "2.0.0",
4063 - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
4064 - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
4065 - "dev": true,
4066 - "engines": {
4067 - "node": ">=10"
4068 - }
4069 - },
4070 - "node_modules/ci-info": {
4071 - "version": "3.9.0",
4072 - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
4073 - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
4074 - "dev": true,
4075 - "funding": [
4076 - {
4077 - "type": "github",
4078 - "url": "https://github.com/sponsors/sibiraj-s"
4079 - }
4080 - ],
4081 - "engines": {
4082 - "node": ">=8"
4083 - }
4084 - },
4085 - "node_modules/classnames": {
4086 - "version": "2.5.1",
4087 - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
4088 - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
4089 - },
4090 - "node_modules/clean-stack": {
4091 - "version": "2.2.0",
4092 - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
4093 - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
4094 - "dev": true,
4095 - "engines": {
4096 - "node": ">=6"
4097 - }
4098 - },
4099 - "node_modules/cli-cursor": {
4100 - "version": "3.1.0",
4101 - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
4102 - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
4103 - "dev": true,
4104 - "dependencies": {
4105 - "restore-cursor": "^3.1.0"
4106 - },
4107 - "engines": {
4108 - "node": ">=8"
4109 - }
4110 - },
4111 - "node_modules/cli-progress": {
4112 - "version": "3.12.0",
4113 - "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz",
4114 - "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==",
4115 - "dev": true,
4116 - "dependencies": {
4117 - "string-width": "^4.2.3"
4118 - },
4119 - "engines": {
4120 - "node": ">=4"
4121 - }
4122 - },
4123 - "node_modules/cli-table3": {
4124 - "version": "0.6.3",
4125 - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz",
4126 - "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==",
4127 - "dev": true,
4128 - "dependencies": {
4129 - "string-width": "^4.2.0"
4130 - },
4131 - "engines": {
4132 - "node": "10.* || >= 12.*"
4133 - },
4134 - "optionalDependencies": {
4135 - "@colors/colors": "1.5.0"
4136 - }
4137 - },
4138 - "node_modules/cli-truncate": {
4139 - "version": "2.1.0",
4140 - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
4141 - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
4142 - "dev": true,
4143 - "dependencies": {
4144 - "slice-ansi": "^3.0.0",
4145 - "string-width": "^4.2.0"
4146 - },
4147 - "engines": {
4148 - "node": ">=8"
4149 - },
4150 - "funding": {
4151 - "url": "https://github.com/sponsors/sindresorhus"
4152 - }
4153 - },
4154 - "node_modules/cliui": {
4155 - "version": "8.0.1",
4156 - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
4157 - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
4158 - "dev": true,
4159 - "dependencies": {
4160 - "string-width": "^4.2.0",
4161 - "strip-ansi": "^6.0.1",
4162 - "wrap-ansi": "^7.0.0"
4163 - },
4164 - "engines": {
4165 - "node": ">=12"
4166 - }
4167 - },
4168 - "node_modules/cliui/node_modules/ansi-styles": {
4169 - "version": "4.3.0",
4170 - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
4171 - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
4172 - "dev": true,
4173 - "dependencies": {
4174 - "color-convert": "^2.0.1"
4175 - },
4176 - "engines": {
4177 - "node": ">=8"
4178 - },
4179 - "funding": {
4180 - "url": "https://github.com/chalk/ansi-styles?sponsor=1"
4181 - }
4182 - },
4183 - "node_modules/cliui/node_modules/color-convert": {
4184 - "version": "2.0.1",
4185 - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
4186 - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
4187 - "dev": true,
4188 - "dependencies": {
4189 - "color-name": "~1.1.4"
4190 - },
4191 - "engines": {
4192 - "node": ">=7.0.0"
4193 - }
4194 - },
4195 - "node_modules/cliui/node_modules/color-name": {
4196 - "version": "1.1.4",
4197 - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
4198 - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
4199 - "dev": true
4200 - },
4201 - "node_modules/cliui/node_modules/wrap-ansi": {
4202 - "version": "7.0.0",
4203 - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
4204 - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
4205 - "dev": true,
4206 - "dependencies": {
4207 - "ansi-styles": "^4.0.0",
4208 - "string-width": "^4.1.0",
4209 - "strip-ansi": "^6.0.0"
4210 - },
4211 - "engines": {
4212 - "node": ">=10"
4213 - },
4214 - "funding": {
4215 - "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
4216 - }
4217 - },
4218 - "node_modules/co": {
4219 - "version": "4.6.0",
4220 - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
4221 - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
4222 - "dev": true,
4223 - "engines": {
4224 - "iojs": ">= 1.0.0",
4225 - "node": ">= 0.12.0"
4226 - }
4227 - },
4228 - "node_modules/color-convert": {
4229 - "version": "1.9.3",
4230 - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
4231 - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
4232 - "dev": true,
4233 - "dependencies": {
4234 - "color-name": "1.1.3"
4235 - }
4236 - },
4237 - "node_modules/color-name": {
4238 - "version": "1.1.3",
4239 - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
4240 - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
4241 - "dev": true
4242 - },
4243 - "node_modules/colord": {
4244 - "version": "2.9.3",
4245 - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
4246 - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="
4247 - },
4248 - "node_modules/colorette": {
4249 - "version": "2.0.20",
4250 - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
4251 - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
4252 - "dev": true
4253 - },
4254 - "node_modules/combined-stream": {
4255 - "version": "1.0.8",
4256 - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
4257 - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
4258 - "dev": true,
4259 - "dependencies": {
4260 - "delayed-stream": "~1.0.0"
4261 - },
4262 - "engines": {
4263 - "node": ">= 0.8"
4264 - }
4265 - },
4266 - "node_modules/commander": {
4267 - "version": "6.2.1",
4268 - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
4269 - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
4270 - "dev": true,
4271 - "engines": {
4272 - "node": ">= 6"
4273 - }
4274 - },
4275 - "node_modules/common-tags": {
4276 - "version": "1.8.2",
4277 - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz",
4278 - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==",
4279 - "dev": true,
4280 - "engines": {
4281 - "node": ">=4.0.0"
4282 - }
4283 - },
4284 - "node_modules/computeds": {
4285 - "version": "0.0.1",
4286 - "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz",
4287 - "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==",
4288 - "dev": true
4289 - },
4290 - "node_modules/concat-map": {
4291 - "version": "0.0.1",
4292 - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
4293 - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
4294 - },
4295 - "node_modules/config-chain": {
4296 - "version": "1.1.13",
4297 - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
4298 - "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
4299 - "dev": true,
4300 - "dependencies": {
4301 - "ini": "^1.3.4",
4302 - "proto-list": "~1.2.1"
4303 - }
4304 - },
4305 - "node_modules/config-chain/node_modules/ini": {
4306 - "version": "1.3.8",
4307 - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
4308 - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
4309 - "dev": true
4310 - },
4311 - "node_modules/content-disposition": {
4312 - "version": "0.5.4",
4313 - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
4314 - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
4315 - "dev": true,
4316 - "dependencies": {
4317 - "safe-buffer": "5.2.1"
4318 - },
4319 - "engines": {
4320 - "node": ">= 0.6"
4321 - }
4322 - },
4323 - "node_modules/content-type": {
4324 - "version": "1.0.5",
4325 - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
4326 - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
4327 - "dev": true,
4328 - "engines": {
4329 - "node": ">= 0.6"
4330 - }
4331 - },
4332 - "node_modules/convert-source-map": {
4333 - "version": "2.0.0",
4334 - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
4335 - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
4336 - "dev": true
4337 - },
4338 - "node_modules/cookies": {
4339 - "version": "0.9.1",
4340 - "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz",
4341 - "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==",
4342 - "dev": true,
4343 - "dependencies": {
4344 - "depd": "~2.0.0",
4345 - "keygrip": "~1.1.0"
4346 - },
4347 - "engines": {
4348 - "node": ">= 0.8"
4349 - }
4350 - },
4351 - "node_modules/core-util-is": {
4352 - "version": "1.0.2",
4353 - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
4354 - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
4355 - "dev": true
4356 - },
4357 - "node_modules/create-require": {
4358 - "version": "1.1.1",
4359 - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
4360 - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
4361 - "dev": true
4362 - },
4363 - "node_modules/cross-spawn": {
4364 - "version": "7.0.3",
4365 - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
4366 - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
4367 - "dependencies": {
4368 - "path-key": "^3.1.0",
4369 - "shebang-command": "^2.0.0",
4370 - "which": "^2.0.1"
4371 - },
4372 - "engines": {
4373 - "node": ">= 8"
4374 - }
4375 - },
4376 - "node_modules/crypto-js": {
4377 - "version": "4.2.0",
4378 - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
4379 - "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
4380 - },
4381 - "node_modules/css-render": {
4382 - "version": "0.15.12",
4383 - "resolved": "https://registry.npmjs.org/css-render/-/css-render-0.15.12.tgz",
4384 - "integrity": "sha512-eWzS66patiGkTTik+ipO9qNGZ+uNuGyTmnz6/+EJIiFg8+3yZRpnMwgFo8YdXhQRsiePzehnusrxVvugNjXzbw==",
4385 - "dependencies": {
4386 - "@emotion/hash": "~0.8.0",
4387 - "csstype": "~3.0.5"
4388 - }
4389 - },
4390 - "node_modules/css-render/node_modules/csstype": {
4391 - "version": "3.0.11",
4392 - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz",
4393 - "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw=="
4394 - },
4395 - "node_modules/css-select": {
4396 - "version": "5.1.0",
4397 - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
4398 - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
4399 - "dev": true,
4400 - "dependencies": {
4401 - "boolbase": "^1.0.0",
4402 - "css-what": "^6.1.0",
4403 - "domhandler": "^5.0.2",
4404 - "domutils": "^3.0.1",
4405 - "nth-check": "^2.0.1"
4406 - },
4407 - "funding": {
4408 - "url": "https://github.com/sponsors/fb55"
4409 - }
4410 - },
4411 - "node_modules/css-tree": {
4412 - "version": "2.3.1",
4413 - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
4414 - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
4415 - "dev": true,
4416 - "dependencies": {
4417 - "mdn-data": "2.0.30",
4418 - "source-map-js": "^1.0.1"
4419 - },
4420 - "engines": {
4421 - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
4422 - }
4423 - },
4424 - "node_modules/css-what": {
4425 - "version": "6.1.0",
4426 - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
4427 - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
4428 - "dev": true,
4429 - "engines": {
4430 - "node": ">= 6"
4431 - },
4432 - "funding": {
4433 - "url": "https://github.com/sponsors/fb55"
4434 - }
4435 - },
4436 - "node_modules/cssesc": {
4437 - "version": "3.0.0",
4438 - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
4439 - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
4440 - "bin": {
4441 - "cssesc": "bin/cssesc"
4442 - },
4443 - "engines": {
4444 - "node": ">=4"
4445 - }
4446 - },
4447 - "node_modules/csso": {
4448 - "version": "5.0.5",
4449 - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
4450 - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
4451 - "dev": true,
4452 - "dependencies": {
4453 - "css-tree": "~2.2.0"
4454 - },
4455 - "engines": {
4456 - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
4457 - "npm": ">=7.0.0"
4458 - }
4459 - },
4460 - "node_modules/csso/node_modules/css-tree": {
4461 - "version": "2.2.1",
4462 - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
4463 - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
4464 - "dev": true,
4465 - "dependencies": {
4466 - "mdn-data": "2.0.28",
4467 - "source-map-js": "^1.0.1"
4468 - },
4469 - "engines": {
4470 - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
4471 - "npm": ">=7.0.0"
4472 - }
4473 - },
4474 - "node_modules/csso/node_modules/mdn-data": {
4475 - "version": "2.0.28",
4476 - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
4477 - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
4478 - "dev": true
4479 - },
4480 - "node_modules/cssstyle": {
4481 - "version": "4.0.1",
4482 - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz",
4483 - "integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==",
4484 - "dev": true,
4485 - "dependencies": {
4486 - "rrweb-cssom": "^0.6.0"
4487 - },
4488 - "engines": {
4489 - "node": ">=18"
4490 - }
4491 - },
4492 - "node_modules/csstype": {
4493 - "version": "3.1.3",
4494 - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
4495 - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
4496 - },
4497 - "node_modules/cypress": {
4498 - "version": "13.6.4",
4499 - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.4.tgz",
4500 - "integrity": "sha512-pYJjCfDYB+hoOoZuhysbbYhEmNW7DEDsqn+ToCLwuVowxUXppIWRr7qk4TVRIU471ksfzyZcH+mkoF0CQUKnpw==",
4501 - "dev": true,
4502 - "hasInstallScript": true,
4503 - "dependencies": {
4504 - "@cypress/request": "^3.0.0",
4505 - "@cypress/xvfb": "^1.2.4",
4506 - "@types/sinonjs__fake-timers": "8.1.1",
4507 - "@types/sizzle": "^2.3.2",
4508 - "arch": "^2.2.0",
4509 - "blob-util": "^2.0.2",
4510 - "bluebird": "^3.7.2",
4511 - "buffer": "^5.6.0",
4512 - "cachedir": "^2.3.0",
4513 - "chalk": "^4.1.0",
4514 - "check-more-types": "^2.24.0",
4515 - "cli-cursor": "^3.1.0",
4516 - "cli-table3": "~0.6.1",
4517 - "commander": "^6.2.1",
4518 - "common-tags": "^1.8.0",
4519 - "dayjs": "^1.10.4",
4520 - "debug": "^4.3.4",
4521 - "enquirer": "^2.3.6",
4522 - "eventemitter2": "6.4.7",
4523 - "execa": "4.1.0",
4524 - "executable": "^4.1.1",
4525 - "extract-zip": "2.0.1",
4526 - "figures": "^3.2.0",
4527 - "fs-extra": "^9.1.0",
4528 - "getos": "^3.2.1",
4529 - "is-ci": "^3.0.0",
4530 - "is-installed-globally": "~0.4.0",
4531 - "lazy-ass": "^1.6.0",
4532 - "listr2": "^3.8.3",
4533 - "lodash": "^4.17.21",
4534 - "log-symbols": "^4.0.0",
4535 - "minimist": "^1.2.8",
4536 - "ospath": "^1.2.2",
4537 - "pretty-bytes": "^5.6.0",
4538 - "process": "^0.11.10",
4539 - "proxy-from-env": "1.0.0",
4540 - "request-progress": "^3.0.0",
4541 - "semver": "^7.5.3",
4542 - "supports-color": "^8.1.1",
4543 - "tmp": "~0.2.1",
4544 - "untildify": "^4.0.0",
4545 - "yauzl": "^2.10.0"
4546 - },
4547 - "bin": {
4548 - "cypress": "bin/cypress"
4549 - },
4550 - "engines": {
4551 - "node": "^16.0.0 || ^18.0.0 || >=20.0.0"
4552 - }
4553 - },
4554 - "node_modules/cypress/node_modules/ansi-styles": {
4555 - "version": "4.3.0",
4556 - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
4557 - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
4558 - "dev": true,
4559 - "dependencies": {
4560 - "color-convert": "^2.0.1"
4561 - },
4562 - "engines": {
4563 - "node": ">=8"
4564 - },
4565 - "funding": {
4566 - "url": "https://github.com/chalk/ansi-styles?sponsor=1"
4567 - }
4568 - },
4569 - "node_modules/cypress/node_modules/chalk": {
4570 - "version": "4.1.2",
4571 - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
4572 - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
4573 - "dev": true,
4574 - "dependencies": {
4575 - "ansi-styles": "^4.1.0",
4576 - "supports-color": "^7.1.0"
4577 - },
4578 - "engines": {
4579 - "node": ">=10"
4580 - },
4581 - "funding": {
4582 - "url": "https://github.com/chalk/chalk?sponsor=1"
4583 - }
4584 - },
4585 - "node_modules/cypress/node_modules/chalk/node_modules/supports-color": {
4586 - "version": "7.2.0",
4587 - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
4588 - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
4589 - "dev": true,
4590 - "dependencies": {
4591 - "has-flag": "^4.0.0"
4592 - },
4593 - "engines": {
4594 - "node": ">=8"
4595 - }
4596 - },
4597 - "node_modules/cypress/node_modules/color-convert": {
4598 - "version": "2.0.1",
4599 - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
4600 - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
4601 - "dev": true,
4602 - "dependencies": {
4603 - "color-name": "~1.1.4"
4604 - },
4605 - "engines": {
4606 - "node": ">=7.0.0"
4607 - }
4608 - },
4609 - "node_modules/cypress/node_modules/color-name": {
4610 - "version": "1.1.4",
4611 - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
4612 - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
4613 - "dev": true
4614 - },
4615 - "node_modules/cypress/node_modules/fs-extra": {
4616 - "version": "9.1.0",
4617 - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
4618 - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
4619 - "dev": true,
4620 - "dependencies": {
4621 - "at-least-node": "^1.0.0",
4622 - "graceful-fs": "^4.2.0",
4623 - "jsonfile": "^6.0.1",
4624 - "universalify": "^2.0.0"
4625 - },
4626 - "engines": {
4627 - "node": ">=10"
4628 - }
4629 - },
4630 - "node_modules/cypress/node_modules/has-flag": {
4631 - "version": "4.0.0",
4632 - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
4633 - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
4634 - "dev": true,
4635 - "engines": {
4636 - "node": ">=8"
4637 - }
4638 - },
4639 - "node_modules/cypress/node_modules/supports-color": {
4640 - "version": "8.1.1",
4641 - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
4642 - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
4643 - "dev": true,
4644 - "dependencies": {
4645 - "has-flag": "^4.0.0"
4646 - },
4647 - "engines": {
4648 - "node": ">=10"
4649 - },
4650 - "funding": {
4651 - "url": "https://github.com/chalk/supports-color?sponsor=1"
4652 - }
4653 - },
4654 - "node_modules/dashdash": {
4655 - "version": "1.14.1",
4656 - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
4657 - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
4658 - "dev": true,
4659 - "dependencies": {
4660 - "assert-plus": "^1.0.0"
4661 - },
4662 - "engines": {
4663 - "node": ">=0.10"
4664 - }
4665 - },
4666 - "node_modules/data-urls": {
4667 - "version": "5.0.0",
4668 - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
4669 - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
4670 - "dev": true,
4671 - "dependencies": {
4672 - "whatwg-mimetype": "^4.0.0",
4673 - "whatwg-url": "^14.0.0"
4674 - },
4675 - "engines": {
4676 - "node": ">=18"
4677 - }
4678 - },
4679 - "node_modules/date-fns": {
4680 - "version": "2.30.0",
4681 - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
4682 - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
4683 - "dependencies": {
4684 - "@babel/runtime": "^7.21.0"
4685 - },
4686 - "engines": {
4687 - "node": ">=0.11"
4688 - },
4689 - "funding": {
4690 - "type": "opencollective",
4691 - "url": "https://opencollective.com/date-fns"
4692 - }
4693 - },
4694 - "node_modules/date-fns-tz": {
4695 - "version": "2.0.0",
4696 - "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-2.0.0.tgz",
4697 - "integrity": "sha512-OAtcLdB9vxSXTWHdT8b398ARImVwQMyjfYGkKD2zaGpHseG2UPHbHjXELReErZFxWdSLph3c2zOaaTyHfOhERQ==",
4698 - "peerDependencies": {
4699 - "date-fns": ">=2.0.0"
4700 - }
4701 - },
4702 - "node_modules/dayjs": {
4703 - "version": "1.11.10",
4704 - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
4705 - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
4706 - },
4707 - "node_modules/de-indent": {
4708 - "version": "1.0.2",
4709 - "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
4710 - "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
4711 - "dev": true
4712 - },
4713 - "node_modules/debounce": {
4714 - "version": "1.2.1",
4715 - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
4716 - "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="
4717 - },
4718 - "node_modules/debug": {
4719 - "version": "4.3.4",
4720 - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
4721 - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
4722 - "dependencies": {
4723 - "ms": "2.1.2"
4724 - },
4725 - "engines": {
4726 - "node": ">=6.0"
4727 - },
4728 - "peerDependenciesMeta": {
4729 - "supports-color": {
4730 - "optional": true
4731 - }
4732 - }
4733 - },
4734 - "node_modules/decimal.js": {
4735 - "version": "10.4.3",
4736 - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
4737 - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==",
4738 - "dev": true
4739 - },
4740 - "node_modules/deep-eql": {
4741 - "version": "4.1.3",
4742 - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
4743 - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==",
4744 - "dev": true,
4745 - "dependencies": {
4746 - "type-detect": "^4.0.0"
4747 - },
4748 - "engines": {
4749 - "node": ">=6"
4750 - }
4751 - },
4752 - "node_modules/deep-equal": {
4753 - "version": "1.0.1",
4754 - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
4755 - "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==",
4756 - "dev": true
4757 - },
4758 - "node_modules/deep-is": {
4759 - "version": "0.1.4",
4760 - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
4761 - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
4762 - },
4763 - "node_modules/deepmerge": {
4764 - "version": "4.3.1",
4765 - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
4766 - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
4767 - "dev": true,
4768 - "engines": {
4769 - "node": ">=0.10.0"
4770 - }
4771 - },
4772 - "node_modules/default-browser": {
4773 - "version": "4.0.0",
4774 - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz",
4775 - "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==",
4776 - "dev": true,
4777 - "dependencies": {
4778 - "bundle-name": "^3.0.0",
4779 - "default-browser-id": "^3.0.0",
4780 - "execa": "^7.1.1",
4781 - "titleize": "^3.0.0"
4782 - },
4783 - "engines": {
4784 - "node": ">=14.16"
4785 - },
4786 - "funding": {
4787 - "url": "https://github.com/sponsors/sindresorhus"
4788 - }
4789 - },
4790 - "node_modules/default-browser-id": {
4791 - "version": "3.0.0",
4792 - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz",
4793 - "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==",
4794 - "dev": true,
4795 - "dependencies": {
4796 - "bplist-parser": "^0.2.0",
4797 - "untildify": "^4.0.0"
4798 - },
4799 - "engines": {
4800 - "node": ">=12"
4801 - },
4802 - "funding": {
4803 - "url": "https://github.com/sponsors/sindresorhus"
4804 - }
4805 - },
4806 - "node_modules/default-browser/node_modules/execa": {
4807 - "version": "7.2.0",
4808 - "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz",
4809 - "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==",
4810 - "dev": true,
4811 - "dependencies": {
4812 - "cross-spawn": "^7.0.3",
4813 - "get-stream": "^6.0.1",
4814 - "human-signals": "^4.3.0",
4815 - "is-stream": "^3.0.0",
4816 - "merge-stream": "^2.0.0",
4817 - "npm-run-path": "^5.1.0",
4818 - "onetime": "^6.0.0",
4819 - "signal-exit": "^3.0.7",
4820 - "strip-final-newline": "^3.0.0"
4821 - },
4822 - "engines": {
4823 - "node": "^14.18.0 || ^16.14.0 || >=18.0.0"
4824 - },
4825 - "funding": {
4826 - "url": "https://github.com/sindresorhus/execa?sponsor=1"
4827 - }
4828 - },
4829 - "node_modules/default-browser/node_modules/get-stream": {
4830 - "version": "6.0.1",
4831 - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
4832 - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
4833 - "dev": true,
4834 - "engines": {
4835 - "node": ">=10"
4836 - },
4837 - "funding": {
4838 - "url": "https://github.com/sponsors/sindresorhus"
4839 - }
4840 - },
4841 - "node_modules/default-browser/node_modules/human-signals": {
4842 - "version": "4.3.1",
4843 - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz",
4844 - "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==",
4845 - "dev": true,
4846 - "engines": {
4847 - "node": ">=14.18.0"
4848 - }
4849 - },
4850 - "node_modules/default-browser/node_modules/is-stream": {
4851 - "version": "3.0.0",
4852 - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
4853 - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
4854 - "dev": true,
4855 - "engines": {
4856 - "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
4857 - },
4858 - "funding": {
4859 - "url": "https://github.com/sponsors/sindresorhus"
4860 - }
4861 - },
4862 - "node_modules/default-browser/node_modules/mimic-fn": {
4863 - "version": "4.0.0",
4864 - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
4865 - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
4866 - "dev": true,
4867 - "engines": {
4868 - "node": ">=12"
4869 - },
4870 - "funding": {
4871 - "url": "https://github.com/sponsors/sindresorhus"
4872 - }
4873 - },
4874 - "node_modules/default-browser/node_modules/npm-run-path": {
4875 - "version": "5.2.0",
4876 - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz",
4877 - "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==",
4878 - "dev": true,
4879 - "dependencies": {
4880 - "path-key": "^4.0.0"
4881 - },
4882 - "engines": {
4883 - "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
4884 - },
4885 - "funding": {
4886 - "url": "https://github.com/sponsors/sindresorhus"
4887 - }
4888 - },
4889 - "node_modules/default-browser/node_modules/onetime": {
4890 - "version": "6.0.0",
4891 - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
4892 - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
4893 - "dev": true,
4894 - "dependencies": {
4895 - "mimic-fn": "^4.0.0"
4896 - },
4897 - "engines": {
4898 - "node": ">=12"
4899 - },
4900 - "funding": {
4901 - "url": "https://github.com/sponsors/sindresorhus"
4902 - }
4903 - },
4904 - "node_modules/default-browser/node_modules/path-key": {
4905 - "version": "4.0.0",
4906 - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
4907 - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
4908 - "dev": true,
4909 - "engines": {
4910 - "node": ">=12"
4911 - },
4912 - "funding": {
4913 - "url": "https://github.com/sponsors/sindresorhus"
4914 - }
4915 - },
4916 - "node_modules/default-browser/node_modules/strip-final-newline": {
4917 - "version": "3.0.0",
4918 - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
4919 - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
4920 - "dev": true,
4921 - "engines": {
4922 - "node": ">=12"
4923 - },
4924 - "funding": {
4925 - "url": "https://github.com/sponsors/sindresorhus"
4926 - }
4927 - },
4928 - "node_modules/define-data-property": {
4929 - "version": "1.1.1",
4930 - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
4931 - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
4932 - "dev": true,
4933 - "dependencies": {
4934 - "get-intrinsic": "^1.2.1",
4935 - "gopd": "^1.0.1",
4936 - "has-property-descriptors": "^1.0.0"
4937 - },
4938 - "engines": {
4939 - "node": ">= 0.4"
4940 - }
4941 - },
4942 - "node_modules/define-lazy-prop": {
4943 - "version": "2.0.0",
4944 - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
4945 - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
4946 - "dev": true,
4947 - "engines": {
4948 - "node": ">=8"
4949 - }
4950 - },
4951 - "node_modules/define-properties": {
4952 - "version": "1.2.1",
4953 - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
4954 - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
4955 - "dev": true,
4956 - "dependencies": {
4957 - "define-data-property": "^1.0.1",
4958 - "has-property-descriptors": "^1.0.0",
4959 - "object-keys": "^1.1.1"
4960 - },
4961 - "engines": {
4962 - "node": ">= 0.4"
4963 - },
4964 - "funding": {
4965 - "url": "https://github.com/sponsors/ljharb"
4966 - }
4967 - },
4968 - "node_modules/defu": {
4969 - "version": "6.1.4",
4970 - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
4971 - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="
4972 - },
4973 - "node_modules/delayed-stream": {
4974 - "version": "1.0.0",
4975 - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
4976 - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
4977 - "dev": true,
4978 - "engines": {
4979 - "node": ">=0.4.0"
4980 - }
4981 - },
4982 - "node_modules/delegates": {
4983 - "version": "1.0.0",
4984 - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
4985 - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
4986 - "dev": true
4987 - },
4988 - "node_modules/depd": {
4989 - "version": "2.0.0",
4990 - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
4991 - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
4992 - "dev": true,
4993 - "engines": {
4994 - "node": ">= 0.8"
4995 - }
4996 - },
4997 - "node_modules/destroy": {
4998 - "version": "1.2.0",
4999 - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",

This file is too large to show in full.

vercel.json deleted
-8
@@ -1,8 +0,0 @@
1 -{
2 - "rewrites": [
3 - {
4 - "source": "/(.*)",
5 - "destination": "/index.html"
6 - }
7 - ]
8 -}