diff --git a/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikClassificationEvents.yaml b/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikClassificationEvents.yaml new file mode 100644 index 00000000000..7f762f91199 --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikClassificationEvents.yaml @@ -0,0 +1,52 @@ +id: 9563dc15-ea0a-438a-9d50-7ab243378241 +name: Create Incident for Classification evnets 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 == "Classification" and EventName == "ClassficationResultsAvailable" + | extend NewSeverity = case(Severity=="info", "Low", + Severity=="warning", "Medium", + Severity=="critical", "High", + "Informational") +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: P7D + matchingMethod: Selected + groupByCustomDetails: + - ObjectId +eventGroupingSettings: + aggregationKind: AlertPerResult +alertDetailsOverride: + alertDisplayNameFormat: '{{Class}} event occurred for object {{ObjectName}} with type {{ObjectType}}' + alertDescriptionFormat: '{{Summary}}' + alertSeverityColumnName: NewSeverity +customDetails: + ObjectId: ObjectId + ObjectName: ObjectName + ObjectType: ObjectType + EventType: Class + EventName: EventName + Summary: Summary + EventTimestamp: EventTimestamp + Severity: Severity + SnapshotId: SnapshotId diff --git a/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikCriticalAnomaly.yaml b/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikCriticalAnomaly.yaml index 0354b27af4f..c09f298b371 100644 --- a/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikCriticalAnomaly.yaml +++ b/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikCriticalAnomaly.yaml @@ -1,6 +1,6 @@ id: 54c70d21-696f-4f03-9238-9d7118d079fe name: Rubrik Critical Anomaly -version: 1.0.0 +version: 1.1.0 kind: Scheduled description: | 'Rubrik Critical Anomaly rule matches Severity and if Critical severity found then generate the incident for each object.' @@ -20,14 +20,14 @@ relevantTechniques: - T1546 query: | Rubrik_Anomaly_Data_CL - | where severity_s == "critical" + | where Severity == "critical" eventGroupingSettings: aggregationKind: AlertPerResult customDetails: - ClusterIdentifier: custom_details_clusterId_g - ObjectId: custom_details_objectId_g - ObjectName: custom_details_objectName_s - ObjectType: custom_details_objectType_s - ClusterName: custom_details_clusterName_s + ClusterIdentifier: ClusterId + ObjectId: ObjectId + ObjectName: ObjectName + ObjectType: ObjectType + ClusterName: ClusterName incidentConfiguration: createIncident: true \ No newline at end of file diff --git a/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikDataSecurityViolation.yaml b/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikDataSecurityViolation.yaml new file mode 100644 index 00000000000..00bfbbb1c10 --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikDataSecurityViolation.yaml @@ -0,0 +1,57 @@ +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 == "SecurityViolation" and EventName in ("CriticalSeverityDataViolationDetected", + "CriticalSeverityDataViolationReOpen", + "HighSeverityDataViolationDetected", + "HighSeverityDataViolationReOpen", + "LowOrMediumSeverityDataViolationDetected", + "LowOrMediumSeverityDataViolationReOpen") + | extend NewSeverity = case(Severity=="info", "Low", + Severity=="warning", "Medium", + Severity=="critical", "High", + "Informational") +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: P7D + matchingMethod: Selected + groupByCustomDetails: + - ViolationId +eventGroupingSettings: + aggregationKind: AlertPerResult +alertDetailsOverride: + alertDisplayNameFormat: '{{Class}} event occurred for object {{ObjectName}} with type {{ObjectType}}' + alertDescriptionFormat: '{{Summary}}' + alertSeverityColumnName: NewSeverity +customDetails: + ObjectId: ObjectId + ObjectName: ObjectName + ObjectType: ObjectType + EventType: Class + EventName: EventName + ViolationId: SeriesId + Summary: Summary + EventTimestamp: EventTimestamp + Severity: Severity diff --git a/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikIdentitySecurityViolation.yaml b/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikIdentitySecurityViolation.yaml new file mode 100644 index 00000000000..9a9d6e45888 --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikIdentitySecurityViolation.yaml @@ -0,0 +1,60 @@ +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: + - CredentialAccess +relevantTechniques: + - T1078 +query: | + Rubrik_Violation_Data_CL + | where (Class == "IdentityViolation" and EventName in ("CriticalSeverityDataViolationDetected", + "CriticalSeverityDataViolationReOpen", "HighSeverityDataViolationDetected", + "HighSeverityDataViolationReOpen", "LowOrMediumSeverityDataViolationDetected", + "LowOrMediumSeverityDataViolationReOpen")) or + (Class == "IdentityAlerts" and EventName in ("CriticalSeverityIdentityAlertDetected", + "CriticalSeverityIdentityAlertReOpened", "HighSeverityIdentityAlertDetected", + "HighSeverityIdentityAlertReOpened", "LowOrMediumSeverityIdentityAlertDetected", + "LowOrMediumSeverityIdentityAlertReOpened" + )) + | extend NewSeverity = case(Severity=="info", "Low", + Severity=="warning", "Medium", + Severity=="critical", "High", + "Informational") +incidentConfiguration: + createIncident: true + groupingConfiguration: + enabled: true + reopenClosedIncident: false + lookbackDuration: P7D + matchingMethod: Selected + groupByCustomDetails: + - ViolationId +eventGroupingSettings: + aggregationKind: AlertPerResult +alertDetailsOverride: + alertDisplayNameFormat: '{{Class}} event occurred for object {{ObjectName}} with type {{ObjectType}}' + alertDescriptionFormat: '{{Summary}}' + alertSeverityColumnName: NewSeverity +customDetails: + ObjectId: ObjectId + ObjectName: ObjectName + ObjectType: ObjectType + EventType: Class + EventName: EventName + ViolationId: SeriesId + Summary: Summary + EventTimestamp: EventTimestamp + Severity: Severity diff --git a/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikThreatMonitoring.yaml b/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikThreatMonitoring.yaml index 6968bdcd86d..ac4667a5363 100644 --- a/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikThreatMonitoring.yaml +++ b/Solutions/RubrikSecurityCloud/Analytic Rules/RubrikThreatMonitoring.yaml @@ -1,6 +1,6 @@ id: 0083cbc4-776e-42ca-8694-6950fd605df9 name: Rubrik Threat Monitoring -version: 1.0.0 +version: 1.1.0 kind: Scheduled description: | 'Rubrik Threat Monitoring matches Event Name and if match found then generate the incident for each object.' @@ -20,11 +20,11 @@ relevantTechniques: - T1546 query: | Rubrik_Events_Data_CL - | where custom_details_eventName_s endswith "AnalysisMatchesFound" or custom_details_eventName_s contains "ThreatMonitoringHashCatalogAnalysisFailed" or custom_details_eventName_s contains "ThreatMonitoringHashMatchesFound" or custom_details_eventName_s contains "ThreatMonitoringYaraMatchesFound" - | extend hashMatchCount = toint(extract("Found ([0-9]+) hash matches",1, summary_s)), yaraMatchCount = toint(extract("Found ([0-9]+) YARA rule matches", 1, summary_s)), fileHashMatchCount = toint(extract("Found file hash matches for ([0-9]+) files", 1, summary_s)) - | extend count_ = coalesce(hashMatchCount,yaraMatchCount, fileHashMatchCount), eventname = substring(custom_details_eventName_s,16, strlen(custom_details_eventName_s) - 28) + | where EventName endswith "AnalysisMatchesFound" or EventName contains "ThreatMonitoringHashCatalogAnalysisFailed" or EventName contains "ThreatMonitoringHashMatchesFound" or EventName contains "ThreatMonitoringYaraMatchesFound" + | extend hashMatchCount = toint(extract("Found ([0-9]+) hash matches",1, Summary)), yaraMatchCount = toint(extract("Found ([0-9]+) YARA rule matches", 1, Summary)), fileHashMatchCount = toint(extract("Found file hash matches for ([0-9]+) files", 1, Summary)) + | extend count_ = coalesce(hashMatchCount,yaraMatchCount, fileHashMatchCount), eventname = substring(EventName,16, strlen(EventName) - 28) | where count_ > 0 - | summarize arg_max(TimeGenerated,*) by eventname, custom_details_objectId_g, count_ + | summarize arg_max(TimeGenerated,*) by eventname, ObjectId, count_ incidentConfiguration: createIncident: true groupingConfiguration: @@ -38,12 +38,12 @@ incidentConfiguration: eventGroupingSettings: aggregationKind: AlertPerResult alertDetailsOverride: - alertDisplayNameFormat: ThreatMonitoring Found {{count_}} {{eventname}} Matches for {{custom_details_objectName_s}} + alertDisplayNameFormat: ThreatMonitoring Found {{count_}} {{eventname}} Matches for {{ObjectName}} customDetails: - EventName: custom_details_eventName_s - Summary: summary_s - ClusterIdentifier: custom_details_clusterId_g - ObjectId: custom_details_objectId_g - ObjectName: custom_details_objectName_s - ObjectType: custom_details_objectType_s - Url: custom_details_url_s + EventName: EventName + Summary: Summary + ClusterIdentifier: ClusterId + ObjectId: ObjectId + ObjectName: ObjectName + ObjectType: ObjectType + Url: Url 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..00558586518 Binary files /dev/null and b/Solutions/RubrikSecurityCloud/Data Connectors/RubrikWebhookEvents/RubrikDataConnector.zip differ 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..95427e4c6e3 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": { @@ -103,15 +122,6 @@ "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": [ @@ -132,31 +142,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://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fniralishah-crest%2FMSSentinelIntegrations%2Frefs%2Fheads%2Fmain%2Fazuredeploy_Connector_RubrikWebhookEvents_AzureFunction.json)\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 Name\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 +154,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-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.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\tWorkspaceName\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 +170,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..9b7a9f53cee 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", + "WorkspaceName": { + "type": "string", "minLength": 1, "metadata": { - "description": "Enter Primary Key of log analytics Workspace" + "description": "Enter the name of the Log Analytics Workspace." } }, "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,36 @@ "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'))]" + "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 +205,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 +239,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://github.com/niralishah-crest/MSSentinelIntegrations/raw/refs/heads/main/RubrikDataConnector.zip" } } ] @@ -248,6 +292,386 @@ "properties": { "shareQuota": 5120 } + }, + { + "type": "Microsoft.Insights/dataCollectionEndpoints", + "apiVersion": "2022-06-01", + "name": "[variables('DceName')]", + "location": "[parameters('Location')]", + "properties": { + "networkAcls": { + "publicNetworkAccess": "Enabled" + } + } + }, + { + "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", "type": "string" }, + { "name": "EventSource", "type": "string" }, + { "name": "Severity", "type": "string" }, + { "name": "EventTimestamp", "type": "datetime" }, + { "name": "Class", "type": "string" }, + { "name": "Component", "type": "string" }, + { "name": "Group", "type": "string" }, + { "name": "SeriesId", "type": "string" }, + { "name": "EventType", "type": "string" }, + { "name": "EventId", "type": "string" }, + { "name": "ObjectId", "type": "string" }, + { "name": "ObjectName", "type": "string" }, + { "name": "ObjectType", "type": "string" }, + { "name": "Status", "type": "string" }, + { "name": "ClusterName", "type": "string" }, + { "name": "ClusterId", "type": "string" }, + { "name": "EventName", "type": "string" }, + { "name": "ErrorId", "type": "string" }, + { "name": "ErrorCode", "type": "string" }, + { "name": "ErrorRemedy", "type": "string" }, + { "name": "ErrorReason", "type": "string" }, + { "name": "ErrorMessage", "type": "string" }, + { "name": "Location", "type": "string" }, + { "name": "Url", "type": "string" }, + { "name": "CustomerId", "type": "string" }, + { "name": "LogicalSizeInBytes", "type": "long" }, + { "name": "DataTransferredInBytes", "type": "long" }, + { "name": "EffectiveThroughput", "type": "long" }, + { "name": "SnapshotId", "type": "string" } + ] + } + } + }, + { + "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", "type": "string" }, + { "name": "EventSource", "type": "string" }, + { "name": "Severity", "type": "string" }, + { "name": "EventTimestamp", "type": "datetime" }, + { "name": "Class", "type": "string" }, + { "name": "Component", "type": "string" }, + { "name": "Group", "type": "string" }, + { "name": "SeriesId", "type": "string" }, + { "name": "EventType", "type": "string" }, + { "name": "EventId", "type": "string" }, + { "name": "ObjectId", "type": "string" }, + { "name": "ObjectName", "type": "string" }, + { "name": "ObjectType", "type": "string" }, + { "name": "Status", "type": "string" }, + { "name": "ClusterName", "type": "string" }, + { "name": "ClusterId", "type": "string" }, + { "name": "EventName", "type": "string" }, + { "name": "ErrorId", "type": "string" }, + { "name": "ErrorCode", "type": "string" }, + { "name": "ErrorRemedy", "type": "string" }, + { "name": "ErrorReason", "type": "string" }, + { "name": "ErrorMessage", "type": "string" }, + { "name": "Location", "type": "string" }, + { "name": "Url", "type": "string" }, + { "name": "CustomerId", "type": "string" }, + { "name": "LogicalSizeInBytes", "type": "long" }, + { "name": "DataTransferredInBytes", "type": "long" }, + { "name": "EffectiveThroughput", "type": "long" }, + { "name": "SnapshotId", "type": "string" } + ] + } + } + }, + { + "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", "type": "string" }, + { "name": "EventSource", "type": "string" }, + { "name": "Severity", "type": "string" }, + { "name": "EventTimestamp", "type": "datetime" }, + { "name": "Class", "type": "string" }, + { "name": "Component", "type": "string" }, + { "name": "Group", "type": "string" }, + { "name": "SeriesId", "type": "string" }, + { "name": "EventType", "type": "string" }, + { "name": "EventId", "type": "string" }, + { "name": "ObjectId", "type": "string" }, + { "name": "ObjectName", "type": "string" }, + { "name": "ObjectType", "type": "string" }, + { "name": "Status", "type": "string" }, + { "name": "ClusterName", "type": "string" }, + { "name": "ClusterId", "type": "string" }, + { "name": "EventName", "type": "string" }, + { "name": "ErrorId", "type": "string" }, + { "name": "ErrorCode", "type": "string" }, + { "name": "ErrorRemedy", "type": "string" }, + { "name": "ErrorReason", "type": "string" }, + { "name": "ErrorMessage", "type": "string" }, + { "name": "Location", "type": "string" }, + { "name": "Url", "type": "string" }, + { "name": "CustomerId", "type": "string" }, + { "name": "LogicalSizeInBytes", "type": "long" }, + { "name": "DataTransferredInBytes", "type": "long" }, + { "name": "EffectiveThroughput", "type": "long" }, + { "name": "SnapshotId", "type": "string" } + ] + } + } + }, + { + "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", "type": "string" }, + { "name": "EventSource", "type": "string" }, + { "name": "Severity", "type": "string" }, + { "name": "EventTimestamp", "type": "datetime" }, + { "name": "Class", "type": "string" }, + { "name": "Component", "type": "string" }, + { "name": "Group", "type": "string" }, + { "name": "SeriesId", "type": "string" }, + { "name": "EventType", "type": "string" }, + { "name": "EventId", "type": "string" }, + { "name": "ObjectId", "type": "string" }, + { "name": "ObjectName", "type": "string" }, + { "name": "ObjectType", "type": "string" }, + { "name": "Status", "type": "string" }, + { "name": "ClusterName", "type": "string" }, + { "name": "ClusterId", "type": "string" }, + { "name": "EventName", "type": "string" }, + { "name": "ErrorId", "type": "string" }, + { "name": "ErrorCode", "type": "string" }, + { "name": "ErrorRemedy", "type": "string" }, + { "name": "ErrorReason", "type": "string" }, + { "name": "ErrorMessage", "type": "string" }, + { "name": "Location", "type": "string" }, + { "name": "Url", "type": "string" }, + { "name": "CustomerId", "type": "string" }, + { "name": "LogicalSizeInBytes", "type": "long" }, + { "name": "DataTransferredInBytes", "type": "long" }, + { "name": "EffectiveThroughput", "type": "long" }, + { "name": "SnapshotId", "type": "string" } + ] + } + } + }, + { + "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", "type": "string" }, + { "name": "EventSource", "type": "string" }, + { "name": "Severity", "type": "string" }, + { "name": "EventTimestamp", "type": "datetime" }, + { "name": "Class", "type": "string" }, + { "name": "Component", "type": "string" }, + { "name": "Group", "type": "string" }, + { "name": "SeriesId", "type": "string" }, + { "name": "EventType", "type": "string" }, + { "name": "EventId", "type": "string" }, + { "name": "ObjectId", "type": "string" }, + { "name": "ObjectName", "type": "string" }, + { "name": "ObjectType", "type": "string" }, + { "name": "Status", "type": "string" }, + { "name": "ClusterName", "type": "string" }, + { "name": "ClusterId", "type": "string" }, + { "name": "EventName", "type": "string" }, + { "name": "ErrorId", "type": "string" }, + { "name": "ErrorCode", "type": "string" }, + { "name": "ErrorRemedy", "type": "string" }, + { "name": "ErrorReason", "type": "string" }, + { "name": "ErrorMessage", "type": "string" }, + { "name": "Location", "type": "string" }, + { "name": "Url", "type": "string" }, + { "name": "CustomerId", "type": "string" }, + { "name": "LogicalSizeInBytes", "type": "long" }, + { "name": "DataTransferredInBytes", "type": "long" }, + { "name": "EffectiveThroughput", "type": "long" }, + { "name": "ObjectFid", "type": "string" }, + { "name": "AuditUserName", "type": "string" }, + { "name": "AuditUserId", "type": "string" }, + { "name": "IpAddress", "type": "string" }, + { "name": "UserNote", "type": "string" }, + { "name": "IsPolarisAudit", "type": "boolean" }, + { "name": "OrgId", "type": "string" } + ] + } + } + }, + { + "type": "Microsoft.Insights/dataCollectionRules", + "apiVersion": "2022-06-01", + "name": "[variables('WebhookDcrName')]", + "location": "[parameters('Location')]", + "dependsOn": [ + "[variables('dceId')]" + ], + "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": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('WorkspaceName'))]" + } + ] + }, + "dataFlows": [ + { + "streams": ["Custom-Rubrik_Anomaly_Data_CL"], + "destinations": ["clv2ws1"], + "outputStream": "Custom-Rubrik_Anomaly_Data_CL", + "transformKql": "source | extend cd = parse_json(tostring(custom_details)) | project TimeGenerated = now(), Summary = tostring(summary), EventSource = tostring(source), Severity = tostring(severity), EventTimestamp = todatetime(timestamp), Class = tostring(class), Component = tostring(component), Group = tostring(group), SeriesId = tostring(cd.seriesId), EventType = tostring(cd.type), EventId = tostring(cd.id), ObjectId = tostring(cd.objectId), ObjectName = tostring(cd.objectName), ObjectType = tostring(cd.objectType), Status = tostring(cd.status), ClusterName = tostring(cd.clusterName), ClusterId = tostring(cd.clusterId), EventName = tostring(cd.eventName), ErrorId = tostring(cd.errorId), ErrorCode = tostring(cd.errorCode), ErrorRemedy = tostring(cd.errorRemedy), ErrorReason = tostring(cd.errorReason), ErrorMessage = tostring(cd.errorMessage), Location = tostring(cd.location), Url = tostring(cd.url), CustomerId = tostring(cd.customerID), LogicalSizeInBytes = tolong(cd.logicalSizeInBytes), DataTransferredInBytes = tolong(cd.dataTransferredInBytes), EffectiveThroughput = tolong(cd.effectiveThroughput), SnapshotId = tostring(cd.snapshotId)" + }, + { + "streams": ["Custom-Rubrik_Ransomware_Data_CL"], + "destinations": ["clv2ws1"], + "outputStream": "Custom-Rubrik_Ransomware_Data_CL", + "transformKql": "source | extend cd = parse_json(tostring(custom_details)) | project TimeGenerated = now(), Summary = tostring(summary), EventSource = tostring(source), Severity = tostring(severity), EventTimestamp = todatetime(timestamp), Class = tostring(class), Component = tostring(component), Group = tostring(group), SeriesId = tostring(cd.seriesId), EventType = tostring(cd.type), EventId = tostring(cd.id), ObjectId = tostring(cd.objectId), ObjectName = tostring(cd.objectName), ObjectType = tostring(cd.objectType), Status = tostring(cd.status), ClusterName = tostring(cd.clusterName), ClusterId = tostring(cd.clusterId), EventName = tostring(cd.eventName), ErrorId = tostring(cd.errorId), ErrorCode = tostring(cd.errorCode), ErrorRemedy = tostring(cd.errorRemedy), ErrorReason = tostring(cd.errorReason), ErrorMessage = tostring(cd.errorMessage), Location = tostring(cd.location), Url = tostring(cd.url), CustomerId = tostring(cd.customerID), LogicalSizeInBytes = tolong(cd.logicalSizeInBytes), DataTransferredInBytes = tolong(cd.dataTransferredInBytes), EffectiveThroughput = tolong(cd.effectiveThroughput), SnapshotId = tostring(cd.snapshotId)" + }, + { + "streams": ["Custom-Rubrik_ThreatHunt_Data_CL"], + "destinations": ["clv2ws1"], + "outputStream": "Custom-Rubrik_ThreatHunt_Data_CL", + "transformKql": "source | extend cd = parse_json(tostring(custom_details)) | project TimeGenerated = now(), Summary = tostring(summary), EventSource = tostring(source), Severity = tostring(severity), EventTimestamp = todatetime(timestamp), Class = tostring(class), Component = tostring(component), Group = tostring(group), SeriesId = tostring(cd.seriesId), EventType = tostring(cd.type), EventId = tostring(cd.id), ObjectId = tostring(cd.objectId), ObjectName = tostring(cd.objectName), ObjectType = tostring(cd.objectType), Status = tostring(cd.status), ClusterName = tostring(cd.clusterName), ClusterId = tostring(cd.clusterId), EventName = tostring(cd.eventName), ErrorId = tostring(cd.errorId), ErrorCode = tostring(cd.errorCode), ErrorRemedy = tostring(cd.errorRemedy), ErrorReason = tostring(cd.errorReason), ErrorMessage = tostring(cd.errorMessage), Location = tostring(cd.location), Url = tostring(cd.url), CustomerId = tostring(cd.customerID), LogicalSizeInBytes = tolong(cd.logicalSizeInBytes), DataTransferredInBytes = tolong(cd.dataTransferredInBytes), EffectiveThroughput = tolong(cd.effectiveThroughput), SnapshotId = tostring(cd.snapshotId)" + }, + { + "streams": ["Custom-Rubrik_Violation_Data_CL"], + "destinations": ["clv2ws1"], + "outputStream": "Custom-Rubrik_Violation_Data_CL", + "transformKql": "source | extend cd = parse_json(tostring(custom_details)) | project TimeGenerated = now(), Summary = tostring(summary), EventSource = tostring(source), Severity = tostring(severity), EventTimestamp = todatetime(timestamp), Class = tostring(class), Component = tostring(component), Group = tostring(group), SeriesId = tostring(cd.seriesId), EventType = tostring(cd.type), EventId = tostring(cd.id), ObjectId = tostring(cd.objectId), ObjectName = tostring(cd.objectName), ObjectType = tostring(cd.objectType), Status = tostring(cd.status), ClusterName = tostring(cd.clusterName), ClusterId = tostring(cd.clusterId), EventName = tostring(cd.eventName), ErrorId = tostring(cd.errorId), ErrorCode = tostring(cd.errorCode), ErrorRemedy = tostring(cd.errorRemedy), ErrorReason = tostring(cd.errorReason), ErrorMessage = tostring(cd.errorMessage), Location = tostring(cd.location), Url = tostring(cd.url), CustomerId = tostring(cd.customerID), LogicalSizeInBytes = tolong(cd.logicalSizeInBytes), DataTransferredInBytes = tolong(cd.dataTransferredInBytes), EffectiveThroughput = tolong(cd.effectiveThroughput), SnapshotId = tostring(cd.snapshotId)" + } + ] + } + }, + { + "type": "Microsoft.Insights/dataCollectionRules", + "apiVersion": "2022-06-01", + "name": "[variables('EventsDcrName')]", + "location": "[parameters('Location')]", + "dependsOn": [ + "[variables('dceId')]" + ], + "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": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('WorkspaceName'))]" + } + ] + }, + "dataFlows": [ + { + "streams": ["Custom-Rubrik_Events_Data_CL"], + "destinations": ["clv2ws1"], + "outputStream": "Custom-Rubrik_Events_Data_CL", + "transformKql": "source | extend cd = parse_json(tostring(custom_details)) | project TimeGenerated = now(), Summary = tostring(summary), EventSource = tostring(source), Severity = tostring(severity), EventTimestamp = todatetime(timestamp), Class = tostring(class), Component = tostring(component), Group = tostring(group), SeriesId = tostring(iff(isnull(cd.seriesId), cd.seriesID, cd.seriesId)), EventType = tostring(cd.type), EventId = tostring(cd.id), ObjectId = tostring(cd.objectId), ObjectName = tostring(cd.objectName), ObjectType = tostring(cd.objectType), Status = tostring(cd.status), ClusterName = tostring(cd.clusterName), ClusterId = tostring(cd.clusterId), EventName = tostring(cd.eventName), ErrorId = tostring(cd.errorId), ErrorCode = tostring(cd.errorCode), ErrorRemedy = tostring(cd.errorRemedy), ErrorReason = tostring(cd.errorReason), ErrorMessage = tostring(cd.errorMessage), Location = tostring(cd.location), Url = tostring(cd.url), CustomerId = tostring(cd.customerID), LogicalSizeInBytes = tolong(cd.logicalSizeInBytes), DataTransferredInBytes = tolong(cd.dataTransferredInBytes), EffectiveThroughput = tolong(cd.effectiveThroughput), ObjectFid = tostring(cd.objectFid), AuditUserName = tostring(cd.auditUserName), AuditUserId = tostring(cd.auditUserId), IpAddress = tostring(cd.ipAddress), UserNote = tostring(cd.userNote), IsPolarisAudit = tobool(cd.isPolarisAudit), OrgId = tostring(cd.orgID)" + } + ] + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(resourceGroup().id, reference(resourceId('Microsoft.Web/sites', variables('FunctionName')), '2018-11-01', 'full').identity.principalId, 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/Solution_RubrikSecurityCloud.json b/Solutions/RubrikSecurityCloud/Data/Solution_RubrikSecurityCloud.json index acb556e89e4..635f3efbb92 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", @@ -29,7 +32,10 @@ ], "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.2", diff --git a/Solutions/RubrikSecurityCloud/Package/3.5.2.zip b/Solutions/RubrikSecurityCloud/Package/3.5.2.zip index e8fbdde1346..7e268e53893 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/createUiDefinition.json b/Solutions/RubrikSecurityCloud/Package/createUiDefinition.json index e10547f19a6..82082ef6b25 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:** 2, **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:** 2, **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", @@ -135,6 +135,48 @@ } } ] + }, + { + "name": "analytic3", + "type": "Microsoft.Common.Section", + "label": "Create Incident for Classification evnets for non DSPM license users", + "elements": [ + { + "name": "analytic3-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "Creates an incident for Classification events (ClassficationResultsAvailable) 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 55059898225..6a88e4f2ce8 100644 --- a/Solutions/RubrikSecurityCloud/Package/mainTemplate.json +++ b/Solutions/RubrikSecurityCloud/Package/mainTemplate.json @@ -27,6 +27,20 @@ "metadata": { "description": "Workspace name for Log Analytics where Microsoft Sentinel is setup" } + }, + "resourceGroupName": { + "type": "string", + "defaultValue": "[resourceGroup().name]", + "metadata": { + "description": "resource group name where Microsoft Sentinel is setup" + } + }, + "subscription": { + "type": "string", + "defaultValue": "[last(split(subscription().id, '/'))]", + "metadata": { + "description": "subscription id where Microsoft Sentinel is setup" + } } }, "variables": { @@ -183,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.2.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.0.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", @@ -192,24 +230,45 @@ "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": "1.0.0", + "dataConnectorCCPVersion": "3.5.2", "_dataConnectorContentIdConnectorDefinition2": "RubrikProtectionStatus", "dataConnectorTemplateNameConnectorDefinition2": "[concat(parameters('workspace'),'-dc-',uniquestring(variables('_dataConnectorContentIdConnectorDefinition2')))]", "_dataConnectorContentIdConnections2": "RubrikProtectionStatusConnections", "dataConnectorTemplateNameConnections2": "[concat(parameters('workspace'),'-dc-',uniquestring(variables('_dataConnectorContentIdConnections2')))]", "analyticRuleObject1": { - "analyticRuleVersion1": "1.0.0", + "analyticRuleVersion1": "1.1.0", "_analyticRulecontentId1": "54c70d21-696f-4f03-9238-9d7118d079fe", "analyticRuleId1": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', '54c70d21-696f-4f03-9238-9d7118d079fe')]", "analyticRuleTemplateSpecName1": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('54c70d21-696f-4f03-9238-9d7118d079fe')))]", - "_analyticRulecontentProductId1": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','54c70d21-696f-4f03-9238-9d7118d079fe','-', '1.0.0')))]" + "_analyticRulecontentProductId1": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','54c70d21-696f-4f03-9238-9d7118d079fe','-', '1.1.0')))]" }, "analyticRuleObject2": { - "analyticRuleVersion2": "1.0.0", + "analyticRuleVersion2": "1.1.0", "_analyticRulecontentId2": "0083cbc4-776e-42ca-8694-6950fd605df9", "analyticRuleId2": "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', '0083cbc4-776e-42ca-8694-6950fd605df9')]", "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')))]" + "_analyticRulecontentProductId2": "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','0083cbc4-776e-42ca-8694-6950fd605df9','-', '1.1.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'))))]" }, @@ -33574,443 +33633,7070 @@ { "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.2", + "description": "RubrikDSPMViolationRemediation Playbook with template version 3.5.2", "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.2.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_Provided_Snapshot_Fid": [ + "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']}" + } + } + } + } + }, + "For_each_alert": { + "runAfter": { + "Is_Authentication_Failed": [ + "Succeeded" + ] + }, + "type": "Foreach", + "foreach": "@triggerBody()?['object']?['properties']?['Alerts']", + "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": { + "Set_Physical_Host_from_violation": [ + "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'))}" + } + } + } + } + }, + "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(variables('CSV_Export_Start')))))" + } + }, + "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": [ + { + "not": { + "equals": [ + "@variables('Download_External_Id')", + "" + ] + } + } + ] + }, + "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(variables('Log_Export_Start')))))" + } + }, + "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": [ + { + "not": { + "equals": [ + "@variables('Download_External_Id')", + "" + ] + } + } + ] + }, + "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" + }, + "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']", + "createdBy": "@items('Append_files_full')?['node']?['createdBy']", + "modifiedBy": "@items('Append_files_full')?['node']?['modifiedBy']", + "platform": "@variables('Platform')", + "physicalHost": "@variables('Physical_Host')" + } + } + } + } + }, + "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'), ' | ')}" + } + } + } + } + }, + "Select_files_for_quarantine_full": { + "runAfter": { + "Check_File_Hits_Failed_full": [ + "Succeeded" + ] + }, + "type": "Select", + "inputs": { + "from": "@variables('Files_To_Process')", + "select": { + "filename": "@item()?['filename']", + "filePath": "@item()?['nativePath']", + "ownerUpn": "@coalesce(item()?['createdBy'], item()?['modifiedBy'], '')", + "objectType": "@coalesce(item()?['platform'], variables('Object_Type'), '')", + "physicalHost": "@item()?['physicalHost']" + } + } + }, + "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')}

