@cryptotaxi247 / CoPilot / commits / 95f21932

Wazuh agent upgrade (#225)

* feat: Add endpoint for upgrading Wazuh agent This commit adds a new endpoint `/upgrade` to upgrade a Wazuh agent in the `agents.py` file. The endpoint requires the `agent_id` as a parameter and upgrades the corresponding agent. The upgrade process may take a few minutes to complete. This feature improves the functionality of the application by allowing users to easily upgrade Wazuh agents. * added Upgrade wazuh agent button * updated Upgrade Wazuh Agent button * feat: Add endpoint for purging monitoring alerts This commit adds a new endpoint `/purge` to the `monitoring_alert.py` file. The endpoint allows users with admin or analyst scopes to purge all monitoring alerts from the database. It retrieves all monitoring alerts, deletes them from the database, and returns the purged alerts in the response. This feature improves the functionality of the application by providing a convenient way to remove all monitoring alerts when needed. * refactor: Update monitoring alert delete endpoint URL This commit updates the URL for the delete endpoint in the `monitoringAlerts.ts` file. The previous URL was `/monitoring_alert/{alertId}`, and it has been changed to `/monitoring_alert/single_alert/{alertId}`. This change improves the clarity and consistency of the endpoint URL, making it more descriptive and aligned with the purpose of the endpoint. * added monitoring_alert purge button * refactor * refactor: Update get_current_process_names function in wazuh.py This commit updates the `get_current_process_names` function in the `wazuh.py` file. The function now uses the `get` method with default values to safely retrieve the `process_name` from the alert context. This change improves the robustness of the function and ensures that it returns an empty list if the necessary data is not available. --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed May 29, 2024 at 09:36 UTC 95f21932ce66e2a77a801aa20438fd863f2b2f2d
15 files changed +279 -47
.vscode/extensions.json
+2 -2
@@ -9,9 +9,9 @@
9 "esbenp.prettier-vscode",
10 "ms-vscode.remtoe-remote-wsl",
11 "dbaeumer.vscode-eslint",
12 - "Vue.volar",
12 "Gruntfuggly.todo-tree",
13 "usernamehw.errorlens",
15 - "streetsidesoftware.code-spell-checker"
14 + "streetsidesoftware.code-spell-checker",
15 + "vue.volar"
16 ]
17 }
backend/app/agents/routes/agents.py
+40 -2
@@ -11,7 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
11 from sqlalchemy.future import select
12
13 from app.agents.dfir_iris.services.cases import collect_agent_soc_cases
14 -from app.agents.schema.agents import AgentModifyResponse
14 +from app.agents.schema.agents import AgentModifyResponse, AgentWazuhUpgradeResponse
15 from app.agents.schema.agents import AgentsResponse
16 from app.agents.schema.agents import OutdatedVelociraptorAgentsResponse
17 from app.agents.schema.agents import OutdatedWazuhAgentsResponse
@@ -24,7 +24,7 @@ from app.agents.velociraptor.services.agents import delete_agent_velociraptor
24 from app.agents.wazuh.schema.agents import WazuhAgentScaPolicyResultsResponse
25 from app.agents.wazuh.schema.agents import WazuhAgentScaResponse
26 from app.agents.wazuh.schema.agents import WazuhAgentVulnerabilitiesResponse
27 -from app.agents.wazuh.services.agents import delete_agent_wazuh
27 +from app.agents.wazuh.services.agents import delete_agent_wazuh, upgrade_wazuh_agent
28 from app.agents.wazuh.services.sca import collect_agent_sca
29 from app.agents.wazuh.services.sca import collect_agent_sca_policy_results
30 from app.agents.wazuh.services.vulnerabilities import collect_agent_vulnerabilities
@@ -348,6 +348,44 @@ async def mark_agent_as_not_critical(
348 detail=f"Failed to mark agent as not critical: {str(e)}",
349 )
350
351 +@agents_router.post(
352 + "/{agent_id}/wazuh/upgrade",
353 + response_model=AgentModifyResponse,
354 + description="Upgrade wazuh agent",
355 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
356 +)
357 +async def upgrade_wazuh_agent_route(
358 + agent_id: str,
359 + session: AsyncSession = Depends(get_db),
360 +) -> AgentWazuhUpgradeResponse:
361 + """
362 + Upgrade Wazuh agent.
363 +
364 + Args:
365 + agent_id (str): The ID of the agent to be upgraded.
366 + session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
367 +
368 + Returns:
369 + AgentModifyResponse: The response indicating the success or failure of the operation.
370 + """
371 + logger.info(f"Upgrading Wazuh agent {agent_id}")
372 + try:
373 + result = await session.execute(select(Agents).filter(Agents.agent_id == agent_id))
374 + agent = result.scalars().first()
375 + if not agent:
376 + raise HTTPException(status_code=404, detail=f"Agent with agent_id {agent_id} not found")
377 + return await upgrade_wazuh_agent(agent_id)
378 + return AgentWazuhUpgradeResponse(
379 + success=True,
380 + message=f"Agent {agent_id} upgraded successfully started. Upgrade may take a few minutes to complete.",
381 + )
382 + except Exception as e:
383 + logger.error(f"Failed to upgrade Wazuh agent {agent_id}: {e}")
384 + raise HTTPException(
385 + status_code=500,
386 + detail=f"Failed to upgrade Wazuh agent {agent_id}: {e}",
387 + )
388 +
389
390 @agents_router.get(
391 "/{agent_id}/vulnerabilities",
backend/app/agents/schema/agents.py
+4
@@ -52,3 +52,7 @@ class AgentUpdateCustomerCodeBody(BaseModel):
52 class AgentUpdateCustomerCodeResponse(BaseModel):
53 success: bool
54 message: str
55 +
56 +class AgentWazuhUpgradeResponse(BaseModel):
57 + success: bool
58 + message: str
backend/app/agents/wazuh/services/agents.py
+64 -2
@@ -3,11 +3,11 @@ import asyncio
3 from fastapi import HTTPException
4 from loguru import logger
5
6 -from app.agents.schema.agents import AgentModifyResponse
6 +from app.agents.schema.agents import AgentModifyResponse, AgentWazuhUpgradeResponse
7 from app.agents.wazuh.schema.agents import WazuhAgent
8 from app.agents.wazuh.schema.agents import WazuhAgentsList
9 from app.connectors.wazuh_manager.utils.universal import send_delete_request
10 -from app.connectors.wazuh_manager.utils.universal import send_get_request
10 +from app.connectors.wazuh_manager.utils.universal import send_get_request, send_put_request
11
12
13 async def collect_wazuh_agents() -> WazuhAgentsList:
@@ -145,3 +145,65 @@ async def delete_agent_wazuh(agent_id: str) -> AgentModifyResponse:
145 status_code=500,
146 detail=f"Failed to delete agent {agent_id} from Wazuh Manager: {e}",
147 )
148 +
149 +def handle_agent_upgrade_response(agent_upgraded: dict) -> AgentWazuhUpgradeResponse:
150 + """
151 + Handle the response from the agent upgrade request.
152 +
153 + Args:
154 + agent_upgraded (dict): The response from the agent upgrade request.
155 +
156 + Returns:
157 + AgentWazuhUpgradeResponse: The response indicating the status of the agent upgrade.
158 + """
159 + data = agent_upgraded.get('data', {}).get('data', {})
160 + total_failed_items = data.get('total_failed_items', 0)
161 +
162 + if total_failed_items == 0:
163 + # Upgrade was successful
164 + return AgentWazuhUpgradeResponse(
165 + success=True,
166 + message=agent_upgraded.get('data', {}).get('message', 'Unknown error'),
167 + )
168 + else:
169 + # Upgrade failed
170 + failed_items = data.get('failed_items', [{}])
171 + error_message = failed_items[0].get('error', {}).get('message', 'Unknown error')
172 + return AgentWazuhUpgradeResponse(
173 + success=False,
174 + message=error_message,
175 + )
176 +
177 +async def upgrade_wazuh_agent(agent_id: str) -> AgentWazuhUpgradeResponse:
178 + """Upgrade agent from Wazuh Manager.
179 +
180 + Args:
181 + agent_id (str): The ID of the agent to be upgraded.
182 +
183 + Returns:
184 + AgentWazuhUpgradeResponse: The response indicating the status of the agent upgrade.
185 +
186 + Raises:
187 + HTTPException: If there is an HTTP error during the upgrade process.
188 + """
189 + logger.info(f"Upgrading agent {agent_id} from Wazuh Manager")
190 +
191 + params = {
192 + "agents_list": [agent_id],
193 + }
194 +
195 + try:
196 + agent_upgraded = await send_put_request(endpoint="agents/upgrade", data=None, params=params)
197 + logger.info(f"Agent upgrade response: {agent_upgraded}")
198 + return handle_agent_upgrade_response(agent_upgraded)
199 +
200 + except HTTPException as http_e:
201 + # * Catch any HTTPException and re-raise it
202 + raise http_e
203 +
204 + except Exception as e:
205 + # * Catch-all for other exceptions
206 + raise HTTPException(
207 + status_code=500,
208 + detail=f"Failed to upgrade agent {agent_id} from Wazuh Manager: {e}",
209 + )
backend/app/integrations/monitoring_alert/routes/monitoring_alert.py
+36 -1
@@ -7,6 +7,7 @@ from fastapi import Security
7 from loguru import logger
8 from sqlalchemy.ext.asyncio import AsyncSession
9 from sqlalchemy.future import select
10 +from sqlalchemy.sql.expression import delete
11
12 from app.auth.utils import AuthHandler
13 from app.db.db_session import get_db
@@ -152,7 +153,7 @@ async def invoke_monitoring_alert(
153
154
155 @monitoring_alerts_router.delete(
155 - "/{monitoring_alert_id}",
156 + "/single_alert/{monitoring_alert_id}",
157 response_model=MonitoringAlertsResponseModel,
158 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
159 )
@@ -187,6 +188,40 @@ async def delete_monitoring_alert(
188 message="Monitoring alert deleted successfully",
189 )
190
191 +@monitoring_alerts_router.delete(
192 + "/purge",
193 + response_model=MonitoringAlertsResponseModel,
194 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
195 +)
196 +async def purge_monitoring_alerts(
197 + session: AsyncSession = Depends(get_db),
198 +) -> MonitoringAlertsResponseModel:
199 + """
200 + Purge all monitoring alerts.
201 +
202 + Args:
203 + session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
204 +
205 + Returns:
206 + MonitoringAlertsResponseModel: The monitoring alerts that were purged.
207 + """
208 + logger.info("Purging monitoring alerts")
209 +
210 + monitoring_alerts = await session.execute(select(MonitoringAlerts))
211 + monitoring_alerts = monitoring_alerts.scalars().all()
212 +
213 + if not monitoring_alerts:
214 + raise HTTPException(status_code=404, detail="No monitoring alerts found")
215 +
216 + await session.execute(delete(MonitoringAlerts))
217 + await session.commit()
218 +
219 + return MonitoringAlertsResponseModel(
220 + monitoring_alerts=monitoring_alerts,
221 + success=True,
222 + message="Monitoring alerts purged successfully",
223 + )
224 +
225
226 @monitoring_alerts_router.post("/create", response_model=GraylogPostResponse)
227 async def create_monitoring_alert(
backend/app/integrations/monitoring_alert/services/wazuh.py
+1 -1
@@ -508,7 +508,7 @@ async def get_current_process_names(client, alert_client, iris_alert_id):
508 alert_client.get_alert,
509 iris_alert_id,
510 )
511 - return result["data"]["alert_context"]["process_name"]
511 + return result.get("data", {}).get("alert_context", {}).get("process_name", [])
512
513
514 async def get_current_alert_context(client, alert_client, iris_alert_id):
frontend/src/api/agents.ts
+3
@@ -63,6 +63,9 @@ export default {
63 }
64 )
65 },
66 + upgradeWazuhAgent(agentId: string) {
67 + return HttpClient.post<FlaskBaseResponse>(`/agents/${agentId}/wazuh/upgrade`)
68 + },
69
70 // IGNORE AT THE MOMENT !
71 agentsWazuhOutdated() {
frontend/src/api/monitoringAlerts.ts
+5 -2
@@ -52,7 +52,10 @@ export default {
52 invoke(alertId: number) {
53 return HttpClient.post<FlaskBaseResponse>(`/monitoring_alert/invoke/${alertId}`)
54 },
55 - delete(alertId: number) {
56 - return HttpClient.delete<FlaskBaseResponse>(`/monitoring_alert/${alertId}`)
55 + deleteAlert(alertId: number) {
56 + return HttpClient.delete<FlaskBaseResponse>(`/monitoring_alert/single_alert/${alertId}`)
57 + },
58 + purge() {
59 + return HttpClient.delete<FlaskBaseResponse>(`/monitoring_alert/purge`)
60 }
61 }
frontend/src/components/agents/OverviewSection.vue
+1 -5
@@ -11,11 +11,7 @@
11 </code>
12 </template>
13 <template v-else-if="item.key === 'velociraptor_id'">
14 - <AgentVelociraptorIdForm
15 - v-model:velociraptorId="item.val"
16 - :agent="agent"
17 - @updated="emit('updated')"
18 - />
14 + <AgentVelociraptorIdForm v-model:velociraptorId="item.val" :agent @updated="emit('updated')" />
15 </template>
16 <template v-else>
17 {{ item.val ?? "-" }}
frontend/src/components/agents/sca/ScaItem.vue
+1 -1
@@ -70,7 +70,7 @@
70 </n-tab-pane>
71 <n-tab-pane name="SCA Results" tab="SCA Results" display-directive="show:lazy">
72 <div class="p-7 pt-4">
73 - <ScaResults :sca="sca" :agent="agent" />
73 + <ScaResults :sca="sca" :agent />
74 </div>
75 </n-tab-pane>
76 </n-tabs>
frontend/src/components/agents/sca/ScaTable.vue
+1 -1
@@ -72,7 +72,7 @@
72 segmented
73 content-class="!p-0"
74 >
75 - <ScaItem v-if="selectedSca" :sca="selectedSca" :agent="agent"></ScaItem>
75 + <ScaItem v-if="selectedSca" :sca="selectedSca" :agent></ScaItem>
76 </n-modal>
77 </n-spin>
78 </template>
frontend/src/components/monitoringAlerts/ItemActions.vue
+1 -1
@@ -109,7 +109,7 @@ function deleteAlert() {
109 loadingDelete.value = true
110
111 Api.monitoringAlerts
112 - .delete(alert.id)
112 + .deleteAlert(alert.id)
113 .then(res => {
114 if (res.data.success) {
115 emit("deleted")
frontend/src/components/monitoringAlerts/List.vue
+54 -1
@@ -19,6 +19,20 @@
19 </div>
20 </div>
21 </n-popover>
22 +
23 + <n-button
24 + size="small"
25 + type="error"
26 + ghost
27 + @click="handlePurge()"
28 + :loading="loadingPurge"
29 + v-if="monitoringAlerts.length"
30 + >
31 + <div class="flex items-center gap-2">
32 + <Icon :name="TrashIcon" :size="16"></Icon>
33 + <span class="hidden xs:block">Purge</span>
34 + </div>
35 + </n-button>
36 </div>
37 <n-pagination
38 v-model:page="currentPage"
@@ -61,14 +75,16 @@
75
76 <script setup lang="ts">
77 import { ref, onBeforeMount, computed } from "vue"
64 -import { useMessage, NSpin, NPopover, NButton, NEmpty, NPagination } from "naive-ui"
78 +import { useMessage, NSpin, NPopover, NButton, NEmpty, NPagination, useDialog } from "naive-ui"
79 import Api from "@/api"
80 import Icon from "@/components/common/Icon.vue"
81 import { useResizeObserver } from "@vueuse/core"
82 import Alert from "./Item.vue"
83 import type { MonitoringAlert } from "@/types/monitoringAlerts.d"
84
85 +const dialog = useDialog()
86 const message = useMessage()
87 +const loadingPurge = ref(false)
88 const loading = ref(false)
89 const monitoringAlerts = ref<MonitoringAlert[]>([])
90
@@ -87,6 +103,7 @@ const itemsPaginated = computed(() => {
103 return monitoringAlerts.value.slice(from, to)
104 })
105
106 +const TrashIcon = "carbon:trash-can"
107 const InfoIcon = "carbon:information"
108
109 const total = computed<number>(() => {
@@ -115,6 +132,42 @@ function getData() {
132 })
133 }
134
135 +function handlePurge() {
136 + dialog.warning({
137 + title: "Confirm",
138 + content: "This will remove ALL Pending Alerts, are you sure you want to proceed?",
139 + positiveText: "Yes I'm sure",
140 + negativeText: "Cancel",
141 + onPositiveClick: () => {
142 + purge()
143 + },
144 + onNegativeClick: () => {
145 + message.info("Purge canceled")
146 + }
147 + })
148 +}
149 +
150 +function purge() {
151 + loadingPurge.value = true
152 +
153 + Api.monitoringAlerts
154 + .purge()
155 + .then(res => {
156 + if (res.data.success) {
157 + getData()
158 + message.success(res.data?.message || "Pending Alerts purged successfully")
159 + } else {
160 + message.warning(res.data?.message || "An error occurred. Please try again later.")
161 + }
162 + })
163 + .catch(err => {
164 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
165 + })
166 + .finally(() => {
167 + loadingPurge.value = false
168 + })
169 +}
170 +
171 useResizeObserver(header, entries => {
172 const entry = entries[0]
173 const { width } = entry.contentRect
frontend/src/components/soc/SocCases/SocCasesList.vue
+8 -1
@@ -20,7 +20,14 @@
20 </div>
21 </n-popover>
22
23 - <n-button size="small" type="error" ghost @click="handlePurge()" :loading="loadingPurge">
23 + <n-button
24 + size="small"
25 + type="error"
26 + ghost
27 + @click="handlePurge()"
28 + :loading="loadingPurge"
29 + v-if="casesList.length"
30 + >
31 <div class="flex items-center gap-2">
32 <Icon :name="TrashIcon" :size="16"></Icon>
33 <span class="hidden xs:block">Purge</span>
frontend/src/views/agents/Overview.vue
+58 -27
@@ -9,40 +9,48 @@
9 </div>
10 <n-spin
11 class="agent-header py-5 px-7 my-4"
12 + content-class="flex justify-between gap-y-1 gap-x-6 flex-wrap items-start"
13 :class="{ critical: agent?.critical_asset, online: isOnline }"
14 :show="loadingAgent"
15 >
15 - <div class="title">
16 - <div class="critical" :class="{ active: agent?.critical_asset }" v-if="agent">
17 - <n-tooltip>
18 - Toggle Critical Assets
19 - <template #trigger>
20 - <n-button
21 - text
22 - :type="agent?.critical_asset ? 'warning' : 'default'"
23 - circle
24 - @click.stop="toggleCritical(agent.agent_id, agent.critical_asset)"
25 - >
26 - <template #icon>
27 - <Icon :name="StarIcon"></Icon>
28 - </template>
29 - </n-button>
30 - </template>
31 - </n-tooltip>
32 - </div>
16 + <div class="info grow">
17 + <div class="title">
18 + <div class="critical" :class="{ active: agent?.critical_asset }" v-if="agent">
19 + <n-tooltip>
20 + Toggle Critical Assets
21 + <template #trigger>
22 + <n-button
23 + text
24 + :type="agent?.critical_asset ? 'warning' : 'default'"
25 + circle
26 + @click.stop="toggleCritical(agent.agent_id, agent.critical_asset)"
27 + >
28 + <template #icon>
29 + <Icon :name="StarIcon"></Icon>
30 + </template>
31 + </n-button>
32 + </template>
33 + </n-tooltip>
34 + </div>
35
34 - <h1 v-if="agent?.hostname">
35 - {{ agent?.hostname }}
36 - </h1>
36 + <h1 v-if="agent?.hostname">
37 + {{ agent?.hostname }}
38 + </h1>
39
38 - <span class="online-badge" v-if="isOnline">ONLINE</span>
40 + <span class="online-badge" v-if="isOnline">ONLINE</span>
41
40 - <span class="quarantined-badge flex items-center gap-1" v-if="isQuarantined">
41 - <Icon :name="QuarantinedIcon" :size="15"></Icon>
42 - <span>QUARANTINED</span>
43 - </span>
42 + <span class="quarantined-badge flex items-center gap-1" v-if="isQuarantined">
43 + <Icon :name="QuarantinedIcon" :size="15"></Icon>
44 + <span>QUARANTINED</span>
45 + </span>
46 + </div>
47 + <div class="label text-secondary-color mt-2">Agent #{{ agent?.agent_id }}</div>
48 + </div>
49 + <div class="actions flex items-center justify-end grow">
50 + <n-button size="small" ghost type="primary" :loading="upgradingAgent" @click="upgradeWazuhAgent()">
51 + Upgrade Wazuh Agent
52 + </n-button>
53 </div>
45 - <div class="label text-secondary-color mt-2">Agent #{{ agent?.agent_id }}</div>
54 </n-spin>
55 <n-card class="py-1 px-4 pb-4" content-style="padding:0">
56 <n-spin :show="loadingAgent">
@@ -142,6 +150,7 @@ const router = useRouter()
150 const dialog = useDialog()
151 const route = useRoute()
152 const loadingAgent = ref(false)
153 +const upgradingAgent = ref(false)
154 const agent = ref<Agent | null>(null)
155 const agentId = ref<string | null>(null)
156
@@ -179,6 +188,28 @@ function getAgent() {
188 }
189 }
190
191 +function upgradeWazuhAgent() {
192 + if (agentId.value) {
193 + upgradingAgent.value = true
194 +
195 + Api.agents
196 + .upgradeWazuhAgent(agentId.value)
197 + .then(res => {
198 + if (res.data.success) {
199 + message.success(res.data?.message || "Agent upgraded successfully")
200 + } else {
201 + message.error(res.data?.message || "An error occurred. Please try again later.")
202 + }
203 + })
204 + .catch(err => {
205 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
206 + })
207 + .finally(() => {
208 + upgradingAgent.value = false
209 + })
210 + }
211 +}
212 +
213 function toggleCritical(agentId: string, criticalStatus: boolean) {
214 toggleAgentCritical({
215 agentId,