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
}