Skip to content

Commit a0e12dc

Browse files
authored
Apollo fixes 2 (#70)
* 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 * 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 * 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
1 parent cace214 commit a0e12dc

6 files changed

Lines changed: 143 additions & 18 deletions

File tree

Backend/api/app/routers/scenarios.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import asyncio
77
import time
88
import json
9+
import sys
910
from datetime import datetime, timedelta
1011
from pathlib import Path as PathLib
1112

@@ -29,6 +30,17 @@ class SIEMQueryRequest(BaseModel):
2930
end_time_hours: int = 0 # How far back to end (default now)
3031
anchor_configs: Optional[List[Dict[str, Any]]] = None
3132

33+
34+
class CorrelationRunRequest(BaseModel):
35+
"""Request model for correlation scenario execution"""
36+
scenario_id: str
37+
destination_id: str
38+
siem_context: Optional[Dict[str, Any]] = None
39+
trace_id: Optional[str] = None
40+
tag_phase: bool = True
41+
tag_trace: bool = True
42+
workers: int = 10
43+
3244
# Initialize scenario service
3345
scenario_service = ScenarioService()
3446

@@ -278,6 +290,56 @@ async def execute_siem_query(
278290
raise HTTPException(status_code=500, detail=str(e))
279291

280292

293+
@router.post("/correlation/run", response_model=BaseResponse)
294+
async def run_correlation_scenario(
295+
request: CorrelationRunRequest,
296+
background_tasks: BackgroundTasks,
297+
_: str = Depends(require_write_access)
298+
):
299+
"""
300+
Execute a correlation scenario with SIEM context and trace ID support
301+
"""
302+
try:
303+
# Validate scenario exists and supports correlation
304+
scenarios_dir = PathLib(__file__).parent.parent.parent / "scenarios"
305+
if str(scenarios_dir) not in sys.path:
306+
sys.path.insert(0, str(scenarios_dir))
307+
308+
scenario_modules = {
309+
"apollo_ransomware_scenario": "apollo_ransomware_scenario"
310+
}
311+
312+
if request.scenario_id not in scenario_modules:
313+
raise HTTPException(status_code=404, detail=f"Correlation scenario '{request.scenario_id}' not found")
314+
315+
# Start correlation scenario execution with trace ID
316+
execution_id = await scenario_service.start_correlation_scenario(
317+
scenario_id=request.scenario_id,
318+
siem_context=request.siem_context or {},
319+
trace_id=request.trace_id,
320+
tag_phase=request.tag_phase,
321+
tag_trace=request.tag_trace,
322+
background_tasks=background_tasks
323+
)
324+
325+
return BaseResponse(
326+
success=True,
327+
data={
328+
"execution_id": execution_id,
329+
"scenario_id": request.scenario_id,
330+
"status": "started",
331+
"trace_id": request.trace_id,
332+
"tag_phase": request.tag_phase,
333+
"tag_trace": request.tag_trace,
334+
"started_at": datetime.utcnow().isoformat()
335+
}
336+
)
337+
except HTTPException:
338+
raise
339+
except Exception as e:
340+
raise HTTPException(status_code=500, detail=str(e))
341+
342+
281343
# =============================================================================
282344
# GENERIC SCENARIO ENDPOINTS (catch-all /{scenario_id} routes must be last)
283345
# =============================================================================

Backend/api/app/services/scenario_service.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
import uuid
66
import time
77
import asyncio
8+
import json
89
from datetime import datetime
910
import logging
1011
import os
1112
import sys
1213
import importlib
13-
import json
1414
from pathlib import Path
1515

1616
logger = logging.getLogger(__name__)
@@ -264,11 +264,14 @@ async def start_correlation_scenario(
264264
self,
265265
scenario_id: str,
266266
siem_context: Dict[str, Any],
267+
trace_id: Optional[str] = None,
268+
tag_phase: bool = True,
269+
tag_trace: bool = True,
267270
speed: str = "fast",
268271
dry_run: bool = False,
269272
background_tasks=None
270273
) -> str:
271-
"""Start correlation scenario execution with SIEM context"""
274+
"""Start correlation scenario execution with SIEM context and trace ID support"""
272275
execution_id = str(uuid.uuid4())
273276

274277
self.running_scenarios[execution_id] = {
@@ -279,16 +282,35 @@ async def start_correlation_scenario(
279282
"speed": speed,
280283
"dry_run": dry_run,
281284
"siem_context": siem_context,
285+
"trace_id": trace_id,
286+
"tag_phase": tag_phase,
287+
"tag_trace": tag_trace,
282288
"progress": 0
283289
}
284290

285291
if background_tasks:
286-
background_tasks.add_task(self._execute_correlation_scenario, execution_id, scenario_id, siem_context)
292+
background_tasks.add_task(
293+
self._execute_correlation_scenario,
294+
execution_id,
295+
scenario_id,
296+
siem_context,
297+
trace_id,
298+
tag_phase,
299+
tag_trace
300+
)
287301

288302
return execution_id
289303

290-
async def _execute_correlation_scenario(self, execution_id: str, scenario_id: str, siem_context: Dict[str, Any]):
291-
"""Execute correlation scenario with SIEM context"""
304+
async def _execute_correlation_scenario(
305+
self,
306+
execution_id: str,
307+
scenario_id: str,
308+
siem_context: Dict[str, Any],
309+
trace_id: Optional[str] = None,
310+
tag_phase: bool = True,
311+
tag_trace: bool = True
312+
):
313+
"""Execute correlation scenario with SIEM context and trace ID support"""
292314
import sys
293315
import os
294316
from pathlib import Path
@@ -303,6 +325,12 @@ async def _execute_correlation_scenario(self, execution_id: str, scenario_id: st
303325
siem_context_json = json.dumps(siem_context)
304326
os.environ['SIEM_CONTEXT'] = siem_context_json
305327

328+
# Set trace ID and tagging environment variables
329+
if trace_id:
330+
os.environ['S1_TRACE_ID'] = trace_id
331+
os.environ['S1_TAG_PHASE'] = '1' if tag_phase else '0'
332+
os.environ['S1_TAG_TRACE'] = '1' if tag_trace else '0'
333+
306334
# Import and run the scenario
307335
module = __import__(scenario_id)
308336
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
321349
self.running_scenarios[execution_id]["error"] = str(e)
322350
self.running_scenarios[execution_id]["completed_at"] = datetime.utcnow().isoformat()
323351
finally:
324-
# Clean up environment variable
352+
# Clean up environment variables
325353
os.environ.pop('SIEM_CONTEXT', None)
354+
if trace_id:
355+
os.environ.pop('S1_TRACE_ID', None)
356+
os.environ.pop('S1_TAG_PHASE', None)
357+
os.environ.pop('S1_TAG_TRACE', None)
326358

327359
async def _execute_scenario(self, execution_id: str, scenario: Dict[str, Any]):
328360
"""Execute scenario in background"""

Backend/parsers/community/microsoft_365_collaboration-latest/microsoft_365_collaboration.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"attributes": {
3-
"dataSource.name": "Microsoft 365 Collaboration",
3+
"dataSource.name": "Microsoft O365",
44
"dataSource.vendor": "Microsoft",
55
"dataSource.category": "security",
66
"metadata.product.name": "Microsoft 365 SharePoint/OneDrive",

Backend/scenarios/apollo_ransomware_scenario.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@
8888
"name": "Apollo Ransomware - STARFLEET Attack",
8989
"description": "Correlates Proofpoint and M365 events with existing EDR/WEL data for the Apollo ransomware attack chain targeting STARFLEET.",
9090

91-
"default_query": """dataSource.name in ('SentinelOne','Windows Event Logs') endpoint.name contains ("Enterprise", "bridge")
91+
"default_query": """dataSource.name in ('SentinelOne','Windows Event Logs') endpoint.name contains ("Enterprise", "bridge") AND (winEventLog.id = 4698 or * contains "apollo")
92+
9293
| group newest_timestamp = newest(timestamp), oldest_timestamp = oldest(timestamp) by event.type, src.process.user, endpoint.name, src.endpoint.ip.address, dst.ip.address
9394
| sort newest_timestamp
9495
| 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]:
450451
"""Generate M365 events for email access and attachment download"""
451452
events = []
452453

453-
# Email accessed - 5 minutes after delivery (user checks email)
454-
email_access_time = get_scenario_time(base_time, 5)
454+
# Email accessed - 30 seconds after Proofpoint delivery
455+
email_access_time = get_scenario_time(base_time, 0, 30)
455456
m365_email_access = microsoft_365_collaboration_log()
456457
m365_email_access['TimeStamp'] = email_access_time
457458
m365_email_access['UserId'] = VICTIM_PROFILE['email']
@@ -465,10 +466,12 @@ def generate_m365_email_interaction(base_time: datetime) -> List[Dict]:
465466
m365_email_access['SiteUrl'] = f"https://outlook.office365.com/mail/inbox"
466467
# Parser-mapped fields for OCSF synthetic columns
467468
m365_email_access['RequestedBy'] = VICTIM_PROFILE['name'] # -> actor.user.name
469+
m365_email_access['user.email_addr'] = VICTIM_PROFILE['email'] # -> actor.user.email_addr
470+
m365_email_access['object_id'] = f"/Inbox/{ATTACKER_PROFILE['malicious_xlsx']}" # -> object_id for mail items analysis
468471
events.append(create_event(email_access_time, "microsoft_365_collaboration", "email_interaction", m365_email_access))
469472

470-
# Attachment preview/download - 6 minutes after delivery
471-
attachment_time = get_scenario_time(base_time, 6)
473+
# Attachment preview/download - 30 seconds after MailItemsAccessed (1 min after delivery)
474+
attachment_time = get_scenario_time(base_time, 1)
472475
m365_attachment = microsoft_365_collaboration_log()
473476
m365_attachment['TimeStamp'] = attachment_time
474477
m365_attachment['UserId'] = VICTIM_PROFILE['email']
@@ -481,10 +484,12 @@ def generate_m365_email_interaction(base_time: datetime) -> List[Dict]:
481484
m365_attachment['SiteUrl'] = f"https://outlook.office365.com/mail/inbox"
482485
# Parser-mapped fields for OCSF synthetic columns
483486
m365_attachment['RequestedBy'] = VICTIM_PROFILE['name'] # -> actor.user.name
487+
m365_attachment['user.email_addr'] = VICTIM_PROFILE['email'] # -> actor.user.email_addr
488+
m365_attachment['object_id'] = f"/Attachments/{ATTACKER_PROFILE['malicious_xlsx']}" # -> object_id for mail items analysis
484489
events.append(create_event(attachment_time, "microsoft_365_collaboration", "email_interaction", m365_attachment))
485490

486-
# File opened in Excel Online / locally - 7 minutes after delivery
487-
file_open_time = get_scenario_time(base_time, 7)
491+
# File opened in Excel Online / locally - 30 seconds after FileDownloaded (1 min 30 sec after delivery)
492+
file_open_time = get_scenario_time(base_time, 1, 30)
488493
m365_file_open = microsoft_365_collaboration_log()
489494
m365_file_open['TimeStamp'] = file_open_time
490495
m365_file_open['UserId'] = VICTIM_PROFILE['email']
@@ -497,6 +502,8 @@ def generate_m365_email_interaction(base_time: datetime) -> List[Dict]:
497502
m365_file_open['SiteUrl'] = f"https://starfleet-my.sharepoint.com/personal/{VICTIM_PROFILE['username']}"
498503
# Parser-mapped fields for OCSF synthetic columns
499504
m365_file_open['RequestedBy'] = VICTIM_PROFILE['name'] # -> actor.user.name
505+
m365_file_open['user.email_addr'] = VICTIM_PROFILE['email'] # -> actor.user.email_addr
506+
m365_file_open['object_id'] = f"/Documents/{ATTACKER_PROFILE['malicious_xlsx']}" # -> object_id for mail items analysis
500507
events.append(create_event(file_open_time, "microsoft_365_collaboration", "file_access", m365_file_open))
501508

502509
return events
@@ -785,9 +792,15 @@ def send_to_hec(event_data: dict, event_type: str, trace_id: str = None, phase:
785792

786793
product = type_to_product.get(event_type, event_type)
787794

795+
# Special handling for dataSource.name to match expected values
796+
if event_type == "microsoft_365_collaboration":
797+
data_source_name = "Microsoft O365"
798+
else:
799+
data_source_name = event_type.replace('_', ' ').title()
800+
788801
attr_fields = {
789802
"dataSource.vendor": event_type.split('_')[0].title() if '_' in event_type else event_type.title(),
790-
"dataSource.name": event_type.replace('_', ' ').title(),
803+
"dataSource.name": data_source_name,
791804
"dataSource.category": "security"
792805
}
793806

Backend/utilities/parsers/community_new/ai-siem-main/parsers/community/microsoft_365_collaboration-latest/microsoft_365_collaboration.conf

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"attributes": {
3-
"dataSource.name": "Microsoft 365 Collaboration",
3+
"dataSource.name": "Microsoft O365",
44
"dataSource.vendor": "Microsoft",
55
"dataSource.category": "security",
66
"metadata.product.name": "Microsoft 365 SharePoint/OneDrive",
@@ -93,11 +93,23 @@
9393
}
9494
},
9595
{
96-
"rename": {
96+
"copy": {
9797
"from": "unmapped.Operation",
9898
"to": "activity_name"
9999
}
100100
},
101+
{
102+
"copy": {
103+
"from": "unmapped.Operation",
104+
"to": "unmapped.operation"
105+
}
106+
},
107+
{
108+
"copy": {
109+
"from": "unmapped.Operation",
110+
"to": "event.type"
111+
}
112+
},
101113
{
102114
"rename": {
103115
"from": "unmapped.SiteUrl",
@@ -128,6 +140,12 @@
128140
"to": "actor.user.name"
129141
}
130142
},
143+
{
144+
"copy": {
145+
"from": "unmapped.user.email_addr",
146+
"to": "user.email_addr"
147+
}
148+
},
131149
{
132150
"rename": {
133151
"from": "unmapped.Details",

Backend/utilities/parsers/sentinelone_new/ai-siem-main/parsers/community/microsoft_365_collaboration-latest/microsoft_365_collaboration.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"attributes": {
3-
"dataSource.name": "Microsoft 365 Collaboration",
3+
"dataSource.name": "Microsoft O365",
44
"dataSource.vendor": "Microsoft",
55
"dataSource.category": "security",
66
"metadata.product.name": "Microsoft 365 SharePoint/OneDrive",

0 commit comments

Comments
 (0)