From eccd71a51aa4be9d5d42511c5db50e6e1931257b Mon Sep 17 00:00:00 2001 From: jmorascalyr <42879226+jmorascalyr@users.noreply.github.com> Date: Fri, 20 Feb 2026 06:23:06 -0700 Subject: [PATCH 1/8] feat: Add trace ID support and M365 parser improvements for correlation scenarios (#70) - Added CorrelationRunRequest model with trace_id, tag_phase, and tag_trace fields for correlation scenario execution - Implemented /correlation/run endpoint to execute scenarios with SIEM context and trace ID tagging - Updated start_correlation_scenario() and _execute_correlation_scenario() to accept and pass trace_id via S1_TRACE_ID environment variable - Added tag_phase and tag_trace boolean flags with S1_TAG --- Backend/api/app/routers/scenarios.py | 62 +++++++++++++++++++ Backend/api/app/services/scenario_service.py | 44 +++++++++++-- .../microsoft_365_collaboration.json | 2 +- .../scenarios/apollo_ransomware_scenario.py | 23 ++++--- .../microsoft_365_collaboration.conf | 16 ++++- .../microsoft_365_collaboration.conf | 2 +- 6 files changed, 131 insertions(+), 18 deletions(-) diff --git a/Backend/api/app/routers/scenarios.py b/Backend/api/app/routers/scenarios.py index eed6dd3..8489a41 100644 --- a/Backend/api/app/routers/scenarios.py +++ b/Backend/api/app/routers/scenarios.py @@ -6,6 +6,7 @@ import asyncio import time import json +import sys from datetime import datetime, timedelta from pathlib import Path as PathLib @@ -29,6 +30,17 @@ class SIEMQueryRequest(BaseModel): end_time_hours: int = 0 # How far back to end (default now) anchor_configs: Optional[List[Dict[str, Any]]] = None + +class CorrelationRunRequest(BaseModel): + """Request model for correlation scenario execution""" + scenario_id: str + destination_id: str + siem_context: Optional[Dict[str, Any]] = None + trace_id: Optional[str] = None + tag_phase: bool = True + tag_trace: bool = True + workers: int = 10 + # Initialize scenario service scenario_service = ScenarioService() @@ -278,6 +290,56 @@ async def execute_siem_query( raise HTTPException(status_code=500, detail=str(e)) +@router.post("/correlation/run", response_model=BaseResponse) +async def run_correlation_scenario( + request: CorrelationRunRequest, + background_tasks: BackgroundTasks, + _: str = Depends(require_write_access) +): + """ + Execute a correlation scenario with SIEM context and trace ID support + """ + try: + # Validate scenario exists and supports correlation + scenarios_dir = PathLib(__file__).parent.parent.parent / "scenarios" + if str(scenarios_dir) not in sys.path: + sys.path.insert(0, str(scenarios_dir)) + + scenario_modules = { + "apollo_ransomware_scenario": "apollo_ransomware_scenario" + } + + if request.scenario_id not in scenario_modules: + raise HTTPException(status_code=404, detail=f"Correlation scenario '{request.scenario_id}' not found") + + # Start correlation scenario execution with trace ID + execution_id = await scenario_service.start_correlation_scenario( + scenario_id=request.scenario_id, + siem_context=request.siem_context or {}, + trace_id=request.trace_id, + tag_phase=request.tag_phase, + tag_trace=request.tag_trace, + background_tasks=background_tasks + ) + + return BaseResponse( + success=True, + data={ + "execution_id": execution_id, + "scenario_id": request.scenario_id, + "status": "started", + "trace_id": request.trace_id, + "tag_phase": request.tag_phase, + "tag_trace": request.tag_trace, + "started_at": datetime.utcnow().isoformat() + } + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + # ============================================================================= # GENERIC SCENARIO ENDPOINTS (catch-all /{scenario_id} routes must be last) # ============================================================================= diff --git a/Backend/api/app/services/scenario_service.py b/Backend/api/app/services/scenario_service.py index 9a40a2f..c8e3d07 100644 --- a/Backend/api/app/services/scenario_service.py +++ b/Backend/api/app/services/scenario_service.py @@ -5,12 +5,12 @@ import uuid import time import asyncio +import json from datetime import datetime import logging import os import sys import importlib -import json from pathlib import Path logger = logging.getLogger(__name__) @@ -264,11 +264,14 @@ async def start_correlation_scenario( self, scenario_id: str, siem_context: Dict[str, Any], + trace_id: Optional[str] = None, + tag_phase: bool = True, + tag_trace: bool = True, speed: str = "fast", dry_run: bool = False, background_tasks=None ) -> str: - """Start correlation scenario execution with SIEM context""" + """Start correlation scenario execution with SIEM context and trace ID support""" execution_id = str(uuid.uuid4()) self.running_scenarios[execution_id] = { @@ -279,16 +282,35 @@ async def start_correlation_scenario( "speed": speed, "dry_run": dry_run, "siem_context": siem_context, + "trace_id": trace_id, + "tag_phase": tag_phase, + "tag_trace": tag_trace, "progress": 0 } if background_tasks: - background_tasks.add_task(self._execute_correlation_scenario, execution_id, scenario_id, siem_context) + background_tasks.add_task( + self._execute_correlation_scenario, + execution_id, + scenario_id, + siem_context, + trace_id, + tag_phase, + tag_trace + ) return execution_id - async def _execute_correlation_scenario(self, execution_id: str, scenario_id: str, siem_context: Dict[str, Any]): - """Execute correlation scenario with SIEM context""" + async def _execute_correlation_scenario( + self, + execution_id: str, + scenario_id: str, + siem_context: Dict[str, Any], + trace_id: Optional[str] = None, + tag_phase: bool = True, + tag_trace: bool = True + ): + """Execute correlation scenario with SIEM context and trace ID support""" import sys import os from pathlib import Path @@ -303,6 +325,12 @@ async def _execute_correlation_scenario(self, execution_id: str, scenario_id: st siem_context_json = json.dumps(siem_context) os.environ['SIEM_CONTEXT'] = siem_context_json + # Set trace ID and tagging environment variables + if trace_id: + os.environ['S1_TRACE_ID'] = trace_id + os.environ['S1_TAG_PHASE'] = '1' if tag_phase else '0' + os.environ['S1_TAG_TRACE'] = '1' if tag_trace else '0' + # Import and run the scenario module = __import__(scenario_id) scenario_result = module.generate_apollo_ransomware_scenario(siem_context=siem_context) @@ -321,8 +349,12 @@ async def _execute_correlation_scenario(self, execution_id: str, scenario_id: st self.running_scenarios[execution_id]["error"] = str(e) self.running_scenarios[execution_id]["completed_at"] = datetime.utcnow().isoformat() finally: - # Clean up environment variable + # Clean up environment variables os.environ.pop('SIEM_CONTEXT', None) + if trace_id: + os.environ.pop('S1_TRACE_ID', None) + os.environ.pop('S1_TAG_PHASE', None) + os.environ.pop('S1_TAG_TRACE', None) async def _execute_scenario(self, execution_id: str, scenario: Dict[str, Any]): """Execute scenario in background""" diff --git a/Backend/parsers/community/microsoft_365_collaboration-latest/microsoft_365_collaboration.json b/Backend/parsers/community/microsoft_365_collaboration-latest/microsoft_365_collaboration.json index 29e2a89..2d5869a 100644 --- a/Backend/parsers/community/microsoft_365_collaboration-latest/microsoft_365_collaboration.json +++ b/Backend/parsers/community/microsoft_365_collaboration-latest/microsoft_365_collaboration.json @@ -1,6 +1,6 @@ { "attributes": { - "dataSource.name": "Microsoft 365 Collaboration", + "dataSource.name": "Microsoft O365", "dataSource.vendor": "Microsoft", "dataSource.category": "security", "metadata.product.name": "Microsoft 365 SharePoint/OneDrive", diff --git a/Backend/scenarios/apollo_ransomware_scenario.py b/Backend/scenarios/apollo_ransomware_scenario.py index fca6fb0..fcd0f56 100644 --- a/Backend/scenarios/apollo_ransomware_scenario.py +++ b/Backend/scenarios/apollo_ransomware_scenario.py @@ -88,7 +88,8 @@ "name": "Apollo Ransomware - STARFLEET Attack", "description": "Correlates Proofpoint and M365 events with existing EDR/WEL data for the Apollo ransomware attack chain targeting STARFLEET.", - "default_query": """dataSource.name in ('SentinelOne','Windows Event Logs') endpoint.name contains ("Enterprise", "bridge") + "default_query": """dataSource.name in ('SentinelOne','Windows Event Logs') endpoint.name contains ("Enterprise", "bridge") AND (winEventLog.id = 4698 or * contains "apollo") + | group newest_timestamp = newest(timestamp), oldest_timestamp = oldest(timestamp) by event.type, src.process.user, endpoint.name, src.endpoint.ip.address, dst.ip.address | sort newest_timestamp | columns event.type, src.process.user, endpoint.name, oldest_timestamp, newest_timestamp, src.endpoint.ip.address, dst.ip.address""", @@ -450,8 +451,8 @@ def generate_m365_email_interaction(base_time: datetime) -> List[Dict]: """Generate M365 events for email access and attachment download""" events = [] - # Email accessed - 5 minutes after delivery (user checks email) - email_access_time = get_scenario_time(base_time, 5) + # Email accessed - 30 seconds after Proofpoint delivery + email_access_time = get_scenario_time(base_time, 0, 30) m365_email_access = microsoft_365_collaboration_log() m365_email_access['TimeStamp'] = email_access_time m365_email_access['UserId'] = VICTIM_PROFILE['email'] @@ -467,8 +468,8 @@ def generate_m365_email_interaction(base_time: datetime) -> List[Dict]: m365_email_access['RequestedBy'] = VICTIM_PROFILE['name'] # -> actor.user.name events.append(create_event(email_access_time, "microsoft_365_collaboration", "email_interaction", m365_email_access)) - # Attachment preview/download - 6 minutes after delivery - attachment_time = get_scenario_time(base_time, 6) + # Attachment preview/download - 30 seconds after MailItemsAccessed (1 min after delivery) + attachment_time = get_scenario_time(base_time, 1) m365_attachment = microsoft_365_collaboration_log() m365_attachment['TimeStamp'] = attachment_time m365_attachment['UserId'] = VICTIM_PROFILE['email'] @@ -483,8 +484,8 @@ def generate_m365_email_interaction(base_time: datetime) -> List[Dict]: m365_attachment['RequestedBy'] = VICTIM_PROFILE['name'] # -> actor.user.name events.append(create_event(attachment_time, "microsoft_365_collaboration", "email_interaction", m365_attachment)) - # File opened in Excel Online / locally - 7 minutes after delivery - file_open_time = get_scenario_time(base_time, 7) + # File opened in Excel Online / locally - 30 seconds after FileDownloaded (1 min 30 sec after delivery) + file_open_time = get_scenario_time(base_time, 1, 30) m365_file_open = microsoft_365_collaboration_log() m365_file_open['TimeStamp'] = file_open_time m365_file_open['UserId'] = VICTIM_PROFILE['email'] @@ -785,9 +786,15 @@ def send_to_hec(event_data: dict, event_type: str, trace_id: str = None, phase: product = type_to_product.get(event_type, event_type) + # Special handling for dataSource.name to match expected values + if event_type == "microsoft_365_collaboration": + data_source_name = "Microsoft O365" + else: + data_source_name = event_type.replace('_', ' ').title() + attr_fields = { "dataSource.vendor": event_type.split('_')[0].title() if '_' in event_type else event_type.title(), - "dataSource.name": event_type.replace('_', ' ').title(), + "dataSource.name": data_source_name, "dataSource.category": "security" } diff --git a/Backend/utilities/parsers/community_new/ai-siem-main/parsers/community/microsoft_365_collaboration-latest/microsoft_365_collaboration.conf b/Backend/utilities/parsers/community_new/ai-siem-main/parsers/community/microsoft_365_collaboration-latest/microsoft_365_collaboration.conf index 29e2a89..5158a5e 100644 --- a/Backend/utilities/parsers/community_new/ai-siem-main/parsers/community/microsoft_365_collaboration-latest/microsoft_365_collaboration.conf +++ b/Backend/utilities/parsers/community_new/ai-siem-main/parsers/community/microsoft_365_collaboration-latest/microsoft_365_collaboration.conf @@ -1,6 +1,6 @@ { "attributes": { - "dataSource.name": "Microsoft 365 Collaboration", + "dataSource.name": "Microsoft O365", "dataSource.vendor": "Microsoft", "dataSource.category": "security", "metadata.product.name": "Microsoft 365 SharePoint/OneDrive", @@ -93,11 +93,23 @@ } }, { - "rename": { + "copy": { "from": "unmapped.Operation", "to": "activity_name" } }, + { + "copy": { + "from": "unmapped.Operation", + "to": "unmapped.operation" + } + }, + { + "copy": { + "from": "unmapped.Operation", + "to": "event.type" + } + }, { "rename": { "from": "unmapped.SiteUrl", diff --git a/Backend/utilities/parsers/sentinelone_new/ai-siem-main/parsers/community/microsoft_365_collaboration-latest/microsoft_365_collaboration.conf b/Backend/utilities/parsers/sentinelone_new/ai-siem-main/parsers/community/microsoft_365_collaboration-latest/microsoft_365_collaboration.conf index 29e2a89..2d5869a 100644 --- a/Backend/utilities/parsers/sentinelone_new/ai-siem-main/parsers/community/microsoft_365_collaboration-latest/microsoft_365_collaboration.conf +++ b/Backend/utilities/parsers/sentinelone_new/ai-siem-main/parsers/community/microsoft_365_collaboration-latest/microsoft_365_collaboration.conf @@ -1,6 +1,6 @@ { "attributes": { - "dataSource.name": "Microsoft 365 Collaboration", + "dataSource.name": "Microsoft O365", "dataSource.vendor": "Microsoft", "dataSource.category": "security", "metadata.product.name": "Microsoft 365 SharePoint/OneDrive", From 22b3b253a6bb6269d11a473589730a1fc089d488 Mon Sep 17 00:00:00 2001 From: jmorascalyr <42879226+jmorascalyr@users.noreply.github.com> Date: Fri, 20 Feb 2026 06:41:09 -0700 Subject: [PATCH 2/8] feat: Add user.email_addr field to M365 collaboration events for actor email correlation - Added user.email_addr field to M365 email interaction events (MailItemsAccessed, FileDownloaded, FileAccessed) using VICTIM_PROFILE['email'] - Updated microsoft_365_collaboration parser to copy unmapped.user.email_addr to user.email_addr for OCSF actor.user.email_addr mapping - Enables correlation of M365 collaboration events with email security events via actor email address --- Backend/scenarios/apollo_ransomware_scenario.py | 3 +++ .../microsoft_365_collaboration.conf | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/Backend/scenarios/apollo_ransomware_scenario.py b/Backend/scenarios/apollo_ransomware_scenario.py index fcd0f56..fb7895b 100644 --- a/Backend/scenarios/apollo_ransomware_scenario.py +++ b/Backend/scenarios/apollo_ransomware_scenario.py @@ -466,6 +466,7 @@ def generate_m365_email_interaction(base_time: datetime) -> List[Dict]: m365_email_access['SiteUrl'] = f"https://outlook.office365.com/mail/inbox" # Parser-mapped fields for OCSF synthetic columns m365_email_access['RequestedBy'] = VICTIM_PROFILE['name'] # -> actor.user.name + m365_email_access['user.email_addr'] = VICTIM_PROFILE['email'] # -> actor.user.email_addr events.append(create_event(email_access_time, "microsoft_365_collaboration", "email_interaction", m365_email_access)) # Attachment preview/download - 30 seconds after MailItemsAccessed (1 min after delivery) @@ -482,6 +483,7 @@ def generate_m365_email_interaction(base_time: datetime) -> List[Dict]: m365_attachment['SiteUrl'] = f"https://outlook.office365.com/mail/inbox" # Parser-mapped fields for OCSF synthetic columns m365_attachment['RequestedBy'] = VICTIM_PROFILE['name'] # -> actor.user.name + m365_attachment['user.email_addr'] = VICTIM_PROFILE['email'] # -> actor.user.email_addr events.append(create_event(attachment_time, "microsoft_365_collaboration", "email_interaction", m365_attachment)) # File opened in Excel Online / locally - 30 seconds after FileDownloaded (1 min 30 sec after delivery) @@ -498,6 +500,7 @@ def generate_m365_email_interaction(base_time: datetime) -> List[Dict]: m365_file_open['SiteUrl'] = f"https://starfleet-my.sharepoint.com/personal/{VICTIM_PROFILE['username']}" # Parser-mapped fields for OCSF synthetic columns m365_file_open['RequestedBy'] = VICTIM_PROFILE['name'] # -> actor.user.name + m365_file_open['user.email_addr'] = VICTIM_PROFILE['email'] # -> actor.user.email_addr events.append(create_event(file_open_time, "microsoft_365_collaboration", "file_access", m365_file_open)) return events diff --git a/Backend/utilities/parsers/community_new/ai-siem-main/parsers/community/microsoft_365_collaboration-latest/microsoft_365_collaboration.conf b/Backend/utilities/parsers/community_new/ai-siem-main/parsers/community/microsoft_365_collaboration-latest/microsoft_365_collaboration.conf index 5158a5e..a6932cd 100644 --- a/Backend/utilities/parsers/community_new/ai-siem-main/parsers/community/microsoft_365_collaboration-latest/microsoft_365_collaboration.conf +++ b/Backend/utilities/parsers/community_new/ai-siem-main/parsers/community/microsoft_365_collaboration-latest/microsoft_365_collaboration.conf @@ -140,6 +140,12 @@ "to": "actor.user.name" } }, + { + "copy": { + "from": "unmapped.user.email_addr", + "to": "user.email_addr" + } + }, { "rename": { "from": "unmapped.Details", From 8d13ad555dac6d4cdeb5d65b1fa47c02c9788e8e Mon Sep 17 00:00:00 2001 From: jmorascalyr <42879226+jmorascalyr@users.noreply.github.com> Date: Fri, 20 Feb 2026 06:47:57 -0700 Subject: [PATCH 3/8] feat: Add object_id field to M365 email interaction events for mail items analysis - Added object_id field to MailItemsAccessed, FileDownloaded, and FileAccessed events with contextual paths (/Inbox/, /Attachments/, /Documents/) - Enables mail items analysis and tracking of malicious attachment flow through M365 collaboration events - Maps to OCSF object_id field for consistent object identification across email interaction phases --- Backend/scenarios/apollo_ransomware_scenario.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Backend/scenarios/apollo_ransomware_scenario.py b/Backend/scenarios/apollo_ransomware_scenario.py index fb7895b..cc2b46b 100644 --- a/Backend/scenarios/apollo_ransomware_scenario.py +++ b/Backend/scenarios/apollo_ransomware_scenario.py @@ -467,6 +467,7 @@ def generate_m365_email_interaction(base_time: datetime) -> List[Dict]: # Parser-mapped fields for OCSF synthetic columns m365_email_access['RequestedBy'] = VICTIM_PROFILE['name'] # -> actor.user.name m365_email_access['user.email_addr'] = VICTIM_PROFILE['email'] # -> actor.user.email_addr + m365_email_access['object_id'] = f"/Inbox/{ATTACKER_PROFILE['malicious_xlsx']}" # -> object_id for mail items analysis events.append(create_event(email_access_time, "microsoft_365_collaboration", "email_interaction", m365_email_access)) # Attachment preview/download - 30 seconds after MailItemsAccessed (1 min after delivery) @@ -484,6 +485,7 @@ def generate_m365_email_interaction(base_time: datetime) -> List[Dict]: # Parser-mapped fields for OCSF synthetic columns m365_attachment['RequestedBy'] = VICTIM_PROFILE['name'] # -> actor.user.name m365_attachment['user.email_addr'] = VICTIM_PROFILE['email'] # -> actor.user.email_addr + m365_attachment['object_id'] = f"/Attachments/{ATTACKER_PROFILE['malicious_xlsx']}" # -> object_id for mail items analysis events.append(create_event(attachment_time, "microsoft_365_collaboration", "email_interaction", m365_attachment)) # File opened in Excel Online / locally - 30 seconds after FileDownloaded (1 min 30 sec after delivery) @@ -501,6 +503,7 @@ def generate_m365_email_interaction(base_time: datetime) -> List[Dict]: # Parser-mapped fields for OCSF synthetic columns m365_file_open['RequestedBy'] = VICTIM_PROFILE['name'] # -> actor.user.name m365_file_open['user.email_addr'] = VICTIM_PROFILE['email'] # -> actor.user.email_addr + m365_file_open['object_id'] = f"/Documents/{ATTACKER_PROFILE['malicious_xlsx']}" # -> object_id for mail items analysis events.append(create_event(file_open_time, "microsoft_365_collaboration", "file_access", m365_file_open)) return events From d309967c56c3e8ea1934bb1fffd9a195cf9469aa Mon Sep 17 00:00:00 2001 From: jmorascalyr <42879226+jmorascalyr@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:38:11 -0700 Subject: [PATCH 4/8] feat: Add overwrite_parser flag to enable parser updates across all execution paths - Added overwrite_parser boolean field to GeneratorExecuteRequest, ScenarioExecuteRequest, CorrelationRunRequest, ParserSyncRequest, and SingleParserSyncRequest models - Updated parser sync service to skip existence check when overwrite=True, allowing parser updates instead of skipping existing parsers - Added JARVIS_OVERWRITE_PARSER environment variable support in hec_sender for generator-level parser overwrite control --- Backend/api/app/models/requests.py | 2 + Backend/api/app/routers/generators.py | 3 +- Backend/api/app/routers/parser_sync.py | 6 +- Backend/api/app/routers/scenarios.py | 4 + Backend/api/app/services/generator_service.py | 3 +- .../api/app/services/parser_sync_service.py | 28 ++++-- Backend/api/app/services/scenario_service.py | 26 ++++-- Backend/event_generators/shared/hec_sender.py | 2 + Frontend/log_generator_ui.py | 92 ++++++++++++++++++- Frontend/templates/log_generator.html | 36 +++++++- 10 files changed, 179 insertions(+), 23 deletions(-) diff --git a/Backend/api/app/models/requests.py b/Backend/api/app/models/requests.py index 2d379a7..0f07f2a 100644 --- a/Backend/api/app/models/requests.py +++ b/Backend/api/app/models/requests.py @@ -14,6 +14,7 @@ class GeneratorExecuteRequest(BaseModel): continuous: bool = Field(default=False, description="Run indefinitely (ignores count)") eps: Optional[float] = Field(None, ge=0.1, le=10000, description="Events per second rate") speed_mode: bool = Field(False, description="Pre-generate 1K events and loop for max throughput (auto-enabled for EPS > 1000)") + overwrite_parser: bool = Field(False, description="Overwrite existing parsers during execution") options: Dict[str, Any] = Field(default_factory=dict, description="Generator-specific options") @validator('count') @@ -52,6 +53,7 @@ class ScenarioExecuteRequest(BaseModel): """Scenario execution request""" speed: str = Field("fast", pattern="^(realtime|fast|instant)$") dry_run: bool = Field(False) + overwrite_parser: bool = Field(False, description="Overwrite existing parsers during execution") class Config: extra = "forbid" diff --git a/Backend/api/app/routers/generators.py b/Backend/api/app/routers/generators.py index 8f575f1..8f0e155 100644 --- a/Backend/api/app/routers/generators.py +++ b/Backend/api/app/routers/generators.py @@ -190,7 +190,8 @@ async def execute_generator( count=request.count, format=request.format, star_trek_theme=request.star_trek_theme, - options=request.options + options=request.options, + overwrite_parser=request.overwrite_parser ) execution_time = (time.time() - start_time) * 1000 # Convert to ms diff --git a/Backend/api/app/routers/parser_sync.py b/Backend/api/app/routers/parser_sync.py index d68c8a3..7cb2ba5 100644 --- a/Backend/api/app/routers/parser_sync.py +++ b/Backend/api/app/routers/parser_sync.py @@ -22,6 +22,7 @@ class ParserSyncRequest(BaseModel): github_repo_urls: Optional[List[str]] = Field(None, description="Optional GitHub repository URLs to fetch parsers from") github_token: Optional[str] = Field(None, description="Optional GitHub token for private repositories") selected_parsers: Optional[Dict[str, Dict]] = Field(None, description="Optional user-selected parsers for similar name resolution") + overwrite_parser: bool = Field(False, description="If true, overwrite existing parsers instead of skipping them") class ParserSyncResponse(BaseModel): @@ -77,7 +78,8 @@ async def sync_parsers( config_write_token=request.config_write_token, github_repo_urls=request.github_repo_urls, github_token=request.github_token, - selected_parsers=request.selected_parsers + selected_parsers=request.selected_parsers, + overwrite=request.overwrite_parser ) # Count results (include uploaded_from_github in uploaded count) @@ -107,6 +109,7 @@ class SingleParserSyncRequest(BaseModel): config_write_token: str = Field(..., description="Config API token for reading and writing parsers") github_repo_urls: Optional[List[str]] = Field(None, description="Optional GitHub repository URLs to fetch parsers from") github_token: Optional[str] = Field(None, description="Optional GitHub token for private repositories") + overwrite_parser: bool = Field(False, description="If true, overwrite existing parser instead of skipping") class SingleParserSyncResponse(BaseModel): @@ -141,6 +144,7 @@ async def sync_single_parser( config_write_token=request.config_write_token, github_repo_urls=request.github_repo_urls, github_token=request.github_token, + overwrite=request.overwrite_parser, ) status = result.get("status", "no_parser") message = result.get("message", "Unknown status") diff --git a/Backend/api/app/routers/scenarios.py b/Backend/api/app/routers/scenarios.py index 8489a41..c0d118c 100644 --- a/Backend/api/app/routers/scenarios.py +++ b/Backend/api/app/routers/scenarios.py @@ -40,6 +40,7 @@ class CorrelationRunRequest(BaseModel): tag_phase: bool = True tag_trace: bool = True workers: int = 10 + overwrite_parser: bool = False # Initialize scenario service scenario_service = ScenarioService() @@ -319,6 +320,7 @@ async def run_correlation_scenario( trace_id=request.trace_id, tag_phase=request.tag_phase, tag_trace=request.tag_trace, + overwrite_parser=request.overwrite_parser, background_tasks=background_tasks ) @@ -371,6 +373,7 @@ async def execute_scenario( scenario_id: str = Path(..., description="Scenario identifier"), speed: str = Query("fast", description="Execution speed: realtime, fast, instant"), dry_run: bool = Query(False, description="Simulate without generating events"), + overwrite_parser: bool = Query(False, description="Overwrite existing parsers during execution"), _: str = Depends(require_write_access) ): """Execute an attack scenario""" @@ -385,6 +388,7 @@ async def execute_scenario( scenario_id=scenario_id, speed=speed, dry_run=dry_run, + overwrite_parser=overwrite_parser, background_tasks=background_tasks ) diff --git a/Backend/api/app/services/generator_service.py b/Backend/api/app/services/generator_service.py index 2d8438e..26ee8b0 100644 --- a/Backend/api/app/services/generator_service.py +++ b/Backend/api/app/services/generator_service.py @@ -172,7 +172,8 @@ async def execute_generator( count: int = 1, format: str = "json", star_trek_theme: bool = True, - options: Dict[str, Any] = None + options: Dict[str, Any] = None, + overwrite_parser: bool = False ) -> List[Dict[str, Any]]: """Execute a generator and return events""" if generator_id not in self.generator_metadata: diff --git a/Backend/api/app/services/parser_sync_service.py b/Backend/api/app/services/parser_sync_service.py index dd7d429..a79c20b 100644 --- a/Backend/api/app/services/parser_sync_service.py +++ b/Backend/api/app/services/parser_sync_service.py @@ -136,12 +136,9 @@ def get_parser_path_in_siem(self, sourcetype: str) -> str: Returns: The parser path in SIEM (e.g., '/logParsers/okta_authentication-latest') """ - # In the Scalyr/SentinelOne config tree, log parsers are stored as JSON files - # under /logParsers. - leaf = sourcetype - if not leaf.endswith(".json"): - leaf = f"{leaf}.json" - return f"/logParsers/{leaf}" + # In the Scalyr/SentinelOne config tree, log parsers are stored + # under /logParsers without a file extension. + return f"/logParsers/{sourcetype}" def _local_parser_directories_for_sourcetype(self, sourcetype: str) -> List[Path]: local_name = LOCAL_PARSER_ALIASES.get(sourcetype, sourcetype) @@ -207,15 +204,19 @@ def ensure_parser_for_sourcetype( github_repo_urls: Optional[List[str]] = None, github_token: Optional[str] = None, selected_parser: Optional[Dict] = None, + overwrite: bool = False, ) -> Dict[str, str]: parser_path = self.get_parser_path_in_siem(sourcetype) exists, _ = self.check_parser_exists(config_write_token, parser_path) - if exists: + if exists and not overwrite: return { "status": "exists", "message": f"Parser already exists: {parser_path}", } + + if exists and overwrite: + logger.info(f"Overwriting existing parser: {parser_path}") parser_content = self.load_local_parser(sourcetype) from_github = False @@ -434,7 +435,8 @@ def ensure_parsers_for_sources( config_write_token: str, github_repo_urls: Optional[List[str]] = None, github_token: Optional[str] = None, - selected_parsers: Optional[Dict[str, Dict]] = None + selected_parsers: Optional[Dict[str, Dict]] = None, + overwrite: bool = False ) -> Dict[str, dict]: """ Ensure all required parsers exist in the destination SIEM @@ -445,6 +447,7 @@ def ensure_parsers_for_sources( github_repo_urls: Optional list of GitHub repository URLs to fetch parsers from github_token: Optional GitHub personal access token for private repos selected_parsers: Optional dict mapping sourcetype to user-selected parser info + overwrite: If True, overwrite existing parsers instead of skipping them Returns: Dict with status for each source: @@ -492,7 +495,7 @@ def ensure_parsers_for_sources( # Check if parser exists (write token can also read) exists, _ = self.check_parser_exists(config_write_token, parser_path) - if exists: + if exists and not overwrite: results[source] = { "status": "exists", "sourcetype": sourcetype, @@ -500,6 +503,13 @@ def ensure_parsers_for_sources( } continue + if exists and overwrite: + # Parser exists but we need to overwrite it + logger.info(f"Overwriting existing parser: {parser_path}") + else: + # Parser doesn't exist, we need to create it + logger.info(f"Creating new parser: {parser_path}") + # Parser doesn't exist, try to find it parser_content = None from_github = False diff --git a/Backend/api/app/services/scenario_service.py b/Backend/api/app/services/scenario_service.py index c8e3d07..cbfbd35 100644 --- a/Backend/api/app/services/scenario_service.py +++ b/Backend/api/app/services/scenario_service.py @@ -1,16 +1,15 @@ """ Scenario service for managing attack scenarios """ -from typing import Dict, Any, List, Optional -import uuid -import time -import asyncio -import json -from datetime import datetime import logging import os import sys +import json +import uuid +import asyncio import importlib +from datetime import datetime +from typing import Dict, Any, Optional, List from pathlib import Path logger = logging.getLogger(__name__) @@ -129,6 +128,17 @@ def __init__(self): ] } , + "apollo_ransomware_scenario": { + "id": "apollo_ransomware_scenario", + "name": "Apollo Ransomware Scenario", + "description": "Proofpoint phishing, M365 email interaction, SharePoint recon & exfiltration", + "phases": [ + {"name": "Phishing Delivery", "generators": ["proofpoint"], "duration": 5}, + {"name": "Email Interaction", "generators": ["microsoft_365_collaboration"], "duration": 5}, + {"name": "SharePoint Recon", "generators": ["microsoft_365_collaboration"], "duration": 15}, + {"name": "Data Exfiltration", "generators": ["microsoft_365_collaboration"], "duration": 10} + ] + }, "hr_phishing_pdf_c2": { "id": "hr_phishing_pdf_c2", "name": "HR Phishing PDF -> PowerShell -> Scheduled Task -> C2", @@ -236,6 +246,7 @@ async def start_scenario( scenario_id: str, speed: str = "fast", dry_run: bool = False, + overwrite_parser: bool = False, background_tasks=None ) -> str: """Start scenario execution""" @@ -252,6 +263,7 @@ async def start_scenario( "started_at": datetime.utcnow().isoformat(), "speed": speed, "dry_run": dry_run, + "overwrite_parser": overwrite_parser, "progress": 0 } @@ -269,6 +281,7 @@ async def start_correlation_scenario( tag_trace: bool = True, speed: str = "fast", dry_run: bool = False, + overwrite_parser: bool = False, background_tasks=None ) -> str: """Start correlation scenario execution with SIEM context and trace ID support""" @@ -285,6 +298,7 @@ async def start_correlation_scenario( "trace_id": trace_id, "tag_phase": tag_phase, "tag_trace": tag_trace, + "overwrite_parser": overwrite_parser, "progress": 0 } diff --git a/Backend/event_generators/shared/hec_sender.py b/Backend/event_generators/shared/hec_sender.py index 1cbc99b..14e358c 100644 --- a/Backend/event_generators/shared/hec_sender.py +++ b/Backend/event_generators/shared/hec_sender.py @@ -958,6 +958,7 @@ def _send_batch(lines: list, is_json: bool, product: str): # Optional: ensure parsers exist in the destination SIEM before sending events _ENSURE_PARSER = os.getenv("JARVIS_ENSURE_PARSER", "false").lower() == "true" +_OVERWRITE_PARSER = os.getenv("JARVIS_OVERWRITE_PARSER", "false").lower() == "true" _JARVIS_API_BASE_URL = os.getenv("JARVIS_API_BASE_URL", "http://localhost:8000").rstrip("/") _JARVIS_API_KEY = os.getenv("JARVIS_API_KEY") _S1_CONFIG_API_URL = os.getenv("S1_CONFIG_API_URL") @@ -1002,6 +1003,7 @@ def _ensure_parser_in_destination(product: str) -> None: "sourcetype": sync_sourcetype, "config_api_url": _S1_CONFIG_API_URL, "config_write_token": _S1_CONFIG_WRITE_TOKEN, + "overwrite_parser": _OVERWRITE_PARSER, } try: diff --git a/Frontend/log_generator_ui.py b/Frontend/log_generator_ui.py index fc7bc91..ffde8e1 100644 --- a/Frontend/log_generator_ui.py +++ b/Frontend/log_generator_ui.py @@ -544,6 +544,7 @@ def run_correlation_scenario(): tag_trace = data.get('tag_trace', True) trace_id = (data.get('trace_id') or '').strip() local_token = data.get('hec_token') + overwrite_parser = data.get('overwrite_parser', False) if not scenario_id: return jsonify({'error': 'scenario_id is required'}), 400 @@ -551,6 +552,10 @@ def run_correlation_scenario(): return jsonify({'error': 'destination_id is required'}), 400 # Resolve destination + config_write_token = None + config_api_url = None + github_repo_urls = [] + github_token = None try: dest_resp = requests.get( f"{API_BASE_URL}/api/v1/destinations/{destination_id}", @@ -580,6 +585,35 @@ def run_correlation_scenario(): if not hec_url or not hec_token: return jsonify({'error': 'HEC destination incomplete'}), 400 + + # Fetch config token and URL for parser sync if available + config_api_url = chosen.get('config_api_url') + if chosen.get('has_config_write_token') and config_api_url: + try: + config_resp = requests.get( + f"{API_BASE_URL}/api/v1/destinations/{destination_id}/config-tokens", + headers=_get_api_headers(), + timeout=10 + ) + if config_resp.status_code == 200: + config_tokens = config_resp.json() + config_write_token = config_tokens.get('config_write_token') + except Exception as ce: + logger.warning(f"Failed to retrieve config token for correlation: {ce}") + + # Fetch GitHub parser repositories from settings + try: + repos_resp = requests.get( + f"{API_BASE_URL}/api/v1/settings/parser-repositories", + headers=_get_api_headers(), + timeout=10 + ) + if repos_resp.status_code == 200: + repos_data = repos_resp.json() + github_repo_urls = [url for url in repos_data.get('repositories', []) if url] + github_token = repos_data.get('github_token') + except Exception as ge: + logger.warning(f"Failed to retrieve GitHub parser repositories: {ge}") except Exception as e: return jsonify({'error': f'Failed to resolve destination: {str(e)}'}), 500 @@ -608,6 +642,50 @@ def generate_and_stream(): try: yield "INFO: Starting correlation scenario execution...\n" + # Parser sync: Check and upload required parsers before running scenario + if config_write_token and config_api_url: + if overwrite_parser: + yield "INFO: Checking required parsers in destination SIEM (overwrite mode ON)...\n" + else: + yield "INFO: Checking required parsers in destination SIEM...\n" + try: + sync_payload = { + "scenario_id": scenario_id, + "config_api_url": config_api_url, + "config_write_token": config_write_token, + "overwrite_parser": overwrite_parser + } + if github_repo_urls: + sync_payload["github_repo_urls"] = github_repo_urls + if github_token: + sync_payload["github_token"] = github_token + + sync_resp = requests.post( + f"{API_BASE_URL}/api/v1/parser-sync/sync", + headers=_get_api_headers(), + json=sync_payload, + timeout=120 + ) + if sync_resp.status_code == 200: + sync_result = sync_resp.json() + for source, info in sync_result.get('results', {}).items(): + status = info.get('status', 'unknown') + message = info.get('message', '') + sourcetype = info.get('sourcetype', 'unknown') + if status == 'exists': + yield f"INFO: Parser exists: {source} -> {sourcetype}\n" + elif status in ('uploaded', 'uploaded_from_github'): + yield f"INFO: Parser uploaded: {source} -> {sourcetype}\n" + elif status == 'failed': + yield f"WARN: Parser sync failed: {source} -> {sourcetype} - {message}\n" + elif status == 'no_parser': + yield f"WARN: No parser mapping: {source}\n" + yield "INFO: Parser sync complete\n" + else: + yield f"WARN: Parser sync API returned {sync_resp.status_code}, continuing without sync\n" + except Exception as pe: + yield f"WARN: Parser sync failed: {pe}, continuing without sync\n" + if siem_context and siem_context.get('results'): yield f"INFO: Using SIEM context with {len(siem_context.get('results', []))} results\n" if siem_context.get('anchors'): @@ -843,6 +921,7 @@ def run_scenario(): local_token = data.get('hec_token') # Token from browser localStorage sync_parsers = data.get('sync_parsers', True) # Enable parser sync by default debug_mode = data.get('debug_mode', False) # Verbose logging mode + overwrite_parser = data.get('overwrite_parser', False) # Overwrite existing parsers if not scenario_id: return jsonify({'error': 'scenario_id is required'}), 400 @@ -952,13 +1031,17 @@ def generate_and_stream(): # Parser sync: Check and upload required parsers before running scenario if sync_parsers and config_write_token and config_api_url: - yield "INFO: Checking required parsers in destination SIEM...\n" + if overwrite_parser: + yield "INFO: Checking required parsers in destination SIEM (overwrite mode ON)...\n" + else: + yield "INFO: Checking required parsers in destination SIEM...\n" try: # Call the parser sync API with GitHub repos sync_payload = { "scenario_id": scenario_id, "config_api_url": config_api_url, - "config_write_token": config_write_token + "config_write_token": config_write_token, + "overwrite_parser": overwrite_parser } if github_repo_urls: sync_payload["github_repo_urls"] = github_repo_urls @@ -1686,7 +1769,8 @@ def generate_logs(): eps = float(data.get('eps', 1.0)) continuous = data.get('continuous', False) speed_mode = data.get('speed_mode', False) - ensure_parser = bool(data.get('ensure_parser', False)) + overwrite_parser = bool(data.get('overwrite_parser', False)) + ensure_parser = bool(data.get('ensure_parser', False)) or overwrite_parser syslog_ip = data.get('ip') syslog_port = int(data.get('port')) if data.get('port') is not None else None syslog_protocol = data.get('protocol') @@ -1916,6 +2000,8 @@ def _normalize_hec_url(u: str) -> str: if ensure_parser: env['JARVIS_ENSURE_PARSER'] = 'true' + if overwrite_parser: + env['JARVIS_OVERWRITE_PARSER'] = 'true' env['JARVIS_API_BASE_URL'] = API_BASE_URL if BACKEND_API_KEY: env['JARVIS_API_KEY'] = BACKEND_API_KEY diff --git a/Frontend/templates/log_generator.html b/Frontend/templates/log_generator.html index 5bfe58a..0afa7ea 100644 --- a/Frontend/templates/log_generator.html +++ b/Frontend/templates/log_generator.html @@ -379,6 +379,14 @@

For HEC destinations: checks & uploads the parser to the destination SIEM before sending events (requires Config API URL + write token on the destination).

+
+ +

If checked, will overwrite existing parsers in the destination SIEM instead of skipping them. Use with caution.

+
+
+
+ Parser Options +
+ +

If checked, will overwrite existing parsers in the destination SIEM instead of skipping them. Use with caution.

+
+
+
+ +

If checked, will overwrite existing parsers in the destination SIEM instead of skipping them. Use with caution.

@@ -2217,7 +2241,8 @@

Select Parser Version

const eps = parseFloat(document.getElementById('eps').value); const continuousMode = continuousModeCheckbox.checked; const speedMode = speedModeCheckbox.checked; - const ensureParser = document.getElementById('ensure-parser')?.checked === true; + const overwriteParser = document.getElementById('overwrite-parser')?.checked === true; + const ensureParser = overwriteParser || (document.getElementById('ensure-parser')?.checked === true); const selectedOpt = destSelect.options[destSelect.selectedIndex]; const destinationId = selectedOpt ? selectedOpt.value : ''; const destinationType = selectedOpt ? selectedOpt.dataset.type : ''; @@ -2280,7 +2305,8 @@

Select Parser Version

destination_id: destinationId, hec_token: localToken, // Pass local token if available metadata: metadataFields, // Pass metadata fields if provided - ensure_parser: ensureParser + ensure_parser: ensureParser, + overwrite_parser: overwriteParser } : { destination: 'syslog', script: scriptPath, @@ -2502,6 +2528,9 @@

Select Parser Version

const generateNoise = document.getElementById('generate-noise')?.checked || false; const noiseEventsCount = parseInt(document.getElementById('noise-events-count')?.value || '1200', 10); + // Parser options + const overwriteParser = document.getElementById('scenario-overwrite-parser')?.checked || false; + // Get local token if available let localToken = null; if (window.tokenVault && window.tokenVault.hasToken(destinationId)) { @@ -2522,6 +2551,7 @@

Select Parser Version

trace_id: traceId, generate_noise: generateNoise, noise_events_count: noiseEventsCount, + overwrite_parser: overwriteParser, hec_token: localToken, // Pass local token if available debug_mode: debugMode // Pass debug mode for server-side filtering }), @@ -3060,6 +3090,7 @@

Select Parser Version

const workerCount = parseInt(document.getElementById('correlation-worker-count').value, 10) || 10; const tagPhase = document.getElementById('correlation-tag-phase').checked; const tagTrace = document.getElementById('correlation-tag-trace').checked; + const overwriteParser = document.getElementById('correlation-overwrite-parser').checked; let traceId = correlationTraceIdInput.value.trim(); if (tagTrace && !traceId) { @@ -3091,6 +3122,7 @@

Select Parser Version

tag_phase: tagPhase, tag_trace: tagTrace, trace_id: traceId, + overwrite_parser: overwriteParser, hec_token: localToken }) }); From a6fc2f0eadaa43768086898b69c9c10fc04e38ea Mon Sep 17 00:00:00 2001 From: jmorascalyr <42879226+jmorascalyr@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:05:02 -0700 Subject: [PATCH 5/8] feat: Standardize alert template resource UIDs to use DYNAMIC_RESOURCE_UID placeholder - Replaced hardcoded resource UIDs (helios-asset-001, res-001, jeanluc@starfleet.com) with DYNAMIC_RESOURCE_UID placeholder across all alert templates - Enables dynamic resource UID injection during alert generation for correlation scenarios - Updated 50+ O365 alert templates and default/advanced sample alerts to use consistent placeholder format --- .../alerts/templates/advanced_sample_alert.json | 4 ++-- .../api/app/alerts/templates/default_alert.json | 2 +- .../templates/o365_admin_consent_all.json | 2 +- .../templates/o365_antiphish_rule_disabled.json | 2 +- .../templates/o365_app_role_assigned.json | 2 +- .../templates/o365_attachment_removed.json | 2 +- .../app/alerts/templates/o365_audit_bypass.json | 2 +- .../alerts/templates/o365_auto_delete_rule.json | 2 +- .../alerts/templates/o365_bec_inbox_rule.json | 2 +- .../alerts/templates/o365_bec_rss_redirect.json | 2 +- .../alerts/templates/o365_bec_short_param.json | 2 +- .../templates/o365_brute_force_success.json | 2 +- .../templates/o365_ca_policy_deleted.json | 2 +- .../templates/o365_ca_policy_updated.json | 2 +- .../templates/o365_cloudsponge_activity.json | 2 +- .../templates/o365_connector_removed.json | 2 +- .../templates/o365_copilot_jailbreak.json | 2 +- .../templates/o365_dlp_policy_deleted.json | 2 +- .../templates/o365_emclient_activity.json | 2 +- .../templates/o365_external_redirection.json | 2 +- .../templates/o365_fasthttp_activity.json | 2 +- .../templates/o365_fastmail_activity.json | 2 +- .../templates/o365_federation_domain.json | 2 +- .../alerts/templates/o365_forwarding_rule.json | 2 +- .../alerts/templates/o365_full_access_app.json | 2 +- .../templates/o365_inbound_connector.json | 2 +- .../templates/o365_inbox_rule_redirect.json | 2 +- .../alerts/templates/o365_intune_ca_bypass.json | 2 +- .../templates/o365_mail_transport_rule.json | 2 +- .../templates/o365_mailbox_delegation.json | 2 +- .../templates/o365_malware_filter_disabled.json | 2 +- .../templates/o365_malware_policy_deleted.json | 2 +- .../templates/o365_management_group_role.json | 2 +- .../templates/o365_noncompliant_login.json | 2 +- .../alerts/templates/o365_oauth_email_name.json | 2 +- .../alerts/templates/o365_oauth_nonalpha.json | 2 +- .../templates/o365_outbound_connector.json | 2 +- .../templates/o365_perfectdata_activity.json | 2 +- .../alerts/templates/o365_rclone_download.json | 2 +- .../alerts/templates/o365_rclone_modify.json | 2 +- .../templates/o365_rdp_sharepoint_access.json | 2 +- .../app/alerts/templates/o365_rdp_upload.json | 2 +- .../templates/o365_restricted_sending.json | 2 +- .../alerts/templates/o365_sc_high_alert.json | 2 +- .../alerts/templates/o365_sc_info_alert.json | 2 +- .../app/alerts/templates/o365_sc_low_alert.json | 2 +- .../alerts/templates/o365_sc_medium_alert.json | 2 +- .../templates/o365_service_principal.json | 2 +- .../templates/o365_sigparser_activity.json | 2 +- .../app/alerts/templates/o365_sneaky_2fa.json | 2 +- .../alerts/templates/o365_spike_activity.json | 2 +- .../templates/o365_supermailer_activity.json | 2 +- .../app/alerts/templates/o365_threats_zap.json | 2 +- .../templates/o365_transport_rule_disabled.json | 2 +- .../app/alerts/templates/o365_url_removed.json | 2 +- .../app/alerts/templates/o365_zap_removed.json | 2 +- .../templates/o365_zoominfo_activity.json | 2 +- .../proofpoint_attachment_delivered.json | 2 +- .../templates/proofpoint_email_alert.json | 2 +- .../proofpoint_impostor_unblocked.json | 2 +- .../templates/proofpoint_large_attachments.json | 2 +- .../templates/proofpoint_outbound_phishing.json | 2 +- .../proofpoint_phishing_link_clicked.json | 2 +- .../proofpoint_phishing_unblocked.json | 2 +- .../proofpoint_source_code_attachments.json | 2 +- .../api/app/alerts/templates/sample_alert.json | 4 ++-- .../templates/sharepoint_data_exfil_alert.json | 2 +- Backend/api/app/services/alert_service.py | 17 +++++++++++++++-- Backend/scenarios/apollo_ransomware_scenario.py | 5 +++-- 69 files changed, 87 insertions(+), 73 deletions(-) diff --git a/Backend/api/app/alerts/templates/advanced_sample_alert.json b/Backend/api/app/alerts/templates/advanced_sample_alert.json index 6a74f95..5f5ad67 100644 --- a/Backend/api/app/alerts/templates/advanced_sample_alert.json +++ b/Backend/api/app/alerts/templates/advanced_sample_alert.json @@ -72,8 +72,8 @@ }, "resources": [ { - "uid": "helios-asset-001", - "name": "helios-endpoint-01" + "uid": "DYNAMIC_RESOURCE_UID", + "name": "helios-endpoint" } ], "severity": "high", diff --git a/Backend/api/app/alerts/templates/default_alert.json b/Backend/api/app/alerts/templates/default_alert.json index 636ee83..793e7cd 100644 --- a/Backend/api/app/alerts/templates/default_alert.json +++ b/Backend/api/app/alerts/templates/default_alert.json @@ -60,7 +60,7 @@ "resources": [ { "name": "endpoint-workstation-01", - "uid": "res-001" + "uid": "DYNAMIC_RESOURCE_UID" } ], "metadata": { diff --git a/Backend/api/app/alerts/templates/o365_admin_consent_all.json b/Backend/api/app/alerts/templates/o365_admin_consent_all.json index f530c99..e580d33 100644 --- a/Backend/api/app/alerts/templates/o365_admin_consent_all.json +++ b/Backend/api/app/alerts/templates/o365_admin_consent_all.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_antiphish_rule_disabled.json b/Backend/api/app/alerts/templates/o365_antiphish_rule_disabled.json index c01ef10..539e509 100644 --- a/Backend/api/app/alerts/templates/o365_antiphish_rule_disabled.json +++ b/Backend/api/app/alerts/templates/o365_antiphish_rule_disabled.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_app_role_assigned.json b/Backend/api/app/alerts/templates/o365_app_role_assigned.json index 8bc990b..8919aeb 100644 --- a/Backend/api/app/alerts/templates/o365_app_role_assigned.json +++ b/Backend/api/app/alerts/templates/o365_app_role_assigned.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_attachment_removed.json b/Backend/api/app/alerts/templates/o365_attachment_removed.json index f203901..58f8d9c 100644 --- a/Backend/api/app/alerts/templates/o365_attachment_removed.json +++ b/Backend/api/app/alerts/templates/o365_attachment_removed.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_audit_bypass.json b/Backend/api/app/alerts/templates/o365_audit_bypass.json index b4cffa3..842c876 100644 --- a/Backend/api/app/alerts/templates/o365_audit_bypass.json +++ b/Backend/api/app/alerts/templates/o365_audit_bypass.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_auto_delete_rule.json b/Backend/api/app/alerts/templates/o365_auto_delete_rule.json index ad31fd8..dbcc435 100644 --- a/Backend/api/app/alerts/templates/o365_auto_delete_rule.json +++ b/Backend/api/app/alerts/templates/o365_auto_delete_rule.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_bec_inbox_rule.json b/Backend/api/app/alerts/templates/o365_bec_inbox_rule.json index 130ef41..a0abf22 100644 --- a/Backend/api/app/alerts/templates/o365_bec_inbox_rule.json +++ b/Backend/api/app/alerts/templates/o365_bec_inbox_rule.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_bec_rss_redirect.json b/Backend/api/app/alerts/templates/o365_bec_rss_redirect.json index 995ebae..08a4a44 100644 --- a/Backend/api/app/alerts/templates/o365_bec_rss_redirect.json +++ b/Backend/api/app/alerts/templates/o365_bec_rss_redirect.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_bec_short_param.json b/Backend/api/app/alerts/templates/o365_bec_short_param.json index 9417a6b..bf4f8e2 100644 --- a/Backend/api/app/alerts/templates/o365_bec_short_param.json +++ b/Backend/api/app/alerts/templates/o365_bec_short_param.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_brute_force_success.json b/Backend/api/app/alerts/templates/o365_brute_force_success.json index e4ee154..ebffb3c 100644 --- a/Backend/api/app/alerts/templates/o365_brute_force_success.json +++ b/Backend/api/app/alerts/templates/o365_brute_force_success.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_ca_policy_deleted.json b/Backend/api/app/alerts/templates/o365_ca_policy_deleted.json index ecdbf56..595bbc8 100644 --- a/Backend/api/app/alerts/templates/o365_ca_policy_deleted.json +++ b/Backend/api/app/alerts/templates/o365_ca_policy_deleted.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_ca_policy_updated.json b/Backend/api/app/alerts/templates/o365_ca_policy_updated.json index d259410..a3cfda0 100644 --- a/Backend/api/app/alerts/templates/o365_ca_policy_updated.json +++ b/Backend/api/app/alerts/templates/o365_ca_policy_updated.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_cloudsponge_activity.json b/Backend/api/app/alerts/templates/o365_cloudsponge_activity.json index 14d8c6a..baad5ca 100644 --- a/Backend/api/app/alerts/templates/o365_cloudsponge_activity.json +++ b/Backend/api/app/alerts/templates/o365_cloudsponge_activity.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_connector_removed.json b/Backend/api/app/alerts/templates/o365_connector_removed.json index af77af8..b5dfd1c 100644 --- a/Backend/api/app/alerts/templates/o365_connector_removed.json +++ b/Backend/api/app/alerts/templates/o365_connector_removed.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_copilot_jailbreak.json b/Backend/api/app/alerts/templates/o365_copilot_jailbreak.json index 6143bcd..54d4489 100644 --- a/Backend/api/app/alerts/templates/o365_copilot_jailbreak.json +++ b/Backend/api/app/alerts/templates/o365_copilot_jailbreak.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_dlp_policy_deleted.json b/Backend/api/app/alerts/templates/o365_dlp_policy_deleted.json index 9075492..f7411cb 100644 --- a/Backend/api/app/alerts/templates/o365_dlp_policy_deleted.json +++ b/Backend/api/app/alerts/templates/o365_dlp_policy_deleted.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_emclient_activity.json b/Backend/api/app/alerts/templates/o365_emclient_activity.json index b498fb9..ec90399 100644 --- a/Backend/api/app/alerts/templates/o365_emclient_activity.json +++ b/Backend/api/app/alerts/templates/o365_emclient_activity.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_external_redirection.json b/Backend/api/app/alerts/templates/o365_external_redirection.json index 15b0cd9..52abf77 100644 --- a/Backend/api/app/alerts/templates/o365_external_redirection.json +++ b/Backend/api/app/alerts/templates/o365_external_redirection.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_fasthttp_activity.json b/Backend/api/app/alerts/templates/o365_fasthttp_activity.json index 9e2911b..962b5ee 100644 --- a/Backend/api/app/alerts/templates/o365_fasthttp_activity.json +++ b/Backend/api/app/alerts/templates/o365_fasthttp_activity.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_fastmail_activity.json b/Backend/api/app/alerts/templates/o365_fastmail_activity.json index 732b0f6..fff52b7 100644 --- a/Backend/api/app/alerts/templates/o365_fastmail_activity.json +++ b/Backend/api/app/alerts/templates/o365_fastmail_activity.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_federation_domain.json b/Backend/api/app/alerts/templates/o365_federation_domain.json index 5b03647..bcba9de 100644 --- a/Backend/api/app/alerts/templates/o365_federation_domain.json +++ b/Backend/api/app/alerts/templates/o365_federation_domain.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_forwarding_rule.json b/Backend/api/app/alerts/templates/o365_forwarding_rule.json index e73e8a2..fecefd3 100644 --- a/Backend/api/app/alerts/templates/o365_forwarding_rule.json +++ b/Backend/api/app/alerts/templates/o365_forwarding_rule.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_full_access_app.json b/Backend/api/app/alerts/templates/o365_full_access_app.json index 90ead46..a34e7a7 100644 --- a/Backend/api/app/alerts/templates/o365_full_access_app.json +++ b/Backend/api/app/alerts/templates/o365_full_access_app.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_inbound_connector.json b/Backend/api/app/alerts/templates/o365_inbound_connector.json index da6aa60..71211d3 100644 --- a/Backend/api/app/alerts/templates/o365_inbound_connector.json +++ b/Backend/api/app/alerts/templates/o365_inbound_connector.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_inbox_rule_redirect.json b/Backend/api/app/alerts/templates/o365_inbox_rule_redirect.json index aae746e..ec1984e 100644 --- a/Backend/api/app/alerts/templates/o365_inbox_rule_redirect.json +++ b/Backend/api/app/alerts/templates/o365_inbox_rule_redirect.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_intune_ca_bypass.json b/Backend/api/app/alerts/templates/o365_intune_ca_bypass.json index 03a1ef8..7bef90d 100644 --- a/Backend/api/app/alerts/templates/o365_intune_ca_bypass.json +++ b/Backend/api/app/alerts/templates/o365_intune_ca_bypass.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_mail_transport_rule.json b/Backend/api/app/alerts/templates/o365_mail_transport_rule.json index 7405490..34b4106 100644 --- a/Backend/api/app/alerts/templates/o365_mail_transport_rule.json +++ b/Backend/api/app/alerts/templates/o365_mail_transport_rule.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_mailbox_delegation.json b/Backend/api/app/alerts/templates/o365_mailbox_delegation.json index 7093855..3b5a596 100644 --- a/Backend/api/app/alerts/templates/o365_mailbox_delegation.json +++ b/Backend/api/app/alerts/templates/o365_mailbox_delegation.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_malware_filter_disabled.json b/Backend/api/app/alerts/templates/o365_malware_filter_disabled.json index fdf4f23..b05815a 100644 --- a/Backend/api/app/alerts/templates/o365_malware_filter_disabled.json +++ b/Backend/api/app/alerts/templates/o365_malware_filter_disabled.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_malware_policy_deleted.json b/Backend/api/app/alerts/templates/o365_malware_policy_deleted.json index 4443a96..dff30dd 100644 --- a/Backend/api/app/alerts/templates/o365_malware_policy_deleted.json +++ b/Backend/api/app/alerts/templates/o365_malware_policy_deleted.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_management_group_role.json b/Backend/api/app/alerts/templates/o365_management_group_role.json index 68b5929..ddb58fd 100644 --- a/Backend/api/app/alerts/templates/o365_management_group_role.json +++ b/Backend/api/app/alerts/templates/o365_management_group_role.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_noncompliant_login.json b/Backend/api/app/alerts/templates/o365_noncompliant_login.json index f5d91dd..0223348 100644 --- a/Backend/api/app/alerts/templates/o365_noncompliant_login.json +++ b/Backend/api/app/alerts/templates/o365_noncompliant_login.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_oauth_email_name.json b/Backend/api/app/alerts/templates/o365_oauth_email_name.json index c968ddf..318500a 100644 --- a/Backend/api/app/alerts/templates/o365_oauth_email_name.json +++ b/Backend/api/app/alerts/templates/o365_oauth_email_name.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_oauth_nonalpha.json b/Backend/api/app/alerts/templates/o365_oauth_nonalpha.json index 9d3bc57..a056942 100644 --- a/Backend/api/app/alerts/templates/o365_oauth_nonalpha.json +++ b/Backend/api/app/alerts/templates/o365_oauth_nonalpha.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_outbound_connector.json b/Backend/api/app/alerts/templates/o365_outbound_connector.json index a247ce4..2827aa5 100644 --- a/Backend/api/app/alerts/templates/o365_outbound_connector.json +++ b/Backend/api/app/alerts/templates/o365_outbound_connector.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_perfectdata_activity.json b/Backend/api/app/alerts/templates/o365_perfectdata_activity.json index 86c2ad0..3b28604 100644 --- a/Backend/api/app/alerts/templates/o365_perfectdata_activity.json +++ b/Backend/api/app/alerts/templates/o365_perfectdata_activity.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_rclone_download.json b/Backend/api/app/alerts/templates/o365_rclone_download.json index 525c5f1..2b0f6fd 100644 --- a/Backend/api/app/alerts/templates/o365_rclone_download.json +++ b/Backend/api/app/alerts/templates/o365_rclone_download.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_rclone_modify.json b/Backend/api/app/alerts/templates/o365_rclone_modify.json index 624bc5c..a9f390d 100644 --- a/Backend/api/app/alerts/templates/o365_rclone_modify.json +++ b/Backend/api/app/alerts/templates/o365_rclone_modify.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_rdp_sharepoint_access.json b/Backend/api/app/alerts/templates/o365_rdp_sharepoint_access.json index 58ec997..05ba5b7 100644 --- a/Backend/api/app/alerts/templates/o365_rdp_sharepoint_access.json +++ b/Backend/api/app/alerts/templates/o365_rdp_sharepoint_access.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_rdp_upload.json b/Backend/api/app/alerts/templates/o365_rdp_upload.json index fec96a8..472bc14 100644 --- a/Backend/api/app/alerts/templates/o365_rdp_upload.json +++ b/Backend/api/app/alerts/templates/o365_rdp_upload.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_restricted_sending.json b/Backend/api/app/alerts/templates/o365_restricted_sending.json index 8574d74..10b2cec 100644 --- a/Backend/api/app/alerts/templates/o365_restricted_sending.json +++ b/Backend/api/app/alerts/templates/o365_restricted_sending.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_sc_high_alert.json b/Backend/api/app/alerts/templates/o365_sc_high_alert.json index 88ac195..09ac08a 100644 --- a/Backend/api/app/alerts/templates/o365_sc_high_alert.json +++ b/Backend/api/app/alerts/templates/o365_sc_high_alert.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_sc_info_alert.json b/Backend/api/app/alerts/templates/o365_sc_info_alert.json index 2b45f2a..33776aa 100644 --- a/Backend/api/app/alerts/templates/o365_sc_info_alert.json +++ b/Backend/api/app/alerts/templates/o365_sc_info_alert.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_sc_low_alert.json b/Backend/api/app/alerts/templates/o365_sc_low_alert.json index 4b8e7da..4e7f0a7 100644 --- a/Backend/api/app/alerts/templates/o365_sc_low_alert.json +++ b/Backend/api/app/alerts/templates/o365_sc_low_alert.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_sc_medium_alert.json b/Backend/api/app/alerts/templates/o365_sc_medium_alert.json index 81dddca..862c945 100644 --- a/Backend/api/app/alerts/templates/o365_sc_medium_alert.json +++ b/Backend/api/app/alerts/templates/o365_sc_medium_alert.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_service_principal.json b/Backend/api/app/alerts/templates/o365_service_principal.json index 550c98c..6b45bd9 100644 --- a/Backend/api/app/alerts/templates/o365_service_principal.json +++ b/Backend/api/app/alerts/templates/o365_service_principal.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_sigparser_activity.json b/Backend/api/app/alerts/templates/o365_sigparser_activity.json index 121096a..9b0e383 100644 --- a/Backend/api/app/alerts/templates/o365_sigparser_activity.json +++ b/Backend/api/app/alerts/templates/o365_sigparser_activity.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_sneaky_2fa.json b/Backend/api/app/alerts/templates/o365_sneaky_2fa.json index ebf441a..d33c49a 100644 --- a/Backend/api/app/alerts/templates/o365_sneaky_2fa.json +++ b/Backend/api/app/alerts/templates/o365_sneaky_2fa.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_spike_activity.json b/Backend/api/app/alerts/templates/o365_spike_activity.json index 7142c8e..7a20575 100644 --- a/Backend/api/app/alerts/templates/o365_spike_activity.json +++ b/Backend/api/app/alerts/templates/o365_spike_activity.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_supermailer_activity.json b/Backend/api/app/alerts/templates/o365_supermailer_activity.json index 7423d29..b864514 100644 --- a/Backend/api/app/alerts/templates/o365_supermailer_activity.json +++ b/Backend/api/app/alerts/templates/o365_supermailer_activity.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_threats_zap.json b/Backend/api/app/alerts/templates/o365_threats_zap.json index c0c82ad..07903e4 100644 --- a/Backend/api/app/alerts/templates/o365_threats_zap.json +++ b/Backend/api/app/alerts/templates/o365_threats_zap.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_transport_rule_disabled.json b/Backend/api/app/alerts/templates/o365_transport_rule_disabled.json index 78c3737..1464965 100644 --- a/Backend/api/app/alerts/templates/o365_transport_rule_disabled.json +++ b/Backend/api/app/alerts/templates/o365_transport_rule_disabled.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_url_removed.json b/Backend/api/app/alerts/templates/o365_url_removed.json index d6aa97e..f3f3e70 100644 --- a/Backend/api/app/alerts/templates/o365_url_removed.json +++ b/Backend/api/app/alerts/templates/o365_url_removed.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_zap_removed.json b/Backend/api/app/alerts/templates/o365_zap_removed.json index f1f76d8..71f4492 100644 --- a/Backend/api/app/alerts/templates/o365_zap_removed.json +++ b/Backend/api/app/alerts/templates/o365_zap_removed.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/o365_zoominfo_activity.json b/Backend/api/app/alerts/templates/o365_zoominfo_activity.json index 831efaf..0b11295 100644 --- a/Backend/api/app/alerts/templates/o365_zoominfo_activity.json +++ b/Backend/api/app/alerts/templates/o365_zoominfo_activity.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/proofpoint_attachment_delivered.json b/Backend/api/app/alerts/templates/proofpoint_attachment_delivered.json index 68b1619..503d07c 100644 --- a/Backend/api/app/alerts/templates/proofpoint_attachment_delivered.json +++ b/Backend/api/app/alerts/templates/proofpoint_attachment_delivered.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/proofpoint_email_alert.json b/Backend/api/app/alerts/templates/proofpoint_email_alert.json index 1b0a9d1..37ec97b 100644 --- a/Backend/api/app/alerts/templates/proofpoint_email_alert.json +++ b/Backend/api/app/alerts/templates/proofpoint_email_alert.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/proofpoint_impostor_unblocked.json b/Backend/api/app/alerts/templates/proofpoint_impostor_unblocked.json index 7a773da..bc1a434 100644 --- a/Backend/api/app/alerts/templates/proofpoint_impostor_unblocked.json +++ b/Backend/api/app/alerts/templates/proofpoint_impostor_unblocked.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/proofpoint_large_attachments.json b/Backend/api/app/alerts/templates/proofpoint_large_attachments.json index a311154..364b8c5 100644 --- a/Backend/api/app/alerts/templates/proofpoint_large_attachments.json +++ b/Backend/api/app/alerts/templates/proofpoint_large_attachments.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/proofpoint_outbound_phishing.json b/Backend/api/app/alerts/templates/proofpoint_outbound_phishing.json index 06ce0b2..43a9b57 100644 --- a/Backend/api/app/alerts/templates/proofpoint_outbound_phishing.json +++ b/Backend/api/app/alerts/templates/proofpoint_outbound_phishing.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/proofpoint_phishing_link_clicked.json b/Backend/api/app/alerts/templates/proofpoint_phishing_link_clicked.json index 52e9633..7076eb9 100644 --- a/Backend/api/app/alerts/templates/proofpoint_phishing_link_clicked.json +++ b/Backend/api/app/alerts/templates/proofpoint_phishing_link_clicked.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/proofpoint_phishing_unblocked.json b/Backend/api/app/alerts/templates/proofpoint_phishing_unblocked.json index 2299747..2af421e 100644 --- a/Backend/api/app/alerts/templates/proofpoint_phishing_unblocked.json +++ b/Backend/api/app/alerts/templates/proofpoint_phishing_unblocked.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/proofpoint_source_code_attachments.json b/Backend/api/app/alerts/templates/proofpoint_source_code_attachments.json index 49b5f7b..da227e7 100644 --- a/Backend/api/app/alerts/templates/proofpoint_source_code_attachments.json +++ b/Backend/api/app/alerts/templates/proofpoint_source_code_attachments.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/alerts/templates/sample_alert.json b/Backend/api/app/alerts/templates/sample_alert.json index fa20f5f..8f5dac9 100644 --- a/Backend/api/app/alerts/templates/sample_alert.json +++ b/Backend/api/app/alerts/templates/sample_alert.json @@ -6,8 +6,8 @@ }, "resources": [ { - "uid": "helios-asset-001", - "name": "helios-endpoint-01" + "uid": "DYNAMIC_RESOURCE_UID", + "name": "helios-endpoint" } ], "severity": "high", diff --git a/Backend/api/app/alerts/templates/sharepoint_data_exfil_alert.json b/Backend/api/app/alerts/templates/sharepoint_data_exfil_alert.json index 83a16ba..13c69ba 100644 --- a/Backend/api/app/alerts/templates/sharepoint_data_exfil_alert.json +++ b/Backend/api/app/alerts/templates/sharepoint_data_exfil_alert.json @@ -6,7 +6,7 @@ }, "resources": [ { - "uid": "jeanluc@starfleet.com", + "uid": "DYNAMIC_RESOURCE_UID", "name": "jeanluc@starfleet.com" } ], diff --git a/Backend/api/app/services/alert_service.py b/Backend/api/app/services/alert_service.py index 5061ab3..d9e091b 100644 --- a/Backend/api/app/services/alert_service.py +++ b/Backend/api/app/services/alert_service.py @@ -109,9 +109,9 @@ def prepare_scenario_alert( alert["metadata"]["logged_time"] = time_ms alert["metadata"]["modified_time"] = time_ms - # Set user as the resource + # Set user as the resource (uid must be a UUID for site-scoped alerts) alert["resources"] = [{ - "uid": user_email, + "uid": str(uuid.uuid4()), "name": user_email }] @@ -164,6 +164,11 @@ def prepare_alert( for event in alert["finding_info"]["related_events"]: event["uid"] = str(uuid.uuid4()) + # Generate UIDs for resources with placeholder + for resource in alert.get("resources", []): + if resource.get("uid") == "DYNAMIC_RESOURCE_UID": + resource["uid"] = str(uuid.uuid4()) + # Apply overrides if overrides: for key, value in overrides.items(): @@ -219,6 +224,7 @@ def egress_alert( "S1-Scope": scope, "Content-Encoding": "gzip", "Content-Type": "application/json", + "S1-Trace-Id": "helios-ingest-uam:alwayslog", } # UAM ingest API expects a single alert object (batch not supported for alerts) @@ -228,6 +234,13 @@ def egress_alert( url = uam_ingest_url.rstrip("/") + "/v1/alerts" + logger.info( + "UAM alert egress: url=%s scope=%s token_len=%d token_prefix=%s token_suffix=%s headers=%s payload_size=%d gzip_size=%d", + url, scope, len(token), token[:20] + "...", "..." + token[-20:], + {k: v for k, v in headers.items() if k != "Authorization"}, + len(payload), len(gzipped_alert) + ) + try: logger.info(f"Sending POST request to {url}") response = requests.post(url, headers=headers, data=gzipped_alert, timeout=30) diff --git a/Backend/scenarios/apollo_ransomware_scenario.py b/Backend/scenarios/apollo_ransomware_scenario.py index cc2b46b..2421f96 100644 --- a/Backend/scenarios/apollo_ransomware_scenario.py +++ b/Backend/scenarios/apollo_ransomware_scenario.py @@ -301,9 +301,9 @@ def send_phase_alert( alert["metadata"]["logged_time"] = time_ms alert["metadata"]["modified_time"] = time_ms - # Set user as the resource + # Set user as the resource (uid must be a UUID for site-scoped alerts) alert["resources"] = [{ - "uid": VICTIM_PROFILE["email"], + "uid": str(uuid.uuid4()), "name": VICTIM_PROFILE["email"] }] @@ -333,6 +333,7 @@ def send_phase_alert( "S1-Scope": scope, "Content-Encoding": "gzip", "Content-Type": "application/json", + "S1-Trace-Id": "helios-ingest-uam:alwayslog", } payload = json.dumps(alert).encode("utf-8") From a6ff0afd4b7630576a597bf57f9455f17a9185d3 Mon Sep 17 00:00:00 2001 From: jmorascalyr <42879226+jmorascalyr@users.noreply.github.com> Date: Fri, 20 Feb 2026 18:28:35 -0700 Subject: [PATCH 6/8] feat: Adjust alert timing offsets and update RDP alert title in Apollo scenario - Updated data exfiltration alert offset from 20 to 25 minutes with clarifying comment (after last document download at base+24:30) - Updated RDP download alert offset from 25 to 35 minutes with clarifying comment (after RDP file download event at base+25) - Changed RDP alert title from "Apollo Ransomware - RDP Files Downloaded" to "OneDrive RDP Files Downloaded" for consistency --- Backend/scenarios/apollo_ransomware_scenario.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Backend/scenarios/apollo_ransomware_scenario.py b/Backend/scenarios/apollo_ransomware_scenario.py index 2421f96..96f6c08 100644 --- a/Backend/scenarios/apollo_ransomware_scenario.py +++ b/Backend/scenarios/apollo_ransomware_scenario.py @@ -159,7 +159,7 @@ }, "šŸ“¤ PHASE 4: Data Exfiltration": { "template": "sharepoint_data_exfil_alert", - "offset_minutes": 20, # 20 min after initial compromise + "offset_minutes": 25, # After last document download (base+24:30) "overrides": { "finding_info.title": "Data Exfiltration from SharePoint", "finding_info.desc": f"User {VICTIM_PROFILE['email']} downloaded sensitive documents including Personnel Records and Command Codes" @@ -167,9 +167,9 @@ }, "rdp_download": { "template": "o365_rdp_sharepoint_access", - "offset_minutes": 25, # 25 min after initial compromise + "offset_minutes": 35, # After RDP file download event (base+25) "overrides": { - "finding_info.title": "Apollo Ransomware - RDP Files Downloaded", + "finding_info.title": "OneDrive RDP Files Downloaded", "finding_info.desc": f"User {VICTIM_PROFILE['email']} downloaded RDP files from SharePoint - potential lateral movement preparation" } } From 9f8d44296bb4bd971b76f6fbce28e9f5fbf9321e Mon Sep 17 00:00:00 2001 From: jmorascalyr <42879226+jmorascalyr@users.noreply.github.com> Date: Sat, 21 Feb 2026 07:10:15 -0700 Subject: [PATCH 7/8] Adding asset id discovery service for correlated alerts --- Backend/api/app/models/destination.py | 8 + Backend/api/app/routers/destinations.py | 64 +++ Backend/api/app/services/alert_service.py | 53 +++ .../api/app/services/destination_service.py | 14 + .../scenarios/apollo_ransomware_scenario.py | 59 ++- Backend/test_alert_site.py | 392 ++++++++++++++++++ Frontend/log_generator_ui.py | 99 +++++ Frontend/templates/log_generator.html | 144 ++++++- 8 files changed, 827 insertions(+), 6 deletions(-) create mode 100644 Backend/test_alert_site.py diff --git a/Backend/api/app/models/destination.py b/Backend/api/app/models/destination.py index 89446ea..38070ae 100644 --- a/Backend/api/app/models/destination.py +++ b/Backend/api/app/models/destination.py @@ -24,6 +24,10 @@ class Destination(Base): config_write_token_encrypted = Column(Text, nullable=True) # For putFile API powerquery_read_token_encrypted = Column(Text, nullable=True) # For PowerQuery Log Read Access + # S1 Management API (for asset lookups, agent queries) + s1_management_url = Column(String, nullable=True) # e.g., https://demo.sentinelone.net + s1_api_token_encrypted = Column(Text, nullable=True) # Encrypted S1 API Token (for /web/api/v2.1/agents) + # UAM Alert Ingest (Service Account - separate from HEC) uam_ingest_url = Column(String, nullable=True) # e.g., https://ingest.us1.sentinelone.net uam_account_id = Column(String, nullable=True) # SentinelOne account ID @@ -69,6 +73,10 @@ def to_dict(self, include_token=False, encryption_service=None): result['has_config_write_token'] = bool(self.config_write_token_encrypted) result['has_powerquery_read_token'] = bool(self.powerquery_read_token_encrypted) + # S1 Management API + result['s1_management_url'] = self.s1_management_url + result['has_s1_api_token'] = bool(self.s1_api_token_encrypted) + # UAM Alert Ingest settings result['uam_ingest_url'] = self.uam_ingest_url result['uam_account_id'] = self.uam_account_id diff --git a/Backend/api/app/routers/destinations.py b/Backend/api/app/routers/destinations.py index 673b5b9..1ba552c 100644 --- a/Backend/api/app/routers/destinations.py +++ b/Backend/api/app/routers/destinations.py @@ -28,6 +28,10 @@ class DestinationCreate(BaseModel): config_write_token: Optional[str] = Field(None, description="Config API token for reading and writing parsers") powerquery_read_token: Optional[str] = Field(None, description="PowerQuery Log Read Access token for querying SIEM data") + # S1 Management API + s1_management_url: Optional[str] = Field(None, description="S1 Management API URL for asset lookups (e.g., https://demo.sentinelone.net)") + s1_api_token: Optional[str] = Field(None, description="S1 API Token for management API calls (Settings → Users → API Token)") + # UAM Alert Ingest (Service Account) uam_ingest_url: Optional[str] = Field(None, description="UAM ingest URL (e.g., https://ingest.us1.sentinelone.net)") uam_account_id: Optional[str] = Field(None, description="SentinelOne account ID for S1-Scope header") @@ -48,6 +52,8 @@ class DestinationUpdate(BaseModel): config_api_url: Optional[str] = None config_write_token: Optional[str] = None powerquery_read_token: Optional[str] = None + s1_management_url: Optional[str] = None + s1_api_token: Optional[str] = None uam_ingest_url: Optional[str] = None uam_account_id: Optional[str] = None uam_site_id: Optional[str] = None @@ -72,6 +78,8 @@ class DestinationResponse(BaseModel): config_api_url: Optional[str] = None # Config API URL for parser management has_config_write_token: Optional[bool] = None # True if config API token is set has_powerquery_read_token: Optional[bool] = None # True if PowerQuery read token is set + s1_management_url: Optional[str] = None # S1 Management API URL + has_s1_api_token: Optional[bool] = None # True if S1 API token is set uam_ingest_url: Optional[str] = None # UAM ingest URL uam_account_id: Optional[str] = None # SentinelOne account ID uam_site_id: Optional[str] = None # Optional site ID @@ -153,6 +161,8 @@ async def create_destination( config_api_url=destination.config_api_url, config_write_token=destination.config_write_token, powerquery_read_token=destination.powerquery_read_token, + s1_management_url=destination.s1_management_url, + s1_api_token=destination.s1_api_token, uam_ingest_url=destination.uam_ingest_url, uam_account_id=destination.uam_account_id, uam_site_id=destination.uam_site_id, @@ -290,6 +300,8 @@ async def update_destination( config_api_url=update.config_api_url, config_write_token=update.config_write_token, powerquery_read_token=update.powerquery_read_token, + s1_management_url=update.s1_management_url, + s1_api_token=update.s1_api_token, uam_ingest_url=update.uam_ingest_url, uam_account_id=update.uam_account_id, uam_site_id=update.uam_site_id, @@ -472,3 +484,55 @@ async def get_destination_uam_token( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to decrypt UAM token" ) + + +@router.get("/{dest_id}/s1-api-token") +async def get_destination_s1_api_token( + dest_id: str, + session: AsyncSession = Depends(get_session), + auth_info: tuple = Depends(get_api_key) +): + """ + Get decrypted S1 API Token for a destination (internal use only) + + Returns the decrypted S1 API token along with the management URL + for making agent/asset lookups via the S1 management API + """ + service = DestinationService(session) + destination = await service.get_destination(dest_id) + if not destination: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Destination '{dest_id}' not found" + ) + + if destination.type != 'hec': + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Only HEC destinations have S1 API tokens" + ) + + if not destination.s1_api_token_encrypted: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No S1 API Token found for this destination" + ) + + if not destination.s1_management_url: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="No S1 Management URL configured for this destination" + ) + + try: + token = service.decrypt_token(destination.s1_api_token_encrypted) + return { + "token": token, + "s1_management_url": destination.s1_management_url, + } + except Exception as e: + logger.error(f"Failed to decrypt S1 API token: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to decrypt S1 API token" + ) diff --git a/Backend/api/app/services/alert_service.py b/Backend/api/app/services/alert_service.py index d9e091b..1af69aa 100644 --- a/Backend/api/app/services/alert_service.py +++ b/Backend/api/app/services/alert_service.py @@ -194,6 +194,59 @@ def _replace_dynamic(self, obj: Any, time_ms: int) -> None: elif isinstance(item, (dict, list)): self._replace_dynamic(item, time_ms) + def lookup_xdr_asset_id( + self, + s1_management_url: str, + api_token: str, + asset_name: str, + account_id: str, + site_id: Optional[str] = None, + ) -> Optional[str]: + """Look up an XDR Asset ID from the S1 management API. + + The XDR Asset ID (e.g., 'eimvmdpvax6mtmbpdbxtoaem5q') is the value + needed for resources[].uid to link alerts to real S1 agent assets. + + Args: + s1_management_url: S1 management console URL (e.g., https://demo.sentinelone.net) + api_token: S1 API token + asset_name: Asset hostname to look up (e.g., 'bridge') + account_id: S1 account ID + site_id: Optional site ID to scope the search + + Returns: + The XDR Asset ID string, or None if not found + """ + try: + url = f"{s1_management_url.rstrip('/')}/web/api/v2.1/xdr/assets" + params = {"accountIds": account_id} + if site_id: + params["siteIds"] = site_id + headers = { + "Authorization": f"ApiToken {api_token}", + "Content-Type": "application/json", + } + response = requests.get(url, headers=headers, params=params, timeout=15) + response.raise_for_status() + data = response.json() + assets = data.get("data", []) + + # Find the real agent asset (has 'agent' field), matching by name + for asset in assets: + if asset.get("name", "").lower() == asset_name.lower() and asset.get("agent"): + asset_id = asset.get("id", "") + logger.info( + f"XDR asset found: name={asset.get('name')} " + f"asset_id={asset_id} category={asset.get('category')}" + ) + return asset_id + + logger.warning(f"No XDR agent asset found for name={asset_name} (checked {len(assets)} assets)") + return None + except requests.exceptions.RequestException as e: + logger.error(f"XDR asset lookup failed for {asset_name}: {e}") + return None + def build_scope(self, account_id: str, site_id: Optional[str] = None) -> str: """Build the S1-Scope header value""" if site_id: diff --git a/Backend/api/app/services/destination_service.py b/Backend/api/app/services/destination_service.py index e386fa5..5efe349 100644 --- a/Backend/api/app/services/destination_service.py +++ b/Backend/api/app/services/destination_service.py @@ -76,6 +76,8 @@ async def create_destination( config_api_url: Optional[str] = None, config_write_token: Optional[str] = None, powerquery_read_token: Optional[str] = None, + s1_management_url: Optional[str] = None, + s1_api_token: Optional[str] = None, uam_ingest_url: Optional[str] = None, uam_account_id: Optional[str] = None, uam_site_id: Optional[str] = None, @@ -95,6 +97,8 @@ async def create_destination( config_api_url: Config API URL for parser management (e.g., https://xdr.us1.sentinelone.net) config_write_token: Config API token for reading and writing parsers (will be encrypted) powerquery_read_token: PowerQuery Log Read Access token for querying SIEM data (will be encrypted) + s1_management_url: S1 Management API URL for asset lookups (e.g., https://demo.sentinelone.net) + s1_api_token: S1 API Token for management API calls (will be encrypted) ip: Syslog IP (for syslog destinations) port: Syslog port (for syslog destinations) protocol: 'UDP' or 'TCP' (for syslog destinations) @@ -137,6 +141,10 @@ async def create_destination( destination.config_write_token_encrypted = self.encryption.encrypt(config_write_token) if powerquery_read_token: destination.powerquery_read_token_encrypted = self.encryption.encrypt(powerquery_read_token) + if s1_management_url: + destination.s1_management_url = s1_management_url.rstrip('/') + if s1_api_token: + destination.s1_api_token_encrypted = self.encryption.encrypt(s1_api_token) if uam_ingest_url: destination.uam_ingest_url = uam_ingest_url.rstrip('/') if uam_account_id: @@ -185,6 +193,8 @@ async def update_destination( config_api_url: Optional[str] = None, config_write_token: Optional[str] = None, powerquery_read_token: Optional[str] = None, + s1_management_url: Optional[str] = None, + s1_api_token: Optional[str] = None, uam_ingest_url: Optional[str] = None, uam_account_id: Optional[str] = None, uam_site_id: Optional[str] = None, @@ -212,6 +222,10 @@ async def update_destination( destination.config_write_token_encrypted = self.encryption.encrypt(config_write_token) if powerquery_read_token: destination.powerquery_read_token_encrypted = self.encryption.encrypt(powerquery_read_token) + if s1_management_url is not None: + destination.s1_management_url = s1_management_url.rstrip('/') if s1_management_url else None + if s1_api_token: + destination.s1_api_token_encrypted = self.encryption.encrypt(s1_api_token) if uam_ingest_url: destination.uam_ingest_url = uam_ingest_url.rstrip('/') if uam_account_id: diff --git a/Backend/scenarios/apollo_ransomware_scenario.py b/Backend/scenarios/apollo_ransomware_scenario.py index 96f6c08..50d0964 100644 --- a/Backend/scenarios/apollo_ransomware_scenario.py +++ b/Backend/scenarios/apollo_ransomware_scenario.py @@ -301,11 +301,19 @@ def send_phase_alert( alert["metadata"]["logged_time"] = time_ms alert["metadata"]["modified_time"] = time_ms - # Set user as the resource (uid must be a UUID for site-scoped alerts) - alert["resources"] = [{ - "uid": str(uuid.uuid4()), - "name": VICTIM_PROFILE["email"] - }] + # 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: + alert["resources"] = [{ + "uid": str(uuid.uuid4()), + "name": VICTIM_PROFILE["email"] + }] # Apply overrides overrides = mapping.get("overrides", {}) @@ -673,6 +681,47 @@ def generate_apollo_ransomware_scenario(siem_context: Optional[Dict] = None) -> 'uam_service_token': uam_service_token, 'uam_site_id': uam_site_id, } + + # Look up bridge XDR Asset ID for linking alerts to real endpoint + 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...") + 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 + lookup_url = f"{s1_mgmt_url.rstrip('/')}/web/api/v2.1/xdr/assets?{urllib.parse.urlencode(params)}" + req = urllib.request.Request(lookup_url, headers={ + "Authorization": f"ApiToken {s1_api_token}", + "Content-Type": "application/json", + }) + 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 + 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: + print(f" ⚠ No XDR agent asset found for '{bridge_name}'") + print(f" Found {len(assets)} total assets") + except Exception as e: + print(f" ⚠ XDR asset lookup failed: {e}") + print("\n🚨 ALERT DETONATION ENABLED") print(f" UAM Ingest: {uam_ingest_url}") print(f" Account ID: {uam_account_id}") diff --git a/Backend/test_alert_site.py b/Backend/test_alert_site.py new file mode 100644 index 0000000..f6b367d --- /dev/null +++ b/Backend/test_alert_site.py @@ -0,0 +1,392 @@ +#!/usr/bin/env python3 +""" +Test script for sending UAM alerts to SentinelOne at site scope. +Used for experimenting with resource/asset fields and payload structure. + +Usage: + python3 test_alert_site.py --template api/app/alerts/templates/advanced_sample_alert.json + python3 test_alert_site.py --minimal + python3 test_alert_site.py --minimal --resource-name "jeanluc@starfleet.com" --resource-type "user" + python3 test_alert_site.py --minimal --resource-name "bridge-workstation" --resource-type "endpoint" +""" + +import argparse +import gzip +import json +import os +import sys +import uuid +import time +from datetime import datetime, timezone +from urllib.request import Request, urlopen +from urllib.error import HTTPError, URLError + + +def load_template(path: str) -> dict: + """Load a JSON alert template and inject dynamic fields.""" + with open(path) as f: + alert = json.load(f) + + now_ms = int(datetime.now(timezone.utc).timestamp() * 1000) + + # Inject fresh finding UID + if "finding_info" not in alert: + alert["finding_info"] = {} + alert["finding_info"]["uid"] = str(uuid.uuid4()) + + # Inject timestamps + alert["time"] = now_ms + if "metadata" not in alert: + alert["metadata"] = {} + alert["metadata"]["logged_time"] = now_ms + alert["metadata"]["modified_time"] = now_ms + + # Generate UIDs for related events + if "related_events" in alert.get("finding_info", {}): + for event in alert["finding_info"]["related_events"]: + event["uid"] = str(uuid.uuid4()) + + # Generate UIDs for resources with placeholder + for resource in alert.get("resources", []): + if resource.get("uid") == "DYNAMIC_RESOURCE_UID": + resource["uid"] = str(uuid.uuid4()) + + return alert + + +def build_minimal_alert( + title: str = "Test Alert", + desc: str = "Test alert from CLI script", + resource_name: str = "test-endpoint", + resource_uid: str = None, + resource_type: str = None, + severity: str = None, + severity_id: int = None, + product_name: str = "HELIOS Test", + vendor_name: str = "RoarinPenguin", + extra_resource_fields: dict = None, +) -> dict: + """Build a minimal OCSF alert payload for testing.""" + now_ms = int(datetime.now(timezone.utc).timestamp() * 1000) + + resource = { + "uid": resource_uid or str(uuid.uuid4()), + "name": resource_name, + } + if resource_type: + resource["type"] = resource_type + if extra_resource_fields: + resource.update(extra_resource_fields) + + alert = { + "finding_info": { + "uid": str(uuid.uuid4()), + "title": title, + "desc": desc, + }, + "resources": [resource], + "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": product_name, + "vendor_name": vendor_name, + }, + "logged_time": now_ms, + "modified_time": now_ms, + }, + "time": now_ms, + "attack_surface_ids": [1], + "severity_id": severity_id or 4, + "state_id": 1, + "s1_classification_id": 1, + } + + if severity: + alert["severity"] = severity + + return alert + + +def send_alert(alert: dict, ingest_url: str, token: str, account_id: str, site_id: str = None) -> dict: + """Send an alert via UAM ingest API. Returns response info.""" + url = ingest_url.rstrip("/") + "/v1/alerts" + scope = account_id + if site_id: + scope = f"{account_id}:{site_id}" + + payload = json.dumps(alert).encode("utf-8") + gzipped = gzip.compress(payload) + + headers = { + "Authorization": f"Bearer {token}", + "S1-Scope": scope, + "Content-Encoding": "gzip", + "Content-Type": "application/json", + "S1-Trace-Id": "helios-ingest-uam:alwayslog", + } + + req = Request(url, data=gzipped, headers=headers, method="POST") + try: + with urlopen(req) as resp: + body = resp.read().decode("utf-8") + return {"status": resp.status, "body": body} + except HTTPError as e: + body = e.read().decode("utf-8") + return {"status": e.code, "body": body, "error": str(e)} + except URLError as e: + return {"status": 0, "body": "", "error": str(e)} + + +def print_alert_summary(alert: dict, label: str = ""): + """Pretty-print alert summary.""" + if label: + print(f"\n{'='*60}") + print(f" {label}") + print(f"{'='*60}") + print(f" Title: {alert.get('finding_info', {}).get('title', 'N/A')}") + print(f" Desc: {alert.get('finding_info', {}).get('desc', 'N/A')[:80]}") + resources = alert.get("resources", []) + for i, r in enumerate(resources): + print(f" Resource[{i}]:") + for k, v in r.items(): + print(f" {k}: {v}") + print(f" Severity: {alert.get('severity', 'N/A')} (id={alert.get('severity_id', 'N/A')})") + ts = alert.get("time", 0) + print(f" Time: {ts} ({datetime.fromtimestamp(ts/1000, tz=timezone.utc).isoformat()})") + payload = json.dumps(alert).encode("utf-8") + gzipped = gzip.compress(payload) + print(f" Size: {len(payload)} bytes -> {len(gzipped)} bytes (gzip)") + print(f"\n Full JSON:\n{json.dumps(alert, indent=2)}") + + +def main(): + parser = argparse.ArgumentParser(description="Test UAM alert sending to SentinelOne") + parser.add_argument("--ingest-url", default=os.environ.get("UAM_INGEST_URL", "https://ingest.us1.sentinelone.net")) + parser.add_argument("--token", default=os.environ.get("UAM_TOKEN")) + parser.add_argument("--account-id", default=os.environ.get("UAM_ACCOUNT_ID", "1908275390083300395")) + parser.add_argument("--site-id", default=os.environ.get("UAM_SITE_ID", "2178041589156878742")) + parser.add_argument("--no-site", action="store_true", help="Send to account scope only") + + # Template mode + parser.add_argument("--template", help="Path to JSON alert template file") + + # Minimal mode + parser.add_argument("--minimal", action="store_true", help="Use minimal alert payload") + parser.add_argument("--title", default="Test Alert") + parser.add_argument("--desc", default="Test alert from CLI script") + parser.add_argument("--resource-name", default="test-endpoint") + parser.add_argument("--resource-uid", default=None, help="Resource UID (default: auto UUID)") + parser.add_argument("--resource-type", default=None, help="Resource type field") + parser.add_argument("--severity", default=None) + parser.add_argument("--severity-id", type=int, default=4) + parser.add_argument("--product", default="HELIOS Test") + parser.add_argument("--vendor", default="RoarinPenguin") + + # Batch experiment mode + parser.add_argument("--experiment", action="store_true", help="Run a batch of resource experiments") + parser.add_argument("--delay", type=float, default=3.0, help="Delay between sends (seconds)") + + # Output + parser.add_argument("--dry-run", action="store_true", help="Print payload but don't send") + parser.add_argument("--json-field", help="Add arbitrary JSON field as key=value (can repeat)", action="append", default=[]) + + args = parser.parse_args() + + if not args.token: + print("ERROR: No token. Set UAM_TOKEN env var or use --token") + sys.exit(1) + + site_id = None if args.no_site else args.site_id + + if args.experiment: + run_experiments(args, site_id) + return + + # Build alert + if args.template: + alert = load_template(args.template) + label = f"Template: {args.template}" + elif args.minimal: + alert = build_minimal_alert( + title=args.title, + desc=args.desc, + resource_name=args.resource_name, + resource_uid=args.resource_uid, + resource_type=args.resource_type, + severity=args.severity, + severity_id=args.severity_id, + product_name=args.product, + vendor_name=args.vendor, + ) + label = "Minimal Alert" + else: + print("ERROR: Specify --template or --minimal") + sys.exit(1) + + # Apply extra JSON fields + for field in args.json_field: + key, _, value = field.partition("=") + keys = key.split(".") + current = alert + for k in keys[:-1]: + if k not in current: + current[k] = {} + current = current[k] + # Try to parse as JSON, fall back to string + try: + current[keys[-1]] = json.loads(value) + except (json.JSONDecodeError, ValueError): + current[keys[-1]] = value + + print_alert_summary(alert, label) + + if args.dry_run: + print("\n [DRY RUN - not sending]") + return + + print(f"\n Sending to {'site' if site_id else 'account'} scope...") + result = send_alert(alert, args.ingest_url, args.token, args.account_id, site_id) + print(f" Response: {result['status']} {result['body']}") + if result.get("error"): + print(f" Error: {result['error']}") + + +def run_experiments(args, site_id): + """Run a batch of resource field experiments to see what lands in SDL.""" + # Real bridge agent identifiers from S1 console: + # Agent UUID: a0a693e2-f325-4a47-a80e-798f97bbd96d + # Agent Asset ID: eimvmdpvax6mtmbpdbxtoaem5q + # UAM Asset ID (created by alerts): ivhhhtkbinovgccxxjk53zwnje + # Serial: 02J2ZH-PBPTK27Z + # Domain: STARFLEET + # GW IP: 206.198.150.53 + AGENT_UUID = "a0a693e2-f325-4a47-a80e-798f97bbd96d" + AGENT_ASSET_ID = "eimvmdpvax6mtmbpdbxtoaem5q" + UAM_ASSET_ID = "ivhhhtkbinovgccxxjk53zwnje" + SERIAL = "02J2ZH-PBPTK27Z" + + experiments = [ + { + "label": "E1: Agent UUID as resource uid", + "resource_name": "bridge", + "resource_uid": AGENT_UUID, + "resource_type": None, + "extra": {}, + }, + { + "label": "E2: Agent Asset ID as resource uid", + "resource_name": "bridge", + "resource_uid": AGENT_ASSET_ID, + "resource_type": None, + "extra": {}, + }, + { + "label": "E3: UAM Asset ID as resource uid", + "resource_name": "bridge", + "resource_uid": UAM_ASSET_ID, + "resource_type": None, + "extra": {}, + }, + { + "label": "E4: Agent UUID + type=endpoint", + "resource_name": "bridge", + "resource_uid": AGENT_UUID, + "resource_type": "endpoint", + "extra": {}, + }, + { + "label": "E5: Agent UUID + serial_number", + "resource_name": "bridge", + "resource_uid": AGENT_UUID, + "resource_type": None, + "extra": {"serial_number": SERIAL}, + }, + { + "label": "E6: Agent UUID + domain + ip", + "resource_name": "bridge", + "resource_uid": AGENT_UUID, + "resource_type": None, + "extra": {"domain": "STARFLEET", "ip": "206.198.150.53"}, + }, + { + "label": "E7: Agent UUID + agent_id field", + "resource_name": "bridge", + "resource_uid": AGENT_UUID, + "resource_type": None, + "extra": {"agent_id": AGENT_UUID}, + }, + { + "label": "E8: UAM Asset ID + type=endpoint + name=bridge", + "resource_name": "bridge", + "resource_uid": UAM_ASSET_ID, + "resource_type": "endpoint", + "extra": {}, + }, + { + "label": "E9: Random UUID + agent_id=Agent UUID", + "resource_name": "bridge", + "resource_uid": None, + "resource_type": None, + "extra": {"agent_id": AGENT_UUID}, + }, + { + "label": "E10: Random UUID + ext.s1.agent_id", + "resource_name": "bridge", + "resource_uid": None, + "resource_type": None, + "extra": {"ext": {"s1": {"agent_id": AGENT_UUID}}}, + }, + ] + + print(f"\n{'='*60}") + print(f" RESOURCE FIELD EXPERIMENTS ({len(experiments)} tests)") + print(f" Scope: {'site ' + site_id if site_id else 'account'}") + print(f" Delay: {args.delay}s between sends") + print(f"{'='*60}") + + for i, exp in enumerate(experiments): + alert = build_minimal_alert( + title=f"Asset Test - {exp['label']}", + desc=f"Testing resource fields: {exp['label']}", + resource_name=exp["resource_name"], + resource_uid=exp["resource_uid"], + resource_type=exp.get("resource_type"), + severity_id=4, + extra_resource_fields=exp.get("extra", {}), + ) + + print(f"\n--- {exp['label']} ---") + print(f" Resources: {json.dumps(alert['resources'], indent=4)}") + + if args.dry_run: + print(" [DRY RUN]") + else: + result = send_alert(alert, args.ingest_url, args.token, args.account_id, site_id) + print(f" Response: {result['status']} {result['body']}") + if result.get("error"): + print(f" Error: {result['error']}") + + if i < len(experiments) - 1 and not args.dry_run: + print(f" Waiting {args.delay}s...") + time.sleep(args.delay) + + print(f"\n{'='*60}") + print(f" Done! Check SDL for {len(experiments)} alerts.") + print(f" Search: tag = 'alert' in the last 15 minutes") + print(f"{'='*60}") + + +if __name__ == "__main__": + main() diff --git a/Frontend/log_generator_ui.py b/Frontend/log_generator_ui.py index ffde8e1..04b1645 100644 --- a/Frontend/log_generator_ui.py +++ b/Frontend/log_generator_ui.py @@ -248,6 +248,10 @@ def update_destination(dest_id): payload['config_write_token'] = data['config_write_token'] if data.get('powerquery_read_token'): payload['powerquery_read_token'] = data['powerquery_read_token'] + if 's1_management_url' in data: + payload['s1_management_url'] = data['s1_management_url'] + if data.get('s1_api_token'): + payload['s1_api_token'] = data['s1_api_token'] if data.get('uam_ingest_url'): payload['uam_ingest_url'] = data['uam_ingest_url'] if data.get('uam_account_id'): @@ -532,6 +536,69 @@ def execute_correlation_query(): return jsonify({'error': str(e), 'results': []}), 500 +@app.route('/xdr/assets', methods=['POST']) +def get_xdr_assets(): + """Fetch XDR assets from S1 management API via destination credentials""" + try: + data = request.get_json(silent=True) or {} + destination_id = data.get('destination_id') + if not destination_id: + return jsonify({'error': 'No destination_id provided'}), 400 + + # Resolve S1 API token from destination + s1_resp = requests.get( + f"{API_BASE_URL}/api/v1/destinations/{destination_id}/s1-api-token", + headers=_get_api_headers(), + timeout=10 + ) + if s1_resp.status_code != 200: + return jsonify({'error': 'No S1 API Token configured for this destination. Go to Settings → Edit Destination.'}), 400 + s1_data = s1_resp.json() + s1_token = s1_data.get('token', '') + s1_mgmt_url = s1_data.get('s1_management_url', '') + + if not s1_token or not s1_mgmt_url: + return jsonify({'error': 'S1 Management URL or API Token missing'}), 400 + + # Get UAM account/site IDs from destination for scoping + dest_resp = requests.get( + f"{API_BASE_URL}/api/v1/destinations/{destination_id}", + headers=_get_api_headers(), + timeout=10 + ) + if dest_resp.status_code != 200: + return jsonify({'error': 'Failed to fetch destination details'}), 400 + dest = dest_resp.json() + + params = {} + if dest.get('uam_account_id'): + params['accountIds'] = dest['uam_account_id'] + if dest.get('uam_site_id'): + params['siteIds'] = dest['uam_site_id'] + + # Call XDR assets API + xdr_url = f"{s1_mgmt_url.rstrip('/')}/web/api/v2.1/xdr/assets" + xdr_resp = requests.get( + xdr_url, + headers={ + "Authorization": f"ApiToken {s1_token}", + "Content-Type": "application/json", + }, + params=params, + timeout=20 + ) + xdr_resp.raise_for_status() + assets_data = xdr_resp.json() + + return jsonify(assets_data) + except requests.exceptions.RequestException as e: + logger.error(f"XDR assets API call failed: {e}") + return jsonify({'error': f'XDR assets API call failed: {str(e)}'}), 500 + except Exception as e: + logger.error(f"Failed to fetch XDR assets: {e}") + return jsonify({'error': str(e)}), 500 + + @app.route('/correlation-scenarios/run', methods=['POST']) def run_correlation_scenario(): """Execute a correlation scenario with SIEM context""" @@ -732,6 +799,22 @@ def generate_and_stream(): env['UAM_SERVICE_TOKEN'] = uam_service_token if uam_site_id: env['UAM_SITE_ID'] = uam_site_id + # Resolve S1 API token for asset lookups + try: + s1_resp = requests.get( + f"{API_BASE_URL}/api/v1/destinations/{destination_id}/s1-api-token", + headers=_get_api_headers(), + timeout=10 + ) + if s1_resp.status_code == 200: + s1_data = s1_resp.json() + env['S1_MANAGEMENT_URL'] = s1_data.get('s1_management_url', '') + env['S1_API_TOKEN'] = s1_data.get('token', '') + yield "INFO: šŸ”— S1 asset linking enabled (API token found)\n" + else: + yield "INFO: āš ļø S1 asset linking disabled (no S1 API token on destination)\n" + except Exception as s1e: + logger.warning(f"Could not resolve S1 API token: {s1e}") yield "INFO: 🚨 Alert detonation enabled (UAM credentials found)\n" else: yield "INFO: āš ļø Alert detonation disabled (no UAM credentials on destination)\n" @@ -1130,6 +1213,22 @@ def generate_and_stream(): env['UAM_SERVICE_TOKEN'] = uam_service_token if uam_site_id: env['UAM_SITE_ID'] = uam_site_id + # Resolve S1 API token for asset lookups + try: + s1_resp = requests.get( + f"{API_BASE_URL}/api/v1/destinations/{destination_id}/s1-api-token", + headers=_get_api_headers(), + timeout=10 + ) + if s1_resp.status_code == 200: + s1_data = s1_resp.json() + env['S1_MANAGEMENT_URL'] = s1_data.get('s1_management_url', '') + env['S1_API_TOKEN'] = s1_data.get('token', '') + yield "INFO: šŸ”— S1 asset linking enabled (API token found)\n" + else: + yield "INFO: āš ļø S1 asset linking disabled (no S1 API token on destination)\n" + except Exception as s1e: + logger.warning(f"Could not resolve S1 API token: {s1e}") yield "INFO: 🚨 Alert detonation enabled (UAM credentials found)\n" else: yield "INFO: āš ļø Alert detonation disabled (no UAM credentials on destination)\n" diff --git a/Frontend/templates/log_generator.html b/Frontend/templates/log_generator.html index 0afa7ea..1edb190 100644 --- a/Frontend/templates/log_generator.html +++ b/Frontend/templates/log_generator.html @@ -733,12 +733,26 @@

šŸ” Run Query + + + +

Configure a Service Account token and scope for sending UAM alerts. This is separate from HEC.

+
+ + +

Management console URL — used for asset lookups to link alerts to endpoints

+
+
+ +
+ + +
+

For agent/asset lookups — generate from S1 Console → Settings → Users → API Token

+
@@ -1224,6 +1251,19 @@

Parser Sync & PowerQuery C

UAM Alert Configuration

+
+ + +

Management console URL — used for asset lookups to link alerts to endpoints

+
+
+ +
+ + +
+

For agent/asset lookups — generate from S1 Console → Settings → Users → API Token

+
@@ -1540,6 +1580,7 @@

Select Parser Version

document.getElementById('edit-dest-token').value = ''; document.getElementById('edit-config-api-url').value = d.config_api_url || 'https://xdr.us1.sentinelone.net'; document.getElementById('edit-config-write-token').value = ''; + document.getElementById('edit-s1-management-url').value = d.s1_management_url || ''; document.getElementById('edit-uam-ingest-url').value = d.uam_ingest_url || 'https://ingest.us1.sentinelone.net'; document.getElementById('edit-uam-account-id').value = d.uam_account_id || ''; document.getElementById('edit-uam-site-id').value = d.uam_site_id || ''; @@ -1685,7 +1726,11 @@

Select Parser Version

if (configWriteToken) payload.config_write_token = configWriteToken; if (powerqueryReadToken) payload.powerquery_read_token = powerqueryReadToken; - // Add UAM alert settings (optional) + // Add S1 Management URL, API Token, and UAM alert settings (optional) + const s1ManagementUrl = document.getElementById('dest-s1-management-url')?.value?.trim(); + if (s1ManagementUrl) payload.s1_management_url = s1ManagementUrl; + const s1ApiToken = document.getElementById('dest-s1-api-token')?.value?.trim(); + if (s1ApiToken) payload.s1_api_token = s1ApiToken; const uamIngestUrl = document.getElementById('dest-uam-ingest-url')?.value?.trim(); const uamAccountId = document.getElementById('dest-uam-account-id')?.value?.trim(); const uamSiteId = document.getElementById('dest-uam-site-id')?.value?.trim(); @@ -2093,6 +2138,8 @@

Select Parser Version

const configApiUrl = document.getElementById('edit-config-api-url').value.trim(); const configWriteToken = document.getElementById('edit-config-write-token').value.trim(); const powerqueryReadToken = document.getElementById('edit-powerquery-read-token').value.trim(); + const s1ManagementUrl = document.getElementById('edit-s1-management-url').value.trim(); + const s1ApiToken = document.getElementById('edit-s1-api-token').value.trim(); const uamIngestUrl = document.getElementById('edit-uam-ingest-url').value.trim(); const uamAccountId = document.getElementById('edit-uam-account-id').value.trim(); const uamSiteId = document.getElementById('edit-uam-site-id').value.trim(); @@ -2106,6 +2153,8 @@

Select Parser Version

if (configApiUrl) payload.config_api_url = configApiUrl; if (configWriteToken) payload.config_write_token = configWriteToken; if (powerqueryReadToken) payload.powerquery_read_token = powerqueryReadToken; + payload.s1_management_url = s1ManagementUrl; // Allow clearing with empty string + if (s1ApiToken) payload.s1_api_token = s1ApiToken; if (uamIngestUrl) payload.uam_ingest_url = uamIngestUrl; if (uamAccountId) payload.uam_account_id = uamAccountId; payload.uam_site_id = uamSiteId; // Allow clearing with empty string @@ -2885,6 +2934,10 @@

Select Parser Version

// Config API URL and PowerQuery token are now resolved from the selected destination const genCorrelationTraceIdBtn = document.getElementById('gen-correlation-trace-id'); const correlationTraceIdInput = document.getElementById('correlation-trace-id'); + const getXdrAssetsBtn = document.getElementById('get-xdr-assets-btn'); + const xdrAssetsPanel = document.getElementById('xdr-assets-panel'); + const xdrAssetsList = document.getElementById('xdr-assets-list'); + const xdrAssetsCount = document.getElementById('xdr-assets-count'); let correlationScenarios = []; let currentCorrelationScenario = null; @@ -2941,15 +2994,19 @@

Select Parser Version

runCorrelationQueryBtn.disabled = false; runCorrelationFallbackBtn.disabled = false; runCorrelationScenarioBtn.disabled = true; // Enabled after query + if (getXdrAssetsBtn) getXdrAssetsBtn.disabled = false; correlationAnchorsPanel.style.display = 'none'; correlationAnchorsList.innerHTML = ''; + if (xdrAssetsPanel) xdrAssetsPanel.style.display = 'none'; } else { correlationScenarioDetails.style.display = 'none'; correlationQuery.value = ''; runCorrelationQueryBtn.disabled = true; runCorrelationFallbackBtn.disabled = true; runCorrelationScenarioBtn.disabled = true; + if (getXdrAssetsBtn) getXdrAssetsBtn.disabled = true; + if (xdrAssetsPanel) xdrAssetsPanel.style.display = 'none'; } }); @@ -2960,6 +3017,91 @@

Select Parser Version

} }); + // Get XDR Assets + if (getXdrAssetsBtn) { + getXdrAssetsBtn.addEventListener('click', async () => { + const destinationId = destSelect.value; + if (!destinationId) { + alert('Please select a destination'); + return; + } + + getXdrAssetsBtn.disabled = true; + getXdrAssetsBtn.textContent = 'šŸ”„ Loading...'; + correlationOutputBox.innerText = 'Fetching XDR assets from S1 management API...\n'; + + try { + const res = await fetch('/xdr/assets', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ destination_id: destinationId }) + }); + + const data = await res.json(); + + if (data.error) { + correlationOutputBox.innerText += `\nāŒ ${data.error}\n`; + if (xdrAssetsPanel) xdrAssetsPanel.style.display = 'none'; + return; + } + + const assets = data.data || []; + correlationOutputBox.innerText += `āœ“ Found ${assets.length} assets\n`; + + if (xdrAssetsPanel && xdrAssetsList && xdrAssetsCount) { + xdrAssetsPanel.style.display = ''; + xdrAssetsCount.textContent = `${assets.length} assets`; + xdrAssetsList.innerHTML = ''; + + // Separate agent assets from device-only assets + const agentAssets = assets.filter(a => a.agent); + const deviceAssets = assets.filter(a => !a.agent); + + agentAssets.forEach(asset => { + const div = document.createElement('div'); + div.className = 'flex items-center gap-2 p-2 bg-[#1a1629] rounded border border-[#3c325c]'; + const ip = asset.agent?.lastReportedIp || ''; + const version = asset.agent?.agentVersion || ''; + const status = asset.agent?.networkStatus === 'connected' ? '🟢' : 'šŸ”“'; + div.innerHTML = ` + ${status} + ${asset.name || '?'} + | + ${asset.category || ''} + | + ${asset.id} + ${ip ? `| ${ip}` : ''} + ${version ? `v${version}` : ''} + `; + xdrAssetsList.appendChild(div); + }); + + deviceAssets.forEach(asset => { + const div = document.createElement('div'); + div.className = 'flex items-center gap-2 p-2 bg-[#1a1629] rounded border border-[#2a2040] opacity-60'; + div.innerHTML = ` + šŸ“± + ${asset.name || '?'} + | + ${asset.resourceType || asset.category || 'Device'} + | + ${asset.id} + `; + xdrAssetsList.appendChild(div); + }); + + correlationOutputBox.innerText += ` ${agentAssets.length} agent assets (real endpoints)\n`; + correlationOutputBox.innerText += ` ${deviceAssets.length} device assets (created by alerts)\n`; + } + } catch (err) { + correlationOutputBox.innerText += `\nāŒ Error: ${err.message}\n`; + } finally { + getXdrAssetsBtn.disabled = false; + getXdrAssetsBtn.textContent = 'šŸ–„ļø Get Assets'; + } + }); + } + // Generate trace ID if (genCorrelationTraceIdBtn && correlationTraceIdInput) { genCorrelationTraceIdBtn.addEventListener('click', () => { 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 8/8] 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 = {