diff --git a/source/Private/Test-IsAdmin.ps1 b/source/Private/Test-IsAdmin.ps1 new file mode 100644 index 0000000..64c47dc --- /dev/null +++ b/source/Private/Test-IsAdmin.ps1 @@ -0,0 +1,22 @@ +function Test-IsAdmin { + <# + .SYNOPSIS + Checks if the current user is an administrator on the machine. + .DESCRIPTION + This private function returns a Boolean value indicating whether + the current user has administrator privileges on the machine. + It does this by creating a new WindowsPrincipal object, passing + in a WindowsIdentity object representing the current user, and + then checking if that principal is in the Administrator role. + .INPUTS + None. + .OUTPUTS + Boolean. Returns True if the current user is an administrator, and False otherwise. + .EXAMPLE + PS C:\> Test-IsAdmin + True + #> + + # Create a new WindowsPrincipal object for the current user and check if it is in the Administrator role + (New-Object Security.Principal.WindowsPrincipal ([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) +} \ No newline at end of file diff --git a/source/Private/Write-AuditLog.ps1 b/source/Private/Write-AuditLog.ps1 new file mode 100644 index 0000000..1ea0fb8 --- /dev/null +++ b/source/Private/Write-AuditLog.ps1 @@ -0,0 +1,212 @@ +function Write-AuditLog { + <# + .SYNOPSIS + Writes log messages to the console and updates the script-wide log variable. + .DESCRIPTION + The Write-AuditLog function writes log messages to the console based on the severity (Verbose, Warning, or Error) and updates + the script-wide log variable ($script:LogString) with the log entry. You can use the Start, End, and EndFunction switches to + manage the lifecycle of the logging. + .INPUTS + System.String + You can pipe a string to the Write-AuditLog function as the Message parameter. + You can also pipe an object with a Severity property as the Severity parameter. + .OUTPUTS + None + The Write-AuditLog function doesn't output any objects to the pipeline. It writes messages to the console and updates the + script-wide log variable ($script:LogString). + .PARAMETER BeginFunction + Sets the message to "Begin [FunctionName] function log.", where FunctionName is the name of the calling function, and adds it to the log variable. + .PARAMETER Message + The message string to log. + .PARAMETER Severity + The severity of the log message. Accepted values are 'Information', 'Warning', and 'Error'. Defaults to 'Information'. + .PARAMETER Start + Initializes the script-wide log variable and sets the message to "Begin [FunctionName] Log.", where FunctionName is the name of the calling function. + .PARAMETER End + Sets the message to "End Log" and exports the log to a CSV file if the OutputPath parameter is provided. + .PARAMETER EndFunction + Sets the message to "End [FunctionName] log.", where FunctionName is the name of the calling function, and adds it to the log variable. + .PARAMETER OutputPath + The file path for exporting the log to a CSV file when using the End switch. + .EXAMPLE + Write-AuditLog -Message "This is a test message." + + Writes a test message with the default severity (Information) to the console and adds it to the log variable. + .EXAMPLE + Write-AuditLog -Message "This is a warning message." -Severity "Warning" + + Writes a warning message to the console and adds it to the log variable. + .EXAMPLE + Write-AuditLog -Start + + Initializes the log variable and sets the message to "Begin [FunctionName] Log.", where FunctionName is the name of the calling function. + .EXAMPLE + Write-AuditLog -BeginFunction + + Sets the message to "Begin [FunctionName] function log.", where FunctionName is the name of the calling function, and adds it to the log variable. + .EXAMPLE + Write-AuditLog -EndFunction + + Sets the message to "End [FunctionName] log.", where FunctionName is the name of the calling function, and adds it to the log variable. + .EXAMPLE + Write-AuditLog -End -OutputPath "C:\Logs\auditlog.csv" + + Sets the message to "End Log", adds it to the log variable, and exports the log to a CSV file. + .NOTES + Author: DrIOSx +#> + [CmdletBinding(DefaultParameterSetName = 'Default')] + param( + ### + [Parameter( + Mandatory = $false, + HelpMessage = 'Input a Message string.', + Position = 0, + ParameterSetName = 'Default', + ValueFromPipeline = $true + )] + [ValidateNotNullOrEmpty()] + [string]$Message, + ### + [Parameter( + Mandatory = $false, + HelpMessage = 'Information, Warning or Error.', + Position = 1, + ParameterSetName = 'Default', + ValueFromPipelineByPropertyName = $true + )] + [ValidateNotNullOrEmpty()] + [ValidateSet('Information', 'Warning', 'Error')] + [string]$Severity = 'Information', + ### + [Parameter( + Mandatory = $false, + ParameterSetName = 'End' + )] + [switch]$End, + ### + [Parameter( + Mandatory = $false, + ParameterSetName = 'BeginFunction' + )] + [switch]$BeginFunction, + [Parameter( + Mandatory = $false, + ParameterSetName = 'EndFunction' + )] + [switch]$EndFunction, + ### + [Parameter( + Mandatory = $false, + ParameterSetName = 'Start' + )] + [switch]$Start, + ### + [Parameter( + Mandatory = $false, + ParameterSetName = 'End' + )] + [string]$OutputPath + ) + begin { + $ErrorActionPreference = "SilentlyContinue" + # Define variables to hold information about the command that was invoked. + $ModuleName = $Script:MyInvocation.MyCommand.Name -replace '\..*' + $callStack = Get-PSCallStack + if ($callStack.Count -gt 1) { + $FuncName = $callStack[1].Command + } else { + $FuncName = "DirectCall" # Or any other default name you prefer + } + #Write-Verbose "Funcname Name is $FuncName!" -Verbose + $ModuleVer = $MyInvocation.MyCommand.Version.ToString() + # Set the error action preference to continue. + $ErrorActionPreference = "Continue" + } + process { + try { + if (-not $Start -and -not (Test-Path variable:script:LogString)) { + throw "The logging variable is not initialized. Please call Write-AuditLog with the -Start switch or ensure $script:LogString is set." + } + $Function = $($FuncName + '.v' + $ModuleVer) + if ($Start) { + $script:LogString = @() + $Message = '+++ Begin Log | ' + $Function + ' |' + } + elseif ($BeginFunction) { + $Message = '>>> Begin Function Log | ' + $Function + ' |' + } + $logEntry = [pscustomobject]@{ + Time = ((Get-Date).ToString('yyyy-MM-dd hh:mmTss')) + Module = $ModuleName + PSVersion = ($PSVersionTable.PSVersion).ToString() + PSEdition = ($PSVersionTable.PSEdition).ToString() + IsAdmin = $(Test-IsAdmin) + User = "$Env:USERDOMAIN\$Env:USERNAME" + HostName = $Env:COMPUTERNAME + InvokedBy = $Function + Severity = $Severity + Message = $Message + RunID = -1 + } + if ($BeginFunction) { + $maxRunID = ($script:LogString | Where-Object { $_.InvokedBy -eq $Function } | Measure-Object -Property RunID -Maximum).Maximum + if ($null -eq $maxRunID) { $maxRunID = -1 } + $logEntry.RunID = $maxRunID + 1 + } + else { + $lastRunID = ($script:LogString | Where-Object { $_.InvokedBy -eq $Function } | Select-Object -Last 1).RunID + if ($null -eq $lastRunID) { $lastRunID = 0 } + $logEntry.RunID = $lastRunID + } + if ($EndFunction) { + $FunctionStart = "$((($script:LogString | Where-Object {$_.InvokedBy -eq $Function -and $_.RunId -eq $lastRunID } | Sort-Object Time)[0]).Time)" + $startTime = ([DateTime]::ParseExact("$FunctionStart", 'yyyy-MM-dd hh:mmTss', $null)) + $endTime = Get-Date + $timeTaken = $endTime - $startTime + $Message = '<<< End Function Log | ' + $Function + ' | Runtime: ' + "$($timeTaken.Minutes) min $($timeTaken.Seconds) sec" + $logEntry.Message = $Message + } + elseif ($End) { + $startTime = ([DateTime]::ParseExact($($script:LogString[0].Time), 'yyyy-MM-dd hh:mmTss', $null)) + $endTime = Get-Date + $timeTaken = $endTime - $startTime + $Message = '--- End Log | ' + $Function + ' | Runtime: ' + "$($timeTaken.Minutes) min $($timeTaken.Seconds) sec" + $logEntry.Message = $Message + } + $script:LogString += $logEntry + switch ($Severity) { + 'Warning' { + Write-Warning ('[WARNING] ! ' + $Message) + $UserInput = Read-Host "Warning encountered! Do you want to continue? (Y/N)" + if ($UserInput -eq 'N') { + throw "Script execution stopped by user." + } + } + 'Error' { Write-Error ('[ERROR] X - ' + $FuncName + ' ' + $Message) -ErrorAction Continue } + 'Verbose' { Write-Verbose ('[VERBOSE] ~ ' + $Message) } + Default { Write-Information ('[INFO] * ' + $Message) -InformationAction Continue} + } + } + catch { + throw "Write-AuditLog encountered an error (process block): $($_)" + } + + } + end { + try { + if ($End) { + if (-not [string]::IsNullOrEmpty($OutputPath)) { + $script:LogString | Export-Csv -Path $OutputPath -NoTypeInformation + Write-Verbose "LogPath: $(Split-Path -Path $OutputPath -Parent)" + } + else { + throw "OutputPath is not specified for End action." + } + } + } + catch { + throw "Error in Write-AuditLog (end block): $($_.Exception.Message)" + } + } +} \ No newline at end of file diff --git a/tests/Unit/Private/Test-IsAdmin.tests.ps1 b/tests/Unit/Private/Test-IsAdmin.tests.ps1 new file mode 100644 index 0000000..4a2aa69 --- /dev/null +++ b/tests/Unit/Private/Test-IsAdmin.tests.ps1 @@ -0,0 +1,27 @@ +$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path +$ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{ + ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and + $(try { Test-ModuleManifest $_.FullName -ErrorAction Stop } catch { $false } ) + }).BaseName + + +Import-Module $ProjectName + +InModuleScope $ProjectName { + Describe Get-PrivateFunction { + Context 'Default' { + BeforeEach { + $return = Get-PrivateFunction -PrivateData 'string' + } + + It 'Returns a single object' { + ($return | Measure-Object).Count | Should -Be 1 + } + + It 'Returns a string based on the parameter PrivateData' { + $return | Should -Be 'string' + } + } + } +} + diff --git a/tests/Unit/Private/Write-AuditLog.tests.ps1 b/tests/Unit/Private/Write-AuditLog.tests.ps1 new file mode 100644 index 0000000..4a2aa69 --- /dev/null +++ b/tests/Unit/Private/Write-AuditLog.tests.ps1 @@ -0,0 +1,27 @@ +$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path +$ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{ + ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and + $(try { Test-ModuleManifest $_.FullName -ErrorAction Stop } catch { $false } ) + }).BaseName + + +Import-Module $ProjectName + +InModuleScope $ProjectName { + Describe Get-PrivateFunction { + Context 'Default' { + BeforeEach { + $return = Get-PrivateFunction -PrivateData 'string' + } + + It 'Returns a single object' { + ($return | Measure-Object).Count | Should -Be 1 + } + + It 'Returns a string based on the parameter PrivateData' { + $return | Should -Be 'string' + } + } + } +} +