Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions powershell/Maester.Format.ps1xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,23 @@
}
</ScriptBlock>
</ListItem>
<ListItem>
<ItemSelectionCondition>
<ScriptBlock>
$null -ne $_.SharePoint
</ScriptBlock>
</ItemSelectionCondition>
<Label>SharePoint Online</Label>
<ScriptBlock>
if ($_.SharePoint) {
"Connected`n" +
"URL: $($_.SharePoint.Url)`n" +
"Tenant: $($_.SharePoint.Tenant)`n"
} else {
''
}
</ScriptBlock>
</ListItem>
<ListItem>
<ItemSelectionCondition>
<ScriptBlock>
Expand Down
10 changes: 7 additions & 3 deletions powershell/Maester.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
'Get-MtAzureManagementGroup', 'Get-MailAuthenticationRecord', 'Get-MtAdminPortalUrl', 'Get-MtAuthenticationMethodPolicyConfig',
'Get-MtConditionalAccessPolicy', 'Get-MtExo', 'Get-MtGraphScope', 'Get-MtGroupMember',
'Get-MtExoThreatPolicyMalware',
'Get-MtHtmlReport', 'Import-MtMaesterResult', 'Merge-MtMaesterResult', 'Get-MtLicenseInformation', 'Get-MtRole', 'Get-MtRoleMember', 'Get-MtSafeMarkdown', 'Get-MtSession',
'Get-MtHtmlReport', 'Import-MtMaesterResult', 'Merge-MtMaesterResult', 'Get-MtLicenseInformation', 'Get-MtRole', 'Get-MtRoleMember', 'Get-MtSafeMarkdown', 'Get-MtSession', 'Get-MtSpo',
'Get-MtUser', 'Get-MtUserAuthenticationMethod', 'Get-MtUserAuthenticationMethodInfoByType',
'Install-MaesterTests',
'Invoke-Maester', 'Invoke-MtGraphRequest', 'Invoke-MtAzureRequest', 'Invoke-MtAzureResourceGraphRequest',
Expand Down Expand Up @@ -135,8 +135,12 @@
'Test-MtCisaPrivilegedPhishResistant', 'Test-MtCisaRequireActivationApproval', 'Test-MtCisaSafeLink',
'Test-MtCisaSafeLinkClickTracking', 'Test-MtCisaSafeLinkDownloadScan', 'Test-MtCisaSmtpAuthentication',
'Test-MtCisaSpamAction', 'Test-MtCisaSpamAlternative', 'Test-MtCisaSpamBypass', 'Test-MtCisaSpamFilter',
'Test-MtCisaSpfDirective', 'Test-MtCisaSpfRestriction', 'Test-MtCisaSpoSharing',
'Test-MtCisaSpoSharingAllowedDomain', 'Test-MtCisaUnmanagedRoleAssignment', 'Test-MtCisaWeakFactor',
'Test-MtCisaSpfDirective', 'Test-MtCisaSpfRestriction',
'Test-MtCisaSpoAnyoneLinkExpiration', 'Test-MtCisaSpoAnyoneLinkPermission',
'Test-MtCisaSpoDefaultSharingPermission', 'Test-MtCisaSpoDefaultSharingScope',
'Test-MtCisaSpoOneDriveSharing', 'Test-MtCisaSpoSharing',
'Test-MtCisaSpoSharingAllowedDomain', 'Test-MtCisaSpoVerificationCodeReauth',
'Test-MtCisaUnmanagedRoleAssignment', 'Test-MtCisaWeakFactor',
'Test-MtConditionalAccessWhatIf', 'Test-MtConnection', 'Test-MtDeviceComplianceSettings',
'Test-MtExoRejectDirectSend',
'Test-MtExoSetScl',
Expand Down
1 change: 1 addition & 0 deletions powershell/Maester.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ $__MtSession = @{
DataverseApiBase = $null # Resolved Dataverse OData API base URL (e.g. https://org123.api.crm.dynamics.com/api/data/v9.2)
DataverseResourceUrl = $null # Dataverse resource URL for token acquisition (e.g. https://org123.crm.dynamics.com)
DataverseEnvironmentId = $null # Environment identifier for display (e.g. org123.crm.dynamics.com)
SpoCache = @{} # Cache for SharePoint Online tenant settings retrieved via PnP
}
New-Variable -Name __MtSession -Value $__MtSession -Scope Script -Force

