Repository: briandelmsft/STAT-Function

Issue:
File Module - Hyperlink Filename to filehash in XDR portal
Possibly just a file search if there's not a more appropriate option:

![Image](https://github.com/user-attachments/assets/7c6c9359-0474-4205-a826-1098d561298c)

Hints from issue comments:

Solution patch:
diff --git a/classes/__init__.py b/classes/__init__.py
index 07cfc28..82e4a7f 100644
--- a/classes/__init__.py
+++ b/classes/__init__.py
@@ -36,6 +36,7 @@ class BaseModule:
     def __init__(self):
         self.Accounts = []
         self.AccountsCount = 0
+        self.AccountsOnPrem = []
         self.Alerts = []
         self.Domains = []
         self.DomainsCount = 0
@@ -85,6 +86,7 @@ def load_alert_trigger(self, req_body):
     def load_from_input(self, basebody):
         self.Accounts = basebody['Accounts']
         self.AccountsCount = basebody['AccountsCount']
+        self.AccountsOnPrem = basebody.get('AccountsOnPrem', [])
         self.Alerts = basebody.get('Alerts', [])
         self.Domains = basebody['Domains']
         self.DomainsCount = basebody['DomainsCount']
@@ -115,8 +117,18 @@ def load_from_input(self, basebody):
         self.CurrentVersion = basebody.get('CurrentVersion')
         self.ModuleName = basebody.get('ModuleName')
 
-    def add_ip_entity(self, address, geo_data, rawentity):
-        self.IPs.append({'Address': address, 'GeoData': geo_data, 'RawEntity': rawentity })
+    def add_ip_entity(self, address, geo_data, rawentity, ip_type:int=9):
+        '''Adds an IP entity, types 1=global, 2=private, 3=link-local, 9=unknown'''
+        self.IPs.append({'Address': address, 'IPType': ip_type, 'GeoData': geo_data, 'RawEntity': rawentity })
+
+    def check_global_and_local_ips(self):
+        '''Checks if any private or global IPs are in the IPs array for IP comment'''
+        found = False
+        for ip in self.IPs:
+            if ip.get('IPType') != 3:
+                found = True
+                break
+        return found
 
     def add_host_entity(self, fqdn, hostname, dnsdomain, mdedeviceid, rawentity, mde_enrichment:str='Unknown'):
         if mdedeviceid:
@@ -127,6 +139,9 @@ def add_host_entity(self, fqdn, hostname, dnsdomain, mdedeviceid, rawentity, mde
     def add_account_entity(self, data):
         self.Accounts.append(data)
 
+    def add_onprem_account_entity(self, data):
+        self.AccountsOnPrem.append(data)
+
     def get_ip_list(self):
         ip_list = []
         for ip in self.IPs:
@@ -171,13 +186,17 @@ def get_ip_kql_table(self):
 '''
         return kql
     
-    def get_account_kql_table(self):
+    def get_account_kql_table(self, include_unsynced:bool=False):
 
         account_data = []
 
         for account in self.Accounts:
             account_data.append({'userPrincipalName': account.get('userPrincipalName'), 'SamAccountName': account.get('onPremisesSamAccountName'), \
                                  'SID': account.get('onPremisesSecurityIdentifier'), 'id': account.get('id'), 'ManagerUPN': account.get('manager', {}).get('userPrincipalName')})
+        if include_unsynced:
+            for onprem_account in self.AccountsOnPrem:
+                account_data.append({'userPrincipalName': onprem_account.get('userPrincipalName', 'NoUPNFound'), 'SamAccountName': onprem_account.get('onPremisesSamAccountName'), \
+                                     'SID': onprem_account.get('onPremisesSecurityIdentifier'), 'id': onprem_account.get('id', 'NoIdFound'), 'ManagerUPN': 'NoManagerUpnFound'})
 
         encoded = urllib.parse.quote(json.dumps(account_data))
 
@@ -245,27 +264,61 @@ def get_domain_kql_table(self):
 '''
         return kql
         
-    def get_account_id_list(self):
+    def get_account_id_and_sid_list(self):
         account_list = []
         for account in self.Accounts:
-            account_list.append(account['id'])
+            try:
+                account_list.append(account['id'])
+            except KeyError:
+                raw_entity = account.get('RawEntity', {})
+                sid = data.coalesce(raw_entity.get('properties',{}).get('sid'), raw_entity.get('sid'), raw_entity.get('Sid'))
+                if sid:
+                    account_list.append(sid)
+
+        for onprem_account in self.AccountsOnPrem:
+            try:
+                account_list.append(onprem_account['onPremisesSecurityIdentifier'])
+            except KeyError:
+                pass
         
         return account_list
 
-    def get_account_upn_list(self):
+    def get_account_upn_list(self, include_unsynced:bool=False):
+        '''Returns a list of all UPNs, including unsynced accounts accounts'''
         account_list = []
         for account in self.Accounts:
-            account_list.append(account['userPrincipalName'])
+            try:
+                account_list.append(account['userPrincipalName'])
+            except KeyError:
+                pass
+
+        if include_unsynced:
+            for onprem_account in self.AccountsOnPrem:
+                try:
+                    account_list.append(onprem_account['userPrincipalName'])
+                except KeyError:
+                    pass
         
         return account_list
     
     def get_account_sam_list(self):
         account_list = []
         for account in self.Accounts:
-            account_list.append(account['onPremisesSamAccountName'])
+            try:
+                account_list.append(account['onPremisesSamAccountName'])
+            except:
+                pass
 
         return account_list
     
+    def get_host_mdeid_list(self):
+        host_list = []
+        for host in self.Hosts:
+            if host.get('MdatpDeviceId'):
+                host_list.append(host.get('MdatpDeviceId'))
+        
+        return host_list
+    
     def get_alert_ids(self):
         alert_list = []
         for alert in self.Alerts:
@@ -495,6 +548,7 @@ def __init__(self):
         self.TopUserThresholdCount = 0
         self.AnyThreatScoreTrendingUp = False
         self.ModuleName = 'MDCAModule'
+        self.Warning = "The Sentinel Triage AssistanT's (STAT) Microsoft Defender for Cloud Apps module has been deprecated. This is due to Microsoft's deprecation of the MDCA investigation score. Please remove the MDCA module from your STAT Analysis."
 
 class RunPlaybook:
     '''A RunPlaybook module object'''
@@ -562,6 +616,58 @@ def load_from_input(self, body):
         self.HostsHighestRiskScore = body['HostsHighestRiskScore']
         self.DetailedResults = body['DetailedResults']
 
+class DeviceExposureModule:
+    '''An Device Exposure module object'''
+    def __init__(self):
+        self.AnalyzedEntities = 0
+        self.ModuleName = 'DeviceExposureModule'
+        self.Nodes = []
+        self.Paths = []
+
+    def load_from_input(self, body):
+        self.AnalyzedEntities = body['AnalyzedEntities']
+        self.Nodes = body['Nodes']
+        self.Paths = body['Paths']
+
+    def nodes_without_paths(self):
+        '''Only returns nodes where no path from node is present'''
+        out = []
+        path_nodes = []
+        for path in self.Paths:
+            path_nodes.append(path['ComputerNodeId'])
+
+        for node in self.Nodes:
+            if node['ComputerNodeId'] not in path_nodes:
+                out.append(node)
+
+        return out
+
+class UserExposureModule:
+    '''An User Exposure module object'''
+    def __init__(self):
+        self.AnalyzedEntities = 0
+        self.ModuleName = 'UserExposureModule'
+        self.Nodes = []
+        self.Paths = []
+
+    def load_from_input(self, body):
+        self.AnalyzedEntities = body['AnalyzedEntities']
+        self.Nodes = body['Nodes']
+        self.Paths = body['Paths']
+
+    def nodes_without_paths(self):
+        '''Only returns nodes where no path from node is present'''
+        out = []
+        path_nodes = []
+        for path in self.Paths:
+            path_nodes.append(path['UserNodeId'])
+
+        for node in self.Nodes:
+            if node['UserNodeId'] not in path_nodes:
+                out.append(node)
+
+        return out
+
 class CreateIncident:
     '''A CreateIncident object'''
     def __init__(self):
diff --git a/debug/debug.py b/debug/debug.py
index 342eb87..64b96c8 100644
--- a/debug/debug.py
+++ b/debug/debug.py
@@ -23,6 +23,9 @@ def debug_module (req_body):
             default_debug(debug_out)
             token_debug(debug_out)
             rbac_debug(debug_out)
+        case 'comment':
+            default_debug(debug_out)
+            comment_debug(debug_out)
         case 'exception':
             exception_debug(debug_out)
         case _:
@@ -36,7 +39,6 @@ def default_debug(debug_out:DebugModule):
     debug_out.LAEndpoint = os.getenv('LOGANALYTICS_ENDPOINT')
     debug_out.M365Endpoint = os.getenv('M365_ENDPOINT')
     debug_out.MDEEndpoint = os.getenv('MDE_ENDPOINT')
-    debug_out.MDCAEndpoint = os.getenv('MDCA_ENDPOINT')
     debug_out.TenantId = os.getenv('AZURE_TENANT_ID')
     debug_out.KVEndpoint = os.getenv('KEYVAULT_ENDPOINT')
     debug_out.KVSecretName = os.getenv('KEYVAULT_SECRET_NAME')
@@ -135,4 +137,14 @@ def parse_response(debug_out:DebugModule, response):
 
     debug_out.StatusCode = response.status_code
     debug_out.Url = response.url
-    debug_out.TextContent = response.text
\ No newline at end of file
+    debug_out.TextContent = response.text
+
+def comment_debug(debug_out:DebugModule):
+    comment = debug_out.Params.get('Comment')
+    base_module = BaseModule()
+    base_module.MultiTenantConfig = debug_out.Params.get('MultiTenantConfig', {})
+    base_module.IncidentARMId = debug_out.Params['IncidentARMId']
+    base_module.IncidentAvailable = True
+    response = rest.add_incident_comment(base_module, comment)
+    debug_out.CommentStatus = response.status_code
+    debug_out.CommentResponse = response.json()
\ No newline at end of file
diff --git a/modules/base.py b/modules/base.py
index d39eb3a..c196c32 100644
--- a/modules/base.py
+++ b/modules/base.py
@@ -4,6 +4,7 @@
 import time
 import logging
 import requests
+import ipaddress
 
 stat_version = None
 
@@ -15,7 +16,10 @@ def execute_base_module (req_body):
     
     base_object = BaseModule()
 
-    trigger_type = req_body['Body'].get('objectSchemaType', 'alert')
+    try:
+        trigger_type = req_body['Body'].get('objectSchemaType', 'alert')
+    except:
+        raise STATError('The Base Module Incident or Alert body is missing or invalid. This may be caused by a missing or incorrect input to the module, or by running the logic app manually with no incident context.')
 
     base_object.MultiTenantConfig = req_body.get('MultiTenantConfig', {})
     enrich_mfa = req_body.get('EnrichAccountsWithMFA', True)
@@ -42,7 +46,7 @@ def execute_base_module (req_body):
     append_other_entities(entities)
 
     base_object.CurrentVersion = data.get_current_version()
-    base_object.EntitiesCount = base_object.AccountsCount + base_object.DomainsCount + base_object.FileHashesCount + base_object.FilesCount + base_object.HostsCount + base_object.OtherEntitiesCount + base_object.URLsCount
+    base_object.EntitiesCount = base_object.AccountsCount + base_object.IPsCount + base_object.DomainsCount + base_object.FileHashesCount + base_object.FilesCount + base_object.HostsCount + base_object.OtherEntitiesCount + base_object.URLsCount
 
     org_info = json.loads(rest.rest_call_get(base_object, api='msgraph', path='/v1.0/organization').content)
     base_object.TenantDisplayName = org_info['value'][0]['displayName']
@@ -67,10 +71,10 @@ def execute_base_module (req_body):
     if req_body.get('AddAccountComments', True) and base_object.AccountsCount > 0:
         account_comment = 'Account Info:<br>' + get_account_comment()
 
-    if req_body.get('AddIPComments', True) and base_object.IPsCount > 0:
+    if req_body.get('AddIPComments', True) and base_object.check_global_and_local_ips():
         ip_comment = 'IP Info:<br>' + get_ip_comment()
 
-    if (req_body.get('AddAccountComments', True) and base_object.AccountsCount > 0) or (req_body.get('AddIPComments', True) and base_object.IPsCount > 0):
+    if (req_body.get('AddAccountComments', True) and base_object.AccountsCount > 0) or (req_body.get('AddIPComments', True) and base_object.check_global_and_local_ips()):
         comment = account_comment + '<br><p>' + ip_comment
         rest.add_incident_comment(base_object, comment)
 
@@ -128,21 +132,37 @@ def process_alert_trigger (req_body):
 
 def enrich_ips (entities, get_geo):
     ip_entities = list(filter(lambda x: x['kind'].lower() == 'ip', entities))
-    base_object.IPsCount = len(ip_entities)
 
     for ip in ip_entities:
-        current_ip = data.coalesce(ip.get('properties', {}).get('address'), ip.get('Address'))
-        raw_entity = data.coalesce(ip.get('properties'), ip)
-        if get_geo:
-            path = base_object.SentinelRGARMId + "/providers/Microsoft.SecurityInsights/enrichment/ip/geodata/?api-version=2023-04-01-preview&ipAddress=" + current_ip
-            try:
-                response = rest.rest_call_get(base_object, api='arm', path=path)
-            except STATError:
-                base_object.add_ip_entity(address=current_ip, geo_data={}, rawentity=raw_entity)
+        try:
+            ip_data = ipaddress.ip_address(data.coalesce(ip.get('properties', {}).get('address'), ip.get('Address')))
+            raw_entity = data.coalesce(ip.get('properties'), ip)
+        except:
+            #Skip any IPs that cannot be parsed
+            continue
+        
+        if ip_data.is_loopback:
+            #Skip any loopback IPs
+            continue
+        elif ip_data.is_link_local:
+            base_object.add_ip_entity(address=ip_data.compressed, ip_type=3, geo_data={}, rawentity=raw_entity)
+        elif ip_data.is_private:
+            base_object.add_ip_entity(address=ip_data.compressed, ip_type=2, geo_data={}, rawentity=raw_entity)
+        elif ip_data.is_global:
+            if get_geo:
+                path = base_object.SentinelRGARMId + "/providers/Microsoft.SecurityInsights/enrichment/ip/geodata/?api-version=2023-04-01-preview&ipAddress=" + ip_data.compressed
+                try:
+                    response = rest.rest_call_get(base_object, api='arm', path=path)
+                except STATError:
+                    base_object.add_ip_entity(address=ip_data.compressed, ip_type=1, geo_data={}, rawentity=raw_entity)
+                else:
+                    base_object.add_ip_entity(address=ip_data.compressed, ip_type=1, geo_data=json.loads(response.content), rawentity=raw_entity)
             else:
-                base_object.add_ip_entity(address=current_ip, geo_data=json.loads(response.content), rawentity=raw_entity)
+                base_object.add_ip_entity(address=ip_data.compressed, ip_type=1, geo_data={}, rawentity=raw_entity)
         else:
-            base_object.add_ip_entity(address=current_ip, geo_data={}, rawentity=raw_entity)
+            base_object.add_ip_entity(address=ip_data.compressed, ip_type=8, geo_data={}, rawentity=raw_entity)
+
+    base_object.IPsCount = len(base_object.IPs)
 
 def enrich_accounts(entities):
     account_entities = list(filter(lambda x: x['kind'].lower() == 'account', entities))
@@ -155,7 +175,7 @@ def enrich_accounts(entities):
         upn_suffix = data.coalesce(account.get('properties',{}).get('upnSuffix'), account.get('UPNSuffix'))
         account_name = data.coalesce(account.get('properties',{}).get('accountName'), account.get('Name'))
         friendly_name = data.coalesce(account.get('properties',{}).get('friendlyName'), account.get('DisplayName'), account.get('Name'))
-        sid = data.coalesce(account.get('properties',{}).get('sid'), account.get('Sid'))
+        sid = data.coalesce(account.get('properties',{}).get('sid'), account.get('Sid'), account.get('sid'))
         nt_domain = data.coalesce(account.get('properties',{}).get('ntDomain'), account.get('NTDomain'))
         object_guid = data.coalesce(account.get('properties',{}).get('objectGuid'), account.get('ObjectGuid'))
         properties = data.coalesce(account.get('properties'), account)
@@ -239,6 +259,12 @@ def get_account_by_upn_or_id(account, attributes, properties, enrich_method:str=
         append_account_details(account, user_info, properties, enrich_method)
 
 def get_account_by_mail(account, attributes, properties, enrich_method:str='Mail'):
+    query = f'''union isfuzzy=true
+(datatable(userPrincipalName:string)[]),
+(IdentityInfo
+| where AccountUPN =~ '{account}'
+| summarize arg_max(TimeGenerated, *) by AccountUPN
+| project userPrincipalName=AccountUPN, id=AccountObjectId, onPremisesSecurityIdentifier=AccountSID, onPremisesDistinguishedName=OnPremisesDistinguishedName, onPremisesDomainName=AccountDomain, onPremisesSamAccountName=AccountName, mail=MailAddress, department=Department, jobTitle=JobTitle, accountEnabled=IsAccountEnabled, manager=Manager)'''
     try:
         user_info = json.loads(rest.rest_call_get(base_object, api='msgraph', path=f'''/v1.0/users?$filter=(mail%20eq%20'{account}')&$select={attributes}''').content)
     except STATError:
@@ -247,24 +273,43 @@ def get_account_by_mail(account, attributes, properties, enrich_method:str='Mail
         if user_info['value']:
             append_account_details(account, user_info['value'][0], properties, enrich_method)
         else:
-            base_object.add_account_entity({'EnrichmentMethod': f'{enrich_method} - No Match', 'RawEntity': properties})
+            results = rest.execute_la_query(base_object, query, 14)
+            if results:
+                user = results[0]
+                user['EnrichmentMethod'] = f'UPN-IdentityInfo'
+                base_object.add_onprem_account_entity(user)
+            else:
+                base_object.add_account_entity({'EnrichmentMethod': f'UPN-IdentityInfo - No Match', 'RawEntity': properties})
+            #base_object.add_account_entity({'EnrichmentMethod': f'{enrich_method} - No Match', 'RawEntity': properties})
 
 def get_account_by_dn(account, attributes, properties, enrich_method:str='DN'):
 
     query = f'''union isfuzzy=true
-(datatable(test:string)[]),
+(datatable(onPremisesDistinguishedName:string)[]),
 (IdentityInfo
 | where OnPremisesDistinguishedName =~ '{account}'
 | summarize arg_max(TimeGenerated, *) by OnPremisesDistinguishedName
-| project AccountUPN)'''
+| project userPrincipalName=AccountUPN, id=AccountObjectId, onPremisesSecurityIdentifier=AccountSID, onPremisesDistinguishedName=OnPremisesDistinguishedName, onPremisesDomainName=AccountDomain, onPremisesSamAccountName=AccountName, mail=MailAddress, department=Department, jobTitle=JobTitle, accountEnabled=IsAccountEnabled, manager=Manager)'''
 
     results = rest.execute_la_query(base_object, query, 14)
-    if results:
-        get_account_by_upn_or_id(results[0]['AccountUPN'], attributes, properties, enrich_method)
+    if results and results[0]['id']:
+        get_account_by_upn_or_id(results[0]['userPrincipalName'], attributes, properties, enrich_method)
+    elif results:
+        user = results[0]
+        user['EnrichmentMethod'] = f'DN-IdentityInfo'
+        base_object.add_onprem_account_entity(user)
     else:
         base_object.add_account_entity({'EnrichmentMethod': f'{enrich_method} - No Match', 'RawEntity': properties})
 
 def get_account_by_sid(account, attributes, properties, enrich_method:str='SID'):
+
+    query = f'''union isfuzzy=true
+(datatable(onPremisesSecurityIdentifier:string)[]),
+(IdentityInfo
+| where AccountSID =~ '{account}'
+| summarize arg_max(TimeGenerated, *) by AccountSID
+| project userPrincipalName=AccountUPN, id=AccountObjectId, onPremisesSecurityIdentifier=AccountSID, onPremisesDistinguishedName=OnPremisesDistinguishedName, onPremisesDomainName=AccountDomain, onPremisesSamAccountName=AccountName, mail=MailAddress, department=Department, jobTitle=JobTitle, accountEnabled=IsAccountEnabled, manager=Manager)'''
+
     try:
         user_info = json.loads(rest.rest_call_get(base_object, api='msgraph', path=f'''/v1.0/users?$filter=(onPremisesSecurityIdentifier%20eq%20'{account}')&$select={attributes}''').content)
     except STATError:
@@ -273,19 +318,31 @@ def get_account_by_sid(account, attributes, properties, enrich_method:str='SID')
         if user_info['value']:
             append_account_details(account, user_info['value'][0], properties, enrich_method)
         else:
-            base_object.add_account_entity({'EnrichmentMethod': f'{enrich_method} - No Match', 'RawEntity': properties})
+            results = rest.execute_la_query(base_object, query, 14)
+            if results:
+                user = results[0]
+                user['EnrichmentMethod'] = f'SID-IdentityInfo'
+                base_object.add_onprem_account_entity(user)
+            else:
+                base_object.add_account_entity({'EnrichmentMethod': f'SID-IdentityInfo - No Match', 'RawEntity': properties})
 
 def get_account_by_samaccountname(account, attributes, properties, enrich_method:str='SAMAccountName'):
     query = f'''union isfuzzy=true
-(datatable(test:string)[]),
+(datatable(onPremisesSecurityIdentifier:string)[]),
 (IdentityInfo
 | where AccountName =~ '{account}'
-| summarize arg_max(TimeGenerated, *) by AccountName
-| project AccountUPN)'''
+| summarize arg_max(TimeGenerated, *) by AccountSID, AccountObjectId
+| project userPrincipalName=AccountUPN, id=AccountObjectId, onPremisesSecurityIdentifier=AccountSID, onPremisesDistinguishedName=OnPremisesDistinguishedName, onPremisesDomainName=AccountDomain, onPremisesSamAccountName=AccountName, mail=MailAddress, department=Department, jobTitle=JobTitle, accountEnabled=IsAccountEnabled, manager=Manager)'''
 
     results = rest.execute_la_query(base_object, query, 14)
-    if results:
-        get_account_by_upn_or_id(results[0]['AccountUPN'], attributes, properties, enrich_method)
+    if len(results) == 1 and results[0].get('id'):
+        get_account_by_upn_or_id(results[0]['id'], attributes, properties, enrich_method)
+    elif len(results) == 1:
+        user = results[0]
+        user['EnrichmentMethod'] = f'SAMAccountName-IdentityInfo'
+        base_object.add_onprem_account_entity(user)
+    elif len(results) > 1:
+        base_object.add_account_entity({'EnrichmentMethod': f'{enrich_method} - Multiple Matches', 'RawEntity': properties})
     else:
         base_object.add_account_entity({'EnrichmentMethod': f'{enrich_method} - No Match', 'RawEntity': properties})
 
@@ -394,7 +451,10 @@ def get_account_comment():
                              'MfaRegistered': account.get('isMfaRegistered'), 'SSPREnabled': account.get('isSSPREnabled'), \
                              'SSPRRegistered': account.get('isSSPRRegistered')})
         
-    link_template = f'https://portal.azure.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/overview/userId/ed2a76d8-c545-4ada-9f45-8c86667394f4'
+    for onprem_acct in base_object.AccountsOnPrem:
+        account_list.append(
+            {'UserPrincipalName': data.coalesce(onprem_acct.get('userPrincipalName'),onprem_acct.get('onPremisesSamAccountName')), 'Department': onprem_acct.get('department'), 'JobTitle': onprem_acct.get('jobTitle'), 'ManagerUPN': onprem_acct.get('manager'), 'Notes': 'On-Prem - No Entra Sync'}
+        )
         
     return data.list_to_html_table(account_list, 20, 20, escape_html=False)
 
@@ -402,9 +462,13 @@ def get_ip_comment():
     
     ip_list = []
     for ip in base_object.IPs:
-        geo = ip.get('GeoData')
-        ip_list.append({'IP': ip.get('Address'), 'City': geo.get('city'), 'State': geo.get('state'), 'Country': geo.get('country'), \
-                        'Organization': geo.get('organization'), 'OrganizationType': geo.get('organizationType'), 'ASN': geo.get('asn') })
+        if ip.get('IPType') != 3:
+            #Excludes link local addresses from the IP comment
+            geo = ip.get('GeoData')
+            ip_list.append({'IP': ip.get('Address'), 'City': geo.get('city'), 'State': geo.get('state'), 'Country': geo.get('country'), \
+                        'Organization': geo.get('organization'), 'OrganizationType': geo.get('organizationType'), 'ASN': geo.get('asn'), 'IPType': ip.get('IPType')})
+            
+    ip_list = data.sort_list_by_key(ip_list, 'IPType', ascending=True, drop_columns=['IPType'])
         
     return data.list_to_html_table(ip_list)
 
diff --git a/modules/exposure_device.py b/modules/exposure_device.py
new file mode 100644
index 0000000..f221793
--- /dev/null
+++ b/modules/exposure_device.py
@@ -0,0 +1,111 @@
+from classes import BaseModule, Response, DeviceExposureModule
+from shared import rest, data
+import json
+
+def execute_device_exposure_module (req_body):
+
+    #Inputs AddIncidentComments, AddIncidentTask, BaseModuleBody, IncidentTaskInstructions, AddIncidentTags
+
+    base_object = BaseModule()
+    base_object.load_from_input(req_body['BaseModuleBody'])
+    lookback = req_body.get('LookbackInDays', 14)
+    exp_object = DeviceExposureModule()
+
+    mde_ids = base_object.get_host_mdeid_list()
+
+    if mde_ids:
+        exp_object.AnalyzedEntities = len(mde_ids)
+        query = data.load_text_from_file('exposure-device.kql', mde_id_list=mde_ids)
+        response = rest.execute_m365d_query(base_object, query)
+
+        exp_object.Nodes = list(filter(lambda x: x['NodeType'] == 'Node', response))
+        exp_object.Paths = list(filter(lambda x: x['NodeType'] == 'Path', response))
+
+    if req_body.get('AddIncidentComments', True) and base_object.IncidentAvailable:
+        add_comment_to_incident(base_object, exp_object)
+
+    if req_body.get('AddIncidentTags', True) and base_object.IncidentAvailable:
+        add_tags_to_incident(base_object, exp_object)
+
+    if req_body.get('AddIncidentTask', False) and len(exp_object.Nodes) > 0 and base_object.IncidentAvailable:
+        rest.add_incident_task(base_object, 'Review Device Exposure Information', data.coalesce(req_body.get('IncidentTaskInstructions'), 'Review the output of the Device Exposure Module in the incident comments.'))
+
+    return Response(exp_object)
+
+
+def add_tags_to_incident (base_object, exp_object):
+    tags = []
+    for x in exp_object.Nodes:
+        if x['ComputerCriticalityRules']:
+            tags += x['ComputerCriticalityRules']
+    rest.add_incident_tags(base_object, tags)
+
+def add_comment_to_incident (base_object, exp_object):
+    crit_level = {
+        0: 'Very High<br />🟥🟥🟥🟥',
+        1: 'High<br />🟥🟥🟥⬜ ',
+        2: 'Medium<br />🟥🟥⬜⬜',
+        3: 'Low<br />🟥⬜⬜⬜'
+    }
+
+    out = []
+
+    for x in exp_object.Paths:
+
+        sid = None
+        entra_sid = None
+        aadid = None
+
+        for acct_id in x.get('UserEntityIds', []):
+            if acct_id['type'] == 'SecurityIdentifier':
+                if acct_id.get('id').startswith('S-1-12-'):
+                    entra_sid = acct_id['id']
+                else:
+                    sid = acct_id['id']
+            elif acct_id['type'] == 'AadObjectId':
+                aadid = data.parse_kv_string(acct_id['id']).get('objectid')
+
+        if aadid:
+            user_on_device = f"<a href=\"https://security.microsoft.com/user?aad={aadid}&tab=overview\" target=\"_blank\">{x['UsersOnDevice']}</a>"
+        elif sid:
+            user_on_device = f"<a href=\"https://security.microsoft.com/user?sid={sid}&tab=overview\" target=\"_blank\">{x['UsersOnDevice']}</a>"
+        else:
+            user_on_device = x['UsersOnDevice']
+
+        if x['UserNodeLabel'] == 'managedidentity':
+            user_on_device += f"<br />(Managed Identity)"
+
+        out.append({
+            'Computer': get_device_link(x),
+            'Computer Details': f"<b>Criticality:</b><br /> {crit_level[x['ComputerCrit']]}<br /><p><b>Asset Rules:</b> {data.list_to_string(x['ComputerCriticalityRules'])}<br /><b>Computer Tags:</b> {data.list_to_string(x['ComputerTags'])}<p><b>Risk Level:</b> {x['ComputerRiskScore']}<br /><b>Exposure Level:</b> {x['ComputerExposureScore']}<br /><b>Max CVSS Score:</b> {x['ComputerMaxCVSSScore']}<br /><b>Onboarding:</b> {x['ComputerOnboarding']}<br /><b>Sensor:</b> {x['ComputerSensorHealth']}",
+            'UsersOnDevice': user_on_device,
+            'UserCriticality': f"{crit_level[x['UserCrit']]}<br /><p><b>Asset Rules:</b> {data.list_to_string(x['UserCriticalityRules'])}<br /><b>User Tags:</b> {data.list_to_string(x['UserTags'])}"
+        })
+
+    for x in exp_object.nodes_without_paths():
+        out.append({
+            'Computer': get_device_link(x),
+            'Computer Details': f"<b>Criticality:</b><br /> {crit_level[x['ComputerCrit']]}<br /><p><b>Asset Rules:</b> {data.list_to_string(x['ComputerCriticalityRules'])}<br /><b>Computer Tags:</b> {data.list_to_string(x['ComputerTags'])}<p><b>Risk Level:</b> {x['ComputerRiskScore']}<br /><b>Exposure Level:</b> {x['ComputerExposureScore']}<br /><b>Max CVSS Score:</b> {x['ComputerMaxCVSSScore']}<br /><b>Onboarding:</b> {x['ComputerOnboarding']}<br /><b>Sensor:</b> {x['ComputerSensorHealth']}",
+            'UsersOnDevice': "None Detected"
+        })
+
+    comment = f'<h3>Device Exposure Module</h3>'
+    if out:
+        html_table = data.list_to_html_table(out, index=False, max_cols=20, escape_html=False)
+        comment += html_table
+    else:
+        comment += f'No Device Exposure Results Detected'
+    
+    comment_result = rest.add_incident_comment(base_object, comment)
+
+def get_device_link(x):
+    device_id = None
+    for dev_id in x.get('ComputerEntityIds', []):
+        if dev_id['type'] == 'DeviceInventoryId':
+            device_id = dev_id['id']
+            break
+
+    if device_id:
+        return f"<a href=\"https://security.microsoft.com/machines/v2/{device_id}/overview\" target=\"_blank\">{x['Computer']}</a><br />(<a href=\"https://security.microsoft.com/security-graph?id={device_id}&assetType=DeviceInventoryId\">Exposure Map</a>)"
+    else:
+        return x['Computer']
\ No newline at end of file
diff --git a/modules/exposure_user.py b/modules/exposure_user.py
new file mode 100644
index 0000000..91c82c1
--- /dev/null
+++ b/modules/exposure_user.py
@@ -0,0 +1,95 @@
+from classes import BaseModule, Response, UserExposureModule
+from shared import rest, data
+import json
+
+def execute_user_exposure_module (req_body):
+
+    #Inputs AddIncidentComments, AddIncidentTask, BaseModuleBody, IncidentTaskInstructions, AddIncidentTags
+
+    base_object = BaseModule()
+    base_object.load_from_input(req_body['BaseModuleBody'])
+    lookback = req_body.get('LookbackInDays', 14)
+    exp_object = UserExposureModule()
+
+    user_ids = base_object.get_account_id_and_sid_list()
+
+    if user_ids:
+        exp_object.AnalyzedEntities = len(user_ids)
+        query = data.load_text_from_file('exposure-user.kql', user_id_list=user_ids)
+        response = rest.execute_m365d_query(base_object, query)
+
+        exp_object.Nodes = list(filter(lambda x: x['NodeType'] == 'Node', response))
+        exp_object.Paths = list(filter(lambda x: x['NodeType'] == 'Path', response))
+
+    if req_body.get('AddIncidentComments', True) and base_object.IncidentAvailable:
+        add_comment_to_incident(base_object, exp_object)
+
+    if req_body.get('AddIncidentTags', True) and base_object.IncidentAvailable:
+        add_tags_to_incident(base_object, exp_object)
+
+    if req_body.get('AddIncidentTask', False) and len(exp_object.Nodes) > 0 and base_object.IncidentAvailable:
+        rest.add_incident_task(base_object, 'Review User Exposure Information', data.coalesce(req_body.get('IncidentTaskInstructions'), 'Review the output of the User Exposure Module in the incident comments.'))
+
+    return Response(exp_object)
+
+def add_comment_to_incident (base_object, exp_object):
+    crit_level = {
+        0: 'Very High<br />🟥🟥🟥🟥',
+        1: 'High<br />🟥🟥🟥⬜ ',
+        2: 'Medium<br />🟥🟥⬜⬜',
+        3: 'Low<br />🟥⬜⬜⬜'
+    }
+
+    out = []
+
+    for x in exp_object.Paths:
+        out.append({
+            'User': get_user_link(x),
+            'User Details': f"{crit_level[x['UserCriticality']]}<br /><p><b>Asset Rules:</b> {data.list_to_string(x['UserCriticalityRules'])}<br /><p><b>User Tags:</b> {data.list_to_string(x['UserTags'])}",
+            'ElevatedRightsOn (Top 5)': f"{data.list_to_string(x['ElevatedRightsOn'])}<p><b>Elevated Rights to Computers:</b> {x['ElevatedRightsOnCount']}<br /><b>Local Admin to Computers:</b> {x['LocalAdminCount']}",
+            "HighestComputerCriticality": f"{crit_level[x['HighestComputerCriticality']]}<br /><p><b>Combined Asset Rules:</b> {data.list_to_string(x['ComputerCriticalityRules'])}"
+        })
+
+    for x in exp_object.nodes_without_paths():
+        out.append({
+            'User': get_user_link(x),
+            'User Details': f"{crit_level[x['UserCriticality']]}<br /><p><b>Asset Rules:</b> {data.list_to_string(x['UserCriticalityRules'])}<br /><p><b>User Tags:</b> {data.list_to_string(x['UserTags'])}",
+            'ElevatedRightsOn (Top 5)': "None Detected"
+        })
+
+    comment = f'<h3>User Exposure Module</h3>'
+    if out:
+        html_table = data.list_to_html_table(out, index=False, max_cols=20, escape_html=False)
+        comment += html_table
+    else:
+        comment += f'No User Exposure Results Detected'
+    
+    comment_result = rest.add_incident_comment(base_object, comment)
+
+def add_tags_to_incident (base_object, exp_object):
+    tags = []
+    for x in exp_object.Nodes:
+        if x['UserCriticalityRules']:
+            tags += x['UserCriticalityRules']
+    rest.add_incident_tags(base_object, tags)
+
+def get_user_link(x):
+    sid = None
+    entra_sid = None
+    aadid = None
+
+    for acct_id in x.get('UserEntityIds', []):
+        if acct_id['type'] == 'SecurityIdentifier':
+            if acct_id.get('id').startswith('S-1-12-'):
+                entra_sid = acct_id['id']
+            else:
+                sid = acct_id['id']
+        elif acct_id['type'] == 'AadObjectId':
+            aadid = data.parse_kv_string(acct_id['id']).get('objectid')
+
+    if aadid:
+        return f"<a href=\"https://security.microsoft.com/user?aad={aadid}&tab=overview\" target=\"_blank\">{x['User']}</a><br />(<a href=\"https://security.microsoft.com/security-graph?id={aadid}&assetType=AadObjectId\">Exposure Map</a>)"
+    elif sid:
+        return f"<a href=\"https://security.microsoft.com/user?sid={sid}&tab=overview\" target=\"_blank\">{x['User']}</a><br />(<a href=\"https://security.microsoft.com/security-graph?id={sid}&assetType=SecurityIdentifier\">Exposure Map</a>)"
+    else:
+        return x['User']
diff --git a/modules/file.py b/modules/file.py
index 940bfae..18357a9 100644
--- a/modules/file.py
+++ b/modules/file.py
@@ -124,8 +124,21 @@ def execute_file_module (req_body):
 
 
     if req_body.get('AddIncidentComments', True) and base_object.IncidentAvailable:
+
+        files = []
+        for file in file_object.DetailedResults:
+            if file.get("SHA256"):
+                file_name = f'<a href="https://security.microsoft.com/file/{file.get("SHA256")}/overview">{file.get("FileName","Unknown")}</a>'
+            else:
+                file_name = file.get("FileName","Unknown")
+            files.append({
+                'File': f'<b>Name:</b> {file_name}<br><b>File Size:</b> {file.get("FileSize")} bytes<br><b>ThreatName:</b> {file.get("ThreatName")}',
+                'Signing': f'<b>Signer:</b> {file.get("Signer")}<br><b>Is Cert Valid:</b> {file.get("IsCertificateValid")}',
+                'Observations': f'<b>Global First Seen:</b> {file.get("GlobalFirstSeen")}<br><b>Global Last Seen:</b> {file.get("GlobalLastSeen")}<br><b>Global Prevalence:</b> {file.get("GlobalPrevalence")}<br><b>Email Attachment Count:</b> {file.get("EmailAttachmentCount", 0)}<br><b>Email Attachment First Seen:</b> {file.get("EmailAttachmentFirstSeen", "N/A")}<br><b>Email Attachment Last Seen:</b> {file.get("EmailAttachmentLastSeen", "N/A")}<br><b>DeviceUniqueDeviceCount:</b> {file.get("DeviceUniqueDeviceCount", "N/A")}<br><b>DeviceUniqueFileNameCount:</b> {file.get("DeviceUniqueFileNameCount", "N/A")}<br><b>DeviceFileActionCount:</b> {file.get("DeviceFileActionCount", "N/A")}',
+                'Hashes': f'<b>SHA1:</b> {file.get("SHA1", "Unknown")}<br><b>SHA256:</b> {file.get("SHA256", "Unknown")}<br>'
+            })
         
-        html_table = data.list_to_html_table(file_object.DetailedResults)
+        html_table = data.list_to_html_table(files, escape_html=False)
 
         comment = f'''<h3>File Module</h3>A total of {file_object.AnalyzedEntities} entities were analyzed with data from the last 30 days.<br>{html_table}'''
         comment_result = rest.add_incident_comment(base_object, comment)
diff --git a/modules/files/exchange-audit.kql b/modules/files/exchange-audit.kql
index 558bf80..2f0f595 100644
--- a/modules/files/exchange-audit.kql
+++ b/modules/files/exchange-audit.kql
@@ -1,6 +1,6 @@
 {account_table}
-let UPNs = accountEntities | project UserPrincipalName;
-let IDs = accountEntities | project AADUserId;
+let UPNs = accountEntities | where isnotempty(UserPrincipalName) | project UserPrincipalName;
+let IDs = accountEntities | where isnotempty(AADUserId) | project AADUserId;
 union (OfficeActivity
 | where Operation == "Set-Mailbox"
 | where UserId in~ (UPNs)
diff --git a/modules/files/exposure-device.kql b/modules/files/exposure-device.kql
new file mode 100644
index 0000000..77e4955
--- /dev/null
+++ b/modules/files/exposure-device.kql
@@ -0,0 +1,58 @@
+let mde_ids = dynamic({mde_id_list});
+let selected_nodes = materialize (
+ExposureGraphNodes
+| where NodeLabel in~ ('user','device','entra-userCookie','microsoft.compute/virtualmachines','managedidentity'));
+let selected_edges = 
+ExposureGraphEdges
+| where EdgeLabel in~ ('has credentials of','contains', 'can authenticate as');
+union
+(selected_edges
+| make-graph SourceNodeId --> TargetNodeId with selected_nodes on NodeId
+| graph-match (device)-[hasCreds*1..2]->(user)
+ where device.EntityIds has_any (mde_ids)
+ project 
+ Computer=tostring(device.NodeName),
+ ComputerNodeId=device.NodeId,
+ ComputerCrit = toint(coalesce(device.NodeProperties.rawData.criticalityLevel.criticalityLevel, dynamic(3))),
+ ComputerCriticalityRules = device.NodeProperties.rawData.criticalityLevel.ruleNames,
+ ComputerMaxCVSSScore=device.NodeProperties.rawData.highRiskVulnerabilityInsights.maxCvssScore,
+ ComputerVulnerabilities=device.NodeProperties.rawData.highRiskVulnerabilityInsights,
+ ComputerOnboarding=device.NodeProperties.rawData.onboardingStatus,
+ ComputerSensorHealth=device.NodeProperties.rawData.sensorHealthState,
+ ComputerRiskScore=device.NodeProperties.rawData.riskScore,
+ ComputerExposureScore=device.NodeProperties.rawData.exposureScore,
+ ComputerManualTags=device.NodeProperties.rawData.deviceManualTags,
+ ComputerDynamicTags=device.NodeProperties.rawData.deviceDynamicTags,
+ ComputerEntityIds=device.EntityIds,
+ EdgeLabel=map(hasCreds, EdgeLabel),
+ UsersOnDevice=coalesce(user.NodeProperties.rawData.accountUpn,user.NodeProperties.rawData.accountName,user.NodeProperties.rawData.distinguishedName,user.NodeProperties.rawData.accountDisplayName,user.NodeProperties.rawData.accountObjectId),
+ UserCrit=toint(coalesce(user.NodeProperties.rawData.criticalityLevel.criticalityLevel, dynamic(3))),
+ UserCriticalityRules=user.NodeProperties.rawData.criticalityLevel.ruleNames,
+ UserTags=user.NodeProperties.rawData.tags,
+ UserNodeLabel = user.NodeLabel,
+ UserEntityIds=user.EntityIds,
+ device, user
+| extend ComputerTags = array_concat(ComputerManualTags, ComputerDynamicTags)
+| where array_length(set_difference(EdgeLabel, dynamic(['contains']))) > 0
+| project Computer, ComputerNodeId, ComputerCrit, ComputerCriticalityRules, ComputerRiskScore, ComputerExposureScore, ComputerMaxCVSSScore, ComputerOnboarding, ComputerSensorHealth, ComputerTags, ComputerEntityIds
+    , UsersOnDevice, UserCrit, UserCriticalityRules, UserTags, UserNodeLabel, UserEntityIds, NodeType="Path"
+),
+(selected_nodes
+| where EntityIds has_any (mde_ids)
+| project 
+Computer=tostring(NodeName),
+ ComputerCrit = toint(coalesce(NodeProperties.rawData.criticalityLevel.criticalityLevel, dynamic(3))),
+ ComputerCriticalityRules = NodeProperties.rawData.criticalityLevel.ruleNames,
+ ComputerMaxCVSSScore=NodeProperties.rawData.highRiskVulnerabilityInsights.maxCvssScore,
+ ComputerVulnerabilities=NodeProperties.rawData.highRiskVulnerabilityInsights,
+ ComputerOnboarding=NodeProperties.rawData.onboardingStatus,
+ ComputerSensorHealth=NodeProperties.rawData.sensorHealthState,
+ ComputerRiskScore=NodeProperties.rawData.riskScore,
+ ComputerExposureScore=NodeProperties.rawData.exposureScore,
+ ComputerManualTags=NodeProperties.rawData.deviceManualTags,
+ ComputerDynamicTags=NodeProperties.rawData.deviceDynamicTags,
+ ComputerEntityIds=EntityIds,
+ NodeType="Node",
+ ComputerNodeId=NodeId
+)
+| sort by ComputerCrit asc, UserCrit asc, Computer asc
\ No newline at end of file
diff --git a/modules/files/exposure-user.kql b/modules/files/exposure-user.kql
new file mode 100644
index 0000000..bc11a84
--- /dev/null
+++ b/modules/files/exposure-user.kql
@@ -0,0 +1,41 @@
+let user_ids = dynamic({user_id_list});
+let selected_nodes = materialize (
+ExposureGraphNodes
+| where NodeLabel in~ ('user','group','device','microsoft.compute/virtualmachines'));
+let selected_edges =
+ExposureGraphEdges
+| where EdgeLabel in~ ('can authenticate to', 'member of');
+union
+(selected_edges
+| make-graph SourceNodeId --> TargetNodeId with selected_nodes on NodeId
+| graph-match (user)-[edge*1..5]->(device)
+ where user.EntityIds has_any (user_ids) 
+ project User=tostring(coalesce(user.NodeProperties.rawData.accountUpn,user.NodeProperties.rawData.accountName,user.NodeProperties.rawData.distinguishedName,user.NodeProperties.rawData.accountDisplayName,user.NodeProperties.rawData.accountObjectId))
+ ,UserCriticality=toint(coalesce(user.NodeProperties.rawData.criticalityLevel.criticalityLevel,dynamic(3)))
+ ,UserCriticalityRules=user.NodeProperties.rawData.criticalityLevel.ruleNames
+ ,UserTags=user.NodeProperties.rawData.tags
+ ,UserEntityIds=user.EntityIds
+ ,UserNodeId=user.NodeId
+ ,Computer=device.NodeName
+ ,ComputerCriticality=toint(coalesce(device.NodeProperties.rawData.criticalityLevel.criticalityLevel,dynamic(3)))
+ ,ComputerCriticalityRules=device.NodeProperties.rawData.criticalityLevel.ruleNames
+ ,EdgeLabel=map(edge, EdgeLabel)
+ ,EdgeProperties=map(edge, EdgeProperties)
+ ,Path=map(edge, TargetNodeName), user, device
+| where EdgeLabel has 'can authenticate to'
+| extend RightsOnDevice = EdgeProperties[-1].rawData.userRightsOnDevice
+| extend isLocalAdmin = tobool(RightsOnDevice.isLocalAdmin)
+| sort by UserCriticality asc, ComputerCriticality asc
+| summarize ElevatedRightsOn=make_set(Computer, 5), ElevatedRightsOnCount=count_distinct(Computer), UserCriticality=min(UserCriticality), UserCriticalityRules=make_set(UserCriticalityRules), UserTags=make_set(UserTags), HighestComputerCriticality=min(ComputerCriticality), LocalAdminCount=dcountif(Computer, isLocalAdmin), ComputerCriticalityRules=make_set(ComputerCriticalityRules), UserEntityIds=make_set(UserEntityIds) by User, UserNodeId
+| extend NodeType="Path"
+),
+(selected_nodes
+| where EntityIds has_any (user_ids) 
+| project User=tostring(coalesce(NodeProperties.rawData.accountUpn,NodeProperties.rawData.accountName,NodeProperties.rawData.distinguishedName,NodeProperties.rawData.accountDisplayName,NodeProperties.rawData.accountObjectId))
+ ,UserCriticality=toint(coalesce(NodeProperties.rawData.criticalityLevel.criticalityLevel,dynamic(3)))
+ ,UserCriticalityRules=NodeProperties.rawData.criticalityLevel.ruleNames
+ ,UserTags=NodeProperties.rawData.tags
+ ,UserEntityIds=EntityIds
+ ,UserNodeId=NodeId
+ ,NodeType="Node"
+)
\ No newline at end of file
diff --git a/modules/kql.py b/modules/kql.py
index 6430bf5..2a67995 100644
--- a/modules/kql.py
+++ b/modules/kql.py
@@ -13,7 +13,7 @@ def execute_kql_module (req_body):
 
     arm_id = f'let incidentArmId = "{base_object.IncidentARMId}";\n'
     ip_entities = base_object.get_ip_kql_table()
-    account_entities = base_object.get_account_kql_table()
+    account_entities = base_object.get_account_kql_table(include_unsynced=True)
     host_entities = base_object.get_host_kql_table()
 
     query = arm_id + ip_entities + account_entities + host_entities + req_body['KQLQuery']
diff --git a/modules/mdca.py b/modules/mdca.py
index ba08464..79f42a1 100644
--- a/modules/mdca.py
+++ b/modules/mdca.py
@@ -12,7 +12,7 @@ def execute_mdca_module (req_body):
     mdac_object = MDCAModule()
 
     if req_body.get('AddIncidentComments', True):
-        comment = "The Sentinel Triage AssistanT (STAT) Microsoft Defender for Cloud Apps module has been deprecated.  This is due to Microsoft's depreation of the MDCA investigation score.<br />"
+        comment = "The Sentinel Triage AssistanT's (STAT) Microsoft Defender for Cloud Apps module has been deprecated.  This is due to Microsoft's deprecation of the MDCA investigation score.<br />"
         comment += "Please remove the MDCA module from your STAT Analysis."
         comment_result = rest.add_incident_comment(base_object, comment)
 
diff --git a/modules/relatedalerts.py b/modules/relatedalerts.py
index bb1e235..9063cdf 100644
--- a/modules/relatedalerts.py
+++ b/modules/relatedalerts.py
@@ -37,7 +37,7 @@ def execute_relatedalerts_module (req_body):
 let currentIncidentAlerts = dynamic({str(base_object.get_alert_ids())});
 let isFusionIncident = {related_alerts.FusionIncident};
 let severityOrder = datatable (AlertSeverity:string, Order:int)['Informational', 1, 'Low', 2, 'Medium', 3, 'High', 4];
-{base_object.get_account_kql_table()}let accounts = toscalar(accountEntities
+{base_object.get_account_kql_table(include_unsynced=True)}let accounts = toscalar(accountEntities
 | where {check_accounts}
 | extend UPNName = split(UserPrincipalName,'@')[0]
 | extend EntityData = pack_array(UserPrincipalName, SamAccountName, ObjectSID, AADUserId, UPNName)
diff --git a/modules/ti.py b/modules/ti.py
index 9857b93..08d41f0 100644
--- a/modules/ti.py
+++ b/modules/ti.py
@@ -21,13 +21,12 @@ def execute_ti_module (req_body):
     if check_domains and base_object.Domains:
         query = base_object.get_domain_kql_table() + '''domainEntities
 | extend DomainName = tolower(Domain)
-| join kind=inner (ThreatIntelligenceIndicator
-| extend DomainName = coalesce(DomainName, EmailSourceDomain)
-| where isnotempty(DomainName)
-| summarize arg_max(TimeGenerated, *) by IndicatorId
-| where Active
-| extend DomainName = tolower(DomainName)) on DomainName
-| project TIType="Domain", TIData=Domain, SourceSystem, Description, ThreatType, ConfidenceScore, IndicatorId'''
+| join kind=inner (ThreatIntelIndicators
+| where ObservableKey =~ "domain-name:value"
+| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
+| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
+| extend DomainName = tolower(ObservableValue)) on DomainName
+| project TIType="Domain", TIData=DomainName, SourceSystem, Description=tostring(Data.description), ThreatType=tostring(array_strcat(Data.indicator_types, ', ')), ConfidenceScore=Confidence, IndicatorId=Id'''
         results = rest.execute_la_query(base_object, query, 14)
         ti_object.DetailedResults = ti_object.DetailedResults + results
         ti_object.DomainEntitiesCount = len(base_object.get_domain_list())
@@ -37,12 +36,12 @@ def execute_ti_module (req_body):
     if check_filehashes and base_object.FileHashes:
         query = base_object.get_filehash_kql_table() + '''hashEntities
 | extend FileHash = tolower(FileHash)
-| join kind=inner (ThreatIntelligenceIndicator
-| where isnotempty(FileHashValue)
-| summarize arg_max(TimeGenerated, *) by IndicatorId
-| where Active
-| extend FileHash = tolower(FileHashValue)) on FileHash
-| project TIType="FileHash", TIData=FileHash, SourceSystem, Description, ThreatType, ConfidenceScore, IndicatorId'''
+| join kind=inner (ThreatIntelIndicators
+| where ObservableKey in~ ("file:hashes.'SHA-1'","file:hashes.'SHA-256'","file:hashes.MD5","file:hashes.'MD5'")
+| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
+| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
+| extend FileHash = tolower(ObservableValue)) on FileHash
+| project TIType="FileHash", TIData=FileHash, SourceSystem, Description=tostring(Data.description), ThreatType=tostring(array_strcat(Data.indicator_types, ', ')), ConfidenceScore=Confidence, IndicatorId=Id'''
         results = rest.execute_la_query(base_object, query, 14)
         ti_object.DetailedResults = ti_object.DetailedResults + results
         ti_object.FileHashEntitiesCount = len(base_object.get_filehash_list())
@@ -51,12 +50,13 @@ def execute_ti_module (req_body):
 
     if check_ips and base_object.IPs:
         query = base_object.get_ip_kql_table() + '''ipEntities
-| join kind=inner (ThreatIntelligenceIndicator
-| extend tiIP = coalesce(NetworkIP, NetworkSourceIP, NetworkDestinationIP, EmailSourceIpAddress)
-| where isnotempty(tiIP)
-| summarize arg_max(TimeGenerated, *) by IndicatorId
-| where Active) on $left.IPAddress == $right.tiIP
-| project TIType="IP", TIData=IPAddress, SourceSystem, Description, ThreatType, ConfidenceScore, IndicatorId'''
+| join kind=inner (ThreatIntelIndicators
+| where ObservableKey in~ ("network-traffic:src_ref.value","network-traffic:dst_ref.value","ipv4-addr:value","ipv6-addr:value")
+| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
+| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
+| extend IPAddress = tolower(ObservableValue)
+) on IPAddress
+| project TIType="IP", TIData=IPAddress, SourceSystem, Description=tostring(Data.description), ThreatType=tostring(array_strcat(Data.indicator_types, ', ')), ConfidenceScore=Confidence, IndicatorId=Id'''
         results = rest.execute_la_query(base_object, query, 14)
         ti_object.DetailedResults = ti_object.DetailedResults + results
         ti_object.IPEntitiesCount = len(base_object.get_ip_list())
@@ -64,15 +64,26 @@ def execute_ti_module (req_body):
         ti_object.IPTIFound = bool(results)
 
     if check_urls and base_object.URLs:
-        query = base_object.get_url_kql_table() + '''urlEntities
+        query = base_object.get_url_kql_table() + '''let entities = urlEntities
 | extend Url = tolower(Url)
-| join kind=inner (ThreatIntelligenceIndicator
-| where isnotempty(Url)
-| summarize arg_max(TimeGenerated, *) by IndicatorId
-| where Active
-| extend Url = tolower(Url)) on Url
+| extend DomainName = parse_url(Url)
+| extend DomainName = coalesce(DomainName.Host, Url);
+union isfuzzy=true
+(entities
+| join kind=inner (ThreatIntelIndicators
+| where ObservableKey =~ "url:value"
+| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
+| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
+| extend Url = tolower(ObservableValue)) on Url
 | extend Url = strcat('[', tostring(split(Url, '//')[0]), ']//', tostring(split(Url, '//')[1]))
-| project TIType="URL", TIData=Url, SourceSystem, Description, ThreatType, ConfidenceScore, IndicatorId'''
+| project TIType="URL", TIData=Url, SourceSystem, Description=tostring(Data.description), ThreatType=tostring(array_strcat(Data.indicator_types, ', ')), ConfidenceScore=Confidence, IndicatorId=Id),
+(entities
+| join kind=inner (ThreatIntelIndicators
+| where ObservableKey =~ "domain-name:value"
+| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
+| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
+| extend DomainName = tolower(ObservableValue)) on DomainName
+| project TIType="Domain", TIData=DomainName, SourceSystem, Description=tostring(Data.description), ThreatType=tostring(array_strcat(Data.indicator_types, ', ')), ConfidenceScore=Confidence, IndicatorId=Id)'''
         results = rest.execute_la_query(base_object, query, 14)
         ti_object.DetailedResults = ti_object.DetailedResults + results
         ti_object.URLEntitiesCount = len(base_object.get_url_list())
diff --git a/modules/ueba.py b/modules/ueba.py
index 63cbf33..f338430 100644
--- a/modules/ueba.py
+++ b/modules/ueba.py
@@ -17,17 +17,20 @@ def execute_ueba_module (req_body):
 
     query = f'''let minPriority = {min_priority};
 let lookback = {lookback}d;
-{base_object.get_account_kql_table()}let accountIds = accountEntities
+{base_object.get_account_kql_table(include_unsynced=True)}let accountIds = accountEntities
 | summarize UserNames=make_set(SamAccountName), UPNs=make_set(UserPrincipalName)
 | extend ids = array_concat(UserNames, UPNs);
+let UPNs = accountIds | mv-expand UPNs | where isnotempty(UPNs) | summarize UPNs=make_set(UPNs);
+let UserNames = accountIds | mv-expand UserNames | where isnotempty(UserNames) | summarize UserNames=make_set(UserNames);
 let userDetails = BehaviorAnalytics
 | where TimeGenerated > ago(lookback)
-| join kind=inner (accountEntities) on UserPrincipalName
-| where InvestigationPriority >= minPriority or isnotnull(DevicesInsights.ThreatIntelIndicatorType) 
+| where UserPrincipalName has_any (UPNs) or UserName has_any (UserNames)
+| where InvestigationPriority >= minPriority or isnotnull(DevicesInsights.ThreatIntelIndicatorType)
+| extend UserPrincipalName = coalesce(UserPrincipalName, UserName)
 | summarize InvestigationPrioritySum=sum(InvestigationPriority), InvestigationPriorityAverage=avg(InvestigationPriority), InvestigationPriorityMax=max(InvestigationPriority), ThreatIntelMatches=countif(isnotnull(DevicesInsights.ThreatIntelIndicatorType)), EventCount=count() by UserPrincipalName;
 let userAnomalies = Anomalies
 | where TimeGenerated > ago(lookback)
-| where UserPrincipalName in~ (accountIds) or UserName in~ (accountIds)
+| where UserPrincipalName in~ (UPNs) or UserName in~ (UserNames)
 | mv-expand todynamic(Tactics)
 | summarize AnomalyTactics=make_set(Tactics), AnomalyCount=dcount(Id, 4)
 | extend UserPrincipalName="Total";
diff --git a/modules/version.json b/modules/version.json
index cdd5079..d20a644 100644
--- a/modules/version.json
+++ b/modules/version.json
@@ -1,3 +1,3 @@
 {
-    "FunctionVersion": "2.1.0"
-}
\ No newline at end of file
+    "FunctionVersion": "2.2.0"
+}
diff --git a/modules/watchlist.py b/modules/watchlist.py
index 8cb18c7..ebb619b 100644
--- a/modules/watchlist.py
+++ b/modules/watchlist.py
@@ -23,9 +23,10 @@ def execute_watchlist_module (req_body):
         raise STATError(f'The watchlist name {watchlist_object.WatchlistName} is invalid.', {})
     
     if watchlist_datatype == 'UPN':
-        account_entities = base_object.get_account_kql_table()
-        query = account_entities +  f'''accountEntities
-| project UserPrincipalName
+        account_entities = base_object.get_account_upn_list(include_unsynced=True)
+        query = f'''let accountUpns = dynamic({account_entities});
+print UserPrincipalName = accountUpns
+| mv-expand UserPrincipalName
 | extend UserPrincipalName = tolower(UserPrincipalName)
 | join kind=leftouter (_GetWatchlist("{watchlist_object.WatchlistName}")
 | extend {watchlist_key} = tolower({watchlist_key})) on $left.UserPrincipalName == $right.{watchlist_key}
diff --git a/requirements.txt b/requirements.txt
index 7ed342c..0c034fc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,4 +6,7 @@ azure-functions
 azure-identity
 azure-keyvault-secrets
 requests
-pandas
\ No newline at end of file
+pandas
+cryptography==43.0.3
+
+#Limiting cryptograpy due to https://github.com/Azure/azure-functions-python-worker/issues/1651
\ No newline at end of file
diff --git a/shared/coordinator.py b/shared/coordinator.py
index 47dfc6e..aced643 100644
--- a/shared/coordinator.py
+++ b/shared/coordinator.py
@@ -1,4 +1,4 @@
-from modules import base, kql, watchlist, ti, relatedalerts, scoring, ueba, playbook, exchange, aadrisks, file, createincident, mdca, mde
+from modules import base, kql, watchlist, ti, relatedalerts, scoring, ueba, playbook, exchange, aadrisks, file, createincident, mdca, mde, exposure_device, exposure_user
 from classes import STATError
 
 def initiate_module(module_name, req_body):
@@ -25,6 +25,10 @@ def initiate_module(module_name, req_body):
             return_data = file.execute_file_module(req_body)
         case 'aadrisks':
             return_data = aadrisks.execute_aadrisks_module(req_body)
+        case 'deviceexposure':
+            return_data = exposure_device.execute_device_exposure_module(req_body)
+        case 'userexposure':
+            return_data = exposure_user.execute_user_exposure_module(req_body)
         case 'ueba':
             return_data = ueba.execute_ueba_module(req_body)
         case 'oofmodule' | 'exchangemodule':
diff --git a/shared/data.py b/shared/data.py
index da2591e..7b0785a 100644
--- a/shared/data.py
+++ b/shared/data.py
@@ -117,9 +117,11 @@ def min_column_by_key(input_list, key):
         val = int(0)
     return val
 
-def sort_list_by_key(input_list, key, ascending=False):
+def sort_list_by_key(input_list, key, ascending=False, drop_columns:list=[]):
     df = pd.DataFrame(input_list)
     df = df.sort_values(by=[key], ascending=ascending)
+    if drop_columns:
+        df.drop(columns=drop_columns, inplace=True)
     return df.to_dict('records')
 
 def coalesce(*args):
@@ -177,3 +179,34 @@ def load_json_from_file(file_name:str):
 def load_text_from_file(file_name:str, **kwargs):
     with open(pathlib.Path(__file__).parent.parent / f'modules/files/{file_name}') as f:
         return f.read().format(**kwargs)
+
+def list_to_string(list_in:list, delimiter:str=', ', empty_str:str='N/A'):
+    if not list_in:
+        return empty_str
+        
+    if isinstance(list_in, list):
+        try:
+            return delimiter.join(list_in)
+        except:
+            return list_in
+    else:
+        return list_in
+    
+def parse_kv_string(kv:str, item_delimitter:str=';', value_delimitter:str='='):
+    """
+    Parse a string of key-value pairs into a dictionary.
+    :param kv: The string to parse.
+    :param item_delimitter: The delimiter between items. Default is ';'.
+    :param value_delimitter: The delimiter between key and value. Default is '='.
+    :return: A dictionary of key-value pairs.
+    """
+
+    kv_pairs = kv.split(item_delimitter)
+    result = {}
+    for pair in kv_pairs:
+        if value_delimitter in pair:
+            key, value = pair.split(value_delimitter, 1)
+            result[key.strip()] = value.strip()
+        else:
+            result[pair.strip()] = None
+    return result
\ No newline at end of file
diff --git a/shared/rest.py b/shared/rest.py
index 6c0ec5d..ec8818e 100644
--- a/shared/rest.py
+++ b/shared/rest.py
@@ -17,7 +17,6 @@
 m365_endpoint = os.getenv('M365_ENDPOINT')
 mde_endpoint = os.getenv('MDE_ENDPOINT')
 default_tenant_id = os.getenv('AZURE_TENANT_ID')
-mdca_endpoint = os.getenv('MDCA_ENDPOINT')
 kv_endpoint = os.getenv('KEYVAULT_ENDPOINT')
 kv_secret_name = os.getenv('KEYVAULT_SECRET_NAME')
 kv_client_id = os.getenv('KEYVAULT_CLIENT_ID')
@@ -49,10 +48,6 @@ def token_cache(base_module:BaseModule, api:str):
             tenant = base_module.MultiTenantConfig.get('MDETenantId', base_module.MultiTenantConfig.get('TenantId', default_tenant))
             token_expiration_check(api, stat_token.get(tenant,{}).get('mdetoken'), tenant)
             return stat_token[tenant]['mdetoken']
-        case 'mdca':
-            tenant = base_module.MultiTenantConfig.get('M365DTenantId', base_module.MultiTenantConfig.get('TenantId', default_tenant))
-            token_expiration_check(api, stat_token.get(tenant,{}).get('mdcatoken'), tenant)
-            return stat_token[tenant]['mdcatoken']
 
 def token_expiration_check(api:str, token, tenant:str):
     
@@ -90,8 +85,6 @@ def acquire_token(api:str, tenant:str):
             stat_token[tenant]['m365token'] = cred.get_token(get_endpoint('m365') + "/.default", tenant_id=tenant)
         case 'mde':
             stat_token[tenant]['mdetoken'] = cred.get_token(get_endpoint('mde') + "/.default", tenant_id=tenant)
-        case 'mdca':
-            stat_token[tenant]['mdcatoken'] = cred.get_token("05a65629-4c1b-48c1-a78b-804c4abdd4af/.default", tenant_id=tenant)
 
 def get_kv_secret():
     cred = DefaultAzureCredential()
@@ -141,13 +134,17 @@ def execute_rest_call(base_module:BaseModule, method:str, api:str, path:str, bod
                     raise STATError(error=f'Invalid rest method: {method}.', status_code=400)
             check_rest_response(response, api, path)
         except STATTooManyRequests as e:
-            wait_time += int(e.retry_after)
-            if wait_time >= 40:
-                raise STATTooManyRequests(error=e.error, source_error=e.source_error, status_code=429, retry_after=e.retry_after)
-            time.sleep(int(e.retry_after))
+            try:
+                retry_after = int(e.retry_after)
+            except ValueError:
+                retry_after = 10
+            wait_time += retry_after
+            if wait_time > 60:
+                raise STATTooManyRequests(error=e.error, source_error=e.source_error, status_code=e.status_code, retry_after=e.retry_after)
+            time.sleep(retry_after)
         except ConnectionError as e:
             wait_time += 20
-            if wait_time >= 40:
+            if wait_time >= 60:
                 raise STATError(error=f'Failed to establish a new connection to {url}', source_error=e, status_code=500)
             time.sleep(20)
         else:
@@ -159,8 +156,8 @@ def execute_rest_call(base_module:BaseModule, method:str, api:str, path:str, bod
 def check_rest_response(response:Response, api, path):
     if response.status_code == 404:
         raise STATNotFound(f'The API call to {api} with path {path} failed with status {response.status_code}', source_error={'status_code': int(response.status_code), 'reason': str(response.reason)})
-    elif response.status_code == 429:
-        raise STATTooManyRequests(f'The API call to {api} with path {path} failed with status {response.status_code}', source_error={'status_code': int(response.status_code), 'reason': str(response.reason)}, retry_after=response.headers.get('Retry-After'), status_code=429)
+    elif response.status_code == 429 or response.status_code == 408:
+        raise STATTooManyRequests(f'The API call to {api} with path {path} failed with status {response.status_code}', source_error={'status_code': int(response.status_code), 'reason': str(response.reason)}, retry_after=response.headers.get('Retry-After', 10), status_code=int(response.status_code))
     elif response.status_code >= 300:
         raise STATError(f'The API call to {api} with path {path} failed with status {response.status_code}', source_error={'status_code': int(response.status_code), 'reason': str(response.reason)})
     return
@@ -220,12 +217,10 @@ def get_endpoint(api:str):
                 return 'https://' + m365_endpoint
             case 'mde':
                 return 'https://' + mde_endpoint
-            case 'mdca':
-                return 'https://' + mdca_endpoint
     except TypeError:
         raise STATError(f'The STAT Function Application Setting was not configured for the {api} API. '
                         'Ensure that all API endpoint enrivonrment variables are correctly set in the STAT Function App '
-                        '(ARM_ENDPOINT, GRAPH_ENDPOINT, LOGANALYTICS_ENDPOINT, M365_ENDPOINT, MDE_ENDPOINT, and MDCA_ENDPOINT).')
+                        '(ARM_ENDPOINT, GRAPH_ENDPOINT, LOGANALYTICS_ENDPOINT, M365_ENDPOINT, and MDE_ENDPOINT).')
     
 def add_incident_comment(base_module:BaseModule, comment:str):
     path = base_module.IncidentARMId + '/comments/' + str(uuid.uuid4()) + '?api-version=2023-02-01'
@@ -250,6 +245,39 @@ def add_incident_task(base_module:BaseModule, title:str, description:str, status
             response = 'Task addition failed'
     return response
 
+def add_incident_tags(base_module:BaseModule, tags:list):
+    if tags:
+        path = base_module.IncidentARMId + '?api-version=2025-03-01'
+        tags_to_add = False
+        try:
+            response = json.loads(rest_call_get(base_module, 'arm', path).content)
+
+            all_tags = response.get('properties', {}).get('labels', [])
+            for tag in tags:
+                if any(existing_tag.get('labelName') == tag for existing_tag in all_tags):
+                    continue
+                else:
+                    all_tags.append({'labelName': tag, 'labelType': 'User'})
+                    tags_to_add = True
+
+            body = {
+                'etag': response['etag'],
+                'properties': {
+                    'severity': response['properties']['severity'],
+                    'status': response['properties']['status'],
+                    'title': response['properties']['title'],
+                    'labels': all_tags,
+                }
+            }
+
+            if tags_to_add:
+                response_put = rest_call_put(base_module, 'arm', path, body=body)
+            else:
+                response_put = 'No new tags to add'
+        except:
+            response_put = 'Tag addition failed'
+        return response_put
+
 def check_app_role(base_module:BaseModule, token_type:str, app_roles:list):
     token = token_cache(base_module, token_type)
 


Test patch:
diff --git a/tests/test_data.py b/tests/test_data.py
index 3140edb..b2e2c9d 100644
--- a/tests/test_data.py
+++ b/tests/test_data.py
@@ -100,6 +100,27 @@ def test_html_remove_empty_col():
     expected_output = '<tableborder="1"class="dataframe"><thead><trstyle="text-align:left;"><th>All</th><th>NotAllEmpty</th></tr></thead><tbody><tr><td>a</td><td>a</td></tr><tr><td>b</td><td></td></tr></tbody></table>'
     assert table == expected_output
 
+def test_list_to_string():
+    list_data = ['a', 'b', 'c']
+    string_data = data.list_to_string(list_data)
+    assert string_data == 'a, b, c'
+
+def test_list_to_string_empty():
+    list_data = []
+    string_data = data.list_to_string(list_data)
+    assert string_data == 'N/A'
+
+def test_list_to_string_non_list_str():
+    list_data = 'abcde'
+    string_data = data.list_to_string(list_data)
+    assert string_data == 'abcde'
+
+def test_parse_kv_string():
+    kvdata = 'key1=value1;key2=value2;key3=value3'
+    parsed_data = data.parse_kv_string(kvdata)
+    assert parsed_data['key1'] == 'value1'
+    assert parsed_data['key2'] == 'value2'
+
 def list_data():
     test_data = [
         {
diff --git a/tests/test_rest.py b/tests/test_rest.py
index 1d8a597..7ffc684 100644
--- a/tests/test_rest.py
+++ b/tests/test_rest.py
@@ -5,18 +5,11 @@
 
 def test_get_endpoint():
 
-    mdca_endpoint = str(rest.get_endpoint('mdca'))
-    if mdca_endpoint.startswith('https://') and mdca_endpoint.endswith('portal.cloudappsecurity.com'):
-        mdca_valid = True
-    else:
-        mdca_valid = False
-
     assert rest.get_endpoint('msgraph') == 'https://graph.microsoft.com'
     assert rest.get_endpoint('la') == 'https://api.loganalytics.io'
     assert rest.get_endpoint('arm') == 'https://management.azure.com'
     assert rest.get_endpoint('m365') == 'https://api.security.microsoft.com'
     assert rest.get_endpoint('mde') == 'https://api.securitycenter.microsoft.com'
-    assert mdca_valid == True
 
 def test_rest_get():
     result = rest.rest_call_get(get_base_module_object(), 'msgraph', '/v1.0/organization')
diff --git a/tests/test_stat_exposure.py b/tests/test_stat_exposure.py
new file mode 100644
index 0000000..a86be4e
--- /dev/null
+++ b/tests/test_stat_exposure.py
@@ -0,0 +1,27 @@
+from modules import base, exposure_device, exposure_user
+from classes import Response
+import json, os, requests
+
+def test_user_exposure_module():
+    exposure_input = {
+        'AddIncidentComments': False,
+        'AddIncidentTask': False,
+        'BaseModuleBody': get_base_module_body()
+    }
+    exp_response:Response = exposure_user.execute_user_exposure_module(exposure_input)
+
+    assert exp_response.statuscode == 200
+
+def test_device_exposure_module():
+    exposure_input = {
+        'AddIncidentComments': False,
+        'AddIncidentTask': False,
+        'BaseModuleBody': get_base_module_body()
+    }
+    exp_response:Response = exposure_device.execute_device_exposure_module(exposure_input)
+
+    assert exp_response.statuscode == 200
+
+def get_base_module_body():
+    base_module_body = json.loads(requests.get(url=os.getenv('BASEDATA')).content)
+    return base_module_body
\ No newline at end of file
diff --git a/tests/test_stat_kql.py b/tests/test_stat_kql.py
index 469adf9..15df786 100644
--- a/tests/test_stat_kql.py
+++ b/tests/test_stat_kql.py
@@ -9,7 +9,7 @@ def test_kql_sentinel():
         'KQLQuery': 'SigninLogs | take 5 | project UserPrincipalName',
         'RunQueryAgainst': 'Sentinel',
         'QueryDescription': 'Test Query',
-        'LookbackInDays': 30,
+        'LookbackInDays': 7,
         'BaseModuleBody': get_base_module_body()
     }
     kql_response:Response = kql.execute_kql_module(kql_input)
@@ -22,10 +22,10 @@ def test_kql_sentinel_search():
         'AddIncidentComments': False,
         'AddIncidentTask': False,
         'Endpoint': 'search',
-        'KQLQuery': 'FirewallAUX_CL | take 5',
+        'KQLQuery': 'CommonSecurityLogAux_CL | take 5',
         'RunQueryAgainst': 'Sentinel',
         'QueryDescription': 'Test Query',
-        'LookbackInDays': 3,
+        'LookbackInDays': 7,
         'BaseModuleBody': get_base_module_body()
     }
     kql_response:Response = kql.execute_kql_module(kql_input)
@@ -40,7 +40,7 @@ def test_kql_m365():
         'KQLQuery': 'DeviceInfo | take 5 | project DeviceId',
         'RunQueryAgainst': 'M365',
         'QueryDescription': 'Test Query',
-        'LookbackInDays': 30,
+        'LookbackInDays': 7,
         'BaseModuleBody': get_base_module_body()
     }
     kql_response:Response = kql.execute_kql_module(kql_input)