main
vue 412 lines 11.1 KB
Raw
1 <template>
2 <div class="simulator-windows-attack-wizard flex min-h-120 flex-col gap-4 overflow-hidden">
3 <div>
4 <n-scrollbar x-scrollable trigger="none">
5 <div class="px-7 pt-4 pb-2">
6 <n-steps :current size="small" :status="currentStatus">
7 <n-step title="Attack" />
8 <n-step title="Endpoint agent" />
9 <n-step title="Simulate" />
10 </n-steps>
11 </div>
12 </n-scrollbar>
13 </div>
14
15 <div class="flex grow flex-col overflow-hidden">
16 <Transition :name="`slide-form-${slideFormDirection}`">
17 <div v-if="current === 1" class="grow overflow-hidden">
18 <n-scrollbar ref="parametersScroll" style="max-height: 350px" trigger="none">
19 <ParametersList
20 v-model:selected="selectedAttack"
21 :technique-id
22 class="px-7"
23 :os-list
24 :parameters-list
25 @loaded="parametersList = $event"
26 />
27 </n-scrollbar>
28 </div>
29 <div v-else-if="current === 2" class="grow overflow-hidden">
30 <n-scrollbar ref="agentsScroll" style="max-height: 350px" trigger="none">
31 <AgentsList
32 v-model:selected="selectedAgent"
33 class="px-7"
34 :agents-list
35 :filter="agentsListFilter"
36 @loaded="agentsList = $event"
37 />
38 </n-scrollbar>
39 </div>
40 <div v-else class="flex flex-col gap-6 px-7">
41 <CardEntity size="small" embedded>
42 <template #header>selected</template>
43 <template #footer>
44 <div class="flex flex-col gap-2">
45 <CardEntity v-if="selectedAttack" embedded size="small">
46 <template #header>
47 {{ selectedAttack.name }}
48 </template>
49 <template #default>
50 {{ selectedAttack.description }}
51 </template>
52 </CardEntity>
53 <CardEntity v-if="selectedAgent" embedded size="small">
54 <template #headerMain>
55 {{ selectedAgent.hostname }}
56 </template>
57 <template #headerExtra>
58 <code
59 class="text-primary cursor-pointer"
60 @click.stop="routeAgent(selectedAgent.agent_id).navigate()"
61 >
62 {{ selectedAgent.agent_id }}
63 <Icon :name="LinkIcon" :size="13" class="relative top-0.5" />
64 </code>
65 </template>
66 <template #default>
67 {{ selectedAgent.ip_address }}
68 <code>{{ selectedAgent.label }}</code>
69 </template>
70 <template #footer>
71 <div class="flex flex-wrap items-center gap-2">
72 <Icon :name="iconFromOs(selectedAgent.os)" :size="14" />
73 <span>
74 {{ selectedAgent.os }}
75 </span>
76 </div>
77 </template>
78 </CardEntity>
79 </div>
80 </template>
81 </CardEntity>
82
83 <div v-if="collectResponse" class="flex flex-col gap-2">
84 <div v-if="collectTime" class="text-xs">
85 <span class="text-sm">last simulation:</span>
86 <code>{{ formatDate(collectTime, dFormats.datetimesec) }}</code>
87 </div>
88 <CardEntity v-for="report of collectResponse" :key="`${report.GUID}`" size="small">
89 <template #default>
90 <table>
91 <tbody class="text-xs">
92 <tr v-for="(value, key) in report" :key="`${key}`">
93 <td class="text-secondary p-1 pr-4 whitespace-nowrap">{{ key }}</td>
94 <td class="p-1">{{ value }}</td>
95 </tr>
96 </tbody>
97 </table>
98 </template>
99 </CardEntity>
100 </div>
101 <template v-else>
102 <div v-if="!loading" class="p-6 text-center">
103 Run “
104 <strong>Simulate attack</strong>
105 ” to view the report here.
106 </div>
107 <div v-else class="flex flex-col gap-2">
108 <n-skeleton height="20px" width="50%" :sharp="false" />
109 <n-skeleton height="100px" width="100%" :sharp="false" />
110 </div>
111 </template>
112 </div>
113 </Transition>
114 </div>
115
116 <div class="flex justify-between gap-4 px-7 pb-4">
117 <div class="flex items-center gap-4">
118 <n-button v-if="isPrevStepEnabled" :disabled="loading" @click="prev()">
119 <template #icon>
120 <Icon :name="ArrowLeftIcon" />
121 </template>
122 Prev
123 </n-button>
124 <n-button v-if="isNextStepEnabled" icon-placement="right" :disabled="loading" @click="next()">
125 <template #icon>
126 <Icon :name="ArrowRightIcon" />
127 </template>
128 Next
129 </n-button>
130 </div>
131 <n-button v-if="isSubmitEnabled" type="primary" :disabled="!isSubmitValid" :loading @click="submit()">
132 <template #icon>
133 <Icon :name="AttackIcon" />
134 </template>
135 Simulate attack
136 </n-button>
137 </div>
138 </div>
139 </template>
140
141 <script setup lang="ts">
142 import type { ScrollbarInst, StepsProps } from "naive-ui"
143 import type { CollectRequest } from "@/api/endpoints/artifacts"
144 import type { Agent } from "@/types/agents.d"
145 import type { MatchingParameter } from "@/types/artifacts.d"
146 import { NButton, NScrollbar, NSkeleton, NStep, NSteps, useMessage } from "naive-ui"
147 import { computed, onMounted, ref, watch } from "vue"
148 import Api from "@/api"
149 import CardEntity from "@/components/common/cards/CardEntity.vue"
150 import Icon from "@/components/common/Icon.vue"
151 import { useNavigation } from "@/composables/useNavigation"
152 import { useSettingsStore } from "@/stores/settings"
153 import { getOS, iconFromOs } from "@/utils"
154 import { formatDate } from "@/utils/format"
155 import AgentsList from "./AgentsList.vue"
156 import ParametersList from "./ParametersList.vue"
157
158 export interface Report {
159 "Execution Time (UTC)": Date
160 "Execution Time (Local)": Date
161 Technique: string
162 "Test Number": number
163 "Test Name": string
164 Hostname: string
165 Username: string
166 GUID: string
167 }
168
169 const { techniqueId, osList } = defineProps<{
170 techniqueId: string
171 osList: string[]
172 }>()
173
174 const emit = defineEmits<{
175 (e: "update:loading", value: boolean): void
176 (e: "close"): void
177 (e: "submitted"): void
178 (e: "mounted", value: { reset: () => void }): void
179 }>()
180
181 const ArrowRightIcon = "carbon:arrow-right"
182 const ArrowLeftIcon = "carbon:arrow-left"
183 const AttackIcon = "mdi:target"
184 const LinkIcon = "carbon:launch"
185
186 const { routeAgent } = useNavigation()
187 const dFormats = useSettingsStore().dateFormat
188 const message = useMessage()
189 const current = ref<number>(1)
190 const currentStatus = ref<StepsProps["status"]>("process")
191 const slideFormDirection = ref<"right" | "left">("right")
192
193 const parametersList = ref<MatchingParameter[] | null>(null)
194 const agentsList = ref<Agent[] | null>(null)
195 const selectedAttack = ref<MatchingParameter | null>(null)
196 const selectedAgent = ref<Agent | null>(null)
197 const loading = ref(false)
198 const collectResponse = ref<Report[] | null>(null)
199 const collectTime = ref<Date | null>(null)
200 const parametersScroll = ref<ScrollbarInst | null>(null)
201 const agentsScroll = ref<ScrollbarInst | null>(null)
202
203 const isNextStepEnabled = computed(
204 () => (current.value === 1 && selectedAttack.value) || (current.value === 2 && selectedAgent.value)
205 )
206 const isPrevStepEnabled = computed(() => current.value > 1)
207 const isSubmitEnabled = computed(() => current.value === 3)
208 const isSubmitValid = computed(() => {
209 if (!selectedAttack.value) {
210 return false
211 }
212
213 if (!selectedAgent.value) {
214 return false
215 }
216
217 return true
218 })
219
220 function submit() {
221 if (selectedAttack.value && selectedAgent.value) {
222 if (getOS(selectedAgent.value.os) === "MacOS") {
223 return
224 }
225
226 currentStatus.value = "finish"
227 loading.value = true
228
229 let artifact_name = ""
230
231 if (getOS(selectedAgent.value.os) === "Linux") {
232 artifact_name = "Linux.AttackSimulation.AtomicRedTeam"
233 }
234
235 if (getOS(selectedAgent.value.os) === "Windows") {
236 artifact_name = "Windows.AttackSimulation.AtomicRedTeam"
237 }
238
239 const payload: CollectRequest = {
240 hostname: selectedAgent.value.hostname,
241 artifact_name,
242 parameters: {
243 env: [
244 {
245 key: "InstallART",
246 value: "N"
247 },
248 {
249 key: selectedAttack.value.name,
250 value: "Y"
251 }
252 ]
253 }
254 }
255
256 Api.artifacts
257 .collect(payload)
258 .then(res => {
259 if (res.data.success) {
260 emit("submitted")
261 collectResponse.value = (res.data.results as unknown as Report[]) || []
262 collectTime.value = new Date()
263 message.success(res.data?.message || "Successfully executed query.")
264 } else {
265 message.warning(res.data?.message || "An error occurred. Please try again later.")
266 }
267 })
268 .catch(err => {
269 // MOCK
270 /*
271 const res = {
272 message: "Successfully executed query",
273 success: true,
274 results: [
275 {
276 "Execution Time (UTC)": "2025-05-31T20:43:02Z",
277 "Execution Time (Local)": "2025-05-31T13:43:02Z",
278 Technique:
279 "[T1003.001](https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1003.001/T1003.001.md)",
280 "Test Number": 10,
281 "Test Name": "Powershell Mimikatz",
282 Hostname: "WIN-HFOU106TD7K",
283 Username: "nt authority\\system",
284 GUID: "66fb0bc1-3c3f-47e9-a298-550ecfefacbc"
285 }
286 ]
287 }
288
289 emit("submitted")
290 collectResponse.value = (res.results as unknown as Report[]) || []
291 collectTime.value = new Date()
292 message.success(res?.message || "Successfully executed query.")
293
294 throw err
295 */
296 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
297 })
298 .finally(() => {
299 loading.value = false
300 })
301 }
302 }
303
304 function reset() {
305 currentStatus.value = "process"
306 slideFormDirection.value = "right"
307 current.value = 1
308
309 selectedAttack.value = null
310 selectedAgent.value = null
311 collectResponse.value = null
312 collectTime.value = null
313 }
314
315 function next() {
316 currentStatus.value = "process"
317 slideFormDirection.value = "right"
318 current.value++
319 }
320
321 function prev() {
322 currentStatus.value = "process"
323 slideFormDirection.value = "left"
324 current.value--
325 }
326
327 function agentsListFilter(agent: Agent) {
328 for (const os of osList) {
329 if (getOS(agent.os) === getOS(os)) {
330 return true
331 }
332 }
333 return false
334 }
335
336 function scrollInView(scrollContainer: ScrollbarInst) {
337 // @ts-expect-error $el property not mapped
338 const wrap = (scrollContainer.$el.nextSibling || scrollContainer.$el.nextElementSibling) as HTMLElement
339
340 const element = wrap.querySelector(".highlighted") as HTMLElement
341
342 if (element) {
343 const middle = element.offsetTop - wrap.offsetHeight / 2 + element.offsetHeight / 2
344
345 scrollContainer.scrollTo({ top: middle, behavior: "smooth" })
346 }
347 }
348
349 watch(selectedAttack, val => {
350 if (val) {
351 next()
352 }
353 })
354
355 watch(selectedAgent, val => {
356 if (val) {
357 next()
358 }
359 })
360
361 watch([current, parametersList, agentsList], () => {
362 if (current.value === 1 && parametersList.value?.length && selectedAttack.value) {
363 setTimeout(() => {
364 if (parametersScroll.value) {
365 scrollInView(parametersScroll.value)
366 }
367 }, 200)
368 }
369 if (current.value === 2 && agentsList.value?.length && selectedAgent.value) {
370 setTimeout(() => {
371 if (agentsScroll.value) {
372 scrollInView(agentsScroll.value)
373 }
374 }, 200)
375 }
376 })
377
378 onMounted(() => {
379 emit("mounted", {
380 reset
381 })
382 })
383 </script>
384
385 <style lang="scss" scoped>
386 .simulator-windows-attack-wizard {
387 .slide-form-right-enter-active,
388 .slide-form-right-leave-active,
389 .slide-form-left-enter-active,
390 .slide-form-left-leave-active {
391 transition: all 0.2s ease-out;
392 position: absolute;
393 width: 100%;
394 }
395
396 .slide-form-left-enter-from {
397 transform: translateX(-100%);
398 }
399
400 .slide-form-left-leave-to {
401 transform: translateX(100%);
402 }
403
404 .slide-form-right-enter-from {
405 transform: translateX(100%);
406 }
407
408 .slide-form-right-leave-to {
409 transform: translateX(-100%);
410 }
411 }
412 </style>