main
vue 243 lines 7.11 KB
Raw
1 <template>
2 <n-spin :show="loading">
3 <div v-if="rule" class="flex flex-col gap-4 pb-1">
4 <!-- Basic Information -->
5 <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
6 <PropsList :list="infoFields" embedded title="Information" />
7 <PropsList :list="riskAssessmentFields" embedded title="Risk Assessment" />
8 </div>
9
10 <!-- Description -->
11 <CardKV>
12 <template #key>Description</template>
13 <template #value>{{ rule.description }}</template>
14 </CardKV>
15
16 <!-- Graylog Query -->
17 <CardKV v-if="rule.graylog?.query">
18 <template #key>
19 <div class="flex items-center justify-between gap-2">
20 <div class="flex items-center gap-2">
21 <Icon :name="GraylogIcon" :size="14" />
22 <span>Graylog Query</span>
23 </div>
24 <n-button size="tiny" type="primary" secondary @click="showProvisionModal = true">
25 <template #icon>
26 <Icon :name="ProvisionIcon" />
27 </template>
28 Provision Graylog Alert
29 </n-button>
30 </div>
31 </template>
32 <template #value>
33 <CodeSource :code="rule.graylog.query" lang="sql" />
34 </template>
35 </CardKV>
36
37 <!-- MITRE ATT&CK -->
38 <CardKV v-if="rule.tags.mitre_attack_id?.length">
39 <template #key>MITRE ATT&CK Techniques</template>
40 <template #value>
41 <div class="flex flex-wrap gap-2">
42 <Badge v-for="mitre of rule.tags.mitre_attack_id" :key="mitre" color="primary">
43 <template #value>{{ mitre }}</template>
44 </Badge>
45 </div>
46 </template>
47 </CardKV>
48
49 <!-- Data Sources -->
50 <CardKV v-if="rule.data_source?.length">
51 <template #key>Data Sources</template>
52 <template #value>
53 <div class="flex flex-wrap gap-2">
54 <Badge v-for="source of rule.data_source" :key="source">
55 <template #value>{{ source }}</template>
56 </Badge>
57 </div>
58 </template>
59 </CardKV>
60
61 <!-- Parameters -->
62 <CardKV v-if="rule.parameters?.length">
63 <template #key>Parameters</template>
64 <template #value>
65 <div class="grid grid-cols-1 gap-3 py-1 lg:grid-cols-2">
66 <CardEntity
67 v-for="param in rule.parameters"
68 :key="param.name"
69 embedded
70 size="small"
71 class="h-full"
72 main-box-class="grow"
73 card-entity-wrapper-class="h-full"
74 >
75 <template #headerMain>
76 <div class="text-default flex items-center gap-4">
77 <div class="text-sm font-semibold">{{ param.name }}</div>
78 <Badge :color="param.required ? 'danger' : 'success'" type="splitted">
79 <template #value>
80 <span class="text-xs">{{ param.required ? "Required" : "Optional" }}</span>
81 </template>
82 </Badge>
83 </div>
84 </template>
85 <template #headerExtra>
86 <Badge>
87 <template #value>
88 <span class="text-xs">{{ param.type }}</span>
89 </template>
90 </Badge>
91 </template>
92
93 <template v-if="param.description" #default>
94 <p class="text-xs">{{ param.description }}</p>
95 </template>
96
97 <template #footer>
98 <div class="flex flex-col gap-1">
99 <div
100 v-if="param.default !== null && param.default !== undefined"
101 class="text-xs opacity-60"
102 >
103 <span class="font-medium">Default:</span>
104 <code class="code-block ml-1 rounded px-1 py-0.5 text-xs">
105 {{ param.default }}
106 </code>
107 </div>
108 <div
109 v-if="param.example !== null && param.example !== undefined"
110 class="text-xs opacity-60"
111 >
112 <span class="font-medium">Example:</span>
113 <code class="code-block ml-1 rounded px-1 py-0.5 text-xs">
114 {{ param.example }}
115 </code>
116 </div>
117 </div>
118 </template>
119 </CardEntity>
120 </div>
121 </template>
122 </CardKV>
123
124 <!-- How to Implement -->
125 <CardKV v-if="rule.how_to_implement">
126 <template #key>How to Implement</template>
127 <template #value>{{ rule.how_to_implement }}</template>
128 </CardKV>
129
130 <!-- Known False Positives -->
131 <CardKV v-if="rule.known_false_positives">
132 <template #key>Known False Positives</template>
133 <template #value>{{ rule.known_false_positives }}</template>
134 </CardKV>
135
136 <!-- References -->
137 <CardKV v-if="rule.references?.length">
138 <template #key>References</template>
139 <template #value>
140 <div class="flex flex-col gap-1">
141 <a
142 v-for="item of rule.references"
143 :key="item"
144 :href="item"
145 target="_blank"
146 rel="noopener"
147 class="text-primary-color text-sm hover:underline"
148 >
149 {{ item }}
150 </a>
151 </div>
152 </template>
153 </CardKV>
154
155 <!-- Analytic Stories -->
156 <div v-if="rule.tags.analytic_story?.length" class="flex flex-wrap gap-2">
157 <code v-for="story of rule.tags.analytic_story" :key="story">#{{ story }}</code>
158 </div>
159 </div>
160
161 <n-empty v-else-if="!loading" description="Failed to load rule details" />
162
163 <!-- Provision Graylog Alert Modal -->
164 <n-modal
165 v-model:show="showProvisionModal"
166 preset="card"
167 :style="{ maxWidth: 'min(550px, 90vw)' }"
168 title="Provision Graylog Alert"
169 :bordered="false"
170 display-directive="show"
171 segmented
172 >
173 <ProvisionGraylogForm
174 v-if="rule"
175 :rule-data="rule"
176 @success="showProvisionModal = false"
177 @close="showProvisionModal = false"
178 />
179 </n-modal>
180 </n-spin>
181 </template>
182
183 <script setup lang="ts">
184 import type { RuleDetail } from "@/types/copilotSearches.d"
185 import _pick from "lodash/pick"
186 import { NButton, NEmpty, NModal, NSpin, useMessage } from "naive-ui"
187 import { computed, onBeforeMount, ref } from "vue"
188 import Api from "@/api"
189 import Badge from "@/components/common/Badge.vue"
190 import CardEntity from "@/components/common/cards/CardEntity.vue"
191 import CardKV from "@/components/common/cards/CardKV.vue"
192 import CodeSource from "@/components/common/CodeSource.vue"
193 import Icon from "@/components/common/Icon.vue"
194 import PropsList from "@/components/common/PropsList.vue"
195 import ProvisionGraylogForm from "./ProvisionGraylogForm.vue"
196
197 const props = defineProps<{
198 ruleId?: string
199 ruleData?: RuleDetail
200 }>()
201
202 const loading = ref(false)
203 const rule = ref<RuleDetail | null>(null)
204 const showProvisionModal = ref(false)
205 const message = useMessage()
206
207 const GraylogIcon = "carbon:notification"
208 const ProvisionIcon = "carbon:add-alt"
209
210 const infoFields = computed(() => _pick(rule.value, ["author", "version", "date", "status", "type"]))
211 const riskAssessmentFields = computed(() => ({
212 severity: rule.value?.response.severity,
213 risk_score: rule.value?.response.risk_score,
214 platform: rule.value?.tags.asset_type,
215 security_domain: rule.value?.tags.security_domain
216 }))
217
218 async function loadRule(ruleId: string) {
219 loading.value = true
220 try {
221 const res = await Api.copilotSearches.getRuleById(ruleId)
222 if (res.data.success) {
223 rule.value = res.data.rule
224 } else {
225 message.error(res.data?.message || "Failed to load rule details")
226 }
227 } catch (err: any) {
228 message.error(err.response?.data?.message || "Failed to load rule details")
229 } finally {
230 loading.value = false
231 }
232 }
233
234 onBeforeMount(() => {
235 if (props.ruleData) {
236 rule.value = props.ruleData
237 } else if (props.ruleId) {
238 loadRule(props.ruleId)
239 } else {
240 message.error("No rule data or rule ID provided")
241 }
242 })
243 </script>