@cryptotaxi247 / CoPilot / commits / a019579d

Overview page (#113)

* added overview page * Add check for unique ports in customer provisioning * Add wazuh API port to customer meta * updated overview page * updated overview page * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Dec 31, 2023 at 07:34 UTC a019579d05e99f486363631e44073e5f79ea2bb1
21 files changed +966 -101
backend/app/auth/services/universal.py
+1 -2
@@ -1,3 +1,4 @@
1 +from fastapi import HTTPException
2 from loguru import logger
3
4 # ! New with Async
@@ -6,7 +7,6 @@ from sqlmodel import select
7
8 from app.auth.models.users import Password
9 from app.auth.models.users import Role
9 -from fastapi import HTTPException
10 from app.auth.models.users import User
11 from app.db.db_session import async_engine
12
@@ -188,4 +188,3 @@ def get_scheduler_password():
188 str: The unhashed password of the scheduler user.
189 """
190 return passwords_in_memory.get("scheduler")
191 -
backend/app/customer_provisioning/routes/provision.py
+46
@@ -1,3 +1,5 @@
1 +from typing import List
2 +
3 from fastapi import APIRouter
4 from fastapi import Body
5 from fastapi import Depends
@@ -82,6 +84,49 @@ async def check_customer_exists(customer_code: str, session: AsyncSession = Depe
84 return customer
85
86
87 +async def check_unique_ports(request: ProvisionNewCustomer, session: AsyncSession):
88 + """
89 + Ensures that the ports specified in the request are unique.
90 +
91 + Args:
92 + request: The request data containing the ports to check.
93 + session: The database session.
94 +
95 + Raises:
96 + HTTPException: If the ports are not unique.
97 + """
98 + ports = {"registration": request.wazuh_registration_port, "logs": request.wazuh_logs_port, "api": request.wazuh_api_port}
99 +
100 + for port_type, port_value in ports.items():
101 + customer_meta = await get_customer_meta_by_port(port_value, session)
102 + if customer_meta:
103 + raise HTTPException(
104 + status_code=400,
105 + detail=f"Ports must be unique. {port_type.capitalize()} port {port_value} is already in use for customer {customer_meta.customer_code}.",
106 + )
107 +
108 +
109 +async def get_customer_meta_by_port(port: int, session: AsyncSession):
110 + """
111 + Retrieves customer metadata based on the provided port.
112 +
113 + Args:
114 + port: The port to check.
115 + session: The database session.
116 +
117 + Returns:
118 + Customer metadata if a match is found, otherwise None.
119 + """
120 + result = await session.execute(
121 + select(CustomersMeta).filter(
122 + (CustomersMeta.customer_meta_wazuh_registration_port == port)
123 + | (CustomersMeta.customer_meta_wazuh_log_ingestion_port == port)
124 + | (CustomersMeta.customer_meta_wazuh_api_port == port),
125 + ),
126 + )
127 + return result.scalars().first()
128 +
129 +
130 @customer_provisioning_router.post(
131 "/provision",
132 response_model=CustomerProvisionResponse,
@@ -104,6 +149,7 @@ async def provision_customer_route(
149 Returns:
150 CustomerProvisionResponse: The response data for the provisioned customer.
151 """
152 + await check_unique_ports(request, session)
153 logger.info("Provisioning new customer")
154 customer_provision = await provision_wazuh_customer(request, session=session)
155 return customer_provision
backend/app/customer_provisioning/services/provision.py
+1
@@ -136,6 +136,7 @@ async def update_customer_meta_table(request: ProvisionNewCustomer, customer_met
136 customer_meta_index_retention=str(request.hot_data_retention),
137 customer_meta_wazuh_registration_port=request.wazuh_registration_port,
138 customer_meta_wazuh_log_ingestion_port=request.wazuh_logs_port,
139 + customer_meta_wazuh_api_port=request.wazuh_api_port,
140 customer_meta_wazuh_auth_password=request.wazuh_auth_password,
141 customer_meta_iris_customer_id=customer_meta.iris_customer_id,
142 )
backend/app/db/universal_models.py
+2
@@ -55,6 +55,7 @@ class CustomersMeta(SQLModel, table=True):
55 customer_meta_index_retention: Optional[str] = Field()
56 customer_meta_wazuh_registration_port: Optional[str] = Field()
57 customer_meta_wazuh_log_ingestion_port: Optional[str] = Field()
58 + customer_meta_wazuh_api_port: Optional[str] = Field()
59 customer_meta_wazuh_auth_password: Optional[str] = Field(max_length=1024)
60 customer_meta_iris_customer_id: Optional[int] = Field()
61 customer_meta_office365_organization_id: Optional[str] = Field(max_length=1024)
@@ -74,6 +75,7 @@ class CustomersMeta(SQLModel, table=True):
75 self.customer_meta_index_retention = customer_meta.customer_meta_index_retention
76 self.customer_meta_wazuh_registration_port = customer_meta.customer_meta_wazuh_registration_port
77 self.customer_meta_wazuh_log_ingestion_port = customer_meta.customer_meta_wazuh_log_ingestion_port
78 + self.customer_meta_wazuh_api_port = customer_meta.customer_meta_wazuh_api_port
79 self.customer_meta_wazuh_auth_password = customer_meta.customer_meta_wazuh_auth_password
80 self.customer_meta_iris_customer_id = customer_meta.customer_meta_iris_customer_id
81 self.customer_meta_office365_organization_id = customer_meta.customer_meta_office365_organization_id
src/components/alerts/AlertsList.vue
+3 -23
@@ -37,7 +37,7 @@
37 </template>
38 Filters
39 </n-button>
40 - <n-button size="small" type="primary" @click="showThreatIntelDrawer = true">Threat Intel</n-button>
40 + <ThreatIntelButton size="small" type="primary" />
41 </div>
42 </div>
43 <n-spin :show="loading">
@@ -58,18 +58,6 @@
58 </div>
59 </n-spin>
60
61 - <n-drawer
62 - v-model:show="showThreatIntelDrawer"
63 - :width="500"
64 - style="max-width: 90vw"
65 - :trap-focus="false"
66 - display-directive="show"
67 - >
68 - <n-drawer-content title="SOCFortress Threat Intel" closable :native-scrollbar="false">
69 - <ThreatIntel @mounted="threatIntelCTX = $event" />
70 - </n-drawer-content>
71 - </n-drawer>
72 -
61 <n-drawer
62 v-model:show="showStatsDrawer"
63 :width="700"
@@ -146,10 +134,10 @@
134 // import { alerts_summary } from "./mock"
135 // import type { AlertsSummary } from "@/types/alerts"
136
149 -import { ref, onBeforeMount, toRefs, computed, nextTick, onMounted } from "vue"
137 +import { ref, onBeforeMount, toRefs, computed, nextTick, onMounted, onBeforeUnmount } from "vue"
138 import { useMessage, NSpin, NPopover, NButton, NEmpty, NDrawer, NDrawerContent, NSelect } from "naive-ui"
139 import Api from "@/api"
152 -import ThreatIntel from "./ThreatIntel.vue"
140 +import ThreatIntelButton from "./ThreatIntelButton.vue"
141 import AlertsStats, { type AlertsStatsCTX } from "./AlertsStats.vue"
142 import AlertsFilters from "./AlertsFilters.vue"
143 import AlertsSummaryItem, { type AlertsSummaryExt } from "./AlertsSummary.vue"
@@ -158,8 +146,6 @@ import type { AlertsSummaryQuery } from "@/api/alerts"
146 import type { IndexStats } from "@/types/indices.d"
147 import axios from "axios"
148 import type { Agent } from "@/types/agents.d"
161 -import { onBeforeUnmount } from "vue"
162 -import { watch } from "vue"
149
150 const props = defineProps<{ agentHostname?: string; indexName?: string }>()
151 const { agentHostname, indexName } = toRefs(props)
@@ -174,8 +160,6 @@ const alertsSummaryList = ref<AlertsSummaryExt[]>([])
160 const loadingFilters = ref(true)
161 const showFiltersDrawer = ref(true)
162 const showStatsDrawer = ref(false)
177 -const showThreatIntelDrawer = ref(false)
178 -const threatIntelCTX = ref<{ restore: () => void } | null>(null)
163 let abortController: AbortController | null = null
164
165 const InfoIcon = "carbon:information"
@@ -337,10 +321,6 @@ function cancelSearch() {
321 abortController?.abort()
322 }
323
340 -watch(showThreatIntelDrawer, () => {
341 - threatIntelCTX.value?.restore()
342 -})
343 -
324 onBeforeMount(() => {
325 if (agentHostname?.value) {
326 filters.value.agentHostname = agentHostname.value
src/components/alerts/ThreatIntelButton.vue new
+33
@@ -0,0 +1,33 @@
1 +<template>
2 + <n-button :size="size" :type="type" @click="showThreatIntelDrawer = true">Threat Intel</n-button>
3 +
4 + <n-drawer
5 + v-model:show="showThreatIntelDrawer"
6 + :width="500"
7 + style="max-width: 90vw"
8 + :trap-focus="false"
9 + display-directive="show"
10 + >
11 + <n-drawer-content title="SOCFortress Threat Intel" closable :native-scrollbar="false">
12 + <ThreatIntelForm @mounted="threatIntelCTX = $event" />
13 + </n-drawer-content>
14 + </n-drawer>
15 +</template>
16 +
17 +<script setup lang="ts">
18 +import { ref, watch } from "vue"
19 +import { NButton, NDrawer, NDrawerContent } from "naive-ui"
20 +import ThreatIntelForm from "./ThreatIntelForm.vue"
21 +
22 +const { type, size } = defineProps<{
23 + size?: "tiny" | "small" | "medium" | "large"
24 + type?: "default" | "tertiary" | "primary" | "info" | "success" | "warning" | "error"
25 +}>()
26 +
27 +const showThreatIntelDrawer = ref(false)
28 +const threatIntelCTX = ref<{ restore: () => void } | null>(null)
29 +
30 +watch(showThreatIntelDrawer, () => {
31 + threatIntelCTX.value?.restore()
32 +})
33 +</script>
src/components/alerts/ThreatIntelForm.vue renamed
src/components/cards/CardActions.vue
+2 -2
@@ -71,7 +71,7 @@ const menuOptions = computed(() =>
71 key: "reload",
72 icon: renderIcon(ReloadIcon)
73 }
74 - ]
74 + ]
75 : [
76 {
77 label: "Collapse",
@@ -83,7 +83,7 @@ const menuOptions = computed(() =>
83 key: "reload",
84 icon: renderIcon(ReloadIcon)
85 }
86 - ]
86 + ]
87 )
88
89 function menuSelect(key: string) {
src/components/common/CardStats.vue new
+61
@@ -0,0 +1,61 @@
1 +<template>
2 + <n-card :class="{ hovered }">
3 + <div class="flex items-center h-full overflow-hidden">
4 + <div class="card-wrap flex gap-4" :class="{ 'flex-col items-center text-center': vertical }">
5 + <div class="icon flex flex-col justify-center">
6 + <slot name="icon"></slot>
7 + </div>
8 + <div class="info flex flex-col grow overflow-hidden">
9 + <div class="title">{{ title }}</div>
10 + <div class="value mt-1" v-if="value">{{ value }}</div>
11 + </div>
12 + </div>
13 + </div>
14 + </n-card>
15 +</template>
16 +
17 +<script setup lang="ts">
18 +import { NCard } from "naive-ui"
19 +import { toRefs } from "vue"
20 +
21 +const props = defineProps<{
22 + title: string
23 + value?: number | string
24 + vertical?: boolean
25 + hovered?: boolean
26 +}>()
27 +const { title, value, vertical } = toRefs(props)
28 +</script>
29 +
30 +<style scoped lang="scss">
31 +.n-card {
32 + overflow: hidden;
33 +
34 + .card-wrap {
35 + width: 100%;
36 + overflow: hidden;
37 +
38 + .title {
39 + font-size: 16px;
40 + text-overflow: ellipsis;
41 + white-space: nowrap;
42 + overflow: hidden;
43 + }
44 +
45 + .value {
46 + font-family: var(--font-family-display);
47 + font-size: 22px;
48 + font-weight: bold;
49 + text-overflow: ellipsis;
50 + white-space: nowrap;
51 + overflow: hidden;
52 + }
53 + }
54 +
55 + &.hovered {
56 + &:hover {
57 + border-color: var(--primary-color);
58 + }
59 + }
60 +}
61 +</style>
src/components/common/CardStatsDouble.vue new
+130
@@ -0,0 +1,130 @@
1 +<template>
2 + <n-card content-style="padding:0" :class="{ hovered }">
3 + <div class="flex flex-col overflow-hidden">
4 + <div class="card-header flex gap-4 items-center justify-between">
5 + <div class="title">{{ title }}</div>
6 + <div class="icon">
7 + <slot name="icon"></slot>
8 + </div>
9 + </div>
10 + <div class="flex card-content">
11 + <div class="flex flex-col basis-1/2 value-box" :class="firstStatus">
12 + <div class="value">{{ value }}</div>
13 + <div class="label" v-if="firstLabel">{{ firstLabel }}</div>
14 + </div>
15 + <div class="flex flex-col basis-1/2 value-box" :class="secondStatus">
16 + <div class="value">{{ subValue }}</div>
17 + <div class="label" v-if="secondLabel">{{ secondLabel }}</div>
18 + </div>
19 + </div>
20 + </div>
21 + </n-card>
22 +</template>
23 +
24 +<script setup lang="ts">
25 +import { NCard } from "naive-ui"
26 +import { toRefs } from "vue"
27 +
28 +const props = defineProps<{
29 + title: string
30 + value?: number | string
31 + subValue?: number | string
32 + firstLabel?: string
33 + secondLabel?: string
34 + firstStatus?: "success" | "warning" | "error"
35 + secondStatus?: "success" | "warning" | "error"
36 + hovered?: boolean
37 +}>()
38 +const { title, value, subValue, firstLabel, secondLabel, firstStatus, secondStatus } = toRefs(props)
39 +</script>
40 +
41 +<style scoped lang="scss">
42 +.n-card {
43 + overflow: hidden;
44 +
45 + .card-header {
46 + border-bottom: var(--border-small-050);
47 + overflow: hidden;
48 + padding: 10px 16px;
49 +
50 + .title {
51 + font-size: 16px;
52 + text-overflow: ellipsis;
53 + white-space: nowrap;
54 + overflow: hidden;
55 + }
56 + }
57 +
58 + .card-content {
59 + .value-box {
60 + text-align: center;
61 + overflow: hidden;
62 +
63 + &:first-child {
64 + border-right: var(--border-small-050);
65 + }
66 +
67 + .value {
68 + font-family: var(--font-family-display);
69 + padding: 10px 6px;
70 + font-size: 22px;
71 + text-overflow: ellipsis;
72 + white-space: nowrap;
73 + overflow: hidden;
74 + font-weight: bold;
75 + text-overflow: ellipsis;
76 + white-space: nowrap;
77 + line-height: 1;
78 + overflow: hidden;
79 + }
80 +
81 + .label {
82 + font-family: var(--font-family-mono);
83 + border-top: var(--border-small-050);
84 + color: var(--fg-secondary-color);
85 + font-size: 13px;
86 + padding: 6px;
87 + line-height: 1;
88 + background-color: var(--bg-secondary-color);
89 + text-overflow: ellipsis;
90 + white-space: nowrap;
91 + overflow: hidden;
92 + text-transform: uppercase;
93 + }
94 +
95 + &.success {
96 + .value {
97 + color: var(--success-color);
98 + }
99 + .label {
100 + color: var(--success-color);
101 + }
102 + }
103 +
104 + &.warning {
105 + .value {
106 + color: var(--warning-color);
107 + }
108 + .label {
109 + color: var(--warning-color);
110 + }
111 + }
112 +
113 + &.error {
114 + .value {
115 + color: var(--error-color);
116 + }
117 + .label {
118 + color: var(--error-color);
119 + }
120 + }
121 + }
122 + }
123 +
124 + &.hovered {
125 + &:hover {
126 + border-color: var(--primary-color);
127 + }
128 + }
129 +}
130 +</style>
src/components/common/CardStatsIcon.vue new
+59
@@ -0,0 +1,59 @@
1 +<template>
2 + <div class="icon" :class="{ boxed }" :style="`--size:${boxSize}px`">
3 + <div class="bg" v-if="boxed"></div>
4 + <Icon :size="iconFinalSize" v-if="$slots.default">
5 + <slot></slot>
6 + </Icon>
7 + <Icon :size="iconFinalSize" :name="iconName" v-else></Icon>
8 + </div>
9 +</template>
10 +
11 +<script setup lang="ts">
12 +import { toRefs, computed } from "vue"
13 +import { useThemeStore } from "@/stores/theme"
14 +import Icon from "@/components/common/Icon.vue"
15 +
16 +const props = withDefaults(
17 + defineProps<{
18 + boxSize?: number
19 + iconSize?: number
20 + iconName?: string
21 + boxed?: boolean
22 + color?: string
23 + }>(),
24 + { boxSize: 40, iconSize: 28, boxed: false }
25 +)
26 +const { boxed, boxSize, iconSize, color } = toRefs(props)
27 +
28 +const style = computed<{ [key: string]: any }>(() => useThemeStore().style)
29 +
30 +const iconColor = computed(() => color?.value || style.value["--primary-color"])
31 +const iconBoxedSize = computed(() => (boxSize.value / 100) * 45)
32 +const iconFinalSize = computed(() => (boxed?.value ? iconBoxedSize.value : iconSize.value))
33 +</script>
34 +
35 +<style scoped lang="scss">
36 +.icon {
37 + color: v-bind(iconColor);
38 + width: var(--size);
39 + display: flex;
40 + align-items: center;
41 + justify-content: center;
42 + position: relative;
43 +
44 + &.boxed {
45 + height: var(--size);
46 +
47 + .bg {
48 + background-color: v-bind(iconColor);
49 + opacity: 0.1;
50 + position: absolute;
51 + top: 0;
52 + left: 0;
53 + border-radius: 50%;
54 + width: 100%;
55 + height: 100%;
56 + }
57 + }
58 +}
59 +</style>
src/components/graylog/Pipelines/PipeList.vue new
+114
@@ -0,0 +1,114 @@
1 +<template>
2 + <div class="pipe-list">
3 + <n-card>
4 + <n-spin :show="loading">
5 + <n-collapse
6 + v-model:expanded-names="selectedPipeline"
7 + accordion
8 + :style="minHeight ? `min-height: ${minHeight}` : ''"
9 + >
10 + <n-collapse-item :title="pipe.title" :name="pipe.id" v-for="pipe of pipelines" :key="pipe.id">
11 + <template #header>
12 + <PipeTitle :pipeline="pipe" />
13 + </template>
14 + <template #header-extra>
15 + <n-button size="small" @click.stop="openModal(pipe)">
16 + <template #icon>
17 + <Icon :name="InfoIcon"></Icon>
18 + </template>
19 + </n-button>
20 + </template>
21 + <div class="overflow-hidden">
22 + <PipeDetails :pipeline="pipe" @click-rule="emit('openRule', $event)" />
23 + </div>
24 + </n-collapse-item>
25 + </n-collapse>
26 + </n-spin>
27 + </n-card>
28 +
29 + <n-modal
30 + v-model:show="showDetails"
31 + preset="card"
32 + content-style="padding:0px"
33 + :style="{ maxWidth: 'min(600px, 90vw)', overflow: 'hidden' }"
34 + :title="highlightPipe?.title"
35 + :bordered="false"
36 + segmented
37 + >
38 + <PipeInfo :pipeline="highlightPipe" />
39 + </n-modal>
40 + </div>
41 +</template>
42 +
43 +<script setup lang="ts">
44 +import { useMessage, NCollapse, NCollapseItem, NSpin, NButton, NModal, NCard } from "naive-ui"
45 +import { onBeforeMount, ref, toRefs, watch } from "vue"
46 +import type { PipelineFull } from "@/types/graylog/pipelines.d"
47 +import Api from "@/api"
48 +import Icon from "@/components/common/Icon.vue"
49 +import PipeDetails from "@/components/graylog/Pipelines/PipeDetails.vue"
50 +import PipeInfo from "@/components/graylog/Pipelines/PipeInfo.vue"
51 +import PipeTitle from "@/components/graylog/Pipelines/PipeTitle.vue"
52 +
53 +const props = defineProps<{
54 + minHeight?: string
55 +}>()
56 +const { minHeight } = toRefs(props)
57 +
58 +const emit = defineEmits<{
59 + (e: "openRule", value: string): void
60 +}>()
61 +
62 +const InfoIcon = "carbon:information"
63 +
64 +const message = useMessage()
65 +const showDetails = ref(false)
66 +const loading = ref(false)
67 +const pipelines = ref<PipelineFull[]>([])
68 +const selectedPipeline = ref<string | null>(null)
69 +const highlightPipe = ref<PipelineFull | undefined>(undefined)
70 +const highlightRule = ref<string | null>(null)
71 +const showRulesDrawer = ref(false)
72 +
73 +function setHighlightPipe(pipeline: PipelineFull) {
74 + highlightPipe.value = pipeline
75 +}
76 +
77 +function openModal(pipeline: PipelineFull) {
78 + setHighlightPipe(pipeline)
79 + showDetails.value = true
80 +}
81 +
82 +function getPipelines() {
83 + loading.value = true
84 +
85 + Api.graylog
86 + .getPipelinesFull()
87 + .then(res => {
88 + if (res.data.success) {
89 + pipelines.value = res.data.pipelines || []
90 + if (pipelines.value.length && !selectedPipeline.value) {
91 + selectedPipeline.value = pipelines.value[0].id
92 + }
93 + } else {
94 + message.warning(res.data?.message || "An error occurred. Please try again later.")
95 + }
96 + })
97 + .catch(err => {
98 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
99 + })
100 + .finally(() => {
101 + loading.value = false
102 + })
103 +}
104 +
105 +watch(showRulesDrawer, val => {
106 + if (!val) {
107 + highlightRule.value = null
108 + }
109 +})
110 +
111 +onBeforeMount(() => {
112 + getPipelines()
113 +})
114 +</script>
src/components/indices/Marquee.vue
+44 -6
@@ -12,7 +12,7 @@
12 gradient-length="10%"
13 >
14 <span
15 - v-for="item in indices"
15 + v-for="item in list"
16 :key="item.index"
17 class="item flex items-center gap-2"
18 :class="item.health"
@@ -25,7 +25,7 @@
25 </Vue3Marquee>
26 </n-spin>
27 </n-card>
28 - <div class="info" v-if="indices?.length">
28 + <div class="info" v-if="list?.length">
29 <i class="mdi mdi-information-outline"></i>
30 Click on an index to select
31 </div>
@@ -33,25 +33,63 @@
33 </template>
34
35 <script setup lang="ts">
36 -import { computed, toRefs } from "vue"
36 +import { computed, ref, toRefs, watch, onBeforeMount } from "vue"
37 import type { IndexStats } from "@/types/indices.d"
38 import { Vue3Marquee } from "vue3-marquee"
39 import IndexIcon from "@/components/indices/IndexIcon.vue"
40 -import { NSpin, NCard } from "naive-ui"
40 +import { NSpin, NCard, useMessage } from "naive-ui"
41 import { useThemeStore } from "@/stores/theme"
42 +import Api from "@/api"
43
44 const emit = defineEmits<{
45 (e: "click", value: IndexStats): void
46 }>()
47
48 const props = defineProps<{
48 - indices: IndexStats[] | null
49 + indices?: IndexStats[] | null
50 }>()
51 const { indices } = toRefs(props)
52
53 +const list = ref(indices.value)
54 +
55 +watch(indices, val => {
56 + list.value = val
57 +})
58 +
59 +const message = useMessage()
60 const style = computed<{ [key: string]: any }>(() => useThemeStore().style)
61 const gradientColor = computed(() => style.value["--bg-color-rgb"].split(", "))
54 -const loading = computed(() => !indices?.value || indices.value === null)
62 +const loading = computed(() => !list?.value || list.value === null)
63 +
64 +function getIndices() {
65 + Api.indices
66 + .getIndices()
67 + .then(res => {
68 + if (res.data.success) {
69 + list.value = res.data.indices_stats
70 + } else {
71 + message.error(res.data?.message || "An error occurred. Please try again later.")
72 + }
73 + })
74 + .catch(err => {
75 + if (err.response?.status === 401) {
76 + message.error(
77 + err.response?.data?.message ||
78 + "Wazuh-Indexer returned Unauthorized. Please check your connector credentials."
79 + )
80 + } else if (err.response?.status === 404) {
81 + message.error(err.response?.data?.message || "No indices were found.")
82 + } else {
83 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
84 + }
85 + })
86 +}
87 +
88 +onBeforeMount(() => {
89 + if (indices.value === undefined) {
90 + getIndices()
91 + }
92 +})
93 </script>
94
95 <style lang="scss" scoped>
src/components/overview/AgentsCard.vue new
+85
@@ -0,0 +1,85 @@
1 +<template>
2 + <n-spin :show="loading">
3 + <CardStatsDouble
4 + title="Agents"
5 + firstLabel="Total"
6 + hovered
7 + class="cursor-pointer"
8 + @click="gotoAgentsPage()"
9 + :value="total"
10 + secondLabel="Online"
11 + :subValue="onlineTotal"
12 + :secondStatus="onlineTotal ? 'success' : undefined"
13 + >
14 + <template #icon>
15 + <CardStatsIcon :iconName="AgentsIcon" boxed :boxSize="30"></CardStatsIcon>
16 + </template>
17 + </CardStatsDouble>
18 + </n-spin>
19 +</template>
20 +
21 +<script setup lang="ts">
22 +import { computed, onBeforeMount, ref } from "vue"
23 +import { type Agent } from "@/types/agents.d"
24 +import { isAgentOnline } from "@/components/agents/utils"
25 +import CardStatsDouble from "@/components/common/CardStatsDouble.vue"
26 +import CardStatsIcon from "@/components/common/CardStatsIcon.vue"
27 +import Api from "@/api"
28 +import { useMessage, NSpin } from "naive-ui"
29 +import { useRouter } from "vue-router"
30 +
31 +const AgentsIcon = "carbon:network-3"
32 +const router = useRouter()
33 +const message = useMessage()
34 +const loading = ref(false)
35 +const agents = ref<Agent[]>([])
36 +
37 +const total = computed<number>(() => {
38 + return agents.value.length || 0
39 +})
40 +
41 +const onlineTotal = computed(() => {
42 + return agents.value.filter(({ online }) => online).length || 0
43 +})
44 +
45 +function getData() {
46 + loading.value = true
47 +
48 + Api.agents
49 + .getAgents()
50 + .then(res => {
51 + if (res.data.success) {
52 + agents.value = (res.data.agents || []).map(o => {
53 + o.online = isAgentOnline(o.wazuh_last_seen)
54 + return o
55 + })
56 + } else {
57 + message.error(res.data?.message || "An error occurred. Please try again later.")
58 + }
59 + })
60 + .catch(err => {
61 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
62 + })
63 + .finally(() => {
64 + loading.value = false
65 + })
66 +}
67 +
68 +function gotoAgentsPage() {
69 + router.push(`/agents`).catch(() => {})
70 +}
71 +
72 +onBeforeMount(() => {
73 + getData()
74 +})
75 +</script>
76 +
77 +<style lang="scss" scoped>
78 +.n-spin-container {
79 + :deep() {
80 + .n-spin-content {
81 + height: 100%;
82 + }
83 + }
84 +}
85 +</style>
src/components/overview/CustomersCard.vue new
+79
@@ -0,0 +1,79 @@
1 +<template>
2 + <n-spin :show="loading">
3 + <CardStats
4 + title="Customers"
5 + :value="total"
6 + :vertical="vertical"
7 + hovered
8 + class="cursor-pointer h-full"
9 + @click="gotoCustomersPage()"
10 + >
11 + <template #icon>
12 + <CardStatsIcon :iconName="CustomersIcon" boxed :boxSize="40"></CardStatsIcon>
13 + </template>
14 + </CardStats>
15 + </n-spin>
16 +</template>
17 +
18 +<script setup lang="ts">
19 +import { computed, onBeforeMount, ref, toRefs } from "vue"
20 +import CardStatsIcon from "@/components/common/CardStatsIcon.vue"
21 +import CardStats from "@/components/common/CardStats.vue"
22 +import Api from "@/api"
23 +import { useMessage, NSpin } from "naive-ui"
24 +import type { Customer } from "@/types/customers.d"
25 +import { useRouter } from "vue-router"
26 +
27 +const props = defineProps<{
28 + vertical?: boolean
29 +}>()
30 +const { vertical } = toRefs(props)
31 +
32 +const CustomersIcon = "carbon:user-multiple"
33 +const router = useRouter()
34 +const message = useMessage()
35 +const loading = ref(false)
36 +const customers = ref<Customer[]>([])
37 +
38 +const total = computed<number>(() => {
39 + return customers.value.length || 0
40 +})
41 +
42 +function getData() {
43 + loading.value = true
44 +
45 + Api.customers
46 + .getCustomers()
47 + .then(res => {
48 + if (res.data.success) {
49 + customers.value = res.data?.customers || []
50 + } else {
51 + message.warning(res.data?.message || "An error occurred. Please try again later.")
52 + }
53 + })
54 + .catch(err => {
55 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
56 + })
57 + .finally(() => {
58 + loading.value = false
59 + })
60 +}
61 +
62 +function gotoCustomersPage() {
63 + router.push(`/customers`).catch(() => {})
64 +}
65 +
66 +onBeforeMount(() => {
67 + getData()
68 +})
69 +</script>
70 +
71 +<style lang="scss" scoped>
72 +.n-spin-container {
73 + :deep() {
74 + .n-spin-content {
75 + height: 100%;
76 + }
77 + }
78 +}
79 +</style>
src/components/overview/HealthcheckCard.vue new
+83
@@ -0,0 +1,83 @@
1 +<template>
2 + <n-spin :show="loading">
3 + <CardStatsDouble
4 + title="Healthcheck"
5 + firstLabel="Total"
6 + :value="total"
7 + hovered
8 + class="cursor-pointer"
9 + @click="gotoHealthcheckPage()"
10 + secondLabel="Critical"
11 + :subValue="criticalTotal"
12 + :secondStatus="criticalTotal ? 'warning' : undefined"
13 + >
14 + <template #icon>
15 + <CardStatsIcon :iconName="HealthcheckIcon" boxed :boxSize="30"></CardStatsIcon>
16 + </template>
17 + </CardStatsDouble>
18 + </n-spin>
19 +</template>
20 +
21 +<script setup lang="ts">
22 +import { computed, onBeforeMount, ref } from "vue"
23 +import CardStatsDouble from "@/components/common/CardStatsDouble.vue"
24 +import CardStatsIcon from "@/components/common/CardStatsIcon.vue"
25 +import Api from "@/api"
26 +import { useMessage, NSpin } from "naive-ui"
27 +import { InfluxDBAlertLevel, type InfluxDBAlert } from "@/types/healthchecks.d"
28 +import { useRouter } from "vue-router"
29 +
30 +const HealthcheckIcon = "ph:heartbeat"
31 +const router = useRouter()
32 +const message = useMessage()
33 +const loading = ref(false)
34 +const healthcheck = ref<InfluxDBAlert[]>([])
35 +
36 +const total = computed<number>(() => {
37 + return healthcheck.value.length || 0
38 +})
39 +
40 +const criticalTotal = computed<number>(() => {
41 + return healthcheck.value.filter(o => o.level === InfluxDBAlertLevel.Crit).length || 0
42 +})
43 +
44 +function getData() {
45 + loading.value = true
46 +
47 + Api.healthchecks
48 + .getHealthchecks()
49 + .then(res => {
50 + if (res.data.success) {
51 + healthcheck.value = res.data.alerts || []
52 + } else {
53 + message.warning(res.data?.message || "An error occurred. Please try again later.")
54 + }
55 + })
56 + .catch(err => {
57 + healthcheck.value = []
58 +
59 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
60 + })
61 + .finally(() => {
62 + loading.value = false
63 + })
64 +}
65 +
66 +function gotoHealthcheckPage() {
67 + router.push(`/healthcheck`).catch(() => {})
68 +}
69 +
70 +onBeforeMount(() => {
71 + getData()
72 +})
73 +</script>
74 +
75 +<style lang="scss" scoped>
76 +.n-spin-container {
77 + :deep() {
78 + .n-spin-content {
79 + height: 100%;
80 + }
81 + }
82 +}
83 +</style>
src/components/overview/SocAlertsCard.vue new
+79
@@ -0,0 +1,79 @@
1 +<template>
2 + <n-spin :show="loading">
3 + <CardStats
4 + title="SOC Alerts"
5 + :value="total"
6 + :vertical="vertical"
7 + hovered
8 + class="cursor-pointer h-full"
9 + @click="gotoSocAlertsPage()"
10 + >
11 + <template #icon>
12 + <CardStatsIcon :iconName="SOCIcon" boxed :boxSize="40"></CardStatsIcon>
13 + </template>
14 + </CardStats>
15 + </n-spin>
16 +</template>
17 +
18 +<script setup lang="ts">
19 +import { computed, onBeforeMount, ref, toRefs } from "vue"
20 +import CardStatsIcon from "@/components/common/CardStatsIcon.vue"
21 +import CardStats from "@/components/common/CardStats.vue"
22 +import Api from "@/api"
23 +import { useMessage, NSpin } from "naive-ui"
24 +import type { SocAlert } from "@/types/soc/alert.d"
25 +import { useRouter } from "vue-router"
26 +
27 +const props = defineProps<{
28 + vertical?: boolean
29 +}>()
30 +const { vertical } = toRefs(props)
31 +
32 +const SOCIcon = "carbon:security"
33 +const router = useRouter()
34 +const message = useMessage()
35 +const loading = ref(false)
36 +const alerts = ref<SocAlert[]>([])
37 +
38 +const total = computed<number>(() => {
39 + return alerts.value.length || 0
40 +})
41 +
42 +function getData() {
43 + loading.value = true
44 +
45 + Api.soc
46 + .getAlerts()
47 + .then(res => {
48 + if (res.data.success) {
49 + alerts.value = res.data?.alerts || []
50 + } else {
51 + message.warning(res.data?.message || "An error occurred. Please try again later.")
52 + }
53 + })
54 + .catch(err => {
55 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
56 + })
57 + .finally(() => {
58 + loading.value = false
59 + })
60 +}
61 +
62 +function gotoSocAlertsPage() {
63 + router.push(`/soc/alerts`).catch(() => {})
64 +}
65 +
66 +onBeforeMount(() => {
67 + getData()
68 +})
69 +</script>
70 +
71 +<style lang="scss" scoped>
72 +.n-spin-container {
73 + :deep() {
74 + .n-spin-content {
75 + height: 100%;
76 + }
77 + }
78 +}
79 +</style>
src/layouts/common/Navbar/items.tsx
+16 -1
@@ -3,8 +3,9 @@ import { h } from "vue"
3 import { RouterLink } from "vue-router"
4 import { type MenuMixedOption } from "naive-ui/es/menu/src/interface"
5
6 +const OverviewIcon = "carbon:dashboard"
7 const IndiciesIcon = "ph:list-magnifying-glass"
7 -const AgentsIcon = "iconoir:network-reverse"
8 +const AgentsIcon = "carbon:network-3"
9 const ConnectorsIcon = "carbon:hybrid-networking"
10 const GraylogIcon = "majesticons:pulse-line"
11 const AlertsIcon = "carbon:warning-hex"
@@ -16,6 +17,20 @@ const LogsIcon = "carbon:cloud-logging"
17
18 export default function getItems(mode: "vertical" | "horizontal", collapsed: boolean): MenuMixedOption[] {
19 return [
20 + {
21 + label: () =>
22 + h(
23 + RouterLink,
24 + {
25 + to: {
26 + name: "Overview"
27 + }
28 + },
29 + { default: () => "Overview" }
30 + ),
31 + key: "Overview",
32 + icon: renderIcon(OverviewIcon)
33 + },
34 {
35 label: () =>
36 h(
src/router/index.ts
+11 -4
@@ -1,5 +1,6 @@
1 import { createRouter, createWebHistory } from "vue-router"
2 -import Indices from "@/views/Indices.vue"
2 +import Overview from "@/views/Overview.vue"
3 +import Login from "@/views/Auth/Login.vue"
4 import { UserRole } from "@/types/auth.d"
5 import { Layout } from "@/types/theme.d"
6 import { authCheck } from "@/utils/auth"
@@ -9,12 +10,18 @@ const router = createRouter({
10 routes: [
11 {
12 path: "/",
12 - redirect: "/indices"
13 + redirect: "/overview"
14 + },
15 + {
16 + path: "/overview",
17 + name: "Overview",
18 + component: Overview,
19 + meta: { title: "Overview", auth: true, roles: UserRole.All }
20 },
21 {
22 path: "/indices",
23 name: "Indices",
17 - component: Indices,
24 + component: () => import("@/views/Indices.vue"),
25 meta: { title: "Indices", auth: true, roles: UserRole.All }
26 },
27 {
@@ -131,7 +138,7 @@ const router = createRouter({
138 {
139 path: "/login",
140 name: "Login",
134 - component: () => import("@/views/Auth/Login.vue"),
141 + component: Login,
142 meta: { title: "Login", forceLayout: Layout.Blank, checkAuth: true }
143 },
144 {
src/views/Overview.vue new
+108
@@ -0,0 +1,108 @@
1 +<template>
2 + <div class="page" ref="page">
3 + <div class="section justify-end flex">
4 + <ThreatIntelButton size="small" type="primary" />
5 + </div>
6 + <div class="section">
7 + <div class="columns overflow-hidden">
8 + <div class="basis-1/3">
9 + <AgentsCard />
10 + </div>
11 + <div class="basis-1/3">
12 + <HealthcheckCard />
13 + </div>
14 + <div class="basis-1/3 flex gap-6">
15 + <div class="grow overflow-hidden">
16 + <SocAlertsCard class="h-full" :vertical="cardDirection === 'vertical'" />
17 + </div>
18 + <div class="grow overflow-hidden">
19 + <CustomersCard class="h-full" :vertical="cardDirection === 'vertical'" />
20 + </div>
21 + </div>
22 + </div>
23 + </div>
24 + <div class="section">
25 + <IndicesMarquee @click="gotoIndicesPage" />
26 + </div>
27 + <div class="section">
28 + <div class="columns">
29 + <div class="col basis-1/2">
30 + <ClusterHealth class="stretchy" />
31 + </div>
32 + <div class="col basis-1/2">
33 + <NodeAllocation class="stretchy" />
34 + </div>
35 + </div>
36 + </div>
37 + <div class="section">
38 + <PipeList minHeight="28px" @open-rule="gotoPipelinesPage($event)" />
39 + </div>
40 + </div>
41 +</template>
42 +
43 +<script setup lang="ts">
44 +import { ref } from "vue"
45 +import { useRouter } from "vue-router"
46 +import ClusterHealth from "@/components/indices/ClusterHealth.vue"
47 +import NodeAllocation from "@/components/indices/NodeAllocation.vue"
48 +import IndicesMarquee from "@/components/indices/Marquee.vue"
49 +import ThreatIntelButton from "@/components/alerts/ThreatIntelButton.vue"
50 +import AgentsCard from "@/components/overview/AgentsCard.vue"
51 +import HealthcheckCard from "@/components/overview/HealthcheckCard.vue"
52 +import SocAlertsCard from "@/components/overview/SocAlertsCard.vue"
53 +import CustomersCard from "@/components/overview/CustomersCard.vue"
54 +import PipeList from "@/components/graylog/Pipelines/PipeList.vue"
55 +import type { IndexStats } from "@/types/indices.d"
56 +import { useResizeObserver } from "@vueuse/core"
57 +
58 +const router = useRouter()
59 +const page = ref()
60 +const cardDirection = ref<"horizontal" | "vertical">("horizontal")
61 +
62 +function gotoIndicesPage(index: IndexStats) {
63 + router.push(`/indices?index_name=${index.index}`).catch(() => {})
64 +}
65 +
66 +function gotoPipelinesPage(rule: string) {
67 + router.push(`/graylog/pipelines?rule=${rule}`).catch(() => {})
68 +}
69 +
70 +useResizeObserver(page, entries => {
71 + const entry = entries[0]
72 + const { width } = entry.contentRect
73 +
74 + cardDirection.value = width > 500 ? "horizontal" : "vertical"
75 +})
76 +</script>
77 +
78 +<style lang="scss" scoped>
79 +.page {
80 + .section {
81 + @apply mb-6;
82 +
83 + .columns {
84 + display: flex;
85 + @apply gap-6;
86 +
87 + .stretchy {
88 + height: 100%;
89 + }
90 + }
91 + }
92 +
93 + @media (max-width: 1000px) {
94 + .section {
95 + .columns {
96 + flex-direction: column;
97 + }
98 + }
99 + }
100 + @media (max-width: 1200px) {
101 + .section {
102 + .columns.column-1200 {
103 + flex-direction: column;
104 + }
105 + }
106 + }
107 +}
108 +</style>
src/views/graylog/Pipelines.vue
+9 -63
@@ -8,27 +8,8 @@
8 View All Rules
9 </n-button>
10 </div>
11 - <n-card>
12 - <n-spin :show="loading">
13 - <n-collapse v-model:expanded-names="selectedPipeline" accordion style="min-height: 100px">
14 - <n-collapse-item :title="pipe.title" :name="pipe.id" v-for="pipe of pipelines" :key="pipe.id">
15 - <template #header>
16 - <PipeTitle :pipeline="pipe" />
17 - </template>
18 - <template #header-extra>
19 - <n-button size="small" @click.stop="openModal(pipe)">
20 - <template #icon>
21 - <Icon :name="InfoIcon"></Icon>
22 - </template>
23 - </n-button>
24 - </template>
25 - <div class="overflow-hidden">
26 - <PipeDetails :pipeline="pipe" @click-rule="openRule($event)" />
27 - </div>
28 - </n-collapse-item>
29 - </n-collapse>
30 - </n-spin>
31 - </n-card>
11 +
12 + <PipeList @open-rule="openRule($event)" minHeight="100px" />
13
14 <n-modal
15 v-model:show="showDetails"
@@ -61,24 +42,19 @@
42 </template>
43
44 <script setup lang="ts">
64 -import { useMessage, NCollapse, NCollapseItem, NSpin, NButton, NModal, NCard, NDrawer, NDrawerContent } from "naive-ui"
45 +import { NButton, NModal, NDrawer, NDrawerContent } from "naive-ui"
46 import { onBeforeMount, ref, watch } from "vue"
47 import type { PipelineFull } from "@/types/graylog/pipelines.d"
67 -import Api from "@/api"
48 import Icon from "@/components/common/Icon.vue"
69 -import PipeDetails from "@/components/graylog/Pipelines/PipeDetails.vue"
49 +import PipeList from "@/components/graylog/Pipelines/PipeList.vue"
50 import PipeInfo from "@/components/graylog/Pipelines/PipeInfo.vue"
71 -import PipeTitle from "@/components/graylog/Pipelines/PipeTitle.vue"
51 import RulesList from "@/components/graylog/Pipelines/RulesList.vue"
52 +import { useRoute } from "vue-router"
53
54 const RulesIcon = "ic:outline-swipe-right-alt"
75 -const InfoIcon = "carbon:information"
55
77 -const message = useMessage()
56 +const route = useRoute()
57 const showDetails = ref(false)
79 -const loading = ref(false)
80 -const pipelines = ref<PipelineFull[]>([])
81 -const selectedPipeline = ref<string | null>(null)
58 const highlightPipe = ref<PipelineFull | undefined>(undefined)
59 const highlightRule = ref<string | null>(null)
60 const showRulesDrawer = ref(false)
@@ -89,38 +65,6 @@ function openRule(id: string) {
65 showRulesDrawer.value = true
66 }
67
92 -function setHighlightPipe(pipeline: PipelineFull) {
93 - highlightPipe.value = pipeline
94 -}
95 -
96 -function openModal(pipeline: PipelineFull) {
97 - setHighlightPipe(pipeline)
98 - showDetails.value = true
99 -}
100 -
101 -function getPipelines() {
102 - loading.value = true
103 -
104 - Api.graylog
105 - .getPipelinesFull()
106 - .then(res => {
107 - if (res.data.success) {
108 - pipelines.value = res.data.pipelines || []
109 - if (pipelines.value.length && !selectedPipeline.value) {
110 - selectedPipeline.value = pipelines.value[0].id
111 - }
112 - } else {
113 - message.warning(res.data?.message || "An error occurred. Please try again later.")
114 - }
115 - })
116 - .catch(err => {
117 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
118 - })
119 - .finally(() => {
120 - loading.value = false
121 - })
122 -}
123 -
68 watch(showRulesDrawer, val => {
69 if (!val) {
70 highlightRule.value = null
@@ -128,6 +72,8 @@ watch(showRulesDrawer, val => {
72 })
73
74 onBeforeMount(() => {
131 - getPipelines()
75 + if (route.query?.rule) {
76 + openRule(route.query.rule.toString())
77 + }
78 })
79 </script>