From 7b1e7b8b2dedafb2e2d002eac3b215281971b1c7 Mon Sep 17 00:00:00 2001 From: Nisheet Jain Date: Fri, 9 Oct 2026 11:40:38 -0700 Subject: [PATCH 1/6] docs: add SMS cost estimation guide Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 749249f5-dd43-4595-896b-1c4b439ae048 --- README.md | 3 + docs/COST-CALCULATOR.md | 149 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 docs/COST-CALCULATOR.md diff --git a/README.md b/README.md index 8ab0042..f3ff8ba 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,9 @@ In Security Store, open **Private solutions** and select your approved subscript its available offers. See [sample offers](docs/ONBOARDING.md#before-purchasing-or-deploying) in the detailed guide; a listing does not guarantee support in this setup script. +Use the [SMS cost estimation guide](docs/COST-CALCULATOR.md) to create a planning estimate, +then confirm destination rates, fees, and final costs with your provider. + Have these ready: - **Provider account:** complete the provider's account and sender registration for your chosen diff --git a/docs/COST-CALCULATOR.md b/docs/COST-CALCULATOR.md new file mode 100644 index 0000000..deebe84 --- /dev/null +++ b/docs/COST-CALCULATOR.md @@ -0,0 +1,149 @@ +# Estimate SMS provider costs + +Use this guide to create a planning estimate for SMS traffic before onboarding an +External Phone Provider (EPP). The report groups recent sign-in activity for users +who have a phone authentication method. Apply your provider's country or region +rates to the report to estimate a budget. + +**This is an estimate, not a usage or billing report.** A sign-in does not prove that +an SMS was sent, and one sign-in can result in no message or more than one message. +The sign-in location is based on the sign-in event and might not match the country +of the destination phone number. Confirm the assumptions, supported destinations, +message segmentation, taxes, fees, and final pricing with your provider. + +## Prerequisites + +Run the script in PowerShell 7 from a secured administrator workstation. The signed-in +account must be allowed to consent to or use these Microsoft Graph delegated permissions: + +- `User.Read.All` +- `UserAuthenticationMethod.Read.All` +- `AuditLog.Read.All` + +Access to sign-in logs also depends on your Microsoft Entra licensing, directory role, +and log-retention period. Review the requested permissions before consenting. The output +contains user principal names and sign-in location data; store, share, and delete it +according to your organization's privacy and retention requirements. + +Create `C:\temp` before running the script, or change `$outputPath` to an approved folder. + +## Generate the activity report + +The following sample installs any missing Microsoft Graph modules for the current user, +connects to Microsoft Graph, identifies users with a phone authentication method, and +groups up to 500 recent sign-ins per user by sign-in country or region. + +```powershell +# 1. Install missing modules silently +"Microsoft.Graph.Users", "Microsoft.Graph.Identity.SignIns", "Microsoft.Graph.Reports" | + ForEach-Object { + if (-not (Get-Module -ListAvailable $_)) { + Install-Module $_ -Scope CurrentUser -Force -AllowClobber | Out-Null + } + } + +# 2. Connect to Microsoft Graph +Connect-MgGraph ` + -Scopes "User.Read.All", "UserAuthenticationMethod.Read.All", "AuditLog.Read.All" | + Out-Null + +# 3. Process users and build the report +$report = [System.Collections.Generic.List[PSCustomObject]]::new() +$users = Get-MgUser -All -Property Id, UserPrincipalName + +foreach ($user in $users) { + $phoneMethods = Get-MgUserAuthenticationPhoneMethod ` + -UserId $user.Id ` + -ErrorAction SilentlyContinue + + if ($phoneMethods) { + $logs = Get-MgAuditLogSignIn ` + -Filter "userId eq '$($user.Id)'" ` + -Top 500 ` + -ErrorAction SilentlyContinue + + if ($logs) { + $logs | + Group-Object -Property { $_.Location.CountryOrRegion } | + ForEach-Object { + $countryOrRegion = if ([string]::IsNullOrWhiteSpace($_.Name)) { + "Unknown" + } + else { + $_.Name + } + + $report.Add([PSCustomObject]@{ + UserPrincipalName = $user.UserPrincipalName + CountryOrRegion = $countryOrRegion + SignInCount = $_.Count + }) + } + } + } +} + +# 4. Display and export the report +$outputPath = "C:\temp\SMS_Voice_Users_Country_Counts.csv" +$report | Sort-Object CountryOrRegion, UserPrincipalName | Format-Table -AutoSize +$report | Export-Csv -Path $outputPath -NoTypeInformation +``` + +The script suppresses per-user read errors so that one inaccessible record does not stop +the report. Investigate unexpectedly missing users or countries before relying on the +result. For a large tenant or a longer analysis window, replace the 500-record cap with +an organization-approved reporting approach that handles Microsoft Graph pagination, +throttling, and your available sign-in-log retention. + +## Convert activity into a cost estimate + +First, summarize the exported activity by country or region: + +```powershell +$activity = Import-Csv "C:\temp\SMS_Voice_Users_Country_Counts.csv" + +$activity | + Group-Object CountryOrRegion | + ForEach-Object { + [PSCustomObject]@{ + CountryOrRegion = $_.Name + SignInCount = ($_.Group.SignInCount | Measure-Object -Sum).Sum + } + } | + Sort-Object CountryOrRegion | + Export-Csv "C:\temp\SMS_Activity_By_Country.csv" -NoTypeInformation +``` + +For each country or region, obtain the provider's applicable price and estimate: + +```text +Estimated messages = sign-in count x assumed SMS messages per sign-in +Estimated cost = estimated messages x provider price per SMS +Projected cost = estimated cost x projected days / observed days +``` + +For example, if the report covers 30 days, contains 10,000 relevant sign-ins, and your +planning assumption is 0.25 SMS messages per sign-in: + +```text +Estimated messages = 10,000 x 0.25 = 2,500 +``` + +Apply the provider's destination-specific rates to those estimated messages. Use separate +rows when rates differ by destination, sender type, route, or message category. Include a +contingency for growth, retries, fallback behavior, and seasonal peaks. + +## Confirm the final estimate with your provider + +Work with your provider to validate: + +- Whether pricing uses the destination phone number rather than sign-in location. +- Country and carrier coverage, sender registration, and route-specific rates. +- SMS segment rules, including Unicode and messages that exceed one segment. +- Minimum commitments, volume tiers, taxes, regulatory fees, and other surcharges. +- Charges for failed, rejected, retried, or duplicate submissions. +- The expected ratio of SMS sends to sign-ins for your authentication policies and users. + +After deployment, compare the estimate with provider billing and approved operational +telemetry. Do not treat this report as proof of message submission, acceptance, delivery, +or the amount that the provider will invoice. From 08195dbb6c2b80acf9f749046fb38887e89536a7 Mon Sep 17 00:00:00 2001 From: Nisheet Jain Date: Fri, 9 Oct 2026 16:08:56 -0700 Subject: [PATCH 2/6] docs: aggregate SMS estimates by country Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 749249f5-dd43-4595-896b-1c4b439ae048 --- docs/COST-CALCULATOR.md | 117 ++++++++++++++-------------------------- 1 file changed, 40 insertions(+), 77 deletions(-) diff --git a/docs/COST-CALCULATOR.md b/docs/COST-CALCULATOR.md index deebe84..5971afb 100644 --- a/docs/COST-CALCULATOR.md +++ b/docs/COST-CALCULATOR.md @@ -21,100 +21,63 @@ account must be allowed to consent to or use these Microsoft Graph delegated per - `AuditLog.Read.All` Access to sign-in logs also depends on your Microsoft Entra licensing, directory role, -and log-retention period. Review the requested permissions before consenting. The output -contains user principal names and sign-in location data; store, share, and delete it -according to your organization's privacy and retention requirements. +and log-retention period. Review the requested permissions before consenting. The script +processes directory users, authentication methods, and sign-in location data. Store, share, +and delete the aggregated output according to your organization's privacy and retention +requirements. -Create `C:\temp` before running the script, or change `$outputPath` to an approved folder. +Create `C:\temp` before running the script, or change the `Export-Csv` path to an +approved folder. ## Generate the activity report The following sample installs any missing Microsoft Graph modules for the current user, connects to Microsoft Graph, identifies users with a phone authentication method, and -groups up to 500 recent sign-ins per user by sign-in country or region. +groups sign-ins from a selected UTC time range by sign-in country or region. Replace +`$start` and `$end` with the period you want to analyze. ```powershell # 1. Install missing modules silently "Microsoft.Graph.Users", "Microsoft.Graph.Identity.SignIns", "Microsoft.Graph.Reports" | - ForEach-Object { - if (-not (Get-Module -ListAvailable $_)) { - Install-Module $_ -Scope CurrentUser -Force -AllowClobber | Out-Null - } - } - -# 2. Connect to Microsoft Graph -Connect-MgGraph ` - -Scopes "User.Read.All", "UserAuthenticationMethod.Read.All", "AuditLog.Read.All" | - Out-Null - -# 3. Process users and build the report -$report = [System.Collections.Generic.List[PSCustomObject]]::new() -$users = Get-MgUser -All -Property Id, UserPrincipalName - -foreach ($user in $users) { - $phoneMethods = Get-MgUserAuthenticationPhoneMethod ` - -UserId $user.Id ` - -ErrorAction SilentlyContinue - - if ($phoneMethods) { - $logs = Get-MgAuditLogSignIn ` - -Filter "userId eq '$($user.Id)'" ` - -Top 500 ` - -ErrorAction SilentlyContinue - - if ($logs) { - $logs | - Group-Object -Property { $_.Location.CountryOrRegion } | - ForEach-Object { - $countryOrRegion = if ([string]::IsNullOrWhiteSpace($_.Name)) { - "Unknown" - } - else { - $_.Name - } - - $report.Add([PSCustomObject]@{ - UserPrincipalName = $user.UserPrincipalName - CountryOrRegion = $countryOrRegion - SignInCount = $_.Count - }) - } - } - } -} - -# 4. Display and export the report -$outputPath = "C:\temp\SMS_Voice_Users_Country_Counts.csv" -$report | Sort-Object CountryOrRegion, UserPrincipalName | Format-Table -AutoSize -$report | Export-Csv -Path $outputPath -NoTypeInformation + ? { -not (Get-Module -ListAvailable $_) } | + % { Install-Module $_ -Scope CurrentUser -Force -AllowClobber | Out-Null } + +# 2. Connect to Graph API +Connect-MgGraph -Scopes "User.Read.All", "UserAuthenticationMethod.Read.All", "AuditLog.Read.All" | Out-Null + +# 3. Define Start and End Time Range (ISO 8601 UTC) +$start = "2026-10-01T00:00:00Z" +$end = "2026-10-30T23:59:59Z" + +# 4. Single Pipeline: Filter users with Phone Auth -> Fetch Sign-ins -> Extract Country -> Group & Count +$report = Get-MgUser -All -Property Id | + ? { Get-MgUserAuthenticationPhoneMethod -UserId $_.Id -ErrorAction SilentlyContinue } | + % { Get-MgAuditLogSignIn -Filter "userId eq '$($_.Id)' and createdDateTime ge $start and createdDateTime le $end" -All -ErrorAction SilentlyContinue } | + Group-Object -Property { $_.Location.CountryOrRegion } | + Select-Object @{N="CountryCode"; E={$_.Name}}, @{N="SignInCount"; E={$_.Count}} | + Sort-Object SignInCount -Descending + +# 5. Output and Export +$report | Format-Table -AutoSize +$report | Export-Csv -Path "C:\temp\SMS_Voice_SignIns_By_Country.csv" -NoTypeInformation ``` The script suppresses per-user read errors so that one inaccessible record does not stop the report. Investigate unexpectedly missing users or countries before relying on the -result. For a large tenant or a longer analysis window, replace the 500-record cap with -an organization-approved reporting approach that handles Microsoft Graph pagination, -throttling, and your available sign-in-log retention. +result. `-All` requests all available pages, but large tenants should still account for +Microsoft Graph throttling and execution time. -## Convert activity into a cost estimate - -First, summarize the exported activity by country or region: +The Microsoft Graph +[list signIns API documentation](https://learn.microsoft.com/en-us/graph/api/signin-list?view=graph-rest-1.0&tabs=http) +states: **"The maximum and default page size is 1,000 objects and by default, the most +recent sign-ins are returned first. Only sign-in events that occurred within the +Microsoft Entra ID default retention period are available."** Selecting an earlier +`$start` date does not make events outside the available retention period accessible. -```powershell -$activity = Import-Csv "C:\temp\SMS_Voice_Users_Country_Counts.csv" - -$activity | - Group-Object CountryOrRegion | - ForEach-Object { - [PSCustomObject]@{ - CountryOrRegion = $_.Name - SignInCount = ($_.Group.SignInCount | Measure-Object -Sum).Sum - } - } | - Sort-Object CountryOrRegion | - Export-Csv "C:\temp\SMS_Activity_By_Country.csv" -NoTypeInformation -``` +## Convert activity into a cost estimate -For each country or region, obtain the provider's applicable price and estimate: +The exported report already contains one aggregated row per sign-in country or region. +For each row, obtain the provider's applicable price and estimate: ```text Estimated messages = sign-in count x assumed SMS messages per sign-in From 04ebd9785c19883423b778aea2032c23a5aa36fc Mon Sep 17 00:00:00 2001 From: Nisheet Jain Date: Fri, 9 Oct 2026 17:01:09 -0700 Subject: [PATCH 3/6] docs: estimate costs by phone calling code Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 749249f5-dd43-4595-896b-1c4b439ae048 --- docs/COST-CALCULATOR.md | 104 +++++++++++++++++++++++++++------------- 1 file changed, 72 insertions(+), 32 deletions(-) diff --git a/docs/COST-CALCULATOR.md b/docs/COST-CALCULATOR.md index 5971afb..956a3c3 100644 --- a/docs/COST-CALCULATOR.md +++ b/docs/COST-CALCULATOR.md @@ -2,14 +2,16 @@ Use this guide to create a planning estimate for SMS traffic before onboarding an External Phone Provider (EPP). The report groups recent sign-in activity for users -who have a phone authentication method. Apply your provider's country or region -rates to the report to estimate a budget. +who have a phone authentication method by the registered phone number's international +calling code, such as `+1` or `+91`. Apply your provider's destination rates to the +report to estimate a budget. **This is an estimate, not a usage or billing report.** A sign-in does not prove that -an SMS was sent, and one sign-in can result in no message or more than one message. -The sign-in location is based on the sign-in event and might not match the country -of the destination phone number. Confirm the assumptions, supported destinations, -message segmentation, taxes, fees, and final pricing with your provider. +an SMS or voice call was sent. This guide uses one message or call per counted sign-in +as its planning assumption. Actual traffic can differ because a user can use another +authentication method, reuse an existing session, retry authentication, or fall back +between methods. Confirm the assumptions, supported destinations, message segmentation, +taxes, fees, and final pricing with your provider. ## Prerequisites @@ -22,9 +24,9 @@ account must be allowed to consent to or use these Microsoft Graph delegated per Access to sign-in logs also depends on your Microsoft Entra licensing, directory role, and log-retention period. Review the requested permissions before consenting. The script -processes directory users, authentication methods, and sign-in location data. Store, share, -and delete the aggregated output according to your organization's privacy and retention -requirements. +processes directory users, registered phone numbers, and sign-in records. It exports only +aggregated calling codes and counts, not full phone numbers. Store, share, and delete the +output according to your organization's privacy and retention requirements. Create `C:\temp` before running the script, or change the `Export-Csv` path to an approved folder. @@ -33,8 +35,8 @@ approved folder. The following sample installs any missing Microsoft Graph modules for the current user, connects to Microsoft Graph, identifies users with a phone authentication method, and -groups sign-ins from a selected UTC time range by sign-in country or region. Replace -`$start` and `$end` with the period you want to analyze. +groups sign-ins from a selected UTC time range by the registered phone number's calling +code. Replace `$start` and `$end` with the period you want to analyze. ```powershell # 1. Install missing modules silently @@ -49,23 +51,62 @@ Connect-MgGraph -Scopes "User.Read.All", "UserAuthenticationMethod.Read.All", "A $start = "2026-10-01T00:00:00Z" $end = "2026-10-30T23:59:59Z" -# 4. Single Pipeline: Filter users with Phone Auth -> Fetch Sign-ins -> Extract Country -> Group & Count -$report = Get-MgUser -All -Property Id | - ? { Get-MgUserAuthenticationPhoneMethod -UserId $_.Id -ErrorAction SilentlyContinue } | - % { Get-MgAuditLogSignIn -Filter "userId eq '$($_.Id)' and createdDateTime ge $start and createdDateTime le $end" -All -ErrorAction SilentlyContinue } | - Group-Object -Property { $_.Location.CountryOrRegion } | - Select-Object @{N="CountryCode"; E={$_.Name}}, @{N="SignInCount"; E={$_.Count}} | +# 4. Fetch sign-ins and associate them with the user's preferred registered phone method +$activity = Get-MgUser -All -Property Id | + % { + $user = $_ + $phoneMethod = Get-MgUserAuthenticationPhoneMethod ` + -UserId $user.Id ` + -ErrorAction SilentlyContinue | + Sort-Object @{ E = { + switch ($_.PhoneType) { + "mobile" { 1 } + "alternateMobile" { 2 } + "office" { 3 } + default { 4 } + } + }} | + Select-Object -First 1 + + if ($phoneMethod) { + $callingCode = if ($phoneMethod.PhoneNumber -match '^\s*(\+\d{1,3})(?:\s|$)') { + $Matches[1] + } + else { + "Unknown" + } + + Get-MgAuditLogSignIn ` + -Filter "userId eq '$($user.Id)' and createdDateTime ge $start and createdDateTime le $end" ` + -All ` + -ErrorAction SilentlyContinue | + % { + [PSCustomObject]@{ + CountryCallingCode = $callingCode + } + } + } + } + +# 5. Group and count by the phone number's international calling code +$report = $activity | + Group-Object CountryCallingCode | + Select-Object @{N="CountryCallingCode"; E={$_.Name}}, @{N="SignInCount"; E={$_.Count}} | Sort-Object SignInCount -Descending -# 5. Output and Export +# 6. Output and Export $report | Format-Table -AutoSize -$report | Export-Csv -Path "C:\temp\SMS_Voice_SignIns_By_Country.csv" -NoTypeInformation +$report | Export-Csv -Path "C:\temp\SMS_Voice_SignIns_By_Calling_Code.csv" -NoTypeInformation ``` The script suppresses per-user read errors so that one inaccessible record does not stop -the report. Investigate unexpectedly missing users or countries before relying on the -result. `-All` requests all available pages, but large tenants should still account for -Microsoft Graph throttling and execution time. +the report. It prefers a user's `mobile` method, followed by `alternateMobile`, then +`office`, so each sign-in is counted once when more than one phone method is registered. +The calling-code extraction expects the number to start with a plus-prefixed code followed +by a space, such as `+91 1234567890`; other formats are grouped as `Unknown`. Investigate +unexpectedly missing users or calling codes before relying on the result. `-All` requests +all available pages, but large tenants should still account for Microsoft Graph throttling +and execution time. The Microsoft Graph [list signIns API documentation](https://learn.microsoft.com/en-us/graph/api/signin-list?view=graph-rest-1.0&tabs=http) @@ -76,23 +117,22 @@ Microsoft Entra ID default retention period are available."** Selecting an earli ## Convert activity into a cost estimate -The exported report already contains one aggregated row per sign-in country or region. -For each row, obtain the provider's applicable price and estimate: +The exported report contains one aggregated row per registered phone-number calling code. +Under this guide's one-request-per-sign-in planning assumption, estimate: ```text -Estimated messages = sign-in count x assumed SMS messages per sign-in -Estimated cost = estimated messages x provider price per SMS -Projected cost = estimated cost x projected days / observed days +Estimated SMS/voice requests = sign-in count +Estimated cost = sign-in count x provider price per request +Projected cost = estimated cost x projected days / observed days ``` -For example, if the report covers 30 days, contains 10,000 relevant sign-ins, and your -planning assumption is 0.25 SMS messages per sign-in: +For example, if the report covers 30 days and contains 10,000 relevant sign-ins: ```text -Estimated messages = 10,000 x 0.25 = 2,500 +Estimated SMS/voice requests = 10,000 ``` -Apply the provider's destination-specific rates to those estimated messages. Use separate +Apply the provider's destination-specific rates to those estimated requests. Use separate rows when rates differ by destination, sender type, route, or message category. Include a contingency for growth, retries, fallback behavior, and seasonal peaks. @@ -100,7 +140,7 @@ contingency for growth, retries, fallback behavior, and seasonal peaks. Work with your provider to validate: -- Whether pricing uses the destination phone number rather than sign-in location. +- How each international calling code maps to the provider's destination pricing. - Country and carrier coverage, sender registration, and route-specific rates. - SMS segment rules, including Unicode and messages that exceed one segment. - Minimum commitments, volume tiers, taxes, regulatory fees, and other surcharges. From 69578bba5eec9f9d998d5adb4cb51a30ecd6c92b Mon Sep 17 00:00:00 2001 From: Nisheet Jain Date: Fri, 9 Oct 2026 17:26:22 -0700 Subject: [PATCH 4/6] docs: estimate actual phone authentication steps Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 749249f5-dd43-4595-896b-1c4b439ae048 --- docs/COST-CALCULATOR.md | 153 +++++++++++++++++++++------------------- 1 file changed, 80 insertions(+), 73 deletions(-) diff --git a/docs/COST-CALCULATOR.md b/docs/COST-CALCULATOR.md index 956a3c3..32c6084 100644 --- a/docs/COST-CALCULATOR.md +++ b/docs/COST-CALCULATOR.md @@ -1,112 +1,118 @@ # Estimate SMS provider costs Use this guide to create a planning estimate for SMS traffic before onboarding an -External Phone Provider (EPP). The report groups recent sign-in activity for users -who have a phone authentication method by the registered phone number's international -calling code, such as `+1` or `+91`. Apply your provider's destination rates to the -report to estimate a budget. +External Phone Provider (EPP). The report finds SMS and voice authentication steps in +recent sign-in activity and groups them by the phone number's international calling +code, such as `+1` or `+91`. Apply your provider's destination rates to the report to +estimate a budget. **This is an estimate, not a usage or billing report.** A sign-in does not prove that -an SMS or voice call was sent. This guide uses one message or call per counted sign-in -as its planning assumption. Actual traffic can differ because a user can use another -authentication method, reuse an existing session, retry authentication, or fall back -between methods. Confirm the assumptions, supported destinations, message segmentation, -taxes, fees, and final pricing with your provider. +the provider accepted or delivered an SMS or voice call. Authentication details can +contain multiple SMS or voice steps for one sign-in, so this guide counts each matching +step as one estimated request. Actual provider billing can differ because of retries, +fallback, failures, message segmentation, and provider-specific charging rules. Confirm +the assumptions, supported destinations, taxes, fees, and final pricing with your provider. + +The `authenticationDetails` data used by this guide is currently available through the +[Microsoft Graph beta API](https://learn.microsoft.com/en-us/graph/api/resources/authenticationdetail?view=graph-rest-beta). +Beta APIs are subject to change and are not supported for production applications. Review +and test this reporting script before each planning exercise. ## Prerequisites Run the script in PowerShell 7 from a secured administrator workstation. The signed-in account must be allowed to consent to or use these Microsoft Graph delegated permissions: -- `User.Read.All` -- `UserAuthenticationMethod.Read.All` - `AuditLog.Read.All` Access to sign-in logs also depends on your Microsoft Entra licensing, directory role, and log-retention period. Review the requested permissions before consenting. The script -processes directory users, registered phone numbers, and sign-in records. It exports only -aggregated calling codes and counts, not full phone numbers. Store, share, and delete the -output according to your organization's privacy and retention requirements. +processes sign-in authentication details that can include phone numbers. It exports only +aggregated calling codes, methods, and counts, not full phone numbers. Store, share, and +delete the output according to your organization's privacy and retention requirements. Create `C:\temp` before running the script, or change the `Export-Csv` path to an approved folder. ## Generate the activity report -The following sample installs any missing Microsoft Graph modules for the current user, -connects to Microsoft Graph, identifies users with a phone authentication method, and -groups sign-ins from a selected UTC time range by the registered phone number's calling -code. Replace `$start` and `$end` with the period you want to analyze. +The following sample installs the Microsoft Graph beta reports module for the current +user, connects to Microsoft Graph, identifies SMS and voice authentication steps, and +groups them by the phone number's calling code. Replace `$start` and `$end` with the +period you want to analyze. ```powershell -# 1. Install missing modules silently -"Microsoft.Graph.Users", "Microsoft.Graph.Identity.SignIns", "Microsoft.Graph.Reports" | - ? { -not (Get-Module -ListAvailable $_) } | - % { Install-Module $_ -Scope CurrentUser -Force -AllowClobber | Out-Null } +# 1. Install the beta reports module if it is missing +if (-not (Get-Module -ListAvailable "Microsoft.Graph.Beta.Reports")) { + Install-Module "Microsoft.Graph.Beta.Reports" ` + -Scope CurrentUser ` + -Force ` + -AllowClobber | + Out-Null +} # 2. Connect to Graph API -Connect-MgGraph -Scopes "User.Read.All", "UserAuthenticationMethod.Read.All", "AuditLog.Read.All" | Out-Null +Connect-MgGraph -Scopes "AuditLog.Read.All" | Out-Null # 3. Define Start and End Time Range (ISO 8601 UTC) $start = "2026-10-01T00:00:00Z" $end = "2026-10-30T23:59:59Z" -# 4. Fetch sign-ins and associate them with the user's preferred registered phone method -$activity = Get-MgUser -All -Property Id | - % { - $user = $_ - $phoneMethod = Get-MgUserAuthenticationPhoneMethod ` - -UserId $user.Id ` - -ErrorAction SilentlyContinue | - Sort-Object @{ E = { - switch ($_.PhoneType) { - "mobile" { 1 } - "alternateMobile" { 2 } - "office" { 3 } - default { 4 } +# 4. Fetch sign-ins and emit one row for each SMS or voice authentication step +$activity = Get-MgBetaAuditLogSignIn ` + -Filter "createdDateTime ge $start and createdDateTime le $end" ` + -Property "authenticationDetails" ` + -All ` + -ErrorAction Stop | + ForEach-Object { + $_.AuthenticationDetails | + Where-Object { $_.AuthenticationMethod -in "SMS", "Voice" } | + ForEach-Object { + $callingCode = if ( + $_.AuthenticationMethodDetail -match '^\s*(\+\d{1,3})(?:\s|$)' + ) { + $Matches[1] + } + else { + "Unknown" } - }} | - Select-Object -First 1 - - if ($phoneMethod) { - $callingCode = if ($phoneMethod.PhoneNumber -match '^\s*(\+\d{1,3})(?:\s|$)') { - $Matches[1] - } - else { - "Unknown" - } - Get-MgAuditLogSignIn ` - -Filter "userId eq '$($user.Id)' and createdDateTime ge $start and createdDateTime le $end" ` - -All ` - -ErrorAction SilentlyContinue | - % { - [PSCustomObject]@{ - CountryCallingCode = $callingCode - } + [PSCustomObject]@{ + CountryCallingCode = $callingCode + AuthenticationMethod = $_.AuthenticationMethod } - } + } } -# 5. Group and count by the phone number's international calling code +# 5. Group and count by calling code and authentication method $report = $activity | - Group-Object CountryCallingCode | - Select-Object @{N="CountryCallingCode"; E={$_.Name}}, @{N="SignInCount"; E={$_.Count}} | - Sort-Object SignInCount -Descending + Group-Object CountryCallingCode, AuthenticationMethod | + ForEach-Object { + [PSCustomObject]@{ + CountryCallingCode = $_.Group[0].CountryCallingCode + AuthenticationMethod = $_.Group[0].AuthenticationMethod + EstimatedRequests = $_.Count + } + } | + Sort-Object CountryCallingCode, AuthenticationMethod -# 6. Output and Export +# 6. Display and export the aggregated report $report | Format-Table -AutoSize $report | Export-Csv -Path "C:\temp\SMS_Voice_SignIns_By_Calling_Code.csv" -NoTypeInformation ``` -The script suppresses per-user read errors so that one inaccessible record does not stop -the report. It prefers a user's `mobile` method, followed by `alternateMobile`, then -`office`, so each sign-in is counted once when more than one phone method is registered. -The calling-code extraction expects the number to start with a plus-prefixed code followed -by a space, such as `+91 1234567890`; other formats are grouped as `Unknown`. Investigate -unexpectedly missing users or calling codes before relying on the result. `-All` requests -all available pages, but large tenants should still account for Microsoft Graph throttling -and execution time. +Microsoft documents `SMS` and `Voice` as authentication method values and states that +`authenticationMethodDetail` can contain the phone number for those methods. The script +does not match an undocumented `Text` value. It also does not use sign-in geography or a +user's currently registered default number, because those can differ from the phone method +recorded for the authentication step. + +The calling-code extraction expects the detail to start with a plus-prefixed code followed +by a space, such as `+91 1234567890`. Masked values or other formats are grouped as +`Unknown`; review a small, securely handled sample in your tenant before relying on the +aggregation. `-All` requests all available pages, but large tenants should still account +for Microsoft Graph throttling and execution time. The script uses `-ErrorAction Stop` +rather than silently producing a partial report when the sign-in query fails. The Microsoft Graph [list signIns API documentation](https://learn.microsoft.com/en-us/graph/api/signin-list?view=graph-rest-1.0&tabs=http) @@ -117,16 +123,17 @@ Microsoft Entra ID default retention period are available."** Selecting an earli ## Convert activity into a cost estimate -The exported report contains one aggregated row per registered phone-number calling code. -Under this guide's one-request-per-sign-in planning assumption, estimate: +The exported report contains one row per calling code and authentication method. It counts +each SMS or voice authentication step as one estimated request: ```text -Estimated SMS/voice requests = sign-in count -Estimated cost = sign-in count x provider price per request +Estimated SMS/voice requests = matching authentication-step count +Estimated cost = estimated requests x provider price per request Projected cost = estimated cost x projected days / observed days ``` -For example, if the report covers 30 days and contains 10,000 relevant sign-ins: +For example, if the report covers 30 days and contains 10,000 matching authentication +steps: ```text Estimated SMS/voice requests = 10,000 From ac2e0c267c1fb15c2fe068025a9bf51906fcc27b Mon Sep 17 00:00:00 2001 From: Nisheet Jain Date: Tue, 13 Oct 2026 13:48:27 -0700 Subject: [PATCH 5/6] docs: resolve masked phone calling codes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 749249f5-dd43-4595-896b-1c4b439ae048 --- docs/COST-CALCULATOR.md | 114 +++++++++++++++++++++++++++++----------- 1 file changed, 83 insertions(+), 31 deletions(-) diff --git a/docs/COST-CALCULATOR.md b/docs/COST-CALCULATOR.md index 32c6084..5015bf9 100644 --- a/docs/COST-CALCULATOR.md +++ b/docs/COST-CALCULATOR.md @@ -16,7 +16,9 @@ the assumptions, supported destinations, taxes, fees, and final pricing with you The `authenticationDetails` data used by this guide is currently available through the [Microsoft Graph beta API](https://learn.microsoft.com/en-us/graph/api/resources/authenticationdetail?view=graph-rest-beta). Beta APIs are subject to change and are not supported for production applications. Review -and test this reporting script before each planning exercise. +and test this reporting script before each planning exercise. Because sign-in details mask +phone numbers, the script matches their visible trailing digits to the user's registered +phone authentication methods and derives the calling code from the matched registration. ## Prerequisites @@ -24,6 +26,7 @@ Run the script in PowerShell 7 from a secured administrator workstation. The sig account must be allowed to consent to or use these Microsoft Graph delegated permissions: - `AuditLog.Read.All` +- `UserAuthenticationMethod.Read.All` Access to sign-in logs also depends on your Microsoft Entra licensing, directory role, and log-retention period. Review the requested permissions before consenting. The script @@ -36,40 +39,83 @@ approved folder. ## Generate the activity report -The following sample installs the Microsoft Graph beta reports module for the current -user, connects to Microsoft Graph, identifies SMS and voice authentication steps, and -groups them by the phone number's calling code. Replace `$start` and `$end` with the -period you want to analyze. +The following sample installs the required Microsoft Graph modules for the current user, +connects to Microsoft Graph, identifies SMS and voice authentication steps, and matches +each masked phone detail to a registered phone method. It caches phone-method lookups so +each user is queried only once. Replace `$start` and `$end` with the period you want to +analyze. ```powershell -# 1. Install the beta reports module if it is missing -if (-not (Get-Module -ListAvailable "Microsoft.Graph.Beta.Reports")) { - Install-Module "Microsoft.Graph.Beta.Reports" ` - -Scope CurrentUser ` - -Force ` - -AllowClobber | - Out-Null -} +# 1. Install required modules if they are missing +"Microsoft.Graph.Beta.Reports", "Microsoft.Graph.Identity.SignIns" | + Where-Object { -not (Get-Module -ListAvailable $_) } | + ForEach-Object { + Install-Module $_ -Scope CurrentUser -Force -AllowClobber | Out-Null + } # 2. Connect to Graph API -Connect-MgGraph -Scopes "AuditLog.Read.All" | Out-Null +Connect-MgGraph ` + -Scopes "AuditLog.Read.All", "UserAuthenticationMethod.Read.All" | + Out-Null # 3. Define Start and End Time Range (ISO 8601 UTC) $start = "2026-10-01T00:00:00Z" $end = "2026-10-30T23:59:59Z" -# 4. Fetch sign-ins and emit one row for each SMS or voice authentication step +# 4. Cache registered phone methods and resolve masked sign-in details +$phoneMethodsByUser = @{} + $activity = Get-MgBetaAuditLogSignIn ` -Filter "createdDateTime ge $start and createdDateTime le $end" ` - -Property "authenticationDetails" ` + -Property "userId", "authenticationDetails" ` -All ` -ErrorAction Stop | ForEach-Object { - $_.AuthenticationDetails | - Where-Object { $_.AuthenticationMethod -in "SMS", "Voice" } | + $signIn = $_ + $phoneDetails = @( + $signIn.AuthenticationDetails | + Where-Object { $_.AuthenticationMethod -in "SMS", "Voice" } + ) + + if ($phoneDetails.Count -gt 0) { + if ([string]::IsNullOrWhiteSpace($signIn.UserId)) { + $registeredMethods = @() + } + else { + if (-not $phoneMethodsByUser.ContainsKey($signIn.UserId)) { + $phoneMethodsByUser[$signIn.UserId] = @( + Get-MgUserAuthenticationPhoneMethod ` + -UserId $signIn.UserId ` + -ErrorAction Stop + ) + } + + $registeredMethods = $phoneMethodsByUser[$signIn.UserId] + } + + $phoneDetails | ForEach-Object { + $authenticationDetail = $_ + $visibleDigits = $authenticationDetail.AuthenticationMethodDetail -replace '\D', '' + + $matchingMethods = @( + $registeredMethods | + Where-Object { + $registeredDigits = $_.PhoneNumber -replace '\D', '' + $supportsMethod = ( + $authenticationDetail.AuthenticationMethod -eq "Voice" -or + $_.PhoneType -in "mobile", "alternateMobile" + ) + + $supportsMethod -and + $visibleDigits.Length -ge 2 -and + $registeredDigits.EndsWith($visibleDigits) + } + ) + $callingCode = if ( - $_.AuthenticationMethodDetail -match '^\s*(\+\d{1,3})(?:\s|$)' + $matchingMethods.Count -eq 1 -and + $matchingMethods[0].PhoneNumber -match '^\s*(\+\d{1,3})(?:\s|$)' ) { $Matches[1] } @@ -79,9 +125,10 @@ $activity = Get-MgBetaAuditLogSignIn ` [PSCustomObject]@{ CountryCallingCode = $callingCode - AuthenticationMethod = $_.AuthenticationMethod + AuthenticationMethod = $authenticationDetail.AuthenticationMethod } } + } } # 5. Group and count by calling code and authentication method @@ -102,17 +149,22 @@ $report | Export-Csv -Path "C:\temp\SMS_Voice_SignIns_By_Calling_Code.csv" -NoTy ``` Microsoft documents `SMS` and `Voice` as authentication method values and states that -`authenticationMethodDetail` can contain the phone number for those methods. The script -does not match an undocumented `Text` value. It also does not use sign-in geography or a -user's currently registered default number, because those can differ from the phone method -recorded for the authentication step. - -The calling-code extraction expects the detail to start with a plus-prefixed code followed -by a space, such as `+91 1234567890`. Masked values or other formats are grouped as -`Unknown`; review a small, securely handled sample in your tenant before relying on the -aggregation. `-All` requests all available pages, but large tenants should still account -for Microsoft Graph throttling and execution time. The script uses `-ErrorAction Stop` -rather than silently producing a partial report when the sign-in query fails. +`authenticationMethodDetail` can contain the phone number for those methods. In sign-in +results, that value can be masked, for example `+x XXXXXXXX90`. The script removes +non-digits from that detail and matches the visible suffix (`90`) against the user's +registered methods. It then extracts the calling code from the uniquely matched registered +number, whose Graph format includes a space after the code, such as `+91 1234567890`. + +For SMS, only `mobile` and `alternateMobile` registrations are considered; voice can use +any registered phone type. A row is grouped as `Unknown` when the masked detail exposes +fewer than two digits, no registered number matches, more than one registered number has +the same visible suffix, or the matched number has an unexpected format. This avoids +guessing a destination. Review the `Unknown` total before relying on the estimate. + +The script does not match an undocumented `Text` method or use sign-in geography. `-All` +requests all available pages, but large tenants should still account for Microsoft Graph +throttling and execution time. The script uses `-ErrorAction Stop` rather than silently +producing a partial report when either sign-in or phone-method retrieval fails. The Microsoft Graph [list signIns API documentation](https://learn.microsoft.com/en-us/graph/api/signin-list?view=graph-rest-1.0&tabs=http) From 088b7d41b9e8c5a59231d4a6b37942bbfac35899 Mon Sep 17 00:00:00 2001 From: Nisheet Jain Date: Tue, 13 Oct 2026 16:10:54 -0700 Subject: [PATCH 6/6] docs: support observed text message sign-ins Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 749249f5-dd43-4595-896b-1c4b439ae048 --- docs/COST-CALCULATOR.md | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/docs/COST-CALCULATOR.md b/docs/COST-CALCULATOR.md index 5015bf9..3a8326d 100644 --- a/docs/COST-CALCULATOR.md +++ b/docs/COST-CALCULATOR.md @@ -67,14 +67,15 @@ $phoneMethodsByUser = @{} $activity = Get-MgBetaAuditLogSignIn ` -Filter "createdDateTime ge $start and createdDateTime le $end" ` - -Property "userId", "authenticationDetails" ` -All ` -ErrorAction Stop | ForEach-Object { $signIn = $_ $phoneDetails = @( $signIn.AuthenticationDetails | - Where-Object { $_.AuthenticationMethod -in "SMS", "Voice" } + Where-Object { + $_.AuthenticationMethod -in "SMS", "Text message", "Voice" + } ) if ($phoneDetails.Count -gt 0) { @@ -96,6 +97,15 @@ $activity = Get-MgBetaAuditLogSignIn ` $phoneDetails | ForEach-Object { $authenticationDetail = $_ + $authenticationMethod = if ( + $authenticationDetail.AuthenticationMethod -in "SMS", "Text message" + ) { + "SMS" + } + else { + "Voice" + } + $visibleDigits = $authenticationDetail.AuthenticationMethodDetail -replace '\D', '' $matchingMethods = @( @@ -103,7 +113,7 @@ $activity = Get-MgBetaAuditLogSignIn ` Where-Object { $registeredDigits = $_.PhoneNumber -replace '\D', '' $supportsMethod = ( - $authenticationDetail.AuthenticationMethod -eq "Voice" -or + $authenticationMethod -eq "Voice" -or $_.PhoneType -in "mobile", "alternateMobile" ) @@ -125,7 +135,7 @@ $activity = Get-MgBetaAuditLogSignIn ` [PSCustomObject]@{ CountryCallingCode = $callingCode - AuthenticationMethod = $authenticationDetail.AuthenticationMethod + AuthenticationMethod = $authenticationMethod } } } @@ -149,11 +159,16 @@ $report | Export-Csv -Path "C:\temp\SMS_Voice_SignIns_By_Calling_Code.csv" -NoTy ``` Microsoft documents `SMS` and `Voice` as authentication method values and states that -`authenticationMethodDetail` can contain the phone number for those methods. In sign-in -results, that value can be masked, for example `+x XXXXXXXX90`. The script removes -non-digits from that detail and matches the visible suffix (`90`) against the user's -registered methods. It then extracts the calling code from the uniquely matched registered -number, whose Graph format includes a space after the code, such as `+91 1234567890`. +`authenticationMethodDetail` can contain the phone number for those methods. Some tenant +sign-in results report SMS as `Text message`; the script accepts both values and normalizes +them to `SMS` in the exported report. It does not use the cmdlet's `-Property` parameter, +because `authenticationDetails` is returned on the beta sign-in objects without that +selection and the parameter has not behaved consistently across module versions. + +The phone detail can be masked, for example `+x XXXXXXXX90`. The script removes non-digits +from that detail and matches the visible suffix (`90`) against the user's registered +methods. It then extracts the calling code from the uniquely matched registered number, +whose Graph format includes a space after the code, such as `+91 1234567890`. For SMS, only `mobile` and `alternateMobile` registrations are considered; voice can use any registered phone type. A row is grouped as `Unknown` when the masked detail exposes @@ -161,10 +176,10 @@ fewer than two digits, no registered number matches, more than one registered nu the same visible suffix, or the matched number has an unexpected format. This avoids guessing a destination. Review the `Unknown` total before relying on the estimate. -The script does not match an undocumented `Text` method or use sign-in geography. `-All` -requests all available pages, but large tenants should still account for Microsoft Graph -throttling and execution time. The script uses `-ErrorAction Stop` rather than silently -producing a partial report when either sign-in or phone-method retrieval fails. +The script does not use sign-in geography. `-All` requests all available pages, but large +tenants should still account for Microsoft Graph throttling and execution time. The script +uses `-ErrorAction Stop` rather than silently producing a partial report when either +sign-in or phone-method retrieval fails. The Microsoft Graph [list signIns API documentation](https://learn.microsoft.com/en-us/graph/api/signin-list?view=graph-rest-1.0&tabs=http)