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