The listed files have been 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'))}

" + }, + "host": { + "connection": { + "name": "@parameters('$connections')['azuresentinel']['connectionId']" + } + }, + "method": "post", + "path": "/Incidents/Comment" + } + } + } + }, + "Classification": { + "case": "Classification", + "actions": { + "List_file_hits_reduced": { + "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" + }, + "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']", + "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')" + } + } + } + } + }, + "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'), ' | ')}" + } + } + } + } + }, + "Select_files_for_quarantine_reduced": { + "runAfter": { + "Check_File_Hits_Failed_reduced": [ + "Succeeded" + ] + }, + "type": "Select", + "inputs": { + "from": "@variables('Files_To_Process')", + "select": { + "filename": "@item()?['filename']", + "filePath": "@item()?['nativePath']", + "ownerUpn": "@coalesce(item()?['createdBy'], item()?['modifiedBy'], '')", + "objectType": "@coalesce(item()?['platform'], variables('Object_Type'), '')", + "physicalHost": "@item()?['physicalHost']" + } + } + }, + "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'))}

The listed files have been processed for quarantine. See the Sentinel incident for per-file results.

" + } + } + }, + "Comment_reduced_summary": { + "runAfter": { + "Notify_owner_reduced": [ + "Succeeded", + "Failed" + ] + }, + "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).