Expand Down
1 change: 1 addition & 0 deletions powershell/internal/Clear-ModuleVariable.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@
Clear-MtDnsCache
Clear-MtExoCache
$__MtSession.AIAgentInfo = $null
$__MtSession.SpoCache = @{}
# $__MtSession.Connections = @() # Do not clear connections as they are used to track the connection state. This module variable should only be set by Connect-Maester and Disconnect-Maester.
}
1 change: 1 addition & 0 deletions powershell/internal/Get-MtSkippedReason.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"NotConnectedSecurityCompliance" { "Not connected to Security & Compliance. See [Connecting to Security & Compliance](https://maester.dev/docs/connect-maester/#connect-to-azure-exchange-online-and-teams)"; break}
"NotConnectedTeams" { "Not connected to Teams. See [Connecting to Teams](https://maester.dev/docs/connect-maester/#connect-to-azure-exchange-online-and-teams)"; break}
"NotConnectedGraph" { "Not connected to Graph. See [Connect-Maester](https://maester.dev/docs/commands/Connect-Maester#examples)"; break}
"NotConnectedSharePoint" { "Not connected to SharePoint Online. See [Connecting to SharePoint Online](https://maester.dev/docs/connect-maester/#connect-to-sharepoint-online)"; break}
"NotDotGovDomain" { "This test is only for federal, executive branch, departments and agencies. To override use [Test-MtCisaDmarcAggregateCisa -Force](https://maester.dev/docs/commands/Test-MtCisaDmarcAggregateCisa)"; break}
"NotLicensedEntraIDP1" { "This test is for tenants that are licensed for Entra ID P1. See [Entra ID licensing](https://learn.microsoft.com/entra/fundamentals/licensing)"; break}
"NotLicensedEntraIDP2" { "This test is for tenants that are licensed for Entra ID P2. See [Entra ID licensing](https://learn.microsoft.com/entra/fundamentals/licensing)"; break}
Expand Down
51 changes: 48 additions & 3 deletions powershell/public/Connect-Maester.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,18 @@
[ValidateSet('TeamsChina', 'TeamsGCCH', 'TeamsDOD')]
[string]$TeamsEnvironmentName = $null, #ToValidate: Don't use this parameter, this is the default.

# The services to connect to such as Azure, Dataverse (for Copilot Studio tests), and EXO. Default is Graph.
[ValidateSet('All', 'Azure', 'Dataverse', 'ExchangeOnline', 'Graph', 'SecurityCompliance', 'Teams')]
# The services to connect to such as Azure, Dataverse (for Copilot Studio tests), EXO, and SharePoint Online. Default is Graph.
[ValidateSet('All', 'Azure', 'Dataverse', 'ExchangeOnline', 'Graph', 'SecurityCompliance', 'SharePointOnline', 'Teams')]
[string[]]$Service = 'Graph',

# The Tenant ID to connect to, if not specified the sign-in user's default tenant is used.
[string]$TenantId,

# The Client ID of the app to connect to for Graph. If not specified, the default Graph PowerShell CLI enterprise app will be used. Reference on how to create an enterprise app: https://learn.microsoft.com/en-us/powershell/microsoftgraph/authentication-commands?view=graph-powershell-1.0#use-delegated-access-with-a-custom-application-for-microsoft-graph-powershell
[string]$GraphClientId
[string]$GraphClientId,

# The Client ID of the PnP Entra ID app for SharePoint Online. Required when Service includes SharePointOnline. Create the app using Register-PnPEntraIDAppForInteractiveLogin.
[string]$SharePointClientId
)

$__MtSession.Connections = $Service
Expand Down Expand Up @@ -353,4 +356,46 @@
}
} # end switch OrderedImport

