main
vue 363 lines 10.4 KB
Raw
1 <template>
2 <n-spin :show="uploading">
3 <div class="flex flex-col gap-3">
4 <div class="flex flex-col gap-1">
5 <n-upload v-model:file-list="fileList" :max="1" :disabled="uploading">
6 <n-upload-dragger>
7 <div>
8 <Icon :name="UploadIcon" :size="28" :depth="3" />
9 </div>
10 <div class="font-medium">Click or drag a file to this area to upload</div>
11 </n-upload-dragger>
12 </n-upload>
13 <n-collapse-transition :show="!!newFile">
14 <div class="mt-1 flex justify-end">
15 <n-checkbox v-model:checked="isPasswordProtected">It is password protected</n-checkbox>
16 </div>
17 </n-collapse-transition>
18 <n-collapse-transition :show="!!newFile && isPasswordProtected">
19 <n-input
20 v-model:value="filePassword"
21 type="password"
22 show-password-on="click"
23 class="mt-3"
24 placeholder="File password"
25 />
26 </n-collapse-transition>
27 </div>
28 <div class="mb-2 flex justify-end">
29 <n-button type="primary" :disabled="!isValid" :loading="uploading" @click="submit()">Submit</n-button>
30 </div>
31 <div v-if="error" class="response bg-secondary error">
32 <div class="px-4 py-2.5">
33 {{ error }}
34 </div>
35 </div>
36 <div v-else class="flex flex-col gap-3">
37 <div v-if="fileResponse" class="response bg-secondary p-4">
38 <div class="flex flex-col gap-4 text-sm">
39 <div class="font-semibold">
40 This link gives you access to the instance created from your uploaded file.
41 </div>
42 <n-alert type="warning">
43 <template #icon>
44 <Icon name="carbon:warning-alt" :size="14" />
45 </template>
46 <template #header>
47 <div class="text-xs">Please note</div>
48 </template>
49 <template #default>
50 <div class="text-xs">
51 Once you close this window, the link cannot be retrieved again. If you think you
52 might need it later, make sure to copy and save it.
53 </div>
54 </template>
55 </n-alert>
56 <div>
57 <a
58 :href="fileResponse.links.self"
59 target="_blank"
60 alt="references url"
61 rel="nofollow noopener noreferrer"
62 class="leading-6"
63 >
64 <span>
65 {{ fileResponse.links.self }}
66 </span>
67 <Icon :name="LinkIcon" :size="14" class="relative top-0.5 ml-2" />
68 </a>
69 </div>
70 <div v-if="isCopySupported" class="flex justify-end">
71 <n-tooltip :show="showCopyTooltip" trigger="manual">
72 <template #trigger>
73 <n-button size="small" secondary @click="copyLink()">
74 <template #icon>
75 <Icon name="carbon:copy" :size="14" />
76 </template>
77 Copy
78 </n-button>
79 </template>
80 <div class="text-xs">Copied!</div>
81 </n-tooltip>
82 </div>
83 </div>
84 </div>
85 <div
86 v-if="fileResponse && !analysisResponse"
87 class="response bg-secondary flex flex-wrap items-center gap-2 p-4"
88 >
89 <Icon :name="LoadingIcon" :size="16" class="relative top-0.5" />
90 analyzing...
91 </div>
92 <n-spin v-if="analysisResponse" :show="loading">
93 <div class="response bg-secondary overflow-hidden">
94 <div class="bg-default flex items-center justify-between p-4">
95 <div>Analysis</div>
96 <n-button :loading secondary size="small" @click="analysis()">
97 <template #icon>
98 <Icon :name="RefreshIcon" :size="14" />
99 </template>
100 Reload
101 </n-button>
102 </div>
103 <div class="divide-border flex flex-col divide-y-2 text-sm">
104 <div class="flex flex-col gap-1 p-4">
105 <div class="text-secondary text-xs">status</div>
106 <div
107 class="flex items-center gap-1"
108 :class="{
109 'text-success': analysisResponse.attributes.status === 'completed',
110 'text-warning': analysisResponse.attributes.status === 'queued'
111 }"
112 >
113 <Icon
114 :name="
115 analysisResponse.attributes.status === 'completed'
116 ? 'carbon:checkmark-outline'
117 : 'carbon:hourglass'
118 "
119 :size="14"
120 />
121 {{ analysisResponse.attributes.status }}
122 </div>
123 </div>
124 <div v-if="!_isEmpty(analysisResponse.attributes.stats)" class="flex flex-col gap-1 p-4">
125 <div class="text-secondary text-xs">stats</div>
126 <div class="divide-border flex flex-col gap-2 divide-y">
127 <div
128 v-for="(val, key) of analysisResponse.attributes.stats"
129 :key
130 class="flex items-end justify-between gap-4"
131 >
132 <div>{{ key }}</div>
133 <div class="text-right font-mono">{{ val }}</div>
134 </div>
135 </div>
136 </div>
137 <div
138 v-if="!_isEmpty(analysisResponse.attributes.results)"
139 class="flex flex-col gap-2 overflow-hidden p-4"
140 >
141 <div class="text-secondary flex items-center justify-between text-xs">
142 <div>results</div>
143 <n-button
144 size="tiny"
145 secondary
146 @click="analysisResultCollapsed = !analysisResultCollapsed"
147 >
148 <template #icon>
149 <Icon
150 :name="
151 analysisResultCollapsed
152 ? 'carbon:chevron-right'
153 : 'carbon:chevron-down'
154 "
155 :size="14"
156 />
157 </template>
158 {{ analysisResultCollapsed ? "expand" : "collapse" }}
159 </n-button>
160 </div>
161 <div v-if="!analysisResultCollapsed" class="flex flex-col gap-4">
162 <div
163 v-for="(resultVal, resultKey) of analysisResponse.attributes.results"
164 :key="resultKey"
165 class="border-default border"
166 >
167 <div class="px-2 py-1">{{ resultKey }}</div>
168 <div class="divide-border bg-default flex flex-col gap-1 divide-y">
169 <div
170 v-for="(val, key) of resultVal"
171 :key
172 class="flex items-end justify-between gap-4 px-2 py-0.5"
173 >
174 <div>{{ key }}</div>
175 <div class="text-right font-mono">{{ val || "—" }}</div>
176 </div>
177 </div>
178 </div>
179 </div>
180 </div>
181 <div v-if="!_isEmpty(analysisResponse.links)" class="flex flex-col gap-1 p-4">
182 <div class="text-secondary text-xs">links</div>
183 <div class="divide-border flex flex-col gap-2 divide-y">
184 <div
185 v-for="(val, key) of analysisResponse.links"
186 :key
187 class="flex items-end justify-between"
188 >
189 <a
190 :href="val"
191 target="_blank"
192 alt="references url"
193 rel="nofollow noopener noreferrer"
194 class="leading-6"
195 >
196 <span>
197 {{ val }}
198 </span>
199 <Icon :name="LinkIcon" :size="14" class="relative top-0.5 ml-2" />
200 </a>
201 </div>
202 </div>
203 </div>
204 </div>
205 </div>
206 </n-spin>
207 </div>
208 </div>
209 </n-spin>
210 </template>
211
212 <script setup lang="ts">
213 import type { UploadFileInfo } from "naive-ui"
214 import type { VirusTotalAnalysis, VirusTotalFileCheckResponse } from "@/types/threatIntel.d"
215 import { useClipboard } from "@vueuse/core"
216 import _isEmpty from "lodash/isEmpty"
217 import {
218 NAlert,
219 NButton,
220 NCheckbox,
221 NCollapseTransition,
222 NInput,
223 NSpin,
224 NTooltip,
225 NUpload,
226 NUploadDragger,
227 useMessage
228 } from "naive-ui"
229 import { computed, onMounted, ref, watch } from "vue"
230 import Api from "@/api"
231 import Icon from "@/components/common/Icon.vue"
232
233 const emit = defineEmits<{
234 (
235 e: "mounted",
236 value: {
237 restore: () => void
238 }
239 ): void
240 }>()
241
242 const LinkIcon = "carbon:launch"
243 const RefreshIcon = "carbon:renew"
244 const LoadingIcon = "eos-icons:loading"
245 const UploadIcon = "carbon:cloud-upload"
246
247 const message = useMessage()
248 const loading = ref(false)
249 const uploading = ref(false)
250 const analysisResultCollapsed = ref(true)
251 const fileResponse = ref<VirusTotalFileCheckResponse | null>(null)
252 const analysisResponse = ref<VirusTotalAnalysis | null>(null)
253 const error = ref<string>("")
254 const fileList = ref<UploadFileInfo[]>([])
255 const newFile = computed<File | null>(() => fileList.value?.[0]?.file || null)
256 const isPasswordProtected = ref(false)
257 const filePassword = ref<string | null>(null)
258 let abortController: AbortController | null = null
259
260 const fileLink = computed(() => fileResponse.value?.links.self || "")
261 const { copy: copyLink, copied: showCopyTooltip, isSupported: isCopySupported } = useClipboard({ source: fileLink })
262
263 const isValid = computed(() => {
264 if (isPasswordProtected.value && !filePassword.value) {
265 return false
266 }
267
268 return !!newFile.value
269 })
270
271 function restore() {
272 fileList.value = []
273 loading.value = false
274 uploading.value = false
275 fileResponse.value = null
276 analysisResponse.value = null
277 error.value = ""
278 abortController?.abort()
279 }
280
281 function submit() {
282 if (!newFile.value) return
283
284 uploading.value = true
285
286 Api.threatIntel
287 .virusTotalFileCheck(newFile.value, filePassword.value || undefined)
288 .then(res => {
289 if (res.data.success) {
290 error.value = ""
291 fileResponse.value = res.data.data
292 analysisResponse.value = null
293
294 analysis()
295 message.success(res.data?.message || "File submitted successfully for analysis.")
296 } else {
297 error.value = res.data?.message || "An error occurred. Please try again later."
298 message.warning(error.value)
299 }
300 })
301 .catch(err => {
302 error.value = err.response?.data?.message || "An error occurred. Please try again later."
303 message.error(error.value)
304 })
305 .finally(() => {
306 uploading.value = false
307 })
308 }
309
310 function analysis() {
311 abortController?.abort()
312
313 if (!fileResponse.value?.id) return
314
315 loading.value = true
316 abortController = new AbortController()
317
318 Api.threatIntel
319 .virusTotalAnalysis(fileResponse.value.id, abortController.signal)
320 .then(res => {
321 if (res.data.success) {
322 error.value = ""
323 analysisResponse.value = res.data.data
324 message.success(res.data?.message || "Analysis status retrieved successfully.")
325 } else {
326 error.value = res.data?.message || "An error occurred. Please try again later."
327 message.warning(error.value)
328 }
329 })
330 .catch(err => {
331 error.value = err.response?.data?.message || "An error occurred. Please try again later."
332 message.error(error.value)
333 })
334 .finally(() => {
335 loading.value = false
336 })
337 }
338
339 watch(newFile, () => {
340 isPasswordProtected.value = false
341 })
342
343 watch(isPasswordProtected, () => {
344 filePassword.value = null
345 })
346
347 onMounted(() => {
348 emit("mounted", {
349 restore
350 })
351 })
352 </script>
353
354 <style scoped lang="scss">
355 .response {
356 border-radius: var(--border-radius);
357 border: 1px solid var(--success-color);
358
359 &.error {
360 border-color: var(--error-color);
361 }
362 }
363 </style>