diff --git a/AddAlert/run.ps1 b/AddAlert/run.ps1
index 8ea4cae2d533..d00fcc89d804 100644
--- a/AddAlert/run.ps1
+++ b/AddAlert/run.ps1
@@ -13,18 +13,15 @@ $Results = foreach ($Tenant in $tenants) {
try {
$CompleteObject = [PSCustomObject]@{
tenant = $tenant
- AdminPassword = $Request.body.AdminPassword
- DefenderMalware = $Request.body.DefenderMalware
- DefenderStatus = $Request.body.DefenderStatus
- DisableRestart = $Request.body.DisableRestart
- InstallAsSystem = $Request.body.InstallAsSystem
- MFAAdmins = $Request.body.MFAAdmins
- MFAAlertUsers = $Request.body.MFAAlertUsers
- NewApprovedApp = $Request.body.NewApprovedApp
- NewGA = $Request.body.NewGA
- NewRole = $Request.body.NewRole
- QuotaUsed = $Request.body.QuotaUsed
- UnusedLicenses = $Request.body.UnusedLicenses
+ AdminPassword = [bool]$Request.body.AdminPassword
+ DefenderMalware = [bool]$Request.body.DefenderMalware
+ DefenderStatus = [bool]$Request.body.DefenderStatus
+ MFAAdmins = [bool]$Request.body.MFAAdmins
+ MFAAlertUsers = [bool]$Request.body.MFAAlertUsers
+ NewGA = [bool]$Request.body.NewGA
+ NewRole = [bool]$Request.body.NewRole
+ QuotaUsed = [bool]$Request.body.QuotaUsed
+ UnusedLicenses = [bool]$Request.body.UnusedLicenses
Type = "Alert"
} | ConvertTo-Json
diff --git a/AddEnrollment/run.ps1 b/AddEnrollment/run.ps1
index 7aa229caf943..066dae2ac42f 100644
--- a/AddEnrollment/run.ps1
+++ b/AddEnrollment/run.ps1
@@ -11,9 +11,7 @@ Log-Request -user $request.headers.'x-ms-client-principal' -API $APINAME -messa
Write-Host "PowerShell HTTP trigger function processed a request."
# Input bindings are passed in via param block.
-$user = $request.headers.'x-ms-client-principal'
$Tenants = ($Request.body | Select-Object Select_*).psobject.properties.value
-$AssignTo = if ($request.body.Assignto -ne "on") { $request.body.Assignto }
$Profbod = $Request.body
$results = foreach ($Tenant in $tenants) {
try {
@@ -23,7 +21,7 @@ $results = foreach ($Tenant in $tenants) {
"displayName" = "All users and all devices"
"description" = "This is the default enrollment status screen configuration applied with the lowest priority to all users and all devices regardless of group membership."
"showInstallationProgress" = [bool]$Profbod.ShowProgress
- "blockDeviceSetupRetryByUser" = [bool]$Profbod.AllowRetry
+ "blockDeviceSetupRetryByUser" = [bool]$Profbod.blockDevice
"allowDeviceResetOnInstallFailure" = [bool]$Profbod.AllowReset
"allowLogCollectionOnInstallFailure" = [bool]$Profbod.EnableLog
"customErrorMessage" = $Profbod.ErrorMessage
diff --git a/BestPracticeAnalyser_All/run.ps1 b/BestPracticeAnalyser_All/run.ps1
index 2dff99e2b726..4e46f7f6bd7b 100644
--- a/BestPracticeAnalyser_All/run.ps1
+++ b/BestPracticeAnalyser_All/run.ps1
@@ -226,12 +226,14 @@ try {
'X-Requested-With' = 'XMLHttpRequest'
}
+ # Import the licenses conversion table
+ $ConvertTable = Import-Csv Conversiontable.csv | Sort-Object -Property 'guid' -Unique
$WhiteListedSKUs = "FLOW_FREE", "TEAMS_EXPLORATORY", "TEAMS_COMMERCIAL_TRIAL", "POWERAPPS_VIRAL", "POWER_BI_STANDARD", "DYN365_ENTERPRISE_P1_IW", "STREAM", "Dynamics 365 for Financials for IWs", "POWERAPPS_PER_APP_IW"
$UnusedLicenses = $LicenseUsage | Where-Object { ($_.Purchased -ne $_.Consumed) -and ($WhiteListedSKUs -notcontains $_.AccountSkuId.SkuPartNumber) }
$UnusedLicensesCount = $UnusedLicenses | Measure-Object | Select-Object -ExpandProperty Count
$UnusedLicensesResult = if ($UnusedLicensesCount -gt 0) { "FAIL" } else { "PASS" }
$Result.UnusedLicenseList = ($UnusedLicensesListBuilder = foreach ($License in $UnusedLicenses) {
- "SKU: $($License.AccountSkuId.SkuPartNumber), Purchased: $($License.Purchased), Consumed: $($License.Consumed)"
+ "License: $($License.Name), Purchased: $($License.Purchased), Consumed: $($License.Consumed)"
}) -join "
"
$TempCount = 0
diff --git a/DNSHelper.psm1 b/DNSHelper.psm1
index f6dc3c5ee591..65a411f29cf4 100644
--- a/DNSHelper.psm1
+++ b/DNSHelper.psm1
@@ -59,21 +59,36 @@ function Resolve-DnsHttpsQuery {
Write-Verbose "### $Uri ###"
- try {
- $Results = Invoke-RestMethod -Uri $Uri -Headers $Headers -ErrorAction Stop
- }
- catch {
- Write-Verbose "$Resolver DoH Query Exception - $($_.Exception.Message)"
- return $null
- }
+ $Retry = 0
+ $DataReturned = $false
+ while (!$DataReturned) {
+ try {
+ $Results = Invoke-RestMethod -Uri $Uri -Headers $Headers -ErrorAction Stop
+ }
+ catch {
+ Write-Verbose "$Resolver DoH Query Exception - $($_.Exception.Message)"
+ }
- if ($Resolver -eq 'Cloudflare' -and $RecordType -eq 'txt' -and $Results.Answer) {
- $Results.Answer | ForEach-Object {
- $_.data = $_.data -replace '"' -replace '\s+', ' '
+ if ($Resolver -eq 'Cloudflare' -and $RecordType -eq 'txt' -and $Results.Answer) {
+ $Results.Answer | ForEach-Object {
+ $_.data = $_.data -replace '"' -replace '\s+', ' '
+ }
+ }
+
+ if ($Results.Answer) {
+ $DataReturned = $true
+ }
+ else {
+ if ($Retry -gt 3) {
+ $Results = $null
+ $DataReturned = $true
+ }
+ $Retry++
+ Start-Sleep -Milliseconds 50
}
}
- #Write-Verbose ($Results | ConvertTo-Json)
+ Write-Verbose ($Results | ConvertTo-Json)
return $Results
}
@@ -121,7 +136,7 @@ function Test-DNSSEC {
$RecordCount = ($Result.Answer.data | Measure-Object).Count
if ($null -eq $Result) {
- $ValidationFails.Add('DNSSEC validation failed, no dnskey record found') | Out-Null
+ $ValidationFails.Add('DNSSEC is not set up for this domain.') | Out-Null
}
else {
if ($Result.Status -eq 2) {
@@ -130,19 +145,19 @@ function Test-DNSSEC {
}
}
elseif ($Result.Status -eq 3) {
- $ValidationFails.Add('Record does not exist (NXDOMAIN)') | Out-Null
+ $ValidationFails.Add('DNSSEC is not set up for this domain.') | Out-Null
}
elseif ($RecordCount -gt 0) {
if ($Result.AD -eq $false) {
- $ValidationFails.Add('DNSSEC enabled, but response was not validated. Ensure DNSSEC has been enabled at your registrar') | Out-Null
+ $ValidationFails.Add('DNSSEC is enabled, but the DNS query response was not validated. Ensure DNSSEC has been enabled on your domain provider.') | Out-Null
}
else {
- $ValidationPasses.Add('DNSSEC enabled and validated for this domain') | Out-Null
+ $ValidationPasses.Add('DNSSEC is enabled and validated for this domain.') | Out-Null
}
$DSResults.Keys = $Result.answer.data
}
else {
- $ValidationFails.Add('DNSSEC validation failed, no dnskey record found') | Out-Null
+ $ValidationFails.Add('DNSSEC is not set up for this domain.') | Out-Null
}
}
@@ -194,12 +209,12 @@ function Read-NSRecord {
}
catch { $Result = $null }
if ($Result.Status -ne 0 -or -not ($Result.Answer)) {
- $ValidationFails.Add("$Domain - NS record does not exist") | Out-Null
+ $ValidationFails.Add('No nameservers found for this domain.') | Out-Null
$NSRecords = $null
}
else {
$NSRecords = $Result.Answer.data
- $ValidationPasses.Add("$Domain - NS record is present") | Out-Null
+ $ValidationPasses.Add('Nameserver record is present.') | Out-Null
$NSResults.Records = @($NSRecords)
}
$NSResults.ValidationPasses = $ValidationPasses
@@ -252,6 +267,8 @@ function Read-MXRecord {
RecordType = 'mx'
Domain = $Domain
}
+
+ $NoMxValidation = 'There are no mail exchanger records for this domain. If you do not want to receive mail for this domain use a Null MX record of . with a priority 0 (RFC 7505).'
$MXResults.Domain = $Domain
@@ -261,12 +278,12 @@ function Read-MXRecord {
catch { $Result = $null }
if ($Result.Status -ne 0 -or -not ($Result.Answer)) {
if ($Result.Status -eq 3) {
- $ValidationFails.Add('Record does not exist (nxdomain). If you do not want to receive mail for this domain use a Null MX record of . with a priority 0 (RFC 7505)') | Out-Null
+ $ValidationFails.Add($NoMxValidation) | Out-Null
$MXResults.MailProvider = Get-Content 'MailProviders\Null.json' | ConvertFrom-Json
$MXResults.Selectors = $MXRecords.MailProvider.Selectors
}
else {
- $ValidationFails.Add("$Domain - MX record does not exist, if you do not want to receive mail for this domain use a Null MX record of . with a priority 0 (RFC 7505)") | Out-Null
+ $ValidationFails.Add($NoMxValidation) | Out-Null
$MXResults.MailProvider = Get-Content 'MailProviders\Null.json' | ConvertFrom-Json
$MXResults.Selectors = $MXRecords.MailProvider.Selectors
}
@@ -283,7 +300,7 @@ function Read-MXRecord {
}
catch {}
}
- $ValidationPasses.Add("$Domain - MX record is present") | Out-Null
+ $ValidationPasses.Add('Mail exchanger records record(s) are present for this domain.') | Out-Null
$MXRecords = $MXRecords | Sort-Object -Property Priority
# Attempt to identify mail provider based on MX record
@@ -292,7 +309,7 @@ function Read-MXRecord {
'DomainNameDashNotation' = $Domain -replace '\.', '-'
}
if ($MXRecords.Hostname -eq '') {
- $ValidationFails.Add("Blank MX record found for $Domain, if you do not want to receive mail for this domain use a Null MX record of . with a priority 0 (RFC 7505)") | Out-Null
+ $ValidationFails.Add($NoMxValidation) | Out-Null
$MXResults.MailProvider = Get-Content 'MailProviders\Null.json' | ConvertFrom-Json
}
else {
@@ -313,7 +330,7 @@ function Read-MXRecord {
}
}
- $ExpectedInclude = $Provider.SpfInclude -f ($ReplaceList -join ',')
+ $ExpectedInclude = $Provider.SpfInclude -f ($ReplaceList -join ', ')
}
else {
$ExpectedInclude = $Provider.SpfInclude
@@ -425,6 +442,8 @@ function Read-SpfRecord {
Domain = $Domain
}
+ $NoSpfValidation = 'No SPF record was detected for this domain.'
+
# Query DNS for SPF Record
try {
switch ($PSCmdlet.ParameterSetName) {
@@ -436,25 +455,27 @@ function Read-SpfRecord {
$Query = Resolve-DnsHttpsQuery @DnsQuery
if ($Query.Status -ne 0) {
if ($Query.Status -eq 3) {
- $ValidationFails.Add("$Domain - Record does not exist, nxdomain") | Out-Null
+ $ValidationFails.Add($NoSpfValidation) | Out-Null
$Status = 'permerror'
}
else {
- $ValidationFails.Add("$Domain - Does not resolve an SPF record.") | Out-Null
+ Write-Host $Query
+ $ValidationFails.Add($NoSpfValidation) | Out-Null
$Status = 'temperror'
}
}
else {
+
$Answer = ($Query.answer | Where-Object { $_.data -match '^v=spf1' })
- $RecordCount = ($Answer | Measure-Object).count
+ $RecordCount = ($Answer.data | Measure-Object).count
$Record = $Answer.data
if ($RecordCount -eq 0) {
- $ValidationFails.Add("$Domain does not resolve an SPF record.") | Out-Null
+ $ValidationFails.Add($NoSpfValidation) | Out-Null
$Status = 'permerror'
}
# Check for the correct number of records
elseif ($RecordCount -gt 1 -and $Level -eq 'Parent') {
- $ValidationFails.Add("There must only be one SPF record, $RecordCount detected") | Out-Null
+ $ValidationFails.Add("There must only be one SPF record per domain, we found $RecordCount.") | Out-Null
$Recommendations.Add([pscustomobject]@{
Message = 'Delete one of the records beginning with v=spf1'
Match = ''
@@ -489,7 +510,7 @@ function Read-SpfRecord {
if ($null -ne $Matches.Discard) {
if ($Matches.Discard -notmatch '^exp=(?.+)$') {
- $ValidationWarns.Add("$Domain - The terms '$($Matches.Discard)' are past the all mechanism and will be discarded") | Out-Null
+ $ValidationWarns.Add("The terms '$($Matches.Discard)' are past the all mechanism and will be discarded.") | Out-Null
$Recommendations.Add([pscustomobject]@{
Message = 'Remove entries following all';
Match = $Matches.Discard
@@ -505,7 +526,7 @@ function Read-SpfRecord {
Write-Verbose '-----REDIRECT-----'
$LookupCount++
if ($Record -match '(?[+-~?])all') {
- $ValidationFails.Add("$Domain - A record with a redirect modifier must not contain an all mechanism, permerror") | Out-Null
+ $ValidationFails.Add('A record with a redirect modifier must not contain an all mechanism. This will result in a failure.') | Out-Null
$Status = 'permerror'
$Recommendations.Add([pscustomobject]@{
Message = "Remove the 'all' mechanism from this record.";
@@ -517,7 +538,7 @@ function Read-SpfRecord {
# Follow redirect modifier
$RedirectedLookup = Read-SpfRecord -Domain $Matches.Domain -Level 'Redirect'
if (($RedirectedLookup | Measure-Object).Count -eq 0) {
- $ValidationFails.Add("$Domain Redirected lookup does not contain a SPF record, permerror") | Out-Null
+ $ValidationFails.Add("$Domain Redirected lookup does not contain a SPF record, this will result in a failure.") | Out-Null
$Status = 'permerror'
}
else {
@@ -543,9 +564,9 @@ function Read-SpfRecord {
Write-Verbose "Looking up include $($Matches.Value)"
$IncludeLookup = Read-SpfRecord -Domain $Matches.Value -Level 'Include'
- if (($IncludeLookup | Measure-Object).Count -eq 0) {
+ if ([string]::IsNullOrEmpty($IncludeLookup.Record) -and $Level -eq 'Parent') {
Write-Verbose '-----END INCLUDE (SPF MISSING)-----'
- $ValidationFails.Add("$Domain Include lookup does not contain a SPF record, permerror") | Out-Null
+ $ValidationFails.Add("Include lookup for $($Matches.Value) does not contain a SPF record, this will result in a failure.") | Out-Null
$Status = 'permerror'
}
else {
@@ -600,7 +621,7 @@ function Read-SpfRecord {
}
if ($MxCount -gt 10) {
- $ValidationWarns.Add("$Domain - Mechanism 'mx' lookup for $MxDomain exceeded the 10 lookup limit (RFC 7208, Section 4.6.4") | Out-Null
+ $ValidationWarns.Add("$Domain - Mechanism 'mx' lookup for $MxDomain has exceeded the 10 lookup limit(RFC 7208, Section 4.6.4).") | Out-Null
$TypeResult = $null
break
}
@@ -615,7 +636,7 @@ function Read-SpfRecord {
}
if ($null -eq $TypeResult -or $TypeResult.Status -ne 0) {
- $Message = "$Domain - Type lookup for mechanism '$($TypeQuery.RecordType)' did not return any results"
+ $Message = "$Domain - Type lookup for the mechanism '$($TypeQuery.RecordType)' did not return any results."
switch ($Level) {
'Parent' {
$ValidationFails.Add("$Message") | Out-Null
@@ -646,7 +667,7 @@ function Read-SpfRecord {
}
else {
- $ValidationWarns.Add("No domain specified and mechanism '$Term' does not have one defined. Specify a domain to perform a lookup on this record.") | Out-Null
+ $ValidationWarns.Add("No domain was specified and mechanism '$Term' does not have one defined. Specify a domain to perform a lookup on this record.") | Out-Null
}
}
@@ -673,17 +694,17 @@ function Read-SpfRecord {
if ($MXRecord.MailProvider.Name -eq 'Null') {
if ($Record -eq 'v=spf1 -all') {
- $ValidationPasses.Add('SPF record is valid for a Null MX configuration') | Out-Null
+ $ValidationPasses.Add('This SPF record is valid for a Null MX configuration') | Out-Null
}
else {
- $ValidationFails.Add('SPF record is not valid for a Null MX configuration. Expected record: "v=spf1 -all"') | Out-Null
+ $ValidationFails.Add('This SPF record is not valid for a Null MX configuration. Expected record: "v=spf1 -all"') | Out-Null
}
}
if ($TypeLookups.RecordType -contains 'mx') {
$Recommendations.Add([pscustomobject]@{
Message = "Remove the 'mx' modifier from your record. Check the mail provider documentation for the correct SPF include.";
- Match = '\s*(mx)\s+'
+ Match = '\s*([+-~?]?mx)\s+'
Replace = ' '
}) | Out-Null
}
@@ -698,19 +719,19 @@ function Read-SpfRecord {
$ExpectedIPCount = $ExpectedIncludeSpf.IPAddresses | Measure-Object | Select-Object -ExpandProperty Count
$FoundIPCount = Compare-Object $IPAddresses $ExpectedIncludeSpf.IPAddresses -IncludeEqual | Where-Object -Property SideIndicator -EQ '==' | Measure-Object | Select-Object -ExpandProperty Count
if ($ExpectedIPCount -eq $FoundIPCount) {
- $ValidationPasses.Add("Expected SPF ($ExpectedInclude) IP addresses were found") | Out-Null
+ $ValidationPasses.Add('The expected mail provider IP address ranges were found.') | Out-Null
}
else {
- $ValidationFails.Add("Expected SPF include of '$ExpectedInclude' was not found in the SPF record") | Out-Null
+ $ValidationFails.Add('The expected mail provider entry was not found in the record.') | Out-Null
$Recommendations.Add([pscustomobject]@{
Message = ("Add 'include:{0} to your record." -f $ExpectedInclude)
Match = '^v=spf1 (.+?)([-~?+]all)?$'
- Replace = "v=spf1 `$1 include:$ExpectedInclude `$2"
+ Replace = "v=spf1 include:$ExpectedInclude `$1 `$2"
}) | Out-Null
}
}
else {
- $ValidationPasses.Add("Expected SPF record ($ExpectedInclude) was included") | Out-Null
+ $ValidationPasses.Add('The expected mail provider entry is part of the record.') | Out-Null
}
}
@@ -721,23 +742,24 @@ function Read-SpfRecord {
# Check legacy SPF type
$LegacySpfType = Resolve-DnsHttpsQuery -Domain $Domain -RecordType 'SPF'
if ($null -ne $LegacySpfType -and $LegacySpfType -eq 0) {
- $ValidationWarns.Add("Domain: $Domain Record Type SPF detected, this is legacy and should not be used. It is recommeded to delete this record. (RFC 7208 Section 14.1)") | Out-Null
+ $ValidationWarns.Add("The record type 'SPF' was detected, this is legacy and should not be used. It is recommeded to delete this record (RFC 7208 Section 14.1).") | Out-Null
}
}
if ($Level -eq 'Parent' -and $RecordCount -gt 0) {
# Check for the correct all mechanism
if ($AllMechanism -eq '' -and $Record -ne '') {
- $ValidationFails.Add('All mechanism is missing from SPF record, defaulting to ?all') | Out-Null
+ $ValidationFails.Add("The 'all' mechanism is missing from SPF record, the default is a neutral qualifier (?all).") | Out-Null
$AllMechanism = '?all'
}
+
if ($AllMechanism -eq '-all') {
- $ValidationPasses.Add('SPF record ends in -all') | Out-Null
+ $ValidationPasses.Add('The SPF record ends with a hard fail qualifier (-all). This is best practice and will instruct recipients to discard unauthorized senders.') | Out-Null
}
elseif ($Record -ne '') {
- $ValidationFails.Add('SPF record should end in -all to prevent spamming') | Out-Null
+ $ValidationFails.Add('The SPF record should end in -all to prevent spamming.') | Out-Null
$Recommendations.Add([PSCustomObject]@{
Message = "Replace '{0}' with '-all' to make a SPF failure result in a hard fail." -f $AllMechanism
- Match = $AllMechanism
+ Match = [regex]::escape($AllMechanism)
Replace = '-all'
}) | Out-Null
}
@@ -749,9 +771,9 @@ function Read-SpfRecord {
if ($SpfRecord.LookupCount -ge 5) {
$SpecificLookupsFound = $true
$IncludeLookupCount = $SpfRecord.LookupCount + 1
- $Match = ('include:{0}' -f $SpfRecord.Domain)
+ $Match = ('[+-~?]?include:{0}' -f $SpfRecord.Domain)
$Recommendations.Add([PSCustomObject]@{
- Message = ("Remove include modifier for domain '{0}', this adds {1} lookups towards the max of 10. Alternatively, reduce the number of lookups inside this record if you are able to." -f $SpfRecord.Domain, $IncludeLookupCount)
+ Message = ("Remove the include modifier for domain '{0}', this adds {1} lookups towards the max of 10. Alternatively, reduce the number of lookups inside this record if you are able to." -f $SpfRecord.Domain, $IncludeLookupCount)
Match = $Match
Replace = ''
}) | Out-Null
@@ -766,24 +788,24 @@ function Read-SpfRecord {
}
if ($LookupCount -gt 10) {
- $ValidationFails.Add("Lookup count: $LookupCount/10. SPF evaluation will fail with a permerror (RFC 7208 Section 4.6.4)") | Out-Null
+ $ValidationFails.Add("Lookup count: $LookupCount/10. The SPF evaluation will fail with a permanent error (RFC 7208 Section 4.6.4).") | Out-Null
$Status = 'permerror'
}
elseif ($LookupCount -ge 9 -and $LookupCount -le 10) {
- $ValidationWarns.Add("Lookup count: $LookupCount/10. Excessive lookups can cause the SPF evaluation to fail (RFC 7208 Section 4.6.4)") | Out-Null
+ $ValidationWarns.Add("Lookup count: $LookupCount/10. Excessive lookups can cause the SPF evaluation to fail (RFC 7208 Section 4.6.4).") | Out-Null
}
else {
- $ValidationPasses.Add("Lookup count: $LookupCount/10") | Out-Null
+ $ValidationPasses.Add("Lookup count: $LookupCount/10.") | Out-Null
}
# Report pass if no PermErrors are found
if ($Status -ne 'permerror') {
- $ValidationPasses.Add('No PermError detected in SPF record') | Out-Null
+ $ValidationPasses.Add('No permanent errors detected in the SPF record.') | Out-Null
}
# Report pass if no errors are found
if (($ValidationFails | Measure-Object | Select-Object -ExpandProperty Count) -eq 0) {
- $ValidationPasses.Add('All validation succeeded. No errors detected with SPF record') | Out-Null
+ $ValidationPasses.Add('All validation checks passed.') | Out-Null
}
}
@@ -912,15 +934,13 @@ function Read-DmarcPolicy {
}
if ($Query.Status -ne 0 -or $RecordCount -eq 0) {
- if ($Query.Status -eq 3) {
- $ValidationFails.Add('Record does not exist (NXDOMAIN)') | Out-Null
- }
- else {
- $ValidationFails.Add("$Domain does not have a DMARC record") | Out-Null
- }
+ $ValidationFails.Add('This domain does not have a DMARC record.') | Out-Null
+ }
+ elseif (($Query.Answer | Measure-Object).Count -eq 1 -and $RecordCount -eq 0) {
+ $ValidationFails.Add("The record must begin with 'v=DMARC1'.") | Out-Null
}
elseif ($RecordCount -gt 1) {
- $ValidationFails.Add("$Domain has multiple DMARC records") | Out-Null
+ $ValidationFails.Add('This domain has multiple records. The policy evaluation will fail.') | Out-Null
}
# Split DMARC record into name/value pairs
@@ -941,8 +961,6 @@ function Read-DmarcPolicy {
switch ($Tag.Name) {
'v' {
# REQUIRED: Version
- if ($x -ne 0) { $ValidationFails.Add('v=DMARC1 must be at the beginning of the record') | Out-Null }
- if ($Tag.Value -ne 'DMARC1') { $ValidationFails.Add("Version must be DMARC1 - found $($Tag.Value)") | Out-Null }
$DmarcAnalysis.Version = $Tag.Value
}
'p' {
@@ -958,7 +976,7 @@ function Read-DmarcPolicy {
$ReportingEmails = $Tag.Value -split ', '
$ReportEmailsSet = $false
foreach ($MailTo in $ReportingEmails) {
- if ($MailTo -notmatch '^mailto:') { $ValidationFails.Add("Aggregate report email must begin with 'mailto:', multiple addresses must be separated by commas - found $($Tag.Value)") | Out-Null }
+ if ($MailTo -notmatch '^mailto:') { $ValidationFails.Add("Aggregate report email addresses must begin with 'mailto:', multiple addresses must be separated by commas.") | Out-Null }
else {
$ReportEmailsSet = $true
if ($MailTo -match '^mailto:(?.+@(?[^!]+?))(?:!(?[0-9]+[kmgt]?))?$') {
@@ -970,10 +988,10 @@ function Read-DmarcPolicy {
}
}
if ($ReportEmailsSet) {
- $ValidationPasses.Add('Aggregate reports are being sent') | Out-Null
+ $ValidationPasses.Add('Aggregate reports are being sent.') | Out-Null
}
else {
- $ValidationWarns.Add('Aggregate reports are not being sent') | Out-Null
+ $ValidationWarns.Add('Aggregate reports are not being sent.') | Out-Null
}
}
'ruf' {
@@ -1029,59 +1047,55 @@ function Read-DmarcPolicy {
$ReportDmarcQuery = Resolve-DnsHttpsQuery @DnsQuery
$ReportDmarcRecord = $ReportDmarcQuery.Answer.data
if ($null -eq $ReportDmarcQuery -or $ReportDmarcQuery.Status -ne 0) {
- $ValidationWarns.Add("Report DMARC policy for $Domain is missing from $ReportDomain, reports will not be delivered. Expected record: $Domain._report._dmarc.$ReportDomain - Expected value: v=DMARC1; ") | Out-Null
+ $ValidationWarns.Add("Report DMARC policy for $Domain is missing from $ReportDomain, reports will not be delivered. Expected record: '$Domain._report._dmarc.$ReportDomain' - Expected value: 'v=DMARC1;'") | Out-Null
$ReportDomainsPass = $false
}
elseif ($ReportDmarcRecord -notmatch '^v=DMARC1') {
- $ValidationWarns.Add("Report DMARC policy for $Domain is missing from $ReportDomain, reports will not be delivered. Expected record: $Domain._report._dmarc.$ReportDomain - Expected value: v=DMARC1; ") | Out-Null
+ $ValidationWarns.Add("Report DMARC policy for $Domain is missing from $ReportDomain, reports will not be delivered. Expected record: '$Domain._report._dmarc.$ReportDomain' - Expected value: 'v=DMARC1;'.") | Out-Null
$ReportDomainsPass = $false
}
}
if ($ReportDomainsPass) {
- $ValidationPasses.Add("All external reporting domains ($($ReportDomains -join ', ')) allow $Domain to send DMARC reports") | Out-Null
+ $ValidationPasses.Add('All external reporting domains allow this domain to send DMARC reports.') | Out-Null
}
}
# Check for missing record tags and set defaults
- if ($DmarcAnalysis.Policy -eq '') { $ValidationFails.Add('Policy record is missing') | Out-Null }
+ if ($DmarcAnalysis.Policy -eq '') { $ValidationFails.Add('The policy tag (p=) is missing from this record. Set this to none, quarantine or reject.') | Out-Null }
if ($DmarcAnalysis.SubdomainPolicy -eq '') { $DmarcAnalysis.SubdomainPolicy = $DmarcAnalysis.Policy }
# Check policy for errors and best practice
- if ($PolicyValues -notcontains $DmarcAnalysis.Policy) { $ValidationFails.Add("Policy must be one of the following - none, quarantine, reject. Found $($Tag.Value)") | Out-Null }
- if ($DmarcAnalysis.Policy -eq 'reject') { $ValidationPasses.Add('Policy is sufficiently strict') | Out-Null }
- if ($DmarcAnalysis.Policy -eq 'quarantine') { $ValidationWarns.Add('Policy is only partially enforced with quarantine') | Out-Null }
- if ($DmarcAnalysis.Policy -eq 'none') { $ValidationFails.Add('Policy is not being enforced') | Out-Null }
+ if ($PolicyValues -notcontains $DmarcAnalysis.Policy) { $ValidationFails.Add("The policy must be one of the following: none, quarantine or reject. Found $($Tag.Value)") | Out-Null }
+ if ($DmarcAnalysis.Policy -eq 'reject') { $ValidationPasses.Add('The domain policy is set to reject, this is best practice.') | Out-Null }
+ if ($DmarcAnalysis.Policy -eq 'quarantine') { $ValidationWarns.Add('The domain policy is only partially enforced with quarantine.') | Out-Null }
+ if ($DmarcAnalysis.Policy -eq 'none') { $ValidationFails.Add('The domain policy is not being enforced.') | Out-Null }
# Check subdomain policy
- if ($PolicyValues -notcontains $DmarcAnalysis.SubdomainPolicy) { $ValidationFails.Add("Subdomain policy must be one of the following - none, quarantine, reject. Found $($DmarcAnalysis.SubdomainPolicy)") | Out-Null }
- if ($DmarcAnalysis.SubdomainPolicy -eq 'reject') { $ValidationPasses.Add('Subdomain policy is sufficiently strict') | Out-Null }
- if ($DmarcAnalysis.SubdomainPolicy -eq 'quarantine') { $ValidationWarns.Add('Subdomain policy is only partially enforced with quarantine') | Out-Null }
- if ($DmarcAnalysis.SubdomainPolicy -eq 'none') { $ValidationFails.Add('Subdomain policy is not being enforced') | Out-Null }
+ if ($PolicyValues -notcontains $DmarcAnalysis.SubdomainPolicy) { $ValidationFails.Add("The subdomain policy must be one of the following: none, quarantine or reject. Found $($DmarcAnalysis.SubdomainPolicy)") | Out-Null }
+ if ($DmarcAnalysis.SubdomainPolicy -eq 'reject') { $ValidationPasses.Add('The subdomain policy is set to reject, this is best practice.') | Out-Null }
+ if ($DmarcAnalysis.SubdomainPolicy -eq 'quarantine') { $ValidationWarns.Add('The subdomain policy is only partially enforced with quarantine.') | Out-Null }
+ if ($DmarcAnalysis.SubdomainPolicy -eq 'none') { $ValidationFails.Add('The subdomain policy is not being enforced.') | Out-Null }
# Check percentage - validate range and ensure 100%
- if ($DmarcAnalysis.Percent -lt 100 -and $DmarcAnalysis.Percent -gt 0) { $ValidationWarns.Add('Not all emails will be processed by the DMARC policy') | Out-Null }
- if ($DmarcAnalysis.Percent -gt 100 -or $DmarcAnalysis.Percent -lt 1) { $ValidationFails.Add('Percentage must be between 1 and 100') | Out-Null }
+ if ($DmarcAnalysis.Percent -lt 100 -and $DmarcAnalysis.Percent -ge 0) { $ValidationWarns.Add('Not all emails will be processed by the DMARC policy.') | Out-Null }
+ if ($DmarcAnalysis.Percent -gt 100 -or $DmarcAnalysis.Percent -lt 0) { $ValidationFails.Add('The percentage tag (pct=) must be between 0 and 100.') | Out-Null }
# Check report format
- if ($ReportFormatValues -notcontains $DmarcAnalysis.ReportFormat) { $ValidationFails.Add("The report format '$($DmarcAnalysis.ReportFormat)' is not supported") | Out-Null }
+ if ($ReportFormatValues -notcontains $DmarcAnalysis.ReportFormat) { $ValidationFails.Add("The report format '$($DmarcAnalysis.ReportFormat)' is not supported.") | Out-Null }
# Check forensic reports and failure options
$ForensicCount = ($DmarcAnalysis.ForensicEmails | Measure-Object | Select-Object -ExpandProperty Count)
- if ($ForensicCount -eq 0 -and $DmarcAnalysis.FailureReport -ne '') { $ValidationWarns.Add('Forensic email reports recipients are not defined and failure report options are set. No reports will be sent.') | Out-Null }
+ if ($ForensicCount -eq 0 -and $DmarcAnalysis.FailureReport -ne '') { $ValidationWarns.Add('Forensic email reports recipients are not defined and failure report options are set. No reports will be sent. This is not an issue unless you are expecting forensic reports.') | Out-Null }
if ($DmarcAnalysis.FailureReport -eq '' -and $null -ne $DmarcRecord) { $DmarcAnalysis.FailureReport = '0' }
if ($ForensicCount -gt 0) {
- if ($FailureReportValues -notcontains $DmarcAnalysis.FailureReport) { $ValidationFails.Add('Failure reporting options must be 0, 1, d or s') | Out-Null }
- if ($DmarcAnalysis.FailureReport -eq '1') { $ValidationPasses.Add('Failure report option 1 generates forensic reports on SPF or DKIM misalignment') | Out-Null }
- if ($DmarcAnalysis.FailureReport -eq '0') { $ValidationWarns.Add('Failure report option 0 will only generate a forensic report on both SPF and DKIM misalignment. It is recommended to set this value to 1') | Out-Null }
- if ($DmarcAnalysis.FailureReport -eq 'd') { $ValidationWarns.Add('Failure report option d will only generate a forensic report on failed DKIM evaluation. It is recommended to set this value to 1') | Out-Null }
- if ($DmarcAnalysis.FailureReport -eq 's') { $ValidationWarns.Add('Failure report option s will only generate a forensic report on failed SPF evaluation. It is recommended to set this value to 1') | Out-Null }
+ if ($FailureReportValues -notcontains $DmarcAnalysis.FailureReport) { $ValidationFails.Add('Failure reporting options must be 0, 1, d or s.') | Out-Null }
+ if ($DmarcAnalysis.FailureReport -eq '1') { $ValidationPasses.Add('Failure report option 1 generates forensic reports on SPF or DKIM misalignment.') | Out-Null }
+ if ($DmarcAnalysis.FailureReport -eq '0') { $ValidationWarns.Add('Failure report option 0 will only generate a forensic report on both SPF and DKIM misalignment. It is recommended to set this value to 1.') | Out-Null }
+ if ($DmarcAnalysis.FailureReport -eq 'd') { $ValidationWarns.Add('Failure report option d will only generate a forensic report on failed DKIM evaluation. It is recommended to set this value to 1.') | Out-Null }
+ if ($DmarcAnalysis.FailureReport -eq 's') { $ValidationWarns.Add('Failure report option s will only generate a forensic report on failed SPF evaluation. It is recommended to set this value to 1.') | Out-Null }
}
}
-
- if ($RecordCount -gt 1) {
- $ValidationWarns.Add('Multiple DMARC records detected, this may cause unexpected behavior.') | Out-Null
- }
# Add the validation lists
$DmarcAnalysis.ValidationPasses = @($ValidationPasses)
@@ -1128,6 +1142,7 @@ function Read-DkimRecord {
$DkimAnalysis = [PSCustomObject]@{
Domain = $Domain
+ Selectors = $Selectors
MailProvider = ''
Records = [System.Collections.Generic.List[object]]::new()
ValidationPasses = [System.Collections.Generic.List[string]]::new()
@@ -1150,6 +1165,7 @@ function Read-DkimRecord {
if ($MXRecord.MailProvider.PSObject.Properties.Name -contains 'MinimumSelectorPass') {
$MinimumSelectorPass = $MXRecord.MailProvider.MinimumSelectorPass
}
+ $DkimAnalysis.Selectors = $Selectors
}
catch {}
@@ -1168,13 +1184,9 @@ function Read-DkimRecord {
}
catch {}
}
-
- if (($Selectors | Measure-Object | Select-Object -ExpandProperty Count) -eq 0) {
- $ValidationFails.Add("$Domain - No selectors provided") | Out-Null
- }
}
- if (($Selectors | Measure-Object | Select-Object -ExpandProperty Count) -gt 0 -and $Selectors -notcontains '') {
+ if (($Selectors | Measure-Object | Select-Object -ExpandProperty Count) -gt 0) {
foreach ($Selector in $Selectors) {
# Initialize object
$DkimRecord = [PSCustomObject]@{
@@ -1199,10 +1211,12 @@ function Read-DkimRecord {
$QueryResults = Resolve-DnsHttpsQuery @DnsQuery
+ if ([string]::IsNullOrEmpty($Selector)) { continue }
+
if ($QueryResults -eq '' -or $QueryResults.Status -ne 0) {
if ($QueryResults.Status -eq 3) {
if ($MinimumSelectorPass -eq 0) {
- $ValidationFails.Add("$Selector - Selector record does not exist (NXDOMAIN)") | Out-Null
+ $ValidationFails.Add("$Selector - The selector record does not exist for this domain.") | Out-Null
}
}
else {
@@ -1211,7 +1225,7 @@ function Read-DkimRecord {
$Record = ''
}
else {
- $QueryData = ($QueryResults.Answer).data | Where-Object { $_ -match '^v=DKIM1' }
+ $QueryData = ($QueryResults.Answer).data | Where-Object { $_ -match '(v=|k=|t=|p=)' }
if (( $QueryData | Measure-Object).Count -gt 1) {
$Record = $QueryData[-1]
}
@@ -1240,12 +1254,13 @@ function Read-DkimRecord {
# Loop through name/value pairs and set object properties
$x = 0
- foreach ($Tag in $TagList) {
+ foreach ($Tag in $TagList) {
+ if ($x -eq 0 -and $Tag.Value -ne 'DKIM1') { $ValidationFails.Add("$Selector - The record must being with 'v=DKIM1'.") | Out-Null }
+
switch ($Tag.Name) {
'v' {
# REQUIRED: Version
- if ($x -ne 0) { $ValidationFails.Add("$Selector - v=DKIM1 must be at the beginning of the record") | Out-Null }
- if ($Tag.Value -ne 'DKIM1') { $ValidationFails.Add("$Selector - Version must be DKIM1 - found $($Tag.Value)") | Out-Null }
+ if ($x -ne 0) { $ValidationFails.Add("$Selector - The record must being with 'v=DKIM1'.") | Out-Null }
$DkimRecord.Version = $Tag.Value
}
'p' {
@@ -1256,10 +1271,10 @@ function Read-DkimRecord {
}
else {
if ($MXRecord.MailProvider.Name -eq 'Null') {
- $ValidationPasses.Add("$Selector - DKIM configuration is valid for a Null MX record configuration") | Out-Null
+ $ValidationPasses.Add("$Selector - DKIM configuration is valid for a Null MX record configuration.") | Out-Null
}
else {
- $ValidationFails.Add("$Selector - No public key specified for DKIM record or key revoked") | Out-Null
+ $ValidationFails.Add("$Selector - There is no public key specified for this DKIM record or the key is revoked.") | Out-Null
}
}
}
@@ -1296,10 +1311,10 @@ function Read-DkimRecord {
$UnrecognizedTagCount = $UnrecognizedTags | Measure-Object | Select-Object -ExpandProperty Count
if ($UnrecognizedTagCount -gt 0) {
$TagString = ($UnrecognizedTags | ForEach-Object { '{0}={1}' -f $_.Tag, $_.Value }) -join ', '
- $ValidationWarns.Add("$Selector - $UnrecognizedTagCount tag(s) detected in DKIM record. This can cause issues with some mailbox providers. Tags: $TagString")
+ $ValidationWarns.Add("$Selector - $UnrecognizedTagCount urecognized tag(s) were detected in the DKIM record. This can cause issues with some mailbox providers. Tags: $TagString")
}
if ($DkimRecord.Flags -eq 'y') {
- $ValidationWarns.Add("$Selector - This flag 't=y' indicates that this domain is testing mode currently. If DKIM is fully deployed, this flag should be changed to t=s unless subdomaining is required.") | Out-Null
+ $ValidationWarns.Add("$Selector - The flag 't=y' indicates that this domain is testing mode currently. If DKIM is fully deployed, this flag should be changed to t=s unless subdomaining is required.") | Out-Null
}
if ($DkimRecord.PublicKeyInfo.SignatureAlgorithm -ne $DkimRecord.KeyType -and $MXRecord.MailProvider.Name -ne 'Null') {
@@ -1307,32 +1322,31 @@ function Read-DkimRecord {
}
if ($DkimRecord.PublicKeyInfo.KeySize -lt 1024 -and $MXRecord.MailProvider.Name -ne 'Null') {
- $ValidationFails.Add("$Selector - Key size is less than 1024 bit, found $($DkimRecord.PublicKeyInfo.KeySize)") | Out-Null
+ $ValidationFails.Add("$Selector - Key size is less than 1024 bit, found $($DkimRecord.PublicKeyInfo.KeySize).") | Out-Null
}
else {
if ($MXRecord.MailProvider.Name -ne 'Null') {
- $ValidationPasses.Add("$Selector - DKIM key validation succeeded ($($DkimRecord.PublicKeyInfo.KeySize) bit)") | Out-Null
+ $ValidationPasses.Add("$Selector - DKIM key validation succeeded.") | Out-Null
}
$SelectorPasses++
}
- ($DkimAnalysis.Records).Add($DkimRecord) | Out-Null
-
if (($ValidationFails | Measure-Object | Select-Object -ExpandProperty Count) -eq 0) {
- $ValidationPasses.Add("$Selector - No errors detected with DKIM record") | Out-Null
+ $ValidationPasses.Add("$Selector - No errors detected with DKIM record.") | Out-Null
}
- }
+ }
+ ($DkimAnalysis.Records).Add($DkimRecord) | Out-Null
}
}
- else {
- $ValidationWarns.Add('No DKIM selectors provided') | Out-Null
+ if (($DkimAnalysis.Records | Measure-Object | Select-Object -ExpandProperty Count) -eq 0 -and [string]::IsNullOrEmpty($DkimAnalysis.Selectors)) {
+ $ValidationWarns.Add('No DKIM selectors provided, set them in the domain options.') | Out-Null
}
if ($MinimumSelectorPass -gt 0 -and $SelectorPasses -eq 0) {
- $ValidationFails.Add(('Minimum number of selector record passes were not met {0}/{1}' -f $SelectorPasses, $MinimumSelectorPass)) | Out-Null
+ $ValidationFails.Add(('{0} DKIM record(s) found. The minimum number of valid records ({1}) was not met.' -f $SelectorPasses, $MinimumSelectorPass)) | Out-Null
}
elseif ($MinimumSelectorPass -gt 0 -and $SelectorPasses -ge $MinimumSelectorPass) {
- $ValidationPasses.Add(('Minimum number of selector record passes were met {0}/{1}' -f $SelectorPasses, $MinimumSelectorPass))
+ $ValidationPasses.Add(('Minimum number of valid DKIM records were met {0}/{1}.' -f $SelectorPasses, $MinimumSelectorPass))
}
# Collect validation results
@@ -1466,7 +1480,7 @@ function Read-WhoisRecord {
if ($Server -ne $ReferralServer) {
$LastResult = $Results
$Results = Read-WhoisRecord -Query $Query -Server $ReferralServer -Port $Port
- if ($Results._Raw -Match '(No match|Not Found|No Data)' -and $TopLevelReferrers -notcontains $Server) {
+ if ($Results._Raw -Match '(No match|Not Found|No Data|The queried object does not exist)' -and $TopLevelReferrers -notcontains $Server) {
$Results = $LastResult
}
else {
@@ -1735,6 +1749,11 @@ function Test-HttpsCertificate {
$ParsedUrl = [System.Uri]::new($Url)
$Hostname = $ParsedUrl.Host
+ # Valdiations
+ $ValidationPasses = [System.Collections.Generic.List[string]]::new()
+ $ValidationWarns = [System.Collections.Generic.List[string]]::new()
+ $ValidationFails = [System.Collections.Generic.List[string]]::new()
+
# Grab certificate data
$Validation = Get-ServerCertificateValidation -Url $Url
$Certificate = $Validation.Certificate | Select-Object FriendlyName, IssuerName, NotBefore, NotAfter, SerialNumber, SignatureAlgorithm, SubjectName, Thumbprint, Issuer, Subject, DnsNameList
@@ -1746,40 +1765,40 @@ function Test-HttpsCertificate {
# Check to see if certificate is contained in the DNS name list
if ($Certificate.DnsNameList -contains $Hostname -or $Certificate.DnsNameList -eq "*.$Domain") {
- $Test.ValidationPasses.Add(('{0} - Certificate DNS name list contains hostname.' -f $Hostname)) | Out-Null
+ $ValidationPasses.Add(('{0} - Certificate DNS name list contains hostname.' -f $Hostname)) | Out-Null
}
else {
- $Test.ValidationFails.Add(('{0} - Certificate DNS name list does not contain hostname' -f $Hostname)) | Out-Null
+ $ValidationFails.Add(('{0} - Certificate DNS name list does not contain hostname' -f $Hostname)) | Out-Null
}
# Check certificate validity
if ($Certificate.NotBefore -ge $CurrentDate) {
# NotBefore is in the future
- $Test.ValidationFails.Add(('{0} - Certificate is not yet valid.' -f $Hostname)) | Out-Null
+ $ValidationFails.Add(('{0} - Certificate is not yet valid.' -f $Hostname)) | Out-Null
}
elseif ($Certificate.NotAfter -le $CurrentDate) {
# NotAfter is in the past
- $Test.ValidationFails.Add(('{0} - Certificate expired {1} day(s) ago.' -f $Hostname, [Math]::Abs($TimeSpan.Days))) | Out-Null
+ $ValidationFails.Add(('{0} - Certificate expired {1} day(s) ago.' -f $Hostname, [Math]::Abs($TimeSpan.Days))) | Out-Null
}
elseif ($Certificate.NotAfter -ge $CurrentDate -and $TimeSpan.Days -lt 30) {
# NotAfter is under 30 days away
- $Test.ValidationWarns.Add(('{0} - Certificate will expire in {1} day(s).' -f $Hostname, $TimeSpan.Days)) | Out-Null
+ $ValidationWarns.Add(('{0} - Certificate will expire in {1} day(s).' -f $Hostname, $TimeSpan.Days)) | Out-Null
}
else {
# Certificate is valid and not expired
- $Test.ValidationPasses.Add(('{0} - Certificate is valid for the next {1} days.' -f $Hostname, $TimeSpan.Days)) | Out-Null
+ $ValidationPasses.Add(('{0} - Certificate is valid for the next {1} days.' -f $Hostname, $TimeSpan.Days)) | Out-Null
}
# Certificate chain errors
if (($Chain.ChainStatus | Measure-Object).Count -gt 0) {
foreach ($Status in $Chain.ChainStatus) {
- $Test.ValidationFails.Add(('{0} - {1}' -f $Hostname, $Status.StatusInformation)) | Out-Null
+ $ValidationFails.Add(('{0} - {1}' -f $Hostname, $Status.StatusInformation)) | Out-Null
}
}
# Website status errorr
if ([int]$HttpResponse.StatusCode -ge 400) {
- $Test.ValidationFails.Add(('{0} - Website responded with: {1}' -f $Hostname, $HttpResponse.ReasonPhrase))
+ $ValidationFails.Add(('{0} - Website responded with: {1}' -f $Hostname, $HttpResponse.ReasonPhrase))
}
# Set values and return Test object
@@ -1789,6 +1808,10 @@ function Test-HttpsCertificate {
$Test.HttpResponse = $HttpResponse
$Test.ValidityDays = $TimeSpan.Days
+ $Test.ValidationPasses = @($ValidationPasses)
+ $Test.ValidationWarns = @($ValidationWarns)
+ $Test.ValidationFails = @($ValidationFails)
+
# Return test
$Test
}
diff --git a/EditUser/run.ps1 b/EditUser/run.ps1
index b51da80a5754..b37270f2a804 100644
--- a/EditUser/run.ps1
+++ b/EditUser/run.ps1
@@ -34,7 +34,7 @@ try {
"forceChangePasswordNextSignIn" = [bool]$UserObj.mustchangepass
}
} | ForEach-Object {
- $NonEmptyProperties = $_.psobject.Properties | Where-Object { $_.Value } | Select-Object -ExpandProperty Name
+ $NonEmptyProperties = $_.psobject.Properties | Select-Object -ExpandProperty Name
$_ | Select-Object -Property $NonEmptyProperties | ConvertTo-Json
}
$GraphRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($userobj.Userid)" -tenantid $Userobj.tenantid -type PATCH -body $BodyToship -verbose
diff --git a/ExecAccessChecks/run.ps1 b/ExecAccessChecks/run.ps1
index c949457e1566..76e4efd2fbb7 100644
--- a/ExecAccessChecks/run.ps1
+++ b/ExecAccessChecks/run.ps1
@@ -4,44 +4,108 @@
param($Request, $TriggerMetadata)
$APIName = $TriggerMetadata.FunctionName
-Log-Request -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Accessed this API" -Sev "Debug"
+Log-Request -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug'
# Write to the Azure Functions log stream.
-Write-Host "PowerShell HTTP trigger function processed a request."
-if ($Request.query.Permissions -eq "true") {
- Log-Request -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Started permissions check" -Sev "Debug"
- $Results = try {
+Write-Host 'PowerShell HTTP trigger function processed a request.'
+if ($Request.query.Permissions -eq 'true') {
+ Log-Request -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Started permissions check' -Sev 'Debug'
+ $Messages = [System.Collections.Generic.List[string]]::new()
+ $MissingPermissions = [System.Collections.Generic.List[string]]::new()
+ $Links = [System.Collections.Generic.List[object]]::new()
+ $AccessTokenDetails = [PSCustomObject]@{
+ AppId = ''
+ AppName = ''
+ Audience = ''
+ AuthMethods = ''
+ IPAddress = ''
+ Name = ''
+ Scope = ''
+ TenantId = ''
+ UserPrincipalName = ''
+ }
+ $Success = $true
+ try {
$ExpectedPermissions = @(
- "Application.Read.All", "Application.ReadWrite.All", "AuditLog.Read.All", "Channel.Create", "Channel.Delete.All", "Channel.ReadBasic.All", "ChannelMember.Read.All", "ChannelMember.ReadWrite.All", "ChannelMessage.Delete", "ChannelMessage.Edit", "ChannelMessage.Read.All", "ChannelMessage.Send", "ChannelSettings.Read.All", "ChannelSettings.ReadWrite.All", "ConsentRequest.Read.All", "Device.Command", "Device.Read", "Device.Read.All", "DeviceManagementApps.ReadWrite.All", "DeviceManagementConfiguration.ReadWrite.All", "DeviceManagementManagedDevices.ReadWrite.All", "DeviceManagementRBAC.ReadWrite.All", "DeviceManagementServiceConfig.ReadWrite.All", "Directory.AccessAsUser.All", "Domain.Read.All", "Group.ReadWrite.All", "GroupMember.ReadWrite.All", "Mail.Send", "Mail.Send.Shared", "Member.Read.Hidden", "Organization.ReadWrite.All", "Policy.Read.All", "Policy.ReadWrite.AuthenticationFlows", "Policy.ReadWrite.AuthenticationMethod", "Policy.ReadWrite.Authorization", "Policy.ReadWrite.ConsentRequest", "Policy.ReadWrite.DeviceConfiguration", "PrivilegedAccess.Read.AzureResources", "PrivilegedAccess.ReadWrite.AzureResources", "Reports.Read.All", "RoleManagement.ReadWrite.Directory", "SecurityActions.ReadWrite.All", "SecurityEvents.ReadWrite.All", "ServiceHealth.Read.All", "ServiceMessage.Read.All", "Sites.ReadWrite.All", "Team.Create", "Team.ReadBasic.All", "TeamMember.ReadWrite.All", "TeamMember.ReadWriteNonOwnerRole.All", "TeamsActivity.Read", "TeamsActivity.Send", "TeamsApp.Read", "TeamsApp.Read.All", "TeamsApp.ReadWrite", "TeamsApp.ReadWrite.All", "TeamsAppInstallation.ReadForChat", "TeamsAppInstallation.ReadForTeam", "TeamsAppInstallation.ReadForUser", "TeamsAppInstallation.ReadWriteForChat", "TeamsAppInstallation.ReadWriteForTeam", "TeamsAppInstallation.ReadWriteForUser", "TeamsAppInstallation.ReadWriteSelfForChat", "TeamsAppInstallation.ReadWriteSelfForTeam", "TeamsAppInstallation.ReadWriteSelfForUser", "TeamSettings.Read.All", "TeamSettings.ReadWrite.All", "TeamsTab.Create", "TeamsTab.Read.All", "TeamsTab.ReadWrite.All", "TeamsTab.ReadWriteForChat", "TeamsTab.ReadWriteForTeam", "TeamsTab.ReadWriteForUser", "ThreatAssessment.ReadWrite.All", "UnifiedGroupMember.Read.AsGuest", "User.ManageIdentities.All", "User.Read", "User.ReadWrite.All", "UserAuthenticationMethod.Read.All", "UserAuthenticationMethod.ReadWrite", "UserAuthenticationMethod.ReadWrite.All"
+ 'Application.Read.All', 'Application.ReadWrite.All', 'AuditLog.Read.All', 'Channel.Create', 'Channel.Delete.All', 'Channel.ReadBasic.All', 'ChannelMember.Read.All', 'ChannelMember.ReadWrite.All', 'ChannelMessage.Delete', 'ChannelMessage.Edit', 'ChannelMessage.Read.All', 'ChannelMessage.Send', 'ChannelSettings.Read.All', 'ChannelSettings.ReadWrite.All', 'ConsentRequest.Read.All', 'Device.Command', 'Device.Read', 'Device.Read.All', 'DeviceManagementApps.ReadWrite.All', 'DeviceManagementConfiguration.ReadWrite.All', 'DeviceManagementManagedDevices.ReadWrite.All', 'DeviceManagementRBAC.ReadWrite.All', 'DeviceManagementServiceConfig.ReadWrite.All', 'Directory.AccessAsUser.All', 'Domain.Read.All', 'Group.ReadWrite.All', 'GroupMember.ReadWrite.All', 'Mail.Send', 'Mail.Send.Shared', 'Member.Read.Hidden', 'Organization.ReadWrite.All', 'Policy.Read.All', 'Policy.ReadWrite.AuthenticationFlows', 'Policy.ReadWrite.AuthenticationMethod', 'Policy.ReadWrite.Authorization', 'Policy.ReadWrite.ConsentRequest', 'Policy.ReadWrite.DeviceConfiguration', 'PrivilegedAccess.Read.AzureResources', 'PrivilegedAccess.ReadWrite.AzureResources', 'Reports.Read.All', 'RoleManagement.ReadWrite.Directory', 'SecurityActions.ReadWrite.All', 'SecurityEvents.ReadWrite.All', 'ServiceHealth.Read.All', 'ServiceMessage.Read.All', 'Sites.ReadWrite.All', 'Team.Create', 'Team.ReadBasic.All', 'TeamMember.ReadWrite.All', 'TeamMember.ReadWriteNonOwnerRole.All', 'TeamsActivity.Read', 'TeamsActivity.Send', 'TeamsApp.Read', 'TeamsApp.Read.All', 'TeamsApp.ReadWrite', 'TeamsApp.ReadWrite.All', 'TeamsAppInstallation.ReadForChat', 'TeamsAppInstallation.ReadForTeam', 'TeamsAppInstallation.ReadForUser', 'TeamsAppInstallation.ReadWriteForChat', 'TeamsAppInstallation.ReadWriteForTeam', 'TeamsAppInstallation.ReadWriteForUser', 'TeamsAppInstallation.ReadWriteSelfForChat', 'TeamsAppInstallation.ReadWriteSelfForTeam', 'TeamsAppInstallation.ReadWriteSelfForUser', 'TeamSettings.Read.All', 'TeamSettings.ReadWrite.All', 'TeamsTab.Create', 'TeamsTab.Read.All', 'TeamsTab.ReadWrite.All', 'TeamsTab.ReadWriteForChat', 'TeamsTab.ReadWriteForTeam', 'TeamsTab.ReadWriteForUser', 'ThreatAssessment.ReadWrite.All', 'UnifiedGroupMember.Read.AsGuest', 'User.ManageIdentities.All', 'User.Read', 'User.ReadWrite.All', 'UserAuthenticationMethod.Read.All', 'UserAuthenticationMethod.ReadWrite', 'UserAuthenticationMethod.ReadWrite.All'
)
- $GraphPermissions = ((Get-GraphToken -returnRefresh $true).scope).split(' ') -replace "https://graph.microsoft.com//", "" | Where-Object { $_ -notin @("email", "openid", "profile", ".default") }
- Write-Host ($GraphPermissions | ConvertTo-Json)
+ $GraphToken = Get-GraphToken -returnRefresh $true
+ $GraphPermissions = $GraphToken.scope.split(' ') -replace 'https://graph.microsoft.com//', '' | Where-Object { $_ -notin @('email', 'openid', 'profile', '.default') }
+ #Write-Host ($GraphPermissions | ConvertTo-Json)
+
+ try {
+ $AccessTokenDetails = Read-JwtAccessDetails -Token $GraphToken.access_token
+ #Write-Host ($AccessTokenDetails | ConvertTo-Json)
+ }
+ catch {
+ $AccessTokenDetails = [PSCustomObject]@{
+ Name = ''
+ AuthMethods = @()
+ }
+ Log-Request -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message "Token exception: $($_) " -Sev 'Error'
+ $Success = $false
+ }
+
+ if ($AccessTokenDetails.Name -eq '') {
+ $Messages.Add('Your refresh token is invalid, check for line breaks or missing characters.') | Out-Null
+ $Success = $false
+ }
+ else {
+ if ($AccessTokenDetails.AuthMethods -contains 'mfa') {
+ $Messages.Add('Your access token contains the MFA claim.') | Out-Null
+ }
+ else {
+ $Messages.Add('Your access token does not contain the MFA claim, Refresh your SAM tokens.') | Out-Null
+ $Success = $false
+ $Links.Add([PSCustomObject]@{
+ Text = 'MFA Troubleshooting'
+ Href = 'https://cipp.app/docs/general/troubleshooting/#multi-factor-authentication-troubleshooting'
+ }
+ ) | Out-Null
+ }
+ }
+
$MissingPermissions = $ExpectedPermissions | Where-Object { $_ -notin $GraphPermissions }
if ($MissingPermissions) {
- @{ MissingPermissions = @($MissingPermissions) }
+ $MissingPermissions = @($MissingPermissions)
+ $Success = $false
+ $Links.Add([PSCustomObject]@{
+ Text = 'Permissions'
+ Href = 'https://cipp.app/docs/user/gettingstarted/permissions/#permissions'
+ }
+ ) | Out-Null
}
else {
- "Your Secure Application Model has all required permissions"
+ $Messages.Add('Your Secure Application Model has all required permissions') | Out-Null
}
}
catch {
- Log-Request -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Permissions check failed: $($_) " -Sev "Error"
- "We could not connect to the API to retrieve the permissions. There might be a problem with the secure application model configuration. The returned error is: $($_.Exception.Response.StatusCode.value__ ) - $($_.Exception.Message)"
+ Log-Request -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Permissions check failed: $($_) " -Sev 'Error'
+ $Messages.Add("We could not connect to the API to retrieve the permissions. There might be a problem with the secure application model configuration. The returned error is: $($_.Exception.Response.StatusCode.value__ ) - $($_.Exception.Message)") | Out-Null
+ $Success = $false
+ }
+
+ $Results = [PSCustomObject]@{
+ AccessTokenDetails = $AccessTokenDetails
+ Messages = @($Messages)
+ MissingPermissions = @($MissingPermissions)
+ Links = @($Links)
+ Success = $Success
}
}
-if ($Request.query.Tenants -eq "true") {
+if ($Request.query.Tenants -eq 'true') {
$Tenants = ($Request.body.tenantid).split(',')
- if (!$Tenants) { $results = "Could not load the tenants list from cache. Please run permissions check first, or visit the tenants page." }
+ if (!$Tenants) { $results = 'Could not load the tenants list from cache. Please run permissions check first, or visit the tenants page.' }
$results = foreach ($tenant in $Tenants) {
try {
$token = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/users/delta?$select=displayName' -tenantid $tenant
@{
TenantName = "$($Tenant)"
- Status = "Succesfully connected"
+ Status = 'Succesfully connected'
}
- Log-Request -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message "Tenant access check executed succesfully" -Sev "Info"
+ Log-Request -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message 'Tenant access check executed succesfully' -Sev 'Info'
}
catch {
@@ -49,21 +113,21 @@ if ($Request.query.Tenants -eq "true") {
TenantName = "$($tenant)"
Status = "Failed to connect to $($_.Exception.Message)"
}
- Log-Request -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message "Tenant access check failed: $($_) " -Sev "Error"
+ Log-Request -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message "Tenant access check failed: $($_) " -Sev 'Error'
}
try {
- $upn = "notRequired@required.com"
+ $upn = 'notRequired@required.com'
$tokenvalue = ConvertTo-SecureString (Get-GraphToken -AppID 'a0c73c16-a7e3-4564-9a95-2bdf47383716' -RefreshToken $ENV:ExchangeRefreshToken -Scope 'https://outlook.office365.com/.default' -Tenantid $tenant).Authorization -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($upn, $tokenValue)
$session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://ps.outlook.com/powershell-liveid?DelegatedOrg=$($tenant)&BasicAuthToOAuthConversion=true" -Credential $credential -Authentication Basic -AllowRedirection -ErrorAction Continue
- $session = Import-PSSession $session -ea Silentlycontinue -AllowClobber -CommandName "Get-OrganizationConfig"
+ $session = Import-PSSession $session -ea Silentlycontinue -AllowClobber -CommandName 'Get-OrganizationConfig'
$org = Get-OrganizationConfig
$null = Get-PSSession | Remove-PSSession
@{
TenantName = "$($Tenant)"
- Status = "Succesfully connected to Exchange"
+ Status = 'Succesfully connected to Exchange'
}
}
catch {
@@ -73,13 +137,13 @@ if ($Request.query.Tenants -eq "true") {
TenantName = "$($Tenant)"
Status = "Failed to connect to Exchange: $($Message)"
}
- Log-Request -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message "Tenant access check for Exchange failed: $($Message) " -Sev "Error"
+ Log-Request -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message "Tenant access check for Exchange failed: $($Message) " -Sev 'Error'
}
}
- if (!$Tenants) { $results = "Could not load the tenants list from cache. Please run permissions check first, or visit the tenants page." }
+ if (!$Tenants) { $results = 'Could not load the tenants list from cache. Please run permissions check first, or visit the tenants page.' }
}
-$body = [pscustomobject]@{"Results" = $Results }
+$body = [pscustomobject]@{'Results' = $Results }
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
diff --git a/ExecDnsHelper/function.json b/ExecCreateTAP/function.json
similarity index 100%
rename from ExecDnsHelper/function.json
rename to ExecCreateTAP/function.json
diff --git a/ExecCreateTAP/run.ps1 b/ExecCreateTAP/run.ps1
new file mode 100644
index 000000000000..f545f0a1deb4
--- /dev/null
+++ b/ExecCreateTAP/run.ps1
@@ -0,0 +1,26 @@
+using namespace System.Net
+
+# Input bindings are passed in via param block.
+param($Request, $TriggerMetadata)
+
+$APIName = $TriggerMetadata.FunctionName
+Log-Request -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Accessed this API" -Sev "Debug"
+
+# Interact with query parameters or the body of the request.
+$TenantFilter = $Request.Query.TenantFilter
+$Body = "{}"
+try {
+ $GraphRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($Request.query.ID)/authentication/temporaryAccessPassMethods" -tenantid $TenantFilter -type POST -body $Body -verbose
+ $Results = [pscustomobject]@{"Results" = "The TAP for this user is $($GraphRequest.temporaryAccessPass) - This TAP is usable for the next $($GraphRequest.LifetimeInMinutes) minutes" }
+ Log-Request -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Created temporary access pass for user $($Request.Query.id)" -Sev "Info"
+
+}
+catch {
+ $Results = [pscustomobject]@{"Results" = "Failed. $($_.Exception.Message)" }
+}
+
+# Associate values to output bindings by calling 'Push-OutputBinding'.
+Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = [HttpStatusCode]::OK
+ Body = $Results
+ })
\ No newline at end of file
diff --git a/ExecEditCalendarPermissions/function.json b/ExecEditCalendarPermissions/function.json
new file mode 100644
index 000000000000..bec6849b58ab
--- /dev/null
+++ b/ExecEditCalendarPermissions/function.json
@@ -0,0 +1,19 @@
+{
+ "bindings": [
+ {
+ "authLevel": "anonymous",
+ "type": "httpTrigger",
+ "direction": "in",
+ "name": "Request",
+ "methods": [
+ "get",
+ "post"
+ ]
+ },
+ {
+ "type": "http",
+ "direction": "out",
+ "name": "Response"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ExecEditCalendarPermissions/run.ps1 b/ExecEditCalendarPermissions/run.ps1
new file mode 100644
index 000000000000..173c7c288515
--- /dev/null
+++ b/ExecEditCalendarPermissions/run.ps1
@@ -0,0 +1,45 @@
+using namespace System.Net
+
+# Input bindings are passed in via param block.
+param($Request, $TriggerMetadata)
+
+$APIName = $TriggerMetadata.FunctionName
+Log-Request -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Accessed this API" -Sev "Debug"
+$UserID = ($request.query.UserID)
+$UserToGetPermissions = $Request.query.UserToGetPermissions
+$Tenantfilter = $request.Query.tenantfilter
+$Permissions = @($Request.query.permissions)
+$folderName = $Request.query.folderName
+
+
+$CalParam = [PSCustomObject]@{
+ Identity = "$($UserID):\$folderName"
+ AccessRights = @($Permissions)
+ User = $UserToGetPermissions
+}
+try {
+ if ($Request.query.removeaccess) {
+ $GraphRequest = New-ExoRequest -tenantid $Tenantfilter -cmdlet "Remove-MailboxFolderPermission" -cmdParams @{Identity = "$($UserID):\$folderName"; User = $Request.query.RemoveAccess }
+ $Result = "Successfully removed access for $($Request.query.RemoveAccess) from calender $($CalParam.Identity)"
+ }
+ else {
+ try {
+ $GraphRequest = New-ExoRequest -tenantid $Tenantfilter -cmdlet "Set-MailboxFolderPermission" -cmdParams $CalParam
+ }
+ catch {
+ $GraphRequest = New-ExoRequest -tenantid $Tenantfilter -cmdlet "Add-MailboxFolderPermission" -cmdParams $CalParam
+ }
+ Log-request -API 'List Calendar Permissions' -tenant $tenantfilter -message "Calendar permissions listed for $($tenantfilter)" -sev Debug
+
+ $Result = "Succesfully set permissions on folder $($CalParam.Identity). The user $UserToGetPermissions now has $Permissions permissions on this folder."
+ }
+}
+catch {
+ $ErrorMessage = Get-NormalizedError -Message $_.Exception
+ $Result = $ErrorMessage
+}
+# Associate values to output bindings by calling 'Push-OutputBinding'.
+Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = [HttpStatusCode]::OK
+ Body = @{Results = $Result }
+ })
diff --git a/ExecGraphRequest/function.json b/ExecGraphRequest/function.json
new file mode 100644
index 000000000000..306b0c51e560
--- /dev/null
+++ b/ExecGraphRequest/function.json
@@ -0,0 +1,19 @@
+{
+ "bindings": [
+ {
+ "authLevel": "anonymous",
+ "type": "httpTrigger",
+ "direction": "in",
+ "name": "Request",
+ "methods": [
+ "get",
+ "post"
+ ]
+ },
+ {
+ "type": "http",
+ "direction": "out",
+ "name": "Response"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ExecGraphRequest/run.ps1 b/ExecGraphRequest/run.ps1
new file mode 100644
index 000000000000..9f45f8298eb1
--- /dev/null
+++ b/ExecGraphRequest/run.ps1
@@ -0,0 +1,105 @@
+using namespace System.Net
+
+# Input bindings are passed in via param block.
+param($Request, $TriggerMetadata)
+
+$APIName = $TriggerMetadata.FunctionName
+Log-Request -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Accessed this API" -Sev "Debug"
+
+Function ConvertTo-FlatObject {
+ # https://evotec.xyz/powershell-converting-advanced-object-to-flat-object/ - MIT License
+ [CmdletBinding()]
+ Param (
+ [Parameter(ValueFromPipeLine)][Object[]]$Objects,
+ [String]$Separator = ".",
+ [ValidateSet("", 0, 1)]$Base = 1,
+ [int]$Depth = 5,
+ [Parameter(DontShow)][String[]]$Path,
+ [Parameter(DontShow)][System.Collections.IDictionary] $OutputObject
+ )
+ Begin {
+ $InputObjects = [System.Collections.Generic.List[Object]]::new()
+ }
+ Process {
+ foreach ($O in $Objects) {
+ $InputObjects.Add($O)
+ }
+ }
+ End {
+ If ($PSBoundParameters.ContainsKey("OutputObject")) {
+ $Object = $InputObjects[0]
+ $Iterate = [ordered] @{}
+ if ($null -eq $Object) {
+ #Write-Verbose -Message "ConvertTo-FlatObject - Object is null"
+ }
+ elseif ($Object.GetType().Name -in 'String', 'DateTime', 'TimeSpan', 'Version', 'Enum') {
+ $Object = $Object.ToString()
+ }
+ elseif ($Depth) {
+ $Depth--
+ If ($Object -is [System.Collections.IDictionary]) {
+ $Iterate = $Object
+ }
+ elseif ($Object -is [Array] -or $Object -is [System.Collections.IEnumerable]) {
+ $i = $Base
+ foreach ($Item in $Object.GetEnumerator()) {
+ $Iterate["$i"] = $Item
+ $i += 1
+ }
+ }
+ else {
+ foreach ($Prop in $Object.PSObject.Properties) {
+ if ($Prop.IsGettable) {
+ $Iterate["$($Prop.Name)"] = $Object.$($Prop.Name)
+ }
+ }
+ }
+ }
+ If ($Iterate.Keys.Count) {
+ foreach ($Key in $Iterate.Keys) {
+ ConvertTo-FlatObject -Objects @(, $Iterate["$Key"]) -Separator $Separator -Base $Base -Depth $Depth -Path ($Path + $Key) -OutputObject $OutputObject
+ }
+ }
+ else {
+ $Property = $Path -Join $Separator
+ $OutputObject[$Property] = $Object
+ }
+ }
+ elseif ($InputObjects.Count -gt 0) {
+ foreach ($ItemObject in $InputObjects) {
+ $OutputObject = [ordered]@{}
+ ConvertTo-FlatObject -Objects @(, $ItemObject) -Separator $Separator -Base $Base -Depth $Depth -Path $Path -OutputObject $OutputObject
+ [PSCustomObject] $OutputObject
+ }
+ }
+ }
+}
+$TenantFilter = $Request.Query.TenantFilter
+try {
+ if ($TenantFilter -ne "AllTenants") {
+ $RawGraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/$($Request.Query.Endpoint)" -tenantid $TenantFilter
+ }
+ else {
+ $RawGraphRequest = Get-tenants | ForEach-Object {
+ $DefaultDomainName = $_.defaultdomainname
+ $TenantName = $_.displayName
+ New-GraphGetRequest -uri "https://graph.microsoft.com/beta/$($Request.Query.Endpoint)" -tenantid $DefaultDomainName } | Select-Object @{
+ label = 'Tenant'
+ expression = { $TenantName }
+ }, *
+
+ }
+ $GraphRequest = $RawGraphRequest | Where-Object -Property '@odata.context' -EQ $null | ConvertTo-FlatObject
+ $StatusCode = [HttpStatusCode]::OK
+}
+catch {
+ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
+ $StatusCode = [HttpStatusCode]::Forbidden
+ $GraphRequest = $ErrorMessage
+}
+
+# Associate values to output bindings by calling 'Push-OutputBinding'.
+Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = @($GraphRequest)
+ })
diff --git a/GetDashboard/function.json b/GetDashboard/function.json
new file mode 100644
index 000000000000..306b0c51e560
--- /dev/null
+++ b/GetDashboard/function.json
@@ -0,0 +1,19 @@
+{
+ "bindings": [
+ {
+ "authLevel": "anonymous",
+ "type": "httpTrigger",
+ "direction": "in",
+ "name": "Request",
+ "methods": [
+ "get",
+ "post"
+ ]
+ },
+ {
+ "type": "http",
+ "direction": "out",
+ "name": "Response"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/GetDashboard/run.ps1 b/GetDashboard/run.ps1
new file mode 100644
index 000000000000..8d293914815d
--- /dev/null
+++ b/GetDashboard/run.ps1
@@ -0,0 +1,206 @@
+using namespace System.Net
+
+# Input bindings are passed in via param block.
+param($Request, $TriggerMetadata)
+Function Test-CronRange {
+ <#
+ .EXAMPLE
+ # * always passes
+ Test-CronRange -Range '*' -InputValue 10 -Verbose
+ # a min-max range
+ Test-CronRange -Range '1-15' -InputValue 10 -Verbose
+ # stepped value
+ Test-CronRange -Range '*/15' -InputValue 30 -verbose
+ # A specific value list
+ Test-CronRange -Range '2,5,8,9' -InputValue 10 -verbose
+ Test-CronRange -Range '*/4' -InputValue 60 -verbose
+ #>
+ [cmdletbinding()]
+ param(
+ [ValidatePattern("^[\d-*/,]*$")]
+ [string]$range
+ ,
+ [int]$inputvalue
+ )
+ Write-Verbose "Testing $range"
+ If ($range -eq '*') {
+ Return $true
+ }
+ If ($range -match '^\d+$') {
+ Write-Verbose 'Specific Value(int)'
+ Return ($inputvalue -eq [int]$range)
+ }
+ If ($range -match '[\d]+-[\d]+([/][\d])*') {
+ Write-Verbose 'min-max range'
+ [int]$min, [int]$max = $range -split '-'
+ Return ($inputvalue -ge $min -and $inputvalue -le $max)
+ }
+ If ($range -match ('([*]+|[\d]+-[\d]+)[/][\d]+')) {
+ Write-Verbose 'Step Value'
+ $list, $step = $range -split '/'
+ Write-Verbose "Using Step of $step"
+ $IsInStep = ( ($inputvalue / $step).GetType().Name -eq 'Int32' )
+ Return ( $IsInStep )
+ }
+ If ($range -match '(\d+)(,\s*\d+)*') {
+ Write-Verbose 'value list'
+ $list = @()
+ $list = $range -split ','
+ Return ( $list -contains $InputValue )
+ }
+ Write-Error "Could not process Range format: $Range"
+}
+Function ConvertFrom-DateTable {
+ Param (
+ $DateTable
+ )
+ $datestring = "{0}-{1:00}-{2:00} {3:00}:{4:00}" -f $DateTable.year, $DateTable.month, $DateTable.day, $DateTable.hour, $DateTable.Minute
+ $date = [datetime]::ParseExact($datestring, "yyyy-MM-dd HH:mm", $null)
+ return $date
+}
+Function Invoke-CronIncrement {
+ param(
+ [psobject]
+ $DateTable
+ ,
+ [ValidateSet('Minute', 'Hour', 'Day', 'Month')]
+ [string]
+ $Increment
+ )
+ $date = ConvertFrom-DateTable -DateTable $DateTable
+ $date = switch ($Increment) {
+ 'Minute' { $date.AddMinutes(1) }
+ 'Hour' { $date.AddHours(1) }
+ 'Day' { $date.AddDays(1) }
+ 'Month' { $date.AddMonths(1) }
+ }
+ $output = [ordered]@{
+ Minute = $date.Minute
+ Hour = $date.hour
+ Day = $date.day
+ Weekday = $date.DayOfWeek.value__
+ Month = $date.month
+ Year = $date.year
+ }
+ Return $output
+}
+Function Get-CronNextExecutionTime {
+ <#
+ .SYNOPSIS
+ Currently only support * or digits
+ todo: add support for ',' '-' '/' ','
+ .EXAMPLE
+ Get-CronNextExecutionTime -Expression '* * * * *'
+ Get-CronNextExecutionTime -Expression '5 * * * *'
+ Get-CronNextExecutionTime -Expression '* 13-21 * * *'
+ Get-CronNextExecutionTime -Expression '0 0 2 * *'
+ Get-CronNextExecutionTime -Expression '15 14 * 1-3 *'
+ Get-CronNextExecutionTime -Expression '15 14 * * 4'
+ Get-CronNextExecutionTime -Expression '15 14 * 2 *'
+ Get-CronNextExecutionTime -Expression '15 14-20 * * *'
+ Get-CronNextExecutionTime -Expression '15 14 * * 1'
+ #>
+ [cmdletbinding()]
+ param(
+ [string]
+ $Expression = '* * * * *'
+ ,
+ $InputDate
+ )
+ # Split Expression in variables and set to INT if possible
+ $cronMinute, $cronHour, $cronDay, $cronMonth, $cronWeekday = $Expression -Split ' '
+ Get-Variable -Scope local | Where-Object { $_.name -like 'cron*' } | ForEach-Object {
+ If ($_.Value -ne '*') {
+ Try {
+ [int]$newValue = $_.Value
+ Set-Variable -Name $_.Name -Value $newValue -ErrorAction Ignore
+ }
+ Catch {}
+ }
+ }
+ # Get the next default Time (= next minute)
+ $nextdate = If ($InputDate) { $InputDate } Else { Get-Date }
+ $nextdate = $nextdate.addMinutes(1)
+ $next = [ordered]@{
+ Minute = $nextdate.Minute
+ Hour = $nextdate.hour
+ Day = $nextdate.day
+ Weekday = $nextdate.DayOfWeek.value__
+ Month = $nextdate.month
+ Year = $nextdate.year
+ }
+ # Increase Minutes until it is in the range.
+ # If Minutes passes the 60 mark, the hour is incremented
+ $done = $false
+ Do {
+ If ((Test-CronRange -InputValue $next.Minute -range $cronMinute) -eq $False) {
+ Do {
+ $next = Invoke-CronIncrement -DateTable $Next -Increment Minute
+ } While ( (Test-CronRange -InputValue $next.Minute -range $cronMinute) -eq $False )
+ continue
+ }
+ # Check if the next Hour is in the desired range
+ # Add a Day because the desired Hour has already passed
+ If ((Test-CronRange -InputValue $next.Hour -range $cronHour) -eq $False) {
+ Do {
+ $next = Invoke-CronIncrement -DateTable $Next -Increment Hour
+ $next.Minute = 0
+ } While ((Test-CronRange -InputValue $next.Hour -range $cronHour) -eq $False)
+ continue
+ }
+ # Increase Days until it is in the range.
+ # If Days passes the 30/31 mark, the Month is incremented
+ If ((Test-CronRange -InputValue $next.day -range $cronday) -eq $False) {
+ Do {
+ $next = Invoke-CronIncrement -DateTable $Next -Increment Day
+ $next.Hour = 0
+ $next.Minute = 0
+ } While ((Test-CronRange -InputValue $next.day -range $cronday) -eq $False)
+ continue
+ }
+ # Increase Months until it is in the range.
+ # If Months passes the 12 mark, the Year is incremented
+ If ((Test-CronRange -InputValue $next.Month -range $cronMonth) -eq $False) {
+ Do {
+ $next = Invoke-CronIncrement -DateTable $Next -Increment Month
+ $next.Hour = 0
+ $next.Minute = 0
+ } While ((Test-CronRange -InputValue $next.Month -range $cronMonth) -eq $False)
+ continue
+ }
+ If ((Test-CronRange -InputValue $Next.WeekDay -Range $cronWeekday) -eq $false) {
+ Do {
+ $next = Invoke-CronIncrement -DateTable $Next -Increment Day
+ $next.Hour = 0
+ $next.Minute = 0
+ } While ( (Test-CronRange -InputValue $Next.WeekDay -Range $cronWeekday) -eq $false )
+ continue
+ }
+ $done = $true
+ } While ($done -eq $false)
+ $date = ConvertFrom-DateTable -DateTable $next
+ If (!$date) { Throw 'Could not create date' }
+
+ # Add Days until weekday matches
+
+ Return $Date
+}
+$APIName = $TriggerMetadata.FunctionName
+Log-Request -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Accessed this API" -Sev "Debug"
+$dash = [PSCustomObject]@{
+ NextStandardsRun = (Get-CronNextExecutionTime -Expression '0 */3 * * *').tostring('s')
+ NextBPARun = (Get-CronNextExecutionTime -Expression '0 3 * * *').tostring('s')
+ queuedApps = [int64](Get-ChildItem '.\ChocoApps.Cache' -ErrorAction SilentlyContinue).count
+ queuedStandards = [int64](Get-ChildItem '.\Cache_Standards' -ErrorAction SilentlyContinue).count
+ tenantCount = (get-tenants).count
+ RefreshTokenDate = (Get-CronNextExecutionTime -Expression '0 0 * * 0').AddDays('-7').tostring('s') -split "T" | Select-Object -First 1
+ ExchangeTokenDate = (Get-CronNextExecutionTime -Expression '0 0 * * 0').AddDays('-7').tostring('s') -split "T" | Select-Object -First 1
+ LastLog = Get-Content "Logs\$((Get-Date).ToString('ddMMyyyy')).log" | ConvertFrom-Csv -Header "DateTime", "Tenant", "API", "Message", "User", "Severity" -Delimiter "|" | Select-Object -Last 10
+}
+# Write to the Azure Functions log stream.
+
+# Associate values to output bindings by calling 'Push-OutputBinding'.
+Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = [HttpStatusCode]::OK
+ Body = $dash
+ })
\ No newline at end of file
diff --git a/GraphHelper.psm1 b/GraphHelper.psm1
index a44aaf4f4725..e492e8c195f0 100644
--- a/GraphHelper.psm1
+++ b/GraphHelper.psm1
@@ -4,11 +4,11 @@ function Get-NormalizedError {
[string]$message
)
switch -Wildcard ($message) {
- "Request not applicable to target tenant." { "Required license not available for this tenant" }
- "Neither tenant is B2C or tenant doesn't have premium license" { "This feature requires a P1 license or higher" }
- "Response status code does not indicate success: 400 (Bad Request)." { "Error 400 occured. There is an issue with the token configuration for this tenant. Please perform an access check" }
- "*Microsoft.Skype.Sync.Pstn.Tnm.Common.Http.HttpResponseException*" { "Could not connect to Teams Admin center - Tenant might be missing a Teams license" }
- "*Provide valid credential.*" { "Error 400: There is an issue with your Exchange Token configuration. Please perform an access check for this tenant" }
+ 'Request not applicable to target tenant.' { 'Required license not available for this tenant' }
+ "Neither tenant is B2C or tenant doesn't have premium license" { 'This feature requires a P1 license or higher' }
+ 'Response status code does not indicate success: 400 (Bad Request).' { 'Error 400 occured. There is an issue with the token configuration for this tenant. Please perform an access check' }
+ '*Microsoft.Skype.Sync.Pstn.Tnm.Common.Http.HttpResponseException*' { 'Could not connect to Teams Admin center - Tenant might be missing a Teams license' }
+ '*Provide valid credential.*' { 'Error 400: There is an issue with your Exchange Token configuration. Please perform an access check for this tenant' }
Default { $message }
}
}
@@ -21,7 +21,7 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $refreshToken, $Retur
client_secret = $ENV:ApplicationSecret
scope = $Scope
refresh_token = $ENV:RefreshToken
- grant_type = "refresh_token"
+ grant_type = 'refresh_token'
}
if ($asApp -eq $true) {
@@ -29,7 +29,7 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $refreshToken, $Retur
client_id = $ENV:ApplicationId
client_secret = $ENV:ApplicationSecret
scope = $Scope
- grant_type = "client_credentials"
+ grant_type = 'client_credentials'
}
}
@@ -38,7 +38,7 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $refreshToken, $Retur
client_id = $appid
refresh_token = $RefreshToken
scope = $Scope
- grant_type = "refresh_token"
+ grant_type = 'refresh_token'
}
}
@@ -51,16 +51,16 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $refreshToken, $Retur
function Log-Request ($message, $tenant, $API, $user, $sev) {
$username = ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($user)) | ConvertFrom-Json).userDetails
- New-Item -Path "Logs" -ItemType Directory -ErrorAction SilentlyContinue
+ New-Item -Path 'Logs' -ItemType Directory -ErrorAction SilentlyContinue
$date = (Get-Date).ToString('s')
- $LogMutex = New-Object System.Threading.Mutex($false, "LogMutex")
- if (!$username) { $username = "CIPP" }
- if (!$tenant) { $tenant = "None" }
- if ($sev -eq "Debug" -and $env:DebugMode -ne "true") {
- Write-Information "Not writing to log file - Debug mode is not enabled."
+ $LogMutex = New-Object System.Threading.Mutex($false, 'LogMutex')
+ if (!$username) { $username = 'CIPP' }
+ if (!$tenant) { $tenant = 'None' }
+ if ($sev -eq 'Debug' -and $env:DebugMode -ne 'true') {
+ Write-Information 'Not writing to log file - Debug mode is not enabled.'
return
}
- $CleanMessage = [string]::join(" ", ($message.Split("`n")))
+ $CleanMessage = [string]::join(' ', ($message.Split("`n")))
$logdata = "$($date)|$($tenant)|$($API)|$($CleanMessage)|$($username)|$($sev)"
if ($LogMutex.WaitOne(1000)) {
$logdata | Out-File -Append -FilePath "Logs\$((Get-Date).ToString('ddMMyyyy')).log" -Force
@@ -70,7 +70,7 @@ function Log-Request ($message, $tenant, $API, $user, $sev) {
function New-GraphGetRequest ($uri, $tenantid, $scope, $AsApp, $noPagination) {
- if ($scope -eq "ExchangeOnline") {
+ if ($scope -eq 'ExchangeOnline') {
$Headers = Get-GraphToken -AppID 'a0c73c16-a7e3-4564-9a95-2bdf47383716' -RefreshToken $ENV:ExchangeRefreshToken -Scope 'https://outlook.office365.com/.default' -Tenantid $tenantid
}
else {
@@ -82,12 +82,12 @@ function New-GraphGetRequest ($uri, $tenantid, $scope, $AsApp, $noPagination) {
if ((Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) {
$ReturnedData = do {
try {
- $Data = (Invoke-RestMethod -Uri $nextURL -Method GET -Headers $headers -ContentType "application/json; charset=utf-8")
+ $Data = (Invoke-RestMethod -Uri $nextURL -Method GET -Headers $headers -ContentType 'application/json; charset=utf-8')
if ($data.value) { $data.value } else { ($Data) }
if ($noPagination) { $nextURL = $null } else { $nextURL = $data.'@odata.nextLink' }
}
catch {
- $Message = ($_.ErrorDetails.Message | ConvertFrom-Json).error.message
+ $Message = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message
if ($Message -eq $null) { $Message = $($_.Exception.Message) }
throw $Message
}
@@ -95,7 +95,7 @@ function New-GraphGetRequest ($uri, $tenantid, $scope, $AsApp, $noPagination) {
return $ReturnedData
}
else {
- Write-Error "Not allowed. You cannot manage your own tenant or tenants not under your scope"
+ Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope'
}
}
@@ -109,18 +109,17 @@ function New-GraphPOSTRequest ($uri, $tenantid, $body, $type, $scope, $AsApp) {
if ((Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) {
try {
- $ReturnedData = (Invoke-RestMethod -Uri $($uri) -Method $TYPE -Body $body -Headers $headers -ContentType "application/json; charset=utf-8")
+ $ReturnedData = (Invoke-RestMethod -Uri $($uri) -Method $TYPE -Body $body -Headers $headers -ContentType 'application/json; charset=utf-8')
}
catch {
- Write-Host ($_.ErrorDetails.Message | ConvertFrom-Json).error.message
- $Message = ($_.ErrorDetails.Message | ConvertFrom-Json).error.message
+ $Message = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message
if ($Message -eq $null) { $Message = $($_.Exception.Message) }
throw $Message
}
return $ReturnedData
}
else {
- Write-Error "Not allowed. You cannot manage your own tenant or tenants not under your scope"
+ Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope'
}
}
@@ -135,7 +134,7 @@ function Get-ClassicAPIToken($tenantID, $Resource) {
$uri = "https://login.microsoftonline.com/$($TenantID)/oauth2/token"
$body = "resource=$Resource&grant_type=refresh_token&refresh_token=$($ENV:ExchangeRefreshToken)"
try {
- $token = Invoke-RestMethod $uri -Body $body -ContentType "application/x-www-form-urlencoded" -ErrorAction SilentlyContinue -Method post
+ $token = Invoke-RestMethod $uri -Body $body -ContentType 'application/x-www-form-urlencoded' -ErrorAction SilentlyContinue -Method post
return $token
}
catch {
@@ -153,8 +152,8 @@ function New-TeamsAPIGetRequest($Uri, $tenantID, $Method = 'GET', $Resource = '4
try {
$Data = Invoke-RestMethod -ContentType "$ContentType;charset=UTF-8" -Uri $NextURL -Method $Method -Headers @{
Authorization = "Bearer $($token.access_token)";
- "x-ms-client-request-id" = [guid]::NewGuid().ToString();
- "x-ms-client-session-id" = [guid]::NewGuid().ToString()
+ 'x-ms-client-request-id' = [guid]::NewGuid().ToString();
+ 'x-ms-client-session-id' = [guid]::NewGuid().ToString()
'x-ms-correlation-id' = [guid]::NewGuid()
'X-Requested-With' = 'XMLHttpRequest'
'x-ms-tnm-applicationid' = '045268c0-445e-4ac1-9157-d58f67b167d9'
@@ -170,7 +169,7 @@ function New-TeamsAPIGetRequest($Uri, $tenantID, $Method = 'GET', $Resource = '4
return $ReturnedData
}
else {
- Write-Error "Not allowed. You cannot manage your own tenant or tenants not under your scope"
+ Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope'
}
}
@@ -184,8 +183,8 @@ function New-ClassicAPIGetRequest($TenantID, $Uri, $Method = 'GET', $Resource =
try {
$Data = Invoke-RestMethod -ContentType "$ContentType;charset=UTF-8" -Uri $NextURL -Method $Method -Headers @{
Authorization = "Bearer $($token.access_token)";
- "x-ms-client-request-id" = [guid]::NewGuid().ToString();
- "x-ms-client-session-id" = [guid]::NewGuid().ToString()
+ 'x-ms-client-request-id' = [guid]::NewGuid().ToString();
+ 'x-ms-client-session-id' = [guid]::NewGuid().ToString()
'x-ms-correlation-id' = [guid]::NewGuid()
'X-Requested-With' = 'XMLHttpRequest'
}
@@ -199,7 +198,7 @@ function New-ClassicAPIGetRequest($TenantID, $Uri, $Method = 'GET', $Resource =
return $ReturnedData
}
else {
- Write-Error "Not allowed. You cannot manage your own tenant or tenants not under your scope"
+ Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope'
}
}
@@ -209,10 +208,10 @@ function New-ClassicAPIPostRequest($TenantID, $Uri, $Method = 'POST', $Resource
if ((Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) {
try {
- $ReturnedData = Invoke-RestMethod -ContentType "application/json;charset=UTF-8" -Uri $Uri -Method $Method -Body $Body -Headers @{
+ $ReturnedData = Invoke-RestMethod -ContentType 'application/json;charset=UTF-8' -Uri $Uri -Method $Method -Body $Body -Headers @{
Authorization = "Bearer $($token.access_token)";
- "x-ms-client-request-id" = [guid]::NewGuid().ToString();
- "x-ms-client-session-id" = [guid]::NewGuid().ToString()
+ 'x-ms-client-request-id' = [guid]::NewGuid().ToString();
+ 'x-ms-client-session-id' = [guid]::NewGuid().ToString()
'x-ms-correlation-id' = [guid]::NewGuid()
'X-Requested-With' = 'XMLHttpRequest'
}
@@ -224,12 +223,12 @@ function New-ClassicAPIPostRequest($TenantID, $Uri, $Method = 'POST', $Resource
return $ReturnedData
}
else {
- Write-Error "Not allowed. You cannot manage your own tenant or tenants not under your scope"
+ Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope'
}
}
function Get-AuthorisedRequest($TenantID, $Uri) {
- if ($uri -like "https://graph.microsoft.com/beta/contracts*" -or $uri -like "*/customers/*" -or $uri -eq "https://graph.microsoft.com/v1.0/me/sendMail" -or $uri -like "https://graph.microsoft.com/beta/tenantRelationships/managedTenants*") {
+ if ($uri -like 'https://graph.microsoft.com/beta/contracts*' -or $uri -like '*/customers/*' -or $uri -eq 'https://graph.microsoft.com/v1.0/me/sendMail' -or $uri -like 'https://graph.microsoft.com/beta/tenantRelationships/managedTenants*') {
return $true
}
if ($TenantID -in (Get-Tenants).defaultdomainname) {
@@ -254,8 +253,8 @@ function Get-Tenants {
if ((!$Script:SkipListCache -and !$Script:SkipListCacheEmpty) -or !$Script:IncludedTenantsCache) {
# We create the excluded tenants file. This is not set to force so will not overwrite
- New-Item -ErrorAction SilentlyContinue -ItemType File -Path "ExcludedTenants"
- $Script:SkipListCache = Get-Content "ExcludedTenants" | ConvertFrom-Csv -Delimiter "|" -Header "Name", "User", "Date"
+ New-Item -ErrorAction SilentlyContinue -ItemType File -Path 'ExcludedTenants'
+ $Script:SkipListCache = Get-Content 'ExcludedTenants' | ConvertFrom-Csv -Delimiter '|' -Header 'Name', 'User', 'Date'
if ($null -eq $Script:SkipListCache) {
$Script:SkipListCacheEmpty = $true
}
@@ -263,10 +262,20 @@ function Get-Tenants {
# Load or refresh the cache if older than 24 hours
$Testfile = Get-Item $cachefile -ErrorAction SilentlyContinue | Where-Object -Property LastWriteTime -GT (Get-Date).Addhours(-24)
if ($Testfile) {
- $Script:IncludedTenantsCache = Get-Content $cachefile -ErrorAction SilentlyContinue | ConvertFrom-Json
+ $Script:IncludedTenantsCache = Get-Content $cachefile -ErrorAction SilentlyContinue | ConvertFrom-Json
}
else {
$Script:IncludedTenantsCache = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/contracts?`$top=999" -tenantid $ENV:Tenantid) | Select-Object CustomerID, DefaultdomainName, DisplayName, domains | Where-Object -Property DefaultdomainName -NotIn $Script:SkipListCache.name
+ if ($ENV:PartnerTenantAvailable) {
+ $PartnerTenant = @([PSCustomObject]@{
+ customerId = $env:TenantID
+ defaultDomainName = $env:TenantID
+ displayName = '*Partner Tenant'
+ domains = 'PartnerTenant'
+ })
+ $Script:IncludedTenantsCache = $PartnerTenant + $Script:IncludedTenantsCache
+ }
+
if ($Script:IncludedTenantsCache) {
$Script:IncludedTenantsCache | ConvertTo-Json | Out-File $cachefile
}
@@ -278,6 +287,7 @@ function Get-Tenants {
if ($IncludeAll) {
return (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/contracts?`$top=999" -tenantid $ENV:Tenantid) | Select-Object CustomerID, DefaultdomainName, DisplayName, domains
}
+
else {
return $Script:IncludedTenantsCache
}
@@ -285,15 +295,21 @@ function Get-Tenants {
function Remove-CIPPCache {
Remove-Item 'tenants.cache.json' -Force
- Get-ChildItem -Path "Cache_BestPracticeAnalyser" -Filter *.json | Remove-Item -Force -ErrorAction SilentlyContinue
- Get-ChildItem -Path "Cache_DomainAnalyser" -Filter *.json | Remove-Item -Force -ErrorAction SilentlyContinue
+ Get-ChildItem -Path 'Cache_BestPracticeAnalyser' -Filter *.json | Remove-Item -Force -ErrorAction SilentlyContinue
+ Get-ChildItem -Path 'Cache_DomainAnalyser' -Filter *.json | Remove-Item -Force -ErrorAction SilentlyContinue
+ Get-ChildItem -Path 'Cache_BestPracticeAnalyser\CurrentlyRunning.txt' -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
+ Get-ChildItem -Path 'ChocoApps.Cache\CurrentlyRunning.txt' -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
+ Get-ChildItem -Path 'Cache_DomainAnalyser\CurrentlyRunning.txt' -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
+ Get-ChildItem -Path 'Cache_Scheduler\CurrentlyRunning.txt' -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
+ Get-ChildItem -Path 'SecurityBaselines_All\CurrentlyRunning.txt' -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
+ Get-ChildItem -Path 'Cache_Standards\CurrentlyRunning.txt' -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
$Script:SkipListCache = $Null
$Script:SkipListCacheEmpty = $Null
$Script:IncludedTenantsCache = $Null
}
function New-ExoRequest ($tenantid, $cmdlet, $cmdParams) {
- $Headers = Get-GraphToken -AppID 'a0c73c16-a7e3-4564-9a95-2bdf47383716' -RefreshToken $ENV:ExchangeRefreshToken -Scope 'https://outlook.office365.com/.default' -Tenantid $tenantid
+ $token = Get-ClassicAPIToken -resource 'https://outlook.office365.com' -Tenantid $tenantid
if ((Get-AuthorisedRequest -TenantID $tenantid)) {
$tenant = (get-tenants | Where-Object -Property defaultDomainName -EQ $tenantid).customerid
if ($cmdParams) {
@@ -302,16 +318,119 @@ function New-ExoRequest ($tenantid, $cmdlet, $cmdParams) {
else {
$Params = @{}
}
- $ExoBody = @{
+ $ExoBody = ConvertTo-Json -Depth 5 -InputObject @{
CmdletInput = @{
CmdletName = $cmdlet
Parameters = $Params
}
- } | ConvertTo-Json
- $ReturnedData = Invoke-RestMethod "https://outlook.office365.com/adminapi/beta/$($tenant)/InvokeCommand" -Method POST -Body $ExoBody -Headers $Headers -ContentType "application/json; charset=utf-8"
+ }
+ $OnMicrosoft = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/domains?$top=999' -tenantid $tenantid | Where-Object -Property isInitial -EQ $true).id
+ $Headers = @{
+ Authorization = "Bearer $($token.access_token)"
+ 'X-AnchorMailbox' = "UPN:SystemMailbox{bb558c35-97f1-4cb9-8ff7-d53741dc928c}@$($OnMicrosoft)"
+
+ }
+ try {
+ $ReturnedData = Invoke-RestMethod "https://outlook.office365.com/adminapi/beta/$($tenant)/InvokeCommand" -Method POST -Body $ExoBody -Headers $Headers -ContentType 'application/json; charset=utf-8'
+ }
+ catch {
+ $Message = ($_.ErrorDetails | ConvertFrom-Json -ErrorAction SilentlyContinue).error.details.message
+ if ($Message -eq $null) { $Message = $($_.Exception.Message) }
+ throw $Message
+ }
return $ReturnedData.value
}
else {
- Write-Error "Not allowed. You cannot manage your own tenant or tenants not under your scope"
+ Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope'
}
-}
\ No newline at end of file
+}
+
+function Read-JwtAccessDetails {
+ <#
+ .SYNOPSIS
+ Parse Microsoft JWT access tokens
+
+ .DESCRIPTION
+ Extract JWT access token details for verification
+
+ .PARAMETER Token
+ Token to get details for
+
+ #>
+ [cmdletbinding()]
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$Token
+ )
+
+ # Default token object
+ $TokenDetails = [PSCustomObject]@{
+ AppId = ''
+ AppName = ''
+ Audience = ''
+ AuthMethods = ''
+ IPAddress = ''
+ Name = ''
+ Scope = ''
+ TenantId = ''
+ UserPrincipalName = ''
+ }
+
+ if (!$Token.Contains('.') -or !$token.StartsWith('eyJ')) { return $TokenDetails }
+
+ # Get token payload
+ $tokenPayload = $token.Split('.')[1].Replace('-', '+').Replace('_', '/')
+ while ($tokenPayload.Length % 4) {
+ $tokenPayload = '{0}=' -f $tokenPayload
+ }
+
+ # Convert base64 to json to object
+ $tokenByteArray = [System.Convert]::FromBase64String($tokenPayload)
+ $tokenArray = [System.Text.Encoding]::ASCII.GetString($tokenByteArray)
+ $TokenObj = $tokenArray | ConvertFrom-Json
+
+ # Convert token details to human readable
+ $TokenDetails.AppId = $TokenObj.appid
+ $TokenDetails.AppName = $TokenObj.app_displayname
+ $TokenDetails.Audience = $TokenObj.aud
+ $TokenDetails.AuthMethods = $TokenObj.amr
+ $TokenDetails.IPAddress = $TokenObj.ipaddr
+ $TokenDetails.Name = $TokenObj.name
+ $TokenDetails.Scope = $TokenObj.scp -split ' '
+ $TokenDetails.TenantId = $TokenObj.tid
+ $TokenDetails.UserPrincipalName = $TokenObj.upn
+
+ return $TokenDetails
+}
+
+function Get-CIPPMSolUsers {
+ [CmdletBinding()]
+ param (
+ [string]$tenant
+ )
+ $AADGraphtoken = (Get-GraphToken -scope 'https://graph.windows.net/.default')
+ $tenantid = (get-tenants | Where-Object -Property DefaultDomainName -EQ $tenant).CustomerID
+ $TrackingGuid = New-Guid
+ $LogonPost = @"
+http://provisioning.microsoftonline.com/IProvisioningWebService/MsolConnecturn:uuid:$TrackingGuidhttp://www.w3.org/2005/08/addressing/anonymous$($AADGraphtoken['Authorization'])50afce61-c917-435b-8c6d-60aa5a8b8aa71.2.183.57Version47$($TrackingGuid)https://provisioningapi.microsoftonline.com/provisioningwebservice.svcVersion4
+"@
+ $DataBlob = (Invoke-RestMethod -Method POST -Uri 'https://provisioningapi.microsoftonline.com/provisioningwebservice.svc' -ContentType 'application/soap+xml; charset=utf-8' -Body $LogonPost).envelope.header.BecContext.DataBlob.'#text'
+
+ $MSOLXML = @"
+http://provisioning.microsoftonline.com/IProvisioningWebService/ListUsersurn:uuid:$TrackingGuidhttp://www.w3.org/2005/08/addressing/anonymous$($AADGraphtoken['Authorization'])$DataBlob250afce61-c917-435b-8c6d-60aa5a8b8aa71.2.183.57Version474e6cb653-c968-4a3a-8a11-2c8919218aebhttps://provisioningapi.microsoftonline.com/provisioningwebservice.svcVersion16$($tenantid)500AscendingNone
+"@
+ $userlist = do {
+ if ($null -eq $page) {
+ $Page = (Invoke-RestMethod -Uri 'https://provisioningapi.microsoftonline.com/provisioningwebservice.svc' -Method post -Body $MSOLXML -ContentType 'application/soap+xml; charset=utf-8').envelope.body.ListUsersResponse.listusersresult.returnvalue
+ $Page.results.user
+ }
+ else {
+ $Page = (Invoke-RestMethod -Uri 'https://provisioningapi.microsoftonline.com/provisioningwebservice.svc' -Method post -Body $MSOLXML -ContentType 'application/soap+xml; charset=utf-8').envelope.body.NavigateUserResultsResponse.NavigateUserResultsResult.returnvalue
+ $Page.results.user
+ }
+ $MSOLXML = @"
+http://provisioning.microsoftonline.com/IProvisioningWebService/NavigateUserResultsurn:uuid:$TrackingGuidhttp://www.w3.org/2005/08/addressing/anonymous$($AADGraphtoken['Authorization'])$DataBlob13050afce61-c917-435b-8c6d-60aa5a8b8aa71.2.183.57Version47$($TrackingGuid)https://provisioningapi.microsoftonline.com/provisioningwebservice.svcVersion16$($tenantid)$($page.listcontext)Next
+"@
+ } until ($page.IsLastPage -eq $true -or $null -eq $page)
+ return $userlist
+}
\ No newline at end of file
diff --git a/ListAlertsQueue/run.ps1 b/ListAlertsQueue/run.ps1
index 0c0b9ae1cd89..f374634c1da8 100644
--- a/ListAlertsQueue/run.ps1
+++ b/ListAlertsQueue/run.ps1
@@ -16,8 +16,16 @@ $CurrentStandards = foreach ($QueueFile in $QueuedApps) {
$ApplicationFile = Get-Content "$($QueueFile)" | ConvertFrom-Json
if ($ApplicationFile.Tenant -eq $null) { continue }
[PSCustomObject]@{
- tenantName = $ApplicationFile.tenant
- alerts = (($ApplicationFile.psobject.properties.name | Where-Object { $_ -NE "Tenant" }) -join ' & ')
+ tenantName = $ApplicationFile.tenant
+ AdminPassword = [bool]$ApplicationFile.AdminPassword
+ DefenderMalware = [bool]$ApplicationFile.DefenderMalware
+ DefenderStatus = [bool]$ApplicationFile.DefenderStatus
+ MFAAdmins = [bool]$ApplicationFile.MFAAdmins
+ MFAAlertUsers = [bool]$ApplicationFile.MFAAlertUsers
+ NewGA = [bool]$ApplicationFile.NewGA
+ NewRole = [bool]$ApplicationFile.NewRole
+ QuotaUsed = [bool]$ApplicationFile.QuotaUsed
+ UnusedLicenses = [bool]$ApplicationFile.UnusedLicenses
}
}
diff --git a/ListCalendarPermissions/run.ps1 b/ListCalendarPermissions/run.ps1
index 6b29aa5eabc7..ba36ea74ef2e 100644
--- a/ListCalendarPermissions/run.ps1
+++ b/ListCalendarPermissions/run.ps1
@@ -7,32 +7,23 @@ $APIName = $TriggerMetadata.FunctionName
Log-Request -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Accessed this API" -Sev "Debug"
$UserID = $request.Query.UserID
$Tenantfilter = $request.Query.tenantfilter
-Write-Information "My username is $UserID"
-Write-Information "My tenantfilter is $Tenantfilter"
-if ($UserID -eq $null) { exit }
-#$userid = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($username)" -tenantid $Tenantfilter).id
-$UserDetails = New-GraphGetRequest -uri "https://outlook.office365.com/adminapi/beta/$($tenantfilter)/Mailbox('$UserID')" -Tenantid $tenantfilter -scope ExchangeOnline -noPagination $true
-$Results = [System.Collections.ArrayList]@()
-$upn = "notrequired@notrequired.com"
-$tokenvalue = ConvertTo-SecureString (Get-GraphToken -AppID 'a0c73c16-a7e3-4564-9a95-2bdf47383716' -RefreshToken $ENV:ExchangeRefreshToken -Scope 'https://outlook.office365.com/.default' -Tenantid $tenantFilter).Authorization -AsPlainText -Force
-$credential = New-Object System.Management.Automation.PSCredential($upn, $tokenValue)
-$session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://ps.outlook.com/powershell-liveid?DelegatedOrg=$($tenantFilter)&BasicAuthToOAuthConversion=true" -Credential $credential -Authentication Basic -AllowRedirection -ea Stop
-$upn = 'notRequired@required.com'
-
try {
- Import-PSSession $session -ea Silentlycontinue -AllowClobber -CommandName "Get-MailboxFolderPermission"
- # $CalPerms = (Get-MailboxFolderPermission -Identity $($userid.PrimarySmtpAddress))
- $CalPerms = (Get-MailboxFolderPermission -Identity $($UserDetails.PrimarySmtpAddress))
-
- Get-PSSession | Remove-PSSession
- Log-request -API 'List Calendar Permissions' -tenant $tenantfilter -message "Calendar permissions listed for $($tenantfilter)" -sev Info
-} catch {
- Log-request -API 'List Calendar Permissions' -tenant $tenantfilter -message "Failed to list calendar permissions. Error: $($_.exception.message)" -sev Error
+ $GetCalParam = @{Identity = $UserID; FolderScope = 'Calendar' }
+ $CalendarFolder = New-ExoRequest -tenantid $Tenantfilter -cmdlet "Get-MailboxFolderStatistics" -cmdParams $GetCalParam | Select-Object -First 1
+ $CalParam = @{Identity = "$($UserID):\$($CalendarFolder.name)" }
+ $GraphRequest = New-ExoRequest -tenantid $Tenantfilter -cmdlet "Get-MailboxFolderPermission" -cmdParams $CalParam | Select-Object Identity, User, AccessRights, FolderName
+ Log-request -API 'List Calendar Permissions' -tenant $tenantfilter -message "Calendar permissions listed for $($tenantfilter)" -sev Debug
+ $StatusCode = [HttpStatusCode]::OK
}
-
+catch {
+ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
+ $StatusCode = [HttpStatusCode]::Forbidden
+ $GraphRequest = $ErrorMessage
+}
+
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = [HttpStatusCode]::OK
- Body = $CalPerms
-})
+ StatusCode = $StatusCode
+ Body = @($GraphRequest)
+ })
diff --git a/ListContacts/run.ps1 b/ListContacts/run.ps1
index d1bfd8f968ac..a1281fe207cf 100644
--- a/ListContacts/run.ps1
+++ b/ListContacts/run.ps1
@@ -29,9 +29,3 @@ catch {
$StatusCode = [HttpStatusCode]::Forbidden
$GraphRequest = $ErrorMessage
}
-# Associate values to output bindings by calling 'Push-OutputBinding'.
-Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = $StatusCode
- Body = @($GraphRequest)
- })
-
diff --git a/ListDevices/run.ps1 b/ListDevices/run.ps1
index a357c2813352..f06692c71935 100644
--- a/ListDevices/run.ps1
+++ b/ListDevices/run.ps1
@@ -15,8 +15,8 @@ $TenantFilter = $Request.Query.TenantFilter
try {
$GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/devices" -Tenantid $tenantfilter | Select-Object @{ Name = 'ID'; Expression = { $_.'id' } },
@{ Name = 'accountEnabled'; Expression = { $_.'accountEnabled' } },
- @{ Name = 'approximateLastSignInDateTime'; Expression = { $_.'approximateLastSignInDateTime' | Out-String } },
- @{ Name = 'createdDateTime'; Expression = { $_.'createdDateTime' | Out-String } },
+ @{ Name = 'approximateLastSignInDateTime'; Expression = { ($_.'approximateLastSignInDateTime').ToString("yyyy-MM-dd HH:mm") } },
+ @{ Name = 'createdDateTime'; Expression = { ($_.'createdDateTime').ToString("yyyy-MM-dd") } },
@{ Name = 'deviceOwnership'; Expression = { $_.'deviceOwnership' } },
@{ Name = 'displayName'; Expression = { $_.'displayName' } },
@{ Name = 'enrollmentType'; Expression = { $_.'enrollmentType' } },
diff --git a/ListDomainHealth/function.json b/ListDomainHealth/function.json
new file mode 100644
index 000000000000..306b0c51e560
--- /dev/null
+++ b/ListDomainHealth/function.json
@@ -0,0 +1,19 @@
+{
+ "bindings": [
+ {
+ "authLevel": "anonymous",
+ "type": "httpTrigger",
+ "direction": "in",
+ "name": "Request",
+ "methods": [
+ "get",
+ "post"
+ ]
+ },
+ {
+ "type": "http",
+ "direction": "out",
+ "name": "Response"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ExecDnsHelper/run.ps1 b/ListDomainHealth/run.ps1
similarity index 100%
rename from ExecDnsHelper/run.ps1
rename to ListDomainHealth/run.ps1
diff --git a/ListMFAUsers/run.ps1 b/ListMFAUsers/run.ps1
index 73941c281811..83d013928798 100644
--- a/ListMFAUsers/run.ps1
+++ b/ListMFAUsers/run.ps1
@@ -9,25 +9,14 @@ Log-Request -user $request.headers.'x-ms-client-principal' -API $APINAME -messag
# Write to the Azure Functions log stream.
Write-Host 'PowerShell HTTP trigger function processed a request.'
-#Let's hack the provisioning API to get per user MFA
-$AADGraphtoken = (Get-GraphToken -scope 'https://graph.windows.net/.default')
-$tenantid = ((Get-Content 'tenants.cache.json' | ConvertFrom-Json) | Where-Object -Property DefaultDomainName -EQ $Request.Query.TenantFilter).CustomerID
-$TrackingGuid = New-Guid
-$LogonPost = @"
-http://provisioning.microsoftonline.com/IProvisioningWebService/MsolConnecturn:uuid:$TrackingGuidhttp://www.w3.org/2005/08/addressing/anonymous$($AADGraphtoken['Authorization'])50afce61-c917-435b-8c6d-60aa5a8b8aa71.2.183.57Version47$($TrackingGuid)https://provisioningapi.microsoftonline.com/provisioningwebservice.svcVersion4
-"@
-$DataBlob = (Invoke-RestMethod -Method POST -Uri 'https://provisioningapi.microsoftonline.com/provisioningwebservice.svc' -ContentType 'application/soap+xml; charset=utf-8' -Body $LogonPost).envelope.header.BecContext.DataBlob.'#text'
-
-$MSOLXML = @"
-http://provisioning.microsoftonline.com/IProvisioningWebService/ListUsersurn:uuid:$TrackingGuidhttp://www.w3.org/2005/08/addressing/anonymous$($AADGraphtoken['Authorization'])$DataBlob250afce61-c917-435b-8c6d-60aa5a8b8aa71.2.183.57Version474e6cb653-c968-4a3a-8a11-2c8919218aebhttps://provisioningapi.microsoftonline.com/provisioningwebservice.svcVersion16$($tenantid)500AscendingNone
-"@
-$Users = (Invoke-RestMethod -Uri 'https://provisioningapi.microsoftonline.com/provisioningwebservice.svc' -Method post -Body $MSOLXML -ContentType 'application/soap+xml; charset=utf-8').envelope.body.ListUsersResponse.listusersresult.returnvalue.results.user
+$users = Get-CIPPMSolUsers -tenant $Request.query.TenantFilter
$SecureDefaultsState = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/identitySecurityDefaultsEnforcementPolicy' -tenantid $Request.query.TenantFilter ).IsEnabled
$CAState = New-Object System.Collections.ArrayList
+$CAPolicies = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies' -tenantid $Request.query.TenantFilter )
+
try {
$ExcludeAllUsers = New-Object System.Collections.ArrayList
$ExcludeSpecific = New-Object System.Collections.ArrayList
- $CAPolicies = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies' -tenantid $Request.query.TenantFilter )
foreach ($Policy in $CAPolicies) {
if (($policy.grantControls.builtincontrols -eq 'mfa') -or ($policy.grantControls.customAuthenticationFactors -eq 'RequireDuoMfa')) {
diff --git a/ListMailboxStatistics/run.ps1 b/ListMailboxStatistics/run.ps1
index d72be9a78c5b..5075569e8bcc 100644
--- a/ListMailboxStatistics/run.ps1
+++ b/ListMailboxStatistics/run.ps1
@@ -19,7 +19,7 @@ try {
@{ Name = 'UsedGB'; Expression = { [math]::round($_.'Storage Used (Byte)' / 1GB, 0) } },
@{ Name = 'QuotaGB'; Expression = { [math]::round($_.'Prohibit Send/Receive Quota (Byte)' / 1GB, 0) } },
@{ Name = 'ItemCount'; Expression = { $_.'Item Count' } },
- @{ Name = 'HasArchive'; Expression = { $_.'Has Archive' } }
+ @{ Name = 'HasArchive'; Expression = { If (($_.'Has Archive').ToLower() -eq 'true') { [bool]$true } else { [bool]$false } } }
$StatusCode = [HttpStatusCode]::OK
}
catch {
diff --git a/ListMailboxes/run.ps1 b/ListMailboxes/run.ps1
index a7ac160bbd90..bebd197f7e9a 100644
--- a/ListMailboxes/run.ps1
+++ b/ListMailboxes/run.ps1
@@ -31,9 +31,3 @@ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = $StatusCode
Body = @($GraphRequest)
})
-
-# Associate values to output bindings by calling 'Push-OutputBinding'.
-Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = [HttpStatusCode]::OK
- Body = @($GraphRequest)
- })
diff --git a/ListSites/run.ps1 b/ListSites/run.ps1
index 26e957dbc228..103b608da5d0 100644
--- a/ListSites/run.ps1
+++ b/ListSites/run.ps1
@@ -28,10 +28,11 @@ try {
$GraphRequest = $ParsedRequest | Select-Object @{ Name = 'UPN'; Expression = { $_.'Owner Principal Name' } },
@{ Name = 'displayName'; Expression = { $_.'Owner Display Name' } },
@{ Name = 'LastActive'; Expression = { $_.'Last Activity Date' } },
- @{ Name = 'FileCount'; Expression = { $_.'File Count' } },
+ @{ Name = 'FileCount'; Expression = { [int]$_.'File Count' } },
@{ Name = 'UsedGB'; Expression = { [math]::round($_.'Storage Used (Byte)' / 1GB, 0) } },
@{ Name = 'URL'; Expression = { $_.'Site URL' } },
- @{ Name = 'Allocated'; Expression = { $_.'Storage Allocated (Byte)' / 1GB } }
+ @{ Name = 'Allocated'; Expression = { $_.'Storage Allocated (Byte)' / 1GB } },
+ @{ Name = 'Template'; Expression = { $_.'Root Web Template' } }
$StatusCode = [HttpStatusCode]::OK
}
catch {
diff --git a/ListStandards/run.ps1 b/ListStandards/run.ps1
index 1085b15dc3c0..df1aa5aeb87c 100644
--- a/ListStandards/run.ps1
+++ b/ListStandards/run.ps1
@@ -14,11 +14,27 @@ $Tenants = Get-ChildItem "Cache_Standards\*.standards.json"
$CurrentStandards = foreach ($tenant in $tenants) {
$StandardsFile = Get-Content "$($tenant)" | ConvertFrom-Json
- if ($StandardsFile.Tenant -eq $null) { continue }
+ if ($null -eq $StandardsFile.Tenant) { continue }
[PSCustomObject]@{
- displayName = $StandardsFile.tenant
- standardName = ($standardsFile.Standards.psobject.properties.name -join ' & ')
- appliedBy = $StandardsFile.addedby
+ displayName = $StandardsFile.tenant
+ appliedBy = $StandardsFile.addedby
+ appliedAt = ($tenant).LastWriteTime.toString('s')
+ "DisableBasicAuth" = $StandardsFile.standards.DisableBasicAuth
+ "ModernAuth" = $StandardsFile.standards.ModernAuth
+ "AuditLog" = $StandardsFile.standards.AuditLog
+ "AutoExpandArchive" = $StandardsFile.standards.AutoExpandArchive
+ "SecurityDefaults" = $StandardsFile.standards.SecurityDefaults
+ "DisableSharedMailbox" = $StandardsFile.standards.DisableSharedMailbox
+ "UndoOauth" = $StandardsFile.standards.UndoOauth
+ "DisableSelfServiceLicenses" = $StandardsFile.standards.DisableSelfServiceLicenses
+ "AnonReportDisable" = $StandardsFile.standards.AnonReportDisable
+ "UndoSSPR" = $StandardsFile.standards.UndoSSPR
+ "PasswordExpireDisabled" = $StandardsFile.standards.PasswordExpireDisabled
+ "DelegateSentItems" = $StandardsFile.standards.DelegateSentItems
+ "OauthConsent" = $StandardsFile.standards.OauthConsent
+ "SSPR" = $StandardsFile.standards.SSPR
+ "LegacyMFA" = $StandardsFile.standards.LegacyMFA
+ "SpoofWarn" = $StandardsFile.standards.SpoofWarn
}
}
diff --git a/ListTenants/run.ps1 b/ListTenants/run.ps1
index 3943ed5704d5..0248233b5ff1 100644
--- a/ListTenants/run.ps1
+++ b/ListTenants/run.ps1
@@ -28,7 +28,7 @@ try {
$allTenants = @([PSCustomObject]@{
customerId = 'AllTenants'
defaultDomainName = 'AllTenants'
- displayName = 'All Tenants'
+ displayName = '*All Tenants'
domains = 'AllTenants'
})
$body = $allTenants + $tenants
@@ -40,7 +40,9 @@ try {
else {
$body = Get-Tenants | Where-Object -Property DefaultdomainName -EQ $Tenantfilter
}
- Log-Request -user $request.headers.'x-ms-client-principal' -tenant $Tenantfilter -API $APINAME -message 'Listed Tenant Details' -Sev 'Info'
+
+
+ Log-Request -user $request.headers.'x-ms-client-principal' -tenant $Tenantfilter -API $APINAME -message 'Listed Tenant Details' -Sev 'Debug'
}
catch {
Log-Request -user $request.headers.'x-ms-client-principal' -tenant $Tenantfilter -API $APINAME -message "List Tenant failed. The error is: $($_.Exception.Message)" -Sev 'Error'
diff --git a/ListUsers/run.ps1 b/ListUsers/run.ps1
index ea61e458ace4..495fd9e1edce 100644
--- a/ListUsers/run.ps1
+++ b/ListUsers/run.ps1
@@ -41,7 +41,6 @@ if ($userid) {
@{ Name = 'LastSigninResult'; Expression = { if ($LastSignIn.Status.ErrorCode -eq 0) { "Success" } else { "Failure" } } },
@{ Name = 'LastSigninFailureReason'; Expression = { if ($LastSignIn.Status.ErrorCode -eq 0) { "Sucessfully signed in" } else { $LastSignIn.status.FailureReason } } }
}
-
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = [HttpStatusCode]::OK
diff --git a/Scheduler_Alert/run.ps1 b/Scheduler_Alert/run.ps1
index 82ed0f965d71..55e00443c146 100644
--- a/Scheduler_Alert/run.ps1
+++ b/Scheduler_Alert/run.ps1
@@ -1,39 +1,95 @@
param($tenant)
+Write-Host $($Tenant.tenant)
+Write-Host $($Tenant.tag)
+Write-Host $($Tenant | ConvertTo-Json)
+#thoughts: add more delta/tracking to prevent duplicate alerts.
+if ($Tenant.tag -eq "AllTenants") {
+ $Alerts = Get-Content ".\Cache_Scheduler\AllTenants.alert.json" | ConvertFrom-Json
+}
+else {
+ $Alerts = Get-Content ".\Cache_Scheduler\$($tenant.tenant).alert.json" | ConvertFrom-Json
+}
+$ShippedAlerts = switch ($Alerts) {
+ { $Alerts."AdminPassword" -eq $true } {
+ New-GraphGETRequest -uri "https://graph.microsoft.com/beta/roleManagement/directory/roleAssignments?`$filter=roleDefinitionId eq '62e90394-69f5-4237-9190-012177145e10'" -tenantid $($tenant.tenant) | ForEach-Object {
+ $LastChanges = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/users/$($_.principalId)?`$select=UserPrincipalName,lastPasswordChangeDateTime" -tenant $($tenant.tenant)
+ if ([datetime]$LastChanges.LastPasswordChangeDateTime -gt (Get-Date).AddDays(-1)) { "Admin password has been changed for $($LastChanges.UserPrincipalName) in last 24 hours" }
+ }
+ }
+ { $_."DefenderMalware" -eq $true } {
-try {
- if ($Tenant.tag -eq "AllTenants") {
- $Alerts = Get-Content ".\Cache_Scheduler\AllTenants.alert.json" | ConvertFrom-Json
- }
- else {
- $Alerts = Get-Content ".\Cache_Scheduler\$($tenant.tenant).alert.json" | ConvertFrom-Json
- }
- #Does not work yet.
- $ShippedAlerts = switch ($Alerts) {
- { $_."AdminPassword" -eq $true } {
- New-GraphGETRequest -uri "https://graph.microsoft.com/beta/roleManagement/directory/roleAssignments?`$filter=roleDefinitionId eq '62e90394-69f5-4237-9190-012177145e10'" -tenantid $($tenant.tenant) | ForEach-Object {
- $LastChanges = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/roleManagement/users/$($_.PrincipalID)/`$select=UserPrincipalName,lastPasswordChangeDateTime" -tenant $($tenant.tenant)
- if ($LastChanges.LastPasswordChangeDateTime -lt (Get-Date).AddDays(-300)) { Write-Host "Admin password has been changed for $($LastChanges.UserPrincipalName) in last 24 hours" }
- }
+ New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/managedTenants/windowsDeviceMalwareStates?`$top=999&`$filter=tenantId eq '$($Tenant.tenantid)'" | Where-Object { $_.malwareThreatState -eq "Active" } | ForEach-Object {
+ "$($_.managedDeviceName): Malware found and active. Severity: $($_.MalwareSeverity). Malware name: $($_.MalwareDisplayName)"
+ }
+ }
+ { $_."DefenderStatus" -eq $true } {
+ New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/managedTenants/windowsProtectionStates?`$top=999&`$filter=tenantId eq '$($Tenant.tenantid)'" | Where-Object { $_.realTimeProtectionEnabled -eq $false -or $_.MalwareprotectionEnabled -eq $false } | ForEach-Object {
+ "$($_.managedDeviceName) - Real Time Protection: $($_.realTimeProtectionEnabled) & Malware Protection: $($_.MalwareprotectionEnabled)"
+ }
+ }
+ { $_."MFAAdmins" -eq $true } {
+ $AdminIds = (New-GraphGETRequest -uri "https://graph.microsoft.com/beta/roleManagement/directory/roleAssignments?`$filter=roleDefinitionId eq '62e90394-69f5-4237-9190-012177145e10'&expand=principal" -tenantid $($tenant.tenant)).principal
+ $AdminList = Get-CIPPMSolUsers -tenant $tenant.tenant | Where-Object -Property ObjectID -In $AdminIds.id
+ $MFARegistration = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/reports/credentialUserRegistrationDetails' -tenantid $tenant.tenant)
+ $AdminList | Where-Object { $_.Usertype -eq "Member" -and $_.BlockCredential -eq $false } | ForEach-Object {
+ $CARegistered = ($MFARegistration | Where-Object -Property UserPrincipalName -EQ $_.UserPrincipalName).IsMFARegistered
+ if ($_.StrongAuthenticationRequirements.StrongAuthenticationRequirement.state -eq $null -and $CARegistered -eq $false) { "Admin $($_.UserPrincipalName) is enabled but does not have any form of MFA configured." }
}
- { $_."DefenderMalware" -eq $true } {}
- { $_."DefenderStatus" -eq $true } {}
- { $_."DisableRestart" -eq $true } {}
- { $_."InstallAsSystem" -eq $true } {}
- { $_."MFAAdmins" -eq $true } {}
- { $_."MFAAlertUsers" -eq $true } {}
- { $_."NewApprovedApp" -eq $true } {}
- { $_."NewGA" -eq $true } {}
- { $_."NewRole" -eq $true } {}
- { $_."QuotaUsed" -eq $true } {}
- { $_."UnusedLicenses" -eq $true } {}
- }
- Write-Host "Shipped in switch."
- Write-Host $ShippedAlerts
+ }
+ { $_."MFAAlertUsers" -eq $true } {
+ $users = Get-CIPPMSolUsers -tenant $tenant.tenant
+ $MFARegistration = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/reports/credentialUserRegistrationDetails' -tenantid $tenant.tenant)
+ $users | Where-Object { $_.Usertype -eq "Member" -and $_.BlockCredential -eq $false } | ForEach-Object {
+ $CARegistered = ($MFARegistration | Where-Object -Property UserPrincipalName -EQ $_.UserPrincipalName).IsMFARegistered
+ if ($_.StrongAuthenticationRequirements.StrongAuthenticationRequirement.state -eq $null -and $CARegistered -eq $false) { "User $($_.UserPrincipalName) is enabled but does not have any form of MFA configured." }
+ }
+ }
- #EmailAllAlertsInNiceTable
- Log-request -API "Scheduler" -tenant $($tenant.tenant) -message "Collecting alerts for $($($tenant.tenant))" -sev debug
+ { $_."NewRole" -eq $true } {
+ $AdminDelta = Get-Content ".\Cache_AlertsCheck\$($Tenant.tenant).AdminDelta.json" | ConvertFrom-Json
+ $NewDelta = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/directoryRoles?`$expand=members" -tenantid $Tenant.tenant) | Select-Object displayname, Members | ForEach-Object {
+ [PSCustomObject]@{
+ GroupName = $_.displayname
+ Members = $_.Members.UserPrincipalName
+ }
+ }
+ $null = New-Item ".\Cache_AlertsCheck\$($Tenant.tenant).AdminDelta.json" -Value ($NewDelta | ConvertTo-Json) -Force
+ if ($AdminDelta) {
+ foreach ($Group in $NewDelta) {
+ $OldDelta = $AdminDelta | Where-Object { $_.GroupName -eq $Group.GroupName }
+ $Group.members | Where-Object { $_ -notin $OldDelta.members } | ForEach-Object {
+ "$_ has been added to the $($Group.GroupName) Role"
+ }
+ }
+ }
+ }
+ { $_."QuotaUsed" -eq $true } {
+ New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/getMailboxUsageDetail(period='D7')?`$format=application/json" -tenantid $Tenant.tenant | ForEach-Object {
+ $PercentLeft = [math]::round($_.StorageUsedInBytes / $_.prohibitSendReceiveQuotaInBytes * 100)
+ if ($PercentLeft -gt 80) { "$($_.UserPrincipalName): Mailbox has less than 10% space left. Mailbox is $PercentLeft% full" }
+ }
+ }
+ { $Alerts."UnusedLicenses" -eq $true } {
+ $ConvertTable = Import-Csv Conversiontable.csv
+ $ExcludedSkuList = Get-Content ".\config\ExcludeSkuList.json" | ConvertFrom-Json
+ New-GraphGetRequest -uri "https://graph.microsoft.com/beta/subscribedSkus" -tenantid $Tenant.tenant | ForEach-Object {
+ $skuid = $_
+ foreach ($sku in $skuid) {
+ if ($sku.skuId -in $ExcludedSkuList.guid) { continue }
+ $PrettyName = ($ConvertTable | Where-Object { $_.guid -eq $sku.skuid }).'Product_Display_Name' | Select-Object -Last 1
+ if (!$PrettyName) { $PrettyName = $skuid.skuPartNumber }
+ if ($sku.prepaidUnits.enabled - $sku.consumedUnits -ne 0) {
+ "$PrettyName has unused licenses. Using $($sku.consumedUnits) of $($sku.prepaidUnits.enabled)."
+ }
+ }
+ }
+ }
+}
+$currentlog = Get-Content "Logs\$((Get-Date).ToString('ddMMyyyy')).log" | ConvertFrom-Csv -Header "DateTime", "Tenant", "API", "Message", "User", "Severity" -Delimiter "|" | Where-Object -Property Tenant -EQ $tenant.tenant
+$ShippedAlerts | ForEach-Object {
+ if ($_ -in $currentlog.message) {
+ continue
+ }
+ Log-Request -message $_ -API "Alerts" -tenant $tenant.tenant -sev Alert
}
-catch {
- Log-request -API "Scheduler" -tenant $($tenant.tenant) -message "Failed to get alerts for $($($tenant.tenant)) Error: $($_.exception.message)" -sev Error
-}
\ No newline at end of file
diff --git a/Scheduler_CIPPNotifications/run.ps1 b/Scheduler_CIPPNotifications/run.ps1
index 9cf3285a772e..e84f860c2d00 100644
--- a/Scheduler_CIPPNotifications/run.ps1
+++ b/Scheduler_CIPPNotifications/run.ps1
@@ -4,24 +4,22 @@ param($tenant)
$currentUTCtime = (Get-Date).ToUniversalTime()
if (Test-Path '.\Config\Config_Notifications.Json') {
- $Config = Get-Content '.\Config\Config_Notifications.Json' | ConvertFrom-Json
+ $Config = Get-Content '.\Config\Config_Notifications.Json' | ConvertFrom-Json
}
else {
- Write-Host 'Done - No config active'
- exit
+ Write-Host 'Done - No config active'
+ exit
}
-$Settings = $Config.psobject.properties.name
+$Settings = $Config.psobject.properties.name + "Alerts"
$logdate = (Get-Date).ToString('ddMMyyyy')
$Currentlog = Get-Content "Logs\$($logdate).log" | ConvertFrom-Csv -Header 'DateTime', 'Tenant', 'API', 'Message', 'User', 'Severity' -Delimiter '|' | Where-Object { [datetime]$_.Datetime -gt (Get-Date).AddMinutes(-16) -and $_.api -in $Settings -and $_.Severity -ne 'debug' }
-Write-Host "Current log: $CurrentLog"
-Write-Host $Config
if ($Config.email -ne '' -and $null -ne $CurrentLog) {
- $HTMLLog = ($CurrentLog | ConvertTo-Html -frag) -replace '', '' | Out-String
- $JSONBody = @"
+ $HTMLLog = ($CurrentLog | ConvertTo-Html -frag) -replace '', '' | Out-String
+ $JSONBody = @"
{
"message": {
- "subject": "CIPP Alert: Alerts found starting at $((Get-Date).AddMinutes(-16))",
+ "subject": "CIPP Alert: Alerts found starting at $((Get-Date).AddMinutes(-10))",
"body": {
"contentType": "HTML",
"content": "You've setup your alert policies to be alerted whenever specific events happen. We've found some of these events in the log:
@@ -42,39 +40,36 @@ if ($Config.email -ne '' -and $null -ne $CurrentLog) {
"saveToSentItems": "false"
}
"@
- New-GraphPostRequest -uri 'https://graph.microsoft.com/v1.0/me/sendMail' -tenantid $env:TenantID -type POST -body ($JSONBody)
+ New-GraphPostRequest -uri 'https://graph.microsoft.com/v1.0/me/sendMail' -tenantid $env:TenantID -type POST -body ($JSONBody)
}
-
if ($Config.webhook -ne '' -and $null -ne $CurrentLog) {
- switch -wildcard ($config.Webhook) {
+ switch -wildcard ($config.Webhook) {
- '*webhook.office.com*' {
- $Log = $Currentlog | ConvertTo-Html -frag | Out-String
- $JSonBody = "{`"text`": `"You've setup your alert policies to be alerted whenever specific events happen. We've found some of these events in the log.
$Log`"}"
- Invoke-RestMethod -Uri $config.webhook -Method POST -ContentType 'Application/json' -Body $JSONBody
- }
+ '*webhook.office.com*' {
+ $Log = $Currentlog | ConvertTo-Html -frag | Out-String
+ $JSonBody = "{`"text`": `"You've setup your alert policies to be alerted whenever specific events happen. We've found some of these events in the log.
$Log`"}"
+ Invoke-RestMethod -Uri $config.webhook -Method POST -ContentType 'Application/json' -Body $JSONBody
+ }
- '*slack.com*' {
- $Log = $Currentlog | ForEach-Object {
- $JSonBody = @"
+ '*slack.com*' {
+ $Log = $Currentlog | ForEach-Object {
+ $JSonBody = @"
{"blocks":[{"type":"header","text":{"type":"plain_text","text":"New Alert from CIPP","emoji":true}},{"type":"section","fields":[{"type":"mrkdwn","text":"*DateTime:*\n$($_.DateTime)"},{"type":"mrkdwn","text":"*Tenant:*\n$($_.Tenant)"},{"type":"mrkdwn","text":"*API:*\n$($_.API)"},{"type":"mrkdwn","text":"*User:*\n$($_.User)."}]},{"type":"section","text":{"type":"mrkdwn","text":"*Message:*\n$($_.message)"}}]}
"@
- Invoke-RestMethod -Uri $config.webhook -Method POST -ContentType 'Application/json' -Body $JSONBody
- }
- }
+ Invoke-RestMethod -Uri $config.webhook -Method POST -ContentType 'Application/json' -Body $JSONBody
+ }
+ }
- '*discord.com*' {
- $Log = $Currentlog | ConvertTo-Html -frag | Out-String
- $JSonBody = "{`"content`": `"You've setup your alert policies to be alerted whenever specific events happen. We've found some of these events in the log. $Log`"}"
- Invoke-RestMethod -Uri $config.webhook -Method POST -ContentType 'Application/json' -Body $JSONBody
- }
+ '*discord.com*' {
+ $Log = $Currentlog | ConvertTo-Html -frag | Out-String
+ $JSonBody = "{`"content`": `"You've setup your alert policies to be alerted whenever specific events happen. We've found some of these events in the log. $Log`"}"
+ Invoke-RestMethod -Uri $config.webhook -Method POST -ContentType 'Application/json' -Body $JSONBody
}
+ }
}
-# Write an information log with the current time.
-Write-Host "PowerShell timer trigger function ran! TIME: $currentUTCtime"
\ No newline at end of file
diff --git a/Scheduler_GetQueue/run.ps1 b/Scheduler_GetQueue/run.ps1
index cd9b669eb872..1a5faecef714 100644
--- a/Scheduler_GetQueue/run.ps1
+++ b/Scheduler_GetQueue/run.ps1
@@ -6,17 +6,19 @@ $object = foreach ($Tenant in $tenants) {
$TypeFile = Get-Content "$($tenant)" | ConvertFrom-Json
if ($Typefile.Tenant -ne "AllTenants") {
[pscustomobject]@{
- Tenant = $Typefile.Tenant
- Type = $Typefile.Type
+ Tenant = $Typefile.Tenant
+ TenantID = $TypeFile.tenantId
+ Type = $Typefile.Type
}
}
else {
Write-Host "All tenants, doing them all"
get-tenants | ForEach-Object {
[pscustomobject]@{
- Tenant = $_.defaultDomainName
- Tag = "AllTenants"
- Type = $Typefile.Type
+ Tenant = $_.defaultDomainName
+ Tag = "AllTenants"
+ TenantID = $_.customerId
+ Type = $Typefile.Type
}
}
}
diff --git a/Scheduler_Orchestration/run.ps1 b/Scheduler_Orchestration/run.ps1
index 7c261c5eb953..420d9303c561 100644
--- a/Scheduler_Orchestration/run.ps1
+++ b/Scheduler_Orchestration/run.ps1
@@ -12,4 +12,4 @@ $ParallelTasks = foreach ($Item in $Batch) {
$Outputs = Wait-ActivityFunction -Task $ParallelTasks
Write-Host $Outputs
Remove-Item "Cache_Scheduler\CurrentlyRunning.txt" -Force
-Log-request -API "Scheduler" -tenant $tenant -message "Scheduler Ran." -sev Info
\ No newline at end of file
+Log-request -API "Scheduler" -tenant $tenant -message "Scheduler Ran." -sev Debug
\ No newline at end of file
diff --git a/Scheduler_Timer/function.json b/Scheduler_Timer/function.json
index 910cae1230a6..b8c54e695997 100644
--- a/Scheduler_Timer/function.json
+++ b/Scheduler_Timer/function.json
@@ -2,7 +2,7 @@
"bindings": [
{
"name": "Timer",
- "schedule": "0 */15 * * * *",
+ "schedule": "0 */10 * * * *",
"direction": "in",
"type": "timerTrigger"
},
@@ -12,4 +12,4 @@
"direction": "in"
}
]
-}
\ No newline at end of file
+}
diff --git a/Standards_PWdisplayAppInformationRequiredState/function.json b/Standards_PWdisplayAppInformationRequiredState/function.json
new file mode 100644
index 000000000000..2d4ea9094b24
--- /dev/null
+++ b/Standards_PWdisplayAppInformationRequiredState/function.json
@@ -0,0 +1,9 @@
+{
+ "bindings": [
+ {
+ "name": "tenant",
+ "direction": "in",
+ "type": "activityTrigger"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Standards_PWdisplayAppInformationRequiredState/run.ps1 b/Standards_PWdisplayAppInformationRequiredState/run.ps1
new file mode 100644
index 000000000000..53a27f6406bd
--- /dev/null
+++ b/Standards_PWdisplayAppInformationRequiredState/run.ps1
@@ -0,0 +1,13 @@
+param($tenant)
+
+try {
+ $body = @"
+ {"@odata.context":"https://graph.microsoft.com/beta/$metadata#authenticationMethodConfigurations/$entity","@odata.type":"#microsoft.graph.microsoftAuthenticatorAuthenticationMethodConfiguration","id":"MicrosoftAuthenticator","state":"enabled","includeTargets@odata.context":"https://graph.microsoft.com/beta/$metadata#policies/authenticationMethodsPolicy/authenticationMethodConfigurations('MicrosoftAuthenticator')/microsoft.graph.microsoftAuthenticatorAuthenticationMethodConfiguration/includeTargets","includeTargets":[{"targetType":"group","id":"all_users","isRegistrationRequired":false,"authenticationMode":"any","outlookMobileAllowedState":"default","displayAppInformationRequiredState":"enabled","numberMatchingRequiredState":"enabled"}]}
+"@
+ (New-GraphPostRequest -tenantid $tenant -Uri "https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy/authenticationMethodConfigurations/microsoftAuthenticator" -Type patch -Body $body -ContentType "application/json")
+
+ Log-request -API "Standards" -tenant $tenant -message "Enabled passwordless with Information and Number Matching." -sev Info
+}
+catch {
+ Log-request -API "Standards" -tenant $tenant -message "Failed to enable passwordless with Information and Number Matching. Error: $($_.exception.message)"
+}
\ No newline at end of file
diff --git a/Standards_PWnumberMatchingRequiredState/function.json b/Standards_PWnumberMatchingRequiredState/function.json
new file mode 100644
index 000000000000..2d4ea9094b24
--- /dev/null
+++ b/Standards_PWnumberMatchingRequiredState/function.json
@@ -0,0 +1,9 @@
+{
+ "bindings": [
+ {
+ "name": "tenant",
+ "direction": "in",
+ "type": "activityTrigger"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Standards_PWnumberMatchingRequiredState/run.ps1 b/Standards_PWnumberMatchingRequiredState/run.ps1
new file mode 100644
index 000000000000..5fe0c060aa0e
--- /dev/null
+++ b/Standards_PWnumberMatchingRequiredState/run.ps1
@@ -0,0 +1,13 @@
+param($tenant)
+
+try {
+ $body = @"
+ {"@odata.context":"https://graph.microsoft.com/beta/$metadata#authenticationMethodConfigurations/$entity","@odata.type":"#microsoft.graph.microsoftAuthenticatorAuthenticationMethodConfiguration","id":"MicrosoftAuthenticator","state":"enabled","includeTargets@odata.context":"https://graph.microsoft.com/beta/$metadata#policies/authenticationMethodsPolicy/authenticationMethodConfigurations('MicrosoftAuthenticator')/microsoft.graph.microsoftAuthenticatorAuthenticationMethodConfiguration/includeTargets","includeTargets":[{"targetType":"group","id":"all_users","isRegistrationRequired":false,"authenticationMode":"any","outlookMobileAllowedState":"default","displayAppInformationRequiredState":"default","numberMatchingRequiredState":"enabled"}]}
+"@
+ (New-GraphPostRequest -tenantid $tenant -Uri "https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy/authenticationMethodConfigurations/microsoftAuthenticator" -Type patch -Body $body -ContentType "application/json")
+
+ Log-request -API "Standards" -tenant $tenant -message "Enabled passwordless with Number Matching." -sev Info
+}
+catch {
+ Log-request -API "Standards" -tenant $tenant -message "Failed to enable passwordless with Number Matching. Error: $($_.exception.message)"
+}
\ No newline at end of file
diff --git a/Standards_TAP/function.json b/Standards_TAP/function.json
new file mode 100644
index 000000000000..2d4ea9094b24
--- /dev/null
+++ b/Standards_TAP/function.json
@@ -0,0 +1,9 @@
+{
+ "bindings": [
+ {
+ "name": "tenant",
+ "direction": "in",
+ "type": "activityTrigger"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Standards_TAP/run.ps1 b/Standards_TAP/run.ps1
new file mode 100644
index 000000000000..7f5404c264aa
--- /dev/null
+++ b/Standards_TAP/run.ps1
@@ -0,0 +1,27 @@
+param($tenant)
+
+try {
+ $MinimumLifetime = "60" #Minutes
+ $MaximumLifetime = "480" #minutes
+ $DefaultLifeTime = "60" #minutes
+ $DefaultLength = "8"
+ $body = @"
+ {"@odata.type":"#microsoft.graph.temporaryAccessPassAuthenticationMethodConfiguration",
+ "id":"TemporaryAccessPass",
+ "includeTargets":[{"id":"all_users",
+ "isRegistrationRequired":false,
+ "targetType":"group","displayName":"All users"}],
+ "defaultLength":$DefaultLength,
+ "defaultLifetimeInMinutes":$DefaultLifeTime,
+ "isUsableOnce":true,
+ "maximumLifetimeInMinutes":$MaximumLifetime,
+ "minimumLifetimeInMinutes":$MinimumLifetime,
+ "state":"enabled"}
+"@
+ (New-GraphPostRequest -tenantid $tenant -Uri "https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/TemporaryAccessPass" -Type patch -Body $body -ContentType "application/json")
+
+ Log-request -API "Standards" -tenant $tenant -message "Enabled Temporary Access Passwords." -sev Info
+}
+catch {
+ Log-request -API "Standards" -tenant $tenant -message "Failed to enable TAP. Error: $($_.exception.message)"
+}
\ No newline at end of file
diff --git a/UpdateTokens/run.ps1 b/UpdateTokens/run.ps1
index addd2f32a5f1..abdf5e5bd9c1 100644
--- a/UpdateTokens/run.ps1
+++ b/UpdateTokens/run.ps1
@@ -17,12 +17,11 @@ $KV = Get-AzKeyVault -SubscriptionID $Subscription -ResourceGroupName $ResourceG
if ($Refreshtoken) {
Set-AzKeyVaultSecret -VaultName $kv.vaultname -Name 'RefreshToken' -SecretValue (ConvertTo-SecureString -String $Refreshtoken -AsPlainText -Force)
-
}
else { log-request -message "Could not update refresh token. Will try again in 7 days." -sev "CRITICAL" }
if ($ExchangeRefreshtoken) {
Set-AzKeyVaultSecret -VaultName $kv.vaultname -Name 'ExchangeRefreshToken' -SecretValue (ConvertTo-SecureString -String $ExchangeRefreshtoken -AsPlainText -Force)
- log-request -message "System API: Updated Refresh token." -sev "info" -API "TokensUpdater"
+ log-request -message "System API: Updated Exchange Refresh token." -sev "info" -API "TokensUpdater"
}
else {
log-request -message "Could not update Exchange refresh token. Will try again in 7 days." -sev "CRITICAL" -API "TokensUpdater"
diff --git a/version_latest.txt b/version_latest.txt
index 0a182f2e3af7..afa2b3515e91 100644
--- a/version_latest.txt
+++ b/version_latest.txt
@@ -1 +1 @@
-1.7.2
\ No newline at end of file
+1.8.0
\ No newline at end of file