# SharePoint Online via PnP — must run AFTER Graph to avoid Microsoft.Graph.Core DLL conflict
if ($Service -contains 'SharePointOnline' -or $Service -contains 'All') {
Write-Verbose 'Connecting to SharePoint Online via PnP'

if (-not $SharePointClientId) {
Write-Host "`nSharePointOnline requires the -SharePointClientId parameter. Create a PnP app registration using Register-PnPEntraIDAppForInteractiveLogin.`nFor more information see https://maester.dev/docs/sections/create-entra-app" -ForegroundColor Red
} else {
try {
# Resolve the SharePoint admin URL from the tenant's initial domain
$domains = Invoke-MtGraphRequest -RelativeUri "domains" -ApiVersion "v1.0"
$initialDomain = ($domains | Where-Object { $_.isInitial -eq $true }).id
$tenantPrefix = ($initialDomain -split '\.')[0]
$spoAdminUrl = "https://$tenantPrefix-admin.sharepoint.com"
Comment on lines +359 to +371

Comment on lines +371 to +372
Write-Verbose "Resolved SharePoint admin URL: $spoAdminUrl"

Import-Module PnP.PowerShell -ErrorAction Stop

$pnpParams = @{
Url = $spoAdminUrl
ClientId = $SharePointClientId
}

if ($UseDeviceCode) {
$pnpParams['DeviceLogin'] = $true
}

if ($TenantId) {
$pnpParams['Tenant'] = $TenantId
}

Connect-PnPOnline @pnpParams

} catch [Management.Automation.CommandNotFoundException] {
Write-Host "`nThe PnP.PowerShell module is not installed. Please install the module using the following command.`nFor more information see https://pnp.github.io/powershell/articles/installation.html" -ForegroundColor Red
Write-Host "`nInstall-Module PnP.PowerShell -Scope CurrentUser`n" -ForegroundColor Yellow
} catch {
Comment on lines +375 to +395
Write-Host "`nFailed to connect to SharePoint Online: $($_.Exception.Message)" -ForegroundColor Red
}
}
}

} # end function Connect-Maester
10 changes: 10 additions & 0 deletions powershell/public/Disconnect-Maester.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,14 @@
Write-Verbose -Message "Disconnecting from Microsoft Teams."
Disconnect-MicrosoftTeams
}

