@cryptotaxi247 / CoPilot / commits / 8fa4f662

Stack provisioning (#167)

* Update provision_content_pack_route description * added stack provisioning api/types * Fix customer_meta retrieval and use dictionary key access for field values * added stack provisioning form * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Feb 28, 2024 at 11:07 UTC 8fa4f66207ef3f73383d8cb50a8030ade7acf5d3
10 files changed +271 -12
backend/app/integrations/monitoring_alert/routes/monitoring_alert.py
+6 -6
@@ -121,16 +121,16 @@ async def create_monitoring_alert(
121
122 customer_meta = await session.execute(
123 select(CustomersMeta).where(
124 - CustomersMeta.customer_code == monitoring_alert.event.fields.CUSTOMER_CODE,
124 + CustomersMeta.customer_code == monitoring_alert.event.fields["CUSTOMER_CODE"],
125 ),
126 )
127 customer_meta = customer_meta.scalars().first()
128
129 if not customer_meta:
130 - logger.info(f"Getting customer meta for customer_meta_office365_organization_id: {monitoring_alert.event.fields.CUSTOMER_CODE}")
130 + logger.info(f"Getting customer meta for customer_meta_office365_organization_id: {monitoring_alert.event.fields['CUSTOMER_CODE']}")
131 customer_meta = await session.execute(
132 select(CustomersMeta).where(
133 - CustomersMeta.customer_meta_office365_organization_id == monitoring_alert.event.fields.CUSTOMER_CODE,
133 + CustomersMeta.customer_meta_office365_organization_id == monitoring_alert.event.fields["CUSTOMER_CODE"],
134 ),
135 )
136 customer_meta = customer_meta.scalars().first()
@@ -140,10 +140,10 @@ async def create_monitoring_alert(
140
141 try:
142 monitoring_alert = MonitoringAlerts(
143 - alert_id=monitoring_alert.event.fields.ALERT_ID,
143 + alert_id=monitoring_alert.event.fields["ALERT_ID"],
144 alert_index=monitoring_alert.event.alert_index,
145 - customer_code=monitoring_alert.event.fields.CUSTOMER_CODE,
146 - alert_source=monitoring_alert.event.fields.ALERT_SOURCE,
145 + customer_code=monitoring_alert.event.fields["CUSTOMER_CODE"],
146 + alert_source=monitoring_alert.event.fields["ALERT_SOURCE"],
147 )
148 session.add(monitoring_alert)
149 await session.commit()
backend/app/stack_provisioning/graylog/routes/provision.py
+1 -1
@@ -124,7 +124,7 @@ async def get_available_content_packs_route() -> AvailableContentPacksResponse:
124 @stack_provisioning_graylog_router.post(
125 "/graylog/provision/content_pack",
126 response_model=ProvisionGraylogResponse,
127 - description="Provision the Wazuh Content Pack in the Graylog instance",
127 + description="Provision the Content Pack in the Graylog instance",
128 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
129 )
130 async def provision_content_pack_route(
frontend/src/api/index.ts
+3 -1
@@ -15,6 +15,7 @@ import flow from "./flow"
15 import integrations from "./integrations"
16 import monitoringAlerts from "./monitoringAlerts"
17 import activeResponse from "./activeResponse"
18 +import stackProvisioning from "./stackProvisioning"
19
20 export default {
21 agents,
@@ -33,5 +34,6 @@ export default {
34 flow,
35 integrations,
36 monitoringAlerts,
36 - activeResponse
37 + activeResponse,
38 + stackProvisioning
39 }
frontend/src/api/stackProvisioning.ts new
+16
@@ -0,0 +1,16 @@
1 +import { type FlaskBaseResponse } from "@/types/flask.d"
2 +import { HttpClient } from "./httpClient"
3 +import type { AvailableContentPack } from "@/types/stackProvisioning"
4 +
5 +export default {
6 + getAvailableContentPacks() {
7 + return HttpClient.get<FlaskBaseResponse & { available_content_packs: AvailableContentPack[] }>(
8 + `/stack_provisioning/graylog/available/content_packs`
9 + )
10 + },
11 + provisionContentPack(contentPackName: string) {
12 + return HttpClient.post<FlaskBaseResponse>(`/stack_provisioning/graylog/provision/content_pack`, {
13 + content_pack_name: contentPackName
14 + })
15 + }
16 +}
frontend/src/components/graylog/MonitoringAlerts/Item.vue
+1 -1
@@ -184,7 +184,7 @@ function provisionsMonitoringAlert() {
184 .provisionsMonitoringAlert(alert.name, params)
185 .then(res => {
186 if (res.data.success) {
187 - message.success("Alert Provisioned Successfully")
187 + message.success(res.data?.message || `Monitoring alert ${alert.name} provisioned successfully`)
188 emit("provisioned")
189 } else {
190 message.warning(res.data?.message || "An error occurred. Please try again later.")
frontend/src/components/stackProvisioning/StackProvisioningButton.vue new
+33
@@ -0,0 +1,33 @@
1 +<template>
2 + <n-button :size="size" :type="type" @click="showForm = true">
3 + <template #icon><Icon :name="PackIcon"></Icon></template>
4 + Stack Provisioning
5 + </n-button>
6 +
7 + <n-modal
8 + v-model:show="showForm"
9 + display-directive="show"
10 + preset="card"
11 + :style="{ maxWidth: 'min(600px, 90vw)', minHeight: 'min(300px, 90vh)', overflow: 'hidden' }"
12 + title="Deploy Content Packs"
13 + :bordered="false"
14 + segmented
15 + >
16 + <StackProvisioningList />
17 + </n-modal>
18 +</template>
19 +
20 +<script setup lang="ts">
21 +import { ref } from "vue"
22 +import { NButton, NModal } from "naive-ui"
23 +import Icon from "@/components/common/Icon.vue"
24 +import StackProvisioningList from "./StackProvisioningList.vue"
25 +
26 +const { type, size } = defineProps<{
27 + size?: "tiny" | "small" | "medium" | "large"
28 + type?: "default" | "tertiary" | "primary" | "info" | "success" | "warning" | "error"
29 +}>()
30 +
31 +const PackIcon = "mdi:package-variant"
32 +const showForm = ref(false)
33 +</script>
frontend/src/components/stackProvisioning/StackProvisioningItem.vue new
+114
@@ -0,0 +1,114 @@
1 +<template>
2 + <div class="item flex flex-col gap-2 px-5 py-3">
3 + <div class="header-box flex justify-between gap-4">
4 + <div class="name">{{ contentPack.name }}</div>
5 + </div>
6 + <div class="main-box flex justify-between gap-4">
7 + <div class="description">{{ contentPack.description }}</div>
8 + <div class="actions-box">
9 + <n-button :loading="loadingProvision" type="success" secondary @click="provision(contentPack.name)">
10 + <template #icon><Icon :name="DeployIcon"></Icon></template>
11 + Deploy
12 + </n-button>
13 + </div>
14 + </div>
15 + <div class="footer-box flex justify-between items-center gap-4">
16 + <div class="actions-box">
17 + <n-button
18 + :loading="loadingProvision"
19 + type="success"
20 + secondary
21 + size="small"
22 + @click="provision(contentPack.name)"
23 + >
24 + <template #icon><Icon :name="DeployIcon"></Icon></template>
25 + Deploy
26 + </n-button>
27 + </div>
28 + </div>
29 + </div>
30 +</template>
31 +
32 +<script setup lang="ts">
33 +import { ref } from "vue"
34 +import Icon from "@/components/common/Icon.vue"
35 +import { NButton, useMessage } from "naive-ui"
36 +import Api from "@/api"
37 +import type { AvailableContentPack } from "@/types/stackProvisioning"
38 +
39 +const emit = defineEmits<{
40 + (e: "provisioned"): void
41 +}>()
42 +
43 +const { contentPack } = defineProps<{ contentPack: AvailableContentPack }>()
44 +
45 +const DeployIcon = "mdi:package-variant-closed-check"
46 +const loadingProvision = ref(false)
47 +const message = useMessage()
48 +
49 +function provision(contentPackName: string) {
50 + loadingProvision.value = true
51 +
52 + Api.stackProvisioning
53 + .provisionContentPack(contentPackName)
54 + .then(res => {
55 + if (res.data.success) {
56 + message.success(res.data?.message || "Content Pack Provisioned Successfully")
57 + emit("provisioned")
58 + } else {
59 + message.warning(res.data?.message || "An error occurred. Please try again later.")
60 + }
61 + })
62 + .catch(err => {
63 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
64 + })
65 + .finally(() => {
66 + loadingProvision.value = false
67 + })
68 +}
69 +</script>
70 +
71 +<style lang="scss" scoped>
72 +.item {
73 + border-radius: var(--border-radius);
74 + background-color: var(--bg-secondary-color);
75 + transition: all 0.2s var(--bezier-ease);
76 + border: var(--border-small-050);
77 +
78 + .header-box {
79 + font-size: 13px;
80 +
81 + .name {
82 + font-family: var(--font-family-mono);
83 + word-break: break-word;
84 + color: var(--fg-secondary-color);
85 + }
86 + }
87 + .main-box {
88 + .description {
89 + word-break: break-word;
90 + }
91 + }
92 +
93 + .footer-box {
94 + display: none;
95 + font-size: 13px;
96 + margin-top: 10px;
97 + }
98 +
99 + &:hover {
100 + box-shadow: 0px 0px 0px 1px inset var(--primary-color);
101 + }
102 +
103 + @container (max-width: 450px) {
104 + .main-box {
105 + .actions-box {
106 + display: none;
107 + }
108 + }
109 + .footer-box {
110 + display: flex;
111 + }
112 + }
113 +}
114 +</style>
frontend/src/components/stackProvisioning/StackProvisioningList.vue new
+58
@@ -0,0 +1,58 @@
1 +<template>
2 + <div class="stack-provisioning-list">
3 + <n-spin :show="loading">
4 + <div class="list my-3">
5 + <template v-if="list.length">
6 + <StackProvisioningItem v-for="item of list" :key="item.name" :content-pack="item" class="mb-2" />
7 + </template>
8 + <template v-else>
9 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
10 + </template>
11 + </div>
12 + </n-spin>
13 + </div>
14 +</template>
15 +
16 +<script setup lang="ts">
17 +import { ref, onBeforeMount, computed } from "vue"
18 +import { useMessage, NSpin, NEmpty } from "naive-ui"
19 +import Api from "@/api"
20 +import StackProvisioningItem from "./StackProvisioningItem.vue"
21 +import type { AvailableContentPack } from "@/types/stackProvisioning"
22 +
23 +const message = useMessage()
24 +const loadingList = ref(false)
25 +const list = ref<AvailableContentPack[]>([])
26 +const loading = computed(() => loadingList.value)
27 +
28 +function getData() {
29 + loadingList.value = true
30 +
31 + Api.stackProvisioning
32 + .getAvailableContentPacks()
33 + .then(res => {
34 + if (res.data.success) {
35 + list.value = res.data.available_content_packs || []
36 + } else {
37 + message.warning(res.data?.message || "An error occurred. Please try again later.")
38 + }
39 + })
40 + .catch(err => {
41 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
42 + })
43 + .finally(() => {
44 + loadingList.value = false
45 + })
46 +}
47 +
48 +onBeforeMount(() => {
49 + getData()
50 +})
51 +</script>
52 +
53 +<style lang="scss" scoped>
54 +.list {
55 + container-type: inline-size;
56 + min-height: 200px;
57 +}
58 +</style>
frontend/src/types/stackProvisioning.d.ts new
+4
@@ -0,0 +1,4 @@
1 +export interface AvailableContentPack {
2 + name: string
3 + description: string
4 +}
frontend/src/views/Overview.vue
+35 -3
@@ -1,8 +1,19 @@
1 <template>
2 <div class="page" ref="page">
3 - <div class="section justify-end flex gap-3">
4 - <ActiveResponseWizardButton size="small" type="primary" />
5 - <ThreatIntelButton size="small" type="primary" />
3 + <div class="section justify-end sm:justify-between flex gap-3">
4 + <div class="left-box hidden sm:flex gap-3">
5 + <StackProvisioningButton size="small" type="primary" />
6 + </div>
7 + <div class="right-box hidden sm:flex gap-3">
8 + <ActiveResponseWizardButton size="small" type="primary" />
9 + <ThreatIntelButton size="small" type="primary" />
10 + </div>
11 + <div class="mobile-box block sm:hidden">
12 + <n-button size="small" type="primary" @click="showQuickActions = true">
13 + <template #icon><Icon :name="QuickActionsIcon"></Icon></template>
14 + Quick Actions
15 + </n-button>
16 + </div>
17 </div>
18 <div class="section">
19 <div class="columns column-800 overflow-hidden">
@@ -40,17 +51,35 @@
51 <div class="section">
52 <PipeList minHeight="28px" @open-rule="gotoPipelinesPage($event)" />
53 </div>
54 +
55 + <n-drawer
56 + v-model:show="showQuickActions"
57 + :width="250"
58 + style="max-width: 90vw"
59 + :trap-focus="false"
60 + display-directive="show"
61 + >
62 + <n-drawer-content title="Quick Actions" closable :native-scrollbar="false">
63 + <div class="flex flex-col gap-3">
64 + <StackProvisioningButton size="small" type="primary" />
65 + <ActiveResponseWizardButton size="small" type="primary" />
66 + <ThreatIntelButton size="small" type="primary" />
67 + </div>
68 + </n-drawer-content>
69 + </n-drawer>
70 </div>
71 </template>
72
73 <script setup lang="ts">
74 import { ref } from "vue"
75 +import { NButton, NDrawer, NDrawerContent } from "naive-ui"
76 import { useRouter } from "vue-router"
77 import ClusterHealth from "@/components/indices/ClusterHealth.vue"
78 import NodeAllocation from "@/components/indices/NodeAllocation.vue"
79 import IndicesMarquee from "@/components/indices/Marquee.vue"
80 import ThreatIntelButton from "@/components/alerts/ThreatIntelButton.vue"
81 import ActiveResponseWizardButton from "@/components/activeResponse/ActiveResponseWizardButton.vue"
82 +import StackProvisioningButton from "@/components/stackProvisioning/StackProvisioningButton.vue"
83 import AgentsCard from "@/components/overview/AgentsCard.vue"
84 import HealthcheckCard from "@/components/overview/HealthcheckCard.vue"
85 // import SocAlertsCard from "@/components/overview/SocAlertsCard.vue"
@@ -58,10 +87,13 @@ import CustomersCard from "@/components/overview/CustomersCard.vue"
87 import PipeList from "@/components/graylog/Pipelines/PipeList.vue"
88 import type { IndexStats } from "@/types/indices.d"
89 import { useResizeObserver } from "@vueuse/core"
90 +import Icon from "@/components/common/Icon.vue"
91
92 +const QuickActionsIcon = "ant-design:thunderbolt-outlined"
93 const router = useRouter()
94 const page = ref()
95 const cardDirection = ref<"horizontal" | "vertical">("horizontal")
96 +const showQuickActions = ref(false)
97
98 function gotoIndicesPage(index: IndexStats) {
99 router.push({ name: "Indices", query: { index_name: index.index } })