" + }, + "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" + } + } + } + } + } + } + }, + "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 Sentinel incident.", + "prerequisites": [ + "Deploy the RubrikCustomConnector (Microsoft.Web/customApis) first; this playbook uses its 'Authentication' (/api/client_token) operation to obtain the RSC bearer token.", + "Deploy the RubrikQuarantineFiles sub-playbook (HTTP Request-triggered) before this playbook; it is called once per file to quarantine and must return { status, newLocation }.", + "An Azure Key Vault holding the RSC service-account credentials in secrets 'Rubrik-AS-Int-ClientId' and 'Rubrik-AS-Int-ClientSecret'.", + "An Azure Storage account and blob container for the exported CSV/log files (SecurityViolation flow only).", + "A Microsoft Teams team and channel for owner notifications.", + "An analytics rule that maps the Rubrik event fields into incident Custom Details (eventType, objectId, objectName, objectType, seriesId, eventName, snapshotId)." + ], + "tags": [ + "Microsoft Sentinel", + "Incident", + "Rubrik", + "DSPM", + "Remediation", + "Quarantine" + ], + "postDeployment": [ + "Grant the Logic App system-assigned managed identity the 'Microsoft Sentinel Responder' role on the Sentinel workspace (comment on and update incidents).", + "Grant the Logic App managed identity 'Key Vault Secrets User' (or an access policy with Get on secrets) on the Key Vault named by KeyVaultName.", + "Authorize the RubrikCustomConnector API connection (used for the /api/client_token authentication call).", + "Authorize (sign in to) the Microsoft Teams API connection so the owner-notification card can be posted.", + "Authorize the Azure Blob Storage API connection (StorageAccountName). The account used by that connection must have write access to StorageContainerName; the connection is used to upload the CSV/log and to create the read-only SAS link.", + "Deploy the RubrikQuarantineFiles sub-playbook and set RubrikQuarantineFilesPlaybookName to its Logic App name; it must exist in the same resource group and expose an HTTP Request trigger returning { status, newLocation }.", + "Create a Microsoft Sentinel automation rule to run this playbook on the relevant DSPM incidents (playbooks do not auto-attach)." + ], + "releaseNotes": [ + { + "version": "1.2.0", + "title": "Rubrik DSPM Violation Remediation", + "notes": [ + "Hardened the Download Snapshot Results CSV and Download Remediation Log flows. Validate the incident-supplied Object Name (case-insensitive), Resource ID, and Snapshot ID against the policyViolation and hard-fail on mismatch. Locate the exported file in allUserFiles by object-name filename pattern (spaces replaced with hyphens for the CSV) in addition to file type and post-start-time, and fail the export (non-fatal) when more than one file matches after the action start time. Fixed the remediation-log filter to match REMEDIATION_ACTIONS_LOG_CSV instead of SNAPSHOT_RESULTS_CSV. CSV export-trigger failure is now non-fatal (matches the remediation-log path); every download failure/edge case sets a descriptive link message so the final incident comment never shows a blank link. Download links in the incident comment are now rendered as the raw URL instead of an HTML anchor tag so they redirect correctly. Hardened createdAt comparison to use ticks()." + ] + }, + { + "version": "1.1.0", + "title": "Rubrik DSPM Violation Remediation", + "notes": [ + "Added physicalHost to the per-file records sent to RubrikQuarantineFiles so the NTFS (WINDOWS_FILESHARE) branch can resolve the Microsoft Defender for Endpoint device. physicalHost is captured once from the DSPM violation (resourceMetadata.metadata.physicalHost) and stamped onto every file. Removed the ONEDRIVE fallback when selecting a file's objectType, so an unknown objectType is no longer coerced to ONEDRIVE (the child returns NotImplemented instead)." + ] + }, + { + "version": "1.0.0", + "title": "Rubrik DSPM Violation Remediation", + "notes": [ + "Initial version. Handles SecurityViolation (full flow) and ClassificationResultsAvailable (reduced flow)." + ] + } + ], + "lastUpdateTime": "2026-07-22T00:00:00Z" + } + }, + "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": "RubrikIRViolationRemediation Playbook with template version 3.5.2", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('playbookVersion20')]", + "parameters": { + "PlaybookName": { + "defaultValue": "RubrikIRViolationRemediation", + "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-Remediation", + "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 Remediation 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 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']}" + } + } + } + } + }, + "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 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": { + "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 Remediation 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 Remediation", + "description": "Incident-triggered playbook that reads a Rubrik IR (Identity) violation ID from the 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": [ + "Deploy the RubrikCustomConnector (Microsoft.Web/customApis) first; this playbook uses its 'Authentication' (/api/client_token) operation to obtain the RSC bearer token.", + "A Rubrik Security Cloud service account (client_id + client_secret) with permission to read and update policy violations.", + "An Azure Key Vault containing the Rubrik client_id and client_secret secrets.", + "A Microsoft Teams team/channel for SOC notifications." + ], + "entities": [ + "Account" + ], + "tags": [ + "Microsoft Sentinel", + "Incident", + "Rubrik", + "Identity Resilience", + "Remediation" + ], + "postDeployment": [ + "Deploy the RubrikCustomConnector template before this playbook and pass its name via the 'Rubrik Connector name' parameter.", + "Grant the Logic App's system-assigned Managed Identity the 'Microsoft Sentinel Responder' role on the Sentinel workspace.", + "Grant the Logic App's system-assigned Managed Identity the 'Key Vault Secrets User' role (or a get-secret access policy) on the Key Vault named by the KeyVaultName parameter.", + "Store the Rubrik service-account credentials in Key Vault as two secrets named 'Rubrik-AS-Int-ClientId' and 'Rubrik-AS-Int-ClientSecret' (same secrets used by the other Rubrik playbooks).", + "Authorize the RubrikCustomConnector API connection in the portal.", + "Authorize the Microsoft Teams API connection in the portal (sign in with an account that can post to the target SOC channel), and supply the TeamsGroupId and TeamsChannelId parameters.", + "Create a Sentinel automation rule that runs this playbook on incidents produced by the Rubrik IR-violation analytics rule." + ], + "releaseNotes": [ + { + "version": "1.0.0.0", + "title": "Initial release", + "notes": [ + "Retrieve Rubrik IR violation, post a formatted notification to a SOC Teams channel and a matching incident comment, mark violation In Progress, with full API error handling." + ] + } + ], + "lastUpdateTime": "2026-07-10T00:00:00Z" + } + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('_playbookContentId20')]", + "contentKind": "Playbook", + "displayName": "RubrikIRViolationRemediation", + "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.2", + "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." + } + }, + "GraphClientIdSecretName": { + "defaultValue": "Graph-App-ClientId", + "type": "string", + "metadata": { + "description": "Key Vault secret name holding the Graph app registration client id." + } + }, + "GraphClientSecretSecretName": { + "defaultValue": "Graph-App-ClientSecret", + "type": "string", + "metadata": { + "description": "Key Vault secret name holding the Graph app registration client secret." + } + }, + "SearchRegion": { + "defaultValue": "IND", + "type": "string", + "metadata": { + "description": "ISO 3166-1 region code REQUIRED by Microsoft Graph /search/query for application (app-only) permissions. Must match the tenant's data location (e.g. NAM, EUR, GBR, APC, AUS, CAN, IND, JPN). Without it Graph returns 'Region is required when request with application permission.'" + } + } + }, + "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.0.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" + }, + "ownerUpn": { + "type": "string" + }, + "objectType": { + "type": "string" + }, + "physicalHost": { + "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(variables('Object_Type'), 'O365_SITE'), equals(variables('Object_Type'), 'O365_ONEDRIVE'), equals(variables('Object_Type'), 'O365_MAILBOX')), 'ONEDRIVE', if(or(contains(variables('Object_Type'), 'WINDOWS_FILESHARE'), 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_Folder_Name": { + "runAfter": { + "Initialize_Access_Token": [ + "Succeeded" + ] + }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "Quarantine_Folder_Name", + "type": "string" + } + ] + } + }, + "Initialize_Failure_Message": { + "runAfter": { + "Initialize_Quarantine_Folder_Name": [ + "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_MDE_Access_Token": { + "runAfter": { + "Initialize_Matched_Resource": [ + "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_Ntfs_File_Handled": [ + "Succeeded" + ] + }, + "type": "ApiConnection", + "inputs": { + "host": { + "connection": { + "name": "@parameters('$connections')['keyvault']['connectionId']" + } + }, + "method": "get", + "path": "[[concat('/secrets/@{encodeURIComponent(''', parameters('GraphClientIdSecretName'), ''')}/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": "[[concat('/secrets/@{encodeURIComponent(''', parameters('GraphClientSecretSecretName'), ''')}/value')]" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Get_Graph_Token": { + "runAfter": { + "Get_Graph_Client_Secret": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "[[concat('https://login.microsoftonline.com/', parameters('TenantId'), '/oauth2/v2.0/token')]", + "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')" + } + } + } + } + }, + "Parse_Graph_Token": { + "runAfter": { + "Check_Get_Graph_Token": [ + "Succeeded" + ] + }, + "type": "ParseJson", + "inputs": { + "content": "@body('Get_Graph_Token')", + "schema": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "token_type": { + "type": "string" + } + } + } + } + }, + "Set_Access_Token": { + "runAfter": { + "Parse_Graph_Token": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Access_Token", + "value": "@{body('Parse_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": "Enter the quarantine folder name. @{length(coalesce(triggerBody()?['files'], json('[]')))} file(s) will be moved into a folder with this name inside each file owner's OneDrive (the folder is created if it does not exist)." + }, + { + "type": "Input.Text", + "id": "quarantineFolderName", + "label": "Quarantine folder name", + "placeholder": "e.g. Quarantine", + "isRequired": true, + "errorMessage": "Please enter a folder name." + } + ], + "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_Folder_Name": { + "runAfter": { + "Post_folder_card_and_wait": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Quarantine_Folder_Name", + "value": "@{trim(coalesce(body('Post_folder_card_and_wait')?['data']?['quarantineFolderName'], ''))}" + } + }, + "If_folder_name_provided": { + "runAfter": { + "Set_Quarantine_Folder_Name": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "not": { + "equals": [ + "@variables('Quarantine_Folder_Name')", + "" + ] + } + } + ] + }, + "actions": { + "For_each_file": { + "type": "Foreach", + "foreach": "@coalesce(triggerBody()?['files'], json('[]'))", + "runtimeConfiguration": { + "concurrency": { + "repetitions": 1 + } + }, + "actions": { + "Search_file": { + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Graph_Base_URL')}/v1.0/search/query", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json" + }, + "body": { + "requests": [ + { + "entityTypes": [ + "driveItem" + ], + "query": { + "queryString": "@{coalesce(items('For_each_file')?['filename'], items('For_each_file')?['filePath'])}" + }, + "region": "[[parameters('SearchRegion')]", + "from": 0, + "size": 25 + } + ] + } + } + }, + "Check_Search_file": { + "runAfter": { + "Search_file": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "or": [ + { + "less": [ + "@coalesce(outputs('Search_file')?['statusCode'], 0)", + 200 + ] + }, + { + "greaterOrEquals": [ + "@coalesce(outputs('Search_file')?['statusCode'], 0)", + 300 + ] + } + ] + }, + "actions": { + "Append_result_search_failed": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "Failed to search for the file in OneDrive (Graph status @{coalesce(outputs('Search_file')?['statusCode'], 'none')}). @{string(coalesce(body('Search_file'), ''))}" + } + } + } + }, + "else": { + "actions": { + "Parse_search_result": { + "type": "ParseJson", + "inputs": { + "content": "@body('Search_file')", + "schema": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { + "type": "object", + "properties": { + "hitsContainers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "hits": { + "type": "array", + "items": { + "type": "object", + "properties": { + "resource": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "webUrl": { + "type": "string" + }, + "parentReference": { + "type": "object", + "properties": { + "driveId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "path": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + }, + "Filter_matching_hits": { + "runAfter": { + "Parse_search_result": [ + "Succeeded" + ] + }, + "type": "Query", + "inputs": { + "from": "@coalesce(first(first(body('Parse_search_result')?['value'])?['hitsContainers'])?['hits'], json('[]'))", + "where": "@and(equals(toLower(coalesce(item()?['resource']?['name'], '')), toLower(coalesce(items('For_each_file')?['filename'], ''))), or(equals(trim(coalesce(items('For_each_file')?['filePath'], '')), ''), endsWith(toLower(replace(replace(coalesce(item()?['resource']?['webUrl'], ''), '%20', ' '), '\\', '/')), toLower(replace(coalesce(items('For_each_file')?['filePath'], ''), '\\', '/')))))" + } + }, + "Set_Matched_Resource": { + "runAfter": { + "Filter_matching_hits": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Matched_Resource", + "value": "@coalesce(first(body('Filter_matching_hits'))?['resource'], first(coalesce(first(first(body('Parse_search_result')?['value'])?['hitsContainers'])?['hits'], json('[]')))?['resource'], json('{}'))" + } + }, + "If_file_found": { + "runAfter": { + "Set_Matched_Resource": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "not": { + "equals": [ + "@coalesce(variables('Matched_Resource')?['id'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Resolve_or_create_folder": { + "type": "Http", + "inputs": { + "method": "PATCH", + "uri": "@{variables('Graph_Base_URL')}/v1.0/drives/@{encodeURIComponent(variables('Matched_Resource')?['parentReference']?['driveId'])}/root:/@{encodeURIComponent(variables('Quarantine_Folder_Name'))}:", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json" + }, + "body": { + "@@microsoft.graph.conflictBehavior": "replace" + } + } + }, + "Append_result_folder_failed": { + "runAfter": { + "Resolve_or_create_folder": [ + "Failed", + "TimedOut" + ] + }, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "Found the file but failed to resolve/create the quarantine folder in its OneDrive (Graph status @{coalesce(outputs('Resolve_or_create_folder')?['statusCode'], 'none')}). @{string(coalesce(body('Resolve_or_create_folder'), ''))}" + } + } + }, + "Parse_folder": { + "runAfter": { + "Resolve_or_create_folder": [ + "Succeeded" + ] + }, + "type": "ParseJson", + "inputs": { + "content": "@body('Resolve_or_create_folder')", + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parentReference": { + "type": "object", + "properties": { + "driveId": { + "type": "string" + } + } + } + } + } + } + }, + "Download_file_content": { + "runAfter": { + "Parse_folder": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{variables('Graph_Base_URL')}/v1.0/drives/@{encodeURIComponent(variables('Matched_Resource')?['parentReference']?['driveId'])}/items/@{encodeURIComponent(variables('Matched_Resource')?['id'])}/content", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}" + }, + "redirect": { + "count": 0 + } + } + }, + "Check_Download_file_content": { + "runAfter": { + "Download_file_content": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "or": [ + { + "less": [ + "@coalesce(outputs('Download_file_content')?['statusCode'], 0)", + 200 + ] + }, + { + "greaterOrEquals": [ + "@coalesce(outputs('Download_file_content')?['statusCode'], 0)", + 300 + ] + } + ] + }, + { + "not": { + "equals": [ + "@outputs('Download_file_content')?['statusCode']", + 302 + ] + } + } + ] + }, + "actions": { + "Append_result_download_failed": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "Found the file but failed to download its content from OneDrive (Graph status @{coalesce(outputs('Download_file_content')?['statusCode'], 'none')})." + } + } + } + }, + "else": { + "actions": { + "Fetch_download_url": { + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{if(equals(outputs('Download_file_content')?['statusCode'], 302), coalesce(outputs('Download_file_content')?['headers']?['Location'], outputs('Download_file_content')?['headers']?['location']), concat(variables('Graph_Base_URL'), '/v1.0/drives/', encodeURIComponent(variables('Matched_Resource')?['parentReference']?['driveId']), '/items/', encodeURIComponent(variables('Matched_Resource')?['id']), '/content'))}" + } + }, + "Append_result_fetch_failed": { + "runAfter": { + "Fetch_download_url": [ + "Failed", + "TimedOut" + ] + }, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "Found the file but failed to fetch its content from the OneDrive download URL (status @{coalesce(outputs('Fetch_download_url')?['statusCode'], 'none')})." + } + } + }, + "Upload_file": { + "runAfter": { + "Fetch_download_url": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "method": "PUT", + "uri": "@{variables('Graph_Base_URL')}/v1.0/drives/@{encodeURIComponent(body('Parse_folder')?['parentReference']?['driveId'])}/items/@{encodeURIComponent(body('Parse_folder')?['id'])}:/@{encodeURIComponent(items('For_each_file')?['filename'])}:/content", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/octet-stream" + }, + "body": "@body('Fetch_download_url')" + } + }, + "Append_result_upload_failed": { + "runAfter": { + "Upload_file": [ + "Failed", + "TimedOut" + ] + }, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "Downloaded the file but failed to upload it to the quarantine folder (Graph status @{coalesce(outputs('Upload_file')?['statusCode'], 'none')}). Source NOT deleted. @{string(coalesce(body('Upload_file'), ''))}" + } + } + }, + "Parse_upload": { + "runAfter": { + "Upload_file": [ + "Succeeded" + ] + }, + "type": "ParseJson", + "inputs": { + "content": "@body('Upload_file')", + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "webUrl": { + "type": "string" + } + } + } + } + }, + "Delete_source_file": { + "runAfter": { + "Parse_upload": [ + "Succeeded" + ] + }, + "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": "Success", + "newLocation": "@body('Parse_upload')?['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.'))" + } + } + } + } + } + } + }, + "else": { + "actions": { + "Append_result_not_found": { + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "File not found in OneDrive via Graph search." + } + } + } + } + } + } + } + } + } + } + }, + "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(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" + } + }, + "Set_Failure_Message_no_folder": { + "runAfter": { + "Set_Overall_Status_no_folder": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "Failure_Message", + "value": "No quarantine folder name was provided by the analyst." + } + }, + "Append_no_folder_error": { + "runAfter": { + "Set_Failure_Message_no_folder": [ + "Succeeded" + ] + }, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "[variables('blanks')]", + "status": "Failed", + "newLocation": "[variables('blanks')]", + "error": "No quarantine folder name was provided by the analyst." + } + } + } + } + } + } + } + }, + "NTFS": { + "case": "NTFS", + "actions": { + "Get_MDE_Token": { + "type": "Http", + "inputs": { + "method": "POST", + "uri": "[[concat('https://login.microsoftonline.com/', parameters('TenantId'), '/oauth2/v2.0/token')]", + "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" + } + } + }, + "else": { + "actions": { + "Parse_MDE_Token": { + "type": "ParseJson", + "inputs": { + "content": "@body('Get_MDE_Token')", + "schema": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "token_type": { + "type": "string" + } + } + } + } + }, + "Set_MDE_Access_Token": { + "runAfter": { + "Parse_MDE_Token": [ + "Succeeded" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "MDE_Access_Token", + "value": "@{body('Parse_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 + } + }, + "If_physical_host_provided": { + "runAfter": { + "Reset_Ntfs_File_Handled": [ + "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 NTFS file; cannot resolve the MDE device to quarantine it." + } + } + }, + "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 '@{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']}" + } + } + } + } + }, + "Run_advanced_hunting_for_sha1": { + "runAfter": { + "Check_Get_MDE_Machine": [ + "Succeeded" + ] + }, + "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 == '@{variables('MDE_Machine_Id')}' and FileName == '@{items('For_each_ntfs_file')?['filename']}' 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": 30, + "timeout": "PT5M" + }, + "actions": { + "Wait_5_seconds": { + "type": "Wait", + "inputs": { + "interval": { + "count": 5, + "unit": "Second" + } + } + }, + "Get_action_status": { + "runAfter": { + "Wait_5_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), 'Failed', 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')" + } + } + } + } + } + } + } + }, + "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": "Child (HTTP Request-triggered) playbook that quarantines a BATCH of files. The caller (RubrikViolationRemediation) passes the whole files array in a single call; this playbook iterates internally so the sub-playbook is triggered once per incident instead of once per file. It branches on objectType into exactly three paths: ONEDRIVE, NTFS, and default. A single app registration (client id/secret read from Key Vault, tenant id from TenantId) is used for BOTH Microsoft Graph and Microsoft Defender for Endpoint (MDE) because both are Microsoft APIs on the same tenant; only the OAuth2 token scope differs per branch. ONEDRIVE (objectType containing ONEDRIVE, or equal to O365_SITE / O365_ONEDRIVE / O365_MAILBOX) is quarantined via Microsoft Graph (scope https://graph.microsoft.com/.default): the analyst is asked ONCE (per run) for a quarantine folder NAME via a Teams Adaptive Card, and each file is moved (search -> download -> upload to the resolved folder -> delete source only after a confirmed upload). NTFS (objectType WINDOWS_FILESHARE) is quarantined via MDE (scope https://api.securitycenter.microsoft.com/.default): the device is resolved by physicalHost (computerDnsName), each file's SHA1 is resolved via MDE advanced hunting (DeviceFileEvents by device + filename), a StopAndQuarantineFile machine action is created and polled (up to 30 attempts, 5s apart) at /api/machineactions/{id}; if the SHA1 cannot be resolved or the quarantine action fails, that file is recorded as Failed and the batch continues (no machine isolation). The default branch returns NotImplemented with the error 'not a valid objectType for quarantine'. Every API call is status-checked; auth-phase failures short-circuit and terminate the run; per-file failures are captured per file. Returns { status, results: [ { filename, status, newLocation, error } ] }.", + "prerequisites": [ + "A SINGLE Azure AD app registration used for BOTH Microsoft Graph and Microsoft Defender for Endpoint. It needs Microsoft Graph APPLICATION permissions Files.ReadWrite.All, Sites.ReadWrite.All, User.Read.All AND WindowsDefenderATP (Microsoft Defender for Endpoint) APPLICATION permissions Machine.ReadWrite.All (or Machine.StopAndQuarantine) and AdvancedQuery.Read.All (all admin-consented). Its client id and client secret are stored in Key Vault.", + "A Key Vault holding the app credentials in the secrets named by GraphClientIdSecretName and GraphClientSecretSecretName (these same credentials authenticate both Graph and MDE).", + "A Microsoft Teams team and channel for the folder-name Adaptive Card (used by the ONEDRIVE branch only).", + "For NTFS quarantine: the target file shares must be hosted on devices onboarded to Microsoft Defender for Endpoint, reachable by their physicalHost / computerDnsName.", + "The caller invokes this playbook with body { objectType, files: [ { filename, filePath, ownerUpn, objectType, physicalHost } ] }. For NTFS, physicalHost is REQUIRED per file (the MDE-onboarded host that stores the file share)." + ], + "tags": [ + "Microsoft Sentinel", + "Rubrik", + "DSPM", + "Quarantine", + "Microsoft Graph", + "Child Playbook" + ], + "postDeployment": [ + "Grant the Logic App managed identity Key Vault Secrets User (or an access policy with Get on secrets) on the Key Vault named by KeyVaultName.", + "Create/point ONE app registration used for both Microsoft services and store its client id/secret in Key Vault under GraphClientIdSecretName / GraphClientSecretSecretName. Grant it Microsoft Graph app permissions Files.ReadWrite.All, Sites.ReadWrite.All, User.Read.All AND WindowsDefenderATP app permissions Machine.ReadWrite.All (or Machine.StopAndQuarantine) and AdvancedQuery.Read.All, all admin-consented.", + "Authorize (sign in to) the Microsoft Teams API connection so the folder-name card can be posted and awaited (ONEDRIVE branch).", + "For NTFS quarantine, ensure the file-share hosts are onboarded to Microsoft Defender for Endpoint and resolvable by their physicalHost / computerDnsName.", + "This playbook has an HTTP Request trigger; the caller RubrikViolationRemediation references it by RubrikQuarantineFilesPlaybookName and must be in the same resource group.", + "objectType routing: ONEDRIVE (contains ONEDRIVE, or equals O365_SITE / O365_ONEDRIVE / O365_MAILBOX) -> Microsoft Graph move; NTFS (WINDOWS_FILESHARE) -> MDE StopAndQuarantineFile; anything else -> NotImplemented ('not a valid objectType for quarantine')." + ], + "releaseNotes": [ + { + "version": "3.0.0", + "title": "Rubrik Quarantine Files", + "notes": [ + "Added a real NTFS quarantine flow via Microsoft Defender for Endpoint (MDE): resolve device by physicalHost, resolve each file SHA1 via MDE advanced hunting, create a StopAndQuarantineFile machine action and poll /api/machineactions/{id} (30 x 5s). No machine-isolation fallback: on SHA1-miss or action failure the file is recorded Failed and the batch continues. Reworked objectType routing to exactly three branches (ONEDRIVE / NTFS / default) and removed the previous default fallback to ONEDRIVE; unknown objectTypes now return NotImplemented with 'not a valid objectType for quarantine'. The same app registration authenticates both Graph and MDE; only the token scope differs." + ] + }, + { + "version": "2.2.0", + "title": "Rubrik Quarantine Files", + "notes": [ + "Fixed wrong-file quarantine when the same filename exists in multiple OneDrive locations: the Graph /search hits are now disambiguated by filePath (owner drive + parent folder) before download/upload/delete, so the exact file at the reported location is moved and its source removed. Download no longer follows the Graph 302 with the bearer token; it fetches bytes from the returned Location URL." + ] + }, + { + "version": "2.1.0", + "title": "Rubrik Quarantine Files", + "notes": [ + "Added full API status-code handling: every Key Vault, Graph token, and per-file Graph call now stores a failure message on any non-200 status, returns a Failed response to the parent playbook, and terminates this playbook as Failed." + ] + }, + { + "version": "2.0.0", + "title": "Rubrik Quarantine Files", + "notes": [ + "Batch input (files array, single call). OAuth2 client-credentials Graph auth via Key Vault. Folder NAME prompt via user-posted Teams Adaptive Card; path-based folder resolution." + ] + } + ], + "lastUpdateTime": "2026-07-22T00:00:00Z" + } + }, + "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.2", + "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 + } + } + ], + "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://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fniralishah-crest%2FMSSentinelIntegrations%2Frefs%2Fheads%2Fmain%2Fazuredeploy_Connector_RubrikWebhookEvents_AzureFunction.json)\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 Name\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-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.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\tWorkspaceName\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 + } + } + ], + "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://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fniralishah-crest%2FMSSentinelIntegrations%2Frefs%2Fheads%2Fmain%2Fazuredeploy_Connector_RubrikWebhookEvents_AzureFunction.json)\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 Name\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" }, { @@ -34018,10 +40704,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-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.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\tWorkspaceName\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" @@ -34031,11 +40717,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')]" @@ -35012,7 +41698,7 @@ "description": "Rubrik Critical Anomaly rule matches Severity and if Critical severity found then generate the incident for each object.", "displayName": "Rubrik Critical Anomaly", "enabled": false, - "query": "Rubrik_Anomaly_Data_CL\n| where severity_s == \"critical\"\n", + "query": "Rubrik_Anomaly_Data_CL\n| where Severity == \"critical\"\n", "queryFrequency": "PT10M", "queryPeriod": "PT10M", "severity": "Medium", @@ -35023,10 +41709,10 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "RubrikSecurityCloudAzureFunctions", "dataTypes": [ "RubrikAnomalyData" - ], - "connectorId": "RubrikSecurityCloudAzureFunctions" + ] } ], "tactics": [ @@ -35039,11 +41725,11 @@ "aggregationKind": "AlertPerResult" }, "customDetails": { - "ClusterIdentifier": "custom_details_clusterId_g", - "ObjectId": "custom_details_objectId_g", - "ObjectType": "custom_details_objectType_s", - "ObjectName": "custom_details_objectName_s", - "ClusterName": "custom_details_clusterName_s" + "ObjectType": "ObjectType", + "ObjectId": "ObjectId", + "ClusterIdentifier": "ClusterId", + "ClusterName": "ClusterName", + "ObjectName": "ObjectName" }, "incidentConfiguration": { "createIncident": true @@ -35118,7 +41804,7 @@ "description": "Rubrik Threat Monitoring matches Event Name and if match found then generate the incident for each object.", "displayName": "Rubrik Threat Monitoring", "enabled": false, - "query": "Rubrik_Events_Data_CL\n| where custom_details_eventName_s endswith \"AnalysisMatchesFound\" or custom_details_eventName_s contains \"ThreatMonitoringHashCatalogAnalysisFailed\" or custom_details_eventName_s contains \"ThreatMonitoringHashMatchesFound\" or custom_details_eventName_s contains \"ThreatMonitoringYaraMatchesFound\"\n| extend hashMatchCount = toint(extract(\"Found ([0-9]+) hash matches\",1, summary_s)), yaraMatchCount = toint(extract(\"Found ([0-9]+) YARA rule matches\", 1, summary_s)), fileHashMatchCount = toint(extract(\"Found file hash matches for ([0-9]+) files\", 1, summary_s))\n| extend count_ = coalesce(hashMatchCount,yaraMatchCount, fileHashMatchCount), eventname = substring(custom_details_eventName_s,16, strlen(custom_details_eventName_s) - 28)\n| where count_ > 0\n| summarize arg_max(TimeGenerated,*) by eventname, custom_details_objectId_g, count_\n", + "query": "Rubrik_Events_Data_CL\n| where EventName endswith \"AnalysisMatchesFound\" or EventName contains \"ThreatMonitoringHashCatalogAnalysisFailed\" or EventName contains \"ThreatMonitoringHashMatchesFound\" or EventName contains \"ThreatMonitoringYaraMatchesFound\"\n| extend hashMatchCount = toint(extract(\"Found ([0-9]+) hash matches\",1, Summary)), yaraMatchCount = toint(extract(\"Found ([0-9]+) YARA rule matches\", 1, Summary)), fileHashMatchCount = toint(extract(\"Found file hash matches for ([0-9]+) files\", 1, Summary))\n| extend count_ = coalesce(hashMatchCount,yaraMatchCount, fileHashMatchCount), eventname = substring(EventName,16, strlen(EventName) - 28)\n| where count_ > 0\n| summarize arg_max(TimeGenerated,*) by eventname, ObjectId, count_\n", "queryFrequency": "PT10M", "queryPeriod": "PT10M", "severity": "Medium", @@ -35129,10 +41815,10 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "RubrikSecurityCloudAzureFunctions", "dataTypes": [ "RubrikEventsData" - ], - "connectorId": "RubrikSecurityCloudAzureFunctions" + ] } ], "tactics": [ @@ -35145,29 +41831,29 @@ "aggregationKind": "AlertPerResult" }, "customDetails": { - "ObjectType": "custom_details_objectType_s", - "Url": "custom_details_url_s", - "ClusterIdentifier": "custom_details_clusterId_g", - "Summary": "summary_s", - "ObjectId": "custom_details_objectId_g", - "EventName": "custom_details_eventName_s", - "ObjectName": "custom_details_objectName_s" + "Summary": "Summary", + "ClusterIdentifier": "ClusterId", + "ObjectName": "ObjectName", + "Url": "Url", + "ObjectType": "ObjectType", + "EventName": "EventName", + "ObjectId": "ObjectId" }, "alertDetailsOverride": { - "alertDisplayNameFormat": "ThreatMonitoring Found {{count_}} {{eventname}} Matches for {{custom_details_objectName_s}}" + "alertDisplayNameFormat": "ThreatMonitoring Found {{count_}} {{eventname}} Matches for {{ObjectName}}" }, "incidentConfiguration": { - "createIncident": true, "groupingConfiguration": { "groupByCustomDetails": [ "ObjectName", "ObjectId" ], "enabled": true, - "lookbackDuration": "P7D", + "reopenClosedIncident": false, "matchingMethod": "Selected", - "reopenClosedIncident": false - } + "lookbackDuration": "P7D" + }, + "createIncident": true } } }, @@ -35213,6 +41899,378 @@ "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.2", + "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 (ClassficationResultsAvailable) for non DSPM license users.", + "displayName": "Create Incident for Classification evnets for non DSPM license users", + "enabled": false, + "query": "Rubrik_Violation_Data_CL\n| where Class == \"Classification\" and EventName == \"ClassficationResultsAvailable\"\n| extend NewSeverity = case(Severity==\"info\", \"Low\",\n Severity==\"warning\", \"Medium\",\n Severity==\"critical\", \"High\",\n \"Informational\")\n", + "queryFrequency": "PT5M", + "queryPeriod": "PT5M", + "severity": "Medium", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "connectorId": "RubrikSecurityCloudAzureFunctions", + "dataTypes": [ + "RubrikViolationData" + ] + } + ], + "tactics": [ + "Collection" + ], + "techniques": [ + "T1213" + ], + "eventGroupingSettings": { + "aggregationKind": "AlertPerResult" + }, + "customDetails": { + "Summary": "Summary", + "EventTimestamp": "EventTimestamp", + "SnapshotId": "SnapshotId", + "EventName": "EventName", + "ObjectName": "ObjectName", + "Severity": "Severity", + "ObjectType": "ObjectType", + "ObjectId": "ObjectId", + "EventType": "Class" + }, + "alertDetailsOverride": { + "alertDisplayNameFormat": "{{Class}} event occurred for object {{ObjectName}} with type {{ObjectType}}", + "alertDescriptionFormat": "{{Summary}}", + "alertSeverityColumnName": "NewSeverity" + }, + "incidentConfiguration": { + "groupingConfiguration": { + "groupByCustomDetails": [ + "ObjectId" + ], + "enabled": true, + "reopenClosedIncident": false, + "matchingMethod": "Selected", + "lookbackDuration": "P7D" + }, + "createIncident": true + } + } + }, + { + "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 evnets 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.2", + "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 == \"SecurityViolation\" and EventName in (\"CriticalSeverityDataViolationDetected\",\n\"CriticalSeverityDataViolationReOpen\",\n\"HighSeverityDataViolationDetected\",\n\"HighSeverityDataViolationReOpen\",\n\"LowOrMediumSeverityDataViolationDetected\",\n\"LowOrMediumSeverityDataViolationReOpen\")\n| extend NewSeverity = case(Severity==\"info\", \"Low\",\n Severity==\"warning\", \"Medium\",\n Severity==\"critical\", \"High\",\n \"Informational\")\n", + "queryFrequency": "PT5M", + "queryPeriod": "PT5M", + "severity": "Medium", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "connectorId": "RubrikSecurityCloudAzureFunctions", + "dataTypes": [ + "RubrikViolationData" + ] + } + ], + "tactics": [ + "Exfiltration" + ], + "techniques": [ + "T1567" + ], + "eventGroupingSettings": { + "aggregationKind": "AlertPerResult" + }, + "customDetails": { + "Summary": "Summary", + "EventTimestamp": "EventTimestamp", + "EventName": "EventName", + "ObjectName": "ObjectName", + "ViolationId": "SeriesId", + "Severity": "Severity", + "ObjectType": "ObjectType", + "ObjectId": "ObjectId", + "EventType": "Class" + }, + "alertDetailsOverride": { + "alertDisplayNameFormat": "{{Class}} event occurred for object {{ObjectName}} with type {{ObjectType}}", + "alertDescriptionFormat": "{{Summary}}", + "alertSeverityColumnName": "NewSeverity" + }, + "incidentConfiguration": { + "groupingConfiguration": { + "groupByCustomDetails": [ + "ViolationId" + ], + "enabled": true, + "reopenClosedIncident": false, + "matchingMethod": "Selected", + "lookbackDuration": "P7D" + }, + "createIncident": true + } + } + }, + { + "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.2", + "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 == \"IdentityViolation\" and EventName in (\"CriticalSeverityDataViolationDetected\",\n\"CriticalSeverityDataViolationReOpen\", \"HighSeverityDataViolationDetected\",\n\"HighSeverityDataViolationReOpen\", \"LowOrMediumSeverityDataViolationDetected\",\n\"LowOrMediumSeverityDataViolationReOpen\")) or\n(Class == \"IdentityAlerts\" and EventName in (\"CriticalSeverityIdentityAlertDetected\",\n\"CriticalSeverityIdentityAlertReOpened\", \"HighSeverityIdentityAlertDetected\",\n\"HighSeverityIdentityAlertReOpened\", \"LowOrMediumSeverityIdentityAlertDetected\",\n\"LowOrMediumSeverityIdentityAlertReOpened\"\n))\n| extend NewSeverity = case(Severity==\"info\", \"Low\",\n Severity==\"warning\", \"Medium\",\n Severity==\"critical\", \"High\",\n \"Informational\")\n", + "queryFrequency": "PT5M", + "queryPeriod": "PT5M", + "severity": "Medium", + "suppressionDuration": "PT1H", + "suppressionEnabled": false, + "triggerOperator": "GreaterThan", + "triggerThreshold": 0, + "status": "Available", + "requiredDataConnectors": [ + { + "connectorId": "RubrikSecurityCloudAzureFunctions", + "dataTypes": [ + "RubrikViolationData" + ] + } + ], + "tactics": [ + "CredentialAccess" + ], + "techniques": [ + "T1078" + ], + "eventGroupingSettings": { + "aggregationKind": "AlertPerResult" + }, + "customDetails": { + "Summary": "Summary", + "EventTimestamp": "EventTimestamp", + "EventName": "EventName", + "ObjectName": "ObjectName", + "ViolationId": "SeriesId", + "Severity": "Severity", + "ObjectType": "ObjectType", + "ObjectId": "ObjectId", + "EventType": "Class" + }, + "alertDetailsOverride": { + "alertDisplayNameFormat": "{{Class}} event occurred for object {{ObjectName}} with type {{ObjectType}}", + "alertDescriptionFormat": "{{Summary}}", + "alertSeverityColumnName": "NewSeverity" + }, + "incidentConfiguration": { + "groupingConfiguration": { + "groupByCustomDetails": [ + "ViolationId" + ], + "enabled": true, + "reopenClosedIncident": false, + "matchingMethod": "Selected", + "lookbackDuration": "P7D" + }, + "createIncident": true + } + } + }, + { + "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", @@ -35223,7 +42281,7 @@ "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: 2, 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: 2, 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')]", @@ -35338,6 +42396,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')]", @@ -35357,6 +42430,21 @@ "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]" } ] }, diff --git a/Solutions/RubrikSecurityCloud/Playbooks/RubrikDSPMViolationRemediation/README.md b/Solutions/RubrikSecurityCloud/Playbooks/RubrikDSPMViolationRemediation/README.md new file mode 100644 index 00000000000..3696f42dfa2 --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Playbooks/RubrikDSPMViolationRemediation/README.md @@ -0,0 +1,72 @@ +# 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, 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 file owner in Microsoft Teams, and marks the violation `REMEDIATED` only when every quarantine succeeded. For `ClassificationResultsAvailable` (no DSPM license) it runs a reduced flow: list file hits -> quarantine -> notify. Both flows write a final comment back to the Sentinel incident. + +### 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 file to quarantine and must return `{ status, newLocation }`. +5. 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. +6. An Azure Storage account and an EXISTING blob container for the exported CSV/log files (SecurityViolation flow only). +7. Obtain the Teams group id and channel id where the owner-notification card will be posted. +8. 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. + * 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. + * DownloadPollMaxAttempts: Maximum Download Bar poll attempts before giving up on an export. + +[![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 like Rubrik custom connector, teams, keyvault, azure blob storage. + +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 file. 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, newLocation }`. + +#### 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). diff --git a/Solutions/RubrikSecurityCloud/Playbooks/RubrikDSPMViolationRemediation/azuredeploy.json b/Solutions/RubrikSecurityCloud/Playbooks/RubrikDSPMViolationRemediation/azuredeploy.json new file mode 100644 index 00000000000..b6a8a1d2b19 --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Playbooks/RubrikDSPMViolationRemediation/azuredeploy.json @@ -0,0 +1,1968 @@ +{ + "$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 Sentinel incident.", + "prerequisites": [ + "Deploy the RubrikCustomConnector (Microsoft.Web/customApis) first; this playbook uses its 'Authentication' (/api/client_token) operation to obtain the RSC bearer token.", + "Deploy the RubrikQuarantineFiles sub-playbook (HTTP Request-triggered) before this playbook; it is called once per file to quarantine and must return { status, newLocation }.", + "An Azure Key Vault holding the RSC service-account credentials in secrets 'Rubrik-AS-Int-ClientId' and 'Rubrik-AS-Int-ClientSecret'.", + "An Azure Storage account and blob container for the exported CSV/log files (SecurityViolation flow only).", + "A Microsoft Teams team and channel for owner notifications.", + "An analytics rule that maps the Rubrik event fields into incident Custom Details (eventType, objectId, objectName, objectType, seriesId, eventName, snapshotId)." + ], + "entities": [], + "tags": ["Microsoft Sentinel", "Incident", "Rubrik", "DSPM", "Remediation", "Quarantine"], + "support": { "tier": "community" }, + "author": { "name": "Rubrik" }, + "postDeployment": [ + "Grant the Logic App system-assigned managed identity the 'Microsoft Sentinel Responder' role on the Sentinel workspace (comment on and update incidents).", + "Grant the Logic App managed identity 'Key Vault Secrets User' (or an access policy with Get on secrets) on the Key Vault named by KeyVaultName.", + "Authorize the RubrikCustomConnector API connection (used for the /api/client_token authentication call).", + "Authorize (sign in to) the Microsoft Teams API connection so the owner-notification card can be posted.", + "Authorize the Azure Blob Storage API connection (StorageAccountName). The account used by that connection must have write access to StorageContainerName; the connection is used to upload the CSV/log and to create the read-only SAS link.", + "Deploy the RubrikQuarantineFiles sub-playbook and set RubrikQuarantineFilesPlaybookName to its Logic App name; it must exist in the same resource group and expose an HTTP Request trigger returning { status, newLocation }.", + "Create a Microsoft Sentinel automation rule to run this playbook on the relevant DSPM incidents (playbooks do not auto-attach)." + ], + "releaseNotes": [ + { "version": "1.2.0", "title": "Rubrik DSPM Violation Remediation", "notes": ["Hardened the Download Snapshot Results CSV and Download Remediation Log flows. Validate the incident-supplied Object Name (case-insensitive), Resource ID, and Snapshot ID against the policyViolation and hard-fail on mismatch. Locate the exported file in allUserFiles by object-name filename pattern (spaces replaced with hyphens for the CSV) in addition to file type and post-start-time, and fail the export (non-fatal) when more than one file matches after the action start time. Fixed the remediation-log filter to match REMEDIATION_ACTIONS_LOG_CSV instead of SNAPSHOT_RESULTS_CSV. CSV export-trigger failure is now non-fatal (matches the remediation-log path); every download failure/edge case sets a descriptive link message so the final incident comment never shows a blank link. Download links in the incident comment are now rendered as the raw URL instead of an HTML anchor tag so they redirect correctly. Hardened createdAt comparison to use ticks()."] }, + { "version": "1.1.0", "title": "Rubrik DSPM Violation Remediation", "notes": ["Added physicalHost to the per-file records sent to RubrikQuarantineFiles so the NTFS (WINDOWS_FILESHARE) branch can resolve the Microsoft Defender for Endpoint device. physicalHost is captured once from the DSPM violation (resourceMetadata.metadata.physicalHost) and stamped onto every file. Removed the ONEDRIVE fallback when selecting a file's objectType, so an unknown objectType is no longer coerced to ONEDRIVE (the child returns NotImplemented instead)."] }, + { "version": "1.0.0", "title": "Rubrik DSPM Violation Remediation", "notes": ["Initial version. Handles SecurityViolation (full flow) and ClassificationResultsAvailable (reduced flow)."] } + ], + "lastUpdateTime": "2026-07-22T00:00:00Z" + }, + "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.2.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_Provided_Snapshot_Fid": ["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']}" } + } + } + } + }, + "For_each_alert": { + "runAfter": { "Is_Authentication_Failed": ["Succeeded"] }, + "type": "Foreach", + "foreach": "@triggerBody()?['object']?['properties']?['Alerts']", + "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": { "Set_Physical_Host_from_violation": ["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'))}" } + } + } + } + }, + "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(variables('CSV_Export_Start')))))" + } + }, + "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": [ + { "not": { "equals": ["@variables('Download_External_Id')", ""] } } + ] + }, + "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(variables('Log_Export_Start')))))" + } + }, + "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": [ + { "not": { "equals": ["@variables('Download_External_Id')", ""] } } + ] + }, + "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" + }, + "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']", + "createdBy": "@items('Append_files_full')?['node']?['createdBy']", + "modifiedBy": "@items('Append_files_full')?['node']?['modifiedBy']", + "platform": "@variables('Platform')", + "physicalHost": "@variables('Physical_Host')" + } + } + } + } + }, + "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": {} } + }, + "Select_files_for_quarantine_full": { + "runAfter": { "Check_File_Hits_Failed_full": ["Succeeded"] }, + "type": "Select", + "inputs": { + "from": "@variables('Files_To_Process')", + "select": { + "filename": "@item()?['filename']", + "filePath": "@item()?['nativePath']", + "ownerUpn": "@coalesce(item()?['createdBy'], item()?['modifiedBy'], '')", + "objectType": "@coalesce(item()?['platform'], variables('Object_Type'), '')", + "physicalHost": "@item()?['physicalHost']" + } + } + }, + "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')}

The listed files have been 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'))}

" + }, + "host": { "connection": { "name": "@parameters('$connections')['azuresentinel']['connectionId']" } }, + "method": "post", + "path": "/Incidents/Comment" + } + } + } + }, + "Classification": { + "case": "Classification", + "actions": { + "List_file_hits_reduced": { + "runAfter": {}, + "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" }, + "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']", + "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')" + } + } + } + } + }, + "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": {} } + }, + "Select_files_for_quarantine_reduced": { + "runAfter": { "Check_File_Hits_Failed_reduced": ["Succeeded"] }, + "type": "Select", + "inputs": { + "from": "@variables('Files_To_Process')", + "select": { + "filename": "@item()?['filename']", + "filePath": "@item()?['nativePath']", + "ownerUpn": "@coalesce(item()?['createdBy'], item()?['modifiedBy'], '')", + "objectType": "@coalesce(item()?['platform'], variables('Object_Type'), '')", + "physicalHost": "@item()?['physicalHost']" + } + } + }, + "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'))}

The listed files have been processed for quarantine. See the Sentinel incident for per-file results.

" + } + } + }, + "Comment_reduced_summary": { + "runAfter": { "Notify_owner_reduced": ["Succeeded", "Failed"] }, + "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).

" + }, + "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" + } + } + } + } + } + }, + "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..daa6d2596c6 --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Playbooks/RubrikIRViolation/README.md @@ -0,0 +1,57 @@ +# Rubrik IR Violation Remediation + +## 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. + +### 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 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 notification will be posted. + * TeamsChannelId: Id of the Teams Channel where the 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 like Rubrik custom connector, teams, 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. +2. Configure the automation rules to trigger the playbook. diff --git a/Solutions/RubrikSecurityCloud/Playbooks/RubrikIRViolation/azuredeploy.json b/Solutions/RubrikSecurityCloud/Playbooks/RubrikIRViolation/azuredeploy.json new file mode 100644 index 00000000000..038e80c4b45 --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Playbooks/RubrikIRViolation/azuredeploy.json @@ -0,0 +1,691 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "title": "Rubrik IR Violation Remediation", + "description": "Incident-triggered playbook that reads a Rubrik IR (Identity) violation ID from the 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": [ + "Deploy the RubrikCustomConnector (Microsoft.Web/customApis) first; this playbook uses its 'Authentication' (/api/client_token) operation to obtain the RSC bearer token.", + "A Rubrik Security Cloud service account (client_id + client_secret) with permission to read and update policy violations.", + "An Azure Key Vault containing the Rubrik client_id and client_secret secrets.", + "A Microsoft Teams team/channel for SOC notifications." + ], + "entities": ["Account"], + "tags": ["Microsoft Sentinel", "Incident", "Rubrik", "Identity Resilience", "Remediation"], + "support": { "tier": "community" }, + "author": { "name": "Crest Data" }, + "postDeployment": [ + "Deploy the RubrikCustomConnector template before this playbook and pass its name via the 'Rubrik Connector name' parameter.", + "Grant the Logic App's system-assigned Managed Identity the 'Microsoft Sentinel Responder' role on the Sentinel workspace.", + "Grant the Logic App's system-assigned Managed Identity the 'Key Vault Secrets User' role (or a get-secret access policy) on the Key Vault named by the KeyVaultName parameter.", + "Store the Rubrik service-account credentials in Key Vault as two secrets named 'Rubrik-AS-Int-ClientId' and 'Rubrik-AS-Int-ClientSecret' (same secrets used by the other Rubrik playbooks).", + "Authorize the RubrikCustomConnector API connection in the portal.", + "Authorize the Microsoft Teams API connection in the portal (sign in with an account that can post to the target SOC channel), and supply the TeamsGroupId and TeamsChannelId parameters.", + "Create a Sentinel automation rule that runs this playbook on incidents produced by the Rubrik IR-violation analytics rule." + ], + "releaseNotes": [ + { "version": "1.0.0.0", "title": "Initial release", "notes": ["Retrieve Rubrik IR violation, post a formatted notification to a SOC Teams channel and a matching incident comment, mark violation In Progress, with full API error handling."] } + ], + "lastUpdateTime": "2026-07-10T00:00:00Z" + }, + "parameters": { + "PlaybookName": { + "defaultValue": "RubrikIRViolationRemediation", + "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-Remediation", + "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 Remediation 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 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']}" + } + } + } + } + }, + "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 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": { + "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 Remediation 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..4de2fe6e579 --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Playbooks/RubrikQuarantineFiles/README.md @@ -0,0 +1,62 @@ +# 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 (RubrikViolationRemediation) passes the whole files array in a single call, and this playbook iterates internally and routes on objectType: OneDrive/O365 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. + +### 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. +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 the secrets named by GraphClientIdSecretName ('Graph-App-ClientId') & GraphClientSecretSecretName ('Graph-App-ClientSecret') for storing the app registration client_id and client_secret respectively (these same credentials authenticate both Graph and MDE). +3. Obtain the Teams group id and channel id where the folder-name Adaptive Card will be posted (used by the OneDrive 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 (RubrikViolationRemediation). + * TeamsGroupId: Id of the Teams Group where the folder-name Adaptive Card is posted. + * TeamsChannelId: Id of the Teams Channel where the folder-name 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. + * GraphClientIdSecretName: Key Vault secret name holding the app registration client id (default 'Graph-App-ClientId'). + * GraphClientSecretSecretName: Key Vault secret name holding the app registration client secret (default 'Graph-App-ClientSecret'). + * SearchRegion: ISO 3166-1 region code required by Microsoft Graph /search/query for app-only permissions; must match the tenant's data location (e.g. NAM, EUR, GBR, APC, AUS, CAN, IND, JPN). + +[![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 GraphClientIdSecretName / GraphClientSecretSecretName. +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 RubrikViolationRemediation references it by the RubrikQuarantineFilesPlaybookName parameter and must be deployed in the same resource group. Ensure the PlaybookName here matches that value. diff --git a/Solutions/RubrikSecurityCloud/Playbooks/RubrikQuarantineFiles/azuredeploy.json b/Solutions/RubrikSecurityCloud/Playbooks/RubrikQuarantineFiles/azuredeploy.json new file mode 100644 index 00000000000..c0038ab1bed --- /dev/null +++ b/Solutions/RubrikSecurityCloud/Playbooks/RubrikQuarantineFiles/azuredeploy.json @@ -0,0 +1,1278 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "title": "Rubrik Quarantine Files", + "description": "Child (HTTP Request-triggered) playbook that quarantines a BATCH of files. The caller (RubrikViolationRemediation) passes the whole files array in a single call; this playbook iterates internally so the sub-playbook is triggered once per incident instead of once per file. It branches on objectType into exactly three paths: ONEDRIVE, NTFS, and default. A single app registration (client id/secret read from Key Vault, tenant id from TenantId) is used for BOTH Microsoft Graph and Microsoft Defender for Endpoint (MDE) because both are Microsoft APIs on the same tenant; only the OAuth2 token scope differs per branch. ONEDRIVE (objectType containing ONEDRIVE, or equal to O365_SITE / O365_ONEDRIVE / O365_MAILBOX) is quarantined via Microsoft Graph (scope https://graph.microsoft.com/.default): the analyst is asked ONCE (per run) for a quarantine folder NAME via a Teams Adaptive Card, and each file is moved (search -> download -> upload to the resolved folder -> delete source only after a confirmed upload). NTFS (objectType WINDOWS_FILESHARE) is quarantined via MDE (scope https://api.securitycenter.microsoft.com/.default): the device is resolved by physicalHost (computerDnsName), each file's SHA1 is resolved via MDE advanced hunting (DeviceFileEvents by device + filename), a StopAndQuarantineFile machine action is created and polled (up to 30 attempts, 5s apart) at /api/machineactions/{id}; if the SHA1 cannot be resolved or the quarantine action fails, that file is recorded as Failed and the batch continues (no machine isolation). The default branch returns NotImplemented with the error 'not a valid objectType for quarantine'. Every API call is status-checked; auth-phase failures short-circuit and terminate the run; per-file failures are captured per file. Returns { status, results: [ { filename, status, newLocation, error } ] }.", + "prerequisites": [ + "A SINGLE Azure AD app registration used for BOTH Microsoft Graph and Microsoft Defender for Endpoint. It needs Microsoft Graph APPLICATION permissions Files.ReadWrite.All, Sites.ReadWrite.All, User.Read.All AND WindowsDefenderATP (Microsoft Defender for Endpoint) APPLICATION permissions Machine.ReadWrite.All (or Machine.StopAndQuarantine) and AdvancedQuery.Read.All (all admin-consented). Its client id and client secret are stored in Key Vault.", + "A Key Vault holding the app credentials in the secrets named by GraphClientIdSecretName and GraphClientSecretSecretName (these same credentials authenticate both Graph and MDE).", + "A Microsoft Teams team and channel for the folder-name Adaptive Card (used by the ONEDRIVE branch only).", + "For NTFS quarantine: the target file shares must be hosted on devices onboarded to Microsoft Defender for Endpoint, reachable by their physicalHost / computerDnsName.", + "The caller invokes this playbook with body { objectType, files: [ { filename, filePath, ownerUpn, objectType, physicalHost } ] }. For NTFS, physicalHost is REQUIRED per file (the MDE-onboarded host that stores the file share)." + ], + "entities": [], + "tags": ["Microsoft Sentinel", "Rubrik", "DSPM", "Quarantine", "Microsoft Graph", "Child Playbook"], + "support": { "tier": "community" }, + "author": { "name": "Rubrik" }, + "postDeployment": [ + "Grant the Logic App managed identity Key Vault Secrets User (or an access policy with Get on secrets) on the Key Vault named by KeyVaultName.", + "Create/point ONE app registration used for both Microsoft services and store its client id/secret in Key Vault under GraphClientIdSecretName / GraphClientSecretSecretName. Grant it Microsoft Graph app permissions Files.ReadWrite.All, Sites.ReadWrite.All, User.Read.All AND WindowsDefenderATP app permissions Machine.ReadWrite.All (or Machine.StopAndQuarantine) and AdvancedQuery.Read.All, all admin-consented.", + "Authorize (sign in to) the Microsoft Teams API connection so the folder-name card can be posted and awaited (ONEDRIVE branch).", + "For NTFS quarantine, ensure the file-share hosts are onboarded to Microsoft Defender for Endpoint and resolvable by their physicalHost / computerDnsName.", + "This playbook has an HTTP Request trigger; the caller RubrikViolationRemediation references it by RubrikQuarantineFilesPlaybookName and must be in the same resource group.", + "objectType routing: ONEDRIVE (contains ONEDRIVE, or equals O365_SITE / O365_ONEDRIVE / O365_MAILBOX) -> Microsoft Graph move; NTFS (WINDOWS_FILESHARE) -> MDE StopAndQuarantineFile; anything else -> NotImplemented ('not a valid objectType for quarantine')." + ], + "releaseNotes": [ + { "version": "3.0.0", "title": "Rubrik Quarantine Files", "notes": ["Added a real NTFS quarantine flow via Microsoft Defender for Endpoint (MDE): resolve device by physicalHost, resolve each file SHA1 via MDE advanced hunting, create a StopAndQuarantineFile machine action and poll /api/machineactions/{id} (30 x 5s). No machine-isolation fallback: on SHA1-miss or action failure the file is recorded Failed and the batch continues. Reworked objectType routing to exactly three branches (ONEDRIVE / NTFS / default) and removed the previous default fallback to ONEDRIVE; unknown objectTypes now return NotImplemented with 'not a valid objectType for quarantine'. The same app registration authenticates both Graph and MDE; only the token scope differs."] }, + { "version": "2.2.0", "title": "Rubrik Quarantine Files", "notes": ["Fixed wrong-file quarantine when the same filename exists in multiple OneDrive locations: the Graph /search hits are now disambiguated by filePath (owner drive + parent folder) before download/upload/delete, so the exact file at the reported location is moved and its source removed. Download no longer follows the Graph 302 with the bearer token; it fetches bytes from the returned Location URL."] }, + { "version": "2.1.0", "title": "Rubrik Quarantine Files", "notes": ["Added full API status-code handling: every Key Vault, Graph token, and per-file Graph call now stores a failure message on any non-200 status, returns a Failed response to the parent playbook, and terminates this playbook as Failed."] }, + { "version": "2.0.0", "title": "Rubrik Quarantine Files", "notes": ["Batch input (files array, single call). OAuth2 client-credentials Graph auth via Key Vault. Folder NAME prompt via user-posted Teams Adaptive Card; path-based folder resolution."] } + ], + "lastUpdateTime": "2026-07-22T00:00:00Z" + }, + "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." } + }, + "GraphClientIdSecretName": { + "defaultValue": "Graph-App-ClientId", + "type": "string", + "metadata": { "description": "Key Vault secret name holding the Graph app registration client id." } + }, + "GraphClientSecretSecretName": { + "defaultValue": "Graph-App-ClientSecret", + "type": "string", + "metadata": { "description": "Key Vault secret name holding the Graph app registration client secret." } + }, + "SearchRegion": { + "defaultValue": "IND", + "type": "string", + "metadata": { "description": "ISO 3166-1 region code REQUIRED by Microsoft Graph /search/query for application (app-only) permissions. Must match the tenant's data location (e.g. NAM, EUR, GBR, APC, AUS, CAN, IND, JPN). Without it Graph returns 'Region is required when request with application permission.'" } + } + }, + "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.0.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" }, + "ownerUpn": { "type": "string" }, + "objectType": { "type": "string" }, + "physicalHost": { "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(variables('Object_Type'), 'O365_SITE'), equals(variables('Object_Type'), 'O365_ONEDRIVE'), equals(variables('Object_Type'), 'O365_MAILBOX')), 'ONEDRIVE', if(or(contains(variables('Object_Type'), 'WINDOWS_FILESHARE'), 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_Folder_Name": { + "runAfter": { "Initialize_Access_Token": ["Succeeded"] }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { "name": "Quarantine_Folder_Name", "type": "string" } + ] + } + }, + "Initialize_Failure_Message": { + "runAfter": { "Initialize_Quarantine_Folder_Name": ["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_MDE_Access_Token": { + "runAfter": { "Initialize_Matched_Resource": ["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_Ntfs_File_Handled": ["Succeeded"] }, + "type": "ApiConnection", + "inputs": { + "host": { "connection": { "name": "@parameters('$connections')['keyvault']['connectionId']" } }, + "method": "get", + "path": "[concat('/secrets/@{encodeURIComponent(''', parameters('GraphClientIdSecretName'), ''')}/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": "[concat('/secrets/@{encodeURIComponent(''', parameters('GraphClientSecretSecretName'), ''')}/value')]" + }, + "runtimeConfiguration": { "secureData": { "properties": ["inputs", "outputs"] } } + }, + "Get_Graph_Token": { + "runAfter": { "Get_Graph_Client_Secret": ["Succeeded"] }, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "[concat('https://login.microsoftonline.com/', parameters('TenantId'), '/oauth2/v2.0/token')]", + "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": {} } + }, + "Parse_Graph_Token": { + "runAfter": { "Check_Get_Graph_Token": ["Succeeded"] }, + "type": "ParseJson", + "inputs": { + "content": "@body('Get_Graph_Token')", + "schema": { + "type": "object", + "properties": { + "access_token": { "type": "string" }, + "expires_in": { "type": "integer" }, + "token_type": { "type": "string" } + } + } + } + }, + "Set_Access_Token": { + "runAfter": { "Parse_Graph_Token": ["Succeeded"] }, + "type": "SetVariable", + "inputs": { "name": "Access_Token", "value": "@{body('Parse_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": "Enter the quarantine folder name. @{length(coalesce(triggerBody()?['files'], json('[]')))} file(s) will be moved into a folder with this name inside each file owner's OneDrive (the folder is created if it does not exist)." }, + { "type": "Input.Text", "id": "quarantineFolderName", "label": "Quarantine folder name", "placeholder": "e.g. Quarantine", "isRequired": true, "errorMessage": "Please enter a folder name." } + ], + "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_Folder_Name": { + "runAfter": { "Post_folder_card_and_wait": ["Succeeded"] }, + "type": "SetVariable", + "inputs": { "name": "Quarantine_Folder_Name", "value": "@{trim(coalesce(body('Post_folder_card_and_wait')?['data']?['quarantineFolderName'], ''))}" } + }, + "If_folder_name_provided": { + "runAfter": { "Set_Quarantine_Folder_Name": ["Succeeded"] }, + "type": "If", + "expression": { + "and": [ + { "not": { "equals": ["@variables('Quarantine_Folder_Name')", ""] } } + ] + }, + "actions": { + "For_each_file": { + "runAfter": {}, + "type": "Foreach", + "foreach": "@coalesce(triggerBody()?['files'], json('[]'))", + "runtimeConfiguration": { "concurrency": { "repetitions": 1 } }, + "actions": { + "Search_file": { + "runAfter": {}, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "@{variables('Graph_Base_URL')}/v1.0/search/query", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json" + }, + "body": { + "requests": [ + { + "entityTypes": ["driveItem"], + "query": { "queryString": "@{coalesce(items('For_each_file')?['filename'], items('For_each_file')?['filePath'])}" }, + "region": "[parameters('SearchRegion')]", + "from": 0, + "size": 25 + } + ] + } + } + }, + "Check_Search_file": { + "runAfter": { "Search_file": ["Succeeded", "Failed", "TimedOut"] }, + "type": "If", + "expression": { + "or": [ + { "less": ["@coalesce(outputs('Search_file')?['statusCode'], 0)", 200] }, + { "greaterOrEquals": ["@coalesce(outputs('Search_file')?['statusCode'], 0)", 300] } + ] + }, + "actions": { + "Append_result_search_failed": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "", + "error": "Failed to search for the file in OneDrive (Graph status @{coalesce(outputs('Search_file')?['statusCode'], 'none')}). @{string(coalesce(body('Search_file'), ''))}" + } + } + } + }, + "else": { + "actions": { + "Parse_search_result": { + "runAfter": {}, + "type": "ParseJson", + "inputs": { + "content": "@body('Search_file')", + "schema": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { + "type": "object", + "properties": { + "hitsContainers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "hits": { + "type": "array", + "items": { + "type": "object", + "properties": { + "resource": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "webUrl": { "type": "string" }, + "parentReference": { + "type": "object", + "properties": { + "driveId": { "type": "string" }, + "id": { "type": "string" }, + "path": { "type": "string" } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + }, + "Filter_matching_hits": { + "runAfter": { "Parse_search_result": ["Succeeded"] }, + "type": "Query", + "inputs": { + "from": "@coalesce(first(first(body('Parse_search_result')?['value'])?['hitsContainers'])?['hits'], json('[]'))", + "where": "@and(equals(toLower(coalesce(item()?['resource']?['name'], '')), toLower(coalesce(items('For_each_file')?['filename'], ''))), or(equals(trim(coalesce(items('For_each_file')?['filePath'], '')), ''), endsWith(toLower(replace(replace(coalesce(item()?['resource']?['webUrl'], ''), '%20', ' '), '\\', '/')), toLower(replace(coalesce(items('For_each_file')?['filePath'], ''), '\\', '/')))))" + } + }, + "Set_Matched_Resource": { + "runAfter": { "Filter_matching_hits": ["Succeeded"] }, + "type": "SetVariable", + "inputs": { + "name": "Matched_Resource", + "value": "@coalesce(first(body('Filter_matching_hits'))?['resource'], first(coalesce(first(first(body('Parse_search_result')?['value'])?['hitsContainers'])?['hits'], json('[]')))?['resource'], json('{}'))" + } + }, + "If_file_found": { + "runAfter": { "Set_Matched_Resource": ["Succeeded"] }, + "type": "If", + "expression": { + "and": [ + { "not": { "equals": ["@coalesce(variables('Matched_Resource')?['id'], '')", ""] } } + ] + }, + "actions": { + "Resolve_or_create_folder": { + "runAfter": {}, + "type": "Http", + "inputs": { + "method": "PATCH", + "uri": "@{variables('Graph_Base_URL')}/v1.0/drives/@{encodeURIComponent(variables('Matched_Resource')?['parentReference']?['driveId'])}/root:/@{encodeURIComponent(variables('Quarantine_Folder_Name'))}:", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/json" + }, + "body": { + "folder": {}, + "@@microsoft.graph.conflictBehavior": "replace" + } + } + }, + "Append_result_folder_failed": { + "runAfter": { "Resolve_or_create_folder": ["Failed", "TimedOut"] }, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "", + "error": "Found the file but failed to resolve/create the quarantine folder in its OneDrive (Graph status @{coalesce(outputs('Resolve_or_create_folder')?['statusCode'], 'none')}). @{string(coalesce(body('Resolve_or_create_folder'), ''))}" + } + } + }, + "Parse_folder": { + "runAfter": { "Resolve_or_create_folder": ["Succeeded"] }, + "type": "ParseJson", + "inputs": { + "content": "@body('Resolve_or_create_folder')", + "schema": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "parentReference": { "type": "object", "properties": { "driveId": { "type": "string" } } } + } + } + } + }, + "Download_file_content": { + "runAfter": { "Parse_folder": ["Succeeded"] }, + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{variables('Graph_Base_URL')}/v1.0/drives/@{encodeURIComponent(variables('Matched_Resource')?['parentReference']?['driveId'])}/items/@{encodeURIComponent(variables('Matched_Resource')?['id'])}/content", + "headers": { "Authorization": "Bearer @{variables('Access_Token')}" }, + "redirect": { "count": 0 } + } + }, + "Check_Download_file_content": { + "runAfter": { "Download_file_content": ["Succeeded", "Failed", "TimedOut"] }, + "type": "If", + "expression": { + "and": [ + { "or": [ + { "less": ["@coalesce(outputs('Download_file_content')?['statusCode'], 0)", 200] }, + { "greaterOrEquals": ["@coalesce(outputs('Download_file_content')?['statusCode'], 0)", 300] } + ] }, + { "not": { "equals": ["@outputs('Download_file_content')?['statusCode']", 302] } } + ] + }, + "actions": { + "Append_result_download_failed": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "", + "error": "Found the file but failed to download its content from OneDrive (Graph status @{coalesce(outputs('Download_file_content')?['statusCode'], 'none')})." + } + } + } + }, + "else": { + "actions": { + "Fetch_download_url": { + "runAfter": {}, + "type": "Http", + "inputs": { + "method": "GET", + "uri": "@{if(equals(outputs('Download_file_content')?['statusCode'], 302), coalesce(outputs('Download_file_content')?['headers']?['Location'], outputs('Download_file_content')?['headers']?['location']), concat(variables('Graph_Base_URL'), '/v1.0/drives/', encodeURIComponent(variables('Matched_Resource')?['parentReference']?['driveId']), '/items/', encodeURIComponent(variables('Matched_Resource')?['id']), '/content'))}" + } + }, + "Append_result_fetch_failed": { + "runAfter": { "Fetch_download_url": ["Failed", "TimedOut"] }, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "", + "error": "Found the file but failed to fetch its content from the OneDrive download URL (status @{coalesce(outputs('Fetch_download_url')?['statusCode'], 'none')})." + } + } + }, + "Upload_file": { + "runAfter": { "Fetch_download_url": ["Succeeded"] }, + "type": "Http", + "inputs": { + "method": "PUT", + "uri": "@{variables('Graph_Base_URL')}/v1.0/drives/@{encodeURIComponent(body('Parse_folder')?['parentReference']?['driveId'])}/items/@{encodeURIComponent(body('Parse_folder')?['id'])}:/@{encodeURIComponent(items('For_each_file')?['filename'])}:/content", + "headers": { + "Authorization": "Bearer @{variables('Access_Token')}", + "Content-Type": "application/octet-stream" + }, + "body": "@body('Fetch_download_url')" + } + }, + "Append_result_upload_failed": { + "runAfter": { "Upload_file": ["Failed", "TimedOut"] }, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "", + "error": "Downloaded the file but failed to upload it to the quarantine folder (Graph status @{coalesce(outputs('Upload_file')?['statusCode'], 'none')}). Source NOT deleted. @{string(coalesce(body('Upload_file'), ''))}" + } + } + }, + "Parse_upload": { + "runAfter": { "Upload_file": ["Succeeded"] }, + "type": "ParseJson", + "inputs": { + "content": "@body('Upload_file')", + "schema": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "webUrl": { "type": "string" } + } + } + } + }, + "Delete_source_file": { + "runAfter": { "Parse_upload": ["Succeeded"] }, + "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": "Success", + "newLocation": "@body('Parse_upload')?['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.'))" + } + } + } + } + } + } + }, + "else": { + "actions": { + "Append_result_not_found": { + "runAfter": {}, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { + "filename": "@items('For_each_file')?['filename']", + "status": "Failed", + "newLocation": "", + "error": "File not found in OneDrive via Graph search." + } + } + } + } + } + } + } + } + } + } + }, + "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(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" } + }, + "Set_Failure_Message_no_folder": { + "runAfter": { "Set_Overall_Status_no_folder": ["Succeeded"] }, + "type": "SetVariable", + "inputs": { "name": "Failure_Message", "value": "No quarantine folder name was provided by the analyst." } + }, + "Append_no_folder_error": { + "runAfter": { "Set_Failure_Message_no_folder": ["Succeeded"] }, + "type": "AppendToArrayVariable", + "inputs": { + "name": "Results", + "value": { "filename": "", "status": "Failed", "newLocation": "", "error": "No quarantine folder name was provided by the analyst." } + } + } + } + } + } + } + }, + "NTFS": { + "case": "NTFS", + "actions": { + "Get_MDE_Token": { + "runAfter": {}, + "type": "Http", + "inputs": { + "method": "POST", + "uri": "[concat('https://login.microsoftonline.com/', parameters('TenantId'), '/oauth2/v2.0/token')]", + "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" } + } + }, + "else": { + "actions": { + "Parse_MDE_Token": { + "runAfter": {}, + "type": "ParseJson", + "inputs": { + "content": "@body('Get_MDE_Token')", + "schema": { + "type": "object", + "properties": { + "access_token": { "type": "string" }, + "expires_in": { "type": "integer" }, + "token_type": { "type": "string" } + } + } + } + }, + "Set_MDE_Access_Token": { + "runAfter": { "Parse_MDE_Token": ["Succeeded"] }, + "type": "SetVariable", + "inputs": { "name": "MDE_Access_Token", "value": "@{body('Parse_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 } + }, + "If_physical_host_provided": { + "runAfter": { "Reset_Ntfs_File_Handled": ["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 NTFS file; cannot resolve the MDE device to quarantine it." + } + } + }, + "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 '@{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']}" } + } + } + } + }, + "Run_advanced_hunting_for_sha1": { + "runAfter": { "Check_Get_MDE_Machine": ["Succeeded"] }, + "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 == '@{variables('MDE_Machine_Id')}' and FileName == '@{items('For_each_ntfs_file')?['filename']}' 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": 30, "timeout": "PT5M" }, + "actions": { + "Wait_5_seconds": { + "runAfter": {}, + "type": "Wait", + "inputs": { "interval": { "count": 5, "unit": "Second" } } + }, + "Get_action_status": { + "runAfter": { "Wait_5_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), 'Failed', 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')" + } + } + } + } + } + } + }, + "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" } + } + } + } + } + } + } + } + ] +}