main
vue 128 lines 4.12 KB
Raw
1 <template>
2 <n-card title="IOC verdict corrections" size="small" segmented>
3 <n-spin :show="loading" class="min-h-40">
4 <div class="flex flex-col gap-3">
5 <p class="text-sm">
6 Toggle off any IOC where the VirusTotal verdict above was wrong. Optionally note why.
7 </p>
8 <div v-if="iocs.length" class="flex flex-col gap-2">
9 <CardEntity v-for="ioc of iocs" :key="ioc.id" size="small" embedded>
10 <template #headerExtra>
11 <div class="flex items-center gap-3">
12 <Badge type="splitted" bright>
13 <template #label>Type</template>
14 <template #value>{{ ioc.ioc_type }}</template>
15 </Badge>
16 <Badge type="splitted" bright :color="verdictColor(ioc.vt_verdict)">
17 <template #label>VT</template>
18 <template #value>{{ ioc.vt_verdict }}</template>
19 </Badge>
20 <n-tooltip placement="top">
21 <template #trigger>
22 <n-switch
23 :value="iocCorrect(ioc.id)"
24 @update:value="setIocCorrect(ioc.id, $event)"
25 />
26 </template>
27 {{ iocCorrect(ioc.id) ? "Verdict correct" : "Verdict wrong" }}
28 </n-tooltip>
29 </div>
30 </template>
31 <template #default>
32 <CodeSource :code="ioc.ioc_value" />
33 </template>
34 <template #mainExtra>
35 <n-input
36 :value="iocNote(ioc.id)"
37 type="textarea"
38 placeholder="Optional reviewer note"
39 :autosize="{ minRows: 1, maxRows: 4 }"
40 @update:value="setIocNote(ioc.id, $event)"
41 />
42 </template>
43 </CardEntity>
44 </div>
45 <n-empty v-else description="No IOCs recorded for this report" class="min-h-24 justify-center" />
46 </div>
47 </n-spin>
48 </n-card>
49 </template>
50
51 <script setup lang="ts">
52 import type { AiAnalystIoc, AiAnalystReport } from "@/types/aiAnalyst.d"
53 import { NCard, NEmpty, NInput, NSpin, NSwitch, NTooltip, useMessage } from "naive-ui"
54 import { onBeforeMount, ref, toRefs } from "vue"
55 import Api from "@/api"
56 import Badge from "@/components/common/Badge.vue"
57 import CardEntity from "@/components/common/cards/CardEntity.vue"
58 import CodeSource from "@/components/common/CodeSource.vue"
59
60 export interface IocState {
61 verdict_correct: boolean
62 note: string
63 }
64
65 const props = defineProps<{
66 report: AiAnalystReport
67 }>()
68
69 const { report } = toRefs(props)
70
71 // Per-IOC review state — keyed by ioc.id so order stays stable with the list
72 const iocState = defineModel<Map<number, IocState>>("state", { required: true, default: () => new Map() })
73 const iocs = defineModel<AiAnalystIoc[]>("iocs", { required: true, default: () => [] })
74
75 const message = useMessage()
76 const loading = ref(false)
77
78 function iocCorrect(iocId: number): boolean {
79 return iocState.value.get(iocId)?.verdict_correct ?? true
80 }
81 function iocNote(iocId: number): string {
82 return iocState.value.get(iocId)?.note ?? ""
83 }
84 function setIocCorrect(iocId: number, val: boolean) {
85 const cur = iocState.value.get(iocId) ?? { verdict_correct: true, note: "" }
86 iocState.value.set(iocId, { ...cur, verdict_correct: val })
87 }
88 function setIocNote(iocId: number, val: string) {
89 const cur = iocState.value.get(iocId) ?? { verdict_correct: true, note: "" }
90 iocState.value.set(iocId, { ...cur, note: val })
91 }
92
93 function verdictColor(verdict: string) {
94 if (verdict === "malicious") return "danger"
95 if (verdict === "suspicious") return "warning"
96 if (verdict === "clean") return "success"
97 return undefined
98 }
99
100 function seedIocDefaults() {
101 // Any IOC not yet in state defaults to "verdict correct". Preserves
102 // per-IOC state hydrated from an existing review.
103 for (const ioc of iocs.value) {
104 if (!iocState.value.has(ioc.id)) {
105 iocState.value.set(ioc.id, { verdict_correct: true, note: "" })
106 }
107 }
108 }
109
110 async function loadIocs() {
111 loading.value = true
112
113 try {
114 const iocsRes = await Api.aiAnalyst.getIocsByReport(report.value.id)
115 if (iocsRes.data.success) iocs.value = iocsRes.data.iocs || []
116 seedIocDefaults()
117 } catch (err: unknown) {
118 const e = err as { response?: { data?: { message?: string } }; message?: string }
119 message.error(e.response?.data?.message || e.message || "Failed to load review data")
120 } finally {
121 loading.value = false
122 }
123 }
124
125 onBeforeMount(() => {
126 loadIocs()
127 })
128 </script>