From 6fa9596f41539e8d5ba27aef5361c5e1c473423c Mon Sep 17 00:00:00 2001 From: AdminDroid <49208841+admindroid-community@users.noreply.github.com> Date: Thu, 20 Jun 2024 17:54:48 +0530 Subject: [PATCH 01/14] Create README.md --- Export OneDrive Urls and Usage Size/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Export OneDrive Urls and Usage Size/README.md diff --git a/Export OneDrive Urls and Usage Size/README.md b/Export OneDrive Urls and Usage Size/README.md new file mode 100644 index 0000000..5e61c25 --- /dev/null +++ b/Export OneDrive Urls and Usage Size/README.md @@ -0,0 +1,16 @@ +## Get All OneDrive Site URLs for Users Using PowerShell +Download the handy PowerShell script to quickly compile a list of all users' OneDrive URLs in your organization + +***Sample Output:*** + +The script exports an output CSV file that looks similar to the screenshot below. + +![OneDrive URLs and Size](https://m365scripts.com/wp-content/uploads/2024/06/List-OneDrive-URLs-and-Size-604x175.png?v=1718874429) + +## Microsoft 365 Reporting Tool by AdminDroid +Looking for more in-depth reporting? [AdminDroid Microsoft 365 reporting tool](https://admindroid.com/?src=GitHub) offers an extensive collection of over 1800 out-of-the-box reports and dashboards. It’s the perfect complement to your PowerShell scripts. + +*View more comprehensive OneDrive reports through AdminDroid: [https://demo.admindroid.com/#/1/11/reports/70021/1/20*](https://demo.admindroid.com/#/1/11/reports/70021/1/20)* + + + From 4f1f674f10a3c42d3ff2ccce878dc756a212bb15 Mon Sep 17 00:00:00 2001 From: AdminDroid <49208841+admindroid-community@users.noreply.github.com> Date: Thu, 20 Jun 2024 17:55:19 +0530 Subject: [PATCH 02/14] Export OneDrive Urls and Storage Size --- .../GetOneDriveSiteUrls.ps1 | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 Export OneDrive Urls and Usage Size/GetOneDriveSiteUrls.ps1 diff --git a/Export OneDrive Urls and Usage Size/GetOneDriveSiteUrls.ps1 b/Export OneDrive Urls and Usage Size/GetOneDriveSiteUrls.ps1 new file mode 100644 index 0000000..168797a --- /dev/null +++ b/Export OneDrive Urls and Usage Size/GetOneDriveSiteUrls.ps1 @@ -0,0 +1,101 @@ +<# +============================================================================================= +Name: Get OneDrive site urls and size using PowerShell +Version: 1.0 +Website: m365scripts.com + + +Script Highlights : +~~~~~~~~~~~~~~~~~ +1. The script fetches all the OneDrive URLs and their storage usage. +2. Exports report results into CSV format for easy access and analysis. +3. The script is scheduler friendly. I.e., You can pass the credential as parameters instead of saving inside the script. + +For detailed script execution: https://m365scripts.com/microsoft365/get-all-onedrive-site-urls-for-users-using-powershell/ +============================================================================================ +#> + + +param ( + [string] $UserName, + [string] $Password, + [string] $HostName + +) + +#Checks SharePointOnline module availability and connects the module +Function ConnectSPOService +{ + $SPOService = (Get-Module Microsoft.Online.SharePoint.PowerShell -ListAvailable).Name + if ($SPOService -eq $null) + { + Write-host "Important: SharePoint Online Management Shell module is unavailable. It is mandatory to have this module installed in the system to run the script successfully." + $confirm = Read-Host Are you sure you want to install module? [Y] Yes [N] No + if ($confirm -match "[Y]") + { + Write-host `n"Installing SharePoint Online Management Shell Module" + Install-Module -Name Microsoft.Online.SharePoint.PowerShell -Allowclobber -Repository PSGallery -Force -Scope CurrentUser + Import-Module Microsoft.Online.SharePoint.PowerShell -DisableNameChecking + } + else + { + Write-host "Exiting. `nNote: SharePoint Online Management Shell module must be available in your system to run the script." + Exit + } + } + + #Connecting to SharePoint Online PowerShell + if($HostName -eq "") + { + Write-Host SharePoint organization name is required.`nEg: Contoso for admin@Contoso.Onmicrosoft.com -ForegroundColor Yellow + $HostName= Read-Host "Please enter SharePoint organization name" + } + $ConnectionUrl = "https://$HostName-admin.sharepoint.com/" + Write-Host `n"Connecting SharePoint Online Management Shell..."`n + if (($UserName -ne "") -and ($Password -ne "") ) + { + $SecuredPassword = ConvertTo-SecureString -AsPlainText $Password -Force + $Credential = New-Object System.Management.Automation.PSCredential $UserName, $SecuredPassword + Connect-SPOService -Credential $Credential -Url $ConnectionUrl | Out-Null + } + else + { + Connect-SPOService -Url $ConnectionUrl | Out-Null + } + +} +ConnectSPOService +$Location=Get-Location +$ExportCSV="$Location\List_OneDriveURLs_$((Get-Date -format yyyy-MMM-dd-ddd` hh-mm` tt).ToString()).csv" +$Count=0 +Get-SPOSite -IncludePersonalSite $true -Limit all -Filter "Url -like '-my.sharepoint.com/personal/'" | foreach { + $Count++ + $UserName=$_.Title + $UPN=$_.Owner + $Url=$_.Url + Write-Progress -Activity "`n Processed OneDrive site count: $Count "`n" Currently processing site: $url" + $StorageSize=$_.StorageUsageCurrent + $LastContentModifiedDate=$_.LastContentModifiedDate + $StorageQuota=$_.StorageQuota + $Result=@{ 'Owner UPN'=$UPN;'OneDrive Url'=$url;'Storage Used Size (MB)'=$StorageSize;'Storage Quota (MB)'=$StorageQuota;'Status'=$Status;'Last Content Modified Date'=$LastContentModifiedDate} + $Results= New-Object PSObject -Property $Result + $Results | Select-Object 'OneDrive Url','Owner UPN','Storage Used Size (MB)','Storage Quota (MB)','Last Content Modified Date'| Export-Csv -Path $ExportCSV -Notype -Append } + + if((Test-Path -Path $ExportCSV) -eq "True") + { + Write-Host `nThe exported report contains $Count OneDrive sites. + Write-Host `nOneDrive sites report available in: -NoNewline -Foregroundcolor Yellow; Write-Host $ExportCSV + Write-Host `n~~ Script prepared by AdminDroid Community ~~`n -ForegroundColor Green + Write-Host "~~ Check out " -NoNewline -ForegroundColor Green; Write-Host "admindroid.com" -ForegroundColor Yellow -NoNewline; Write-Host " to get access to 1800+ Microsoft 365 reports. ~~" -ForegroundColor Green `n`n + $Prompt = New-Object -ComObject wscript.shell + $UserInput = $Prompt.popup("Do you want to open output file?",` + 0,"Open Output File",4) + If ($UserInput -eq 6) + { + Invoke-Item "$ExportCSV" + } + } + else + { + Write-Host No items found. + } \ No newline at end of file From 5136cdf929406d9ab6f3e200a8a6f7e949096e1e Mon Sep 17 00:00:00 2001 From: AdminDroid <49208841+admindroid-community@users.noreply.github.com> Date: Fri, 21 Jun 2024 18:44:15 +0530 Subject: [PATCH 03/14] Create README.md --- File Version History Report/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 File Version History Report/README.md diff --git a/File Version History Report/README.md b/File Version History Report/README.md new file mode 100644 index 0000000..7152b44 --- /dev/null +++ b/File Version History Report/README.md @@ -0,0 +1,15 @@ +## **Get File Version History Report** +Learn how to export SharePoint Online file version history report using PowerShell and keep track of all version changes. + +***Sample Output*** + +The exported SharePoint version size report looks like the screenshot below. + +![File version history report](https://o365reports.com/wp-content/uploads/2024/06/SharePoint-version-history-report-1024x177.png? v=1718880863) +## **Powerful Microsoft 365 Reporting Tool by AdminDroid** +If this script is helpful, you’ll love [AdminDroid’s Microsoft 365 reporting tool](https://admindroid.com/?src=GitHub)! With over 1800 pre-built reports and dashboards, it’s a fantastic way to dive deeper into your M365 data. + +*View more comprehensive SPO file version history reports through AdminDroid:* + +[*https://demo.admindroid.com/#/1/11/reports/22601/1/20*](https://demo.admindroid.com/#/1/11/reports/22601/1/20) + From 3fb8e0a29aad49560aab6e67a6b21dcf1f590d2d Mon Sep 17 00:00:00 2001 From: AdminDroid <49208841+admindroid-community@users.noreply.github.com> Date: Fri, 21 Jun 2024 18:44:38 +0530 Subject: [PATCH 04/14] File Version History Report --- .../GetFileVersionHistoryReport.ps1 | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 File Version History Report/GetFileVersionHistoryReport.ps1 diff --git a/File Version History Report/GetFileVersionHistoryReport.ps1 b/File Version History Report/GetFileVersionHistoryReport.ps1 new file mode 100644 index 0000000..926da7d --- /dev/null +++ b/File Version History Report/GetFileVersionHistoryReport.ps1 @@ -0,0 +1,215 @@ +<# +============================================================================================= +Name: Export SharePoint Online File Version History Report Using PowerShell +Version: 1.0 +website: o365reports.com + +~~~~~~~~~~~~~~~~~~ +Script Highlights: +~~~~~~~~~~~~~~~~~~ +1. Retrieves file version history for all documents in a site. +2. Exports file version history for a list of sites. +3. Exports version history information for files uploaded by a specific user. +4. Finds files with larger version history based on your input. +5. File size can be exported in preferred units such as MB, KB, B, and GB. +6. Automatically installs the PnP PowerShell module (if not installed already) upon your confirmation. +7. The script can be executed with an MFA-enabled account too. +8. Exports report results as a CSV file. +9. The script is scheduler friendly. +10. It can be executed with certificate-based authentication (CBA) too. + +For detailed Script execution: https://o365reports.com/2024/06/20/export-sharepoint-online-file-version-history-report-using-powershell/ +============================================================================================ +#> +Param +( + [Parameter(Mandatory = $false)] + [string]$AdminName, + [string]$Password, + [String] $ClientId, + [String] $CertificateThumbprint, + [String] $TenantName, + [string]$SiteUrl, + [int]$VersionCount = -1, + [string]$ImportCsv, + [string]$UserId, + [ValidateSet('GB', 'MB', 'KB', 'B')] + [string]$Unit +) +Function Installation-Module{ + $Module = Get-InstalledModule -Name PnP.PowerShell -RequiredVersion 1.12.0 -ErrorAction SilentlyContinue + If($Module -eq $null){ + Write-Host PnP PowerShell Module is not available -ForegroundColor Yellow + $Confirm = Read-Host Are you sure you want to install module? [Yy] Yes [Nn] No + If($Confirm -match "[yY]") { + Write-Host "Installing PnP PowerShell module..." + Install-Module PnP.PowerShell -RequiredVersion 1.12.0 -Force -AllowClobber -Scope CurrentUser + Import-Module -Name Pnp.Powershell -RequiredVersion 1.12.0 + } + Else{ + Write-Host PnP PowerShell module is required to connect SharePoint Online.Please install module using Install-Module PnP.PowerShell cmdlet. + Exit + } + } + Write-Host `nConnecting to SharePoint Online... +} + + +Function Connection-Module{ + param + ( + [Parameter(Mandatory = $true)] + [String] $Url + ) + if(($AdminName -ne "") -and ($Password -ne "")) + { + $SecuredPassword = ConvertTo-SecureString -AsPlainText $Password -Force + $Credential = New-Object System.Management.Automation.PSCredential $AdminName,$SecuredPassword + Connect-PnPOnline -Url $Url -Credential $Credential + } + elseif($TenantName -ne "" -and $ClientId -ne "" -and $CertificateThumbprint -ne "") + { + Connect-PnPOnline -Url $Url -ClientId $ClientId -Thumbprint $CertificateThumbprint -Tenant "$TenantName.onmicrosoft.com" + } + else + { + Connect-PnPOnline -Url $Url -interactive + + } +} +Function Convert-Bytes { + $Bytes = switch ($Unit.ToUpper()) { + 'GB' { 1GB } + 'MB' { 1MB } + 'KB' { 1KB } + 'B' { 1 } + Default { + Write-Host "Invalid unit is provided. csv will display file sizes in megabyte" + 1MB + } + } + return $Bytes +} +Function HumanReadableByteSize ($Size) { + Switch ($Size) { + {$_ -gt 1TB} {$Size = ($Size / 1TB).ToString("n2") + " TB";break} + {$_ -gt 1GB} {$Size = ($Size / 1GB).ToString("n2") + " GB";break} + {$_ -gt 1MB} {$Size = ($Size / 1MB).ToString("n2") + " MB";break} + {$_ -gt 1KB} {$Size = ($Size / 1KB).ToString("n2") + " KB";break} + default {$Size = "$Size B"} + } +Return $Size +} +Function Store-FileVersionInformation($FileItem){ + write-host $fileItem.FieldValues.FileRef -f Green + If($Versions.Count -ge $VersionCount){ + $VersionSize = $Versions | Measure-Object -Property Size -Sum | Select-Object -expand Sum + $VersionSize = [Math]::Round(($VersionSize/$bytes),2) + $FileSize = [Math]::Round(($FileItem.FieldValues.File_x0020_Size/$Bytes),2) + $TotalFileSize = $FileSize + $VersionSize + + $FileVersionData = [PSCustomObject][Ordered]@{ + "Site" = $Site + "Library" = $List.Title + "File Name" = $FileItem.FieldValues.FileLeafRef + "File URL" = $File.ServerRelativeUrl + "Major Versions" = $File.MajorVersion + "Minor Versions" = $file.MinorVersion + "Versions Count" = $Versions.Count + "File Size" = HumanReadableByteSize ($FileSize*$Bytes) + "Version History Size" = HumanReadableByteSize ($VersionSize*$Bytes) + "Total File Size" = HumanReadableByteSize ($TotalFileSize*$Bytes) + "File Size ($unit)" = $FileSize + "Version History Size ($unit) " = $VersionSize + "Total File Size ($unit)" = $TotalFileSize + "Created By" = $FileItem.FieldValues.Author.Email + "Created Date" = $File.TimeCreated + "Modified By" = $FileItem.FieldValues.Editor.Email + "Last Modified" = $file.TimeLastModified + + } + $FileVersionData | Export-Csv -Path $ReportOutput -NoTypeInformation -Append -force + $Global:ItemCount++ + } +} +Function Get-SiteFileVersion($DocumentLibraries){ + ForEach($List in $DocumentLibraries) + { + $Files = Get-PnPListItem -List $List -PageSize 2000 -Fields File_x0020_Size, FileRef, Author | Where {$_.FileSystemObjectType -eq "File"} + Foreach($FileItem in $Files){ + Write-Progress -Activity ("Site : "+$Site +" | List : "+$List.Title) -Status ("Processing Item: "+$FileItem.FieldValues.FileLeafRef) + $File = Get-PnPProperty -ClientObject $FileItem -Property File + $Versions = Get-PnPProperty -ClientObject $File -Property Versions + If($UserId -ne "" -and $UserId -eq $FileItem.FieldValues.Author.Email){ + Store-FileVersionInformation $FileItem + } + ElseIf($UserId -eq ""){ + Store-FileVersionInformation $FileItem + } + } + } +} + +If($Unit -ne ""){ + $Bytes = Convert-Bytes +} +else{ + Write-Host -f cyan "Note: By default file sizes will be shown in MB." + Write-Host -f cyan "You can specify the unit[B, KB, MB, GB] by passing the desired unit using '-Unit' argument." + $Bytes = 1MB + $Unit = "MB" +} +Installation-Module + +$timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss" +$ReportOutput = "$PSScriptRoot\SPO File Version History Report $timestamp.csv" +$Global:ItemCount = 0 + +If($ImportCsv -ne ""){ + $ListOfSites = Import-csv -Path $ImportCsv + Foreach($Site in $ListOfSites){ + Connection-Module -Url $Site.SiteUrl + $Site = (Get-PnPWeb | Select Title).Title + $ExcludedLists = @("Form Templates","Style Library","Site Assets","Site Pages", "Preservation Hold Library", "Pages", "Images", + "Site Collection Documents", "Site Collection Images") + $DocumentLibraries = Get-PnPList | Where-Object {$_.Hidden -eq $False -and $_.Title -notin $ExcludedLists -and $_.BaseType -eq "DocumentLibrary"} + Get-SiteFileVersion $DocumentLibraries + Disconnect-PnPOnline + } + +} +Else{ + If($SiteUrl -eq ""){ + $SiteUrl = Read-Host "Site Url " + } + + Connection-Module -Url $SiteUrl + $Site = (Get-PnPWeb | Select Title).Title + $ExcludedLists = @("Form Templates","Style Library","Site Assets","Site Pages", "Preservation Hold Library", "Pages", "Images", + "Site Collection Documents", "Site Collection Images") + $DocumentLibraries = Get-PnPList | Where-Object {$_.Hidden -eq $False -and $_.Title -notin $ExcludedLists -and $_.BaseType -eq "DocumentLibrary"} + Get-SiteFileVersion $DocumentLibraries + Disconnect-PnPOnline +} + + +if((Test-Path -Path $ReportOutput) -eq "True") +{ + Write-Host `nThe output file contains $Global:ItemCount files + Write-Host `n The Output file availble in: -NoNewline -ForegroundColor Yellow + Write-Host $OutputCSV + Write-Host `n~~ Script prepared by AdminDroid Community ~~`n -ForegroundColor Green + Write-Host "~~ Check out " -NoNewline -ForegroundColor Green; Write-Host "admindroid.com" -ForegroundColor Yellow -NoNewline; Write-Host " to get access to 1800+ Microsoft 365 reports. ~~" -ForegroundColor Green `n`n + $Prompt = New-Object -ComObject wscript.shell + $UserInput = $Prompt.popup("Do you want to open output file?",` + 0,"Open Output File",4) + If ($UserInput -eq 6) + { + Invoke-Item "$ReportOutput" + } +} +else{ + Write-Host -f Yellow "No Records Found" +} + + From dfafca6f9a0cb086848c99a53140bec1e14e66e4 Mon Sep 17 00:00:00 2001 From: AdminDroid <49208841+admindroid-community@users.noreply.github.com> Date: Fri, 21 Jun 2024 18:49:44 +0530 Subject: [PATCH 05/14] Get File Version History Report --- File Version History Report/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/File Version History Report/README.md b/File Version History Report/README.md index 7152b44..9ada14c 100644 --- a/File Version History Report/README.md +++ b/File Version History Report/README.md @@ -5,7 +5,7 @@ Learn how to export SharePoint Online file version history report using PowerShe The exported SharePoint version size report looks like the screenshot below. -![File version history report](https://o365reports.com/wp-content/uploads/2024/06/SharePoint-version-history-report-1024x177.png? v=1718880863) +![File version history report](https://o365reports.com/wp-content/uploads/2024/06/SharePoint-version-history-report-1024x177.png?v=1718880863) ## **Powerful Microsoft 365 Reporting Tool by AdminDroid** If this script is helpful, you’ll love [AdminDroid’s Microsoft 365 reporting tool](https://admindroid.com/?src=GitHub)! With over 1800 pre-built reports and dashboards, it’s a fantastic way to dive deeper into your M365 data. From 36d5121c2a2d2242ba957d211df4e7ea7643b7cc Mon Sep 17 00:00:00 2001 From: AdminDroid <49208841+admindroid-community@users.noreply.github.com> Date: Tue, 25 Jun 2024 16:24:21 +0530 Subject: [PATCH 06/14] Microsoft 365 License Cost and Usage Report scope update --- .../M365LicenseCostReport.ps1 | 60 +++++++++---------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/M365 License Cost & Usage Report/M365LicenseCostReport.ps1 b/M365 License Cost & Usage Report/M365LicenseCostReport.ps1 index 8ff4f4f..76d2b2f 100644 --- a/M365 License Cost & Usage Report/M365LicenseCostReport.ps1 +++ b/M365 License Cost & Usage Report/M365LicenseCostReport.ps1 @@ -1,8 +1,8 @@ <# ============================================================================================= -Name : Export Microsoft 365 License Cost Report Using PowerShell -Version : 1.0 +Name : Export Microsoft 365 License Cost & Usage Report Using PowerShell +Version : 1.1 website : o365reports.com ----------------- @@ -82,7 +82,7 @@ function ConnectMgGraph{ #Connect to MgGraph. else { - Connect-MgGraph -Scopes "User.Read.All","UserAuthenticationMethod.Read.All " -NoWelcome + Connect-MgGraph -Scopes "User.Read.All","AuditLog.Read.All","Directory.Read.All" -NoWelcome if( (Get-MgContext) -ne $null ) { Write-Host "Connected to Microsoft Graph PowerShell using" (Get-MgContext).Account "account.`n" -ForegroundColor Green @@ -106,7 +106,8 @@ function LicenceUsageReport $TotalUnusedUnitsCost=0 #result path for Organization license report. - $Global:organizationLicenseResultPath= "$PSScriptRoot\LicenseUsageReport "+$DateTime+".csv" + $Location=Get-Location + $Global:organizationLicenseResultPath= "$Location\LicenseUsageReport "+$DateTime+".csv" #Get all the license used by the organization. Get-MgBetaSubscribedSku | Select-Object SkuId , ConsumedUnits , @{Name="PurchasedUnits"; Expression={$_.PrepaidUnits.Enabled} } | @@ -155,7 +156,7 @@ function LicenceUsageReport $OrganizationLicenseTotalCostObject = New-Object PSObject -Property $OrganizationLicenseTotalCost $OrganizationLicenseTotalCostObject| Select-object 'License Name','Cost','Purchased Units','Consumed Units','Unused Units','Purchased Units Cost','Consumed Units Cost','Unused Units Cost','SkuID' | Export-csv -path $Global:organizationLicenseResultPath -NoType -Append -Force - Write-Host "`nLicense usage summary report is stored in $Global:organizationLicenseResultPath .`n" -ForegroundColor Green + } #Funtion to process the data and export. @@ -167,7 +168,7 @@ function LicensedUserExport [object] $User ) - $Global:UserLicenseResultPath= "$PSScriptRoot\UsersLicenseCostReport "+$DateTime+".csv" + $Global:UserLicenseResultPath= "$Location\UsersLicenseCostReport "+$DateTime+".csv" #SignInDateTime $LastSignInDateTime=if($User.SignInActivity.LastSignInDateTime) @@ -377,7 +378,7 @@ function LicensedUserExport #Function to Licensed Users. function AllLicensedUserReport { - Write-Host "Processing Users...." + Write-Host "Generating license cost report...." $Global:Count = 0 #Get all Licensed users. @@ -387,16 +388,7 @@ function AllLicensedUserReport $UserPrincipalName=$_.UserPrincipalName LicensedUserExport -AssignedLicenses $_.AssignedLicenses -UserPrincipalName $UserPrincipalName -User $_ } - #Check The file exist or not. - if(Test-Path $Global:UserLicenseResultPath –PathType Leaf) - { - Write-Host "`nAll Licensed User cost report is stored in $Global:UserLicenseResultPath ." -ForegroundColor Green - } - else - { - Write-Host "There are no users matching the specified filters" - } } @@ -411,7 +403,7 @@ function SelectedUserReport Return } - Write-Host "Processing Selected Users....`n" + Write-Host "Generating license cost report....`n" $Global:Count = 0 #Import the UserId from the given CSV file. @@ -425,15 +417,7 @@ function SelectedUserReport } } - #Check The file exist or not. - if(Test-Path $Global:UserLicenseResultPath –PathType Leaf) - { - Write-Host "Selected user license cost report is stored in $Global:UserLicenseResultPath .`n" -ForegroundColor Green - } - else - { - Write-Host "There are no users matching the specified filters" - } + } #--------------------------------------------------------------------------------Main Function Starts--------------------------------------------------------------------------------# @@ -473,23 +457,35 @@ else $Prompt = New-Object -ComObject wscript.shell #Check The $Global:UserLicenseResultPath path and open file. -if(Test-Path $Global:UserLicenseResultPath –PathType Leaf) -{ - $UserInput = $Prompt.popup("Do you want to open LicenseUsageReport and UserLicenseCostReport CSV files?",` 0,"Open Output Files",4) +if(((Test-Path $Global:UserLicenseResultPath) -eq "True") -and ((Test-Path $Global:organizationLicenseResultPath) -eq "True")) +{ + Write-Host "`nDetailed license usage and users' license cost reports are stored in: $Location" -ForegroundColor Cyan + $UserInput = $Prompt.popup("Do you want to open output files?",` 0,"Open Output Files",4) If ($UserInput -eq 6) { Invoke-Item "$Global:organizationLicenseResultPath" ,"$Global:UserLicenseResultPath" - } + } + } -else +elseif (Test-Path $Global:organizationLicenseResultPath -eq "True") { - $UserInput = $Prompt.popup("Do you want to open LicenseUsageReport CSV file?",` 0,"Open Output File",4) + Write-Host "`nDetailed license usage & cost report stored in: $Location" -ForegroundColor Cyan + $UserInput = $Prompt.popup("Do you want to open output file?",` 0,"Open Output File",4) If ($UserInput -eq 6) { Invoke-Item "$Global:organizationLicenseResultPath" } + } +else +{ + Write-Host "There are no users matching the specified filters" +} + +Write-Host `n~~ Script prepared by AdminDroid Community ~~`n -ForegroundColor Green +Write-Host "~~ Check out " -NoNewline -ForegroundColor Green; Write-Host "admindroid.com" -ForegroundColor Yellow -NoNewline; Write-Host " to get access to 1800+ Microsoft 365 reports. ~~" -ForegroundColor Green `n`n + Disconnect-MgGraph | Out-Null From 11e743f86ed9d3cc120f3ad1e19bd12a7970e5c2 Mon Sep 17 00:00:00 2001 From: AdminDroid <49208841+admindroid-community@users.noreply.github.com> Date: Tue, 25 Jun 2024 16:27:54 +0530 Subject: [PATCH 07/14] Create README.md --- SPO Document Library Report/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 SPO Document Library Report/README.md diff --git a/SPO Document Library Report/README.md b/SPO Document Library Report/README.md new file mode 100644 index 0000000..a4a6473 --- /dev/null +++ b/SPO Document Library Report/README.md @@ -0,0 +1,12 @@ +## SharePoint Online Document Library Report +Get quick report with a list of all document library in SharePoint Online, including size, files & folder count, etc., to optimize storage. + +***Sample Output*** + +![SharePoint Online document library report](https://o365reports.com/wp-content/uploads/2024/06/SharePoint-document-library-report-Sample-output-300x65.png) +## Microsoft 365 Reporting Tool by AdminDroid +Seeking more in-depth reporting? [AdminDroid Microsoft 365 reporting tool](https://admindroid.com/?src=GitHub) provides 1800+ comprehensive reports and 30+ outstanding dashboards to better enhance your Microsoft 365 management effectively. + +*View detailed SharePoint document library reports in AdminDroid:* +** + From 5f5bfb5dad2c4fcedab3df726d4657569fb5666d Mon Sep 17 00:00:00 2001 From: AdminDroid <49208841+admindroid-community@users.noreply.github.com> Date: Tue, 25 Jun 2024 16:28:11 +0530 Subject: [PATCH 08/14] SPO Document Library Report --- .../DocumentLibraryReport.ps1 | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 SPO Document Library Report/DocumentLibraryReport.ps1 diff --git a/SPO Document Library Report/DocumentLibraryReport.ps1 b/SPO Document Library Report/DocumentLibraryReport.ps1 new file mode 100644 index 0000000..475dd3c --- /dev/null +++ b/SPO Document Library Report/DocumentLibraryReport.ps1 @@ -0,0 +1,191 @@ +<#------------------------------------------------------------------------------------------------------------------------------------ +Name: Export a List of all Document Library in SPO Using PowerShell +Version: 1.0 +Website: o365reports.com + +~~~~~~~~~~~~~~~~~~ +Script Highlights: +~~~~~~~~~~~~~~~~~~ +1. The script automatically verifies and installs the PnP module (if not installed already) upon your confirmation. +2. Retrieves all the document libraries in SharePoint Online along with their details. +3. Provides a list of all document libraries in a single site. +4. Allows to get all document libraries and their details for multiple sites. +5. The script can be executed with an MFA-enabled account too. +6. The script supports Certificate-based authentication (CBA) too. +7. Exports the report results to a CSV file. +8. The script is scheduler friendly. + +For detailed script execution: https://o365rpeorts.com/2024/06/25/list-all-document-library-in-spo-using-powershell/ +----------------------------------------------------------------------------------------------------------------------------------- +#> +Param +( + [Parameter(Mandatory = $false)] + [String] $UserName , + [String] $Password , + [String] $ClientId, + [String] $CertificateThumbprint, + [String] $TenantName, #(Example : If your tenant name is 'contoso.com', then enter 'contoso' as a tenant name ) + [String] $SiteAddress, #(Enter the specific site URL that you want to retrieve the data from.) + [String] $SitesCsv +) +$PnPOnline = (Get-Module PnP.PowerShell -ListAvailable).Name +if($PnPOnline -eq $null) +{ + Write-Host "Important: SharePoint PnP PowerShell module is unavailable. It is mandatory to have this module installed in the system to run the script successfully." + $Confirm= Read-Host Are you sure you want to install module? [Y] Yes [N] No + if($Confirm -match "[yY]") + { + Write-Host "Installing SharePoint PnP PowerShell module..." -ForegroundColor Magenta + Install-Module PnP.Powershell -Repository PsGallery -Force -AllowClobber + Import-Module PnP.Powershell -Force + #Register a new Azure AD Application and Grant Access to the tenant + Register-PnPManagementShellAccess + } + else + { + Write-Host "Exiting. `nNote: SharePoint PnP PowerShell module must be available in your system to run the script" + Exit + } +} +#Connecting to SharePoint PnPPowerShellOnline module....... +Write-Host "Connecting to SharePoint PnPPowerShellOnline module..." -ForegroundColor Cyan +function Connect_SharePoint +{ + param + ( + [Parameter(Mandatory = $true)] + [String] $Url + ) + try + { + if(($UserName -ne "") -and ($Password -ne "") -and ($TenantName -ne "")) + { + $SecuredPassword = ConvertTo-SecureString -AsPlainText $Password -Force + $Credential = New-Object System.Management.Automation.PSCredential $UserName,$SecuredPassword + Connect-PnPOnline -Url $Url -Credential $Credential + } + elseif($TenantName -ne "" -and $ClientId -ne "" -and $CertificateThumbprint -ne "") + { + Connect-PnPOnline -Url $Url -ClientId $ClientId -Thumbprint $CertificateThumbprint -Tenant "$TenantName.onmicrosoft.com" + } + else + { + Connect-PnPOnline -Url $Url -Interactive + } + } + catch + { + Write-Host "Error occured $($Url) : $_.Exception.Message" -Foreground Red; + } +} +if($TenantName -eq "") +{ + $TenantName = Read-Host "Enter your Tenant Name to Connect to SharePoint Online (Example : If your tenant name is 'contoso.com', then enter 'contoso' as a tenant name ) " +} + +$AdminUrl = "https://$TenantName.sharepoint.com" +connect_sharepoint -Url $AdminUrl +$Location=Get-Location +$OutputCSV = "$Location\SPO Document Library Report " + ((Get-Date -format "MMM-dd hh-mm-ss tt").ToString()) + ".csv" + +#Converting to the nearest Unit of size +function Convert_ToNearestUnit { + param ( + [long]$LibrarySizeInBytes + ) + if ($LibrarySizeInBytes -eq 0) { + return "0 Bytes" + } + $Units = ("Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") + $NearestIndex = [Math]::Min([Math]::Floor([Math]::Log($LibrarySizeInBytes, 1024)), $Units.Count - 1) + $SizeInNearestUnit = [Math]::Round($LibrarySizeInBytes / [Math]::Pow(1024, $NearestIndex), 2) + $Unit = $Units[$NearestIndex] + return "$sizeInNearestUnit $Unit" +} +#Collecting Document Reports +function Get_Statistics +{ + param + ( + [String] $SiteUrl, + [String] $SiteTitle + ) + Get-PnPList | Where-Object {$_.BaseType -eq "DocumentLibrary" -and $_.Hidden -eq $false} | ForEach-Object{ + if($_.Title -ne "Form Templates" -and $_.Title -ne "Style Library") + { + $LibrarySize = Get-PnPFolderStorageMetric -List $_.Title | Select TotalSize,TotalFileCount + $FolderCount = $_.ItemCount - $LibrarySize.TotalFileCount + $FilesCount = $LibrarySize.TotalFileCount + $LibrarySizeInBytes = $LibrarySize.TotalSize + $LibrarySize = Convert_ToNearestUnit -LibrarySizeInBytes $LibrarySizeInBytes + $ExportResult = @{ + "Document Library Name" = $_.Title; + "Document Library Url" = $AdminUrl+$_.DefaultViewUrl; + "Created On" = $_.Created; + "Site Url" = $SiteUrl; + "Site Name" = if ($SiteTitle) {$SiteTitle} else { "-" }; + "Library Size(Bytes)" = $LibrarySizeInBytes; + "Folders Count" = $FolderCount; + "Files Count" = $FilesCount; + "Library Size" = $LibrarySize; + } + $ExportResult = New-Object PSObject -Property $ExportResult + #Exporting data in to Csv + $ExportResult | Select-Object "Site Name","Site Url","Document Library Name","Document Library Url","Created On","Library Size","Library Size(Bytes)","Folders Count","Files Count" | Export-Csv -path $OutputCSV -Append -NoTypeInformation + + } + } +} + +#Retriving the data for site presesent in the tenant +if($SiteAddress -ne "") +{ + Connect_SharePoint -Url $SiteAddress + $Web = Get-PnPWeb | Select Title,Url + Get_Statistics -SiteUrl $Web.Url -SiteTitle $Web.Title +} +#Retriving data for specified sites present in the tenant +elseif($SitesCsv -ne "") +{ + try + { + Import-Csv -path $SitesCsv | ForEach-Object{ + Write-Progress -activity "Processing $($_.SitesUrl)" + Connect_Sharepoint -Url $_.SitesUrl + $Web = Get-PnPWeb | Select Url,Title + Get_Statistics -Objecttype $ObjectType -SiteUrl $Web.Url -SiteTitle $Web.Title + } + } + catch + { + Write-Host "Error occured : $_" -Foreground Red; + } +} +#Retriving data from all sites present in the tenant +else +{ + Get-PnPTenantSite | Select Url,Title | ForEach-Object{ + Write-Progress -activity "Processing $($_.Url)" + Connect_SharePoint -Url $_.Url + Get_Statistics -SiteUrl $_.Url -SiteTitle $_.Title + } +} + +#Open output file after execution +if((Test-Path -Path $OutputCSV) -eq "True") +{ + Write-Host `n "The Output file availble in:" -NoNewline -ForegroundColor Yellow; Write-Host "$OutputCSV" `n + Write-Host `n~~ Script prepared by AdminDroid Community ~~`n -ForegroundColor Green + Write-Host "~~ Check out " -NoNewline -ForegroundColor Green; Write-Host "admindroid.com" -ForegroundColor Yellow -NoNewline; Write-Host " to get access to 1800+ Microsoft 365 reports. ~~" -ForegroundColor Green `n`n + $Prompt = New-Object -ComObject wscript.shell + $UserInput = $Prompt.popup("Do you want to open output file?",` 0,"Open Output File", 4) + + If ($UserInput -eq 6) + { + Invoke-Item $OutputCSV + } +} + +#Disconnect the sharePoint PnPOnline module +Disconnect-PnPOnline \ No newline at end of file From 843498d22f9a873ac21eff1d618a87f900465551 Mon Sep 17 00:00:00 2001 From: AdminDroid <49208841+admindroid-community@users.noreply.github.com> Date: Tue, 25 Jun 2024 18:01:49 +0530 Subject: [PATCH 09/14] SPO Document Library Report --- SPO Document Library Report/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SPO Document Library Report/README.md b/SPO Document Library Report/README.md index a4a6473..f6c2ef3 100644 --- a/SPO Document Library Report/README.md +++ b/SPO Document Library Report/README.md @@ -3,10 +3,10 @@ Get quick report with a list of all document library in SharePoint Online, inclu ***Sample Output*** -![SharePoint Online document library report](https://o365reports.com/wp-content/uploads/2024/06/SharePoint-document-library-report-Sample-output-300x65.png) +![SharePoint Online document library report](https://o365reports.com/wp-content/uploads/2024/06/SharePoint-document-library-report-Sample-output.png) ## Microsoft 365 Reporting Tool by AdminDroid Seeking more in-depth reporting? [AdminDroid Microsoft 365 reporting tool](https://admindroid.com/?src=GitHub) provides 1800+ comprehensive reports and 30+ outstanding dashboards to better enhance your Microsoft 365 management effectively. *View detailed SharePoint document library reports in AdminDroid:* -** +** \ No newline at end of file From 96db8239f3cea6687dce9c903d9f3405b0282439 Mon Sep 17 00:00:00 2001 From: AdminDroid <49208841+admindroid-community@users.noreply.github.com> Date: Thu, 27 Jun 2024 15:33:23 +0530 Subject: [PATCH 10/14] Create README.md --- .../README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 Identify MFA Deployment Source for M365 Users/README.md diff --git a/Identify MFA Deployment Source for M365 Users/README.md b/Identify MFA Deployment Source for M365 Users/README.md new file mode 100644 index 0000000..47ad930 --- /dev/null +++ b/Identify MFA Deployment Source for M365 Users/README.md @@ -0,0 +1,15 @@ +## **Identity MFA Deployment Source in Microsoft 365 Using PowerShell** +Identify MFA deployment source methods whether it's per-user MFA, security defaults, or Conditional Access policies. + +***Sample Output:*** + +The script outputs CSV file detailing the MFA deployment source report of all users. + +![MFA Deployment Source Report](https://o365reports.com/wp-content/uploads/2024/06/Identify-MFA-Deployment-Source-Report-1024x420.png?v=1719384701) + +## **Microsoft 365 Reporting Tool by AdminDroid** +Looking to get more from your Microsoft 365 data? With [AdminDroid Microsoft 365 reporting tool](https://admindroid.com/?src=GitHub), you can! Explore over 1900+ pre-built reports and 20+ interactive dashboards to dive deep into your Microsoft 365 data. Experience the insights you've been missing! + +*View more comprehensive user sign-ins based on MFA enforcement sources through AdminDroid:* [*https://demo.admindroid.com/#/1/11/reports/20363/1/20*](https://demo.admindroid.com/#/1/11/reports/20363/1/20) + + From 380c02457111984534be80cb866206cb22bb9106 Mon Sep 17 00:00:00 2001 From: AdminDroid <49208841+admindroid-community@users.noreply.github.com> Date: Thu, 27 Jun 2024 15:34:07 +0530 Subject: [PATCH 11/14] Find how users get MFA, whether it's via per-user MFA, security defaults, or CA policies --- .../IdentifyMFADeploymentSourcesReport.ps1 | 467 ++++++++++++++++++ 1 file changed, 467 insertions(+) create mode 100644 Identify MFA Deployment Source for M365 Users/IdentifyMFADeploymentSourcesReport.ps1 diff --git a/Identify MFA Deployment Source for M365 Users/IdentifyMFADeploymentSourcesReport.ps1 b/Identify MFA Deployment Source for M365 Users/IdentifyMFADeploymentSourcesReport.ps1 new file mode 100644 index 0000000..a0c4e6c --- /dev/null +++ b/Identify MFA Deployment Source for M365 Users/IdentifyMFADeploymentSourcesReport.ps1 @@ -0,0 +1,467 @@ +<# +============================================================================================= +Name: Identify MFA Deployment Sources in Microsoft 365 Using PowerShell +Version: 1.0 +website: o365reports.com + +~~~~~~~~~~~~~~~~~~ +Script Highlights: +~~~~~~~~~~~~~~~~~~ +1. The script identifies and exports MFA enforcement sources for all users. +2. Helps you understand user MFA registration status (registered or not) to plan your MFA rollout campaigns efficiently. +3. It specifically identifies MFA sources for external users as well. +4. The script checks which Conditional Access policies demand MFA and tells you if users have registered for that MFA method as required by those policies. +5. Automatically install the missing required modules (Microsoft Graph Beta, MSOnline) with your confirmation. +6. The script can be executed with an MFA-enabled account too.  +7. Exports report results as a CSV file.  +8. The script is scheduler-friendly, making it easy to automate. +9. It supports certificate-based authentication (CBA) too. + +For detailed Script execution: : https://o365reports.com/2024/06/26/identity-mfa-deployment-source-in-microsoft-365-using-powershell/ +============================================================================================ +#> +param +( + [string]$TenantId, + [string]$AppId, + [string]$CertificateThumbprint, + [string]$UserName, + [string]$Password +) + +$ErrorActionPreference = "Stop" +#Check for Module Availability +function Check-ModuleAvailability +{ + param ( + [string]$ModuleName, + [string]$ModuleDisplayName + ) + $module = Get-Module $ModuleName -ListAvailable + if ($module -eq $null) + { + Write-Host "Important: $ModuleDisplayName module is unavailable. It is mandatory to have this module installed in the system to run the script successfully." + $confirm = Read-Host "Are you sure you want to install $ModuleDisplayName module? [Y] Yes [N] No" + if ($confirm -match "[yY]") + { + Write-Host "Installing $ModuleDisplayName module..." + Install-Module $ModuleName -Scope CurrentUser -AllowClobber + Write-Host "$ModuleDisplayName module is installed in the machine successfully" -ForegroundColor Magenta + } + else + { + Write-Host "Exiting. `nNote: $ModuleDisplayName module must be available in your system to run the script" -ForegroundColor Red + Exit + } + } +} + +function Process-ExternalUsers +{ + param( + [System.Object]$ExternalTenantUser, + [hashtable] $UsersinTenant, + [System.Array] $B2BGuest, + [System.Array] $B2BMember, + [System.Array] $LocalGuest, + [System.Array] $B2BDirectConnect + ) + $processedUsers = @() + if ($ExternalTenantUser) + { + $Members = $ExternalTenantUser.ExternalTenants.AdditionalProperties.members + if ($Members) + { + foreach ($Member in $Members) + { + if ($UsersinTenant.ContainsKey($Member)) + { + $processedUsers += $ExternalTenantUser.GuestOrExternalUserTypes -split ',' | ForEach-Object { + switch -Wildcard ($_) + { + 'b2bCollaborationGuest' { + $B2BGuest | Where-Object { $_ -in $UsersinTenant[$Member] } + } + 'b2bCollaborationMember' { + $B2BMember | Where-Object { $_ -in $UsersinTenant[$Member] } + } + 'internalGuest' { + $LocalGuest + } + 'b2bDirectConnectUser' { + $B2BDirectConnect | Select-Object -Unique | Where-Object { $_.Id -in $UsersinTenant[$Member] } + } + } + } + } + } + } + else + { + $processedUsers += $ExternalTenantUser.GuestOrExternalUserTypes -split ',' | ForEach-Object { + switch -Wildcard ($_) { + 'b2bCollaborationGuest' { + $B2BGuest + } + 'b2bCollaborationMember' { + $B2BMember + } + 'internalGuest' { + $LocalGuest + } + 'b2bDirectConnectUser' { + $B2BDirectConnect | Select-Object -Unique + } + } + } + } + } + + return $processedUsers +} + +#Function to get the members of the roles +function Get-UserIdsByRole { + param ( + [array]$Roles, + [array]$DirectoryRole + ) + + $UserIds = @() + + foreach ($Role in $Roles) { + $DirRole = $DirectoryRole | Where-Object { $_.RoleTemplateId -eq $Role } + if ($DirRole) { + $RoleMembers = Get-MgBetaDirectoryRoleMember -DirectoryRoleId $DirRole.Id + if ($RoleMembers) { + $UserIds += $RoleMembers.Id + } + } + } + + return $UserIds +} + +# Check for Module Availabilit +Check-ModuleAvailability -ModuleName "MsOnline" -ModuleDisplayName "MsOnline" +Check-ModuleAvailability -ModuleName "Microsoft.Graph.Beta" -ModuleDisplayName "Microsoft Graph Beta" + +#Disconnect from the Microsoft Graph If already connected +if (Get-MgContext) { + Write-Host Disconnecting from the previous sesssion.... -ForegroundColor Yellow + Disconnect-MgGraph | Out-Null +} + + +#Credentials check and Certificate check +if(($UserName -ne "") -and ($Password -ne "")) +{ + $SecuredPassword = ConvertTo-SecureString -AsPlainText $Password -Force + $Credential = New-Object System.Management.Automation.PSCredential $UserName,$SecuredPassword + $CredentialPassed = $true +} + +if((($AppId -ne "") -and ($CertificateThumbPrint -ne "")) -and ($TenantId -ne "")) +{ + $CBA=$true +} + +#version check +# Connecting to Microsoft Graph +Write-Host "Connecting to Microsoft Graph..." +try +{ + if($CBA -eq $true) + { + Connect-MgGraph -ApplicationId $AppId -TenantId $TenantId -CertificateThumbPrint $CertificateThumbprint + Write-Host Connected to Microsoft Graph PowerShell using (Get-MgContext).AppName application -ForegroundColor Yellow + } + else + { + Connect-MgGraph -Scopes 'User.Read.All','Policy.Read.all','Policy.ReadWrite.SecurityDefaults','Team.ReadBasic.All','Directory.Read.All' -NoWelcome + Write-Host Connected to Microsoft Graph PowerShell using (Get-MgContext).Account account -ForegroundColor Yellow + } +} +catch [Exception] +{ + Write-Host "Error: An error occurred while connecting to Microsoft Graph: $($_.Exception.Message)" -ForegroundColor Red + Exit +} + +#Connecting to Msonline +Write-Host "Connecting to MSOnline......" +try +{ + if($CredentialPassed -eq $true) + { + Connect-MsolService -Credential $Credential + } + elseif($CBA -eq $true) + { + Write-Host "MSonline module doesn't support certificate based authentication. Please enter the credential in the prompt" + Connect-MsolService + } + else + { + Connect-MsolService + } + Write-Host Connected to MSOnline -ForegroundColor Yellow +} +catch +{ + Write-Host "Error: An error occurred while connecting to MsOnline: $($_.Exception.Message)" -ForegroundColor Red + Exit +} + +#Get Users From Msonline +$Users = Get-MsolUser -All | Select-Object ObjectId , StrongAuthenticationRequirements +$MgBetaUsers = Get-MgBetaUser -All | Sort-Object DisplayName +#Check Security Default is enabled or not +$SecurityDefault = (Get-MgBetaPolicyIdentitySecurityDefaultEnforcementPolicy).IsEnabled +$DirectoryRole = Get-MgBetaDirectoryRole -All + +#Get the User Rgistration Details +$UserAuthenticationDetail = Get-MgBetaReportAuthenticationMethodUserRegistrationDetail -All | Select-Object UserPrincipalName, MethodsRegistered, IsMFARegistered , Id +$ProcessedUserCount =0 + +#check for Security default if disabled start to process the Conditional access policies +$PolicySetting = 'True' +$TotalUser = $Users.count +if($SecurityDefault) +{ + $PolicySetting = 'False' +} +else +{ + #Initialize the array + $IncludeId = @() + $ExcludeId = @() + $IncludeUsers = @() + $ExcludeUsers = @() + $Registered = @() + $NotRegistered = @() + $UsersInPolicy = @{} + # Get conditional access policies that involve MFA and enabled + $Policies = Get-MgBetaIdentityConditionalAccessPolicy -All | Where-Object { ($_.GrantControls.BuiltInControls -contains 'mfa' -or $_.GrantControls.AuthenticationStrength.RequirementsSatisfied -contains 'mfa') -and $_.State -contains 'enabled' } + $Policy = $Policies | Where-Object { $_.displayname -eq 'Authentication' } + $ProcessedPolicyCount = 0 + #Get the External users if it was specified in the policy + if($Policies.Conditions.Users.IncludeGuestsOrExternalUsers -ne $null -or $Policies.Conditions.Users.ExcludeGuestsOrExternalUsers -ne $null) + { + Write-Host "Getting Information about Exernal Users" + $ExternalUsers = $MgBetaUsers | where-object {$_.ExternalUserState -ne $null} + $UsersinTenant = @{} + foreach($GuestUser in $ExternalUsers) + { + try + { + if($GuestUser.othermails -ne $null) + { + $Parts = $GuestUser.othermails -split "@" + $DomainName = $Parts[1] + $Url = "https://login.microsoftonline.com/$DomainName/.well-known/openid-configuration" + $Response = Invoke-RestMethod -Uri $Url -Method Get + $Issuer = $Response.issuer + $TenantId = [regex]::Match($Issuer, "[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}").Value + if (-not $UsersinTenant.ContainsKey($TenantId)) + { + $UsersinTenant[$TenantId] = @($GuestUser.Id) + } + else + { + $UsersinTenant[$TenantId] += $GuestUser.Id + } + } + } + catch + { + Write-Host "External Domain Name $DomainName is Invalid" -ForegroundColor Red + continue; + } + } + $B2BGuest = $ExternalUsers | where-object {$_.UserType -eq 'Guest'} + $B2BMember = $ExternalUsers | where-object {$_.UserType -ne 'Member'} + $LocalGuest = $MgBetaUsers | where-object { $_.ExternalUserState -eq $null -and $_.UserType -eq 'Guest'} + + #B2B Direct connect + $Groups = Get-MgBetaTeam -All + $B2BDirectConnect = @() + ForEach($ExternalUser in $ExternalUsers) + { + $MemberOfs = Get-MgBetaUserMemberof -UserId $ExternalUser.Id | Where-Object {$_.Id -ne $null} + ForEach($MemberOf in $MemberOfs) + { + if($Groups.Id -contains $MemberOf.Id) + { + $B2BDirectConnect += $ExternalUser.Id + } + } + } + + } + + #Hash table ofthe Required Authentication Strength with respect to the Registered method + $AllowedCombinations = @{ + "mobilephone" = @("sms","Password,sms") + "alternateMobilePhone" = @("sms","Password,sms") + "officePhone" = @("sms","Password,sms") + "microsoftAuthenticatorPush" = @("microsoftAuthenticatorPush", "Password,microsoftAuthenticatorPush") + "softwareOneTimePasscode" = @("Password,SoftwareOath") + "MicrosoftAuthenticatorPzasswordless" = @("MicrosoftAuthenticator(PhoneSignIn)") + "windowsHelloForBusiness" = @("windowsHelloForBusiness") + "hardwareOneTimePasscode" = @("password,hardwareOath") + "passKeyDeviceBound" = @("fido2") + "passKeyDeviceBoundAuthenticator" = @("fido2") + "passKeyDeviceBoundWindowsHello" = @("fido2") + "fido2SecurityKey" = @("fido2") + "temporaryAccessPass" = @("TemporaryAccessPassOneTime", "TemporaryAccessPassMultiuse") + } + + Write-Host "Processing the policies..." + # Loop through each policy + foreach ( $Policy in $Policies) + { + $ProcessedPolicyCount++ + Write-Progress -Activity "`n Processed Policy count: $ProcessedPolicyCount `n" -Status "Currently processing Policy: $($Policy.DisplayName)" + ### Conditions ### + $IncludeUsers = $null + $ExcludeUsers = $null + $Check = $true + $CurrentPolicy = $false + $IncludedExternalUser = $Policy.Conditions.Users.IncludeGuestsOrExternalUsers + $ExcludedExternalUser = $Policy.Conditions.Users.ExcludeGuestsOrExternalUsers + $IncludeUsers = if($Policy.Conditions.Users.IncludeUsers -ne 'All') + { + $Policy.Conditions.Users.IncludeUsers + } + elseif($Policy.Conditions.Users.IncludeUsers -eq 'All') + { + $MgBetaUsers.Id + $Check = $false + } + if($Check) + { + $IncludeUsers += if($Policy.Conditions.Users.IncludeGroups){ $Policy.Conditions.Users.IncludeGroups | ForEach-Object { if ($Members = Get-MgBetaGroupMember -GroupId $_) { $Members.Id}if($Owner = Get-MgBetaGroupOwner -GroupId $_){$Owner.Id} } } + $IncludeUsers += Get-UserIdsByRole -Roles $Policy.Conditions.Users.IncludeRoles -DirectoryRole $DirectoryRole + $IncludeUsers += Process-ExternalUsers -ExternalTenantUser $IncludedExternalUser -UsersinTenant $UsersinTenant -B2BGuest $B2BGuest.Id -B2BMember $B2BMember.Id -LocalGuest $LocalGuest.Id -B2BDirectConnect $B2BDirectConnect + } + $ExcludeUsers = if($Policy.Conditions.Users.ExcludeUsers){$Policy.Conditions.Users.ExcludeUsers} + $ExcludeUsers += if($Policy.Conditions.Users.ExcludeGroups){$Policy.Conditions.Users.ExcludeGroups | ForEach-Object { if ($Members = Get-MgBetaGroupMember -GroupId $_) { $Members.Id} if($Owner = Get-MgBetaGroupOwner -GroupId $_){$Owner.Id}}} + $ExcludeUsers += Get-UserIdsByRole -Roles $Policy.Conditions.Users.ExcludeRoles -DirectoryRole $DirectoryRole + $ExcludeUsers += Process-ExternalUsers -ExternalTenantUser $ExcludedExternalUser -UsersinTenant $UsersinTenant -B2BGuest $B2BGuest.Id -B2BMember $B2BMember.Id -LocalGuest $LocalGuest.Id -B2BDirectConnect $B2BDirectConnect + $ExcludeId += $ExcludeUsers + $IncludeId += $IncludeUsers | Where-Object { $_ -notin $ExcludeUsers } + $UsersInPolicy[$Policy.DisplayName] += $IncludeUsers | Where-Object { $_ -notin $ExcludeUsers } + if ($Policy.GrantControls.AuthenticationStrength.RequirementsSatisfied -contains 'mfa') + { + $NotRegistered += $IncludeUsers| Where-Object { $_ -notin $ExcludeUsers } + $CurrentPolicy = $true + $Strength = $Policy.GrantControls.AuthenticationStrength.AllowedCombinations + foreach ($IncludeUser in $IncludeUsers) + { + $UserAuthDetails = $UserAuthenticationDetail | Where-Object { $_.Id -eq $IncludeUser } + $MethodsRegistered = if ($UserAuthDetails.MethodsRegistered -ne $null) { $UserAuthDetails.MethodsRegistered -split ',' } else {'None'} + foreach ($Method in $MethodsRegistered) + { + if ($AllowedCombinations.ContainsKey($Method)) + { + foreach($MFA in $AllowedCombinations[$Method]) + { + if($Strength -contains $MFA) + { + # Check if the user is included in any other policies with MFA strength + $Registered += $IncludeUser -join',' + } + } + } + } + } + } + $Registered = $Registered | Select-Object -Unique + if(!$CurrentPolicy) + { + $NotRegistered = $NotRegistered.GUID | Where-Object { $_ -notin $IncludeUsers.GUID } + } + $IncludedUsers = $IncludeId | Select-Object -Unique + } +} +$ProcessedUserCount = 0 +Write-Host "Processing the Users" +$FilePath = ".\MFA_Deployment_Sources_Report_$((Get-Date -format 'yyyy-MMM-dd-ddd hh-mm tt').ToString()).csv" + +#Now starts the Process of Checking various conditions for the users and Export to the .csv file in the D local storage +foreach ($User in $MgBetaUsers) +{ + $name = @() + $ProcessedUserCount++ + $percent = ($ProcessedUserCount/$TotalUser)*100 + Write-Progress -Activity "`n Processed user count: $ProcessedUserCount `n" -Status "Currently processing User: $($User.DisplayName)" -PercentComplete $percent + $Peruser = $Users | Where-Object {$_.ObjectID -contains $User.Id} + # Get user authentication details + $UserAuthDetails = $UserAuthenticationDetail | Where-Object { $_.UserPrincipalName -eq $User.UserPrincipalName } + $MethodsRegistered = if ($UserAuthDetails.MethodsRegistered -ne "") { $UserAuthDetails.MethodsRegistered -join ',' } else { 'None' } + $Name += foreach($Pol in $Policies.DisplayName){if($UsersInPolicy[$pol] -contains $user.Id){$Pol}} + $PolicyName = $Name -join',' + $MFAEnforce = @{ + 'User Display Name' = $User.DisplayName + 'User Principal Name' = $User.UserPrincipalName + 'MFA Enforced Via' = if($PerUser.StrongAuthenticationRequirements.State -eq 'Enforced' -and $PolicySetting -eq 'True' -and $IncludedUsers -contains $User.Id){'Per User MFA , Conditional Access Policy'} + elseif ( $PerUser.StrongAuthenticationRequirements.State -eq 'Enforced' -and $SecurityDefault -eq $true) { 'Per User MFA , Security Default' } + elseif ($PerUser.StrongAuthenticationRequirements.State -eq 'Enforced') { 'Per User MFA' } + elseif ($SecurityDefault -eq $true) { 'Security Default' } + elseif ($PolicySetting -eq 'True' -and $IncludedUsers -contains $User.Id){'Conditional Access Policy'}elseif ($Peruser.BlockCredential -eq $true) {'SignIn Blocked'}else {'Disabled'} + 'Is Registered MFA Supported in CA' = if($IncludedUsers -contains $User.Id){if($UserAuthDetails.IsMFARegistered -contains 'True'){if($PolicySetting -eq 'True' -and $NotRegistered -notcontains $User.Id) {$true}elseif($Registered -contains $User.Id){$true}else{'False'}}else{'False'}}else{''} + 'CA MFA Status' = if($IncludedUsers -contains $User.Id){'Enabled'}else{'Disabled'} + 'Assigned CA Policy' = if($IncludedUsers -contains $User.Id){$PolicyName}else{''} + 'Per User MFA Status' = if ($PerUser.StrongAuthenticationRequirements.State) { $PerUser.StrongAuthenticationRequirements.State } else { 'Disabled' } + 'Security Default Status' = if ($SecurityDefault -eq $false){'Disabled'} else{'Enabled'} + 'MFA Registered' = $UserAuthDetails.IsMFARegistered -contains 'True' + 'Methods Registered' = if($MethodsRegistered){$MethodsRegistered}else{'None'} + } + $MFAEnforced = New-Object PSObject -Property $MFAEnforce + try + { + $MFAEnforced | Select-Object 'User Display Name','User Principal Name','MFA Registered','Methods Registered','MFA Enforced Via','Per User MFA Status','Security Default Status','CA MFA Status','Assigned CA Policy','Is Registered MFA Supported in CA' | Export-Csv -Path $FilePath -NoTypeInformation -Append + } + catch + { + Write-Host "Error occurred While Exporting: $_" -ForegroundColor Red + } + } + + <# +$Prompt = New-Object -ComObject wscript.shell +$UserInput = $Prompt.popup("Do you want to open output file?",` 0,"Open Output File",4) +If ($UserInput -eq 6) +{ + Invoke-Item "$FilePath" +} + +Write-Host "Report was generated Successfully" +Write-Host "Total number of Users processed: $ProcessedUserCount" +#Disconnect from the Microsoft Graph +Disconnect-MgGraph | Out-Null +Write-Host `n~~ Script prepared by AdminDroid Community ~~`n -ForegroundColor Green +Write-Host "~~ Check out " -NoNewline -ForegroundColor Green; Write-Host "admindroid.com" -ForegroundColor Yellow -NoNewline; Write-Host " to get access to 1800+ Microsoft 365 reports. ~~" -ForegroundColor Green `n`n +#> + +#Open output file after execution +Write-Host `nScript executed successfully +if((Test-Path -Path $FilePath) -eq "True") +{ + Write-Host `n~~ Script prepared by AdminDroid Community ~~`n -ForegroundColor Green + Write-Host "~~ Check out " -NoNewline -ForegroundColor Green; Write-Host "admindroid.com" -ForegroundColor Yellow -NoNewline; Write-Host " to get access to 1800+ Microsoft 365 reports. ~~" -ForegroundColor Green `n`n + Write-Host "Exported report has $ProcessedUserCount user(s)" -ForegroundColor cyan + $Prompt = New-Object -ComObject wscript.shell + $UserInput = $Prompt.popup("Do you want to open output file?",` 0,"Open Output File",4) + if ($UserInput -eq 6) + { + Invoke-Item "$FilePath" + } + Write-Host "Detailed report available in: " -NoNewline -ForegroundColor Yellow + Write-Host $FilePath +} +else +{ + Write-Host "No user(s) found" -ForegroundColor Red +} \ No newline at end of file From 7498822a7936ed8abc6bc63f3b17a6ebbb9d50c4 Mon Sep 17 00:00:00 2001 From: AdminDroid <49208841+admindroid-community@users.noreply.github.com> Date: Wed, 3 Jul 2024 13:33:44 +0530 Subject: [PATCH 12/14] Create README.md --- Export Entra Sign-in Logs/README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 Export Entra Sign-in Logs/README.md diff --git a/Export Entra Sign-in Logs/README.md b/Export Entra Sign-in Logs/README.md new file mode 100644 index 0000000..8e1d645 --- /dev/null +++ b/Export Entra Sign-in Logs/README.md @@ -0,0 +1,23 @@ +## **Get Entra Sign-in Logs** + +Discover how to export comprehensive sign-in reports for Microsoft 365 +users using PowerShell. Enhance security with practical execution +examples for streamlined reporting. + +***Sample Output*** + +The exported report on Microsoft Entra ID sign-in logs looks like the +screenshot below. + +\![M365 User Sign-in +Report\](https://o365reports.com/wp-content/uploads/2024/07/Export-Microsoft-365-Users-Sign-in-Report-Using-PowerShell.png?v=1719913223) + +## **AdminDroid: Your Go-To Tool for Microsoft 365 Reporting** + +Need more than what this script offers? Explore [AdminDroid Microsoft +365 reporting tool](https://admindroid.com/?src=GitHub) to get access to +1800+ out-of-box M365 reports and insightful dashboards. + +*View more comprehensive M365 user sign-in reports through AdminDroid:* + +[*https://demo.admindroid.com/#/1/11/reports/20161/1/20*](https://demo.admindroid.com/#/1/11/reports/20161/1/20) From e0cb9e62b1b595878f215755d09009ec365dacb6 Mon Sep 17 00:00:00 2001 From: AdminDroid <49208841+admindroid-community@users.noreply.github.com> Date: Wed, 3 Jul 2024 13:34:47 +0530 Subject: [PATCH 13/14] Export Entra Sign-in Logs --- .../GetEntraSigninLogs.ps1 | 251 ++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 Export Entra Sign-in Logs/GetEntraSigninLogs.ps1 diff --git a/Export Entra Sign-in Logs/GetEntraSigninLogs.ps1 b/Export Entra Sign-in Logs/GetEntraSigninLogs.ps1 new file mode 100644 index 0000000..ba1fa9f --- /dev/null +++ b/Export Entra Sign-in Logs/GetEntraSigninLogs.ps1 @@ -0,0 +1,251 @@ +<# +============================================================================================= +Name: Export Microsoft 365 Users’ Sign-in Report Using PowerShell +Version: 1.0 +Website: o365reports.com + +~~~~~~~~~~~~~~~~~~ +Script Highlights: +~~~~~~~~~~~~~~~~~~ +1. Exports all Entra ID sign-in logs in user-friendly format. +2. Allows you to findout successful and failed sign-in attempts separately. +3. Filters interactive/non-interactive user sign-ins. +4. Helps export risky sign-ins alone. +5. Tracks guest users’ sign-in history. +6. Segments sign-in attempts based on Conditional Access applied & not applied. +7. Helps to monitor CA policy sign-in failures & success separately. +8. You can export the report to choose either ‘All users’ or ‘Specific user(s)’. +9. The script uses MS Graph PowerShell and installs MS Graph Beta PowerShell SDK (if not installed already) upon your confirmation. +10. The script can be executed with an MFA-enabled account too. +11. Exports report results as a CSV file. +12. The script is scheduler friendly. +13. It can be executed with certificate-based authentication (CBA) too. + +For detailed script execution:https://o365reports.com/2024/07/02/export-microsoft-365-users-sign-in-report-using-powershell/ + +============================================================================================ +#> +Param +( + [switch]$RiskySignInsOnly, + [switch]$GuestUserSignInsOnly, + [switch]$Success, + [switch]$Failure, + [switch]$InteractiveOnly, + [switch]$NonInteractiveOnly, + [switch]$CAPNotAppliedOnly, + [switch]$CAPAppliedOnly, + [switch]$CAPSuccessOnly, + [switch]$CAPFailedOnly, + [switch]$CreateSession, + [string[]]$UserPrincipalName, + [string]$TenantId, + [string]$ClientId, + [string]$CertificateThumbprint +) + +Function Connect_MgGraph +{ + #Check for module installation + $Module=Get-Module -Name microsoft.graph.beta -ListAvailable + if($Module.count -eq 0) + { + Write-Host Microsoft Graph PowerShell SDK is not available -ForegroundColor yellow + $Confirm= Read-Host Are you sure you want to install module? [Y] Yes [N] No + if($Confirm -match "[yY]") + { + Write-host "Installing Microsoft Graph PowerShell module..." + Install-Module Microsoft.Graph.beta -Repository PSGallery -Scope CurrentUser -AllowClobber -Force + } + else + { + Write-Host "Microsoft Graph Beta PowerShell module is required to run this script. Please install module using Install-Module Microsoft.Graph cmdlet." + Exit + } + } + #Disconnect Existing MgGraph session + if($CreateSession.IsPresent) + { + Disconnect-MgGraph + } + + + Write-Host Connecting to Microsoft Graph... + if(($TenantId -ne "") -and ($ClientId -ne "") -and ($CertificateThumbprint -ne "")) + { + Connect-MgGraph -TenantId $TenantId -AppId $ClientId -CertificateThumbprint $CertificateThumbprint -NoWelcome + } + else + { + Connect-MgGraph -Scopes "AuditLog.Read.All", "Directory.Read.All", "Policy.Read.ConditionalAccess" -NoWelcome + } +} +Connect_MgGraph + +$Location=Get-Location +$ExportCSV = "$Location\M365Users_Signin_Report$((Get-Date -format yyyy-MMM-dd-ddd` hh-mm-ss` tt).ToString()).csv" +$ExportResult="" +$ExportResults=@() + + +$Count=0 +$PrintedLogs=0 +#retrieve signin activities +Write-Host "Generating M365 users' signin report..." +Get-MgBetaAuditLogSignIn -All | ForEach-Object { + $Count++ + $UPN=$_.UserPrincipalName + Write-Progress -Activity "`n Processed sign-in record: $count " + $CreatedDate=$_.CreatedDateTime + $Id=$_.Id + $UserDisplayName=$_.UserDisplayName + $UPN=$_.UserPrincipalName + $AuthenticationRequirement=$_.AuthenticationRequirement + $Location="$($_.Location.City),$($_.Location.State),$($_.Location.CountryOrRegion)" + $DeviceName=$_.DeviceDetail.DisplayName + $Browser=$_.DeviceDetail.Browser + $OperatingSystem=$_.DeviceDetail.OperatingSystem + $IpAddress=$_.IpAddress + $ErrorCode=$_.Status.ErrorCode + $FailureReason=$_.Status.FailureReason + $UserType=$_.UserType + $RiskDetail=$_.RiskDetail + $IsInteractive=$_.IsInteractive + $RiskState=$_.RiskState + $AppDispalyName=$_.AppDisplayName + $ResourceDisplayName=$_.ResourceDisplayName + $ConditionalAccessStatus=$_.ConditionalAccessStatus + $AppliedConditionalAccessPolicies=$_.AppliedConditionalAccessPolicies + + + + $AppliedPolicies = @() + $AppliedConditionalAccessPolicies | ForEach-Object { + if($_.Result -eq 'Success' -or $_.Result -eq 'Failed'){ + $AppliedPolicies += $_.DisplayName + } + } + if($AppliedPolicies.Count -eq 0){ + $AppliedPolicies = "None" + } else{ + $AppliedPolicies = $AppliedPolicies -join ", " + } + + + if($ErrorCode -eq 0) + { + $Status='Success' + } + else + { + $Status='Failed' + } + + if($FailureReason -eq 'Other.') { + $FailureReason="None" + } + + #Declaring Print flag for all sign-ins + $Print=1 + + #Filter for successful sign-ins + if(($Success.IsPresent) -and ($Status -ne 'Success')) + { + $Print=0 + } + + #Filter for failed sign-ins + if(($Failure.IsPresent) -and ($Status -ne 'Failed')) + { + $Print=0 + } + + #Filter for applied conditional access policies + if(($CAPAppliedOnly.IsPresent) -and ($ConditionalAccessStatus -eq 'NotApplied')) + { + $Print=0 + } + + #Filter for not applied conditional access policies + if(($CAPNotAppliedOnly.IsPresent) -and ($ConditionalAccessStatus -ne 'NotApplied')) + { + $Print=0 + } + + #Filter for failed conditional access policies + if(($CAPFailedOnly.IsPresent) -and ($ConditionalAccessStatus -ne 'Failed')) + { + $Print=0 + } + + #Filter for succeeded conditional access policies + if(($CAPSuccessOnly.IsPresent) -and ($ConditionalAccessStatus -ne 'Success')) + { + $Print=0 + } + + #Filter for risky sign-ins + if(($RiskySignInsOnly.IsPresent) -and ($RiskDetail -eq 'none')) + { + $Print=0 + } + + #Filter for guest user sign-ins + if(($GuestUserSignInsOnly.IsPresent) -and ($UserType -eq 'member')) + { + $Print=0 + } + + #Filter for guest user sign-ins + if(($UserPrincipalName -ne $null) -and ($UserPrincipalName -notcontains $UPN)) + { + $Print=0 + } + + + #Filter for interactive sign-ins + if(($InteractiveOnly.IsPresent) -and (!$IsInteractive)) + { + $Print=0 + } + + + #Filter for non-interactive sign-ins + if(($NonInteractiveOnly.IsPresent) -and ($IsInteractive)) + { + $Print=0 + } + + + #Export users to output file + if($Print -eq 1) + { + $PrintedLogs++ + $ExportResult=[PSCustomObject]@{'Signin Date'=$CreatedDate; 'User Name'=$UserDisplayName; 'SigninId'=$Id;'UPN'=$UPN; 'Status'=$Status; 'Ip Address'=$IpAddress; 'Location'=$Location; 'Device Name'=$DeviceName; 'Browser'=$Browser; 'Operating System'=$OperatingSystem; 'User type'=$UserType; 'Authentication Requirement'=$AuthenticationRequirement; 'Risk detail'=$RiskDetail; 'Risk state'=$RiskState; 'Conditional access status'=$ConditionalAccessStatus; 'Applied Conditional Access Policies'=$AppliedPolicies; 'IsInteractive'=$IsInteractive;} + $ExportResult | Export-Csv -Path $ExportCSV -Notype -Append + } +} + +#Disconnect the session after execution +Disconnect-MgGraph | Out-Null + +#Open output file after execution + +if((Test-Path -Path $ExportCSV) -eq "True") +{ + Write-Host `n "The Output file availble in:" -NoNewline -ForegroundColor Yellow; Write-Host "$ExportCSV" `n + Write-Host " Exported report has $PrintedLogs signin activities" + Write-Host `n~~ Script prepared by AdminDroid Community ~~`n -ForegroundColor Green + Write-Host "~~ Check out " -NoNewline -ForegroundColor Green; Write-Host "admindroid.com" -ForegroundColor Yellow -NoNewline; Write-Host " to get access to 1800+ Microsoft 365 reports. ~~" -ForegroundColor Green `n`n + $Prompt = New-Object -ComObject wscript.shell + $UserInput = $Prompt.popup("Do you want to open output file?",` 0,"Open Output File",4) + if ($UserInput -eq 6) + { + Invoke-Item "$ExportCSV" + } + +} +else +{ + Write-Host "No logs found" +} \ No newline at end of file From ec5f1eff3907fcad03acc85aa66340ecc6266e57 Mon Sep 17 00:00:00 2001 From: AdminDroid <49208841+admindroid-community@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:43:46 +0530 Subject: [PATCH 14/14] Export Entra Sign-in Logs --- Export Entra Sign-in Logs/README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Export Entra Sign-in Logs/README.md b/Export Entra Sign-in Logs/README.md index 8e1d645..2a17554 100644 --- a/Export Entra Sign-in Logs/README.md +++ b/Export Entra Sign-in Logs/README.md @@ -9,8 +9,8 @@ examples for streamlined reporting. The exported report on Microsoft Entra ID sign-in logs looks like the screenshot below. -\![M365 User Sign-in -Report\](https://o365reports.com/wp-content/uploads/2024/07/Export-Microsoft-365-Users-Sign-in-Report-Using-PowerShell.png?v=1719913223) +![M365 User Sign-in +Report](https://o365reports.com/wp-content/uploads/2024/07/Export-Microsoft-365-Users-Sign-in-Report-Using-PowerShell.png?v=1719913223) ## **AdminDroid: Your Go-To Tool for Microsoft 365 Reporting** @@ -18,6 +18,4 @@ Need more than what this script offers? Explore [AdminDroid Microsoft 365 reporting tool](https://admindroid.com/?src=GitHub) to get access to 1800+ out-of-box M365 reports and insightful dashboards. -*View more comprehensive M365 user sign-in reports through AdminDroid:* - -[*https://demo.admindroid.com/#/1/11/reports/20161/1/20*](https://demo.admindroid.com/#/1/11/reports/20161/1/20) +*View more comprehensive M365 user sign-in reports through AdminDroid:* \ No newline at end of file