main
vue 221 lines 6.27 KB
Raw
1 <template>
2 <div class="@container flex flex-col gap-4">
3 <n-alert type="info">
4 SCA Policies provides CIS benchmark policies for Security Configuration Assessment using
5 <a
6 href="https://documentation.wazuh.com/current/user-manual/capabilities/sec-config-assessment/index.html"
7 target="_blank"
8 >
9 Wazuh Security Configuration Assessment (SCA)
10 </a>
11 . Deploy policy
12 <code>.yml</code>
13 files to your endpoints under
14 <code>/var/ossec/ruleset/sca/</code>
15 , set ownership, and restart the Wazuh agent. See
16 <a href="https://github.com/socfortress/CoPilot-SCA" target="_blank">CoPilot-SCA</a>
17 for available policies.
18 </n-alert>
19
20 <div class="flex flex-col">
21 <div class="flex flex-wrap items-center justify-end gap-2">
22 <div class="flex min-w-80 grow gap-2">
23 <n-popover overlap placement="bottom-start">
24 <template #trigger>
25 <div class="bg-default rounded-lg">
26 <n-button size="small" class="cursor-help!">
27 <template #icon>
28 <Icon :name="InfoIcon" />
29 </template>
30 </n-button>
31 </div>
32 </template>
33 <div class="flex flex-col gap-2">
34 <div class="box">
35 Total Policies:
36 <code>{{ policies.length }}</code>
37 </div>
38 <div class="box">
39 Filtered:
40 <code>{{ filteredPolicies.length }}</code>
41 </div>
42 </div>
43 </n-popover>
44
45 <n-input
46 v-model:value="searchQuery"
47 size="small"
48 placeholder="Search policies..."
49 class="max-w-120"
50 clearable
51 >
52 <template #prefix>
53 <Icon :name="SearchIcon" />
54 </template>
55 </n-input>
56
57 <n-popover :show="showFilters" trigger="manual" overlap placement="right" class="px-0!">
58 <template #trigger>
59 <div class="bg-default rounded-lg">
60 <n-badge :show="filtered" dot type="success" :offset="[-4, 0]">
61 <n-button size="small" @click="showFilters = true">
62 <template #icon>
63 <Icon :name="FilterIcon" />
64 </template>
65 </n-button>
66 </n-badge>
67 </div>
68 </template>
69 <div class="divide-border flex w-50 flex-col gap-0 divide-y">
70 <div class="flex flex-col gap-2.5 px-3 pt-1 pb-3">
71 <n-select
72 v-model:value="selectedPlatform"
73 :options="platformOptions"
74 size="small"
75 placeholder="Platform"
76 class="w-full"
77 clearable
78 :consistent-menu-width="false"
79 />
80 <n-select
81 v-model:value="selectedApplication"
82 :options="applicationOptions"
83 clearable
84 size="small"
85 placeholder="Application"
86 class="w-full"
87 :consistent-menu-width="false"
88 />
89 </div>
90 <div class="flex justify-between gap-2 px-3 pt-2">
91 <div class="flex justify-start gap-2">
92 <n-button size="small" quaternary @click="showFilters = false">Close</n-button>
93 </div>
94 <div class="flex justify-end gap-2">
95 <n-button size="small" secondary @click="resetFilters()">Reset</n-button>
96 </div>
97 </div>
98 </div>
99 </n-popover>
100 </div>
101 </div>
102
103 <n-spin :show="loading">
104 <div class="my-3">
105 <div
106 v-if="paginatedPolicies.length"
107 class="grid grid-cols-1 gap-4 @2xl:grid-cols-2 @5xl:grid-cols-3 @6xl:grid-cols-4"
108 >
109 <PolicyCard v-for="policy of paginatedPolicies" :key="policy.id" :policy />
110 </div>
111
112 <template v-else>
113 <n-empty v-if="!loading" description="No policies found" class="h-48 justify-center" />
114 </template>
115 </div>
116 </n-spin>
117
118 <div class="flex justify-end">
119 <n-pagination
120 v-if="filteredPolicies.length > pageSize"
121 v-model:page="currentPage"
122 :page-size
123 :item-count="filteredPolicies.length"
124 :page-slot="6"
125 />
126 </div>
127 </div>
128 </div>
129 </template>
130
131 <script setup lang="ts">
132 import type { ScaPolicyItem } from "@/types/sca.d"
133 import { NAlert, NBadge, NButton, NEmpty, NInput, NPagination, NPopover, NSelect, NSpin, useMessage } from "naive-ui"
134 import { computed, onBeforeMount, ref } from "vue"
135 import Api from "@/api"
136 import Icon from "@/components/common/Icon.vue"
137 import PolicyCard from "./PolicyCard.vue"
138
139 const loading = ref(false)
140 const message = useMessage()
141 const policies = ref<ScaPolicyItem[]>([])
142
143 const searchQuery = ref<string | null>(null)
144 const selectedPlatform = ref<string | null>(null)
145 const selectedApplication = ref<string | null>(null)
146 const showFilters = ref(false)
147 const currentPage = ref(1)
148 const pageSize = 24
149
150 const filtered = computed(() => !!selectedPlatform.value || !!selectedApplication.value)
151
152 const InfoIcon = "carbon:information"
153 const FilterIcon = "carbon:filter-edit"
154 const SearchIcon = "carbon:search"
155
156 const platformOptions = computed(() => {
157 const platforms = [...new Set(policies.value.map(p => p.platform))].sort()
158 return platforms.map(p => ({ label: p, value: p }))
159 })
160
161 const applicationOptions = computed(() => {
162 const apps = [...new Set(policies.value.map(p => p.application))].sort()
163 return apps.map(a => ({ label: a, value: a }))
164 })
165
166 const filteredPolicies = computed(() => {
167 let result = policies.value
168
169 if (searchQuery.value) {
170 const q = searchQuery.value.toLowerCase()
171 result = result.filter(
172 p =>
173 p.name.toLowerCase().includes(q) ||
174 p.description.toLowerCase().includes(q) ||
175 p.application.toLowerCase().includes(q) ||
176 p.id.toLowerCase().includes(q)
177 )
178 }
179
180 if (selectedPlatform.value) {
181 result = result.filter(p => p.platform === selectedPlatform.value)
182 }
183
184 if (selectedApplication.value) {
185 result = result.filter(p => p.application === selectedApplication.value)
186 }
187
188 return result
189 })
190
191 const paginatedPolicies = computed(() => {
192 const start = (currentPage.value - 1) * pageSize
193 return filteredPolicies.value.slice(start, start + pageSize)
194 })
195
196 function resetFilters() {
197 selectedPlatform.value = null
198 selectedApplication.value = null
199 showFilters.value = false
200 }
201
202 async function loadPolicies() {
203 loading.value = true
204 try {
205 const res = await Api.sca.getPolicies()
206 if (res.data.success) {
207 policies.value = res.data.policies || []
208 } else {
209 message.warning(res.data?.message || "Failed to load SCA policies")
210 }
211 } catch (err: any) {
212 message.error(err.response?.data?.message || "Failed to load SCA policies")
213 } finally {
214 loading.value = false
215 }
216 }
217
218 onBeforeMount(() => {
219 loadPolicies()
220 })
221 </script>