@cryptotaxi247 / CoPilot / commits / 862a706d

some precommit fixes

Taylor committed Oct 4, 2023 at 16:54 UTC 862a706ddcf0222769b0a0285329c1f8a70966df
113 files changed +2849 -1530
.gitignore
+1 -1
@@ -41,4 +41,4 @@ wheels/
41 .env
42 *.sqbpro
43 site/
44 -backend/app/file-store/api.config.yaml
44 +backend/file-store/api.config.yaml
.pre-commit-config.yaml
+4 -4
@@ -43,10 +43,10 @@ repos:
43 - id: flake8
44 name: Check project styling
45
46 - # - repo: https://github.com/pre-commit/mirrors-prettier
47 - # rev: "v3.0.0"
48 - # hooks:
49 - # - id: prettier
46 + - repo: https://github.com/pre-commit/mirrors-prettier
47 + rev: "v3.0.0"
48 + hooks:
49 + - id: prettier
50
51 # - repo: https://github.com/pre-commit/mirrors-eslint
52 # rev: v8.41.0
backend/app/agents/routes/agents.py
+50 -13
@@ -1,32 +1,52 @@
1 from typing import List
2 -from fastapi import APIRouter, HTTPException, Security, Depends
3 -from starlette.status import HTTP_401_UNAUTHORIZED
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 starlette.status import HTTP_401_UNAUTHORIZED
9 +
10 +from app.agents.schema.agents import AgentModifyResponse
11 +from app.agents.schema.agents import AgentsResponse
12 +from app.agents.schema.agents import AgentUpdateCustomerCodeBody
13 +from app.agents.schema.agents import AgentUpdateCustomerCodeResponse
14 +from app.agents.schema.agents import OutdatedVelociraptorAgentsResponse
15 +from app.agents.schema.agents import OutdatedWazuhAgentsResponse
16 +from app.agents.schema.agents import SyncedAgent
17 +from app.agents.schema.agents import SyncedAgentsResponse
18 +from app.agents.services.modify import delete_agent_db
19 +from app.agents.services.modify import delete_agent_wazuh
20 +from app.agents.services.modify import mark_agent_criticality
21 +from app.agents.services.status import get_outdated_agents_velociraptor
22 +from app.agents.services.status import get_outdated_agents_wazuh
23 +from app.agents.services.sync import sync_agents
24 +from app.agents.velociraptor.services.agents import delete_agent_velociraptor
25 +from app.agents.wazuh.schema.agents import WazuhAgent
26 +from app.agents.wazuh.schema.agents import WazuhAgentsList
27 +from app.agents.wazuh.schema.agents import WazuhAgentVulnerabilitiesResponse
28 +from app.agents.wazuh.services.vulnerabilities import collect_agent_vulnerabilities
29
30 # App specific imports
31 from app.auth.routes.auth import auth_handler
32 from app.db.db_session import session
33 from app.db.universal_models import Agents
10 -from app.agents.schema.agents import AgentsResponse, SyncedAgentsResponse, SyncedAgent, AgentModifyResponse, OutdatedWazuhAgentsResponse, OutdatedVelociraptorAgentsResponse, AgentUpdateCustomerCodeBody, AgentUpdateCustomerCodeResponse
11 -from app.agents.wazuh.schema.agents import WazuhAgent, WazuhAgentsList, WazuhAgentVulnerabilitiesResponse
12 -from app.agents.services.sync import sync_agents
13 -from app.agents.services.modify import mark_agent_criticality, delete_agent_db, delete_agent_wazuh
14 -from app.agents.wazuh.services.vulnerabilities import collect_agent_vulnerabilities
15 -from app.agents.services.status import get_outdated_agents_wazuh, get_outdated_agents_velociraptor
16 -from app.agents.velociraptor.services.agents import delete_agent_velociraptor
34
35 agents_router = APIRouter()
36
37 +
38 def verify_admin(user):
39 if not user.is_admin:
40 raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Unauthorized")
41
42 +
43 @agents_router.get("", response_model=AgentsResponse, description="Get all disabled rules")
44 async def get_agents() -> AgentsResponse:
45 logger.info(f"Fetching all agents")
46 agents = session.query(Agents).all()
47 return AgentsResponse(agents=agents, success=True, message="Agents fetched successfully")
48
49 +
50 @agents_router.get("/{agent_id}", response_model=AgentsResponse, description="Get agent by agent_id")
51 async def get_agent(agent_id: str) -> AgentsResponse:
52 logger.info(f"Fetching agent with agent_id: {agent_id}")
@@ -35,6 +55,7 @@ async def get_agent(agent_id: str) -> AgentsResponse:
55 raise HTTPException(status_code=404, detail=f"Agent with agent_id {agent_id} not found")
56 return AgentsResponse(agents=[agent], success=True, message="Agent fetched successfully")
57
58 +
59 @agents_router.get("/hostname/{hostname}", response_model=AgentsResponse, description="Get agent by hostname")
60 async def get_agent_by_hostname(hostname: str) -> AgentsResponse:
61 logger.info(f"Fetching agent with hostname: {hostname}")
@@ -43,46 +64,62 @@ async def get_agent_by_hostname(hostname: str) -> AgentsResponse:
64 raise HTTPException(status_code=404, detail=f"Agent with hostname {hostname} not found")
65 return AgentsResponse(agents=[agent], success=True, message="Agent fetched successfully")
66
67 +
68 @agents_router.post("/sync", response_model=SyncedAgentsResponse, description="Sync agents from Wazuh Manager")
69 async def sync_all_agents() -> SyncedAgentsResponse:
70 logger.info("Syncing agents from Wazuh Manager")
71 return sync_agents()
72
73 +
74 @agents_router.post("/{agent_id}/critical", response_model=AgentModifyResponse, description="Mark agent as critical")
75 async def mark_agent_as_critical(agent_id: str) -> AgentModifyResponse:
76 logger.info(f"Marking agent {agent_id} as critical")
77 return mark_agent_criticality(agent_id, True)
78
79 +
80 @agents_router.post("/{agent_id}/noncritical", response_model=AgentModifyResponse, description="Mark agent as not critical")
81 async def mark_agent_as_not_critical(agent_id: str) -> AgentModifyResponse:
82 logger.info(f"Marking agent {agent_id} as not critical")
83 return mark_agent_criticality(agent_id, False)
84
85 +
86 @agents_router.get("/{agent_id}/vulnerabilities", response_model=WazuhAgentVulnerabilitiesResponse, description="Get agent vulnerabilities")
87 async def get_agent_vulnerabilities(agent_id: str) -> WazuhAgentVulnerabilitiesResponse:
88 logger.info(f"Fetching agent {agent_id} vulnerabilities")
89 return collect_agent_vulnerabilities(agent_id)
90
91 +
92 @agents_router.get("/wazuh/outdated", response_model=OutdatedWazuhAgentsResponse, description="Get all outdated Wazuh agents")
93 async def get_outdated_wazuh_agents() -> OutdatedWazuhAgentsResponse:
94 logger.info(f"Fetching all outdated Wazuh agents")
95 return get_outdated_agents_wazuh()
96
71 -@agents_router.get("/velociraptor/outdated", response_model=OutdatedVelociraptorAgentsResponse, description="Get all outdated Velociraptor agents")
97 +
98 +@agents_router.get(
99 + "/velociraptor/outdated",
100 + response_model=OutdatedVelociraptorAgentsResponse,
101 + description="Get all outdated Velociraptor agents",
102 +)
103 async def get_outdated_velociraptor_agents() -> OutdatedVelociraptorAgentsResponse:
104 logger.info(f"Fetching all outdated Velociraptor agents")
105 return get_outdated_agents_velociraptor()
106
107 +
108 @agents_router.delete("/{agent_id}/delete", response_model=AgentModifyResponse, description="Delete agent")
109 async def delete_agent(agent_id: str) -> AgentModifyResponse:
110 logger.info(f"Deleting agent {agent_id}")
79 - #delete_agent_db(agent_id)
80 - #delete_agent_wazuh(agent_id)
111 + # delete_agent_db(agent_id)
112 + # delete_agent_wazuh(agent_id)
113 client_id = session.query(Agents).filter(Agents.agent_id == agent_id).first().velociraptor_id
114 delete_agent_velociraptor(client_id)
115 return {"success": True, "message": f"Agent {agent_id} deleted from database and Wazuh"}
116
85 -@agents_router.put("/{agent_id}/update-customer-code", response_model=AgentUpdateCustomerCodeResponse, description="Update agent customer code")
117 +
118 +@agents_router.put(
119 + "/{agent_id}/update-customer-code",
120 + response_model=AgentUpdateCustomerCodeResponse,
121 + description="Update agent customer code",
122 +)
123 async def update_agent_customer_code(agent_id: str, body: AgentUpdateCustomerCodeBody) -> AgentUpdateCustomerCodeResponse:
124 logger.info(f"Updating agent {agent_id} customer code to {body.customer_code}")
125 agent = session.query(Agents).filter(Agents.agent_id == agent_id).first()
backend/app/agents/schema/agents.py
+18 -5
@@ -1,41 +1,54 @@
1 -from pydantic import BaseModel, Field
2 -from typing import Optional, List, Dict, Any
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 from typing import Union
4 -from app.db.universal_models import Agents
5 -from app.agents.wazuh.schema.agents import WazuhAgent
6 +
7 +from pydantic import BaseModel
8 +from pydantic import Field
9 +
10 from app.agents.velociraptor.schema.agents import VelociraptorAgent
11 +from app.agents.wazuh.schema.agents import WazuhAgent
12 +from app.db.universal_models import Agents
13 +
14
15 class AgentsResponse(BaseModel):
16 agents: List[Agents]
17 success: bool
18 message: str
19
20 +
21 class SyncedAgent(WazuhAgent, VelociraptorAgent):
22 pass
23
24 +
25 class SyncedAgentsResponse(BaseModel):
26 agents_added: List[SyncedAgent]
27 success: bool
28 message: str
29
30 +
31 class AgentModifyResponse(BaseModel):
32 success: bool
33 message: str
34
35 +
36 class OutdatedWazuhAgentsResponse(BaseModel):
37 outdated_wazuh_agents: List[Agents]
38 success: bool
39 message: str
40
41 +
42 class OutdatedVelociraptorAgentsResponse(BaseModel):
43 outdated_velociraptor_agents: List[Agents]
44 success: bool
45 message: str
46
47 +
48 class AgentUpdateCustomerCodeBody(BaseModel):
49 customer_code: str = Field(None, description="Customer code to be updated")
50
51 +
52 class AgentUpdateCustomerCodeResponse(BaseModel):
53 success: bool
54 message: str
41 -
backend/app/agents/services/modify.py
+13 -7
@@ -1,13 +1,17 @@
1 from typing import List
2 +
3 +from fastapi import HTTPException
4 from loguru import logger
3 -from app.db.db_session import session
4 -from app.db.universal_models import Agents
5 -import app.agents.wazuh.services.agents as wazuh_services
5 +
6 import app.agents.velociraptor.services.agents as velociraptor_services
7 -from app.agents.schema.agents import SyncedAgentsResponse, SyncedAgent
8 -from app.agents.wazuh.schema.agents import WazuhAgent, WazuhAgentsList
7 +import app.agents.wazuh.services.agents as wazuh_services
8 +from app.agents.schema.agents import SyncedAgent
9 +from app.agents.schema.agents import SyncedAgentsResponse
10 from app.agents.velociraptor.schema.agents import VelociraptorAgent
10 -from fastapi import HTTPException
11 +from app.agents.wazuh.schema.agents import WazuhAgent
12 +from app.agents.wazuh.schema.agents import WazuhAgentsList
13 +from app.db.db_session import session
14 +from app.db.universal_models import Agents
15
16
17 def mark_agent_criticality(agent_id: str, critical: bool):
@@ -19,6 +23,7 @@ def mark_agent_criticality(agent_id: str, critical: bool):
23 session.commit()
24 return {"success": True, "message": f"Agent {agent_id} marked as critical: {critical}"}
25
26 +
27 def delete_agent_db(agent_id: str):
28 """Delete agent from database."""
29 agent = session.query(Agents).filter(Agents.agent_id == agent_id).first()
@@ -28,10 +33,11 @@ def delete_agent_db(agent_id: str):
33 session.commit()
34 return {"success": True, "message": f"Agent {agent_id} deleted from database"}
35
36 +
37 def delete_agent_wazuh(agent_id: str):
38 """Delete agent from Wazuh service."""
39 try:
40 wazuh_services.delete_agent(agent_id)
41 return {"success": True, "message": f"Agent {agent_id} deleted from Wazuh"}
42 except Exception as e:
37 - raise HTTPException(status_code=500, detail=f"Failed to delete agent {agent_id} from Wazuh: {e}")
\ No newline at end of file
43 + raise HTTPException(status_code=500, detail=f"Failed to delete agent {agent_id} from Wazuh: {e}")
backend/app/agents/services/status.py
+23 -16
@@ -1,24 +1,26 @@
1 from typing import List
2 +
3 from loguru import logger
4 +
5 +from app.agents.schema.agents import OutdatedVelociraptorAgentsResponse
6 +from app.agents.schema.agents import OutdatedWazuhAgentsResponse
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
5 -from app.agents.schema.agents import OutdatedWazuhAgentsResponse, OutdatedVelociraptorAgentsResponse
6 -from app.connectors.velociraptor.utils.universal import (
7 - UniversalService
8 -)
10
11
12 def get_agent(agent_id: str) -> List[Agents]:
12 - """
13 - Retrieves a specific agent from the database using its ID.
13 + """
14 + Retrieves a specific agent from the database using its ID.
15
15 - Args:
16 - agent_id (str): The ID of the agent to retrieve.
16 + Args:
17 + agent_id (str): The ID of the agent to retrieve.
18 +
19 + Returns:
20 + AgentMetadata: The agent object if found, otherwise None.
21 + """
22 + return session.query(Agents).filter(Agents.agent_id == agent_id).first()
23
18 - Returns:
19 - AgentMetadata: The agent object if found, otherwise None.
20 - """
21 - return session.query(Agents).filter(Agents.agent_id == agent_id).first()
24
25 def get_outdated_agents_wazuh() -> OutdatedWazuhAgentsResponse:
26 """
@@ -32,9 +34,12 @@ def get_outdated_agents_wazuh() -> OutdatedWazuhAgentsResponse:
34 logger.error("Wazuh Manager with agent_id '000' not found.")
35 return {"message": "Wazuh Manager with agent_id '000' not found.", "success": False}
36
35 - outdated_wazuh_agents = session.query(Agents).filter(Agents.agent_id != "000", Agents.wazuh_agent_version != wazuh_manager.wazuh_agent_version).all()
37 + outdated_wazuh_agents = (
38 + session.query(Agents).filter(Agents.agent_id != "000", Agents.wazuh_agent_version != wazuh_manager.wazuh_agent_version).all()
39 + )
40 return {"message": "Outdated Wazuh agents fetched successfully.", "success": True, "outdated_wazuh_agents": outdated_wazuh_agents}
41
42 +
43 def get_outdated_agents_velociraptor() -> OutdatedVelociraptorAgentsResponse:
44 """
45 Retrieves all agents with outdated Velociraptor client versions from the database.
@@ -49,6 +54,8 @@ def get_outdated_agents_velociraptor() -> OutdatedVelociraptorAgentsResponse:
54 for agent in agents:
55 if agent.velociraptor_agent_version != server_version:
56 outdated_velociraptor_agents.append(agent)
52 - return {"message": "Outdated Velociraptor agents fetched successfully.", "success": True, "outdated_velociraptor_agents": outdated_velociraptor_agents}
53 -
54 -
57 + return {
58 + "message": "Outdated Velociraptor agents fetched successfully.",
59 + "success": True,
60 + "outdated_velociraptor_agents": outdated_velociraptor_agents,
61 + }
backend/app/agents/services/sync.py
+16 -8
@@ -1,12 +1,17 @@
1 from typing import List
2 +
3 from loguru import logger
3 -from app.db.db_session import session
4 -from app.db.universal_models import Agents
5 -import app.agents.wazuh.services.agents as wazuh_services
4 +
5 import app.agents.velociraptor.services.agents as velociraptor_services
7 -from app.agents.schema.agents import SyncedAgentsResponse, SyncedAgent
8 -from app.agents.wazuh.schema.agents import WazuhAgent, WazuhAgentsList
6 +import app.agents.wazuh.services.agents as wazuh_services
7 +from app.agents.schema.agents import SyncedAgent
8 +from app.agents.schema.agents import SyncedAgentsResponse
9 from app.agents.velociraptor.schema.agents import VelociraptorAgent
10 +from app.agents.wazuh.schema.agents import WazuhAgent
11 +from app.agents.wazuh.schema.agents import WazuhAgentsList
12 +from app.db.db_session import session
13 +from app.db.universal_models import Agents
14 +
15
16 def fetch_wazuh_agents() -> WazuhAgentsList:
17 """Fetch agents from Wazuh service."""
@@ -17,10 +22,12 @@ def fetch_wazuh_agents() -> WazuhAgentsList:
22 message=collected_wazuh_agents.message,
23 )
24
25 +
26 def fetch_velociraptor_agent(agent_name: str) -> VelociraptorAgent:
27 """Fetch agent details from Velociraptor service."""
28 return velociraptor_services.collect_velociraptor_agent(agent_name)
29
30 +
31 def add_agent_to_db(agent: WazuhAgent, client: VelociraptorAgent, customer_code: str):
32 """Add new agent to database."""
33 new_agent = Agents.create_from_model(agent, client, customer_code)
@@ -28,17 +35,20 @@ def add_agent_to_db(agent: WazuhAgent, client: VelociraptorAgent, customer_code:
35 session.commit()
36 logger.info(f"Agent {agent.agent_name} added to the database")
37
38 +
39 def update_agent_in_db(existing_agent: Agents, agent: WazuhAgent, client: VelociraptorAgent, customer_code: str):
40 """Update existing agent in database."""
41 existing_agent.update_from_model(agent, client, customer_code)
42 session.commit()
43 logger.info(f"Agent {agent.agent_name} updated in the database")
44
45 +
46 def extract_customer_code(customer_code: str):
47 """Extract customer code from agent label."""
48 parts = customer_code.split("_")
49 return parts[1] if len(parts) > 1 else None
50
51 +
52 def sync_agents() -> SyncedAgentsResponse:
53 """Synchronize agents from Wazuh and Velociraptor services."""
54 wazuh_agents_list = fetch_wazuh_agents()
@@ -48,7 +58,7 @@ def sync_agents() -> SyncedAgentsResponse:
58
59 for wazuh_agent in wazuh_agents_list.agents:
60 logger.info(f"Collecting Velociraptor Agent for {wazuh_agent.agent_name}")
51 -
61 +
62 velociraptor_agent = fetch_velociraptor_agent(wazuh_agent.agent_name)
63
64 customer_code = extract_customer_code(wazuh_agent.agent_label)
@@ -63,8 +73,6 @@ def sync_agents() -> SyncedAgentsResponse:
73 # Combine the wazuh agent and velociraptor agent into one object
74 synced_agent = SyncedAgent(**wazuh_agent.dict(), **velociraptor_agent.dict())
75 agents_added_list.append(synced_agent)
66 -
67 -
76
77 logger.info(f"Agents Added List: {agents_added_list}")
78 return SyncedAgentsResponse(success=True, message="Agents synced successfully", agents_added=agents_added_list)
backend/app/agents/velociraptor/schema/agents.py
+9 -3
@@ -1,6 +1,12 @@
1 -from pydantic import BaseModel, Field
2 -from typing import Optional, List, Dict, Any
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
6 +
7 +from pydantic import BaseModel
8 +from pydantic import Field
9 +
10
11 class VelociraptorAgent(BaseModel):
12 client_id: Optional[str] = Field("n/a", alias="velociraptor_id")
@@ -9,7 +15,7 @@ class VelociraptorAgent(BaseModel):
15
16 @property
17 def client_last_seen_as_datetime(self):
12 - dt = datetime.strptime(self.client_last_seen, '%Y-%m-%dT%H:%M:%S%z')
18 + dt = datetime.strptime(self.client_last_seen, "%Y-%m-%dT%H:%M:%S%z")
19 return dt.replace(tzinfo=None)
20
21 class Config:
backend/app/agents/velociraptor/services/agents.py
+23 -25
@@ -1,28 +1,26 @@
1 -from typing import Dict, List, Optional, Any, Tuple, Union
1 +import json
2 +from datetime import datetime
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
8 +from typing import Union
9 +
10 import requests
11 import xmltodict
12 from loguru import logger
5 -import json
6 -from datetime import datetime
13
8 -from app.connectors.wazuh_manager.schema.rules import (
9 - RuleDisable, RuleDisableResponse, RuleEnable, RuleEnableResponse
10 -)
14 from app.agents.schema.agents import AgentsResponse
12 -
15 from app.agents.velociraptor.schema.agents import VelociraptorAgent
16 +from app.agents.velociraptor.utils.universal import parse_date
17 +from app.connectors.velociraptor.services.artifacts import ArtifactsService
18 +from app.connectors.velociraptor.utils.universal import UniversalService
19 +from app.connectors.wazuh_manager.schema.rules import RuleDisable
20 +from app.connectors.wazuh_manager.schema.rules import RuleDisableResponse
21 +from app.connectors.wazuh_manager.schema.rules import RuleEnable
22 +from app.connectors.wazuh_manager.schema.rules import RuleEnableResponse
23
15 -from app.agents.velociraptor.utils.universal import (
16 - parse_date
17 -)
18 -
19 -from app.connectors.velociraptor.utils.universal import (
20 - UniversalService
21 -)
22 -
23 -from app.connectors.velociraptor.services.artifacts import (
24 - ArtifactsService
25 -)
24
25 def collect_velociraptor_agent(agent_name: str) -> VelociraptorAgent:
26 """
@@ -41,26 +39,29 @@ def collect_velociraptor_agent(agent_name: str) -> VelociraptorAgent:
39 except (KeyError, IndexError, TypeError) as e:
40 logger.error(f"Failed to get client ID for {agent_name}. Error: {e}")
41 return VelociraptorAgent(client_id="Unknown", client_last_seen="Unknown", client_version="Unknown")
44 -
42 +
43 try:
44 vql_last_seen_at = f"select last_seen_at from clients(search='host:{agent_name}')"
45 last_seen_at = UniversalService()._get_last_seen_timestamp(vql_last_seen_at)
46 client_last_seen = datetime.fromtimestamp(
47 int(last_seen_at) / 1000000,
50 - ).strftime('%Y-%m-%dT%H:%M:%S+00:00') # Converting to string format
48 + ).strftime(
49 + "%Y-%m-%dT%H:%M:%S+00:00",
50 + ) # Converting to string format
51 except Exception as e:
52 logger.error(f"Failed to get or convert last seen at for {agent_name}. Error: {e}")
53 client_last_seen = "1970-01-01T00:00:00+00:00"
54 -
54 +
55 try:
56 vql_client_version = f"select * from clients(search='host:{agent_name}')"
57 client_version = UniversalService()._get_client_version(vql_client_version)
58 except Exception as e:
59 logger.error(f"Failed to get client version for {agent_name}. Error: {e}")
60 client_version = "Unknown"
61 -
61 +
62 return VelociraptorAgent(client_id=client_id, client_last_seen=client_last_seen, client_version=client_version)
63
64 +
65 def delete_agent_velociraptor(client_id: str) -> AgentsResponse:
66 """
67 Deletes an agent from Velociraptor.
@@ -78,6 +79,3 @@ def delete_agent_velociraptor(client_id: str) -> AgentsResponse:
79 except Exception as e:
80 logger.error(f"Failed to delete agent {client_id}. Error: {e}")
81 return AgentsResponse(success=False, message="Failed to delete agent")
81 -
82 -
83 -
\ No newline at end of file
backend/app/agents/velociraptor/utils/universal.py
+14 -12
@@ -1,18 +1,20 @@
1 from datetime import datetime
2 +
3 from loguru import logger
4
5 +
6 def parse_date(date_string: str) -> datetime:
5 - """
6 - Parses a date string into a datetime object.
7 + """
8 + Parses a date string into a datetime object.
9
8 - Args:
9 - date_string (str): The date string to parse.
10 + Args:
11 + date_string (str): The date string to parse.
12
11 - Returns:
12 - datetime: The parsed datetime object.
13 - """
14 - try:
15 - return datetime.strptime(date_string, "%Y-%m-%dT%H:%M:%S+00:00")
16 - except ValueError:
17 - logger.info(f"Invalid format for date: {date_string}. Using the epoch time as default.")
18 - return datetime.strptime("1970-01-01T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S+00:00")
\ No newline at end of file
13 + Returns:
14 + datetime: The parsed datetime object.
15 + """
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.")
20 + 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
+14 -4
@@ -1,8 +1,15 @@
1 -from pydantic import BaseModel, Field
2 -from typing import Optional, List, Dict, Any
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
6 from typing import Union
7 +
8 +from pydantic import BaseModel
9 +from pydantic import Field
10 +
11 from app.db.universal_models import Agents
5 -from datetime import datetime
12 +
13
14 class WazuhAgent(BaseModel):
15 agent_id: str = Field(..., alias="agent_id")
@@ -15,12 +22,13 @@ class WazuhAgent(BaseModel):
22
23 @property
24 def agent_last_seen_as_datetime(self):
18 - dt = datetime.strptime(self.agent_last_seen, '%Y-%m-%dT%H:%M:%S%z')
25 + dt = datetime.strptime(self.agent_last_seen, "%Y-%m-%dT%H:%M:%S%z")
26 return dt.replace(tzinfo=None)
27
28 class Config:
29 allow_population_by_field_name = True
30
31 +
32 class WazuhAgentsList(BaseModel):
33 agents: List[WazuhAgent]
34 success: bool
@@ -29,6 +37,7 @@ class WazuhAgentsList(BaseModel):
37 class Config:
38 allow_population_by_field_name = True
39
40 +
41 class WazuhAgentVulnerabilities(BaseModel):
42 severity: Optional[str]
43 updated: Optional[str]
@@ -46,6 +55,7 @@ class WazuhAgentVulnerabilities(BaseModel):
55 title: Optional[str]
56 cvss2_score: Optional[float]
57
58 +
59 class WazuhAgentVulnerabilitiesResponse(BaseModel):
60 vulnerabilities: Optional[List[WazuhAgentVulnerabilities]]
61 success: bool
backend/app/agents/wazuh/services/agents.py
+26 -20
@@ -1,19 +1,27 @@
1 -from typing import Dict, List, Optional, Any, Tuple, Union
1 +import json
2 +from typing import Any
3 +from typing import Dict
4 +from typing import List
5 +from typing import Optional
6 +from typing import Tuple
7 +from typing import Union
8 +
9 import requests
10 import xmltodict
11 from loguru import logger
5 -import json
12
7 -from app.connectors.wazuh_manager.schema.rules import (
8 - RuleDisable, RuleDisableResponse, RuleEnable, RuleEnableResponse
9 -)
13 from app.agents.schema.agents import AgentModifyResponse
14 +from app.agents.wazuh.schema.agents import WazuhAgent
15 +from app.agents.wazuh.schema.agents import WazuhAgentsList
16 +from app.connectors.wazuh_manager.schema.rules import RuleDisable
17 +from app.connectors.wazuh_manager.schema.rules import RuleDisableResponse
18 +from app.connectors.wazuh_manager.schema.rules import RuleEnable
19 +from app.connectors.wazuh_manager.schema.rules import RuleEnableResponse
20 +from app.connectors.wazuh_manager.utils.universal import restart_service
21 +from app.connectors.wazuh_manager.utils.universal import send_delete_request
22 +from app.connectors.wazuh_manager.utils.universal import send_get_request
23 +from app.connectors.wazuh_manager.utils.universal import send_put_request
24
12 -from app.agents.wazuh.schema.agents import WazuhAgentsList, WazuhAgent
13 -
14 -from app.connectors.wazuh_manager.utils.universal import (
15 - send_get_request, send_put_request, restart_service, send_delete_request
16 -)
25
26 def collect_wazuh_agents() -> WazuhAgentsList:
27 logger.info("Collecting all agents from Wazuh Manager")
@@ -34,28 +42,26 @@ def collect_wazuh_agents() -> WazuhAgentsList:
42 agent_os=os_name,
43 agent_label=agent_group,
44 agent_last_seen=last_keep_alive,
37 - wazuh_agent_version=agent["version"] if "version" in agent else 'n/a'
45 + wazuh_agent_version=agent["version"] if "version" in agent else "n/a",
46 )
47 wazuh_agents_list.append(wazuh_agent)
48
49 return WazuhAgentsList(agents=wazuh_agents_list, success=True, message="Agents collected successfully")
50 else:
51 return WazuhAgentsList(agents=[], success=False, message="Failed to collect agents")
44 -
52 +
53 +
54 def delete_agent(agent_id: str) -> AgentModifyResponse:
55 """Delete agent from Wazuh Manager."""
56 logger.info(f"Deleting agent {agent_id} from Wazuh Manager")
57 params = {
49 - "purge": True,
50 - "agents_list": [agent_id],
51 - "status": "all",
52 - "older_than": "0s",
53 - }
58 + "purge": True,
59 + "agents_list": [agent_id],
60 + "status": "all",
61 + "older_than": "0s",
62 + }
63 agent_deleted = send_delete_request(endpoint="/agents", params=params)
64 if agent_deleted["success"]:
65 return AgentModifyResponse(success=True, message="Agent deleted successfully")
66 else:
67 return AgentModifyResponse(success=False, message="Failed to delete agent")
59 -
60 -
61 -
\ No newline at end of file
backend/app/agents/wazuh/services/vulnerabilities.py
+26 -15
@@ -1,19 +1,28 @@
1 -from typing import Dict, List, Optional, Any, Tuple, Union
1 +import json
2 +from typing import Any
3 +from typing import Dict
4 +from typing import List
5 +from typing import Optional
6 +from typing import Tuple
7 +from typing import Union
8 +
9 import requests
10 import xmltodict
11 from loguru import logger
5 -import json
12
7 -from app.connectors.wazuh_manager.schema.rules import (
8 - RuleDisable, RuleDisableResponse, RuleEnable, RuleEnableResponse
9 -)
13 from app.agents.schema.agents import AgentsResponse
14 +from app.agents.wazuh.schema.agents import WazuhAgent
15 +from app.agents.wazuh.schema.agents import WazuhAgentsList
16 +from app.agents.wazuh.schema.agents import WazuhAgentVulnerabilities
17 +from app.agents.wazuh.schema.agents import WazuhAgentVulnerabilitiesResponse
18 +from app.connectors.wazuh_manager.schema.rules import RuleDisable
19 +from app.connectors.wazuh_manager.schema.rules import RuleDisableResponse
20 +from app.connectors.wazuh_manager.schema.rules import RuleEnable
21 +from app.connectors.wazuh_manager.schema.rules import RuleEnableResponse
22 +from app.connectors.wazuh_manager.utils.universal import restart_service
23 +from app.connectors.wazuh_manager.utils.universal import send_get_request
24 +from app.connectors.wazuh_manager.utils.universal import send_put_request
25
12 -from app.agents.wazuh.schema.agents import WazuhAgentsList, WazuhAgent, WazuhAgentVulnerabilities, WazuhAgentVulnerabilitiesResponse
13 -
14 -from app.connectors.wazuh_manager.utils.universal import (
15 - send_get_request, send_put_request, restart_service
16 -)
26
27 def collect_agent_vulnerabilities(agent_id: str):
28 """Collect agent vulnerabilities from Wazuh Manager."""
@@ -21,11 +30,13 @@ def collect_agent_vulnerabilities(agent_id: str):
30 agent_vulnerabilities = send_get_request(endpoint=f"/vulnerability/{agent_id}")
31 if agent_vulnerabilities["success"]:
32 processed_vulnerabilities = process_agent_vulnerabilities(agent_vulnerabilities["data"])
24 - return WazuhAgentVulnerabilitiesResponse(vulnerabilities=processed_vulnerabilities, success=True, message="Vulnerabilities collected successfully")
25 -
33 + return WazuhAgentVulnerabilitiesResponse(
34 + vulnerabilities=processed_vulnerabilities,
35 + success=True,
36 + message="Vulnerabilities collected successfully",
37 + )
38 +
39 +
40 def process_agent_vulnerabilities(agent_vulnerabilities: dict) -> List[WazuhAgentVulnerabilities]:
41 vulnerabilities = agent_vulnerabilities.get("data", {}).get("affected_items", [])
42 return [WazuhAgentVulnerabilities(**vuln) for vuln in vulnerabilities]
29 -
30 -
31 -
\ No newline at end of file
backend/app/auth/models/users.py
+12 -8
@@ -1,8 +1,11 @@
1 import datetime
2 from typing import Optional
3
4 -from pydantic import validator, EmailStr
5 -from sqlmodel import SQLModel, Field, Relationship
4 +from pydantic import EmailStr
5 +from pydantic import validator
6 +from sqlmodel import Field
7 +from sqlmodel import Relationship
8 +from sqlmodel import SQLModel
9
10
11 class User(SQLModel, table=True):
@@ -23,10 +26,10 @@ class UserInput(SQLModel):
26 email: EmailStr
27 is_admin: bool = False
28
26 - @validator('password2')
29 + @validator("password2")
30 def password_match(cls, v, values, **kwargs):
28 - if 'password' in values and v != values['password']:
29 - raise ValueError('passwords don\'t match')
31 + if "password" in values and v != values["password"]:
32 + raise ValueError("passwords don't match")
33 return v
34
35
@@ -45,6 +48,7 @@ class SMTP(SQLModel, table=True):
48
49 user: "User" = Relationship(back_populates="smtp")
50
51 +
52 class SMTPInput(SQLModel):
53 email: EmailStr
54 smtp_password: str = Field(max_length=256)
@@ -52,8 +56,8 @@ class SMTPInput(SQLModel):
56 smtp_server: str = Field(max_length=256)
57 smtp_port: int
58
55 - @validator('smtp_password2')
59 + @validator("smtp_password2")
60 def password_match(cls, v, values, **kwargs):
57 - if 'smtp_password' in values and v != values['smtp_password']:
58 - raise ValueError('passwords don\'t match')
61 + if "smtp_password" in values and v != values["smtp_password"]:
62 + raise ValueError("passwords don't match")
63 return v
backend/app/auth/routes/auth.py
+20 -13
@@ -1,41 +1,48 @@
1 -from fastapi import APIRouter, HTTPException, Security, security, Depends
1 +from fastapi import APIRouter
2 +from fastapi import Depends
3 +from fastapi import HTTPException
4 +from fastapi import Security
5 +from fastapi import security
6 from fastapi.security import HTTPAuthorizationCredentials
3 -from app.auth.schema.auth import UserResponse, UserLoginResponse
7
8 +from app.auth.models.users import User
9 +from app.auth.models.users import UserInput
10 +from app.auth.models.users import UserLogin
11 +from app.auth.schema.auth import UserLoginResponse
12 +from app.auth.schema.auth import UserResponse
13 +from app.auth.services.universal import find_user
14 +from app.auth.services.universal import select_all_users
15 from app.auth.utils import AuthHandler
16 from app.db.db_session import session
7 -from app.auth.models.users import UserInput, User, UserLogin
8 -from app.auth.services.universal import select_all_users, find_user
17
18 user_router = APIRouter()
19 auth_handler = AuthHandler()
20
21
14 -@user_router.post('/register', response_model=UserResponse, status_code=201, description='Register new user')
22 +@user_router.post("/register", response_model=UserResponse, status_code=201, description="Register new user")
23 def register(user: UserInput):
24 users = select_all_users()
25 if any(x.username == user.username for x in users):
18 - raise HTTPException(status_code=400, detail='Username is taken')
26 + raise HTTPException(status_code=400, detail="Username is taken")
27 hashed_pwd = auth_handler.get_password_hash(user.password)
20 - u = User(username=user.username, password=hashed_pwd, email=user.email,
21 - is_admin=user.is_admin)
28 + u = User(username=user.username, password=hashed_pwd, email=user.email, is_admin=user.is_admin)
29 session.add(u)
30 session.commit()
31 return {"message": "User created successfully", "success": True}
32
33
27 -@user_router.post('/login', response_model=UserLoginResponse, description='Login user')
34 +@user_router.post("/login", response_model=UserLoginResponse, description="Login user")
35 def login(user: UserLogin):
36 user_found = find_user(user.username)
37 if not user_found:
31 - raise HTTPException(status_code=401, detail='Invalid username and/or password')
38 + raise HTTPException(status_code=401, detail="Invalid username and/or password")
39 verified = auth_handler.verify_password(user.password, user_found.password)
40 if not verified:
34 - raise HTTPException(status_code=401, detail='Invalid username and/or password')
41 + raise HTTPException(status_code=401, detail="Invalid username and/or password")
42 token = auth_handler.encode_token(user_found.username)
36 - return {'token': token, 'success': True, 'message': 'Login successful'}
43 + return {"token": token, "success": True, "message": "Login successful"}
44
45
39 -@user_router.get('/users/me', description='Get current user')
46 +@user_router.get("/users/me", description="Get current user")
47 def get_current_user(user: User = Depends(auth_handler.get_current_user)):
48 return user
backend/app/auth/schema/auth.py
+2
@@ -1,9 +1,11 @@
1 from pydantic import BaseModel
2
3 +
4 class UserResponse(BaseModel):
5 message: str
6 success: bool
7
8 +
9 class UserLoginResponse(BaseModel):
10 token: str
11 message: str
backend/app/auth/services/universal.py
+5 -3
@@ -1,6 +1,8 @@
1 -from sqlmodel import Session, select
2 -from app.db.db_session import engine
1 +from sqlmodel import Session
2 +from sqlmodel import select
3 +
4 from app.auth.models.users import User
5 +from app.db.db_session import engine
6
7
8 def select_all_users():
@@ -13,4 +15,4 @@ def select_all_users():
15 def find_user(name):
16 with Session(engine) as session:
17 statement = select(User).where(User.username == name)
16 - return session.exec(statement).first()
\ No newline at end of file
18 + return session.exec(statement).first()
backend/app/auth/utils.py
+15 -20
@@ -1,9 +1,11 @@
1 import datetime
2
3 -from fastapi import Security, HTTPException
4 -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
5 -from passlib.context import CryptContext
3 import jwt
4 +from fastapi import HTTPException
5 +from fastapi import Security
6 +from fastapi.security import HTTPAuthorizationCredentials
7 +from fastapi.security import HTTPBearer
8 +from passlib.context import CryptContext
9 from starlette import status
10
11 from app.auth.services.universal import find_user
@@ -11,8 +13,8 @@ from app.auth.services.universal import find_user
13
14 class AuthHandler:
15 security = HTTPBearer()
14 - pwd_context = CryptContext(schemes=['bcrypt'])
15 - secret = 'supersecret'
16 + pwd_context = CryptContext(schemes=["bcrypt"])
17 + secret = "supersecret"
18
19 def get_password_hash(self, password):
20 return self.pwd_context.hash(password)
@@ -21,34 +23,27 @@ class AuthHandler:
23 return self.pwd_context.verify(pwd, hashed_pwd)
24
25 def encode_token(self, user_id):
24 - payload = {
25 - 'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=8),
26 - 'iat': datetime.datetime.utcnow(),
27 - 'sub': user_id
28 - }
29 - return jwt.encode(payload, self.secret, algorithm='HS256')
26 + payload = {"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=8), "iat": datetime.datetime.utcnow(), "sub": user_id}
27 + return jwt.encode(payload, self.secret, algorithm="HS256")
28
29 def decode_token(self, token):
30 try:
33 - payload = jwt.decode(token, self.secret, algorithms=['HS256'])
34 - return payload['sub']
31 + payload = jwt.decode(token, self.secret, algorithms=["HS256"])
32 + return payload["sub"]
33 except jwt.ExpiredSignatureError:
36 - raise HTTPException(status_code=401, detail='Expired signature')
34 + raise HTTPException(status_code=401, detail="Expired signature")
35 except jwt.InvalidTokenError:
38 - raise HTTPException(status_code=401, detail='Invalid token')
36 + raise HTTPException(status_code=401, detail="Invalid token")
37
38 def auth_wrapper(self, auth: HTTPAuthorizationCredentials = Security(security)):
39 return self.decode_token(auth.credentials)
40
41 def get_current_user(self, auth: HTTPAuthorizationCredentials = Security(security)):
44 - credentials_exception = HTTPException(
45 - status_code=status.HTTP_401_UNAUTHORIZED,
46 - detail='Could not validate credentials'
47 - )
42 + credentials_exception = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials")
43 username = self.decode_token(auth.credentials)
44 if username is None:
45 raise credentials_exception
46 user = find_user(username)
47 if user is None:
48 raise credentials_exception
54 - return user
\ No newline at end of file
49 + return user
backend/app/connectors/cortex/routes/analyzers.py
+20 -11
@@ -1,37 +1,46 @@
1 +from datetime import timedelta
2 +from typing import Dict
3 from typing import List
2 -from fastapi import APIRouter, HTTPException, Security, Depends
3 -from starlette.status import HTTP_401_UNAUTHORIZED
4 +from typing import Optional
5 +from typing import Union
6 +
7 +from fastapi import APIRouter
8 +from fastapi import Depends
9 +from fastapi import HTTPException
10 +from fastapi import Security
11 from loguru import logger
5 -from datetime import timedelta
6 -from typing import Union, Dict, Optional
12 +from starlette.status import HTTP_401_UNAUTHORIZED
13
14 # App specific imports
15 from app.auth.routes.auth import auth_handler
16 +from app.connectors.cortex.schema.analyzers import AnalyzerJobData
17 +from app.connectors.cortex.schema.analyzers import AnalyzersResponse
18 +from app.connectors.cortex.schema.analyzers import RunAnalyzerBody
19 +from app.connectors.cortex.schema.analyzers import RunAnalyzerResponse
20 +from app.connectors.cortex.services.analyzers import get_analyzers
21 +from app.connectors.cortex.services.analyzers import run_analyzer
22 from app.db.db_session import session
23
12 -from app.connectors.cortex.schema.analyzers import (
13 - AnalyzersResponse, AnalyzerJobData, RunAnalyzerBody, RunAnalyzerResponse
14 -)
15 -
16 -from app.connectors.cortex.services.analyzers import get_analyzers, run_analyzer
17 -
18 -
24 cortex_analyzer_router = APIRouter()
25
26 +
27 def get_available_analyzers() -> List[str]:
28 return get_analyzers().analyzers
29
30 +
31 def verify_analyzer_exists(run_analyzer_body: RunAnalyzerBody) -> RunAnalyzerBody:
32 available_analyzers = get_available_analyzers()
33 if run_analyzer_body.analyzer_name not in available_analyzers:
34 raise HTTPException(status_code=400, detail=f"Analyzer {run_analyzer_body.analyzer_name} does not exist.")
35 return run_analyzer_body
36
37 +
38 @cortex_analyzer_router.get("", response_model=AnalyzersResponse, description="Get all analyzers")
39 async def get_all_analyzers() -> AnalyzersResponse:
40 logger.info(f"Fetching all analyzers")
41 return get_analyzers()
42
43 +
44 @cortex_analyzer_router.post("/run", response_model=RunAnalyzerResponse, description="Run an analyzer")
45 async def run_analyzer_route(run_analyzer_body: RunAnalyzerBody = Depends(verify_analyzer_exists)) -> RunAnalyzerResponse:
46 is_valid, data_type = RunAnalyzerBody.is_valid_datatype(run_analyzer_body.analyzer_data)
backend/app/connectors/cortex/schema/analyzers.py
+18 -8
@@ -1,10 +1,19 @@
1 -from pydantic import BaseModel, validator, Field
1 import ipaddress
2 import re
4 -from typing import List, Optional, Dict, Any, Tuple
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
8
6 -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 -DOMAIN_REGEX = re.compile(r'^(?:[a-z0-9](?:[a-z0-9\-]{0,61}[a-z0-9])?\.)+[a-z]{2,6}$') # Update this regex to match your specific domain format
9 +from pydantic import BaseModel
10 +from pydantic import Field
11 +from pydantic import validator
12 +
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
14 +DOMAIN_REGEX = re.compile(
15 + r"^(?:[a-z0-9](?:[a-z0-9\-]{0,61}[a-z0-9])?\.)+[a-z]{2,6}$",
16 +) # Update this regex to match your specific domain format
17
18
19 class AnalyzersResponse(BaseModel):
@@ -12,17 +21,18 @@ class AnalyzersResponse(BaseModel):
21 message: str
22 success: bool
23
24 +
25 class RunAnalyzerBody(BaseModel):
26 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")
29
20 - @validator('analyzer_data', pre=True, always=True)
30 + @validator("analyzer_data", pre=True, always=True)
31 def validate_and_set_data_type(cls, value: str, values: dict) -> str:
32 is_valid, data_type = cls.is_valid_datatype(value)
33 if not is_valid:
34 raise ValueError(f"Invalid data type: {data_type}")
25 - values['data_type'] = data_type
35 + values["data_type"] = data_type
36 return value
37
38 @classmethod
@@ -52,15 +62,15 @@ class RunAnalyzerBody(BaseModel):
62 def _is_valid_domain(value: str) -> bool:
63 return bool(DOMAIN_REGEX.match(value))
64
65 +
66 class RunAnalyzerResponse(BaseModel):
67 report: Dict[str, Any]
68 message: str
69 success: bool
70
71
61 -
72 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 tlp: int = Field(1, description="Traffic Light Protocol (TLP) level.")
66 - message: str = Field("custom message sent to analyzer", description="Custom message.")
\ No newline at end of file
76 + message: str = Field("custom message sent to analyzer", description="Custom message.")
backend/app/connectors/cortex/services/analyzers.py
+41 -18
@@ -1,17 +1,35 @@
1 # analyzers.py
2
3 -from datetime import datetime
4 -from typing import List, Dict, Any, Callable, Tuple, Optional, Union
5 -import time
3 import json
7 -from fastapi import HTTPException
8 -from loguru import logger
9 -from dfir_iris_client.alert import Alert # Assuming this import is needed in your context
4 +import time
5 +from datetime import datetime
6 +from typing import Any
7 +from typing import Callable
8 +from typing import Dict
9 +from typing import List
10 +from typing import Optional
11 +from typing import Tuple
12 +from typing import Union
13 +
14 from cortex4py.api import Api
11 -from app.connectors.cortex.schema.analyzers import AnalyzersResponse, RunAnalyzerBody, RunAnalyzerResponse, AnalyzerJobData # Assuming this import is needed
12 -from app.connectors.cortex.utils.universal import create_cortex_client # Importing create_cortex_client
13 -from app.connectors.cortex.utils.universal import run_and_wait_for_analyzer # Importing from universal.py
15 +from dfir_iris_client.alert import (
16 + Alert, # Assuming this import is needed in your context
17 +)
18 from fastapi import HTTPException
19 +from loguru import logger
20 +
21 +from app.connectors.cortex.schema.analyzers import (
22 + AnalyzerJobData, # Assuming this import is needed
23 +)
24 +from app.connectors.cortex.schema.analyzers import AnalyzersResponse
25 +from app.connectors.cortex.schema.analyzers import RunAnalyzerBody
26 +from app.connectors.cortex.schema.analyzers import RunAnalyzerResponse
27 +from app.connectors.cortex.utils.universal import (
28 + create_cortex_client, # Importing create_cortex_client
29 +)
30 +from app.connectors.cortex.utils.universal import (
31 + run_and_wait_for_analyzer, # Importing from universal.py
32 +)
33
34 ############################# Helpful to find the attributes of the analyzer object
35 # def fetch_analyzers(api: Api) -> List[Dict]:
@@ -37,7 +55,7 @@ from fastapi import HTTPException
55 # api = create_cortex_client('Cortex')
56 # if api is None:
57 # return {"success": False, "message": "API initialization failed"}
40 -
58 +
59 # analyzers = fetch_analyzers(api)
60 # return AnalyzersResponse(success=True, message="Successfully fetched analyzers", analyzers=build_analyzer_response(analyzers))
61
@@ -45,7 +63,7 @@ from fastapi import HTTPException
63 # api = create_cortex_client('Cortex')
64 # if api is None:
65 # return {"success": False, "message": "API initialization failed"}
48 -
66 +
67 # analyzer_name = run_analyzer_body.analyzer_name
68 # analyzer_data = run_analyzer_body.analyzer_data
69 # job_data = AnalyzerJobData(data=analyzer_data, dataType='ip')
@@ -58,6 +76,7 @@ from fastapi import HTTPException
76 def fetch_analyzers(api: Api) -> List[Dict]:
77 return api.analyzers.find_all({}, range="all")
78
79 +
80 def extract_analyzer_names(analyzers: List[Dict]) -> List[str]:
81 try:
82 return [analyzer.name for analyzer in analyzers]
@@ -65,8 +84,10 @@ def extract_analyzer_names(analyzers: List[Dict]) -> List[str]:
84 logger.error(f"Error processing analyzers: {e}")
85 raise HTTPException(status_code=500, detail=f"Error processing analyzers: {e}")
86
87 +
88 def init_cortex_client() -> Union[Api, None]:
69 - return create_cortex_client('Cortex')
89 + return create_cortex_client("Cortex")
90 +
91
92 def handle_api_initialization(api: Union[Api, None]) -> Api:
93 if api is None:
@@ -74,28 +95,30 @@ def handle_api_initialization(api: Union[Api, None]) -> Api:
95 raise HTTPException(status_code=500, detail="API initialization failed")
96 return api
97
98 +
99 def get_analyzers() -> AnalyzersResponse:
100 api = init_cortex_client()
101 handle_api_initialization(api)
80 -
102 +
103 analyzers = fetch_analyzers(api)
104 analyzer_names = extract_analyzer_names(analyzers)
83 -
105 +
106 return AnalyzersResponse(success=True, message="Successfully fetched analyzers", analyzers=analyzer_names)
107
108 +
109 def run_analyzer(run_analyzer_body: RunAnalyzerBody, data_type: str) -> RunAnalyzerResponse:
110 api = init_cortex_client()
111 handle_api_initialization(api)
89 -
112 +
113 analyzer_name = run_analyzer_body.analyzer_name
114 analyzer_data = run_analyzer_body.analyzer_data
115 logger.info(f"Running analyzer {analyzer_name} with data {analyzer_data} of type {data_type}")
116 job_data = AnalyzerJobData(data=analyzer_data, dataType=data_type)
94 -
117 +
118 result = run_and_wait_for_analyzer(analyzer_name=analyzer_name, job_data=job_data)
96 -
119 +
120 if result is None:
121 logger.error(f"Failed to run analyzer {analyzer_name}")
122 raise HTTPException(status_code=500, detail=f"Failed to run analyzer {analyzer_name}")
100 -
123 +
124 return RunAnalyzerResponse(success=True, message="Successfully ran analyzer", report=result)
backend/app/connectors/cortex/utils/universal.py
+33 -20
@@ -1,21 +1,29 @@
1 -from typing import Dict, Any, List, Generator, Type
2 -from sqlmodel import Session, select
3 -from app.connectors.models import Connectors
4 -from elasticsearch7 import Elasticsearch
5 -from loguru import logger
6 -from app.db.db_session import engine
7 -import requests
8 -from app.connectors.schema import ConnectorResponse
9 -from app.connectors.utils import get_connector_info_from_db
10 -from app.connectors.wazuh_indexer.schema.indices import Indices, IndexConfigModel
11 -from app.connectors.cortex.schema.analyzers import AnalyzerJobData
12 -from datetime import datetime, timedelta
13 -from typing import Iterable, Tuple
14 -from cortex4py.api import Api
1 import time
2 import traceback
3 +from datetime import datetime
4 +from datetime import timedelta
5 +from typing import Any
6 +from typing import Dict
7 +from typing import Generator
8 +from typing import Iterable
9 +from typing import List
10 +from typing import Tuple
11 +from typing import Type
12
13 +import requests
14 +from cortex4py.api import Api
15 +from elasticsearch7 import Elasticsearch
16 +from loguru import logger
17 +from sqlmodel import Session
18 +from sqlmodel import select
19
20 +from app.connectors.cortex.schema.analyzers import AnalyzerJobData
21 +from app.connectors.models import Connectors
22 +from app.connectors.schema import ConnectorResponse
23 +from app.connectors.utils import get_connector_info_from_db
24 +from app.connectors.wazuh_indexer.schema.indices import IndexConfigModel
25 +from app.connectors.wazuh_indexer.schema.indices import Indices
26 +from app.db.db_session import engine
27
28
29 def verify_cortex_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
@@ -26,7 +34,7 @@ def verify_cortex_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
34 dict: A dictionary containing 'connectionSuccessful' status and 'authToken' if the connection is successful.
35 """
36 logger.info(f"Verifying the Cortex connection to {attributes['connector_url']}")
29 -
37 +
38 try:
39 api = Api(attributes["connector_url"], attributes["connector_api_key"], verify_cert=False)
40 # Get Cortex Status
@@ -40,7 +48,8 @@ def verify_cortex_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
48 except Exception as e:
49 logger.error(f"Connection to {attributes['connector_url']} failed with error: {e}")
50 return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error: {e}"}
43 -
51 +
52 +
53 def verify_cortex_connection(connector_name: str) -> str:
54 """
55 Returns the authentication token for the Cortex service.
@@ -54,6 +63,7 @@ def verify_cortex_connection(connector_name: str) -> str:
63 return None
64 return verify_cortex_credentials(attributes)
65
66 +
67 def create_cortex_client(connector_name: str) -> Api:
68 """
69 Returns an Cortex client for the Wazuh Indexer service.
@@ -67,10 +77,11 @@ def create_cortex_client(connector_name: str) -> Api:
77 return None
78 return Api(attributes["connector_url"], attributes["connector_api_key"], verify_cert=False)
79
80 +
81 def run_and_wait_for_analyzer(analyzer_name: str, job_data: AnalyzerJobData) -> Dict[str, Any]:
71 - api = create_cortex_client('Cortex') # Create Api object
82 + api = create_cortex_client("Cortex") # Create Api object
83 if api is None:
73 - return {"success": False, "message": "API initialization failed"}
84 + return {"success": False, "message": "API initialization failed"}
85 try:
86 # job = api.analyzers.run_by_name(
87 # analyzer_name,
@@ -92,6 +103,7 @@ def run_and_wait_for_analyzer(analyzer_name: str, job_data: AnalyzerJobData) ->
103 logger.debug(f"Error running analyzer {analyzer_name}: {e}", exc_info=True)
104 return {"success": False, "message": f"Error running analyzer {analyzer_name}: {e}"}
105
106 +
107 def monitor_analyzer_job(api: Api, job: Any) -> Dict[str, Any]:
108 r_json = job.json()
109 job_id = r_json["id"]
@@ -104,7 +116,7 @@ def monitor_analyzer_job(api: Api, job: Any) -> Dict[str, Any]:
116 if timer == 60:
117 logger.error("Job failed to complete after 5 minutes.")
118 return {"success": False, "message": "Job timed out"}
107 -
119 +
120 timer += 1
121 logger.info(f"Timer is: {timer}")
122
@@ -120,7 +132,8 @@ def monitor_analyzer_job(api: Api, job: Any) -> Dict[str, Any]:
132
133 return retrieve_final_report(api, job_id)
134
135 +
136 def retrieve_final_report(api: Api, job_id: str) -> Dict[str, Any]:
137 report = api.jobs.get_report(job_id).report
138 final_report = report["full"]
126 - return {"success": True, "message": "Analyzer ran successfully", "report": final_report}
\ No newline at end of file
139 + return {"success": True, "message": "Analyzer ran successfully", "report": final_report}
backend/app/connectors/dfir_iris/routes/alerts.py
+20 -13
@@ -1,22 +1,26 @@
1 +from datetime import timedelta
2 +from typing import Dict
3 from typing import List
2 -from fastapi import APIRouter, HTTPException, Security, Depends
3 -from starlette.status import HTTP_401_UNAUTHORIZED
4 +from typing import Optional
5 +from typing import Union
6 +
7 +from fastapi import APIRouter
8 +from fastapi import Depends
9 +from fastapi import HTTPException
10 +from fastapi import Security
11 from loguru import logger
5 -from datetime import timedelta
6 -from typing import Union, Dict, Optional
12 +from starlette.status import HTTP_401_UNAUTHORIZED
13
14 # App specific imports
15 from app.auth.routes.auth import auth_handler
10 -from app.db.db_session import session
11 -
12 -from app.connectors.dfir_iris.schema.alerts import (
13 - AlertsResponse, AlertResponse, BookmarkedAlertsResponse
14 -)
15 -
16 -from app.connectors.dfir_iris.services.alerts import get_alerts, bookmark_alert, get_bookmarked_alerts
17 -
18 -
16 +from app.connectors.dfir_iris.schema.alerts import AlertResponse
17 +from app.connectors.dfir_iris.schema.alerts import AlertsResponse
18 +from app.connectors.dfir_iris.schema.alerts import BookmarkedAlertsResponse
19 +from app.connectors.dfir_iris.services.alerts import bookmark_alert
20 +from app.connectors.dfir_iris.services.alerts import get_alerts
21 +from app.connectors.dfir_iris.services.alerts import get_bookmarked_alerts
22 from app.connectors.dfir_iris.utils.universal import check_alert_exists
23 +from app.db.db_session import session
24
25
26 def verify_alert_exists(alert_id: str) -> str:
@@ -24,6 +28,7 @@ def verify_alert_exists(alert_id: str) -> str:
28 raise HTTPException(status_code=400, detail=f"Alert {alert_id} does not exist.")
29 return alert_id
30
31 +
32 dfir_iris_alerts_router = APIRouter()
33
34
@@ -32,6 +37,7 @@ async def get_all_alerts() -> AlertsResponse:
37 logger.info(f"Fetching all alerts")
38 return get_alerts()
39
40 +
41 @dfir_iris_alerts_router.get("/bookmark", response_model=BookmarkedAlertsResponse, description="Get all bookmarked alerts")
42 async def get_all_bookmarked_alerts() -> BookmarkedAlertsResponse:
43 logger.info(f"Fetching all bookmarked alerts")
@@ -43,6 +49,7 @@ async def bookmark_alert_route(alert_id: str = Depends(verify_alert_exists)) ->
49 logger.info(f"Bookmarking alert {alert_id}")
50 return bookmark_alert(alert_id, bookmarked=True)
51
52 +
53 @dfir_iris_alerts_router.delete("/bookmark/{alert_id}", response_model=AlertResponse, description="Unbookmark an alert")
54 async def unbookmark_alert_route(alert_id: str = Depends(verify_alert_exists)) -> AlertResponse:
55 logger.info(f"Unbookmarking alert {alert_id}")
backend/app/connectors/dfir_iris/routes/assets.py
+13 -12
@@ -1,23 +1,24 @@
1 +from datetime import timedelta
2 +from typing import Dict
3 from typing import List
2 -from fastapi import APIRouter, HTTPException, Security, Depends
3 -from starlette.status import HTTP_401_UNAUTHORIZED
4 +from typing import Optional
5 +from typing import Union
6 +
7 +from fastapi import APIRouter
8 +from fastapi import Depends
9 +from fastapi import HTTPException
10 +from fastapi import Security
11 from loguru import logger
5 -from datetime import timedelta
6 -from typing import Union, Dict, Optional
12 +from starlette.status import HTTP_401_UNAUTHORIZED
13
14 # App specific imports
15 from app.auth.routes.auth import auth_handler
10 -from app.db.db_session import session
11 -
12 -from app.connectors.dfir_iris.schema.assets import (
13 - AssetResponse
14 -)
15 -
16 +from app.connectors.dfir_iris.schema.assets import AssetResponse
17 from app.connectors.dfir_iris.services.assets import get_case_assets
17 -
18 from app.connectors.dfir_iris.utils.universal import check_case_exists
19 -
19 from app.connectors.wazuh_indexer.utils.universal import collect_indices
20 +from app.db.db_session import session
21 +
22
23 def verify_case_exists(case_id: int) -> int:
24 if not check_case_exists(case_id):
backend/app/connectors/dfir_iris/routes/cases.py
+26 -17
@@ -1,24 +1,33 @@
1 +from datetime import timedelta
2 +from typing import Dict
3 from typing import List
2 -from fastapi import APIRouter, HTTPException, Security, Depends
3 -from starlette.status import HTTP_401_UNAUTHORIZED
4 +from typing import Optional
5 +from typing import Union
6 +
7 +from fastapi import APIRouter
8 +from fastapi import Depends
9 +from fastapi import HTTPException
10 +from fastapi import Security
11 from loguru import logger
5 -from datetime import timedelta
6 -from typing import Union, Dict, Optional
12 +from starlette.status import HTTP_401_UNAUTHORIZED
13
14 # App specific imports
15 from app.auth.routes.auth import auth_handler
10 -from app.db.db_session import session
11 -from app.connectors.dfir_iris.schema.cases import (
12 - CaseResponse, CaseOlderThanBody, TimeUnit, CasesBreachedResponse, SingleCaseResponse, SingleCaseBody
13 -)
14 -from app.connectors.dfir_iris.schema.notes import (
15 - NotesQueryParams, NotesResponse
16 -)
17 -
18 -from app.connectors.dfir_iris.services.cases import get_all_cases, get_cases_older_than, get_single_case
16 +from app.connectors.dfir_iris.schema.cases import CaseOlderThanBody
17 +from app.connectors.dfir_iris.schema.cases import CaseResponse
18 +from app.connectors.dfir_iris.schema.cases import CasesBreachedResponse
19 +from app.connectors.dfir_iris.schema.cases import SingleCaseBody
20 +from app.connectors.dfir_iris.schema.cases import SingleCaseResponse
21 +from app.connectors.dfir_iris.schema.cases import TimeUnit
22 +from app.connectors.dfir_iris.schema.notes import NotesQueryParams
23 +from app.connectors.dfir_iris.schema.notes import NotesResponse
24 +from app.connectors.dfir_iris.services.cases import get_all_cases
25 +from app.connectors.dfir_iris.services.cases import get_cases_older_than
26 +from app.connectors.dfir_iris.services.cases import get_single_case
27 from app.connectors.dfir_iris.utils.universal import check_case_exists
20 -
28 from app.connectors.wazuh_indexer.utils.universal import collect_indices
29 +from app.db.db_session import session
30 +
31
32 def verify_case_exists(case_id: int) -> int:
33 if not check_case_exists(case_id):
@@ -28,6 +37,7 @@ def verify_case_exists(case_id: int) -> int:
37
38 cases_router = APIRouter()
39
40 +
41 def get_timedelta(older_than: int, time_unit: TimeUnit) -> CaseOlderThanBody:
42 delta = None
43 if time_unit == TimeUnit.HOURS:
@@ -39,21 +49,20 @@ def get_timedelta(older_than: int, time_unit: TimeUnit) -> CaseOlderThanBody:
49 return CaseOlderThanBody(older_than=delta, time_unit=time_unit)
50
51
42 -
52 @cases_router.get("", response_model=CaseResponse, description="Get all cases")
53 async def get_cases_route() -> CaseResponse:
54 logger.info(f"Fetching all cases")
55 return get_all_cases()
56
57 +
58 @cases_router.post("/older_than", response_model=CasesBreachedResponse, description="Get all cases older than a specified date")
59 async def get_cases_older_than_route(case_older_than_body: CaseOlderThanBody = Depends(get_timedelta)) -> CaseResponse:
60 logger.info(f"Fetching all cases older than {case_older_than_body.older_than} ({case_older_than_body.time_unit.value})")
61 return get_cases_older_than(case_older_than_body)
62
63 +
64 @cases_router.get("/{case_id}", response_model=SingleCaseResponse, description="Get a single case")
65 async def get_single_case_route(case_id: int = Depends(verify_case_exists)) -> SingleCaseResponse:
66 logger.info(f"Fetching case {case_id}")
67 single_case_body = SingleCaseBody(case_id=case_id)
68 return get_single_case(single_case_body.case_id)
58 -
59 -
backend/app/connectors/dfir_iris/routes/notes.py
+22 -25
@@ -1,27 +1,29 @@
1 +from datetime import timedelta
2 +from typing import Dict
3 from typing import List
2 -from fastapi import APIRouter, HTTPException, Security, Depends
3 -from starlette.status import HTTP_401_UNAUTHORIZED
4 +from typing import Optional
5 +from typing import Union
6 +
7 +from fastapi import APIRouter
8 +from fastapi import Depends
9 +from fastapi import HTTPException
10 +from fastapi import Security
11 from loguru import logger
5 -from datetime import timedelta
6 -from typing import Union, Dict, Optional
12 +from starlette.status import HTTP_401_UNAUTHORIZED
13
14 # App specific imports
15 from app.auth.routes.auth import auth_handler
10 -from app.db.db_session import session
11 -
12 -from app.connectors.dfir_iris.schema.notes import (
13 - NotesQueryParams, NotesResponse, NoteCreationBody, NoteCreationResponse
14 -)
15 -
16 -from app.connectors.dfir_iris.schema.cases import (
17 - SingleCaseBody
18 -)
19 -
20 -from app.connectors.dfir_iris.services.notes import get_case_notes, create_case_note
21 -
16 +from app.connectors.dfir_iris.schema.cases import SingleCaseBody
17 +from app.connectors.dfir_iris.schema.notes import NoteCreationBody
18 +from app.connectors.dfir_iris.schema.notes import NoteCreationResponse
19 +from app.connectors.dfir_iris.schema.notes import NotesQueryParams
20 +from app.connectors.dfir_iris.schema.notes import NotesResponse
21 +from app.connectors.dfir_iris.services.notes import create_case_note
22 +from app.connectors.dfir_iris.services.notes import get_case_notes
23 from app.connectors.dfir_iris.utils.universal import check_case_exists
23 -
24 from app.connectors.wazuh_indexer.utils.universal import collect_indices
25 +from app.db.db_session import session
26 +
27
28 def verify_case_exists(case_id: int) -> int:
29 if not check_case_exists(case_id):
@@ -33,18 +35,13 @@ notes_router = APIRouter()
35
36
37 @notes_router.get("/{case_id}", response_model=NotesResponse, description="Get all notes for a case")
36 -async def get_case_notes_route(
37 - case_id: int = Depends(verify_case_exists),
38 - search_term: Optional[str] = "%") -> NotesResponse:
38 +async def get_case_notes_route(case_id: int = Depends(verify_case_exists), search_term: Optional[str] = "%") -> NotesResponse:
39 logger.info(f"Fetching notes for case {case_id}")
40 return get_case_notes(case_id, search_term)
41
42 +
43 @notes_router.post("/{case_id}", response_model=NoteCreationResponse, description="Create a note for a case")
43 -async def create_case_note_route(
44 - case_id: int,
45 - note_creation_body: NoteCreationBody) -> NoteCreationResponse:
44 +async def create_case_note_route(case_id: int, note_creation_body: NoteCreationBody) -> NoteCreationResponse:
45 verify_case_exists(case_id)
46 logger.info(f"Creating a note for case {case_id}")
47 return create_case_note(case_id, note_creation_body)
49 -
50 -
backend/app/connectors/dfir_iris/routes/users.py
+19 -17
@@ -1,32 +1,34 @@
1 +from datetime import timedelta
2 +from typing import Dict
3 from typing import List
2 -from fastapi import APIRouter, HTTPException, Security, Depends
3 -from starlette.status import HTTP_401_UNAUTHORIZED
4 +from typing import Optional
5 +from typing import Union
6 +
7 +from fastapi import APIRouter
8 +from fastapi import Depends
9 +from fastapi import HTTPException
10 +from fastapi import Security
11 from loguru import logger
5 -from datetime import timedelta
6 -from typing import Union, Dict, Optional
12 +from starlette.status import HTTP_401_UNAUTHORIZED
13
14 # App specific imports
15 from app.auth.routes.auth import auth_handler
16 +from app.connectors.dfir_iris.schema.alerts import AlertResponse
17 +from app.connectors.dfir_iris.schema.users import User
18 +from app.connectors.dfir_iris.schema.users import UsersResponse
19 +from app.connectors.dfir_iris.services.users import assign_user_to_alert
20 +from app.connectors.dfir_iris.services.users import get_users
21 +from app.connectors.dfir_iris.utils.universal import check_alert_exists
22 +from app.connectors.dfir_iris.utils.universal import check_user_exists
23 from app.db.db_session import session
24
12 -from app.connectors.dfir_iris.schema.users import (
13 - UsersResponse, User
14 -)
15 -
16 -from app.connectors.dfir_iris.schema.alerts import (
17 - AlertResponse
18 -)
19 -
20 -from app.connectors.dfir_iris.services.users import get_users, assign_user_to_alert
21 -
22 -from app.connectors.dfir_iris.utils.universal import check_user_exists, check_alert_exists
23 -
25
26 def verify_user_exists(user_id: int) -> int:
27 if not check_user_exists(user_id):
28 raise HTTPException(status_code=400, detail=f"User {user_id} does not exist.")
29 return user_id
30
31 +
32 def verify_alert_exists(alert_id: str) -> str:
33 if not check_alert_exists(alert_id):
34 raise HTTPException(status_code=400, detail=f"Alert {alert_id} does not exist.")
@@ -41,8 +43,8 @@ async def get_all_users() -> UsersResponse:
43 logger.info(f"Fetching all users")
44 return get_users()
45
46 +
47 @dfir_iris_users_router.post("/assign/{alert_id}/{user_id}", response_model=AlertResponse, description="Assign a user to an alert")
48 async def assign_user_to_alert_route(alert_id: str = Depends(verify_alert_exists), user_id: int = Depends(verify_user_exists)) -> User:
49 logger.info(f"Assigning user {user_id} to alert {alert_id}")
50 return assign_user_to_alert(alert_id, user_id)
48 -
backend/app/connectors/dfir_iris/schema/alerts.py
+10 -3
@@ -1,18 +1,25 @@
1 -from typing import List, Optional, Dict, Any
2 -from pydantic import BaseModel, Field
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 +
6 +from pydantic import BaseModel
7 +from pydantic import Field
8 +
9
10 class AlertsResponse(BaseModel):
11 alerts: Optional[List[Dict[str, Any]]] = Field([], description="The alerts returned from the search.")
12 message: str
13 success: bool
14
15 +
16 class AlertResponse(BaseModel):
17 alert: Optional[Dict[str, Any]] = Field({}, description="The alert returned from the search.")
18 message: str
19 success: bool
20
21 +
22 class BookmarkedAlertsResponse(BaseModel):
23 bookmarked_alerts: Optional[List[Dict[str, Any]]] = Field([], description="The alerts returned from the search.")
24 message: str
25 success: bool
18 -
backend/app/connectors/dfir_iris/schema/assets.py
+8 -3
@@ -1,10 +1,14 @@
1 -from typing import List, Optional
1 +from typing import List
2 +from typing import Optional
3 +
4 from pydantic import BaseModel
5
6 +
7 class AssetState(BaseModel):
8 object_last_update: str
9 object_state: int
10
11 +
12 class Asset(BaseModel):
13 analysis_status: str
14 analysis_status_id: int
@@ -23,13 +27,14 @@ class Asset(BaseModel):
27 ioc_links: Optional[None]
28 link: List
29
30 +
31 class AssetData(BaseModel):
32 assets: List[Asset]
33 state: AssetState
34
35 +
36 class AssetResponse(BaseModel):
31 - assets: List[Asset]
37 + assets: List[Asset]
38 state: AssetState
39 message: str
40 success: bool
35 -
backend/app/connectors/dfir_iris/schema/cases.py
+19 -8
@@ -1,9 +1,13 @@
1 -from pydantic import BaseModel, Field
2 -from typing import List, Optional
3 -from enum import Enum
4 -from datetime import timedelta
5 -from typing import Union, Dict
1 from datetime import datetime
2 +from datetime import timedelta
3 +from enum import Enum
4 +from typing import Dict
5 +from typing import List
6 +from typing import Optional
7 +from typing import Union
8 +
9 +from pydantic import BaseModel
10 +from pydantic import Field
11
12
13 class CaseModel(BaseModel):
@@ -25,16 +29,19 @@ class CaseModel(BaseModel):
29 state_id: int
30 state_name: str
31
32 +
33 class CaseResponse(BaseModel):
34 cases: List[CaseModel]
35 message: str
36 success: bool
37
38 +
39 class ModificationHistoryItem(BaseModel):
40 action: str
41 user: str
42 user_id: int
43
44 +
45 class SingleCaseModel(BaseModel):
46 case_description: str
47 case_id: int
@@ -63,18 +70,22 @@ class SingleCaseModel(BaseModel):
70 status_id: int
71 status_name: str
72
73 +
74 class SingleCaseBody(BaseModel):
75 case_id: int
76
77 +
78 class SingleCaseResponse(BaseModel):
79 case: SingleCaseModel
80 message: str
81 success: bool
82
83 +
84 class TimeUnit(str, Enum):
75 - HOURS = 'hours'
76 - DAYS = 'days'
77 - WEEKS = 'weeks'
85 + HOURS = "hours"
86 + DAYS = "days"
87 + WEEKS = "weeks"
88 +
89
90 class CaseOlderThanBody(BaseModel):
91 older_than: timedelta = Field(..., description="Amount of time to filter cases by")
backend/app/connectors/dfir_iris/schema/notes.py
+16 -3
@@ -1,11 +1,17 @@
1 from datetime import datetime
2 -from typing import List, Dict, Optional
3 -from pydantic import BaseModel, Field
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 +
6 +from pydantic import BaseModel
7 +from pydantic import Field
8 +
9
10 class CustomAttributes(BaseModel):
11 # Define additional fields if custom_attributes contains specific keys
12 pass
13
14 +
15 class NoteDetails(BaseModel):
16 custom_attributes: CustomAttributes
17 group_id: int
@@ -18,29 +24,35 @@ class NoteDetails(BaseModel):
24 note_title: str
25 note_uuid: str
26
27 +
28 class NoteDetailsResponse(BaseModel):
29 note_details: NoteDetails
30 message: str
31 success: bool
32
33 +
34 class NoteItem(BaseModel):
35 note_details: NoteDetails
36 note_id: int
37 note_title: str
38
39 +
40 class NotesResponse(BaseModel):
41 notes: List[NoteItem]
42 message: str
43 success: bool
44
45 +
46 class NotesQueryParams(BaseModel):
47 case_id: int
48 search_term: Optional[str] = Field("%", description="Search term to filter notes by. Defaults to wildcard search (%).")
49
50 +
51 class NoteCreationBody(BaseModel):
52 note_title: str = Field(..., description="Title of the note to be created.")
53 note_content: str = Field(..., description="Content of the note to be created.")
54
55 +
56 class NoteAttributes(BaseModel):
57 custom_attributes: Dict[str, str] = Field(...)
58 note_content: str = Field(...)
@@ -50,7 +62,8 @@ class NoteAttributes(BaseModel):
62 note_title: str = Field(...)
63 note_uuid: str = Field(...)
64
65 +
66 class NoteCreationResponse(BaseModel):
67 message: str = Field(...)
68 note: NoteAttributes = Field(...)
56 - success: bool = Field(...)
69 + success: bool = Field(...)
backend/app/connectors/dfir_iris/schema/users.py
+3
@@ -1,6 +1,8 @@
1 from typing import List
2 +
3 from pydantic import BaseModel
4
5 +
6 class User(BaseModel):
7 user_active: bool
8 user_id: int
@@ -8,6 +10,7 @@ class User(BaseModel):
10 user_name: str
11 user_uuid: str
12
13 +
14 class UsersResponse(BaseModel):
15 message: str
16 success: bool
backend/app/connectors/dfir_iris/services/alerts.py
+17 -7
@@ -1,11 +1,21 @@
1 from datetime import datetime
2 -from typing import List, Dict, Any, Callable, Tuple
2 +from typing import Any
3 +from typing import Callable
4 +from typing import Dict
5 +from typing import List
6 +from typing import Tuple
7 +
8 +from dfir_iris_client.alert import Alert
9 from fastapi import HTTPException
10 from loguru import logger
5 -from dfir_iris_client.alert import Alert
6 -from app.connectors.dfir_iris.schema.alerts import AlertsResponse, AlertResponse, BookmarkedAlertsResponse
7 -from app.connectors.dfir_iris.utils.universal import create_dfir_iris_client, fetch_and_parse_data, initialize_client_and_alert, fetch_and_validate_data
11
12 +from app.connectors.dfir_iris.schema.alerts import AlertResponse
13 +from app.connectors.dfir_iris.schema.alerts import AlertsResponse
14 +from app.connectors.dfir_iris.schema.alerts import BookmarkedAlertsResponse
15 +from app.connectors.dfir_iris.utils.universal import create_dfir_iris_client
16 +from app.connectors.dfir_iris.utils.universal import fetch_and_parse_data
17 +from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
18 +from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
19
20
21 def get_alerts() -> AlertsResponse:
@@ -13,6 +23,7 @@ def get_alerts() -> AlertsResponse:
23 result = fetch_and_validate_data(client, alert.filter_alerts)
24 return AlertsResponse(success=True, message="Successfully fetched alerts", alerts=result["data"]["alerts"])
25
26 +
27 def bookmark_alert(alert_id: str, bookmarked: bool) -> AlertResponse:
28 client, alert = initialize_client_and_alert("DFIR-IRIS")
29 if bookmarked:
@@ -21,13 +32,12 @@ def bookmark_alert(alert_id: str, bookmarked: bool) -> AlertResponse:
32 result = fetch_and_validate_data(client, alert.update_alert, alert_id, {"alert_tags": ""})
33 return AlertResponse(success=True, message="Successfully removed bookmark from alert", alert=result["data"])
34
35 +
36 def get_bookmarked_alerts() -> BookmarkedAlertsResponse:
37 alerts = get_alerts().alerts
38 bookmarked_alerts = []
39 for alert in alerts:
40 if alert["alert_tags"] is not None and "bookmarked" in alert["alert_tags"]:
41 bookmarked_alerts.append(alert)
30 -
31 - return BookmarkedAlertsResponse(success=True, message="Successfully fetched bookmarked alerts", bookmarked_alerts=bookmarked_alerts)
32 -
42
43 + return BookmarkedAlertsResponse(success=True, message="Successfully fetched bookmarked alerts", bookmarked_alerts=bookmarked_alerts)
backend/app/connectors/dfir_iris/services/assets.py
+22 -11
@@ -1,23 +1,34 @@
1 from datetime import datetime
2 -from typing import List, Dict, Any, Callable, Tuple
2 +from typing import Any
3 +from typing import Callable
4 +from typing import Dict
5 +from typing import List
6 +from typing import Tuple
7 +
8 +from dfir_iris_client.case import Case
9 from fastapi import HTTPException
10 from loguru import logger
5 -from dfir_iris_client.case import Case
6 -from app.connectors.dfir_iris.schema.assets import AssetResponse, Asset, AssetState
7 -from app.connectors.dfir_iris.utils.universal import create_dfir_iris_client, fetch_and_parse_data, initialize_client_and_case, fetch_and_validate_data, handle_error
11 +
12 +from app.connectors.dfir_iris.schema.assets import Asset
13 +from app.connectors.dfir_iris.schema.assets import AssetResponse
14 +from app.connectors.dfir_iris.schema.assets import AssetState
15 +from app.connectors.dfir_iris.utils.universal import create_dfir_iris_client
16 +from app.connectors.dfir_iris.utils.universal import fetch_and_parse_data
17 +from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
18 +from app.connectors.dfir_iris.utils.universal import handle_error
19 +from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
20
21
22 def get_case_assets(case_id: int) -> AssetResponse:
23 client, case = initialize_client_and_case("DFIR-IRIS")
24 result = fetch_and_validate_data(client, case.list_assets, case_id)
13 -
14 - asset_list = result["data"]["assets"]
25 +
26 + asset_list = result["data"]["assets"]
27 state_data = result["data"]["state"]
16 -
28 +
29 return AssetResponse(
18 - success=True,
19 - message="Successfully fetched assets for case",
30 + success=True,
31 + message="Successfully fetched assets for case",
32 assets=[Asset(**asset) for asset in asset_list], # List[Asset]
21 - state=AssetState(**state_data) # AssetState
33 + state=AssetState(**state_data), # AssetState
34 )
23 -
backend/app/connectors/dfir_iris/services/cases.py
+33 -8
@@ -1,11 +1,27 @@
1 from datetime import datetime
2 -from typing import List, Dict, Any, Callable, Tuple
2 +from typing import Any
3 +from typing import Callable
4 +from typing import Dict
5 +from typing import List
6 +from typing import Tuple
7 +
8 +from dfir_iris_client.case import Case
9 from fastapi import HTTPException
10 from loguru import logger
5 -from dfir_iris_client.case import Case
6 -from app.connectors.dfir_iris.schema.cases import CaseModel, CaseResponse, CaseOlderThanBody, CasesBreachedResponse, SingleCaseBody, SingleCaseResponse
7 -from app.connectors.dfir_iris.schema.notes import NotesResponse, NotesQueryParams, NoteDetails, NoteDetailsResponse
8 -from app.connectors.dfir_iris.utils.universal import create_dfir_iris_client, fetch_and_parse_data
11 +
12 +from app.connectors.dfir_iris.schema.cases import CaseModel
13 +from app.connectors.dfir_iris.schema.cases import CaseOlderThanBody
14 +from app.connectors.dfir_iris.schema.cases import CaseResponse
15 +from app.connectors.dfir_iris.schema.cases import CasesBreachedResponse
16 +from app.connectors.dfir_iris.schema.cases import SingleCaseBody
17 +from app.connectors.dfir_iris.schema.cases import SingleCaseResponse
18 +from app.connectors.dfir_iris.schema.notes import NoteDetails
19 +from app.connectors.dfir_iris.schema.notes import NoteDetailsResponse
20 +from app.connectors.dfir_iris.schema.notes import NotesQueryParams
21 +from app.connectors.dfir_iris.schema.notes import NotesResponse
22 +from app.connectors.dfir_iris.utils.universal import create_dfir_iris_client
23 +from app.connectors.dfir_iris.utils.universal import fetch_and_parse_data
24 +
25
26 def get_client_and_cases() -> Dict:
27 """
@@ -20,6 +36,7 @@ def get_client_and_cases() -> Dict:
36 result = fetch_and_parse_data(dfir_iris_client, case.list_cases)
37 return result
38
39 +
40 def filter_open_cases(cases: List[Dict]) -> List[Dict]:
41 """
42 Filters out cases that are still open.
@@ -32,6 +49,7 @@ def filter_open_cases(cases: List[Dict]) -> List[Dict]:
49 """
50 return [case for case in cases if case["case_close_date"] == ""]
51
52 +
53 def filter_cases_older_than(cases: List[Dict], older_than: datetime) -> List[Dict]:
54 """
55 Filters out cases that are older than the specified time.
@@ -46,12 +64,17 @@ def filter_cases_older_than(cases: List[Dict], older_than: datetime) -> List[Dic
64 current_time = datetime.now()
65 filtered_cases = []
66 for case in cases:
49 - case_open_date = datetime.strptime(case["case_open_date"], "%m/%d/%Y") if not isinstance(case["case_open_date"], datetime) else case["case_open_date"]
67 + case_open_date = (
68 + datetime.strptime(case["case_open_date"], "%m/%d/%Y")
69 + if not isinstance(case["case_open_date"], datetime)
70 + else case["case_open_date"]
71 + )
72 if case_open_date < current_time - older_than:
73 case["case_open_date"] = case_open_date.strftime("%m/%d/%Y") # Convert back to string to match the model
74 filtered_cases.append(case)
75 return filtered_cases
76
77 +
78 def get_all_cases() -> CaseResponse:
79 result = get_client_and_cases()
80 if not result["success"]:
@@ -59,12 +82,13 @@ def get_all_cases() -> CaseResponse:
82 return HTTPException(status_code=500, detail=f"Failed to get all cases: {result['message']}")
83 return CaseResponse(success=True, message="Successfully fetched all cases", cases=result["data"])
84
85 +
86 def get_cases_older_than(case_older_than_body: CaseOlderThanBody) -> CasesBreachedResponse:
87 result = get_client_and_cases()
88 if not result["success"]:
89 logger.error(f"Failed to get all cases: {result['message']}")
90 return HTTPException(status_code=500, detail=f"Failed to get all cases: {result['message']}")
67 -
91 +
92 open_cases = filter_open_cases(result["data"])
93 breached_cases = filter_cases_older_than(open_cases, case_older_than_body.older_than)
94 return CasesBreachedResponse(
@@ -73,8 +97,9 @@ def get_cases_older_than(case_older_than_body: CaseOlderThanBody) -> CasesBreach
97 cases_breached=breached_cases,
98 )
99
100 +
101 def get_single_case(case_id: SingleCaseBody) -> SingleCaseResponse:
102 dfir_iris_client = create_dfir_iris_client("DFIR-IRIS")
103 case = Case(session=dfir_iris_client)
104 result = fetch_and_parse_data(dfir_iris_client, case.get_case, case_id)
80 - return SingleCaseResponse(success=True, message="Successfully fetched single case", case=result["data"])
\ No newline at end of file
105 + return SingleCaseResponse(success=True, message="Successfully fetched single case", case=result["data"])
backend/app/connectors/dfir_iris/services/notes.py
+38 -6
@@ -1,11 +1,31 @@
1 from datetime import datetime
2 -from typing import List, Dict, Any, Callable, Tuple
2 +from typing import Any
3 +from typing import Callable
4 +from typing import Dict
5 +from typing import List
6 +from typing import Tuple
7 +
8 +from dfir_iris_client.case import Case
9 from fastapi import HTTPException
10 from loguru import logger
5 -from dfir_iris_client.case import Case
6 -from app.connectors.dfir_iris.schema.cases import CaseModel, CaseResponse, CaseOlderThanBody, CasesBreachedResponse, SingleCaseBody, SingleCaseResponse
7 -from app.connectors.dfir_iris.schema.notes import NotesResponse, NotesQueryParams, NoteDetails, NoteDetailsResponse, NoteCreationBody, NoteCreationResponse
8 -from app.connectors.dfir_iris.utils.universal import create_dfir_iris_client, fetch_and_parse_data, initialize_client_and_case, fetch_and_validate_data, handle_error
11 +
12 +from app.connectors.dfir_iris.schema.cases import CaseModel
13 +from app.connectors.dfir_iris.schema.cases import CaseOlderThanBody
14 +from app.connectors.dfir_iris.schema.cases import CaseResponse
15 +from app.connectors.dfir_iris.schema.cases import CasesBreachedResponse
16 +from app.connectors.dfir_iris.schema.cases import SingleCaseBody
17 +from app.connectors.dfir_iris.schema.cases import SingleCaseResponse
18 +from app.connectors.dfir_iris.schema.notes import NoteCreationBody
19 +from app.connectors.dfir_iris.schema.notes import NoteCreationResponse
20 +from app.connectors.dfir_iris.schema.notes import NoteDetails
21 +from app.connectors.dfir_iris.schema.notes import NoteDetailsResponse
22 +from app.connectors.dfir_iris.schema.notes import NotesQueryParams
23 +from app.connectors.dfir_iris.schema.notes import NotesResponse
24 +from app.connectors.dfir_iris.utils.universal import create_dfir_iris_client
25 +from app.connectors.dfir_iris.utils.universal import fetch_and_parse_data
26 +from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
27 +from app.connectors.dfir_iris.utils.universal import handle_error
28 +from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
29
30
31 def process_notes(notes: List[Dict], case_id: int) -> List[Dict]:
@@ -17,23 +37,35 @@ def process_notes(notes: List[Dict], case_id: int) -> List[Dict]:
37 processed_notes.append(note)
38 return processed_notes
39
40 +
41 def get_case_notes(case_id: int, search_term: str) -> NotesResponse:
42 client, case = initialize_client_and_case("DFIR-IRIS")
43 result = fetch_and_validate_data(client, case.search_notes, search_term, case_id)
44 processed_notes = process_notes(result["data"], case_id)
45 return NotesResponse(success=True, message="Successfully fetched notes for case", notes=processed_notes)
46
47 +
48 def get_case_note_details(note_id: int, case_id: int) -> NoteDetailsResponse:
49 client, case = initialize_client_and_case("DFIR-IRIS")
50 result = fetch_and_validate_data(client, case.get_note, note_id, case_id)
51 note_details = NoteDetails(**result["data"])
52 return NoteDetailsResponse(success=True, message="Successfully fetched note details", note_details=note_details)
53
54 +
55 def perform_note_creation(client: Any, case: Case, note_creation_body: NoteCreationBody, case_id: int) -> Dict:
56 result = fetch_and_validate_data(client, case.add_notes_group, note_creation_body.note_title, case_id)
57 note_id = result["data"]["group_id"]
58 custom_attributes = {}
36 - return fetch_and_validate_data(client, case.add_note, note_creation_body.note_title, note_creation_body.note_content, note_id, custom_attributes, case_id)
59 + return fetch_and_validate_data(
60 + client,
61 + case.add_note,
62 + note_creation_body.note_title,
63 + note_creation_body.note_content,
64 + note_id,
65 + custom_attributes,
66 + case_id,
67 + )
68 +
69
70 def create_case_note(case_id: int, note_creation_body: NoteCreationBody) -> NoteCreationResponse:
71 client, case = initialize_client_and_case("DFIR-IRIS")
backend/app/connectors/dfir_iris/services/users.py
+17 -6
@@ -1,12 +1,22 @@
1 from datetime import datetime
2 -from typing import List, Dict, Any, Callable, Tuple
2 +from typing import Any
3 +from typing import Callable
4 +from typing import Dict
5 +from typing import List
6 +from typing import Tuple
7 +
8 +from dfir_iris_client.alert import Alert
9 from fastapi import HTTPException
10 from loguru import logger
5 -from dfir_iris_client.alert import Alert
6 -from app.connectors.dfir_iris.schema.users import UsersResponse, User
7 -from app.connectors.dfir_iris.schema.alerts import AlertResponse
8 -from app.connectors.dfir_iris.utils.universal import create_dfir_iris_client, fetch_and_parse_data, initialize_client_and_user, fetch_and_validate_data, initialize_client_and_alert
11
12 +from app.connectors.dfir_iris.schema.alerts import AlertResponse
13 +from app.connectors.dfir_iris.schema.users import User
14 +from app.connectors.dfir_iris.schema.users import UsersResponse
15 +from app.connectors.dfir_iris.utils.universal import create_dfir_iris_client
16 +from app.connectors.dfir_iris.utils.universal import fetch_and_parse_data
17 +from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
18 +from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
19 +from app.connectors.dfir_iris.utils.universal import initialize_client_and_user
20
21
22 def get_users() -> UsersResponse:
@@ -14,7 +24,8 @@ def get_users() -> UsersResponse:
24 result = fetch_and_validate_data(client, user.list_users)
25 return UsersResponse(success=True, message="Successfully fetched users", users=result["data"])
26
27 +
28 def assign_user_to_alert(alert_id: str, user_id: int) -> AlertResponse:
29 client, alert = initialize_client_and_alert("DFIR-IRIS")
30 result = fetch_and_validate_data(client, alert.update_alert, alert_id, {"alert_owner_id": user_id})
20 - return AlertResponse(success=True, message="Successfully assigned user to alert", alert=result["data"])
\ No newline at end of file
31 + return AlertResponse(success=True, message="Successfully assigned user to alert", alert=result["data"])
backend/app/connectors/dfir_iris/utils/universal.py
+62 -41
@@ -1,23 +1,35 @@
1 -from typing import Dict, Any, List, Generator, Type, Callable
2 -from sqlmodel import Session, select
3 -from app.connectors.models import Connectors
4 -from elasticsearch7 import Elasticsearch
5 -from loguru import logger
6 -from app.db.db_session import engine
1 +from datetime import datetime
2 +from datetime import timedelta
3 +from typing import Any
4 +from typing import Callable
5 +from typing import Dict
6 +from typing import Generator
7 +from typing import Iterable
8 +from typing import List
9 +from typing import Optional
10 +from typing import Tuple
11 +from typing import Type
12 +from typing import Union
13 +
14 import requests
8 -from app.connectors.schema import ConnectorResponse
9 -from app.connectors.utils import get_connector_info_from_db
10 -from app.connectors.wazuh_indexer.schema.indices import Indices, IndexConfigModel
11 -from datetime import datetime, timedelta
12 -from typing import Iterable, Tuple, Union, Optional
15 +from dfir_iris_client.alert import Alert
16 +from dfir_iris_client.case import Case
17 from dfir_iris_client.helper.utils import assert_api_resp
18 from dfir_iris_client.helper.utils import get_data_from_resp
19 from dfir_iris_client.session import ClientSession
16 -from dfir_iris_client.alert import Alert
17 -from dfir_iris_client.case import Case
20 from dfir_iris_client.users import User
21 +from elasticsearch7 import Elasticsearch
22 from fastapi import HTTPException
23 +from loguru import logger
24 +from sqlmodel import Session
25 +from sqlmodel import select
26
27 +from app.connectors.models import Connectors
28 +from app.connectors.schema import ConnectorResponse
29 +from app.connectors.utils import get_connector_info_from_db
30 +from app.connectors.wazuh_indexer.schema.indices import IndexConfigModel
31 +from app.connectors.wazuh_indexer.schema.indices import Indices
32 +from app.db.db_session import engine
33
34
35 def verify_dfir_iris_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
@@ -28,7 +40,7 @@ def verify_dfir_iris_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
40 dict: A dictionary containing 'connectionSuccessful' status and 'authToken' if the connection is successful.
41 """
42 logger.info(f"Verifying the DFIR-IRIS connection to {attributes['connector_url']}")
31 -
43 +
44 try:
45 headers = {
46 "Authorization": f"Bearer {attributes['connector_api_key']}",
@@ -48,7 +60,8 @@ def verify_dfir_iris_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
60 except Exception as e:
61 logger.error(f"Connection to {attributes['connector_url']} failed with error: {e}")
62 return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error: {e}"}
51 -
63 +
64 +
65 def verify_dfir_iris_connection(connector_name: str) -> str:
66 """
67 Returns the authentication token for the DFIR-IRIS service.
@@ -62,6 +75,7 @@ def verify_dfir_iris_connection(connector_name: str) -> str:
75 return None
76 return verify_dfir_iris_credentials(attributes)
77
78 +
79 def create_dfir_iris_client(connector_name: str) -> ClientSession:
80 """
81 Creates a session with DFIR-IRIS.
@@ -87,29 +101,30 @@ def create_dfir_iris_client(connector_name: str) -> ClientSession:
101 except Exception as e:
102 logger.error(f"Error creating session with DFIR-IRIS: {e}")
103 return HTTPException(status_code=500, detail=f"Error creating session with DFIR-IRIS: {e}")
90 -
104 +
105 +
106 def fetch_and_parse_data(session: ClientSession, action: Callable, *args) -> Dict[str, Union[bool, Optional[Dict]]]:
92 - """
93 - Fetches and parses data from DFIR-IRIS using a specified action.
94 -
95 - Args:
96 - session (ClientSession): The DFIR-IRIS session object.
97 - action (Callable): The function to execute to fetch data from DFIR-IRIS. This function should accept *args.
98 - args: The arguments to pass to the action function.
99 -
100 - Returns:
101 - dict: A dictionary containing the success status and either the fetched data or None if the operation was unsuccessful.
102 - """
103 - try:
104 - logger.info(f"Executing {action.__name__}... on args: {args}")
105 - status = action(*args)
106 - assert_api_resp(status, soft_fail=False)
107 - data = get_data_from_resp(status)
108 - logger.info(f"Successfully executed {action.__name__}")
109 - return {"success": True, "data": data}
110 - except Exception as err:
111 - logger.error(f"Failed to execute {action.__name__}: {err}")
112 - return HTTPException(status_code=500, detail=f"Failed to execute {action.__name__}: {err}")
107 + """
108 + Fetches and parses data from DFIR-IRIS using a specified action.
109 +
110 + Args:
111 + session (ClientSession): The DFIR-IRIS session object.
112 + action (Callable): The function to execute to fetch data from DFIR-IRIS. This function should accept *args.
113 + args: The arguments to pass to the action function.
114 +
115 + Returns:
116 + dict: A dictionary containing the success status and either the fetched data or None if the operation was unsuccessful.
117 + """
118 + try:
119 + logger.info(f"Executing {action.__name__}... on args: {args}")
120 + status = action(*args)
121 + assert_api_resp(status, soft_fail=False)
122 + data = get_data_from_resp(status)
123 + logger.info(f"Successfully executed {action.__name__}")
124 + return {"success": True, "data": data}
125 + except Exception as err:
126 + logger.error(f"Failed to execute {action.__name__}: {err}")
127 + return HTTPException(status_code=500, detail=f"Failed to execute {action.__name__}: {err}")
128
129
130 def initialize_client_and_case(service_name: str) -> Tuple[Any, Case]:
@@ -117,26 +132,31 @@ def initialize_client_and_case(service_name: str) -> Tuple[Any, Case]:
132 case = Case(session=dfir_iris_client)
133 return dfir_iris_client, case
134
135 +
136 def initialize_client_and_alert(service_name: str) -> Tuple[Any, Alert]:
137 dfir_iris_client = create_dfir_iris_client(service_name)
138 alert = Alert(session=dfir_iris_client)
139 return dfir_iris_client, alert
140
141 +
142 def initialize_client_and_user(service_name: str) -> Tuple[Any, Alert]:
143 dfir_iris_client = create_dfir_iris_client(service_name)
144 user = User(session=dfir_iris_client)
145 return dfir_iris_client, user
146
147 +
148 def handle_error(error_message: str, status_code: int = 500):
149 logger.error(error_message)
150 raise HTTPException(status_code=status_code, detail=error_message)
151
152 +
153 def fetch_and_validate_data(client: Any, func: Callable, *args: Any) -> Dict:
154 result = fetch_and_parse_data(client, func, *args)
155 if not result["success"]:
156 handle_error(f"Failed to fetch data: {result['message']}")
157 return result
139 -
158 +
159 +
160 def check_case_exists(case_id: int) -> bool:
161 try:
162 logger.info(f"Checking if case {case_id} exists")
@@ -153,7 +173,8 @@ def check_case_exists(case_id: int) -> bool:
173 except Exception as e:
174 logger.error(f"Failed to check if case {case_id} exists: {e}")
175 return False
156 -
176 +
177 +
178 def check_alert_exists(alert_id: str) -> bool:
179 try:
180 logger.info(f"Checking if alert {alert_id} exists")
@@ -170,7 +191,8 @@ def check_alert_exists(alert_id: str) -> bool:
191 except Exception as e:
192 logger.error(f"Failed to check if alert {alert_id} exists: {e}")
193 return False
173 -
194 +
195 +
196 def check_user_exists(user_id: int) -> bool:
197 try:
198 logger.info(f"Checking if user {user_id} exists")
@@ -187,4 +209,3 @@ def check_user_exists(user_id: int) -> bool:
209 except Exception as e:
210 logger.error(f"Failed to check if user {user_id} exists: {e}")
211 return False
190 -
backend/app/connectors/graylog/routes/collector.py
+18 -7
@@ -1,15 +1,23 @@
1 from typing import List
2 -from fastapi import APIRouter, HTTPException, Security, Depends
3 -from starlette.status import HTTP_401_UNAUTHORIZED
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 starlette.status import HTTP_401_UNAUTHORIZED
9
10 # App specific imports
11 from app.auth.routes.auth import auth_handler
12 +from app.connectors.graylog.schema.collector import ConfiguredInputsResponse
13 +from app.connectors.graylog.schema.collector import GraylogIndicesResponse
14 +from app.connectors.graylog.schema.collector import GraylogInputsResponse
15 +from app.connectors.graylog.schema.collector import RunningInputsResponse
16 +from app.connectors.graylog.services.collector import get_indices_full
17 +from app.connectors.graylog.services.collector import get_inputs
18 +from app.connectors.graylog.services.collector import get_inputs_configured
19 +from app.connectors.graylog.services.collector import get_inputs_running
20 from app.db.db_session import session
9 -from app.connectors.graylog.schema.collector import (
10 - GraylogIndicesResponse, GraylogInputsResponse, RunningInputsResponse, ConfiguredInputsResponse
11 -)
12 -from app.connectors.graylog.services.collector import get_indices_full, get_inputs, get_inputs_running, get_inputs_configured
21
22 graylog_collector_router = APIRouter()
23
@@ -19,17 +27,20 @@ async def get_all_indices() -> GraylogIndicesResponse:
27 logger.info(f"Fetching all graylog indices")
28 return get_indices_full()
29
30 +
31 @graylog_collector_router.get("/inputs", response_model=GraylogInputsResponse, description="Get all inputs")
32 async def get_all_inputs() -> GraylogInputsResponse:
33 logger.info(f"Fetching all graylog inputs")
34 return get_inputs()
35
36 +
37 @graylog_collector_router.get("/inputs/running", response_model=RunningInputsResponse, description="Get all running inputs")
38 async def get_all_running_inputs() -> RunningInputsResponse:
39 logger.info(f"Fetching all graylog running inputs")
40 return get_inputs_running()
41
42 +
43 @graylog_collector_router.get("/inputs/configured", response_model=ConfiguredInputsResponse, description="Get all configured inputs")
44 async def get_all_configured_inputs() -> ConfiguredInputsResponse:
45 logger.info(f"Fetching all graylog configured inputs")
35 - return get_inputs_configured()
\ No newline at end of file
46 + return get_inputs_configured()
backend/app/connectors/graylog/routes/events.py
+13 -7
@@ -1,15 +1,20 @@
1 from typing import List
2 -from fastapi import APIRouter, HTTPException, Security, Depends
3 -from starlette.status import HTTP_401_UNAUTHORIZED
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 starlette.status import HTTP_401_UNAUTHORIZED
9
10 # App specific imports
11 from app.auth.routes.auth import auth_handler
12 +from app.connectors.graylog.schema.events import AlertQuery
13 +from app.connectors.graylog.schema.events import GraylogAlertsResponse
14 +from app.connectors.graylog.schema.events import GraylogEventDefinitionsResponse
15 +from app.connectors.graylog.services.events import get_alerts
16 +from app.connectors.graylog.services.events import get_event_definitions
17 from app.db.db_session import session
9 -from app.connectors.graylog.schema.events import (
10 - GraylogEventDefinitionsResponse, AlertQuery, GraylogAlertsResponse
11 -)
12 -from app.connectors.graylog.services.events import get_event_definitions, get_alerts
18
19 graylog_events_router = APIRouter()
20
@@ -19,7 +24,8 @@ async def get_all_event_definitions() -> GraylogEventDefinitionsResponse:
24 logger.info(f"Fetching all graylog event definitions")
25 return get_event_definitions()
26
27 +
28 @graylog_events_router.post("/event/alerts", response_model=GraylogAlertsResponse, description="Get all alerts")
29 async def get_all_alerts(alert_query: AlertQuery) -> GraylogAlertsResponse:
30 logger.info(f"Fetching all graylog alerts")
25 - return get_alerts(alert_query)
\ No newline at end of file
31 + return get_alerts(alert_query)
backend/app/connectors/graylog/routes/management.py
+48 -37
@@ -1,44 +1,62 @@
1 from typing import List
2 -from fastapi import APIRouter, HTTPException, Security, Depends
3 -from starlette.status import HTTP_401_UNAUTHORIZED
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 starlette.status import HTTP_401_UNAUTHORIZED
9
10 # App specific imports
11 from app.auth.routes.auth import auth_handler
12 +from app.connectors.graylog.schema.management import DeletedIndexBody
13 +from app.connectors.graylog.schema.management import DeletedIndexResponse
14 +from app.connectors.graylog.schema.management import StartInputBody
15 +from app.connectors.graylog.schema.management import StartInputResponse
16 +from app.connectors.graylog.schema.management import StartStreamBody
17 +from app.connectors.graylog.schema.management import StartStreamResponse
18 +from app.connectors.graylog.schema.management import StopInputBody
19 +from app.connectors.graylog.schema.management import StopInputResponse
20 +from app.connectors.graylog.schema.management import StopStreamBody
21 +from app.connectors.graylog.schema.management import StopStreamResponse
22 +from app.connectors.graylog.services.collector import get_index_names
23 +from app.connectors.graylog.services.collector import get_input_ids
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
29 +from app.connectors.graylog.services.streams import get_stream_ids
30 from app.db.db_session import session
9 -from app.connectors.graylog.schema.management import (
10 - DeletedIndexResponse, DeletedIndexBody, StopInputBody, StopInputResponse, StartInputBody, StartInputResponse, StopStreamBody, StopStreamResponse, StartStreamBody, StartStreamResponse
11 -)
12 -from app.connectors.graylog.services.management import delete_index, stop_input, start_input, stop_stream, start_stream
13 -
14 -from app.connectors.graylog.services.collector import (
15 - get_index_names, get_input_ids
16 -)
17 -
18 -from app.connectors.graylog.services.streams import (
19 - get_stream_ids
20 -)
31
32 graylog_management_router = APIRouter()
33
34 +
35 def get_managed_index_names() -> List[str]:
36 return get_index_names()
37
38 +
39 def get_managed_input_ids() -> List[str]:
40 return get_input_ids()
41
42 +
43 def get_managed_stream_ids() -> List[str]:
44 return get_stream_ids()
45
46 +
47 def verify_index_name(deleted_index_body: DeletedIndexBody) -> DeletedIndexBody:
48 # Remove any extra spaces from index_name
49 deleted_index_body.index_name = deleted_index_body.index_name.strip()
50
51 managed_index_names = get_managed_index_names()
52 if deleted_index_body.index_name not in managed_index_names:
39 - raise HTTPException(status_code=400, detail=f"Index name '{deleted_index_body.index_name}' is not managed by Graylog or no longer exists.")
53 + raise HTTPException(
54 + status_code=400,
55 + detail=f"Index name '{deleted_index_body.index_name}' is not managed by Graylog or no longer exists.",
56 + )
57 return deleted_index_body
58
59 +
60 def verify_input_id(stop_input_body: StopInputBody) -> StopInputBody:
61 # Remove any extra spaces from input_id
62 stop_input_body.input_id = stop_input_body.input_id.strip()
@@ -48,57 +66,50 @@ def verify_input_id(stop_input_body: StopInputBody) -> StopInputBody:
66 raise HTTPException(status_code=400, detail=f"Input ID '{stop_input_body.input_id}' is not managed by Graylog or no longer exists.")
67 return stop_input_body
68
69 +
70 def verify_stream_id(stop_stream_body: StopStreamBody) -> StopStreamBody:
71 # Remove any extra spaces from stream_id
72 stop_stream_body.stream_id = stop_stream_body.stream_id.strip()
73
74 managed_stream_ids = get_managed_stream_ids()
75 if stop_stream_body.stream_id not in managed_stream_ids:
57 - raise HTTPException(status_code=400, detail=f"Stream ID '{stop_stream_body.stream_id}' is not managed by Graylog or no longer exists.")
76 + raise HTTPException(
77 + status_code=400,
78 + detail=f"Stream ID '{stop_stream_body.stream_id}' is not managed by Graylog or no longer exists.",
79 + )
80 return stop_stream_body
81
60 -@graylog_management_router.delete("/index", response_model=DeletedIndexResponse, description="Delete index")
61 -async def delete_index_route(
62 - deleted_index_body: DeletedIndexBody = Depends(verify_index_name)
63 -) -> DeletedIndexResponse:
82
83 +@graylog_management_router.delete("/index", response_model=DeletedIndexResponse, description="Delete index")
84 +async def delete_index_route(deleted_index_body: DeletedIndexBody = Depends(verify_index_name)) -> DeletedIndexResponse:
85 logger.info(f"Deleting index {deleted_index_body.index_name}")
86
87 return delete_index(deleted_index_body.index_name)
88
69 -@graylog_management_router.post("/input/stop", response_model=StopInputResponse, description="Stop input")
70 -async def stop_input_route(
71 - stop_input_body: StopInputBody = Depends(verify_input_id)
72 -) -> StopInputResponse:
89
90 +@graylog_management_router.post("/input/stop", response_model=StopInputResponse, description="Stop input")
91 +async def stop_input_route(stop_input_body: StopInputBody = Depends(verify_input_id)) -> StopInputResponse:
92 logger.info(f"Stopping input {stop_input_body.input_id}")
93
94 return stop_input(stop_input_body.input_id)
95
78 -@graylog_management_router.post("/input/start", response_model=StartInputResponse, description="Start input")
79 -async def start_input_route(
80 - start_input_body: StartInputBody = Depends(verify_input_id)
81 -) -> StartInputResponse:
96
97 +@graylog_management_router.post("/input/start", response_model=StartInputResponse, description="Start input")
98 +async def start_input_route(start_input_body: StartInputBody = Depends(verify_input_id)) -> StartInputResponse:
99 logger.info(f"Starting input {start_input_body.input_id}")
100
101 return start_input(start_input_body.input_id)
102
87 -@graylog_management_router.post("/stream/stop", response_model=StopStreamResponse, description="Stop stream")
88 -async def stop_stream_route(
89 - stop_stream_body: StopStreamBody = Depends(verify_stream_id)
90 -) -> StopStreamResponse:
103
104 +@graylog_management_router.post("/stream/stop", response_model=StopStreamResponse, description="Stop stream")
105 +async def stop_stream_route(stop_stream_body: StopStreamBody = Depends(verify_stream_id)) -> StopStreamResponse:
106 logger.info(f"Stopping stream {stop_stream_body.stream_id}")
107
108 return stop_stream(stop_stream_body.stream_id)
109
96 -@graylog_management_router.post("/stream/start", response_model=StartStreamResponse, description="Start stream")
97 -async def start_stream_route(
98 - start_stream_body: StartStreamBody = Depends(verify_stream_id)
99 -) -> StartStreamResponse:
110
111 +@graylog_management_router.post("/stream/start", response_model=StartStreamResponse, description="Start stream")
112 +async def start_stream_route(start_stream_body: StartStreamBody = Depends(verify_stream_id)) -> StartStreamResponse:
113 logger.info(f"Starting stream {start_stream_body.stream_id}")
114
115 return start_stream(start_stream_body.stream_id)
104 -
backend/app/connectors/graylog/routes/monitoring.py
+13 -7
@@ -1,15 +1,20 @@
1 from typing import List
2 -from fastapi import APIRouter, HTTPException, Security, Depends
3 -from starlette.status import HTTP_401_UNAUTHORIZED
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 starlette.status import HTTP_401_UNAUTHORIZED
9
10 # App specific imports
11 from app.auth.routes.auth import auth_handler
12 +from app.connectors.graylog.schema.monitoring import GraylogMessages
13 +from app.connectors.graylog.schema.monitoring import GraylogMessagesResponse
14 +from app.connectors.graylog.schema.monitoring import GraylogMetricsResponse
15 +from app.connectors.graylog.services.monitoring import get_messages
16 +from app.connectors.graylog.services.monitoring import get_metrics
17 from app.db.db_session import session
9 -from app.connectors.graylog.schema.monitoring import (
10 - GraylogMessages, GraylogMessagesResponse, GraylogMetricsResponse
11 -)
12 -from app.connectors.graylog.services.monitoring import get_messages, get_metrics
18
19 graylog_monitoring_router = APIRouter()
20
@@ -20,7 +25,8 @@ async def get_all_messages(page_number: int = 1) -> GraylogMessagesResponse:
25 logger.info(f"Page number: {page_number}")
26 return get_messages(page_number)
27
28 +
29 @graylog_monitoring_router.get("/metrics", response_model=GraylogMetricsResponse, description="Get all metrics")
30 async def get_all_metrics() -> GraylogMetricsResponse:
31 logger.info(f"Fetching all graylog metrics")
26 - return get_metrics()
\ No newline at end of file
32 + return get_metrics()
backend/app/connectors/graylog/routes/pipelines.py
+12 -7
@@ -1,15 +1,19 @@
1 from typing import List
2 -from fastapi import APIRouter, HTTPException, Security, Depends
3 -from starlette.status import HTTP_401_UNAUTHORIZED
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 starlette.status import HTTP_401_UNAUTHORIZED
9
10 # App specific imports
11 from app.auth.routes.auth import auth_handler
12 +from app.connectors.graylog.schema.pipelines import GraylogPipelinesResponse
13 +from app.connectors.graylog.schema.pipelines import PipelineRulesResponse
14 +from app.connectors.graylog.services.pipelines import get_pipeline_rules
15 +from app.connectors.graylog.services.pipelines import get_pipelines
16 from app.db.db_session import session
9 -from app.connectors.graylog.schema.pipelines import (
10 - GraylogPipelinesResponse, PipelineRulesResponse
11 -)
12 -from app.connectors.graylog.services.pipelines import get_pipelines, get_pipeline_rules
17
18 graylog_pipelines_router = APIRouter()
19
@@ -19,7 +23,8 @@ async def get_all_pipelines() -> GraylogPipelinesResponse:
23 logger.info(f"Fetching all graylog pipelines")
24 return get_pipelines()
25
26 +
27 @graylog_pipelines_router.get("/pipeline/rules", response_model=PipelineRulesResponse, description="Get all pipeline rules")
28 async def get_all_pipeline_rules() -> PipelineRulesResponse:
29 logger.info(f"Fetching all graylog pipeline rules")
25 - return get_pipeline_rules()
\ No newline at end of file
30 + return get_pipeline_rules()
backend/app/connectors/graylog/routes/streams.py
+11 -7
@@ -1,15 +1,19 @@
1 from typing import List
2 -from fastapi import APIRouter, HTTPException, Security, Depends
3 -from starlette.status import HTTP_401_UNAUTHORIZED
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 starlette.status import HTTP_401_UNAUTHORIZED
9
10 # App specific imports
11 from app.auth.routes.auth import auth_handler
8 -from app.db.db_session import session
9 -from app.connectors.graylog.schema.streams import (
10 - Rule, Stream, GraylogStreamsResponse
11 -)
12 +from app.connectors.graylog.schema.streams import GraylogStreamsResponse
13 +from app.connectors.graylog.schema.streams import Rule
14 +from app.connectors.graylog.schema.streams import Stream
15 from app.connectors.graylog.services.streams import get_streams
16 +from app.db.db_session import session
17
18 graylog_streams_router = APIRouter()
19
@@ -17,4 +21,4 @@ graylog_streams_router = APIRouter()
21 @graylog_streams_router.get("/streams", response_model=GraylogStreamsResponse, description="Get all streams")
22 async def get_all_streams() -> GraylogStreamsResponse:
23 logger.info(f"Fetching all graylog streams")
20 - return get_streams()
\ No newline at end of file
24 + return get_streams()
backend/app/connectors/graylog/schema/collector.py
+24 -5
@@ -1,14 +1,21 @@
1 -from typing import List, Dict, Optional
2 -from pydantic import BaseModel, Field
1 +from typing import Dict
2 +from typing import List
3 +from typing import Optional
4 +
5 +from pydantic import BaseModel
6 +from pydantic import Field
7 +
8
9 class Document(BaseModel):
10 count: int
11 deleted: int
12
13 +
14 class Operation(BaseModel):
15 time_seconds: int
16 total: int
17
18 +
19 class ShardInfo(BaseModel):
20 documents: Document
21 flush: Operation
@@ -22,6 +29,7 @@ class ShardInfo(BaseModel):
29 segments: int
30 store_size_bytes: int
31
32 +
33 class Routing(BaseModel):
34 active: bool
35 id: int
@@ -32,21 +40,25 @@ class Routing(BaseModel):
40 relocating_to: Optional[None] # Assuming this is always None based on your example
41 state: str
42
43 +
44 class IndexInfo(BaseModel):
45 all_shards: ShardInfo
46 primary_shards: ShardInfo
47 reopened: bool
48 routing: List[Routing]
49
50 +
51 class GraylogIndexItem(BaseModel):
52 index_name: str
53 index_info: IndexInfo
54
55 +
56 class GraylogIndicesResponse(BaseModel):
57 indices: List[GraylogIndexItem]
58 message: str
59 success: bool
60
61 +
62 class ConfiguredInputAttributes(BaseModel):
63 recv_buffer_size: int
64 tcp_keepalive: bool
@@ -68,9 +80,10 @@ class ConfiguredInputAttributes(BaseModel):
80 charset_name: Optional[str]
81 allow_override_date: Optional[bool]
82
83 +
84 class ConfiguredInput(BaseModel):
85 title: str
73 - global_field: bool = Field(alias='global')
86 + global_field: bool = Field(alias="global")
87 name: str
88 content_pack: Optional[str]
89 created_at: str
@@ -81,6 +94,7 @@ class ConfiguredInput(BaseModel):
94 node: str
95 id: str
96
97 +
98 class MessageInputAttributes(BaseModel):
99 recv_buffer_size: int
100 tcp_keepalive: bool
@@ -96,9 +110,10 @@ class MessageInputAttributes(BaseModel):
110 max_message_size: int
111 tls_client_auth: str
112
113 +
114 class MessageInput(BaseModel):
115 title: str
101 - global_field: bool = Field(alias='global')
116 + global_field: bool = Field(alias="global")
117 name: str
118 content_pack: Optional[str]
119 created_at: str
@@ -109,6 +124,7 @@ class MessageInput(BaseModel):
124 node: str
125 id: str
126
127 +
128 class RunningInput(BaseModel):
129 id: str
130 state: str
@@ -116,18 +132,21 @@ class RunningInput(BaseModel):
132 detailed_message: Optional[str]
133 message_input: MessageInput
134
135 +
136 class ConfiguredInputsResponse(BaseModel):
137 configured_inputs: List[ConfiguredInput]
138 message: str
139 success: bool
140
141 +
142 class RunningInputsResponse(BaseModel):
143 running_inputs: List[RunningInput]
144 message: str
145 success: bool
146
147 +
148 class GraylogInputsResponse(BaseModel):
149 configured_inputs: List[ConfiguredInput]
150 running_inputs: List[RunningInput]
151 message: str
133 - success: bool
\ No newline at end of file
152 + success: bool
backend/app/connectors/graylog/schema/events.py
+26 -3
@@ -1,18 +1,26 @@
1 -from typing import List, Optional, Dict, Union
1 +from typing import Dict
2 +from typing import List
3 +from typing import Optional
4 +from typing import Union
5 +
6 from pydantic import BaseModel
7
8 +
9 class Provider(BaseModel):
10 require_values: bool
11 template: str
12 type: str
13
14 +
15 class FieldSpecItem(BaseModel):
16 data_type: str
17 providers: List[Provider]
18
19 +
20 class Conditions(BaseModel):
21 expression: Optional[str]
22
23 +
24 class Config(BaseModel):
25 conditions: Conditions
26 execute_every_ms: int
@@ -24,14 +32,17 @@ class Config(BaseModel):
32 streams: List[str]
33 type: str
34
35 +
36 class NotificationSettings(BaseModel):
37 backlog_size: int
38 grace_period_ms: int
39
40 +
41 class Storage(BaseModel):
42 streams: List[str]
43 type: str
44
45 +
46 class EventDefinition(BaseModel):
47 _scope: str
48 alert: bool
@@ -46,11 +57,13 @@ class EventDefinition(BaseModel):
57 storage: List[Storage]
58 title: str
59
60 +
61 class GraylogEventDefinitionsResponse(BaseModel):
62 event_definitions: List[EventDefinition]
63 message: str
64 success: bool
65
66 +
67 class AlertQuery(BaseModel):
68 query: Optional[str] = ""
69 page: int = 1
@@ -58,23 +71,28 @@ class AlertQuery(BaseModel):
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"}
73
74 +
75 class SimplifiedEventDefinition(BaseModel):
76 description: str
77 id: str
78 title: str
79
80 +
81 class Stream(BaseModel):
82 description: str
83 id: str
84 title: str
85
86 +
87 class Context(BaseModel):
88 event_definitions: Dict[str, SimplifiedEventDefinition]
89 streams: Dict[str, Stream]
90
91 +
92 class Fields(BaseModel):
93 test: str
94
95 +
96 class Event(BaseModel):
97 alert: bool
98 event_definition_id: str
@@ -95,6 +113,7 @@ class Event(BaseModel):
113 timestamp: str
114 timestamp_processing: str
115
116 +
117 class AlertEvent(BaseModel):
118 event: Event
119 index_name: str
@@ -103,12 +122,14 @@ class AlertEvent(BaseModel):
122
123 class Filter(BaseModel):
124 alerts: str
106 - event_definitions: List[str]
125 + event_definitions: List[str]
126 +
127
128 class Timerange(BaseModel):
129 range: int
130 type: str
131
132 +
133 class Parameters(BaseModel):
134 page: int
135 per_page: int
@@ -118,6 +139,7 @@ class Parameters(BaseModel):
139 timerange: Timerange
140 filter: Filter
141
142 +
143 class Alerts(BaseModel):
144 context: Context
145 duration: int
@@ -126,7 +148,8 @@ class Alerts(BaseModel):
148 total_events: int
149 used_indices: List[str]
150
151 +
152 class GraylogAlertsResponse(BaseModel):
153 alerts: Alerts
154 message: str
132 - success: bool
\ No newline at end of file
155 + success: bool
backend/app/connectors/graylog/schema/management.py
+16 -2
@@ -1,37 +1,51 @@
1 -from typing import List, Optional, Dict, Union
1 +from typing import Dict
2 +from typing import List
3 +from typing import Optional
4 +from typing import Union
5 +
6 from pydantic import BaseModel
7
8 +
9 class DeletedIndexBody(BaseModel):
10 index_name: str
11
12 +
13 class DeletedIndexResponse(BaseModel):
14 success: bool
15 message: str
16
17 +
18 class StopInputBody(BaseModel):
19 input_id: str
20
21 +
22 class StopInputResponse(BaseModel):
23 success: bool
24 message: str
25
26 +
27 class StartInputBody(BaseModel):
28 input_id: str
29
30 +
31 class StartInputResponse(BaseModel):
32 success: bool
33 message: str
34
35 +
36 class StopStreamBody(BaseModel):
37 stream_id: str
38
39 +
40 class StopStreamResponse(BaseModel):
41 success: bool
42 message: str
43
44 +
45 class StartStreamBody(BaseModel):
46 stream_id: str
47
48 +
49 class StartStreamResponse(BaseModel):
50 success: bool
37 - message: str
\ No newline at end of file
51 + message: str
backend/app/connectors/graylog/schema/monitoring.py
+14 -3
@@ -1,26 +1,36 @@
1 -from pydantic import BaseModel, Field
2 -from typing import Optional, List, Dict, Any
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 from typing import Union
6
7 +from pydantic import BaseModel
8 +from pydantic import Field
9 +
10 +
11 class GraylogMessages(BaseModel):
12 caller: str
13 content: str
14 node_id: str
15 timestamp: str
16
17 +
18 class GraylogTotalMessages(BaseModel):
19 total: int
20
21 +
22 class GraylogMessagesResponse(BaseModel):
23 graylog_messages: List[GraylogMessages]
24 success: bool
25 message: str
26 total_messages: int
27
28 +
29 class GraylogThroughputMetrics(BaseModel):
30 metric: str
31 value: float
32
33 +
34 class GraylogThroughputMetricsCollection(BaseModel):
35 graylog2_buffers_input_usage: Optional[str] = Field(alias="org.graylog2.buffers.input.usage")
36 graylog2_buffers_output_usage: Optional[str] = Field(alias="org.graylog2.buffers.output.usage")
@@ -31,13 +41,14 @@ class GraylogThroughputMetricsCollection(BaseModel):
41 graylog2_throughput_input: Optional[str] = Field(alias="org.graylog2.throughput.input")
42
43
34 -
44 class GraylogThroughputMetricsList(BaseModel):
45 throughput_metrics: List[GraylogThroughputMetrics]
46
47 +
48 class GraylogUncommittedJournalEntries(BaseModel):
49 uncommitted_journal_entries: int
50
51 +
52 class GraylogMetricsResponse(BaseModel):
53 throughput_metrics: List[GraylogThroughputMetrics]
54 uncommitted_journal_entries: int
backend/app/connectors/graylog/schema/pipelines.py
+11 -3
@@ -1,11 +1,16 @@
1 -from pydantic import BaseModel, Field
2 -from typing import List, Optional
1 +from typing import List
2 +from typing import Optional
3 +
4 +from pydantic import BaseModel
5 +from pydantic import Field
6 +
7
8 class Stage(BaseModel):
9 match: str
10 rules: List[str]
11 stage: int
12
13 +
14 class Pipeline(BaseModel):
15 created_at: str
16 description: str
@@ -16,11 +21,13 @@ class Pipeline(BaseModel):
21 stages: List[Stage]
22 title: str
23
24 +
25 class GraylogPipelinesResponse(BaseModel):
26 message: str
27 pipelines: List[Pipeline]
28 success: bool
29
30 +
31 class PipelineRule(BaseModel):
32 created_at: str
33 description: str
@@ -30,8 +37,9 @@ class PipelineRule(BaseModel):
37 source: str
38 title: str
39
40 +
41 # Define the main response model
42 class PipelineRulesResponse(BaseModel):
43 message: str
44 pipeline_rules: List[PipelineRule]
37 - success: bool
\ No newline at end of file
45 + success: bool
backend/app/connectors/graylog/schema/streams.py
+8 -3
@@ -1,6 +1,9 @@
1 -from typing import List, Optional
1 +from typing import List
2 +from typing import Optional
3 +
4 from pydantic import BaseModel
5
6 +
7 class Rule(BaseModel):
8 description: Optional[str]
9 field: str
@@ -10,8 +13,9 @@ class Rule(BaseModel):
13 type: int
14 value: str
15
16 +
17 class Stream(BaseModel):
14 - content_pack: Optional[str]
18 + content_pack: Optional[str]
19 created_at: str
20 creator_user_id: str
21 description: str
@@ -21,11 +25,12 @@ class Stream(BaseModel):
25 is_default: bool
26 is_editable: bool
27 matching_type: str
24 - outputs: list
28 + outputs: list
29 remove_matches_from_default_stream: bool
30 rules: List[Rule]
31 title: str
32
33 +
34 class GraylogStreamsResponse(BaseModel):
35 message: str
36 streams: List[Stream]
backend/app/connectors/graylog/services/collector.py
+37 -36
@@ -1,16 +1,24 @@
1 -from typing import Dict, List, Optional, Any, Tuple, Union
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 +from typing import Tuple
6 +from typing import Union
7 +
8 import requests
9 import xmltodict
10 from loguru import logger
11 from pydantic import Field
12
7 -from app.connectors.graylog.schema.collector import (
8 - GraylogIndicesResponse, GraylogIndexItem, GraylogInputsResponse, ConfiguredInput, RunningInput, ConfiguredInputsResponse, RunningInputsResponse
9 -)
13 +from app.connectors.graylog.schema.collector import ConfiguredInput
14 +from app.connectors.graylog.schema.collector import ConfiguredInputsResponse
15 +from app.connectors.graylog.schema.collector import GraylogIndexItem
16 +from app.connectors.graylog.schema.collector import GraylogIndicesResponse
17 +from app.connectors.graylog.schema.collector import GraylogInputsResponse
18 +from app.connectors.graylog.schema.collector import RunningInput
19 +from app.connectors.graylog.schema.collector import RunningInputsResponse
20 +from app.connectors.graylog.utils.universal import send_get_request
21
11 -from app.connectors.graylog.utils.universal import (
12 - send_get_request
13 -)
22
23 def get_indices_full() -> GraylogIndicesResponse:
24 """Get indices from Graylog."""
@@ -18,14 +26,15 @@ def get_indices_full() -> GraylogIndicesResponse:
26 indices_collected = send_get_request(endpoint="/api/system/indexer/indices")
27 if indices_collected["success"]:
28 indices_data = indices_collected["data"]["all"]["indices"]
21 -
29 +
30 # Convert the dictionary to a list of GraylogIndexItem
31 indices_list = [GraylogIndexItem(index_name=name, index_info=info) for name, info in indices_data.items()]
24 -
32 +
33 return GraylogIndicesResponse(indices=indices_list, success=True, message="Indices collected successfully")
34 else:
35 return GraylogIndicesResponse(indices=[], success=False, message="Failed to collect indices")
28 -
36 +
37 +
38 def fetch_configured_inputs() -> Tuple[bool, List[ConfiguredInput]]:
39 configured_inputs_collected = send_get_request(endpoint="/api/system/inputs")
40 success = configured_inputs_collected.get("success", False)
@@ -36,6 +45,7 @@ def fetch_configured_inputs() -> Tuple[bool, List[ConfiguredInput]]:
45 logger.error("Failed to fetch configured inputs")
46 return False, []
47
48 +
49 def fetch_running_inputs() -> Tuple[bool, List[RunningInput]]:
50 running_inputs_collected = send_get_request(endpoint="/api/system/inputstates")
51 success = running_inputs_collected.get("success", False)
@@ -46,6 +56,7 @@ def fetch_running_inputs() -> Tuple[bool, List[RunningInput]]:
56 logger.error("Failed to fetch running inputs")
57 return False, []
58
59 +
60 def get_inputs() -> GraylogInputsResponse:
61 """Get inputs from Graylog."""
62 logger.info("Getting inputs from Graylog")
@@ -59,28 +70,21 @@ def get_inputs() -> GraylogInputsResponse:
70 configured_inputs=configured_inputs_list,
71 running_inputs=running_inputs_list,
72 success=True,
62 - message="Successfully retrieved inputs"
73 + message="Successfully retrieved inputs",
74 )
75 else:
76 logger.error("Failed to fetch one or both types of inputs")
66 - return GraylogInputsResponse(
67 - configured_inputs=[],
68 - running_inputs=[],
69 - success=False,
70 - message="Failed to collect inputs"
71 - )
72 -
77 + return GraylogInputsResponse(configured_inputs=[], running_inputs=[], success=False, message="Failed to collect inputs")
78 +
79 +
80 def get_inputs_running() -> RunningInputsResponse:
81 """Get running inputs from Graylog."""
82 logger.info("Getting running inputs from Graylog")
83 run_success, running_inputs_list = fetch_running_inputs()
84 if run_success:
78 - return RunningInputsResponse(
79 - running_inputs=running_inputs_list,
80 - success=True,
81 - message="Successfully retrieved running inputs"
82 - )
83 -
85 + return RunningInputsResponse(running_inputs=running_inputs_list, success=True, message="Successfully retrieved running inputs")
86 +
87 +
88 def get_inputs_configured() -> ConfiguredInputsResponse:
89 """Get configured inputs from Graylog."""
90 logger.info("Getting configured inputs from Graylog")
@@ -89,9 +93,10 @@ def get_inputs_configured() -> ConfiguredInputsResponse:
93 return ConfiguredInputsResponse(
94 configured_inputs=configured_inputs_list,
95 success=True,
92 - message="Successfully retrieved configured inputs"
96 + message="Successfully retrieved configured inputs",
97 )
94 -
98 +
99 +
100 def get_index_names() -> List[str]:
101 """
102 Gets the names of all the indices in Graylog.
@@ -100,15 +105,16 @@ def get_index_names() -> List[str]:
105 List[str]: A list of all the index names.
106 """
107 logger.info("Getting index names from Graylog")
103 -
108 +
109 indices_collected = get_indices_full()
105 -
110 +
111 if indices_collected.success:
112 # Access the index_name attribute directly
113 return [index.index_name for index in indices_collected.indices]
114 else:
115 return []
111 -
116 +
117 +
118 def get_input_ids() -> List[str]:
119 """
120 Gets the IDs of all the inputs in Graylog.
@@ -117,16 +123,11 @@ def get_input_ids() -> List[str]:
123 List[str]: A list of all the input IDs.
124 """
125 logger.info("Getting input IDs from Graylog")
120 -
126 +
127 success, inputs_collected = fetch_configured_inputs()
122 -
128 +
129 if success:
130 # Access the input_id attribute directly
131 return [input.id for input in inputs_collected]
132 else:
133 return []
128 -
129 -
130 -
131 -
132 -
backend/app/connectors/graylog/services/events.py
+33 -22
@@ -1,16 +1,27 @@
1 -from typing import Dict, List, Optional, Any, Tuple, Union
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 +from typing import Tuple
6 +from typing import Union
7 +
8 import requests
9 import xmltodict
10 from loguru import logger
11 from pydantic import Field
12
7 -from app.connectors.graylog.schema.events import (
8 - EventDefinition, GraylogEventDefinitionsResponse, Event, GraylogAlertsResponse, AlertQuery, Alerts, Context, Parameters, AlertEvent
9 -)
13 +from app.connectors.graylog.schema.events import AlertEvent
14 +from app.connectors.graylog.schema.events import AlertQuery
15 +from app.connectors.graylog.schema.events import Alerts
16 +from app.connectors.graylog.schema.events import Context
17 +from app.connectors.graylog.schema.events import Event
18 +from app.connectors.graylog.schema.events import EventDefinition
19 +from app.connectors.graylog.schema.events import GraylogAlertsResponse
20 +from app.connectors.graylog.schema.events import GraylogEventDefinitionsResponse
21 +from app.connectors.graylog.schema.events import Parameters
22 +from app.connectors.graylog.utils.universal import send_get_request
23 +from app.connectors.graylog.utils.universal import send_post_request
24
11 -from app.connectors.graylog.utils.universal import (
12 - send_get_request, send_post_request
13 -)
25
26 def get_event_definitions() -> GraylogEventDefinitionsResponse:
27 """Get event definitions from Graylog."""
@@ -18,23 +29,28 @@ def get_event_definitions() -> GraylogEventDefinitionsResponse:
29 event_definitions_collected = send_get_request(endpoint="/api/events/definitions")
30 if event_definitions_collected["success"]:
31 event_definitions_data = event_definitions_collected["data"]["event_definitions"]
21 -
32 +
33 # Convert the dictionary to a list of GraylogIndexItem
34 event_definitions_list = [EventDefinition(**event_definition_data) for event_definition_data in event_definitions_data]
24 -
25 - return GraylogEventDefinitionsResponse(event_definitions=event_definitions_list, success=True, message="Event definitions collected successfully")
35 +
36 + return GraylogEventDefinitionsResponse(
37 + event_definitions=event_definitions_list,
38 + success=True,
39 + message="Event definitions collected successfully",
40 + )
41 else:
42 return GraylogEventDefinitionsResponse(event_definitions=[], success=False, message="Failed to collect event definitions")
28 -
43 +
44 +
45 def get_alerts(alert_query: AlertQuery) -> GraylogAlertsResponse:
46 logger.info("Getting alerts from Graylog")
47 response = send_post_request(endpoint="/api/events/search", data=alert_query.dict())
32 -
48 +
49 if response["success"]:
50 raw_alerts_data = response["data"]
51 # Convert raw event data to Event objects
52 event_objects = [AlertEvent(**event_data) for event_data in raw_alerts_data["events"]]
37 -
53 +
54 # Build the Alerts object
55 alerts = Alerts(
56 context=Context(**raw_alerts_data["context"]),
@@ -42,18 +58,13 @@ def get_alerts(alert_query: AlertQuery) -> GraylogAlertsResponse:
58 events=event_objects,
59 parameters=Parameters(**raw_alerts_data["parameters"]),
60 total_events=raw_alerts_data["total_events"],
45 - used_indices=raw_alerts_data["used_indices"]
61 + used_indices=raw_alerts_data["used_indices"],
62 )
47 -
63 +
64 # Build the final GraylogAlertsResponse
49 - final_response = GraylogAlertsResponse(
50 - alerts=alerts,
51 - message="Successfully collected alerts",
52 - success=True
53 - )
54 -
65 + final_response = GraylogAlertsResponse(alerts=alerts, message="Successfully collected alerts", success=True)
66 +
67 logger.info(f"Events collected: {event_objects}")
68 return final_response
69 else:
70 return GraylogAlertsResponse(alerts=Alerts(events=[]), success=False, message="Failed to collect alerts")
59 -
backend/app/connectors/graylog/services/management.py
+32 -17
@@ -1,20 +1,31 @@
1 -from typing import Dict, List, Optional, Any, Tuple, Union
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 +from typing import Tuple
6 +from typing import Union
7 +
8 import requests
9 import xmltodict
10 from loguru import logger
11 from pydantic import Field
12
7 -from app.connectors.graylog.schema.management import (
8 - DeletedIndexResponse, DeletedIndexBody, StopInputBody, StopInputResponse, StartInputBody, StartInputResponse, StopStreamBody, StopStreamResponse, StartStreamBody, StartStreamResponse
9 -)
10 -
11 -from app.connectors.graylog.utils.universal import (
12 - send_get_request, send_delete_request, send_put_request, send_post_request
13 -)
13 +from app.connectors.graylog.schema.management import DeletedIndexBody
14 +from app.connectors.graylog.schema.management import DeletedIndexResponse
15 +from app.connectors.graylog.schema.management import StartInputBody
16 +from app.connectors.graylog.schema.management import StartInputResponse
17 +from app.connectors.graylog.schema.management import StartStreamBody
18 +from app.connectors.graylog.schema.management import StartStreamResponse
19 +from app.connectors.graylog.schema.management import StopInputBody
20 +from app.connectors.graylog.schema.management import StopInputResponse
21 +from app.connectors.graylog.schema.management import StopStreamBody
22 +from app.connectors.graylog.schema.management import StopStreamResponse
23 +from app.connectors.graylog.services.collector import get_index_names
24 +from app.connectors.graylog.utils.universal import send_delete_request
25 +from app.connectors.graylog.utils.universal import send_get_request
26 +from app.connectors.graylog.utils.universal import send_post_request
27 +from app.connectors.graylog.utils.universal import send_put_request
28
15 -from app.connectors.graylog.services.collector import (
16 - get_index_names
17 -)
29
30 def delete_index(index_name: DeletedIndexBody) -> DeletedIndexResponse:
31 """Delete an index from Graylog."""
@@ -24,10 +35,14 @@ def delete_index(index_name: DeletedIndexBody) -> DeletedIndexResponse:
35 index_names = get_index_names()
36 logger.info(f"Index names: {index_names}")
37 if index_name in index_names:
27 - return DeletedIndexResponse(success=False, message=f"Failed to delete index {index_name}. If the index is still in use, it cannot be deleted.")
38 + return DeletedIndexResponse(
39 + success=False,
40 + message=f"Failed to delete index {index_name}. If the index is still in use, it cannot be deleted.",
41 + )
42 else:
43 return DeletedIndexResponse(success=True, message=f"Successfully deleted index {index_name}")
44
45 +
46 def stop_input(input_id: StopInputBody) -> StopInputResponse:
47 """Stop an input in Graylog."""
48 logger.info(f"Stopping input {input_id} in Graylog")
@@ -36,7 +51,8 @@ def stop_input(input_id: StopInputBody) -> StopInputResponse:
51 return StopInputResponse(success=True, message=f"Successfully stopped input {input_id}")
52 else:
53 return StopInputResponse(success=False, message=f"Failed to stop input {input_id}")
39 -
54 +
55 +
56 def start_input(input_id: StartInputBody) -> StartInputResponse:
57 """Start an input in Graylog."""
58 logger.info(f"Starting input {input_id} in Graylog")
@@ -45,7 +61,7 @@ def start_input(input_id: StartInputBody) -> StartInputResponse:
61 return StartInputResponse(success=True, message=f"Successfully started input {input_id}")
62 else:
63 return StartInputResponse(success=False, message=f"Failed to start input {input_id}")
48 -
64 +
65
66 def stop_stream(stream_id: StopStreamBody) -> StopStreamResponse:
67 """Stop a stream in Graylog."""
@@ -56,7 +72,8 @@ def stop_stream(stream_id: StopStreamBody) -> StopStreamResponse:
72 return StopStreamResponse(success=True, message=f"Successfully stopped stream {stream_id}")
73 else:
74 return StopStreamResponse(success=False, message=f"Failed to stop stream {stream_id}")
59 -
75 +
76 +
77 def start_stream(stream_id: StartStreamBody) -> StartStreamResponse:
78 """Start a stream in Graylog."""
79 logger.info(f"Starting stream {stream_id} in Graylog")
@@ -65,5 +82,3 @@ def start_stream(stream_id: StartStreamBody) -> StartStreamResponse:
82 return StartStreamResponse(success=True, message=f"Successfully started stream {stream_id}")
83 else:
84 return StartStreamResponse(success=False, message=f"Failed to start stream {stream_id}")
68 -
69 -
backend/app/connectors/graylog/services/monitoring.py
+38 -21
@@ -1,22 +1,30 @@
1 -from typing import Dict, List, Optional, Any, Tuple, Union
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 +from typing import Tuple
6 +from typing import Union
7 +
8 import requests
9 import xmltodict
10 from loguru import logger
11 from pydantic import Field
12
7 -from app.connectors.graylog.schema.monitoring import (
8 - GraylogMessages, GraylogMessagesResponse, GraylogTotalMessages, GraylogThroughputMetrics, GraylogThroughputMetricsList, GraylogUncommittedJournalEntries, GraylogMetricsResponse, GraylogThroughputMetricsCollection
9 -)
10 -from app.connectors.graylog.utils.universal import (
11 - send_get_request
12 -)
13 +from app.connectors.graylog.schema.monitoring import GraylogMessages
14 +from app.connectors.graylog.schema.monitoring import GraylogMessagesResponse
15 +from app.connectors.graylog.schema.monitoring import GraylogMetricsResponse
16 +from app.connectors.graylog.schema.monitoring import GraylogThroughputMetrics
17 +from app.connectors.graylog.schema.monitoring import GraylogThroughputMetricsCollection
18 +from app.connectors.graylog.schema.monitoring import GraylogThroughputMetricsList
19 +from app.connectors.graylog.schema.monitoring import GraylogTotalMessages
20 +from app.connectors.graylog.schema.monitoring import GraylogUncommittedJournalEntries
21 +from app.connectors.graylog.utils.universal import send_get_request
22 +
23
24 def get_messages(page_number: int) -> GraylogMessagesResponse:
25 """Get messages from Graylog."""
26 logger.info(f"Getting messages from Graylog")
17 - params = {
18 - "page": page_number
19 - }
27 + params = {"page": page_number}
28 messages_collected = send_get_request(endpoint="/api/system/messages", params=params)
29 if messages_collected["success"]:
30 graylog_messages_list = []
@@ -28,30 +36,40 @@ def get_messages(page_number: int) -> GraylogMessagesResponse:
36 timestamp=message["timestamp"],
37 )
38 graylog_messages_list.append(graylog_message)
31 - return GraylogMessagesResponse(graylog_messages=graylog_messages_list, success=True, message="Messages collected successfully", total_messages=messages_collected["data"]["total"])
39 + return GraylogMessagesResponse(
40 + graylog_messages=graylog_messages_list,
41 + success=True,
42 + message="Messages collected successfully",
43 + total_messages=messages_collected["data"]["total"],
44 + )
45 else:
46 return GraylogMessagesResponse(graylog_messages=[], success=False, message="Failed to collect messages")
34 -
47 +
48 +
49 def fetch_metrics_from_graylog() -> dict:
50 return send_get_request(endpoint="/api/system/metrics")
51
52 +
53 def fetch_uncommitted_journal_entries() -> dict:
54 return send_get_request(endpoint="/api/system/journal")
55
56 +
57 def merge_metrics_data(throughput_metrics_collected: dict) -> dict:
58 throughput_metrics = throughput_metrics_collected["data"]["gauges"]
59 input_output_metrics = throughput_metrics_collected["data"]["counters"]
60 return {**throughput_metrics, **input_output_metrics}
61
62 +
63 def filter_and_create_throughput_metrics(merged_metrics: dict) -> list:
64 model_fields = [field_info.alias for field_info in GraylogThroughputMetricsCollection.__fields__.values()]
65 throughput_metrics_list = [
49 - GraylogThroughputMetrics(metric=metric_name, value=metric_data.get('value', 0))
66 + GraylogThroughputMetrics(metric=metric_name, value=metric_data.get("value", 0))
67 for metric_name, metric_data in merged_metrics.items()
68 if metric_name in model_fields
69 ]
70 return throughput_metrics_list
71
72 +
73 def get_metrics() -> GraylogMetricsResponse:
74 logger.info("Getting metrics from Graylog")
75 throughput_metrics_collected = fetch_metrics_from_graylog()
@@ -60,22 +78,21 @@ def get_metrics() -> GraylogMetricsResponse:
78 if throughput_metrics_collected["success"] and uncommitted_journal_entries_collected["success"]:
79 merged_metrics = merge_metrics_data(throughput_metrics_collected)
80 throughput_metrics_list = filter_and_create_throughput_metrics(merged_metrics)
63 -
81 +
82 uncommitted_journal_entries = GraylogUncommittedJournalEntries(
65 - uncommitted_journal_entries=uncommitted_journal_entries_collected["data"]["uncommitted_journal_entries"]
83 + uncommitted_journal_entries=uncommitted_journal_entries_collected["data"]["uncommitted_journal_entries"],
84 )
85
86 return GraylogMetricsResponse(
87 throughput_metrics=throughput_metrics_list,
88 uncommitted_journal_entries=uncommitted_journal_entries.uncommitted_journal_entries,
89 success=True,
72 - message="Metrics collected successfully"
90 + message="Metrics collected successfully",
91 )
92 else:
93 return GraylogMetricsResponse(
76 - throughput_metrics=[],
77 - uncommitted_journal_entries=0,
78 - success=False,
79 - message="Failed to collect metrics"
94 + throughput_metrics=[],
95 + uncommitted_journal_entries=0,
96 + success=False,
97 + message="Failed to collect metrics",
98 )
81 -
backend/app/connectors/graylog/services/pipelines.py
+17 -9
@@ -1,16 +1,23 @@
1 -from typing import Dict, List, Optional, Any, Tuple, Union
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 +from typing import Tuple
6 +from typing import Union
7 +
8 import requests
9 import xmltodict
10 from loguru import logger
11 from pydantic import Field
12
7 -from app.connectors.graylog.schema.pipelines import (
8 - Stage, Pipeline, GraylogPipelinesResponse, PipelineRule, PipelineRulesResponse
9 -)
13 +from app.connectors.graylog.schema.pipelines import GraylogPipelinesResponse
14 +from app.connectors.graylog.schema.pipelines import Pipeline
15 +from app.connectors.graylog.schema.pipelines import PipelineRule
16 +from app.connectors.graylog.schema.pipelines import PipelineRulesResponse
17 +from app.connectors.graylog.schema.pipelines import Stage
18 +from app.connectors.graylog.utils.universal import send_get_request
19 +from app.connectors.graylog.utils.universal import send_post_request
20
11 -from app.connectors.graylog.utils.universal import (
12 - send_get_request, send_post_request
13 -)
21
22 def get_pipelines() -> GraylogPipelinesResponse:
23 """Get pipelines from Graylog."""
@@ -21,7 +28,8 @@ def get_pipelines() -> GraylogPipelinesResponse:
28 return GraylogPipelinesResponse(pipelines=pipelines_list, success=True, message="Pipelines collected successfully")
29 else:
30 return GraylogPipelinesResponse(pipelines=[], success=False, message="Failed to collect pipelines")
24 -
31 +
32 +
33 def get_pipeline_rules() -> PipelineRulesResponse:
34 """Get pipeline rules from Graylog."""
35 logger.info(f"Getting pipeline rules from Graylog")
@@ -30,4 +38,4 @@ def get_pipeline_rules() -> PipelineRulesResponse:
38 pipeline_rules_list = [PipelineRule(**pipeline_rule_data) for pipeline_rule_data in pipeline_rules_collected["data"]]
39 return PipelineRulesResponse(pipeline_rules=pipeline_rules_list, success=True, message="Pipeline rules collected successfully")
40 else:
33 - return PipelineRulesResponse(pipeline_rules=[], success=False, message="Failed to collect pipeline rules")
\ No newline at end of file
41 + return PipelineRulesResponse(pipeline_rules=[], success=False, message="Failed to collect pipeline rules")
backend/app/connectors/graylog/services/streams.py
+20 -10
@@ -1,16 +1,20 @@
1 -from typing import Dict, List, Optional, Any, Tuple, Union
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 +from typing import Tuple
6 +from typing import Union
7 +
8 import requests
9 import xmltodict
10 from loguru import logger
11 from pydantic import Field
12
7 -from app.connectors.graylog.schema.streams import (
8 - Stream, GraylogStreamsResponse
9 -)
13 +from app.connectors.graylog.schema.streams import GraylogStreamsResponse
14 +from app.connectors.graylog.schema.streams import Stream
15 +from app.connectors.graylog.utils.universal import send_get_request
16 +from app.connectors.graylog.utils.universal import send_post_request
17
11 -from app.connectors.graylog.utils.universal import (
12 - send_get_request, send_post_request
13 -)
18
19 def get_streams() -> GraylogStreamsResponse:
20 """Get streams from Graylog."""
@@ -18,10 +22,16 @@ def get_streams() -> GraylogStreamsResponse:
22 streams_collected = send_get_request(endpoint="/api/streams")
23 if streams_collected["success"]:
24 streams_list = [Stream(**stream_data) for stream_data in streams_collected["data"]["streams"]]
21 - return GraylogStreamsResponse(streams=streams_list, success=True, message="Streams collected successfully", total=streams_collected["data"]["total"])
25 + return GraylogStreamsResponse(
26 + streams=streams_list,
27 + success=True,
28 + message="Streams collected successfully",
29 + total=streams_collected["data"]["total"],
30 + )
31 else:
32 return GraylogStreamsResponse(streams=[], success=False, message="Failed to collect streams", total=0)
24 -
33 +
34 +
35 def get_stream_ids() -> List[str]:
36 """Get stream IDs from Graylog."""
37 logger.info(f"Getting stream IDs from Graylog")
@@ -29,4 +39,4 @@ def get_stream_ids() -> List[str]:
39 if streams_collected["success"]:
40 return [stream_data["id"] for stream_data in streams_collected["data"]["streams"]]
41 else:
32 - return []
\ No newline at end of file
42 + return []
backend/app/connectors/graylog/utils/universal.py
+35 -14
@@ -1,15 +1,24 @@
1 -from typing import Dict, Any, List, Generator, Type, Optional
2 -from sqlmodel import Session, select
3 -from app.connectors.models import Connectors
1 +from typing import Any
2 +from typing import Dict
3 +from typing import Generator
4 +from typing import List
5 +from typing import Optional
6 +from typing import Type
7 +
8 +import requests
9 from elasticsearch7 import Elasticsearch
10 from loguru import logger
6 -from app.db.db_session import engine
7 -import requests
11 +from sqlmodel import Session
12 +from sqlmodel import select
13 +
14 +from app.connectors.models import Connectors
15 from app.connectors.schema import ConnectorResponse
16 from app.connectors.utils import get_connector_info_from_db
17 +from app.db.db_session import engine
18
19 HEADERS = {"X-Requested-By": "CoPilot"}
20
21 +
22 def verify_graylog_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
23 """
24 Verifies the connection to Graylog service.
@@ -18,8 +27,8 @@ def verify_graylog_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
27 dict: A dictionary containing 'connectionSuccessful' status and 'authToken' if the connection is successful.
28 """
29 logger.info(
21 - f"Verifying the graylog connection to {attributes['connector_url']}",
22 - )
30 + f"Verifying the graylog connection to {attributes['connector_url']}",
31 + )
32 try:
33 graylog_roles = requests.get(
34 f"{attributes['connector_url']}/api/authz/roles/user/{attributes['connector_username']}",
@@ -38,13 +47,17 @@ def verify_graylog_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
47 logger.error(
48 f"Connection to {attributes['connector_url']} failed with error: {graylog_roles.text}",
49 )
41 - return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error: {graylog_roles.text}"}
50 + return {
51 + "connectionSuccessful": False,
52 + "message": f"Connection to {attributes['connector_url']} failed with error: {graylog_roles.text}",
53 + }
54 except Exception as e:
55 logger.error(
56 f"Connection to {attributes['connector_url']} failed with error: {e}",
57 )
58 return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error: {e}"}
59
60 +
61 def verify_graylog_connection(connector_name: str) -> str:
62 """
63 Returns if connection to Graylog service is successful.
@@ -56,6 +69,7 @@ def verify_graylog_connection(connector_name: str) -> str:
69 return None
70 return verify_graylog_credentials(attributes)
71
72 +
73 def send_get_request(endpoint: str, params: Optional[Dict[str, Any]] = None, connector_name: str = "Graylog") -> Dict[str, Any]:
74 """
75 Sends a GET request to the Graylog service.
@@ -88,7 +102,8 @@ def send_get_request(endpoint: str, params: Optional[Dict[str, Any]] = None, con
102 except Exception as e:
103 logger.error(f"Failed to send GET request to {endpoint} with error: {e}")
104 return {"success": False, "message": f"Failed to send GET request to {endpoint} with error: {e}"}
91 -
105 +
106 +
107 def send_post_request(endpoint: str, data: Dict[str, Any] = None, connector_name: str = "Graylog") -> Dict[str, Any]:
108 """
109 Sends a POST request to the Graylog service.
@@ -118,16 +133,21 @@ def send_post_request(endpoint: str, data: Dict[str, Any] = None, connector_name
133 json=data,
134 verify=False,
135 )
121 -
136 +
137 if response.status_code == 204:
138 return {"data": None, "success": True, "message": "Successfully completed request with no content"}
139 else:
125 - return {"data": response.json(), "success": False if response.status_code >= 400 else True, "message": "Successfully retrieved data" if response.status_code < 400 else "Failed to retrieve data"}
140 + return {
141 + "data": response.json(),
142 + "success": False if response.status_code >= 400 else True,
143 + "message": "Successfully retrieved data" if response.status_code < 400 else "Failed to retrieve data",
144 + }
145 except Exception as e:
146 logger.debug(f"Response: {response}")
147 logger.error(f"Failed to send POST request to {endpoint} with error: {e}")
148 return {"success": False, "message": f"Failed to send POST request to {endpoint} with error: {e}"}
130 -
149 +
150 +
151 def send_delete_request(endpoint: str, params: Optional[Dict[str, Any]] = None, connector_name: str = "Graylog") -> Dict[str, Any]:
152 """
153 Sends a DELETE request to the Graylog service.
@@ -160,7 +180,8 @@ def send_delete_request(endpoint: str, params: Optional[Dict[str, Any]] = None,
180 except Exception as e:
181 logger.error(f"Failed to send DELETE request to {endpoint} with error: {e}")
182 return {"success": False, "message": f"Failed to send DELETE request to {endpoint} with error: {e}"}
163 -
183 +
184 +
185 def send_put_request(endpoint: str, data: Optional[Dict[str, Any]] = None, connector_name: str = "Graylog") -> Dict[str, Any]:
186 """
187 Sends a PUT request to the Graylog service.
@@ -192,4 +213,4 @@ def send_put_request(endpoint: str, data: Optional[Dict[str, Any]] = None, conne
213 return {"data": response.json(), "success": True, "message": "Successfully retrieved data"}
214 except Exception as e:
215 logger.error(f"Failed to send PUT request to {endpoint} with error: {e}")
195 - return {"success": False, "message": f"Failed to send PUT request to {endpoint} with error: {e}"}
\ No newline at end of file
216 + return {"success": False, "message": f"Failed to send PUT request to {endpoint} with error: {e}"}
backend/app/connectors/models.py
+13 -6
@@ -1,6 +1,11 @@
1 from datetime import datetime
2 -from sqlmodel import SQLModel, Field, Relationship
3 -from typing import Optional, List
2 +from typing import List
3 +from typing import Optional
4 +
5 +from sqlmodel import Field
6 +from sqlmodel import Relationship
7 +from sqlmodel import SQLModel
8 +
9
10 class ConnectorHistory(SQLModel, table=True):
11 """
@@ -12,13 +17,14 @@ class ConnectorHistory(SQLModel, table=True):
17 :ivar change_description: Description of the change.
18 :ivar connector: Relationship to the Connectors model.
19 """
20 +
21 id: Optional[int] = Field(default=None, primary_key=True)
22 connector_id: int = Field(foreign_key="connectors.id")
23 change_timestamp: datetime = Field(default=datetime.utcnow())
24 change_description: str
25
26 # Relationship
21 - connector: Optional['Connectors'] = Relationship(back_populates='history_logs')
27 + connector: Optional["Connectors"] = Relationship(back_populates="history_logs")
28
29
30 class Connectors(SQLModel, table=True):
@@ -42,6 +48,7 @@ class Connectors(SQLModel, table=True):
48 :ivar connector_accepts_file: Boolean indicating if the connector accepts files.
49 :ivar history_logs: Relationship to the ConnectorHistory model.
50 """
51 +
52 id: Optional[int] = Field(default=None, primary_key=True)
53 connector_name: str = Field()
54 connector_type: str = Field()
@@ -50,7 +57,7 @@ class Connectors(SQLModel, table=True):
57 connector_username: Optional[str] = Field(default=None)
58 connector_password: Optional[str] = Field(default=None)
59 connector_api_key: Optional[str] = Field(default=None)
53 -
60 +
61 # Fields moved from ConnectorsAvailable
62 connector_description: Optional[str] = Field(default=None)
63 connector_supports: Optional[str] = Field(default=None)
@@ -59,9 +66,9 @@ class Connectors(SQLModel, table=True):
66 connector_accepts_api_key: bool = Field(default=False)
67 connector_accepts_username_password: bool = Field(default=False)
68 connector_accepts_file: bool = Field(default=False)
62 -
69 +
70 # Relationship
64 - history_logs: List[ConnectorHistory] = Relationship(back_populates='connector')
71 + history_logs: List[ConnectorHistory] = Relationship(back_populates="connector")
72
73
74 # Example usage
backend/app/connectors/routes.py
+44 -35
@@ -1,24 +1,42 @@
1 -from fastapi import APIRouter, HTTPException, Request, File, UploadFile
2 -from fastapi.responses import JSONResponse, FileResponse
3 -from typing import List, Union
4 -from app.connectors.schema import ConnectorResponse, ConnectorListResponse, VerifyConnectorResponse, ConnectorsListResponse, UpdateConnector
5 -from app.connectors.services import ConnectorServices
6 -from loguru import logger
7 -from app.db.db_session import session
1 +from typing import List
2 +from typing import Union
3
4 ## Auth Things
10 -from fastapi import APIRouter, HTTPException, Security, security, Depends
5 +from fastapi import APIRouter
6 +from fastapi import Depends
7 +from fastapi import File
8 +from fastapi import HTTPException
9 +from fastapi import Request
10 +from fastapi import Security
11 +from fastapi import UploadFile
12 +from fastapi import security
13 +from fastapi.responses import FileResponse
14 +from fastapi.responses import JSONResponse
15 from fastapi.security import HTTPAuthorizationCredentials
12 -from app.auth.schema.auth import UserResponse, UserLoginResponse
16 +from loguru import logger
17 +from starlette.status import HTTP_204_NO_CONTENT
18 +from starlette.status import HTTP_401_UNAUTHORIZED
19 +from starlette.status import HTTP_404_NOT_FOUND
20
21 +from app.auth.models.users import User
22 +from app.auth.models.users import UserInput
23 +from app.auth.models.users import UserLogin
24 from app.auth.routes.auth import auth_handler
25 +from app.auth.schema.auth import UserLoginResponse
26 +from app.auth.schema.auth import UserResponse
27 +from app.auth.services.universal import find_user
28 +from app.auth.services.universal import select_all_users
29 +from app.connectors.schema import ConnectorListResponse
30 +from app.connectors.schema import ConnectorResponse
31 +from app.connectors.schema import ConnectorsListResponse
32 +from app.connectors.schema import UpdateConnector
33 +from app.connectors.schema import VerifyConnectorResponse
34 +from app.connectors.services import ConnectorServices
35 from app.db.db_session import session
16 -from app.auth.models.users import UserInput, User, UserLogin
17 -from app.auth.services.universal import select_all_users, find_user
18 -from starlette.status import HTTP_204_NO_CONTENT, HTTP_404_NOT_FOUND, HTTP_401_UNAUTHORIZED
36
37 connector_router = APIRouter()
38
39 +
40 @connector_router.get("", response_model=ConnectorsListResponse, description="Fetch all available connectors")
41 async def get_connectors(user=Depends(auth_handler.get_current_user)) -> ConnectorListResponse:
42 """
@@ -37,17 +55,13 @@ async def get_connectors(user=Depends(auth_handler.get_current_user)) -> Connect
55 if not user.is_admin:
56 raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Unauthorized")
57
40 -
41 - connectors = ConnectorServices.fetch_all_connectors()
58 + connectors = ConnectorServices.fetch_all_connectors()
59 if connectors:
43 - return {
44 - "connectors": connectors,
45 - "success": True,
46 - "message": "Connectors fetched successfully"
47 - }
60 + return {"connectors": connectors, "success": True, "message": "Connectors fetched successfully"}
61 else:
62 raise HTTPException(status_code=404, detail="No connectors found")
63
64 +
65 @connector_router.get("/{connector_id}", response_model=ConnectorListResponse, description="Fetch a specific connector")
66 async def get_connector(connector_id: int) -> Union[ConnectorResponse, HTTPException]:
67 """
@@ -66,15 +80,16 @@ async def get_connector(connector_id: int) -> Union[ConnectorResponse, HTTPExcep
80 """
81 connector = ConnectorServices.fetch_connector_by_id(connector_id)
82 if connector is not None:
69 - return {
70 - "connector": connector,
71 - "success": True,
72 - "message": "Connector fetched successfully"
73 - }
83 + return {"connector": connector, "success": True, "message": "Connector fetched successfully"}
84 else:
85 raise HTTPException(status_code=404, detail=f"No connector found for ID: {connector_id}".format(connector_id=connector_id))
76 -
77 -@connector_router.post("/verify/{connector_id}", response_model=VerifyConnectorResponse, description="Verify a connector. Makes an API call to the connector to verify it is working.")
86 +
87 +
88 +@connector_router.post(
89 + "/verify/{connector_id}",
90 + response_model=VerifyConnectorResponse,
91 + description="Verify a connector. Makes an API call to the connector to verify it is working.",
92 +)
93 async def verify_connector(connector_id: int) -> Union[VerifyConnectorResponse, HTTPException]:
94 """
95 Verify a connector by its ID.
@@ -96,7 +111,7 @@ async def verify_connector(connector_id: int) -> Union[VerifyConnectorResponse,
111 return connector
112 else:
113 raise HTTPException(status_code=404, detail=f"No connector found for ID: {connector_id}".format(connector_id=connector_id))
99 -
114 +
115
116 @connector_router.put("/{connector_id}", response_model=ConnectorListResponse, description="Update a connector")
117 async def update_connector(connector_id: int, connector: UpdateConnector) -> ConnectorListResponse:
@@ -117,14 +132,10 @@ async def update_connector(connector_id: int, connector: UpdateConnector) -> Con
132 """
133 updated_connector = ConnectorServices.update_connector_by_id(connector_id, connector)
134 if updated_connector is not None:
120 - return {
121 - "connector": updated_connector,
122 - "success": True,
123 - "message": "Connector updated successfully"
124 - }
135 + return {"connector": updated_connector, "success": True, "message": "Connector updated successfully"}
136 else:
137 raise HTTPException(status_code=404, detail=f"No connector found for ID: {connector_id}".format(connector_id=connector_id))
127 -
138 +
139
140 @connector_router.post("/upload/{connector_id}", description="Upload a YAML file for a specific connector")
141 async def upload_yaml_file(connector_id: int, file: UploadFile = File(...)) -> dict:
@@ -157,5 +168,3 @@ async def upload_yaml_file(connector_id: int, file: UploadFile = File(...)) -> d
168 except Exception as e:
169 logger.error(f"Failed to upload file: {e}")
170 raise HTTPException(status_code=500, detail="Failed to upload file")
160 -
161 -
backend/app/connectors/schema.py
+12 -2
@@ -1,6 +1,11 @@
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
6 +
7 from pydantic import BaseModel
3 -from typing import Optional, List, Dict, Any
8 +
9
10 class ConnectorHistoryResponse(BaseModel):
11 id: Optional[int]
@@ -11,6 +16,7 @@ class ConnectorHistoryResponse(BaseModel):
16 class Config:
17 orm_mode = True
18
19 +
20 class ConnectorResponse(BaseModel):
21 id: Optional[int]
22 connector_name: str
@@ -32,22 +38,26 @@ class ConnectorResponse(BaseModel):
38 class Config:
39 orm_mode = True
40
41 +
42 class ConnectorsListResponse(BaseModel):
43 connectors: List[ConnectorResponse]
44 success: bool
45 message: str
46
47 +
48 class ConnectorListResponse(BaseModel):
49 connector: ConnectorResponse
50 success: bool
51 message: str
52
53 +
54 class VerifyConnectorResponse(BaseModel):
55 connectionSuccessful: bool
56 message: str
57
58 +
59 class UpdateConnector(BaseModel):
60 connector_url: str
61 connector_username: Optional[str]
62 connector_password: Optional[str]
53 - connector_api_key: Optional[str]
\ No newline at end of file
63 + connector_api_key: Optional[str]
backend/app/connectors/services.py
+56 -43
@@ -1,74 +1,91 @@
1 -from sqlmodel import Session, select
1 +import os
2 from contextlib import contextmanager
3 -from app.db.db_session import engine # Import the shared engine
3 +from datetime import datetime
4 +from typing import Any
5 +from typing import Dict
6 +from typing import Generator
7 +from typing import List
8 +from typing import Optional
9 +from typing import Type
10 +
11 +from fastapi import UploadFile
12 +from loguru import logger
13 +from pydantic import BaseModel
14 +from sqlmodel import Session
15 +from sqlmodel import select
16 +from werkzeug.utils import secure_filename
17 +
18 +from app.connectors.cortex.utils.universal import verify_cortex_connection
19 +from app.connectors.dfir_iris.utils.universal import verify_dfir_iris_connection
20 +from app.connectors.graylog.utils.universal import verify_graylog_connection
21 from app.connectors.models import Connectors
22 from app.connectors.schema import ConnectorResponse
6 -from app.connectors.wazuh_manager.utils.universal import verify_wazuh_manager_connection
7 -from app.connectors.wazuh_indexer.utils.universal import verify_wazuh_indexer_connection
8 -from app.connectors.velociraptor.utils.universal import verify_velociraptor_connection
9 -from app.connectors.graylog.utils.universal import verify_graylog_connection
10 -from app.connectors.dfir_iris.utils.universal import verify_dfir_iris_connection
11 -from app.connectors.cortex.utils.universal import verify_cortex_connection
23 from app.connectors.shuffle.utils.universal import verify_shuffle_connection
24 from app.connectors.sublime.utils.universal import verify_sublime_connection
14 -from werkzeug.utils import secure_filename
15 -from datetime import datetime
16 -from typing import List, Optional, Generator, Type
17 -from loguru import logger
18 -from pydantic import BaseModel
19 -from typing import Dict, Any
20 -from fastapi import UploadFile
21 -import os
25 +from app.connectors.velociraptor.utils.universal import verify_velociraptor_connection
26 +from app.connectors.wazuh_indexer.utils.universal import verify_wazuh_indexer_connection
27 +from app.connectors.wazuh_manager.utils.universal import verify_wazuh_manager_connection
28 +from app.db.db_session import engine # Import the shared engine
29
30 UPLOAD_FOLDER = "file-store"
31 UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), UPLOAD_FOLDER)
32 ALLOWED_EXTENSIONS = set(["yaml"]) # replace with your allowed file extensions
33
34 +
35 # Create an interface for connector services
36 class ConnectorServiceInterface(BaseModel):
37 def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
38 raise NotImplementedError
39
40 +
41 # Wazuh Manager Service
42 class WazuhManagerService(ConnectorServiceInterface):
43 def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
44 return verify_wazuh_manager_connection(connector.connector_name)
36 -
45 +
46 +
47 # Wazuh Indexer Service
48 class WazuhIndexerService(ConnectorServiceInterface):
49 def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
50 return verify_wazuh_indexer_connection(connector.connector_name)
41 -
51 +
52 +
53 # Velociraptor Service
54 class VelociraptorService(ConnectorServiceInterface):
55 def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
56 return verify_velociraptor_connection(connector.connector_name)
46 -
57 +
58 +
59 # Graylog Service
60 class GraylogService(ConnectorServiceInterface):
61 def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
62 return verify_graylog_connection(connector.connector_name)
51 -
63 +
64 +
65 # DFIR-IRIS Service
66 class DfirIrisService(ConnectorServiceInterface):
67 def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
68 return verify_dfir_iris_connection(connector.connector_name)
56 -
69 +
70 +
71 # Cortex Service
72 class CortexService(ConnectorServiceInterface):
73 def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
74 return verify_cortex_connection(connector.connector_name)
61 -
75 +
76 +
77 # Shuffle Service
78 class ShuffleService(ConnectorServiceInterface):
79 def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
80 return verify_shuffle_connection(connector.connector_name)
66 -
81 +
82 +
83 # Sublime Service
84 class SublimeService(ConnectorServiceInterface):
85 def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
86 return verify_sublime_connection(connector.connector_name)
71 -
87 +
88 +
89 # Factory function to create a service instance based on connector name
90 def get_connector_service(connector_name: str) -> Type[ConnectorServiceInterface]:
91 service_map = {
@@ -95,7 +112,7 @@ class ConnectorServices:
112 """
113 Get a new session for database interaction.
114
98 - This method is a context manager, which ensures that the session is closed
115 + This method is a context manager, which ensures that the session is closed
116 once the operations within the context are completed.
117
118 Yields:
@@ -112,7 +129,7 @@ class ConnectorServices:
129 """
130 Fetch all connectors from the database.
131
115 - This method retrieves all connector records from the database, converts them
132 + This method retrieves all connector records from the database, converts them
133 to Pydantic models, and returns them as a list.
134
135 Returns:
@@ -124,9 +141,7 @@ class ConnectorServices:
141 connectors = session.exec(query).all()
142
143 # Convert the SQLModel object to a Pydantic model
127 - connector_responses = [
128 - ConnectorResponse.from_orm(connector) for connector in connectors
129 - ]
144 + connector_responses = [ConnectorResponse.from_orm(connector) for connector in connectors]
145 return connector_responses
146
147 @classmethod
@@ -134,7 +149,7 @@ class ConnectorServices:
149 """
150 Fetch a connector by its ID from the database.
151
137 - Given a connector ID, this method retrieves the corresponding connector
152 + Given a connector ID, this method retrieves the corresponding connector
153 record from the database, if it exists.
154
155 Args:
@@ -159,13 +174,13 @@ class ConnectorServices:
174 except Exception as e:
175 logger.exception(f"Failed to create ConnectorResponse object: {e}")
176 return None
162 -
177 +
178 @classmethod
179 def verify_connector_by_id(cls, connector_id: int) -> Optional[ConnectorResponse]:
180 """
181 Verify a connector by making an API call to it.
182
168 - Given a connector ID, this method retrieves the corresponding connector
183 + Given a connector ID, this method retrieves the corresponding connector
184 record from the database, if it exists, and makes an API call to the connector.
185
186 Args:
@@ -186,28 +201,28 @@ class ConnectorServices:
201 try:
202 # Convert the SQLModel object to a Pydantic model
203 connector_response = ConnectorResponse.from_orm(connector)
189 -
204 +
205 # Get the appropriate service for this connector
206 ServiceClass = get_connector_service(connector_response.connector_name)
192 -
207 +
208 if ServiceClass is not None:
209 service_instance = ServiceClass()
210 connector_response = service_instance.verify_authentication(connector_response)
211 else:
212 logger.error(f"Connector type {connector_response.connector_name} is not supported")
213 return None
199 -
214 +
215 return connector_response
216 except Exception as e:
217 logger.exception(f"Failed to create ConnectorResponse object: {e}")
218 return None
204 -
219 +
220 @classmethod
221 def update_connector_by_id(cls, connector_id: int, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
222 """
223 Update a connector by its ID in the database.
224
210 - Given a connector ID and a Pydantic representation of a connector, this method
225 + Given a connector ID and a Pydantic representation of a connector, this method
226 updates the corresponding connector record in the database, if it exists.
227
228 Args:
@@ -244,17 +259,17 @@ class ConnectorServices:
259 except Exception as e:
260 logger.exception(f"Failed to update connector: {e}")
261 return None
247 -
262 +
263 @staticmethod
264 def allowed_file(filename):
265 return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
251 -
266 +
267 @classmethod
268 def save_file(cls, file: UploadFile):
269 if file and cls.allowed_file(file.filename):
270 filename = secure_filename(file.filename)
271 file_path = os.path.join(UPLOAD_FOLDER, filename)
257 -
272 +
273 # Save the file
274 with open(file_path, "wb") as buffer:
275 buffer.write(file.file.read())
@@ -264,10 +279,8 @@ class ConnectorServices:
279 connector.connector_configured = True
280 connector.connector_api_key = file_path
281 cls.update_connector_by_id(6, connector)
267 -
282 +
283 connector_response = ConnectorResponse.from_orm(connector)
284 return connector_response
285 else:
286 return False
272 -
273 -
backend/app/connectors/shuffle/routes/workflows.py
+26 -26
@@ -1,56 +1,56 @@
1 -from typing import List
2 -from fastapi import APIRouter, HTTPException, Security, Depends
3 -from starlette.status import HTTP_401_UNAUTHORIZED
4 -from loguru import logger
5 -import pydantic
1 import json
2 +from typing import List
3
4 +import pydantic
5 +from fastapi import APIRouter
6 +from fastapi import Depends
7 +from fastapi import HTTPException
8 +from fastapi import Security
9 +from loguru import logger
10 +from starlette.status import HTTP_401_UNAUTHORIZED
11
12 # App specific imports
13 from app.auth.routes.auth import auth_handler
14 +from app.connectors.shuffle.schema.workflows import WorkflowExecutionBodyModel
15 +from app.connectors.shuffle.schema.workflows import WorkflowExecutionResponseModel
16 +from app.connectors.shuffle.schema.workflows import WorkflowExecutionStatusResponseModel
17 +from app.connectors.shuffle.schema.workflows import WorkflowsResponse
18 +from app.connectors.shuffle.services.workflows import get_workflow_executions
19 +from app.connectors.shuffle.services.workflows import get_workflows
20 from app.db.db_session import session
12 -from app.connectors.shuffle.schema.workflows import (
13 - WorkflowsResponse, WorkflowExecutionResponseModel, WorkflowExecutionStatusResponseModel, WorkflowExecutionBodyModel,
14 -)
15 -
16 -from app.connectors.shuffle.services.workflows import get_workflows, get_workflow_executions
17 -
21
22 shuffle_workflows_router = APIRouter()
23
24 +
25 @shuffle_workflows_router.get("", response_model=WorkflowsResponse, description="Get all workflows")
26 async def get_all_workflows() -> WorkflowsResponse:
27 logger.info(f"Fetching all workflows")
28 return get_workflows()
29
30 +
31 @shuffle_workflows_router.get("/executions", response_model=WorkflowExecutionResponseModel, description="Get all workflow executions")
32 async def get_all_workflow_executions() -> WorkflowExecutionResponseModel:
33 logger.info(f"Fetching all workflow executions")
29 -
34 +
35 # Initialize an empty list for storing workflow details
36 workflow_details = []
32 -
37 +
38 # Get the workflow response by awaiting the asynchronous function get_workflows()
39 workflow_response = await get_all_workflows()
40
36 -
41 # Access the workflows attribute from the response
42 workflows = workflow_response.workflows
39 -
43 +
44 # Check if workflows is not None before proceeding
45 if workflows:
46 for workflow in workflows:
43 - workflow_details.append({
44 - "workflow_id": workflow['id'],
45 - "workflow_name": workflow['name'],
46 - "status": get_workflow_executions(WorkflowExecutionBodyModel(workflow_id=workflow['id']))
47 - })
47 + workflow_details.append(
48 + {
49 + "workflow_id": workflow["id"],
50 + "workflow_name": workflow["name"],
51 + "status": get_workflow_executions(WorkflowExecutionBodyModel(workflow_id=workflow["id"])),
52 + },
53 + )
54 return WorkflowExecutionResponseModel(success=True, message="Successfully fetched workflow executions", workflows=workflow_details)
55 else:
56 raise HTTPException(status_code=404, detail="No workflows found")
51 -
52 -
53 -
54 -
55 -
56 -
backend/app/connectors/shuffle/schema/workflows.py
+11 -2
@@ -1,12 +1,19 @@
1 -from pydantic import BaseModel, Field
2 -from typing import Optional, List, Dict, Any
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 from typing import Union
6
7 +from pydantic import BaseModel
8 +from pydantic import Field
9 +
10 +
11 class WorkflowsResponse(BaseModel):
12 message: str
13 success: bool
14 workflows: Optional[List[Dict[str, Any]]] = Field([], description="The alerts returned from the search.")
15
16 +
17 class WorkflowStatusExecutionModel(BaseModel):
18 executions: Optional[str] = Field(None, description="Status of workflow executions")
19 message: str = Field(..., description="Status message")
@@ -20,11 +27,13 @@ class WorkflowExecutionBodyModel(BaseModel):
27 class WorkflowExecutionStatusResponseModel(BaseModel):
28 last_run: Optional[str] = Field(..., description="Status of workflow executions")
29
30 +
31 class WorkflowExecutionModel(BaseModel):
32 status: WorkflowExecutionStatusResponseModel = Field(..., description="Status object")
33 workflow_id: str = Field(..., description="Unique identifier for the workflow")
34 workflow_name: str = Field(..., description="Name of the workflow")
35
36 +
37 class WorkflowExecutionResponseModel(BaseModel):
38 message: str = Field(..., description="Response message")
39 success: bool = Field(..., description="Success status")
backend/app/connectors/shuffle/services/workflows.py
+14 -8
@@ -1,17 +1,22 @@
1 -from typing import Dict, List, Optional, Any, Tuple, Union
1 +import json
2 +from typing import Any
3 +from typing import Dict
4 +from typing import List
5 +from typing import Optional
6 +from typing import Tuple
7 +from typing import Union
8 +
9 import requests
10 import xmltodict
11 from loguru import logger
12 from pydantic import Field
6 -import json
13
8 -from app.connectors.shuffle.schema.workflows import (
9 - WorkflowsResponse, WorkflowExecutionResponseModel, WorkflowExecutionBodyModel, WorkflowExecutionStatusResponseModel
10 -)
14 +from app.connectors.shuffle.schema.workflows import WorkflowExecutionBodyModel
15 +from app.connectors.shuffle.schema.workflows import WorkflowExecutionResponseModel
16 +from app.connectors.shuffle.schema.workflows import WorkflowExecutionStatusResponseModel
17 +from app.connectors.shuffle.schema.workflows import WorkflowsResponse
18 +from app.connectors.shuffle.utils.universal import send_get_request
19
12 -from app.connectors.shuffle.utils.universal import (
13 - send_get_request
14 -)
20
21 def get_workflows() -> WorkflowsResponse:
22 """
@@ -23,6 +28,7 @@ def get_workflows() -> WorkflowsResponse:
28 return WorkflowsResponse(success=False, message="Failed to get workflows", workflows=[])
29 return WorkflowsResponse(success=True, message="Successfully fetched workflows", workflows=response["data"])
30
31 +
32 def get_workflow_executions(exection_body: WorkflowExecutionBodyModel) -> WorkflowExecutionStatusResponseModel:
33 """
34 Returns a list of workflow executions.
backend/app/connectors/shuffle/utils/universal.py
+40 -20
@@ -1,12 +1,21 @@
1 -from typing import Dict, Any, List, Generator, Type, Optional
2 -from sqlmodel import Session, select
3 -from app.connectors.models import Connectors
1 +from typing import Any
2 +from typing import Dict
3 +from typing import Generator
4 +from typing import List
5 +from typing import Optional
6 +from typing import Type
7 +
8 +import requests
9 from elasticsearch7 import Elasticsearch
10 from loguru import logger
6 -from app.db.db_session import engine
7 -import requests
11 +from sqlmodel import Session
12 +from sqlmodel import select
13 +
14 +from app.connectors.models import Connectors
15 from app.connectors.schema import ConnectorResponse
16 from app.connectors.utils import get_connector_info_from_db
17 +from app.db.db_session import engine
18 +
19
20 def verify_shuffle_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
21 """
@@ -16,17 +25,17 @@ def verify_shuffle_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
25 dict: A dictionary containing 'connectionSuccessful' status and 'authToken' if the connection is successful.
26 """
27 logger.info(
19 - f"Verifying the Shuffle connection to {attributes['connector_url']}",
20 - )
28 + f"Verifying the Shuffle connection to {attributes['connector_url']}",
29 + )
30 try:
31 headers = {
23 - "Authorization": f"Bearer {attributes['connector_api_key']}",
24 - }
32 + "Authorization": f"Bearer {attributes['connector_api_key']}",
33 + }
34 shuffle_apps = requests.get(
26 - f"{attributes['connector_url']}/api/v1/apps/authentication",
27 - headers=headers,
28 - verify=False,
29 - )
35 + f"{attributes['connector_url']}/api/v1/apps/authentication",
36 + headers=headers,
37 + verify=False,
38 + )
39 if shuffle_apps.status_code == 200:
40 logger.info(
41 f"Connection to {attributes['connector_url']} successful",
@@ -36,13 +45,17 @@ def verify_shuffle_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
45 logger.error(
46 f"Connection to {attributes['connector_url']} failed with error: {shuffle_apps.text}",
47 )
39 - return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error: {shuffle_apps.text}"}
48 + return {
49 + "connectionSuccessful": False,
50 + "message": f"Connection to {attributes['connector_url']} failed with error: {shuffle_apps.text}",
51 + }
52 except Exception as e:
53 logger.error(
54 f"Connection to {attributes['connector_url']} failed with error: {e}",
55 )
56 return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error: {e}"}
57
58 +
59 def verify_shuffle_connection(connector_name: str) -> str:
60 """
61 Returns if connection to Shuffle service is successful.
@@ -86,7 +99,8 @@ def send_get_request(endpoint: str, params: Optional[Dict[str, Any]] = None, con
99 except Exception as e:
100 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}"}
89 -
102 +
103 +
104 def send_post_request(endpoint: str, data: Dict[str, Any] = None, connector_name: str = "Graylog") -> Dict[str, Any]:
105 """
106 Sends a POST request to the Graylog service.
@@ -116,16 +130,21 @@ def send_post_request(endpoint: str, data: Dict[str, Any] = None, connector_name
130 json=data,
131 verify=False,
132 )
119 -
133 +
134 if response.status_code == 204:
135 return {"data": None, "success": True, "message": "Successfully completed request with no content"}
136 else:
123 - return {"data": response.json(), "success": False if response.status_code >= 400 else True, "message": "Successfully retrieved data" if response.status_code < 400 else "Failed to retrieve data"}
137 + return {
138 + "data": response.json(),
139 + "success": False if response.status_code >= 400 else True,
140 + "message": "Successfully retrieved data" if response.status_code < 400 else "Failed to retrieve data",
141 + }
142 except Exception as e:
143 logger.debug(f"Response: {response}")
144 logger.error(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}"}
128 -
146 +
147 +
148 def send_delete_request(endpoint: str, params: Optional[Dict[str, Any]] = None, connector_name: str = "Graylog") -> Dict[str, Any]:
149 """
150 Sends a DELETE request to the Graylog service.
@@ -158,7 +177,8 @@ def send_delete_request(endpoint: str, params: Optional[Dict[str, Any]] = None,
177 except Exception as e:
178 logger.error(f"Failed to send DELETE request to {endpoint} with error: {e}")
179 return {"success": False, "message": f"Failed to send DELETE request to {endpoint} with error: {e}"}
161 -
180 +
181 +
182 def send_put_request(endpoint: str, data: Optional[Dict[str, Any]] = None, connector_name: str = "Graylog") -> Dict[str, Any]:
183 """
184 Sends a PUT request to the Graylog service.
@@ -190,4 +210,4 @@ def send_put_request(endpoint: str, data: Optional[Dict[str, Any]] = None, conne
210 return {"data": response.json(), "success": True, "message": "Successfully retrieved data"}
211 except Exception as e:
212 logger.error(f"Failed to send PUT request to {endpoint} with error: {e}")
193 - return {"success": False, "message": f"Failed to send PUT request to {endpoint} with error: {e}"}
\ No newline at end of file
213 + return {"success": False, "message": f"Failed to send PUT request to {endpoint} with error: {e}"}
backend/app/connectors/sublime/models/alerts.py
+12 -3
@@ -1,6 +1,11 @@
1 -from typing import List, Optional
2 -from sqlmodel import SQLModel, Field, Relationship
1 import datetime
2 +from typing import List
3 +from typing import Optional
4 +
5 +from sqlmodel import Field
6 +from sqlmodel import Relationship
7 +from sqlmodel import SQLModel
8 +
9
10 class FlaggedRule(SQLModel, table=True):
11 id: Optional[int] = Field(default=None, primary_key=True)
@@ -9,10 +14,11 @@ class FlaggedRule(SQLModel, table=True):
14 severity: Optional[str] = Field(None, description="Severity level of the flagged rule")
15 tags: str
16 sublime_alert_id: int = Field(foreign_key="sublimealerts.id")
12 -
17 +
18 # Relationship attribute
19 sublime_alert: "SublimeAlerts" = Relationship(back_populates="flagged_rules")
20
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")
@@ -22,6 +28,7 @@ class Mailbox(SQLModel, table=True):
28 # Relationship attribute
29 sublime_alert: "SublimeAlerts" = Relationship(back_populates="mailbox")
30
31 +
32 class TriggeredAction(SQLModel, table=True):
33 id: Optional[int] = Field(default=None, primary_key=True)
34 action_id: str
@@ -32,6 +39,7 @@ class TriggeredAction(SQLModel, table=True):
39 # Relationship attribute
40 sublime_alert: "SublimeAlerts" = Relationship(back_populates="triggered_actions")
41
42 +
43 class Sender(SQLModel, table=True):
44 id: Optional[int] = Field(default=None, primary_key=True)
45 email: str
@@ -41,6 +49,7 @@ class Sender(SQLModel, table=True):
49 # Relationship attribute
50 sublime_alert: "SublimeAlerts" = Relationship(back_populates="sender")
51
52 +
53 class Recipient(SQLModel, table=True):
54 id: Optional[int] = Field(default=None, primary_key=True)
55 email: str
backend/app/connectors/sublime/routes/alerts.py
+16 -11
@@ -1,23 +1,27 @@
1 -from typing import List, Any
2 -from fastapi import APIRouter, HTTPException, Security, Depends
3 -from starlette.status import HTTP_401_UNAUTHORIZED
4 -from loguru import logger
5 -import pydantic
1 import json
2 +from typing import Any
3 +from typing import List
4
5 +import pydantic
6 +from fastapi import APIRouter
7 +from fastapi import Depends
8 +from fastapi import HTTPException
9 +from fastapi import Security
10 +from loguru import logger
11 +from starlette.status import HTTP_401_UNAUTHORIZED
12
13 # App specific imports
14 from app.auth.routes.auth import auth_handler
15 +from app.connectors.sublime.schema.alerts import AlertRequestBody
16 +from app.connectors.sublime.schema.alerts import AlertResponseBody
17 +from app.connectors.sublime.schema.alerts import SublimeAlertsResponse
18 +from app.connectors.sublime.services.alerts import collect_alerts
19 +from app.connectors.sublime.services.alerts import store_sublime_alert
20 from app.db.db_session import session
12 -from app.connectors.sublime.schema.alerts import (
13 - AlertRequestBody, AlertResponseBody, SublimeAlertsResponse
14 -)
15 -
16 -from app.connectors.sublime.services.alerts import store_sublime_alert, collect_alerts
17 -
21
22 sublime_alerts_router = APIRouter()
23
24 +
25 @sublime_alerts_router.post("/alert", description="Receive alert from Sublime and store it in the database")
26 async def receive_sublime_alert(alert_request_body: AlertRequestBody) -> AlertResponseBody:
27 """
@@ -30,6 +34,7 @@ async def receive_sublime_alert(alert_request_body: AlertRequestBody) -> AlertRe
34 logger.info(f"Received alert from Sublime: {alert_request_body}")
35 return store_sublime_alert(alert_request_body)
36
37 +
38 @sublime_alerts_router.get("/alerts", response_model=SublimeAlertsResponse, description="Get all alerts")
39 async def get_sublime_alerts() -> SublimeAlertsResponse:
40 """
backend/app/connectors/sublime/schema/alerts.py
+20 -3
@@ -1,6 +1,10 @@
1 -from typing import List, Optional
2 -from pydantic import BaseModel, Field
1 import datetime
2 +from typing import List
3 +from typing import Optional
4 +
5 +from pydantic import BaseModel
6 +from pydantic import Field
7 +
8
9 class FlaggedRule(BaseModel):
10 id: str = Field(..., description="Unique identifier for the flagged rule")
@@ -8,10 +12,12 @@ class FlaggedRule(BaseModel):
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")
14
15 +
16 class Mailbox(BaseModel):
17 external_id: Optional[str] = Field(None, description="External identifier for the mailbox")
18 id: str = Field(..., description="Unique identifier for the mailbox")
19
20 +
21 class Message(BaseModel):
22 canonical_id: str = Field(..., description="Canonical identifier for the message")
23 external_id: Optional[str] = Field(None, description="External identifier for the mailbox")
@@ -19,16 +25,19 @@ class Message(BaseModel):
25 mailbox: Mailbox = Field(..., description="Mailbox details")
26 message_source_id: str = Field(..., description="Source identifier for the message")
27
28 +
29 class TriggeredAction(BaseModel):
30 id: str = Field(..., description="Unique identifier for the triggered action")
31 name: str = Field(..., description="Name of the triggered action")
32 type: str = Field(..., description="Type of the triggered action")
33
34 +
35 class Data(BaseModel):
36 flagged_rules: List[FlaggedRule] = Field(..., description="List of flagged rules")
37 message: Message = Field(..., description="Message details")
38 triggered_actions: List[TriggeredAction] = Field(..., description="List of triggered actions")
39
40 +
41 class AlertRequestBody(BaseModel):
42 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")
@@ -36,10 +45,12 @@ class AlertRequestBody(BaseModel):
45 id: str = Field(..., description="Unique identifier for the request body")
46 type: str = Field(..., description="Type of event, e.g., message.flagged")
47
48 +
49 class AlertResponseBody(BaseModel):
50 success: bool = Field(..., description="Success status of the request")
51 message: str = Field(..., description="Message describing the result of the request")
52
53 +
54 ### SQLModel Schema
55 class FlaggedRuleSchema(BaseModel):
56 rule_id: str
@@ -50,6 +61,7 @@ class FlaggedRuleSchema(BaseModel):
61 class Config:
62 orm_mode = True
63
64 +
65 class MailboxSchema(BaseModel):
66 external_id: Optional[str] = Field(None, description="External identifier for the mailbox")
67 mailbox_id: str
@@ -57,6 +69,7 @@ class MailboxSchema(BaseModel):
69 class Config:
70 orm_mode = True
71
72 +
73 class TriggeredActionSchema(BaseModel):
74 action_id: str
75 name: str
@@ -65,6 +78,7 @@ class TriggeredActionSchema(BaseModel):
78 class Config:
79 orm_mode = True
80
81 +
82 class SenderSchema(BaseModel):
83 email: str
84 name: str
@@ -72,6 +86,7 @@ class SenderSchema(BaseModel):
86 class Config:
87 orm_mode = True
88
89 +
90 class RecipientSchema(BaseModel):
91 email: str
92 name: str
@@ -79,6 +94,7 @@ class RecipientSchema(BaseModel):
94 class Config:
95 orm_mode = True
96
97 +
98 class SublimeAlertsSchema(BaseModel):
99 api_version: str
100 created_at: str
@@ -98,7 +114,8 @@ class SublimeAlertsSchema(BaseModel):
114 class Config:
115 orm_mode = True
116
117 +
118 class SublimeAlertsResponse(BaseModel):
119 sublime_alerts: List[SublimeAlertsSchema]
120 success: bool
104 - message: str
\ No newline at end of file
121 + message: str
backend/app/connectors/sublime/services/alerts.py
+49 -36
@@ -1,12 +1,23 @@
1 +import json
2 from typing import List
3 +
4 +from fastapi import HTTPException
5 from loguru import logger
6 +
7 +from app.connectors.sublime.models.alerts import FlaggedRule
8 +from app.connectors.sublime.models.alerts import Mailbox
9 +from app.connectors.sublime.models.alerts import Recipient
10 +from app.connectors.sublime.models.alerts import Sender
11 +from app.connectors.sublime.models.alerts import SublimeAlerts
12 +from app.connectors.sublime.models.alerts import TriggeredAction
13 +from app.connectors.sublime.schema.alerts import AlertRequestBody
14 +from app.connectors.sublime.schema.alerts import AlertResponseBody
15 +from app.connectors.sublime.schema.alerts import SublimeAlertsResponse
16 +from app.connectors.sublime.schema.alerts import SublimeAlertsSchema
17 +from app.connectors.sublime.utils.universal import send_get_request
18 from app.db.db_session import session
19 from app.db.universal_models import Agents
5 -from app.connectors.sublime.models.alerts import SublimeAlerts, FlaggedRule, Mailbox, TriggeredAction, Sender, Recipient
6 -from app.connectors.sublime.schema.alerts import AlertRequestBody, AlertResponseBody, SublimeAlertsSchema, SublimeAlertsResponse
7 -from app.connectors.sublime.utils.universal import send_get_request
8 -import json
9 -from fastapi import HTTPException
20 +
21
22 def create_sublime_alert(alert_request_body: AlertRequestBody) -> SublimeAlerts:
23 return SublimeAlerts(
@@ -17,40 +28,37 @@ def create_sublime_alert(alert_request_body: AlertRequestBody) -> SublimeAlerts:
28 message_id=alert_request_body.data.message.id,
29 canonical_id=alert_request_body.data.message.canonical_id,
30 external_id=alert_request_body.data.message.external_id,
20 - message_source_id=alert_request_body.data.message.message_source_id
31 + message_source_id=alert_request_body.data.message.message_source_id,
32 )
33
34 +
35 def create_flagged_rules(alert_request_body: AlertRequestBody, sublime_alert_id: int) -> List[FlaggedRule]:
36 flagged_rules = []
37 for rule in alert_request_body.data.flagged_rules:
38 tags_str = json.dumps(rule.tags)
27 - flagged_rules.append(FlaggedRule(
28 - rule_id=rule.id,
29 - name=rule.name,
30 - severity=rule.severity,
31 - tags=tags_str,
32 - sublime_alert_id=sublime_alert_id
33 - ))
39 + flagged_rules.append(
40 + FlaggedRule(rule_id=rule.id, name=rule.name, severity=rule.severity, tags=tags_str, sublime_alert_id=sublime_alert_id),
41 + )
42 return flagged_rules
43
44 +
45 def create_mailbox(alert_request_body: AlertRequestBody, sublime_alert_id: int) -> Mailbox:
46 return Mailbox(
47 external_id=alert_request_body.data.message.mailbox.external_id,
48 mailbox_id=alert_request_body.data.message.mailbox.id,
40 - sublime_alert_id=sublime_alert_id
49 + sublime_alert_id=sublime_alert_id,
50 )
51
52 +
53 def create_triggered_actions(alert_request_body: AlertRequestBody, sublime_alert_id: int) -> List[TriggeredAction]:
54 triggered_actions = []
55 for action in alert_request_body.data.triggered_actions:
46 - triggered_actions.append(TriggeredAction(
47 - action_id=action.id,
48 - name=action.name,
49 - type=action.type,
50 - sublime_alert_id=sublime_alert_id
51 - ))
56 + triggered_actions.append(
57 + TriggeredAction(action_id=action.id, name=action.name, type=action.type, sublime_alert_id=sublime_alert_id),
58 + )
59 return triggered_actions
60
61 +
62 def store_sublime_alert(alert_request_body: AlertRequestBody) -> AlertResponseBody:
63 try:
64 sublime_alert = create_sublime_alert(alert_request_body)
@@ -77,20 +85,15 @@ def store_sublime_alert(alert_request_body: AlertRequestBody) -> AlertResponseBo
85 except Exception as e:
86 logger.error(f"Failed to store alert {alert_request_body.id} in the database: {e}")
87 raise HTTPException(status_code=500, detail=f"Failed to store alert {alert_request_body.id} in the database: {e}")
80 -
88 +
89 +
90 def create_sender(alert_request_body: AlertRequestBody, sublime_alert_id: int) -> Sender:
82 - return Sender(
83 - email=collect_sender(alert_request_body.data.message.id),
84 - name="n/a",
85 - sublime_alert_id=sublime_alert_id
86 - )
91 + return Sender(email=collect_sender(alert_request_body.data.message.id), name="n/a", sublime_alert_id=sublime_alert_id)
92 +
93
94 def create_recipient(alert_request_body: AlertRequestBody, sublime_alert_id: int) -> Recipient:
89 - return Recipient(
90 - email=collect_recipient(alert_request_body.data.message.id),
91 - name="n/a",
92 - sublime_alert_id=sublime_alert_id
93 - )
95 + return Recipient(email=collect_recipient(alert_request_body.data.message.id), name="n/a", sublime_alert_id=sublime_alert_id)
96 +
97
98 def collect_sender(message_id: str) -> Sender:
99 """
@@ -100,10 +103,14 @@ def collect_sender(message_id: str) -> Sender:
103 message_details = send_get_request(f"/v0/messages/{message_id}")
104 if not message_details["success"]:
105 logger.error(f"Failed to get Sublime Alert with message_id {message_id}: {message_details['message']}")
103 - raise HTTPException(status_code=500, detail=f"Failed to get Sublime Alert with message_id {message_id}: {message_details['message']}")
106 + raise HTTPException(
107 + status_code=500,
108 + detail=f"Failed to get Sublime Alert with message_id {message_id}: {message_details['message']}",
109 + )
110 logger.info(f"Successfully retrieved Sublime Alert with message_id {message_id}")
111 return message_details["data"]["sender"]["email"]
112
113 +
114 def collect_recipient(message_id: str) -> Recipient:
115 """
116 Get a single Sublime Alert from the database
@@ -112,10 +119,14 @@ def collect_recipient(message_id: str) -> Recipient:
119 message_details = send_get_request(f"/v0/messages/{message_id}")
120 if not message_details["success"]:
121 logger.error(f"Failed to get Sublime Alert with message_id {message_id}: {message_details['message']}")
115 - raise HTTPException(status_code=500, detail=f"Failed to get Sublime Alert with message_id {message_id}: {message_details['message']}")
122 + raise HTTPException(
123 + status_code=500,
124 + detail=f"Failed to get Sublime Alert with message_id {message_id}: {message_details['message']}",
125 + )
126 logger.info(f"Successfully retrieved Sublime Alert with message_id {message_id}")
127 return message_details["data"]["recipients"][0]["email"]
128
129 +
130 def collect_alerts() -> List[SublimeAlertsResponse]:
131 """
132 Get all Sublime Alerts from the database
@@ -130,6 +141,8 @@ def collect_alerts() -> List[SublimeAlertsResponse]:
141 alert.sender = [session.query(Sender).filter(Sender.sublime_alert_id == alert.id).first()]
142 alert.recipients = session.query(Recipient).filter(Recipient.sublime_alert_id == alert.id).all()
143 logger.info("Successfully retrieved all Sublime Alerts")
133 - return SublimeAlertsResponse(success=True, message="Successfully retrieved all Sublime Alerts", sublime_alerts=[SublimeAlertsSchema.from_orm(alert) for alert in alerts])
134 -
135 -
144 + return SublimeAlertsResponse(
145 + success=True,
146 + message="Successfully retrieved all Sublime Alerts",
147 + sublime_alerts=[SublimeAlertsSchema.from_orm(alert) for alert in alerts],
148 + )
backend/app/connectors/sublime/utils/universal.py
+33 -20
@@ -1,12 +1,21 @@
1 -from typing import Dict, Any, List, Generator, Type, Optional
2 -from sqlmodel import Session, select
3 -from app.connectors.models import Connectors
1 +from typing import Any
2 +from typing import Dict
3 +from typing import Generator
4 +from typing import List
5 +from typing import Optional
6 +from typing import Type
7 +
8 +import requests
9 from elasticsearch7 import Elasticsearch
10 from loguru import logger
6 -from app.db.db_session import engine
7 -import requests
11 +from sqlmodel import Session
12 +from sqlmodel import select
13 +
14 +from app.connectors.models import Connectors
15 from app.connectors.schema import ConnectorResponse
16 from app.connectors.utils import get_connector_info_from_db
17 +from app.db.db_session import engine
18 +
19
20 def verify_sublime_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
21 """
@@ -16,22 +25,22 @@ def verify_sublime_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
25 dict: A dictionary containing 'connectionSuccessful' status and 'authToken' if the connection is successful.
26 """
27 logger.info(
19 - f"Verifying the Sublime connection to {attributes['connector_url']}",
20 - )
28 + f"Verifying the Sublime connection to {attributes['connector_url']}",
29 + )
30 try:
31 headers = {
23 - "Authorization": f"Bearer {attributes['connector_api_key']}",
24 - "Content-Type": "application/json",
25 - }
32 + "Authorization": f"Bearer {attributes['connector_api_key']}",
33 + "Content-Type": "application/json",
34 + }
35 params = {
36 "limit": 1,
37 }
38 sublime = requests.get(
30 - f"{attributes['connector_url']}/v0/rules",
31 - headers=headers,
32 - params=params,
33 - verify=False,
34 - )
39 + f"{attributes['connector_url']}/v0/rules",
40 + headers=headers,
41 + params=params,
42 + verify=False,
43 + )
44 if sublime.status_code == 200:
45 logger.info(
46 f"Connection to {attributes['connector_url']} successful",
@@ -41,13 +50,17 @@ def verify_sublime_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
50 logger.error(
51 f"Connection to {attributes['connector_url']} failed with error: {sublime.text}",
52 )
44 - return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error: {sublime.text}"}
53 + return {
54 + "connectionSuccessful": False,
55 + "message": f"Connection to {attributes['connector_url']} failed with error: {sublime.text}",
56 + }
57 except Exception as e:
58 logger.error(
59 f"Connection to {attributes['connector_url']} failed with error: {e}",
60 )
61 return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error: {e}"}
62
63 +
64 def verify_sublime_connection(connector_name: str) -> str:
65 """
66 Returns if connection to Sublime service is successful.
@@ -79,9 +92,9 @@ def send_get_request(endpoint: str, params: Optional[Dict[str, Any]] = None, con
92 return None
93 try:
94 HEADERS = {
82 - "Authorization": f"Bearer {attributes['connector_api_key']}",
83 - "Content-Type": "application/json",
84 - }
95 + "Authorization": f"Bearer {attributes['connector_api_key']}",
96 + "Content-Type": "application/json",
97 + }
98 response = requests.get(
99 f"{attributes['connector_url']}{endpoint}",
100 headers=HEADERS,
@@ -91,4 +104,4 @@ def send_get_request(endpoint: str, params: Optional[Dict[str, Any]] = None, con
104 return {"data": response.json(), "success": True, "message": "Successfully retrieved data"}
105 except Exception as e:
106 logger.error(f"Failed to send GET request to {endpoint} with error: {e}")
94 - return {"success": False, "message": f"Failed to send GET request to {endpoint} with error: {e}"}
\ No newline at end of file
107 + return {"success": False, "message": f"Failed to send GET request to {endpoint} with error: {e}"}
backend/app/connectors/utils.py
+15 -7
@@ -1,13 +1,21 @@
1 -from sqlmodel import Session, select
1 from contextlib import contextmanager
3 -from app.db.db_session import engine # Import the shared engine
4 -from app.connectors.models import Connectors
5 -from app.connectors.schema import ConnectorResponse
2 from datetime import datetime
7 -from typing import List, Optional, Generator, Type
3 +from typing import Any
4 +from typing import Dict
5 +from typing import Generator
6 +from typing import List
7 +from typing import Optional
8 +from typing import Type
9 +
10 from loguru import logger
11 from pydantic import BaseModel
10 -from typing import Dict, Any
12 +from sqlmodel import Session
13 +from sqlmodel import select
14 +
15 +from app.connectors.models import Connectors
16 +from app.connectors.schema import ConnectorResponse
17 +from app.db.db_session import engine # Import the shared engine
18 +
19
20 def get_connector_info_from_db(connector_name: str) -> Dict[str, Any]:
21 with Session(engine) as session:
@@ -19,4 +27,4 @@ def get_connector_info_from_db(connector_name: str) -> Dict[str, Any]:
27 return connector_dict
28 else:
29 logger.warning("No connector found.")
22 - return None
\ No newline at end of file
30 + return None
backend/app/connectors/velociraptor/routes/artifacts.py
+60 -23
@@ -1,30 +1,42 @@
1 +from datetime import timedelta
2 +from typing import Dict
3 from typing import List
2 -from fastapi import APIRouter, HTTPException, Security, Depends
3 -from starlette.status import HTTP_401_UNAUTHORIZED
4 +from typing import Optional
5 +from typing import Union
6 +
7 +from fastapi import APIRouter
8 +from fastapi import Depends
9 +from fastapi import HTTPException
10 +from fastapi import Security
11 from loguru import logger
5 -from datetime import timedelta
6 -from typing import Union, Dict, Optional
12 +from starlette.status import HTTP_401_UNAUTHORIZED
13
14 # App specific imports
15 from app.auth.routes.auth import auth_handler
16 +from app.connectors.velociraptor.schema.artifacts import ArtifactsResponse
17 +from app.connectors.velociraptor.schema.artifacts import CollectArtifactBody
18 +from app.connectors.velociraptor.schema.artifacts import CollectArtifactResponse
19 +from app.connectors.velociraptor.schema.artifacts import OSPrefixEnum
20 +from app.connectors.velociraptor.schema.artifacts import OSPrefixModel
21 +from app.connectors.velociraptor.schema.artifacts import QuarantineBody
22 +from app.connectors.velociraptor.schema.artifacts import QuarantineResponse
23 +from app.connectors.velociraptor.schema.artifacts import RunCommandBody
24 +from app.connectors.velociraptor.schema.artifacts import RunCommandResponse
25 +from app.connectors.velociraptor.services.artifacts import get_artifacts
26 +from app.connectors.velociraptor.services.artifacts import quarantine_host
27 +from app.connectors.velociraptor.services.artifacts import run_artifact_collection
28 +from app.connectors.velociraptor.services.artifacts import run_remote_command
29 from app.db.db_session import session
30 from app.db.universal_models import Agents
31
13 -from app.connectors.velociraptor.schema.artifacts import (
14 - ArtifactsResponse, OSPrefixEnum, OSPrefixModel, CollectArtifactBody, CollectArtifactResponse, RunCommandBody, RunCommandResponse, QuarantineBody, QuarantineResponse
15 -)
16 -
17 -
18 -
19 -from app.connectors.velociraptor.services.artifacts import get_artifacts, run_artifact_collection, run_remote_command, quarantine_host
20 -
21 -
32 velociraptor_artifacts_router = APIRouter()
33
34 +
35 # Get all valid OS prefixes
36 def get_valid_os_prefixes() -> List[str]:
37 return [prefix.name.lower() for prefix in OSPrefixEnum]
38
39 +
40 # Verify the OS prefix exists and return the appropriate Enum value
41 def verify_os_prefix_exists(os_prefix: str) -> str:
42 os_prefix_lower = os_prefix.lower()
@@ -36,6 +48,7 @@ def verify_os_prefix_exists(os_prefix: str) -> str:
48
49 return OSPrefixEnum[os_prefix_upper].value # Use the uppercase version for Enum matching
50
51 +
52 def get_os_prefix_from_os_name(os_name: str) -> str:
53 # Use the OSPrefixModel to get the OS prefix from the OS name
54 logger.info(f"Getting OS prefix from OS name {os_name}")
@@ -44,6 +57,7 @@ def get_os_prefix_from_os_name(os_name: str) -> str:
57 logger.info(f"OS prefix for OS name {os_name} is {result}")
58 return result
59
60 +
61 def get_velociraptor_id(hostname: str) -> str:
62 # Get the velociraptor_id from the hostname
63 logger.info(f"Getting velociraptor_id from hostname {hostname}")
@@ -57,12 +71,18 @@ def get_velociraptor_id(hostname: str) -> str:
71 logger.info(f"velociraptor_id for hostname {hostname} is {velociraptor_id}")
72 return velociraptor_id
73
74 +
75 @velociraptor_artifacts_router.get("", response_model=ArtifactsResponse, description="Get all artifacts")
76 async def get_all_artifacts() -> ArtifactsResponse:
77 logger.info(f"Fetching all artifacts")
78 return get_artifacts()
79
65 -@velociraptor_artifacts_router.get("/{os_prefix}", response_model=ArtifactsResponse, description="Get all artifacts for a specific OS prefix")
80 +
81 +@velociraptor_artifacts_router.get(
82 + "/{os_prefix}",
83 + response_model=ArtifactsResponse,
84 + description="Get all artifacts for a specific OS prefix",
85 +)
86 async def get_all_artifacts_for_os_prefix(os_prefix: str = Depends(verify_os_prefix_exists)) -> ArtifactsResponse:
87 logger.info(f"Fetching all artifacts for OS prefix {os_prefix}")
88 # Get all the artifacts names that begin with the OS prefix
@@ -70,7 +90,12 @@ async def get_all_artifacts_for_os_prefix(os_prefix: str = Depends(verify_os_pre
90 artifacts_for_os_prefix = [artifact for artifact in artifacts if artifact.name.startswith(os_prefix)]
91 return ArtifactsResponse(success=True, message=f"All artifacts for OS prefix {os_prefix} retrieved", artifacts=artifacts_for_os_prefix)
92
73 -@velociraptor_artifacts_router.get("/hostname/{hostname}", response_model=ArtifactsResponse, description="Get all artifacts for a specific host's OS prefix")
93 +
94 +@velociraptor_artifacts_router.get(
95 + "/hostname/{hostname}",
96 + response_model=ArtifactsResponse,
97 + description="Get all artifacts for a specific host's OS prefix",
98 +)
99 async def get_all_artifacts_for_hostname(hostname: str) -> ArtifactsResponse:
100 logger.info(f"Fetching all artifacts for hostname {hostname}")
101 agent = session.query(Agents).filter(Agents.hostname == hostname).first()
@@ -80,7 +105,12 @@ async def get_all_artifacts_for_hostname(hostname: str) -> ArtifactsResponse:
105 if not os_prefix:
106 raise HTTPException(status_code=404, detail=f"OS prefix of {agent.os.lower()} for hostname {hostname} not found")
107 result = await get_all_artifacts_for_os_prefix(os_prefix)
83 - return ArtifactsResponse(success=True, message=f"All available artifacts that can be ran for hostname {hostname} retrieved", artifacts=result.artifacts)
108 + return ArtifactsResponse(
109 + success=True,
110 + message=f"All available artifacts that can be ran for hostname {hostname} retrieved",
111 + artifacts=result.artifacts,
112 + )
113 +
114
115 @velociraptor_artifacts_router.post("/collect", response_model=CollectArtifactResponse, description="Run an analyzer")
116 async def collect_artifact(collect_artifact_body: CollectArtifactBody) -> CollectArtifactResponse:
@@ -89,37 +119,44 @@ async def collect_artifact(collect_artifact_body: CollectArtifactBody) -> Collec
119 result = await get_all_artifacts_for_hostname(collect_artifact_body.hostname)
120 artifact_names = [artifact.name for artifact in result.artifacts]
121 if collect_artifact_body.artifact_name not in artifact_names:
92 - raise HTTPException(status_code=400, detail=f"Artifact name {collect_artifact_body.artifact_name} does not apply for hostname {collect_artifact_body.hostname} or does not exist")
122 + raise HTTPException(
123 + status_code=400,
124 + detail=f"Artifact name {collect_artifact_body.artifact_name} does not apply for hostname {collect_artifact_body.hostname} or does not exist",
125 + )
126 # Add the velociraptor_id to the run_analyzer_body object
127 collect_artifact_body.velociraptor_id = get_velociraptor_id(collect_artifact_body.hostname)
128 # Run the analyzer
129 return run_artifact_collection(collect_artifact_body)
130
131 +
132 @velociraptor_artifacts_router.post("/command", response_model=RunCommandResponse, description="Run a remote command")
133 async def run_command(run_command_body: RunCommandBody) -> RunCommandResponse:
134 logger.info(f"Received request to run command {run_command_body}")
135 result = await get_all_artifacts_for_hostname(run_command_body.hostname)
136 artifact_names = [artifact.name for artifact in result.artifacts]
137 if run_command_body.artifact_name not in artifact_names:
104 - raise HTTPException(status_code=400, detail=f"Artifact name {run_command_body.artifact_name.value} does not apply for hostname {run_command_body.hostname} or does not exist")
138 + raise HTTPException(
139 + status_code=400,
140 + detail=f"Artifact name {run_command_body.artifact_name.value} does not apply for hostname {run_command_body.hostname} or does not exist",
141 + )
142 # Add the velociraptor_id to the run_command_body object
143 run_command_body.velociraptor_id = get_velociraptor_id(run_command_body.hostname)
144 # Run the command
145 return run_remote_command(run_command_body)
146
147 +
148 @velociraptor_artifacts_router.post("/quarantine", response_model=QuarantineResponse, description="Quarantine a host")
149 async def quarantine(quarantine_body: QuarantineBody) -> QuarantineResponse:
150 logger.info(f"Received request to quarantine host {quarantine_body}")
151 result = await get_all_artifacts_for_hostname(quarantine_body.hostname)
152 artifact_names = [artifact.name for artifact in result.artifacts]
153 if quarantine_body.artifact_name not in artifact_names:
116 - raise HTTPException(status_code=400, detail=f"Artifact name {quarantine_body.artifact_name.value} does not apply for hostname {quarantine_body.hostname} or does not exist")
154 + raise HTTPException(
155 + status_code=400,
156 + detail=f"Artifact name {quarantine_body.artifact_name.value} does not apply for hostname {quarantine_body.hostname} or does not exist",
157 + )
158 # Add the velociraptor_id to the run_command_body object
159 # Add the velociraptor_id to the quarantine_body object
160 quarantine_body.velociraptor_id = get_velociraptor_id(quarantine_body.hostname)
161 # Quarantine the host
162 return quarantine_host(quarantine_body)
122 -
123 -
124 -
125 -
backend/app/connectors/velociraptor/schema/artifacts.py
+26 -5
@@ -1,29 +1,38 @@
1 -from pydantic import BaseModel, Field
2 -from typing import List, Optional, Dict, Any
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 Field
9 +
10
11 class Artifacts(BaseModel):
12 description: str = Field(..., description="Description of the artifact.")
13 name: str = Field(..., description="Name of the artifact.")
14
15 +
16 class ArtifactsResponse(BaseModel):
17 message: str = Field(...)
18 # make artifacts optional
19 artifacts: Optional[List[Artifacts]]
20 success: str = Field(...)
21
22 +
23 class OSPrefixEnum(Enum):
24 LINUX = "Linux."
25 WINDOWS = "Windows."
26 MACOS = "MacOS."
19 -
27 +
28 +
29 class OSPrefixModel(BaseModel):
30 os_name: Optional[str]
31 os_prefix_mapping: Dict[str, str] = {
32 "windows": "Windows",
33 "linux": "Linux",
34 "mac": "MacOS",
26 - "ubuntu": "Linux" # Add more mappings as needed
35 + "ubuntu": "Linux", # Add more mappings as needed
36 }
37
38 def get_os_prefix(self) -> Optional[str]:
@@ -37,50 +46,62 @@ class OSPrefixModel(BaseModel):
46 if keyword in os_name_lower:
47 return prefix
48 return None
40 -
49 +
50 +
51 class OperationEnum(str, Enum):
52 collect_artifact = "collect_artifact"
53 run_command = "run_command"
54 quarantine = "quarantine"
55
56 +
57 class ActionEnum(str, Enum):
58 quarantine = "quarantine"
59 remove_quarantine = "remove_quarantine"
60
61 +
62 class CommandArtifactsEnum(str, Enum):
63 windows_powershell = "Windows.System.PowerShell"
64 windows_cmd = "Windows.System.CmdShell"
65 linux_bash = "Linux.Sys.BashShell"
66
67 +
68 class QuarantineArtifactsEnum(str, Enum):
69 windows_quarantine = "Windows.Remediation.Quarantine"
70 linux_quarantine = "Linux.Remediation.Quarantine"
71
72 +
73 class BaseBody(BaseModel):
74 hostname: str = Field(..., description="Name of the client")
75 velociraptor_id: Optional[str] = Field(None, description="Client ID of the client")
76
77 +
78 class CollectArtifactBody(BaseBody):
79 artifact_name: Optional[str] = Field(None, description="Name of the artifact for collection or command running")
80
81 +
82 class RunCommandBody(BaseBody):
83 command: Optional[str] = Field(None, description="Command to run")
84 artifact_name: CommandArtifactsEnum = Field(None, description="Name of the artifact for command running")
85
86 +
87 class QuarantineBody(BaseBody):
88 action: ActionEnum = Field(..., description="Action to perform")
89 artifact_name: QuarantineArtifactsEnum = Field(None, description="Name of the artifact for quarantine or removal of quarantine")
90
91 +
92 class BaseResponse(BaseModel):
93 message: str = Field(...)
94 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")
96
97 +
98 class CollectArtifactResponse(BaseResponse):
99 pass # If you have additional fields, you can define them here
100
101 +
102 class RunCommandResponse(BaseResponse):
103 pass # If you have additional fields, you can define them here
104
105 +
106 class QuarantineResponse(BaseResponse):
107 pass # If you have additional fields, you can define them here
backend/app/connectors/velociraptor/services/artifacts.py
+89 -69
@@ -2,16 +2,24 @@ from typing import Any
2 from typing import Dict
3 from typing import Optional
4
5 -from loguru import logger
6 -from app.db.db_session import session
5 from fastapi import HTTPException
6 +from loguru import logger
7
9 -from app.db.universal_models import Agents
8 +from app.connectors.velociraptor.schema.artifacts import Artifacts
9 +from app.connectors.velociraptor.schema.artifacts import ArtifactsResponse
10 +from app.connectors.velociraptor.schema.artifacts import CollectArtifactBody
11 +from app.connectors.velociraptor.schema.artifacts import CollectArtifactResponse
12 +from app.connectors.velociraptor.schema.artifacts import QuarantineBody
13 +from app.connectors.velociraptor.schema.artifacts import QuarantineResponse
14 +from app.connectors.velociraptor.schema.artifacts import RunCommandBody
15 +from app.connectors.velociraptor.schema.artifacts import RunCommandResponse
16 from app.connectors.velociraptor.utils.universal import UniversalService
11 -from app.connectors.velociraptor.schema.artifacts import ArtifactsResponse, Artifacts, CollectArtifactBody, CollectArtifactResponse, RunCommandBody, RunCommandResponse, QuarantineBody, QuarantineResponse
17 +from app.db.db_session import session
18 +from app.db.universal_models import Agents
19
20 universal_service = UniversalService()
21
22 +
23 def create_query(query: str) -> str:
24 """
25 Create a query string.
@@ -24,6 +32,7 @@ def create_query(query: str) -> str:
32 """
33 return query
34
35 +
36 def get_artifact_key(analyzer_body: CollectArtifactBody) -> str:
37 """
38 Construct the artifact key.
@@ -37,8 +46,8 @@ def get_artifact_key(analyzer_body: CollectArtifactBody) -> str:
46 Returns:
47 str: The constructed artifact key.
48 """
40 - action = getattr(analyzer_body, 'action', None)
41 - command = getattr(analyzer_body, 'command', None)
49 + action = getattr(analyzer_body, "action", None)
50 + command = getattr(analyzer_body, "command", None)
51
52 if action == "quarantine":
53 return f'collect_client(client_id="{analyzer_body.velociraptor_id}", artifacts=["{analyzer_body.artifact_name}"], spec=dict(`{analyzer_body.artifact_name}`=dict()))'
@@ -58,15 +67,15 @@ def get_artifacts() -> ArtifactsResponse:
67 ArtifactsResponse: A dictionary containing the artifacts.
68 """
69 logger.info(f"Fetching artifacts from Velociraptor")
61 - query = create_query("SELECT name,description FROM artifact_definitions()")
70 + query = create_query("SELECT name,description FROM artifact_definitions()")
71 all_artifacts = universal_service.execute_query(query)
63 - if all_artifacts['success']:
64 - artifacts = [Artifacts(**artifact) for artifact in all_artifacts['results']]
72 + if all_artifacts["success"]:
73 + artifacts = [Artifacts(**artifact) for artifact in all_artifacts["results"]]
74 return ArtifactsResponse(success=True, message="All artifacts retrieved", artifacts=artifacts)
75 else:
67 - raise HTTPException(status_code=500, detail=f"Failed to get all artifacts: {all_artifacts['message']}")
76 + raise HTTPException(status_code=500, detail=f"Failed to get all artifacts: {all_artifacts['message']}")
77 +
78
69 -
79 def run_artifact_collection(collect_artifact_body: CollectArtifactBody) -> CollectArtifactResponse:
80 """
81 Run an artifact collection on a client.
@@ -79,28 +88,33 @@ def run_artifact_collection(collect_artifact_body: CollectArtifactBody) -> Colle
88 """
89 try:
90 query = create_query(
82 - f"SELECT collect_client(client_id='{collect_artifact_body.velociraptor_id}', artifacts=['{collect_artifact_body.artifact_name}']) FROM scope()",
83 - )
91 + f"SELECT collect_client(client_id='{collect_artifact_body.velociraptor_id}', artifacts=['{collect_artifact_body.artifact_name}']) FROM scope()",
92 + )
93 flow = universal_service.execute_query(query)
94 logger.info(f"Successfully ran artifact collection on {flow}")
95
96 artifact_key = get_artifact_key(analyzer_body=collect_artifact_body)
97
89 - flow_id = flow['results'][0][artifact_key]['flow_id']
98 + flow_id = flow["results"][0][artifact_key]["flow_id"]
99 logger.info(f"Extracted flow_id: {flow_id}")
100
101 completed = universal_service.watch_flow_completion(flow_id)
102 logger.info(f"Successfully watched flow completion on {completed}")
103
95 - results = universal_service.read_collection_results(client_id=collect_artifact_body.velociraptor_id, flow_id=flow_id, artifact=collect_artifact_body.artifact_name)
104 + results = universal_service.read_collection_results(
105 + client_id=collect_artifact_body.velociraptor_id,
106 + flow_id=flow_id,
107 + artifact=collect_artifact_body.artifact_name,
108 + )
109
110 logger.info(f"Successfully read collection results on {results}")
111
99 - return CollectArtifactResponse(success=results['success'], message=results['message'], results=results['results'])
112 + return CollectArtifactResponse(success=results["success"], message=results["message"], results=results["results"])
113 except Exception as err:
114 logger.error(f"Failed to run artifact collection on {collect_artifact_body}: {err}")
115 raise HTTPException(status_code=500, detail=f"Failed to run artifact collection on {collect_artifact_body}: {err}")
103 -
116 +
117 +
118 def run_remote_command(run_command_body: RunCommandBody) -> RunCommandResponse:
119 """
120 Run a remote command on a client.
@@ -115,29 +129,34 @@ def run_remote_command(run_command_body: RunCommandBody) -> RunCommandResponse:
129 run_command_body.artifact_name = run_command_body.artifact_name.value
130 logger.info(f"Running remote command on {run_command_body}")
131 query = create_query(
118 - f"SELECT collect_client(client_id='{run_command_body.velociraptor_id}', urgent=true, artifacts=['{run_command_body.artifact_name}'], env=dict(Command='{run_command_body.command}')) "
119 - "FROM scope()",
120 - )
132 + f"SELECT collect_client(client_id='{run_command_body.velociraptor_id}', urgent=true, artifacts=['{run_command_body.artifact_name}'], env=dict(Command='{run_command_body.command}')) "
133 + "FROM scope()",
134 + )
135 flow = universal_service.execute_query(query)
136 logger.info(f"Successfully ran artifact collection on {flow}")
137
138 artifact_key = get_artifact_key(analyzer_body=run_command_body)
139
126 - flow_id = flow['results'][0][artifact_key]['flow_id']
140 + flow_id = flow["results"][0][artifact_key]["flow_id"]
141 logger.info(f"Extracted flow_id: {flow_id}")
142
143 completed = universal_service.watch_flow_completion(flow_id)
144 logger.info(f"Successfully watched flow completion on {completed}")
145
132 - results = universal_service.read_collection_results(client_id=run_command_body.velociraptor_id, flow_id=flow_id, artifact=run_command_body.artifact_name)
146 + results = universal_service.read_collection_results(
147 + client_id=run_command_body.velociraptor_id,
148 + flow_id=flow_id,
149 + artifact=run_command_body.artifact_name,
150 + )
151
152 logger.info(f"Successfully read collection results on {results}")
153
136 - return RunCommandResponse(success=results['success'], message=results['message'], results=results['results'])
154 + return RunCommandResponse(success=results["success"], message=results["message"], results=results["results"])
155 except Exception as err:
156 logger.error(f"Failed to run artifact collection on {run_command_body}: {err}")
157 raise HTTPException(status_code=500, detail=f"Failed to run artifact collection on {run_command_body}: {err}")
140 -
158 +
159 +
160 def quarantine_host(quarantine_body: QuarantineBody) -> QuarantineResponse:
161 """
162 Quarantine a host.
@@ -153,71 +172,72 @@ def quarantine_host(quarantine_body: QuarantineBody) -> QuarantineResponse:
172 quarantine_body.action = quarantine_body.action.value
173 if quarantine_body.action == "quarantine":
174 query = create_query(
156 - f'SELECT collect_client(client_id="{quarantine_body.velociraptor_id}", artifacts=["{quarantine_body.artifact_name}"], spec=dict(`{quarantine_body.artifact_name}`=dict())) FROM scope()'
157 - )
175 + f'SELECT collect_client(client_id="{quarantine_body.velociraptor_id}", artifacts=["{quarantine_body.artifact_name}"], spec=dict(`{quarantine_body.artifact_name}`=dict())) FROM scope()',
176 + )
177 else:
178 query = create_query(
160 - f'SELECT collect_client(client_id="{quarantine_body.velociraptor_id}", artifacts=["{quarantine_body.artifact_name}"], spec=dict(`{quarantine_body.artifact_name}`=dict(`RemovePolicy`="Y"))) FROM scope()'
161 - )
179 + f'SELECT collect_client(client_id="{quarantine_body.velociraptor_id}", artifacts=["{quarantine_body.artifact_name}"], spec=dict(`{quarantine_body.artifact_name}`=dict(`RemovePolicy`="Y"))) FROM scope()',
180 + )
181 flow = universal_service.execute_query(query)
182 logger.info(f"Successfully ran artifact collection on {flow}")
183
184 artifact_key = get_artifact_key(analyzer_body=quarantine_body)
185
167 - flow_id = flow['results'][0][artifact_key]['flow_id']
186 + flow_id = flow["results"][0][artifact_key]["flow_id"]
187 logger.info(f"Extracted flow_id: {flow_id}")
188
189 completed = universal_service.watch_flow_completion(flow_id)
190 logger.info(f"Successfully watched flow completion on {completed}")
191
173 - results = universal_service.read_collection_results(client_id=quarantine_body.velociraptor_id, flow_id=flow_id, artifact=quarantine_body.artifact_name)
192 + results = universal_service.read_collection_results(
193 + client_id=quarantine_body.velociraptor_id,
194 + flow_id=flow_id,
195 + artifact=quarantine_body.artifact_name,
196 + )
197
198 logger.info(f"Successfully read collection results on {results}")
199
177 - return QuarantineResponse(success=results['success'], message=results['message'], results=results['results'])
200 + return QuarantineResponse(success=results["success"], message=results["message"], results=results["results"])
201 except Exception as err:
202 logger.error(f"Failed to run artifact collection on {quarantine_body}: {err}")
203 raise HTTPException(status_code=500, detail=f"Failed to run artifact collection on {quarantine_body}: {err}")
204
205
183 -
184 -
185 -
206 ######################## KEEP
207 class ArtifactsService:
208 def delete_client(self, client_id: str) -> dict:
189 - """
190 - Delete a client from Velociraptor.
191 -
192 - Args:
193 - client_id (str): The ID of the client.
194 -
195 - Returns:
196 - dict: A dictionary with the success status and a message.
197 - """
198 - try:
199 - query = self._create_query(
200 - f"SELECT collect_client(client_id='server', artifacts=['Server.Utils.DeleteClient'], env=dict(ClientIdList='{client_id}',ReallyDoIt='Y')) "
201 - "FROM scope()",
202 - )
203 -
204 - flow = self.universal_service.execute_query(query)
205 - logger.info(f"Successfully ran artifact collection on {flow}")
206 -
207 - #artifact_key = f"collect_client(client_id='server', artifacts=['Server.Utils.DeleteClient'], env=dict(ClientIdList='{client_id}',ReallyDoIt='Y'))"
208 - flow_id = flow['results'][0][query]['flow_id']
209 - logger.info(f"Extracted flow_id: {flow_id}")
210 -
211 - completed = self.universal_service.watch_flow_completion(flow_id)
212 - logger.info(f"Successfully watched flow completion on {completed}")
213 -
214 - return {
215 - "message": f"Successfully deleted client {client_id}",
216 - "success": True,
217 - }
218 - except Exception as err:
219 - logger.error(f"Failed to delete client {client_id}: {err}")
220 - return {
221 - "message": f"Failed to delete client {client_id}",
222 - "success": False,
223 - }
\ No newline at end of file
209 + """
210 + Delete a client from Velociraptor.
211 +
212 + Args:
213 + client_id (str): The ID of the client.
214 +
215 + Returns:
216 + dict: A dictionary with the success status and a message.
217 + """
218 + try:
219 + query = self._create_query(
220 + f"SELECT collect_client(client_id='server', artifacts=['Server.Utils.DeleteClient'], env=dict(ClientIdList='{client_id}',ReallyDoIt='Y')) "
221 + "FROM scope()",
222 + )
223 +
224 + flow = self.universal_service.execute_query(query)
225 + logger.info(f"Successfully ran artifact collection on {flow}")
226 +
227 + # artifact_key = f"collect_client(client_id='server', artifacts=['Server.Utils.DeleteClient'], env=dict(ClientIdList='{client_id}',ReallyDoIt='Y'))"
228 + flow_id = flow["results"][0][query]["flow_id"]
229 + logger.info(f"Extracted flow_id: {flow_id}")
230 +
231 + completed = self.universal_service.watch_flow_completion(flow_id)
232 + logger.info(f"Successfully watched flow completion on {completed}")
233 +
234 + return {
235 + "message": f"Successfully deleted client {client_id}",
236 + "success": True,
237 + }
238 + except Exception as err:
239 + logger.error(f"Failed to delete client {client_id}: {err}")
240 + return {
241 + "message": f"Failed to delete client {client_id}",
242 + "success": False,
243 + }
backend/app/connectors/velociraptor/utils/universal.py
+16 -9
@@ -1,19 +1,24 @@
1 -from typing import Dict, Any, Optional
2 -from sqlmodel import Session, select
3 -from app.connectors.models import Connectors
4 -from loguru import logger
5 -from app.db.db_session import engine
6 -import requests
1 +import json
2 +from datetime import datetime
3 +from typing import Any
4 +from typing import Dict
5 +from typing import Optional
6 +
7 import grpc
8 import pika
9 -import json
10 -from datetime import datetime
9 import pyvelociraptor
12 -import json
10 +import requests
11 +from loguru import logger
12 from pyvelociraptor import api_pb2
13 from pyvelociraptor import api_pb2_grpc
14 +from sqlmodel import Session
15 +from sqlmodel import select
16 +
17 +from app.connectors.models import Connectors
18 from app.connectors.schema import ConnectorResponse
19 from app.connectors.utils import get_connector_info_from_db
20 +from app.db.db_session import engine
21 +
22
23 def verify_velociraptor_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
24 """
@@ -68,6 +73,7 @@ def verify_velociraptor_credentials(attributes: Dict[str, Any]) -> Dict[str, Any
73 logger.error(f"Failed to get connector_api_key from the database: {e}")
74 return {"connectionSuccessful": False, "message": f"Failed to get connector_api_key from the database: {e}"}
75
76 +
77 def verify_velociraptor_connection(connector_name: str) -> str:
78 """
79 Verifies the connection to Velociraptor service.
@@ -79,6 +85,7 @@ def verify_velociraptor_connection(connector_name: str) -> str:
85 return None
86 return verify_velociraptor_credentials(attributes)
87
88 +
89 class UniversalService:
90 """
91 A service class that encapsulates the logic for polling messages from Velociraptor.
backend/app/connectors/wazuh_indexer/routes/alerts.py
+41 -16
@@ -1,19 +1,31 @@
1 from typing import List
2 -from fastapi import APIRouter, HTTPException, Security, Depends
3 -from starlette.status import HTTP_401_UNAUTHORIZED
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 starlette.status import HTTP_401_UNAUTHORIZED
9
10 # App specific imports
11 from app.auth.routes.auth import auth_handler
8 -from app.db.db_session import session
9 -from app.connectors.wazuh_indexer.schema.alerts import (
10 - AlertsSearchBody, AlertsSearchResponse, HostAlertsSearchBody, HostAlertsSearchResponse, IndexAlertsSearchBody, IndexAlertsSearchResponse, AlertsByHostResponse, AlertsByRuleResponse, AlertsByRulePerHostResponse
11 -)
12 -
13 -from app.connectors.wazuh_indexer.services.alerts import get_alerts, get_host_alerts, get_index_alerts, get_alerts_by_host, get_alerts_by_rule, get_alerts_by_rule_per_host
14 -
12 +from app.connectors.wazuh_indexer.schema.alerts import AlertsByHostResponse
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 HostAlertsSearchBody
18 +from app.connectors.wazuh_indexer.schema.alerts import HostAlertsSearchResponse
19 +from app.connectors.wazuh_indexer.schema.alerts import IndexAlertsSearchBody
20 +from app.connectors.wazuh_indexer.schema.alerts import IndexAlertsSearchResponse
21 +from app.connectors.wazuh_indexer.services.alerts import get_alerts
22 +from app.connectors.wazuh_indexer.services.alerts import get_alerts_by_host
23 +from app.connectors.wazuh_indexer.services.alerts import get_alerts_by_rule
24 +from app.connectors.wazuh_indexer.services.alerts import get_alerts_by_rule_per_host
25 +from app.connectors.wazuh_indexer.services.alerts import get_host_alerts
26 +from app.connectors.wazuh_indexer.services.alerts import get_index_alerts
27 from app.connectors.wazuh_indexer.utils.universal import collect_indices
16 -
28 +from app.db.db_session import session
29
30 wazuh_indexer_alerts_router = APIRouter()
31
@@ -22,41 +34,57 @@ def get_index_names() -> List[str]:
34 indices = collect_indices()
35 return indices.indices_list
36
37 +
38 def verify_index_name(index_alerts_search_body: IndexAlertsSearchBody) -> IndexAlertsSearchBody:
39 # Remove any extra spaces from index_name
40 index_alerts_search_body.index_name = index_alerts_search_body.index_name.strip()
41
42 managed_index_names = get_index_names()
43 if index_alerts_search_body.index_name not in managed_index_names:
31 - raise HTTPException(status_code=400, detail=f"Index name '{index_alerts_search_body.index_name}' is not managed by Wazuh Indexer or no longer exists.")
44 + raise HTTPException(
45 + status_code=400,
46 + detail=f"Index name '{index_alerts_search_body.index_name}' is not managed by Wazuh Indexer or no longer exists.",
47 + )
48 return index_alerts_search_body
49
50 +
51 @wazuh_indexer_alerts_router.post("", response_model=AlertsSearchResponse, description="Get all alerts")
52 async def get_all_alerts(alerts_search_body: AlertsSearchBody) -> AlertsSearchResponse:
53 logger.info(f"Fetching all alerts")
54 return get_alerts(alerts_search_body)
55
56 +
57 @wazuh_indexer_alerts_router.post("/host", response_model=HostAlertsSearchResponse, description="Get all alerts for a host")
58 async def get_all_alerts_for_host(host_alerts_search_body: HostAlertsSearchBody) -> HostAlertsSearchResponse:
59 logger.info(f"Fetching all alerts for host {host_alerts_search_body.agent_name}")
60 return get_host_alerts(host_alerts_search_body)
61
62 +
63 @wazuh_indexer_alerts_router.post("/index", response_model=IndexAlertsSearchResponse, description="Get all alerts for an index")
45 -async def get_all_alerts_for_index(index_alerts_search_body: IndexAlertsSearchBody = Depends(verify_index_name)) -> IndexAlertsSearchResponse:
64 +async def get_all_alerts_for_index(
65 + index_alerts_search_body: IndexAlertsSearchBody = Depends(verify_index_name),
66 +) -> IndexAlertsSearchResponse:
67 logger.info(f"Fetching all alerts for index {index_alerts_search_body.index_name}")
68 return get_index_alerts(index_alerts_search_body)
69
70 +
71 @wazuh_indexer_alerts_router.post("/hosts/all", response_model=AlertsByHostResponse, description="Get number of all alerts for all hosts")
72 async def get_all_alerts_by_host(alerts_search_body: AlertsSearchBody) -> AlertsByHostResponse:
73 logger.info(f"Fetching number of all alerts for all hosts")
74 return get_alerts_by_host(alerts_search_body)
75
76 +
77 @wazuh_indexer_alerts_router.post("/rules/all", response_model=AlertsByRuleResponse, description="Get number of all alerts for all rules")
78 async def get_all_alerts_by_rule(alerts_search_body: AlertsSearchBody) -> AlertsByRuleResponse:
79 logger.info(f"Fetching number of all alerts for all rules")
80 return get_alerts_by_rule(alerts_search_body)
81
59 -@wazuh_indexer_alerts_router.post("/rules/hosts/all", response_model=AlertsByRulePerHostResponse, description="Get number of all alerts for all rules per host")
82 +
83 +@wazuh_indexer_alerts_router.post(
84 + "/rules/hosts/all",
85 + response_model=AlertsByRulePerHostResponse,
86 + description="Get number of all alerts for all rules per host",
87 +)
88 async def get_all_alerts_by_rule_per_host(alerts_search_body: AlertsSearchBody) -> AlertsByRulePerHostResponse:
89 """
90 Get number of all alerts for all rules per host
@@ -69,6 +97,3 @@ async def get_all_alerts_by_rule_per_host(alerts_search_body: AlertsSearchBody)
97 """
98 logger.info(f"Fetching number of all alerts for all rules per host")
99 return get_alerts_by_rule_per_host(alerts_search_body)
72 -
73 -
74 -
backend/app/connectors/wazuh_indexer/routes/monitoring.py
+31 -11
@@ -1,14 +1,31 @@
1 -from fastapi import APIRouter, HTTPException, Request
1 +from typing import List
2 +from typing import Union
3 +
4 +from fastapi import APIRouter
5 +from fastapi import HTTPException
6 +from fastapi import Request
7 from fastapi.responses import JSONResponse
3 -from typing import List, Union
4 -from app.connectors.schema import ConnectorResponse, ConnectorListResponse, VerifyConnectorResponse, ConnectorsListResponse
5 -from app.connectors.services import ConnectorServices
6 -#from app.connectors.wazuh_indexer.schema import WazuhIndexerResponse, WazuhIndexerListResponse
7 -from app.connectors.wazuh_indexer.services.monitoring import cluster_healthcheck, node_allocation, indices_stats, shards
8 -from app.connectors.wazuh_indexer.schema.monitoring import ClusterHealthResponse, NodeAllocationResponse, IndicesStatsResponse, ShardsResponse
8 from loguru import logger
9 +
10 +from app.connectors.schema import ConnectorListResponse
11 +from app.connectors.schema import ConnectorResponse
12 +from app.connectors.schema import ConnectorsListResponse
13 +from app.connectors.schema import VerifyConnectorResponse
14 +from app.connectors.services import ConnectorServices
15 +from app.connectors.wazuh_indexer.schema.monitoring import ClusterHealthResponse
16 +from app.connectors.wazuh_indexer.schema.monitoring import IndicesStatsResponse
17 +from app.connectors.wazuh_indexer.schema.monitoring import NodeAllocationResponse
18 +from app.connectors.wazuh_indexer.schema.monitoring import ShardsResponse
19 +
20 +# from app.connectors.wazuh_indexer.schema import WazuhIndexerResponse, WazuhIndexerListResponse
21 +from app.connectors.wazuh_indexer.services.monitoring import cluster_healthcheck
22 +from app.connectors.wazuh_indexer.services.monitoring import indices_stats
23 +from app.connectors.wazuh_indexer.services.monitoring import node_allocation
24 +from app.connectors.wazuh_indexer.services.monitoring import shards
25 +
26 wazuh_indexer_router = APIRouter()
27
28 +
29 @wazuh_indexer_router.get("/health", response_model=ClusterHealthResponse, description="Fetch Wazuh Indexer cluster health")
30 async def get_cluster_health() -> Union[ClusterHealthResponse, HTTPException]:
31 """
@@ -27,7 +44,8 @@ async def get_cluster_health() -> Union[ClusterHealthResponse, HTTPException]:
44 return cluster_health
45 else:
46 raise HTTPException(status_code=500, detail="Failed to retrieve cluster health.")
30 -
47 +
48 +
49 @wazuh_indexer_router.get("/allocation", response_model=NodeAllocationResponse, description="Fetch Wazuh Indexer node allocation")
50 async def get_node_allocation() -> Union[NodeAllocationResponse, HTTPException]:
51 """
@@ -46,7 +64,8 @@ async def get_node_allocation() -> Union[NodeAllocationResponse, HTTPException]:
64 return node_allocation_response
65 else:
66 raise HTTPException(status_code=500, detail="Failed to retrieve node allocation.")
49 -
67 +
68 +
69 @wazuh_indexer_router.get("/indices", response_model=IndicesStatsResponse, description="Fetch Wazuh Indexer indices stats")
70 async def get_indices_stats() -> Union[IndicesStatsResponse, HTTPException]:
71 """
@@ -65,7 +84,8 @@ async def get_indices_stats() -> Union[IndicesStatsResponse, HTTPException]:
84 return indices_stats_response
85 else:
86 raise HTTPException(status_code=500, detail="Failed to retrieve indices stats.")
68 -
87 +
88 +
89 @wazuh_indexer_router.get("/shards", response_model=ShardsResponse, description="Fetch Wazuh Indexer shards")
90 async def get_shards() -> Union[ShardsResponse, HTTPException]:
91 """
@@ -83,4 +103,4 @@ async def get_shards() -> Union[ShardsResponse, HTTPException]:
103 if shards_response is not None:
104 return shards_response
105 else:
86 - raise HTTPException(status_code=500, detail="Failed to retrieve shards.")
\ No newline at end of file
106 + raise HTTPException(status_code=500, detail="Failed to retrieve shards.")
backend/app/connectors/wazuh_indexer/schema/alerts.py
+24 -4
@@ -1,12 +1,20 @@
1 -from pydantic import BaseModel, Field, validator
2 -from typing import Optional, List, Dict, Any
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 from typing import Union
6
7 +from pydantic import BaseModel
8 +from pydantic import Field
9 +from pydantic import validator
10 +
11 +
12 class Alert(BaseModel):
13 index_name: str
14 total_alerts: int
15 alerts: Optional[List[Dict[str, Any]]] = Field([], description="The alerts returned from the search.")
16
17 +
18 class AlertsSearchBody(BaseModel):
19 size: int = Field(10, description="The number of alerts to return.")
20 timerange: str = Field("24h", description="The time range to search alerts in.")
@@ -18,62 +26,74 @@ class AlertsSearchBody(BaseModel):
26 def validate_timerange(cls, value):
27 if value[-1] not in ("h", "d", "w", "m"):
28 raise ValueError("Invalid timerange format. The string should end with either 'h', 'd', 'w', or 'm'.")
21 -
29 +
30 # Optionally, you can check that the prefix is a number
31 if not value[:-1].isdigit():
32 raise ValueError("Invalid timerange format. The string should start with a number.")
25 -
33 +
34 return value
35
36 +
37 class AlertsSearchResponse(BaseModel):
38 alerts_summary: List[Alert]
39 success: bool
40 message: str
41
42 +
43 class CollectAlertsResponse(BaseModel):
44 alerts: List[Dict[str, Any]]
45 success: bool
46 message: str
47
48 +
49 class HostAlertsSearchBody(AlertsSearchBody):
50 agent_name: str = Field(..., description="The name of the agent to search alerts for.")
51
52 +
53 class HostAlertsSearchResponse(BaseModel):
54 alerts_summary: List[Alert]
55 success: bool
56 message: str
57
58 +
59 class IndexAlertsSearchBody(AlertsSearchBody):
60 index_name: str = Field(..., description="The name of the index to search alerts for.")
61
62 +
63 class IndexAlertsSearchResponse(BaseModel):
64 alerts_summary: List[Alert]
65 success: bool
66 message: str
67
68 +
69 class AlertsByHost(BaseModel):
70 agent_name: str
71 number_of_alerts: int
72
73 +
74 class AlertsByHostResponse(BaseModel):
75 alerts_by_host: List[AlertsByHost]
76 success: bool
77 message: str
78
79 +
80 class AlertsByRule(BaseModel):
81 rule: str
82 number_of_alerts: int
83
84 +
85 class AlertsByRuleResponse(BaseModel):
86 alerts_by_rule: List[AlertsByRule]
87 success: bool
88 message: str
89
90 +
91 class AlertsByRulePerHost(BaseModel):
92 agent_name: str
93 number_of_alerts: int
94 rule: str
95
96 +
97 class AlertsByRulePerHostResponse(BaseModel):
98 alerts_by_rule_per_host: List[AlertsByRulePerHost]
99 success: bool
backend/app/connectors/wazuh_indexer/schema/indices.py
+10 -6
@@ -1,19 +1,26 @@
1 -from pydantic import BaseModel, Field
2 -from typing import Optional, List, Dict, Any
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 from typing import Union
6
7 +from pydantic import BaseModel
8 +from pydantic import Field
9 +
10 +
11 class Indices(BaseModel):
12 indices_list: list
13 success: bool
14 message: str
15
16 +
17 class IndexConfigModel(BaseModel):
18 SKIP_INDEX_NAMES: Dict[str, bool] = Field(
19 default={
20 "wazuh-statistics": True,
21 "wazuh-monitoring": True,
22 },
16 - description="A dictionary containing index names to be skipped and their skip status."
23 + description="A dictionary containing index names to be skipped and their skip status.",
24 )
25
26 def is_index_skipped(self, index_name: str) -> bool:
@@ -39,6 +46,3 @@ class IndexConfigModel(BaseModel):
46 bool: True if the index is valid, False otherwise.
47 """
48 return index_name.startswith("wazuh") and not self.is_index_skipped(index_name)
42 -
43 -
44 -
backend/app/connectors/wazuh_indexer/schema/monitoring.py
+13 -2
@@ -1,7 +1,12 @@
1 -from pydantic import BaseModel
2 -from typing import Optional, List, Dict, Any
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 from typing import Union
6
7 +from pydantic import BaseModel
8 +
9 +
10 class ClusterHealth(BaseModel):
11 active_primary_shards: int
12 active_shards: int
@@ -21,6 +26,7 @@ class ClusterHealth(BaseModel):
26 timed_out: bool
27 unassigned_shards: int
28
29 +
30 class ClusterHealthResponse(BaseModel):
31 cluster_health: Optional[ClusterHealth]
32 message: str
@@ -34,11 +40,13 @@ class NodeAllocation(BaseModel):
40 disk_used: str
41 node: str
42
43 +
44 class NodeAllocationResponse(BaseModel):
45 node_allocation: Optional[List[NodeAllocation]]
46 message: str
47 success: bool
48
49 +
50 class IndicesStats(BaseModel):
51 docs_count: str
52 health: str
@@ -46,11 +54,13 @@ class IndicesStats(BaseModel):
54 replica_count: str
55 store_size: str
56
57 +
58 class IndicesStatsResponse(BaseModel):
59 indices_stats: Optional[List[IndicesStats]]
60 message: str
61 success: bool
62
63 +
64 class Shards(BaseModel):
65 index: str
66 node: str
@@ -58,6 +68,7 @@ class Shards(BaseModel):
68 state: str
69 size: str
70
71 +
72 class ShardsResponse(BaseModel):
73 shards: Optional[List[Shards]]
74 message: str
backend/app/connectors/wazuh_indexer/services/alerts.py
+53 -31
@@ -1,23 +1,47 @@
1 -from typing import Dict, Any, Type, Optional, List
2 -from pydantic import BaseModel
3 -from sqlmodel import Session, select
4 -from app.connectors.models import Connectors
5 -from elasticsearch7 import Elasticsearch
6 -from loguru import logger
7 -from app.db.db_session import engine
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 +from typing import Type
6 from typing import Union
7 +
8 import requests
9 +from elasticsearch7 import Elasticsearch
10 +from fastapi import HTTPException
11 +from loguru import logger
12 +from pydantic import BaseModel
13 +from sqlmodel import Session
14 +from sqlmodel import select
15 +
16 +from app.connectors.models import Connectors
17 from app.connectors.schema import ConnectorResponse
18 from app.connectors.utils import get_connector_info_from_db
12 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client, format_node_allocation, format_indices_stats, format_shards, collect_indices, AlertsQueryBuilder
13 -from app.connectors.wazuh_indexer.schema.alerts import AlertsSearchBody, CollectAlertsResponse, AlertsSearchResponse, HostAlertsSearchBody, HostAlertsSearchResponse, IndexAlertsSearchBody, IndexAlertsSearchResponse, AlertsByHost, AlertsByHostResponse, AlertsByRuleResponse, AlertsByRule, AlertsByRulePerHost, AlertsByRulePerHostResponse
19 +from app.connectors.wazuh_indexer.schema.alerts import AlertsByHost
20 +from app.connectors.wazuh_indexer.schema.alerts import AlertsByHostResponse
21 +from app.connectors.wazuh_indexer.schema.alerts import AlertsByRule
22 +from app.connectors.wazuh_indexer.schema.alerts import AlertsByRulePerHost
23 +from app.connectors.wazuh_indexer.schema.alerts import AlertsByRulePerHostResponse
24 +from app.connectors.wazuh_indexer.schema.alerts import AlertsByRuleResponse
25 +from app.connectors.wazuh_indexer.schema.alerts import AlertsSearchBody
26 +from app.connectors.wazuh_indexer.schema.alerts import AlertsSearchResponse
27 +from app.connectors.wazuh_indexer.schema.alerts import CollectAlertsResponse
28 +from app.connectors.wazuh_indexer.schema.alerts import HostAlertsSearchBody
29 +from app.connectors.wazuh_indexer.schema.alerts import HostAlertsSearchResponse
30 +from app.connectors.wazuh_indexer.schema.alerts import IndexAlertsSearchBody
31 +from app.connectors.wazuh_indexer.schema.alerts import IndexAlertsSearchResponse
32 from app.connectors.wazuh_indexer.schema.indices import IndexConfigModel
15 -from fastapi import HTTPException
33 +from app.connectors.wazuh_indexer.utils.universal import AlertsQueryBuilder
34 +from app.connectors.wazuh_indexer.utils.universal import collect_indices
35 +from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
36 +from app.connectors.wazuh_indexer.utils.universal import format_indices_stats
37 +from app.connectors.wazuh_indexer.utils.universal import format_node_allocation
38 +from app.connectors.wazuh_indexer.utils.universal import format_shards
39 +from app.db.db_session import engine
40
41 # def collect_and_aggregate_alerts(field_name: str, search_body: AlertsSearchBody) -> Dict[str, int]:
42 # indices = collect_indices()
43 # aggregated_alerts_dict = {}
20 -
44 +
45 # for index_name in indices.indices_list:
46 # try:
47 # alerts_response = collect_alerts_generic(index_name, body=search_body)
@@ -27,13 +51,14 @@ from fastapi import HTTPException
51 # aggregated_alerts_dict[field_value] = aggregated_alerts_dict.get(field_value, 0) + 1
52 # except HTTPException as e:
53 # logger.warning(f"An error occurred while processing index {index_name}: {e.detail}")
30 -
54 +
55 # return aggregated_alerts_dict
56
57 +
58 def collect_and_aggregate_alerts(field_names: List[str], search_body: AlertsSearchBody) -> Dict[str, int]:
59 indices = collect_indices()
60 aggregated_alerts_dict = {}
36 -
61 +
62 for index_name in indices.indices_list:
63 try:
64 alerts_response = collect_alerts_generic(index_name, body=search_body)
@@ -43,11 +68,12 @@ def collect_and_aggregate_alerts(field_names: List[str], search_body: AlertsSear
68 aggregated_alerts_dict[composite_key] = aggregated_alerts_dict.get(composite_key, 0) + 1
69 except HTTPException as e:
70 logger.warning(f"An error occurred while processing index {index_name}: {e.detail}")
46 -
71 +
72 return aggregated_alerts_dict
73
74 +
75 def collect_alerts_generic(index_name: str, body: AlertsSearchBody, is_host_specific: bool = False) -> CollectAlertsResponse:
50 - es_client = create_wazuh_indexer_client('Wazuh-Indexer')
76 + es_client = create_wazuh_indexer_client("Wazuh-Indexer")
77 query_builder = AlertsQueryBuilder()
78 query_builder.add_time_range(timerange=body.timerange, timestamp_field=body.timestamp_field)
79 query_builder.add_matches(matches=[(body.alert_field, body.alert_value)])
@@ -68,12 +94,13 @@ def collect_alerts_generic(index_name: str, body: AlertsSearchBody, is_host_spec
94 logger.debug(f"Failed to collect alerts: {e}")
95 return CollectAlertsResponse(alerts=[], success=False, message=f"Failed to collect alerts: {e}")
96
97 +
98 def get_alerts_generic(search_body: Type[AlertsSearchBody], is_host_specific: bool = False, index_name: Optional[str] = None):
99 logger.info(f"Collecting Wazuh Indexer alerts for host {search_body.agent_name if is_host_specific else ''}")
100 alerts_summary = []
101 indices = collect_indices()
102 index_list = [index_name] if index_name else indices.indices_list # Use the provided index_name or get all indices
76 -
103 +
104 for index_name in index_list:
105 try:
106 alerts = collect_alerts_generic(index_name, body=search_body, is_host_specific=is_host_specific)
@@ -87,26 +114,30 @@ def get_alerts_generic(search_body: Type[AlertsSearchBody], is_host_specific: bo
114 )
115 except HTTPException as e:
116 logger.warning(f"An error occurred while processing index {index_name}: {e.detail}")
90 -
117 +
118 if len(alerts_summary) == 0:
119 message = "No alerts found"
120 else:
121 message = f"Succesfully collected top {search_body.size} alerts for each index"
95 -
122 +
123 return {"alerts_summary": alerts_summary, "success": len(alerts_summary) > 0, "message": message}
124
125 +
126 def get_alerts(search_body: AlertsSearchBody) -> AlertsSearchResponse:
127 result = get_alerts_generic(search_body)
128 return AlertsSearchResponse(**result)
129
130 +
131 def get_host_alerts(search_body: HostAlertsSearchBody) -> HostAlertsSearchResponse:
132 result = get_alerts_generic(search_body, is_host_specific=True)
133 return HostAlertsSearchResponse(**result)
134
135 +
136 def get_index_alerts(search_body: IndexAlertsSearchBody) -> IndexAlertsSearchResponse:
137 result = get_alerts_generic(search_body, index_name=search_body.index_name)
138 return IndexAlertsSearchResponse(**result)
139
140 +
141 def get_alerts_by_host(search_body: AlertsSearchBody) -> AlertsByHostResponse:
142 aggregated_by_host = collect_and_aggregate_alerts(["agent_name"], search_body)
143 alerts_by_host_list: List[AlertsByHost] = [
@@ -116,7 +147,7 @@ def get_alerts_by_host(search_body: AlertsSearchBody) -> AlertsByHostResponse:
147 return AlertsByHostResponse(
148 alerts_by_host=alerts_by_host_list,
149 success=bool(alerts_by_host_list),
119 - message="Successfully collected alerts by host"
150 + message="Successfully collected alerts by host",
151 )
152
153
@@ -129,7 +160,7 @@ def get_alerts_by_rule(search_body: AlertsSearchBody) -> AlertsByRuleResponse:
160 return AlertsByRuleResponse(
161 alerts_by_rule=alerts_by_rule_list,
162 success=bool(alerts_by_rule_list),
132 - message="Successfully collected alerts by rule"
163 + message="Successfully collected alerts by rule",
164 )
165
166
@@ -139,18 +170,9 @@ def get_alerts_by_rule_per_host(search_body: AlertsSearchBody) -> AlertsByRulePe
170 AlertsByRulePerHost(agent_name=agent_name, rule=rule, number_of_alerts=count)
171 for (agent_name, rule), count in aggregated_by_rule_per_host.items()
172 ]
142 -
173 +
174 return AlertsByRulePerHostResponse(
175 alerts_by_rule_per_host=alerts_by_rule_per_host_list,
176 success=bool(alerts_by_rule_per_host_list),
146 - message="Successfully collected alerts by rule per host"
177 + message="Successfully collected alerts by rule per host",
178 )
148 -
149 -
150 -
151 -
152 -
153 -
154 -
155 -
156 -
backend/app/connectors/wazuh_indexer/services/monitoring.py
+39 -26
@@ -1,15 +1,29 @@
1 -from typing import Dict, Any
2 -from sqlmodel import Session, select
3 -from app.connectors.models import Connectors
4 -from elasticsearch7 import Elasticsearch
5 -from loguru import logger
6 -from app.db.db_session import engine
1 +from typing import Any
2 +from typing import Dict
3 from typing import Union
4 +
5 import requests
6 +from elasticsearch7 import Elasticsearch
7 +from loguru import logger
8 +from sqlmodel import Session
9 +from sqlmodel import select
10 +
11 +from app.connectors.models import Connectors
12 from app.connectors.schema import ConnectorResponse
13 from app.connectors.utils import get_connector_info_from_db
11 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client, format_node_allocation, format_indices_stats, format_shards
12 -from app.connectors.wazuh_indexer.schema.monitoring import ClusterHealthResponse, ClusterHealth, NodeAllocationResponse, NodeAllocation, IndicesStatsResponse, IndicesStats, Shards, ShardsResponse
14 +from app.connectors.wazuh_indexer.schema.monitoring import ClusterHealth
15 +from app.connectors.wazuh_indexer.schema.monitoring import ClusterHealthResponse
16 +from app.connectors.wazuh_indexer.schema.monitoring import IndicesStats
17 +from app.connectors.wazuh_indexer.schema.monitoring import IndicesStatsResponse
18 +from app.connectors.wazuh_indexer.schema.monitoring import NodeAllocation
19 +from app.connectors.wazuh_indexer.schema.monitoring import NodeAllocationResponse
20 +from app.connectors.wazuh_indexer.schema.monitoring import Shards
21 +from app.connectors.wazuh_indexer.schema.monitoring import ShardsResponse
22 +from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
23 +from app.connectors.wazuh_indexer.utils.universal import format_indices_stats
24 +from app.connectors.wazuh_indexer.utils.universal import format_node_allocation
25 +from app.connectors.wazuh_indexer.utils.universal import format_shards
26 +from app.db.db_session import engine
27
28
29 def cluster_healthcheck() -> Union[ClusterHealthResponse, Dict[str, str]]:
@@ -23,19 +37,20 @@ def cluster_healthcheck() -> Union[ClusterHealthResponse, Dict[str, str]]:
37 Exception: An exception is raised if the cluster health cannot be retrieved.
38 """
39 logger.info("Collecting Wazuh Indexer healthcheck")
26 - es_client = create_wazuh_indexer_client('Wazuh-Indexer')
40 + es_client = create_wazuh_indexer_client("Wazuh-Indexer")
41 try:
42 cluster_health_data = es_client.cluster.health()
43 cluster_health_model = ClusterHealth(**cluster_health_data)
44 return ClusterHealthResponse(
45 cluster_health=cluster_health_model,
46 success=True,
33 - message="Successfully collected Wazuh Indexer cluster health"
47 + message="Successfully collected Wazuh Indexer cluster health",
48 )
49 except Exception as e:
50 logger.error(f"Cluster health check failed with error: {e}")
51 return {"success": False, "message": f"Cluster health check failed with error: {e}"}
38 -
52 +
53 +
54 def node_allocation() -> Union[NodeAllocationResponse, Dict[str, bool]]:
55 """
56 Returns the node allocation of the Wazuh Indexer service.
@@ -47,24 +62,25 @@ def node_allocation() -> Union[NodeAllocationResponse, Dict[str, bool]]:
62 Exception: An exception is raised if the node allocation cannot be retrieved.
63 """
64 logger.info("Collecting Wazuh Indexer node allocation")
50 - es_client = create_wazuh_indexer_client('Wazuh-Indexer')
65 + es_client = create_wazuh_indexer_client("Wazuh-Indexer")
66 try:
67 raw_node_allocation_data = es_client.cat.allocation(format="json")
68 logger.info(raw_node_allocation_data)
54 -
69 +
70 formatted_node_allocation_data = format_node_allocation(raw_node_allocation_data)
56 -
71 +
72 node_allocation_models = [NodeAllocation(**node) for node in formatted_node_allocation_data]
73
74 return NodeAllocationResponse(
75 node_allocation=node_allocation_models,
76 success=True,
62 - message="Successfully collected Wazuh Indexer node allocation"
77 + message="Successfully collected Wazuh Indexer node allocation",
78 )
79 except Exception as e:
80 logger.error(f"Node allocation check failed with error: {e}")
81 return {"success": False, "message": f"Node allocation check failed with error: {e}"}
82
83 +
84 def indices_stats() -> Union[IndicesStatsResponse, Dict[str, str]]:
85 """
86 Returns the indices stats of the Wazuh Indexer service.
@@ -76,23 +92,24 @@ def indices_stats() -> Union[IndicesStatsResponse, Dict[str, str]]:
92 Exception: An exception is raised if the indices stats cannot be retrieved.
93 """
94 logger.info("Collecting Wazuh Indexer indices stats")
79 - es_client = create_wazuh_indexer_client('Wazuh-Indexer')
95 + es_client = create_wazuh_indexer_client("Wazuh-Indexer")
96 try:
97 raw_indices_stats_data = es_client.cat.indices(format="json")
98
99 formatted_indices_stats_data = format_indices_stats(raw_indices_stats_data)
84 -
100 +
101 indices_stats_models = [IndicesStats(**index) for index in formatted_indices_stats_data]
102
103 return IndicesStatsResponse(
104 indices_stats=indices_stats_models,
105 success=True,
90 - message="Successfully collected Wazuh Indexer indices stats"
106 + message="Successfully collected Wazuh Indexer indices stats",
107 )
108 except Exception as e:
109 logger.error(f"Indices stats check failed with error: {e}")
110 return {"success": False, "message": f"Indices stats check failed with error: {e}"}
95 -
111 +
112 +
113 def shards() -> Union[ShardsResponse, Dict[str, str]]:
114 """
115 Returns the shards of the Wazuh Indexer service.
@@ -104,7 +121,7 @@ def shards() -> Union[ShardsResponse, Dict[str, str]]:
121 Exception: An exception is raised if the shards cannot be retrieved.
122 """
123 logger.info("Collecting Wazuh Indexer shards")
107 - es_client = create_wazuh_indexer_client('Wazuh-Indexer')
124 + es_client = create_wazuh_indexer_client("Wazuh-Indexer")
125 try:
126 raw_shards_data = es_client.cat.shards(format="json")
127
@@ -112,11 +129,7 @@ def shards() -> Union[ShardsResponse, Dict[str, str]]:
129
130 shard_models = [Shards(**shard) for shard in formatted_shards_data]
131
115 - return ShardsResponse(
116 - shards=shard_models,
117 - success=True,
118 - message="Successfully collected Wazuh Indexer shards"
119 - )
132 + return ShardsResponse(shards=shard_models, success=True, message="Successfully collected Wazuh Indexer shards")
133 except Exception as e:
134 logger.error(f"Shards check failed with error: {e}")
122 - return {"success": False, "message": f"Shards check failed with error: {e}"}
\ No newline at end of file
135 + return {"success": False, "message": f"Shards check failed with error: {e}"}
backend/app/connectors/wazuh_indexer/utils/universal.py
+85 -69
@@ -1,17 +1,25 @@
1 -from typing import Dict, Any, List, Generator, Type
2 -from sqlmodel import Session, select
3 -from app.connectors.models import Connectors
1 +from datetime import datetime
2 +from datetime import timedelta
3 +from typing import Any
4 +from typing import Dict
5 +from typing import Generator
6 +from typing import Iterable
7 +from typing import List
8 +from typing import Tuple
9 +from typing import Type
10 +
11 +import requests
12 from elasticsearch7 import Elasticsearch
13 from loguru import logger
6 -from app.db.db_session import engine
7 -import requests
14 +from sqlmodel import Session
15 +from sqlmodel import select
16 +
17 +from app.connectors.models import Connectors
18 from app.connectors.schema import ConnectorResponse
19 from app.connectors.utils import get_connector_info_from_db
10 -from app.connectors.wazuh_indexer.schema.indices import Indices, IndexConfigModel
11 -from datetime import datetime, timedelta
12 -from typing import Iterable, Tuple
13 -
14 -
20 +from app.connectors.wazuh_indexer.schema.indices import IndexConfigModel
21 +from app.connectors.wazuh_indexer.schema.indices import Indices
22 +from app.db.db_session import engine
23
24
25 def verify_wazuh_indexer_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
@@ -22,7 +30,7 @@ def verify_wazuh_indexer_credentials(attributes: Dict[str, Any]) -> Dict[str, An
30 dict: A dictionary containing 'connectionSuccessful' status and 'authToken' if the connection is successful.
31 """
32 logger.info(f"Verifying the wazuh-indexer connection to {attributes['connector_url']}")
25 -
33 +
34 try:
35 es = Elasticsearch(
36 [attributes["connector_url"]],
@@ -38,7 +46,8 @@ def verify_wazuh_indexer_credentials(attributes: Dict[str, Any]) -> Dict[str, An
46 except Exception as e:
47 logger.error(f"Connection to {attributes['connector_url']} failed with error: {e}")
48 return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error: {e}"}
41 -
49 +
50 +
51 def verify_wazuh_indexer_connection(connector_name: str) -> str:
52 """
53 Returns the authentication token for the Wazuh Indexer service.
@@ -52,6 +61,7 @@ def verify_wazuh_indexer_connection(connector_name: str) -> str:
61 return None
62 return verify_wazuh_indexer_credentials(attributes)
63
64 +
65 def create_wazuh_indexer_client(connector_name: str) -> Elasticsearch:
66 """
67 Returns an Elasticsearch client for the Wazuh Indexer service.
@@ -72,71 +82,75 @@ def create_wazuh_indexer_client(connector_name: str) -> Elasticsearch:
82 retry_on_timeout=False,
83 )
84
85 +
86 def format_node_allocation(node_allocation):
76 - """
77 - Format the node allocation details into a list of dictionaries. Each dictionary contains disk used, disk available, total disk, disk
78 - usage percentage, and node name.
87 + """
88 + Format the node allocation details into a list of dictionaries. Each dictionary contains disk used, disk available, total disk, disk
89 + usage percentage, and node name.
90
80 - Args:
81 - node_allocation: Node allocation details from Elasticsearch.
91 + Args:
92 + node_allocation: Node allocation details from Elasticsearch.
93 +
94 + Returns:
95 + list: A list of dictionaries containing formatted node allocation details.
96 + """
97 + return [
98 + {
99 + "disk_used": node["disk.used"],
100 + "disk_available": node["disk.avail"],
101 + "disk_total": node["disk.total"],
102 + "disk_percent": node["disk.percent"],
103 + "node": node["node"],
104 + }
105 + for node in node_allocation
106 + ]
107
83 - Returns:
84 - list: A list of dictionaries containing formatted node allocation details.
85 - """
86 - return [
87 - {
88 - "disk_used": node["disk.used"],
89 - "disk_available": node["disk.avail"],
90 - "disk_total": node["disk.total"],
91 - "disk_percent": node["disk.percent"],
92 - "node": node["node"],
93 - }
94 - for node in node_allocation
95 - ]
108
109 def format_indices_stats(indices_stats):
98 - """
99 - Format the indices stats details into a list of dictionaries. Each dictionary contains the index name, the number of documents in the index,
100 - the size of the index, and the number of shards in the index.
110 + """
111 + Format the indices stats details into a list of dictionaries. Each dictionary contains the index name, the number of documents in the index,
112 + the size of the index, and the number of shards in the index.
113
102 - Args:
103 - indices_stats: Indices stats details from Elasticsearch.
114 + Args:
115 + indices_stats: Indices stats details from Elasticsearch.
116 +
117 + Returns:
118 + list: A list of dictionaries containing formatted indices stats details.
119 + """
120 + return [
121 + {
122 + "index": index["index"],
123 + "docs_count": index["docs.count"],
124 + "store_size": index["store.size"],
125 + "replica_count": index["rep"],
126 + "health": index["health"],
127 + }
128 + for index in indices_stats
129 + ]
130
105 - Returns:
106 - list: A list of dictionaries containing formatted indices stats details.
107 - """
108 - return [
109 - {
110 - "index": index["index"],
111 - "docs_count": index["docs.count"],
112 - "store_size": index["store.size"],
113 - "replica_count": index["rep"],
114 - "health": index["health"],
115 - }
116 - for index in indices_stats
117 - ]
131
132 def format_shards(shards):
120 - """
121 - Format the shards details into a list of dictionaries. Each dictionary contains the index name, the shard number, the shard state, the shard
122 - size, and the node name.
133 + """
134 + Format the shards details into a list of dictionaries. Each dictionary contains the index name, the shard number, the shard state, the shard
135 + size, and the node name.
136
124 - Args:
125 - shards: Shards details from Elasticsearch.
137 + Args:
138 + shards: Shards details from Elasticsearch.
139 +
140 + Returns:
141 + list: A list of dictionaries containing formatted shards details.
142 + """
143 + return [
144 + {
145 + "index": shard["index"],
146 + "shard": shard["shard"],
147 + "state": shard["state"],
148 + "size": shard["store"],
149 + "node": shard["node"],
150 + }
151 + for shard in shards
152 + ]
153
127 - Returns:
128 - list: A list of dictionaries containing formatted shards details.
129 - """
130 - return [
131 - {
132 - "index": shard["index"],
133 - "shard": shard["shard"],
134 - "state": shard["state"],
135 - "size": shard["store"],
136 - "node": shard["node"],
137 - }
138 - for shard in shards
139 - ]
154
155 def collect_indices() -> Indices:
156 """
@@ -158,6 +172,7 @@ def collect_indices() -> Indices:
172 logger.error(f"Failed to collect indices: {e}")
173 return Indices(message="Failed to collect indices", success=False)
174
175 +
176 class AlertsQueryBuilder:
177 @staticmethod
178 def _get_time_range_start(timerange: str) -> str:
@@ -201,7 +216,7 @@ class AlertsQueryBuilder:
216 for field, value in matches:
217 self.query["query"]["bool"]["must"].append({"match": {field: value}})
218 return self
204 -
219 +
220 def add_match_phrase(self, matches: Iterable[Tuple[str, str]]):
221 for field, value in matches:
222 self.query["query"]["bool"]["must"].append({"match_phrase": {field: value}})
@@ -217,7 +232,8 @@ class AlertsQueryBuilder:
232
233 def build(self):
234 return self.query
220 -
235 +
236 +
237 class LogsQueryBuilder:
238 @staticmethod
239 def _get_time_range_start(timerange: str) -> str:
@@ -263,7 +279,7 @@ class LogsQueryBuilder:
279 for field, value in matches:
280 self.query["query"]["bool"]["must"].append({"match": {field: value}})
281 return self
266 -
282 +
283 def add_match_phrase(self, matches: Iterable[Tuple[str, str]]):
284 for field, value in matches:
285 self.query["query"]["bool"]["must"].append({"match_phrase": {field: value}})
backend/app/connectors/wazuh_manager/models/rules.py
+6 -3
@@ -1,8 +1,11 @@
1 import datetime
2 from typing import Optional
3
4 -from pydantic import validator, EmailStr
5 -from sqlmodel import SQLModel, Field, Relationship
4 +from pydantic import EmailStr
5 +from pydantic import validator
6 +from sqlmodel import Field
7 +from sqlmodel import Relationship
8 +from sqlmodel import SQLModel
9
10
11 class DisabledRule(SQLModel, table=True):
@@ -13,4 +16,4 @@ class DisabledRule(SQLModel, table=True):
16 reason_for_disabling: str = Field(max_length=256)
17 length_of_time: str = Field(max_length=256)
18 date_disabled: datetime.datetime = datetime.datetime.now()
16 - disabled_by: str = Field(max_length=256)
\ No newline at end of file
19 + disabled_by: str = Field(max_length=256)
backend/app/connectors/wazuh_manager/routes/rules.py
+21 -13
@@ -1,37 +1,44 @@
1 from typing import List
2 -from fastapi import APIRouter, HTTPException, Security, Depends
3 -from starlette.status import HTTP_401_UNAUTHORIZED
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 starlette.status import HTTP_401_UNAUTHORIZED
9
10 # App specific imports
11 from app.auth.routes.auth import auth_handler
8 -from app.db.db_session import session
9 -from app.connectors.wazuh_manager.schema.rules import (
10 - RuleDisable, RuleDisableResponse, RuleEnableResponse, RuleEnable, AllDisabledRuleResponse
11 -)
12 from app.connectors.wazuh_manager.models.rules import DisabledRule
13 -from app.connectors.wazuh_manager.services.rules import disable_rule, enable_rule
13 +from app.connectors.wazuh_manager.schema.rules import AllDisabledRuleResponse
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.services.rules import disable_rule
19 +from app.connectors.wazuh_manager.services.rules import enable_rule
20 +from app.db.db_session import session
21
22 NEW_LEVEL = "1"
23 wazuh_manager_router = APIRouter()
24
25 +
26 def verify_admin(user):
27 if not user.is_admin:
28 raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Unauthorized")
29
30 +
31 def query_disabled_rule(rule_id: str):
32 return session.query(DisabledRule).filter(DisabledRule.rule_id == rule_id).first()
33
34 +
35 @wazuh_manager_router.get("/rule/disabled", response_model=AllDisabledRuleResponse, description="Get all disabled rules")
36 async def get_disabled_rules(user=Depends(auth_handler.get_current_user)) -> AllDisabledRuleResponse:
37 logger.info(f"Fetching all disabled rules for user: {user.username}")
38 verify_admin(user)
39 disabled_rules = session.query(DisabledRule).all()
30 - return AllDisabledRuleResponse(
31 - disabled_rules=disabled_rules,
32 - success=True,
33 - message="Successfully fetched all disabled rules"
34 - )
40 + return AllDisabledRuleResponse(disabled_rules=disabled_rules, success=True, message="Successfully fetched all disabled rules")
41 +
42
43 @wazuh_manager_router.post("/rule/disable", response_model=RuleDisableResponse, description="Disable a Wazuh Rule")
44 async def disable_wazuh_rule(rule: RuleDisable, user=Depends(auth_handler.get_current_user)) -> RuleDisableResponse:
@@ -49,7 +56,7 @@ async def disable_wazuh_rule(rule: RuleDisable, user=Depends(auth_handler.get_cu
56 new_level=NEW_LEVEL,
57 reason_for_disabling=rule.reason_for_disabling,
58 length_of_time=rule.length_of_time,
52 - disabled_by=user.username
59 + disabled_by=user.username,
60 )
61 session.add(new_disabled_rule)
62 session.commit()
@@ -57,6 +64,7 @@ async def disable_wazuh_rule(rule: RuleDisable, user=Depends(auth_handler.get_cu
64 else:
65 raise HTTPException(status_code=404, detail="Was not able to disable rule")
66
67 +
68 @wazuh_manager_router.post("/rule/enable", response_model=RuleEnableResponse, description="Enable a Wazuh Rule")
69 async def enable_wazuh_rule(rule: RuleEnable, user=Depends(auth_handler.get_current_user)) -> RuleEnableResponse:
70 logger.info(f"Enabling rule for user: {user.username}")
backend/app/connectors/wazuh_manager/schema/rules.py
+13 -3
@@ -1,26 +1,35 @@
1 -from pydantic import BaseModel
2 -from typing import Optional, List, Dict, Any
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 from typing import Union
6
7 +from pydantic import BaseModel
8 +
9 +
10 class RuleDisable(BaseModel):
11 rule_id: str
12 reason_for_disabling: str
13 length_of_time: str
14
15 +
16 class RuleDisableResponse(BaseModel):
17 previous_level: Optional[str]
18 message: str
19 success: bool
20
21 +
22 class RuleEnable(BaseModel):
23 rule_id: str
24 reason_for_enabling: str
25
26 +
27 class RuleEnableResponse(BaseModel):
28 new_level: Optional[str]
29 message: str
30 success: bool
31
32 +
33 class AllDisabledRule(BaseModel):
34 rule_id: str
35 previous_level: str
@@ -29,7 +38,8 @@ class AllDisabledRule(BaseModel):
38 length_of_time: str
39 disabled_by: str
40
41 +
42 class AllDisabledRuleResponse(BaseModel):
43 disabled_rules: List[AllDisabledRule]
44 success: bool
35 - message: str
\ No newline at end of file
45 + message: str
backend/app/connectors/wazuh_manager/services/rules.py
+15 -8
@@ -1,14 +1,21 @@
1 -from typing import Dict, List, Optional, Any, Tuple, Union
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 +from typing import Tuple
6 +from typing import Union
7 +
8 import requests
9 import xmltodict
10 from loguru import logger
11
6 -from app.connectors.wazuh_manager.schema.rules import (
7 - RuleDisable, RuleDisableResponse, RuleEnable, RuleEnableResponse
8 -)
9 -from app.connectors.wazuh_manager.utils.universal import (
10 - send_get_request, send_put_request, restart_service
11 -)
12 +from app.connectors.wazuh_manager.schema.rules import RuleDisable
13 +from app.connectors.wazuh_manager.schema.rules import RuleDisableResponse
14 +from app.connectors.wazuh_manager.schema.rules import RuleEnable
15 +from app.connectors.wazuh_manager.schema.rules import RuleEnableResponse
16 +from app.connectors.wazuh_manager.utils.universal import restart_service
17 +from app.connectors.wazuh_manager.utils.universal import send_get_request
18 +from app.connectors.wazuh_manager.utils.universal import send_put_request
19
20
21 def fetch_filename(rule_id: str) -> str:
@@ -76,7 +83,7 @@ def process_rule(rule, rule_action_func, ResponseModel):
83 return ResponseModel(
84 previous_level=previous_level,
85 success=True,
79 - message=f"Rule {rule.rule_id} successfully processed in file {filename}."
86 + message=f"Rule {rule.rule_id} successfully processed in file {filename}.",
87 )
88
89
backend/app/connectors/wazuh_manager/utils/universal.py
+33 -17
@@ -1,11 +1,17 @@
1 -from typing import Dict, Any, Optional
2 -from sqlmodel import Session, select
3 -from app.connectors.models import Connectors
4 -from loguru import logger
5 -from app.db.db_session import engine
1 +from typing import Any
2 +from typing import Dict
3 +from typing import Optional
4 +
5 import requests
6 +from loguru import logger
7 +from sqlmodel import Session
8 +from sqlmodel import select
9 +
10 +from app.connectors.models import Connectors
11 from app.connectors.schema import ConnectorResponse
12 from app.connectors.utils import get_connector_info_from_db
13 +from app.db.db_session import engine
14 +
15
16 def verify_wazuh_manager_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
17 """
@@ -15,7 +21,7 @@ def verify_wazuh_manager_credentials(attributes: Dict[str, Any]) -> Dict[str, An
21 dict: A dictionary containing 'connectionSuccessful' status and 'authToken' if the connection is successful.
22 """
23 logger.info(f"Verifying the wazuh-manager connection to {attributes['connector_url']}")
18 -
24 +
25 try:
26 wazuh_auth_token = requests.get(
27 f"{attributes['connector_url']}/security/user/authenticate",
@@ -25,19 +31,20 @@ def verify_wazuh_manager_credentials(attributes: Dict[str, Any]) -> Dict[str, An
31 ),
32 verify=False,
33 )
28 -
34 +
35 if wazuh_auth_token.status_code == 200:
30 - logger.debug("Wazuh Authentication Token successful")
36 + logger.debug("Wazuh Authentication Token successful")
37 return {"connectionSuccessful": True, "message": "Wazuh Manager authentication successful"}
38 else:
39 logger.error(f"Connection to {attributes['connector_url']} failed with error: {wazuh_auth_token.text}")
34 -
40 +
41 return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed"}
42 except Exception as e:
43 logger.error(f"Connection to {attributes['connector_url']} failed with error: {e}")
38 -
44 +
45 return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error."}
46
47 +
48 def verify_wazuh_manager_connection(connector_name: str) -> str:
49 """
50 Returns the authentication token for the Wazuh manager service.
@@ -52,6 +59,7 @@ def verify_wazuh_manager_connection(connector_name: str) -> str:
59 return None
60 return verify_wazuh_manager_credentials(attributes)
61
62 +
63 def create_wazuh_manager_client(connector_name: str) -> str:
64 """
65 Returns the authentication token for the Wazuh manager service.
@@ -88,7 +96,8 @@ def create_wazuh_manager_client(connector_name: str) -> str:
96 logger.error(f"Connection to {attributes['connector_url']} failed with error: {e}")
97
98 return None
91 -
99 +
100 +
101 def send_get_request(endpoint: str, params: Optional[Dict[str, Any]] = None, connector_name: str = "Wazuh-Manager") -> Dict[str, Any]:
102 """
103 Sends a GET request to the Wazuh Manager service.
@@ -119,7 +128,8 @@ def send_get_request(endpoint: str, params: Optional[Dict[str, Any]] = None, con
128 except Exception as e:
129 logger.error(f"Failed to send GET request to {endpoint} with error: {e}")
130 return {"success": False, "message": f"Failed to send GET request to {endpoint} with error: {e}"}
122 -
131 +
132 +
133 def send_post_request(endpoint: str, data: Dict[str, Any], connector_name: str = "Wazuh-Manager") -> Dict[str, Any]:
134 """
135 Sends a POST request to the Wazuh Manager service.
@@ -150,8 +160,14 @@ def send_post_request(endpoint: str, data: Dict[str, Any], connector_name: str =
160 except Exception as e:
161 logger.error(f"Failed to send POST request to {endpoint} with error: {e}")
162 return {"success": False, "message": f"Failed to send POST request to {endpoint} with error: {e}"}
153 -
154 -def send_put_request(endpoint: str, data: Optional[Dict[str, Any]], params: Optional[Dict[str, str]] = None, connector_name: str = "Wazuh-Manager") -> Dict[str, Any]:
163 +
164 +
165 +def send_put_request(
166 + endpoint: str,
167 + data: Optional[Dict[str, Any]],
168 + params: Optional[Dict[str, str]] = None,
169 + connector_name: str = "Wazuh-Manager",
170 +) -> Dict[str, Any]:
171 """
172 Sends a PUT request to the Wazuh Manager service.
173
@@ -182,7 +198,8 @@ def send_put_request(endpoint: str, data: Optional[Dict[str, Any]], params: Opti
198 except Exception as e:
199 logger.error(f"Failed to send PUT request to {endpoint} with error: {e}")
200 return {"success": False, "message": f"Failed to send PUT request to {endpoint} with error: {e}"}
185 -
201 +
202 +
203 def send_delete_request(endpoint: str, params: Optional[Dict[str, Any]] = None, connector_name: str = "Wazuh-Manager") -> Dict[str, Any]:
204 """
205 Sends a DELETE request to the Wazuh Manager service.
@@ -213,7 +230,7 @@ def send_delete_request(endpoint: str, params: Optional[Dict[str, Any]] = None,
230 except Exception as e:
231 logger.error(f"Failed to send DELETE request to {endpoint} with error: {e}")
232 return {"success": False, "message": f"Failed to send DELETE request to {endpoint} with error: {e}"}
216 -
233 +
234
235 def restart_service(connector_name: str = "Wazuh-Manager") -> Dict[str, Any]:
236 """
@@ -239,4 +256,3 @@ def restart_service(connector_name: str = "Wazuh-Manager") -> Dict[str, Any]:
256 except Exception as e:
257 logger.error(f"Failed to restart Wazuh Manager service with error: {e}")
258 return {"success": False, "message": f"Failed to restart Wazuh Manager service with error: {e}"}
242 -
\ No newline at end of file
backend/app/customers/routes/customers.py
+99 -23
@@ -1,27 +1,56 @@
1 -from typing import List, Dict, Any, Optional, Type
2 -from fastapi import APIRouter, HTTPException, Security, Depends, Query
3 -from starlette.status import HTTP_401_UNAUTHORIZED
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 +from typing import Type
6 +
7 +from fastapi import APIRouter
8 +from fastapi import Depends
9 +from fastapi import HTTPException
10 +from fastapi import Query
11 +from fastapi import Security
12 from loguru import logger
13 +from starlette.status import HTTP_401_UNAUTHORIZED
14
15 # App specific imports
16 from app.auth.routes.auth import auth_handler
17 +from app.customers.schema.customers import AgentModel
18 +from app.customers.schema.customers import AgentsResponse
19 +from app.customers.schema.customers import CustomerFullResponse
20 +from app.customers.schema.customers import CustomerMetaRequestBody
21 +from app.customers.schema.customers import CustomerMetaResponse
22 +from app.customers.schema.customers import CustomerRequestBody
23 +from app.customers.schema.customers import CustomerResponse
24 +from app.customers.schema.customers import CustomersResponse
25 from app.db.db_session import session
9 -from app.db.universal_models import Customers, CustomersMeta, Agents
10 -from app.customers.schema.customers import CustomerRequestBody, CustomerResponse, CustomersResponse, CustomerMetaRequestBody, CustomerMetaResponse, CustomerFullResponse, AgentModel, AgentsResponse
11 -from app.healthchecks.agents.schema.agents import AgentHealthCheckResponse, AgentModel, TimeCriteriaModel, HostLogsSearchBody, HostLogsSearchResponse
12 -from app.healthchecks.agents.services.agents import wazuh_agents_healthcheck, wazuh_agent_healthcheck, velociraptor_agents_healthcheck, velociraptor_agent_healthcheck, host_logs
26 +from app.db.universal_models import Agents
27 +from app.db.universal_models import Customers
28 +from app.db.universal_models import CustomersMeta
29 +from app.healthchecks.agents.schema.agents import AgentHealthCheckResponse
30 +from app.healthchecks.agents.schema.agents import AgentModel
31 +from app.healthchecks.agents.schema.agents import HostLogsSearchBody
32 +from app.healthchecks.agents.schema.agents import HostLogsSearchResponse
33 +from app.healthchecks.agents.schema.agents import TimeCriteriaModel
34 +from app.healthchecks.agents.services.agents import host_logs
35 +from app.healthchecks.agents.services.agents import velociraptor_agent_healthcheck
36 +from app.healthchecks.agents.services.agents import velociraptor_agents_healthcheck
37 +from app.healthchecks.agents.services.agents import wazuh_agent_healthcheck
38 +from app.healthchecks.agents.services.agents import wazuh_agents_healthcheck
39
40 customers_router = APIRouter()
41
42 +
43 def verify_admin(user):
44 if not user.is_admin:
45 raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Unauthorized")
19 -
46 +
47 +
48 def verify_unique_customer_code(customer: CustomerRequestBody):
49 existing_customer = session.query(Customers).filter(Customers.customer_code == customer.customer_code).first()
50 if existing_customer:
51 raise HTTPException(status_code=400, detail="Customer with this customer_code already exists")
52
53 +
54 @customers_router.post("", response_model=CustomerResponse, description="Create a new customer")
55 async def create_customer(customer: CustomerRequestBody) -> CustomerResponse:
56 verify_unique_customer_code(customer)
@@ -31,6 +60,7 @@ async def create_customer(customer: CustomerRequestBody) -> CustomerResponse:
60 session.commit()
61 return CustomerResponse(customer=customer, success=True, message="Customer created successfully")
62
63 +
64 @customers_router.get("", response_model=CustomersResponse, description="Get all customers")
65 async def get_customers() -> CustomersResponse:
66 logger.info(f"Fetching all customers")
@@ -39,13 +69,19 @@ async def get_customers() -> CustomersResponse:
69 customers = [CustomerRequestBody.parse_obj(customer.__dict__) for customer in customers]
70 return CustomersResponse(customers=customers, success=True, message="Customers fetched successfully")
71
72 +
73 @customers_router.get("/{customer_code}", response_model=CustomerResponse, description="Get customer by customer_code")
74 async def get_customer(customer_code: str) -> CustomerResponse:
75 logger.info(f"Fetching customer with customer_code: {customer_code}")
76 customer = session.query(Customers).filter(Customers.customer_code == customer_code).first()
77 if not customer:
78 raise HTTPException(status_code=404, detail=f"Customer with customer_code {customer_code} not found")
48 - return CustomerResponse(customer=CustomerRequestBody.parse_obj(customer.__dict__), success=True, message="Customer fetched successfully")
79 + return CustomerResponse(
80 + customer=CustomerRequestBody.parse_obj(customer.__dict__),
81 + success=True,
82 + message="Customer fetched successfully",
83 + )
84 +
85
86 @customers_router.put("/{customer_code}", response_model=CustomerResponse, description="Update customer by customer_code")
87 async def update_customer(customer_code: str, customer: CustomerRequestBody) -> CustomerResponse:
@@ -55,7 +91,12 @@ async def update_customer(customer_code: str, customer: CustomerRequestBody) ->
91 raise HTTPException(status_code=404, detail=f"Customer with customer_code {customer_code} not found")
92 existing_customer.update_from_model(customer)
93 session.commit()
58 - return CustomerResponse(customer=CustomerRequestBody.parse_obj(customer.__dict__), success=True, message="Customer updated successfully")
94 + return CustomerResponse(
95 + customer=CustomerRequestBody.parse_obj(customer.__dict__),
96 + success=True,
97 + message="Customer updated successfully",
98 + )
99 +
100
101 # ! TODO: Fix delete customer
102 # @customers_router.delete("/{customer_code}", response_model=CustomerResponse, description="Delete customer by customer_code")
@@ -68,6 +109,7 @@ async def update_customer(customer_code: str, customer: CustomerRequestBody) ->
109 # session.commit()
110 # return CustomerResponse(customer=CustomerRequestBody.parse_obj(existing_customer.__dict__), success=True, message="Customer deleted successfully")
111
112 +
113 @customers_router.post("/{customer_code}/meta", response_model=CustomerMetaResponse, description="Add new customer meta")
114 async def add_customer_meta(customer_code: str, customer_meta: CustomerMetaRequestBody) -> CustomerMetaResponse:
115 logger.info(f"Adding new customer meta: {customer_meta}")
@@ -83,13 +125,19 @@ async def add_customer_meta(customer_code: str, customer_meta: CustomerMetaReque
125 session.commit()
126 return CustomerMetaResponse(customer_meta=customer_meta, success=True, message="Customer meta added successfully")
127
128 +
129 @customers_router.get("/{customer_code}/meta", response_model=CustomerMetaResponse, description="Get customer meta by customer_code")
130 async def get_customer_meta(customer_code: str) -> CustomerMetaResponse:
131 logger.info(f"Fetching customer meta with customer_code: {customer_code}")
132 customer_meta = session.query(CustomersMeta).filter(CustomersMeta.customer_code == customer_code).first()
133 if not customer_meta:
134 raise HTTPException(status_code=404, detail=f"Customer meta with customer_code {customer_code} not found")
92 - return CustomerMetaResponse(customer_meta=CustomerMetaRequestBody.parse_obj(customer_meta.__dict__), success=True, message="Customer meta fetched successfully")
135 + return CustomerMetaResponse(
136 + customer_meta=CustomerMetaRequestBody.parse_obj(customer_meta.__dict__),
137 + success=True,
138 + message="Customer meta fetched successfully",
139 + )
140 +
141
142 @customers_router.put("/{customer_code}/meta", response_model=CustomerMetaResponse, description="Update customer meta by customer_code")
143 async def update_customer_meta(customer_code: str, customer_meta: CustomerMetaRequestBody) -> CustomerMetaResponse:
@@ -105,7 +153,11 @@ async def update_customer_meta(customer_code: str, customer_meta: CustomerMetaRe
153 # Commit the changes to the database
154 session.commit()
155
108 - return CustomerMetaResponse(customer_meta=CustomerMetaRequestBody.parse_obj(customer_meta.__dict__), success=True, message="Customer meta updated successfully")
156 + return CustomerMetaResponse(
157 + customer_meta=CustomerMetaRequestBody.parse_obj(customer_meta.__dict__),
158 + success=True,
159 + message="Customer meta updated successfully",
160 + )
161
162
163 # ! TODO: Fix delete customer meta
@@ -119,7 +171,12 @@ async def update_customer_meta(customer_code: str, customer_meta: CustomerMetaRe
171 # session.commit()
172 # return CustomerMetaResponse(customer_meta=CustomerMetaRequestBody.parse_obj(existing_customer_meta.__dict__), success=True, message="Customer meta deleted successfully")
173
122 -@customers_router.get("/{customer_code}/full", response_model=CustomerFullResponse, description="Get customer and customer meta by customer_code")
174 +
175 +@customers_router.get(
176 + "/{customer_code}/full",
177 + response_model=CustomerFullResponse,
178 + description="Get customer and customer meta by customer_code",
179 +)
180 async def get_customer_full(customer_code: str) -> CustomerFullResponse:
181 logger.info(f"Fetching customer and customer meta with customer_code: {customer_code}")
182 customer = session.query(Customers).filter(Customers.customer_code == customer_code).first()
@@ -128,7 +185,12 @@ async def get_customer_full(customer_code: str) -> CustomerFullResponse:
185 customer_meta = session.query(CustomersMeta).filter(CustomersMeta.customer_code == customer_code).first()
186 if not customer_meta:
187 raise HTTPException(status_code=404, detail=f"Customer meta with customer_code {customer_code} not found")
131 - return CustomerFullResponse(customer=CustomerRequestBody.parse_obj(customer.__dict__), customer_meta=CustomerMetaRequestBody.parse_obj(customer_meta.__dict__), success=True, message="Customer and customer meta fetched successfully")
188 + return CustomerFullResponse(
189 + customer=CustomerRequestBody.parse_obj(customer.__dict__),
190 + customer_meta=CustomerMetaRequestBody.parse_obj(customer_meta.__dict__),
191 + success=True,
192 + message="Customer and customer meta fetched successfully",
193 + )
194
195
196 # Get Agents for the given customer_code
@@ -145,10 +207,17 @@ async def get_agents(customer_code: str) -> AgentsResponse:
207
208
209 # Retrieve the agents for the given customer_code then perform a healthcheck on them
148 -@customers_router.get("/{customer_code}/agents/healthcheck/wazuh", response_model=AgentHealthCheckResponse, description="Get agents healthcheck for the given customer_code")
149 -async def get_agents_healthcheck(customer_code: str, minutes: int = Query(60, description="Number of minutes within which the agent should have been last seen to be considered healthy."),
150 - hours: int = Query(0, description="Number of hours within which the agent should have been last seen to be considered healthy."),
151 - days: int = Query(0, description="Number of days within which the agent should have been last seen to be considered healthy.")) -> AgentHealthCheckResponse:
210 +@customers_router.get(
211 + "/{customer_code}/agents/healthcheck/wazuh",
212 + response_model=AgentHealthCheckResponse,
213 + description="Get agents healthcheck for the given customer_code",
214 +)
215 +async def get_agents_healthcheck(
216 + customer_code: str,
217 + minutes: int = Query(60, description="Number of minutes within which the agent should have been last seen to be considered healthy."),
218 + hours: int = Query(0, description="Number of hours within which the agent should have been last seen to be considered healthy."),
219 + days: int = Query(0, description="Number of days within which the agent should have been last seen to be considered healthy."),
220 +) -> AgentHealthCheckResponse:
221 logger.info(f"Fetching agents for customer_code: {customer_code}")
222 customer = session.query(Customers).filter(Customers.customer_code == customer_code).first()
223 if not customer:
@@ -159,11 +228,19 @@ async def get_agents_healthcheck(customer_code: str, minutes: int = Query(60, de
228 time_criteria = TimeCriteriaModel(minutes=minutes, hours=hours, days=days)
229 return wazuh_agents_healthcheck(agents, time_criteria)
230
231 +
232 # Retrieve the agents for the given customer_code then perform a healthcheck on them
163 -@customers_router.get("/{customer_code}/agents/healthcheck/velociraptor", response_model=AgentHealthCheckResponse, description="Get agents healthcheck for the given customer_code")
164 -async def get_agents_healthcheck(customer_code: str, minutes: int = Query(60, description="Number of minutes within which the agent should have been last seen to be considered healthy."),
165 - hours: int = Query(0, description="Number of hours within which the agent should have been last seen to be considered healthy."),
166 - days: int = Query(0, description="Number of days within which the agent should have been last seen to be considered healthy.")) -> AgentHealthCheckResponse:
233 +@customers_router.get(
234 + "/{customer_code}/agents/healthcheck/velociraptor",
235 + response_model=AgentHealthCheckResponse,
236 + description="Get agents healthcheck for the given customer_code",
237 +)
238 +async def get_agents_healthcheck(
239 + customer_code: str,
240 + minutes: int = Query(60, description="Number of minutes within which the agent should have been last seen to be considered healthy."),
241 + hours: int = Query(0, description="Number of hours within which the agent should have been last seen to be considered healthy."),
242 + days: int = Query(0, description="Number of days within which the agent should have been last seen to be considered healthy."),
243 +) -> AgentHealthCheckResponse:
244 logger.info(f"Fetching agents for customer_code: {customer_code}")
245 customer = session.query(Customers).filter(Customers.customer_code == customer_code).first()
246 if not customer:
@@ -173,4 +250,3 @@ async def get_agents_healthcheck(customer_code: str, minutes: int = Query(60, de
250 agents = [AgentModel.parse_obj(agent.__dict__) for agent in agents]
251 time_criteria = TimeCriteriaModel(minutes=minutes, hours=hours, days=days)
252 return velociraptor_agents_healthcheck(agents, time_criteria)
176 -
backend/app/customers/schema/customers.py
+18 -8
@@ -1,13 +1,17 @@
1 -from pydantic import BaseModel, Field
1 from datetime import datetime
3 -from typing import Optional, List
2 +from typing import List
3 +from typing import Optional
4 +
5 +from pydantic import BaseModel
6 +from pydantic import Field
7 +
8
9 class CustomerRequestBody(BaseModel):
10 customer_code: str = Field(..., description="Unique code for the customer")
11 customer_name: str = Field(..., description="Name of the customer")
12 contact_last_name: str = Field(..., description="Last name of the contact person")
13 contact_first_name: str = Field(..., description="First name of the contact person")
10 -
14 +
15 parent_customer_code: Optional[str] = Field(None, description="Code for the parent customer")
16 phone: Optional[str] = Field(None, description="Phone number")
17 address_line1: Optional[str] = Field(None, description="First line of the address")
@@ -34,20 +38,23 @@ class CustomerRequestBody(BaseModel):
38 "postal_code": "12345",
39 "country": "USA",
40 "customer_type": "Enterprise",
37 - "logo_file": "logo.png"
38 - }
41 + "logo_file": "logo.png",
42 + },
43 }
44
45 +
46 class CustomerResponse(BaseModel):
47 customer: Optional[CustomerRequestBody]
48 success: bool
49 message: str
50
51 +
52 class CustomersResponse(BaseModel):
53 customers: list[CustomerRequestBody]
54 success: bool
55 message: str
56
57 +
58 ############# Customer Meta
59 class CustomerMetaRequestBody(BaseModel):
60 customer_meta_graylog_index: str = Field(..., description="Graylog index for the customer")
@@ -69,15 +76,17 @@ class CustomerMetaRequestBody(BaseModel):
76 "customer_meta_wazuh_group": "wazuh_group",
77 "index_retention": 30,
78 "wazuh_registration_port": 1514,
72 - "wazuh_log_ingestion_port": 1515
73 - }
79 + "wazuh_log_ingestion_port": 1515,
80 + },
81 }
82
83 +
84 class CustomerMetaResponse(BaseModel):
85 customer_meta: Optional[CustomerMetaRequestBody]
86 success: bool
87 message: str
88
89 +
90 ############# Customer Full Response
91 class CustomerFullResponse(BaseModel):
92 customer: Optional[CustomerRequestBody]
@@ -105,7 +114,8 @@ class AgentModel(BaseModel):
114 class Config:
115 orm_mode = True
116
117 +
118 class AgentsResponse(BaseModel):
119 agents: Optional[List[AgentModel]] = Field([], description="List of agents")
120 success: bool
111 - message: str
\ No newline at end of file
121 + message: str
backend/app/db/all_models.py
+2 -2
@@ -1,6 +1,6 @@
1 # all_models.py
2 -from app.connectors.models import Connectors
2 from app.auth.models.users import User
4 -from app.connectors.wazuh_manager.models.rules import DisabledRule
3 +from app.connectors.models import Connectors
4 from app.connectors.sublime.models.alerts import SublimeAlerts
5 +from app.connectors.wazuh_manager.models.rules import DisabledRule
6 from app.db.universal_models import *
backend/app/db/db_populate.py
+103 -16
@@ -1,34 +1,121 @@
1 -from app.connectors.models import Connectors
1 +from datetime import datetime
2 +
3 from loguru import logger
4 from sqlmodel import Session
4 -from datetime import datetime
5 +
6 +from app.connectors.models import Connectors
7 +
8
9 def add_connectors_if_not_exist(session: Session):
10 # List of connectors to add
11 connector_list = [
9 - {"connector_name": "Wazuh-Indexer", "connector_type": "4.4.1", "connector_url": "https://ashwix01.socfortress.local:9200", "connector_username": "admin", "connector_password": "hmx7KPy15XPhJkgjlFrVgrWZ+Aid6QNm", "connector_api_key": None},
10 - {"connector_name": "Wazuh-Manager", "connector_type": "4.4.1", "connector_url": "https://ashwzhma.socfortress.local:55000", "connector_username": "wazuh-wui", "connector_password": "wazuh-wui", "connector_api_key": None},
11 - {"connector_name": "Graylog", "connector_type": "5.0.7", "connector_url": "http://ashgrl02.socfortress.local:9000", "connector_username": "socfortress_graylog_manager", "connector_password": "R{2PvE5TQkU7[xS$pX>fw>`y", "connector_api_key": None},
12 - {"connector_name": "Shuffle", "connector_type": "1.1.0", "connector_url": "https://ASHDKR02.socfortress.local:3443", "connector_username": "sting", "connector_password": "string", "connector_api_key": "bc5d1e18-6230-40f0-b032-6ed898c307c5"},
13 - {"connector_name": "DFIR-IRIS", "connector_type": "2.0", "connector_url": "https://ashirs01.socfortress.local", "connector_username": None, "connector_password": None, "connector_api_key": "I3Hwvkpvdk8Z0XRFlyGm4WXGw8jksnEzvKNoD9BobtSQ2AgWmdo_p-pfmJCg_ev2cm8I-zgWzAfya3jLBWZ6qw"},
14 - {"connector_name": "Velociraptor", "connector_type": "0.6.8", "connector_url": "https://ashvlo01.socfortress.local:8001", "connector_username": None, "connector_password": None, "connector_api_key": "C:\\Users\\walto\\Desktop\\GitHub\\CoPilot\\backend\\app\\file-store\\api.config.yaml"},
15 - {"connector_name": "RabbitMQ", "connector_type": "3", "connector_url": "ashdkr02.socfortress.local:5672", "connector_username": "guest", "connector_password": "guest", "connector_api_key": None},
16 - {"connector_name": "Sublime", "connector_type": "3", "connector_url": "http://ashdkr02.socfortress.local:8000", "connector_username": None, "connector_password": None, "connector_api_key": "7653trxhakxn4wxdh8bbatbvu97hm8fopos7wztzjrwfd12gf5i2kyebhvke9rt4"},
17 - {"connector_name": "InfluxDB", "connector_type": "3", "connector_url": "http://ashdkr02.socfortress.local:8086", "connector_username": "SOCFortress", "connector_password": None, "connector_api_key": "gOLoFKucQXXd5d1rDx59YYktIz6OfrHIe4jRowJKZ8iB4IcZES8rOhRPaDEejEkahch8Ze2FiMzZxbQ9ZV8K6g=="},
18 - {"connector_name": "AskSocfortress", "connector_type": "3", "connector_url": "https://api.socfortress.co/rule", "connector_username": None, "connector_password": None, "connector_api_key": "CkKmw1B9NM1hG669tC4sTazLm1HlRfSXVvMZkxa9"},
19 - {"connector_name": "SocfortressThreatIntel", "connector_type": "3", "connector_url": "https://intel.socfortress.co/search", "connector_username": None, "connector_password": None, "connector_api_key": "ozH1jHp1zmacCePYrAZmxarJCGptcMth93a86Jq8"},
20 - {"connector_name": "Cortex", "connector_type": "3", "connector_url": "http://ashvlo01.socfortress.local:9001", "connector_username": None, "connector_password": None, "connector_api_key": "+k/DvVYMEYURbc8sUdXA5/hW9VhJZV3v"}
12 + {
13 + "connector_name": "Wazuh-Indexer",
14 + "connector_type": "4.4.1",
15 + "connector_url": "https://ashwix01.socfortress.local:9200",
16 + "connector_username": "admin",
17 + "connector_password": "hmx7KPy15XPhJkgjlFrVgrWZ+Aid6QNm",
18 + "connector_api_key": None,
19 + },
20 + {
21 + "connector_name": "Wazuh-Manager",
22 + "connector_type": "4.4.1",
23 + "connector_url": "https://ashwzhma.socfortress.local:55000",
24 + "connector_username": "wazuh-wui",
25 + "connector_password": "wazuh-wui",
26 + "connector_api_key": None,
27 + },
28 + {
29 + "connector_name": "Graylog",
30 + "connector_type": "5.0.7",
31 + "connector_url": "http://ashgrl02.socfortress.local:9000",
32 + "connector_username": "socfortress_graylog_manager",
33 + "connector_password": "R{2PvE5TQkU7[xS$pX>fw>`y",
34 + "connector_api_key": None,
35 + },
36 + {
37 + "connector_name": "Shuffle",
38 + "connector_type": "1.1.0",
39 + "connector_url": "https://ASHDKR02.socfortress.local:3443",
40 + "connector_username": "sting",
41 + "connector_password": "string",
42 + "connector_api_key": "bc5d1e18-6230-40f0-b032-6ed898c307c5",
43 + },
44 + {
45 + "connector_name": "DFIR-IRIS",
46 + "connector_type": "2.0",
47 + "connector_url": "https://ashirs01.socfortress.local",
48 + "connector_username": None,
49 + "connector_password": None,
50 + "connector_api_key": "I3Hwvkpvdk8Z0XRFlyGm4WXGw8jksnEzvKNoD9BobtSQ2AgWmdo_p-pfmJCg_ev2cm8I-zgWzAfya3jLBWZ6qw",
51 + },
52 + {
53 + "connector_name": "Velociraptor",
54 + "connector_type": "0.6.8",
55 + "connector_url": "https://ashvlo01.socfortress.local:8001",
56 + "connector_username": None,
57 + "connector_password": None,
58 + "connector_api_key": "C:\\Users\\walto\\Desktop\\GitHub\\CoPilot\\backend\\app\\file-store\\api.config.yaml",
59 + },
60 + {
61 + "connector_name": "RabbitMQ",
62 + "connector_type": "3",
63 + "connector_url": "ashdkr02.socfortress.local:5672",
64 + "connector_username": "guest",
65 + "connector_password": "guest",
66 + "connector_api_key": None,
67 + },
68 + {
69 + "connector_name": "Sublime",
70 + "connector_type": "3",
71 + "connector_url": "http://ashdkr02.socfortress.local:8000",
72 + "connector_username": None,
73 + "connector_password": None,
74 + "connector_api_key": "7653trxhakxn4wxdh8bbatbvu97hm8fopos7wztzjrwfd12gf5i2kyebhvke9rt4",
75 + },
76 + {
77 + "connector_name": "InfluxDB",
78 + "connector_type": "3",
79 + "connector_url": "http://ashdkr02.socfortress.local:8086",
80 + "connector_username": "SOCFortress",
81 + "connector_password": None,
82 + "connector_api_key": "gOLoFKucQXXd5d1rDx59YYktIz6OfrHIe4jRowJKZ8iB4IcZES8rOhRPaDEejEkahch8Ze2FiMzZxbQ9ZV8K6g==",
83 + },
84 + {
85 + "connector_name": "AskSocfortress",
86 + "connector_type": "3",
87 + "connector_url": "https://api.socfortress.co/rule",
88 + "connector_username": None,
89 + "connector_password": None,
90 + "connector_api_key": "CkKmw1B9NM1hG669tC4sTazLm1HlRfSXVvMZkxa9",
91 + },
92 + {
93 + "connector_name": "SocfortressThreatIntel",
94 + "connector_type": "3",
95 + "connector_url": "https://intel.socfortress.co/search",
96 + "connector_username": None,
97 + "connector_password": None,
98 + "connector_api_key": "ozH1jHp1zmacCePYrAZmxarJCGptcMth93a86Jq8",
99 + },
100 + {
101 + "connector_name": "Cortex",
102 + "connector_type": "3",
103 + "connector_url": "http://ashvlo01.socfortress.local:9001",
104 + "connector_username": None,
105 + "connector_password": None,
106 + "connector_api_key": "+k/DvVYMEYURbc8sUdXA5/hW9VhJZV3v",
107 + },
108 ]
109
110 for connector_data in connector_list:
111 # Check if connector already exists in the database
112 existing_connector = session.query(Connectors).filter_by(connector_name=connector_data["connector_name"]).first()
26 -
113 +
114 if existing_connector is None:
115 # If connector does not exist, create new connector entry
116 new_connector = Connectors(**connector_data)
117 session.add(new_connector)
118 logger.info(f"Added new connector: {connector_data['connector_name']}")
32 -
119 +
120 # Commit the changes if any new connectors were added
121 session.commit()
backend/app/db/db_session.py
+3 -2
@@ -1,6 +1,7 @@
1 -from sqlmodel import create_engine
1 from sqlmodel import Session
2 +from sqlmodel import create_engine
3 +
4 from settings import SQLALCHEMY_DATABASE_URI
5
6 engine = create_engine(SQLALCHEMY_DATABASE_URI)
6 -session = Session(bind=engine)
\ No newline at end of file
7 +session = Session(bind=engine)
backend/app/db/db_setup.py
+8 -6
@@ -1,25 +1,27 @@
1 -from sqlmodel import SQLModel, Session
1 +from loguru import logger
2 from sqlalchemy import inspect
3 -# from app.connectors.models import Connectors
3 +from sqlmodel import Session
4 +from sqlmodel import SQLModel
5 +
6 +# from app.connectors.models import Connectors
7 # from app.auth.models.users import User
8 # from app.connectors.wazuh_manager.models.rules import DisabledRule
9 # from app.agents.models.agents import Agents
10 # from app.customers.models.customers import Customers
11 # from app.connectors.sublime.models.alerts import SublimeAlerts
12 from app.db.all_models import *
10 -from loguru import logger
13 from app.db.db_populate import add_connectors_if_not_exist
14
15 +
16 def create_tables(engine):
17 logger.info("Creating tables")
15 -
18 +
19 # Create an inspector object based on the engine
20 inspector = inspect(engine)
18 -
21 +
22 # Get the names of all tables in the database
23 existing_tables = inspector.get_table_names()
24
22 -
25 # Loop through all your models (tables)
26 for table in SQLModel.metadata.sorted_tables:
27 if table.name not in existing_tables:
backend/app/db/universal_models.py
+13 -7
@@ -1,6 +1,10 @@
1 -from sqlmodel import SQLModel, Field, Column, Relationship
2 -from typing import Optional
1 from datetime import datetime
2 +from typing import Optional
3 +
4 +from sqlmodel import Column
5 +from sqlmodel import Field
6 +from sqlmodel import Relationship
7 +from sqlmodel import SQLModel
8
9
10 class Customers(SQLModel, table=True):
@@ -20,7 +24,7 @@ class Customers(SQLModel, table=True):
24 customer_type: Optional[str] = Field(max_length=50)
25 logo_file: Optional[str] = Field(max_length=64)
26 created_at: datetime = Field(default=datetime.utcnow())
23 -
27 +
28 agents: list["Agents"] = Relationship(back_populates="customer")
29 meta: Optional["CustomersMeta"] = Relationship(back_populates="customer")
30
@@ -40,6 +44,7 @@ class Customers(SQLModel, table=True):
44 self.customer_type = customer.customer_type
45 self.logo_file = customer.logo_file
46
47 +
48 class CustomersMeta(SQLModel, table=True):
49 id: Optional[int] = Field(primary_key=True)
50 customer_code: str = Field(foreign_key="customers.customer_code", nullable=False)
@@ -57,9 +62,9 @@ class CustomersMeta(SQLModel, table=True):
62 customer: Optional["Customers"] = Relationship(back_populates="meta")
63
64 def update_from_model(self, customer_meta):
60 - if hasattr(customer_meta, 'customer_code'):
65 + if hasattr(customer_meta, "customer_code"):
66 self.customer_code = customer_meta.customer_code
62 - if hasattr(customer_meta, 'customer_name'):
67 + if hasattr(customer_meta, "customer_name"):
68 self.customer_name = customer_meta.customer_name
69 self.customer_meta_graylog_index = customer_meta.customer_meta_graylog_index
70 self.customer_meta_graylog_stream = customer_meta.customer_meta_graylog_stream
@@ -70,6 +75,7 @@ class CustomersMeta(SQLModel, table=True):
75 self.wazuh_registration_port = customer_meta.wazuh_registration_port
76 self.wazuh_log_ingestion_port = customer_meta.wazuh_log_ingestion_port
77
78 +
79 class Agents(SQLModel, table=True):
80 id: Optional[int] = Field(primary_key=True)
81 agent_id: str = Field(index=True)
@@ -84,7 +90,7 @@ class Agents(SQLModel, table=True):
90 wazuh_agent_version: str = Field(max_length=256)
91 velociraptor_agent_version: str = Field(max_length=256)
92 customer_code: Optional[str] = Field(foreign_key="customers.customer_code")
87 -
93 +
94 customer: Optional[Customers] = Relationship(back_populates="agents")
95
96 @classmethod
@@ -100,7 +106,7 @@ class Agents(SQLModel, table=True):
106 velociraptor_id=velociraptor_agent.client_id if velociraptor_agent.client_id else "n/a",
107 velociraptor_last_seen=velociraptor_agent.client_last_seen_as_datetime,
108 velociraptor_agent_version=velociraptor_agent.client_version,
103 - customer_code=customer_code
109 + customer_code=customer_code,
110 )
111
112 def update_from_model(self, wazuh_agent, velociraptor_agent, customer_code):
backend/app/healthchecks/agents/routes/agents.py
+57 -21
@@ -1,55 +1,91 @@
1 -from typing import List, Dict
2 -from fastapi import APIRouter, HTTPException, Security, Depends, Query
3 -from starlette.status import HTTP_401_UNAUTHORIZED
4 -from loguru import logger
1 from datetime import datetime
2 +from typing import Dict
3 +from typing import List
4 +
5 +from fastapi import APIRouter
6 +from fastapi import Depends
7 +from fastapi import HTTPException
8 +from fastapi import Query
9 +from fastapi import Security
10 +from loguru import logger
11 +from starlette.status import HTTP_401_UNAUTHORIZED
12
13 # App specific imports
14 from app.auth.routes.auth import auth_handler
15 from app.db.db_session import session
16 from app.db.universal_models import Agents
11 -from app.healthchecks.agents.schema.agents import AgentHealthCheckResponse, AgentModel, TimeCriteriaModel, HostLogsSearchBody, HostLogsSearchResponse
12 -from app.healthchecks.agents.services.agents import wazuh_agents_healthcheck, wazuh_agent_healthcheck, velociraptor_agents_healthcheck, velociraptor_agent_healthcheck, host_logs
17 +from app.healthchecks.agents.schema.agents import AgentHealthCheckResponse
18 +from app.healthchecks.agents.schema.agents import AgentModel
19 +from app.healthchecks.agents.schema.agents import HostLogsSearchBody
20 +from app.healthchecks.agents.schema.agents import HostLogsSearchResponse
21 +from app.healthchecks.agents.schema.agents import TimeCriteriaModel
22 +from app.healthchecks.agents.services.agents import host_logs
23 +from app.healthchecks.agents.services.agents import velociraptor_agent_healthcheck
24 +from app.healthchecks.agents.services.agents import velociraptor_agents_healthcheck
25 +from app.healthchecks.agents.services.agents import wazuh_agent_healthcheck
26 +from app.healthchecks.agents.services.agents import wazuh_agents_healthcheck
27
28 healtcheck_agents_router = APIRouter()
29
30 +
31 def verify_admin(user):
32 if not user.is_admin:
33 raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Unauthorized")
19 -
34 +
35
36 @healtcheck_agents_router.get("/wazuh", response_model=AgentHealthCheckResponse, description="Get Wazuh agents healthcheck")
22 -async def get_wazuh_agent_healthcheck(minutes: int = Query(60, description="Number of minutes within which the agent should have been last seen to be considered healthy."),
23 - hours: int = Query(0, description="Number of hours within which the agent should have been last seen to be considered healthy."),
24 - days: int = Query(0, description="Number of days within which the agent should have been last seen to be considered healthy.")) -> AgentHealthCheckResponse:
37 +async def get_wazuh_agent_healthcheck(
38 + minutes: int = Query(60, description="Number of minutes within which the agent should have been last seen to be considered healthy."),
39 + hours: int = Query(0, description="Number of hours within which the agent should have been last seen to be considered healthy."),
40 + days: int = Query(0, description="Number of days within which the agent should have been last seen to be considered healthy."),
41 +) -> AgentHealthCheckResponse:
42 time_criteria = TimeCriteriaModel(minutes=minutes, hours=hours, days=days)
43 agents = session.query(Agents).all()
44 return wazuh_agents_healthcheck(agents, time_criteria)
45
46 +
47 # Get single agent by agent_id
30 -@healtcheck_agents_router.get("/wazuh/{agent_id}", response_model=AgentHealthCheckResponse, description="Get Wazuh agent healthcheck by agent_id")
31 -async def get_wazuh_agent_healthcheck_by_agent_id(agent_id: str, minutes: int = Query(60, description="Number of minutes within which the agent should have been last seen to be considered healthy."),
32 - hours: int = Query(0, description="Number of hours within which the agent should have been last seen to be considered healthy."),
33 - days: int = Query(0, description="Number of days within which the agent should have been last seen to be considered healthy.")) -> AgentHealthCheckResponse:
48 +@healtcheck_agents_router.get(
49 + "/wazuh/{agent_id}",
50 + response_model=AgentHealthCheckResponse,
51 + description="Get Wazuh agent healthcheck by agent_id",
52 +)
53 +async def get_wazuh_agent_healthcheck_by_agent_id(
54 + agent_id: str,
55 + minutes: int = Query(60, description="Number of minutes within which the agent should have been last seen to be considered healthy."),
56 + hours: int = Query(0, description="Number of hours within which the agent should have been last seen to be considered healthy."),
57 + days: int = Query(0, description="Number of days within which the agent should have been last seen to be considered healthy."),
58 +) -> AgentHealthCheckResponse:
59 time_criteria = TimeCriteriaModel(minutes=minutes, hours=hours, days=days)
60 agent = session.query(Agents).filter(Agents.agent_id == agent_id).first()
61 if not agent:
62 raise HTTPException(status_code=404, detail=f"Agent with agent_id {agent_id} not found")
63 return wazuh_agent_healthcheck(agent, time_criteria)
64
65 +
66 @healtcheck_agents_router.get("/velociraptor", response_model=AgentHealthCheckResponse, description="Get Velociraptor agents healthcheck")
41 -async def get_velociraptor_agent_healthcheck(minutes: int = Query(60, description="Number of minutes within which the agent should have been last seen to be considered healthy."),
42 - hours: int = Query(0, description="Number of hours within which the agent should have been last seen to be considered healthy."),
43 - days: int = Query(0, description="Number of days within which the agent should have been last seen to be considered healthy.")) -> AgentHealthCheckResponse:
67 +async def get_velociraptor_agent_healthcheck(
68 + minutes: int = Query(60, description="Number of minutes within which the agent should have been last seen to be considered healthy."),
69 + hours: int = Query(0, description="Number of hours within which the agent should have been last seen to be considered healthy."),
70 + days: int = Query(0, description="Number of days within which the agent should have been last seen to be considered healthy."),
71 +) -> AgentHealthCheckResponse:
72 time_criteria = TimeCriteriaModel(minutes=minutes, hours=hours, days=days)
73 agents = session.query(Agents).all()
74 return velociraptor_agents_healthcheck(agents, time_criteria)
75
76 +
77 # Get single agent by agent_id
49 -@healtcheck_agents_router.get("/velociraptor/{agent_id}", response_model=AgentHealthCheckResponse, description="Get Velociraptor agent healthcheck by agent_id")
50 -async def get_velociraptor_agent_healthcheck_by_agent_id(agent_id: str, minutes: int = Query(60, description="Number of minutes within which the agent should have been last seen to be considered healthy."),
51 - hours: int = Query(0, description="Number of hours within which the agent should have been last seen to be considered healthy."),
52 - days: int = Query(0, description="Number of days within which the agent should have been last seen to be considered healthy.")) -> AgentHealthCheckResponse:
78 +@healtcheck_agents_router.get(
79 + "/velociraptor/{agent_id}",
80 + response_model=AgentHealthCheckResponse,
81 + description="Get Velociraptor agent healthcheck by agent_id",
82 +)
83 +async def get_velociraptor_agent_healthcheck_by_agent_id(
84 + agent_id: str,
85 + minutes: int = Query(60, description="Number of minutes within which the agent should have been last seen to be considered healthy."),
86 + hours: int = Query(0, description="Number of hours within which the agent should have been last seen to be considered healthy."),
87 + days: int = Query(0, description="Number of days within which the agent should have been last seen to be considered healthy."),
88 +) -> AgentHealthCheckResponse:
89 time_criteria = TimeCriteriaModel(minutes=minutes, hours=hours, days=days)
90 agent = session.query(Agents).filter(Agents.agent_id == agent_id).first()
91 if not agent:
backend/app/healthchecks/agents/schema/agents.py
+20 -5
@@ -1,7 +1,14 @@
1 -from typing import List, Optional, Dict, Any
2 -from pydantic import BaseModel, Field, validator
1 from datetime import datetime
2 from enum import Enum
3 +from typing import Any
4 +from typing import Dict
5 +from typing import List
6 +from typing import Optional
7 +
8 +from pydantic import BaseModel
9 +from pydantic import Field
10 +from pydantic import validator
11 +
12
13 class AgentModel(BaseModel):
14 id: Optional[int]
@@ -21,11 +28,13 @@ class AgentModel(BaseModel):
28 class Config:
29 orm_mode = True
30
31 +
32 class ExtendedAgentModel(AgentModel):
33 unhealthy_wazuh_agent: Optional[bool] = Field(None, description="Whether the agent is unhealthy in Wazuh")
34 unhealthy_velociraptor_agent: Optional[bool] = Field(None, description="Whether the agent is unhealthy in Velociraptor")
35 unhealthy_recent_logs_collected: Optional[bool] = Field(None, description="Whether the agent has not collected logs recently")
36
37 +
38 class AgentHealthCheckResponse(BaseModel):
39 healthy_wazuh_agents: Optional[List[ExtendedAgentModel]]
40 unhealthy_wazuh_agents: Optional[List[ExtendedAgentModel]]
@@ -36,6 +45,7 @@ class AgentHealthCheckResponse(BaseModel):
45 message: str
46 success: bool
47
48 +
49 class TimeCriteriaModel(BaseModel):
50 minutes: int = Field(60, description="Number of minutes within which the agent should have been last seen to be considered healthy.")
51 hours: int = Field(0, description="Number of hours within which the agent should have been last seen to be considered healthy.")
@@ -44,11 +54,13 @@ class TimeCriteriaModel(BaseModel):
54
55 ########## Logs Schemas ##########
56
57 +
58 class Log(BaseModel):
59 index_name: str
60 total_logs: int
61 logs: Optional[List[Dict[str, Any]]] = Field([], description="The logs returned from the search.")
62
63 +
64 class LogsSearchBody(BaseModel):
65 size: int = Field(1, description="The number of logs to return.")
66 timerange: str = Field("24h", description="The time range to search logs in.")
@@ -60,29 +72,32 @@ class LogsSearchBody(BaseModel):
72 def validate_timerange(cls, value):
73 if value[-1] not in ("h", "d", "w", "m"):
74 raise ValueError("Invalid timerange format. The string should end with either 'h', 'd', 'w', or 'm'.")
63 -
75 +
76 # Optionally, you can check that the prefix is a number
77 if not value[:-1].isdigit():
78 raise ValueError("Invalid timerange format. The string should start with a number.")
67 -
79 +
80 return value
81
82 +
83 class LogsSearchResponse(BaseModel):
84 logs_summary: List[Log]
85 success: bool
86 message: str
87
88 +
89 class CollectLogsResponse(BaseModel):
90 logs: List[Dict[str, Any]]
91 success: bool
92 message: str
93
94 +
95 class HostLogsSearchBody(LogsSearchBody):
96 agent_name: str = Field(..., description="The name of the agent to search logs for.")
97
98 +
99 class HostLogsSearchResponse(BaseModel):
100 logs_summary: Optional[List[Log]] = Field([], description="The logs summary returned from the search.")
101 healthy: bool = Field(False, description="Whether the host is healthy or not.")
102 success: bool
103 message: str
88 -
backend/app/healthchecks/agents/services/agents.py
+61 -25
@@ -1,16 +1,38 @@
1 -from typing import List, Dict, Any, Optional, Type
2 -from fastapi import APIRouter, HTTPException, Security, Depends
3 -from starlette.status import HTTP_401_UNAUTHORIZED
4 -from loguru import logger
1 from datetime import datetime
2 from datetime import timedelta
3 +from typing import Any
4 +from typing import Dict
5 +from typing import List
6 +from typing import Optional
7 +from typing import Type
8 +
9 +from fastapi import APIRouter
10 +from fastapi import Depends
11 +from fastapi import HTTPException
12 +from fastapi import Security
13 +from loguru import logger
14 +from starlette.status import HTTP_401_UNAUTHORIZED
15
16 # App specific imports
17 from app.auth.routes.auth import auth_handler
18 +from app.connectors.wazuh_indexer.utils.universal import LogsQueryBuilder
19 +from app.connectors.wazuh_indexer.utils.universal import collect_indices
20 +from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
21 +from app.connectors.wazuh_indexer.utils.universal import format_indices_stats
22 +from app.connectors.wazuh_indexer.utils.universal import format_node_allocation
23 +from app.connectors.wazuh_indexer.utils.universal import format_shards
24 from app.db.db_session import session
25 from app.db.universal_models import Agents
12 -from app.healthchecks.agents.schema.agents import AgentHealthCheckResponse, AgentModel, ExtendedAgentModel, TimeCriteriaModel, HostLogsSearchResponse, HostLogsSearchBody, LogsSearchBody, LogsSearchResponse, CollectLogsResponse
13 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client, format_node_allocation, format_indices_stats, format_shards, collect_indices, LogsQueryBuilder
26 +from app.healthchecks.agents.schema.agents import AgentHealthCheckResponse
27 +from app.healthchecks.agents.schema.agents import AgentModel
28 +from app.healthchecks.agents.schema.agents import CollectLogsResponse
29 +from app.healthchecks.agents.schema.agents import ExtendedAgentModel
30 +from app.healthchecks.agents.schema.agents import HostLogsSearchBody
31 +from app.healthchecks.agents.schema.agents import HostLogsSearchResponse
32 +from app.healthchecks.agents.schema.agents import LogsSearchBody
33 +from app.healthchecks.agents.schema.agents import LogsSearchResponse
34 +from app.healthchecks.agents.schema.agents import TimeCriteriaModel
35 +
36
37 def is_wazuh_agent_unhealthy(agent: AgentModel, time_criteria: TimeCriteriaModel) -> ExtendedAgentModel:
38 current_time = datetime.now()
@@ -19,7 +41,7 @@ def is_wazuh_agent_unhealthy(agent: AgentModel, time_criteria: TimeCriteriaModel
41 if wazuh_last_seen > current_time:
42 logger.info(f"Agent {agent} has a wazuh_last_seen time in the future: {wazuh_last_seen}")
43 return ExtendedAgentModel(**agent.dict(), unhealthy_wazuh_agent=True)
22 -
44 +
45 # Calculate the total time delta based on the criteria
46 total_minutes = time_criteria.minutes + time_criteria.hours * 60 + time_criteria.days * 24 * 60
47 time_delta = timedelta(minutes=total_minutes)
@@ -27,6 +49,7 @@ def is_wazuh_agent_unhealthy(agent: AgentModel, time_criteria: TimeCriteriaModel
49 is_unhealthy = (current_time - wazuh_last_seen) > time_delta
50 return ExtendedAgentModel(**agent.dict(), unhealthy_wazuh_agent=is_unhealthy)
51
52 +
53 def is_velociraptor_agent_unhealthy(agent: AgentModel, time_criteria: TimeCriteriaModel) -> ExtendedAgentModel:
54 current_time = datetime.now()
55 velociraptor_last_seen = agent.velociraptor_last_seen
@@ -34,7 +57,7 @@ def is_velociraptor_agent_unhealthy(agent: AgentModel, time_criteria: TimeCriter
57 if velociraptor_last_seen > current_time:
58 logger.info(f"Agent {agent} has a velociraptor_last_seen time in the future: {velociraptor_last_seen}")
59 return ExtendedAgentModel(**agent.dict(), unhealthy_velociraptor_agent=True)
37 -
60 +
61 # Calculate the total time delta based on the criteria
62 total_minutes = time_criteria.minutes + time_criteria.hours * 60 + time_criteria.days * 24 * 60
63 time_delta = timedelta(minutes=total_minutes)
@@ -42,6 +65,7 @@ def is_velociraptor_agent_unhealthy(agent: AgentModel, time_criteria: TimeCriter
65 is_unhealthy = (current_time - velociraptor_last_seen) > time_delta
66 return ExtendedAgentModel(**agent.dict(), unhealthy_velociraptor_agent=is_unhealthy)
67
68 +
69 def wazuh_agents_healthcheck(agents: list, time_criteria: TimeCriteriaModel) -> AgentHealthCheckResponse:
70 healthy_wazuh_agents = []
71 unhealthy_wazuh_agents = []
@@ -61,9 +85,10 @@ def wazuh_agents_healthcheck(agents: list, time_criteria: TimeCriteriaModel) ->
85 healthy_wazuh_agents=healthy_wazuh_agents,
86 unhealthy_wazuh_agents=unhealthy_wazuh_agents,
87 success=True,
64 - message="Wazuh agent healthcheck fetched successfully"
88 + message="Wazuh agent healthcheck fetched successfully",
89 )
90
91 +
92 def wazuh_agent_healthcheck(agent: AgentModel, time_criteria: TimeCriteriaModel) -> AgentHealthCheckResponse:
93 extended_agent = is_wazuh_agent_unhealthy(agent, time_criteria)
94 if extended_agent.unhealthy_wazuh_agent:
@@ -71,16 +96,17 @@ def wazuh_agent_healthcheck(agent: AgentModel, time_criteria: TimeCriteriaModel)
96 healthy_wazuh_agents=[],
97 unhealthy_wazuh_agents=[extended_agent],
98 success=True,
74 - message="Wazuh agent healthcheck fetched successfully"
99 + message="Wazuh agent healthcheck fetched successfully",
100 )
101 else:
102 return AgentHealthCheckResponse(
103 healthy_wazuh_agents=[extended_agent],
104 unhealthy_wazuh_agents=[],
105 success=True,
81 - message="Wazuh agent healthcheck fetched successfully"
106 + message="Wazuh agent healthcheck fetched successfully",
107 )
108
109 +
110 def velociraptor_agents_healthcheck(agents: list, time_criteria: TimeCriteriaModel) -> AgentHealthCheckResponse:
111 healthy_velociraptor_agents = []
112 unhealthy_velociraptor_agents = []
@@ -100,9 +126,10 @@ def velociraptor_agents_healthcheck(agents: list, time_criteria: TimeCriteriaMod
126 healthy_velociraptor_agents=healthy_velociraptor_agents,
127 unhealthy_velociraptor_agents=unhealthy_velociraptor_agents,
128 success=True,
103 - message="Velociraptor agent healthcheck fetched successfully"
129 + message="Velociraptor agent healthcheck fetched successfully",
130 )
131
132 +
133 def velociraptor_agent_healthcheck(agent: AgentModel, time_criteria: TimeCriteriaModel) -> AgentHealthCheckResponse:
134 extended_agent = is_velociraptor_agent_unhealthy(agent, time_criteria)
135 if extended_agent.unhealthy_velociraptor_agent:
@@ -110,16 +137,17 @@ def velociraptor_agent_healthcheck(agent: AgentModel, time_criteria: TimeCriteri
137 healthy_velociraptor_agents=[],
138 unhealthy_velociraptor_agents=[extended_agent],
139 success=True,
113 - message="Velociraptor agent healthcheck fetched successfully"
140 + message="Velociraptor agent healthcheck fetched successfully",
141 )
142 else:
143 return AgentHealthCheckResponse(
144 healthy_velociraptor_agents=[extended_agent],
145 unhealthy_velociraptor_agents=[],
146 success=True,
120 - message="Velociraptor agent healthcheck fetched successfully"
147 + message="Velociraptor agent healthcheck fetched successfully",
148 )
122 -
149 +
150 +
151 def host_logs(search_body: HostLogsSearchBody) -> HostLogsSearchResponse:
152 result = get_logs_generic(search_body, is_host_specific=True)
153 logger.info(f"Host logs search result: {result}")
@@ -128,14 +156,22 @@ def host_logs(search_body: HostLogsSearchBody) -> HostLogsSearchResponse:
156 total_logs = 0
157
158 # Loop through each item in logs_summary to count total logs
131 - for log_summary in result['logs_summary']:
132 - total_logs += log_summary['total_logs']
159 + for log_summary in result["logs_summary"]:
160 + total_logs += log_summary["total_logs"]
161
162 # Check if there are any logs
163 if total_logs > 0:
136 - return HostLogsSearchResponse(success=True, healthy=True, message=f"Host is healthy. At least one log was found within the specified time range of {search_body.timerange}")
164 + return HostLogsSearchResponse(
165 + success=True,
166 + healthy=True,
167 + message=f"Host is healthy. At least one log was found within the specified time range of {search_body.timerange}",
168 + )
169 else:
138 - return HostLogsSearchResponse(success=True, healthy=False, message=f"Host is unhealthy. No logs were found within the specified time range of {search_body.timerange}")
170 + return HostLogsSearchResponse(
171 + success=True,
172 + healthy=False,
173 + message=f"Host is unhealthy. No logs were found within the specified time range of {search_body.timerange}",
174 + )
175
176
177 def get_logs_generic(search_body: Type[LogsSearchBody], is_host_specific: bool = False, index_name: Optional[str] = None):
@@ -143,7 +179,7 @@ def get_logs_generic(search_body: Type[LogsSearchBody], is_host_specific: bool =
179 logs_summary = []
180 indices = collect_indices()
181 index_list = [index_name] if index_name else indices.indices_list # Use the provided index_name or get all indices
146 -
182 +
183 for index_name in index_list:
184 try:
185 logs = collect_logs_generic(index_name, body=search_body, is_host_specific=is_host_specific)
@@ -155,20 +191,20 @@ def get_logs_generic(search_body: Type[LogsSearchBody], is_host_specific: bool =
191 "logs": logs.logs,
192 },
193 )
158 - break # Only collect logs from the first index that has logs
194 + break # Only collect logs from the first index that has logs
195 except HTTPException as e:
196 logger.warning(f"An error occurred while processing index {index_name}: {e.detail}")
161 -
197 +
198 if len(logs_summary) == 0:
199 message = "No logs found"
200 else:
201 message = f"Succesfully collected top {search_body.size} logs for each index"
166 -
202 +
203 return {"logs_summary": logs_summary, "success": len(logs_summary) > 0, "message": message}
204
205
206 def collect_logs_generic(index_name: str, body: LogsSearchBody, is_host_specific: bool = False) -> CollectLogsResponse:
171 - es_client = create_wazuh_indexer_client('Wazuh-Indexer')
207 + es_client = create_wazuh_indexer_client("Wazuh-Indexer")
208 query_builder = LogsQueryBuilder()
209 query_builder.add_time_range(timerange=body.timerange, timestamp_field=body.timestamp_field)
210 query_builder.add_matches(matches=[(body.log_field, body.log_value)])
@@ -187,4 +223,4 @@ def collect_logs_generic(index_name: str, body: LogsSearchBody, is_host_specific
223 return CollectLogsResponse(logs=logs_list, success=True, message="logs collected successfully")
224 except Exception as e:
225 logger.debug(f"Failed to collect logs: {e}")
190 - return CollectLogsResponse(logs=[], success=False, message=f"Failed to collect logs: {e}")
\ No newline at end of file
226 + return CollectLogsResponse(logs=[], success=False, message=f"Failed to collect logs: {e}")
backend/app/integrations/alert_escalation/routes/general_alert.py
+11 -10
@@ -1,23 +1,24 @@
1 from typing import List
2 -from fastapi import APIRouter, HTTPException, Security, Depends
3 -from starlette.status import HTTP_401_UNAUTHORIZED
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 starlette.status import HTTP_401_UNAUTHORIZED
9
10 # App specific imports
11 from app.auth.routes.auth import auth_handler
12 +from app.connectors.wazuh_indexer.utils.universal import collect_indices
13 from app.db.db_session import session
9 -from app.integrations.alert_escalation.schema.general_alert import (
10 - CreateAlertRequest, CreateAlertResponse
11 -)
12 -
14 +from app.integrations.alert_escalation.schema.general_alert import CreateAlertRequest
15 +from app.integrations.alert_escalation.schema.general_alert import CreateAlertResponse
16 from app.integrations.alert_escalation.services.general_alert import create_alert
17
15 -from app.connectors.wazuh_indexer.utils.universal import collect_indices
16 -
17 -
18 integration_general_alerts_router = APIRouter()
19
20 +
21 @integration_general_alerts_router.post("/create", response_model=CreateAlertResponse, description="Create an alert in IRIS")
22 async def create_alert_route(create_alert_request: CreateAlertRequest) -> CreateAlertResponse:
23 logger.info(f"Creating alert {create_alert_request.alert_id} in IRIS")
23 - return create_alert(create_alert_request)
\ No newline at end of file
24 + return create_alert(create_alert_request)
backend/app/integrations/alert_escalation/schema/general_alert.py
+24 -7
@@ -1,26 +1,37 @@
1 -from pydantic import BaseModel, Field, validator, Extra
2 -from typing import Optional, List, Dict, Any
3 -from typing import Union
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 +from typing import Union
7 +
8 +from pydantic import BaseModel
9 +from pydantic import Extra
10 +from pydantic import Field
11 +from pydantic import validator
12 +
13
14 class ValidIocFields(Enum):
15 MISP_VALUE = "misp_value"
16 OPENCTI_VALUE = "opencti_value"
17 THREAT_INTEL_VALUE = "threat_intel_value"
18
19 +
20 class CreateAlertRequest(BaseModel):
21 index_name: str = Field(..., description="The name of the index to search alerts for.")
22 alert_id: str = Field(..., description="The alert id to create.")
23
24 +
25 class CreateAlertResponse(BaseModel):
26 success: bool
27 message: str
28 alert_id: int = Field(..., description="The alert id as created in IRIS.")
29
30 +
31 class GenericSourceModel(BaseModel):
32 agent_name: str = Field(..., description="The name of the agent.")
33 agent_id: str = Field(..., description="The id of the agent.")
23 - agent_labels_customer: str = Field(..., description="The customer of the agent.")
34 + agent_labels_customer: str = Field(..., description="The customer of the agent.")
35 rule_id: str = Field(..., description="The id of the rule.")
36 rule_level: int = Field(..., description="The level of the rule.")
37 rule_description: str = Field(..., description="The description of the rule.")
@@ -30,18 +41,23 @@ class GenericSourceModel(BaseModel):
41 class Config:
42 extra = Extra.allow
43
44 +
45 class GenericAlertModel(BaseModel):
46 _index: str
47 _id: str
48 _version: int
49 _source: GenericSourceModel # Nested model
38 - asset_type_id: Optional[int] = Field(None, description="The asset type id of the alert which is needed for when we add the asset to IRIS.")
50 + asset_type_id: Optional[int] = Field(
51 + None,
52 + description="The asset type id of the alert which is needed for when we add the asset to IRIS.",
53 + )
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.")
56
57 class Config:
58 extra = Extra.allow
59
60 +
61 # Sample data from `get_single_alert_details`
62 sample_data = {
63 "_index": "some_index",
@@ -56,7 +72,6 @@ sample_data = {
72 }
73
74
59 -
75 ########### Create Alerts Schemas ###########
76 class IrisAsset(BaseModel):
77 asset_name: str = Field(..., description="Name of the asset", example="Server01")
@@ -64,12 +79,14 @@ class IrisAsset(BaseModel):
79 asset_description: str = Field(..., description="Description of the asset", example="Windows Server")
80 asset_type_id: int = Field(..., description="Type ID of the asset", example=1)
81
82 +
83 class IrisIoc(BaseModel):
84 ioc_value: str = Field(..., description="Value of the IoC", example="www.google.com")
85 ioc_description: str = Field(..., description="Description of the IoC", example="Google")
86 ioc_tlp_id: int = Field(1, description="TLP ID of the IoC", example=1)
87 ioc_type_id: int = Field(20, description="Type ID of the IoC", example=20)
88
89 +
90 class IrisAlertContext(BaseModel):
91 alert_id: str = Field(..., description="ID of the alert", example="123")
92 alert_name: str = Field(..., description="Name of the alert", example="Intrusion Detected")
@@ -83,6 +100,7 @@ class IrisAlertContext(BaseModel):
100 rule_mitre_tactic: Optional[str] = Field("n/a", description="MITRE ATT&CK Tactic", example="Execution")
101 rule_mitre_technique: Optional[str] = Field("n/a", description="MITRE ATT&CK Technique", example="Scripting")
102
103 +
104 class IrisAlertPayload(BaseModel):
105 alert_title: str = Field(..., description="Title of the alert", example="Intrusion Detected")
106 alert_description: str = Field(..., description="Description of the alert", example="Intrusion Detected by Firewall")
@@ -97,4 +115,3 @@ class IrisAlertPayload(BaseModel):
115
116 def to_dict(self):
117 return self.dict(exclude_none=True)
100 -
backend/app/integrations/alert_escalation/services/general_alert.py
+57 -33
@@ -1,24 +1,47 @@
1 -from typing import Dict, Any, Type, Optional, List, Union, Set
2 -from pydantic import BaseModel
3 -from sqlmodel import Session, select
4 -from app.connectors.models import Connectors
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 +from typing import Set
6 +from typing import Type
7 +from typing import Union
8 +
9 +from dfir_iris_client.alert import Alert
10 from elasticsearch7 import Elasticsearch
11 +from fastapi import HTTPException
12 from loguru import logger
7 -from app.db.db_session import engine
8 -from dfir_iris_client.alert import Alert
13 +from pydantic import BaseModel
14 +from sqlmodel import Session
15 +from sqlmodel import select
16 +
17 +from app.connectors.dfir_iris.utils.universal import create_dfir_iris_client
18 +from app.connectors.dfir_iris.utils.universal import fetch_and_parse_data
19 +from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
20 +from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
21 +from app.connectors.models import Connectors
22 from app.connectors.schema import ConnectorResponse
23 from app.connectors.utils import get_connector_info_from_db
11 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client, format_node_allocation, format_indices_stats, format_shards, collect_indices, AlertsQueryBuilder
12 -from app.integrations.alert_escalation.schema.general_alert import (
13 - CreateAlertRequest, CreateAlertResponse, GenericAlertModel, GenericSourceModel, ValidIocFields, IrisAsset, IrisAlertContext, IrisAlertPayload, IrisIoc
14 -)
15 -from app.integrations.alert_escalation.utils.universal import (
16 - get_agent_data, get_asset_type_id, validate_ioc_type
17 -)
18 -
19 -from app.connectors.dfir_iris.utils.universal import create_dfir_iris_client, fetch_and_parse_data, initialize_client_and_alert, fetch_and_validate_data
24 from app.connectors.wazuh_indexer.schema.indices import IndexConfigModel
21 -from fastapi import HTTPException
25 +from app.connectors.wazuh_indexer.utils.universal import AlertsQueryBuilder
26 +from app.connectors.wazuh_indexer.utils.universal import collect_indices
27 +from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
28 +from app.connectors.wazuh_indexer.utils.universal import format_indices_stats
29 +from app.connectors.wazuh_indexer.utils.universal import format_node_allocation
30 +from app.connectors.wazuh_indexer.utils.universal import format_shards
31 +from app.db.db_session import engine
32 +from app.integrations.alert_escalation.schema.general_alert import CreateAlertRequest
33 +from app.integrations.alert_escalation.schema.general_alert import CreateAlertResponse
34 +from app.integrations.alert_escalation.schema.general_alert import GenericAlertModel
35 +from app.integrations.alert_escalation.schema.general_alert import GenericSourceModel
36 +from app.integrations.alert_escalation.schema.general_alert import IrisAlertContext
37 +from app.integrations.alert_escalation.schema.general_alert import IrisAlertPayload
38 +from app.integrations.alert_escalation.schema.general_alert import IrisAsset
39 +from app.integrations.alert_escalation.schema.general_alert import IrisIoc
40 +from app.integrations.alert_escalation.schema.general_alert import ValidIocFields
41 +from app.integrations.alert_escalation.utils.universal import get_agent_data
42 +from app.integrations.alert_escalation.utils.universal import get_asset_type_id
43 +from app.integrations.alert_escalation.utils.universal import validate_ioc_type
44 +
45
46 def valid_ioc_fields() -> Set[str]:
47 """
@@ -30,38 +53,37 @@ def valid_ioc_fields() -> Set[str]:
53 """
54 return {field.value for field in ValidIocFields}
55
56 +
57 def get_single_alert_details(alert_details: CreateAlertRequest) -> GenericAlertModel:
58 logger.info(f"Fetching alert details for alert {alert_details.alert_id} in index {alert_details.index_name}")
35 - es_client = create_wazuh_indexer_client('Wazuh-Indexer')
59 + es_client = create_wazuh_indexer_client("Wazuh-Indexer")
60 try:
61 alert = es_client.get(index=alert_details.index_name, id=alert_details.alert_id)
38 - source_model = GenericSourceModel(**alert['_source'])
39 - return GenericAlertModel(_source=source_model, _id=alert['_id'], _index=alert['_index'], _version=alert['_version'])
62 + source_model = GenericSourceModel(**alert["_source"])
63 + return GenericAlertModel(_source=source_model, _id=alert["_id"], _index=alert["_index"], _version=alert["_version"])
64 except Exception as e:
65 logger.debug(f"Failed to collect alert details: {e}")
66 raise HTTPException(status_code=400, detail=f"Failed to collect alert details: {e}")
67
68 +
69 def build_ioc_payload(alert_details: GenericAlertModel) -> Optional[IrisIoc]:
70 for field in valid_ioc_fields():
71 if hasattr(alert_details._source, field):
72 ioc_value = getattr(alert_details._source, field)
73 ioc_type = validate_ioc_type(ioc_value=ioc_value)
49 - return IrisIoc(
50 - ioc_value=ioc_value,
51 - ioc_description='IoC found in alert',
52 - ioc_tlp_id=1,
53 - ioc_type_id=ioc_type
54 - )
74 + return IrisIoc(ioc_value=ioc_value, ioc_description="IoC found in alert", ioc_tlp_id=1, ioc_type_id=ioc_type)
75 return None
76
77 +
78 def build_asset_payload(agent_data, alert_details) -> IrisAsset:
79 return IrisAsset(
80 asset_name=agent_data.hostname,
81 asset_ip=agent_data.ip_address,
82 asset_description=agent_data.os,
62 - asset_type_id=alert_details.asset_type_id
83 + asset_type_id=alert_details.asset_type_id,
84 )
85
86 +
87 def build_alert_context_payload(alert_details: GenericAlertModel, agent_data) -> IrisAlertContext:
88 return IrisAlertContext(
89 alert_id=alert_details._id,
@@ -71,12 +93,13 @@ def build_alert_context_payload(alert_details: GenericAlertModel, agent_data) ->
93 asset_name=agent_data.hostname,
94 asset_ip=agent_data.ip_address,
95 asset_type=alert_details.asset_type_id,
74 - process_id=getattr(alert_details._source, 'process_id', 'No process id found'),
75 - rule_mitre_id=getattr(alert_details._source, 'rule_mitre_id', 'No rule mitre id found'),
76 - rule_mitre_tactic=getattr(alert_details._source, 'rule_mitre_tactic', 'No rule mitre tactic found'),
77 - rule_mitre_technique=getattr(alert_details._source, 'rule_mitre_technique', 'No rule mitre technique found'),
96 + process_id=getattr(alert_details._source, "process_id", "No process id found"),
97 + rule_mitre_id=getattr(alert_details._source, "rule_mitre_id", "No rule mitre id found"),
98 + rule_mitre_tactic=getattr(alert_details._source, "rule_mitre_tactic", "No rule mitre tactic found"),
99 + rule_mitre_technique=getattr(alert_details._source, "rule_mitre_technique", "No rule mitre technique found"),
100 )
101
102 +
103 def build_alert_payload(alert_details: GenericAlertModel, agent_data, ioc_payload: Optional[IrisIoc]) -> IrisAlertPayload:
104 asset_payload = build_asset_payload(agent_data, alert_details)
105 context_payload = build_alert_context_payload(alert_details, agent_data)
@@ -85,21 +108,21 @@ def build_alert_payload(alert_details: GenericAlertModel, agent_data, ioc_payloa
108 return IrisAlertPayload(
109 alert_title=alert_details._source.rule_description,
110 alert_description=alert_details._source.rule_description,
88 - alert_source='CoPilot',
111 + alert_source="CoPilot",
112 assets=[asset_payload],
113 alert_status_id=3,
114 alert_severity_id=5,
115 alert_customer_id=1,
116 alert_source_content=alert_details._source,
117 alert_context=context_payload,
95 - alert_iocs=[ioc_payload]
118 + alert_iocs=[ioc_payload],
119 )
120 else:
121 logger.info(f"Alert does not have IoC")
122 return IrisAlertPayload(
123 alert_title=alert_details._source.rule_description,
124 alert_description=alert_details._source.rule_description,
102 - alert_source='CoPilot',
125 + alert_source="CoPilot",
126 assets=[asset_payload],
127 alert_status_id=3,
128 alert_severity_id=5,
@@ -108,6 +131,7 @@ def build_alert_payload(alert_details: GenericAlertModel, agent_data, ioc_payloa
131 alert_context=context_payload,
132 )
133
134 +
135 def create_alert(alert: CreateAlertRequest) -> CreateAlertResponse:
136 logger.info(f"Creating alert {alert.alert_id} in IRIS")
137 alert_details = get_single_alert_details(alert_details=alert)
backend/app/integrations/alert_escalation/utils/universal.py
+39 -20
@@ -1,23 +1,36 @@
1 -from typing import Dict, Any, List, Generator, Type, Optional, Union, Tuple
2 -from sqlmodel import Session, select
1 import ipaddress
4 -import regex
2 import re
6 -from app.connectors.models import Connectors
3 +from abc import ABC
4 +from datetime import datetime
5 +from datetime import timedelta
6 +from typing import Any
7 +from typing import Dict
8 +from typing import Generator
9 +from typing import Iterable
10 +from typing import List
11 +from typing import Optional
12 +from typing import Tuple
13 +from typing import Type
14 +from typing import Union
15 +
16 +import regex
17 +import requests
18 from elasticsearch7 import Elasticsearch
19 +from fastapi import HTTPException
20 from loguru import logger
9 -from app.db.db_session import engine
10 -from app.db.db_session import session
11 -import requests
21 +from sqlmodel import Session
22 +from sqlmodel import select
23 +
24 +from app.connectors.models import Connectors
25 from app.connectors.schema import ConnectorResponse
13 -from app.db.all_models import Agents
26 from app.connectors.utils import get_connector_info_from_db
15 -from app.connectors.wazuh_indexer.schema.indices import Indices, IndexConfigModel
16 -from app.healthchecks.agents.schema.agents import AgentHealthCheckResponse, AgentModel
17 -from datetime import datetime, timedelta
18 -from typing import Iterable, Tuple
19 -from fastapi import HTTPException
20 -from abc import ABC
27 +from app.connectors.wazuh_indexer.schema.indices import IndexConfigModel
28 +from app.connectors.wazuh_indexer.schema.indices import Indices
29 +from app.db.all_models import Agents
30 +from app.db.db_session import engine
31 +from app.db.db_session import session
32 +from app.healthchecks.agents.schema.agents import AgentHealthCheckResponse
33 +from app.healthchecks.agents.schema.agents import AgentModel
34
35
36 #################### ! DFIR IRIS ASSET VALIDATOR ! ####################
@@ -176,13 +189,14 @@ class AssetTypeResolver:
189
190 # Return default asset type id (1) if no validators succeed
191 return 1
179 -
180 -#################### ! DFIR IRIS ASSET VALIDATOR END ! ####################
192
193
194 +#################### ! DFIR IRIS ASSET VALIDATOR END ! ####################
195 +
196
197 #################### ! DFIR IRIS IOC VALIDATOR ! ##########################
198
199 +
200 class IoCValidator(ABC):
201 """
202 Base class for validators.
@@ -281,6 +295,7 @@ class DomainValidator(IoCValidator):
295
296 #################### ! DFIR IRIS IOC VALIDATOR END ! ##########################
297
298 +
299 def verify_wazuh_indexer_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
300 """
301 Verifies the connection to Wazuh Indexer service.
@@ -289,7 +304,7 @@ def verify_wazuh_indexer_credentials(attributes: Dict[str, Any]) -> Dict[str, An
304 dict: A dictionary containing 'connectionSuccessful' status and 'authToken' if the connection is successful.
305 """
306 logger.info(f"Verifying the wazuh-indexer connection to {attributes['connector_url']}")
292 -
307 +
308 try:
309 es = Elasticsearch(
310 [attributes["connector_url"]],
@@ -305,7 +320,8 @@ def verify_wazuh_indexer_credentials(attributes: Dict[str, Any]) -> Dict[str, An
320 except Exception as e:
321 logger.error(f"Connection to {attributes['connector_url']} failed with error: {e}")
322 return {"connectionSuccessful": False, "message": f"Connection to {attributes['connector_url']} failed with error: {e}"}
308 -
323 +
324 +
325 def verify_wazuh_indexer_connection(connector_name: str) -> str:
326 """
327 Returns the authentication token for the Wazuh Indexer service.
@@ -319,6 +335,7 @@ def verify_wazuh_indexer_connection(connector_name: str) -> str:
335 return None
336 return verify_wazuh_indexer_credentials(attributes)
337
338 +
339 def create_wazuh_indexer_client(connector_name: str) -> Elasticsearch:
340 """
341 Returns an Elasticsearch client for the Wazuh Indexer service.
@@ -339,6 +356,7 @@ def create_wazuh_indexer_client(connector_name: str) -> Elasticsearch:
356 retry_on_timeout=False,
357 )
358
359 +
360 def get_agent_data(agent_id: str) -> AgentModel:
361 """
362 Get agent data based on the agent id from the agents table.
@@ -354,7 +372,8 @@ def get_agent_data(agent_id: str) -> AgentModel:
372 return agent_details
373 else:
374 raise HTTPException(status_code=404, detail=f"Agent with id {agent_id} not found in agents table")
357 -
375 +
376 +
377 def get_asset_type_id(os: str) -> int:
378 """
379 Use AssetTypeResolver to determine the asset type ID to set within DFIR-IRIS.
@@ -372,6 +391,7 @@ def get_asset_type_id(os: str) -> int:
391 asset_resolver = AssetTypeResolver(os)
392 return asset_resolver.get_asset_type_id()
393
394 +
395 def validate_ioc_type(ioc_value: str) -> str:
396 """
397 Validate IoC type using validators.
@@ -400,4 +420,3 @@ def validate_ioc_type(ioc_value: str) -> str:
420 if ioc_type is None:
421 logger.error("Failed to validate IoC value.")
422 return ioc_type
403 -
backend/app/integrations/dnstwist/routes/analyze.py
+17 -7
@@ -1,15 +1,23 @@
1 -from fastapi import APIRouter, HTTPException, Security, security, Depends
2 -from fastapi.security import HTTPAuthorizationCredentials
3 -from app.smtp.schema.configure import SMTPResponse
1 import regex
2 +from fastapi import APIRouter
3 +from fastapi import Depends
4 +from fastapi import HTTPException
5 +from fastapi import Security
6 +from fastapi import security
7 +from fastapi.security import HTTPAuthorizationCredentials
8 +from loguru import logger
9 +
10 from app.auth.utils import AuthHandler
11 from app.db.db_session import session
7 -from app.integrations.dnstwist.schema.analyze import DomainAnalysisResponse, DomainRequestBody
8 -from app.integrations.dnstwist.services.analyze import analyze_domain, analyze_domain_phishing
9 -from loguru import logger
12 +from app.integrations.dnstwist.schema.analyze import DomainAnalysisResponse
13 +from app.integrations.dnstwist.schema.analyze import DomainRequestBody
14 +from app.integrations.dnstwist.services.analyze import analyze_domain
15 +from app.integrations.dnstwist.services.analyze import analyze_domain_phishing
16 +from app.smtp.schema.configure import SMTPResponse
17
18 dnstwist_router = APIRouter()
19
20 +
21 def is_domain(domain: str) -> DomainRequestBody:
22 """
23 Check if the provided domain is valid.
@@ -28,10 +36,12 @@ def is_domain(domain: str) -> DomainRequestBody:
36 raise HTTPException(status_code=400, detail="Invalid domain")
37 return DomainRequestBody(domain=domain)
38
31 -@dnstwist_router.post('/analyze', response_model=DomainAnalysisResponse, status_code=200, description='Analyze domain with DNS Twist')
39 +
40 +@dnstwist_router.post("/analyze", response_model=DomainAnalysisResponse, status_code=200, description="Analyze domain with DNS Twist")
41 async def analyze(body: DomainRequestBody = Depends(is_domain)):
42 return analyze_domain(body.domain)
43
44 +
45 # ! TODO: Add phishing analysis - Need more clarification on this
46 # @dnstwist_router.post('/analyze/phishing', response_model=DomainAnalysisResponse, status_code=200, description='Analyze domain with DNS Twist')
47 # async def analyze_phishing(body: DomainRequestBody = Depends(is_domain)):
backend/app/integrations/dnstwist/schema/analyze.py
+9 -3
@@ -1,5 +1,9 @@
1 -from typing import List, Optional
2 -from pydantic import BaseModel, Field
1 +from typing import List
2 +from typing import Optional
3 +
4 +from pydantic import BaseModel
5 +from pydantic import Field
6 +
7
8 class DomainData(BaseModel):
9 dns_a: Optional[List[str]]
@@ -8,10 +12,12 @@ class DomainData(BaseModel):
12 domain: str
13 fuzzer: str
14
15 +
16 class DomainAnalysisResponse(BaseModel):
17 data: List[DomainData]
18 message: str
19 success: bool
20
21 +
22 class DomainRequestBody(BaseModel):
17 - domain: str = Field("socfortress.co", description="The domain to analyze.")
\ No newline at end of file
23 + domain: str = Field("socfortress.co", description="The domain to analyze.")
backend/app/integrations/dnstwist/services/analyze.py
+10 -7
@@ -2,9 +2,11 @@ from typing import Any
2 from typing import Optional
3
4 import dnstwist
5 -from app.integrations.dnstwist.schema.analyze import DomainRequestBody, DomainAnalysisResponse
5 from loguru import logger
6
7 +from app.integrations.dnstwist.schema.analyze import DomainAnalysisResponse
8 +from app.integrations.dnstwist.schema.analyze import DomainRequestBody
9 +
10
11 def analyze_domain(domain: DomainRequestBody) -> DomainAnalysisResponse:
12 """
@@ -21,6 +23,7 @@ def analyze_domain(domain: DomainRequestBody) -> DomainAnalysisResponse:
23 data = dnstwist.run(domain=domain, registered=True, format="json")
24 return DomainAnalysisResponse(data=data, message="Domain analysis completed.", success=True)
25
26 +
27 def analyze_domain_phishing(domain: DomainRequestBody) -> DomainAnalysisResponse:
28 """
29 Analyze the domain using dnstwist and return the results for registered domains.
@@ -34,9 +37,9 @@ def analyze_domain_phishing(domain: DomainRequestBody) -> DomainAnalysisResponse
37 logger.info(f"Analyzing domain {domain} with DNS Twist.")
38 logger.info("Analyzing domain for registered domains.")
39 data = dnstwist.run(
37 - domain=domain,
38 - registered=True,
39 - format="json",
40 - lsh=True,
41 - )
42 - return DomainAnalysisResponse(data=data, message="Domain analysis completed.", success=True)
\ No newline at end of file
40 + domain=domain,
41 + registered=True,
42 + format="json",
43 + lsh=True,
44 + )
45 + return DomainAnalysisResponse(data=data, message="Domain analysis completed.", success=True)
backend/app/smtp/routes/configure.py
+28 -18
@@ -1,50 +1,60 @@
1 -from fastapi import APIRouter, HTTPException, Security, security, Depends
1 +from fastapi import APIRouter
2 +from fastapi import Depends
3 +from fastapi import HTTPException
4 +from fastapi import Security
5 +from fastapi import security
6 from fastapi.security import HTTPAuthorizationCredentials
3 -from app.smtp.schema.configure import SMTPResponse
7 +from loguru import logger
8
9 +from app.auth.models.users import SMTP
10 +from app.auth.models.users import SMTPInput
11 +from app.auth.models.users import User
12 +from app.auth.services.universal import find_user
13 +from app.auth.services.universal import select_all_users
14 from app.auth.utils import AuthHandler
15 from app.db.db_session import session
7 -from app.auth.models.users import SMTP, SMTPInput, User
8 -from app.auth.services.universal import select_all_users, find_user
9 -from loguru import logger
16 +from app.smtp.schema.configure import SMTPResponse
17
18 smtp_router = APIRouter()
19 auth_handler = AuthHandler()
20
14 -@smtp_router.post('/{user_id}/register', response_model=SMTPResponse, status_code=200, description='Register new SMTP for user')
21 +
22 +@smtp_router.post("/{user_id}/register", response_model=SMTPResponse, status_code=200, description="Register new SMTP for user")
23 async def register(user_id: int, smtp: SMTPInput):
24 users = select_all_users()
25 logger.info(users)
26 if not any(x.id == user_id for x in users):
19 - raise HTTPException(status_code=400, detail='User not found')
27 + raise HTTPException(status_code=400, detail="User not found")
28 # Check if SMTP already exists for user
29 smtp_found = session.query(SMTP).filter(SMTP.user_id == user_id).first()
30 if smtp_found:
23 - raise HTTPException(status_code=400, detail='SMTP already exists for user')
31 + raise HTTPException(status_code=400, detail="SMTP already exists for user")
32 hashed_pwd = auth_handler.get_password_hash(smtp.smtp_password)
33 u = SMTP(email=smtp.email, smtp_password=hashed_pwd, smtp_server=smtp.smtp_server, smtp_port=smtp.smtp_port, user_id=user_id)
34 session.add(u)
35 session.commit()
36 return {"message": "SMTP created successfully", "success": True}
37
30 -@smtp_router.get('/{user_id}', response_model=SMTP, status_code=200, description='Get SMTP for user')
38 +
39 +@smtp_router.get("/{user_id}", response_model=SMTP, status_code=200, description="Get SMTP for user")
40 async def get_smtp(user_id: int):
41 users = select_all_users()
42 if not any(x.id == user_id for x in users):
34 - raise HTTPException(status_code=400, detail='User not found')
43 + raise HTTPException(status_code=400, detail="User not found")
44 smtp_found = session.query(SMTP).filter(SMTP.user_id == user_id).first()
45 if not smtp_found:
37 - raise HTTPException(status_code=400, detail='SMTP not found for user')
46 + raise HTTPException(status_code=400, detail="SMTP not found for user")
47 return smtp_found
48
40 -@smtp_router.put('/{user_id}', response_model=SMTPResponse, status_code=200, description='Update SMTP for user')
49 +
50 +@smtp_router.put("/{user_id}", response_model=SMTPResponse, status_code=200, description="Update SMTP for user")
51 async def update_smtp(user_id: int, smtp: SMTPInput):
52 users = select_all_users()
53 if not any(x.id == user_id for x in users):
44 - raise HTTPException(status_code=400, detail='User not found')
54 + raise HTTPException(status_code=400, detail="User not found")
55 smtp_found = session.query(SMTP).filter(SMTP.user_id == user_id).first()
56 if not smtp_found:
47 - raise HTTPException(status_code=400, detail='SMTP not found for user')
57 + raise HTTPException(status_code=400, detail="SMTP not found for user")
58 smtp_found.email = smtp.email
59 smtp_found.smtp_server = smtp.smtp_server
60 smtp_found.smtp_port = smtp.smtp_port
@@ -52,15 +62,15 @@ async def update_smtp(user_id: int, smtp: SMTPInput):
62 session.commit()
63 return {"message": "SMTP updated successfully", "success": True}
64
55 -@smtp_router.delete('/{user_id}', response_model=SMTPResponse, status_code=200, description='Delete SMTP for user')
65 +
66 +@smtp_router.delete("/{user_id}", response_model=SMTPResponse, status_code=200, description="Delete SMTP for user")
67 async def delete_smtp(user_id: int):
68 users = select_all_users()
69 if not any(x.id == user_id for x in users):
59 - raise HTTPException(status_code=400, detail='User not found')
70 + raise HTTPException(status_code=400, detail="User not found")
71 smtp_found = session.query(SMTP).filter(SMTP.user_id == user_id).first()
72 if not smtp_found:
62 - raise HTTPException(status_code=400, detail='SMTP not found for user')
73 + raise HTTPException(status_code=400, detail="SMTP not found for user")
74 session.delete(smtp_found)
75 session.commit()
76 return {"message": "SMTP deleted successfully", "success": True}
66 -
backend/app/smtp/routes/reports.py
+17 -9
@@ -1,28 +1,36 @@
1 -from fastapi import APIRouter, HTTPException, Security, security, Depends
1 +from fastapi import APIRouter
2 +from fastapi import Depends
3 +from fastapi import HTTPException
4 +from fastapi import Security
5 +from fastapi import security
6 from fastapi.security import HTTPAuthorizationCredentials
3 -from app.smtp.schema.configure import SMTPResponse
7 +from loguru import logger
8
9 +from app.auth.models.users import SMTP
10 +from app.auth.models.users import SMTPInput
11 +from app.auth.models.users import User
12 +from app.auth.services.universal import find_user
13 +from app.auth.services.universal import select_all_users
14 from app.auth.utils import AuthHandler
15 from app.db.db_session import session
7 -from app.auth.models.users import SMTP, SMTPInput, User
8 -from app.auth.services.universal import select_all_users, find_user
9 -from loguru import logger
16 +from app.smtp.schema.configure import SMTPResponse
17
18 smtp_reports_router = APIRouter()
19
20 +
21 # ! TODO: Add SMTP reporting all things. Example is in the services/reports.py and services/create_report.py file
14 -@smtp_reports_router.post('/{user_id}/register', response_model=SMTPResponse, status_code=200, description='Register new SMTP for user')
22 +@smtp_reports_router.post("/{user_id}/register", response_model=SMTPResponse, status_code=200, description="Register new SMTP for user")
23 async def register(user_id: int, smtp: SMTPInput):
24 users = select_all_users()
25 logger.info(users)
26 if not any(x.id == user_id for x in users):
19 - raise HTTPException(status_code=400, detail='User not found')
27 + raise HTTPException(status_code=400, detail="User not found")
28 # Check if SMTP already exists for user
29 smtp_found = session.query(SMTP).filter(SMTP.user_id == user_id).first()
30 if smtp_found:
23 - raise HTTPException(status_code=400, detail='SMTP already exists for user')
31 + raise HTTPException(status_code=400, detail="SMTP already exists for user")
32 hashed_pwd = auth_handler.get_password_hash(smtp.smtp_password)
33 u = SMTP(email=smtp.email, smtp_password=hashed_pwd, smtp_server=smtp.smtp_server, smtp_port=smtp.smtp_port, user_id=user_id)
34 session.add(u)
35 session.commit()
28 - return {"message": "SMTP created successfully", "success": True}
\ No newline at end of file
36 + return {"message": "SMTP created successfully", "success": True}
backend/app/smtp/schema/configure.py
+2 -1
@@ -1,5 +1,6 @@
1 from pydantic import BaseModel
2
3 +
4 class SMTPResponse(BaseModel):
5 message: str
5 - success: bool
\ No newline at end of file
6 + success: bool
backend/app/smtp/services/reports.py
+2 -2
@@ -9,9 +9,9 @@ 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
11
12 -
12 # ! SEND REPORT
13
14 +
15 class EmailReportSender:
16 """
17 Class for sending an email report with PDF attachments.
@@ -113,4 +113,4 @@ class EmailReportSender:
113 text = msg.as_string()
114 server.sendmail(credentials["email"], self.to_email, text)
115
116 - return {"message": "Report sent successfully", "success": True}
\ No newline at end of file
116 + return {"message": "Report sent successfully", "success": True}
backend/copilot.py
+35 -23
@@ -1,38 +1,45 @@
1 -from fastapi import FastAPI, HTTPException, Request
2 -from fastapi.responses import JSONResponse
3 -from sqlmodel import create_engine, SQLModel
4 -from app.db.db_session import engine
1 import uvicorn
2 +from fastapi import FastAPI
3 +from fastapi import HTTPException
4 +from fastapi import Request
5 from fastapi.middleware.cors import CORSMiddleware
7 -from settings import SQLALCHEMY_DATABASE_URI
8 -from app.auth.routes.auth import user_router
9 -from app.connectors.routes import connector_router
10 -#from app.connectors.wazuh_indexer.routes.routes import wazuh_indexer_router
11 -from app.connectors.wazuh_indexer.routes.monitoring import wazuh_indexer_router
12 -from app.connectors.wazuh_manager.routes.rules import wazuh_manager_router
6 +from fastapi.responses import JSONResponse
7 +from sqlmodel import SQLModel
8 +from sqlmodel import create_engine
9 +
10 from app.agents.routes.agents import agents_router
14 -from app.connectors.graylog.routes.monitoring import graylog_monitoring_router
11 +from app.auth.routes.auth import user_router
12 +from app.connectors.cortex.routes.analyzers import cortex_analyzer_router
13 +from app.connectors.dfir_iris.routes.alerts import dfir_iris_alerts_router
14 +from app.connectors.dfir_iris.routes.assets import assets_router
15 +from app.connectors.dfir_iris.routes.cases import cases_router
16 +from app.connectors.dfir_iris.routes.notes import notes_router
17 +from app.connectors.dfir_iris.routes.users import dfir_iris_users_router
18 from app.connectors.graylog.routes.collector import graylog_collector_router
19 from app.connectors.graylog.routes.events import graylog_events_router
20 +from app.connectors.graylog.routes.management import graylog_management_router
21 +from app.connectors.graylog.routes.monitoring import graylog_monitoring_router
22 from app.connectors.graylog.routes.pipelines import graylog_pipelines_router
23 from app.connectors.graylog.routes.streams import graylog_streams_router
19 -from app.connectors.graylog.routes.management import graylog_management_router
20 -from app.connectors.wazuh_indexer.routes.alerts import wazuh_indexer_alerts_router
21 -from app.connectors.dfir_iris.routes.cases import cases_router
22 -from app.connectors.dfir_iris.routes.notes import notes_router
23 -from app.connectors.dfir_iris.routes.assets import assets_router
24 -from app.connectors.dfir_iris.routes.alerts import dfir_iris_alerts_router
25 -from app.connectors.dfir_iris.routes.users import dfir_iris_users_router
26 -from app.connectors.cortex.routes.analyzers import cortex_analyzer_router
27 -from app.connectors.velociraptor.routes.artifacts import velociraptor_artifacts_router
24 +from app.connectors.routes import connector_router
25 from app.connectors.shuffle.routes.workflows import shuffle_workflows_router
26 from app.connectors.sublime.routes.alerts import sublime_alerts_router
27 +from app.connectors.velociraptor.routes.artifacts import velociraptor_artifacts_router
28 +from app.connectors.wazuh_indexer.routes.alerts import wazuh_indexer_alerts_router
29 +
30 +# from app.connectors.wazuh_indexer.routes.routes import wazuh_indexer_router
31 +from app.connectors.wazuh_indexer.routes.monitoring import wazuh_indexer_router
32 +from app.connectors.wazuh_manager.routes.rules import wazuh_manager_router
33 from app.customers.routes.customers import customers_router
34 +from app.db.db_session import engine
35 +from app.db.db_setup import create_tables
36 from app.healthchecks.agents.routes.agents import healtcheck_agents_router
32 -from app.smtp.routes.configure import smtp_router
37 +from app.integrations.alert_escalation.routes.general_alert import (
38 + integration_general_alerts_router,
39 +)
40 from app.integrations.dnstwist.routes.analyze import dnstwist_router
34 -from app.integrations.alert_escalation.routes.general_alert import integration_general_alerts_router
35 -from app.db.db_setup import create_tables
41 +from app.smtp.routes.configure import smtp_router
42 +from settings import SQLALCHEMY_DATABASE_URI
43
44 app = FastAPI(description="CoPilot API", version="0.1.0", title="CoPilot API")
45
@@ -45,6 +52,7 @@ app.add_middleware(
52 allow_headers=["*"],
53 )
54
55 +
56 @app.exception_handler(HTTPException)
57 async def custom_http_exception_handler(request: Request, exc: HTTPException):
58 return JSONResponse(
@@ -55,6 +63,7 @@ async def custom_http_exception_handler(request: Request, exc: HTTPException):
63 },
64 )
65
66 +
67 app.include_router(connector_router, prefix="/connectors", tags=["connectors"])
68 app.include_router(wazuh_indexer_router, prefix="/wazuh_indexer", tags=["wazuh-indexer"])
69 app.include_router(user_router, prefix="/auth", tags=["auth"])
@@ -82,13 +91,16 @@ app.include_router(smtp_router, prefix="/smtp", tags=["smtp"])
91 app.include_router(dnstwist_router, prefix="/dnstwist", tags=["dnstwist"])
92 app.include_router(integration_general_alerts_router, prefix="/alerts", tags=["alerts"])
93
94 +
95 @app.on_event("startup")
96 async def init_db():
97 create_tables(engine)
98
99 +
100 @app.get("/")
101 def hello():
102 return {"message": "Hello World"}
103
104 +
105 if __name__ == "__main__":
106 uvicorn.run(app, host="localhost", port=5000)
backend/file-store/api.config.yaml
+68 -68
@@ -1,72 +1,72 @@
1 ca_certificate: |
2 - -----BEGIN CERTIFICATE-----
3 - MIIDTDCCAjSgAwIBAgIRAKim5DSDvIpnbc7BN8WxDrAwDQYJKoZIhvcNAQELBQAw
4 - GjEYMBYGA1UEChMPVmVsb2NpcmFwdG9yIENBMB4XDTIzMDYwMjEzMzU0MFoXDTMz
5 - MDUzMDEzMzU0MFowGjEYMBYGA1UEChMPVmVsb2NpcmFwdG9yIENBMIIBIjANBgkq
6 - hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu6aTXL1/NcNsrWbqn0SWZk4d5fbwz48+
7 - SYcbZxM8aseCv7LlZEyA+aOQXyFFzdX+B3E+7+25+bfmEU6B0/9N5S0Qk+bkhw14
8 - 0+Edk9uC9qEW2LDTNAH34T4Znb2ki+OjYgB78MWfKm7AR4KxM1wfgOq9VTQtF3Vi
9 - HwieHyYkvnmwedA6McA/SxwY05XTlCOgrtenDRyDP2fRVPPbj6vVdLHb3EpjxpKP
10 - 0rB/h1hoePaQ0l/AGZ8kWV2seCkmYkf+drbqxzHre6tbzawJjngcu2/FwW2J6yfR
11 - Xcx8ETM7o8iAuSGPWoMAljjND2+bJRz2t6GJibL749tkge2lE5NnRQIDAQABo4GM
12 - MIGJMA4GA1UdDwEB/wQEAwICpDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUH
13 - AwIwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUiXe4VSsY3g0DVOwxHGl7SDhV
14 - zm8wKAYDVR0RBCEwH4IdVmVsb2NpcmFwdG9yX2NhLnZlbG9jaWRleC5jb20wDQYJ
15 - KoZIhvcNAQELBQADggEBABPWrJ0TuRuJYVdvz8qEhW/ZhFC4fs0cPcPvfQBNiBW1
16 - n/6esctopeDw6wW9A+cTf2jHqnBSNosDOcATa+JDR3tbq6qHAbso6FkZlgcmmYkN
17 - qwcmeJMedym7UMQGSkN+PqfLx4nPGrMkDhsmacYM1IJ0mlGwnTmMqeA0/oRXNVEg
18 - x4kgieeYa5J6K48CSyWAgwwCJ03vWJ+n3cpD+hWuVmK1tn3To05AG6gHMUeSK17N
19 - qIz+2JyvBBBlwgTYUUmEYzgjNYKP0Crx57jvJZ8vs/vadpXdU29UOzeViyFEyRV4
20 - JIo2Kx/jaPfLCPd9oE37KekiGBBkCgtxVp6sKHUYX3o=
21 - -----END CERTIFICATE-----
2 + -----BEGIN CERTIFICATE-----
3 + MIIDTDCCAjSgAwIBAgIRAKim5DSDvIpnbc7BN8WxDrAwDQYJKoZIhvcNAQELBQAw
4 + GjEYMBYGA1UEChMPVmVsb2NpcmFwdG9yIENBMB4XDTIzMDYwMjEzMzU0MFoXDTMz
5 + MDUzMDEzMzU0MFowGjEYMBYGA1UEChMPVmVsb2NpcmFwdG9yIENBMIIBIjANBgkq
6 + hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu6aTXL1/NcNsrWbqn0SWZk4d5fbwz48+
7 + SYcbZxM8aseCv7LlZEyA+aOQXyFFzdX+B3E+7+25+bfmEU6B0/9N5S0Qk+bkhw14
8 + 0+Edk9uC9qEW2LDTNAH34T4Znb2ki+OjYgB78MWfKm7AR4KxM1wfgOq9VTQtF3Vi
9 + HwieHyYkvnmwedA6McA/SxwY05XTlCOgrtenDRyDP2fRVPPbj6vVdLHb3EpjxpKP
10 + 0rB/h1hoePaQ0l/AGZ8kWV2seCkmYkf+drbqxzHre6tbzawJjngcu2/FwW2J6yfR
11 + Xcx8ETM7o8iAuSGPWoMAljjND2+bJRz2t6GJibL749tkge2lE5NnRQIDAQABo4GM
12 + MIGJMA4GA1UdDwEB/wQEAwICpDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUH
13 + AwIwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUiXe4VSsY3g0DVOwxHGl7SDhV
14 + zm8wKAYDVR0RBCEwH4IdVmVsb2NpcmFwdG9yX2NhLnZlbG9jaWRleC5jb20wDQYJ
15 + KoZIhvcNAQELBQADggEBABPWrJ0TuRuJYVdvz8qEhW/ZhFC4fs0cPcPvfQBNiBW1
16 + n/6esctopeDw6wW9A+cTf2jHqnBSNosDOcATa+JDR3tbq6qHAbso6FkZlgcmmYkN
17 + qwcmeJMedym7UMQGSkN+PqfLx4nPGrMkDhsmacYM1IJ0mlGwnTmMqeA0/oRXNVEg
18 + x4kgieeYa5J6K48CSyWAgwwCJ03vWJ+n3cpD+hWuVmK1tn3To05AG6gHMUeSK17N
19 + qIz+2JyvBBBlwgTYUUmEYzgjNYKP0Crx57jvJZ8vs/vadpXdU29UOzeViyFEyRV4
20 + JIo2Kx/jaPfLCPd9oE37KekiGBBkCgtxVp6sKHUYX3o=
21 + -----END CERTIFICATE-----
22 client_cert: |
23 - -----BEGIN CERTIFICATE-----
24 - MIIDWjCCAkKgAwIBAgIQS/Lj2MAoxGZCF1if5sfkUzANBgkqhkiG9w0BAQsFADAa
25 - MRgwFgYDVQQKEw9WZWxvY2lyYXB0b3IgQ0EwHhcNMjMwNjAyMTM0MzMyWhcNMjQw
26 - NjAxMTM0MzMyWjA1MRUwEwYDVQQKEwxWZWxvY2lyYXB0b3IxHDAaBgNVBAMME2lu
27 - Zm9Ac29jZm9ydHJlc3MuY28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
28 - AQC63lhkXRY+x6ceGQv008OrmZuG50xORhG6iYCABtkwd3scQmMTIGVMPL6Bergr
29 - s1F/d+jDVJnannrjwdY0yiP1yIgNFNVEr7li5IeSsJak58qMs2cGI6Yb3BajbtUf
30 - dFXCIADF3wUJNJEoXLOdpOL4sVsrAk9tX8XErd8iSjUqNIFykMD26YTxtM8cyQgB
31 - X9XYO0OIdMJW3TSQeQYDmS22C9v9k3wNVc3Sz2TOgxrcbbva6nA++OlYQnBfta3n
32 - QwZybJWvbUhkxrnd8Wlsu+Lvab7DGarYvaMb7c5D/YV3jyWv7K4wuFPYDmU+yuDS
33 - uBP+fF3YNXX7KUm9ki9VQBzZAgMBAAGjgYAwfjAOBgNVHQ8BAf8EBAMCBaAwHQYD
34 - VR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHwYDVR0j
35 - BBgwFoAUiXe4VSsY3g0DVOwxHGl7SDhVzm8wHgYDVR0RBBcwFYITaW5mb0Bzb2Nm
36 - b3J0cmVzcy5jbzANBgkqhkiG9w0BAQsFAAOCAQEAYYex+11Q1n5hFen2R+4kwHAy
37 - 7EhhLOhVuPDqVrQ+cYbUVWqJmf+x+HPpl8W/8bfoOefCy7r09Bpi8q7yBAKH6q3J
38 - NqkZJDByaE92UAa64M5W16ptomsA51hh4PWEsLb55IVFw0h7thSl/2daaJX1x24U
39 - oyac+rm/99NwLctJ1zBCnOWufPcwxmMLDKkBbO/cXDvgOLafqeYdagO9ssEWSI5T
40 - FSl10PNE3c2cslDwnUU5/HZ99xRa+wDv/l7zcoe81+vgGHzIAm14aOVSnBukB+Ht
41 - evdf3hnkPXNPUPq/yt/iV3VD7Dlb0AMU8ofQl9niuaX/ZSLoueSAXrLaOJgFqA==
42 - -----END CERTIFICATE-----
23 + -----BEGIN CERTIFICATE-----
24 + MIIDWjCCAkKgAwIBAgIQS/Lj2MAoxGZCF1if5sfkUzANBgkqhkiG9w0BAQsFADAa
25 + MRgwFgYDVQQKEw9WZWxvY2lyYXB0b3IgQ0EwHhcNMjMwNjAyMTM0MzMyWhcNMjQw
26 + NjAxMTM0MzMyWjA1MRUwEwYDVQQKEwxWZWxvY2lyYXB0b3IxHDAaBgNVBAMME2lu
27 + Zm9Ac29jZm9ydHJlc3MuY28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
28 + AQC63lhkXRY+x6ceGQv008OrmZuG50xORhG6iYCABtkwd3scQmMTIGVMPL6Bergr
29 + s1F/d+jDVJnannrjwdY0yiP1yIgNFNVEr7li5IeSsJak58qMs2cGI6Yb3BajbtUf
30 + dFXCIADF3wUJNJEoXLOdpOL4sVsrAk9tX8XErd8iSjUqNIFykMD26YTxtM8cyQgB
31 + X9XYO0OIdMJW3TSQeQYDmS22C9v9k3wNVc3Sz2TOgxrcbbva6nA++OlYQnBfta3n
32 + QwZybJWvbUhkxrnd8Wlsu+Lvab7DGarYvaMb7c5D/YV3jyWv7K4wuFPYDmU+yuDS
33 + uBP+fF3YNXX7KUm9ki9VQBzZAgMBAAGjgYAwfjAOBgNVHQ8BAf8EBAMCBaAwHQYD
34 + VR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHwYDVR0j
35 + BBgwFoAUiXe4VSsY3g0DVOwxHGl7SDhVzm8wHgYDVR0RBBcwFYITaW5mb0Bzb2Nm
36 + b3J0cmVzcy5jbzANBgkqhkiG9w0BAQsFAAOCAQEAYYex+11Q1n5hFen2R+4kwHAy
37 + 7EhhLOhVuPDqVrQ+cYbUVWqJmf+x+HPpl8W/8bfoOefCy7r09Bpi8q7yBAKH6q3J
38 + NqkZJDByaE92UAa64M5W16ptomsA51hh4PWEsLb55IVFw0h7thSl/2daaJX1x24U
39 + oyac+rm/99NwLctJ1zBCnOWufPcwxmMLDKkBbO/cXDvgOLafqeYdagO9ssEWSI5T
40 + FSl10PNE3c2cslDwnUU5/HZ99xRa+wDv/l7zcoe81+vgGHzIAm14aOVSnBukB+Ht
41 + evdf3hnkPXNPUPq/yt/iV3VD7Dlb0AMU8ofQl9niuaX/ZSLoueSAXrLaOJgFqA==
42 + -----END CERTIFICATE-----
43 client_private_key: |
44 - -----BEGIN RSA PRIVATE KEY-----
45 - MIIEpAIBAAKCAQEAut5YZF0WPsenHhkL9NPDq5mbhudMTkYRuomAgAbZMHd7HEJj
46 - EyBlTDy+gXq4K7NRf3fow1SZ2p5648HWNMoj9ciIDRTVRK+5YuSHkrCWpOfKjLNn
47 - BiOmG9wWo27VH3RVwiAAxd8FCTSRKFyznaTi+LFbKwJPbV/FxK3fIko1KjSBcpDA
48 - 9umE8bTPHMkIAV/V2DtDiHTCVt00kHkGA5kttgvb/ZN8DVXN0s9kzoMa3G272upw
49 - PvjpWEJwX7Wt50MGcmyVr21IZMa53fFpbLvi72m+wxmq2L2jG+3OQ/2Fd48lr+yu
50 - MLhT2A5lPsrg0rgT/nxd2DV1+ylJvZIvVUAc2QIDAQABAoIBAFoBajW9GE/Yvkei
51 - 7L1Zmi925xBNK9Wvri5YuEnyAn5zrhpoZ2v4+JGF2IRo5Xg3AJQS30vl3c0M9Efr
52 - Pw9iJXvmwJD8bdSNhw9430vqPkTjWA35AzBTz1gv47+ITKK/1+aOn5Cu4LAUX64/
53 - KExP9PqwAidvD26w6ILY9FaBw6W1nZy5ZfpZi4RnnKmnBxTpzO0yTBgT9nBlRhYU
54 - BmtDq5PcO0G8THKso1N/1IT2mKTA+475OjSrnJpXb43itiNazz+u5c4WMnlGRbBY
55 - qPlu+pqQ99Jf6TefsILRMEDBNsYiIydwqfp2CRgZWblMero8wVuDIG5fwzqWvtof
56 - 71lDJ3kCgYEA49E9F5lenpSx2tyTNS+Y8RP16mwUkvKPuxBIT63kzaRCh+1XLeNg
57 - TNdnkXHLEohsdLYewITsgNOrwMtMMWguSAlbyK0VgZvgESpAlrwWR6RSTSHJeLNW
58 - 2yj7ZkUgkSooxGv3KCh4tSJ4cwzT0N8YtHwB/U/G8w4A+fv355spmgMCgYEA0fxL
59 - 5xnHg9fzKdTWIsjQ+9byRmwhAQxkhPDrE2kq+A31xz3l/0+IKjhiIdxHrg2Qi+aN
60 - noSnXRl0j1LdbTNPTyt2AF6u59yR3owYS7kIc22kw/OuNdDKa1hTAPd4mucwvwlf
61 - vJp0kMqek5hPwMKHYxNunji5oSAKLeoQrJKzpPMCgYAJksdgcH97ZoA61D4TZBan
62 - OtGAsl4C9tJ3Z+3B+2q8AYUSNTUOpplrYTnm8MM52iXEmcqdCHjvyPVUurZO9TPM
63 - ryf+PNfEhIpb7kyciPbet9cFir/upIqn1wcJeyotL3pbFrZiJ6E662HoY8ea5WUi
64 - YHus62dO223LE32NbAXJhQKBgQCNyJgLm/l+SWLTvPU1mXiag0ElUb9bMN3ycaWY
65 - fqtXwD1S4bWZlT7wmw+Po2f22wvdmrfG7/T9xMFSQPZn1HxZjZPongXlYqZPqTKC
66 - dpaBMehNswzPI4J5xrKM9YvGtBHS++Zbt8K7PUSjjfVTx0WZHTbUuKKGa9bTt7a4
67 - f3mzBwKBgQCaL8PuI05KZ0WxzY2b4WFErMBO01iG4ueVhslCn1c1w5GNlRDRh/Gy
68 - ocit+c0jeOBkA+JjkyFjf7KhgzPXJQ/BmS1UeaNStUy/TPY/gx3mjuMmli+i/Z7V
69 - drZUWaGTet9/lZLvp1jYg2shnsRyaexXUV9wlrkPft5TJhHNKvAVOw==
70 - -----END RSA PRIVATE KEY-----
44 + -----BEGIN RSA PRIVATE KEY-----
45 + MIIEpAIBAAKCAQEAut5YZF0WPsenHhkL9NPDq5mbhudMTkYRuomAgAbZMHd7HEJj
46 + EyBlTDy+gXq4K7NRf3fow1SZ2p5648HWNMoj9ciIDRTVRK+5YuSHkrCWpOfKjLNn
47 + BiOmG9wWo27VH3RVwiAAxd8FCTSRKFyznaTi+LFbKwJPbV/FxK3fIko1KjSBcpDA
48 + 9umE8bTPHMkIAV/V2DtDiHTCVt00kHkGA5kttgvb/ZN8DVXN0s9kzoMa3G272upw
49 + PvjpWEJwX7Wt50MGcmyVr21IZMa53fFpbLvi72m+wxmq2L2jG+3OQ/2Fd48lr+yu
50 + MLhT2A5lPsrg0rgT/nxd2DV1+ylJvZIvVUAc2QIDAQABAoIBAFoBajW9GE/Yvkei
51 + 7L1Zmi925xBNK9Wvri5YuEnyAn5zrhpoZ2v4+JGF2IRo5Xg3AJQS30vl3c0M9Efr
52 + Pw9iJXvmwJD8bdSNhw9430vqPkTjWA35AzBTz1gv47+ITKK/1+aOn5Cu4LAUX64/
53 + KExP9PqwAidvD26w6ILY9FaBw6W1nZy5ZfpZi4RnnKmnBxTpzO0yTBgT9nBlRhYU
54 + BmtDq5PcO0G8THKso1N/1IT2mKTA+475OjSrnJpXb43itiNazz+u5c4WMnlGRbBY
55 + qPlu+pqQ99Jf6TefsILRMEDBNsYiIydwqfp2CRgZWblMero8wVuDIG5fwzqWvtof
56 + 71lDJ3kCgYEA49E9F5lenpSx2tyTNS+Y8RP16mwUkvKPuxBIT63kzaRCh+1XLeNg
57 + TNdnkXHLEohsdLYewITsgNOrwMtMMWguSAlbyK0VgZvgESpAlrwWR6RSTSHJeLNW
58 + 2yj7ZkUgkSooxGv3KCh4tSJ4cwzT0N8YtHwB/U/G8w4A+fv355spmgMCgYEA0fxL
59 + 5xnHg9fzKdTWIsjQ+9byRmwhAQxkhPDrE2kq+A31xz3l/0+IKjhiIdxHrg2Qi+aN
60 + noSnXRl0j1LdbTNPTyt2AF6u59yR3owYS7kIc22kw/OuNdDKa1hTAPd4mucwvwlf
61 + vJp0kMqek5hPwMKHYxNunji5oSAKLeoQrJKzpPMCgYAJksdgcH97ZoA61D4TZBan
62 + OtGAsl4C9tJ3Z+3B+2q8AYUSNTUOpplrYTnm8MM52iXEmcqdCHjvyPVUurZO9TPM
63 + ryf+PNfEhIpb7kyciPbet9cFir/upIqn1wcJeyotL3pbFrZiJ6E662HoY8ea5WUi
64 + YHus62dO223LE32NbAXJhQKBgQCNyJgLm/l+SWLTvPU1mXiag0ElUb9bMN3ycaWY
65 + fqtXwD1S4bWZlT7wmw+Po2f22wvdmrfG7/T9xMFSQPZn1HxZjZPongXlYqZPqTKC
66 + dpaBMehNswzPI4J5xrKM9YvGtBHS++Zbt8K7PUSjjfVTx0WZHTbUuKKGa9bTt7a4
67 + f3mzBwKBgQCaL8PuI05KZ0WxzY2b4WFErMBO01iG4ueVhslCn1c1w5GNlRDRh/Gy
68 + ocit+c0jeOBkA+JjkyFjf7KhgzPXJQ/BmS1UeaNStUy/TPY/gx3mjuMmli+i/Z7V
69 + drZUWaGTet9/lZLvp1jYg2shnsRyaexXUV9wlrkPft5TJhHNKvAVOw==
70 + -----END RSA PRIVATE KEY-----
71 api_connection_string: ashvlo01.socfortress.local:8001
72 -name: info@socfortress.co
\ No newline at end of file
72 +name: info@socfortress.co
backend/settings.py
+1 -1
@@ -23,4 +23,4 @@ SQLALCHEMY_TRACK_MODIFICATIONS = env.bool(
23 "SQLALCHEMY_TRACK_MODIFICATIONS",
24 default=False,
25 )
26 -UPLOAD_FOLDER = env.str("UPLOAD_FOLDER", str(Path.home() / "Desktop/copilot_uploads"))
\ No newline at end of file
26 +UPLOAD_FOLDER = env.str("UPLOAD_FOLDER", str(Path.home() / "Desktop/copilot_uploads"))
unplugin.components.d.ts
+35 -35
@@ -5,39 +5,39 @@
5 // Read more: https://github.com/vuejs/core/pull/3399
6 export {}
7
8 -declare module 'vue' {
9 - export interface GlobalComponents {
10 - CardActions: typeof import('./src/components/cards/CardActions.vue')['default']
11 - CardBasic1: typeof import('./src/components/cards/basic/CardBasic1.vue')['default']
12 - CardBasic2: typeof import('./src/components/cards/basic/CardBasic2.vue')['default']
13 - CardBasic3: typeof import('./src/components/cards/basic/CardBasic3.vue')['default']
14 - CardBasic4: typeof import('./src/components/cards/basic/CardBasic4.vue')['default']
15 - CardBasic5: typeof import('./src/components/cards/basic/CardBasic5.vue')['default']
16 - CardBasic6: typeof import('./src/components/cards/basic/CardBasic6.vue')['default']
17 - CardCodeExample: typeof import('./src/components/cards/CardCodeExample.vue')['default']
18 - CardCombo1: typeof import('./src/components/cards/combo/CardCombo1.vue')['default']
19 - CardCombo2: typeof import('./src/components/cards/combo/CardCombo2.vue')['default']
20 - CardCombo3: typeof import('./src/components/cards/combo/CardCombo3.vue')['default']
21 - CardCombo4: typeof import('./src/components/cards/combo/CardCombo4.vue')['default']
22 - CardCombo5: typeof import('./src/components/cards/combo/CardCombo5.vue')['default']
23 - CardCombo6: typeof import('./src/components/cards/combo/CardCombo6.vue')['default']
24 - CardCombo7: typeof import('./src/components/cards/combo/CardCombo7.vue')['default']
25 - CardCombo8: typeof import('./src/components/cards/combo/CardCombo8.vue')['default']
26 - CardComboIcon: typeof import('./src/components/cards/combo/CardComboIcon.vue')['default']
27 - CardEcommerce1: typeof import('./src/components/cards/ecommerce/CardEcommerce1.vue')['default']
28 - CardEcommerce2: typeof import('./src/components/cards/ecommerce/CardEcommerce2.vue')['default']
29 - CardEcommerce3: typeof import('./src/components/cards/ecommerce/CardEcommerce3.vue')['default']
30 - CardEcommerce4: typeof import('./src/components/cards/ecommerce/CardEcommerce4.vue')['default']
31 - CardExtra1: typeof import('./src/components/cards/extra/CardExtra1.vue')['default']
32 - CardExtra2: typeof import('./src/components/cards/extra/CardExtra2.vue')['default']
33 - CardExtra3: typeof import('./src/components/cards/extra/CardExtra3.vue')['default']
34 - CardExtra4: typeof import('./src/components/cards/extra/CardExtra4.vue')['default']
35 - CardExtra5: typeof import('./src/components/cards/extra/CardExtra5.vue')['default']
36 - CardExtra6: typeof import('./src/components/cards/extra/CardExtra6.vue')['default']
37 - CardExtra7: typeof import('./src/components/cards/extra/CardExtra7.vue')['default']
38 - CardSocial1: typeof import('./src/components/cards/social/CardSocial1.vue')['default']
39 - CardWrapper: typeof import('./src/components/cards/CardWrapper.vue')['default']
40 - RouterLink: typeof import('vue-router')['RouterLink']
41 - RouterView: typeof import('vue-router')['RouterView']
42 - }
8 +declare module "vue" {
9 + export interface GlobalComponents {
10 + CardActions: typeof import("./src/components/cards/CardActions.vue")["default"]
11 + CardBasic1: typeof import("./src/components/cards/basic/CardBasic1.vue")["default"]
12 + CardBasic2: typeof import("./src/components/cards/basic/CardBasic2.vue")["default"]
13 + CardBasic3: typeof import("./src/components/cards/basic/CardBasic3.vue")["default"]
14 + CardBasic4: typeof import("./src/components/cards/basic/CardBasic4.vue")["default"]
15 + CardBasic5: typeof import("./src/components/cards/basic/CardBasic5.vue")["default"]
16 + CardBasic6: typeof import("./src/components/cards/basic/CardBasic6.vue")["default"]
17 + CardCodeExample: typeof import("./src/components/cards/CardCodeExample.vue")["default"]
18 + CardCombo1: typeof import("./src/components/cards/combo/CardCombo1.vue")["default"]
19 + CardCombo2: typeof import("./src/components/cards/combo/CardCombo2.vue")["default"]
20 + CardCombo3: typeof import("./src/components/cards/combo/CardCombo3.vue")["default"]
21 + CardCombo4: typeof import("./src/components/cards/combo/CardCombo4.vue")["default"]
22 + CardCombo5: typeof import("./src/components/cards/combo/CardCombo5.vue")["default"]
23 + CardCombo6: typeof import("./src/components/cards/combo/CardCombo6.vue")["default"]
24 + CardCombo7: typeof import("./src/components/cards/combo/CardCombo7.vue")["default"]
25 + CardCombo8: typeof import("./src/components/cards/combo/CardCombo8.vue")["default"]
26 + CardComboIcon: typeof import("./src/components/cards/combo/CardComboIcon.vue")["default"]
27 + CardEcommerce1: typeof import("./src/components/cards/ecommerce/CardEcommerce1.vue")["default"]
28 + CardEcommerce2: typeof import("./src/components/cards/ecommerce/CardEcommerce2.vue")["default"]
29 + CardEcommerce3: typeof import("./src/components/cards/ecommerce/CardEcommerce3.vue")["default"]
30 + CardEcommerce4: typeof import("./src/components/cards/ecommerce/CardEcommerce4.vue")["default"]
31 + CardExtra1: typeof import("./src/components/cards/extra/CardExtra1.vue")["default"]
32 + CardExtra2: typeof import("./src/components/cards/extra/CardExtra2.vue")["default"]
33 + CardExtra3: typeof import("./src/components/cards/extra/CardExtra3.vue")["default"]
34 + CardExtra4: typeof import("./src/components/cards/extra/CardExtra4.vue")["default"]
35 + CardExtra5: typeof import("./src/components/cards/extra/CardExtra5.vue")["default"]
36 + CardExtra6: typeof import("./src/components/cards/extra/CardExtra6.vue")["default"]
37 + CardExtra7: typeof import("./src/components/cards/extra/CardExtra7.vue")["default"]
38 + CardSocial1: typeof import("./src/components/cards/social/CardSocial1.vue")["default"]
39 + CardWrapper: typeof import("./src/components/cards/CardWrapper.vue")["default"]
40 + RouterLink: typeof import("vue-router")["RouterLink"]
41 + RouterView: typeof import("vue-router")["RouterView"]
42 + }
43 }