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 } })