From d86a6e07d7043f51ecd4335c7e51ce1b1b8488cc Mon Sep 17 00:00:00 2001 From: jmorascalyr <42879226+jmorascalyr@users.noreply.github.com> Date: Sat, 21 Feb 2026 07:54:56 -0700 Subject: [PATCH] feat: Add WEL alerts for Bridge and Enterprise machines with multi-machine asset correlation - Added 4 new WEL alert mappings: hidden scheduled tasks (bridge/enterprise), brute force success (enterprise), and AD admin group creation (enterprise) - Implemented target_machine field in alert mappings to specify which machine (email/bridge/enterprise) each alert correlates to - Updated alert resource UID logic to use separate XDR asset IDs for bridge (xdr_asset_id_bridge) and enterprise (xdr_asset_ --- .../templates/wel_ad_global_admin_group.json | 40 +++++ .../templates/wel_brute_force_success.json | 40 +++++ .../templates/wel_hidden_scheduled_task.json | 40 +++++ .../scenarios/apollo_ransomware_scenario.py | 146 +++++++++++++----- 4 files changed, 229 insertions(+), 37 deletions(-) create mode 100644 Backend/api/app/alerts/templates/wel_ad_global_admin_group.json create mode 100644 Backend/api/app/alerts/templates/wel_brute_force_success.json create mode 100644 Backend/api/app/alerts/templates/wel_hidden_scheduled_task.json diff --git a/Backend/api/app/alerts/templates/wel_ad_global_admin_group.json b/Backend/api/app/alerts/templates/wel_ad_global_admin_group.json new file mode 100644 index 0000000..e248e57 --- /dev/null +++ b/Backend/api/app/alerts/templates/wel_ad_global_admin_group.json @@ -0,0 +1,40 @@ +{ + "finding_info": { + "uid": "placeholder_uid", + "title": "HELIOS - WEL Active Directory Global Admin Group Created", + "desc": "Detects the creation of security-enabled global administrative groups (4727) in Active Directory. This may indicate unauthorized modifications to privileged group memberships, posing a security risk. Extended Windows Event Log collection must be enabled for this rule to work properly." + }, + "resources": [ + { + "uid": "DYNAMIC_RESOURCE_UID", + "name": "endpoint" + } + ], + "severity": "high", + "category_uid": 2, + "class_uid": 99602001, + "class_name": "S1 Security Alert", + "type_uid": 9960200101, + "type_name": "S1 Security Alert: Create", + "category_name": "Findings", + "activity_id": 1, + "metadata": { + "version": "1.1.0", + "extension": { + "name": "s1", + "uid": "998", + "version": "0.1.0" + }, + "product": { + "name": "Windows Event Logs", + "vendor_name": "Microsoft" + }, + "logged_time": "DYNAMIC", + "modified_time": "DYNAMIC" + }, + "time": "DYNAMIC", + "attack_surface_ids": [1], + "severity_id": 4, + "state_id": 1, + "s1_classification_id": 28 +} diff --git a/Backend/api/app/alerts/templates/wel_brute_force_success.json b/Backend/api/app/alerts/templates/wel_brute_force_success.json new file mode 100644 index 0000000..a8665f5 --- /dev/null +++ b/Backend/api/app/alerts/templates/wel_brute_force_success.json @@ -0,0 +1,40 @@ +{ + "finding_info": { + "uid": "placeholder_uid", + "title": "HELIOS - WEL Successful Brute Force Attack", + "desc": "Detects a successful brute force attack against a Windows endpoint. Multiple failed authentication attempts followed by a successful logon indicate credential compromise. This technique is commonly used by attackers to gain initial access or escalate privileges within a network. Extended Windows Event Log collection must be enabled for this rule to work properly." + }, + "resources": [ + { + "uid": "DYNAMIC_RESOURCE_UID", + "name": "endpoint" + } + ], + "severity": "critical", + "category_uid": 2, + "class_uid": 99602001, + "class_name": "S1 Security Alert", + "type_uid": 9960200101, + "type_name": "S1 Security Alert: Create", + "category_name": "Findings", + "activity_id": 1, + "metadata": { + "version": "1.1.0", + "extension": { + "name": "s1", + "uid": "998", + "version": "0.1.0" + }, + "product": { + "name": "Windows Event Logs", + "vendor_name": "Microsoft" + }, + "logged_time": "DYNAMIC", + "modified_time": "DYNAMIC" + }, + "time": "DYNAMIC", + "attack_surface_ids": [1], + "severity_id": 5, + "state_id": 1, + "s1_classification_id": 28 +} diff --git a/Backend/api/app/alerts/templates/wel_hidden_scheduled_task.json b/Backend/api/app/alerts/templates/wel_hidden_scheduled_task.json new file mode 100644 index 0000000..9e7f9b3 --- /dev/null +++ b/Backend/api/app/alerts/templates/wel_hidden_scheduled_task.json @@ -0,0 +1,40 @@ +{ + "finding_info": { + "uid": "placeholder_uid", + "title": "HELIOS - WEL Hidden Scheduled Task Creation", + "desc": "Detects the creation of a hidden scheduled task, a tactic often used by attackers to maintain persistence on a compromised system. Hidden tasks can evade detection by administrators and standard monitoring tools, allowing adversaries to execute malicious activities stealthily. This technique is commonly employed to run malicious scripts, download payloads, or maintain remote access without raising suspicion. Extended Windows Event Log collection must be enabled for this rule to work properly." + }, + "resources": [ + { + "uid": "DYNAMIC_RESOURCE_UID", + "name": "endpoint" + } + ], + "severity": "high", + "category_uid": 2, + "class_uid": 99602001, + "class_name": "S1 Security Alert", + "type_uid": 9960200101, + "type_name": "S1 Security Alert: Create", + "category_name": "Findings", + "activity_id": 1, + "metadata": { + "version": "1.1.0", + "extension": { + "name": "s1", + "uid": "998", + "version": "0.1.0" + }, + "product": { + "name": "Windows Event Logs", + "vendor_name": "Microsoft" + }, + "logged_time": "DYNAMIC", + "modified_time": "DYNAMIC" + }, + "time": "DYNAMIC", + "attack_surface_ids": [1], + "severity_id": 4, + "state_id": 1, + "s1_classification_id": 28 +} diff --git a/Backend/scenarios/apollo_ransomware_scenario.py b/Backend/scenarios/apollo_ransomware_scenario.py index 50d0964..3c7a8ca 100644 --- a/Backend/scenarios/apollo_ransomware_scenario.py +++ b/Backend/scenarios/apollo_ransomware_scenario.py @@ -152,26 +152,61 @@ "šŸ“¬ PHASE 2: Email Interaction": { "template": "proofpoint_email_alert", "offset_minutes": 2, # 2 min after delivery (user clicks link) + "target_machine": "email", "overrides": { - "finding_info.title": "Malicious Email Link Clicked", + "finding_info.title": "HELIOS - Malicious Email Link Clicked", "finding_info.desc": f"User {VICTIM_PROFILE['email']} clicked malicious link in phishing email from {ATTACKER_PROFILE['sender_email']}" } }, "šŸ“¤ PHASE 4: Data Exfiltration": { "template": "sharepoint_data_exfil_alert", "offset_minutes": 25, # After last document download (base+24:30) + "target_machine": "email", "overrides": { - "finding_info.title": "Data Exfiltration from SharePoint", + "finding_info.title": "HELIOS - Data Exfiltration from SharePoint", "finding_info.desc": f"User {VICTIM_PROFILE['email']} downloaded sensitive documents including Personnel Records and Command Codes" } }, "rdp_download": { "template": "o365_rdp_sharepoint_access", "offset_minutes": 35, # After RDP file download event (base+25) + "target_machine": "email", "overrides": { - "finding_info.title": "OneDrive RDP Files Downloaded", + "finding_info.title": "HELIOS - OneDrive RDP Files Downloaded", "finding_info.desc": f"User {VICTIM_PROFILE['email']} downloaded RDP files from SharePoint - potential lateral movement preparation" } + }, + "wel_hidden_schtask_bridge": { + "template": "wel_hidden_scheduled_task", + "offset_minutes": 8, # After PowerShell spawns and creates persistence task on bridge + "target_machine": "bridge", + "overrides": { + "finding_info.desc": f"Hidden scheduled task 'WindowsUpdate' created on {VICTIM_PROFILE['machine_bridge']} by {VICTIM_PROFILE['domain']}\\{VICTIM_PROFILE['username']} to execute {ATTACKER_PROFILE['malware_name']}. This persistence mechanism allows the attacker to maintain access even after system reboots." + } + }, + "wel_hidden_schtask_enterprise": { + "template": "wel_hidden_scheduled_task", + "offset_minutes": 40, # After lateral movement to Enterprise + "target_machine": "enterprise", + "overrides": { + "finding_info.desc": f"Hidden scheduled task created on {VICTIM_PROFILE['machine_enterprise']} Domain Controller by {VICTIM_PROFILE['domain']}\\{VICTIM_PROFILE['username']} to execute {ATTACKER_PROFILE['malware_name']}. Lateral movement persistence established on critical infrastructure." + } + }, + "wel_brute_force_enterprise": { + "template": "wel_brute_force_success", + "offset_minutes": 30, # After credential dump and brute force attempts + "target_machine": "enterprise", + "overrides": { + "finding_info.desc": f"Successful brute force attack detected on {VICTIM_PROFILE['machine_enterprise']}. Multiple failed logon attempts from {VICTIM_PROFILE['machine_bridge']} ({VICTIM_PROFILE['client_ip']}) followed by successful authentication using stolen credentials from Mimikatz dump." + } + }, + "wel_ad_admin_group_enterprise": { + "template": "wel_ad_global_admin_group", + "offset_minutes": 45, # After gaining access to Enterprise DC + "target_machine": "enterprise", + "overrides": { + "finding_info.desc": f"Security-enabled global admin group created on {VICTIM_PROFILE['machine_enterprise']} Domain Controller by {VICTIM_PROFILE['domain']}\\{VICTIM_PROFILE['username']}. This may indicate privilege escalation after lateral movement from {VICTIM_PROFILE['machine_bridge']}." + } } } @@ -254,13 +289,18 @@ def create_event(timestamp: str, source: str, phase: str, event_data: dict) -> D def load_alert_template(template_id: str) -> Optional[Dict]: """Load an alert template JSON from the templates directory""" - templates_dir = os.path.join(backend_dir, 'api', 'app', 'alerts', 'templates') - template_path = os.path.join(templates_dir, f"{template_id}.json") - if not os.path.exists(template_path): - print(f" āš ļø Template not found: {template_path}") - return None - with open(template_path, 'r') as f: - return json.load(f) + # Try multiple paths: local dev layout and Docker container layout + candidate_dirs = [ + os.path.join(backend_dir, 'api', 'app', 'alerts', 'templates'), # local dev + os.path.join(backend_dir, 'app', 'alerts', 'templates'), # Docker (/app/app/alerts/templates) + ] + for templates_dir in candidate_dirs: + template_path = os.path.join(templates_dir, f"{template_id}.json") + if os.path.exists(template_path): + with open(template_path, 'r') as f: + return json.load(f) + print(f" āš ļø Template not found: {template_id}.json (searched {candidate_dirs})") + return None def send_phase_alert( @@ -301,19 +341,32 @@ def send_phase_alert( alert["metadata"]["logged_time"] = time_ms alert["metadata"]["modified_time"] = time_ms - # Set resource - use XDR Asset ID if available for linking to real endpoint - xdr_asset_id = uam_config.get('xdr_asset_id') - if xdr_asset_id: - resource_name = uam_config.get('xdr_asset_name', VICTIM_PROFILE["email"]) - alert["resources"] = [{ - "uid": xdr_asset_id, - "name": resource_name - }] - else: + # Set resource - use XDR Asset ID for endpoint alerts, shared GUID for email/user alerts + target_machine = mapping.get("target_machine", "bridge") + if target_machine == "email": + # Proofpoint/M365 alerts link to the user email with a consistent GUID + email_asset_uid = uam_config.get('email_asset_uid') + if not email_asset_uid: + email_asset_uid = str(uuid.uuid5(uuid.NAMESPACE_DNS, VICTIM_PROFILE["email"])) + uam_config['email_asset_uid'] = email_asset_uid alert["resources"] = [{ - "uid": str(uuid.uuid4()), + "uid": email_asset_uid, "name": VICTIM_PROFILE["email"] }] + elif target_machine == "enterprise": + xdr_asset_id = uam_config.get('xdr_asset_id_enterprise') + xdr_asset_name = uam_config.get('xdr_asset_name_enterprise', VICTIM_PROFILE["machine_enterprise"]) + if xdr_asset_id: + alert["resources"] = [{"uid": xdr_asset_id, "name": xdr_asset_name}] + else: + alert["resources"] = [{"uid": str(uuid.uuid4()), "name": VICTIM_PROFILE["machine_enterprise"]}] + else: + xdr_asset_id = uam_config.get('xdr_asset_id_bridge') + xdr_asset_name = uam_config.get('xdr_asset_name_bridge', VICTIM_PROFILE["machine_bridge"]) + if xdr_asset_id: + alert["resources"] = [{"uid": xdr_asset_id, "name": xdr_asset_name}] + else: + alert["resources"] = [{"uid": str(uuid.uuid4()), "name": VICTIM_PROFILE["machine_bridge"]}] # Apply overrides overrides = mapping.get("overrides", {}) @@ -682,16 +735,16 @@ def generate_apollo_ransomware_scenario(siem_context: Optional[Dict] = None) -> 'uam_site_id': uam_site_id, } - # Look up bridge XDR Asset ID for linking alerts to real endpoint + # Look up Bridge and Enterprise XDR Asset IDs for linking alerts to real endpoints s1_mgmt_url = os.getenv('S1_MANAGEMENT_URL', '') s1_api_token = os.getenv('S1_API_TOKEN', '') if s1_mgmt_url and s1_api_token: bridge_name = VICTIM_PROFILE['machine_bridge'] - print(f"\nšŸ” Looking up XDR asset '{bridge_name}' for alert linking...") + enterprise_name = VICTIM_PROFILE['machine_enterprise'] + print(f"\nšŸ” Looking up XDR assets for alert linking...") try: import urllib.request import urllib.parse - # Use XDR assets endpoint — returns the Asset ID needed for resource UID linking params = {"accountIds": uam_account_id} if uam_site_id: params["siteIds"] = uam_site_id @@ -703,22 +756,27 @@ def generate_apollo_ransomware_scenario(siem_context: Optional[Dict] = None) -> with urllib.request.urlopen(req, timeout=15) as resp: assets_data = json.loads(resp.read().decode()) assets = assets_data.get("data", []) - # Find the real agent asset (has 'agent' field), matching by name + # Find real agent assets (have 'agent' field) for both machines for asset in assets: - if asset.get("name", "").lower() == bridge_name.lower() and asset.get("agent"): - asset_id = asset.get("id", "") - agent_name = asset.get("name", "") - agent_uuid = asset.get("agent", {}).get("uuid", "") - print(f" āœ“ XDR Asset found: {agent_name}") - print(f" Asset ID: {asset_id}") - print(f" Agent UUID: {agent_uuid}") - print(f" Category: {asset.get('category')}") - uam_config['xdr_asset_id'] = asset_id - uam_config['xdr_asset_name'] = agent_name - break - else: + if not asset.get("agent"): + continue + name = asset.get("name", "").lower() + asset_id = asset.get("id", "") + agent_uuid = asset.get("agent", {}).get("uuid", "") + if name == bridge_name.lower(): + print(f" āœ“ Bridge asset: {asset.get('name')} → {asset_id}") + uam_config['xdr_asset_id_bridge'] = asset_id + uam_config['xdr_asset_name_bridge'] = asset.get("name", "") + elif name == enterprise_name.lower(): + print(f" āœ“ Enterprise asset: {asset.get('name')} → {asset_id}") + uam_config['xdr_asset_id_enterprise'] = asset_id + uam_config['xdr_asset_name_enterprise'] = asset.get("name", "") + + if not uam_config.get('xdr_asset_id_bridge'): print(f" ⚠ No XDR agent asset found for '{bridge_name}'") - print(f" Found {len(assets)} total assets") + if not uam_config.get('xdr_asset_id_enterprise'): + print(f" ⚠ No XDR agent asset found for '{enterprise_name}'") + print(f" Scanned {len(assets)} total assets") except Exception as e: print(f" ⚠ XDR asset lookup failed: {e}") @@ -776,6 +834,20 @@ def generate_apollo_ransomware_scenario(siem_context: Optional[Dict] = None) -> success = send_phase_alert("rdp_download", phase_base_time, uam_config) print(f"{'āœ“' if success else 'āœ—'}") + # Send standalone WEL alerts (not tied to a specific event generation phase) + if alerts_enabled: + print(f"\nšŸ”” SENDING WEL ALERTS") + wel_alerts = [ + ("wel_hidden_schtask_bridge", "Hidden Scheduled Task → Bridge"), + ("wel_hidden_schtask_enterprise", "Hidden Scheduled Task → Enterprise"), + ("wel_brute_force_enterprise", "Brute Force Success → Enterprise"), + ("wel_ad_admin_group_enterprise", "AD Global Admin Group → Enterprise"), + ] + for alert_key, alert_desc in wel_alerts: + print(f" šŸ“¤ {alert_desc}...", end=" ") + success = send_phase_alert(alert_key, base_time, uam_config) + print(f"{'āœ“' if success else 'āœ—'}") + all_events.sort(key=lambda x: x["timestamp"]) scenario = {