diff --git a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/BloodHoundAzureFunction.zip b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/BloodHoundAzureFunction.zip index 0db93870c80..800de464c88 100644 Binary files a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/BloodHoundAzureFunction.zip and b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/BloodHoundAzureFunction.zip differ diff --git a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/attack_path_collector.py b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/attack_path_collector.py index 802bb9554c8..01a2cda879a 100644 --- a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/attack_path_collector.py +++ b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/attack_path_collector.py @@ -1,9 +1,10 @@ import logging import time import datetime +import json from typing import Dict, List, Tuple, Optional, Any from dataclasses import dataclass -from ..utility.utils import load_environment_configs, EnvironmentConfig, AzureConfig +from ..utility.utils import load_environment_configs, EnvironmentConfig, AzureConfig, get_lookup_days, get_azure_batch_size from ..utility.bloodhound_manager import BloodhoundManager @dataclass @@ -104,12 +105,12 @@ def collect_attack_paths( bloodhound_manager: BloodhoundManager, domains: List[Dict[str, Any]], tenant_domain: str, - last_attack_path_timestamps: Dict[str, Dict[str, str]], - default_lookback_days: int = 1 + last_attack_path_timestamps: Dict[str, Dict[str, str]] ) -> Tuple[List[Dict[str, Any]], Dict[str, str]]: """Collect attack paths for each domain and finding type.""" all_collected_paths = [] domain_latest_timestamps = {} + default_lookback_days = get_lookup_days() for domain in domains: domain_id = domain.get("id") @@ -153,6 +154,64 @@ def collect_attack_paths( return all_collected_paths, domain_latest_timestamps +def _prepare_attack_path_log_entry(attack_data: Dict[str, Any], unique_finding_types_data: Dict[str, Any], + tenant_domain: str, domains_data: List[Dict[str, Any]]) -> Dict[str, Any]: + """Helper function to prepare a single attack path log entry.""" + domain_name = "" + for domain in domains_data: + if domain.get("id") == attack_data.get("DomainSID"): + domain_name = domain.get("name", "") + break + + finding_type = attack_data.get("Finding", "") + path_title = unique_finding_types_data.get(finding_type, "") + short_description = unique_finding_types_data.get(f"{finding_type}_short_description", "") + short_remediation = unique_finding_types_data.get(f"{finding_type}_short_remediation", "") + long_remediation = unique_finding_types_data.get(f"{finding_type}_long_remediation", "") + + return { + "Accepted": attack_data.get("Accepted", False), + "AcceptedUntil": attack_data.get("AcceptedUntil", ""), + "ComboGraphRelationID": str(attack_data.get("ComboGraphRelationID", "")), + "created_at": attack_data.get("created_at", ""), + "deleted_at": json.dumps(attack_data.get("deleted_at", {})), + "DomainSID": attack_data.get("DomainSID", ""), + "Environment": attack_data.get("Environment", ""), + "ExposureCount": attack_data.get("ExposureCount", 0), + "ExposurePercentage": str(round(float(attack_data.get("ExposurePercentage", "0")) * 100, 2)), + "Finding": attack_data.get("Finding", ""), + "NonTierZeroPrincipalEnvironment": attack_data.get("FromEnvironment", ""), + "NonTierZeroPrincipalEnvironmentID": attack_data.get("FromEnvironmentID", ""), + "NonTierZeroPrincipal": attack_data.get("FromPrincipal", ""), + "NonTierZeroPrincipalKind": attack_data.get("FromPrincipalKind", ""), + "NonTierZeroPrincipalName": attack_data.get("FromPrincipalName", ""), + "NonTierZeroPrincipalProps": json.dumps(attack_data.get("FromPrincipalProps", {})), + "id": int(attack_data.get("id", 0)), + "ImpactCount": attack_data.get("ImpactCount", 0), + "ImpactPercentage": str(round(float(attack_data.get("ImpactPercentage", "0")) * 100, 2)), + "IsInherited": str(attack_data.get("IsInherited", "")), + "Principal": attack_data.get("ToPrincipal", ""), + "PrincipalHash": attack_data.get("PrincipalHash", ""), + "PrincipalKind": attack_data.get("ToPrincipalKind", ""), + "PrincipalName": attack_data.get("ToPrincipalName", ""), + "RelProps": json.dumps(attack_data.get("RelProps", {})), + "Severity": attack_data.get("Severity", ""), + "ImpactedPrincipalEnvironment": attack_data.get("ToEnvironment", attack_data.get("Environment")), + "ImpactedPrincipalEnvironmentID": attack_data.get("ToEnvironmentID", ""), + "ImpactedPrincipal": attack_data.get("ToPrincipal", attack_data.get("Principal")), + "ImpactedPrincipalKind": attack_data.get("ToPrincipalKind", attack_data.get("PrincipalKind")), + "ImpactedPrincipalName": attack_data.get("ToPrincipalName", attack_data.get("PrincipalName")), + "ImpactedPrincipalProps": json.dumps(attack_data.get("ToPrincipalProps", attack_data.get("Props"))), + "updated_at": attack_data.get("updated_at", ""), + "PathTitle": path_title, + "ShortDescription": short_description, + "ShortRemediation": short_remediation, + "LongRemediation": long_remediation, + "tenant_url": tenant_domain, + "domain_name": domain_name, + "Remediation": f"{tenant_domain}ui/remediation?findingType={finding_type}", + } + def send_attack_paths_to_azure_monitor( attack_paths: List[Dict[str, Any]], bloodhound_manager: BloodhoundManager, @@ -161,34 +220,58 @@ def send_attack_paths_to_azure_monitor( tenant_domain: str, domains_data: List[Dict[str, Any]] ) -> Tuple[int, int]: - """Send collected attack paths to Azure Monitor.""" + """Send collected attack paths to Azure Monitor in batches.""" successful_submissions = 0 failed_submissions = 0 + batch_size = get_azure_batch_size() if not attack_paths: logging.info("No attack path details to send to Azure Monitor.") return successful_submissions, failed_submissions - logging.info(f"Sending {len(attack_paths)} collected attack path details to Azure Monitor.") - for i, data_item in enumerate(attack_paths, 1): - result = bloodhound_manager.send_attack_data( - data_item, + logging.info(f"Sending {len(attack_paths)} collected attack path details to Azure Monitor in batches of {batch_size}.") + + # Process in batches + for batch_start in range(0, len(attack_paths), batch_size): + batch_end = min(batch_start + batch_size, len(attack_paths)) + batch = attack_paths[batch_start:batch_end] + + # Prepare log entries for this batch + log_entries = [] + for data_item in batch: + try: + log_entry = _prepare_attack_path_log_entry( + data_item, finding_types_data, tenant_domain, domains_data + ) + log_entries.append(log_entry) + except Exception as e: + failed_submissions += 1 + logging.error(f"Failed to prepare attack path log entry for ID {data_item.get('id')}: {str(e)}") + + if not log_entries: + continue + + # Send batch to Azure Monitor + logging.info(f"Sending batch {batch_start//batch_size + 1} ({len(log_entries)} entries): IDs {[item.get('id') for item in batch[:5]]}...") + result = bloodhound_manager._send_to_azure_monitor( + log_entries, azure_monitor_token, - finding_types_data, - tenant_domain, - domains_data + bloodhound_manager.dce_uri, + bloodhound_manager.dcr_immutable_id, + bloodhound_manager.table_name ) - logging.info(f"Processing attack path log entry {i}/{len(attack_paths)}: {data_item.get('id')}") - + if result and result.get("status") == "success": - successful_submissions += 1 - logging.info(f"Successfully sent attack path for '{data_item.get('id')}'") + entries_sent = result.get("entries_sent", len(log_entries)) + successful_submissions += entries_sent + logging.info(f"Successfully sent batch of {entries_sent} attack paths") else: - failed_submissions += 1 - logging.error(f"Failed to send attack path for '{data_item.get('id')}': {result.get('message', 'Unknown error')}") + failed_submissions += len(log_entries) + logging.error(f"Failed to send batch: {result.get('message', 'Unknown error')}") - time.sleep(0.1) # Rate limiting between requests + # Rate limiting is handled automatically by azure_monitor_rate_limiter in _send_to_azure_monitor() + logging.info(f"Attack path sending complete. Successful: {successful_submissions}, Failed: {failed_submissions}") return successful_submissions, failed_submissions def process_environment( diff --git a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/attack_path_timeline_collector.py b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/attack_path_timeline_collector.py index da03e340546..fc71271ff17 100644 --- a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/attack_path_timeline_collector.py +++ b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/attack_path_timeline_collector.py @@ -1,8 +1,7 @@ import logging import time -import json import datetime -from ..utility.utils import load_environment_configs +from ..utility.utils import load_environment_configs, get_azure_batch_size from ..utility.bloodhound_manager import BloodhoundManager @@ -111,14 +110,37 @@ def update_timestamps(domain_entries, current_tenant_domain, domain_name, last_t logging.info(f"Updated last_attack_path_timeline_timestamps for {current_tenant_domain}/{domain_name} to {latest_timestamp}") -def submit_attack_path_data(bloodhound_manager, attack_data, token, unique_finding_types_data, final_domains): - """Submit a single attack path data entry to Azure Monitor.""" - logging.info(f"Sending attack data: ID {attack_data.get('id')}") - result = bloodhound_manager.send_attack_path_timeline_data( - attack_data, token, unique_finding_types_data, final_domains - ) - logging.info(f"Result of sending attack data ID {attack_data.get('id')} is {result}") - return {"id": attack_data.get("id"), "status": "success", "response": result} +def _prepare_attack_path_timeline_log_entry(attack_data: dict, unique_finding_types_data: dict, + tenant_domain: str, domains_data: list) -> dict: + """Helper function to prepare a single attack path timeline log entry.""" + domain_name = "" + # Find the domain name from the domains_data based on DomainSID + for domain in domains_data: + if domain.get("id") == attack_data.get("DomainSID"): + domain_name = domain.get("name", "") + break + + finding_type = attack_data.get("Finding", "") + path_title = unique_finding_types_data.get(finding_type, "") + + return { + "CompositeRisk": str(round(float(attack_data.get("CompositeRisk")), 2)), + "FindingCount": attack_data.get("FindingCount"), + "ExposureCount": attack_data.get("ExposureCount"), + "ImpactCount": attack_data.get("ImpactCount"), + "ImpactedAssetCount": attack_data.get("ImpactedAssetCount"), + "DomainSID": attack_data.get("DomainSID"), + "Finding": attack_data.get("Finding"), + "id": attack_data.get("id"), + "created_at": attack_data.get("created_at"), + "updated_at": attack_data.get("updated_at"), + "deleted_at": attack_data.get("deleted_at"), + "tenant_url": tenant_domain, + "domain_name": domain_name, + "path_title": path_title, + "finding_type": finding_type, + "TimeGenerated": datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="milliseconds") + "Z", + } def process_environment(bloodhound_manager, env_config, tenant_domain, last_timestamps): @@ -161,17 +183,59 @@ def process_environment(bloodhound_manager, env_config, tenant_domain, last_time logging.info("No attack path timeline data to send to Azure Monitor.") return last_timestamps - # Submit data to Azure Monitor + # Submit data to Azure Monitor in batches token = bloodhound_manager.get_bearer_token() if not token: logging.error("Failed to obtain Bearer token for Azure Monitor.") return last_timestamps - for i, attack in enumerate(consolidated_timeline, 1): - logging.info(f"Processing attack path log entry {i}/{len(consolidated_timeline)}: {attack.get('id')}") - submit_attack_path_data(bloodhound_manager, attack, token, unique_finding_types_data, final_domains) - time.sleep(0.1) + batch_size = get_azure_batch_size() + logging.info(f"Sending {len(consolidated_timeline)} attack path timeline records to Azure Monitor in batches of {batch_size}.") + + successful_submissions = 0 + failed_submissions = 0 + + # Process in batches + for batch_start in range(0, len(consolidated_timeline), batch_size): + batch_end = min(batch_start + batch_size, len(consolidated_timeline)) + batch = consolidated_timeline[batch_start:batch_end] + + # Prepare log entries for this batch + log_entries = [] + for attack_data in batch: + try: + log_entry = _prepare_attack_path_timeline_log_entry( + attack_data, unique_finding_types_data, tenant_domain, final_domains + ) + log_entries.append(log_entry) + except Exception as e: + failed_submissions += 1 + logging.error(f"Failed to prepare attack path timeline log entry for ID {attack_data.get('id')}: {str(e)}") + + if not log_entries: + continue + + # Send batch to Azure Monitor + logging.info(f"Sending batch {batch_start//batch_size + 1} ({len(log_entries)} entries): IDs {[entry.get('id') for entry in log_entries[:5]]}...") + result = bloodhound_manager._send_to_azure_monitor( + log_entries, + token, + bloodhound_manager.dce_uri, + bloodhound_manager.dcr_immutable_id, + bloodhound_manager.table_name + ) + + if result and result.get("status") == "success": + entries_sent = result.get("entries_sent", len(log_entries)) + successful_submissions += entries_sent + logging.info(f"Successfully sent batch of {entries_sent} attack path timeline records") + else: + failed_submissions += len(log_entries) + logging.error(f"Failed to send batch: {result.get('message', 'Unknown error')}") + + # Rate limiting is handled automatically by azure_monitor_rate_limiter in _send_to_azure_monitor() + logging.info(f"Attack path timeline sending complete. Successful: {successful_submissions}, Failed: {failed_submissions}") return last_timestamps diff --git a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/audit_log_collector.py b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/audit_log_collector.py index e88fbc35e4b..d77924d03e0 100644 --- a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/audit_log_collector.py +++ b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/audit_log_collector.py @@ -1,7 +1,8 @@ from typing import Dict, List, Tuple, Optional, Any import logging import time -from ..utility.utils import load_environment_configs, EnvironmentConfig, AzureConfig +import json +from ..utility.utils import load_environment_configs, EnvironmentConfig, AzureConfig, get_azure_batch_size from ..utility.bloodhound_manager import BloodhoundManager def process_environment( @@ -126,7 +127,7 @@ def send_audit_logs_to_azure_monitor( current_tenant_domain: str ) -> Tuple[int, int]: """ - Sends audit logs to Azure Monitor. + Sends audit logs to Azure Monitor in batches of 100. Args: audit_logs: List of audit log entries to process @@ -141,22 +142,53 @@ def send_audit_logs_to_azure_monitor( Exception: If there's an error sending logs to Azure Monitor """ successful_submissions = failed_submissions = 0 - logging.info(f"Processing {len(audit_logs)} audit logs for '{current_tenant_domain}'") - - for log_entry in audit_logs: - log_id = log_entry.get('id', 'unknown') - logging.info(f"Processing log entry: ID {log_id}") + batch_size = get_azure_batch_size() + logging.info(f"Processing {len(audit_logs)} audit logs for '{current_tenant_domain}' in batches of {batch_size}") + + # Process in batches + for batch_start in range(0, len(audit_logs), batch_size): + batch_end = min(batch_start + batch_size, len(audit_logs)) + batch = audit_logs[batch_start:batch_end] + + # Transform batch entries to the expected schema for Azure Monitor + log_entries = [] + for data in batch: + log_entry = { + "action": data.get("action", ""), + "actor_email": data.get("actor_email", ""), + "actor_id": data.get("actor_id", ""), + "actor_name": data.get("actor_name", ""), + "commit_id": data.get("commit_id", ""), + "created_at": data.get("created_at", ""), + "fields": json.dumps(data.get("fields", {})), # fields should be a string in Log Analytics + "id": data.get("id", ""), + "request_id": data.get("request_id", ""), + "source_ip_address": data.get("source_ip_address", ""), + "status": data.get("status", ""), + "tenant_url": current_tenant_domain, + } + log_entries.append(log_entry) - result = bloodhound_manager.send_audit_logs_data(log_entry, azure_monitor_token) + logging.info(f"Sending batch {batch_start//batch_size + 1} ({len(log_entries)} entries): IDs {[log.get('id', 'unknown') for log in log_entries[:5]]}...") + + # Send batch to Azure Monitor + result = bloodhound_manager._send_to_azure_monitor( + log_entries, + azure_monitor_token, + bloodhound_manager.dce_uri, + bloodhound_manager.dcr_immutable_id, + bloodhound_manager.table_name + ) - if result.get("status") == "success": - successful_submissions += 1 + if result and result.get("status") == "success": + entries_sent = result.get("entries_sent", len(log_entries)) + successful_submissions += entries_sent + logging.info(f"Successfully sent batch of {entries_sent} audit logs") else: - failed_submissions += 1 - logging.error(f"Failed to send audit log ID {log_id}: {result.get('message', 'Unknown error')}") + failed_submissions += len(log_entries) + logging.error(f"Failed to send batch: {result.get('message', 'Unknown error') if result else 'No response'}") - # Rate limiting to prevent overwhelming the API - time.sleep(0.1) + # Rate limiting is handled automatically by azure_monitor_rate_limiter in _send_to_azure_monitor() logging.info( f"Audit log processing for '{current_tenant_domain}' complete. " diff --git a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/finding_trends_collector.py b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/finding_trends_collector.py index f30e519965f..ea420b56d7f 100644 --- a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/finding_trends_collector.py +++ b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/finding_trends_collector.py @@ -7,7 +7,8 @@ from ..utility.utils import ( EnvironmentConfig, AzureConfig, - load_environment_configs + load_environment_configs, + get_azure_batch_size ) from ..utility.bloodhound_manager import BloodhoundManager @@ -28,7 +29,7 @@ def send_finding_trends_to_azure_monitor( domains_data: List[Dict[str, Any]] ) -> Tuple[int, int]: """ - Sends finding trends data to Azure Monitor. + Sends finding trends data to Azure Monitor in batches of 100. Args: findings: List of finding trends to send (dictionaries) @@ -47,42 +48,72 @@ def send_finding_trends_to_azure_monitor( logging.info("No finding trends to send to Azure Monitor for this environment") return successful_submissions, failed_submissions - # Convert dictionaries to FindingTrend objects - finding_trends = [ - FindingTrend( - finding=item["finding"], - period=item["period"], - environment_id=item["environment_id"], - start_date=item["start_date"], - end_date=item["end_date"] - ) - for item in findings - ] - - logging.info(f"Sending {len(finding_trends)} collected finding trends to Azure Monitor.") - for idx, item in enumerate(finding_trends, 1): - logging.info(f"Processing finding trends log entry {idx}/{len(finding_trends)}: {item.finding.get('finding')} in environment ID {item.environment_id}") + batch_size = get_azure_batch_size() + logging.info(f"Sending {len(findings)} collected finding trends to Azure Monitor in batches of {batch_size}.") + + # Process in batches + for batch_start in range(0, len(findings), batch_size): + batch_end = min(batch_start + batch_size, len(findings)) + batch = findings[batch_start:batch_end] + + # Transform batch entries to the expected schema for Azure Monitor + log_entries = [] + for item in batch: + env_id = item.get("environment_id", "") + domain_name = ( + next( + ( + domain["name"] + for domain in domains_data + if domain.get("id") == env_id + ), + None, + ) + if domains_data + else None + ) + + finding_data = item.get("finding", {}) + log_entry = { + "composite_risk": str(round(float(finding_data.get("composite_risk", "")), 2)), + "display_title": str(finding_data.get("display_title", "")), + "display_type": str(finding_data.get("display_type", "")), + "environment_id": env_id, + "exposure_count": int(finding_data.get("exposure_count", 0)), + "finding": str(finding_data.get("finding", "")), + "finding_count_decrease": int(finding_data.get("finding_count_decrease", 0)), + "finding_count_end": int(finding_data.get("finding_count_end", 0)), + "finding_count_increase": int(finding_data.get("finding_count_increase", 0)), + "finding_count_start": int(finding_data.get("finding_count_start", 0)), + "impact_count": int(finding_data.get("impact_count", 0)), + "tenant_url": current_tenant_domain, + "domain_name": domain_name, + "start_date": item.get("start_date", ""), + "end_date": item.get("end_date", ""), + "period": item.get("period", ""), + } + log_entries.append(log_entry) - result = bloodhound_manager.send_finding_trends_logs( - item.finding, - azure_monitor_token, - current_tenant_domain, - domains_data, - environment_id=item.environment_id, - start_date=item.start_date, - end_date=item.end_date, - period=item.period + logging.info(f"Sending batch {batch_start//batch_size + 1} ({len(log_entries)} entries): Findings {[log.get('finding', 'unknown') for log in log_entries[:5]]}...") + + # Send batch to Azure Monitor + result = bloodhound_manager._send_to_azure_monitor( + log_entries, + azure_monitor_token, + bloodhound_manager.dce_uri, + bloodhound_manager.dcr_immutable_id, + bloodhound_manager.table_name ) - + if result and result.get("status") == "success": - successful_submissions += 1 - logging.info(f"Successfully sent finding trends for '{item.finding.get('finding')}'") + entries_sent = result.get("entries_sent", len(log_entries)) + successful_submissions += entries_sent + logging.info(f"Successfully sent batch of {entries_sent} finding trends") else: - failed_submissions += 1 - error_msg = result.get("message", "Unknown error") if result else "No response" - logging.error(f"Failed to send finding trends for '{item.finding.get('finding')}': {error_msg}") + failed_submissions += len(log_entries) + logging.error(f"Failed to send batch: {result.get('message', 'Unknown error') if result else 'No response'}") - time.sleep(0.1) # Rate limiting between requests + # Rate limiting is handled automatically by azure_monitor_rate_limiter in _send_to_azure_monitor() logging.info( f"Finding trends processing for '{current_tenant_domain}' complete. Successful: {successful_submissions}, Failed: {failed_submissions}." diff --git a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/posture_history_collector.py b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/posture_history_collector.py index 74f370a14e9..0acd72eea0b 100644 --- a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/posture_history_collector.py +++ b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/posture_history_collector.py @@ -1,7 +1,7 @@ import logging import time from typing import Dict, List, Any, Optional -from ..utility.utils import load_environment_configs, get_token_lists +from ..utility.utils import load_environment_configs, get_token_lists, get_azure_batch_size from ..utility.bloodhound_manager import BloodhoundManager @@ -159,7 +159,7 @@ def collect_posture_history(bloodhound_manager, env_id, data_types, current_tena def send_posture_history_to_azure_monitor(posture_history_data, bloodhound_manager, azure_monitor_token, current_tenant_domain, domains_data): """ - Sends posture history data to Azure Monitor and returns the count of successful and failed submissions. + Sends posture history data to Azure Monitor in batches of 100 and returns the count of successful and failed submissions. """ successful_submissions = 0 failed_submissions = 0 @@ -168,22 +168,63 @@ def send_posture_history_to_azure_monitor(posture_history_data, bloodhound_manag logging.info("No posture history data was collected to send to Azure Monitor for this environment.") return successful_submissions, failed_submissions - logging.info(f"Sending {len(posture_history_data)} posture history records to Azure Monitor.") + batch_size = get_azure_batch_size() + logging.info(f"Sending {len(posture_history_data)} posture history records to Azure Monitor in batches of {batch_size}.") - for i, data_item in enumerate(posture_history_data, 1): - result = bloodhound_manager.send_posture_history_logs( - data_item, azure_monitor_token, current_tenant_domain, domains_data + # Process in batches + for batch_start in range(0, len(posture_history_data), batch_size): + batch_end = min(batch_start + batch_size, len(posture_history_data)) + batch = posture_history_data[batch_start:batch_end] + + # Transform batch entries to the expected schema for Azure Monitor + log_entries = [] + for data_item in batch: + domain_id = data_item.get("domain_id", "") + domain_name = ( + next( + ( + domain["name"] + for domain in domains_data + if domain.get("id") == domain_id + ), + None, + ) + if domains_data + else None + ) + + log_entry = { + "metric_date": data_item.get("date", ""), + "value": str(data_item.get("value", "")), + "start_time": data_item.get("start_date", ""), + "end_time": data_item.get("end_date", ""), + "domain_id": domain_id, + "data_type": data_item.get("type", ""), + "domain_name": domain_name, + "tenant_url": current_tenant_domain, + } + log_entries.append(log_entry) + + logging.info(f"Sending batch {batch_start//batch_size + 1} ({len(log_entries)} entries): Types {[log.get('data_type', 'unknown') for log in log_entries[:5]]}...") + + # Send batch to Azure Monitor + result = bloodhound_manager._send_to_azure_monitor( + log_entries, + azure_monitor_token, + bloodhound_manager.dce_uri, + bloodhound_manager.dcr_immutable_id, + bloodhound_manager.table_name ) - logging.info(f"Processing posture history entry {i}/{len(posture_history_data)}: {data_item}") - logging.info(f"Result of sending posture history is {result}") - if result.get("status") == "success": - successful_submissions += 1 + if result and result.get("status") == "success": + entries_sent = result.get("entries_sent", len(log_entries)) + successful_submissions += entries_sent + logging.info(f"Successfully sent batch of {entries_sent} posture history records") else: - failed_submissions += 1 - logging.error(f"Failed to send posture history for date '{data_item.get('value')}': {result.get('message', 'Unknown error')}") + failed_submissions += len(log_entries) + logging.error(f"Failed to send batch: {result.get('message', 'Unknown error') if result else 'No response'}") - time.sleep(0.1) + # Rate limiting is handled automatically by azure_monitor_rate_limiter in _send_to_azure_monitor() logging.info(f"Posture history processing complete for '{current_tenant_domain}'. Successful submissions: {successful_submissions}, Failed submissions: {failed_submissions}.") return successful_submissions, failed_submissions \ No newline at end of file diff --git a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/tier_zero_assets_collector.py b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/tier_zero_assets_collector.py index 5104b6b5448..44827807ea0 100644 --- a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/tier_zero_assets_collector.py +++ b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/azure_functions/tier_zero_assets_collector.py @@ -1,9 +1,11 @@ import logging import time +import datetime +import json from azure.core.exceptions import ResourceNotFoundError from ..utility.utils import ( - load_environment_configs + load_environment_configs, get_azure_batch_size ) from ..utility.bloodhound_manager import BloodhoundManager @@ -16,7 +18,7 @@ def send_tier_zero_assets_to_azure_monitor( filtered_domains_by_env, ): """ - Sends tier zero assets data to Azure Monitor and returns the count of successful and failed submissions. + Sends tier zero assets data to Azure Monitor in batches of 100 and returns the count of successful and failed submissions. """ successful_submissions = 0 failed_submissions = 0 @@ -25,24 +27,77 @@ def send_tier_zero_assets_to_azure_monitor( logging.info("No Tier Zero Assets data to send to Azure Monitor for this environment.") return successful_submissions, failed_submissions - for idx, data in enumerate(nodes_array, 1): - logging.info( - f"Sending Tier Zero Asset data {idx}/{len(nodes_array)}: ID {data.get('nodeId')} ({data.get('name')})" - ) - res = bloodhound_manager.send_tier_zero_assets_data( - data, azure_monitor_token, filtered_domains_by_env + batch_size = get_azure_batch_size() + logging.info(f"Sending {len(nodes_array)} Tier Zero Assets to Azure Monitor in batches of {batch_size}.") + + # Process in batches + for batch_start in range(0, len(nodes_array), batch_size): + batch_end = min(batch_start + batch_size, len(nodes_array)) + batch = nodes_array[batch_start:batch_end] + + # Transform batch entries to the expected schema for Azure Monitor + log_entries = [] + for node_data in batch: + properties = node_data.get("properties", {}) + node_id = node_data.get("nodeId", "") + name = bloodhound_manager.extract_name(node_data, properties, node_id) + domain_name = bloodhound_manager.extract_domain_name( + node_data, properties, name, filtered_domains_by_env + ) + + # Initialize base log entry with common fields + log_entry = { + "nodeId": node_id, + "label": node_data.get("label", ""), + "kindType": node_data.get("kind", ""), + "objectId": node_data.get( + "objectId", properties.get("owner_objectid", "") + ), # Prioritize nodeId's objectId, then properties + "isTierZero": node_data.get("isTierZero", False), + "isOwnedObject": node_data.get("isOwnedObject", False), + "lastSeen": node_data.get("lastSeen", ""), + "tenant_url": current_tenant_domain, + "domain_name": domain_name.upper(), # Ensure domain name is uppercase + "name": name, + "TimeGenerated": datetime.datetime.now(datetime.timezone.utc).isoformat( + timespec="milliseconds" + ) + + "Z", # Standard Azure Monitor timestamp + } + + # Dynamically add all properties from the node + for prop_key, prop_value in properties.items(): + # Flatten some common nested properties or rename for clarity if needed + if prop_key == "date": # Example of specific mapping if desired + log_entry["Date"] = prop_value + elif prop_key == "title": + log_entry["Title"] = prop_value + else: + # Add all other properties as is + log_entry[prop_key] = prop_value + + log_entries.append(log_entry) + + logging.info(f"Sending batch {batch_start//batch_size + 1} ({len(log_entries)} entries): IDs {[log.get('nodeId', 'unknown') for log in log_entries[:5]]}...") + + # Send batch to Azure Monitor + result = bloodhound_manager._send_to_azure_monitor( + log_entries, + azure_monitor_token, + bloodhound_manager.dce_uri, + bloodhound_manager.dcr_immutable_id, + bloodhound_manager.table_name ) - - if res.get("status") == "success": - successful_submissions += 1 + + if result and result.get("status") == "success": + entries_sent = result.get("entries_sent", len(log_entries)) + successful_submissions += entries_sent + logging.info(f"Successfully sent batch of {entries_sent} Tier Zero Assets") else: - failed_submissions += 1 - logging.error( - f"Failed to send Tier Zero Asset data ID {data.get('nodeId')}: " - f"{res.get('message', 'Unknown error')}" - ) - - time.sleep(0.1) # small pause for rate limiting + failed_submissions += len(log_entries) + logging.error(f"Failed to send batch: {result.get('message', 'Unknown error') if result else 'No response'}") + + # Rate limiting is handled automatically by azure_monitor_rate_limiter in _send_to_azure_monitor() logging.info( f"Tier Zero Asset processing for '{current_tenant_domain}' complete. " diff --git a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/utility/bloodhound_manager.py b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/utility/bloodhound_manager.py index 097a94da59d..741a331ce74 100644 --- a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/utility/bloodhound_manager.py +++ b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/utility/bloodhound_manager.py @@ -6,42 +6,171 @@ from urllib.parse import urljoin, urlparse, urlencode, parse_qs import datetime import json +import time +from .utils import get_lookup_days, get_api_page_size, get_max_retries +from .rate_limiter import get_global_rate_limiter, get_azure_monitor_rate_limiter + class BloodhoundManager: """ Manages interactions with the BloodHound Enterprise API and Azure Monitor for audit logs, finding trends, posture history, posture statistics, and attack paths. """ - - DEFAULT_LOOKBACK_DAYS = 1 - def _send_to_azure_monitor(self, log_entry, bearer_token, dce_uri, dcr_immutable_id, table_name): + def _get_retry_after_delay(self, response): + """ + Extract Retry-After header from Azure Monitor response. + According to Azure Monitor API docs, Retry-After header is included when + rate limits (requests/minute or data/minute) are exceeded. + + Args: + response: HTTP response object + + Returns: + int or None: Retry-After value in seconds, or None if not present/invalid + """ + if response and hasattr(response, 'headers') and 'Retry-After' in response.headers: + try: + retry_after = int(response.headers['Retry-After']) + if retry_after > 0: + return retry_after + except (ValueError, TypeError): + pass + return None + + def _send_to_azure_monitor(self, log_entry, bearer_token, dce_uri, dcr_immutable_id, table_name, max_retries: int = None): """ - Helper to send a log entry to Azure Monitor via Data Collection Endpoint (DCE). - Handles POST request, error handling, and logging. + Helper to send log entry(ies) to Azure Monitor via Data Collection Endpoint (DCE). + Handles POST request, error handling, logging, and rate limiting. + Respects Retry-After header from Azure Monitor responses as per API documentation. + + Args: + log_entry: Single log entry dict OR list of log entry dicts for batching + bearer_token: Azure Monitor bearer token + dce_uri: Data Collection Endpoint URI + dcr_immutable_id: Data Collection Rule immutable ID + table_name: Table name for the log entries + max_retries: Maximum number of retries for rate limit errors (default: from env var MAX_RETRIES, max 10) """ + if max_retries is None: + max_retries = get_max_retries() + api_url = f"{dce_uri}/dataCollectionRules/{dcr_immutable_id}/streams/Custom-{table_name}?api-version=2023-01-01" headers = { "Authorization": f"Bearer {bearer_token}", "Content-Type": "application/json", } - response = requests.post( - api_url, headers=headers, data=json.dumps([log_entry]) - ) - - if response.status_code >= 400: - self._log_error( - f"[Send Error] Failed to send log entry to Azure Monitor: HTTP {response.status_code}. Response: {response.text}" - ) - return {"status": "error", "message": f"HTTP Error {response.status_code}"} + # Handle both single entry and batch (list of entries) + if isinstance(log_entry, list): + log_entries = log_entry + else: + log_entries = [log_entry] - response_content = ( - response.json() - if response.content - else {"status": "success", "message": "No content in response"} - ) - return {"status": "success", "response": response_content} + for attempt in range(max_retries + 1): + # Wait before making request (rate limiting using token bucket) + self.azure_monitor_rate_limiter.wait() + + # Set timeout to prevent hanging (30 seconds connect, 60 seconds read) + try: + response = requests.post( + api_url, headers=headers, data=json.dumps(log_entries), timeout=(30, 60) + ) + except (requests.exceptions.ConnectionError, requests.exceptions.RequestException) as e: + # Connection errors (DNS, max retries, etc.) - treat as data ingestion limit issue + # Increase delay with each retry: 30s, 60s, 90s, 120s (max 2 minutes) + delay = min(30 + (attempt * 30), 120) # Start at 30s, increase by 30s each attempt, max 120s + self.logger.warning( + f"Connection error (possibly due to Azure Monitor data ingestion limit). " + f"Waiting {delay} seconds before retry (attempt {attempt + 1}/{max_retries + 1})... " + f"Error: {str(e)}" + ) + if attempt < max_retries: + time.sleep(delay) # Increasing delay for connection/data limit issues + continue + else: + self._log_error( + f"[Send Error] Connection error after {max_retries} retries. " + f"Failed to send {len(log_entries)} log entry(ies) to Azure Monitor. " + f"Error: {str(e)}" + ) + return {"status": "error", "message": f"Connection error after {max_retries} retries"} + + # Handle rate limit (429) - check Retry-After header + if response.status_code == 429: + if attempt < max_retries: + # Check for Retry-After header first + retry_after = self._get_retry_after_delay(response) + if retry_after: + delay = retry_after + self.logger.warning( + f"Azure Monitor rate limited (HTTP 429). Retry-After header: {retry_after} seconds. " + f"Waiting {delay} seconds before retry (attempt {attempt + 1}/{max_retries + 1})..." + ) + else: + # Fall back to rate limiter's handle_rate_limit method + delay = self.azure_monitor_rate_limiter.handle_rate_limit(response) + self.logger.warning( + f"Azure Monitor rate limited (HTTP 429). Waiting {delay:.2f} seconds before retry " + f"(attempt {attempt + 1}/{max_retries + 1})..." + ) + time.sleep(delay) + continue + else: + self._log_error( + f"[Send Error] Rate limit error after {max_retries} retries. " + f"Failed to send {len(log_entries)} log entry(ies) to Azure Monitor." + ) + return {"status": "error", "message": f"Rate limit error after {max_retries} retries"} + + # Handle service unavailable errors (502, 503, 504) - check Retry-After header + # These can occur when data ingestion limits (2 GB/min or 12,000 requests/min) are exceeded + if response.status_code in [502, 503, 504]: + retry_after = self._get_retry_after_delay(response) + if attempt < max_retries: + if retry_after: + delay = retry_after + self.logger.warning( + f"Service unavailable error (HTTP {response.status_code}, possibly due to Azure Monitor data ingestion limit). " + f"Retry-After header: {retry_after} seconds. " + f"Waiting {delay} seconds before retry (attempt {attempt + 1}/{max_retries + 1})..." + ) + else: + # Default delay if no Retry-After header + delay = 30 + self.logger.warning( + f"Service unavailable error (HTTP {response.status_code}, possibly due to Azure Monitor data ingestion limit). " + f"No Retry-After header found. Waiting {delay} seconds before retry (attempt {attempt + 1}/{max_retries + 1})..." + ) + time.sleep(delay) + continue + else: + self._log_error( + f"[Send Error] Service unavailable error (HTTP {response.status_code}) after {max_retries} retries. " + f"Failed to send {len(log_entries)} log entry(ies) to Azure Monitor. " + f"Response: {response.text}" + ) + return {"status": "error", "message": f"Service unavailable error (HTTP {response.status_code}) after {max_retries} retries"} + + # Handle other HTTP errors + if response.status_code >= 400: + self._log_error( + f"[Send Error] Failed to send {len(log_entries)} log entry(ies) to Azure Monitor: " + f"HTTP {response.status_code}. Response: {response.text}" + ) + return {"status": "error", "message": f"HTTP Error {response.status_code}"} + + # Success - recover rate limiter if needed + self.azure_monitor_rate_limiter.handle_success() + + response_content = ( + response.json() + if response.content + else {"status": "success", "message": "No content in response"} + ) + return {"status": "success", "response": response_content, "entries_sent": len(log_entries)} + + return {"status": "error", "message": "Failed after all retries"} def __init__( self, tenant_domain: str, token_id: str, token_key: str, logger: logging.Logger @@ -73,6 +202,14 @@ def __init__( None # This will store the *current* table name for send operations ) + self.lookup_days = get_lookup_days() + + # Use global rate limiter for BloodHound API (shared across all instances) + self.bloodhound_rate_limiter = get_global_rate_limiter(logger=logger) + + # Use global rate limiter for Azure Monitor (separate instance with token bucket algorithm) + self.azure_monitor_rate_limiter = get_azure_monitor_rate_limiter(logger=logger) + def set_azure_monitor_config( self, tenant_id: str, @@ -120,54 +257,201 @@ def _get_headers(self, method: str, uri: str, payload: str = None) -> dict: return headers def _validate_response( - self, response: requests.Response, error_msg: str = "An error occurred" + self, response: requests.Response, error_msg: str = "An error occurred", + method: str = None, url: str = None ) -> bool: """ Validates the HTTP response and logs errors if any. + + Args: + response: HTTP response object + error_msg: Error message to log + method: HTTP method (optional, for better logging) + url: Full URL (optional, for better logging) """ if response.status_code >= 400: self._log_error( - f"{error_msg}: HTTP Error - Status Code: {response.status_code} - Response: {response.text}" + error_msg, + method=method, + url=url or response.url if hasattr(response, 'url') else None, + status_code=response.status_code, + response_text=response.text ) return False return True def _api_request( - self, uri, return_json: bool = True, method: str = "GET", payload=None + self, uri, return_json: bool = True, method: str = "GET", payload=None, max_retries: int = None ): """ - Centralized function to handle API requests and error handling. + Centralized function to handle API requests and error handling with rate limiting. Args: method (str): HTTP method (GET, POST, etc.) - endpoint_key (str): Key in ENDPOINTS dictionary return_json (bool): Whether to return JSON response or raw response - **kwargs: Includes parameters for URL path formatting, query parameters, and post body. + payload: Request payload for POST requests + max_retries: Maximum number of retries for rate limit errors (default: from env var MAX_RETRIES, max 10) Returns: Response data or None if request fails. """ + if max_retries is None: + max_retries = get_max_retries() + full_url: str = f"{self.tenant_domain}{uri}" + + for attempt in range(max_retries + 1): + try: + # Wait before making request (rate limiting) - uses global rate limiter + self.bloodhound_rate_limiter.wait() + except Exception: + self._log_error( + "Rate limiter error before API request", + method=method, + url=full_url, + exc_info=True + ) + return None + + headers = self._get_headers(method, uri, payload) + if headers is None: + self._log_error( + "Failed to generate headers for API request", + method=method, + url=full_url + ) + return None - headers = self._get_headers(method, uri, payload) - if headers is None: - self.logger.error("Failed to generate headers for API request.") - return None - - self.logger.info(f"Making {method} request to {full_url}") - response = requests.request(method, full_url, headers=headers, data=payload) - self.logger.info(f"Response status code: {response.status_code}") - - if not self._validate_response( - response, f"API request to {full_url} failed" - ): - return None - - # For text-based endpoints (like .md files), we return raw text, not JSON - if full_url.__contains__(".md") and return_json is False: - return response - - return response.json() if return_json else response + self.logger.info(f"Making {method} request to {full_url} (attempt {attempt + 1}/{max_retries + 1})") + + try: + # For POST requests with payload, ensure it's sent as bytes + # Keep payload as string for signature calculation, but encode when sending + # Set timeout to prevent hanging (30 seconds connect, 60 seconds read) + timeout = (30, 60) + if method == "POST" and payload: + if isinstance(payload, str): + response = requests.request(method, full_url, headers=headers, data=payload.encode('utf-8'), timeout=timeout) + else: + response = requests.request(method, full_url, headers=headers, data=payload, timeout=timeout) + else: + response = requests.request(method, full_url, headers=headers, data=payload, timeout=timeout) + except requests.exceptions.Timeout as e: + self._log_error( + f"Request timeout error", + method=method, + url=full_url, + exc_info=True + ) + if attempt < max_retries: + self.logger.info(f"Retrying after timeout (attempt {attempt + 1}/{max_retries + 1})...") + time.sleep(min(2.0 * (attempt + 1), 10.0)) + continue + return None + except requests.exceptions.ConnectionError as e: + self._log_error( + f"Connection error - unable to reach server", + method=method, + url=full_url, + exc_info=True + ) + if attempt < max_retries: + self.logger.info(f"Retrying after connection error (attempt {attempt + 1}/{max_retries + 1})...") + time.sleep(min(2.0 * (attempt + 1), 10.0)) + continue + return None + except requests.exceptions.RequestException as e: + self._log_error( + f"Request exception: {str(e)}", + method=method, + url=full_url, + exc_info=True + ) + if attempt < max_retries: + self.logger.info(f"Retrying after request exception (attempt {attempt + 1}/{max_retries + 1})...") + time.sleep(min(2.0 * (attempt + 1), 10.0)) + continue + return None + except Exception as e: + self._log_error( + f"Unexpected error during API request: {type(e).__name__}: {str(e)}", + method=method, + url=full_url, + exc_info=True + ) + return None + + self.logger.info(f"Response status code: {response.status_code}") + + # Handle rate limit (429) - should rarely happen with proactive rate limiting + if response.status_code == 429: + if attempt < max_retries: + # If we hit 429, wait longer and reduce rate temporarily + retry_after = None + if 'Retry-After' in response.headers: + try: + retry_after = int(response.headers['Retry-After']) + except ValueError: + pass + + delay = retry_after if retry_after else min(2.0 * (attempt + 1), 60.0) + self.logger.warning( + f"Rate limit (429) encountered despite rate limiter. " + f"Method: {method} | URL: {full_url} | " + f"Waiting {delay:.2f} seconds before retry (attempt {attempt + 1}/{max_retries + 1})..." + ) + time.sleep(delay) + continue + else: + self._log_error( + f"Rate limit error after {max_retries} retries", + method=method, + url=full_url, + status_code=429, + response_text=response.text + ) + return None + + # Handle other HTTP errors + if response.status_code >= 400: + self._log_error( + f"API request failed with HTTP error", + method=method, + url=full_url, + status_code=response.status_code, + response_text=response.text + ) + return None + + # Success - global rate limiter handles success automatically via token consumption + try: + # For text-based endpoints (like .md files), we return raw text, not JSON + if full_url.__contains__(".md") and return_json is False: + return response + + return response.json() if return_json else response + except ValueError as e: + # JSON decode error + self._log_error( + f"Failed to parse JSON response", + method=method, + url=full_url, + status_code=response.status_code, + response_text=response.text[:500] if response.text else "No response body", + exc_info=True + ) + return None + except Exception as e: + self._log_error( + f"Unexpected error processing response: {type(e).__name__}: {str(e)}", + method=method, + url=full_url, + status_code=response.status_code, + exc_info=True + ) + return None + + return None def test_connection(self) -> requests.Response | None: """ @@ -187,7 +471,9 @@ def get_available_domains(self) -> dict | None: Fetches available domains from the BloodHound Enterprise API. """ self.logger.info("Fetching available domains from BloodHound API...") - response = self._api_request("/api/v2/available-domains", return_json=True) + uri = "/api/v2/available-domains" + full_url = f"{self.tenant_domain}{uri}" + response = self._api_request(uri, return_json=True) if isinstance(response, dict): self.logger.info( @@ -195,7 +481,11 @@ def get_available_domains(self) -> dict | None: ) return response else: - self._log_error(f"Expected dict from API, got {type(response)}: {response}") + self._log_error( + f"Expected dict from API, got {type(response)}: {response}", + method="GET", + url=full_url + ) return {} def get_audit_logs(self, after = "") -> list[dict] : @@ -206,14 +496,14 @@ def get_audit_logs(self, after = "") -> list[dict] : """ audit_logs_list: list = [] skip: int = 0 - limit = 1000 + limit = get_api_page_size() while True: if after is not None and after != "": uri: str = f"/api/v2/audit?skip={skip}&limit={limit}&after={after}" else: two_days_ago_midnight = ( - (datetime.datetime.now() - datetime.timedelta(days=self.DEFAULT_LOOKBACK_DAYS)) + (datetime.datetime.now() - datetime.timedelta(days=self.lookup_days)) .replace(hour=0, minute=0, second=0, microsecond=0) .strftime('%Y-%m-%dT%H:%M:%SZ') ) @@ -286,7 +576,7 @@ def get_posture_history(self, data_type: str, environment_id=None, start = "") - uri: str = f"/api/v2/posture-history/{data_type}?environments={environment_id}&start={start}" else: two_days_ago_midnight = ( - (datetime.datetime.now() - datetime.timedelta(days=self.DEFAULT_LOOKBACK_DAYS)) + (datetime.datetime.now() - datetime.timedelta(days=self.lookup_days)) .replace(hour=0, minute=0, second=0, microsecond=0) .strftime('%Y-%m-%dT%H:%M:%SZ') ) @@ -316,6 +606,7 @@ def get_available_types_for_domain(self, domain_id: str) -> list: """ self.logger.info(f"Fetching available types for domain ID: {domain_id}") uri: str = f"/api/v2/domains/{domain_id}/available-types" + full_url = f"{self.tenant_domain}{uri}" # No change needed here, as domain_id is a path param, and no query params response = self._api_request(uri) if response: @@ -325,7 +616,9 @@ def get_available_types_for_domain(self, domain_id: str) -> list: return response.get("data", []) else: self._log_error( - f"Received empty response or request failed for available types for domain ID: {domain_id}." + f"Received empty response or request failed for available types for domain ID: {domain_id}", + method="GET", + url=full_url ) return [] @@ -345,15 +638,15 @@ def get_attack_path_details(self, domain_id: str, finding_type: str) -> list: ) all_attack_paths = [] skip = 0 - page_size = 10 # Default page size for this endpoint if not specified + page_size = get_api_page_size() while True: # Fetch a page of attack path details # domain_id is a path parameter - # 'finding' and 'skip' are query parameters + # 'finding', 'skip', and 'limit' are query parameters page_data = self._api_request( - f"/api/v2/domains/{domain_id}/details?finding={finding_type}&skip={skip}" + f"/api/v2/domains/{domain_id}/details?finding={finding_type}&skip={skip}&limit={page_size}" ) if not page_data or not page_data.get("data"): @@ -398,7 +691,7 @@ def get_attack_path_sparkline_timeline( uri: str = f"/api/v2/domains/{domain_id}/sparkline?finding={finding_type}&from={start_from}" else: two_days_ago_midnight = ( - (datetime.datetime.now() - datetime.timedelta(days=self.DEFAULT_LOOKBACK_DAYS)) + (datetime.datetime.now() - datetime.timedelta(days=self.lookup_days)) .replace(hour=0, minute=0, second=0, microsecond=0) .strftime('%Y-%m-%dT%H:%M:%SZ') ) @@ -429,13 +722,18 @@ def get_path_asset_text_details(self, uri) -> str: Returns: str: The text content, or an empty string if fetching fails. """ + full_url = f"{self.tenant_domain}{uri}" # No change needed here, finding_type is a path param, no query params response = self._api_request(uri, return_json=False) if response and response.status_code == 200: return response.text else: self._log_error( - f"Failed to fetch data. Status: {response.status_code if response else 'No response'}" + f"Failed to fetch data", + method="GET", + url=full_url, + status_code=response.status_code if response else None, + response_text=response.text if response else "No response" ) return "" @@ -489,11 +787,34 @@ def get_all_path_asset_details_for_finding_types(self, domains_data: list) -> di ) return all_asset_details - def _log_error(self, message: str): + def _log_error(self, message: str, method: str = None, url: str = None, + status_code: int = None, response_text: str = None, exc_info: bool = False): """ - Logs the provided error message using the logger. + Logs error messages with comprehensive context. + + Args: + message: Error message + method: HTTP method (GET, POST, etc.) + url: Full URL of the failed request + status_code: HTTP status code if available + response_text: Response body text if available + exc_info: Whether to include exception traceback """ - self.logger.error(message) + error_details = [message] + + if method: + error_details.append(f"Method: {method}") + if url: + error_details.append(f"URL: {url}") + if status_code: + error_details.append(f"Status Code: {status_code}") + if response_text: + # Truncate long responses to avoid log bloat + truncated_response = response_text[:500] + "..." if len(response_text) > 500 else response_text + error_details.append(f"Response: {truncated_response}") + + full_message = " | ".join(error_details) + self.logger.error(full_message, exc_info=exc_info) def get_bearer_token(self) -> str | None: """ @@ -514,7 +835,8 @@ def get_bearer_token(self) -> str | None: self.logger.info("Attempting to obtain Bearer token for Azure Monitor.") - response = requests.post(token_url, headers=headers, data=body) + # Set timeout to prevent hanging (30 seconds connect, 60 seconds read) + response = requests.post(token_url, headers=headers, data=body, timeout=(30, 60)) response.raise_for_status() access_token = response.json().get("access_token") if access_token: diff --git a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/utility/constant.py b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/utility/constant.py new file mode 100644 index 00000000000..7d887d8ed18 --- /dev/null +++ b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/utility/constant.py @@ -0,0 +1 @@ +DEFAULT_LOOKBACK_DAYS = 2 \ No newline at end of file diff --git a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/utility/rate_limiter.py b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/utility/rate_limiter.py new file mode 100644 index 00000000000..29bee0a06aa --- /dev/null +++ b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/utility/rate_limiter.py @@ -0,0 +1,453 @@ +""" +Global rate limiter for BloodHound API requests. +Uses token bucket algorithm to ensure we never exceed the API rate limit. +Thread-safe and shared across all BloodhoundManager instances. +""" +import threading +import time +import logging +import random +from typing import Optional +from .utils import get_max_requests_per_second + + +class GlobalRateLimiter: + """ + Thread-safe global rate limiter using token bucket algorithm. + Ensures all BloodHound API requests across all functions stay within rate limits. + + The rate limiter maintains a bucket of tokens that refill at a fixed rate. + Each API request consumes one token. If no tokens are available, the request waits. + """ + + _instance: Optional['GlobalRateLimiter'] = None + _lock = threading.Lock() + + def __init__(self, max_requests_per_second: float = 50.0, logger: Optional[logging.Logger] = None): + """ + Initialize the global rate limiter. + + Args: + max_requests_per_second: Maximum requests per second (default: 50, well under 65 limit) + logger: Logger instance (optional) + """ + self.original_max_requests_per_second = max_requests_per_second + self.max_requests_per_second = max_requests_per_second + self.tokens_per_second = max_requests_per_second + self.max_tokens = max_requests_per_second # Bucket capacity equals refill rate + self.current_tokens = self.max_tokens # Start with full bucket + self.last_refill_time = time.time() + self._lock = threading.Lock() + self.logger = logger or logging.getLogger(__name__) + + # Statistics + self.total_requests = 0 + self.total_wait_time = 0.0 + + # Dynamic rate limiting (for handling 429 errors) + self.consecutive_429s = 0 + self.successful_requests_since_429 = 0 + self.min_requests_per_second = max_requests_per_second * 0.1 # Don't go below 10% of original + + # Request rate tracking (for per-second logging) + self.request_timestamps = [] # Store timestamps of recent requests + self.last_rate_log_time = time.time() + self.rate_log_interval = 1.0 # Log rate every 1 second + + self.logger.info( + f"GlobalRateLimiter initialized: {max_requests_per_second} requests/second " + f"(max tokens: {self.max_tokens})" + ) + + @classmethod + def get_instance(cls, max_requests_per_second: Optional[float] = None, + logger: Optional[logging.Logger] = None) -> 'GlobalRateLimiter': + """ + Get or create the singleton instance of GlobalRateLimiter. + + Args: + max_requests_per_second: Maximum requests per second (only used on first creation) + logger: Logger instance (optional) + + Returns: + GlobalRateLimiter: The singleton instance + """ + if cls._instance is None: + with cls._lock: + # Double-check locking pattern + if cls._instance is None: + if max_requests_per_second is None: + # Get from environment variable or use default + max_requests_per_second = get_max_requests_per_second() + cls._instance = cls(max_requests_per_second, logger) + return cls._instance + + def _refill_tokens(self): + """Refill tokens based on elapsed time since last refill.""" + try: + now = time.time() + elapsed = now - self.last_refill_time + + if elapsed > 0: + # Add tokens based on refill rate + tokens_to_add = elapsed * self.tokens_per_second + self.current_tokens = min( + self.max_tokens, + self.current_tokens + tokens_to_add + ) + self.last_refill_time = now + except Exception as e: + self.logger.error( + f"Rate limiter error in _refill_tokens(): {type(e).__name__}: {str(e)}", + exc_info=True + ) + # Reset to safe state on error + self.last_refill_time = time.time() + raise + + def acquire(self, timeout: Optional[float] = None) -> bool: + """ + Acquire a token for making an API request. + Blocks until a token is available or timeout occurs. + + Args: + timeout: Maximum time to wait for a token (None = wait indefinitely) + + Returns: + bool: True if token acquired, False if timeout + """ + start_time = time.time() + + with self._lock: + while True: + # Refill tokens + self._refill_tokens() + + # Enforce minimum time between requests to prevent exceeding rate limit + min_time_between_requests = 1.0 / self.max_requests_per_second + now = time.time() + + # Check if we need to wait based on last request time + if self.request_timestamps: + time_since_last_request = now - self.request_timestamps[-1] + if time_since_last_request < min_time_between_requests: + # Need to wait to enforce rate limit + wait_needed = min_time_between_requests - time_since_last_request + self._lock.release() + try: + time.sleep(wait_needed) + except Exception as e: + self.logger.error( + f"Rate limiter error during sleep: {type(e).__name__}: {str(e)}", + exc_info=True + ) + raise + finally: + try: + self._lock.acquire() + except Exception as e: + self.logger.error( + f"Rate limiter error re-acquiring lock: {type(e).__name__}: {str(e)}", + exc_info=True + ) + raise + # Refill tokens again after waiting and update now + self._refill_tokens() + now = time.time() + + # Check if we have a token available + if self.current_tokens >= 1.0: + self.current_tokens -= 1.0 + self.total_requests += 1 + wait_time = now - start_time + self.total_wait_time += wait_time + + # Track request timestamp for rate calculation + self.request_timestamps.append(now) + + # Clean old timestamps (keep only last 5 seconds) + cutoff_time = now - 5.0 + self.request_timestamps = [ts for ts in self.request_timestamps if ts > cutoff_time] + + # Log requests per second periodically (WARNING level = yellow) + if now - self.last_rate_log_time >= self.rate_log_interval: + requests_in_last_second = len([ts for ts in self.request_timestamps if ts > now - 1.0]) + self.logger.info( + f"Rate Limiter Stats: {requests_in_last_second} request(s)/second " + f"(Limit: {self.max_requests_per_second}/sec) | " + f"Total requests: {self.total_requests} | " + f"Available tokens: {self.current_tokens:.2f}/{self.max_tokens:.2f}" + ) + self.last_rate_log_time = now + + if wait_time > 0.01: # Log if we had to wait more than 10ms + self.logger.debug( + f"Rate limiter: waited {wait_time:.3f}s for token. " + f"Remaining tokens: {self.current_tokens:.2f}" + ) + return True + + # No token available, calculate wait time + tokens_needed = 1.0 - self.current_tokens + wait_time = tokens_needed / self.tokens_per_second + + # Check timeout + if timeout is not None: + elapsed = time.time() - start_time + if elapsed + wait_time > timeout: + self.logger.warning( + f"Rate limiter timeout: could not acquire token within {timeout}s" + ) + return False + + # Release lock and wait + self._lock.release() + try: + time.sleep(wait_time) + except Exception as e: + self.logger.error( + f"Rate limiter error during sleep: {type(e).__name__}: {str(e)}", + exc_info=True + ) + raise + finally: + try: + self._lock.acquire() + except Exception as e: + self.logger.error( + f"Rate limiter error re-acquiring lock: {type(e).__name__}: {str(e)}", + exc_info=True + ) + raise + + def wait(self): + """ + Wait until a token is available, then consume it. + This is the main method to call before making an API request. + + Raises: + RuntimeError: If token acquisition fails unexpectedly + """ + try: + if not self.acquire(): + raise RuntimeError( + "Rate limiter failed to acquire token. This should not happen " + "with default timeout=None (infinite wait)." + ) + except Exception as e: + self.logger.error( + f"Rate limiter error in wait(): {type(e).__name__}: {str(e)}", + exc_info=True + ) + raise + + def get_stats(self, timeout: Optional[float] = 5.0) -> dict: + """ + Get statistics about rate limiter usage. + + Args: + timeout: Maximum time to wait for lock acquisition (default: 5 seconds) + + Returns: + dict: Statistics including total requests, average wait time, etc. + Returns empty dict with error message if lock cannot be acquired. + """ + # Try to acquire lock with timeout + if not self._lock.acquire(timeout=timeout): + self.logger.warning( + f"Could not acquire lock for get_stats() within {timeout}s. " + "Returning partial stats without lock." + ) + # Return stats that don't require lock (immutable values) + return { + "error": "Lock acquisition timeout", + "max_requests_per_second": self.max_requests_per_second, + "tokens_per_second": self.tokens_per_second, + } + + try: + avg_wait_time = ( + self.total_wait_time / self.total_requests + if self.total_requests > 0 + else 0.0 + ) + return { + "total_requests": self.total_requests, + "total_wait_time": self.total_wait_time, + "average_wait_time": avg_wait_time, + "current_tokens": self.current_tokens, + "max_requests_per_second": self.max_requests_per_second, + "tokens_per_second": self.tokens_per_second, + } + finally: + self._lock.release() + + def reset_stats(self, timeout: Optional[float] = 5.0): + """ + Reset statistics counters. + + Args: + timeout: Maximum time to wait for lock acquisition (default: 5 seconds) + + Returns: + bool: True if stats were reset, False if lock acquisition failed + """ + if not self._lock.acquire(timeout=timeout): + self.logger.warning( + f"Could not acquire lock for reset_stats() within {timeout}s." + ) + return False + + try: + self.total_requests = 0 + self.total_wait_time = 0.0 + return True + finally: + self._lock.release() + + def handle_rate_limit(self, response=None): + """ + Handle rate limit error (429) by dynamically reducing the rate limit. + Uses Retry-After header if available, otherwise uses exponential backoff. + + Args: + response: HTTP response object (may contain Retry-After header) + + Returns: + float: Delay in seconds to wait before retry + """ + with self._lock: + self.consecutive_429s += 1 + self.successful_requests_since_429 = 0 + + # Check for Retry-After header + retry_after = None + if response and hasattr(response, 'headers') and 'Retry-After' in response.headers: + try: + retry_after = int(response.headers['Retry-After']) + self.logger.info(f"Rate limit detected. Retry-After header: {retry_after} seconds") + except (ValueError, TypeError): + pass + + if retry_after: + # Reduce rate limit based on Retry-After + # If Retry-After is large, reduce rate more aggressively + reduction_factor = min(0.5, retry_after / 60.0) # Max 50% reduction + new_rate = max( + self.min_requests_per_second, + self.max_requests_per_second * (1.0 - reduction_factor) + ) + delay = retry_after + random.uniform(0, 2) # Add jitter + else: + # Exponential backoff: reduce rate by 50% for each consecutive 429 + reduction_factor = 0.5 ** min(self.consecutive_429s, 5) # Cap at 5 consecutive 429s + new_rate = max( + self.min_requests_per_second, + self.original_max_requests_per_second * reduction_factor + ) + # Calculate delay based on exponential backoff + base_delay = 1.0 * (2 ** min(self.consecutive_429s - 1, 10)) + delay = base_delay + random.uniform(0, base_delay * 0.2) + + # Update rate limit + old_rate = self.max_requests_per_second + self.max_requests_per_second = new_rate + self.tokens_per_second = new_rate + self.max_tokens = new_rate + + # Reduce current tokens to reflect the new rate limit + if self.current_tokens > new_rate: + self.current_tokens = new_rate + + self.logger.warning( + f"Rate limit error (429). Reducing rate limit from {old_rate:.2f} to {new_rate:.2f} req/s. " + f"Consecutive 429s: {self.consecutive_429s}. Waiting {delay:.2f} seconds before retry." + ) + + return delay + + def handle_success(self): + """ + Handle successful request by gradually recovering the rate limit. + """ + with self._lock: + if self.consecutive_429s > 0: + self.successful_requests_since_429 += 1 + + # After 10 successful requests, start recovering rate + if self.successful_requests_since_429 >= 10: + # Gradually increase rate limit back towards original + recovery_factor = 1.1 # Increase by 10% + new_rate = min( + self.original_max_requests_per_second, + self.max_requests_per_second * recovery_factor + ) + + old_rate = self.max_requests_per_second + self.max_requests_per_second = new_rate + self.tokens_per_second = new_rate + self.max_tokens = new_rate + + # Reduce consecutive 429s counter + self.consecutive_429s = max(0, self.consecutive_429s - 1) + self.successful_requests_since_429 = 0 + + if self.consecutive_429s == 0: + # Fully recovered + self.max_requests_per_second = self.original_max_requests_per_second + self.tokens_per_second = self.original_max_requests_per_second + self.max_tokens = self.original_max_requests_per_second + self.logger.info( + f"Rate limit fully recovered. Rate limit restored to {self.original_max_requests_per_second:.2f} req/s." + ) + else: + self.logger.info( + f"Rate limit recovering. Increased from {old_rate:.2f} to {new_rate:.2f} req/s. " + f"Consecutive 429s remaining: {self.consecutive_429s}" + ) + + +# Azure Monitor rate limiter instance (separate from BloodHound API) +_azure_monitor_rate_limiter: Optional[GlobalRateLimiter] = None +_azure_monitor_lock = threading.Lock() + +def get_azure_monitor_rate_limiter(max_requests_per_second: Optional[float] = None, + logger: Optional[logging.Logger] = None) -> GlobalRateLimiter: + """ + Get the Azure Monitor rate limiter instance. + Uses a separate instance from the BloodHound API rate limiter. + Rate limit is read from MAX_REQUESTS_PER_SECOND_LIMIT environment variable. + + Args: + max_requests_per_second: Maximum requests per second (default: from env var MAX_REQUESTS_PER_SECOND_LIMIT) + logger: Logger instance (optional) + + Returns: + GlobalRateLimiter: The Azure Monitor rate limiter instance + """ + global _azure_monitor_rate_limiter + + if _azure_monitor_rate_limiter is None: + with _azure_monitor_lock: + if _azure_monitor_rate_limiter is None: + if max_requests_per_second is None: + # Get from environment variable MAX_REQUESTS_PER_SECOND_LIMIT (capped at 50) + max_requests_per_second = get_max_requests_per_second() + _azure_monitor_rate_limiter = GlobalRateLimiter(max_requests_per_second, logger) + + return _azure_monitor_rate_limiter + + +# Convenience function to get the global rate limiter instance +def get_global_rate_limiter(logger: Optional[logging.Logger] = None) -> GlobalRateLimiter: + """ + Get the global rate limiter instance for BloodHound API. + + Args: + logger: Logger instance (optional) + + Returns: + GlobalRateLimiter: The singleton rate limiter instance + """ + return GlobalRateLimiter.get_instance(logger=logger) + diff --git a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/utility/utils.py b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/utility/utils.py index b4ade956a8b..73b33ad5e95 100644 --- a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/utility/utils.py +++ b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/SharedCode/utility/utils.py @@ -5,6 +5,7 @@ from azure.identity import DefaultAzureCredential from azure.keyvault.secrets import SecretClient from azure.core.exceptions import ResourceNotFoundError +from .constant import DEFAULT_LOOKBACK_DAYS @dataclass class EnvironmentConfig: @@ -99,8 +100,8 @@ def load_environment_configs(table_name: str) -> Tuple[List[EnvironmentConfig], "DCR_IMMUTABLE_ID", table_name, "KEY_VAULT_URL", - # "BLOODHOUND_TOKEN_ID", - # "BLOODHOUND_TOKEN_KEY", + "BLOODHOUND_TOKEN_ID", + "BLOODHOUND_TOKEN_KEY", "SELECTED_BLOODHOUND_ENVIRONMENTS", "SELECTED_FINDING_TYPES" ]) @@ -108,22 +109,22 @@ def load_environment_configs(table_name: str) -> Tuple[List[EnvironmentConfig], # Parse environment configs tenant_domains = [td.strip() for td in env_vars["BLOODHOUND_TENANT_DOMAIN"].split(',')] - # if env_vars["BLOODHOUND_TOKEN_ID"] and env_vars["BLOODHOUND_TOKEN_KEY"]: - # token_ids = [tid.strip() for tid in env_vars["BLOODHOUND_TOKEN_ID"].split(',')] - # token_keys = [tkey.strip() for tkey in env_vars["BLOODHOUND_TOKEN_KEY"].split(',')] - # else: - # token_ids, token_keys = get_token_lists( - # key_vault_url=env_vars["KEY_VAULT_URL"], - # token_ids_secret_name=env_vars["BLOODHOUND_TOKEN_ID_SECRET_NAME"], - # token_keys_secret_name=env_vars["BLOODHOUND_TOKEN_KEY_SECRET_NAME"] - # ) - - token_ids, token_keys = get_token_lists( + if env_vars["BLOODHOUND_TOKEN_ID"] and env_vars["BLOODHOUND_TOKEN_KEY"]: + token_ids = [tid.strip() for tid in env_vars["BLOODHOUND_TOKEN_ID"].split(',')] + token_keys = [tkey.strip() for tkey in env_vars["BLOODHOUND_TOKEN_KEY"].split(',')] + else: + token_ids, token_keys = get_token_lists( key_vault_url=env_vars["KEY_VAULT_URL"], token_ids_secret_name=env_vars["BLOODHOUND_TOKEN_ID_SECRET_NAME"], token_keys_secret_name=env_vars["BLOODHOUND_TOKEN_KEY_SECRET_NAME"] ) + # token_ids, token_keys = get_token_lists( + # key_vault_url=env_vars["KEY_VAULT_URL"], + # token_ids_secret_name=env_vars["BLOODHOUND_TOKEN_ID_SECRET_NAME"], + # token_keys_secret_name=env_vars["BLOODHOUND_TOKEN_KEY_SECRET_NAME"] + # ) + if not (len(tenant_domains) == len(token_ids) == len(token_keys)): raise ValueError("Environment variable lists for domains, token IDs, and token keys have a mismatch in length") @@ -153,3 +154,52 @@ def load_environment_configs(table_name: str) -> Tuple[List[EnvironmentConfig], ) return env_configs, azure_config + +def get_lookup_days(): + """ + Fetch the LOOKUP_DAYS value from the environment variable. + If not set, fallback to the DEFAULT_LOOKBACK_DAYS from the constant file. + """ + lookup_days = int(os.getenv("LOOKUP_DAYS", DEFAULT_LOOKBACK_DAYS)) + return lookup_days + +def get_api_page_size(): + """ + Fetch the API_PAGE_SIZE value from the environment variable. + Used for pagination when fetching data from BloodHound API (audit logs, attack paths). + Default: 1000 + Maximum: 1000 (values > 1000 will be capped at 1000) + """ + page_size = int(os.getenv("API_PAGE_SIZE", "1000")) + return min(page_size, 1000) + +def get_azure_batch_size(): + """ + Fetch the AZURE_BATCH_SIZE value from the environment variable. + Used for batching when sending data to Azure Monitor. + Default: 100 + Maximum: 100 (values > 100 will be capped at 100) + """ + batch_size = int(os.getenv("AZURE_BATCH_SIZE", "100")) + return min(batch_size, 100) + +def get_max_retries(): + """ + Fetch the MAX_RETRIES value from the environment variable. + Used for retrying failed API requests (rate limit errors). + Default: 9 + Maximum: 9 (values > 9 will be capped at 9) + """ + max_retries = int(os.getenv("MAX_RETRIES", "9")) + return min(max_retries, 9) + +def get_max_requests_per_second(): + """ + Fetch the MAX_REQUESTS_PER_SECOND_LIMIT value from the environment variable. + Used for global rate limiting of BloodHound API requests. + Default: 50.0 (well under the 65 requests/second limit) + Recommended: Keep between 40-55 to provide safety margin + """ + max_rps = float(os.getenv("MAX_REQUESTS_PER_SECOND_LIMIT", "50.0")) + # Cap at 50 to ensure we never exceed the 65 limit + return min(max_rps, 50.0) \ No newline at end of file diff --git a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/attack_path_collector/__init__.py b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/attack_path_collector/__init__.py index ad28d684ccc..5059902bdb9 100644 --- a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/attack_path_collector/__init__.py +++ b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/attack_path_collector/__init__.py @@ -2,36 +2,92 @@ import os import json import azure.functions as func -from azure.core.exceptions import ResourceNotFoundError +from azure.core.exceptions import ResourceNotFoundError, AzureError +from azure.storage.blob import BlobServiceClient from ..SharedCode.azure_functions.attack_path_collector import run_attack_paths_collection_process -# Path to state.json inside the same directory as __init__.py -STATE_FILE = os.path.join(os.path.dirname(__file__), "state.json") +# Azure Blob Storage configuration +STORAGE_CONNECTION_STRING = os.environ.get("AzureWebJobsStorage") +CONTAINER_NAME = "attack-path-function-state" +BLOB_NAME = "attack_path_collector_state.json" + +def get_connection_string(): + """ + Get the storage connection string from environment variable. + For local development, provide a full connection string in local.settings.json. + For production, AzureWebJobsStorage will contain the connection string. + """ + connection_string = STORAGE_CONNECTION_STRING + + if not connection_string: + raise ValueError("AzureWebJobsStorage connection string is not configured") + + return connection_string def read_state(): """ - Read the state from state.json. Return {} if file does not exist or is empty. + Read the state from Azure Blob Storage. Return {} if blob does not exist or is empty. Note: This function does not handle exceptions. The caller is responsible for - catching and logging any errors that occur during file I/O or JSON decoding. + catching and logging any errors that occur during blob I/O or JSON decoding. """ - if not os.path.exists(STATE_FILE): - with open(STATE_FILE, "w") as f: - json.dump({}, f) + if not STORAGE_CONNECTION_STRING: + logging.error("AzureWebJobsStorage connection string is not configured") return {} - - with open(STATE_FILE, "r") as f: - content = f.read().strip() + + try: + blob_service_client = BlobServiceClient.from_connection_string(get_connection_string()) + blob_client = blob_service_client.get_blob_client(container=CONTAINER_NAME, blob=BLOB_NAME) + + # Check if blob exists + if not blob_client.exists(): + logging.info("State blob does not exist. Initializing with empty state.") + # Create container if it doesn't exist + container_client = blob_service_client.get_container_client(CONTAINER_NAME) + if not container_client.exists(): + container_client.create_container() + # Write empty state + blob_client.upload_blob(json.dumps({}), overwrite=True) + return {} + + # Download and parse blob content + blob_data = blob_client.download_blob() + content = blob_data.readall().decode('utf-8').strip() + if not content: - logging.warning("state.json is empty. Initializing with {}.") + logging.warning("State blob is empty. Initializing with {}.") return {} + return json.loads(content) + except AzureError as e: + logging.error(f"Azure Blob Storage error while reading state: {e}") + return {} + except json.JSONDecodeError as e: + logging.error(f"JSON decode error while reading state: {e}") + return {} def write_state(state): - """Write updated state to state.json""" - with open(STATE_FILE, "w") as f: - json.dump(state, f) + """Write updated state to Azure Blob Storage""" + if not STORAGE_CONNECTION_STRING: + logging.error("AzureWebJobsStorage connection string is not configured") + return + + try: + blob_service_client = BlobServiceClient.from_connection_string(get_connection_string()) + + # Ensure container exists + container_client = blob_service_client.get_container_client(CONTAINER_NAME) + if not container_client.exists(): + container_client.create_container() + + # Upload state to blob + blob_client = blob_service_client.get_blob_client(container=CONTAINER_NAME, blob=BLOB_NAME) + blob_client.upload_blob(json.dumps(state, indent=2), overwrite=True) + logging.info(f"State successfully written to blob: {CONTAINER_NAME}/{BLOB_NAME}") + except AzureError as e: + logging.error(f"Azure Blob Storage error while writing state: {e}") + raise def main(myTimer: func.TimerRequest) -> None: """Main function for the attack path collector Azure Function.""" @@ -45,8 +101,9 @@ def main(myTimer: func.TimerRequest) -> None: # Read previous state state = read_state() + logging.info(f"State: {state}") last_attack_path_timestamp = state.get("last_attack_path_timestamp", {}) - logging.info(f"Last attack path timestamp from state.json: {last_attack_path_timestamp}") + logging.info(f"Last attack path timestamp from Azure Blob Storage: {last_attack_path_timestamp}") # Call main function with last value new_attack_path_timestamp = run_attack_paths_collection_process(last_attack_path_timestamp) @@ -58,17 +115,21 @@ def main(myTimer: func.TimerRequest) -> None: logging.info(f"New attack path timestamp: {new_attack_path_timestamp}") - # Update state.json + # Update state in Azure Blob Storage state["last_attack_path_timestamp"] = new_attack_path_timestamp write_state(state) - logging.info(f"State updated in state.json: {new_attack_path_timestamp}") + logging.info(f"State updated in Azure Blob Storage: {new_attack_path_timestamp}") except KeyError as e: logging.error(f"Missing one or more required environment variables: {e}") + except json.JSONDecodeError as e: + logging.error(f"Error reading or writing state blob: {e}") except ValueError as e: logging.error(f"Configuration error: {e}") - except json.JSONDecodeError as e: - logging.error(f"Error reading or writing state file: {e}") + except ResourceNotFoundError as e: + logging.error(f"Azure Key Vault secret not found: {e}") + except AzureError as e: + logging.error(f"Azure Blob Storage error: {e}") except Exception as e: logging.error(f"Unexpected error: {str(e)}", exc_info=True) finally: diff --git a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/attack_path_collector/state.json b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/attack_path_collector/state.json index 83b23705563..9e26dfeeb6e 100644 --- a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/attack_path_collector/state.json +++ b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/attack_path_collector/state.json @@ -1 +1 @@ -{"last_attack_path_timestamp": {"https://maplesyrup.bloodhoundenterprise.io/": {"GHOST.CORP": "2025-09-08T09:01:58.963983Z", "WRAITH.CORP": "2025-09-08T09:01:59.624618Z"}}} \ No newline at end of file +{} \ No newline at end of file diff --git a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/attack_path_timeline_collector/__init__.py b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/attack_path_timeline_collector/__init__.py index 2cf45cb29e4..97e7a1a7cde 100644 --- a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/attack_path_timeline_collector/__init__.py +++ b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/attack_path_timeline_collector/__init__.py @@ -2,35 +2,92 @@ import os import json import azure.functions as func +from azure.storage.blob import BlobServiceClient +from azure.core.exceptions import AzureError from ..SharedCode.azure_functions.attack_path_timeline_collector import run_attack_paths_timeline_collection_process -# Path to state.json inside the same directory as __init__.py -STATE_FILE = os.path.join(os.path.dirname(__file__), "state.json") +# Azure Blob Storage configuration +STORAGE_CONNECTION_STRING = os.environ.get("AzureWebJobsStorage") +CONTAINER_NAME = "attack-path-timeline-function-state" +BLOB_NAME = "attack_path_timeline_collector_state.json" + +def get_connection_string(): + """ + Get the storage connection string from environment variable. + For local development, provide a full connection string in local.settings.json. + For production, AzureWebJobsStorage will contain the connection string. + """ + connection_string = STORAGE_CONNECTION_STRING + + if not connection_string: + raise ValueError("AzureWebJobsStorage connection string is not configured") + + return connection_string def read_state(): """ - Read the state from state.json. Return {} if file does not exist or is empty. + Read the state from Azure Blob Storage. Return {} if blob does not exist or is empty. Note: This function does not handle exceptions. The caller is responsible for - catching and logging any errors that occur during file I/O or JSON decoding. + catching and logging any errors that occur during blob I/O or JSON decoding. """ - if not os.path.exists(STATE_FILE): - with open(STATE_FILE, "w") as f: - json.dump({}, f) + if not STORAGE_CONNECTION_STRING: + logging.error("AzureWebJobsStorage connection string is not configured") return {} - - with open(STATE_FILE, "r") as f: - content = f.read().strip() + + try: + blob_service_client = BlobServiceClient.from_connection_string(get_connection_string()) + blob_client = blob_service_client.get_blob_client(container=CONTAINER_NAME, blob=BLOB_NAME) + + # Check if blob exists + if not blob_client.exists(): + logging.info("State blob does not exist. Initializing with empty state.") + # Create container if it doesn't exist + container_client = blob_service_client.get_container_client(CONTAINER_NAME) + if not container_client.exists(): + container_client.create_container() + # Write empty state + blob_client.upload_blob(json.dumps({}), overwrite=True) + return {} + + # Download and parse blob content + blob_data = blob_client.download_blob() + content = blob_data.readall().decode('utf-8').strip() + if not content: - logging.warning("state.json is empty. Initializing with {}.") + logging.warning("State blob is empty. Initializing with {}.") return {} + return json.loads(content) + except AzureError as e: + logging.error(f"Azure Blob Storage error while reading state: {e}") + return {} + except json.JSONDecodeError as e: + logging.error(f"JSON decode error while reading state: {e}") + return {} def write_state(state): - """Write updated state to state.json""" - with open(STATE_FILE, "w") as f: - json.dump(state, f) + """Write updated state to Azure Blob Storage""" + if not STORAGE_CONNECTION_STRING: + logging.error("AzureWebJobsStorage connection string is not configured") + return + + try: + blob_service_client = BlobServiceClient.from_connection_string(get_connection_string()) + + # Ensure container exists + container_client = blob_service_client.get_container_client(CONTAINER_NAME) + if not container_client.exists(): + container_client.create_container() + + # Upload state to blob + blob_client = blob_service_client.get_blob_client(container=CONTAINER_NAME, blob=BLOB_NAME) + blob_client.upload_blob(json.dumps(state, indent=2), overwrite=True) + logging.info(f"State successfully written to blob: {CONTAINER_NAME}/{BLOB_NAME}") + except AzureError as e: + logging.error(f"Azure Blob Storage error while writing state: {e}") + raise def main(myTimer: func.TimerRequest) -> None: try: @@ -41,18 +98,19 @@ def main(myTimer: func.TimerRequest) -> None: # Read previous state state = read_state() + logging.info(f"State: {state}") last_attack_path_timeline_timestamp = state.get("last_attack_path_timeline_timestamp", {}) - logging.info(f"Last attack path timeline timestamp from state.json: {last_attack_path_timeline_timestamp}") + logging.info(f"Last attack path timeline timestamp from Azure Blob Storage: {last_attack_path_timeline_timestamp}") # Call main function with last value and capture new timestamp new_attack_path_timeline_timestamp = run_attack_paths_timeline_collection_process(last_attack_path_timeline_timestamp) if new_attack_path_timeline_timestamp: - # Update state.json only if we got valid results + # Update state in Azure Blob Storage only if we got valid results state["last_attack_path_timeline_timestamp"] = new_attack_path_timeline_timestamp write_state(state) - logging.info(f"State updated in state.json: {new_attack_path_timeline_timestamp}") + logging.info(f"State updated in Azure Blob Storage: {new_attack_path_timeline_timestamp}") else: logging.warning("No new timestamps were collected. State remains unchanged.") @@ -61,6 +119,12 @@ def main(myTimer: func.TimerRequest) -> None: except KeyError as e: logging.error(f"Missing required environment variable: {e}") raise + except json.JSONDecodeError as e: + logging.error(f"Error reading or writing state blob: {e}") + raise + except AzureError as e: + logging.error(f"Azure Blob Storage error: {e}") + raise except Exception as e: logging.error(f"Unexpected error in attack_path_timeline_collector: {str(e)}") raise diff --git a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/audit_log_collector/__init__.py b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/audit_log_collector/__init__.py index cf789a8f437..509d6402a8d 100644 --- a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/audit_log_collector/__init__.py +++ b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/audit_log_collector/__init__.py @@ -2,35 +2,91 @@ import os import json import azure.functions as func +from azure.storage.blob import BlobServiceClient +from azure.core.exceptions import AzureError from ..SharedCode.azure_functions.audit_log_collector import bloodhound_audit_logs_collector_main_function -# Path to state.json inside the same directory as __init__.py -STATE_FILE = os.path.join(os.path.dirname(__file__), "state.json") +# Azure Blob Storage configuration +STORAGE_CONNECTION_STRING = os.environ.get("AzureWebJobsStorage") +CONTAINER_NAME = "audit-log-function-state" +BLOB_NAME = "audit_log_collector_state.json" + +def get_connection_string(): + """ + Get the storage connection string from environment variable. + For local development, provide a full connection string in local.settings.json. + For production, AzureWebJobsStorage will contain the connection string. + """ + connection_string = STORAGE_CONNECTION_STRING + + if not connection_string: + raise ValueError("AzureWebJobsStorage connection string is not configured") + + return connection_string def read_state(): """ - Read the state from state.json. Return {} if file does not exist or is empty. + Read the state from Azure Blob Storage. Return {} if blob does not exist or is empty. Note: This function does not handle exceptions. The caller is responsible for - catching and logging any errors that occur during file I/O or JSON decoding. + catching and logging any errors that occur during blob I/O or JSON decoding. """ - if not os.path.exists(STATE_FILE): - with open(STATE_FILE, "w") as f: - json.dump({}, f) + if not STORAGE_CONNECTION_STRING: + logging.error("AzureWebJobsStorage connection string is not configured") return {} - - with open(STATE_FILE, "r") as f: - content = f.read().strip() + + try: + blob_service_client = BlobServiceClient.from_connection_string(get_connection_string()) + blob_client = blob_service_client.get_blob_client(container=CONTAINER_NAME, blob=BLOB_NAME) + + # Check if blob exists + if not blob_client.exists(): + logging.info("State blob does not exist. Initializing with empty state.") + # Create container if it doesn't exist + container_client = blob_service_client.get_container_client(CONTAINER_NAME) + if not container_client.exists(): + container_client.create_container() + # Write empty state + blob_client.upload_blob(json.dumps({}), overwrite=True) + return {} + + # Download and parse blob content + blob_data = blob_client.download_blob() + content = blob_data.readall().decode('utf-8').strip() + if not content: - logging.warning("state.json is empty. Initializing with {}.") + logging.warning("State blob is empty. Initializing with {}.") return {} + return json.loads(content) - + except AzureError as e: + logging.error(f"Azure Blob Storage error while reading state: {e}") + return {} + except json.JSONDecodeError as e: + logging.error(f"JSON decode error while reading state: {e}") + return {} def write_state(state): - """Write updated state to state.json""" - with open(STATE_FILE, "w") as f: - json.dump(state, f) + """Write updated state to Azure Blob Storage""" + if not STORAGE_CONNECTION_STRING: + logging.error("AzureWebJobsStorage connection string is not configured") + return + + try: + blob_service_client = BlobServiceClient.from_connection_string(get_connection_string()) + + # Ensure container exists + container_client = blob_service_client.get_container_client(CONTAINER_NAME) + if not container_client.exists(): + container_client.create_container() + + # Upload state to blob + blob_client = blob_service_client.get_blob_client(container=CONTAINER_NAME, blob=BLOB_NAME) + blob_client.upload_blob(json.dumps(state, indent=2), overwrite=True) + logging.info(f"State successfully written to blob: {CONTAINER_NAME}/{BLOB_NAME}") + except AzureError as e: + logging.error(f"Azure Blob Storage error while writing state: {e}") + raise def main(myTimer: func.TimerRequest): """Main function for the audit log collector Azure Function.""" @@ -42,8 +98,9 @@ def main(myTimer: func.TimerRequest): # Read previous state state = read_state() + logging.info(f"State: {state}") last_audit_logs_timestamp = state.get("last_audit_logs_timestamp", {}) - logging.info(f"Last value from state.json: {last_audit_logs_timestamp}") + logging.info(f"Last value from Azure Blob Storage: {last_audit_logs_timestamp}") # Call main function with last value new_audit_logs_timestamp = bloodhound_audit_logs_collector_main_function(last_audit_logs_timestamp) @@ -51,17 +108,19 @@ def main(myTimer: func.TimerRequest): logging.error("Failed to collect audit logs. Check the logs for details.") return - # Update state.json + # Update state in Azure Blob Storage state["last_audit_logs_timestamp"] = new_audit_logs_timestamp write_state(state) - logging.info(f"State updated in state.json: {new_audit_logs_timestamp}") + logging.info(f"State updated in Azure Blob Storage: {new_audit_logs_timestamp}") except KeyError as e: logging.error(f"Missing one or more required environment variables: {e}") + except json.JSONDecodeError as e: + logging.error(f"Error reading or writing state blob: {e}") except ValueError as e: logging.error(f"Configuration error: {e}") - except json.JSONDecodeError as e: - logging.error(f"Error reading or writing state file: {e}") + except AzureError as e: + logging.error(f"Azure Blob Storage error: {e}") except Exception as e: logging.error(f"Unexpected error: {str(e)}", exc_info=True) finally: diff --git a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/host.json b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/host.json index 9df913614d9..e269213f29d 100644 --- a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/host.json +++ b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/host.json @@ -1,5 +1,6 @@ { "version": "2.0", + "functionTimeout": "02:00:00", "logging": { "applicationInsights": { "samplingSettings": { diff --git a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/posture_history_collector/__init__.py b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/posture_history_collector/__init__.py index 40397f61274..ed07f80ba85 100644 --- a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/posture_history_collector/__init__.py +++ b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/posture_history_collector/__init__.py @@ -3,35 +3,92 @@ import json import azure.functions as func from azure.core.exceptions import ResourceNotFoundError +from azure.storage.blob import BlobServiceClient +from azure.core.exceptions import AzureError from ..SharedCode.azure_functions.posture_history_collector import run_posture_history_collection_process -# Path to state.json inside the same directory as __init__.py -STATE_FILE = os.path.join(os.path.dirname(__file__), "state.json") +# Azure Blob Storage configuration +STORAGE_CONNECTION_STRING = os.environ.get("AzureWebJobsStorage") +CONTAINER_NAME = "posture-history-function-state" +BLOB_NAME = "posture_history_collector_state.json" + +def get_connection_string(): + """ + Get the storage connection string from environment variable. + For local development, provide a full connection string in local.settings.json. + For production, AzureWebJobsStorage will contain the connection string. + """ + connection_string = STORAGE_CONNECTION_STRING + + if not connection_string: + raise ValueError("AzureWebJobsStorage connection string is not configured") + + return connection_string def read_state(): """ - Read the state from state.json. Return {} if file does not exist or is empty. + Read the state from Azure Blob Storage. Return {} if blob does not exist or is empty. Note: This function does not handle exceptions. The caller is responsible for - catching and logging any errors that occur during file I/O or JSON decoding. + catching and logging any errors that occur during blob I/O or JSON decoding. """ - if not os.path.exists(STATE_FILE): - with open(STATE_FILE, "w") as f: - json.dump({}, f) + if not STORAGE_CONNECTION_STRING: + logging.error("AzureWebJobsStorage connection string is not configured") return {} - - with open(STATE_FILE, "r") as f: - content = f.read().strip() + + try: + blob_service_client = BlobServiceClient.from_connection_string(get_connection_string()) + blob_client = blob_service_client.get_blob_client(container=CONTAINER_NAME, blob=BLOB_NAME) + + # Check if blob exists + if not blob_client.exists(): + logging.info("State blob does not exist. Initializing with empty state.") + # Create container if it doesn't exist + container_client = blob_service_client.get_container_client(CONTAINER_NAME) + if not container_client.exists(): + container_client.create_container() + # Write empty state + blob_client.upload_blob(json.dumps({}), overwrite=True) + return {} + + # Download and parse blob content + blob_data = blob_client.download_blob() + content = blob_data.readall().decode('utf-8').strip() + if not content: - logging.warning("state.json is empty. Initializing with {}.") + logging.warning("State blob is empty. Initializing with {}.") return {} + return json.loads(content) + except AzureError as e: + logging.error(f"Azure Blob Storage error while reading state: {e}") + return {} + except json.JSONDecodeError as e: + logging.error(f"JSON decode error while reading state: {e}") + return {} def write_state(state): - """Write updated state to state.json""" - with open(STATE_FILE, "w") as f: - json.dump(state, f) + """Write updated state to Azure Blob Storage""" + if not STORAGE_CONNECTION_STRING: + logging.error("AzureWebJobsStorage connection string is not configured") + return + + try: + blob_service_client = BlobServiceClient.from_connection_string(get_connection_string()) + + # Ensure container exists + container_client = blob_service_client.get_container_client(CONTAINER_NAME) + if not container_client.exists(): + container_client.create_container() + + # Upload state to blob + blob_client = blob_service_client.get_blob_client(container=CONTAINER_NAME, blob=BLOB_NAME) + blob_client.upload_blob(json.dumps(state, indent=2), overwrite=True) + logging.info(f"State successfully written to blob: {CONTAINER_NAME}/{BLOB_NAME}") + except AzureError as e: + logging.error(f"Azure Blob Storage error while writing state: {e}") + raise def main(myTimer: func.TimerRequest) -> None: """Main function for the posture history collector Azure Function.""" @@ -45,8 +102,9 @@ def main(myTimer: func.TimerRequest) -> None: # Read previous state state = read_state() + logging.info(f"State: {state}") last_posture_history_timestamp = state.get("last_posture_history_timestamp", {}) - logging.info(f"Last posture history timestamp from state.json: {last_posture_history_timestamp}") + logging.info(f"Last posture history timestamp from Azure Blob Storage: {last_posture_history_timestamp}") # Call main function with last value new_posture_history_timestamp = run_posture_history_collection_process(last_posture_history_timestamp) @@ -56,19 +114,19 @@ def main(myTimer: func.TimerRequest) -> None: logging.warning("Collection process did not return new timestamps. Keeping old values.") new_posture_history_timestamp = last_posture_history_timestamp - # Update state.json + # Update state in Azure Blob Storage state["last_posture_history_timestamp"] = new_posture_history_timestamp write_state(state) - logging.info(f"State updated in state.json: {new_posture_history_timestamp}") + logging.info(f"State updated in Azure Blob Storage: {new_posture_history_timestamp}") except KeyError as e: logging.error(f"Missing one or more required environment variables: {e}") + except json.JSONDecodeError as e: + logging.error(f"Error reading or writing state blob: {e}") except ValueError as e: logging.error(f"Configuration error: {e}") except ResourceNotFoundError as e: logging.error(f"Azure Key Vault secret not found: {e}") - except json.JSONDecodeError as e: - logging.error(f"Error reading or writing state file: {e}") except Exception as e: logging.error(f"Unexpected error: {str(e)}", exc_info=True) finally: diff --git a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/requirements.txt b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/requirements.txt index f35bbdc0c6b..33974223a59 100644 --- a/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/requirements.txt +++ b/Solutions/BloodHound Enterprise/Data Connectors/BloodHoundDataConnector/requirements.txt @@ -2,10 +2,11 @@ # The Python Worker is managed by Azure Functions platform # Manually managing azure-functions-worker may cause unexpected issues -azure-functions +azure-functions==1.18.0 azure-identity==1.16.0 -azure-keyvault-secrets -azure-core -azure-functions-durable == 1.3.3 -cryptography==44.0.1 +azure-keyvault-secrets==4.8.0 +azure-core>=1.29.5,<2.0.0 +azure-functions-durable==1.3.3 +azure-storage-blob==12.19.0 +cryptography==3.4.8 requests diff --git a/Solutions/BloodHound Enterprise/Data Connectors/azuredeploy_BloodHoundEnterprise_FunctionApp.json b/Solutions/BloodHound Enterprise/Data Connectors/azuredeploy_BloodHoundEnterprise_FunctionApp.json index b1552aeb66b..e786dd68ec6 100644 --- a/Solutions/BloodHound Enterprise/Data Connectors/azuredeploy_BloodHoundEnterprise_FunctionApp.json +++ b/Solutions/BloodHound Enterprise/Data Connectors/azuredeploy_BloodHoundEnterprise_FunctionApp.json @@ -3,60 +3,74 @@ "contentVersion": "1.0.0.0", "parameters": { "functionAppName": { - "type": "string", - "metadata": { - "description": "Name of the Function App. This must be unique across Azure, since each instance requires its own Function App (e.g., BloodHoundEnterprise-Maple)." - } + "type": "String", + "metadata": { + "description": "Name of the Function App. This must be unique across Azure, since each instance requires its own Function App (e.g., BloodHoundEnterprise-Maple)." + } }, "logAnalyticsWorkspaceName": { - "type": "string", + "type": "String", "metadata": { "description": "The name of the existing Log Analytics Workspace where you want to create a Data Collection Endpoint (DCE) and Data Collection Rule (DCR) for custom tables." } }, "bloodhoundTenantDomain": { - "type": "string", + "type": "String", "metadata": { "description": "The URL for the BloodHound Enterprise tenant domain." } }, "Bloodhound-Token-Id-Secret-Value": { - "type": "securestring", + "type": "SecureString", "metadata": { "description": "The value for the BloodHound token ID. This value will be stored in an Azure Key Vault secret." } }, "Bloodhound-Token-Key-Secret-Value": { - "type": "securestring", + "type": "SecureString", "metadata": { "description": "The value for the BloodHound token key. This value will be stored in an Azure Key Vault secret." } }, "microsoftEntraIdApplicationAppId": { - "type": "string", + "type": "String", "metadata": { "description": "The unique identifier for the Microsoft Entra ID application. This ID, also known as the Client ID, is used to authenticate your application to the Microsoft identity platform." } }, "microsoftEntraIdApplicationAppSecret": { - "type": "securestring", + "type": "SecureString", "metadata": { "description": "A confidential secret generated for your Microsoft Entra ID application. This secret, also known as the Client Secret, is used along with the App ID to prove the application's identity when requesting an access token." } }, + "lookupDays": { + "defaultValue": 2, + "type": "Int", + "metadata": { + "description": "Specifies the number of days in the past for which the system should fetch data. A higher value means more historical data will be retrieved, which increases the time and compute resources required, during the first iteration when setting up the system. This parameter sets the default lookback period when no previous timestamp is available." + } + }, "selectedBloodhoundEnvironments": { - "type": "string", "defaultValue": "All", + "type": "String", "metadata": { "description": "The selected BloodHound environments from which you want to fetch data. These should be provided as comma-separated values (e.g., Ghost.Corp, Phantom.Corp). The default value is All." } }, "selectedFindingTypes": { - "type": "string", "defaultValue": "All", + "type": "String", "metadata": { "description": "The selected finding types." } + }, + "utcTimeZone": { + "type": "String", + "metadata": { + "description": "The UTC time zone to use for the Function App." + }, + "defaultValue": "[utcNow()]" } }, "variables": { @@ -79,14 +93,19 @@ "applicationInsightsName": "[parameters('functionAppName')]", "subscriptionId": "[subscription().subscriptionId]", "logAnalyticsWorkspaceLocation": "[resourceGroup().location]", - "logAnalyticsWorkspaceResourceGroupName": "[resourceGroup().name]", + "logAnalyticsWorkspaceResourceGroupName": "[resourceGroup().name]", "tenantId": "[subscription().tenantId]", "storageAccountType": "Standard_LRS", "keyVaultName": "[concat('kv-', toLower(parameters('functionAppName')))]", "bloodhoundTokenIdSecretName": "Bloodhound-Token-Id-Secret-Value", "bloodhoundTokenKeySecretName": "Bloodhound-Token-Key-Secret-Value", "dceName": "BloodHoundDCE", - "dcrName": "BloodHoundDCR" + "dcrName": "BloodHoundDCR", + "managedIdentityName": "[concat('deploymentScriptIdentity-', parameters('functionAppName'))]", + "storageBlobDataContributorRoleId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "deploymentContainerName": "deploymentpackage", + "zipFileName": "[concat(parameters('functionAppName'), '.zip')]", + "zipFileUrl": "https://github.com/metron-labs/Azure-Sentinel/blob/bloodhound/Solutions/BloodHound%20Enterprise/Data%20Connectors/BloodHoundDataConnector/BloodHoundAzureFunction.zip?raw=true" }, "resources": [ { @@ -101,9 +120,9 @@ } }, { - "name": "[concat(parameters('logAnalyticsWorkspaceName'), '/', variables('bheAttackPathsTable'))]", "type": "Microsoft.OperationalInsights/workspaces/tables", "apiVersion": "2022-10-01", + "name": "[concat(parameters('logAnalyticsWorkspaceName'), '/', variables('bheAttackPathsTable'))]", "properties": { "schema": { "name": "[variables('bheAttackPathsTable')]", @@ -230,11 +249,11 @@ }, { "name": "created_at", - "type": "string" + "type": "datetime" }, { "name": "updated_at", - "type": "string" + "type": "datetime" }, { "name": "deleted_at", @@ -269,9 +288,9 @@ } }, { - "name": "[concat(parameters('logAnalyticsWorkspaceName'), '/', variables('bheAuditLogsTable'))]", "type": "Microsoft.OperationalInsights/workspaces/tables", "apiVersion": "2022-10-01", + "name": "[concat(parameters('logAnalyticsWorkspaceName'), '/', variables('bheAuditLogsTable'))]", "properties": { "schema": { "name": "[variables('bheAuditLogsTable')]", @@ -337,9 +356,9 @@ } }, { - "name": "[concat(parameters('logAnalyticsWorkspaceName'), '/', variables('bheFindingTrendsTable'))]", "type": "Microsoft.OperationalInsights/workspaces/tables", "apiVersion": "2022-10-01", + "name": "[concat(parameters('logAnalyticsWorkspaceName'), '/', variables('bheFindingTrendsTable'))]", "properties": { "schema": { "name": "[variables('bheFindingTrendsTable')]", @@ -421,9 +440,9 @@ } }, { - "name": "[concat(parameters('logAnalyticsWorkspaceName'), '/', variables('bhePostureHistoryTable'))]", "type": "Microsoft.OperationalInsights/workspaces/tables", "apiVersion": "2022-10-01", + "name": "[concat(parameters('logAnalyticsWorkspaceName'), '/', variables('bhePostureHistoryTable'))]", "properties": { "schema": { "name": "[variables('bhePostureHistoryTable')]", @@ -473,9 +492,9 @@ } }, { - "name": "[concat(parameters('logAnalyticsWorkspaceName'), '/', variables('bheTierZeroAssetsTable'))]", - "type": "Microsoft.OperationalInsights/workspaces/tables", + "type": "Microsoft.OperationalInsights/workspaces/tables", "apiVersion": "2022-10-01", + "name": "[concat(parameters('logAnalyticsWorkspaceName'), '/', variables('bheTierZeroAssetsTable'))]", "properties": { "schema": { "name": "[variables('bheTierZeroAssetsTable')]", @@ -933,9 +952,9 @@ } }, { - "name": "[concat(parameters('logAnalyticsWorkspaceName'), '/', variables('bheAttackPathsTimelineTable'))]", - "type": "Microsoft.OperationalInsights/workspaces/tables", + "type": "Microsoft.OperationalInsights/workspaces/tables", "apiVersion": "2022-10-01", + "name": "[concat(parameters('logAnalyticsWorkspaceName'), '/', variables('bheAttackPathsTimelineTable'))]", "properties": { "schema": { "name": "[variables('bheAttackPathsTimelineTable')]", @@ -1149,11 +1168,11 @@ }, { "name": "created_at", - "type": "string" + "type": "datetime" }, { "name": "updated_at", - "type": "string" + "type": "datetime" }, { "name": "deleted_at", @@ -1968,23 +1987,24 @@ "tags": { "[format('hidden-link:{0}', resourceId('Microsoft.Web/sites', parameters('functionAppName')))]": "Resource" }, + "kind": "web", "properties": { "Application_Type": "web" - }, - "kind": "web" + } }, { "type": "Microsoft.Web/sites", - "apiVersion": "2024-04-01", + "apiVersion": "2023-12-01", "name": "[parameters('functionAppName')]", "location": "[variables('logAnalyticsWorkspaceLocation')]", - "kind": "functionapp,linux", "dependsOn": [ "[resourceId('Microsoft.Insights/components', variables('applicationInsightsName'))]", "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]", - "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]" + "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]", + "[resourceId('Microsoft.Resources/deploymentScripts', 'uploadZipToBlob')]" ], + "kind": "functionapp,linux", "identity": { "type": "SystemAssigned" }, @@ -1995,20 +2015,12 @@ "appSettings": [ { "name": "AzureWebJobsStorage", - "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2022-09-01').keys[0].value)]" + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2023-01-01').keys[0].value, ';EndpointSuffix=core.windows.net')]" }, { "name": "FUNCTIONS_EXTENSION_VERSION", "value": "~4" }, - { - "name": "FUNCTIONS_WORKER_RUNTIME", - "value": "python" - }, - { - "name": "WEBSITE_RUN_FROM_PACKAGE", - "value": "https://github.com/metron-labs/Azure-Sentinel/blob/bloodhound/Solutions/BloodHound%20Enterprise/Data%20Connectors/BloodHoundDataConnector/BloodHoundAzureFunction.zip?raw=true" - }, { "name": "BLOODHOUND_TENANT_DOMAIN", "value": "[parameters('bloodhoundTenantDomain')]" @@ -2069,6 +2081,10 @@ "name": "KEY_VAULT_URL", "value": "[reference(resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName')), '2021-06-01-preview').vaultUri]" }, + { + "name": "LOOKUP_DAYS", + "value": "[parameters('lookupDays')]" + }, { "name": "SELECTED_BLOODHOUND_ENVIRONMENTS", "value": "[parameters('selectedBloodhoundEnvironments')]" @@ -2076,6 +2092,30 @@ { "name": "SELECTED_FINDING_TYPES", "value": "[parameters('selectedFindingTypes')]" + }, + { + "name": "API_PAGE_SIZE", + "value": "1000" + }, + { + "name": "AZURE_BATCH_SIZE", + "value": "100" + }, + { + "name": "MAX_RETRIES", + "value": "9" + }, + { + "name": "MAX_REQUESTS_PER_SECOND_LIMIT", + "value": "10" + }, + { + "name": "APPINSIGHTS_INSTRUMENTATIONKEY", + "value": "[reference(resourceId('Microsoft.Insights/components', variables('applicationInsightsName')), '2020-02-02').InstrumentationKey]" + }, + { + "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", + "value": "[reference(resourceId('Microsoft.Insights/components', variables('applicationInsightsName')), '2020-02-02').ConnectionString]" } ], "cors": { @@ -2083,23 +2123,41 @@ "https://portal.azure.com" ] }, - "linuxFxVersion": "Python|3.12", "ftpsState": "Disabled" + }, + "functionAppConfig": { + "deployment": { + "storage": { + "type": "BlobContainer", + "value": "[concat(reference(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2022-09-01').primaryEndpoints.blob, variables('deploymentContainerName'))]", + "authentication": { + "type": "SystemAssignedIdentity" + } + } + }, + "scaleAndConcurrency": { + "maximumInstanceCount": 40, + "instanceMemoryMB": 2048 + }, + "runtime": { + "name": "python", + "version": "3.12" + } } } }, { "type": "Microsoft.Web/serverfarms", - "apiVersion": "2022-03-01", + "apiVersion": "2023-01-01", "name": "[variables('appServicePlanName')]", "location": "[variables('logAnalyticsWorkspaceLocation')]", "sku": { - "name": "Y1", - "tier": "Dynamic" + "name": "FC1", + "tier": "FlexConsumption" }, "kind": "functionapp", "properties": { - "reserved": true + "reserved": true } }, { @@ -2113,11 +2171,144 @@ "kind": "StorageV2", "properties": {} }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices", + "apiVersion": "2023-01-01", + "name": "[concat(variables('storageAccountName'), '/default')]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + ], + "properties": { + "deleteRetentionPolicy": { + "enabled": false + } + } + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2023-01-01", + "name": "[concat(variables('storageAccountName'), '/default/deploymentpackage')]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', variables('storageAccountName'), 'default')]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + ], + "properties": { + "publicAccess": "None" + } + }, + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-01-31", + "name": "[variables('managedIdentityName')]", + "location": "[variables('logAnalyticsWorkspaceLocation')]" + }, + { + "type": "Microsoft.Storage/storageAccounts/providers/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[concat(variables('storageAccountName'), '/Microsoft.Authorization/', guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), variables('managedIdentityName'), 'StorageBlobDataContributor'))]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', variables('storageAccountName'), 'default')]", + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('managedIdentityName'))]" + ], + "properties": { + "roleDefinitionId": "[variables('storageBlobDataContributorRoleId')]", + "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('managedIdentityName')), '2023-01-31').principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2020-10-01", + "name": "uploadZipToBlob", + "location": "[variables('logAnalyticsWorkspaceLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageAccountName'), 'default', variables('deploymentContainerName'))]", + "[resourceId('Microsoft.Storage/storageAccounts/providers/roleAssignments', variables('storageAccountName'), 'Microsoft.Authorization', guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), variables('managedIdentityName'), 'StorageBlobDataContributor'))]" + ], + "kind": "AzureCLI", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('managedIdentityName'))]": {} + } + }, + "properties": { + "azCliVersion": "2.52.0", + "timeout": "PT10M", + "retentionInterval": "PT1H", + "cleanupPreference": "OnSuccess", + "forceUpdateTag": "[parameters('utcTimeZone')]", + "scriptContent": "[concat('#!/bin/bash\nset -e\necho \"Starting deployment script...\"\necho \"Waiting for role assignment to propagate...\"\nsleep 30\necho \"Downloading zip file...\"\ncurl -L --fail --show-error -o ', variables('zipFileName'), ' \"', variables('zipFileUrl'), '\"\necho \"Verifying downloaded file...\"\nif [ ! -f ', variables('zipFileName'), ' ]; then\n echo \"Error: Downloaded file does not exist\"\n exit 1\nfi\necho \"File downloaded successfully\"\necho \"Uploading to storage account...\"\naz storage blob upload --account-name ', variables('storageAccountName'), ' --container-name ', variables('deploymentContainerName'), ' --name ', variables('zipFileName'), ' --file ', variables('zipFileName'), ' --auth-mode login --overwrite --no-progress\necho \"Upload completed successfully\"\n')]" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(resourceGroup().id, 'StorageBlobDataContributor', parameters('functionAppName'))]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "[resourceId('Microsoft.Web/sites', parameters('functionAppName'))]" + ], + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionAppName')), '2023-12-01', 'Full').identity.principalId]", + "principalType": "ServicePrincipal" + }, + "scope": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + }, + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2020-10-01", + "name": "WaitSection", + "location": "[variables('logAnalyticsWorkspaceLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', parameters('functionAppName'))]" + ], + "kind": "AzurePowerShell", + "properties": { + "azPowerShellVersion": "7.0", + "scriptContent": "start-sleep -Seconds 30", + "cleanupPreference": "Always", + "retentionInterval": "PT1H" + } + }, + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2020-10-01", + "name": "generateSasUrl", + "location": "[variables('logAnalyticsWorkspaceLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Resources/deploymentScripts', 'uploadZipToBlob')]", + "[resourceId('Microsoft.Resources/deploymentScripts', 'WaitSection')]" + ], + "kind": "AzurePowerShell", + "properties": { + "azPowerShellVersion": "7.0", + "timeout": "PT5M", + "retentionInterval": "PT1H", + "cleanupPreference": "OnSuccess", + "scriptContent": "[concat('$storageAccountName = \"', variables('storageAccountName'), '\"\n$containerName = \"', variables('deploymentContainerName'), '\"\n$blobName = \"', variables('zipFileName'), '\"\n$storageAccountKey = \"', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2023-01-01').keys[0].value, '\"\n$ctx = New-AzStorageContext -StorageAccountName $storageAccountName -StorageAccountKey $storageAccountKey\n$expiryTime = (Get-Date).AddHours(1)\n$sasToken = New-AzStorageBlobSASToken -Context $ctx -Container $containerName -Blob $blobName -Permission r -ExpiryTime $expiryTime\n$blobUrl = \"', reference(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2022-09-01').primaryEndpoints.blob, variables('deploymentContainerName'), '/', variables('zipFileName'), '?\" + $sasToken\n$DeploymentScriptOutputs = @{}\n$DeploymentScriptOutputs[''result''] = $blobUrl\n')]" + } + }, + { + "type": "Microsoft.Web/sites/extensions", + "apiVersion": "2022-09-01", + "name": "[concat(parameters('functionAppName'), '/onedeploy')]", + "dependsOn": [ + "[resourceId('Microsoft.Resources/deploymentScripts', 'generateSasUrl')]" + ], + "properties": { + "packageUri": "[reference(resourceId('Microsoft.Resources/deploymentScripts', 'generateSasUrl'), '2020-10-01').outputs.result]", + "remoteBuild": true + } + }, { "type": "Microsoft.KeyVault/vaults", "apiVersion": "2021-06-01-preview", "name": "[variables('keyVaultName')]", "location": "[variables('logAnalyticsWorkspaceLocation')]", + "dependsOn": [], "properties": { "sku": { "family": "A", @@ -2129,7 +2320,6 @@ "enabledForTemplateDeployment": true, "accessPolicies": [] }, - "dependsOn": [], "resources": [ { "type": "secrets", @@ -2159,7 +2349,6 @@ "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", "name": "[guid(resourceGroup().id, 'KeyVaultSecretsUser', parameters('functionAppName'))]", - "scope": "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]", "dependsOn": [ "[resourceId('Microsoft.Web/sites', parameters('functionAppName'))]", "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]" @@ -2167,7 +2356,26 @@ "properties": { "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6')]", "principalId": "[reference(resourceId('Microsoft.Web/sites', parameters('functionAppName')), '2022-03-01', 'Full').identity.principalId]" - } + }, + "scope": "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]" + } + ], + "outputs": { + "storageAccountName": { + "type": "String", + "value": "[variables('storageAccountName')]" + }, + "containerName": { + "type": "String", + "value": "[variables('deploymentContainerName')]" + }, + "zipFileName": { + "type": "String", + "value": "[variables('zipFileName')]" + }, + "blobUrl": { + "type": "String", + "value": "[concat(reference(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2022-09-01').primaryEndpoints.blob, variables('deploymentContainerName'), '/', variables('zipFileName'))]" } - ] + } } \ No newline at end of file diff --git a/Solutions/BloodHound Enterprise/Package/3.2.0.zip b/Solutions/BloodHound Enterprise/Package/3.2.0.zip index 7ed9942f3c8..b2d7bd26fa0 100644 Binary files a/Solutions/BloodHound Enterprise/Package/3.2.0.zip and b/Solutions/BloodHound Enterprise/Package/3.2.0.zip differ diff --git a/Solutions/BloodHound Enterprise/Package/mainTemplate.json b/Solutions/BloodHound Enterprise/Package/mainTemplate.json index 1f8c7fd9406..535a83e3f8d 100644 --- a/Solutions/BloodHound Enterprise/Package/mainTemplate.json +++ b/Solutions/BloodHound Enterprise/Package/mainTemplate.json @@ -874,7 +874,7 @@ }, "properties": { "displayName": "[parameters('workbook1-name')]", - "serializedData": "{\"version\":\"Notebook/1.0\",\"items\":[{\"type\":9,\"content\":{\"version\":\"KqlParameterItem/1.0\",\"parameters\":[{\"id\":\"f0dfd85a-6c9c-4dab-91d7-d67cb23b1fb2\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"bhe_tenant\",\"label\":\"BHE Tenant\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEAttackPathsData_CL\\n| where isnotempty(tenant_url)\\n| summarize arg_max(TimeGenerated, *) by tenant_url\\n| extend display_name = replace_string(tenant_url, @\\\"https://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"http://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"/\\\", \\\"\\\") \\n| project tenant_url, display_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":259200000},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"]},{\"id\":\"390213b5-e0d3-476c-99ca-89c76f417e7a\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"domain_name\",\"label\":\"Environment\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEAttackPathsData_CL\\n| where isnotempty(domain_name)\\n| where tenant_url in~ ({bhe_tenant})\\n| distinct domain_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":259200000},\"defaultValue\":\"value::all\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"]},{\"id\":\"3d301840-15be-455d-bb48-d2ca8e3d4c2f\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"finding_type\",\"label\":\"Attack Path Types\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEAttackPathsData_CL \\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| extend CleanPathTitle = trim(\\\" \\\", replace_string(replace_string(PathTitle, \\\"\\\\n\\\", \\\"\\\"), \\\"\\\\'\\\", \\\"\\\"))\\n| distinct CleanPathTitle\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":259200000},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"]},{\"id\":\"9e7a3119-3a53-4df7-8878-d2b56a948732\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"severity\",\"label\":\"Severity\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEAttackPathsData_CL\\r\\n| where isnotempty(Severity)\\r\\n| where tenant_url in~ ({bhe_tenant})\\r\\n| where domain_name in~ ({domain_name})\\r\\n| distinct Severity\\r\\n\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":259200000},\"defaultValue\":\"value::all\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"]},{\"id\":\"7e02e873-7550-447e-88aa-fc99dc923c14\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"time\",\"label\":\"Time Range Picker\",\"type\":4,\"isRequired\":true,\"typeSettings\":{\"selectableValues\":[{\"durationMs\":300000},{\"durationMs\":900000},{\"durationMs\":1800000},{\"durationMs\":3600000},{\"durationMs\":14400000},{\"durationMs\":43200000},{\"durationMs\":86400000},{\"durationMs\":172800000},{\"durationMs\":259200000},{\"durationMs\":604800000},{\"durationMs\":1209600000},{\"durationMs\":2419200000},{\"durationMs\":2592000000},{\"durationMs\":5184000000},{\"durationMs\":7776000000}]},\"value\":{\"durationMs\":604800000}}],\"style\":\"pills\"},\"name\":\"parameters - 3\",\"id\":\"8773eb3e-9066-4dde-8447-fd0939000edc\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEAttackPathsData_CL\\n| summarize arg_max(TimeGenerated, *) by id, domain_name\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| where Severity in~ ({severity})\\n| extend CleanPathTitle = trim(\\\" \\\", replace_string(PathTitle, \\\"\\\\n\\\", \\\"\\\"))\\n| where CleanPathTitle in~ ({finding_type})\\n| extend \\n NonTierZeroPrincipalDistinguishedName = tostring(parse_json(NonTierZeroPrincipalProps).distinguishedname),\\n NonTierZeroPrincipalSAMAccountName = tostring(parse_json(NonTierZeroPrincipalProps).samaccountname),\\n NonTierZeroPrincipalLastLogon = todatetime(unixtime_seconds_todatetime(tolong(parse_json(NonTierZeroPrincipalProps).lastlogon))),\\n NonTierZeroPrincipalLastLogonTimestamp = todatetime(unixtime_seconds_todatetime(tolong(parse_json(NonTierZeroPrincipalProps).lastlogontimestamp))),\\n NonTierZeroPrincipalCreated = todatetime(unixtime_seconds_todatetime(tolong(parse_json(NonTierZeroPrincipalProps).whencreated))),\\n IsTierZero = case(\\n tostring(parse_json(ImpactedPrincipalProps).system_tags) contains \\\"admin_tier_0\\\", true,\\n false\\n )\\n| project \\n [\\\"Non Tier Zero Principal\\\"] = NonTierZeroPrincipalName,\\n [\\\"Non Tier Zero Principal Type\\\"] = NonTierZeroPrincipalKind,\\n [\\\"Impacted Principal\\\"] = ImpactedPrincipalName,\\n [\\\"Impacted Principal Type\\\"] = ImpactedPrincipalKind,\\n [\\\"Finding Type\\\"] = PathTitle,\\n [\\\"Finding Key\\\"] = Finding,\\n [\\\"Environment (domain)\\\"] = domain_name,\\n [\\\"Severity\\\"] = Severity,\\n [\\\"Impact %\\\"] = round(todouble(ImpactPercentage), 0),\\n [\\\"Impact Count\\\"] = toint(ImpactCount),\\n [\\\"Exposure %\\\"] = round(todouble(ExposurePercentage), 0),\\n [\\\"Exposure Count\\\"] = toint(ExposureCount),\\n [\\\"First Seen\\\"] = todatetime(created_at),\\n [\\\"Last Updated\\\"] = todatetime(updated_at),\\n [\\\"Impacted Distinguished Name\\\"] = NonTierZeroPrincipalDistinguishedName,\\n [\\\"SAM Account Name\\\"] = NonTierZeroPrincipalSAMAccountName,\\n [\\\"Impacted ObjectID\\\"] = ImpactedPrincipal\\n| order by [\\\"Last Updated\\\"] desc\\n\\n\\n\\n\\n\\n\",\"size\":0,\"title\":\"Principals\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"gridSettings\":{\"rowLimit\":10000,\"filter\":true}},\"name\":\"query - 2\",\"id\":\"8537bfa2-797b-4341-b7ec-d92db2901627\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEAttackPathsTimelineData_CL\\n| where isnotempty(domain_name)\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| extend CleanPathTitle = trim(\\\" \\\", replace_string(path_title, \\\"\\\\n\\\", \\\"\\\"))\\n| where CleanPathTitle in~ ({finding_type})\\n| extend event_day = format_datetime(todatetime(updated_at), \\\"yyyy-MM-dd\\\")\\n| summarize arg_max(updated_at, *) by event_day, tenant_url, domain_name, CleanPathTitle\\n| extend _time = todatetime(event_day)\\n| extend CompositeRisk = round(todouble(CompositeRisk), 2)\\n| summarize LatestCompositeRisk = max(CompositeRisk) by bin(_time, 1d)\\n| order by _time asc\\n\",\"size\":0,\"aggregation\":2,\"title\":\"Maximum Exposure Percentage\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"areachart\",\"tileSettings\":{\"showBorder\":false}},\"name\":\"query - 4\",\"id\":\"27480ded-ba27-4733-bf00-9157dd53c578\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEAttackPathsTimelineData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| extend CleanPathTitle = trim(\\\" \\\", replace_string(path_title, \\\"\\\\n\\\", \\\"\\\"))\\n| where CleanPathTitle in~ ({finding_type})\\n| extend _time = todatetime(updated_at)\\n| where isnotnull(_time)\\n| summarize arg_max(TimeGenerated, *) by Finding, domain_name, tenant_url\\n| summarize SumFindingCount = sum(toint(FindingCount)) by bin(_time, 1d)\\n| order by _time asc\\n\",\"size\":0,\"title\":\"Total Number of Findings\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"areachart\"},\"name\":\"query - 6\",\"id\":\"02021bd8-8939-4b21-a9d1-f7dc4405cd4b\"}],\"fromTemplateId\":\"sentinel-UserWorkbook\",\"$schema\":\"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"}\r\n", + "serializedData": "{\"version\":\"Notebook/1.0\",\"items\":[{\"type\":9,\"content\":{\"version\":\"KqlParameterItem/1.0\",\"parameters\":[{\"id\":\"f0dfd85a-6c9c-4dab-91d7-d67cb23b1fb2\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"bhe_tenant\",\"label\":\"BHE Tenant\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEAttackPathsData_CL\\n| where isnotempty(tenant_url)\\n| summarize arg_max(TimeGenerated, *) by tenant_url\\n| extend display_name = replace_string(tenant_url, @\\\"https://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"http://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"/\\\", \\\"\\\") \\n| project tenant_url, display_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"],\"key\":\"f0dfd85a-6c9c-4dab-91d7-d67cb23b1fb2\",\"timeContextFromParameter\":\"time\"},{\"id\":\"390213b5-e0d3-476c-99ca-89c76f417e7a\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"domain_name\",\"label\":\"Environment\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEAttackPathsData_CL\\n| where isnotempty(domain_name)\\n| where tenant_url in~ ({bhe_tenant})\\n| distinct domain_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"defaultValue\":\"value::all\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"],\"key\":\"390213b5-e0d3-476c-99ca-89c76f417e7a\",\"timeContextFromParameter\":\"time\"},{\"id\":\"3d301840-15be-455d-bb48-d2ca8e3d4c2f\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"finding_type\",\"label\":\"Attack Path Types\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEAttackPathsData_CL \\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| extend CleanPathTitle = trim(\\\" \\\", replace_string(replace_string(PathTitle, \\\"\\\\n\\\", \\\"\\\"), \\\"\\\\'\\\", \\\"\\\"))\\n| distinct CleanPathTitle\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"],\"key\":\"3d301840-15be-455d-bb48-d2ca8e3d4c2f\",\"timeContextFromParameter\":\"time\"},{\"id\":\"9e7a3119-3a53-4df7-8878-d2b56a948732\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"severity\",\"label\":\"Severity\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEAttackPathsData_CL\\r\\n| where isnotempty(Severity)\\r\\n| where tenant_url in~ ({bhe_tenant})\\r\\n| where domain_name in~ ({domain_name})\\r\\n| distinct Severity\\r\\n\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"defaultValue\":\"value::all\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"],\"key\":\"9e7a3119-3a53-4df7-8878-d2b56a948732\",\"timeContextFromParameter\":\"time\"},{\"id\":\"7e02e873-7550-447e-88aa-fc99dc923c14\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"time\",\"label\":\"Time Range Picker\",\"type\":4,\"isRequired\":true,\"typeSettings\":{\"selectableValues\":[{\"durationMs\":300000},{\"durationMs\":900000},{\"durationMs\":1800000},{\"durationMs\":3600000},{\"durationMs\":14400000},{\"durationMs\":43200000},{\"durationMs\":86400000},{\"durationMs\":172800000},{\"durationMs\":259200000},{\"durationMs\":604800000},{\"durationMs\":1209600000},{\"durationMs\":2419200000},{\"durationMs\":2592000000},{\"durationMs\":5184000000},{\"durationMs\":7776000000}]},\"value\":{\"durationMs\":604800000},\"key\":\"7e02e873-7550-447e-88aa-fc99dc923c14\"}],\"style\":\"pills\"},\"name\":\"parameters - 3\",\"id\":\"8773eb3e-9066-4dde-8447-fd0939000edc\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEAttackPathsData_CL\\n| summarize arg_max(TimeGenerated, *) by id, domain_name\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| where Severity in~ ({severity})\\n| where updated_at {time}\\n| extend CleanPathTitle = trim(\\\" \\\", replace_string(PathTitle, \\\"\\\\n\\\", \\\"\\\"))\\n| where CleanPathTitle in~ ({finding_type})\\n| extend \\n NonTierZeroPrincipalDistinguishedName = tostring(parse_json(NonTierZeroPrincipalProps).distinguishedname),\\n NonTierZeroPrincipalSAMAccountName = tostring(parse_json(NonTierZeroPrincipalProps).samaccountname),\\n NonTierZeroPrincipalLastLogon = todatetime(unixtime_seconds_todatetime(tolong(parse_json(NonTierZeroPrincipalProps).lastlogon))),\\n NonTierZeroPrincipalLastLogonTimestamp = todatetime(unixtime_seconds_todatetime(tolong(parse_json(NonTierZeroPrincipalProps).lastlogontimestamp))),\\n NonTierZeroPrincipalCreated = todatetime(unixtime_seconds_todatetime(tolong(parse_json(NonTierZeroPrincipalProps).whencreated))),\\n IsTierZero = case(\\n tostring(parse_json(ImpactedPrincipalProps).system_tags) contains \\\"admin_tier_0\\\", true,\\n false\\n )\\n| project \\n [\\\"Non Tier Zero Principal\\\"] = NonTierZeroPrincipalName,\\n [\\\"Non Tier Zero Principal Type\\\"] = NonTierZeroPrincipalKind,\\n [\\\"Impacted Principal\\\"] = ImpactedPrincipalName,\\n [\\\"Impacted Principal Type\\\"] = ImpactedPrincipalKind,\\n [\\\"Finding Type\\\"] = PathTitle,\\n [\\\"Finding Key\\\"] = Finding,\\n [\\\"Environment (domain)\\\"] = domain_name,\\n [\\\"Severity\\\"] = Severity,\\n [\\\"Impact %\\\"] = round(todouble(ImpactPercentage), 0),\\n [\\\"Impact Count\\\"] = toint(ImpactCount),\\n [\\\"Exposure %\\\"] = round(todouble(ExposurePercentage), 0),\\n [\\\"Exposure Count\\\"] = toint(ExposureCount),\\n [\\\"First Seen\\\"] = todatetime(created_at),\\n [\\\"Last Updated\\\"] = todatetime(updated_at),\\n [\\\"Impacted Distinguished Name\\\"] = NonTierZeroPrincipalDistinguishedName,\\n [\\\"SAM Account Name\\\"] = NonTierZeroPrincipalSAMAccountName,\\n [\\\"Impacted ObjectID\\\"] = ImpactedPrincipal\\n| order by [\\\"Last Updated\\\"] desc\\n\\n\\n\\n\\n\\n\",\"size\":0,\"title\":\"Principals\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"gridSettings\":{\"rowLimit\":10000,\"filter\":true}},\"name\":\"query - 2\",\"id\":\"8537bfa2-797b-4341-b7ec-d92db2901627\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEAttackPathsTimelineData_CL\\n| where isnotempty(domain_name)\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| where created_at {time}\\n| extend CleanPathTitle = trim(\\\" \\\", replace_string(path_title, \\\"\\\\n\\\", \\\"\\\"))\\n| where CleanPathTitle in~ ({finding_type})\\n| extend event_day = format_datetime(todatetime(updated_at), \\\"yyyy-MM-dd\\\")\\n| summarize arg_max(updated_at, *) by event_day, tenant_url, domain_name, CleanPathTitle\\n| extend _time = todatetime(event_day)\\n| extend CompositeRisk = round(todouble(CompositeRisk), 2)\\n| summarize LatestCompositeRisk = max(CompositeRisk) by bin(_time, 1d)\\n| order by _time asc\\n\",\"size\":0,\"aggregation\":2,\"title\":\"Maximum Exposure Percentage\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"areachart\",\"tileSettings\":{\"showBorder\":false}},\"name\":\"query - 4\",\"id\":\"27480ded-ba27-4733-bf00-9157dd53c578\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEAttackPathsTimelineData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| extend CleanPathTitle = trim(\\\" \\\", replace_string(path_title, \\\"\\\\n\\\", \\\"\\\"))\\n| where created_at {time}\\n| where CleanPathTitle in~ ({finding_type})\\n| extend _time = todatetime(updated_at)\\n| where isnotnull(_time)\\n| summarize arg_max(TimeGenerated, *) by Finding, domain_name, tenant_url\\n| summarize SumFindingCount = sum(toint(FindingCount)) by bin(_time, 1d)\\n| order by _time asc\\n\",\"size\":0,\"title\":\"Total Number of Findings\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"areachart\"},\"name\":\"query - 6\",\"id\":\"02021bd8-8939-4b21-a9d1-f7dc4405cd4b\"}],\"fromTemplateId\":\"sentinel-UserWorkbook\",\"$schema\":\"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"}\r\n", "version": "1.0", "sourceId": "[variables('workspaceResourceId')]", "category": "sentinel" @@ -962,7 +962,7 @@ }, "properties": { "displayName": "[parameters('workbook2-name')]", - "serializedData": "{\"version\":\"Notebook/1.0\",\"items\":[{\"type\":9,\"content\":{\"version\":\"KqlParameterItem/1.0\",\"parameters\":[{\"id\":\"6de26de7-0eec-4312-b568-9fbbaf5c7f71\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"bhe_tenant\",\"label\":\"BHE Tenant\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEAttackPathsData_CL \\n| where isnotempty(tenant_url)\\n| summarize arg_max(TimeGenerated, *) by tenant_url\\n| extend display_name = replace_string(tenant_url, @\\\"https://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"http://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"/\\\", \\\"\\\") \\n| project tenant_url, display_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":259200000},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"]},{\"id\":\"cb118fcd-4473-4470-ac89-28c6ea644d5d\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"domain_name\",\"label\":\"Environment\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEAttackPathsData_CL \\n| where isnotempty(domain_name)\\n| where tenant_url in~ ({bhe_tenant})\\n| distinct domain_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":259200000},\"defaultValue\":\"value::all\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\"},{\"id\":\"9e7a3119-3a53-4df7-8878-d2b56a948732\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"severity\",\"label\":\"Severity\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEAttackPathsData_CL\\r\\n| where isnotempty(Severity)\\r\\n| where tenant_url in~ ({bhe_tenant})\\r\\n| where domain_name in~ ({domain_name})\\r\\n| distinct Severity\\r\\n\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":259200000},\"defaultValue\":\"value::all\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"]},{\"id\":\"dafad90f-2d00-41cd-9463-efbe87d3888f\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"time\",\"label\":\"Time Range Picker\",\"type\":4,\"isRequired\":true,\"typeSettings\":{\"selectableValues\":[{\"durationMs\":300000},{\"durationMs\":900000},{\"durationMs\":1800000},{\"durationMs\":3600000},{\"durationMs\":14400000},{\"durationMs\":43200000},{\"durationMs\":86400000},{\"durationMs\":172800000},{\"durationMs\":259200000},{\"durationMs\":604800000},{\"durationMs\":1209600000},{\"durationMs\":2419200000},{\"durationMs\":2592000000},{\"durationMs\":5184000000},{\"durationMs\":7776000000}]},\"value\":{\"durationMs\":604800000}}],\"style\":\"pills\"},\"name\":\"parameters - 2\",\"id\":\"411d55f1-a1a9-401f-94e3-099311400cb5\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEAttackPathsData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| where Severity in~ ({severity})\\n| summarize arg_max(TimeGenerated, *) by id, domain_name, tenant_url\\n| summarize TotalAttackPathsFindings = dcount(id) by DomainName = domain_name\\n| sort by TotalAttackPathsFindings desc\",\"size\":1,\"title\":\"Total Attack Paths Findings per Domain\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"barchart\",\"graphSettings\":{\"type\":0,\"topContent\":{\"columnMatch\":\"DomainName\",\"formatter\":1},\"centerContent\":{\"columnMatch\":\"TotalAttackPathsFindings\",\"formatter\":1,\"numberFormat\":{\"unit\":17,\"options\":{\"maximumSignificantDigits\":3,\"maximumFractionDigits\":2}}}},\"chartSettings\":{\"showLegend\":true}},\"name\":\"query - 2\",\"id\":\"a2a75d1b-0822-4cbe-b834-8873bb80b72d\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEAttackPathsData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| where Severity in~ ({severity})\\n| summarize arg_max(TimeGenerated, *) by id, domain_name, tenant_url\\n| summarize Count = dcount(id) by Severity\\n| sort by Count desc\\n\",\"size\":0,\"title\":\"Severity Breakdown\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"piechart\"},\"name\":\"query - 5\",\"id\":\"98a3ac6b-a667-4950-af14-d83b28a75f65\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEAttackPathsData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| where Severity in~ ({severity})\\n| summarize arg_max(TimeGenerated, *) by id\\n| where isnotempty(NonTierZeroPrincipalName)\\n| summarize FindingsCount = count() by NonTierZeroPrincipalName, domain_name\\n| project-rename Environment = domain_name\\n| sort by FindingsCount desc\\n| take 5\\n\",\"size\":1,\"aggregation\":2,\"title\":\"Top 5 Non-Tier Zero Principals Involved in Findings\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"chartSettings\":{\"xAxis\":\"FindingsCount\",\"yAxis\":[\"FindingsCount\"],\"showLegend\":true}},\"name\":\"query - 7\",\"id\":\"06ca7136-8db4-41a3-b09e-1cf692baa8eb\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEAttackPathsData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| where Severity in~ ({severity})\\n| summarize arg_max(TimeGenerated, *) by id, domain_name, tenant_url\\n| summarize Frequency = dcount(id) by Finding\\n| project-rename [\\\"Finding Key\\\"] = Finding\\n| sort by Frequency desc\\n| take 5\\n\",\"size\":1,\"title\":\"Top 5 Most Common Findings (Finding Keys)\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Frequency\",\"formatter\":3,\"formatOptions\":{\"palette\":\"blue\"}}]},\"chartSettings\":{\"showLegend\":true}},\"name\":\"query - 9\",\"id\":\"39e36309-ece3-4752-8b4b-7999e64da85d\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEAttackPathsData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| where Severity in~ ({severity})\\n| partition by domain_name\\n(\\n summarize arg_max(TimeGenerated, *) by id, domain_name, tenant_url\\n| summarize Frequency = dcount(id) by domain_name, Finding\\n| sort by Frequency desc\\n| take 5\\n)\\n| sort by Frequency desc\",\"size\":0,\"title\":\"Top 5 Most Common Findings (Finding Keys) per Environment\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Frequency\",\"formatter\":3,\"formatOptions\":{\"palette\":\"blue\"}}],\"hierarchySettings\":{\"treeType\":1,\"groupBy\":[\"domain_name\"],\"expandTopLevel\":true},\"labelSettings\":[{\"columnId\":\"domain_name\",\"label\":\"Environment\"}]},\"chartSettings\":{\"showLegend\":true}},\"name\":\"query - 9 - Copy\",\"id\":\"36dd9f9d-913e-47da-a9c7-e43398bf5a0e\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEAttackPathsData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| where Severity in~ ({severity})\\n| summarize arg_max(TimeGenerated, *) by id, domain_name, tenant_url\\n| extend exposure_val = toreal(ExposurePercentage),\\n impact_val = toreal(ImpactPercentage)\\n| extend exposure_pct = iif(isnull(exposure_val), round(impact_val, 2), round(exposure_val, 2))\\n| extend impact_pct = round(impact_val, 2)\\n| summarize \\n ExposurePercent = max(exposure_pct),\\n ImpactPercent = max(impact_pct),\\n Count = count()\\n by Environment = domain_name, AttackPath = PathTitle, Severity\\n| project Environment, AttackPath, Severity, Count, ['Exposure (%)'] = ExposurePercent, ['Impact (%)'] = ImpactPercent\\n| sort by ['Exposure (%)'] desc\\n\",\"size\":0,\"title\":\"All Attack Paths List\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"gridSettings\":{\"rowLimit\":500}},\"name\":\"query - 11\",\"id\":\"7b4a8d14-420a-4097-b2ae-a3c76f45fb1f\"}],\"fromTemplateId\":\"sentinel-UserWorkbook\",\"$schema\":\"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"}\r\n", + "serializedData": "{\"version\":\"Notebook/1.0\",\"items\":[{\"type\":9,\"content\":{\"version\":\"KqlParameterItem/1.0\",\"parameters\":[{\"id\":\"6de26de7-0eec-4312-b568-9fbbaf5c7f71\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"bhe_tenant\",\"label\":\"BHE Tenant\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEAttackPathsData_CL \\n| where isnotempty(tenant_url)\\n| summarize arg_max(TimeGenerated, *) by tenant_url\\n| extend display_name = replace_string(tenant_url, @\\\"https://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"http://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"/\\\", \\\"\\\") \\n| project tenant_url, display_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"],\"key\":\"6de26de7-0eec-4312-b568-9fbbaf5c7f71\",\"timeContextFromParameter\":\"time\"},{\"id\":\"cb118fcd-4473-4470-ac89-28c6ea644d5d\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"domain_name\",\"label\":\"Environment\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEAttackPathsData_CL \\n| where isnotempty(domain_name)\\n| where tenant_url in~ ({bhe_tenant})\\n| distinct domain_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"defaultValue\":\"value::all\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"key\":\"cb118fcd-4473-4470-ac89-28c6ea644d5d\",\"timeContextFromParameter\":\"time\"},{\"id\":\"9e7a3119-3a53-4df7-8878-d2b56a948732\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"severity\",\"label\":\"Severity\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEAttackPathsData_CL\\r\\n| where isnotempty(Severity)\\r\\n| where tenant_url in~ ({bhe_tenant})\\r\\n| where domain_name in~ ({domain_name})\\r\\n| distinct Severity\\r\\n\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"defaultValue\":\"value::all\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"],\"key\":\"9e7a3119-3a53-4df7-8878-d2b56a948732\",\"timeContextFromParameter\":\"time\"},{\"id\":\"dafad90f-2d00-41cd-9463-efbe87d3888f\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"time\",\"label\":\"Time Range Picker\",\"type\":4,\"isRequired\":true,\"typeSettings\":{\"selectableValues\":[{\"durationMs\":300000},{\"durationMs\":900000},{\"durationMs\":1800000},{\"durationMs\":3600000},{\"durationMs\":14400000},{\"durationMs\":43200000},{\"durationMs\":86400000},{\"durationMs\":172800000},{\"durationMs\":259200000},{\"durationMs\":604800000},{\"durationMs\":1209600000},{\"durationMs\":2419200000},{\"durationMs\":2592000000},{\"durationMs\":5184000000},{\"durationMs\":7776000000}]},\"value\":{\"durationMs\":604800000},\"key\":\"dafad90f-2d00-41cd-9463-efbe87d3888f\"}],\"style\":\"pills\"},\"name\":\"parameters - 2\",\"id\":\"411d55f1-a1a9-401f-94e3-099311400cb5\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEAttackPathsData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| where Severity in~ ({severity})\\n| where updated_at {time}\\n| summarize arg_max(TimeGenerated, *) by id, domain_name, tenant_url\\n| summarize TotalAttackPathsFindings = dcount(id) by DomainName = domain_name\\n| sort by TotalAttackPathsFindings desc\",\"size\":1,\"title\":\"Total Attack Paths Findings per Domain\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"barchart\",\"graphSettings\":{\"type\":0,\"topContent\":{\"columnMatch\":\"DomainName\",\"formatter\":1},\"centerContent\":{\"columnMatch\":\"TotalAttackPathsFindings\",\"formatter\":1,\"numberFormat\":{\"unit\":17,\"options\":{\"maximumSignificantDigits\":3,\"maximumFractionDigits\":2}}}},\"chartSettings\":{\"showLegend\":true}},\"name\":\"query - 2\",\"id\":\"a2a75d1b-0822-4cbe-b834-8873bb80b72d\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEAttackPathsData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| where Severity in~ ({severity})\\n| where updated_at {time}\\n| summarize arg_max(TimeGenerated, *) by id, domain_name, tenant_url\\n| summarize Count = dcount(id) by Severity\\n| sort by Count desc\\n\",\"size\":0,\"title\":\"Severity Breakdown\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"piechart\"},\"name\":\"query - 5\",\"id\":\"98a3ac6b-a667-4950-af14-d83b28a75f65\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEAttackPathsData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| where Severity in~ ({severity})\\n| where updated_at {time}\\n| summarize arg_max(TimeGenerated, *) by id\\n| where isnotempty(NonTierZeroPrincipalName)\\n| summarize FindingsCount = count() by NonTierZeroPrincipalName, domain_name\\n| project-rename Environment = domain_name\\n| sort by FindingsCount desc\\n| take 5\\n\",\"size\":1,\"aggregation\":2,\"title\":\"Top 5 Non-Tier Zero Principals Involved in Findings\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"chartSettings\":{\"xAxis\":\"FindingsCount\",\"yAxis\":[\"FindingsCount\"],\"showLegend\":true}},\"name\":\"query - 7\",\"id\":\"06ca7136-8db4-41a3-b09e-1cf692baa8eb\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEAttackPathsData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| where Severity in~ ({severity})\\n| where updated_at {time}\\n| summarize arg_max(TimeGenerated, *) by id, domain_name, tenant_url\\n| summarize Frequency = dcount(id) by Finding\\n| project-rename [\\\"Finding Key\\\"] = Finding\\n| sort by Frequency desc\\n| take 5\\n\",\"size\":1,\"title\":\"Top 5 Most Common Findings (Finding Keys)\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Frequency\",\"formatter\":3,\"formatOptions\":{\"palette\":\"blue\"}}]},\"chartSettings\":{\"showLegend\":true}},\"name\":\"query - 9\",\"id\":\"39e36309-ece3-4752-8b4b-7999e64da85d\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEAttackPathsData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| where Severity in~ ({severity})\\n| where updated_at {time}\\n| partition by domain_name\\n(\\n summarize arg_max(TimeGenerated, *) by id, domain_name, tenant_url\\n| summarize Frequency = dcount(id) by domain_name, Finding\\n| sort by Frequency desc\\n| take 5\\n)\\n| sort by Frequency desc\",\"size\":0,\"title\":\"Top 5 Most Common Findings (Finding Keys) per Environment\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"gridSettings\":{\"formatters\":[{\"columnMatch\":\"Frequency\",\"formatter\":3,\"formatOptions\":{\"palette\":\"blue\"}}],\"hierarchySettings\":{\"treeType\":1,\"groupBy\":[\"domain_name\"],\"expandTopLevel\":true},\"labelSettings\":[{\"columnId\":\"domain_name\",\"label\":\"Environment\"}]},\"chartSettings\":{\"showLegend\":true}},\"name\":\"query - 9 - Copy\",\"id\":\"36dd9f9d-913e-47da-a9c7-e43398bf5a0e\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEAttackPathsData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| where Severity in~ ({severity})\\n| where updated_at {time}\\n| summarize arg_max(TimeGenerated, *) by id, domain_name, tenant_url\\n| extend exposure_val = toreal(ExposurePercentage),\\n impact_val = toreal(ImpactPercentage)\\n| extend exposure_pct = iif(isnull(exposure_val), round(impact_val, 2), round(exposure_val, 2))\\n| extend impact_pct = round(impact_val, 2)\\n| summarize \\n ExposurePercent = max(exposure_pct),\\n ImpactPercent = max(impact_pct),\\n Count = count()\\n by Environment = domain_name, AttackPath = PathTitle, Severity\\n| project Environment, AttackPath, Severity, Count, ['Exposure (%)'] = ExposurePercent, ['Impact (%)'] = ImpactPercent\\n| sort by ['Exposure (%)'] desc\\n\",\"size\":0,\"title\":\"All Attack Paths List\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"gridSettings\":{\"rowLimit\":500}},\"name\":\"query - 11\",\"id\":\"7b4a8d14-420a-4097-b2ae-a3c76f45fb1f\"}],\"fromTemplateId\":\"sentinel-UserWorkbook\",\"$schema\":\"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"}\r\n", "version": "1.0", "sourceId": "[variables('workspaceResourceId')]", "category": "sentinel" @@ -1050,7 +1050,7 @@ }, "properties": { "displayName": "[parameters('workbook3-name')]", - "serializedData": "{\"version\":\"Notebook/1.0\",\"items\":[{\"type\":9,\"content\":{\"version\":\"KqlParameterItem/1.0\",\"parameters\":[{\"id\":\"b8d3205b-b903-4db7-8c7a-f82bacd94242\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"bhe_tenant\",\"label\":\"BHE Tenant\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEAuditLogsData_CL\\n| where isnotempty(tenant_url)\\n| summarize arg_max(TimeGenerated, *) by tenant_url\\n| extend display_name = replace_string(tenant_url, @\\\"https://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"http://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"/\\\", \\\"\\\") \\n| project tenant_url, display_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":259200000},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"]},{\"id\":\"752a2195-64fc-402e-b80f-c7c4fb9b49bd\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"event_type\",\"label\":\"Event Type\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEAuditLogsData_CL\\n| where isnotempty(action)\\n| where tenant_url in~ ({bhe_tenant})\\n| distinct action\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":259200000},\"defaultValue\":\"value::all\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"]},{\"id\":\"5de91703-b999-47c1-98e3-648bb4724abf\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"actor_name\",\"label\":\"Actor name\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEAuditLogsData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where action in~ ({event_type})\\n| distinct actor_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":259200000},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"]},{\"id\":\"705413e9-2968-4485-975f-a782991db8df\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"time\",\"label\":\"Time Range Picker\",\"type\":4,\"isRequired\":true,\"typeSettings\":{\"selectableValues\":[{\"durationMs\":300000},{\"durationMs\":900000},{\"durationMs\":1800000},{\"durationMs\":3600000},{\"durationMs\":14400000},{\"durationMs\":43200000},{\"durationMs\":86400000},{\"durationMs\":172800000},{\"durationMs\":259200000},{\"durationMs\":604800000},{\"durationMs\":1209600000},{\"durationMs\":2419200000},{\"durationMs\":2592000000},{\"durationMs\":5184000000},{\"durationMs\":7776000000}]},\"value\":{\"durationMs\":604800000}}],\"style\":\"pills\"},\"name\":\"parameters - 2\",\"id\":\"a3898d6c-4920-4197-96ae-c00c3a6642ef\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEAuditLogsData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where action in~ ({event_type})\\n| where actor_name in~ ({actor_name})\\n| summarize arg_max(created_at, *) by created_at\\n| project-away _ResourceId, created_at1\\n| order by TimeGenerated desc\\n| project [\\\"Created At\\\"] = created_at, \\n TimeGenerated = TimeGenerated,\\n [\\\"Ingestion Time\\\"] = IngestionTime,\\n Id = id,\\n [\\\"Actor Id\\\"] = actor_id,\\n [\\\"Actor Name\\\"] = actor_name,\\n [\\\"Action\\\"] = action,\\n [\\\"Fields\\\"] = fields,\\n [\\\"Request Id\\\"] = request_id,\\n [\\\"Source IP Address\\\"] = source_ip_address,\\n [\\\"Status\\\"] = status,\\n [\\\"Commit Id\\\"] = commit_id,\\n [\\\"Tenant URL\\\"] = tenant_url,\\n [\\\"Tenant ID\\\"] = TenantId,\\n [\\\"Type\\\"] = Type\\n\\n\\n\\n\\n\",\"size\":3,\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"gridSettings\":{\"sortBy\":[{\"itemKey\":\"TimeGenerated\",\"sortOrder\":2}]},\"sortBy\":[{\"itemKey\":\"TimeGenerated\",\"sortOrder\":2}]},\"name\":\"query - 2\",\"id\":\"1f14cdc2-d692-4f28-ba58-7e920cfd104b\"}],\"isLocked\":false,\"fromTemplateId\":\"sentinel-UserWorkbook\"}\r\n", + "serializedData": "{\"version\":\"Notebook/1.0\",\"items\":[{\"type\":9,\"content\":{\"version\":\"KqlParameterItem/1.0\",\"parameters\":[{\"id\":\"b8d3205b-b903-4db7-8c7a-f82bacd94242\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"bhe_tenant\",\"label\":\"BHE Tenant\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEAuditLogsData_CL\\n| where isnotempty(tenant_url)\\n| summarize arg_max(TimeGenerated, *) by tenant_url\\n| extend display_name = replace_string(tenant_url, @\\\"https://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"http://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"/\\\", \\\"\\\") \\n| project tenant_url, display_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"],\"key\":\"b8d3205b-b903-4db7-8c7a-f82bacd94242\",\"timeContextFromParameter\":\"time\"},{\"id\":\"752a2195-64fc-402e-b80f-c7c4fb9b49bd\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"event_type\",\"label\":\"Event Type\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEAuditLogsData_CL\\n| where isnotempty(action)\\n| where tenant_url in~ ({bhe_tenant})\\n| distinct action\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"defaultValue\":\"value::all\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"],\"key\":\"752a2195-64fc-402e-b80f-c7c4fb9b49bd\",\"timeContextFromParameter\":\"time\"},{\"id\":\"5de91703-b999-47c1-98e3-648bb4724abf\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"actor_name\",\"label\":\"Actor name\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEAuditLogsData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where action in~ ({event_type})\\n| distinct actor_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"],\"key\":\"5de91703-b999-47c1-98e3-648bb4724abf\",\"timeContextFromParameter\":\"time\"},{\"id\":\"705413e9-2968-4485-975f-a782991db8df\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"time\",\"label\":\"Time Range Picker\",\"type\":4,\"isRequired\":true,\"typeSettings\":{\"selectableValues\":[{\"durationMs\":300000},{\"durationMs\":900000},{\"durationMs\":1800000},{\"durationMs\":3600000},{\"durationMs\":14400000},{\"durationMs\":43200000},{\"durationMs\":86400000},{\"durationMs\":172800000},{\"durationMs\":259200000},{\"durationMs\":604800000},{\"durationMs\":1209600000},{\"durationMs\":2419200000},{\"durationMs\":2592000000},{\"durationMs\":5184000000},{\"durationMs\":7776000000}]},\"value\":{\"durationMs\":604800000},\"key\":\"705413e9-2968-4485-975f-a782991db8df\"}],\"style\":\"pills\"},\"name\":\"parameters - 2\",\"id\":\"a3898d6c-4920-4197-96ae-c00c3a6642ef\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEAuditLogsData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where action in~ ({event_type})\\n| where actor_name in~ ({actor_name})\\n| where created_at {time}\\n| summarize arg_max(created_at, *) by created_at\\n| project-away _ResourceId, created_at1\\n| order by TimeGenerated desc\\n| project [\\\"Created At\\\"] = created_at, \\n [\\\"Ingestion Time\\\"] = IngestionTime,\\n Id = id,\\n [\\\"Actor Id\\\"] = actor_id,\\n [\\\"Actor Name\\\"] = actor_name,\\n [\\\"Action\\\"] = action,\\n [\\\"Fields\\\"] = fields,\\n [\\\"Request Id\\\"] = request_id,\\n [\\\"Source IP Address\\\"] = source_ip_address,\\n [\\\"Status\\\"] = status,\\n [\\\"Commit Id\\\"] = commit_id,\\n [\\\"Tenant URL\\\"] = tenant_url,\\n [\\\"Tenant ID\\\"] = TenantId,\\n [\\\"Type\\\"] = Type\\n\\n\\n\\n\\n\",\"size\":3,\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"gridSettings\":{\"sortBy\":[{\"itemKey\":\"TimeGenerated\",\"sortOrder\":2}]},\"sortBy\":[{\"itemKey\":\"TimeGenerated\",\"sortOrder\":2}]},\"name\":\"query - 2\",\"id\":\"1f14cdc2-d692-4f28-ba58-7e920cfd104b\"}],\"isLocked\":false,\"fromTemplateId\":\"sentinel-UserWorkbook\"}\r\n", "version": "1.0", "sourceId": "[variables('workspaceResourceId')]", "category": "sentinel" @@ -1138,7 +1138,7 @@ }, "properties": { "displayName": "[parameters('workbook4-name')]", - "serializedData": "{\"version\":\"Notebook/1.0\",\"items\":[{\"type\":9,\"content\":{\"version\":\"KqlParameterItem/1.0\",\"parameters\":[{\"id\":\"87db674f-0507-4481-a57b-0bf202e968b2\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"bhe_tenant\",\"label\":\"BHE Tenant\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHETierZeroAssetsData_CL \\n| where isnotempty(tenant_url)\\n| summarize arg_max(TimeGenerated, *) by tenant_url\\n| extend display_name = replace_string(tenant_url, @\\\"https://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"http://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"/\\\", \\\"\\\") \\n| project tenant_url, display_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"selectAllValue\":\"\",\"showDefault\":false},\"timeContext\":{\"durationMs\":86400000},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"]},{\"id\":\"a4cadb58-5551-4279-9a60-3b4e46b3ddf9\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"domain_name\",\"label\":\"Environment\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHETierZeroAssetsData_CL \\n| where isnotempty(domain_name)\\n| where tenant_url in~ ({bhe_tenant})\\n| distinct domain_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":2592000000},\"defaultValue\":\"value::all\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"]},{\"id\":\"98c37fb8-534d-4661-b8cf-2ce60a74b67a\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"kind_type\",\"label\":\"Type\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHETierZeroAssetsData_CL \\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| where isnotempty(kindType)\\n| distinct kindType\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":2592000000},\"defaultValue\":\"value::all\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"]}],\"style\":\"pills\"},\"name\":\"parameters - 2\",\"id\":\"070dd276-740f-4feb-939f-3a03b1a19679\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHETierZeroAssetsData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| where kindType in~ ({kind_type})\\n| summarize arg_max(TimeGenerated, *) by objectId, owner_objectid\\n| project Name = name,\\n Environment = domain_name,\\n Type = kindType,\\n [\\\"Object Id\\\"] = objectId\",\"size\":0,\"title\":\"Tier Zero Asset List\",\"timeContext\":{\"durationMs\":604800000},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"gridSettings\":{\"rowLimit\":10000,\"filter\":true}},\"name\":\"query - 2\",\"id\":\"45e1e2fb-b6e1-44f8-b2dd-695e2b11258a\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"\\nBHETierZeroAssetsData_CL\\n| where tenant_url in~ ({bhe_tenant}) \\n| where domain_name in~ ({domain_name})\\n| where kindType in~ ({kind_type})\\n| where isnotempty(objectId)\\n| extend domain_name = toupper(domain_name)\\n| summarize arg_max(TimeGenerated, *) by objectId // dedup objectid, keep latest\\n| summarize Count = count() by kindType, domain_name, tenant_url\\n| project [\\\"kind\\\"] = kindType, domain_name, Count, tenant_url\\n| order by Count desc\\n\\n\",\"size\":0,\"title\":\"Tier Zero Asset Distribution\",\"timeContext\":{\"durationMs\":604800000},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"barchart\",\"tileSettings\":{\"showBorder\":false,\"titleContent\":{\"columnMatch\":\"kind\",\"formatter\":1},\"leftContent\":{\"columnMatch\":\"Count\",\"formatter\":12,\"formatOptions\":{\"palette\":\"auto\"},\"numberFormat\":{\"unit\":17,\"options\":{\"maximumSignificantDigits\":3,\"maximumFractionDigits\":2}}}},\"graphSettings\":{\"type\":0,\"topContent\":{\"columnMatch\":\"kind\",\"formatter\":1},\"centerContent\":{\"columnMatch\":\"Count\",\"formatter\":1,\"numberFormat\":{\"unit\":17,\"options\":{\"maximumSignificantDigits\":3,\"maximumFractionDigits\":2}}},\"rightContent\":{\"columnMatch\":\"domain_name\"},\"bottomContent\":{\"columnMatch\":\"tenant_url\"},\"nodeIdField\":\"kind\",\"sourceIdField\":\"domain_name\",\"targetIdField\":\"Count\",\"graphOrientation\":3,\"showOrientationToggles\":false,\"staticNodeSize\":100,\"hivesMargin\":5},\"chartSettings\":{\"xAxis\":\"domain_name\",\"group\":\"kind\",\"showLegend\":true,\"xSettings\":{\"label\":\"Kind Type\"},\"ySettings\":{\"label\":\"Count\"}},\"mapSettings\":{\"locInfo\":\"LatLong\",\"sizeSettings\":\"Count\",\"sizeAggregation\":\"Sum\",\"legendMetric\":\"Count\",\"legendAggregation\":\"Sum\",\"itemColorSettings\":{\"type\":\"heatmap\",\"colorAggregation\":\"Sum\",\"nodeColorField\":\"Count\",\"heatmapPalette\":\"greenRed\"}}},\"name\":\"query - 4\",\"id\":\"1768910c-ab25-4c1d-bb79-51167f9efd1b\"}],\"fromTemplateId\":\"sentinel-UserWorkbook\",\"$schema\":\"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"}\r\n", + "serializedData": "{\"version\":\"Notebook/1.0\",\"items\":[{\"type\":9,\"content\":{\"version\":\"KqlParameterItem/1.0\",\"parameters\":[{\"id\":\"87db674f-0507-4481-a57b-0bf202e968b2\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"bhe_tenant\",\"label\":\"BHE Tenant\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHETierZeroAssetsData_CL \\n| where isnotempty(tenant_url)\\n| summarize arg_max(TimeGenerated, *) by tenant_url\\n| extend display_name = replace_string(tenant_url, @\\\"https://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"http://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"/\\\", \\\"\\\") \\n| project tenant_url, display_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"selectAllValue\":\"\",\"showDefault\":false},\"timeContext\":{\"durationMs\":604800000},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"],\"key\":\"87db674f-0507-4481-a57b-0bf202e968b2\"},{\"id\":\"a4cadb58-5551-4279-9a60-3b4e46b3ddf9\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"domain_name\",\"label\":\"Environment\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHETierZeroAssetsData_CL \\n| where isnotempty(domain_name)\\n| where tenant_url in~ ({bhe_tenant})\\n| distinct domain_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":604800000},\"defaultValue\":\"value::all\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"],\"key\":\"a4cadb58-5551-4279-9a60-3b4e46b3ddf9\"},{\"id\":\"98c37fb8-534d-4661-b8cf-2ce60a74b67a\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"kind_type\",\"label\":\"Type\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHETierZeroAssetsData_CL \\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| where isnotempty(kindType)\\n| distinct kindType\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":604800000},\"defaultValue\":\"value::all\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"],\"key\":\"98c37fb8-534d-4661-b8cf-2ce60a74b67a\"}],\"style\":\"pills\"},\"name\":\"parameters - 2\",\"id\":\"070dd276-740f-4feb-939f-3a03b1a19679\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHETierZeroAssetsData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| where kindType in~ ({kind_type})\\n| summarize arg_max(TimeGenerated, *) by objectId, owner_objectid\\n| project Name = name,\\n Environment = domain_name,\\n Type = kindType,\\n [\\\"Object Id\\\"] = objectId\",\"size\":0,\"title\":\"Tier Zero Asset List\",\"timeContext\":{\"durationMs\":604800000},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"gridSettings\":{\"rowLimit\":10000,\"filter\":true}},\"name\":\"query - 2\",\"id\":\"45e1e2fb-b6e1-44f8-b2dd-695e2b11258a\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"\\nBHETierZeroAssetsData_CL\\n| where tenant_url in~ ({bhe_tenant}) \\n| where domain_name in~ ({domain_name})\\n| where kindType in~ ({kind_type})\\n| where isnotempty(objectId)\\n| extend domain_name = toupper(domain_name)\\n| summarize arg_max(TimeGenerated, *) by objectId // dedup objectid, keep latest\\n| summarize Count = count() by kindType, domain_name, tenant_url\\n| project [\\\"kind\\\"] = kindType, domain_name, Count, tenant_url\\n| order by Count desc\\n\\n\",\"size\":0,\"title\":\"Tier Zero Asset Distribution\",\"timeContext\":{\"durationMs\":604800000},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"barchart\",\"tileSettings\":{\"showBorder\":false,\"titleContent\":{\"columnMatch\":\"kind\",\"formatter\":1},\"leftContent\":{\"columnMatch\":\"Count\",\"formatter\":12,\"formatOptions\":{\"palette\":\"auto\"},\"numberFormat\":{\"unit\":17,\"options\":{\"maximumSignificantDigits\":3,\"maximumFractionDigits\":2}}}},\"graphSettings\":{\"type\":0,\"topContent\":{\"columnMatch\":\"kind\",\"formatter\":1},\"centerContent\":{\"columnMatch\":\"Count\",\"formatter\":1,\"numberFormat\":{\"unit\":17,\"options\":{\"maximumSignificantDigits\":3,\"maximumFractionDigits\":2}}},\"rightContent\":{\"columnMatch\":\"domain_name\"},\"bottomContent\":{\"columnMatch\":\"tenant_url\"},\"nodeIdField\":\"kind\",\"sourceIdField\":\"domain_name\",\"targetIdField\":\"Count\",\"graphOrientation\":3,\"showOrientationToggles\":false,\"staticNodeSize\":100,\"hivesMargin\":5},\"chartSettings\":{\"xAxis\":\"domain_name\",\"group\":\"kind\",\"showLegend\":true,\"xSettings\":{\"label\":\"Kind Type\"},\"ySettings\":{\"label\":\"Count\"}},\"mapSettings\":{\"locInfo\":\"LatLong\",\"sizeSettings\":\"Count\",\"sizeAggregation\":\"Sum\",\"legendMetric\":\"Count\",\"legendAggregation\":\"Sum\",\"itemColorSettings\":{\"type\":\"heatmap\",\"colorAggregation\":\"Sum\",\"nodeColorField\":\"Count\",\"heatmapPalette\":\"greenRed\"}}},\"name\":\"query - 4\",\"id\":\"1768910c-ab25-4c1d-bb79-51167f9efd1b\"}],\"fromTemplateId\":\"sentinel-UserWorkbook\",\"$schema\":\"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"}\r\n", "version": "1.0", "sourceId": "[variables('workspaceResourceId')]", "category": "sentinel" @@ -1226,7 +1226,7 @@ }, "properties": { "displayName": "[parameters('workbook5-name')]", - "serializedData": "{\"version\":\"Notebook/1.0\",\"items\":[{\"type\":9,\"content\":{\"version\":\"KqlParameterItem/1.0\",\"parameters\":[{\"id\":\"b5affbc4-b123-4844-bab6-a352911dcd05\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"bhe_tenant\",\"label\":\"BHE Tenant\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEFindingTrendsData_CL \\n| where isnotempty(tenant_url)\\n| summarize arg_max(TimeGenerated, *) by tenant_url\\n| extend display_name = replace_string(tenant_url, @\\\"https://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"http://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"/\\\", \\\"\\\") \\n| project tenant_url, display_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":259200000},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"],\"key\":\"b5affbc4-b123-4844-bab6-a352911dcd05\"},{\"id\":\"511679f9-e193-414d-9d06-c778e0372a69\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"domain_name\",\"label\":\"Environment\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEFindingTrendsData_CL\\n| where isnotempty(domain_name)\\n| where tenant_url in~ ({bhe_tenant})\\n| distinct domain_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":259200000},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"],\"key\":\"511679f9-e193-414d-9d06-c778e0372a69\"},{\"id\":\"56999a28-3611-436b-8b96-98bd48de863a\",\"key\":\"56999a28-3611-436b-8b96-98bd48de863a\",\"name\":\"category\",\"label\":\"Finding Category\",\"value\":[\"value::all\"],\"type\":2,\"isRequired\":true,\"version\":\"\",\"multiSelect\":true,\"queryType\":0,\"query\":\"BHEFindingTrendsData_CL\\r\\n| where isnotempty(domain_name)\\r\\n| where tenant_url in~ ({bhe_tenant})\\r\\n| where domain_name in~ ({domain_name})\\r\\n| where period == \\\"{period}\\\"\\r\\n| extend display_type = replace_string(display_type, \\\" Attack Paths\\\", \\\"\\\")\\r\\n| distinct display_type\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"selectAllValue\":\"\"},\"timeContext\":{\"durationMs\":259200000},\"defaultValue\":\"value::all\",\"quote\":\"'\",\"delimiter\":\",\"},{\"id\":\"bfad7f56-9fd7-4cc1-9386-5be8422b9eeb\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"period\",\"label\":\"Time Period\",\"type\":2,\"isRequired\":true,\"query\":\"BHEFindingTrendsData_CL\\r\\n| where tenant_url in~ ({bhe_tenant})\\r\\n| where domain_name in~ ({domain_name})\\r\\n| distinct period\\r\\n\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::1\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":86400000},\"defaultValue\":\"value::1\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"key\":\"bfad7f56-9fd7-4cc1-9386-5be8422b9eeb\",\"value\":\"value::1\"}],\"style\":\"pills\",\"title\":\"\"},\"name\":\"parameters - 2\",\"id\":\"e49a6a11-fecf-4e83-9833-3a152812fbe8\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEFindingTrendsData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| where period == \\\"{period}\\\"\\n| extend display_type = replace_string(display_type, \\\" Attack Paths\\\", \\\"\\\")\\n| summarize arg_max(TimeGenerated, *) by display_title, finding\\n| where display_type in~ ({category})\\n| extend count = finding_count_end\\n| extend change = finding_count_increase - finding_count_decrease\\n| project \\n [\\\"Name\\\"] = display_title,\\n [\\\"Finding Category\\\"] = display_type,\\n [\\\"Count\\\"] = count,\\n [\\\"Initial Findings\\\"] = finding_count_start,\\n [\\\"New Findings\\\"] = finding_count_increase,\\n [\\\"Resolved Findings\\\"] = finding_count_decrease,\\n [\\\"Change\\\"] = change,\\n [\\\"Period\\\"] = period\\n\\n\\n\",\"size\":0,\"title\":\"Attack Path Trends\",\"noDataMessageStyle\":2,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"tileSettings\":{\"showBorder\":false},\"graphSettings\":{\"type\":0},\"chartSettings\":{\"createOtherGroup\":20,\"showLegend\":true},\"mapSettings\":{\"locInfo\":\"LatLong\",\"sizeSettings\":\"TotalImpactCount\",\"sizeAggregation\":\"Sum\",\"legendMetric\":\"TotalImpactCount\",\"legendAggregation\":\"Sum\",\"itemColorSettings\":{\"type\":\"heatmap\",\"colorAggregation\":\"Sum\",\"nodeColorField\":\"TotalImpactCount\",\"heatmapPalette\":\"greenRed\"}},\"timeContext\":{\"durationMs\":259200000}},\"name\":\"query - 3 - Copy\",\"id\":\"e56e8666-1f73-4ca4-8237-49a077fd32b0\"}],\"fromTemplateId\":\"sentinel-UserWorkbook\",\"$schema\":\"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"}\r\n", + "serializedData": "{\"version\":\"Notebook/1.0\",\"items\":[{\"type\":9,\"content\":{\"version\":\"KqlParameterItem/1.0\",\"parameters\":[{\"id\":\"b5affbc4-b123-4844-bab6-a352911dcd05\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"bhe_tenant\",\"label\":\"BHE Tenant\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEFindingTrendsData_CL \\n| where isnotempty(tenant_url)\\n| summarize arg_max(TimeGenerated, *) by tenant_url\\n| extend display_name = replace_string(tenant_url, @\\\"https://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"http://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"/\\\", \\\"\\\") \\n| project tenant_url, display_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":604800000},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"],\"key\":\"b5affbc4-b123-4844-bab6-a352911dcd05\"},{\"id\":\"511679f9-e193-414d-9d06-c778e0372a69\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"domain_name\",\"label\":\"Environment\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEFindingTrendsData_CL\\n| where isnotempty(domain_name)\\n| where tenant_url in~ ({bhe_tenant})\\n| distinct domain_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":604800000},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"],\"key\":\"511679f9-e193-414d-9d06-c778e0372a69\"},{\"id\":\"56999a28-3611-436b-8b96-98bd48de863a\",\"key\":\"56999a28-3611-436b-8b96-98bd48de863a\",\"name\":\"category\",\"label\":\"Finding Category\",\"value\":[\"value::all\"],\"type\":2,\"isRequired\":true,\"version\":\"\",\"multiSelect\":true,\"queryType\":0,\"query\":\"BHEFindingTrendsData_CL\\r\\n| where isnotempty(domain_name)\\r\\n| where tenant_url in~ ({bhe_tenant})\\r\\n| where domain_name in~ ({domain_name})\\r\\n| where period == \\\"{period}\\\"\\r\\n| extend display_type = replace_string(display_type, \\\" Attack Paths\\\", \\\"\\\")\\r\\n| distinct display_type\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"selectAllValue\":\"\"},\"timeContext\":{\"durationMs\":604800000},\"defaultValue\":\"value::all\",\"quote\":\"'\",\"delimiter\":\",\"},{\"id\":\"bfad7f56-9fd7-4cc1-9386-5be8422b9eeb\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"period\",\"label\":\"Time Period\",\"type\":2,\"isRequired\":true,\"query\":\"BHEFindingTrendsData_CL\\r\\n| where tenant_url in~ ({bhe_tenant})\\r\\n| where domain_name in~ ({domain_name})\\r\\n| distinct period\\r\\n\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::1\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":604800000},\"defaultValue\":\"value::1\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"key\":\"bfad7f56-9fd7-4cc1-9386-5be8422b9eeb\",\"value\":\"value::1\"}],\"style\":\"pills\",\"title\":\"\"},\"name\":\"parameters - 2\",\"id\":\"e49a6a11-fecf-4e83-9833-3a152812fbe8\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEFindingTrendsData_CL\\n| where tenant_url in~ ({bhe_tenant})\\n| where domain_name in~ ({domain_name})\\n| where period == \\\"{period}\\\"\\n| extend display_type = replace_string(display_type, \\\" Attack Paths\\\", \\\"\\\")\\n| summarize arg_max(TimeGenerated, *) by display_title, finding\\n| where display_type in~ ({category})\\n| extend count = finding_count_end\\n| extend change = finding_count_increase - finding_count_decrease\\n| project \\n [\\\"Name\\\"] = display_title,\\n [\\\"Finding Category\\\"] = display_type,\\n [\\\"Count\\\"] = count,\\n [\\\"Initial Findings\\\"] = finding_count_start,\\n [\\\"New Findings\\\"] = finding_count_increase,\\n [\\\"Resolved Findings\\\"] = finding_count_decrease,\\n [\\\"Change\\\"] = change,\\n [\\\"Period\\\"] = period\\n\\n\\n\",\"size\":0,\"title\":\"Attack Path Trends\",\"noDataMessageStyle\":2,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"table\",\"tileSettings\":{\"showBorder\":false},\"graphSettings\":{\"type\":0},\"chartSettings\":{\"createOtherGroup\":20,\"showLegend\":true},\"mapSettings\":{\"locInfo\":\"LatLong\",\"sizeSettings\":\"TotalImpactCount\",\"sizeAggregation\":\"Sum\",\"legendMetric\":\"TotalImpactCount\",\"legendAggregation\":\"Sum\",\"itemColorSettings\":{\"type\":\"heatmap\",\"colorAggregation\":\"Sum\",\"nodeColorField\":\"TotalImpactCount\",\"heatmapPalette\":\"greenRed\"}},\"timeContext\":{\"durationMs\":604800000}},\"name\":\"query - 3 - Copy\",\"id\":\"e56e8666-1f73-4ca4-8237-49a077fd32b0\"}],\"fromTemplateId\":\"sentinel-UserWorkbook\",\"$schema\":\"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"}\r\n", "version": "1.0", "sourceId": "[variables('workspaceResourceId')]", "category": "sentinel" @@ -1314,7 +1314,7 @@ }, "properties": { "displayName": "[parameters('workbook6-name')]", - "serializedData": "{\"version\":\"Notebook/1.0\",\"items\":[{\"type\":9,\"content\":{\"version\":\"KqlParameterItem/1.0\",\"parameters\":[{\"id\":\"a4422263-5b38-4e1b-96bf-010956675fe7\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"bhe_tenant\",\"label\":\"BHE Tenant\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEPostureHistoryData_CL\\n| where isnotempty(tenant_url)\\n| summarize arg_max(TimeGenerated, *) by tenant_url\\n| extend display_name = replace_string(tenant_url, @\\\"https://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"http://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"/\\\", \\\"\\\") \\n| project tenant_url, display_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":259200000},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"],\"key\":\"a4422263-5b38-4e1b-96bf-010956675fe7\"},{\"id\":\"627c2e6a-326c-479e-a0aa-9a5c0354c719\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"domain_name\",\"label\":\"Environment\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEPostureHistoryData_CL\\n| where isnotempty(domain_name)\\n| where tenant_url in~ ({bhe_tenant})\\n| distinct domain_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"timeContext\":{\"durationMs\":259200000},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"],\"key\":\"627c2e6a-326c-479e-a0aa-9a5c0354c719\"},{\"id\":\"2318e21e-11ac-4959-8c09-8e2f0cce91c7\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"time\",\"label\":\"Time Range Picker\",\"type\":4,\"isRequired\":true,\"typeSettings\":{\"selectableValues\":[{\"durationMs\":300000},{\"durationMs\":900000},{\"durationMs\":1800000},{\"durationMs\":3600000},{\"durationMs\":14400000},{\"durationMs\":43200000},{\"durationMs\":86400000},{\"durationMs\":172800000},{\"durationMs\":259200000},{\"durationMs\":604800000},{\"durationMs\":1209600000},{\"durationMs\":2419200000},{\"durationMs\":2592000000},{\"durationMs\":5184000000},{\"durationMs\":7776000000}]},\"value\":{\"durationMs\":604800000},\"key\":\"2318e21e-11ac-4959-8c09-8e2f0cce91c7\"}],\"style\":\"pills\"},\"name\":\"parameters - 2\",\"id\":\"99f208c9-917d-4b7e-ac81-a3a6cb561707\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEPostureHistoryData_CL\\n| where data_type == \\\"exposure\\\"\\n| where domain_name in~ ({domain_name}) and tenant_url in~ ({bhe_tenant})\\n| extend metric_ts = todatetime(metric_date)\\n| summarize avg_exposure = avg(todouble(value)) by bin(metric_ts, 1d), domain_name, tenant_url\\n| extend avg_exposure = round(avg_exposure, 3) * 100\\n| order by metric_ts asc\\n\",\"size\":0,\"title\":\"Exposure Percentage\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"areachart\",\"chartSettings\":{\"showLegend\":true}},\"name\":\"query - 3\",\"id\":\"85e6b990-8ee4-4ce2-9e97-a9b9958cafb5\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEPostureHistoryData_CL\\n| where data_type == \\\"findings\\\"\\n| where domain_name in~ ({domain_name}) and tenant_url in~ ({bhe_tenant})\\n| extend metric_ts = todatetime(metric_date)\\n| summarize avg_findings = avg(todouble(value)) by bin(metric_ts, 1d), domain_name, tenant_url\\n| extend avg_findings = round(avg_findings, 3)\\n| order by metric_ts asc\\n\",\"size\":0,\"aggregation\":2,\"title\":\"Findings Trend Over Time\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"areachart\",\"chartSettings\":{\"showLegend\":true,\"group\":\"domain_name\"}},\"name\":\"Findings Trend Over Time\",\"id\":\"2b821f70-01e3-4962-be20-860c50905ab8\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEPostureHistoryData_CL\\r\\n| where data_type == \\\"attack-paths\\\"\\r\\n| where domain_name in~ ({domain_name}) and tenant_url in~ ({bhe_tenant})\\r\\n| extend metric_ts = todatetime(metric_date)\\r\\n| summarize avg_findings = avg(todouble(value)) by bin(metric_ts, 1d), domain_name, tenant_url\\r\\n| extend avg_findings = round(avg_findings, 3)\\r\\n| order by metric_ts asc\\r\\n\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"areachart\",\"title\":\"Total Attack Paths\",\"timeContextFromParameter\":\"time\"},\"name\":\"query - 4\",\"id\":\"2a11a65c-1e6a-46b3-918a-84748f809ff0\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEPostureHistoryData_CL\\n| where data_type == \\\"assets\\\"\\n| where domain_name in~ ({domain_name}) and tenant_url in~ ({bhe_tenant})\\n| extend metric_ts = todatetime(metric_date)\\n| summarize avg_assets = avg(todouble(value)) by bin(metric_ts, 1d), domain_name, tenant_url\\n| extend avg_assets = round(avg_assets, 3)\\n| order by metric_ts asc\\n\",\"size\":0,\"aggregation\":2,\"title\":\"Assets Trend Over Time\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"areachart\",\"chartSettings\":{\"showLegend\":true}},\"name\":\"query - 4\",\"id\":\"94d835c2-c01c-4473-bc85-9e2de3d5d58e\"}],\"fromTemplateId\":\"sentinel-UserWorkbook\",\"$schema\":\"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"}\r\n", + "serializedData": "{\"version\":\"Notebook/1.0\",\"items\":[{\"type\":9,\"content\":{\"version\":\"KqlParameterItem/1.0\",\"parameters\":[{\"id\":\"a4422263-5b38-4e1b-96bf-010956675fe7\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"bhe_tenant\",\"label\":\"BHE Tenant\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEPostureHistoryData_CL\\n| where isnotempty(tenant_url)\\n| summarize arg_max(TimeGenerated, *) by tenant_url\\n| extend display_name = replace_string(tenant_url, @\\\"https://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"http://\\\", \\\"\\\")\\n| extend display_name = replace_string(display_name, @\\\"/\\\", \\\"\\\") \\n| project tenant_url, display_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"],\"key\":\"a4422263-5b38-4e1b-96bf-010956675fe7\",\"timeContextFromParameter\":\"time\"},{\"id\":\"627c2e6a-326c-479e-a0aa-9a5c0354c719\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"domain_name\",\"label\":\"Environment\",\"type\":2,\"isRequired\":true,\"multiSelect\":true,\"quote\":\"'\",\"delimiter\":\",\",\"query\":\"BHEPostureHistoryData_CL\\n| where isnotempty(domain_name)\\n| where tenant_url in~ ({bhe_tenant})\\n| distinct domain_name\",\"typeSettings\":{\"additionalResourceOptions\":[\"value::all\"],\"showDefault\":false},\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"value\":[\"value::all\"],\"key\":\"627c2e6a-326c-479e-a0aa-9a5c0354c719\",\"timeContextFromParameter\":\"time\"},{\"id\":\"2318e21e-11ac-4959-8c09-8e2f0cce91c7\",\"version\":\"KqlParameterItem/1.0\",\"name\":\"time\",\"label\":\"Time Range Picker\",\"type\":4,\"isRequired\":true,\"typeSettings\":{\"selectableValues\":[{\"durationMs\":300000},{\"durationMs\":900000},{\"durationMs\":1800000},{\"durationMs\":3600000},{\"durationMs\":14400000},{\"durationMs\":43200000},{\"durationMs\":86400000},{\"durationMs\":172800000},{\"durationMs\":259200000},{\"durationMs\":604800000},{\"durationMs\":1209600000},{\"durationMs\":2419200000},{\"durationMs\":2592000000},{\"durationMs\":5184000000},{\"durationMs\":7776000000}]},\"value\":{\"durationMs\":604800000},\"key\":\"2318e21e-11ac-4959-8c09-8e2f0cce91c7\"}],\"style\":\"pills\"},\"name\":\"parameters - 2\",\"id\":\"99f208c9-917d-4b7e-ac81-a3a6cb561707\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEPostureHistoryData_CL\\n| where data_type == \\\"exposure\\\"\\n| where domain_name in~ ({domain_name}) and tenant_url in~ ({bhe_tenant})\\n| where metric_date {time}\\n| extend metric_ts = todatetime(metric_date)\\n| summarize avg_exposure = avg(todouble(value)) by bin(metric_ts, 1d), domain_name, tenant_url\\n| extend avg_exposure = round(avg_exposure, 3) * 100\\n| order by metric_ts asc\\n\",\"size\":0,\"title\":\"Exposure Percentage\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"areachart\",\"chartSettings\":{\"showLegend\":true}},\"name\":\"query - 3\",\"id\":\"85e6b990-8ee4-4ce2-9e97-a9b9958cafb5\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEPostureHistoryData_CL\\n| where data_type == \\\"findings\\\"\\n| where domain_name in~ ({domain_name}) and tenant_url in~ ({bhe_tenant})\\n| where metric_date {time}\\n| extend metric_ts = todatetime(metric_date)\\n| summarize avg_findings = avg(todouble(value)) by bin(metric_ts, 1d), domain_name, tenant_url\\n| extend avg_findings = round(avg_findings, 3)\\n| order by metric_ts asc\\n\",\"size\":0,\"aggregation\":2,\"title\":\"Findings Trend Over Time\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"areachart\",\"chartSettings\":{\"showLegend\":true,\"group\":\"domain_name\"}},\"name\":\"Findings Trend Over Time\",\"id\":\"2b821f70-01e3-4962-be20-860c50905ab8\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEPostureHistoryData_CL\\r\\n| where data_type == \\\"attack-paths\\\"\\r\\n| where domain_name in~ ({domain_name}) and tenant_url in~ ({bhe_tenant})\\r\\n| where metric_date {time}\\r\\n| extend metric_ts = todatetime(metric_date)\\r\\n| summarize avg_findings = avg(todouble(value)) by bin(metric_ts, 1d), domain_name, tenant_url\\r\\n| extend avg_findings = round(avg_findings, 3)\\r\\n| order by metric_ts asc\\r\\n\",\"size\":0,\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"areachart\",\"title\":\"Total Attack Paths\",\"timeContextFromParameter\":\"time\"},\"name\":\"query - 4\",\"id\":\"2a11a65c-1e6a-46b3-918a-84748f809ff0\"},{\"type\":3,\"content\":{\"version\":\"KqlItem/1.0\",\"query\":\"BHEPostureHistoryData_CL\\n| where data_type == \\\"assets\\\"\\n| where domain_name in~ ({domain_name}) and tenant_url in~ ({bhe_tenant})\\n| where metric_date {time}\\n| extend metric_ts = todatetime(metric_date)\\n| summarize avg_assets = avg(todouble(value)) by bin(metric_ts, 1d), domain_name, tenant_url\\n| extend avg_assets = round(avg_assets, 3)\\n| order by metric_ts asc\\n\",\"size\":0,\"aggregation\":2,\"title\":\"Assets Trend Over Time\",\"timeContextFromParameter\":\"time\",\"queryType\":0,\"resourceType\":\"microsoft.operationalinsights/workspaces\",\"visualization\":\"areachart\",\"chartSettings\":{\"showLegend\":true}},\"name\":\"query - 4\",\"id\":\"94d835c2-c01c-4473-bc85-9e2de3d5d58e\"}],\"fromTemplateId\":\"sentinel-UserWorkbook\",\"$schema\":\"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"}\r\n", "version": "1.0", "sourceId": "[variables('workspaceResourceId')]", "category": "sentinel" @@ -1412,21 +1412,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -1514,21 +1514,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -1616,21 +1616,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -1718,21 +1718,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -1820,21 +1820,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -1922,21 +1922,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -2024,21 +2024,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -2126,21 +2126,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -2228,21 +2228,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -2330,21 +2330,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -2432,21 +2432,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -2534,21 +2534,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -2636,21 +2636,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -2738,21 +2738,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -2840,21 +2840,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -2942,21 +2942,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -3044,21 +3044,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -3146,21 +3146,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -3248,21 +3248,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -3350,21 +3350,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -3452,21 +3452,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -3554,21 +3554,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -3656,21 +3656,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -3758,21 +3758,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -3860,21 +3860,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -3962,21 +3962,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -4064,21 +4064,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -4166,21 +4166,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -4268,21 +4268,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -4370,21 +4370,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -4472,21 +4472,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -4574,21 +4574,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -4676,21 +4676,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -4778,21 +4778,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -4880,21 +4880,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -4982,21 +4982,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -5084,21 +5084,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -5186,21 +5186,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -5288,21 +5288,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -5390,21 +5390,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -5492,21 +5492,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -5594,21 +5594,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -5696,21 +5696,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -5798,21 +5798,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -5900,21 +5900,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -6002,21 +6002,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -6104,21 +6104,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -6206,21 +6206,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -6308,21 +6308,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -6410,21 +6410,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -6512,21 +6512,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -6614,21 +6614,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -6716,21 +6716,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -6818,21 +6818,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -6920,21 +6920,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -7022,21 +7022,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -7124,21 +7124,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -7226,21 +7226,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -7328,21 +7328,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -7430,21 +7430,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -7532,21 +7532,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -7634,21 +7634,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -7736,21 +7736,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -7838,21 +7838,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -7940,21 +7940,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -8042,21 +8042,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -8144,21 +8144,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -8246,21 +8246,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -8348,21 +8348,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -8450,21 +8450,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -8552,21 +8552,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -8654,21 +8654,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -8756,21 +8756,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -8858,21 +8858,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -8960,21 +8960,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -9062,21 +9062,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -9164,21 +9164,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -9266,21 +9266,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -9368,21 +9368,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -9470,21 +9470,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -9572,21 +9572,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -9674,21 +9674,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -9776,21 +9776,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -9878,21 +9878,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -9980,21 +9980,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -10082,21 +10082,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -10184,21 +10184,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -10286,21 +10286,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -10388,21 +10388,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -10490,21 +10490,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -10592,21 +10592,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -10694,21 +10694,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -10796,21 +10796,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -10898,21 +10898,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -11000,21 +11000,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -11102,21 +11102,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -11204,21 +11204,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -11306,21 +11306,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -11408,21 +11408,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -11510,21 +11510,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -11612,21 +11612,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { @@ -11714,21 +11714,21 @@ "status": "Available", "requiredDataConnectors": [ { + "connectorId": "BloodHoundEnterprise", "dataTypes": [ "BHEAttackPathsData_CL" - ], - "connectorId": "BloodHoundEnterprise" + ] } ], "entityMappings": [ { - "entityType": "URL", "fieldMappings": [ { - "identifier": "Url", - "columnName": "domain_name" + "columnName": "domain_name", + "identifier": "Url" } - ] + ], + "entityType": "URL" } ], "customDetails": { diff --git a/Solutions/BloodHound Enterprise/Workbooks/BloodHoundEnterpriseAttackPathDetails.json b/Solutions/BloodHound Enterprise/Workbooks/BloodHoundEnterpriseAttackPathDetails.json index 6fe9894b15b..e9ff628ebbd 100644 --- a/Solutions/BloodHound Enterprise/Workbooks/BloodHoundEnterpriseAttackPathDetails.json +++ b/Solutions/BloodHound Enterprise/Workbooks/BloodHoundEnterpriseAttackPathDetails.json @@ -23,14 +23,13 @@ ], "showDefault": false }, - "timeContext": { - "durationMs": 259200000 - }, "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", "value": [ "value::all" - ] + ], + "key": "f0dfd85a-6c9c-4dab-91d7-d67cb23b1fb2", + "timeContextFromParameter": "time" }, { "id": "390213b5-e0d3-476c-99ca-89c76f417e7a", @@ -49,15 +48,14 @@ ], "showDefault": false }, - "timeContext": { - "durationMs": 259200000 - }, "defaultValue": "value::all", "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", "value": [ "value::all" - ] + ], + "key": "390213b5-e0d3-476c-99ca-89c76f417e7a", + "timeContextFromParameter": "time" }, { "id": "3d301840-15be-455d-bb48-d2ca8e3d4c2f", @@ -76,14 +74,13 @@ ], "showDefault": false }, - "timeContext": { - "durationMs": 259200000 - }, "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", "value": [ "value::all" - ] + ], + "key": "3d301840-15be-455d-bb48-d2ca8e3d4c2f", + "timeContextFromParameter": "time" }, { "id": "9e7a3119-3a53-4df7-8878-d2b56a948732", @@ -102,15 +99,14 @@ ], "showDefault": false }, - "timeContext": { - "durationMs": 259200000 - }, "defaultValue": "value::all", "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", "value": [ "value::all" - ] + ], + "key": "9e7a3119-3a53-4df7-8878-d2b56a948732", + "timeContextFromParameter": "time" }, { "id": "7e02e873-7550-447e-88aa-fc99dc923c14", @@ -170,7 +166,8 @@ }, "value": { "durationMs": 604800000 - } + }, + "key": "7e02e873-7550-447e-88aa-fc99dc923c14" } ], "style": "pills" @@ -182,7 +179,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "BHEAttackPathsData_CL\n| summarize arg_max(TimeGenerated, *) by id, domain_name\n| where tenant_url in~ ({bhe_tenant})\n| where domain_name in~ ({domain_name})\n| where Severity in~ ({severity})\n| extend CleanPathTitle = trim(\" \", replace_string(PathTitle, \"\\n\", \"\"))\n| where CleanPathTitle in~ ({finding_type})\n| extend \n NonTierZeroPrincipalDistinguishedName = tostring(parse_json(NonTierZeroPrincipalProps).distinguishedname),\n NonTierZeroPrincipalSAMAccountName = tostring(parse_json(NonTierZeroPrincipalProps).samaccountname),\n NonTierZeroPrincipalLastLogon = todatetime(unixtime_seconds_todatetime(tolong(parse_json(NonTierZeroPrincipalProps).lastlogon))),\n NonTierZeroPrincipalLastLogonTimestamp = todatetime(unixtime_seconds_todatetime(tolong(parse_json(NonTierZeroPrincipalProps).lastlogontimestamp))),\n NonTierZeroPrincipalCreated = todatetime(unixtime_seconds_todatetime(tolong(parse_json(NonTierZeroPrincipalProps).whencreated))),\n IsTierZero = case(\n tostring(parse_json(ImpactedPrincipalProps).system_tags) contains \"admin_tier_0\", true,\n false\n )\n| project \n [\"Non Tier Zero Principal\"] = NonTierZeroPrincipalName,\n [\"Non Tier Zero Principal Type\"] = NonTierZeroPrincipalKind,\n [\"Impacted Principal\"] = ImpactedPrincipalName,\n [\"Impacted Principal Type\"] = ImpactedPrincipalKind,\n [\"Finding Type\"] = PathTitle,\n [\"Finding Key\"] = Finding,\n [\"Environment (domain)\"] = domain_name,\n [\"Severity\"] = Severity,\n [\"Impact %\"] = round(todouble(ImpactPercentage), 0),\n [\"Impact Count\"] = toint(ImpactCount),\n [\"Exposure %\"] = round(todouble(ExposurePercentage), 0),\n [\"Exposure Count\"] = toint(ExposureCount),\n [\"First Seen\"] = todatetime(created_at),\n [\"Last Updated\"] = todatetime(updated_at),\n [\"Impacted Distinguished Name\"] = NonTierZeroPrincipalDistinguishedName,\n [\"SAM Account Name\"] = NonTierZeroPrincipalSAMAccountName,\n [\"Impacted ObjectID\"] = ImpactedPrincipal\n| order by [\"Last Updated\"] desc\n\n\n\n\n\n", + "query": "BHEAttackPathsData_CL\n| summarize arg_max(TimeGenerated, *) by id, domain_name\n| where tenant_url in~ ({bhe_tenant})\n| where domain_name in~ ({domain_name})\n| where Severity in~ ({severity})\n| where updated_at {time}\n| extend CleanPathTitle = trim(\" \", replace_string(PathTitle, \"\\n\", \"\"))\n| where CleanPathTitle in~ ({finding_type})\n| extend \n NonTierZeroPrincipalDistinguishedName = tostring(parse_json(NonTierZeroPrincipalProps).distinguishedname),\n NonTierZeroPrincipalSAMAccountName = tostring(parse_json(NonTierZeroPrincipalProps).samaccountname),\n NonTierZeroPrincipalLastLogon = todatetime(unixtime_seconds_todatetime(tolong(parse_json(NonTierZeroPrincipalProps).lastlogon))),\n NonTierZeroPrincipalLastLogonTimestamp = todatetime(unixtime_seconds_todatetime(tolong(parse_json(NonTierZeroPrincipalProps).lastlogontimestamp))),\n NonTierZeroPrincipalCreated = todatetime(unixtime_seconds_todatetime(tolong(parse_json(NonTierZeroPrincipalProps).whencreated))),\n IsTierZero = case(\n tostring(parse_json(ImpactedPrincipalProps).system_tags) contains \"admin_tier_0\", true,\n false\n )\n| project \n [\"Non Tier Zero Principal\"] = NonTierZeroPrincipalName,\n [\"Non Tier Zero Principal Type\"] = NonTierZeroPrincipalKind,\n [\"Impacted Principal\"] = ImpactedPrincipalName,\n [\"Impacted Principal Type\"] = ImpactedPrincipalKind,\n [\"Finding Type\"] = PathTitle,\n [\"Finding Key\"] = Finding,\n [\"Environment (domain)\"] = domain_name,\n [\"Severity\"] = Severity,\n [\"Impact %\"] = round(todouble(ImpactPercentage), 0),\n [\"Impact Count\"] = toint(ImpactCount),\n [\"Exposure %\"] = round(todouble(ExposurePercentage), 0),\n [\"Exposure Count\"] = toint(ExposureCount),\n [\"First Seen\"] = todatetime(created_at),\n [\"Last Updated\"] = todatetime(updated_at),\n [\"Impacted Distinguished Name\"] = NonTierZeroPrincipalDistinguishedName,\n [\"SAM Account Name\"] = NonTierZeroPrincipalSAMAccountName,\n [\"Impacted ObjectID\"] = ImpactedPrincipal\n| order by [\"Last Updated\"] desc\n\n\n\n\n\n", "size": 0, "title": "Principals", "timeContextFromParameter": "time", @@ -200,7 +197,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "BHEAttackPathsTimelineData_CL\n| where isnotempty(domain_name)\n| where tenant_url in~ ({bhe_tenant})\n| where domain_name in~ ({domain_name})\n| extend CleanPathTitle = trim(\" \", replace_string(path_title, \"\\n\", \"\"))\n| where CleanPathTitle in~ ({finding_type})\n| extend event_day = format_datetime(todatetime(updated_at), \"yyyy-MM-dd\")\n| summarize arg_max(updated_at, *) by event_day, tenant_url, domain_name, CleanPathTitle\n| extend _time = todatetime(event_day)\n| extend CompositeRisk = round(todouble(CompositeRisk), 2)\n| summarize LatestCompositeRisk = max(CompositeRisk) by bin(_time, 1d)\n| order by _time asc\n", + "query": "BHEAttackPathsTimelineData_CL\n| where isnotempty(domain_name)\n| where tenant_url in~ ({bhe_tenant})\n| where domain_name in~ ({domain_name})\n| where created_at {time}\n| extend CleanPathTitle = trim(\" \", replace_string(path_title, \"\\n\", \"\"))\n| where CleanPathTitle in~ ({finding_type})\n| extend event_day = format_datetime(todatetime(updated_at), \"yyyy-MM-dd\")\n| summarize arg_max(updated_at, *) by event_day, tenant_url, domain_name, CleanPathTitle\n| extend _time = todatetime(event_day)\n| extend CompositeRisk = round(todouble(CompositeRisk), 2)\n| summarize LatestCompositeRisk = max(CompositeRisk) by bin(_time, 1d)\n| order by _time asc\n", "size": 0, "aggregation": 2, "title": "Maximum Exposure Percentage", @@ -219,7 +216,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "BHEAttackPathsTimelineData_CL\n| where tenant_url in~ ({bhe_tenant})\n| where domain_name in~ ({domain_name})\n| extend CleanPathTitle = trim(\" \", replace_string(path_title, \"\\n\", \"\"))\n| where CleanPathTitle in~ ({finding_type})\n| extend _time = todatetime(updated_at)\n| where isnotnull(_time)\n| summarize arg_max(TimeGenerated, *) by Finding, domain_name, tenant_url\n| summarize SumFindingCount = sum(toint(FindingCount)) by bin(_time, 1d)\n| order by _time asc\n", + "query": "BHEAttackPathsTimelineData_CL\n| where tenant_url in~ ({bhe_tenant})\n| where domain_name in~ ({domain_name})\n| extend CleanPathTitle = trim(\" \", replace_string(path_title, \"\\n\", \"\"))\n| where created_at {time}\n| where CleanPathTitle in~ ({finding_type})\n| extend _time = todatetime(updated_at)\n| where isnotnull(_time)\n| summarize arg_max(TimeGenerated, *) by Finding, domain_name, tenant_url\n| summarize SumFindingCount = sum(toint(FindingCount)) by bin(_time, 1d)\n| order by _time asc\n", "size": 0, "title": "Total Number of Findings", "timeContextFromParameter": "time", diff --git a/Solutions/BloodHound Enterprise/Workbooks/BloodHoundEnterpriseAttackPathOverview.json b/Solutions/BloodHound Enterprise/Workbooks/BloodHoundEnterpriseAttackPathOverview.json index 19fe1fce431..0b35c28a8a1 100644 --- a/Solutions/BloodHound Enterprise/Workbooks/BloodHoundEnterpriseAttackPathOverview.json +++ b/Solutions/BloodHound Enterprise/Workbooks/BloodHoundEnterpriseAttackPathOverview.json @@ -23,14 +23,13 @@ ], "showDefault": false }, - "timeContext": { - "durationMs": 259200000 - }, "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", "value": [ "value::all" - ] + ], + "key": "6de26de7-0eec-4312-b568-9fbbaf5c7f71", + "timeContextFromParameter": "time" }, { "id": "cb118fcd-4473-4470-ac89-28c6ea644d5d", @@ -49,12 +48,11 @@ ], "showDefault": false }, - "timeContext": { - "durationMs": 259200000 - }, "defaultValue": "value::all", "queryType": 0, - "resourceType": "microsoft.operationalinsights/workspaces" + "resourceType": "microsoft.operationalinsights/workspaces", + "key": "cb118fcd-4473-4470-ac89-28c6ea644d5d", + "timeContextFromParameter": "time" }, { "id": "9e7a3119-3a53-4df7-8878-d2b56a948732", @@ -73,15 +71,14 @@ ], "showDefault": false }, - "timeContext": { - "durationMs": 259200000 - }, "defaultValue": "value::all", "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", "value": [ "value::all" - ] + ], + "key": "9e7a3119-3a53-4df7-8878-d2b56a948732", + "timeContextFromParameter": "time" }, { "id": "dafad90f-2d00-41cd-9463-efbe87d3888f", @@ -141,7 +138,8 @@ }, "value": { "durationMs": 604800000 - } + }, + "key": "dafad90f-2d00-41cd-9463-efbe87d3888f" } ], "style": "pills" @@ -153,7 +151,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "BHEAttackPathsData_CL\n| where tenant_url in~ ({bhe_tenant})\n| where domain_name in~ ({domain_name})\n| where Severity in~ ({severity})\n| summarize arg_max(TimeGenerated, *) by id, domain_name, tenant_url\n| summarize TotalAttackPathsFindings = dcount(id) by DomainName = domain_name\n| sort by TotalAttackPathsFindings desc", + "query": "BHEAttackPathsData_CL\n| where tenant_url in~ ({bhe_tenant})\n| where domain_name in~ ({domain_name})\n| where Severity in~ ({severity})\n| where updated_at {time}\n| summarize arg_max(TimeGenerated, *) by id, domain_name, tenant_url\n| summarize TotalAttackPathsFindings = dcount(id) by DomainName = domain_name\n| sort by TotalAttackPathsFindings desc", "size": 1, "title": "Total Attack Paths Findings per Domain", "timeContextFromParameter": "time", @@ -189,7 +187,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "BHEAttackPathsData_CL\n| where tenant_url in~ ({bhe_tenant})\n| where domain_name in~ ({domain_name})\n| where Severity in~ ({severity})\n| summarize arg_max(TimeGenerated, *) by id, domain_name, tenant_url\n| summarize Count = dcount(id) by Severity\n| sort by Count desc\n", + "query": "BHEAttackPathsData_CL\n| where tenant_url in~ ({bhe_tenant})\n| where domain_name in~ ({domain_name})\n| where Severity in~ ({severity})\n| where updated_at {time}\n| summarize arg_max(TimeGenerated, *) by id, domain_name, tenant_url\n| summarize Count = dcount(id) by Severity\n| sort by Count desc\n", "size": 0, "title": "Severity Breakdown", "timeContextFromParameter": "time", @@ -204,7 +202,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "BHEAttackPathsData_CL\n| where tenant_url in~ ({bhe_tenant})\n| where domain_name in~ ({domain_name})\n| where Severity in~ ({severity})\n| summarize arg_max(TimeGenerated, *) by id\n| where isnotempty(NonTierZeroPrincipalName)\n| summarize FindingsCount = count() by NonTierZeroPrincipalName, domain_name\n| project-rename Environment = domain_name\n| sort by FindingsCount desc\n| take 5\n", + "query": "BHEAttackPathsData_CL\n| where tenant_url in~ ({bhe_tenant})\n| where domain_name in~ ({domain_name})\n| where Severity in~ ({severity})\n| where updated_at {time}\n| summarize arg_max(TimeGenerated, *) by id\n| where isnotempty(NonTierZeroPrincipalName)\n| summarize FindingsCount = count() by NonTierZeroPrincipalName, domain_name\n| project-rename Environment = domain_name\n| sort by FindingsCount desc\n| take 5\n", "size": 1, "aggregation": 2, "title": "Top 5 Non-Tier Zero Principals Involved in Findings", @@ -227,7 +225,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "BHEAttackPathsData_CL\n| where tenant_url in~ ({bhe_tenant})\n| where domain_name in~ ({domain_name})\n| where Severity in~ ({severity})\n| summarize arg_max(TimeGenerated, *) by id, domain_name, tenant_url\n| summarize Frequency = dcount(id) by Finding\n| project-rename [\"Finding Key\"] = Finding\n| sort by Frequency desc\n| take 5\n", + "query": "BHEAttackPathsData_CL\n| where tenant_url in~ ({bhe_tenant})\n| where domain_name in~ ({domain_name})\n| where Severity in~ ({severity})\n| where updated_at {time}\n| summarize arg_max(TimeGenerated, *) by id, domain_name, tenant_url\n| summarize Frequency = dcount(id) by Finding\n| project-rename [\"Finding Key\"] = Finding\n| sort by Frequency desc\n| take 5\n", "size": 1, "title": "Top 5 Most Common Findings (Finding Keys)", "timeContextFromParameter": "time", @@ -256,7 +254,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "BHEAttackPathsData_CL\n| where tenant_url in~ ({bhe_tenant})\n| where domain_name in~ ({domain_name})\n| where Severity in~ ({severity})\n| partition by domain_name\n(\n summarize arg_max(TimeGenerated, *) by id, domain_name, tenant_url\n| summarize Frequency = dcount(id) by domain_name, Finding\n| sort by Frequency desc\n| take 5\n)\n| sort by Frequency desc", + "query": "BHEAttackPathsData_CL\n| where tenant_url in~ ({bhe_tenant})\n| where domain_name in~ ({domain_name})\n| where Severity in~ ({severity})\n| where updated_at {time}\n| partition by domain_name\n(\n summarize arg_max(TimeGenerated, *) by id, domain_name, tenant_url\n| summarize Frequency = dcount(id) by domain_name, Finding\n| sort by Frequency desc\n| take 5\n)\n| sort by Frequency desc", "size": 0, "title": "Top 5 Most Common Findings (Finding Keys) per Environment", "timeContextFromParameter": "time", @@ -298,7 +296,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "BHEAttackPathsData_CL\n| where tenant_url in~ ({bhe_tenant})\n| where domain_name in~ ({domain_name})\n| where Severity in~ ({severity})\n| summarize arg_max(TimeGenerated, *) by id, domain_name, tenant_url\n| extend exposure_val = toreal(ExposurePercentage),\n impact_val = toreal(ImpactPercentage)\n| extend exposure_pct = iif(isnull(exposure_val), round(impact_val, 2), round(exposure_val, 2))\n| extend impact_pct = round(impact_val, 2)\n| summarize \n ExposurePercent = max(exposure_pct),\n ImpactPercent = max(impact_pct),\n Count = count()\n by Environment = domain_name, AttackPath = PathTitle, Severity\n| project Environment, AttackPath, Severity, Count, ['Exposure (%)'] = ExposurePercent, ['Impact (%)'] = ImpactPercent\n| sort by ['Exposure (%)'] desc\n", + "query": "BHEAttackPathsData_CL\n| where tenant_url in~ ({bhe_tenant})\n| where domain_name in~ ({domain_name})\n| where Severity in~ ({severity})\n| where updated_at {time}\n| summarize arg_max(TimeGenerated, *) by id, domain_name, tenant_url\n| extend exposure_val = toreal(ExposurePercentage),\n impact_val = toreal(ImpactPercentage)\n| extend exposure_pct = iif(isnull(exposure_val), round(impact_val, 2), round(exposure_val, 2))\n| extend impact_pct = round(impact_val, 2)\n| summarize \n ExposurePercent = max(exposure_pct),\n ImpactPercent = max(impact_pct),\n Count = count()\n by Environment = domain_name, AttackPath = PathTitle, Severity\n| project Environment, AttackPath, Severity, Count, ['Exposure (%)'] = ExposurePercent, ['Impact (%)'] = ImpactPercent\n| sort by ['Exposure (%)'] desc\n", "size": 0, "title": "All Attack Paths List", "timeContextFromParameter": "time", diff --git a/Solutions/BloodHound Enterprise/Workbooks/BloodHoundEnterpriseAuditLogs.json b/Solutions/BloodHound Enterprise/Workbooks/BloodHoundEnterpriseAuditLogs.json index 824d3d2ad87..2dee5e93639 100644 --- a/Solutions/BloodHound Enterprise/Workbooks/BloodHoundEnterpriseAuditLogs.json +++ b/Solutions/BloodHound Enterprise/Workbooks/BloodHoundEnterpriseAuditLogs.json @@ -23,14 +23,13 @@ ], "showDefault": false }, - "timeContext": { - "durationMs": 259200000 - }, "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", "value": [ "value::all" - ] + ], + "key": "b8d3205b-b903-4db7-8c7a-f82bacd94242", + "timeContextFromParameter": "time" }, { "id": "752a2195-64fc-402e-b80f-c7c4fb9b49bd", @@ -49,15 +48,14 @@ ], "showDefault": false }, - "timeContext": { - "durationMs": 259200000 - }, "defaultValue": "value::all", "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", "value": [ "value::all" - ] + ], + "key": "752a2195-64fc-402e-b80f-c7c4fb9b49bd", + "timeContextFromParameter": "time" }, { "id": "5de91703-b999-47c1-98e3-648bb4724abf", @@ -76,14 +74,13 @@ ], "showDefault": false }, - "timeContext": { - "durationMs": 259200000 - }, "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", "value": [ "value::all" - ] + ], + "key": "5de91703-b999-47c1-98e3-648bb4724abf", + "timeContextFromParameter": "time" }, { "id": "705413e9-2968-4485-975f-a782991db8df", @@ -143,7 +140,8 @@ }, "value": { "durationMs": 604800000 - } + }, + "key": "705413e9-2968-4485-975f-a782991db8df" } ], "style": "pills" @@ -155,7 +153,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "BHEAuditLogsData_CL\n| where tenant_url in~ ({bhe_tenant})\n| where action in~ ({event_type})\n| where actor_name in~ ({actor_name})\n| summarize arg_max(created_at, *) by created_at\n| project-away _ResourceId, created_at1\n| order by TimeGenerated desc\n| project [\"Created At\"] = created_at, \n TimeGenerated = TimeGenerated,\n [\"Ingestion Time\"] = IngestionTime,\n Id = id,\n [\"Actor Id\"] = actor_id,\n [\"Actor Name\"] = actor_name,\n [\"Action\"] = action,\n [\"Fields\"] = fields,\n [\"Request Id\"] = request_id,\n [\"Source IP Address\"] = source_ip_address,\n [\"Status\"] = status,\n [\"Commit Id\"] = commit_id,\n [\"Tenant URL\"] = tenant_url,\n [\"Tenant ID\"] = TenantId,\n [\"Type\"] = Type\n\n\n\n\n", + "query": "BHEAuditLogsData_CL\n| where tenant_url in~ ({bhe_tenant})\n| where action in~ ({event_type})\n| where actor_name in~ ({actor_name})\n| where created_at {time}\n| summarize arg_max(created_at, *) by created_at\n| project-away _ResourceId, created_at1\n| order by TimeGenerated desc\n| project [\"Created At\"] = created_at, \n [\"Ingestion Time\"] = IngestionTime,\n Id = id,\n [\"Actor Id\"] = actor_id,\n [\"Actor Name\"] = actor_name,\n [\"Action\"] = action,\n [\"Fields\"] = fields,\n [\"Request Id\"] = request_id,\n [\"Source IP Address\"] = source_ip_address,\n [\"Status\"] = status,\n [\"Commit Id\"] = commit_id,\n [\"Tenant URL\"] = tenant_url,\n [\"Tenant ID\"] = TenantId,\n [\"Type\"] = Type\n\n\n\n\n", "size": 3, "timeContextFromParameter": "time", "queryType": 0, diff --git a/Solutions/BloodHound Enterprise/Workbooks/BloodHoundEnterpriseTierZeroSearch.json b/Solutions/BloodHound Enterprise/Workbooks/BloodHoundEnterpriseTierZeroSearch.json index 9a3b05c3d45..6f003d97fdb 100644 --- a/Solutions/BloodHound Enterprise/Workbooks/BloodHoundEnterpriseTierZeroSearch.json +++ b/Solutions/BloodHound Enterprise/Workbooks/BloodHoundEnterpriseTierZeroSearch.json @@ -25,13 +25,14 @@ "showDefault": false }, "timeContext": { - "durationMs": 86400000 + "durationMs": 604800000 }, "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", "value": [ "value::all" - ] + ], + "key": "87db674f-0507-4481-a57b-0bf202e968b2" }, { "id": "a4cadb58-5551-4279-9a60-3b4e46b3ddf9", @@ -51,14 +52,15 @@ "showDefault": false }, "timeContext": { - "durationMs": 2592000000 + "durationMs": 604800000 }, "defaultValue": "value::all", "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", "value": [ "value::all" - ] + ], + "key": "a4cadb58-5551-4279-9a60-3b4e46b3ddf9" }, { "id": "98c37fb8-534d-4661-b8cf-2ce60a74b67a", @@ -78,14 +80,15 @@ "showDefault": false }, "timeContext": { - "durationMs": 2592000000 + "durationMs": 604800000 }, "defaultValue": "value::all", "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", "value": [ "value::all" - ] + ], + "key": "98c37fb8-534d-4661-b8cf-2ce60a74b67a" } ], "style": "pills" diff --git a/Solutions/BloodHound Enterprise/Workbooks/BloodHoundFindingTrends.json b/Solutions/BloodHound Enterprise/Workbooks/BloodHoundFindingTrends.json index 76482dc5963..91be3d2c9ce 100644 --- a/Solutions/BloodHound Enterprise/Workbooks/BloodHoundFindingTrends.json +++ b/Solutions/BloodHound Enterprise/Workbooks/BloodHoundFindingTrends.json @@ -24,7 +24,7 @@ "showDefault": false }, "timeContext": { - "durationMs": 259200000 + "durationMs": 604800000 }, "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", @@ -51,7 +51,7 @@ "showDefault": false }, "timeContext": { - "durationMs": 259200000 + "durationMs": 604800000 }, "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", @@ -81,7 +81,7 @@ "selectAllValue": "" }, "timeContext": { - "durationMs": 259200000 + "durationMs": 604800000 }, "defaultValue": "value::all", "quote": "'", @@ -102,7 +102,7 @@ "showDefault": false }, "timeContext": { - "durationMs": 86400000 + "durationMs": 604800000 }, "defaultValue": "value::1", "queryType": 0, @@ -152,7 +152,7 @@ } }, "timeContext": { - "durationMs": 259200000 + "durationMs": 604800000 } }, "name": "query - 3 - Copy", diff --git a/Solutions/BloodHound Enterprise/Workbooks/BloodHoundPostureHistory.json b/Solutions/BloodHound Enterprise/Workbooks/BloodHoundPostureHistory.json index 5146339f7d0..b76b509ef63 100644 --- a/Solutions/BloodHound Enterprise/Workbooks/BloodHoundPostureHistory.json +++ b/Solutions/BloodHound Enterprise/Workbooks/BloodHoundPostureHistory.json @@ -23,15 +23,13 @@ ], "showDefault": false }, - "timeContext": { - "durationMs": 259200000 - }, "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", "value": [ "value::all" ], - "key": "a4422263-5b38-4e1b-96bf-010956675fe7" + "key": "a4422263-5b38-4e1b-96bf-010956675fe7", + "timeContextFromParameter": "time" }, { "id": "627c2e6a-326c-479e-a0aa-9a5c0354c719", @@ -50,15 +48,13 @@ ], "showDefault": false }, - "timeContext": { - "durationMs": 259200000 - }, "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", "value": [ "value::all" ], - "key": "627c2e6a-326c-479e-a0aa-9a5c0354c719" + "key": "627c2e6a-326c-479e-a0aa-9a5c0354c719", + "timeContextFromParameter": "time" }, { "id": "2318e21e-11ac-4959-8c09-8e2f0cce91c7", @@ -131,7 +127,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "BHEPostureHistoryData_CL\n| where data_type == \"exposure\"\n| where domain_name in~ ({domain_name}) and tenant_url in~ ({bhe_tenant})\n| extend metric_ts = todatetime(metric_date)\n| summarize avg_exposure = avg(todouble(value)) by bin(metric_ts, 1d), domain_name, tenant_url\n| extend avg_exposure = round(avg_exposure, 3) * 100\n| order by metric_ts asc\n", + "query": "BHEPostureHistoryData_CL\n| where data_type == \"exposure\"\n| where domain_name in~ ({domain_name}) and tenant_url in~ ({bhe_tenant})\n| where metric_date {time}\n| extend metric_ts = todatetime(metric_date)\n| summarize avg_exposure = avg(todouble(value)) by bin(metric_ts, 1d), domain_name, tenant_url\n| extend avg_exposure = round(avg_exposure, 3) * 100\n| order by metric_ts asc\n", "size": 0, "title": "Exposure Percentage", "timeContextFromParameter": "time", @@ -149,7 +145,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "BHEPostureHistoryData_CL\n| where data_type == \"findings\"\n| where domain_name in~ ({domain_name}) and tenant_url in~ ({bhe_tenant})\n| extend metric_ts = todatetime(metric_date)\n| summarize avg_findings = avg(todouble(value)) by bin(metric_ts, 1d), domain_name, tenant_url\n| extend avg_findings = round(avg_findings, 3)\n| order by metric_ts asc\n", + "query": "BHEPostureHistoryData_CL\n| where data_type == \"findings\"\n| where domain_name in~ ({domain_name}) and tenant_url in~ ({bhe_tenant})\n| where metric_date {time}\n| extend metric_ts = todatetime(metric_date)\n| summarize avg_findings = avg(todouble(value)) by bin(metric_ts, 1d), domain_name, tenant_url\n| extend avg_findings = round(avg_findings, 3)\n| order by metric_ts asc\n", "size": 0, "aggregation": 2, "title": "Findings Trend Over Time", @@ -169,7 +165,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "BHEPostureHistoryData_CL\r\n| where data_type == \"attack-paths\"\r\n| where domain_name in~ ({domain_name}) and tenant_url in~ ({bhe_tenant})\r\n| extend metric_ts = todatetime(metric_date)\r\n| summarize avg_findings = avg(todouble(value)) by bin(metric_ts, 1d), domain_name, tenant_url\r\n| extend avg_findings = round(avg_findings, 3)\r\n| order by metric_ts asc\r\n", + "query": "BHEPostureHistoryData_CL\r\n| where data_type == \"attack-paths\"\r\n| where domain_name in~ ({domain_name}) and tenant_url in~ ({bhe_tenant})\r\n| where metric_date {time}\r\n| extend metric_ts = todatetime(metric_date)\r\n| summarize avg_findings = avg(todouble(value)) by bin(metric_ts, 1d), domain_name, tenant_url\r\n| extend avg_findings = round(avg_findings, 3)\r\n| order by metric_ts asc\r\n", "size": 0, "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", @@ -184,7 +180,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "BHEPostureHistoryData_CL\n| where data_type == \"assets\"\n| where domain_name in~ ({domain_name}) and tenant_url in~ ({bhe_tenant})\n| extend metric_ts = todatetime(metric_date)\n| summarize avg_assets = avg(todouble(value)) by bin(metric_ts, 1d), domain_name, tenant_url\n| extend avg_assets = round(avg_assets, 3)\n| order by metric_ts asc\n", + "query": "BHEPostureHistoryData_CL\n| where data_type == \"assets\"\n| where domain_name in~ ({domain_name}) and tenant_url in~ ({bhe_tenant})\n| where metric_date {time}\n| extend metric_ts = todatetime(metric_date)\n| summarize avg_assets = avg(todouble(value)) by bin(metric_ts, 1d), domain_name, tenant_url\n| extend avg_assets = round(avg_assets, 3)\n| order by metric_ts asc\n", "size": 0, "aggregation": 2, "title": "Assets Trend Over Time",