Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
be0b6e0129 | ||
|
642cdfe2ab | ||
|
a8b76c7e16 | ||
|
fbf40fa98e | ||
|
f409e8a5f1 | ||
|
c341279531 | ||
|
6a8438bbe8 | ||
|
87c635210d | ||
|
07592569b4 |
15
CHANGELOG.md
15
CHANGELOG.md
@@ -4,6 +4,20 @@ The format is based on and uses the types of changes according to [Keep a Change
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Get-SPOSite command to return all but voided output for no code runs (Ex: PowerAutomate)
|
||||||
|
|
||||||
|
## [0.1.27] - 2025-01-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added additional error handling to connect function to identify problematic steps when they occur.
|
||||||
|
- Added new method of verifying spo tenant for Connect-SPOService branch of connect function.
|
||||||
|
- Added method to avoid "assembly already loaded" error in PNP Powershell function on first run, subsequent runs in the same session will still throw the error.
|
||||||
|
|
||||||
|
## [0.1.26] - 2024-08-04
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added `New-M365SecurityAuditAuthObject` function to create a new authentication object for the security audit for app-based authentication.
|
- Added `New-M365SecurityAuditAuthObject` function to create a new authentication object for the security audit for app-based authentication.
|
||||||
@@ -19,7 +33,6 @@ The format is based on and uses the types of changes according to [Keep a Change
|
|||||||
|
|
||||||
- Fixed test 1.3.1 as notification window for password expiration is no longer required.
|
- Fixed test 1.3.1 as notification window for password expiration is no longer required.
|
||||||
|
|
||||||
|
|
||||||
## [0.1.24] - 2024-07-07
|
## [0.1.24] - 2024-07-07
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
# M365FoundationsCISReport Module
|
# M365FoundationsCISReport Module
|
||||||
[](https://github.com/CriticalSolutionsNetwork/M365FoundationsCISReport/actions/workflows/powershell.yml)
|
[](https://github.com/CriticalSolutionsNetwork/M365FoundationsCISReport/actions/workflows/powershell.yml)
|
||||||
|
[](https://github.com/CriticalSolutionsNetwork/M365FoundationsCISReport/actions/workflows/pages/pages-build-deployment)
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This PowerShell module is based on CIS benchmarks and is distributed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. This means:
|
This PowerShell module is based on CIS benchmarks and is distributed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. This means:
|
||||||
@@ -76,7 +77,8 @@ Sync-CISExcelAndCsvData -ExcelPath "path\to\excel.xlsx" -CsvPath "path\to\data.c
|
|||||||
Grant-M365SecurityAuditConsent -UserPrincipalNameForConsent 'user@example.com'
|
Grant-M365SecurityAuditConsent -UserPrincipalNameForConsent 'user@example.com'
|
||||||
|
|
||||||
# Example 8: (PowerShell 7.x Only) Creating a new authentication object for the security audit for app-based authentication.
|
# Example 8: (PowerShell 7.x Only) Creating a new authentication object for the security audit for app-based authentication.
|
||||||
$authParams = New-M365SecurityAuditAuthObject -ClientCertThumbPrint "ABCDEF1234567890ABCDEF1234567890ABCDEF12" `
|
$authParams = New-M365SecurityAuditAuthObject `
|
||||||
|
-ClientCertThumbPrint "ABCDEF1234567890ABCDEF1234567890ABCDEF12" `
|
||||||
-ClientId "12345678-1234-1234-1234-123456789012" `
|
-ClientId "12345678-1234-1234-1234-123456789012" `
|
||||||
-TenantId "12345678-1234-1234-1234-123456789012" `
|
-TenantId "12345678-1234-1234-1234-123456789012" `
|
||||||
-OnMicrosoftUrl "yourcompany.onmicrosoft.com" `
|
-OnMicrosoftUrl "yourcompany.onmicrosoft.com" `
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
# M365FoundationsCISReport Module
|
# M365FoundationsCISReport Module
|
||||||
[](https://github.com/CriticalSolutionsNetwork/M365FoundationsCISReport/actions/workflows/powershell.yml)
|
[](https://github.com/CriticalSolutionsNetwork/M365FoundationsCISReport/actions/workflows/powershell.yml)
|
||||||
|
[](https://github.com/CriticalSolutionsNetwork/M365FoundationsCISReport/actions/workflows/pages/pages-build-deployment)
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This PowerShell module is based on CIS benchmarks and is distributed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. This means:
|
This PowerShell module is based on CIS benchmarks and is distributed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. This means:
|
||||||
@@ -76,7 +77,8 @@ Sync-CISExcelAndCsvData -ExcelPath "path\to\excel.xlsx" -CsvPath "path\to\data.c
|
|||||||
Grant-M365SecurityAuditConsent -UserPrincipalNameForConsent 'user@example.com'
|
Grant-M365SecurityAuditConsent -UserPrincipalNameForConsent 'user@example.com'
|
||||||
|
|
||||||
# Example 8: (PowerShell 7.x Only) Creating a new authentication object for the security audit for app-based authentication.
|
# Example 8: (PowerShell 7.x Only) Creating a new authentication object for the security audit for app-based authentication.
|
||||||
$authParams = New-M365SecurityAuditAuthObject -ClientCertThumbPrint "ABCDEF1234567890ABCDEF1234567890ABCDEF12" `
|
$authParams = New-M365SecurityAuditAuthObject `
|
||||||
|
-ClientCertThumbPrint "ABCDEF1234567890ABCDEF1234567890ABCDEF12" `
|
||||||
-ClientId "12345678-1234-1234-1234-123456789012" `
|
-ClientId "12345678-1234-1234-1234-123456789012" `
|
||||||
-TenantId "12345678-1234-1234-1234-123456789012" `
|
-TenantId "12345678-1234-1234-1234-123456789012" `
|
||||||
-OnMicrosoftUrl "yourcompany.onmicrosoft.com" `
|
-OnMicrosoftUrl "yourcompany.onmicrosoft.com" `
|
||||||
|
@@ -5,10 +5,10 @@ Import-Module .\output\module\M365FoundationsCISReport\*\*.psd1
|
|||||||
|
|
||||||
|
|
||||||
<#
|
<#
|
||||||
$ver = "v0.1.26"
|
$ver = "v0.1.28"
|
||||||
git checkout main
|
git checkout main
|
||||||
git pull origin main
|
git pull origin main
|
||||||
git tag -a $ver -m "Release version $ver refactor Update"
|
git tag -a $ver -m "Release version $ver bugfix Update"
|
||||||
git push origin $ver
|
git push origin $ver
|
||||||
"Fix: PR #37"
|
"Fix: PR #37"
|
||||||
git push origin $ver
|
git push origin $ver
|
||||||
@@ -53,8 +53,8 @@ Register-SecretVault -Name ModuleBuildCreds -ModuleName `
|
|||||||
"SecretManagement.JustinGrote.CredMan" -ErrorAction Stop
|
"SecretManagement.JustinGrote.CredMan" -ErrorAction Stop
|
||||||
|
|
||||||
|
|
||||||
Set-Secret -Name "GalleryApiToken" -Vault ModuleBuildCreds
|
#Set-Secret -Name "GalleryApiToken" -Vault ModuleBuildCreds
|
||||||
Set-Secret -Name "GitHubToken" -Vault ModuleBuildCreds
|
#Set-Secret -Name "GitHubToken" -Vault ModuleBuildCreds
|
||||||
|
|
||||||
|
|
||||||
$GalleryApiToken = Get-Secret -Name "GalleryApiToken" -Vault ModuleBuildCreds -AsPlainText
|
$GalleryApiToken = Get-Secret -Name "GalleryApiToken" -Vault ModuleBuildCreds -AsPlainText
|
||||||
|
@@ -20,6 +20,17 @@ function Assert-ModuleAvailability {
|
|||||||
else {
|
else {
|
||||||
Write-Verbose "$ModuleName module is already at required version or newer."
|
Write-Verbose "$ModuleName module is already at required version or newer."
|
||||||
}
|
}
|
||||||
|
if ($ModuleName -eq "Microsoft.Graph") {
|
||||||
|
# "Preloading Microsoft.Graph assembly to prevent type-loading issues..."
|
||||||
|
Write-Verbose "Preloading Microsoft.Graph assembly to prevent type-loading issues..."
|
||||||
|
try {
|
||||||
|
# Run a harmless cmdlet to preload the assembly
|
||||||
|
Get-MgGroup -Top 1 -ErrorAction SilentlyContinue | Out-Null
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Verbose "Could not preload Microsoft.Graph assembly. Error: $_"
|
||||||
|
}
|
||||||
|
}
|
||||||
if ($SubModules.Count -gt 0) {
|
if ($SubModules.Count -gt 0) {
|
||||||
foreach ($subModule in $SubModules) {
|
foreach ($subModule in $SubModules) {
|
||||||
Write-Verbose "Importing submodule $ModuleName.$subModule..."
|
Write-Verbose "Importing submodule $ModuleName.$subModule..."
|
||||||
@@ -30,11 +41,9 @@ function Assert-ModuleAvailability {
|
|||||||
Write-Verbose "Importing module $ModuleName..."
|
Write-Verbose "Importing module $ModuleName..."
|
||||||
Import-Module -Name $ModuleName -RequiredVersion $RequiredVersion -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
|
Import-Module -Name $ModuleName -RequiredVersion $RequiredVersion -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
throw "Assert-ModuleAvailability:`n$_"
|
throw "Assert-ModuleAvailability:`n$_"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -2,135 +2,144 @@ function Connect-M365Suite {
|
|||||||
[OutputType([void])]
|
[OutputType([void])]
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param (
|
param (
|
||||||
[Parameter(
|
[Parameter(Mandatory = $false)]
|
||||||
Mandatory = $false
|
|
||||||
)]
|
|
||||||
[string]$TenantAdminUrl,
|
[string]$TenantAdminUrl,
|
||||||
[Parameter(
|
|
||||||
Mandatory = $false
|
[Parameter(Mandatory = $false)]
|
||||||
)]
|
[CISAuthenticationParameters]$AuthParams,
|
||||||
[CISAuthenticationParameters]$AuthParams, # Custom authentication parameters
|
|
||||||
[Parameter(
|
[Parameter(Mandatory)]
|
||||||
Mandatory
|
|
||||||
)]
|
|
||||||
[string[]]$RequiredConnections,
|
[string[]]$RequiredConnections,
|
||||||
[Parameter(
|
|
||||||
Mandatory = $false
|
[Parameter(Mandatory = $false)]
|
||||||
)]
|
|
||||||
[switch]$SkipConfirmation
|
[switch]$SkipConfirmation
|
||||||
)
|
)
|
||||||
if (!$SkipConfirmation) {
|
|
||||||
$VerbosePreference = "Continue"
|
$VerbosePreference = if ($SkipConfirmation) { 'SilentlyContinue' } else { 'Continue' }
|
||||||
}
|
|
||||||
else {
|
|
||||||
$VerbosePreference = "SilentlyContinue"
|
|
||||||
}
|
|
||||||
$tenantInfo = @()
|
$tenantInfo = @()
|
||||||
$connectedServices = @()
|
$connectedServices = @()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($RequiredConnections -contains "Microsoft Graph" -or $RequiredConnections -contains "EXO | Microsoft Graph") {
|
if ($RequiredConnections -contains 'Microsoft Graph' -or $RequiredConnections -contains 'EXO | Microsoft Graph') {
|
||||||
Write-Verbose "Connecting to Microsoft Graph"
|
try {
|
||||||
|
Write-Verbose 'Connecting to Microsoft Graph...'
|
||||||
if ($AuthParams) {
|
if ($AuthParams) {
|
||||||
# Use application-based authentication
|
|
||||||
Connect-MgGraph -CertificateThumbprint $AuthParams.ClientCertThumbPrint -AppId $AuthParams.ClientId -TenantId $AuthParams.TenantId -NoWelcome | Out-Null
|
Connect-MgGraph -CertificateThumbprint $AuthParams.ClientCertThumbPrint -AppId $AuthParams.ClientId -TenantId $AuthParams.TenantId -NoWelcome | Out-Null
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
# Use interactive authentication with scopes
|
Connect-MgGraph -Scopes 'Directory.Read.All', 'Domain.Read.All', 'Policy.Read.All', 'Organization.Read.All' -NoWelcome | Out-Null
|
||||||
Connect-MgGraph -Scopes "Directory.Read.All", "Domain.Read.All", "Policy.Read.All", "Organization.Read.All" -NoWelcome | Out-Null
|
|
||||||
}
|
}
|
||||||
$graphOrgDetails = Get-MgOrganization
|
$graphOrgDetails = Get-MgOrganization
|
||||||
$tenantInfo += [PSCustomObject]@{
|
$tenantInfo += [PSCustomObject]@{
|
||||||
Service = "Microsoft Graph"
|
Service = 'Microsoft Graph'
|
||||||
TenantName = $graphOrgDetails.DisplayName
|
TenantName = $graphOrgDetails.DisplayName
|
||||||
TenantID = $graphOrgDetails.Id
|
TenantID = $graphOrgDetails.Id
|
||||||
}
|
}
|
||||||
$connectedServices += "Microsoft Graph"
|
$connectedServices += 'Microsoft Graph'
|
||||||
Write-Verbose "Successfully connected to Microsoft Graph.`n"
|
Write-Verbose 'Successfully connected to Microsoft Graph.'
|
||||||
}
|
}
|
||||||
if ($RequiredConnections -contains "EXO" -or $RequiredConnections -contains "AzureAD | EXO" -or $RequiredConnections -contains "Microsoft Teams | EXO" -or $RequiredConnections -contains "EXO | Microsoft Graph") {
|
catch {
|
||||||
Write-Verbose "Connecting to Exchange Online..."
|
throw "Failed to connect to Microsoft Graph: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($RequiredConnections -contains 'EXO' -or $RequiredConnections -contains 'AzureAD | EXO' -or $RequiredConnections -contains 'Microsoft Teams | EXO' -or $RequiredConnections -contains 'EXO | Microsoft Graph') {
|
||||||
|
try {
|
||||||
|
Write-Verbose 'Connecting to Exchange Online...'
|
||||||
if ($AuthParams) {
|
if ($AuthParams) {
|
||||||
# Use application-based authentication
|
|
||||||
Connect-ExchangeOnline -AppId $AuthParams.ClientId -CertificateThumbprint $AuthParams.ClientCertThumbPrint -Organization $AuthParams.OnMicrosoftUrl -ShowBanner:$false | Out-Null
|
Connect-ExchangeOnline -AppId $AuthParams.ClientId -CertificateThumbprint $AuthParams.ClientCertThumbPrint -Organization $AuthParams.OnMicrosoftUrl -ShowBanner:$false | Out-Null
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
# Use interactive authentication
|
|
||||||
Connect-ExchangeOnline -ShowBanner:$false | Out-Null
|
Connect-ExchangeOnline -ShowBanner:$false | Out-Null
|
||||||
}
|
}
|
||||||
$exoTenant = (Get-OrganizationConfig).Identity
|
$exoTenant = (Get-OrganizationConfig).Identity
|
||||||
$tenantInfo += [PSCustomObject]@{
|
$tenantInfo += [PSCustomObject]@{
|
||||||
Service = "Exchange Online"
|
Service = 'Exchange Online'
|
||||||
TenantName = $exoTenant
|
TenantName = $exoTenant
|
||||||
TenantID = "N/A"
|
TenantID = 'N/A'
|
||||||
}
|
}
|
||||||
$connectedServices += "EXO"
|
$connectedServices += 'EXO'
|
||||||
Write-Verbose "Successfully connected to Exchange Online.`n"
|
Write-Verbose 'Successfully connected to Exchange Online.'
|
||||||
}
|
}
|
||||||
if ($RequiredConnections -contains "SPO") {
|
catch {
|
||||||
Write-Verbose "Connecting to SharePoint Online..."
|
throw "Failed to connect to Exchange Online: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($RequiredConnections -contains 'SPO') {
|
||||||
|
try {
|
||||||
|
Write-Verbose 'Connecting to SharePoint Online...'
|
||||||
if ($AuthParams) {
|
if ($AuthParams) {
|
||||||
# Use application-based authentication
|
|
||||||
Connect-PnPOnline -Url $AuthParams.SpAdminUrl -ClientId $AuthParams.ClientId -Tenant $AuthParams.OnMicrosoftUrl -Thumbprint $AuthParams.ClientCertThumbPrint | Out-Null
|
Connect-PnPOnline -Url $AuthParams.SpAdminUrl -ClientId $AuthParams.ClientId -Tenant $AuthParams.OnMicrosoftUrl -Thumbprint $AuthParams.ClientCertThumbPrint | Out-Null
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
# Use interactive authentication
|
|
||||||
Connect-SPOService -Url $TenantAdminUrl | Out-Null
|
Connect-SPOService -Url $TenantAdminUrl | Out-Null
|
||||||
}
|
}
|
||||||
# Assuming that Get-SPOCrossTenantHostUrl and Get-UrlLine are valid commands in your context
|
$tenantName = if ($AuthParams) {
|
||||||
if ($AuthParams) {
|
(Get-PnPSite).Url
|
||||||
$spoContext = Get-PnPSite
|
|
||||||
$tenantName = $spoContext.Url
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$spoContext = Get-SPOCrossTenantHostUrl
|
# Supress output from Get-SPOSite for powerautomate to avoid errors
|
||||||
$tenantName = Get-UrlLine -Output $spoContext
|
[void]($sites = Get-SPOSite -Limit All)
|
||||||
|
# Get the URL from the first site collection
|
||||||
|
$url = $sites[0].Url
|
||||||
|
# Use regex to extract the base URL up to the .com portion
|
||||||
|
$baseUrl = [regex]::Match($url, 'https://[^/]+.com').Value
|
||||||
|
# Output the base URL
|
||||||
|
$baseUrl
|
||||||
}
|
}
|
||||||
$tenantInfo += [PSCustomObject]@{
|
$tenantInfo += [PSCustomObject]@{
|
||||||
Service = "SharePoint Online"
|
Service = 'SharePoint Online'
|
||||||
TenantName = $tenantName
|
TenantName = $tenantName
|
||||||
}
|
}
|
||||||
$connectedServices += "SPO"
|
$connectedServices += 'SPO'
|
||||||
Write-Verbose "Successfully connected to SharePoint Online.`n"
|
Write-Verbose 'Successfully connected to SharePoint Online.'
|
||||||
}
|
}
|
||||||
if ($RequiredConnections -contains "Microsoft Teams" -or $RequiredConnections -contains "Microsoft Teams | EXO") {
|
catch {
|
||||||
Write-Verbose "Connecting to Microsoft Teams..."
|
throw "Failed to connect to SharePoint Online: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($RequiredConnections -contains 'Microsoft Teams' -or $RequiredConnections -contains 'Microsoft Teams | EXO') {
|
||||||
|
try {
|
||||||
|
Write-Verbose 'Connecting to Microsoft Teams...'
|
||||||
if ($AuthParams) {
|
if ($AuthParams) {
|
||||||
# Use application-based authentication
|
|
||||||
Connect-MicrosoftTeams -TenantId $AuthParams.TenantId -CertificateThumbprint $AuthParams.ClientCertThumbPrint -ApplicationId $AuthParams.ClientId | Out-Null
|
Connect-MicrosoftTeams -TenantId $AuthParams.TenantId -CertificateThumbprint $AuthParams.ClientCertThumbPrint -ApplicationId $AuthParams.ClientId | Out-Null
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
# Use interactive authentication
|
|
||||||
Connect-MicrosoftTeams | Out-Null
|
Connect-MicrosoftTeams | Out-Null
|
||||||
}
|
}
|
||||||
$teamsTenantDetails = Get-CsTenant
|
$teamsTenantDetails = Get-CsTenant
|
||||||
$tenantInfo += [PSCustomObject]@{
|
$tenantInfo += [PSCustomObject]@{
|
||||||
Service = "Microsoft Teams"
|
Service = 'Microsoft Teams'
|
||||||
TenantName = $teamsTenantDetails.DisplayName
|
TenantName = $teamsTenantDetails.DisplayName
|
||||||
TenantID = $teamsTenantDetails.TenantId
|
TenantID = $teamsTenantDetails.TenantId
|
||||||
}
|
}
|
||||||
$connectedServices += "Microsoft Teams"
|
$connectedServices += 'Microsoft Teams'
|
||||||
Write-Verbose "Successfully connected to Microsoft Teams.`n"
|
Write-Verbose 'Successfully connected to Microsoft Teams.'
|
||||||
}
|
}
|
||||||
# Display tenant information and confirm with the user
|
catch {
|
||||||
|
throw "Failed to connect to Microsoft Teams: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (-not $SkipConfirmation) {
|
if (-not $SkipConfirmation) {
|
||||||
Write-Verbose "Connected to the following tenants:"
|
Write-Verbose 'Connected to the following tenants:'
|
||||||
foreach ($tenant in $tenantInfo) {
|
foreach ($tenant in $tenantInfo) {
|
||||||
Write-Verbose "Service: $($tenant.Service)"
|
Write-Verbose "Service: $($tenant.Service) | Tenant: $($tenant.TenantName)"
|
||||||
Write-Verbose "Tenant Context: $($tenant.TenantName)`n"
|
|
||||||
#Write-Verbose "Tenant ID: $($tenant.TenantID)"
|
|
||||||
}
|
}
|
||||||
$confirmation = Read-Host "Do you want to proceed with these connections? (Y/N)"
|
$confirmation = Read-Host 'Do you want to proceed with these connections? (Y/N)'
|
||||||
if ($confirmation -notLike 'Y') {
|
if ($confirmation -notlike 'Y') {
|
||||||
Write-Verbose "Connection setup aborted by user."
|
|
||||||
Disconnect-M365Suite -RequiredConnections $connectedServices
|
Disconnect-M365Suite -RequiredConnections $connectedServices
|
||||||
throw "User aborted connection setup."
|
throw 'User aborted connection setup.'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
$CatchError = $_
|
$VerbosePreference = 'Continue'
|
||||||
$VerbosePreference = "Continue"
|
throw "Connection failed: $($_.Exception.Message)"
|
||||||
throw $CatchError
|
}
|
||||||
|
finally {
|
||||||
|
$VerbosePreference = 'Continue'
|
||||||
}
|
}
|
||||||
$VerbosePreference = "Continue"
|
|
||||||
}
|
}
|
Reference in New Issue
Block a user