From 17e937b31a82b84005312316e988a22b22011e97 Mon Sep 17 00:00:00 2001 From: Taylor Date: Wed, 19 Jun 2024 16:49:01 -0500 Subject: [PATCH 1/9] 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. --- backend/app/db/db_populate.py | 4 +++ backend/app/integrations/markdown/duo.md | 45 ++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 backend/app/integrations/markdown/duo.md 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/markdown/duo.md b/backend/app/integrations/markdown/duo.md new file mode 100644 index 000000000..214b29416 --- /dev/null +++ b/backend/app/integrations/markdown/duo.md @@ -0,0 +1,45 @@ +# [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. | From e20ee450a736ec0c6c7a2e251df4a3c3c2fcc67d Mon Sep 17 00:00:00 2001 From: Taylor Date: Wed, 19 Jun 2024 17:29:24 -0500 Subject: [PATCH 2/9] 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. --- .../connectors/grafana/schema/dashboards.py | 4 + .../connectors/grafana/services/dashboards.py | 3 +- .../app/integrations/duo/routes/provision.py | 51 +++ .../app/integrations/duo/schema/provision.py | 87 ++++ .../integrations/duo/services/provision.py | 397 ++++++++++++++++++ 5 files changed, 541 insertions(+), 1 deletion(-) create mode 100644 backend/app/integrations/duo/routes/provision.py create mode 100644 backend/app/integrations/duo/schema/provision.py create mode 100644 backend/app/integrations/duo/services/provision.py diff --git a/backend/app/connectors/grafana/schema/dashboards.py b/backend/app/connectors/grafana/schema/dashboards.py index 1c700f764..f0e9906ea 100644 --- a/backend/app/connectors/grafana/schema/dashboards.py +++ b/backend/app/connectors/grafana/schema/dashboards.py @@ -96,6 +96,9 @@ class FortinetDashboard(Enum): 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 +131,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..c364deda3 100644 --- a/backend/app/connectors/grafana/services/dashboards.py +++ b/backend/app/connectors/grafana/services/dashboards.py @@ -14,7 +14,7 @@ from app.connectors.grafana.schema.dashboards import MimecastDashboard from app.connectors.grafana.schema.dashboards import Office365Dashboard from app.connectors.grafana.schema.dashboards import SapSiemDashboard -from app.connectors.grafana.schema.dashboards import WazuhDashboard +from app.connectors.grafana.schema.dashboards import WazuhDashboard, DuoDashboard from app.connectors.grafana.utils.universal import create_grafana_client @@ -181,6 +181,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/integrations/duo/routes/provision.py b/backend/app/integrations/duo/routes/provision.py new file mode 100644 index 000000000..0046631be --- /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_dup_provision_router = APIRouter() + + +@integration_dup_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_collection", + time_interval=provision_duo_request.time_interval, + job_id="invoke_duo_integration_collection", + ), + ) + 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..21713e049 --- /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=[5], + ) + 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 From d9b19c02130e3b8fa8c717c68f14d8ac40f7c124 Mon Sep 17 00:00:00 2001 From: Taylor Date: Wed, 19 Jun 2024 17:32:30 -0500 Subject: [PATCH 3/9] refactor: Rename integration_dup_provision_router to integration_duo_provision_router and add route --- backend/app/integrations/duo/routes/provision.py | 4 ++-- backend/app/routers/duo.py | 15 +++++++++++++++ backend/copilot.py | 2 ++ 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 backend/app/routers/duo.py diff --git a/backend/app/integrations/duo/routes/provision.py b/backend/app/integrations/duo/routes/provision.py index 0046631be..834964213 100644 --- a/backend/app/integrations/duo/routes/provision.py +++ b/backend/app/integrations/duo/routes/provision.py @@ -10,10 +10,10 @@ from app.schedulers.models.scheduler import CreateSchedulerRequest from app.schedulers.scheduler import add_scheduler_jobs -integration_dup_provision_router = APIRouter() +integration_duo_provision_router = APIRouter() -@integration_dup_provision_router.post( +@integration_duo_provision_router.post( "/provision", response_model=ProvisionDuoResponse, description="Provision a Duo integration.", diff --git a/backend/app/routers/duo.py b/backend/app/routers/duo.py new file mode 100644 index 000000000..1988dc241 --- /dev/null +++ b/backend/app/routers/duo.py @@ -0,0 +1,15 @@ +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/copilot.py b/backend/copilot.py index 6dc87be12..0470549d0 100644 --- a/backend/copilot.py +++ b/backend/copilot.py @@ -59,6 +59,7 @@ from app.routers import scheduler from app.routers import scoutsuite from app.routers import shuffle +from app.routers import duo from app.routers import smtp from app.routers import stack_provisioning from app.routers import sublime @@ -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) From 034084350411569da03dbd56034d5d77694a6b46 Mon Sep 17 00:00:00 2001 From: Taylor Date: Wed, 19 Jun 2024 17:49:21 -0500 Subject: [PATCH 4/9] add duo auth logs dashboard template --- .../grafana/dashboards/Duo/duo_auth.json | 2130 +++++++++++++++++ 1 file changed, 2130 insertions(+) create mode 100644 backend/app/connectors/grafana/dashboards/Duo/duo_auth.json 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..7ecf84593 --- /dev/null +++ b/backend/app/connectors/grafana/dashboards/Duo/duo_auth.json @@ -0,0 +1,2130 @@ +{ + "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": "" +} From a29834410acf50e122e460c213699f8b26d6d294 Mon Sep 17 00:00:00 2001 From: Taylor Date: Thu, 20 Jun 2024 09:10:34 -0500 Subject: [PATCH 5/9] 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. --- .../app/integrations/duo/routes/provision.py | 4 +- .../app/integrations/duo/schema/provision.py | 2 +- .../app/integrations/modules/routes/duo.py | 96 +++++++++++++++++++ .../app/integrations/modules/schema/duo.py | 78 +++++++++++++++ .../app/integrations/modules/services/duo.py | 21 ++++ backend/app/routers/modules.py | 9 ++ backend/app/schedulers/scheduler.py | 2 + backend/app/schedulers/services/invoke_duo.py | 52 ++++++++++ 8 files changed, 261 insertions(+), 3 deletions(-) create mode 100644 backend/app/integrations/modules/routes/duo.py create mode 100644 backend/app/integrations/modules/schema/duo.py create mode 100644 backend/app/integrations/modules/services/duo.py create mode 100644 backend/app/schedulers/services/invoke_duo.py diff --git a/backend/app/integrations/duo/routes/provision.py b/backend/app/integrations/duo/routes/provision.py index 834964213..85338c43e 100644 --- a/backend/app/integrations/duo/routes/provision.py +++ b/backend/app/integrations/duo/routes/provision.py @@ -40,9 +40,9 @@ async def provision_duo_route( await provision_duo(provision_duo_request, session) await add_scheduler_jobs( CreateSchedulerRequest( - function_name="invoke_duo_integration_collection", + function_name="invoke_duo_integration_collect", time_interval=provision_duo_request.time_interval, - job_id="invoke_duo_integration_collection", + job_id="invoke_duo_integration_collect", ), ) return ProvisionDuoResponse( diff --git a/backend/app/integrations/duo/schema/provision.py b/backend/app/integrations/duo/schema/provision.py index 21713e049..dea6f5d9c 100644 --- a/backend/app/integrations/duo/schema/provision.py +++ b/backend/app/integrations/duo/schema/provision.py @@ -17,7 +17,7 @@ class ProvisionDuoRequest(BaseModel): time_interval: int = Field( 15, description="The time interval for the scheduler.", - examples=[5], + examples=[15], ) integration_name: str = Field( "Duo", diff --git a/backend/app/integrations/modules/routes/duo.py b/backend/app/integrations/modules/routes/duo.py new file mode 100644 index 000000000..0e7312c84 --- /dev/null +++ b/backend/app/integrations/modules/routes/duo.py @@ -0,0 +1,96 @@ +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.middleware.license import get_license +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/modules.py b/backend/app/routers/modules.py index 77bc87ae2..c279bcb2e 100644 --- a/backend/app/routers/modules.py +++ b/backend/app/routers/modules.py @@ -3,6 +3,7 @@ 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 +from app.integrations.modules.routes.duo import module_duo_router router = APIRouter() @@ -23,3 +24,11 @@ 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..f95fca5fb 100644 --- a/backend/app/schedulers/scheduler.py +++ b/backend/app/schedulers/scheduler.py @@ -18,6 +18,7 @@ invoke_carbonblack_integration_collect, ) from app.schedulers.services.invoke_huntress import invoke_huntress_integration_collect +from app.schedulers.services.invoke_duo import invoke_duo_integration_collect from app.schedulers.services.invoke_mimecast import invoke_mimecast_integration from app.schedulers.services.invoke_mimecast import invoke_mimecast_integration_ttp from app.schedulers.services.invoke_sap_siem import ( @@ -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.") From 5caceb9ede4d5c7e598025d4159143aedf2595ea Mon Sep 17 00:00:00 2001 From: Davide Di Modica Date: Thu, 20 Jun 2024 17:39:12 +0200 Subject: [PATCH 6/9] update: integrations api --- .vscode/settings.json | 1 + frontend/src/api/integrations.ts | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) 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/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 }) } From b42cd220c759c1915820b23fa18835d0e6b580c1 Mon Sep 17 00:00:00 2001 From: Davide Di Modica Date: Thu, 20 Jun 2024 17:40:00 +0200 Subject: [PATCH 7/9] update: CustomerIntegrationActions --- .../CustomerIntegrationActions.vue | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) 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", From dc46a9e0410218d55f2c902bfc7a42d08d7f0e04 Mon Sep 17 00:00:00 2001 From: Davide Di Modica Date: Thu, 20 Jun 2024 18:07:26 +0200 Subject: [PATCH 8/9] improve: pinned-page component --- frontend/src/layouts/common/Toolbar/PinnedPages.vue | 9 ++++++--- frontend/src/layouts/common/Toolbar/index.vue | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) 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 => {