add agents (#91)
* add agents * precommit fixes
taylor_socfortress committed
Aug 31, 2023 at 11:50 UTC
05e936f1ee59f4bdc7dd13d7245243c6e3e57e44
8 files changed
+597
-1
public/static/images/gallery/computer.png
Binary files /dev/null and b/public/static/images/gallery/computer.png differ
src/api/agent.ts
new
+7
@@ -0,0 +1,7 @@
1
+import connectors from "./connectors"
2
+import agents from "./agents"
3
+
4
+export default {
5
+ connectors,
6
+ agents
7
+}
src/api/agents.ts
new
+9
@@ -0,0 +1,9 @@
1
+import { FlaskBaseResponse } from "@/types/flask"
2
+import { HttpClient } from "./httpClient"
3
+import { Agents } from "@/types/agents"
4
+
5
+export default {
6
+ getAgents() {
7
+ return HttpClient.get<FlaskBaseResponse & { agents: Agents[] }>("/agents")
8
+ }
9
+}
src/api/index.ts
+3
-1
@@ -1,7 +1,9 @@
1
import connectors from "./connectors"
2
import indices from "./indices"
3
+import agents from "./agents"
4
5
export default {
6
connectors,
6
- indices
7
+ indices,
8
+ agents
9
}
src/core/nav.vue
+3
@@ -15,6 +15,9 @@
15
<el-menu-item index="/indices">
16
<span slot="title">Indicies</span>
17
</el-menu-item>
18
+ <el-menu-item index="/agents">
19
+ <span slot="title">Agents</span>
20
+ </el-menu-item>
21
<el-menu-item index="/indices-bkp">
22
<span slot="title">Indicies-bkp</span>
23
</el-menu-item>
src/router/index.ts
+12
@@ -13,6 +13,7 @@ import Ecommerce from "./ecommerce"
13
import Connectors from "../views/apps/Connectors.vue"
14
import Indices from "../views/apps/Dashboards/Indices.vue"
15
import IndicesBKP from "../views/apps/Dashboards/_bkp_Indices.vue"
16
+import Agents from "../views/apps/Dashboards/Agents.vue"
17
/*
18
19
//pages
@@ -105,6 +106,17 @@ const router = createRouter({
106
tags: ["app"]
107
}
108
},
109
+ {
110
+ path: "/agents",
111
+ name: "agents",
112
+ component: Agents,
113
+ meta: {
114
+ auth: true,
115
+ layout: layouts.navLeft,
116
+ searchable: true,
117
+ tags: ["app"]
118
+ }
119
+ },
120
{
121
path: "/indices-bkp",
122
name: "indices-bkp",
src/types/agents.d.ts
new
+14
@@ -0,0 +1,14 @@
1
+export interface Agents {
2
+ agent_id: string
3
+ client_id: string
4
+ client_last_seen: string
5
+ critical_asset: boolean
6
+ hostname: string
7
+ id?: number
8
+ ip_address: string
9
+ label: string
10
+ last_seen: string
11
+ os: string
12
+ velociraptor_client_version: string
13
+ wazuh_agent_version: string
14
+}
src/views/apps/Dashboards/Agents.vue
new
+549
@@ -0,0 +1,549 @@
1
+<template>
2
+ <div class="page-contacts flex column" id="page-contacts">
3
+ <resize-observer @notify="__resizeHanlder" />
4
+
5
+ <div class="contacts-root box grow flex gaps justify-center" :class="contactsClass">
6
+ <div class="card-base card-shadow--small search-card scrollable only-y">
7
+ <h1 class="mt-0">Agents</h1>
8
+
9
+ <el-input prefix-icon="el-icon-search" placeholder="Search a contact" clearable v-model="search"> </el-input>
10
+
11
+ <div class="o-050 text-right mt-10 mb-30">
12
+ <strong>{{ agentsFiltered.length }}</strong> Agents
13
+ </div>
14
+
15
+ <el-button @click="sync_agents({})">
16
+ <i class="mdi mdi-account-plus mr-10"></i>
17
+ Sync Agents</el-button
18
+ >
19
+
20
+ <div class="p-20">
21
+ <p>Critical Assets</p>
22
+ <ul class="contacts-favourites">
23
+ <li v-for="agent in agentsFavorite" :key="agent.agent_id" @click="openDialog(c)">
24
+ <img :src="'/static/images/gallery/computer.png'" alt="user favourite avatar" />
25
+ <span>{{ agent.hostname }}</span>
26
+ </li>
27
+ </ul>
28
+ </div>
29
+ </div>
30
+ <div class="contacts-list box grow scrollable only-y">
31
+ <div v-for="agent in agentsFiltered" :key="agent.agent_id" class="flex contact" @click="openEditAgentsModal(agent)">
32
+ <div class="star align-vertical p-10 fs-22">
33
+ <i class="mdi mdi-star align-vertical-middle" v-if="agent.critical_asset"></i>
34
+ <i class="mdi mdi-star-outline align-vertical-middle" v-if="!agent.critical_asset"></i>
35
+ </div>
36
+ <div class="avatar align-vertical">
37
+ <img :src="'/static/images/gallery/computer.png'" class="align-vertical-middle" alt="user avatar" />
38
+ </div>
39
+ <div class="info box grow flex">
40
+ <div class="name box grow flex column justify-center p-10">
41
+ <div class="fullname fs-18">
42
+ <strong>{{ agent.hostname }}</strong>
43
+ </div>
44
+ <div class="ip fs-14 secondary-text">{{ agent.ip_address }}</div>
45
+ <div class="os fs-14 secondary-text">{{ agent.os }}</div>
46
+ </div>
47
+ <div class="phone align-vertical p-10">
48
+ <span class="align-vertical-middle">{{ agent.last_seen }}</span>
49
+ </div>
50
+ <!--Add a el-button to make the agent critical-->
51
+ <div class="phone align-vertical p-10">
52
+ <el-button type="primary" @click="makeAgentCritical(agent.agent_id)" v-if="!agent.critical_asset">
53
+ <i class="mdi mdi-star-outline align-vertical-middle"></i>
54
+ </el-button>
55
+ <el-button type="primary" @click="makeAgentUncritical(agent.agent_id)" v-if="agent.critical_asset">
56
+ <i class="mdi mdi-star align-vertical-middle"></i>
57
+ </el-button>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </div>
63
+
64
+ <user-dialog v-model="dialogvisible" :userdata="userdata"></user-dialog>
65
+ </div>
66
+</template>
67
+
68
+<script>
69
+import UserDialog from "@/components/UserDialog.vue"
70
+import Contacts from "@/assets/data/CONTACTS_MOCK_DATA.json"
71
+import { defineComponent } from "@vue/runtime-core"
72
+import _ from "lodash"
73
+import ResizeObserver from "@/components/vue-resize/ResizeObserver.vue"
74
+import axios from "axios"
75
+
76
+export default defineComponent({
77
+ name: "Contacts",
78
+ data() {
79
+ return {
80
+ loading: false,
81
+ agents: [],
82
+ currentAgent: null,
83
+
84
+ // Configure Modal
85
+ isEditAgentsModalActive: false,
86
+
87
+ search: "",
88
+ dialogvisible: false,
89
+ pageWidth: 0,
90
+ userdata: {},
91
+ contacts: Contacts.slice(0, 30)
92
+ }
93
+ },
94
+ computed: {
95
+ contactsFiltered() {
96
+ return this.contacts.filter(
97
+ ({ full_name, email, phone }) =>
98
+ (full_name + email + phone).toString().toLowerCase().indexOf(this.search.toString().toLowerCase()) !== -1
99
+ )
100
+ },
101
+ contactsClass() {
102
+ return this.pageWidth >= 870 ? "large" : this.pageWidth >= 760 ? "medium" : "small"
103
+ },
104
+ contactsFavourite() {
105
+ return this.contacts.filter(({ starred }) => starred)
106
+ },
107
+ agentsFiltered() {
108
+ return this.agents.filter(
109
+ ({ agent_name, agent_ip, agent_id }) =>
110
+ (agent_name + agent_ip + agent_id).toString().toLowerCase().indexOf(this.search.toString().toLowerCase()) !== -1
111
+ )
112
+ },
113
+ agentsFavorite() {
114
+ return this.agents.filter(({ critical_asset }) => critical_asset)
115
+ }
116
+ },
117
+ methods: {
118
+ openEditAgentsModal(agent) {
119
+ this.currentAgent = agent
120
+ this.isEditAgentsModalActive = true
121
+ },
122
+ closeEditAgentsModal() {
123
+ this.isEditAgentsModalActive = false
124
+ },
125
+
126
+ openDialog(data) {
127
+ this.userdata = data
128
+ this.dialogvisible = true
129
+ },
130
+ setPageWidth() {
131
+ this.pageWidth = document.getElementById("page-contacts").offsetWidth
132
+ },
133
+ __resizeHanlder: _.throttle(function (e) {
134
+ this.setPageWidth()
135
+ }, 700),
136
+ get_agents() {
137
+ const path = "http://localhost:5000/agents"
138
+ this.loading = true
139
+ resizing: true,
140
+ axios
141
+ .get(path)
142
+ .then(response => {
143
+ if (response.data.success && Array.isArray(response.data.agents)) {
144
+ this.agents = response.data.agents
145
+ } else {
146
+ console.error("Received non-array agents: ", response.data)
147
+ this.agents = [] // Reset to empty array
148
+ }
149
+ })
150
+ .catch(error => {
151
+ console.log(error)
152
+ this.loading = false
153
+ })
154
+ },
155
+ sync_agents() {
156
+ const path = "http://localhost:5000/sync"
157
+ this.loading = true
158
+ resizing: true,
159
+ axios
160
+ .get(path)
161
+ .then(response => {
162
+ this.sync = response.data
163
+ this.loading = false
164
+ this.succssMesaage = "Agents Synced Successfully"
165
+ this.$message({
166
+ message: this.succssMesaage,
167
+ type: "success"
168
+ })
169
+ this.get_agents()
170
+ })
171
+ .catch(error => {
172
+ if (error.response.status === 401) {
173
+ this.errorMessage = "Unauthorized"
174
+ this.$message({
175
+ message: this.errorMessage,
176
+ type: "error"
177
+ })
178
+ } else {
179
+ this.$message({
180
+ message: "Failed to Sync Agents",
181
+ type: "error"
182
+ })
183
+ }
184
+ })
185
+ },
186
+ makeAgentCritical(agent_id) {
187
+ const path = "http://localhost:5000/agents/" + agent_id + "/critical"
188
+ axios
189
+ .put(path)
190
+ .then(response => {
191
+ this.loading = false
192
+ this.succssMesaage = "Agent Criticality Updated Successfully"
193
+ this.$message({
194
+ message: this.succssMesaage,
195
+ type: "success"
196
+ })
197
+ this.get_agents()
198
+ })
199
+ .catch(error => {
200
+ if (error.response.status === 401) {
201
+ this.errorMessage = "Unauthorized"
202
+ this.$message({
203
+ message: this.errorMessage,
204
+ type: "error"
205
+ })
206
+ } else {
207
+ this.$message({
208
+ message: "Failed to Update Agent Criticality",
209
+ type: "error"
210
+ })
211
+ }
212
+ })
213
+ },
214
+ makeAgentUncritical(agent_id) {
215
+ const path = "http://localhost:5000/agents/" + agent_id + "/uncritical"
216
+ axios
217
+ .put(path)
218
+ .then(response => {
219
+ this.loading = false
220
+ this.succssMesaage = "Agent Criticality Updated Successfully"
221
+ this.$message({
222
+ message: this.succssMesaage,
223
+ type: "success"
224
+ })
225
+ this.get_agents()
226
+ })
227
+ .catch(error => {
228
+ if (error.response.status === 401) {
229
+ this.errorMessage = "Unauthorized"
230
+ this.$message({
231
+ message: this.errorMessage,
232
+ type: "error"
233
+ })
234
+ } else {
235
+ this.$message({
236
+ message: "Failed to Update Agent Criticality",
237
+ type: "error"
238
+ })
239
+ }
240
+ })
241
+ }
242
+ },
243
+ mounted() {
244
+ this.setPageWidth()
245
+ this.get_agents()
246
+ },
247
+ watch: {
248
+ agentsFiltered(newValue) {
249
+ console.log("agentsFiltered:", newValue)
250
+ }
251
+ },
252
+ components: {
253
+ ResizeObserver,
254
+ UserDialog
255
+ }
256
+})
257
+</script>
258
+
259
+<style lang="scss" scoped>
260
+@import "../../../assets/scss/_variables";
261
+
262
+.page-contacts {
263
+ height: 100%;
264
+ margin: 0 !important;
265
+ padding: 20px;
266
+ padding-bottom: 10px;
267
+ box-sizing: border-box;
268
+
269
+ .search-card {
270
+ padding: 50px;
271
+ max-width: 350px;
272
+ //max-height: 320px;
273
+ box-sizing: border-box;
274
+ margin-bottom: 15px;
275
+
276
+ .el-input,
277
+ .el-button {
278
+ width: 100%;
279
+ }
280
+
281
+ .contacts-favourites {
282
+ margin: 0;
283
+ padding: 0;
284
+ list-style: none;
285
+ overflow: auto;
286
+
287
+ li {
288
+ list-style: none;
289
+ padding: 0;
290
+ margin: 0;
291
+ margin-right: 10px;
292
+ margin-bottom: 10px;
293
+ float: left;
294
+ cursor: pointer;
295
+ background: $background-color;
296
+ color: $text-color-primary;
297
+ border-radius: 4px;
298
+ overflow: hidden;
299
+
300
+ &:hover {
301
+ color: $text-color-accent;
302
+ }
303
+
304
+ img {
305
+ width: 30px;
306
+ height: 30px;
307
+ float: left;
308
+ }
309
+
310
+ span {
311
+ line-height: 30px;
312
+ padding: 0 10px;
313
+ }
314
+ }
315
+ }
316
+ }
317
+
318
+ .search-wrap {
319
+ margin: 0 auto;
320
+ margin-bottom: 10px;
321
+ padding: 0px 30px;
322
+ box-sizing: border-box;
323
+ width: 100%;
324
+ max-width: 600px;
325
+
326
+ i {
327
+ display: inline-block;
328
+ width: 22px;
329
+ }
330
+
331
+ input {
332
+ outline: none;
333
+ background: transparent;
334
+ border: none;
335
+ font-size: 15px;
336
+ position: relative;
337
+ top: -2px;
338
+ width: 100%;
339
+ padding: 0;
340
+ color: $text-color-primary;
341
+ }
342
+
343
+ .contacts-tot {
344
+ margin-right: 20px;
345
+ margin-left: 10px;
346
+ }
347
+
348
+ a {
349
+ border-bottom: 1px solid;
350
+ text-decoration: none;
351
+ color: $text-color-primary;
352
+
353
+ &:hover {
354
+ opacity: 0.6;
355
+ }
356
+ }
357
+ }
358
+
359
+ .contacts-root {
360
+ max-height: 100%;
361
+ }
362
+
363
+ .contacts-list {
364
+ //margin: 0 auto;
365
+ width: 100%;
366
+ max-width: 965px;
367
+ padding: 0px 30px;
368
+ box-sizing: border-box;
369
+
370
+ .contact {
371
+ margin: 10px 0;
372
+ padding: 5px;
373
+ box-sizing: border-box;
374
+ cursor: pointer;
375
+ transition: all 0.5s 0.25s;
376
+
377
+ .star {
378
+ .mdi-star {
379
+ color: #ffd730;
380
+ }
381
+ .mdi-star-outline {
382
+ opacity: 0.5;
383
+ }
384
+ }
385
+
386
+ .avatar {
387
+ width: 60px;
388
+ transition: all 0.5s 0.25s;
389
+
390
+ img {
391
+ border: 1px solid transparentize($text-color-primary, 0.9);
392
+ box-sizing: border-box;
393
+ width: 50px;
394
+ height: 50px;
395
+ border-radius: 50%;
396
+ transition: all 0.5s 0.25s;
397
+ }
398
+ }
399
+
400
+ .info {
401
+ word-break: break-word;
402
+
403
+ .name {
404
+ //.fullname {}
405
+
406
+ .email {
407
+ opacity: 0;
408
+ line-height: 0;
409
+ transition: all 0.5s 0.25s;
410
+ }
411
+
412
+ .phone {
413
+ display: none;
414
+ }
415
+ }
416
+
417
+ //.phone {}
418
+ }
419
+
420
+ &:hover {
421
+ margin: 15px -20px;
422
+ padding: 10px;
423
+ background-color: lighten($background-color, 20%);
424
+ border-radius: 5px;
425
+ box-shadow:
426
+ 0 8px 16px 0 rgba(40, 40, 90, 0.09),
427
+ 0 3px 6px 0 rgba(0, 0, 0, 0.065);
428
+
429
+ .avatar {
430
+ width: 90px;
431
+
432
+ img {
433
+ width: 90px;
434
+ height: 90px;
435
+ }
436
+ }
437
+
438
+ .info {
439
+ .name {
440
+ .email {
441
+ opacity: 1;
442
+ line-height: 1.4;
443
+ }
444
+ }
445
+ }
446
+ }
447
+ }
448
+ }
449
+
450
+ .contacts-root {
451
+ &.medium {
452
+ .search-card {
453
+ padding: 20px;
454
+ max-width: 260px;
455
+ //max-height: 260px;
456
+ }
457
+ }
458
+ &.small {
459
+ overflow-y: auto;
460
+ display: block;
461
+ -webkit-box-orient: vertical;
462
+ -webkit-box-direction: normal;
463
+ -ms-flex-direction: column;
464
+ flex-direction: column;
465
+ padding: 5px;
466
+
467
+ .search-card {
468
+ padding: 20px;
469
+ max-width: 100%;
470
+ width: 100%;
471
+ //max-height: 240px;
472
+ flex: none;
473
+ -webkit-box-flex: none;
474
+ -ms-flex: none;
475
+ display: block;
476
+ overflow: hidden !important;
477
+ }
478
+
479
+ .contacts-list {
480
+ flex: none;
481
+ -webkit-box-flex: none;
482
+ -ms-flex: none;
483
+ display: block;
484
+ overflow: hidden !important;
485
+ }
486
+ }
487
+ }
488
+}
489
+
490
+@media (max-width: 768px) {
491
+ .page-contacts {
492
+ .search-wrap {
493
+ padding: 0;
494
+ }
495
+ .contacts-list {
496
+ padding: 0px;
497
+
498
+ .contact {
499
+ .avatar {
500
+ width: 40px;
501
+
502
+ img {
503
+ width: 40px;
504
+ height: 40px;
505
+ }
506
+ }
507
+
508
+ .info {
509
+ .phone {
510
+ display: none;
511
+ }
512
+
513
+ .name {
514
+ .phone {
515
+ display: block;
516
+ }
517
+ }
518
+ }
519
+
520
+ &:hover {
521
+ margin: 15px 0px;
522
+
523
+ .avatar {
524
+ width: 60px;
525
+
526
+ img {
527
+ width: 60px;
528
+ height: 60px;
529
+ }
530
+ }
531
+ }
532
+ }
533
+ }
534
+
535
+ .contacts-root {
536
+ &.medium {
537
+ .contacts-list {
538
+ padding: 0 30px;
539
+ }
540
+ }
541
+ &.small {
542
+ .contacts-list {
543
+ padding: 8px;
544
+ }
545
+ }
546
+ }
547
+ }
548
+}
549
+</style>