main
vue 641 lines 18.5 KB
Raw
1 <template>
2 <div>
3 <n-button :size="size || 'small'" ghost type="primary" :loading @click="analysis()">
4 <template #icon>
5 <Icon :name="AiIcon" />
6 </template>
7 <div class="flex items-center gap-2">
8 <span>Enrich with VirusTotal</span>
9 </div>
10 </n-button>
11
12 <n-modal
13 v-model:show="showModal"
14 preset="card"
15 content-class="p-0!"
16 :style="{ maxWidth: 'min(710px, 90vw)', minHeight: 'min(500px, 90vh)' }"
17 :bordered="false"
18 segmented
19 title="Virus Total Enrichment"
20 >
21 <n-tabs v-if="virusTotalDataResponse" type="line" animated :tabs-padding="24">
22 <n-tab-pane name="Overview" tab="Overview" display-directive="show">
23 <div class="flex flex-col gap-4 p-7 pt-2">
24 <div class="grid-auto-fit-200 grid gap-4">
25 <CardKV>
26 <template #key>type</template>
27 <template #value>{{ virusTotalDataResponse.type }}</template>
28 </CardKV>
29 <CardKV>
30 <template #key>id</template>
31 <template #value>{{ virusTotalDataResponse.id }}</template>
32 </CardKV>
33 </div>
34 <CardStatsBars
35 title="Total votes"
36 class="h-full cursor-pointer"
37 :values="totalVotes"
38 :show-total="false"
39 ></CardStatsBars>
40 <CardStatsBars
41 title="Last Analysis Stats"
42 class="h-full cursor-pointer"
43 :values="lastAnalysisStats"
44 :show-total="false"
45 show-zero-items
46 ></CardStatsBars>
47 <div
48 v-if="virusTotalDataResponse.attributes?.tags?.length"
49 class="text-secondary flex flex-wrap gap-4 text-sm"
50 >
51 <span v-for="tag of virusTotalDataResponse.attributes.tags" :key="tag">#{{ tag }}</span>
52 </div>
53 <ul v-if="Object.keys(virusTotalDataResponse.links || {}).length">
54 <li v-for="link of virusTotalDataResponse.links" :key="link">
55 <a :href="link" target="_blank">{{ link }}</a>
56 </li>
57 </ul>
58 </div>
59 </n-tab-pane>
60 <n-tab-pane name="Analysis Results" tab="Analysis Results" display-directive="show">
61 <div class="p-7 pt-2">
62 <div
63 v-if="Object.keys(virusTotalDataResponse.attributes?.last_analysis_results || {}).length"
64 class="flex flex-col gap-3"
65 >
66 <div
67 v-if="virusTotalDataResponse.attributes.last_analysis_date"
68 class="text-right font-mono text-sm"
69 >
70 last analysis date:
71 <code>
72 {{
73 formatDate(
74 virusTotalDataResponse.attributes.last_analysis_date,
75 dFormats.datetime
76 )
77 }}
78 </code>
79 </div>
80 <CardEntity
81 v-for="(value, key) of virusTotalDataResponse.attributes.last_analysis_results"
82 :key
83 embedded
84 class="@container"
85 >
86 <template #header>{{ key }}</template>
87 <template #footer>
88 <div class="@xs:flex!-row flex flex-col justify-between gap-4 text-sm">
89 <n-statistic class="grow">
90 <template #label>
91 <span class="text-sm">method</span>
92 </template>
93 <span class="text-base">{{ value.method }}</span>
94 </n-statistic>
95 <n-statistic class="grow">
96 <template #label>
97 <span class="text-sm">category</span>
98 </template>
99 <span class="text-base">{{ value.category }}</span>
100 </n-statistic>
101 <n-statistic class="grow">
102 <template #label>
103 <span class="text-sm">result</span>
104 </template>
105 <span class="text-base">{{ value.result }}</span>
106 </n-statistic>
107 </div>
108 </template>
109 </CardEntity>
110 </div>
111 <n-empty v-else description="No items found" class="h-48 justify-center" />
112 </div>
113 </n-tab-pane>
114 <n-tab-pane name="Details" tab="Details" display-directive="show">
115 <div class="p-6 pt-3">
116 <CodeSource :code="properties" decode />
117 </div>
118 </n-tab-pane>
119 <n-tab-pane
120 v-if="virusTotalDataResponse.attributes.whois"
121 name="Whois"
122 tab="Whois"
123 display-directive="show"
124 >
125 <div class="p-6 pt-3">
126 <div
127 v-if="virusTotalDataResponse.attributes.whois_date"
128 class="mb-4 text-right font-mono text-sm"
129 >
130 whois date:
131 <code>
132 {{ formatDate(virusTotalDataResponse.attributes.whois_date, dFormats.datetime) }}
133 </code>
134 </div>
135 <n-input
136 :value="virusTotalDataResponse.attributes.whois"
137 type="textarea"
138 readonly
139 placeholder="Empty"
140 size="large"
141 :autosize="{
142 minRows: 3,
143 maxRows: 18
144 }"
145 />
146 </div>
147 </n-tab-pane>
148 <n-tab-pane name="HTTPS Certificate" tab="HTTPS Certificate" display-directive="show">
149 <div class="flex flex-col gap-3 p-7 pt-2">
150 <div
151 v-if="virusTotalDataResponse.attributes.last_https_certificate_date"
152 class="text-right font-mono text-sm"
153 >
154 last https certificate date:
155 <code>
156 {{
157 formatDate(
158 virusTotalDataResponse.attributes.last_https_certificate_date,
159 dFormats.datetime
160 )
161 }}
162 </code>
163 </div>
164
165 <CardKV value-class="p-0! overflow-x-auto overflow-y-hidden">
166 <template #key>cert_signature</template>
167 <template #value>
168 <n-table
169 v-if="
170 Object.keys(
171 virusTotalDataResponse.attributes?.last_https_certificate.cert_signature
172 ).length
173 "
174 :bordered="false"
175 single-line
176 >
177 <tbody>
178 <tr
179 v-for="(value, key) of virusTotalDataResponse.attributes
180 .last_https_certificate.cert_signature"
181 :key
182 >
183 <td class="text-sm whitespace-nowrap">{{ key }}</td>
184 <td class="text-sm font-semibold whitespace-nowrap">{{ value }}</td>
185 </tr>
186 </tbody>
187 </n-table>
188 </template>
189 </CardKV>
190
191 <CardKV value-class="p-0! overflow-x-auto overflow-y-hidden">
192 <template #key>authority_key_identifier</template>
193 <template #value>
194 <n-table
195 v-if="
196 Object.keys(
197 virusTotalDataResponse.attributes?.last_https_certificate?.extensions
198 ?.authority_key_identifier || {}
199 ).length
200 "
201 :bordered="false"
202 single-line
203 >
204 <tbody>
205 <tr
206 v-for="(value, key) of virusTotalDataResponse.attributes
207 .last_https_certificate.extensions.authority_key_identifier"
208 :key
209 >
210 <td class="text-sm whitespace-nowrap">{{ key }}</td>
211 <td class="text-sm font-semibold whitespace-nowrap">{{ value }}</td>
212 </tr>
213 </tbody>
214 </n-table>
215 </template>
216 </CardKV>
217
218 <CardKV>
219 <template #key>subject_key_identifier</template>
220 <template #value>
221 {{
222 virusTotalDataResponse.attributes.last_https_certificate?.extensions
223 ?.subject_key_identifier || "-"
224 }}
225 </template>
226 </CardKV>
227
228 <CardKV>
229 <template #key>subject_alternative_name</template>
230 <template #value>
231 <span
232 v-if="
233 !virusTotalDataResponse.attributes.last_https_certificate?.extensions
234 ?.subject_alternative_name?.length
235 "
236 >
237 -
238 </span>
239 <div v-else class="flex flex-wrap gap-2">
240 <Badge
241 v-for="value of virusTotalDataResponse.attributes.last_https_certificate
242 ?.extensions?.subject_alternative_name"
243 :key="value"
244 >
245 <template #value>
246 {{ value }}
247 </template>
248 </Badge>
249 </div>
250 </template>
251 </CardKV>
252
253 <CardKV>
254 <template #key>certificate_policies</template>
255 <template #value>
256 <span
257 v-if="
258 !virusTotalDataResponse.attributes.last_https_certificate?.extensions
259 ?.certificate_policies?.length
260 "
261 >
262 -
263 </span>
264 <div v-else class="flex flex-wrap gap-2">
265 <Badge
266 v-for="value of virusTotalDataResponse.attributes.last_https_certificate
267 ?.extensions?.certificate_policies"
268 :key="value"
269 >
270 <template #value>
271 {{ value }}
272 </template>
273 </Badge>
274 </div>
275 </template>
276 </CardKV>
277
278 <CardKV>
279 <template #key>key_usage</template>
280 <template #value>
281 <span
282 v-if="
283 !virusTotalDataResponse.attributes.last_https_certificate?.extensions?.key_usage
284 ?.length
285 "
286 >
287 -
288 </span>
289 <div v-else class="flex flex-wrap gap-2">
290 <Badge
291 v-for="value of virusTotalDataResponse.attributes.last_https_certificate
292 ?.extensions?.key_usage"
293 :key="value"
294 >
295 <template #value>
296 {{ value }}
297 </template>
298 </Badge>
299 </div>
300 </template>
301 </CardKV>
302
303 <CardKV>
304 <template #key>extended_key_usage</template>
305 <template #value>
306 <span
307 v-if="
308 !virusTotalDataResponse.attributes.last_https_certificate?.extensions
309 ?.extended_key_usage?.length
310 "
311 >
312 -
313 </span>
314 <div v-else class="flex flex-wrap gap-2">
315 <Badge
316 v-for="value of virusTotalDataResponse.attributes.last_https_certificate
317 ?.extensions?.extended_key_usage"
318 :key="value"
319 >
320 <template #value>
321 {{ value }}
322 </template>
323 </Badge>
324 </div>
325 </template>
326 </CardKV>
327
328 <CardKV>
329 <template #key>crl_distribution_points</template>
330 <template #value>
331 <span
332 v-if="
333 !virusTotalDataResponse.attributes.last_https_certificate?.extensions
334 ?.crl_distribution_points?.length
335 "
336 >
337 -
338 </span>
339 <ul v-else class="flex flex-wrap gap-2">
340 <li
341 v-for="value of virusTotalDataResponse.attributes.last_https_certificate
342 ?.extensions?.crl_distribution_points"
343 :key="value"
344 >
345 <a :href="value" target="_blank" rel="nofollow noopener noreferrer">
346 {{ value }}
347 </a>
348 </li>
349 </ul>
350 </template>
351 </CardKV>
352
353 <CardKV value-class="p-0! overflow-x-auto overflow-y-hidden">
354 <template #key>ca_information_access</template>
355 <template #value>
356 <n-table
357 v-if="
358 Object.keys(
359 virusTotalDataResponse.attributes?.last_https_certificate?.extensions
360 ?.ca_information_access || {}
361 ).length
362 "
363 :bordered="false"
364 single-line
365 >
366 <tbody>
367 <tr
368 v-for="(value, key) of virusTotalDataResponse.attributes
369 .last_https_certificate.extensions.ca_information_access"
370 :key
371 >
372 <td class="text-sm whitespace-nowrap">{{ key }}</td>
373 <td class="text-sm font-semibold whitespace-nowrap">
374 <a :href="value" target="_blank" rel="nofollow noopener noreferrer">
375 {{ value }}
376 </a>
377 </td>
378 </tr>
379 </tbody>
380 </n-table>
381 </template>
382 </CardKV>
383
384 <div class="flex gap-3">
385 <CardKV class="grow">
386 <template #key>CA</template>
387 <template #value>
388 {{ virusTotalDataResponse.attributes.last_https_certificate.extensions.CA }}
389 </template>
390 </CardKV>
391 <CardKV class="grow">
392 <template #key>size</template>
393 <template #value>
394 {{ virusTotalDataResponse.attributes.last_https_certificate.size }}
395 </template>
396 </CardKV>
397 <CardKV class="grow">
398 <template #key>version</template>
399 <template #value>
400 {{ virusTotalDataResponse.attributes.last_https_certificate.version }}
401 </template>
402 </CardKV>
403 </div>
404
405 <CardKV value-class="p-0! overflow-x-auto overflow-y-hidden">
406 <template #key>validity</template>
407 <template #value>
408 <n-table
409 v-if="
410 Object.keys(
411 virusTotalDataResponse.attributes?.last_https_certificate?.validity || {}
412 ).length
413 "
414 :bordered="false"
415 single-line
416 >
417 <tbody>
418 <tr
419 v-for="(value, key) of virusTotalDataResponse.attributes
420 .last_https_certificate.validity"
421 :key
422 >
423 <td class="text-sm whitespace-nowrap">{{ key }}</td>
424 <td class="text-sm font-semibold whitespace-nowrap">{{ value }}</td>
425 </tr>
426 </tbody>
427 </n-table>
428 </template>
429 </CardKV>
430
431 <CardKV>
432 <template #key>public_key</template>
433 <template #value>
434 <CodeSource
435 :code="virusTotalDataResponse.attributes.last_https_certificate.public_key"
436 decode
437 />
438 </template>
439 </CardKV>
440
441 <CardKV value-class="p-0! overflow-x-auto overflow-y-hidden">
442 <template #value>
443 <n-table :bordered="false" single-line>
444 <tbody>
445 <tr>
446 <td class="text-sm whitespace-nowrap">thumbprint_sha256</td>
447 <td class="text-sm font-semibold whitespace-nowrap">
448 {{
449 virusTotalDataResponse.attributes.last_https_certificate
450 .thumbprint_sha256
451 }}
452 </td>
453 </tr>
454 <tr>
455 <td class="text-sm whitespace-nowrap">thumbprint</td>
456 <td class="text-sm font-semibold whitespace-nowrap">
457 {{
458 virusTotalDataResponse.attributes.last_https_certificate.thumbprint
459 }}
460 </td>
461 </tr>
462 <tr>
463 <td class="text-sm whitespace-nowrap">serial_number</td>
464 <td class="text-sm font-semibold whitespace-nowrap">
465 {{
466 virusTotalDataResponse.attributes.last_https_certificate
467 .serial_number
468 }}
469 </td>
470 </tr>
471 </tbody>
472 </n-table>
473 </template>
474 </CardKV>
475
476 <CardKV value-class="p-0! overflow-x-auto overflow-y-hidden">
477 <template #key>issuer</template>
478 <template #value>
479 <n-table
480 v-if="
481 Object.keys(
482 virusTotalDataResponse.attributes?.last_https_certificate?.issuer || {}
483 ).length
484 "
485 :bordered="false"
486 single-line
487 >
488 <tbody>
489 <tr
490 v-for="(value, key) of virusTotalDataResponse.attributes
491 .last_https_certificate.issuer"
492 :key
493 >
494 <td class="text-sm whitespace-nowrap">{{ key }}</td>
495 <td class="text-sm font-semibold whitespace-nowrap">{{ value }}</td>
496 </tr>
497 </tbody>
498 </n-table>
499 </template>
500 </CardKV>
501
502 <CardKV value-class="p-0! overflow-x-auto overflow-y-hidden">
503 <template #key>subject</template>
504 <template #value>
505 <n-table
506 v-if="
507 Object.keys(
508 virusTotalDataResponse.attributes?.last_https_certificate?.subject || {}
509 ).length
510 "
511 :bordered="false"
512 single-line
513 >
514 <tbody>
515 <tr
516 v-for="(value, key) of virusTotalDataResponse.attributes
517 .last_https_certificate.subject"
518 :key
519 >
520 <td class="text-sm whitespace-nowrap">{{ key }}</td>
521 <td class="text-sm font-semibold whitespace-nowrap">{{ value }}</td>
522 </tr>
523 </tbody>
524 </n-table>
525 </template>
526 </CardKV>
527 </div>
528 </n-tab-pane>
529 </n-tabs>
530 </n-modal>
531 </div>
532 </template>
533
534 <script setup lang="ts">
535 import type { ButtonSize } from "naive-ui"
536 import type { ItemProps } from "@/components/common/cards/CardStatsBars.vue"
537 import type { VirusTotalData } from "@/types/threatIntel.d"
538 import _pick from "lodash/pick"
539 import { NButton, NEmpty, NInput, NModal, NStatistic, NTable, NTabPane, NTabs, useMessage } from "naive-ui"
540 import { computed, defineAsyncComponent, ref } from "vue"
541 import Api from "@/api"
542 import Icon from "@/components/common/Icon.vue"
543 import { useSettingsStore } from "@/stores/settings"
544 import { formatDate } from "@/utils/format"
545
546 const { iocValue, size } = defineProps<{
547 iocValue: string
548 size?: ButtonSize
549 }>()
550
551 const Badge = defineAsyncComponent(() => import("@/components/common/Badge.vue"))
552 const CardKV = defineAsyncComponent(() => import("@/components/common/cards/CardKV.vue"))
553 const CardEntity = defineAsyncComponent(() => import("@/components/common/cards/CardEntity.vue"))
554 const CardStatsBars = defineAsyncComponent(() => import("@/components/common/cards/CardStatsBars.vue"))
555 const CodeSource = defineAsyncComponent(() => import("@/components/common/CodeSource.vue"))
556
557 const AiIcon = "mage:stars-c"
558 const showModal = ref<boolean>(false)
559 const loading = ref<boolean>(false)
560 const dFormats = useSettingsStore().dateFormat
561 const message = useMessage()
562 const virusTotalDataResponse = ref<VirusTotalData | null>(null)
563 const totalVotes = computed<ItemProps[]>(() =>
564 Object.entries(virusTotalDataResponse.value?.attributes.total_votes || {})
565 .map(([key, value]) => ({
566 value,
567 label: key,
568 status: getStatusByCategory(key)
569 }))
570 .sort((a, b) => a.value - b.value)
571 )
572 const lastAnalysisStats = computed<ItemProps[]>(() =>
573 Object.entries(virusTotalDataResponse.value?.attributes.last_analysis_stats || {})
574 .map(([key, value]) => ({
575 value,
576 label: key,
577 status: getStatusByCategory(key)
578 }))
579 .sort((a, b) => a.value - b.value)
580 )
581 const properties = computed(() => {
582 return _pick(virusTotalDataResponse.value?.attributes || {}, [
583 "regional_internet_registry",
584 "continent",
585 "last_modification_date",
586 "crowdsourced_context",
587 "asn",
588 "reputation",
589 "jarm",
590 "country",
591 "as_owner",
592 "network"
593 ])
594 })
595
596 function getStatusByCategory(category: string) {
597 let status: "success" | "warning" | "error" | "muted" | "primary" = "muted"
598
599 switch (category) {
600 case "harmless":
601 status = "success"
602 break
603 case "malicious":
604 status = "error"
605 break
606 case "suspicious":
607 status = "warning"
608 break
609 case "undetected":
610 case "timeout":
611 status = "muted"
612 break
613 }
614 return status
615 }
616
617 function openResponse() {
618 showModal.value = true
619 }
620
621 function analysis() {
622 loading.value = true
623
624 Api.threatIntel
625 .virusTotalEnrichment(iocValue)
626 .then(res => {
627 if (res.data.success) {
628 virusTotalDataResponse.value = res.data.data.data
629 openResponse()
630 } else {
631 message.warning(res.data?.message || "An error occurred. Please try again later.")
632 }
633 })
634 .catch(err => {
635 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
636 })
637 .finally(() => {
638 loading.value = false
639 })
640 }
641 </script>