GraphEssentials.psm1
function ConvertFrom-DistinguishedName { <# .SYNOPSIS Converts a Distinguished Name to CN, OU, Multiple OUs or DC .DESCRIPTION Converts a Distinguished Name to CN, OU, Multiple OUs or DC .PARAMETER DistinguishedName Distinguished Name to convert .PARAMETER ToOrganizationalUnit Converts DistinguishedName to Organizational Unit .PARAMETER ToDC Converts DistinguishedName to DC .PARAMETER ToDomainCN Converts DistinguishedName to Domain Canonical Name (CN) .PARAMETER ToCanonicalName Converts DistinguishedName to Canonical Name .EXAMPLE $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName -ToOrganizationalUnit Output: OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz .EXAMPLE $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName Output: Przemyslaw Klys .EXAMPLE ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToMultipleOrganizationalUnit -IncludeParent Output: OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz OU=Production,DC=ad,DC=evotec,DC=xyz .EXAMPLE ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToMultipleOrganizationalUnit Output: OU=Production,DC=ad,DC=evotec,DC=xyz .EXAMPLE $Con = @( 'CN=Windows Authorization Access Group,CN=Builtin,DC=ad,DC=evotec,DC=xyz' 'CN=Mmm,DC=elo,CN=nee,DC=RootDNSServers,CN=MicrosoftDNS,CN=System,DC=ad,DC=evotec,DC=xyz' 'CN=e6d5fd00-385d-4e65-b02d-9da3493ed850,CN=Operations,CN=DomainUpdates,CN=System,DC=ad,DC=evotec,DC=xyz' 'OU=Domain Controllers,DC=ad,DC=evotec,DC=pl' 'OU=Microsoft Exchange Security Groups,DC=ad,DC=evotec,DC=xyz' ) ConvertFrom-DistinguishedName -DistinguishedName $Con -ToLastName Output: Windows Authorization Access Group Mmm e6d5fd00-385d-4e65-b02d-9da3493ed850 Domain Controllers Microsoft Exchange Security Groups .EXAMPLEE ConvertFrom-DistinguishedName -DistinguishedName 'DC=ad,DC=evotec,DC=xyz' -ToCanonicalName ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToCanonicalName ConvertFrom-DistinguishedName -DistinguishedName 'CN=test,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToCanonicalName Output: ad.evotec.xyz ad.evotec.xyz\Production\Users ad.evotec.xyz\Production\Users\test .NOTES General notes #> [CmdletBinding(DefaultParameterSetName = 'Default')] param( [Parameter(ParameterSetName = 'ToOrganizationalUnit')] [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')] [Parameter(ParameterSetName = 'ToDC')] [Parameter(ParameterSetName = 'ToDomainCN')] [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'ToLastName')] [Parameter(ParameterSetName = 'ToCanonicalName')] [alias('Identity', 'DN')][Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)][string[]] $DistinguishedName, [Parameter(ParameterSetName = 'ToOrganizationalUnit')][switch] $ToOrganizationalUnit, [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')][alias('ToMultipleOU')][switch] $ToMultipleOrganizationalUnit, [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')][switch] $IncludeParent, [Parameter(ParameterSetName = 'ToDC')][switch] $ToDC, [Parameter(ParameterSetName = 'ToDomainCN')][switch] $ToDomainCN, [Parameter(ParameterSetName = 'ToLastName')][switch] $ToLastName, [Parameter(ParameterSetName = 'ToCanonicalName')][switch] $ToCanonicalName ) Process { foreach ($Distinguished in $DistinguishedName) { if ($ToDomainCN) { $DN = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1' $CN = $DN -replace ',DC=', '.' -replace "DC=" if ($CN) { $CN } } elseif ($ToOrganizationalUnit) { $Value = [Regex]::Match($Distinguished, '(?=OU=)(.*\n?)(?<=.)').Value if ($Value) { $Value } } elseif ($ToMultipleOrganizationalUnit) { if ($IncludeParent) { $Distinguished } while ($true) { #$dn = $dn -replace '^.+?,(?=CN|OU|DC)' $Distinguished = $Distinguished -replace '^.+?,(?=..=)' if ($Distinguished -match '^DC=') { break } $Distinguished } } elseif ($ToDC) { #return [Regex]::Match($DistinguishedName, '(?=DC=)(.*\n?)(?<=.)').Value # return [Regex]::Match($DistinguishedName, '.*?(DC=.*)').Value $Value = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1' if ($Value) { $Value } #return [Regex]::Match($DistinguishedName, 'CN=.*?(DC=.*)').Groups[1].Value } elseif ($ToLastName) { # Would be best if it worked, but there is too many edge cases so hand splits seems to be the best solution # Feel free to change it back to regex if you know how ;) <# https://stackoverflow.com/questions/51761894/regex-extract-ou-from-distinguished-name $Regex = "^(?:(?<cn>CN=(?<name>.*?)),)?(?<parent>(?:(?<path>(?:CN|OU).*?),)?(?<domain>(?:DC=.*)+))$" $Found = $Distinguished -match $Regex if ($Found) { $Matches.name } #> $NewDN = $Distinguished -split ",DC=" if ($NewDN[0].Contains(",OU=")) { [Array] $ChangedDN = $NewDN[0] -split ",OU=" } elseif ($NewDN[0].Contains(",CN=")) { [Array] $ChangedDN = $NewDN[0] -split ",CN=" } else { [Array] $ChangedDN = $NewDN[0] } if ($ChangedDN[0].StartsWith('CN=')) { $ChangedDN[0] -replace 'CN=', '' } else { $ChangedDN[0] -replace 'OU=', '' } } elseif ($ToCanonicalName) { $Domain = $null $Rest = $null foreach ($O in $Distinguished -split '(?<!\\),') { if ($O -match '^DC=') { $Domain += $O.Substring(3) + '.' } else { $Rest = $O.Substring(3) + '\' + $Rest } } if ($Domain -and $Rest) { $Domain.Trim('.') + '\' + ($Rest.TrimEnd('\') -replace '\\,', ',') } elseif ($Domain) { $Domain.Trim('.') } elseif ($Rest) { $Rest.TrimEnd('\') -replace '\\,', ',' } } else { $Regex = '^CN=(?<cn>.+?)(?<!\\),(?<ou>(?:(?:OU|CN).+?(?<!\\),)+(?<dc>DC.+?))$' #$Output = foreach ($_ in $Distinguished) { $Found = $Distinguished -match $Regex if ($Found) { $Matches.cn } #} #$Output.cn } } } } function Convert-Office365License { <# .SYNOPSIS This function helps converting Office 365 licenses from/to their SKU equivalent .DESCRIPTION This function helps converting Office 365 licenses from/to their SKU equivalent .PARAMETER License License SKU or License Name. Takes multiple values. .PARAMETER ToSku Converts license name to SKU .PARAMETER Separator .PARAMETER ReturnArray .EXAMPLE Convert-Office365License -License 'VISIOCLIENT','PROJECTONLINE_PLAN_1','test','tenant:VISIOCLIENT' .EXAMPLE Convert-Office365License -License "Office 365 (Plan A3) for Faculty","Office 365 (Enterprise Preview)", 'test' -ToSku #> [CmdletBinding()] param( [Parameter(Position = 0, ValueFromPipeline)][Array] $License, [alias('SKU')][switch] $ToSku, [string] $Separator = ', ', [switch] $ReturnArray ) Begin { $O365SKU = @{ "ADALLOM_S_STANDALONE" = "Microsoft Defender for Cloud Apps" "RMS_S_PREMIUM2" = "Azure Information Protection Premium P2" "RMS_S_PREMIUM" = "Azure Information Protection Premium P1" "MFA_PREMIUM" = "Microsoft Azure Multi-Factor Authentication" "MTP" = "Microsoft 365 Defender" "SAFEDOCS" = "Office 365 SafeDocs" "WINDEFATP" = "Microsoft Defender for Endpoint" "DYN365_CDS_VIRAL" = "Common Data Service" "FLOW_P2_VIRAL" = "Flow Free" "VIVAENGAGE_CORE" = "Viva Engage Core" "VIVA_LEARNING_SEEDED" = "Viva Learning Seeded" "POWER_VIRTUAL_AGENTS_O365_P2" = "Power Virtual Agents for Office 365" "CDS_O365_P2" = "Common Data Service for Teams" "PROJECT_O365_P2" = "Project for Office (Plan E3)" "DYN365_CDS_O365_P2" = "Common Data Service" "MICROSOFTBOOKINGS" = "Microsoft Bookings" "KAIZALA_O365_P3" = "Microsoft Kaizala Pro" "WHITEBOARD_PLAN2" = "Whiteboard (Plan 2)" "MIP_S_CLP1" = "Information Protection for Office 365 - Standard" "MYANALYTICS_P2" = "Insights by MyAnalytics" "BPOS_S_TODO_2" = "To-Do (Plan 2)" "FORMS_PLAN_E3" = "Microsoft Forms (Plan E3)" "STREAM_O365_E3" = "Microsoft Stream for Office 365 E3" "Deskless" = "Microsoft StaffHub" "FLOW_O365_P2" = "Power Automate for Office 365" "POWERAPPS_O365_P2" = "Power Apps for Office 365" "PROJECTWORKMANAGEMENT" = "Microsoft Planner" "SWAY" = "Sway" "RMS_S_ENTERPRISE" = "Azure Rights Management" "BI_AZURE_P0" = "Power BI (free)" "BI_AZURE_P2" = "Power BI Pro" "AAD_BASIC" = "Azure Active Directory Basic" "AAD_PREMIUM" = "Azure Active Directory Premium P1" "AAD_PREMIUM_P2" = "Azure Active Directory Premium P2" "ADALLOM_O365" = "Office 365 Cloud App Security" "ADALLOM_STANDALONE" = "Microsoft Cloud App Security" "ADV_COMMS" = "Advanced Communications" "ATA" = "Microsoft Defender for Identity" "ATP_ENTERPRISE" = "Microsoft Defender for Office 365 (Plan 1)" "ATP_ENTERPRISE_FACULTY" = "Microsoft Defender for Office 365 (Plan 1) Faculty" "AX7_USER_TRIAL" = "Microsoft Dynamics AX7 User Trial" "BI_AZURE_P1" = "Power BI Reporting and Analytics" "BUSINESS_VOICE_DIRECTROUTING" = "Microsoft 365 Business Voice (without calling plan)" "BUSINESS_VOICE_DIRECTROUTING_MED" = "Microsoft 365 Business Voice (without Calling Plan) for US" "BUSINESS_VOICE_MED2_TELCO" = "Microsoft 365 Business Voice (US)" "CCIBOTS_PRIVPREV_VIRAL" = "Power Virtual Agents Viral Trial" "CDS_DB_CAPACITY" = "Common Data Service Database Capacity" "CDS_LOG_CAPACITY" = "Common Data Service Log Capacity" "CDSAICAPACITY" = "AI Builder Capacity add-on" "CPC_B_2C_4RAM_64GB" = "Windows 365 Business 2 vCPU 4 GB 64 GB" "CPC_B_2C_4RAM_64GB_WHB" = "Windows 365 Business 2 vCPU, 4 GB, 64 GB (with Windows Hybrid Benefit)" "CPC_B_4C_16RAM_128GB_WHB" = "Windows 365 Business 4 vCPU 16 GB 128 GB (with Windows Hybrid Benefit)" "CPC_E_2C_4GB_64GB" = "Windows 365 Enterprise 2 vCPU 4 GB 64 GB" "CRM_ONLINE_PORTAL" = "Dynamics 365 Enterprise Edition - Additional Portal (Qualified Offer)" "CRMINSTANCE" = "Dynamics 365 - Additional Production Instance (Qualified Offer)" "CRMIUR" = "CMRIUR" "CRMPLAN2" = "Dynamics CRM Online Plan 2" "CRMSTANDARD" = "Microsoft Dynamics CRM Online Professional" "CRMSTORAGE" = "Dynamics 365 - Additional Database Storage (Qualified Offer)" "CRMTESTINSTANCE" = "Dynamics 365 - Additional Non-Production Instance (Qualified Offer)" "D365_SALES_ENT_ATTACH" = "Dynamics 365 Sales Enterprise Attach to Qualifying Dynamics 365 Base Offer" "DEFENDER_ENDPOINT_P1" = "Microsoft Defender for Endpoint P1" "DESKLESSPACK" = "OFFICE 365 F3" "DESKLESSPACK_GOV" = "Microsoft Office 365 (Plan F1) for Government" "DESKLESSPACK_YAMMER" = "Office 365 Enterprise F1 with Yammer" "DESKLESSWOFFPACK" = "Office 365 (Plan F2)" "DEVELOPERPACK" = "OFFICE 365 E3 DEVELOPER" "DYN365_AI_SERVICE_INSIGHTS" = "Dynamics 365 Customer Service Insights Trial" "DYN365_ASSETMANAGEMENT" = "Dynamics 365 Asset Management Addl Assets" "DYN365_BUSCENTRAL_ADD_ENV_ADDON" = "Dynamics 365 Business Central Additional Environment Addon" "DYN365_BUSCENTRAL_DB_CAPACITY" = "Dynamics 365 Business Central Database Capacity" "DYN365_BUSCENTRAL_ESSENTIAL" = "Dynamics 365 Business Central Essentials" "DYN365_BUSCENTRAL_PREMIUM" = "Dynamics 365 Business Central Premium" "DYN365_CUSTOMER_SERVICE_PRO" = "Dynamics 365 Customer Service Professional" "DYN365_CUSTOMER_VOICE_ADDON" = "Dynamics 365 Customer Voice Additional Responses" "DYN365_ENTERPRISE_CUSTOMER_SERVICE" = "DYNAMICS 365 FOR CUSTOMER SERVICE ENTERPRISE EDITION" "DYN365_ENTERPRISE_P1_IW" = "Dynamics 365 P1 Trial for Information Workers" "DYN365_ENTERPRISE_PLAN1" = "Dynamics 365 Customer Engagement Plan" "DYN365_ENTERPRISE_SALES" = "Dynamics 365 for Enterprise Sales Edition" "DYN365_ENTERPRISE_SALES_CUSTOMERSERVICE" = "DYNAMICS 365 FOR SALES AND CUSTOMER SERVICE ENTERPRISE EDITION" "DYN365_ENTERPRISE_TEAM_MEMBERS" = "Dynamics 365 For Team Members Enterprise Edition" "DYN365_FINANCE" = "Dynamics 365 Finance" "DYN365_FINANCIALS_ACCOUNTANT_SKU" = "Dynamics 365 Business Central External Accountant" "DYN365_FINANCIALS_BUSINESS_SKU" = "Dynamics 365 for Financials Business Edition" "DYN365_FINANCIALS_TEAM_MEMBERS_SKU" = "Dynamics 365 for Team Members Business Edition" "DYN365_IOT_INTELLIGENCE_ADDL_MACHINES" = "Sensor Data Intelligence Additional Machines Add-in for Dynamics 365 Supply Chain Management" "DYN365_IOT_INTELLIGENCE_SCENARIO" = "Sensor Data Intelligence Scenario Add-in for Dynamics 365 Supply Chain Management" "DYN365_SCM" = "DYNAMICS 365 FOR SUPPLY CHAIN MANAGEMENT" "DYN365_TEAM_MEMBERS" = "DYNAMICS 365 TEAM MEMBERS" "Dynamics_365_for_Operations" = "DYNAMICS 365 UNF OPS PLAN ENT EDITION" "Dynamics_365_for_Operations_Devices" = "Dynamics 365 Operations - Device" "Dynamics_365_for_Operations_Sandbox_Tier2_SKU" = "Dynamics 365 Operations - Sandbox Tier 2:Standard Acceptance Testing" "Dynamics_365_for_Operations_Sandbox_Tier4_SKU" = "Dynamics 365 Operations - Sandbox Tier 4:Standard Performance Testing" "Dynamics_365_Onboarding_SKU" = "Dynamics 365 Talent: Onboard" "ECAL_SERVICES" = "ECAL" "EMS" = "Enterprise Mobility + Security E3" "EMS_GOV" = "Enterprise Mobility + Security G3 GCC" "EMSPREMIUM" = "Enterprise Mobility + Security E5" "EMSPREMIUM_GOV" = "Enterprise Mobility + Security G5 GCC" "ENTERPRISEPACK" = "Office 365 E3" "ENTERPRISEPACK_B_PILOT" = "Office 365 (Enterprise Preview)" "ENTERPRISEPACK_FACULTY" = "Office 365 (Plan A3) for Faculty" "ENTERPRISEPACK_GOV" = "Microsoft Office 365 (Plan G3) for Government" "ENTERPRISEPACK_STUDENT" = "Office 365 (Plan A3) for Students" "ENTERPRISEPACK_USGOV_DOD" = "Office 365 E3_USGOV_DOD" "ENTERPRISEPACK_USGOV_GCCHIGH" = "Office 365 E3_USGOV_GCCHIGH" "ENTERPRISEPACKLRG" = "Office 365 Enterprise E3" "ENTERPRISEPACKPLUS_FACULTY" = "Office 365 A3 for faculty" "ENTERPRISEPACKPLUS_STUDENT" = "Office 365 A3 for students" "ENTERPRISEPACKWITHOUTPROPLUS" = "Office 365 Enterprise E3 without ProPlus Add-on" "ENTERPRISEPREMIUM" = "Office 365 E5" "ENTERPRISEPREMIUM_FACULTY" = "Office 365 A5 for faculty" "ENTERPRISEPREMIUM_GOV" = "Office 365 G5 GCC" "ENTERPRISEPREMIUM_NOPSTNCONF" = "Office 365 E5 (without Audio Conferencing)" "ENTERPRISEPREMIUM_STUDENT" = "Office 365 A5 for students" "ENTERPRISEWITHSCAL" = "Office 365 E4" "ENTERPRISEWITHSCAL_FACULTY" = "Office 365 (Plan A4) for Faculty" "ENTERPRISEWITHSCAL_GOV" = "Microsoft Office 365 (Plan G4) for Government" "ENTERPRISEWITHSCAL_STUDENT" = "Office 365 (Plan A4) for Students" "EOP_ENTERPRISE_FACULTY" = "Exchange Online Protection for Faculty" "EOP_ENTERPRISE_PREMIUM" = "Exchange Enterprise CAL Services (EOP DLP)" "EQUIVIO_ANALYTICS" = "Office 365 Advanced Compliance" "EQUIVIO_ANALYTICS_FACULTY" = "Office 365 Advanced Compliance for faculty" "ESKLESSWOFFPACK_GOV" = "Microsoft Office 365 (Plan F2) for Government" "EXCHANGE_L_STANDARD" = "Exchange Online (Plan 1)" "EXCHANGE_S_ARCHIVE_ADDON_GOV" = "Exchange Online Archiving" "EXCHANGE_S_DESKLESS" = "Exchange Online Kiosk" "EXCHANGE_S_DESKLESS_GOV" = "Exchange Kiosk" "EXCHANGE_S_ENTERPRISE" = "Exchange Online (Plan 2)" "EXCHANGE_S_ENTERPRISE_GOV" = "Exchange Online (Plan 2) for Government" "EXCHANGE_S_ESSENTIALS" = "EXCHANGE ONLINE ESSENTIALS" "EXCHANGE_S_STANDARD" = "Exchange Online (Plan 2)" "EXCHANGE_S_STANDARD_MIDMARKET" = "Exchange Online (Plan 1)" "EXCHANGEARCHIVE" = "EXCHANGE ONLINE ARCHIVING FOR EXCHANGE SERVER" "EXCHANGEARCHIVE_ADDON" = "Exchange Online Archiving For Exchange Online" "EXCHANGEDESKLESS" = "Exchange Online Kiosk" "EXCHANGEENTERPRISE" = "Exchange Online (Plan 2)" "EXCHANGEENTERPRISE_FACULTY" = "Exchange Online Plan 2 for Faculty" "EXCHANGEENTERPRISE_GOV" = "Microsoft Office 365 Exchange Online (Plan 2) only for Government" "EXCHANGEESSENTIALS" = "EXCHANGE ONLINE ESSENTIALS (ExO P1 BASED)" "EXCHANGESTANDARD" = "Exchange Online (Plan 1)" "EXCHANGESTANDARD_GOV" = "Microsoft Office 365 Exchange Online (Plan 1) only for Government" "EXCHANGESTANDARD_STUDENT" = "Exchange Online (Plan 1) for Students" "EXCHANGETELCO" = "EXCHANGE ONLINE POP" "EXPERTS_ON_DEMAND" = "Microsoft Threat Experts - Experts on Demand" "FLOW_BUSINESS_PROCESS" = "Power Automate per flow plan" "FLOW_FREE" = "Microsoft Power Automate Free" "FLOW_P1" = "Microsoft Flow Plan 1" "FLOW_P2" = "MICROSOFT POWER AUTOMATE PLAN 2" "FLOW_PER_USER" = "Power Automate per user plan" "FLOW_PER_USER_DEPT" = "Power Automate per user plan dept" "FORMS_PRO" = "Dynamics 365 Customer Voice Trial" "Forms_Pro_AddOn" = "Dynamics 365 Customer Voice Additional Responses" "Forms_Pro_USL" = "Dynamics 365 Customer Voice USL" "GUIDES_USER" = "Dynamics 365 Guides" "IDENTITY_THREAT_PROTECTION" = "Microsoft 365 E5 Security" "IDENTITY_THREAT_PROTECTION_FOR_EMS_E5" = "Microsoft 365 E5 Security for EMS E5" "INFORMATION_PROTECTION_COMPLIANCE" = "Microsoft 365 E5 Compliance" "Intelligent_Content_Services" = "SharePoint Syntex" "INTUNE_A" = "Microsoft Intune" "INTUNE_A_D" = "Microsoft Intune Device" "INTUNE_A_D_GOV" = "Microsoft Intune Device FOR GOVERNMENT" "INTUNE_A_VL" = "Intune (Volume License)" "INTUNE_SMB" = "Microsoft Intune SMB" "IT_ACADEMY_AD" = "Microsoft Imagine Academy" "LITEPACK" = "Office 365 Small Business" "LITEPACK_P2" = "Office 365 Small Business Premium" "M365_E5_SUITE_COMPONENTS" = "Microsoft 365 E5 Suite features" "M365_F1" = "Microsoft 365 F1" "M365_F1_COMM" = "Microsoft 365 F1" "M365_G3_GOV" = "MICROSOFT 365 G3 GCC" "M365_SECURITY_COMPLIANCE_FOR_FLW" = "Microsoft 365 Security and Compliance for Firstline Workers" "M365EDU_A1" = "Microsoft 365 A1" "M365EDU_A3_FACULTY" = "Microsoft 365 A3 for Faculty" "M365EDU_A3_STUDENT" = "MICROSOFT 365 A3 FOR STUDENTS" "M365EDU_A3_STUUSEBNFT" = "Microsoft 365 A3 for students use benefit" "M365EDU_A3_STUUSEBNFT_RPA1" = "Microsoft 365 A3 - Unattended License for students use benefit" "M365EDU_A5_FACULTY" = "Microsoft 365 A5 for Faculty" "M365EDU_A5_NOPSTNCONF_STUUSEBNFT" = "Microsoft 365 A5 without Audio Conferencing for students use benefit" "M365EDU_A5_STUDENT" = "MICROSOFT 365 A5 FOR STUDENTS" "M365EDU_A5_STUUSEBNFT" = "Microsoft 365 A5 for students use benefit" "MCOCAP" = "Common Area Phone" "MCOCAP_GOV" = "Common Area Phone for GCC" "MCOEV" = "Microsoft 365 Phone System" "MCOEV_DOD" = "MICROSOFT 365 PHONE SYSTEM FOR DOD" "MCOEV_FACULTY" = "MICROSOFT 365 PHONE SYSTEM FOR FACULTY" "MCOEV_GCCHIGH" = "MICROSOFT 365 PHONE SYSTEM FOR GCCHIGH" "MCOEV_GOV" = "MICROSOFT 365 PHONE SYSTEM FOR GCC" "MCOEV_STUDENT" = "MICROSOFT 365 PHONE SYSTEM FOR STUDENTS" "MCOEV_TELSTRA" = "MICROSOFT 365 PHONE SYSTEM FOR TELSTRA" "MCOEV_USGOV_DOD" = "MICROSOFT 365 PHONE SYSTEM_USGOV_DOD" "MCOEV_USGOV_GCCHIGH" = "MICROSOFT 365 PHONE SYSTEM_USGOV_GCCHIGH" "MCOEVSMB_1" = "MICROSOFT 365 PHONE SYSTEM FOR SMALL AND MEDIUM BUSINESS" "MCOIMP" = "SKYPE FOR BUSINESS ONLINE (PLAN 1)" "MCOLITE" = "Skype for Business Online (Plan 1)" "MCOMEETADV" = "Microsoft 365 Audio Conferencing" "MCOMEETADV_GOC" = "MICROSOFT 365 AUDIO CONFERENCING FOR GCC" "MCOMEETADV_GOV" = "MICROSOFT 365 AUDIO CONFERENCING FOR GCC" "MCOPSTN_1_GOV" = "Microsoft 365 Domestic Calling Plan for GCC" "MCOPSTN_5" = "MICROSOFT 365 DOMESTIC CALLING PLAN (120 Minutes)" "MCOPSTN1" = "Microsoft 365 Domestic Calling Plan" "MCOPSTN2" = "Domestic and International Calling Plan" "MCOPSTN5" = "SKYPE FOR BUSINESS PSTN DOMESTIC CALLING (120 Minutes)" "MCOPSTNC" = "Communications Credits" "MCOPSTNEAU2" = "TELSTRA CALLING FOR O365" "MCOSTANDARD" = "Skype for Business Online (Plan 2)" "MCOSTANDARD_GOV" = "Skype for Business Online (Plan 2) Government" "MCOSTANDARD_MIDMARKET" = "Skype for Business Online (Plan 1)" "MDATP_Server" = "Microsoft Defender for Endpoint Server" "MDATP_XPLAT" = "Microsoft Defender for Endpoint P2" "MEE_FACULTY" = "Minecraft Education Edition Faculty" "MEE_STUDENT" = "Minecraft Education Edition Student" "MEETING_ROOM" = "Microsoft Teams Rooms Standard" "MFA_STANDALONE" = "Microsoft Azure Multi-Factor Authentication" "MICROSOFT_BUSINESS_CENTER" = "Microsoft Business Center" "MICROSOFT_REMOTE_ASSIST" = "Dynamics 365 Remote Assist" "MICROSOFT_REMOTE_ASSIST_HOLOLENS" = "Dynamics 365 Remote Assist HoloLens" "MIDSIZEPACK" = "Office 365 Midsize Business" "MS_TEAMS_IW" = "Microsoft Teams Trial" "MTR_PREM" = "Teams Rooms Premium" "O365_BUSINESS" = "Microsoft 365 Apps for Business" "O365_BUSINESS_ESSENTIALS" = "Microsoft 365 Business Basic" "O365_BUSINESS_PREMIUM" = "Microsoft 365 Business Standard" "OFFICE_PRO_PLUS_SUBSCRIPTION_SMBIZ" = "Office ProPlus" "OFFICE365_MULTIGEO" = "Multi-Geo Capabilities in Office 365" "OFFICESUBSCRIPTION" = "Microsoft 365 Apps for Enterprise" "OFFICESUBSCRIPTION_FACULTY" = "Office 365 ProPlus for Faculty" "OFFICESUBSCRIPTION_GOV" = "Office ProPlus" "OFFICESUBSCRIPTION_STUDENT" = "Office ProPlus Student Benefit" "PBI_PREMIUM_P1_ADDON" = "Power BI Premium P1" "PBI_PREMIUM_PER_USER" = "Power BI Premium Per User" "PBI_PREMIUM_PER_USER_ADDON" = "Power BI Premium Per User Add-On" "PBI_PREMIUM_PER_USER_DEPT" = "Power BI Premium Per User Dept" "PHONESYSTEM_VIRTUALUSER" = "MICROSOFT 365 PHONE SYSTEM - VIRTUAL USER" "PHONESYSTEM_VIRTUALUSER_GOV" = "Microsoft 365 Phone System - Virtual User for GCC" "PLANNERSTANDALONE" = "Planner Standalone" "POWERAPPS_DEV" = "Microsoft PowerApps for Developer" "POWER_BI_ADDON" = "Power BI for Office 365 Add-on" "POWER_BI_INDIVIDUAL_USER" = "Power BI" "POWER_BI_PRO" = "Power BI Pro" "POWER_BI_PRO_CE" = "Power BI Pro CE" "POWER_BI_PRO_DEPT" = "Power BI Pro Dept" "POWER_BI_STANDALONE" = "Power BI Stand Alone" "POWER_BI_STANDARD" = "Power BI (free)" "POWERAPPS_INDIVIDUAL_USER" = "Microsoft PowerApps and Logic flows" "POWERAPPS_PER_APP" = "Power Apps per app plan" "POWERAPPS_PER_APP_IW" = "PowerApps per app baseline access" "POWERAPPS_PER_USER" = "Power Apps per user plan" "POWERAPPS_VIRAL" = "Microsoft Power Apps Plan 2 Trial" "POWERAUTOMATE_ATTENDED_RPA" = "Power Automate per user with attended RPA plan" "POWERAUTOMATE_UNATTENDED_RPA" = "Power Automate unattended RPA add-on" "POWERFLOW_P2" = "Microsoft Power Apps Plan 2 (Qualified Offer)" "PROJECT_MADEIRA_PREVIEW_IW_SKU" = "Dynamics 365 Business Central for IWs" "PROJECT_P1" = "Project Plan 1" "PROJECT_PLAN1_DEPT" = "Project Plan 1 (for Department)" "PROJECT_PLAN3_DEPT" = "Project Plan 3 (for Department)" "PROJECTCLIENT" = "Project for Office 365" "PROJECTESSENTIALS" = "Project Online Essentials" "PROJECTONLINE_PLAN_1" = "Project Online Premium (without Project Client)" "PROJECTONLINE_PLAN_1_FACULTY" = "Project Online for Faculty Plan 1" "PROJECTONLINE_PLAN_1_STUDENT" = "Project Online for Students Plan 1" "PROJECTONLINE_PLAN_2" = "Project Online with Project for Office 365" "PROJECTONLINE_PLAN_2_FACULTY" = "Project Online for Faculty Plan 2" "PROJECTONLINE_PLAN_2_STUDENT" = "Project Online for Students Plan 2" "PROJECTPREMIUM" = "Project Plan 5" "PROJECTPREMIUM_GOV" = "Project Plan 5 for GCC" "PROJECTPROFESSIONAL" = "Project Plan 3" "PROJECTPROFESSIONAL_GOV" = "Project Plan 3 for GCC" "RIGHTSMANAGEMENT" = "Azure Information Protection Plan 1" "RIGHTSMANAGEMENT_ADHOC" = "Rights Management Adhoc" "RIGHTSMANAGEMENT_STANDARD_FACULTY" = "Information Rights Management for Faculty" "RIGHTSMANAGEMENT_STANDARD_STUDENT" = "Information Rights Management for Students" "RMS_S_ENTERPRISE_GOV" = "Windows Azure Active Directory Rights Management" "RMSBASIC" = "Rights Management Service Basic Content Protection" "SHAREPOINTDESKLESS" = "SharePoint Online Kiosk" "SHAREPOINTDESKLESS_GOV" = "SharePoint Online Kiosk" "SHAREPOINTENTERPRISE" = "SharePoint (Plan 2)" "SHAREPOINTENTERPRISE_GOV" = "SharePoint (Plan 2) Government" "SHAREPOINTENTERPRISE_MIDMARKET" = "SharePoint Online (Plan 1)" "SHAREPOINTLITE" = "SharePoint Online (Plan 1)" "SHAREPOINTSTANDARD" = "SharePoint Online Plan 1" "SHAREPOINTSTORAGE" = "Office 365 Extra File Storage" "SHAREPOINTSTORAGE_GOV" = "Office 365 Extra File Storage for GCC" "SHAREPOINTWAC" = "Office for the Web" "SHAREPOINTWAC_GOV" = "Office for the Web for Government" "SKU_Dynamics_365_for_HCM_Trial" = "Dynamics 365 for Talent" "SMB_APPS" = "Business Apps (free)" "SMB_BUSINESS" = "MICROSOFT 365 APPS FOR BUSINESS" "SMB_BUSINESS_ESSENTIALS" = "MICROSOFT 365 BUSINESS BASIC" "SMB_BUSINESS_PREMIUM" = "MICROSOFT 365 BUSINESS STANDARD - PREPAID LEGACY" "SPB" = "Microsoft 365 Business Premium" "SPE_E3" = "Microsoft 365 E3" "SPE_E3_RPA1" = "Microsoft 365 E3 - Unattended License" "SPE_E3_USGOV_DOD" = "Microsoft 365 E3_USGOV_DOD" "SPE_E3_USGOV_GCCHIGH" = "Microsoft 365 E3_USGOV_GCCHIGH" "SPE_E5" = "Microsoft 365 E5" "SPE_F1" = "Microsoft 365 F3" "SPZA_IW" = "App Connect IW" "STANDARD_B_PILOT" = "Office 365 (Small Business Preview)" "STANDARDPACK" = "Office 365 E1" "STANDARDPACK_FACULTY" = "Office 365 (Plan A1) for Faculty" "STANDARDPACK_GOV" = "Microsoft Office 365 (Plan G1) for Government" "STANDARDPACK_STUDENT" = "Office 365 (Plan A1) for Students" "STANDARDWOFFPACK" = "Office 365 E2" "STANDARDWOFFPACK_FACULTY" = "Office 365 Education E1 for Faculty" "STANDARDWOFFPACK_GOV" = "Microsoft Office 365 (Plan G2) for Government" "STANDARDWOFFPACK_IW_FACULTY" = "Office 365 Education for Faculty" "STANDARDWOFFPACK_IW_STUDENT" = "Office 365 Education for Students" "STANDARDWOFFPACK_STUDENT" = "Microsoft Office 365 (Plan A2) for Students" "STANDARDWOFFPACKPACK_FACULTY" = "Office 365 (Plan A2) for Faculty" "STANDARDWOFFPACKPACK_STUDENT" = "Office 365 (Plan A2) for Students" "STREAM" = "Microsoft Stream" "STREAM_P2" = "Microsoft Stream Plan 2" "STREAM_STORAGE" = "Microsoft Stream Storage Add-On (500 GB)" "TEAMS_COMMERCIAL_TRIAL" = "Microsoft Teams Commercial Cloud" "TEAMS_EXPLORATORY" = "Microsoft Teams Exploratory" "TEAMS_FREE" = "MICROSOFT TEAMS (FREE)" "TEAMS1" = "Microsoft Teams" "THREAT_INTELLIGENCE" = "Microsoft Defender for Office 365 (Plan 2)" "THREAT_INTELLIGENCE_GOV" = "Microsoft Defender for Office 365 (Plan 2) GCC" "TOPIC_EXPERIENCES" = "Viva Topics" "UNIVERSAL_PRINT" = "Universal Print" "VIDEO_INTEROP" = "Polycom Skype Meeting Video Interop for Skype for Business" "VIRTUAL_AGENT_BASE" = "Power Virtual Agent" "VISIO_PLAN1_DEPT" = "Visio Plan 1" "VISIO_PLAN2_DEPT" = "Visio Plan 2" "VISIOCLIENT" = "Visio Plan 2" "VISIOCLIENT_GOV" = "VISIO PLAN 2 FOR GCC" "VISIOONLINE_PLAN1" = "Visio Online Plan 1" "WACONEDRIVEENTERPRISE" = "OneDrive for Business (Plan 2)" "WACONEDRIVESTANDARD" = "ONEDRIVE FOR BUSINESS (PLAN 1)" "WACSHAREPOINTSTD" = "Office Online STD" "WIN_DEF_ATP" = "Microsoft Defender for Endpoint P1" "WIN10_ENT_A3_FAC" = "Windows 10 Enterprise A3 for faculty" "WIN10_ENT_A3_STU" = "Windows 10 Enterprise A3 for students" "WIN10_PRO_ENT_SUB" = "Windows 10 Enterprise E3" "Win10_VDA_E3" = "Windows 10 Enterprise E3" "WIN10_VDA_E5" = "Windows 10 Enterprise E5" "WINDOWS_STORE" = "Windows Store for Business" "WINE5_GCC_COMPAT" = "Windows 10 Enterprise E5 Commercial (GCC Compatible)" "WORKPLACE_ANALYTICS" = "Microsoft Workplace Analytics" "YAMMER_ENTERPRISE" = "Yammer Enterprise" "YAMMER_MIDSIZE" = "Yammer" } } Process { if (-not $ToSku) { $ConvertedLicenses = foreach ($LicenseToProcess in $License) { if ($LicenseToProcess -is [string]) { $L = $LicenseToProcess } elseif ($LicenseToProcess -is [Microsoft.Online.Administration.UserLicense]) { $L = $LicenseToProcess.AccountSkuId } else { continue } # Remove tenant from SKU #if ($L -match ':') { # $Split = $L -split ':' # $L = $Split[-1] #} # Removes : from tenant:VisioClient $L = $L -replace '.*(:)' $Conversion = $O365SKU[$L] if ($null -eq $Conversion) { $L } else { $Conversion } } } else { $ConvertedLicenses = :Outer foreach ($L in $License) { $Conversion = foreach ($_ in $O365SKU.GetEnumerator()) { if ($_.Value -eq $L) { $_.Name continue Outer } } if ($null -eq $Conversion) { $L } } } if ($ReturnArray) { $ConvertedLicenses } else { $ConvertedLicenses -join $Separator } } End { } } function Copy-Dictionary { [alias('Copy-Hashtable', 'Copy-OrderedHashtable')] [cmdletbinding()] param( [System.Collections.IDictionary] $Dictionary ) # create a deep-clone of an object $ms = [System.IO.MemoryStream]::new() $bf = [System.Runtime.Serialization.Formatters.Binary.BinaryFormatter]::new() $bf.Serialize($ms, $Dictionary) $ms.Position = 0 $clone = $bf.Deserialize($ms) $ms.Close() $clone } function Format-AddSpaceToSentence { <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER Text Parameter description .EXAMPLE $test = @( 'OnceUponATime', 'OnceUponATime1', 'Money@Risk', 'OnceUponATime123', 'AHappyMan2014' 'OnceUponATime_123' ) Format-AddSpaceToSentence -Text $Test $Test | Format-AddSpaceToSentence -ToLowerCase .NOTES General notes #> [CmdletBinding()] param( [Parameter(Mandatory = $false, ValueFromPipeline = $true, Position = 0)][string[]] $Text, [switch] $ToLowerCase ) Begin { } Process { $Value = foreach ($T in $Text) { ($T -creplace '([A-Z\W_]|\d+)(?<![a-z])', ' $&').trim() } if ($ToLowerCase) { $Value.ToLower() } else { $Value } } End { } } function Get-FileName { <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER Extension Parameter description .PARAMETER Temporary Parameter description .PARAMETER TemporaryFileOnly Parameter description .EXAMPLE Get-FileName -Temporary Output: 3ymsxvav.tmp .EXAMPLE Get-FileName -Temporary Output: C:\Users\pklys\AppData\Local\Temp\tmpD74C.tmp .EXAMPLE Get-FileName -Temporary -Extension 'xlsx' Output: C:\Users\pklys\AppData\Local\Temp\tmp45B6.xlsx .NOTES General notes #> [CmdletBinding()] param( [string] $Extension = 'tmp', [switch] $Temporary, [switch] $TemporaryFileOnly ) if ($Temporary) { # C:\Users\przemyslaw.klys\AppData\Local\Temp\p0v4bbif.xlsx return [io.path]::Combine([System.IO.Path]::GetTempPath(), "$($([System.IO.Path]::GetRandomFileName()).Split('.')[0]).$Extension") } if ($TemporaryFileOnly) { # Generates 3ymsxvav.tmp return "$($([System.IO.Path]::GetRandomFileName()).Split('.')[0]).$Extension" } } function Get-GitHubLatestRelease { <# .SYNOPSIS Gets one or more releases from GitHub repository .DESCRIPTION Gets one or more releases from GitHub repository .PARAMETER Url Url to github repository .EXAMPLE Get-GitHubLatestRelease -Url "https://api.github.com1/repos/evotecit/Testimo/releases" | Format-Table .NOTES General notes #> [CmdLetBinding()] param( [parameter(Mandatory)][alias('ReleasesUrl')][uri] $Url ) $ProgressPreference = 'SilentlyContinue' $Responds = Test-Connection -ComputerName $URl.Host -Quiet -Count 1 if ($Responds) { Try { [Array] $JsonOutput = (Invoke-WebRequest -Uri $Url -ErrorAction Stop | ConvertFrom-Json) foreach ($JsonContent in $JsonOutput) { [PSCustomObject] @{ PublishDate = [DateTime] $JsonContent.published_at CreatedDate = [DateTime] $JsonContent.created_at PreRelease = [bool] $JsonContent.prerelease Version = [version] ($JsonContent.name -replace 'v', '') Tag = $JsonContent.tag_name Branch = $JsonContent.target_commitish Errors = '' } } } catch { [PSCustomObject] @{ PublishDate = $null CreatedDate = $null PreRelease = $null Version = $null Tag = $null Branch = $null Errors = $_.Exception.Message } } } else { [PSCustomObject] @{ PublishDate = $null CreatedDate = $null PreRelease = $null Version = $null Tag = $null Branch = $null Errors = "No connection (ping) to $($Url.Host)" } } $ProgressPreference = 'Continue' } function Get-O365TenantID { <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER Domain Parameter description .EXAMPLE Get-O365TenantID -Domain 'evotec.pl' .NOTES General notes #> [cmdletbinding()] param( [parameter(Mandatory)][alias('DomainName')][string] $Domain ) $Invoke = Invoke-RestMethod "https://login.windows.net/$Domain/.well-known/openid-configuration" -Method GET -Verbose:$false if ($Invoke) { $Invoke.userinfo_endpoint.Split("/")[3] } } function Remove-EmptyValue { [alias('Remove-EmptyValues')] [CmdletBinding()] param( [alias('Splat', 'IDictionary')][Parameter(Mandatory)][System.Collections.IDictionary] $Hashtable, [string[]] $ExcludeParameter, [switch] $Recursive, [int] $Rerun, [switch] $DoNotRemoveNull, [switch] $DoNotRemoveEmpty, [switch] $DoNotRemoveEmptyArray, [switch] $DoNotRemoveEmptyDictionary ) foreach ($Key in [string[]] $Hashtable.Keys) { if ($Key -notin $ExcludeParameter) { if ($Recursive) { if ($Hashtable[$Key] -is [System.Collections.IDictionary]) { if ($Hashtable[$Key].Count -eq 0) { if (-not $DoNotRemoveEmptyDictionary) { $Hashtable.Remove($Key) } } else { Remove-EmptyValue -Hashtable $Hashtable[$Key] -Recursive:$Recursive } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } } if ($Rerun) { for ($i = 0; $i -lt $Rerun; $i++) { Remove-EmptyValue -Hashtable $Hashtable -Recursive:$Recursive } } } function Start-TimeLog { [CmdletBinding()] param() [System.Diagnostics.Stopwatch]::StartNew() } function Stop-TimeLog { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)][System.Diagnostics.Stopwatch] $Time, [ValidateSet('OneLiner', 'Array')][string] $Option = 'OneLiner', [switch] $Continue ) Begin { } Process { if ($Option -eq 'Array') { $TimeToExecute = "$($Time.Elapsed.Days) days", "$($Time.Elapsed.Hours) hours", "$($Time.Elapsed.Minutes) minutes", "$($Time.Elapsed.Seconds) seconds", "$($Time.Elapsed.Milliseconds) milliseconds" } else { $TimeToExecute = "$($Time.Elapsed.Days) days, $($Time.Elapsed.Hours) hours, $($Time.Elapsed.Minutes) minutes, $($Time.Elapsed.Seconds) seconds, $($Time.Elapsed.Milliseconds) milliseconds" } } End { if (-not $Continue) { $Time.Stop() } return $TimeToExecute } } function Write-Color { <# .SYNOPSIS Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options. .DESCRIPTION Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options. It provides: - Easy manipulation of colors, - Logging output to file (log) - Nice formatting options out of the box. - Ability to use aliases for parameters .PARAMETER Text Text to display on screen and write to log file if specified. Accepts an array of strings. .PARAMETER Color Color of the text. Accepts an array of colors. If more than one color is specified it will loop through colors for each string. If there are more strings than colors it will start from the beginning. Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White .PARAMETER BackGroundColor Color of the background. Accepts an array of colors. If more than one color is specified it will loop through colors for each string. If there are more strings than colors it will start from the beginning. Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White .PARAMETER StartTab Number of tabs to add before text. Default is 0. .PARAMETER LinesBefore Number of empty lines before text. Default is 0. .PARAMETER LinesAfter Number of empty lines after text. Default is 0. .PARAMETER StartSpaces Number of spaces to add before text. Default is 0. .PARAMETER LogFile Path to log file. If not specified no log file will be created. .PARAMETER DateTimeFormat Custom date and time format string. Default is yyyy-MM-dd HH:mm:ss .PARAMETER LogTime If set to $true it will add time to log file. Default is $true. .PARAMETER LogRetry Number of retries to write to log file, in case it can't write to it for some reason, before skipping. Default is 2. .PARAMETER Encoding Encoding of the log file. Default is Unicode. .PARAMETER ShowTime Switch to add time to console output. Default is not set. .PARAMETER NoNewLine Switch to not add new line at the end of the output. Default is not set. .PARAMETER NoConsoleOutput Switch to not output to console. Default all output goes to console. .EXAMPLE Write-Color -Text "Red ", "Green ", "Yellow " -Color Red,Green,Yellow .EXAMPLE Write-Color -Text "This is text in Green ", "followed by red ", "and then we have Magenta... ", "isn't it fun? ", "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan .EXAMPLE Write-Color -Text "This is text in Green ", "followed by red ", "and then we have Magenta... ", "isn't it fun? ", "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan -StartTab 3 -LinesBefore 1 -LinesAfter 1 .EXAMPLE Write-Color "1. ", "Option 1" -Color Yellow, Green Write-Color "2. ", "Option 2" -Color Yellow, Green Write-Color "3. ", "Option 3" -Color Yellow, Green Write-Color "4. ", "Option 4" -Color Yellow, Green Write-Color "9. ", "Press 9 to exit" -Color Yellow, Gray -LinesBefore 1 .EXAMPLE Write-Color -LinesBefore 2 -Text "This little ","message is ", "written to log ", "file as well." ` -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" -TimeFormat "yyyy-MM-dd HH:mm:ss" Write-Color -Text "This can get ","handy if ", "want to display things, and log actions to file ", "at the same time." ` -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" .EXAMPLE Write-Color -T "My text", " is ", "all colorful" -C Yellow, Red, Green -B Green, Green, Yellow Write-Color -t "my text" -c yellow -b green Write-Color -text "my text" -c red .EXAMPLE Write-Color -Text "TestujÄ™ czy siÄ™ Å‚adnie zapisze, czy bÄ™dÄ… problemy" -Encoding unicode -LogFile 'C:\temp\testinggg.txt' -Color Red -NoConsoleOutput .NOTES Understanding Custom date and time format strings: https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings Project support: https://github.com/EvotecIT/PSWriteColor Original idea: Josh (https://stackoverflow.com/users/81769/josh) #> [alias('Write-Colour')] [CmdletBinding()] param ( [alias ('T')] [String[]]$Text, [alias ('C', 'ForegroundColor', 'FGC')] [ConsoleColor[]]$Color = [ConsoleColor]::White, [alias ('B', 'BGC')] [ConsoleColor[]]$BackGroundColor = $null, [alias ('Indent')][int] $StartTab = 0, [int] $LinesBefore = 0, [int] $LinesAfter = 0, [int] $StartSpaces = 0, [alias ('L')] [string] $LogFile = '', [Alias('DateFormat', 'TimeFormat')][string] $DateTimeFormat = 'yyyy-MM-dd HH:mm:ss', [alias ('LogTimeStamp')][bool] $LogTime = $true, [int] $LogRetry = 2, [ValidateSet('unknown', 'string', 'unicode', 'bigendianunicode', 'utf8', 'utf7', 'utf32', 'ascii', 'default', 'oem')][string]$Encoding = 'Unicode', [switch] $ShowTime, [switch] $NoNewLine, [alias('HideConsole')][switch] $NoConsoleOutput ) if (-not $NoConsoleOutput) { $DefaultColor = $Color[0] if ($null -ne $BackGroundColor -and $BackGroundColor.Count -ne $Color.Count) { Write-Error "Colors, BackGroundColors parameters count doesn't match. Terminated." return } if ($LinesBefore -ne 0) { for ($i = 0; $i -lt $LinesBefore; $i++) { Write-Host -Object "`n" -NoNewline } } # Add empty line before if ($StartTab -ne 0) { for ($i = 0; $i -lt $StartTab; $i++) { Write-Host -Object "`t" -NoNewline } } # Add TABS before text if ($StartSpaces -ne 0) { for ($i = 0; $i -lt $StartSpaces; $i++) { Write-Host -Object ' ' -NoNewline } } # Add SPACES before text if ($ShowTime) { Write-Host -Object "[$([datetime]::Now.ToString($DateTimeFormat))] " -NoNewline } # Add Time before output if ($Text.Count -ne 0) { if ($Color.Count -ge $Text.Count) { # the real deal coloring if ($null -eq $BackGroundColor) { for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline } } else { for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline } } } else { if ($null -eq $BackGroundColor) { for ($i = 0; $i -lt $Color.Length ; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline } for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -NoNewline } } else { for ($i = 0; $i -lt $Color.Length ; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline } for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -BackgroundColor $BackGroundColor[0] -NoNewline } } } } if ($NoNewLine -eq $true) { Write-Host -NoNewline } else { Write-Host } # Support for no new line if ($LinesAfter -ne 0) { for ($i = 0; $i -lt $LinesAfter; $i++) { Write-Host -Object "`n" -NoNewline } } # Add empty line after } if ($Text.Count -and $LogFile) { # Save to file $TextToFile = "" for ($i = 0; $i -lt $Text.Length; $i++) { $TextToFile += $Text[$i] } $Saved = $false $Retry = 0 Do { $Retry++ try { if ($LogTime) { "[$([datetime]::Now.ToString($DateTimeFormat))] $TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false } else { "$TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false } $Saved = $true } catch { if ($Saved -eq $false -and $Retry -eq $LogRetry) { Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Tried ($Retry/$LogRetry))" } else { Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Retrying... ($Retry/$LogRetry)" } } } Until ($Saved -eq $true -or $Retry -ge $LogRetry) } } $Script:Apps = [ordered] @{ Name = 'Azure Active Directory Apps' Enabled = $true Execute = { Get-MyApp } Processing = { } Summary = { } Variables = @{ } Solution = { if ($Script:Reporting['Apps']['Data']) { New-HTMLTable -DataTable $Script:Reporting['Apps']['Data'] -Filtering { New-HTMLTableCondition -Name 'DescriptionWithEmail' -Operator eq -Value $true -ComparisonType string -BackgroundColor SpringGreen -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'KeysCount' -Operator gt -Value 1 -ComparisonType number -BackgroundColor GoldenFizz New-HTMLTableCondition -Name 'KeysCount' -Operator eq -Value 0 -ComparisonType number -BackgroundColor Salmon -Row New-HTMLTableCondition -Name 'KeysCount' -Operator eq -Value 1 -ComparisonType number -BackgroundColor SpringGreen New-HTMLTableCondition -Name 'Expired' -Operator eq -Value "No" -ComparisonType string -BackgroundColor SpringGreen New-HTMLTableCondition -Name 'Expired' -Operator eq -Value "Yes" -ComparisonType string -BackgroundColor Salmon -Row New-HTMLTableCondition -Name 'Expired' -Operator eq -Value "Not available" -ComparisonType string -BackgroundColor Salmon -Row } -ScrollX } } } $Script:AppsCredentials = [ordered] @{ Name = 'Azure Active Directory Apps Credentials' Enabled = $true Execute = { Get-MyAppCredentials } Processing = { } Summary = { } Variables = @{ } Solution = { if ($Script:Reporting['AppsCredentials']['Data']) { New-HTMLTable -DataTable $Script:Reporting['AppsCredentials']['Data'] -Filtering { New-HTMLTableCondition -Name 'Expired' -Operator eq -Value $false -ComparisonType string -BackgroundColor SpringGreen -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'DaysToExpire' -Value 30 -Operator 'ge' -BackgroundColor Conifer -ComparisonType number New-HTMLTableCondition -Name 'DaysToExpire' -Value 30 -Operator 'lt' -BackgroundColor Orange -ComparisonType number New-HTMLTableCondition -Name 'DaysToExpire' -Value 5 -Operator 'lt' -BackgroundColor Red -ComparisonType number } -ScrollX } } } $Script:Licenses = [ordered] @{ Name = 'Azure Licenses' Enabled = $true Execute = { Get-MyLicense } Processing = { } Summary = { } Variables = @{ } Solution = { if ($Script:Reporting['Licenses']['Data']) { New-HTMLTable -DataTable $Script:Reporting['Licenses']['Data'] -Filtering { New-HTMLTableCondition -Name 'CapabilityStatus' -Operator eq -Value 'Enabled' -ComparisonType string -BackgroundColor LightGreen -FailBackgroundColor Orange New-HTMLTableCondition -Name 'LicensesUsedPercent' -Operator gt -Value 100 -ComparisonType number -BackgroundColor Alizarin -HighlightHeaders 'LicensesUsedCount', 'LicensesUsedPercent' New-HTMLTableCondition -Name 'LicensesUsedPercent' -Operator eq -Value 100 -ComparisonType number -BackgroundColor Salmon -HighlightHeaders 'LicensesUsedCount', 'LicensesUsedPercent' New-HTMLTableCondition -Name 'LicensesUsedPercent' -Operator betweenInclusive -Value 70, 99 -ComparisonType number -BackgroundColor Orange -HighlightHeaders 'LicensesUsedCount', 'LicensesUsedPercent' New-HTMLTableCondition -Name 'LicensesUsedPercent' -Operator betweenInclusive -Value 40, 69 -ComparisonType number -BackgroundColor LightSkyBlue -HighlightHeaders 'LicensesUsedCount', 'LicensesUsedPercent' New-HTMLTableCondition -Name 'LicensesUsedPercent' -Operator betweenInclusive -Value 1, 39 -ComparisonType number -BackgroundColor Almond -HighlightHeaders 'LicensesUsedCount', 'LicensesUsedPercent' New-HTMLTableCondition -Name 'LicensesUsedPercent' -Operator eq -Value 0 -ComparisonType number -BackgroundColor LightGreen -HighlightHeaders 'LicensesUsedCount', 'LicensesUsedPercent' } -ScrollX } } } $Script:Devices = [ordered] @{ Name = 'Azure Active Directory Devices' Enabled = $true Execute = { Get-MyDevice } Processing = { } Summary = { } Variables = @{ } Solution = { if ($Script:Reporting['Devices']['Data']) { New-HTMLTable -DataTable $Script:Reporting['Devices']['Data'] -Filtering { New-HTMLTableCondition -Name 'Enabled' -Value $true -Operator eq -ComparisonType string -BackgroundColor MediumSpringGreen New-HTMLTableCondition -Name 'Enabled' -Value $false -Operator eq -ComparisonType string -BackgroundColor Cinnabar New-HTMLTableCondition -Name 'IsManaged' -Value $true -Operator eq -ComparisonType string -BackgroundColor MediumSpringGreen New-HTMLTableCondition -Name 'IsManaged' -Value $false -Operator eq -ComparisonType string -BackgroundColor Cinnabar New-HTMLTableCondition -Name 'IsCompliant' -Value $true -Operator eq -ComparisonType string -BackgroundColor MediumSpringGreen New-HTMLTableCondition -Name 'IsCompliant' -Value $false -Operator eq -ComparisonType string -BackgroundColor Cinnabar New-HTMLTableCondition -Name 'IsSynchronized' -Value $true -Operator eq -ComparisonType string -BackgroundColor MediumSpringGreen New-HTMLTableCondition -Name 'IsSynchronized' -Value $false -Operator eq -ComparisonType string -BackgroundColor Cinnabar New-HTMLTableCondition -Name 'LastSeenDays' -Value 180 -Operator gt -ComparisonType number -BackgroundColor CoralRed -HighlightHeaders 'LastSeenDays', 'LastSeen' New-HTMLTableCondition -Name 'LastSeenDays' -Value 180 -Operator le -ComparisonType number -BackgroundColor SunsetOrange -HighlightHeaders 'LastSeenDays', 'LastSeen' New-HTMLTableCondition -Name 'LastSeenDays' -Value 90 -Operator le -ComparisonType number -BackgroundColor LaserLemon -HighlightHeaders 'LastSeenDays', 'LastSeen' New-HTMLTableCondition -Name 'LastSeenDays' -Value 30 -Operator le -ComparisonType number -BackgroundColor MediumSpringGreen -HighlightHeaders 'LastSeenDays', 'LastSeen' New-HTMLTableCondition -Name 'LastSynchronizedDays' -Value 180 -Operator gt -ComparisonType number -BackgroundColor CoralRed -HighlightHeaders 'LastSynchronizedDays', 'LastSynchronized' New-HTMLTableCondition -Name 'LastSynchronizedDays' -Value 180 -Operator le -ComparisonType number -BackgroundColor SunsetOrange -HighlightHeaders 'LastSynchronizedDays', 'LastSynchronized' New-HTMLTableCondition -Name 'LastSynchronizedDays' -Value 90 -Operator le -ComparisonType number -BackgroundColor LaserLemon -HighlightHeaders 'LastSynchronizedDays', 'LastSynchronized' New-HTMLTableCondition -Name 'LastSynchronizedDays' -Value 30 -Operator le -ComparisonType number -BackgroundColor MediumSpringGreen -HighlightHeaders 'LastSynchronizedDays', 'LastSynchronized' New-HTMLTableCondition -Name 'OwnerEnabled' -ComparisonType string -Operator eq -Value $true -BackgroundColor MediumSpringGreen New-HTMLTableCondition -Name 'OwnerEnabled' -ComparisonType string -Operator eq -Value $false -BackgroundColor Cinnabar } -ScrollX } } } $Script:DevicesIntune = [ordered] @{ Name = 'Azure Active Directory Devices Intune' Enabled = $true Execute = { Get-MyDeviceIntune } Processing = { } Summary = { } Variables = @{ } Solution = { if ($Script:Reporting['DevicesIntune']['Data']) { New-HTMLTable -DataTable $Script:Reporting['DevicesIntune']['Data'] -Filtering { New-HTMLTableCondition -Name 'ComplianceState' -Operator eq -Value 'Compliant' -ComparisonType string -BackgroundColor MediumSpringGreen -FailBackgroundColor Cinnabar New-HTMLTableCondition -Name 'LastSeenDays' -Value 180 -Operator gt -ComparisonType number -BackgroundColor CoralRed -HighlightHeaders 'LastSeenDays', 'LastSeen' New-HTMLTableCondition -Name 'LastSeenDays' -Value 180 -Operator le -ComparisonType number -BackgroundColor SunsetOrange -HighlightHeaders 'LastSeenDays', 'LastSeen' New-HTMLTableCondition -Name 'LastSeenDays' -Value 90 -Operator le -ComparisonType number -BackgroundColor LaserLemon -HighlightHeaders 'LastSeenDays', 'LastSeen' New-HTMLTableCondition -Name 'LastSeenDays' -Value 30 -Operator le -ComparisonType number -BackgroundColor MediumSpringGreen -HighlightHeaders 'LastSeenDays', 'LastSeen' } -ScrollX } } } $Script:Roles = [ordered] @{ Name = 'Azure Active Directory Roles' Enabled = $true Execute = { Get-MyRole -OnlyWithMembers } Processing = { } Summary = { } Variables = @{ } Solution = { if ($Script:Reporting['Roles']['Data']) { New-HTMLTable -DataTable $Script:Reporting['Roles']['Data'] -Filtering { New-HTMLTableCondition -Name 'IsEnabled' -Operator eq -Value $true -ComparisonType string -BackgroundColor SpringGreen -FailBackgroundColor Salmon } -ScrollX } } } $Script:RolesUsers = [ordered] @{ Name = 'Azure Active Directory Roles Users' Enabled = $true Execute = { Get-MyRoleUsers -OnlyWithRoles } Processing = { } Summary = { } Variables = @{ } Solution = { if ($Script:Reporting['RolesUsers']['Data']) { New-HTMLTable -DataTable $Script:Reporting['RolesUsers']['Data'] -Filtering { New-HTMLTableCondition -Name 'Enabled' -Operator eq -Value $true -ComparisonType string -BackgroundColor SpringGreen New-HTMLTableCondition -Name 'Enabled' -Operator eq -Value $false -ComparisonType string -BackgroundColor Salmon New-HTMLTableCondition -Name 'Status' -Operator eq -Value 'Synchronized' -ComparisonType string -BackgroundColor SpringGreen New-HTMLTableCondition -Name 'Status' -Operator eq -Value 'Online' -ComparisonType string -BackgroundColor GoldenFizz } -ScrollX } } } $Script:RolesUsersPerColumn = [ordered] @{ Name = 'Azure Active Directory Roles Users Per Column' Enabled = $true Execute = { Get-MyRoleUsers -OnlyWithRoles -RolePerColumn } Processing = { } Summary = { } Variables = @{ } Solution = { if ($Script:Reporting['RolesUsersPerColumn']['Data']) { New-HTMLTable -DataTable $Script:Reporting['RolesUsersPerColumn']['Data'] -Filtering { New-HTMLTableCondition -Name 'Enabled' -Operator eq -Value $true -ComparisonType string -BackgroundColor SpringGreen New-HTMLTableCondition -Name 'Enabled' -Operator eq -Value $false -ComparisonType string -BackgroundColor Salmon foreach ($Name in $Script:Reporting['RolesUsersPerColumn']['Data'][0].PSObject.Properties.Name) { if ($Name -notin 'Name', 'Enabled', 'UserPrincipalName', 'Mail' , 'Status', 'Type', 'Location', 'CreatedDateTime') { New-HTMLTableCondition -Name $Name -Operator eq -Value 'Direct' -ComparisonType string -BackgroundColor GoldenFizz New-HTMLTableCondition -Name $Name -Operator eq -Value 'Eligible' -ComparisonType string -BackgroundColor SpringGreen New-HTMLTableConditionGroup -Conditions { New-HTMLTableCondition -Name $Name -Operator ne -Value 'Eligible' -ComparisonType string New-HTMLTableCondition -Name $Name -Operator ne -Value 'Direct' -ComparisonType string New-HTMLTableCondition -Name $Name -Operator ne -Value '' -ComparisonType string } -Logic AND -BackgroundColor Orange -HighlightHeaders $Name } } New-HTMLTableCondition -Name 'Status' -Operator eq -Value 'Synchronized' -ComparisonType string -BackgroundColor SpringGreen New-HTMLTableCondition -Name 'Status' -Operator eq -Value 'Online' -ComparisonType string -BackgroundColor GoldenFizz } -ScrollX } } } $Script:Teams = [ordered] @{ Name = 'Microsoft Teams Report' Enabled = $true Execute = { Get-MyTeam } Processing = { } Summary = { } Variables = @{ } Solution = { if ($Script:Reporting['Teams']['Data']) { New-HTMLTable -DataTable $Script:Reporting['Teams']['Data'] -Filtering { } -ScrollX } } } $Script:Users = [ordered] @{ Name = 'Azure Active Directory Users' Enabled = $true Execute = { Get-MyUser } Processing = { } Summary = { } Variables = @{ } Solution = { if ($Script:Reporting['Users']['Data']) { New-HTMLTable -DataTable $Script:Reporting['Users']['Data'] -Filtering { New-HTMLTableCondition -Name 'Enabled' -Operator eq -Value $false -ComparisonType string -BackgroundColor Salmon New-HTMLTableCondition -Name 'Enabled' -Operator eq -Value $true -ComparisonType string -BackgroundColor SpringGreen New-HTMLTableCondition -Name 'IsSynchronized' -Operator eq -Value $false -ComparisonType string -BackgroundColor Salmon New-HTMLTableCondition -Name 'IsSynchronized' -Operator eq -Value $true -ComparisonType string -BackgroundColor SpringGreen New-HTMLTableCondition -Name 'LicensesStatus' -Operator contains -Value 'Direct' -ComparisonType string -BackgroundColor LightSkyBlue New-HTMLTableCondition -Name 'LicensesStatus' -Operator contains -Value 'Group' -ComparisonType string -BackgroundColor LightGreen New-HTMLTableCondition -Name 'LicensesStatus' -Operator contains -Value 'Duplicate' -ComparisonType string -BackgroundColor PeachOrange New-HTMLTableCondition -Name 'LicensesStatus' -Operator contains -Value 'Error' -ComparisonType string -BackgroundColor Salmon -HighlightHeaders 'LicensesStatus', 'LicensesErrors' New-HTMLTableCondition -Name 'LicensesStatus' -Operator eq -Value '' -ComparisonType string -BackgroundColor OldGold New-HTMLTableCondition -Name 'LastPasswordChangeDays' -Value 180 -Operator gt -ComparisonType number -BackgroundColor CoralRed -HighlightHeaders 'LastPasswordChangeDays', 'LastPasswordChangeDateTime' New-HTMLTableCondition -Name 'LastPasswordChangeDays' -Value 180 -Operator le -ComparisonType number -BackgroundColor SunsetOrange -HighlightHeaders 'LastPasswordChangeDays', 'LastPasswordChangeDateTime' New-HTMLTableCondition -Name 'LastPasswordChangeDays' -Value 90 -Operator le -ComparisonType number -BackgroundColor LaserLemon -HighlightHeaders 'LastPasswordChangeDays', 'LastPasswordChangeDateTime' New-HTMLTableCondition -Name 'LastPasswordChangeDays' -Value 30 -Operator le -ComparisonType number -BackgroundColor MediumSpringGreen -HighlightHeaders 'LastPasswordChangeDays', 'LastPasswordChangeDateTime' New-HTMLTableCondition -Name 'LastSynchronizedDays' -Value 180 -Operator gt -ComparisonType number -BackgroundColor CoralRed -HighlightHeaders 'LastSynchronizedDays', 'LastSynchronized' New-HTMLTableCondition -Name 'LastSynchronizedDays' -Value 180 -Operator le -ComparisonType number -BackgroundColor SunsetOrange -HighlightHeaders 'LastSynchronizedDays', 'LastSynchronized' New-HTMLTableCondition -Name 'LastSynchronizedDays' -Value 90 -Operator le -ComparisonType number -BackgroundColor LaserLemon -HighlightHeaders 'LastSynchronizedDays', 'LastSynchronized' New-HTMLTableCondition -Name 'LastSynchronizedDays' -Value 30 -Operator le -ComparisonType number -BackgroundColor MediumSpringGreen -HighlightHeaders 'LastSynchronizedDays', 'LastSynchronized' } -ScrollX } } } $Script:UsersPerLicense = [ordered] @{ Name = 'Azure Active Directory Users Per License' Enabled = $true Execute = { Get-MyUser -PerLicense } Processing = { } Summary = { } Variables = @{ } Solution = { if ($Script:Reporting['UsersPerLicense']['Data']) { New-HTMLTable -DataTable $Script:Reporting['UsersPerLicense']['Data'] -Filtering { New-HTMLTableCondition -Name 'AccountEnabled' -Operator eq -Value $false -ComparisonType string -BackgroundColor Salmon New-HTMLTableCondition -Name 'AccountEnabled' -Operator eq -Value $true -ComparisonType string -BackgroundColor SpringGreen foreach ($Name in $Script:Reporting['UsersPerLicense']['Data'][0].PSObject.Properties.Name) { if ($Name -notin 'DisplayName', 'Id', 'Enabled', 'UserPrincipalName', 'AccountEnabled', 'Mail' , 'Manager', 'LicensesStatus', 'LicensesErrors', 'Surname', 'LastPasswordChangeDateTime', 'GivenName', 'JobTitle') { New-HTMLTableCondition -Name $Name -Operator eq -Value 'Direct' -ComparisonType string -BackgroundColor GoldenFizz New-HTMLTableCondition -Name $Name -Operator eq -Value 'Group' -ComparisonType string -BackgroundColor LightGreen New-HTMLTableConditionGroup -Conditions { New-HTMLTableCondition -Name $Name -Operator ne -Value 'Group' -ComparisonType string New-HTMLTableCondition -Name $Name -Operator ne -Value 'Direct' -ComparisonType string New-HTMLTableCondition -Name $Name -Operator ne -Value '' -ComparisonType string } -Logic AND -BackgroundColor Orange -HighlightHeaders $Name } } } -ScrollX } } } $Script:UsersPerServicePlan = [ordered] @{ Name = 'Azure Active Directory Users Per Service Plan' Enabled = $true Execute = { Get-MyUser -PerServicePlan } Processing = { } Summary = { } Variables = @{ } Solution = { if ($Script:Reporting['UsersPerServicePlan']['Data']) { New-HTMLTable -DataTable $Script:Reporting['UsersPerServicePlan']['Data'] -Filtering { New-HTMLTableCondition -Name 'AccountEnabled' -Operator eq -Value $false -ComparisonType string -BackgroundColor Salmon New-HTMLTableCondition -Name 'AccountEnabled' -Operator eq -Value $true -ComparisonType string -BackgroundColor SpringGreen } -ScrollX } } } function Get-GitHubVersion { [cmdletBinding()] param( [Parameter(Mandatory)][string] $Cmdlet, [Parameter(Mandatory)][string] $RepositoryOwner, [Parameter(Mandatory)][string] $RepositoryName ) $App = Get-Command -Name $Cmdlet -ErrorAction SilentlyContinue if ($App) { [Array] $GitHubReleases = (Get-GitHubLatestRelease -Url "https://api.github.com/repos/$RepositoryOwner/$RepositoryName/releases" -Verbose:$false) $LatestVersion = $GitHubReleases[0] if (-not $LatestVersion.Errors) { if ($App.Version -eq $LatestVersion.Version) { "Current/Latest: $($LatestVersion.Version) at $($LatestVersion.PublishDate)" } elseif ($App.Version -lt $LatestVersion.Version) { "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Update?" } elseif ($App.Version -gt $LatestVersion.Version) { "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Lucky you!" } } else { "Current: $($App.Version)" } } } function New-HTMLReportGraphEssentials { [cmdletBinding()] param( [Array] $Type, [switch] $Online, [switch] $HideHTML, [string] $FilePath ) New-HTML -Author 'PrzemysÅ‚aw KÅ‚ys @ Evotec' -TitleText 'GraphEssentials Report' { New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow New-HTMLPanelStyle -BorderRadius 0px New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ', ' -ArrayJoin New-HTMLHeader { New-HTMLSection -Invisible { New-HTMLSection { New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue } -JustifyContent flex-start -Invisible New-HTMLSection { New-HTMLText -Text "GraphEssentials - $($Script:Reporting['Version'])" -Color Blue } -JustifyContent flex-end -Invisible } } if ($Type.Count -eq 1) { foreach ($T in $Script:GraphEssentialsConfiguration.Keys) { if ($Script:GraphEssentialsConfiguration[$T].Enabled -eq $true) { if ($Script:GraphEssentialsConfiguration[$T]['Summary']) { $Script:Reporting[$T]['Summary'] = Invoke-Command -ScriptBlock $Script:GraphEssentialsConfiguration[$T]['Summary'] } & $Script:GraphEssentialsConfiguration[$T]['Solution'] } } } else { foreach ($T in $Script:GraphEssentialsConfiguration.Keys) { if ($Script:GraphEssentialsConfiguration[$T].Enabled -eq $true) { if ($Script:GraphEssentialsConfiguration[$T]['Summary']) { $Script:Reporting[$T]['Summary'] = Invoke-Command -ScriptBlock $Script:GraphEssentialsConfiguration[$T]['Summary'] } New-HTMLTab -Name $Script:GraphEssentialsConfiguration[$T]['Name'] { & $Script:GraphEssentialsConfiguration[$T]['Solution'] } } } } } -Online:$Online.IsPresent -ShowHTML:(-not $HideHTML) -FilePath $FilePath } function New-HTMLReportGraphEssentialsWithSplit { [cmdletBinding()] param( [Array] $Type, [switch] $Online, [switch] $HideHTML, [string] $FilePath, [string] $CurrentReport ) # Split reports into multiple files for easier viewing $DateName = $(Get-Date -f yyyy-MM-dd_HHmmss) $FileName = [io.path]::GetFileNameWithoutExtension($FilePath) $DirectoryName = [io.path]::GetDirectoryName($FilePath) foreach ($T in $Script:GraphEssentialsConfiguration.Keys) { if ($Script:GraphEssentialsConfiguration[$T].Enabled -eq $true -and ((-not $CurrentReport) -or ($CurrentReport -and $CurrentReport -eq $T))) { $NewFileName = $FileName + '_' + $T + "_" + $DateName + '.html' $FilePath = [io.path]::Combine($DirectoryName, $NewFileName) New-HTML -Author 'PrzemysÅ‚aw KÅ‚ys @ Evotec' -TitleText "GraphEssentials $CurrentReport Report" { New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow New-HTMLPanelStyle -BorderRadius 0px New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ', ' -ArrayJoin New-HTMLHeader { New-HTMLSection -Invisible { New-HTMLSection { New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue } -JustifyContent flex-start -Invisible New-HTMLSection { New-HTMLText -Text "GraphEssentials - $($Script:Reporting['Version'])" -Color Blue } -JustifyContent flex-end -Invisible } } if ($Script:GraphEssentialsConfiguration[$T]['Summary']) { $Script:Reporting[$T]['Summary'] = Invoke-Command -ScriptBlock $Script:GraphEssentialsConfiguration[$T]['Summary'] } & $Script:GraphEssentialsConfiguration[$T]['Solution'] } -Online:$Online.IsPresent -ShowHTML:(-not $HideHTML) -FilePath $FilePath } } } function New-HTMLReportGraphUsage { [cmdletBinding()] param( [System.Collections.IDictionary] $Reports, [switch] $Online, [switch] $HideHTML, [string] $FilePath ) New-HTML -Author 'PrzemysÅ‚aw KÅ‚ys @ Evotec' -TitleText 'GraphEssentials Usage Report' { New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow New-HTMLPanelStyle -BorderRadius 0px New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ', ' -ArrayJoin New-HTMLHeader { New-HTMLSection -Invisible { New-HTMLSection { New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue } -JustifyContent flex-start -Invisible New-HTMLSection { New-HTMLText -Text "GraphEssentials - $($Script:Reporting['Version'])" -Color Blue } -JustifyContent flex-end -Invisible } } if ($Reports.Count -eq 1) { foreach ($T in $Reports.Keys) { New-HTMLTable -DataTable $Reports[$T] -Filtering { } -ScrollX } } else { foreach ($T in $Reports.Keys) { $Name = Format-AddSpaceToSentence -Text $T if ($Reports[$T].Count -gt 0) { $Name = "$Name 💚" } else { $Name = "$Name 💔" } New-HTMLTab -Name $Name { New-HTMLTable -DataTable $Reports[$T] -Filtering { } -ScrollX } } } } -Online:$Online.IsPresent -ShowHTML:(-not $HideHTML) -FilePath $FilePath } function Reset-GraphEssentials { [cmdletBinding()] param( ) if (-not $Script:DefaultTypes) { $Script:DefaultTypes = foreach ($T in $Script:GraphEssentialsConfiguration.Keys) { if ($Script:GraphEssentialsConfiguration[$T].Enabled) { $T } } } else { foreach ($T in $Script:GraphEssentialsConfiguration.Keys) { if ($Script:GraphEssentialsConfiguration[$T]) { $Script:GraphEssentialsConfiguration[$T]['Enabled'] = $false } } foreach ($T in $Script:DefaultTypes) { if ($Script:GraphEssentialsConfiguration[$T]) { $Script:GraphEssentialsConfiguration[$T]['Enabled'] = $true } } } } $Script:GraphEssentialsConfiguration = [ordered] @{ Apps = $Script:Apps AppsCredentials = $Script:AppsCredentials Devices = $Script:Devices DevicesIntune = $Script:DevicesIntune Licenses = $Script:Licenses Roles = $Script:Roles RolesUsers = $Script:RolesUsers RolesUsersPerColumn = $Script:RolesUsersPerColumn Users = $Script:Users UsersPerLicense = $Script:UsersPerLicense UsersPerServicePlan = $Script:UsersPerServicePlan Teams = $Script:Teams } function Get-MgToken { <# .SYNOPSIS Provides a way to get a token for Microsoft Graph API to be used with Connect-MGGraph .DESCRIPTION Provides a way to get a token for Microsoft Graph API to be used with Connect-MGGraph .PARAMETER ClientID Provide the Application ID of the App Registration .PARAMETER ClientSecret Provide the Client Secret of the App Registration .PARAMETER Credential Provide the Client Secret of the App Registration as a PSCredential .PARAMETER TenantID Provide the Tenant ID of the App Registration .PARAMETER Domain Provide the Domain of the tenant where the App is registred .EXAMPLE Get-MgToken -ClientID 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' -ClientSecret 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' -TenantID 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' .EXAMPLE Get-MgToken -ClientID 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' -ClientSecret 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' -Domain 'contoso.com' .EXAMPLE $ClientSecretEncrypted = 'ClientSecretToEncrypt' | ConvertTo-SecureString -AsPlainText | ConvertFrom-SecureString $AccessToken = Get-MgToken -Domain 'evotec.pl' -ClientID 'ClientID' -ClientSecretEncrypted $ClientSecretEncrypted Connect-MgGraph -AccessToken $AccessToken .NOTES General notes #> [CmdletBinding(DefaultParameterSetName = 'Domain')] param( [Parameter(ParameterSetName = 'TenantID', Mandatory)] [Parameter(ParameterSetName = 'Domain', Mandatory)] [Parameter(ParameterSetName = 'TenantIDEncrypted', Mandatory)] [Parameter(ParameterSetName = 'DomainEncrypted', Mandatory)] [alias('ApplicationID')][string] $ClientID, [Parameter(ParameterSetName = 'TenantID', Mandatory)] [Parameter(ParameterSetName = 'Domain', Mandatory)] [string] $ClientSecret, [Parameter(ParameterSetName = 'TenantIDEncrypted', Mandatory)] [Parameter(ParameterSetName = 'DomainEncrypted', Mandatory)] [string] $ClientSecretEncrypted, [Parameter(ParameterSetName = 'TenantIDEncrypted', Mandatory)] [Parameter(ParameterSetName = 'TenantID', Mandatory)][string] $TenantID, [Parameter(ParameterSetName = 'DomainEncrypted', Mandatory)] [Parameter(ParameterSetName = 'Domain', Mandatory)][string] $Domain, [Parameter(ParameterSetName = 'TenantID')] [Parameter(ParameterSetName = 'Domain')] [Parameter(ParameterSetName = 'TenantIDEncrypted')] [Parameter(ParameterSetName = 'DomainEncrypted')] [string] $Proxy, [Parameter(ParameterSetName = 'TenantID')] [Parameter(ParameterSetName = 'Domain')] [Parameter(ParameterSetName = 'TenantIDEncrypted')] [Parameter(ParameterSetName = 'DomainEncrypted')] [PSCredential] $ProxyCredential, [Parameter(ParameterSetName = 'TenantID')] [Parameter(ParameterSetName = 'Domain')] [Parameter(ParameterSetName = 'TenantIDEncrypted')] [Parameter(ParameterSetName = 'DomainEncrypted')] [switch] $ProxyUseDefaultCredentials ) if ($PSBoundParameters.ContainsKey('ClientSecretEncrypted')) { $TemporaryKey = ConvertTo-SecureString -String $ClientSecretEncrypted -Force $ApplicationKey = [System.Net.NetworkCredential]::new([string]::Empty, $TemporaryKey).Password } else { $ApplicationKey = $ClientSecret } $Body = @{ Grant_Type = "client_credentials" Scope = "https://graph.microsoft.com/.default" Client_Id = $ClientID Client_Secret = $ApplicationKey } if ($TenantID) { $Tenant = $TenantID } elseif ($Domain) { $Tenant = Get-O365TenantID -Domain $Domain } if (-not $Tenant) { throw "Get-MgToken - Unable to get Tenant ID" } $invokeRestMethodSplat = @{ Uri = "https://login.microsoftonline.com/$Tenant/oauth2/v2.0/token" Method = 'POST' Body = $Body } if ($PSBoundParameters.ContainsKey('Proxy')) { $invokeRestMethodSplat.Proxy = $Proxy } if ($PSBoundParameters.ContainsKey('ProxyCredential')) { $invokeRestMethodSplat.ProxyCredential = $ProxyCredential } if ($PSBoundParameters.ContainsKey('ProxyUseDefaultCredentials')) { $invokeRestMethodSplat.ProxyUseDefaultCredentials = $ProxyUseDefaultCredentials } $connection = Invoke-RestMethod @invokeRestMethodSplat $connection.access_token } function Get-MyApp { [cmdletBinding()] param( [string] $ApplicationName, [switch] $IncludeCredentials ) if ($ApplicationName) { $Application = Get-MgApplication -Filter "displayName eq '$ApplicationName'" -All -ConsistencyLevel eventual } else { $Application = Get-MgApplication -ConsistencyLevel eventual -All } $Applications = foreach ($App in $Application) { [Array] $DatesSorted = $App.PasswordCredentials.StartDateTime | Sort-Object # Lets translate credentials to different format $AppCredentials = Get-MyAppCredentials -ApplicationList $App # Lets find if description has email $DescriptionWithEmail = $false foreach ($CredentialName in $AppCredentials.ClientSecretName) { if ($CredentialName -like '*@*') { $DescriptionWithEmail = $true break } } $DaysToExpireOldest = $AppCredentials.DaysToExpire | Sort-Object -Descending | Select-Object -Last 1 $DaysToExpireNewest = $AppCredentials.DaysToExpire | Sort-Object -Descending | Select-Object -First 1 if ($AppCredentials.Expired -contains $false) { $Expired = 'No' } elseif ($AppCredentials.Expired -contains $true) { $Expired = 'Yes' } else { $Expired = 'Not available' } $AppInformation = [ordered] @{ ObjectId = $App.Id ClientID = $App.AppId ApplicationName = $App.DisplayName CreatedDate = $App.CreatedDateTime KeysCount = $App.PasswordCredentials.Count KeysExpired = $Expired DaysToExpireOldest = $DaysToExpireOldest DaysToExpireNewest = $DaysToExpireNewest KeysDateOldest = if ($DatesSorted.Count -gt 0) { $DatesSorted[0] } else { } KeysDateNewest = if ($DatesSorted.Count -gt 0) { $DatesSorted[-1] } else { } KeysDescription = $AppCredentials.ClientSecretName DescriptionWithEmail = $DescriptionWithEmail } if ($IncludeCredentials) { $AppInformation['Keys'] = $AppCredentials } [PSCustomObject] $AppInformation } $Applications } function Get-MyAppCredentials { [cmdletBinding()] param( [string] $ApplicationName, [int] $LessThanDaysToExpire, [int] $GreaterThanDaysToExpire, [switch] $Expired, [alias('DescriptionCredentials', 'ClientSecretName')][string] $DisplayNameCredentials, [Parameter(DontShow)][Array] $ApplicationList ) if (-not $ApplicationList) { if ($ApplicationName) { $ApplicationList = Get-MgApplication -Filter "displayName eq '$ApplicationName'" -All -ConsistencyLevel eventual } else { $ApplicationList = Get-MgApplication -All } } else { $ApplicationList = foreach ($App in $ApplicationList) { if ($PSBoundParameters.ContainsKey('ApplicationName')) { if ($App.DisplayName -eq $ApplicationName) { $App } } else { $App } } } $ApplicationsWithCredentials = foreach ($App in $ApplicationList) { if ($App.PasswordCredentials) { foreach ($Credentials in $App.PasswordCredentials) { if ($Credentials.EndDateTime -lt [DateTime]::Now) { $IsExpired = $true } else { $IsExpired = $false } if ($null -ne $Credentials.DisplayName) { $DisplayName = $Credentials.DisplayName } elseif ($null -ne $Credentials.CustomKeyIdentifier) { if ($Credentials.CustomKeyIdentifier[0] -eq 255 -and $Credentials.CustomKeyIdentifier[1] -eq 254 -and $Credentials.CustomKeyIdentifier[0] -ne 0 -and $Credentials.CustomKeyIdentifier[0] -ne 0) { $DisplayName = [System.Text.Encoding]::Unicode.GetString($Credentials.CustomKeyIdentifier) } elseif ($Credentials.CustomKeyIdentifier[0] -eq 255 -and $Credentials.CustomKeyIdentifier[1] -eq 254 -and $Credentials.CustomKeyIdentifier[0] -eq 0 -and $Credentials.CustomKeyIdentifier[0] -eq 0) { $DisplayName = [System.Text.Encoding]::UTF32.GetString($Credentials.CustomKeyIdentifier) } elseif ($Credentials.CustomKeyIdentifier[1] -eq 0 -and $Credentials.CustomKeyIdentifier[3] -eq 0) { $DisplayName = [System.Text.Encoding]::Unicode.GetString($Credentials.CustomKeyIdentifier) } else { $DisplayName = [System.Text.Encoding]::UTF8.GetString($Credentials.CustomKeyIdentifier) } } else { $DisplayName = $Null } $Creds = [PSCustomObject] @{ ObjectId = $App.Id ApplicationName = $App.DisplayName ClientID = $App.AppId CreatedDate = $App.CreatedDateTime ClientSecretName = $DisplayName ClientSecretId = $Credentials.KeyId #ClientSecret = $Credentials.SecretTex ClientSecretHint = $Credentials.Hint Expired = $IsExpired DaysToExpire = ($Credentials.EndDateTime - [DateTime]::Now).Days StartDateTime = $Credentials.StartDateTime EndDateTime = $Credentials.EndDateTime #CustomKeyIdentifier = $Credentials.CustomKeyIdentifier } if ($PSBoundParameters.ContainsKey('DisplayNameCredentials')) { if ($Creds.ClientSecretName -notlike $DisplayNameCredentials) { continue } } if ($PSBoundParameters.ContainsKey('LessThanDaysToExpire')) { if ($LessThanDaysToExpire -ge $Creds.DaysToExpire) { #$Creds } else { continue } } elseif ($PSBoundParameters.ContainsKey('Expired')) { if ($Creds.Expired -eq $true) { } else { continue } } elseif ($PSBoundParameters.ContainsKey('GreaterThanDaysToExpire')) { if ($GreaterThanDaysToExpire -le $Creds.DaysToExpire) { #$Creds } else { continue } } $Creds } } } $ApplicationsWithCredentials } function Get-MyDevice { [cmdletBinding()] param( [ValidateSet('Hybrid AzureAD', 'AzureAD joined', 'AzureAD registered', 'Not available')][string[]] $Type, [switch] $Synchronized ) $TrustTypes = @{ 'ServerAD' = 'Hybrid AzureAD' 'AzureAD' = 'AzureAD joined' 'Workplace' = 'AzureAD registered' } $Today = Get-Date try { $Devices = Get-MgDevice -All -ExpandProperty RegisteredOwners -ErrorAction Stop } catch { Write-Warning -Message "Get-MyDevice - Failed to get devices. Error: $($_.Exception.Message)" return } foreach ($Device in $Devices) { if ($Device.ApproximateLastSignInDateTime) { $LastSeenDays = $( - $($Device.ApproximateLastSignInDateTime - $Today).Days) } else { $LastSeenDays = $null } if ($Device.OnPremisesLastSyncDateTime) { $LastSynchronizedDays = $( - $($Device.OnPremisesLastSyncDateTime - $Today).Days) } else { $LastSynchronizedDays = $null } if ($Device.TrustType) { $TrustType = $TrustTypes[$Device.TrustType] } else { $TrustType = 'Not available' } if ($Synchronized) { # Only return synchronized devices if (-not $Device.OnPremisesSyncEnabled) { continue } } if ($Type) { # Only return devices of the specified type if ($Type -notcontains $TrustType) { continue } } [PSCustomObject] @{ Name = $Device.DisplayName Id = $Device.Id Enabled = $Device.AccountEnabled OperatingSystem = $Device.OperatingSystem OperatingSystemVersion = $Device.OperatingSystemVersion TrustType = $TrustType ProfileType = $Device.ProfileType FirstSeen = $Device.AdditionalProperties.registrationDateTime LastSeen = $Device.ApproximateLastSignInDateTime LastSeenDays = $LastSeenDays Status = $Device.AdditionalProperties.deviceOwnership OwnerDisplayName = $Device.RegisteredOwners.AdditionalProperties.displayName OwnerEnabled = $Device.RegisteredOwners.AdditionalProperties.accountEnabled OwnerUserPrincipalName = $Device.RegisteredOwners.AdditionalProperties.userPrincipalName IsSynchronized = if ($Device.OnPremisesSyncEnabled) { $true } else { $false } LastSynchronized = $Device.OnPremisesLastSyncDateTime LastSynchronizedDays = $LastSynchronizedDays IsCompliant = $Device.IsCompliant IsManaged = $Device.IsManaged DeviceId = $Device.DeviceId Model = $Device.AdditionalProperties.model Manufacturer = $Device.AdditionalProperties.manufacturer ManagementType = $Device.AdditionalProperties.managementType EnrollmentType = $Device.AdditionalProperties.enrollmentType } } } function Get-MyDeviceIntune { [cmdletBinding()] param( ) $Today = Get-Date try { $DevicesIntune = Get-MgDeviceManagementManagedDevice -All -ErrorAction Stop } catch { Write-Warning -Message "Get-MyDeviceIntune - Failed to get intune devices. Error: $($_.Exception.Message)" return } foreach ($DeviceI in $DevicesIntune) { if ($DeviceI.LastSyncDateTime) { $LastSynchronizedDays = $( - $($DeviceI.LastSyncDateTime - $Today).Days) } else { $LastSynchronizedDays = $null } $DeviceInformation = [ordered] @{ Name = $DeviceI.DeviceName # : EVOMONSTER Id = $DeviceI.Id # : 83fe122f-c51c-49dc-a0f3-cc11d9e7d045 ComplianceState = $DeviceI.ComplianceState # : compliant OperatingSystem = $DeviceI.OperatingSystem # : Windows OperatingSystemVersion = $DeviceI.OSVersion # : 10.0.22621.1555 FirstSeen = $DeviceI.EnrolledDateTime # : 2023-01-28 10:34:18 LastSeen = $DeviceI.LastSyncDateTime # : 2023-04-14 04:52:42 LastSeenDays = $LastSynchronizedDays UserDisplayName = $DeviceI.UserDisplayName # : PrzemysÅ‚aw KÅ‚ys UserId = $DeviceI.UserId # : e6a8f1cf-0874-4323-a12f-2bf51bb6dfdd UserPrincipalName = $DeviceI.UserPrincipalName # : przemyslaw.klys@evotec.pl EmailAddress = $DeviceI.EmailAddress # : przemyslaw.klys@evotec.pl ManagedDeviceName = $DeviceI.ManagedDeviceName # : przemyslaw.klys_Windows_1/28/2023_10:34 AM ManagedDeviceOwnerType = $DeviceI.ManagedDeviceOwnerType # : company ManagementAgent = $DeviceI.ManagementAgent # : mdm ManagementCertificateExpirationDate = $DeviceI.ManagementCertificateExpirationDate # : 2024-01-27 17:58:15 ActivationLockBypassCode = $DeviceI.ActivationLockBypassCode # : AndroidSecurityPatchLevel = $DeviceI.AndroidSecurityPatchLevel # : AzureAdDeviceId = $DeviceI.AzureAdDeviceId # : aee87706-674b-40be-8120-74e7c469329b AzureAdRegistered = $DeviceI.AzureAdRegistered # : True ComplianceGracePeriodExpirationDateTime = $DeviceI.ComplianceGracePeriodExpirationDateTime # : 9999-12-31 23:59:59 #ConfigurationManagerClientEnabledFeatures = $DeviceI.ConfigurationManagerClientEnabledFeatures # : Microsoft.Graph.PowerShell.Models.MicrosoftGraphConfigurationManagerClientEnabledFeatures DeviceActionResults = $DeviceI.DeviceActionResults # : {} #DeviceCategory = $DeviceI.DeviceCategory # : Microsoft.Graph.PowerShell.Models.MicrosoftGraphDeviceCategory DeviceCategoryName = $DeviceI.DeviceCategoryDisplayName # : Unknown DeviceCompliancePolicyStates = $DeviceI.DeviceCompliancePolicyStates # : DeviceConfigurationStates = $DeviceI.DeviceConfigurationStates # : DeviceEnrollmentType = $DeviceI.DeviceEnrollmentType # : windowsCoManagement #DeviceHealthAttestationState = $DeviceI.DeviceHealthAttestationState # : Microsoft.Graph.PowerShell.Models.MicrosoftGraphDeviceHealthAttestationState DeviceRegistrationState = $DeviceI.DeviceRegistrationState # : registered EasActivated = $DeviceI.EasActivated # : True EasActivationDateTime = $DeviceI.EasActivationDateTime # : 0001-01-01 00:00:00 EasDeviceId = $DeviceI.EasDeviceId # : E88398D87BD859566D129F86E2FD722C EthernetMacAddress = $DeviceI.EthernetMacAddress # : ExchangeAccessState = $DeviceI.ExchangeAccessState # : none ExchangeAccessStateReason = $DeviceI.ExchangeAccessStateReason # : none ExchangeLastSuccessfulSyncDateTime = $DeviceI.ExchangeLastSuccessfulSyncDateTime # : 0001-01-01 00:00:00 FreeStorageSpaceInBytes = $DeviceI.FreeStorageSpaceInBytes # : 1392111517696 Iccid = $DeviceI.Iccid # : Imei = $DeviceI.Imei # : IsEncrypted = $DeviceI.IsEncrypted # : True IsSupervised = $DeviceI.IsSupervised # : False IsJailBroken = $DeviceI.JailBroken # : Unknown Manufacturer = $DeviceI.Manufacturer # : ASUS Meid = $DeviceI.Meid # : Model = $DeviceI.Model # : System Product Name Notes = $DeviceI.Notes # : PartnerReportedThreatState = $DeviceI.PartnerReportedThreatState # : unknown PhoneNumber = $DeviceI.PhoneNumber # : PhysicalMemoryInBytes = $DeviceI.PhysicalMemoryInBytes # : 0 SerialNumber = $DeviceI.SerialNumber # : SystemSerialNumber SubscriberCarrier = $DeviceI.SubscriberCarrier # : TotalStorageSpaceInBytes = $DeviceI.TotalStorageSpaceInBytes # : 1999609266176 Udid = $DeviceI.Udid # : Users = $DeviceI.Users # : WiFiMacAddress = $DeviceI.WiFiMacAddress # : 8C1D96F0937B RemoteAssistanceSessionErrorDetails = $DeviceI.RemoteAssistanceSessionErrorDetails # : RemoteAssistanceSessionUrl = $DeviceI.RemoteAssistanceSessionUrl # : RequireUserEnrollmentApproval = $DeviceI.RequireUserEnrollmentApproval # : #AdditionalProperties = $DeviceI.AdditionalProperties # : {} } foreach ($D in $DeviceI.ConfigurationManagerClientEnabledFeatures.PSObject.Properties) { if ($D.Name -notin 'AdditionalProperties') { $DeviceInformation.Add("ConfigurationManagerClientEnabledFeatures$($D.Name)", $D.Value) } } foreach ($D in $DeviceI.DeviceCategory.PSObject.Properties) { if ($D.Name -notin 'AdditionalProperties') { $DeviceInformation.Add("DeviceCategory$($D.Name)", $D.Value) } } foreach ($D in $DeviceI.DeviceHealthAttestationState.PSObject.Properties) { if ($D.Name -notin 'AdditionalProperties') { $DeviceInformation.Add("DeviceHealthAttestationState$($D.Name)", $D.Value) } } [PSCustomObject] $DeviceInformation } } function Get-MyLicense { [CmdletBinding()] param( [Parameter(DontShow)][switch] $Internal ) $Skus = Get-MgSubscribedSku -All if ($Internal) { # This is used by Get-MyUser to get the list of licenses and service plans and faster search $Output = [ordered] @{ Licenses = [ordered] @{} ServicePlans = [ordered] @{} } foreach ($SKU in $Skus) { $Output['Licenses'][$Sku.SkuId] = Convert-Office365License -License $SKU.SkuPartNumber foreach ($Plan in $Sku.ServicePlans) { $Output['ServicePlans'][$Plan.ServicePlanId] = Convert-Office365License -License $Plan.ServicePlanName } } $Output } else { foreach ($SKU in $Skus) { if ($SKU.PrepaidUnits.Enabled -gt 0) { $LicensesUsedPercent = [math]::Round(($SKU.ConsumedUnits / $SKU.PrepaidUnits.Enabled) * 100, 0) } else { if ($SKU.ConsumedUnits -gt 0) { $LicensesUsedPercent = [math]::Round($SKU.ConsumedUnits * 100, 0) } else { $LicensesUsedPercent = 0 } } [PSCustomObject] @{ Name = Convert-Office365License -License $SKU.SkuPartNumber SkuId = $SKU.SkuId # : 26124093 - 3d78-432b-b5dc-48bf992543d5 SkuPartNumber = $SKU.SkuPartNumber # : IDENTITY_THREAT_PROTECTION AppliesTo = $SKU.AppliesTo # : User CapabilityStatus = $SKU.CapabilityStatus # : Enabled LicensesUsedPercent = $LicensesUsedPercent LicensesUsedCount = $SKU.ConsumedUnits # : 1 #Id = $SKU.Id # : ceb371f6 - 8745 - 4876-a040 - 69f2d10a9d1a_26124093-3d78-432b-b5dc-48bf992543d5 LicenseCountEnabled = $SKU.PrepaidUnits.Enabled LicenseCountWarning = $SKU.PrepaidUnits.Warning LicenseCountSuspended = $SKU.PrepaidUnits.Suspended #ServicePlans = $SKU.ServicePlans # : { MTP, SAFEDOCS, WINDEFATP, THREAT_INTELLIGENCE… } ServicePlansCount = $SKU.ServicePlans.Count # : 5 ServicePlans = $SKU.ServicePlans | ForEach-Object { Convert-Office365License -License $_.ServicePlanName } #AdditionalProperties = $SKU.AdditionalProperties # : {} } } } } function Get-MyRole { [CmdletBinding()] param( [switch] $OnlyWithMembers ) # $Users = Get-MgUser -All # #$Apps = Get-MgApplication -All # $Groups = Get-MgGroup -All -Filter "IsAssignableToRole eq true" # $ServicePrincipals = Get-MgServicePrincipal -All # #$DirectoryRole = Get-MgDirectoryRole -All # $Roles = Get-MgRoleManagementDirectoryRoleDefinition -All # $RolesAssignement = Get-MgRoleManagementDirectoryRoleAssignment -All #-ExpandProperty "principal" # $EligibilityAssignement = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -All $ErrorsCount = 0 try { $Users = Get-MgUser -ErrorAction Stop -All -Property DisplayName, CreatedDateTime, 'AccountEnabled', 'Mail', 'UserPrincipalName', 'Id', 'UserType', 'OnPremisesDistinguishedName', 'OnPremisesSamAccountName', 'OnPremisesLastSyncDateTime', 'OnPremisesSyncEnabled', 'OnPremisesUserPrincipalName' } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get users. Error: $($_.Exception.Message)" $ErrorsCount++ } try { $Groups = Get-MgGroup -ErrorAction Stop -All -Filter "IsAssignableToRole eq true" -Property CreatedDateTime, Id, DisplayName, Mail, OnPremisesLastSyncDateTime, SecurityEnabled } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get groups. Error: $($_.Exception.Message)" $ErrorsCount++ } #$Apps = Get-MgApplication -All try { $ServicePrincipals = Get-MgServicePrincipal -ErrorAction Stop -All -Property CreatedDateTime, 'ServicePrincipalType', 'DisplayName', 'AccountEnabled', 'Id', 'AppID' } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get service principals. Error: $($_.Exception.Message)" $ErrorsCount++ } #$DirectoryRole = Get-MgDirectoryRole -All try { $Roles = Get-MgRoleManagementDirectoryRoleDefinition -ErrorAction Stop -All } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get roles. Error: $($_.Exception.Message)" $ErrorsCount++ } try { $RolesAssignement = Get-MgRoleManagementDirectoryRoleAssignment -ErrorAction Stop -All #-ExpandProperty "principal" } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get roles assignement. Error: $($_.Exception.Message)" $ErrorsCount++ } try { $EligibilityAssignement = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -ErrorAction Stop -All } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get eligibility assignement. Error: $($_.Exception.Message)" $ErrorsCount++ } if ($ErrorsCount -gt 0) { return } $CacheUsersAndApps = [ordered] @{} foreach ($User in $Users) { $CacheUsersAndApps[$User.Id] = $User } foreach ($ServicePrincipal in $ServicePrincipals) { $CacheUsersAndApps[$ServicePrincipal.Id] = $ServicePrincipal } foreach ($Group in $Groups) { $CacheUsersAndApps[$Group.Id] = $Group } $CacheRoles = [ordered] @{} foreach ($Role in $Roles) { $CacheRoles[$Role.Id] = [ordered] @{ Role = $Role Direct = [System.Collections.Generic.List[object]]::new() Eligible = [System.Collections.Generic.List[object]]::new() Users = [System.Collections.Generic.List[object]]::new() ServicePrincipals = [System.Collections.Generic.List[object]]::new() Groups = [System.Collections.Generic.List[object]]::new() } } foreach ($Role in $RolesAssignement) { if ($CacheRoles[$Role.RoleDefinitionId]) { $CacheRoles[$Role.RoleDefinitionId].Direct.Add($CacheUsersAndApps[$Role.PrincipalId]) if ($CacheUsersAndApps[$Role.PrincipalId].GetType().Name -eq 'MicrosoftGraphUser') { $CacheRoles[$Role.RoleDefinitionId].Users.Add($CacheUsersAndApps[$Role.PrincipalId]) } elseif ($CacheUsersAndApps[$Role.PrincipalId].GetType().Name -eq 'MicrosoftGraphGroup') { $CacheRoles[$Role.RoleDefinitionId].Groups.Add($CacheUsersAndApps[$Role.PrincipalId]) } elseif ($CacheUsersAndApps[$Role.PrincipalId].GetType().Name -eq 'MicrosoftGraphServicePrincipal') { $CacheRoles[$Role.RoleDefinitionId].ServicePrincipals.Add($CacheUsersAndApps[$Role.PrincipalId]) } else { Write-Warning -Message "Unknown type for principal id $($Role.PrincipalId) - not supported yet!" } # MicrosoftGraphServicePrincipal, MicrosoftGraphUser,MicrosoftGraphGroup } else { try { $TemporaryRole = Get-MgRoleManagementDirectoryRoleDefinition -UnifiedRoleDefinitionId $Role.RoleDefinitionId -ErrorAction Stop } catch { Write-Warning -Message "Role $($Role.RoleDefinitionId) was not found. Using direct query failed." } if ($TemporaryRole) { Write-Verbose -Message "Role $($Role.RoleDefinitionId) was not found. Using direct query revealed $($TemporaryRole.DisplayName)." $CacheRoles[$Role.RoleDefinitionId] = [ordered] @{ Role = $TemporaryRole Direct = [System.Collections.Generic.List[object]]::new() Eligible = [System.Collections.Generic.List[object]]::new() Users = [System.Collections.Generic.List[object]]::new() ServicePrincipals = [System.Collections.Generic.List[object]]::new() Groups = [System.Collections.Generic.List[object]]::new() } $CacheRoles[$Role.RoleDefinitionId].Direct.Add($CacheUsersAndApps[$Role.PrincipalId]) if ($CacheUsersAndApps[$Role.PrincipalId].GetType().Name -eq 'MicrosoftGraphUser') { $CacheRoles[$Role.RoleDefinitionId].Users.Add($CacheUsersAndApps[$Role.PrincipalId]) } elseif ($CacheUsersAndApps[$Role.PrincipalId].GetType().Name -eq 'MicrosoftGraphGroup') { $CacheRoles[$Role.RoleDefinitionId].Groups.Add($CacheUsersAndApps[$Role.PrincipalId]) } elseif ($CacheUsersAndApps[$Role.PrincipalId].GetType().Name -eq 'MicrosoftGraphServicePrincipal') { $CacheRoles[$Role.RoleDefinitionId].ServicePrincipals.Add($CacheUsersAndApps[$Role.PrincipalId]) } else { Write-Warning -Message "Unknown type for principal id $($Role.PrincipalId) - not supported yet!" } } } } foreach ($Role in $EligibilityAssignement) { if ($CacheRoles[$Role.RoleDefinitionId]) { $CacheRoles[$Role.RoleDefinitionId].Eligible.Add($CacheUsersAndApps[$Role.PrincipalId]) if ($CacheUsersAndApps[$Role.PrincipalId].GetType().Name -eq 'MicrosoftGraphUser') { $CacheRoles[$Role.RoleDefinitionId].Users.Add($CacheUsersAndApps[$Role.PrincipalId]) } elseif ($CacheUsersAndApps[$Role.PrincipalId].GetType().Name -eq 'MicrosoftGraphGroup') { $CacheRoles[$Role.RoleDefinitionId].Groups.Add($CacheUsersAndApps[$Role.PrincipalId]) } elseif ($CacheUsersAndApps[$Role.PrincipalId].GetType().Name -eq 'MicrosoftGraphServicePrincipal') { $CacheRoles[$Role.RoleDefinitionId].ServicePrincipals.Add($CacheUsersAndApps[$Role.PrincipalId]) } else { Write-Warning -Message "Unknown type for principal id $($Role.PrincipalId) - not supported yet!" } } else { Write-Warning -Message $Role } } # lets get group members of groups we have members in and roles are there too $CacheGroupMembers = [ordered] @{} foreach ($Role in $CacheRoles.Keys) { if ($CacheRoles[$Role].Groups.Count -gt 0) { foreach ($Group in $CacheRoles[$Role].Groups) { if (-not $CacheGroupMembers[$Group.DisplayName]) { $CacheGroupMembers[$Group.DisplayName] = [System.Collections.Generic.List[object]]::new() $GroupMembers = Get-MgGroupMember -GroupId $Group.Id -All #-ErrorAction Stop foreach ($GroupMember in $GroupMembers) { $CacheGroupMembers[$Group.DisplayName].Add($CacheUsersAndApps[$GroupMember.Id]) } } } } } foreach ($Role in $CacheRoles.Keys) { if ($OnlyWithMembers) { if ($CacheRoles[$Role].Direct.Count -eq 0 -and $CacheRoles[$Role].Eligible.Count -eq 0) { continue } } $GroupMembersTotal = 0 foreach ($Group in $CacheRoles[$Role].Groups) { $GroupMembersTotal = + $CacheGroupMembers[$Group.DisplayName].Count } [PSCustomObject] @{ Name = $CacheRoles[$Role].Role.DisplayName Description = $CacheRoles[$Role].Role.Description IsBuiltin = $CacheRoles[$Role].Role.IsBuiltIn IsEnabled = $CacheRoles[$Role].Role.IsEnabled AllowedResourceActions = $CacheRoles[$Role].Role.RolePermissions[0].AllowedResourceActions.Count TotalMembers = $CacheRoles[$Role].Direct.Count + $CacheRoles[$Role].Eligible.Count + $GroupMembersTotal DirectMembers = $CacheRoles[$Role].Direct.Count EligibleMembers = $CacheRoles[$Role].Eligible.Count GroupsMembers = $GroupMembersTotal # here's a split by numbers Users = $CacheRoles[$Role].Users.Count ServicePrincipals = $CacheRoles[$Role].ServicePrincipals.Count Groups = $CacheRoles[$Role].Groups.Count } } } function Get-MyRoleUsers { [CmdletBinding()] param( [switch] $OnlyWithRoles, [switch] $RolePerColumn ) $ErrorsCount = 0 try { $Users = Get-MgUser -ErrorAction Stop -All -Property DisplayName, CreatedDateTime, 'AccountEnabled', 'Mail', 'UserPrincipalName', 'Id', 'UserType', 'OnPremisesDistinguishedName', 'OnPremisesSamAccountName', 'OnPremisesLastSyncDateTime', 'OnPremisesSyncEnabled', 'OnPremisesUserPrincipalName' } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get users. Error: $($_.Exception.Message)" $ErrorsCount++ } try { $Groups = Get-MgGroup -ErrorAction Stop -All -Filter "IsAssignableToRole eq true" -Property CreatedDateTime, Id, DisplayName, Mail, OnPremisesLastSyncDateTime, SecurityEnabled } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get groups. Error: $($_.Exception.Message)" $ErrorsCount++ } #$Apps = Get-MgApplication -All try { $ServicePrincipals = Get-MgServicePrincipal -ErrorAction Stop -All -Property CreatedDateTime, 'ServicePrincipalType', 'DisplayName', 'AccountEnabled', 'Id', 'AppID' } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get service principals. Error: $($_.Exception.Message)" $ErrorsCount++ } #$DirectoryRole = Get-MgDirectoryRole -All try { $Roles = Get-MgRoleManagementDirectoryRoleDefinition -ErrorAction Stop -All } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get roles. Error: $($_.Exception.Message)" $ErrorsCount++ } try { $RolesAssignement = Get-MgRoleManagementDirectoryRoleAssignment -ErrorAction Stop -All #-ExpandProperty "principal" } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get roles assignement. Error: $($_.Exception.Message)" $ErrorsCount++ } try { $EligibilityAssignement = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -ErrorAction Stop -All } catch { Write-Warning -Message "Get-MyRoleUsers - Failed to get eligibility assignement. Error: $($_.Exception.Message)" $ErrorsCount++ } if ($ErrorsCount -gt 0) { return } $CacheUsersAndApps = [ordered] @{} foreach ($User in $Users) { $CacheUsersAndApps[$User.Id] = @{ Identity = $User Direct = [System.Collections.Generic.List[object]]::new() Eligible = [System.Collections.Generic.List[object]]::new() } } foreach ($ServicePrincipal in $ServicePrincipals) { $CacheUsersAndApps[$ServicePrincipal.Id] = @{ Identity = $ServicePrincipal Direct = [System.Collections.Generic.List[object]]::new() Eligible = [System.Collections.Generic.List[object]]::new() } } foreach ($Group in $Groups) { $CacheUsersAndApps[$Group.Id] = @{ Identity = $Group Direct = [System.Collections.Generic.List[object]]::new() Eligible = [System.Collections.Generic.List[object]]::new() } } $CacheRoles = [ordered] @{} foreach ($Role in $Roles) { $CacheRoles[$Role.Id] = [ordered] @{ Role = $Role Members = [System.Collections.Generic.List[object]]::new() Users = [System.Collections.Generic.List[object]]::new() ServicePrincipals = [System.Collections.Generic.List[object]]::new() GroupsDirect = [System.Collections.Generic.List[object]]::new() GroupsEligible = [System.Collections.Generic.List[object]]::new() } } foreach ($Role in $RolesAssignement) { if ($CacheRoles[$Role.RoleDefinitionId]) { $CacheUsersAndApps[$Role.PrincipalId].Direct.Add($CacheRoles[$Role.RoleDefinitionId].Role) if ($CacheUsersAndApps[$Role.PrincipalId].Identity.GetType().Name -eq 'MicrosoftGraphGroup') { $CacheRoles[$Role.RoleDefinitionId].GroupsDirect.Add($CacheUsersAndApps[$Role.PrincipalId].Identity) } } else { try { $TemporaryRole = Get-MgRoleManagementDirectoryRoleDefinition -UnifiedRoleDefinitionId $Role.RoleDefinitionId -ErrorAction Stop } catch { Write-Warning -Message "Role $($Role.RoleDefinitionId) was not found. Using direct query failed." } if ($TemporaryRole) { Write-Verbose -Message "Role $($Role.RoleDefinitionId) was not found. Using direct query revealed $($TemporaryRole.DisplayName)." if (-not $CacheRoles[$Role.RoleDefinitionId]) { $CacheRoles[$Role.RoleDefinitionId] = [ordered] @{ Role = $TemporaryRole Direct = [System.Collections.Generic.List[object]]::new() Eligible = [System.Collections.Generic.List[object]]::new() Users = [System.Collections.Generic.List[object]]::new() ServicePrincipals = [System.Collections.Generic.List[object]]::new() } } $CacheUsersAndApps[$Role.PrincipalId].Direct.Add($CacheRoles[$Role.RoleDefinitionId].Role) } } } foreach ($Role in $EligibilityAssignement) { if ($CacheRoles[$Role.RoleDefinitionId]) { $CacheUsersAndApps[$Role.PrincipalId].Eligible.Add($CacheRoles[$Role.RoleDefinitionId].Role) if ($CacheUsersAndApps[$Role.PrincipalId].Identity.GetType().Name -eq 'MicrosoftGraphGroup') { $CacheRoles[$Role.RoleDefinitionId].GroupsEligible.Add($CacheUsersAndApps[$Role.PrincipalId].Identity) } } else { Write-Warning -Message $Role } } $ListActiveRoles = foreach ($Identity in $CacheUsersAndApps.Keys) { if ($OnlyWithRoles) { if ($CacheUsersAndApps[$Identity].Direct.Count -eq 0 -and $CacheUsersAndApps[$Identity].Eligible.Count -eq 0) { continue } $CacheUsersAndApps[$Identity].Direct.DisplayName $CacheUsersAndApps[$Identity].Eligible.DisplayName } } # lets get group members of groups we have members in and roles are there too $CacheGroupMembers = [ordered] @{} $CacheUserMembers = [ordered] @{} foreach ($Role in $CacheRoles.Keys) { if ($CacheRoles[$Role].GroupsDirect.Count -gt 0) { foreach ($Group in $CacheRoles[$Role].GroupsDirect) { if (-not $CacheGroupMembers[$Group.DisplayName]) { $CacheGroupMembers[$Group.DisplayName] = [ordered] @{ Group = $Group Members = Get-MgGroupMember -GroupId $Group.Id -All } } foreach ($GroupMember in $CacheGroupMembers[$Group.DisplayName].Members) { #$CacheGroupMembers[$Group.DisplayName].Add($CacheUsersAndApps[$GroupMember.Id]) if (-not $CacheUserMembers[$GroupMember.Id]) { $CacheUserMembers[$GroupMember.Id] = [ordered] @{ Identity = $GroupMember Role = [ordered] @{} #Direct = [System.Collections.Generic.List[object]]::new() #Eligible = [System.Collections.Generic.List[object]]::new() } } #$CacheUserMembers[$GroupMember.Id].Direct.Add($Group) $RoleDisplayName = $CacheRoles[$Role].Role.DisplayName if (-not $CacheUserMembers[$GroupMember.Id]['Role'][$RoleDisplayName]) { $CacheUserMembers[$GroupMember.Id]['Role'][$RoleDisplayName] = [ordered] @{ Role = $CacheRoles[$Role].Role GroupsDirect = [System.Collections.Generic.List[object]]::new() GroupsEligible = [System.Collections.Generic.List[object]]::new() } } $CacheUserMembers[$GroupMember.Id]['Role'][$RoleDisplayName].GroupsDirect.Add($Group) } } } if ($CacheRoles[$Role].GroupsEligible.Count -gt 0) { foreach ($Group in $CacheRoles[$Role].GroupsEligible) { if (-not $CacheGroupMembers[$Group.DisplayName]) { $CacheGroupMembers[$Group.DisplayName] = [ordered] @{ Group = $Group Members = Get-MgGroupMember -GroupId $Group.Id -All } } foreach ($GroupMember in $CacheGroupMembers[$Group.DisplayName].Members) { if (-not $CacheUserMembers[$GroupMember.Id]) { $CacheUserMembers[$GroupMember.Id] = [ordered] @{ Identity = $GroupMember Role = [ordered] @{} #Direct = [System.Collections.Generic.List[object]]::new() #Eligible = [System.Collections.Generic.List[object]]::new() } } $RoleDisplayName = $CacheRoles[$Role].Role.DisplayName if (-not $CacheUserMembers[$GroupMember.Id]['Role'][$RoleDisplayName]) { $CacheUserMembers[$GroupMember.Id]['Role'][$RoleDisplayName] = [ordered] @{ Role = $CacheRoles[$Role].Role GroupsDirect = [System.Collections.Generic.List[object]]::new() GroupsEligible = [System.Collections.Generic.List[object]]::new() } } $CacheUserMembers[$GroupMember.Id]['Role'][$RoleDisplayName].GroupsEligible.Add($Group) #$CacheUserMembers[$GroupMember.Id].Eligible.Add($Group) } #} } } } foreach ($Identity in $CacheUsersAndApps.Keys) { $Type = if ($CacheUsersAndApps[$Identity].Identity.ServicePrincipalType) { $CacheUsersAndApps[$Identity].Identity.ServicePrincipalType } elseif ($CacheUsersAndApps[$Identity].Identity.UserType) { $CacheUsersAndApps[$Identity].Identity.UserType } elseif ($null -ne $CacheUsersAndApps[$Identity].Identity.SecurityEnabled) { if ($CacheUsersAndApps[$Identity].Identity.SecurityEnabled) { "SecurityGroup" } else { "DistributionGroup" } } else { "Unknown" } $IsSynced = if ($CacheUsersAndApps[$Identity].Identity.OnPremisesLastSyncDateTime) { 'Synchronized' } else { 'Online' } $CanonicalName = if ($CacheUsersAndApps[$Identity].Identity.OnPremisesDistinguishedName) { ConvertFrom-DistinguishedName -DistinguishedName $CacheUsersAndApps[$Identity].Identity.OnPremisesDistinguishedName -ToOrganizationalUnit } else { $null } if (-not $RolePerColumn) { if ($OnlyWithRoles) { if ($CacheUsersAndApps[$Identity].Direct.Count -eq 0 -and $CacheUsersAndApps[$Identity].Eligible.Count -eq 0) { continue } } [PSCustomObject] @{ Name = $CacheUsersAndApps[$Identity].Identity.DisplayName Enabled = $CacheUsersAndApps[$Identity].Identity.AccountEnabled Status = $IsSynced Type = $Type CreatedDateTime = $CacheUsersAndApps[$Identity].Identity.CreatedDateTime Mail = $CacheUsersAndApps[$Identity].Identity.Mail UserPrincipalName = $CacheUsersAndApps[$Identity].Identity.UserPrincipalName AppId = $CacheUsersAndApps[$Identity].Identity.AppID DirectCount = $CacheUsersAndApps[$Identity].Direct.Count EligibleCount = $CacheUsersAndApps[$Identity].Eligible.Count Direct = $CacheUsersAndApps[$Identity].Direct.DisplayName Eligible = $CacheUsersAndApps[$Identity].Eligible.DisplayName Location = $CanonicalName #OnPremisesSamAccountName = $CacheUsersAndApps[$Identity].Identity.OnPremisesSamAccountName #OnPremisesLastSyncDateTime = $CacheUsersAndApps[$Identity].Identity.OnPremisesLastSyncDateTime } } else { # we need to use different way to count roles for each user # this is because we also count the roles of users nested in groups $RolesCount = 0 $GroupNameMember = $CacheUserMembers[$CacheUsersAndApps[$Identity].Identity.Id] if ($GroupNameMember) { # $GroupNameMember['Role'] # $DirectRoles = $CacheUsersAndApps[$GroupNameMember.id].Direct # $EligibleRoles = $CacheUsersAndApps[$GroupNameMember.id].Eligible # $IdentityOfGroup = $CacheUsersAndApps[$GroupNameMember.id].Identity.DisplayName } else { # $DirectRoles = $null # $EligibleRoles = $null # $IdentityOfGroup = $null } $UserIdentity = [ordered] @{ Name = $CacheUsersAndApps[$Identity].Identity.DisplayName Enabled = $CacheUsersAndApps[$Identity].Identity.AccountEnabled Status = $IsSynced Type = $Type CreatedDateTime = $CacheUsersAndApps[$Identity].Identity.CreatedDateTime Mail = $CacheUsersAndApps[$Identity].Identity.Mail UserPrincipalName = $CacheUsersAndApps[$Identity].Identity.UserPrincipalName } foreach ($Role in $ListActiveRoles | Sort-Object -Unique) { $UserIdentity[$Role] = '' } foreach ($Role in $CacheUsersAndApps[$Identity].Eligible) { if (-not $UserIdentity[$Role.DisplayName] ) { $UserIdentity[$Role.DisplayName] = [System.Collections.Generic.List[string]]::new() } $UserIdentity[$Role.DisplayName].Add('Eligible') $RolesCount++ } foreach ($Role in $CacheUsersAndApps[$Identity].Direct) { if (-not $UserIdentity[$Role.DisplayName] ) { $UserIdentity[$Role.DisplayName] = [System.Collections.Generic.List[string]]::new() } $UserIdentity[$Role.DisplayName].Add('Direct') $RolesCount++ } if ($GroupNameMember) { foreach ($Role in $GroupNameMember['Role'].Keys) { foreach ($Group in $GroupNameMember['Role'][$Role].GroupsDirect) { if (-not $UserIdentity[$Role] ) { $UserIdentity[$Role] = [System.Collections.Generic.List[string]]::new() } $UserIdentity[$Role].Add($Group.DisplayName) $RolesCount++ } foreach ($Group in $GroupNameMember['Role'][$Role].GroupsEligible) { if (-not $UserIdentity[$Role] ) { $UserIdentity[$Role] = [System.Collections.Generic.List[string]]::new() } $UserIdentity[$Role].Add($Group.DisplayName) $RolesCount++ } } # foreach ($Role in $DirectRoles) { # if (-not $UserIdentity[$Role.DisplayName] ) { # $UserIdentity[$Role.DisplayName] = [System.Collections.Generic.List[string]]::new() # } # $UserIdentity[$Role.DisplayName].Add($IdentityOfGroup) # } # foreach ($Role in $EligibleRoles) { # if (-not $UserIdentity[$Role.DisplayName]) { # $UserIdentity[$Role.DisplayName] = [System.Collections.Generic.List[string]]::new() # } # $UserIdentity[$Role.DisplayName].Add($IdentityOfGroup) # } } $UserIdentity['Location'] = $CanonicalName if ($OnlyWithRoles) { if ($RolesCount -eq 0) { continue } } [PSCustomObject] $UserIdentity } } } function Get-MyTeam { [cmdletbinding()] param( [switch] $PerOwner, [switch] $AsHashtable ) $OwnerShip = [ordered] @{} try { $TeamsRaw = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/beta/teams" -ContentType 'application/json; charset=UTF-8' -ErrorAction Stop } catch { Write-Warning -Message "Get-MyTeam - Couldn't get list of teams. Error: $($_.Exception.Message)" return } foreach ($Team in $TeamsRaw.value) { try { $Owner = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/groups/$($Team.id)/owners" -ContentType 'application/json; charset=UTF-8' -ErrorAction Stop } catch { Write-Warning -Message "Get-MyTeam - Couldn't get list of owners. Error: $($_.Exception.Message)" return } try { $TeamDetails = Get-MgTeam -TeamId $Team.id -ErrorAction Stop } catch { Write-Warning -Message "Get-MyTeam - Error: $($_.Exception.Message)" return } $TeamInformation = [ordered] @{ Id = $Team.id CreatedDateTime = $TeamDetails.createdDateTime Team = $Team.displayName Visibility = $Team.visibility OwnerCount = $Owner.value.Count MembersCount = $TeamDetails.Summary.MembersCount GuestsCount = $TeamDetails.Summary.GuestsCount Description = $Team.description OwnerDisplayName = $Owner.value.displayName OwnerMail = $Owner.value.mail OwnerUserPrincipalName = $Owner.value.userPrincipalName OwnerId = $Owner.value.id IsArchived = $Team.isArchived GuestAllowCreateUpdateChannels = $TeamDetails.GuestSettings.AllowCreateUpdateChannels GuestAllowDeleteChannels = $TeamDetails.GuestSettings.AllowDeleteChannels AllowAddRemoveApps = $TeamDetails.MemberSettings.AllowAddRemoveApps AllowCreatePrivateChannels = $TeamDetails.MemberSettings.AllowCreatePrivateChannels AllowCreateUpdateChannels = $TeamDetails.MemberSettings.AllowCreateUpdateChannels AllowCreateUpdateRemoveConnectors = $TeamDetails.MemberSettings.AllowCreateUpdateRemoveConnectors AllowCreateUpdateRemoveTabs = $TeamDetails.MemberSettings.AllowCreateUpdateRemoveTabs AllowDeleteChannels = $TeamDetails.MemberSettings.AllowDeleteChannels IsMembershipLimitedToOwners = $TeamDetails.MemberSettings.isMembershipLimitedToOwners } if ($PerOwner) { foreach ($O in $Owner.value) { if (-not $OwnerShip[$O.userPrincipalName]) { $OwnerShip[$O.userPrincipalName] = [System.Collections.Generic.List[PSCustomObject]]::new() } if ($AsHashtable) { $OwnerShip[$O.userPrincipalName].Add($TeamInformation) } else { $OwnerShip[$O.userPrincipalName].Add([PSCustomObject]$TeamInformation) } } } else { if ($AsHashtable) { $TeamInformation } else { [PSCustomObject]$TeamInformation } } } if ($PerOwner) { $OwnerShip } } function Get-MyUsageReports { [cmdletBinding(DefaultParameterSetName = "Period")] param( [parameter(ParameterSetName = 'Period', Mandatory)][ValidateSet('7', '30', '90', '180')][string] $Period, [parameter(ParameterSetName = 'DateTime', Mandatory)][DateTime] $DateTime, # last 30 days YYYY-MM-DD [parameter(Mandatory)][string][ValidateSet( 'EmailActivityCounts', 'EmailActivityUserCounts', 'EmailActivityUserDetail', 'EmailAppUsageAppsUserCounts', 'EmailAppUsageUserCounts', 'EmailAppUsageUserDetail', 'EmailAppUsageVersionsUserCounts', 'MailboxUsageDetail', 'MailboxUsageMailboxCounts', 'MailboxUsageQuotaStatusMailboxCounts', 'MailboxUsageStorage', 'Office365ActivationCounts', 'Office365ActivationsUserCounts', 'Office365ActivationsUserDetail', 'Office365ActiveUserCounts', 'Office365ActiveUserDetail', 'Office365GroupsActivityCounts', 'Office365GroupsActivityDetail', 'Office365GroupsActivityFileCounts', 'Office365GroupsActivityGroupCounts', 'Office365GroupsActivityStorage', 'Office365ServicesUserCounts', 'OneDriveActivityFileCounts', 'OneDriveActivityUserCounts', 'OneDriveActivityUserDetail', 'OneDriveUsageAccountCounts', 'OneDriveUsageAccountDetail', 'OneDriveUsageFileCounts', 'OneDriveUsageStorage', 'SharePointActivityFileCounts', 'SharePointActivityPages', 'SharePointActivityUserCounts', 'SharePointActivityUserDetail', 'SharePointSiteUsageDetail', 'SharePointSiteUsageFileCounts', 'SharePointSiteUsagePages', 'SharePointSiteUsageSiteCounts', 'SharePointSiteUsageStorage', 'SkypeForBusinessActivityCounts', 'SkypeForBusinessActivityUserCounts', 'SkypeForBusinessActivityUserDetail', 'SkypeForBusinessDeviceUsageDistributionUserCounts', 'SkypeForBusinessDeviceUsageUserCounts', 'SkypeForBusinessDeviceUsageUserDetail', 'SkypeForBusinessOrganizerActivityCounts', 'SkypeForBusinessOrganizerActivityMinuteCounts', 'SkypeForBusinessOrganizerActivityUserCounts', 'SkypeForBusinessParticipantActivityCounts', 'SkypeForBusinessParticipantActivityMinuteCounts', 'SkypeForBusinessParticipantActivityUserCounts', 'SkypeForBusinessPeerToPeerActivityCounts', 'SkypeForBusinessPeerToPeerActivityMinuteCounts', 'SkypeForBusinessPeerToPeerActivityUserCounts', 'TeamsDeviceUsageDistributionUserCounts', 'TeamsDeviceUsageUserCounts', 'TeamsDeviceUsageUserDetail', 'TeamsUserActivityCounts', 'TeamsUserActivityUserCounts', 'TeamsUserActivityUserDetail', 'YammerActivityCounts', 'YammerActivityUserCounts', 'YammerActivityUserDetail', 'YammerDeviceUsageDistributionUserCounts', 'YammerDeviceUsageUserCounts', 'YammerDeviceUsageUserDetail', 'YammerGroupsActivityCounts', 'YammerGroupsActivityDetail', 'YammerGroupsActivityGroupCounts' )] $Report ) if ($Period) { Write-Verbose -Message "Get-MyUsageReports - Report: $Report, Period: $Period" $DayPeriod = "D$Period" $Type = "(period='$DayPeriod')" } else { Write-Verbose -Message "Get-MyUsageReports - Report: $Report, DateTime: $DateTime" # last 30 days YYYY-MM-DD $DateTimeConverted = $DateTime.ToUniversalTime().ToString('yyyy-MM-dd') $Type = "(date=$DateTimeConverted)" } # main url $Url = "https://graph.microsoft.com/v1.0/reports/get$Report" # lets filter out the reports that do not support filtering, and apply some logic to let user get the data anyways if ($Report -in 'Office365ActivationCounts', 'Office365ActivationsUserCounts', 'Office365ActivationsUserDetail') { Write-Warning -Message "Get-MyUsageReports - $Report does not support filtering. Processing all data." } elseif ($Report -eq 'MailboxUsageDetail') { if ($Period) { $Url += "$Type" } else { Write-Warning -Message "Get-MyUsageReports - $Report does not support date filtering. Processing all data for period of 7 days." $Url += "(period='D7')" $Type = "(period='D7')" } } else { if ($Report -match 'Counts$|Pages$|Storage$') { if ($Period) { $Url += "$Type" } else { Write-Warning -Message "Get-MyUsageReports - $Report ending with Counts, Pages or Storage do not support date filtering. Processing data for last 7 days." $Url += "(period='D7')" } } else { $Url += "$Type" } } $TemporaryFile = [System.IO.Path]::GetTempFileName() try { Invoke-MgGraphRequest -Method GET -Uri $Url -ContentType "application/json" -OutputFilePath $TemporaryFile -ErrorAction Stop } catch { $ErrorDefault = $_.Exception.Message $ErrorDetails = $_.ErrorDetails.Message # get only the last line of the error message if ($ErrorDetails) { $ErrorDetails = $ErrorDetails.Split("`n")[-1] Try { $ErrorJSON = $ErrorDetails | ConvertFrom-Json -ErrorAction Stop } catch { Write-Warning -Message "Get-MyUsageReports - Error when requesting data for $Report $Type. Error: $ErrorDefault" } try { $NestedError = $ErrorJSON.error.message | ConvertFrom-Json -ErrorAction Stop $FinalErrorMessage = $NestedError.error.message Write-Warning -Message "Get-MyUsageReports - Error when requesting data for $Report $Type. Error: $FinalErrorMessage" } catch { Write-Warning -Message "Get-MyUsageReports - Error when requesting data for $Report $Type. Error: $ErrorDefault" } } else { Write-Warning -Message "Get-MyUsageReports - Error when requesting data for $Report $Type. Error: $ErrorDefault" } } if (Test-Path -LiteralPath $TemporaryFile) { try { $CSV = Import-Csv -LiteralPath $TemporaryFile -ErrorAction Stop -Encoding Unicode $CSV } catch { Write-Warning -Message "Get-MyUsageReports - Error when importing data $Report $Type. Error: $($_.Exception.Message)" } try { Remove-Item -LiteralPath $TemporaryFile -ErrorAction Stop } catch { Write-Warning -Message "Get-MyUsageReports - Error when removing temporary file $Report $Type. Error: $($_.Exception.Message)" } } } function Get-MyUser { [CmdletBinding(DefaultParameterSetName = 'Default')] param( [Parameter(ParameterSetName = 'PerLicense')][switch] $PerLicense, [Parameter(ParameterSetName = 'PerServicePlan')][switch] $PerServicePlan ) $Today = Get-Date $Properties = @( #'LicenseDetails', 'LicenseAssignmentStates', 'AccountEnabled', 'AssignedLicenses', 'AssignedPlans', 'DisplayName', 'Id', 'GivenName', 'SurName', 'JobTitle', 'LastPasswordChangeDateTime', 'Mail', 'Manager' 'OnPremisesLastSyncDateTime', 'OnPremisesSyncEnabled', 'OnPremisesDistinguishedName' ) Write-Verbose -Message "Get-MyUser - Getting list of licenses" $AllLicenses = Get-MyLicense -Internal $AllLicensesValues = $AllLicenses['Licenses'].Values | Sort-Object $AllServicePlansValues = $AllLicenses['ServicePlans'].Values | Sort-Object $getMgUserSplat = @{ All = $true Property = $Properties } Write-Verbose -Message "Get-MyUser - Getting list of all users" $StartTime = [System.Diagnostics.Stopwatch]::StartNew() $AllUsers = Get-MgUser @getMgUserSplat -ExpandProperty Manager $EndTime = Stop-TimeLog -Time $StartTime -Option OneLiner Write-Verbose -Message "Get-MyUser - Got $($AllUsers.Count) users in $($EndTime). Now processing them." $StartTime = [System.Diagnostics.Stopwatch]::StartNew() $Count = 0 foreach ($User in $AllUsers) { $Count++ Write-Verbose -Message "Get-MyUser - Processing $($User.DisplayName) - $Count/$($AllUsers.Count)" if ($User.LastPasswordChangeDateTime) { $LastPasswordChangeDays = $( - $($User.LastPasswordChangeDateTime - $Today).Days) } else { $LastPasswordChangeDays = $null } if ($User.OnPremisesLastSyncDateTime) { $LastSynchronizedDays = $( - $($User.OnPremisesLastSyncDateTime - $Today).Days) } else { $LastSynchronizedDays = $null } $OutputUser = [ordered] @{ 'DisplayName' = $User.DisplayName 'Id' = $User.Id 'GivenName' = $User.GivenName 'SurName' = $User.SurName 'Enabled' = $User.AccountEnabled 'JobTitle' = $User.JobTitle 'Mail' = $User.Mail 'Manager' = if ($User.Manager.Id) { $User.Manager.Id } else { $null } 'ManagerDisplayName' = if ($User.Manager.Id) { $User.Manager.AdditionalProperties.displayName } else { $null } 'ManagerUserPrincipalName' = if ($User.Manager.Id) { $User.Manager.AdditionalProperties.userPrincipalName } else { $null } 'ManagerIsSynchronized' = if ($User.Manager.Id) { if ($User.Manager.AdditionalProperties.onPremisesSyncEnabled) { $User.Manager.AdditionalProperties.onPremisesSyncEnabled } else { $false } } else { $null } 'LastPasswordChangeDateTime' = $User.LastPasswordChangeDateTime 'LastPasswordChangeDays' = $LastPasswordChangeDays 'IsSynchronized' = if ($User.OnPremisesSyncEnabled) { $User.OnPremisesSyncEnabled } else { $null } 'LastSynchronized' = $User.OnPremisesLastSyncDateTime 'LastSynchronizedDays' = $LastSynchronizedDays 'OnPremisesDistinguishedName' = $User.OnPremisesDistinguishedName } if ($PerLicense) { $LicensesErrors = [System.Collections.Generic.List[string]]::new() $OutputUser['NotMatched'] = [System.Collections.Generic.List[string]]::new() foreach ($License in $AllLicensesValues) { $OutputUser[$License] = [System.Collections.Generic.List[string]]::new() } foreach ($License in $User.LicenseAssignmentStates) { try { $LicenseFound = $AllLicenses['Licenses'][$License.SkuId] if ($LicenseFound) { if ($License.State -eq 'Active' -and $License.AssignedByGroup.Count -gt 0) { $OutputUser[$LicenseFound].Add('Group') } elseif ($License.State -eq 'Active' -and $License.AssignedByGroup.Count -eq 0) { $OutputUser[$LicenseFound].Add('Direct') } } else { if ($License.State -eq 'Active' -and $License.AssignedByGroup.Count -gt 0) { $OutputUser['DifferentLicense'].Add("Group $($License.SkuId)") } elseif ($License.State -eq 'Active' -and $License.AssignedByGroup.Count -eq 0) { $OutputUser['DifferentLicense'].Add("Direct $($License.SkuId)") } Write-Warning -Message "$($License.SkuId) not found in AllLicenses" $LicensesErrors.Add("License ID $($License.SkuId) not found in All Licenses") } } catch { Write-Warning -Message "Error processing $($License.SkuId) for $($User.DisplayName)" } } $OutputUser['LicensesErrors'] = $LicensesErrors | Sort-Object -Unique } elseif ($PerServicePlan) { $OutputUser['DeletedServicePlans'] = [System.Collections.Generic.List[string]]::new() foreach ($ServicePlan in $AllServicePlansValues) { $OutputUser[$ServicePlan] = '' } foreach ($ServicePlan in $User.AssignedPlans) { if ($AllLicenses['ServicePlans'][$ServicePlan.ServicePlanId]) { $OutputUser[$AllLicenses['ServicePlans'][$ServicePlan.ServicePlanId]] = 'Assigned' } else { if ($ServicePlan.CapabilityStatus -ne 'Deleted') { Write-Warning -Message "$($ServicePlan.ServicePlanId) $($ServicePlan.Service) not found in AllLicenses" } else { $OutputUser['DeletedServicePlans'].Add($ServicePlan.ServicePlanId) } } } } else { $LicensesList = [System.Collections.Generic.List[string]]::new() $LicensesStatus = [System.Collections.Generic.List[string]]::new() $LicensesErrors = [System.Collections.Generic.List[string]]::new() $User.LicenseAssignmentStates | ForEach-Object { if ($LicensesList -notcontains $AllLicenses['Licenses'][$_.SkuId]) { $LicensesList.Add($AllLicenses['Licenses'][$_.SkuId]) if ($_.State -eq 'Active' -and $_.AssignedByGroup.Count -gt 0) { $LicensesStatus.Add('Group') } elseif ($_.State -eq 'Active' -and $_.AssignedByGroup.Count -eq 0) { $LicensesStatus.Add('Direct') } else { $LicensesStatus.Add($_.State) if ($LicensesErrors -notcontains $_.Error) { $LicensesErrors.Add($_.Error) } } } else { $LicensesStatus.Add("Duplicate") } } $Plans = foreach ($Object in $User.AssignedPlans) { if ($Object.CapabilityStatus -ne 'Deleted') { $AllLicenses['ServicePlans'][$Object.ServicePlanId] } } $OutputUser['LicensesStatus'] = $LicensesStatus | Sort-Object -Unique $OutputUser['LicensesErrors'] = $LicensesErrors $OutputUser['Licenses'] = $LicensesList $OutputUser['Plans'] = $Plans } [PSCustomObject] $OutputUser } $EndTime = Stop-TimeLog -Time $StartTime -Option OneLiner Write-Verbose -Message "Get-MyUser - Processed all users in $($EndTime)." } function Invoke-MyGraphEssentials { [cmdletBinding()] param( [string] $FilePath, [Parameter(Position = 0)][string[]] $Type, [switch] $PassThru, [switch] $HideHTML, [switch] $HideSteps, [switch] $ShowError, [switch] $ShowWarning, [switch] $Online, [switch] $SplitReports ) Reset-GraphEssentials #$Script:AllUsers = [ordered] @{} $Script:Cache = [ordered] @{} $Script:Reporting = [ordered] @{} $Script:Reporting['Version'] = Get-GitHubVersion -Cmdlet 'Invoke-MyGraphEssentials' -RepositoryOwner 'evotecit' -RepositoryName 'GraphEssentials' $Script:Reporting['Settings'] = @{ ShowError = $ShowError.IsPresent ShowWarning = $ShowWarning.IsPresent HideSteps = $HideSteps.IsPresent } Write-Color '[i]', "[GraphEssentials] ", 'Version', ' [Informative] ', $Script:Reporting['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta # Verify requested types are supported $Supported = [System.Collections.Generic.List[string]]::new() [Array] $NotSupported = foreach ($T in $Type) { if ($T -notin $Script:GraphEssentialsConfiguration.Keys ) { $T } else { $Supported.Add($T) } } if ($Supported) { Write-Color '[i]', "[GraphEssentials] ", 'Supported types', ' [Informative] ', "Chosen by user: ", ($Supported -join ', ') -Color Yellow, DarkGray, Yellow, DarkGray, Yellow, Magenta } if ($NotSupported) { Write-Color '[i]', "[GraphEssentials] ", 'Not supported types', ' [Informative] ', "Following types are not supported: ", ($NotSupported -join ', ') -Color Yellow, DarkGray, Yellow, DarkGray, Yellow, Magenta Write-Color '[i]', "[GraphEssentials] ", 'Not supported types', ' [Informative] ', "Please use one/multiple from the list: ", ($Script:GraphEssentialsConfiguration.Keys -join ', ') -Color Yellow, DarkGray, Yellow, DarkGray, Yellow, Magenta return } # Lets make sure we only enable those types which are requestd by user if ($Type) { foreach ($T in $Script:GraphEssentialsConfiguration.Keys) { $Script:GraphEssentialsConfiguration[$T].Enabled = $false } # Lets enable all requested ones foreach ($T in $Type) { $Script:GraphEssentialsConfiguration[$T].Enabled = $true } } # Build data foreach ($T in $Script:GraphEssentialsConfiguration.Keys) { if ($Script:GraphEssentialsConfiguration[$T].Enabled -eq $true) { $Script:Reporting[$T] = [ordered] @{ Name = $Script:GraphEssentialsConfiguration[$T].Name ActionRequired = $null Data = $null Exclusions = $null WarningsAndErrors = $null Time = $null Summary = $null Variables = Copy-Dictionary -Dictionary $Script:GraphEssentialsConfiguration[$T]['Variables'] } if ($Exclusions) { if ($Exclusions -is [scriptblock]) { $Script:Reporting[$T]['ExclusionsCode'] = $Exclusions } if ($Exclusions -is [Array]) { $Script:Reporting[$T]['Exclusions'] = $Exclusions } } $TimeLogGraphEssentials = Start-TimeLog Write-Color -Text '[i]', '[Start] ', $($Script:GraphEssentialsConfiguration[$T]['Name']) -Color Yellow, DarkGray, Yellow $OutputCommand = Invoke-Command -ScriptBlock $Script:GraphEssentialsConfiguration[$T]['Execute'] -WarningVariable CommandWarnings -ErrorVariable CommandErrors #-ArgumentList $Forest, $ExcludeDomains, $IncludeDomains if ($OutputCommand -is [System.Collections.IDictionary]) { # in some cases the return will be wrapped in Hashtable/orderedDictionary and we need to handle this without an array $Script:Reporting[$T]['Data'] = $OutputCommand } else { # since sometimes it can be 0 or 1 objects being returned we force it being an array $Script:Reporting[$T]['Data'] = [Array] $OutputCommand } Invoke-Command -ScriptBlock $Script:GraphEssentialsConfiguration[$T]['Processing'] $Script:Reporting[$T]['WarningsAndErrors'] = @( if ($ShowWarning) { foreach ($War in $CommandWarnings) { [PSCustomObject] @{ Type = 'Warning' Comment = $War Reason = '' TargetName = '' } } } if ($ShowError) { foreach ($Err in $CommandErrors) { [PSCustomObject] @{ Type = 'Error' Comment = $Err Reason = $Err.CategoryInfo.Reason TargetName = $Err.CategoryInfo.TargetName } } } ) $TimeEndGraphEssentials = Stop-TimeLog -Time $TimeLogGraphEssentials -Option OneLiner $Script:Reporting[$T]['Time'] = $TimeEndGraphEssentials Write-Color -Text '[i]', '[End ] ', $($Script:GraphEssentialsConfiguration[$T]['Name']), " [Time to execute: $TimeEndGraphEssentials]" -Color Yellow, DarkGray, Yellow, DarkGray if ($SplitReports) { Write-Color -Text '[i]', '[HTML ] ', 'Generating HTML report for ', $T -Color Yellow, DarkGray, Yellow $TimeLogHTML = Start-TimeLog New-HTMLReportGraphEssentialsWithSplit -FilePath $FilePath -Online:$Online -HideHTML:$HideHTML -CurrentReport $T $TimeLogEndHTML = Stop-TimeLog -Time $TimeLogHTML -Option OneLiner Write-Color -Text '[i]', '[HTML ] ', 'Generating HTML report for', $T, " [Time to execute: $TimeLogEndHTML]" -Color Yellow, DarkGray, Yellow, DarkGray } } } if ( -not $SplitReports) { Write-Color -Text '[i]', '[HTML ] ', 'Generating HTML report' -Color Yellow, DarkGray, Yellow $TimeLogHTML = Start-TimeLog if (-not $FilePath) { $FilePath = Get-FileName -Extension 'html' -Temporary } New-HTMLReportGraphEssentials -Type $Type -Online:$Online.IsPresent -HideHTML:$HideHTML.IsPresent -FilePath $FilePath $TimeLogEndHTML = Stop-TimeLog -Time $TimeLogHTML -Option OneLiner Write-Color -Text '[i]', '[HTML ] ', 'Generating HTML report', " [Time to execute: $TimeLogEndHTML]" -Color Yellow, DarkGray, Yellow, DarkGray } Reset-GraphEssentials } [scriptblock] $SourcesAutoCompleter = { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) $Script:GraphEssentialsConfiguration.Keys | Sort-Object | Where-Object { $_ -like "*$wordToComplete*" } } Register-ArgumentCompleter -CommandName Invoke-MyGraphEssentials -ParameterName Type -ScriptBlock $SourcesAutoCompleter function Invoke-MyGraphUsageReports { [CmdletBinding()] param( [parameter(ParameterSetName = 'Period', Mandatory)][ValidateSet('7', '30', '90', '180')][string] $Period, [parameter(ParameterSetName = 'DateTime', Mandatory)][DateTime] $DateTime, # last 30 days YYYY-MM-DD [parameter(Mandatory)][string[]][ValidateSet( 'All', 'EmailActivityCounts', 'EmailActivityUserCounts', 'EmailActivityUserDetail', 'EmailAppUsageAppsUserCounts', 'EmailAppUsageUserCounts', 'EmailAppUsageUserDetail', 'EmailAppUsageVersionsUserCounts', 'MailboxUsageDetail', 'MailboxUsageMailboxCounts', 'MailboxUsageQuotaStatusMailboxCounts', 'MailboxUsageStorage', 'Office365ActivationCounts', 'Office365ActivationsUserCounts', 'Office365ActivationsUserDetail', 'Office365ActiveUserCounts', 'Office365ActiveUserDetail', 'Office365GroupsActivityCounts', 'Office365GroupsActivityDetail', 'Office365GroupsActivityFileCounts', 'Office365GroupsActivityGroupCounts', 'Office365GroupsActivityStorage', 'Office365ServicesUserCounts', 'OneDriveActivityFileCounts', 'OneDriveActivityUserCounts', 'OneDriveActivityUserDetail', 'OneDriveUsageAccountCounts', 'OneDriveUsageAccountDetail', 'OneDriveUsageFileCounts', 'OneDriveUsageStorage', 'SharePointActivityFileCounts', 'SharePointActivityPages', 'SharePointActivityUserCounts', 'SharePointActivityUserDetail', 'SharePointSiteUsageDetail', 'SharePointSiteUsageFileCounts', 'SharePointSiteUsagePages', 'SharePointSiteUsageSiteCounts', 'SharePointSiteUsageStorage', 'SkypeForBusinessActivityCounts', 'SkypeForBusinessActivityUserCounts', 'SkypeForBusinessActivityUserDetail', 'SkypeForBusinessDeviceUsageDistributionUserCounts', 'SkypeForBusinessDeviceUsageUserCounts', 'SkypeForBusinessDeviceUsageUserDetail', 'SkypeForBusinessOrganizerActivityCounts', 'SkypeForBusinessOrganizerActivityMinuteCounts', 'SkypeForBusinessOrganizerActivityUserCounts', 'SkypeForBusinessParticipantActivityCounts', 'SkypeForBusinessParticipantActivityMinuteCounts', 'SkypeForBusinessParticipantActivityUserCounts', 'SkypeForBusinessPeerToPeerActivityCounts', 'SkypeForBusinessPeerToPeerActivityMinuteCounts', 'SkypeForBusinessPeerToPeerActivityUserCounts', 'TeamsDeviceUsageDistributionUserCounts', 'TeamsDeviceUsageUserCounts', 'TeamsDeviceUsageUserDetail', 'TeamsUserActivityCounts', 'TeamsUserActivityUserCounts', 'TeamsUserActivityUserDetail', 'YammerActivityCounts', 'YammerActivityUserCounts', 'YammerActivityUserDetail', 'YammerDeviceUsageDistributionUserCounts', 'YammerDeviceUsageUserCounts', 'YammerDeviceUsageUserDetail', 'YammerGroupsActivityCounts', 'YammerGroupsActivityDetail', 'YammerGroupsActivityGroupCounts' )] $Report, [switch] $Online, [switch] $HideHTML, [string] $FilePath, [switch] $DontSuppress ) $Script:Reporting = [ordered] @{} $Script:Reporting['Version'] = Get-GitHubVersion -Cmdlet 'Invoke-MyGraphEssentials' -RepositoryOwner 'evotecit' -RepositoryName 'GraphEssentials' $Script:Reporting['Reports'] = [ordered] @{} if ($Report -contains 'All') { $ParameterList = (Get-Command -Name Get-MyUsageReports).Parameters $Report = $ParameterList["Report"].Attributes.ValidValues } foreach ($R in $Report) { $Splat = @{ Report = $R Period = $Period DateTime = $DateTime } Remove-EmptyValue -Hashtable $Splat $Script:Reporting['Reports'][$R] = Get-MyUsageReports @Splat } $newHTMLReportGraphUsageSplat = @{ Reports = $Script:Reporting['Reports'] Online = $Online HideHTML = $HideHTML FilePath = $FilePath } New-HTMLReportGraphUsage @newHTMLReportGraphUsageSplat if ($DontSuppress) { $Script:Reporting } } function New-MyApp { [cmdletBinding()] param( [parameter(Mandatory)][alias('AppName', 'DisplayName')][string] $ApplicationName, [parameter(Mandatory)][alias('DescriptionCredentials')][string] $DisplayNameCredentials, [string] $Description, [int] $MonthsValid = 12, [switch] $RemoveOldCredentials, [switch] $ServicePrincipal ) $Application = Get-MgApplication -Filter "displayName eq '$ApplicationName'" -All -ErrorAction Stop if (-not $Application) { Write-Verbose -Message "New-MyApp - Creating application $ApplicationName" $newMgApplicationSplat = @{ DisplayName = $ApplicationName Description = $Description } Remove-EmptyValue -Hashtable $newMgApplicationSplat $Application = New-MgApplication @newMgApplicationSplat -ErrorAction Stop } else { Write-Verbose -Message "New-MyApp - Application $ApplicationName already exists. Reusing..." } if ($RemoveOldCredentials -and $Application.PasswordCredentials.Count -gt 0) { foreach ($Credential in $Application.PasswordCredentials) { Write-Verbose -Message "New-MyApp - Removing old credential $($Credential.KeyId) / $($Credential.DisplayName)" try { Remove-MgApplicationPassword -ApplicationId $Application.Id -KeyId $Credential.KeyId -ErrorAction Stop } catch { Write-Warning -Message "New-MyApp - Failed to remove old credential $($Credential.KeyId) / $($Credential.DisplayName)" return } } } $Credentials = New-MyAppCredentials -ObjectID $Application.Id -DisplayName $DisplayNameCredentials -MonthsValid $MonthsValid if ($Application -and $Credentials) { [PSCustomObject] @{ ObjectID = $Application.Id ApplicationName = $Application.DisplayName ClientID = $Application.AppId ClientSecretName = $Credentials.DisplayName ClientSecret = $Credentials.SecretText ClientSecretID = $Credentials.KeyID DaysToExpire = ($Credentials.EndDateTime - [DateTime]::Now).Days StartDateTime = $Credentials.StartDateTime EndDateTime = $Credentials.EndDateTime } } else { Write-Warning -Message "New-MyApp - Application or credentials for $ApplicationName was not created." } if ($ServicePrincipal) { $ServicePrincipalApp = Get-MgServicePrincipal -Filter "appId eq '$($Application.AppId)'" -All -ConsistencyLevel eventual -ErrorAction Stop if (-not $ServicePrincipalApp) { Write-Verbose -Message "New-MyApp - Creating service principal for $ApplicationName" try { $null = New-MgServicePrincipal -AppId $Application.AppId -AccountEnabled:$true -ErrorAction Stop } catch { Write-Warning -Message "New-MyApp - Failed to create service principal for $ApplicationName. Error: $($_.Exception.Message)" } } else { Write-Verbose -Message "New-MyApp - Service principal for $ApplicationName already exists. Skipping..." } } } function New-MyAppCredentials { [cmdletbinding(DefaultParameterSetName = 'AppName')] param( [parameter(Mandatory, ParameterSetName = 'AppId')][string] $ObjectID, [alias('AppName')] [parameter(Mandatory, ParameterSetName = 'AppName')][string] $ApplicationName, [string] $DisplayName, [int] $MonthsValid = 12 ) if ($AppName) { $Application = Get-MgApplication -Filter "DisplayName eq '$ApplicationName'" -ConsistencyLevel eventual -All if ($Application) { $ID = $Application.Id } else { Write-Warning -Message "Application with name '$ApplicationName' not found" return } } else { $ID = $ObjectID } $PasswordCredential = [Microsoft.Graph.PowerShell.Models.IMicrosoftGraphPasswordCredential] @{ StartDateTime = [datetime]::Now } if ($DisplayName) { $PasswordCredential.DisplayName = $DisplayName } $PasswordCredential.EndDateTime = [datetime]::Now.AddMonths($MonthsValid) try { Add-MgApplicationPassword -ApplicationId $ID -PasswordCredential $PasswordCredential -ErrorAction Stop } catch { Write-Warning -Message "Failed to add password credential to application $ID / $ApplicationName. Error: $($_.Exception.Message)" } } function Remove-MyAppCredentials { [cmdletBinding(SupportsShouldProcess)] param( [string] $ApplicationName, [int] $LessThanDaysToExpire, [int] $GreaterThanDaysToExpire, [switch] $Expired, [alias('DescriptionCredentials')][string] $DisplayNameCredentials ) $getMyAppCredentialsSplat = @{} if ($PSBoundParameters.ContainsKey('ApplicationName')) { $getMyAppCredentialsSplat.ApplicationName = $ApplicationName } if ($PSBoundParameters.ContainsKey('LessThanDaysToExpire')) { $getMyAppCredentialsSplat.LessThanDaysToExpire = $LessThanDaysToExpire } if ($PSBoundParameters.ContainsKey('Expired')) { $getMyAppCredentialsSplat.Expired = $Expired } if ($PSBoundParameters.ContainsKey('DisplayNameCredentials')) { $getMyAppCredentialsSplat.DisplayNameCredentials = $DisplayNameCredentials } if ($PSBoundParameters.ContainsKey('GreaterThanDaysToExpire')) { $getMyAppCredentialsSplat.GreaterThanDaysToExpire = $GreaterThanDaysToExpire } $Applications = Get-MyAppCredentials @getMyAppCredentialsSplat foreach ($App in $Applications) { Write-Verbose -Message "Processing application $($App.ApplicationName) for key removal $($App.ClientSecretName)/$($App.ClientSecretID) - Start: $($App.StartDateTime), End: $($App.EndDateTime), IsExpired: $($App.Expired)" if ($PSCmdlet.ShouldProcess($App.ApplicationName, "Remove $($App.ClientSecretName)/$($App.ClientSecretID)")) { try { # it has it's own whatif, but it looks ugly Remove-MgApplicationPassword -ApplicationId $App.ObjectID -KeyId $App.ClientSecretID -ErrorAction Stop } catch { Write-Warning -Message "Failed to remove $($App.ClientSecretName)/$($App.ClientSecretID) from $($App.ApplicationName). Error: $($_.Exception.Message)" } } } } function Send-MyApp { [cmdletBinding()] param( [parameter(Mandatory)][Array] $ApplicationName, [parameter(Mandatory)][string] $EmailFrom, [parameter(Mandatory)][string[]] $EmailTo, [string] $EmailSubject = 'Service Principal for Applications', [parameter(Mandatory)][string] $Domain, [switch] $RemoveOldCredentials ) $TenantID = Get-O365TenantID -Domain $Domain $Applications = foreach ($App in $ApplicationName) { if ($App -is [string]) { $DisplayNameCredentials = $EmailTo -join ";" New-MyApp -ApplicationName $App -DisplayNameCredentials $DisplayNameCredentials -Verbose -RemoveOldCredentials:$RemoveOldCredentials.IsPresent } else { if ($App.DisplayNameCredentials) { New-MyApp @App } else { $DisplayNameCredentials = $EmailTo -join ";" New-MyApp @App -DisplayNameCredentials $DisplayNameCredentials -Verbose -ServicePrincipal -RemoveOldCredentials:$RemoveOldCredentials.IsPresent } } } $EmailBody = EmailBody { EmailText -Text "Hello," -LineBreak EmailText -Text @( "As per your request we have created following Service Principal for you:" ) EmailText -LineBreak foreach ($Application in $Applications) { EmailText -Text @( "Application ", $Application.ApplicationName, " credentials are: " ) -Color None, BlueDiamond, None -TextDecoration none, underline, none -FontWeight normal, bold, normal EmailList { EmailListItem -Text "Application Name: ", $Application.ApplicationName -Color None, BlueDiamond, None -FontWeight normal, bold, normal EmailListItem -Text "Client ID: ", $Application.ClientID -Color None, BlueDiamond, None -FontWeight normal, bold, normal EmailListItem -Text "Client SecretID: ", $Application.ClientSecretID -Color None, BlueDiamond, None -FontWeight normal, bold, normal EmailListItem -Text "Client Secret: ", $Application.ClientSecret -Color None, BlueDiamond, None -FontWeight normal, bold, normal EmailListItem -Text "Expires: ", $Application.EndDateTime, " (Valid days: $($Application.DaysToExpire))" -Color None, BlueDiamond, None -FontWeight normal, bold, normal } } EmailText -LineBreak EmailText -Text @( "If required TenantID/DirectoryID is: ", $TenantID ) -LineBreak -Color None, LawnGreen -FontWeight normal, bold EmailText -Text @( "Please remove this email from your inbox once you have copied the credentials into secure place." ) -FontWeight normal, normal, bold, normal, normal -Color None, None, Salmon, None, None -LineBreak EmailText -Text "Thank you" } $EmailStatus = Send-EmailMessage -From $EmailFrom -To $EmailTo -HTML $EmailBody -Subject $EmailSubject -Verbose -Priority Normal -MgGraphRequest [ordered] @{ EmailStatus = $EmailStatus Applications = $Applications } } function Show-MyApp { [cmdletBinding()] param( [Parameter(Mandatory)][string] $FilePath, [switch] $Online, [switch] $ShowHTML ) $Applications = Get-MyApp $ApplicationsPassword = Get-MyAppCredentials New-HTML { New-HTMLTableOption -DataStore JavaScript -BoolAsString New-HTMLSection -Invisible { New-HTMLSection -HeaderText "Applications" { New-HTMLTable -DataTable $Applications -Filtering { New-TableEvent -ID 'TableAppsCredentials' -SourceColumnID 1 -TargetColumnID 1 } -DataStore JavaScript -DataTableID "TableApps" } New-HTMLSection -HeaderText 'Applications Credentials' { New-HTMLTable -DataTable $ApplicationsPassword -Filtering { New-HTMLTableCondition -Name 'DaysToExpire' -Value 30 -Operator 'ge' -BackgroundColor Conifer -ComparisonType number New-HTMLTableCondition -Name 'DaysToExpire' -Value 30 -Operator 'lt' -BackgroundColor Orange -ComparisonType number New-HTMLTableCondition -Name 'DaysToExpire' -Value 5 -Operator 'lt' -BackgroundColor Red -ComparisonType number New-HTMLTableCondition -Name 'Expired' -Value $true -ComparisonType string -BackgroundColor Salmon -FailBackgroundColor Conifer } -DataStore JavaScript -DataTableID "TableAppsCredentials" } } } -ShowHTML:$ShowHTML.IsPresent -FilePath $FilePath -Online:$Online.IsPresent } $ModuleFunctions = @{ } [Array] $FunctionsAll = 'Get-MgToken', 'Get-MyApp', 'Get-MyAppCredentials', 'Get-MyDevice', 'Get-MyDeviceIntune', 'Get-MyLicense', 'Get-MyRole', 'Get-MyRoleUsers', 'Get-MyTeam', 'Get-MyUsageReports', 'Get-MyUser', 'Invoke-MyGraphEssentials', 'Invoke-MyGraphUsageReports', 'New-MyApp', 'New-MyAppCredentials', 'Remove-MyAppCredentials', 'Send-MyApp', 'Show-MyApp' [Array] $AliasesAll = $AliasesToRemove = [System.Collections.Generic.List[string]]::new() $FunctionsToRemove = [System.Collections.Generic.List[string]]::new() foreach ($Module in $ModuleFunctions.Keys) { try { Import-Module -Name $Module -ErrorAction Stop } catch { foreach ($Function in $ModuleFunctions[$Module].Keys) { $FunctionsToRemove.Add($Function) $ModuleFunctions[$Module][$Function] | ForEach-Object { if ($_) { $AliasesToRemove.Add($_) } } } } } $FunctionsToLoad = foreach ($Function in $FunctionsAll) { if ($Function -notin $FunctionsToRemove) { $Function } } $AliasesToLoad = foreach ($Alias in $AliasesAll) { if ($Alias -notin $AliasesToRemove) { $Alias } } Export-ModuleMember -Function @($FunctionsToLoad) -Alias @($AliasesToLoad) # SIG # Begin signature block # MIInPgYJKoZIhvcNAQcCoIInLzCCJysCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC21l2aYSnKqito # O247L4++nL0/k+OcZ/k+0y7fq/aeMaCCITcwggO3MIICn6ADAgECAhAM5+DlF9hG # /o/lYPwb8DA5MA0GCSqGSIb3DQEBBQUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBa # Fw0zMTExMTAwMDAwMDBaMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lD # ZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC # AQoCggEBAK0OFc7kQ4BcsYfzt2D5cRKlrtwmlIiq9M71IDkoWGAM+IDaqRWVMmE8 # tbEohIqK3J8KDIMXeo+QrIrneVNcMYQq9g+YMjZ2zN7dPKii72r7IfJSYd+fINcf # 4rHZ/hhk0hJbX/lYGDW8R82hNvlrf9SwOD7BG8OMM9nYLxj+KA+zp4PWw25EwGE1 # lhb+WZyLdm3X8aJLDSv/C3LanmDQjpA1xnhVhyChz+VtCshJfDGYM2wi6YfQMlqi # uhOCEe05F52ZOnKh5vqk2dUXMXWuhX0irj8BRob2KHnIsdrkVxfEfhwOsLSSplaz # vbKX7aqn8LfFqD+VFtD/oZbrCF8Yd08CAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGG # MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEXroq/0ksuCMS1Ri6enIZ3zbcgP # MB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBBQUA # A4IBAQCiDrzf4u3w43JzemSUv/dyZtgy5EJ1Yq6H6/LV2d5Ws5/MzhQouQ2XYFwS # TFjk0z2DSUVYlzVpGqhH6lbGeasS2GeBhN9/CTyU5rgmLCC9PbMoifdf/yLil4Qf # 6WXvh+DfwWdJs13rsgkq6ybteL59PyvztyY1bV+JAbZJW58BBZurPSXBzLZ/wvFv # hsb6ZGjrgS2U60K3+owe3WLxvlBnt2y98/Efaww2BxZ/N3ypW2168RJGYIPXJwS+ # S86XvsNnKmgR34DnDDNmvxMNFG7zfx9jEB76jRslbWyPpbdhAbHSoyahEHGdreLD # +cOZUbcrBwjOLuZQsqf6CkUvovDyMIIFMDCCBBigAwIBAgIQBAkYG1/Vu2Z1U0O1 # b5VQCDANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGln # aUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtE # aWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMTMxMDIyMTIwMDAwWhcNMjgx # MDIyMTIwMDAwWjByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5j # MRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBT # SEEyIEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0BAQEF # AAOCAQ8AMIIBCgKCAQEA+NOzHH8OEa9ndwfTCzFJGc/Q+0WZsTrbRPV/5aid2zLX # cep2nQUut4/6kkPApfmJ1DcZ17aq8JyGpdglrA55KDp+6dFn08b7KSfH03sjlOSR # I5aQd4L5oYQjZhJUM1B0sSgmuyRpwsJS8hRniolF1C2ho+mILCCVrhxKhwjfDPXi # TWAYvqrEsq5wMWYzcT6scKKrzn/pfMuSoeU7MRzP6vIK5Fe7SrXpdOYr/mzLfnQ5 # Ng2Q7+S1TqSp6moKq4TzrGdOtcT3jNEgJSPrCGQ+UpbB8g8S9MWOD8Gi6CxR93O8 # vYWxYoNzQYIH5DiLanMg0A9kczyen6Yzqf0Z3yWT0QIDAQABo4IBzTCCAckwEgYD # VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYB # BQUHAwMweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k # aWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0 # LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4 # oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJv # b3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy # dEFzc3VyZWRJRFJvb3RDQS5jcmwwTwYDVR0gBEgwRjA4BgpghkgBhv1sAAIEMCow # KAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCgYIYIZI # AYb9bAMwHQYDVR0OBBYEFFrEuXsqCqOl6nEDwGD5LfZldQ5YMB8GA1UdIwQYMBaA # FEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBCwUAA4IBAQA+7A1aJLPz # ItEVyCx8JSl2qB1dHC06GsTvMGHXfgtg/cM9D8Svi/3vKt8gVTew4fbRknUPUbRu # pY5a4l4kgU4QpO4/cY5jDhNLrddfRHnzNhQGivecRk5c/5CxGwcOkRX7uq+1UcKN # JK4kxscnKqEpKBo6cSgCPC6Ro8AlEeKcFEehemhor5unXCBc2XGxDI+7qPjFEmif # z0DLQESlE/DmZAwlCEIysjaKJAL+L3J+HNdJRZboWR3p+nRka7LrZkPas7CM1ekN # 3fYBIM6ZMWM9CBoYs4GbT8aTEAb8B4H6i9r5gkn3Ym6hU/oSlBiFLpKR6mhsRDKy # ZqHnGKSaZFHvMIIFPTCCBCWgAwIBAgIQBNXcH0jqydhSALrNmpsqpzANBgkqhkiG # 9w0BAQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkw # FwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEy # IEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTIwMDYyNjAwMDAwMFoXDTIz # MDcwNzEyMDAwMFowejELMAkGA1UEBhMCUEwxEjAQBgNVBAgMCcWabMSFc2tpZTER # MA8GA1UEBxMIS2F0b3dpY2UxITAfBgNVBAoMGFByemVteXPFgmF3IEvFgnlzIEVW # T1RFQzEhMB8GA1UEAwwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMIIBIjANBgkq # hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7KB3iyBrhkLUbbFe9qxhKKPBYqDBqln # r3AtpZplkiVjpi9dMZCchSeT5ODsShPuZCIxJp5I86uf8ibo3vi2S9F9AlfFjVye # 3dTz/9TmCuGH8JQt13ozf9niHecwKrstDVhVprgxi5v0XxY51c7zgMA2g1Ub+3ti # i0vi/OpmKXdL2keNqJ2neQ5cYly/GsI8CREUEq9SZijbdA8VrRF3SoDdsWGf3tZZ # zO6nWn3TLYKQ5/bw5U445u/V80QSoykszHRivTj+H4s8ABiforhi0i76beA6Ea41 # zcH4zJuAp48B4UhjgRDNuq8IzLWK4dlvqrqCBHKqsnrF6BmBrv+BXQIDAQABo4IB # xTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0OBBYE # FBixNSfoHFAgJk4JkDQLFLRNlJRmMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK # BggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdpY2Vy # dC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2NybDQu # ZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUwQzA3 # BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu # Y29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNpZ25p # bmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAmr1sz4ls # LARi4wG1eg0B8fVJFowtect7SnJUrp6XRnUG0/GI1wXiLIeow1UPiI6uDMsRXPHU # F/+xjJw8SfIbwava2eXu7UoZKNh6dfgshcJmo0QNAJ5PIyy02/3fXjbUREHINrTC # vPVbPmV6kx4Kpd7KJrCo7ED18H/XTqWJHXa8va3MYLrbJetXpaEPpb6zk+l8Rj9y # G4jBVRhenUBUUj3CLaWDSBpOA/+sx8/XB9W9opYfYGb+1TmbCkhUg7TB3gD6o6ES # Jre+fcnZnPVAPESmstwsT17caZ0bn7zETKlNHbc1q+Em9kyBjaQRcEQoQQNpezQu # g9ufqExx6lHYDjCCBY0wggR1oAMCAQICEA6bGI750C3n79tQ4ghAGFowDQYJKoZI # hvcNAQEMBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ # MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNz # dXJlZCBJRCBSb290IENBMB4XDTIyMDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1OVow # YjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ # d3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290 # IEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAv+aQc2jeu+RdSjww # IjBpM+zCpyUuySE98orYWcLhKac9WKt2ms2uexuEDcQwH/MbpDgW61bGl20dq7J5 # 8soR0uRf1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNwwrK6dZlqczKU0RBEEC7fgvMH # hOZ0O21x4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs06wXGXuxbGrzryc/NrDRAX7F6 # Zu53yEioZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e5TXnMcvak17cjo+A2raRmECQ # ecN4x7axxLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtVgkEy19sEcypukQF8IUzUvK4b # A3VdeGbZOjFEmjNAvwjXWkmkwuapoGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2mHY9 # WV1CdoeJl2l6SPDgohIbZpp0yt5LHucOY67m1O+SkjqePdwA5EUlibaaRBkrfsCU # tNJhbesz2cXfSwQAzH0clcOP9yGyshG3u3/y1YxwLEFgqrFjGESVGnZifvaAsPvo # ZKYz0YkH4b235kOkGLimdwHhD5QMIR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxfjT/J # vNNBERJb5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFrb7GrhotPwtZFX50g/KEexcCP # orF+CiaZ9eRpL5gdLfXZqbId5RsCAwEAAaOCATowggE2MA8GA1UdEwEB/wQFMAMB # Af8wHQYDVR0OBBYEFOzX44LScV1kTN8uZz/nupiuHA9PMB8GA1UdIwQYMBaAFEXr # oq/0ksuCMS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQEAwIBhjB5BggrBgEFBQcBAQRt # MGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEF # BQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJl # ZElEUm9vdENBLmNydDBFBgNVHR8EPjA8MDqgOKA2hjRodHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMBEGA1UdIAQKMAgw # BgYEVR0gADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3v1cH # vZqsoYcs7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy3iS8 # UgPITtAq3votVs/59PesMHqai7Je1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cnRNTn # f+hZqPC/Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4oVaO7KTVPeix3P0c2PR3WlxU # jG/voVA9/HYJaISfb8rbII01YBwCA8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2zm8j # LfR+cWojayL/ErhULSd+2DrZ8LaHlv1b0VysGMNNn3O3AamfV6peKOK5lDCCBq4w # ggSWoAMCAQICEAc2N7ckVHzYR6z9KGYqXlswDQYJKoZIhvcNAQELBQAwYjELMAkG # A1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRp # Z2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MB4X # DTIyMDMyMzAwMDAwMFoXDTM3MDMyMjIzNTk1OVowYzELMAkGA1UEBhMCVVMxFzAV # BgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVk # IEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFtcGluZyBDQTCCAiIwDQYJKoZIhvcN # AQEBBQADggIPADCCAgoCggIBAMaGNQZJs8E9cklRVcclA8TykTepl1Gh1tKD0Z5M # om2gsMyD+Vr2EaFEFUJfpIjzaPp985yJC3+dH54PMx9QEwsmc5Zt+FeoAn39Q7SE # 2hHxc7Gz7iuAhIoiGN/r2j3EF3+rGSs+QtxnjupRPfDWVtTnKC3r07G1decfBmWN # lCnT2exp39mQh0YAe9tEQYncfGpXevA3eZ9drMvohGS0UvJ2R/dhgxndX7RUCyFo # bjchu0CsX7LeSn3O9TkSZ+8OpWNs5KbFHc02DVzV5huowWR0QKfAcsW6Th+xtVhN # ef7Xj3OTrCw54qVI1vCwMROpVymWJy71h6aPTnYVVSZwmCZ/oBpHIEPjQ2OAe3Vu # JyWQmDo4EbP29p7mO1vsgd4iFNmCKseSv6De4z6ic/rnH1pslPJSlRErWHRAKKtz # Q87fSqEcazjFKfPKqpZzQmiftkaznTqj1QPgv/CiPMpC3BhIfxQ0z9JMq++bPf4O # uGQq+nUoJEHtQr8FnGZJUlD0UfM2SU2LINIsVzV5K6jzRWC8I41Y99xh3pP+OcD5 # sjClTNfpmEpYPtMDiP6zj9NeS3YSUZPJjAw7W4oiqMEmCPkUEBIDfV8ju2TjY+Cm # 4T72wnSyPx4JduyrXUZ14mCjWAkBKAAOhFTuzuldyF4wEr1GnrXTdrnSDmuZDNIz # tM2xAgMBAAGjggFdMIIBWTASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBS6 # FtltTYUvcyl2mi91jGogj57IbzAfBgNVHSMEGDAWgBTs1+OC0nFdZEzfLmc/57qY # rhwPTzAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgwdwYIKwYB # BQUHAQEEazBpMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w # QQYIKwYBBQUHMAKGNWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy # dFRydXN0ZWRSb290RzQuY3J0MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwz # LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3JsMCAGA1UdIAQZ # MBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAgEAfVmO # wJO2b5ipRCIBfmbW2CFC4bAYLhBNE88wU86/GPvHUF3iSyn7cIoNqilp/GnBzx0H # 6T5gyNgL5Vxb122H+oQgJTQxZ822EpZvxFBMYh0MCIKoFr2pVs8Vc40BIiXOlWk/ # R3f7cnQU1/+rT4osequFzUNf7WC2qk+RZp4snuCKrOX9jLxkJodskr2dfNBwCnzv # qLx1T7pa96kQsl3p/yhUifDVinF2ZdrM8HKjI/rAJ4JErpknG6skHibBt94q6/ae # sXmZgaNWhqsKRcnfxI2g55j7+6adcq/Ex8HBanHZxhOACcS2n82HhyS7T6NJuXdm # kfFynOlLAlKnN36TU6w7HQhJD5TNOXrd/yVjmScsPT9rp/Fmw0HNT7ZAmyEhQNC3 # EyTN3B14OuSereU0cZLXJmvkOHOrpgFPvT87eK1MrfvElXvtCl8zOYdBeHo46Zzh # 3SP9HSjTx/no8Zhf+yvYfvJGnXUsHicsJttvFXseGYs2uJPU5vIXmVnKcPA3v5gA # 3yAWTyf7YGcWoWa63VXAOimGsJigK+2VQbc61RWYMbRiCQ8KvYHZE/6/pNHzV9m8 # BPqC3jLfBInwAM1dwvnQI38AC+R2AibZ8GV2QqYphwlHK+Z/GqSFD/yYlvZVVCsf # gPrA8g4r5db7qS9EFUrnEw4d2zc4GqEr9u3WfPwwggbAMIIEqKADAgECAhAMTWly # S5T6PCpKPSkHgD1aMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVTMRcwFQYD # VQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBH # NCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwHhcNMjIwOTIxMDAwMDAw # WhcNMzMxMTIxMjM1OTU5WjBGMQswCQYDVQQGEwJVUzERMA8GA1UEChMIRGlnaUNl # cnQxJDAiBgNVBAMTG0RpZ2lDZXJ0IFRpbWVzdGFtcCAyMDIyIC0gMjCCAiIwDQYJ # KoZIhvcNAQEBBQADggIPADCCAgoCggIBAM/spSY6xqnya7uNwQ2a26HoFIV0Mxom # rNAcVR4eNm28klUMYfSdCXc9FZYIL2tkpP0GgxbXkZI4HDEClvtysZc6Va8z7GGK # 6aYo25BjXL2JU+A6LYyHQq4mpOS7eHi5ehbhVsbAumRTuyoW51BIu4hpDIjG8b7g # L307scpTjUCDHufLckkoHkyAHoVW54Xt8mG8qjoHffarbuVm3eJc9S/tjdRNlYRo # 44DLannR0hCRRinrPibytIzNTLlmyLuqUDgN5YyUXRlav/V7QG5vFqianJVHhoV5 # PgxeZowaCiS+nKrSnLb3T254xCg/oxwPUAY3ugjZNaa1Htp4WB056PhMkRCWfk3h # 3cKtpX74LRsf7CtGGKMZ9jn39cFPcS6JAxGiS7uYv/pP5Hs27wZE5FX/NurlfDHn # 88JSxOYWe1p+pSVz28BqmSEtY+VZ9U0vkB8nt9KrFOU4ZodRCGv7U0M50GT6Vs/g # 9ArmFG1keLuY/ZTDcyHzL8IuINeBrNPxB9ThvdldS24xlCmL5kGkZZTAWOXlLimQ # prdhZPrZIGwYUWC6poEPCSVT8b876asHDmoHOWIZydaFfxPZjXnPYsXs4Xu5zGcT # B5rBeO3GiMiwbjJ5xwtZg43G7vUsfHuOy2SJ8bHEuOdTXl9V0n0ZKVkDTvpd6kVz # HIR+187i1Dp3AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/ # BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEE # AjALBglghkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8w # HQYDVR0OBBYEFGKK3tBh/I8xFO2XC809KpQU31KcMFoGA1UdHwRTMFEwT6BNoEuG # SWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQw # OTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQG # CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKG # TGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJT # QTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIB # AFWqKhrzRvN4Vzcw/HXjT9aFI/H8+ZU5myXm93KKmMN31GT8Ffs2wklRLHiIY1UJ # RjkA/GnUypsp+6M/wMkAmxMdsJiJ3HjyzXyFzVOdr2LiYWajFCpFh0qYQitQ/Bu1 # nggwCfrkLdcJiXn5CeaIzn0buGqim8FTYAnoo7id160fHLjsmEHw9g6A++T/350Q # p+sAul9Kjxo6UrTqvwlJFTU2WZoPVNKyG39+XgmtdlSKdG3K0gVnK3br/5iyJpU4 # GYhEFOUKWaJr5yI+RCHSPxzAm+18SLLYkgyRTzxmlK9dAlPrnuKe5NMfhgFknADC # 6Vp0dQ094XmIvxwBl8kZI4DXNlpflhaxYwzGRkA7zl011Fk+Q5oYrsPJy8P7mxNf # arXH4PMFw1nfJ2Ir3kHJU7n/NBBn9iYymHv+XEKUgZSCnawKi8ZLFUrTmJBFYDOA # 4CPe+AOk9kVH5c64A0JH6EE2cXet/aLol3ROLtoeHYxayB6a1cLwxiKoT5u92Bya # UcQvmvZfpyeXupYuhVfAYOd4Vn9q78KVmksRAsiCnMkaBXy6cbVOepls9Oie1FqY # yJ+/jbsYXEP10Cro4mLueATbvdH7WwqocH7wl4R44wgDXUcsY6glOJcB0j862uXl # 9uab3H4szP8XTE0AotjWAQ64i+7m4HJViSwnGWH2dwGMMYIFXTCCBVkCAQEwgYYw # cjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ # d3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVk # IElEIENvZGUgU2lnbmluZyBDQQIQBNXcH0jqydhSALrNmpsqpzANBglghkgBZQME # AgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEM # BgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqG # SIb3DQEJBDEiBCC8jF3evV7RGDDdvEQXStZQuurS4esMoWGLoVixZpHepTANBgkq # hkiG9w0BAQEFAASCAQAUZIBirq4GviBnMCMIRV41coanUxYIndkLaimt0PsVmcZx # tI+7dnXe+hjL/NfNvXqWUXSHQtkmhKsSNEhPeT2zVXcd42smGW28xQdubynwUd5a # A2hNO9tWCNqWG8R4VgC5DjFGQJwV0GbZkpNULng7gvs8DFIPFh0CErJkRre67tI0 # PTuXUj8vrShFUZw8HCvHo6hjvNRchawgcUatIR6eN6Y/Pq2O/JOwZNQEwpLT0xkx # 2QBcrPLsOT4CwODu66uzHXGlxioV78cDaxaKwEUEIWNx+AFBe/1Mvv7XZ8mmiOUW # UN310sEGA2o6CXC3AZ+R2knTHj7henP3ARVb1D5WoYIDIDCCAxwGCSqGSIb3DQEJ # BjGCAw0wggMJAgEBMHcwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0 # LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hB # MjU2IFRpbWVTdGFtcGluZyBDQQIQDE1pckuU+jwqSj0pB4A9WjANBglghkgBZQME # AgEFAKBpMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X # DTIzMDYyMTA5MDI1OVowLwYJKoZIhvcNAQkEMSIEIBj7H3JvEsBarJyd4PN8HR+3 # hlYx3qssGJVduL7KRVuRMA0GCSqGSIb3DQEBAQUABIICAJliIBVXGZq7QNNWf35Q # j42jnrfTgWs1DzA/LYOrRY83sWKeoum1HSicOuyOXRm3LY8rHicA+eNwXqIiWBn4 # L2jRFvl1IIsfc7QDLpPeKGc/4ht7owCw+dtHKr0Qz+AcUbdlTiutFyPrJZJwisgy # OfwlVSvoO9XC1eDQnK3fFbfbQJpu4vUJ75C/poNUuH97m/QjZXLzIqPQ3tQJwSd9 # kGF7ma1r8gdICypdtdmLhu+ZfNXEnWUxGVn4XzwRZu9XF4YZtg98gvIvSu8T8QJo # HC1FOZ1UhiorZDu5uj9+PpKZtcC0CJzcVk4c0YSVcykFe1CeMoP5wftdY6Pp57qr # HetA46j8YPlscq33aZ2O0bf4JRnTWpS1sdEdkudiGRUXf8EprpHZlA/W/1aqO+GN # gQuPaXcLWDjlbm9aiZX8n3IVVWLPzJ8qnuP0z3Ry7+J0IfEqxj8mC9hS10TJgHIE # YcGtoXdZkkBvzrdYSuyrsYQV2+IU7ZJ3fnkiAVKyZd1eM68bN68s+tO+mwbq0ouZ # U1lH8yQUxTTvfmTmxR2pf1h/REHnF/GfbMHK3usG6kk9DjSdgcruNIeaCamdvtzt # 45vr1k6ByE0HKLk7z1Z4wfwaegAkasFoTU99UTOM6ktEdKK5VTpNpfNIngamPOzp # bheIcKZ1a2/TVOZUVrfkC1HK # SIG # End signature block |