Skip to content

Commit

Permalink
Stack provisioning (#167)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
taylorwalton and Linko91 authored Feb 28, 2024
1 parent 4c582db commit 8fa4f66
Show file tree
Hide file tree
Showing 10 changed files with 271 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -121,16 +121,16 @@ async def create_monitoring_alert(

customer_meta = await session.execute(
select(CustomersMeta).where(
CustomersMeta.customer_code == monitoring_alert.event.fields.CUSTOMER_CODE,
CustomersMeta.customer_code == monitoring_alert.event.fields["CUSTOMER_CODE"],
),
)
customer_meta = customer_meta.scalars().first()

if not customer_meta:
logger.info(f"Getting customer meta for customer_meta_office365_organization_id: {monitoring_alert.event.fields.CUSTOMER_CODE}")
logger.info(f"Getting customer meta for customer_meta_office365_organization_id: {monitoring_alert.event.fields['CUSTOMER_CODE']}")
customer_meta = await session.execute(
select(CustomersMeta).where(
CustomersMeta.customer_meta_office365_organization_id == monitoring_alert.event.fields.CUSTOMER_CODE,
CustomersMeta.customer_meta_office365_organization_id == monitoring_alert.event.fields["CUSTOMER_CODE"],
),
)
customer_meta = customer_meta.scalars().first()
Expand All @@ -140,10 +140,10 @@ async def create_monitoring_alert(

try:
monitoring_alert = MonitoringAlerts(
alert_id=monitoring_alert.event.fields.ALERT_ID,
alert_id=monitoring_alert.event.fields["ALERT_ID"],
alert_index=monitoring_alert.event.alert_index,
customer_code=monitoring_alert.event.fields.CUSTOMER_CODE,
alert_source=monitoring_alert.event.fields.ALERT_SOURCE,
customer_code=monitoring_alert.event.fields["CUSTOMER_CODE"],
alert_source=monitoring_alert.event.fields["ALERT_SOURCE"],
)
session.add(monitoring_alert)
await session.commit()
Expand Down
2 changes: 1 addition & 1 deletion backend/app/stack_provisioning/graylog/routes/provision.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ async def get_available_content_packs_route() -> AvailableContentPacksResponse:
@stack_provisioning_graylog_router.post(
"/graylog/provision/content_pack",
response_model=ProvisionGraylogResponse,
description="Provision the Wazuh Content Pack in the Graylog instance",
description="Provision the Content Pack in the Graylog instance",
dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
)
async def provision_content_pack_route(
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import flow from "./flow"
import integrations from "./integrations"
import monitoringAlerts from "./monitoringAlerts"
import activeResponse from "./activeResponse"
import stackProvisioning from "./stackProvisioning"

export default {
agents,
Expand All @@ -33,5 +34,6 @@ export default {
flow,
integrations,
monitoringAlerts,
activeResponse
activeResponse,
stackProvisioning
}
16 changes: 16 additions & 0 deletions frontend/src/api/stackProvisioning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { type FlaskBaseResponse } from "@/types/flask.d"
import { HttpClient } from "./httpClient"
import type { AvailableContentPack } from "@/types/stackProvisioning"

export default {
getAvailableContentPacks() {
return HttpClient.get<FlaskBaseResponse & { available_content_packs: AvailableContentPack[] }>(
`/stack_provisioning/graylog/available/content_packs`
)
},
provisionContentPack(contentPackName: string) {
return HttpClient.post<FlaskBaseResponse>(`/stack_provisioning/graylog/provision/content_pack`, {
content_pack_name: contentPackName
})
}
}
2 changes: 1 addition & 1 deletion frontend/src/components/graylog/MonitoringAlerts/Item.vue
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ function provisionsMonitoringAlert() {
.provisionsMonitoringAlert(alert.name, params)
.then(res => {
if (res.data.success) {
message.success("Alert Provisioned Successfully")
message.success(res.data?.message || `Monitoring alert ${alert.name} provisioned successfully`)
emit("provisioned")
} else {
message.warning(res.data?.message || "An error occurred. Please try again later.")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<template>
<n-button :size="size" :type="type" @click="showForm = true">
<template #icon><Icon :name="PackIcon"></Icon></template>
Stack Provisioning
</n-button>

<n-modal
v-model:show="showForm"
display-directive="show"
preset="card"
:style="{ maxWidth: 'min(600px, 90vw)', minHeight: 'min(300px, 90vh)', overflow: 'hidden' }"
title="Deploy Content Packs"
:bordered="false"
segmented
>
<StackProvisioningList />
</n-modal>
</template>

<script setup lang="ts">
import { ref } from "vue"
import { NButton, NModal } from "naive-ui"
import Icon from "@/components/common/Icon.vue"
import StackProvisioningList from "./StackProvisioningList.vue"
const { type, size } = defineProps<{
size?: "tiny" | "small" | "medium" | "large"
type?: "default" | "tertiary" | "primary" | "info" | "success" | "warning" | "error"
}>()
const PackIcon = "mdi:package-variant"
const showForm = ref(false)
</script>
114 changes: 114 additions & 0 deletions frontend/src/components/stackProvisioning/StackProvisioningItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<template>
<div class="item flex flex-col gap-2 px-5 py-3">
<div class="header-box flex justify-between gap-4">
<div class="name">{{ contentPack.name }}</div>
</div>
<div class="main-box flex justify-between gap-4">
<div class="description">{{ contentPack.description }}</div>
<div class="actions-box">
<n-button :loading="loadingProvision" type="success" secondary @click="provision(contentPack.name)">
<template #icon><Icon :name="DeployIcon"></Icon></template>
Deploy
</n-button>
</div>
</div>
<div class="footer-box flex justify-between items-center gap-4">
<div class="actions-box">
<n-button
:loading="loadingProvision"
type="success"
secondary
size="small"
@click="provision(contentPack.name)"
>
<template #icon><Icon :name="DeployIcon"></Icon></template>
Deploy
</n-button>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { ref } from "vue"
import Icon from "@/components/common/Icon.vue"
import { NButton, useMessage } from "naive-ui"
import Api from "@/api"
import type { AvailableContentPack } from "@/types/stackProvisioning"
const emit = defineEmits<{
(e: "provisioned"): void
}>()
const { contentPack } = defineProps<{ contentPack: AvailableContentPack }>()
const DeployIcon = "mdi:package-variant-closed-check"
const loadingProvision = ref(false)
const message = useMessage()
function provision(contentPackName: string) {
loadingProvision.value = true
Api.stackProvisioning
.provisionContentPack(contentPackName)
.then(res => {
if (res.data.success) {
message.success(res.data?.message || "Content Pack Provisioned Successfully")
emit("provisioned")
} else {
message.warning(res.data?.message || "An error occurred. Please try again later.")
}
})
.catch(err => {
message.error(err.response?.data?.message || "An error occurred. Please try again later.")
})
.finally(() => {
loadingProvision.value = false
})
}
</script>

<style lang="scss" scoped>
.item {
border-radius: var(--border-radius);
background-color: var(--bg-secondary-color);
transition: all 0.2s var(--bezier-ease);
border: var(--border-small-050);
.header-box {
font-size: 13px;
.name {
font-family: var(--font-family-mono);
word-break: break-word;
color: var(--fg-secondary-color);
}
}
.main-box {
.description {
word-break: break-word;
}
}
.footer-box {
display: none;
font-size: 13px;
margin-top: 10px;
}
&:hover {
box-shadow: 0px 0px 0px 1px inset var(--primary-color);
}
@container (max-width: 450px) {
.main-box {
.actions-box {
display: none;
}
}
.footer-box {
display: flex;
}
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<template>
<div class="stack-provisioning-list">
<n-spin :show="loading">
<div class="list my-3">
<template v-if="list.length">
<StackProvisioningItem v-for="item of list" :key="item.name" :content-pack="item" class="mb-2" />
</template>
<template v-else>
<n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
</template>
</div>
</n-spin>
</div>
</template>

<script setup lang="ts">
import { ref, onBeforeMount, computed } from "vue"
import { useMessage, NSpin, NEmpty } from "naive-ui"
import Api from "@/api"
import StackProvisioningItem from "./StackProvisioningItem.vue"
import type { AvailableContentPack } from "@/types/stackProvisioning"
const message = useMessage()
const loadingList = ref(false)
const list = ref<AvailableContentPack[]>([])
const loading = computed(() => loadingList.value)
function getData() {
loadingList.value = true
Api.stackProvisioning
.getAvailableContentPacks()
.then(res => {
if (res.data.success) {
list.value = res.data.available_content_packs || []
} else {
message.warning(res.data?.message || "An error occurred. Please try again later.")
}
})
.catch(err => {
message.error(err.response?.data?.message || "An error occurred. Please try again later.")
})
.finally(() => {
loadingList.value = false
})
}
onBeforeMount(() => {
getData()
})
</script>

<style lang="scss" scoped>
.list {
container-type: inline-size;
min-height: 200px;
}
</style>
4 changes: 4 additions & 0 deletions frontend/src/types/stackProvisioning.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface AvailableContentPack {
name: string
description: string
}
38 changes: 35 additions & 3 deletions frontend/src/views/Overview.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
<template>
<div class="page" ref="page">
<div class="section justify-end flex gap-3">
<ActiveResponseWizardButton size="small" type="primary" />
<ThreatIntelButton size="small" type="primary" />
<div class="section justify-end sm:justify-between flex gap-3">
<div class="left-box hidden sm:flex gap-3">
<StackProvisioningButton size="small" type="primary" />
</div>
<div class="right-box hidden sm:flex gap-3">
<ActiveResponseWizardButton size="small" type="primary" />
<ThreatIntelButton size="small" type="primary" />
</div>
<div class="mobile-box block sm:hidden">
<n-button size="small" type="primary" @click="showQuickActions = true">
<template #icon><Icon :name="QuickActionsIcon"></Icon></template>
Quick Actions
</n-button>
</div>
</div>
<div class="section">
<div class="columns column-800 overflow-hidden">
Expand Down Expand Up @@ -40,28 +51,49 @@
<div class="section">
<PipeList minHeight="28px" @open-rule="gotoPipelinesPage($event)" />
</div>

<n-drawer
v-model:show="showQuickActions"
:width="250"
style="max-width: 90vw"
:trap-focus="false"
display-directive="show"
>
<n-drawer-content title="Quick Actions" closable :native-scrollbar="false">
<div class="flex flex-col gap-3">
<StackProvisioningButton size="small" type="primary" />
<ActiveResponseWizardButton size="small" type="primary" />
<ThreatIntelButton size="small" type="primary" />
</div>
</n-drawer-content>
</n-drawer>
</div>
</template>

<script setup lang="ts">
import { ref } from "vue"
import { NButton, NDrawer, NDrawerContent } from "naive-ui"
import { useRouter } from "vue-router"
import ClusterHealth from "@/components/indices/ClusterHealth.vue"
import NodeAllocation from "@/components/indices/NodeAllocation.vue"
import IndicesMarquee from "@/components/indices/Marquee.vue"
import ThreatIntelButton from "@/components/alerts/ThreatIntelButton.vue"
import ActiveResponseWizardButton from "@/components/activeResponse/ActiveResponseWizardButton.vue"
import StackProvisioningButton from "@/components/stackProvisioning/StackProvisioningButton.vue"
import AgentsCard from "@/components/overview/AgentsCard.vue"
import HealthcheckCard from "@/components/overview/HealthcheckCard.vue"
// import SocAlertsCard from "@/components/overview/SocAlertsCard.vue"
import CustomersCard from "@/components/overview/CustomersCard.vue"
import PipeList from "@/components/graylog/Pipelines/PipeList.vue"
import type { IndexStats } from "@/types/indices.d"
import { useResizeObserver } from "@vueuse/core"
import Icon from "@/components/common/Icon.vue"
const QuickActionsIcon = "ant-design:thunderbolt-outlined"
const router = useRouter()
const page = ref()
const cardDirection = ref<"horizontal" | "vertical">("horizontal")
const showQuickActions = ref(false)
function gotoIndicesPage(index: IndexStats) {
router.push({ name: "Indices", query: { index_name: index.index } })
Expand Down

0 comments on commit 8fa4f66

Please sign in to comment.