Public/Schedule/New-SPCScanSchedule.ps1
|
function New-SPCScanSchedule { <# .SYNOPSIS Creates a Windows Scheduled Task that runs Get-SPCOrphanedUser and Export-SPCReport per SRS 3.5.1. .DESCRIPTION Generates a self-contained PowerShell scan script and registers it with Windows Task Scheduler. Always uses App-only authentication (Interactive cannot be scheduled). Certificate password is encrypted with DPAPI — decryptable only on the same machine and user account. On non-Windows platforms, writes the script file and advises configuring a cron job manually. .PARAMETER TenantName SharePoint tenant name (e.g. 'contoso'). .PARAMETER ClientId Azure AD app registration client ID. .PARAMETER CertificatePath Path to the .pfx certificate file used for App-only auth. .PARAMETER CertificatePassword SecureString password for the .pfx file. Encrypted with DPAPI for storage. .PARAMETER Schedule Recurring schedule shorthand: 'Daily', 'Weekly', or 'Monthly'. Mutually exclusive with -ScheduleAt. .PARAMETER ScheduleAt One-time execution date/time. Mutually exclusive with -Schedule. .PARAMETER ReportFormat Report file format: 'HTML' (default), 'CSV', or 'JSON'. .PARAMETER ReportOutputPath Directory where report files are saved by the scheduled task. .PARAMETER TaskName Windows Task Scheduler task name. Default: 'SPClean_OrphanedUserScan'. .EXAMPLE New-SPCScanSchedule -TenantName contoso -ClientId '...' -CertificatePath C:\cert.pfx -CertificatePassword $pwd -Schedule Weekly -ReportOutputPath C:\Reports .EXAMPLE New-SPCScanSchedule -TenantName contoso -ClientId '...' -CertificatePath C:\cert.pfx -CertificatePassword $pwd -Schedule Daily -ReportOutputPath C:\Reports -WhatIf .OUTPUTS SPC.ScheduleResult #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Recurring')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [string] $TenantName, [Parameter(Mandatory)] [string] $ClientId, [Parameter(Mandatory)] [string] $CertificatePath, [Parameter(Mandatory)] [System.Security.SecureString] $CertificatePassword, [Parameter(Mandatory, ParameterSetName = 'Recurring')] [ValidateSet('Daily', 'Weekly', 'Monthly')] [string] $Schedule, [Parameter(Mandatory, ParameterSetName = 'OneTime')] [datetime] $ScheduleAt, [Parameter()] [ValidateSet('CSV', 'HTML', 'JSON')] [string] $ReportFormat = 'HTML', [Parameter(Mandatory)] [Alias('OutputPath')] [string] $ReportOutputPath, [Parameter()] [string] $TaskName = 'SPClean_OrphanedUserScan' ) process { Assert-SPCProLicense -Feature 'ScheduledScan' # Resolve and validate paths $resolvedCert = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($CertificatePath) if (-not (Test-Path -Path $resolvedCert -PathType Leaf)) { throw "New-SPCScanSchedule: Certificate not found: '$resolvedCert'" } $resolvedOutputDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ReportOutputPath) # Encrypt CertificatePassword with DPAPI — machine+user scoped, never plain text $encryptedPwd = $CertificatePassword | ConvertFrom-SecureString $ext = switch ($ReportFormat.ToUpper()) { 'CSV' { 'csv' } 'HTML' { 'html' } 'JSON' { 'json' } } # Script file lives in the parent of the report directory (or alongside it) $scriptDir = Split-Path -Path $resolvedOutputDir -Parent if ([string]::IsNullOrWhiteSpace($scriptDir) -or -not (Test-Path -Path $scriptDir -PathType Container)) { $scriptDir = $resolvedOutputDir } $scriptPath = Join-Path $scriptDir "${TaskName}.ps1" $scheduleLabel = if ($PSCmdlet.ParameterSetName -eq 'OneTime') { "Once at $($ScheduleAt.ToString('yyyy-MM-dd HH:mm'))" } else { $Schedule } # WhatIf — report intent only if ($WhatIfPreference) { Write-Information "WhatIf: Would write scan script to '$scriptPath' and register task '$TaskName' ($scheduleLabel)." -InformationAction Continue $preview = [PSCustomObject][ordered]@{ TaskName = $TaskName ScriptPath = $scriptPath Schedule = $scheduleLabel NextRun = $null Status = 'WhatIf' Message = 'Dry-run: no files or tasks were created.' } $preview.PSObject.TypeNames.Insert(0, 'SPC.ScheduleResult') $preview return } if (-not $PSCmdlet.ShouldProcess($TaskName, 'New-SPCScanSchedule')) { return } # Generate the scan script — backtick-dollar escapes inner PS variables; # double-backtick produces a single backtick (line continuation) in the output file $scriptContent = @" #Requires -Version 5.1 # SPClean scheduled scan script # Generated by New-SPCScanSchedule on $(( Get-Date ).ToString('yyyy-MM-dd')) # SECURITY: CertificatePassword is DPAPI-encrypted — only decryptable on this machine # by the user account that registered this task. param() `$ErrorActionPreference = 'Stop' `$CertificatePassword = '$encryptedPwd' | ConvertTo-SecureString Import-Module SPClean -ErrorAction Stop try { Connect-SPCTenant `` -TenantName '$TenantName' `` -AuthMethod AppOnly `` -ClientId '$ClientId' `` -CertificatePath '$resolvedCert' `` -CertificatePassword `$CertificatePassword if (-not (Test-Path -Path '$resolvedOutputDir')) { New-Item -ItemType Directory -Path '$resolvedOutputDir' -Force | Out-Null } `$timestamp = Get-Date -Format 'yyyyMMddHHmm' `$reportFile = Join-Path '$resolvedOutputDir' "SPClean_`$timestamp.$ext" Get-SPCOrphanedUser -AllSites | `` Export-SPCReport -Format '$ReportFormat' -OutputPath `$reportFile -IncludeSummary Write-Output "SPClean scan complete: `$reportFile" } finally { Disconnect-SPCTenant -Confirm:`$false -ErrorAction SilentlyContinue } "@ # Ensure script directory exists and write script if (-not (Test-Path -Path $scriptDir -PathType Container)) { [void](New-Item -Path $scriptDir -ItemType Directory -Force) } [System.IO.File]::WriteAllText($scriptPath, $scriptContent, [System.Text.UTF8Encoding]::new($false)) Write-Verbose "New-SPCScanSchedule: Scan script written to '$scriptPath'" # Detect Windows via .NET — reliable across PS 5.1, PS 7+, and module scopes. # Get-Variable/automatic-variable lookup is not trustworthy inside module functions. $isWindows = [System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT $taskRegistered = $false $nextRun = $null $statusMsg = $null if ($isWindows) { try { $pwshPath = if ($PSVersionTable.PSVersion.Major -ge 6) { $p = Get-Command pwsh -ErrorAction SilentlyContinue if ($p) { $p.Source } else { (Get-Command powershell -ErrorAction Stop).Source } } else { (Get-Command powershell -ErrorAction Stop).Source } $action = New-ScheduledTaskAction ` -Execute $pwshPath ` -Argument "-NonInteractive -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$scriptPath`"" $trigger = if ($PSCmdlet.ParameterSetName -eq 'OneTime') { New-ScheduledTaskTrigger -Once -At $ScheduleAt } else { switch ($Schedule) { 'Daily' { New-ScheduledTaskTrigger -Daily -At '02:00' } 'Weekly' { New-ScheduledTaskTrigger -Weekly -At '02:00' -DaysOfWeek Sunday } 'Monthly' { New-ScheduledTaskTrigger -Monthly -At '02:00' -DaysOfMonth 1 } } } $settings = New-ScheduledTaskSettingsSet ` -ExecutionTimeLimit (New-TimeSpan -Hours 2) ` -MultipleInstances IgnoreNew $task = Register-ScheduledTask ` -TaskName $TaskName ` -Action $action ` -Trigger $trigger ` -Settings $settings ` -Force ` -ErrorAction Stop $taskRegistered = $true $nextRun = if ($task.Triggers.Count -gt 0) { $task.Triggers[0].StartBoundary } else { $null } $statusMsg = "Task '$TaskName' registered in Windows Task Scheduler. Script: '$scriptPath'" Write-Verbose "New-SPCScanSchedule: $statusMsg" } catch { Write-Warning "New-SPCScanSchedule: Task Scheduler registration failed — $($_.Exception.Message). Script saved to '$scriptPath'." $statusMsg = "Script created but task registration failed: $($_.Exception.Message)" } } else { $cronExpr = switch ($PSCmdlet.ParameterSetName) { 'OneTime' { "# One-time: $(if ($ScheduleAt) { $ScheduleAt.ToString('mm HH d M') } else { '...' }) *" } 'Recurring' { switch ($Schedule) { 'Daily' { '0 2 * * *' } 'Weekly' { '0 2 * * 0' } 'Monthly' { '0 2 1 * *' } } } } $statusMsg = "Non-Windows platform: Task Scheduler unavailable. Script written to '$scriptPath'. Add this cron entry: $cronExpr pwsh -NonInteractive -File `"$scriptPath`"" Write-Warning "New-SPCScanSchedule: $statusMsg" } $result = [PSCustomObject][ordered]@{ TaskName = $TaskName ScriptPath = $scriptPath Schedule = $scheduleLabel NextRun = $nextRun Status = if ($taskRegistered) { 'Registered' } else { 'ScriptOnly' } Message = $statusMsg } $result.PSObject.TypeNames.Insert(0, 'SPC.ScheduleResult') $result } } |