| 1 | <template> |
| 2 | <div class="artifacts-command"> |
| 3 | <div class="header flex items-start gap-2"> |
| 4 | <div class="flex w-full flex-col gap-2"> |
| 5 | <div class="flex grow flex-wrap items-center gap-2"> |
| 6 | <div v-if="!hideHostnameField" class="grow basis-56"> |
| 7 | <n-select |
| 8 | v-model:value="filters.hostname" |
| 9 | :options="agentHostnameOptions" |
| 10 | placeholder="Agent hostname" |
| 11 | clearable |
| 12 | filterable |
| 13 | size="small" |
| 14 | :disabled="loading" |
| 15 | :loading="loadingAgents" |
| 16 | /> |
| 17 | </div> |
| 18 | <div class="grow basis-56"> |
| 19 | <n-select |
| 20 | v-model:value="filters.artifact_name" |
| 21 | :options="artifactsOptions" |
| 22 | placeholder="Artifact name" |
| 23 | clearable |
| 24 | filterable |
| 25 | size="small" |
| 26 | :disabled="loading" |
| 27 | :loading="loadingArtifacts" |
| 28 | /> |
| 29 | </div> |
| 30 | <div v-if="!hideVelociraptorIdField" class="grow basis-56"> |
| 31 | <n-input |
| 32 | v-model:value="filters.velociraptor_id" |
| 33 | placeholder="Velociraptor id" |
| 34 | :readonly="loading" |
| 35 | clearable |
| 36 | size="small" |
| 37 | /> |
| 38 | </div> |
| 39 | </div> |
| 40 | <div class="flex grow flex-wrap items-center gap-2"> |
| 41 | <n-input |
| 42 | v-model:value="filters.command" |
| 43 | placeholder="Command" |
| 44 | clearable |
| 45 | :readonly="loading" |
| 46 | type="textarea" |
| 47 | :autosize="{ |
| 48 | minRows: 3, |
| 49 | maxRows: 10 |
| 50 | }" |
| 51 | /> |
| 52 | </div> |
| 53 | <div class="flex grow flex-wrap-reverse items-center justify-end gap-2"> |
| 54 | <div class="flex grow flex-wrap gap-2"> |
| 55 | <n-tooltip v-if="commandTime" trigger="hover"> |
| 56 | <template #trigger> |
| 57 | <Badge type="splitted" color="primary" hint-cursor> |
| 58 | <template #iconLeft> |
| 59 | <Icon :name="TimeIcon" /> |
| 60 | </template> |
| 61 | <template #value> |
| 62 | <span class="flex"> |
| 63 | {{ formatDate(commandTime, dFormats.timesec) }} |
| 64 | |
| 65 | <n-spin v-if="loading" :size="12" class="ml-2" /> |
| 66 | |
| 67 | {{ responseTime ? ` / ${formatDate(responseTime, dFormats.timesec)}` : "" }} |
| 68 | </span> |
| 69 | </template> |
| 70 | </Badge> |
| 71 | </template> |
| 72 | Last request time / last response time |
| 73 | </n-tooltip> |
| 74 | |
| 75 | <Badge v-if="diffTime" type="splitted" color="primary"> |
| 76 | <template #iconLeft> |
| 77 | <Icon :name="StopWatchIcon" :size="15" /> |
| 78 | </template> |
| 79 | <template #value> |
| 80 | {{ diffTime }} |
| 81 | </template> |
| 82 | </Badge> |
| 83 | </div> |
| 84 | <n-button |
| 85 | size="small" |
| 86 | type="primary" |
| 87 | secondary |
| 88 | :loading |
| 89 | :disabled="!areFiltersValid" |
| 90 | @click="getData()" |
| 91 | > |
| 92 | Submit |
| 93 | </n-button> |
| 94 | </div> |
| 95 | </div> |
| 96 | </div> |
| 97 | <n-spin :show="loading"> |
| 98 | <div class="my-7 flex min-h-52 flex-col gap-3"> |
| 99 | <template v-if="commandList.length"> |
| 100 | <CommandItem |
| 101 | v-for="command of commandList" |
| 102 | :key="command.Stdout" |
| 103 | :command |
| 104 | class="item-appear item-appear-bottom item-appear-005" |
| 105 | /> |
| 106 | </template> |
| 107 | <template v-else> |
| 108 | <n-empty v-if="!loading" description="No items found" class="h-48 justify-center" /> |
| 109 | </template> |
| 110 | </div> |
| 111 | </n-spin> |
| 112 | </div> |
| 113 | </template> |
| 114 | |
| 115 | <script setup lang="ts"> |
| 116 | import type { CommandRequest } from "@/api/endpoints/artifacts" |
| 117 | import type { Agent } from "@/types/agents.d" |
| 118 | import type { Artifact, CommandResult } from "@/types/artifacts.d" |
| 119 | import { NButton, NEmpty, NInput, NSelect, NSpin, NTooltip, useMessage } from "naive-ui" |
| 120 | import { computed, nextTick, onBeforeMount, ref, toRefs } from "vue" |
| 121 | import Api from "@/api" |
| 122 | import Badge from "@/components/common/Badge.vue" |
| 123 | import Icon from "@/components/common/Icon.vue" |
| 124 | import { useSettingsStore } from "@/stores/settings" |
| 125 | import dayjs from "@/utils/dayjs" |
| 126 | import { formatDate } from "@/utils/format" |
| 127 | import CommandItem from "./CommandItem.vue" |
| 128 | |
| 129 | const props = defineProps<{ |
| 130 | hostname?: string |
| 131 | agents?: Agent[] |
| 132 | artifacts?: Artifact[] |
| 133 | hideHostnameField?: boolean |
| 134 | hideVelociraptorIdField?: boolean |
| 135 | }>() |
| 136 | |
| 137 | const emit = defineEmits<{ |
| 138 | (e: "loaded-agents", value: Agent[]): void |
| 139 | (e: "loaded-artifacts", value: Artifact[]): void |
| 140 | }>() |
| 141 | |
| 142 | const { hostname, agents, artifacts, hideHostnameField, hideVelociraptorIdField } = toRefs(props) |
| 143 | |
| 144 | const TimeIcon = "carbon:time" |
| 145 | const StopWatchIcon = "quill:stopwatch" |
| 146 | |
| 147 | const message = useMessage() |
| 148 | const loadingAgents = ref(false) |
| 149 | const loadingArtifacts = ref(false) |
| 150 | const loading = ref(false) |
| 151 | const agentsList = ref<Agent[]>([]) |
| 152 | const artifactsList = ref<Artifact[]>([]) |
| 153 | const commandList = ref<CommandResult[]>([]) |
| 154 | const commandTime = ref<Date | null>(null) |
| 155 | const responseTime = ref<Date | null>(null) |
| 156 | const dFormats = useSettingsStore().dateFormat |
| 157 | |
| 158 | const diffTime = computed(() => { |
| 159 | if (commandTime.value && responseTime.value) { |
| 160 | return `${dayjs.duration(dayjs(responseTime.value).diff(commandTime.value, "ms", true)).asSeconds()}s` |
| 161 | } else { |
| 162 | return null |
| 163 | } |
| 164 | }) |
| 165 | |
| 166 | const filters = ref<Partial<CommandRequest>>({}) |
| 167 | |
| 168 | const areFiltersValid = computed(() => { |
| 169 | return !!filters.value.artifact_name && !!filters.value.hostname && !!filters.value.command |
| 170 | }) |
| 171 | |
| 172 | const agentHostnameOptions = computed(() => { |
| 173 | if (hostname?.value) { |
| 174 | return [{ value: hostname.value, label: hostname.value }] |
| 175 | } |
| 176 | return (agentsList.value || []).map(o => ({ value: o.hostname, label: o.hostname })) |
| 177 | }) |
| 178 | |
| 179 | const artifactsOptions = computed(() => { |
| 180 | return (artifactsList.value || []).map(o => ({ value: o.name, label: o.name })) |
| 181 | }) |
| 182 | |
| 183 | function getData() { |
| 184 | if (areFiltersValid.value) { |
| 185 | loading.value = true |
| 186 | commandList.value = [] |
| 187 | commandTime.value = new Date() |
| 188 | responseTime.value = null |
| 189 | |
| 190 | Api.artifacts |
| 191 | .command(filters.value as CommandRequest) |
| 192 | .then(res => { |
| 193 | if (res.data.success) { |
| 194 | commandList.value = res.data?.results || [] |
| 195 | } else { |
| 196 | message.warning(res.data?.message || "An error occurred. Please try again later.") |
| 197 | } |
| 198 | }) |
| 199 | .catch(err => { |
| 200 | message.error(err.response?.data?.message || "An error occurred. Please try again later.") |
| 201 | }) |
| 202 | .finally(() => { |
| 203 | responseTime.value = new Date() |
| 204 | loading.value = false |
| 205 | }) |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | function getAgents(cb?: (agents: Agent[]) => void) { |
| 210 | loadingAgents.value = true |
| 211 | |
| 212 | Api.agents |
| 213 | .getAgents() |
| 214 | .then(res => { |
| 215 | if (res.data.success) { |
| 216 | agentsList.value = res.data.agents || [] |
| 217 | |
| 218 | if (cb && typeof cb === "function") { |
| 219 | cb(agentsList.value) |
| 220 | } |
| 221 | } else { |
| 222 | message.error(res.data?.message || "An error occurred. Please try again later.") |
| 223 | } |
| 224 | }) |
| 225 | .catch(err => { |
| 226 | message.error(err.response?.data?.message || "An error occurred. Please try again later.") |
| 227 | }) |
| 228 | .finally(() => { |
| 229 | loadingAgents.value = false |
| 230 | }) |
| 231 | } |
| 232 | |
| 233 | function getArtifacts(cb?: (artifacts: Artifact[]) => void) { |
| 234 | loadingArtifacts.value = true |
| 235 | |
| 236 | Api.artifacts |
| 237 | .getAll() |
| 238 | .then(res => { |
| 239 | if (res.data.success) { |
| 240 | artifactsList.value = res.data.artifacts || [] |
| 241 | |
| 242 | if (cb && typeof cb === "function") { |
| 243 | cb(artifactsList.value) |
| 244 | } |
| 245 | } else { |
| 246 | message.error(res.data?.message || "An error occurred. Please try again later.") |
| 247 | } |
| 248 | }) |
| 249 | .catch(err => { |
| 250 | message.error(err.response?.data?.message || "An error occurred. Please try again later.") |
| 251 | }) |
| 252 | .finally(() => { |
| 253 | loadingArtifacts.value = false |
| 254 | }) |
| 255 | } |
| 256 | |
| 257 | onBeforeMount(() => { |
| 258 | artifactsList.value = ["Windows.System.PowerShell", "Windows.System.CmdShell", "Linux.Sys.BashShell"].map( |
| 259 | o => ({ name: o }) as Artifact |
| 260 | ) |
| 261 | |
| 262 | if (hostname?.value) { |
| 263 | filters.value.hostname = hostname.value |
| 264 | } |
| 265 | |
| 266 | if (agents?.value?.length && !agentsList.value.length) { |
| 267 | agentsList.value = agents.value |
| 268 | } |
| 269 | |
| 270 | if (artifacts?.value?.length && !artifactsList.value.length) { |
| 271 | artifactsList.value = artifacts.value |
| 272 | } |
| 273 | |
| 274 | nextTick(() => { |
| 275 | if (!agentsList.value.length && !hostname?.value) { |
| 276 | getAgents((agents: Agent[]) => { |
| 277 | emit("loaded-agents", agents) |
| 278 | }) |
| 279 | } |
| 280 | if (!artifactsList.value.length) { |
| 281 | getArtifacts((artifacts: Artifact[]) => { |
| 282 | emit("loaded-artifacts", artifacts) |
| 283 | }) |
| 284 | } |
| 285 | }) |
| 286 | |
| 287 | // MOCK |
| 288 | /* |
| 289 | commandList.value = commandResult |
| 290 | */ |
| 291 | }) |
| 292 | </script> |