diff --git a/README.md b/README.md index 8ed8384..b14ca39 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..3a8326d --- /dev/null +++ b/docs/COST-CALCULATOR.md @@ -0,0 +1,226 @@ +# Estimate SMS provider costs + +Use this guide to create a planning estimate for SMS traffic before onboarding an +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 +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. 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 + +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: + +- `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 +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 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 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", "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. Cache registered phone methods and resolve masked sign-in details +$phoneMethodsByUser = @{} + +$activity = Get-MgBetaAuditLogSignIn ` + -Filter "createdDateTime ge $start and createdDateTime le $end" ` + -All ` + -ErrorAction Stop | + ForEach-Object { + $signIn = $_ + $phoneDetails = @( + $signIn.AuthenticationDetails | + Where-Object { + $_.AuthenticationMethod -in "SMS", "Text message", "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 = $_ + $authenticationMethod = if ( + $authenticationDetail.AuthenticationMethod -in "SMS", "Text message" + ) { + "SMS" + } + else { + "Voice" + } + + $visibleDigits = $authenticationDetail.AuthenticationMethodDetail -replace '\D', '' + + $matchingMethods = @( + $registeredMethods | + Where-Object { + $registeredDigits = $_.PhoneNumber -replace '\D', '' + $supportsMethod = ( + $authenticationMethod -eq "Voice" -or + $_.PhoneType -in "mobile", "alternateMobile" + ) + + $supportsMethod -and + $visibleDigits.Length -ge 2 -and + $registeredDigits.EndsWith($visibleDigits) + } + ) + + $callingCode = if ( + $matchingMethods.Count -eq 1 -and + $matchingMethods[0].PhoneNumber -match '^\s*(\+\d{1,3})(?:\s|$)' + ) { + $Matches[1] + } + else { + "Unknown" + } + + [PSCustomObject]@{ + CountryCallingCode = $callingCode + AuthenticationMethod = $authenticationMethod + } + } + } + } + +# 5. Group and count by calling code and authentication method +$report = $activity | + Group-Object CountryCallingCode, AuthenticationMethod | + ForEach-Object { + [PSCustomObject]@{ + CountryCallingCode = $_.Group[0].CountryCallingCode + AuthenticationMethod = $_.Group[0].AuthenticationMethod + EstimatedRequests = $_.Count + } + } | + Sort-Object CountryCallingCode, AuthenticationMethod + +# 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 +``` + +Microsoft documents `SMS` and `Voice` as authentication method values and states that +`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 +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 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) +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. + +## Convert activity into a cost 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 = 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 matching authentication +steps: + +```text +Estimated SMS/voice requests = 10,000 +``` + +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. + +## Confirm the final estimate with your provider + +Work with your provider to validate: + +- 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. +- 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.