diff --git a/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikClassificationEvents.yaml b/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikClassificationEvents.yaml new file mode 100644 index 00000000000..9a727f6efd3 --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikClassificationEvents.yaml @@ -0,0 +1,54 @@ +id: 9563dc15-ea0a-438a-9d50-7ab243378241 +name: Create Incident for Classification events for non DSPM license users +version: 1.0.0 +kind: Scheduled +description: | + 'Creates an incident for Classification events (ClassficationResultsAvailable) for non DSPM license users.' +severity: Medium +status: Available +requiredDataConnectors: + - connectorId: RubrikSecurityCloudAzureFunctions + dataTypes: + - RubrikViolationData +queryFrequency: 5m +queryPeriod: 5m +triggerOperator: gt +triggerThreshold: 0 +tactics: + - Collection +relevantTechniques: + - T1213 +query: | + Rubrik_Violation_Data_CL + | where class_s == "Classification" and custom_details_eventName_s in ("ClassficationResultsAvailable", "ClassificationResultsAvailable") + | extend NewSeverity = case(severity_s=="info", "Low", + severity_s=="warning", "Medium", + severity_s=="critical", "High", + "Informational"), + ObjectId = iff(isnotempty(custom_details_objectId_s), custom_details_objectId_s, tostring(custom_details_objectId_g)), + SnapshotId = custom_details_snapshotId_s +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: P7D + matchingMethod: Selected + groupByCustomDetails: + - ObjectId +eventGroupingSettings: + aggregationKind: AlertPerResult +alertDetailsOverride: + alertDisplayNameFormat: '{{class_s}} event occurred for object {{custom_details_objectName_s}} with type {{custom_details_objectType_s}}' + alertDescriptionFormat: '{{summary_s}}' + alertSeverityColumnName: NewSeverity +customDetails: + ObjectId: ObjectId + ObjectName: custom_details_objectName_s + ObjectType: custom_details_objectType_s + EventType: class_s + EventName: custom_details_eventName_s + Summary: summary_s + EventTimestamp: timestamp_t + Severity: severity_s + SnapshotId: custom_details_snapshotId_s diff --git a/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikDataSecurityViolation.yaml b/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikDataSecurityViolation.yaml new file mode 100644 index 00000000000..eaf077a32a7 --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikDataSecurityViolation.yaml @@ -0,0 +1,59 @@ +id: 5b70b78e-397c-474c-a556-f6c37fccbb18 +name: Create Incident for Data Security Violation events for DSPM license users +version: 1.0.0 +kind: Scheduled +description: | + 'Creates an incident for Data Security Violation events for DSPM license users.' +severity: Medium +status: Available +requiredDataConnectors: + - connectorId: RubrikSecurityCloudAzureFunctions + dataTypes: + - RubrikViolationData +queryFrequency: 5m +queryPeriod: 5m +triggerOperator: gt +triggerThreshold: 0 +tactics: + - Exfiltration +relevantTechniques: + - T1567 +query: | + Rubrik_Violation_Data_CL + | where class_s == "SecurityViolation" and custom_details_eventName_s in ("CriticalSeverityDataViolationDetected", + "CriticalSeverityDataViolationReOpen", + "HighSeverityDataViolationDetected", + "HighSeverityDataViolationReOpen", + "LowOrMediumSeverityDataViolationDetected", + "LowOrMediumSeverityDataViolationReOpen") + | extend NewSeverity = case(severity_s=="info", "Low", + severity_s=="warning", "Medium", + severity_s=="critical", "High", + "Informational"), + ObjectId = iff(isnotempty(custom_details_objectId_s), custom_details_objectId_s, tostring(custom_details_objectId_g)), + ViolationId = iff(isnotempty(custom_details_seriesId_s), custom_details_seriesId_s, tostring(custom_details_seriesId_g)) +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: P7D + matchingMethod: Selected + groupByCustomDetails: + - ViolationId +eventGroupingSettings: + aggregationKind: AlertPerResult +alertDetailsOverride: + alertDisplayNameFormat: '{{class_s}} event occurred for object {{custom_details_objectName_s}} with type {{custom_details_objectType_s}}' + alertDescriptionFormat: '{{summary_s}}' + alertSeverityColumnName: NewSeverity +customDetails: + ObjectId: ObjectId + ObjectName: custom_details_objectName_s + ObjectType: custom_details_objectType_s + EventType: class_s + EventName: custom_details_eventName_s + ViolationId: ViolationId + Summary: summary_s + EventTimestamp: timestamp_t + Severity: severity_s diff --git a/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikIdentitySecurityViolation.yaml b/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikIdentitySecurityViolation.yaml new file mode 100644 index 00000000000..de6b080bc90 --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikIdentitySecurityViolation.yaml @@ -0,0 +1,62 @@ +id: 77e7519f-be7e-41fc-8f02-76bb0a7d5496 +name: Create Incident for Identity Security Violation and Identity Alerts events for IR violations +version: 1.0.0 +kind: Scheduled +description: | + 'Creates an incident for Identity Security Violation and Identity Alerts events for IR violations.' +severity: Medium +status: Available +requiredDataConnectors: + - connectorId: RubrikSecurityCloudAzureFunctions + dataTypes: + - RubrikViolationData +queryFrequency: 5m +queryPeriod: 5m +triggerOperator: gt +triggerThreshold: 0 +tactics: + - InitialAccess +relevantTechniques: + - T1078 +query: | + Rubrik_Violation_Data_CL + | where (class_s == "IdentityViolation" and custom_details_eventName_s in ("CriticalSeverityDataViolationDetected", + "CriticalSeverityDataViolationReOpen", "HighSeverityDataViolationDetected", + "HighSeverityDataViolationReOpen", "LowOrMediumSeverityDataViolationDetected", + "LowOrMediumSeverityDataViolationReOpen")) or + (class_s == "IdentityAlerts" and custom_details_eventName_s in ("CriticalSeverityIdentityAlertDetected", + "CriticalSeverityIdentityAlertReOpened", "HighSeverityIdentityAlertDetected", + "HighSeverityIdentityAlertReOpened", "LowOrMediumSeverityIdentityAlertDetected", + "LowOrMediumSeverityIdentityAlertReOpened" + )) + | extend NewSeverity = case(severity_s=="info", "Low", + severity_s=="warning", "Medium", + severity_s=="critical", "High", + "Informational"), + ObjectId = iff(isnotempty(custom_details_objectId_s), custom_details_objectId_s, tostring(custom_details_objectId_g)), + ViolationId = iff(isnotempty(custom_details_seriesId_s), custom_details_seriesId_s, tostring(custom_details_seriesId_g)) +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: P7D + matchingMethod: Selected + groupByCustomDetails: + - ViolationId +eventGroupingSettings: + aggregationKind: AlertPerResult +alertDetailsOverride: + alertDisplayNameFormat: '{{class_s}} event occurred for object {{custom_details_objectName_s}} with type {{custom_details_objectType_s}}' + alertDescriptionFormat: '{{summary_s}}' + alertSeverityColumnName: NewSeverity +customDetails: + ObjectId: ObjectId + ObjectName: custom_details_objectName_s + ObjectType: custom_details_objectType_s + EventType: class_s + EventName: custom_details_eventName_s + ViolationId: ViolationId + Summary: summary_s + EventTimestamp: timestamp_t + Severity: severity_s diff --git a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikActivity/__init__.py b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikActivity/__init__.py index 1bd28fa7f0d..37a33e0cff9 100644 --- a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikActivity/__init__.py +++ b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikActivity/__init__.py @@ -1,45 +1,58 @@ -"""This __init__ file will be called by Orchastrator function to ingest data in Sentinel.""" +"""This __init__ file is called by the Orchestrator to ingest data into Sentinel.""" import inspect import time from shared_code.logger import applogger -from shared_code.consts import LOGS_STARTS_WITH +from shared_code.consts import LOGS_STARTS_WITH, LOG_FORMAT from shared_code.rubrik_exception import RubrikException from .rubrik import Rubrik +FUNCTION_NAME = "RubrikActivity" + def main(name) -> str: - """Start Execution of Activity Function. + """Start execution of the Activity function. Args: - name (dict): data received via Rubrik Webhook. + name (dict): payload with keys data, log_type, stream_name, rule_id. Returns: - str: status message of activity function. + str: status message of the activity function. """ __method_name = inspect.currentframe().f_code.co_name - try: - applogger.info( - "{}(method={}) Activity function called!".format( - LOGS_STARTS_WITH, __method_name - ) + log_type = name.get("log_type") + applogger.info( + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "Activity function started for log_type={}.".format( + log_type + ), ) + ) + try: start = time.time() rubrik_obj = Rubrik() rubrik_obj.post_data_to_sentinel(name) - end = time.time() + elapsed = int(time.time() - start) applogger.info( - "{}(method={}) time taken for data ingestion is {} sec".format( - LOGS_STARTS_WITH, __method_name, int(end - start) - ) - ) - applogger.info( - "{}(method={}) Activity function Completed!".format( - LOGS_STARTS_WITH, __method_name + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "Data ingestion completed for {} in {} sec.".format(log_type, elapsed), ) ) + return "Data Posted successfully to {}".format(log_type) except RubrikException as err: - return err + applogger.error( + LOG_FORMAT.format(LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, err) + ) + return "Error while posting data to {}: {}".format(log_type, err) except Exception as err: - applogger.error("{}(method={}) {}".format(LOGS_STARTS_WITH, __method_name, err)) - return err - return "Data Posted successfully to {}".format(name.get("log_type")) + applogger.error( + LOG_FORMAT.format(LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, err) + ) + return "Unexpected error while posting data to {}: {}".format( + log_type, err + ) diff --git a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikActivity/rubrik.py b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikActivity/rubrik.py index 812bb861976..31644247401 100644 --- a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikActivity/rubrik.py +++ b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikActivity/rubrik.py @@ -1,88 +1,130 @@ -"""This file contains implementation to ingest Dataminr RTAP alert data into sentinel.""" +"""This file ingests Rubrik webhook data into Sentinel via the Log Ingestion API.""" import inspect +import json from .sentinel import MicrosoftSentinel from shared_code.consts import ( LOGS_STARTS_WITH, + LOG_FORMAT, + AZURE_DATA_COLLECTION_ENDPOINT, + DCR_WEBHOOK_RULE_ID, + DCR_EVENTS_RULE_ID, ANOMALY_LOG_TYPE, RANSOMWARE_LOG_TYPE, THREATHUNT_LOG_TYPE, + EVENTS_LOG_TYPE, + VIOLATION_LOG_TYPE, ) from shared_code.rubrik_exception import RubrikException from shared_code.logger import applogger +FUNCTION_NAME = "RubrikActivity" + class Rubrik: - """This class contains methods to get data from request body pushed via Rubrik Webhook and ingest into Sentinel.""" + """Ingests data received via the Rubrik Webhook into Sentinel.""" def __init__(self) -> None: - """Initialize instance variables for class.""" - self.logs_starts_with = LOGS_STARTS_WITH + """Initialize instance variables and validate environment variables.""" self.microsoftsentinel = MicrosoftSentinel() - self.error_logs = "{}(method={}) {}" self.check_environment_var_existance() def check_environment_var_existance(self): - """To verify that all required environment variables exist. + """Verify all required environment variables are set. Raises: - RubrikException: raise exception if any of the required environment variable is not set. + RubrikException: if any required environment variable is missing. """ __method_name = inspect.currentframe().f_code.co_name env_var = [ {"Anomalies_table_name": ANOMALY_LOG_TYPE}, {"RansomwareAnalysis_table_name": RANSOMWARE_LOG_TYPE}, {"ThreatHunts_table_name": THREATHUNT_LOG_TYPE}, + {"Events_table_name": EVENTS_LOG_TYPE}, + {"Violation_table_name": VIOLATION_LOG_TYPE}, + {"AZURE_DATA_COLLECTION_ENDPOINT": AZURE_DATA_COLLECTION_ENDPOINT}, + {"DCR_WEBHOOK_RULE_ID": DCR_WEBHOOK_RULE_ID}, + {"DCR_EVENTS_RULE_ID": DCR_EVENTS_RULE_ID}, ] try: - applogger.debug( - "{}(method={}) Checking environment variables are exist or not.".format( - self.logs_starts_with, __method_name - ) - ) - for i in env_var: - key, val = next(iter(i.items())) + for item in env_var: + key, val = next(iter(item.items())) if val is None: raise RubrikException( - "{} is not set in the environment please set the environment variable.".format( - key - ) + "{} is not set in the environment; please set the " + "environment variable.".format(key) ) - applogger.debug( - "{}(method={}) All custom environment variables exist.".format( - self.logs_starts_with, __method_name + applogger.info( + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "All required environment variables exist.", ) ) except RubrikException as err: applogger.error( - "{}".format( - self.error_logs.format(self.logs_starts_with, __method_name, err) - ) - ) - raise RubrikException(err) - except Exception as err: - applogger.error( - "{}".format( - self.error_logs.format(self.logs_starts_with, __method_name, err) - ) + LOG_FORMAT.format(LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, err) ) - raise RubrikException(err) + raise def post_data_to_sentinel(self, data): - """To post data received via Rubrik Webhook into Sentinel. + """Post data received via Rubrik Webhook into Sentinel. Args: - data (dict): data received via Rubrik Webhook. + data (dict): payload with keys data, log_type, stream_name, rule_id. """ __method_name = inspect.currentframe().f_code.co_name try: - sentinel_obj = MicrosoftSentinel() body = data.get("data") + stream_name = data.get("stream_name") + rule_id = data.get("rule_id") log_type = data.get("log_type") - sentinel_obj.post_data(body, log_type) + + applogger.info( + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "Processing payload for log_type={}, rule_id={}.".format( + log_type, rule_id + ), + ) + ) + + if isinstance(body, str): + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "Parsing webhook payload from JSON string (length={} bytes).".format( + len(body) + ), + ) + ) + body = json.loads(body) + + events = body if isinstance(body, list) else [body] + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "Posting {} event(s) to table {} via Log Ingestion API.".format( + len(events), log_type + ), + ) + ) + self.microsoftsentinel.post_data(events, stream_name, rule_id) + except json.JSONDecodeError as err: + applogger.error( + LOG_FORMAT.format(LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, err) + ) + raise RubrikException("Invalid JSON in webhook payload: {}".format(err)) + except RubrikException: + raise except Exception as err: applogger.error( - "{}".format( - self.error_logs.format(self.logs_starts_with, __method_name, err) - ) + LOG_FORMAT.format(LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, err) ) - raise RubrikException(err) + raise RubrikException(str(err)) diff --git a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikActivity/sentinel.py b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikActivity/sentinel.py index 93287166268..fd9a2a0692c 100644 --- a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikActivity/sentinel.py +++ b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikActivity/sentinel.py @@ -1,119 +1,142 @@ -"""This file contains AzureSentinel class which is used to post data into log analytics workspace.""" -import base64 -import datetime -import hashlib -import hmac -import requests -from ..shared_code.consts import LOGS_STARTS_WITH, WORKSPACE_ID, WORKSPACE_KEY +"""This file contains MicrosoftSentinel class which posts data via the Log Ingestion API.""" +import inspect +import time +from azure.core.exceptions import HttpResponseError +from azure.identity import DefaultAzureCredential, AzureAuthorityHosts +from azure.monitor.ingestion import LogsIngestionClient +from ..shared_code.consts import ( + LOGS_STARTS_WITH, + LOG_FORMAT, + AZURE_DATA_COLLECTION_ENDPOINT, + SCOPE, + MAX_RETRIES, + RETRYABLE_STATUS, +) from ..shared_code.logger import applogger -from ..shared_code.rubrik_exception import RubrikException +from ..shared_code.rubrik_exception import ( + RubrikException, + RubrikAuthenticationException, +) + +FUNCTION_NAME = "RubrikActivity" class MicrosoftSentinel: - """AzureSentinel class is used to post data into log Analytics workspace.""" + """Posts data into a Log Analytics custom table via the Log Ingestion API.""" def __init__(self) -> None: - """Intialize instance variables for MicrosoftSentinel class.""" - self.logs_start_with = "{}(MicrosoftSentinel)".format(LOGS_STARTS_WITH) - self.customer_id = WORKSPACE_ID - self.shared_key = WORKSPACE_KEY - - def build_signature( - self, - date, - content_length, - method, - content_type, - resource, - ): - """To build signature which is required in header.""" - x_headers = "x-ms-date:" + date - string_to_hash = ( - method - + "\n" - + str(content_length) - + "\n" - + content_type - + "\n" - + x_headers - + "\n" - + resource - ) - bytes_to_hash = bytes(string_to_hash, encoding="utf-8") - decoded_key = base64.b64decode(self.shared_key) - encoded_hash = base64.b64encode( - hmac.new(decoded_key, bytes_to_hash, digestmod=hashlib.sha256).digest() - ).decode() - authorization = "SharedKey {}:{}".format(self.customer_id, encoded_hash) - return authorization + """Initialize the LogsIngestionClient with cloud-aware credentials.""" + __method_name = inspect.currentframe().f_code.co_name + self.endpoint = AZURE_DATA_COLLECTION_ENDPOINT + self.scope = SCOPE + is_gov = ".us" in (self.scope or "").lower() or ".us" in (self.endpoint or "").lower() + try: + if is_gov: + credential = DefaultAzureCredential( + authority=AzureAuthorityHosts.AZURE_GOVERNMENT + ) + credential_scopes = ["https://monitor.azure.us//.default"] + else: + credential = DefaultAzureCredential() + credential_scopes = ["https://monitor.azure.com//.default"] + self.client = LogsIngestionClient( + endpoint=self.endpoint, + credential=credential, + credential_scopes=credential_scopes, + ) + except Exception as err: + applogger.error( + LOG_FORMAT.format(LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, err) + ) + raise RubrikAuthenticationException( + "Failed to initialize LogsIngestionClient: {}".format(err) + ) - # Build and send a request to the POST API - def post_data(self, body, log_type): - """Build and send a request to the POST API. + def post_data(self, events, stream_name, rule_id): + """Upload events to the given DCR stream with bounded retry. Args: - body (str): Data to post into Sentinel log analytics workspace - log_type (str): Custom log table name in which data wil be added. + events (list): list of event dicts to ingest. + stream_name (str): DCR stream name (Custom-{TableName}). + rule_id (str): DCR immutable id. - Returns: - status_code: Returns the response status code got while posting data to sentinel. + Raises: + RubrikException: on non-retryable errors or after exhausting retries. """ - method = "POST" - content_type = "application/json" - resource = "/api/logs" - rfc1123date = datetime.datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT") - content_length = len(body) - try: - signature = self.build_signature( - rfc1123date, - content_length, - method, - content_type, - resource, - ) - except Exception as err: - applogger.error("{} Error occurred: {}".format(self.logs_start_with, err)) - raise RubrikException( - "Error while generating signature for posting data into log analytics." + __method_name = inspect.currentframe().f_code.co_name + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "Uploading batch of {} event(s) to stream {} (rule_id={}).".format( + len(events), stream_name, rule_id + ), ) - uri = ( - "https://" - + self.customer_id - + ".ods.opinsights.azure.com" - + resource - + "?api-version=2016-04-01" ) - - headers = { - "content-type": content_type, - "Authorization": signature, - "Log-Type": log_type, - "x-ms-date": rfc1123date, - } - try: - response = requests.post(uri, data=body, headers=headers) - if response.status_code >= 200 and response.status_code <= 299: + attempt = 0 + while True: + try: + self.client.upload( + rule_id=rule_id, stream_name=stream_name, logs=events + ) applogger.info( - "{} Data posted successfully to {} table in Log Analytics Workspace.".format( - self.logs_start_with, log_type + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "Successfully posted {} event(s) to stream {} on attempt {}.".format( + len(events), stream_name, attempt + 1 + ), ) ) - else: - applogger.info( - "Response code: {} from posting data to log analytics.\nError: {}".format( - response.status_code, response.content + return + except HttpResponseError as err: + status = getattr(err, "status_code", None) + if status in RETRYABLE_STATUS and attempt < MAX_RETRIES: + attempt += 1 + backoff = 2 ** attempt + applogger.warning( + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "Retryable status {} on attempt {}; sleeping {}s.".format( + status, attempt, backoff + ), + ) + ) + time.sleep(backoff) + continue + if status == 401 and attempt < MAX_RETRIES: + attempt += 1 + backoff = 2 ** attempt + applogger.warning( + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "Unauthorized (401) on attempt {}; sleeping {}s before retry.".format( + attempt, backoff + ), + ) + ) + time.sleep(backoff) + continue + applogger.error( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, err ) ) raise RubrikException( - "Response code: {} from posting data to log analytics.\nError: {}".format( - response.status_code, response.content + "Log Ingestion API error (status={}): {}".format(status, err) + ) + except Exception as err: + applogger.error( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, err ) ) - except RubrikException as error: - applogger.error("{} Error:{}".format(self.logs_start_with, error)) - raise RubrikException( - "RubrikException: Error while posting data to sentinel." - ) - except Exception as error: - applogger.error("{} Error:{}".format(self.logs_start_with, error)) - raise RubrikException() + raise RubrikException( + "Unexpected error while posting data to Sentinel: {}".format(err) + ) diff --git a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikAnomalyOrchestrator/__init__.py b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikAnomalyOrchestrator/__init__.py index 26de6b4f7db..47da9a416c6 100644 --- a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikAnomalyOrchestrator/__init__.py +++ b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikAnomalyOrchestrator/__init__.py @@ -1,25 +1,61 @@ -"""This __init__ file will be called by Http Starter function to pass the Anomaly data to activity function.""" +"""This __init__ file is called by the Http Starter to pass Anomaly data to the activity function.""" +import inspect import azure.durable_functions as df -from shared_code.consts import ANOMALY_LOG_TYPE, LOGS_STARTS_WITH +from shared_code.consts import ( + ANOMALY_LOG_TYPE, + ANOMALY_STREAM, + DCR_WEBHOOK_RULE_ID, + LOGS_STARTS_WITH, + LOG_FORMAT, +) from shared_code.logger import applogger +FUNCTION_NAME = "RubrikAnomalyOrchestrator" + def orchestrator_function(context: df.DurableOrchestrationContext): - """Get Anomaly data from durable orchestration context and schedule an activity for execution. + """Route Anomaly data from the orchestration context to the activity function. Args: - context (df.DurableOrchestrationContext): Context of the durable orchestration execution. + context (df.DurableOrchestrationContext): durable orchestration context. Returns: - str: result of Activity function + str: result of the Activity function. """ - applogger.info("{} AnomalyOrchestrator function called!".format(LOGS_STARTS_WITH)) + __method_name = inspect.currentframe().f_code.co_name + applogger.info( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, "function called!" + ) + ) json_data = context.get_input() + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, + "Routing Anomaly data to RubrikActivity (log_type={}, stream={}).".format( + ANOMALY_LOG_TYPE, ANOMALY_STREAM + ), + ) + ) result1 = yield context.call_activity( - "RubrikActivity", {"data": json_data, "log_type": ANOMALY_LOG_TYPE} + "RubrikActivity", + { + "data": json_data, + "log_type": ANOMALY_LOG_TYPE, + "stream_name": ANOMALY_STREAM, + "rule_id": DCR_WEBHOOK_RULE_ID, + }, + ) + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, + "RubrikActivity returned: {}".format(result1), + ) ) applogger.info( - "{} AnomalyOrchestrator function completed!".format(LOGS_STARTS_WITH) + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, "function completed!" + ) ) return result1 diff --git a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikClassificationActivity/__init__.py b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikClassificationActivity/__init__.py new file mode 100644 index 00000000000..9c9bc482997 --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikClassificationActivity/__init__.py @@ -0,0 +1,79 @@ +"""Activity function: RSC classification hit-count gate. + +Input: {"objectId": str, "eventTimestamp": str} +Output: {"snapshotId": str|None, "hitCount": int, "error": str|None} + +Never raises to the orchestrator — on any failure returns hitCount 0 with an error +string (fail-closed: the orchestrator will then skip ingestion). +""" +import inspect +from shared_code.logger import applogger +from shared_code.consts import LOGS_STARTS_WITH, LOG_FORMAT +from shared_code.rubrik_exception import RubrikException +from .rubrik_dspm import RubrikDSPM + +FUNCTION_NAME = "RubrikClassificationActivity" + + +def main(name) -> dict: + """Resolve snapshot and count policy hits for a classification event. + + Args: + name (dict): {"objectId": str, "eventTimestamp": str}. + + Returns: + dict: {"snapshotId": str|None, "hitCount": int, "error": str|None}. + """ + __method_name = inspect.currentframe().f_code.co_name + object_id = name.get("objectId") + event_timestamp = name.get("eventTimestamp") + applogger.info( + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "Classification gate invoked for object_id={}, event_timestamp={}.".format( + object_id, event_timestamp + ), + ) + ) + try: + if not object_id or not event_timestamp: + applogger.warning( + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "Missing required parameter: objectId={}, eventTimestamp={}.".format( + object_id, event_timestamp + ), + ) + ) + return { + "snapshotId": None, + "hitCount": 0, + "error": "missing objectId or eventTimestamp", + } + client = RubrikDSPM() + result = client.get_hit_count(object_id, event_timestamp) + applogger.info( + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "Classification gate result for object {}: hitCount={}, snapshotId={}, error={}".format( + object_id, result.get("hitCount"), result.get("snapshotId"), result.get("error") + ), + ) + ) + return result + except RubrikException as err: + applogger.error( + LOG_FORMAT.format(LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, err) + ) + return {"snapshotId": None, "hitCount": 0, "error": str(err)} + except Exception as err: + applogger.error( + LOG_FORMAT.format(LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, err) + ) + return {"snapshotId": None, "hitCount": 0, "error": str(err)} diff --git a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikClassificationActivity/function.json b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikClassificationActivity/function.json new file mode 100644 index 00000000000..1b03f11007c --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikClassificationActivity/function.json @@ -0,0 +1,10 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "name": "name", + "type": "activityTrigger", + "direction": "in" + } + ] +} diff --git a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikClassificationActivity/rubrik_dspm.py b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikClassificationActivity/rubrik_dspm.py new file mode 100644 index 00000000000..ebb330b7236 --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikClassificationActivity/rubrik_dspm.py @@ -0,0 +1,233 @@ +"""Rubrik Security Cloud (RSC) client used by the Violation classification gate. + +Resolves a snapshot for an object and returns its policy-hit count. Used only to +decide whether to ingest the original webhook payload; it ingests nothing itself. +""" +import inspect +import datetime +import requests +from shared_code.consts import ( + LOGS_STARTS_WITH, + LOG_FORMAT, + RUBRIK_INSTANCE_URL, + RUBRIK_CLIENT_ID, + RUBRIK_CLIENT_SECRET, + RSC_HTTP_TIMEOUT, + RSC_RESOLVE_SNAPSHOT_QUERY, + RSC_HIT_COUNT_QUERY, +) +from shared_code.logger import applogger +from shared_code.rubrik_exception import ( + RubrikException, + RubrikAuthenticationException, +) + +FUNCTION_NAME = "RubrikClassificationActivity" + + +class RubrikDSPM: + """Minimal RSC GraphQL client for the classification hit-count gate.""" + + def __init__(self): + """Validate RSC config and prepare endpoints.""" + __method_name = inspect.currentframe().f_code.co_name + if not (RUBRIK_INSTANCE_URL and RUBRIK_CLIENT_ID and RUBRIK_CLIENT_SECRET): + message = ( + "RSC configuration missing: set RubrikInstanceUrl, RubrikClientId, " + "RubrikClientSecret app settings." + ) + applogger.error( + LOG_FORMAT.format(LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, message) + ) + raise RubrikException(message) + self.base_url = RUBRIK_INSTANCE_URL.rstrip("/") + self.token_url = "{}/api/client_token".format(self.base_url) + self.graphql_url = "{}/api/graphql".format(self.base_url) + self.token = None + + def get_token(self): + """Exchange client credentials for a bearer token.""" + __method_name = inspect.currentframe().f_code.co_name + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "Requesting RSC bearer token: POST {}".format(self.token_url), + ) + ) + resp = requests.post( + self.token_url, + json={"client_id": RUBRIK_CLIENT_ID, "client_secret": RUBRIK_CLIENT_SECRET}, + headers={"Content-Type": "application/json"}, + timeout=RSC_HTTP_TIMEOUT, + ) + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "RSC token response: HTTP {}".format(resp.status_code), + ) + ) + if resp.status_code == 401: + raise RubrikAuthenticationException("RSC token request returned 401 (bad credentials).") + if resp.status_code < 200 or resp.status_code > 299: + raise RubrikException( + "RSC token request failed: {} {}".format(resp.status_code, resp.text) + ) + self.token = resp.json().get("access_token") + if not self.token: + raise RubrikAuthenticationException("RSC token response missing access_token.") + return self.token + + def _graphql(self, query, variables): + """POST a GraphQL query, checking HTTP status and the errors array.""" + resp = requests.post( + self.graphql_url, + json={"query": query, "variables": variables}, + headers={ + "Authorization": "Bearer {}".format(self.token), + "Content-Type": "application/json", + }, + timeout=RSC_HTTP_TIMEOUT, + ) + if resp.status_code == 401: + raise RubrikAuthenticationException("RSC GraphQL returned 401.") + if resp.status_code < 200 or resp.status_code > 299: + raise RubrikException( + "RSC GraphQL HTTP error: {} {}".format(resp.status_code, resp.text) + ) + body = resp.json() + if body.get("errors"): + raise RubrikException("RSC GraphQL errors: {}".format(body["errors"])) + return body.get("data", {}) + + @staticmethod + def _round_up_to_minute(event_timestamp): + """Round an ISO-8601 timestamp UP to the next whole minute, return ISO Z string.""" + ts = event_timestamp.replace("Z", "+00:00") + dt = datetime.datetime.fromisoformat(ts) + if dt.second or dt.microsecond: + dt = dt.replace(second=0, microsecond=0) + datetime.timedelta(minutes=1) + return dt.astimezone(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + def resolve_snapshot(self, object_id, event_timestamp): + """Return the snapshot id closest to (before) the rounded-up event time, or None.""" + __method_name = inspect.currentframe().f_code.co_name + before_time = self._round_up_to_minute(event_timestamp) + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "Resolving snapshot: object_id={}, event_timestamp={}, " + "beforeTime={}".format(object_id, event_timestamp, before_time), + ) + ) + data = self._graphql( + RSC_RESOLVE_SNAPSHOT_QUERY, + {"snappableId": object_id, "beforeTime": before_time}, + ) + results = data.get("allSnapshotsClosestToPointInTime") or [] + if not results: + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "No snapshot returned for object_id={} (beforeTime={}).".format( + object_id, before_time + ), + ) + ) + return None + first = results[0] or {} + if first.get("error"): + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "Snapshot resolution error for object_id={} (beforeTime={}): " + "{}".format(object_id, before_time, first.get("error")), + ) + ) + return None + snap = first.get("snapshot") or {} + snapshot_id = snap.get("id") + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "Resolved snapshot_id={} for object_id={} (snapshot date={}, " + "beforeTime={}).".format( + snapshot_id, object_id, snap.get("date"), before_time + ), + ) + ) + return snapshot_id + + def count_hits(self, object_id, snapshot_id): + """Return policyObj.rootFileResult.hits.totalHits (0 if absent).""" + __method_name = inspect.currentframe().f_code.co_name + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "Counting hits: object_id={}, snapshot_id={}".format( + object_id, snapshot_id + ), + ) + ) + data = self._graphql( + RSC_HIT_COUNT_QUERY, + {"snappableFid": object_id, "snapshotFid": snapshot_id}, + ) + policy_obj = data.get("policyObj") or {} + root = policy_obj.get("rootFileResult") or {} + hits = root.get("hits") or {} + total = hits.get("totalHits") + hit_count = int(total) if total is not None else 0 + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "Hit count for object_id={}, snapshot_id={}: totalHits={}".format( + object_id, snapshot_id, hit_count + ), + ) + ) + return hit_count + + def get_hit_count(self, object_id, event_timestamp): + """Full gate: token -> resolve snapshot -> count hits. Never raises (fail-closed).""" + __method_name = inspect.currentframe().f_code.co_name + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, + FUNCTION_NAME, + __method_name, + "Starting hit-count gate for object_id={}, " + "event_timestamp={}".format(object_id, event_timestamp), + ) + ) + try: + self.get_token() + snapshot_id = self.resolve_snapshot(object_id, event_timestamp) + if not snapshot_id: + return { + "snapshotId": None, + "hitCount": 0, + "error": "snapshot not resolved for object {}".format(object_id), + } + hit_count = self.count_hits(object_id, snapshot_id) + return {"snapshotId": snapshot_id, "hitCount": hit_count, "error": None} + except (RubrikException, requests.RequestException, ValueError) as err: + applogger.error( + LOG_FORMAT.format(LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, err) + ) + return {"snapshotId": None, "hitCount": 0, "error": str(err)} diff --git a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikDataConnector.zip b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikDataConnector.zip new file mode 100644 index 00000000000..cf98617412d Binary files /dev/null and b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikDataConnector.zip differ diff --git a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikEventsOrchestrator/__init__.py b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikEventsOrchestrator/__init__.py index 07ac317f781..14bd0189006 100644 --- a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikEventsOrchestrator/__init__.py +++ b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikEventsOrchestrator/__init__.py @@ -1,26 +1,61 @@ -"""This __init__ file will be called by Http Starter function to pass the Other Events data to activity function.""" +"""This __init__ file is called by the Http Starter to pass Events data to the activity function.""" +import inspect import azure.durable_functions as df -from shared_code.consts import EVENTS_LOG_TYPE, LOGS_STARTS_WITH +from shared_code.consts import ( + EVENTS_LOG_TYPE, + EVENTS_STREAM, + DCR_EVENTS_RULE_ID, + LOGS_STARTS_WITH, + LOG_FORMAT, +) from shared_code.logger import applogger +FUNCTION_NAME = "RubrikEventsOrchestrator" + def orchestrator_function(context: df.DurableOrchestrationContext): - """Get General data from durable orchestration context and schedule an activity for execution. + """Route Events data from the orchestration context to the activity function. Args: - context (df.DurableOrchestrationContext): Context of the durable orchestration execution. + context (df.DurableOrchestrationContext): durable orchestration context. Returns: - str: result of Activity function + str: result of the Activity function. """ - applogger.info("{} RubrikEventOrchestrator function called!".format(LOGS_STARTS_WITH)) + __method_name = inspect.currentframe().f_code.co_name + applogger.info( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, "function called!" + ) + ) json_data = context.get_input() - + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, + "Routing Events data to RubrikActivity (log_type={}, stream={}).".format( + EVENTS_LOG_TYPE, EVENTS_STREAM + ), + ) + ) result1 = yield context.call_activity( - "RubrikActivity", {"data": json_data, "log_type": EVENTS_LOG_TYPE} + "RubrikActivity", + { + "data": json_data, + "log_type": EVENTS_LOG_TYPE, + "stream_name": EVENTS_STREAM, + "rule_id": DCR_EVENTS_RULE_ID, + }, + ) + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, + "RubrikActivity returned: {}".format(result1), + ) ) applogger.info( - "{} RubrikEventOrchestrator function completed!".format(LOGS_STARTS_WITH) + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, "function completed!" + ) ) return result1 diff --git a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikHttpStarter/__init__.py b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikHttpStarter/__init__.py index d6f9694c338..1bef2884075 100644 --- a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikHttpStarter/__init__.py +++ b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikHttpStarter/__init__.py @@ -3,10 +3,12 @@ import json import azure.functions as func import azure.durable_functions as df -from shared_code.consts import LOGS_STARTS_WITH +from shared_code.consts import LOGS_STARTS_WITH, LOG_FORMAT from shared_code.logger import applogger from shared_code.rubrik_exception import RubrikException +FUNCTION_NAME = "RubrikHttpStarter" + def get_data_from_request_body(request): """Get data from request body. @@ -23,10 +25,10 @@ def get_data_from_request_body(request): json_data = json.dumps(data) return json_data except ValueError as value_error: - applogger.error("{}(method={}) {}".format(LOGS_STARTS_WITH, __method_name, value_error)) + applogger.error(LOG_FORMAT.format(LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, value_error)) raise RubrikException(value_error) except Exception as err: - applogger.error("{}(method={}) {}".format(LOGS_STARTS_WITH, __method_name, err)) + applogger.error(LOG_FORMAT.format(LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, err)) raise RubrikException(err) @@ -41,17 +43,31 @@ async def main(req: func.HttpRequest, starter: str) -> func.HttpResponse: func.HttpResponse: Status of Http request process (successful/failed). """ __method_name = inspect.currentframe().f_code.co_name + applogger.info( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, + "HTTP trigger received; routing to orchestrator '{}'.".format( + req.route_params.get("functionName", "unknown") + ), + ) + ) try: - applogger.debug("{} HttpStarter Function Called.".format(LOGS_STARTS_WITH)) data = get_data_from_request_body(req) if data: + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, + "Request body parsed successfully (size={} bytes).".format(len(data)), + ) + ) client = df.DurableOrchestrationClient(starter) instance_id = await client.start_new( req.route_params["functionName"], client_input=data ) applogger.info( - "{} Started orchestration with ID = '{}'.".format( - LOGS_STARTS_WITH, instance_id + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, + "Durable orchestration started with ID = '{}'.".format(instance_id) ) ) body = "Data Received successfully via Rubrik Webhook." @@ -61,7 +77,7 @@ async def main(req: func.HttpRequest, starter: str) -> func.HttpResponse: headers={"Content-Length": str(len(body))}, ) else: - applogger.info("{}(method={})No required data found.".format(LOGS_STARTS_WITH, __method_name)) + applogger.info(LOG_FORMAT.format(LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, "No required data found.")) body = "No required data found." return func.HttpResponse( body=body, diff --git a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikRansomwareOrchestrator/__init__.py b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikRansomwareOrchestrator/__init__.py index 95c502770b7..61b40597933 100644 --- a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikRansomwareOrchestrator/__init__.py +++ b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikRansomwareOrchestrator/__init__.py @@ -1,27 +1,61 @@ -"""This __init__ file will be called by Http Starter function to pass the Ransomware data to activity function.""" +"""This __init__ file is called by the Http Starter to pass Ransomware data to the activity function.""" +import inspect import azure.durable_functions as df -from shared_code.consts import RANSOMWARE_LOG_TYPE, LOGS_STARTS_WITH +from shared_code.consts import ( + RANSOMWARE_LOG_TYPE, + RANSOMWARE_STREAM, + DCR_WEBHOOK_RULE_ID, + LOGS_STARTS_WITH, + LOG_FORMAT, +) from shared_code.logger import applogger +FUNCTION_NAME = "RubrikRansomwareOrchestrator" + def orchestrator_function(context: df.DurableOrchestrationContext): - """Get Ransomware data from durable orchestration context and schedule an activity for execution. + """Route Ransomware data from the orchestration context to the activity function. Args: - context (df.DurableOrchestrationContext): Context of the durable orchestration execution. + context (df.DurableOrchestrationContext): durable orchestration context. Returns: - str: result of Activity function + str: result of the Activity function. """ + __method_name = inspect.currentframe().f_code.co_name applogger.info( - "{} RansomwareOrchestrator function called!".format(LOGS_STARTS_WITH) + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, "function called!" + ) ) json_data = context.get_input() + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, + "Routing Ransomware data to RubrikActivity (log_type={}, stream={}).".format( + RANSOMWARE_LOG_TYPE, RANSOMWARE_STREAM + ), + ) + ) result1 = yield context.call_activity( - "RubrikActivity", {"data": json_data, "log_type": RANSOMWARE_LOG_TYPE} + "RubrikActivity", + { + "data": json_data, + "log_type": RANSOMWARE_LOG_TYPE, + "stream_name": RANSOMWARE_STREAM, + "rule_id": DCR_WEBHOOK_RULE_ID, + }, + ) + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, + "RubrikActivity returned: {}".format(result1), + ) ) applogger.info( - "{} RansomwareOrchestrator function completed!".format(LOGS_STARTS_WITH) + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, "function completed!" + ) ) return result1 diff --git a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikThreathuntOrchestrator/__init__.py b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikThreathuntOrchestrator/__init__.py index df9387c8c25..489ae3080c8 100644 --- a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikThreathuntOrchestrator/__init__.py +++ b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikThreathuntOrchestrator/__init__.py @@ -1,27 +1,61 @@ -"""This __init__ file will be called by Http Starter function to pass the ThreatHunt data to activity function.""" +"""This __init__ file is called by the Http Starter to pass ThreatHunt data to the activity function.""" +import inspect import azure.durable_functions as df -from shared_code.consts import THREATHUNT_LOG_TYPE, LOGS_STARTS_WITH +from shared_code.consts import ( + THREATHUNT_LOG_TYPE, + THREATHUNT_STREAM, + DCR_WEBHOOK_RULE_ID, + LOGS_STARTS_WITH, + LOG_FORMAT, +) from shared_code.logger import applogger +FUNCTION_NAME = "RubrikThreatHuntOrchestrator" + def orchestrator_function(context: df.DurableOrchestrationContext): - """Get ThreatHunt data from durable orchestration context and schedule an activity for execution. + """Route ThreatHunt data from the orchestration context to the activity function. Args: - context (df.DurableOrchestrationContext): Context of the durable orchestration execution. + context (df.DurableOrchestrationContext): durable orchestration context. Returns: - str: result of Activity function + str: result of the Activity function. """ + __method_name = inspect.currentframe().f_code.co_name applogger.info( - "{} ThreatHuntOrchestrator function called!".format(LOGS_STARTS_WITH) + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, "function called!" + ) ) json_data = context.get_input() + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, + "Routing ThreatHunt data to RubrikActivity (log_type={}, stream={}).".format( + THREATHUNT_LOG_TYPE, THREATHUNT_STREAM + ), + ) + ) result1 = yield context.call_activity( - "RubrikActivity", {"data": json_data, "log_type": THREATHUNT_LOG_TYPE} + "RubrikActivity", + { + "data": json_data, + "log_type": THREATHUNT_LOG_TYPE, + "stream_name": THREATHUNT_STREAM, + "rule_id": DCR_WEBHOOK_RULE_ID, + }, + ) + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, + "RubrikActivity returned: {}".format(result1), + ) ) applogger.info( - "{} ThreatHuntOrchestrator function completed!".format(LOGS_STARTS_WITH) + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, "function completed!" + ) ) return result1 diff --git a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikViolationOrchestrator/__init__.py b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikViolationOrchestrator/__init__.py new file mode 100644 index 00000000000..c01f2f92643 --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikViolationOrchestrator/__init__.py @@ -0,0 +1,181 @@ +"""This __init__ file is called by the Http Starter to pass Violation data to the activity function. + +For classification-results events, an RSC hit-count gate (RubrikClassificationActivity) decides +whether to ingest: the original webhook payload is ingested only when policy hits > 0. +""" +import inspect +import json +import azure.durable_functions as df +from shared_code.consts import ( + VIOLATION_LOG_TYPE, + VIOLATION_STREAM, + DCR_WEBHOOK_RULE_ID, + LOGS_STARTS_WITH, + LOG_FORMAT, + CLASSIFICATION_CLASS, + CLASSIFICATION_EVENT_NAME, +) +from shared_code.logger import applogger + +FUNCTION_NAME = "RubrikViolationOrchestrator" + + +def orchestrator_function(context: df.DurableOrchestrationContext): + """Route Violation data; gate classification events on RSC policy-hit count. + + Args: + context (df.DurableOrchestrationContext): durable orchestration context. + + Returns: + str: result of the Activity function, or a status string when gated out. + """ + __method_name = inspect.currentframe().f_code.co_name + applogger.info( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, "function called!" + ) + ) + json_data = context.get_input() + + try: + payload = json.loads(json_data) if isinstance(json_data, str) else json_data + except (ValueError, TypeError) as err: + applogger.error( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, + "Failed to parse webhook payload as JSON; treating as empty: {}".format(err), + ) + ) + payload = {} + if not isinstance(payload, dict): + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, + "Webhook payload is not a JSON object (got {}); treating as empty.".format( + type(payload).__name__ + ), + ) + ) + payload = {} + custom_details = payload.get("custom_details") or {} + + event_class = payload.get("class") + event_name = custom_details.get("eventName") + + ingest_input = { + "data": json_data, + "log_type": VIOLATION_LOG_TYPE, + "stream_name": VIOLATION_STREAM, + "rule_id": DCR_WEBHOOK_RULE_ID, + } + + if event_class == CLASSIFICATION_CLASS: + object_name = custom_details.get("objectName") + object_type = custom_details.get("objectType") + + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, + "Classification event detected: eventName={}, object={}, " + "objectType={}.".format(event_name, object_name, object_type), + ) + ) + + if event_name != CLASSIFICATION_EVENT_NAME: + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, + "Classification event '{}' for object {} of type {} is not '{}'; " + "not ingested.".format( + event_name, + object_name, + object_type, + CLASSIFICATION_EVENT_NAME, + ), + ) + ) + return "Classification event is not {}; not ingested.".format( + CLASSIFICATION_EVENT_NAME + ) + + object_id = custom_details.get("objectId") + event_timestamp = payload.get("timestamp") + + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, + "Invoking hit-count gate (RubrikClassificationActivity) for " + "objectId={}, eventTimestamp={}.".format(object_id, event_timestamp), + ) + ) + gate = yield context.call_activity( + "RubrikClassificationActivity", + {"objectId": object_id, "eventTimestamp": event_timestamp}, + ) + hit_count = (gate or {}).get("hitCount", 0) + snapshot_id = (gate or {}).get("snapshotId") + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, + "Hit-count gate result for object {}: hitCount={}, snapshotId={}, " + "error={}.".format( + object_name, hit_count, snapshot_id, (gate or {}).get("error") + ), + ) + ) + + if hit_count > 0: + # Enrich the original webhook payload with the resolved snapshotId + # (under custom_details) so it lands in the SnapshotId column. + enriched = dict(payload) + enriched_details = dict(custom_details) + enriched_details["snapshotId"] = snapshot_id + enriched["custom_details"] = enriched_details + classification_ingest_input = { + "data": json.dumps(enriched), + "log_type": VIOLATION_LOG_TYPE, + "stream_name": VIOLATION_STREAM, + "rule_id": DCR_WEBHOOK_RULE_ID, + } + result1 = yield context.call_activity( + "RubrikActivity", classification_ingest_input + ) + applogger.info( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, + "Classification event for object {} of type {} had {} hit(s) " + "(snapshot {}); ingested.".format( + object_name, object_type, hit_count, snapshot_id + ), + ) + ) + return result1 + + applogger.info( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, + "No violations/hits found for object {} of type {} for snapshot {} " + "at time {}; not ingested.".format( + object_name, object_type, snapshot_id, event_timestamp + ), + ) + ) + return "No violations/hits found; classification event not ingested." + + applogger.debug( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, + "Non-classification Violation event (class={}); routing to " + "RubrikActivity.".format(event_class), + ) + ) + result1 = yield context.call_activity("RubrikActivity", ingest_input) + applogger.info( + LOG_FORMAT.format( + LOGS_STARTS_WITH, FUNCTION_NAME, __method_name, "function completed!" + ) + ) + return result1 + + +main = df.Orchestrator.create(orchestrator_function) diff --git a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikViolationOrchestrator/function.json b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikViolationOrchestrator/function.json new file mode 100644 index 00000000000..82fabb9a853 --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikViolationOrchestrator/function.json @@ -0,0 +1,10 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "name": "context", + "type": "orchestrationTrigger", + "direction": "in" + } + ] +} \ No newline at end of file diff --git a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikWebhookEvents_FunctionApp.json b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikWebhookEvents_FunctionApp.json index 834727ab189..f3c32b453fe 100644 --- a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikWebhookEvents_FunctionApp.json +++ b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikWebhookEvents_FunctionApp.json @@ -23,6 +23,11 @@ "metricName": "Total Other Events data received", "legend": "Rubrik_Events_Data_CL", "baseQuery": "Rubrik_Events_Data_CL" + }, + { + "metricName": "Total Violation Event data received", + "legend": "Rubrik_Violation_Data_CL", + "baseQuery": "Rubrik_Violation_Data_CL" } ], "sampleQueries": [ @@ -41,6 +46,10 @@ { "description": "Rubrik Other Events - Other Events for all severity types.", "query": "Rubrik_Events_Data_CL\n | sort by TimeGenerated desc" + }, + { + "description": "Rubrik Violation Events - Violation Events for all severity types.", + "query": "Rubrik_Violation_Data_CL\n | sort by TimeGenerated desc" } ], "dataTypes": [ @@ -59,6 +68,10 @@ { "name": "Rubrik_Events_Data_CL", "lastDataReceivedQuery": "Rubrik_Events_Data_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" + }, + { + "name": "Rubrik_Violation_Data_CL", + "lastDataReceivedQuery": "Rubrik_Violation_Data_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" } ], "connectivityCriterias": [ @@ -85,6 +98,12 @@ "value": [ "Rubrik_Events_Data_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" ] + }, + { + "type": "IsConnectedQuery", + "value": [ + "Rubrik_Violation_Data_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" + ] } ], "availability": { @@ -132,31 +151,11 @@ }, { "title": "", - "description": "**STEP 1 - Choose ONE from the following two deployment options to deploy the connector and the associated Azure Function**\n\n>**IMPORTANT:** Before deploying the Rubrik Microsoft Sentinel data connector, have the Workspace ID and Workspace Primary Key (can be copied from the following) readily available..", - "instructions": [ - { - "parameters": { - "fillWith": [ - "WorkspaceId" - ], - "label": "Workspace ID" - }, - "type": "CopyableLabel" - }, - { - "parameters": { - "fillWith": [ - "PrimaryKey" - ], - "label": "Primary Key" - }, - "type": "CopyableLabel" - } - ] + "description": "**STEP 1 - Choose ONE from the following two deployment options to deploy the connector and the associated Azure Function**\n\n>**IMPORTANT:** This connector uses the Azure Monitor Log Ingestion API. Authentication uses the Function App's System-Assigned Managed Identity, which the ARM template grants the Monitoring Metrics Publisher role on the Data Collection Rules. No shared workspace key is required. The Data Collection Endpoint and Data Collection Rule immutable IDs are created and wired automatically by the ARM template." }, { "title": "Option 1 - Azure Resource Manager (ARM) Template", - "description": "Use this method for automated deployment of the Rubrik connector.\n\n1. Click the **Deploy to Azure** button below. \n\n\t[![Deploy To Azure](https://aka.ms/deploytoazurebutton)](https://aka.ms/sentinel-RubrikWebhookEvents-azuredeploy)\n2. Select the preferred **Subscription**, **Resource Group** and **Location**. \n3. Enter the below information : \n\t\tFunction Name \n\t\tWorkspace ID \n\t\tWorkspace Key \n\t\tAnomaliesTableName \n\t\tRansomwareAnalysisTableName \n\t\tThreatHuntsTableName \n\t\tEventsTableName \n\t\tLogLevel \n \n4. Mark the checkbox labeled **I agree to the terms and conditions stated above**. \n5. Click **Purchase** to deploy." + "description": "Use this method for automated deployment of the Rubrik connector.\n\n1. Click the **Deploy to Azure** (Commercial) or **Deploy to Azure Gov** button below.\n\n\t[![Deploy To Azure](https://aka.ms/deploytoazurebutton)](https://aka.ms/sentinel-RubrikWebhookEvents-azuredeploy)\n\t[![Deploy To Azure Gov](https://aka.ms/deploytoazuregovbutton)](https://aka.ms/sentinel-RubrikWebhookEvents-azuredeploy-gov)\n2. Select the preferred **Subscription**, **Resource Group** and **Location**.\n3. Enter the below information:\n\t\tFunction Name\n\t\tWorkspace Resource ID\n\t\tLocation\n\t\tAnomaliesTableName\n\t\tRansomwareAnalysisTableName\n\t\tThreatHuntsTableName\n\t\tEventsTableName\n\t\tViolationTableName\n\t\tRubrikInstanceUrl\n\t\tRubrikClientId\n\t\tRubrikClientSecret\n\t\tLogLevel\n\n4. Mark the checkbox labeled **I agree to the terms and conditions stated above**.\n5. Click **Purchase** to deploy." }, { "title": "Option 2 - Manual Deployment of Azure Functions", @@ -164,11 +163,11 @@ }, { "title": "", - "description": "**1. Deploy a Function App**\n\n> **NOTE:** You will need to [prepare VS code](https://docs.microsoft.com/azure/azure-functions/functions-create-first-function-python#prerequisites) for Azure function development.\n\n1. Download the [Azure Function App](https://aka.ms/sentinel-RubrikWebhookEvents-functionapp) file. Extract archive to your local development computer.\n2. Start VS Code. Choose File in the main menu and select Open Folder.\n3. Select the top level folder from extracted files.\n4. Choose the Azure icon in the Activity bar, then in the **Azure: Functions** area, choose the **Deploy to function app** button.\nIf you aren't already signed in, choose the Azure icon in the Activity bar, then in the **Azure: Functions** area, choose **Sign in to Azure**\nIf you're already signed in, go to the next step.\n5. Provide the following information at the prompts:\n\n\ta. **Select folder:** Choose a folder from your workspace or browse to one that contains your function app.\n\n\tb. **Select Subscription:** Choose the subscription to use.\n\n\tc. Select **Create new Function App in Azure** (Don't choose the Advanced option)\n\n\td. **Enter a globally unique name for the function app:** Type a name that is valid in a URL path. The name you type is validated to make sure that it's unique in Azure Functions. (e.g. RubrikXXXXX).\n\n\te. **Select a runtime:** Choose Python 3.8 or above.\n\n\tf. Select a location for new resources. For better performance and lower costs choose the same [region](https://azure.microsoft.com/regions/) where Microsoft Sentinel is located.\n\n6. Deployment will begin. A notification is displayed after your function app is created and the deployment package is applied.\n7. Go to Azure Portal for the Function App configuration." + "description": "**1. Deploy a Function App**\n\n> **NOTE:** You will need to [prepare VS code](https://docs.microsoft.com/azure/azure-functions/functions-create-first-function-python#prerequisites) for Azure function development.\n\n1. Download the [Azure Function App](https://aka.ms/sentinel-RubrikDataConnector-functionapp) file. Extract archive to your local development computer.\n2. Start VS Code. Choose File in the main menu and select Open Folder.\n3. Select the top level folder from extracted files.\n4. Choose the Azure icon in the Activity bar, then in the **Azure: Functions** area, choose the **Deploy to function app** button.\nIf you aren't already signed in, choose the Azure icon in the Activity bar, then in the **Azure: Functions** area, choose **Sign in to Azure**\nIf you're already signed in, go to the next step.\n5. Provide the following information at the prompts:\n\n\ta. **Select folder:** Choose a folder from your workspace or browse to one that contains your function app.\n\n\tb. **Select Subscription:** Choose the subscription to use.\n\n\tc. Select **Create new Function App in Azure** (Don't choose the Advanced option)\n\n\td. **Enter a globally unique name for the function app:** Type a name that is valid in a URL path. The name you type is validated to make sure that it's unique in Azure Functions. (e.g. RubrikXXXXX).\n\n\te. **Select a runtime:** Choose Python 3.12.\n\n\tf. Select a location for new resources. For better performance and lower costs choose the same [region](https://azure.microsoft.com/regions/) where Microsoft Sentinel is located.\n\n6. Deployment will begin. A notification is displayed after your function app is created and the deployment package is applied.\n7. Go to Azure Portal for the Function App configuration." }, { "title": "", - "description": "**2. Configure the Function App**\n\n1. In the Function App, select the Function App Name and select **Configuration**.\n2. In the **Application settings** tab, select **+ New application setting**.\n3. Add each of the following application settings individually, with their respective values (case-sensitive): \n\t\tWorkspaceID\n\t\tWorkspaceKey\n\t\tAnomaliesTableName\n\t\tRansomwareAnalysisTableName\n\t\tThreatHuntsTableName\n\t\tEventsTableName\n\t\tLogLevel\n\t\tlogAnalyticsUri (optional)\n - Use logAnalyticsUri to override the log analytics API endpoint for dedicated cloud. For example, for public cloud, leave the value empty; for Azure GovUS cloud environment, specify the value in the following format: https://.ods.opinsights.azure.us. \n4. Once all application settings have been entered, click **Save**." + "description": "**2. Configure the Function App**\n\n1. In the Function App, select the Function App Name and select **Configuration**.\n2. In the **Application settings** tab, select **+ New application setting**.\n3. Add each of the following application settings individually, with their respective values (case-sensitive):\n\t\tWorkspaceResourceId\n\t\tLocation\n\t\tAnomaliesTableName\n\t\tRansomwareAnalysisTableName\n\t\tThreatHuntsTableName\n\t\tEventsTableName\n\t\tViolationTableName\n\t\tAZURE_DATA_COLLECTION_ENDPOINT\n\t\tDCR_WEBHOOK_RULE_ID\n\t\tDCR_EVENTS_RULE_ID\n\t\tSCOPE\n\t\tRubrikInstanceUrl\n\t\tRubrikClientId\n\t\tRubrikClientSecret\n\t\tLogLevel\n - For Azure Government, set SCOPE to https://monitor.azure.us/.default and use the Gov Data Collection Endpoint.\n - This connector authenticates using the Function App's System-Assigned Managed Identity, which is granted the Monitoring Metrics Publisher role on the Data Collection Rules by the ARM template.\n - RubrikInstanceUrl / RubrikClientId / RubrikClientSecret are the Rubrik Security Cloud service-account credentials used only by the Violation classification hit-count gate.\n4. Once all application settings have been entered, click **Save**." }, { "title": "", @@ -180,11 +179,11 @@ }, { "title": "2) Add a webhook in RubrikSecurityCloud to send data to Microsoft Sentinel.", - "description": "Follow the Rubrik User Guide instructions to [Add a Webhook](https://docs.rubrik.com/en-us/saas/saas/common/adding_webhook.html) to begin receiving event information \n 1. Select the Microsoft Sentinel as the webhook Provider \n 2. Enter the desired Webhook name \n 3. Enter the URL part from copied Function-url as the webhook URL endpoint and replace **{functionname}** with **\"RubrikAnomalyOrchestrator\"**, for the Rubrik Microsoft Sentinel Solution \n 4. Select the EventType as Anomaly \n 5. Select the following severity levels: Critical, Warning, Informational \n 6. Choose multiple log types, if desired, when running **\"RubrikEventsOrchestrator\"** \n 7. Repeat the same steps to add webhooks for Anomaly Detection Analysis, Threat Hunt and Other Events.\n \n\n NOTE: while adding webhooks for Anomaly Detection Analysis, Threat Hunt and Other Events, replace **{functionname}** with **\"RubrikRansomwareOrchestrator\"**, **\"RubrikThreatHuntOrchestrator\"** and **\"RubrikEventsOrchestrator\"** respectively in copied function-url." + "description": "Follow the Rubrik User Guide instructions to [Add a Webhook](https://docs.rubrik.com/en-us/saas/saas/common/adding_webhook.html) to begin receiving event information \n 1. Select the Microsoft Sentinel as the webhook Provider \n 2. Enter the desired Webhook name \n 3. Enter the URL part from copied Function-url as the webhook URL endpoint and replace **{functionname}** with **\"RubrikAnomalyOrchestrator\"**, for the Rubrik Microsoft Sentinel Solution \n 4. Select the EventType as Anomaly \n 5. Select the following severity levels: Critical, Warning, Informational \n 6. Choose multiple log types, if desired, when running **\"RubrikEventsOrchestrator\"** \n 7. Repeat the same steps to add webhooks for Anomaly Detection Analysis, Threat Hunt, Other Events and Violation.\n \n\n NOTE: while adding webhooks for Anomaly Detection Analysis, Threat Hunt, Other Events and Violation, replace **{functionname}** with **\"RubrikRansomwareOrchestrator\"**, **\"RubrikThreatHuntOrchestrator\"**, **\"RubrikEventsOrchestrator\"** and **\"RubrikViolationOrchestrator\"** respectively in copied function-url.\n\n NOTE: For Violation (classification) events, **\"RubrikViolationOrchestrator\"** applies a hit-count gate: classification-results events are ingested only when Rubrik Security Cloud reports policy hits > 0. This gate uses the **RubrikInstanceUrl**, **RubrikClientId**, and **RubrikClientSecret** application settings configured in Step 2." }, { "title": "", - "description": "*Now we are done with the rubrik Webhook configuration. Once the webhook events triggered , you should be able to see the Anomaly, Anomaly Detection Analysis, Threat Hunt events and Other Events from the Rubrik into respective LogAnalytics workspace table called \"Rubrik_Anomaly_Data_CL\", \"Rubrik_Ransomware_Data_CL\", \"Rubrik_ThreatHunt_Data_CL\", and \"Rubrik_Events_Data_CL\".*\n\n" + "description": "*Now we are done with the rubrik Webhook configuration. Once the webhook events triggered , you should be able to see the Anomaly, Anomaly Detection Analysis, Threat Hunt events, Other Events and Violation events from the Rubrik into respective LogAnalytics workspace table called \"Rubrik_Anomaly_Data_CL\", \"Rubrik_Ransomware_Data_CL\", \"Rubrik_ThreatHunt_Data_CL\", \"Rubrik_Events_Data_CL\", and \"Rubrik_Violation_Data_CL\".*\n\n" } ] } \ No newline at end of file diff --git a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/azuredeploy_Connector_RubrikWebhookEvents_AzureFunction.json b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/azuredeploy_Connector_RubrikWebhookEvents_AzureFunction.json index dccff9be386..56c1a22bc8e 100644 --- a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/azuredeploy_Connector_RubrikWebhookEvents_AzureFunction.json +++ b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/azuredeploy_Connector_RubrikWebhookEvents_AzureFunction.json @@ -8,49 +8,61 @@ "maxLength": 11, "type": "string" }, - "WorkspaceID": { + "Location": { "type": "string", - "minLength": 1, + "defaultValue": "[resourceGroup().location]", "metadata": { - "description": "Enter Workspace ID of log analytics Workspace" + "description": "Location for the Data Collection Endpoint and Data Collection Rules." } }, - "WorkspaceKey": { - "type": "securestring", + "WorkspaceResourceId": { + "type": "string", "minLength": 1, "metadata": { - "description": "Enter Primary Key of log analytics Workspace" + "description": "Full resource ID of the Log Analytics workspace, e.g. '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}'. The workspace can be in a different resource group or subscription than this deployment." } }, "AnomaliesTableName": { "type": "string", - "defaultValue": "Rubrik_Anomaly_Data", + "defaultValue": "Rubrik_Anomaly_Data_CL", + "maxLength": 52, "metadata": { - "description": "Enter name of the table used to store Rubrik Anamaly logs. Default is 'Rubrik_Anomaly_Data_CL'" + "description": "Enter name of the table used to store Rubrik Anomaly logs. Default is 'Rubrik_Anomaly_Data_CL'" } }, "RansomwareAnalysisTableName": { "type": "string", - "defaultValue": "Rubrik_Ransomware_Data", - "metadata": { + "defaultValue": "Rubrik_Ransomware_Data_CL", + "maxLength": 52, + "metadata": { "description": "Enter name of the table used to store Rubrik Ransomware logs. Default is 'Rubrik_Ransomware_Data_CL'" } }, "ThreatHuntsTableName": { "type": "string", - "defaultValue": "Rubrik_ThreatHunt_Data", - "metadata": { + "defaultValue": "Rubrik_ThreatHunt_Data_CL", + "maxLength": 52, + "metadata": { "description": "Enter name of the table used to store Rubrik ThreatHunt logs. Default is 'Rubrik_ThreatHunt_Data_CL'" } }, "EventsTableName": { "type": "string", - "defaultValue": "Rubrik_Events_Data", + "defaultValue": "Rubrik_Events_Data_CL", + "maxLength": 52, "metadata": { "description": "Enter the table name for types other than Anomaly, Ransomware, and Threat Hunt" } }, - + "ViolationTableName": { + "type": "string", + "defaultValue": "Rubrik_Violation_Data_CL", + "maxLength": 52, + "metadata": { + "description": "Enter name of the table used to store Rubrik Violation logs. Default is 'Rubrik_Violation_Data_CL'" + } + }, + "LogLevel": { "type": "string", "metadata": { @@ -69,12 +81,40 @@ "metadata": { "description": "Migrate Classic Application Insights to Log Analytic Workspace which is retiring by 29 Febraury 2024. Use 'Log Analytic Workspace-->Properties' blade having 'Resource ID' property value. This is a fully qualified resourceId which is in format '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}'" } + }, + "RubrikInstanceUrl": { + "type": "string", + "metadata": { + "description": "Rubrik Security Cloud base URL, e.g. https://.my.rubrik.com. Used by the Violation classification hit-count gate." + } + }, + "RubrikClientId": { + "type": "string", + "metadata": { + "description": "RSC service-account client id (client|) for the classification gate." + } + }, + "RubrikClientSecret": { + "type": "securestring", + "metadata": { + "description": "RSC service-account secret for the classification gate." + } } }, "variables": { "FunctionName": "[concat(toLower(parameters('FunctionName')), uniqueString(resourceGroup().id))]", "StorageSuffix": "[environment().suffixes.storage]", - "LogAnaltyicsUri": "[replace(environment().portal, 'https://portal', concat('https://', toLower(parameters('WorkspaceID')), '.ods.opinsights'))]" + "WorkspaceName": "[last(split(parameters('WorkspaceResourceId'), '/'))]", + "WorkspaceSubscriptionId": "[split(parameters('WorkspaceResourceId'), '/')[2]]", + "WorkspaceResourceGroup": "[split(parameters('WorkspaceResourceId'), '/')[4]]", + "TablesDeploymentName": "RubrikLogAnalyticsTables", + "DceName": "[concat(variables('FunctionName'), '-dce')]", + "WebhookDcrName": "RubrikWebhookEventsDCR", + "EventsDcrName": "RubrikEventsDCR", + "MonitoringMetricsPublisherRoleId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '3913510d-42f4-4e42-8a64-420c390055eb')]", + "webhookDcrId": "[resourceId('Microsoft.Insights/dataCollectionRules', variables('WebhookDcrName'))]", + "eventsDcrId": "[resourceId('Microsoft.Insights/dataCollectionRules', variables('EventsDcrName'))]", + "dceId": "[resourceId('Microsoft.Insights/dataCollectionEndpoints', variables('DceName'))]" }, "resources": [ { @@ -169,7 +209,10 @@ "location": "[resourceGroup().location]", "dependsOn": [ "[resourceId('Microsoft.Storage/storageAccounts', tolower(variables('FunctionName')))]", - "[resourceId('Microsoft.Insights/components', variables('FunctionName'))]" + "[resourceId('Microsoft.Insights/components', variables('FunctionName'))]", + "[variables('dceId')]", + "[variables('webhookDcrId')]", + "[variables('eventsDcrId')]" ], "kind": "functionapp,linux", "identity": { @@ -200,15 +243,20 @@ "APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(resourceId('Microsoft.insights/components', variables('FunctionName')), '2015-05-01').InstrumentationKey]", "APPLICATIONINSIGHTS_CONNECTION_STRING": "[reference(resourceId('microsoft.insights/components', variables('FunctionName')), '2015-05-01').ConnectionString]", "AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=', toLower(variables('FunctionName')),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', toLower(variables('FunctionName'))), '2019-06-01').keys[0].value, ';EndpointSuffix=',toLower(variables('StorageSuffix')))]", - "logAnalyticsUri": "[variables('LogAnaltyicsUri')]", - "WorkspaceID": "[parameters('WorkspaceID')]", - "WorkspaceKey": "[parameters('WorkspaceKey')]", "RansomwareAnalysis_table_name": "[parameters('RansomwareAnalysisTableName')]", "ThreatHunts_table_name": "[parameters('ThreatHuntsTableName')]", "Anomalies_table_name": "[parameters('AnomaliesTableName')]", "Events_table_name": "[parameters('EventsTableName')]", + "Violation_table_name": "[parameters('ViolationTableName')]", + "AZURE_DATA_COLLECTION_ENDPOINT": "[reference(variables('dceId'), '2022-06-01').logsIngestion.endpoint]", + "DCR_WEBHOOK_RULE_ID": "[reference(variables('webhookDcrId'), '2022-06-01').immutableId]", + "DCR_EVENTS_RULE_ID": "[reference(variables('eventsDcrId'), '2022-06-01').immutableId]", + "SCOPE": "https://monitor.azure.com/.default", + "RubrikInstanceUrl": "[parameters('RubrikInstanceUrl')]", + "RubrikClientId": "[parameters('RubrikClientId')]", + "RubrikClientSecret": "[parameters('RubrikClientSecret')]", "LogLevel": "[parameters('LogLevel')]", - "WEBSITE_RUN_FROM_PACKAGE": "https://aka.ms/sentinel-RubrikWebhookEvents-functionapp" + "WEBSITE_RUN_FROM_PACKAGE": "https://aka.ms/sentinel-RubrikDataConnector-functionapp" } } ] @@ -248,6 +296,472 @@ "properties": { "shareQuota": 5120 } + }, + { + "type": "Microsoft.Insights/dataCollectionEndpoints", + "apiVersion": "2022-06-01", + "name": "[variables('DceName')]", + "location": "[parameters('Location')]", + "properties": { + "networkAcls": { + "publicNetworkAccess": "Enabled" + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[variables('TablesDeploymentName')]", + "subscriptionId": "[variables('WorkspaceSubscriptionId')]", + "resourceGroup": "[variables('WorkspaceResourceGroup')]", + "properties": { + "mode": "Incremental", + "expressionEvaluationOptions": { + "scope": "inner" + }, + "parameters": { + "WorkspaceName": { "value": "[variables('WorkspaceName')]" }, + "AnomaliesTableName": { "value": "[parameters('AnomaliesTableName')]" }, + "RansomwareAnalysisTableName": { "value": "[parameters('RansomwareAnalysisTableName')]" }, + "ThreatHuntsTableName": { "value": "[parameters('ThreatHuntsTableName')]" }, + "ViolationTableName": { "value": "[parameters('ViolationTableName')]" }, + "EventsTableName": { "value": "[parameters('EventsTableName')]" } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "WorkspaceName": { "type": "string" }, + "AnomaliesTableName": { "type": "string" }, + "RansomwareAnalysisTableName": { "type": "string" }, + "ThreatHuntsTableName": { "type": "string" }, + "ViolationTableName": { "type": "string" }, + "EventsTableName": { "type": "string" } + }, + "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces/tables", + "apiVersion": "2022-10-01", + "name": "[concat(parameters('WorkspaceName'), '/', parameters('AnomaliesTableName'))]", + "properties": { + "schema": { + "name": "[parameters('AnomaliesTableName')]", + "columns": [ + { "name": "TimeGenerated", "type": "datetime" }, + { "name": "summary_s", "type": "string" }, + { "name": "source_s", "type": "string" }, + { "name": "severity_s", "type": "string" }, + { "name": "class_s", "type": "string" }, + { "name": "component_s", "type": "string" }, + { "name": "group_s", "type": "string" }, + { "name": "timestamp_t", "type": "datetime" }, + { "name": "timestamp_s", "type": "string" }, + { "name": "custom_details_type_s", "type": "string" }, + { "name": "custom_details_objectName_s", "type": "string" }, + { "name": "custom_details_objectType_s", "type": "string" }, + { "name": "custom_details_status_s", "type": "string" }, + { "name": "custom_details_clusterName_s", "type": "string" }, + { "name": "custom_details_eventName_s", "type": "string" }, + { "name": "custom_details_errorId_s", "type": "string" }, + { "name": "custom_details_errorCode_s", "type": "string" }, + { "name": "custom_details_errorRemedy_s", "type": "string" }, + { "name": "custom_details_errorReason_s", "type": "string" }, + { "name": "custom_details_errorMessage_s", "type": "string" }, + { "name": "custom_details_location_s", "type": "string" }, + { "name": "custom_details_url_s", "type": "string" }, + { "name": "custom_details_customerID_s", "type": "string" }, + { "name": "custom_details_logicalSizeInBytes_s", "type": "string" }, + { "name": "custom_details_dataTransferredInBytes_s", "type": "string" }, + { "name": "custom_details_effectiveThroughput_s", "type": "string" }, + { "name": "custom_details_snapshotId_s", "type": "string" }, + { "name": "custom_details_auditUserId_s", "type": "string" }, + { "name": "custom_details_auditUserName_s", "type": "string" }, + { "name": "custom_details_ipAddress_s", "type": "string" }, + { "name": "custom_details_seriesId_g", "type": "guid" }, + { "name": "custom_details_seriesId_s", "type": "string" }, + { "name": "custom_details_id_g", "type": "guid" }, + { "name": "custom_details_id_s", "type": "string" }, + { "name": "custom_details_objectId_g", "type": "guid" }, + { "name": "custom_details_objectId_s", "type": "string" }, + { "name": "custom_details_clusterId_g", "type": "guid" }, + { "name": "custom_details_clusterId_s", "type": "string" }, + { "name": "custom_details_eventInfo", "type": "dynamic" } + ] + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/tables", + "apiVersion": "2022-10-01", + "name": "[concat(parameters('WorkspaceName'), '/', parameters('RansomwareAnalysisTableName'))]", + "properties": { + "schema": { + "name": "[parameters('RansomwareAnalysisTableName')]", + "columns": [ + { "name": "TimeGenerated", "type": "datetime" }, + { "name": "summary_s", "type": "string" }, + { "name": "source_s", "type": "string" }, + { "name": "severity_s", "type": "string" }, + { "name": "class_s", "type": "string" }, + { "name": "component_s", "type": "string" }, + { "name": "group_s", "type": "string" }, + { "name": "timestamp_t", "type": "datetime" }, + { "name": "timestamp_s", "type": "string" }, + { "name": "custom_details_type_s", "type": "string" }, + { "name": "custom_details_objectName_s", "type": "string" }, + { "name": "custom_details_objectType_s", "type": "string" }, + { "name": "custom_details_status_s", "type": "string" }, + { "name": "custom_details_clusterName_s", "type": "string" }, + { "name": "custom_details_eventName_s", "type": "string" }, + { "name": "custom_details_errorId_s", "type": "string" }, + { "name": "custom_details_errorCode_s", "type": "string" }, + { "name": "custom_details_errorRemedy_s", "type": "string" }, + { "name": "custom_details_errorReason_s", "type": "string" }, + { "name": "custom_details_errorMessage_s", "type": "string" }, + { "name": "custom_details_location_s", "type": "string" }, + { "name": "custom_details_url_s", "type": "string" }, + { "name": "custom_details_customerID_s", "type": "string" }, + { "name": "custom_details_logicalSizeInBytes_s", "type": "string" }, + { "name": "custom_details_dataTransferredInBytes_s", "type": "string" }, + { "name": "custom_details_effectiveThroughput_s", "type": "string" }, + { "name": "custom_details_snapshotId_s", "type": "string" }, + { "name": "custom_details_auditUserId_s", "type": "string" }, + { "name": "custom_details_auditUserName_s", "type": "string" }, + { "name": "custom_details_ipAddress_s", "type": "string" }, + { "name": "custom_details_seriesId_g", "type": "guid" }, + { "name": "custom_details_seriesId_s", "type": "string" }, + { "name": "custom_details_id_g", "type": "guid" }, + { "name": "custom_details_id_s", "type": "string" }, + { "name": "custom_details_objectId_g", "type": "guid" }, + { "name": "custom_details_objectId_s", "type": "string" }, + { "name": "custom_details_clusterId_g", "type": "guid" }, + { "name": "custom_details_clusterId_s", "type": "string" }, + { "name": "custom_details_eventInfo", "type": "dynamic" } + ] + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/tables", + "apiVersion": "2022-10-01", + "name": "[concat(parameters('WorkspaceName'), '/', parameters('ThreatHuntsTableName'))]", + "properties": { + "schema": { + "name": "[parameters('ThreatHuntsTableName')]", + "columns": [ + { "name": "TimeGenerated", "type": "datetime" }, + { "name": "summary_s", "type": "string" }, + { "name": "source_s", "type": "string" }, + { "name": "severity_s", "type": "string" }, + { "name": "class_s", "type": "string" }, + { "name": "component_s", "type": "string" }, + { "name": "group_s", "type": "string" }, + { "name": "timestamp_t", "type": "datetime" }, + { "name": "timestamp_s", "type": "string" }, + { "name": "custom_details_type_s", "type": "string" }, + { "name": "custom_details_objectName_s", "type": "string" }, + { "name": "custom_details_objectType_s", "type": "string" }, + { "name": "custom_details_status_s", "type": "string" }, + { "name": "custom_details_clusterName_s", "type": "string" }, + { "name": "custom_details_eventName_s", "type": "string" }, + { "name": "custom_details_errorId_s", "type": "string" }, + { "name": "custom_details_errorCode_s", "type": "string" }, + { "name": "custom_details_errorRemedy_s", "type": "string" }, + { "name": "custom_details_errorReason_s", "type": "string" }, + { "name": "custom_details_errorMessage_s", "type": "string" }, + { "name": "custom_details_location_s", "type": "string" }, + { "name": "custom_details_url_s", "type": "string" }, + { "name": "custom_details_customerID_s", "type": "string" }, + { "name": "custom_details_logicalSizeInBytes_s", "type": "string" }, + { "name": "custom_details_dataTransferredInBytes_s", "type": "string" }, + { "name": "custom_details_effectiveThroughput_s", "type": "string" }, + { "name": "custom_details_snapshotId_s", "type": "string" }, + { "name": "custom_details_auditUserId_s", "type": "string" }, + { "name": "custom_details_auditUserName_s", "type": "string" }, + { "name": "custom_details_ipAddress_s", "type": "string" }, + { "name": "custom_details_seriesId_g", "type": "guid" }, + { "name": "custom_details_seriesId_s", "type": "string" }, + { "name": "custom_details_id_g", "type": "guid" }, + { "name": "custom_details_id_s", "type": "string" }, + { "name": "custom_details_objectId_g", "type": "guid" }, + { "name": "custom_details_objectId_s", "type": "string" }, + { "name": "custom_details_clusterId_g", "type": "guid" }, + { "name": "custom_details_clusterId_s", "type": "string" }, + { "name": "custom_details_eventInfo", "type": "dynamic" } + ] + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/tables", + "apiVersion": "2022-10-01", + "name": "[concat(parameters('WorkspaceName'), '/', parameters('ViolationTableName'))]", + "properties": { + "schema": { + "name": "[parameters('ViolationTableName')]", + "columns": [ + { "name": "TimeGenerated", "type": "datetime" }, + { "name": "summary_s", "type": "string" }, + { "name": "source_s", "type": "string" }, + { "name": "severity_s", "type": "string" }, + { "name": "class_s", "type": "string" }, + { "name": "component_s", "type": "string" }, + { "name": "group_s", "type": "string" }, + { "name": "timestamp_t", "type": "datetime" }, + { "name": "timestamp_s", "type": "string" }, + { "name": "custom_details_type_s", "type": "string" }, + { "name": "custom_details_objectName_s", "type": "string" }, + { "name": "custom_details_objectType_s", "type": "string" }, + { "name": "custom_details_status_s", "type": "string" }, + { "name": "custom_details_clusterName_s", "type": "string" }, + { "name": "custom_details_eventName_s", "type": "string" }, + { "name": "custom_details_errorId_s", "type": "string" }, + { "name": "custom_details_errorCode_s", "type": "string" }, + { "name": "custom_details_errorRemedy_s", "type": "string" }, + { "name": "custom_details_errorReason_s", "type": "string" }, + { "name": "custom_details_errorMessage_s", "type": "string" }, + { "name": "custom_details_location_s", "type": "string" }, + { "name": "custom_details_url_s", "type": "string" }, + { "name": "custom_details_customerID_s", "type": "string" }, + { "name": "custom_details_logicalSizeInBytes_s", "type": "string" }, + { "name": "custom_details_dataTransferredInBytes_s", "type": "string" }, + { "name": "custom_details_effectiveThroughput_s", "type": "string" }, + { "name": "custom_details_snapshotId_s", "type": "string" }, + { "name": "custom_details_auditUserId_s", "type": "string" }, + { "name": "custom_details_auditUserName_s", "type": "string" }, + { "name": "custom_details_ipAddress_s", "type": "string" }, + { "name": "custom_details_seriesId_g", "type": "guid" }, + { "name": "custom_details_seriesId_s", "type": "string" }, + { "name": "custom_details_id_g", "type": "guid" }, + { "name": "custom_details_id_s", "type": "string" }, + { "name": "custom_details_objectId_g", "type": "guid" }, + { "name": "custom_details_objectId_s", "type": "string" }, + { "name": "custom_details_clusterId_g", "type": "guid" }, + { "name": "custom_details_clusterId_s", "type": "string" }, + { "name": "custom_details_eventInfo", "type": "dynamic" } + ] + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/tables", + "apiVersion": "2022-10-01", + "name": "[concat(parameters('WorkspaceName'), '/', parameters('EventsTableName'))]", + "properties": { + "schema": { + "name": "[parameters('EventsTableName')]", + "columns": [ + { "name": "TimeGenerated", "type": "datetime" }, + { "name": "summary_s", "type": "string" }, + { "name": "source_s", "type": "string" }, + { "name": "severity_s", "type": "string" }, + { "name": "class_s", "type": "string" }, + { "name": "component_s", "type": "string" }, + { "name": "group_s", "type": "string" }, + { "name": "timestamp_t", "type": "datetime" }, + { "name": "timestamp_s", "type": "string" }, + { "name": "custom_details_type_s", "type": "string" }, + { "name": "custom_details_objectName_s", "type": "string" }, + { "name": "custom_details_objectType_s", "type": "string" }, + { "name": "custom_details_status_s", "type": "string" }, + { "name": "custom_details_clusterName_s", "type": "string" }, + { "name": "custom_details_eventName_s", "type": "string" }, + { "name": "custom_details_errorId_s", "type": "string" }, + { "name": "custom_details_errorCode_s", "type": "string" }, + { "name": "custom_details_errorRemedy_s", "type": "string" }, + { "name": "custom_details_errorReason_s", "type": "string" }, + { "name": "custom_details_errorMessage_s", "type": "string" }, + { "name": "custom_details_location_s", "type": "string" }, + { "name": "custom_details_url_s", "type": "string" }, + { "name": "custom_details_customerID_s", "type": "string" }, + { "name": "custom_details_logicalSizeInBytes_s", "type": "string" }, + { "name": "custom_details_dataTransferredInBytes_s", "type": "string" }, + { "name": "custom_details_effectiveThroughput_s", "type": "string" }, + { "name": "custom_details_snapshotId_s", "type": "string" }, + { "name": "custom_details_auditUserId_s", "type": "string" }, + { "name": "custom_details_auditUserName_s", "type": "string" }, + { "name": "custom_details_ipAddress_s", "type": "string" }, + { "name": "custom_details_seriesId_g", "type": "guid" }, + { "name": "custom_details_seriesId_s", "type": "string" }, + { "name": "custom_details_id_g", "type": "guid" }, + { "name": "custom_details_id_s", "type": "string" }, + { "name": "custom_details_objectId_g", "type": "guid" }, + { "name": "custom_details_objectId_s", "type": "string" }, + { "name": "custom_details_clusterId_g", "type": "guid" }, + { "name": "custom_details_clusterId_s", "type": "string" }, + { "name": "custom_details_auditUserId_g", "type": "guid" }, + { "name": "custom_details_objectFid_g", "type": "guid" }, + { "name": "custom_details_objectFid_s", "type": "string" }, + { "name": "custom_details_orgID_g", "type": "guid" }, + { "name": "custom_details_orgID_s", "type": "string" }, + { "name": "custom_details_seriesID_g", "type": "guid" }, + { "name": "custom_details_seriesID_s", "type": "string" }, + { "name": "custom_details_isPolarisAudit_s", "type": "string" }, + { "name": "custom_details_userNote_s", "type": "string" }, + { "name": "custom_details_eventInfo", "type": "dynamic" }, + { "name": "custom_details_auditInfo", "type": "dynamic" } + ] + } + } + } + ] + } + } + }, + { + "type": "Microsoft.Insights/dataCollectionRules", + "apiVersion": "2022-06-01", + "name": "[variables('WebhookDcrName')]", + "location": "[parameters('Location')]", + "dependsOn": [ + "[variables('dceId')]", + "[resourceId('Microsoft.Resources/deployments', variables('TablesDeploymentName'))]" + ], + "properties": { + "dataCollectionEndpointId": "[variables('dceId')]", + "streamDeclarations": { + "Custom-Rubrik_Anomaly_Data_CL": { + "columns": [ + { "name": "summary", "type": "string" }, + { "name": "source", "type": "string" }, + { "name": "severity", "type": "string" }, + { "name": "timestamp", "type": "string" }, + { "name": "class", "type": "string" }, + { "name": "component", "type": "string" }, + { "name": "group", "type": "string" }, + { "name": "custom_details", "type": "dynamic" } + ] + }, + "Custom-Rubrik_Ransomware_Data_CL": { + "columns": [ + { "name": "summary", "type": "string" }, + { "name": "source", "type": "string" }, + { "name": "severity", "type": "string" }, + { "name": "timestamp", "type": "string" }, + { "name": "class", "type": "string" }, + { "name": "component", "type": "string" }, + { "name": "group", "type": "string" }, + { "name": "custom_details", "type": "dynamic" } + ] + }, + "Custom-Rubrik_ThreatHunt_Data_CL": { + "columns": [ + { "name": "summary", "type": "string" }, + { "name": "source", "type": "string" }, + { "name": "severity", "type": "string" }, + { "name": "timestamp", "type": "string" }, + { "name": "class", "type": "string" }, + { "name": "component", "type": "string" }, + { "name": "group", "type": "string" }, + { "name": "custom_details", "type": "dynamic" } + ] + }, + "Custom-Rubrik_Violation_Data_CL": { + "columns": [ + { "name": "summary", "type": "string" }, + { "name": "source", "type": "string" }, + { "name": "severity", "type": "string" }, + { "name": "timestamp", "type": "string" }, + { "name": "class", "type": "string" }, + { "name": "component", "type": "string" }, + { "name": "group", "type": "string" }, + { "name": "custom_details", "type": "dynamic" } + ] + } + }, + "destinations": { + "logAnalytics": [ + { + "name": "clv2ws1", + "workspaceResourceId": "[parameters('WorkspaceResourceId')]" + } + ] + }, + "dataFlows": [ + { + "streams": ["Custom-Rubrik_Anomaly_Data_CL"], + "destinations": ["clv2ws1"], + "outputStream": "Custom-Rubrik_Anomaly_Data_CL", + "transformKql": "source | extend cd = parse_json(tostring(custom_details)), ts = todatetime(timestamp) | project TimeGenerated = now(), summary_s = tostring(summary), source_s = tostring(source), severity_s = tostring(severity), class_s = tostring(class), component_s = tostring(component), group_s = tostring(group), timestamp_t = ts, timestamp_s = tostring(timestamp), custom_details_type_s = tostring(cd.type), custom_details_objectName_s = tostring(cd.objectName), custom_details_objectType_s = tostring(cd.objectType), custom_details_status_s = tostring(cd.status), custom_details_clusterName_s = tostring(cd.clusterName), custom_details_eventName_s = tostring(cd.eventName), custom_details_errorId_s = tostring(cd.errorId), custom_details_errorCode_s = tostring(cd.errorCode), custom_details_errorRemedy_s = tostring(cd.errorRemedy), custom_details_errorReason_s = tostring(cd.errorReason), custom_details_errorMessage_s = tostring(cd.errorMessage), custom_details_location_s = tostring(cd.location), custom_details_url_s = tostring(cd.url), custom_details_customerID_s = tostring(cd.customerID), custom_details_logicalSizeInBytes_s = tostring(cd.logicalSizeInBytes), custom_details_dataTransferredInBytes_s = tostring(cd.dataTransferredInBytes), custom_details_effectiveThroughput_s = tostring(cd.effectiveThroughput), custom_details_snapshotId_s = tostring(cd.snapshotId), custom_details_auditUserId_s = tostring(cd.auditUserId), custom_details_auditUserName_s = tostring(cd.auditUserName), custom_details_ipAddress_s = tostring(cd.ipAddress), custom_details_seriesId_g = toguid(iff(tostring(cd.seriesId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.seriesId), '')), custom_details_seriesId_s = iff(tostring(cd.seriesId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.seriesId)), custom_details_id_g = toguid(iff(tostring(cd.id) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.id), '')), custom_details_id_s = iff(tostring(cd.id) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.id)), custom_details_objectId_g = toguid(iff(tostring(cd.objectId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.objectId), '')), custom_details_objectId_s = iff(tostring(cd.objectId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.objectId)), custom_details_clusterId_g = toguid(iff(tostring(cd.clusterId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.clusterId), '')), custom_details_clusterId_s = iff(tostring(cd.clusterId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.clusterId)), custom_details_eventInfo = cd.eventInfo" + }, + { + "streams": ["Custom-Rubrik_Ransomware_Data_CL"], + "destinations": ["clv2ws1"], + "outputStream": "Custom-Rubrik_Ransomware_Data_CL", + "transformKql": "source | extend cd = parse_json(tostring(custom_details)), ts = todatetime(timestamp) | project TimeGenerated = now(), summary_s = tostring(summary), source_s = tostring(source), severity_s = tostring(severity), class_s = tostring(class), component_s = tostring(component), group_s = tostring(group), timestamp_t = ts, timestamp_s = tostring(timestamp), custom_details_type_s = tostring(cd.type), custom_details_objectName_s = tostring(cd.objectName), custom_details_objectType_s = tostring(cd.objectType), custom_details_status_s = tostring(cd.status), custom_details_clusterName_s = tostring(cd.clusterName), custom_details_eventName_s = tostring(cd.eventName), custom_details_errorId_s = tostring(cd.errorId), custom_details_errorCode_s = tostring(cd.errorCode), custom_details_errorRemedy_s = tostring(cd.errorRemedy), custom_details_errorReason_s = tostring(cd.errorReason), custom_details_errorMessage_s = tostring(cd.errorMessage), custom_details_location_s = tostring(cd.location), custom_details_url_s = tostring(cd.url), custom_details_customerID_s = tostring(cd.customerID), custom_details_logicalSizeInBytes_s = tostring(cd.logicalSizeInBytes), custom_details_dataTransferredInBytes_s = tostring(cd.dataTransferredInBytes), custom_details_effectiveThroughput_s = tostring(cd.effectiveThroughput), custom_details_snapshotId_s = tostring(cd.snapshotId), custom_details_auditUserId_s = tostring(cd.auditUserId), custom_details_auditUserName_s = tostring(cd.auditUserName), custom_details_ipAddress_s = tostring(cd.ipAddress), custom_details_seriesId_g = toguid(iff(tostring(cd.seriesId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.seriesId), '')), custom_details_seriesId_s = iff(tostring(cd.seriesId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.seriesId)), custom_details_id_g = toguid(iff(tostring(cd.id) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.id), '')), custom_details_id_s = iff(tostring(cd.id) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.id)), custom_details_objectId_g = toguid(iff(tostring(cd.objectId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.objectId), '')), custom_details_objectId_s = iff(tostring(cd.objectId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.objectId)), custom_details_clusterId_g = toguid(iff(tostring(cd.clusterId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.clusterId), '')), custom_details_clusterId_s = iff(tostring(cd.clusterId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.clusterId)), custom_details_eventInfo = cd.eventInfo" + }, + { + "streams": ["Custom-Rubrik_ThreatHunt_Data_CL"], + "destinations": ["clv2ws1"], + "outputStream": "Custom-Rubrik_ThreatHunt_Data_CL", + "transformKql": "source | extend cd = parse_json(tostring(custom_details)), ts = todatetime(timestamp) | project TimeGenerated = now(), summary_s = tostring(summary), source_s = tostring(source), severity_s = tostring(severity), class_s = tostring(class), component_s = tostring(component), group_s = tostring(group), timestamp_t = ts, timestamp_s = tostring(timestamp), custom_details_type_s = tostring(cd.type), custom_details_objectName_s = tostring(cd.objectName), custom_details_objectType_s = tostring(cd.objectType), custom_details_status_s = tostring(cd.status), custom_details_clusterName_s = tostring(cd.clusterName), custom_details_eventName_s = tostring(cd.eventName), custom_details_errorId_s = tostring(cd.errorId), custom_details_errorCode_s = tostring(cd.errorCode), custom_details_errorRemedy_s = tostring(cd.errorRemedy), custom_details_errorReason_s = tostring(cd.errorReason), custom_details_errorMessage_s = tostring(cd.errorMessage), custom_details_location_s = tostring(cd.location), custom_details_url_s = tostring(cd.url), custom_details_customerID_s = tostring(cd.customerID), custom_details_logicalSizeInBytes_s = tostring(cd.logicalSizeInBytes), custom_details_dataTransferredInBytes_s = tostring(cd.dataTransferredInBytes), custom_details_effectiveThroughput_s = tostring(cd.effectiveThroughput), custom_details_snapshotId_s = tostring(cd.snapshotId), custom_details_auditUserId_s = tostring(cd.auditUserId), custom_details_auditUserName_s = tostring(cd.auditUserName), custom_details_ipAddress_s = tostring(cd.ipAddress), custom_details_seriesId_g = toguid(iff(tostring(cd.seriesId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.seriesId), '')), custom_details_seriesId_s = iff(tostring(cd.seriesId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.seriesId)), custom_details_id_g = toguid(iff(tostring(cd.id) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.id), '')), custom_details_id_s = iff(tostring(cd.id) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.id)), custom_details_objectId_g = toguid(iff(tostring(cd.objectId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.objectId), '')), custom_details_objectId_s = iff(tostring(cd.objectId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.objectId)), custom_details_clusterId_g = toguid(iff(tostring(cd.clusterId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.clusterId), '')), custom_details_clusterId_s = iff(tostring(cd.clusterId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.clusterId)), custom_details_eventInfo = cd.eventInfo" + }, + { + "streams": ["Custom-Rubrik_Violation_Data_CL"], + "destinations": ["clv2ws1"], + "outputStream": "Custom-Rubrik_Violation_Data_CL", + "transformKql": "source | extend cd = parse_json(tostring(custom_details)), ts = todatetime(timestamp) | project TimeGenerated = now(), summary_s = tostring(summary), source_s = tostring(source), severity_s = tostring(severity), class_s = tostring(class), component_s = tostring(component), group_s = tostring(group), timestamp_t = ts, timestamp_s = tostring(timestamp), custom_details_type_s = tostring(cd.type), custom_details_objectName_s = tostring(cd.objectName), custom_details_objectType_s = tostring(cd.objectType), custom_details_status_s = tostring(cd.status), custom_details_clusterName_s = tostring(cd.clusterName), custom_details_eventName_s = tostring(cd.eventName), custom_details_errorId_s = tostring(cd.errorId), custom_details_errorCode_s = tostring(cd.errorCode), custom_details_errorRemedy_s = tostring(cd.errorRemedy), custom_details_errorReason_s = tostring(cd.errorReason), custom_details_errorMessage_s = tostring(cd.errorMessage), custom_details_location_s = tostring(cd.location), custom_details_url_s = tostring(cd.url), custom_details_customerID_s = tostring(cd.customerID), custom_details_logicalSizeInBytes_s = tostring(cd.logicalSizeInBytes), custom_details_dataTransferredInBytes_s = tostring(cd.dataTransferredInBytes), custom_details_effectiveThroughput_s = tostring(cd.effectiveThroughput), custom_details_snapshotId_s = tostring(cd.snapshotId), custom_details_auditUserId_s = tostring(cd.auditUserId), custom_details_auditUserName_s = tostring(cd.auditUserName), custom_details_ipAddress_s = tostring(cd.ipAddress), custom_details_seriesId_g = toguid(iff(tostring(cd.seriesId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.seriesId), '')), custom_details_seriesId_s = iff(tostring(cd.seriesId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.seriesId)), custom_details_id_g = toguid(iff(tostring(cd.id) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.id), '')), custom_details_id_s = iff(tostring(cd.id) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.id)), custom_details_objectId_g = toguid(iff(tostring(cd.objectId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.objectId), '')), custom_details_objectId_s = iff(tostring(cd.objectId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.objectId)), custom_details_clusterId_g = toguid(iff(tostring(cd.clusterId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.clusterId), '')), custom_details_clusterId_s = iff(tostring(cd.clusterId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.clusterId)), custom_details_eventInfo = cd.eventInfo" + } + ] + } + }, + { + "type": "Microsoft.Insights/dataCollectionRules", + "apiVersion": "2022-06-01", + "name": "[variables('EventsDcrName')]", + "location": "[parameters('Location')]", + "dependsOn": [ + "[variables('dceId')]", + "[resourceId('Microsoft.Resources/deployments', variables('TablesDeploymentName'))]" + ], + "properties": { + "dataCollectionEndpointId": "[variables('dceId')]", + "streamDeclarations": { + "Custom-Rubrik_Events_Data_CL": { + "columns": [ + { "name": "summary", "type": "string" }, + { "name": "source", "type": "string" }, + { "name": "severity", "type": "string" }, + { "name": "timestamp", "type": "string" }, + { "name": "class", "type": "string" }, + { "name": "component", "type": "string" }, + { "name": "group", "type": "string" }, + { "name": "custom_details", "type": "dynamic" } + ] + } + }, + "destinations": { + "logAnalytics": [ + { + "name": "clv2ws1", + "workspaceResourceId": "[parameters('WorkspaceResourceId')]" + } + ] + }, + "dataFlows": [ + { + "streams": ["Custom-Rubrik_Events_Data_CL"], + "destinations": ["clv2ws1"], + "outputStream": "Custom-Rubrik_Events_Data_CL", + "transformKql": "source | extend cd = parse_json(tostring(custom_details)), ts = todatetime(timestamp) | project TimeGenerated = now(), summary_s = tostring(summary), source_s = tostring(source), severity_s = tostring(severity), class_s = tostring(class), component_s = tostring(component), group_s = tostring(group), timestamp_t = ts, timestamp_s = tostring(timestamp), custom_details_type_s = tostring(cd.type), custom_details_objectName_s = tostring(cd.objectName), custom_details_objectType_s = tostring(cd.objectType), custom_details_status_s = tostring(cd.status), custom_details_clusterName_s = tostring(cd.clusterName), custom_details_eventName_s = tostring(cd.eventName), custom_details_errorId_s = tostring(cd.errorId), custom_details_errorCode_s = tostring(cd.errorCode), custom_details_errorRemedy_s = tostring(cd.errorRemedy), custom_details_errorReason_s = tostring(cd.errorReason), custom_details_errorMessage_s = tostring(cd.errorMessage), custom_details_location_s = tostring(cd.location), custom_details_url_s = tostring(cd.url), custom_details_customerID_s = tostring(cd.customerID), custom_details_logicalSizeInBytes_s = tostring(cd.logicalSizeInBytes), custom_details_dataTransferredInBytes_s = tostring(cd.dataTransferredInBytes), custom_details_effectiveThroughput_s = tostring(cd.effectiveThroughput), custom_details_snapshotId_s = tostring(cd.snapshotId), custom_details_auditUserName_s = tostring(cd.auditUserName), custom_details_ipAddress_s = tostring(cd.ipAddress), custom_details_seriesId_g = toguid(iff(tostring(cd.seriesId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.seriesId), '')), custom_details_seriesId_s = iff(tostring(cd.seriesId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.seriesId)), custom_details_id_g = toguid(iff(tostring(cd.id) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.id), '')), custom_details_id_s = iff(tostring(cd.id) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.id)), custom_details_objectId_g = toguid(iff(tostring(cd.objectId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.objectId), '')), custom_details_objectId_s = iff(tostring(cd.objectId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.objectId)), custom_details_clusterId_g = toguid(iff(tostring(cd.clusterId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.clusterId), '')), custom_details_clusterId_s = iff(tostring(cd.clusterId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.clusterId)), custom_details_auditUserId_g = toguid(iff(tostring(cd.auditUserId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.auditUserId), '')), custom_details_auditUserId_s = iff(tostring(cd.auditUserId) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.auditUserId)), custom_details_objectFid_g = toguid(iff(tostring(cd.objectFid) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.objectFid), '')), custom_details_objectFid_s = iff(tostring(cd.objectFid) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.objectFid)), custom_details_orgID_g = toguid(iff(tostring(cd.orgID) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.orgID), '')), custom_details_orgID_s = iff(tostring(cd.orgID) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.orgID)), custom_details_seriesID_g = toguid(iff(tostring(cd.seriesID) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', tostring(cd.seriesID), '')), custom_details_seriesID_s = iff(tostring(cd.seriesID) matches regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '', tostring(cd.seriesID)), custom_details_isPolarisAudit_s = tostring(cd.isPolarisAudit), custom_details_userNote_s = tostring(cd.userNote), custom_details_eventInfo = cd.eventInfo, custom_details_auditInfo = cd.auditInfo" + } + ] + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(resourceGroup().id, variables('FunctionName'), variables('MonitoringMetricsPublisherRoleId'))]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('FunctionName'))]", + "[variables('webhookDcrId')]", + "[variables('eventsDcrId')]" + ], + "properties": { + "roleDefinitionId": "[variables('MonitoringMetricsPublisherRoleId')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('FunctionName')), '2018-11-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } } ] } \ No newline at end of file diff --git a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/shared_code/consts.py b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/shared_code/consts.py index 7ba95d1b91e..998327ca4a5 100644 --- a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/shared_code/consts.py +++ b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/shared_code/consts.py @@ -1,12 +1,54 @@ """This file contains all constants.""" import os -LOGS_STARTS_WITH = "Rubrik:" +LOGS_STARTS_WITH = "Rubrik" +LOG_FORMAT = "{} {}: (method = {}) : {}" DEFAULT_LOG_LEVEL = "INFO" LOG_LEVEL = os.environ.get("LogLevel", "") -WORKSPACE_ID = os.environ.get("WorkspaceID") -WORKSPACE_KEY = os.environ.get("WorkspaceKey") -ANOMALY_LOG_TYPE = os.environ.get("Anomalies_table_name", "Rubrik_Anomaly_Data") -RANSOMWARE_LOG_TYPE = os.environ.get("RansomwareAnalysis_table_name", "Rubrik_Ransomware_Data") -THREATHUNT_LOG_TYPE = os.environ.get("ThreatHunts_table_name", "Rubrik_ThreatHunt_Data") -EVENTS_LOG_TYPE = os.environ.get("Events_table_name", "Rubrik_Events_Data") +FUNCTION_APP_TIMEOUT_SECONDS = 570 + +# Custom log table names (workspace tables carry the _CL suffix) +ANOMALY_LOG_TYPE = os.environ.get("Anomalies_table_name", "Rubrik_Anomaly_Data_CL") +RANSOMWARE_LOG_TYPE = os.environ.get("RansomwareAnalysis_table_name", "Rubrik_Ransomware_Data_CL") +THREATHUNT_LOG_TYPE = os.environ.get("ThreatHunts_table_name", "Rubrik_ThreatHunt_Data_CL") +EVENTS_LOG_TYPE = os.environ.get("Events_table_name", "Rubrik_Events_Data_CL") +VIOLATION_LOG_TYPE = os.environ.get("Violation_table_name", "Rubrik_Violation_Data_CL") + +# DCR stream names (Custom-{TableName}) +ANOMALY_STREAM = "Custom-Rubrik_Anomaly_Data_CL" +RANSOMWARE_STREAM = "Custom-Rubrik_Ransomware_Data_CL" +THREATHUNT_STREAM = "Custom-Rubrik_ThreatHunt_Data_CL" +EVENTS_STREAM = "Custom-Rubrik_Events_Data_CL" +VIOLATION_STREAM = "Custom-Rubrik_Violation_Data_CL" + +# Log Ingestion API configuration +AZURE_DATA_COLLECTION_ENDPOINT = os.environ.get("AZURE_DATA_COLLECTION_ENDPOINT") +DCR_WEBHOOK_RULE_ID = os.environ.get("DCR_WEBHOOK_RULE_ID") +DCR_EVENTS_RULE_ID = os.environ.get("DCR_EVENTS_RULE_ID") +SCOPE = os.environ.get("SCOPE", "https://monitor.azure.com//.default") + +# Rubrik Security Cloud (RSC) API configuration — used by the Violation classification gate +RUBRIK_INSTANCE_URL = os.environ.get("RubrikInstanceUrl") +RUBRIK_CLIENT_ID = os.environ.get("RubrikClientId") +RUBRIK_CLIENT_SECRET = os.environ.get("RubrikClientSecret") +RSC_HTTP_TIMEOUT = 60 + +RSC_RESOLVE_SNAPSHOT_QUERY = ( + "query ResolveSnapshot($snappableId: String!, $beforeTime: DateTime!) " + "{ allSnapshotsClosestToPointInTime(snappableIds: [$snappableId], beforeTime: $beforeTime) " + "{ snappableId error snapshot { id date } } }" +) + +RSC_HIT_COUNT_QUERY = ( + "query ObjectHitCountQuery($snappableFid: String!, $snapshotFid: String!) " + "{ policyObj(snappableFid: $snappableFid, snapshotFid: $snapshotFid) " + "{ rootFileResult { hits { totalHits violations } } } }" +) + +# Classification-event discriminators (raw webhook JSON) +CLASSIFICATION_CLASS = "Classification" +CLASSIFICATION_EVENT_NAME = "ClassficationResultsAvailable" + +# Log Ingestion API retry configuration — used by RubrikActivity/sentinel.py +MAX_RETRIES = 3 +RETRYABLE_STATUS = (429, 500, 502, 503, 504) diff --git a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/shared_code/logger.py b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/shared_code/logger.py index a9c099e7668..769d8ac3352 100644 --- a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/shared_code/logger.py +++ b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/shared_code/logger.py @@ -8,7 +8,7 @@ applogger = None applogger = logging.getLogger("azure") log_level = log_level.upper() -numeric_level = getattr(logging, log_level, default_log_level) +numeric_level = getattr(logging, log_level, logging.INFO) applogger.setLevel(level=numeric_level) handler = logging.StreamHandler(stream=sys.stdout) applogger.addHandler(handler) diff --git a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/shared_code/rubrik_exception.py b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/shared_code/rubrik_exception.py index 4f5d5e3cc0a..7aacdd823e2 100644 --- a/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/shared_code/rubrik_exception.py +++ b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/shared_code/rubrik_exception.py @@ -1,13 +1,21 @@ -"""This File contains custom Exception class for Rubrik.""" +"""This file contains custom Exception classes for Rubrik.""" class RubrikException(Exception): - """Exception class to handle Rubrik exception. + """Base exception class to handle Rubrik errors. Args: - Exception (string): will print exception message. + message (str): exception message to surface. """ - def __init__(self, message: str) -> None: - """Initialize custom RubrikException with custom message.""" + def __init__(self, message: str = "") -> None: + """Initialize custom RubrikException with a custom message.""" super().__init__(message) + + +class RubrikTimeoutException(RubrikException): + """Raised when the function approaches the Azure Functions execution timeout.""" + + +class RubrikAuthenticationException(RubrikException): + """Raised when authentication to the Log Ingestion API fails after retries.""" diff --git a/Solutions/RubrikSecurityCloud/Data/Solution_RubrikSecurityCloud.json b/Solutions/RubrikSecurityCloud/Data/Solution_RubrikSecurityCloud.json index 27ad3616b91..88790119904 100644 --- a/Solutions/RubrikSecurityCloud/Data/Solution_RubrikSecurityCloud.json +++ b/Solutions/RubrikSecurityCloud/Data/Solution_RubrikSecurityCloud.json @@ -21,7 +21,10 @@ "Playbooks/RubrikUpdateAnomalyStatus/azuredeploy.json", "Playbooks/RubrikUpdateAnomalyStatusViaIncident/azuredeploy.json", "Playbooks/RubrikAdvanceThreatHunt/azuredeploy.json", - "Playbooks/RubrikTurboThreatHunt/azuredeploy.json" + "Playbooks/RubrikTurboThreatHunt/azuredeploy.json", + "Playbooks/RubrikDSPMViolationRemediation/azuredeploy.json", + "Playbooks/RubrikIRViolation/azuredeploy.json", + "Playbooks/RubrikQuarantineFiles/azuredeploy.json" ], "Data Connectors": [ "Data Connectors/RubrikWebhookEvents/RubrikWebhookEvents_FunctionApp.json", @@ -30,10 +33,13 @@ ], "Analytic Rules": [ "Analytic Rules/RubrikCriticalAnomaly.yaml", - "Analytic Rules/RubrikThreatMonitoring.yaml" + "Analytic Rules/RubrikThreatMonitoring.yaml", + "Analytic Rules/RubrikClassificationEvents.yaml", + "Analytic Rules/RubrikDataSecurityViolation.yaml", + "Analytic Rules/RubrikIdentitySecurityViolation.yaml" ], "BasePath": "C:\\GitHub\\Azure-Sentinel\\Solutions\\RubrikSecurityCloud", - "Version": "3.5.4", + "Version": "3.5.5", "Metadata": "SolutionMetadata.json", "TemplateSpec": false, "Is1PConnector": false diff --git a/Solutions/RubrikSecurityCloud/Package/3.5.2.zip b/Solutions/RubrikSecurityCloud/Package/3.5.2.zip index e8fbdde1346..45eb745ecff 100644 Binary files a/Solutions/RubrikSecurityCloud/Package/3.5.2.zip and b/Solutions/RubrikSecurityCloud/Package/3.5.2.zip differ diff --git a/Solutions/RubrikSecurityCloud/Package/3.5.5.zip b/Solutions/RubrikSecurityCloud/Package/3.5.5.zip new file mode 100644 index 00000000000..273b032a9a1 Binary files /dev/null and b/Solutions/RubrikSecurityCloud/Package/3.5.5.zip differ diff --git a/Solutions/RubrikSecurityCloud/Package/createUiDefinition.json b/Solutions/RubrikSecurityCloud/Package/createUiDefinition.json index 956b1002262..92abb64a494 100644 --- a/Solutions/RubrikSecurityCloud/Package/createUiDefinition.json +++ b/Solutions/RubrikSecurityCloud/Package/createUiDefinition.json @@ -6,7 +6,7 @@ "config": { "isWizard": false, "basics": { - "description": "\n\n**Note:** Please refer to the following before installing the solution: \n\n• Review the solution [Release Notes](https://github.com/Azure/Azure-Sentinel/tree/master/Solutions/RubrikSecurityCloud/ReleaseNotes.md)\n\n • There may be [known issues](https://aka.ms/sentinelsolutionsknownissues) pertaining to this Solution, please refer to them before installing.\n\nThe [Rubrik Security Cloud](https://www.rubrik.com/) solution enables security operations teams to integrate insights from Rubrik’s Data Observability services into Microsoft Sentinel. \n\n**Underlying Microsoft Technologies used:**\n\nThis solution takes a dependency on the following technologies, and some of these dependencies either may be in [Preview](https://azure.microsoft.com/support/legal/preview-supplemental-terms/) state or might result in additional ingestion or operational costs:\n\na. [Azure Monitor HTTP Data Collector API](https://learn.microsoft.com/azure/azure-monitor/logs/data-collector-api)\n\nb. [Azure Functions](https://azure.microsoft.com/products/functions/#overview)\n\n**Data Connectors:** 3, **Analytic Rules:** 2, **Custom Azure Logic Apps Connectors:** 1, **Playbooks:** 17\n\n[Learn more about Microsoft Sentinel](https://aka.ms/azuresentinel) | [Learn more about Solutions](https://aka.ms/azuresentinelsolutionsdoc)", + "description": "\n\n**Note:** Please refer to the following before installing the solution: \n\n• Review the solution [Release Notes](https://github.com/Azure/Azure-Sentinel/tree/master/Solutions/RubrikSecurityCloud/ReleaseNotes.md)\n\n • There may be [known issues](https://aka.ms/sentinelsolutionsknownissues) pertaining to this Solution, please refer to them before installing.\n\nThe [Rubrik Security Cloud](https://www.rubrik.com/) solution enables security operations teams to integrate insights from Rubrik’s Data Observability services into Microsoft Sentinel. \n\n**Underlying Microsoft Technologies used:**\n\nThis solution takes a dependency on the following technologies, and some of these dependencies either may be in [Preview](https://azure.microsoft.com/support/legal/preview-supplemental-terms/) state or might result in additional ingestion or operational costs:\n\na. [Azure Monitor HTTP Data Collector API](https://learn.microsoft.com/azure/azure-monitor/logs/data-collector-api)\n\nb. [Azure Functions](https://azure.microsoft.com/products/functions/#overview)\n\n**Data Connectors:** 3, **Analytic Rules:** 5, **Custom Azure Logic Apps Connectors:** 1, **Playbooks:** 20\n\n[Learn more about Microsoft Sentinel](https://aka.ms/azuresentinel) | [Learn more about Solutions](https://aka.ms/azuresentinelsolutionsdoc)", "subscription": { "resourceProviders": [ "Microsoft.OperationsManagement/solutions", @@ -152,6 +152,48 @@ } } ] + }, + { + "name": "analytic3", + "type": "Microsoft.Common.Section", + "label": "Create Incident for Classification events for non DSPM license users", + "elements": [ + { + "name": "analytic3-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Creates an incident for Classification events (ClassificationResultsAvailable) for non DSPM license users." + } + } + ] + }, + { + "name": "analytic4", + "type": "Microsoft.Common.Section", + "label": "Create Incident for Data Security Violation events for DSPM license users", + "elements": [ + { + "name": "analytic4-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Creates an incident for Data Security Violation events for DSPM license users." + } + } + ] + }, + { + "name": "analytic5", + "type": "Microsoft.Common.Section", + "label": "Create Incident for Identity Security Violation and Identity Alerts events for IR violations", + "elements": [ + { + "name": "analytic5-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Creates an incident for Identity Security Violation and Identity Alerts events for IR violations." + } + } + ] } ] }, diff --git a/Solutions/RubrikSecurityCloud/Package/mainTemplate.json b/Solutions/RubrikSecurityCloud/Package/mainTemplate.json index 66381fde45c..7b31e687265 100644 --- a/Solutions/RubrikSecurityCloud/Package/mainTemplate.json +++ b/Solutions/RubrikSecurityCloud/Package/mainTemplate.json @@ -47,7 +47,7 @@ "email": "ben.meadowcroft@rubrik.com", "_email": "[variables('email')]", "_solutionName": "RubrikSecurityCloud", - "_solutionVersion": "3.5.4", + "_solutionVersion": "3.5.5", "solutionId": "rubrik_inc.rubrik_sentinel", "_solutionId": "[variables('solutionId')]", "RubrikCustomConnector": "RubrikCustomConnector", @@ -197,6 +197,30 @@ "playbookId18": "[resourceId('Microsoft.Logic/workflows', variables('playbookContentId18'))]", "playbookTemplateSpecName18": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-pl-',uniquestring(variables('_playbookContentId18'))))]", "_playbookcontentProductId18": "[concat(take(variables('_solutionId'),50),'-','pl','-', uniqueString(concat(variables('_solutionId'),'-','Playbook','-',variables('_playbookContentId18'),'-', variables('playbookVersion18'))))]", + "RubrikDSPMViolationRemediation": "RubrikDSPMViolationRemediation", + "_RubrikDSPMViolationRemediation": "[variables('RubrikDSPMViolationRemediation')]", + "playbookVersion19": "1.6.0", + "playbookContentId19": "RubrikDSPMViolationRemediation", + "_playbookContentId19": "[variables('playbookContentId19')]", + "playbookId19": "[resourceId('Microsoft.Logic/workflows', variables('playbookContentId19'))]", + "playbookTemplateSpecName19": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-pl-',uniquestring(variables('_playbookContentId19'))))]", + "_playbookcontentProductId19": "[concat(take(variables('_solutionId'),50),'-','pl','-', uniqueString(concat(variables('_solutionId'),'-','Playbook','-',variables('_playbookContentId19'),'-', variables('playbookVersion19'))))]", + "RubrikIRViolation": "RubrikIRViolation", + "_RubrikIRViolation": "[variables('RubrikIRViolation')]", + "playbookVersion20": "1.0", + "playbookContentId20": "RubrikIRViolation", + "_playbookContentId20": "[variables('playbookContentId20')]", + "playbookId20": "[resourceId('Microsoft.Logic/workflows', variables('playbookContentId20'))]", + "playbookTemplateSpecName20": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-pl-',uniquestring(variables('_playbookContentId20'))))]", + "_playbookcontentProductId20": "[concat(take(variables('_solutionId'),50),'-','pl','-', uniqueString(concat(variables('_solutionId'),'-','Playbook','-',variables('_playbookContentId20'),'-', variables('playbookVersion20'))))]", + "RubrikQuarantineFiles": "RubrikQuarantineFiles", + "_RubrikQuarantineFiles": "[variables('RubrikQuarantineFiles')]", + "playbookVersion21": "3.2.0", + "playbookContentId21": "RubrikQuarantineFiles", + "_playbookContentId21": "[variables('playbookContentId21')]", + "playbookId21": "[resourceId('Microsoft.Logic/workflows', variables('playbookContentId21'))]", + "playbookTemplateSpecName21": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-pl-',uniquestring(variables('_playbookContentId21'))))]", + "_playbookcontentProductId21": "[concat(take(variables('_solutionId'),50),'-','pl','-', uniqueString(concat(variables('_solutionId'),'-','Playbook','-',variables('_playbookContentId21'),'-', variables('playbookVersion21'))))]", "uiConfigId1": "RubrikSecurityCloudAzureFunctions", "_uiConfigId1": "[variables('uiConfigId1')]", "dataConnectorContentId1": "RubrikSecurityCloudAzureFunctions", @@ -206,7 +230,7 @@ "dataConnectorTemplateSpecName1": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-dc-',uniquestring(variables('_dataConnectorContentId1'))))]", "dataConnectorVersion1": "1.0.0", "_dataConnectorcontentProductId1": "[concat(take(variables('_solutionId'),50),'-','dc','-', uniqueString(concat(variables('_solutionId'),'-','DataConnector','-',variables('_dataConnectorContentId1'),'-', variables('dataConnectorVersion1'))))]", - "dataConnectorCCPVersion": "3.5.4", + "dataConnectorCCPVersion": "3.5.5", "_dataConnectorContentIdConnectorDefinition2": "RubrikProtectionStatus", "dataConnectorTemplateNameConnectorDefinition2": "[concat(parameters('workspace'),'-dc-',uniquestring(variables('_dataConnectorContentIdConnectorDefinition2')))]", "_dataConnectorContentIdConnections2": "RubrikProtectionStatusConnections", @@ -230,6 +254,27 @@ "analyticRuleTemplateSpecName2": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('0083cbc4-776e-42ca-8694-6950fd605df9')))]", "_analyticRulecontentProductId2": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','0083cbc4-776e-42ca-8694-6950fd605df9','-', '1.0.0')))]" }, + "analyticRuleObject3": { + "analyticRuleVersion3": "1.0.0", + "_analyticRulecontentId3": "9563dc15-ea0a-438a-9d50-7ab243378241", + "analyticRuleId3": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', '9563dc15-ea0a-438a-9d50-7ab243378241')]", + "analyticRuleTemplateSpecName3": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('9563dc15-ea0a-438a-9d50-7ab243378241')))]", + "_analyticRulecontentProductId3": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','9563dc15-ea0a-438a-9d50-7ab243378241','-', '1.0.0')))]" + }, + "analyticRuleObject4": { + "analyticRuleVersion4": "1.0.0", + "_analyticRulecontentId4": "5b70b78e-397c-474c-a556-f6c37fccbb18", + "analyticRuleId4": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', '5b70b78e-397c-474c-a556-f6c37fccbb18')]", + "analyticRuleTemplateSpecName4": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('5b70b78e-397c-474c-a556-f6c37fccbb18')))]", + "_analyticRulecontentProductId4": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','5b70b78e-397c-474c-a556-f6c37fccbb18','-', '1.0.0')))]" + }, + "analyticRuleObject5": { + "analyticRuleVersion5": "1.0.0", + "_analyticRulecontentId5": "77e7519f-be7e-41fc-8f02-76bb0a7d5496", + "analyticRuleId5": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', '77e7519f-be7e-41fc-8f02-76bb0a7d5496')]", + "analyticRuleTemplateSpecName5": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('77e7519f-be7e-41fc-8f02-76bb0a7d5496')))]", + "_analyticRulecontentProductId5": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','77e7519f-be7e-41fc-8f02-76bb0a7d5496','-', '1.0.0')))]" + }, "_solutioncontentProductId": "[concat(take(variables('_solutionId'),50),'-','sl','-', uniqueString(concat(variables('_solutionId'),'-','Solution','-',variables('_solutionId'),'-', variables('_solutionVersion'))))]" }, "resources": [ @@ -242,7 +287,7 @@ "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" ], "properties": { - "description": "RubrikCustomConnector Playbook with template version 3.5.4", + "description": "RubrikCustomConnector Playbook with template version 3.5.5", "mainTemplate": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "[variables('playbookVersion1')]", @@ -416,7 +461,7 @@ "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" ], "properties": { - "description": "RubrikAnomalyAnalysis Playbook with template version 3.5.4", + "description": "RubrikAnomalyAnalysis Playbook with template version 3.5.5", "mainTemplate": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "[variables('playbookVersion2')]", @@ -3831,7 +3876,7 @@ "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" ], "properties": { - "description": "RubrikAnomalyIncidentResponse Playbook with template version 3.5.4", + "description": "RubrikAnomalyIncidentResponse Playbook with template version 3.5.5", "mainTemplate": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "[variables('playbookVersion3')]", @@ -4551,7 +4596,7 @@ "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" ], "properties": { - "description": "RubrikDataObjectDiscovery Playbook with template version 3.5.4", + "description": "RubrikDataObjectDiscovery Playbook with template version 3.5.5", "mainTemplate": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "[variables('playbookVersion4')]", @@ -7169,7 +7214,7 @@ "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" ], "properties": { - "description": "RubrikFilesetRansomwareDiscovery Playbook with template version 3.5.4", + "description": "RubrikFilesetRansomwareDiscovery Playbook with template version 3.5.5", "mainTemplate": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "[variables('playbookVersion5')]", @@ -7819,7 +7864,7 @@ "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" ], "properties": { - "description": "RubrikIOCScan Playbook with template version 3.5.4", + "description": "RubrikIOCScan Playbook with template version 3.5.5", "mainTemplate": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "[variables('playbookVersion6')]", @@ -10280,7 +10325,7 @@ "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" ], "properties": { - "description": "RubrikPollAsyncResult Playbook with template version 3.5.4", + "description": "RubrikPollAsyncResult Playbook with template version 3.5.5", "mainTemplate": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "[variables('playbookVersion7')]", @@ -11150,7 +11195,7 @@ "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" ], "properties": { - "description": "RubrikRansomwareDiscoveryAndFileRecovery Playbook with template version 3.5.4", + "description": "RubrikRansomwareDiscoveryAndFileRecovery Playbook with template version 3.5.5", "mainTemplate": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "[variables('playbookVersion8')]", @@ -13088,7 +13133,7 @@ "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" ], "properties": { - "description": "RubrikRansomwareDiscoveryAndVMRecovery Playbook with template version 3.5.4", + "description": "RubrikRansomwareDiscoveryAndVMRecovery Playbook with template version 3.5.5", "mainTemplate": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "[variables('playbookVersion9')]", @@ -17224,7 +17269,7 @@ "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" ], "properties": { - "description": "RubrikFileObjectContextAnalysis Playbook with template version 3.5.4", + "description": "RubrikFileObjectContextAnalysis Playbook with template version 3.5.5", "mainTemplate": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "[variables('playbookVersion10')]", @@ -20491,7 +20536,7 @@ "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" ], "properties": { - "description": "RubrikUserIntelligenceAnalysis Playbook with template version 3.5.4", + "description": "RubrikUserIntelligenceAnalysis Playbook with template version 3.5.5", "mainTemplate": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "[variables('playbookVersion11')]", @@ -22481,7 +22526,7 @@ "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" ], "properties": { - "description": "RubrikRetrieveUserIntelligenceInformation Playbook with template version 3.5.4", + "description": "RubrikRetrieveUserIntelligenceInformation Playbook with template version 3.5.5", "mainTemplate": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "[variables('playbookVersion12')]", @@ -24186,7 +24231,7 @@ "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" ], "properties": { - "description": "RubrikAnomalyGenerateDownloadableLink Playbook with template version 3.5.4", + "description": "RubrikAnomalyGenerateDownloadableLink Playbook with template version 3.5.5", "mainTemplate": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "[variables('playbookVersion13')]", @@ -25544,7 +25589,7 @@ "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" ], "properties": { - "description": "RubrikWorkloadAnalysis Playbook with template version 3.5.4", + "description": "RubrikWorkloadAnalysis Playbook with template version 3.5.5", "mainTemplate": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "[variables('playbookVersion14')]", @@ -27474,7 +27519,7 @@ "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" ], "properties": { - "description": "RubrikUpdateAnomalyStatus Playbook with template version 3.5.4", + "description": "RubrikUpdateAnomalyStatus Playbook with template version 3.5.5", "mainTemplate": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "[variables('playbookVersion15')]", @@ -27950,7 +27995,7 @@ "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" ], "properties": { - "description": "RubrikUpdateAnomalyStatusViaIncident Playbook with template version 3.5.4", + "description": "RubrikUpdateAnomalyStatusViaIncident Playbook with template version 3.5.5", "mainTemplate": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "[variables('playbookVersion16')]", @@ -29135,7 +29180,7 @@ "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" ], "properties": { - "description": "RubrikAdvancedThreatHunt Playbook with template version 3.5.4", + "description": "RubrikAdvancedThreatHunt Playbook with template version 3.5.5", "mainTemplate": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "[variables('playbookVersion17')]", @@ -31663,7 +31708,7 @@ "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" ], "properties": { - "description": "RubrikTurboThreatHunt Playbook with template version 3.5.4", + "description": "RubrikTurboThreatHunt Playbook with template version 3.5.5", "mainTemplate": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "[variables('playbookVersion18')]", @@ -33593,443 +33638,8327 @@ { "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", "apiVersion": "2023-04-01-preview", - "name": "[variables('dataConnectorTemplateSpecName1')]", + "name": "[variables('playbookTemplateSpecName19')]", "location": "[parameters('workspace-location')]", "dependsOn": [ "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" ], "properties": { - "description": "RubrikSecurityCloud data connector with template version 3.5.4", + "description": "RubrikDSPMViolationRemediation Playbook with template version 3.5.5", "mainTemplate": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "[variables('dataConnectorVersion1')]", - "parameters": {}, - "variables": {}, - "resources": [ - { - "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',variables('_dataConnectorContentId1'))]", - "apiVersion": "2021-03-01-preview", - "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", - "location": "[parameters('workspace-location')]", - "kind": "GenericUI", - "properties": { - "connectorUiConfig": { - "id": "[variables('_uiConfigId1')]", - "title": "Rubrik Security Cloud data connector (using Azure Functions)", - "publisher": "Rubrik, Inc", - "descriptionMarkdown": "The Rubrik Security Cloud data connector enables security operations teams to integrate insights from Rubrik's Data Observability services into Microsoft Sentinel. The insights include identification of anomalous filesystem behavior associated with ransomware and mass deletion, assess the blast radius of a ransomware attack, and sensitive data operators to prioritize and more rapidly investigate potential incidents.", - "graphQueries": [ - { - "metricName": "Total Anomaly Event data received", - "legend": "Rubrik_Anomaly_Data_CL", - "baseQuery": "Rubrik_Anomaly_Data_CL" - }, - { - "metricName": "Total Ransomware Event data received", - "legend": "Rubrik_Ransomware_Data_CL", - "baseQuery": "Rubrik_Ransomware_Data_CL" - }, - { - "metricName": "Total ThreatHunt Event data received", - "legend": "Rubrik_ThreatHunt_Data_CL", - "baseQuery": "Rubrik_ThreatHunt_Data_CL" - }, - { - "metricName": "Total Other Events data received", - "legend": "Rubrik_Events_Data_CL", - "baseQuery": "Rubrik_Events_Data_CL" - } - ], - "sampleQueries": [ - { - "description": "Rubrik Anomaly Events - Anomaly Events for all severity types.", - "query": "Rubrik_Anomaly_Data_CL\n | sort by TimeGenerated desc" - }, - { - "description": "Rubrik Ransomware Analysis Events - Ransomware Analysis Events for all severity types.", - "query": "Rubrik_Ransomware_Data_CL\n | sort by TimeGenerated desc" - }, - { - "description": "Rubrik ThreatHunt Events - Threat Hunt Events for all severity types.", - "query": "Rubrik_ThreatHunt_Data_CL\n | sort by TimeGenerated desc" - }, - { - "description": "Rubrik Other Events - Other Events for all severity types.", - "query": "Rubrik_Events_Data_CL\n | sort by TimeGenerated desc" - } - ], - "dataTypes": [ - { - "name": "Rubrik_Anomaly_Data_CL", - "lastDataReceivedQuery": "Rubrik_Anomaly_Data_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" - }, - { - "name": "Rubrik_Ransomware_Data_CL", - "lastDataReceivedQuery": "Rubrik_Ransomware_Data_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" - }, - { - "name": "Rubrik_ThreatHunt_Data_CL", - "lastDataReceivedQuery": "Rubrik_ThreatHunt_Data_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" - }, - { - "name": "Rubrik_Events_Data_CL", - "lastDataReceivedQuery": "Rubrik_Events_Data_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" - } - ], - "connectivityCriterias": [ - { - "type": "IsConnectedQuery", - "value": [ - "Rubrik_Anomaly_Data_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" - ] - }, - { - "type": "IsConnectedQuery", - "value": [ - "Rubrik_Ransomware_Data_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" - ] - }, - { - "type": "IsConnectedQuery", - "value": [ - "Rubrik_ThreatHunt_Data_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" - ] - }, - { - "type": "IsConnectedQuery", - "value": [ - "Rubrik_Events_Data_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" - ] - } - ], - "availability": { - "status": 1, - "isPreview": false - }, - "permissions": { - "resourceProvider": [ - { - "provider": "Microsoft.OperationalInsights/workspaces", - "permissionsDisplayText": "read and write permissions on the workspace are required.", - "providerDisplayName": "Workspace", - "scope": "Workspace", - "requiredPermissions": { - "write": true, - "read": true, - "delete": true - } - }, - { - "provider": "Microsoft.OperationalInsights/workspaces/sharedKeys", - "permissionsDisplayText": "read permissions to shared keys for the workspace are required. [See the documentation to learn more about workspace keys](https://docs.microsoft.com/azure/azure-monitor/platform/agent-windows#obtain-workspace-id-and-key).", - "providerDisplayName": "Keys", - "scope": "Workspace", - "requiredPermissions": { - "action": true - } - } - ], - "customs": [ - { - "name": "Microsoft.Web/sites permissions", - "description": "Read and write permissions to Azure Functions to create a Function App is required. [See the documentation to learn more about Azure Functions](https://docs.microsoft.com/azure/azure-functions/)." - } - ] - }, - "instructionSteps": [ - { - "description": ">**NOTE:** This connector uses Azure Functions to connect to the Rubrik webhook which push its logs into Microsoft Sentinel. This might result in additional data ingestion costs. Check the [Azure Functions pricing page](https://azure.microsoft.com/pricing/details/functions/) for details." - }, - { - "description": ">**(Optional Step)** Securely store workspace and API authorization key(s) or token(s) in Azure Key Vault. Azure Key Vault provides a secure mechanism to store and retrieve key values. [Follow these instructions](https://docs.microsoft.com/azure/app-service/app-service-key-vault-references) to use Azure Key Vault with an Azure Function App." - }, - { - "description": "**STEP 1 - Choose ONE from the following two deployment options to deploy the connector and the associated Azure Function**\n\n>**IMPORTANT:** Before deploying the Rubrik Microsoft Sentinel data connector, have the Workspace ID and Workspace Primary Key (can be copied from the following) readily available..", - "instructions": [ - { - "parameters": { - "fillWith": [ - "WorkspaceId" - ], - "label": "Workspace ID" - }, - "type": "CopyableLabel" - }, - { - "parameters": { - "fillWith": [ - "PrimaryKey" - ], - "label": "Primary Key" - }, - "type": "CopyableLabel" - } - ] - }, - { - "description": "Use this method for automated deployment of the Rubrik connector.\n\n1. Click the **Deploy to Azure** button below. \n\n\t[![Deploy To Azure](https://aka.ms/deploytoazurebutton)](https://aka.ms/sentinel-RubrikWebhookEvents-azuredeploy)\n2. Select the preferred **Subscription**, **Resource Group** and **Location**. \n3. Enter the below information : \n\t\tFunction Name \n\t\tWorkspace ID \n\t\tWorkspace Key \n\t\tAnomaliesTableName \n\t\tRansomwareAnalysisTableName \n\t\tThreatHuntsTableName \n\t\tEventsTableName \n\t\tLogLevel \n \n4. Mark the checkbox labeled **I agree to the terms and conditions stated above**. \n5. Click **Purchase** to deploy.", - "title": "Option 1 - Azure Resource Manager (ARM) Template" - }, - { - "description": "Use the following step-by-step instructions to deploy the Rubrik Microsoft Sentinel data connector manually with Azure Functions (Deployment via Visual Studio Code).", - "title": "Option 2 - Manual Deployment of Azure Functions" - }, - { - "description": "**1. Deploy a Function App**\n\n> **NOTE:** You will need to [prepare VS code](https://docs.microsoft.com/azure/azure-functions/functions-create-first-function-python#prerequisites) for Azure function development.\n\n1. Download the [Azure Function App](https://aka.ms/sentinel-RubrikWebhookEvents-functionapp) file. Extract archive to your local development computer.\n2. Start VS Code. Choose File in the main menu and select Open Folder.\n3. Select the top level folder from extracted files.\n4. Choose the Azure icon in the Activity bar, then in the **Azure: Functions** area, choose the **Deploy to function app** button.\nIf you aren't already signed in, choose the Azure icon in the Activity bar, then in the **Azure: Functions** area, choose **Sign in to Azure**\nIf you're already signed in, go to the next step.\n5. Provide the following information at the prompts:\n\n\ta. **Select folder:** Choose a folder from your workspace or browse to one that contains your function app.\n\n\tb. **Select Subscription:** Choose the subscription to use.\n\n\tc. Select **Create new Function App in Azure** (Don't choose the Advanced option)\n\n\td. **Enter a globally unique name for the function app:** Type a name that is valid in a URL path. The name you type is validated to make sure that it's unique in Azure Functions. (e.g. RubrikXXXXX).\n\n\te. **Select a runtime:** Choose Python 3.8 or above.\n\n\tf. Select a location for new resources. For better performance and lower costs choose the same [region](https://azure.microsoft.com/regions/) where Microsoft Sentinel is located.\n\n6. Deployment will begin. A notification is displayed after your function app is created and the deployment package is applied.\n7. Go to Azure Portal for the Function App configuration." - }, - { - "description": "**2. Configure the Function App**\n\n1. In the Function App, select the Function App Name and select **Configuration**.\n2. In the **Application settings** tab, select **+ New application setting**.\n3. Add each of the following application settings individually, with their respective values (case-sensitive): \n\t\tWorkspaceID\n\t\tWorkspaceKey\n\t\tAnomaliesTableName\n\t\tRansomwareAnalysisTableName\n\t\tThreatHuntsTableName\n\t\tEventsTableName\n\t\tLogLevel\n\t\tlogAnalyticsUri (optional)\n - Use logAnalyticsUri to override the log analytics API endpoint for dedicated cloud. For example, for public cloud, leave the value empty; for Azure GovUS cloud environment, specify the value in the following format: https://.ods.opinsights.azure.us. \n4. Once all application settings have been entered, click **Save**." - }, - { - "description": "**Post Deployment steps**\n\n" - }, - { - "description": "1. Go to Azure function Overview page and Click on **\"Functions\"** tab.\n2. Click on the function called **\"RubrikHttpStarter\"**.\n3. Go to **\"GetFunctionurl\"** and copy the function url.", - "title": "1) Get the Function app endpoint" - }, - { - "description": "Follow the Rubrik User Guide instructions to [Add a Webhook](https://docs.rubrik.com/en-us/saas/saas/common/adding_webhook.html) to begin receiving event information \n 1. Select the Microsoft Sentinel as the webhook Provider \n 2. Enter the desired Webhook name \n 3. Enter the URL part from copied Function-url as the webhook URL endpoint and replace **{functionname}** with **\"RubrikAnomalyOrchestrator\"**, for the Rubrik Microsoft Sentinel Solution \n 4. Select the EventType as Anomaly \n 5. Select the following severity levels: Critical, Warning, Informational \n 6. Choose multiple log types, if desired, when running **\"RubrikEventsOrchestrator\"** \n 7. Repeat the same steps to add webhooks for Anomaly Detection Analysis, Threat Hunt and Other Events.\n \n\n NOTE: while adding webhooks for Anomaly Detection Analysis, Threat Hunt and Other Events, replace **{functionname}** with **\"RubrikRansomwareOrchestrator\"**, **\"RubrikThreatHuntOrchestrator\"** and **\"RubrikEventsOrchestrator\"** respectively in copied function-url.", - "title": "2) Add a webhook in RubrikSecurityCloud to send data to Microsoft Sentinel." - }, - { - "description": "*Now we are done with the rubrik Webhook configuration. Once the webhook events triggered , you should be able to see the Anomaly, Anomaly Detection Analysis, Threat Hunt events and Other Events from the Rubrik into respective LogAnalytics workspace table called \"Rubrik_Anomaly_Data_CL\", \"Rubrik_Ransomware_Data_CL\", \"Rubrik_ThreatHunt_Data_CL\", and \"Rubrik_Events_Data_CL\".*\n\n" - } - ] - } + "contentVersion": "[variables('playbookVersion19')]", + "parameters": { + "PlaybookName": { + "defaultValue": "RubrikDSPMViolationRemediation", + "type": "string", + "metadata": { + "description": "Name of the Logic App / playbook resource." } }, - { - "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", - "apiVersion": "2023-04-01-preview", - "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', last(split(variables('_dataConnectorId1'),'/'))))]", - "properties": { - "parentId": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectors', variables('_dataConnectorContentId1'))]", - "contentId": "[variables('_dataConnectorContentId1')]", - "kind": "DataConnector", - "version": "[variables('dataConnectorVersion1')]", - "source": { - "kind": "Solution", - "name": "RubrikSecurityCloud", - "sourceId": "[variables('_solutionId')]" - }, - "author": { - "name": "Ben Meadowcroft", - "email": "[variables('_email')]" - }, - "support": { - "name": "Rubrik", - "email": "support@rubrik.com", - "tier": "Partner", - "link": "https://support.rubrik.com" - } + "API Hostname": { + "type": "string", + "metadata": { + "description": "Hostname of the Rubrik Security Cloud instance (e.g. customer.my.rubrik.com)." } - } - ] - }, - "packageKind": "Solution", - "packageVersion": "[variables('_solutionVersion')]", - "packageName": "[variables('_solutionName')]", - "packageId": "[variables('_solutionId')]", - "contentSchemaVersion": "3.0.0", - "contentId": "[variables('_dataConnectorContentId1')]", - "contentKind": "DataConnector", - "displayName": "Rubrik Security Cloud data connector (using Azure Functions)", - "contentProductId": "[variables('_dataConnectorcontentProductId1')]", - "id": "[variables('_dataConnectorcontentProductId1')]", - "version": "[variables('dataConnectorVersion1')]" - } - }, - { - "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", - "apiVersion": "2023-04-01-preview", - "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', last(split(variables('_dataConnectorId1'),'/'))))]", - "dependsOn": [ - "[variables('_dataConnectorId1')]" - ], - "location": "[parameters('workspace-location')]", - "properties": { - "parentId": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectors', variables('_dataConnectorContentId1'))]", - "contentId": "[variables('_dataConnectorContentId1')]", - "kind": "DataConnector", - "version": "[variables('dataConnectorVersion1')]", - "source": { - "kind": "Solution", - "name": "RubrikSecurityCloud", - "sourceId": "[variables('_solutionId')]" - }, - "author": { - "name": "Ben Meadowcroft", - "email": "[variables('_email')]" - }, - "support": { - "name": "Rubrik", - "email": "support@rubrik.com", - "tier": "Partner", - "link": "https://support.rubrik.com" - } - } - }, - { - "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',variables('_dataConnectorContentId1'))]", - "apiVersion": "2021-03-01-preview", - "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", - "location": "[parameters('workspace-location')]", - "kind": "GenericUI", - "properties": { - "connectorUiConfig": { - "title": "Rubrik Security Cloud data connector (using Azure Functions)", - "publisher": "Rubrik, Inc", - "descriptionMarkdown": "The Rubrik Security Cloud data connector enables security operations teams to integrate insights from Rubrik's Data Observability services into Microsoft Sentinel. The insights include identification of anomalous filesystem behavior associated with ransomware and mass deletion, assess the blast radius of a ransomware attack, and sensitive data operators to prioritize and more rapidly investigate potential incidents.", - "graphQueries": [ - { - "metricName": "Total Anomaly Event data received", - "legend": "Rubrik_Anomaly_Data_CL", - "baseQuery": "Rubrik_Anomaly_Data_CL" }, - { - "metricName": "Total Ransomware Event data received", - "legend": "Rubrik_Ransomware_Data_CL", - "baseQuery": "Rubrik_Ransomware_Data_CL" + "Rubrik Connector name": { + "defaultValue": "RubrikCustomConnector", + "type": "string", + "metadata": { + "description": "Name of the previously deployed Rubrik custom connector (Microsoft.Web/customApis)." + } }, - { - "metricName": "Total ThreatHunt Event data received", - "legend": "Rubrik_ThreatHunt_Data_CL", - "baseQuery": "Rubrik_ThreatHunt_Data_CL" + "KeyVaultName": { + "type": "string", + "metadata": { + "description": "Key Vault holding the RSC service-account secrets 'Rubrik-AS-Int-ClientId' and 'Rubrik-AS-Int-ClientSecret'." + } }, - { - "metricName": "Total Other Events data received", - "legend": "Rubrik_Events_Data_CL", - "baseQuery": "Rubrik_Events_Data_CL" - } - ], - "dataTypes": [ - { - "name": "Rubrik_Anomaly_Data_CL", - "lastDataReceivedQuery": "Rubrik_Anomaly_Data_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" + "RubrikQuarantineFilesPlaybookName": { + "defaultValue": "RubrikQuarantineFiles", + "type": "string", + "metadata": { + "description": "Logic App name of the RubrikQuarantineFiles sub-playbook (HTTP Request-triggered) in this resource group. Called once per file to quarantine." + } }, - { - "name": "Rubrik_Ransomware_Data_CL", - "lastDataReceivedQuery": "Rubrik_Ransomware_Data_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" + "StorageAccountName": { + "type": "string", + "metadata": { + "description": "Azure Storage account name used to persist the exported CSV/log (SecurityViolation flow only)." + } }, - { - "name": "Rubrik_ThreatHunt_Data_CL", - "lastDataReceivedQuery": "Rubrik_ThreatHunt_Data_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" + "StorageContainerName": { + "type": "string", + "metadata": { + "description": "Name of an EXISTING blob container in StorageAccountName where the violation-files CSV and remediation log are uploaded. The container must be created beforehand; the playbook does not create it at runtime." + } }, - { - "name": "Rubrik_Events_Data_CL", - "lastDataReceivedQuery": "Rubrik_Events_Data_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" - } - ], - "connectivityCriterias": [ - { - "type": "IsConnectedQuery", - "value": [ - "Rubrik_Anomaly_Data_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" - ] + "SasTokenTtlHours": { + "defaultValue": 72, + "type": "int", + "metadata": { + "description": "Validity window (hours) for the read-only SAS link added to the incident comment." + } }, - { - "type": "IsConnectedQuery", - "value": [ - "Rubrik_Ransomware_Data_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" - ] + "TeamsGroupId": { + "type": "string", + "metadata": { + "description": "Microsoft Teams team (group) ID for the owner-notification card." + } }, - { - "type": "IsConnectedQuery", - "value": [ - "Rubrik_ThreatHunt_Data_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" - ] + "TeamsChannelId": { + "type": "string", + "metadata": { + "description": "Microsoft Teams channel ID (within TeamsGroupId) for the owner-notification card." + } }, - { - "type": "IsConnectedQuery", - "value": [ - "Rubrik_Events_Data_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" - ] + "DownloadPollIntervalSec": { + "defaultValue": 15, + "type": "int", + "metadata": { + "description": "Seconds to wait between Download Bar polls." + } + }, + "DownloadPollMaxAttempts": { + "defaultValue": 20, + "type": "int", + "metadata": { + "description": "Maximum Download Bar poll attempts before giving up on an export." + } } - ], - "sampleQueries": [ + }, + "variables": { + "MicrosoftSentinelConnectionName": "[[concat('azuresentinel-', parameters('PlaybookName'))]", + "RubrikcustomconnectorConnectionName": "[[concat('Rubrikcustomconnector-', parameters('PlaybookName'))]", + "KeyvaultConnectionName": "[[concat('keyvault-', parameters('PlaybookName'))]", + "TeamsConnectionName": "[[concat('teams-', parameters('PlaybookName'))]", + "AzureblobConnectionName": "[[concat('azureblob-', parameters('PlaybookName'))]", + "connection-1": "[[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', variables('workspace-location-inline'), '/managedApis/azuresentinel')]", + "_connection-1": "[[variables('connection-1')]", + "connection-2": "[[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', parameters('Rubrik Connector name'))]", + "_connection-2": "[[variables('connection-2')]", + "connection-3": "[[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', variables('workspace-location-inline'), '/managedApis/keyvault')]", + "_connection-3": "[[variables('connection-3')]", + "connection-4": "[[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', variables('workspace-location-inline'), '/managedApis/teams')]", + "_connection-4": "[[variables('connection-4')]", + "connection-5": "[[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', variables('workspace-location-inline'), '/managedApis/azureblob')]", + "_connection-5": "[[variables('connection-5')]", + "workspace-location-inline": "[concat('[resourceGroup().locatio', 'n]')]", + "workspace-name": "[parameters('workspace')]", + "workspaceResourceId": "[[resourceId('microsoft.OperationalInsights/Workspaces', variables('workspace-name'))]" + }, + "resources": [ { - "description": "Rubrik Anomaly Events - Anomaly Events for all severity types.", - "query": "Rubrik_Anomaly_Data_CL\n | sort by TimeGenerated desc" + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[[variables('MicrosoftSentinelConnectionName')]", + "location": "[[variables('workspace-location-inline')]", + "kind": "V1", + "properties": { + "displayName": "[[variables('MicrosoftSentinelConnectionName')]", + "parameterValueType": "Alternative", + "api": { + "id": "[[variables('_connection-1')]" + } + } }, { - "description": "Rubrik Ransomware Analysis Events - Ransomware Analysis Events for all severity types.", - "query": "Rubrik_Ransomware_Data_CL\n | sort by TimeGenerated desc" + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[[variables('RubrikcustomconnectorConnectionName')]", + "location": "[[variables('workspace-location-inline')]", + "properties": { + "displayName": "[[variables('RubrikcustomconnectorConnectionName')]", + "api": { + "id": "[[variables('_connection-2')]" + } + } }, { - "description": "Rubrik ThreatHunt Events - Threat Hunt Events for all severity types.", - "query": "Rubrik_ThreatHunt_Data_CL\n | sort by TimeGenerated desc" + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[[variables('KeyvaultConnectionName')]", + "location": "[[variables('workspace-location-inline')]", + "kind": "V1", + "properties": { + "displayName": "[[variables('KeyvaultConnectionName')]", + "parameterValueType": "Alternative", + "alternativeParameterValues": { + "vaultName": "[[parameters('KeyVaultName')]" + }, + "api": { + "id": "[[variables('_connection-3')]" + } + } }, { - "description": "Rubrik Other Events - Other Events for all severity types.", - "query": "Rubrik_Events_Data_CL\n | sort by TimeGenerated desc" - } - ], - "availability": { - "status": 1, - "isPreview": false - }, - "permissions": { - "resourceProvider": [ - { - "provider": "Microsoft.OperationalInsights/workspaces", - "permissionsDisplayText": "read and write permissions on the workspace are required.", - "providerDisplayName": "Workspace", - "scope": "Workspace", - "requiredPermissions": { - "write": true, - "read": true, - "delete": true - } - }, - { - "provider": "Microsoft.OperationalInsights/workspaces/sharedKeys", - "permissionsDisplayText": "read permissions to shared keys for the workspace are required. [See the documentation to learn more about workspace keys](https://docs.microsoft.com/azure/azure-monitor/platform/agent-windows#obtain-workspace-id-and-key).", - "providerDisplayName": "Keys", - "scope": "Workspace", - "requiredPermissions": { - "action": true + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[[variables('TeamsConnectionName')]", + "location": "[[variables('workspace-location-inline')]", + "kind": "V1", + "properties": { + "displayName": "[[variables('TeamsConnectionName')]", + "api": { + "id": "[[variables('_connection-4')]" } } - ], - "customs": [ - { - "name": "Microsoft.Web/sites permissions", - "description": "Read and write permissions to Azure Functions to create a Function App is required. [See the documentation to learn more about Azure Functions](https://docs.microsoft.com/azure/azure-functions/)." - } - ] - }, - "instructionSteps": [ - { - "description": ">**NOTE:** This connector uses Azure Functions to connect to the Rubrik webhook which push its logs into Microsoft Sentinel. This might result in additional data ingestion costs. Check the [Azure Functions pricing page](https://azure.microsoft.com/pricing/details/functions/) for details." }, { - "description": ">**(Optional Step)** Securely store workspace and API authorization key(s) or token(s) in Azure Key Vault. Azure Key Vault provides a secure mechanism to store and retrieve key values. [Follow these instructions](https://docs.microsoft.com/azure/app-service/app-service-key-vault-references) to use Azure Key Vault with an Azure Function App." + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[[variables('AzureblobConnectionName')]", + "location": "[[variables('workspace-location-inline')]", + "kind": "V1", + "properties": { + "displayName": "[[variables('AzureblobConnectionName')]", + "api": { + "id": "[[variables('_connection-5')]" + } + } }, { - "description": "**STEP 1 - Choose ONE from the following two deployment options to deploy the connector and the associated Azure Function**\n\n>**IMPORTANT:** Before deploying the Rubrik Microsoft Sentinel data connector, have the Workspace ID and Workspace Primary Key (can be copied from the following) readily available..", - "instructions": [ - { - "parameters": { - "fillWith": [ - "WorkspaceId" - ], - "label": "Workspace ID" - }, - "type": "CopyableLabel" - }, - { + "type": "Microsoft.Logic/workflows", + "apiVersion": "2017-07-01", + "name": "[[parameters('PlaybookName')]", + "location": "[[variables('workspace-location-inline')]", + "tags": { + "LogicAppsCategory": "security", + "hidden-SentinelTemplateName": "RubrikViolationRemediation", + "hidden-SentinelTemplateVersion": "1.6.0", + "hidden-SentinelWorkspaceId": "[[variables('workspaceResourceId')]" + }, + "identity": { + "type": "SystemAssigned" + }, + "dependsOn": [ + "[[resourceId('Microsoft.Web/connections', variables('MicrosoftSentinelConnectionName'))]", + "[[resourceId('Microsoft.Web/connections', variables('RubrikcustomconnectorConnectionName'))]", + "[[resourceId('Microsoft.Web/connections', variables('KeyvaultConnectionName'))]", + "[[resourceId('Microsoft.Web/connections', variables('TeamsConnectionName'))]", + "[[resourceId('Microsoft.Web/connections', variables('AzureblobConnectionName'))]" + ], + "properties": { + "state": "Enabled", + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "contentVersion": "1.0.0.0", "parameters": { - "fillWith": [ - "PrimaryKey" - ], - "label": "Primary Key" + "$connections": { + "type": "Object" + }, + "StorageContainerName": { + "defaultValue": "[variables('blanks')]", + "type": "String" + }, + "SasTokenTtlHours": { + "defaultValue": 72, + "type": "Int" + } }, - "type": "CopyableLabel" + "triggers": { + "Microsoft_Sentinel_incident": { + "type": "ApiConnectionWebhook", + "inputs": { + "body": { + "callback_url": "@{listCallbackUrl()}" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "path": "/incident-creation" + } + } + }, + "actions": { + "Initialize_Base_URL": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Base_URL", + "type": "string", + "value": "[[concat('https://', parameters('API Hostname'))]" + } + ] + } + }, + "Initialize_User_Agent": { + "runAfter": { + "Initialize_Base_URL": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "User_Agent", + "type": "string", + "value": "MSFTSentinel-3.5.2" + } + ] + } + }, + "Initialize_Access_Token": { + "runAfter": { + "Initialize_User_Agent": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Access_Token", + "type": "string" + } + ] + } + }, + "Initialize_Event_Type": { + "runAfter": { + "Initialize_Access_Token": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Event_Type", + "type": "string" + } + ] + } + }, + "Initialize_Object_ID": { + "runAfter": { + "Initialize_Event_Type": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Object_ID", + "type": "string" + } + ] + } + }, + "Initialize_Object_Name": { + "runAfter": { + "Initialize_Object_ID": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Object_Name", + "type": "string" + } + ] + } + }, + "Initialize_Object_Type": { + "runAfter": { + "Initialize_Object_Name": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Object_Type", + "type": "string" + } + ] + } + }, + "Initialize_Violation_ID": { + "runAfter": { + "Initialize_Object_Type": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Violation_ID", + "type": "string" + } + ] + } + }, + "Initialize_Snapshot_Fid": { + "runAfter": { + "Initialize_Violation_ID": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Snapshot_Fid", + "type": "string" + } + ] + } + }, + "Initialize_Snappable_Fid": { + "runAfter": { + "Initialize_Snapshot_Fid": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Snappable_Fid", + "type": "string" + } + ] + } + }, + "Initialize_Event_Name": { + "runAfter": { + "Initialize_Snappable_Fid": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Event_Name", + "type": "string" + } + ] + } + }, + "Initialize_Platform": { + "runAfter": { + "Initialize_Event_Name": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Platform", + "type": "string" + } + ] + } + }, + "Initialize_Physical_Host": { + "runAfter": { + "Initialize_Platform": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Physical_Host", + "type": "string" + } + ] + } + }, + "Initialize_Policy_Name": { + "runAfter": { + "Initialize_Physical_Host": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Policy_Name", + "type": "string" + } + ] + } + }, + "Initialize_Owner_UPN": { + "runAfter": { + "Initialize_Policy_Name": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Owner_UPN", + "type": "string" + } + ] + } + }, + "Initialize_Files_To_Process": { + "runAfter": { + "Initialize_Owner_UPN": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Files_To_Process", + "type": "array", + "value": "[variables('TemplateEmptyArray')]" + } + ] + } + }, + "Initialize_Quarantine_Results": { + "runAfter": { + "Initialize_Files_To_Process": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Quarantine_Results", + "type": "array", + "value": "[variables('TemplateEmptyArray')]" + } + ] + } + }, + "Initialize_All_Quarantine_Succeeded": { + "runAfter": { + "Initialize_Quarantine_Results": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "All_Quarantine_Succeeded", + "type": "boolean", + "value": true + } + ] + } + }, + "Initialize_Files_CSV_Link": { + "runAfter": { + "Initialize_All_Quarantine_Succeeded": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Files_CSV_Link", + "type": "string" + } + ] + } + }, + "Initialize_Remediation_Log_Link": { + "runAfter": { + "Initialize_Files_CSV_Link": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Remediation_Log_Link", + "type": "string" + } + ] + } + }, + "Initialize_Next_Cursor": { + "runAfter": { + "Initialize_Remediation_Log_Link": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Next_Cursor", + "type": "string" + } + ] + } + }, + "Initialize_Has_Next_Page": { + "runAfter": { + "Initialize_Next_Cursor": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Has_Next_Page", + "type": "boolean", + "value": true + } + ] + } + }, + "Initialize_Download_External_Id": { + "runAfter": { + "Initialize_Has_Next_Page": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Download_External_Id", + "type": "string" + } + ] + } + }, + "Initialize_Download_Ready": { + "runAfter": { + "Initialize_Download_External_Id": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Download_Ready", + "type": "boolean", + "value": false + } + ] + } + }, + "Initialize_Error_Messages": { + "runAfter": { + "Initialize_Download_Ready": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Error_Messages", + "type": "array", + "value": "[variables('TemplateEmptyArray')]" + } + ] + } + }, + "Initialize_CSV_Export_Start": { + "runAfter": { + "Initialize_Error_Messages": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "CSV_Export_Start", + "type": "string" + } + ] + } + }, + "Initialize_Log_Export_Start": { + "runAfter": { + "Initialize_CSV_Export_Start": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Log_Export_Start", + "type": "string" + } + ] + } + }, + "Initialize_CSV_Downloads": { + "runAfter": { + "Initialize_Log_Export_Start": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "CSV_Downloads", + "type": "array", + "value": "[variables('TemplateEmptyArray')]" + } + ] + } + }, + "Initialize_File_Hits_Failed": { + "runAfter": { + "Initialize_CSV_Downloads": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "File_Hits_Failed", + "type": "boolean", + "value": false + } + ] + } + }, + "Initialize_CSV_Name_Pattern": { + "runAfter": { + "Initialize_File_Hits_Failed": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "CSV_Name_Pattern", + "type": "string" + } + ] + } + }, + "Initialize_Log_Name_Pattern": { + "runAfter": { + "Initialize_CSV_Name_Pattern": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Log_Name_Pattern", + "type": "string" + } + ] + } + }, + "Initialize_CSV_Match_Count": { + "runAfter": { + "Initialize_Log_Name_Pattern": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "CSV_Match_Count", + "type": "integer", + "value": 0 + } + ] + } + }, + "Initialize_Log_Match_Count": { + "runAfter": { + "Initialize_CSV_Match_Count": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Log_Match_Count", + "type": "integer", + "value": 0 + } + ] + } + }, + "Initialize_Provided_Object_Name": { + "runAfter": { + "Initialize_Log_Match_Count": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Provided_Object_Name", + "type": "string" + } + ] + } + }, + "Initialize_Provided_Snappable_Fid": { + "runAfter": { + "Initialize_Provided_Object_Name": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Provided_Snappable_Fid", + "type": "string" + } + ] + } + }, + "Initialize_Provided_Snapshot_Fid": { + "runAfter": { + "Initialize_Provided_Snappable_Fid": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Provided_Snapshot_Fid", + "type": "string" + } + ] + } + }, + "Get_Client_ID": { + "runAfter": { + "Initialize_Snappable_Owner_Name": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['keyvault']['connectionId']" + } + }, + "method": "get", + "path": "/secrets/@{encodeURIComponent('Rubrik-AS-Int-ClientId')}/value" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Get_Client_Secret": { + "runAfter": { + "Get_Client_ID": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['keyvault']['connectionId']" + } + }, + "method": "get", + "path": "/secrets/@{encodeURIComponent('Rubrik-AS-Int-ClientSecret')}/value" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Authentication": { + "runAfter": { + "Get_Client_Secret": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['RubrikCustomConnector']['connectionId']" + } + }, + "method": "post", + "body": { + "client_id": "@body('Get_Client_ID')?['value']", + "client_secret": "@body('Get_Client_Secret')?['value']" + }, + "headers": { + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "path": "/api/client_token" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Is_Authentication_Failed": { + "runAfter": { + "Authentication": [ + "Succeeded", + "Failed" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "not": { + "equals": [ + "@outputs('Authentication')['statusCode']", + 200 + ] + } + }, + { + "equals": [ + "@empty(body('Authentication')?['access_token'])", + true + ] + } + ] + }, + "actions": { + "Append_auth_failure": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Authentication (/api/client_token) failed with status @{outputs('Authentication')?['statusCode']}. Verify the Key Vault secrets and the Rubrik custom connector." + } + }, + "Comment_auth_failure": { + "runAfter": { + "Append_auth_failure": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Violation Remediation failed.

@{join(variables('Error_Messages'), '
')}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + }, + "Terminate_-_Authentication_failed": { + "runAfter": { + "Comment_auth_failure": [ + "Succeeded", + "Failed" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "AuthFailed", + "message": "@{join(variables('Error_Messages'), ' | ')}" + } + } + } + }, + "else": { + "actions": { + "Set_Access_Token": { + "type": "SetVariable", + "inputs": { + "name": "Access_Token", + "value": "@{body('Authentication')?['access_token']}" + } + } + } + } + }, + "Select_alert_times": { + "type": "Select", + "runAfter": { + "Is_Authentication_Failed": [ + "Succeeded" + ] + }, + "inputs": { + "from": "@coalesce(triggerBody()?['object']?['properties']?['Alerts'], json('[]'))", + "select": "@ticks(coalesce(item()?['properties']?['timeGenerated'], '1970-01-01T00:00:00Z'))" + } + }, + "Filter_newest_alert": { + "type": "Query", + "runAfter": { + "Select_alert_times": [ + "Succeeded" + ] + }, + "inputs": { + "from": "@coalesce(triggerBody()?['object']?['properties']?['Alerts'], json('[]'))", + "where": "@equals(ticks(coalesce(item()?['properties']?['timeGenerated'], '1970-01-01T00:00:00Z')), max(body('Select_alert_times')))" + } + }, + "For_each_alert": { + "runAfter": { + "Filter_newest_alert": [ + "Succeeded" + ] + }, + "type": "Foreach", + "foreach": "@take(body('Filter_newest_alert'), 1)", + "runtimeConfiguration": { + "concurrency": { + "repetitions": 1 + } + }, + "actions": { + "Parse_JSON_for_alert": { + "type": "ParseJson", + "inputs": { + "content": "@items('For_each_alert')?['properties']?['additionalData']?['Custom Details']", + "schema": { + "type": "object", + "properties": { + "EventType": { + "type": "array", + "items": { + "type": "string" + } + }, + "ObjectID": { + "type": "array", + "items": { + "type": "string" + } + }, + "ObjectName": { + "type": "array", + "items": { + "type": "string" + } + }, + "ObjectType": { + "type": "array", + "items": { + "type": "string" + } + }, + "ViolationID": { + "type": "array", + "items": { + "type": "string" + } + }, + "EventName": { + "type": "array", + "items": { + "type": "string" + } + }, + "SnapshotID": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "Set_Event_Type": { + "runAfter": { + "Parse_JSON_for_alert": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Event_Type", + "value": "@{first(coalesce(body('Parse_JSON_for_alert')?['EventType'], json('[]')))}" + } + }, + "Set_Object_ID": { + "runAfter": { + "Set_Event_Type": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Object_ID", + "value": "@{first(coalesce(body('Parse_JSON_for_alert')?['ObjectID'], json('[]')))}" + } + }, + "Set_Object_Name": { + "runAfter": { + "Set_Object_ID": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Object_Name", + "value": "@{first(coalesce(body('Parse_JSON_for_alert')?['ObjectName'], json('[]')))}" + } + }, + "Set_Object_Type": { + "runAfter": { + "Set_Object_Name": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Object_Type", + "value": "@{first(coalesce(body('Parse_JSON_for_alert')?['ObjectType'], json('[]')))}" + } + }, + "Set_Violation_ID": { + "runAfter": { + "Set_Object_Type": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Violation_ID", + "value": "@{first(coalesce(body('Parse_JSON_for_alert')?['ViolationID'], json('[]')))}" + } + }, + "Set_Event_Name": { + "runAfter": { + "Set_Violation_ID": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Event_Name", + "value": "@{first(coalesce(body('Parse_JSON_for_alert')?['EventName'], json('[]')))}" + } + }, + "Set_Snapshot_Fid_from_incident": { + "runAfter": { + "Set_Event_Name": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Snapshot_Fid", + "value": "@{first(coalesce(body('Parse_JSON_for_alert')?['SnapshotID'], json('[]')))}" + } + }, + "Set_Snappable_Fid_default": { + "runAfter": { + "Set_Snapshot_Fid_from_incident": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Snappable_Fid", + "value": "@variables('Object_ID')" + } + } + } + }, + "Switch_on_event_type": { + "runAfter": { + "For_each_alert": [ + "Succeeded" + ] + }, + "type": "Switch", + "expression": "@variables('Event_Type')", + "cases": { + "SecurityViolation": { + "case": "SecurityViolation", + "actions": { + "Get_DSPM_Violation": { + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "query DataSecurityViolationGetQuery($violationId: String!) {\n policyViolation(\n violationId: $violationId\n policyTypes: [POLICY_TYPE_DATAGOV]\n ) {\n ...DataAtRiskPanelFragment\n status\n violationSeverity\n policyViolationId\n createdAt\n lastUpdatedAt\n resourceId\n policy {\n policyId\n name\n description\n policyCategory\n policySeverity\n containsAccessFilters\n __typename\n }\n remediations {\n remediationId\n state\n remediationDetails {\n details {\n ticketNumber\n ticketUrl\n __typename\n }\n __typename\n }\n __typename\n }\n resourceMetadata {\n metadata {\n ... on CommonAssetMetadata {\n platform\n cloudAccountInfo {\n accountName\n __typename\n }\n objectType\n clusterInfo {\n clusterName\n clusterUuid\n __typename\n }\n creationTime\n lastAccessTime\n snapshotTimestamp\n physicalHost\n name\n isDeleted\n region\n __typename\n }\n }\n __typename\n }\n __typename\n }\n}\n\nfragment SensitiveHitsChartFragment on DataGovViolationDetails {\n snapshotId\n violatedSensitiveHits\n violatedNoRiskSensitiveHits\n violatedLowRiskSensitiveHits\n violatedMediumRiskSensitiveHits\n violatedHighRiskSensitiveHits\n __typename\n}\n\nfragment DataAtRiskPanelFragment on PolicyViolation {\n details {\n ...SensitiveHitsChartFragment\n ... on DataGovViolationDetails {\n dataCategories {\n id\n name\n totalViolatedHits\n __typename\n }\n dataTypes {\n id\n name\n totalViolatedHits\n __typename\n }\n mipLabels {\n id\n totalViolatedHits\n name\n __typename\n }\n documentTypes {\n id\n name\n totalViolatedHits\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}", + "variables": { + "violationId": "@variables('Violation_ID')" + } + } + } + }, + "Check_Get_DSPM_Violation": { + "runAfter": { + "Get_DSPM_Violation": [ + "Succeeded", + "Failed" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "not": { + "equals": [ + "@outputs('Get_DSPM_Violation')['statusCode']", + 200 + ] + } + }, + { + "not": { + "equals": [ + "@coalesce(body('Get_DSPM_Violation')?['errors'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Append_-_Get_violation_failed": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Get DSPM Violation failed for violation @{variables('Violation_ID')} (status @{outputs('Get_DSPM_Violation')?['statusCode']})." + } + }, + "Comment_-_Get_violation_failed": { + "runAfter": { + "Append_-_Get_violation_failed": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Violation Remediation failed.

@{join(variables('Error_Messages'), '
')}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + }, + "Terminate_-_Get_violation_failed": { + "runAfter": { + "Comment_-_Get_violation_failed": [ + "Succeeded", + "Failed" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "GetViolationFailed", + "message": "@{join(variables('Error_Messages'), ' | ')}" + } + } + } + }, + "else": { + "actions": { + "Capture_Provided_Object_Name": { + "type": "SetVariable", + "inputs": { + "name": "Provided_Object_Name", + "value": "@{coalesce(variables('Object_Name'), '')}" + } + }, + "Capture_Provided_Snappable_Fid": { + "runAfter": { + "Capture_Provided_Object_Name": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Provided_Snappable_Fid", + "value": "@{coalesce(variables('Snappable_Fid'), '')}" + } + }, + "Capture_Provided_Snapshot_Fid": { + "runAfter": { + "Capture_Provided_Snappable_Fid": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Provided_Snapshot_Fid", + "value": "@{coalesce(variables('Snapshot_Fid'), '')}" + } + }, + "Validate_violation_identifiers": { + "runAfter": { + "Capture_Provided_Snapshot_Fid": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "and": [ + { + "not": { + "equals": [ + "@empty(trim(variables('Provided_Object_Name')))", + true + ] + } + }, + { + "not": { + "equals": [ + "@toLower(trim(variables('Provided_Object_Name')))", + "@toLower(trim(coalesce(body('Get_DSPM_Violation')?['data']?['policyViolation']?['resourceMetadata']?['metadata']?['name'], '')))" + ] + } + } + ] + }, + { + "and": [ + { + "not": { + "equals": [ + "@empty(trim(variables('Provided_Snappable_Fid')))", + true + ] + } + }, + { + "not": { + "equals": [ + "@trim(variables('Provided_Snappable_Fid'))", + "@trim(coalesce(body('Get_DSPM_Violation')?['data']?['policyViolation']?['resourceId'], ''))" + ] + } + } + ] + }, + { + "and": [ + { + "not": { + "equals": [ + "@empty(trim(variables('Provided_Snapshot_Fid')))", + true + ] + } + }, + { + "not": { + "equals": [ + "@trim(variables('Provided_Snapshot_Fid'))", + "@trim(coalesce(body('Get_DSPM_Violation')?['data']?['policyViolation']?['details']?['snapshotId'], ''))" + ] + } + } + ] + } + ] + }, + "actions": { + "Append_-_Validation_failed": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Provided information is invalid for violation @{variables('Violation_ID')}. One or more of the supplied identifiers does not match the violation. Object Name (provided='@{variables('Provided_Object_Name')}', actual='@{coalesce(body('Get_DSPM_Violation')?['data']?['policyViolation']?['resourceMetadata']?['metadata']?['name'], '')}'); Resource ID (provided='@{variables('Provided_Snappable_Fid')}', actual='@{coalesce(body('Get_DSPM_Violation')?['data']?['policyViolation']?['resourceId'], '')}'); Snapshot ID (provided='@{variables('Provided_Snapshot_Fid')}', actual='@{coalesce(body('Get_DSPM_Violation')?['data']?['policyViolation']?['details']?['snapshotId'], '')}')." + } + }, + "Comment_-_Validation_failed": { + "runAfter": { + "Append_-_Validation_failed": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Violation Remediation failed (validation).

@{join(variables('Error_Messages'), '
')}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + }, + "Terminate_-_Validation_failed": { + "runAfter": { + "Comment_-_Validation_failed": [ + "Succeeded", + "Failed" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "ValidationFailed", + "message": "@{join(variables('Error_Messages'), ' | ')}" + } + } + } + } + }, + "Set_Object_Name_from_violation": { + "runAfter": { + "Validate_violation_identifiers": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Object_Name", + "value": "@{if(empty(trim(variables('Provided_Object_Name'))), coalesce(body('Get_DSPM_Violation')?['data']?['policyViolation']?['resourceMetadata']?['metadata']?['name'], ''), variables('Provided_Object_Name'))}" + } + }, + "Set_Snapshot_Fid_from_violation": { + "runAfter": { + "Set_Object_Name_from_violation": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Snapshot_Fid", + "value": "@{if(empty(trim(variables('Provided_Snapshot_Fid'))), coalesce(body('Get_DSPM_Violation')?['data']?['policyViolation']?['details']?['snapshotId'], ''), variables('Provided_Snapshot_Fid'))}" + } + }, + "Set_Snappable_Fid_from_violation": { + "runAfter": { + "Set_Snapshot_Fid_from_violation": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Snappable_Fid", + "value": "@{if(empty(trim(variables('Provided_Snappable_Fid'))), coalesce(body('Get_DSPM_Violation')?['data']?['policyViolation']?['resourceId'], ''), variables('Provided_Snappable_Fid'))}" + } + }, + "Set_Platform_from_violation": { + "runAfter": { + "Set_Snappable_Fid_from_violation": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Platform", + "value": "@{body('Get_DSPM_Violation')?['data']?['policyViolation']?['resourceMetadata']?['metadata']?['platform']}" + } + }, + "Set_Physical_Host_from_violation": { + "runAfter": { + "Set_Platform_from_violation": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Physical_Host", + "value": "@{body('Get_DSPM_Violation')?['data']?['policyViolation']?['resourceMetadata']?['metadata']?['physicalHost']}" + } + }, + "Set_Policy_Name_from_violation": { + "runAfter": { + "Check_Get_Snappable_Identity": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Policy_Name", + "value": "@{body('Get_DSPM_Violation')?['data']?['policyViolation']?['policy']?['name']}" + } + }, + "Set_CSV_Name_Pattern": { + "runAfter": { + "Set_Policy_Name_from_violation": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "CSV_Name_Pattern", + "value": "@{toLower(concat(replace(variables('Object_Name'), ' ', '-'), '-violating-files_file_results'))}" + } + }, + "Set_Log_Name_Pattern": { + "runAfter": { + "Set_CSV_Name_Pattern": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Log_Name_Pattern", + "value": "@{toLower(concat(variables('Object_Name'), ' actions log'))}" + } + }, + "Get_Snappable_Identity": { + "runAfter": { + "Set_Physical_Host_from_violation": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "query SnappableIdentity($fid: UUID!) {\n hierarchyObject(fid: $fid) {\n id\n name\n objectType\n ... on O365Site {\n title\n url\n __typename\n }\n ... on O365Onedrive {\n userPrincipalName\n userName\n userID\n __typename\n }\n __typename\n }\n}", + "variables": { + "fid": "@variables('Snappable_Fid')" + } + } + } + }, + "Check_Get_Snappable_Identity": { + "runAfter": { + "Get_Snappable_Identity": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@outputs('Get_Snappable_Identity')?['statusCode']", + 200 + ] + }, + { + "equals": [ + "@coalesce(body('Get_Snappable_Identity')?['errors'], '')", + "" + ] + } + ] + }, + "actions": { + "Set_Site_Url": { + "type": "SetVariable", + "inputs": { + "name": "Site_Url", + "value": "@{coalesce(body('Get_Snappable_Identity')?['data']?['hierarchyObject']?['url'], '')}" + } + }, + "Set_Drive_Owner_Upn": { + "runAfter": { + "Set_Site_Url": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Drive_Owner_Upn", + "value": "@{coalesce(body('Get_Snappable_Identity')?['data']?['hierarchyObject']?['userPrincipalName'], '')}" + } + }, + "Set_Snappable_Owner_Name": { + "runAfter": { + "Set_Drive_Owner_Upn": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Snappable_Owner_Name", + "value": "@{coalesce(body('Get_Snappable_Identity')?['data']?['hierarchyObject']?['userName'], body('Get_Snappable_Identity')?['data']?['hierarchyObject']?['title'], body('Get_Snappable_Identity')?['data']?['hierarchyObject']?['name'], '')}" + } + } + }, + "else": { + "actions": { + "Append_-_Snappable_Identity_failed": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Could not resolve the snappable identity for @{variables('Snappable_Fid')} (status @{coalesce(outputs('Get_Snappable_Identity')?['statusCode'], 'none')}); O365 files cannot be resolved by drive and path without it." + } + } + } + } + } + } + } + }, + "Set_Status_In_Progress": { + "runAfter": { + "Check_Get_DSPM_Violation": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "mutation UpdatePolicyViolationsMutation($input: BulkUpdatePolicyViolationsInput!) {\n bulkUpdatePolicyViolations(input: $input)\n}", + "variables": { + "input": { + "newPolicyViolationStatus": "POLICY_VIOLATION_STATUS_IN_PROGRESS", + "policyViolationIds": [ + "@variables('Violation_ID')" + ] + } + } + } + } + }, + "Check_Set_Status_In_Progress": { + "runAfter": { + "Set_Status_In_Progress": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "not": { + "equals": [ + "@outputs('Set_Status_In_Progress')['statusCode']", + 200 + ] + } + }, + { + "not": { + "equals": [ + "@coalesce(body('Set_Status_In_Progress')?['errors'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Append_-_Set_Status_In_Progress_failed": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Update violation status to IN_PROGRESS failed for violation @{variables('Violation_ID')} (status @{outputs('Set_Status_In_Progress')?['statusCode']})." + } + }, + "Comment_-_Set_Status_In_Progress_failed": { + "runAfter": { + "Append_-_Set_Status_In_Progress_failed": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Violation Remediation failed.

@{join(variables('Error_Messages'), '
')}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + }, + "Terminate_-_Set_Status_In_Progress_failed": { + "runAfter": { + "Comment_-_Set_Status_In_Progress_failed": [ + "Succeeded", + "Failed" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "SetStatusInProgressFailed", + "message": "@{join(variables('Error_Messages'), ' | ')}" + } + } + } + } + }, + "Stamp_CSV_Export_Start": { + "runAfter": { + "Check_Set_Status_In_Progress": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "CSV_Export_Start", + "value": "@{utcNow()}" + } + }, + "Trigger_Files_CSV_export": { + "runAfter": { + "Stamp_CSV_Export_Start": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "mutation DownloadFullSnapshotResultsCsvMutation($filters: DownloadResultsCsvFiltersInput, $snappableFid: String!, $snapshotFid: String!) {\n downloadSnapshotResultsCsv(\n snappableFid: $snappableFid\n snapshotFid: $snapshotFid\n downloadFilter: $filters\n ) {\n isSuccessful\n __typename\n }\n}", + "variables": { + "snappableFid": "@variables('Snappable_Fid')", + "snapshotFid": "@variables('Snapshot_Fid')", + "filters": { + "policyViolationId": "@variables('Violation_ID')", + "fileType": "HITS" + } + } + } + } + }, + "Check_Trigger_Files_CSV_export": { + "runAfter": { + "Trigger_Files_CSV_export": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "not": { + "equals": [ + "@outputs('Trigger_Files_CSV_export')['statusCode']", + 200 + ] + } + }, + { + "not": { + "equals": [ + "@coalesce(body('Trigger_Files_CSV_export')?['errors'], '')", + "" + ] + } + }, + { + "not": { + "equals": [ + "@coalesce(body('Trigger_Files_CSV_export')?['data']?['downloadSnapshotResultsCsv']?['isSuccessful'], false)", + true + ] + } + } + ] + }, + "actions": { + "Append_-_Trigger_Files_CSV_failed": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Files CSV export request (downloadSnapshotResultsCsv) failed or returned isSuccessful=false for violation @{variables('Violation_ID')} (status @{outputs('Trigger_Files_CSV_export')?['statusCode']})." + } + }, + "Set_Files_CSV_Link_trigger_failed": { + "runAfter": { + "Append_-_Trigger_Files_CSV_failed": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Files_CSV_Link", + "value": "Files CSV export request (downloadSnapshotResultsCsv) failed or returned isSuccessful=false; no CSV link available." + } + } + } + }, + "Reset_Download_Ready_for_CSV": { + "runAfter": { + "Check_Trigger_Files_CSV_export": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Download_Ready", + "value": false + } + }, + "Reset_Download_External_Id_for_CSV": { + "runAfter": { + "Reset_Download_Ready_for_CSV": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Download_External_Id", + "value": "@null" + } + }, + "Poll_Download_Bar_for_CSV": { + "runAfter": { + "Reset_Download_External_Id_for_CSV": [ + "Succeeded" + ] + }, + "type": "Until", + "expression": "@equals(variables('Download_Ready'), true)", + "limit": { + "count": "[[parameters('DownloadPollMaxAttempts')]", + "timeout": "PT1H" + }, + "actions": { + "Wait_before_CSV_poll": { + "type": "Wait", + "inputs": { + "interval": { + "count": "[[parameters('DownloadPollIntervalSec')]", + "unit": "Second" + } + } + }, + "Download_Bar_CSV": { + "runAfter": { + "Wait_before_CSV_poll": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "query DownloadBarQuery {\n allUserFiles {\n downloads {\n externalId\n createdAt\n expiresAt\n completedAt\n creator\n filename\n type\n state\n __typename\n }\n __typename\n }\n}" + } + } + }, + "Filter_snapshot_csv_downloads": { + "runAfter": { + "Download_Bar_CSV": [ + "Succeeded" + ] + }, + "type": "Query", + "inputs": { + "from": "@coalesce(first(coalesce(body('Download_Bar_CSV')?['data']?['allUserFiles'], json('[]')))?['downloads'], json('[]'))", + "where": "@and(equals(item()?['type'], 'SNAPSHOT_RESULTS_CSV'), and(contains(toLower(coalesce(item()?['filename'], '')), variables('CSV_Name_Pattern')), greater(ticks(coalesce(item()?['createdAt'], '1970-01-01T00:00:00Z')), ticks(addSeconds(variables('CSV_Export_Start'), -1)))))" + } + }, + "Set_CSV_Match_Count": { + "runAfter": { + "Filter_snapshot_csv_downloads": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "CSV_Match_Count", + "value": "@length(coalesce(body('Filter_snapshot_csv_downloads'), json('[]')))" + } + }, + "Sort_snapshot_csv_desc": { + "runAfter": { + "Set_CSV_Match_Count": [ + "Succeeded" + ] + }, + "type": "Compose", + "inputs": "@reverse(sort(body('Filter_snapshot_csv_downloads'), 'createdAt'))" + }, + "If_csv_match_ambiguous": { + "runAfter": { + "Sort_snapshot_csv_desc": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "greater": [ + "@variables('CSV_Match_Count')", + 1 + ] + } + ] + }, + "actions": { + "Append_CSV_ambiguous": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Files CSV export could not be matched unambiguously for violation @{variables('Violation_ID')}: @{variables('CSV_Match_Count')} files matched the object-name pattern after the export start time. Not attached." + } + }, + "Set_CSV_Ready_on_ambiguous": { + "runAfter": { + "Append_CSV_ambiguous": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Download_Ready", + "value": true + } + } + }, + "else": { + "actions": { + "If_newest_csv_ready_or_failed": { + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('CSV_Match_Count')", + 1 + ] + }, + { + "equals": [ + "@first(outputs('Sort_snapshot_csv_desc'))?['state']", + "READY" + ] + } + ] + }, + "actions": { + "Set_CSV_External_Id": { + "type": "SetVariable", + "inputs": { + "name": "Download_External_Id", + "value": "@{first(outputs('Sort_snapshot_csv_desc'))?['externalId']}" + } + }, + "Set_CSV_Ready_true": { + "runAfter": { + "Set_CSV_External_Id": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Download_Ready", + "value": true + } + } + }, + "else": { + "actions": { + "If_newest_csv_failed": { + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('CSV_Match_Count')", + 1 + ] + }, + { + "equals": [ + "@first(outputs('Sort_snapshot_csv_desc'))?['state']", + "FAILED" + ] + } + ] + }, + "actions": { + "Append_CSV_export_failed": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Rubrik reported the files CSV export (SNAPSHOT_RESULTS_CSV) as FAILED for violation @{variables('Violation_ID')}; CSV link will be unavailable." + } + }, + "Set_CSV_Ready_on_failure": { + "runAfter": { + "Append_CSV_export_failed": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Download_Ready", + "value": true + } + } + } + } + } + } + } + } + } + } + } + }, + "If_CSV_external_id_present": { + "runAfter": { + "Poll_Download_Bar_for_CSV": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@empty(variables('Download_External_Id'))", + false + ] + } + ] + }, + "actions": { + "Download_Files_CSV_bytes": { + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{variables('Base_URL')}/file-downloads/@{encodeURIComponent(variables('Download_External_Id'))}", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "User-Agent": "@variables('User_Agent')" + } + } + }, + "Upload_Files_CSV_blob": { + "runAfter": { + "Download_Files_CSV_bytes": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['azureblob']['connectionId']" + } + }, + "method": "post", + "body": "@body('Download_Files_CSV_bytes')", + "headers": { + "Content-Type": "text/csv" + }, + "path": "[[concat('/v2/datasets/@{encodeURIComponent(encodeURIComponent(''', parameters('StorageAccountName'), '''))}/files')]", + "queries": { + "folderPath": "@{concat('/', parameters('StorageContainerName'), '/dspm/', triggerBody()?['object']?['name'], '/', variables('Violation_ID'))}", + "name": "@{concat('violation_files_', variables('Violation_ID'), '.csv')}", + "queryParametersSingleEncoded": true + } + } + }, + "Create_SAS_for_Files_CSV": { + "runAfter": { + "Upload_Files_CSV_blob": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['azureblob']['connectionId']" + } + }, + "method": "post", + "body": { + "Permissions": "Read", + "ExpiryTime": "@{addHours(utcNow(), parameters('SasTokenTtlHours'))}" + }, + "path": "[[concat('/v2/datasets/@{encodeURIComponent(encodeURIComponent(''', parameters('StorageAccountName'), '''))}/CreateSharedLinkByPath')]", + "queries": { + "path": "@{concat('/', parameters('StorageContainerName'), '/dspm/', triggerBody()?['object']?['name'], '/', variables('Violation_ID'), '/violation_files_', variables('Violation_ID'), '.csv')}" + } + } + }, + "Set_Files_CSV_Link": { + "runAfter": { + "Create_SAS_for_Files_CSV": [ + "Succeeded", + "Failed", + "Skipped" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Files_CSV_Link", + "value": "@{coalesce(body('Create_SAS_for_Files_CSV')?['WebUrl'], concat('CSV uploaded to blob folder dspm/', triggerBody()?['object']?['name'], '/', variables('Violation_ID'), ' (SAS unavailable).'))}" + } + } + }, + "else": { + "actions": { + "If_CSV_trigger_succeeded_for_message": { + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@outputs('Trigger_Files_CSV_export')['statusCode']", + 200 + ] + }, + { + "equals": [ + "@coalesce(body('Trigger_Files_CSV_export')?['data']?['downloadSnapshotResultsCsv']?['isSuccessful'], false)", + true + ] + } + ] + }, + "actions": { + "Set_Files_CSV_Link_unavailable": { + "type": "SetVariable", + "inputs": { + "name": "Files_CSV_Link", + "value": "@{if(equals(variables('CSV_Match_Count'), 0), 'Files CSV download reference could not be located (no file matched the object-name pattern after the export start time, or the export failed/timed out); no CSV link available.', 'Files CSV export was not READY (failed, timed out, or ambiguous match); no CSV link available.')}" + } + } + } + } + } + } + }, + "Stamp_Log_Export_Start": { + "runAfter": { + "If_CSV_external_id_present": [ + "Succeeded", + "Failed", + "Skipped" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Log_Export_Start", + "value": "@{utcNow()}" + } + }, + "Trigger_Remediation_Log_export": { + "runAfter": { + "Stamp_Log_Export_Start": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "mutation CreateViolationRemediationMutation($input: CreateViolationRemediationInput!) {\n createViolationRemediation(input: $input) {\n remediationId\n __typename\n }\n}", + "variables": { + "input": { + "targets": { + "targetIds": [ + "@variables('Violation_ID')" + ], + "targetType": "REMEDIATION_TARGET_TYPE_VIOLATION" + }, + "remediationType": "REMEDIATION_TYPE_EXPORT_ACTIONS_LOG_TO_CSV", + "resourceId": "@variables('Snappable_Fid')" + } + } + } + } + }, + "Check_Trigger_Remediation_Log_export": { + "runAfter": { + "Trigger_Remediation_Log_export": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "not": { + "equals": [ + "@outputs('Trigger_Remediation_Log_export')['statusCode']", + 200 + ] + } + }, + { + "not": { + "equals": [ + "@coalesce(body('Trigger_Remediation_Log_export')?['errors'], '')", + "" + ] + } + }, + { + "equals": [ + "@coalesce(body('Trigger_Remediation_Log_export')?['data']?['createViolationRemediation']?['remediationId'], '')", + "" + ] + } + ] + }, + "actions": { + "Append_-_Trigger_Log_failed": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Remediation actions-log export request (createViolationRemediation) failed or returned no remediationId for violation @{variables('Violation_ID')} (status @{outputs('Trigger_Remediation_Log_export')?['statusCode']}); remediation log link will be unavailable." + } + } + } + }, + "If_Log_trigger_succeeded": { + "runAfter": { + "Check_Trigger_Remediation_Log_export": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@outputs('Trigger_Remediation_Log_export')['statusCode']", + 200 + ] + }, + { + "equals": [ + "@coalesce(body('Trigger_Remediation_Log_export')?['errors'], '')", + "" + ] + }, + { + "not": { + "equals": [ + "@coalesce(body('Trigger_Remediation_Log_export')?['data']?['createViolationRemediation']?['remediationId'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Reset_Download_Ready_for_Log": { + "type": "SetVariable", + "inputs": { + "name": "Download_Ready", + "value": false + } + }, + "Reset_Download_External_Id_for_Log": { + "runAfter": { + "Reset_Download_Ready_for_Log": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Download_External_Id", + "value": "@null" + } + }, + "Poll_Download_Bar_for_Log": { + "runAfter": { + "Reset_Download_External_Id_for_Log": [ + "Succeeded" + ] + }, + "type": "Until", + "expression": "@equals(variables('Download_Ready'), true)", + "limit": { + "count": "[[parameters('DownloadPollMaxAttempts')]", + "timeout": "PT1H" + }, + "actions": { + "Wait_before_Log_poll": { + "type": "Wait", + "inputs": { + "interval": { + "count": "[[parameters('DownloadPollIntervalSec')]", + "unit": "Second" + } + } + }, + "Download_Bar_Log": { + "runAfter": { + "Wait_before_Log_poll": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "query DownloadBarQuery {\n allUserFiles {\n downloads {\n externalId\n createdAt\n expiresAt\n completedAt\n creator\n filename\n type\n state\n __typename\n }\n __typename\n }\n}" + } + } + }, + "Filter_snapshot_log_downloads": { + "runAfter": { + "Download_Bar_Log": [ + "Succeeded" + ] + }, + "type": "Query", + "inputs": { + "from": "@coalesce(first(coalesce(body('Download_Bar_Log')?['data']?['allUserFiles'], json('[]')))?['downloads'], json('[]'))", + "where": "@and(equals(item()?['type'], 'REMEDIATION_ACTIONS_LOG_CSV'), and(contains(toLower(coalesce(item()?['filename'], '')), variables('Log_Name_Pattern')), greater(ticks(coalesce(item()?['createdAt'], '1970-01-01T00:00:00Z')), ticks(addSeconds(variables('Log_Export_Start'), -1)))))" + } + }, + "Set_Log_Match_Count": { + "runAfter": { + "Filter_snapshot_log_downloads": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Log_Match_Count", + "value": "@length(coalesce(body('Filter_snapshot_log_downloads'), json('[]')))" + } + }, + "Sort_snapshot_log_desc": { + "runAfter": { + "Set_Log_Match_Count": [ + "Succeeded" + ] + }, + "type": "Compose", + "inputs": "@reverse(sort(body('Filter_snapshot_log_downloads'), 'createdAt'))" + }, + "If_log_match_ambiguous": { + "runAfter": { + "Sort_snapshot_log_desc": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "greater": [ + "@variables('Log_Match_Count')", + 1 + ] + } + ] + }, + "actions": { + "Append_Log_ambiguous": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Remediation actions-log export could not be matched unambiguously for violation @{variables('Violation_ID')}: @{variables('Log_Match_Count')} files matched the object-name pattern after the export start time. Not attached." + } + }, + "Set_Log_Ready_on_ambiguous": { + "runAfter": { + "Append_Log_ambiguous": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Download_Ready", + "value": true + } + } + }, + "else": { + "actions": { + "If_newest_log_ready_or_failed": { + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Log_Match_Count')", + 1 + ] + }, + { + "equals": [ + "@first(outputs('Sort_snapshot_log_desc'))?['state']", + "READY" + ] + } + ] + }, + "actions": { + "Set_Log_External_Id": { + "type": "SetVariable", + "inputs": { + "name": "Download_External_Id", + "value": "@{first(outputs('Sort_snapshot_log_desc'))?['externalId']}" + } + }, + "Set_Log_Ready_true": { + "runAfter": { + "Set_Log_External_Id": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Download_Ready", + "value": true + } + } + }, + "else": { + "actions": { + "If_newest_log_failed": { + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Log_Match_Count')", + 1 + ] + }, + { + "equals": [ + "@first(outputs('Sort_snapshot_log_desc'))?['state']", + "FAILED" + ] + } + ] + }, + "actions": { + "Append_Log_export_failed": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Rubrik reported the remediation actions-log export (REMEDIATION_ACTIONS_LOG_CSV) as FAILED for violation @{variables('Violation_ID')}; remediation log link will be unavailable." + } + }, + "Set_Log_Ready_on_failure": { + "runAfter": { + "Append_Log_export_failed": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Download_Ready", + "value": true + } + } + } + } + } + } + } + } + } + } + } + }, + "If_Log_external_id_present": { + "runAfter": { + "Poll_Download_Bar_for_Log": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@empty(variables('Download_External_Id'))", + false + ] + } + ] + }, + "actions": { + "Download_Remediation_Log_bytes": { + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{variables('Base_URL')}/file-downloads/@{encodeURIComponent(variables('Download_External_Id'))}", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "User-Agent": "@variables('User_Agent')" + } + } + }, + "Upload_Remediation_Log_blob": { + "runAfter": { + "Download_Remediation_Log_bytes": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['azureblob']['connectionId']" + } + }, + "method": "post", + "body": "@body('Download_Remediation_Log_bytes')", + "headers": { + "Content-Type": "text/csv" + }, + "path": "[[concat('/v2/datasets/@{encodeURIComponent(encodeURIComponent(''', parameters('StorageAccountName'), '''))}/files')]", + "queries": { + "folderPath": "@{concat('/', parameters('StorageContainerName'), '/dspm/', triggerBody()?['object']?['name'], '/', variables('Violation_ID'))}", + "name": "@{concat('remediation_log_', variables('Violation_ID'), '.csv')}", + "queryParametersSingleEncoded": true + } + } + }, + "Create_SAS_for_Remediation_Log": { + "runAfter": { + "Upload_Remediation_Log_blob": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['azureblob']['connectionId']" + } + }, + "method": "post", + "body": { + "Permissions": "Read", + "ExpiryTime": "@{addHours(utcNow(), parameters('SasTokenTtlHours'))}" + }, + "path": "[[concat('/v2/datasets/@{encodeURIComponent(encodeURIComponent(''', parameters('StorageAccountName'), '''))}/CreateSharedLinkByPath')]", + "queries": { + "path": "@{concat('/', parameters('StorageContainerName'), '/dspm/', triggerBody()?['object']?['name'], '/', variables('Violation_ID'), '/remediation_log_', variables('Violation_ID'), '.csv')}" + } + } + }, + "Set_Remediation_Log_Link": { + "runAfter": { + "Create_SAS_for_Remediation_Log": [ + "Succeeded", + "Failed", + "Skipped" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Remediation_Log_Link", + "value": "@{coalesce(body('Create_SAS_for_Remediation_Log')?['WebUrl'], concat('Remediation log uploaded to blob folder dspm/', triggerBody()?['object']?['name'], '/', variables('Violation_ID'), ' (SAS unavailable).'))}" + } + } + }, + "else": { + "actions": { + "Set_Remediation_Log_Link_unavailable": { + "type": "SetVariable", + "inputs": { + "name": "Remediation_Log_Link", + "value": "@{if(equals(variables('Log_Match_Count'), 0), 'Remediation actions-log download reference could not be located (no file matched the object-name pattern after the export start time, or the export failed/timed out); no remediation log link available.', 'Remediation actions-log export was not READY (failed, timed out, or ambiguous match); no remediation log link available.')}" + } + } + } + } + } + }, + "else": { + "actions": { + "Set_Remediation_Log_Link_trigger_failed": { + "type": "SetVariable", + "inputs": { + "name": "Remediation_Log_Link", + "value": "Remediation actions-log export request failed; no remediation log link available." + } + } + } + } + }, + "List_file_hits_full": { + "runAfter": { + "If_Log_trigger_succeeded": [ + "Succeeded", + "Failed", + "Skipped" + ] + }, + "type": "Until", + "expression": "@equals(variables('Has_Next_Page'), false)", + "limit": { + "count": 100, + "timeout": "PT1H" + }, + "actions": { + "List_DSPM_Violation_File_Hits_full": { + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "query DSPMViolationFileListQuery(\n $first: Int!\n $after: String\n $snappableFid: String!\n $snapshotFid: String!\n $filters: ListFileResultFiltersInput\n $sort: FileResultSortInput\n $timezone: String!\n) {\n policyObj(snappableFid: $snappableFid, snapshotFid: $snapshotFid) {\n id: snapshotFid\n fileResultConnection(\n first: $first\n after: $after\n filter: $filters\n sort: $sort\n timezone: $timezone\n ) {\n edges {\n cursor\n node {\n nativePath\n stdPath\n filename\n mode\n size\n directory\n createdBy\n modifiedBy\n hits {\n totalHits\n violations\n __typename\n }\n __typename\n }\n __typename\n }\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n __typename\n }\n hasLatestData\n __typename\n }\n __typename\n }\n}", + "variables": { + "first": 25, + "after": "@if(empty(variables('Next_Cursor')), null, variables('Next_Cursor'))", + "snappableFid": "@variables('Snappable_Fid')", + "snapshotFid": "@variables('Snapshot_Fid')", + "sort": { + "sortBy": "HITS", + "sortOrder": "DESC" + }, + "filters": { + "violationId": "@variables('Violation_ID')", + "fileType": "HITS", + "whitelistEnabled": true + }, + "timezone": "UTC" + } + } + } + }, + "Check_List_file_hits_full": { + "runAfter": { + "List_DSPM_Violation_File_Hits_full": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "not": { + "equals": [ + "@outputs('List_DSPM_Violation_File_Hits_full')['statusCode']", + 200 + ] + } + }, + { + "not": { + "equals": [ + "@coalesce(body('List_DSPM_Violation_File_Hits_full')?['errors'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Append_-_List_file_hits_full_failed": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Listing DSPM violation file hits failed for violation @{variables('Violation_ID')} (status @{outputs('List_DSPM_Violation_File_Hits_full')?['statusCode']})." + } + }, + "Set_File_Hits_Failed_full": { + "runAfter": { + "Append_-_List_file_hits_full_failed": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "File_Hits_Failed", + "value": true + } + }, + "Set_Has_Next_Page_false_on_full_failure": { + "runAfter": { + "Set_File_Hits_Failed_full": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Has_Next_Page", + "value": false + } + } + } + }, + "Append_files_full": { + "runAfter": { + "Check_List_file_hits_full": [ + "Succeeded" + ] + }, + "type": "Foreach", + "foreach": "@coalesce(body('List_DSPM_Violation_File_Hits_full')?['data']?['policyObj']?['fileResultConnection']?['edges'], json('[]'))", + "runtimeConfiguration": { + "concurrency": { + "repetitions": 1 + } + }, + "actions": { + "Append_file_record_full": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Files_To_Process", + "value": { + "filename": "@items('Append_files_full')?['node']?['filename']", + "nativePath": "@items('Append_files_full')?['node']?['nativePath']", + "stdPath": "@items('Append_files_full')?['node']?['stdPath']", + "directory": "@items('Append_files_full')?['node']?['directory']", + "createdBy": "@items('Append_files_full')?['node']?['createdBy']", + "modifiedBy": "@items('Append_files_full')?['node']?['modifiedBy']", + "platform": "@variables('Platform')", + "physicalHost": "@variables('Physical_Host')", + "siteUrl": "@variables('Site_Url')", + "driveOwnerUpn": "@variables('Drive_Owner_Upn')", + "snappableOwnerName": "@variables('Snappable_Owner_Name')" + } + } + } + } + }, + "Set_Has_Next_Page_full": { + "runAfter": { + "Append_files_full": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Has_Next_Page", + "value": "@coalesce(body('List_DSPM_Violation_File_Hits_full')?['data']?['policyObj']?['fileResultConnection']?['pageInfo']?['hasNextPage'], false)" + } + }, + "Set_Next_Cursor_full": { + "runAfter": { + "Set_Has_Next_Page_full": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Next_Cursor", + "value": "@{body('List_DSPM_Violation_File_Hits_full')?['data']?['policyObj']?['fileResultConnection']?['pageInfo']?['endCursor']}" + } + } + } + }, + "Check_File_Hits_Failed_full": { + "runAfter": { + "List_file_hits_full": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('File_Hits_Failed')", + true + ] + } + ] + }, + "actions": { + "Comment_-_File_hits_failed_full": { + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Violation Remediation failed.

@{join(variables('Error_Messages'), '
')}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + }, + "Terminate_-_File_hits_failed_full": { + "runAfter": { + "Comment_-_File_hits_failed_full": [ + "Succeeded", + "Failed" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "ListFileHitsFailed", + "message": "@{join(variables('Error_Messages'), ' | ')}" + } + } + } + } + }, + "If_files_present_full": { + "runAfter": { + "Check_File_Hits_Failed_full": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "greater": [ + "@length(coalesce(variables('Files_To_Process'), json('[]')))", + 0 + ] + } + ] + }, + "actions": { + "Select_files_for_quarantine_full": { + "type": "Select", + "inputs": { + "from": "@variables('Files_To_Process')", + "select": { + "filename": "@item()?['filename']", + "filePath": "@item()?['nativePath']", + "nativePath": "@item()?['nativePath']", + "directory": "@item()?['directory']", + "ownerUpn": "@coalesce(item()?['createdBy'], item()?['modifiedBy'], '')", + "objectType": "@coalesce(item()?['platform'], variables('Object_Type'), '')", + "physicalHost": "@item()?['physicalHost']", + "siteUrl": "@item()?['siteUrl']", + "driveOwnerUpn": "@item()?['driveOwnerUpn']" + } + } + }, + "Call_RubrikQuarantineFiles_full": { + "runAfter": { + "Select_files_for_quarantine_full": [ + "Succeeded" + ] + }, + "type": "Workflow", + "inputs": { + "host": { + "triggerName": "manual", + "workflow": { + "id": "[[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Logic/workflows/', parameters('RubrikQuarantineFilesPlaybookName'))]" + } + }, + "body": { + "objectType": "@variables('Object_Type')", + "files": "@body('Select_files_for_quarantine_full')" + } + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Set_Quarantine_Results_full": { + "runAfter": { + "Call_RubrikQuarantineFiles_full": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Quarantine_Results", + "value": "@coalesce(body('Call_RubrikQuarantineFiles_full')?['results'], json('[]'))" + } + }, + "Set_All_Quarantine_Succeeded_full": { + "runAfter": { + "Set_Quarantine_Results_full": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "All_Quarantine_Succeeded", + "value": "@equals(toLower(coalesce(body('Call_RubrikQuarantineFiles_full')?['status'], 'failed')), 'success')" + } + }, + "Notify_owner_full": { + "runAfter": { + "Set_All_Quarantine_Succeeded_full": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['teams']['connectionId']" + } + }, + "method": "post", + "path": "/beta/teams/conversation/message/poster/Flow bot/location/@{encodeURIComponent('channel')}", + "body": { + "recipient": { + "groupId": "[[parameters('TeamsGroupId')]", + "channelId": "[[parameters('TeamsChannelId')]" + }, + "messageBody": "

Rubrik DSPM Violation Remediation

Hi @{coalesce(variables('Owner_UPN'), 'file owner')},
The Rubrik DSPM policy @{variables('Policy_Name')} (severity: @{triggerBody()?['object']?['properties']?['severity']}) was raised on object @{variables('Object_Name')}.

Violation ID: @{variables('Violation_ID')}
Files processed: @{length(variables('Quarantine_Results'))}
All quarantine succeeded: @{variables('All_Quarantine_Succeeded')}

@{if(greater(length(variables('Quarantine_Results')), 0), 'The listed files have been processed for quarantine.', 'No files were processed for quarantine.')} See the Sentinel incident for the CSV/log export links and per-file results.

" + } + } + }, + "If_all_quarantine_succeeded_full": { + "runAfter": { + "Notify_owner_full": [ + "Succeeded", + "Failed" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('All_Quarantine_Succeeded')", + true + ] + }, + { + "greater": [ + "@length(variables('Quarantine_Results'))", + 0 + ] + } + ] + }, + "actions": { + "Set_Status_Remediated": { + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "mutation UpdatePolicyViolationsMutation($input: BulkUpdatePolicyViolationsInput!) {\n bulkUpdatePolicyViolations(input: $input)\n}", + "variables": { + "input": { + "newPolicyViolationStatus": "POLICY_VIOLATION_STATUS_REMEDIATED", + "policyViolationIds": [ + "@variables('Violation_ID')" + ] + } + } + } + } + } + } + }, + "Comment_full_summary": { + "runAfter": { + "If_all_quarantine_succeeded_full": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Violation Remediation

Event type: SecurityViolation
Policy: @{variables('Policy_Name')}
Object: @{variables('Object_Name')} (@{variables('Object_Type')})
Violation ID: @{variables('Violation_ID')}

Exports:
Files CSV: @{if(empty(coalesce(variables('Files_CSV_Link'), '')), 'Not generated.', variables('Files_CSV_Link'))}
Remediation log: @{if(empty(coalesce(variables('Remediation_Log_Link'), '')), 'Not generated.', variables('Remediation_Log_Link'))}
(SAS links expire in @{parameters('SasTokenTtlHours')} hours)

Files processed: @{length(variables('Quarantine_Results'))}
All quarantine succeeded: @{variables('All_Quarantine_Succeeded')}
Final violation status: @{if(and(equals(variables('All_Quarantine_Succeeded'), true), greater(length(variables('Quarantine_Results')), 0)), 'REMEDIATED', 'IN_PROGRESS')}

Quarantine results:
@{string(variables('Quarantine_Results'))}

Quarantine playbook status: @{coalesce(body('Call_RubrikQuarantineFiles_full')?['status'], 'no response')}
Quarantine playbook message: @{coalesce(body('Call_RubrikQuarantineFiles_full')?['error'], '')}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + } + }, + "else": { + "actions": { + "Set_Status_Remediated_no_files": { + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "mutation UpdatePolicyViolationsMutation($input: BulkUpdatePolicyViolationsInput!) {\n bulkUpdatePolicyViolations(input: $input)\n}", + "variables": { + "input": { + "newPolicyViolationStatus": "POLICY_VIOLATION_STATUS_REMEDIATED", + "policyViolationIds": [ + "@variables('Violation_ID')" + ] + } + } + } + } + }, + "Notify_owner_no_files": { + "runAfter": { + "Set_Status_Remediated_no_files": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['teams']['connectionId']" + } + }, + "method": "post", + "path": "/beta/teams/conversation/message/poster/Flow bot/location/@{encodeURIComponent('channel')}", + "body": { + "recipient": { + "groupId": "[[parameters('TeamsGroupId')]", + "channelId": "[[parameters('TeamsChannelId')]" + }, + "messageBody": "

Rubrik DSPM Violation Remediation

Hi @{coalesce(variables('Owner_UPN'), 'file owner')},
The Rubrik DSPM policy @{variables('Policy_Name')} was raised on object @{variables('Object_Name')}, but the violation contains no files.

Violation ID: @{variables('Violation_ID')}
Files processed: 0

No quarantine action was required. The violation has been marked remediated.

" + } + } + }, + "Comment_full_summary_no_files": { + "runAfter": { + "Notify_owner_no_files": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Violation Remediation

Event type: SecurityViolation
Policy: @{variables('Policy_Name')}
Object: @{variables('Object_Name')} (@{variables('Object_Type')})
Violation ID: @{variables('Violation_ID')}

No files were returned for this violation, so there was nothing to quarantine. The quarantine playbook was not called.

Final violation status: @{if(and(greaterOrEquals(coalesce(outputs('Set_Status_Remediated_no_files')?['statusCode'], 0), 200), less(coalesce(outputs('Set_Status_Remediated_no_files')?['statusCode'], 0), 300)), 'REMEDIATED', 'Update failed - still IN_PROGRESS')}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + } + } + } + } + } + }, + "Classification": { + "case": "Classification", + "actions": { + "Get_Snappable_Identity_reduced": { + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "query SnappableIdentity($fid: UUID!) {\n hierarchyObject(fid: $fid) {\n id\n name\n objectType\n ... on O365Site {\n title\n url\n __typename\n }\n ... on O365Onedrive {\n userPrincipalName\n userName\n userID\n __typename\n }\n __typename\n }\n}", + "variables": { + "fid": "@variables('Snappable_Fid')" + } + } + } + }, + "Check_Get_Snappable_Identity_reduced": { + "runAfter": { + "Get_Snappable_Identity_reduced": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@outputs('Get_Snappable_Identity_reduced')?['statusCode']", + 200 + ] + }, + { + "equals": [ + "@coalesce(body('Get_Snappable_Identity_reduced')?['errors'], '')", + "" + ] + } + ] + }, + "actions": { + "Set_Site_Url_reduced": { + "type": "SetVariable", + "inputs": { + "name": "Site_Url", + "value": "@{coalesce(body('Get_Snappable_Identity_reduced')?['data']?['hierarchyObject']?['url'], '')}" + } + }, + "Set_Drive_Owner_Upn_reduced": { + "runAfter": { + "Set_Site_Url_reduced": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Drive_Owner_Upn", + "value": "@{coalesce(body('Get_Snappable_Identity_reduced')?['data']?['hierarchyObject']?['userPrincipalName'], '')}" + } + }, + "Set_Snappable_Owner_Name_reduced": { + "runAfter": { + "Set_Drive_Owner_Upn_reduced": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Snappable_Owner_Name", + "value": "@{coalesce(body('Get_Snappable_Identity_reduced')?['data']?['hierarchyObject']?['userName'], body('Get_Snappable_Identity_reduced')?['data']?['hierarchyObject']?['title'], body('Get_Snappable_Identity_reduced')?['data']?['hierarchyObject']?['name'], '')}" + } + } + }, + "else": { + "actions": { + "Append_-_Snappable_Identity_reduced_failed": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Could not resolve the snappable identity for @{variables('Snappable_Fid')} (status @{coalesce(outputs('Get_Snappable_Identity_reduced')?['statusCode'], 'none')}); O365 files cannot be resolved by drive and path without it." + } + } + } + } + }, + "List_file_hits_reduced": { + "runAfter": { + "Check_Get_Snappable_Identity_reduced": [ + "Succeeded" + ] + }, + "type": "Until", + "expression": "@equals(variables('Has_Next_Page'), false)", + "limit": { + "count": 100, + "timeout": "PT1H" + }, + "actions": { + "List_DSPM_Violation_File_Hits_reduced": { + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "query DSPMViolationFileListQuery(\n $first: Int!\n $after: String\n $snappableFid: String!\n $snapshotFid: String!\n $filters: ListFileResultFiltersInput\n $sort: FileResultSortInput\n $timezone: String!\n) {\n policyObj(snappableFid: $snappableFid, snapshotFid: $snapshotFid) {\n id: snapshotFid\n fileResultConnection(\n first: $first\n after: $after\n filter: $filters\n sort: $sort\n timezone: $timezone\n ) {\n edges {\n cursor\n node {\n nativePath\n stdPath\n filename\n mode\n size\n directory\n createdBy\n modifiedBy\n hits {\n totalHits\n violations\n __typename\n }\n __typename\n }\n __typename\n }\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n __typename\n }\n hasLatestData\n __typename\n }\n __typename\n }\n}", + "variables": { + "first": 25, + "after": "@if(empty(variables('Next_Cursor')), null, variables('Next_Cursor'))", + "snappableFid": "@variables('Object_ID')", + "snapshotFid": "@variables('Snapshot_Fid')", + "sort": { + "sortBy": "HITS", + "sortOrder": "DESC" + }, + "filters": { + "fileType": "HITS", + "whitelistEnabled": true + }, + "timezone": "UTC" + } + } + } + }, + "Check_List_file_hits_reduced": { + "runAfter": { + "List_DSPM_Violation_File_Hits_reduced": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "not": { + "equals": [ + "@outputs('List_DSPM_Violation_File_Hits_reduced')['statusCode']", + 200 + ] + } + }, + { + "not": { + "equals": [ + "@coalesce(body('List_DSPM_Violation_File_Hits_reduced')?['errors'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Append_-_List_file_hits_reduced_failed": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Listing DSPM classification file hits failed for object @{variables('Object_ID')} (status @{outputs('List_DSPM_Violation_File_Hits_reduced')?['statusCode']})." + } + }, + "Set_File_Hits_Failed_reduced": { + "runAfter": { + "Append_-_List_file_hits_reduced_failed": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "File_Hits_Failed", + "value": true + } + }, + "Set_Has_Next_Page_false_on_reduced_failure": { + "runAfter": { + "Set_File_Hits_Failed_reduced": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Has_Next_Page", + "value": false + } + } + } + }, + "Append_files_reduced": { + "runAfter": { + "Check_List_file_hits_reduced": [ + "Succeeded" + ] + }, + "type": "Foreach", + "foreach": "@coalesce(body('List_DSPM_Violation_File_Hits_reduced')?['data']?['policyObj']?['fileResultConnection']?['edges'], json('[]'))", + "runtimeConfiguration": { + "concurrency": { + "repetitions": 1 + } + }, + "actions": { + "Append_file_record_reduced": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Files_To_Process", + "value": { + "filename": "@items('Append_files_reduced')?['node']?['filename']", + "nativePath": "@items('Append_files_reduced')?['node']?['nativePath']", + "directory": "@items('Append_files_reduced')?['node']?['directory']", + "stdPath": "@items('Append_files_reduced')?['node']?['stdPath']", + "createdBy": "@items('Append_files_reduced')?['node']?['createdBy']", + "modifiedBy": "@items('Append_files_reduced')?['node']?['modifiedBy']", + "platform": "@variables('Object_Type')", + "physicalHost": "@variables('Physical_Host')", + "siteUrl": "@variables('Site_Url')", + "driveOwnerUpn": "@variables('Drive_Owner_Upn')", + "snappableOwnerName": "@variables('Snappable_Owner_Name')" + } + } + } + } + }, + "Set_Has_Next_Page_reduced": { + "runAfter": { + "Append_files_reduced": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Has_Next_Page", + "value": "@coalesce(body('List_DSPM_Violation_File_Hits_reduced')?['data']?['policyObj']?['fileResultConnection']?['pageInfo']?['hasNextPage'], false)" + } + }, + "Set_Next_Cursor_reduced": { + "runAfter": { + "Set_Has_Next_Page_reduced": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Next_Cursor", + "value": "@{body('List_DSPM_Violation_File_Hits_reduced')?['data']?['policyObj']?['fileResultConnection']?['pageInfo']?['endCursor']}" + } + } + } + }, + "Check_File_Hits_Failed_reduced": { + "runAfter": { + "List_file_hits_reduced": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('File_Hits_Failed')", + true + ] + } + ] + }, + "actions": { + "Comment_-_File_hits_failed_reduced": { + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Classification Remediation failed.

@{join(variables('Error_Messages'), '
')}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + }, + "Terminate_-_File_hits_failed_reduced": { + "runAfter": { + "Comment_-_File_hits_failed_reduced": [ + "Succeeded", + "Failed" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "ListFileHitsFailed", + "message": "@{join(variables('Error_Messages'), ' | ')}" + } + } + } + } + }, + "If_files_present_reduced": { + "runAfter": { + "Check_File_Hits_Failed_reduced": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "greater": [ + "@length(coalesce(variables('Files_To_Process'), json('[]')))", + 0 + ] + } + ] + }, + "actions": { + "Select_files_for_quarantine_reduced": { + "type": "Select", + "inputs": { + "from": "@variables('Files_To_Process')", + "select": { + "filename": "@item()?['filename']", + "filePath": "@item()?['nativePath']", + "nativePath": "@item()?['nativePath']", + "directory": "@item()?['directory']", + "ownerUpn": "@coalesce(item()?['createdBy'], item()?['modifiedBy'], '')", + "objectType": "@coalesce(item()?['platform'], variables('Object_Type'), '')", + "physicalHost": "@item()?['physicalHost']", + "siteUrl": "@item()?['siteUrl']", + "driveOwnerUpn": "@item()?['driveOwnerUpn']" + } + } + }, + "Call_RubrikQuarantineFiles_reduced": { + "runAfter": { + "Select_files_for_quarantine_reduced": [ + "Succeeded" + ] + }, + "type": "Workflow", + "inputs": { + "host": { + "triggerName": "manual", + "workflow": { + "id": "[[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Logic/workflows/', parameters('RubrikQuarantineFilesPlaybookName'))]" + } + }, + "body": { + "objectType": "@variables('Object_Type')", + "files": "@body('Select_files_for_quarantine_reduced')" + } + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Set_Quarantine_Results_reduced": { + "runAfter": { + "Call_RubrikQuarantineFiles_reduced": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Quarantine_Results", + "value": "@coalesce(body('Call_RubrikQuarantineFiles_reduced')?['results'], json('[]'))" + } + }, + "Notify_owner_reduced": { + "runAfter": { + "Set_Quarantine_Results_reduced": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['teams']['connectionId']" + } + }, + "method": "post", + "path": "/beta/teams/conversation/message/poster/Flow bot/location/@{encodeURIComponent('channel')}", + "body": { + "recipient": { + "groupId": "[[parameters('TeamsGroupId')]", + "channelId": "[[parameters('TeamsChannelId')]" + }, + "messageBody": "

Rubrik DSPM Classification Remediation

Hi @{coalesce(variables('Owner_UPN'), 'file owner')},
Classification policy hits were found on object @{variables('Object_Name')} (@{variables('Object_Type')}), severity @{triggerBody()?['object']?['properties']?['severity']}.

Object ID: @{variables('Object_ID')}
Files processed: @{length(variables('Quarantine_Results'))}

@{if(greater(length(variables('Quarantine_Results')), 0), 'The listed files have been processed for quarantine.', 'No files were processed for quarantine.')} See the Sentinel incident for per-file results.

" + } + } + }, + "Comment_reduced_summary": { + "runAfter": { + "Notify_owner_reduced": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Classification Remediation

Event type: Classification
Object: @{variables('Object_Name')} (@{variables('Object_Type')})
Object ID: @{variables('Object_ID')}
Snapshot ID: @{variables('Snapshot_Fid')}

Files processed: @{length(variables('Quarantine_Results'))}

Quarantine results:
@{string(variables('Quarantine_Results'))}

No CSV/log export or violation status update is performed for classification events (no DSPM license / no violation object).

Quarantine playbook status: @{coalesce(body('Call_RubrikQuarantineFiles_reduced')?['status'], 'no response')}
Quarantine playbook message: @{coalesce(body('Call_RubrikQuarantineFiles_reduced')?['error'], '')}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + } + }, + "else": { + "actions": { + "Skip_quarantine_no_files_reduced": { + "type": "Compose", + "inputs": "No files returned for this classification event; the quarantine playbook was not called." + }, + "Comment_reduced_summary_no_files": { + "runAfter": { + "Skip_quarantine_no_files_reduced": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Classification Remediation

Event type: Classification
Object: @{variables('Object_Name')} (@{variables('Object_Type')})
Object ID: @{variables('Object_ID')}
Snapshot ID: @{variables('Snapshot_Fid')}

No files were returned for this classification event, so there was nothing to quarantine. The quarantine playbook was not called.

No violation status update is performed for classification events.

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + } + } + } + } + } + } + }, + "default": { + "actions": { + "Comment_unknown_event_type": { + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Violation Remediation: unrecognized eventType '@{variables('Event_Type')}'. Expected 'SecurityViolation' or 'Classification'. No action taken.

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + } + } + } + }, + "Initialize_Site_Url": { + "runAfter": { + "Initialize_Provided_Snapshot_Fid": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Site_Url", + "type": "string" + } + ] + } + }, + "Initialize_Drive_Owner_Upn": { + "runAfter": { + "Initialize_Site_Url": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Drive_Owner_Upn", + "type": "string" + } + ] + } + }, + "Initialize_Snappable_Owner_Name": { + "runAfter": { + "Initialize_Drive_Owner_Upn": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Snappable_Owner_Name", + "type": "string" + } + ] + } + } + } + }, + "parameters": { + "$connections": { + "value": { + "azuresentinel": { + "connectionId": "[[resourceId('Microsoft.Web/connections', variables('MicrosoftSentinelConnectionName'))]", + "connectionName": "[[variables('MicrosoftSentinelConnectionName')]", + "id": "[[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', variables('workspace-location-inline'), '/managedApis/azuresentinel')]", + "connectionProperties": { + "authentication": { + "type": "ManagedServiceIdentity" + } + } + }, + "RubrikCustomConnector": { + "connectionId": "[[resourceId('Microsoft.Web/connections', variables('RubrikcustomconnectorConnectionName'))]", + "connectionName": "[[variables('RubrikcustomconnectorConnectionName')]", + "id": "[[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', parameters('Rubrik Connector name'))]" + }, + "keyvault": { + "connectionId": "[[resourceId('Microsoft.Web/connections', variables('KeyvaultConnectionName'))]", + "connectionName": "[[variables('KeyvaultConnectionName')]", + "id": "[[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', variables('workspace-location-inline'), '/managedApis/keyvault')]", + "connectionProperties": { + "authentication": { + "type": "ManagedServiceIdentity" + } + } + }, + "teams": { + "connectionId": "[[resourceId('Microsoft.Web/connections', variables('TeamsConnectionName'))]", + "connectionName": "[[variables('TeamsConnectionName')]", + "id": "[[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', variables('workspace-location-inline'), '/managedApis/teams')]" + }, + "azureblob": { + "connectionId": "[[resourceId('Microsoft.Web/connections', variables('AzureblobConnectionName'))]", + "connectionName": "[[variables('AzureblobConnectionName')]", + "id": "[[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', variables('workspace-location-inline'), '/managedApis/azureblob')]" + } + } + }, + "StorageContainerName": { + "value": "[[parameters('StorageContainerName')]" + }, + "SasTokenTtlHours": { + "value": "[[parameters('SasTokenTtlHours')]" + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Playbook-', last(split(variables('playbookId19'),'/'))))]", + "properties": { + "parentId": "[variables('playbookId19')]", + "contentId": "[variables('_playbookContentId19')]", + "kind": "Playbook", + "version": "[variables('playbookVersion19')]", + "source": { + "kind": "Solution", + "name": "RubrikSecurityCloud", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "Ben Meadowcroft", + "email": "[variables('_email')]" + }, + "support": { + "name": "Rubrik", + "email": "support@rubrik.com", + "tier": "Partner", + "link": "https://support.rubrik.com" + }, + "dependencies": { + "criteria": [ + { + "kind": "LogicAppsCustomConnector", + "contentId": "[variables('_RubrikCustomConnector')]", + "version": "[variables('playbookVersion1')]" + } + ] + } + } + } + ], + "metadata": { + "title": "Rubrik DSPM Violation Remediation", + "description": "Incident-triggered playbook that remediates Rubrik Security Cloud (RSC) DSPM violations. Branches on eventType: for SecurityViolation it retrieves the violation, exports the violation-files CSV and the remediation-actions log (create -> poll Download Bar -> download bytes -> upload to a blob container -> generate SAS link), lists the violation file hits, quarantines each file via the RubrikQuarantineFiles sub-playbook, notifies the file owner in Teams, and marks the violation REMEDIATED only when every quarantine succeeded. For ClassificationResultsAvailable (no DSPM license) it runs a reduced flow: list file hits (objectId + snapshotId) -> quarantine -> notify. Both flows write a final comment to the Microsoft Sentinel incident.", + "prerequisites": [ + "1. The Rubrik Security Cloud solution should be configured to connect to Rubrik Security Cloud API end points using a Service Account with a role that includes the privileges to read and update policy violations.", + "2. The Rubrik custom connector (RubrikCustomConnector) needs to be deployed prior to the deployment of this playbook, in the same resource group and region. This playbook uses its 'Authentication' (/api/client_token) operation to obtain the RSC bearer token.", + "3. The RubrikQuarantineFiles sub-playbook (HTTP Request-triggered) must be deployed before this playbook; it is called once per incident with the full list of files to quarantine (an objectType plus a files array) and returns { status, results, error }, where results holds one entry per file.", + "4. Store Service account credentials in Key Vault and obtain the Key Vault name and tenantId.", + "a. Create a Key Vault with a unique name.", + "b. Go to KeyVault -> secrets, click Generate/import and create 'Rubrik-AS-Int-ClientId' & 'Rubrik-AS-Int-ClientSecret' for storing client_id and client_secret respectively.", + "5. An Azure Storage account and an EXISTING blob container are required for the exported CSV/log files (SecurityViolation flow only).", + "6. A Microsoft Teams team and channel are required for owner notifications.", + "7. Configure an analytics rule that maps the Rubrik event fields into incident Custom Details (eventType, objectId, objectName, objectType, seriesId, eventName, snapshotId)." + ], + "postDeployment": [ + "**a. Authorize connections**", + "Once deployment is complete, authorize each connection.", + "1. Go to your logic app -> API connections -> Select the connection resource (RubrikCustomConnector, Microsoft Teams, and Azure Blob Storage).", + "2. Go to General -> edit API connection.", + "3. Click Authorize.", + "4. Sign in.", + "5. Click Save.", + "6. Repeat steps for each connection. The Azure Blob Storage account used must have write access to StorageContainerName (used to upload the CSV/log and create the read-only SAS link).", + "**b. Assign Role to comment on and update incidents**", + "1. Go to Log Analytics Workspace -> -> Access Control -> Add.", + "2. Add role assignment.", + "3. Assignment type: Job function roles.", + "4. Role: Microsoft Sentinel Responder.", + "5. Members: select managed identity for assigned access to and add your logic app as member.", + "6. Click on review + assign.", + "**c. Add Access policy in Key Vault**", + "Add an access policy for the playbook's managed identity to read the RSC service-account secrets.", + "1. Go to logic app -> -> Identity -> System assigned managed identity and copy the Object (principal) ID.", + "2. Go to keyvaults -> -> Access policies -> Create.", + "3. Select Get on secrets permissions. Click next.", + "4. In the principal section, search by the copied object ID. Click next.", + "5. Click review + create.", + "**d. Configurations in Microsoft Sentinel**", + "1. Deploy the RubrikQuarantineFiles sub-playbook first and set RubrikQuarantineFilesPlaybookName to its Logic App name; it must exist in the same resource group and expose an HTTP Request trigger that accepts { objectType, files } and returns { status, results, error }.", + "2. In Microsoft Sentinel, configure the automation rules to trigger this playbook on the relevant DSPM incidents (playbooks do not auto-attach)." + ], + "lastUpdateTime": "2026-10-04T00:00:00Z", + "tags": [ + "Microsoft Sentinel", + "Incident", + "Rubrik", + "DSPM", + "Remediation", + "Quarantine" + ], + "releaseNotes": { + "version": "1.6.0", + "title": "[variables('blanks')]", + "notes": [ + "Initial version" + ] + } + } + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('_playbookContentId19')]", + "contentKind": "Playbook", + "displayName": "RubrikDSPMViolationRemediation", + "contentProductId": "[variables('_playbookcontentProductId19')]", + "id": "[variables('_playbookcontentProductId19')]", + "version": "[variables('playbookVersion19')]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('playbookTemplateSpecName20')]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "RubrikIRViolation Playbook with template version 3.5.5", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('playbookVersion20')]", + "parameters": { + "PlaybookName": { + "defaultValue": "RubrikIRViolation", + "type": "string", + "metadata": { + "description": "Name of the Logic App / playbook resource." + } + }, + "API Hostname": { + "type": "string", + "metadata": { + "description": "Hostname of the Rubrik Security Cloud instance (e.g. customer.my.rubrik.com), without scheme or trailing slash." + } + }, + "Rubrik Connector name": { + "defaultValue": "RubrikCustomConnector", + "type": "string", + "metadata": { + "description": "Name of the previously deployed Rubrik custom connector (Microsoft.Web/customApis)." + } + }, + "KeyVaultName": { + "type": "string", + "metadata": { + "description": "Name of the Azure Key Vault that holds the Rubrik service-account credentials." + } + }, + "TeamsGroupId": { + "type": "string", + "metadata": { + "description": "Microsoft Teams group (Team) ID for the SOC channel used as the fallback notification target." + } + }, + "TeamsChannelId": { + "type": "string", + "metadata": { + "description": "Microsoft Teams channel ID within the Team used as the fallback notification target." + } + } + }, + "variables": { + "MicrosoftSentinelConnectionName": "[[concat('azuresentinel-', parameters('PlaybookName'))]", + "RubrikcustomconnectorConnectionName": "[[concat('Rubrikcustomconnector-', parameters('PlaybookName'))]", + "KeyvaultConnectionName": "[[concat('keyvault-', parameters('PlaybookName'))]", + "TeamsConnectionName": "[[concat('teams-', parameters('PlaybookName'))]", + "connection-1": "[[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', variables('workspace-location-inline'), '/managedApis/azuresentinel')]", + "_connection-1": "[[variables('connection-1')]", + "connection-2": "[[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', parameters('Rubrik Connector name'))]", + "_connection-2": "[[variables('connection-2')]", + "connection-3": "[[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', variables('workspace-location-inline'), '/managedApis/keyvault')]", + "_connection-3": "[[variables('connection-3')]", + "connection-4": "[[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', variables('workspace-location-inline'), '/managedApis/teams')]", + "_connection-4": "[[variables('connection-4')]", + "workspace-location-inline": "[concat('[resourceGroup().locatio', 'n]')]", + "workspace-name": "[parameters('workspace')]", + "workspaceResourceId": "[[resourceId('microsoft.OperationalInsights/Workspaces', variables('workspace-name'))]" + }, + "resources": [ + { + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[[variables('MicrosoftSentinelConnectionName')]", + "location": "[[variables('workspace-location-inline')]", + "kind": "V1", + "properties": { + "displayName": "[[parameters('PlaybookName')]", + "parameterValueType": "Alternative", + "api": { + "id": "[[variables('_connection-1')]" + } + } + }, + { + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[[variables('RubrikcustomconnectorConnectionName')]", + "location": "[[variables('workspace-location-inline')]", + "properties": { + "displayName": "[[variables('RubrikcustomconnectorConnectionName')]", + "api": { + "id": "[[variables('_connection-2')]" + } + } + }, + { + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[[variables('KeyvaultConnectionName')]", + "location": "[[variables('workspace-location-inline')]", + "kind": "V1", + "properties": { + "displayName": "[[parameters('PlaybookName')]", + "parameterValueType": "Alternative", + "alternativeParameterValues": { + "vaultName": "[[parameters('KeyVaultName')]" + }, + "api": { + "id": "[[variables('_connection-3')]" + } + } + }, + { + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[[variables('TeamsConnectionName')]", + "location": "[[variables('workspace-location-inline')]", + "properties": { + "displayName": "[[parameters('PlaybookName')]", + "api": { + "id": "[[variables('_connection-4')]" + } + } + }, + { + "type": "Microsoft.Logic/workflows", + "apiVersion": "2017-07-01", + "name": "[[parameters('PlaybookName')]", + "location": "[[variables('workspace-location-inline')]", + "tags": { + "LogicAppsCategory": "security", + "hidden-SentinelTemplateName": "Rubrik-IR-Violation", + "hidden-SentinelTemplateVersion": "1.0", + "hidden-SentinelWorkspaceId": "[[variables('workspaceResourceId')]" + }, + "identity": { + "type": "SystemAssigned" + }, + "dependsOn": [ + "[[resourceId('Microsoft.Web/connections', variables('MicrosoftSentinelConnectionName'))]", + "[[resourceId('Microsoft.Web/connections', variables('RubrikcustomconnectorConnectionName'))]", + "[[resourceId('Microsoft.Web/connections', variables('KeyvaultConnectionName'))]", + "[[resourceId('Microsoft.Web/connections', variables('TeamsConnectionName'))]" + ], + "properties": { + "state": "Enabled", + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "$connections": { + "type": "Object" + } + }, + "triggers": { + "Microsoft_Sentinel_incident": { + "type": "ApiConnectionWebhook", + "inputs": { + "body": { + "callback_url": "@{listCallbackUrl()}" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "path": "/incident-creation" + } + } + }, + "actions": { + "Initialize_Base_URL": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Base_URL", + "type": "string", + "value": "[[concat('https://', parameters('API Hostname'))]" + } + ] + } + }, + "Initialize_User_Agent": { + "runAfter": { + "Initialize_Base_URL": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "User_Agent", + "type": "string", + "value": "MSFTSentinel-1.0.0" + } + ] + } + }, + "Initialize_Access_Token": { + "runAfter": { + "Initialize_User_Agent": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Access_Token", + "type": "string" + } + ] + } + }, + "Initialize_Error_Messages": { + "runAfter": { + "Initialize_Access_Token": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Error_Messages", + "type": "array", + "value": "[variables('TemplateEmptyArray')]" + } + ] + } + }, + "Initialize_ViolationId": { + "runAfter": { + "Initialize_Error_Messages": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "ViolationId", + "type": "string" + } + ] + } + }, + "Initialize_UserPrincipalName": { + "runAfter": { + "Initialize_ViolationId": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "UserPrincipalName", + "type": "string" + } + ] + } + }, + "Initialize_DisplayName": { + "runAfter": { + "Initialize_UserPrincipalName": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "DisplayName", + "type": "string" + } + ] + } + }, + "Initialize_IdpType": { + "runAfter": { + "Initialize_DisplayName": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "IdpType", + "type": "string" + } + ] + } + }, + "Initialize_ManualRemediationProcess": { + "runAfter": { + "Initialize_IdpType": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "ManualRemediationProcess", + "type": "string" + } + ] + } + }, + "Initialize_PolicyType": { + "runAfter": { + "Initialize_ManualRemediationProcess": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "PolicyType", + "type": "string" + } + ] + } + }, + "Parse_JSON_-_Alert_Custom_Details": { + "runAfter": { + "Initialize_PolicyType": [ + "Succeeded" + ] + }, + "type": "ParseJson", + "inputs": { + "content": "@first(triggerBody()?['object']?['properties']?['alerts'])?['properties']?['additionalData']?['Custom Details']", + "schema": { + "type": "object", + "properties": { + "ViolationId": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "Set_ViolationId": { + "runAfter": { + "Parse_JSON_-_Alert_Custom_Details": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "ViolationId", + "value": "@{first(coalesce(body('Parse_JSON_-_Alert_Custom_Details')?['ViolationId'], json('[]')))}" + } + }, + "Check_ViolationId_present": { + "runAfter": { + "Set_ViolationId": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('ViolationId')", + "" + ] + } + ] + }, + "actions": { + "Comment_-_No_ViolationId": { + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik IR Violation failed.

No ViolationId was found in the incident alert Custom Details. Verify that the analytics rule maps ViolationId into Custom Details.

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + }, + "Terminate_-_No_ViolationId": { + "runAfter": { + "Comment_-_No_ViolationId": [ + "Succeeded", + "Failed" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "MissingViolationId", + "message": "No ViolationId present in the incident Custom Details." + } + } + } + } + }, + "Get_Client_ID": { + "runAfter": { + "Check_ViolationId_present": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['keyvault']['connectionId']" + } + }, + "method": "get", + "path": "/secrets/@{encodeURIComponent('Rubrik-AS-Int-ClientId')}/value" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Get_Client_Secret": { + "runAfter": { + "Get_Client_ID": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['keyvault']['connectionId']" + } + }, + "method": "get", + "path": "/secrets/@{encodeURIComponent('Rubrik-AS-Int-ClientSecret')}/value" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Authentication": { + "runAfter": { + "Get_Client_Secret": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['RubrikCustomConnector']['connectionId']" + } + }, + "method": "post", + "body": { + "client_id": "@body('Get_Client_ID')?['value']", + "client_secret": "@body('Get_Client_Secret')?['value']" + }, + "headers": { + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "path": "/api/client_token" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Is_Authentication_Failed": { + "runAfter": { + "Authentication": [ + "Succeeded", + "Failed" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "not": { + "equals": [ + "@outputs('Authentication')['statusCode']", + 200 + ] + } + }, + { + "equals": [ + "@empty(body('Authentication')?['access_token'])", + true + ] + } + ] + }, + "actions": { + "Append_auth_failure": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Authentication (/api/client_token) failed with status @{outputs('Authentication')?['statusCode']}. Verify the Key Vault secrets and the Rubrik custom connector." + } + }, + "Comment_auth_failure": { + "runAfter": { + "Append_auth_failure": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik IR Violation failed.

@{join(variables('Error_Messages'), '
')}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + }, + "Terminate_-_Authentication_failed": { + "runAfter": { + "Comment_auth_failure": [ + "Succeeded", + "Failed" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "AuthFailed", + "message": "@{join(variables('Error_Messages'), ' | ')}" + } + } + } + }, + "else": { + "actions": { + "Set_Access_Token": { + "type": "SetVariable", + "inputs": { + "name": "Access_Token", + "value": "@{body('Authentication')?['access_token']}" + } + } + } + } + }, + "HTTP_-_Get_IR_Violation": { + "runAfter": { + "Is_Authentication_Failed": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer @{variables('Access_Token')}", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "query DataSecurityViolationGetQuery($violationId: String!, $policyTypes: [PolicyType!]!) {\n policyViolation(violationId: $violationId, policyTypes: $policyTypes) {\n policyViolationId\n violationSeverity\n createdAt\n resourceId\n resourceType\n status\n lastUpdatedAt\n policy {\n policyId\n name\n description\n policySeverity\n policyType\n policyCategory\n frameworks\n manualRemediationProcess\n __typename\n }\n details {\n ... on IdentityViolationDetails {\n domainUniqueId\n principalUniqueId\n __typename\n }\n ... on IdpViolationDetails {\n domainUniqueId\n __typename\n }\n __typename\n }\n resourceMetadata {\n metadata {\n ... on IdentityMetadata {\n displayName\n domainName\n idpType\n principalType\n privilegeType\n userPrincipalName\n status\n title\n source\n identityTags\n uniqueId\n nativeType\n __typename\n }\n ... on IdpMetadata {\n domainName\n domainUniqueId\n idpType\n rootDomainName\n rootDomainId\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}", + "variables": { + "violationId": "@variables('ViolationId')", + "policyTypes": [ + "POLICY_TYPE_IDENTITY", + "POLICY_TYPE_IDP", + "POLICY_TYPE_IDENTITY_EVENT" + ] + } + } + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs" + ] + } + } + }, + "Is_Get_IR_Violation_Failed": { + "runAfter": { + "HTTP_-_Get_IR_Violation": [ + "Succeeded", + "Failed" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "not": { + "equals": [ + "@outputs('HTTP_-_Get_IR_Violation')['statusCode']", + 200 + ] + } + }, + { + "not": { + "equals": [ + "@empty(body('HTTP_-_Get_IR_Violation')?['errors'])", + true + ] + } + }, + { + "equals": [ + "@empty(body('HTTP_-_Get_IR_Violation')?['data']?['policyViolation'])", + true + ] + } + ] + }, + "actions": { + "Append_-_Get_violation_failed": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Get IR Violation failed for violation @{variables('ViolationId')} (status @{outputs('HTTP_-_Get_IR_Violation')?['statusCode']})." + } + }, + "Comment_-_Get_violation_failed": { + "runAfter": { + "Append_-_Get_violation_failed": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik IR Violation failed.

@{join(variables('Error_Messages'), '
')}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + }, + "Terminate_-_Get_violation_failed": { + "runAfter": { + "Comment_-_Get_violation_failed": [ + "Succeeded", + "Failed" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "GetViolationFailed", + "message": "@{join(variables('Error_Messages'), ' | ')}" + } + } + } + }, + "else": { + "actions": { + "Parse_JSON_-_Violation": { + "type": "ParseJson", + "inputs": { + "content": "@body('HTTP_-_Get_IR_Violation')", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "policyViolation": { + "type": "object", + "properties": { + "policyViolationId": { + "type": "string" + }, + "violationSeverity": { + "type": "string" + }, + "status": { + "type": "string" + }, + "policy": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "policyType": { + "type": "string" + }, + "manualRemediationProcess": { + "type": "string" + } + } + }, + "resourceMetadata": { + "type": "object", + "properties": { + "metadata": { + "type": "object", + "properties": { + "displayName": { + "type": "string" + }, + "idpType": { + "type": "string" + }, + "userPrincipalName": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + }, + "Set_UserPrincipalName": { + "runAfter": { + "Parse_JSON_-_Violation": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "UserPrincipalName", + "value": "@{coalesce(body('Parse_JSON_-_Violation')?['data']?['policyViolation']?['resourceMetadata']?['metadata']?['userPrincipalName'], '')}" + } + }, + "Set_DisplayName": { + "runAfter": { + "Set_UserPrincipalName": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "DisplayName", + "value": "@{coalesce(body('Parse_JSON_-_Violation')?['data']?['policyViolation']?['resourceMetadata']?['metadata']?['displayName'], '')}" + } + }, + "Set_IdpType": { + "runAfter": { + "Set_DisplayName": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "IdpType", + "value": "@{coalesce(body('Parse_JSON_-_Violation')?['data']?['policyViolation']?['resourceMetadata']?['metadata']?['idpType'], '')}" + } + }, + "Set_ManualRemediationProcess": { + "runAfter": { + "Set_IdpType": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "ManualRemediationProcess", + "value": "@{coalesce(body('Parse_JSON_-_Violation')?['data']?['policyViolation']?['policy']?['manualRemediationProcess'], '')}" + } + }, + "Set_PolicyType": { + "runAfter": { + "Set_ManualRemediationProcess": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "PolicyType", + "value": "@{coalesce(body('Parse_JSON_-_Violation')?['data']?['policyViolation']?['policy']?['policyType'], '')}" + } + }, + "Post_message_in_Teams_channel": { + "runAfter": { + "Set_PolicyType": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "body": { + "recipient": { + "groupId": "[[parameters('TeamsGroupId')]", + "channelId": "[[parameters('TeamsChannelId')]" + }, + "messageBody": "

Rubrik IR Violation

Violation ID: @{variables('ViolationId')}

Violation Severity: @{body('Parse_JSON_-_Violation')?['data']?['policyViolation']?['violationSeverity']}
Name: @{if(empty(variables('DisplayName')), 'N/A', variables('DisplayName'))}
Email: @{if(empty(variables('UserPrincipalName')), 'N/A', variables('UserPrincipalName'))}
Policy Name: @{body('Parse_JSON_-_Violation')?['data']?['policyViolation']?['policy']?['name']}

Policy Type: @{variables('PolicyType')}

Idp Type: @{if(empty(variables('IdpType')), 'N/A', variables('IdpType'))}
Manual remediation steps: @{variables('ManualRemediationProcess')}
MS Sentinel Incident: @{triggerBody()?['object']?['properties']?['incidentUrl']}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['teams']['connectionId']" + } + }, + "method": "post", + "path": "/beta/teams/conversation/message/poster/Flow bot/location/@{encodeURIComponent('channel')}" + } + }, + "HTTP_-_Update_Violation_Status": { + "runAfter": { + "Post_message_in_Teams_channel": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer @{variables('Access_Token')}", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "mutation UpdatePolicyViolationsMutation($input: BulkUpdatePolicyViolationsInput!) {\n bulkUpdatePolicyViolations(input: $input)\n}", + "variables": { + "input": { + "newPolicyViolationStatus": "POLICY_VIOLATION_STATUS_IN_PROGRESS", + "policyViolationIds": [ + "@variables('ViolationId')" + ] + } + } + } + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs" + ] + } + } + }, + "Is_Update_Violation_Status_Failed": { + "runAfter": { + "HTTP_-_Update_Violation_Status": [ + "Succeeded", + "Failed" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "not": { + "equals": [ + "@outputs('HTTP_-_Update_Violation_Status')['statusCode']", + 200 + ] + } + }, + { + "not": { + "equals": [ + "@empty(body('HTTP_-_Update_Violation_Status')?['errors'])", + true + ] + } + } + ] + }, + "actions": { + "Append_-_Update_status_failed": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Update violation status to IN_PROGRESS failed for violation @{variables('ViolationId')} (status @{outputs('HTTP_-_Update_Violation_Status')?['statusCode']})." + } + }, + "Comment_-_Update_status_failed": { + "runAfter": { + "Append_-_Update_status_failed": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik IR Violation partially completed.

The affected party was notified, but updating the Rubrik violation status failed:

@{join(variables('Error_Messages'), '
')}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + }, + "Terminate_-_Update_status_failed": { + "runAfter": { + "Comment_-_Update_status_failed": [ + "Succeeded", + "Failed" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "UpdateStatusFailed", + "message": "@{join(variables('Error_Messages'), ' | ')}" + } + } + } + }, + "else": { + "actions": { + "Add_comment_to_incident_(V3)": { + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik IR Violation

Violation ID: @{variables('ViolationId')}
Name: @{variables('DisplayName')}
Email: @{if(empty(variables('UserPrincipalName')), 'N/A', variables('UserPrincipalName'))}
Violation Severity: @{body('Parse_JSON_-_Violation')?['data']?['policyViolation']?['violationSeverity']}
Policy Type: @{variables('PolicyType')}
Policy Name: @{body('Parse_JSON_-_Violation')?['data']?['policyViolation']?['policy']?['name']}
Recommended remediation steps: @{variables('ManualRemediationProcess')}

Violation Status: In Progress


The violation status was set to IN_PROGRESS in Rubrik and the notification was posted to the SOC Teams channel.

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + } + } + } + } + } + } + } + } + }, + "parameters": { + "$connections": { + "value": { + "azuresentinel": { + "connectionId": "[[resourceId('Microsoft.Web/connections', variables('MicrosoftSentinelConnectionName'))]", + "connectionName": "[[variables('MicrosoftSentinelConnectionName')]", + "id": "[[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', variables('workspace-location-inline'), '/managedApis/azuresentinel')]", + "connectionProperties": { + "authentication": { + "type": "ManagedServiceIdentity" + } + } + }, + "RubrikCustomConnector": { + "connectionId": "[[resourceId('Microsoft.Web/connections', variables('RubrikcustomconnectorConnectionName'))]", + "connectionName": "[[variables('RubrikcustomconnectorConnectionName')]", + "id": "[[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', parameters('Rubrik Connector name'))]" + }, + "keyvault": { + "connectionId": "[[resourceId('Microsoft.Web/connections', variables('KeyvaultConnectionName'))]", + "connectionName": "[[variables('KeyvaultConnectionName')]", + "id": "[[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', variables('workspace-location-inline'), '/managedApis/keyvault')]", + "connectionProperties": { + "authentication": { + "type": "ManagedServiceIdentity" + } + } + }, + "teams": { + "connectionId": "[[resourceId('Microsoft.Web/connections', variables('TeamsConnectionName'))]", + "connectionName": "[[variables('TeamsConnectionName')]", + "id": "[[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', variables('workspace-location-inline'), '/managedApis/teams')]" + } + } + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Playbook-', last(split(variables('playbookId20'),'/'))))]", + "properties": { + "parentId": "[variables('playbookId20')]", + "contentId": "[variables('_playbookContentId20')]", + "kind": "Playbook", + "version": "[variables('playbookVersion20')]", + "source": { + "kind": "Solution", + "name": "RubrikSecurityCloud", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "Ben Meadowcroft", + "email": "[variables('_email')]" + }, + "support": { + "name": "Rubrik", + "email": "support@rubrik.com", + "tier": "Partner", + "link": "https://support.rubrik.com" + }, + "dependencies": { + "criteria": [ + { + "kind": "LogicAppsCustomConnector", + "contentId": "[variables('_RubrikCustomConnector')]", + "version": "[variables('playbookVersion1')]" + } + ] + } + } + } + ], + "metadata": { + "title": "Rubrik IR Violation", + "description": "Incident-triggered playbook that reads a Rubrik IR (Identity) violation ID from the Microsoft Sentinel incident Custom Details, authenticates to Rubrik Security Cloud through the RubrikCustomConnector, retrieves the latest violation details via GraphQL, extracts the affected identity (userPrincipalName, displayName, idpType), the policy details (type, name, severity) and the policy's manualRemediationProcess, posts a formatted notification to a SOC Microsoft Teams channel, adds a matching comment to the incident, and sets the violation status to IN_PROGRESS. Every Rubrik API call is guarded with error handling that comments on the incident and terminates the run on failure.", + "prerequisites": [ + "1. The Rubrik Security Cloud solution should be configured to connect to Rubrik Security Cloud API end points using a Service Account (client_id + client_secret) with a role that includes the privileges to read and update policy violations.", + "2. The Rubrik custom connector (RubrikCustomConnector) needs to be deployed prior to the deployment of this playbook, in the same resource group and region. This playbook uses its 'Authentication' (/api/client_token) operation to obtain the RSC bearer token.", + "3. Store Service account credentials in Key Vault and obtain the Key Vault name and tenantId.", + "a. Create a Key Vault with a unique name.", + "b. Go to KeyVault -> secrets, click Generate/import and create 'Rubrik-AS-Int-ClientId' & 'Rubrik-AS-Int-ClientSecret' for storing client_id and client_secret respectively (same secrets used by the other Rubrik playbooks).", + "4. A Microsoft Teams team and channel are required for SOC notifications." + ], + "postDeployment": [ + "**a. Authorize connections**", + "Once deployment is complete, authorize each connection.", + "1. Go to your logic app -> API connections -> Select the connection resource (RubrikCustomConnector and Microsoft Teams).", + "2. Go to General -> edit API connection.", + "3. Click Authorize.", + "4. Sign in (for Teams, use an account that can post to the target SOC channel).", + "5. Click Save.", + "6. Repeat steps for each connection, and supply the TeamsGroupId and TeamsChannelId parameters.", + "**b. Assign Role to comment on and update incidents**", + "1. Go to Log Analytics Workspace -> -> Access Control -> Add.", + "2. Add role assignment.", + "3. Assignment type: Job function roles.", + "4. Role: Microsoft Sentinel Responder.", + "5. Members: select managed identity for assigned access to and add your logic app as member.", + "6. Click on review + assign.", + "**c. Add Access policy in Key Vault**", + "Add an access policy for the playbook's managed identity to read the RSC service-account secrets.", + "1. Go to logic app -> -> Identity -> System assigned managed identity and copy the Object (principal) ID.", + "2. Go to keyvaults -> -> Access policies -> Create.", + "3. Select Get on secrets permissions. Click next.", + "4. In the principal section, search by the copied object ID. Click next.", + "5. Click review + create.", + "**d. Configurations in Microsoft Sentinel**", + "1. In Microsoft Sentinel, configure the automation rules to trigger this playbook on incidents produced by the Rubrik IR-violation analytics rule." + ], + "lastUpdateTime": "2026-08-05T00:00:00Z", + "entities": [ + "Account" + ], + "tags": [ + "Microsoft Sentinel", + "Incident", + "Rubrik", + "Identity Resilience", + "Remediation" + ], + "releaseNotes": { + "version": "1.0", + "title": "[variables('blanks')]", + "notes": [ + "Initial version" + ] + } + } + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('_playbookContentId20')]", + "contentKind": "Playbook", + "displayName": "RubrikIRViolation", + "contentProductId": "[variables('_playbookcontentProductId20')]", + "id": "[variables('_playbookcontentProductId20')]", + "version": "[variables('playbookVersion20')]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('playbookTemplateSpecName21')]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "RubrikQuarantineFiles Playbook with template version 3.5.5", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('playbookVersion21')]", + "parameters": { + "PlaybookName": { + "defaultValue": "RubrikQuarantineFiles", + "type": "string", + "metadata": { + "description": "Name of the Logic App / playbook resource. Must match RubrikQuarantineFilesPlaybookName on the caller." + } + }, + "TeamsGroupId": { + "type": "string", + "metadata": { + "description": "Microsoft Teams team (group) ID where the folder-name Adaptive Card is posted." + } + }, + "TeamsChannelId": { + "type": "string", + "metadata": { + "description": "Microsoft Teams channel ID (within TeamsGroupId) for the folder-name Adaptive Card." + } + }, + "KeyVaultName": { + "type": "string", + "metadata": { + "description": "Key Vault holding the Graph app registration client id and secret." + } + }, + "TenantId": { + "type": "string", + "metadata": { + "description": "Azure AD tenant ID used for the Microsoft Graph OAuth2 token endpoint." + } + } + }, + "variables": { + "TeamsConnectionName": "[[concat('teams-', parameters('PlaybookName'))]", + "KeyvaultConnectionName": "[[concat('keyvault-', parameters('PlaybookName'))]", + "connection-1": "[[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', variables('workspace-location-inline'), '/managedApis/teams')]", + "_connection-1": "[[variables('connection-1')]", + "connection-2": "[[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', variables('workspace-location-inline'), '/managedApis/keyvault')]", + "_connection-2": "[[variables('connection-2')]", + "workspace-location-inline": "[concat('[resourceGroup().locatio', 'n]')]", + "workspace-name": "[parameters('workspace')]", + "workspaceResourceId": "[[resourceId('microsoft.OperationalInsights/Workspaces', variables('workspace-name'))]" + }, + "resources": [ + { + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[[variables('TeamsConnectionName')]", + "location": "[[variables('workspace-location-inline')]", + "kind": "V1", + "properties": { + "displayName": "[[variables('TeamsConnectionName')]", + "api": { + "id": "[[variables('_connection-1')]" + } + } + }, + { + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[[variables('KeyvaultConnectionName')]", + "location": "[[variables('workspace-location-inline')]", + "kind": "V1", + "properties": { + "displayName": "[[variables('KeyvaultConnectionName')]", + "parameterValueType": "Alternative", + "alternativeParameterValues": { + "vaultName": "[[parameters('KeyVaultName')]" + }, + "api": { + "id": "[[variables('_connection-2')]" + } + } + }, + { + "type": "Microsoft.Logic/workflows", + "apiVersion": "2017-07-01", + "name": "[[parameters('PlaybookName')]", + "location": "[[variables('workspace-location-inline')]", + "tags": { + "LogicAppsCategory": "security", + "hidden-SentinelTemplateName": "RubrikQuarantineFiles", + "hidden-SentinelTemplateVersion": "3.2.0", + "hidden-SentinelWorkspaceId": "[[variables('workspaceResourceId')]" + }, + "identity": { + "type": "SystemAssigned" + }, + "dependsOn": [ + "[[resourceId('Microsoft.Web/connections', variables('TeamsConnectionName'))]", + "[[resourceId('Microsoft.Web/connections', variables('KeyvaultConnectionName'))]" + ], + "properties": { + "state": "Enabled", + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "$connections": { + "type": "Object" + } + }, + "triggers": { + "manual": { + "type": "Request", + "kind": "Http", + "inputs": { + "schema": { + "type": "object", + "properties": { + "objectType": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "filename": { + "type": "string" + }, + "filePath": { + "type": "string" + }, + "nativePath": { + "type": "string" + }, + "directory": { + "type": "string" + }, + "ownerUpn": { + "type": "string" + }, + "objectType": { + "type": "string" + }, + "physicalHost": { + "type": "string" + }, + "driveOwnerUpn": { + "type": "string" + }, + "siteUrl": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "actions": { + "Initialize_Graph_Base_URL": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Graph_Base_URL", + "type": "string", + "value": "https://graph.microsoft.com" + } + ] + } + }, + "Initialize_MDE_Base_URL": { + "runAfter": { + "Initialize_Graph_Base_URL": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "MDE_Base_URL", + "type": "string", + "value": "https://api.securitycenter.microsoft.com" + } + ] + } + }, + "Initialize_Object_Type": { + "runAfter": { + "Initialize_MDE_Base_URL": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Object_Type", + "type": "string", + "value": "@{toUpper(coalesce(triggerBody()?['objectType'], first(coalesce(triggerBody()?['files'], json('[]')))?['objectType'], ''))}" + } + ] + } + }, + "Initialize_Match_Type": { + "runAfter": { + "Initialize_Object_Type": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Match_Type", + "type": "string", + "value": "@{if(or(contains(variables('Object_Type'), 'ONEDRIVE'), equals(replace(variables('Object_Type'), '_', ''), 'O365SITE'), equals(replace(variables('Object_Type'), '_', ''), 'O365ONEDRIVE'), equals(replace(variables('Object_Type'), '_', ''), 'O365MAILBOX'), equals(replace(variables('Object_Type'), '_', ''), 'O365SHAREPOINTDRIVE')), 'ONEDRIVE', if(or(contains(replace(variables('Object_Type'), '_', ''), 'WINDOWSFILESET'), contains(replace(variables('Object_Type'), '_', ''), 'WINDOWSFILESHARE'), contains(variables('Object_Type'), 'NTFS')), 'NTFS', 'UNKNOWN'))}" + } + ] + } + }, + "Initialize_Overall_Status": { + "runAfter": { + "Initialize_Match_Type": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Overall_Status", + "type": "string", + "value": "Failed" + } + ] + } + }, + "Initialize_Results": { + "runAfter": { + "Initialize_Overall_Status": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Results", + "type": "array", + "value": "[variables('TemplateEmptyArray')]" + } + ] + } + }, + "Initialize_Access_Token": { + "runAfter": { + "Initialize_Results": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Access_Token", + "type": "string" + } + ] + } + }, + "Initialize_Quarantine_Share_Link": { + "runAfter": { + "Initialize_Access_Token": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Quarantine_Share_Link", + "type": "string" + } + ] + } + }, + "Initialize_Quarantine_Drive_Id": { + "runAfter": { + "Initialize_Quarantine_Share_Link": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Quarantine_Drive_Id", + "type": "string" + } + ] + } + }, + "Initialize_Quarantine_Item_Id": { + "runAfter": { + "Initialize_Quarantine_Drive_Id": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Quarantine_Item_Id", + "type": "string" + } + ] + } + }, + "Initialize_Quarantine_Folder_Label": { + "runAfter": { + "Initialize_Quarantine_Item_Id": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Quarantine_Folder_Label", + "type": "string" + } + ] + } + }, + "Initialize_Failure_Message": { + "runAfter": { + "Initialize_Quarantine_Folder_Label": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Failure_Message", + "type": "string" + } + ] + } + }, + "Initialize_File_Content": { + "runAfter": { + "Initialize_Failure_Message": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "File_Content", + "type": "string" + } + ] + } + }, + "Initialize_Matched_Resource": { + "runAfter": { + "Initialize_File_Content": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Matched_Resource", + "type": "object", + "value": "[variables('TemplateEmptyObject')]" + } + ] + } + }, + "Initialize_Target_Drive_Id": { + "runAfter": { + "Initialize_Matched_Resource": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Target_Drive_Id", + "type": "string" + } + ] + } + }, + "Initialize_Onedrive_File_Handled": { + "runAfter": { + "Initialize_Target_Drive_Id": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Onedrive_File_Handled", + "type": "boolean", + "value": false + } + ] + } + }, + "Initialize_Item_Path": { + "runAfter": { + "Initialize_Onedrive_File_Handled": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Item_Path", + "type": "string" + } + ] + } + }, + "Initialize_MDE_Access_Token": { + "runAfter": { + "Initialize_Item_Path": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "MDE_Access_Token", + "type": "string" + } + ] + } + }, + "Initialize_MDE_Machine_Id": { + "runAfter": { + "Initialize_MDE_Access_Token": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "MDE_Machine_Id", + "type": "string" + } + ] + } + }, + "Initialize_File_Sha1": { + "runAfter": { + "Initialize_MDE_Machine_Id": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "File_Sha1", + "type": "string" + } + ] + } + }, + "Initialize_MDE_Action_Id": { + "runAfter": { + "Initialize_File_Sha1": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "MDE_Action_Id", + "type": "string" + } + ] + } + }, + "Initialize_Ntfs_File_Handled": { + "runAfter": { + "Initialize_MDE_Action_Id": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Ntfs_File_Handled", + "type": "boolean", + "value": false + } + ] + } + }, + "Get_Graph_Client_Id": { + "runAfter": { + "Initialize_Item_Path_Final": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['keyvault']['connectionId']" + } + }, + "method": "get", + "path": "/secrets/@{encodeURIComponent('Graph-App-ClientId')}/value" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Get_Graph_Client_Secret": { + "runAfter": { + "Get_Graph_Client_Id": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['keyvault']['connectionId']" + } + }, + "method": "get", + "path": "/secrets/@{encodeURIComponent('Graph-App-ClientSecret')}/value" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Get_Graph_Token": { + "runAfter": { + "Get_Graph_Client_Secret": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "[[uri(environment().authentication.loginEndpoint, format('{0}/oauth2/v2.0/token', parameters('TenantId')))]", + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "body": "grant_type=client_credentials&client_id=@{encodeURIComponent(body('Get_Graph_Client_Id')?['value'])}&client_secret=@{encodeURIComponent(body('Get_Graph_Client_Secret')?['value'])}&scope=@{encodeURIComponent(concat(variables('Graph_Base_URL'), '/.default'))}" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Check_Get_Graph_Token": { + "runAfter": { + "Get_Graph_Token": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "less": [ + "@coalesce(outputs('Get_Graph_Token')?['statusCode'], 0)", + 200 + ] + }, + { + "greaterOrEquals": [ + "@coalesce(outputs('Get_Graph_Token')?['statusCode'], 0)", + 300 + ] + } + ] + }, + "actions": { + "Set_Failure_Message_auth": { + "type": "SetVariable", + "inputs": { + "name": "Failure_Message", + "value": "Failed to acquire a Microsoft Graph token (status @{coalesce(outputs('Get_Graph_Token')?['statusCode'], 'none')}). Verify the app registration credentials in Key Vault and the tenant id." + } + }, + "Set_Overall_Status_auth_failed": { + "runAfter": { + "Set_Failure_Message_auth": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Overall_Status", + "value": "Failed" + } + }, + "Response_auth_failed": { + "runAfter": { + "Set_Overall_Status_auth_failed": [ + "Succeeded" + ] + }, + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "status": "Failed", + "results": "@variables('Results')", + "error": "@variables('Failure_Message')" + } + } + }, + "Terminate_auth_failed": { + "runAfter": { + "Response_auth_failed": [ + "Succeeded" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "GraphAuthFailed", + "message": "@variables('Failure_Message')" + } + } + } + } + }, + "Set_Access_Token": { + "runAfter": { + "Check_Get_Graph_Token": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Access_Token", + "value": "@{body('Get_Graph_Token')?['access_token']}" + } + }, + "Switch_on_object_type": { + "runAfter": { + "Set_Access_Token": [ + "Succeeded" + ] + }, + "type": "Switch", + "expression": "@variables('Match_Type')", + "cases": { + "ONEDRIVE": { + "case": "ONEDRIVE", + "actions": { + "Compose_folder_card": { + "type": "Compose", + "inputs": { + "type": "AdaptiveCard", + "version": "1.4", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "body": [ + { + "type": "TextBlock", + "weight": "Bolder", + "size": "Medium", + "wrap": true, + "text": "Rubrik DSPM Quarantine" + }, + { + "type": "TextBlock", + "wrap": true, + "text": "Paste a share link to the destination quarantine folder. @{length(coalesce(triggerBody()?['files'], json('[]')))} file(s) will be moved into that single folder, whichever drive (OneDrive or SharePoint document library) each file came from. Create the folder in OneDrive or SharePoint first, use Share / Copy link, and paste the link here. Files with the same name are renamed rather than overwritten." + }, + { + "type": "Input.Text", + "id": "quarantineShareLink", + "label": "Quarantine folder share link", + "placeholder": "e.g. https://contoso.sharepoint.com/:f:/s/Security/Ex4mpl3...", + "isRequired": true, + "errorMessage": "Please paste the share link of the destination quarantine folder." + } + ], + "actions": [ + { + "type": "Action.Submit", + "title": "Confirm" + } + ] + } + }, + "Post_folder_card_and_wait": { + "runAfter": { + "Compose_folder_card": [ + "Succeeded" + ] + }, + "type": "ApiConnectionWebhook", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['teams']['connectionId']" + } + }, + "body": { + "notificationUrl": "@{listCallbackUrl()}", + "body": { + "messageBody": "@{string(outputs('Compose_folder_card'))}", + "recipient": { + "groupId": "[[parameters('TeamsGroupId')]", + "channelId": "[[parameters('TeamsChannelId')]" + } + } + }, + "path": "/v1.0/teams/conversation/gatherinput/poster/Flow bot/location/@{encodeURIComponent('Channel')}/$subscriptions" + } + }, + "Set_Quarantine_Share_Link": { + "runAfter": { + "Post_folder_card_and_wait": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Quarantine_Share_Link", + "value": "@{trim(coalesce(body('Post_folder_card_and_wait')?['data']?['quarantineShareLink'], ''))}" + } + }, + "Resolve_quarantine_share_link": { + "runAfter": { + "Set_Quarantine_Share_Link": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{variables('Graph_Base_URL')}/v1.0/shares/u!@{replace(replace(replace(trim(base64(variables('Quarantine_Share_Link'))), '=', ''), '/', '_'), '+', '-')}/driveItem", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}" + }, + "queries": { + "$select": "id,name,folder,webUrl,parentReference" + } + } + }, + "Check_resolve_quarantine_share_link": { + "runAfter": { + "Resolve_quarantine_share_link": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "greaterOrEquals": [ + "@coalesce(outputs('Resolve_quarantine_share_link')?['statusCode'], 0)", + 200 + ] + }, + { + "less": [ + "@coalesce(outputs('Resolve_quarantine_share_link')?['statusCode'], 0)", + 300 + ] + }, + { + "not": { + "equals": [ + "@trim(coalesce(body('Resolve_quarantine_share_link')?['id'], ''))", + "" + ] + } + }, + { + "not": { + "equals": [ + "@trim(coalesce(body('Resolve_quarantine_share_link')?['parentReference']?['driveId'], ''))", + "" + ] + } + }, + { + "not": { + "equals": [ + "@string(coalesce(body('Resolve_quarantine_share_link')?['folder'], ''))", + "" + ] + } + } + ] + }, + "actions": { + "Set_Quarantine_Drive_Id": { + "type": "SetVariable", + "inputs": { + "name": "Quarantine_Drive_Id", + "value": "@{body('Resolve_quarantine_share_link')?['parentReference']?['driveId']}" + } + }, + "Set_Quarantine_Item_Id": { + "runAfter": { + "Set_Quarantine_Drive_Id": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Quarantine_Item_Id", + "value": "@{body('Resolve_quarantine_share_link')?['id']}" + } + }, + "Set_Quarantine_Folder_Label": { + "runAfter": { + "Set_Quarantine_Item_Id": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Quarantine_Folder_Label", + "value": "@{coalesce(body('Resolve_quarantine_share_link')?['webUrl'], body('Resolve_quarantine_share_link')?['name'], 'quarantine folder')}" + } + } + }, + "else": { + "actions": { + "Set_Failure_Message_share_link": { + "type": "SetVariable", + "inputs": { + "name": "Failure_Message", + "value": "@{if(empty(trim(coalesce(variables('Quarantine_Share_Link'), ''))), 'No quarantine folder share link was provided by the analyst. No files were quarantined.', concat('Could not resolve the quarantine folder from the supplied share link (Graph status ', string(coalesce(outputs('Resolve_quarantine_share_link')?['statusCode'], 'none')), '). Ensure the link points to a folder (not a file), that it has not expired, and that the app registration can read it. No files were quarantined. ', string(coalesce(body('Resolve_quarantine_share_link'), ''))))}" + } + } + } + } + }, + "If_quarantine_destination_resolved": { + "runAfter": { + "Check_resolve_quarantine_share_link": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "not": { + "equals": [ + "@variables('Quarantine_Item_Id')", + "" + ] + } + } + ] + }, + "actions": { + "Batch_get_site_by_url": { + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{variables('Graph_Base_URL')}/v1.0/sites/@{first(split(replace(replace(trim(coalesce(first(coalesce(triggerBody()?['files'], json('[]')))?['siteUrl'], '')), 'https://', ''), 'http://', ''), '/'))}:@{if(greater(length(split(replace(replace(trim(coalesce(first(coalesce(triggerBody()?['files'], json('[]')))?['siteUrl'], '')), 'https://', ''), 'http://', ''), '/')), 1), join(createArray('', join(skip(split(replace(replace(trim(coalesce(first(coalesce(triggerBody()?['files'], json('[]')))?['siteUrl'], '')), 'https://', ''), 'http://', ''), '/'), 1), '/')), '/'), '')}", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}" + } + } + }, + "Batch_get_site_drives": { + "runAfter": { + "Batch_get_site_by_url": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{variables('Graph_Base_URL')}/v1.0/sites/@{encodeURIComponent(coalesce(body('Batch_get_site_by_url')?['id'], ''))}/drives", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}" + } + } + }, + "Set_Site_Drives": { + "runAfter": { + "Batch_get_site_drives": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Site_Drives", + "value": "@coalesce(body('Batch_get_site_drives')?['value'], json('[]'))" + } + }, + "For_each_file": { + "runAfter": { + "Set_Site_Drives": [ + "Succeeded" + ] + }, + "type": "Foreach", + "foreach": "@coalesce(triggerBody()?['files'], json('[]'))", + "runtimeConfiguration": { + "concurrency": { + "repetitions": 1 + } + }, + "actions": { + "Reset_Onedrive_File_Handled": { + "type": "SetVariable", + "inputs": { + "name": "Onedrive_File_Handled", + "value": false + } + }, + "Reset_Target_Drive_Id": { + "runAfter": { + "Reset_Onedrive_File_Handled": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Target_Drive_Id", + "value": "@null" + } + }, + "Reset_Matched_Resource": { + "runAfter": { + "Reset_Item_Path_Final": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Matched_Resource", + "value": "@null" + } + }, + "Set_Item_Path": { + "runAfter": { + "Reset_Matched_Resource": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Item_Path", + "value": "@{if(startsWith(replace(trim(coalesce(items('For_each_file')?['filePath'], '')), '\\', '/'), '/'), substring(replace(trim(coalesce(items('For_each_file')?['filePath'], '')), '\\', '/'), 1, sub(length(replace(trim(coalesce(items('For_each_file')?['filePath'], '')), '\\', '/')), 1)), replace(trim(coalesce(items('For_each_file')?['filePath'], '')), '\\', '/'))}" + } + }, + "Check_resolution_inputs": { + "runAfter": { + "Set_Item_Path": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "and": [ + { + "equals": [ + "@trim(coalesce(items('For_each_file')?['driveOwnerUpn'], ''))", + "" + ] + }, + { + "equals": [ + "@trim(coalesce(items('For_each_file')?['siteUrl'], ''))", + "" + ] + } + ] + } + ] + }, + "actions": { + "Append_result_no_drive_target": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "No drive owner UPN or site URL was supplied for this file, and no file path is available; cannot resolve the target drive without a tenant-wide search. Ensure the calling playbook sends driveOwnerUpn (OneDrive) or siteUrl (SharePoint) from the RSC snappable." + } + } + }, + "Set_Handled_no_drive_target": { + "runAfter": { + "Append_result_no_drive_target": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Onedrive_File_Handled", + "value": true + } + } + } + }, + "Check_file_path_present": { + "runAfter": { + "Check_resolution_inputs": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Onedrive_File_Handled')", + false + ] + }, + { + "equals": [ + "@variables('Item_Path')", + "" + ] + } + ] + }, + "actions": { + "Append_result_no_file_path": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "No filePath (RSC nativePath) was supplied for this file; cannot resolve it by path. Source NOT modified." + } + } + }, + "Set_Handled_no_file_path": { + "runAfter": { + "Append_result_no_file_path": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Onedrive_File_Handled", + "value": true + } + } + } + }, + "Resolve_target_drive": { + "runAfter": { + "Check_file_path_present": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Onedrive_File_Handled')", + false + ] + }, + { + "not": { + "equals": [ + "@trim(coalesce(items('For_each_file')?['driveOwnerUpn'], ''))", + "" + ] + } + } + ] + }, + "actions": { + "Get_user_drive": { + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{variables('Graph_Base_URL')}/v1.0/users/@{encodeURIComponent(trim(items('For_each_file')?['driveOwnerUpn']))}/drive", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}" + } + } + }, + "Check_Get_user_drive": { + "runAfter": { + "Get_user_drive": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "greaterOrEquals": [ + "@coalesce(outputs('Get_user_drive')?['statusCode'], 0)", + 200 + ] + }, + { + "less": [ + "@coalesce(outputs('Get_user_drive')?['statusCode'], 0)", + 300 + ] + }, + { + "not": { + "equals": [ + "@trim(coalesce(body('Get_user_drive')?['id'], ''))", + "" + ] + } + } + ] + }, + "actions": { + "Set_Target_Drive_Id_onedrive": { + "type": "SetVariable", + "inputs": { + "name": "Target_Drive_Id", + "value": "@{body('Get_user_drive')?['id']}" + } + }, + "Set_Drive_Name_onedrive": { + "runAfter": { + "Set_Target_Drive_Id_onedrive": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Drive_Name", + "value": "@{coalesce(body('Get_user_drive')?['name'], '')}" + } + } + }, + "else": { + "actions": { + "Append_result_onedrive_drive_unresolved": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "Could not resolve the OneDrive for owner '@{items('For_each_file')?['driveOwnerUpn']}' (Graph status @{coalesce(outputs('Get_user_drive')?['statusCode'], 'none')}). Verify the user exists, has a provisioned OneDrive, and that the app registration holds Files.ReadWrite.All. @{string(coalesce(body('Get_user_drive'), ''))}" + } + } + }, + "Set_Handled_onedrive_drive_unresolved": { + "runAfter": { + "Append_result_onedrive_drive_unresolved": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Onedrive_File_Handled", + "value": true + } + } + } + } + } + }, + "else": { + "actions": { + "If_site_url_provided": { + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Onedrive_File_Handled')", + false + ] + }, + { + "not": { + "equals": [ + "@trim(coalesce(items('For_each_file')?['siteUrl'], ''))", + "" + ] + } + } + ] + }, + "actions": { + "Check_Get_site_by_url_response": { + "type": "If", + "expression": { + "and": [ + { + "not": { + "equals": [ + "@trim(coalesce(body('Batch_get_site_by_url')?['id'], ''))", + "" + ] + } + } + ] + }, + "actions": { + "Check_Get_site_drive": { + "type": "If", + "expression": { + "and": [ + { + "greater": [ + "@length(coalesce(variables('Site_Drives'), json('[]')))", + 0 + ] + } + ] + }, + "actions": { + "Filter_drives_by_name": { + "type": "Query", + "inputs": { + "from": "@coalesce(variables('Site_Drives'), json('[]'))", + "where": "@equals(toLower(trim(coalesce(item()?['name'], ''))), toLower(first(split(variables('Item_Path'), '/'))))" + } + }, + "Filter_drives_by_weburl": { + "runAfter": { + "Filter_drives_by_name": [ + "Succeeded" + ] + }, + "type": "Query", + "inputs": { + "from": "@coalesce(variables('Site_Drives'), json('[]'))", + "where": "@endsWith(toLower(replace(coalesce(item()?['webUrl'], ''), '%20', ' ')), toLower(concat('/', first(split(coalesce(variables('Item_Path'), ''), '/')))))" + } + }, + "Set_Matched_Drive_sharepoint": { + "runAfter": { + "Filter_drives_by_weburl": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Matched_Drive", + "value": "@if(greater(length(body('Filter_drives_by_name')), 0), first(body('Filter_drives_by_name')), if(greater(length(body('Filter_drives_by_weburl')), 0), first(body('Filter_drives_by_weburl')), json('{}')))" + } + }, + "Set_Target_Drive_Id_sharepoint": { + "runAfter": { + "Set_Matched_Drive_sharepoint": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Target_Drive_Id", + "value": "@{coalesce(variables('Matched_Drive')?['id'], '')}" + } + }, + "Set_Drive_Name_sharepoint": { + "runAfter": { + "Set_Target_Drive_Id_sharepoint": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Drive_Name", + "value": "@{if(equals(trim(coalesce(variables('Matched_Drive')?['id'], '')), ''), '', first(split(variables('Item_Path'), '/')))}" + } + }, + "Check_library_matched": { + "type": "If", + "runAfter": { + "Set_Drive_Name_sharepoint": [ + "Succeeded" + ] + }, + "expression": { + "and": [ + { + "equals": [ + "@trim(coalesce(variables('Target_Drive_Id'), ''))", + "" + ] + } + ] + }, + "actions": { + "Append_result_library_unmatched": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "No document library on site '@{items('For_each_file')?['siteUrl']}' matches the first segment of the file path '@{variables('Item_Path')}' (by name or URL). Nothing was modified." + } + } + }, + "Set_Handled_library_unmatched": { + "type": "SetVariable", + "runAfter": { + "Append_result_library_unmatched": [ + "Succeeded" + ] + }, + "inputs": { + "name": "Onedrive_File_Handled", + "value": true + } + } + } + } + }, + "else": { + "actions": { + "Append_result_sharepoint_drive_unresolved": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "Resolved the SharePoint site but could not enumerate its document libraries (Graph status @{coalesce(outputs('Batch_get_site_drives')?['statusCode'], 'none')}). @{string(coalesce(body('Batch_get_site_drives'), ''))}" + } + } + }, + "Set_Handled_sharepoint_drive_unresolved": { + "runAfter": { + "Append_result_sharepoint_drive_unresolved": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Onedrive_File_Handled", + "value": true + } + } + } + } + } + }, + "else": { + "actions": { + "Append_result_sharepoint_site_unresolved": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "Could not resolve the SharePoint site '@{items('For_each_file')?['siteUrl']}' (Graph status @{coalesce(outputs('Batch_get_site_by_url')?['statusCode'], 'none')}). Verify the site URL from the RSC snappable and that the app registration holds Sites.ReadWrite.All. @{string(coalesce(body('Batch_get_site_by_url'), ''))}" + } + } + }, + "Set_Handled_sharepoint_site_unresolved": { + "runAfter": { + "Append_result_sharepoint_site_unresolved": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Onedrive_File_Handled", + "value": true + } + } + } + } + } + } + } + } + } + }, + "Resolve_item_by_path": { + "runAfter": { + "Resolve_target_drive": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Onedrive_File_Handled')", + false + ] + }, + { + "not": { + "equals": [ + "@trim(coalesce(variables('Target_Drive_Id'), ''))", + "" + ] + } + } + ] + }, + "actions": { + "Set_Item_Path_final": { + "type": "SetVariable", + "inputs": { + "name": "Item_Path_Final", + "value": "@{if(and(not(equals(trim(coalesce(variables('Drive_Name'), '')), '')), startsWith(toLower(coalesce(variables('Item_Path'), '')), toLower(concat(coalesce(variables('Drive_Name'), ''), '/')))), substring(coalesce(variables('Item_Path'), ''), add(length(coalesce(variables('Drive_Name'), '')), 1), sub(length(coalesce(variables('Item_Path'), '')), add(length(coalesce(variables('Drive_Name'), '')), 1))), coalesce(variables('Item_Path'), ''))}" + } + }, + "Get_item_by_path": { + "runAfter": { + "Set_Item_Path_final": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{variables('Graph_Base_URL')}/v1.0/drives/@{encodeURIComponent(variables('Target_Drive_Id'))}/root:/@{replace(encodeURIComponent(variables('Item_Path_Final')), '%2F', '/')}", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}" + }, + "queries": { + "$select": "id,name,size,file,folder,parentReference,webUrl" + } + } + }, + "Check_Get_item_by_path": { + "runAfter": { + "Get_item_by_path": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "greaterOrEquals": [ + "@coalesce(outputs('Get_item_by_path')?['statusCode'], 0)", + 200 + ] + }, + { + "less": [ + "@coalesce(outputs('Get_item_by_path')?['statusCode'], 0)", + 300 + ] + }, + { + "not": { + "equals": [ + "@trim(coalesce(body('Get_item_by_path')?['id'], ''))", + "" + ] + } + }, + { + "not": { + "equals": [ + "@string(coalesce(body('Get_item_by_path')?['file'], ''))", + "" + ] + } + } + ] + }, + "actions": { + "Set_Matched_Resource": { + "type": "SetVariable", + "inputs": { + "name": "Matched_Resource", + "value": "@body('Get_item_by_path')" + } + } + }, + "else": { + "actions": { + "Append_result_file_not_found_at_path": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "File not found at path '@{variables('Item_Path_Final')}' in document library '@{coalesce(variables('Matched_Drive')?['name'], variables('Drive_Name'), 'default')}' (drive '@{variables('Target_Drive_Id')}') (Graph status @{coalesce(outputs('Get_item_by_path')?['statusCode'], 'none')}). The playbook resolves files by exact drive and path and does not fall back to a tenant-wide search, so nothing was modified. Verify the RSC nativePath matches the current location of the file. @{string(coalesce(body('Get_item_by_path'), ''))}" + } + } + }, + "Set_Handled_file_not_found_at_path": { + "runAfter": { + "Append_result_file_not_found_at_path": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Onedrive_File_Handled", + "value": true + } + } + } + } + } + } + }, + "If_file_found": { + "runAfter": { + "Resolve_item_by_path": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Onedrive_File_Handled')", + false + ] + }, + { + "not": { + "equals": [ + "@coalesce(variables('Matched_Resource')?['id'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Copy_to_quarantine": { + "type": "Http", + "operationOptions": "DisableAsyncPattern", + "inputs": { + "method": "POST", + "uri": "@{variables('Graph_Base_URL')}/v1.0/drives/@{encodeURIComponent(variables('Matched_Resource')?['parentReference']?['driveId'])}/items/@{encodeURIComponent(variables('Matched_Resource')?['id'])}/copy?@{concat('@', 'microsoft.graph.conflictBehavior')}=rename", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json" + }, + "body": { + "parentReference": { + "driveId": "@{variables('Quarantine_Drive_Id')}", + "id": "@{variables('Quarantine_Item_Id')}" + } + } + } + }, + "Check_copy_accepted": { + "type": "If", + "runAfter": { + "Copy_to_quarantine": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "expression": { + "and": [ + { + "equals": [ + "@coalesce(outputs('Copy_to_quarantine')?['statusCode'], 0)", + 202 + ] + }, + { + "not": { + "equals": [ + "@trim(coalesce(outputs('Copy_to_quarantine')?['headers']?['Location'], ''))", + "" + ] + } + } + ] + }, + "actions": { + "Until_copy_finished": { + "type": "Until", + "limit": { + "count": 200, + "timeout": "PT10M" + }, + "expression": "@not(contains(json('[\"notStarted\",\"inProgress\",\"waiting\"]'), coalesce(body('Get_copy_status')?['status'], 'inProgress')))", + "actions": { + "Get_copy_status": { + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{outputs('Copy_to_quarantine')?['headers']?['Location']}" + } + }, + "If_copy_pending": { + "type": "If", + "runAfter": { + "Get_copy_status": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "expression": { + "and": [ + { + "equals": [ + "@contains(json('[\"notStarted\",\"inProgress\",\"waiting\"]'), coalesce(body('Get_copy_status')?['status'], 'inProgress'))", + true + ] + } + ] + }, + "actions": { + "Wait_3_seconds": { + "type": "Wait", + "inputs": { + "interval": { + "count": 3, + "unit": "Second" + } + } + } + } + } + } + }, + "Check_copy_completed": { + "type": "If", + "runAfter": { + "Until_copy_finished": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "expression": { + "and": [ + { + "equals": [ + "@coalesce(body('Get_copy_status')?['status'], '')", + "completed" + ] + }, + { + "not": { + "equals": [ + "@trim(coalesce(body('Get_copy_status')?['resourceId'], ''))", + "" + ] + } + } + ] + }, + "actions": { + "Get_quarantine_item": { + "type": "Http", + "inputs": { + "method": "GET", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}" + }, + "queries": { + "$select": "id,name,webUrl" + }, + "uri": "@{variables('Graph_Base_URL')}/v1.0/drives/@{encodeURIComponent(variables('Quarantine_Drive_Id'))}/items/@{encodeURIComponent(body('Get_copy_status')?['resourceId'])}" + } + }, + "Delete_source_file": { + "runAfter": { + "Get_quarantine_item": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "Http", + "inputs": { + "method": "DELETE", + "uri": "@{variables('Graph_Base_URL')}/v1.0/drives/@{encodeURIComponent(variables('Matched_Resource')?['parentReference']?['driveId'])}/items/@{encodeURIComponent(variables('Matched_Resource')?['id'])}", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}" + } + } + }, + "Append_result_success": { + "runAfter": { + "Delete_source_file": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "@if(and(greaterOrEquals(coalesce(outputs('Delete_source_file')?['statusCode'], 0), 200), less(coalesce(outputs('Delete_source_file')?['statusCode'], 0), 300)), 'Success', 'Failed')", + "newLocation": "@coalesce(body('Get_quarantine_item')?['webUrl'], '')", + "error": "@if(and(greaterOrEquals(coalesce(outputs('Delete_source_file')?['statusCode'], 0), 200), less(coalesce(outputs('Delete_source_file')?['statusCode'], 0), 300)), '', concat('Uploaded to quarantine but failed to delete the source (Graph status ', string(coalesce(outputs('Delete_source_file')?['statusCode'], 'none')), '); delete manually.'))" + } + } + }, + "Set_Handled_success": { + "type": "SetVariable", + "runAfter": { + "Append_result_success": [ + "Succeeded" + ] + }, + "inputs": { + "name": "Onedrive_File_Handled", + "value": true + } + } + }, + "else": { + "actions": { + "Append_result_copy_incomplete": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "The server-side copy to the quarantine folder '@{variables('Quarantine_Folder_Label')}' did not complete within 10 minutes (copy status '@{coalesce(body('Get_copy_status')?['status'], '')}'). Source NOT deleted. @{string(coalesce(body('Get_copy_status')?['error'], ''))}" + } + } + }, + "Set_Handled_copy_incomplete": { + "type": "SetVariable", + "runAfter": { + "Append_result_copy_incomplete": [ + "Succeeded" + ] + }, + "inputs": { + "name": "Onedrive_File_Handled", + "value": true + } + } + } + } + } + }, + "else": { + "actions": { + "Append_result_copy_failed": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "The server-side copy to the quarantine folder '@{variables('Quarantine_Folder_Label')}' could not be started (Graph status @{coalesce(outputs('Copy_to_quarantine')?['statusCode'], 'none')}). Source NOT deleted. @{string(coalesce(body('Copy_to_quarantine')?['error'], ''))}" + } + } + }, + "Set_Handled_copy_failed": { + "type": "SetVariable", + "runAfter": { + "Append_result_copy_failed": [ + "Succeeded" + ] + }, + "inputs": { + "name": "Onedrive_File_Handled", + "value": true + } + } + } + } + } + } + }, + "Reset_Drive_Name": { + "runAfter": { + "Reset_Target_Drive_Id": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Drive_Name", + "value": "@null" + } + }, + "Reset_Matched_Drive": { + "runAfter": { + "Reset_Drive_Name": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Matched_Drive", + "value": "@null" + } + }, + "Reset_Item_Path_Final": { + "runAfter": { + "Reset_Matched_Drive": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Item_Path_Final", + "value": "@null" + } + } + } + }, + "Filter_failed_results": { + "runAfter": { + "For_each_file": [ + "Succeeded" + ] + }, + "type": "Query", + "inputs": { + "from": "@variables('Results')", + "where": "@not(equals(item()?['status'], 'Success'))" + } + }, + "Set_Overall_Status_from_results": { + "runAfter": { + "Filter_failed_results": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Overall_Status", + "value": "@if(equals(length(variables('Results')), 0), 'Success', if(equals(length(body('Filter_failed_results')), 0), 'Success', if(equals(length(body('Filter_failed_results')), length(variables('Results'))), 'Failed', 'PartialSuccess')))" + } + }, + "Set_Failure_Message_from_results": { + "runAfter": { + "Set_Overall_Status_from_results": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Failure_Message", + "value": "@{if(equals(length(body('Filter_failed_results')), 0), '', concat(string(length(body('Filter_failed_results'))), ' of ', string(length(variables('Results'))), ' file(s) failed to quarantine. ', string(body('Filter_failed_results'))))}" + } + } + }, + "else": { + "actions": { + "Set_Overall_Status_no_folder": { + "type": "SetVariable", + "inputs": { + "name": "Overall_Status", + "value": "Failed" + } + }, + "Append_no_folder_error": { + "runAfter": { + "Set_Overall_Status_no_folder": [ + "Succeeded" + ] + }, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "[variables('blanks')]", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "@variables('Failure_Message')" + } + } + } + } + } + } + } + }, + "NTFS": { + "case": "NTFS", + "actions": { + "Get_MDE_Token": { + "type": "Http", + "inputs": { + "method": "POST", + "uri": "[[uri(environment().authentication.loginEndpoint, format('{0}/oauth2/v2.0/token', parameters('TenantId')))]", + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "body": "grant_type=client_credentials&client_id=@{encodeURIComponent(body('Get_Graph_Client_Id')?['value'])}&client_secret=@{encodeURIComponent(body('Get_Graph_Client_Secret')?['value'])}&scope=@{encodeURIComponent(concat(variables('MDE_Base_URL'), '/.default'))}" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Check_Get_MDE_Token": { + "runAfter": { + "Get_MDE_Token": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "less": [ + "@coalesce(outputs('Get_MDE_Token')?['statusCode'], 0)", + 200 + ] + }, + { + "greaterOrEquals": [ + "@coalesce(outputs('Get_MDE_Token')?['statusCode'], 0)", + 300 + ] + } + ] + }, + "actions": { + "Set_Failure_Message_mde_auth": { + "type": "SetVariable", + "inputs": { + "name": "Failure_Message", + "value": "Failed to acquire a Microsoft Defender for Endpoint token (status @{coalesce(outputs('Get_MDE_Token')?['statusCode'], 'none')}). Verify the app registration has WindowsDefenderATP application permissions and the credentials in Key Vault." + } + }, + "Set_Overall_Status_mde_auth_failed": { + "runAfter": { + "Set_Failure_Message_mde_auth": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Overall_Status", + "value": "Failed" + } + }, + "Response_mde_auth_failed": { + "runAfter": { + "Set_Overall_Status_mde_auth_failed": [ + "Succeeded" + ] + }, + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "status": "Failed", + "results": "@variables('Results')", + "error": "@variables('Failure_Message')" + } + } + }, + "Terminate_mde_auth_failed": { + "runAfter": { + "Response_mde_auth_failed": [ + "Succeeded" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "MdeAuthFailed", + "message": "@variables('Failure_Message')" + } + } + } + }, + "else": { + "actions": { + "Set_MDE_Access_Token": { + "type": "SetVariable", + "inputs": { + "name": "MDE_Access_Token", + "value": "@{body('Get_MDE_Token')?['access_token']}" + } + }, + "For_each_ntfs_file": { + "runAfter": { + "Set_MDE_Access_Token": [ + "Succeeded" + ] + }, + "type": "Foreach", + "foreach": "@coalesce(triggerBody()?['files'], json('[]'))", + "runtimeConfiguration": { + "concurrency": { + "repetitions": 1 + } + }, + "actions": { + "Reset_Ntfs_File_Handled": { + "type": "SetVariable", + "inputs": { + "name": "Ntfs_File_Handled", + "value": false + } + }, + "Reset_MDE_Machine_Id": { + "runAfter": { + "Reset_Ntfs_File_Handled": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "MDE_Machine_Id", + "value": "@null" + } + }, + "Reset_File_Sha1": { + "runAfter": { + "Reset_MDE_Machine_Id": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "File_Sha1", + "value": "@null" + } + }, + "Reset_MDE_Action_Id": { + "runAfter": { + "Reset_File_Sha1": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "MDE_Action_Id", + "value": "@null" + } + }, + "If_physical_host_provided": { + "runAfter": { + "Reset_MDE_Action_Id": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@trim(coalesce(items('For_each_ntfs_file')?['physicalHost'], ''))", + "" + ] + } + ] + }, + "actions": { + "Append_result_ntfs_no_host": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_ntfs_file')?['filename']", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "No physicalHost was provided for this Windows Fileset file, so the MDE device could not be resolved and the file was not quarantined. RSC supplies physicalHost only on SecurityViolation events (policyViolation.resourceMetadata.metadata.physicalHost); Classification events do not carry it. Quarantine this file from a SecurityViolation event, or quarantine it manually in Microsoft Defender for Endpoint." + } + } + }, + "Set_Handled_no_host": { + "runAfter": { + "Append_result_ntfs_no_host": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Ntfs_File_Handled", + "value": true + } + } + } + }, + "Get_MDE_Machine": { + "runAfter": { + "If_physical_host_provided": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{variables('MDE_Base_URL')}/api/machines?$filter=computerDnsName eq '@{replace(coalesce(items('For_each_ntfs_file')?['physicalHost'], ''), '''', '''''')}'", + "headers": { + "Authorization": "Bearer @{variables('MDE_Access_Token')}" + } + } + }, + "Check_Get_MDE_Machine": { + "runAfter": { + "Get_MDE_Machine": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Ntfs_File_Handled')", + false + ] + }, + { + "or": [ + { + "less": [ + "@coalesce(outputs('Get_MDE_Machine')?['statusCode'], 0)", + 200 + ] + }, + { + "greaterOrEquals": [ + "@coalesce(outputs('Get_MDE_Machine')?['statusCode'], 0)", + 300 + ] + }, + { + "equals": [ + "@length(coalesce(body('Get_MDE_Machine')?['value'], json('[]')))", + 0 + ] + } + ] + } + ] + }, + "actions": { + "Append_result_ntfs_device_not_found": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_ntfs_file')?['filename']", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "Could not resolve the file-share host '@{items('For_each_ntfs_file')?['physicalHost']}' in Microsoft Defender for Endpoint (status @{coalesce(outputs('Get_MDE_Machine')?['statusCode'], 'none')}). Ensure the host is onboarded to MDE." + } + } + }, + "Set_Handled_device_not_found": { + "runAfter": { + "Append_result_ntfs_device_not_found": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Ntfs_File_Handled", + "value": true + } + } + }, + "else": { + "actions": { + "Set_MDE_Machine_Id": { + "type": "SetVariable", + "inputs": { + "name": "MDE_Machine_Id", + "value": "@{first(coalesce(body('Get_MDE_Machine')?['value'], json('[]')))?['id']}" + } + } + } + } + }, + "If_not_yet_handled": { + "runAfter": { + "Check_Get_MDE_Machine": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Ntfs_File_Handled')", + false + ] + } + ] + }, + "actions": { + "Run_advanced_hunting_for_sha1": { + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('MDE_Base_URL')}/api/advancedqueries/run", + "headers": { + "Authorization": "Bearer @{variables('MDE_Access_Token')}", + "Content-Type": "application/json" + }, + "body": { + "Query": "DeviceFileEvents | where DeviceId == @'@{replace(coalesce(variables('MDE_Machine_Id'), ''), '''', '''''')}' and FileName =~ @'@{replace(coalesce(items('For_each_ntfs_file')?['filename'], ''), '''', '''''')}' and FolderPath =~ @'@{replace(coalesce(items('For_each_ntfs_file')?['nativePath'], items('For_each_ntfs_file')?['filePath'], ''), '''', '''''')}' and isnotempty(SHA1) | top 1 by Timestamp desc | project SHA1" + } + } + }, + "Check_advanced_hunting": { + "runAfter": { + "Run_advanced_hunting_for_sha1": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Ntfs_File_Handled')", + false + ] + }, + { + "or": [ + { + "less": [ + "@coalesce(outputs('Run_advanced_hunting_for_sha1')?['statusCode'], 0)", + 200 + ] + }, + { + "greaterOrEquals": [ + "@coalesce(outputs('Run_advanced_hunting_for_sha1')?['statusCode'], 0)", + 300 + ] + }, + { + "equals": [ + "@trim(coalesce(first(coalesce(body('Run_advanced_hunting_for_sha1')?['Results'], json('[]')))?['SHA1'], ''))", + "" + ] + } + ] + } + ] + }, + "actions": { + "Append_result_ntfs_sha1_not_found": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_ntfs_file')?['filename']", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "Could not resolve a SHA1 for the file via MDE advanced hunting (status @{coalesce(outputs('Run_advanced_hunting_for_sha1')?['statusCode'], 'none')}). The file may not have been observed by MDE on host '@{items('For_each_ntfs_file')?['physicalHost']}'; not quarantined." + } + } + }, + "Set_Handled_sha1_not_found": { + "runAfter": { + "Append_result_ntfs_sha1_not_found": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Ntfs_File_Handled", + "value": true + } + } + }, + "else": { + "actions": { + "Set_File_Sha1": { + "type": "SetVariable", + "inputs": { + "name": "File_Sha1", + "value": "@{first(coalesce(body('Run_advanced_hunting_for_sha1')?['Results'], json('[]')))?['SHA1']}" + } + } + } + } + }, + "Create_stop_and_quarantine": { + "runAfter": { + "Check_advanced_hunting": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('MDE_Base_URL')}/api/machines/@{encodeURIComponent(variables('MDE_Machine_Id'))}/StopAndQuarantineFile", + "headers": { + "Authorization": "Bearer @{variables('MDE_Access_Token')}", + "Content-Type": "application/json" + }, + "body": { + "Comment": "Rubrik DSPM violation quarantine: @{items('For_each_ntfs_file')?['filename']}", + "Sha1": "@{variables('File_Sha1')}" + } + } + }, + "Check_create_stop_and_quarantine": { + "runAfter": { + "Create_stop_and_quarantine": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Ntfs_File_Handled')", + false + ] + }, + { + "or": [ + { + "less": [ + "@coalesce(outputs('Create_stop_and_quarantine')?['statusCode'], 0)", + 200 + ] + }, + { + "greaterOrEquals": [ + "@coalesce(outputs('Create_stop_and_quarantine')?['statusCode'], 0)", + 300 + ] + }, + { + "equals": [ + "@trim(coalesce(body('Create_stop_and_quarantine')?['id'], ''))", + "" + ] + } + ] + } + ] + }, + "actions": { + "Append_result_ntfs_quarantine_create_failed": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_ntfs_file')?['filename']", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "Failed to create the MDE StopAndQuarantineFile action (status @{coalesce(outputs('Create_stop_and_quarantine')?['statusCode'], 'none')}). @{string(coalesce(body('Create_stop_and_quarantine'), ''))}" + } + } + }, + "Set_Handled_create_failed": { + "runAfter": { + "Append_result_ntfs_quarantine_create_failed": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Ntfs_File_Handled", + "value": true + } + } + }, + "else": { + "actions": { + "Set_Quarantine_Action_Id": { + "type": "SetVariable", + "inputs": { + "name": "MDE_Action_Id", + "value": "@{body('Create_stop_and_quarantine')?['id']}" + } + } + } + } + }, + "Until_action_complete": { + "runAfter": { + "Check_create_stop_and_quarantine": [ + "Succeeded" + ] + }, + "type": "Until", + "expression": "@or(equals(variables('Ntfs_File_Handled'), true), not(contains(json('[\"Pending\",\"InProgress\"]'), coalesce(body('Get_action_status')?['status'], 'Pending'))))", + "limit": { + "count": 60, + "timeout": "PT20M" + }, + "actions": { + "Wait_15_seconds": { + "type": "Wait", + "inputs": { + "interval": { + "count": 15, + "unit": "Second" + } + } + }, + "Get_action_status": { + "runAfter": { + "Wait_15_seconds": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{variables('MDE_Base_URL')}/api/machineactions/@{encodeURIComponent(variables('MDE_Action_Id'))}", + "headers": { + "Authorization": "Bearer @{variables('MDE_Access_Token')}" + } + } + } + } + }, + "If_quarantine_succeeded": { + "runAfter": { + "Until_action_complete": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Ntfs_File_Handled')", + false + ] + }, + { + "equals": [ + "@coalesce(body('Get_action_status')?['status'], '')", + "Succeeded" + ] + } + ] + }, + "actions": { + "Append_result_ntfs_success": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_ntfs_file')?['filename']", + "status": "Success", + "newLocation": "MDE quarantine on @{items('For_each_ntfs_file')?['physicalHost']}", + "error": "[variables('blanks')]" + } + } + } + }, + "else": { + "actions": { + "Append_result_ntfs_quarantine_incomplete": { + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Ntfs_File_Handled')", + false + ] + } + ] + }, + "actions": { + "Append_result_ntfs_incomplete_inner": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_ntfs_file')?['filename']", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "MDE StopAndQuarantineFile action did not succeed within the timeout (final status @{coalesce(body('Get_action_status')?['status'], 'Unknown')})." + } + } + } + } + } + } + } + } + } + } + } + }, + "Filter_failed_results_ntfs": { + "runAfter": { + "For_each_ntfs_file": [ + "Succeeded" + ] + }, + "type": "Query", + "inputs": { + "from": "@variables('Results')", + "where": "@not(equals(item()?['status'], 'Success'))" + } + }, + "Set_Overall_Status_ntfs": { + "runAfter": { + "Filter_failed_results_ntfs": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Overall_Status", + "value": "@if(equals(length(variables('Results')), 0), 'Success', if(equals(length(body('Filter_failed_results_ntfs')), 0), 'Success', if(equals(length(body('Filter_failed_results_ntfs')), length(variables('Results'))), 'Failed', 'PartialSuccess')))" + } + }, + "Set_Failure_Message_ntfs": { + "runAfter": { + "Set_Overall_Status_ntfs": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Failure_Message", + "value": "@{if(equals(length(body('Filter_failed_results_ntfs')), 0), '', concat(string(length(body('Filter_failed_results_ntfs'))), ' of ', string(length(variables('Results'))), ' NTFS file(s) failed to quarantine. ', string(body('Filter_failed_results_ntfs'))))}" + } + } + } + } + } + } + } + }, + "default": { + "actions": { + "Set_Overall_Status_unknown": { + "type": "SetVariable", + "inputs": { + "name": "Overall_Status", + "value": "NotImplemented" + } + }, + "Set_Failure_Message_unknown": { + "runAfter": { + "Set_Overall_Status_unknown": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Failure_Message", + "value": "not a valid objectType for quarantine (received '@{variables('Object_Type')}')." + } + } + } + } + }, + "Check_Overall_Status": { + "runAfter": { + "Switch_on_object_type": [ + "Succeeded", + "Failed" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "not": { + "equals": [ + "@variables('Overall_Status')", + "Success" + ] + } + }, + { + "not": { + "equals": [ + "@variables('Overall_Status')", + "NotImplemented" + ] + } + } + ] + }, + "actions": { + "Response_batch_failed": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "status": "@variables('Overall_Status')", + "results": "@variables('Results')", + "error": "@variables('Failure_Message')" + } + } + }, + "Terminate_batch_failed": { + "runAfter": { + "Response_batch_failed": [ + "Succeeded" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "QuarantineFailed", + "message": "@variables('Failure_Message')" + } + } + } + }, + "else": { + "actions": { + "Response_success": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "status": "@variables('Overall_Status')", + "results": "@variables('Results')", + "error": "@variables('Failure_Message')" + } + } + } + } + } + }, + "Initialize_Drive_Name": { + "runAfter": { + "Initialize_Ntfs_File_Handled": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Drive_Name", + "type": "string" + } + ] + } + }, + "Initialize_Matched_Drive": { + "runAfter": { + "Initialize_Drive_Name": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Matched_Drive", + "type": "object", + "value": "[variables('TemplateEmptyObject')]" + } + ] + } + }, + "Initialize_Item_Path_Final": { + "runAfter": { + "Initialize_Site_Drives": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Item_Path_Final", + "type": "string" + } + ] + } + }, + "Initialize_Site_Drives": { + "runAfter": { + "Initialize_Matched_Drive": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Site_Drives", + "type": "array" + } + ] + } + } + } + }, + "parameters": { + "$connections": { + "value": { + "teams": { + "connectionId": "[[resourceId('Microsoft.Web/connections', variables('TeamsConnectionName'))]", + "connectionName": "[[variables('TeamsConnectionName')]", + "id": "[[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', variables('workspace-location-inline'), '/managedApis/teams')]" + }, + "keyvault": { + "connectionId": "[[resourceId('Microsoft.Web/connections', variables('KeyvaultConnectionName'))]", + "connectionName": "[[variables('KeyvaultConnectionName')]", + "id": "[[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', variables('workspace-location-inline'), '/managedApis/keyvault')]", + "connectionProperties": { + "authentication": { + "type": "ManagedServiceIdentity" + } + } + } + } + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Playbook-', last(split(variables('playbookId21'),'/'))))]", + "properties": { + "parentId": "[variables('playbookId21')]", + "contentId": "[variables('_playbookContentId21')]", + "kind": "Playbook", + "version": "[variables('playbookVersion21')]", + "source": { + "kind": "Solution", + "name": "RubrikSecurityCloud", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "Ben Meadowcroft", + "email": "[variables('_email')]" + }, + "support": { + "name": "Rubrik", + "email": "support@rubrik.com", + "tier": "Partner", + "link": "https://support.rubrik.com" + } + } + } + ], + "metadata": { + "title": "Rubrik Quarantine Files", + "description": "This playbook is an HTTP Request-triggered child playbook that quarantines a batch of files reported by a Rubrik Security Cloud DSPM violation. It is called by the RubrikDSPMViolationRemediation playbook with the whole files array in a single call and iterates internally, so it runs once per incident instead of once per file. It branches on objectType into three paths: ONEDRIVE files are quarantined via Microsoft Graph (the analyst is asked once per run for a share link to a single destination quarantine folder via a Teams Adaptive Card; that link is resolved once up front with GET /v1.0/shares/{link}/driveItem into a destination driveId and item id, so the destination is decoupled from each file's source drive and no per-file folder lookup or create is performed. Each file is then resolved directly by drive and path - the drive from the RSC snappable identity, driveOwnerUpn for OneDrive or siteUrl for SharePoint, and the path from filePath/nativePath - downloaded, uploaded into the single destination folder with @microsoft.graph.conflictBehavior=rename so same-named files from different drives do not overwrite each other, and the source deleted only after a confirmed upload; if the file cannot be resolved at that exact path the file is reported Failed and is never matched by a tenant-wide search); NTFS (Windows Fileset) files are quarantined via Microsoft Defender for Endpoint (the device is resolved by physicalHost, each file's SHA1 is resolved via advanced hunting scoped by DeviceId, FileName and FolderPath, and a StopAndQuarantineFile machine action is created and polled for up to 20 minutes). Note that RSC supplies physicalHost only on SecurityViolation events, so Windows Fileset files arriving from a Classification event are reported Failed with an explanatory message rather than silently skipped; any other objectType returns NotImplemented. A single app registration (client id/secret in Key Vault) authenticates both Microsoft Graph and MDE; only the OAuth2 token scope differs per branch. The playbook returns { status, results: [ { filename, status, newLocation, error } ] } to the caller.", + "prerequisites": [ + "1. A single Azure AD app registration is used for BOTH Microsoft Graph and Microsoft Defender for Endpoint. Grant it (all admin-consented):", + "a. Microsoft Graph APPLICATION permissions: Files.ReadWrite.All, Sites.ReadWrite.All, User.Read.All.", + "b. WindowsDefenderATP (Microsoft Defender for Endpoint) APPLICATION permissions: Machine.ReadWrite.All (or Machine.StopAndQuarantine) and AdvancedQuery.Read.All.", + "2. Store the app registration credentials in Key Vault and obtain the Key Vault name and tenantId.", + "a. Create a Key Vault with a unique name.", + "b. Go to KeyVault -> secrets, click Generate/import and create TWO secrets using EXACTLY these names (they are fixed in the playbook and are not deployment parameters): 'Graph-App-ClientId' holding the app registration's Application (client) ID, and 'Graph-App-ClientSecret' holding its client secret VALUE. The same credentials authenticate both Microsoft Graph and Microsoft Defender for Endpoint.", + "NOTE: Make sure the Permission model in the Access Configuration of the Key Vault is set to 'Vault access policy'. If not, change it to 'Vault access policy'.", + "3. For the ONEDRIVE branch, a Microsoft Teams team and channel are required for the Adaptive Card that collects the quarantine folder share link.", + "3a. Create the destination quarantine folder in OneDrive or SharePoint before running the playbook and have its share link ready (Share -> Copy link). All files in a run are moved into that single folder regardless of which drive they came from, and same-named files are renamed rather than overwritten. The app registration must be able to read the link, so prefer a link scoped to people in your organization over a link restricted to named individuals. Exclude this quarantine folder from DSPM scan scope, otherwise the quarantined copies are scanned again and raise fresh violations for files that were just remediated.", + "4. For the NTFS branch, the target file shares must be hosted on devices onboarded to Microsoft Defender for Endpoint and resolvable by their physicalHost / computerDnsName. RSC supplies physicalHost only on SecurityViolation events; Windows Fileset files arriving from a Classification event cannot be quarantined and are reported Failed with an explanatory message. KNOWN LIMITATION: the file's SHA1 is resolved from the most recent DeviceFileEvents record (Defender retains roughly 30 days) and StopAndQuarantineFile is keyed by that hash. If the file has since been modified or deleted, Defender reports the action as successful even though nothing was quarantined.", + "5. This playbook is a prerequisite for the RubrikDSPMViolationRemediation playbook and must be deployed first, in the same resource group. RubrikDSPMViolationRemediation calls this playbook as a sub-playbook (via its RubrikQuarantineFilesPlaybookName parameter) once per incident to quarantine the violation files." + ], + "postDeployment": [ + "**a. Authorize connections**", + "Once deployment is complete, authorize each connection.", + "1. Go to your logic app -> API connections -> Select the Teams connection resource.", + "2. Go to General -> edit API connection.", + "3. Click Authorize.", + "4. Sign in.", + "5. Click Save.", + "6. The Key Vault connection uses the managed identity and does not require an interactive sign-in.", + "**b. Assign Role to comment on and update incidents (if the caller requires it)**", + "1. Go to Log Analytics Workspace -> -> Access Control -> Add.", + "2. Add role assignment.", + "3. Assignment type: Job function roles.", + "4. Role: Microsoft Sentinel Responder.", + "5. Members: select managed identity for assigned access to and add your logic app as member.", + "6. Click on review + assign.", + "**c. Add Access policy in Key Vault**", + "Add an access policy for the playbook's managed identity to read the app-registration secrets.", + "1. Go to logic app -> -> Identity -> System assigned managed identity and copy the Object (principal) ID.", + "2. Go to keyvaults -> -> Access policies -> Create.", + "3. Select Get on secrets permissions. Click next.", + "4. In the principal section, search by the copied object ID. Click next.", + "5. Click review + create.", + "**d. Grant the app registration API permissions**", + "1. In Entra ID -> App registrations -> -> API permissions, add the Microsoft Graph and WindowsDefenderATP application permissions listed in the prerequisites and grant admin consent.", + "2. For the NTFS branch, ensure the file-share hosts are onboarded to Microsoft Defender for Endpoint and resolvable by their physicalHost / computerDnsName.", + "**e. Configurations in Microsoft Sentinel**", + "1. This playbook has an HTTP Request trigger and is invoked by RubrikDSPMViolationRemediation; it does not attach directly to an automation rule.", + "2. Deploy this playbook before RubrikDSPMViolationRemediation. When deploying RubrikDSPMViolationRemediation, set its RubrikQuarantineFilesPlaybookName parameter to this Logic App's name so it can call this sub-playbook." + ], + "lastUpdateTime": "2026-10-04T00:00:00Z", + "tags": [ + "Microsoft Sentinel", + "Rubrik", + "DSPM", + "Quarantine" + ], + "releaseNotes": { + "version": "3.2.0", + "title": "[variables('blanks')]", + "notes": [ + "Initial version" + ] + } + } + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('_playbookContentId21')]", + "contentKind": "Playbook", + "displayName": "RubrikQuarantineFiles", + "contentProductId": "[variables('_playbookcontentProductId21')]", + "id": "[variables('_playbookcontentProductId21')]", + "version": "[variables('playbookVersion21')]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('dataConnectorTemplateSpecName1')]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "RubrikSecurityCloud data connector with template version 3.5.5", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('dataConnectorVersion1')]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',variables('_dataConnectorContentId1'))]", + "apiVersion": "2021-03-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "GenericUI", + "properties": { + "connectorUiConfig": { + "id": "[variables('_uiConfigId1')]", + "title": "Rubrik Security Cloud data connector (using Azure Functions)", + "publisher": "Rubrik, Inc", + "descriptionMarkdown": "The Rubrik Security Cloud data connector enables security operations teams to integrate insights from Rubrik's Data Observability services into Microsoft Sentinel. The insights include identification of anomalous filesystem behavior associated with ransomware and mass deletion, assess the blast radius of a ransomware attack, and sensitive data operators to prioritize and more rapidly investigate potential incidents.", + "graphQueries": [ + { + "metricName": "Total Anomaly Event data received", + "legend": "Rubrik_Anomaly_Data_CL", + "baseQuery": "Rubrik_Anomaly_Data_CL" + }, + { + "metricName": "Total Ransomware Event data received", + "legend": "Rubrik_Ransomware_Data_CL", + "baseQuery": "Rubrik_Ransomware_Data_CL" + }, + { + "metricName": "Total ThreatHunt Event data received", + "legend": "Rubrik_ThreatHunt_Data_CL", + "baseQuery": "Rubrik_ThreatHunt_Data_CL" + }, + { + "metricName": "Total Other Events data received", + "legend": "Rubrik_Events_Data_CL", + "baseQuery": "Rubrik_Events_Data_CL" + }, + { + "metricName": "Total Violation Event data received", + "legend": "Rubrik_Violation_Data_CL", + "baseQuery": "Rubrik_Violation_Data_CL" + } + ], + "sampleQueries": [ + { + "description": "Rubrik Anomaly Events - Anomaly Events for all severity types.", + "query": "Rubrik_Anomaly_Data_CL\n | sort by TimeGenerated desc" + }, + { + "description": "Rubrik Ransomware Analysis Events - Ransomware Analysis Events for all severity types.", + "query": "Rubrik_Ransomware_Data_CL\n | sort by TimeGenerated desc" + }, + { + "description": "Rubrik ThreatHunt Events - Threat Hunt Events for all severity types.", + "query": "Rubrik_ThreatHunt_Data_CL\n | sort by TimeGenerated desc" + }, + { + "description": "Rubrik Other Events - Other Events for all severity types.", + "query": "Rubrik_Events_Data_CL\n | sort by TimeGenerated desc" + }, + { + "description": "Rubrik Violation Events - Violation Events for all severity types.", + "query": "Rubrik_Violation_Data_CL\n | sort by TimeGenerated desc" + } + ], + "dataTypes": [ + { + "name": "Rubrik_Anomaly_Data_CL", + "lastDataReceivedQuery": "Rubrik_Anomaly_Data_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" + }, + { + "name": "Rubrik_Ransomware_Data_CL", + "lastDataReceivedQuery": "Rubrik_Ransomware_Data_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" + }, + { + "name": "Rubrik_ThreatHunt_Data_CL", + "lastDataReceivedQuery": "Rubrik_ThreatHunt_Data_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" + }, + { + "name": "Rubrik_Events_Data_CL", + "lastDataReceivedQuery": "Rubrik_Events_Data_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" + }, + { + "name": "Rubrik_Violation_Data_CL", + "lastDataReceivedQuery": "Rubrik_Violation_Data_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" + } + ], + "connectivityCriterias": [ + { + "type": "IsConnectedQuery", + "value": [ + "Rubrik_Anomaly_Data_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" + ] + }, + { + "type": "IsConnectedQuery", + "value": [ + "Rubrik_Ransomware_Data_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" + ] + }, + { + "type": "IsConnectedQuery", + "value": [ + "Rubrik_ThreatHunt_Data_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" + ] + }, + { + "type": "IsConnectedQuery", + "value": [ + "Rubrik_Events_Data_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" + ] + }, + { + "type": "IsConnectedQuery", + "value": [ + "Rubrik_Violation_Data_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" + ] + } + ], + "availability": { + "status": 1, + "isPreview": false + }, + "permissions": { + "resourceProvider": [ + { + "provider": "Microsoft.OperationalInsights/workspaces", + "permissionsDisplayText": "read and write permissions on the workspace are required.", + "providerDisplayName": "Workspace", + "scope": "Workspace", + "requiredPermissions": { + "write": true, + "read": true, + "delete": true + } + }, + { + "provider": "Microsoft.OperationalInsights/workspaces/sharedKeys", + "permissionsDisplayText": "read permissions to shared keys for the workspace are required. [See the documentation to learn more about workspace keys](https://docs.microsoft.com/azure/azure-monitor/platform/agent-windows#obtain-workspace-id-and-key).", + "providerDisplayName": "Keys", + "scope": "Workspace", + "requiredPermissions": { + "action": true + } + } + ], + "customs": [ + { + "name": "Microsoft.Web/sites permissions", + "description": "Read and write permissions to Azure Functions to create a Function App is required. [See the documentation to learn more about Azure Functions](https://docs.microsoft.com/azure/azure-functions/)." + } + ] + }, + "instructionSteps": [ + { + "description": ">**NOTE:** This connector uses Azure Functions to connect to the Rubrik webhook which push its logs into Microsoft Sentinel. This might result in additional data ingestion costs. Check the [Azure Functions pricing page](https://azure.microsoft.com/pricing/details/functions/) for details." + }, + { + "description": ">**(Optional Step)** Securely store workspace and API authorization key(s) or token(s) in Azure Key Vault. Azure Key Vault provides a secure mechanism to store and retrieve key values. [Follow these instructions](https://docs.microsoft.com/azure/app-service/app-service-key-vault-references) to use Azure Key Vault with an Azure Function App." + }, + { + "description": "**STEP 1 - Choose ONE from the following two deployment options to deploy the connector and the associated Azure Function**\n\n>**IMPORTANT:** This connector uses the Azure Monitor Log Ingestion API. Authentication uses the Function App's System-Assigned Managed Identity, which the ARM template grants the Monitoring Metrics Publisher role on the Data Collection Rules. No shared workspace key is required. The Data Collection Endpoint and Data Collection Rule immutable IDs are created and wired automatically by the ARM template." + }, + { + "description": "Use this method for automated deployment of the Rubrik connector.\n\n1. Click the **Deploy to Azure** (Commercial) or **Deploy to Azure Gov** button below.\n\n\t[![Deploy To Azure](https://aka.ms/deploytoazurebutton)](https://aka.ms/sentinel-RubrikWebhookEvents-azuredeploy)\n\t[![Deploy To Azure Gov](https://aka.ms/deploytoazuregovbutton)](https://aka.ms/sentinel-RubrikWebhookEvents-azuredeploy-gov)\n2. Select the preferred **Subscription**, **Resource Group** and **Location**.\n3. Enter the below information:\n\t\tFunction Name\n\t\tWorkspace Resource ID\n\t\tLocation\n\t\tAnomaliesTableName\n\t\tRansomwareAnalysisTableName\n\t\tThreatHuntsTableName\n\t\tEventsTableName\n\t\tViolationTableName\n\t\tRubrikInstanceUrl\n\t\tRubrikClientId\n\t\tRubrikClientSecret\n\t\tLogLevel\n\n4. Mark the checkbox labeled **I agree to the terms and conditions stated above**.\n5. Click **Purchase** to deploy.", + "title": "Option 1 - Azure Resource Manager (ARM) Template" + }, + { + "description": "Use the following step-by-step instructions to deploy the Rubrik Microsoft Sentinel data connector manually with Azure Functions (Deployment via Visual Studio Code).", + "title": "Option 2 - Manual Deployment of Azure Functions" + }, + { + "description": "**1. Deploy a Function App**\n\n> **NOTE:** You will need to [prepare VS code](https://docs.microsoft.com/azure/azure-functions/functions-create-first-function-python#prerequisites) for Azure function development.\n\n1. Download the [Azure Function App](https://aka.ms/sentinel-RubrikDataConnector-functionapp) file. Extract archive to your local development computer.\n2. Start VS Code. Choose File in the main menu and select Open Folder.\n3. Select the top level folder from extracted files.\n4. Choose the Azure icon in the Activity bar, then in the **Azure: Functions** area, choose the **Deploy to function app** button.\nIf you aren't already signed in, choose the Azure icon in the Activity bar, then in the **Azure: Functions** area, choose **Sign in to Azure**\nIf you're already signed in, go to the next step.\n5. Provide the following information at the prompts:\n\n\ta. **Select folder:** Choose a folder from your workspace or browse to one that contains your function app.\n\n\tb. **Select Subscription:** Choose the subscription to use.\n\n\tc. Select **Create new Function App in Azure** (Don't choose the Advanced option)\n\n\td. **Enter a globally unique name for the function app:** Type a name that is valid in a URL path. The name you type is validated to make sure that it's unique in Azure Functions. (e.g. RubrikXXXXX).\n\n\te. **Select a runtime:** Choose Python 3.12.\n\n\tf. Select a location for new resources. For better performance and lower costs choose the same [region](https://azure.microsoft.com/regions/) where Microsoft Sentinel is located.\n\n6. Deployment will begin. A notification is displayed after your function app is created and the deployment package is applied.\n7. Go to Azure Portal for the Function App configuration." + }, + { + "description": "**2. Configure the Function App**\n\n1. In the Function App, select the Function App Name and select **Configuration**.\n2. In the **Application settings** tab, select **+ New application setting**.\n3. Add each of the following application settings individually, with their respective values (case-sensitive):\n\t\tWorkspaceResourceId\n\t\tLocation\n\t\tAnomaliesTableName\n\t\tRansomwareAnalysisTableName\n\t\tThreatHuntsTableName\n\t\tEventsTableName\n\t\tViolationTableName\n\t\tAZURE_DATA_COLLECTION_ENDPOINT\n\t\tDCR_WEBHOOK_RULE_ID\n\t\tDCR_EVENTS_RULE_ID\n\t\tSCOPE\n\t\tRubrikInstanceUrl\n\t\tRubrikClientId\n\t\tRubrikClientSecret\n\t\tLogLevel\n - For Azure Government, set SCOPE to https://monitor.azure.us/.default and use the Gov Data Collection Endpoint.\n - This connector authenticates using the Function App's System-Assigned Managed Identity, which is granted the Monitoring Metrics Publisher role on the Data Collection Rules by the ARM template.\n - RubrikInstanceUrl / RubrikClientId / RubrikClientSecret are the Rubrik Security Cloud service-account credentials used only by the Violation classification hit-count gate.\n4. Once all application settings have been entered, click **Save**." + }, + { + "description": "**Post Deployment steps**\n\n" + }, + { + "description": "1. Go to Azure function Overview page and Click on **\"Functions\"** tab.\n2. Click on the function called **\"RubrikHttpStarter\"**.\n3. Go to **\"GetFunctionurl\"** and copy the function url.", + "title": "1) Get the Function app endpoint" + }, + { + "description": "Follow the Rubrik User Guide instructions to [Add a Webhook](https://docs.rubrik.com/en-us/saas/saas/common/adding_webhook.html) to begin receiving event information \n 1. Select the Microsoft Sentinel as the webhook Provider \n 2. Enter the desired Webhook name \n 3. Enter the URL part from copied Function-url as the webhook URL endpoint and replace **{functionname}** with **\"RubrikAnomalyOrchestrator\"**, for the Rubrik Microsoft Sentinel Solution \n 4. Select the EventType as Anomaly \n 5. Select the following severity levels: Critical, Warning, Informational \n 6. Choose multiple log types, if desired, when running **\"RubrikEventsOrchestrator\"** \n 7. Repeat the same steps to add webhooks for Anomaly Detection Analysis, Threat Hunt, Other Events and Violation.\n \n\n NOTE: while adding webhooks for Anomaly Detection Analysis, Threat Hunt, Other Events and Violation, replace **{functionname}** with **\"RubrikRansomwareOrchestrator\"**, **\"RubrikThreatHuntOrchestrator\"**, **\"RubrikEventsOrchestrator\"** and **\"RubrikViolationOrchestrator\"** respectively in copied function-url.\n\n NOTE: For Violation (classification) events, **\"RubrikViolationOrchestrator\"** applies a hit-count gate: classification-results events are ingested only when Rubrik Security Cloud reports policy hits > 0. This gate uses the **RubrikInstanceUrl**, **RubrikClientId**, and **RubrikClientSecret** application settings configured in Step 2.", + "title": "2) Add a webhook in RubrikSecurityCloud to send data to Microsoft Sentinel." + }, + { + "description": "*Now we are done with the rubrik Webhook configuration. Once the webhook events triggered , you should be able to see the Anomaly, Anomaly Detection Analysis, Threat Hunt events, Other Events and Violation events from the Rubrik into respective LogAnalytics workspace table called \"Rubrik_Anomaly_Data_CL\", \"Rubrik_Ransomware_Data_CL\", \"Rubrik_ThreatHunt_Data_CL\", \"Rubrik_Events_Data_CL\", and \"Rubrik_Violation_Data_CL\".*\n\n" + } + ] + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2023-04-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', last(split(variables('_dataConnectorId1'),'/'))))]", + "properties": { + "parentId": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectors', variables('_dataConnectorContentId1'))]", + "contentId": "[variables('_dataConnectorContentId1')]", + "kind": "DataConnector", + "version": "[variables('dataConnectorVersion1')]", + "source": { + "kind": "Solution", + "name": "RubrikSecurityCloud", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "Ben Meadowcroft", + "email": "[variables('_email')]" + }, + "support": { + "name": "Rubrik", + "email": "support@rubrik.com", + "tier": "Partner", + "link": "https://support.rubrik.com" } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('_dataConnectorContentId1')]", + "contentKind": "DataConnector", + "displayName": "Rubrik Security Cloud data connector (using Azure Functions)", + "contentProductId": "[variables('_dataConnectorcontentProductId1')]", + "id": "[variables('_dataConnectorcontentProductId1')]", + "version": "[variables('dataConnectorVersion1')]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2023-04-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', last(split(variables('_dataConnectorId1'),'/'))))]", + "dependsOn": [ + "[variables('_dataConnectorId1')]" + ], + "location": "[parameters('workspace-location')]", + "properties": { + "parentId": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectors', variables('_dataConnectorContentId1'))]", + "contentId": "[variables('_dataConnectorContentId1')]", + "kind": "DataConnector", + "version": "[variables('dataConnectorVersion1')]", + "source": { + "kind": "Solution", + "name": "RubrikSecurityCloud", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "Ben Meadowcroft", + "email": "[variables('_email')]" + }, + "support": { + "name": "Rubrik", + "email": "support@rubrik.com", + "tier": "Partner", + "link": "https://support.rubrik.com" + } + } + }, + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',variables('_dataConnectorContentId1'))]", + "apiVersion": "2021-03-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "GenericUI", + "properties": { + "connectorUiConfig": { + "title": "Rubrik Security Cloud data connector (using Azure Functions)", + "publisher": "Rubrik, Inc", + "descriptionMarkdown": "The Rubrik Security Cloud data connector enables security operations teams to integrate insights from Rubrik's Data Observability services into Microsoft Sentinel. The insights include identification of anomalous filesystem behavior associated with ransomware and mass deletion, assess the blast radius of a ransomware attack, and sensitive data operators to prioritize and more rapidly investigate potential incidents.", + "graphQueries": [ + { + "metricName": "Total Anomaly Event data received", + "legend": "Rubrik_Anomaly_Data_CL", + "baseQuery": "Rubrik_Anomaly_Data_CL" + }, + { + "metricName": "Total Ransomware Event data received", + "legend": "Rubrik_Ransomware_Data_CL", + "baseQuery": "Rubrik_Ransomware_Data_CL" + }, + { + "metricName": "Total ThreatHunt Event data received", + "legend": "Rubrik_ThreatHunt_Data_CL", + "baseQuery": "Rubrik_ThreatHunt_Data_CL" + }, + { + "metricName": "Total Other Events data received", + "legend": "Rubrik_Events_Data_CL", + "baseQuery": "Rubrik_Events_Data_CL" + }, + { + "metricName": "Total Violation Event data received", + "legend": "Rubrik_Violation_Data_CL", + "baseQuery": "Rubrik_Violation_Data_CL" + } + ], + "dataTypes": [ + { + "name": "Rubrik_Anomaly_Data_CL", + "lastDataReceivedQuery": "Rubrik_Anomaly_Data_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" + }, + { + "name": "Rubrik_Ransomware_Data_CL", + "lastDataReceivedQuery": "Rubrik_Ransomware_Data_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" + }, + { + "name": "Rubrik_ThreatHunt_Data_CL", + "lastDataReceivedQuery": "Rubrik_ThreatHunt_Data_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" + }, + { + "name": "Rubrik_Events_Data_CL", + "lastDataReceivedQuery": "Rubrik_Events_Data_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" + }, + { + "name": "Rubrik_Violation_Data_CL", + "lastDataReceivedQuery": "Rubrik_Violation_Data_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" + } + ], + "connectivityCriterias": [ + { + "type": "IsConnectedQuery", + "value": [ + "Rubrik_Anomaly_Data_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" + ] + }, + { + "type": "IsConnectedQuery", + "value": [ + "Rubrik_Ransomware_Data_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" + ] + }, + { + "type": "IsConnectedQuery", + "value": [ + "Rubrik_ThreatHunt_Data_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" + ] + }, + { + "type": "IsConnectedQuery", + "value": [ + "Rubrik_Events_Data_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" + ] + }, + { + "type": "IsConnectedQuery", + "value": [ + "Rubrik_Violation_Data_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" ] + } + ], + "sampleQueries": [ + { + "description": "Rubrik Anomaly Events - Anomaly Events for all severity types.", + "query": "Rubrik_Anomaly_Data_CL\n | sort by TimeGenerated desc" + }, + { + "description": "Rubrik Ransomware Analysis Events - Ransomware Analysis Events for all severity types.", + "query": "Rubrik_Ransomware_Data_CL\n | sort by TimeGenerated desc" + }, + { + "description": "Rubrik ThreatHunt Events - Threat Hunt Events for all severity types.", + "query": "Rubrik_ThreatHunt_Data_CL\n | sort by TimeGenerated desc" + }, + { + "description": "Rubrik Other Events - Other Events for all severity types.", + "query": "Rubrik_Events_Data_CL\n | sort by TimeGenerated desc" + }, + { + "description": "Rubrik Violation Events - Violation Events for all severity types.", + "query": "Rubrik_Violation_Data_CL\n | sort by TimeGenerated desc" + } + ], + "availability": { + "status": 1, + "isPreview": false + }, + "permissions": { + "resourceProvider": [ + { + "provider": "Microsoft.OperationalInsights/workspaces", + "permissionsDisplayText": "read and write permissions on the workspace are required.", + "providerDisplayName": "Workspace", + "scope": "Workspace", + "requiredPermissions": { + "write": true, + "read": true, + "delete": true + } + }, + { + "provider": "Microsoft.OperationalInsights/workspaces/sharedKeys", + "permissionsDisplayText": "read permissions to shared keys for the workspace are required. [See the documentation to learn more about workspace keys](https://docs.microsoft.com/azure/azure-monitor/platform/agent-windows#obtain-workspace-id-and-key).", + "providerDisplayName": "Keys", + "scope": "Workspace", + "requiredPermissions": { + "action": true + } + } + ], + "customs": [ + { + "name": "Microsoft.Web/sites permissions", + "description": "Read and write permissions to Azure Functions to create a Function App is required. [See the documentation to learn more about Azure Functions](https://docs.microsoft.com/azure/azure-functions/)." + } + ] + }, + "instructionSteps": [ + { + "description": ">**NOTE:** This connector uses Azure Functions to connect to the Rubrik webhook which push its logs into Microsoft Sentinel. This might result in additional data ingestion costs. Check the [Azure Functions pricing page](https://azure.microsoft.com/pricing/details/functions/) for details." + }, + { + "description": ">**(Optional Step)** Securely store workspace and API authorization key(s) or token(s) in Azure Key Vault. Azure Key Vault provides a secure mechanism to store and retrieve key values. [Follow these instructions](https://docs.microsoft.com/azure/app-service/app-service-key-vault-references) to use Azure Key Vault with an Azure Function App." + }, + { + "description": "**STEP 1 - Choose ONE from the following two deployment options to deploy the connector and the associated Azure Function**\n\n>**IMPORTANT:** This connector uses the Azure Monitor Log Ingestion API. Authentication uses the Function App's System-Assigned Managed Identity, which the ARM template grants the Monitoring Metrics Publisher role on the Data Collection Rules. No shared workspace key is required. The Data Collection Endpoint and Data Collection Rule immutable IDs are created and wired automatically by the ARM template." }, { - "description": "Use this method for automated deployment of the Rubrik connector.\n\n1. Click the **Deploy to Azure** button below. \n\n\t[![Deploy To Azure](https://aka.ms/deploytoazurebutton)](https://aka.ms/sentinel-RubrikWebhookEvents-azuredeploy)\n2. Select the preferred **Subscription**, **Resource Group** and **Location**. \n3. Enter the below information : \n\t\tFunction Name \n\t\tWorkspace ID \n\t\tWorkspace Key \n\t\tAnomaliesTableName \n\t\tRansomwareAnalysisTableName \n\t\tThreatHuntsTableName \n\t\tEventsTableName \n\t\tLogLevel \n \n4. Mark the checkbox labeled **I agree to the terms and conditions stated above**. \n5. Click **Purchase** to deploy.", + "description": "Use this method for automated deployment of the Rubrik connector.\n\n1. Click the **Deploy to Azure** (Commercial) or **Deploy to Azure Gov** button below.\n\n\t[![Deploy To Azure](https://aka.ms/deploytoazurebutton)](https://aka.ms/sentinel-RubrikWebhookEvents-azuredeploy)\n\t[![Deploy To Azure Gov](https://aka.ms/deploytoazuregovbutton)](https://aka.ms/sentinel-RubrikWebhookEvents-azuredeploy-gov)\n2. Select the preferred **Subscription**, **Resource Group** and **Location**.\n3. Enter the below information:\n\t\tFunction Name\n\t\tWorkspace Resource ID\n\t\tLocation\n\t\tAnomaliesTableName\n\t\tRansomwareAnalysisTableName\n\t\tThreatHuntsTableName\n\t\tEventsTableName\n\t\tViolationTableName\n\t\tRubrikInstanceUrl\n\t\tRubrikClientId\n\t\tRubrikClientSecret\n\t\tLogLevel\n\n4. Mark the checkbox labeled **I agree to the terms and conditions stated above**.\n5. Click **Purchase** to deploy.", "title": "Option 1 - Azure Resource Manager (ARM) Template" }, { @@ -34037,10 +41966,10 @@ "title": "Option 2 - Manual Deployment of Azure Functions" }, { - "description": "**1. Deploy a Function App**\n\n> **NOTE:** You will need to [prepare VS code](https://docs.microsoft.com/azure/azure-functions/functions-create-first-function-python#prerequisites) for Azure function development.\n\n1. Download the [Azure Function App](https://aka.ms/sentinel-RubrikWebhookEvents-functionapp) file. Extract archive to your local development computer.\n2. Start VS Code. Choose File in the main menu and select Open Folder.\n3. Select the top level folder from extracted files.\n4. Choose the Azure icon in the Activity bar, then in the **Azure: Functions** area, choose the **Deploy to function app** button.\nIf you aren't already signed in, choose the Azure icon in the Activity bar, then in the **Azure: Functions** area, choose **Sign in to Azure**\nIf you're already signed in, go to the next step.\n5. Provide the following information at the prompts:\n\n\ta. **Select folder:** Choose a folder from your workspace or browse to one that contains your function app.\n\n\tb. **Select Subscription:** Choose the subscription to use.\n\n\tc. Select **Create new Function App in Azure** (Don't choose the Advanced option)\n\n\td. **Enter a globally unique name for the function app:** Type a name that is valid in a URL path. The name you type is validated to make sure that it's unique in Azure Functions. (e.g. RubrikXXXXX).\n\n\te. **Select a runtime:** Choose Python 3.8 or above.\n\n\tf. Select a location for new resources. For better performance and lower costs choose the same [region](https://azure.microsoft.com/regions/) where Microsoft Sentinel is located.\n\n6. Deployment will begin. A notification is displayed after your function app is created and the deployment package is applied.\n7. Go to Azure Portal for the Function App configuration." + "description": "**1. Deploy a Function App**\n\n> **NOTE:** You will need to [prepare VS code](https://docs.microsoft.com/azure/azure-functions/functions-create-first-function-python#prerequisites) for Azure function development.\n\n1. Download the [Azure Function App](https://aka.ms/sentinel-RubrikDataConnector-functionapp) file. Extract archive to your local development computer.\n2. Start VS Code. Choose File in the main menu and select Open Folder.\n3. Select the top level folder from extracted files.\n4. Choose the Azure icon in the Activity bar, then in the **Azure: Functions** area, choose the **Deploy to function app** button.\nIf you aren't already signed in, choose the Azure icon in the Activity bar, then in the **Azure: Functions** area, choose **Sign in to Azure**\nIf you're already signed in, go to the next step.\n5. Provide the following information at the prompts:\n\n\ta. **Select folder:** Choose a folder from your workspace or browse to one that contains your function app.\n\n\tb. **Select Subscription:** Choose the subscription to use.\n\n\tc. Select **Create new Function App in Azure** (Don't choose the Advanced option)\n\n\td. **Enter a globally unique name for the function app:** Type a name that is valid in a URL path. The name you type is validated to make sure that it's unique in Azure Functions. (e.g. RubrikXXXXX).\n\n\te. **Select a runtime:** Choose Python 3.12.\n\n\tf. Select a location for new resources. For better performance and lower costs choose the same [region](https://azure.microsoft.com/regions/) where Microsoft Sentinel is located.\n\n6. Deployment will begin. A notification is displayed after your function app is created and the deployment package is applied.\n7. Go to Azure Portal for the Function App configuration." }, { - "description": "**2. Configure the Function App**\n\n1. In the Function App, select the Function App Name and select **Configuration**.\n2. In the **Application settings** tab, select **+ New application setting**.\n3. Add each of the following application settings individually, with their respective values (case-sensitive): \n\t\tWorkspaceID\n\t\tWorkspaceKey\n\t\tAnomaliesTableName\n\t\tRansomwareAnalysisTableName\n\t\tThreatHuntsTableName\n\t\tEventsTableName\n\t\tLogLevel\n\t\tlogAnalyticsUri (optional)\n - Use logAnalyticsUri to override the log analytics API endpoint for dedicated cloud. For example, for public cloud, leave the value empty; for Azure GovUS cloud environment, specify the value in the following format: https://.ods.opinsights.azure.us. \n4. Once all application settings have been entered, click **Save**." + "description": "**2. Configure the Function App**\n\n1. In the Function App, select the Function App Name and select **Configuration**.\n2. In the **Application settings** tab, select **+ New application setting**.\n3. Add each of the following application settings individually, with their respective values (case-sensitive):\n\t\tWorkspaceResourceId\n\t\tLocation\n\t\tAnomaliesTableName\n\t\tRansomwareAnalysisTableName\n\t\tThreatHuntsTableName\n\t\tEventsTableName\n\t\tViolationTableName\n\t\tAZURE_DATA_COLLECTION_ENDPOINT\n\t\tDCR_WEBHOOK_RULE_ID\n\t\tDCR_EVENTS_RULE_ID\n\t\tSCOPE\n\t\tRubrikInstanceUrl\n\t\tRubrikClientId\n\t\tRubrikClientSecret\n\t\tLogLevel\n - For Azure Government, set SCOPE to https://monitor.azure.us/.default and use the Gov Data Collection Endpoint.\n - This connector authenticates using the Function App's System-Assigned Managed Identity, which is granted the Monitoring Metrics Publisher role on the Data Collection Rules by the ARM template.\n - RubrikInstanceUrl / RubrikClientId / RubrikClientSecret are the Rubrik Security Cloud service-account credentials used only by the Violation classification hit-count gate.\n4. Once all application settings have been entered, click **Save**." }, { "description": "**Post Deployment steps**\n\n" @@ -34050,11 +41979,11 @@ "title": "1) Get the Function app endpoint" }, { - "description": "Follow the Rubrik User Guide instructions to [Add a Webhook](https://docs.rubrik.com/en-us/saas/saas/common/adding_webhook.html) to begin receiving event information \n 1. Select the Microsoft Sentinel as the webhook Provider \n 2. Enter the desired Webhook name \n 3. Enter the URL part from copied Function-url as the webhook URL endpoint and replace **{functionname}** with **\"RubrikAnomalyOrchestrator\"**, for the Rubrik Microsoft Sentinel Solution \n 4. Select the EventType as Anomaly \n 5. Select the following severity levels: Critical, Warning, Informational \n 6. Choose multiple log types, if desired, when running **\"RubrikEventsOrchestrator\"** \n 7. Repeat the same steps to add webhooks for Anomaly Detection Analysis, Threat Hunt and Other Events.\n \n\n NOTE: while adding webhooks for Anomaly Detection Analysis, Threat Hunt and Other Events, replace **{functionname}** with **\"RubrikRansomwareOrchestrator\"**, **\"RubrikThreatHuntOrchestrator\"** and **\"RubrikEventsOrchestrator\"** respectively in copied function-url.", + "description": "Follow the Rubrik User Guide instructions to [Add a Webhook](https://docs.rubrik.com/en-us/saas/saas/common/adding_webhook.html) to begin receiving event information \n 1. Select the Microsoft Sentinel as the webhook Provider \n 2. Enter the desired Webhook name \n 3. Enter the URL part from copied Function-url as the webhook URL endpoint and replace **{functionname}** with **\"RubrikAnomalyOrchestrator\"**, for the Rubrik Microsoft Sentinel Solution \n 4. Select the EventType as Anomaly \n 5. Select the following severity levels: Critical, Warning, Informational \n 6. Choose multiple log types, if desired, when running **\"RubrikEventsOrchestrator\"** \n 7. Repeat the same steps to add webhooks for Anomaly Detection Analysis, Threat Hunt, Other Events and Violation.\n \n\n NOTE: while adding webhooks for Anomaly Detection Analysis, Threat Hunt, Other Events and Violation, replace **{functionname}** with **\"RubrikRansomwareOrchestrator\"**, **\"RubrikThreatHuntOrchestrator\"**, **\"RubrikEventsOrchestrator\"** and **\"RubrikViolationOrchestrator\"** respectively in copied function-url.\n\n NOTE: For Violation (classification) events, **\"RubrikViolationOrchestrator\"** applies a hit-count gate: classification-results events are ingested only when Rubrik Security Cloud reports policy hits > 0. This gate uses the **RubrikInstanceUrl**, **RubrikClientId**, and **RubrikClientSecret** application settings configured in Step 2.", "title": "2) Add a webhook in RubrikSecurityCloud to send data to Microsoft Sentinel." }, { - "description": "*Now we are done with the rubrik Webhook configuration. Once the webhook events triggered , you should be able to see the Anomaly, Anomaly Detection Analysis, Threat Hunt events and Other Events from the Rubrik into respective LogAnalytics workspace table called \"Rubrik_Anomaly_Data_CL\", \"Rubrik_Ransomware_Data_CL\", \"Rubrik_ThreatHunt_Data_CL\", and \"Rubrik_Events_Data_CL\".*\n\n" + "description": "*Now we are done with the rubrik Webhook configuration. Once the webhook events triggered , you should be able to see the Anomaly, Anomaly Detection Analysis, Threat Hunt events, Other Events and Violation events from the Rubrik into respective LogAnalytics workspace table called \"Rubrik_Anomaly_Data_CL\", \"Rubrik_Ransomware_Data_CL\", \"Rubrik_ThreatHunt_Data_CL\", \"Rubrik_Events_Data_CL\", and \"Rubrik_Violation_Data_CL\".*\n\n" } ], "id": "[variables('_uiConfigId1')]" @@ -36560,7 +44489,7 @@ "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" ], "properties": { - "description": "RubrikCriticalAnomaly_AnalyticalRules Analytics Rule with template version 3.5.4", + "description": "RubrikCriticalAnomaly_AnalyticalRules Analytics Rule with template version 3.5.5", "mainTemplate": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "[variables('analyticRuleObject1').analyticRuleVersion1]", @@ -36588,10 +44517,10 @@ "status": "Available", "requiredDataConnectors": [ { - "connectorId": "RubrikSecurityCloudAzureFunctions", "dataTypes": [ "RubrikAnomalyData" - ] + ], + "connectorId": "RubrikSecurityCloudAzureFunctions" } ], "tactics": [ @@ -36604,11 +44533,11 @@ "aggregationKind": "AlertPerResult" }, "customDetails": { - "ClusterIdentifier": "custom_details_clusterId_g", "ObjectId": "custom_details_objectId_g", - "ClusterName": "custom_details_clusterName_s", + "ObjectType": "custom_details_objectType_s", + "ClusterIdentifier": "custom_details_clusterId_g", "ObjectName": "custom_details_objectName_s", - "ObjectType": "custom_details_objectType_s" + "ClusterName": "custom_details_clusterName_s" }, "incidentConfiguration": { "createIncident": true @@ -36666,7 +44595,7 @@ "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" ], "properties": { - "description": "RubrikThreatMonitoring_AnalyticalRules Analytics Rule with template version 3.5.4", + "description": "RubrikThreatMonitoring_AnalyticalRules Analytics Rule with template version 3.5.5", "mainTemplate": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "[variables('analyticRuleObject2').analyticRuleVersion2]", @@ -36694,10 +44623,10 @@ "status": "Available", "requiredDataConnectors": [ { - "connectorId": "RubrikSecurityCloudAzureFunctions", "dataTypes": [ "RubrikEventsData" - ] + ], + "connectorId": "RubrikSecurityCloudAzureFunctions" } ], "tactics": [ @@ -36710,13 +44639,13 @@ "aggregationKind": "AlertPerResult" }, "customDetails": { - "Url": "custom_details_url_s", - "EventName": "custom_details_eventName_s", "ObjectName": "custom_details_objectName_s", + "EventName": "custom_details_eventName_s", "ObjectId": "custom_details_objectId_g", - "ClusterIdentifier": "custom_details_clusterId_g", "ObjectType": "custom_details_objectType_s", - "Summary": "summary_s" + "Url": "custom_details_url_s", + "Summary": "summary_s", + "ClusterIdentifier": "custom_details_clusterId_g" }, "alertDetailsOverride": { "alertDisplayNameFormat": "ThreatMonitoring Found {{count_}} {{eventname}} Matches for {{custom_details_objectName_s}}" @@ -36724,12 +44653,12 @@ "incidentConfiguration": { "createIncident": true, "groupingConfiguration": { - "matchingMethod": "Selected", + "reopenClosedIncident": false, "groupByCustomDetails": [ "ObjectName", "ObjectId" ], - "reopenClosedIncident": false, + "matchingMethod": "Selected", "enabled": true, "lookbackDuration": "P7D" } @@ -36778,17 +44707,389 @@ "version": "[variables('analyticRuleObject2').analyticRuleVersion2]" } }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject3').analyticRuleTemplateSpecName3]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "RubrikClassificationEvents_AnalyticalRules Analytics Rule with template version 3.5.5", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject3').analyticRuleVersion3]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject3')._analyticRulecontentId3]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Creates an incident for Classification events (ClassificationResultsAvailable) for non DSPM license users.", + "displayName": "Create Incident for Classification events for non DSPM license users", + "enabled": false, + "query": "Rubrik_Violation_Data_CL\n| where class_s == \"Classification\" and custom_details_eventName_s in (\"ClassficationResultsAvailable\", \"ClassificationResultsAvailable\")\n| extend NewSeverity = case(severity_s==\"info\", \"Low\",\n severity_s==\"warning\", \"Medium\",\n severity_s==\"critical\", \"High\",\n \"Informational\"),\n ObjectId = iff(isnotempty(custom_details_objectId_s), custom_details_objectId_s, tostring(custom_details_objectId_g)),\n SnapshotId = custom_details_snapshotId_s\n", + "queryFrequency": "PT5M", + "queryPeriod": "PT5M", + "severity": "Medium", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "RubrikViolationData" + ], + "connectorId": "RubrikSecurityCloudAzureFunctions" + } + ], + "tactics": [ + "Collection" + ], + "techniques": [ + "T1213" + ], + "eventGroupingSettings": { + "aggregationKind": "AlertPerResult" + }, + "customDetails": { + "ObjectName": "custom_details_objectName_s", + "EventName": "custom_details_eventName_s", + "ObjectId": "ObjectId", + "ObjectType": "custom_details_objectType_s", + "Severity": "severity_s", + "EventType": "class_s", + "SnapshotId": "custom_details_snapshotId_s", + "Summary": "summary_s", + "EventTimestamp": "timestamp_t" + }, + "alertDetailsOverride": { + "alertSeverityColumnName": "NewSeverity", + "alertDisplayNameFormat": "{{class_s}} event occurred for object {{custom_details_objectName_s}} with type {{custom_details_objectType_s}}", + "alertDescriptionFormat": "{{summary_s}}" + }, + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "reopenClosedIncident": false, + "groupByCustomDetails": [ + "ObjectId" + ], + "matchingMethod": "Selected", + "enabled": true, + "lookbackDuration": "P7D" + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject3').analyticRuleId3,'/'))))]", + "properties": { + "description": "RubrikSecurityCloud Analytics Rule 3", + "parentId": "[variables('analyticRuleObject3').analyticRuleId3]", + "contentId": "[variables('analyticRuleObject3')._analyticRulecontentId3]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject3').analyticRuleVersion3]", + "source": { + "kind": "Solution", + "name": "RubrikSecurityCloud", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "Ben Meadowcroft", + "email": "[variables('_email')]" + }, + "support": { + "name": "Rubrik", + "email": "support@rubrik.com", + "tier": "Partner", + "link": "https://support.rubrik.com" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject3')._analyticRulecontentId3]", + "contentKind": "AnalyticsRule", + "displayName": "Create Incident for Classification events for non DSPM license users", + "contentProductId": "[variables('analyticRuleObject3')._analyticRulecontentProductId3]", + "id": "[variables('analyticRuleObject3')._analyticRulecontentProductId3]", + "version": "[variables('analyticRuleObject3').analyticRuleVersion3]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject4').analyticRuleTemplateSpecName4]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "RubrikDataSecurityViolation_AnalyticalRules Analytics Rule with template version 3.5.5", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject4').analyticRuleVersion4]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject4')._analyticRulecontentId4]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Creates an incident for Data Security Violation events for DSPM license users.", + "displayName": "Create Incident for Data Security Violation events for DSPM license users", + "enabled": false, + "query": "Rubrik_Violation_Data_CL\n| where class_s == \"SecurityViolation\" and custom_details_eventName_s in (\"CriticalSeverityDataViolationDetected\",\n\"CriticalSeverityDataViolationReOpen\",\n\"HighSeverityDataViolationDetected\",\n\"HighSeverityDataViolationReOpen\",\n\"LowOrMediumSeverityDataViolationDetected\",\n\"LowOrMediumSeverityDataViolationReOpen\")\n| extend NewSeverity = case(severity_s==\"info\", \"Low\",\n severity_s==\"warning\", \"Medium\",\n severity_s==\"critical\", \"High\",\n \"Informational\"),\n ObjectId = iff(isnotempty(custom_details_objectId_s), custom_details_objectId_s, tostring(custom_details_objectId_g)),\n ViolationId = iff(isnotempty(custom_details_seriesId_s), custom_details_seriesId_s, tostring(custom_details_seriesId_g))\n", + "queryFrequency": "PT5M", + "queryPeriod": "PT5M", + "severity": "Medium", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "RubrikViolationData" + ], + "connectorId": "RubrikSecurityCloudAzureFunctions" + } + ], + "tactics": [ + "Exfiltration" + ], + "techniques": [ + "T1567" + ], + "eventGroupingSettings": { + "aggregationKind": "AlertPerResult" + }, + "customDetails": { + "ObjectName": "custom_details_objectName_s", + "EventName": "custom_details_eventName_s", + "ObjectId": "ObjectId", + "ObjectType": "custom_details_objectType_s", + "Severity": "severity_s", + "EventType": "class_s", + "ViolationId": "ViolationId", + "Summary": "summary_s", + "EventTimestamp": "timestamp_t" + }, + "alertDetailsOverride": { + "alertSeverityColumnName": "NewSeverity", + "alertDisplayNameFormat": "{{class_s}} event occurred for object {{custom_details_objectName_s}} with type {{custom_details_objectType_s}}", + "alertDescriptionFormat": "{{summary_s}}" + }, + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "reopenClosedIncident": false, + "groupByCustomDetails": [ + "ViolationId" + ], + "matchingMethod": "Selected", + "enabled": true, + "lookbackDuration": "P7D" + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject4').analyticRuleId4,'/'))))]", + "properties": { + "description": "RubrikSecurityCloud Analytics Rule 4", + "parentId": "[variables('analyticRuleObject4').analyticRuleId4]", + "contentId": "[variables('analyticRuleObject4')._analyticRulecontentId4]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject4').analyticRuleVersion4]", + "source": { + "kind": "Solution", + "name": "RubrikSecurityCloud", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "Ben Meadowcroft", + "email": "[variables('_email')]" + }, + "support": { + "name": "Rubrik", + "email": "support@rubrik.com", + "tier": "Partner", + "link": "https://support.rubrik.com" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject4')._analyticRulecontentId4]", + "contentKind": "AnalyticsRule", + "displayName": "Create Incident for Data Security Violation events for DSPM license users", + "contentProductId": "[variables('analyticRuleObject4')._analyticRulecontentProductId4]", + "id": "[variables('analyticRuleObject4')._analyticRulecontentProductId4]", + "version": "[variables('analyticRuleObject4').analyticRuleVersion4]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('analyticRuleObject5').analyticRuleTemplateSpecName5]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "RubrikIdentitySecurityViolation_AnalyticalRules Analytics Rule with template version 3.5.5", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('analyticRuleObject5').analyticRuleVersion5]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.SecurityInsights/AlertRuleTemplates", + "name": "[variables('analyticRuleObject5')._analyticRulecontentId5]", + "apiVersion": "2023-02-01-preview", + "kind": "Scheduled", + "location": "[parameters('workspace-location')]", + "properties": { + "description": "Creates an incident for Identity Security Violation and Identity Alerts events for IR violations.", + "displayName": "Create Incident for Identity Security Violation and Identity Alerts events for IR violations", + "enabled": false, + "query": "Rubrik_Violation_Data_CL\n| where (class_s == \"IdentityViolation\" and custom_details_eventName_s in (\"CriticalSeverityDataViolationDetected\",\n\"CriticalSeverityDataViolationReOpen\", \"HighSeverityDataViolationDetected\",\n\"HighSeverityDataViolationReOpen\", \"LowOrMediumSeverityDataViolationDetected\",\n\"LowOrMediumSeverityDataViolationReOpen\")) or\n(class_s == \"IdentityAlerts\" and custom_details_eventName_s in (\"CriticalSeverityIdentityAlertDetected\",\n\"CriticalSeverityIdentityAlertReOpened\", \"HighSeverityIdentityAlertDetected\",\n\"HighSeverityIdentityAlertReOpened\", \"LowOrMediumSeverityIdentityAlertDetected\",\n\"LowOrMediumSeverityIdentityAlertReOpened\"\n))\n| extend NewSeverity = case(severity_s==\"info\", \"Low\",\n severity_s==\"warning\", \"Medium\",\n severity_s==\"critical\", \"High\",\n \"Informational\"),\n ObjectId = iff(isnotempty(custom_details_objectId_s), custom_details_objectId_s, tostring(custom_details_objectId_g)),\n ViolationId = iff(isnotempty(custom_details_seriesId_s), custom_details_seriesId_s, tostring(custom_details_seriesId_g))\n", + "queryFrequency": "PT5M", + "queryPeriod": "PT5M", + "severity": "Medium", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "dataTypes": [ + "RubrikViolationData" + ], + "connectorId": "RubrikSecurityCloudAzureFunctions" + } + ], + "tactics": [ + "InitialAccess" + ], + "techniques": [ + "T1078" + ], + "eventGroupingSettings": { + "aggregationKind": "AlertPerResult" + }, + "customDetails": { + "ObjectName": "custom_details_objectName_s", + "EventName": "custom_details_eventName_s", + "ObjectId": "ObjectId", + "ObjectType": "custom_details_objectType_s", + "Severity": "severity_s", + "EventType": "class_s", + "ViolationId": "ViolationId", + "Summary": "summary_s", + "EventTimestamp": "timestamp_t" + }, + "alertDetailsOverride": { + "alertSeverityColumnName": "NewSeverity", + "alertDisplayNameFormat": "{{class_s}} event occurred for object {{custom_details_objectName_s}} with type {{custom_details_objectType_s}}", + "alertDescriptionFormat": "{{summary_s}}" + }, + "incidentConfiguration": { + "createIncident": true, + "groupingConfiguration": { + "reopenClosedIncident": false, + "groupByCustomDetails": [ + "ViolationId" + ], + "matchingMethod": "Selected", + "enabled": true, + "lookbackDuration": "P7D" + } + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject5').analyticRuleId5,'/'))))]", + "properties": { + "description": "RubrikSecurityCloud Analytics Rule 5", + "parentId": "[variables('analyticRuleObject5').analyticRuleId5]", + "contentId": "[variables('analyticRuleObject5')._analyticRulecontentId5]", + "kind": "AnalyticsRule", + "version": "[variables('analyticRuleObject5').analyticRuleVersion5]", + "source": { + "kind": "Solution", + "name": "RubrikSecurityCloud", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "Ben Meadowcroft", + "email": "[variables('_email')]" + }, + "support": { + "name": "Rubrik", + "email": "support@rubrik.com", + "tier": "Partner", + "link": "https://support.rubrik.com" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('analyticRuleObject5')._analyticRulecontentId5]", + "contentKind": "AnalyticsRule", + "displayName": "Create Incident for Identity Security Violation and Identity Alerts events for IR violations", + "contentProductId": "[variables('analyticRuleObject5')._analyticRulecontentProductId5]", + "id": "[variables('analyticRuleObject5')._analyticRulecontentProductId5]", + "version": "[variables('analyticRuleObject5').analyticRuleVersion5]" + } + }, { "type": "Microsoft.OperationalInsights/workspaces/providers/contentPackages", "apiVersion": "2023-04-01-preview", "location": "[parameters('workspace-location')]", "properties": { - "version": "3.5.4", + "version": "3.5.5", "kind": "Solution", "contentSchemaVersion": "3.0.0", "displayName": "RubrikSecurityCloud", "publisherDisplayName": "Rubrik", - "descriptionHtml": "

Note: Please refer to the following before installing the solution:

\n

• Review the solution Release Notes

\n

• There may be known issues pertaining to this Solution, please refer to them before installing.

\n

The Rubrik Security Cloud solution enables security operations teams to integrate insights from Rubrik’s Data Observability services into Microsoft Sentinel.

\n

Underlying Microsoft Technologies used:

\n

This solution takes a dependency on the following technologies, and some of these dependencies either may be in Preview state or might result in additional ingestion or operational costs:

\n
    \n
  1. Azure Monitor HTTP Data Collector API

    \n
  2. \n
  3. Azure Functions

    \n
  4. \n
\n

Data Connectors: 3, Analytic Rules: 2, Custom Azure Logic Apps Connectors: 1, Playbooks: 17

\n

Learn more about Microsoft Sentinel | Learn more about Solutions

\n", + "descriptionHtml": "

Note: Please refer to the following before installing the solution:

\n

• Review the solution Release Notes

\n

• There may be known issues pertaining to this Solution, please refer to them before installing.

\n

The Rubrik Security Cloud solution enables security operations teams to integrate insights from Rubrik’s Data Observability services into Microsoft Sentinel.

\n

Underlying Microsoft Technologies used:

\n

This solution takes a dependency on the following technologies, and some of these dependencies either may be in Preview state or might result in additional ingestion or operational costs:

\n
    \n
  1. Azure Monitor HTTP Data Collector API

    \n
  2. \n
  3. Azure Functions

    \n
  4. \n
\n

Data Connectors: 3, Analytic Rules: 5, Custom Azure Logic Apps Connectors: 1, Playbooks: 20

\n

Learn more about Microsoft Sentinel | Learn more about Solutions

\n", "contentKind": "Solution", "contentProductId": "[variables('_solutioncontentProductId')]", "id": "[variables('_solutioncontentProductId')]", @@ -36903,6 +45204,21 @@ "contentId": "[variables('_RubrikTurboThreatHunt')]", "version": "[variables('playbookVersion18')]" }, + { + "kind": "Playbook", + "contentId": "[variables('_RubrikDSPMViolationRemediation')]", + "version": "[variables('playbookVersion19')]" + }, + { + "kind": "Playbook", + "contentId": "[variables('_RubrikIRViolation')]", + "version": "[variables('playbookVersion20')]" + }, + { + "kind": "Playbook", + "contentId": "[variables('_RubrikQuarantineFiles')]", + "version": "[variables('playbookVersion21')]" + }, { "kind": "DataConnector", "contentId": "[variables('_dataConnectorContentId1')]", @@ -36927,11 +45243,26 @@ "kind": "AnalyticsRule", "contentId": "[variables('analyticRuleObject2')._analyticRulecontentId2]", "version": "[variables('analyticRuleObject2').analyticRuleVersion2]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject3')._analyticRulecontentId3]", + "version": "[variables('analyticRuleObject3').analyticRuleVersion3]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject4')._analyticRulecontentId4]", + "version": "[variables('analyticRuleObject4').analyticRuleVersion4]" + }, + { + "kind": "AnalyticsRule", + "contentId": "[variables('analyticRuleObject5')._analyticRulecontentId5]", + "version": "[variables('analyticRuleObject5').analyticRuleVersion5]" } ] }, "firstPublishDate": "2022-07-19", - "lastPublishDate": "2026-09-03", + "lastPublishDate": "2026-10-12", "providers": [ "Rubrik" ], diff --git a/Solutions/RubrikSecurityCloud/Playbooks/RubrikDSPMViolationRemediation/README.md b/Solutions/RubrikSecurityCloud/Playbooks/RubrikDSPMViolationRemediation/README.md new file mode 100644 index 00000000000..6bd67c6cbc1 --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Playbooks/RubrikDSPMViolationRemediation/README.md @@ -0,0 +1,144 @@ +# Rubrik DSPM Violation Remediation + +## Summary + +This is an incident-triggered playbook that remediates Rubrik Security Cloud (RSC) DSPM violations raised as Microsoft Sentinel incidents. It branches on the incident's `eventType`. For a `SecurityViolation` it retrieves the violation, validates it against the incident, marks it `IN_PROGRESS`, exports the violation-files CSV and the remediation-actions log (create -> poll the Download Bar -> download the bytes -> upload to a blob container -> generate a read-only SAS link), lists the violation file hits, quarantines each file by calling the RubrikQuarantineFiles sub-playbook, notifies the owner in Microsoft Teams, and marks the violation `REMEDIATED` only when every quarantine succeeded. For `ClassificationResultsAvailable` (no DSPM license) it runs a reduced flow: resolve the snappable identity -> list file hits -> quarantine -> notify. Both flows write a final comment back to the Sentinel incident. + +Template version: **1.6.0** (`hidden-SentinelTemplateVersion`). + +### Flow detail + +**Initialize (both flows)** + +* The newest alert on the incident is selected and its Custom Details parsed into variables: `eventType`, `objectId`, `objectName`, `objectType`, `seriesId` (-> violation id), `eventName`, `snapshotId`. +* The RSC service-account client id and secret are read from Key Vault and exchanged for a bearer token through the Rubrik custom connector. An authentication failure comments on the incident and terminates. +* `eventType` routes the run: `SecurityViolation` -> full flow, `Classification` -> reduced flow, anything else -> an "unknown event type" comment on the incident. + +**SecurityViolation (full) flow** + +1. **Get the violation** — `policyViolation` with `policyTypes: [POLICY_TYPE_DATAGOV]`. Supplies `status`, `violationSeverity`, `resourceId`, `details.snapshotId`, `policy.name` and from `resourceMetadata.metadata` the `platform` and `physicalHost`. +2. **Validate the identifiers** — `objectName`, `objectId` and `snapshotId` taken from the incident are compared against `resourceMetadata.metadata.name`, `resourceId` and `details.snapshotId` returned by RSC. On a mismatch the playbook comments and terminates; it never remediates against an object the incident did not describe. +3. **Resolve the snappable identity** — see below. +4. **Mark `IN_PROGRESS`** before any remediation work starts. A failure here comments and terminates. +5. **Export the violation-files CSV** — `downloadSnapshotResultsCsv`, then poll the Download Bar, download the bytes, upload to blob, create a read-only SAS link. +6. **Export the remediation-actions log** — `createViolationRemediation` with `REMEDIATION_TYPE_EXPORT_ACTIONS_LOG_TO_CSV`, then the same poll/download/upload/SAS sequence. +7. **List the file hits** — paginated 25 at a time, with `fileType: HITS` and `whitelistEnabled: true`. +8. **Quarantine** — the whole files array goes to RubrikQuarantineFiles in a single call. +9. **Notify and close out** — post the owner card to Teams, comment on the incident with the SAS links and per-file results, and set `REMEDIATED` only if every file was quarantined. + +**ClassificationResultsAvailable (reduced) flow** + +Used when the customer has no DSPM license. There is no violation object, so there is no `policyViolationId`; the `snapshotId` arrives on the incident as a Custom Detail. The flow resolves the snappable identity, lists the file hits (`filters.violationId` omitted), quarantines, notifies and comments. It does **not** export the CSV or log, and does **not** update any violation status. + +### Where `siteUrl` / `driveOwnerUpn` come from + +Both flows call the RSC `SnappableIdentity` query **once per violation**, not per file: + +```graphql +query SnappableIdentity($fid: UUID!) { + hierarchyObject(fid: $fid) { + id name objectType + ... on O365Site { title url __typename } + ... on O365Onedrive { userPrincipalName userName userID __typename } + __typename + } +} +``` + +Only one inline fragment is populated, depending on the object type: `O365Site.url` -> `siteUrl`, or `O365Onedrive.userPrincipalName` -> `driveOwnerUpn`. The resolved values are then stamped onto **every** file record sent to the sub-playbook, which uses them to address each file's exact drive instead of searching the tenant. + +A failure of this query is **non-fatal** — the run continues, and the sub-playbook reports the affected O365 files as `Failed` rather than acting on the wrong file. + +### Export polling + +Each export is matched in the Download Bar on three conditions together: the download `type` (`SNAPSHOT_RESULTS_CSV` for the files export), a `filename` containing the object's name pattern, and a `createdAt` at or after the export start time stamped immediately before the export was triggered. This prevents an older export of the same object being picked up. If several downloads match, the newest is used and the ambiguity is recorded in the run's error messages. + +Polling runs every `DownloadPollIntervalSec` seconds for at most `DownloadPollMaxAttempts` attempts. An export that fails or never becomes ready is **non-fatal**: the incident comment states that the export was unavailable and the rest of the flow proceeds. + +Uploaded exports are written to `/{StorageContainerName}/dspm/{incidentName}/{violationId}/` and shared as read-only SAS links valid for `SasTokenTtlHours`. + +### Prerequisites + +1. The Rubrik Security Cloud data connector should be configured to send appropriate events to Microsoft Sentinel. +2. The Rubrik Security Cloud solution should be configured to [connect to Rubrik Security Cloud API end points using a Service Account](https://docs.rubrik.com/en-us/saas/saas/polaris_api_access_with_service_accounts.html); the service account should be assigned a role that includes the privileges necessary to read and update policy violations (see [Roles and Permissions](https://docs.rubrik.com/en-us/saas/saas/common/roles_and_permissions.html) in the Rubrik Security Cloud user guide). +3. Deploy the Rubrik custom connector (`Microsoft.Web/customApis`) first, in the same resource group and region. This playbook uses its `Authentication` (`/api/client_token`) operation to obtain the RSC bearer token. +4. Deploy the RubrikQuarantineFiles sub-playbook (HTTP Request-triggered) before this playbook; it is called once per incident with the whole files array and returns `{ status, results: [ { filename, status, newLocation, error } ], error }`. +5. **Version pairing (important):** this playbook sends `siteUrl` and `driveOwnerUpn` on every file record, taken from the RSC snappable identity, so that RubrikQuarantineFiles can resolve each O365 file by its exact drive and path instead of by a tenant-wide search. It must be deployed together with **RubrikQuarantineFiles v3.1.0 or later** (current: v3.2.0). An older sub-playbook ignores the two new fields (harmless); a v3.1.0+ sub-playbook paired with an older parent reports every O365 file as `Failed` with "No drive owner UPN or site URL was supplied", by design, rather than silently acting on the wrong file. +6. Store Service account credentials in Key Vault and obtain the keyvault name. + a. Create a Key Vault with a unique name. + b. Go to KeyVault -> secrets -> Generate/import and create 'Rubrik-AS-Int-ClientId' & 'Rubrik-AS-Int-ClientSecret' for storing client_id and client_secret respectively. +7. An Azure Storage account and an EXISTING blob container for the exported CSV/log files (SecurityViolation flow only). +8. Obtain the Teams group id and channel id where the owner-notification card will be posted. +9. An analytics rule that maps the Rubrik event fields into incident Custom Details (eventType, objectId, objectName, objectType, seriesId, eventName, snapshotId). + +### Deployment instructions + +1. To deploy the Playbook, click the Deploy to Azure button. This will launch the ARM Template deployment wizard. +2. Fill in the required parameters: + * PlaybookName: Enter the playbook name here. + * API Hostname: Hostname of the Rubrik Security Cloud instance (e.g. customer.my.rubrik.com), without scheme or trailing slash. + * Rubrik Connector name: Name of the Rubrik Custom Connector deployed previously. + * KeyVaultName: Name of the keyvault where the RSC service-account secrets ('Rubrik-AS-Int-ClientId' and 'Rubrik-AS-Int-ClientSecret') are stored. + * RubrikQuarantineFilesPlaybookName: Logic App name of the RubrikQuarantineFiles sub-playbook. It must exist in the same resource group and expose an HTTP Request trigger. + * StorageAccountName: Azure Storage account name used to persist the exported CSV/log. + * StorageContainerName: Name of an EXISTING blob container in StorageAccountName where the violation-files export is written. + * SasTokenTtlHours: Validity window (hours) for the read-only SAS link added to the incident comment. Default 72. + * TeamsGroupId: Id of the Teams Group where the owner-notification card is posted. + * TeamsChannelId: Id of the Teams Channel where the owner-notification card is posted. + * DownloadPollIntervalSec: Seconds to wait between Download Bar polls. Default 15. + * DownloadPollMaxAttempts: Maximum Download Bar poll attempts before giving up on an export. Default 20. + +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2FAzure-Sentinel%2Fmaster%2FSolutions%2FRubrikSecurityCloud%2FPlaybooks%2FRubrikDSPMViolationRemediation%2Fazuredeploy.json) [![Deploy to Azure](https://aka.ms/deploytoazuregovbutton)](https://portal.azure.us/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2FAzure-Sentinel%2Fmaster%2FSolutions%2FRubrikSecurityCloud%2FPlaybooks%2FRubrikDSPMViolationRemediation%2Fazuredeploy.json) + +### Post-Deployment instructions + +#### a. Authorize connections + +Once deployment is complete, authorize each connection: Rubrik custom connector, Microsoft Teams, Key Vault, Azure Blob Storage and Microsoft Sentinel. + +1. Click the connection resource +2. Click edit API connection +3. Click Authorize +4. Sign in +5. Click Save +6. Repeat steps for other connections + +The Azure Blob Storage connection's account must have write access to StorageContainerName; it is used to upload the CSV/log and to create the read-only SAS link. + +#### b. Assign roles to the playbook + +Assign roles to this playbook's managed identity. + +1. Go to Log Analytics Workspace → → Access Control → Add +2. Add role assignment +3. Assignment type: Job function roles -> Add 'Microsoft Sentinel Responder' as a Role (to comment on and update incidents) +4. Members: select managed identity for assigned access to and add your logic app as member +5. Click on review+assign +6. Grant the playbook's managed identity the 'Key Vault Secrets User' role (or a get-secret access policy) on the Key Vault named by the KeyVaultName parameter. + +#### c. Wire up the sub-playbook + +This playbook calls the RubrikQuarantineFiles sub-playbook once per incident, passing the whole files array. Deploy that sub-playbook first (same resource group) and set RubrikQuarantineFilesPlaybookName to its Logic App name; it must expose an HTTP Request trigger returning `{ status, results: [ ... ], error }`. + +Each file record sent to the sub-playbook carries `filename`, `filePath` (RSC `nativePath`), `nativePath`, `directory`, `ownerUpn` (`createdBy`, falling back to `modifiedBy`), `objectType`, `physicalHost` (NTFS) and `siteUrl` / `driveOwnerUpn` from the violation's RSC snappable identity. The last two let the sub-playbook address the file's drive directly; see the version-pairing note in the prerequisites. + +#### d. Configurations in Microsoft Sentinel + +1. In Microsoft Sentinel, analytical rules should be configured to trigger an incident. The incident should carry the DSPM Custom Details (eventType, objectId, objectName, objectType, seriesId, eventName, snapshotId) obtained from the corresponding fields in Rubrik DSPM event logs. Check the [documentation](https://docs.microsoft.com/azure/sentinel/surface-custom-details-in-alerts) to learn more about adding custom details to incidents. +2. Configure the automation rules to trigger this playbook on the relevant DSPM incidents (playbooks do not auto-attach). + +### Error handling + +Every RSC call checks both the HTTP status code and the presence of a non-empty `errors` key, because RSC returns GraphQL errors with HTTP 200. The checks run after `Succeeded`, `Failed` and `TimedOut` so a transport failure is handled rather than silently ending the run. + +| Failure point | Behaviour | +|---------------|-----------| +| Authentication | Comment on incident, terminate | +| Get DSPM Violation | Comment on incident, terminate | +| Identifier validation mismatch | Comment on incident, terminate — no remediation is attempted | +| Set status `IN_PROGRESS` | Comment on incident, terminate | +| Get Snappable Identity | Non-fatal: record the error and continue; affected O365 files report `Failed` in the sub-playbook | +| CSV / remediation-log export or poll | Non-fatal: record the error; the incident comment states the export was unavailable | +| List file hits | Comment on incident, terminate | +| Quarantine partially failed | Violation is left `IN_PROGRESS`; per-file failures are listed in the incident comment | +| No file hits at all | Violation is marked `REMEDIATED`; the notification and comment record that no files required quarantine | diff --git a/Solutions/RubrikSecurityCloud/Playbooks/RubrikDSPMViolationRemediation/azuredeploy.json b/Solutions/RubrikSecurityCloud/Playbooks/RubrikDSPMViolationRemediation/azuredeploy.json new file mode 100644 index 00000000000..e1f7943cd88 --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Playbooks/RubrikDSPMViolationRemediation/azuredeploy.json @@ -0,0 +1,3858 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "title": "Rubrik DSPM Violation Remediation", + "description": "Incident-triggered playbook that remediates Rubrik Security Cloud (RSC) DSPM violations. Branches on eventType: for SecurityViolation it retrieves the violation, exports the violation-files CSV and the remediation-actions log (create -> poll Download Bar -> download bytes -> upload to a blob container -> generate SAS link), lists the violation file hits, quarantines each file via the RubrikQuarantineFiles sub-playbook, notifies the file owner in Teams, and marks the violation REMEDIATED only when every quarantine succeeded. For ClassificationResultsAvailable (no DSPM license) it runs a reduced flow: list file hits (objectId + snapshotId) -> quarantine -> notify. Both flows write a final comment to the Microsoft Sentinel incident.", + "prerequisites": [ + "1. The Rubrik Security Cloud solution should be configured to connect to Rubrik Security Cloud API end points using a Service Account with a role that includes the privileges to read and update policy violations.", + "2. The Rubrik custom connector (RubrikCustomConnector) needs to be deployed prior to the deployment of this playbook, in the same resource group and region. This playbook uses its 'Authentication' (/api/client_token) operation to obtain the RSC bearer token.", + "3. The RubrikQuarantineFiles sub-playbook (HTTP Request-triggered) must be deployed before this playbook; it is called once per incident with the full list of files to quarantine (an objectType plus a files array) and returns { status, results, error }, where results holds one entry per file.", + "4. Store Service account credentials in Key Vault and obtain the Key Vault name and tenantId.", + "a. Create a Key Vault with a unique name.", + "b. Go to KeyVault -> secrets, click Generate/import and create 'Rubrik-AS-Int-ClientId' & 'Rubrik-AS-Int-ClientSecret' for storing client_id and client_secret respectively.", + "5. An Azure Storage account and an EXISTING blob container are required for the exported CSV/log files (SecurityViolation flow only).", + "6. A Microsoft Teams team and channel are required for owner notifications.", + "7. Configure an analytics rule that maps the Rubrik event fields into incident Custom Details (eventType, objectId, objectName, objectType, seriesId, eventName, snapshotId)." + ], + "postDeployment": [ + "**a. Authorize connections**", + "Once deployment is complete, authorize each connection.", + "1. Go to your logic app -> API connections -> Select the connection resource (RubrikCustomConnector, Microsoft Teams, and Azure Blob Storage).", + "2. Go to General -> edit API connection.", + "3. Click Authorize.", + "4. Sign in.", + "5. Click Save.", + "6. Repeat steps for each connection. The Azure Blob Storage account used must have write access to StorageContainerName (used to upload the CSV/log and create the read-only SAS link).", + "**b. Assign Role to comment on and update incidents**", + "1. Go to Log Analytics Workspace -> -> Access Control -> Add.", + "2. Add role assignment.", + "3. Assignment type: Job function roles.", + "4. Role: Microsoft Sentinel Responder.", + "5. Members: select managed identity for assigned access to and add your logic app as member.", + "6. Click on review + assign.", + "**c. Add Access policy in Key Vault**", + "Add an access policy for the playbook's managed identity to read the RSC service-account secrets.", + "1. Go to logic app -> -> Identity -> System assigned managed identity and copy the Object (principal) ID.", + "2. Go to keyvaults -> -> Access policies -> Create.", + "3. Select Get on secrets permissions. Click next.", + "4. In the principal section, search by the copied object ID. Click next.", + "5. Click review + create.", + "**d. Configurations in Microsoft Sentinel**", + "1. Deploy the RubrikQuarantineFiles sub-playbook first and set RubrikQuarantineFilesPlaybookName to its Logic App name; it must exist in the same resource group and expose an HTTP Request trigger that accepts { objectType, files } and returns { status, results, error }.", + "2. In Microsoft Sentinel, configure the automation rules to trigger this playbook on the relevant DSPM incidents (playbooks do not auto-attach)." + ], + "prerequisitesDeployTemplateFile": "Azure-Sentinel/Solutions/RubrikSecurityCloud/Playbooks/RubrikCustomConnector/Rubrik_custom_conn.json", + "lastUpdateTime": "2026-10-04T00:00:00.000Z", + "entities": [], + "tags": [ + "Microsoft Sentinel", + "Incident", + "Rubrik", + "DSPM", + "Remediation", + "Quarantine" + ], + "support": { + "tier": "community", + "armtemplate": "Generated from https://github.com/Azure/Azure-Sentinel/tree/master/Tools/Playbook-ARM-Template-Generator" + }, + "author": { + "name": "Rubrik" + } + }, + "parameters": { + "PlaybookName": { + "defaultValue": "RubrikDSPMViolationRemediation", + "type": "string", + "metadata": { + "description": "Name of the Logic App / playbook resource." + } + }, + "API Hostname": { + "type": "string", + "metadata": { + "description": "Hostname of the Rubrik Security Cloud instance (e.g. customer.my.rubrik.com)." + } + }, + "Rubrik Connector name": { + "defaultValue": "RubrikCustomConnector", + "type": "string", + "metadata": { + "description": "Name of the previously deployed Rubrik custom connector (Microsoft.Web/customApis)." + } + }, + "KeyVaultName": { + "type": "string", + "metadata": { + "description": "Key Vault holding the RSC service-account secrets 'Rubrik-AS-Int-ClientId' and 'Rubrik-AS-Int-ClientSecret'." + } + }, + "RubrikQuarantineFilesPlaybookName": { + "defaultValue": "RubrikQuarantineFiles", + "type": "string", + "metadata": { + "description": "Logic App name of the RubrikQuarantineFiles sub-playbook (HTTP Request-triggered) in this resource group. Called once per file to quarantine." + } + }, + "StorageAccountName": { + "type": "string", + "metadata": { + "description": "Azure Storage account name used to persist the exported CSV/log (SecurityViolation flow only)." + } + }, + "StorageContainerName": { + "type": "string", + "metadata": { + "description": "Name of an EXISTING blob container in StorageAccountName where the violation-files CSV and remediation log are uploaded. The container must be created beforehand; the playbook does not create it at runtime." + } + }, + "SasTokenTtlHours": { + "defaultValue": 72, + "type": "int", + "metadata": { + "description": "Validity window (hours) for the read-only SAS link added to the incident comment." + } + }, + "TeamsGroupId": { + "type": "string", + "metadata": { + "description": "Microsoft Teams team (group) ID for the owner-notification card." + } + }, + "TeamsChannelId": { + "type": "string", + "metadata": { + "description": "Microsoft Teams channel ID (within TeamsGroupId) for the owner-notification card." + } + }, + "DownloadPollIntervalSec": { + "defaultValue": 15, + "type": "int", + "metadata": { + "description": "Seconds to wait between Download Bar polls." + } + }, + "DownloadPollMaxAttempts": { + "defaultValue": 20, + "type": "int", + "metadata": { + "description": "Maximum Download Bar poll attempts before giving up on an export." + } + } + }, + "variables": { + "MicrosoftSentinelConnectionName": "[concat('azuresentinel-', parameters('PlaybookName'))]", + "RubrikcustomconnectorConnectionName": "[concat('Rubrikcustomconnector-', parameters('PlaybookName'))]", + "KeyvaultConnectionName": "[concat('keyvault-', parameters('PlaybookName'))]", + "TeamsConnectionName": "[concat('teams-', parameters('PlaybookName'))]", + "AzureblobConnectionName": "[concat('azureblob-', parameters('PlaybookName'))]" + }, + "resources": [ + { + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[variables('MicrosoftSentinelConnectionName')]", + "location": "[resourceGroup().location]", + "kind": "V1", + "properties": { + "displayName": "[variables('MicrosoftSentinelConnectionName')]", + "customParameterValues": {}, + "parameterValueType": "Alternative", + "api": { + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/azuresentinel')]" + } + } + }, + { + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[variables('RubrikcustomconnectorConnectionName')]", + "location": "[resourceGroup().location]", + "properties": { + "displayName": "[variables('RubrikcustomconnectorConnectionName')]", + "customParameterValues": {}, + "api": { + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', parameters('Rubrik Connector name'))]" + } + } + }, + { + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[variables('KeyvaultConnectionName')]", + "location": "[resourceGroup().location]", + "kind": "V1", + "properties": { + "displayName": "[variables('KeyvaultConnectionName')]", + "customParameterValues": {}, + "parameterValueType": "Alternative", + "alternativeParameterValues": { + "vaultName": "[parameters('KeyVaultName')]" + }, + "api": { + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/keyvault')]" + } + } + }, + { + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[variables('TeamsConnectionName')]", + "location": "[resourceGroup().location]", + "kind": "V1", + "properties": { + "displayName": "[variables('TeamsConnectionName')]", + "customParameterValues": {}, + "api": { + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/teams')]" + } + } + }, + { + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[variables('AzureblobConnectionName')]", + "location": "[resourceGroup().location]", + "kind": "V1", + "properties": { + "displayName": "[variables('AzureblobConnectionName')]", + "customParameterValues": {}, + "api": { + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/azureblob')]" + } + } + }, + { + "type": "Microsoft.Logic/workflows", + "apiVersion": "2017-07-01", + "name": "[parameters('PlaybookName')]", + "location": "[resourceGroup().location]", + "tags": { + "LogicAppsCategory": "security", + "hidden-SentinelTemplateName": "RubrikViolationRemediation", + "hidden-SentinelTemplateVersion": "1.6.0" + }, + "identity": { + "type": "SystemAssigned" + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/connections', variables('MicrosoftSentinelConnectionName'))]", + "[resourceId('Microsoft.Web/connections', variables('RubrikcustomconnectorConnectionName'))]", + "[resourceId('Microsoft.Web/connections', variables('KeyvaultConnectionName'))]", + "[resourceId('Microsoft.Web/connections', variables('TeamsConnectionName'))]", + "[resourceId('Microsoft.Web/connections', variables('AzureblobConnectionName'))]" + ], + "properties": { + "state": "Enabled", + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "$connections": { + "defaultValue": {}, + "type": "Object" + }, + "StorageContainerName": { + "defaultValue": "", + "type": "String" + }, + "SasTokenTtlHours": { + "defaultValue": 72, + "type": "Int" + } + }, + "triggers": { + "Microsoft_Sentinel_incident": { + "type": "ApiConnectionWebhook", + "inputs": { + "body": { + "callback_url": "@{listCallbackUrl()}" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "path": "/incident-creation" + } + } + }, + "actions": { + "Initialize_Base_URL": { + "runAfter": {}, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Base_URL", + "type": "string", + "value": "[concat('https://', parameters('API Hostname'))]" + } + ] + } + }, + "Initialize_User_Agent": { + "runAfter": { + "Initialize_Base_URL": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "User_Agent", + "type": "string", + "value": "MSFTSentinel-3.5.2" + } + ] + } + }, + "Initialize_Access_Token": { + "runAfter": { + "Initialize_User_Agent": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Access_Token", + "type": "string" + } + ] + } + }, + "Initialize_Event_Type": { + "runAfter": { + "Initialize_Access_Token": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Event_Type", + "type": "string" + } + ] + } + }, + "Initialize_Object_ID": { + "runAfter": { + "Initialize_Event_Type": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Object_ID", + "type": "string" + } + ] + } + }, + "Initialize_Object_Name": { + "runAfter": { + "Initialize_Object_ID": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Object_Name", + "type": "string" + } + ] + } + }, + "Initialize_Object_Type": { + "runAfter": { + "Initialize_Object_Name": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Object_Type", + "type": "string" + } + ] + } + }, + "Initialize_Violation_ID": { + "runAfter": { + "Initialize_Object_Type": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Violation_ID", + "type": "string" + } + ] + } + }, + "Initialize_Snapshot_Fid": { + "runAfter": { + "Initialize_Violation_ID": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Snapshot_Fid", + "type": "string" + } + ] + } + }, + "Initialize_Snappable_Fid": { + "runAfter": { + "Initialize_Snapshot_Fid": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Snappable_Fid", + "type": "string" + } + ] + } + }, + "Initialize_Event_Name": { + "runAfter": { + "Initialize_Snappable_Fid": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Event_Name", + "type": "string" + } + ] + } + }, + "Initialize_Platform": { + "runAfter": { + "Initialize_Event_Name": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Platform", + "type": "string" + } + ] + } + }, + "Initialize_Physical_Host": { + "runAfter": { + "Initialize_Platform": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Physical_Host", + "type": "string" + } + ] + } + }, + "Initialize_Policy_Name": { + "runAfter": { + "Initialize_Physical_Host": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Policy_Name", + "type": "string" + } + ] + } + }, + "Initialize_Owner_UPN": { + "runAfter": { + "Initialize_Policy_Name": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Owner_UPN", + "type": "string" + } + ] + } + }, + "Initialize_Files_To_Process": { + "runAfter": { + "Initialize_Owner_UPN": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Files_To_Process", + "type": "array", + "value": [] + } + ] + } + }, + "Initialize_Quarantine_Results": { + "runAfter": { + "Initialize_Files_To_Process": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Quarantine_Results", + "type": "array", + "value": [] + } + ] + } + }, + "Initialize_All_Quarantine_Succeeded": { + "runAfter": { + "Initialize_Quarantine_Results": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "All_Quarantine_Succeeded", + "type": "boolean", + "value": true + } + ] + } + }, + "Initialize_Files_CSV_Link": { + "runAfter": { + "Initialize_All_Quarantine_Succeeded": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Files_CSV_Link", + "type": "string" + } + ] + } + }, + "Initialize_Remediation_Log_Link": { + "runAfter": { + "Initialize_Files_CSV_Link": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Remediation_Log_Link", + "type": "string" + } + ] + } + }, + "Initialize_Next_Cursor": { + "runAfter": { + "Initialize_Remediation_Log_Link": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Next_Cursor", + "type": "string" + } + ] + } + }, + "Initialize_Has_Next_Page": { + "runAfter": { + "Initialize_Next_Cursor": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Has_Next_Page", + "type": "boolean", + "value": true + } + ] + } + }, + "Initialize_Download_External_Id": { + "runAfter": { + "Initialize_Has_Next_Page": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Download_External_Id", + "type": "string" + } + ] + } + }, + "Initialize_Download_Ready": { + "runAfter": { + "Initialize_Download_External_Id": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Download_Ready", + "type": "boolean", + "value": false + } + ] + } + }, + "Initialize_Error_Messages": { + "runAfter": { + "Initialize_Download_Ready": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Error_Messages", + "type": "array", + "value": [] + } + ] + } + }, + "Initialize_CSV_Export_Start": { + "runAfter": { + "Initialize_Error_Messages": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "CSV_Export_Start", + "type": "string" + } + ] + } + }, + "Initialize_Log_Export_Start": { + "runAfter": { + "Initialize_CSV_Export_Start": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Log_Export_Start", + "type": "string" + } + ] + } + }, + "Initialize_CSV_Downloads": { + "runAfter": { + "Initialize_Log_Export_Start": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "CSV_Downloads", + "type": "array", + "value": [] + } + ] + } + }, + "Initialize_File_Hits_Failed": { + "runAfter": { + "Initialize_CSV_Downloads": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "File_Hits_Failed", + "type": "boolean", + "value": false + } + ] + } + }, + "Initialize_CSV_Name_Pattern": { + "runAfter": { + "Initialize_File_Hits_Failed": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "CSV_Name_Pattern", + "type": "string" + } + ] + } + }, + "Initialize_Log_Name_Pattern": { + "runAfter": { + "Initialize_CSV_Name_Pattern": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Log_Name_Pattern", + "type": "string" + } + ] + } + }, + "Initialize_CSV_Match_Count": { + "runAfter": { + "Initialize_Log_Name_Pattern": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "CSV_Match_Count", + "type": "integer", + "value": 0 + } + ] + } + }, + "Initialize_Log_Match_Count": { + "runAfter": { + "Initialize_CSV_Match_Count": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Log_Match_Count", + "type": "integer", + "value": 0 + } + ] + } + }, + "Initialize_Provided_Object_Name": { + "runAfter": { + "Initialize_Log_Match_Count": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Provided_Object_Name", + "type": "string" + } + ] + } + }, + "Initialize_Provided_Snappable_Fid": { + "runAfter": { + "Initialize_Provided_Object_Name": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Provided_Snappable_Fid", + "type": "string" + } + ] + } + }, + "Initialize_Provided_Snapshot_Fid": { + "runAfter": { + "Initialize_Provided_Snappable_Fid": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Provided_Snapshot_Fid", + "type": "string" + } + ] + } + }, + "Get_Client_ID": { + "runAfter": { + "Initialize_Snappable_Owner_Name": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['keyvault']['connectionId']" + } + }, + "method": "get", + "path": "/secrets/@{encodeURIComponent('Rubrik-AS-Int-ClientId')}/value" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Get_Client_Secret": { + "runAfter": { + "Get_Client_ID": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['keyvault']['connectionId']" + } + }, + "method": "get", + "path": "/secrets/@{encodeURIComponent('Rubrik-AS-Int-ClientSecret')}/value" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Authentication": { + "runAfter": { + "Get_Client_Secret": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['RubrikCustomConnector']['connectionId']" + } + }, + "method": "post", + "body": { + "client_id": "@body('Get_Client_ID')?['value']", + "client_secret": "@body('Get_Client_Secret')?['value']" + }, + "headers": { + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "path": "/api/client_token" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Is_Authentication_Failed": { + "runAfter": { + "Authentication": [ + "Succeeded", + "Failed" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "not": { + "equals": [ + "@outputs('Authentication')['statusCode']", + 200 + ] + } + }, + { + "equals": [ + "@empty(body('Authentication')?['access_token'])", + true + ] + } + ] + }, + "actions": { + "Append_auth_failure": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Authentication (/api/client_token) failed with status @{outputs('Authentication')?['statusCode']}. Verify the Key Vault secrets and the Rubrik custom connector." + } + }, + "Comment_auth_failure": { + "runAfter": { + "Append_auth_failure": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Violation Remediation failed.

@{join(variables('Error_Messages'), '
')}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + }, + "Terminate_-_Authentication_failed": { + "runAfter": { + "Comment_auth_failure": [ + "Succeeded", + "Failed" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "AuthFailed", + "message": "@{join(variables('Error_Messages'), ' | ')}" + } + } + } + }, + "else": { + "actions": { + "Set_Access_Token": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Access_Token", + "value": "@{body('Authentication')?['access_token']}" + } + } + } + } + }, + "Select_alert_times": { + "type": "Select", + "runAfter": { + "Is_Authentication_Failed": [ + "Succeeded" + ] + }, + "inputs": { + "from": "@coalesce(triggerBody()?['object']?['properties']?['Alerts'], json('[]'))", + "select": "@ticks(coalesce(item()?['properties']?['timeGenerated'], '1970-01-01T00:00:00Z'))" + } + }, + "Filter_newest_alert": { + "type": "Query", + "runAfter": { + "Select_alert_times": [ + "Succeeded" + ] + }, + "inputs": { + "from": "@coalesce(triggerBody()?['object']?['properties']?['Alerts'], json('[]'))", + "where": "@equals(ticks(coalesce(item()?['properties']?['timeGenerated'], '1970-01-01T00:00:00Z')), max(body('Select_alert_times')))" + } + }, + "For_each_alert": { + "runAfter": { + "Filter_newest_alert": [ + "Succeeded" + ] + }, + "type": "Foreach", + "foreach": "@take(body('Filter_newest_alert'), 1)", + "runtimeConfiguration": { + "concurrency": { + "repetitions": 1 + } + }, + "actions": { + "Parse_JSON_for_alert": { + "runAfter": {}, + "type": "ParseJson", + "inputs": { + "content": "@items('For_each_alert')?['properties']?['additionalData']?['Custom Details']", + "schema": { + "type": "object", + "properties": { + "EventType": { + "type": "array", + "items": { + "type": "string" + } + }, + "ObjectID": { + "type": "array", + "items": { + "type": "string" + } + }, + "ObjectName": { + "type": "array", + "items": { + "type": "string" + } + }, + "ObjectType": { + "type": "array", + "items": { + "type": "string" + } + }, + "ViolationID": { + "type": "array", + "items": { + "type": "string" + } + }, + "EventName": { + "type": "array", + "items": { + "type": "string" + } + }, + "SnapshotID": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "Set_Event_Type": { + "runAfter": { + "Parse_JSON_for_alert": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Event_Type", + "value": "@{first(coalesce(body('Parse_JSON_for_alert')?['EventType'], json('[]')))}" + } + }, + "Set_Object_ID": { + "runAfter": { + "Set_Event_Type": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Object_ID", + "value": "@{first(coalesce(body('Parse_JSON_for_alert')?['ObjectID'], json('[]')))}" + } + }, + "Set_Object_Name": { + "runAfter": { + "Set_Object_ID": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Object_Name", + "value": "@{first(coalesce(body('Parse_JSON_for_alert')?['ObjectName'], json('[]')))}" + } + }, + "Set_Object_Type": { + "runAfter": { + "Set_Object_Name": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Object_Type", + "value": "@{first(coalesce(body('Parse_JSON_for_alert')?['ObjectType'], json('[]')))}" + } + }, + "Set_Violation_ID": { + "runAfter": { + "Set_Object_Type": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Violation_ID", + "value": "@{first(coalesce(body('Parse_JSON_for_alert')?['ViolationID'], json('[]')))}" + } + }, + "Set_Event_Name": { + "runAfter": { + "Set_Violation_ID": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Event_Name", + "value": "@{first(coalesce(body('Parse_JSON_for_alert')?['EventName'], json('[]')))}" + } + }, + "Set_Snapshot_Fid_from_incident": { + "runAfter": { + "Set_Event_Name": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Snapshot_Fid", + "value": "@{first(coalesce(body('Parse_JSON_for_alert')?['SnapshotID'], json('[]')))}" + } + }, + "Set_Snappable_Fid_default": { + "runAfter": { + "Set_Snapshot_Fid_from_incident": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Snappable_Fid", + "value": "@variables('Object_ID')" + } + } + } + }, + "Switch_on_event_type": { + "runAfter": { + "For_each_alert": [ + "Succeeded" + ] + }, + "type": "Switch", + "expression": "@variables('Event_Type')", + "cases": { + "SecurityViolation": { + "case": "SecurityViolation", + "actions": { + "Get_DSPM_Violation": { + "runAfter": {}, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "query DataSecurityViolationGetQuery($violationId: String!) {\n policyViolation(\n violationId: $violationId\n policyTypes: [POLICY_TYPE_DATAGOV]\n ) {\n ...DataAtRiskPanelFragment\n status\n violationSeverity\n policyViolationId\n createdAt\n lastUpdatedAt\n resourceId\n policy {\n policyId\n name\n description\n policyCategory\n policySeverity\n containsAccessFilters\n __typename\n }\n remediations {\n remediationId\n state\n remediationDetails {\n details {\n ticketNumber\n ticketUrl\n __typename\n }\n __typename\n }\n __typename\n }\n resourceMetadata {\n metadata {\n ... on CommonAssetMetadata {\n platform\n cloudAccountInfo {\n accountName\n __typename\n }\n objectType\n clusterInfo {\n clusterName\n clusterUuid\n __typename\n }\n creationTime\n lastAccessTime\n snapshotTimestamp\n physicalHost\n name\n isDeleted\n region\n __typename\n }\n }\n __typename\n }\n __typename\n }\n}\n\nfragment SensitiveHitsChartFragment on DataGovViolationDetails {\n snapshotId\n violatedSensitiveHits\n violatedNoRiskSensitiveHits\n violatedLowRiskSensitiveHits\n violatedMediumRiskSensitiveHits\n violatedHighRiskSensitiveHits\n __typename\n}\n\nfragment DataAtRiskPanelFragment on PolicyViolation {\n details {\n ...SensitiveHitsChartFragment\n ... on DataGovViolationDetails {\n dataCategories {\n id\n name\n totalViolatedHits\n __typename\n }\n dataTypes {\n id\n name\n totalViolatedHits\n __typename\n }\n mipLabels {\n id\n totalViolatedHits\n name\n __typename\n }\n documentTypes {\n id\n name\n totalViolatedHits\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}", + "variables": { + "violationId": "@variables('Violation_ID')" + } + } + } + }, + "Check_Get_DSPM_Violation": { + "runAfter": { + "Get_DSPM_Violation": [ + "Succeeded", + "Failed" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "not": { + "equals": [ + "@outputs('Get_DSPM_Violation')['statusCode']", + 200 + ] + } + }, + { + "not": { + "equals": [ + "@coalesce(body('Get_DSPM_Violation')?['errors'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Append_-_Get_violation_failed": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Get DSPM Violation failed for violation @{variables('Violation_ID')} (status @{outputs('Get_DSPM_Violation')?['statusCode']})." + } + }, + "Comment_-_Get_violation_failed": { + "runAfter": { + "Append_-_Get_violation_failed": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Violation Remediation failed.

@{join(variables('Error_Messages'), '
')}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + }, + "Terminate_-_Get_violation_failed": { + "runAfter": { + "Comment_-_Get_violation_failed": [ + "Succeeded", + "Failed" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "GetViolationFailed", + "message": "@{join(variables('Error_Messages'), ' | ')}" + } + } + } + }, + "else": { + "actions": { + "Capture_Provided_Object_Name": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Provided_Object_Name", + "value": "@{coalesce(variables('Object_Name'), '')}" + } + }, + "Capture_Provided_Snappable_Fid": { + "runAfter": { + "Capture_Provided_Object_Name": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Provided_Snappable_Fid", + "value": "@{coalesce(variables('Snappable_Fid'), '')}" + } + }, + "Capture_Provided_Snapshot_Fid": { + "runAfter": { + "Capture_Provided_Snappable_Fid": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Provided_Snapshot_Fid", + "value": "@{coalesce(variables('Snapshot_Fid'), '')}" + } + }, + "Validate_violation_identifiers": { + "runAfter": { + "Capture_Provided_Snapshot_Fid": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "and": [ + { + "not": { + "equals": [ + "@empty(trim(variables('Provided_Object_Name')))", + true + ] + } + }, + { + "not": { + "equals": [ + "@toLower(trim(variables('Provided_Object_Name')))", + "@toLower(trim(coalesce(body('Get_DSPM_Violation')?['data']?['policyViolation']?['resourceMetadata']?['metadata']?['name'], '')))" + ] + } + } + ] + }, + { + "and": [ + { + "not": { + "equals": [ + "@empty(trim(variables('Provided_Snappable_Fid')))", + true + ] + } + }, + { + "not": { + "equals": [ + "@trim(variables('Provided_Snappable_Fid'))", + "@trim(coalesce(body('Get_DSPM_Violation')?['data']?['policyViolation']?['resourceId'], ''))" + ] + } + } + ] + }, + { + "and": [ + { + "not": { + "equals": [ + "@empty(trim(variables('Provided_Snapshot_Fid')))", + true + ] + } + }, + { + "not": { + "equals": [ + "@trim(variables('Provided_Snapshot_Fid'))", + "@trim(coalesce(body('Get_DSPM_Violation')?['data']?['policyViolation']?['details']?['snapshotId'], ''))" + ] + } + } + ] + } + ] + }, + "actions": { + "Append_-_Validation_failed": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Provided information is invalid for violation @{variables('Violation_ID')}. One or more of the supplied identifiers does not match the violation. Object Name (provided='@{variables('Provided_Object_Name')}', actual='@{coalesce(body('Get_DSPM_Violation')?['data']?['policyViolation']?['resourceMetadata']?['metadata']?['name'], '')}'); Resource ID (provided='@{variables('Provided_Snappable_Fid')}', actual='@{coalesce(body('Get_DSPM_Violation')?['data']?['policyViolation']?['resourceId'], '')}'); Snapshot ID (provided='@{variables('Provided_Snapshot_Fid')}', actual='@{coalesce(body('Get_DSPM_Violation')?['data']?['policyViolation']?['details']?['snapshotId'], '')}')." + } + }, + "Comment_-_Validation_failed": { + "runAfter": { + "Append_-_Validation_failed": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Violation Remediation failed (validation).

@{join(variables('Error_Messages'), '
')}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + }, + "Terminate_-_Validation_failed": { + "runAfter": { + "Comment_-_Validation_failed": [ + "Succeeded", + "Failed" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "ValidationFailed", + "message": "@{join(variables('Error_Messages'), ' | ')}" + } + } + } + }, + "else": { + "actions": {} + } + }, + "Set_Object_Name_from_violation": { + "runAfter": { + "Validate_violation_identifiers": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Object_Name", + "value": "@{if(empty(trim(variables('Provided_Object_Name'))), coalesce(body('Get_DSPM_Violation')?['data']?['policyViolation']?['resourceMetadata']?['metadata']?['name'], ''), variables('Provided_Object_Name'))}" + } + }, + "Set_Snapshot_Fid_from_violation": { + "runAfter": { + "Set_Object_Name_from_violation": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Snapshot_Fid", + "value": "@{if(empty(trim(variables('Provided_Snapshot_Fid'))), coalesce(body('Get_DSPM_Violation')?['data']?['policyViolation']?['details']?['snapshotId'], ''), variables('Provided_Snapshot_Fid'))}" + } + }, + "Set_Snappable_Fid_from_violation": { + "runAfter": { + "Set_Snapshot_Fid_from_violation": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Snappable_Fid", + "value": "@{if(empty(trim(variables('Provided_Snappable_Fid'))), coalesce(body('Get_DSPM_Violation')?['data']?['policyViolation']?['resourceId'], ''), variables('Provided_Snappable_Fid'))}" + } + }, + "Set_Platform_from_violation": { + "runAfter": { + "Set_Snappable_Fid_from_violation": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Platform", + "value": "@{body('Get_DSPM_Violation')?['data']?['policyViolation']?['resourceMetadata']?['metadata']?['platform']}" + } + }, + "Set_Physical_Host_from_violation": { + "runAfter": { + "Set_Platform_from_violation": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Physical_Host", + "value": "@{body('Get_DSPM_Violation')?['data']?['policyViolation']?['resourceMetadata']?['metadata']?['physicalHost']}" + } + }, + "Set_Policy_Name_from_violation": { + "runAfter": { + "Check_Get_Snappable_Identity": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Policy_Name", + "value": "@{body('Get_DSPM_Violation')?['data']?['policyViolation']?['policy']?['name']}" + } + }, + "Set_CSV_Name_Pattern": { + "runAfter": { + "Set_Policy_Name_from_violation": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "CSV_Name_Pattern", + "value": "@{toLower(concat(replace(variables('Object_Name'), ' ', '-'), '-violating-files_file_results'))}" + } + }, + "Set_Log_Name_Pattern": { + "runAfter": { + "Set_CSV_Name_Pattern": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Log_Name_Pattern", + "value": "@{toLower(concat(variables('Object_Name'), ' actions log'))}" + } + }, + "Get_Snappable_Identity": { + "runAfter": { + "Set_Physical_Host_from_violation": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "query SnappableIdentity($fid: UUID!) {\n hierarchyObject(fid: $fid) {\n id\n name\n objectType\n ... on O365Site {\n title\n url\n __typename\n }\n ... on O365Onedrive {\n userPrincipalName\n userName\n userID\n __typename\n }\n __typename\n }\n}", + "variables": { + "fid": "@variables('Snappable_Fid')" + } + } + } + }, + "Check_Get_Snappable_Identity": { + "runAfter": { + "Get_Snappable_Identity": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@outputs('Get_Snappable_Identity')?['statusCode']", + 200 + ] + }, + { + "equals": [ + "@coalesce(body('Get_Snappable_Identity')?['errors'], '')", + "" + ] + } + ] + }, + "actions": { + "Set_Site_Url": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Site_Url", + "value": "@{coalesce(body('Get_Snappable_Identity')?['data']?['hierarchyObject']?['url'], '')}" + } + }, + "Set_Drive_Owner_Upn": { + "runAfter": { + "Set_Site_Url": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Drive_Owner_Upn", + "value": "@{coalesce(body('Get_Snappable_Identity')?['data']?['hierarchyObject']?['userPrincipalName'], '')}" + } + }, + "Set_Snappable_Owner_Name": { + "runAfter": { + "Set_Drive_Owner_Upn": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Snappable_Owner_Name", + "value": "@{coalesce(body('Get_Snappable_Identity')?['data']?['hierarchyObject']?['userName'], body('Get_Snappable_Identity')?['data']?['hierarchyObject']?['title'], body('Get_Snappable_Identity')?['data']?['hierarchyObject']?['name'], '')}" + } + } + }, + "else": { + "actions": { + "Append_-_Snappable_Identity_failed": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Could not resolve the snappable identity for @{variables('Snappable_Fid')} (status @{coalesce(outputs('Get_Snappable_Identity')?['statusCode'], 'none')}); O365 files cannot be resolved by drive and path without it." + } + } + } + } + } + } + } + }, + "Set_Status_In_Progress": { + "runAfter": { + "Check_Get_DSPM_Violation": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "mutation UpdatePolicyViolationsMutation($input: BulkUpdatePolicyViolationsInput!) {\n bulkUpdatePolicyViolations(input: $input)\n}", + "variables": { + "input": { + "newPolicyViolationStatus": "POLICY_VIOLATION_STATUS_IN_PROGRESS", + "policyViolationIds": [ + "@variables('Violation_ID')" + ] + } + } + } + } + }, + "Check_Set_Status_In_Progress": { + "runAfter": { + "Set_Status_In_Progress": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "not": { + "equals": [ + "@outputs('Set_Status_In_Progress')['statusCode']", + 200 + ] + } + }, + { + "not": { + "equals": [ + "@coalesce(body('Set_Status_In_Progress')?['errors'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Append_-_Set_Status_In_Progress_failed": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Update violation status to IN_PROGRESS failed for violation @{variables('Violation_ID')} (status @{outputs('Set_Status_In_Progress')?['statusCode']})." + } + }, + "Comment_-_Set_Status_In_Progress_failed": { + "runAfter": { + "Append_-_Set_Status_In_Progress_failed": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Violation Remediation failed.

@{join(variables('Error_Messages'), '
')}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + }, + "Terminate_-_Set_Status_In_Progress_failed": { + "runAfter": { + "Comment_-_Set_Status_In_Progress_failed": [ + "Succeeded", + "Failed" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "SetStatusInProgressFailed", + "message": "@{join(variables('Error_Messages'), ' | ')}" + } + } + } + }, + "else": { + "actions": {} + } + }, + "Stamp_CSV_Export_Start": { + "runAfter": { + "Check_Set_Status_In_Progress": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "CSV_Export_Start", + "value": "@{utcNow()}" + } + }, + "Trigger_Files_CSV_export": { + "runAfter": { + "Stamp_CSV_Export_Start": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "mutation DownloadFullSnapshotResultsCsvMutation($filters: DownloadResultsCsvFiltersInput, $snappableFid: String!, $snapshotFid: String!) {\n downloadSnapshotResultsCsv(\n snappableFid: $snappableFid\n snapshotFid: $snapshotFid\n downloadFilter: $filters\n ) {\n isSuccessful\n __typename\n }\n}", + "variables": { + "snappableFid": "@variables('Snappable_Fid')", + "snapshotFid": "@variables('Snapshot_Fid')", + "filters": { + "policyViolationId": "@variables('Violation_ID')", + "fileType": "HITS" + } + } + } + } + }, + "Check_Trigger_Files_CSV_export": { + "runAfter": { + "Trigger_Files_CSV_export": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "not": { + "equals": [ + "@outputs('Trigger_Files_CSV_export')['statusCode']", + 200 + ] + } + }, + { + "not": { + "equals": [ + "@coalesce(body('Trigger_Files_CSV_export')?['errors'], '')", + "" + ] + } + }, + { + "not": { + "equals": [ + "@coalesce(body('Trigger_Files_CSV_export')?['data']?['downloadSnapshotResultsCsv']?['isSuccessful'], false)", + true + ] + } + } + ] + }, + "actions": { + "Append_-_Trigger_Files_CSV_failed": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Files CSV export request (downloadSnapshotResultsCsv) failed or returned isSuccessful=false for violation @{variables('Violation_ID')} (status @{outputs('Trigger_Files_CSV_export')?['statusCode']})." + } + }, + "Set_Files_CSV_Link_trigger_failed": { + "runAfter": { + "Append_-_Trigger_Files_CSV_failed": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Files_CSV_Link", + "value": "Files CSV export request (downloadSnapshotResultsCsv) failed or returned isSuccessful=false; no CSV link available." + } + } + }, + "else": { + "actions": {} + } + }, + "Reset_Download_Ready_for_CSV": { + "runAfter": { + "Check_Trigger_Files_CSV_export": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Download_Ready", + "value": false + } + }, + "Reset_Download_External_Id_for_CSV": { + "runAfter": { + "Reset_Download_Ready_for_CSV": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Download_External_Id", + "value": "@null" + } + }, + "Poll_Download_Bar_for_CSV": { + "runAfter": { + "Reset_Download_External_Id_for_CSV": [ + "Succeeded" + ] + }, + "type": "Until", + "expression": "@equals(variables('Download_Ready'), true)", + "limit": { + "count": "[parameters('DownloadPollMaxAttempts')]", + "timeout": "PT1H" + }, + "actions": { + "Wait_before_CSV_poll": { + "runAfter": {}, + "type": "Wait", + "inputs": { + "interval": { + "count": "[parameters('DownloadPollIntervalSec')]", + "unit": "Second" + } + } + }, + "Download_Bar_CSV": { + "runAfter": { + "Wait_before_CSV_poll": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "query DownloadBarQuery {\n allUserFiles {\n downloads {\n externalId\n createdAt\n expiresAt\n completedAt\n creator\n filename\n type\n state\n __typename\n }\n __typename\n }\n}", + "variables": {} + } + } + }, + "Filter_snapshot_csv_downloads": { + "runAfter": { + "Download_Bar_CSV": [ + "Succeeded" + ] + }, + "type": "Query", + "inputs": { + "from": "@coalesce(first(coalesce(body('Download_Bar_CSV')?['data']?['allUserFiles'], json('[]')))?['downloads'], json('[]'))", + "where": "@and(equals(item()?['type'], 'SNAPSHOT_RESULTS_CSV'), and(contains(toLower(coalesce(item()?['filename'], '')), variables('CSV_Name_Pattern')), greater(ticks(coalesce(item()?['createdAt'], '1970-01-01T00:00:00Z')), ticks(addSeconds(variables('CSV_Export_Start'), -1)))))" + } + }, + "Set_CSV_Match_Count": { + "runAfter": { + "Filter_snapshot_csv_downloads": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "CSV_Match_Count", + "value": "@length(coalesce(body('Filter_snapshot_csv_downloads'), json('[]')))" + } + }, + "Sort_snapshot_csv_desc": { + "runAfter": { + "Set_CSV_Match_Count": [ + "Succeeded" + ] + }, + "type": "Compose", + "inputs": "@reverse(sort(body('Filter_snapshot_csv_downloads'), 'createdAt'))" + }, + "If_csv_match_ambiguous": { + "runAfter": { + "Sort_snapshot_csv_desc": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "greater": [ + "@variables('CSV_Match_Count')", + 1 + ] + } + ] + }, + "actions": { + "Append_CSV_ambiguous": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Files CSV export could not be matched unambiguously for violation @{variables('Violation_ID')}: @{variables('CSV_Match_Count')} files matched the object-name pattern after the export start time. Not attached." + } + }, + "Set_CSV_Ready_on_ambiguous": { + "runAfter": { + "Append_CSV_ambiguous": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Download_Ready", + "value": true + } + } + }, + "else": { + "actions": { + "If_newest_csv_ready_or_failed": { + "runAfter": {}, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('CSV_Match_Count')", + 1 + ] + }, + { + "equals": [ + "@first(outputs('Sort_snapshot_csv_desc'))?['state']", + "READY" + ] + } + ] + }, + "actions": { + "Set_CSV_External_Id": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Download_External_Id", + "value": "@{first(outputs('Sort_snapshot_csv_desc'))?['externalId']}" + } + }, + "Set_CSV_Ready_true": { + "runAfter": { + "Set_CSV_External_Id": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Download_Ready", + "value": true + } + } + }, + "else": { + "actions": { + "If_newest_csv_failed": { + "runAfter": {}, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('CSV_Match_Count')", + 1 + ] + }, + { + "equals": [ + "@first(outputs('Sort_snapshot_csv_desc'))?['state']", + "FAILED" + ] + } + ] + }, + "actions": { + "Append_CSV_export_failed": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Rubrik reported the files CSV export (SNAPSHOT_RESULTS_CSV) as FAILED for violation @{variables('Violation_ID')}; CSV link will be unavailable." + } + }, + "Set_CSV_Ready_on_failure": { + "runAfter": { + "Append_CSV_export_failed": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Download_Ready", + "value": true + } + } + }, + "else": { + "actions": {} + } + } + } + } + } + } + } + } + } + }, + "If_CSV_external_id_present": { + "runAfter": { + "Poll_Download_Bar_for_CSV": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@empty(variables('Download_External_Id'))", + false + ] + } + ] + }, + "actions": { + "Download_Files_CSV_bytes": { + "runAfter": {}, + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{variables('Base_URL')}/file-downloads/@{encodeURIComponent(variables('Download_External_Id'))}", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "User-Agent": "@variables('User_Agent')" + } + } + }, + "Upload_Files_CSV_blob": { + "runAfter": { + "Download_Files_CSV_bytes": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['azureblob']['connectionId']" + } + }, + "method": "post", + "body": "@body('Download_Files_CSV_bytes')", + "headers": { + "Content-Type": "text/csv" + }, + "path": "[concat('/v2/datasets/@{encodeURIComponent(encodeURIComponent(''', parameters('StorageAccountName'), '''))}/files')]", + "queries": { + "folderPath": "@{concat('/', parameters('StorageContainerName'), '/dspm/', triggerBody()?['object']?['name'], '/', variables('Violation_ID'))}", + "name": "@{concat('violation_files_', variables('Violation_ID'), '.csv')}", + "queryParametersSingleEncoded": true + } + } + }, + "Create_SAS_for_Files_CSV": { + "runAfter": { + "Upload_Files_CSV_blob": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['azureblob']['connectionId']" + } + }, + "method": "post", + "body": { + "Permissions": "Read", + "ExpiryTime": "@{addHours(utcNow(), parameters('SasTokenTtlHours'))}" + }, + "path": "[concat('/v2/datasets/@{encodeURIComponent(encodeURIComponent(''', parameters('StorageAccountName'), '''))}/CreateSharedLinkByPath')]", + "queries": { + "path": "@{concat('/', parameters('StorageContainerName'), '/dspm/', triggerBody()?['object']?['name'], '/', variables('Violation_ID'), '/violation_files_', variables('Violation_ID'), '.csv')}" + } + } + }, + "Set_Files_CSV_Link": { + "runAfter": { + "Create_SAS_for_Files_CSV": [ + "Succeeded", + "Failed", + "Skipped" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Files_CSV_Link", + "value": "@{coalesce(body('Create_SAS_for_Files_CSV')?['WebUrl'], concat('CSV uploaded to blob folder dspm/', triggerBody()?['object']?['name'], '/', variables('Violation_ID'), ' (SAS unavailable).'))}" + } + } + }, + "else": { + "actions": { + "If_CSV_trigger_succeeded_for_message": { + "runAfter": {}, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@outputs('Trigger_Files_CSV_export')['statusCode']", + 200 + ] + }, + { + "equals": [ + "@coalesce(body('Trigger_Files_CSV_export')?['data']?['downloadSnapshotResultsCsv']?['isSuccessful'], false)", + true + ] + } + ] + }, + "actions": { + "Set_Files_CSV_Link_unavailable": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Files_CSV_Link", + "value": "@{if(equals(variables('CSV_Match_Count'), 0), 'Files CSV download reference could not be located (no file matched the object-name pattern after the export start time, or the export failed/timed out); no CSV link available.', 'Files CSV export was not READY (failed, timed out, or ambiguous match); no CSV link available.')}" + } + } + }, + "else": { + "actions": {} + } + } + } + } + }, + "Stamp_Log_Export_Start": { + "runAfter": { + "If_CSV_external_id_present": [ + "Succeeded", + "Failed", + "Skipped" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Log_Export_Start", + "value": "@{utcNow()}" + } + }, + "Trigger_Remediation_Log_export": { + "runAfter": { + "Stamp_Log_Export_Start": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "mutation CreateViolationRemediationMutation($input: CreateViolationRemediationInput!) {\n createViolationRemediation(input: $input) {\n remediationId\n __typename\n }\n}", + "variables": { + "input": { + "targets": { + "targetIds": [ + "@variables('Violation_ID')" + ], + "targetType": "REMEDIATION_TARGET_TYPE_VIOLATION" + }, + "remediationType": "REMEDIATION_TYPE_EXPORT_ACTIONS_LOG_TO_CSV", + "resourceId": "@variables('Snappable_Fid')" + } + } + } + } + }, + "Check_Trigger_Remediation_Log_export": { + "runAfter": { + "Trigger_Remediation_Log_export": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "not": { + "equals": [ + "@outputs('Trigger_Remediation_Log_export')['statusCode']", + 200 + ] + } + }, + { + "not": { + "equals": [ + "@coalesce(body('Trigger_Remediation_Log_export')?['errors'], '')", + "" + ] + } + }, + { + "equals": [ + "@coalesce(body('Trigger_Remediation_Log_export')?['data']?['createViolationRemediation']?['remediationId'], '')", + "" + ] + } + ] + }, + "actions": { + "Append_-_Trigger_Log_failed": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Remediation actions-log export request (createViolationRemediation) failed or returned no remediationId for violation @{variables('Violation_ID')} (status @{outputs('Trigger_Remediation_Log_export')?['statusCode']}); remediation log link will be unavailable." + } + } + }, + "else": { + "actions": {} + } + }, + "If_Log_trigger_succeeded": { + "runAfter": { + "Check_Trigger_Remediation_Log_export": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@outputs('Trigger_Remediation_Log_export')['statusCode']", + 200 + ] + }, + { + "equals": [ + "@coalesce(body('Trigger_Remediation_Log_export')?['errors'], '')", + "" + ] + }, + { + "not": { + "equals": [ + "@coalesce(body('Trigger_Remediation_Log_export')?['data']?['createViolationRemediation']?['remediationId'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Reset_Download_Ready_for_Log": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Download_Ready", + "value": false + } + }, + "Reset_Download_External_Id_for_Log": { + "runAfter": { + "Reset_Download_Ready_for_Log": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Download_External_Id", + "value": "@null" + } + }, + "Poll_Download_Bar_for_Log": { + "runAfter": { + "Reset_Download_External_Id_for_Log": [ + "Succeeded" + ] + }, + "type": "Until", + "expression": "@equals(variables('Download_Ready'), true)", + "limit": { + "count": "[parameters('DownloadPollMaxAttempts')]", + "timeout": "PT1H" + }, + "actions": { + "Wait_before_Log_poll": { + "runAfter": {}, + "type": "Wait", + "inputs": { + "interval": { + "count": "[parameters('DownloadPollIntervalSec')]", + "unit": "Second" + } + } + }, + "Download_Bar_Log": { + "runAfter": { + "Wait_before_Log_poll": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "query DownloadBarQuery {\n allUserFiles {\n downloads {\n externalId\n createdAt\n expiresAt\n completedAt\n creator\n filename\n type\n state\n __typename\n }\n __typename\n }\n}", + "variables": {} + } + } + }, + "Filter_snapshot_log_downloads": { + "runAfter": { + "Download_Bar_Log": [ + "Succeeded" + ] + }, + "type": "Query", + "inputs": { + "from": "@coalesce(first(coalesce(body('Download_Bar_Log')?['data']?['allUserFiles'], json('[]')))?['downloads'], json('[]'))", + "where": "@and(equals(item()?['type'], 'REMEDIATION_ACTIONS_LOG_CSV'), and(contains(toLower(coalesce(item()?['filename'], '')), variables('Log_Name_Pattern')), greater(ticks(coalesce(item()?['createdAt'], '1970-01-01T00:00:00Z')), ticks(addSeconds(variables('Log_Export_Start'), -1)))))" + } + }, + "Set_Log_Match_Count": { + "runAfter": { + "Filter_snapshot_log_downloads": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Log_Match_Count", + "value": "@length(coalesce(body('Filter_snapshot_log_downloads'), json('[]')))" + } + }, + "Sort_snapshot_log_desc": { + "runAfter": { + "Set_Log_Match_Count": [ + "Succeeded" + ] + }, + "type": "Compose", + "inputs": "@reverse(sort(body('Filter_snapshot_log_downloads'), 'createdAt'))" + }, + "If_log_match_ambiguous": { + "runAfter": { + "Sort_snapshot_log_desc": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "greater": [ + "@variables('Log_Match_Count')", + 1 + ] + } + ] + }, + "actions": { + "Append_Log_ambiguous": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Remediation actions-log export could not be matched unambiguously for violation @{variables('Violation_ID')}: @{variables('Log_Match_Count')} files matched the object-name pattern after the export start time. Not attached." + } + }, + "Set_Log_Ready_on_ambiguous": { + "runAfter": { + "Append_Log_ambiguous": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Download_Ready", + "value": true + } + } + }, + "else": { + "actions": { + "If_newest_log_ready_or_failed": { + "runAfter": {}, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Log_Match_Count')", + 1 + ] + }, + { + "equals": [ + "@first(outputs('Sort_snapshot_log_desc'))?['state']", + "READY" + ] + } + ] + }, + "actions": { + "Set_Log_External_Id": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Download_External_Id", + "value": "@{first(outputs('Sort_snapshot_log_desc'))?['externalId']}" + } + }, + "Set_Log_Ready_true": { + "runAfter": { + "Set_Log_External_Id": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Download_Ready", + "value": true + } + } + }, + "else": { + "actions": { + "If_newest_log_failed": { + "runAfter": {}, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Log_Match_Count')", + 1 + ] + }, + { + "equals": [ + "@first(outputs('Sort_snapshot_log_desc'))?['state']", + "FAILED" + ] + } + ] + }, + "actions": { + "Append_Log_export_failed": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Rubrik reported the remediation actions-log export (REMEDIATION_ACTIONS_LOG_CSV) as FAILED for violation @{variables('Violation_ID')}; remediation log link will be unavailable." + } + }, + "Set_Log_Ready_on_failure": { + "runAfter": { + "Append_Log_export_failed": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Download_Ready", + "value": true + } + } + }, + "else": { + "actions": {} + } + } + } + } + } + } + } + } + } + }, + "If_Log_external_id_present": { + "runAfter": { + "Poll_Download_Bar_for_Log": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@empty(variables('Download_External_Id'))", + false + ] + } + ] + }, + "actions": { + "Download_Remediation_Log_bytes": { + "runAfter": {}, + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{variables('Base_URL')}/file-downloads/@{encodeURIComponent(variables('Download_External_Id'))}", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "User-Agent": "@variables('User_Agent')" + } + } + }, + "Upload_Remediation_Log_blob": { + "runAfter": { + "Download_Remediation_Log_bytes": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['azureblob']['connectionId']" + } + }, + "method": "post", + "body": "@body('Download_Remediation_Log_bytes')", + "headers": { + "Content-Type": "text/csv" + }, + "path": "[concat('/v2/datasets/@{encodeURIComponent(encodeURIComponent(''', parameters('StorageAccountName'), '''))}/files')]", + "queries": { + "folderPath": "@{concat('/', parameters('StorageContainerName'), '/dspm/', triggerBody()?['object']?['name'], '/', variables('Violation_ID'))}", + "name": "@{concat('remediation_log_', variables('Violation_ID'), '.csv')}", + "queryParametersSingleEncoded": true + } + } + }, + "Create_SAS_for_Remediation_Log": { + "runAfter": { + "Upload_Remediation_Log_blob": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['azureblob']['connectionId']" + } + }, + "method": "post", + "body": { + "Permissions": "Read", + "ExpiryTime": "@{addHours(utcNow(), parameters('SasTokenTtlHours'))}" + }, + "path": "[concat('/v2/datasets/@{encodeURIComponent(encodeURIComponent(''', parameters('StorageAccountName'), '''))}/CreateSharedLinkByPath')]", + "queries": { + "path": "@{concat('/', parameters('StorageContainerName'), '/dspm/', triggerBody()?['object']?['name'], '/', variables('Violation_ID'), '/remediation_log_', variables('Violation_ID'), '.csv')}" + } + } + }, + "Set_Remediation_Log_Link": { + "runAfter": { + "Create_SAS_for_Remediation_Log": [ + "Succeeded", + "Failed", + "Skipped" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Remediation_Log_Link", + "value": "@{coalesce(body('Create_SAS_for_Remediation_Log')?['WebUrl'], concat('Remediation log uploaded to blob folder dspm/', triggerBody()?['object']?['name'], '/', variables('Violation_ID'), ' (SAS unavailable).'))}" + } + } + }, + "else": { + "actions": { + "Set_Remediation_Log_Link_unavailable": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Remediation_Log_Link", + "value": "@{if(equals(variables('Log_Match_Count'), 0), 'Remediation actions-log download reference could not be located (no file matched the object-name pattern after the export start time, or the export failed/timed out); no remediation log link available.', 'Remediation actions-log export was not READY (failed, timed out, or ambiguous match); no remediation log link available.')}" + } + } + } + } + } + }, + "else": { + "actions": { + "Set_Remediation_Log_Link_trigger_failed": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Remediation_Log_Link", + "value": "Remediation actions-log export request failed; no remediation log link available." + } + } + } + } + }, + "List_file_hits_full": { + "runAfter": { + "If_Log_trigger_succeeded": [ + "Succeeded", + "Failed", + "Skipped" + ] + }, + "type": "Until", + "expression": "@equals(variables('Has_Next_Page'), false)", + "limit": { + "count": 100, + "timeout": "PT1H" + }, + "actions": { + "List_DSPM_Violation_File_Hits_full": { + "runAfter": {}, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "query DSPMViolationFileListQuery(\n $first: Int!\n $after: String\n $snappableFid: String!\n $snapshotFid: String!\n $filters: ListFileResultFiltersInput\n $sort: FileResultSortInput\n $timezone: String!\n) {\n policyObj(snappableFid: $snappableFid, snapshotFid: $snapshotFid) {\n id: snapshotFid\n fileResultConnection(\n first: $first\n after: $after\n filter: $filters\n sort: $sort\n timezone: $timezone\n ) {\n edges {\n cursor\n node {\n nativePath\n stdPath\n filename\n mode\n size\n directory\n createdBy\n modifiedBy\n hits {\n totalHits\n violations\n __typename\n }\n __typename\n }\n __typename\n }\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n __typename\n }\n hasLatestData\n __typename\n }\n __typename\n }\n}", + "variables": { + "first": 25, + "after": "@if(empty(variables('Next_Cursor')), null, variables('Next_Cursor'))", + "snappableFid": "@variables('Snappable_Fid')", + "snapshotFid": "@variables('Snapshot_Fid')", + "sort": { + "sortBy": "HITS", + "sortOrder": "DESC" + }, + "filters": { + "violationId": "@variables('Violation_ID')", + "fileType": "HITS", + "whitelistEnabled": true + }, + "timezone": "UTC" + } + } + } + }, + "Check_List_file_hits_full": { + "runAfter": { + "List_DSPM_Violation_File_Hits_full": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "not": { + "equals": [ + "@outputs('List_DSPM_Violation_File_Hits_full')['statusCode']", + 200 + ] + } + }, + { + "not": { + "equals": [ + "@coalesce(body('List_DSPM_Violation_File_Hits_full')?['errors'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Append_-_List_file_hits_full_failed": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Listing DSPM violation file hits failed for violation @{variables('Violation_ID')} (status @{outputs('List_DSPM_Violation_File_Hits_full')?['statusCode']})." + } + }, + "Set_File_Hits_Failed_full": { + "runAfter": { + "Append_-_List_file_hits_full_failed": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "File_Hits_Failed", + "value": true + } + }, + "Set_Has_Next_Page_false_on_full_failure": { + "runAfter": { + "Set_File_Hits_Failed_full": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Has_Next_Page", + "value": false + } + } + }, + "else": { + "actions": {} + } + }, + "Append_files_full": { + "runAfter": { + "Check_List_file_hits_full": [ + "Succeeded" + ] + }, + "type": "Foreach", + "foreach": "@coalesce(body('List_DSPM_Violation_File_Hits_full')?['data']?['policyObj']?['fileResultConnection']?['edges'], json('[]'))", + "runtimeConfiguration": { + "concurrency": { + "repetitions": 1 + } + }, + "actions": { + "Append_file_record_full": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Files_To_Process", + "value": { + "filename": "@items('Append_files_full')?['node']?['filename']", + "nativePath": "@items('Append_files_full')?['node']?['nativePath']", + "stdPath": "@items('Append_files_full')?['node']?['stdPath']", + "directory": "@items('Append_files_full')?['node']?['directory']", + "createdBy": "@items('Append_files_full')?['node']?['createdBy']", + "modifiedBy": "@items('Append_files_full')?['node']?['modifiedBy']", + "platform": "@variables('Platform')", + "physicalHost": "@variables('Physical_Host')", + "siteUrl": "@variables('Site_Url')", + "driveOwnerUpn": "@variables('Drive_Owner_Upn')", + "snappableOwnerName": "@variables('Snappable_Owner_Name')" + } + } + } + } + }, + "Set_Has_Next_Page_full": { + "runAfter": { + "Append_files_full": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Has_Next_Page", + "value": "@coalesce(body('List_DSPM_Violation_File_Hits_full')?['data']?['policyObj']?['fileResultConnection']?['pageInfo']?['hasNextPage'], false)" + } + }, + "Set_Next_Cursor_full": { + "runAfter": { + "Set_Has_Next_Page_full": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Next_Cursor", + "value": "@{body('List_DSPM_Violation_File_Hits_full')?['data']?['policyObj']?['fileResultConnection']?['pageInfo']?['endCursor']}" + } + } + } + }, + "Check_File_Hits_Failed_full": { + "runAfter": { + "List_file_hits_full": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('File_Hits_Failed')", + true + ] + } + ] + }, + "actions": { + "Comment_-_File_hits_failed_full": { + "runAfter": {}, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Violation Remediation failed.

@{join(variables('Error_Messages'), '
')}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + }, + "Terminate_-_File_hits_failed_full": { + "runAfter": { + "Comment_-_File_hits_failed_full": [ + "Succeeded", + "Failed" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "ListFileHitsFailed", + "message": "@{join(variables('Error_Messages'), ' | ')}" + } + } + } + }, + "else": { + "actions": {} + } + }, + "If_files_present_full": { + "runAfter": { + "Check_File_Hits_Failed_full": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "greater": [ + "@length(coalesce(variables('Files_To_Process'), json('[]')))", + 0 + ] + } + ] + }, + "actions": { + "Select_files_for_quarantine_full": { + "runAfter": {}, + "type": "Select", + "inputs": { + "from": "@variables('Files_To_Process')", + "select": { + "filename": "@item()?['filename']", + "filePath": "@item()?['nativePath']", + "nativePath": "@item()?['nativePath']", + "directory": "@item()?['directory']", + "ownerUpn": "@coalesce(item()?['createdBy'], item()?['modifiedBy'], '')", + "objectType": "@coalesce(item()?['platform'], variables('Object_Type'), '')", + "physicalHost": "@item()?['physicalHost']", + "siteUrl": "@item()?['siteUrl']", + "driveOwnerUpn": "@item()?['driveOwnerUpn']" + } + } + }, + "Call_RubrikQuarantineFiles_full": { + "runAfter": { + "Select_files_for_quarantine_full": [ + "Succeeded" + ] + }, + "type": "Workflow", + "inputs": { + "host": { + "triggerName": "manual", + "workflow": { + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Logic/workflows/', parameters('RubrikQuarantineFilesPlaybookName'))]" + } + }, + "body": { + "objectType": "@variables('Object_Type')", + "files": "@body('Select_files_for_quarantine_full')" + } + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Set_Quarantine_Results_full": { + "runAfter": { + "Call_RubrikQuarantineFiles_full": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Quarantine_Results", + "value": "@coalesce(body('Call_RubrikQuarantineFiles_full')?['results'], json('[]'))" + } + }, + "Set_All_Quarantine_Succeeded_full": { + "runAfter": { + "Set_Quarantine_Results_full": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "All_Quarantine_Succeeded", + "value": "@equals(toLower(coalesce(body('Call_RubrikQuarantineFiles_full')?['status'], 'failed')), 'success')" + } + }, + "Notify_owner_full": { + "runAfter": { + "Set_All_Quarantine_Succeeded_full": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['teams']['connectionId']" + } + }, + "method": "post", + "path": "/beta/teams/conversation/message/poster/Flow bot/location/@{encodeURIComponent('channel')}", + "body": { + "recipient": { + "groupId": "[parameters('TeamsGroupId')]", + "channelId": "[parameters('TeamsChannelId')]" + }, + "messageBody": "

Rubrik DSPM Violation Remediation

Hi @{coalesce(variables('Owner_UPN'), 'file owner')},
The Rubrik DSPM policy @{variables('Policy_Name')} (severity: @{triggerBody()?['object']?['properties']?['severity']}) was raised on object @{variables('Object_Name')}.

Violation ID: @{variables('Violation_ID')}
Files processed: @{length(variables('Quarantine_Results'))}
All quarantine succeeded: @{variables('All_Quarantine_Succeeded')}

@{if(greater(length(variables('Quarantine_Results')), 0), 'The listed files have been processed for quarantine.', 'No files were processed for quarantine.')} See the Sentinel incident for the CSV/log export links and per-file results.

" + } + } + }, + "If_all_quarantine_succeeded_full": { + "runAfter": { + "Notify_owner_full": [ + "Succeeded", + "Failed" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('All_Quarantine_Succeeded')", + true + ] + }, + { + "greater": [ + "@length(variables('Quarantine_Results'))", + 0 + ] + } + ] + }, + "actions": { + "Set_Status_Remediated": { + "runAfter": {}, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "mutation UpdatePolicyViolationsMutation($input: BulkUpdatePolicyViolationsInput!) {\n bulkUpdatePolicyViolations(input: $input)\n}", + "variables": { + "input": { + "newPolicyViolationStatus": "POLICY_VIOLATION_STATUS_REMEDIATED", + "policyViolationIds": [ + "@variables('Violation_ID')" + ] + } + } + } + } + } + }, + "else": { + "actions": {} + } + }, + "Comment_full_summary": { + "runAfter": { + "If_all_quarantine_succeeded_full": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Violation Remediation

Event type: SecurityViolation
Policy: @{variables('Policy_Name')}
Object: @{variables('Object_Name')} (@{variables('Object_Type')})
Violation ID: @{variables('Violation_ID')}

Exports:
Files CSV: @{if(empty(coalesce(variables('Files_CSV_Link'), '')), 'Not generated.', variables('Files_CSV_Link'))}
Remediation log: @{if(empty(coalesce(variables('Remediation_Log_Link'), '')), 'Not generated.', variables('Remediation_Log_Link'))}
(SAS links expire in @{parameters('SasTokenTtlHours')} hours)

Files processed: @{length(variables('Quarantine_Results'))}
All quarantine succeeded: @{variables('All_Quarantine_Succeeded')}
Final violation status: @{if(and(equals(variables('All_Quarantine_Succeeded'), true), greater(length(variables('Quarantine_Results')), 0)), 'REMEDIATED', 'IN_PROGRESS')}

Quarantine results:
@{string(variables('Quarantine_Results'))}

Quarantine playbook status: @{coalesce(body('Call_RubrikQuarantineFiles_full')?['status'], 'no response')}
Quarantine playbook message: @{coalesce(body('Call_RubrikQuarantineFiles_full')?['error'], '')}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + } + }, + "else": { + "actions": { + "Set_Status_Remediated_no_files": { + "runAfter": {}, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "mutation UpdatePolicyViolationsMutation($input: BulkUpdatePolicyViolationsInput!) {\n bulkUpdatePolicyViolations(input: $input)\n}", + "variables": { + "input": { + "newPolicyViolationStatus": "POLICY_VIOLATION_STATUS_REMEDIATED", + "policyViolationIds": [ + "@variables('Violation_ID')" + ] + } + } + } + } + }, + "Notify_owner_no_files": { + "runAfter": { + "Set_Status_Remediated_no_files": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['teams']['connectionId']" + } + }, + "method": "post", + "path": "/beta/teams/conversation/message/poster/Flow bot/location/@{encodeURIComponent('channel')}", + "body": { + "recipient": { + "groupId": "[parameters('TeamsGroupId')]", + "channelId": "[parameters('TeamsChannelId')]" + }, + "messageBody": "

Rubrik DSPM Violation Remediation

Hi @{coalesce(variables('Owner_UPN'), 'file owner')},
The Rubrik DSPM policy @{variables('Policy_Name')} was raised on object @{variables('Object_Name')}, but the violation contains no files.

Violation ID: @{variables('Violation_ID')}
Files processed: 0

No quarantine action was required. The violation has been marked remediated.

" + } + } + }, + "Comment_full_summary_no_files": { + "runAfter": { + "Notify_owner_no_files": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Violation Remediation

Event type: SecurityViolation
Policy: @{variables('Policy_Name')}
Object: @{variables('Object_Name')} (@{variables('Object_Type')})
Violation ID: @{variables('Violation_ID')}

No files were returned for this violation, so there was nothing to quarantine. The quarantine playbook was not called.

Final violation status: @{if(and(greaterOrEquals(coalesce(outputs('Set_Status_Remediated_no_files')?['statusCode'], 0), 200), less(coalesce(outputs('Set_Status_Remediated_no_files')?['statusCode'], 0), 300)), 'REMEDIATED', 'Update failed - still IN_PROGRESS')}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + } + } + } + } + } + }, + "Classification": { + "case": "Classification", + "actions": { + "Get_Snappable_Identity_reduced": { + "runAfter": {}, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "query SnappableIdentity($fid: UUID!) {\n hierarchyObject(fid: $fid) {\n id\n name\n objectType\n ... on O365Site {\n title\n url\n __typename\n }\n ... on O365Onedrive {\n userPrincipalName\n userName\n userID\n __typename\n }\n __typename\n }\n}", + "variables": { + "fid": "@variables('Snappable_Fid')" + } + } + } + }, + "Check_Get_Snappable_Identity_reduced": { + "runAfter": { + "Get_Snappable_Identity_reduced": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@outputs('Get_Snappable_Identity_reduced')?['statusCode']", + 200 + ] + }, + { + "equals": [ + "@coalesce(body('Get_Snappable_Identity_reduced')?['errors'], '')", + "" + ] + } + ] + }, + "actions": { + "Set_Site_Url_reduced": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Site_Url", + "value": "@{coalesce(body('Get_Snappable_Identity_reduced')?['data']?['hierarchyObject']?['url'], '')}" + } + }, + "Set_Drive_Owner_Upn_reduced": { + "runAfter": { + "Set_Site_Url_reduced": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Drive_Owner_Upn", + "value": "@{coalesce(body('Get_Snappable_Identity_reduced')?['data']?['hierarchyObject']?['userPrincipalName'], '')}" + } + }, + "Set_Snappable_Owner_Name_reduced": { + "runAfter": { + "Set_Drive_Owner_Upn_reduced": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Snappable_Owner_Name", + "value": "@{coalesce(body('Get_Snappable_Identity_reduced')?['data']?['hierarchyObject']?['userName'], body('Get_Snappable_Identity_reduced')?['data']?['hierarchyObject']?['title'], body('Get_Snappable_Identity_reduced')?['data']?['hierarchyObject']?['name'], '')}" + } + } + }, + "else": { + "actions": { + "Append_-_Snappable_Identity_reduced_failed": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Could not resolve the snappable identity for @{variables('Snappable_Fid')} (status @{coalesce(outputs('Get_Snappable_Identity_reduced')?['statusCode'], 'none')}); O365 files cannot be resolved by drive and path without it." + } + } + } + } + }, + "List_file_hits_reduced": { + "runAfter": { + "Check_Get_Snappable_Identity_reduced": [ + "Succeeded" + ] + }, + "type": "Until", + "expression": "@equals(variables('Has_Next_Page'), false)", + "limit": { + "count": 100, + "timeout": "PT1H" + }, + "actions": { + "List_DSPM_Violation_File_Hits_reduced": { + "runAfter": {}, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "query DSPMViolationFileListQuery(\n $first: Int!\n $after: String\n $snappableFid: String!\n $snapshotFid: String!\n $filters: ListFileResultFiltersInput\n $sort: FileResultSortInput\n $timezone: String!\n) {\n policyObj(snappableFid: $snappableFid, snapshotFid: $snapshotFid) {\n id: snapshotFid\n fileResultConnection(\n first: $first\n after: $after\n filter: $filters\n sort: $sort\n timezone: $timezone\n ) {\n edges {\n cursor\n node {\n nativePath\n stdPath\n filename\n mode\n size\n directory\n createdBy\n modifiedBy\n hits {\n totalHits\n violations\n __typename\n }\n __typename\n }\n __typename\n }\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n __typename\n }\n hasLatestData\n __typename\n }\n __typename\n }\n}", + "variables": { + "first": 25, + "after": "@if(empty(variables('Next_Cursor')), null, variables('Next_Cursor'))", + "snappableFid": "@variables('Object_ID')", + "snapshotFid": "@variables('Snapshot_Fid')", + "sort": { + "sortBy": "HITS", + "sortOrder": "DESC" + }, + "filters": { + "fileType": "HITS", + "whitelistEnabled": true + }, + "timezone": "UTC" + } + } + } + }, + "Check_List_file_hits_reduced": { + "runAfter": { + "List_DSPM_Violation_File_Hits_reduced": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "not": { + "equals": [ + "@outputs('List_DSPM_Violation_File_Hits_reduced')['statusCode']", + 200 + ] + } + }, + { + "not": { + "equals": [ + "@coalesce(body('List_DSPM_Violation_File_Hits_reduced')?['errors'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Append_-_List_file_hits_reduced_failed": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Listing DSPM classification file hits failed for object @{variables('Object_ID')} (status @{outputs('List_DSPM_Violation_File_Hits_reduced')?['statusCode']})." + } + }, + "Set_File_Hits_Failed_reduced": { + "runAfter": { + "Append_-_List_file_hits_reduced_failed": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "File_Hits_Failed", + "value": true + } + }, + "Set_Has_Next_Page_false_on_reduced_failure": { + "runAfter": { + "Set_File_Hits_Failed_reduced": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Has_Next_Page", + "value": false + } + } + }, + "else": { + "actions": {} + } + }, + "Append_files_reduced": { + "runAfter": { + "Check_List_file_hits_reduced": [ + "Succeeded" + ] + }, + "type": "Foreach", + "foreach": "@coalesce(body('List_DSPM_Violation_File_Hits_reduced')?['data']?['policyObj']?['fileResultConnection']?['edges'], json('[]'))", + "runtimeConfiguration": { + "concurrency": { + "repetitions": 1 + } + }, + "actions": { + "Append_file_record_reduced": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Files_To_Process", + "value": { + "filename": "@items('Append_files_reduced')?['node']?['filename']", + "nativePath": "@items('Append_files_reduced')?['node']?['nativePath']", + "directory": "@items('Append_files_reduced')?['node']?['directory']", + "stdPath": "@items('Append_files_reduced')?['node']?['stdPath']", + "createdBy": "@items('Append_files_reduced')?['node']?['createdBy']", + "modifiedBy": "@items('Append_files_reduced')?['node']?['modifiedBy']", + "platform": "@variables('Object_Type')", + "physicalHost": "@variables('Physical_Host')", + "siteUrl": "@variables('Site_Url')", + "driveOwnerUpn": "@variables('Drive_Owner_Upn')", + "snappableOwnerName": "@variables('Snappable_Owner_Name')" + } + } + } + } + }, + "Set_Has_Next_Page_reduced": { + "runAfter": { + "Append_files_reduced": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Has_Next_Page", + "value": "@coalesce(body('List_DSPM_Violation_File_Hits_reduced')?['data']?['policyObj']?['fileResultConnection']?['pageInfo']?['hasNextPage'], false)" + } + }, + "Set_Next_Cursor_reduced": { + "runAfter": { + "Set_Has_Next_Page_reduced": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Next_Cursor", + "value": "@{body('List_DSPM_Violation_File_Hits_reduced')?['data']?['policyObj']?['fileResultConnection']?['pageInfo']?['endCursor']}" + } + } + } + }, + "Check_File_Hits_Failed_reduced": { + "runAfter": { + "List_file_hits_reduced": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('File_Hits_Failed')", + true + ] + } + ] + }, + "actions": { + "Comment_-_File_hits_failed_reduced": { + "runAfter": {}, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Classification Remediation failed.

@{join(variables('Error_Messages'), '
')}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + }, + "Terminate_-_File_hits_failed_reduced": { + "runAfter": { + "Comment_-_File_hits_failed_reduced": [ + "Succeeded", + "Failed" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "ListFileHitsFailed", + "message": "@{join(variables('Error_Messages'), ' | ')}" + } + } + } + }, + "else": { + "actions": {} + } + }, + "If_files_present_reduced": { + "runAfter": { + "Check_File_Hits_Failed_reduced": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "greater": [ + "@length(coalesce(variables('Files_To_Process'), json('[]')))", + 0 + ] + } + ] + }, + "actions": { + "Select_files_for_quarantine_reduced": { + "runAfter": {}, + "type": "Select", + "inputs": { + "from": "@variables('Files_To_Process')", + "select": { + "filename": "@item()?['filename']", + "filePath": "@item()?['nativePath']", + "nativePath": "@item()?['nativePath']", + "directory": "@item()?['directory']", + "ownerUpn": "@coalesce(item()?['createdBy'], item()?['modifiedBy'], '')", + "objectType": "@coalesce(item()?['platform'], variables('Object_Type'), '')", + "physicalHost": "@item()?['physicalHost']", + "siteUrl": "@item()?['siteUrl']", + "driveOwnerUpn": "@item()?['driveOwnerUpn']" + } + } + }, + "Call_RubrikQuarantineFiles_reduced": { + "runAfter": { + "Select_files_for_quarantine_reduced": [ + "Succeeded" + ] + }, + "type": "Workflow", + "inputs": { + "host": { + "triggerName": "manual", + "workflow": { + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Logic/workflows/', parameters('RubrikQuarantineFilesPlaybookName'))]" + } + }, + "body": { + "objectType": "@variables('Object_Type')", + "files": "@body('Select_files_for_quarantine_reduced')" + } + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Set_Quarantine_Results_reduced": { + "runAfter": { + "Call_RubrikQuarantineFiles_reduced": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Quarantine_Results", + "value": "@coalesce(body('Call_RubrikQuarantineFiles_reduced')?['results'], json('[]'))" + } + }, + "Notify_owner_reduced": { + "runAfter": { + "Set_Quarantine_Results_reduced": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['teams']['connectionId']" + } + }, + "method": "post", + "path": "/beta/teams/conversation/message/poster/Flow bot/location/@{encodeURIComponent('channel')}", + "body": { + "recipient": { + "groupId": "[parameters('TeamsGroupId')]", + "channelId": "[parameters('TeamsChannelId')]" + }, + "messageBody": "

Rubrik DSPM Classification Remediation

Hi @{coalesce(variables('Owner_UPN'), 'file owner')},
Classification policy hits were found on object @{variables('Object_Name')} (@{variables('Object_Type')}), severity @{triggerBody()?['object']?['properties']?['severity']}.

Object ID: @{variables('Object_ID')}
Files processed: @{length(variables('Quarantine_Results'))}

@{if(greater(length(variables('Quarantine_Results')), 0), 'The listed files have been processed for quarantine.', 'No files were processed for quarantine.')} See the Sentinel incident for per-file results.

" + } + } + }, + "Comment_reduced_summary": { + "runAfter": { + "Notify_owner_reduced": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Classification Remediation

Event type: Classification
Object: @{variables('Object_Name')} (@{variables('Object_Type')})
Object ID: @{variables('Object_ID')}
Snapshot ID: @{variables('Snapshot_Fid')}

Files processed: @{length(variables('Quarantine_Results'))}

Quarantine results:
@{string(variables('Quarantine_Results'))}

No CSV/log export or violation status update is performed for classification events (no DSPM license / no violation object).

Quarantine playbook status: @{coalesce(body('Call_RubrikQuarantineFiles_reduced')?['status'], 'no response')}
Quarantine playbook message: @{coalesce(body('Call_RubrikQuarantineFiles_reduced')?['error'], '')}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + } + }, + "else": { + "actions": { + "Skip_quarantine_no_files_reduced": { + "runAfter": {}, + "type": "Compose", + "inputs": "No files returned for this classification event; the quarantine playbook was not called." + }, + "Comment_reduced_summary_no_files": { + "runAfter": { + "Skip_quarantine_no_files_reduced": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Classification Remediation

Event type: Classification
Object: @{variables('Object_Name')} (@{variables('Object_Type')})
Object ID: @{variables('Object_ID')}
Snapshot ID: @{variables('Snapshot_Fid')}

No files were returned for this classification event, so there was nothing to quarantine. The quarantine playbook was not called.

No violation status update is performed for classification events.

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + } + } + } + } + } + } + }, + "default": { + "actions": { + "Comment_unknown_event_type": { + "runAfter": {}, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik DSPM Violation Remediation: unrecognized eventType '@{variables('Event_Type')}'. Expected 'SecurityViolation' or 'Classification'. No action taken.

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + } + } + } + }, + "Initialize_Site_Url": { + "runAfter": { + "Initialize_Provided_Snapshot_Fid": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Site_Url", + "type": "string" + } + ] + } + }, + "Initialize_Drive_Owner_Upn": { + "runAfter": { + "Initialize_Site_Url": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Drive_Owner_Upn", + "type": "string" + } + ] + } + }, + "Initialize_Snappable_Owner_Name": { + "runAfter": { + "Initialize_Drive_Owner_Upn": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Snappable_Owner_Name", + "type": "string" + } + ] + } + } + }, + "outputs": {} + }, + "parameters": { + "$connections": { + "value": { + "azuresentinel": { + "connectionId": "[resourceId('Microsoft.Web/connections', variables('MicrosoftSentinelConnectionName'))]", + "connectionName": "[variables('MicrosoftSentinelConnectionName')]", + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/azuresentinel')]", + "connectionProperties": { + "authentication": { + "type": "ManagedServiceIdentity" + } + } + }, + "RubrikCustomConnector": { + "connectionId": "[resourceId('Microsoft.Web/connections', variables('RubrikcustomconnectorConnectionName'))]", + "connectionName": "[variables('RubrikcustomconnectorConnectionName')]", + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', parameters('Rubrik Connector name'))]" + }, + "keyvault": { + "connectionId": "[resourceId('Microsoft.Web/connections', variables('KeyvaultConnectionName'))]", + "connectionName": "[variables('KeyvaultConnectionName')]", + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/keyvault')]", + "connectionProperties": { + "authentication": { + "type": "ManagedServiceIdentity" + } + } + }, + "teams": { + "connectionId": "[resourceId('Microsoft.Web/connections', variables('TeamsConnectionName'))]", + "connectionName": "[variables('TeamsConnectionName')]", + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/teams')]" + }, + "azureblob": { + "connectionId": "[resourceId('Microsoft.Web/connections', variables('AzureblobConnectionName'))]", + "connectionName": "[variables('AzureblobConnectionName')]", + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/azureblob')]" + } + } + }, + "StorageContainerName": { + "value": "[parameters('StorageContainerName')]" + }, + "SasTokenTtlHours": { + "value": "[parameters('SasTokenTtlHours')]" + } + } + } + } + ] +} diff --git a/Solutions/RubrikSecurityCloud/Playbooks/RubrikIRViolation/README.md b/Solutions/RubrikSecurityCloud/Playbooks/RubrikIRViolation/README.md new file mode 100644 index 00000000000..d81ed72f845 --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Playbooks/RubrikIRViolation/README.md @@ -0,0 +1,78 @@ +# Rubrik IR Violation + +## Summary + +This playbook is triggered by a Microsoft Sentinel incident raised for a Rubrik Identity Resilience (IR) violation. It reads the violation ID from the incident and retrieves the latest violation details from Rubrik Security Cloud. It then posts a formatted notification to a SOC Microsoft Teams channel, adds a matching comment to the incident, and sets the violation status to `IN_PROGRESS` in Rubrik. + +### Flow detail + +1. **Read the violation ID** — the incident's alert Custom Details are parsed and `ViolationId` is read. If it is missing, the playbook comments on the incident and terminates. +2. **Authenticate** — the RSC service-account credentials are read from Key Vault and exchanged for a bearer token through the Rubrik custom connector. +3. **Get the violation** — `policyViolation` is queried with `policyTypes: [POLICY_TYPE_IDENTITY, POLICY_TYPE_IDP, POLICY_TYPE_IDENTITY_EVENT]`. The playbook extracts `violationSeverity`, `status`, `policy.name`, `policy.description`, `policy.policyType`, `policy.manualRemediationProcess` and, from `resourceMetadata.metadata`, the `displayName`, `userPrincipalName`, `idpType`, `domainName`, `principalType`, `privilegeType` and `title`. +4. **Notify the SOC in Teams** — a formatted message is posted to the configured channel carrying the violation ID, severity, identity display name and email, policy name, policy type, IdP type, the manual remediation steps returned by RSC, and a direct link to the Sentinel incident. +5. **Update the violation** — `bulkUpdatePolicyViolations` sets `POLICY_VIOLATION_STATUS_IN_PROGRESS`, and a matching comment is added to the incident. + +> **Note:** the identity is **not** notified by email. The notification goes to the SOC Teams channel configured by `TeamsGroupId` / `TeamsChannelId` so an analyst can act on the manual remediation steps. There is no Send Email action and no Office 365 Outlook connection in this template. + +### Prerequisites + +1. The Rubrik Security Cloud data connector should be configured to send appropriate events to Microsoft Sentinel. +2. The Rubrik Security Cloud solution should be configured to [connect to Rubrik Security Cloud API end points using a Service Account](https://docs.rubrik.com/en-us/saas/saas/polaris_api_access_with_service_accounts.html), the service account should be assigned a role that includes the relevant privileges necessary to read and update policy violations (see [Roles and Permissions](https://docs.rubrik.com/en-us/saas/saas/common/roles_and_permissions.html) in the Rubrik Security Cloud user guide). +3. Rubrik custom connector needs to be deployed prior to the deployment of this playbook, in the same resource group and region. Relevant instructions can be found in the connector doc page. +4. Store Service account credentials in Key Vault and obtain the keyvault name. + a. Create a Key Vault with a unique name. + b. Go to KeyVault -> secrets -> Generate/import and create 'Rubrik-AS-Int-ClientId' & 'Rubrik-AS-Int-ClientSecret' for storing client_id and client_secret respectively. +5. Obtain the Teams group id and channel id where the SOC notification will be posted. + +### Deployment instructions + +1. To deploy the Playbook, click the Deploy to Azure button. This will launch the ARM Template deployment wizard. +2. Fill in the required parameters: + * PlaybookName: Enter the playbook name here. + * API Hostname: Hostname of the Rubrik Security Cloud instance (e.g. customer.my.rubrik.com), without scheme or trailing slash. + * Rubrik Connector name: Name of the Rubrik Custom Connector deployed previously. + * KeyVaultName: Name of the keyvault where secrets are stored. + * TeamsGroupId: Id of the Teams Group where the SOC notification will be posted. + * TeamsChannelId: Id of the Teams Channel where the SOC notification will be posted. + +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2FAzure-Sentinel%2Fmaster%2FSolutions%2FRubrikSecurityCloud%2FPlaybooks%2FRubrikIRViolation%2Fazuredeploy.json) [![Deploy to Azure](https://aka.ms/deploytoazuregovbutton)](https://portal.azure.us/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2FAzure-Sentinel%2Fmaster%2FSolutions%2FRubrikSecurityCloud%2FPlaybooks%2FRubrikIRViolation%2Fazuredeploy.json) + +### Post-Deployment instructions + +#### a. Authorize connections + +Once deployment is complete, authorize each connection: Rubrik custom connector, Microsoft Teams, Key Vault and Microsoft Sentinel. + +1. Click the connection resource +2. Click edit API connection +3. Click Authorize +4. Sign in +5. Click Save +6. Repeat steps for other connections + +#### b. Assign roles to the playbook + +Assign roles to this playbook's managed identity. + +1. Go to Log Analytics Workspace → → Access Control → Add +2. Add role assignment +3. Assignment type: Job function roles -> Add 'Microsoft Sentinel Responder' as a Role +4. Members: select managed identity for assigned access to and add your logic app as member +5. Click on review+assign +6. Grant the playbook's managed identity the 'Key Vault Secrets User' role (or a get-secret access policy) on the Key Vault named by the KeyVaultName parameter. + +#### c. Configurations in Microsoft Sentinel + +1. In Microsoft Sentinel, analytical rules should be configured to trigger an incident. An incident should have the *ViolationId* - custom detail that contains the identity violation id generated in Rubrik. It can be obtained from the corresponding field in Rubrik IR Violation event logs. Check the [documentation](https://docs.microsoft.com/azure/sentinel/surface-custom-details-in-alerts) to learn more about adding custom details to incidents. `ViolationId` is the only Custom Detail this playbook requires. +2. Configure the automation rules to trigger the playbook. + +### Error handling + +Each RSC call checks both the HTTP status code and the presence of a non-empty `errors` key, because RSC returns GraphQL errors with HTTP 200. + +| Failure point | Behaviour | +|---------------|-----------| +| `ViolationId` missing from the incident | Comment on incident, terminate | +| Authentication | Comment on incident, terminate | +| Get IR Violation | Comment on incident, terminate | +| Update violation status | Comment on incident, terminate | diff --git a/Solutions/RubrikSecurityCloud/Playbooks/RubrikIRViolation/azuredeploy.json b/Solutions/RubrikSecurityCloud/Playbooks/RubrikIRViolation/azuredeploy.json new file mode 100644 index 00000000000..aa834c087a9 --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Playbooks/RubrikIRViolation/azuredeploy.json @@ -0,0 +1,713 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "title": "Rubrik IR Violation", + "description": "Incident-triggered playbook that reads a Rubrik IR (Identity) violation ID from the Microsoft Sentinel incident Custom Details, authenticates to Rubrik Security Cloud through the RubrikCustomConnector, retrieves the latest violation details via GraphQL, extracts the affected identity (userPrincipalName, displayName, idpType), the policy details (type, name, severity) and the policy's manualRemediationProcess, posts a formatted notification to a SOC Microsoft Teams channel, adds a matching comment to the incident, and sets the violation status to IN_PROGRESS. Every Rubrik API call is guarded with error handling that comments on the incident and terminates the run on failure.", + "prerequisites": [ + "1. The Rubrik Security Cloud solution should be configured to connect to Rubrik Security Cloud API end points using a Service Account (client_id + client_secret) with a role that includes the privileges to read and update policy violations.", + "2. The Rubrik custom connector (RubrikCustomConnector) needs to be deployed prior to the deployment of this playbook, in the same resource group and region. This playbook uses its 'Authentication' (/api/client_token) operation to obtain the RSC bearer token.", + "3. Store Service account credentials in Key Vault and obtain the Key Vault name and tenantId.", + "a. Create a Key Vault with a unique name.", + "b. Go to KeyVault -> secrets, click Generate/import and create 'Rubrik-AS-Int-ClientId' & 'Rubrik-AS-Int-ClientSecret' for storing client_id and client_secret respectively (same secrets used by the other Rubrik playbooks).", + "4. A Microsoft Teams team and channel are required for SOC notifications." + ], + "postDeployment": [ + "**a. Authorize connections**", + "Once deployment is complete, authorize each connection.", + "1. Go to your logic app -> API connections -> Select the connection resource (RubrikCustomConnector and Microsoft Teams).", + "2. Go to General -> edit API connection.", + "3. Click Authorize.", + "4. Sign in (for Teams, use an account that can post to the target SOC channel).", + "5. Click Save.", + "6. Repeat steps for each connection, and supply the TeamsGroupId and TeamsChannelId parameters.", + "**b. Assign Role to comment on and update incidents**", + "1. Go to Log Analytics Workspace -> -> Access Control -> Add.", + "2. Add role assignment.", + "3. Assignment type: Job function roles.", + "4. Role: Microsoft Sentinel Responder.", + "5. Members: select managed identity for assigned access to and add your logic app as member.", + "6. Click on review + assign.", + "**c. Add Access policy in Key Vault**", + "Add an access policy for the playbook's managed identity to read the RSC service-account secrets.", + "1. Go to logic app -> -> Identity -> System assigned managed identity and copy the Object (principal) ID.", + "2. Go to keyvaults -> -> Access policies -> Create.", + "3. Select Get on secrets permissions. Click next.", + "4. In the principal section, search by the copied object ID. Click next.", + "5. Click review + create.", + "**d. Configurations in Microsoft Sentinel**", + "1. In Microsoft Sentinel, configure the automation rules to trigger this playbook on incidents produced by the Rubrik IR-violation analytics rule." + ], + "prerequisitesDeployTemplateFile": "Azure-Sentinel/Solutions/RubrikSecurityCloud/Playbooks/RubrikCustomConnector/Rubrik_custom_conn.json", + "lastUpdateTime": "2026-08-05T00:00:00.000Z", + "entities": ["Account"], + "tags": [ "Microsoft Sentinel", "Incident", "Rubrik", "Identity Resilience", "Remediation" ], + "support": { + "tier": "community", + "armtemplate": "Generated from https://github.com/Azure/Azure-Sentinel/tree/master/Tools/Playbook-ARM-Template-Generator" + }, + "author": { + "name": "Rubrik" + } + }, + "parameters": { + "PlaybookName": { + "defaultValue": "RubrikIRViolation", + "type": "string", + "metadata": { "description": "Name of the Logic App / playbook resource." } + }, + "API Hostname": { + "type": "string", + "metadata": { "description": "Hostname of the Rubrik Security Cloud instance (e.g. customer.my.rubrik.com), without scheme or trailing slash." } + }, + "Rubrik Connector name": { + "defaultValue": "RubrikCustomConnector", + "type": "string", + "metadata": { "description": "Name of the previously deployed Rubrik custom connector (Microsoft.Web/customApis)." } + }, + "KeyVaultName": { + "type": "string", + "metadata": { "description": "Name of the Azure Key Vault that holds the Rubrik service-account credentials." } + }, + "TeamsGroupId": { + "type": "string", + "metadata": { "description": "Microsoft Teams group (Team) ID for the SOC channel used as the fallback notification target." } + }, + "TeamsChannelId": { + "type": "string", + "metadata": { "description": "Microsoft Teams channel ID within the Team used as the fallback notification target." } + } + }, + "variables": { + "MicrosoftSentinelConnectionName": "[concat('azuresentinel-', parameters('PlaybookName'))]", + "RubrikcustomconnectorConnectionName": "[concat('Rubrikcustomconnector-', parameters('PlaybookName'))]", + "KeyvaultConnectionName": "[concat('keyvault-', parameters('PlaybookName'))]", + "TeamsConnectionName": "[concat('teams-', parameters('PlaybookName'))]" + }, + "resources": [ + { + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[variables('MicrosoftSentinelConnectionName')]", + "location": "[resourceGroup().location]", + "kind": "V1", + "properties": { + "displayName": "[parameters('PlaybookName')]", + "customParameterValues": {}, + "parameterValueType": "Alternative", + "api": { + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/azuresentinel')]" + } + } + }, + { + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[variables('RubrikcustomconnectorConnectionName')]", + "location": "[resourceGroup().location]", + "properties": { + "displayName": "[variables('RubrikcustomconnectorConnectionName')]", + "customParameterValues": {}, + "api": { + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', parameters('Rubrik Connector name'))]" + } + } + }, + { + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[variables('KeyvaultConnectionName')]", + "location": "[resourceGroup().location]", + "kind": "V1", + "properties": { + "displayName": "[parameters('PlaybookName')]", + "parameterValueType": "Alternative", + "alternativeParameterValues": { + "vaultName": "[parameters('KeyVaultName')]" + }, + "customParameterValues": {}, + "api": { + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/keyvault')]" + } + } + }, + { + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[variables('TeamsConnectionName')]", + "location": "[resourceGroup().location]", + "properties": { + "displayName": "[parameters('PlaybookName')]", + "customParameterValues": {}, + "api": { + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/teams')]" + } + } + }, + { + "type": "Microsoft.Logic/workflows", + "apiVersion": "2017-07-01", + "name": "[parameters('PlaybookName')]", + "location": "[resourceGroup().location]", + "tags": { + "LogicAppsCategory": "security", + "hidden-SentinelTemplateName": "Rubrik-IR-Violation", + "hidden-SentinelTemplateVersion": "1.0" + }, + "identity": { "type": "SystemAssigned" }, + "dependsOn": [ + "[resourceId('Microsoft.Web/connections', variables('MicrosoftSentinelConnectionName'))]", + "[resourceId('Microsoft.Web/connections', variables('RubrikcustomconnectorConnectionName'))]", + "[resourceId('Microsoft.Web/connections', variables('KeyvaultConnectionName'))]", + "[resourceId('Microsoft.Web/connections', variables('TeamsConnectionName'))]" + ], + "properties": { + "state": "Enabled", + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "$connections": { "defaultValue": {}, "type": "Object" } + }, + "triggers": { + "Microsoft_Sentinel_incident": { + "type": "ApiConnectionWebhook", + "inputs": { + "body": { "callback_url": "@{listCallbackUrl()}" }, + "host": { + "connection": { "name": "@parameters('$connections')['azuresentinel']['connectionId']" } + }, + "path": "/incident-creation" + } + } + }, + "actions": { + "Initialize_Base_URL": { + "runAfter": {}, + "type": "InitializeVariable", + "inputs": { + "variables": [{ "name": "Base_URL", "type": "string", "value": "[concat('https://', parameters('API Hostname'))]" }] + } + }, + "Initialize_User_Agent": { + "runAfter": { "Initialize_Base_URL": ["Succeeded"] }, + "type": "InitializeVariable", + "inputs": { + "variables": [{ "name": "User_Agent", "type": "string", "value": "MSFTSentinel-1.0.0" }] + } + }, + "Initialize_Access_Token": { + "runAfter": { "Initialize_User_Agent": ["Succeeded"] }, + "type": "InitializeVariable", + "inputs": { + "variables": [{ "name": "Access_Token", "type": "string" }] + } + }, + "Initialize_Error_Messages": { + "runAfter": { "Initialize_Access_Token": ["Succeeded"] }, + "type": "InitializeVariable", + "inputs": { + "variables": [{ "name": "Error_Messages", "type": "array", "value": [] }] + } + }, + "Initialize_ViolationId": { + "runAfter": { "Initialize_Error_Messages": ["Succeeded"] }, + "type": "InitializeVariable", + "inputs": { + "variables": [{ "name": "ViolationId", "type": "string" }] + } + }, + "Initialize_UserPrincipalName": { + "runAfter": { "Initialize_ViolationId": ["Succeeded"] }, + "type": "InitializeVariable", + "inputs": { + "variables": [{ "name": "UserPrincipalName", "type": "string" }] + } + }, + "Initialize_DisplayName": { + "runAfter": { "Initialize_UserPrincipalName": ["Succeeded"] }, + "type": "InitializeVariable", + "inputs": { + "variables": [{ "name": "DisplayName", "type": "string" }] + } + }, + "Initialize_IdpType": { + "runAfter": { "Initialize_DisplayName": ["Succeeded"] }, + "type": "InitializeVariable", + "inputs": { + "variables": [{ "name": "IdpType", "type": "string" }] + } + }, + "Initialize_ManualRemediationProcess": { + "runAfter": { "Initialize_IdpType": ["Succeeded"] }, + "type": "InitializeVariable", + "inputs": { + "variables": [{ "name": "ManualRemediationProcess", "type": "string" }] + } + }, + "Initialize_PolicyType": { + "runAfter": { "Initialize_ManualRemediationProcess": ["Succeeded"] }, + "type": "InitializeVariable", + "inputs": { + "variables": [{ "name": "PolicyType", "type": "string" }] + } + }, + "Parse_JSON_-_Alert_Custom_Details": { + "runAfter": { "Initialize_PolicyType": ["Succeeded"] }, + "type": "ParseJson", + "inputs": { + "content": "@first(triggerBody()?['object']?['properties']?['alerts'])?['properties']?['additionalData']?['Custom Details']", + "schema": { + "type": "object", + "properties": { + "ViolationId": { + "type": "array", + "items": { "type": "string" } + } + } + } + } + }, + "Set_ViolationId": { + "runAfter": { "Parse_JSON_-_Alert_Custom_Details": ["Succeeded"] }, + "type": "SetVariable", + "inputs": { + "name": "ViolationId", + "value": "@{first(coalesce(body('Parse_JSON_-_Alert_Custom_Details')?['ViolationId'], json('[]')))}" + } + }, + "Check_ViolationId_present": { + "runAfter": { "Set_ViolationId": ["Succeeded"] }, + "type": "If", + "expression": { + "and": [ + { "equals": ["@variables('ViolationId')", ""] } + ] + }, + "actions": { + "Comment_-_No_ViolationId": { + "runAfter": {}, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik IR Violation failed.

No ViolationId was found in the incident alert Custom Details. Verify that the analytics rule maps ViolationId into Custom Details.

" + }, + "host": { "connection": { "name": "@parameters('$connections')['azuresentinel']['connectionId']" } }, + "method": "post", + "path": "/Incidents/Comment" + } + }, + "Terminate_-_No_ViolationId": { + "runAfter": { "Comment_-_No_ViolationId": ["Succeeded", "Failed"] }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "MissingViolationId", + "message": "No ViolationId present in the incident Custom Details." + } + } + } + }, + "else": { "actions": {} } + }, + "Get_Client_ID": { + "runAfter": { "Check_ViolationId_present": ["Succeeded"] }, + "type": "ApiConnection", + "inputs": { + "host": { "connection": { "name": "@parameters('$connections')['keyvault']['connectionId']" } }, + "method": "get", + "path": "/secrets/@{encodeURIComponent('Rubrik-AS-Int-ClientId')}/value" + }, + "runtimeConfiguration": { + "secureData": { "properties": ["inputs", "outputs"] } + } + }, + "Get_Client_Secret": { + "runAfter": { "Get_Client_ID": ["Succeeded"] }, + "type": "ApiConnection", + "inputs": { + "host": { "connection": { "name": "@parameters('$connections')['keyvault']['connectionId']" } }, + "method": "get", + "path": "/secrets/@{encodeURIComponent('Rubrik-AS-Int-ClientSecret')}/value" + }, + "runtimeConfiguration": { + "secureData": { "properties": ["inputs", "outputs"] } + } + }, + "Authentication": { + "runAfter": { "Get_Client_Secret": ["Succeeded"] }, + "type": "ApiConnection", + "inputs": { + "host": { "connection": { "name": "@parameters('$connections')['RubrikCustomConnector']['connectionId']" } }, + "method": "post", + "body": { + "client_id": "@body('Get_Client_ID')?['value']", + "client_secret": "@body('Get_Client_Secret')?['value']" + }, + "headers": { + "Content-Type": "application/json", + "User-Agent": "@variables('User_Agent')" + }, + "path": "/api/client_token" + }, + "runtimeConfiguration": { + "secureData": { "properties": ["inputs", "outputs"] } + } + }, + "Is_Authentication_Failed": { + "runAfter": { "Authentication": ["Succeeded", "Failed"] }, + "type": "If", + "expression": { + "or": [ + { "not": { "equals": ["@outputs('Authentication')['statusCode']", 200] } }, + { "equals": ["@empty(body('Authentication')?['access_token'])", true] } + ] + }, + "actions": { + "Append_auth_failure": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Authentication (/api/client_token) failed with status @{outputs('Authentication')?['statusCode']}. Verify the Key Vault secrets and the Rubrik custom connector." + } + }, + "Comment_auth_failure": { + "runAfter": { "Append_auth_failure": ["Succeeded"] }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik IR Violation failed.

@{join(variables('Error_Messages'), '
')}

" + }, + "host": { "connection": { "name": "@parameters('$connections')['azuresentinel']['connectionId']" } }, + "method": "post", + "path": "/Incidents/Comment" + } + }, + "Terminate_-_Authentication_failed": { + "runAfter": { "Comment_auth_failure": ["Succeeded", "Failed"] }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "AuthFailed", + "message": "@{join(variables('Error_Messages'), ' | ')}" + } + } + } + }, + "else": { + "actions": { + "Set_Access_Token": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Access_Token", + "value": "@{body('Authentication')?['access_token']}" + } + } + } + } + }, + "HTTP_-_Get_IR_Violation": { + "runAfter": { "Is_Authentication_Failed": ["Succeeded"] }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer @{variables('Access_Token')}", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "query DataSecurityViolationGetQuery($violationId: String!, $policyTypes: [PolicyType!]!) {\n policyViolation(violationId: $violationId, policyTypes: $policyTypes) {\n policyViolationId\n violationSeverity\n createdAt\n resourceId\n resourceType\n status\n lastUpdatedAt\n policy {\n policyId\n name\n description\n policySeverity\n policyType\n policyCategory\n frameworks\n manualRemediationProcess\n __typename\n }\n details {\n ... on IdentityViolationDetails {\n domainUniqueId\n principalUniqueId\n __typename\n }\n ... on IdpViolationDetails {\n domainUniqueId\n __typename\n }\n __typename\n }\n resourceMetadata {\n metadata {\n ... on IdentityMetadata {\n displayName\n domainName\n idpType\n principalType\n privilegeType\n userPrincipalName\n status\n title\n source\n identityTags\n uniqueId\n nativeType\n __typename\n }\n ... on IdpMetadata {\n domainName\n domainUniqueId\n idpType\n rootDomainName\n rootDomainId\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}", + "variables": { + "violationId": "@variables('ViolationId')", + "policyTypes": ["POLICY_TYPE_IDENTITY", "POLICY_TYPE_IDP", "POLICY_TYPE_IDENTITY_EVENT"] + } + } + }, + "runtimeConfiguration": { + "secureData": { "properties": ["inputs"] } + } + }, + "Is_Get_IR_Violation_Failed": { + "runAfter": { "HTTP_-_Get_IR_Violation": ["Succeeded", "Failed"] }, + "type": "If", + "expression": { + "or": [ + { "not": { "equals": ["@outputs('HTTP_-_Get_IR_Violation')['statusCode']", 200] } }, + { "not": { "equals": ["@empty(body('HTTP_-_Get_IR_Violation')?['errors'])", true] } }, + { "equals": ["@empty(body('HTTP_-_Get_IR_Violation')?['data']?['policyViolation'])", true] } + ] + }, + "actions": { + "Append_-_Get_violation_failed": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Get IR Violation failed for violation @{variables('ViolationId')} (status @{outputs('HTTP_-_Get_IR_Violation')?['statusCode']})." + } + }, + "Comment_-_Get_violation_failed": { + "runAfter": { "Append_-_Get_violation_failed": ["Succeeded"] }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik IR Violation failed.

@{join(variables('Error_Messages'), '
')}

" + }, + "host": { "connection": { "name": "@parameters('$connections')['azuresentinel']['connectionId']" } }, + "method": "post", + "path": "/Incidents/Comment" + } + }, + "Terminate_-_Get_violation_failed": { + "runAfter": { "Comment_-_Get_violation_failed": ["Succeeded", "Failed"] }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "GetViolationFailed", + "message": "@{join(variables('Error_Messages'), ' | ')}" + } + } + } + }, + "else": { + "actions": { + "Parse_JSON_-_Violation": { + "runAfter": {}, + "type": "ParseJson", + "inputs": { + "content": "@body('HTTP_-_Get_IR_Violation')", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "policyViolation": { + "type": "object", + "properties": { + "policyViolationId": { "type": "string" }, + "violationSeverity": { "type": "string" }, + "status": { "type": "string" }, + "policy": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "policyType": { "type": "string" }, + "manualRemediationProcess": { "type": "string" } + } + }, + "resourceMetadata": { + "type": "object", + "properties": { + "metadata": { + "type": "object", + "properties": { + "displayName": { "type": "string" }, + "idpType": { "type": "string" }, + "userPrincipalName": { "type": "string" } + } + } + } + } + } + } + } + } + } + } + } + }, + "Set_UserPrincipalName": { + "runAfter": { "Parse_JSON_-_Violation": ["Succeeded"] }, + "type": "SetVariable", + "inputs": { + "name": "UserPrincipalName", + "value": "@{coalesce(body('Parse_JSON_-_Violation')?['data']?['policyViolation']?['resourceMetadata']?['metadata']?['userPrincipalName'], '')}" + } + }, + "Set_DisplayName": { + "runAfter": { "Set_UserPrincipalName": ["Succeeded"] }, + "type": "SetVariable", + "inputs": { + "name": "DisplayName", + "value": "@{coalesce(body('Parse_JSON_-_Violation')?['data']?['policyViolation']?['resourceMetadata']?['metadata']?['displayName'], '')}" + } + }, + "Set_IdpType": { + "runAfter": { "Set_DisplayName": ["Succeeded"] }, + "type": "SetVariable", + "inputs": { + "name": "IdpType", + "value": "@{coalesce(body('Parse_JSON_-_Violation')?['data']?['policyViolation']?['resourceMetadata']?['metadata']?['idpType'], '')}" + } + }, + "Set_ManualRemediationProcess": { + "runAfter": { "Set_IdpType": ["Succeeded"] }, + "type": "SetVariable", + "inputs": { + "name": "ManualRemediationProcess", + "value": "@{coalesce(body('Parse_JSON_-_Violation')?['data']?['policyViolation']?['policy']?['manualRemediationProcess'], '')}" + } + }, + "Set_PolicyType": { + "runAfter": { "Set_ManualRemediationProcess": ["Succeeded"] }, + "type": "SetVariable", + "inputs": { + "name": "PolicyType", + "value": "@{coalesce(body('Parse_JSON_-_Violation')?['data']?['policyViolation']?['policy']?['policyType'], '')}" + } + }, + "Post_message_in_Teams_channel": { + "runAfter": { "Set_PolicyType": ["Succeeded"] }, + "type": "ApiConnection", + "inputs": { + "body": { + "recipient": { + "groupId": "[parameters('TeamsGroupId')]", + "channelId": "[parameters('TeamsChannelId')]" + }, + "messageBody": "

Rubrik IR Violation

Violation ID: @{variables('ViolationId')}

Violation Severity: @{body('Parse_JSON_-_Violation')?['data']?['policyViolation']?['violationSeverity']}
Name: @{if(empty(variables('DisplayName')), 'N/A', variables('DisplayName'))}
Email: @{if(empty(variables('UserPrincipalName')), 'N/A', variables('UserPrincipalName'))}
Policy Name: @{body('Parse_JSON_-_Violation')?['data']?['policyViolation']?['policy']?['name']}

Policy Type: @{variables('PolicyType')}

Idp Type: @{if(empty(variables('IdpType')), 'N/A', variables('IdpType'))}
Manual remediation steps: @{variables('ManualRemediationProcess')}
MS Sentinel Incident: @{triggerBody()?['object']?['properties']?['incidentUrl']}

" + }, + "host": { "connection": { "name": "@parameters('$connections')['teams']['connectionId']" } }, + "method": "post", + "path": "/beta/teams/conversation/message/poster/Flow bot/location/@{encodeURIComponent('channel')}" + } + }, + "HTTP_-_Update_Violation_Status": { + "runAfter": { "Post_message_in_Teams_channel": ["Succeeded"] }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Base_URL')}/api/graphql", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer @{variables('Access_Token')}", + "User-Agent": "@variables('User_Agent')" + }, + "body": { + "query": "mutation UpdatePolicyViolationsMutation($input: BulkUpdatePolicyViolationsInput!) {\n bulkUpdatePolicyViolations(input: $input)\n}", + "variables": { + "input": { + "newPolicyViolationStatus": "POLICY_VIOLATION_STATUS_IN_PROGRESS", + "policyViolationIds": ["@variables('ViolationId')"] + } + } + } + }, + "runtimeConfiguration": { + "secureData": { "properties": ["inputs"] } + } + }, + "Is_Update_Violation_Status_Failed": { + "runAfter": { "HTTP_-_Update_Violation_Status": ["Succeeded", "Failed"] }, + "type": "If", + "expression": { + "or": [ + { "not": { "equals": ["@outputs('HTTP_-_Update_Violation_Status')['statusCode']", 200] } }, + { "not": { "equals": ["@empty(body('HTTP_-_Update_Violation_Status')?['errors'])", true] } } + ] + }, + "actions": { + "Append_-_Update_status_failed": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Error_Messages", + "value": "Update violation status to IN_PROGRESS failed for violation @{variables('ViolationId')} (status @{outputs('HTTP_-_Update_Violation_Status')?['statusCode']})." + } + }, + "Comment_-_Update_status_failed": { + "runAfter": { "Append_-_Update_status_failed": ["Succeeded"] }, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik IR Violation partially completed.

The affected party was notified, but updating the Rubrik violation status failed:

@{join(variables('Error_Messages'), '
')}

" + }, + "host": { "connection": { "name": "@parameters('$connections')['azuresentinel']['connectionId']" } }, + "method": "post", + "path": "/Incidents/Comment" + } + }, + "Terminate_-_Update_status_failed": { + "runAfter": { "Comment_-_Update_status_failed": ["Succeeded", "Failed"] }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "UpdateStatusFailed", + "message": "@{join(variables('Error_Messages'), ' | ')}" + } + } + } + }, + "else": { + "actions": { + "Add_comment_to_incident_(V3)": { + "runAfter": {}, + "type": "ApiConnection", + "inputs": { + "body": { + "incidentArmId": "@triggerBody()?['object']?['id']", + "message": "

Rubrik IR Violation

Violation ID: @{variables('ViolationId')}
Name: @{variables('DisplayName')}
Email: @{if(empty(variables('UserPrincipalName')), 'N/A', variables('UserPrincipalName'))}
Violation Severity: @{body('Parse_JSON_-_Violation')?['data']?['policyViolation']?['violationSeverity']}
Policy Type: @{variables('PolicyType')}
Policy Name: @{body('Parse_JSON_-_Violation')?['data']?['policyViolation']?['policy']?['name']}
Recommended remediation steps: @{variables('ManualRemediationProcess')}

Violation Status: In Progress


The violation status was set to IN_PROGRESS in Rubrik and the notification was posted to the SOC Teams channel.

" + }, + "host": { "connection": { "name": "@parameters('$connections')['azuresentinel']['connectionId']" } }, + "method": "post", + "path": "/Incidents/Comment" + } + } + } + } + } + } + } + } + }, + "outputs": {} + }, + "parameters": { + "$connections": { + "value": { + "azuresentinel": { + "connectionId": "[resourceId('Microsoft.Web/connections', variables('MicrosoftSentinelConnectionName'))]", + "connectionName": "[variables('MicrosoftSentinelConnectionName')]", + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/azuresentinel')]", + "connectionProperties": { + "authentication": { "type": "ManagedServiceIdentity" } + } + }, + "RubrikCustomConnector": { + "connectionId": "[resourceId('Microsoft.Web/connections', variables('RubrikcustomconnectorConnectionName'))]", + "connectionName": "[variables('RubrikcustomconnectorConnectionName')]", + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', parameters('Rubrik Connector name'))]" + }, + "keyvault": { + "connectionId": "[resourceId('Microsoft.Web/connections', variables('KeyvaultConnectionName'))]", + "connectionName": "[variables('KeyvaultConnectionName')]", + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/keyvault')]", + "connectionProperties": { + "authentication": { "type": "ManagedServiceIdentity" } + } + }, + "teams": { + "connectionId": "[resourceId('Microsoft.Web/connections', variables('TeamsConnectionName'))]", + "connectionName": "[variables('TeamsConnectionName')]", + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/teams')]" + } + } + } + } + } + } + ] +} diff --git a/Solutions/RubrikSecurityCloud/Playbooks/RubrikQuarantineFiles/README.md b/Solutions/RubrikSecurityCloud/Playbooks/RubrikQuarantineFiles/README.md new file mode 100644 index 00000000000..75eae13d78d --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Playbooks/RubrikQuarantineFiles/README.md @@ -0,0 +1,216 @@ +# Rubrik Quarantine Files + +## Summary + +This is a child (HTTP Request-triggered) playbook that quarantines a batch of files reported in a Rubrik DSPM violation. The caller (RubrikDSPMViolationRemediation) passes the whole files array in a single call, and this playbook iterates internally and routes on `objectType`: OneDrive / SharePoint files are moved to a quarantine folder via Microsoft Graph, and Windows file-share (NTFS) files are quarantined on their host via Microsoft Defender for Endpoint. It returns a per-file result set to the caller. + +Template version: **3.2.0** (`hidden-SentinelTemplateVersion`). + +### Request and response contract + +The caller sends the whole batch in one call: + +```json +{ + "objectType": "O365_ONEDRIVE", + "files": [ + { + "filename": "OrgwideFile.json", + "filePath": "/Documents/CrestTesting/OrgwideFile.json", + "nativePath": "/Documents/CrestTesting/OrgwideFile.json", + "directory": "/Documents/CrestTesting", + "ownerUpn": "admin@contoso.com", + "objectType": "O365", + "physicalHost": "fileserver01.contoso.com", + "driveOwnerUpn": "alex.carter@contoso.com", + "siteUrl": "https://contoso.sharepoint.com/sites/Finance" + } + ] +} +``` + +The response carries the overall status, the per-file results and a top-level error summary: + +```json +{ + "status": "success", + "results": [ + { "filename": "OrgwideFile.json", "status": "Success", + "newLocation": "https://contoso.sharepoint.com/.../Quarantine/OrgwideFile.json", + "error": "" } + ], + "error": "" +} +``` + +`status` is `success` only when no per-file result is `Failed`. The caller uses exactly this value to decide whether the violation may be marked `REMEDIATED`. + +### Routing on objectType + +`objectType` is taken from the request body, falling back to the first file's own `objectType`, and is upper-cased before matching. Raw RSC object types are accepted directly — there is no need for the caller to normalise them: + +| Routed to | Matches | +|-----------|---------| +| OneDrive / SharePoint (Graph) | anything containing `ONEDRIVE`, plus `O365_SITE`, `O365_ONEDRIVE`, `O365_MAILBOX`, `O365_SHAREPOINT_DRIVE` | +| NTFS (Defender for Endpoint) | anything containing `WINDOWS_FILESET`, `WINDOWS_FILESHARE` or `NTFS` | +| Unsupported | anything else — reported as an unknown object type, nothing is modified | + +### How O365 files are located (v3.2.0) + +Each O365 file is resolved **directly by drive and path** — never by a tenant-wide search: + +1. The target drive comes from the RSC snappable identity supplied by the caller: + * `driveOwnerUpn` (from `O365Onedrive.userPrincipalName`) -> `GET /v1.0/users/{upn}/drive` + * `siteUrl` (from `O365Site.url`) -> `GET /v1.0/sites/{host}:{path}` -> `GET /v1.0/sites/{siteId}/drives` +2. The item comes from `filePath` (RSC `nativePath`). RSC may prefix this with the + document-library name (e.g. `/Documents/CrestTesting/report.csv`), but Graph's + `root:` **is** that library — so the playbook strips a leading segment matching the + resolved drive's own `name` before the lookup: + `GET /v1.0/drives/{driveId}/root:/{path}` + + The strip uses the actual drive name, so it works for any library name, is a no-op + for unprefixed paths, and will not remove a genuine folder that merely looks similar. + +Backslashes in `filePath` are normalised to forward slashes and any leading slash is removed before the lookup. + +#### Sites with multiple document libraries + +A SharePoint site can host more than the default `Documents` library, and the library +can vary **file by file within a single violation** (e.g. one violation containing both +`/Documents/report.csv` and `/TestLibrary2/hr-records.csv`). + +The playbook therefore enumerates **all** libraries for the site once per file batch via +`GET /v1.0/sites/{siteId}/drives`, then for each file matches the leading segment of +`filePath` against each library's `name` — and, as a fallback, the last segment of its +`webUrl` (these differ for renamed libraries, e.g. display name `Documents` vs URL +segment `Shared Documents`). The matched library supplies the `driveId` used for the +lookup, and the leading segment is then stripped from the path. + +If no library matches the leading segment, the playbook falls back to the site's default +library and treats the whole path as drive-relative — the original behavior, which is +correct for single-library sites and for paths that carry no library prefix. + +This is a per-file match against one already-fetched list, not a per-library probe: there +is exactly one extra-cost-free Graph call per site (`/drives` replaces `/drive`), and no +retry loop. The quarantine folder and the moved copy are created in the **same** library +as the source file, because every downstream call derives its drive from the resolved +item's own `parentReference.driveId`. + +If the file cannot be resolved at that exact drive and path, it is reported as `Failed` and **nothing is +modified**. There is deliberately no fallback to a filename search: a search cannot see files in a user's +private OneDrive under application permissions, and where the same filename exists elsewhere in the tenant +it can return — and act on — the wrong copy. + +> Requires the caller to send `driveOwnerUpn` / `siteUrl`. See RubrikDSPMViolationRemediation, which +> resolves them once per violation from the RSC `SnappableIdentity` (`hierarchyObject`) query and stamps +> them onto every file record. + +#### How the move is performed + +The file is **copied server-side and then deleted** — the bytes never pass through the Logic App: + +1. `POST /v1.0/drives/{sourceDriveId}/items/{sourceItemId}/copy` with the quarantine folder as `parentReference`. Graph returns `202 Accepted` and a `Location` header. +2. The monitor URL is polled every 3 seconds until the copy leaves `inProgress` / `notStarted`. +3. The new item is read back to capture its `webUrl`, which is returned as `newLocation`. +4. `DELETE /v1.0/drives/{sourceDriveId}/items/{sourceItemId}` completes the move. + +Because there is no download/upload step, there is no 250 MB simple-upload limit and no upload-session handling. + +> If the copy succeeds but the delete fails, the file is reported as **`Failed`** with +> "Uploaded to quarantine but failed to delete the source; delete manually." The copy in quarantine is +> left in place — this is a deliberate fail-safe, not a silent success. + +Files with the same name are renamed by Graph rather than overwritten. + +### How NTFS files are quarantined + +Windows file-share files are quarantined on their host through Microsoft Defender for Endpoint: + +1. `GET /api/machines?$filter=computerDnsName eq '{physicalHost}'` resolves the device. +2. An advanced hunting query over `DeviceFileEvents`, matched on `DeviceId`, `FileName` and `FolderPath`, obtains the file's SHA1 — MDE addresses files by hash, not by path. +3. `POST /api/machines/{machineId}/StopAndQuarantineFile` submits the action. +4. `GET /api/machineactions/{actionId}` is polled every 15 seconds until a terminal state is reached. Only `Succeeded` is reported as a success. + +> NTFS quarantine depends on `physicalHost`, which RSC populates only on **SecurityViolation** events. +> Classification events never carry it, so NTFS quarantine is not available on the caller's reduced flow. + +### Prerequisites + +1. A single Azure AD app registration used for BOTH Microsoft Graph and Microsoft Defender for Endpoint (MDE). It needs Microsoft Graph **application** permissions `Files.ReadWrite.All`, `Sites.ReadWrite.All`, `User.Read.All` AND WindowsDefenderATP (MDE) **application** permissions `Machine.ReadWrite.All` (or `Machine.StopAndQuarantine`) and `AdvancedQuery.Read.All`, all admin-consented. + + The same client id/secret is used to request two separate tokens via the client-credentials grant, one per scope: `https://graph.microsoft.com/.default` and `https://api.securitycenter.microsoft.com/.default`. +2. Store the app registration credentials in Key Vault and obtain the keyvault name. + a. Create a Key Vault with a unique name. + b. Go to KeyVault -> secrets -> Generate/import and create TWO secrets using **exactly** these names — they are fixed in the playbook and are **not** deployment parameters: + * `Graph-App-ClientId` — the app registration's Application (client) ID + * `Graph-App-ClientSecret` — the app registration's client secret VALUE + + The same credentials authenticate both Microsoft Graph and MDE. +3. Obtain the Teams group id and channel id where the quarantine-folder Adaptive Card will be posted (used by the OneDrive/SharePoint branch only). +4. For NTFS quarantine, the target file shares must be hosted on devices onboarded to Microsoft Defender for Endpoint, reachable by their physicalHost / computerDnsName. + +### Deployment instructions + +1. To deploy the Playbook, click the Deploy to Azure button. This will launch the ARM Template deployment wizard. +2. Fill in the required parameters: + * PlaybookName: Enter the playbook name here. Must match the RubrikQuarantineFilesPlaybookName parameter on the caller (RubrikDSPMViolationRemediation). + * TeamsGroupId: Id of the Teams Group where the quarantine-folder Adaptive Card is posted. + * TeamsChannelId: Id of the Teams Channel where the quarantine-folder Adaptive Card is posted. + * KeyVaultName: Name of the keyvault where the app registration credentials are stored. + * TenantId: Azure AD tenant ID used for the Microsoft Graph / MDE OAuth2 token endpoint. + +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2FAzure-Sentinel%2Fmaster%2FSolutions%2FRubrikSecurityCloud%2FPlaybooks%2FRubrikQuarantineFiles%2Fazuredeploy.json) [![Deploy to Azure](https://aka.ms/deploytoazuregovbutton)](https://portal.azure.us/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2FAzure-Sentinel%2Fmaster%2FSolutions%2FRubrikSecurityCloud%2FPlaybooks%2FRubrikQuarantineFiles%2Fazuredeploy.json) + +### Post-Deployment instructions + +#### a. Authorize connections + +Once deployment is complete, authorize each connection like teams, keyvault. + +1. Click the connection resource +2. Click edit API connection +3. Click Authorize +4. Sign in +5. Click Save +6. Repeat steps for other connections + +#### b. Assign role to access Key Vault secrets + +Assign a role to this playbook's managed identity. + +1. Go to the Key Vault named by KeyVaultName → Access Control (IAM) → Add +2. Add role assignment +3. Assignment type: Job function roles -> Add 'Key Vault Secrets User' as a Role (or add an access policy with Get on secrets) +4. Members: select managed identity for assigned access to and add your logic app as member +5. Click on review+assign + +#### c. Grant the app registration API permissions + +1. Create/point ONE app registration used for both Microsoft services and store its client id/secret in Key Vault under the fixed secret names `Graph-App-ClientId` and `Graph-App-ClientSecret`. +2. Grant it Microsoft Graph application permissions `Files.ReadWrite.All`, `Sites.ReadWrite.All`, `User.Read.All` AND WindowsDefenderATP application permissions `Machine.ReadWrite.All` (or `Machine.StopAndQuarantine`) and `AdvancedQuery.Read.All`, all admin-consented. +3. For NTFS quarantine, ensure the file-share hosts are onboarded to Microsoft Defender for Endpoint and resolvable by their physicalHost / computerDnsName. + +#### d. Wire up the caller + +This playbook has an HTTP Request trigger and does not run on its own. The caller RubrikDSPMViolationRemediation references it by the RubrikQuarantineFilesPlaybookName parameter and must be deployed in the same resource group. Ensure the PlaybookName here matches that value. + +#### e. Operator action at run time + +The OneDrive/SharePoint branch posts an Adaptive Card to the configured Teams channel and **waits** for a reply before touching any file. The operator pastes a share link to the destination quarantine folder (create the folder first, then use Share / Copy link). All files in the batch are moved into that single folder, whichever drive each file came from. + +If the share link cannot be resolved, the whole batch is reported as failed and **no file is modified**. + +### Error handling + +| Failure point | Behaviour | +|---------------|-----------| +| Graph or MDE authentication | Return a failed response to the caller and terminate | +| Quarantine folder share link not resolved | Whole batch reported failed; no file is touched | +| Missing both `driveOwnerUpn` and `siteUrl` | File reported `Failed` with "No drive owner UPN or site URL was supplied" | +| Missing `filePath` | File reported `Failed` | +| Drive or site unresolved | File reported `Failed` | +| Item not found at the exact drive + path | File reported `Failed`; nothing is modified, no search fallback | +| Copy not accepted or did not complete | File reported `Failed`; the source file is **not** deleted | +| Copy succeeded but delete failed | File reported `Failed`; quarantine copy left in place, delete the source manually | +| NTFS: no `physicalHost`, device not found, or SHA1 not found | File reported `Failed` with the specific reason | +| NTFS: machine action did not reach `Succeeded` | File reported `Failed` with the terminal state as the reason | diff --git a/Solutions/RubrikSecurityCloud/Playbooks/RubrikQuarantineFiles/azuredeploy.json b/Solutions/RubrikSecurityCloud/Playbooks/RubrikQuarantineFiles/azuredeploy.json new file mode 100644 index 00000000000..7b0eff483b3 --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Playbooks/RubrikQuarantineFiles/azuredeploy.json @@ -0,0 +1,2952 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "title": "Rubrik Quarantine Files", + "description": "This playbook is an HTTP Request-triggered child playbook that quarantines a batch of files reported by a Rubrik Security Cloud DSPM violation. It is called by the RubrikDSPMViolationRemediation playbook with the whole files array in a single call and iterates internally, so it runs once per incident instead of once per file. It branches on objectType into three paths: ONEDRIVE files are quarantined via Microsoft Graph (the analyst is asked once per run for a share link to a single destination quarantine folder via a Teams Adaptive Card; that link is resolved once up front with GET /v1.0/shares/{link}/driveItem into a destination driveId and item id, so the destination is decoupled from each file's source drive and no per-file folder lookup or create is performed. Each file is then resolved directly by drive and path - the drive from the RSC snappable identity, driveOwnerUpn for OneDrive or siteUrl for SharePoint, and the path from filePath/nativePath - downloaded, uploaded into the single destination folder with @microsoft.graph.conflictBehavior=rename so same-named files from different drives do not overwrite each other, and the source deleted only after a confirmed upload; if the file cannot be resolved at that exact path the file is reported Failed and is never matched by a tenant-wide search); NTFS (Windows Fileset) files are quarantined via Microsoft Defender for Endpoint (the device is resolved by physicalHost, each file's SHA1 is resolved via advanced hunting scoped by DeviceId, FileName and FolderPath, and a StopAndQuarantineFile machine action is created and polled for up to 20 minutes). Note that RSC supplies physicalHost only on SecurityViolation events, so Windows Fileset files arriving from a Classification event are reported Failed with an explanatory message rather than silently skipped; any other objectType returns NotImplemented. A single app registration (client id/secret in Key Vault) authenticates both Microsoft Graph and MDE; only the OAuth2 token scope differs per branch. The playbook returns { status, results: [ { filename, status, newLocation, error } ] } to the caller.", + "prerequisites": [ + "1. A single Azure AD app registration is used for BOTH Microsoft Graph and Microsoft Defender for Endpoint. Grant it (all admin-consented):", + "a. Microsoft Graph APPLICATION permissions: Files.ReadWrite.All, Sites.ReadWrite.All, User.Read.All.", + "b. WindowsDefenderATP (Microsoft Defender for Endpoint) APPLICATION permissions: Machine.ReadWrite.All (or Machine.StopAndQuarantine) and AdvancedQuery.Read.All.", + "2. Store the app registration credentials in Key Vault and obtain the Key Vault name and tenantId.", + "a. Create a Key Vault with a unique name.", + "b. Go to KeyVault -> secrets, click Generate/import and create TWO secrets using EXACTLY these names (they are fixed in the playbook and are not deployment parameters): 'Graph-App-ClientId' holding the app registration's Application (client) ID, and 'Graph-App-ClientSecret' holding its client secret VALUE. The same credentials authenticate both Microsoft Graph and Microsoft Defender for Endpoint.", + "NOTE: Make sure the Permission model in the Access Configuration of the Key Vault is set to 'Vault access policy'. If not, change it to 'Vault access policy'.", + "3. For the ONEDRIVE branch, a Microsoft Teams team and channel are required for the Adaptive Card that collects the quarantine folder share link.", + "3a. Create the destination quarantine folder in OneDrive or SharePoint before running the playbook and have its share link ready (Share -> Copy link). All files in a run are moved into that single folder regardless of which drive they came from, and same-named files are renamed rather than overwritten. The app registration must be able to read the link, so prefer a link scoped to people in your organization over a link restricted to named individuals. Exclude this quarantine folder from DSPM scan scope, otherwise the quarantined copies are scanned again and raise fresh violations for files that were just remediated.", + "4. For the NTFS branch, the target file shares must be hosted on devices onboarded to Microsoft Defender for Endpoint and resolvable by their physicalHost / computerDnsName. RSC supplies physicalHost only on SecurityViolation events; Windows Fileset files arriving from a Classification event cannot be quarantined and are reported Failed with an explanatory message. KNOWN LIMITATION: the file's SHA1 is resolved from the most recent DeviceFileEvents record (Defender retains roughly 30 days) and StopAndQuarantineFile is keyed by that hash. If the file has since been modified or deleted, Defender reports the action as successful even though nothing was quarantined.", + "5. This playbook is a prerequisite for the RubrikDSPMViolationRemediation playbook and must be deployed first, in the same resource group. RubrikDSPMViolationRemediation calls this playbook as a sub-playbook (via its RubrikQuarantineFilesPlaybookName parameter) once per incident to quarantine the violation files." + ], + "postDeployment": [ + "**a. Authorize connections**", + "Once deployment is complete, authorize each connection.", + "1. Go to your logic app -> API connections -> Select the Teams connection resource.", + "2. Go to General -> edit API connection.", + "3. Click Authorize.", + "4. Sign in.", + "5. Click Save.", + "6. The Key Vault connection uses the managed identity and does not require an interactive sign-in.", + "**b. Assign Role to comment on and update incidents (if the caller requires it)**", + "1. Go to Log Analytics Workspace -> -> Access Control -> Add.", + "2. Add role assignment.", + "3. Assignment type: Job function roles.", + "4. Role: Microsoft Sentinel Responder.", + "5. Members: select managed identity for assigned access to and add your logic app as member.", + "6. Click on review + assign.", + "**c. Add Access policy in Key Vault**", + "Add an access policy for the playbook's managed identity to read the app-registration secrets.", + "1. Go to logic app -> -> Identity -> System assigned managed identity and copy the Object (principal) ID.", + "2. Go to keyvaults -> -> Access policies -> Create.", + "3. Select Get on secrets permissions. Click next.", + "4. In the principal section, search by the copied object ID. Click next.", + "5. Click review + create.", + "**d. Grant the app registration API permissions**", + "1. In Entra ID -> App registrations -> -> API permissions, add the Microsoft Graph and WindowsDefenderATP application permissions listed in the prerequisites and grant admin consent.", + "2. For the NTFS branch, ensure the file-share hosts are onboarded to Microsoft Defender for Endpoint and resolvable by their physicalHost / computerDnsName.", + "**e. Configurations in Microsoft Sentinel**", + "1. This playbook has an HTTP Request trigger and is invoked by RubrikDSPMViolationRemediation; it does not attach directly to an automation rule.", + "2. Deploy this playbook before RubrikDSPMViolationRemediation. When deploying RubrikDSPMViolationRemediation, set its RubrikQuarantineFilesPlaybookName parameter to this Logic App's name so it can call this sub-playbook." + ], + "lastUpdateTime": "2026-10-04T00:00:00.000Z", + "entities": [], + "tags": [ + "Microsoft Sentinel", + "Rubrik", + "DSPM", + "Quarantine" + ], + "support": { + "tier": "community", + "armtemplate": "Generated from https://github.com/Azure/Azure-Sentinel/tree/master/Tools/Playbook-ARM-Template-Generator" + }, + "author": { + "name": "Rubrik" + } + }, + "parameters": { + "PlaybookName": { + "defaultValue": "RubrikQuarantineFiles", + "type": "string", + "metadata": { + "description": "Name of the Logic App / playbook resource. Must match RubrikQuarantineFilesPlaybookName on the caller." + } + }, + "TeamsGroupId": { + "type": "string", + "metadata": { + "description": "Microsoft Teams team (group) ID where the folder-name Adaptive Card is posted." + } + }, + "TeamsChannelId": { + "type": "string", + "metadata": { + "description": "Microsoft Teams channel ID (within TeamsGroupId) for the folder-name Adaptive Card." + } + }, + "KeyVaultName": { + "type": "string", + "metadata": { + "description": "Key Vault holding the Graph app registration client id and secret." + } + }, + "TenantId": { + "type": "string", + "metadata": { + "description": "Azure AD tenant ID used for the Microsoft Graph OAuth2 token endpoint." + } + } + }, + "variables": { + "TeamsConnectionName": "[concat('teams-', parameters('PlaybookName'))]", + "KeyvaultConnectionName": "[concat('keyvault-', parameters('PlaybookName'))]" + }, + "resources": [ + { + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[variables('TeamsConnectionName')]", + "location": "[resourceGroup().location]", + "kind": "V1", + "properties": { + "displayName": "[variables('TeamsConnectionName')]", + "customParameterValues": {}, + "api": { + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/teams')]" + } + } + }, + { + "type": "Microsoft.Web/connections", + "apiVersion": "2016-06-01", + "name": "[variables('KeyvaultConnectionName')]", + "location": "[resourceGroup().location]", + "kind": "V1", + "properties": { + "displayName": "[variables('KeyvaultConnectionName')]", + "customParameterValues": {}, + "parameterValueType": "Alternative", + "alternativeParameterValues": { + "vaultName": "[parameters('KeyVaultName')]" + }, + "api": { + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/keyvault')]" + } + } + }, + { + "type": "Microsoft.Logic/workflows", + "apiVersion": "2017-07-01", + "name": "[parameters('PlaybookName')]", + "location": "[resourceGroup().location]", + "tags": { + "LogicAppsCategory": "security", + "hidden-SentinelTemplateName": "RubrikQuarantineFiles", + "hidden-SentinelTemplateVersion": "3.2.0" + }, + "identity": { + "type": "SystemAssigned" + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/connections', variables('TeamsConnectionName'))]", + "[resourceId('Microsoft.Web/connections', variables('KeyvaultConnectionName'))]" + ], + "properties": { + "state": "Enabled", + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "$connections": { + "defaultValue": {}, + "type": "Object" + } + }, + "triggers": { + "manual": { + "type": "Request", + "kind": "Http", + "inputs": { + "schema": { + "type": "object", + "properties": { + "objectType": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "filename": { + "type": "string" + }, + "filePath": { + "type": "string" + }, + "nativePath": { + "type": "string" + }, + "directory": { + "type": "string" + }, + "ownerUpn": { + "type": "string" + }, + "objectType": { + "type": "string" + }, + "physicalHost": { + "type": "string" + }, + "driveOwnerUpn": { + "type": "string" + }, + "siteUrl": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "actions": { + "Initialize_Graph_Base_URL": { + "runAfter": {}, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Graph_Base_URL", + "type": "string", + "value": "https://graph.microsoft.com" + } + ] + } + }, + "Initialize_MDE_Base_URL": { + "runAfter": { + "Initialize_Graph_Base_URL": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "MDE_Base_URL", + "type": "string", + "value": "https://api.securitycenter.microsoft.com" + } + ] + } + }, + "Initialize_Object_Type": { + "runAfter": { + "Initialize_MDE_Base_URL": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Object_Type", + "type": "string", + "value": "@{toUpper(coalesce(triggerBody()?['objectType'], first(coalesce(triggerBody()?['files'], json('[]')))?['objectType'], ''))}" + } + ] + } + }, + "Initialize_Match_Type": { + "runAfter": { + "Initialize_Object_Type": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Match_Type", + "type": "string", + "value": "@{if(or(contains(variables('Object_Type'), 'ONEDRIVE'), equals(replace(variables('Object_Type'), '_', ''), 'O365SITE'), equals(replace(variables('Object_Type'), '_', ''), 'O365ONEDRIVE'), equals(replace(variables('Object_Type'), '_', ''), 'O365MAILBOX'), equals(replace(variables('Object_Type'), '_', ''), 'O365SHAREPOINTDRIVE')), 'ONEDRIVE', if(or(contains(replace(variables('Object_Type'), '_', ''), 'WINDOWSFILESET'), contains(replace(variables('Object_Type'), '_', ''), 'WINDOWSFILESHARE'), contains(variables('Object_Type'), 'NTFS')), 'NTFS', 'UNKNOWN'))}" + } + ] + } + }, + "Initialize_Overall_Status": { + "runAfter": { + "Initialize_Match_Type": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Overall_Status", + "type": "string", + "value": "Failed" + } + ] + } + }, + "Initialize_Results": { + "runAfter": { + "Initialize_Overall_Status": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Results", + "type": "array", + "value": [] + } + ] + } + }, + "Initialize_Access_Token": { + "runAfter": { + "Initialize_Results": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Access_Token", + "type": "string" + } + ] + } + }, + "Initialize_Quarantine_Share_Link": { + "runAfter": { + "Initialize_Access_Token": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Quarantine_Share_Link", + "type": "string" + } + ] + } + }, + "Initialize_Quarantine_Drive_Id": { + "runAfter": { + "Initialize_Quarantine_Share_Link": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Quarantine_Drive_Id", + "type": "string" + } + ] + } + }, + "Initialize_Quarantine_Item_Id": { + "runAfter": { + "Initialize_Quarantine_Drive_Id": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Quarantine_Item_Id", + "type": "string" + } + ] + } + }, + "Initialize_Quarantine_Folder_Label": { + "runAfter": { + "Initialize_Quarantine_Item_Id": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Quarantine_Folder_Label", + "type": "string" + } + ] + } + }, + "Initialize_Failure_Message": { + "runAfter": { + "Initialize_Quarantine_Folder_Label": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Failure_Message", + "type": "string" + } + ] + } + }, + "Initialize_File_Content": { + "runAfter": { + "Initialize_Failure_Message": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "File_Content", + "type": "string" + } + ] + } + }, + "Initialize_Matched_Resource": { + "runAfter": { + "Initialize_File_Content": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Matched_Resource", + "type": "object", + "value": {} + } + ] + } + }, + "Initialize_Target_Drive_Id": { + "runAfter": { + "Initialize_Matched_Resource": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Target_Drive_Id", + "type": "string" + } + ] + } + }, + "Initialize_Onedrive_File_Handled": { + "runAfter": { + "Initialize_Target_Drive_Id": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Onedrive_File_Handled", + "type": "boolean", + "value": false + } + ] + } + }, + "Initialize_Item_Path": { + "runAfter": { + "Initialize_Onedrive_File_Handled": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Item_Path", + "type": "string" + } + ] + } + }, + "Initialize_MDE_Access_Token": { + "runAfter": { + "Initialize_Item_Path": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "MDE_Access_Token", + "type": "string" + } + ] + } + }, + "Initialize_MDE_Machine_Id": { + "runAfter": { + "Initialize_MDE_Access_Token": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "MDE_Machine_Id", + "type": "string" + } + ] + } + }, + "Initialize_File_Sha1": { + "runAfter": { + "Initialize_MDE_Machine_Id": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "File_Sha1", + "type": "string" + } + ] + } + }, + "Initialize_MDE_Action_Id": { + "runAfter": { + "Initialize_File_Sha1": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "MDE_Action_Id", + "type": "string" + } + ] + } + }, + "Initialize_Ntfs_File_Handled": { + "runAfter": { + "Initialize_MDE_Action_Id": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Ntfs_File_Handled", + "type": "boolean", + "value": false + } + ] + } + }, + "Get_Graph_Client_Id": { + "runAfter": { + "Initialize_Item_Path_Final": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['keyvault']['connectionId']" + } + }, + "method": "get", + "path": "/secrets/@{encodeURIComponent('Graph-App-ClientId')}/value" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Get_Graph_Client_Secret": { + "runAfter": { + "Get_Graph_Client_Id": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['keyvault']['connectionId']" + } + }, + "method": "get", + "path": "/secrets/@{encodeURIComponent('Graph-App-ClientSecret')}/value" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Get_Graph_Token": { + "runAfter": { + "Get_Graph_Client_Secret": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "[uri(environment().authentication.loginEndpoint, format('{0}/oauth2/v2.0/token', parameters('TenantId')))]", + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "body": "grant_type=client_credentials&client_id=@{encodeURIComponent(body('Get_Graph_Client_Id')?['value'])}&client_secret=@{encodeURIComponent(body('Get_Graph_Client_Secret')?['value'])}&scope=@{encodeURIComponent(concat(variables('Graph_Base_URL'), '/.default'))}" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Check_Get_Graph_Token": { + "runAfter": { + "Get_Graph_Token": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "less": [ + "@coalesce(outputs('Get_Graph_Token')?['statusCode'], 0)", + 200 + ] + }, + { + "greaterOrEquals": [ + "@coalesce(outputs('Get_Graph_Token')?['statusCode'], 0)", + 300 + ] + } + ] + }, + "actions": { + "Set_Failure_Message_auth": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Failure_Message", + "value": "Failed to acquire a Microsoft Graph token (status @{coalesce(outputs('Get_Graph_Token')?['statusCode'], 'none')}). Verify the app registration credentials in Key Vault and the tenant id." + } + }, + "Set_Overall_Status_auth_failed": { + "runAfter": { + "Set_Failure_Message_auth": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Overall_Status", + "value": "Failed" + } + }, + "Response_auth_failed": { + "runAfter": { + "Set_Overall_Status_auth_failed": [ + "Succeeded" + ] + }, + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "status": "Failed", + "results": "@variables('Results')", + "error": "@variables('Failure_Message')" + } + } + }, + "Terminate_auth_failed": { + "runAfter": { + "Response_auth_failed": [ + "Succeeded" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "GraphAuthFailed", + "message": "@variables('Failure_Message')" + } + } + } + }, + "else": { + "actions": {} + } + }, + "Set_Access_Token": { + "runAfter": { + "Check_Get_Graph_Token": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Access_Token", + "value": "@{body('Get_Graph_Token')?['access_token']}" + } + }, + "Switch_on_object_type": { + "runAfter": { + "Set_Access_Token": [ + "Succeeded" + ] + }, + "type": "Switch", + "expression": "@variables('Match_Type')", + "cases": { + "ONEDRIVE": { + "case": "ONEDRIVE", + "actions": { + "Compose_folder_card": { + "runAfter": {}, + "type": "Compose", + "inputs": { + "type": "AdaptiveCard", + "version": "1.4", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "body": [ + { + "type": "TextBlock", + "weight": "Bolder", + "size": "Medium", + "wrap": true, + "text": "Rubrik DSPM Quarantine" + }, + { + "type": "TextBlock", + "wrap": true, + "text": "Paste a share link to the destination quarantine folder. @{length(coalesce(triggerBody()?['files'], json('[]')))} file(s) will be moved into that single folder, whichever drive (OneDrive or SharePoint document library) each file came from. Create the folder in OneDrive or SharePoint first, use Share / Copy link, and paste the link here. Files with the same name are renamed rather than overwritten." + }, + { + "type": "Input.Text", + "id": "quarantineShareLink", + "label": "Quarantine folder share link", + "placeholder": "e.g. https://contoso.sharepoint.com/:f:/s/Security/Ex4mpl3...", + "isRequired": true, + "errorMessage": "Please paste the share link of the destination quarantine folder." + } + ], + "actions": [ + { + "type": "Action.Submit", + "title": "Confirm" + } + ] + } + }, + "Post_folder_card_and_wait": { + "runAfter": { + "Compose_folder_card": [ + "Succeeded" + ] + }, + "type": "ApiConnectionWebhook", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['teams']['connectionId']" + } + }, + "body": { + "notificationUrl": "@{listCallbackUrl()}", + "body": { + "messageBody": "@{string(outputs('Compose_folder_card'))}", + "recipient": { + "groupId": "[parameters('TeamsGroupId')]", + "channelId": "[parameters('TeamsChannelId')]" + } + } + }, + "path": "/v1.0/teams/conversation/gatherinput/poster/Flow bot/location/@{encodeURIComponent('Channel')}/$subscriptions" + } + }, + "Set_Quarantine_Share_Link": { + "runAfter": { + "Post_folder_card_and_wait": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Quarantine_Share_Link", + "value": "@{trim(coalesce(body('Post_folder_card_and_wait')?['data']?['quarantineShareLink'], ''))}" + } + }, + "Resolve_quarantine_share_link": { + "runAfter": { + "Set_Quarantine_Share_Link": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{variables('Graph_Base_URL')}/v1.0/shares/u!@{replace(replace(replace(trim(base64(variables('Quarantine_Share_Link'))), '=', ''), '/', '_'), '+', '-')}/driveItem", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}" + }, + "queries": { + "$select": "id,name,folder,webUrl,parentReference" + } + } + }, + "Check_resolve_quarantine_share_link": { + "runAfter": { + "Resolve_quarantine_share_link": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "greaterOrEquals": [ + "@coalesce(outputs('Resolve_quarantine_share_link')?['statusCode'], 0)", + 200 + ] + }, + { + "less": [ + "@coalesce(outputs('Resolve_quarantine_share_link')?['statusCode'], 0)", + 300 + ] + }, + { + "not": { + "equals": [ + "@trim(coalesce(body('Resolve_quarantine_share_link')?['id'], ''))", + "" + ] + } + }, + { + "not": { + "equals": [ + "@trim(coalesce(body('Resolve_quarantine_share_link')?['parentReference']?['driveId'], ''))", + "" + ] + } + }, + { + "not": { + "equals": [ + "@string(coalesce(body('Resolve_quarantine_share_link')?['folder'], ''))", + "" + ] + } + } + ] + }, + "actions": { + "Set_Quarantine_Drive_Id": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Quarantine_Drive_Id", + "value": "@{body('Resolve_quarantine_share_link')?['parentReference']?['driveId']}" + } + }, + "Set_Quarantine_Item_Id": { + "runAfter": { + "Set_Quarantine_Drive_Id": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Quarantine_Item_Id", + "value": "@{body('Resolve_quarantine_share_link')?['id']}" + } + }, + "Set_Quarantine_Folder_Label": { + "runAfter": { + "Set_Quarantine_Item_Id": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Quarantine_Folder_Label", + "value": "@{coalesce(body('Resolve_quarantine_share_link')?['webUrl'], body('Resolve_quarantine_share_link')?['name'], 'quarantine folder')}" + } + } + }, + "else": { + "actions": { + "Set_Failure_Message_share_link": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Failure_Message", + "value": "@{if(empty(trim(coalesce(variables('Quarantine_Share_Link'), ''))), 'No quarantine folder share link was provided by the analyst. No files were quarantined.', concat('Could not resolve the quarantine folder from the supplied share link (Graph status ', string(coalesce(outputs('Resolve_quarantine_share_link')?['statusCode'], 'none')), '). Ensure the link points to a folder (not a file), that it has not expired, and that the app registration can read it. No files were quarantined. ', string(coalesce(body('Resolve_quarantine_share_link'), ''))))}" + } + } + } + } + }, + "If_quarantine_destination_resolved": { + "runAfter": { + "Check_resolve_quarantine_share_link": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "not": { + "equals": [ + "@variables('Quarantine_Item_Id')", + "" + ] + } + } + ] + }, + "actions": { + "Batch_get_site_by_url": { + "runAfter": {}, + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{variables('Graph_Base_URL')}/v1.0/sites/@{first(split(replace(replace(trim(coalesce(first(coalesce(triggerBody()?['files'], json('[]')))?['siteUrl'], '')), 'https://', ''), 'http://', ''), '/'))}:@{if(greater(length(split(replace(replace(trim(coalesce(first(coalesce(triggerBody()?['files'], json('[]')))?['siteUrl'], '')), 'https://', ''), 'http://', ''), '/')), 1), join(createArray('', join(skip(split(replace(replace(trim(coalesce(first(coalesce(triggerBody()?['files'], json('[]')))?['siteUrl'], '')), 'https://', ''), 'http://', ''), '/'), 1), '/')), '/'), '')}", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}" + } + } + }, + "Batch_get_site_drives": { + "runAfter": { + "Batch_get_site_by_url": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{variables('Graph_Base_URL')}/v1.0/sites/@{encodeURIComponent(coalesce(body('Batch_get_site_by_url')?['id'], ''))}/drives", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}" + } + } + }, + "Set_Site_Drives": { + "runAfter": { + "Batch_get_site_drives": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Site_Drives", + "value": "@coalesce(body('Batch_get_site_drives')?['value'], json('[]'))" + } + }, + "For_each_file": { + "runAfter": { + "Set_Site_Drives": [ + "Succeeded" + ] + }, + "type": "Foreach", + "foreach": "@coalesce(triggerBody()?['files'], json('[]'))", + "runtimeConfiguration": { + "concurrency": { + "repetitions": 1 + } + }, + "actions": { + "Reset_Onedrive_File_Handled": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Onedrive_File_Handled", + "value": false + } + }, + "Reset_Target_Drive_Id": { + "runAfter": { + "Reset_Onedrive_File_Handled": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Target_Drive_Id", + "value": "@null" + } + }, + "Reset_Matched_Resource": { + "runAfter": { + "Reset_Item_Path_Final": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Matched_Resource", + "value": "@null" + } + }, + "Set_Item_Path": { + "runAfter": { + "Reset_Matched_Resource": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Item_Path", + "value": "@{if(startsWith(replace(trim(coalesce(items('For_each_file')?['filePath'], '')), '\\', '/'), '/'), substring(replace(trim(coalesce(items('For_each_file')?['filePath'], '')), '\\', '/'), 1, sub(length(replace(trim(coalesce(items('For_each_file')?['filePath'], '')), '\\', '/')), 1)), replace(trim(coalesce(items('For_each_file')?['filePath'], '')), '\\', '/'))}" + } + }, + "Check_resolution_inputs": { + "runAfter": { + "Set_Item_Path": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "and": [ + { + "equals": [ + "@trim(coalesce(items('For_each_file')?['driveOwnerUpn'], ''))", + "" + ] + }, + { + "equals": [ + "@trim(coalesce(items('For_each_file')?['siteUrl'], ''))", + "" + ] + } + ] + } + ] + }, + "actions": { + "Append_result_no_drive_target": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "", + "error": "No drive owner UPN or site URL was supplied for this file, and no file path is available; cannot resolve the target drive without a tenant-wide search. Ensure the calling playbook sends driveOwnerUpn (OneDrive) or siteUrl (SharePoint) from the RSC snappable." + } + } + }, + "Set_Handled_no_drive_target": { + "runAfter": { + "Append_result_no_drive_target": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Onedrive_File_Handled", + "value": true + } + } + }, + "else": { + "actions": {} + } + }, + "Check_file_path_present": { + "runAfter": { + "Check_resolution_inputs": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Onedrive_File_Handled')", + false + ] + }, + { + "equals": [ + "@variables('Item_Path')", + "" + ] + } + ] + }, + "actions": { + "Append_result_no_file_path": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "", + "error": "No filePath (RSC nativePath) was supplied for this file; cannot resolve it by path. Source NOT modified." + } + } + }, + "Set_Handled_no_file_path": { + "runAfter": { + "Append_result_no_file_path": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Onedrive_File_Handled", + "value": true + } + } + }, + "else": { + "actions": {} + } + }, + "Resolve_target_drive": { + "runAfter": { + "Check_file_path_present": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Onedrive_File_Handled')", + false + ] + }, + { + "not": { + "equals": [ + "@trim(coalesce(items('For_each_file')?['driveOwnerUpn'], ''))", + "" + ] + } + } + ] + }, + "actions": { + "Get_user_drive": { + "runAfter": {}, + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{variables('Graph_Base_URL')}/v1.0/users/@{encodeURIComponent(trim(items('For_each_file')?['driveOwnerUpn']))}/drive", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}" + } + } + }, + "Check_Get_user_drive": { + "runAfter": { + "Get_user_drive": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "greaterOrEquals": [ + "@coalesce(outputs('Get_user_drive')?['statusCode'], 0)", + 200 + ] + }, + { + "less": [ + "@coalesce(outputs('Get_user_drive')?['statusCode'], 0)", + 300 + ] + }, + { + "not": { + "equals": [ + "@trim(coalesce(body('Get_user_drive')?['id'], ''))", + "" + ] + } + } + ] + }, + "actions": { + "Set_Target_Drive_Id_onedrive": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Target_Drive_Id", + "value": "@{body('Get_user_drive')?['id']}" + } + }, + "Set_Drive_Name_onedrive": { + "runAfter": { + "Set_Target_Drive_Id_onedrive": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Drive_Name", + "value": "@{coalesce(body('Get_user_drive')?['name'], '')}" + } + } + }, + "else": { + "actions": { + "Append_result_onedrive_drive_unresolved": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "", + "error": "Could not resolve the OneDrive for owner '@{items('For_each_file')?['driveOwnerUpn']}' (Graph status @{coalesce(outputs('Get_user_drive')?['statusCode'], 'none')}). Verify the user exists, has a provisioned OneDrive, and that the app registration holds Files.ReadWrite.All. @{string(coalesce(body('Get_user_drive'), ''))}" + } + } + }, + "Set_Handled_onedrive_drive_unresolved": { + "runAfter": { + "Append_result_onedrive_drive_unresolved": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Onedrive_File_Handled", + "value": true + } + } + } + } + } + }, + "else": { + "actions": { + "If_site_url_provided": { + "runAfter": {}, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Onedrive_File_Handled')", + false + ] + }, + { + "not": { + "equals": [ + "@trim(coalesce(items('For_each_file')?['siteUrl'], ''))", + "" + ] + } + } + ] + }, + "actions": { + "Check_Get_site_by_url_response": { + "runAfter": {}, + "type": "If", + "expression": { + "and": [ + { + "not": { + "equals": [ + "@trim(coalesce(body('Batch_get_site_by_url')?['id'], ''))", + "" + ] + } + } + ] + }, + "actions": { + "Check_Get_site_drive": { + "runAfter": {}, + "type": "If", + "expression": { + "and": [ + { + "greater": [ + "@length(coalesce(variables('Site_Drives'), json('[]')))", + 0 + ] + } + ] + }, + "actions": { + "Filter_drives_by_name": { + "runAfter": {}, + "type": "Query", + "inputs": { + "from": "@coalesce(variables('Site_Drives'), json('[]'))", + "where": "@equals(toLower(trim(coalesce(item()?['name'], ''))), toLower(first(split(variables('Item_Path'), '/'))))" + } + }, + "Filter_drives_by_weburl": { + "runAfter": { + "Filter_drives_by_name": [ + "Succeeded" + ] + }, + "type": "Query", + "inputs": { + "from": "@coalesce(variables('Site_Drives'), json('[]'))", + "where": "@endsWith(toLower(replace(coalesce(item()?['webUrl'], ''), '%20', ' ')), toLower(concat('/', first(split(coalesce(variables('Item_Path'), ''), '/')))))" + } + }, + "Set_Matched_Drive_sharepoint": { + "runAfter": { + "Filter_drives_by_weburl": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Matched_Drive", + "value": "@if(greater(length(body('Filter_drives_by_name')), 0), first(body('Filter_drives_by_name')), if(greater(length(body('Filter_drives_by_weburl')), 0), first(body('Filter_drives_by_weburl')), json('{}')))" + } + }, + "Set_Target_Drive_Id_sharepoint": { + "runAfter": { + "Set_Matched_Drive_sharepoint": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Target_Drive_Id", + "value": "@{coalesce(variables('Matched_Drive')?['id'], '')}" + } + }, + "Set_Drive_Name_sharepoint": { + "runAfter": { + "Set_Target_Drive_Id_sharepoint": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Drive_Name", + "value": "@{if(equals(trim(coalesce(variables('Matched_Drive')?['id'], '')), ''), '', first(split(variables('Item_Path'), '/')))}" + } + }, + "Check_library_matched": { + "type": "If", + "runAfter": { + "Set_Drive_Name_sharepoint": [ + "Succeeded" + ] + }, + "expression": { + "and": [ + { + "equals": [ + "@trim(coalesce(variables('Target_Drive_Id'), ''))", + "" + ] + } + ] + }, + "actions": { + "Append_result_library_unmatched": { + "type": "AppendToArrayVariable", + "runAfter": {}, + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "", + "error": "No document library on site '@{items('For_each_file')?['siteUrl']}' matches the first segment of the file path '@{variables('Item_Path')}' (by name or URL). Nothing was modified." + } + } + }, + "Set_Handled_library_unmatched": { + "type": "SetVariable", + "runAfter": { + "Append_result_library_unmatched": [ + "Succeeded" + ] + }, + "inputs": { + "name": "Onedrive_File_Handled", + "value": true + } + } + }, + "else": { + "actions": {} + } + } + }, + "else": { + "actions": { + "Append_result_sharepoint_drive_unresolved": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "", + "error": "Resolved the SharePoint site but could not enumerate its document libraries (Graph status @{coalesce(outputs('Batch_get_site_drives')?['statusCode'], 'none')}). @{string(coalesce(body('Batch_get_site_drives'), ''))}" + } + } + }, + "Set_Handled_sharepoint_drive_unresolved": { + "runAfter": { + "Append_result_sharepoint_drive_unresolved": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Onedrive_File_Handled", + "value": true + } + } + } + } + } + }, + "else": { + "actions": { + "Append_result_sharepoint_site_unresolved": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "", + "error": "Could not resolve the SharePoint site '@{items('For_each_file')?['siteUrl']}' (Graph status @{coalesce(outputs('Batch_get_site_by_url')?['statusCode'], 'none')}). Verify the site URL from the RSC snappable and that the app registration holds Sites.ReadWrite.All. @{string(coalesce(body('Batch_get_site_by_url'), ''))}" + } + } + }, + "Set_Handled_sharepoint_site_unresolved": { + "runAfter": { + "Append_result_sharepoint_site_unresolved": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Onedrive_File_Handled", + "value": true + } + } + } + } + } + }, + "else": { + "actions": {} + } + } + } + } + }, + "Resolve_item_by_path": { + "runAfter": { + "Resolve_target_drive": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Onedrive_File_Handled')", + false + ] + }, + { + "not": { + "equals": [ + "@trim(coalesce(variables('Target_Drive_Id'), ''))", + "" + ] + } + } + ] + }, + "actions": { + "Set_Item_Path_final": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Item_Path_Final", + "value": "@{if(and(not(equals(trim(coalesce(variables('Drive_Name'), '')), '')), startsWith(toLower(coalesce(variables('Item_Path'), '')), toLower(concat(coalesce(variables('Drive_Name'), ''), '/')))), substring(coalesce(variables('Item_Path'), ''), add(length(coalesce(variables('Drive_Name'), '')), 1), sub(length(coalesce(variables('Item_Path'), '')), add(length(coalesce(variables('Drive_Name'), '')), 1))), coalesce(variables('Item_Path'), ''))}" + } + }, + "Get_item_by_path": { + "runAfter": { + "Set_Item_Path_final": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{variables('Graph_Base_URL')}/v1.0/drives/@{encodeURIComponent(variables('Target_Drive_Id'))}/root:/@{replace(encodeURIComponent(variables('Item_Path_Final')), '%2F', '/')}", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}" + }, + "queries": { + "$select": "id,name,size,file,folder,parentReference,webUrl" + } + } + }, + "Check_Get_item_by_path": { + "runAfter": { + "Get_item_by_path": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "greaterOrEquals": [ + "@coalesce(outputs('Get_item_by_path')?['statusCode'], 0)", + 200 + ] + }, + { + "less": [ + "@coalesce(outputs('Get_item_by_path')?['statusCode'], 0)", + 300 + ] + }, + { + "not": { + "equals": [ + "@trim(coalesce(body('Get_item_by_path')?['id'], ''))", + "" + ] + } + }, + { + "not": { + "equals": [ + "@string(coalesce(body('Get_item_by_path')?['file'], ''))", + "" + ] + } + } + ] + }, + "actions": { + "Set_Matched_Resource": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Matched_Resource", + "value": "@body('Get_item_by_path')" + } + } + }, + "else": { + "actions": { + "Append_result_file_not_found_at_path": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "", + "error": "File not found at path '@{variables('Item_Path_Final')}' in document library '@{coalesce(variables('Matched_Drive')?['name'], variables('Drive_Name'), 'default')}' (drive '@{variables('Target_Drive_Id')}') (Graph status @{coalesce(outputs('Get_item_by_path')?['statusCode'], 'none')}). The playbook resolves files by exact drive and path and does not fall back to a tenant-wide search, so nothing was modified. Verify the RSC nativePath matches the current location of the file. @{string(coalesce(body('Get_item_by_path'), ''))}" + } + } + }, + "Set_Handled_file_not_found_at_path": { + "runAfter": { + "Append_result_file_not_found_at_path": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Onedrive_File_Handled", + "value": true + } + } + } + } + } + }, + "else": { + "actions": {} + } + }, + "If_file_found": { + "runAfter": { + "Resolve_item_by_path": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Onedrive_File_Handled')", + false + ] + }, + { + "not": { + "equals": [ + "@coalesce(variables('Matched_Resource')?['id'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Copy_to_quarantine": { + "type": "Http", + "runAfter": {}, + "operationOptions": "DisableAsyncPattern", + "inputs": { + "method": "POST", + "uri": "@{variables('Graph_Base_URL')}/v1.0/drives/@{encodeURIComponent(variables('Matched_Resource')?['parentReference']?['driveId'])}/items/@{encodeURIComponent(variables('Matched_Resource')?['id'])}/copy?@{concat('@', 'microsoft.graph.conflictBehavior')}=rename", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json" + }, + "body": { + "parentReference": { + "driveId": "@{variables('Quarantine_Drive_Id')}", + "id": "@{variables('Quarantine_Item_Id')}" + } + } + } + }, + "Check_copy_accepted": { + "type": "If", + "runAfter": { + "Copy_to_quarantine": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "expression": { + "and": [ + { + "equals": [ + "@coalesce(outputs('Copy_to_quarantine')?['statusCode'], 0)", + 202 + ] + }, + { + "not": { + "equals": [ + "@trim(coalesce(outputs('Copy_to_quarantine')?['headers']?['Location'], ''))", + "" + ] + } + } + ] + }, + "actions": { + "Until_copy_finished": { + "type": "Until", + "runAfter": {}, + "limit": { + "count": 200, + "timeout": "PT10M" + }, + "expression": "@not(contains(json('[\"notStarted\",\"inProgress\",\"waiting\"]'), coalesce(body('Get_copy_status')?['status'], 'inProgress')))", + "actions": { + "Get_copy_status": { + "type": "Http", + "runAfter": {}, + "inputs": { + "method": "GET", + "uri": "@{outputs('Copy_to_quarantine')?['headers']?['Location']}" + } + }, + "If_copy_pending": { + "type": "If", + "runAfter": { + "Get_copy_status": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "expression": { + "and": [ + { + "equals": [ + "@contains(json('[\"notStarted\",\"inProgress\",\"waiting\"]'), coalesce(body('Get_copy_status')?['status'], 'inProgress'))", + true + ] + } + ] + }, + "actions": { + "Wait_3_seconds": { + "type": "Wait", + "runAfter": {}, + "inputs": { + "interval": { + "count": 3, + "unit": "Second" + } + } + } + }, + "else": { + "actions": {} + } + } + } + }, + "Check_copy_completed": { + "type": "If", + "runAfter": { + "Until_copy_finished": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "expression": { + "and": [ + { + "equals": [ + "@coalesce(body('Get_copy_status')?['status'], '')", + "completed" + ] + }, + { + "not": { + "equals": [ + "@trim(coalesce(body('Get_copy_status')?['resourceId'], ''))", + "" + ] + } + } + ] + }, + "actions": { + "Get_quarantine_item": { + "type": "Http", + "runAfter": {}, + "inputs": { + "method": "GET", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}" + }, + "queries": { + "$select": "id,name,webUrl" + }, + "uri": "@{variables('Graph_Base_URL')}/v1.0/drives/@{encodeURIComponent(variables('Quarantine_Drive_Id'))}/items/@{encodeURIComponent(body('Get_copy_status')?['resourceId'])}" + } + }, + "Delete_source_file": { + "runAfter": { + "Get_quarantine_item": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "Http", + "inputs": { + "method": "DELETE", + "uri": "@{variables('Graph_Base_URL')}/v1.0/drives/@{encodeURIComponent(variables('Matched_Resource')?['parentReference']?['driveId'])}/items/@{encodeURIComponent(variables('Matched_Resource')?['id'])}", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}" + } + } + }, + "Append_result_success": { + "runAfter": { + "Delete_source_file": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "@if(and(greaterOrEquals(coalesce(outputs('Delete_source_file')?['statusCode'], 0), 200), less(coalesce(outputs('Delete_source_file')?['statusCode'], 0), 300)), 'Success', 'Failed')", + "newLocation": "@coalesce(body('Get_quarantine_item')?['webUrl'], '')", + "error": "@if(and(greaterOrEquals(coalesce(outputs('Delete_source_file')?['statusCode'], 0), 200), less(coalesce(outputs('Delete_source_file')?['statusCode'], 0), 300)), '', concat('Uploaded to quarantine but failed to delete the source (Graph status ', string(coalesce(outputs('Delete_source_file')?['statusCode'], 'none')), '); delete manually.'))" + } + } + }, + "Set_Handled_success": { + "type": "SetVariable", + "runAfter": { + "Append_result_success": [ + "Succeeded" + ] + }, + "inputs": { + "name": "Onedrive_File_Handled", + "value": true + } + } + }, + "else": { + "actions": { + "Append_result_copy_incomplete": { + "type": "AppendToArrayVariable", + "runAfter": {}, + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "", + "error": "The server-side copy to the quarantine folder '@{variables('Quarantine_Folder_Label')}' did not complete within 10 minutes (copy status '@{coalesce(body('Get_copy_status')?['status'], '')}'). Source NOT deleted. @{string(coalesce(body('Get_copy_status')?['error'], ''))}" + } + } + }, + "Set_Handled_copy_incomplete": { + "type": "SetVariable", + "runAfter": { + "Append_result_copy_incomplete": [ + "Succeeded" + ] + }, + "inputs": { + "name": "Onedrive_File_Handled", + "value": true + } + } + } + } + } + }, + "else": { + "actions": { + "Append_result_copy_failed": { + "type": "AppendToArrayVariable", + "runAfter": {}, + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "", + "error": "The server-side copy to the quarantine folder '@{variables('Quarantine_Folder_Label')}' could not be started (Graph status @{coalesce(outputs('Copy_to_quarantine')?['statusCode'], 'none')}). Source NOT deleted. @{string(coalesce(body('Copy_to_quarantine')?['error'], ''))}" + } + } + }, + "Set_Handled_copy_failed": { + "type": "SetVariable", + "runAfter": { + "Append_result_copy_failed": [ + "Succeeded" + ] + }, + "inputs": { + "name": "Onedrive_File_Handled", + "value": true + } + } + } + } + } + }, + "else": { + "actions": {} + } + }, + "Reset_Drive_Name": { + "runAfter": { + "Reset_Target_Drive_Id": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Drive_Name", + "value": "@null" + } + }, + "Reset_Matched_Drive": { + "runAfter": { + "Reset_Drive_Name": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Matched_Drive", + "value": "@null" + } + }, + "Reset_Item_Path_Final": { + "runAfter": { + "Reset_Matched_Drive": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Item_Path_Final", + "value": "@null" + } + } + } + }, + "Filter_failed_results": { + "runAfter": { + "For_each_file": [ + "Succeeded" + ] + }, + "type": "Query", + "inputs": { + "from": "@variables('Results')", + "where": "@not(equals(item()?['status'], 'Success'))" + } + }, + "Set_Overall_Status_from_results": { + "runAfter": { + "Filter_failed_results": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Overall_Status", + "value": "@if(equals(length(variables('Results')), 0), 'Success', if(equals(length(body('Filter_failed_results')), 0), 'Success', if(equals(length(body('Filter_failed_results')), length(variables('Results'))), 'Failed', 'PartialSuccess')))" + } + }, + "Set_Failure_Message_from_results": { + "runAfter": { + "Set_Overall_Status_from_results": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Failure_Message", + "value": "@{if(equals(length(body('Filter_failed_results')), 0), '', concat(string(length(body('Filter_failed_results'))), ' of ', string(length(variables('Results'))), ' file(s) failed to quarantine. ', string(body('Filter_failed_results'))))}" + } + } + }, + "else": { + "actions": { + "Set_Overall_Status_no_folder": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Overall_Status", + "value": "Failed" + } + }, + "Append_no_folder_error": { + "runAfter": { + "Set_Overall_Status_no_folder": [ + "Succeeded" + ] + }, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "", + "status": "Failed", + "newLocation": "", + "error": "@variables('Failure_Message')" + } + } + } + } + } + } + } + }, + "NTFS": { + "case": "NTFS", + "actions": { + "Get_MDE_Token": { + "runAfter": {}, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "[uri(environment().authentication.loginEndpoint, format('{0}/oauth2/v2.0/token', parameters('TenantId')))]", + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "body": "grant_type=client_credentials&client_id=@{encodeURIComponent(body('Get_Graph_Client_Id')?['value'])}&client_secret=@{encodeURIComponent(body('Get_Graph_Client_Secret')?['value'])}&scope=@{encodeURIComponent(concat(variables('MDE_Base_URL'), '/.default'))}" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Check_Get_MDE_Token": { + "runAfter": { + "Get_MDE_Token": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "less": [ + "@coalesce(outputs('Get_MDE_Token')?['statusCode'], 0)", + 200 + ] + }, + { + "greaterOrEquals": [ + "@coalesce(outputs('Get_MDE_Token')?['statusCode'], 0)", + 300 + ] + } + ] + }, + "actions": { + "Set_Failure_Message_mde_auth": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Failure_Message", + "value": "Failed to acquire a Microsoft Defender for Endpoint token (status @{coalesce(outputs('Get_MDE_Token')?['statusCode'], 'none')}). Verify the app registration has WindowsDefenderATP application permissions and the credentials in Key Vault." + } + }, + "Set_Overall_Status_mde_auth_failed": { + "runAfter": { + "Set_Failure_Message_mde_auth": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Overall_Status", + "value": "Failed" + } + }, + "Response_mde_auth_failed": { + "runAfter": { + "Set_Overall_Status_mde_auth_failed": [ + "Succeeded" + ] + }, + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "status": "Failed", + "results": "@variables('Results')", + "error": "@variables('Failure_Message')" + } + } + }, + "Terminate_mde_auth_failed": { + "runAfter": { + "Response_mde_auth_failed": [ + "Succeeded" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "MdeAuthFailed", + "message": "@variables('Failure_Message')" + } + } + } + }, + "else": { + "actions": { + "Set_MDE_Access_Token": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "MDE_Access_Token", + "value": "@{body('Get_MDE_Token')?['access_token']}" + } + }, + "For_each_ntfs_file": { + "runAfter": { + "Set_MDE_Access_Token": [ + "Succeeded" + ] + }, + "type": "Foreach", + "foreach": "@coalesce(triggerBody()?['files'], json('[]'))", + "runtimeConfiguration": { + "concurrency": { + "repetitions": 1 + } + }, + "actions": { + "Reset_Ntfs_File_Handled": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Ntfs_File_Handled", + "value": false + } + }, + "Reset_MDE_Machine_Id": { + "runAfter": { + "Reset_Ntfs_File_Handled": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "MDE_Machine_Id", + "value": "@null" + } + }, + "Reset_File_Sha1": { + "runAfter": { + "Reset_MDE_Machine_Id": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "File_Sha1", + "value": "@null" + } + }, + "Reset_MDE_Action_Id": { + "runAfter": { + "Reset_File_Sha1": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "MDE_Action_Id", + "value": "@null" + } + }, + "If_physical_host_provided": { + "runAfter": { + "Reset_MDE_Action_Id": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@trim(coalesce(items('For_each_ntfs_file')?['physicalHost'], ''))", + "" + ] + } + ] + }, + "actions": { + "Append_result_ntfs_no_host": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_ntfs_file')?['filename']", + "status": "Failed", + "newLocation": "", + "error": "No physicalHost was provided for this Windows Fileset file, so the MDE device could not be resolved and the file was not quarantined. RSC supplies physicalHost only on SecurityViolation events (policyViolation.resourceMetadata.metadata.physicalHost); Classification events do not carry it. Quarantine this file from a SecurityViolation event, or quarantine it manually in Microsoft Defender for Endpoint." + } + } + }, + "Set_Handled_no_host": { + "runAfter": { + "Append_result_ntfs_no_host": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Ntfs_File_Handled", + "value": true + } + } + }, + "else": { + "actions": {} + } + }, + "Get_MDE_Machine": { + "runAfter": { + "If_physical_host_provided": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{variables('MDE_Base_URL')}/api/machines?$filter=computerDnsName eq '@{replace(coalesce(items('For_each_ntfs_file')?['physicalHost'], ''), '''', '''''')}'", + "headers": { + "Authorization": "Bearer @{variables('MDE_Access_Token')}" + } + } + }, + "Check_Get_MDE_Machine": { + "runAfter": { + "Get_MDE_Machine": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Ntfs_File_Handled')", + false + ] + }, + { + "or": [ + { + "less": [ + "@coalesce(outputs('Get_MDE_Machine')?['statusCode'], 0)", + 200 + ] + }, + { + "greaterOrEquals": [ + "@coalesce(outputs('Get_MDE_Machine')?['statusCode'], 0)", + 300 + ] + }, + { + "equals": [ + "@length(coalesce(body('Get_MDE_Machine')?['value'], json('[]')))", + 0 + ] + } + ] + } + ] + }, + "actions": { + "Append_result_ntfs_device_not_found": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_ntfs_file')?['filename']", + "status": "Failed", + "newLocation": "", + "error": "Could not resolve the file-share host '@{items('For_each_ntfs_file')?['physicalHost']}' in Microsoft Defender for Endpoint (status @{coalesce(outputs('Get_MDE_Machine')?['statusCode'], 'none')}). Ensure the host is onboarded to MDE." + } + } + }, + "Set_Handled_device_not_found": { + "runAfter": { + "Append_result_ntfs_device_not_found": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Ntfs_File_Handled", + "value": true + } + } + }, + "else": { + "actions": { + "Set_MDE_Machine_Id": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "MDE_Machine_Id", + "value": "@{first(coalesce(body('Get_MDE_Machine')?['value'], json('[]')))?['id']}" + } + } + } + } + }, + "If_not_yet_handled": { + "runAfter": { + "Check_Get_MDE_Machine": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Ntfs_File_Handled')", + false + ] + } + ] + }, + "actions": { + "Run_advanced_hunting_for_sha1": { + "runAfter": {}, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('MDE_Base_URL')}/api/advancedqueries/run", + "headers": { + "Authorization": "Bearer @{variables('MDE_Access_Token')}", + "Content-Type": "application/json" + }, + "body": { + "Query": "DeviceFileEvents | where DeviceId == @'@{replace(coalesce(variables('MDE_Machine_Id'), ''), '''', '''''')}' and FileName =~ @'@{replace(coalesce(items('For_each_ntfs_file')?['filename'], ''), '''', '''''')}' and FolderPath =~ @'@{replace(coalesce(items('For_each_ntfs_file')?['nativePath'], items('For_each_ntfs_file')?['filePath'], ''), '''', '''''')}' and isnotempty(SHA1) | top 1 by Timestamp desc | project SHA1" + } + } + }, + "Check_advanced_hunting": { + "runAfter": { + "Run_advanced_hunting_for_sha1": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Ntfs_File_Handled')", + false + ] + }, + { + "or": [ + { + "less": [ + "@coalesce(outputs('Run_advanced_hunting_for_sha1')?['statusCode'], 0)", + 200 + ] + }, + { + "greaterOrEquals": [ + "@coalesce(outputs('Run_advanced_hunting_for_sha1')?['statusCode'], 0)", + 300 + ] + }, + { + "equals": [ + "@trim(coalesce(first(coalesce(body('Run_advanced_hunting_for_sha1')?['Results'], json('[]')))?['SHA1'], ''))", + "" + ] + } + ] + } + ] + }, + "actions": { + "Append_result_ntfs_sha1_not_found": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_ntfs_file')?['filename']", + "status": "Failed", + "newLocation": "", + "error": "Could not resolve a SHA1 for the file via MDE advanced hunting (status @{coalesce(outputs('Run_advanced_hunting_for_sha1')?['statusCode'], 'none')}). The file may not have been observed by MDE on host '@{items('For_each_ntfs_file')?['physicalHost']}'; not quarantined." + } + } + }, + "Set_Handled_sha1_not_found": { + "runAfter": { + "Append_result_ntfs_sha1_not_found": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Ntfs_File_Handled", + "value": true + } + } + }, + "else": { + "actions": { + "Set_File_Sha1": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "File_Sha1", + "value": "@{first(coalesce(body('Run_advanced_hunting_for_sha1')?['Results'], json('[]')))?['SHA1']}" + } + } + } + } + }, + "Create_stop_and_quarantine": { + "runAfter": { + "Check_advanced_hunting": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('MDE_Base_URL')}/api/machines/@{encodeURIComponent(variables('MDE_Machine_Id'))}/StopAndQuarantineFile", + "headers": { + "Authorization": "Bearer @{variables('MDE_Access_Token')}", + "Content-Type": "application/json" + }, + "body": { + "Comment": "Rubrik DSPM violation quarantine: @{items('For_each_ntfs_file')?['filename']}", + "Sha1": "@{variables('File_Sha1')}" + } + } + }, + "Check_create_stop_and_quarantine": { + "runAfter": { + "Create_stop_and_quarantine": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Ntfs_File_Handled')", + false + ] + }, + { + "or": [ + { + "less": [ + "@coalesce(outputs('Create_stop_and_quarantine')?['statusCode'], 0)", + 200 + ] + }, + { + "greaterOrEquals": [ + "@coalesce(outputs('Create_stop_and_quarantine')?['statusCode'], 0)", + 300 + ] + }, + { + "equals": [ + "@trim(coalesce(body('Create_stop_and_quarantine')?['id'], ''))", + "" + ] + } + ] + } + ] + }, + "actions": { + "Append_result_ntfs_quarantine_create_failed": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_ntfs_file')?['filename']", + "status": "Failed", + "newLocation": "", + "error": "Failed to create the MDE StopAndQuarantineFile action (status @{coalesce(outputs('Create_stop_and_quarantine')?['statusCode'], 'none')}). @{string(coalesce(body('Create_stop_and_quarantine'), ''))}" + } + } + }, + "Set_Handled_create_failed": { + "runAfter": { + "Append_result_ntfs_quarantine_create_failed": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Ntfs_File_Handled", + "value": true + } + } + }, + "else": { + "actions": { + "Set_Quarantine_Action_Id": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "MDE_Action_Id", + "value": "@{body('Create_stop_and_quarantine')?['id']}" + } + } + } + } + }, + "Until_action_complete": { + "runAfter": { + "Check_create_stop_and_quarantine": [ + "Succeeded" + ] + }, + "type": "Until", + "expression": "@or(equals(variables('Ntfs_File_Handled'), true), not(contains(json('[\"Pending\",\"InProgress\"]'), coalesce(body('Get_action_status')?['status'], 'Pending'))))", + "limit": { + "count": 60, + "timeout": "PT20M" + }, + "actions": { + "Wait_15_seconds": { + "runAfter": {}, + "type": "Wait", + "inputs": { + "interval": { + "count": 15, + "unit": "Second" + } + } + }, + "Get_action_status": { + "runAfter": { + "Wait_15_seconds": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{variables('MDE_Base_URL')}/api/machineactions/@{encodeURIComponent(variables('MDE_Action_Id'))}", + "headers": { + "Authorization": "Bearer @{variables('MDE_Access_Token')}" + } + } + } + } + }, + "If_quarantine_succeeded": { + "runAfter": { + "Until_action_complete": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Ntfs_File_Handled')", + false + ] + }, + { + "equals": [ + "@coalesce(body('Get_action_status')?['status'], '')", + "Succeeded" + ] + } + ] + }, + "actions": { + "Append_result_ntfs_success": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_ntfs_file')?['filename']", + "status": "Success", + "newLocation": "MDE quarantine on @{items('For_each_ntfs_file')?['physicalHost']}", + "error": "" + } + } + } + }, + "else": { + "actions": { + "Append_result_ntfs_quarantine_incomplete": { + "runAfter": {}, + "type": "If", + "expression": { + "and": [ + { + "equals": [ + "@variables('Ntfs_File_Handled')", + false + ] + } + ] + }, + "actions": { + "Append_result_ntfs_incomplete_inner": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_ntfs_file')?['filename']", + "status": "Failed", + "newLocation": "", + "error": "MDE StopAndQuarantineFile action did not succeed within the timeout (final status @{coalesce(body('Get_action_status')?['status'], 'Unknown')})." + } + } + } + }, + "else": { + "actions": {} + } + } + } + } + } + } + } + } + }, + "Filter_failed_results_ntfs": { + "runAfter": { + "For_each_ntfs_file": [ + "Succeeded" + ] + }, + "type": "Query", + "inputs": { + "from": "@variables('Results')", + "where": "@not(equals(item()?['status'], 'Success'))" + } + }, + "Set_Overall_Status_ntfs": { + "runAfter": { + "Filter_failed_results_ntfs": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Overall_Status", + "value": "@if(equals(length(variables('Results')), 0), 'Success', if(equals(length(body('Filter_failed_results_ntfs')), 0), 'Success', if(equals(length(body('Filter_failed_results_ntfs')), length(variables('Results'))), 'Failed', 'PartialSuccess')))" + } + }, + "Set_Failure_Message_ntfs": { + "runAfter": { + "Set_Overall_Status_ntfs": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Failure_Message", + "value": "@{if(equals(length(body('Filter_failed_results_ntfs')), 0), '', concat(string(length(body('Filter_failed_results_ntfs'))), ' of ', string(length(variables('Results'))), ' NTFS file(s) failed to quarantine. ', string(body('Filter_failed_results_ntfs'))))}" + } + } + } + } + } + } + } + }, + "default": { + "actions": { + "Set_Overall_Status_unknown": { + "runAfter": {}, + "type": "SetVariable", + "inputs": { + "name": "Overall_Status", + "value": "NotImplemented" + } + }, + "Set_Failure_Message_unknown": { + "runAfter": { + "Set_Overall_Status_unknown": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Failure_Message", + "value": "not a valid objectType for quarantine (received '@{variables('Object_Type')}')." + } + } + } + } + }, + "Check_Overall_Status": { + "runAfter": { + "Switch_on_object_type": [ + "Succeeded", + "Failed" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "not": { + "equals": [ + "@variables('Overall_Status')", + "Success" + ] + } + }, + { + "not": { + "equals": [ + "@variables('Overall_Status')", + "NotImplemented" + ] + } + } + ] + }, + "actions": { + "Response_batch_failed": { + "runAfter": {}, + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "status": "@variables('Overall_Status')", + "results": "@variables('Results')", + "error": "@variables('Failure_Message')" + } + } + }, + "Terminate_batch_failed": { + "runAfter": { + "Response_batch_failed": [ + "Succeeded" + ] + }, + "type": "Terminate", + "inputs": { + "runStatus": "Failed", + "runError": { + "code": "QuarantineFailed", + "message": "@variables('Failure_Message')" + } + } + } + }, + "else": { + "actions": { + "Response_success": { + "runAfter": {}, + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "status": "@variables('Overall_Status')", + "results": "@variables('Results')", + "error": "@variables('Failure_Message')" + } + } + } + } + } + }, + "Initialize_Drive_Name": { + "runAfter": { + "Initialize_Ntfs_File_Handled": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Drive_Name", + "type": "string" + } + ] + } + }, + "Initialize_Matched_Drive": { + "runAfter": { + "Initialize_Drive_Name": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Matched_Drive", + "type": "object", + "value": {} + } + ] + } + }, + "Initialize_Item_Path_Final": { + "runAfter": { + "Initialize_Site_Drives": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Item_Path_Final", + "type": "string" + } + ] + } + }, + "Initialize_Site_Drives": { + "runAfter": { + "Initialize_Matched_Drive": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Site_Drives", + "type": "array" + } + ] + } + } + }, + "outputs": {} + }, + "parameters": { + "$connections": { + "value": { + "teams": { + "connectionId": "[resourceId('Microsoft.Web/connections', variables('TeamsConnectionName'))]", + "connectionName": "[variables('TeamsConnectionName')]", + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/teams')]" + }, + "keyvault": { + "connectionId": "[resourceId('Microsoft.Web/connections', variables('KeyvaultConnectionName'))]", + "connectionName": "[variables('KeyvaultConnectionName')]", + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/keyvault')]", + "connectionProperties": { + "authentication": { + "type": "ManagedServiceIdentity" + } + } + } + } + } + } + } + } + ] +} diff --git a/Solutions/RubrikSecurityCloud/ReleaseNotes.md b/Solutions/RubrikSecurityCloud/ReleaseNotes.md index 12e51f58111..2d53f35154d 100644 --- a/Solutions/RubrikSecurityCloud/ReleaseNotes.md +++ b/Solutions/RubrikSecurityCloud/ReleaseNotes.md @@ -1,5 +1,6 @@ | **Version** | **Date Modified (DD-MM-YYYY)** | **Change History** | |-------------|--------------------------------|---------------------------------------------| +| 3.5.5 | 26-10-2026 | Added support of DSPM & IR Violation events collection as part of Azure function app based dta connector, Added analytic rules and playbooks rleated DSPM & IR Violation and quarntine. | 3.5.4 | 12-10-2026 | Updated Events Data table schema for CCF Connector. | 3.5.3 | 03-09-2026 | Added RubrikCCFConnector to ingest Anomaly, Ransomware, ThreatHunt and other events data along with audit logs via webhook. | | 3.5.2 | 16-07-2026 | Added RubrikSecurityCloud CCF (Codeless Connector Framework) Data Connector for ingesting Rubrik Protection Status data into Microsoft Sentinel. Enables backup compliance monitoring, ransomware recovery assessment, and incident correlation with protection status for Azure VMs. Update python package version to resolve vulnerability | diff --git a/Solutions/RubrikSecurityCloud/SolutionMetadata.json b/Solutions/RubrikSecurityCloud/SolutionMetadata.json index 2bf6ffff9b7..d68d7ba155b 100644 --- a/Solutions/RubrikSecurityCloud/SolutionMetadata.json +++ b/Solutions/RubrikSecurityCloud/SolutionMetadata.json @@ -2,7 +2,7 @@ "publisherId": "rubrik_inc", "offerId": "rubrik_sentinel", "firstPublishDate": "2022-07-19", - "lastPublishDate": "2026-10-12", + "lastPublishDate": "2026-10-26", "providers": [ "Rubrik" ],