if($__MtSession.Connections -contains "SharePointOnline" -or $__MtSession.Connections -contains "All"){
Write-Verbose -Message "Disconnecting from SharePoint Online (PnP)."
try {
Disconnect-PnPOnline -ErrorAction Stop
} catch {
Write-Verbose "Disconnect-PnPOnline encountered an error: $($_.Exception.Message)"
}
$__MtSession.SpoCache = @{}
}
}
45 changes: 45 additions & 0 deletions powershell/public/cisa/spo/Get-MtSpo.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
function Get-MtSpo {
<#
.SYNOPSIS
Retrieves SharePoint Online tenant settings via PnP with session caching.

.DESCRIPTION
Returns the full SPO tenant configuration from Get-PnPTenant.
Results are cached in $__MtSession.SpoCache for the duration of the session.
Use -ClearCache to force a fresh retrieval.

.EXAMPLE
Get-MtSpo

Returns the cached (or freshly retrieved) SPO tenant settings.

.EXAMPLE
Get-MtSpo -ClearCache

Clears the cached settings and retrieves fresh data from Get-PnPTenant.

.LINK
https://maester.dev/docs/commands/Get-MtSpo
#>
[CmdletBinding()]
param(
# Clear the cached SPO tenant settings and retrieve fresh data.
[switch]$ClearCache
)

if ($ClearCache) {
$__MtSession.SpoCache = @{}
Write-Verbose "SPO cache cleared."
}

if ($null -eq $__MtSession.SpoCache.SpoTenant) {
Write-Verbose "SPO tenant settings not in cache, requesting."
$response = Get-PnPTenant -ErrorAction Stop
$__MtSession.SpoCache.SpoTenant = $response
} else {
Write-Verbose "SPO tenant settings in cache."
$response = $__MtSession.SpoCache.SpoTenant
}

return $response
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Expiration days for Anyone links SHALL be set to 30 days or less.

Rationale: Anyone links that do not expire or have excessively long expiration periods pose a persistent risk of unauthorized access. Setting an expiration of 30 days or less ensures that shared content access is time-limited.

#### Remediation action:

1. Sign in to the [SharePoint admin center](https://go.microsoft.com/fwlink/?linkid=2185219).
2. Select Policies > Sharing.
3. Under Advanced settings for Anyone links, check **These links must expire within this many days** and set to **30** days or less.
4. Select Save.

#### Related links

* [CISA 3 Anyone Link Expiration - MS.SHAREPOINT.3.1v1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/sharepoint.md#mssharepoint31v1)
* [CISA ScubaGear Rego Reference](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/Rego/SharepointConfig.rego)

<!--- Results --->
%TestResult%
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
function Test-MtCisaSpoAnyoneLinkExpiration {
<#
.SYNOPSIS
Checks state of Anyone link expiration for SharePoint Online

.DESCRIPTION
An expiration date SHOULD be set for Anyone links.

.EXAMPLE
Test-MtCisaSpoAnyoneLinkExpiration

Returns true if Anyone links have an expiration set

.LINK
https://maester.dev/docs/commands/Test-MtCisaSpoAnyoneLinkExpiration
#>
[CmdletBinding()]
[OutputType([bool])]
param()

if (!(Test-MtConnection SharePoint)) {
Add-MtTestResultDetail -SkippedBecause NotConnectedSharePoint
return $null
}

try {
$spoTenant = Get-MtSpo

if ($spoTenant.SharingCapability -ne "ExternalUserAndGuestSharing") {
Add-MtTestResultDetail -SkippedBecause Custom -SkippedCustomReason "Anyone links are not enabled. This test only applies when sharing is set to Anyone."
return $null
}

# RequireAnonymousLinksExpireInDays: 0 = no expiration, >0 = days until expiry
# CISA requires >= 1 AND <= 30 days
$days = $spoTenant.RequireAnonymousLinksExpireInDays
$testResult = $days -ge 1 -and $days -le 30

if ($testResult) {
$testResultMarkdown = "Well done. Anyone links expire after $days day(s)."
} elseif ($days -eq 0) {
$testResultMarkdown = "Anyone links do not have an expiration date set."
} else {
$testResultMarkdown = "Anyone links expiration is set to $days day(s), which exceeds the 30-day maximum."
}

Add-MtTestResultDetail -Result $testResultMarkdown

return $testResult
} catch {
Add-MtTestResultDetail -SkippedBecause Error -SkippedError $_
return $null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Allowable file and folder permissions for Anyone links SHALL be set to View only.

Rationale: Allowing edit permissions on Anyone links increases the risk of unauthorized modifications to shared content. Restricting to View only limits the potential impact of anonymous access.

#### Remediation action:

1. Sign in to the [SharePoint admin center](https://go.microsoft.com/fwlink/?linkid=2185219).
2. Select Policies > Sharing.
3. Under Advanced settings for Anyone links, set the file and folder permissions to **View**.
4. Select Save.

#### Related links

* [CISA 3 Anyone Link Permission - MS.SHAREPOINT.3.2v1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/sharepoint.md#mssharepoint32v1)
* [CISA ScubaGear Rego Reference](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/Rego/SharepointConfig.rego)

<!--- Results --->
%TestResult%
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
function Test-MtCisaSpoAnyoneLinkPermission {
<#
.SYNOPSIS
Checks state of Anyone link permissions for SharePoint Online

.DESCRIPTION
Anyone link permissions SHOULD be limited to View only.

.EXAMPLE
Test-MtCisaSpoAnyoneLinkPermission

Returns true if Anyone links are limited to View

.LINK
https://maester.dev/docs/commands/Test-MtCisaSpoAnyoneLinkPermission
#>
[CmdletBinding()]
[OutputType([bool])]
param()

if (!(Test-MtConnection SharePoint)) {
Add-MtTestResultDetail -SkippedBecause NotConnectedSharePoint
return $null
}

try {
$spoTenant = Get-MtSpo

if ($spoTenant.SharingCapability -ne "ExternalUserAndGuestSharing") {
Add-MtTestResultDetail -SkippedBecause Custom -SkippedCustomReason "Anyone links are not enabled. This test only applies when sharing is set to Anyone."
return $null
}

# FileAnonymousLinkType: 1 = View, 2 = Edit
# FolderAnonymousLinkType: 1 = View, 2 = Edit
$testResult = $spoTenant.FileAnonymousLinkType -eq 1 -and $spoTenant.FolderAnonymousLinkType -eq 1

if ($testResult) {
$testResultMarkdown = "Well done. Anyone link permissions are limited to View only."
} else {
$testResultMarkdown = "Anyone link permissions are not limited to View only.`n`n* File Anyone link type: ``$($spoTenant.FileAnonymousLinkType)`` `n* Folder Anyone link type: ``$($spoTenant.FolderAnonymousLinkType)``"
}

Add-MtTestResultDetail -Result $testResultMarkdown

return $testResult
} catch {
Add-MtTestResultDetail -SkippedBecause Error -SkippedError $_
return $null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
File and folder default sharing permissions SHALL be set to View only.

Rationale: Setting the default permission to View reduces the risk of accidental editing of shared documents by external parties.

#### Remediation action:

1. Sign in to the [SharePoint admin center](https://go.microsoft.com/fwlink/?linkid=2185219).
2. Select Policies > Sharing.
3. Under File and folder links, set the default link permission to **View**.
4. Select Save.

#### Related links

* [CISA 2 Default Sharing Permission - MS.SHAREPOINT.2.2v1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/sharepoint.md#mssharepoint22v1)
* [CISA ScubaGear Rego Reference](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/Rego/SharepointConfig.rego)

<!--- Results --->
%TestResult%
Loading