@cryptotaxi247 / CoPilot / commits / fa211826

Duo integration (#247)

* feat: Add DUO integration to SOCFortress The code changes in `db_populate.py` add the DUO integration to SOCFortress by including the necessary information for DUO authentication keys. This allows users to integrate DUO with SOCFortress for enhanced security. * feat: Add DUO dashboard to Grafana schema and services This commit adds the DUO dashboard to the Grafana schema and services. The `dashboards.py` file in the `grafana/schema` directory is updated to include the `DuoDashboard` class with the `DUO_AUTH` dashboard. Similarly, the `dashboards.py` file in the `grafana/services` directory is updated to import and include the `DuoDashboard` in the `provision_dashboards` function. This allows users to provision the DUO dashboard in Grafana for enhanced security monitoring. * refactor: Rename integration_dup_provision_router to integration_duo_provision_router and add route * add duo auth logs dashboard template * refactor: Update DUO integration function names for consistency This commit updates the function names related to DUO integration for consistency. The function names in the `provision.py` file are changed from `invoke_duo_integration_collection` to `invoke_duo_integration_collect`. Similarly, the function names in the `scheduler.py` file are updated to match the changes. This ensures that all DUO integration functions have consistent naming conventions. * update: integrations api * update: CustomerIntegrationActions * improve: pinned-page component * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Jun 20, 2024 at 11:41 UTC fa211826e6d19cd6cb83c9c3c89652eee425eb5a
21 files changed +2987 -7
.vscode/settings.json
+1
@@ -10,6 +10,7 @@
10 "colord",
11 "commonmark",
12 "creationdate",
13 + "Crowdstrike",
14 "datejs",
15 "datetimesec",
16 "DFIR",
backend/app/connectors/grafana/dashboards/Duo/duo_auth.json new
+2074
@@ -0,0 +1,2074 @@
1 +{
2 + "annotations": {
3 + "list": [
4 + {
5 + "builtIn": 1,
6 + "datasource": {
7 + "type": "datasource",
8 + "uid": "grafana"
9 + },
10 + "enable": true,
11 + "hide": true,
12 + "iconColor": "rgba(0, 211, 255, 1)",
13 + "name": "Annotations & Alerts",
14 + "target": {
15 + "limit": 100,
16 + "matchAny": false,
17 + "tags": [],
18 + "type": "dashboard"
19 + },
20 + "type": "dashboard"
21 + }
22 + ]
23 + },
24 + "editable": false,
25 + "fiscalYearStartMonth": 0,
26 + "graphTooltip": 0,
27 + "id": null,
28 + "links": [
29 + {
30 + "asDropdown": true,
31 + "icon": "external link",
32 + "includeVars": true,
33 + "keepTime": true,
34 + "tags": ["EDR"],
35 + "targetBlank": true,
36 + "title": "",
37 + "type": "dashboards"
38 + }
39 + ],
40 + "liveNow": false,
41 + "panels": [
42 + {
43 + "datasource": {
44 + "type": "grafana-opensearch-datasource",
45 + "uid": "replace_datasource_uid"
46 + },
47 + "fieldConfig": {
48 + "defaults": {
49 + "mappings": [
50 + {
51 + "options": {
52 + "match": "null",
53 + "result": {
54 + "text": "N/A"
55 + }
56 + },
57 + "type": "special"
58 + }
59 + ],
60 + "thresholds": {
61 + "mode": "absolute",
62 + "steps": [
63 + {
64 + "color": "blue",
65 + "value": null
66 + }
67 + ]
68 + },
69 + "unit": "none"
70 + },
71 + "overrides": []
72 + },
73 + "gridPos": {
74 + "h": 7,
75 + "w": 4,
76 + "x": 0,
77 + "y": 0
78 + },
79 + "id": 43,
80 + "links": [],
81 + "options": {
82 + "colorMode": "value",
83 + "graphMode": "area",
84 + "justifyMode": "auto",
85 + "orientation": "horizontal",
86 + "reduceOptions": {
87 + "calcs": ["sum"],
88 + "fields": "",
89 + "values": false
90 + },
91 + "text": {},
92 + "textMode": "auto",
93 + "wideLayout": true
94 + },
95 + "pluginVersion": "10.2.3",
96 + "targets": [
97 + {
98 + "bucketAggs": [
99 + {
100 + "$$hashKey": "object:183",
101 + "field": "timestamp",
102 + "id": "2",
103 + "settings": {
104 + "interval": "auto",
105 + "min_doc_count": 0,
106 + "trimEdges": 0
107 + },
108 + "type": "date_histogram"
109 + }
110 + ],
111 + "datasource": {
112 + "type": "grafana-opensearch-datasource",
113 + "uid": "replace_datasource_uid"
114 + },
115 + "metrics": [
116 + {
117 + "$$hashKey": "object:181",
118 + "field": "select field",
119 + "id": "1",
120 + "type": "count"
121 + }
122 + ],
123 + "query": "",
124 + "refId": "A",
125 + "timeField": "timestamp"
126 + }
127 + ],
128 + "title": "DUO EVENTS",
129 + "type": "stat"
130 + },
131 + {
132 + "datasource": {
133 + "type": "grafana-opensearch-datasource",
134 + "uid": "replace_datasource_uid"
135 + },
136 + "fieldConfig": {
137 + "defaults": {
138 + "color": {
139 + "mode": "thresholds"
140 + },
141 + "custom": {
142 + "align": "auto",
143 + "cellOptions": {
144 + "type": "auto"
145 + },
146 + "inspect": false
147 + },
148 + "mappings": [],
149 + "thresholds": {
150 + "mode": "absolute",
151 + "steps": [
152 + {
153 + "color": "blue",
154 + "value": null
155 + }
156 + ]
157 + }
158 + },
159 + "overrides": [
160 + {
161 + "matcher": {
162 + "id": "byName",
163 + "options": "Count"
164 + },
165 + "properties": [
166 + {
167 + "id": "displayName",
168 + "value": "EVENTS"
169 + },
170 + {
171 + "id": "unit",
172 + "value": "short"
173 + },
174 + {
175 + "id": "decimals",
176 + "value": -1
177 + },
178 + {
179 + "id": "custom.align"
180 + }
181 + ]
182 + },
183 + {
184 + "matcher": {
185 + "id": "byName",
186 + "options": "USER NAME"
187 + },
188 + "properties": [
189 + {
190 + "id": "custom.width",
191 + "value": 388
192 + }
193 + ]
194 + }
195 + ]
196 + },
197 + "gridPos": {
198 + "h": 7,
199 + "w": 8,
200 + "x": 4,
201 + "y": 0
202 + },
203 + "id": 31,
204 + "options": {
205 + "cellHeight": "sm",
206 + "footer": {
207 + "countRows": false,
208 + "fields": "",
209 + "reducer": ["sum"],
210 + "show": false
211 + },
212 + "showHeader": true,
213 + "sortBy": []
214 + },
215 + "pluginVersion": "10.2.3",
216 + "targets": [
217 + {
218 + "bucketAggs": [
219 + {
220 + "$$hashKey": "object:65",
221 + "fake": true,
222 + "field": "user_name",
223 + "id": "4",
224 + "settings": {
225 + "min_doc_count": 1,
226 + "order": "desc",
227 + "orderBy": "_count",
228 + "size": "0"
229 + },
230 + "type": "terms"
231 + }
232 + ],
233 + "datasource": {
234 + "type": "grafana-opensearch-datasource",
235 + "uid": "replace_datasource_uid"
236 + },
237 + "metrics": [
238 + {
239 + "$$hashKey": "object:63",
240 + "field": "select field",
241 + "id": "1",
242 + "type": "count"
243 + }
244 + ],
245 + "query": "",
246 + "refId": "A",
247 + "timeField": "timestamp"
248 + }
249 + ],
250 + "title": "EVENTS BY ACCOUNT",
251 + "transformations": [
252 + {
253 + "id": "organize",
254 + "options": {
255 + "excludeByName": {},
256 + "includeByName": {},
257 + "indexByName": {},
258 + "renameByName": {
259 + "data_user_name": "USER",
260 + "user_name": "USER NAME"
261 + }
262 + }
263 + }
264 + ],
265 + "type": "table"
266 + },
267 + {
268 + "datasource": {
269 + "type": "grafana-opensearch-datasource",
270 + "uid": "replace_datasource_uid"
271 + },
272 + "fieldConfig": {
273 + "defaults": {
274 + "color": {
275 + "mode": "thresholds"
276 + },
277 + "custom": {
278 + "align": "auto",
279 + "cellOptions": {
280 + "type": "auto"
281 + },
282 + "inspect": false
283 + },
284 + "mappings": [],
285 + "thresholds": {
286 + "mode": "absolute",
287 + "steps": [
288 + {
289 + "color": "dark-orange",
290 + "value": null
291 + }
292 + ]
293 + }
294 + },
295 + "overrides": [
296 + {
297 + "matcher": {
298 + "id": "byName",
299 + "options": "Count"
300 + },
301 + "properties": [
302 + {
303 + "id": "displayName",
304 + "value": "EVENTS"
305 + },
306 + {
307 + "id": "unit",
308 + "value": "short"
309 + },
310 + {
311 + "id": "decimals",
312 + "value": -1
313 + },
314 + {
315 + "id": "custom.align"
316 + },
317 + {
318 + "id": "thresholds",
319 + "value": {
320 + "mode": "absolute",
321 + "steps": [
322 + {
323 + "color": "rgba(50, 172, 45, 0.97)",
324 + "value": null
325 + },
326 + {
327 + "color": "rgba(237, 129, 40, 0.89)",
328 + "value": 0
329 + },
330 + {
331 + "color": "#FA6400",
332 + "value": 1
333 + }
334 + ]
335 + }
336 + }
337 + ]
338 + },
339 + {
340 + "matcher": {
341 + "id": "byName",
342 + "options": "factor"
343 + },
344 + "properties": [
345 + {
346 + "id": "displayName",
347 + "value": "EVENTS BY TYPE"
348 + },
349 + {
350 + "id": "unit",
351 + "value": "short"
352 + },
353 + {
354 + "id": "decimals",
355 + "value": -1
356 + },
357 + {
358 + "id": "custom.align"
359 + }
360 + ]
361 + },
362 + {
363 + "matcher": {
364 + "id": "byName",
365 + "options": "ALERTS BY TYPE"
366 + },
367 + "properties": [
368 + {
369 + "id": "custom.width",
370 + "value": 717
371 + }
372 + ]
373 + }
374 + ]
375 + },
376 + "gridPos": {
377 + "h": 7,
378 + "w": 12,
379 + "x": 12,
380 + "y": 0
381 + },
382 + "id": 44,
383 + "options": {
384 + "cellHeight": "sm",
385 + "footer": {
386 + "countRows": false,
387 + "fields": "",
388 + "reducer": ["sum"],
389 + "show": false
390 + },
391 + "showHeader": true,
392 + "sortBy": []
393 + },
394 + "pluginVersion": "10.2.3",
395 + "targets": [
396 + {
397 + "bucketAggs": [
398 + {
399 + "$$hashKey": "object:206",
400 + "fake": true,
401 + "field": "factor",
402 + "id": "4",
403 + "settings": {
404 + "min_doc_count": 1,
405 + "order": "desc",
406 + "orderBy": "_term",
407 + "size": "0"
408 + },
409 + "type": "terms"
410 + }
411 + ],
412 + "datasource": {
413 + "type": "grafana-opensearch-datasource",
414 + "uid": "replace_datasource_uid"
415 + },
416 + "metrics": [
417 + {
418 + "$$hashKey": "object:204",
419 + "field": "select field",
420 + "id": "1",
421 + "type": "count"
422 + }
423 + ],
424 + "query": "",
425 + "refId": "A",
426 + "timeField": "timestamp"
427 + }
428 + ],
429 + "title": "EVENTS BY TYPE",
430 + "transformations": [],
431 + "type": "table"
432 + },
433 + {
434 + "datasource": {
435 + "type": "grafana-opensearch-datasource",
436 + "uid": "replace_datasource_uid"
437 + },
438 + "fieldConfig": {
439 + "defaults": {
440 + "color": {
441 + "mode": "thresholds"
442 + },
443 + "custom": {
444 + "align": "auto",
445 + "cellOptions": {
446 + "type": "auto"
447 + },
448 + "inspect": false
449 + },
450 + "mappings": [],
451 + "thresholds": {
452 + "mode": "absolute",
453 + "steps": [
454 + {
455 + "color": "blue",
456 + "value": null
457 + }
458 + ]
459 + }
460 + },
461 + "overrides": [
462 + {
463 + "matcher": {
464 + "id": "byName",
465 + "options": "Time"
466 + },
467 + "properties": [
468 + {
469 + "id": "displayName",
470 + "value": "Time"
471 + },
472 + {
473 + "id": "unit",
474 + "value": "time: YYYY-MM-DD HH:mm:ss"
475 + },
476 + {
477 + "id": "custom.align"
478 + }
479 + ]
480 + },
481 + {
482 + "matcher": {
483 + "id": "byName",
484 + "options": "Count"
485 + },
486 + "properties": [
487 + {
488 + "id": "displayName",
489 + "value": "EVENTS"
490 + },
491 + {
492 + "id": "unit",
493 + "value": "short"
494 + },
495 + {
496 + "id": "decimals",
497 + "value": -1
498 + },
499 + {
500 + "id": "custom.align"
501 + }
502 + ]
503 + },
504 + {
505 + "matcher": {
506 + "id": "byName",
507 + "options": "ACCESS DEVICE HOSTNAME"
508 + },
509 + "properties": [
510 + {
511 + "id": "custom.width",
512 + "value": 388
513 + }
514 + ]
515 + }
516 + ]
517 + },
518 + "gridPos": {
519 + "h": 20,
520 + "w": 6,
521 + "x": 0,
522 + "y": 7
523 + },
524 + "id": 50,
525 + "options": {
526 + "cellHeight": "sm",
527 + "footer": {
528 + "countRows": false,
529 + "fields": "",
530 + "reducer": ["sum"],
531 + "show": false
532 + },
533 + "showHeader": true,
534 + "sortBy": []
535 + },
536 + "pluginVersion": "10.2.3",
537 + "targets": [
538 + {
539 + "bucketAggs": [
540 + {
541 + "$$hashKey": "object:65",
542 + "fake": true,
543 + "field": "access_device_hostname",
544 + "id": "4",
545 + "settings": {
546 + "min_doc_count": 1,
547 + "order": "desc",
548 + "orderBy": "_count",
549 + "size": "0"
550 + },
551 + "type": "terms"
552 + }
553 + ],
554 + "datasource": {
555 + "type": "grafana-opensearch-datasource",
556 + "uid": "replace_datasource_uid"
557 + },
558 + "metrics": [
559 + {
560 + "$$hashKey": "object:63",
561 + "field": "select field",
562 + "id": "1",
563 + "type": "count"
564 + }
565 + ],
566 + "query": "!access_device_hostname:null",
567 + "refId": "A",
568 + "timeField": "timestamp"
569 + }
570 + ],
571 + "title": "EVENTS BY ACCESS DEVICE",
572 + "transformations": [
573 + {
574 + "id": "organize",
575 + "options": {
576 + "excludeByName": {},
577 + "includeByName": {},
578 + "indexByName": {},
579 + "renameByName": {
580 + "access_device_hostname": "ACCESS DEVICE HOSTNAME",
581 + "data_access_device_hostname": "ACCESS DEVICE",
582 + "data_user_name": "USER"
583 + }
584 + }
585 + }
586 + ],
587 + "type": "table"
588 + },
589 + {
590 + "datasource": {
591 + "type": "grafana-opensearch-datasource",
592 + "uid": "replace_datasource_uid"
593 + },
594 + "fieldConfig": {
595 + "defaults": {
596 + "color": {
597 + "mode": "palette-classic"
598 + },
599 + "custom": {
600 + "axisBorderShow": false,
601 + "axisCenteredZero": false,
602 + "axisColorMode": "text",
603 + "axisLabel": "",
604 + "axisPlacement": "auto",
605 + "barAlignment": 0,
606 + "drawStyle": "bars",
607 + "fillOpacity": 0,
608 + "gradientMode": "none",
609 + "hideFrom": {
610 + "legend": false,
611 + "tooltip": false,
612 + "viz": false
613 + },
614 + "insertNulls": false,
615 + "lineInterpolation": "linear",
616 + "lineWidth": 1,
617 + "pointSize": 5,
618 + "scaleDistribution": {
619 + "type": "linear"
620 + },
621 + "showPoints": "auto",
622 + "spanNulls": false,
623 + "stacking": {
624 + "group": "A",
625 + "mode": "none"
626 + },
627 + "thresholdsStyle": {
628 + "mode": "off"
629 + }
630 + },
631 + "mappings": [],
632 + "thresholds": {
633 + "mode": "absolute",
634 + "steps": [
635 + {
636 + "color": "green",
637 + "value": null
638 + },
639 + {
640 + "color": "red",
641 + "value": 80
642 + }
643 + ]
644 + }
645 + },
646 + "overrides": []
647 + },
648 + "gridPos": {
649 + "h": 12,
650 + "w": 18,
651 + "x": 6,
652 + "y": 7
653 + },
654 + "id": 49,
655 + "options": {
656 + "legend": {
657 + "calcs": [],
658 + "displayMode": "list",
659 + "placement": "right",
660 + "showLegend": true
661 + },
662 + "tooltip": {
663 + "mode": "single",
664 + "sort": "none"
665 + }
666 + },
667 + "targets": [
668 + {
669 + "alias": "",
670 + "bucketAggs": [
671 + {
672 + "field": "user_name",
673 + "id": "2",
674 + "settings": {
675 + "min_doc_count": "1",
676 + "order": "desc",
677 + "orderBy": "_count",
678 + "size": "10"
679 + },
680 + "type": "terms"
681 + },
682 + {
683 + "field": "timestamp",
684 + "id": "3",
685 + "settings": {
686 + "interval": "10m",
687 + "min_doc_count": "0",
688 + "timeZone": "utc",
689 + "trimEdges": "0"
690 + },
691 + "type": "date_histogram"
692 + }
693 + ],
694 + "datasource": {
695 + "type": "grafana-opensearch-datasource",
696 + "uid": "replace_datasource_uid"
697 + },
698 + "metrics": [
699 + {
700 + "id": "1",
701 + "type": "count"
702 + }
703 + ],
704 + "query": "",
705 + "refId": "A",
706 + "timeField": "timestamp"
707 + }
708 + ],
709 + "title": "TOP 10 ACCOUNTS - HISTOGRAM",
710 + "type": "timeseries"
711 + },
712 + {
713 + "datasource": {
714 + "type": "grafana-opensearch-datasource",
715 + "uid": "replace_datasource_uid"
716 + },
717 + "fieldConfig": {
718 + "defaults": {
719 + "color": {
720 + "mode": "thresholds"
721 + },
722 + "custom": {
723 + "hideFrom": {
724 + "legend": false,
725 + "tooltip": false,
726 + "viz": false
727 + }
728 + },
729 + "mappings": [],
730 + "thresholds": {
731 + "mode": "absolute",
732 + "steps": [
733 + {
734 + "color": "rgba(245, 54, 54, 0.9)",
735 + "value": null
736 + },
737 + {
738 + "color": "rgba(237, 129, 40, 0.89)",
739 + "value": 0
740 + },
741 + {
742 + "color": "rgba(50, 172, 45, 0.97)",
743 + "value": 10
744 + }
745 + ]
746 + }
747 + },
748 + "overrides": []
749 + },
750 + "gridPos": {
751 + "h": 8,
752 + "w": 7,
753 + "x": 6,
754 + "y": 19
755 + },
756 + "id": 53,
757 + "maxDataPoints": 1,
758 + "options": {
759 + "basemap": {
760 + "name": "Basemap",
761 + "type": "default"
762 + },
763 + "controls": {
764 + "mouseWheelZoom": false,
765 + "showAttribution": true,
766 + "showDebug": false,
767 + "showMeasure": false,
768 + "showScale": false,
769 + "showZoom": true
770 + },
771 + "layers": [
772 + {
773 + "config": {
774 + "showLegend": true,
775 + "style": {
776 + "color": {
777 + "fixed": "dark-green"
778 + },
779 + "opacity": 0.4,
780 + "rotation": {
781 + "fixed": 0,
782 + "max": 360,
783 + "min": -360,
784 + "mode": "mod"
785 + },
786 + "size": {
787 + "fixed": 5,
788 + "max": 30,
789 + "min": 2
790 + },
791 + "symbol": {
792 + "fixed": "img/icons/marker/circle.svg",
793 + "mode": "fixed"
794 + },
795 + "symbolAlign": {
796 + "horizontal": "center",
797 + "vertical": "center"
798 + },
799 + "textConfig": {
800 + "fontSize": 12,
801 + "offsetX": 0,
802 + "offsetY": 0,
803 + "textAlign": "center",
804 + "textBaseline": "middle"
805 + }
806 + }
807 + },
808 + "location": {
809 + "gazetteer": "public/gazetteer/countries.json",
810 + "mode": "lookup"
811 + },
812 + "name": "Layer 0",
813 + "tooltip": true,
814 + "type": "markers"
815 + }
816 + ],
817 + "tooltip": {
818 + "mode": "details"
819 + },
820 + "view": {
821 + "allLayers": true,
822 + "id": "zero",
823 + "lat": 0,
824 + "lon": 0,
825 + "zoom": 1
826 + }
827 + },
828 + "pluginVersion": "10.2.3",
829 + "targets": [
830 + {
831 + "alias": "",
832 + "bucketAggs": [
833 + {
834 + "field": "access_device_location_country",
835 + "id": "3",
836 + "settings": {
837 + "min_doc_count": "1",
838 + "order": "desc",
839 + "orderBy": "_count",
840 + "size": "0"
841 + },
842 + "type": "terms"
843 + },
844 + {
845 + "field": "timestamp",
846 + "id": "2",
847 + "settings": {
848 + "interval": "auto"
849 + },
850 + "type": "date_histogram"
851 + }
852 + ],
853 + "datasource": {
854 + "type": "grafana-opensearch-datasource",
855 + "uid": "replace_datasource_uid"
856 + },
857 + "metrics": [
858 + {
859 + "id": "1",
860 + "type": "count"
861 + }
862 + ],
863 + "query": "",
864 + "refId": "A",
865 + "timeField": "timestamp"
866 + }
867 + ],
868 + "title": "ACCESS DEVICE GeoIP",
869 + "transformations": [
870 + {
871 + "id": "reduce",
872 + "options": {
873 + "reducers": ["sum"]
874 + }
875 + }
876 + ],
877 + "type": "geomap"
878 + },
879 + {
880 + "datasource": {
881 + "type": "grafana-opensearch-datasource",
882 + "uid": "replace_datasource_uid"
883 + },
884 + "fieldConfig": {
885 + "defaults": {
886 + "color": {
887 + "mode": "thresholds"
888 + },
889 + "custom": {
890 + "align": "auto",
891 + "cellOptions": {
892 + "type": "auto"
893 + },
894 + "inspect": false
895 + },
896 + "mappings": [],
897 + "thresholds": {
898 + "mode": "absolute",
899 + "steps": [
900 + {
901 + "color": "blue",
902 + "value": null
903 + }
904 + ]
905 + }
906 + },
907 + "overrides": [
908 + {
909 + "matcher": {
910 + "id": "byName",
911 + "options": "Count"
912 + },
913 + "properties": [
914 + {
915 + "id": "displayName",
916 + "value": "EVENTS"
917 + },
918 + {
919 + "id": "unit",
920 + "value": "short"
921 + },
922 + {
923 + "id": "decimals",
924 + "value": -1
925 + },
926 + {
927 + "id": "custom.align"
928 + }
929 + ]
930 + },
931 + {
932 + "matcher": {
933 + "id": "byName",
934 + "options": "AUTH DEVICE NAME"
935 + },
936 + "properties": [
937 + {
938 + "id": "custom.width",
939 + "value": 388
940 + }
941 + ]
942 + }
943 + ]
944 + },
945 + "gridPos": {
946 + "h": 8,
947 + "w": 5,
948 + "x": 13,
949 + "y": 19
950 + },
951 + "id": 51,
952 + "options": {
953 + "cellHeight": "sm",
954 + "footer": {
955 + "countRows": false,
956 + "fields": "",
957 + "reducer": ["sum"],
958 + "show": false
959 + },
960 + "showHeader": true,
961 + "sortBy": []
962 + },
963 + "pluginVersion": "10.2.3",
964 + "targets": [
965 + {
966 + "bucketAggs": [
967 + {
968 + "$$hashKey": "object:65",
969 + "fake": true,
970 + "field": "auth_device_name",
971 + "id": "4",
972 + "settings": {
973 + "min_doc_count": 1,
974 + "order": "desc",
975 + "orderBy": "_count",
976 + "size": "0"
977 + },
978 + "type": "terms"
979 + }
980 + ],
981 + "datasource": {
982 + "type": "grafana-opensearch-datasource",
983 + "uid": "replace_datasource_uid"
984 + },
985 + "metrics": [
986 + {
987 + "$$hashKey": "object:63",
988 + "field": "select field",
989 + "id": "1",
990 + "type": "count"
991 + }
992 + ],
993 + "query": "!auth_device_name:null",
994 + "refId": "A",
995 + "timeField": "timestamp"
996 + }
997 + ],
998 + "title": "EVENTS BY AUTH DEVICE",
999 + "transformations": [
1000 + {
1001 + "id": "organize",
1002 + "options": {
1003 + "excludeByName": {},
1004 + "includeByName": {},
1005 + "indexByName": {},
1006 + "renameByName": {
1007 + "auth_device_name": "AUTH DEVICE NAME",
1008 + "data_access_device_hostname": "ACCESS DEVICE",
1009 + "data_auth_device_name": "AUTH DEVICE",
1010 + "data_user_name": "USER"
1011 + }
1012 + }
1013 + }
1014 + ],
1015 + "type": "table"
1016 + },
1017 + {
1018 + "datasource": {
1019 + "type": "grafana-opensearch-datasource",
1020 + "uid": "replace_datasource_uid"
1021 + },
1022 + "fieldConfig": {
1023 + "defaults": {
1024 + "color": {
1025 + "mode": "thresholds"
1026 + },
1027 + "custom": {
1028 + "hideFrom": {
1029 + "legend": false,
1030 + "tooltip": false,
1031 + "viz": false
1032 + }
1033 + },
1034 + "mappings": [],
1035 + "thresholds": {
1036 + "mode": "absolute",
1037 + "steps": [
1038 + {
1039 + "color": "rgba(245, 54, 54, 0.9)",
1040 + "value": null
1041 + },
1042 + {
1043 + "color": "rgba(237, 129, 40, 0.89)",
1044 + "value": 0
1045 + },
1046 + {
1047 + "color": "rgba(50, 172, 45, 0.97)",
1048 + "value": 10
1049 + }
1050 + ]
1051 + }
1052 + },
1053 + "overrides": []
1054 + },
1055 + "gridPos": {
1056 + "h": 8,
1057 + "w": 6,
1058 + "x": 18,
1059 + "y": 19
1060 + },
1061 + "id": 54,
1062 + "maxDataPoints": 1,
1063 + "options": {
1064 + "basemap": {
1065 + "name": "Basemap",
1066 + "type": "default"
1067 + },
1068 + "controls": {
1069 + "mouseWheelZoom": false,
1070 + "showAttribution": true,
1071 + "showDebug": false,
1072 + "showMeasure": false,
1073 + "showScale": false,
1074 + "showZoom": true
1075 + },
1076 + "layers": [
1077 + {
1078 + "config": {
1079 + "showLegend": true,
1080 + "style": {
1081 + "color": {
1082 + "fixed": "dark-green"
1083 + },
1084 + "opacity": 0.4,
1085 + "rotation": {
1086 + "fixed": 0,
1087 + "max": 360,
1088 + "min": -360,
1089 + "mode": "mod"
1090 + },
1091 + "size": {
1092 + "fixed": 5,
1093 + "max": 30,
1094 + "min": 2
1095 + },
1096 + "symbol": {
1097 + "fixed": "img/icons/marker/circle.svg",
1098 + "mode": "fixed"
1099 + },
1100 + "symbolAlign": {
1101 + "horizontal": "center",
1102 + "vertical": "center"
1103 + },
1104 + "textConfig": {
1105 + "fontSize": 12,
1106 + "offsetX": 0,
1107 + "offsetY": 0,
1108 + "textAlign": "center",
1109 + "textBaseline": "middle"
1110 + }
1111 + }
1112 + },
1113 + "location": {
1114 + "gazetteer": "public/gazetteer/countries.json",
1115 + "mode": "lookup"
1116 + },
1117 + "name": "Layer 0",
1118 + "tooltip": true,
1119 + "type": "markers"
1120 + }
1121 + ],
1122 + "tooltip": {
1123 + "mode": "details"
1124 + },
1125 + "view": {
1126 + "allLayers": true,
1127 + "id": "zero",
1128 + "lat": 0,
1129 + "lon": 0,
1130 + "zoom": 1
1131 + }
1132 + },
1133 + "pluginVersion": "10.2.3",
1134 + "targets": [
1135 + {
1136 + "alias": "",
1137 + "bucketAggs": [
1138 + {
1139 + "field": "auth_device_location_country",
1140 + "id": "3",
1141 + "settings": {
1142 + "min_doc_count": "1",
1143 + "order": "desc",
1144 + "orderBy": "_count",
1145 + "size": "0"
1146 + },
1147 + "type": "terms"
1148 + },
1149 + {
1150 + "field": "timestamp",
1151 + "id": "2",
1152 + "settings": {
1153 + "interval": "auto"
1154 + },
1155 + "type": "date_histogram"
1156 + }
1157 + ],
1158 + "datasource": {
1159 + "type": "grafana-opensearch-datasource",
1160 + "uid": "replace_datasource_uid"
1161 + },
1162 + "metrics": [
1163 + {
1164 + "id": "1",
1165 + "type": "count"
1166 + }
1167 + ],
1168 + "query": "",
1169 + "refId": "A",
1170 + "timeField": "timestamp"
1171 + }
1172 + ],
1173 + "title": "AUTH DEVICE GeoIP",
1174 + "transformations": [
1175 + {
1176 + "id": "reduce",
1177 + "options": {
1178 + "reducers": ["sum"]
1179 + }
1180 + }
1181 + ],
1182 + "type": "geomap"
1183 + },
1184 + {
1185 + "datasource": {
1186 + "type": "grafana-opensearch-datasource",
1187 + "uid": "replace_datasource_uid"
1188 + },
1189 + "fieldConfig": {
1190 + "defaults": {
1191 + "color": {
1192 + "mode": "palette-classic"
1193 + },
1194 + "custom": {
1195 + "hideFrom": {
1196 + "legend": false,
1197 + "tooltip": false,
1198 + "viz": false
1199 + }
1200 + },
1201 + "decimals": 0,
1202 + "mappings": [],
1203 + "unit": "short"
1204 + },
1205 + "overrides": [
1206 + {
1207 + "matcher": {
1208 + "id": "byName",
1209 + "options": "1"
1210 + },
1211 + "properties": [
1212 + {
1213 + "id": "color",
1214 + "value": {
1215 + "fixedColor": "#C8F2C2",
1216 + "mode": "fixed"
1217 + }
1218 + }
1219 + ]
1220 + },
1221 + {
1222 + "matcher": {
1223 + "id": "byName",
1224 + "options": "success"
1225 + },
1226 + "properties": [
1227 + {
1228 + "id": "color",
1229 + "value": {
1230 + "fixedColor": "#96D98D",
1231 + "mode": "fixed"
1232 + }
1233 + }
1234 + ]
1235 + },
1236 + {
1237 + "matcher": {
1238 + "id": "byName",
1239 + "options": "denied"
1240 + },
1241 + "properties": [
1242 + {
1243 + "id": "color",
1244 + "value": {
1245 + "fixedColor": "#56A64B",
1246 + "mode": "fixed"
1247 + }
1248 + }
1249 + ]
1250 + },
1251 + {
1252 + "matcher": {
1253 + "id": "byName",
1254 + "options": "4"
1255 + },
1256 + "properties": [
1257 + {
1258 + "id": "color",
1259 + "value": {
1260 + "fixedColor": "#37872D",
1261 + "mode": "fixed"
1262 + }
1263 + }
1264 + ]
1265 + },
1266 + {
1267 + "matcher": {
1268 + "id": "byName",
1269 + "options": "5"
1270 + },
1271 + "properties": [
1272 + {
1273 + "id": "color",
1274 + "value": {
1275 + "fixedColor": "#FFF899",
1276 + "mode": "fixed"
1277 + }
1278 + }
1279 + ]
1280 + },
1281 + {
1282 + "matcher": {
1283 + "id": "byName",
1284 + "options": "7"
1285 + },
1286 + "properties": [
1287 + {
1288 + "id": "color",
1289 + "value": {
1290 + "fixedColor": "#F2CC0C",
1291 + "mode": "fixed"
1292 + }
1293 + }
1294 + ]
1295 + },
1296 + {
1297 + "matcher": {
1298 + "id": "byName",
1299 + "options": "9"
1300 + },
1301 + "properties": [
1302 + {
1303 + "id": "color",
1304 + "value": {
1305 + "fixedColor": "#E0B400",
1306 + "mode": "fixed"
1307 + }
1308 + }
1309 + ]
1310 + },
1311 + {
1312 + "matcher": {
1313 + "id": "byName",
1314 + "options": "10"
1315 + },
1316 + "properties": [
1317 + {
1318 + "id": "color",
1319 + "value": {
1320 + "fixedColor": "#FFCB7D",
1321 + "mode": "fixed"
1322 + }
1323 + }
1324 + ]
1325 + },
1326 + {
1327 + "matcher": {
1328 + "id": "byName",
1329 + "options": "12"
1330 + },
1331 + "properties": [
1332 + {
1333 + "id": "color",
1334 + "value": {
1335 + "fixedColor": "#FFA6B0",
1336 + "mode": "fixed"
1337 + }
1338 + }
1339 + ]
1340 + },
1341 + {
1342 + "matcher": {
1343 + "id": "byName",
1344 + "options": "13"
1345 + },
1346 + "properties": [
1347 + {
1348 + "id": "color",
1349 + "value": {
1350 + "fixedColor": "#FF7383",
1351 + "mode": "fixed"
1352 + }
1353 + }
1354 + ]
1355 + },
1356 + {
1357 + "matcher": {
1358 + "id": "byName",
1359 + "options": "N/A"
1360 + },
1361 + "properties": [
1362 + {
1363 + "id": "color",
1364 + "value": {
1365 + "fixedColor": "semi-dark-orange",
1366 + "mode": "fixed"
1367 + }
1368 + }
1369 + ]
1370 + },
1371 + {
1372 + "matcher": {
1373 + "id": "byName",
1374 + "options": "failed"
1375 + },
1376 + "properties": [
1377 + {
1378 + "id": "color",
1379 + "value": {
1380 + "fixedColor": "semi-dark-red",
1381 + "mode": "fixed"
1382 + }
1383 + }
1384 + ]
1385 + },
1386 + {
1387 + "matcher": {
1388 + "id": "byName",
1389 + "options": "denied"
1390 + },
1391 + "properties": [
1392 + {
1393 + "id": "color",
1394 + "value": {
1395 + "fixedColor": "light-red",
1396 + "mode": "fixed"
1397 + }
1398 + }
1399 + ]
1400 + }
1401 + ]
1402 + },
1403 + "gridPos": {
1404 + "h": 12,
1405 + "w": 6,
1406 + "x": 0,
1407 + "y": 27
1408 + },
1409 + "id": 47,
1410 + "links": [],
1411 + "maxDataPoints": 3,
1412 + "options": {
1413 + "legend": {
1414 + "calcs": [],
1415 + "displayMode": "table",
1416 + "placement": "right",
1417 + "showLegend": true,
1418 + "values": ["value"]
1419 + },
1420 + "pieType": "donut",
1421 + "reduceOptions": {
1422 + "calcs": ["sum"],
1423 + "fields": "",
1424 + "values": false
1425 + },
1426 + "tooltip": {
1427 + "mode": "single",
1428 + "sort": "none"
1429 + }
1430 + },
1431 + "targets": [
1432 + {
1433 + "bucketAggs": [
1434 + {
1435 + "$$hashKey": "object:493",
1436 + "fake": true,
1437 + "field": "result",
1438 + "id": "3",
1439 + "settings": {
1440 + "min_doc_count": 1,
1441 + "missing": "N/A",
1442 + "order": "desc",
1443 + "orderBy": "_term",
1444 + "size": "10"
1445 + },
1446 + "type": "terms"
1447 + },
1448 + {
1449 + "$$hashKey": "object:494",
1450 + "field": "timestamp",
1451 + "id": "2",
1452 + "settings": {
1453 + "interval": "auto",
1454 + "min_doc_count": 0,
1455 + "trimEdges": 0
1456 + },
1457 + "type": "date_histogram"
1458 + }
1459 + ],
1460 + "datasource": {
1461 + "type": "grafana-opensearch-datasource",
1462 + "uid": "replace_datasource_uid"
1463 + },
1464 + "metrics": [
1465 + {
1466 + "$$hashKey": "object:491",
1467 + "field": "select field",
1468 + "id": "1",
1469 + "meta": {},
1470 + "settings": {},
1471 + "type": "count"
1472 + }
1473 + ],
1474 + "query": "",
1475 + "refId": "A",
1476 + "timeField": "timestamp"
1477 + }
1478 + ],
1479 + "title": "RESULTS",
1480 + "type": "piechart"
1481 + },
1482 + {
1483 + "datasource": {
1484 + "type": "grafana-opensearch-datasource",
1485 + "uid": "replace_datasource_uid"
1486 + },
1487 + "fieldConfig": {
1488 + "defaults": {
1489 + "color": {
1490 + "mode": "thresholds"
1491 + },
1492 + "custom": {
1493 + "align": "auto",
1494 + "cellOptions": {
1495 + "type": "auto"
1496 + },
1497 + "inspect": false
1498 + },
1499 + "decimals": 0,
1500 + "mappings": [],
1501 + "thresholds": {
1502 + "mode": "absolute",
1503 + "steps": [
1504 + {
1505 + "color": "orange",
1506 + "value": null
1507 + }
1508 + ]
1509 + },
1510 + "unit": "short"
1511 + },
1512 + "overrides": [
1513 + {
1514 + "matcher": {
1515 + "id": "byName",
1516 + "options": "USER NAME"
1517 + },
1518 + "properties": [
1519 + {
1520 + "id": "color",
1521 + "value": {
1522 + "fixedColor": "orange",
1523 + "mode": "fixed"
1524 + }
1525 + },
1526 + {
1527 + "id": "custom.cellOptions",
1528 + "value": {
1529 + "type": "color-text"
1530 + }
1531 + }
1532 + ]
1533 + }
1534 + ]
1535 + },
1536 + "gridPos": {
1537 + "h": 12,
1538 + "w": 6,
1539 + "x": 6,
1540 + "y": 27
1541 + },
1542 + "id": 46,
1543 + "links": [],
1544 + "maxDataPoints": 3,
1545 + "options": {
1546 + "cellHeight": "sm",
1547 + "footer": {
1548 + "countRows": false,
1549 + "fields": "",
1550 + "reducer": ["sum"],
1551 + "show": false
1552 + },
1553 + "showHeader": true,
1554 + "sortBy": []
1555 + },
1556 + "pluginVersion": "10.2.3",
1557 + "targets": [
1558 + {
1559 + "bucketAggs": [
1560 + {
1561 + "$$hashKey": "object:493",
1562 + "fake": true,
1563 + "field": "user_name",
1564 + "id": "3",
1565 + "settings": {
1566 + "min_doc_count": 1,
1567 + "order": "desc",
1568 + "orderBy": "_count",
1569 + "size": "10"
1570 + },
1571 + "type": "terms"
1572 + }
1573 + ],
1574 + "datasource": {
1575 + "type": "grafana-opensearch-datasource",
1576 + "uid": "replace_datasource_uid"
1577 + },
1578 + "metrics": [
1579 + {
1580 + "$$hashKey": "object:491",
1581 + "field": "select field",
1582 + "id": "1",
1583 + "meta": {},
1584 + "settings": {},
1585 + "type": "count"
1586 + }
1587 + ],
1588 + "query": "result:denied",
1589 + "refId": "A",
1590 + "timeField": "timestamp"
1591 + }
1592 + ],
1593 + "title": "AUTHS DENIED",
1594 + "transformations": [
1595 + {
1596 + "id": "organize",
1597 + "options": {
1598 + "excludeByName": {},
1599 + "includeByName": {},
1600 + "indexByName": {},
1601 + "renameByName": {
1602 + "data_sca_policy": "POLICY",
1603 + "data_user_name": "USER",
1604 + "user_name": "USER NAME"
1605 + }
1606 + }
1607 + }
1608 + ],
1609 + "type": "table"
1610 + },
1611 + {
1612 + "datasource": {
1613 + "type": "grafana-opensearch-datasource",
1614 + "uid": "replace_datasource_uid"
1615 + },
1616 + "fieldConfig": {
1617 + "defaults": {
1618 + "color": {
1619 + "mode": "thresholds"
1620 + },
1621 + "custom": {
1622 + "align": "auto",
1623 + "cellOptions": {
1624 + "type": "auto"
1625 + },
1626 + "inspect": false
1627 + },
1628 + "decimals": 0,
1629 + "mappings": [],
1630 + "thresholds": {
1631 + "mode": "absolute",
1632 + "steps": [
1633 + {
1634 + "color": "orange",
1635 + "value": null
1636 + }
1637 + ]
1638 + },
1639 + "unit": "short"
1640 + },
1641 + "overrides": [
1642 + {
1643 + "matcher": {
1644 + "id": "byName",
1645 + "options": "ACCESS DEVICE HOSTNAME"
1646 + },
1647 + "properties": [
1648 + {
1649 + "id": "color",
1650 + "value": {
1651 + "fixedColor": "orange",
1652 + "mode": "fixed"
1653 + }
1654 + },
1655 + {
1656 + "id": "custom.cellOptions",
1657 + "value": {
1658 + "type": "color-text"
1659 + }
1660 + }
1661 + ]
1662 + }
1663 + ]
1664 + },
1665 + "gridPos": {
1666 + "h": 12,
1667 + "w": 6,
1668 + "x": 12,
1669 + "y": 27
1670 + },
1671 + "id": 55,
1672 + "links": [],
1673 + "maxDataPoints": 3,
1674 + "options": {
1675 + "cellHeight": "sm",
1676 + "footer": {
1677 + "countRows": false,
1678 + "fields": "",
1679 + "reducer": ["sum"],
1680 + "show": false
1681 + },
1682 + "showHeader": true,
1683 + "sortBy": []
1684 + },
1685 + "pluginVersion": "10.2.3",
1686 + "targets": [
1687 + {
1688 + "bucketAggs": [
1689 + {
1690 + "$$hashKey": "object:493",
1691 + "fake": true,
1692 + "field": "access_device_hostname",
1693 + "id": "3",
1694 + "settings": {
1695 + "min_doc_count": 1,
1696 + "order": "desc",
1697 + "orderBy": "_count",
1698 + "size": "10"
1699 + },
1700 + "type": "terms"
1701 + }
1702 + ],
1703 + "datasource": {
1704 + "type": "grafana-opensearch-datasource",
1705 + "uid": "replace_datasource_uid"
1706 + },
1707 + "metrics": [
1708 + {
1709 + "$$hashKey": "object:491",
1710 + "field": "select field",
1711 + "id": "1",
1712 + "meta": {},
1713 + "settings": {},
1714 + "type": "count"
1715 + }
1716 + ],
1717 + "query": "result:denied AND !access_device_hostname:null",
1718 + "refId": "A",
1719 + "timeField": "timestamp"
1720 + }
1721 + ],
1722 + "title": "AUTHS DENIED (ACCESS DEVICE)",
1723 + "transformations": [
1724 + {
1725 + "id": "organize",
1726 + "options": {
1727 + "excludeByName": {},
1728 + "includeByName": {},
1729 + "indexByName": {},
1730 + "renameByName": {
1731 + "access_device_hostname": "ACCESS DEVICE HOSTNAME",
1732 + "data_access_device_hostname": "ACCESS DEVICE",
1733 + "data_sca_policy": "POLICY",
1734 + "data_user_name": "USER"
1735 + }
1736 + }
1737 + }
1738 + ],
1739 + "type": "table"
1740 + },
1741 + {
1742 + "datasource": {
1743 + "type": "grafana-opensearch-datasource",
1744 + "uid": "replace_datasource_uid"
1745 + },
1746 + "fieldConfig": {
1747 + "defaults": {
1748 + "color": {
1749 + "mode": "thresholds"
1750 + },
1751 + "custom": {
1752 + "align": "auto",
1753 + "cellOptions": {
1754 + "type": "auto"
1755 + },
1756 + "inspect": false
1757 + },
1758 + "decimals": 0,
1759 + "mappings": [],
1760 + "thresholds": {
1761 + "mode": "absolute",
1762 + "steps": [
1763 + {
1764 + "color": "orange",
1765 + "value": null
1766 + }
1767 + ]
1768 + },
1769 + "unit": "short"
1770 + },
1771 + "overrides": [
1772 + {
1773 + "matcher": {
1774 + "id": "byName",
1775 + "options": "AUTH DEVICE NAME"
1776 + },
1777 + "properties": [
1778 + {
1779 + "id": "color",
1780 + "value": {
1781 + "fixedColor": "orange",
1782 + "mode": "fixed"
1783 + }
1784 + },
1785 + {
1786 + "id": "custom.cellOptions",
1787 + "value": {
1788 + "type": "color-text"
1789 + }
1790 + }
1791 + ]
1792 + }
1793 + ]
1794 + },
1795 + "gridPos": {
1796 + "h": 12,
1797 + "w": 6,
1798 + "x": 18,
1799 + "y": 27
1800 + },
1801 + "id": 56,
1802 + "links": [],
1803 + "maxDataPoints": 3,
1804 + "options": {
1805 + "cellHeight": "sm",
1806 + "footer": {
1807 + "countRows": false,
1808 + "fields": "",
1809 + "reducer": ["sum"],
1810 + "show": false
1811 + },
1812 + "showHeader": true,
1813 + "sortBy": []
1814 + },
1815 + "pluginVersion": "10.2.3",
1816 + "targets": [
1817 + {
1818 + "bucketAggs": [
1819 + {
1820 + "$$hashKey": "object:493",
1821 + "fake": true,
1822 + "field": "auth_device_name",
1823 + "id": "3",
1824 + "settings": {
1825 + "min_doc_count": 1,
1826 + "order": "desc",
1827 + "orderBy": "_count",
1828 + "size": "10"
1829 + },
1830 + "type": "terms"
1831 + }
1832 + ],
1833 + "datasource": {
1834 + "type": "grafana-opensearch-datasource",
1835 + "uid": "replace_datasource_uid"
1836 + },
1837 + "metrics": [
1838 + {
1839 + "$$hashKey": "object:491",
1840 + "field": "select field",
1841 + "id": "1",
1842 + "meta": {},
1843 + "settings": {},
1844 + "type": "count"
1845 + }
1846 + ],
1847 + "query": "result:denied AND !auth_device_name:null",
1848 + "refId": "A",
1849 + "timeField": "timestamp"
1850 + }
1851 + ],
1852 + "title": "AUTHS DENIED (AUTH DEVICE)",
1853 + "transformations": [
1854 + {
1855 + "id": "organize",
1856 + "options": {
1857 + "excludeByName": {},
1858 + "includeByName": {},
1859 + "indexByName": {},
1860 + "renameByName": {
1861 + "auth_device_name": "AUTH DEVICE NAME",
1862 + "data_access_device_hostname": "ACCESS DEVICE",
1863 + "data_auth_device_name": "AUTH DEVICE",
1864 + "data_sca_policy": "POLICY",
1865 + "data_user_name": "USER"
1866 + }
1867 + }
1868 + }
1869 + ],
1870 + "type": "table"
1871 + },
1872 + {
1873 + "datasource": {
1874 + "type": "grafana-opensearch-datasource",
1875 + "uid": "replace_datasource_uid"
1876 + },
1877 + "fieldConfig": {
1878 + "defaults": {
1879 + "color": {
1880 + "mode": "thresholds"
1881 + },
1882 + "custom": {
1883 + "align": "auto",
1884 + "cellOptions": {
1885 + "type": "auto"
1886 + },
1887 + "filterable": true,
1888 + "inspect": false
1889 + },
1890 + "mappings": [],
1891 + "thresholds": {
1892 + "mode": "absolute",
1893 + "steps": [
1894 + {
1895 + "color": "semi-dark-orange",
1896 + "value": null
1897 + }
1898 + ]
1899 + }
1900 + },
1901 + "overrides": [
1902 + {
1903 + "matcher": {
1904 + "id": "byName",
1905 + "options": "EVENT ID"
1906 + },
1907 + "properties": [
1908 + {
1909 + "id": "links",
1910 + "value": [
1911 + {
1912 + "title": "VIEW EVENT DETAILS",
1913 + "url": "https://grafana.company.local/explore?left=%7B%22datasource%22:%22DUO%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22query%22:%22_id:${__value.text}%22,%22alias%22:%22%22,%22metrics%22:%5B%7B%22id%22:%221%22,%22type%22:%22logs%22,%22settings%22:%7B%22limit%22:%22500%22%7D%7D%5D,%22bucketAggs%22:%5B%5D,%22timeField%22:%22timestamp%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D"
1914 + }
1915 + ]
1916 + }
1917 + ]
1918 + }
1919 + ]
1920 + },
1921 + "gridPos": {
1922 + "h": 16,
1923 + "w": 24,
1924 + "x": 0,
1925 + "y": 39
1926 + },
1927 + "id": 27,
1928 + "options": {
1929 + "cellHeight": "sm",
1930 + "footer": {
1931 + "countRows": false,
1932 + "enablePagination": true,
1933 + "fields": "",
1934 + "reducer": ["sum"],
1935 + "show": false
1936 + },
1937 + "showHeader": true,
1938 + "sortBy": []
1939 + },
1940 + "pluginVersion": "10.2.3",
1941 + "targets": [
1942 + {
1943 + "bucketAggs": [],
1944 + "datasource": {
1945 + "type": "grafana-opensearch-datasource",
1946 + "uid": "replace_datasource_uid"
1947 + },
1948 + "metrics": [
1949 + {
1950 + "id": "1",
1951 + "settings": {
1952 + "size": "500"
1953 + },
1954 + "type": "raw_data"
1955 + }
1956 + ],
1957 + "query": "",
1958 + "refId": "A",
1959 + "timeField": "timestamp"
1960 + }
1961 + ],
1962 + "title": "DUO EVENTS",
1963 + "transformations": [
1964 + {
1965 + "id": "filterFieldsByName",
1966 + "options": {
1967 + "include": {
1968 + "names": ["_id", "email", "result", "auth_device_location_city"]
1969 + }
1970 + }
1971 + },
1972 + {
1973 + "id": "organize",
1974 + "options": {
1975 + "excludeByName": {},
1976 + "includeByName": {},
1977 + "indexByName": {
1978 + "_id": 0,
1979 + "auth_device_location_city": 2,
1980 + "email": 1,
1981 + "result": 3
1982 + },
1983 + "renameByName": {
1984 + "_id": "EVENT ID",
1985 + "agent_ip": "AGENT IP",
1986 + "agent_name": "AGENT",
1987 + "auth_device_location_city": "DEVICE CITY",
1988 + "data_access_device_hostname": "ACCESS DEVICE",
1989 + "data_access_device_ip": "DEVICE IP",
1990 + "data_access_device_ip_country_code": "COUNTRY",
1991 + "data_application_name": "APP",
1992 + "data_auth_device_ip": "DEVICE IP",
1993 + "data_auth_device_name": "AUTH DEVICE",
1994 + "data_email": "EMAIL",
1995 + "data_event_type": "EVENT TYPE",
1996 + "data_reason": "REASON",
1997 + "data_result": "RESULT",
1998 + "data_sca_check_reason": "REASON",
1999 + "data_sca_check_remediation": "REMEDIATION",
2000 + "data_sca_check_result": "RESULT",
2001 + "data_sca_check_title": "CONTROL",
2002 + "data_sca_policy": "POLICY",
2003 + "data_sca_type": "",
2004 + "data_user_name": "USERNAME",
2005 + "email": "EMAIL",
2006 + "result": "RESULT",
2007 + "timestamp": "DATE/TIME"
2008 + }
2009 + }
2010 + }
2011 + ],
2012 + "transparent": true,
2013 + "type": "table"
2014 + }
2015 + ],
2016 + "refresh": "",
2017 + "schemaVersion": 39,
2018 + "tags": ["EDR"],
2019 + "templating": {
2020 + "list": [
2021 + {
2022 + "datasource": {
2023 + "type": "elasticsearch",
2024 + "uid": "duo_datasource_uid"
2025 + },
2026 + "filters": [],
2027 + "hide": 0,
2028 + "label": "",
2029 + "name": "Filters",
2030 + "skipUrlSync": false,
2031 + "type": "adhoc"
2032 + },
2033 + {
2034 + "current": {
2035 + "selected": false,
2036 + "text": "All",
2037 + "value": "$__all"
2038 + },
2039 + "datasource": {
2040 + "type": "elasticsearch",
2041 + "uid": "duo_datasource_uid"
2042 + },
2043 + "definition": "{ \"find\": \"terms\", \"field\": \"agent_name\", \"query\": \"rule_groups:rootcheck OR rule_groups:oscap OR rule_groups:sca\"}",
2044 + "hide": 0,
2045 + "includeAll": true,
2046 + "label": "Agent",
2047 + "multi": false,
2048 + "name": "agent_name",
2049 + "options": [],
2050 + "query": "{ \"find\": \"terms\", \"field\": \"agent_name\", \"query\": \"rule_groups:rootcheck OR rule_groups:oscap OR rule_groups:sca\"}",
2051 + "refresh": 2,
2052 + "regex": "",
2053 + "skipUrlSync": false,
2054 + "sort": 2,
2055 + "tagValuesQuery": "",
2056 + "tagsQuery": "",
2057 + "type": "query",
2058 + "useTags": false
2059 + }
2060 + ]
2061 + },
2062 + "time": {
2063 + "from": "now-24h",
2064 + "to": "now"
2065 + },
2066 + "timepicker": {
2067 + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
2068 + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
2069 + },
2070 + "timezone": "",
2071 + "title": "DUO AUTH LOGS",
2072 + "version": 9,
2073 + "weekStart": ""
2074 +}
backend/app/connectors/grafana/schema/dashboards.py
+5
@@ -97,6 +97,10 @@ class CrowdstrikeDashboard(Enum):
97 CROWDSTRIKE_SUMMARY = ("Crowdstrike", "summary.json")
98
99
100 +class DuoDashboard(Enum):
101 + DUO_AUTH = ("Duo", "duo_auth.json")
102 +
103 +
104 class DashboardProvisionRequest(BaseModel):
105 dashboards: List[str] = Field(
106 ...,
@@ -128,6 +132,7 @@ class DashboardProvisionRequest(BaseModel):
132 + list(CarbonBlackDashboard)
133 + list(FortinetDashboard)
134 + list(CrowdstrikeDashboard)
135 + + list(DuoDashboard)
136 }
137 if e not in valid_dashboards:
138 raise ValueError(f'Dashboard identifier "{e}" is not recognized.')
backend/app/connectors/grafana/services/dashboards.py
+2
@@ -7,6 +7,7 @@ from loguru import logger
7 from app.connectors.grafana.schema.dashboards import CarbonBlackDashboard
8 from app.connectors.grafana.schema.dashboards import CrowdstrikeDashboard
9 from app.connectors.grafana.schema.dashboards import DashboardProvisionRequest
10 +from app.connectors.grafana.schema.dashboards import DuoDashboard
11 from app.connectors.grafana.schema.dashboards import FortinetDashboard
12 from app.connectors.grafana.schema.dashboards import GrafanaDashboard
13 from app.connectors.grafana.schema.dashboards import GrafanaDashboardResponse
@@ -181,6 +182,7 @@ async def provision_dashboards(
182 + list(CarbonBlackDashboard)
183 + list(FortinetDashboard)
184 + list(CrowdstrikeDashboard)
185 + + list(DuoDashboard)
186 }
187
188 for dashboard_name in dashboard_request.dashboards:
backend/app/db/db_populate.py
+4
@@ -273,6 +273,7 @@ def get_available_integrations_list():
273 ("Huntress", "Integrate Huntress with SOCFortress."),
274 ("CarbonBlack", "Integrate CarbonBlack with SOCFortress."),
275 ("Crowdstrike", "Integrate Crowdstrike with SOCFortress."),
276 + ("DUO", "Integrate DUO with SOCFortress."),
277 # ... Add more available integrations as needed ...
278 ]
279
@@ -385,6 +386,9 @@ async def get_available_integrations_auth_keys_list(session: AsyncSession):
386 ("Crowdstrike", "CLIENT_SECRET"),
387 ("Crowdstrike", "BASE_URL"),
388 ("Crowdstrike", "SYSLOG_PORT"),
389 + ("DUO", "API_HOSTNAME"),
390 + ("DUO", "INTEGRATION_KEY"),
391 + ("DUO", "SECRET_KEY"),
392 # ... Add more available integrations auth keys as needed ...
393 ]
394 logger.info("Getting available integrations auth keys.")
backend/app/integrations/duo/routes/provision.py new
+51
@@ -0,0 +1,51 @@
1 +from fastapi import APIRouter
2 +from fastapi import Depends
3 +from sqlalchemy.ext.asyncio import AsyncSession
4 +
5 +from app.db.db_session import get_db
6 +from app.integrations.duo.schema.provision import ProvisionDuoRequest
7 +from app.integrations.duo.schema.provision import ProvisionDuoResponse
8 +from app.integrations.duo.services.provision import provision_duo
9 +from app.integrations.utils.utils import get_customer_integration_response
10 +from app.schedulers.models.scheduler import CreateSchedulerRequest
11 +from app.schedulers.scheduler import add_scheduler_jobs
12 +
13 +integration_duo_provision_router = APIRouter()
14 +
15 +
16 +@integration_duo_provision_router.post(
17 + "/provision",
18 + response_model=ProvisionDuoResponse,
19 + description="Provision a Duo integration.",
20 +)
21 +async def provision_duo_route(
22 + provision_duo_request: ProvisionDuoRequest,
23 + session: AsyncSession = Depends(get_db),
24 +) -> ProvisionDuoResponse:
25 + """
26 + Provisions a duo integration.
27 +
28 + Args:
29 + provision_duo_request (ProvisionDuoRequest): The request object containing the necessary data for provisioning.
30 + session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
31 +
32 + Returns:
33 + ProvisionDuoResponse: The response object indicating the success or failure of the provisioning process.
34 + """
35 + # Check if the customer integration settings are available and can be provisioned
36 + await get_customer_integration_response(
37 + provision_duo_request.customer_code,
38 + session,
39 + )
40 + await provision_duo(provision_duo_request, session)
41 + await add_scheduler_jobs(
42 + CreateSchedulerRequest(
43 + function_name="invoke_duo_integration_collect",
44 + time_interval=provision_duo_request.time_interval,
45 + job_id="invoke_duo_integration_collect",
46 + ),
47 + )
48 + return ProvisionDuoResponse(
49 + success=True,
50 + message="Duo integration provisioned successfully.",
51 + )
backend/app/integrations/duo/schema/provision.py new
+87
@@ -0,0 +1,87 @@
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 +
6 +from pydantic import BaseModel
7 +from pydantic import Field
8 +from pydantic import root_validator
9 +
10 +
11 +class ProvisionDuoRequest(BaseModel):
12 + customer_code: str = Field(
13 + ...,
14 + description="The customer code.",
15 + examples=["00002"],
16 + )
17 + time_interval: int = Field(
18 + 15,
19 + description="The time interval for the scheduler.",
20 + examples=[15],
21 + )
22 + integration_name: str = Field(
23 + "Duo",
24 + description="The integration name.",
25 + examples=["Duo"],
26 + )
27 +
28 + # ensure the `integration_name` is always set to "Mimecast"
29 + @root_validator(pre=True)
30 + def set_integration_name(cls, values: Dict[str, Any]) -> Dict[str, Any]:
31 + values["integration_name"] = "Duo"
32 + return values
33 +
34 +
35 +class ProvisionDuoResponse(BaseModel):
36 + success: bool
37 + message: str
38 +
39 +
40 +# ! STREAMS ! #
41 +class StreamRule(BaseModel):
42 + field: str
43 + type: int
44 + inverted: bool
45 + value: str
46 +
47 +
48 +class DuoEventStream(BaseModel):
49 + title: str = Field(..., description="Title of the stream")
50 + description: str = Field(..., description="Description of the stream")
51 + index_set_id: str = Field(..., description="ID of the associated index set")
52 + rules: List[StreamRule] = Field(..., description="List of rules for the stream")
53 + matching_type: str = Field(..., description="Matching type for the rules")
54 + remove_matches_from_default_stream: bool = Field(
55 + ...,
56 + description="Whether to remove matches from the default stream",
57 + )
58 + content_pack: Optional[str] = Field(
59 + None,
60 + description="Associated content pack, if any",
61 + )
62 +
63 + class Config:
64 + schema_extra = {
65 + "example": {
66 + "title": "Duo SIEM EVENTS - Example Company",
67 + "description": "Duo SIEM EVENTS - Example Company",
68 + "index_set_id": "12345",
69 + "rules": [
70 + {
71 + "field": "customer_code",
72 + "type": 1,
73 + "inverted": False,
74 + "value": "ExampleCode",
75 + },
76 + {
77 + "field": "integration",
78 + "type": 1,
79 + "inverted": False,
80 + "value": "huntress",
81 + },
82 + ],
83 + "matching_type": "AND",
84 + "remove_matches_from_default_stream": True,
85 + "content_pack": None,
86 + },
87 + }
backend/app/integrations/duo/services/provision.py new
+397
@@ -0,0 +1,397 @@
1 +import json
2 +from datetime import datetime
3 +
4 +from loguru import logger
5 +from sqlalchemy import and_
6 +from sqlalchemy import update
7 +from sqlalchemy.ext.asyncio import AsyncSession
8 +
9 +from app.connectors.grafana.schema.dashboards import DashboardProvisionRequest
10 +from app.connectors.grafana.schema.dashboards import DuoDashboard
11 +from app.connectors.grafana.services.dashboards import provision_dashboards
12 +from app.connectors.grafana.utils.universal import create_grafana_client
13 +from app.connectors.graylog.services.management import start_stream
14 +from app.connectors.graylog.utils.universal import send_post_request
15 +from app.connectors.wazuh_indexer.services.monitoring import (
16 + output_shard_number_to_be_set_based_on_nodes,
17 +)
18 +from app.customer_provisioning.schema.grafana import GrafanaDatasource
19 +from app.customer_provisioning.schema.grafana import GrafanaDataSourceCreationResponse
20 +from app.customer_provisioning.schema.graylog import GraylogIndexSetCreationResponse
21 +from app.customer_provisioning.schema.graylog import StreamCreationResponse
22 +from app.customer_provisioning.schema.graylog import TimeBasedIndexSet
23 +from app.customer_provisioning.services.grafana import create_grafana_folder
24 +from app.customer_provisioning.services.grafana import get_opensearch_version
25 +from app.customers.routes.customers import get_customer
26 +from app.customers.routes.customers import get_customer_meta
27 +from app.integrations.duo.schema.provision import DuoEventStream
28 +from app.integrations.duo.schema.provision import ProvisionDuoRequest
29 +from app.integrations.duo.schema.provision import ProvisionDuoResponse
30 +from app.integrations.models.customer_integration_settings import CustomerIntegrations
31 +from app.integrations.routes import create_integration_meta
32 +from app.integrations.schema import CustomerIntegrationsMetaSchema
33 +from app.utils import get_connector_attribute
34 +
35 +
36 +################## ! GRAYLOG ! ##################
37 +async def build_index_set_config(
38 + customer_code: str,
39 + session: AsyncSession,
40 +) -> TimeBasedIndexSet:
41 + """
42 + Build the configuration for a time-based index set.
43 +
44 + Args:
45 + request (ProvisionNewCustomer): The request object containing customer information.
46 +
47 + Returns:
48 + TimeBasedIndexSet: The configured time-based index set.
49 + """
50 + return TimeBasedIndexSet(
51 + title=f"{(await get_customer(customer_code, session)).customer.customer_name} - DUO",
52 + description=f"{customer_code} - DUO",
53 + index_prefix=f"duo-{customer_code}",
54 + rotation_strategy_class="org.graylog2.indexer.rotation.strategies.TimeBasedRotationStrategy",
55 + rotation_strategy={
56 + "type": "org.graylog2.indexer.rotation.strategies.TimeBasedRotationStrategyConfig",
57 + "rotation_period": "P1D",
58 + "rotate_empty_index_set": False,
59 + "max_rotation_period": None,
60 + },
61 + retention_strategy_class="org.graylog2.indexer.retention.strategies.DeletionRetentionStrategy",
62 + retention_strategy={
63 + "type": "org.graylog2.indexer.retention.strategies.DeletionRetentionStrategyConfig",
64 + "max_number_of_indices": 30,
65 + },
66 + creation_date=datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
67 + index_analyzer="standard",
68 + shards=await output_shard_number_to_be_set_based_on_nodes(),
69 + replicas=0,
70 + index_optimization_max_num_segments=1,
71 + index_optimization_disabled=False,
72 + writable=True,
73 + field_type_refresh_interval=5000,
74 + )
75 +
76 +
77 +# Function to send the POST request and handle the response
78 +async def send_index_set_creation_request(
79 + index_set: TimeBasedIndexSet,
80 +) -> GraylogIndexSetCreationResponse:
81 + """
82 + Sends a request to create an index set in Graylog.
83 +
84 + Args:
85 + index_set (TimeBasedIndexSet): The index set to be created.
86 +
87 + Returns:
88 + GraylogIndexSetCreationResponse: The response from Graylog after creating the index set.
89 + """
90 + json_index_set = json.dumps(index_set.dict())
91 + logger.info(f"json_index_set set: {json_index_set}")
92 + response_json = await send_post_request(
93 + endpoint="/api/system/indices/index_sets",
94 + data=index_set.dict(),
95 + )
96 + return GraylogIndexSetCreationResponse(**response_json)
97 +
98 +
99 +async def create_index_set(
100 + customer_code: str,
101 + session: AsyncSession,
102 +) -> GraylogIndexSetCreationResponse:
103 + """
104 + Creates an index set for a new customer.
105 +
106 + Args:
107 + request (ProvisionNewCustomer): The request object containing the customer information.
108 +
109 + Returns:
110 + GraylogIndexSetCreationResponse: The response object containing the result of the index set creation.
111 + """
112 + logger.info(f"Creating index set for customer {customer_code}")
113 + index_set_config = await build_index_set_config(customer_code, session)
114 + return await send_index_set_creation_request(index_set_config)
115 +
116 +
117 +# ! Event STREAMS ! #
118 +# Function to create event stream configuration
119 +async def build_event_stream_config(
120 + customer_code: str,
121 + index_set_id: str,
122 + session: AsyncSession,
123 +) -> DuoEventStream:
124 + """
125 + Builds the configuration for the Duo event stream.
126 +
127 + Args:
128 + customer_code (str): The customer code.
129 + index_set_id (str): The index set ID.
130 + session (AsyncSession): The async session.
131 +
132 + Returns:
133 + DuoEventStream: The configured Duo event stream.
134 + """
135 + return DuoEventStream(
136 + title=f"{(await get_customer(customer_code, session)).customer.customer_name} - DUO",
137 + description=f"{(await get_customer(customer_code, session)).customer.customer_name} - DUO",
138 + index_set_id=index_set_id,
139 + rules=[
140 + {
141 + "field": "integration",
142 + "type": 1,
143 + "inverted": False,
144 + "value": "duo",
145 + },
146 + {
147 + "field": "customer_code",
148 + "type": 1,
149 + "inverted": False,
150 + "value": f"{customer_code}",
151 + },
152 + ],
153 + matching_type="AND",
154 + remove_matches_from_default_stream=True,
155 + content_pack=None,
156 + )
157 +
158 +
159 +async def send_event_stream_creation_request(
160 + event_stream: DuoEventStream,
161 +) -> StreamCreationResponse:
162 + """
163 + Sends a request to create an event stream.
164 +
165 + Args:
166 + event_stream (SapSiemEventStream): The event stream to be created.
167 +
168 + Returns:
169 + StreamCreationResponse: The response containing the created event stream.
170 + """
171 + json_event_stream = json.dumps(event_stream.dict())
172 + logger.info(f"json_event_stream set: {json_event_stream}")
173 + response_json = await send_post_request(
174 + endpoint="/api/streams",
175 + data=event_stream.dict(),
176 + )
177 + return StreamCreationResponse(**response_json)
178 +
179 +
180 +async def create_event_stream(
181 + customer_code: str,
182 + index_set_id: str,
183 + session: AsyncSession,
184 +) -> StreamCreationResponse:
185 + """
186 + Creates an event stream for a customer.
187 +
188 + Args:
189 + request (ProvisionNewCustomer): The request object containing customer information.
190 + index_set_id (str): The ID of the index set.
191 +
192 + Returns:
193 + The result of the event stream creation request.
194 + """
195 + event_stream_config = await build_event_stream_config(
196 + customer_code,
197 + index_set_id,
198 + session,
199 + )
200 + return await send_event_stream_creation_request(event_stream_config)
201 +
202 +
203 +#### ! GRAFANA ! ####
204 +async def create_grafana_datasource(
205 + customer_code: str,
206 + session: AsyncSession,
207 +) -> GrafanaDataSourceCreationResponse:
208 + """
209 + Creates a Grafana datasource for the specified customer.
210 +
211 + Args:
212 + customer_code (str): The customer code.
213 + session (AsyncSession): The async session.
214 +
215 + Returns:
216 + GrafanaDataSourceCreationResponse: The response containing the created datasource details.
217 + """
218 + logger.info("Creating Grafana datasource")
219 + grafana_client = await create_grafana_client("Grafana")
220 + # Switch to the newly created organization
221 + grafana_client.user.switch_actual_user_organisation(
222 + (await get_customer_meta(customer_code, session)).customer_meta.customer_meta_grafana_org_id,
223 + )
224 + datasource_payload = GrafanaDatasource(
225 + name="DUO",
226 + type="grafana-opensearch-datasource",
227 + typeName="OpenSearch",
228 + access="proxy",
229 + url=await get_connector_attribute(
230 + connector_id=1,
231 + column_name="connector_url",
232 + session=session,
233 + ),
234 + database=f"duo-{customer_code}*",
235 + basicAuth=True,
236 + basicAuthUser=await get_connector_attribute(
237 + connector_id=1,
238 + column_name="connector_username",
239 + session=session,
240 + ),
241 + secureJsonData={
242 + "basicAuthPassword": await get_connector_attribute(
243 + connector_id=1,
244 + column_name="connector_password",
245 + session=session,
246 + ),
247 + },
248 + isDefault=False,
249 + jsonData={
250 + "database": f"duo-{customer_code}*",
251 + "flavor": "opensearch",
252 + "includeFrozen": False,
253 + "logLevelField": "severity",
254 + "logMessageField": "summary",
255 + "maxConcurrentShardRequests": 5,
256 + "pplEnabled": True,
257 + "timeField": "timestamp",
258 + "tlsSkipVerify": True,
259 + "version": await get_opensearch_version(),
260 + },
261 + readOnly=True,
262 + )
263 + results = grafana_client.datasource.create_datasource(
264 + datasource=datasource_payload.dict(),
265 + )
266 + return GrafanaDataSourceCreationResponse(**results)
267 +
268 +
269 +async def provision_duo(
270 + provision_duo_request: ProvisionDuoRequest,
271 + session: AsyncSession,
272 +) -> ProvisionDuoResponse:
273 + logger.info(
274 + f"Provisioning Duo integration for customer {provision_duo_request.customer_code}.",
275 + )
276 +
277 + # Create Index Set
278 + index_set_id = (
279 + await create_index_set(
280 + customer_code=provision_duo_request.customer_code,
281 + session=session,
282 + )
283 + ).data.id
284 + logger.info(f"Index set: {index_set_id}")
285 + # Create event stream
286 + stream_id = (
287 + await create_event_stream(
288 + provision_duo_request.customer_code,
289 + index_set_id,
290 + session,
291 + )
292 + ).data.stream_id
293 + # Start stream
294 + await start_stream(stream_id=stream_id)
295 +
296 + # Grafana Deployment
297 + duo_datasource_uid = (
298 + await create_grafana_datasource(
299 + customer_code=provision_duo_request.customer_code,
300 + session=session,
301 + )
302 + ).datasource.uid
303 + grafana_duo_folder_id = (
304 + await create_grafana_folder(
305 + organization_id=(
306 + await get_customer_meta(
307 + provision_duo_request.customer_code,
308 + session,
309 + )
310 + ).customer_meta.customer_meta_grafana_org_id,
311 + folder_title="DUO",
312 + )
313 + ).id
314 + await provision_dashboards(
315 + DashboardProvisionRequest(
316 + dashboards=[dashboard.name for dashboard in DuoDashboard],
317 + organizationId=(
318 + await get_customer_meta(
319 + provision_duo_request.customer_code,
320 + session,
321 + )
322 + ).customer_meta.customer_meta_grafana_org_id,
323 + folderId=grafana_duo_folder_id,
324 + datasourceUid=duo_datasource_uid,
325 + ),
326 + )
327 + await create_integration_meta_entry(
328 + CustomerIntegrationsMetaSchema(
329 + customer_code=provision_duo_request.customer_code,
330 + integration_name="Duo",
331 + graylog_input_id=None,
332 + graylog_index_id=index_set_id,
333 + graylog_stream_id=stream_id,
334 + grafana_org_id=(
335 + await get_customer_meta(
336 + provision_duo_request.customer_code,
337 + session,
338 + )
339 + ).customer_meta.customer_meta_grafana_org_id,
340 + grafana_dashboard_folder_id=grafana_duo_folder_id,
341 + ),
342 + session,
343 + )
344 + await update_customer_integration_table(
345 + provision_duo_request.customer_code,
346 + session,
347 + )
348 +
349 + return ProvisionDuoResponse(
350 + success=True,
351 + message="Duo integration provisioned successfully.",
352 + )
353 +
354 +
355 +############## ! WRITE TO DB ! ##############
356 +async def create_integration_meta_entry(
357 + customer_integration_meta: CustomerIntegrationsMetaSchema,
358 + session: AsyncSession,
359 +) -> None:
360 + """
361 + Creates an entry for the customer integration meta in the database.
362 +
363 + Args:
364 + customer_integration_meta (CustomerIntegrationsMetaSchema): The customer integration meta object.
365 + session (AsyncSession): The async session object for database operations.
366 + """
367 + await create_integration_meta(customer_integration_meta, session)
368 + logger.info(
369 + f"Integration meta entry created for customer {customer_integration_meta.customer_code}.",
370 + )
371 +
372 +
373 +async def update_customer_integration_table(
374 + customer_code: str,
375 + session: AsyncSession,
376 +) -> None:
377 + """
378 + Updates the `customer_integrations` table to set the `deployed` column to True where the `customer_code`
379 + matches the given customer code and the `integration_service_name` is "Duo".
380 +
381 + Args:
382 + customer_code (str): The customer code.
383 + session (AsyncSession): The async session object for making HTTP requests.
384 + """
385 + await session.execute(
386 + update(CustomerIntegrations)
387 + .where(
388 + and_(
389 + CustomerIntegrations.customer_code == customer_code,
390 + CustomerIntegrations.integration_service_name == "Duo",
391 + ),
392 + )
393 + .values(deployed=True),
394 + )
395 + await session.commit()
396 +
397 + return None
backend/app/integrations/markdown/duo.md new
+44
@@ -0,0 +1,44 @@
1 +# [DUO Integration](https://duo.com/docs/adminapi#overview)
2 +
3 +# Duo Admin API
4 +
5 +CoPilot and DUO. Ingest DUO auth logs into your SIEM stack
6 +
7 +## Overview
8 +
9 +CoPilot's integration with DUO allows for administrators to collect and analyze Duo authentication logs seamlessly. By integrating the Duo Admin API with CoPilot, you can enhance your security monitoring and incident response capabilities within your SIEM stack.
10 +
11 +### Key Features
12 +
13 +- **Automated Log Collection:** Automatically ingest Duo authentication logs into your SIEM for real-time analysis.
14 +- **Comprehensive Monitoring:** Monitor Duo-related activities, including user logins, telephony logs, and administrator actions.
15 +- **Custom Alerts:** Set up custom alerts based on Duo log data to quickly identify and respond to potential security incidents.
16 +- **Detailed Reports:** Generate detailed reports on Duo authentication events, helping you to meet compliance requirements and improve security posture.
17 +
18 +## First Steps
19 +
20 +**Role required: Owner**
21 +
22 +Note that only administrators with the Owner role can create or modify an Admin API application in the Duo Admin Panel.
23 +
24 +1. Sign up for a Duo account.
25 +2. Log in to the Duo Admin Panel and navigate to Applications.
26 +3. Click **Protect an Application** and locate the entry for Admin API in the applications list. Click **Protect** to the far-right to configure the application and get your integration key, secret key, and API hostname. You'll need this information to complete your setup. See [Protecting Applications](https://duo.com/docs/protecting-applications) for more information about protecting applications in Duo and additional application options.
27 +
28 +### Treat your secret key like a password
29 +
30 +The security of your Duo application is tied to the security of your secret key (skey). Secure it as you would any sensitive credential. Don't share it with unauthorized individuals or email it to anyone under any circumstances!
31 +
32 +Determine the permissions you want to grant to this Admin API application. Refer to the API endpoint descriptions throughout this document for information about required permissions for operations.
33 +
34 +### Permission Details
35 +
36 +| Permission | Details |
37 +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
38 +| Grant administrators | The Admin API application can read information about, create, update, and delete Duo administrators and administrative units. |
39 +| Grant read information | The Admin API application can read information about the Duo customer account's utilization. |
40 +| Grant applications | The Admin API application can add, modify, and delete applications (referred to as "integrations" in the API), including permissions on itself or other Admin API applications. |
41 +| Grant settings | The Admin API application can read and change global Duo account settings. |
42 +| Grant read log | The Admin API application can read authentication, offline access, telephony, and administrator action log information. |
43 +| Grant read resource | The Admin API application can read information about resource objects such as end users and devices. |
44 +| Grant write resource | The Admin API application can create, update, and delete resource objects such as end users and devices. |
backend/app/integrations/modules/routes/duo.py new
+94
@@ -0,0 +1,94 @@
1 +from fastapi import APIRouter
2 +from fastapi import Depends
3 +from loguru import logger
4 +from sqlalchemy.ext.asyncio import AsyncSession
5 +
6 +from app.db.db_session import get_db
7 +from app.integrations.modules.schema.duo import CollectDuo
8 +from app.integrations.modules.schema.duo import DuoAuthKeys
9 +from app.integrations.modules.schema.duo import InvokeDuoRequest
10 +from app.integrations.modules.schema.duo import InvokeDuoResponse
11 +from app.integrations.modules.services.duo import post_to_copilot_duo_module
12 +from app.integrations.routes import find_customer_integration
13 +from app.integrations.utils.utils import extract_auth_keys
14 +from app.integrations.utils.utils import get_customer_integration_response
15 +from app.utils import get_connector_attribute
16 +
17 +module_duo_router = APIRouter()
18 +
19 +
20 +async def get_duo_auth_keys(customer_integration) -> DuoAuthKeys:
21 + """
22 + Extract the Duo authentication keys from the CustomerIntegration.
23 +
24 + Args:
25 + customer_integration (CustomerIntegration): The CustomerIntegration containing the
26 + Duo authentication keys.
27 +
28 + Returns:
29 + DuoAuthKeys: The extracted Duo authentication keys.
30 + """
31 + duo_auth_keys = extract_auth_keys(
32 + customer_integration,
33 + service_name="DUO",
34 + )
35 +
36 + return DuoAuthKeys(**duo_auth_keys)
37 +
38 +
39 +async def get_collect_duo_data(duo_request, session, auth_keys):
40 + return CollectDuo(
41 + integration="duo",
42 + customer_code=duo_request.customer_code,
43 + graylog_host=await get_connector_attribute(
44 + connector_id=14,
45 + column_name="connector_url",
46 + session=session,
47 + ),
48 + graylog_port=await get_connector_attribute(
49 + connector_id=14,
50 + column_name="connector_extra_data",
51 + session=session,
52 + ),
53 + integration_key=auth_keys.INTEGRATION_KEY,
54 + secret_key=auth_keys.SECRET_KEY,
55 + api_host=auth_keys.API_HOSTNAME,
56 + api_endpoint="/admin/v2/logs/authentication",
57 + range="15m",
58 + )
59 +
60 +
61 +@module_duo_router.post(
62 + "",
63 + response_model=InvokeDuoResponse,
64 + description="Invoke the Duo module.",
65 +)
66 +async def collect_duo_route(duo_request: InvokeDuoRequest, session: AsyncSession = Depends(get_db)):
67 + """Pull down Duo Events."""
68 + logger.info(f"Invoke Duo Request: {duo_request}")
69 + try:
70 + customer_integration_response = await get_customer_integration_response(
71 + duo_request.customer_code,
72 + session,
73 + )
74 +
75 + # ! SWITCH TO CAPS DUE TO HOW THAT IS STORED IN DB ! #
76 + duo_request.integration_name = "DUO"
77 +
78 + customer_integration = await find_customer_integration(
79 + duo_request.customer_code,
80 + duo_request.integration_name,
81 + customer_integration_response,
82 + )
83 +
84 + auth_keys = await get_duo_auth_keys(customer_integration)
85 +
86 + collect_duo_data = await get_collect_duo_data(duo_request, session, auth_keys)
87 +
88 + await post_to_copilot_duo_module(data=collect_duo_data)
89 +
90 + except Exception as e:
91 + logger.error(f"Error during DB session: {str(e)}")
92 + return InvokeDuoResponse(success=False, message=str(e))
93 +
94 + return InvokeDuoResponse(success=True, message="Duo Events collected successfully.")
backend/app/integrations/modules/schema/duo.py new
+78
@@ -0,0 +1,78 @@
1 +from fastapi import HTTPException
2 +from pydantic import BaseModel
3 +from pydantic import Field
4 +from pydantic import validator
5 +
6 +
7 +class InvokeDuoRequest(BaseModel):
8 + customer_code: str = Field(
9 + ...,
10 + description="The customer code.",
11 + examples=["00002"],
12 + )
13 + integration_name: str = Field(
14 + "Duo",
15 + description="The integration name.",
16 + examples=["Duo"],
17 + )
18 +
19 +
20 +class DuoAuthKeys(BaseModel):
21 + API_HOSTNAME: str = Field(
22 + ...,
23 + description="The API key.",
24 + examples=["123456"],
25 + )
26 + INTEGRATION_KEY: str = Field(
27 + ...,
28 + description="The integration key.",
29 + examples=["123456"],
30 + )
31 + SECRET_KEY: str = Field(
32 + ...,
33 + description="The secret key.",
34 + examples=["123456"],
35 + )
36 +
37 +
38 +class InvokeDuoResponse(BaseModel):
39 + success: bool = Field(
40 + ...,
41 + description="The success status.",
42 + examples=[True],
43 + )
44 + message: str = Field(
45 + ...,
46 + description="The message.",
47 + examples=["Duo Events collected successfully."],
48 + )
49 +
50 +
51 +class CollectDuo(BaseModel):
52 + integration: str = Field(..., example="duo")
53 + customer_code: str = Field(..., example="socfortress")
54 + integration_key: str = Field(..., example="1234567890")
55 + secret_key: str = Field(..., example="1234567890")
56 + api_host: str = Field(..., example="api-1234567890.duosecurity.com")
57 + api_endpoint: str = Field(..., example="/admin/v2/logs/authentication")
58 + graylog_host: str = Field(..., example="127.0.0.1")
59 + graylog_port: str = Field(..., example=12201)
60 + range: str = Field(..., example="15m") # New field for range
61 +
62 + @validator("integration")
63 + def check_integration(cls, v):
64 + if v != "duo":
65 + raise HTTPException(
66 + status_code=400,
67 + detail="Invalid integration. Only 'duo' is supported.",
68 + )
69 + return v
70 +
71 + @validator("range")
72 + def validate_range(cls, v):
73 + if not v.endswith(("m", "h", "d")):
74 + raise HTTPException(
75 + status_code=400,
76 + detail="Invalid range. Use 'm' for minutes, 'h' for hours, or 'd' for days.",
77 + )
78 + return v
backend/app/integrations/modules/services/duo.py new
+21
@@ -0,0 +1,21 @@
1 +import httpx
2 +from loguru import logger
3 +
4 +from app.integrations.modules.schema.duo import CollectDuo
5 +
6 +
7 +async def post_to_copilot_duo_module(data: CollectDuo):
8 + """
9 + Send a POST request to the copilot-duo-module Docker container.
10 +
11 + Args:
12 + data (CollectDuo): The data to send to the copilot-duo-module Docker container.
13 + """
14 + logger.info(f"Sending POST request to http://copilot-duo-module/auth with data: {data.dict()}")
15 + async with httpx.AsyncClient() as client:
16 + await client.post(
17 + "http://copilot-duo-module/auth",
18 + json=data.dict(),
19 + timeout=120,
20 + )
21 + return None
backend/app/routers/duo.py new
+13
@@ -0,0 +1,13 @@
1 +from fastapi import APIRouter
2 +
3 +from app.integrations.duo.routes.provision import integration_duo_provision_router
4 +
5 +# Instantiate the APIRouter
6 +router = APIRouter()
7 +
8 +# Include the Duo Provision APIRouter
9 +router.include_router(
10 + integration_duo_provision_router,
11 + prefix="/duo",
12 + tags=["duo"],
13 +)
backend/app/routers/modules.py
+7
@@ -1,5 +1,6 @@
1 from fastapi import APIRouter
2
3 +from app.integrations.modules.routes.duo import module_duo_router
4 from app.integrations.modules.routes.huntress import module_huntress_router
5 from app.integrations.modules.routes.mimecast import module_mimecast_router
6 from app.integrations.modules.routes.sap_siem import module_sap_siem_router
@@ -23,3 +24,9 @@ router.include_router(
24 prefix="/integrations/modules/sap_siem",
25 tags=["SAP SIEM"],
26 )
27 +
28 +router.include_router(
29 + module_duo_router,
30 + prefix="/integrations/modules/duo",
31 + tags=["DUO"],
32 +)
backend/app/schedulers/scheduler.py
+2
@@ -17,6 +17,7 @@ from app.schedulers.services.agent_sync import agent_sync
17 from app.schedulers.services.invoke_carbonblack import (
18 invoke_carbonblack_integration_collect,
19 )
20 +from app.schedulers.services.invoke_duo import invoke_duo_integration_collect
21 from app.schedulers.services.invoke_huntress import invoke_huntress_integration_collect
22 from app.schedulers.services.invoke_mimecast import invoke_mimecast_integration
23 from app.schedulers.services.invoke_mimecast import invoke_mimecast_integration_ttp
@@ -209,6 +210,7 @@ def get_function_by_name(function_name: str):
210 "invoke_sap_siem_integration_brute_force_failed_logins_same_ip": invoke_sap_siem_integration_brute_force_failed_logins_same_ip,
211 "invoke_sap_siem_integration_successful_login_after_multiple_failed_logins": invoke_sap_siem_integration_successful_login_after_multiple_failed_logins,
212 "invoke_huntress_integration_collection": invoke_huntress_integration_collect,
213 + "invoke_duo_integration_collect": invoke_duo_integration_collect,
214 "invoke_carbonblack_integration_collection": invoke_carbonblack_integration_collect,
215 # Add other function mappings here
216 }
backend/app/schedulers/services/invoke_duo.py new
+52
@@ -0,0 +1,52 @@
1 +from datetime import datetime
2 +
3 +from dotenv import load_dotenv
4 +from loguru import logger
5 +from sqlalchemy import select
6 +
7 +from app.db.db_session import get_db_session
8 +from app.db.db_session import get_sync_db_session
9 +from app.integrations.models.customer_integration_settings import CustomerIntegrations
10 +from app.integrations.modules.routes.duo import collect_duo_route
11 +from app.integrations.modules.schema.duo import InvokeDuoRequest
12 +from app.integrations.modules.schema.duo import InvokeDuoResponse
13 +from app.schedulers.models.scheduler import JobMetadata
14 +
15 +load_dotenv()
16 +
17 +
18 +async def invoke_duo_integration_collect() -> InvokeDuoResponse:
19 + """
20 + Invokes the Duo integration collection.
21 + """
22 + logger.info("Invoking Duo integration collection.")
23 + customer_codes = []
24 + async with get_db_session() as session:
25 + stmt = select(CustomerIntegrations).where(
26 + CustomerIntegrations.integration_service_name == "DUO",
27 + )
28 + result = await session.execute(stmt)
29 + customer_codes = [row.customer_code for row in result.scalars()]
30 + logger.info(f"customer_codes: {customer_codes}")
31 + for customer_code in customer_codes:
32 + await collect_duo_route(
33 + InvokeDuoRequest(
34 + customer_code=customer_code,
35 + integration_name="Duo",
36 + ),
37 + session,
38 + )
39 + # Close the session
40 + await session.close()
41 + with get_sync_db_session() as session:
42 + # Synchronous ORM operations
43 + job_metadata = session.query(JobMetadata).filter_by(job_id="invoke_duo_integration_collection").one_or_none()
44 + if job_metadata:
45 + job_metadata.last_success = datetime.utcnow()
46 + session.add(job_metadata)
47 + session.commit()
48 + else:
49 + # Handle the case where job_metadata does not exist
50 + print("JobMetadata for 'invoke_duo_integration_collection' not found.")
51 +
52 + return InvokeDuoResponse(success=True, message="Duo integration invoked.")
backend/copilot.py
+2
@@ -41,6 +41,7 @@ from app.routers import customer_provisioning
41 from app.routers import customers
42 from app.routers import dfir_iris
43 from app.routers import dnstwist
44 +from app.routers import duo
45 from app.routers import grafana
46 from app.routers import graylog
47 from app.routers import healthcheck
@@ -146,6 +147,7 @@ api_router.include_router(network_connectors.router)
147 api_router.include_router(crowdstrike.router)
148 api_router.include_router(scoutsuite.router)
149 api_router.include_router(nuclei.router)
150 +api_router.include_router(duo.router)
151
152 # Include the APIRouter in the FastAPI app
153 app.include_router(api_router)
frontend/src/api/integrations.ts
+9 -2
@@ -48,6 +48,12 @@ export default {
48 })
49 },
50
51 + office365Provision(customerCode: string, integrationName: string) {
52 + return HttpClient.post<FlaskBaseResponse>(`/office365/provision`, {
53 + customer_code: customerCode,
54 + integration_name: integrationName
55 + })
56 + },
57 mimecastProvision(customerCode: string, integrationName: string) {
58 return HttpClient.post<FlaskBaseResponse>(`/mimecast/provision`, {
59 customer_code: customerCode,
@@ -60,9 +66,10 @@ export default {
66 integration_name: integrationName
67 })
68 },
63 - office365Provision(customerCode: string, integrationName: string) {
64 - return HttpClient.post<FlaskBaseResponse>(`/office365/provision`, {
69 + duoProvision(customerCode: string, integrationName: string) {
70 + return HttpClient.post<FlaskBaseResponse>(`/duo/provision`, {
71 customer_code: customerCode,
72 + time_interval: 15,
73 integration_name: integrationName
74 })
75 }
frontend/src/components/customers/integrations/CustomerIntegrationActions.vue
+37 -1
@@ -36,6 +36,18 @@
36 Deploy
37 </n-button>
38
39 + <n-button
40 + v-if="isDuo && !integration.deployed"
41 + :loading="loadingDuoProvision"
42 + @click="duoProvision()"
43 + type="success"
44 + :size="size"
45 + secondary
46 + >
47 + <template #icon><Icon :name="DeployIcon"></Icon></template>
48 + Deploy
49 + </n-button>
50 +
51 <n-button
52 :size="size"
53 type="error"
@@ -82,12 +94,14 @@ const message = useMessage()
94 const loadingOffice365Provision = ref(false)
95 const loadingMimecastProvision = ref(false)
96 const loadingCrowdstrikeProvision = ref(false)
97 +const loadingDuoProvision = ref(false)
98 const loadingDelete = ref(false)
99 const loading = computed(
100 () =>
101 + loadingOffice365Provision.value ||
102 loadingMimecastProvision.value ||
103 loadingCrowdstrikeProvision.value ||
90 - loadingOffice365Provision.value ||
104 + loadingDuoProvision.value ||
105 loadingDelete.value
106 )
107
@@ -96,6 +110,7 @@ const customerCode = computed(() => integration.customer_code)
110 const isOffice365 = computed(() => serviceName.value === "Office365")
111 const isMimecast = computed(() => serviceName.value === "Mimecast")
112 const isCrowdstrike = computed(() => serviceName.value === "Crowdstrike")
113 +const isDuo = computed(() => serviceName.value === "DUO")
114
115 watch(loading, val => {
116 if (val) {
@@ -168,6 +183,27 @@ function crowdstrikeProvision() {
183 })
184 }
185
186 +function duoProvision() {
187 + loadingDuoProvision.value = true
188 +
189 + Api.integrations
190 + .duoProvision(customerCode.value, serviceName.value)
191 + .then(res => {
192 + if (res.data.success) {
193 + emit("deployed")
194 + message.success(res.data?.message || "Customer integration successfully deployed.")
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 + loadingDuoProvision.value = false
204 + })
205 +}
206 +
207 function handleDelete() {
208 dialog.warning({
209 title: "Confirm",
frontend/src/layouts/common/Toolbar/PinnedPages.vue
+6 -3
@@ -36,6 +36,7 @@
36 </div>
37 </n-tag>
38 </TransitionGroup>
39 +
40 <div class="bar"></div>
41 </div>
42 </template>
@@ -110,7 +111,8 @@ router.afterEach(route => {
111 <style lang="scss" scoped>
112 .pinned-pages {
113 position: relative;
113 - overflow-x: clip;
114 + overflow: hidden;
115 + padding: 8px 0;
116
117 :deep() {
118 .n-tag {
@@ -155,7 +157,7 @@ router.afterEach(route => {
157 .bar {
158 background-color: var(--bg-sidebar);
159 position: absolute;
158 - bottom: -7px;
160 + bottom: 0px;
161 border-radius: 6px;
162 left: 0;
163 width: 100%;
@@ -165,8 +167,9 @@ router.afterEach(route => {
167 .divider {
168 height: 8px;
169 width: 8px;
170 + min-width: 8px;
171 position: relative;
169 - top: 9px;
172 + top: 10px;
173 z-index: 1;
174 border-radius: 50%;
175 border: 2px solid var(--bg-body);
frontend/src/layouts/common/Toolbar/index.vue
+1 -1
@@ -102,7 +102,7 @@ onMounted(() => {
102 gap: 14px;
103 }
104
105 - @media (max-width: 950px) {
105 + @media (max-width: 1100px) {
106 .pinned-pages {
107 display: none;
108 }