diff --git a/src/components/agents/AgentChartStatus.vue b/src/components/agents/AgentChartStatus.vue
index 4f2678b..e4b643f 100644
--- a/src/components/agents/AgentChartStatus.vue
+++ b/src/components/agents/AgentChartStatus.vue
@@ -10,22 +10,39 @@ const $api = inject("$api");
const agentStore = useAgentStore();
const { agents } = storeToRefs(agentStore);
+
const agentChartStatus = ref(null);
const chart = ref(null);
+const poll = ref(null);
-onMounted(() => {
- initChart();
+onMounted(async () => {
window.addEventListener("resize", resizeChart);
+
+ const [cfg, ag] = await Promise.allSettled([
+ agentStore.getAgentConfig($api),
+ agentStore.getAgents($api),
+ ]);
+ if (cfg.status === "rejected") console.warn("[cmp] getAgentConfig failed:", cfg.reason);
+ if (ag.status === "rejected") console.error("[cmp] getAgents failed:", ag.reason);
+
+ await initChart();
+
+ poll.value = setInterval(() => {
+ agentStore.getAgents($api).catch(err => console.warn("[poll] getAgents failed:", err));
+ }, 10_000);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", resizeChart);
+ if (poll.value) clearInterval(poll.value);
+ if (chart.value) {
+ chart.value.dispose(); // avoid ECharts memory leaks
+ chart.value = null;
+ }
});
-
watch(agents, () => {
- setChartOption();
+ if (chart.value) setChartOption();
});
-
async function initChart() {
chart.value = echarts.init(agentChartStatus.value);
chart.value.showLoading("default", {
@@ -34,27 +51,25 @@ async function initChart() {
});
resizeChart();
- await agentStore.getAgents($api);
- setChartOption();
- chart.value.hideLoading();
+ try {
+ await agentStore.getAgents($api); // initial fetch
+ } finally {
+ setChartOption();
+ chart.value.hideLoading();
+ }
}
function setChartOption() {
- chart.value.setOption({
+ const option = {
title: {
- text: `${agents.value.length} Agent${agents.value.length == 1 ? "" : "s"}`,
- textStyle: {
- color: "white",
- },
- },
- tooltip: {
- trigger: "item",
- },
- legend: {
- show: false,
+ text: `${agents.value.length} Agent${agents.value.length === 1 ? "" : "s"}`,
+ textStyle: { color: "white" },
},
+ tooltip: { trigger: "item" },
+ legend: { show: false },
series: [
{
+ id: "agent-status",
name: "Agent Status",
type: "pie",
radius: ["40%", "70%"],
@@ -64,47 +79,39 @@ function setChartOption() {
borderColor: "hsl(0deg, 0%, 14%)",
borderWidth: 2,
},
- label: {
- show: false,
- position: "center",
- },
- labelLine: {
- show: false,
- },
+ label: { show: false, position: "center" },
+ labelLine: { show: false },
data: getChartData(),
},
],
- });
+ };
+ chart.value.setOption(option, { notMerge: true, replaceMerge: ["series"], lazyUpdate: true });
}
function getChartData() {
if (!agents.value.length) return [];
+ const nowMs = agentStore.serverNowMs ?? Date.now();
+ const cfg = agentStore.agentConfig;
+
return [
{
name: "Alive (trusted)",
- value: agents.value.filter(
- (agent) => getAgentStatus(agent) === "alive" && agent.trusted
- ).length,
+ value: agents.value.filter(a => getAgentStatus(a, nowMs, cfg) === "alive" && a.trusted).length,
itemStyle: { color: "#4a9" },
},
{
name: "Alive (untrusted)",
- value: agents.value.filter(
- (agent) => getAgentStatus(agent) === "alive" && !agent.trusted
- ).length,
+ value: agents.value.filter(a => getAgentStatus(a, nowMs, cfg) === "alive" && !a.trusted).length,
itemStyle: { color: "#F7DB89" },
},
{
name: "Pending kill",
- value: agents.value.filter(
- (agent) => getAgentStatus(agent) === "pending kill"
- ).length,
+ value: agents.value.filter(a => getAgentStatus(a, nowMs, cfg) === "pending kill").length,
itemStyle: { color: "hsl(207deg, 61%, 53%)" },
},
{
name: "Dead",
- value: agents.value.filter((agent) => getAgentStatus(agent) === "dead")
- .length,
+ value: agents.value.filter(a => getAgentStatus(a, nowMs, cfg) === "dead").length,
itemStyle: { color: "#c31" },
},
];
@@ -121,8 +128,5 @@ function resizeChart() {
diff --git a/src/components/agents/DetailsModal.vue b/src/components/agents/DetailsModal.vue
index 1797d66..3bfc091 100644
--- a/src/components/agents/DetailsModal.vue
+++ b/src/components/agents/DetailsModal.vue
@@ -1,178 +1,202 @@
.modal(:class="{ 'is-active': modals.agents.showDetails }")
- .modal-background(@click="modals.agents.showDetails = false")
- .modal-card
- header.modal-card-head
- p.modal-card-title Agent Details
- .modal-card-body
- p.has-text-weight-bold.has-text-centered.mb-3 Settings
- table
- col(width="30%")
- col(width="70%")
- tbody
- tr
- th.has-text-right Contact
- td
- .select.control
- select(v-model="selectedAgent.pending_contact")
- option(v-for="contact in selectedAgent.available_contacts" :key="contact" :value="contact") {{ contact }}
- tr
- th.has-text-right Group
- td
- input.input(type="text" v-model="selectedAgent.group" :class="{ 'is-danger': validation.group }")
- p.help.has-text-danger(v-if="validation.group") {{ validation.group }}
- tr
- th.has-text-right Sleep Timer
- td
- .is-flex.is-align-items-center
- label.mr-3 min
- input.input.mr-4(v-model="selectedAgent.sleep_min" type="number" placeholder="30" min="0" :max="selectedAgent.sleep_max" :class="{ 'is-danger': validation.beaconTimer }")
- label.mr-3 max
- input.input(v-model="selectedAgent.sleep_max" type="number" placeholder="60" :min="selectedAgent.sleep_min" :class="{ 'is-danger': validation.beaconTimer }")
- p.help.has-text-danger(v-if="validation.beaconTimer") {{ validation.beaconTimer }}
- tr
- th.has-text-right Watchdog Timer
- td
- input.input(type="number" v-model="selectedAgent.watchdog" min="0" :class="{ 'is-danger': validation.watchdogTimer }")
- p.help.has-text-danger(v-if="validation.watchdogTimer") {{ validation.watchdogTimer }}
- button.button.is-primary.is-fullwidth.mt-4(@click="saveAgent()") Save Settings
- hr
- p.has-text-weight-bold.has-text-centered.mb-3 Agent Details
- table
- col(width="30%")
- col(width="70%")
- tbody
- tr
- th.has-text-right Status
- td
- span(:class="{ 'has-text-warning': getAgentStatus(selectedAgent) === 'dead', 'has-text-success': getAgentStatus(selectedAgent) === 'alive', 'has-text-info': getAgentStatus(selectedAgent) === 'pending kill' }") {{ getAgentStatus(selectedAgent) }}
- span ,
- span(:class="{ 'has-text-warning': !selectedAgent.trusted, 'has-text-success': selectedAgent.trusted }") {{ selectedAgent.trusted ? 'trusted' : 'untrusted' }}
- tr
- th.has-text-right Paw
- td {{ selectedAgent.paw }}
- tr
- th.has-text-right Host
- td {{ `${selectedAgent.host} (${selectedAgent.host_ip_addrs ? selectedAgent.host_ip_addrs.join(', ') : ''})` }}
- tr(v-if="selectedAgent.display_name")
- th.has-text-right Display Name
- td {{ selectedAgent.display_name }}
- tr
- th.has-text-right Username
- td {{ selectedAgent.username }}
- tr
- th.has-text-right Privilege
- td {{ selectedAgent.privilege }}
- tr
- th.has-text-right Last Seen
- td {{ new Date(selectedAgent.last_seen).toLocaleString() }}
- tr
- th.has-text-right Created
- td {{ new Date(selectedAgent.created).toLocaleString() }}
- tr
- th.has-text-right Architecture
- td {{ selectedAgent.architecture }}
- tr
- th.has-text-right Platform
- td {{ selectedAgent.pid }}
- tr
- th.has-text-right PID
- td {{ selectedAgent.pid }}
- tr
- th.has-text-right PPID
- td {{ selectedAgent.ppid }}
- tr
- th.has-text-right Executable Name
- td {{ selectedAgent.exe_name }}
- tr
- th.has-text-right Location
- td {{ selectedAgent.location }}
- tr
- th.has-text-right Executors
- td {{ selectedAgent.executors ? selectedAgent.executors.join(", ") : '' }}
- tr(v-if="selectedAgent.host_ip_addrs")
- th.has-text-right Host IP Addresses
- td {{ selectedAgent.host_ip_addrs ? selectedAgent.host_ip_addrs.join(", ") : '' }}
- tr
- th.has-text-right Peer-to-Peer Proxy Receivers
- td {{ (selectedAgent.proxy_receivers && Object.keys(selectedAgent.proxy_receivers).length) ? Object.keys(selectedAgent.proxy_receivers) : 'No local P2P proxy receivers active.' }}
- tr
- th.has-text-right Peer-toPeer Proxy Chains
- td {{ (selectedAgent.proxy_chain && selectedAgent.proxy_chain.length) ? selectedAgent.proxy_chain.join(', ') : 'Not using P2P agents to reach C2.' }}
- footer.modal-card-foot.is-flex.is-justify-content-flex-end
- button.button(@click="modals.agents.showDetails = false") Close
- button.button.is-danger.is-outlined(@click="agentStore.killAgent($api, selectedAgent.paw); modals.agents.showDetails = false;")
- span.icon
- font-awesome-icon(icon="fas fa-skull-crossbones")
- span Kill Agent
+ .modal-background(@click="modals.agents.showDetails = false")
+ .modal-card
+ header.modal-card-head
+ p.modal-card-title Agent Details
+ .modal-card-body(v-if="selectedAgent")
+ p.has-text-weight-bold.has-text-centered.mb-3 Settings
+ table
+ col(width="30%")
+ col(width="70%")
+ tbody
+ tr
+ th.has-text-right Contact
+ td
+ .select.control
+ select(v-model="selectedAgent.pending_contact")
+ option(v-for="contact in selectedAgent.available_contacts" :key="contact" :value="contact") {{ contact }}
+ tr
+ th.has-text-right Group
+ td
+ input.input(type="text" v-model="selectedAgent.group" :class="{ 'is-danger': validation.group }")
+ p.help.has-text-danger(v-if="validation.group") {{ validation.group }}
+ tr
+ th.has-text-right Sleep Timer
+ td
+ .is-flex.is-align-items-center
+ label.mr-3 min
+ input.input.mr-4(
+ v-model="selectedAgent.sleep_min"
+ type="number" placeholder="30" min="0"
+ :max="selectedAgent.sleep_max"
+ :class="{ 'is-danger': validation.beaconTimer }"
+ )
+ label.mr-3 max
+ input.input(
+ v-model="selectedAgent.sleep_max"
+ type="number" placeholder="60"
+ :min="selectedAgent.sleep_min"
+ :class="{ 'is-danger': validation.beaconTimer }"
+ )
+ p.help.has-text-danger(v-if="validation.beaconTimer") {{ validation.beaconTimer }}
+ tr
+ th.has-text-right Watchdog Timer
+ td
+ input.input(type="number" v-model="selectedAgent.watchdog" min="0" :class="{ 'is-danger': validation.watchdogTimer }")
+ p.help.has-text-danger(v-if="validation.watchdogTimer") {{ validation.watchdogTimer }}
+
+ button.button.is-primary.is-fullwidth.mt-4(@click="saveAgent()") Save Settings
+ hr
+
+ p.has-text-weight-bold.has-text-centered.mb-3 Agent Details
+ table
+ col(width="30%")
+ col(width="70%")
+ tbody
+ tr
+ th.has-text-right Status
+ td
+ span(:class="statusClass(status)") {{ status }}
+ span ,
+ span(:class="{ 'has-text-success': isTrusted, 'has-text-warning': !isTrusted }") {{ isTrusted ? 'trusted' : 'untrusted' }}
+ tr
+ th.has-text-right Paw
+ td {{ selectedAgent.paw }}
+ tr
+ th.has-text-right Host
+ td {{ `${selectedAgent.host} (${selectedAgent.host_ip_addrs ? selectedAgent.host_ip_addrs.join(', ') : ''})` }}
+ tr(v-if="selectedAgent.display_name")
+ th.has-text-right Display Name
+ td {{ selectedAgent.display_name }}
+ tr
+ th.has-text-right Username
+ td {{ selectedAgent.username }}
+ tr
+ th.has-text-right Privilege
+ td {{ selectedAgent.privilege }}
+ tr
+ th.has-text-right Last Seen
+ td {{ new Date(selectedAgent.last_seen).toLocaleString() }}
+ tr
+ th.has-text-right Created
+ td {{ new Date(selectedAgent.created).toLocaleString() }}
+ tr
+ th.has-text-right Architecture
+ td {{ selectedAgent.architecture }}
+ tr
+ th.has-text-right Platform
+ td {{ selectedAgent.platform }}
+ tr
+ th.has-text-right PID
+ td {{ selectedAgent.pid }}
+ tr
+ th.has-text-right PPID
+ td {{ selectedAgent.ppid }}
+ tr
+ th.has-text-right Executable Name
+ td {{ selectedAgent.exe_name }}
+ tr
+ th.has-text-right Location
+ td {{ selectedAgent.location }}
+ tr
+ th.has-text-right Executors
+ td {{ selectedAgent.executors ? selectedAgent.executors.join(", ") : '' }}
+ tr(v-if="selectedAgent.host_ip_addrs")
+ th.has-text-right Host IP Addresses
+ td {{ selectedAgent.host_ip_addrs ? selectedAgent.host_ip_addrs.join(", ") : '' }}
+ tr
+ th.has-text-right Peer-to-Peer Proxy Receivers
+ td {{ (selectedAgent.proxy_receivers && Object.keys(selectedAgent.proxy_receivers).length) ? Object.keys(selectedAgent.proxy_receivers) : 'No local P2P proxy receivers active.' }}
+ tr
+ th.has-text-right Peer-toPeer Proxy Chains
+ td {{ (selectedAgent.proxy_chain && selectedAgent.proxy_chain.length) ? selectedAgent.proxy_chain.join(', ') : 'Not using P2P agents to reach C2.' }}
+
+ footer.modal-card-foot.is-flex.is-justify-content-flex-end
+ button.button(@click="modals.agents.showDetails = false") Close
+ button.button.is-danger.is-outlined(@click="agentStore.killAgent($api, selectedAgent.paw); modals.agents.showDetails = false;")
+ span.icon
+ font-awesome-icon(icon="fas fa-skull-crossbones")
+ span Kill Agent
+