diff --git a/.vscode/settings.json b/.vscode/settings.json index e2eb7124a..07229a86b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,7 @@ "colord", "commonmark", "creationdate", + "Crowdstrike", "datejs", "datetimesec", "DFIR", diff --git a/backend/app/connectors/grafana/dashboards/Duo/duo_auth.json b/backend/app/connectors/grafana/dashboards/Duo/duo_auth.json new file mode 100644 index 000000000..9a9e4bdf0 --- /dev/null +++ b/backend/app/connectors/grafana/dashboards/Duo/duo_auth.json @@ -0,0 +1,2074 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": false, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [ + { + "asDropdown": true, + "icon": "external link", + "includeVars": true, + "keepTime": true, + "tags": ["EDR"], + "targetBlank": true, + "title": "", + "type": "dashboards" + } + ], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 4, + "x": 0, + "y": 0 + }, + "id": 43, + "links": [], + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": ["sum"], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.2.3", + "targets": [ + { + "bucketAggs": [ + { + "$$hashKey": "object:183", + "field": "timestamp", + "id": "2", + "settings": { + "interval": "auto", + "min_doc_count": 0, + "trimEdges": 0 + }, + "type": "date_histogram" + } + ], + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "metrics": [ + { + "$$hashKey": "object:181", + "field": "select field", + "id": "1", + "type": "count" + } + ], + "query": "", + "refId": "A", + "timeField": "timestamp" + } + ], + "title": "DUO EVENTS", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Count" + }, + "properties": [ + { + "id": "displayName", + "value": "EVENTS" + }, + { + "id": "unit", + "value": "short" + }, + { + "id": "decimals", + "value": -1 + }, + { + "id": "custom.align" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "USER NAME" + }, + "properties": [ + { + "id": "custom.width", + "value": 388 + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 4, + "y": 0 + }, + "id": 31, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "10.2.3", + "targets": [ + { + "bucketAggs": [ + { + "$$hashKey": "object:65", + "fake": true, + "field": "user_name", + "id": "4", + "settings": { + "min_doc_count": 1, + "order": "desc", + "orderBy": "_count", + "size": "0" + }, + "type": "terms" + } + ], + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "metrics": [ + { + "$$hashKey": "object:63", + "field": "select field", + "id": "1", + "type": "count" + } + ], + "query": "", + "refId": "A", + "timeField": "timestamp" + } + ], + "title": "EVENTS BY ACCOUNT", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "data_user_name": "USER", + "user_name": "USER NAME" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-orange", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Count" + }, + "properties": [ + { + "id": "displayName", + "value": "EVENTS" + }, + { + "id": "unit", + "value": "short" + }, + { + "id": "decimals", + "value": -1 + }, + { + "id": "custom.align" + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 0 + }, + { + "color": "#FA6400", + "value": 1 + } + ] + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "factor" + }, + "properties": [ + { + "id": "displayName", + "value": "EVENTS BY TYPE" + }, + { + "id": "unit", + "value": "short" + }, + { + "id": "decimals", + "value": -1 + }, + { + "id": "custom.align" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "ALERTS BY TYPE" + }, + "properties": [ + { + "id": "custom.width", + "value": 717 + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 44, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "10.2.3", + "targets": [ + { + "bucketAggs": [ + { + "$$hashKey": "object:206", + "fake": true, + "field": "factor", + "id": "4", + "settings": { + "min_doc_count": 1, + "order": "desc", + "orderBy": "_term", + "size": "0" + }, + "type": "terms" + } + ], + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "metrics": [ + { + "$$hashKey": "object:204", + "field": "select field", + "id": "1", + "type": "count" + } + ], + "query": "", + "refId": "A", + "timeField": "timestamp" + } + ], + "title": "EVENTS BY TYPE", + "transformations": [], + "type": "table" + }, + { + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "displayName", + "value": "Time" + }, + { + "id": "unit", + "value": "time: YYYY-MM-DD HH:mm:ss" + }, + { + "id": "custom.align" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Count" + }, + "properties": [ + { + "id": "displayName", + "value": "EVENTS" + }, + { + "id": "unit", + "value": "short" + }, + { + "id": "decimals", + "value": -1 + }, + { + "id": "custom.align" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "ACCESS DEVICE HOSTNAME" + }, + "properties": [ + { + "id": "custom.width", + "value": 388 + } + ] + } + ] + }, + "gridPos": { + "h": 20, + "w": 6, + "x": 0, + "y": 7 + }, + "id": 50, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "10.2.3", + "targets": [ + { + "bucketAggs": [ + { + "$$hashKey": "object:65", + "fake": true, + "field": "access_device_hostname", + "id": "4", + "settings": { + "min_doc_count": 1, + "order": "desc", + "orderBy": "_count", + "size": "0" + }, + "type": "terms" + } + ], + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "metrics": [ + { + "$$hashKey": "object:63", + "field": "select field", + "id": "1", + "type": "count" + } + ], + "query": "!access_device_hostname:null", + "refId": "A", + "timeField": "timestamp" + } + ], + "title": "EVENTS BY ACCESS DEVICE", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "access_device_hostname": "ACCESS DEVICE HOSTNAME", + "data_access_device_hostname": "ACCESS DEVICE", + "data_user_name": "USER" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 18, + "x": 6, + "y": 7 + }, + "id": 49, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "alias": "", + "bucketAggs": [ + { + "field": "user_name", + "id": "2", + "settings": { + "min_doc_count": "1", + "order": "desc", + "orderBy": "_count", + "size": "10" + }, + "type": "terms" + }, + { + "field": "timestamp", + "id": "3", + "settings": { + "interval": "10m", + "min_doc_count": "0", + "timeZone": "utc", + "trimEdges": "0" + }, + "type": "date_histogram" + } + ], + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "metrics": [ + { + "id": "1", + "type": "count" + } + ], + "query": "", + "refId": "A", + "timeField": "timestamp" + } + ], + "title": "TOP 10 ACCOUNTS - HISTOGRAM", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(245, 54, 54, 0.9)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 0 + }, + { + "color": "rgba(50, 172, 45, 0.97)", + "value": 10 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 7, + "x": 6, + "y": 19 + }, + "id": 53, + "maxDataPoints": 1, + "options": { + "basemap": { + "name": "Basemap", + "type": "default" + }, + "controls": { + "mouseWheelZoom": false, + "showAttribution": true, + "showDebug": false, + "showMeasure": false, + "showScale": false, + "showZoom": true + }, + "layers": [ + { + "config": { + "showLegend": true, + "style": { + "color": { + "fixed": "dark-green" + }, + "opacity": 0.4, + "rotation": { + "fixed": 0, + "max": 360, + "min": -360, + "mode": "mod" + }, + "size": { + "fixed": 5, + "max": 30, + "min": 2 + }, + "symbol": { + "fixed": "img/icons/marker/circle.svg", + "mode": "fixed" + }, + "symbolAlign": { + "horizontal": "center", + "vertical": "center" + }, + "textConfig": { + "fontSize": 12, + "offsetX": 0, + "offsetY": 0, + "textAlign": "center", + "textBaseline": "middle" + } + } + }, + "location": { + "gazetteer": "public/gazetteer/countries.json", + "mode": "lookup" + }, + "name": "Layer 0", + "tooltip": true, + "type": "markers" + } + ], + "tooltip": { + "mode": "details" + }, + "view": { + "allLayers": true, + "id": "zero", + "lat": 0, + "lon": 0, + "zoom": 1 + } + }, + "pluginVersion": "10.2.3", + "targets": [ + { + "alias": "", + "bucketAggs": [ + { + "field": "access_device_location_country", + "id": "3", + "settings": { + "min_doc_count": "1", + "order": "desc", + "orderBy": "_count", + "size": "0" + }, + "type": "terms" + }, + { + "field": "timestamp", + "id": "2", + "settings": { + "interval": "auto" + }, + "type": "date_histogram" + } + ], + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "metrics": [ + { + "id": "1", + "type": "count" + } + ], + "query": "", + "refId": "A", + "timeField": "timestamp" + } + ], + "title": "ACCESS DEVICE GeoIP", + "transformations": [ + { + "id": "reduce", + "options": { + "reducers": ["sum"] + } + } + ], + "type": "geomap" + }, + { + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Count" + }, + "properties": [ + { + "id": "displayName", + "value": "EVENTS" + }, + { + "id": "unit", + "value": "short" + }, + { + "id": "decimals", + "value": -1 + }, + { + "id": "custom.align" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "AUTH DEVICE NAME" + }, + "properties": [ + { + "id": "custom.width", + "value": 388 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 5, + "x": 13, + "y": 19 + }, + "id": 51, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "10.2.3", + "targets": [ + { + "bucketAggs": [ + { + "$$hashKey": "object:65", + "fake": true, + "field": "auth_device_name", + "id": "4", + "settings": { + "min_doc_count": 1, + "order": "desc", + "orderBy": "_count", + "size": "0" + }, + "type": "terms" + } + ], + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "metrics": [ + { + "$$hashKey": "object:63", + "field": "select field", + "id": "1", + "type": "count" + } + ], + "query": "!auth_device_name:null", + "refId": "A", + "timeField": "timestamp" + } + ], + "title": "EVENTS BY AUTH DEVICE", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "auth_device_name": "AUTH DEVICE NAME", + "data_access_device_hostname": "ACCESS DEVICE", + "data_auth_device_name": "AUTH DEVICE", + "data_user_name": "USER" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(245, 54, 54, 0.9)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 0 + }, + { + "color": "rgba(50, 172, 45, 0.97)", + "value": 10 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 18, + "y": 19 + }, + "id": 54, + "maxDataPoints": 1, + "options": { + "basemap": { + "name": "Basemap", + "type": "default" + }, + "controls": { + "mouseWheelZoom": false, + "showAttribution": true, + "showDebug": false, + "showMeasure": false, + "showScale": false, + "showZoom": true + }, + "layers": [ + { + "config": { + "showLegend": true, + "style": { + "color": { + "fixed": "dark-green" + }, + "opacity": 0.4, + "rotation": { + "fixed": 0, + "max": 360, + "min": -360, + "mode": "mod" + }, + "size": { + "fixed": 5, + "max": 30, + "min": 2 + }, + "symbol": { + "fixed": "img/icons/marker/circle.svg", + "mode": "fixed" + }, + "symbolAlign": { + "horizontal": "center", + "vertical": "center" + }, + "textConfig": { + "fontSize": 12, + "offsetX": 0, + "offsetY": 0, + "textAlign": "center", + "textBaseline": "middle" + } + } + }, + "location": { + "gazetteer": "public/gazetteer/countries.json", + "mode": "lookup" + }, + "name": "Layer 0", + "tooltip": true, + "type": "markers" + } + ], + "tooltip": { + "mode": "details" + }, + "view": { + "allLayers": true, + "id": "zero", + "lat": 0, + "lon": 0, + "zoom": 1 + } + }, + "pluginVersion": "10.2.3", + "targets": [ + { + "alias": "", + "bucketAggs": [ + { + "field": "auth_device_location_country", + "id": "3", + "settings": { + "min_doc_count": "1", + "order": "desc", + "orderBy": "_count", + "size": "0" + }, + "type": "terms" + }, + { + "field": "timestamp", + "id": "2", + "settings": { + "interval": "auto" + }, + "type": "date_histogram" + } + ], + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "metrics": [ + { + "id": "1", + "type": "count" + } + ], + "query": "", + "refId": "A", + "timeField": "timestamp" + } + ], + "title": "AUTH DEVICE GeoIP", + "transformations": [ + { + "id": "reduce", + "options": { + "reducers": ["sum"] + } + } + ], + "type": "geomap" + }, + { + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "decimals": 0, + "mappings": [], + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "1" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C8F2C2", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "success" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#96D98D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "denied" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#56A64B", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "4" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#37872D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "5" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FFF899", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "7" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F2CC0C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "9" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0B400", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "10" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FFCB7D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "12" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FFA6B0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "13" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FF7383", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "N/A" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "semi-dark-orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "failed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "semi-dark-red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "denied" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "light-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 6, + "x": 0, + "y": 27 + }, + "id": 47, + "links": [], + "maxDataPoints": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "values": ["value"] + }, + "pieType": "donut", + "reduceOptions": { + "calcs": ["sum"], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "bucketAggs": [ + { + "$$hashKey": "object:493", + "fake": true, + "field": "result", + "id": "3", + "settings": { + "min_doc_count": 1, + "missing": "N/A", + "order": "desc", + "orderBy": "_term", + "size": "10" + }, + "type": "terms" + }, + { + "$$hashKey": "object:494", + "field": "timestamp", + "id": "2", + "settings": { + "interval": "auto", + "min_doc_count": 0, + "trimEdges": 0 + }, + "type": "date_histogram" + } + ], + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "metrics": [ + { + "$$hashKey": "object:491", + "field": "select field", + "id": "1", + "meta": {}, + "settings": {}, + "type": "count" + } + ], + "query": "", + "refId": "A", + "timeField": "timestamp" + } + ], + "title": "RESULTS", + "type": "piechart" + }, + { + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "orange", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "USER NAME" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 6, + "x": 6, + "y": 27 + }, + "id": 46, + "links": [], + "maxDataPoints": 3, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "10.2.3", + "targets": [ + { + "bucketAggs": [ + { + "$$hashKey": "object:493", + "fake": true, + "field": "user_name", + "id": "3", + "settings": { + "min_doc_count": 1, + "order": "desc", + "orderBy": "_count", + "size": "10" + }, + "type": "terms" + } + ], + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "metrics": [ + { + "$$hashKey": "object:491", + "field": "select field", + "id": "1", + "meta": {}, + "settings": {}, + "type": "count" + } + ], + "query": "result:denied", + "refId": "A", + "timeField": "timestamp" + } + ], + "title": "AUTHS DENIED", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "data_sca_policy": "POLICY", + "data_user_name": "USER", + "user_name": "USER NAME" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "orange", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "ACCESS DEVICE HOSTNAME" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 6, + "x": 12, + "y": 27 + }, + "id": 55, + "links": [], + "maxDataPoints": 3, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "10.2.3", + "targets": [ + { + "bucketAggs": [ + { + "$$hashKey": "object:493", + "fake": true, + "field": "access_device_hostname", + "id": "3", + "settings": { + "min_doc_count": 1, + "order": "desc", + "orderBy": "_count", + "size": "10" + }, + "type": "terms" + } + ], + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "metrics": [ + { + "$$hashKey": "object:491", + "field": "select field", + "id": "1", + "meta": {}, + "settings": {}, + "type": "count" + } + ], + "query": "result:denied AND !access_device_hostname:null", + "refId": "A", + "timeField": "timestamp" + } + ], + "title": "AUTHS DENIED (ACCESS DEVICE)", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "access_device_hostname": "ACCESS DEVICE HOSTNAME", + "data_access_device_hostname": "ACCESS DEVICE", + "data_sca_policy": "POLICY", + "data_user_name": "USER" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "orange", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "AUTH DEVICE NAME" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 6, + "x": 18, + "y": 27 + }, + "id": 56, + "links": [], + "maxDataPoints": 3, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "10.2.3", + "targets": [ + { + "bucketAggs": [ + { + "$$hashKey": "object:493", + "fake": true, + "field": "auth_device_name", + "id": "3", + "settings": { + "min_doc_count": 1, + "order": "desc", + "orderBy": "_count", + "size": "10" + }, + "type": "terms" + } + ], + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "metrics": [ + { + "$$hashKey": "object:491", + "field": "select field", + "id": "1", + "meta": {}, + "settings": {}, + "type": "count" + } + ], + "query": "result:denied AND !auth_device_name:null", + "refId": "A", + "timeField": "timestamp" + } + ], + "title": "AUTHS DENIED (AUTH DEVICE)", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "auth_device_name": "AUTH DEVICE NAME", + "data_access_device_hostname": "ACCESS DEVICE", + "data_auth_device_name": "AUTH DEVICE", + "data_sca_policy": "POLICY", + "data_user_name": "USER" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": true, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "semi-dark-orange", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "EVENT ID" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "title": "VIEW EVENT DETAILS", + "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" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 16, + "w": 24, + "x": 0, + "y": 39 + }, + "id": 27, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": true, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "10.2.3", + "targets": [ + { + "bucketAggs": [], + "datasource": { + "type": "grafana-opensearch-datasource", + "uid": "replace_datasource_uid" + }, + "metrics": [ + { + "id": "1", + "settings": { + "size": "500" + }, + "type": "raw_data" + } + ], + "query": "", + "refId": "A", + "timeField": "timestamp" + } + ], + "title": "DUO EVENTS", + "transformations": [ + { + "id": "filterFieldsByName", + "options": { + "include": { + "names": ["_id", "email", "result", "auth_device_location_city"] + } + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": { + "_id": 0, + "auth_device_location_city": 2, + "email": 1, + "result": 3 + }, + "renameByName": { + "_id": "EVENT ID", + "agent_ip": "AGENT IP", + "agent_name": "AGENT", + "auth_device_location_city": "DEVICE CITY", + "data_access_device_hostname": "ACCESS DEVICE", + "data_access_device_ip": "DEVICE IP", + "data_access_device_ip_country_code": "COUNTRY", + "data_application_name": "APP", + "data_auth_device_ip": "DEVICE IP", + "data_auth_device_name": "AUTH DEVICE", + "data_email": "EMAIL", + "data_event_type": "EVENT TYPE", + "data_reason": "REASON", + "data_result": "RESULT", + "data_sca_check_reason": "REASON", + "data_sca_check_remediation": "REMEDIATION", + "data_sca_check_result": "RESULT", + "data_sca_check_title": "CONTROL", + "data_sca_policy": "POLICY", + "data_sca_type": "", + "data_user_name": "USERNAME", + "email": "EMAIL", + "result": "RESULT", + "timestamp": "DATE/TIME" + } + } + } + ], + "transparent": true, + "type": "table" + } + ], + "refresh": "", + "schemaVersion": 39, + "tags": ["EDR"], + "templating": { + "list": [ + { + "datasource": { + "type": "elasticsearch", + "uid": "duo_datasource_uid" + }, + "filters": [], + "hide": 0, + "label": "", + "name": "Filters", + "skipUrlSync": false, + "type": "adhoc" + }, + { + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "elasticsearch", + "uid": "duo_datasource_uid" + }, + "definition": "{ \"find\": \"terms\", \"field\": \"agent_name\", \"query\": \"rule_groups:rootcheck OR rule_groups:oscap OR rule_groups:sca\"}", + "hide": 0, + "includeAll": true, + "label": "Agent", + "multi": false, + "name": "agent_name", + "options": [], + "query": "{ \"find\": \"terms\", \"field\": \"agent_name\", \"query\": \"rule_groups:rootcheck OR rule_groups:oscap OR rule_groups:sca\"}", + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 2, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-24h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + }, + "timezone": "", + "title": "DUO AUTH LOGS", + "version": 9, + "weekStart": "" +} diff --git a/backend/app/connectors/grafana/schema/dashboards.py b/backend/app/connectors/grafana/schema/dashboards.py index 1c700f764..8da34c742 100644 --- a/backend/app/connectors/grafana/schema/dashboards.py +++ b/backend/app/connectors/grafana/schema/dashboards.py @@ -97,6 +97,10 @@ class CrowdstrikeDashboard(Enum): CROWDSTRIKE_SUMMARY = ("Crowdstrike", "summary.json") +class DuoDashboard(Enum): + DUO_AUTH = ("Duo", "duo_auth.json") + + class DashboardProvisionRequest(BaseModel): dashboards: List[str] = Field( ..., @@ -128,6 +132,7 @@ def check_dashboard_exists(cls, e): + list(CarbonBlackDashboard) + list(FortinetDashboard) + list(CrowdstrikeDashboard) + + list(DuoDashboard) } if e not in valid_dashboards: raise ValueError(f'Dashboard identifier "{e}" is not recognized.') diff --git a/backend/app/connectors/grafana/services/dashboards.py b/backend/app/connectors/grafana/services/dashboards.py index 9a196c670..0c713b261 100644 --- a/backend/app/connectors/grafana/services/dashboards.py +++ b/backend/app/connectors/grafana/services/dashboards.py @@ -7,6 +7,7 @@ from app.connectors.grafana.schema.dashboards import CarbonBlackDashboard from app.connectors.grafana.schema.dashboards import CrowdstrikeDashboard from app.connectors.grafana.schema.dashboards import DashboardProvisionRequest +from app.connectors.grafana.schema.dashboards import DuoDashboard from app.connectors.grafana.schema.dashboards import FortinetDashboard from app.connectors.grafana.schema.dashboards import GrafanaDashboard from app.connectors.grafana.schema.dashboards import GrafanaDashboardResponse @@ -181,6 +182,7 @@ async def provision_dashboards( + list(CarbonBlackDashboard) + list(FortinetDashboard) + list(CrowdstrikeDashboard) + + list(DuoDashboard) } for dashboard_name in dashboard_request.dashboards: diff --git a/backend/app/db/db_populate.py b/backend/app/db/db_populate.py index f03732b35..98afb9561 100644 --- a/backend/app/db/db_populate.py +++ b/backend/app/db/db_populate.py @@ -273,6 +273,7 @@ def get_available_integrations_list(): ("Huntress", "Integrate Huntress with SOCFortress."), ("CarbonBlack", "Integrate CarbonBlack with SOCFortress."), ("Crowdstrike", "Integrate Crowdstrike with SOCFortress."), + ("DUO", "Integrate DUO with SOCFortress."), # ... Add more available integrations as needed ... ] @@ -385,6 +386,9 @@ async def get_available_integrations_auth_keys_list(session: AsyncSession): ("Crowdstrike", "CLIENT_SECRET"), ("Crowdstrike", "BASE_URL"), ("Crowdstrike", "SYSLOG_PORT"), + ("DUO", "API_HOSTNAME"), + ("DUO", "INTEGRATION_KEY"), + ("DUO", "SECRET_KEY"), # ... Add more available integrations auth keys as needed ... ] logger.info("Getting available integrations auth keys.") diff --git a/backend/app/integrations/duo/routes/provision.py b/backend/app/integrations/duo/routes/provision.py new file mode 100644 index 000000000..85338c43e --- /dev/null +++ b/backend/app/integrations/duo/routes/provision.py @@ -0,0 +1,51 @@ +from fastapi import APIRouter +from fastapi import Depends +from sqlalchemy.ext.asyncio import AsyncSession + +from app.db.db_session import get_db +from app.integrations.duo.schema.provision import ProvisionDuoRequest +from app.integrations.duo.schema.provision import ProvisionDuoResponse +from app.integrations.duo.services.provision import provision_duo +from app.integrations.utils.utils import get_customer_integration_response +from app.schedulers.models.scheduler import CreateSchedulerRequest +from app.schedulers.scheduler import add_scheduler_jobs + +integration_duo_provision_router = APIRouter() + + +@integration_duo_provision_router.post( + "/provision", + response_model=ProvisionDuoResponse, + description="Provision a Duo integration.", +) +async def provision_duo_route( + provision_duo_request: ProvisionDuoRequest, + session: AsyncSession = Depends(get_db), +) -> ProvisionDuoResponse: + """ + Provisions a duo integration. + + Args: + provision_duo_request (ProvisionDuoRequest): The request object containing the necessary data for provisioning. + session (AsyncSession, optional): The database session. Defaults to Depends(get_db). + + Returns: + ProvisionDuoResponse: The response object indicating the success or failure of the provisioning process. + """ + # Check if the customer integration settings are available and can be provisioned + await get_customer_integration_response( + provision_duo_request.customer_code, + session, + ) + await provision_duo(provision_duo_request, session) + await add_scheduler_jobs( + CreateSchedulerRequest( + function_name="invoke_duo_integration_collect", + time_interval=provision_duo_request.time_interval, + job_id="invoke_duo_integration_collect", + ), + ) + return ProvisionDuoResponse( + success=True, + message="Duo integration provisioned successfully.", + ) diff --git a/backend/app/integrations/duo/schema/provision.py b/backend/app/integrations/duo/schema/provision.py new file mode 100644 index 000000000..dea6f5d9c --- /dev/null +++ b/backend/app/integrations/duo/schema/provision.py @@ -0,0 +1,87 @@ +from typing import Any +from typing import Dict +from typing import List +from typing import Optional + +from pydantic import BaseModel +from pydantic import Field +from pydantic import root_validator + + +class ProvisionDuoRequest(BaseModel): + customer_code: str = Field( + ..., + description="The customer code.", + examples=["00002"], + ) + time_interval: int = Field( + 15, + description="The time interval for the scheduler.", + examples=[15], + ) + integration_name: str = Field( + "Duo", + description="The integration name.", + examples=["Duo"], + ) + + # ensure the `integration_name` is always set to "Mimecast" + @root_validator(pre=True) + def set_integration_name(cls, values: Dict[str, Any]) -> Dict[str, Any]: + values["integration_name"] = "Duo" + return values + + +class ProvisionDuoResponse(BaseModel): + success: bool + message: str + + +# ! STREAMS ! # +class StreamRule(BaseModel): + field: str + type: int + inverted: bool + value: str + + +class DuoEventStream(BaseModel): + title: str = Field(..., description="Title of the stream") + description: str = Field(..., description="Description of the stream") + index_set_id: str = Field(..., description="ID of the associated index set") + rules: List[StreamRule] = Field(..., description="List of rules for the stream") + matching_type: str = Field(..., description="Matching type for the rules") + remove_matches_from_default_stream: bool = Field( + ..., + description="Whether to remove matches from the default stream", + ) + content_pack: Optional[str] = Field( + None, + description="Associated content pack, if any", + ) + + class Config: + schema_extra = { + "example": { + "title": "Duo SIEM EVENTS - Example Company", + "description": "Duo SIEM EVENTS - Example Company", + "index_set_id": "12345", + "rules": [ + { + "field": "customer_code", + "type": 1, + "inverted": False, + "value": "ExampleCode", + }, + { + "field": "integration", + "type": 1, + "inverted": False, + "value": "huntress", + }, + ], + "matching_type": "AND", + "remove_matches_from_default_stream": True, + "content_pack": None, + }, + } diff --git a/backend/app/integrations/duo/services/provision.py b/backend/app/integrations/duo/services/provision.py new file mode 100644 index 000000000..a473f3760 --- /dev/null +++ b/backend/app/integrations/duo/services/provision.py @@ -0,0 +1,397 @@ +import json +from datetime import datetime + +from loguru import logger +from sqlalchemy import and_ +from sqlalchemy import update +from sqlalchemy.ext.asyncio import AsyncSession + +from app.connectors.grafana.schema.dashboards import DashboardProvisionRequest +from app.connectors.grafana.schema.dashboards import DuoDashboard +from app.connectors.grafana.services.dashboards import provision_dashboards +from app.connectors.grafana.utils.universal import create_grafana_client +from app.connectors.graylog.services.management import start_stream +from app.connectors.graylog.utils.universal import send_post_request +from app.connectors.wazuh_indexer.services.monitoring import ( + output_shard_number_to_be_set_based_on_nodes, +) +from app.customer_provisioning.schema.grafana import GrafanaDatasource +from app.customer_provisioning.schema.grafana import GrafanaDataSourceCreationResponse +from app.customer_provisioning.schema.graylog import GraylogIndexSetCreationResponse +from app.customer_provisioning.schema.graylog import StreamCreationResponse +from app.customer_provisioning.schema.graylog import TimeBasedIndexSet +from app.customer_provisioning.services.grafana import create_grafana_folder +from app.customer_provisioning.services.grafana import get_opensearch_version +from app.customers.routes.customers import get_customer +from app.customers.routes.customers import get_customer_meta +from app.integrations.duo.schema.provision import DuoEventStream +from app.integrations.duo.schema.provision import ProvisionDuoRequest +from app.integrations.duo.schema.provision import ProvisionDuoResponse +from app.integrations.models.customer_integration_settings import CustomerIntegrations +from app.integrations.routes import create_integration_meta +from app.integrations.schema import CustomerIntegrationsMetaSchema +from app.utils import get_connector_attribute + + +################## ! GRAYLOG ! ################## +async def build_index_set_config( + customer_code: str, + session: AsyncSession, +) -> TimeBasedIndexSet: + """ + Build the configuration for a time-based index set. + + Args: + request (ProvisionNewCustomer): The request object containing customer information. + + Returns: + TimeBasedIndexSet: The configured time-based index set. + """ + return TimeBasedIndexSet( + title=f"{(await get_customer(customer_code, session)).customer.customer_name} - DUO", + description=f"{customer_code} - DUO", + index_prefix=f"duo-{customer_code}", + rotation_strategy_class="org.graylog2.indexer.rotation.strategies.TimeBasedRotationStrategy", + rotation_strategy={ + "type": "org.graylog2.indexer.rotation.strategies.TimeBasedRotationStrategyConfig", + "rotation_period": "P1D", + "rotate_empty_index_set": False, + "max_rotation_period": None, + }, + retention_strategy_class="org.graylog2.indexer.retention.strategies.DeletionRetentionStrategy", + retention_strategy={ + "type": "org.graylog2.indexer.retention.strategies.DeletionRetentionStrategyConfig", + "max_number_of_indices": 30, + }, + creation_date=datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + index_analyzer="standard", + shards=await output_shard_number_to_be_set_based_on_nodes(), + replicas=0, + index_optimization_max_num_segments=1, + index_optimization_disabled=False, + writable=True, + field_type_refresh_interval=5000, + ) + + +# Function to send the POST request and handle the response +async def send_index_set_creation_request( + index_set: TimeBasedIndexSet, +) -> GraylogIndexSetCreationResponse: + """ + Sends a request to create an index set in Graylog. + + Args: + index_set (TimeBasedIndexSet): The index set to be created. + + Returns: + GraylogIndexSetCreationResponse: The response from Graylog after creating the index set. + """ + json_index_set = json.dumps(index_set.dict()) + logger.info(f"json_index_set set: {json_index_set}") + response_json = await send_post_request( + endpoint="/api/system/indices/index_sets", + data=index_set.dict(), + ) + return GraylogIndexSetCreationResponse(**response_json) + + +async def create_index_set( + customer_code: str, + session: AsyncSession, +) -> GraylogIndexSetCreationResponse: + """ + Creates an index set for a new customer. + + Args: + request (ProvisionNewCustomer): The request object containing the customer information. + + Returns: + GraylogIndexSetCreationResponse: The response object containing the result of the index set creation. + """ + logger.info(f"Creating index set for customer {customer_code}") + index_set_config = await build_index_set_config(customer_code, session) + return await send_index_set_creation_request(index_set_config) + + +# ! Event STREAMS ! # +# Function to create event stream configuration +async def build_event_stream_config( + customer_code: str, + index_set_id: str, + session: AsyncSession, +) -> DuoEventStream: + """ + Builds the configuration for the Duo event stream. + + Args: + customer_code (str): The customer code. + index_set_id (str): The index set ID. + session (AsyncSession): The async session. + + Returns: + DuoEventStream: The configured Duo event stream. + """ + return DuoEventStream( + title=f"{(await get_customer(customer_code, session)).customer.customer_name} - DUO", + description=f"{(await get_customer(customer_code, session)).customer.customer_name} - DUO", + index_set_id=index_set_id, + rules=[ + { + "field": "integration", + "type": 1, + "inverted": False, + "value": "duo", + }, + { + "field": "customer_code", + "type": 1, + "inverted": False, + "value": f"{customer_code}", + }, + ], + matching_type="AND", + remove_matches_from_default_stream=True, + content_pack=None, + ) + + +async def send_event_stream_creation_request( + event_stream: DuoEventStream, +) -> StreamCreationResponse: + """ + Sends a request to create an event stream. + + Args: + event_stream (SapSiemEventStream): The event stream to be created. + + Returns: + StreamCreationResponse: The response containing the created event stream. + """ + json_event_stream = json.dumps(event_stream.dict()) + logger.info(f"json_event_stream set: {json_event_stream}") + response_json = await send_post_request( + endpoint="/api/streams", + data=event_stream.dict(), + ) + return StreamCreationResponse(**response_json) + + +async def create_event_stream( + customer_code: str, + index_set_id: str, + session: AsyncSession, +) -> StreamCreationResponse: + """ + Creates an event stream for a customer. + + Args: + request (ProvisionNewCustomer): The request object containing customer information. + index_set_id (str): The ID of the index set. + + Returns: + The result of the event stream creation request. + """ + event_stream_config = await build_event_stream_config( + customer_code, + index_set_id, + session, + ) + return await send_event_stream_creation_request(event_stream_config) + + +#### ! GRAFANA ! #### +async def create_grafana_datasource( + customer_code: str, + session: AsyncSession, +) -> GrafanaDataSourceCreationResponse: + """ + Creates a Grafana datasource for the specified customer. + + Args: + customer_code (str): The customer code. + session (AsyncSession): The async session. + + Returns: + GrafanaDataSourceCreationResponse: The response containing the created datasource details. + """ + logger.info("Creating Grafana datasource") + grafana_client = await create_grafana_client("Grafana") + # Switch to the newly created organization + grafana_client.user.switch_actual_user_organisation( + (await get_customer_meta(customer_code, session)).customer_meta.customer_meta_grafana_org_id, + ) + datasource_payload = GrafanaDatasource( + name="DUO", + type="grafana-opensearch-datasource", + typeName="OpenSearch", + access="proxy", + url=await get_connector_attribute( + connector_id=1, + column_name="connector_url", + session=session, + ), + database=f"duo-{customer_code}*", + basicAuth=True, + basicAuthUser=await get_connector_attribute( + connector_id=1, + column_name="connector_username", + session=session, + ), + secureJsonData={ + "basicAuthPassword": await get_connector_attribute( + connector_id=1, + column_name="connector_password", + session=session, + ), + }, + isDefault=False, + jsonData={ + "database": f"duo-{customer_code}*", + "flavor": "opensearch", + "includeFrozen": False, + "logLevelField": "severity", + "logMessageField": "summary", + "maxConcurrentShardRequests": 5, + "pplEnabled": True, + "timeField": "timestamp", + "tlsSkipVerify": True, + "version": await get_opensearch_version(), + }, + readOnly=True, + ) + results = grafana_client.datasource.create_datasource( + datasource=datasource_payload.dict(), + ) + return GrafanaDataSourceCreationResponse(**results) + + +async def provision_duo( + provision_duo_request: ProvisionDuoRequest, + session: AsyncSession, +) -> ProvisionDuoResponse: + logger.info( + f"Provisioning Duo integration for customer {provision_duo_request.customer_code}.", + ) + + # Create Index Set + index_set_id = ( + await create_index_set( + customer_code=provision_duo_request.customer_code, + session=session, + ) + ).data.id + logger.info(f"Index set: {index_set_id}") + # Create event stream + stream_id = ( + await create_event_stream( + provision_duo_request.customer_code, + index_set_id, + session, + ) + ).data.stream_id + # Start stream + await start_stream(stream_id=stream_id) + + # Grafana Deployment + duo_datasource_uid = ( + await create_grafana_datasource( + customer_code=provision_duo_request.customer_code, + session=session, + ) + ).datasource.uid + grafana_duo_folder_id = ( + await create_grafana_folder( + organization_id=( + await get_customer_meta( + provision_duo_request.customer_code, + session, + ) + ).customer_meta.customer_meta_grafana_org_id, + folder_title="DUO", + ) + ).id + await provision_dashboards( + DashboardProvisionRequest( + dashboards=[dashboard.name for dashboard in DuoDashboard], + organizationId=( + await get_customer_meta( + provision_duo_request.customer_code, + session, + ) + ).customer_meta.customer_meta_grafana_org_id, + folderId=grafana_duo_folder_id, + datasourceUid=duo_datasource_uid, + ), + ) + await create_integration_meta_entry( + CustomerIntegrationsMetaSchema( + customer_code=provision_duo_request.customer_code, + integration_name="Duo", + graylog_input_id=None, + graylog_index_id=index_set_id, + graylog_stream_id=stream_id, + grafana_org_id=( + await get_customer_meta( + provision_duo_request.customer_code, + session, + ) + ).customer_meta.customer_meta_grafana_org_id, + grafana_dashboard_folder_id=grafana_duo_folder_id, + ), + session, + ) + await update_customer_integration_table( + provision_duo_request.customer_code, + session, + ) + + return ProvisionDuoResponse( + success=True, + message="Duo integration provisioned successfully.", + ) + + +############## ! WRITE TO DB ! ############## +async def create_integration_meta_entry( + customer_integration_meta: CustomerIntegrationsMetaSchema, + session: AsyncSession, +) -> None: + """ + Creates an entry for the customer integration meta in the database. + + Args: + customer_integration_meta (CustomerIntegrationsMetaSchema): The customer integration meta object. + session (AsyncSession): The async session object for database operations. + """ + await create_integration_meta(customer_integration_meta, session) + logger.info( + f"Integration meta entry created for customer {customer_integration_meta.customer_code}.", + ) + + +async def update_customer_integration_table( + customer_code: str, + session: AsyncSession, +) -> None: + """ + Updates the `customer_integrations` table to set the `deployed` column to True where the `customer_code` + matches the given customer code and the `integration_service_name` is "Duo". + + Args: + customer_code (str): The customer code. + session (AsyncSession): The async session object for making HTTP requests. + """ + await session.execute( + update(CustomerIntegrations) + .where( + and_( + CustomerIntegrations.customer_code == customer_code, + CustomerIntegrations.integration_service_name == "Duo", + ), + ) + .values(deployed=True), + ) + await session.commit() + + return None diff --git a/backend/app/integrations/markdown/duo.md b/backend/app/integrations/markdown/duo.md new file mode 100644 index 000000000..568b33373 --- /dev/null +++ b/backend/app/integrations/markdown/duo.md @@ -0,0 +1,44 @@ +# [DUO Integration](https://duo.com/docs/adminapi#overview) + +# Duo Admin API + +CoPilot and DUO. Ingest DUO auth logs into your SIEM stack + +## Overview + +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. + +### Key Features + +- **Automated Log Collection:** Automatically ingest Duo authentication logs into your SIEM for real-time analysis. +- **Comprehensive Monitoring:** Monitor Duo-related activities, including user logins, telephony logs, and administrator actions. +- **Custom Alerts:** Set up custom alerts based on Duo log data to quickly identify and respond to potential security incidents. +- **Detailed Reports:** Generate detailed reports on Duo authentication events, helping you to meet compliance requirements and improve security posture. + +## First Steps + +**Role required: Owner** + +Note that only administrators with the Owner role can create or modify an Admin API application in the Duo Admin Panel. + +1. Sign up for a Duo account. +2. Log in to the Duo Admin Panel and navigate to Applications. +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. + +### Treat your secret key like a password + +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! + +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. + +### Permission Details + +| Permission | Details | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Grant administrators | The Admin API application can read information about, create, update, and delete Duo administrators and administrative units. | +| Grant read information | The Admin API application can read information about the Duo customer account's utilization. | +| 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. | +| Grant settings | The Admin API application can read and change global Duo account settings. | +| Grant read log | The Admin API application can read authentication, offline access, telephony, and administrator action log information. | +| Grant read resource | The Admin API application can read information about resource objects such as end users and devices. | +| Grant write resource | The Admin API application can create, update, and delete resource objects such as end users and devices. | diff --git a/backend/app/integrations/modules/routes/duo.py b/backend/app/integrations/modules/routes/duo.py new file mode 100644 index 000000000..0b7965036 --- /dev/null +++ b/backend/app/integrations/modules/routes/duo.py @@ -0,0 +1,94 @@ +from fastapi import APIRouter +from fastapi import Depends +from loguru import logger +from sqlalchemy.ext.asyncio import AsyncSession + +from app.db.db_session import get_db +from app.integrations.modules.schema.duo import CollectDuo +from app.integrations.modules.schema.duo import DuoAuthKeys +from app.integrations.modules.schema.duo import InvokeDuoRequest +from app.integrations.modules.schema.duo import InvokeDuoResponse +from app.integrations.modules.services.duo import post_to_copilot_duo_module +from app.integrations.routes import find_customer_integration +from app.integrations.utils.utils import extract_auth_keys +from app.integrations.utils.utils import get_customer_integration_response +from app.utils import get_connector_attribute + +module_duo_router = APIRouter() + + +async def get_duo_auth_keys(customer_integration) -> DuoAuthKeys: + """ + Extract the Duo authentication keys from the CustomerIntegration. + + Args: + customer_integration (CustomerIntegration): The CustomerIntegration containing the + Duo authentication keys. + + Returns: + DuoAuthKeys: The extracted Duo authentication keys. + """ + duo_auth_keys = extract_auth_keys( + customer_integration, + service_name="DUO", + ) + + return DuoAuthKeys(**duo_auth_keys) + + +async def get_collect_duo_data(duo_request, session, auth_keys): + return CollectDuo( + integration="duo", + customer_code=duo_request.customer_code, + graylog_host=await get_connector_attribute( + connector_id=14, + column_name="connector_url", + session=session, + ), + graylog_port=await get_connector_attribute( + connector_id=14, + column_name="connector_extra_data", + session=session, + ), + integration_key=auth_keys.INTEGRATION_KEY, + secret_key=auth_keys.SECRET_KEY, + api_host=auth_keys.API_HOSTNAME, + api_endpoint="/admin/v2/logs/authentication", + range="15m", + ) + + +@module_duo_router.post( + "", + response_model=InvokeDuoResponse, + description="Invoke the Duo module.", +) +async def collect_duo_route(duo_request: InvokeDuoRequest, session: AsyncSession = Depends(get_db)): + """Pull down Duo Events.""" + logger.info(f"Invoke Duo Request: {duo_request}") + try: + customer_integration_response = await get_customer_integration_response( + duo_request.customer_code, + session, + ) + + # ! SWITCH TO CAPS DUE TO HOW THAT IS STORED IN DB ! # + duo_request.integration_name = "DUO" + + customer_integration = await find_customer_integration( + duo_request.customer_code, + duo_request.integration_name, + customer_integration_response, + ) + + auth_keys = await get_duo_auth_keys(customer_integration) + + collect_duo_data = await get_collect_duo_data(duo_request, session, auth_keys) + + await post_to_copilot_duo_module(data=collect_duo_data) + + except Exception as e: + logger.error(f"Error during DB session: {str(e)}") + return InvokeDuoResponse(success=False, message=str(e)) + + return InvokeDuoResponse(success=True, message="Duo Events collected successfully.") diff --git a/backend/app/integrations/modules/schema/duo.py b/backend/app/integrations/modules/schema/duo.py new file mode 100644 index 000000000..ce3de1423 --- /dev/null +++ b/backend/app/integrations/modules/schema/duo.py @@ -0,0 +1,78 @@ +from fastapi import HTTPException +from pydantic import BaseModel +from pydantic import Field +from pydantic import validator + + +class InvokeDuoRequest(BaseModel): + customer_code: str = Field( + ..., + description="The customer code.", + examples=["00002"], + ) + integration_name: str = Field( + "Duo", + description="The integration name.", + examples=["Duo"], + ) + + +class DuoAuthKeys(BaseModel): + API_HOSTNAME: str = Field( + ..., + description="The API key.", + examples=["123456"], + ) + INTEGRATION_KEY: str = Field( + ..., + description="The integration key.", + examples=["123456"], + ) + SECRET_KEY: str = Field( + ..., + description="The secret key.", + examples=["123456"], + ) + + +class InvokeDuoResponse(BaseModel): + success: bool = Field( + ..., + description="The success status.", + examples=[True], + ) + message: str = Field( + ..., + description="The message.", + examples=["Duo Events collected successfully."], + ) + + +class CollectDuo(BaseModel): + integration: str = Field(..., example="duo") + customer_code: str = Field(..., example="socfortress") + integration_key: str = Field(..., example="1234567890") + secret_key: str = Field(..., example="1234567890") + api_host: str = Field(..., example="api-1234567890.duosecurity.com") + api_endpoint: str = Field(..., example="/admin/v2/logs/authentication") + graylog_host: str = Field(..., example="127.0.0.1") + graylog_port: str = Field(..., example=12201) + range: str = Field(..., example="15m") # New field for range + + @validator("integration") + def check_integration(cls, v): + if v != "duo": + raise HTTPException( + status_code=400, + detail="Invalid integration. Only 'duo' is supported.", + ) + return v + + @validator("range") + def validate_range(cls, v): + if not v.endswith(("m", "h", "d")): + raise HTTPException( + status_code=400, + detail="Invalid range. Use 'm' for minutes, 'h' for hours, or 'd' for days.", + ) + return v diff --git a/backend/app/integrations/modules/services/duo.py b/backend/app/integrations/modules/services/duo.py new file mode 100644 index 000000000..3877e8a62 --- /dev/null +++ b/backend/app/integrations/modules/services/duo.py @@ -0,0 +1,21 @@ +import httpx +from loguru import logger + +from app.integrations.modules.schema.duo import CollectDuo + + +async def post_to_copilot_duo_module(data: CollectDuo): + """ + Send a POST request to the copilot-duo-module Docker container. + + Args: + data (CollectDuo): The data to send to the copilot-duo-module Docker container. + """ + logger.info(f"Sending POST request to http://copilot-duo-module/auth with data: {data.dict()}") + async with httpx.AsyncClient() as client: + await client.post( + "http://copilot-duo-module/auth", + json=data.dict(), + timeout=120, + ) + return None diff --git a/backend/app/routers/duo.py b/backend/app/routers/duo.py new file mode 100644 index 000000000..c02715a65 --- /dev/null +++ b/backend/app/routers/duo.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter + +from app.integrations.duo.routes.provision import integration_duo_provision_router + +# Instantiate the APIRouter +router = APIRouter() + +# Include the Duo Provision APIRouter +router.include_router( + integration_duo_provision_router, + prefix="/duo", + tags=["duo"], +) diff --git a/backend/app/routers/modules.py b/backend/app/routers/modules.py index 77bc87ae2..40ea622f1 100644 --- a/backend/app/routers/modules.py +++ b/backend/app/routers/modules.py @@ -1,5 +1,6 @@ from fastapi import APIRouter +from app.integrations.modules.routes.duo import module_duo_router from app.integrations.modules.routes.huntress import module_huntress_router from app.integrations.modules.routes.mimecast import module_mimecast_router from app.integrations.modules.routes.sap_siem import module_sap_siem_router @@ -23,3 +24,9 @@ prefix="/integrations/modules/sap_siem", tags=["SAP SIEM"], ) + +router.include_router( + module_duo_router, + prefix="/integrations/modules/duo", + tags=["DUO"], +) diff --git a/backend/app/schedulers/scheduler.py b/backend/app/schedulers/scheduler.py index e13c8dab6..fa8b8e6be 100644 --- a/backend/app/schedulers/scheduler.py +++ b/backend/app/schedulers/scheduler.py @@ -17,6 +17,7 @@ from app.schedulers.services.invoke_carbonblack import ( invoke_carbonblack_integration_collect, ) +from app.schedulers.services.invoke_duo import invoke_duo_integration_collect from app.schedulers.services.invoke_huntress import invoke_huntress_integration_collect from app.schedulers.services.invoke_mimecast import invoke_mimecast_integration from app.schedulers.services.invoke_mimecast import invoke_mimecast_integration_ttp @@ -209,6 +210,7 @@ def get_function_by_name(function_name: str): "invoke_sap_siem_integration_brute_force_failed_logins_same_ip": invoke_sap_siem_integration_brute_force_failed_logins_same_ip, "invoke_sap_siem_integration_successful_login_after_multiple_failed_logins": invoke_sap_siem_integration_successful_login_after_multiple_failed_logins, "invoke_huntress_integration_collection": invoke_huntress_integration_collect, + "invoke_duo_integration_collect": invoke_duo_integration_collect, "invoke_carbonblack_integration_collection": invoke_carbonblack_integration_collect, # Add other function mappings here } diff --git a/backend/app/schedulers/services/invoke_duo.py b/backend/app/schedulers/services/invoke_duo.py new file mode 100644 index 000000000..4b2b683e1 --- /dev/null +++ b/backend/app/schedulers/services/invoke_duo.py @@ -0,0 +1,52 @@ +from datetime import datetime + +from dotenv import load_dotenv +from loguru import logger +from sqlalchemy import select + +from app.db.db_session import get_db_session +from app.db.db_session import get_sync_db_session +from app.integrations.models.customer_integration_settings import CustomerIntegrations +from app.integrations.modules.routes.duo import collect_duo_route +from app.integrations.modules.schema.duo import InvokeDuoRequest +from app.integrations.modules.schema.duo import InvokeDuoResponse +from app.schedulers.models.scheduler import JobMetadata + +load_dotenv() + + +async def invoke_duo_integration_collect() -> InvokeDuoResponse: + """ + Invokes the Duo integration collection. + """ + logger.info("Invoking Duo integration collection.") + customer_codes = [] + async with get_db_session() as session: + stmt = select(CustomerIntegrations).where( + CustomerIntegrations.integration_service_name == "DUO", + ) + result = await session.execute(stmt) + customer_codes = [row.customer_code for row in result.scalars()] + logger.info(f"customer_codes: {customer_codes}") + for customer_code in customer_codes: + await collect_duo_route( + InvokeDuoRequest( + customer_code=customer_code, + integration_name="Duo", + ), + session, + ) + # Close the session + await session.close() + with get_sync_db_session() as session: + # Synchronous ORM operations + job_metadata = session.query(JobMetadata).filter_by(job_id="invoke_duo_integration_collection").one_or_none() + if job_metadata: + job_metadata.last_success = datetime.utcnow() + session.add(job_metadata) + session.commit() + else: + # Handle the case where job_metadata does not exist + print("JobMetadata for 'invoke_duo_integration_collection' not found.") + + return InvokeDuoResponse(success=True, message="Duo integration invoked.") diff --git a/backend/copilot.py b/backend/copilot.py index 6dc87be12..82b08aa64 100644 --- a/backend/copilot.py +++ b/backend/copilot.py @@ -41,6 +41,7 @@ from app.routers import customers from app.routers import dfir_iris from app.routers import dnstwist +from app.routers import duo from app.routers import grafana from app.routers import graylog from app.routers import healthcheck @@ -146,6 +147,7 @@ api_router.include_router(crowdstrike.router) api_router.include_router(scoutsuite.router) api_router.include_router(nuclei.router) +api_router.include_router(duo.router) # Include the APIRouter in the FastAPI app app.include_router(api_router) diff --git a/frontend/src/api/integrations.ts b/frontend/src/api/integrations.ts index 6d2ceb43b..0580a7de5 100644 --- a/frontend/src/api/integrations.ts +++ b/frontend/src/api/integrations.ts @@ -48,6 +48,12 @@ export default { }) }, + office365Provision(customerCode: string, integrationName: string) { + return HttpClient.post(`/office365/provision`, { + customer_code: customerCode, + integration_name: integrationName + }) + }, mimecastProvision(customerCode: string, integrationName: string) { return HttpClient.post(`/mimecast/provision`, { customer_code: customerCode, @@ -60,9 +66,10 @@ export default { integration_name: integrationName }) }, - office365Provision(customerCode: string, integrationName: string) { - return HttpClient.post(`/office365/provision`, { + duoProvision(customerCode: string, integrationName: string) { + return HttpClient.post(`/duo/provision`, { customer_code: customerCode, + time_interval: 15, integration_name: integrationName }) } diff --git a/frontend/src/components/customers/integrations/CustomerIntegrationActions.vue b/frontend/src/components/customers/integrations/CustomerIntegrationActions.vue index 400fe289a..4047589b8 100644 --- a/frontend/src/components/customers/integrations/CustomerIntegrationActions.vue +++ b/frontend/src/components/customers/integrations/CustomerIntegrationActions.vue @@ -36,6 +36,18 @@ Deploy + + + Deploy + + + loadingOffice365Provision.value || loadingMimecastProvision.value || loadingCrowdstrikeProvision.value || - loadingOffice365Provision.value || + loadingDuoProvision.value || loadingDelete.value ) @@ -96,6 +110,7 @@ const customerCode = computed(() => integration.customer_code) const isOffice365 = computed(() => serviceName.value === "Office365") const isMimecast = computed(() => serviceName.value === "Mimecast") const isCrowdstrike = computed(() => serviceName.value === "Crowdstrike") +const isDuo = computed(() => serviceName.value === "DUO") watch(loading, val => { if (val) { @@ -168,6 +183,27 @@ function crowdstrikeProvision() { }) } +function duoProvision() { + loadingDuoProvision.value = true + + Api.integrations + .duoProvision(customerCode.value, serviceName.value) + .then(res => { + if (res.data.success) { + emit("deployed") + message.success(res.data?.message || "Customer integration successfully deployed.") + } else { + message.warning(res.data?.message || "An error occurred. Please try again later.") + } + }) + .catch(err => { + message.error(err.response?.data?.message || "An error occurred. Please try again later.") + }) + .finally(() => { + loadingDuoProvision.value = false + }) +} + function handleDelete() { dialog.warning({ title: "Confirm", diff --git a/frontend/src/layouts/common/Toolbar/PinnedPages.vue b/frontend/src/layouts/common/Toolbar/PinnedPages.vue index 1301e9986..50af88c62 100644 --- a/frontend/src/layouts/common/Toolbar/PinnedPages.vue +++ b/frontend/src/layouts/common/Toolbar/PinnedPages.vue @@ -36,6 +36,7 @@ +
@@ -110,7 +111,8 @@ router.afterEach(route => {