Skip to content

Commit 5183ad2

Browse files
authored
Merge pull request #315 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents c8e25b4 + 5d35bf9 commit 5183ad2

File tree

10 files changed

+400
-80
lines changed

10 files changed

+400
-80
lines changed

Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ function Push-DomainAnalyserTenant {
3030
'*.signature365.net'
3131
'*.myteamsconnect.io'
3232
'*.teams.dstny.com'
33+
'*.msteams.8x8.com'
34+
'*.ucconnect.co.uk'
3335
)
3436
$Domains = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/domains' -tenantid $Tenant.customerId | Where-Object { $_.isVerified -eq $true } | ForEach-Object {
3537
$Domain = $_

Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecAccessChecks.ps1

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using namespace System.Net
22

3-
Function Invoke-ExecAccessChecks {
3+
function Invoke-ExecAccessChecks {
44
<#
55
.FUNCTIONALITY
66
Entrypoint
@@ -50,16 +50,17 @@ Function Invoke-ExecAccessChecks {
5050
$Results = foreach ($Tenant in $Tenants) {
5151
$TenantCheck = $AccessChecks | Where-Object -Property RowKey -EQ $Tenant.customerId | Select-Object -Property Data
5252
$TenantResult = [PSCustomObject]@{
53-
TenantId = $Tenant.customerId
54-
TenantName = $Tenant.displayName
55-
DefaultDomainName = $Tenant.defaultDomainName
56-
GraphStatus = 'Not run yet'
57-
ExchangeStatus = 'Not run yet'
58-
GDAPRoles = ''
59-
MissingRoles = ''
60-
LastRun = ''
61-
GraphTest = ''
62-
ExchangeTest = ''
53+
TenantId = $Tenant.customerId
54+
TenantName = $Tenant.displayName
55+
DefaultDomainName = $Tenant.defaultDomainName
56+
GraphStatus = 'Not run yet'
57+
ExchangeStatus = 'Not run yet'
58+
GDAPRoles = ''
59+
MissingRoles = ''
60+
LastRun = ''
61+
GraphTest = ''
62+
ExchangeTest = ''
63+
OrgManagementRoles = @()
6364
}
6465
if ($TenantCheck) {
6566
$Data = @($TenantCheck.Data | ConvertFrom-Json -ErrorAction Stop)
@@ -70,6 +71,7 @@ Function Invoke-ExecAccessChecks {
7071
$TenantResult.LastRun = $Data.LastRun
7172
$TenantResult.GraphTest = $Data.GraphTest
7273
$TenantResult.ExchangeTest = $Data.ExchangeTest
74+
$TenantResult.OrgManagementRoles = $Data.OrgManagementRoles
7375
}
7476
$TenantResult
7577
}

Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecCreateTAP.ps1

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,23 @@ Function Invoke-ExecCreateTAP {
1717
# Interact with query parameters or the body of the request.
1818
$TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter
1919
$UserID = $Request.Query.ID ?? $Request.Body.ID
20+
$LifetimeInMinutes = $Request.Query.lifetimeInMinutes ?? $Request.Body.lifetimeInMinutes
21+
$IsUsableOnce = $Request.Query.isUsableOnce ?? $Request.Body.isUsableOnce
22+
$StartDateTime = $Request.Query.startDateTime ?? $Request.Body.startDateTime
2023

2124
try {
22-
$TAPResult = New-CIPPTAP -userid $UserID -TenantFilter $TenantFilter -APIName $APIName -Headers $Headers
25+
# Create parameter hashtable for splatting
26+
$TAPParams = @{
27+
UserID = $UserID
28+
TenantFilter = $TenantFilter
29+
APIName = $APIName
30+
Headers = $Headers
31+
LifetimeInMinutes = $LifetimeInMinutes
32+
IsUsableOnce = $IsUsableOnce
33+
StartDateTime = $StartDateTime
34+
}
35+
36+
$TAPResult = New-CIPPTAP @TAPParams
2337

2438
# Create results array with both TAP and UserID as separate items
2539
$Results = @(
@@ -33,7 +47,7 @@ Function Invoke-ExecCreateTAP {
3347

3448
$StatusCode = [HttpStatusCode]::OK
3549
} catch {
36-
$Results = Get-NormalizedError -message $_.Exception.Message
50+
$Results = $_.Exception.Message
3751
$StatusCode = [HttpStatusCode]::InternalServerError
3852
}
3953

Modules/CIPPCore/Public/New-CIPPTAP.ps1

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,75 @@
11
function New-CIPPTAP {
22
[CmdletBinding()]
33
param (
4-
$userid,
4+
$UserID,
55
$TenantFilter,
66
$APIName = 'Create TAP',
7-
$Headers
7+
$Headers,
8+
$LifetimeInMinutes,
9+
[bool]$IsUsableOnce,
10+
$StartDateTime
811
)
912

1013
try {
11-
$GraphRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($userid)/authentication/temporaryAccessPassMethods" -tenantid $TenantFilter -type POST -body '{}' -verbose
12-
Write-LogMessage -headers $Headers -API $APIName -message "Created Temporary Access Password (TAP) for $userid" -Sev 'Info' -tenant $TenantFilter
14+
# Build the request body based on provided parameters
15+
$RequestBody = @{}
16+
17+
if ($LifetimeInMinutes) {
18+
$RequestBody.lifetimeInMinutes = [int]$LifetimeInMinutes
19+
}
20+
21+
if ($null -ne $IsUsableOnce) {
22+
$RequestBody.isUsableOnce = $IsUsableOnce
23+
}
24+
25+
if ($StartDateTime) {
26+
# Convert Unix timestamp to DateTime if it's a number
27+
if ($StartDateTime -match '^\d+$') {
28+
$dateTime = [DateTimeOffset]::FromUnixTimeSeconds([int]$StartDateTime).DateTime
29+
$RequestBody.startDateTime = Get-Date $dateTime -Format 'o'
30+
} else {
31+
# If it's already a date string, format it properly
32+
$dateTime = Get-Date $StartDateTime
33+
$RequestBody.startDateTime = Get-Date $dateTime -Format 'o'
34+
}
35+
}
36+
37+
# Convert request body to JSON
38+
$BodyJson = if ($RequestBody) { $RequestBody | ConvertTo-Json } else { '{}' }
39+
$GraphRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserID)/authentication/temporaryAccessPassMethods" -tenantid $TenantFilter -type POST -body $BodyJson -verbose
40+
41+
# Build log message parts based on actual response values
42+
$logParts = [System.Collections.Generic.List[string]]::new()
43+
$logParts.Add("Lifetime: $($GraphRequest.lifetimeInMinutes) minutes")
44+
45+
$logParts.Add($GraphRequest.isUsableOnce ? 'one-time use' : 'multi-use')
46+
47+
$logParts.Add($StartDateTime ? "starts at $(Get-Date $GraphRequest.startDateTime -Format 'yyyy-MM-dd HH:mm:ss') UTC" : 'starts immediately')
48+
49+
# Create parameter string for logging
50+
$paramString = ' with ' + ($logParts -join ', ')
51+
52+
Write-LogMessage -headers $Headers -API $APIName -message "Created Temporary Access Password (TAP) for $UserID$paramString" -Sev 'Info' -tenant $TenantFilter
53+
54+
# Build result text with parameters
55+
$resultText = "The TAP for $UserID is $($GraphRequest.temporaryAccessPass) - This TAP is usable for the next $($GraphRequest.LifetimeInMinutes) minutes"
56+
$resultText += $GraphRequest.isUsableOnce ? ' (one-time use only)' : ''
57+
$resultText += $StartDateTime ? " starting at $(Get-Date $GraphRequest.startDateTime -Format 'yyyy-MM-dd HH:mm:ss') UTC" : ''
58+
1359
return @{
14-
resultText = "The TAP for $userid is $($GraphRequest.temporaryAccessPass) - This TAP is usable for the next $($GraphRequest.LifetimeInMinutes) minutes"
15-
userid = $userid
60+
resultText = $resultText
61+
userId = $UserID
1662
copyField = $GraphRequest.temporaryAccessPass
1763
temporaryAccessPass = $GraphRequest.temporaryAccessPass
1864
lifetimeInMinutes = $GraphRequest.LifetimeInMinutes
1965
startDateTime = $GraphRequest.startDateTime
66+
isUsableOnce = $GraphRequest.isUsableOnce
2067
state = 'success'
2168
}
2269

2370
} catch {
2471
$ErrorMessage = Get-CippException -Exception $_
25-
$Result = "Failed to create Temporary Access Password (TAP) for $($userid): $($ErrorMessage.NormalizedError)"
72+
$Result = "Failed to create Temporary Access Password (TAP) for $($UserID): $($ErrorMessage.NormalizedError)"
2673
Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage
2774
throw $Result
2875
}

Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ function Invoke-CIPPStandardAddDKIM {
7676
'*.signature365.net'
7777
'*.myteamsconnect.io'
7878
'*.teams.dstny.com'
79+
'*.msteams.8x8.com'
80+
'*.ucconnect.co.uk'
7981
)
8082

8183
$AllDomains = ($BatchResults | Where-Object { $_.DomainName }).DomainName | ForEach-Object {
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
function Invoke-CIPPStandardAddDMARCToMOERA {
2+
<#
3+
.FUNCTIONALITY
4+
Internal
5+
.COMPONENT
6+
(APIName) AddDMARCToMOERA
7+
.SYNOPSIS
8+
(Label) Enables DMARC on MOERA (onmicrosoft.com) domains
9+
.DESCRIPTION
10+
(Helptext) Note: requires 'Domain Name Administrator' GDAP role. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default value is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100%
11+
(DocsDescription) Note: requires 'Domain Name Administrator' GDAP role. Adds a DMARC record to MOERA (onmicrosoft.com) domains. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default record is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100%
12+
.NOTES
13+
CAT
14+
Global Standards
15+
TAG
16+
"CIS"
17+
"Security"
18+
"PhishingProtection"
19+
ADDEDCOMPONENT
20+
{"type":"autoComplete","multiple":false,"creatable":true,"required":false,"placeholder":"v=DMARC1; p=reject; (recommended)","label":"Value","name":"standards.AddDMARCToMOERA.RecordValue","options":[{"label":"v=DMARC1; p=reject; (recommended)","value":"v=DMARC1; p=reject;"}]}
21+
IMPACT
22+
Low Impact
23+
ADDEDDATE
24+
2025-06-16
25+
POWERSHELLEQUIVALENT
26+
Portal only
27+
RECOMMENDEDBY
28+
"CIS"
29+
"Microsoft"
30+
UPDATECOMMENTBLOCK
31+
Run the Tools\Update-StandardsComments.ps1 script to update this comment block
32+
.LINK
33+
https://docs.cipp.app/user-documentation/tenant/standards/list-standards
34+
#>
35+
36+
param($Tenant, $Settings)
37+
#$Rerun -Type Standard -Tenant $Tenant -API 'AddDMARCToMOERA' -Settings $Settings
38+
39+
$RecordModel = [PSCustomObject]@{
40+
HostName = '_dmarc'
41+
TtlValue = 3600
42+
Type = 'TXT'
43+
Value = $Settings.RecordValue.Value ?? "v=DMARC1; p=reject;"
44+
}
45+
46+
# Get all fallback domains (onmicrosoft.com domains) and check if the DMARC record is set correctly
47+
try {
48+
$Domains = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $Tenant -Uri 'https://admin.microsoft.com/admin/api/Domains/List' | Where-Object -Property Name -like "*.onmicrosoft.com"
49+
50+
$CurrentInfo = $Domains | ForEach-Object {
51+
# Get current DNS records that matches _dmarc hostname and TXT type
52+
$CurrentRecords = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $Tenant -Uri "https://admin.microsoft.com/admin/api/Domains/Records?domainName=$($_.Name)" | Select-Object -ExpandProperty DnsRecords | Where-Object { $_.HostName -eq $RecordModel.HostName -and $_.Type -eq $RecordModel.Type }
53+
54+
if ($CurrentRecords.count -eq 0) {
55+
#record not found, return a model with Match set to false
56+
[PSCustomObject]@{
57+
DomainName = $_.Name
58+
Match = $false
59+
CurrentRecord = $null
60+
}
61+
} else {
62+
foreach ($CurrentRecord in $CurrentRecords) {
63+
# Create variable matching the RecordModel used for comparison
64+
$CurrentRecordModel = [PSCustomObject]@{
65+
HostName = $CurrentRecord.HostName
66+
TtlValue = $CurrentRecord.TtlValue
67+
Type = $CurrentRecord.Type
68+
Value = $CurrentRecord.Value
69+
}
70+
71+
# Compare the current record with the expected record model
72+
if (!(Compare-Object -ReferenceObject $RecordModel -DifferenceObject $CurrentRecordModel -Property HostName, TtlValue, Type, Value)) {
73+
[PSCustomObject]@{
74+
DomainName = $_.Name
75+
Match = $true
76+
CurrentRecord = $CurrentRecord
77+
}
78+
} else {
79+
[PSCustomObject]@{
80+
DomainName = $_.Name
81+
Match = $false
82+
CurrentRecord = $CurrentRecord
83+
}
84+
}
85+
}
86+
}
87+
}
88+
# Check if match is true and there is only one DMARC record for the domain
89+
$StateIsCorrect = $false -notin $CurrentInfo.Match -and $CurrentInfo.Count -eq 1
90+
} catch {
91+
if ($_.Exception.Message -like '*403*') {
92+
$Message = "AddDMARCToMOERA: Insufficient permissions. Please ensure the tenant GDAP relationship includes the 'Domain Name Administrator' role: $(Get-NormalizedError -message $_.Exception.message)"
93+
}
94+
else {
95+
$Message = "Failed to get dns records for MOERA domains: $(Get-NormalizedError -message $_.Exception.message)"
96+
}
97+
Write-LogMessage -API 'Standards' -tenant $tenant -message $Message -sev Error
98+
throw $Message
99+
}
100+
101+
If ($Settings.remediate -eq $true) {
102+
if ($StateIsCorrect -eq $true) {
103+
Write-LogMessage -API 'Standards' -tenant $tenant -message 'DMARC record is already set for all MOERA (onmicrosoft.com) domains.' -sev Info
104+
}
105+
else {
106+
# Loop through each domain and set the DMARC record, existing misconfigured records and duplicates will be deleted
107+
foreach ($Domain in ($CurrentInfo | Sort-Object -Property DomainName -Unique)) {
108+
try {
109+
foreach ($Record in ($CurrentInfo | Where-Object -Property DomainName -eq $Domain.DomainName)) {
110+
if ($Record.CurrentRecord) {
111+
New-GraphPOSTRequest -tenantid $tenant -scope 'https://admin.microsoft.com/.default' -Uri "https://admin.microsoft.com/admin/api/Domains/Record?domainName=$($Domain.DomainName)" -Body ($Record.CurrentRecord | ConvertTo-Json -Compress) -AddedHeaders @{'x-http-method-override' = 'Delete'}
112+
Write-LogMessage -API 'Standards' -tenant $tenant -message "Deleted incorrect DMARC record for domain $($Domain.DomainName)" -sev Info
113+
}
114+
New-GraphPOSTRequest -tenantid $tenant -scope 'https://admin.microsoft.com/.default' -type "PUT" -Uri "https://admin.microsoft.com/admin/api/Domains/Record?domainName=$($Domain.DomainName)" -Body (@{RecordModel = $RecordModel} | ConvertTo-Json -Compress)
115+
Write-LogMessage -API 'Standards' -tenant $tenant -message "Set DMARC record for domain $($Domain.DomainName)" -sev Info
116+
}
117+
}
118+
catch {
119+
Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set DMARC record for domain $($Domain.DomainName): $(Get-NormalizedError -message $_.Exception.message)" -sev Error
120+
}
121+
}
122+
}
123+
}
124+
125+
if ($Settings.alert -eq $true) {
126+
if ($StateIsCorrect -eq $true) {
127+
Write-LogMessage -API 'Standards' -tenant $tenant -message 'DMARC record is already set for all MOERA (onmicrosoft.com) domains.' -sev Info
128+
} else {
129+
$UniqueDomains = ($CurrentInfo | Sort-Object -Property DomainName -Unique)
130+
$NotSetDomains = @($UniqueDomains | ForEach-Object {if ($_.Match -eq $false -or ($CurrentInfo | Where-Object -Property DomainName -eq $_.DomainName).Count -eq 1) { $_.DomainName } })
131+
$Message = "DMARC record is not set for $($NotSetDomains.count) of $($UniqueDomains.count) MOERA (onmicrosoft.com) domains."
132+
133+
Write-StandardsAlert -message $Message -object @{MissingDMARC = ($NotSetDomains -join ', ')} -tenant $tenant -standardName 'AddDMARCToMOERA' -standardId $Settings.standardId
134+
Write-LogMessage -API 'Standards' -tenant $tenant -message "$Message. Missing for: $($NotSetDomains -join ', ')" -sev Info
135+
}
136+
}
137+
138+
if ($Settings.report -eq $true) {
139+
set-CIPPStandardsCompareField -FieldName 'standards.AddDMARCToMOERA' -FieldValue $StateIsCorrect -TenantFilter $Tenant
140+
Add-CIPPBPAField -FieldName 'AddDMARCToMOERA' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant
141+
}
142+
}

Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ function Invoke-CIPPStandardDisableSelfServiceLicenses {
77
.SYNOPSIS
88
(Label) Disable Self Service Licensing
99
.DESCRIPTION
10-
(Helptext) This standard disables all self service licenses and enables all exclusions
11-
(DocsDescription) This standard disables all self service licenses and enables all exclusions
10+
(Helptext) Note: requires 'Billing Administrator' GDAP role. This standard disables all self service licenses and enables all exclusions
11+
(DocsDescription) Note: requires 'Billing Administrator' GDAP role. This standard disables all self service licenses and enables all exclusions
1212
.NOTES
1313
CAT
1414
Entra (AAD) Standards
@@ -31,15 +31,17 @@ function Invoke-CIPPStandardDisableSelfServiceLicenses {
3131
param($Tenant, $Settings)
3232
##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableSelfServiceLicenses'
3333

34-
# disable for now - MS enforced role requirement
35-
Write-LogMessage -API 'Standards' -tenant $tenant -message 'Self Service Licenses cannot be disabled' -sev Error
36-
return
37-
3834
try {
3935
$selfServiceItems = (New-GraphGETRequest -scope 'aeb86249-8ea3-49e2-900b-54cc8e308f85/.default' -uri 'https://licensing.m365.microsoft.com/v1.0/policies/AllowSelfServicePurchase/products' -tenantid $Tenant).items
4036
} catch {
41-
Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to retrieve self service products: $($_.Exception.Message)" -sev Error
42-
throw "Failed to retrieve self service products: $($_.Exception.Message)"
37+
if ($_.Exception.Message -like '*403*') {
38+
$Message = "Failed to retrieve self service products: Insufficient permissions. Please ensure the tenant GDAP relationship includes the 'Billing Administrator' role: $($_.Exception.Message)"
39+
}
40+
else {
41+
$Message = "Failed to retrieve self service products: $($_.Exception.Message)"
42+
}
43+
Write-LogMessage -API 'Standards' -tenant $tenant -message $Message -sev Error
44+
throw $Message
4345
}
4446

4547
if ($settings.remediate) {

0 commit comments

Comments
 (0)