main
vue 279 lines 7.84 KB
Raw
1 <template>
2 <div class="rule-card-wrap h-full" :class="{ 'is-selected': selectable && selected }">
3 <n-checkbox
4 v-if="selectable"
5 :checked="selected"
6 class="rule-card-checkbox"
7 size="small"
8 @update:checked="emit('update:selected', $event)"
9 @click.stop
10 />
11 <CardEntity
12 hoverable
13 clickable
14 :embedded
15 class="@container h-full"
16 main-box-class="grow"
17 card-entity-wrapper-class="h-full"
18 header-box-class="flex-nowrap! items-start"
19 @click.stop="showDetails = true"
20 >
21 <template #headerMain>
22 <div class="flex flex-wrap items-center gap-2">
23 <Badge v-if="rule.status" :color="getStatusColor(rule.status)" size="small">
24 <template #value>{{ rule.status }}</template>
25 </Badge>
26
27 <Badge v-if="rule.type" type="splitted" size="small">
28 <template #label>type</template>
29 <template #value>{{ rule.type }}</template>
30 </Badge>
31
32 <div v-if="rule.mitre_attack_id?.length" class="flex items-center gap-1">
33 <Badge
34 v-for="mitre of rule.mitre_attack_id.slice(0, 2)"
35 :key="mitre"
36 size="small"
37 color="primary"
38 >
39 <template #value>{{ mitre }}</template>
40 </Badge>
41 <Badge v-if="rule.mitre_attack_id.length > 2" size="small">
42 <template #value>+{{ rule.mitre_attack_id.length - 2 }}</template>
43 </Badge>
44 </div>
45 </div>
46 </template>
47 <template #headerExtra>
48 <div class="text-default pt-.5 flex h-full items-center gap-2">
49 <n-tooltip v-if="provisioned">
50 <template #trigger>
51 <div class="provisioned-chip">
52 <Icon :name="ProvisionedIcon" :size="11" />
53 <span>in Graylog</span>
54 </div>
55 </template>
56 An event definition with this rule's title already exists in Graylog
57 </n-tooltip>
58 <n-tooltip v-if="rule.has_graylog_query">
59 <template #trigger>
60 <Icon :name="GraylogIcon" :size="16" />
61 </template>
62 Has Graylog Query
63 </n-tooltip>
64 </div>
65 </template>
66 <template #default>
67 <div class="flex flex-col gap-2">
68 <div>{{ rule.name }}</div>
69 <p class="line-clamp-3 text-sm">
70 {{ rule.description }}
71 </p>
72 </div>
73 </template>
74 <template #mainExtra>
75 <div class="flex flex-wrap items-center justify-between gap-2">
76 <SeverityBadge :severity="rule.severity" />
77 <Badge>
78 <template #value>
79 <div class="flex items-center gap-2">
80 <Icon :name="platformInfo.icon" :size="14" />
81 <span class="whitespace-nowrap">{{ platformInfo.label }}</span>
82 </div>
83 </template>
84 </Badge>
85 </div>
86 </template>
87 <template #footerExtra>
88 <div class="flex w-full items-center justify-end gap-2">
89 <n-tooltip v-if="rule.has_graylog_query">
90 <template #trigger>
91 <n-button size="small" secondary @click.stop="showProvisionModal = true">
92 <template #icon>
93 <Icon :name="ProvisionIcon" />
94 </template>
95 </n-button>
96 </template>
97 Provision Graylog Alert
98 </n-tooltip>
99 <n-button size="small" type="primary" secondary @click.stop="showExecuteModal = true">
100 <template #icon>
101 <Icon :name="PlayIcon" />
102 </template>
103 Execute
104 </n-button>
105 </div>
106 </template>
107 </CardEntity>
108
109 <!-- Rule Details Modal -->
110 <n-modal
111 v-model:show="showDetails"
112 preset="card"
113 :style="{ maxWidth: 'min(750px, 90vw)', minHeight: 'min(600px, 90vh)', overflow: 'hidden' }"
114 title="Detection Rule"
115 :bordered="false"
116 segmented
117 >
118 <RuleCardContent :rule-id="rule.id" />
119 </n-modal>
120
121 <!-- Execute Search Modal -->
122 <n-modal
123 v-model:show="showExecuteModal"
124 preset="card"
125 :style="{ maxWidth: 'min(550px, 90vw)' }"
126 title="Execute Search"
127 :bordered="false"
128 display-directive="show"
129 segmented
130 >
131 <ExecuteSearchForm
132 :rule-id="rule.id"
133 show-header
134 @success="handleExecuteSuccess"
135 @close="showExecuteModal = false"
136 />
137 </n-modal>
138
139 <!-- Provision Graylog Alert Modal -->
140 <n-modal
141 v-model:show="showProvisionModal"
142 preset="card"
143 :style="{ maxWidth: 'min(550px, 90vw)' }"
144 title="Provision Graylog Alert"
145 :bordered="false"
146 display-directive="show"
147 segmented
148 >
149 <ProvisionGraylogForm
150 :rule-id="rule.id"
151 @success="handleProvisionSuccess"
152 @close="showProvisionModal = false"
153 />
154 </n-modal>
155 </div>
156 </template>
157
158 <script setup lang="ts">
159 import type { BadgeColor } from "@/components/common/Badge.vue"
160 import type { RuleSummary } from "@/types/copilotSearches.d"
161 import { NButton, NCheckbox, NModal, NTooltip, useMessage } from "naive-ui"
162 import { computed, ref } from "vue"
163 import Badge from "@/components/common/Badge.vue"
164 import CardEntity from "@/components/common/cards/CardEntity.vue"
165 import Icon from "@/components/common/Icon.vue"
166 import ExecuteSearchForm from "./ExecuteSearchForm.vue"
167 import ProvisionGraylogForm from "./ProvisionGraylogForm.vue"
168 import RuleCardContent from "./RuleCardContent.vue"
169 import SeverityBadge from "./SeverityBadge.vue"
170
171 const { rule } = defineProps<{
172 rule: RuleSummary
173 embedded?: boolean
174 provisioned?: boolean
175 selectable?: boolean
176 selected?: boolean
177 }>()
178
179 const emit = defineEmits<{
180 (e: "update:selected", value: boolean): void
181 }>()
182
183 const showDetails = ref(false)
184 const showExecuteModal = ref(false)
185 const showProvisionModal = ref(false)
186 const message = useMessage()
187
188 const PlayIcon = "carbon:play"
189 const ProvisionIcon = "carbon:add-alt"
190 const GraylogIcon = "carbon:notification"
191 const ProvisionedIcon = "carbon:checkmark-filled"
192
193 // Platform → icon + label mapping. Mirrors the platforms the CoPilot Searches
194 // backend actually emits (linux, windows, powershell, cve, unknown). Done
195 // locally instead of using the shared PlatformBadge so we cover values like
196 // "powershell" and "cve" that the shared `getOS` util doesn't recognize.
197 const PLATFORM_INFO: Record<string, { icon: string; label: string }> = {
198 linux: { icon: "mdi:linux", label: "Linux" },
199 windows: { icon: "mdi:microsoft-windows", label: "Windows" },
200 powershell: { icon: "mdi:powershell", label: "PowerShell" },
201 cve: { icon: "carbon:security", label: "CVE" }
202 }
203 const platformInfo = computed(() => {
204 const key = (rule.platform || "").toLowerCase()
205 return PLATFORM_INFO[key] || { icon: "mdi:help-box", label: "Unknown" }
206 })
207
208 function getStatusColor(status: string): BadgeColor | undefined {
209 switch (status.toLowerCase()) {
210 case "production":
211 return "success"
212 case "experimental":
213 return "warning"
214 case "deprecated":
215 return "danger"
216 default:
217 return undefined
218 }
219 }
220
221 function handleExecuteSuccess() {
222 showExecuteModal.value = false
223 message.success("Search executed successfully!")
224 }
225
226 function handleProvisionSuccess() {
227 showProvisionModal.value = false
228 }
229 </script>
230
231 <style scoped lang="scss">
232 .provisioned-chip {
233 display: inline-flex;
234 align-items: center;
235 gap: 4px;
236 padding: 2px 6px;
237 font-size: 0.65rem;
238 font-weight: 600;
239 letter-spacing: 0.02em;
240 color: var(--success-color);
241 background: rgba(var(--success-color-rgb) / 0.1);
242 border: 1px solid rgba(var(--success-color-rgb) / 0.4);
243 border-radius: 3px;
244 white-space: nowrap;
245 }
246
247 .rule-card-wrap {
248 position: relative;
249 }
250
251 /* Sits just outside the card's top-left corner like a sticker so it never
252 overlaps the header badges. The card's own click handler still triggers
253 the rule-detail modal; the checkbox stops propagation. */
254 .rule-card-checkbox {
255 position: absolute;
256 top: -7px;
257 left: -7px;
258 z-index: 2;
259 display: flex;
260 align-items: center;
261 justify-content: center;
262 background: var(--bg-default-color);
263 border: 1px solid var(--border-color);
264 border-radius: 4px;
265 padding: 2px;
266 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
267 transition:
268 border-color 0.12s,
269 box-shadow 0.12s;
270 }
271 .rule-card-wrap.is-selected .rule-card-checkbox {
272 border-color: rgba(var(--primary-color-rgb) / 0.6);
273 box-shadow: 0 1px 4px rgba(var(--primary-color-rgb) / 0.4);
274 }
275
276 .rule-card-wrap.is-selected :deep(.card-entity) {
277 box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb) / 0.6);
278 }
279 </style>