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>