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/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/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/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/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/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/alert_service.py b/Backend/api/app/services/alert_service.py index 5061ab3..1af69aa 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(): @@ -189,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: @@ -219,6 +277,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 +287,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/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/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..6dad8e4 100644 --- a/Backend/api/app/services/scenario_service.py +++ b/Backend/api/app/services/scenario_service.py @@ -1,6 +1,15 @@ """ Scenario service for managing attack scenarios """ +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 typing import Dict, Any, List, Optional import uuid import time @@ -129,6 +138,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 +256,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 +273,7 @@ async def start_scenario( "started_at": datetime.utcnow().isoformat(), "speed": speed, "dry_run": dry_run, + "overwrite_parser": overwrite_parser, "progress": 0 } @@ -269,6 +291,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 +308,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/Backend/scenarios/apollo_ransomware_scenario.py b/Backend/scenarios/apollo_ransomware_scenario.py index cc2b46b..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": 20, # 20 min after initial compromise + "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": 25, # 25 min after initial compromise + "offset_minutes": 35, # After RDP file download event (base+25) + "target_machine": "email", "overrides": { - "finding_info.title": "Apollo Ransomware - 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,11 +341,32 @@ def send_phase_alert( alert["metadata"]["logged_time"] = time_ms alert["metadata"]["modified_time"] = time_ms - # Set user as the resource - alert["resources"] = [{ - "uid": VICTIM_PROFILE["email"], - "name": VICTIM_PROFILE["email"] - }] + # 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": 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", {}) @@ -333,6 +394,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") @@ -672,6 +734,52 @@ 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 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'] + enterprise_name = VICTIM_PROFILE['machine_enterprise'] + print(f"\nšŸ” Looking up XDR assets for alert linking...") + try: + import urllib.request + import urllib.parse + 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 real agent assets (have 'agent' field) for both machines + for asset in assets: + 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}'") + 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}") + print("\n🚨 ALERT DETONATION ENABLED") print(f" UAM Ingest: {uam_ingest_url}") print(f" Account ID: {uam_account_id}") @@ -726,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 = { 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 fc7bc91..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""" @@ -544,6 +611,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 +619,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 +652,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 +709,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'): @@ -654,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" @@ -843,6 +1004,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 +1114,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 @@ -1047,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" @@ -1686,7 +1868,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 +2099,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..1edb190 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.

@@ -1006,6 +1044,19 @@

UAM Alert Configuration (Option

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

+
@@ -1200,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

+
@@ -1516,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 || ''; @@ -1661,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(); @@ -2069,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(); @@ -2082,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 @@ -2217,7 +2290,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 +2354,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 +2577,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 +2600,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 }), @@ -2855,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; @@ -2911,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'; } }); @@ -2930,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', () => { @@ -3060,6 +3232,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 +3264,7 @@

Select Parser Version

tag_phase: tagPhase, tag_trace: tagTrace, trace_id: traceId, + overwrite_parser: overwriteParser, hec_token: localToken }) });