diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/sentinelone_data_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/sentinelone_data_generator.ts index 43384ec5bc947..19aedd173abbf 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/sentinelone_data_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/sentinelone_data_generator.ts @@ -5,9 +5,16 @@ * 2.0. */ -import type { DeepPartial } from 'utility-types'; +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type { DeepPartial, Mutable } from 'utility-types'; import { merge } from 'lodash'; import type { SearchResponse, SearchHit } from '@elastic/elasticsearch/lib/api/types'; +import type { + SentinelOneGetActivitiesResponse, + SentinelOneGetAgentsResponse, + SentinelOneActivityRecord, +} from '@kbn/stack-connectors-plugin/common/sentinelone/types'; import { EndpointActionGenerator } from './endpoint_action_generator'; import { SENTINEL_ONE_ACTIVITY_INDEX_PATTERN } from '../..'; import type { @@ -79,4 +86,783 @@ export class SentinelOneDataGenerator extends EndpointActionGenerator { ): SearchResponse { return this.toEsSearchResponse(docs); } + + generateSentinelOneApiActivityResponse( + activityRecordOverrides: DeepPartial> = {} + ): SentinelOneGetActivitiesResponse { + const today = new Date(); + today.setMinutes(today.getMinutes() - 5); + const activityType = activityRecordOverrides.activityType ?? this.randomActivityType(); + const activity: SentinelOneActivityRecord = { + accountId: this.randomString(10), + accountName: 'Elastic', + activityType, + activityUuid: this.seededUUIDv4(), + agentId: this.seededUUIDv4(), + agentUpdatedVersion: null, + comments: null, + createdAt: today.toISOString(), + data: this.generateActivityDataForType(activityType), + description: null, + groupId: '1392053568591146999', + groupName: 'Default Group', + hash: null, + id: this.seededUUIDv4(), + osFamily: null, + primaryDescription: sentinelOneActivityTypes[activityType.toString()], + secondaryDescription: 'IP address: 108.77.84.191', + siteId: '1392053568582758390', + siteName: 'Default site', + threatId: null, + updatedAt: today.toISOString(), + userId: this.randomUser(), + }; + + return { + data: [merge(activity, activityRecordOverrides)], + pagination: { + nextCursor: 'eyJpZF9jb2x9', + totalItems: 1, + }, + }; + } + + randomActivityType(): number { + return Number(this.randomChoice(Object.keys(sentinelOneActivityTypes))); + } + + generateActivityDataForType = {}>( + activityType: number, + overrides: Record = {} + ): SentinelOneActivityRecord['data'] { + let activityData: Record = { + accountName: 'elastic', + groupName: 'Default Group', + }; + + switch (activityType) { + // File fetch requested + case 81: + { + const ip = this.randomIP(); + activityData = { + accountName: 'Elastic', + commandBatchUuid: this.seededUUIDv4(), + computerName: this.randomHostname(), + externalIp: ip, + fullScopeDetails: 'Group Default Group in Site Default site of Account Elastic', + fullScopeDetailsPath: 'Global / Elastic / Default site / Default Group', + groupName: 'Default Group', + groupType: 'Manual', + ipAddress: ip, + scopeLevel: 'Group', + scopeName: 'Default Group', + siteName: 'Default site', + username: 'Defend Workflows Automation', + uuid: this.seededUUIDv4(), + }; + } + break; + + // File was successful retrieved from host (uploaded by host to sentinelone) + case 80: + activityData = { + accountName: 'Elastic', + commandBatchUuid: this.seededUUIDv4(), + commandId: this.seededUUIDv4(), + computerName: this.randomHostname(), + downloadUrl: '/agents/1889978925339985309/uploads/1891428432577857502', + externalIp: this.randomIP(), + externalServiceId: null, + filePath: '/agents/1889978925339985309/uploads/1891428432577857502', + filename: 'file_9665_2024-02-23_16_10_39.663.zip', + fullScopeDetails: 'Group Default Group in Site Default site of Account Elastic', + fullScopeDetailsPath: 'Global / Elastic / Default site / Default Group', + groupName: 'Default Group', + ipAddress: null, + realUser: null, + scopeLevel: 'Group', + scopeName: 'Default Group', + siteName: 'Default site', + sourceType: 'API', + uploadedFilename: 'file_fetch_23-02-24_11_10_39.zip', + }; + break; + + // Host was isolated (disconnected from network) + case 1001: + activityData = { + accountName: 'Elastic', + computerName: this.randomHostname(), + fullScopeDetails: 'Group Default Group in Site Default site of Account Elastic', + fullScopeDetailsPath: 'Global / Elastic / Default site / Default Group', + groupName: 'Default Group', + ipAddress: null, + realUser: null, + scopeLevel: 'Group', + scopeName: 'Default Group', + siteName: 'Default site', + sourceType: 'API', + }; + break; + + // Host was release (re-connected to the network) + case 1002: + activityData = { + accountName: 'Elastic', + computerName: this.randomHostname(), + fullScopeDetails: 'Group Default Group in Site Default site of Account Elastic', + fullScopeDetailsPath: 'Global / Elastic / Default site / Default Group', + groupName: 'Default Group', + ipAddress: null, + realUser: null, + scopeLevel: 'Group', + scopeName: 'Default Group', + siteName: 'Default site', + sourceType: 'API', + }; + break; + } + + return activityData as SentinelOneActivityRecord['data']; + } + + generateSentinelOneApiAgentsResponse( + agentDetailsOverrides: DeepPartial = {} + ): SentinelOneGetAgentsResponse { + const id = agentDetailsOverrides.id || agentDetailsOverrides.uuid || this.seededUUIDv4(); + + const agent: SentinelOneGetAgentsResponse['data'][number] = { + accountId: this.seededUUIDv4(), + accountName: 'Elastic', + activeDirectory: { + computerDistinguishedName: null, + computerMemberOf: [], + lastUserDistinguishedName: null, + lastUserMemberOf: [], + mail: null, + userPrincipalName: null, + }, + activeThreats: 0, + agentVersion: '23.4.2.14', + allowRemoteShell: true, + appsVulnerabilityStatus: 'not_applicable', + cloudProviders: {}, + computerName: this.randomHostname(), + consoleMigrationStatus: 'N/A', + coreCount: 1, + cpuCount: 1, + cpuId: 'ARM Cortex-A72', + createdAt: this.randomPastDate(), + detectionState: null, + domain: 'unknown', + encryptedApplications: false, + externalId: '', + externalIp: this.randomIP(), + firewallEnabled: false, + firstFullModeTime: null, + fullDiskScanLastUpdatedAt: this.randomPastDate(), + groupId: '1392053568591146999', + groupIp: '108.77.84.x', + groupName: 'Default Group', + id, + inRemoteShellSession: false, + infected: false, + installerType: '.deb', + isActive: true, + isDecommissioned: false, + isPendingUninstall: false, + isUninstalled: false, + isUpToDate: true, + lastActiveDate: this.randomPastDate(), + lastIpToMgmt: this.randomIP(), + lastLoggedInUserName: '', + licenseKey: '', + locationEnabled: false, + locationType: 'not_supported', + locations: null, + machineType: 'server', + mitigationMode: 'detect', + mitigationModeSuspicious: 'detect', + modelName: 'QEMU QEMU Virtual Machine', + policyUpdatedAt: null, + groupUpdatedAt: null, + networkInterfaces: [ + { + gatewayIp: '192.168.64.1', + gatewayMacAddress: 'be:d0:74:50:d8:64', + id: '1913920934593053818', + inet: ['192.168.64.2'], + inet6: ['fdf4:f033:b1d4:8c51:5054:ff:fe5b:15e7'], + name: 'enp0s1', + physical: '52:54:00:5B:15:E7', + }, + ], + networkQuarantineEnabled: false, + networkStatus: 'connected', + operationalState: 'na', + operationalStateExpiration: null, + osArch: '64 bit', + osName: 'Linux', + osRevision: 'Ubuntu 22.04.4 LTS 5.15.0-102-generic', + osStartTime: '2024-04-16T22:48:33Z', + osType: 'linux', + osUsername: 'root', + rangerStatus: 'Enabled', + rangerVersion: '23.4.1.1', + registeredAt: '2024-03-25T16:59:14.860010Z', + remoteProfilingState: 'disabled', + remoteProfilingStateExpiration: null, + scanAbortedAt: null, + scanFinishedAt: '2024-03-25T17:21:43.371381Z', + scanStartedAt: '2024-03-25T17:00:19.774123Z', + scanStatus: 'finished', + serialNumber: null, + showAlertIcon: false, + siteId: '1392053568582758390', + siteName: 'Default site', + storageName: null, + storageType: null, + tags: { sentinelone: [] }, + threatRebootRequired: false, + totalMemory: 1966, + updatedAt: this.randomPastDate(), + userActionsNeeded: [], + uuid: id, + }; + + return { + pagination: { totalItems: 1, nextCursor: null }, + data: [merge(agent, agentDetailsOverrides)], + errors: null, + }; + } } + +// Activity types from SentinelOne. Values can be retrieved from the SentineOne API at: +// `/web/api/v2.1/activities/types` +const sentinelOneActivityTypes: Record = { + '2': 'Hash Defined as Malicious By Cloud', + '5': 'Agent Software Update Downloaded', + '6': '', + '7': '', + '8': '', + '15': 'User Marked Agent As Up To Date', + '16': 'Agent software updated', + '17': 'Agent subscribed', + '18': 'New Threat Mitigated', + '19': 'New Malicious Threat Not Mitigated', + '20': 'New Threat Preemptive Block', + '21': 'Threat Resolved', + '22': 'Threat Benign', + '23': 'User Added', + '24': 'User Modified', + '25': 'User Deleted', + '26': 'Management Updated', + '27': 'User Logged In', + '28': 'Activity Marked As Resolved By Cloud', + '29': 'Activity Marked As Unresolved By Cloud', + '30': '', + '31': '', + '32': '', + '33': 'User Logged Out', + '34': 'Threat Unresolved', + '35': 'Verification email', + '36': 'Verification complete', + '37': 'User modified', + '38': 'Immune Settings Modified', + '39': 'Research Settings Modified', + '40': 'Cloud Intelligence Settings Modified', + '41': 'Learning Mode Settings Modified', + '42': 'Global 2FA modified', + '43': 'Agent updated', + '44': 'Auto decommission On', + '45': 'Auto decommission Off', + '46': 'Auto Decommission Period Modified', + '47': 'Agent Decommissioned', + '48': 'Agent Recommissioned', + '49': 'Agent Request Uninstall', + '50': 'Uninstall Agent', + '51': 'Agent Uninstalled', + '52': 'User Approved Agent Uninstall Request', + '53': 'User Rejected Agent Uninstall Request', + '54': 'User Decommissioned Agent', + '55': 'User Recommissioned Agent', + '56': 'Auto Mitigation Actions Modified', + '57': 'Quarantine Network Settings Modified', + '58': 'Notification Option Level Modified', + '59': 'Event Severity Level Modified', + '60': 'Notification - Recipients Configuration Modified', + '61': 'User Disconnected Agent From Network', + '62': 'User Reconnected Agent to Network', + '63': 'User Shutdown Agent', + '64': 'User Requested Passphrase', + '65': 'User Requested Full Log Report', + '66': 'Agent Uploaded Full Log Report', + '67': 'User 2FA Modified', + '68': 'Engine Modified In Policy', + '69': 'Mitigation Policy Modified', + '70': 'Policy Setting - Agent Notification On Suspicious Modified', + '71': 'Scan Initiated', + '72': 'Scan Aborted', + '73': 'Scan New Agents Changed', + '74': 'Machine Restart', + '75': 'On Access Modified', + '76': 'Anti Tampering Modified', + '77': 'Agent UI Settings Modified', + '78': 'Snapshots Settings Modified', + '79': 'Agent Logging Modified', + '80': 'Agent Uploaded Fetched Files', + '81': 'User Requested Fetch Files', + '82': 'Monitor On Execute', + '83': 'Monitor On Write', + '84': 'Deep Visibility Settings Modified', + '85': 'User Requested Fetch Threat File', + '86': 'Agent Uploaded Threat File', + '87': 'Remote Shell Settings Modified', + '88': 'User Remote Shell Modified', + '89': 'User Requested Randomized Agent UUID', + '90': 'Agent Started Full Disk Scan', + '91': 'Agent Aborted Full Disk Scan', + '92': 'Agent Completed Full Disk Scan', + '93': "User Reset Agent's Local Config", + '94': 'User Moved Agent To Another Site', + '95': 'User Moved Agent to Group', + '96': 'User Moved Agent from Site', + '97': 'User Commanded Agent To Move To Another Console', + '98': 'Agent Was Not Moved To Another Console', + '99': 'Agent Successfully Moved To Another Console', + '101': "User Changed Agent's Customer Identifier", + '102': 'User Deleted', + '103': 'User Deleted', + '104': 'Global licenses modified', + '105': 'Deep Visibility Settings Modified', + '106': 'User Commanded Agents To Move To Another Console', + '107': 'User Created RBAC Role', + '108': 'User Edited RBAC Role', + '109': 'User Deleted RBAC Role', + '110': 'Enable API Token Generation', + '111': 'Disable API Token Generation', + '112': 'API token Generated', + '113': 'API Token Revoked', + '114': 'API Token Revoked', + '115': 'Windows Event Log Collection Modified', + '116': 'Policy Settings Modified', + '117': 'User Disabled Agent', + '118': 'User Enabled Agent', + '119': 'Agent Disabled', + '120': 'Agent Enabled', + '121': 'User Started Remote Profiling', + '122': 'User Stopped Remote Profiling', + '123': 'Remote Profiler Enabled', + '124': 'Remote Profiler Disabled', + '125': 'Disable Agent Error', + '126': 'Agent Disabled', + '127': 'Agent Disabled', + '128': 'Agent Disabled Because of Database Corruption', + '129': 'Allowed Domains Settings Changed', + '130': 'Opt-in To EA program', + '131': 'Opt-out From EA Program', + '132': 'EA Platform Settings Changed', + '133': 'Existing User Login Failure', + '134': 'Unknown User Login', + '135': 'Remote Profiler Completed', + '136': 'Remote Profiler Failure', + '137': 'Remote Profiler Upload Failure', + '138': 'User Started an Unrestricted Session', + '139': 'User Failed to Start an Unrestricted Session', + '140': 'Service User creation', + '141': 'Service User modification', + '142': 'Service User deletion', + '143': 'User Locked from Unrestricted Session', + '144': 'User Temporarily Locked', + '145': '2FA - Enroll', + '146': '2FA - Reset', + '147': '2FA - Configured', + '148': '2FA - Delete', + '150': 'Live Updates Policy Modified', + '151': 'Live Updates Policy Inheritance Setting Changed', + '160': 'Agent Update Pending User Action', + '161': 'Agent Update Canceled', + '162': 'Agent Update Failed', + '163': '', + '200': 'File Upload Settings Modified', + '201': 'File Upload Enabled/Disabled', + '203': 'User Downloaded File', + '204': 'Scheduled Report Removed', + '221': 'Threat Automatically Resolved', + '701': 'User Created Policy Override', + '702': 'User Updated Policy Override', + '703': 'User Deleted Policy Override', + '1001': 'Agent Disconnected From Network', + '1002': 'Agent Reconnected To Network', + '1023': 'SSO User Added', + '1024': 'SSO User Modified', + '1100': 'User Network Quarantine Container', + '1101': 'User Network Unquarantine Container', + '1102': 'User Network Quarantine Container', + '1103': 'User Network Unquarantine Container', + '1104': 'User Network Unquarantine Container', + '1105': 'User Network Quarantine Container', + '1501': 'Location Created', + '1502': 'Location Copied', + '1503': 'Location Modified', + '1504': 'Location Deleted', + '2001': 'Threat Mitigation Report Kill Success', + '2002': 'Threat Mitigation Report Remediate Success', + '2003': 'Threat Mitigation Report Rollback Success', + '2004': 'Threat Mitigation Report Quarantine Success', + '2005': '', + '2006': 'Threat Mitigation Report Kill Failed', + '2007': 'Threat Mitigation Report Remediate Failed', + '2008': 'Threat Mitigation Report Rollback Failed', + '2009': 'Threat Mitigation Report Quarantine Failed', + '2010': 'Agent Mitigation Report Quarantine Network Failed', + '2011': 'User Issued Kill Command', + '2012': 'User Issued Remediate Command', + '2013': 'User Issued Rollback Command', + '2014': 'User Issued Quarantine Command', + '2015': 'User Issued Unquarantine Command', + '2016': 'User Marked Application As Threat', + '2017': 'User Issued Remove Macros Command', + '2018': 'User Issued Restore Macros Command', + '2021': 'Threat Killed By Policy', + '2022': 'Threat Remediated By Policy', + '2023': 'Threat Rolled Back By Policy', + '2024': 'Threat Quarantined By Policy', + '2025': 'Agent Disconnected From Network Due to Threat Mitigation By Policy', + '2026': 'Threat Mitigation Report Unquarantine Success', + '2027': 'Threat Mitigation Report Unquarantine Failed', + '2028': 'Threat Incident Status Changed', + '2029': 'Ticket Number Changes', + '2030': 'Analyst Verdict Changes', + '2031': 'Threat Mitigation Report Kill Pending Reboot', + '2032': 'Threat Mitigation Report Remediate Pending Reboot', + '2033': 'Threat Mitigation Report Rollback Pending Reboot', + '2034': 'Threat Mitigation Report Quarantine Pending Reboot', + '2035': 'Threat Mitigation Report Unquarantine Pending Reboot', + '2036': 'Threat Confidence Level Changed By Agent', + '2037': 'Threat Confidence Level Changed By Cloud', + '2038': 'Threat Mitigation Report Remove Macros Success', + '2039': 'Threat Mitigation Report Remove Macros Failed', + '2040': 'Threat Mitigation Report Restore Macros Success', + '2041': 'Threat Mitigation Report Restore Macros Failed', + '2042': 'Threat Mitigation Report Remove Macros Pending Reboot', + '2043': 'Threat Mitigation Report Restore Macros Pending Reboot', + '2100': 'Upgrade Policy - Concurrency Limit Changed', + '2101': 'Upgrade Policy - Concurrency Limit Inheritance Changed', + '2110': 'Upgrade Policy - Maintenance Window Time Changed', + '2111': 'Upgrade Policy - Maintenance Window Time Inheritance Changed', + '3001': 'User Added Hash Exclusion', + '3002': 'User Added Blocklist Hash', + '3003': '', + '3004': '', + '3005': 'Cloud Added Hash Exclusion', + '3006': 'Cloud Added Blocklist Hash', + '3007': '', + '3008': 'New Path Exclusion', + '3009': 'New Signer Identity Exclusion', + '3010': 'New File Type Exclusion', + '3011': 'New Browser Type Exclusion', + '3012': 'Path Exclusion Modified', + '3013': 'Signer Identity Exclusion Modified', + '3014': 'File Type Exclusion Modified', + '3015': 'Browser Type Exclusion Modified', + '3016': 'Path Exclusion Deleted', + '3017': 'Signer Identity Exclusion Deleted', + '3018': 'File Type Exclusion Deleted', + '3019': 'Browser Type Exclusion Deleted', + '3020': 'User Deleted Hash From Blocklist', + '3021': 'User Deleted Hash Exclusion', + '3022': 'Cloud Deleted Hash Exclusion', + '3023': 'Cloud Deleted Blocklist Hash', + '3050': 'Identity Exclusion Created', + '3051': 'Identity Exclusion Updated', + '3052': 'Identity Exclusion Updated', + '3100': 'User Added Package', + '3101': 'User Modified Package', + '3102': 'User Deleted Package', + '3103': 'Package Deleted By System - Too Many Packages', + '3200': 'User Started Remote Shell', + '3201': 'Remote Shell Created', + '3202': 'Remote Shell Failed', + '3203': 'Remote Shell Terminated', + '3204': 'Remote Shell Terminated By User', + '3400': 'Agent Uploaded Remote Shell history', + '3500': 'User Toggled Ranger Status', + '3501': 'Ranger Settings Modified', + '3502': 'Ranger Network Settings Modified', + '3503': 'Ranger - Inventory Scan Completed', + '3505': 'Ranger - Devices Discovered', + '3506': 'Ranger - Device Review Modified', + '3507': 'Ranger - Device Tag Modified On Host', + '3520': 'Ranger Deploy - Master passphrase', + '3521': 'Ranger Deploy Initiated', + '3522': 'Ranger Deploy - Credential Group Created', + '3523': 'Ranger Deploy -Credential Group Edited', + '3524': 'Ranger Deploy - Credential Group Deleted', + '3525': 'Ranger Deploy - Credential Created', + '3526': 'Ranger Deploy - Credential Deleted', + '3527': 'Ranger Deploy - Credential Overridden', + '3530': 'Ranger Labels Updated', + '3531': 'Ranger labels reverted', + '3532': 'Ranger Deploy - Trusted Hosts', + '3533': 'Ranger Deploy - Linux Enforcement', + '3534': 'Ranger Deploy - PsDrive WMI Session', + '3600': 'Custom Rules - User Created A Rule', + '3601': 'Custom Rules - User Changed A Rule', + '3602': 'Custom Rules - User Deleted A Rule', + '3603': 'Custom Rules - Rule Status Changed', + '3604': 'Custom Rules - Rule Status Change Failed', + '3605': 'Custom Rules - Rule Will Expire Soon', + '3606': 'Custom Rules - Rule Expired', + '3607': 'Custom Rules - Rule Reached Alert Limit', + '3608': 'Custom Rules - New Alert', + '3610': 'Account Uninstall Password Viewed', + '3611': 'Account Uninstall Password Generated', + '3612': 'Account Uninstall Password Regenerated', + '3613': 'Account Uninstall Password Revoked', + '3614': 'Agent Started On-Demand Disk Scan', + '3615': 'Agent Aborted On-Demand Scan', + '3616': 'New Script Created', + '3617': 'Agent Completed On-Demand Scan', + '3618': 'Script Action Initiated', + '3620': 'Manual CIS Scan Initiated', + '3621': 'Manual CIS scan Completed', + '3622': 'Script Deleted', + '3623': 'Script Updated', + '3624': 'User 2FA Verification Success', + '3625': 'User 2FA Verification Failed', + '3626': 'User 2FA Email Verification Changed', + '3627': 'User 2FA Verification Sent', + '3628': '2FA Code Verification', + '3629': 'Login Using Saved 2FA Recovery Code', + '3630': 'Live Update Sent To Agent', + '3631': 'Live Update Merged To Agent', + '3632': 'Live Update Not Merged To Agent', + '3633': 'Marketplace - App Installed', + '3634': 'Marketplace - App Deleted', + '3635': 'Marketplace - App Disabled', + '3636': 'Marketplace - App Enabled', + '3637': 'Marketplace - App Edit', + '3638': 'Marketplace - App Disabled Error', + '3639': 'Marketplace - App Disabled Error', + '3640': 'Ranger Self Provisioning', + '3641': 'Ranger self Provisioning Default Features Modified', + '3642': 'Ranger Self Provisioning Site Features Change', + '3650': 'Tag Manager - User Created New Tag', + '3651': 'Tag Manager - User Modified Tag', + '3652': 'Tag Manager - User Deleted Tag', + '3653': 'Tag Manager - User Attached Tag', + '3654': 'Tag Manager - User Detached Tag', + '3655': 'Tag Detached Because Tag Deleted', + '3656': 'Tag Manager - Tag Detached Because Scope Changed', + '3660': 'Vulnerability scan triggered', + '3661': 'User Add Missing CVE To Application', + '3662': 'User Marked CVE as False Positive on Application', + '3663': 'User created a vulnerability ticket', + '3664': 'User Enabled Extensive Scan', + '3665': 'User Disabled Extensive Scan', + '3666': 'User Enabled Linux Extensive Scan', + '3667': 'User Disabled Linux Extensive Scan', + '3668': 'User Enabled Vulnerability Scan', + '3669': 'User Disabled Vulnerability Scan', + '3670': 'User Modified Inheritance', + '3701': 'XDR - User Initiated Action', + '3702': 'XDR - Application Action Response', + '3710': 'User Reset Password with Forgot Password from the Login', + '3711': 'User Changed Their Password', + '3712': 'Prompt Reset Password', + '3714': 'User Changed Their Password Due To Expiration', + '3715': 'User Reset Password by Admin Request', + '3720': 'CIS Skip Created', + '3721': 'CIS Skip Deleted', + '3730': 'User Requested Set Password On Next Login', + '3750': 'Auto-Upgrade Policy Created', + '3751': 'Auto-Upgrade Policy Disabled', + '3752': 'Auto-Upgrade Policy Activated', + '3753': 'Auto-Upgrade Policy Deleted', + '3754': 'Auto-Upgrade Policy Reordered', + '3755': 'Upgrade Policy Inheritance Setting Changed', + '3756': 'Auto-Upgrade Policy Edited', + '3757': 'Custom Rules - New Alert', + '3767': 'Local Upgrade Authorized', + '3768': 'Local Upgrade Authorized', + '3769': 'Local Upgrade Authorized', + '3770': 'Local Upgrade Authorization Expiry Date Changed', + '3771': 'Local Upgrade Authorization Expiry Date Changed', + '3772': 'Local Upgrade Unauthorized', + '3773': 'Local Upgrade Authorization Inherits from Site Level', + '3774': 'Local Upgrade Authorization Inherits from Site Level', + '3775': 'Downloaded list of authorized agents', + '4001': 'Suspicious Threat Was Marked As Threat', + '4002': 'Suspicious Threat Was Resolved', + '4003': 'New Suspicious Threat Not Mitigated', + '4004': 'Policy Setting - Show Suspicious Activities Configuration Enabled', + '4005': 'Policy Setting - Show Suspicious Activities Configuration Disabled', + '4006': 'Session Timeout Timeout Modified', + '4007': 'Suspicious Threat Was Marked As Benign', + '4008': 'Threat Mitigation Status Changed', + '4009': 'Process Was Marked As Threat', + '4010': '', + '4011': 'Suspicious Threat Was Unresolved', + '4012': 'UI Inactivity Timeout Modified', + '4013': 'Password Expiration Modified', + '4020': 'New Threat Note Added', + '4021': 'Threat Note Updated', + '4022': 'Threat Note Deleted', + '4100': 'User Marked Deep Visibility Event As Threat', + '4101': 'User Marked Deep Visibility Event As Suspicious', + '4102': 'Agent Failed To Mark Deep Visibility Event As Threat', + '4103': 'Agent Failed To Mark Deep Visibility Event As Suspicious', + '4104': 'STAR Manual Response Marked Event As Malicious', + '4105': 'STAR Manual Response Marked Event As Suspicious', + '4106': 'STAR Active Response Marked Event As Malicious', + '4107': 'STAR Active Response Marked Event As Suspicious', + '4108': 'New Malicious Threat Not Mitigated', + '4109': 'New Suspicious Threat Not Mitigated', + '4110': 'Singularity Threat Intelligence Engine Marked Event As Malicious', + '4111': 'Watchtower Cloud Detection Engine Marked Event As Threat', + '4112': 'Watchtower Cloud Detection Engine Marked Event As Suspicious', + '4113': 'Singularity Threat Intelligence Engine Marked Event As Suspicious', + '5000': 'AD Sync Started', + '5001': 'AD Sync Finished', + '5002': 'Dynamic Group Creation Started', + '5003': 'Dynamic Group Creation Finished', + '5004': 'Dynamic Group Update Started', + '5005': 'Dynamic Group Update Finished', + '5006': 'Group Deleted', + '5007': 'Group Info Changed', + '5008': 'User created a Manual or Pinned Group', + '5009': 'Agent Moved To A Different Group', + '5010': 'Group Ranking Changed', + '5011': 'Group Policy Reverted', + '5012': 'Group Token Regenerated', + '5013': 'Group Deleted Because Site Deleted', + '5020': 'Site Created', + '5021': 'Site Modified', + '5022': 'Site Deleted', + '5023': 'Site Expired', + '5024': 'Site Policy Reverted', + '5025': 'Site Marked As Expired', + '5026': 'Site Duplicated', + '5027': 'Site Token Regenerated', + '5028': 'Site Expired Because Account Expired', + '5029': 'Site Deleted Because Account Deleted', + '5040': 'Account Created', + '5041': 'Account Modified', + '5042': 'Account Deleted', + '5043': 'Account Expired', + '5044': 'Account Policy Reverted', + '5045': 'Account Marked As Expired', + '5120': 'Device Rule Created', + '5121': 'Device Rule Modified', + '5122': 'Device Rule Deleted', + '5123': 'Device Rules Reordered', + '5124': 'Device Rules Settings Modified', + '5125': 'Device Control Blocked Event', + '5126': 'Device Control Approved Event', + '5127': 'Device Rule Moved From Scope', + '5128': 'Device Rule Moved To Scope', + '5129': 'Device Rule Copied To Scope', + '5220': 'Firewall Rule Created', + '5221': 'Firewall Rule Modified', + '5222': 'Firewall Rule Deleted', + '5225': 'Firewall Control Settings Modified', + '5226': 'Firewall Rules Reordered', + '5227': 'User Initiated Fetch Firewall Rules From Agent', + '5228': 'Agent Uploaded Firewall Rules', + '5229': 'Firewall Rule Moved From Scope', + '5230': 'Firewall Rule Moved To Scope', + '5231': 'Firewall Rule Copied To Scope', + '5232': 'Firewall Control Blocked Event', + '5233': 'User Requested Firewall Logging', + '5234': 'Network Quarantine Rule Created', + '5235': 'Network Quarantine Rule Modified', + '5236': 'Network Quarantine Rule Deleted', + '5237': 'Network Quarantine Control Settings Modified', + '5238': 'Network Quarantine Rules Reordered', + '5239': 'Network Quarantine Rule Moved From Scope', + '5240': 'Network Quarantine Rule Moved To Scope', + '5241': 'Network Quarantine Rule Copied To Scope', + '5242': 'Ranger - Device Tag Created', + '5243': 'Ranger - Device Tag Updated', + '5244': 'Ranger - Device Tag Deleted', + '5250': 'Firewall Control Tag Created', + '5251': 'Firewall Control Tag Updated', + '5252': 'Firewall Control Tag Updated', + '5253': 'Network Quarantine Control Tag Created', + '5254': 'Network Quarantine Control Tag Updated', + '5255': 'Network Quarantine Control Tag Deleted', + '5256': 'Firewall Control Tag Added/Removed From Rule', + '5257': 'Firewall Control Tag Inherited', + '5258': 'Network Quarantine Control Tag Added/Removed From Rule', + '5259': 'Network Quarantine Control Tag Inherited', + '6000': 'Mobile Policy updated', + '6001': 'Mobile Policy created', + '6002': 'Mobile Policy removed', + '6010': 'UEM Connection created', + '6011': 'UEM Connection updated', + '6012': 'UEM Connection Removed', + '6013': 'UEM Connection Synced', + '6030': 'Mobile Device Updated', + '6031': 'Mobile Device Created', + '6032': 'Mobile Device Removed', + '6050': 'Mobile Incident Detected', + '6051': 'Mobile Incident Mitigated', + '6052': 'Mobile Incident Automatically Resolved', + '6053': 'Mobile Incident Resolved', + '6054': 'Mobile Incident Status Changed', + '6055': 'Mobile Incident Analyst Verdict Changed', + '6999': 'Unknown mobile activity', + '7100': 'Exclusion import', + '7101': 'Blacklist import', + '7200': 'Add cloud account', + '7201': 'Disable cloud Account', + '7202': 'Enable cloud Account', + '7203': 'Disable and Purge cloud (AKA Delete)', + '7210': 'Add cloud policy', + '7211': 'Update cloud policy', + '7212': 'Delete cloud policy', + '7500': 'Remote Ops Password Configured', + '7501': 'Remote Ops Password Deleted', + '7502': 'Forensics Profile Executed', + '7503': 'Forensics Profile Created', + '7504': 'Forensics Profile Modified', + '7505': 'Forensics Profile Deleted', + '7600': 'User Submitted Script Execution For Review', + '7601': 'User Approved Script Execution', + '7602': 'User Edited Run Script Guardrails', + '7603': 'User Enabled Run Script Guardrails', + '7604': 'User Disabled Run Script Guardrails', + '7700': 'User Added New AD configuration', + '7701': 'User Updated AD configuration', + '7702': 'User Deleted AD configuration', + '7800': 'User Deployed AD Connector', + '7801': 'User Updated AD Connector', + '7802': 'User Deleted AD Connector', + '7830': 'Ranger AD Scan Completed', + '7831': 'AD Assessment Detected New Exposure', + '7853': 'User Triggered AD Exposure Assessment', + '7854': 'User Exported AD Exposure', + '7855': 'User Performed AD Exposure Skip', + '7856': 'User Performed AD Exposure Unskip', + '7857': 'User Performed AD Exposure Ack', + '7858': 'User Performed AD Exposure Unack', + '7860': 'User Created Mitigation Task', + '7861': 'User Created Mitigation Task', + '7862': 'User Downloaded Mitigation Script', + '7863': 'User Ran Mitigation Script', + '7870': 'User updated application status', + '7871': 'User updated application with spesific version status', + '7872': 'User updated application status', + '7900': 'Vigilance Response Policy Modified', + '7901': 'Vigilance Response Policy Inheritance Modified', + '7902': 'Vigilance Escalation Contacts Modified', + '7903': 'Vigilance Escalation Contacts Inheritance Modified', + '8001': 'Sign SAML Request - Enabled / Disabled', + '8002': 'Accessing Protected Actions Configuration', + '9001': 'User Added Deep Visibility Exclusion', + '9002': 'User Deleted Deep Visibility Exclusion', + '10000': 'Agent Failed To Move To Another Console', +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/README.md b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/README.md new file mode 100644 index 0000000000000..48e1da2ef442f --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/README.md @@ -0,0 +1,67 @@ +# API Emulator + +API Emulator is a framework wrapped around [Hapi](https://hapi.dev/) that enables developer to quickly create API interfaces for development and testing purposes. Emulator plugins (a wrapper around [Hapi plugins](https://hapi.dev/api/?v=21.3.3#plugins)) is the mechanism used to create an given set of APIs for emulation, and these are then added to the server framework which makes them available via the server's routes. + +The following script can be used to start the External EDR Server Emulator from the command line: + +```shell +node x-pack/plugins/security_solution/scripts/endpoint/start_external_edr_server_emulator.js +``` + +Use the `--help` option to view what arguments can be used + +For usages other than the command line, see the Development section below. + + + +## Development + +### Adding an new Plugin + +Plugins are the mechanism for adding API emulators into this framework. Each plugin is defined via an object that includes at a minimum the `name` and a `register()` callback. This callback for registering the plugin will be provided with an interface that allows the plugin to interact with the HTTP server and provide access to "core" services available at the server level for use by all plugins. + +Example: A method that returns the definition for a plugin + +```typescript + +export const getFooPluginRegistration = () => { + return { + name: 'foo', // [1] + register(server) { + // register routes + server.router.route({ + path: '/api/get', // [2] + method: 'GET', + handler: async (req, h) => { + return 'alive!'; + } + }) + } + } +} +``` + +In the above example: + +1. a plugin with the name `foo` [1] will be registered. The name of the plugin will also be the default `prefix` to all API routes (an optional attributed named `prefix` is also available if wanting to use a different value for the namespacing the routes). +2. the `register()` callback will be given a `server` argument that provides access to server level services like the HTTP `router` +3. a new route is registered [2], which will be mounted at `/foo/api/get` - note the use of the plugin name as the route prefix + + +#### Plugin HTTP routes + +HTTP route handlers work very similar to the route handlers in Kibana today. You are given a `Request` and a Response Factory by the Hapi framework - see the [HAPI docs on Lifecycle Methods](https://hapi.dev/api/?v=21.3.3#lifecycle-methods) for more details. + +This emulator framework will expose the core services (ex. for the EDR server emulator, this would include Kibana and Elasticsearch clients) to each route under `request.pre` (pre-handler methods). + +Example: a route handler under the EDR server emulator that returns the version of kibana + +```typescript + +const handler = async (req, h) => { + const kbnStatus = await req.pre.services.kbnClient.status.get(); + + return kbnStatus.version.number; +} + +``` diff --git a/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/crowdstrike/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/crowdstrike/index.ts new file mode 100644 index 0000000000000..cdfbdff81c84b --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/crowdstrike/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { EmulatorServerPlugin } from '../../lib/emulator_server.types'; + +export const getCrowdstrikeEmulator = () => { + const plugin: EmulatorServerPlugin = { + name: 'crowdstrike', + register({ router }) { + router.route({ + path: '/', + method: 'GET', + handler: () => { + return { message: `Live! But not implemented` }; + }, + }); + }, + }; + + return plugin; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/sentinelone/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/sentinelone/index.ts new file mode 100644 index 0000000000000..fcc7f4373cea1 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/sentinelone/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ExternalEdrServerEmulatorCoreServices } from '../..'; +import { getSentinelOneRouteDefinitions } from './routes'; +import type { EmulatorServerPlugin } from '../../lib/emulator_server.types'; + +export const getSentinelOneEmulator = + (): EmulatorServerPlugin => { + const plugin: EmulatorServerPlugin = { + name: 'sentinelone', + register({ router, expose, services }) { + router.route(getSentinelOneRouteDefinitions()); + + // TODO:PT define the interface for programmatically interact with sentinelone api emulator + expose('setResponse', () => { + services.logger.info('setResponse() is available'); + }); + }, + }; + + return plugin; + }; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/sentinelone/routes/activities_route.ts b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/sentinelone/routes/activities_route.ts new file mode 100644 index 0000000000000..4eecf26de67c4 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/sentinelone/routes/activities_route.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + SentinelOneActivityRecord, + SentinelOneGetActivitiesParams, +} from '@kbn/stack-connectors-plugin/common/sentinelone/types'; +import type { DeepPartial, Mutable } from 'utility-types'; +import { SentinelOneDataGenerator } from '../../../../../../common/endpoint/data_generators/sentinelone_data_generator'; +import { buildSentinelOneRoutePath } from './utils'; +import type { ExternalEdrServerEmulatorRouteHandlerMethod } from '../../../external_edr_server_emulator.types'; +import type { EmulatorServerRouteDefinition } from '../../../lib/emulator_server.types'; + +const generator = new SentinelOneDataGenerator(); + +export const getActivitiesRouteDefinition = (): EmulatorServerRouteDefinition => { + return { + path: buildSentinelOneRoutePath('/activities'), + method: 'GET', + handler: activitiesRouteHandler, + }; +}; + +const activitiesRouteHandler: ExternalEdrServerEmulatorRouteHandlerMethod< + {}, + NonNullable +> = async (request) => { + const queryParams = request.query; + const activityOverrides: DeepPartial> = {}; + + if (queryParams?.activityTypes) { + activityOverrides.activityType = Number(queryParams.activityTypes.split(',').at(0)); + } + + if (queryParams?.agentIds) { + activityOverrides.agentId = queryParams.agentIds.split(',').at(0); + } + + return generator.generateSentinelOneApiActivityResponse(activityOverrides); +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/sentinelone/routes/agent_action_connect_route.ts b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/sentinelone/routes/agent_action_connect_route.ts new file mode 100644 index 0000000000000..7716b52caa256 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/sentinelone/routes/agent_action_connect_route.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ExternalEdrServerEmulatorRouteHandlerMethod } from '../../..'; +import { buildSentinelOneRoutePath } from './utils'; +import type { EmulatorServerRouteDefinition } from '../../../lib/emulator_server.types'; + +export const getAgentActionConnectRouteDefinition = (): EmulatorServerRouteDefinition => { + return { + path: buildSentinelOneRoutePath('/agents/actions/connect'), + method: 'POST', + handler: connectActionRouteHandler, + }; +}; + +const connectActionRouteHandler: ExternalEdrServerEmulatorRouteHandlerMethod< + {}, + {}, + { + filter: { + ids: string; + }; + } +> = async (request) => { + return { + data: { + affected: request.payload.filter.ids.split(',').length, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/sentinelone/routes/agent_action_disconnect_route.ts b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/sentinelone/routes/agent_action_disconnect_route.ts new file mode 100644 index 0000000000000..dbcf0fcfe22c2 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/sentinelone/routes/agent_action_disconnect_route.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ExternalEdrServerEmulatorRouteHandlerMethod } from '../../..'; +import { buildSentinelOneRoutePath } from './utils'; +import type { EmulatorServerRouteDefinition } from '../../../lib/emulator_server.types'; + +export const getAgentActionDisconnectRouteDefinition = (): EmulatorServerRouteDefinition => { + return { + path: buildSentinelOneRoutePath('/agents/actions/disconnect'), + method: 'POST', + handler: disconnectActionRouteHandler, + }; +}; + +const disconnectActionRouteHandler: ExternalEdrServerEmulatorRouteHandlerMethod< + {}, + {}, + { + filter: { + ids: string; + }; + } +> = async (request) => { + return { + data: { + affected: request.payload.filter.ids.split(',').length, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/sentinelone/routes/agents_route.ts b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/sentinelone/routes/agents_route.ts new file mode 100644 index 0000000000000..6316e68636cb9 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/sentinelone/routes/agents_route.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + SentinelOneGetAgentsParams, + SentinelOneGetAgentsResponse, +} from '@kbn/stack-connectors-plugin/common/sentinelone/types'; +import type { DeepPartial, Mutable } from 'utility-types'; +import { SentinelOneDataGenerator } from '../../../../../../common/endpoint/data_generators/sentinelone_data_generator'; +import { buildSentinelOneRoutePath } from './utils'; +import type { ExternalEdrServerEmulatorRouteHandlerMethod } from '../../../external_edr_server_emulator.types'; +import type { EmulatorServerRouteDefinition } from '../../../lib/emulator_server.types'; + +const generator = new SentinelOneDataGenerator(); + +export const getAgentsRouteDefinition = (): EmulatorServerRouteDefinition => { + return { + path: buildSentinelOneRoutePath('/agents'), + method: 'GET', + handler: agentsRouteHandler, + }; +}; + +const agentsRouteHandler: ExternalEdrServerEmulatorRouteHandlerMethod< + {}, + SentinelOneGetAgentsParams +> = async (request) => { + const queryParams = request.query; + const agent: Mutable> = {}; + + if (queryParams.uuid) { + agent.uuid = queryParams.uuid; + } + + if (queryParams.ids) { + agent.id = queryParams.ids.split(',').at(0); + } + + return generator.generateSentinelOneApiAgentsResponse(agent); +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/sentinelone/routes/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/sentinelone/routes/index.ts new file mode 100644 index 0000000000000..0a8e8441b6351 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/sentinelone/routes/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getAgentActionConnectRouteDefinition } from './agent_action_connect_route'; +import { getAgentActionDisconnectRouteDefinition } from './agent_action_disconnect_route'; +import { getActivitiesRouteDefinition } from './activities_route'; +import { getAgentsRouteDefinition } from './agents_route'; +import type { EmulatorServerRouteDefinition } from '../../../lib/emulator_server.types'; + +export const getSentinelOneRouteDefinitions = (): EmulatorServerRouteDefinition[] => { + return [ + getAgentsRouteDefinition(), + getActivitiesRouteDefinition(), + getAgentActionConnectRouteDefinition(), + getAgentActionDisconnectRouteDefinition(), + ]; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/sentinelone/routes/utils.ts b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/sentinelone/routes/utils.ts new file mode 100644 index 0000000000000..d2b6f29376423 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/emulator_plugins/sentinelone/routes/utils.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// The base API path for the API requests for sentinelone. Value is the same as the one defined here: +// `x-pack/plugins/stack_connectors/server/connector_types/sentinelone/sentinelone.ts:50` +const BASE_API_PATH = '/web/api/v2.1'; + +export const buildSentinelOneRoutePath = (path: string): string => { + if (!path.startsWith('/')) { + throw new Error(`'path' must start with '/'!`); + } + + return `${BASE_API_PATH}${path}`; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/external_edr_server_emulator.ts b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/external_edr_server_emulator.ts new file mode 100644 index 0000000000000..270272de77d58 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/external_edr_server_emulator.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getCrowdstrikeEmulator } from './emulator_plugins/crowdstrike'; +import { handleProcessInterruptions } from '../common/nodejs_utils'; +import { EmulatorServer } from './lib/emulator_server'; +import type { ExternalEdrServerEmulatorCoreServices } from './external_edr_server_emulator.types'; +import { getSentinelOneEmulator } from './emulator_plugins/sentinelone'; + +export interface StartExternalEdrServerEmulatorOptions { + coreServices: ExternalEdrServerEmulatorCoreServices; + /** + * The port where the server should listen on. Default is `0` which means an available port is + * auto-assigned. + */ + port?: number; +} + +/** + * Starts a server that provides API emulator for external EDR systems in support of bi-directional + * response actions. + * + * After staring the server, the `emulatorServer.stopped` property provides a way to `await` until it + * is stopped + * + * @param options + */ +export const startExternalEdrServerEmulator = async ({ + port, + coreServices, +}: StartExternalEdrServerEmulatorOptions): Promise => { + const emulator = new EmulatorServer({ + logger: coreServices.logger, + port: port ?? 0, + services: coreServices, + }); + + // Register all emulators + await emulator.register(getSentinelOneEmulator()); + await emulator.register(getCrowdstrikeEmulator()); + + let wasStartedPromise: ReturnType; + + handleProcessInterruptions( + async () => { + wasStartedPromise = emulator.start(); + await wasStartedPromise; + await emulator.stopped; + }, + () => { + coreServices.logger.warning( + `Process was interrupted. Shutting down External EDR Server Emulator` + ); + emulator.stop(); + } + ); + + // @ts-expect-error TS2454: Variable 'wasStartedPromise' is used before being assigned. + await wasStartedPromise; + return emulator; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/external_edr_server_emulator.types.ts b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/external_edr_server_emulator.types.ts new file mode 100644 index 0000000000000..9773af6aae1bf --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/external_edr_server_emulator.types.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type { KbnClient } from '@kbn/test'; +import type { Client } from '@elastic/elasticsearch'; +import type { ToolingLog } from '@kbn/tooling-log'; +import type HapiTypes from '@hapi/hapi'; +import type { EmulatorServerRouteHandlerMethod } from './lib/emulator_server.types'; + +/** + * Core services that will be made available to all emulator plugin routes via `Request.pre.services`. + * + * **NOTE**: Only services that are emulator agnostic should be added here. Any service that is specific + * for only a given emulator should instead be added to the route registration via + * `route.options.pre`. See {@link https://hapi.dev/api/?v=21.3.3#-routeoptionspre|Hapi Route Options} + * for more on how to use `pre`requisites option + */ +export interface ExternalEdrServerEmulatorCoreServices { + readonly kbnClient: KbnClient; + readonly esClient: Client; + readonly logger: ToolingLog; +} + +export type ExternalEdrServerEmulatorRouteHandlerMethod< + TParams extends HapiTypes.Request['params'] = any, + TQuery extends HapiTypes.Request['query'] = any, + TPayload extends HapiTypes.Request['payload'] = any, + TPre extends HapiTypes.Request['pre'] = { services: ExternalEdrServerEmulatorCoreServices } +> = EmulatorServerRouteHandlerMethod; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/index.ts new file mode 100644 index 0000000000000..15a48654f2963 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/index.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RunFn } from '@kbn/dev-cli-runner'; +import { run } from '@kbn/dev-cli-runner'; +import { dump } from '../common/utils'; +import { startExternalEdrServerEmulator } from './external_edr_server_emulator'; +import type { ExternalEdrServerEmulatorCoreServices } from './external_edr_server_emulator.types'; +import { createRuntimeServices } from '../common/stack_services'; +import { createToolingLogger } from '../../../common/endpoint/data_loaders/utils'; + +export { + startExternalEdrServerEmulator, + type StartExternalEdrServerEmulatorOptions, +} from './external_edr_server_emulator'; +export * from './external_edr_server_emulator.types'; + +export const cli = () => { + run( + cliRunner, + + // Options + { + description: `Start external API emulator`, + flags: { + string: ['kibana', 'elastic', 'username', 'password', 'apiKey'], + boolean: ['asSuperuser'], + default: { + kibana: 'http://127.0.0.1:5601', + elasticsearch: 'http://127.0.0.1:9200', + username: 'elastic', + password: 'changeme', + apiKey: '', + asSuperuser: false, + port: 0, + }, + help: ` + --port The port number where the server should listen on + (Default is 0 - which means an available port is assigned randomly) + --username User name to be used for auth against elasticsearch and + kibana (Default: elastic). + **IMPORTANT:** if 'asSuperuser' option is not used, then the + user defined here MUST have 'superuser' AND 'kibana_system' roles + --password User name Password (Default: changeme) + --apiKey An API key to use for communication with Kibana/Elastisearch. Would be + used instead of username/password + --asSuperuser If defined, then a Security super user will be created using the + the credentials defined via 'username' and 'password' options. This + new user will then be used to run this utility. + --kibana The url to Kibana (Default: http://127.0.0.1:5601) + --elasticsearch The url to Elasticsearch (Default: http://127.0.0.1:9200) + `, + }, + } + ); +}; + +const cliRunner: RunFn = async (cliContext) => { + const username = cliContext.flags.username as string; + const password = cliContext.flags.password as string; + const apiKey = cliContext.flags.apiKey as string; + const kibanaUrl = cliContext.flags.kibana as string; + const elasticsearchUrl = cliContext.flags.elasticsearch as string; + const asSuperuser = cliContext.flags.asSuperuser as boolean; + const log = cliContext.log; + const port = Number(cliContext.flags.port as string); + + createToolingLogger.setDefaultLogLevelFromCliFlags(cliContext.flags); + + const { kbnClient, esClient } = await createRuntimeServices({ + username, + password, + apiKey, + kibanaUrl, + elasticsearchUrl, + asSuperuser, + log, + noCertForSsl: true, + }); + + const coreServices: ExternalEdrServerEmulatorCoreServices = { + get kbnClient() { + return kbnClient; + }, + get esClient() { + return esClient; + }, + get logger() { + return log; + }, + }; + + const emulatorServer = await startExternalEdrServerEmulator({ + port, + coreServices, + }); + + log.debug(`Server info:\n${dump(emulatorServer.info)}`); + + await emulatorServer.stopped; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/lib/emulator_server.ts b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/lib/emulator_server.ts new file mode 100644 index 0000000000000..cfffe0b942a80 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/lib/emulator_server.ts @@ -0,0 +1,214 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type { ServerRoute } from '@hapi/hapi'; +import Hapi from '@hapi/hapi'; +import type { ToolingLog } from '@kbn/tooling-log'; +import type { + EmulatorServerPlugin, + EmulatorServerPluginRegisterOptions, + EmulatorServerRouteDefinition, +} from './emulator_server.types'; +import type { DeferredPromiseInterface } from '../../common/utils'; +import { getDeferredPromise, prefixedOutputLogger } from '../../common/utils'; +import { createToolingLogger } from '../../../../common/endpoint/data_loaders/utils'; + +interface EmulatorServerOptions { + /** + * An object that contains services to be exposed + */ + services?: TServices; + logger?: ToolingLog; + logPrefix?: string; + port?: number; +} + +/** + * An HTTP server module wrapped around Hapi + */ +export class EmulatorServer { + protected readonly server: Hapi.Server & { app: { services: TServices | {} } }; + protected log: ToolingLog; + private stoppedDeferred: DeferredPromiseInterface; + private wasStarted: boolean = false; + + constructor(protected readonly options: EmulatorServerOptions = {}) { + this.log = prefixedOutputLogger( + (this.options.logPrefix || this.constructor.name) ?? 'EmulatorServer', + options.logger ?? createToolingLogger() + ); + + // @ts-expect-error + this.server = Hapi.server({ + port: this.options.port ?? 0, + }); + this.server.app.services = this.options.services ?? {}; + this.stoppedDeferred = getDeferredPromise(); + this.stoppedDeferred.resolve(); + + this.server.events.on('route', (route) => { + this.log.info( + `added route: [${route.realm.plugin ?? 'CORE'}] ${route.method.toUpperCase()} ${route.path}` + ); + }); + + this.registerInternalRoutes(); + this.log.verbose(`Instance created`); + } + + protected registerInternalRoutes() { + this.server.route({ + method: 'GET', + path: '/_status', + handler: async (req) => ({ + status: 'ok', + started: new Date(req.server.info.started).toISOString(), + uri: req.server.info.uri, + plugins: Object.keys(req.server.registrations), + routes: req.server.table().map((route) => `${route.method.toUpperCase()} ${route.path}`), + }), + }); + } + + /** + * Utility that creates a Hapi Route definition based on the Route Definition defined for this framework + * @param routesToRegister + * @protected + */ + protected createHapiRouteDefinition( + routesToRegister: EmulatorServerRouteDefinition | EmulatorServerRouteDefinition[] + ): ServerRoute[] { + const routes = Array.isArray(routesToRegister) ? routesToRegister : [routesToRegister]; + + for (const routeDefinition of routes) { + if (typeof routeDefinition.options === 'function') { + throw new Error(`a callback function for 'route.options' is not currently supported!`); + } + + // Inject `services` to every request under `request.pre.services` + routeDefinition.options = routeDefinition.options ?? {}; + routeDefinition.options.pre = routeDefinition.options.pre ?? []; + routeDefinition.options.pre.unshift({ + method: () => this.server.app.services, + assign: 'services', + }); + } + + return routes; + } + + /** + * Access information about the running server + */ + public get info(): Hapi.ServerInfo { + return this.server.info; + } + + /** + * A promise that resolves when the server is stopped + * + * **IMPORTANT**: Only use this property after server has been started! + */ + public get stopped(): Promise { + if (!this.wasStarted) { + this.log.warning(`Can not access 'stopped' promise. Server not started`); + return Promise.resolve(); + } + + return this.stoppedDeferred.promise; + } + + /** + * Register a plugin with the server + * @param register + * @param prefix + * @param options + */ + public async register({ register, prefix, ...options }: EmulatorServerPlugin) { + const createHapiRouteDefinition = this.createHapiRouteDefinition.bind(this); + const services = this.server.app.services; + + await this.server.register( + { + ...options, + register(pluginScopedServer) { + const scopedServer: EmulatorServerPluginRegisterOptions = { + router: { + route: (routesToRegister) => { + return pluginScopedServer.route(createHapiRouteDefinition(routesToRegister)); + }, + }, + + expose: (key: string, value: any): void => { + pluginScopedServer.expose(key, value); + }, + + services, + }; + + return register(scopedServer); + }, + }, + { + routes: { + // All plugin routes are namespaced by `prefix` or the plugin name if `prefix` not defined + prefix: prefix || `/${options.name}`, + }, + } + ); + } + + /** + * Register a route with the root server (non-plugin scoped) + * @param routesToRegister + */ + public route( + routesToRegister: EmulatorServerRouteDefinition | EmulatorServerRouteDefinition[] + ): void { + return this.server.route(this.createHapiRouteDefinition(routesToRegister)); + } + + public async start() { + if (this.wasStarted) { + this.log.warning(`Can not start server - it already has been started and is running`); + } + + this.stoppedDeferred = getDeferredPromise(); + await this.server.start(); + this.wasStarted = true; + + this.server.events.once('stop', () => { + this.log.verbose(`Hapi server was stopped!`); + this.wasStarted = false; + this.stoppedDeferred.resolve(); + }); + + this.log.info(`Server started and available at: ${this.server.info.uri}`); + } + + public async stop() { + this.log.debug(`Stopping Hapi server: ${this.server.info.uri}`); + await this.server.stop(); + } + + /** + * Returns a plugin client that enables interactions with the given plugin emulator. + * Plugins can expose interfaces via the `expose()` method that is available to the plugin's + * `register()` method. + */ + public getClient(pluginName: string): TClient { + const pluginExposedInterface = this.server.plugins[pluginName as keyof Hapi.PluginProperties]; + + if (!pluginExposedInterface) { + throw new Error(`No plugin named [${pluginName}] registered!`); + } + + return pluginExposedInterface as TClient; + } +} diff --git a/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/lib/emulator_server.types.ts b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/lib/emulator_server.types.ts new file mode 100644 index 0000000000000..5d8cb5c1ee595 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/api_emulator/lib/emulator_server.types.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type HapiTypes from '@hapi/hapi'; + +export interface EmulatorServerPluginRegisterOptions = {}> { + router: { + route(route: EmulatorServerRouteDefinition | EmulatorServerRouteDefinition[]): void; + }; + + /** + * Expose content on the server related to the plugin + * + * @param key + * @param value + * + * @see https://hapi.dev/api/?v=21.3.3#-serverexposekey-value-options + */ + expose: (key: string, value: any) => void; + + /** + * Core services defined at the server level + */ + services: TServices; +} + +export interface EmulatorServerPlugin = any> + extends Omit, 'register'> { + register: (options: EmulatorServerPluginRegisterOptions) => void | Promise; + name: string; + /** + * A prefix for the routes that will be registered via this plugin. Default is the plugin's `name` + */ + prefix?: Required['routes']['prefix']; +} + +export interface EmulatorServerRequest< + TParams extends HapiTypes.Request['params'] = any, + TQuery extends HapiTypes.Request['query'] = any, + TPayload extends HapiTypes.Request['payload'] = any, + TPre extends HapiTypes.Request['pre'] = any +> extends HapiTypes.Request { + params: TParams; + query: TQuery; + payload: TPayload; + pre: TPre; +} + +export type EmulatorServerRouteHandlerMethod< + TParams extends HapiTypes.Request['params'] = any, + TQuery extends HapiTypes.Request['query'] = any, + TPayload extends HapiTypes.Request['payload'] = any, + TPre extends HapiTypes.Request['pre'] = any +> = ( + request: EmulatorServerRequest, + h: HapiTypes.ResponseToolkit, + err?: Error +) => HapiTypes.Lifecycle.ReturnValue; + +export interface EmulatorServerRouteDefinition< + TParams extends HapiTypes.Request['params'] = any, + TQuery extends HapiTypes.Request['query'] = any, + TPayload extends HapiTypes.Request['payload'] = any, + TPre extends HapiTypes.Request['pre'] = any +> extends Omit { + handler: EmulatorServerRouteHandlerMethod; +} diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/agent_downloads_service.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/agent_downloads_service.ts index 5e2e003b6096b..488e1b10160e8 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/agent_downloads_service.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/agent_downloads_service.ts @@ -10,6 +10,7 @@ import { join } from 'path'; import fs from 'fs'; import nodeFetch from 'node-fetch'; import { finished } from 'stream/promises'; +import { handleProcessInterruptions } from './nodejs_utils'; import { createToolingLogger } from '../../../common/endpoint/data_loaders/utils'; import { SettingsStorage } from './settings_storage'; @@ -173,43 +174,6 @@ class AgentDownloadStorage extends SettingsStorage } } -const handleProcessInterruptions = async ( - runFn: (() => T) | (() => Promise), - /** The synchronous cleanup callback */ - cleanup: () => void -): Promise => { - const eventNames = ['SIGINT', 'exit', 'uncaughtException', 'unhandledRejection']; - const stopListeners = () => { - for (const eventName of eventNames) { - process.off(eventName, cleanup); - } - }; - - for (const eventName of eventNames) { - process.on(eventName, cleanup); - } - - let runnerResponse: T | Promise; - - try { - runnerResponse = runFn(); - } catch (e) { - stopListeners(); - throw e; - } - - // @ts-expect-error upgrade typescript v4.9.5 - if ('finally' in runnerResponse) { - (runnerResponse as Promise).finally(() => { - stopListeners(); - }); - } else { - stopListeners(); - } - - return runnerResponse; -}; - const agentDownloadsClient = new AgentDownloadStorage(); export interface DownloadAndStoreAgentResponse extends DownloadedAgentInfo { diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/nodejs_utils.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/nodejs_utils.ts new file mode 100644 index 0000000000000..ca204d2a74ff6 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/nodejs_utils.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Executes/Runs the provided function and sets up event listeners for NodeJs process interruptions so + * that a `cleanup()` method can executed if an interruption is caught + * @param runFn + * @param cleanup + */ +export const handleProcessInterruptions = async ( + runFn: (() => T) | (() => Promise), + /** The synchronous cleanup callback */ + cleanup: () => void +): Promise => { + const eventNames = ['SIGINT', 'exit', 'uncaughtException', 'unhandledRejection']; + const stopListeners = () => { + for (const eventName of eventNames) { + process.off(eventName, cleanup); + } + }; + + for (const eventName of eventNames) { + process.on(eventName, cleanup); + } + + let runnerResponse: T | Promise; + + try { + runnerResponse = runFn(); + } catch (e) { + stopListeners(); + throw e; + } + + // @ts-expect-error upgrade typescript v4.9.5 + if ('finally' in runnerResponse) { + (runnerResponse as Promise).finally(() => { + stopListeners(); + }); + } else { + stopListeners(); + } + + return runnerResponse; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/utils.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/utils.ts index 72a59743d4435..0161c3470716d 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/utils.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/utils.ts @@ -71,3 +71,26 @@ export const prefixedOutputLogger = (prefix: string, log: ToolingLog): ToolingLo export const dump = (content: any, depth: number = 5): string => { return inspect(content, { depth }); }; + +export interface DeferredPromiseInterface { + promise: Promise; + resolve: (data: T) => void; + reject: (e: Error) => void; +} + +/** + * Returns back an interface that provide a Promise along with exposed method to resolve it and reject it + * from outside of the actual Promise executor + */ +export const getDeferredPromise = function (): DeferredPromiseInterface { + let resolve: DeferredPromiseInterface['resolve']; + let reject: DeferredPromiseInterface['reject']; + + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + + // @ts-ignore + return { promise, resolve, reject }; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/sentinelone_host/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/sentinelone_host/index.ts index d2f4190821413..2c179f6e853ab 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/sentinelone_host/index.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/sentinelone_host/index.ts @@ -111,6 +111,7 @@ const runCli: RunFn = async ({ log, flags }) => { url: kibanaUrl, username, password, + noCertForSsl: true, }); const runningS1VMs = ( diff --git a/x-pack/plugins/security_solution/scripts/endpoint/start_external_edr_server_emulator.js b/x-pack/plugins/security_solution/scripts/endpoint/start_external_edr_server_emulator.js new file mode 100644 index 0000000000000..5874264a85e2b --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/start_external_edr_server_emulator.js @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +require('../../../../../src/setup_node_env'); +require('./api_emulator').cli();