diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..19505d3 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Launch (current file)", + "script": "${file}", + "args": [], + "cwd": "${file}" + }, + { + "type": "PowerShell", + "request": "attach", + "name": "PowerShell Attach to Host Process", + "processId": "${command.PickPSHostProcess}", + "runspaceId": 1 + }, + { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Interactive Session", + "cwd": "${workspaceRoot}" + } + ] +} \ No newline at end of file diff --git a/AD/Get-ADCommonGroup.ps1 b/AD/Get-ADCommonGroup.ps1 new file mode 100644 index 0000000..cb2d4cd --- /dev/null +++ b/AD/Get-ADCommonGroup.ps1 @@ -0,0 +1,40 @@ +[CmdletBinding()] +param ( + [Parameter(Mandatory = $true)] + [ValidateScript({ Get-ADGroup -Identity $_ })] + [string]$GroupName +) + +begin { + Import-Module ActiveDirectory + $Group = Get-ADGroup -Identity $GroupName + $AllGroups = @{} +} + +process { + $Members = Get-ADGroupMember -Identity $Group + + $Members | ForEach-Object { + $User = $_.SamAccountName + + (Get-ADUser $User -Properties MemberOf).MemberOf | ForEach-Object { + # Check if the group exists in our list + if ($AllGroups.ContainsKey($_)) { + $AllGroups.($_)++ + } else { + # Item is unique so add it to the list + $AllGroups.Add($_, 1) + } + } + } + + Write-Host "Results:" + $AllGroups | Format-Table -AutoSize + $AllGroups.GetEnumerator() | Select-Object -Property Key, Value | + Export-Csv -Path .\$GroupName-CommonGroups.csv -NoTypeInformation + + Write-Host "Common memberships:" + $AllGroups.GetEnumerator() | ForEach-Object { + if ($_.Value -eq $Members.Count) { $_.Key } + } +} \ No newline at end of file diff --git a/AD/Get-ADGroupReport.ps1 b/AD/Get-ADGroupReport.ps1 new file mode 100644 index 0000000..a8fe501 --- /dev/null +++ b/AD/Get-ADGroupReport.ps1 @@ -0,0 +1,119 @@ +function Get-ADGroupReport { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [ValidateNotNullorEmpty()] + [string]$SaveToCsv, + + [Parameter(Mandatory = $false)] + [ValidateNotNullorEmpty()] + [string]$ADGroupFilter = 'GroupCategory -eq "Distribution"' + ) + + begin { + Import-Module ActiveDirectory + + Write-Progress -Id 1 -Activity "Getting AD groups using filter $ADGroupFilter..." + $Groups = Get-ADGroup -Filter $ADGroupFilter + $Users = @{} + } + + process { + $GroupCount = $Groups.Count + $GroupCounter = 0 + + foreach ($Group in $Groups) { + $GroupCounter++ + + $GroupName = $Group.Name + + $GroupProgressParams = @{ + 'Id' = 1 + 'Activity' = "Processing group $GroupCounter of $GroupCount" + 'Status' = $GroupName + 'PercentComplete' = ($GroupCounter / $GroupCount) * 100 + } + Write-Progress @GroupProgressParams + + $Members = Get-ADGroupMember $Group -Recursive + + $MemberCount = $Members.Count + $MemberCounter = 0 + + foreach ($Member in $Members) { + $MemberCounter++ + + $MemberName = $Member.SamAccountName + + $MemberProgressParams = @{ + 'Id' = 2 + 'ParentId' = 1 + 'Activity' = "Processing member $MemberCounter of $MemberCount" + 'Status' = $MemberName + } + if ($MemberCount.GetType() -eq [int]) { + $MemberProgressParams.Add('PercentComplete', ($MemberCounter / $MemberCount) * 100) + } + Write-Progress @MemberProgressParams + + if ($Member.objectClass -eq 'user') { + Write-Verbose "$MemberName is a user" + + if (-not $Users.ContainsKey($MemberName)) { + Write-Verbose "Adding $MemberName to users list" + $Users.Add($MemberName, @{}) + } + + Write-Verbose "Adding $($GroupName) to $MemberName's group list" + $Users.($MemberName).Add($GroupName, $true) + } else { + Write-Verbose "$MemberName is not a user" + } + } + + Write-Progress -Id 2 -ParentId 1 -Activity "Processing members" -Completed + } + + $ReportProgressParams = @{ + 'Id' = 1 + 'Activity' = 'Building report...' + } + Write-Progress @ReportProgressParams + + $Results = @() + + foreach ($User in $Users.GetEnumerator()) { + $ADUser = Get-ADUser -Identity $User.Name -Properties @( + 'physicalDeliveryOfficeName', 'Office', 'Department', 'Company', 'City', 'telephoneNumber' + ) + + $UserObject = New-Object psobject -Property @{ + UserId = $User.Name + Email = $ADUser.UserPrincipalName + Phone = $ADUser.telephoneNumber + FullName = $ADUser.Name + Enabled = $ADUser.Enabled + PhysicalOffice = $ADUser.physicalDeliveryOfficeName + Office = $ADUser.Office + Department = $ADUser.Department + Company = $ADUser.Company + City = $ADUser.City + } + + foreach ($Group in $Groups) { + $UserObject | Add-Member -MemberType NoteProperty -Name $Group.Name -Value '' + + if ($User.Value.ContainsKey($Group.Name)) { + $UserObject.($Group.Name) = 'x' + } + } + + $Results += $UserObject + } + } + + end { + if ($SaveToCsv) { $Results | Export-Csv -Path $SaveToCsv -NoTypeInformation } + $Results + } +} \ No newline at end of file diff --git a/AD/Get-DisabledUser.ps1 b/AD/Get-DisabledUser.ps1 new file mode 100644 index 0000000..2990c1d --- /dev/null +++ b/AD/Get-DisabledUser.ps1 @@ -0,0 +1,43 @@ +#Requires –Version 3 + +function Get-DisabledUser { + param ( + [Parameter(Mandatory = $true)] + [string]$UsersFilePath, + + [Parameter(Mandatory = $false)] + [ValidateSet('Email', 'FullName', 'UserID')] + [string]$ListType = 'Email' + ) + + begin { + Import-Module ActiveDirectory + + $Users = Get-Content -Path $UsersFilePath + $ADUsers = Get-ADUser -SearchBase 'OU=Users,DC=company,DC=LOCAL' -Filter * + $Results = @() + } + + process { + foreach ($User in $Users) { + switch ($ListType) { + 'Email' { $SearchProperty = 'UserPrincipalName' } + 'FullName' { $SearchProperty = 'Name' } + 'UserID' { $SearchProperty = 'SamAccountName' } + } + + $FoundUser = $ADUsers | Where-Object { $_.$SearchProperty -eq $User } + + $Results += New-Object -TypeName PSObject -Property @{ + SearchProperty = $User + UserPrincipalName = $FoundUser.UserPrincipalName + Name = $FoundUser.Name + SamAccountName = $FoundUser.SamAccountName + Enabled = if ($FoundUser.Enabled) { $true } else { $false } + } + } + $Results | Sort-Object Enabled | Format-Table -AutoSize + } +} + +Get-DisabledUser -UsersFilePath C:\TestUsers.txt -ListType Email \ No newline at end of file diff --git a/AD/Reset-ADLastPasswordSetTime.ps1 b/AD/Reset-ADLastPasswordSetTime.ps1 new file mode 100644 index 0000000..816fba6 --- /dev/null +++ b/AD/Reset-ADLastPasswordSetTime.ps1 @@ -0,0 +1,5 @@ +$creds = Get-Credential + +$Me = Get-ADUser -Filter * | Where-Object -Property SamAccountName -Like userid* +$Me | Set-ADUser -ChangePasswordAtLogon $true -Credential $creds +$Me | Set-ADUser -ChangePasswordAtLogon $false -Credential $creds \ No newline at end of file diff --git a/AD/Test-Credential.ps1 b/AD/Test-Credential.ps1 new file mode 100644 index 0000000..ac4b048 --- /dev/null +++ b/AD/Test-Credential.ps1 @@ -0,0 +1,65 @@ +<# +.DESCRIPTION + Simulates an Authentication Request in a Domain envrionment using a PSCredential Object. Returns $true if both Username and Password pair are valid. +.VERSION + 1.3 +.GUID + 6a18515f-73d3-4fb4-884f-412395aa5054 +.AUTHOR + Thomas Malkewitz @dotps1 +.TAGS + PSCredential, Credential +.RELEASENOTES + Updated $Domain default value to $Credential.GetNetworkCredential().Domain. + Added support for multipul credential objects to be passed into $Credential. +.PROJECTURI + http://dotps1.github.io +.NOTES + Slight modifications by Nick + #> + +Function Test-Credential { + [OutputType([Bool])] + + Param ( + [Parameter( + Mandatory = $true, + ValueFromPipeLine = $true, + ValueFromPipelineByPropertyName = $true + )] + [Alias('PSCredential')] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [Parameter()] + [String] + $Domain = $Credential.GetNetworkCredential().Domain + ) + + Begin { + [System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.AccountManagement") | + Out-Null + + $principalContext = New-Object System.DirectoryServices.AccountManagement.PrincipalContext( + [System.DirectoryServices.AccountManagement.ContextType]::Domain, $Domain + ) + } + + Process { + foreach ($item in $Credential) { + $networkCredential = $Credential.GetNetworkCredential() + + Write-Output -InputObject $( + $principalContext.ValidateCredentials( + $networkCredential.UserName, $networkCredential.Password + ) + ) + } + } + + End { + $principalContext.Dispose() + } +} \ No newline at end of file diff --git a/AD/Update-ADMFARegistrationGroup.ps1 b/AD/Update-ADMFARegistrationGroup.ps1 new file mode 100644 index 0000000..4c68b9b --- /dev/null +++ b/AD/Update-ADMFARegistrationGroup.ps1 @@ -0,0 +1,18 @@ +Import-Module ActiveDirectory + +# Get all MSO users and their MFA status, convert into AD users +$MFAUsers = . "MSO\Get-MFAEnabledUser.ps1" +$RegisteredUsers = $MFAUsers | Where-Object -Property DefaultMethod -NE $null | ForEach-Object { + Get-ADUser -Filter "UserPrincipalName -eq '$($_.UserPrincipalName)'" +} + +# Get the group that holds unregistered MFA users and the users within +$UnregisteredGroup = Get-ADGroup -Identity 'MFA Registration Incomplete' +$UnregisteredUsers = $UnregisteredGroup | Get-ADGroupMember | Get-ADUser + +# Find users in the group that have a default MFA method +$UsersToRemove = $UnregisteredUsers | Compare-Object -ReferenceObject $RegisteredUsers -IncludeEqual | +Where-Object -Property SideIndicator -eq '==' + +# Remove users with a default MFA method +Remove-ADGroupMember -Identity $UnregisteredGroup -Members $UsersToRemove.InputObject \ No newline at end of file diff --git a/Application Management/Get-AADConnectVersion.ps1 b/Application Management/Get-AADConnectVersion.ps1 new file mode 100644 index 0000000..a48aff6 --- /dev/null +++ b/Application Management/Get-AADConnectVersion.ps1 @@ -0,0 +1,7 @@ +$Creds = Get-Credential +$DirSyncServer = 'dirsync01' + +Invoke-Command -ComputerName $DirSyncServer -Credential $Creds -ScriptBlock { + $RegKey = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" + Get-ItemProperty -Path $RegKey | Where-Object -Property DisplayName -EQ 'Microsoft Azure AD Connect' +} | Select-Object -Property DisplayName, PSPath, Version, DisplayVersion | Format-List \ No newline at end of file diff --git a/CaseWare/New-CWSyncServer.ps1 b/CaseWare/New-CWSyncServer.ps1 new file mode 100644 index 0000000..ce0f1bd --- /dev/null +++ b/CaseWare/New-CWSyncServer.ps1 @@ -0,0 +1,41 @@ +function New-CWSyncServer { + <# + .DESCRIPTION + Add SmartSync server to CaseWare Working Papers. + + .PARAMETER HostName + Host name of SmartSync server. + + .PARAMETER Label + Friendly name of SmartSync server as it will appear in CaseWare Working Papers. + + .EXAMPLE + New-CWSyncServer site01.company.com + + .NOTES + Created by Nick Rodriguez + Adds new SmartSync server to registry that will appear in CaseWare Working Papers. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param( + [Parameter(Mandatory = $true)] + [String] + $HostName, + + [Parameter(Mandatory = $false)] + [String] + $FriendlyName + ) + + # Create the site key + $RegPath = 'HKCU:\SOFTWARE\CaseWare International\Working Papers\*\SyncServer' + if (-not (Test-Path -Path "$RegPath\{$HostName}")) { New-Item -Path $RegPath -Name "{$HostName}" } + + # Set the two site key properties + Set-ItemProperty -Path "$RegPath\{$HostName}" -Name Host -Value $HostName + Set-ItemProperty -Path "$RegPath\{$HostName}" -Name Label -Value $FriendlyName + + # Validate with SCCM friendly exit codes + $SiteKey = Get-ItemProperty -Path "$RegPath\{$HostName}" + if ($SiteKey.Host -eq $HostName -and $SiteKey.Label -eq $FriendlyName) { exit 0 } else { exit 999 } +} \ No newline at end of file diff --git a/Data/Convert-RobocopyExitCode.ps1 b/Data/Convert-RobocopyExitCode.ps1 new file mode 100644 index 0000000..ec2a794 --- /dev/null +++ b/Data/Convert-RobocopyExitCode.ps1 @@ -0,0 +1,22 @@ +function Convert-RobocopyExitCode ($ExitCode) { + switch ($ExitCode) { + 16 {'***FATAL ERROR***'} + 15 {'OKCOPY + FAIL + MISMATCHES + XTRA'} + 14 {'FAIL + MISMATCHES + XTRA'} + 13 {'OKCOPY + FAIL + MISMATCHES'} + 12 {'FAIL + MISMATCHES'} + 11 {'OKCOPY + FAIL + XTRA'} + 10 {'FAIL + XTRA'} + 9 {'OKCOPY + FAIL'} + 8 {'FAIL'} + 7 {'OKCOPY + MISMATCHES + XTRA'} + 6 {'MISMATCHES + XTRA'} + 5 {'OKCOPY + MISMATCHES'} + 4 {'MISMATCHES'} + 3 {'OKCOPY + XTRA'} + 2 {'XTRA'} + 1 {'OKCOPY'} + 0 {'No Change'} + default {'Unknown'} + } +} \ No newline at end of file diff --git a/Data/Convert-TableToHTML.ps1 b/Data/Convert-TableToHTML.ps1 new file mode 100644 index 0000000..b17a8a1 --- /dev/null +++ b/Data/Convert-TableToHTML.ps1 @@ -0,0 +1,22 @@ +# You can embed the css in the script or alternatively you can reference a css file + +# File reference +$Message = $Table | ConvertTo-Html -CssUri $CSSFilePath | Out-String + +# Embed +$CSS = @" + +"@ +$Message = $Table | ConvertTo-Html -Head $CSS | Out-String + +# And then use this to send the email +Send-MailMessage -Body $Message -BodyAsHtml -From 'Alerts@company.com' ` + -SmtpServer smtp.company.local -Subject 'Report' -To 'me@company.com' \ No newline at end of file diff --git a/Data/ConvertFrom-Base64Image.ps1 b/Data/ConvertFrom-Base64Image.ps1 new file mode 100644 index 0000000..83daa76 --- /dev/null +++ b/Data/ConvertFrom-Base64Image.ps1 @@ -0,0 +1,3 @@ +$Base64Icon = '' +$IconStream = [System.Convert]::FromBase64String($Base64Icon) +$IconBMP = [System.Drawing.Image]::FromStream($IconStream) \ No newline at end of file diff --git a/Data/ConvertTo-Base64Image.ps1 b/Data/ConvertTo-Base64Image.ps1 new file mode 100644 index 0000000..1706d70 --- /dev/null +++ b/Data/ConvertTo-Base64Image.ps1 @@ -0,0 +1,5 @@ +[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | Out-Null +$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog +$OpenFileDialog.ShowDialog() | Out-Null +$Image = Get-Item $OpenFileDialog.FileName +[System.Convert]::ToBase64String((Get-Content $Image -Encoding Byte)) >> .\EncodedImage.txt \ No newline at end of file diff --git a/Data/ConvertTo-Icon.ps1 b/Data/ConvertTo-Icon.ps1 new file mode 100644 index 0000000..6361c8b --- /dev/null +++ b/Data/ConvertTo-Icon.ps1 @@ -0,0 +1,25 @@ +[CmdletBinding()] +param( + [Parameter( + Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true + )] + [ValidateScript({ + if (Test-Path -Path $_ -PathType Leaf) { + $true + } else { + throw "[$_] is not a valid file." + $false + } + })] + [string[]]$FilePath +) + +foreach ($Path in $FilePath) { + Add-Type -AssemblyName System.Drawing + + $File = Get-Item -Path $Path + $Icon = [System.Drawing.Icon]::ExtractAssociatedIcon($File.FullName) + $Icon.ToBitmap().Save($File.FullName.Replace($File.Extension, '.ico')) +} \ No newline at end of file diff --git a/Data/Get-MD5Checksum.ps1 b/Data/Get-MD5Checksum.ps1 new file mode 100644 index 0000000..20024d1 --- /dev/null +++ b/Data/Get-MD5Checksum.ps1 @@ -0,0 +1,10 @@ +# If the content is a string: +$someString = "Hello World!" +$md5 = new-object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider +$utf8 = new-object -TypeName System.Text.UTF8Encoding +$hash = [System.BitConverter]::ToString($md5.ComputeHash($utf8.GetBytes($someString))) + +# If the content is a file: +$someFilePath = "C:\foo.txt" +$md5 = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider +$hash = [System.BitConverter]::ToString($md5.ComputeHash([System.IO.File]::ReadAllBytes($someFilePath))) \ No newline at end of file diff --git a/Exchange/Enter-EOSession.ps1 b/Exchange/Enter-EOSession.ps1 new file mode 100644 index 0000000..9ed7349 --- /dev/null +++ b/Exchange/Enter-EOSession.ps1 @@ -0,0 +1,7 @@ +# If you get an Access Denied message but you're a member of Exchange Online Admins, +# make sure you don't have MFA enabled +$ExchangeOnlineSession = New-PSSession -ConfigurationName Microsoft.Exchange ` + -ConnectionUri https://outlook.office365.com/powershell-liveid/ ` + -Credential (Get-Credential) -Authentication Basic -AllowRedirection + +Import-PSSession $ExchangeOnlineSession \ No newline at end of file diff --git a/Exchange/GalSync.psm1 b/Exchange/GalSync.psm1 new file mode 100644 index 0000000..45774bc --- /dev/null +++ b/Exchange/GalSync.psm1 @@ -0,0 +1,277 @@ +function Sync-Gal { + <# + .DESCRIPTION + Does a one-way sync between two Exchange Online Gal's. + + .PARAMETER PrimaryTenantCreds + Credentials to login to the primary tenant. + + .PARAMETER SecondaryTenantCreds + Credentials to login to the secondary tenant. + + .PARAMETER AsJob + Enables the updates to be run as PS Jobs. + + .PARAMETER Jobs + The number of PS Jobs to create to run the updates. Limited to 3 because of an O365 default max concurrent connection. + + .PARAMETER ContactLimit + Number of contacts to sync. Default is Unlimited. Useful for doing quick tests. + + .EXAMPLE + ./Sync-Gal.ps1 -PrimaryTenantCreds (Get-Credential) -SecondaryTenantCreds (Get-Credential) -AsJob -Jobs 3 + + .EXAMPLE + ./Sync-Gal.ps1 + + .NOTES + Created by Nick Rodriguez + Syncs the Gal of Exchange Online across two Office 365 tenants + Note that this will break the creation of external user objects until MS addresses a known issue: + https://products.office.com/en-us/business/office-365-roadmap?filters=&featureid=72273 + #> + [CmdletBinding( + SupportsShouldProcess = $true, + DefaultParameterSetName = 'Synchronous' + )] + Param( + [Parameter(Mandatory = $true)] + [PSCredential] + $PrimaryTenantCreds, + + [Parameter(Mandatory = $true)] + [PSCredential] + $SecondaryTenantCreds, + + [Parameter( + Mandatory = $false, + ParameterSetName = 'Asynchronous' + )] + [Switch] + $AsJob, + + [Parameter( + Mandatory = $false, + ParameterSetName = 'Asynchronous' + )] + [ValidateRange(1, 3)] + [Int] + $Jobs = 2, + + [Parameter(Mandatory = $false)] + [ValidateRange(0, [Int]::MaxValue)] + [Int] + $ContactLimit = 0 + ) + + begin { + # Log everything + $LogDirectory = (New-Item -ItemType Directory "C:\powershell-scripts\Exchange Online\Logs" -Force).FullName + $Date = (Get-Date).ToString('yyyyMMdd-HHmm') + Start-Transcript -Path "$LogDirectory\$($MyInvocation.MyCommand.Name)-$Date.log" + } + + process { + # Create Exchange Online PowerShell session for primary tenant + $PrimaryTenantEOSession = New-EOSession -Credential $PrimaryTenantCreds + + # Enter session on primary tenant + Import-PSSession $PrimaryTenantEOSession + + # Get all Gal recipients using the primary filter + $GalFilter = (Get-GlobalAddressList).RecipientFilter + $ResultSizeLimit = if ($ContactLimit -eq 0) { 'Unlimited' } else { $ContactLimit } + $Gal = Get-Recipient -ResultSize $ResultSizeLimit -Filter $GalFilter + + # Export Gal to Csv file + $Gal | Export-Csv -Path "$LogDirectory\Gal.csv" -NoTypeInformation -Force + + # Remove session on primary tenant + Remove-PSSession -Session $PrimaryTenantEOSession + + # Create/Update contact for each Gal entry + # If Jobs param specified, break up the list into smaller lists that can be started as jobs + if ($AsJob) { + $ContactLists = @{} + $Count = 0 + + # Separate the contacts into smaller lists + $Gal | ForEach-Object { + $ContactLists[$Count % $Jobs] += @($_) + $Count++ + } + + # Create a job for each sublist of contacts + foreach ($ContactList in $ContactLists.Values) { + Start-Job -ArgumentList $SecondaryTenantCreds, $ContactList -ScriptBlock { + # Create Exchange Online PS session + $SecondaryTenantEOSession = New-EOSession -Credential $args[0] + + # Enter session on secondary tenant + Import-PSSession $SecondaryTenantEOSession + + Update-GalContact -Gal $args[1] + + # Remove session on secondary tenant + Remove-PSSession -Session $SecondaryTenantEOSession + } -InitializationScript { Import-Module 'C:\powershell-scripts\Exchange Online\GalSync.psm1' } + } + + # Wait for all jobs to finish then receive and remove the jobs + Get-Job | Wait-Job | Receive-Job + Get-Job | Remove-Job + } else { + # Create Exchange Online PS session + $SecondaryTenantEOSession = New-EOSession -Credential $SecondaryTenantCreds + + # Enter session on secondary tenant + Import-PSSession $SecondaryTenantEOSession + + Update-GalContact -Gal $Gal + + # Remove session on secondary tenant + Remove-PSSession -Session $SecondaryTenantEOSession + } + } + + end { + # Remove any lingering PSSessions and stop the logging + Get-PSSession | Remove-PSSession + Stop-Transcript + } +} + +function New-EOSession { + <# + .DESCRIPTION + Create a new Exchange Online PowerShell session + + .PARAMETER Credential + The credentials to use for the session. + + .EXAMPLE + New-EOSession -Credential $creds + + .NOTES + Created by Nick Rodriguez + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param( + [Parameter(Mandatory = $true)] + [PSCredential] + $Credential + ) + + New-PSSession -ConfigurationName Microsoft.Exchange -Authentication Basic -AllowRedirection ` + -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $Credential +} + +function Update-GalContact { + <# + .DESCRIPTION + Takes an array of recipients and updates the Gal. + + .PARAMETER Gal + The recipient or list of reipients to update. + + .EXAMPLE + Update-GalContact -Gal $ExternalGal + + .NOTES + Created by Nick Rodriguez + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param( + [PSObject[]]$Gal + ) + + foreach ($Recipient in $Gal) { + # Create a new contact if one doesn't exist + if (Get-MailContact -Identity $Recipient.Name) { + Write-Host "Contact $($Recipient.Name) already exists." + } else { + try { + New-MailContact ` + -ExternalEmailAddress $Recipient.PrimarySmtpAddress ` + -Name $Recipient.Name ` + -FirstName $Recipient.FirstName ` + -LastName $Recipient.LastName ` + -DisplayName $Recipient.DisplayName ` + -Alias $Recipient.Alias + Write-Host "New contact created for $($Recipient.Name)" + } catch { + Write-Host "Error creating new contact for $($Recipient.Name): $_" + } + } + + try { + # Update mail contact properties + Set-MailContact ` + -Identity $Recipient.Name ` + -ExternalEmailAddress $Recipient.PrimarySmtpAddress ` + -Name $Recipient.Name ` + -DisplayName $Recipient.DisplayName ` + -Alias $Recipient.Alias ` + -CustomAttribute1 $Recipient.CustomAttribute1 ` + -CustomAttribute2 $Recipient.CustomAttribute2 ` + -CustomAttribute3 $Recipient.CustomAttribute3 ` + -CustomAttribute4 $Recipient.CustomAttribute4 ` + -CustomAttribute5 $Recipient.CustomAttribute5 ` + -CustomAttribute6 $Recipient.CustomAttribute6 ` + -CustomAttribute7 $Recipient.CustomAttribute7 ` + -CustomAttribute8 $Recipient.CustomAttribute8 ` + -CustomAttribute9 $Recipient.CustomAttribute9 ` + -CustomAttribute10 $Recipient.CustomAttribute10 ` + -CustomAttribute11 $Recipient.CustomAttribute11 ` + -CustomAttribute12 $Recipient.CustomAttribute12 ` + -CustomAttribute13 $Recipient.CustomAttribute13 ` + -CustomAttribute14 $Recipient.CustomAttribute14 ` + -CustomAttribute15 $Recipient.CustomAttribute15 ` + -ExtensionCustomAttribute1 $Recipient.ExtensionCustomAttribute1 ` + -ExtensionCustomAttribute2 $Recipient.ExtensionCustomAttribute2 ` + -ExtensionCustomAttribute3 $Recipient.ExtensionCustomAttribute3 ` + -ExtensionCustomAttribute4 $Recipient.ExtensionCustomAttribute4 ` + -ExtensionCustomAttribute5 $Recipient.ExtensionCustomAttribute5 ` + | Out-Null + + # Update Windows Email Address only if it's populated + if ($Recipient.WindowsLiveID -ne '') { + Set-MailContact -Identity $Recipient.Name -WindowsEmailAddress $Recipient.WindowsLiveID | Out-Null + } + } catch { + Write-Host "Error updating mail contact info for $($Recipient.Name): $_" + } + + try { + # Update contact properties + Set-Contact ` + -Identity $Recipient.Name ` + -FirstName $Recipient.FirstName ` + -LastName $Recipient.LastName ` + -Department $Recipient.Department ` + -Company $Recipient.Company ` + -Phone $Recipient.Phone ` + -HomePhone $Recipient.HomePhone ` + -OtherHomePhone $Recipient.OtherHomePhone ` + -MobilePhone $Recipient.MobilePhone ` + -OtherTelephone $Recipient.OtherTelephone ` + -Pager $Recipient.Pager ` + -Fax $Recipient.Fax ` + -OtherFax $Recipient.OtherFax ` + -Office $Recipient.Office ` + -CountryOrRegion $Recipient.UsageLocation ` + -StreetAddress $Recipient.StreetAddress ` + -City $Recipient.City ` + -StateOrProvince $Recipient.StateOrProvince ` + -PostalCode $Recipient.PostalCode ` + -PostOfficeBox $Recipient.PostOfficeBox ` + -Title $Recipient.Title ` + -Manager $Recipient.Manager ` + -AssistantName $Recipient.AssistantName ` + -Notes $Recipient.Notes ` + | Out-Null + } catch { + Write-Host "Error updating contact info for $($Recipient.Name): $_" + } + } +} \ No newline at end of file diff --git a/Exchange/Get-AllMailboxSizes.ps1 b/Exchange/Get-AllMailboxSizes.ps1 new file mode 100644 index 0000000..60454be --- /dev/null +++ b/Exchange/Get-AllMailboxSizes.ps1 @@ -0,0 +1,25 @@ +$Creds = Get-Credential + +# If you get an Access Denied message but you're a member of Exchange Online Admins, +# make sure you don't have MFA enabled +$ExchangeOnlineSession = New-PSSession -ConfigurationName Microsoft.Exchange ` + -ConnectionUri https://outlook.office365.com/powershell-liveid/ ` + -Credential $Creds -Authentication Basic -AllowRedirection + +Import-PSSession $ExchangeOnlineSession + +$LogDirectory = (New-Item -ItemType Directory "$PSScriptRoot\Logs" -Force).FullName +$Date = (Get-Date).ToString('yyyyMMdd-HHmm') +$CsvPath = "$LogDirectory\$($MyInvocation.MyCommand.Name)-$Date.csv" + +Get-Mailbox -ResultSize Unlimited | + Get-MailboxStatistics | + Select-Object DisplayName, StorageLimitStatus, ItemCount, ` + @{ + name = "TotalItemSize (MB)" + expression = { [math]::Round( ` + ($_.TotalItemSize.ToString().Split("(")[1].Split(" ")[0].Replace(",","")/1MB),2) + } + } | + Sort-Object "TotalItemSize (MB)" -Descending | + Export-CSV $CsvPath -NoTypeInformation \ No newline at end of file diff --git a/Exchange/Set-MSOMailboxAuditing.ps1 b/Exchange/Set-MSOMailboxAuditing.ps1 new file mode 100644 index 0000000..bb76f61 --- /dev/null +++ b/Exchange/Set-MSOMailboxAuditing.ps1 @@ -0,0 +1,91 @@ +# Start a transcript of everything we do +$LogDirectory = (New-Item -ItemType Directory "$PSScriptRoot\Logs" -Force).FullName +$Date = (Get-Date).ToString('yyyyMMdd-HHmm') +$LogPath = "$LogDirectory\$($MyInvocation.MyCommand.Name)-$Date.log" +$ResultsPath = "$LogDirectory\$($MyInvocation.MyCommand.Name)-$Date.csv" + +# Connect to Exchange Online +$Creds = Get-Credential +$ExchangeOnlineSession = New-PSSession -ConfigurationName Microsoft.Exchange ` + -ConnectionUri https://outlook.office365.com/powershell-liveid/ ` + -Credential $Creds -Authentication Basic -AllowRedirection +Import-PSSession $ExchangeOnlineSession + +Start-Transcript -Path $LogPath + +# Audit options +$AuditOwnerOptions = @( + 'Create', 'HardDelete', 'MailboxLogin', 'Move', 'MoveToDeletedItems', + 'SoftDelete', 'Update' +) + +$AuditAdminOptions = @( + 'Copy', 'Create', 'FolderBind', 'HardDelete', 'MessageBind', 'Move', + 'MoveToDeletedItems', 'SendAs', 'SendOnBehalf', 'SoftDelete', 'Update' +) + +$AuditDelegateOptions = @( + 'Create', 'FolderBind', 'HardDelete', 'Move', 'MoveToDeletedItems', + 'SendAs', 'SendOnBehalf', 'SoftDelete', 'Update' +) + +# Keep track of how many accounts we fix +$BadAccounts = 0 + +# Get all users and enable auditing options for their mailboxes +Get-Mailbox -ResultSize Unlimited -RecipientTypeDetails UserMailbox | ForEach-Object { + $Params = @{ 'Identity' = $_.UserPrincipalName } + + # Verify audit options are enabled + if (-not $_.AuditEnabled) { + Write-Output "$($_.UserPrincipalName) - enabling audit." + $Params.Add('AuditEnabled', $true) + } + + # Verify audit owner options are correct + if (Compare-Object -ReferenceObject $_.AuditOwner -DifferenceObject $AuditOwnerOptions) { + Write-Output "$($_.UserPrincipalName) - resetting Audit Owner options." + $Params.Add('AuditOwner', $AuditOwnerOptions) + } + + # Verify audit admin options are correct + if (Compare-Object -ReferenceObject $_.AuditAdmin -DifferenceObject $AuditAdminOptions) { + Write-Output "$($_.UserPrincipalName) - resetting Audit Admin options." + $Params.Add('AuditAdmin', $AuditAdminOptions) + } + + # Verify audit delegate options are correct + if (Compare-Object -ReferenceObject $_.AuditDelegate -DifferenceObject $AuditDelegateOptions) { + Write-Output "$($_.UserPrincipalName) - resetting Audit Delegate options." + $Params.Add('AuditDelegate', $AuditDelegateOptions) + } + + # Update user options if any don't match out settings + if ($Params.Count -gt 1) { + Write-Output "$($_.UserPrincipalName) - setting mailbox options." + try { Set-Mailbox @Params -WhatIf } catch { $_ } + $BadAccounts++ + } +} + +Write-Output "Audit options were set on $BadAccounts mailboxes." + +# Save resulting audit rules of all users to csv +Get-Mailbox -ResultSize Unlimited -RecipientTypeDetails UserMailbox | + Select-Object PrimarySmtpAddress, Name, Audit* | + Export-Csv -Path $ResultsPath -NoTypeInformation + +# Disconnect from Exchange Online +Remove-PSSession $ExchangeOnlineSession +Stop-Transcript + +# Send results via email +$Params = @{ + 'Body' = Get-Content -Path $LogPath | Out-String + 'From' = 'Alerts@company.com' + 'SmtpServer' = 'smtp.company.local' + 'Subject' = 'Enable Mailbox Auditing' + 'To' = 'person@company.com' + 'Attachments' = $ResultsPath +} +Send-MailMessage @Params \ No newline at end of file diff --git a/File Management/Archive-DuplicateFile.ps1 b/File Management/Archive-DuplicateFile.ps1 index ab4f33f..f22bec6 100644 --- a/File Management/Archive-DuplicateFile.ps1 +++ b/File Management/Archive-DuplicateFile.ps1 @@ -31,46 +31,68 @@ function Archive-DuplicateFile { Write-Verbose "Getting all files within [$Dir]..." Get-ChildItem -Path $Dir -File -Recurse -Exclude $DupeDir -Force -ErrorAction Continue | Where-Object { + # Don't search for duplicates within the directory we're archiving to $_.FullName -notlike "$DupeDir\*" } | ForEach-Object { + Write-Host $_.FullName + # Check if the file name exists in our list if ($AllItems.ContainsKey($_.Name)) { + + # Create an object with details about the item we're about to archive $ArchiveFile = New-Object -TypeName psobject -Property @{ Name = $_.Name - Path = $_.FullName + OriginalPath = $_.FullName + ArchivePath = '' LastWriteTime = $_.LastWriteTime + Error = '' } - $CurrLastWriteTime = $AllItems.($_.Name)[0] - $CurrFullName = $AllItems.($_.Name)[1] + $PreviousLastWriteTime = $AllItems.($ArchiveFile.Name)[0] + $PreviousFullName = $AllItems.($ArchiveFile.Name)[1] # If this file is newer than what was previously found, archive old and add this to list - if ($_.LastWriteTime -gt $CurrLastWriteTime) { - Write-Verbose "[$($_.Name)] found in list already, this one is newer, archiving older copy..." + if ($ArchiveFile.LastWriteTime -gt $PreviousLastWriteTime) { + Write-Verbose "[$($ArchiveFile.Name)] found already, this one is newer, archiving [$PreviousFullName]..." - If ($PSCmdlet.ShouldProcess($CurrFullName, 'Archive Item')) { - # Archive the old item and log it - $ArchiveFile.LastWriteTime = $CurrLastWriteTime - $ArchiveFile.Path = $CurrFullName - $ArchiveFile | Export-Csv -Path $Log -Append -NoTypeInformation - Move-Item -Path $CurrFullName -Destination $DupeDir - } + if ($PSCmdlet.ShouldProcess($PreviousFullName, 'Archive Item')) { + # Archive the old item and add the new item to the list + $ArchiveFile.ArchivePath = $PreviousFullName.Replace($Dir, $DupeDir) + try { + New-Item -Path $ArchiveFile.ArchivePath.TrimEnd($_.Name) -ItemType Directory -Force + Move-Item -Path $PreviousFullName -Destination $ArchiveFile.ArchivePath -Force + } catch { + $ArchiveFile.Error = $_.Exception.Message + } + $ArchiveFile.LastWriteTime = $PreviousLastWriteTime + $ArchiveFile.OriginalPath = $PreviousFullName - # Add our new item to the list - $CurrLastWriteTime = $_.LastWriteTime - $CurrFullName = $_.FullName + # Update what we have in our list + $AllItems.($ArchiveFile.Name)[0] = $PreviousLastWriteTime + $AllItems.($ArchiveFile.Name)[1] = $PreviousFullName + } # If this file is older than what was previously found, archive it } else { - Write-Verbose "[$($_.Name)] found in list already, this one has older date, archiving..." - If ($PSCmdlet.ShouldProcess($_.FullName, 'Archive Item')) { - # Archive this item and log it - $ArchiveFile | Export-Csv -Path $Log -Append -NoTypeInformation - Move-Item -Path $_.FullName -Destination $DupeDir + Write-Verbose "[$($ArchiveFile.Name)] found already, this one is older, archiving [$($_.FullName)]..." + + if ($PSCmdlet.ShouldProcess($_.FullName, 'Archive Item')) { + # Archive this item + $ArchiveFile.ArchivePath = $_.FullName.Replace($Dir, $DupeDir) + try { + New-Item -Path $ArchiveFile.ArchivePath.TrimEnd($_.Name) -ItemType Directory -Force + Move-Item -Path $_.FullName -Destination $ArchiveFile.ArchivePath -Force + } catch { + $ArchiveFile.Error = $_.Exception.Message + } } } + + # Log it + $ArchiveFile | Export-Csv -Path $Log -Append -NoTypeInformation + } else { - Write-Verbose "Adding [$($_.Name)] to list..." + # Item is unique so add it to the list $AllItems.Add($_.Name, @($_.LastWriteTime, $_.FullName)) } } diff --git a/File Management/Compress-Log.ps1 b/File Management/Compress-Log.ps1 new file mode 100644 index 0000000..ec1ecc6 --- /dev/null +++ b/File Management/Compress-Log.ps1 @@ -0,0 +1,59 @@ +#Requires -Version 5 +<# +.DESCRIPTION +Compress logs older than given time into an archive. Uses LastWriteTime property to determine age. + +.PARAMETER Path +Path of log files. + +.PARAMETER Extension +Extension of the log files to search for, default is 'log'. + +.PARAMETER DaysOld +How many days old the file must be to get archived. + +.EXAMPLE +./CompressLog.ps1 -Path ~\Downloads\logs -DaysOld 8 +Searches for log files (.log extension) in the given path that are older than 8 days old and compresses them. +#> + +[CmdletBinding(SupportsShouldProcess = $true)] +Param( + [Parameter(Mandatory = $true)] + [ValidateScript({ Test-Path -Path $_ -PathType Container })] + [string]$Path, + + [Parameter(Mandatory = $false)] + [string]$Extension = 'log', + + [Parameter(Mandatory = $false)] + [int]$DaysOld = 7 +) + +begin { + $CompareDate = (Get-Date).AddDays(-$DaysOld) + Write-Verbose "Threshold date: $CompareDate" + + $ArchiveName = "LogsOlderThan-$($CompareDate.ToString('yyyy-MM-dd'))" + Write-Verbose "Archive name: $ArchiveName" +} + +process { + # Get logs older than the compare date + $Files = Get-ChildItem -Path $Path -File -Filter "*.$Extension" | + Where-Object -Property LastWriteTime -LT $CompareDate + + if ($Files.Count) { + Write-Verbose "$($Files.Count) old logs found" + + # Create archive will old logs in it + if ($PSCmdlet.ShouldProcess($ArchiveName, 'New archive')) { + Compress-Archive -Path $Files.FullName -DestinationPath "$Path\$ArchiveName" -Update + } + + # Delete old logs + $Files | Remove-Item + } else { + Write-Verbose 'No old logs found' + } +} \ No newline at end of file diff --git a/Create-CAB.ps1 b/File Management/Create-CAB.ps1 old mode 100755 new mode 100644 similarity index 96% rename from Create-CAB.ps1 rename to File Management/Create-CAB.ps1 index 3857d4b..6f24225 --- a/Create-CAB.ps1 +++ b/File Management/Create-CAB.ps1 @@ -1,63 +1,63 @@ -$project = Read-Host "Folder to convert to a CAB" -$ddf = $project + ".ddf" -$cab = $project + ".cab" - -# delete any preexisting ddf -if (Test-Path $ddf) { - Clear-Content $ddf -} else { - $ddf = New-Item -type file $ddf -} - -# delete any preexisting cab -if (Test-Path $cab) { - Remove-Item $cab -} - -# create the ddf -Add-Content $ddf ".OPTION EXPLICIT" -Add-Content $ddf ".Set CabinetNameTemplate=$cab" -Add-Content $ddf ".Set Cabinet=on" -Add-Content $ddf ".Set Compress=on`n`n" - -# add the manifest and any other files in the top most directory of the project -Get-ChildItem $project -File | ForEach-Object { - $trash, $file = $_.FullName -split $project, 2 - $file = $project + $file - $file = $([char]34) + $file + $([char]34) - - Add-Content $ddf $file -} - -# create a new destination directory for each sub directory -Get-ChildItem $project -Directory -Recurse | ForEach-Object { - - # if the directory has no files, skip it - if ($_.GetFiles().Count) { - - $trash, $folder = $_.FullName -split $project, 2 - $folder = $folder.TrimStart('\') - $folder = $([char]34) + $folder + $([char]34) - - Add-Content $ddf "`n`n.Set DestinationDir=$folder" - - # place the files for each sub directory under its destination directory entry - Get-ChildItem $_.FullName -File | ForEach-Object { - $trash, $file = $_.FullName -split $project, 2 - $file = $project + $file - $file = $([char]34) + $file + $([char]34) - - Add-Content $ddf $file - } - } -} - -# create the cab file -Start-Process MakeCab -ArgumentList "/F ""$ddf""" -Wait - -# clean up -Move-Item "disk1\$cab" . -Remove-Item -Force "disk1" -Remove-Item $ddf -Remove-Item ".\setup.inf" -Remove-Item ".\setup.rpt" +$project = Read-Host "Folder to convert to a CAB" +$ddf = $project + ".ddf" +$cab = $project + ".cab" + +# delete any preexisting ddf +if (Test-Path $ddf) { + Clear-Content $ddf +} else { + $ddf = New-Item -type file $ddf +} + +# delete any preexisting cab +if (Test-Path $cab) { + Remove-Item $cab +} + +# create the ddf +Add-Content $ddf ".OPTION EXPLICIT" +Add-Content $ddf ".Set CabinetNameTemplate=$cab" +Add-Content $ddf ".Set Cabinet=on" +Add-Content $ddf ".Set Compress=on`n`n" + +# add the manifest and any other files in the top most directory of the project +Get-ChildItem $project -File | ForEach-Object { + $trash, $file = $_.FullName -split $project, 2 + $file = $project + $file + $file = $([char]34) + $file + $([char]34) + + Add-Content $ddf $file +} + +# create a new destination directory for each sub directory +Get-ChildItem $project -Directory -Recurse | ForEach-Object { + + # if the directory has no files, skip it + if ($_.GetFiles().Count) { + + $trash, $folder = $_.FullName -split $project, 2 + $folder = $folder.TrimStart('\') + $folder = $([char]34) + $folder + $([char]34) + + Add-Content $ddf "`n`n.Set DestinationDir=$folder" + + # place the files for each sub directory under its destination directory entry + Get-ChildItem $_.FullName -File | ForEach-Object { + $trash, $file = $_.FullName -split $project, 2 + $file = $project + $file + $file = $([char]34) + $file + $([char]34) + + Add-Content $ddf $file + } + } +} + +# create the cab file +Start-Process MakeCab -ArgumentList "/F ""$ddf""" -Wait + +# clean up +Move-Item "disk1\$cab" . +Remove-Item -Force "disk1" +Remove-Item $ddf +Remove-Item ".\setup.inf" +Remove-Item ".\setup.rpt" diff --git a/Create-WSP.ps1 b/File Management/Create-WSP.ps1 old mode 100755 new mode 100644 similarity index 96% rename from Create-WSP.ps1 rename to File Management/Create-WSP.ps1 index b0d5279..222e05d --- a/Create-WSP.ps1 +++ b/File Management/Create-WSP.ps1 @@ -1,66 +1,66 @@ -$project = Read-Host "Folder to convert to a WSP" -$ddf = $project + ".ddf" -$cab = $project + ".cab" - -# delete any preexisting ddf -if (Test-Path $ddf) { - Clear-Content $ddf -} else { - $ddf = New-Item -type file $ddf -} - -# delete any preexisting cab -if (Test-Path $cab) { - Remove-Item $cab -} - -# create the ddf -Add-Content $ddf ".OPTION EXPLICIT" -Add-Content $ddf ".Set CabinetNameTemplate=$cab" -Add-Content $ddf ".Set Cabinet=on" -Add-Content $ddf ".Set Compress=on`n`n" - -# add the manifest and any other files in the top most directory of the project -Get-ChildItem $project -File | ForEach-Object { - $trash, $file = $_.FullName -split $project, 2 - $file = $project + $file - $file = $([char]34) + $file + $([char]34) - - Add-Content $ddf $file -} - -# create a new destination directory for each sub directory -Get-ChildItem $project -Directory -Recurse | ForEach-Object { - - # if the directory has no files, skip it - if ($_.GetFiles().Count) { - - $trash, $folder = $_.FullName -split $project, 2 - $folder = $folder.TrimStart('\') - $folder = $([char]34) + $folder + $([char]34) - - Add-Content $ddf "`n`n.Set DestinationDir=$folder" - - # place the files for each sub directory under its destination directory entry - Get-ChildItem $_.FullName -File | ForEach-Object { - $trash, $file = $_.FullName -split $project, 2 - $file = $project + $file - $file = $([char]34) + $file + $([char]34) - - Add-Content $ddf $file - } - } -} - -# create the cab file -Start-Process MakeCab -ArgumentList "/F ""$ddf""" -Wait - -# clean up -Move-Item "disk1\$cab" . -Remove-Item -Force "disk1" -Remove-Item $ddf -Remove-Item ".\setup.inf" -Remove-Item ".\setup.rpt" - -# rename to wsp -Rename-Item $cab ($project + ".wsp") +$project = Read-Host "Folder to convert to a WSP" +$ddf = $project + ".ddf" +$cab = $project + ".cab" + +# delete any preexisting ddf +if (Test-Path $ddf) { + Clear-Content $ddf +} else { + $ddf = New-Item -type file $ddf +} + +# delete any preexisting cab +if (Test-Path $cab) { + Remove-Item $cab +} + +# create the ddf +Add-Content $ddf ".OPTION EXPLICIT" +Add-Content $ddf ".Set CabinetNameTemplate=$cab" +Add-Content $ddf ".Set Cabinet=on" +Add-Content $ddf ".Set Compress=on`n`n" + +# add the manifest and any other files in the top most directory of the project +Get-ChildItem $project -File | ForEach-Object { + $trash, $file = $_.FullName -split $project, 2 + $file = $project + $file + $file = $([char]34) + $file + $([char]34) + + Add-Content $ddf $file +} + +# create a new destination directory for each sub directory +Get-ChildItem $project -Directory -Recurse | ForEach-Object { + + # if the directory has no files, skip it + if ($_.GetFiles().Count) { + + $trash, $folder = $_.FullName -split $project, 2 + $folder = $folder.TrimStart('\') + $folder = $([char]34) + $folder + $([char]34) + + Add-Content $ddf "`n`n.Set DestinationDir=$folder" + + # place the files for each sub directory under its destination directory entry + Get-ChildItem $_.FullName -File | ForEach-Object { + $trash, $file = $_.FullName -split $project, 2 + $file = $project + $file + $file = $([char]34) + $file + $([char]34) + + Add-Content $ddf $file + } + } +} + +# create the cab file +Start-Process MakeCab -ArgumentList "/F ""$ddf""" -Wait + +# clean up +Move-Item "disk1\$cab" . +Remove-Item -Force "disk1" +Remove-Item $ddf +Remove-Item ".\setup.inf" +Remove-Item ".\setup.rpt" + +# rename to wsp +Rename-Item $cab ($project + ".wsp") diff --git a/File Management/Get-DirectoryLargerThan.ps1 b/File Management/Get-DirectoryLargerThan.ps1 new file mode 100644 index 0000000..148d42c --- /dev/null +++ b/File Management/Get-DirectoryLargerThan.ps1 @@ -0,0 +1,64 @@ +begin { + $RootDirectory = '\\server\users' + [long] $Script:Threshold = 5GB + + function Format-Size { + param ( + [Parameter(Mandatory = $true)] + [long] $Size + ) + + if ($Threshold -gt 1GB) { + "{0:N2}" -f ($Size / 1GB) + ' GB' + } else { + "{0:N2}" -f ($Size) + ' MB' + } + } + + function Measure-Content { + param ( + [Parameter(Mandatory = $true)] + [string] $DirectoryPath + ) + + $Contents = Get-ChildItem $DirectoryPath -Recurse -Force -ErrorAction Continue | Where-Object { $_.PSIsContainer -eq $false } + [long] ($Contents | Measure-Object -Property Length -Sum | Select-Object Sum).Sum + } + + + function Measure-Directory { + param ( + [Parameter(Mandatory = $true)] + [string] $RootDirectory + ) + + Write-Output "Measuring children of $RootDirectory`n`n" + + $LargeDirectories = @() + + Get-ChildItem $RootDirectory | Where-Object { $_.PSIsContainer -eq $true } | ForEach-Object { + Write-Output "Measuring $_" + + [long] $Size = Measure-Content $_.FullName + + if ($Size -gt $Threshold) { + $LargeDirectories += New-Object psobject -Property @{ + Size = Format-Size $Size + Path = $_.FullName + Directory = $_.Name + } + } + } + + $Script:LargeDirectories + } +} + +process { + Start-Transcript ".\logs\DirectoryLargerThan$(Format-Size $Threshold)-$(Get-Date -Format yyyy-MM-dd-HHmm).txt" + Measure-Directory $RootDirectory + Write-Output "`n`n`n`nThere are $($Results.Count) directories larger than $(Format-Size $Threshold)`n" + $LargeDirectories | Format-Table -AutoSize +} + +end { Stop-Transcript } \ No newline at end of file diff --git a/File Management/Get-DuplicateFile.ps1 b/File Management/Get-DuplicateFile.ps1 new file mode 100644 index 0000000..c5c21c0 --- /dev/null +++ b/File Management/Get-DuplicateFile.ps1 @@ -0,0 +1,34 @@ +function Get-DuplicateFile { + [CmdletBinding(SupportsShouldProcess = $true)] + Param ( + [Parameter( + Mandatory = $false, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true + )] + [ValidateScript({ + if (Test-Path -Path $_ -PathType Container) { + $true + } else { + throw "[$_] is not a valid directory." + $false + } + })] + [string[]]$Directory = '.' + ) + + begin { + $AllItems = @() + } + + process { + foreach ($Dir in $Directory) { + Write-Verbose "Getting all files within [$Dir]..." + $AllItems += Get-ChildItem -Path $Dir -File -Recurse -Force -ErrorAction Continue + } + + $AllItems | Group-Object -Property Name | Where-Object { $_.Count -gt 1 } | ForEach-Object { + $_.Group | ForEach-Object { $_ } + } + } +} \ No newline at end of file diff --git a/File Management/Get-FilesModifyDate.ps1 b/File Management/Get-FilesModifyDate.ps1 new file mode 100644 index 0000000..292ecad --- /dev/null +++ b/File Management/Get-FilesModifyDate.ps1 @@ -0,0 +1,19 @@ +# Get the path this script is running from +$ScriptPath = Split-Path $MyInvocation.MyCommand.Path -Parent + +# Array to store our info in +$Output = @() + +# Servers we're checking +$Servers = @('server01', 'server02') + +# Path we're checking +$Path = 'ShareName' + +# Loop through each server +ForEach ($Server in $Servers) { + $Output += Get-ChildItem "\\$Server\$Path" -Recurse | Where { $_.Extension -eq '.bak' } +} + +# Create csv at script root with output +$Output | select FullName, LastWriteTime | Export-Csv "$ScriptPath\Backups.csv" -NoTypeInformation \ No newline at end of file diff --git a/File Management/Move-EmptyFolder.ps1 b/File Management/Move-EmptyFolder.ps1 new file mode 100644 index 0000000..2f86881 --- /dev/null +++ b/File Management/Move-EmptyFolder.ps1 @@ -0,0 +1,10 @@ +$Folders = Get-ChildItem '\\server\users' + +$EmptyDir = New-Item -ItemType Directory '.\empty' -Force + +foreach ($Folder in $Folders) { + if ((Get-ChildItem $Folder.FullName).Count -eq 0) { + Write-Host "moving: $Folder" + Move-Item $Folder.FullName $EmptyDir + } +} \ No newline at end of file diff --git a/File Management/Move-GFRClientFiles.ps1 b/File Management/Move-GFRClientFiles.ps1 new file mode 100644 index 0000000..1df3e0c --- /dev/null +++ b/File Management/Move-GFRClientFiles.ps1 @@ -0,0 +1,91 @@ +[CmdletBinding(SupportsShouldProcess = $true)] +param( + [Parameter(Mandatory = $true)] + [ValidateScript({ + # Validate the path + if (Test-Path -Path $_) { + # Validate the directory contains files + if ((Get-ChildItem -Path $_ -File).Count) { + $true + } else { + throw "Docs directory is empty" + } + } else { + throw "Invalid path given: $_" + } + })] + [string] $DocsPath, + + [Parameter(Mandatory = $true)] + [ValidateScript({ + # Validate the path + if (Test-Path -Path $_) { + # Validate the file has the .xml extension + if ((Get-Item -Path $_).Extension -eq '.xml') { + $true + } else { + throw "HitList given does not have the '.xml' extension" + } + } else { + throw "Invalid path given: $_" + } + })] + [string] $HitListPath, + + [Parameter(Mandatory = $false)] + [switch] $Copy +) + +begin { + Write-Verbose "Gathering files within $DocsPath..." + $Docs = Get-ChildItem -Path $DocsPath -File + $DocsCount = $Docs.Count + Write-Verbose "$DocsCount files found." + + Write-Verbose "Importing HitList from $HitListPath (this may take a while depending on the size of the file)..." + $HitList = [xml] (Get-Content -Path $HitListPath) + $HitListCount = $HitList.dcs.dc.Count + Write-Verbose "$HitListCount entries found." + + $HitListDictionary = @{} +} + +process { + $Counter = 1 + foreach ($Doc in $HitList.dcs.dc) { + $ClientName = $Doc.i1.'#cdata-section' + $FileName = "$($Doc.doc_name).$($Doc.tp)" + + Write-Progress -Activity "Processing HitList..." -Status "($Counter / $HitListCount)" ` + -PercentComplete ($Counter / $HitListCount * 100) -CurrentOperation "$ClientName - $FileName" + + $HitListDictionary.Add($FileName, $ClientName) + $Counter++ + } + + if ($Copy) { Write-Verbose "Script run in 'Copy' mode - source files will remain intact." } + + $Counter = 1 + foreach ($File in $Docs) { + try { + $Client = $HitListDictionary.($File.Name) + $ClientDirectory = "$DocsPath\$Client" + + Write-Progress -Activity "Processing Docs..." -Status "($Counter / $DocsCount)" ` + -PercentComplete ($Counter / $DocsCount * 100) -CurrentOperation $File.FullName + + if (-not (Test-Path -Path $ClientDirectory)) { + New-Item -Path $ClientDirectory -ItemType Directory + } + if ($Copy) { + Copy-Item -Path $File.FullName -Destination $ClientDirectory + } else { + Move-Item -Path $File.FullName -Destination $ClientDirectory + } + } catch { + Write-Error -Message "Error processing $($File.FullName): $($_.Exception.Message)" + } finally { + $Counter++ + } + } +} \ No newline at end of file diff --git a/File Management/Rename-Season.ps1 b/File Management/Rename-Season.ps1 new file mode 100644 index 0000000..9fba26c --- /dev/null +++ b/File Management/Rename-Season.ps1 @@ -0,0 +1,13 @@ +[CmdletBinding()] +Param( + [ValidateScript({ Test-Path -Path $_ -PathType Container })] + [string]$Path +) + +Get-ChildItem -Path $Path | ForEach-Object { + $NewName = $_.Name -replace 'Season ', 'S' -replace ', Episode ', 'E' + $Season = 'S' + "{0:D2}" -f [int]($NewName -split 'S', 2 -split 'E', 2)[1] + $Episode = 'E' + "{0:D2}" -f [int]($NewName -split 'E', 2 -split ' ', 2)[1] + $NewName = "$Season$Episode - $(($NewName -split ' ', 2)[1])" + Rename-Item -Path $_.FullName -NewName $NewName +} \ No newline at end of file diff --git a/Get-BitcoinPrice.ps1 b/Fun/Get-BitcoinPriceChart.ps1 similarity index 88% rename from Get-BitcoinPrice.ps1 rename to Fun/Get-BitcoinPriceChart.ps1 index 9c02b8e..a6f7f2e 100644 --- a/Get-BitcoinPrice.ps1 +++ b/Fun/Get-BitcoinPriceChart.ps1 @@ -1,6 +1,6 @@ $PriceData = [System.Collections.Specialized.OrderedDictionary]@{} -$Data = (Invoke-RestMethod -Method Get -Uri 'https://api.coindesk.com/v1/bpi/historical/close.json' ` +$Data = (Invoke-RestMethod -Method Get -Uri 'http://api.coindesk.com/v1/bpi/historical/close.json' ` -ContentType 'application/json').bpi | Out-String $Data.Trim() -split "`r`n" | ForEach-Object { @@ -38,10 +38,10 @@ $Chart.Series['Price'].ChartArea = "ChartArea1" $Chart.Series['Price'].Color = "#62B5CC" $Chart.Series['Price'].Points.DataBindXY($PriceData.Keys, $PriceData.Values) -# Display the chart on a form +# Display the chart on a form $Chart.Anchor = [System.Windows.Forms.AnchorStyles]::Bottom -bor - [System.Windows.Forms.AnchorStyles]::Right -bor - [System.Windows.Forms.AnchorStyles]::Top -bor + [System.Windows.Forms.AnchorStyles]::Right -bor + [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left $Form = New-Object Windows.Forms.Form $Form.Text = "Bitcoin Price Chart" diff --git a/Fun/Get-FeaturedBrew.ps1 b/Fun/Get-FeaturedBrew.ps1 new file mode 100644 index 0000000..41c04b5 --- /dev/null +++ b/Fun/Get-FeaturedBrew.ps1 @@ -0,0 +1,14 @@ +# http://www.brewerydb.com/developers/docs +$BreweryDB = 'http://api.brewerydb.com/v2' +$APIKey = 'cd4f34c5b35a1a2c4c76dcad8c5253bc' +$Request = 'features' + +$Result = Invoke-RestMethod -Method Get -Uri "$BreweryDB/$Request/?key=$APIKey" -ContentType 'application/json' +$Result.data | Select-Object Brewery, Beer | ForEach-Object { + New-Object -TypeName psobject -Property @{ + BreweryName = $_.Brewery.Name + #BreweryDescription = $_.Brewery.Description + BeerName = $_.Beer.Name + #BeerDescription = $_.Beer.Description + } +} | Out-GridView diff --git a/Fun/Get-Weather.ps1 b/Fun/Get-Weather.ps1 new file mode 100644 index 0000000..72a5a31 --- /dev/null +++ b/Fun/Get-Weather.ps1 @@ -0,0 +1,139 @@ +#Requires -Version 3 + +$UtilityName = 'Archaic Weather Gathering Utility - Nick Rodriguez' + +# Default values +$City = 'Asheville' +$Country = 'United States' +$ZipCode = '28803' +$Days = '6' + +function Get-Weather { + param( + [Parameter(Mandatory = $true)] + [string]$City, + + [Parameter(Mandatory = $true)] + [string]$Country + ) + + $WebService = New-WebServiceProxy -Uri 'http://www.webservicex.net/globalweather.asmx?WSDL' + return ([xml]$WebService.GetWeather($City, $Country)).CurrentWeather +} + +function Get-Forecast { + param( + [Parameter(Mandatory = $true)] + [string]$ZipCode, + + [Parameter(Mandatory = $true)] + [int]$Days + ) + + $URI = 'http://www.weather.gov/forecasts/xml/DWMLgen/wsdl/ndfdXML.wsdl' + $Proxy = New-WebServiceProxy -uri $URI -namespace WebServiceProxy + $LatLonList = ([xml]$Proxy.LatLonListZipCode($ZipCode)).dwml.LatLonlist -split ',' + $Lat = $LatLonList[0] + $Lon = $LatLonList[1] + $Date = Get-Date -UFormat %Y-%m-%d + $Format = "Item24hourly" + [xml]$Weather = $Proxy.NDFDgenByDay($Lat, $Lon, $Date, $Days, 'e', $Format) + + + $Forecast = for ($Day = 0; $Day -le $Days - 1; $Day ++) { + New-Object PSObject -Property @{ + Date = ((Get-Date).AddDays($i)).ToString("MM/dd/yyyy") ; + MaxTemp = $Weather.dwml.data.parameters.temperature[0].Value[$Day] ; + MinTemp = $Weather.dwml.data.parameters.temperature[1].Value[$Day] ; + Summary = $Weather.dwml.data.parameters.weather."weather-conditions"[$Day]."Weather-summary" + } + } + + return $Forecast | Format-Table -Property Date, MaxTemp, MinTemp, Summary -AutoSize +} + +function Load-MenuSystem { + [int]$MenuLevel1 = 0 + [int]$MenuLevel2 = 0 + [boolean]$xValidSelection = $false + + while ($MenuLevel1 -lt 1 -or $MenuLevel1 -gt 3) { + Clear-Host + + # Present the Menu Options + Write-Host "`n`t$UtilityName`n" -ForegroundColor Magenta + Write-Host "`t`tPlease select an option`n" -ForegroundColor Cyan + Write-Host "`t`t`t1. Current Weather" -ForegroundColor Cyan + Write-Host "`t`t`t2. Forecast" -ForegroundColor Cyan + Write-Host "`t`t`t3. Exit`n" -ForegroundColor Cyan + # Retrieve the response from the user + [int]$MenuLevel1 = Read-Host "`t`tEnter Menu Option Number" + if ( $MenuLevel1 -lt 1 -or $MenuLevel1 -gt 3 ) { + Write-Host "`tPlease select one of the options available.`n" -ForegroundColor Red + Start-Sleep -Seconds 1 + } + } + + switch ($MenuLevel1){ # User has selected a valid entry.. load next menu + 1 { + while ($MenuLevel2 -lt 1 -or $MenuLevel2 -gt 4) { + Clear-Host + + # Present the Menu Options + Write-Host "`n`t$UtilityName`n" -ForegroundColor Magenta + Write-Host "`t`tCurrent weather for $City, $Country`n" -ForegroundColor Cyan + Write-Host "`t`t`t1. Refresh" -ForegroundColor Cyan + Write-Host "`t`t`t2. Change City" -ForegroundColor Cyan + Write-Host "`t`t`t3. Change Country" -ForegroundColor Cyan + Write-Host "`t`t`t4. Go to Main Menu`n" -ForegroundColor Cyan + [int]$MenuLevel2 = Read-Host "`t`tEnter Menu Option Number" + if( $MenuLevel2 -lt 1 -or $MenuLevel2 -gt 4 ) { + Write-Host "`tPlease select one of the options available.`n" -ForegroundColor Red + Start-Sleep -Seconds 1 + } + } + switch ($MenuLevel2) { + 1 { + Get-Weather $City $Country + pause + } + 2 { $City = Read-Host "`n`tEnter a city name" } + 3 { $Country = Read-Host "`n`tEnter a country name" } + default { break } + } + } + + 2 { + while ($MenuLevel2 -lt 1 -or $MenuLevel2 -gt 4) { + Clear-Host + + # Present the Menu Options + Write-Host "`n`t$UtilityName`n" -Fore Magenta + Write-Host "`t`t$Days day forecast for area code $ZipCode`n" -Fore Cyan + Write-Host "`t`t`t1. Refresh" -Fore Cyan + Write-Host "`t`t`t2. Change Zip Code" -Fore Cyan + Write-Host "`t`t`t3. Change Number of Days" -Fore Cyan + Write-Host "`t`t`t4. Go to Main Menu`n" -Fore Cyan + [int]$MenuLevel2 = Read-Host "`t`tEnter Menu Option Number" + } + if( $MenuLevel2 -lt 1 -or $MenuLevel2 -gt 4 ){ + Write-Host "`tPlease select one of the options available.`n" -Fore Red; Start-Sleep -Seconds 1 + } + Switch ($MenuLevel2) { + 1 { + Get-Forecast $ZipCode $Days + pause + } + 2 { $ZipCode = Read-Host "`n`tEnter a zip code" } + 3 { $Days = Read-Host "`n`tEnter the number of days to forecast" } + default { break } + } + } + + default { exit } + } + + Load-MenuSystem +} + +Load-MenuSystem \ No newline at end of file diff --git a/Fun/Get-XMas.ps1 b/Fun/Get-XMas.ps1 new file mode 100644 index 0000000..5301113 --- /dev/null +++ b/Fun/Get-XMas.ps1 @@ -0,0 +1,69 @@ +# inspired by: +# http://forums.microsoft.com/TechNet-FR/ShowPost.aspx?PostID=2555221&SiteID=45 +$notes = @' + 4A4 4A4 2A4 4A4 4A4 2A4 4A4 4C4 4F3 8G3 1A4 + 4Bb4 4Bb4 4Bb4 8Bb4 4Bb4 4A4 4A4 8A4 8A4 4A4 4G3 4G3 4A4 2G3 2C4 + 4A4 4A4 2A4 4A4 4A4 2A4 4A4 4C4 4F3 4G3 1A4 4Bb4 4Bb4 4Bb4 4Bb4 + 4Bb4 4A4 4A4 8A4 8A4 4C4 4C4 4Bb4 4G3 1F3 4C3 4A4 4G3 4F3 2C3 8C3 8C3 + 4C3 4A4 4G3 4F3 1D3 4D3 4Bb4 4A4 4G3 1E3 4C4 4C4 4Bb4 4G3 + 1A4 4C3 4A4 4G3 4F3 1C3 4C3 4A4 4G3 4F3 1D3 + 4D3 4Bb3 4A4 4G3 4C4 4C4 4C4 8C4 8C4 4D4 4C4 4Bb4 4G3 4F3 2C4 4A4 4A4 2A4 + 4A4 4A4 2A4 4A4 4C4 4C3 8G3 1A4 4Bb4 4Bb4 4Bb4 8Bb4 4Bb4 4A4 4A4 8A4 8A4 + 4A4 4G3 4G3 4A4 2G3 2C4 4A4 4A4 2A4 4A4 4A4 2A4 4A4 4C4 4F3 8G3 + 1A4 4Bb4 4Bb4 4Bb4 4Bb4 4Bb4 4A4 4A4 8A4 8A4 4C4 4C4 4Bb4 4G3 1F3 +'@ + +# Note is given by fn=f0 * (a)^n +# a is the twelth root of 2 +# n is the number of half steps from f0, positive or negative +# f0 used here is A4 at 440 Hz + +$StandardDuration = 1000 +$f0 = 440 +$a = [math]::pow(2,(1/12)) # Twelth root of 2 + +function Get-Frequency([string]$note) +{ + # n is the number of half steps from the fixed note + $null = $note -match '([A-G#]{1,2})(\d+)' + $octave = ([int] $matches[2]) - 4; + $n = $octave * 12 + ( Get-HalfSteps $matches[1] ); + $f0 * [math]::Pow($a, $n); +} + +function Get-HalfSteps([string]$note) +{ + switch($note) + { + 'A' { 0 } + 'A#' { 1 } + 'Bb' { 1 } + 'B' { 2 } + 'C' { 3 } + 'C#' { 4 } + 'Db' { 4 } + 'D' { 5 } + 'D#' { 6 } + 'Eb' { 6 } + 'E' { 7 } + 'F' { 8 } + 'F#' { 9 } + 'Gb' { 9 } + 'G' { 10 } + 'G#' { 11 } + 'Ab' { 11 } + } +} + +$notes.Split(' ') | ForEach-Object { + + if ($_ -match '(\d)(.+)') + { + $duration = $StandardDuration / ([int]$matches[1]) + $playNote = $matches[2] + $freq = Get-Frequency $playNote + + [console]::Beep( $freq, $duration) + Start-Sleep -Milliseconds 50 + } +} \ No newline at end of file diff --git a/Fun/Get-nVidiaDriver.ps1 b/Fun/Get-nVidiaDriver.ps1 new file mode 100644 index 0000000..e8149b3 --- /dev/null +++ b/Fun/Get-nVidiaDriver.ps1 @@ -0,0 +1,140 @@ +# Most logic taken from https://github.com/ElPumpo/TinyNvidiaUpdateChecker/blob/master/TinyNvidiaUpdateChecker/MainConsole.cs + +$osVersion = [Environment]::OSVersion.Version.ToString() +$is64Bit = [Environment]::Is64BitOperatingSystem + +switch ($osVersion) { + { $_ -like '10.0*' } { + $winVer = '10' + + if ($is64Bit) { + $osId = 57 + } + else { + $osId = 56 + } + } + + { $_ -like '6.3*' } { + $winVer = '8.1' + + if ($is64Bit) { + $osId = 41 + } + else { + $osId = 40 + } + } + + { $_ -like '6.2*' } { + $winVer = '8' + + if ($is64Bit) { + $osId = 28 + } + else { + $osId = 27 + } + } + + { $_ -like '6.1*' } { + $winVer = '7' + + if ($is64Bit) { + $osId = 19 + } + else { + $osId = 18 + } + } +} + +Write-Verbose "Windows $winVer version $osVersion" +Write-Verbose "64-Bit: $is64Bit" + +$langId = switch (Get-Culture) { + 'en-US' { 1 } + 'en-GB' { 2 } + 'zh-CHS' { 5 } + 'zh-CHT' { 6 } + 'ja-JP' { 7 } + 'ko-KR' { 8 } + 'de-DE' { 9 } + 'es-ES' { 10 } + 'fr-FR' { 12 } + 'it-IT' { 13 } + 'pl-PL' { 14 } + 'pt-BR' { 15 } + 'ru-RU' { 16 } + 'tr-TR' { 19 } + default { 17 } +} + +Write-Verbose "Language ID: $langId" + +foreach ($gpu in Get-CimInstance -ClassName Win32_VideoController) { + Write-Verbose $gpu.Description + + if ($gpu.Description.Split() -contains 'nvidia') { + $gpuName = $gpu.Description.Trim() + $offlineGpuVersion = $gpu.DriverVersion + } + elseif ($gpu.PNPDeviceID.Split() -contains 'ven_10de') { + Get-CimInstance -ClassName Win32_SystemEnclosure | ForEach-Object { + if ($_.ChassisTypes -eq 3) { + $gpuName = "GTX" + } + else { + $gpuName = "GTX M" + } + } + } +} + +if (-not $gpuName) { + Write-Warning "No nVidia GPU found" + exit +} + +if ($gpuName -contains 'M') { + $psId = 99 + $pfId = 758 +} +else { + $psId = 98 + $pfId = 756 +} + +$gpuUrl = "http://www.nvidia.com/Download/processDriver.aspx?psid=$psID&pfid=$pfID&rpf=1&osid=$osId&lid=$langID&ctk=0" +$processUrl = Invoke-WebRequest $gpuUrl | Select-Object -ExpandProperty Content + +$objXmlHttp = New-Object -ComObject MSXML2.ServerXMLHTTP +$objXmlHttp.Open("GET", $processUrl, $False) +$objXmlHttp.Send() +$response = $objXmlHttp.responseText +$html = New-Object -Com "HTMLFile" +$html.IHTMLDocument2_write($response) + +$version = $html.getElementById("tdVersion").innerText.Split(' ')[0] +$releaseDate = $html.getElementById("tdReleaseDate").innerText.Split(' ')[0].Split('.') +$friendlyReleaseDate = Get-Date -Year $releaseDate[0] -Month $releaseDate[1] -Day $releaseDate[2] -Format D +$releaseNotes = $html.getElementsByTagName('a') | Where-Object href -like "*release-notes.pdf*" | Select-Object -ExpandProperty href +$releaseDescription = $html.getElementById("tab1_content").innerText +$confirmUrl = $html.getElementsByTagName('a') | Where-Object href -like "*/content/DriverDownload-March2009/*" | ForEach-Object { + 'http://www.nvidia.com/' + $_.pathname + $_.search +} + +$objXmlHttp.Open("GET", $confirmUrl, $False) +$objXmlHttp.Send() +$response = $objXmlHttp.responseText +$html = New-Object -Com "HTMLFile" +$html.IHTMLDocument2_write($response) + +$downloadUrl = $html.getElementsByTagName('a') | Where-Object href -like "*download.nvidia*" | Select-Object -ExpandProperty href + + + +Write-Output "Download URL: $downloadUrl" +Write-Output "Release notes: $releaseNotes" +Write-Output "Offline version: $offlineGpuVersion" +Write-Output "Online version: $version released on $friendlyReleaseDate" diff --git a/Fun/PlayingCards.ps1 b/Fun/PlayingCards.ps1 new file mode 100644 index 0000000..2206d98 --- /dev/null +++ b/Fun/PlayingCards.ps1 @@ -0,0 +1,201 @@ +#requires -Version 5.0 + +class Card { + [CardCollection]$CardCollection + [CardSuit]$CardSuit + [CardRank]$CardRank + [string]$CardName + [string]$CardImage + + # Constructor + Card() { + $this.CardSuit = [CardSuit]([Enum]::GetValues([CardSuit]) | Get-Random) + $this.CardRank = [CardRank]([Enum]::GetValues([CardRank]) | Get-Random) + $this.CardName = $this.PrintName() + $this.CardImage = $this.PrintImage() + } + + # Constructor + Card([CardSuit]$CardSuit, [CardRank]$CardRank) { + [CardSuit]$this.CardSuit = $CardSuit + [CardRank]$this.CardRank = $CardRank + $this.CardName = $this.PrintName() + $this.CardImage = $this.PrintImage() + } + + # Constructor + Card([CardCollection]$CardCollection, [CardSuit]$CardSuit, [CardRank]$CardRank) { + [CardCollection]$this.CardCollection = $CardCollection + [CardSuit]$this.CardSuit = $CardSuit + [CardRank]$this.CardRank = $CardRank + $this.CardName = $this.PrintName() + $this.CardImage = $this.PrintImage() + } + + hidden [string] PrintName() { + return "$($this.CardRank) of $($this.CardSuit)s" + } + + hidden [string] PrintImage() { + $Rank = switch ($this.CardRank) { + Joker { 'j' } + Ace { 'A' } + Two { '2' } + Three { '3' } + Four { '4' } + Five { '5' } + Six { '6' } + Seven { '7' } + Eight { '8' } + Nine { '9' } + Ten { '10' } + Jack { 'J' } + Queen { 'Q' } + King { 'K' } + } + + $Suit = switch ($this.CardSuit) { + Spade { 'â™ ' } + Club { '♣' } + Heart { '♥' } + Diamond { '♦' } + } + + return "$Rank$Suit" + } +} + +class CardCollection { + [string]$Name = 'Deck' + [System.Collections.ArrayList]$Cards = (New-Object System.Collections.ArrayList) + + CardCollection() { } + + CardCollection([string]$Name) { + [string]$this.Name = $Name + } + + CardCollection([string]$Name, [bool]$Jokers) { + [string]$this.Name = $Name + $this.NewStandardDeck($Jokers) + } + + CardCollection([bool]$Jokers) { + $this.NewStandardDeck($Jokers) + } + + CardCollection([string]$Name, [int]$DeckCount) { + [string]$this.Name = $Name + for ($i = 0; $i -lt $DeckCount; $i++) { + $this.NewStandardDeck($false) + } + } + + CardCollection([string]$Name, [int]$DeckCount, [bool]$Jokers) { + [string]$this.Name = $Name + for ($i = 0; $i -lt $DeckCount; $i++) { + $this.NewStandardDeck($Jokers) + } + } + + CardCollection([int]$DeckCount, [bool]$Jokers) { + for ($i = 0; $i -lt $DeckCount; $i++) { + $this.NewStandardDeck($Jokers) + } + } + + NewStandardDeck([bool]$Jokers) { + foreach ($CardSuit in [enum]::GetValues([CardSuit])) { + foreach ($CardRank in [enum]::GetValues([CardRank])) { + if ($Jokers -or [int]$CardRank -gt 0) { + $this.Push([Card]::new($this, $CardSuit, $CardRank)) + } + } + } + + $this.Shuffle() + } + + [void] Clear() { + $this.Cards = New-Object System.Collections.ArrayList + } + + [void] Push([Card]$Card) { + Write-Verbose "Adding $($Card.PrintImage()) to $($this.Name)." + $Card.CardCollection = $this + $this.Cards.Add($Card) + } + + [Card] Pop() { + $Card = $this.Cards[-1] + Write-Verbose "Removing $($Card.PrintImage()) from $($this.Name)." + $Card.CardCollection = $null + $this.Cards.Remove($Card) + return $Card + } + + [void] Shuffle() { + $this.Cards = $this.Cards | Sort-Object { Get-Random } + } + + [string[]] PrintCards() { + return ($this.Cards.GetEnumerator() | ForEach-Object { $_.PrintImage() }) -join ', ' + } +} + +class Player { + [CardCollection]$Hand = [CardCollection]::new() + [bool]$Active + [int]$Player + + Player([int]$Player) { + [int]$this.Player = $Player + } + + [void] DrawHand([CardCollection]$Deck, [int]$HandMax) { + while ($this.Hand.Cards.Count -lt $HandMax) { + $this.Hand.Push($Deck.Pop()) + } + } +} + +class Game { + [int]$ActivePlayer + [CardCollection]$Deck + [Player[]]$Players + [int]$HandSize + + Game([int]$PlayerCount, [int]$HandSize) { + $this.Deck = [CardCollection]::new($false) + + for ($i = 0; $i -lt $PlayerCount; $i++) { + $this.Players += [Player]::new($i) + } + + $this.Players.DrawHand($this.Deck, $HandSize) + } +} + +enum CardSuit { + Spade = 0 + Club = 1 + Heart = 2 + Diamond = 3 +} + +enum CardRank { + Joker = 0 + Ace = 1 + Two = 2 + Three = 3 + Four = 4 + Five = 5 + Six = 6 + Seven = 7 + Eight = 8 + Nine = 9 + Ten = 10 + Jack = 11 + Queen = 12 + King = 13 +} \ No newline at end of file diff --git a/Send-CatFactMessage.ps1 b/Fun/Send-CatFactMessage.ps1 old mode 100755 new mode 100644 similarity index 63% rename from Send-CatFactMessage.ps1 rename to Fun/Send-CatFactMessage.ps1 index 59406ac..88ef858 --- a/Send-CatFactMessage.ps1 +++ b/Fun/Send-CatFactMessage.ps1 @@ -1,34 +1,45 @@ -function Send-CatFactMessage { - <# - .SYNOPSIS - Send a cat fact to users on a computer. - .DESCRIPTION - Send a random cat fact to any number of computers and all users or a specific user. Supports credential passing. - .EXAMPLE - Send-CatFactMessage -PlayAudio - Sends cat fact message to all users on localhost and outputs fact through speakers. - .EXAMPLE - Get-ADComputer -Filter * | Send-CatFactMessage -UserName JDoe -Credential (Get-Credential) - Send cat fact to jDoe on all AD computers. Prompt user for credentials to run command with. - .EXAMPLE - Send-CatFactMessage -ComputerName pc1, pc2, pc3 - Send cat fact to all users on provided computer names. - .PARAMETER ComputerName - The computer name to execute against. Default is local computer. - .PARAMETER UserName - The name the user to display the message to. Default is all users. - .PARAMETER PlayAudio - Use Windows Speech Synthesizer to output the fact using text to speech. - .PARAMETER Credential - The credential object to execute the command with. - #> - +function Send-CatFactMessage { + <# + .SYNOPSIS + Send a cat fact to users on a computer. + + .DESCRIPTION + Send a random cat fact to any number of computers and all users or a specific user. Supports credential passing. + + .EXAMPLE + Send-CatFactMessage -PlayAudio + Sends cat fact message to all users on localhost and outputs fact through speakers. + + .EXAMPLE + Get-ADComputer -Filter * | Send-CatFactMessage -UserName JDoe -Credential (Get-Credential) + Send cat fact to jDoe on all AD computers. Prompt user for credentials to run command with. + + .EXAMPLE + Send-CatFactMessage -ComputerName pc1, pc2, pc3 + Send cat fact to all users on provided computer names. + + .PARAMETER ComputerName + The computer name to execute against. Default is local computer. + + .PARAMETER UserName + The name the user to display the message to. Default is all users. + + .PARAMETER PlayAudio + Use Windows Speech Synthesizer to output the fact using text to speech. + + .PARAMETER DisplaySeconds + Seconds to display the message on the user's screen. Default is 0, which waits for the user to click a button. + + .PARAMETER Credential + The credential object to execute the command with. + #> + [CmdletBinding(SupportsShouldProcess = $true)] param ( - [Parameter( - Mandatory = $false, - ValueFromPipeline = $true, - ValueFromPipelineByPropertyName = $true + [Parameter( + Mandatory = $false, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true )] [string[]]$ComputerName = $env:COMPUTERNAME, @@ -38,42 +49,57 @@ [Parameter(Mandatory = $false)] [switch]$PlayAudio, + [Parameter(Mandatory = $false)] + [int]$DisplaySeconds = 0, + [Parameter(Mandatory = $false)] [PSCredential]$Credential ) - - $CatFact = (ConvertFrom-Json (Invoke-WebRequest -Uri 'http://catfacts-api.appspot.com/api/facts')).facts - - if ($pscmdlet.ShouldProcess("User: $UserName, Computer: $ComputerName", "Send cat fact, $CatFact")) { - $ScriptBlock = { - param ( - [string]$UserName, - - [string]$CatFact, - - [bool]$PlayAudio = $false - ) - - msg $UserName $CatFact - - if ($PlayAudio) { - Add-Type -AssemblyName System.Speech - $SpeechSynth = New-Object System.Speech.Synthesis.SpeechSynthesizer - $SpeechSynth.Speak($CatFact) - } - } - - if ($Credential) { - Write-Verbose "Sending cat fact using credential $($Credential.UserName)" - - Invoke-Command -ComputerName $ComputerName -ScriptBlock $ScriptBlock ` - -ArgumentList $UserName, $CatFact, $PlayAudio -AsJob -Credential $Credential - } else { - Invoke-Command -ComputerName $ComputerName -ScriptBlock $ScriptBlock ` - -ArgumentList $UserName, $CatFact, $PlayAudio -AsJob - } - - Get-Job | Wait-Job | Receive-Job - Get-Job | Remove-Job - } + + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + $CatFact = Invoke-RestMethod -Uri 'https://catfact.ninja/fact' -Method Get | + Select-Object -ExpandProperty fact + + if ($pscmdlet.ShouldProcess("User: $UserName, Computer: $ComputerName", "Send cat fact, $CatFact")) { + $ScriptBlock = { + param ( + [Parameter(Mandatory = $true)] + [string]$UserName, + + [Parameter(Mandatory = $true)] + [string]$CatFact, + + [Parameter(Mandatory = $true)] + [bool]$PlayAudio = $false, + + [Parameter(Mandatory = $true)] + [int]$DisplaySeconds + ) + + if ($DisplaySeconds -gt 0) { + msg $UserName /time:$DisplaySeconds $CatFact + } else { + msg $UserName $CatFact + } + + if ($PlayAudio) { + Add-Type -AssemblyName System.Speech + $SpeechSynth = New-Object System.Speech.Synthesis.SpeechSynthesizer + $SpeechSynth.Speak($CatFact) + } + } + + $Params = @{ + 'ComputerName' = $ComputerName + 'ScriptBlock' = $ScriptBlock + 'ArgumentList' = @($UserName, $CatFact, $PlayAudio, $DisplaySeconds) + 'AsJob' = $true + } + if ($Credential) { $Params.Add('Credential', $Credential) } + + Invoke-Command @Params + + Get-Job | Wait-Job | Receive-Job + Get-Job | Remove-Job + } } \ No newline at end of file diff --git a/Write-Type.ps1 b/Fun/Write-Type.ps1 similarity index 96% rename from Write-Type.ps1 rename to Fun/Write-Type.ps1 index 667f84b..509376a 100644 --- a/Write-Type.ps1 +++ b/Fun/Write-Type.ps1 @@ -1,49 +1,49 @@ -function Write-Type { - <# - .Synopsis - Make Write-Host text appear as if it is being typed - - .DESCRIPTION - Input text and if desired specify the write speed (25-500 milliseconds) and foreground color for the text - - .EXAMPLE - Write-Typewriter 'Hello world!' - - .EXAMPLE - Write-Typewriter 'Hello world!' 250 - - .EXAMPLE - Write-Typewriter -Text '2 spooky 4 me!' -TypeSpeed 400 -ForegroundColor 'Red' - - .NOTES - v1.1 - 2016-04-04 - Nick Rodriguez - -Changed name - -Changed TypeSpeed range - -Added ForegroundColor param - -Changed sleep to not use method after seeing it slow performance with Measure-Command - -Changed code formatting to my liking - - v1.0 - 2016-01-25 - Nathan Kasco (http://poshcode.org/6193) - #> - - [CmdletBinding()] - [OutputType([string])] - - param ( - [Parameter(Mandatory = $true, Position = 0)] - [string] $Text, - - [Parameter(Mandatory = $false, Position = 1)] - [ValidateRange(25, 500)] - [int] $TypeSpeed = 125, - - [Parameter(Mandatory = $false, Position = 2)] - [string] $ForegroundColor = 'White' - ) - - # Pause after typing each letter - $Text.GetEnumerator() | ForEach-Object { - Write-Host $_ -NoNewline -ForegroundColor $ForegroundColor - Start-Sleep -Milliseconds $TypeSpeed - } +function Write-Type { + <# + .Synopsis + Make Write-Host text appear as if it is being typed + + .DESCRIPTION + Input text and if desired specify the write speed (25-500 milliseconds) and foreground color for the text + + .EXAMPLE + Write-Typewriter 'Hello world!' + + .EXAMPLE + Write-Typewriter 'Hello world!' 250 + + .EXAMPLE + Write-Typewriter -Text '2 spooky 4 me!' -TypeSpeed 400 -ForegroundColor 'Red' + + .NOTES + v1.1 - 2016-04-04 - Nick Rodriguez + -Changed name + -Changed TypeSpeed range + -Added ForegroundColor param + -Changed sleep to not use method after seeing it slow performance with Measure-Command + -Changed code formatting to my liking + + v1.0 - 2016-01-25 - Nathan Kasco (http://poshcode.org/6193) + #> + + [CmdletBinding()] + [OutputType([string])] + + param ( + [Parameter(Mandatory = $true, Position = 0)] + [string] $Text, + + [Parameter(Mandatory = $false, Position = 1)] + [ValidateRange(25, 500)] + [int] $TypeSpeed = 125, + + [Parameter(Mandatory = $false, Position = 2)] + [string] $ForegroundColor = 'White' + ) + + # Pause after typing each letter + $Text.GetEnumerator() | ForEach-Object { + Write-Host $_ -NoNewline -ForegroundColor $ForegroundColor + Start-Sleep -Milliseconds $TypeSpeed + } } \ No newline at end of file diff --git a/GUI/Create-MsgBoxWithImage.ps1 b/GUI/Create-MsgBoxWithImage.ps1 new file mode 100644 index 0000000..bb6f336 --- /dev/null +++ b/GUI/Create-MsgBoxWithImage.ps1 @@ -0,0 +1,21 @@ +Add-Type -AssemblyName System.Windows.Forms + +# Get the Base64 string of an image from here https://www.base64-image.de/ and paste below +$Base64Icon = '' +$IconStream = [System.IO.MemoryStream][System.Convert]::FromBase64String($Base64Icon) +$IconBMP = [System.Drawing.Bitmap][System.Drawing.Image]::FromStream($IconStream) + +$form = New-Object System.Windows.Forms.Form +$form.Text = 'Test Title' +$form.Size = '640, 480' +$form.FormBorderStyle='FixedToolWindow' +$form.StartPosition='CenterScreen' + +$image=New-Object Windows.Forms.PictureBox +$image.Size='256,256' +$image.Image = $IconBMP +$image.Location='50,50' +$form.controls.add($image) + +$form.Add_Shown({$form.Activate()}) +[void]$form.ShowDialog() \ No newline at end of file diff --git a/GUI/Get-Directory.ps1 b/GUI/Get-Directory.ps1 new file mode 100644 index 0000000..644fb0f --- /dev/null +++ b/GUI/Get-Directory.ps1 @@ -0,0 +1,14 @@ +function Get-Directory { + [CmdletBinding()] + [OutputType([psobject])] + param() + + [System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null + $OpenDirectoryDialog = New-Object Windows.Forms.FolderBrowserDialog + $OpenDirectoryDialog.ShowDialog() | Out-Null + try { + Get-Item $OpenDirectoryDialog.SelectedPath + } catch { + Write-Warning 'Open Directory Dialog was closed or cancelled without selecting a Directory' + } +} \ No newline at end of file diff --git a/GUI/Get-File.ps1 b/GUI/Get-File.ps1 new file mode 100644 index 0000000..9d6af55 --- /dev/null +++ b/GUI/Get-File.ps1 @@ -0,0 +1,112 @@ +Function Get-File { + <# + .SYNOPSIS + Prompt user to select a file. + + .DESCRIPTION + + + .PARAMETER TypeName + The type of file you're prompting for. This appears in the Open File Dialog and is only used to help the user. + + .PARAMETER TypeExtension + The extension you're prompting for (e.g. "exe") + + .PARAMETER MultipleExtensions + Filter by multiple extensions. Comma separated list. + + .PARAMETER MultipleFiles + Use this to allow the user to select multiple files. + + .PARAMETER InitialDirectory + Directory the Open File Dialog will start from. + + .PARAMETER Title + Title that will appear in the Title Bar of the Open File Dialog. + + .INPUTS + None. You cannot pipe input to this function. + + .OUTPUTS + System.IO.FileSystemInfo + + .EXAMPLE + Get-File + # Prompts the user to select a file of any type + + .EXAMPLE + Get-File -TypeName 'Setup File' -TypeExtension 'msi' -InitialDirectory 'C:\Temp\Downloads' + # Prompts the user to select an msi file and begin the prompt in the C:\Temp\Downloads directory + + .EXAMPLE + Get-File -TypeName 'Log File' -MultipleExtensions 'log', 'txt' -MultipleFiles + # Prompts the user to select one or more txt or log file + + .NOTES + Created by Nick Rodriguez + + Version 1.0 - 2/26/16 + + #> + [CmdletBinding(DefaultParameterSetName = 'SingleExtension')] + [OutputType([psobject[]])] + param ( + [Parameter(Mandatory=$false, ParameterSetName = 'SingleExtension')] + [string] + $TypeName = 'All Files (*.*)', + + [Parameter(Mandatory=$false, ParameterSetName = 'SingleExtension')] + [string] + $TypeExtension = '*', + + [Parameter(Mandatory=$false, ParameterSetName = 'MultipleExtensions')] + [string[]] + $MultipleExtensions, + + [Parameter(Mandatory=$false)] + [switch] + $MultipleFiles, + + [Parameter(Mandatory=$false)] + [ValidateScript({ + if (-not (Test-Path $_ )) { + throw "The path [$_] was not found." + } else { $true } + })] + [string[]] + $InitialDirectory = $PSScriptRoot, + + [Parameter(Mandatory=$false)] + [string] + $Title = 'Select a file' + ) + + [System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null + + $OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog + $OpenFileDialog.Title = $Title + $OpenFileDialog.InitialDirectory = $InitialDirectory + + if ($PSCmdlet.ParameterSetName -eq 'MultipleExtensions' ) { + foreach ($Extension in $MultipleExtensions) { + $TypeExtensionName += "*.$Extension, " + $TypeExtensionFilter += "*.$Extension; " + } + $TypeExtensionName = $TypeExtensionName.TrimEnd(', ') + $TypeExtension = $TypeExtension.TrimEnd('; ') + $OpenFileDialog.Filter = "$TypeName ($TypeExtensionName)| $TypeExtensionFilter" + } else { + $OpenFileDialog.Filter = "$TypeName (*.$TypeExtension)| *.$TypeExtension" + } + + $OpenFileDialog.ShowHelp = $true + $OpenFileDialog.ShowDialog() | Out-Null + + try { + if ($MultipleFiles) { + foreach ($FileName in $OpenFileDialog.FileNames) { Get-Item $FileName } + } else { + Get-Item $OpenFileDialog.FileName + } + } catch { } # User closed the window or hit Cancel, return nothing +} \ No newline at end of file diff --git a/GUI/New-MsgBoxWithImage.ps1 b/GUI/New-MsgBoxWithImage.ps1 new file mode 100644 index 0000000..bb6f336 --- /dev/null +++ b/GUI/New-MsgBoxWithImage.ps1 @@ -0,0 +1,21 @@ +Add-Type -AssemblyName System.Windows.Forms + +# Get the Base64 string of an image from here https://www.base64-image.de/ and paste below +$Base64Icon = '' +$IconStream = [System.IO.MemoryStream][System.Convert]::FromBase64String($Base64Icon) +$IconBMP = [System.Drawing.Bitmap][System.Drawing.Image]::FromStream($IconStream) + +$form = New-Object System.Windows.Forms.Form +$form.Text = 'Test Title' +$form.Size = '640, 480' +$form.FormBorderStyle='FixedToolWindow' +$form.StartPosition='CenterScreen' + +$image=New-Object Windows.Forms.PictureBox +$image.Size='256,256' +$image.Image = $IconBMP +$image.Location='50,50' +$form.controls.add($image) + +$form.Add_Shown({$form.Activate()}) +[void]$form.ShowDialog() \ No newline at end of file diff --git a/GUI/Prompt.ps1 b/GUI/Prompt.ps1 new file mode 100644 index 0000000..8d59dbe --- /dev/null +++ b/GUI/Prompt.ps1 @@ -0,0 +1,22 @@ +# Give the user an interface to run options +Function Prompt { + $Title = "Title" + $Message = "What do you want to do?" + $Get = New-Object System.Management.Automation.Host.ChoiceDescription "&Get", "Description." + $Start = New-Object System.Management.Automation.Host.ChoiceDescription "&Start", "Description." + $Kill = New-Object System.Management.Automation.Host.ChoiceDescription "&Kill", "Description." + $Exit = New-Object System.Management.Automation.Host.ChoiceDescription "&Exit", "Exits this utility." + $Options = [System.Management.Automation.Host.ChoiceDescription[]]($Get, $Start, $Kill, $Exit) + $Result = $Host.UI.PromptForChoice($Title, $Message, $Options, 0) + + Switch ($Result) { + 0 {'Getting services...'; break} + 1 {'Starting services...'; break} + 2 {'Stopping Services...'; break} + 3 {'Exiting...'; exit} + } + + Prompt +} + +Prompt \ No newline at end of file diff --git a/GUI/Read-FolderBrowserDialog.ps1 b/GUI/Read-FolderBrowserDialog.ps1 new file mode 100644 index 0000000..71f62e8 --- /dev/null +++ b/GUI/Read-FolderBrowserDialog.ps1 @@ -0,0 +1,5 @@ +function Read-FolderBrowserDialog { + $ShellApp = New-Object -ComObject Shell.Application + $Directory = $ShellApp.BrowseForFolder(0, 'Select a directory', 0, 'C:\') + if ($Directory) { return $Directory.Self.Path } else { return '' } +} \ No newline at end of file diff --git a/Migrate-SQLBackup.ps1 b/GUI/Read-Menu.ps1 similarity index 86% rename from Migrate-SQLBackup.ps1 rename to GUI/Read-Menu.ps1 index dd70ba4..3104943 100644 --- a/Migrate-SQLBackup.ps1 +++ b/GUI/Read-Menu.ps1 @@ -1,24 +1,24 @@ -$OldSQL = Read-Host 'Current SQL server' -$NewSQL = Read-Host 'New SQL server' -$DB = Read-Host 'Name of Database' - -# Prompt the user whether they want to merge the database -Function Prompt { - CLS - $Title = "Database Backup Migration`n`n`tOLD SQL SERVER`t:`t$OldSQL`n`tNEW SQL SERVER`t:`t$NewSQL`n`tDATABASE`t:`t$DB" - $Message = "`nDo you want to merge database?" - $Yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Perform a database merge on $DB, moving it from $OldSQL to $NewSQL." - $No = New-Object System.Management.Automation.Host.ChoiceDescription "&No", 'Exit this utility.' - $Options = [System.Management.Automation.Host.ChoiceDescription[]]($Yes, $No) - $Result = $Host.UI.PromptForChoice($Title, $Message, $Options, 0) - CLS - - switch ($Result) { - 0 {'Merging...'; Start-Sleep -Seconds 1; Break} - 1 {'Exiting...'; Start-Sleep -Seconds 1; Exit} - } - - Prompt -} - +$OldSQL = Read-Host 'Current SQL server' +$NewSQL = Read-Host 'New SQL server' +$DB = Read-Host 'Name of Database' + +# Prompt the user whether they want to merge the database +Function Prompt { + CLS + $Title = "Database Backup Migration`n`n`tOLD SQL SERVER`t:`t$OldSQL`n`tNEW SQL SERVER`t:`t$NewSQL`n`tDATABASE`t:`t$DB" + $Message = "`nDo you want to merge database?" + $Yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Perform a database merge on $DB, moving it from $OldSQL to $NewSQL." + $No = New-Object System.Management.Automation.Host.ChoiceDescription "&No", 'Exit this utility.' + $Options = [System.Management.Automation.Host.ChoiceDescription[]]($Yes, $No) + $Result = $Host.UI.PromptForChoice($Title, $Message, $Options, 0) + CLS + + switch ($Result) { + 0 { 'Merging...'; Start-Sleep -Seconds 1; Break } + 1 { 'Exiting...'; Start-Sleep -Seconds 1; Exit } + } + + Prompt +} + Prompt \ No newline at end of file diff --git a/GUI/Read-YesOrNo.ps1 b/GUI/Read-YesOrNo.ps1 new file mode 100644 index 0000000..99d6e24 --- /dev/null +++ b/GUI/Read-YesOrNo.ps1 @@ -0,0 +1,7 @@ +function Read-YesOrNo { + param ([string] $Message) + + $Prompt = Read-Host $Message + while ('yes', 'no' -notcontains $Prompt) { $Prompt = Read-Host "Please enter either 'yes' or 'no'" } + if ($Prompt -eq 'yes') { $true } else { $false } +} \ No newline at end of file diff --git a/Get-Certificate.ps1 b/Get-Certificate.ps1 new file mode 100644 index 0000000..2dac1d4 --- /dev/null +++ b/Get-Certificate.ps1 @@ -0,0 +1,116 @@ +<# + .NOTES + =========================================================================== + Created with: SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.126 + Created on: 7/27/2016 11:37 AM + Created by: Nick Rodriguez + Filename: Get-Certificate.ps1 + =========================================================================== + .DESCRIPTION + Get certificates installed on local or remote computers + .PARAMETER Name + Computer name or IP to search. Can be an array or a piped variable. + .PARAMETER Subject + Certificate subject name to search for. By default all subjects will be returned. Wildcards supported (e.g. "*DHG*"). + .PARAMETER DaysLeft + Days left before a certificate expires. To return all certs that expire within 30 days, set this to 30. + .PARAMETER NotExpired + Use this switch to only return certificates that have not expired. +#> + +function Get-Certificate { + [cmdletbinding()] + param ( + [Parameter( + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true + )] + [Alias('ComputerName', 'Computer')] + [string[]]$Name = 'localhost', + + [PSCredential]$Credential, + + [string]$Subject = '*' + ) + + DynamicParam { + # Set the dynamic parameters' name + $ParameterName = 'StorePath' + + # Create the dictionary + $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary + + # Create the collection of attributes + $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + + # Create and set the parameters' attributes + $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute + $ParameterAttribute.Mandatory = $true + $ParameterAttribute.Position = 2 + + # Add the attributes to the attributes collection + $AttributeCollection.Add($ParameterAttribute) + + # Generate and set the ValidateSet + $arrSet = Get-ChildItem -Path 'Cert:' | ForEach-Object { + $Parent = $_.Location + Get-ChildItem -Path "Cert:\$Parent" | ForEach-Object { + Write-Output "Cert:\$Parent\$($_.Name)" + } + } + + $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet) + + # Add the ValidateSet to the attributes collection + $AttributeCollection.Add($ValidateSetAttribute) + + # Create and return the dynamic parameter + $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection) + $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter) + return $RuntimeParameterDictionary + } + + begin { + $StorePath = $PsBoundParameters[$ParameterName] + + $ScriptBlock = { + [cmdletbinding()] + param ( + [string]$StorePath, + + [string]$Subject = '*' + ) + + # Get all certs in specified store filtering by subject if provided + Get-ChildItem -Path $StorePath | Where-Object { + $_.Subject -like $Subject + } | ForEach-Object { + $ExtensionsFormatted = try { $_.Extensions.Format(1) } catch { } + $Template = try { ($_.Extensions.Format(0) -replace "(.+)?=(.+)\((.+)?", '$2')[0] } catch { } + $Days = (New-TimeSpan -End $_.NotAfter).Days + + $Cert = $_.psobject.Copy() + $Cert | Add-Member -Name ExtensionsFormatted -MemberType NoteProperty -Value $ExtensionsFormatted + $Cert | Add-Member -Name DaysUntilExpired -MemberType NoteProperty -Value $Days + $Cert | Add-Member -Name Template -MemberType NoteProperty -Value $Template + + Write-Output $Cert + } + } + } + + process { + if ($Credential) { + Write-Verbose 'Credentials provided' + Invoke-Command -ComputerName $Name -ScriptBlock $ScriptBlock -ArgumentList $StorePath, $Subject -Credential $Credential + } else { + Write-Verbose 'Credentials not provided' + Invoke-Command -ComputerName $Name -ScriptBlock $ScriptBlock -ArgumentList $StorePath, $Subject + } + } +} + +$Computers = @('server01', 'server03') + +$Computers | Get-Certificate -Credential $Creds -StorePath Cert:\LocalMachine\AuthRoot | + Select-Object FriendlyName, DaysUntilExpired, PSComputerName, Template, ExtensionsFormatted | Format-Table -AutoSize \ No newline at end of file diff --git a/Get-WanSpeed.ps1 b/Get-WanSpeed.ps1 new file mode 100644 index 0000000..aeb6085 --- /dev/null +++ b/Get-WanSpeed.ps1 @@ -0,0 +1,100 @@ +<#PSScriptInfo + +.VERSION 2.0 + +.GUID a6048a09-3e66-467a-acd4-ce3e97098a65 + +.AUTHOR velecky@velecky.onmicrosoft.com modified by Nick Rodriguez + +.PROJECTURI https://www.powershellgallery.com/packages/Speedtest/2.0 + +.DESCRIPTION + WAN speed test + +#> + +[CmdletBinding()] +param( + [int]$Repetitions = 4 +) + +function Invoke-SpeedTest { + param( + [string]$UploadUrl + ) + + $topServerUrlSpilt = $UploadUrl -split 'upload' + $url = $topServerUrlSpilt[0] + 'random2000x2000.jpg' + $col = New-Object System.Collections.Specialized.NameValueCollection + $wc = New-Object System.Net.WebClient + $wc.QueryString = $col + $downloadElaspedTime = (Measure-Command { $webpage1 = $wc.DownloadData($url) }).TotalMilliseconds + $downSize = ($webpage1.length + $webpage2.length) / 1MB + $downloadSize = [Math]::Round($downSize, 2) + $downloadTimeSec = $downloadElaspedTime * 0.001 + $downSpeed = ($downloadSize / $downloadTimeSec) * 8 + $downloadSpeed = [Math]::Round($downSpeed, 2) + + Write-Verbose "Downloaded $downloadSize MB in $downloadTimeSec seconds at a speed of $downloadSpeed mbps" + + return $downloadSpeed +} + +# Interact with speedtest page avoiding api +$objXmlHttp = New-Object -ComObject MSXML2.ServerXMLHTTP +$objXmlHttp.Open("GET", "http://www.speedtest.net/speedtest-config.php", $False) +$objXmlHttp.Send() +[xml]$content = $objXmlHttp.responseText + +# Select closest server based on lat/lon +$oriLat = $content.settings.client.lat +$oriLon = $content.settings.client.lon +Write-Verbose "Latitude: $oriLat" +Write-Verbose "Longitude: $oriLon" + +# Make another request, this time to get the server list from the site +$objXmlHttp.Open("GET", "http://www.speedtest.net/speedtest-servers.php", $False) +$objXmlHttp.Send() +[xml]$ServerList = $objXmlHttp.responseText + +# Cons contains all of the information about every server in the speedtest.net database +$cons = $ServerList.settings.servers.server + +# Calculate servers relative closeness by doing math against latitude and longitude +Write-Verbose "Searching for closest geographical servers from list of $($cons.Count)..." +foreach ($val in $cons) { + $R = 6371 + $pi = [Math]::PI + + [float]$dlat = ([float]$oriLat - [float]$val.lat) * $pi / 180 + [float]$dlon = ([float]$oriLon - [float]$val.lon) * $pi / 180 + [float]$a = [math]::Sin([float]$dLat / 2) * [math]::Sin([float]$dLat / 2) + [math]::Cos([float]$oriLat * $pi / 180 ) * [math]::Cos([float]$val.lat * $pi / 180 ) * [math]::Sin([float]$dLon / 2) * [math]::Sin([float]$dLon / 2) + [float]$c = 2 * [math]::Atan2([math]::Sqrt([float]$a ), [math]::Sqrt(1 - [float]$a)) + [float]$d = [float]$R * [float]$c + + $serverInformation += @([pscustomobject]@{ + Distance = $d + Country = $val.country + Sponsor = $val.sponsor + Url = $val.url + }) +} + +$serverInformation = $serverInformation | Sort-Object -Property distance + +$speedResults = @() + +for ($i = 0; $i -lt $Repetitions; $i++) { + $url = $serverInformation[$i].url + Write-Verbose "Download attempt ($($i + 1) of $Repetitions) from $url..." + $speed = Invoke-SpeedTest $url + $speedResults += $speed +} + +$results = $speedResults | Measure-Object -Average -Minimum -Maximum + +New-Object psobject -Property @{ + "Fastest (mbps)" = $results.Maximum + "Slowest (mbps)" = $results.Minimum + "Average (mbps)" = $results.Average +} \ No newline at end of file diff --git a/Get-Weather.ps1 b/Get-Weather.ps1 deleted file mode 100644 index 0faa0a4..0000000 --- a/Get-Weather.ps1 +++ /dev/null @@ -1,110 +0,0 @@ -$UtilityName = 'Archaic Weather Gathering Utility - Nick Rodriguez' - -# Default values -$City = 'Asheville' -$Country = 'United States' -$ZipCode = '28803' -$Days = '6' - -Function Get-Weather { - param([string]$City, [string]$Country) - $webservice = New-WebServiceProxy -Uri 'http://www.webservicex.net/globalweather.asmx?WSDL' - $CurrentWeather = ([xml]$webservice.GetWeather($City, $Country)).CurrentWeather - return $CurrentWeather -} - -Function Get-Forecast { - Param([string]$ZipCode, [int]$Days) - $URI = 'http://www.weather.gov/forecasts/xml/DWMLgen/wsdl/ndfdXML.wsdl' - $Proxy = New-WebServiceProxy -uri $URI -namespace WebServiceProxy - [xml]$latlon=$proxy.LatLonListZipCode($ZipCode) - $Forecast = foreach($l in $latlon) { - $a = $l.dwml.latlonlist -split "," - $lat = $a[0] - $lon = $a[1] - $sDate = get-date -UFormat %Y-%m-%d - $format = "Item24hourly" - [xml]$weather = $Proxy.NDFDgenByDay($lat,$lon,$sDate,$Days,'e',$format) - For($i = 0 ; $i -le $Days -1 ; $i ++) { - New-Object psObject -Property @{ - "Date" = ((Get-Date).addDays($i)).tostring("MM/dd/yyyy") ; - "maxTemp" = $weather.dwml.data.parameters.temperature[0].value[$i] ; - "minTemp" = $weather.dwml.data.parameters.temperature[1].value[$i] ; - "Summary" = $weather.dwml.data.parameters.weather."weather-conditions"[$i]."Weather-summary" - } - } - } - - return $Forecast | Format-Table -Property date, maxTemp, minTemp, Summary -AutoSize -} - -Function LoadMenuSystem { - [INT]$MenuLevel1=0 - [INT]$xMenuLevel2=0 - [BOOLEAN]$xValidSelection=$false - while ( $MenuLevel1 -lt 1 -or $MenuLevel1 -gt 3 ) { - CLS - #… Present the Menu Options - Write-Host "`n`t$UtilityName`n" -ForegroundColor Magenta - Write-Host "`t`tPlease select an option`n" -Fore Cyan - Write-Host "`t`t`t1. Current Weather" -Fore Cyan - Write-Host "`t`t`t2. Forecast" -Fore Cyan - Write-Host "`t`t`t3. Exit`n" -Fore Cyan - #… Retrieve the response from the user - [int]$MenuLevel1 = Read-Host "`t`tEnter Menu Option Number" - if( $MenuLevel1 -lt 1 -or $MenuLevel1 -gt 3 ) { - Write-Host "`tPlease select one of the options available.`n" -Fore Red; Start-Sleep -Seconds 1 - } - } - Switch ($MenuLevel1){ #… User has selected a valid entry.. load next menu - 1 { - while ( $xMenuLevel2 -lt 1 -or $xMenuLevel2 -gt 4 ) { - CLS - # Present the Menu Options - Write-Host "`n`t$UtilityName`n" -Fore Magenta - Write-Host "`t`tCurrent weather for $City, $Country`n" -Fore Cyan - Write-Host "`t`t`t1. Refresh" -Fore Cyan - Write-Host "`t`t`t2. Change City" -Fore Cyan - Write-Host "`t`t`t3. Change Country" -Fore Cyan - Write-Host "`t`t`t4. Go to Main Menu`n" -Fore Cyan - [int]$xMenuLevel2 = Read-Host "`t`tEnter Menu Option Number" - if( $xMenuLevel2 -lt 1 -or $xMenuLevel2 -gt 4 ) { - Write-Host "`tPlease select one of the options available.`n" -Fore Red; Start-Sleep -Seconds 1 - } - } - Switch ($xMenuLevel2) { - 1{ Get-Weather $City $Country; pause } - 2{ $City = Read-Host "`n`tEnter a city name" } - 3{ $Country = Read-Host "`n`tEnter a country name" } - default { break} - } - } - 2 { - while ( $xMenuLevel2 -lt 1 -or $xMenuLevel2 -gt 4 ) { - CLS - # Present the Menu Options - Write-Host "`n`t$UtilityName`n" -Fore Magenta - Write-Host "`t`t$Days day forecast for area code $ZipCode`n" -Fore Cyan - Write-Host "`t`t`t1. Refresh" -Fore Cyan - Write-Host "`t`t`t2. Change Zip Code" -Fore Cyan - Write-Host "`t`t`t3. Change Number of Days" -Fore Cyan - Write-Host "`t`t`t4. Go to Main Menu`n" -Fore Cyan - [int]$xMenuLevel2 = Read-Host "`t`tEnter Menu Option Number" - } - if( $xMenuLevel2 -lt 1 -or $xMenuLevel2 -gt 4 ){ - Write-Host "`tPlease select one of the options available.`n" -Fore Red; Start-Sleep -Seconds 1 - } - Switch ($xMenuLevel2) { - 1{ Get-Forecast $ZipCode $Days; pause } - 2{ $ZipCode = Read-Host "`n`tEnter a zip code" } - 3{ $Days = Read-Host "`n`tEnter the number of days to forecast" } - default { break} - } - } - default { exit } - } - - LoadMenuSystem -} - -LoadMenuSystem \ No newline at end of file diff --git a/Google/Get-GoogleHangoutsData.ps1 b/Google/Get-GoogleHangoutsData.ps1 new file mode 100644 index 0000000..40af555 --- /dev/null +++ b/Google/Get-GoogleHangoutsData.ps1 @@ -0,0 +1,110 @@ + +$HangoutsFilePath = '~\downloads\hangouts.json' +$HangoutsRaw = Get-Content $HangoutsFilePath +$HangoutsData = Get-Content $HangoutsFilePath | ConvertFrom-Json + +$Conversations = @() +$AllParticipants = @() + +function Process-GoogleHangoutsData { + # First we want to get all participants, so we loop fully once + foreach ($Key in $HangoutsData.conversation_state) { + $Conversation = $HangoutsData.conversation_state.$Key.'conversation_state'.'conversation' + + # Get all participants + foreach ($PersonData in $Conversation.'participant_data') { + $Person = $Conversation.'participant_data'.$PersonData + $Gaia_id = $Person.'id'.'gaia_id' + if (-not $person.'fallback_name' -or $Person.'fallback_name' -eq $null) { break } + if (-not $AllParticipants.$Gaia_id) { + $AllParticipants.$gaia_id = $Person.'fallback_name' + } + } + } + foreach ($Key in $HangoutsData.'conversation_state') { + $conversation_state = $HangoutsData.'conversation_state'.$key + $id = $conversation_state.'conversation_id'.'id' + $conversation = $conversation_state.'conversation_state'.'conversation' + + # Find participants + $participants = @() + $participants_obj = @{} + foreach ($person_key in $conversation.'participant_data') { + $person = $conversation.'participant_data'.$person_key + $gaia_id = $person.'id'.'gaia_id' + $name = 'Unknown' + if ($person.'fallback_name') { + $name = $person.'fallback_name' + } else { + $name = $AllParticipants.$gaia_id + } + $participants.push($name) + $participants_obj.$gaia_id = $name + } + $participants_string = $participants.join(", ") + # Add to list + #$(".convo-list").append("" + participants_string + "") + # Parse events + $events = @() + for (event_key in conversation_state.'conversation_state'.'event'){ + $convo_event = conversation_state.'conversation_state'.'event'.event_key + $timestamp = convo_event.'timestamp' + $msgtime = formatTimestamp(timestamp) + $sender = convo_event.'sender_id'.'gaia_id' + $message = "" + if(convo_event.'chat_message'){ + # Get message + for(msg_key in convo_event.'chat_message'.'message_content'.'segment'){ + $segment = convo_event.'chat_message'.'message_content'.'segment'.msg_key + if(segment.'type' == 'LINE_BREAK') $message += "\n" + if(!segment.'text') continue + message += twemoji.parse(segment.'text') + } + # Check for images on event + if(convo_event.'chat_message'.'message_content'.'attachment'){ + foreach ($attach_key in convo_event.'chat_message'.'message_content'.'attachment'){ + $attachment = convo_event.'chat_message'.'message_content'.'attachment'.attach_key + console.log(attachment) + if(attachment.'embed_item'.'type'.0 == "PLUS_PHOTO"){ + message += "\n" + } + } + } + events.push({msgtime: msgtime, sender: participants_obj.sender, message: message, timestamp: timestamp}) + } + } + <# Sort events by timestamp + $events.sort(function($a, $b){ + $keyA = $a.timestamp, + $keyB = $b.timestamp + if($keyA < $keyB) return -1 + if($keyA > $keyB) return 1 + return 0 + })#> + # Add events + $Conversations.$id = $events + } +} + +function switchConvo($id) { + $('.txt').text('') + foreach ($event_id in Conversations.id){ + $convo_event = Conversations.id.event_id + $('.txt').append($convo_event.msgtime + ": " + $convo_event.sender + ": " + $convo_event.message + "\n") + } +} + +function zeroPad($string) { + return ($string < 10) ? "0" + $string : $string +} + +function formatTimestamp($timestamp) { + $d = new Date($timestamp/1000) + $formattedDate = $d.getFullYear() + "-" + + zeroPad($d.getMonth() + 1) + "-" + + zeroPad($d.getDate()) + $hours = zeroPad($d.getHours()) + $minutes = zeroPad($d.getMinutes()) + $formattedTime = $hours + ":" + $minutes + return $formattedDate + " " + $formattedTime +} \ No newline at end of file diff --git a/Install-Choco.ps1 b/Install-Choco.ps1 new file mode 100644 index 0000000..2a294ce --- /dev/null +++ b/Install-Choco.ps1 @@ -0,0 +1 @@ +iwr https://chocolatey.org/install.ps1 -UseBasicParsing | iex \ No newline at end of file diff --git a/Install-PoshSSH.ps1 b/Install-PoshSSH.ps1 new file mode 100644 index 0000000..7362d42 --- /dev/null +++ b/Install-PoshSSH.ps1 @@ -0,0 +1,2 @@ +#iex (New-Object Net.WebClient).DownloadString("https://gist.github.com/darkoperator/6152630/raw/c67de4f7cd780ba367cccbc2593f38d18ce6df89/instposhsshdev") +Install-Module -Name Posh-SSH \ No newline at end of file diff --git a/Install-PowerShellGet.ps1 b/Install-PowerShellGet.ps1 new file mode 100644 index 0000000..68eca4b --- /dev/null +++ b/Install-PowerShellGet.ps1 @@ -0,0 +1,3 @@ +# Install PowerShellGet from this link - https://www.microsoft.com/en-us/download/details.aspx?id=51451 +Install-PackageProvider NuGet -Force +Import-PackageProvider NuGet -Force \ No newline at end of file diff --git a/MSO/Copy-SPOList.ps1 b/MSO/Copy-SPOList.ps1 new file mode 100644 index 0000000..8453e30 --- /dev/null +++ b/MSO/Copy-SPOList.ps1 @@ -0,0 +1,66 @@ +function Copy-SPOList { + [cmdletbinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$SiteUrl, + + [Parameter(Mandatory = $true)] + [string]$SourceListName, + + [Parameter(Mandatory = $true)] + [string]$DestinationListName + ) + + Import-Module .\OneDrive.psm1 + Import-SharePointClientComponents + + $Context = New-Object Microsoft.SharePoint.Client.ClientContext($SiteUrl) + $Creds = Get-Credential + $SPOCreds = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($Creds.UserName, $Creds.Password) + $Context.Credentials = $SPOCreds + + $SourceList = $Context.Web.Lists.GetByTitle($SourceListName) + $DestinationList = $Context.Web.Lists.GetByTitle($DestinationListName) + $ItemsToCopy = $SourceList.GetItems([Microsoft.SharePoint.Client.CamlQuery]::CreateAllItemsQuery()) + $Fields = $SourceList.Fields + + Write-Progress -Activity "Gathering all items from $SourceListName..." + $Context.Load($ItemsToCopy) + $Context.Load($SourceList) + $Context.Load($DestinationList) + $Context.Load($Fields) + $Context.ExecuteQuery() + + $Count = $ItemsToCopy.Count + $Counter = 0 + + foreach ($Item in $ItemsToCopy) { + $Counter++ + + $SPOItem = New-Object -TypeName psobject + $SPOItem | Add-Member -Name ID -MemberType NoteProperty -Value $Item.ID + + Write-Progress -Activity "Copying items from $SourceListName to $DestinationListName" ` + -Status "Item $Counter of $Count - $($SPOItem.ID)" -PercentComplete (($Counter / $Count) * 100) + + $ListItemInfo = New-Object Microsoft.SharePoint.Client.ListItemCreationInformation + $NewItem = $DestinationList.AddItem($ListItemInfo) + + foreach ($Field in $Fields) { + if ( + (-Not ($Field.ReadOnlyField)) -and + (-Not ($Field.Hidden)) -and + ($Field.InternalName -ne "Attachments") -and + ($Field.InternalName -ne "ContentType") + ) { + $SPOItem | Add-Member -Name $Field.InternalName -MemberType NoteProperty -Value $Item[$Field.InternalName] + $NewItem[$Field.InternalName] = $Item[$Field.InternalName] + $NewItem.update() + } + } + + $SPOItem + + $Context.ExecuteQuery() + } +} \ No newline at end of file diff --git a/MSO/Get-DisabledLicensedUser.ps1 b/MSO/Get-DisabledLicensedUser.ps1 new file mode 100644 index 0000000..08f7f87 --- /dev/null +++ b/MSO/Get-DisabledLicensedUser.ps1 @@ -0,0 +1,2 @@ +Connect-MsolService +Get-MsolUser -All -EnabledFilter DisabledOnly | Where-Object { $_.IsLicensed } \ No newline at end of file diff --git a/MSO/Get-MFAEnabledUser.ps1 b/MSO/Get-MFAEnabledUser.ps1 new file mode 100644 index 0000000..216eef1 --- /dev/null +++ b/MSO/Get-MFAEnabledUser.ps1 @@ -0,0 +1,18 @@ +Connect-MsolService + +Get-MsolUser -EnabledFilter EnabledOnly -All | ForEach-Object { + $AuthMethods = $_.StrongAuthenticationMethods + $DefaultMethod = ($AuthMethods | Where-Object -Property IsDefault -EQ $true).MethodType + + New-Object -TypeName psobject -Property @{ + UserPrincipalName = $_.UserPrincipalName + RelyingParty = $_.StrongAuthenticationRequirements.RelyingParty + RememberDevicesNotIssuedBefore = $_.StrongAuthenticationRequirements.RememberDevicesNotIssuedBefore + State = $_.StrongAuthenticationRequirements.State + DefaultMethod = $DefaultMethod + MethodType1 = $AuthMethods[0].MethodType + MethodType2 = $AuthMethods[1].MethodType + MethodType3 = $AuthMethods[2].MethodType + MethodType4 = $AuthMethods[3].MethodType + } +} \ No newline at end of file diff --git a/MSO/Get-MSOLUsersWithAlias.ps1 b/MSO/Get-MSOLUsersWithAlias.ps1 new file mode 100644 index 0000000..451faf9 --- /dev/null +++ b/MSO/Get-MSOLUsersWithAlias.ps1 @@ -0,0 +1,14 @@ +$Creds = Get-Credential +$PSSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.outlook.com/powershell/ ` + -Credential $Creds -Authentication Basic –AllowRedirection + +Import-PSSession $PSSession + +$Results = Get-Mailbox | Select-Object DisplayName, @{ + Name = 'EmailAddresses' + Expression = { ($_.EmailAddresses | Where-Object { $_ -LIKE "SMTP:*" }).TrimStart('SMTP:') } +} + +$Results | Format-Table -AutoSize + +#$Results | Export-Csv c:\temp\UsersWithAlias.csv -NoTypeInformation \ No newline at end of file diff --git a/MSO/Remove-OneDriveIRM.ps1 b/MSO/Remove-OneDriveIRM.ps1 index d8d6ebf..c6711a7 100644 --- a/MSO/Remove-OneDriveIRM.ps1 +++ b/MSO/Remove-OneDriveIRM.ps1 @@ -1,7 +1,7 @@ # These 3 lines get the Documents site from SPO for the specified user -$webUrl = "https://company-my.sharepoint.com/personal/company” +$webUrl = "https://company-my.sharepoint.com/personal/company" $clientContext = New-Object Microsoft.SharePoint.Client.ClientContext($webUrl) -$list = $clientContext.Web.Lists.GetByTitle(“Documents") +$list = $clientContext.Web.Lists.GetByTitle("Documents") # reset the value to the default settings $list.InformationRightsManagementSettings.Reset() diff --git a/MSO/Set-License.ps1 b/MSO/Set-License.ps1 new file mode 100644 index 0000000..c2f0ee8 --- /dev/null +++ b/MSO/Set-License.ps1 @@ -0,0 +1,95 @@ +<# +.SYNOPSIS + Grant MSO license to users + +.DESCRIPTION + +.PARAMETER xx + +.EXAMPLE + +.NOTES + Created by Nick Rodriguez + +#> + +begin { + # https://support.microsoft.com/en-us/kb/3108269 + + # Connect to MS Online + Connect-MsolService + + function Get-LicenseSkuId { + param ([string] $LicenseName) + + # Get the license name + $LicenseObj = Get-MsolAccountSku | Where-Object { + $_.SkuPartNumber -eq $LicenseName -or + $_.AccountSkuId -eq $LicenseName + } + $LicenseObj.AccountSkuId + } + + function Set-License { + [CmdletBinding(DefaultParameterSetName = 'SpecificUser')] + param ( + [Parameter(Mandatory=$true)] + [ValidateSet('All','SpecificUser','Department')] + [string] $UserSet, + + [Parameter(Mandatory=$true)] + [string] $LicenseSkuId, + + [Parameter(Mandatory=$false, ParameterSetName = 'Group')] + [ValidateSet('All','EnabledOnly','DisabledOnly')] + [string] $FilterIsLicensed, + + [Parameter(Mandatory=$false, ParameterSetName = 'SpecificUser')] + [string] $Email, + + [Parameter(Mandatory=$false, ParameterSetName = 'Group')] + [string] $Department + ) + + switch ($UserSet) { + # Give license to all users + All { $Users = Get-MSOLUser -All -EnabledFilter $FilterIsLicensed } + + # Get specific user based on email + SpecificUser { $Users = Get-MsolUser -UserPrincipalName $Email } + + # Get specific user group based on department + Department { $Users = Get-MsolUser -Department $Department -EnabledFilter $FilterIsLicensed } + } + + # Grant license to given users + $Users | Set-MsolUserLicense -AddLicenses $LicenseSkuId -Verbose + + # Verify license was granted + $Users | Select UserPrincipalName, Licenses | ForEach-Object { + Write-Host "$($_.UserPrincipalName) - " -NoNewline + + if ($_.Licenses.AccountSkuId -contains $LicenseSkuId) { + Write-Host $true -ForegroundColor Green + } else { + Write-Host $false -ForegroundColor Red + } + } + } +} + +process { + $License = Get-LicenseSkuId -LicenseName 'enterprisewithscal' + + # Add users from file + #Get-Content 'C:\users.txt' | ForEach-Object { Set-License -UserSet SpecificUser -LicenseSkuId $License -Email $_ } + + # Add single user + #Set-License -UserSet SpecificUser -LicenseSkuId $License -Email 'me@company.com' + + # Add department + #Set-License -UserSet Department -LicenseSkuId $License -Department 'IT' -FilterIsLicensed EnabledOnly + + # All enabled users + Set-License -UserSet All -LicenseSkuId $License -FilterIsLicensed EnabledOnly +} \ No newline at end of file diff --git a/MapDrivesUtility/Invoke-MapNetworkDrive.ps1 b/MapDrivesUtility/Invoke-MapNetworkDrive.ps1 new file mode 100644 index 0000000..2afd4e2 --- /dev/null +++ b/MapDrivesUtility/Invoke-MapNetworkDrive.ps1 @@ -0,0 +1 @@ +This script has been moved to a new project repo - https://github.com/nickrod518/MapDrivesUtility \ No newline at end of file diff --git a/Munki/Create-MunkiRepo.ps1 b/Munki/Create-MunkiRepo.ps1 new file mode 100644 index 0000000..afe8191 --- /dev/null +++ b/Munki/Create-MunkiRepo.ps1 @@ -0,0 +1,39 @@ +$Script = { + # Remove the port 80 binding on "Default Web Site" because we'll need that for our Munki repo + Try { + $Binding = Get-WebBinding -Port 80 -Name "Default Web Site" + Write-Verbose "Port 80 binding found on Default Web Site - removing..." + $Binding | Remove-WebBinding + } Catch { + Write-Verbose "Port 80 binding not found on Default Web Site - moving on..." + } + + # Check if the Munki repo already exists + $MunkiExists = $false + Get-Website -Name "Munki" | ForEach-Object { + If ($_.Name -eq "Munki") { $MunkiExists = $true } + } + + # Create the Munki repo if it doesn't exist + If ($MunkiExists) { + Write-Verbose "Munki repo already exists - moving on..." + } else { + Write-Verbose "Munki repo doesn't exist - creating..." + + # Create site directory + New-Item -ItemType Directory "R:\repo" -Force + + # Create the new Munki website + New-Website -Name "Munki" -PhysicalPath "R:\" + + # Set MIME types + Add-WebConfigurationProperty -PSPath IIS:\Sites\Munki -Filter system.webServer/staticContent -Name '.' -Value ` + @{ fileExtension = '.'; mimeType = 'application/octet-stream' }, # Accept all extensions + @{ fileExtension = '*'; mimeType = 'application/octet-stream' } # Accept all files + + # Allow Directory Browsing + Set-WebConfigurationProperty -PSPath IIS:\sites\Munki -Filter system.webServer/directoryBrowse -Name enabled -Value 'true' + } +} + +Invoke-Command -ComputerName server01 -ScriptBlock $Script \ No newline at end of file diff --git a/Munki/Get-MunkiRepoIP.ps1 b/Munki/Get-MunkiRepoIP.ps1 new file mode 100644 index 0000000..57afcb2 --- /dev/null +++ b/Munki/Get-MunkiRepoIP.ps1 @@ -0,0 +1,22 @@ +# List of servers with Munki repos setup +$Servers = (Get-ADGroupMember -Identity MunkiRepos).Name + +$Creds = Get-Credential + +foreach ($Computer in $Servers) { + if(Test-Connection -ComputerName $Computer -Count 1 -ea 0) { + try { + $Networks = Get-WmiObject Win32_NetworkAdapterConfiguration -ComputerName $Computer -EA Stop -Credential $Creds | ? {$_.IPEnabled} + } catch { + Write-Warning "Error occurred while querying $Computer." + Continue + } + foreach ($Network in $Networks) { + $IPAddress = $Network.IpAddress[0] + $OutputObj = New-Object -Type PSObject + $OutputObj | Add-Member -MemberType NoteProperty -Name Repo -Value $Computer.ToUpper() + $OutputObj | Add-Member -MemberType NoteProperty -Name IPAddress -Value $IPAddress + $OutputObj + } + } + } \ No newline at end of file diff --git a/Munki/Set-Repo b/Munki/Set-Repo new file mode 100644 index 0000000..92b807e --- /dev/null +++ b/Munki/Set-Repo @@ -0,0 +1,45 @@ +#!/bin/bash + +# Get our log file, or create one +Log="/Users/Shared/Munki/MunkiRepo.log" +if [ ! -f $Log ]; then + mkdir -p /Users/Shared/Munki/ + touch $Log +fi + +# Get the active network interface +ActiveIFace=`route get google.com | awk '/interface/ {print $2}'` +echo "`date`: The active interface is $ActiveIFace" >> $Log + +# Get the current IP address +CurrentIP=`ifconfig $ActiveIFace | awk '/inet / {print $2}'` +echo "`date`: Your IP address is $CurrentIP" >> $Log + +# Office IP addresses associative array +Locations=( + '10.20.0::munkirepo01' # Region1 + '10.40.0::munkirepo02' # Region2 + '192.168.1::munkirepo03' # VPN +) + +# Set fallback repo if no matches are found +Repo='munkirepo01' + +# Go through each IP in our locations map to check for matches +for Location in "${Locations[@]}" ; do + # First 3 octets of repo's IP + IP=${Location%%::*} + + # Check if current IP contains match + if [[ "$CurrentIP" == "$IP"* ]]; then + Repo=${Location##*::} + break + fi +done + +# Update the Munki preferences to match the new repo +{ + defaults write /Library/Preferences/ManagedInstalls.plist SoftwareRepoURL "http://$Repo.company.local/repo" && echo "`date`: Repo set to $Repo" >> $Log +} || { + echo "`date`: Error setting repo to $Repo" >> $Log +} diff --git a/Munki/Sync-MunkiRepos.ps1 b/Munki/Sync-MunkiRepos.ps1 new file mode 100644 index 0000000..6259e64 --- /dev/null +++ b/Munki/Sync-MunkiRepos.ps1 @@ -0,0 +1,86 @@ +function Mail-Results { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [string] + $MessageBody, + + [Parameter(Mandatory=$true)] + [string[]] + $Attachments + ) + + $SMTPServer = 'smtp1.corp.local' + $SMTP = New-Object Net.Mail.SmtpClient($SMTPServer) + + $Message = New-Object Net.Mail.MailMessage + $Message.From = 'MunkiAlerts@company.com' + $Message.To.Add('me@company.com') + $Message.Subject = 'Munki Sync Results' + $Message.Body = $MessageBody + foreach ($Report in $Attachments) { + $Message.Attachments.Add($Report) + } + $SMTP.Send($Message) +} + +function Convert-RobocopyExitCode ($ExitCode) { + switch ($ExitCode) { + 16 {'***FATAL ERROR***'} + 15 {'OKCOPY + FAIL + MISMATCHES + XTRA'} + 14 {'FAIL + MISMATCHES + XTRA'} + 13 {'OKCOPY + FAIL + MISMATCHES'} + 12 {'FAIL + MISMATCHES'} + 11 {'OKCOPY + FAIL + XTRA'} + 10 {'FAIL + XTRA'} + 9 {'OKCOPY + FAIL'} + 8 {'FAIL'} + 7 {'OKCOPY + MISMATCHES + XTRA'} + 6 {'MISMATCHES + XTRA'} + 5 {'OKCOPY + MISMATCHES'} + 4 {'MISMATCHES'} + 3 {'OKCOPY + XTRA'} + 2 {'XTRA'} + 1 {'OKCOPY'} + 0 {'No Change'} + default {'Unknown'} + } +} + +# Create our log directory +$LogDir = New-Item -ItemType Directory "$PSScriptRoot\Logs" -Force + +# Repo servers that are currently standing +$Servers = @( + 'munkirepo01', + 'munkirepo02', + 'munkirepo03' +) + +$MessageBody = '' +$Logs = @() +$SourceRepo = '\\macserver.company.local\repo' + +# Distribute content from the central repo to each node repo +foreach ($Repo in $Servers) { + $Log = (New-Item -ItemType File "$LogDir\MunkiSync-$Repo-$( (Get-Date).ToString('yyyyMMdd-HHmm') ).log").FullName + + # Update repo + ROBOCOPY $SourceRepo "\\$Repo\r$\repo" /DCOPY:DA /MIR /FFT /Z /XA:SH /R:10 /LOG:$Log /XJD + $ExitCode = $LASTEXITCODE + + # Get volume info + $Volume = Get-WmiObject Win32_Volume -ComputerName $Repo | Where-Object { $_.Name -eq 'R:\' } | Select-Object Name, Capacity, FreeSpace + + $Results += @([pscustomobject]@{ + Repo = $Repo + 'RoboCopy Results' = "$ExitCode`: $(Convert-RobocopyExitCode $ExitCode)" + 'Free (MB)' = [math]::truncate($Volume.FreeSpace / 1MB) + 'Capacity (MB)' = [math]::truncate($Volume.Capacity / 1MB) + 'Used (MB)' = [math]::truncate(($Volume.Capacity - $Volume.FreeSpace) / 1MB) + 'Completion Time' = Get-Date + }) + $Logs += $Log +} + +Mail-Results -MessageBody ($Results | Format-Table -AutoSize | Out-String) -Attachments $Logs \ No newline at end of file diff --git a/Munki/installmunki b/Munki/installmunki new file mode 100644 index 0000000..ef4fc05 --- /dev/null +++ b/Munki/installmunki @@ -0,0 +1,55 @@ +#!/bin/bash + +## +# Download and install the latest Munki2 tools from munkibuilds.org +## + +cat < $log" >> $crontab + fi +else + echo "SHELL=/bin/sh" > $crontab + echo "PATH=/bin:/sbin:/usr/bin:/usr/sbin" >> $crontab + echo "30 * * * * root /usr/sbin/softwareupdate -l &> $log" >> $crontab +fi + +/usr/bin/tail -1 $log diff --git a/Munki/updatemunkirepo b/Munki/updatemunkirepo new file mode 100644 index 0000000..76e8e9c --- /dev/null +++ b/Munki/updatemunkirepo @@ -0,0 +1,28 @@ +#!/bin/bash + +cat < + +Pause \ No newline at end of file diff --git a/Office/Enable-OutlookQuarantineFolder.ps1 b/Office/Enable-OutlookQuarantineFolder.ps1 new file mode 100644 index 0000000..ec6d164 --- /dev/null +++ b/Office/Enable-OutlookQuarantineFolder.ps1 @@ -0,0 +1,50 @@ +begin { Start-Transcript $env:TEMP\olQuarantineEnable.log } + +process { + # Get Outlook object + Add-Type -AssemblyName Microsoft.Office.Interop.Outlook + $Outlook = New-Object -ComObject Outlook.Application + + # Get Inbox + $Namespace = $Outlook.GetNamespace('MAPI') + $Inbox = $Namespace.GetDefaultFolder([Microsoft.Office.Interop.Outlook.OlDefaultFolders]::olFolderInbox) + + try { + # Create new Quarantine folder + $Quarantine = $Inbox.Folders.Add('Quarantine') + } catch { + # Get existing Quarantine folder + $Quarantine = $Inbox.Folders | Where-Object -Property Name -eq 'Quarantine' + } + + # Set the web Url to the Quarantine admin site and set web view as default + $Quarantine.WebViewURL = 'https://admin.protection.outlook.com/quarantine' + $Quarantine.WebViewOn = $true + + # Add Quarantine folder to favorites + $OutlookModule = $Outlook.ActiveExplorer().NavigationPane.Modules.Item('Mail') + $Favorites = $OutlookModule.NavigationGroups.Item('Favorites') + $Favorites.NavigationFolders.Add($Quarantine) + + # Validate settings + $Quarantine = $Inbox.Folders | Where-Object -Property Name -eq 'Quarantine' + + if ($Quarantine) { + if ($Quarantine.WebViewURL -ne 'https://admin.protection.outlook.com/quarantine') { exit 996 } + + if (-not $Quarantine.WebViewOn) { exit 997 } + + if (-not $Favorites.NavigationFolders.Item($Quarantine.Name)) { exit 998 } + + exit 0 + } else { + exit 995 + } +} + +end { + # Used as detection method in SCCM + New-Item -Path 'HKLM:\Software\SMS' -ItemType Key -Force | + New-ItemProperty -Name olQuarantineEnabled -Value $LASTEXITCODE -PropertyType String -Force + Stop-Transcript +} \ No newline at end of file diff --git a/README.md b/README.md index ed8b329..203aa3f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # PowerShell-Scripts -Just a bunch of PowerShell scripts that help me get through the day. +Just a bunch of PowerShell scripts for sysadmins. diff --git a/Registry/Check-DotNetVersion.ps1 b/Registry/Check-DotNetVersion.ps1 new file mode 100644 index 0000000..1d2af48 --- /dev/null +++ b/Registry/Check-DotNetVersion.ps1 @@ -0,0 +1,6 @@ +$version = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' -Name Version -ErrorAction SilentlyContinue).Version +if ($version -like "4.5*") { + return $true +} else { + return $false +} \ No newline at end of file diff --git a/Registry/Disable-UAC.ps1 b/Registry/Disable-UAC.ps1 new file mode 100644 index 0000000..c40aec7 --- /dev/null +++ b/Registry/Disable-UAC.ps1 @@ -0,0 +1,3 @@ +New-ItemProperty -Path HKLM:Software\Microsoft\Windows\CurrentVersion\policies\system -Name EnableLUA -PropertyType DWord -Value 0 -Force +Write-Host "You must restart before changes will be in effect." +pause diff --git a/Registry/Suspend-SS.ps1 b/Registry/Suspend-SS.ps1 new file mode 100644 index 0000000..f83a552 --- /dev/null +++ b/Registry/Suspend-SS.ps1 @@ -0,0 +1,54 @@ +[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null +[string]$regPath = 'HKCU:\Software\Policies\Microsoft\Windows\Control Panel\Desktop\' +[string]$Status = "Time before screen saver default settings are restored:" + +# Validate input; must be between 15-240 +do { [int]$Minutes = Read-Host "Minutes to suspend screen saver (15-240)" } +while ((15..240) -notcontains $Minutes) + +[Int32]$Seconds = $Minutes * 60 +[string]$Message = "Screen saver suspended for $Minutes minutes..." + +function Set-SS { + Param ( + [Parameter(Mandatory=$true)] + [int] + $TimeOut, + + [Parameter(Mandatory=$true)] + [int] + $Active, + + [Parameter(Mandatory=$true)] + [int] + $IsSecure + ) + + try { + # Set screen saver registry properties + Set-ItemProperty -Path $regPath -Name ScreenSaveTimeOut -Value $TimeOut -ErrorAction Stop + Set-ItemProperty -Path $regPath -Name ScreenSaveActive -Value $Active -ErrorAction Stop + Set-ItemProperty -Path $regPath -Name ScreenSaverIsSecure -Value $IsSecure -ErrorAction Stop + } catch { + Write-Host 'There was an issue disabling the screen saver:' + Write-Host $_.Exception.Message -ForegroundColor Red + Read-Host 'Press [Enter] to exit' + exit + } +} + +# Disable screen saver +Set-SS -TimeOut 0 -Active 0 -IsSecure 0 + +foreach ($Sec in (1..$Seconds)) { + # Update progress bar every second + Write-Progress -Activity $Message -Status $Status -SecondsRemaining ($Seconds - $Sec) + + # Disable screen saver every 5 minutes + if ( !($Sec % 300) ) { Set-SS -TimeOut 0 -Active 0 -IsSecure 0 } + + Start-Sleep -Seconds 1 + + # Restore screen saver during last second... literally + if ($Sec -eq $Seconds) { Set-SS -TimeOut 900 -Active 1 -IsSecure 1 } +} \ No newline at end of file diff --git a/Registry/Suspend-SSWithAuth.ps1 b/Registry/Suspend-SSWithAuth.ps1 new file mode 100644 index 0000000..ad9c5c0 --- /dev/null +++ b/Registry/Suspend-SSWithAuth.ps1 @@ -0,0 +1,53 @@ +[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null + +#Get User's DN +$objSearcher = New-Object System.DirectoryServices.DirectorySearcher +$objSearcher.SearchRoot = New-Object System.DirectoryServices.DirectoryEntry +$objSearcher.Filter = "(&(objectCategory=user)(SamAccountname=$($env:USERNAME)))" +$objSearcher.SearchScope = "Subtree" +$obj = $objSearcher.FindOne() +$User = $obj.Properties["distinguishedname"] + +#Now get the members of the group +$Group = "Workstation Admins" +$objSearcher.Filter = "(&(objectCategory=group)(SamAccountname=$Group))" +$objSearcher.SearchScope = "Subtree" +$obj = $objSearcher.FindOne() +[String[]]$Members = $obj.Properties["member"] + +If ($Members -notcontains $User) { + [System.Windows.Forms.MessageBox]::Show("You are not authorized to run this.", "Suspend Screensaver", 0) +} Else { + [string]$regPath = 'HKCU:\Software\Policies\Microsoft\Windows\Control Panel\Desktop\' + [string]$status = "Time before screen saver default settings are restored:" + + # validate input; must be between 15-240 + do { [int]$minutes = Read-Host "Minutes to suspend screen saver (15-240)" } + while ((15..240) -notcontains $minutes) + + [Int32]$seconds = $minutes * 60 + [string]$message = "Screen saver suspended for $minutes minutes..." + + # function for setting screen saver properties... saves lines later + function Set-SS { + Set-ItemProperty -Path $regPath -Name ScreenSaveTimeOut -Value $args[0] + Set-ItemProperty -Path $regPath -Name ScreenSaveActive -Value $args[1] + Set-ItemProperty -Path $regPath -Name ScreenSaverIsSecure -Value $args[2] + } + + # disable screen saver + Set-SS 0 0 0 + + ForEach ($sec in (1..$seconds)) { + # update progress bar every second + Write-Progress -Activity $message -Status $status -SecondsRemaining ($seconds - $sec) + + # disable screen saver every 5 minutes + If ( !($sec % 300) ) { Set-SS 0 0 0 } + + Start-Sleep -Seconds 1 + + # restore screen saver during last second... literally + If ($sec -eq $seconds) { Set-SS 900 1 1 } + } +} diff --git a/RemoteApp/Install-Apps.ps1 b/RemoteApp/Install-Apps.ps1 new file mode 100644 index 0000000..52e6fad --- /dev/null +++ b/RemoteApp/Install-Apps.ps1 @@ -0,0 +1,132 @@ +$url = 'RDSBROKER.COMPANY.COM' +$domain = 'DOMAIN' + +Write-Host "Cleaning up previous RemoteApp install..." + +try { + $feeds = Get-ChildItem 'HKCU:\Software\Microsoft\Workspaces\Feeds' -ErrorAction Stop +} +catch { + Write-Host "No feeds found" +} + +if ($feeds) { + foreach ($feed in $feeds) { + $id = (Get-ItemProperty $feed.PSPath -Name WorkspaceId).WorkspaceId + + if ($id -eq $url) { + Write-Host "Previous install found" + + Write-Host "Removing Workspace folder..." + $workspaceFolder = (Get-ItemProperty $feed.PSPath -Name WorkspaceFolder).WorkspaceFolder + Remove-Item $workspaceFolder -Recurse -ErrorAction SilentlyContinue + + Write-Host "Removing Desktop icons..." + $startFolder = (Get-ItemProperty $feed.PSPath -Name StartMenuRoot).StartMenuRoot + $apps = Get-ChildItem $startFolder + $desktopIcons = Get-ChildItem "$env:USERPROFILE\Desktop" + foreach ($icon in $desktopIcons) { + if ($apps.Name -contains $icon.Name) { + Remove-Item $icon.FullName + } + } + + Write-Host "Removing Start Menu items..." + Remove-Item $startFolder -Recurse + + Write-Host "Removing registry items..." + Remove-Item $feed.PSPath -Recurse + } + + Write-Host "Cleanup complete" + + break + } +} + +Write-Host "`n`nEnter your credentials..." + +try { + $creds = Get-Credential -Credential $null +} +catch { + Write-Warning "You must enter credentials to complete the setup" + Read-Host "`n`nPress [Enter] to exit" + exit +} + +$username = $creds.UserName +$password = $creds.GetNetworkCredential().Password + +Write-Host "`n`nAdding credentials to Credential Manager..." +cmdkey /add:$url /user:$domain$username /password:$password +cmdkey /add:*.company.com /user:$domain$username /password:$password +cmdkey /add:TERMSRV/$url /user:$domain$username /password:$password + +Write-Host "`n`nSetting up Workspace..." + +$winVer = [System.Environment]::OSVersion.Version.Major +if ($winVer -ne "10" ) { + Write-Host "Windows 10 not detected" + $userProf = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\RemoteApp and Desktop Connections\Work Resources\" +} +else { + Write-Host "Windows 10 detected" + $userProf = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Work Resources (RADC)" +} + +# Create the wcx file +Write-Host "Creating setup wcx file..." +$wcxPath = "$env:TEMP\RDSWebSetup.wcx" +$config = @" + + + + +"@ +New-Item -Path $wcxPath -ItemType "File" -Value $config -Force | Out-Null + +# Silently run the RemoteApp config +Write-Host "Running wcx setup..." +rundll32.exe tsworkspace, WorkspaceSilentSetup $wcxPath + + + +# Wait until the icons appear in the user profile and then copy them to the desktop +Write-Host "Waiting for Workspace icons to be created..." +$counter = 0 +$timeout = $false + +while (-not (Test-Path $userProf) -and $counter -lt 15) { + Start-Sleep -Seconds 2 + $counter++ +} + +if ($counter -eq 15) { + $timeout = $true +} + +$found = $false + +$feeds = Get-ChildItem 'HKCU:\Software\Microsoft\Workspaces\Feeds' +foreach ($feed in $feeds) { + $id = (Get-ItemProperty $feed.PSPath -Name WorkspaceId).WorkspaceId + + if ($id -eq $url) { + $found = $true + } +} + +if (-not $found -or $timeout) { + Write-Host "`n`nCredentials invalid or timeout reached. Please follow the instructions in the new window..." -ForegroundColor Red + + Start-Process $wcxPath -Wait +} + +Remove-Item $wcxPath + +Write-Host "`n`nCopying icons to desktop..." +Copy-Item "$userProf\*" "$env:USERPROFILE\Desktop\" -Recurse -Force + +Read-Host "`n`nPress [Enter] to exit" +exit \ No newline at end of file diff --git a/RemoteApp/Update-Apps.ps1 b/RemoteApp/Update-Apps.ps1 new file mode 100644 index 0000000..c709592 --- /dev/null +++ b/RemoteApp/Update-Apps.ps1 @@ -0,0 +1,24 @@ +$url = 'RDSBROKER.COMPANY.COM' + +$feeds = Get-ChildItem 'HKCU:\Software\Microsoft\Workspaces\Feeds' +foreach ($feed in $feeds) { + $id = (Get-ItemProperty $feed.PSPath -Name WorkspaceId).WorkspaceId + + if ($id -eq $url) { + # Remove Start folder and desktop icons + $startFolder = (Get-ItemProperty $feed.PSPath -Name StartMenuRoot).StartMenuRoot + $apps = Get-ChildItem $startFolder + $desktopIcons = Get-ChildItem "$env:USERPROFILE\Desktop" + foreach ($icon in $desktopIcons) { + if ($apps.Name -contains $icon.Name) { + Remove-Item $icon.FullName + } + } + } +} + +rundll32 tsworkspace,TaskUpdateWorkspaces2 + +Start-Sleep -Seconds 2 + +Copy-Item "$startFolder\*" "$env:USERPROFILE\Desktop\" -Recurse -Force \ No newline at end of file diff --git a/Retire-CMApplication.ps1 b/Retire-CMApplication.ps1 index a299076..ddfe82d 100644 --- a/Retire-CMApplication.ps1 +++ b/Retire-CMApplication.ps1 @@ -6,7 +6,7 @@ function Retire-CMApplication { ) # import cm module - Import-Module 'C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1' + Import-Module ($Env:SMS_ADMIN_UI_PATH.Substring(0,$Env:SMS_ADMIN_UI_PATH.Length-5) + '\ConfigurationManager.psd1') # change to the cm site drive $PSD = Get-PSDrive -PSProvider CMSite diff --git a/SCCM/Invoke-CMDeploymentSummary.ps1 b/SCCM/Invoke-CMDeploymentSummary.ps1 new file mode 100644 index 0000000..19189c8 --- /dev/null +++ b/SCCM/Invoke-CMDeploymentSummary.ps1 @@ -0,0 +1,35 @@ +[CmdletBinding()] +Param( + [Parameter(Mandatory = $true)] + [string]$CollectionName, + + [Parameter(Mandatory = $true)] + [pscredential]$Credential +) + +Import-Module "C:\powershell-scripts\dependencies\ConfigurationManager.psd1" +$StartingLocation = Get-Location +Set-Location -Path "$(Get-PSDrive -PSProvider CMSite):\" -ErrorAction Stop + +Invoke-Command -Credential $Credential -ComputerName dcsccm03 -ScriptBlock { + Param($CollectionName) + + Import-Module "C:\powershell-scripts\dependencies\ConfigurationManager.psd1" + $StartingLocation = Get-Location + Set-Location -Path "$(Get-PSDrive -PSProvider CMSite):\" -ErrorAction Stop + + Invoke-CMDeploymentSummarization -CollectionName $CollectionName -Verbose + + Set-Location $StartingLocation + + Start-Sleep -Seconds 10 +} -ArgumentList $CollectionName + +Get-CMDeployment -CollectionName $CollectionName | + Select-Object -Property ApplicationName, NumberSuccess, NumberTargeted, SummarizationTime, + @{ + Name = 'DeploymentSummary' + Expression = { "{0:P0}" -f ($_.NumberSuccess / $_.NumberTargeted) } + } + +Set-Location $StartingLocation \ No newline at end of file diff --git a/SCCM/Manage-CMMobileDevice.ps1 b/SCCM/Manage-CMMobileDevice.ps1 new file mode 100644 index 0000000..9596708 --- /dev/null +++ b/SCCM/Manage-CMMobileDevice.ps1 @@ -0,0 +1,361 @@ +# Search and replace the following strings with whatever's appropriate for your environment +# CMSERVER, DOMAINNAME, MOBILEDEVICECOLLECTIONID + +# Update the output log that the user sees, and scroll to keep up +function Update-Log { + param( + [string]$Text, + + [Parameter(Mandatory = $false)] + #[ValidateSet('White', 'Yellow', 'Red', 'Green')] + [string]$Color = 'White', + + [switch]$NoNewLine + ) + + $LogTextBox.SelectionColor = $Color + $LogTextBox.AppendText("$Text") + if (-not $NoNewLine) { $LogTextBox.AppendText("`n") } + $LogTextBox.Update() + $LogTextBox.ScrollToCaret() +} + +# Verify that the username exists +function Test-User { + param([string]$UserID) + + $UserObject = Get-CMUser -Name "DOMAINNAME\$UserID" + + if ($UserObject -ne $null) { + Update-Log "$UserID is a valid user." + + return $UserObject + } else { + if ($UserID -eq '') { + Update-Log "User ID field is blank." -Color Red -NoNewLine + } else { + Update-Log "$UserID was not found." -Color Red -NoNewLine + } + + Update-Log " Aborted - no actions performed.`n" -Color Red + + return + } +} + +# verify that the device exists +function Test-Device { + param([string]$DeviceID) + + $DeviceObject = Get-CMDevice -ResourceId $DeviceID + + if ($DeviceObject -ne $null) { + Update-Log "$DeviceID is a valid device ID." + return $DeviceObject + } else { + if ($DeviceID -eq '') { + Update-Log "device ID field is blank." -Color Red -NoNewLine + } else { + Update-Log "$DeviceID was not found." -Color Red -NoNewLine + } + + Update-Log " Aborted - no actions performed." -Color Red + + return + } +} + +# allow the user to use "*" to search for usernames and select the one they want +function Get-User { + param([string]$UserID) + + $Users = Get-CMUser -Name "DOMAINNAME\$UserID" + + if ($Users -ne $null) { + try { + $Selected = if ($Users.Count -gt 1) { + $Users.Name | Out-GridView -Title 'Select a user' -OutputMode Single + } else { + $Users.Name + } + + $Selected = $Selected.Replace("DOMAINNAME\","") + $Selected = $Selected.Split(" ")[0] + $UserTextBox.Text = $Selected + + Update-Log "Updated user ID field to $Selected." + } catch { } + } else { + if ($UserID -eq '') { + Update-Log "User ID field is blank." -Color Yellow + } else { + Update-Log "No users were found with ID $UserID." -Color Yellow + } + } +} + +# get the mobile devices assigned to the user +function Get-UserDevices { + param([string]$UserID) + + $UserObject = Test-User $UserID + + Try { + # Get all the primary devices for the user + $PrimaryDevices = Get-CMUserDeviceAffinity -UserId $UserObject.ResourceID + + # With those id's, create a list of device objects + Update-Log 'Searching for devices' -NoNewLine + + $Devices = foreach ($Device in $PrimaryDevices) { + Get-CMDevice -ResourceID $Device.ResourceID + Update-Log '.' -NoNewLine + } + + Update-Log '' + + # Filter the objects list so we're only looking at mobile devices not equal to OS X + $MobileDevices = $Devices | Where-Object { ($_.ClientType -eq 3) -and ($_.DeviceOS -notlike "OS X*") } + + # Output what we have and let + $Selected = if ($MobileDevices.Count -gt 1) { + $MobileDevices | Select-Object -Property ResourceID, Name, DeviceOS, LastActiveTime, WipeStatus | Out-GridView -Title 'Select a device' -OutputMode Single + } else { + $MobileDevices + } + + if ($Selected) { + $DeviceIDTextBox.Text = $Selected.ResourceID + $DeviceNameTextBox.Text = $Selected.Name + Update-Log "Set device field to resource ID $($Selected.ResourceID) and name field to $($Selected.Name)." + } else { + Update-Log "No mobile devices found for $UserID." + } + } Catch { + Update-Log "There was a problem trying to update the device ID and name fields." -Color Yellow + } +} + +# generate a list of all mobile devices inactive for 30+ days +function Get-InactiveDevices { + Update-Log 'Searching for devices' -NoNewLine + Get-CMDevice -CollectionId 'MOBILEDEVICECOLLECTIONID' | Where-Object { $_.LastActiveTime -lt (Get-Date).AddDays(-30) } | ForEach-Object { + New-Object PSObject -Property @{ + UserName = (Get-CMUserDeviceAffinity -DeviceId $_.ResourceID).UniqueUserName.Replace("DOMAINNAME\","") + ResourceID = [string]$_.ResourceID + Name = $_.Name + DeviceOS = $_.DeviceOS + LastActiveTime = $_.LastActiveTime + WipeStatus = $_.WipeStatus + } + + Update-Log '.' -NoNewLine + } | Select-Object -Property UserName, ResourceID, Name, DeviceOS, LastActiveTime, WipeStatus | Out-GridView -Title 'Inactive devices' + + Update-Log '' +} + +function Write-Title { + Update-Log " __ ___ __ _ __ ___ _ " -Color Orange + Update-Log " / |/ /__ / / (_) /__ / _ \___ _ __(_)______ " -Color Orange + Update-Log " / /|_/ / _ \/ _ \/ / / -_) / // / -_) |/ / / __/ -_)" -Color Orange + Update-Log " /_/__/_/\___/_.__/_/_/\__/ /____/\__/|___/_/\__/\__/ " -Color Orange + Update-Log " / |/ /__ ____ ___ ____ ____ __ _ ___ ___ / /_ " -Color Orange + Update-Log " / /|_/ / _ ``/ _ \/ _ ``/ _ ``/ -_) ' \/ -_) _ \/ __/ " -Color Orange + Update-Log " /_/ /_/\_,_/_//_/\_,_/\_, /\__/_/_/_/\__/_//_/\__/ v1.2" -Color Orange + Update-Log " /___/ " -Color Orange -NoNewLine + Update-Log " by Nick Rodriguez " -Color Gold + Update-Log '' +} + +function New-UtilityForm { + # References for building forms + [void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing") + [void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") + + $Form = New-Object System.Windows.Forms.Form + $Form.FormBorderStyle = 'FixedDialog' + $Form.Text = 'Config Manager Mobile Device Management' + $Form.Size = New-Object System.Drawing.Size(490, 590) + $Form.StartPosition = "CenterScreen" + + # Creates output textbox + $LogTextBox = New-Object System.Windows.Forms.RichTextBox + $LogTextBox.Location = New-Object System.Drawing.Size(12, 120) + $LogTextBox.Size = New-Object System.Drawing.Size(460, 430) + $LogTextBox.ReadOnly = 'True' + $LogTextBox.BackColor = 'Black' + $LogTextBox.ForeColor = 'White' + $LogTextBox.Font = 'Consolas' + $Form.Controls.Add($LogTextBox) + + # User id input + $UserSearchButton = New-Object System.Windows.Forms.Button + $UserSearchButton.Location = New-Object System.Drawing.Size(12, 14) + $UserSearchButton.Size = New-Object System.Drawing.Size(75, 22) + $UserSearchButton.Text = "User ID" + $UserSearchButton.Add_Click( + { Script:Get-User $UserTextBox.Text }) + $Form.Controls.Add($UserSearchButton) + + $UserTextBox = New-Object System.Windows.Forms.TextBox + $UserTextBox.Location = New-Object System.Drawing.Size(110, 15) + $UserTextBox.Size = New-Object System.Drawing.Size(60, 20) + $Form.Controls.Add($UserTextBox) + + # Button to search for devices assigned to user + $DeviceSearchButton = New-Object System.Windows.Forms.Button + $DeviceSearchButton.Location = New-Object System.Drawing.Size(250, 14) + $DeviceSearchButton.Size = New-Object System.Drawing.Size(75, 22) + $DeviceSearchButton.Text = 'Device ID' + $DeviceSearchButton.Add_Click({ Get-UserDevices $UserTextBox.Text }) + $Form.Controls.Add($DeviceSearchButton) + + # Device ID + $DeviceIDTextBox = New-Object System.Windows.Forms.TextBox + $DeviceIDTextBox.Location = New-Object System.Drawing.Size(345, 15) + $DeviceIDTextBox.Size = New-Object System.Drawing.Size(125, 20) + $DeviceIDTextBox.ReadOnly = 'True' + $Form.Controls.Add($DeviceIDTextBox) + + # Device ID + $DeviceNameLabel = New-Object System.Windows.Forms.Label + $DeviceNameLabel.Location = New-Object System.Drawing.Size(250, 48) + $DeviceNameLabel.Size = New-Object System.Drawing.Size(75, 20) + $DeviceNameLabel.Text = "Device Name" + $Form.Controls.Add($DeviceNameLabel) + + # Device Name + $DeviceNameTextBox = New-Object System.Windows.Forms.TextBox + $DeviceNameTextBox.Location = New-Object System.Drawing.Size(345, 44) + $DeviceNameTextBox.Size = New-Object System.Drawing.Size(125, 20) + $DeviceNameTextBox.ReadOnly = 'True' + $Form.Controls.Add($DeviceNameTextBox) + + # Retire button + $MigrateButton = New-Object System.Windows.Forms.Button + $MigrateButton.Location = New-Object System.Drawing.Size(50, 85) + $MigrateButton.Size = New-Object System.Drawing.Size(75, 22) + $MigrateButton.Text = 'Retire' + $MigrateButton.Add_Click({ + try { + Invoke-CMDeviceRetire -Id $DeviceIDTextBox.Text -Force + Update-Log "Successfully retired $($DeviceNameTextBox.Text) [$($DeviceIDTextBox.Text)]." -Color Green + } catch { + Update-Log "There was a problem retiring $($DeviceNameTextBox.Text) [$($DeviceIDTextBox.Text)]:" -Color Red + Update-Log $_.Exception.Message -Color Red + } + }) + $Form.Controls.Add($MigrateButton) + + # Lock button + $LockButton = New-Object System.Windows.Forms.Button + $LockButton.Location = New-Object System.Drawing.Size(150, 85) + $LockButton.Size = New-Object System.Drawing.Size(75, 22) + $LockButton.Text = 'Lock' + $LockButton.Add_Click({ + try { + Invoke-CMDeviceAction -Id $DeviceIDTextBox.Text -Action Lock -ErrorAction Stop + + Update-Log "Lock on $($DeviceNameTextBox.Text) [$($DeviceIDTextBox.Text)] successfully initiated." -Color Green + } catch { + Update-Log "There was a problem performing Lock on $($DeviceNameTextBox.Text) [$($DeviceIDTextBox.Text)]:" -Color Red + Update-Log $_.Exception.Message -Color Red + } + }) + $Form.Controls.Add($LockButton) + + # Reset pin button + $ResetPinButton = New-Object System.Windows.Forms.Button + $ResetPinButton.Location = New-Object System.Drawing.Size(250, 85) + $ResetPinButton.Size = New-Object System.Drawing.Size(75, 22) + $ResetPinButton.Text = 'Reset Pin' + $ResetPinButton.Add_Click({ + try { + Invoke-CMDeviceAction -Id $DeviceIDTextBox.Text -Action PinReset -ErrorAction Stop + + Update-Log "Pin Reset on $($DeviceNameTextBox.Text) [$($DeviceIDTextBox.Text)] successfully initiated." -Color Green + } catch { + Update-Log "There was a problem performing Pin Reset on $($DeviceNameTextBox.Text) [$($DeviceIDTextBox.Text)]:" -Color Red + Update-Log $_.Exception.Message -Color Red + } + }) + $Form.Controls.Add($ResetPinButton) + + # Status button + $StatusButton = New-Object System.Windows.Forms.Button + $StatusButton.Location = New-Object System.Drawing.Size(350, 85) + $StatusButton.Size = New-Object System.Drawing.Size(75, 22) + $StatusButton.Text = 'Status' + $StatusButton.Add_Click({ + try { + $Action = Get-CMDeviceAction -Id $DeviceIDTextBox.Text -Fast -ErrorAction Stop + + if ($Action -ne $null) { + Update-Log "Getting action history for $($DeviceNameTextBox.Text) [$($DeviceIDTextBox.Text)]..." + Update-Log "Action`t`tState`t`tLast Update Time`tPin" -Color Gray + + $Action | ForEach-Object { + Update-Log "$($_.Action)$(if ($_.Action -eq 'Lock') { "`t" })`t" -NoNewLine + Update-Log "$(switch ($_.State) { 1 { "Pending`t" } 4 { "Complete" } })`t" -NoNewLine + Update-Log "$($_.LastUpdateTime)`t" -NoNewLine + if ($_.Action -eq 'PinReset') { + $PinResetState = Get-WmiObject -ComputerName CMSERVER -NameSpace root/SMS/site_$(Get-PSDrive -PSProvider CMSite) ` + -Class SMS_DeviceAction -Filter "Action='PinReset' and ResourceID='$($DeviceIDTextBox.Text)'" -ErrorAction Stop + + $Pin = ([wmi]$PinResetState.__PATH).ResponseText + Update-Log $Pin + } else { + Update-Log 'N/A' + } + } + } + else { + Update-Log "No state information available for $($DeviceNameTextBox.Text) [$($DeviceIDTextBox.Text)]." -Color Yellow + } + } catch { + Update-Log "Failed to get the state information for $($DeviceNameTextBox.Text) [$($DeviceIDTextBox.Text)]:" -Color Red + Update-Log $_.Exception.Message -Color Red + } + }) + $Form.Controls.Add($StatusButton) + + # Report button + $ReportButton = New-Object System.Windows.Forms.Button + $ReportButton.Location = New-Object System.Drawing.Size(110, 44) + $ReportButton.Size = New-Object System.Drawing.Size(110, 22) + $ReportButton.Text = "Inactive Devices" + $ReportButton.Add_Click({ Get-InactiveDevices }) + $Form.Controls.Add($ReportButton) + + # Clear log button + $ClearButton = New-Object System.Windows.Forms.Button + $ClearButton.Location = New-Object System.Drawing.Size(12, 44) + $ClearButton.Size = New-Object System.Drawing.Size(75, 22) + $ClearButton.Text = "Clear Log" + $ClearButton.Add_Click({ $LogTextBox.Clear() }) + $Form.Controls.Add($ClearButton) + + Write-Title + + $Form.Add_Shown({ $Form.Activate() }) + $Form.ShowDialog() +} + +try { + # Verify we have access to CM commands before we continue + Import-Module ConfigurationManager + Set-Location -Path "$(Get-PSDrive -PSProvider CMSite):\" -ErrorAction Stop + + $CMPSSuppressFastNotUsedCheck = $true + + New-UtilityForm +} catch { + [System.Windows.Forms.MessageBox]::Show( + "Failed to set CM site drive. Are you running this from CMSERVER and is the console is up to date? + `n`nError: $($_.Exception.Message)", + 'Fail!' + ) + + exit +} \ No newline at end of file diff --git a/SCCM/Retire-CMApplication.ps1 b/SCCM/Retire-CMApplication.ps1 index 9813701..b1ec9f5 100644 --- a/SCCM/Retire-CMApplication.ps1 +++ b/SCCM/Retire-CMApplication.ps1 @@ -2,26 +2,30 @@ function Retire-CMApplication { [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - $RetiringApps = @() + $RetiringApps = @(), + [Parameter(Mandatory = $false)] + $rename = $false ) # import cm module - Import-Module '\\sccm01\SMS_Company\AdminConsole\bin\ConfigurationManager.psd1' + Import-Module ($Env:SMS_ADMIN_UI_PATH.Substring(0,$Env:SMS_ADMIN_UI_PATH.Length-5) + '\ConfigurationManager.psd1') # change to the cm site drive $PSD = Get-PSDrive -PSProvider CMSite cd "$($PSD):" # for each provided app name, remove deployments, rename, and retire - foreach ($app in $RetiringApps) { - if ($RetiringApp = Get-CMApplication -Name $app) { - Write-Host "So long, $app!" + foreach ($RetiringAppName in $RetiringApps) { + + if ($RetiringApp = Get-CMApplication -Name $RetiringAppName) + { + Write-Host "So long, $RetiringAppName!" # checking retired status, setting to active so that we can make changes - if ($RetiringApp.IsExpired) { - $appWMI = gwmi -Namespace Root\SMS\Site_$PSD -class SMS_ApplicationLatest -Filter "LocalizedDisplayName = '$app'" - $appWMI.SetIsExpired($false) | Out-Null - Write-Host "Setting Status of $app to Active so that changes can be made." + if ($RetiringApp.IsExpired) + { + Resume-CMApplication -Name "$RetiringAppName" + Write-Host "Setting Status of $RetiringAppName to Active so that changes can be made." } $oldDeploys = Get-CMDeployment -SoftwareName $RetiringApp.LocalizedDisplayName @@ -29,19 +33,26 @@ function Retire-CMApplication { # remove all deployments for the app if ($oldDeploys) { $oldDeploys | ForEach-Object { - Remove-CMDeployment -ApplicationName $app -DeploymentId $_.DeploymentID -Force + Remove-CMDeployment -ApplicationName $RetiringAppName -DeploymentId $_.DeploymentID -Force } - Write-Host "Removed $($oldDeploys.Count) deployments of $app." + Write-Host "Removed $($oldDeploys.Count) deployments of $RetiringApp." } # remove content from all dp's and dpg's Write-Host -NoNewline "Removing content from all distribution points" - $DPs = Get-CMDistributionPoint - foreach ($DP in $DPs) { + $DPs = Get-CMDistributionPoint -AllSite + foreach ($DP in $DPs) + { + $dpName = ($dp.NetworkOSPath).Substring(2) + + + Write-Verbose "Removing $RetiringAppName from $dpName" Write-Host -NoNewline "." - try { - Remove-CMContentDistribution -Application $RetiringApp -DistributionPointName ($DP).NetworkOSPath -Force -EA SilentlyContinue - } catch { } + try + { + Remove-CMContentDistribution -ApplicationName "$RetiringAppName" -DistributionPointName $dpName -Force -EA SilentlyContinue #TODO: parallelize this + } + catch { } } Write-Host Write-Host -NoNewline "Removing content from all distribution point groups" @@ -49,33 +60,37 @@ function Retire-CMApplication { foreach ($DPG in $DPGs) { Write-Host -NoNewline "." try { - Remove-CMContentDistribution -Application $RetiringApp -DistributionPointGroupName ($DPG).Name -Force -EA SilentlyContinue + Remove-CMContentDistribution -ApplicationName "$RetiringAppName" -DistributionPointGroupName ($DPG).Name -Force -EA SilentlyContinue #TODO: parallelize this } catch { } } Write-Host - # rename the app - $app = $app.Replace('Retired-', '') - try { - Set-CMApplication -Name $app -NewName "Retired-$app" - } catch { } - Write-Host "Renamed to Retired-$app." + If ($rename){ + # rename the app + $RetiringAppName = $RetiringApp.Replace('Retired-', '') + try { + Set-CMApplication -Name $RetiringAppName -NewName "Retired-$RetiringApp" + } catch { } + Write-Host "Renamed to Retired-$RetiringAppName." - # move the app according to category - if ($RetiringApp.LocalizedCategoryInstanceNames -eq "Mac") { - Move-CMObject -FolderPath "Application\Retired" -InputObject $RetiringApp - Write-Host "Moved to Retired." - } else { - Move-CMObject -FolderPath "Application\Retired" -InputObject $RetiringApp - Write-Host "Moved to Retired." + # move the app according to category + if ($RetiringApp.LocalizedCategoryInstanceNames -eq "Mac") { + Move-CMObject -FolderPath "Application\Retired" -InputObject $RetiringApp + Write-Host "Moved to Retired." + } else { + Move-CMObject -FolderPath "Application\Retired" -InputObject $RetiringApp + Write-Host "Moved to Retired." + } } - + # retire the app - if (!$RetiringApp.IsExpired) { - $appWMI = gwmi -Namespace Root\SMS\Site_$PSD -class SMS_ApplicationLatest -Filter "LocalizedDisplayName = 'Retired-$app'" - $appWMI.SetIsExpired($true) | Out-Null + if (!$RetiringApp.IsExpired) + { + Suspend-CMApplication -Name "$RetiringAppName" Write-Host "Set status to Retired." - } else { + } + else + { Write-Host "Status was already set to Retired." } @@ -85,7 +100,7 @@ function Retire-CMApplication { Write-Host "Don't forget to delete the source files from $loc." } else { - Write-Host "$app was not found. No actions performed." + Write-Host "$RetiringAppName was not found. No actions performed." } } } diff --git a/SCCM/Retire-CMApplicationGUI.ps1 b/SCCM/Retire-CMApplicationGUI.ps1 index c8f7a2a..59d0cfb 100644 --- a/SCCM/Retire-CMApplicationGUI.ps1 +++ b/SCCM/Retire-CMApplicationGUI.ps1 @@ -140,7 +140,7 @@ function Create-UtilityForm { try { # make sure we have access to CM commands before we continue - Import-Module 'E:\SCCM\AdminConsole\bin\ConfigurationManager.psd1' + Import-Module ($Env:SMS_ADMIN_UI_PATH.Substring(0,$Env:SMS_ADMIN_UI_PATH.Length-5) + '\ConfigurationManager.psd1') Set-Location -Path "$(Get-PSDrive -PSProvider CMSite):\" -ErrorAction Stop Create-UtilityForm } catch { diff --git a/Send-LinuxCommand.ps1 b/Send-LinuxCommand.ps1 new file mode 100644 index 0000000..0678d04 --- /dev/null +++ b/Send-LinuxCommand.ps1 @@ -0,0 +1,81 @@ +[CmdletBinding(SupportsShouldProcess = $true)] +Param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [ValidateSet( + 'Linux1', 'Linux2', 'All' + )] + [string[]]$Server, + [switch]$Sudo, + [switch]$Update, + [switch]$Upgrade, + [switch]$DistUpgrade, + [switch]$Clean, + [string[]]$Install, + [string]$Command, + [switch]$Reboot, + [switch]$AsJob, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [PSCredential]$Credential +) + +$PLinkPath = '\\192.168.1.4\data\Programs\plink.exe' +if (-not (Test-Path -Path $PLinkPath)) { + Write-Warning "$PLinkPath not found, downloading..." + try { + Invoke-WebRequest -Uri 'https://the.earth.li/~sgtatham/putty/latest/w64/plink.exe' -OutFile $PLinkPath + } catch { + Write-Warning "Unable to download PLink." + Pause + exit + } +} + +$UserName = $Creds.GetNetworkCredential().UserName +$Password = $Creds.GetNetworkCredential().Password + +$ServerList = @{ + 'Linux1' = '192.168.1.2'; + 'Linux2' = '192.168.1.3'; +} + +$Commands = @("hostname") +if ($Command) { $Commands += $Command } +if ($Sudo) { $Commands += "echo $Password | sudo -S true;" } +if ($Update) { $Commands += "sudo apt -y update;" } +if ($Upgrade) { $Commands += "sudo apt -y upgrade;" } +if ($DistUpgrade) { $Commands += "sudo apt -y dist-upgrade;" } +if ($Clean) { $Commands += @("sudo apt -y autoremove;", "sudo apt -y autoclean;") } +if ($Install) { $Commands += "sudo apt -y install $Install;" } +if ($Reboot) { $Commands += "sudo shutdown -r;" } + +# Make sure the end of each line contains a semicolon +$Commands = $Commands | ForEach-Object { "$($_.ToString().TrimEnd(';'));" } + +if ($Server -eq 'All') { $Server = $ServerList.Keys } + +foreach ($ComputerName in $Server) { + if ($PSCmdlet.ShouldProcess($Server, "Run the following commands:`n$(($Commands | Out-String) -replace $Password, '***')")) { + if ($AsJob) { + Write-Verbose "Running commands as job..." + Start-Job -Name "$ComputerName Update" -ScriptBlock { + param($IP, $UserName, $Password, $Commands, $PLinkPath) + + Set-Alias plink $PLinkPath + Write-Output 'y' | plink -ssh $ServerList.$ComputerName -l $Username -pw $Password exit + plink -batch -ssh $ServerList.$ComputerName -l $UserName -pw $Password $Commands + } -ArgumentList $ServerList.$ComputerName, $UserName, $Password, $Commands, $PLinkPath + } else { + Set-Alias plink $PLinkPath + Write-Output 'y' | plink -ssh $ServerList.$ComputerName -l $Username -pw $Password exit + plink -batch -ssh $ServerList.$ComputerName -l $UserName -pw $Password $Commands + } + } +} + +if ($AsJob) { + Write-Verbose "Waiting for jobs to finish..." + Get-Job | Wait-Job | Receive-Job + Get-Job | Remove-Job -Force +} \ No newline at end of file diff --git a/Set-ScriptRoot.ps1 b/Set-ScriptRoot.ps1 new file mode 100644 index 0000000..9e2a4b1 --- /dev/null +++ b/Set-ScriptRoot.ps1 @@ -0,0 +1,6 @@ +# Set ScripRoot variable to the path which the script is executed from +$ScriptRoot = if ($PSVersionTable.PSVersion.Major -lt 3) { + Split-Path -Path $MyInvocation.MyCommand.Path +} else { + $PSScriptRoot +} \ No newline at end of file diff --git a/Test-IsAdmin.ps1 b/Test-IsAdmin.ps1 new file mode 100644 index 0000000..a035cd9 --- /dev/null +++ b/Test-IsAdmin.ps1 @@ -0,0 +1,8 @@ +function Test-IsAdmin { + $UserIdentity = [Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent() + + if (-not $UserIdentity.IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) { + Update-Log "You are not running this script with an admin account. " -Color 'Red' -NoNewLine + Update-Log "Some tasks may fail if not run with admin credentials.`n" -Color 'Red' + } +} \ No newline at end of file diff --git a/Test-IsIse.ps1 b/Test-IsIse.ps1 new file mode 100644 index 0000000..c715da5 --- /dev/null +++ b/Test-IsIse.ps1 @@ -0,0 +1,2 @@ +# Return $true if run from ISE +function Test-IsISE { if ($psISE) { $true } else { $false } } \ No newline at end of file diff --git a/Validate-CSVHeaders.ps1 b/Validate-CSVHeaders.ps1 index 35b4b09..ae3ae84 100644 --- a/Validate-CSVHeaders.ps1 +++ b/Validate-CSVHeaders.ps1 @@ -8,7 +8,7 @@ function Validate-CSVHeaders ($correctHeaders) { [ValidateSet('Yes','No')]$validateHeaders = Read-Host "Validate headers?" if ($validateHeaders -eq 'Yes') { # put all the headers into a comma separated array - $headers = (Get-Content $fileName | Select-Object -First 1).Split(",") + $headers = (Get-Content $fileName -TotalCount 1).Split(",") for ($i = 0; $i -lt $headers.Count; $i++) { # trim any leading white space and compare the headers @@ -21,4 +21,4 @@ function Validate-CSVHeaders ($correctHeaders) { } } -Validate-CSVHeaders $correctHeaders \ No newline at end of file +Validate-CSVHeaders $correctHeaders diff --git a/Windows Updates/Get-InstalledUpdates.ps1 b/Windows Updates/Get-InstalledUpdates.ps1 new file mode 100644 index 0000000..80e3855 --- /dev/null +++ b/Windows Updates/Get-InstalledUpdates.ps1 @@ -0,0 +1,20 @@ +Function Get-InstalledUpdates($title) { + $UpdateSession = New-Object -ComObject Microsoft.Update.Session + $SearchResult = $null + $UpdateSearcher = $UpdateSession.CreateUpdateSearcher() + $UpdateSearcher.Online = $true + $SearchResult = $UpdateSearcher.Search("IsInstalled=1 and Type='Software'") + + $i = 1 + foreach($Update in $SearchResult.Updates) + { + If ($Update.Title -like "*$title*") { + Write-Host "$i) $($Update.Title + " | " + $Update.SecurityBulletinIDs)" + $i += 1 + } + } +} + +cls + +Get-InstalledUpdates("Lync") \ No newline at end of file diff --git a/Windows Updates/Get-MicrosoftUpdates.ps1 b/Windows Updates/Get-MicrosoftUpdates.ps1 new file mode 100644 index 0000000..f32fb0f Binary files /dev/null and b/Windows Updates/Get-MicrosoftUpdates.ps1 differ diff --git a/Enable-BitLocker.ps1 b/Windows/Enable-BitLocker.ps1 similarity index 97% rename from Enable-BitLocker.ps1 rename to Windows/Enable-BitLocker.ps1 index c62ed5d..99182b5 100644 --- a/Enable-BitLocker.ps1 +++ b/Windows/Enable-BitLocker.ps1 @@ -1,71 +1,71 @@ -$Creds = Get-Credential - -$Computers = @( - 'computer1', 'computer2' -) - -# Log file -$Date = (Get-Date).ToString('yyyyMMdd-HHmm') -$LogFolder = New-Item -ItemType Directory "C:\Logs" -Force -$Log = New-Item -ItemType File "$LogFolder\Enable-BitLocker-$Date.log" -Force - -foreach ($Computer in $Computers) { - - Add-Content $Log "$Computer..." - - if (Test-Connection $Computer -Quiet -Count 3) { - - $Return = Invoke-Command -ComputerName $Computer -Credential $Creds -ScriptBlock { - - # Delete any current BitLocker pin - try { - $Result = ((manage-bde -protectors -delete C: | Select-String -Pattern "(?<=(ERROR:)).*").ToString()).TrimStart() - $Return = 'Deleted old protectors.' - } catch { - $Return += $Error - } - $Return += "`n" - - # Add new Pin and TPM security - try { - $Result = ((manage-bde -protectors -add C: -tp newpin | Select-String -Pattern "(?<=(ERROR:)).*").ToString()).TrimStart() - $Return += 'Added new protectors.' - } catch { - $Return += $Error - } - $Return += "`n" - - # Get the BitLocker's status after changes - $Result = ((manage-bde -status | Select-String -Pattern "(?<=(Protection Status:)).*").ToString()).Replace('Protection Status:', '').TrimStart() - if (!$Result) { - $Return += 'ERROR: Unable to capture BitLocker status.' - } elseif ($Result -eq 'Protection Off') { - # Enable protection - try { - $Result = ((manage-bde -protectors -enable C: | Select-String -Pattern "(?<=(ERROR:)).*").ToString()).TrimStart() - $Return += 'Enabled protectors.' - } catch { - $Return += $Error - } - $Return += "`n" - - # Get the BitLocker's status after changes - $Result = ((manage-bde -status | Select-String -Pattern "(?<=(Protection Status:)).*").ToString()).Replace('Protection Status:', '').TrimStart() - if ($Result -eq 'Protection Off') { - $Return += 'ERROR: Unable to enable protectors.' - } elseif ($Result -eq 'Protection On') { - $Return += $Result - } - } elseif ($Result -eq 'Protection On') { - $Return += $Result - } - - Return $Return - } - - Add-Content $Log $Return - - } else { - Add-Content $Log "ERROR: Failed to connect." - } +$Creds = Get-Credential + +$Computers = @( + 'computer1', 'computer2' +) + +# Log file +$Date = (Get-Date).ToString('yyyyMMdd-HHmm') +$LogFolder = New-Item -ItemType Directory "C:\Logs" -Force +$Log = New-Item -ItemType File "$LogFolder\Enable-BitLocker-$Date.log" -Force + +foreach ($Computer in $Computers) { + + Add-Content $Log "$Computer..." + + if (Test-Connection $Computer -Quiet -Count 3) { + + $Return = Invoke-Command -ComputerName $Computer -Credential $Creds -ScriptBlock { + + # Delete any current BitLocker pin + try { + $Result = ((manage-bde -protectors -delete C: | Select-String -Pattern "(?<=(ERROR:)).*").ToString()).TrimStart() + $Return = 'Deleted old protectors.' + } catch { + $Return += $Error + } + $Return += "`n" + + # Add new Pin and TPM security + try { + $Result = ((manage-bde -protectors -add C: -tp newpin | Select-String -Pattern "(?<=(ERROR:)).*").ToString()).TrimStart() + $Return += 'Added new protectors.' + } catch { + $Return += $Error + } + $Return += "`n" + + # Get the BitLocker's status after changes + $Result = ((manage-bde -status | Select-String -Pattern "(?<=(Protection Status:)).*").ToString()).Replace('Protection Status:', '').TrimStart() + if (!$Result) { + $Return += 'ERROR: Unable to capture BitLocker status.' + } elseif ($Result -eq 'Protection Off') { + # Enable protection + try { + $Result = ((manage-bde -protectors -enable C: | Select-String -Pattern "(?<=(ERROR:)).*").ToString()).TrimStart() + $Return += 'Enabled protectors.' + } catch { + $Return += $Error + } + $Return += "`n" + + # Get the BitLocker's status after changes + $Result = ((manage-bde -status | Select-String -Pattern "(?<=(Protection Status:)).*").ToString()).Replace('Protection Status:', '').TrimStart() + if ($Result -eq 'Protection Off') { + $Return += 'ERROR: Unable to enable protectors.' + } elseif ($Result -eq 'Protection On') { + $Return += $Result + } + } elseif ($Result -eq 'Protection On') { + $Return += $Result + } + + Return $Return + } + + Add-Content $Log $Return + + } else { + Add-Content $Log "ERROR: Failed to connect." + } } \ No newline at end of file diff --git a/Windows/Enable-Ping.ps1 b/Windows/Enable-Ping.ps1 new file mode 100644 index 0000000..d81b88b --- /dev/null +++ b/Windows/Enable-Ping.ps1 @@ -0,0 +1,3 @@ +Invoke-Command -ComputerName (Get-ADComputer -Filter *).DNSHostName -ScriptBlock { + New-NetFirewallRule –DisplayName “Allow Ping” –Direction Inbound –Action Allow –Protocol icmpv4 –Enabled True +} \ No newline at end of file diff --git a/Windows/Enable-RDP.ps1 b/Windows/Enable-RDP.ps1 new file mode 100644 index 0000000..a0dcec8 --- /dev/null +++ b/Windows/Enable-RDP.ps1 @@ -0,0 +1,3 @@ +Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server'-name "fDenyTSConnections" -Value 0 +Enable-NetFirewallRule -DisplayGroup "Remote Desktop" +Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -name "UserAuthentication" -Value 1 \ No newline at end of file diff --git a/Windows/Get-BackupStatus.ps1 b/Windows/Get-BackupStatus.ps1 new file mode 100644 index 0000000..a358f5b --- /dev/null +++ b/Windows/Get-BackupStatus.ps1 @@ -0,0 +1,15 @@ +if ($env:COMPUTERNAME -ne 'BackupServer') { + Write-Warning 'This needs to be run from [BackupServer]' + Pause + exit +} + +Get-WBJob + +while ((Get-WBJob).JobState -eq 'Running') { + (Get-WBJob).CurrentOperation + Start-Sleep -Seconds 30 +} + +Get-WBJob +Pause \ No newline at end of file diff --git a/Windows/Get-ComputerNetInfo.ps1 b/Windows/Get-ComputerNetInfo.ps1 new file mode 100644 index 0000000..eed3128 --- /dev/null +++ b/Windows/Get-ComputerNetInfo.ps1 @@ -0,0 +1,31 @@ +function Get-ComputerNetInfo { + param ( + [Parameter( + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true + )] + [string[]]$ComputerName = $env:COMPUTERNAME + ) + + process { + foreach ($Computer in $ComputerName ) { + # Get hostname and IP address from DNS provider + Write-Verbose "Getting DNS providor info for $Computer..." + $DNSHostEntry = [System.Net.Dns]::GetHostEntry($Computer) + $HostName = $DNSHostEntry.HostName + $IPAddress = $DNSHostEntry.AddressList.IPAddressToString.Split('.', 1)[0] + + # Get MAC from WMI class + Write-Verbose "Getting WMI providor info for $Computer..." + $NetAdapter = Get-WmiObject -Class Win32_NetworkAdapterConfiguration -ComputerName $Computer + $MACAddress = ($NetAdapter | Where-Object {$_.IpAddress -eq $IPAddress}).MACAddress + + # Create and output a custom psobject with the net info + $NetInfo = New-Object -TypeName psobject + $NetInfo | Add-Member -MemberType NoteProperty -Name DNSHostName -Value $HostName + $NetInfo | Add-Member -MemberType NoteProperty -Name IPAddress -Value $IPAddress + $NetInfo | Add-Member -MemberType NoteProperty -Name MACAddress -Value $MACAddress + Write-Output $NetInfo + } + } +} \ No newline at end of file diff --git a/Windows/Get-DetailsofGroupsUserMemberOf.ps1 b/Windows/Get-DetailsofGroupsUserMemberOf.ps1 new file mode 100644 index 0000000..5f34a37 --- /dev/null +++ b/Windows/Get-DetailsofGroupsUserMemberOf.ps1 @@ -0,0 +1,3 @@ +#Powershell one line script to get details of all groups user memberof + +Get-ADPrincipalGroupMembership ashishanand |Get-ADGroup -Properties * | select name, managedby, * diff --git a/Windows/Get-DiskInfo.ps1 b/Windows/Get-DiskInfo.ps1 new file mode 100644 index 0000000..c57b1f0 --- /dev/null +++ b/Windows/Get-DiskInfo.ps1 @@ -0,0 +1,69 @@ +<# +.Synopsis + Get info on each partition and logical volume of a machine. + +#> +[CmdletBinding()] +param( + [Parameter( + ValueFromPipeline=$true, + ValueFromPipelineByPropertyName=$true, + Position = 0 + )] + [string[]]$ComputerName = $env:COMPUTERNAME, + + [pscredential]$Credential +) + +foreach ($Computer in $ComputerName) { + try { + $WmiObjectParams = @{ + ComputerName = $Computer + Credential = $creds + Class = 'Win32_DiskPartition' + Property = 'Name, DiskIndex, Type' + } + $Disks = Get-WmiObject @WmiObjectParams -ErrorAction Stop | + Select-Object -Property Name, DiskIndex, @{ + Name="GPT" + Expression = { $_.Type.StartsWith("GPT") } + } + } catch { + New-Object -TypeName psobject -Property @{ + ComputerName = $Computer + Error = $_.Exception.Message + VolumeName = '' + DiskName = '' + GPT = '' + DiskIndex = '' + DriveLetter = '' + FreeSpaceGB = '' + Utilization = '' + SizeGB = '' + FileSystem = '' + } + continue + } + + Get-WmiObject Win32_LogicalDisk -ComputerName $Computer -Credential $creds | + Where-Object -Property DriveType -EQ 3 | Foreach-Object { + $Query = "Associators of {Win32_LogicalDisk.DeviceID='$($_.DeviceID)'} WHERE ResultRole=Antecedent" + $Volume = Get-WmiObject -ComputerName $Computer -Credential $creds -Query $Query + + $Disk = $Disks | Where-Object -Property DiskIndex -EQ $Volume.DiskIndex + + New-Object -TypeName psobject -Property @{ + ComputerName = $Computer + Error = '' + VolumeName = $_.VolumeName + DiskName = $Disk.Name -join ', ' + GPT = $Disk.GPT -join ', ' + DiskIndex = $Volume.DiskIndex + DriveLetter = $_.DeviceID + FreeSpaceGB = [Math]::Round($_.FreeSpace / 1GB, 2) + Utilization = (1 - (($_.FreeSpace / 1GB) / ($Volume.Size / 1GB))).ToString('P') + SizeGB = [Math]::Round($Volume.Size / 1GB, 2) + FileSystem = $_.FileSystem + } + } +} \ No newline at end of file diff --git a/Windows/Get-NTPTimeOffset.ps1 b/Windows/Get-NTPTimeOffset.ps1 new file mode 100644 index 0000000..2b6bd7d --- /dev/null +++ b/Windows/Get-NTPTimeOffset.ps1 @@ -0,0 +1,80 @@ +$Times = @{} +$Count = 0 +$CountLimit = 60 +$Server = 'server05' +$ServerToCompare = 'server06' + +function Get-TimeOffset { + $ResultObject = New-Object -TypeName psobject + Add-Member -InputObject $ResultObject NoteProperty Computer 'pool.ntp.org' + $W32TMResult = w32tm /stripchart /computer:'pool.ntp.org' /dataonly /samples:3 /ipprotocol:4 + + if (-not ($W32TMResult -is [array])) { + Add-Member -InputObject $ResultObject NoteProperty Status "Offline" + Add-Member -InputObject $ResultObject NoteProperty Offset $null + } else { + $FoundTime = $false + + # Go through the 5 samples to find a response with timeoffset + for ($i = 3; $i -lt 8; $i++) { + if (-not $FoundTime) { + if ($W32TMResult[$i] -match ", ([-+]\d+\.\d+)s") { + $Offset = [float]$Matches[1] + Add-Member -InputObject $ResultObject NoteProperty Status "Online" + Add-Member -InputObject $ResultObject NoteProperty Offset $Offset + $FoundTime = $true + } + } + } + + # If no time samples were found check for error + if (-not $FoundTime) { + if ($W32TMResult[3] -match "error") { + #0x800705B4 is not advertising/responding + Add-Member -InputObject $ResultObject NoteProperty Status "NTP not responding" + } else { + Add-Member -InputObject $ResultObject NoteProperty Status $W32TMResult[3] + } + Add-Member -InputObject $ResultObject NoteProperty Offset $null + } + } + + $ResultObject +} + +while ($true) { + $Count++ + + if (Test-Connection -Quiet -ComputerName $Server) { + $Offset = try { + $Result = Invoke-Command -ComputerName $Server -ScriptBlock ${function:Get-TimeOffset} + try { $Result.Offset } catch { $Result.Status } + } catch { + $_ | Out-String + } + + $Times.Add((Get-Date), $Offset) + Start-Sleep -Seconds 10 + } else { + $Times.Add((Get-Date), 'Unreachable') + Start-Sleep -Seconds 10 + } + + if ($Count -ge $CountLimit) { + $OffsetToCompare = try { + $Result = Invoke-Command -ComputerName $Server -ScriptBlock ${function:Get-TimeOffset} + try { $Result.Offset } catch { $Result.Status } + } catch { + $_ | Out-String + } + + $SortedTimesString = $Times.GetEnumerator() | Sort-Object -Property Name | Out-String + + Send-MailMessage -Body "$ServerToCompare offset = $OffsetToCompare`n`n$Server datestamp and offset:`n$SortedTimesString" ` + -Subject 'server05 time script' -From 'Alerts@company.com' -To 'me@company.com' -SmtpServer smtp.company.local + + Write-Host 'Reset the clocks...' + $Count = 0 + $Times = @{} + } +} \ No newline at end of file diff --git a/Windows/Get-NetSessionInfoString.ps1 b/Windows/Get-NetSessionInfoString.ps1 new file mode 100644 index 0000000..1c65c72 --- /dev/null +++ b/Windows/Get-NetSessionInfoString.ps1 @@ -0,0 +1,13 @@ +$Creds = Get-Credential + +$Servers = (Get-ADGroup -Identity 'Domain Controllers' | Get-ADGroupMember).Name + +foreach ($Server in $Servers) { + $Server + + (Invoke-Command -ComputerName $Server -Credential $Creds -ScriptBlock { + $key = "HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\DefaultSecurity" + $name = "SrvsvcSessionInfo" + (Get-ItemProperty -Path $key -Name $name).SrvsvcSessionInfo + }) -join '' +} \ No newline at end of file diff --git a/Windows/Get-ServerLoad.ps1 b/Windows/Get-ServerLoad.ps1 new file mode 100644 index 0000000..bc86f69 --- /dev/null +++ b/Windows/Get-ServerLoad.ps1 @@ -0,0 +1,26 @@ +[CmdletBinding(SupportsShouldProcess = $true)] +Param( + [Parameter( + Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true + )] + [string[]]$ComputerName +) + +foreach ($Computer in $ComputerName) { + $CPULoad = Get-WmiObject -Class win32_processor -ComputerName $Computer | + Measure-Object -Property LoadPercentage -Average | Select-Object Average + + $MemLoad = Get-WmiObject -Class win32_operatingsystem -ComputerName $Computer | + Select-Object @{ + Name = "MemoryUsage" + Expression = { "{0:N2}" -f ((($_.TotalVisibleMemorySize - $_.FreePhysicalMemory) * 100) / $_.TotalVisibleMemorySize) } + } + + New-Object -TypeName PSObject -Property @{ + ComputerName = $Computer + CPUUsage = $CPULoad.Average + MemoryUsage = $MemLoad.MemoryUsage + } +} \ No newline at end of file diff --git a/Windows/Install-RemovedWindowsFeatures.ps1 b/Windows/Install-RemovedWindowsFeatures.ps1 new file mode 100644 index 0000000..833cb0b --- /dev/null +++ b/Windows/Install-RemovedWindowsFeatures.ps1 @@ -0,0 +1,5 @@ +# Get server index numbers +dism /get-wiminfo /wimfile:d:\sources\install.wim + +# Change feature name and final index number +Install-WindowsFeature -Name RDS-Gateway -Source wim:D:\sources\install.wim:1 \ No newline at end of file diff --git a/Windows/Set-TimeZoneEST.ps1 b/Windows/Set-TimeZoneEST.ps1 new file mode 100644 index 0000000..a7ee2f4 --- /dev/null +++ b/Windows/Set-TimeZoneEST.ps1 @@ -0,0 +1 @@ +Get-ADComputer -Filter * | Invoke-Command -ScriptBlock { tzutil /s "Eastern Standard Time" } \ No newline at end of file diff --git a/Windows/Test-Is64BitOs.ps1 b/Windows/Test-Is64BitOs.ps1 new file mode 100644 index 0000000..d5cbe00 --- /dev/null +++ b/Windows/Test-Is64BitOs.ps1 @@ -0,0 +1,8 @@ +# Return $true if 64-bit OS +function Test-64BitOS { + if ((Get-WmiObject Win32_OperatingSystem).OSArchitecture -eq '64-bit') { + $true + } else { + $false + } +} \ No newline at end of file diff --git a/Windows/Test-WebPorts.ps1 b/Windows/Test-WebPorts.ps1 new file mode 100644 index 0000000..7aa5ddf --- /dev/null +++ b/Windows/Test-WebPorts.ps1 @@ -0,0 +1,6 @@ +$Servers = @('server02' ,'server08') +$Servers | ForEach-Object { + Write-Host $_ + Test-NetConnection -ComputerName $_ -Port 80 -InformationLevel Quiet + Test-NetConnection -ComputerName $_ -Port 443 -InformationLevel Quiet +} \ No newline at end of file diff --git a/Windows/UploadUserPhotoinAD.ps1 b/Windows/UploadUserPhotoinAD.ps1 new file mode 100644 index 0000000..4c81d8b --- /dev/null +++ b/Windows/UploadUserPhotoinAD.ps1 @@ -0,0 +1 @@ +Set-UserPhoto "" -PictureData ([System.IO.File]::ReadAllBytes("")) diff --git a/Write-CMTraceLog.ps1 b/Write-CMTraceLog.ps1 new file mode 100644 index 0000000..c896aa6 --- /dev/null +++ b/Write-CMTraceLog.ps1 @@ -0,0 +1,47 @@ +function Write-CMTraceLog { + <# + .Description + Write to a cmtrace readable log. + .Example + $LogFile = "C:\TestFolder\TestLog.Log" + Write-CMTraceLog -LogFile $LogFile + Write-CMTraceLog -Message "This is a normal message" -ErrorMessage $Error -LogFile $LogFile + Write-CMTraceLog -Message "This is a warning" -Type 2 -Component "Test Component" -LogFile $LogFile + Write-CMTraceLog -Message "This is an Error!" -Type 3 -Component "Error Component" -LogFile $LogFile + #> + param ( + [Parameter(Mandatory = $false)] + [string]$Message, + + [Parameter(Mandatory = $false)] + [string]$ErrorMessage, + + [Parameter(Mandatory = $false)] + [string]$Component, + + # 1 = Normal, 2 = Warning (yellow), 3 = Error (red) + [Parameter(Mandatory = $false)] + [int]$Type, + + [Parameter(Mandatory = $true)] + [string]$LogFile + ) + + $Time = Get-Date -Format "HH:mm:ss.ffffff" + $Date = Get-Date -Format "MM-dd-yyyy" + + if ($ErrorMessage -ne $null) { + $Type = 3 + } + if ($Component -eq $null) { + $Component = " " + } + if ($Type -eq $null) { + $Type = 1 + } + + $LogMessage = "" + $LogMessage | Out-File -Append -Encoding UTF8 -FilePath $LogFile + $LogMessage | Out-File -Append -Encoding UTF8 -FilePath $LogFile + $LogMessage | Out-File -Append -Encoding UTF8 -FilePath $LogFile +} \ No newline at end of file diff --git a/Yammer.psm1 b/Yammer.psm1 new file mode 100644 index 0000000..459dfe8 --- /dev/null +++ b/Yammer.psm1 @@ -0,0 +1,635 @@ +$Global:YammerAuthToken = "your-auth-token" + +function Get-YammerAuthHeader { + @{ AUTHORIZATION = "Bearer $YammerAuthToken" } +} + +function Export-YammerData { + <# + .SYNOPSIS + Export Yammer data. + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param( + [Parameter(Mandatory = $false)] + [string]$Path = "$PSScriptRoot\export.zip", + + [Parameter(Mandatory = $false)] + [ValidateSet('User', 'Group', 'Message', 'MessageVersion', 'Topic', 'UploadedFileVersion', 'DocumentVersion')] + [string[]]$Model, + + [Parameter(Mandatory = $true)] + [datetime]$StartDate, + + [Parameter(Mandatory = $false)] + [datetime]$EndDate, + + [Parameter(Mandatory = $false)] + [ValidateSet('Csv', 'All')] + [string]$IncludeFileAttachments + ) + + $Uri = 'https://www.yammer.com/api/v1/export?' + + if ($PSBoundParameters.ContainsKey('Model')) { + foreach ($Type in $Model) { + if (-not $Uri.EndsWith('?')) { + $Uri += '&' + } + $Uri += "model=$Type" + } + } + + if ($PSBoundParameters.ContainsKey('StartDate')) { + if (-not $Uri.EndsWith('?')) { + $Uri += '&' + } + $Uri += "since=$(Get-Date -Date $StartDate -Format s)" + } + + if ($PSBoundParameters.ContainsKey('EndDate')) { + if (-not $Uri.EndsWith('?')) { + $Uri += '&' + } + $Uri += "until=$(Get-Date -Date $EndDate -Format s)" + } + + if ($PSBoundParameters.ContainsKey('IncludeFileAttachments')) { + if (-not $Uri.EndsWith('?')) { + $Uri += '&' + } + $Uri += "include=$IncludeFileAttachments" + } + + if ($pscmdlet.ShouldProcess($Uri, 'Export Yammer data')) { + $authHeader = Get-YammerAuthHeader + Invoke-RestMethod -Uri $Uri -OutFile $Path -Headers $authHeader + } +} + +function Remove-YammerMessage { + <# + .SYNOPSIS + Delete message from Yammer by message ID. + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param( + [Parameter(Mandatory = $true)] + [int]$Id + ) + + $authHeader = Get-YammerAuthHeader + $Uri = "https://yammer.com/api/v1/messages/$Id" + + if ($pscmdlet.ShouldProcess($Id, 'Delete Yammer message')) { + Invoke-RestMethod -Uri $Uri -Method Delete -Headers $authHeader + } +} + +function Get-YammerToken { + <# + .SYNOPSIS + Used to get Yammer API tokens under the current account. + + #> + $IE = New-Object -ComObject InternetExplorer.Application + $IE.Navigate('https://www.yammer.com/client_applications') + $IE.Visible = $true + + Write-Host 'Login and select the app you want to use and provide some information.' + $ClientId = Read-Host 'Client ID' + $ClientSecret = Read-Host 'Client secret' + + $IE.Quit() + + $CodeUrl = "https://www.yammer.com/dialog/oauth?client_id=$ClientId" + $SleepInterval = 1 + + $IE = New-Object -ComObject InternetExplorer.Application + $IE.Navigate($codeUrl) + $IE.Visible = $true + + while ($IE.LocationUrl -notmatch 'code=') { + Write-Debug -Message ('Sleeping {0} seconds for access URL' -f $SleepInterval) + Start-Sleep -Seconds $SleepInterval + } + + Write-Debug -Message ('Callback URL is: {0}' -f $IE.LocationUrl) + [Void]($IE.LocationUrl -match '=([\w\.]+)') + $TempCode = $Matches[1] + + $IE.Quit() + + $Request = Invoke-WebRequest https://www.yammer.com/oauth2/access_token.json?client_id=$ClientId"&"client_secret=$ClientSecret"&"code=$TempCode | + ConvertFrom-Json + + Write-Host "Temporary code used: $TempCode" + $AccessToken = $Request.access_token.token + + $Headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" + $Headers.Add("Authorization", 'Bearer ' + $AccessToken) + + $Headers + + $CurrentUser = Invoke-RestMethod 'https://www.yammer.com/api/v1/users/current.json' -Headers $Headers + $AllTokens = Invoke-RestMethod 'https://www.yammer.com/api/v1/oauth/tokens.json'-Headers $Headers + + + Write-Host $CurrentUser.name + Write-Host $CurrentUser.network_name + Write-Host $CurrentUser.email + + + foreach ($Token in $AllTokens) { + $Token | Format-Table user_id, network_name, token -AutoSize + } +} + +function Compare-YammerADUserJobTitle { + <# + .SYNOPSIS + Export all Yammer users and compare to MSO users. + + .NOTES + Modified from MS script + + #> + [CmdletBinding()] + Param( + $YammerUsers + ) + + $usersAD = Get-ADUser -Filter * -Property Title, mail + + foreach ($user in $YammerUsers) { + $usersAD | Where-Object { + $_.mail -eq $user.email -and $_.Title -ne $user.job_title + } | ForEach-Object { + New-Object -TypeName psobject -Property @{ + Name = $user.name + Id = $user.id + SamAccountName = $_.SamAccountName + UserPrincipalName = $_.UserPrincipalName + Email = $user.email + TitleAD = $_.Title + TitleYammer = $user.job_title + ApiUrl = $user.url + ADEnabled = $_.Enabled + YammerState = $user.state + } + } + } +} + +function Compare-YammerMsoUser { + <# + .SYNOPSIS + Export all Yammer users and compare to MSO users. + + #> + Param( + [switch]$UseExistingMsoConnection, + + [Parameter(Mandatory = $true)] + [pscredential]$Credential + ) + + Write-Progress -Id 1 -Activity "Getting Yammer users..." + + # Export users from Yammer + $startDate = (Get-Date).AddYears(-20) + $exportPath = "$env:TEMP\export.zip" + Export-YammerData -Model User -Path $exportPath -StartDate $startDate + Expand-Archive -Path $exportPath -DestinationPath "$env:TEMP\export" + Remove-Item -Path $exportPath + $users = Import-Csv -Path "$env:TEMP\export\Users.csv" + Remove-Item -Path "$env:TEMP\export" -Recurse + $yammerUsers = $users | Where-Object -Property state -eq "active" + + if (-not $UseExistingMsoConnection) { + Connect-MsolService -Credential $Credential + } + + Write-Progress -Id 1 -Activity "Getting MSO users..." + + $msoUsers = Get-MsolUser -All | Select-Object UserPrincipalName, ProxyAddresses, ObjectId, IsLicensed + + $userCounter = 0 + $userCount = $msoUsers.Count + $o365usershash = @{} + + foreach ($msoUser in $msoUsers) { + $upn = $msoUser.UserPrincipalName + + $GroupProgressParams = @{ + Id = 1 + Activity = "Processing user $userCounter of $userCount" + Status = "Processing MSO users..." + CurrentOperation = $upn + PercentComplete = ($userCounter / $userCount) * 100 + } + Write-Progress @GroupProgressParams + + $o365usershash.Add($upn, $msoUser) + + $msoUser.ProxyAddresses | ForEach-Object { + $email = ($msoUser -Replace "SMTP:(\\*)*", "").Trim() + + if (-not $o365usershash.Contains($email)) { + $o365usershash.Add($email, $msoUser) + } + } + + $userCounter++ + } + + $userCounter = 0 + $userCount = $yammerUsers.Count + + $yammerUsers | ForEach-Object { + $email = $_.email + + $GroupProgressParams = @{ + Id = 1 + Activity = "Updating user $userCounter of $userCount" + Status = "Updating Yammer users..." + CurrentOperation = $email + PercentComplete = ($userCounter / $userCount) * 100 + } + Write-Progress @GroupProgressParams + + $enabledInAD = Get-ADUser -Filter { UserPrincipalName -eq $email } -Properties Enabled | + Select-Object -ExpandProperty Enabled + $_ | Add-Member -MemberType NoteProperty -Name "ad_enabled" -Value $enabledInAD + + $o365user = $o365usershash[$email] + $existsInAzure = ($o365user -ne $null) + $_ | Add-Member -MemberType NoteProperty -Name "azure_exists" -Value $existsInAzure + + if ($existsInAzure) { + $_ | Add-Member -MemberType NoteProperty -Name "azure_object_id" -Value $o365user.ObjectId + $_ | Add-Member -MemberType NoteProperty -Name "azure_licensed" -Value $o365user.IsLicensed + } + + Write-Output $_ + + $userCounter++ + } +} + +function Get-YammerUser { + <# + .SYNOPSIS + Get Yammer user(s). + + #> + [CmdletBinding(DefaultParameterSetName = "All")] + Param( + [Parameter( + Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + ParameterSetName = "Id" + )] + [int]$Id, + + [Parameter( + Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + ParameterSetName = "UserPrincipalName" + )] + [string]$UserPrincipalName, + + [Parameter( + Mandatory = $true, + ParameterSetName = "Current" + )] + [switch]$Current, + + [Parameter( + Mandatory = $true, + ParameterSetName = "All" + )] + [switch]$All + ) + + begin { + function Get-YammerAllUsers { + [CmdletBinding()] + param( + [int]$Page = 1, + + [System.Collections.ArrayList]$UserList = (New-Object System.Collections.ArrayList($null)) + ) + + try { + $uri = "https://www.yammer.com/api/v1/users.json?page=$Page" + $authHeader = Get-YammerAuthHeader + $response = Invoke-RestMethod -Uri $uri -Method Get -Headers $authHeader + } catch { + throw $_ + exit + } + + if ($response.Count -ne 0) { + $UserList.AddRange($response) + + if ($UserList.Count % 50 -eq 0) { + return Get-YammerAllUsers -Page ($Page + 1) -UserList $UserList + } + } + + return $UserList + } + } + + process { + if ($PSCmdlet.ParameterSetName -ne "All") { + $uri = "https://www.yammer.com/api/v1/users" + + $uri += switch ($PSCmdlet.ParameterSetName) { + Id { + "/$Id.json" + } + UserPrincipalName { + "/by_email.json?email=$UserPrincipalName" + } + Current { + "/current.json" + } + } + + $authHeader = Get-YammerAuthHeader + Invoke-RestMethod -Uri $uri -Method Get -Headers $authHeader + } else { + Get-YammerAllUsers + } + } +} + +function Remove-YammerUser { + <# + .SYNOPSIS + Suspend or permanently delete Yammer user(s) by ID. + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param( + [Parameter( + Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true + )] + [int[]]$Id, + + [Parameter(Mandatory = $false)] + [switch]$TrueDelete + ) + + begin { + $Uri = "https://www.yammer.com/api/v1/users/$Id" + + if ($TrueDelete) { + $Uri += "&delete=TRUE" + } + + $action = if ($TrueDelete) { + "Permanently delete" + } else { + "Suspend" + } + $authHeader = Get-YammerAuthHeader + } + + process { + foreach ($user in $Id) { + if ($pscmdlet.ShouldProcess($user, "$action Yammer user")) { + Invoke-RestMethod -Uri $Uri -Method Delete -Headers $authHeader + } + } + } +} + +function Update-YammerUser { + <# + .SYNOPSIS + Set properties of Yammer user. + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param( + [Parameter( + Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true + )] + [int]$Id, + + [string]$JobTitle + ) + + $uri = "https://www.yammer.com/api/v1/users/$Id.json" + + $requestBody = @{} + + if ($PSBoundParameters.ContainsKey("JobTitle")) { + $requestBody.Add("job_title", $JobTitle) + } + + if ($pscmdlet.ShouldProcess($Id, "Update Yammer user")) { + $authHeader = Get-YammerAuthHeader + $jsonRequestBody = $requestBody | ConvertTo-Json + Invoke-RestMethod -Uri $uri -Body $jsonRequestBody -Method Put -Headers $authHeader -ContentType "application/json" + } +} + +function Get-YammerGroup { + <# + .SYNOPSIS + Get Yammer group(s). + + #> + [CmdletBinding(DefaultParameterSetName = "All")] + Param( + [Parameter( + Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + ParameterSetName = "Id" + )] + [int]$Id, + + [Parameter( + Mandatory = $true, + ParameterSetName = "All" + )] + [switch]$All + ) + + begin { + function Get-YammerAllGroups { + [CmdletBinding()] + param( + [int]$Page = 1, + + [System.Collections.ArrayList]$GroupList = (New-Object System.Collections.ArrayList($null)) + ) + + try { + $uri = "https://www.yammer.com/api/v1/groups.json?page=$Page" + $authHeader = Get-YammerAuthHeader + $response = Invoke-RestMethod -Uri $uri -Method Get -Headers $authHeader + } catch { + throw $_ + exit + } + + if ($response.Count -ne 0) { + $GroupList.AddRange($response) + + if ($GroupList.Count % 50 -eq 0) { + return Get-YammerAllGroups -Page ($Page + 1) -GroupList $GroupList + } + } + + return $GroupList + } + } + + process { + if ($PSCmdlet.ParameterSetName -ne "All") { + $uri = "https://www.yammer.com/api/v1/groups/$Id.json" + + $authHeader = Get-YammerAuthHeader + Invoke-RestMethod -Uri $uri -Method Get -Headers $authHeader + } else { + Get-YammerAllGroups + } + } +} + +function Get-YammerGroupMember { + <# + .SYNOPSIS + Get all members of a Yammer group specified by AD. + + #> + [CmdletBinding()] + Param( + [Parameter( + Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true + )] + [int]$Id + ) + + begin { + function Get-YammerAllMembers { + [CmdletBinding()] + param( + [int]$Id, + + [int]$Page = 1, + + [System.Collections.ArrayList]$MemberList = (New-Object System.Collections.ArrayList($null)) + ) + + try { + # $uri = "https://www.yammer.com/api/v1/users/in_group/$Id.json?page=$Page" + # We're using the undocumented endpoint below because it supports the is_admin property + $uri = "https://www.yammer.com/api/v1/groups/$Id/members.json?page=$Page" + $authHeader = Get-YammerAuthHeader + $response = Invoke-RestMethod -Uri $uri -Method Get -Headers $authHeader + $users = if ($response.users) { + $response.users + } else { + $null + } + } catch { + throw $_ + exit + } + + if ($users.Count -ne 0) { + $MemberList.AddRange($users) + + if ($MemberList.Count % 50 -eq 0) { + return Get-YammerAllMembers -Id $Id -Page ($Page + 1) -MemberList $MemberList + } + } + + return $MemberList + } + } + + process { + Get-YammerAllMembers -Id $Id + } +} + +function Send-YammerMessage { + <# + .SYNOPSIS + Post message to Yammer user, group, or reply. + + #> + [CmdletBinding( + SupportsShouldProcess = $true, + DefaultParameterSetName = "User" + )] + Param( + [Parameter( + Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + ParameterSetName = "User" + )] + [int[]]$Id, + + [Parameter( + Mandatory = $true, + ParameterSetName = "Group" + )] + [int]$GroupId, + + [Parameter( + Mandatory = $true, + ParameterSetName = "Reply" + )] + [int]$ReplyId, + + [Parameter(Mandatory = $true)] + [string]$Message + ) + + $uri = "https://www.yammer.com/api/v1/messages.json" + + $requestBody = @{} + + switch ($PSCmdlet.ParameterSetName) { + User { + $requestBody.Add("direct_to_user_ids", $Id) + } + Group { + $requestBody.Add("group_id", $GroupId) + } + Reply { + $requestBody.Add("replied_to_id", $ReplyId) + } + } + + $requestBody.Add("body", $Message) + + $jsonBody = $requestBody | ConvertTo-Json + + if ($pscmdlet.ShouldProcess($jsonBody, "Send Yammer message")) { + $authHeader = Get-YammerAuthHeader + Invoke-RestMethod -Uri $uri -Body $jsonBody -Method Post -Headers $authHeader -ContentType "application/json" + } +} + +Export-ModuleMember -Function * \ No newline at end of file diff --git a/Zoom/README.md b/Zoom/README.md new file mode 100644 index 0000000..8294f5d --- /dev/null +++ b/Zoom/README.md @@ -0,0 +1 @@ +I've migrated the Zoom module and scripts to their own repo for better visibility. I've also updated the repo to support their new auth and much more functionality. Check it out over at https://github.com/nickrod518/Zoom-PowerShell